refactor: hide sandbox implementation helpers
This commit is contained in:
@@ -176,7 +176,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:60`](../packages/bash/bash-sandbox/src/index.ts)
|
||||
Source: [`packages/bash/bash-sandbox/src/index.ts:61`](../packages/bash/bash-sandbox/src/index.ts)
|
||||
|
||||
## `@deepseek-ai/dsh-code-runtime-worker`
|
||||
|
||||
@@ -521,7 +521,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/sandbox/sandbox-local/src/index.ts:36`](../packages/sandbox/sandbox-local/src/index.ts)
|
||||
Source: [`packages/sandbox/sandbox-local/src/index.ts:35`](../packages/sandbox/sandbox-local/src/index.ts)
|
||||
|
||||
## `@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/)) — the model-facing tool layer (`dsh-tool-bash`) is untouched; that swap is exactly what the seams exist for.
|
||||
|
||||
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 |
|
||||
|
||||
49
packages/bash/bash-sandbox/src/helpers.ts
Normal file
49
packages/bash/bash-sandbox/src/helpers.ts
Normal 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()))
|
||||
}
|
||||
@@ -49,6 +49,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
|
||||
@@ -67,75 +68,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`'\''`)}'`
|
||||
}
|
||||
|
||||
/**
|
||||
* Conservative sandbox-denial classifier: a run counts as denied only when it
|
||||
* FAILED (nonzero exit — a signal kill is not a denial) and its stderr
|
||||
* carries one of the SELECTED BACKEND's own denial signatures — the dialect
|
||||
* the provider stamps on every wrap (`ConfinedArgv.denialSignatures`:
|
||||
* `Read-only file system` under bwrap's EROFS mounts, `Permission denied`
|
||||
* under Landlock's EACCES, `Operation not permitted` under Seatbelt's
|
||||
* EPERM). Matching the backend's dialect rather than a cross-backend union
|
||||
* keeps the classifier from claiming denials the active backend never
|
||||
* produces (bare EPERM text under a Linux runner names non-file boundaries —
|
||||
* mount, kill, ptrace — that fail the same way unsandboxed). Text inference
|
||||
* is the fallback signal until a runner provides a structured one (which
|
||||
* wins once it exists); it errs toward NOT claiming a denial, and its known
|
||||
* residual imprecision is non-sandbox text in the active dialect (an ssh
|
||||
* auth failure reads as a denial under Landlock, a refused `kill` under
|
||||
* Seatbelt).
|
||||
* @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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Runner-failure classifier: a failed run whose stderr carries the SELECTED
|
||||
* BACKEND's own runner-failure signature (`ConfinedArgv.
|
||||
* runnerFailureSignatures`: the runner's error prefix, which also matches
|
||||
* the shell's runner-not-found message) means the SANDBOX itself failed and
|
||||
* the command never ran. Checked BEFORE {@link classifyDenial} — a runner's
|
||||
* error text can contain denial words (an unopenable grant root reports
|
||||
* `Permission denied`) — and surfaced as the fail-closed
|
||||
* `SANDBOX_UNAVAILABLE` error on the foreground path, `sandbox.runnerFailed`
|
||||
* on a settled background task. Same conservative-text-inference stance and
|
||||
* residual imprecision as the denial classifier (a failing task that itself
|
||||
* prints the runner's prefix reads as a runner failure).
|
||||
* @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)
|
||||
}
|
||||
|
||||
/**
|
||||
* The classifier core shared by foreground results and settled background
|
||||
* tasks: failed AND signature present. Lowercases BOTH sides — the seam
|
||||
* declares its signatures case-insensitive, and producers compose them from
|
||||
* runtime data of any case (an `argv0` path, `No such file or directory`).
|
||||
*/
|
||||
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()))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sandbox-consuming bash executor. Registers as `ctx.bash` (loading it
|
||||
* INSTEAD OF `dsh-bash-local`, together with a `ctx.sandbox` provider, is
|
||||
|
||||
@@ -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'
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,7 +16,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-'))
|
||||
|
||||
@@ -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'
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Local implementation of the [`@deepseek-ai/dsh-sandbox`](../sandbox/) seam: wraps a caller's argv in a platform confinement runner. Selection is BY PLATFORM, resolved once and cached: each platform names its runner chain, a chain of one is selected directly (probing arbitrates between candidates — a sole candidate leaves nothing to arbitrate), and a chain of several is probed functionally in preference order. Linux: [`bwrap`](https://github.com/containers/bubblewrap) when its probe passes, else the [`landlock-run`](https://www.npmjs.com/package/node-addon-landlock-run) Landlock launcher (kernel confinement that needs no userns/mount privileges — see the [sandbox RFC](../../../docs/rfc/implemented/feature/2026-07-06-sandbox.md) for the prebuilt-binary decision and profile-parity notes); darwin: `sandbox-exec` speaking a Seatbelt (SBPL) profile, unprobed. A platform with no chain means `confine()` FAILS CLOSED with the seam's structured `SANDBOX_UNAVAILABLE` error (win32 today: a reserved, deliberately empty chain awaiting an AppContainer-family runner); an unprobed runner that turns out unusable fails closed at EXECUTION instead — it refuses to run the command, and every wrap's `runnerFailureSignatures` let the consumer classify that as a sandbox failure rather than a task failure. Never a silent unconfined passthrough on any path.
|
||||
|
||||
The package root exports the default and named `LocalSandboxProvider` plugin, `Config`, and its public test-injection seam; platform profile builders stay internal.
|
||||
|
||||
Policy is per call (`SandboxPolicy`: mode + workspace root); the provider holds only the mechanism and the cached ladder verdict. Every wrap reports the selected runner's `enforcement` (`full`, or `partial` on an older Landlock ABI that governs only a subset of accesses — read from the launcher's `--probe` report line) and its `denialSignatures` — the stderr dialect that rung's kernel speaks on a denied file effect (EROFS text under bwrap, EACCES under Landlock, EPERM under Seatbelt), which stderr-inferring consumers match instead of a cross-runner union. A non-empty `runnerCommand` config is the operator's assertion of a runner that fully enforces the bwrap-shaped profile: the ladder and probes are skipped (the wrap carries both Linux denial dialects, the mechanism being unknown) — also the deterministic fake-runner seam for keyless test tiers. Its runner-failure dialect is the OUTER shell's argv0-scoped failure shapes (`exec: <argv0>: not found`, `<argv0>: No such file or directory`, `<argv0>: Permission denied`) — the consumer re-joins the wrap through `bash -c 'exec …'`, so a missing or unexecutable configured runner classifies as a sandbox failure (fail closed at execution), never as a failing command or a denial. `probeTimeoutMs` (default 5000) bounds each functional probe, the escape hatch for hosts slow enough that a timed-out probe would otherwise misread as `SANDBOX_UNAVAILABLE`.
|
||||
|
||||
The Seatbelt profile is allow-default with `(deny file-write*)` plus write allow-lists, so exactly the mode's promised file effects are governed: `read-only` grants the `/dev/null` literal alone; `workspace-write` adds the workspace root, `/tmp`, and the per-user darwin temp dir (`os.tmpdir()` — the platform's real temp area for mkstemp-family tools), every root canonicalized because Seatbelt matches resolved paths (`/tmp` IS `/private/tmp`). Apple marks the `sandbox-exec` CLI deprecated but ships it on every macOS; the functional probe is what fails closed if that ever changes.
|
||||
|
||||
@@ -23,14 +23,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 {
|
||||
@@ -73,103 +72,6 @@ export interface Config {
|
||||
probeTimeoutMs?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* The `bwrap` profile arguments for one policy. The whole host tree is bound
|
||||
* read-only; a fresh `/dev` keeps `>/dev/null` redirects working and a fresh
|
||||
* `/proc` keeps process-inspecting tools working. `workspace-write`
|
||||
* additionally mounts an ephemeral writable `/tmp` and rebinds the workspace
|
||||
* root read-write (bind order matters: later binds overlay earlier ones).
|
||||
* Deliberately NO `--unshare-pid` (it would break the process-group kill
|
||||
* semantics shell consumers rely on) and NO network unsharing (the seam's
|
||||
* mode vocabulary promises file effects only).
|
||||
* @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
|
||||
}
|
||||
|
||||
/**
|
||||
* The `landlock-run` grant arguments for one policy — the bwrap
|
||||
* profile's file-effect semantics expressed as a Landlock allow-list
|
||||
* (Landlock cannot mount, so there are no fresh/ephemeral filesystems). The
|
||||
* whole tree is readable and executable; of `/dev`, ONLY `/dev/null` is
|
||||
* writable — a whole-`/dev` grant would expose real host paths beneath it
|
||||
* (`/dev/shm`, a shared tmpfs) to persistent writes, which `read-only`
|
||||
* promises never happen. bwrap can hand out a fresh ephemeral `/dev`; on the
|
||||
* host's own `/dev` the write grant must be node-by-node, and `>/dev/null`
|
||||
* is the one redirects need. `workspace-write` adds the HOST `/tmp` (shared
|
||||
* and persistent, where bwrap's is ephemeral — the honest difference,
|
||||
* recorded in the sandbox RFC's runner notes) plus the workspace
|
||||
* root read-write. The flag spelling belongs to `node-addon-landlock-run`'s
|
||||
* `grantArgs`; this function owns only the policy → grants mapping.
|
||||
* @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 {
|
||||
// realpathSync failed: the path (or a prefix) is missing or unreadable.
|
||||
// Grant the spelling as-is — an unresolvable root matches nothing until
|
||||
// it exists, which is the conservative outcome, and inventing a fallback
|
||||
// resolution here would grant a path the caller never named.
|
||||
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`\"`)}"`
|
||||
}
|
||||
|
||||
/**
|
||||
* The `sandbox-exec` arguments for one policy: `-p` plus a Seatbelt (SBPL)
|
||||
* profile with the same file-effect semantics as the other dialects, built
|
||||
* as allow-default → `(deny file-write*)` → write allow-list (later rules
|
||||
* win), so exactly the mode's promised file effects are governed — network
|
||||
* and process visibility stay unrestricted, which is all the seam's mode
|
||||
* vocabulary claims. Of `/dev`, ONLY the `/dev/null` literal is writable
|
||||
* (the same node-not-directory reasoning as the Landlock grant).
|
||||
* `workspace-write` adds the workspace root, the host `/tmp`, and the
|
||||
* per-user darwin temp dir (`os.tmpdir()`, launchd's `TMPDIR`, inherited by
|
||||
* the confined child) — on darwin that directory IS the platform's `/tmp`
|
||||
* for every mkstemp-family tool, so omitting it would deny the mode's
|
||||
* promised temp area. All granted roots are canonicalized because Seatbelt
|
||||
* matches resolved paths ({@link canonicalPath}); duplicates after
|
||||
* resolution collapse.
|
||||
* @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(' ')]
|
||||
}
|
||||
|
||||
/**
|
||||
* Functional `bwrap` probe: can it actually build the read-only profile on
|
||||
* this host? (`--version` alone would miss a disabled unprivileged user
|
||||
|
||||
67
packages/sandbox/sandbox-local/src/profiles.ts
Normal file
67
packages/sandbox/sandbox-local/src/profiles.ts
Normal 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(' ')]
|
||||
}
|
||||
@@ -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 bwrap integration proof for the BACKEND: the REAL `bwrap` confining
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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 Seatbelt integration proof for the BACKEND: the REAL macOS
|
||||
|
||||
Reference in New Issue
Block a user