Merge pull request #293 from deepseek-harness/codex/simp-prune-sandbox-surface

refactor: hide sandbox implementation helpers
This commit is contained in:
Tianyi Cui
2026-07-15 23:07:12 +08:00
committed by GitHub
13 changed files with 136 additions and 130 deletions

View File

@@ -181,7 +181,7 @@ export interface Config extends LocalConfig {
Depends on: [`LocalConfig`](#deepseek-aidsh-bash-local) · [`SandboxMode`](core-data-structures/sandbox.md)
Source: [`packages/bash/bash-sandbox/src/index.ts:26`](../packages/bash/bash-sandbox/src/index.ts)
Source: [`packages/bash/bash-sandbox/src/index.ts:27`](../packages/bash/bash-sandbox/src/index.ts)
## `@deepseek-ai/dsh-code-runtime-worker`
@@ -574,7 +574,7 @@ export interface Config {
}
```
Source: [`packages/sandbox/sandbox-local/src/index.ts:20`](../packages/sandbox/sandbox-local/src/index.ts)
Source: [`packages/sandbox/sandbox-local/src/index.ts:19`](../packages/sandbox/sandbox-local/src/index.ts)
## `@deepseek-ai/dsh-session-persistence-jsonl`

View File

@@ -2,6 +2,8 @@
Sandbox-consuming implementation of the [`@deepseek-ai/dsh-bash`](../bash/) executor seam. Load it **instead of** `@deepseek-ai/dsh-bash-local`, together with a [`ctx.sandbox`](../../sandbox/sandbox/) provider (e.g. [`@deepseek-ai/dsh-sandbox-local`](../../sandbox/sandbox-local/)) — no alternate tool plugin is needed; `dsh-tool-bash` detects the executor's `sandboxMode` capability and adds the escalation fields.
The package root exports the default and named `SandboxBashExecutor` plugin plus its `Config`; quoting and result-classification helpers stay internal.
Every command is confined by handing the provider the exact `['bash', '-c', command]` argv this executor is about to spawn and spawning the returned (wrapped) argv instead. WHICH platform runner confines it — and whether one is usable at all (fail closed with a structured `SANDBOX_UNAVAILABLE` error, never a silent unconfined run) — is the provider's concern; this package owns the bash side only.
| Mode | File effects |

View File

@@ -0,0 +1,49 @@
/**
* Internal shell-quoting and sandbox-result classification helpers.
*
* @module @deepseek-ai/dsh-bash-sandbox/helpers
*/
import type { BashRunResult } from '@deepseek-ai/dsh-bash'
/**
* Quote one string as a single-quoted POSIX shell word.
* @param text - raw argv element to preserve through the outer shell parse.
* @returns the quoted shell word.
*/
export function shellQuote(text: string): string {
return `'${text.replaceAll("'", String.raw`'\''`)}'`
}
/**
* Classify a failed run against the selected backend's denial dialect.
* @param result - settled foreground run.
* @param signatures - case-insensitive denial substrings from the active wrap.
* @returns whether the failed run matches that denial dialect.
*/
export function classifyDenial(result: BashRunResult, signatures: readonly string[]): boolean {
return matchesSignature(result.exitCode, result.stderr.text, signatures)
}
/**
* Classify a failed run against the selected backend's runner-failure dialect.
* @param result - settled foreground run.
* @param signatures - case-insensitive runner-failure substrings from the active wrap.
* @returns whether the failed run matches that runner-failure dialect.
*/
export function classifyRunnerFailure(result: BashRunResult, signatures: readonly string[]): boolean {
return matchesSignature(result.exitCode, result.stderr.text, signatures)
}
/**
* Match a non-zero exit against case-insensitive stderr signatures.
* @param exitCode - process exit code; null means signal termination.
* @param stderr - collected stderr text.
* @param signatures - substrings identifying the selected backend's dialect.
* @returns whether this is a non-zero exit whose stderr matches a signature.
*/
export function matchesSignature(exitCode: number | null, stderr: string, signatures: readonly string[]): boolean {
if (exitCode === null || exitCode === 0) return false
const lowered = stderr.toLowerCase()
return signatures.some(signature => lowered.includes(signature.toLowerCase()))
}

View File

@@ -15,6 +15,7 @@ import { SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
import type { ConfinedSandboxMode, SandboxEnforcement, SandboxMode } from '@deepseek-ai/dsh-sandbox'
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
import type { Config as LocalConfig } from '@deepseek-ai/dsh-bash-local'
import { classifyDenial, classifyRunnerFailure, matchesSignature, shellQuote } from './helpers.ts'
/**
* Plugin config: the local executor's knobs plus the sandbox policy. All
@@ -33,52 +34,6 @@ export interface Config extends LocalConfig {
workspaceRoot?: string
}
/**
* Quote one string as a single-quoted POSIX shell word (embedded single
* quotes become `'\''`), so a wrapped argv element survives the outer
* `bash -c` re-parse byte-for-byte.
* @param text - the raw argv element to quote.
* @returns the single-quoted shell word.
*/
export function shellQuote(text: string): string {
return `'${text.replaceAll("'", String.raw`'\''`)}'`
}
/**
* Conservatively classify a nonzero, non-signal run using only the selected
* backend's denial signatures. Text inference may miss a denial or match
* unrelated stderr in that dialect; it never uses another backend's terms.
* @param result - the settled foreground run to classify.
* @param signatures - the active wrap's denial dialect, case-insensitive stderr substrings.
* @returns whether the run's failure reads as a sandbox denial.
*/
export function classifyDenial(result: BashRunResult, signatures: readonly string[]): boolean {
return matchesSignature(result.exitCode, result.stderr.text, signatures)
}
/**
* Classify a nonzero run using the selected backend's runner-failure
* signatures. Callers check this before denial because runner diagnostics may
* contain denial words; the command did not run.
* @param result - the settled foreground run to classify.
* @param signatures - the active wrap's runner-failure signatures,
* case-insensitive stderr substrings.
* @returns whether the run's failure reads as the runner itself failing.
*/
export function classifyRunnerFailure(result: BashRunResult, signatures: readonly string[]): boolean {
return matchesSignature(result.exitCode, result.stderr.text, signatures)
}
/**
* Shared classifier for failed runs. Signatures are case-insensitive and may
* include runtime values such as an executable path.
*/
function matchesSignature(exitCode: number | null, stderr: string, signatures: readonly string[]): boolean {
if (exitCode === null || exitCode === 0) return false
const lowered = stderr.toLowerCase()
return signatures.some(signature => lowered.includes(signature.toLowerCase()))
}
/**
* Registers as `ctx.bash` in place of the local executor and requires a
* `ctx.sandbox` provider; the tool layer is unchanged. The configured mode is

View File

@@ -5,7 +5,8 @@ import { homedir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { bwrapProfileArgs, LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
import { bwrapProfileArgs } from '@deepseek-ai/dsh-sandbox-local/src/profiles.ts'
import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
/**

View File

@@ -13,7 +13,8 @@ import { Context } from 'cordis'
import type { BashRunResult, CollectedOutput } from '@deepseek-ai/dsh-bash'
import { SANDBOX_UNAVAILABLE, SandboxProvider, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
import type { ConfinedArgv, SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
import { classifyDenial, classifyRunnerFailure, SandboxBashExecutor, shellQuote } from '@deepseek-ai/dsh-bash-sandbox'
import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
import { classifyDenial, classifyRunnerFailure, shellQuote } from '../src/helpers.ts'
import type { Config } from '@deepseek-ai/dsh-bash-sandbox'
const spillDir = mkdtempSync(join(tmpdir(), 'dsh-bash-sandbox-spec-'))

View File

@@ -5,7 +5,8 @@ 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 { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
import { seatbeltProfileArgs } from '@deepseek-ai/dsh-sandbox-local/src/profiles.ts'
import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
/**

View File

@@ -2,6 +2,8 @@
Local implementation of the [`dsh-sandbox`](../sandbox/) seam. It selects and caches one platform runner: Linux prefers a working `bwrap` then Landlock; macOS uses Seatbelt. Multiple candidates are probed in order, while a sole candidate is selected directly.
The package root exports the default and named `LocalSandboxProvider` plugin, `Config`, and its public test-injection seam; platform profile builders stay internal.
Unsupported platforms and unusable runners fail closed with `SANDBOX_UNAVAILABLE`; execution never silently falls through unconfined. Each wrap carries runner-failure signatures so consumers can distinguish a broken sandbox from a command failure. The [sandbox RFC](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md) owns selection rationale and profile differences.
Policy is per call; the provider stores only the mechanism and cached runner verdict. Each wrap reports enforcement completeness plus backend-specific denial and runner-failure signatures. `runnerCommand` is an operator assertion of a bwrap-shaped runner and skips probes, but missing or unexecutable commands still fail closed at execution. Because its mechanism is unknown, it carries both Linux denial dialects. `probeTimeoutMs` bounds functional probes. The [sandbox RFC](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md) owns selection and failure semantics.

View File

@@ -7,14 +7,13 @@
*/
import { spawnSync } from 'node:child_process'
import { realpathSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { grantArgs as landlockGrantArgs, LAUNCHER_BIN, launcherPath as landlockLauncherPath, probe as defaultProbeLandlock } from 'node-addon-landlock-run'
import { LAUNCHER_BIN, launcherPath as landlockLauncherPath, probe as defaultProbeLandlock } from 'node-addon-landlock-run'
import { Context } from 'cordis'
import z from 'schemastery'
import { assertNever } from '@deepseek-ai/dsh-llm'
import { SandboxProvider, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
import type { ConfinedArgv, ConfinedSandboxMode, SandboxEnforcement, SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
import { bwrapProfileArgs, landlockProfileArgs, seatbeltProfileArgs } from './profiles.ts'
/** Plugin config. All optional — `static Config` supplies the defaults. */
export interface Config {
@@ -38,77 +37,6 @@ export interface Config {
probeTimeoutMs?: number
}
/**
* Build a bwrap profile: the host is read-only with fresh `/dev` and `/proc`;
* workspace-write overlays writable temp and workspace mounts. PID and network
* isolation are intentionally outside the file-effect policy.
*
* @param policy - the file-effect policy to express as bwrap arguments.
* @returns the bwrap profile arguments (before the trailing `--` + argv).
*/
export function bwrapProfileArgs(policy: SandboxPolicy): string[] {
const args = ['--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent']
if (policy.mode === 'workspace-write') {
args.push('--tmpfs', '/tmp')
args.push('--bind', policy.workspaceRoot, policy.workspaceRoot)
}
return args
}
/**
* Build Landlock grants for the same file policy without synthetic mounts.
* Read-only grants only `/dev/null` for writes; workspace-write also grants the
* host temp root and workspace.
*
* @param policy - the file-effect policy to express as launcher grants.
* @returns the launcher grant arguments (before `--` + argv).
*/
export function landlockProfileArgs(policy: SandboxPolicy): string[] {
const readWrite = ['/dev/null']
if (policy.mode === 'workspace-write') {
readWrite.push('/tmp', policy.workspaceRoot)
}
return landlockGrantArgs({ readOnly: ['/'], readWrite })
}
/**
* Resolve a granted root to the path the kernel actually sees. Seatbelt path
* filters match the CANONICAL path (symlinks resolved), and the roots this
* profile grants are symlinked on every macOS: `/tmp` is `/private/tmp` and
* the user temp dir lives under `/var` → `/private/var` — an as-spelled
* grant would match nothing.
*/
function canonicalPath(path: string): string {
try {
return realpathSync(path)
} catch {
// An unresolved grant matches nothing until the named path exists; keep its spelling.
return path
}
}
/** Quote one path as an SBPL string literal (backslashes and double quotes escaped). */
function sbplString(path: string): string {
return `"${path.replaceAll('\\', String.raw`\\`).replaceAll('"', String.raw`\"`)}"`
}
/**
* Build a Seatbelt profile that denies file writes then allows `/dev/null` and,
* for workspace-write, the canonical workspace, host temp, and per-user macOS
* temp roots. Network and process visibility remain unrestricted.
*
* @param policy - the file-effect policy to express as an SBPL profile.
* @returns the `sandbox-exec` arguments (`-p` + profile, before `--` + argv).
*/
export function seatbeltProfileArgs(policy: SandboxPolicy): string[] {
const forms = ['(version 1)', '(allow default)', '(deny file-write*)', `(allow file-write* (literal ${sbplString('/dev/null')}))`]
if (policy.mode === 'workspace-write') {
const roots = [...new Set([policy.workspaceRoot, '/tmp', tmpdir()].map(canonicalPath))]
forms.push(`(allow file-write* ${roots.map(root => `(subpath ${sbplString(root)})`).join(' ')})`)
}
return ['-p', forms.join(' ')]
}
/** Probe whether `bwrap` can create the profile; the provider caches the bounded result. */
function defaultProbeBwrap(timeoutMs: number): boolean {
const probe = spawnSync('bwrap', ['--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent', '--', 'true'], {

View File

@@ -0,0 +1,67 @@
/**
* Internal platform-profile builders for the local sandbox provider.
*
* @module @deepseek-ai/dsh-sandbox-local/profiles
*/
import { realpathSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { grantArgs as landlockGrantArgs } from 'node-addon-landlock-run'
import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
/**
* Build the bwrap profile arguments for one file-effect policy.
* @param policy - file-effect policy to express as bwrap mounts.
* @returns profile arguments before the trailing separator and command argv.
*/
export function bwrapProfileArgs(policy: SandboxPolicy): string[] {
const args = ['--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent']
if (policy.mode === 'workspace-write') {
args.push('--tmpfs', '/tmp')
args.push('--bind', policy.workspaceRoot, policy.workspaceRoot)
}
return args
}
/**
* Build the Landlock launcher grants for one file-effect policy.
* @param policy - file-effect policy to express as Landlock allow-list grants.
* @returns launcher grant arguments before the trailing separator and command argv.
*/
export function landlockProfileArgs(policy: SandboxPolicy): string[] {
const readWrite = ['/dev/null']
if (policy.mode === 'workspace-write') {
readWrite.push('/tmp', policy.workspaceRoot)
}
return landlockGrantArgs({ readOnly: ['/'], readWrite })
}
/** Resolve a granted root to the canonical path the Seatbelt kernel sees. */
function canonicalPath(path: string): string {
try {
return realpathSync(path)
} catch {
// Missing or unreadable roots stay as spelled; an unresolved root grants
// nothing until it exists, which is the conservative outcome.
return path
}
}
/** Quote one path as an SBPL string literal. */
function sbplString(path: string): string {
return `"${path.replaceAll('\\', String.raw`\\`).replaceAll('"', String.raw`\"`)}"`
}
/**
* Build the sandbox-exec arguments and SBPL profile for one policy.
* @param policy - file-effect policy to express as an SBPL profile.
* @returns sandbox-exec arguments before the trailing separator and command argv.
*/
export function seatbeltProfileArgs(policy: SandboxPolicy): string[] {
const forms = ['(version 1)', '(allow default)', '(deny file-write*)', `(allow file-write* (literal ${sbplString('/dev/null')}))`]
if (policy.mode === 'workspace-write') {
const roots = [...new Set([policy.workspaceRoot, '/tmp', tmpdir()].map(canonicalPath))]
forms.push(`(allow file-write* ${roots.map(root => `(subpath ${sbplString(root)})`).join(' ')})`)
}
return ['-p', forms.join(' ')]
}

View File

@@ -6,7 +6,8 @@ import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
import { bwrapProfileArgs, LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
import { bwrapProfileArgs } from '../src/profiles.ts'
/**
* Keyless backend integration through `confine()` and a real bwrap process. With no rung forced,

View File

@@ -15,12 +15,10 @@ import { Context } from 'cordis'
import { SANDBOX_UNAVAILABLE, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
import {
bwrapProfileArgs,
landlockProfileArgs,
LocalSandboxProvider,
seatbeltProfileArgs,
} from '@deepseek-ai/dsh-sandbox-local'
import type { Config } from '@deepseek-ai/dsh-sandbox-local'
import { bwrapProfileArgs, landlockProfileArgs, seatbeltProfileArgs } from '../src/profiles.ts'
const RO: SandboxPolicy = { mode: 'read-only', workspaceRoot: '/ws' }
const WW: SandboxPolicy = { mode: 'workspace-write', workspaceRoot: '/ws' }

View File

@@ -6,7 +6,8 @@ import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
import { LocalSandboxProvider, seatbeltProfileArgs } from '@deepseek-ai/dsh-sandbox-local'
import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
import { seatbeltProfileArgs } from '../src/profiles.ts'
/**
* Keyless backend integration through `confine()` and a real macOS Seatbelt process, with Linux