Skip to content

Conversation

@TooTallNate
Copy link
Member

@TooTallNate TooTallNate commented Jan 27, 2026

Fixed class serialization/deserialization to properly respect VM context boundaries, ensuring workflow code only accesses classes registered in its own global context.

What changed?

  • Modified getSerializationClass() to no longer fall back to checking globalThis when a class isn't found in the provided global context
  • Updated the serialization tests to properly simulate how classes are registered and accessed in both host and VM contexts
  • Added VM-specific class registration in tests to better match production behavior where workflow code runs in isolation

How to test?

  • Run the updated serialization tests which now properly simulate the VM boundary
  • Verify that class serialization/deserialization works correctly when classes are registered in their respective contexts
  • Confirm that classes registered in one context aren't accessible from another context

Why make this change?

This change ensures proper isolation between the host environment and workflow VM contexts, matching production serverless behavior where workflow code runs in isolation. Previously, the code could leak class registrations between contexts, potentially causing unexpected behavior or security issues. This fix maintains proper boundaries between execution contexts.

@changeset-bot
Copy link

changeset-bot bot commented Jan 27, 2026

🦋 Changeset detected

Latest commit: 55dc421

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
Name Type
@workflow/core Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/docs-typecheck Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/web-shared Patch
workflow Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/world-testing Patch
@workflow/nuxt Patch
@workflow/example-nest Patch
@workflow/ai Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Contributor

github-actions bot commented Jan 27, 2026

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 457 0 38 495
✅ 💻 Local Development 418 0 32 450
✅ 📦 Local Production 418 0 32 450
✅ 🐘 Local Postgres 418 0 32 450
✅ 🪟 Windows 45 0 0 45
❌ 🌍 Community Worlds 30 162 0 192
✅ 📋 Other 123 0 12 135
Total 1909 162 146 2217

❌ Failed Tests

🌍 Community Worlds (162 failed)

mongodb (40 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

redis (40 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

starter (41 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (CLI) - workflow health command reports healthy endpoints
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

turso (41 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • readableStreamWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • outputStreamWorkflow
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (CLI) - workflow health command reports healthy endpoints
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 41 0 4
✅ example 41 0 4
✅ express 41 0 4
✅ fastify 41 0 4
✅ hono 41 0 4
✅ nextjs-turbopack 44 0 1
✅ nextjs-webpack 44 0 1
✅ nitro 41 0 4
✅ nuxt 41 0 4
✅ sveltekit 41 0 4
✅ vite 41 0 4
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 41 0 4
✅ express-stable 41 0 4
✅ fastify-stable 41 0 4
✅ hono-stable 41 0 4
✅ nextjs-turbopack-stable 45 0 0
✅ nextjs-webpack-stable 45 0 0
✅ nitro-stable 41 0 4
✅ nuxt-stable 41 0 4
✅ sveltekit-stable 41 0 4
✅ vite-stable 41 0 4
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 41 0 4
✅ express-stable 41 0 4
✅ fastify-stable 41 0 4
✅ hono-stable 41 0 4
✅ nextjs-turbopack-stable 45 0 0
✅ nextjs-webpack-stable 45 0 0
✅ nitro-stable 41 0 4
✅ nuxt-stable 41 0 4
✅ sveltekit-stable 41 0 4
✅ vite-stable 41 0 4
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 41 0 4
✅ express-stable 41 0 4
✅ fastify-stable 41 0 4
✅ hono-stable 41 0 4
✅ nextjs-turbopack-stable 45 0 0
✅ nextjs-webpack-stable 45 0 0
✅ nitro-stable 41 0 4
✅ nuxt-stable 41 0 4
✅ sveltekit-stable 41 0 4
✅ vite-stable 41 0 4
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 45 0 0
❌ 🌍 Community Worlds
App Passed Failed Skipped
✅ mongodb-dev 3 0 0
❌ mongodb 5 40 0
✅ redis-dev 3 0 0
❌ redis 5 40 0
✅ starter-dev 3 0 0
❌ starter 4 41 0
✅ turso-dev 3 0 0
❌ turso 4 41 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 41 0 4
✅ e2e-local-postgres-nest-stable 41 0 4
✅ e2e-local-prod-nest-stable 41 0 4

📋 View full workflow run

@vercel
Copy link
Contributor

vercel bot commented Jan 27, 2026

Copy link
Member Author

TooTallNate commented Jan 27, 2026

@TooTallNate TooTallNate marked this pull request as ready for review January 30, 2026 09:06
Copilot AI review requested due to automatic review settings January 30, 2026 09:06
@TooTallNate TooTallNate force-pushed the 01-27-ensure_class_serialization___deserialization_only_happens_in_the_proper_global_context branch from 80a9195 to b0a69ea Compare January 30, 2026 09:13
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR tightens class serialization/deserialization so that it is always scoped to the appropriate global object, preventing leakage of class registrations between the host environment and workflow VM contexts.

Changes:

  • Updated getCommonRevivers to pass the active global object into getSerializationClass, ensuring deserialization uses the correct registry for the current VM/host context.
  • Simplified getSerializationClass to look up classes only on the provided global’s registry (removing the globalThis fallback) and expanded custom class serialization tests to more accurately simulate VM vs host behavior.
  • Added a changeset entry for @workflow/core documenting the behavior change as a patch.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
packages/core/src/serialization.ts Ensures class and instance revivers use the provided global when resolving class constructors, aligning deserialization with the current VM/host context.
packages/core/src/class-serialization.ts Removes cross-context fallback in getSerializationClass and requires an explicit global, so class lookup is strictly scoped to a given global registry.
packages/core/src/serialization.test.ts Adjusts custom class serialization tests to define/register classes separately in host and VM contexts and to assert across-context instance behavior using constructor names.
.changeset/olive-cars-run.md Records a patch release note describing the tightened global-context scoping for class serialization/deserialization.
Comments suppressed due to low confidence (1)

packages/core/src/serialization.test.ts:1100

  • These updated custom class serialization tests exercise the positive path for VM/host isolation, but there is no test that verifies the new behavior where a class registered only on the host is not deserializable inside the VM (i.e., that the old globalThis fallback is truly gone). To fully cover the new semantics described in the PR (that classes registered in one context are not accessible from another), consider adding a test in this block that registers a class only on the host, hydrates using vmGlobalThis, and asserts that deserialization fails with the expected "Class "..." not found" error.
          101,
          97,
          109,
          34,
          44,
          49,
          51,
          93,
          44,
          123,
          34,
          110,
          97,
          109,
          101,
          34,
          58,
          49,
          52,
          44,
          34,
          116,
          121,
          112,
          101,
          34,
          58,
          49,
          53,
          125,
          44,
          34,
          115,
          116,
          114,
          109,
          95,
          48,
          49,
          65,
          82,
          90,
          51,
          78,
          68,
          69,
          75,
          84,
          83,
          86,
          52,
          82,
          82,
          70,
          70,
          81,
          54,
          57,
          71,
          53,
          70,
          65,
          49,

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

export function getSerializationClass(
classId: string,
global: Record<string, any> = globalThis
global: Record<string, any>
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing getSerializationClass to require a non-optional global parameter removes the previous default of globalThis and is a breaking API change for any external callers that relied on calling this exported helper with just classId. To preserve backward compatibility (especially since this is released as a patch), consider keeping global optional with a default of globalThis while still dropping the cross-context fallback so that existing call sites outside this file continue to compile and behave as before in the host context.

Suggested change
global: Record<string, any>
global: Record<string, any> = globalThis

Copilot uses AI. Check for mistakes.
@TooTallNate TooTallNate force-pushed the 01-27-fix_class_id_generation_when_class_is_bound_to_a_variable branch from 80bfb95 to 1973bae Compare January 30, 2026 20:33
@TooTallNate TooTallNate force-pushed the 01-27-ensure_class_serialization___deserialization_only_happens_in_the_proper_global_context branch from b0a69ea to 55dc421 Compare January 30, 2026 20:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants