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:
@@ -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(`
|
||||
|
||||
Reference in New Issue
Block a user