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:
@@ -242,9 +242,23 @@ Approval service that applies session policy before answerers and logs every ask
|
||||
* append commit point.
|
||||
*/
|
||||
async request(req: ApprovalRequest): Promise<ApprovalOutcome>
|
||||
|
||||
/**
|
||||
* Stamp the parent's approval-policy OVERRIDE onto a child session through
|
||||
* the canonical write path — the delegation-inheritance step: a `'never'`
|
||||
* (headless/CI) parent must not mint children that fall back to a prompting
|
||||
* default. Only the override chain is copied: an unswitched parent stamps
|
||||
* nothing, so the child keeps following the LIVE configured default. A
|
||||
* child whose log (e.g. a fork seed) already folds to the inherited policy
|
||||
* is left untouched. Callers must append inside an open child turn — a bare
|
||||
* between-turn event is crash-tail garbage on reload.
|
||||
* @param parent - the delegating session whose effective override is read.
|
||||
* @param child - the child session the override is appended to.
|
||||
*/
|
||||
inheritOverride(parent: Session, child: Session): void
|
||||
```
|
||||
|
||||
Types: [ApprovalOutcome](../core-data-structures/approval.md) · [ApprovalRequest](../core-data-structures/approval.md)
|
||||
Types: [ApprovalOutcome](../core-data-structures/approval.md) · [ApprovalRequest](../core-data-structures/approval.md) · [Session](../core-data-structures/session.md)
|
||||
|
||||
Source: [`packages/ui/user-approval/src/index.ts:213`](../../packages/ui/user-approval/src/index.ts)
|
||||
|
||||
@@ -874,9 +888,23 @@ The sandbox-policy service (`ctx.sandboxPolicy`). Owns the deployment default mo
|
||||
* @returns the fully resolved per-call mode and absolute workspace root.
|
||||
*/
|
||||
resolve(request: SandboxPolicyRequest = {}): SandboxExecutionPolicy
|
||||
|
||||
/**
|
||||
* Stamp the parent's sandbox-mode OVERRIDE onto a child session through the
|
||||
* canonical write path — the delegation-inheritance step: a child agent runs
|
||||
* under the policy its delegating parent was switched to, not under the
|
||||
* (possibly wider) deployment default. Only the override chain is copied: an
|
||||
* unswitched parent stamps nothing, so the child keeps following the LIVE
|
||||
* deployment default. A child whose log (e.g. a fork seed) already folds to
|
||||
* the inherited mode is left untouched. Callers must append inside an open
|
||||
* child turn — a bare between-turn event is crash-tail garbage on reload.
|
||||
* @param parent - the delegating session whose effective override is read.
|
||||
* @param child - the child session the override is appended to.
|
||||
*/
|
||||
inheritOverride(parent: Session, child: Session): void
|
||||
```
|
||||
|
||||
Types: [SandboxExecutionPolicy](../core-data-structures/sandbox.md) · [SandboxPolicyRequest](../core-data-structures/sandbox.md)
|
||||
Types: [SandboxExecutionPolicy](../core-data-structures/sandbox.md) · [SandboxPolicyRequest](../core-data-structures/sandbox.md) · [Session](../core-data-structures/session.md)
|
||||
|
||||
Source: [`packages/sandbox/sandbox-policy/src/index.ts:68`](../../packages/sandbox/sandbox-policy/src/index.ts)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:365`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session), `runtime`, [`tui`](../packages/ui/tui) |
|
||||
| `agent/post-step` | `serial` | [`packages/core/agent/src/types.ts:315`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`compact-basic`](../packages/compact/compact-basic), [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy) |
|
||||
| `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:246`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`time-context`](../packages/context/time-context), [`user-approval`](../packages/ui/user-approval) |
|
||||
| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:262`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) |
|
||||
| `agent/prompt-submit` | `waterfall` | [`packages/core/agent/src/types.ts:262`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`acp`](../packages/ui/acp), [`goal-session`](../packages/goal/goal-session), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) |
|
||||
| `agent/queued` | `emit` | [`packages/core/agent/src/types.ts:207`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-session`](../packages/goal/goal-session), [`tui`](../packages/ui/tui) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:276`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/types.ts:330`](../packages/core/agent/src/types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compact-basic`](../packages/compact/compact-basic), [`llm-retry`](../packages/llm/llm-retry), [`plan-mode`](../packages/plan/plan-mode) |
|
||||
|
||||
Reference in New Issue
Block a user