mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
document-pg-world-auth
97 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fe2fd8c457 |
Classify Workflow stream failures (#3850)
## Summary & Motivation Stream infrastructure failures (HTTP/2 session wedges, transport timeouts, non-2xx stream responses) surfaced as plain `Error`, so terminal classification attributed them to customer code as `USER_ERROR`. They now carry a catchable `StreamError` with a `STREAM_ERROR` run error code, attributed to the SDK and retried when transport-level or 5xx. The v4 events response body is wrapped so a post-header stream failure is classified and reported to the dispatcher recycler — a response header arriving is not yet a successful streamed request. ## Test Plan Unit tests added across classification, serialization round-trip, the streamer, and the v4 transport; 331 `@workflow/core` and 123 `@workflow/world-vercel` focused tests pass. |
||
|
|
f9073d0739 |
Add attribute inspection to the CLI (#3950)
* Add attribute inspection to the CLI and probe the cancel window once
`wf inspect attributes` lists the distinct attribute keys on a project's
runs with their run counts and first/last seen times, and
`wf inspect runs --attribute key=value` filters by them. Between them
they turn attributes from something you can only write into something
you can discover and query. Both are analytics-only — storage has no
cross-run attribute index — so the listing says so rather than falling
back, and the filter warns and is ignored the way --since/--until
already do.
The flag is parsed and bounded in lib/inspect so the error names
--attribute rather than the parameter it becomes, and so it is testable
next to the other inspect flag helpers. It splits on the first `=` only,
since a value may contain one, and keeps an empty value, which matches
runs whose attribute was set to the empty string.
`wf cancel` also probed the plan's listing window inside its per-status
fan-out, so a four-status cancel issued four identical probes. The
window is a property of the plan rather than of a status, so the probe
is hoisted above the fan-out: eight requests become five. The harness
only ever modelled the storage path, so that probe logic had no
coverage; the new test fails with two probes before the change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Do not depend on an unreleased world export for the flag cap
The --attribute cap was imported from @workflow/world, where the
constant is added by a different branch, so on main it resolved to
undefined and `values.length > undefined` was always false: the flag
accepted any number of pairs and the test for it never threw.
Declare the cap in the CLI instead. The World and the backend enforce
the same bound independently, and this copy exists only so the error can
name the flag the user typed rather than the parameter it becomes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Degrade sleeps to the event log and bound the inspect flags
`wf inspect sleeps` was the only list path that could not degrade: it
branched on analytics being present and either returned or exited, so on
any backend providing analytics the storage branch below it was
unreachable and an analytics failure ended the command. It now warns and
falls through, like the run, step, and event listings. An argument the
World rejected is not retried — the same argument fails either path, so
falling back would trade a precise message for a slower failure.
handleApiError also only recognised errors carrying an HTTP status.
A client-side argument rejection has none, because no request was made,
so it fell past every branch and was rethrown as an unhandled error. It
is now reported as given: the message already names the method, the
parameter, and what it received.
--limit and --runId are checked before any backend setup so a mistyped
value names the flag and costs no round trip. The limit bound is
deliberately looser than the per-endpoint caps, which differ by resource
and stay with the World; this one catches a typo'd digit or a negative.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Scope --attribute to inspect and document the inspect flags
--attribute was added to the shared cliFlags, which cancel, health,
start, and web all spread — so `workflow health --attribute k=v` parsed
and was silently ignored. It belongs with the other inspect-only
filters in the command's own flags, next to --runId and --since.
The configuration reference documented every shared flag but none of
the inspect-only ones, so --runId, --stepId, --hookId, --since/--until,
--withData and --decrypt had no entries at all. They now do, in an
Inspect filtering section, alongside --attribute. --status and
--workflowName were documented under bulk cancel only; both also filter
inspect listings, which is now noted where they are.
--limit's entry described a default with no bound and is now rejected
outside 1 to 1000, so it says so, and points out that individual
listings cap lower.
The attributes guide claimed filtering was available "through the
Analytics API", which is no longer the whole story: the CLI can now
discover keys and filter by them, so that section splits into a CLI half
and an API half.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Stop dropping inspect flags silently
Three flags the caller typed were being discarded without saying so —
the same failure the World argument guards were added to remove,
reintroduced one layer up.
--attribute and --since/--until warned that the backend has no
analytics read path, but that condition is also false when --withData
asks for payloads, which only storage carries. Blaming the backend for
the caller's own flag sends them looking in the wrong place, so the
warning now names whichever applies.
inspect attributes dropped --sort entirely, explained only by a code
comment. It is forwarded now, and still left unset when absent so the
backend's alphabetical key order stands rather than the `desc` the
time-ordered listings impose.
A repeated --attribute key silently kept the last value, and a test
asserted that as if it were intended. Matching is per-key, so resolving
it means discarding a filter the caller typed: it is rejected instead.
The shared --limit entry also stated the 1-to-1000 bound that only
inspect enforces, which is wrong for cancel's own 1-to-500. The bound
moves to an inspect entry and the shared one points at both.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Reject --attribute on listings that cannot use it
Only the runs listing filters by attributes, but the flag was parsed for
every inspect resource: `inspect steps --attribute tenant=acme` returned
a normal, unfiltered step list with no warning, as did events, hooks,
attributes, and `inspect run <id>`, which already names one run. That is
the silent drop the preceding commit set out to remove, missed one layer
up in the command itself.
Validated alongside the other flag bounds, before any backend setup, so
a flag on the wrong subcommand costs no round trip.
Covered at the command level as well as in the unit, since the defect
was not in the validator but in nothing calling it: the tests drive
`Inspect.run` with a mocked setup module and assert the backend is never
reached. Five of them fail without this change.
Reported in review; verified against a real project rather than found
by the suite, which is why the command-level coverage goes in with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Resolve the test's oclif root without a URL pathname
`new URL('../..', import.meta.url).pathname` yields `/D:/a/...` on
Windows — a leading slash before the drive letter — so `Config.load`
could not find package.json and every command-level test failed there
while passing on Linux. `fileURLToPath` handles both.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Address review on the attribute flag
Attribute keys naming an Object.prototype member were rejected as
duplicates before anything was stored, because the duplicate check used
`in`, which walks the prototype. `--attribute toString=v` failed on
first sight, and `__proto__=v` would have set the prototype rather than
stored a value had it got that far. The map is null-prototype now and
the check uses Object.hasOwn.
--url and --web return before the filter is parsed, and neither
forwards it, so `inspect runs --attribute k=v --url` opened an
unfiltered view and a malformed pair skipped validation entirely. Both
are rejected: the dashboard takes no attribute filter.
--sort carried an oclif default of desc, so the "forward only when
asked" check in the attribute listing was always true and overrode the
backend's alphabetical key order. Every time-ordered listing already
falls back to desc itself, so the parser-level default is gone and the
flag now means what it says.
The docs claimed --since and --until must be supplied together, but the
CLI resolves the pair before the World sees it: --since alone is valid
and --until defaults to now. Only --until alone is rejected.
The vercel[bot] comment about ANALYTICS_MAX_ATTRIBUTE_FILTERS not being
exported was already addressed in
|
||
|
|
e1e64e3de3 |
docs: apply Vercel technical writing standards (#3704)
* docs: apply Vercel technical writing standards Audit the complete documentation corpus, package READMEs, skills, and source TSDoc/comments against the vercel-technical-writing skill and style-rules.md. Normalize sentence-case headings without changing published anchors, remove prose em dashes and filler wording, improve active voice and self-contained phrasing, standardize product/brand capitalization, American English, list punctuation, units, and code fence languages, and preserve exact runtime strings/table placeholders. All executable code is unchanged. Modified skills have their metadata versions bumped. * docs: extend writing audit to repository Markdown Apply the same technical-writing rules to design documents, compiler specifications, workbench guides, package changelogs, and the remaining tracked Markdown outside the deployed docs corpus. Preserve historical meaning, commands, output literals, table placeholders, and heading anchors. * docs: exclude generated package changelogs from audit |
||
|
|
2150798ca6 |
feat(cli): bulk-cancel runs in a single operation (#3348)
* feat(cli): bulk-cancel runs in a single operation Replace the per-run cancel loop in `workflow cancel` with one `cancelRuns` call, validate `--limit` (1-500), print a compact outcome summary with per-run lines for surfaced failures, and exit nonzero only when a run fails. The bulk logic lives in a dependency-injected `performBulkCancel` helper so it is unit-testable without an oclif harness. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(cli): address bulk cancel review feedback --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
25715d4521 |
[RFC] feat(nitro): embed observability dashboard in-process at /_workflow (#2548)
* feat(nitro): embed observability dashboard in-process at /_workflow Serve the @workflow/web observability UI inside the Nitro process at a configurable route (default /_workflow) instead of spawning a separate web server and 302-redirecting to it. Enabled in dev, omitted from production builds by default (so prod bundles carry no @workflow/web import). Never mounted on Vercel deploys (use the hosted dashboard). - @workflow/web: add a framework-neutral `@workflow/web/handler` (createWorkflowWebHandler) that serves SSR + static client assets + RPC as one Web Request->Response handler under a runtime basename (asset manifest URLs + publicPath are reprefixed so the dashboard is self-contained under its mount). Add `@workflow/web/registry` for embedded-dashboard discovery; make the RPC/stream client basename-aware. - @workflow/nitro: mount the handler in-process (Nitro v2 h3 + v3 native paths), gated by a new `dashboard` option (default = dev). - @workflow/cli: `workflow web` / `inspect --web` defer to a running embedded dashboard instead of starting a redundant server; pass `--standalone` to force the standalone UI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(nitro): normalize dashboard path once, use isNitroV2() helper Address review feedback on the embedded dashboard: - Normalize the dashboard mount path in one place before it feeds both the Nitro route registration (`[path, path + '/**']`) and the handler `basename`. Force a single leading slash, strip trailing slashes, and reject the root mount, so a custom `path` can't make the route and the handler's internal `normalizeBasename` disagree. - Replace the handler-level `!nitro.routing` v2 checks with the existing `isNitroV2()` helper for consistent v2/v3 detection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Peter Wielander <mittgfu@gmail.com> |
||
|
|
a09d00135b | Revert "Statically inject workflow world target" (#2752) (#3142) | ||
|
|
e8bc7d6aad |
feat: decrypt sealed payloads in the dashboard and CLI (#3146)
* feat: decrypt sealed payloads in the dashboard and CLI 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. * review: narrow the o11y decrypt key type and dedupe an import - `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. * review: record @workflow/web in the changeset This PR changes the dashboard's stream reader (`packages/web/app/lib/hooks/use-stream-reader.ts`) so it dispatches on the envelope format and can read sealed (`encp`) frames, but the changeset listed only core, web-shared and cli. `@workflow/web` is published, so without an entry the change would still ship — just as an incidental dependency bump, with nothing in that package's release notes explaining that sealed-stream decryption landed. |
||
|
|
62d570ed4b | Remove retired v1 step route plumbing (#3061) | ||
|
|
c31e30caac |
cli: show world-specific run fields in inspect output via World.describeRun (#2896)
* cli: show run region in inspect output via World.regionForRunId
Adds an optional reverse-lookup hook to the World interface —
regionForRunId(runId): string | null — so tooling can display a run's
region generically. Worlds without a regional dimension simply omit
the hook and no region output appears.
- @workflow/world: new optional interface member (documented: must not
throw; null = undeterminable)
- @workflow/world-vercel: implements it from the run-ID region tag
(tagged -> embedded region, untagged legacy -> default region,
malformed -> null)
- @workflow/cli: 'workflow inspect runs' gains a region column
(between workflowName and status) and 'workflow inspect run <id>'
a region property, in both table and JSON output — only when the
world defines the hook
* Generalize the inspect hook: World.describeRun display fields
Replaces regionForRunId on the World interface with describeRun, per
review: worlds may want to expose more than a region, and the
information need not be encoded in the run ID — describeRun receives
the run entity itself (loosely typed, mirroring createRunId), so a
world can derive fields from executionContext or any other property.
Each returned key becomes an inspect column/property; null values are
preserved in structured output ('applicable but undeterminable' vs.
the hook being absent entirely).
- world-vercel: describeRun returns { region } decoded from the run
ID tag (regionForRunId stays exported as a utility); entities
without a usable runId contribute nothing
- CLI listing: columns come from the union of keys the world returns
for the page, inserted before status; both analytics and storage
paths; detached call site binds this
- CLI showRun: merges the world fields into detail/JSON output via a
method-style call (preserves this), keeping nulls
- tests: field merging (multi-key), null preservation in JSON, hook
absent, and world-vercel describeRun coverage incl. no-runId
entities
* cli: evaluate describeRun defensively
Per review: the World interface says describeRun is pure and must not
throw, but it is an external extension point and the CLI should not
trust that. New safeWorldFields helper, used by both the listing and
showRun paths:
- a throwing implementation contributes no fields instead of crashing
the inspect command
- keys that already exist on the run row are dropped, so a world can
never overwrite canonical fields (status, runId, ...) in output
Tests: canonical fields survive a clobbering describeRun (extra keys
still merged); a throwing describeRun leaves rows untouched and the
command succeeds.
* Allow async describeRun implementations
Per review: widening a sync signature to async later would break every
consumer, while accepting sync-or-async from day one is free — sync
implementations (like world-vercel's) remain valid, and consumers
simply await, which handles both. The performance intent lives on as
documented guidance: the hook is called once per displayed run, so
implementations should stay cheap and avoid I/O; the CLI evaluates a
page's rows concurrently so an async world costs one await per page,
not per row. Promise rejections get the same treatment as throws:
no fields, never a crash.
* Update packages/world/src/interfaces.ts
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Nathan Rajlich <n@n8.io>
---------
Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
|
||
|
|
145835b647 |
Centralize workflow event semantics (#2790)
* Centralize workflow event semantics * Simplify centralized event helper usage * refactor: finish centralizing event semantics * refactor(world): derive Hook from its schema * fix(world): preserve event helper compatibility |
||
|
|
fe327e69e2 |
[world][web][cli] o11y: window-aware runs listing (#2812)
* web: infinite scroll for the runs table Replace Previous/Next cursor paging with front-style infinite scroll: a useInfiniteList hook accumulates cursor pages with per-run dedup and generation-guarded resets, and useLoadMoreOnScroll drives loadMore from an IntersectionObserver sentinel (400px prefetch margin, guarded against double-fetch, observed against the table's scroll container). Footer now shows the loaded count and the analytics lookback window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * web: back the runs infinite list with SWR so tab switches serve from cache Rewrite useInfiniteList on useSWRInfinite: pages are keyed by [cacheKey, cursor] in SWR's global cache, so unmount/remount (switching tabs) restores fetched pages instantly instead of refetching. Revalidation is conservative because analytics list queries are expensive: revalidateFirstPage and revalidateIfStale are off; freshness comes from the Refresh button and the visibility-change auto-reload, which map to reload() (reset to first page + revalidate). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * world: expose startTime/endTime on analytics runs listing The workflow-server /v2/analytics/runs endpoint has accepted a bounded startTime/endTime window since it shipped, and is significantly faster with one (the window prunes the ClickHouse scan: ~2s for 12h vs ~8s for the default 30-day entitlement window). The world client never exposed the params, so the CLI and web UI could only issue windowless requests. Pass them through so clients can send bounded windows (e.g. a period picker like front's workflows o11y). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * web: front-style period picker for the runs list Add a time-window picker (1h/6h/24h/3d/7d/30d, default 24h, URL-backed via ?period=) that sends an explicit startTime/endTime window through fetchRuns -> world.analytics.runs.list, keeping the ClickHouse scan bounded. The window is frozen per selection/refresh so all cursor pages share the same bounds, and it participates in the SWR cache key. Plan tiers are honored data-driven from the server's pageInfo: presets longer than the plan's observability lookback are disabled in the picker (labeled Observability Plus when an upgrade is available), and a 402 observability-upgrade-required response renders through the existing upgrade-required error handling. The footer now labels the selected window instead of the plan lookback. The runtime (local) fallback path ignores the window since the storage API has no time filter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * web: allow status filtering without a workflow name filter The status dropdown was disabled on Vercel backends until a workflow was selected — a limitation of the runtime DynamoDB API's index design. The runs list now reads via world.analytics, whose ClickHouse query filters derived status independently of workflowName, so drop the guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * cli: time-window flags for runs listing; widen name lookups past the default window The analytics backend now defaults windowless runs listings to the trailing 24h. Replicate the web's window support in the CLI: - 'workflow inspect runs' gains --since/--until (relative durations like 30m/12h/7d/2w, or timestamps) which are sent as an explicit startTime/endTime window. Out-of-plan windows surface through the existing observability-upgrade-required handling; non-analytics backends warn that the flags are ignored. - 'workflow start <name>' resolves the workflow's latest run via a windowless (default-window) listing and now retries across the plan's whole observability window on a miss, so names idle for more than a day keep resolving. - Bulk 'workflow cancel' matches across the plan window up front — a run can sleep or wait on a hook for days without recent events, so the default recent window must not bound cancellation matching. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: tighten changeset descriptions Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * web: persist frozen listing windows across remounts; minimize lockfile diff Address review findings: - The frozen startTime/endTime lived in component state, but RunsTable fully remounts on tab switches, so every remount minted a new SWR cache key — the cached-pages restore never hit and cache entries grew unboundedly (one per key, including every 5s local-backend poll tick). Move the frozen windows to a module-scope store keyed by period: a remount reuses the stored window (same cache key, instant restore), and the window only advances on explicit refresh/reload. Non-analytics backends now send no window at all (the runtime APIs ignore it anyway), which also hides the period picker and window label there. - Regenerate pnpm-lock.yaml from main so the diff contains only the swr addition (plus its own use-sync-external-store dependency), dropping the unrelated docs-importer radix-ui re-resolutions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
49a50e83d9 | Document configuration environment variables (v5) (#2468) | ||
|
|
54f46f976d |
cli: restore dynamic world loading for community backends (#2806)
* cli: restore dynamic world loading for community backends Since #2752 moved world selection to static injection, setupCliWorld only constructed the vercel, local, and postgres worlds explicitly and threw 'Unsupported workflow backend' for anything else — breaking community worlds (e.g. @workflow-worlds/turso) that previously loaded through the dynamic createWorld() in @workflow/core/runtime. Generalize the postgres-only dynamic path: any backend other than vercel/local is now resolved from the user's project directory and loaded via its createWorld() export, matching @workflow/web's world construction (#2804), with clear errors when the package is missing or does not export createWorld(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * cli: fall back to default.createWorld for CJS world packages Review feedback on #2804: import() of a require-resolved CJS entry relies on cjs-module-lexer to surface named exports; fall back to mod.default.createWorld when detection fails. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Update .changeset/cli-generic-world-backends.md Signed-off-by: Peter Wielander <mittgfu@gmail.com> --------- Signed-off-by: Peter Wielander <mittgfu@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Peter Wielander <mittgfu@gmail.com> |
||
|
|
17d4ce2253 |
cli: read list views from world.analytics when available (#2648)
* Add workflow analytics world APIs * cli: read list views from world.analytics when available inspect list views (runs, steps, events, hooks, sleeps) now read from the optional world.analytics namespace when the active backend provides one, falling back to the runtime storage APIs otherwise. Payload and detail views are unchanged. Deprecate --with-data for list views; payloads are viewable per-resource via 'inspect <resource> <id>'. * cli: keep hook listing on the runtime storage API The analytics read path omits ownerId (and the secret hook token), so routing hook listing through it silently drops the ownerId column. Keep inspect hooks on the runtime APIs, consistent with the web observability UI. Runs, steps, events, and sleeps continue to use the analytics read path when available. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Handle analytics access metadata in CLI * test(cli): preserve analytics pageInfo in json output * fix(cli): paginate analytics sleeps output * fix(cli): correct deprecation message flag name to --withData The list-view deprecation warning referenced '--with-data', but the actual oclif flag is '--withData' (with '-d' alias); '--with-data' errors with "Nonexistent flag". Fix the warning text, the doc comment, and the changeset to reference the real flag name. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): preserve inspect json array output * fix(cli): fall back when analytics lists are empty --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
0f557d5ae4 |
Statically inject workflow world target (#2752)
* Statically inject workflow world target * Fix static world injection in host bundles * Fix static world injection gaps * Fix Vite Nitro server startup * Fix Nitro pg-native aliasing * Fix static world target CI gaps * Fix static world dev rebuild gaps * Avoid broad runtime alias in Nitro * Refresh Next dev route for step HMR * Externalize Nest target world * Use canary HMR rediscovery timeout * Bundle local world in Nest builds * Dedupe world target helpers and fix SvelteKit chunk patch guard |
||
|
|
cb181392b9 |
feat(cli): print run deep links with --url, fix dashboard route (#2467)
Add a `--url` flag to `inspect`/`web` that prints a run's observability dashboard deep link to stdout and exits — no browser, no local server — so scripts and agents can share a link instead of opening a UI. Fix the Vercel dashboard URL to the current `…/workflows/runs/<id>?environment=<env>` route (drop the legacy `/observability` segment) and respect `--env`. Apply the same route fix to the e2e helpers, CI aggregation scripts, and the nextjs-turbopack workbench. Document deep-linking in the workflow skill and observability docs. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
5bf2c167a5 | Add serializable reviver compatibility check (#2250) | ||
|
|
5f22832675 |
Serialize run_failed/step_failed errors through serialization pipeline (#1851)
* Serialize run_failed/step_failed errors through serialization pipeline
Switch run_failed, step_failed, and step_retrying events to persist
the full thrown value via the workflow serialization pipeline (as
SerializedData / Uint8Array) instead of a lossy { message, stack, code }
StructuredError shape. Consumers hydrate via hydrateRunError /
hydrateStepError to reconstruct the original thrown value, preserving
Error subclass identity, cause chains, and custom properties.
- WorkflowRun.error and Step.error are now SerializedData
- WorkflowRun gains a top-level errorCode plaintext field
- WorkflowRunFailedError.cause is now the hydrated thrown value
- Adds world-postgres migration 0010_add_error_code.sql
- Legacy pre-pipeline errorJson records surface as undefined on read
* Update Next.js workbenches for new WorkflowRunFailedError.cause type
cause is now `unknown` (the hydrated thrown value) rather than
`Error & { code }`. Defensively extract Error-shaped fields when the
hydrated value is an Error, otherwise round-trip the raw value, and
expose the new `errorCode` classification field.
* Update docs for WorkflowRunFailedError.cause: unknown
The hydrated `cause` is now `unknown` (the original thrown value
through the serialization pipeline) and the error classification has
moved to the top-level `errorCode` property. Update the two affected
docs pages and the `TSDoc` interface to reflect the new shape, and
narrow `cause` with `instanceof Error` before accessing fields.
* Expand test coverage for the run/step error serialization pipeline
Unit tests:
- 19 new dehydrate/hydrate{Step,Run}Error round-trip tests covering
FatalError, plain Error, built-in Error subclasses, non-Error thrown
values (string, plain object), cause chains, encryption round-trip,
the binary format prefix contract, and the unserializable / unknown-
format error paths.
- 5 new tests for Run.returnValue when the run is failed: hydrated
FatalError + cause as cause, plain Error preservation, non-Error
thrown values surfaced verbatim, cross-class cause chains, and the
hydration-failure fallback that still surfaces errorCode.
E2E tests (new, in 99_e2e.ts + e2e.test.ts):
- Step throw → workflow catch round-trips a FatalError with a TypeError
cause chain, asserting class identity, fatal marker, and cause name +
message all survive the step_failed event pipeline.
- Workflow throw → run_failed reaches status with the new
top-level errorCode metadata exposed (cause-shape coverage lives at
the unit level, since the SWC plugin's class registration is not
invoked in the plain-Node e2e runner).
- Workflow throw of a non-Error value round-trips that value verbatim
as WorkflowRunFailedError.cause.
Adjustments to existing assertions:
- error.cause is now ; tests narrow with
and use the new top-level field instead of .
- step.error / run.error from CLI --withData are now hydrated payloads:
unregistered class instances surface as Instance refs whose
carries the original message + stack.
Observability hydration:
- hydrateStepIO / hydrateWorkflowIO in serialization-format.ts now
hydrate the field via hydrateData, so the CLI and web UI
continue to surface readable run/step error messages and stacks.
* Tighten error serialization changeset description
* Trim error serialization changeset to a single sentence
* Resolve FatalError/RetryableError revivers via cross-realm registry
When a workflow runs in a Node `vm` context, its bundled
`@workflow/errors` is a different module instance than the host's
import (separate prototype chains, separate class identity). Calling
`new FatalError(...)` from the host-side reviver produces a
host-realm instance that fails `err instanceof FatalError` checks
in the workflow code — even when the serialized payload was correctly
tagged via the dedicated `FatalError` reducer.
Surfaced by the local-prod e2e "step throw round-trips FatalError"
test on Next.js Turbopack: each route gets its own bundled chunk, so
the flow handler's `@workflow/errors` and the workflow VM bundle's
`@workflow/errors` are two distinct copies of the same module.
Fix:
- Each bundled copy of `@workflow/errors` self-registers its
`FatalError` and `RetryableError` classes on `globalThis` via
`Symbol.for("@workflow/errors//FatalError")` /
`Symbol.for("@workflow/errors//RetryableError")`. First load wins
per realm; the descriptor is non-writable / non-configurable to make
accidental clobbering loud.
- The revivers in `@workflow/core`'s common reducers module read the
consumer's `globalThis` (passed in as `global`) to pick up the
realm-local class, falling back to the host-imported class when no
registration is present (e.g. in the CLI / test runner).
* Use `types.isNativeError` to remap workflow stacks across VM realms
The runtime's run-failure path computes a source-map-remapped stack
and then assigns it back onto the thrown value via `if (err
instanceof Error) err.stack = errorStack`. Workflows run inside a
Node `vm` context, so a workflow-thrown error is an instance of the
VM realm's `Error` — `instanceof` against the host realm's
`Error` returns `false`, the assignment is skipped, and the
serialized `run_failed` event carries the un-remapped (bundled-line-
number) stack instead of the source-mapped one.
Switch the gate to `types.isNativeError`, which uses V8's internal
type tag and works across realms — same approach already in place
for the serialization reducers.
Caught by the local-prod e2e "nested function calls preserve message
and stack trace" and "cross-file imports preserve message and stack
trace" tests, which assert that the persisted run-error stack
contains `99_e2e.ts` / `helpers.ts`.
* Sync CLI revivers with core + add toJSON shim for Error subclasses
Two issues with the CLI's hand-rolled reviver list:
1. It hadn't been updated for the new first-class Error subclass
reducers (`TypeError`, `RangeError`, `FatalError`, `RetryableError`,
etc.). devalue throws "Unknown type X" when it encounters a
reduced value with no matching reviver, and `hydrateResourceIO`
swallows that error and surfaces the raw `Uint8Array` payload —
so `step.error` / `run.error` showed up as raw byte dumps in
`workflow inspect` output.
2. Even with all the right revivers, `Error.prototype`'s `message`
/ `stack` / `cause` are non-enumerable, so `JSON.stringify`
(used by `workflow inspect --json`) drops them — leaving the
subclass-specific enumerable fields (e.g. `FatalError.fatal`)
visible but the actual error data missing.
Fix:
- Build the CLI reviver set on top of `getCommonRevivers()` from
`@workflow/core` so the CLI stays in sync with the runtime's
reducer set automatically. New core reducers/revivers will Just
Work without any CLI-side change.
- Wrap each Error reviver from the common set with a thin shim that
attaches a non-enumerable `toJSON` method to the produced
`Error` instance. `JSON.stringify` calls `toJSON` and gets a
full object (`name` + `message` + `stack` + `cause` + any
enumerable subclass fields like `fatal` / `retryAfter` /
`errors`); `util.inspect` ignores `toJSON` and renders the
canonical `Error: msg\\n at ...` format. Best of both worlds for
CLI output without compromising the runtime hydration path.
Caught by the local-prod e2e "basic step error preserves" and
"cross-file step error preserves" tests, which read
`failedStep.error.message` / `.stack` from the CLI's JSON output.
* Clarify parseErrorJson JSDoc to match its always-null return
The previous JSDoc described preserving legacy values "for best-effort
hydration" which contradicted the implementation, where legacy errors
are intentionally surfaced as absent (the pre-pipeline shapes can't be
hydrated by the new error revivers). Rewrite the comment so the contract
matches behavior. Also rename the now-unused parameter to `_errorJson`
to reflect that the function ignores it.
Caught by a code review on #1851.
* Refine error-handler ergonomics on the step / run hot paths
Three review-driven adjustments that all touch the queue handlers and
their interaction with the error serialization pipeline:
1. Memoize the per-run encryption key fetch. The step handler used to
eagerly fetch + import the key at the top of every step delivery so
the value would be in scope for every potential dehydrateStepError
path. That pessimized step-started early-return cases (the fetch
happens unconditionally even when the step never reaches user code)
and required duplicating the same boilerplate at four call sites in
runtime.ts. Introduce `memoizeEncryptionKey(world, run)` in
runtime/helpers.ts that returns a lazy, single-fetch accessor;
step-handler / runtime call sites use `await getEncryptionKey()`
instead. The first caller pays the fetch cost, subsequent callers
await the cached promise, and steps that fail before any
encryption-aware work happens skip the fetch entirely.
2. Preserve the prior attempt's serialized error as the cause on the
defensive max-retries-exceeded `step_failed` re-invocation guard.
The existing comment explicitly opted out of cause attachment, but
the symmetric post-failure path below already does this and the
reviewer is right that consumers shouldn't have to walk the
step_retrying event history to recover the underlying error. Best-
effort: if hydration of the prior `step.error` throws, fall back
to a FatalError without cause rather than letting the event write
itself fail.
3. Document the intentional `unflatten` throw in
`hydrateStepError` / `hydrateRunError` for non-Uint8Array input.
SDK version is pinned per workflow run via skew protection so the
non-binary branch is dead in production; if a misshapen value
reaches it, surfacing the throw via the surrounding o11y try/catch
is more debuggable than masking it. Add a comment so future
reviewers don't reach for a defensive fallback.
A standalone `falls back to plaintext` suggestion on the run_failed
key fetch was rejected: when encryption is configured we should fail
loudly rather than silently emit plaintext error data. The queue's
redelivery semantics will retry the key fetch; persistent KMS outages
get logged with the existing "persistent error preventing the run from
being terminated" message rather than a security regression.
* Hydrate `event.eventData.error` in event listings
`hydrateEventData` enumerated the per-event fields that need
hydration (`result`, `input`, `output`, `metadata`, `payload`)
but omitted the new `error` field on `step_failed`,
`step_retrying`, and `run_failed` events. Without this branch,
o11y tools that list events (e.g. `workflow inspect events`) surface
the raw `Uint8Array` payload instead of a hydrated
`{ name, message, stack, … }` object even though the entity-level
`Run.error` / `Step.error` paths already hydrate.
Mirrors the existing per-field branches; the `try/catch` leaves the
field un-hydrated on parse failure rather than failing the whole
event view. Adds a unit test.
* Use `.is()` static checks in `classifyRunError` for cross-realm safety
Workflows execute inside a separate `vm` realm: the
`WorkflowRuntimeError` class bundled into the workflow code and the
host-imported one are distinct constructors, so an
`err instanceof WorkflowRuntimeError` check on a VM-thrown error
returns `false` and we'd misclassify genuine runtime errors (corrupted
event log, missing timestamps, workflow/step not registered) as user
errors.
Switch to each subclass's `.is()` static (a name-based duck check that
works across realms). Since `WorkflowRuntimeError.is` only matches its
own concrete name, enumerate every concrete subclass we want to
recognize (`StepNotRegisteredError`, `WorkflowNotRegisteredError`)
in a `RUNTIME_ERROR_CHECKS` table; keep that table in sync with the
class hierarchy in `@workflow/errors`.
Existing `classify-error.test.ts` already covers `WorkflowRuntimeError`
and `WorkflowNotRegisteredError` cases — both still pass.
* Add e2e coverage for step throws of non-Error values
We had `errorWorkflowThrowNonErrorValue` (workflow body throws a plain
object — round-trips verbatim as `WorkflowRunFailedError.cause`) but
no symmetric coverage for the step-throw side. Step-throw goes through
a different code path: non-Error values aren't recognized as
`FatalError` (no `name === 'FatalError'`) nor `RetryableError`,
so they take the transient retry path. After max retries the runtime
wraps the original thrown value as `cause` on a fresh `FatalError`
which the workflow's catch block then sees.
Add a workflow that throws a recognizable plain object from a step
with `maxRetries = 0` (so we exhaust on first attempt and avoid a
long test wait) and a workflow that asserts the wrapped FatalError
shape: `isFatal`, `instanceof FatalError`, message includes the
original object's serialized form, `cause` is the original non-Error
object verbatim with structure preserved.
Documents the current retry-then-wrap behavior so any future change
to "non-Error throws skip retries" semantics has to update the test.
* Note legacy postgres error-data loss in the run/step error changeset
Pre-upgrade failed runs that wrote into world-postgres's deprecated
`error` text column can't be hydrated through the new pipeline (the
shape is incompatible with the new revivers). The new runtime
intentionally surfaces them as `error: undefined` on read; the
original payload is still readable directly from the `errorJson`
column for manual inspection. Add a one-sentence note to the
changeset's migration text so consumers upgrading don't get blindsided
by suddenly-empty error fields on historical runs.
|
||
|
|
417c4930be |
refactor(swc-plugin): remove client transform mode, merge into step mode (#1686)
* refactor(swc-plugin): remove client transform mode, merge into step mode Remove the `client` transform mode from the SWC compiler plugin. The `client` and `step` modes were nearly identical — both preserved step function bodies, replaced workflow bodies with throw stubs, and emitted the same JSON manifest. Step mode now absorbs all client-mode behaviors: - Dead code elimination (previously only workflow + client) - Hoisted variable references for object property steps - All integrations use mode: 'step' instead of 'client' BREAKING CHANGE: The `client` value for the SWC plugin `mode` option is no longer accepted. Use `step` instead. * fix(nitro): force-inline workflow packages in dev mode for serde classId registration In dev mode, Nitro's Rollup externalizes npm packages like @workflow/core, so the SWC transform plugin never processes files like run.js. This means serde classes (e.g. Run) never get the classId registration IIFE, causing serialization failures when step functions return Run instances. Uses a Rollup resolveId hook to force workflow SDK packages to be bundled (non-external) while leaving all other dependencies external. This is more targeted than noExternals=true which bundles everything and causes TDZ errors from circular imports in packages like vue-bundle-renderer/h3. The Nitro module now also ignores .nitro/workflow/** in watchOptions so writing generated workflow bundles does not retrigger Nitro's own dev bundle rebuild loop. Also wraps dev:reload workflow rebuilds and makes LocalBuilder.build() atomic (writes to temp files, renames on success) to avoid partial output state during HMR. For Nuxt, also configures Vite's ssr.noExternal to bundle workflow packages in the SSR context. * fix(nitro,nuxt): address review feedback on dev-mode classId fix - nitro builders: use crypto.randomUUID() for temp file suffix instead of Date.now() to avoid collisions under rapid/concurrent build() calls, and serialize concurrent build() calls through an internal queue so two overlapping dev rebuilds cannot clobber each other's temp outputs. - nitro index: use fileURLToPath() to convert file:// URLs to filesystem paths, which correctly handles Windows paths (file:///C:/... -> C:\...) and percent-decoding, instead of relying on new URL(...).pathname. - nuxt module: normalize vite.ssr.noExternal to an array (preserving any existing string/RegExp/array entry) before appending workflow package matchers, so the force-bundle behavior is not a no-op when noExternal is already set to a non-array value. |
||
|
|
173756dc4d |
[docs] Rename workflowdevkit to workflowsdk and useworkflow.dev to workflow-sdk.dev (#1759)
* [docs] Rename workflowdevkit references to workflowsdk * [docs] Rename useworkflow.dev to workflow-sdk.dev * [chore] Add changeset for domain rename * [docs] Revert sitemap rewrite to useworkflow.dev (crawled-sitemap not yet available for new domain) |
||
|
|
eba7df381c |
[cli] Fix false "data expired" warning in CLI for non-expired runs (#1736)
* fix cli expiredAt check * fix cli expiredAt check * fix cli expiredAt check |
||
|
|
ac09f40771 |
feat: add clickable Run reference rendering in observability UI (#1681)
* feat: add clickable Run reference rendering in observability UI
When a serialized Run object appears in step input/output data, it is
now rendered as a clickable purple badge showing the runId. Clicking
navigates to the target run's detail page.
Changes:
- serialization-format.ts: Add RunRef type, isRunRef(), serializedRunToRunRef(),
and 'Run' entry in observabilityRevivers
- data-inspector.tsx: Add RunRefInline component (purple badge), RunClickContext,
collapseRefs() to make refs non-expandable in ObjectInspector
- attribute-panel.tsx: Thread onRunClick prop, wrap in RunClickContext.Provider
- entity-detail-panel.tsx: Thread onRunClick prop
- run-trace-view.tsx: Thread onRunClick prop
- workflow-trace-view.tsx: Thread onRunClick prop, reset selected span on run change
- trace-span-construction.ts: Show step name for builtin steps instead of empty string
- hydration.ts: Re-export RunRef types
- run-detail-view.tsx: Add handleRunRefClick that navigates to /run/{targetRunId}
* fix: guard collapseRefs against class instances and memoize result
Only recurse into plain objects (prototype is Object.prototype or null)
to avoid stripping class instances like Date, Error, Map, etc. that
have their own rendering in NodeRenderer. Also memoize the collapsed
result to avoid recomputing on every render.
* fix: detect Run instances in Instance reviver instead of fake Run serde key
The Run class goes through the standard Instance serialization pipeline
(WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE), not a dedicated 'Run' key.
Move the RunRef detection into serializedInstanceToRef() which checks
if the className is 'Run' and the data contains a runId string, then
returns a RunRef instead of a generic ClassInstanceRef.
* fix: use serializedInstanceToRef in web and CLI Instance revivers
Both the web and CLI hydration layers override the observabilityRevivers
Instance handler with their own implementation (for react-inspector
named constructors and CLI inspect.custom respectively), bypassing the
RunRef detection in serializedInstanceToRef. Fix by calling
serializedInstanceToRef first and returning a RunRef when detected.
|
||
|
|
873b4e2bb4 | [core] Refactor getWorld interface to be asynchronous (#942) | ||
|
|
66d49c0db6 | [world] Restructure stream interface, require run ID for all step and stream operations (#1293) | ||
|
|
e4362421ab | [builders] Switch Vercel Build Output API from CJS to ESM (#1562) | ||
|
|
a6bcea9d28 | [cli] [core] Probe deployment specVersion before CLI start (#1629) | ||
|
|
2680a427f0 | fix: add Request and Response revivers to web and CLI hydration (#1414) | ||
|
|
f5d2aef58f |
Add serde compliance tooling and improve custom class serialization DX (#1552)
* Add serde compliance tooling and update skill documentation - Add serde compliance checker library to @workflow/builders - Add build-time warnings for serde classes with Node.js imports in workflow bundle - Add 'workflow transform' CLI command for inspecting SWC output - Implement 'workflow validate' CLI command with serde compliance checks - Add serde analysis panel to SWC playground - Update workflow skill with custom class serialization documentation * Fix --json + --strict: use process.exitCode so JSON output is returned before exit * Address code review feedback - Fix --strict exit code: honor process.exitCode in BaseCommand.finally() - Remove unused --module-specifier flag from transform command - Add missing Node.js builtins (e.g. test) to playground detection list - De-dupe build-time serde warnings by grouping identical issues across classes - Make Serde Analysis panel collapsible like the output panels * Use .gitignore for validate file discovery; fix changeset wording |
||
|
|
5837d577c2 | [cli] [world-local] Ensure update checks don't suggest upgrading from stable release to pre-releases (#1490) | ||
|
|
0d72b2d363 | [cli] Add bulk cancel, --status filter, fix step JSON hydration (#1467) | ||
|
|
da6adf7798 | [o11y] Polish display when run data has expired (#1438) | ||
|
|
fdbe853531 | Fix CLI health check for Astro/Sveltekit and add debug http logs to world-vercel (#1442) | ||
|
|
3c3f80a1f0 |
fix(cli): remove short flag collision on -e in health command (#1343)
The health command's `endpoint` flag and the shared `env` flag both declared `char: 'e'`, causing ambiguity. Remove the short flag from `endpoint` so `-e` unambiguously maps to `--env`. |
||
|
|
9f3551caec |
Fix flaky Vercel prod e2e tests by skipping CLI update check (#1350)
The e2e tests spawn a CLI subprocess for every inspect/cancel/health call. Each subprocess performs an npm registry version check on startup, which can hang under load and exceed the 20s spawn timeout, causing SIGTERM. Add WORKFLOW_NO_UPDATE_CHECK=1 env var support to skip the check, and set it in the e2e test harness. |
||
|
|
d842ce1c43 |
fix: surface 429 rate-limit errors in e2e tests and CLI (#1309)
Multiple layered issues caused encryption key 429 errors to produce
confusing, unrelated-looking test failures:
- awaitCommand() in e2e utils now rejects on non-zero exit codes
instead of silently resolving (the most critical fix)
- cliInspectJson/cliHealthJson throw on empty stdout instead of
falling back to '{}'
- maybeDecryptFields re-throws HTTP errors instead of silently
falling back to encrypted placeholders
- fetchRunKey includes response body and status text in errors
- Added 429 to CLI status text map
|
||
|
|
887cc2bd55 | [web-shared] [cli] Refactor observability data fetching (#1261) | ||
|
|
83dbd46456 |
Stop reading WORKFLOW_VERCEL_* env vars at runtime to prevent unintended proxy routing (#1304)
* Stop reading WORKFLOW_VERCEL_* env vars at runtime to prevent unintended proxy routing createWorld() in core no longer reads WORKFLOW_VERCEL_PROJECT, WORKFLOW_VERCEL_TEAM, WORKFLOW_VERCEL_AUTH_TOKEN, WORKFLOW_VERCEL_ENV, or WORKFLOW_VERCEL_PROJECT_NAME from process.env. These env vars are intended for CLI/observability tooling only, and reading them at runtime caused all workflow traffic to route through the api.vercel.com proxy when users mistakenly set them as Vercel project env vars. The Vercel runtime already provides everything needed: OIDC tokens for auth, VERCEL_PROJECT_ID for encryption context, and VERCEL_DEPLOYMENT_ID for world selection. createWorld() now calls createVercelWorld() with no config. A warning is emitted if the env vars are detected at runtime, telling users to remove them. The CLI and e2e tests are updated to call createVercelWorld() directly with an explicit config object and inject via setWorld(), keeping the WORKFLOW_VERCEL_* env vars scoped to tooling contexts only. * Refactor inferVercelEnvVars to return config instead of relying on process.env inferVercelEnvVars() now returns a VercelEnvVars object with the resolved config values. setup.ts uses the returned object directly for createVercelWorld() instead of re-reading from process.env via getEnvVars(). The writeEnvVars() calls inside inferVercelEnvVars are consolidated into a single call at the end, retained only for the embedded web UI which reads process.env as a fallback in its server actions. The scattered writeEnvVars calls after each inference step are removed. * Address review: consistent e2e gate and expanded warning - Use WORKFLOW_VERCEL_ENV as the gate in both e2e.test.ts and bench.bench.ts for consistency (was WORKFLOW_VERCEL_AUTH_TOKEN in e2e.test.ts, WORKFLOW_VERCEL_ENV in bench.bench.ts) - Expand the misconfiguration warning to also detect WORKFLOW_VERCEL_AUTH_TOKEN and WORKFLOW_VERCEL_ENV, listing the specific env vars that are set * Add changeset Signed-off-by: Nathan Rajlich <n@n8.io> --------- Signed-off-by: Nathan Rajlich <n@n8.io> |
||
|
|
97932d3086 |
fix: thread runId through stream inspection for encryption key resolution (#1277)
CLI showStream: - Requires --run with --decrypt for encrypted stream decryption - Warns when --decrypt is used without --run Web stream reading: - readStreamServerAction accepts runId parameter for key resolution - Stream API route reads runId from query param - readStream client function passes runId to the API route - useStreamReader hook accepts and passes runId - run-detail-view passes runId to useStreamReader Removes getRunIdFromStreamId helper (stream IDs don't always share the run's ULID, e.g. streams serialized across step/workflow boundaries). |
||
|
|
b68ed630ec |
fix(cli): read orgId from project entry in repo.json, not root (#1263)
The Vercel CLI's repo.json format puts orgId on each project entry, not at the root level. The CLI was reading repoConfig.orgId (undefined) instead of project.orgId, which meant teamId was never set. Without teamId, the world-vercel URL selector used the direct vercel-workflow.com URL instead of the api.vercel.com proxy, causing 401 errors because vercel-workflow.com only accepts OIDC tokens (not CLI auth tokens). Also fix the RepoProjectConfig/RepoProjectsConfig type definitions to match the actual repo.json structure. |
||
|
|
bbe40ff00a |
Opt-in decryption for o11y tooling (CLI + web) (#1256)
* Add browser-compatible AES-GCM to core and HKDF key derivation to world-vercel * update changeset * Move HKDF key derivation server-side: API returns per-run derived key * Refactor encrypt/decrypt to accept CryptoKey, export importKey for callers to import once per run * Overload getEncryptionKeyForRun: accept context for start(), fetch WorkflowRun in resume-hook * Split changeset into per-package descriptions for world, world-vercel, and core * Remove unnecessary Uint8Array.from() wrapper around Buffer.from() * Use zod to parse Vercel API response * fix: restore world-vercel files to main versions The rebase incorrectly picked up older versions of these files from early encryption branch commits. The main versions are correct and up-to-date. * fix: add type cast for hydrateStepReturnValue return in hook.ts * Make decryption an explicit opt-in for o11y tooling * Restore encrypted data handling in o11y hydration layer * Use EncryptedDataRef with util.inspect.custom for CLI encrypted data display * Fix Decrypt button crash: use correct 'refresh' callback from useWorkflowResourceData * Implement client-side decryption for web o11y with getEncryptionKeyForRun RPC * Fix CLI decrypt: fetch WorkflowRun for key resolution, cache per runId * Use named constructor pattern for encrypted data display in web o11y * Decrypt event data when encryption key is available after Decrypt button click * Lift encryption key to run-level state, auto-decrypt on fetch, fix field pollution * Re-load expanded event data when encryption key becomes available * Consolidate Decrypt to title bar Button, remove sidebar decrypt card * Add hover tooltip to Decrypt button explaining scope and state * Show flat Encrypted label for encrypted fields, use Lucide Lock icon in DataInspector * Render eventData subfields individually to avoid encrypted markers in collapsed preview * Revert: render eventData subfields individually * Fix Lock icon vertical alignment in DataInspector encrypted label * update changeset * Update CLI, web, and stream callers for CryptoKey: importKey at resolution sites * Pass teamId to the get-key endpoint * fix: remove unused DataInspector import in events-list.tsx * fix: restore world-vercel files to base branch versions Cherry-pick conflict resolution incorrectly took the older opt-in-decrypt versions of these files, reverting improvements from main (dispatcher, createGetEncryptionKeyForRun extraction, nullable key response). * fix: address PR review feedback - Remove duplicate AttributePanel/EventsList rendering in entity-detail-panel.tsx. Thread encryptionKey into the existing EventsList render instead. - Restore missing re-exports (isClassInstanceRef, isStreamId, isStreamRef) in web-shared/src/index.ts to maintain backwards compatibility. - Add 'error' to replaceEncryptedWithMarkers field list in web-shared hydration.ts to match the decrypt path. - Extend CLI hydration eventData decrypt/placeholder to cover all known serialized fields (output, metadata, payload) not just result/input. - Add 'error' to CLI replaceEncryptedWithRef field list. - Remove invalid encryptionKey option from useWorkflowResourceData call (hook doesn't support it yet), add TODO. - Add 4 unit tests for hydrateDataWithKey in serialization-format.test.ts: encrypted+key decrypts, encrypted+noKey returns raw, non-encrypted hydrates normally, non-Uint8Array legacy data passes through. * feat: thread encryptionKey through useWorkflowResourceData hook Instead of leaving a TODO, implement the encryptionKey support directly: - Add optional encryptionKey to useWorkflowResourceData options - When key is available, use hydrateResourceIOWithKey (async decrypt) instead of hydrateResourceIO for all resource types - Remove redundant hydrateResourceIO from fetchResourceWithCorrelationId * fix: address comprehensive review feedback on PR #1256 High priority: - Gate showStream key fetch on --decrypt flag, warn when --decrypt used without --run - Fix workflow-server-actions.server.ts missing cryptoKey params (undefined for both getExternalRevivers and getDeserializeStream) - Add hydration + decryption to listEvents (was completely missing) - Fix error/eventData display: check isEncryptedMarker before hasDisplayContent so encrypted markers don't silently disappear Medium priority: - handleDecrypt: use toast.error() instead of console.error for user-visible feedback on key fetch failures - CLI maybeDecryptFields: add try/catch with graceful fallback to encrypted placeholders + warning, also decrypt error field - use-resource-data: wrap hook/sleep hydrate() in try/catch to prevent stuck loading state on decryption errors - Decrypt button: also check run.error and step input/output for encrypted markers, not just run.input/output Low priority: - event-list-view: add .catch() to re-load useEffect promise - Export ENCRYPTED_DISPLAY_NAME from hydration.ts and import in data-inspector.tsx instead of raw 'Encrypted' string |
||
|
|
7618ac36c2 |
Wire AES-GCM encryption into serialization layer (#1251)
* fix(core): chain unconsumed event check onto promiseQueue to prevent false positives The EventsConsumer's unconsumed event check (setTimeout(0)) was racing against the promiseQueue's async deserialization. When parallel steps completed and their hydrateStepReturnValue did real async work (e.g., decryption), the setTimeout(0) fired before the promise chain resolved the step results and triggered the next subscribe() call. This caused step_created events for sequential steps to be falsely flagged as unconsumed/orphaned. Fix: chain the unconsumed check onto the promiseQueue via getPromiseQueue() so it only fires after all pending async work completes. Use process.nextTick (not setTimeout) after the queue drains to give synchronous subscribe() calls from resolved user code a chance to cancel. Version-based cancellation replaces clearTimeout since the check is now promise-based. Adds getPromiseQueue option to EventsConsumerOptions. The workflow.ts context uses a getter/setter to keep the promiseQueue holder in sync. Reproduction test: parallel steps A+B with 10ms mock deserialization delay, followed by sequential step C. Previously failed with 'Unconsumed event: step_created(C)'. Now passes. * fix: chain hydrateWorkflowArguments onto promiseQueue to prevent false unconsumed events The unconsumed event check was firing during the async gap between run_started consumption and the workflow function subscribing its first step callbacks. This happened because hydrateWorkflowArguments is async, and during its await, the EventsConsumer advanced to step_created events that had no subscriber yet. Fix: chain hydrateWorkflowArguments onto the promiseQueue so the unconsumed check (which waits for the queue to drain) doesn't fire until after the workflow arguments are hydrated and the workflow function has been invoked. * fix: use setTimeout(0) macrotask for unconsumed check to ensure VM promise propagation completes The process.nextTick-based unconsumed check was still racing against VM promise propagation. After promiseQueue resolves and the user code's resolve() fires, there are multiple microtask hops through the VM boundary before the workflow code actually calls subscribe() for the next steps. process.nextTick fires before those VM microtasks complete. setTimeout(0) is a macrotask that is guaranteed to fire only after ALL microtasks (including VM promise chain propagation) have drained. The pendingUnconsumedTimeout handle is stored and cleared in subscribe() to prevent keeping the event loop alive unnecessarily. * fix: increase unconsumed event check delay to 100ms for cross-VM promise propagation setTimeout(0) is insufficient because Node.js does not guarantee that macrotasks fire after all cross-context (VM boundary) microtasks settle. After promiseQueue resolves and resolve() fires in the host context, there are multiple microtask hops through the VM boundary before the workflow code actually calls subscribe(). A 100ms delay provides sufficient time for this propagation while still detecting truly orphaned events promptly. Also update sleep.test.ts to wait 200ms for the unconsumed check. * Add browser-compatible AES-GCM to core and HKDF key derivation to world-vercel * update changeset * Move HKDF key derivation server-side: API returns per-run derived key * Refactor encrypt/decrypt to accept CryptoKey, export importKey for callers to import once per run * Overload getEncryptionKeyForRun: accept context for start(), fetch WorkflowRun in resume-hook * Split changeset into per-package descriptions for world, world-vercel, and core * Remove unnecessary Uint8Array.from() wrapper around Buffer.from() * Use zod to parse Vercel API response * Wire encryption into serialization layer * Wire AES-GCM encryption into serialization layer * update changeset * Add encryption unit tests: primitives, maybeEncrypt/maybeDecrypt, isEncrypted, complex type round-trips * Accept CryptoKey in encrypt/decrypt, export importKey for callers to import once per run * Fix review comments: cache stream encryption key, remove redundant casts, fix stale comments * Trying to clean up some type non-sense * fix: restore world-vercel files to main versions The rebase incorrectly picked up older versions of these files from early encryption branch commits. The main versions are correct and up-to-date. * fix: add type cast for hydrateStepReturnValue return in hook.ts * fix: address review feedback on encryption PR - Remove Vercel-specific error message from maybeDecrypt (core should not reference VERCEL_DEPLOYMENT_KEY) - Move stream encryption/decryption from transport layer (WorkflowServerReadableStream/WritableStream) to framing layer (getSerializeStream/getDeserializeStream). Frame length headers stay in the clear so frame boundaries are always parseable regardless of transport chunking; encryption wraps the frame payload. - Remove explicit Promise<unknown> return types from all 4 hydrate functions. On main these had inferred types (any from devalue), so callers didn't need casts. The encryption branch added explicit annotations that broke this. - Revert unnecessary type casts in run.ts, step-handler.ts, hook.ts that were only needed due to the explicit Promise<unknown> annotations - Revert closureVars type from unknown back to Record<string, any> in context-storage.ts to match the contract with getClosureVars - Fix hydrateWorkflowArguments JSDoc for unused _runId parameter * Revert more unnecessary changes * cleanup: remove unused runId param, deduplicate processFrames, add legacy comments - Remove unused _runId parameter from WorkflowServerReadableStream constructor and all 4 call sites - Deduplicate processFrames decryption: decrypt first and reassign format/payload, then fall through to single deserialization path - Add comments on all legacy non-Uint8Array branches explaining when this happens (specVersion 1 runs stored data as plain JSON arrays) - Fix duplicate code block in hydrateStepReturnValue * feat: wire cryptoKey through stream serialize/deserialize pipeline Thread the encryption key through the entire stream serialization chain so that ReadableStream and WritableStream values are encrypted/decrypted at the framing level. - Add optional cryptoKey param to getExternalReducers, getStepReducers, getExternalRevivers, getStepRevivers - Pass cryptoKey to getSerializeStream/getDeserializeStream at all 8 internal call sites within reducers/revivers - Thread key from dehydrate/hydrate functions into their reducers/revivers - Cache encryption key in Run class (resolved once via getEncryptionKey(), reused for returnValue, getReadable(), etc.) - Make Run#getReadable() async to resolve the cached key before creating the deserialize stream - Add encryptionKey to step context storage so getWritable() can access it during step execution * fix: make cryptoKey required-but-nullable to prevent silent omission, add stream encryption tests Change cryptoKey parameter from optional (cryptoKey?) to required-but- nullable (cryptoKey: CryptoKey | undefined) on all 6 functions: - getSerializeStream, getDeserializeStream - getExternalReducers, getStepReducers - getExternalRevivers, getStepRevivers This ensures every call site must explicitly pass the key or undefined, making it impossible to accidentally omit it and silently skip encryption. Add 7 stream encryption round-trip tests: - Encrypted frames have 'encr' prefix inside length header - Full round-trip: encrypt serialize -> decrypt deserialize - Concatenated encrypted frames (transport coalescing) - Split encrypted frames (transport splitting) - Error when encrypted data encountered without key - No encryption when key is undefined - Large payload round-trip Full audit confirms all encryption key threading is complete: - All 8 dehydrate/hydrate functions pass key to reducers/revivers - All stream serialize/deserialize call sites pass key - Run class caches key for reuse across returnValue and getReadable() - Step context storage carries key for getWritable() * fix: keep Run#getReadable() sync, resolve encryption key lazily in streams - Revert Run#getReadable() to synchronous (non-breaking API). The encryption key is passed as a Promise through the chain and resolved lazily inside the first async transform() call. - Add EncryptionKeyParam type alias that accepts CryptoKey, undefined, or Promise<CryptoKey | undefined>. Used by getSerializeStream, getDeserializeStream, and all reducer/reviver functions. - Key promises are resolved once on first use via a keyState cache object inside each stream's transform closure. - Fix CLI showStream to resolve encryption key from world when runId is provided via --run flag, instead of passing undefined. - Remove incorrect CLI warning that --run is not supported for streams (it is now needed for encrypted stream decryption). * . * fix: address review feedback from PR #1251 - Fix 4 broken dehydrateWorkflowArguments calls in workflow.test.ts that were passing ops as runId (missing runId and key params) - Use WorkflowRuntimeError instead of plain Error in decodeFormatPrefix for unknown serialization formats, for consistency and programmatic error handling - Document maybeDecrypt throw behavior: callers should be aware this surfaces as a rejected promise during key rotation/misconfiguration - Document key-fetch rejection timing in streams: promise rejection won't surface until the first chunk is processed |
||
|
|
a9fea9132e |
Update workbench tests to build and run outside of monorepo (#1230)
* Setup fixes * ci: run local e2e against staged tarball workbenches * ci: update staged workbench tarball setup script * chore: set nextjs workbenches back to next 16.1.6 * update lock * test(e2e): resolve workbench path from WORKBENCH_APP_PATH * fix: address deferred builder issues outside monorepo * ci: stage tarball workbenches only for nextjs local e2e * fix(next): discover deferred steps imported via workflows * test(core): gate deferred step-discovery dev test to canary * test(e2e): cover cross-file imported step in build/start lanes * fix(e2e): use local manifest in local runs and relax dev rebuild timeout * fix(workbench): add imported-step workflow symlink for sveltekit/astro * test(e2e): scope imported-step workflow test to nextjs lanes * fix(next): rebuild deferred entries on discovered file updates * fix(next): watch transitive deferred step deps for dev rebuilds * fix(next): restore socket-driven deferred step rebuilds * add changeset * chore: address review feedback on deferred e2e updates * fix(cli): guard stream flush against closed write streams |
||
|
|
dda67421cf | [world-postgres] [cli] Skip graphile logs for CLI json mode, observe DEBUG env (#1167) | ||
|
|
14863bf622 |
fix(workflow): improve runtime API stub error message and fix SKILL.md streaming docs (#1077)
* fix(workflow): improve error message for runtime API usage in workflow context and fix SKILL.md streaming docs The workflow stub error now tells users to move calls to a step function or outside the workflow context. Also corrects the SKILL.md streaming section which incorrectly stated getWritable() only works in steps. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: add changeset Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(skill): address PR feedback on SKILL.md - Remove `npx workflow start` from debugging section (args can't easily be passed via CLI) - Mention --json and --help flags for CLI inspection - Add reminder to check AI docs for DurableAgent details Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(skill): fix SKILL.md debugging section CLI commands Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * other improvements * fix(skill): address Copilot PR feedback on streaming examples - Add missing imports and define all referenced variables - Wrap writer.releaseLock() in finally blocks for safety - Remove undefined `data`/`agent`/`messages` references - Simplify examples to be self-contained and copy-pasteable Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore(skill): bump SKILL.md version to 1.1 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add skill version bump rule to CLAUDE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: John Lindquist <johnlindquist@gmail.com> |
||
|
|
ea3254e7ce |
Fix projectConfig.projectId containing project name instead of ID (#999)
* Fix projectConfig.projectId containing project name instead of ID * add changeset * Address review: fix changeset description, normalize slug-based project IDs, add clarifying comments |
||
|
|
6e72b295e7 |
Add World.getEncryptionKeyForRun and thread encryption key through serialization (#979)
## Summary - Adds `World.getEncryptionKeyForRun(run)` returning `Uint8Array | undefined` as the interface for retrieving per-run encryption keys - Updates all 8 dehydrate/hydrate serialization functions to accept `key: Uint8Array | undefined` - Updates runtime callers, CLI, and tests to thread the key parameter through |
||
|
|
0d5323c0a7 |
Make serialization functions async (#978)
## Summary - Makes all 8 dehydrate/hydrate serialization functions in `@workflow/core` async (returning `Promise<...>`) - Updates all call sites across runtime, CLI, and test code to `await` the results - Pure mechanical refactor — no functional changes, prerequisite for encryption support Part 1 of the end-to-end encryption PR stack. |
||
|
|
54879835f3 | Fix pages router default args: use empty array instead of [42] (#1081) | ||
|
|
262ef3a21a |
Fix CLI missing specVersion in "run_cancelled" event payload (#1078)
The CLI's `cancelRun` in `packages/cli/src/lib/inspect/run.ts` sent `{ eventType: 'run_cancelled' }` without `specVersion`, causing the Vercel API's `/v2/runs/{id}/events` endpoint to reject the request with a 400 error. The core runtime's `cancelRun` already handled this correctly, so the fix removes the redundant CLI wrapper and calls the core implementation directly.
The bug was introduced in
|
||
|
|
8cfb43808b |
Use @vercel/cli-auth for auth token reading and OAuth refresh (#1043)
* Use `@vercel/cli-auth` for auth token reading and OAuth refresh Replace the manual auth.json reading logic in the CLI with the `@vercel/cli-auth` package, which handles credential storage via CredentialsStore and OAuth token refresh via the OAuth client. Previously, the CLI would read the token from disk but had no refresh logic — if the token was expired, API calls would fail. Now, getAuthToken() checks token expiry and automatically refreshes it using the stored refresh token before returning it. * Remove dead logging |