workflow: hook promises and hook failures are realm-built too

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.
This commit is contained in:
Tianyi Cui
2026-07-05 21:24:30 +08:00
parent 95c8c878e1
commit 7234d41b91
5 changed files with 203 additions and 25 deletions
@@ -288,9 +288,23 @@ describe('dsh-workflow-vm', () => {
() => { throw new Error('boom') },
() => agent('fine'),
() => 'plain value',
() => { throw 'string throw' },
() => { throw new Proxy({ name: 'WorkflowError', fatal: true }, {}) },
() => { throw { name: 'WorkflowError', fatal: 'forged-but-not-true' } },
])
`))
expect(result.value).toEqual([null, 'stub reply', 'plain value'])
// The last three probe the fatal-clone recognition: a non-object, a
// proxy (never inspected), and a shape miss are all ordinary nulls.
expect(result.value).toEqual([null, 'stub reply', 'plain value', null, null, null])
})
it('a script forging a fatal clone kills only its own run (self-sabotage, not a bypass)', async () => {
const { ctx, parent } = await setup()
const result = await run(ctx, parent, script(`
return await parallel([() => { throw { name: 'WorkflowError', fatal: true, message: 'forged fatal' } }])
`))
expect(result.stopReason).toBe('error')
expect(result.error).toContain('forged fatal')
})
it('FATAL errors propagate through parallel AND pipeline instead of dissolving into null', async () => {
@@ -433,6 +447,86 @@ describe('dsh-workflow-vm', () => {
expect((await run(ctx, parent, script('return typeof args'))).value).toBe('undefined')
})
it('hook promises are REALM promises: instanceof holds in-script, host Promise.prototype stays unreachable', async () => {
const { ctx, parent } = await setup()
const result = await run(ctx, parent, script(`
const p = agent('x')
const par = parallel([() => 'v'])
const pipe = pipeline([1], (n) => n)
Object.getPrototypeOf(p).wfLeakProbe = 'realm-only'
return {
agentIsRealmPromise: p instanceof Promise,
parallelIsRealmPromise: par instanceof Promise,
pipelineIsRealmPromise: pipe instanceof Promise,
value: await p,
}
`))
expect(result.stopReason).toBe('completed')
expect(result.value).toEqual({
agentIsRealmPromise: true,
parallelIsRealmPromise: true,
pipelineIsRealmPromise: true,
value: 'stub reply',
})
expect((Promise.prototype as unknown as Record<string, unknown>).wfLeakProbe).toBeUndefined()
delete (Promise.prototype as unknown as Record<string, unknown>).wfLeakProbe
})
it('hook failures cross the boundary as realm-built WorkflowError clones', async () => {
const { ctx, parent } = await setup()
const result = await run(ctx, parent, script(`
try {
await agent('p', { bogus: true })
return 'unreachable'
} catch (e) {
Object.getPrototypeOf(Object.getPrototypeOf(e)).wfErrLeakProbe = 'realm-only'
return { isRealmError: e instanceof Error, name: e.name, code: e.code, fatal: e.fatal, message: e.message }
}
`))
expect(result.stopReason).toBe('completed')
expect(result.value).toMatchObject({ isRealmError: true, name: 'WorkflowError', code: 'UNSUPPORTED_OPTION', fatal: true })
expect((result.value as { message: string }).message).toContain('"bogus" is not recognized')
// The script mutated its error's prototype CHAIN — host intrinsics untouched.
expect((Object.prototype as unknown as Record<string, unknown>).wfErrLeakProbe).toBeUndefined()
expect((Error.prototype as unknown as Record<string, unknown>).wfErrLeakProbe).toBeUndefined()
})
it('a non-WorkflowError host failure (a rejecting provider result) crosses as a generic realm clone', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
const provider: SubagentProvider = {
name: 'rejecting',
capabilities: { outputSchema: true, depthLimit: true, toolFilter: true },
start: () => ({
id: AgentId('reject-child'),
result: Promise.reject(new Error('backend exploded')),
cancel: () => { /* nothing in flight */ },
dispose: () => Promise.resolve(),
}),
}
ctx.subagents.registerProvider(provider)
await ctx.plugin(VmWorkflowEngine, { provider: 'rejecting' })
const result = await run(ctx, fakeParent(), script(`
try { await agent('p'); return 'unreachable' } catch (e) { return { isRealmError: e instanceof Error, name: e.name, message: e.message } }
`))
expect(result.value).toMatchObject({ isRealmError: true, name: 'Error' })
expect((result.value as { message: string }).message).toContain('backend exploded')
})
it('phase()/log() synchronous throws cross as realm clones too', async () => {
const { ctx, parent } = await setup()
const result = await run(ctx, parent, script(`
try { phase(3) } catch (e) {
if (!(e instanceof Error) || e.name !== 'WorkflowError') throw e
}
try { log(3) } catch (e) {
return { isRealmError: e instanceof Error, name: e.name, message: e.message }
}
`))
expect(result.value).toMatchObject({ isRealmError: true, name: 'WorkflowError' })
expect((result.value as { message: string }).message).toContain('log() requires')
})
it('parallel/pipeline resolve to REALM arrays: instanceof holds in-script, host intrinsics stay unreachable', async () => {
const { ctx, parent } = await setup()
const result = await run(ctx, parent, script(`