mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
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.
68 lines
2.8 KiB
TypeScript
68 lines
2.8 KiB
TypeScript
/**
|
|
* Tests for the sandbox-policy home: the deployment default (mode +
|
|
* workspaceRoot) the service exposes, and the per-session `sandbox/mode`
|
|
* override kit (fold + write path) both enforcing families read.
|
|
*/
|
|
|
|
import { resolve } from 'node:path'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { Context } from 'cordis'
|
|
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
|
import SandboxPolicyService, { SANDBOX_MODES, effectiveSandboxMode, setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
|
|
|
|
async function mounted(config: { mode?: 'read-only' | 'workspace-write' | 'danger-full-access'; workspaceRoot?: string } = {}) {
|
|
const ctx = new Context()
|
|
await ctx.plugin(SandboxPolicyService, config)
|
|
return ctx
|
|
}
|
|
|
|
describe('SandboxPolicyService', () => {
|
|
it('defaults to read-only under the process cwd', async () => {
|
|
const ctx = await mounted()
|
|
expect(ctx.sandboxPolicy.defaultMode).toBe('read-only')
|
|
expect(ctx.sandboxPolicy.workspaceRoot).toBe(resolve(process.cwd()))
|
|
})
|
|
|
|
it('carries a configured mode and resolves the workspace root absolute', async () => {
|
|
const ctx = await mounted({ mode: 'workspace-write', workspaceRoot: '/ws/../ws/./sub' })
|
|
expect(ctx.sandboxPolicy.defaultMode).toBe('workspace-write')
|
|
expect(ctx.sandboxPolicy.workspaceRoot).toBe(resolve('/ws/../ws/./sub'))
|
|
})
|
|
|
|
it('rejects a mode outside the closed vocabulary at load', async () => {
|
|
const ctx = new Context()
|
|
// schemastery rejects the union violation when the plugin loads.
|
|
await expect(ctx.plugin(SandboxPolicyService, { mode: 'yolo' as never })).rejects.toThrow()
|
|
})
|
|
|
|
it('unregisters cleanly from a child fiber (HMR safety)', async () => {
|
|
const ctx = new Context()
|
|
const fiber = await ctx.plugin(SandboxPolicyService, {})
|
|
expect(ctx.sandboxPolicy).toBeDefined()
|
|
await fiber.dispose()
|
|
expect(ctx.get('sandboxPolicy')).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('the sandbox/mode session kit', () => {
|
|
it('SANDBOX_MODES lists every mode for advertisement and validation', () => {
|
|
expect(SANDBOX_MODES).toEqual(['read-only', 'workspace-write', 'danger-full-access'])
|
|
})
|
|
|
|
it('effectiveSandboxMode folds to the last switch, or undefined without one', () => {
|
|
const session = new Session(SessionId('sess-fold'))
|
|
expect(effectiveSandboxMode(session.events)).toBeUndefined()
|
|
setSandboxMode(session, 'workspace-write')
|
|
setSandboxMode(session, 'read-only')
|
|
expect(effectiveSandboxMode(session.events)).toBe('read-only')
|
|
})
|
|
|
|
it('setSandboxMode appends exactly one sandbox/mode event per switch', () => {
|
|
const session = new Session(SessionId('sess-write'))
|
|
setSandboxMode(session, 'danger-full-access')
|
|
const modeEvents = session.events.filter(e => e.type === 'sandbox/mode')
|
|
expect(modeEvents).toHaveLength(1)
|
|
expect(modeEvents[0]?.data).toEqual({ mode: 'danger-full-access' })
|
|
})
|
|
})
|