Codex code-review round 5: agent()/parallel()/pipeline() returned HOST Promise
objects into the script realm — Object.getPrototypeOf(agent('x')) reached host
Promise.prototype, contradicting the realm contract (correctness containment,
not the accepted sandbox stance). The rejection channel had the same leak one
hop away: a caught hook failure was a host WorkflowError (host Error.prototype
chain), and phase()/log() threw host errors synchronously.
All three surfaces are realm-built now:
- hook promises: the realm's own Promise.resolve (bound at context setup)
assimilates the host promise, so the script-visible promise carries realm
prototypes; the realm promise gets the same no-op rejection consumer as the
host one (a script may drop it).
- hook failures: rejections and phase/log sync throws are translated at the
boundary into realm-built clones (name/code/message/fatal via an in-realm
factory); non-WorkflowError host failures become generic realm Errors
carrying their describeThrown rendering.
- the combinators recognize FATAL clones structurally
(isFatalWorkflowErrorClone: proxy-guarded descriptor reads), preserving the
fatal-vs-null discipline across the boundary; a script forging the shape
kills only its own run. drive() maps any post-cancel failure to 'cancelled'
by run state (a CANCELLED clone deliberately fails the host instanceof).
Tests: realm-promise identity for all three hooks + host Promise.prototype
pollution unreachable; clone shape (instanceof realm Error, name/code/fatal/
message) with prototype-chain mutation staying realm-side; a rejecting
provider result crossing as a generic clone; phase/log sync-throw clones;
combinator catch branches (string throw, proxy throw, shape-miss forgery →
null; forged fatal → kills own run); existing fatal-propagation, cancellation,
and unhandled-rejection tests as canaries.
workflow/ — dynamic-workflow capability family
The workflow seam: a model-written JavaScript orchestration script that fans out subagents at scale (phases, structured per-agent results, concurrency caps), modeled on Claude Code's dynamic workflows. A capability seam (see capability seams) in the bash shape: ONE engine implementation per context registers as ctx.workflows; the model-facing tool consumes it.
| Package | Role | ctx key |
|---|---|---|
workflow/ |
Abstract workflow seam: service base class + run vocabulary + workflow/* events |
ctx.workflows |
workflow-vm/ |
In-process node:vm engine: parses the script, injects the hooks, drives ctx.subagents |
(provides ctx.workflows) |
tool-workflow/ |
Model-facing workflow tool over ctx.workflows |
(registers on ctx.tools) |
The interface lives at workflow/workflow/. The engine's agent() hook rides the subagent seam (any registered provider; the shipped examples use spawn), and agent({ schema }) rides the structured-output support the in-process backends implement. The seam split exists for engine hardening: node:vm is in-process and cannot kill a pathological synchronous spin — a worker-thread or isolated-vm engine swaps in behind the same interface if that ever matters.
The proposal, decisions, and deferred work: docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md.