subagent: capture overrides at delegation; stamp ahead of prompt vetoes

Review fixes (ds-review-bot on #623):

- Capture-at-delegation: the driver now reads overrideOf(parent.session)
  for both knobs synchronously before its first await, and the prompt-submit
  listener stamps those captured values — a parent switch racing the child's
  asynchronous creation belongs to the parent's future, not the child. The
  inheritOverride(parent, child) service method is split into its two halves
  (overrideOf / stampOverride) accordingly.
- Veto safety: the one-shot prompt-submit listener registers with
  prepend: true, so a veto-capable listener (a denying UserPromptSubmit
  hook) cannot close the child's first turn without the durable stamp.

Both regressions are pinned red-first in inheritance.spec.ts: the
delegation-vs-late-switch race (delegate tool flips the caller wider while
the creation transaction is pending) and a blocking prompt-submit listener
(stamp survives a promptless first turn). Service contract tests renamed to
the split API; READMEs and the bilingual Agent Note updated.
This commit is contained in:
kingwl
2026-07-25 10:51:25 +08:00
parent 669771097d
commit 6fa2377e34
12 changed files with 192 additions and 91 deletions
@@ -100,6 +100,17 @@ export async function startInProcessRun(
subagentDepth: childDepth,
}
// Policy inheritance, read half: capture the parent's sandbox/approval
// OVERRIDES synchronously, before the first await — the delegation moment
// is the semantic snapshot point, and a parent switch racing the child's
// asynchronous creation must belong to the parent's future, not the child.
// Both services are consumed opportunistically — without them, delegation
// stays policy-free.
const sandboxPolicy = parent.ctx.get('sandboxPolicy')
const approval = parent.ctx.get('approval')
const inheritedMode = sandboxPolicy?.overrideOf(parent.session)
const inheritedPolicy = approval?.overrideOf(parent.session)
let structured: StructuredAttachment | undefined
const setup = (childCtx: Context): void => {
if (request.persona !== undefined) {
@@ -109,20 +120,23 @@ export async function startInProcessRun(
if (request.outputSchema !== undefined) {
structured = attachStructuredRuntime(childCtx, request.outputSchema)
}
// Policy inheritance: stamp the parent's sandbox/approval OVERRIDES onto
// the child once, anchored inside the child's FIRST turn (prompt-submit
// runs after turn/start, before prompt assembly) — a bare between-turn
// append would be crash-tail garbage on reload, and stamping here also
// orders the override after any stale switch a fork seed carried, so the
// ordinary last-event-wins fold resolves it. One-shot: later turns must
// not re-stamp over a switch the child made itself. Both services are
// consumed opportunistically — without them, delegation stays policy-free.
const disposeInherit = childCtx.on('agent/prompt-submit', (childAgent, _content, _source, _signal, next) => {
disposeInherit()
parent.ctx.get('sandboxPolicy')?.inheritOverride(parent.session, childAgent.session)
parent.ctx.get('approval')?.inheritOverride(parent.session, childAgent.session)
return next()
})
// Write half: stamp the captured overrides once, anchored inside the
// child's FIRST turn (prompt-submit runs after turn/start, before prompt
// assembly) — a bare between-turn append would be crash-tail garbage on
// reload, and stamping here also orders the override after any stale
// switch a fork seed carried, so the ordinary last-event-wins fold
// resolves it. PREPENDED so a veto-capable listener (a denying
// UserPromptSubmit hook) cannot close the first turn without the stamp —
// the stamp must be durable even for a blocked first prompt. One-shot:
// later turns must not re-stamp over a switch the child made itself.
if (inheritedMode !== undefined || inheritedPolicy !== undefined) {
const disposeInherit = childCtx.on('agent/prompt-submit', (childAgent, _content, _source, _signal, next) => {
disposeInherit()
if (inheritedMode !== undefined) sandboxPolicy?.stampOverride(childAgent.session, inheritedMode)
if (inheritedPolicy !== undefined) approval?.stampOverride(childAgent.session, inheritedPolicy)
return next()
}, { prepend: true })
}
}
const flags = { cancelled: false }