diff --git a/apps/web/tests/shipped-composition.e2e.ts b/apps/web/tests/shipped-composition.e2e.ts index dec962ef80..25da06d173 100644 --- a/apps/web/tests/shipped-composition.e2e.ts +++ b/apps/web/tests/shipped-composition.e2e.ts @@ -22,6 +22,7 @@ import type {} from '@deepseek-ai/dsh-permission-presets' import type {} from '@deepseek-ai/dsh-agent-presets' import type {} from '@deepseek-ai/dsh-commands' import type {} from '@deepseek-ai/dsh-system-prompt' +import type {} from '@deepseek-ai/dsh-terminal' import { launchWebScaffold, readPersistedEvents, type WebScaffold } from './scaffold.ts' import { REPO_ROOT } from './support.ts' @@ -34,7 +35,7 @@ const AUTO_CHILD_OVERLAY_PATH = join(REPO_ROOT, 'apps/web/tests/auto-review-chil const AUTO_PROVIDER = 'shipped-auto-review-test' const AUTO_MODEL = 'same-route' const AUTO_CALL_ID = ToolCallId('shipped-auto-review-denied-delete') -const AUTO_RAW_REASON = ' direct user authorized inspection only\ndeletion scope was not authorized ' +const AUTO_RAW_REASON = ` direct user authorized inspection only\nTEST_ONLY_SECRET_${'x'.repeat(16_384)} ` const AUTO_FINAL_TEXT = 'SHIPPED_AUTO_REVIEW_DENIAL_OBSERVED' const AUTO_CHILD_ONE_SHOT = 'AUTO_CHILD_ONE_SHOT' const AUTO_CHILD_CONTINUABLE = 'AUTO_CHILD_CONTINUABLE' @@ -769,6 +770,7 @@ it('routes one browser-authored Auto request through the same model before a rea const finalModelInput = JSON.stringify(finalMain?.messages) expect(finalModelInput).toContain('Auto review rejected tool \\"bash\\"; its body was not executed') expect(finalModelInput).not.toContain('direct user authorized inspection only') + expect(finalModelInput).not.toContain('TEST_ONLY_SECRET_') const events = agent.session.snapshotEvents() const prompt = events.find((event): event is Extract => ( @@ -790,6 +792,7 @@ it('routes one browser-authored Auto request through the same model before a rea const durableModelResult = JSON.stringify(result?.data.message) expect(durableModelResult).toContain('Auto review rejected tool \\"bash\\"; its body was not executed') expect(durableModelResult).not.toContain('direct user authorized inspection only') + expect(durableModelResult).not.toContain('TEST_ONLY_SECRET_') expect(events.some(event => ( event.type === 'assistant/message' && JSON.stringify(event.data.message).includes(AUTO_FINAL_TEXT) @@ -1035,10 +1038,14 @@ it('withdraws Auto on shipped Loader unload and does not restore migrated live s if (autoEntry === undefined) throw new Error('shipped Auto review Loader entry is missing') const handle = await ctx.agents.create({ sessionId: SessionId('shipped-auto-hot-plug'), - meta: { cwd: scaffold.workspaceCwd }, - setup: agentCtx => ctx.agentPresets.mount(agentCtx).then(() => undefined), + meta: { cwd: scaffold.workspaceCwd, agentPreset: 'minimal' }, + setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined), }) + const terminals = ctx.agentPresets.serviceFor(handle.agent, 'terminals') try { + if (terminals === undefined) throw new Error('shipped minimal preset has no terminal registry') + ctx.permissionPresets.set(handle.agent.session, 'danger-full-access') + const terminal = await terminals.spawn(handle.agent, { type: 'shell', cwd: scaffold.workspaceCwd }) ctx.permissionPresets.set(handle.agent.session, 'auto') expect(ctx.permissionPresets.current(handle.agent.session)).toBe('auto') @@ -1046,6 +1053,16 @@ it('withdraws Auto on shipped Loader unload and does not restore migrated live s await ctx.loader.await() expect(ctx.permissionPresets.names).not.toContain('auto') expect(ctx.permissionPresets.current(handle.agent.session)).toBe('danger-full-access') + expect(ctx.sandboxPolicy.overrideOf(handle.agent.session)).toBe('danger-full-access') + expect(ctx.approval.overrideOf(handle.agent.session)).toBe('never') + expect(terminals.list(handle.agent)).toMatchObject([ + { sessionId: terminal.sessionId, pid: terminal.pid, status: { kind: 'running' } }, + ]) + const sent = await terminals.startSend(handle.agent, terminal.sessionId, { + text: 'echo AUTO_TERMINAL_SURVIVED', submit: true, + }).done + expect(sent.sessionStatus).toEqual({ kind: 'running' }) + expect(sent.viewport).toContain('AUTO_TERMINAL_SURVIVED') await autoEntry.update({ disabled: false }) await ctx.loader.await() @@ -1054,4 +1071,5 @@ it('withdraws Auto on shipped Loader unload and does not restore migrated live s } finally { await handle.dispose() } + expect(terminals?.hasOwnerActivity(handle.agent)).toBe(false) }, 120_000) diff --git a/packages/interaction/auto-review/tests/auto-review.e2e.ts b/packages/interaction/auto-review/tests/auto-review.e2e.ts index cf6ba882cb..f492e843aa 100644 --- a/packages/interaction/auto-review/tests/auto-review.e2e.ts +++ b/packages/interaction/auto-review/tests/auto-review.e2e.ts @@ -161,7 +161,7 @@ async function mount(ctx: Context, workspace: string, dshHome: string): Promise< function orchestrate(ctx: Context, real: boolean) { let script: StreamChunk[][] = [] - let expected: Verdict | undefined + let expected: (Verdict & { reason?: string }) | undefined let mainAgent: Agent | undefined let offeredTool = '' let bodyStarts = 0 @@ -194,7 +194,7 @@ function orchestrate(ctx: Context, real: boolean) { calls: () => reviewCalls, async action( agent: Agent, name: string, args: Record, path: Path, - prompt: string, verdict?: Verdict, + prompt: string, verdict?: Verdict & { reason?: string }, ): Promise<{ events: readonly SessionEvent[]; bodies: number }> { expect(script).toHaveLength(0) mainAgent = agent @@ -369,12 +369,19 @@ it.each(['native', 'ptc-inner'] as const)('feeds denial back, re-reviews a new c }) try { ctx.permissionPresets.set(handle.agent.session, AUTO_PRESET) + const rawReason = ` TEST_ONLY_SECRET_${'x'.repeat(16_384)}\nexact deletion was not authorized ` for (let attempt = 0; attempt < 2; attempt += 1) { const result = await runner.action(handle.agent, 'bash', { command: `rm -- ${quote(target)}` }, path, - 'Inspect only; do not delete existing files.', { risk: 'medium', decision: 'deny' }) + 'Inspect only; do not delete existing files.', { risk: 'medium', decision: 'deny', reason: rawReason }) expect(result.bodies).toBe(0) expect(outcome(result.events, path).denied).toBe(true) + const denials = result.events + .filter(event => event.type === 'tool/result' || event.type === 'tool/code-dispatch') + .map(event => event.data.error) + .filter(error => error?.code === DENIED) + expect(denials).toEqual([{ name: 'AutoReviewDeniedError', code: DENIED, reason: rawReason }]) expect(runner.postToolInputs.at(-1)?.includes('Auto review rejected tool \\"bash\\"; its body was not executed')).toBe(true) + expect(runner.postToolInputs.at(-1)).not.toContain('TEST_ONLY_SECRET_') } const result = await runner.action(handle.agent, 'write', { file_path: join(root, 'analysis.txt'), content: 'analysis\n' }, path, 'Write the local analysis instead.', { risk: 'low', decision: 'allow' }) diff --git a/packages/subagent/subagent-in-process-driver/tests/inheritance.spec.ts b/packages/subagent/subagent-in-process-driver/tests/inheritance.spec.ts index 6dcb9909a7..167a89acda 100644 --- a/packages/subagent/subagent-in-process-driver/tests/inheritance.spec.ts +++ b/packages/subagent/subagent-in-process-driver/tests/inheritance.spec.ts @@ -107,23 +107,30 @@ describe('in-process policy inheritance', () => { }, ) - it('places delegated Full access after an Auto fork prefix', async () => { + it.each([ + { seedPreset: 'auto', preset: 'danger-full-access' }, + { seedPreset: 'danger-full-access', preset: 'auto' }, + ] as const)('captures $preset before child creation and overrides the $seedPreset fork prefix', async ({ seedPreset, preset }) => { const { ctx, parent } = await setupWalled([textResponse('child done')]) - parent.session.append('permission/preset', { preset: 'auto' }) + parent.session.append('permission/preset', { preset: seedPreset }) setSandboxMode(parent.session, 'danger-full-access') const seed = parent.session.snapshotEvents() - parent.session.append('permission/preset', { preset: 'danger-full-access' }) + parent.session.append('permission/preset', { preset }) + let currentPreset: 'auto' | 'danger-full-access' = preset ctx.provide('permissionPresets', { - current: (session: Session) => session === parent.session ? 'danger-full-access' : 'custom', + current: (session: Session) => session === parent.session ? currentPreset : 'custom', } as never) - const run = await startInProcessRun(spawnRequest(parent), { seed }) + const starting = startInProcessRun(spawnRequest(parent), { seed }) + currentPreset = seedPreset + parent.session.append('permission/preset', { preset: seedPreset }) + const run = await starting try { await run.result const child = run.localAgent as Agent expect(child.session.snapshotEvents().filter(event => event.type === 'permission/preset')).toMatchObject([ - { data: { preset: 'auto' } }, - { data: { preset: 'danger-full-access' } }, + { data: { preset: seedPreset } }, + { data: { preset } }, ]) } finally { await run.dispose() diff --git a/packages/subagent/subagent/tests/continuation-inheritance.spec.ts b/packages/subagent/subagent/tests/continuation-inheritance.spec.ts index 27272e8702..93878e4795 100644 --- a/packages/subagent/subagent/tests/continuation-inheritance.spec.ts +++ b/packages/subagent/subagent/tests/continuation-inheritance.spec.ts @@ -89,15 +89,14 @@ function foldedApprovalPolicy(ctx: Context, id: SessionId, events: readonly Sess describe('continuable policy inheritance', () => { it.each(['auto', 'danger-full-access'] as const)( - 'persists the parent %s identity for a DSH in-process child', + 'persists the delegated %s identity across cold resume without reading the parent again', { timeout: 20_000 }, async (preset) => { - const { ctx, parent } = await setup([textResponse('child done')]) + const { ctx, parent } = await setup([textResponse('child done'), textResponse('resumed child done')]) parent.session.append('permission/preset', { preset }) setSandboxMode(parent.session, 'danger-full-access') - ctx.provide('permissionPresets', { - current: (session: Session) => session === parent.session ? preset : 'custom', - } as never) + const current = vi.fn((session: Session) => session === parent.session ? preset : 'custom') + ctx.provide('permissionPresets', { current } as never) const started = await ctx.subagents.startContinuable(startSpec(parent)) await waitNoActivation(ctx, started.childId) @@ -112,29 +111,49 @@ describe('continuable policy inheritance', () => { { type: 'approval/policy', data: { policy: 'never', source: 'delegation' } }, { type: 'permission/preset', data: { preset } }, ]) + expect(current).toHaveBeenCalledExactlyOnceWith(parent.session) + parent.session.append('permission/preset', { preset: preset === 'auto' ? 'danger-full-access' : 'auto' }) + current.mockImplementation(() => { throw new Error('cold resume must not read parent permission') }) + await queueHostSubagentPrompt( + ctx.subagents, parent, started.childId, + [{ type: 'text', text: 'continue please' }], { kind: 'user' }, new AbortController().signal, + ) + await waitNoActivation(ctx, started.childId) + const resumed = await loadStoredSession(ctx.sessionPersistence, started.childId) + expect(resumed.events.filter(event => event.type === 'permission/preset')).toMatchObject([ + { data: { preset } }, + ]) + expect(current).toHaveBeenCalledTimes(1) }, ) - it('places delegated Full access after an Auto fork prefix', { timeout: 20_000 }, async () => { + it.each([ + { seedPreset: 'auto', preset: 'danger-full-access' }, + { seedPreset: 'danger-full-access', preset: 'auto' }, + ] as const)('captures $preset before child creation and overrides the $seedPreset fork prefix', { timeout: 20_000 }, async ({ seedPreset, preset }) => { const { ctx, parent } = await setup([textResponse('parent turn'), textResponse('forked child')]) - parent.session.append('permission/preset', { preset: 'auto' }) + parent.session.append('permission/preset', { preset: seedPreset }) setSandboxMode(parent.session, 'danger-full-access') parent.followup(createUserMessage({ content: [{ type: 'text', text: 'parent work' }], source: { kind: 'user' }, })) await parent.whenIdle() - parent.session.append('permission/preset', { preset: 'danger-full-access' }) + parent.session.append('permission/preset', { preset }) + let currentPreset: 'auto' | 'danger-full-access' = preset ctx.provide('permissionPresets', { - current: (session: Session) => session === parent.session ? 'danger-full-access' : 'custom', + current: (session: Session) => session === parent.session ? currentPreset : 'custom', } as never) - const started = await ctx.subagents.startContinuable(startSpec(parent, 'fork')) + const starting = ctx.subagents.startContinuable(startSpec(parent, 'fork')) + currentPreset = seedPreset + parent.session.append('permission/preset', { preset: seedPreset }) + const started = await starting await waitNoActivation(ctx, started.childId) const loaded = await loadStoredSession(ctx.sessionPersistence, started.childId) expect(loaded.events.filter(event => event.type === 'permission/preset')).toMatchObject([ - { data: { preset: 'auto' } }, - { data: { preset: 'danger-full-access' } }, + { data: { preset: seedPreset } }, + { data: { preset } }, ]) })