review: symmetric policy-service type imports, drop stale inprocess peers, pin child-switch and fork-default cases

- child-agent.ts declares both policy-service augmentations as explicit
  empty type imports, so removing the ApprovalPolicy import cannot
  silently degrade ctx.get('approval') typing.
- dsh-subagent-inprocess no longer consumes the policy services in src,
  so its optional peers and tsconfig references are dropped; both
  policy-inheritance Agent Notes state the current ownership.
- The continuable suite pins that a later child-side switch beats the
  delegation snapshot and that an unswitched fork parent seeds no
  policy events.
This commit is contained in:
Hypatia May
2026-08-10 14:45:34 +08:00
parent 3639b10c83
commit 5173678543
10 changed files with 47 additions and 24 deletions
@@ -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 .agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md
2026-07-25-subagent-policy-inheritance.md: a2f4d578de857ee63df5e7741c433210d5f1ef86
2026-07-25-subagent-policy-inheritance.zh.md: f069bf290586447afc0b7d46a41ad4de9bfcbe8f
2026-07-25-subagent-policy-inheritance.md: 910581a595f48b356eea9c6242a06159c52b3854
2026-07-25-subagent-policy-inheritance.zh.md: a0edb3c6beb59a9fe8fdfb801ee718f7c034c296
@@ -33,4 +33,4 @@ A confined child gets the ordinary denial marker. No answerer currently owns an
- Spawn, fork, and nested in-process children retain a parent's explicit sandbox and approval overrides. 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 is `workspace-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-subagent` and `dsh-subagent-inprocess` have optional peer types for the two policy services; 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.
- Each delegation adds at most two log-only events. `dsh-subagent` owns the optional peer types for the two policy services — its shared helpers hold the `ctx.get` consumption; 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.
@@ -33,4 +33,4 @@ Status: implemented
- spawn、fork 和嵌套的进程内子 agent 会保留父级显式的沙箱与审批覆盖项。聚焦测试套件证明真实文件系统拒绝、陈旧 fork 优先级、委派时捕获、实时事件边界、默认值省略与上下文释放。
- 无密钥 headless 快照是组装后应用层面的回归测试:只有父级是 `read-only`,部署默认值是 `workspace-write`;若移除捕获,子 agent 的持久化事件与被拒的磁盘写入这两项检查都会失败。
- 每次委派最多增加两条仅日志事件。`dsh-subagent``dsh-subagent-inprocess` 为两个策略服务提供可选 peer 类型;未组合任一服务的组合保持原有行为。进程外子 agent 仍采用自身的部署策略,正在运行的子 agent 不跟随父级后续切换。
- 每次委派最多增加两条仅日志事件。两个策略服务的可选 peer 类型由 `dsh-subagent` 拥有——其共享辅助函数持有 `ctx.get` 消费;未组合任一服务的组合保持原有行为。进程外子 agent 仍采用自身的部署策略,正在运行的子 agent 不跟随父级后续切换。
@@ -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 .agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.md
2026-08-10-continuable-subagent-policy-inheritance.md: 39df910a920e6995ba6048fdd2613d2c216f5ec2
2026-08-10-continuable-subagent-policy-inheritance.zh.md: 2a977eaa9aade189213fdd20a7d888e8e62efb48
2026-08-10-continuable-subagent-policy-inheritance.md: 04bcd0329a4608445b672d75b6c1e56dd265b25a
2026-08-10-continuable-subagent-policy-inheritance.zh.md: 9ef457df814ed848b04f42c5891024a0890bc9dd
@@ -24,6 +24,6 @@ The capture/append pair moved from the one-shot driver into the seam's shared ch
## Consequences
- Default-bundle background delegation (`backgroundMode: continuable`) now inherits a parent's explicit sandbox and approval overrides; compositions without either policy service behave unchanged.
- `dsh-subagent` gains optional peer types on `dsh-sandbox-policy` and `dsh-user-approval` (the `ctx.get` pattern the one-shot driver used); `dsh-subagent-inprocess` keeps its optional peers but delegates to the shared helpers.
- `dsh-subagent` gains optional peer types on `dsh-sandbox-policy` and `dsh-user-approval` (the `ctx.get` pattern the one-shot driver used); `dsh-subagent-inprocess` drops its policy-service peers and type imports entirely and delegates to the shared helpers.
- The continuable suite (`packages/subagent/subagent/tests/continuation-inheritance.spec.ts`) pins fresh-start seeding, pre-await capture, default omission, cold-resume snapshot stability, and fork-seed precedence; the ACP snapshot scenario `subagent-continuable-inheritance` pins the child's delegation event and read-only runtime context through the assembled app and fails when the capture is removed.
- Out-of-process providers (`acp`, `dsh-sdk`, `claude-code`, `codex`) support no continuable children (`prepareContinuable` absent), and their one-shot children keep their own deployment policy (`inheritsParentContext = false`); cross-process policy propagation remains out of scope.
@@ -24,6 +24,6 @@ Status: implemented
## 后果
- 默认组合包的后台委派(`backgroundMode: continuable`)现在会继承父级显式的沙箱与审批覆盖项;未组合任一策略服务的组合保持原有行为。
- `dsh-subagent` 新增针对 `dsh-sandbox-policy``dsh-user-approval` 的可选 peer 类型(即一次性驱动器所用的 `ctx.get` 模式);`dsh-subagent-inprocess` 保留自己的可选 peer,但委托给共享辅助函数。
- `dsh-subagent` 新增针对 `dsh-sandbox-policy``dsh-user-approval` 的可选 peer 类型(即一次性驱动器所用的 `ctx.get` 模式);`dsh-subagent-inprocess` 完全移除自己的策略服务 peer 与类型导入,委托给共享辅助函数。
- 可继续测试套件(`packages/subagent/subagent/tests/continuation-inheritance.spec.ts`)锁定全新启动的种子写入、await 前捕获、默认值省略、冷恢复快照稳定性与 fork 种子优先级;ACP 快照场景 `subagent-continuable-inheritance` 经组装后的应用锁定子级的委派事件与只读运行时上下文,移除捕获时即失败。
- 进程外提供方(`acp``dsh-sdk``claude-code``codex`)不支持可继续子 agent(没有 `prepareContinuable`),其一次性子 agent 保留自身的部署策略(`inheritsParentContext = false`);跨进程策略传播仍不在范围内。
@@ -28,22 +28,12 @@
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-sandbox-policy": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-subagent": "^0.0.1",
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
"@deepseek-ai/dsh-tools": "^0.0.1",
"@deepseek-ai/dsh-user-approval": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
"peerDependenciesMeta": {
"@deepseek-ai/dsh-sandbox-policy": {
"optional": true
},
"@deepseek-ai/dsh-user-approval": {
"optional": true
}
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-loop": "workspace:^",
@@ -32,14 +32,8 @@
{
"path": "../../core/tools"
},
{
"path": "../../sandbox/sandbox-policy"
},
{
"path": "../../support/invariants"
},
{
"path": "../../interaction/user-approval"
}
]
}
@@ -17,7 +17,10 @@ import type { ApprovalPolicy } from '@deepseek-ai/dsh-user-approval'
// Type-only: make `ctx.get('sandboxPolicy')` / `ctx.get('approval')` resolve
// to the policy services when composed — delegation consumes both
// opportunistically (the documented `ctx.get` pattern), never as a hard dep.
// The user-approval side stays an explicit empty import so its augmentation
// does not ride the `ApprovalPolicy` import above.
import type {} from '@deepseek-ai/dsh-sandbox-policy'
import type {} from '@deepseek-ai/dsh-user-approval'
import { delegationDepthOf } from './depth.ts'
/** Thrown when starting a child would exceed the requested depth cap. */
@@ -124,6 +124,42 @@ describe('continuable policy inheritance', () => {
expect(policyEvents(loaded.events)).toEqual([])
})
it('does not freeze deployment defaults into an unswitched fork child either', async () => {
const { ctx, parent } = await setup([textResponse('parent turn'), textResponse('forked child')])
parent.followup(createUserMessage({
content: [{ type: 'text', text: 'parent work' }],
source: { kind: 'user' },
}))
await parent.whenIdle()
const started = await ctx.subagents.startContinuable(startSpec(parent, 'fork'))
await waitNoActivation(ctx, started.childId)
const loaded = await ctx.sessionPersistence.load(started.childId)
expect(loaded.meta.seedLength).toBeGreaterThan(0)
expect(policyEvents(loaded.events)).toEqual([])
})
it('lets a later child-side switch win over the delegation snapshot', async () => {
const { ctx, parent } = await setup([textResponse('child done')])
setSandboxMode(parent.session, 'danger-full-access')
let child: Agent | undefined
ctx.on('agent/created', ({ agent }) => {
if (agent !== parent) child = agent
})
const started = await ctx.subagents.startContinuable(startSpec(parent))
if (child === undefined) throw new Error('expected the continuable child to be created')
expect(ctx.sandboxPolicy.overrideOf(child.session)).toBe('danger-full-access')
// Last event wins: the child's own runtime switch beats the seeded snapshot.
setSandboxMode(child.session, 'read-only')
expect(ctx.sandboxPolicy.overrideOf(child.session)).toBe('read-only')
await waitNoActivation(ctx, started.childId)
const loaded = await ctx.sessionPersistence.load(started.childId)
expect(effectiveSandboxMode(loaded.events)).toBe('read-only')
})
it('cold-resumes on the persisted snapshot without re-capturing the parent', async () => {
const { ctx, parent } = await setup([textResponse('first'), textResponse('after resume')])
setSandboxMode(parent.session, 'read-only')