Browse Source

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.
Hypatia May 2 tháng trước cách đây
mục cha
commit
5173678543

+ 2 - 2
.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.i18n.yaml

@@ -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

+ 1 - 1
.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md

@@ -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.

+ 1 - 1
.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.zh.md

@@ -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 - 2
.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.i18n.yaml

@@ -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

+ 1 - 1
.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.md

@@ -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.

+ 1 - 1
.agents/notes/implemented/feature/2026-08-10-continuable-subagent-policy-inheritance.zh.md

@@ -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`);跨进程策略传播仍不在范围内。

+ 0 - 10
packages/subagent/subagent-inprocess/package.json

@@ -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:^",

+ 0 - 6
packages/subagent/subagent-inprocess/tsconfig.json

@@ -32,14 +32,8 @@
     {
       "path": "../../core/tools"
     },
-    {
-      "path": "../../sandbox/sandbox-policy"
-    },
     {
       "path": "../../support/invariants"
-    },
-    {
-      "path": "../../interaction/user-approval"
     }
   ]
 }

+ 3 - 0
packages/subagent/subagent/src/child-agent.ts

@@ -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. */

+ 36 - 0
packages/subagent/subagent/tests/continuation-inheritance.spec.ts

@@ -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')