feat(sandbox): cross-family file sandbox — one policy home, sandboxed fs provider, fs escalation parity
Extend SandboxMode enforcement from bash to the filesystem tools, the sandbox RFC's deferred cross-family phase. - dsh-sandbox-policy (new, ctx.sandboxPolicy): the single home for the deployment default mode + workspaceRoot and the per-session override event, renamed bash/sandbox-mode -> sandbox/mode and moved here with its fold/setter. Decouples the bash seam from dsh-session. - dsh-fs-sandbox (new): SandboxedFileSystem extends LocalFileSystem and fences write/edit by the per-call mode (read-only denies, workspace-write contains to the workspace + temp roots via the shared writableRoots, danger passes through); reads pass through. Structured FS_SANDBOX_DENIED; in-lock parent re-canonicalization. A policy fence in trusted code, not a kernel boundary. - dsh-sandbox: the shared escalation kit (writableRoots, the strictly-wider ladder, denial/hint markers, approveEscalation) both tool families use; approveEscalation takes a structural approver so dsh-sandbox gains no approval/agent dependency, and both tools stay duplication-free. - tool-fs: write/edit advertise sandbox_permissions/justification under a confining ctx.fs, map FS_SANDBOX_DENIED to the shared [sandbox: ...] marker, and resolve the same one-approved-wider retry. - examples/acp-agent: composes sandbox-policy + fs-sandbox, drops the gating that disabled the fs stack under confined modes. RFC docs/rfc/implemented/feature/2026-07-14-cross-family-fs-sandbox.md; the old sandbox RFC's In-process/deferred/FAQ sections updated to shipped fact.
This commit is contained in:
@@ -4,7 +4,7 @@ import { join } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import { BashExecutor, BashTaskId, setSandboxMode } from '@deepseek-ai/dsh-bash'
|
||||
import { BashExecutor, BashTaskId } from '@deepseek-ai/dsh-bash'
|
||||
import type { BashExecRequest, BashExecSpec, BashRunResult, BashTask, BashTaskRead, OwnerToken } from '@deepseek-ai/dsh-bash'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
@@ -15,6 +15,7 @@ import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
|
||||
import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
|
||||
import { SandboxProvider } from '@deepseek-ai/dsh-sandbox'
|
||||
import type { ConfinedArgv } from '@deepseek-ai/dsh-sandbox'
|
||||
import { SandboxPolicyService, setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
|
||||
import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
|
||||
import ApprovalService from '@deepseek-ai/dsh-user-approval'
|
||||
import type { ApprovalOutcome } from '@deepseek-ai/dsh-user-approval'
|
||||
@@ -1040,6 +1041,7 @@ describe('sandbox rendering', () => {
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(LocalSandboxProvider, PASSTHROUGH_RUNNER_CONFIG)
|
||||
await ctx.plugin(SandboxPolicyService, {})
|
||||
await ctx.plugin(SandboxBashExecutor, { graceMs: 200 })
|
||||
const bash = ctx.bash as SandboxBashExecutor
|
||||
bash.internals = { spillDir }
|
||||
@@ -1106,6 +1108,7 @@ describe('sandbox rendering', () => {
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(FakeProvider)
|
||||
await ctx.plugin(SandboxPolicyService, {})
|
||||
await ctx.plugin(SandboxBashExecutor, { graceMs: 200 })
|
||||
const bash = ctx.bash as SandboxBashExecutor
|
||||
bash.internals = { spillDir }
|
||||
@@ -1126,6 +1129,7 @@ describe('sandbox rendering', () => {
|
||||
runnerCommand: ['bash', '-c', `printf '${signature}\\n' >&2; exit 125`, 'custom-runner'],
|
||||
runnerFailureSignatures: [signature],
|
||||
})
|
||||
await ctx.plugin(SandboxPolicyService, {})
|
||||
await ctx.plugin(SandboxBashExecutor, { graceMs: 200 })
|
||||
const bash = ctx.bash as SandboxBashExecutor
|
||||
bash.internals = { spillDir }
|
||||
@@ -1144,6 +1148,7 @@ describe('sandbox rendering', () => {
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(LocalSandboxProvider, PASSTHROUGH_RUNNER_CONFIG)
|
||||
await ctx.plugin(SandboxPolicyService, {})
|
||||
await ctx.plugin(SandboxBashExecutor, { graceMs: 200 })
|
||||
const bash = ctx.bash as SandboxBashExecutor
|
||||
bash.internals = { spillDir }
|
||||
@@ -1167,7 +1172,8 @@ describe('sandbox escalation (sandbox_permissions / justification)', () => {
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(LocalSandboxProvider, PASSTHROUGH_RUNNER_CONFIG)
|
||||
await ctx.plugin(SandboxBashExecutor, { graceMs: 200, ...mode !== undefined ? { mode } : {} })
|
||||
await ctx.plugin(SandboxPolicyService, mode !== undefined ? { mode } : {})
|
||||
await ctx.plugin(SandboxBashExecutor, { graceMs: 200 })
|
||||
const bash = ctx.bash as SandboxBashExecutor
|
||||
bash.internals = { spillDir }
|
||||
if (opts.approval === true) await ctx.plugin(ApprovalService, opts.policy !== undefined ? { policy: opts.policy } : {})
|
||||
@@ -1381,7 +1387,7 @@ describe('sandbox escalation (sandbox_permissions / justification)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('per-session sandbox mode (the bash/sandbox-mode fold)', () => {
|
||||
describe('per-session sandbox mode (the sandbox/mode fold)', () => {
|
||||
/** Compose the real sandbox stack (passthrough runner) at a given default mode. */
|
||||
async function setupModal(mode: 'read-only' | 'workspace-write' | 'danger-full-access' = 'read-only', opts: { approval?: boolean } = {}) {
|
||||
const ctx = new Context()
|
||||
@@ -1389,7 +1395,8 @@ describe('per-session sandbox mode (the bash/sandbox-mode fold)', () => {
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(LocalSandboxProvider, PASSTHROUGH_RUNNER_CONFIG)
|
||||
await ctx.plugin(SandboxBashExecutor, { graceMs: 200, mode })
|
||||
await ctx.plugin(SandboxPolicyService, { mode })
|
||||
await ctx.plugin(SandboxBashExecutor, { graceMs: 200 })
|
||||
;(ctx.bash as SandboxBashExecutor).internals = { spillDir }
|
||||
if (opts.approval === true) await ctx.plugin(ApprovalService)
|
||||
await ctx.plugin(ToolBash)
|
||||
|
||||
Reference in New Issue
Block a user