/** Policy inheritance through child session events appended before publication. */ import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { mkdtemp, readFile, realpath, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { Context } from 'cordis' import type { Agent } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' import SandboxedFileSystem from '@deepseek-ai/dsh-fs-sandbox' import type { ContentBlock } from '@deepseek-ai/dsh-llm' import SandboxPolicyService, { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy' import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session' import * as ToolFs from '@deepseek-ai/dsh-tool-fs' import ApprovalService, { setApprovalPolicy } from '@deepseek-ai/dsh-user-approval' import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent' import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' import { startInProcessRun } from '../src/index.ts' type Script = ConstructorParameters[0] const READ_ONLY_DENIAL = '[sandbox: file access denied under read-only mode]' const contexts: Context[] = [] let workspace: string beforeEach(async () => { workspace = await realpath(await mkdtemp(join(tmpdir(), 'dsh-inherit-'))) }) afterEach(async () => { for (const ctx of contexts.splice(0).reverse()) await ctx.fiber.dispose() await rm(workspace, { recursive: true, force: true }) }) async function setupWalled(script: Script): Promise<{ ctx: Context; parent: Agent }> { const ctx = new Context() contexts.push(ctx) await mountAgentLoopTestDependencies(ctx) await ctx.plugin(SandboxPolicyService, { mode: 'workspace-write', workspaceRoot: workspace }) await ctx.plugin(SandboxedFileSystem, { cwd: workspace }) await ctx.plugin(ToolFs) await ctx.plugin(ApprovalService) await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], new MockAdapter(script)) const parent = ctx.agentLoop.create( SessionId('parent'), { provider: 'mock', model: 'mock' }, { cwd: workspace }, ) return { ctx, parent } } function spawnRequest(parent: Agent) { return { label: 'child task', prompt: [{ type: 'text' as const, text: 'child task' }], parent, signal: new AbortController().signal, descriptor: snapshotSubagentDescriptor({ mode: 'one-shot', provider: 'spawn', label: 'child task', }), } } function toolResultTexts(agent: Agent): string[] { return agent.session.events .filter((event): event is SessionEvent<'tool/result'> => event.type === 'tool/result') .map(event => event.data.message.content .flatMap(block => block.content) .filter((block): block is Extract => block.type === 'text') .map(block => block.text) .join('')) } describe('in-process policy inheritance', () => { it('records parent overrides before publishing a spawn child', async () => { const script: Script = [] const { ctx, parent } = await setupWalled(script) const blocked = join(workspace, 'spawn-blocked.txt') setSandboxMode(parent.session, 'read-only') setApprovalPolicy(parent.session, 'never') const parentLogLength = parent.session.events.length script.push( toolCallResponse('write', 'write', { file_path: blocked, content: 'escaped' }), textResponse('child done'), ) const run = await startInProcessRun(spawnRequest(parent), {}) try { const result = await run.result const child = run.localAgent as Agent await expect(readFile(blocked, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }) expect(toolResultTexts(child).join('\n')).toContain(READ_ONLY_DENIAL) expect(result.stopReason).toBe('completed') expect(child.session.events.slice(0, 2)).toMatchObject([ { type: 'sandbox/mode', seq: 0, data: { mode: 'read-only', source: 'delegation' } }, { type: 'approval/policy', seq: 1, data: { policy: 'never', source: 'delegation' } }, ]) expect(child.session.firstLiveSeq).toBe(0) expect(child.session.header.seedLength).toBeUndefined() expect(ctx.sandboxPolicy.overrideOf(child.session)).toBe('read-only') expect(ctx.approval.overrideOf(child.session)).toBe('never') const request = child.session.events.find( (event): event is SessionEvent<'request/header'> => event.type === 'request/header', ) const runtimeContext = child.session.events.find( (event): event is SessionEvent<'user/message'> => event.type === 'user/message' && event.data.source.kind === 'plugin' && event.data.source.plugin === '@deepseek-ai/dsh-system-prompt', ) if (request === undefined || runtimeContext === undefined) throw new Error('child request lacks its runtime policy context') expect(runtimeContext.seq).toBeLessThan(request.seq) const contextText = runtimeContext.data.content .filter((block): block is Extract => block.type === 'text') .map(block => block.text) .join('\n') expect(contextText).toContain('Current DSH file policy: read-only') expect(contextText).toContain('Approval prompts are disabled') expect(request.data.header.system).not.toContain('Approval prompts are disabled') expect(parent.session.events).toHaveLength(parentLogLength) } finally { await run.dispose() } }) it('places inherited events after a fork prefix so fresh policy wins stale seed state', async () => { const script: Script = [] const { ctx, parent } = await setupWalled(script) const blocked = join(workspace, 'fork-blocked.txt') setSandboxMode(parent.session, 'workspace-write') const seed = [...parent.session.events] setSandboxMode(parent.session, 'read-only') script.push( toolCallResponse('write', 'write', { file_path: blocked, content: 'escaped' }), textResponse('child done'), ) const run = await startInProcessRun(spawnRequest(parent), { seed }) try { await run.result const child = run.localAgent as Agent expect(child.session.header.seedLength).toBe(1) expect(child.session.firstLiveSeq).toBe(seed.length) // seq 1 is the constructor's end-seed marker. expect(child.session.events.filter(event => event.type === 'sandbox/mode')).toMatchObject([ { seq: 0, data: { mode: 'workspace-write' } }, { seq: 2, data: { mode: 'read-only', source: 'delegation' } }, ]) await expect(readFile(blocked, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }) expect(ctx.sandboxPolicy.overrideOf(child.session)).toBe('read-only') setSandboxMode(child.session, 'danger-full-access') expect(ctx.sandboxPolicy.overrideOf(child.session)).toBe('danger-full-access') } finally { await run.dispose() } }) it('captures policy at delegation before asynchronous child creation', async () => { const script: Script = [textResponse('child done')] const { ctx, parent } = await setupWalled(script) setSandboxMode(parent.session, 'read-only') const starting = startInProcessRun(spawnRequest(parent), {}) setSandboxMode(parent.session, 'danger-full-access') const run = await starting try { await run.result const child = run.localAgent as Agent expect(ctx.sandboxPolicy.overrideOf(parent.session)).toBe('danger-full-access') expect(ctx.sandboxPolicy.overrideOf(child.session)).toBe('read-only') } finally { await run.dispose() } }) it('does not freeze deployment defaults into an unswitched child', async () => { const script: Script = [] const { parent } = await setupWalled(script) const allowed = join(workspace, 'default-allowed.txt') script.push( toolCallResponse('write', 'write', { file_path: allowed, content: 'fine' }), textResponse('child done'), ) const run = await startInProcessRun(spawnRequest(parent), {}) try { await run.result const child = run.localAgent as Agent expect(await readFile(allowed, 'utf8')).toBe('fine') expect(child.session.events.some( event => event.type === 'sandbox/mode' || event.type === 'approval/policy', )).toBe(false) expect(child.session.firstLiveSeq).toBe(0) } finally { await run.dispose() } }) })