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

View File

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

View File

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

View File

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

View File

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