diff --git a/docs/config-catalog.md b/docs/config-catalog.md index b0391d7a65..af46798fc5 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -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` diff --git a/packages/bash/bash-sandbox/README.md b/packages/bash/bash-sandbox/README.md index 2a5ae63b8e..0aab0965be 100644 --- a/packages/bash/bash-sandbox/README.md +++ b/packages/bash/bash-sandbox/README.md @@ -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 | diff --git a/packages/bash/bash-sandbox/src/helpers.ts b/packages/bash/bash-sandbox/src/helpers.ts new file mode 100644 index 0000000000..a98f47216e --- /dev/null +++ b/packages/bash/bash-sandbox/src/helpers.ts @@ -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())) +} diff --git a/packages/bash/bash-sandbox/src/index.ts b/packages/bash/bash-sandbox/src/index.ts index 090d06b2fe..5236040a16 100644 --- a/packages/bash/bash-sandbox/src/index.ts +++ b/packages/bash/bash-sandbox/src/index.ts @@ -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 diff --git a/packages/bash/bash-sandbox/tests/bwrap.e2e.ts b/packages/bash/bash-sandbox/tests/bwrap.e2e.ts index 6a748bc389..92531c5302 100644 --- a/packages/bash/bash-sandbox/tests/bwrap.e2e.ts +++ b/packages/bash/bash-sandbox/tests/bwrap.e2e.ts @@ -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' /** diff --git a/packages/bash/bash-sandbox/tests/sandbox.spec.ts b/packages/bash/bash-sandbox/tests/sandbox.spec.ts index 4f92ba38f0..c197951322 100644 --- a/packages/bash/bash-sandbox/tests/sandbox.spec.ts +++ b/packages/bash/bash-sandbox/tests/sandbox.spec.ts @@ -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-')) diff --git a/packages/bash/bash-sandbox/tests/seatbelt.e2e.ts b/packages/bash/bash-sandbox/tests/seatbelt.e2e.ts index 8ae25a8d39..f6564a7b07 100644 --- a/packages/bash/bash-sandbox/tests/seatbelt.e2e.ts +++ b/packages/bash/bash-sandbox/tests/seatbelt.e2e.ts @@ -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' /** diff --git a/packages/sandbox/sandbox-local/README.md b/packages/sandbox/sandbox-local/README.md index abf7511438..669ebef3dd 100644 --- a/packages/sandbox/sandbox-local/README.md +++ b/packages/sandbox/sandbox-local/README.md @@ -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: : not found`, `: No such file or directory`, `: 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. diff --git a/packages/sandbox/sandbox-local/src/index.ts b/packages/sandbox/sandbox-local/src/index.ts index 66f3077319..e1fa429b6d 100644 --- a/packages/sandbox/sandbox-local/src/index.ts +++ b/packages/sandbox/sandbox-local/src/index.ts @@ -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 diff --git a/packages/sandbox/sandbox-local/src/profiles.ts b/packages/sandbox/sandbox-local/src/profiles.ts new file mode 100644 index 0000000000..9303583cbb --- /dev/null +++ b/packages/sandbox/sandbox-local/src/profiles.ts @@ -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(' ')] +} diff --git a/packages/sandbox/sandbox-local/tests/bwrap.e2e.ts b/packages/sandbox/sandbox-local/tests/bwrap.e2e.ts index da6e683da1..c5211adafb 100644 --- a/packages/sandbox/sandbox-local/tests/bwrap.e2e.ts +++ b/packages/sandbox/sandbox-local/tests/bwrap.e2e.ts @@ -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 diff --git a/packages/sandbox/sandbox-local/tests/local.spec.ts b/packages/sandbox/sandbox-local/tests/local.spec.ts index 852a50e1e9..e3aee5e32c 100644 --- a/packages/sandbox/sandbox-local/tests/local.spec.ts +++ b/packages/sandbox/sandbox-local/tests/local.spec.ts @@ -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' } diff --git a/packages/sandbox/sandbox-local/tests/seatbelt.e2e.ts b/packages/sandbox/sandbox-local/tests/seatbelt.e2e.ts index 7136e4e524..bc12750e10 100644 --- a/packages/sandbox/sandbox-local/tests/seatbelt.e2e.ts +++ b/packages/sandbox/sandbox-local/tests/seatbelt.e2e.ts @@ -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