You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
carry the owning deployment ID on forwarded writable stream descriptors so a child on a newer deployment encrypts parent-stream chunks with the parent key
fall back to loading the owning run for descriptors already serialized by older SDK versions
add focused regression tests and document the expanded World key-resolution usage
Root Cause
Cross-run writable revivers only carried the parent runId and called getEncryptionKeyForRun(targetRunId). In @workflow/world-vercel, a string lookup without deployment context resolves against the executing deployment, so a child started on a newer deployment encrypted new chunks using its key while the parent stream is read using the older run key.
Context
This upstreams the behavior temporarily patched downstream in https://github.com/vercel/ash/pull/852, while avoiding its extra owner-run lookup for newly serialized descriptors. Legacy or in-flight descriptors retain a correctness fallback.
Validation
pnpm turbo build --filter=@workflow/core...
pnpm --filter @workflow/core typecheck
pnpm --filter @workflow/core exec vitest run src/serialization.test.ts src/step/writable-stream.test.ts
The reason will be displayed to describe this comment to others. Learn more.
Approve — solid fix, with one small consistency concern worth thinking about
The cross-deployment encryption bug is real and the fix is well-targeted: add deploymentId to the writable stream descriptor wire format, stamp it via STREAM_SERVER_DEPLOYMENT_ID_SYMBOL on writables that have it, and use it in getEncryptionKeyForRun(runId, { deploymentId }) instead of the assumed-current-deployment fallback. Legacy descriptors without a deploymentId fall back to loading the owning run via world.runs.get(runId) — that's correctness-preserving for in-flight serialized payloads.
Verified
After rebasing onto current main: 12 files, +278/-22 (the scary "removals" of experimentalSetAttributes from interfaces.ts and the encryption error handling in serialization.ts are stale-branch artifacts from being forked before #2134, #2157, and #2145 landed)
pnpm install --frozen-lockfile ✓
pnpm turbo run build --filter @workflow/core ✓
pnpm --filter @workflow/core test ✓ (1083/1083)
The two new serialization tests both pass:
Happy path — forwarded writable with deploymentId → uses getEncryptionKeyForRun(runId, { deploymentId }) and does NOT call runs.get
Legacy fallback — descriptor without deploymentId → calls runs.get(runId) first, then getEncryptionKeyForRun(run) ✓
CI noise
14 failures on the latest run, but they're all cascading from swc-plugin-workflow WASM build failing with undefined symbol: __emit_diagnostics. That's the bug fixed by #2174 (5dabbeeca fix(swc-plugin): allow wasm host imports during link) which landed on main after this branch was forked. A rebase would resolve them all.
One thing worth thinking about — step-handler.ts uses process.env.VERCEL_DEPLOYMENT_ID
The two workflowDeploymentId sites are:
Site
Source
runtime.ts (inline step execution)
workflowRun.deploymentId / bgRun.deploymentId — the workflow's ACTUAL deployment ✓
step-handler.ts (background step execution)
process.env.VERCEL_DEPLOYMENT_ID — the CURRENT runtime deployment
For background steps, these are only the same if the queue routes steps to the workflow's original deployment. If queues route steps to a NEWER deployment, process.env.VERCEL_DEPLOYMENT_ID is the newer one, not the workflow's.
Reading the world-vercel getEncryptionKeyForRun, the existing background-step encryption key resolution ALREADY assumes current-deployment (it calls memoizeEncryptionKey(world, workflowRunId) with no context, which then uses local HKDF when running inside Vercel). So this PR's choice in step-handler.ts is at least consistent with how step encryption already works — if the existing background-step encryption is correct (i.e., queue routes to original deployment, or some other invariant), then the new workflowDeploymentId source is also correct.
If the existing encryption has a latent bug for background steps running on newer deployments, this PR carries that forward — but doesn't make it worse, and the parent/child cross-deployment case (which is what the PR is actually trying to fix) IS handled correctly via the inline runtime.ts paths using workflowRun.deploymentId.
Worth confirming the invariant: do background step queues always route to the workflow's original deployment, or can they route to newer deployments? If the latter, there's a follow-up to also resolve the workflow's actual deployment in step-handler.ts (probably load the run first, similar to the legacy fallback in this PR's getForwardedWritableEncryptionKey).
Not a blocker — the PR's scope is the parent/child writable case, and that's handled correctly.
Bonus: the test design caught a subtle correctness point
The legacy-fallback test asserts the right behavior when descriptors are mid-flight from older SDK versions:
That confirms the fallback uses the full WorkflowRun overload (which extracts deploymentId from run.deploymentId), not the broken (runId, undefined-context) overload. Good test instinct.
PR status
Currently draft. Approving in spirit pending:
Rebase onto current main (resolves the stale-branch CI cascades + the deletions-in-diff confusion)
Optional: thread an explicit workflowDeploymentId through the background step path if cross-deployment step routing is actually possible
Rebased onto current main to pick up the SWC linker fix from #2174 (5dabbeeca). Focused validation on the new head passed locally:
pnpm --filter @workflow/swc-plugin build
pnpm turbo build --filter=@workflow/core...
pnpm --filter @workflow/core typecheck
pnpm --filter @workflow/core test (44 files / 1083 tests)
pnpm changeset status --since=origin-https/main
I left the existing V1 step-handler deployment behavior unchanged; this patch only carries the writable stream owner deployment through serialization so forwarded stream writes resolve their original key.
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR fixes encryption-key resolution for forwarded WritableStream handles when parent/child workflow runs execute on different Vercel deployments, by carrying the owning deployment ID through serialization and using it to resolve the correct per-run encryption key.
Changes:
Stamp forwarded WritableStream descriptors with an owning deploymentId and thread deployment context through step hydration/execution.
Update serialization reducers/revivers to persist and revive the owning deployment ID, and resolve the forwarded-stream encryption key with deployment context (with legacy fallback).
Add regression tests and a changeset; clarify World.getEncryptionKeyForRun documentation to cover forwarded-stream usage.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated no comments.
Show a summary per file
File
Description
packages/world/src/interfaces.ts
Expands getEncryptionKeyForRun docs to include forwarded-stream key resolution context.
packages/core/src/symbols.ts
Adds a new symbol to tag forwarded streams with owning deployment ID.
packages/core/src/step/context-storage.ts
Extends StepContext to carry workflowDeploymentId for forwarded streams.
packages/core/src/step/writable-stream.ts
Tags created writables with owning deployment ID when available.
packages/core/src/step/writable-stream.test.ts
Adds a test asserting the deployment tag is applied to step-level writables.
packages/core/src/serialization/types.ts
Extends serializable WritableStream descriptor shape with deploymentId.
packages/core/src/serialization.ts
Propagates deploymentId in reducers/revivers and resolves forwarded-stream encryption keys with deployment context + legacy fallback.
Passes deployment ID into step hydration and step context for stream forwarding.
packages/core/src/runtime/step-executor.ts
Threads workflowDeploymentId through step execution to hydration/context.
packages/core/src/runtime.ts
Populates workflowDeploymentId from the loaded run when executing steps.
.changeset/cross-deployment-stream-keys.md
Publishes patch changes for @workflow/core and @workflow/world.
Comments suppressed due to low confidence (1)
packages/core/src/serialization.ts:1363
The legacy (no deploymentId) path loads the full run via world.runs.get(runId) to resolve the encryption key. runs.get() defaults to resolveData: 'all', which can fetch large input/output/error blobs unnecessarily just to read deploymentId, increasing latency and memory usage when hydrating forwarded streams.
Consider fetching the owner run with resolveData: 'none' for this fallback lookup.
tagAbortPair(controller, value);
controller.abort(value.reason);
} else if (value.streamName) {
Backport to stable failed — the cherry-pick had conflicts that could not be resolved automatically (backport job run).
To resolve manually, push a backport branch and open a PR against stable (the workflow never pushes directly to stable). Note: this repository requires verified signatures on every branch, so your local commits must be signed (git config commit.gpgsign true with a configured GPG/SSH signing key, or git cherry-pick -S).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Root Cause
Cross-run writable revivers only carried the parent
runIdand calledgetEncryptionKeyForRun(targetRunId). In@workflow/world-vercel, a string lookup without deployment context resolves against the executing deployment, so a child started on a newer deployment encrypted new chunks using its key while the parent stream is read using the older run key.Context
This upstreams the behavior temporarily patched downstream in https://github.com/vercel/ash/pull/852, while avoiding its extra owner-run lookup for newly serialized descriptors. Legacy or in-flight descriptors retain a correctness fallback.
Validation
pnpm turbo build --filter=@workflow/core...pnpm --filter @workflow/core typecheckpnpm --filter @workflow/core exec vitest run src/serialization.test.ts src/step/writable-stream.test.tspnpm --filter @workflow/core testpnpm biome format <changed files>pnpm changeset status --since=main