feat: decrypt sealed payloads in the dashboard and CLI#3097
feat: decrypt sealed payloads in the dashboard and CLI#3097TooTallNate wants to merge 2 commits into
Conversation
🦋 Changeset detectedLatest commit: 24ed7ff The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
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 |
🧪 E2E Test Results❌ Some tests failed ❌ Failed E2E Tests▲ Vercel Production (389 failed)astro (27 failed):
example (60 failed):
express (53 failed):
fastify (26 failed):
hono (21 failed):
nextjs-turbopack (28 failed):
nextjs-webpack (25 failed):
nitro (38 failed):
nuxt (47 failed):
sveltekit (26 failed):
vite (38 failed):
💻 Local Development (1 failed)nuxt-stable (1 failed):
📦 Local Production (1 failed)nuxt-stable (1 failed):
📋 Other (57 failed)e2e-vercel-prod-nest (27 failed):
e2e-vercel-prod-tanstack-start (30 failed):
E2E Test SummarySummary
Details by Category❌ ▲ Vercel Production
❌ 💻 Local Development
❌ 📦 Local Production
✅ 🐘 Local Postgres
✅ 🪟 Windows
❌ 📋 Other
✅ vercel-multi-region
|
There was a problem hiding this comment.
Pull request overview
Updates observability decryption paths (dashboard + CLI) to correctly open sealed (encp) envelopes by deriving the run’s full payload-key capability and delegating decryption to the envelope layer (prefix-dispatch), avoiding the prior AES-GCM-only behavior.
Changes:
- Web dashboard stream reader now decrypts via
decryptEnvelopeand derivesRunPayloadKeys(not just a symmetric key) so sealed frames can be opened. - Web-shared hydration derives full run payload keys and uses envelope-aware
hydrateDataWithKey. - Core
hydrateDataWithKeynow decrypts via the envelope decryptor (prefix dispatch), with new tests covering real sealed payload hydration. - CLI
--decryptpaths derive full run payload keys for both field hydration and stream output. - Adds a changeset bumping
@workflow/core,@workflow/web-shared, and@workflow/cli.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/web/app/lib/hooks/use-stream-reader.ts | Switches stream frame decryption to envelope-aware decrypt and derives full run payload keys for sealed frames. |
| packages/web-shared/src/lib/hydration.ts | Derives full run payload keys in async hydration so sealed payloads can be decrypted for UI display. |
| packages/core/src/serialization.ts | Re-exports deriveRunPayloadKeys from the envelope layer for Node/CLI consumers. |
| packages/core/src/serialization-format.ts | Adds browser-safe re-exports of envelope/key helpers; updates hydrateDataWithKey to use envelope decryption. |
| packages/core/src/serialization-format.test.ts | Adds tests ensuring o11y hydration can open real sealed payloads and still opens symmetric payloads. |
| packages/cli/src/lib/inspect/output.ts | Updates stream output decryption key resolution to derive full run payload keys. |
| packages/cli/src/lib/inspect/hydration.ts | Updates --decrypt field hydration to derive full run payload keys for sealed payload support. |
| .changeset/encp-o11y-decrypt.md | Changeset for the new o11y sealed-payload decryption support. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
70bd706 to
54a585a
Compare
eae23f8 to
2d856fb
Compare
Without this, any payload another run sealed to this one renders as a lock icon with no way to open it — a visible regression for anyone debugging a run that received a cross-deployment hook resumption. The user is entitled to read the data and has already supplied the key; only the plumbing was missing. `hydrateDataWithKey` now delegates to the envelope layer, which dispatches on the format prefix, instead of unconditionally running AES-GCM. All four o11y key-resolution sites (web-shared hydration, the web stream reader, and both CLI `--decrypt` paths) resolve the full capability rather than just the symmetric key. Each already had the raw 32 bytes in hand, so this costs one extra derivation and no additional requests. A caller that supplies only a symmetric key still gets the ciphertext placeholder for sealed payloads rather than a decryption error, since that key never could have opened them. **Browser bundling.** The obvious import for the new helper is `@workflow/core/serialization`, but that module graph reaches `node:util` and `node:async_hooks` and cannot be bundled for the browser — which is what `@workflow/core/serialization-format` exists to avoid. The key helpers are re-exported from that browser-safe entrypoint instead, and the two browser consumers import from there; the CLI keeps the direct import since it runs on Node. Verified by walking the built import graph: the entrypoint reaches 6 modules and zero Node built-ins. Unrelated: `pnpm --filter @workflow/web build` currently fails on `main` too (`reducers/common.js` importing `node:util`). Turbo caching had been hiding it; touching core caused a cache miss that surfaced it. Not addressed here.
- `hydrateDataWithKey` accepted `PayloadKey`, which includes `SealTarget`. A seal target holds only a public key, so it can open neither scheme — passing one compiled fine and then always failed at runtime. Added a `DecryptionKey` alias (`CryptoKey | RunPayloadKeys`) and narrowed the signature, so that misuse is now a compile error. A `@ts-expect-error` test pins the guarantee. - `hydrateResourceIOAsync` dynamically imported `@workflow/core/serialization-format` twice. Destructure both bindings from the single existing import instead.
8ba434a to
136f3a3
Compare
24ed7ff to
560c5f8
Compare
karthikscale3
left a comment
There was a problem hiding this comment.
ai review: Reviewed the sealed observability read path across core, CLI, web-shared, and the dashboard stream reader. All four key-resolution sites now derive the full RunPayloadKeys capability, envelope dispatch handles both encr and encp, the browser-facing entrypoint avoids static Node built-ins, and decrypt-side typing excludes write-only SealTarget values. The two existing inline findings are fixed and resolved. This PR also addresses the already-flagged CLI regression on #3096.
PR 5 of 7. Stacked on #3096; review the stack in order, starting from #3093.
encpsealed-box encryption primitive #3093 — theencpsealed-box primitiveresumeHook()seals instead of fetching the key ← the payoffstart()gets the key from the probe it already awaitsWhy this is required, not optional
Once #3096 lands, a hook resumption from another deployment writes a sealed payload into the run's event log. Observability tooling can't open it:
hydrateDataWithKeyonly ever ran AES-GCM. So the dashboard and CLI would render a 🔒 placeholder on data the user is entitled to read and has already supplied the key for — a visible debugging regression on exactly the runs this work speeds up.What changed
hydrateDataWithKeydelegates to the envelope layer, which dispatches on the 4-byte format prefix, instead of unconditionally decrypting with AES-GCM.All four o11y key-resolution sites now resolve the run's full key capability rather than just its symmetric key:
@workflow/web-sharedhydrateResourceIOAsync@workflow/webuse-stream-reader(hand-rolled frame decode)@workflow/cli--decryptfield hydration@workflow/cli--decryptstream outputEach already had the raw 32 bytes in hand, so this costs one extra key derivation and no additional requests or round trips.
A caller that supplies only a symmetric key still gets the ciphertext placeholder for sealed payloads rather than a decryption error — that key never could have opened them, so surfacing a failure would be misleading.
Browser bundling
The natural import for the new helper is
@workflow/core/serialization, but that module graph reachesnode:utilandnode:async_hooksand cannot be bundled for the browser — which is precisely what the@workflow/core/serialization-formatentrypoint exists to avoid. I hit this as a build failure and moved the key helpers to a re-export from that browser-safe entrypoint; the two browser consumers import from there, while the CLI keeps the direct import since it runs on Node.Verified by walking the built import graph:
serialization-format.jsreaches 6 modules and zero Node built-ins.Tests
Two new cases assert the o11y path opens a real sealed payload with the derived keypair, and that the same resolved key still opens the run's own symmetric payloads — a run's event log mixes both.
Core suite 1598 passing, web-shared 160, CLI 78. Zero new lint diagnostics (diffed against a stashed baseline).