Merge remote-tracking branch 'origin/master' into codex/enforce-tool-cancellation

# Conflicts:
#	docs/cookbook/adding-a-tool.i18n.yaml
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl
#	packages/bash/tool-bash/src/index.ts
#	packages/core/agent-loop/README.md
#	packages/core/tools/README.md
#	packages/core/tools/tests/scoped.spec.ts
#	packages/fs/tool-fs-search/tests/tools.spec.ts
#	website/zh-CN/api/harness/events.md
#	website/zh-CN/api/harness/tools.md
This commit is contained in:
Tianyi Cui
2026-07-20 23:00:21 +08:00
736 changed files with 22158 additions and 13229 deletions

View File

@@ -24,6 +24,8 @@ import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
import { STREAM_MIN_SIZE } from '../src/read.ts'
import { formatReadOutput } from '../src/read-render.ts'
import type { FileReadOutcome } from '../src/read-render.ts'
import ApprovalService from '@deepseek-ai/dsh-user-approval'
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
const testToolSignal = new AbortController().signal
@@ -583,3 +585,163 @@ describe('read caps are plugin config', () => {
expect('default' in ToolFs).toBe(false)
})
})
describe('sandbox escalation surface (write/edit)', () => {
/** A confining fake `ctx.fs`: reports a default mode, records the per-call mode stamped, and can arm a sandbox denial. */
class SandboxingFakeFs extends FakeFs {
stamped: (SandboxMode | undefined)[] = []
override get sandboxMode(): SandboxMode {
return 'workspace-write'
}
override async writeText(
target: FsTarget,
content: string,
expected?: FsWriteIntent,
_signal?: AbortSignal,
sandboxMode?: SandboxMode,
): Promise<FsWriteOutcome> {
this.stamped.push(sandboxMode)
return super.writeText(target, content, expected)
}
override async editText(
target: FsTarget,
edit: FsEditRequest,
expected?: { version: FsVersion },
_signal?: AbortSignal,
sandboxMode?: SandboxMode,
): Promise<FsEditOutcome> {
this.stamped.push(sandboxMode)
return super.editText(target, edit, expected)
}
}
async function setupConfining(opts: { approval?: boolean } = {}) {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SandboxingFakeFs)
await ctx.plugin(FsPolicy)
if (opts.approval === true) await ctx.plugin(ApprovalService)
await ctx.plugin(ToolFs)
return { ctx, fs: ctx.fs as SandboxingFakeFs }
}
/** A fake agent whose session records appends (the approval audit surface), mid-turn, carrying the given events for the fold. */
function escalationAgent(events: Array<{ type: string; data?: Record<string, unknown> }> = []): object {
return {
id: 'agent-fs-esc',
session: {
header: { version: 0, id: 'sess-fs-esc', createdAt: 0 },
events: [{ type: 'turn/start' }, ...events],
append: (type: string, data: Record<string, unknown>) => { events.push({ type, data }) },
},
}
}
function fsSchema(ctx: Context, name: 'write' | 'edit') {
const schema = ctx.tools.schemas().find(s => s.name === name)
if (!schema) throw new Error(`${name} tool not registered`)
return schema as unknown as { parameters: { properties: Record<string, { enum?: string[] }> } }
}
it('advertises no escalation fields under a non-confining backend', async () => {
const { ctx } = await setup()
expect(ctx.fs.sandboxMode).toBeUndefined()
for (const name of ['write', 'edit'] as const) {
const props = fsSchema(ctx, name).parameters.properties
expect(props['sandbox_permissions']).toBeUndefined()
expect(props['justification']).toBeUndefined()
}
})
it('advertises the closed target vocabulary on write and edit under a confining backend', async () => {
const { ctx } = await setupConfining()
for (const name of ['write', 'edit'] as const) {
const props = fsSchema(ctx, name).parameters.properties
expect(props['sandbox_permissions']?.enum).toEqual(['workspace-write', 'danger-full-access'])
expect(props['justification']).toBeDefined()
}
})
it('a plain write stamps nothing (backend default) and no session override folds without one', async () => {
const { ctx, fs } = await setupConfining()
await call(ctx, 'write', { file_path: 'a.txt', content: 'x' }, escalationAgent())
expect(fs.stamped).toEqual([undefined])
})
it('a standing session override folds onto the stamp', async () => {
const { ctx, fs } = await setupConfining()
await call(ctx, 'write', { file_path: 'a.txt', content: 'x' }, escalationAgent([{ type: 'sandbox/mode', data: { mode: 'read-only' } }]))
expect(fs.stamped).toEqual(['read-only'])
})
it('a denied write maps to the shared marker plus the escalation hint (isError)', async () => {
const { ctx, fs } = await setupConfining()
fs.rejectWith = new FsError('denied', 'FS_SANDBOX_DENIED')
const result = await call(ctx, 'write', { file_path: 'a.txt', content: 'x' }, escalationAgent())
expect(result.isError).toBe(true)
expect(text(result)).toContain('[sandbox: file access denied under workspace-write mode]')
expect(text(result)).toContain('retry this exact operation once with sandbox_permissions')
})
it('a non-FS_SANDBOX_DENIED provider error passes through unchanged', async () => {
const { ctx, fs } = await setupConfining()
fs.rejectWith = new FsError('boom', 'FS_IO_ERROR')
const result = await call(ctx, 'write', { file_path: 'a.txt', content: 'x' }, escalationAgent())
expect(result.isError).toBe(true)
expect(text(result)).toContain('boom')
expect(text(result)).not.toContain('[sandbox:')
})
it('an approved escalation stamps the granted mode onto that write', async () => {
const { ctx, fs } = await setupConfining({ approval: true })
ctx.on('approval/request', () => Promise.resolve('allowed-once' as const))
// Pass a signal so the escalation ask forwards it to the approval request
// (the request rides the tool-execution abort signal).
await ctx.tools.execute({
callId: CallId('call-fs-esc-grant'),
name: 'write',
arguments: { file_path: 'a.txt', content: 'x', sandbox_permissions: 'danger-full-access', justification: 'the test needs it' },
agent: escalationAgent() as never,
signal: new AbortController().signal,
})
expect(fs.stamped).toEqual(['danger-full-access'])
})
it('a rejected escalation fails closed with its own text and never mutates', async () => {
const { ctx, fs } = await setupConfining({ approval: true })
ctx.on('approval/request', () => Promise.resolve('rejected' as const))
const result = await call(ctx, 'edit', { file_path: 'a.txt', old_string: 'x', new_string: 'y', sandbox_permissions: 'danger-full-access', justification: 'the test needs it' }, escalationAgent())
expect(result.isError).toBe(true)
expect(text(result)).toContain('the user rejected escalating this operation to "danger-full-access"')
expect(fs.stamped).toEqual([])
})
it('escalation without an approval service fails closed', async () => {
const { ctx } = await setupConfining()
const result = await call(ctx, 'write', { file_path: 'a.txt', content: 'x', sandbox_permissions: 'danger-full-access', justification: 'why' }, escalationAgent())
expect(result.isError).toBe(true)
expect(text(result)).toContain('no approval service is composed')
})
it('escalation with an approval service but no agent fails closed', async () => {
const { ctx } = await setupConfining({ approval: true })
const result = await call(ctx, 'write', { file_path: 'a.txt', content: 'x', sandbox_permissions: 'danger-full-access', justification: 'why' })
expect(result.isError).toBe(true)
expect(text(result)).toContain('no agent to route it through')
})
it('rejects the escalation argument pairing (one field without the other)', async () => {
const { ctx } = await setupConfining()
const missing = await call(ctx, 'write', { file_path: 'a.txt', content: 'x', sandbox_permissions: 'workspace-write' }, escalationAgent())
expect(missing.isError).toBe(true)
expect(text(missing)).toContain('sandbox_permissions requires a justification')
})
it('sandbox_permissions under a non-confining backend fails closed (unadvertised field still reaches execute)', async () => {
const { ctx } = await setup()
const result = await call(ctx, 'write', { file_path: 'a.txt', content: 'x', sandbox_permissions: 'workspace-write', justification: 'why' }, escalationAgent())
expect(result.isError).toBe(true)
expect(text(result)).toContain('not available in this composition')
})
})