Merge pull request #293 from deepseek-harness/codex/simp-prune-sandbox-surface
refactor: hide sandbox implementation helpers
This commit is contained in:
@@ -181,7 +181,7 @@ export interface Config extends LocalConfig {
|
|||||||
|
|
||||||
Depends on: [`LocalConfig`](#deepseek-aidsh-bash-local) · [`SandboxMode`](core-data-structures/sandbox.md)
|
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`
|
## `@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`
|
## `@deepseek-ai/dsh-session-persistence-jsonl`
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
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.
|
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 |
|
| Mode | File effects |
|
||||||
|
|||||||
@@ -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()))
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import { SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
|
|||||||
import type { ConfinedSandboxMode, SandboxEnforcement, SandboxMode } from '@deepseek-ai/dsh-sandbox'
|
import type { ConfinedSandboxMode, SandboxEnforcement, SandboxMode } from '@deepseek-ai/dsh-sandbox'
|
||||||
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
|
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
|
||||||
import type { Config as LocalConfig } 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
|
* Plugin config: the local executor's knobs plus the sandbox policy. All
|
||||||
@@ -33,52 +34,6 @@ export interface Config extends LocalConfig {
|
|||||||
workspaceRoot?: string
|
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
|
* 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
|
* `ctx.sandbox` provider; the tool layer is unchanged. The configured mode is
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { homedir } from 'node:os'
|
|||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
import { afterEach, describe, expect, it } from 'vitest'
|
import { afterEach, describe, expect, it } from 'vitest'
|
||||||
import { Context } from 'cordis'
|
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'
|
import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import { Context } from 'cordis'
|
|||||||
import type { BashRunResult, CollectedOutput } from '@deepseek-ai/dsh-bash'
|
import type { BashRunResult, CollectedOutput } from '@deepseek-ai/dsh-bash'
|
||||||
import { SANDBOX_UNAVAILABLE, SandboxProvider, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
|
import { SANDBOX_UNAVAILABLE, SandboxProvider, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
|
||||||
import type { ConfinedArgv, SandboxPolicy } 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'
|
import type { Config } from '@deepseek-ai/dsh-bash-sandbox'
|
||||||
|
|
||||||
const spillDir = mkdtempSync(join(tmpdir(), 'dsh-bash-sandbox-spec-'))
|
const spillDir = mkdtempSync(join(tmpdir(), 'dsh-bash-sandbox-spec-'))
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { homedir } from 'node:os'
|
|||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
import { afterEach, describe, expect, it } from 'vitest'
|
import { afterEach, describe, expect, it } from 'vitest'
|
||||||
import { Context } from 'cordis'
|
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'
|
import { SandboxBashExecutor } from '@deepseek-ai/dsh-bash-sandbox'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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.
|
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.
|
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.
|
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.
|
||||||
|
|||||||
@@ -7,14 +7,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { spawnSync } from 'node:child_process'
|
import { spawnSync } from 'node:child_process'
|
||||||
import { realpathSync } from 'node:fs'
|
import { LAUNCHER_BIN, launcherPath as landlockLauncherPath, probe as defaultProbeLandlock } from 'node-addon-landlock-run'
|
||||||
import { tmpdir } from 'node:os'
|
|
||||||
import { grantArgs as landlockGrantArgs, LAUNCHER_BIN, launcherPath as landlockLauncherPath, probe as defaultProbeLandlock } from 'node-addon-landlock-run'
|
|
||||||
import { Context } from 'cordis'
|
import { Context } from 'cordis'
|
||||||
import z from 'schemastery'
|
import z from 'schemastery'
|
||||||
import { assertNever } from '@deepseek-ai/dsh-llm'
|
import { assertNever } from '@deepseek-ai/dsh-llm'
|
||||||
import { SandboxProvider, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
|
import { SandboxProvider, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
|
||||||
import type { ConfinedArgv, ConfinedSandboxMode, SandboxEnforcement, SandboxPolicy } 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. */
|
/** Plugin config. All optional — `static Config` supplies the defaults. */
|
||||||
export interface Config {
|
export interface Config {
|
||||||
@@ -38,77 +37,6 @@ export interface Config {
|
|||||||
probeTimeoutMs?: number
|
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. */
|
/** Probe whether `bwrap` can create the profile; the provider caches the bounded result. */
|
||||||
function defaultProbeBwrap(timeoutMs: number): boolean {
|
function defaultProbeBwrap(timeoutMs: number): boolean {
|
||||||
const probe = spawnSync('bwrap', ['--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent', '--', 'true'], {
|
const probe = spawnSync('bwrap', ['--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent', '--', 'true'], {
|
||||||
|
|||||||
@@ -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(' ')]
|
||||||
|
}
|
||||||
@@ -6,7 +6,8 @@ import { join } from 'node:path'
|
|||||||
import { afterEach, describe, expect, it } from 'vitest'
|
import { afterEach, describe, expect, it } from 'vitest'
|
||||||
import { Context } from 'cordis'
|
import { Context } from 'cordis'
|
||||||
import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
|
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,
|
* Keyless backend integration through `confine()` and a real bwrap process. With no rung forced,
|
||||||
|
|||||||
@@ -15,12 +15,10 @@ import { Context } from 'cordis'
|
|||||||
import { SANDBOX_UNAVAILABLE, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
|
import { SANDBOX_UNAVAILABLE, SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
|
||||||
import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
|
import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
|
||||||
import {
|
import {
|
||||||
bwrapProfileArgs,
|
|
||||||
landlockProfileArgs,
|
|
||||||
LocalSandboxProvider,
|
LocalSandboxProvider,
|
||||||
seatbeltProfileArgs,
|
|
||||||
} from '@deepseek-ai/dsh-sandbox-local'
|
} from '@deepseek-ai/dsh-sandbox-local'
|
||||||
import type { Config } 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 RO: SandboxPolicy = { mode: 'read-only', workspaceRoot: '/ws' }
|
||||||
const WW: SandboxPolicy = { mode: 'workspace-write', workspaceRoot: '/ws' }
|
const WW: SandboxPolicy = { mode: 'workspace-write', workspaceRoot: '/ws' }
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { join } from 'node:path'
|
|||||||
import { afterEach, describe, expect, it } from 'vitest'
|
import { afterEach, describe, expect, it } from 'vitest'
|
||||||
import { Context } from 'cordis'
|
import { Context } from 'cordis'
|
||||||
import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
|
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
|
* Keyless backend integration through `confine()` and a real macOS Seatbelt process, with Linux
|
||||||
|
|||||||
Reference in New Issue
Block a user