subagent: carry inherited policy overrides in the child session header
Review fix (ds-review-bot critical #2 on #623): the first-turn event stamp had a durability hole no turn anchoring can close — an idle SessionStart- style injection persists a complete one-shot turn before any prompt turn opens, so a crash in that window left a resumable-looking child with no inherited policy, falling back to a possibly wider deployment default. The captured overrides now ride the child's creation meta into its immutable SessionHeader (sandboxMode/approvalPolicy, neutral strings at the session boundary — the delegationDepth precedent), durable from the moment the session exists: no listener ordering can starve the baseline and no crash window can lose it. overrideOf(session) on both policy services resolves fold(events past header.seedLength) ?? header baseline, validating against the closed vocabulary on read; stampOverride and the prompt-submit listener machinery are deleted. The header field rides both persistence backends (JSONL header line; SQLite sessions columns, SCHEMA_VERSION 11 — pre-release, no migration). pty-local reads through overrideOf so PTY spawns see the baseline too. Red-first: header-durability-before-any-turn test (the injection crash window shape), baseline/seed-boundary/closed-vocabulary contract tests in both service suites; the real-wall suite (race, veto, fork stale-seed, grandchild) re-anchored on header assertions and green. The Agent Note's Alternatives now records the superseded event-stamping iteration with the review evidence; bilingual docs updated.
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-25-subagent-policy-inheritance.md: 000e59dc9a9208e57db5dd44d34bd6ce540801da
|
||||
2026-07-25-subagent-policy-inheritance.zh.md: 90738b4c298eea879a0093bf919139c8c70f00ef
|
||||
2026-07-25-subagent-policy-inheritance.md: a15a20aeb8324f00fcb9ca85aac5d5e2352dfac3
|
||||
2026-07-25-subagent-policy-inheritance.zh.md: 2ac844fdd9bbb1b4a57e76a96a5cd5270c23d2aa
|
||||
|
||||
@@ -10,28 +10,28 @@ Session policy overrides are per-session log folds: the effective sandbox mode i
|
||||
|
||||
## Decision
|
||||
|
||||
The shared in-process driver (`startInProcessRun` in `packages/subagent/subagent-inprocess`) snapshots the parent's policy overrides at delegation and stamps them onto the child as ordinary log events inside the child's FIRST turn:
|
||||
The shared in-process driver (`startInProcessRun` in `packages/subagent/subagent-inprocess`) captures the parent's policy overrides synchronously at delegation and carries them into the child's IMMUTABLE session header as creation meta — the `delegationDepth` precedent:
|
||||
|
||||
- **Capture synchronously at delegation, stamp at first `agent/prompt-submit`.** The driver reads `overrideOf(parent.session)` for both knobs BEFORE its first await — the delegation moment is the snapshot point, so a parent switch racing the asynchronous child creation belongs to the parent's future, not the child — and installs a one-shot child-scoped `agent/prompt-submit` listener during the creation transaction's setup window, PREPENDED so a veto-capable listener (a denying UserPromptSubmit hook) cannot close the first turn without the stamp. Prompt-submit runs after `turn/start` and before prompt assembly, so the stamped events are turn-enclosed (durable — a bare between-turn event is crash-tail garbage on reload) and visible to the child's very first request (an inherited `'never'` reaches the child's first system prompt). An injection-triggered first turn (a SessionStart hook racing the prompt) cannot starve the listener: injection turns dispatch no prompt-submit and no model request, so the stamp still lands before the child's first model request.
|
||||
- **Only the override chain is copied, through the canonical write paths.** `overrideOf(session)` is the fold alone — never the deployment/configured default — so an unswitched parent stamps nothing and a resumed child keeps following the LIVE default; `stampOverride(child, value)` appends via `setSandboxMode`/`setApprovalPolicy` unless the child already folds to the value. The driver consumes both services opportunistically (`ctx.get`, type-only imports): compositions without them delegate policy-free, unchanged.
|
||||
- **Fork stale-seed precedence falls out of log order.** The stamped event lands after any switch the seed carried, so the existing last-event-wins fold resolves the child's mode with no new precedence machinery; an equal seed-carried override is deduplicated instead of re-stamped.
|
||||
- **Nesting composes by construction.** A grandchild's stamp folds its parent-the-child's log, which already contains the child's stamped (or self-switched) override — the chain collapses one level per delegation, at any depth. One-shot `allowed-once` escalation grants never enter any log, so they can never leak down the chain.
|
||||
- **Capture synchronously at delegation, persist in the creation-time header.** The driver reads `overrideOf(parent.session)` for both knobs BEFORE its first await — the delegation moment is the snapshot point, so a parent switch racing the asynchronous child creation belongs to the parent's future, not the child — and stamps the captured values into the child's creation `meta` (`sandboxMode`/`approvalPolicy` on `SessionHeader`). The baseline is durable from the moment the session exists: no listener ordering can starve it (a denying UserPromptSubmit hook that vetoes the first prompt changes nothing), and no crash window can lose it — the decisive case being an idle SessionStart-style injection that persists a complete turn before any prompt turn opens, after which a first-turn event would not yet exist while the session already looks resumable.
|
||||
- **Only the override chain is copied, and the owners validate on read.** `overrideOf(session)` — on `SandboxPolicyService` and `ApprovalService` — resolves `fold(events past the seed boundary) ?? header baseline`, never the deployment/configured default: an unswitched parent writes no baseline, so its children keep following the LIVE default across restarts. The header fields are neutral strings at the session boundary; each policy owner validates against its closed vocabulary on every read (durable-boundary validation) and throws on foreign values. The driver consumes both services opportunistically (`ctx.get`, type-only imports): compositions without them delegate policy-free, unchanged.
|
||||
- **Fork stale-seed precedence falls out of the seed boundary.** A fork seed may carry the parent's OLD switch events; `overrideOf` folds only events past `header.seedLength`, so seed-carried history is subsumed by the delegation-time baseline while a switch the child makes ITSELF still outranks it. The log stays free of synthetic events — the header is the baseline's one home, and the canonical `setSandboxMode`/`setApprovalPolicy` write paths remain reserved for real runtime switches.
|
||||
- **Nesting composes by construction.** A grandchild's capture resolves its parent-the-child's chain (own fold ?? baseline), so the chain collapses one level per delegation, at any depth. One-shot `allowed-once` escalation grants never enter a log or header, so they can never leak down the chain.
|
||||
|
||||
### What a blocked child experiences
|
||||
|
||||
A confined child that hits the wall gets the ordinary denial marker; an escalation retry resolves through the real approval waterfall, where no answerer owns an in-process child, to the distinct fail-closed reason (`no approval channel is available`). The recovery path is reporting the denial upward: the parent — owned by an editor — escalates in its own session or re-delegates after the user widens the mode. An inherited `'never'` skips even that wasted retry: the child's first system prompt already says not to request escalation.
|
||||
A confined child that hits the wall gets the ordinary denial marker; an escalation retry resolves through the real approval waterfall, where no answerer owns an in-process child, to the distinct fail-closed reason (`no approval channel is available`). The recovery path is reporting the denial upward: the parent — owned by a controller that can answer — escalates in its own session or re-delegates after the user widens the mode. An inherited `'never'` skips even that wasted retry: the child's first system prompt already says not to request escalation.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **A `sandboxMode`/`approvalPolicy` baseline in `SessionHeader` meta (the `delegationDepth` precedent)** — rejected: it survives the one corner the event approach loses (a child hard-killed before its first `turn/end` and then resumed loses the stamp), but that child has completed nothing and has no resume value, while the header field costs a session-format extension, a durable-boundary validation path, seed-slicing precedence logic in every fold consumer (`resolve()`, pty-local, permission display), and a second home for policy state. The event approach changes no fold, no format, and no consumer.
|
||||
- **Stamping the inherited override as `sandbox/mode`/`approval/policy` events inside the child's first turn (the shipped first iteration)** — superseded: it kept the log-as-store idiom with zero format changes, but review surfaced a durability hole the turn-enclosure contract cannot patch — an idle SessionStart-style injection persists a complete one-shot turn BEFORE any prompt turn opens, so a crash in that window leaves a resumable-looking child with no inherited policy, and no event anchor exists earlier (creation-time appends are crash-tail garbage, injection turns dispatch no waterfall, `session/event` listeners cannot re-append). The header baseline closes every timing window at once and deletes the listener/prepend/dedup machinery the event approach needed.
|
||||
- **Stamping at child creation (outside any turn)** — rejected: the persistence contract commits at turn boundaries, so a pre-turn bare event is truncated as a torn tail on reload; the session invariant suite fails such an append outright.
|
||||
- **Live resolution walking `parentSession` at 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 `approvalPolicy: 'never'` onto every in-process child** — rejected: true today (no answerer owns them) but it forecloses a future child-capable answerer silently and muddies inheritance semantics; inheriting only the parent's override keeps the fail-closed outcome with honest per-request reasons.
|
||||
- **Routing a child's approval asks to the root session's editor** — deferred, unchanged from [the approval-seam Agent Note](2026-07-06-approval-seam.md): the ACP prompt must attach to a streamed tool call, a background child's originating call has already returned, and the bridge would need parent-chain ownership plus the spawning `callId` on the start request. Recorded here so the obstacles are not re-derived.
|
||||
- **Routing a child's approval asks to the root session's controller** — deferred, unchanged from [the approval-seam Agent Note](2026-07-06-approval-seam.md): the ACP prompt must attach to a streamed tool call, a background child's originating call has already returned, and the bridge would need parent-chain ownership plus the spawning `callId` on the start request. Recorded here so the obstacles are not re-derived.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A parent's tightened sandbox mode and `'never'` approval stance now bind spawn children, fork children (regardless of seed timing), and grandchildren; the delegation bypass is closed at every depth. Pinned by the real-wall suite in `packages/subagent/subagent-inprocess/tests/inheritance.spec.ts` (a scripted-model child hitting the real `dsh-fs-sandbox` fence through the real `write` tool, asserted on disk state and denial markers — including the delegation-vs-late-switch race and a veto-capable prompt-submit listener) and the `overrideOf`/`stampOverride` contract tests in the two service suites.
|
||||
- The stamped override is the child's own durable record: resume replays it like any switch, and the child may later be switched independently without the driver re-stamping over it (one-shot listener + fold dedup).
|
||||
- Accepted limits: a parent switch made while a child is already running does not propagate (snapshot semantics); a child hard-killed before its first `turn/end` loses the stamp on resume (worthless-resume corner, recorded above); out-of-process backends (`subagent-acp`, subprocess children) inherit nothing here — their policy belongs to the child harness's own deployment, the sandbox Agent Note's deferred phase.
|
||||
- `dsh-subagent-inprocess` now declares `dsh-sandbox-policy` and `dsh-user-approval` as peers for the `ctx.get` typing; both remain runtime-optional.
|
||||
- A parent's tightened sandbox mode and `'never'` approval stance now bind spawn children, fork children (regardless of seed timing), and grandchildren; the delegation bypass is closed at every depth, with no first-turn timing window (veto, injection, crash). Pinned by the real-wall suite in `packages/subagent/subagent-inprocess/tests/inheritance.spec.ts` (a scripted-model child hitting the real `dsh-fs-sandbox` fence through the real `write` tool, asserted on disk state and denial markers — including the delegation-vs-late-switch race, a veto-capable prompt-submit listener, and header durability before any child turn) and the `overrideOf` contract tests in the two service suites (baseline read, seed-boundary precedence, closed-vocabulary rejection).
|
||||
- The baseline rides `SessionHeader` through both persistence backends (a JSONL header-line field; SQLite `sessions` columns with `SCHEMA_VERSION` bumped to 11 — pre-release, no migration), so resume restores it like `delegationDepth`; the child may later be switched independently, its own post-seed events outranking the baseline.
|
||||
- Accepted limits: a parent switch made while a child is already running does not propagate (snapshot semantics); out-of-process backends (`subagent-acp`, subprocess children) inherit nothing here — their policy belongs to the child harness's own deployment, the sandbox Agent Note's deferred phase.
|
||||
- `dsh-subagent-inprocess` declares `dsh-sandbox-policy` and `dsh-user-approval` as peers for the `ctx.get` typing; both remain runtime-optional. `SessionHeader` gains two neutral optional string fields; `SESSION_FORMAT_VERSION` stays 0 (additive, pre-release).
|
||||
|
||||
@@ -10,28 +10,28 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
共享的进程内驱动器(`packages/subagent/subagent-inprocess` 中的 `startInProcessRun`)在委派时快照父级的策略覆盖项,并在子 agent 的第一个轮次内把它们作为普通日志事件盖章写入子会话:
|
||||
共享的进程内驱动器(`packages/subagent/subagent-inprocess` 中的 `startInProcessRun`)在委派时同步捕获父级的策略覆盖项,并将其作为创建元数据带入子 agent 不可变的会话头——沿用 `delegationDepth` 先例:
|
||||
|
||||
- **委派时同步捕获,首个 `agent/prompt-submit` 时盖章。**驱动器在自己的第一个 await 之前就为两个策略旋钮读取 `overrideOf(parent.session)`——委派时刻即快照点,因此与异步的子 agent 创建过程赛跑的父级切换属于父级的未来,而非子 agent——并在创建事务的 setup 窗口内安装一个一次性的、限定子 agent 作用域的 `agent/prompt-submit` 监听器,且采用前置安装,使得具备否决能力的监听器(会作出拒绝的 UserPromptSubmit 钩子)无法在未盖章的情况下结束第一个轮次。prompt-submit 阶段在 `turn/start` 之后、提示词组装之前运行,因此盖章事件被包围在轮次内(具备持久性:轮次之间的裸事件在重新加载时只是崩溃残留的尾部垃圾),并且对子 agent 的第一次请求可见(继承来的 `'never'` 能进入子 agent 的第一份系统提示词)。由注入触发的第一个轮次(SessionStart 钩子与提示词赛跑)不会饿死该监听器:注入轮次既不派发 prompt-submit 也不发起模型请求,因此盖章仍会落在子 agent 的第一次模型请求之前。
|
||||
- **只复制覆盖链,且全部走规范写入路径。**`overrideOf(session)` 只是折叠本身——从不包含部署/配置默认值——因此未切换过的父级不盖任何章,恢复后的子 agent 继续跟随实时默认值;`stampOverride(child, value)` 通过 `setSandboxMode`/`setApprovalPolicy` 追加,除非子 agent 已折叠出该值。驱动器以可选方式消费这两个服务(`ctx.get`,仅类型导入):未挂载它们的组合照旧进行无策略委派,行为不变。
|
||||
- **fork 陈旧种子的优先级由日志顺序自然得出。**盖章事件落在种子携带的任何切换之后,因此既有的「最后一个事件生效」折叠即可解析出子 agent 的模式,无需新增优先级机制;种子已携带相同覆盖项时会去重,而不会重复盖章。
|
||||
- **嵌套按构造即可组合。**孙代 agent 盖章时折叠的是其父级(即上一层的子 agent)的日志,而该日志已经包含这个子 agent 被盖章(或自行切换)的覆盖项:这条链在每层委派处收拢一级,任意深度均成立。一次性的 `allowed-once` 升级授权从不进入任何日志,因此永远不可能沿链向下泄漏。
|
||||
- **委派时同步捕获,持久化在创建时的会话头中。**驱动器在自己的第一个 await 之前就为两个策略旋钮读取 `overrideOf(parent.session)`——委派时刻即快照点,因此与异步的子 agent 创建过程赛跑的父级切换属于父级的未来,而非子 agent——并把捕获值盖章写入子 agent 的创建 `meta`(`SessionHeader` 上的 `sandboxMode`/`approvalPolicy`)。该基线从会话存在的那一刻起就具备持久性:任何监听器顺序都不可能饿死它(即便一个作出拒绝的 UserPromptSubmit 钩子否决了第一个提示词,也不会产生任何影响),任何崩溃窗口也不可能丢失它——决定性的场景是空闲时的 SessionStart 式注入在任何提示词轮次开启之前就持久化了一个完整轮次,在那之后第一个轮次内的事件尚不存在,而会话已经看起来可以恢复。
|
||||
- **只复制覆盖链,且由策略 owner 在读取时校验。**`overrideOf(session)`——位于 `SandboxPolicyService` 与 `ApprovalService` 上——解析为 `fold(events past the seed boundary) ?? header baseline`,从不包含部署/配置默认值:未切换过的父级不写入任何基线,因此其子 agent 跨重启继续跟随实时默认值。这两个会话头字段在会话边界上只是中性字符串;每个策略 owner 在每次读取时都按自己的封闭词汇校验(持久边界校验),遇到词汇之外的值即抛出异常。驱动器以可选方式消费这两个服务(`ctx.get`,仅类型导入):未挂载它们的组合照旧进行无策略委派,行为不变。
|
||||
- **fork 陈旧种子的优先级由种子边界自然得出。**fork 种子可能携带父级旧的切换事件;`overrideOf` 只折叠 `header.seedLength` 之后的事件,因此种子携带的历史已被委派时的基线所涵盖,而子 agent 自己做出的切换仍然优先于基线。日志中不含任何合成事件——会话头是基线的唯一存放处,规范写入路径 `setSandboxMode`/`setApprovalPolicy` 仍然只留给真实的运行时切换。
|
||||
- **嵌套按构造即可组合。**孙代 agent 捕获时解析的是其父级(即上一层的子 agent)的覆盖链(自身折叠 ?? 基线),这条链在每层委派处收拢一级,任意深度均成立。一次性的 `allowed-once` 升级授权从不进入任何日志或会话头,因此永远不可能沿链向下泄漏。
|
||||
|
||||
### 被拦住的子 agent 会经历什么
|
||||
|
||||
受限子 agent 撞上围栏时得到的是普通拒绝标记;升级重试会经过真实的审批 waterfall(瀑布式事件)解析,而其中没有任何应答器认领进程内子 agent,最终落到那个独立的 fail-closed 原因(`no approval channel is available`)。恢复路径是把拒绝向上汇报:父 agent 由编辑器持有,可以在自己的会话里发起升级,或在用户放宽模式后重新委派。继承来的 `'never'` 连这次注定无效的重试都会省去:子 agent 的第一份系统提示词已经写明不要请求升级。
|
||||
受限子 agent 撞上围栏时得到的是普通拒绝标记;升级重试会经过真实的审批 waterfall(瀑布式事件)解析,而其中没有任何应答器认领进程内子 agent,最终落到那个独立的 fail-closed 原因(`no approval channel is available`)。恢复路径是把拒绝向上汇报:父 agent 由一个能够应答的控制方持有,可以在自己的会话里发起升级,或在用户放宽模式后重新委派。继承来的 `'never'` 连这次注定无效的重试都会省去:子 agent 的第一份系统提示词已经写明不要请求升级。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
- **在 `SessionHeader` 的 meta 中放 `sandboxMode`/`approvalPolicy` 基线(沿用 `delegationDepth` 先例)**:不予采纳。它确实能扛住事件方案唯一丢失的边角场景(子 agent 在第一个 `turn/end` 前被强制杀死、随后又被恢复时会丢失盖章),但那样的子 agent 尚未完成任何工作,恢复毫无价值;而该会话头字段的代价是一次会话格式扩展、一条持久边界上的校验路径、每个折叠消费方(`resolve()`、pty-local、权限展示)里的种子切片优先级逻辑,以及策略状态的第二个存放处。事件方案不改动任何折叠、任何格式、任何消费方。
|
||||
- **在子 agent 的第一个轮次内,把继承的覆盖项作为 `sandbox/mode`/`approval/policy` 事件盖章写入(已合入的第一版实现)**:已被取代。它保住了「日志即存储」的惯用法,零格式变更,但评审发现了一个轮次封闭契约无法修补的持久性漏洞:空闲时的 SessionStart 式注入会在任何提示词轮次开启之前就持久化一个完整的一次性轮次,在该窗口内崩溃会留下一个看似可恢复、却没有任何继承策略的子 agent,而且不存在更早的事件锚点(创建时的追加只是崩溃残留的尾部垃圾,注入轮次不派发任何 waterfall,`session/event` 监听器也无法重入追加)。会话头基线一举关闭所有时序窗口,并删除了事件方案所需的监听器/前置安装/去重机制。
|
||||
- **在子 agent 创建时(任何轮次之外)盖章**:不予采纳。持久化契约在轮次边界提交,因此轮次开始前的裸事件在重新加载时会被当作撕裂尾部截断;会话不变量测试套件会直接判这种追加失败。
|
||||
- **每次调用时沿 `parentSession` 实时解析**:不予采纳。这会打破「两个会话永远看不到彼此状态」的隔离不变量,要求父会话在子 agent 的整个生命周期内保持加载,还会让父级在子 agent 运行途中做的切换追溯性地改变一个正在运行的子 agent。委派时快照才是本设计的语义:子 agent 保持它被交付时的策略;取消后重新 spawn 即可拿到收紧后的策略。
|
||||
- **给每个进程内子 agent 强制设置 `approvalPolicy: 'never'`**:不予采纳。这在今天是事实(没有应答器认领它们),但它会静默排除未来能够服务子 agent 的应答器,并搅浑继承语义;只继承父级的覆盖项既保住 fail-closed 结果,又让每次请求的拒绝原因保持诚实。
|
||||
- **把子 agent 的审批请求路由给根会话的编辑器**:继续延后,结论与[审批 seam Agent Note](2026-07-06-approval-seam.md) 相比没有变化:ACP 提示必须附着在一个流式工具调用上,后台子 agent 的发起调用早已返回,而且桥接器还需要父链所有权以及 start 请求上携带发起 spawn 的 `callId`。在此记录,以免这些障碍被再次推导。
|
||||
- **把子 agent 的审批请求路由给根会话的控制方**:继续延后,结论与[审批 seam Agent Note](2026-07-06-approval-seam.md) 相比没有变化:ACP 提示必须附着在一个流式工具调用上,后台子 agent 的发起调用早已返回,而且桥接器还需要父链所有权以及 start 请求上携带发起 spawn 的 `callId`。在此记录,以免这些障碍被再次推导。
|
||||
|
||||
## 后果
|
||||
|
||||
- 父级收紧后的沙箱模式与 `'never'` 审批立场现在会约束 spawn 子 agent、fork 子 agent(无论种子时机如何)与孙代 agent;委派旁路在每一层深度都已封死。该行为由 `packages/subagent/subagent-inprocess/tests/inheritance.spec.ts` 中的真实围栏测试套件钉住(脚本化模型驱动的子 agent 通过真实 `write` 工具撞上真实的 `dsh-fs-sandbox` 围栏,按落盘状态与拒绝标记断言——其中包括委派与延迟切换之间的竞态用例,以及一个具备否决能力的 prompt-submit 监听器用例),并由两个服务各自测试套件中的 `overrideOf`/`stampOverride` 契约测试钉住。
|
||||
- 盖章写入的覆盖项是子 agent 自己的持久记录:恢复时它像任何一次切换一样被回放;子 agent 之后仍可被独立切换,驱动器不会重新盖章覆盖它(一次性监听器加折叠去重)。
|
||||
- 已接受的限制:子 agent 已在运行时父级再做的切换不会传播(快照语义);子 agent 在第一个 `turn/end` 前被强制杀死后,恢复时会丢失盖章(恢复无价值的边角场景,上文已记录);进程外后端(`subagent-acp`、子进程形态的子 agent)在这里不继承任何内容:它们的策略归子 harness 自身的部署所有,属于沙箱 Agent Note 中延后的阶段。
|
||||
- `dsh-subagent-inprocess` 现在将 `dsh-sandbox-policy` 与 `dsh-user-approval` 声明为对等依赖(peer dependency),以支撑 `ctx.get` 的类型;两者在运行时仍然可选。
|
||||
- 父级收紧后的沙箱模式与 `'never'` 审批立场现在会约束 spawn 子 agent、fork 子 agent(无论种子时机如何)与孙代 agent;委派旁路在每一层深度都已封死,且不存在任何第一轮次的时序窗口(否决、注入、崩溃)。该行为由 `packages/subagent/subagent-inprocess/tests/inheritance.spec.ts` 中的真实围栏测试套件钉住(脚本化模型驱动的子 agent 通过真实 `write` 工具撞上真实的 `dsh-fs-sandbox` 围栏,按落盘状态与拒绝标记断言——其中包括委派与延迟切换之间的竞态用例、一个具备否决能力的 prompt-submit 监听器用例,以及子 agent 任何轮次开始前的会话头持久性用例),并由两个服务各自测试套件中的 `overrideOf` 契约测试钉住(基线读取、种子边界优先级、封闭词汇拒绝)。
|
||||
- 基线随 `SessionHeader` 通过两个持久化后端存储(JSONL 头部行字段;SQLite `sessions` 表中的列,`SCHEMA_VERSION` 提升到 11——预发布阶段,无迁移),因此恢复时它像 `delegationDepth` 一样被还原;子 agent 之后仍可被独立切换,其自身种子之后的事件优先于基线。
|
||||
- 已接受的限制:子 agent 已在运行时父级再做的切换不会传播(快照语义);进程外后端(`subagent-acp`、子进程形态的子 agent)在这里不继承任何内容:它们的策略归子 harness 自身的部署所有,属于沙箱 Agent Note 中延后的阶段。
|
||||
- `dsh-subagent-inprocess` 将 `dsh-sandbox-policy` 与 `dsh-user-approval` 声明为对等依赖(peer dependency),以支撑 `ctx.get` 的类型;两者在运行时仍然可选。`SessionHeader` 新增两个中性的可选字符串字段;`SESSION_FORMAT_VERSION` 保持为 0(仅新增字段,预发布阶段)。
|
||||
|
||||
Reference in New Issue
Block a user