mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
dsh-bash grows the per-call policy carrier: BashExecRequest.sandboxMode (request-optional, spec required-but-nullable — the owner pattern; resolve() is the one explicit defaulting step) and the BashExecutor.sandboxMode capability fact (undefined in the base class — composition truth the tool layer can read). dsh-bash-local carries the field verbatim and confines nothing. dsh-bash-sandbox extends LocalBashExecutor and hands ctx.sandbox the exact argv it is about to spawn. A denial is a RESULT FACT (the command RAN; result.sandbox.denied is orthogonal to exitCode/signal), classified conservatively against the wrap own dialect; a RUNNER failure outranks denial — foreground re-throws the structured SANDBOX_UNAVAILABLE, a settled background task stamps sandbox.runnerFailed — so a broken sandbox never reads as a failing command and the command never runs unconfined. dsh-tool-bash renders the markers and teaches the model not to retry around a policy denial; escalation and per-session switching are staged follow-ups.
102 lines
5.1 KiB
TypeScript
102 lines
5.1 KiB
TypeScript
import { spawnSync } from 'node:child_process'
|
|
import { existsSync, readFileSync } from 'node:fs'
|
|
import { mkdtemp, rm } from 'node:fs/promises'
|
|
import { homedir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import { Context } from 'cordis'
|
|
import { LocalSandboxProvider, seatbeltProfileArgs } from '@deepseek-ai/dsh-sandbox-local'
|
|
import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
|
|
|
|
/**
|
|
* KEYLESS consumer-integration proof on macOS: the REAL `LocalSandboxProvider`
|
|
* (Linux rungs forced off, so `sandbox-exec`/Seatbelt confines) underneath
|
|
* the REAL `SandboxBashExecutor`, driven through the executor's public
|
|
* run/start paths. Verifies the WORLD (files exist or don't) plus the
|
|
* stamped result facts — in particular that Seatbelt's EPERM denial text
|
|
* classifies as `denied: true` through the wrap-carried dialect; the
|
|
* backend-only confinement proofs live with `@deepseek-ai/dsh-sandbox-local`.
|
|
*
|
|
* Self-skips wherever the functional probe fails — every non-macOS host, or
|
|
* a macOS whose `sandbox-exec` refuses the profile.
|
|
*/
|
|
|
|
const probe = spawnSync('sandbox-exec', [...seatbeltProfileArgs({ mode: 'read-only', workspaceRoot: '/' }), '--', 'true'], { timeout: 5_000, stdio: 'ignore' })
|
|
const seatbeltUsable = probe.status === 0
|
|
|
|
let ctx: Context | undefined
|
|
const tempDirs: string[] = []
|
|
|
|
afterEach(async () => {
|
|
await ctx?.fiber.dispose()
|
|
ctx = undefined
|
|
await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
|
|
})
|
|
|
|
async function tempDir(base: string): Promise<string> {
|
|
const dir = await mkdtemp(join(base, 'dsh-seatbelt-e2e-'))
|
|
tempDirs.push(dir)
|
|
return dir
|
|
}
|
|
|
|
async function sandboxedBash(workspace: string, mode: 'read-only' | 'workspace-write'): Promise<SandboxBashExecutor> {
|
|
ctx = new Context()
|
|
await ctx.plugin(LocalSandboxProvider, {})
|
|
;(ctx.sandbox as LocalSandboxProvider).internals = { probeBwrap: () => false, probeLandlock: () => 'unusable' }
|
|
await ctx.plugin(SandboxBashExecutor, { mode, cwd: workspace, workspaceRoot: workspace, timeoutMs: 30_000 })
|
|
return ctx.bash as SandboxBashExecutor
|
|
}
|
|
|
|
describe.skipIf(!seatbeltUsable)('bash-sandbox: real Seatbelt confinement through ctx.bash', () => {
|
|
it('read-only denies a write — the file must NOT exist, and EPERM text classifies as a denial', async () => {
|
|
const workdir = await tempDir(homedir())
|
|
const bash = await sandboxedBash(workdir, 'read-only')
|
|
const result = await bash.run(bash.resolve({ command: `echo hi > ${workdir}/denied.txt` }))
|
|
expect(result.exitCode).not.toBe(0)
|
|
expect(result.sandbox).toEqual({ mode: 'read-only', denied: true, enforcement: 'full' })
|
|
expect(existsSync(join(workdir, 'denied.txt'))).toBe(false)
|
|
})
|
|
|
|
it('workspace-write lands a write inside the workspace root and still denies one beside it', async () => {
|
|
// HOME-based dirs on purpose: workspace-write grants /tmp and the
|
|
// per-user temp dir wholesale, so only paths outside both prove the
|
|
// workspace-root boundary.
|
|
const workdir = await tempDir(homedir())
|
|
const outside = await tempDir(homedir())
|
|
const bash = await sandboxedBash(workdir, 'workspace-write')
|
|
|
|
const inside = await bash.run(bash.resolve({ command: `printf seatbelt-ok > ${workdir}/allowed.txt` }))
|
|
expect(inside.exitCode).toBe(0)
|
|
expect(inside.sandbox).toEqual({ mode: 'workspace-write', denied: false, enforcement: 'full' })
|
|
expect(readFileSync(join(workdir, 'allowed.txt'), 'utf8')).toBe('seatbelt-ok')
|
|
|
|
const denied = await bash.run(bash.resolve({ command: `echo hi > ${outside}/denied.txt` }))
|
|
expect(denied.exitCode).not.toBe(0)
|
|
expect(denied.sandbox).toEqual({ mode: 'workspace-write', denied: true, enforcement: 'full' })
|
|
expect(existsSync(join(outside, 'denied.txt'))).toBe(false)
|
|
})
|
|
|
|
it('classifies a background denial once the task settles', async () => {
|
|
const workdir = await tempDir(homedir())
|
|
const bash = await sandboxedBash(workdir, 'read-only')
|
|
const task = bash.start(bash.resolve({ command: `echo hi > ${workdir}/bg-denied.txt` }))
|
|
await task.done
|
|
expect(task.sandbox).toEqual({ mode: 'read-only', denied: true, enforcement: 'full' })
|
|
expect(existsSync(join(workdir, 'bg-denied.txt'))).toBe(false)
|
|
})
|
|
|
|
it('an approved escalated retry — the spec-level workspace-write override — lands the exact write read-only denied', async () => {
|
|
const workdir = await tempDir(homedir())
|
|
const bash = await sandboxedBash(workdir, 'read-only')
|
|
const command = `printf escalated > ${workdir}/escalated.txt`
|
|
const strict = await bash.run(bash.resolve({ command }))
|
|
expect(strict.exitCode).not.toBe(0)
|
|
expect(strict.sandbox).toEqual({ mode: 'read-only', denied: true, enforcement: 'full' })
|
|
expect(existsSync(join(workdir, 'escalated.txt'))).toBe(false)
|
|
const retried = await bash.run(bash.resolve({ command, sandboxMode: 'workspace-write' }))
|
|
expect(retried.exitCode).toBe(0)
|
|
expect(retried.sandbox).toEqual({ mode: 'workspace-write', denied: false, enforcement: 'full' })
|
|
expect(readFileSync(join(workdir, 'escalated.txt'), 'utf8')).toBe('escalated')
|
|
})
|
|
})
|