subagent: inherit parent sandbox/approval overrides in in-process children

Per-session policy overrides (sandbox/mode, approval/policy) never crossed
the delegation boundary: a spawn child of a read-only-switched parent ran
under the wider deployment default, and a fork child missed any switch made
after its seed boundary — delegation was a bypass channel for a user's
tightening.

The in-process driver now snapshots the delegating parent's override chain
and stamps it onto the child through the canonical write paths
(SandboxPolicyService.inheritOverride / ApprovalService.inheritOverride),
anchored inside the child's first turn via a one-shot agent/prompt-submit
listener: turn-enclosed (durable), ahead of the first request (an inherited
'never' reaches the child's first system prompt), and positioned after any
stale fork-seed switch so the ordinary last-event-wins fold resolves it.
Only overrides are copied — an unswitched parent stamps nothing and the
child follows the live deployment default; both services are consumed
opportunistically, so compositions without them delegate unchanged. Nesting
composes by construction (each stamp folds the already-stamped parent log).

Evidence: inheritance.spec.ts drives scripted-model children into the real
dsh-fs-sandbox fence through the real write tool (disk-state + denial-marker
assertions; spawn, stale-seed fork, grandchild, escalation fail-closed, and
no-stamp guards), inheritOverride contract tests in both service suites, and
the recorded subagent-sandbox-inheritance ACP snapshot (read-only preset →
delegate → child denied, replayed keylessly).

See .agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md.
This commit is contained in:
kingwl
2026-07-25 04:06:19 +08:00
parent 690c53dc03
commit 669771097d
29 changed files with 2168 additions and 7 deletions
+14
View File
@@ -38,6 +38,7 @@ const PTY_CONFIG = fileURLToPath(new URL('../pty.cordis.yml', import.meta.url))
const DEPTH_TWO_CONFIG = fileURLToPath(new URL('../depth-two.cordis.yml', import.meta.url))
const PACKED_CHUNKS_CONFIG = fileURLToPath(new URL('../packed-chunks.cordis.yml', import.meta.url))
const SESSION_SANDBOX_ROOT_CONFIG = fileURLToPath(new URL('../session-sandbox-root.cordis.yml', import.meta.url))
const SUBAGENT_INHERITANCE_CONFIG = fileURLToPath(new URL('../subagent-inheritance.cordis.yml', import.meta.url))
const LSP_CONFIG = fileURLToPath(new URL('./lsp.cordis.yml', import.meta.url))
const SNAPSHOTS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'snapshots')
const PACKED_CHUNKS_SOURCE = 'hook-cc-pretool-deny'
@@ -241,6 +242,19 @@ const SCENARIOS: Scenario[] = [
{ name: 'escalation-approved', hasModelTurn: true, recorded: true, headerClass: 'sandbox' },
{ name: 'escalation-rejected', hasModelTurn: true, recorded: true, headerClass: 'sandbox' },
{ name: 'fs-escalation-approved', hasModelTurn: true, recorded: true, headerClass: 'sandbox' },
// Policy inheritance across delegation: the overlay adds a read-only preset,
// the script tightens the PARENT session to it, and the delegated child's
// real write is then denied by the fs fence — proving the parent's override
// crossed the delegation boundary instead of the child escaping to the
// composition default (the subagent policy-inheritance Agent Note).
{
name: 'subagent-sandbox-inheritance',
hasModelTurn: true,
recorded: true,
headerClass: 'subagent-inheritance',
pinsHeader: true,
configPath: SUBAGENT_INHERITANCE_CONFIG,
},
// Unlike ordinary snapshots, this session cwd is outside the platform temp
// roots that workspace-write always grants. The overlay points the
// deployment fallback at /tmp, so a successful relative write proves the