In-process subagent policy inheritance — the child starts under the parent's sandbox override
进程内 subagent 策略继承——子 agent 在父级的沙箱覆盖项下启动
Sandbox and approval overrides are per-session log folds. An in-process subagent gets a new session, so a spawn child once fell back to deployment defaults and a fork child saw only switches inside its completed-turn prefix. Delegation could therefore widen a parent that had switched to `read-only`.
English
Problem
Sandbox and approval overrides are per-session log folds. An in-process subagent gets a new session, so a spawn child once fell back to deployment defaults and a fork child saw only switches inside its completed-turn prefix. Delegation could therefore widen a parent that had switched to read-only.
Decision
The delegation boundary snapshots sandboxPolicy.overrideOf(parent.session) before its first await, through the shared child-agent helpers (captureDelegatedPolicyOverrides/appendDelegatedPolicyOverrides in dsh-subagent), which the one-shot driver and the continuable start both call. A later parent switch belongs to the parent's future; cancel-and-redelegate takes a new snapshot. The sandbox-policy service is optional, and only the explicit session override is copied, never deployment defaults or one-shot grants. The approval policy is not inherited: the same capture pins every child to 'never' — the approvals-pinned decision supersedes this note's original approval-override inheritance.
Each captured value becomes a source-tagged sandbox/mode or approval/policy event appended during the child factory's unpublished setup. The session constructor has already fixed Session.firstLiveSeq at the fork-prefix length, so the inherited facts follow fork history, reach telemetry when the child is announced, and leave SessionHeader.seedLength at the prefix length. Existing last-event-wins folds therefore make the delegation snapshot beat stale fork history and let a later child switch beat the snapshot. A grandchild folds its parent's logged state, so the rule composes without another inheritance mechanism.
Ordinary session appends validate the inherited events before publication, and persistence captures the complete unpublished log when the session is announced. Any materialized child log therefore stores the inherited events with its first batch; there is no second policy store, schema field, or query index. The source: 'delegation' marker lets approval narration distinguish inheritance from a child-side user switch.
What a blocked child experiences
A confined child gets the ordinary denial marker, and an escalation request is rejected deterministically by the child's pinned 'never' policy; the subagent:delegation runtime-context statement tells the child to report the limitation instead of retrying, and a controller-owned parent may widen its own session and delegate again (approvals-pinned decision).
Alternatives considered
- Generic
SessionHeaderpolicy fields — rejected: they duplicate an event-sourced fact in metadata and require propagation through core session types, persistence backends, query indexes, collision identity, and every policy consumer. Unpublished setup events have the required ordering and reuse the existing durable store. - Combining new policy facts with constructor history — rejected:
Session.firstLiveSeqclassifies the complete constructor seed as replayed history, so telemetry would skip child-only facts. Unpublished setup keeps history and new facts on their existing sides of that boundary without another session option. - A first-prompt listener — rejected: it introduces listener ordering and a later timing boundary even though the creation transaction already permits log appends before publication.
- Copying deployment defaults — rejected: defaults remain operator-owned and may change; an unswitched parent stamps nothing, so its child follows the current deployment.
- Live resolution walking
parentSessionat each call — rejected: it breaks the "two sessions never see each other's state" isolation invariant, requires the parent session to stay loaded for the child's lifetime, and makes a mid-run parent switch retroactively change a running child. Snapshot-at-delegation is the semantic: the child keeps the policy it was handed; cancel-and-respawn picks up a tightening. - Forcing
'never'— originally rejected here as inheritance behavior because a forced value forecloses a future child answerer; that verdict is reversed by the approvals-pinned decision, which owns the current rationale. Routing asks to the root controller needs parent-chain ownership and the spawningcallId, and remains deferred in the approval-seam Agent Note.
Consequences
- Spawn, fork, and nested in-process children retain a parent's explicit sandbox override and are pinned to
'never'approvals. The focused suite proves real filesystem denial, stale-fork precedence, delegation-time capture, the live-event boundary, default omission, and context disposal. - The keyless headless snapshot is the assembled regression: only the parent is
read-only, the deployment default isworkspace-write, and the child's persisted event plus denied disk write both fail if capture is removed. - Each delegation adds at most two log-only events.
dsh-subagentowns the optional peer types for the two policy services — its shared helpers hold thectx.getconsumption; compositions without either service behave unchanged. Out-of-process children retain their own deployment policy, and a running child does not follow later parent switches.
中文
问题
沙箱与审批覆盖项都是按会话的日志折叠。进程内 subagent 会获得一个新会话,因此 spawn 子 agent(智能体)过去会回退到部署默认值,fork 子 agent 则只能看到其已完成轮次前缀中的切换。因此,委派可能放宽已经切换到 read-only 的父级。
决策
委派边界在第一次 await 之前,经由共享的子 agent 辅助函数(dsh-subagent 中的 captureDelegatedPolicyOverrides/appendDelegatedPolicyOverrides)对 sandboxPolicy.overrideOf(parent.session) 获取快照;一次性驱动器与可继续启动都会调用这些辅助函数。父级后续的切换属于父级的未来;取消后重新委派会取得新快照。沙箱策略服务为可选,仅复制显式会话覆盖项,绝不复制部署默认值或一次性授权。审批策略不继承:同一次捕获会把每个子 agent 钉定为 'never'——审批钉定决策取代了本 note 原先的审批覆盖项继承。
每个捕获值都会成为子 agent 工厂在未发布设置阶段追加的一条带来源标记的 sandbox/mode 或 approval/policy 事件。会话构造函数已将 Session.firstLiveSeq 固定为 fork 前缀的长度,因此继承事实会排在 fork 历史之后,在子 agent 公布时进入遥测,同时让 SessionHeader.seedLength 保持为此前缀的长度。因此,既有的末事件胜出折叠会让委派快照压过陈旧的 fork 历史,并让子 agent 后续的切换压过该快照。孙代 agent 会折叠其父级已记录的状态,因此无需另一套继承机制即可组合此规则。
普通的会话追加会在发布前校验继承事件,持久化层则在会话公布时捕获完整的未发布日志。因此,任何已物化的子 agent 日志都会在首批数据中存下继承事件;不存在第二套策略存储、schema 字段或查询索引。source: 'delegation' 标记让审批叙述能够区分继承与子 agent 侧的用户切换。
被拦住的子 agent 会经历什么
受限子 agent 会得到普通拒绝标记,升级请求则被子 agent 钉定的 'never' 策略确定性拒绝;subagent:delegation 运行时上下文声明告知子 agent 上报限制而不是重试,由控制器持有的父 agent 可以放宽自己的会话后重新委派(审批钉定决策)。
考虑过的替代方案
- 通用的
SessionHeader策略字段:不予采纳。它们会在元数据中复制一项事件溯源事实,并要求贯穿核心会话类型、持久化后端、查询索引、碰撞标识与每个策略消费方进行传播。未发布设置阶段的事件具备所需顺序,并复用现有持久化存储。 - 将新策略事实与构造历史合并:不予采纳。
Session.firstLiveSeq会把完整的构造种子归类为回放历史,因此遥测会跳过仅属于子 agent 的事实。未发布设置让历史与新事实留在该边界各自原有的一侧,无需再增加会话选项。 - 首个提示词监听器:不予采纳。尽管创建事务已经允许在发布前追加日志,它仍会引入监听器顺序与更晚的时序边界。
- 复制部署默认值:不予采纳。默认值仍由运维人员拥有且可能变化;未切换的父级不会记录任何值,因此其子 agent 跟随当前部署。
- 每次调用时沿
parentSession实时解析:不予采纳。这会打破「两个会话永远看不到彼此状态」的隔离不变量,要求父会话在子 agent 的整个生命周期内保持加载,还会让父级在子 agent 运行途中做的切换追溯性地改变一个正在运行的子 agent。委派时快照才是本设计的语义:子 agent 保持它被交付时的策略;取消后重新 spawn 即可拿到收紧后的策略。 - 强制使用
'never':本 note 当初不作为继承行为采纳,理由是强制值会排除未来的子 agent 应答器;该结论已被审批钉定决策推翻,现行理由归其所有。把 ask 路由到根控制器需要父链所有权与发起 spawn 的callId,仍按审批 seam Agent Note 所述延期。
后果
- spawn、fork 和嵌套的进程内子 agent 会保留父级显式的沙箱覆盖项,并被钉定为
'never'审批。聚焦测试套件证明真实文件系统拒绝、陈旧 fork 优先级、委派时捕获、实时事件边界、默认值省略与上下文释放。 - 无密钥 headless 快照是组装后应用层面的回归测试:只有父级是
read-only,部署默认值是workspace-write;若移除捕获,子 agent 的持久化事件与被拒的磁盘写入这两项检查都会失败。 - 每次委派最多增加两条仅日志事件。两个策略服务的可选 peer 类型由
dsh-subagent拥有——其共享辅助函数持有ctx.get消费;未组合任一服务的组合保持原有行为。进程外子 agent 仍采用自身的部署策略,正在运行的子 agent 不跟随父级后续切换。