feat(sandbox): the confinement seam and the per-platform native runner chains
ctx.sandbox (dsh-sandbox): confine(argv, policy) returns the argv to spawn instead — wrapped so the process and its children run confined — plus the enforcement completeness and the backend denial/runner-failure dialects; no usable backend throws the fail-closed SANDBOX_UNAVAILABLE. Policy rides per call. dsh-sandbox-local selects by platform and caches the verdict: multi-candidate chains probe FUNCTIONALLY in preference order (Linux: bwrap → the registry-installed node-addon-landlock-run launcher), a sole candidate is selected unprobed (darwin: sandbox-exec/Seatbelt) and fails closed at execution via runnerFailureSignatures; win32 is a reserved empty chain. Profile parity is honest per backend (documented temp-area and ABI differences; enforcement full|partial is a structured result fact). CI: the sandbox-e2e matrix proves real-kernel confinement per rung (bwrap, Landlock per architecture through the registry-installed launcher, Seatbelt), failing on a silent all-skip; the packed-install rehearsal installs the launcher family from the registry and asserts the binary executable apart from kernel enforcement.
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { existsSync, readFileSync, rmSync } from 'node:fs'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { homedir, tmpdir } from 'node:os'
|
||||
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'
|
||||
|
||||
/**
|
||||
* KEYLESS bwrap integration proof for the BACKEND: the REAL `bwrap` confining
|
||||
* REAL processes through `confine()` + a direct spawn of the returned argv.
|
||||
* Nothing is forced off: bwrap is the ladder's FIRST rung, so a passing probe
|
||||
* selects it naturally — the wrap shape assertion pins that. Verifies the
|
||||
* WORLD (files exist or don't) and that the kernel's denial text matches the
|
||||
* dialect the wrap advertises; the through-`ctx.bash` consumer proof lives
|
||||
* with `@deepseek-ai/dsh-bash-sandbox`.
|
||||
*
|
||||
* Self-skips wherever the functional probe fails — no `bwrap` on PATH, or a
|
||||
* host that denies unprivileged user namespaces (the probe is the same
|
||||
* profile the provider enforces, so skip conditions match runtime exactly).
|
||||
*
|
||||
* Workspaces for the workspace-write tests live under the HOME directory on
|
||||
* purpose: bwrap's `/tmp` is an EPHEMERAL mount (the documented
|
||||
* bwrap-profile difference — pinned by its own test below), so only a
|
||||
* workspace OUTSIDE `/tmp` proves the workspace-root rebind itself.
|
||||
*/
|
||||
|
||||
const probe = spawnSync('bwrap', [...bwrapProfileArgs({ mode: 'read-only', workspaceRoot: '/' }), '--', 'true'], { timeout: 5_000, stdio: 'ignore' })
|
||||
const bwrapUsable = probe.status === 0
|
||||
|
||||
let ctx: Context | undefined
|
||||
const tempDirs: string[] = []
|
||||
const tempFiles: string[] = []
|
||||
|
||||
afterEach(async () => {
|
||||
await ctx?.fiber.dispose()
|
||||
ctx = undefined
|
||||
await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
|
||||
for (const file of tempFiles.splice(0)) rmSync(file, { force: true })
|
||||
})
|
||||
|
||||
async function tempDir(base: string): Promise<string> {
|
||||
const dir = await mkdtemp(join(base, 'dsh-bwrap-e2e-'))
|
||||
tempDirs.push(dir)
|
||||
return dir
|
||||
}
|
||||
|
||||
async function provider(): Promise<LocalSandboxProvider> {
|
||||
ctx = new Context()
|
||||
await ctx.plugin(LocalSandboxProvider, {})
|
||||
return ctx.sandbox as LocalSandboxProvider
|
||||
}
|
||||
|
||||
/** Confine a shell command under `policy` and run it for real; returns the spawn result and the wrap's facts. */
|
||||
function runConfined(sandbox: LocalSandboxProvider, command: string, policy: SandboxPolicy) {
|
||||
const confined = sandbox.confine(['bash', '-c', command], policy)
|
||||
const result = spawnSync(confined.argv[0] as string, confined.argv.slice(1), { timeout: 30_000, encoding: 'utf8' })
|
||||
return { result, confined }
|
||||
}
|
||||
|
||||
describe.skipIf(!bwrapUsable)('sandbox-local: real bwrap confinement', () => {
|
||||
it('the passing probe selects the bwrap rung naturally — first in the ladder, full enforcement, EROFS dialect', async () => {
|
||||
const workdir = await tempDir(tmpdir())
|
||||
const sandbox = await provider()
|
||||
const confined = sandbox.confine(['true'], { mode: 'read-only', workspaceRoot: workdir })
|
||||
expect(confined.argv[0]).toBe('bwrap')
|
||||
expect(confined.enforcement).toBe('full')
|
||||
expect(confined.denialSignatures).toEqual(['read-only file system'])
|
||||
})
|
||||
|
||||
it('read-only denies a write — the file must NOT exist, and the kernel speaks the advertised dialect', async () => {
|
||||
const workdir = await tempDir(tmpdir())
|
||||
const sandbox = await provider()
|
||||
const { result } = runConfined(sandbox, `echo hi > ${workdir}/denied.txt`, { mode: 'read-only', workspaceRoot: workdir })
|
||||
expect(result.status).not.toBe(0)
|
||||
// The wrap's denialSignatures must be what the kernel actually prints.
|
||||
expect(result.stderr.toLowerCase()).toContain('read-only file system')
|
||||
expect(existsSync(join(workdir, 'denied.txt'))).toBe(false)
|
||||
})
|
||||
|
||||
it('read-only keeps the tree readable/executable and the fresh /dev/null writable', async () => {
|
||||
const workdir = await tempDir(tmpdir())
|
||||
const sandbox = await provider()
|
||||
const { result } = runConfined(sandbox, 'ls / > /dev/null && echo dev-ok', { mode: 'read-only', workspaceRoot: workdir })
|
||||
expect(result.status).toBe(0)
|
||||
expect(result.stdout).toBe('dev-ok\n')
|
||||
})
|
||||
|
||||
it('workspace-write lands a write inside the workspace root and still denies one beside it', async () => {
|
||||
const workdir = await tempDir(homedir())
|
||||
const outside = await tempDir(homedir())
|
||||
const sandbox = await provider()
|
||||
|
||||
const inside = runConfined(sandbox, `printf bwrap-ok > ${workdir}/allowed.txt`, { mode: 'workspace-write', workspaceRoot: workdir })
|
||||
expect(inside.result.status).toBe(0)
|
||||
expect(readFileSync(join(workdir, 'allowed.txt'), 'utf8')).toBe('bwrap-ok')
|
||||
|
||||
const denied = runConfined(sandbox, `echo hi > ${outside}/denied.txt`, { mode: 'workspace-write', workspaceRoot: workdir })
|
||||
expect(denied.result.status).not.toBe(0)
|
||||
expect(existsSync(join(outside, 'denied.txt'))).toBe(false)
|
||||
})
|
||||
|
||||
it('workspace-write mounts an EPHEMERAL /tmp: the write succeeds inside, the host /tmp stays untouched', async () => {
|
||||
// The documented bwrap-profile difference: Landlock and Seatbelt grant
|
||||
// the HOST temp areas, bwrap swaps in a fresh tmpfs that dies with the
|
||||
// process — the strongest of the three temp semantics.
|
||||
const workdir = await tempDir(homedir())
|
||||
const target = `/tmp/dsh-bwrap-e2e-ephemeral-${process.pid}.txt`
|
||||
tempFiles.push(target)
|
||||
const sandbox = await provider()
|
||||
const { result } = runConfined(sandbox, `printf tmp-ok > ${target} && cat ${target}`, { mode: 'workspace-write', workspaceRoot: workdir })
|
||||
expect(result.status).toBe(0)
|
||||
expect(result.stdout).toBe('tmp-ok')
|
||||
expect(existsSync(target)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,118 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { homedir, tmpdir } from 'node:os'
|
||||
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 { launcherPath } from 'node-addon-landlock-run'
|
||||
import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
|
||||
|
||||
/**
|
||||
* KEYLESS Landlock integration proof for the BACKEND: the REAL npm-distributed
|
||||
* `landlock-run` launcher (`node-addon-landlock-run`) confining REAL processes through `confine()` + a direct
|
||||
* spawn of the returned argv, with the bwrap rung forced off so the ladder
|
||||
* lands on the launcher. Verifies the WORLD (files exist or don't), not the
|
||||
* wrapper argv alone; the through-`ctx.bash` consumer proof lives with
|
||||
* `@deepseek-ai/dsh-bash-sandbox`.
|
||||
*
|
||||
* Self-skips when the running kernel does not enforce Landlock (or this
|
||||
* platform has no launcher package — the probe cannot pass then). The
|
||||
* binary itself arrives with `pnpm install`, so absence is not a checkout
|
||||
* state.
|
||||
*
|
||||
* Workspaces live under the HOME directory on purpose: `workspace-write`
|
||||
* grants the host `/tmp` wholesale (the documented Landlock-profile
|
||||
* difference), so only a workspace OUTSIDE `/tmp` proves the workspace-root
|
||||
* grant itself.
|
||||
*/
|
||||
|
||||
const probe = spawnSync(launcherPath(), ['--probe'], { timeout: 5_000, encoding: 'utf8' })
|
||||
const landlockUsable = probe.status === 0
|
||||
/** The running kernel's enforcement level, from the launcher's probe report — every wrap below must carry exactly this. */
|
||||
const enforcement = /partially enforced/.test(probe.stdout ?? '') ? 'partial' : 'full'
|
||||
|
||||
let ctx: Context | undefined
|
||||
const tempDirs: string[] = []
|
||||
|
||||
afterEach(async () => {
|
||||
await ctx?.fiber.dispose()
|
||||
ctx = undefined
|
||||
await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
|
||||
})
|
||||
|
||||
async function tempDir(base: string): Promise<string> {
|
||||
const dir = await mkdtemp(join(base, 'dsh-landlock-e2e-'))
|
||||
tempDirs.push(dir)
|
||||
return dir
|
||||
}
|
||||
|
||||
async function provider(): Promise<LocalSandboxProvider> {
|
||||
ctx = new Context()
|
||||
await ctx.plugin(LocalSandboxProvider, {})
|
||||
const sandbox = ctx.sandbox as LocalSandboxProvider
|
||||
sandbox.internals = { probeBwrap: () => false }
|
||||
return sandbox
|
||||
}
|
||||
|
||||
/** Confine a shell command under `policy` and run it for real; returns the spawn result and the wrap's enforcement. */
|
||||
function runConfined(sandbox: LocalSandboxProvider, command: string, policy: SandboxPolicy) {
|
||||
const confined = sandbox.confine(['bash', '-c', command], policy)
|
||||
const result = spawnSync(confined.argv[0] as string, confined.argv.slice(1), { timeout: 30_000, encoding: 'utf8' })
|
||||
return { result, enforcement: confined.enforcement }
|
||||
}
|
||||
|
||||
describe.skipIf(!landlockUsable)('sandbox-local: real Landlock confinement through the bundled launcher', () => {
|
||||
it('read-only denies a write — the file must NOT exist, the wrap reports the probed enforcement', async () => {
|
||||
const workdir = await tempDir(tmpdir())
|
||||
const sandbox = await provider()
|
||||
const { result, enforcement: wrapped } = runConfined(sandbox, `echo hi > ${workdir}/denied.txt`, { mode: 'read-only', workspaceRoot: workdir })
|
||||
expect(result.status).not.toBe(0)
|
||||
expect(wrapped).toBe(enforcement)
|
||||
expect(existsSync(join(workdir, 'denied.txt'))).toBe(false)
|
||||
})
|
||||
|
||||
it('read-only keeps the tree readable/executable and /dev/null writable', async () => {
|
||||
const workdir = await tempDir(tmpdir())
|
||||
const sandbox = await provider()
|
||||
const { result } = runConfined(sandbox, 'ls / > /dev/null && echo dev-ok', { mode: 'read-only', workspaceRoot: workdir })
|
||||
expect(result.status).toBe(0)
|
||||
expect(result.stdout).toBe('dev-ok\n')
|
||||
})
|
||||
|
||||
it('read-only denies a write beneath the host /dev (the /dev/shm tmpfs must stay untouched)', async () => {
|
||||
// The grant is /dev/null the FILE, not /dev the directory: /dev/shm is a
|
||||
// world-writable host tmpfs, and a write landing there would be exactly
|
||||
// the persistent host effect read-only promises never happen.
|
||||
const workdir = await tempDir(tmpdir())
|
||||
const sandbox = await provider()
|
||||
const target = `/dev/shm/dsh-landlock-e2e-${process.pid}`
|
||||
const { result } = runConfined(sandbox, `echo hi > ${target}`, { mode: 'read-only', workspaceRoot: workdir })
|
||||
expect(result.status).not.toBe(0)
|
||||
expect(existsSync(target)).toBe(false)
|
||||
})
|
||||
|
||||
it('workspace-write lands a write inside the workspace root and still denies one beside it', async () => {
|
||||
const workdir = await tempDir(homedir())
|
||||
const outside = await tempDir(homedir())
|
||||
const sandbox = await provider()
|
||||
|
||||
const inside = runConfined(sandbox, `printf landlock-ok > ${workdir}/allowed.txt`, { mode: 'workspace-write', workspaceRoot: workdir })
|
||||
expect(inside.result.status).toBe(0)
|
||||
expect(readFileSync(join(workdir, 'allowed.txt'), 'utf8')).toBe('landlock-ok')
|
||||
|
||||
const denied = runConfined(sandbox, `echo hi > ${outside}/denied.txt`, { mode: 'workspace-write', workspaceRoot: workdir })
|
||||
expect(denied.result.status).not.toBe(0)
|
||||
expect(existsSync(join(outside, 'denied.txt'))).toBe(false)
|
||||
})
|
||||
|
||||
it('workspace-write grants the host /tmp (the documented Landlock-profile difference)', async () => {
|
||||
const workdir = await tempDir(homedir())
|
||||
const scratch = await tempDir(tmpdir())
|
||||
const sandbox = await provider()
|
||||
const { result } = runConfined(sandbox, `printf tmp-ok > ${scratch}/scratch.txt`, { mode: 'workspace-write', workspaceRoot: workdir })
|
||||
expect(result.status).toBe(0)
|
||||
expect(readFileSync(join(scratch, 'scratch.txt'), 'utf8')).toBe('tmp-ok')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,349 @@
|
||||
/**
|
||||
* LocalSandboxProvider tests. No real runner is assumed to exist on the test
|
||||
* host: `runnerCommand` injects deterministic runner argvs, and `internals`
|
||||
* injects probe verdicts plus fake Landlock launcher / `sandbox-exec`
|
||||
* scripts, so profile dialects, ladder selection, verdict caching,
|
||||
* probe-report parsing, per-rung denial signatures, and fail-closed behavior
|
||||
* are all exercised through the real `confine()` path.
|
||||
*/
|
||||
|
||||
import { mkdtempSync, realpathSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
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'
|
||||
|
||||
const RO: SandboxPolicy = { mode: 'read-only', workspaceRoot: '/ws' }
|
||||
const WW: SandboxPolicy = { mode: 'workspace-write', workspaceRoot: '/ws' }
|
||||
|
||||
async function setup(config: Config = {}, internals: LocalSandboxProvider['internals'] = {}) {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LocalSandboxProvider, config)
|
||||
const sandbox = ctx.sandbox as LocalSandboxProvider
|
||||
sandbox.internals = internals
|
||||
return { ctx, sandbox }
|
||||
}
|
||||
|
||||
/** Write an executable fake `landlock-run` that answers `--probe` with `report`. */
|
||||
function fakeLauncher(report = 'landlock: fully enforced'): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-fake-landlock-'))
|
||||
const launcher = join(dir, 'landlock-run')
|
||||
writeFileSync(launcher, `#!/bin/sh\nif [ "$1" = "--probe" ]; then echo "${report}"; exit 0; fi\nexit 125\n`, { mode: 0o755 })
|
||||
return launcher
|
||||
}
|
||||
|
||||
/** Write an executable fake `sandbox-exec` that exits `status` for any invocation. */
|
||||
function fakeSeatbeltExec(status: number): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-fake-seatbelt-'))
|
||||
const exec = join(dir, 'sandbox-exec')
|
||||
writeFileSync(exec, `#!/bin/sh\nexit ${status}\n`, { mode: 0o755 })
|
||||
return exec
|
||||
}
|
||||
|
||||
/** The seatbelt read-only profile — every seatbelt profile starts with these forms. */
|
||||
const SEATBELT_RO_PROFILE = '(version 1) (allow default) (deny file-write*) (allow file-write* (literal "/dev/null"))'
|
||||
|
||||
describe('profile dialects', () => {
|
||||
it('bwrap read-only: whole tree read-only with fresh /dev and /proc, no writable mounts', () => {
|
||||
expect(bwrapProfileArgs(RO)).toEqual(['--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent'])
|
||||
})
|
||||
|
||||
it('bwrap workspace-write: adds an ephemeral /tmp and rebinds the workspace root', () => {
|
||||
expect(bwrapProfileArgs(WW)).toEqual([
|
||||
'--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', '--die-with-parent',
|
||||
'--tmpfs', '/tmp', '--bind', '/ws', '/ws',
|
||||
])
|
||||
})
|
||||
|
||||
it('landlock read-only: readable tree plus a writable /dev/null, nothing else', () => {
|
||||
// /dev/null specifically, NOT /dev: a whole-/dev grant would let confined
|
||||
// commands write real host paths beneath it (/dev/shm) under read-only.
|
||||
expect(landlockProfileArgs(RO)).toEqual(['--ro', '/', '--rw', '/dev/null'])
|
||||
})
|
||||
|
||||
it('landlock workspace-write: adds the host /tmp and the workspace root', () => {
|
||||
expect(landlockProfileArgs(WW)).toEqual(['--ro', '/', '--rw', '/dev/null', '--rw', '/tmp', '--rw', '/ws'])
|
||||
})
|
||||
|
||||
it('seatbelt read-only: allow-default with every file write denied except the /dev/null literal', () => {
|
||||
expect(seatbeltProfileArgs(RO)).toEqual(['-p', SEATBELT_RO_PROFILE])
|
||||
})
|
||||
|
||||
it('seatbelt workspace-write: one more allow for the canonicalized workspace root, /tmp, and the user temp dir', () => {
|
||||
// `/ws` does not exist, so it is granted as spelled (the canonicalization
|
||||
// fallback); `/tmp` and `os.tmpdir()` exist everywhere and are granted
|
||||
// CANONICALIZED — Seatbelt matches resolved paths (`/tmp` IS
|
||||
// `/private/tmp` on macOS), and both collapse to one grant on hosts
|
||||
// where they resolve to the same directory.
|
||||
const roots = [...new Set(['/ws', realpathSync('/tmp'), realpathSync(tmpdir())])]
|
||||
const allow = `(allow file-write* ${roots.map(root => `(subpath "${root}")`).join(' ')})`
|
||||
expect(seatbeltProfileArgs(WW)).toEqual(['-p', `${SEATBELT_RO_PROFILE} ${allow}`])
|
||||
})
|
||||
|
||||
it('seatbelt workspace-write dedups a workspace root that already IS the temp dir', () => {
|
||||
const profile = seatbeltProfileArgs({ mode: 'workspace-write', workspaceRoot: tmpdir() })[1] as string
|
||||
const grant = `(subpath "${realpathSync(tmpdir())}")`
|
||||
expect(profile).toContain(grant)
|
||||
expect(profile.split(grant)).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('runnerCommand config', () => {
|
||||
it('a non-empty runnerCommand skips the chain: runner argv + bwrap-shaped profile + -- + caller argv, asserted full', async () => {
|
||||
const probeBwrap = vi.fn(() => false)
|
||||
const probeLandlock = vi.fn(() => 'unusable' as const)
|
||||
const probeSeatbelt = vi.fn(() => false)
|
||||
const { sandbox } = await setup({ runnerCommand: ['fake-runner', '--flag'] }, { probeBwrap, probeLandlock, probeSeatbelt })
|
||||
const confined = sandbox.confine(['bash', '-c', 'echo hi'], WW)
|
||||
expect(confined).toEqual({
|
||||
argv: ['fake-runner', '--flag', ...bwrapProfileArgs(WW), '--', 'bash', '-c', 'echo hi'],
|
||||
enforcement: 'full',
|
||||
// An operator runner's kernel mechanism is unknown: both Linux
|
||||
// file-denial dialects, never bare EPERM.
|
||||
denialSignatures: ['read-only file system', 'permission denied'],
|
||||
// The runner's own dialect is unknown, but the consumer re-joins the
|
||||
// wrap through an outer `bash -c 'exec …'` — a missing or
|
||||
// unexecutable runner fails with the OUTER shell's argv0-scoped
|
||||
// shapes, and those classify as sandbox failures like any rung.
|
||||
runnerFailureSignatures: [
|
||||
'exec: fake-runner: not found',
|
||||
'fake-runner: No such file or directory',
|
||||
'fake-runner: Permission denied',
|
||||
],
|
||||
})
|
||||
expect(probeBwrap).not.toHaveBeenCalled()
|
||||
expect(probeLandlock).not.toHaveBeenCalled()
|
||||
expect(probeSeatbelt).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('an EMPTY runnerCommand means unconfigured: the platform chain still gates the wrap', async () => {
|
||||
const probeBwrap = vi.fn(() => false)
|
||||
const { sandbox } = await setup({ runnerCommand: [] }, { platform: 'linux', probeBwrap, probeLandlock: () => 'unusable' })
|
||||
expect(() => sandbox.confine(['true'], RO)).toThrow(SandboxUnavailableError)
|
||||
expect(probeBwrap).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('the platform chains', () => {
|
||||
it('linux probes bwrap first: a passing probe wraps with the bwrap dialect at full enforcement', async () => {
|
||||
const probeBwrap = vi.fn(() => true)
|
||||
const probeLandlock = vi.fn(() => 'full' as const)
|
||||
const { sandbox } = await setup({}, { platform: 'linux', probeBwrap, probeLandlock })
|
||||
const confined = sandbox.confine(['true'], RO)
|
||||
expect(confined).toEqual({
|
||||
argv: ['bwrap', ...bwrapProfileArgs(RO), '--', 'true'],
|
||||
enforcement: 'full',
|
||||
denialSignatures: ['read-only file system'],
|
||||
runnerFailureSignatures: ['bwrap: '],
|
||||
})
|
||||
expect(probeLandlock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('linux falls back to the launcher when the bwrap probe fails, speaking the landlock dialect', async () => {
|
||||
const probeBwrap = vi.fn(() => false)
|
||||
const probeLandlock = vi.fn(() => 'full' as const)
|
||||
const launcher = fakeLauncher()
|
||||
const { sandbox } = await setup({}, { platform: 'linux', probeBwrap, probeLandlock, landlockLauncher: launcher })
|
||||
const confined = sandbox.confine(['bash', '-c', 'echo hi'], WW)
|
||||
expect(confined).toEqual({
|
||||
argv: [launcher, ...landlockProfileArgs(WW), '--', 'bash', '-c', 'echo hi'],
|
||||
enforcement: 'full',
|
||||
denialSignatures: ['permission denied'],
|
||||
runnerFailureSignatures: ['landlock-run: '],
|
||||
})
|
||||
expect(probeLandlock).toHaveBeenCalledWith(launcher)
|
||||
})
|
||||
|
||||
it('darwin selects its sole candidate WITHOUT probing: nothing to arbitrate', async () => {
|
||||
// The safety property moves to execution time: an unusable sandbox-exec
|
||||
// refuses to run the command, and the wrap's runnerFailureSignatures let
|
||||
// the consumer classify that as a sandbox failure, not a task failure.
|
||||
const probeSeatbelt = vi.fn(() => true)
|
||||
const { sandbox } = await setup({}, { platform: 'darwin', probeSeatbelt })
|
||||
const confined = sandbox.confine(['bash', '-c', 'echo hi'], RO)
|
||||
expect(confined).toEqual({
|
||||
argv: ['sandbox-exec', ...seatbeltProfileArgs(RO), '--', 'bash', '-c', 'echo hi'],
|
||||
enforcement: 'full',
|
||||
denialSignatures: ['operation not permitted'],
|
||||
runnerFailureSignatures: ['sandbox-exec: '],
|
||||
})
|
||||
expect(probeSeatbelt).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('a platform with no chain fails closed without a single probe: the command never runs', async () => {
|
||||
const probeBwrap = vi.fn(() => true)
|
||||
const probeLandlock = vi.fn(() => 'full' as const)
|
||||
const probeSeatbelt = vi.fn(() => true)
|
||||
const { sandbox } = await setup({}, { platform: 'freebsd', probeBwrap, probeLandlock, probeSeatbelt })
|
||||
expect(() => sandbox.confine(['true'], RO)).toThrow(expect.objectContaining({ name: 'SandboxUnavailableError', code: SANDBOX_UNAVAILABLE }))
|
||||
expect(probeBwrap).not.toHaveBeenCalled()
|
||||
expect(probeLandlock).not.toHaveBeenCalled()
|
||||
expect(probeSeatbelt).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('win32 is a reserved EMPTY chain: fails closed identically until a Windows runner fills it', async () => {
|
||||
// The slot exists so Windows support is an additive fill-in (chain entry
|
||||
// + runner union member), never a redesign — and reserving it must not
|
||||
// weaken the fail-closed end in the meantime.
|
||||
const { sandbox } = await setup({}, { platform: 'win32' })
|
||||
expect(() => sandbox.confine(['true'], RO)).toThrow(expect.objectContaining({ code: SANDBOX_UNAVAILABLE }))
|
||||
})
|
||||
|
||||
it('caches the verdict for the provider lifetime: one chain walk across wraps', async () => {
|
||||
const probeBwrap = vi.fn(() => true)
|
||||
const { sandbox } = await setup({}, { platform: 'linux', probeBwrap })
|
||||
sandbox.confine(['true'], RO)
|
||||
sandbox.confine(['true'], WW)
|
||||
expect(probeBwrap).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('the unavailable verdict is cached too, and the error is structured', async () => {
|
||||
const probeBwrap = vi.fn(() => false)
|
||||
const probeLandlock = vi.fn(() => 'unusable' as const)
|
||||
const { sandbox } = await setup({}, { platform: 'linux', probeBwrap, probeLandlock })
|
||||
expect(() => sandbox.confine(['true'], RO)).toThrow(expect.objectContaining({ name: 'SandboxUnavailableError', code: SANDBOX_UNAVAILABLE }))
|
||||
expect(() => sandbox.confine(['true'], RO)).toThrow(SandboxUnavailableError)
|
||||
expect(probeBwrap).toHaveBeenCalledTimes(1)
|
||||
expect(probeLandlock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('a multi-rung chain probes a seatbelt rung like any other (the walk, not the platform table, decides)', async () => {
|
||||
// The product chains reach seatbelt only as darwin's sole (unprobed)
|
||||
// candidate; the chain seam exercises the probing path it would take in
|
||||
// a grown chain, keeping the default seatbelt probe honest.
|
||||
const exec = fakeSeatbeltExec(0)
|
||||
const probeBwrap = vi.fn(() => false)
|
||||
const { sandbox } = await setup({}, { chain: ['bwrap', 'seatbelt'], probeBwrap, seatbeltExec: exec })
|
||||
const confined = sandbox.confine(['true'], RO)
|
||||
expect(confined.argv[0]).toBe(exec)
|
||||
expect(confined.enforcement).toBe('full')
|
||||
expect(probeBwrap).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('a rogue chain entry throws via the probe walk\'s exhaustiveness guard (closed union)', async () => {
|
||||
// Same convention as the wrap switch below: the union is closed, so a
|
||||
// runner added later fails to compile at the probe switch instead of
|
||||
// silently selecting without a probe. Only a cast can reach the guard.
|
||||
const { sandbox } = await setup({}, { chain: ['chroot', 'bwrap'] as unknown as readonly ['bwrap'] })
|
||||
expect(() => sandbox.confine(['true'], RO)).toThrow('unreachable variant')
|
||||
})
|
||||
|
||||
it('a rogue cached runner tag throws via the exhaustiveness guard (closed union)', async () => {
|
||||
// The wrap switches on the chain verdict's runner tag and ends with
|
||||
// assertNever: a rogue tag (only reachable by a cast — the union is
|
||||
// closed and chainVerdict writes only its own literals) must throw, so a
|
||||
// runner added later fails to compile at the switch instead of silently
|
||||
// wrapping with another runner's dialect.
|
||||
const { sandbox } = await setup()
|
||||
;(sandbox as unknown as { selectedRunner: unknown }).selectedRunner = { runner: 'chroot', enforcement: 'full' }
|
||||
expect(() => sandbox.confine(['true'], RO)).toThrow('unreachable variant')
|
||||
})
|
||||
|
||||
it('runs the real default probes on the linux chain when none are injected (usable here or fail closed there)', async () => {
|
||||
// Pinning the platform (not the probes) makes the REAL defaultProbeBwrap
|
||||
// spawn run on every host: bwrap answers on a Linux box, ENOENT reads as
|
||||
// an unusable rung anywhere else — either way the walk is genuine.
|
||||
const { sandbox } = await setup({}, { platform: 'linux' })
|
||||
const verdict = (() => {
|
||||
try {
|
||||
sandbox.confine(['true'], RO)
|
||||
return 'usable'
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof SandboxUnavailableError) return 'unavailable'
|
||||
throw error
|
||||
}
|
||||
})()
|
||||
expect(['usable', 'unavailable']).toContain(verdict)
|
||||
})
|
||||
|
||||
it('walks the real platform chain when nothing is injected (usable here or fail closed there)', async () => {
|
||||
const { sandbox } = await setup({}, {})
|
||||
const verdict = (() => {
|
||||
try {
|
||||
sandbox.confine(['true'], RO)
|
||||
return 'usable'
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof SandboxUnavailableError) return 'unavailable'
|
||||
throw error
|
||||
}
|
||||
})()
|
||||
expect(['usable', 'unavailable']).toContain(verdict)
|
||||
})
|
||||
})
|
||||
|
||||
describe('the default landlock probe (launcher CLI contract)', () => {
|
||||
it('parses a fully-enforced probe report as full enforcement', async () => {
|
||||
const { sandbox } = await setup({}, { platform: 'linux', probeBwrap: () => false, landlockLauncher: fakeLauncher() })
|
||||
expect(sandbox.confine(['true'], RO).enforcement).toBe('full')
|
||||
})
|
||||
|
||||
it('parses a partially-enforced (older-ABI) probe report as partial enforcement', async () => {
|
||||
const launcher = fakeLauncher('landlock: partially enforced (older ABI)')
|
||||
const { sandbox } = await setup({}, { platform: 'linux', probeBwrap: () => false, landlockLauncher: launcher })
|
||||
expect(sandbox.confine(['true'], RO).enforcement).toBe('partial')
|
||||
})
|
||||
|
||||
it('reads a failing launcher as unusable: the chain ends and fails closed', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-fake-landlock-'))
|
||||
const launcher = join(dir, 'landlock-run')
|
||||
writeFileSync(launcher, '#!/bin/sh\nexit 125\n', { mode: 0o755 })
|
||||
const { sandbox } = await setup({}, { platform: 'linux', probeBwrap: () => false, landlockLauncher: launcher })
|
||||
expect(() => sandbox.confine(['true'], RO)).toThrow(expect.objectContaining({ code: SANDBOX_UNAVAILABLE }))
|
||||
})
|
||||
})
|
||||
|
||||
describe('probeTimeoutMs config', () => {
|
||||
it('rejects 0 at construction: Node treats a 0 spawnSync timeout as UNBOUNDED, the opposite of the field', async () => {
|
||||
const ctx = new Context()
|
||||
await expect(ctx.plugin(LocalSandboxProvider, { probeTimeoutMs: 0 }))
|
||||
.rejects.toThrow(/probeTimeoutMs must be a positive finite number/)
|
||||
})
|
||||
|
||||
it('bounds the default probes: a launcher slower than the configured timeout reads as unusable', async () => {
|
||||
// The same sleeping launcher passes under the default 5000ms budget and
|
||||
// fails under a 250ms one — the config demonstrably reaches spawnSync.
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-slow-landlock-'))
|
||||
const launcher = join(dir, 'landlock-run')
|
||||
writeFileSync(launcher, '#!/bin/sh\nsleep 1\necho "landlock: fully enforced"\nexit 0\n', { mode: 0o755 })
|
||||
|
||||
const patient = await setup({}, { platform: 'linux', probeBwrap: () => false, landlockLauncher: launcher })
|
||||
expect(patient.sandbox.confine(['true'], RO).enforcement).toBe('full')
|
||||
|
||||
const impatient = await setup(
|
||||
{ probeTimeoutMs: 250 },
|
||||
{ platform: 'linux', probeBwrap: () => false, landlockLauncher: launcher },
|
||||
)
|
||||
expect(() => impatient.sandbox.confine(['true'], RO)).toThrow(expect.objectContaining({ code: SANDBOX_UNAVAILABLE }))
|
||||
})
|
||||
})
|
||||
|
||||
describe('the default seatbelt probe (sandbox-exec contract)', () => {
|
||||
// The product chains reach seatbelt only unprobed (darwin's sole
|
||||
// candidate), so the default probe's contract is pinned through the chain
|
||||
// seam: a grown chain must probe it like any other rung.
|
||||
it('selects the rung when the executable applies the read-only profile and exits 0', async () => {
|
||||
const exec = fakeSeatbeltExec(0)
|
||||
const { sandbox } = await setup({}, { chain: ['bwrap', 'seatbelt'], probeBwrap: () => false, seatbeltExec: exec })
|
||||
const confined = sandbox.confine(['true'], RO)
|
||||
expect(confined).toEqual({
|
||||
argv: [exec, ...seatbeltProfileArgs(RO), '--', 'true'],
|
||||
enforcement: 'full',
|
||||
denialSignatures: ['operation not permitted'],
|
||||
runnerFailureSignatures: ['sandbox-exec: '],
|
||||
})
|
||||
})
|
||||
|
||||
it('reads a failing executable as unusable: the chain ends and fails closed', async () => {
|
||||
const { sandbox } = await setup({}, { chain: ['bwrap', 'seatbelt'], probeBwrap: () => false, seatbeltExec: fakeSeatbeltExec(1) })
|
||||
expect(() => sandbox.confine(['true'], RO)).toThrow(expect.objectContaining({ code: SANDBOX_UNAVAILABLE }))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,173 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { accessSync, constants, existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
|
||||
/**
|
||||
* KEYLESS publish-path rehearsal for this package's own distribution: the
|
||||
* provider must work from its PACKED tarball plus its REGISTRY launcher
|
||||
* dependency, not the git checkout. `pnpm pack` produces the EXACT bytes
|
||||
* `pnpm publish` would upload; this suite packs the workspace closure
|
||||
* (`dsh-sandbox-local` + its `@deepseek-ai` peers), installs the tarballs
|
||||
* into a throwaway consumer OUTSIDE the repo — npm resolving the
|
||||
* `node-addon-landlock-run` dependency (and its os/cpu-selected platform
|
||||
* package) from the public registry, the real consumer path — and drives
|
||||
* the INSTALLED packages under plain `node`: no tsx, no tsconfig paths, no
|
||||
* workspace resolution, so a `files`-list omission, a broken launcher
|
||||
* dependency, or a mode-stripped binary fails here instead of at the first
|
||||
* real install.
|
||||
*
|
||||
* World-proofs: the registry-installed launcher carries this host's ELF
|
||||
* architecture and IS executable (a tarball that loses the mode bit would
|
||||
* otherwise masquerade as a non-enforcing kernel — the fail-closed branch
|
||||
* below must never absorb that), and the installed provider confines a real
|
||||
* process THROUGH it (bwrap forced off) — or fails closed when the running
|
||||
* kernel does not enforce Landlock, which is itself the installed
|
||||
* fail-closed contract. Byte provenance of the launcher is the
|
||||
* `node-addon-landlock-run` repository's own release-pipeline concern.
|
||||
*
|
||||
* Self-skips off Linux or when the built `lib/` is absent (run
|
||||
* `pnpm run build` first — CI's landlock legs do).
|
||||
*/
|
||||
|
||||
const packageDir = fileURLToPath(new URL('..', import.meta.url))
|
||||
const repoRoot = fileURLToPath(new URL('../../../..', import.meta.url))
|
||||
|
||||
/** The closure the consumer needs: the package and its transitive `@deepseek-ai` peers; the launcher family arrives from the registry. */
|
||||
const WORKSPACE_CLOSURE = [
|
||||
'packages/sandbox/sandbox-local',
|
||||
'packages/sandbox/sandbox',
|
||||
'packages/llm/llm',
|
||||
'packages/util/brand',
|
||||
]
|
||||
|
||||
/** ELF `e_machine` (offset 18, LE) for this host: x86-64 = 62, AArch64 = 183. */
|
||||
const E_MACHINE = { x64: 62, arm64: 183 }[process.arch as 'x64' | 'arm64']
|
||||
|
||||
const packable = process.platform === 'linux'
|
||||
&& E_MACHINE !== undefined
|
||||
&& existsSync(join(packageDir, 'lib', 'index.js'))
|
||||
|
||||
let consumerDir = ''
|
||||
let workDir = ''
|
||||
/** The consumer script's JSON verdict (see its source below). */
|
||||
let verdict: {
|
||||
launcher: string
|
||||
launcherExists: boolean
|
||||
enforcing: boolean
|
||||
wrapArgv0?: string
|
||||
enforcement?: string
|
||||
exitCode?: number | null
|
||||
stderrHasDialect?: boolean
|
||||
confineOutcome?: string
|
||||
} = { launcher: '', launcherExists: false, enforcing: false }
|
||||
|
||||
describe.skipIf(!packable)('sandbox-local: packed-tarball distribution (publish-path rehearsal)', () => {
|
||||
beforeAll(async () => {
|
||||
const packDest = mkdtempSync(join(tmpdir(), 'dsh-pack-'))
|
||||
consumerDir = mkdtempSync(join(tmpdir(), 'dsh-packed-consumer-'))
|
||||
workDir = mkdtempSync(join(tmpdir(), 'dsh-packed-work-'))
|
||||
|
||||
// Pack each closure member with the exact bytes publish would upload.
|
||||
const tarballs: string[] = []
|
||||
for (const pkg of WORKSPACE_CLOSURE) {
|
||||
const pack = spawnSync('pnpm', ['pack', '--pack-destination', packDest], {
|
||||
cwd: join(repoRoot, pkg),
|
||||
encoding: 'utf8',
|
||||
timeout: 120_000,
|
||||
})
|
||||
expect(pack.status, `pnpm pack failed for ${pkg}:\n${pack.stdout}\n${pack.stderr}`).toBe(0)
|
||||
const lines = pack.stdout.trim().split('\n')
|
||||
tarballs.push(lines[lines.length - 1] as string)
|
||||
}
|
||||
|
||||
// A real consumer: plain ESM project, tarballs installed by npm — the
|
||||
// peer ranges (^0.0.1) resolve to the tarball versions, cordis pins to
|
||||
// the peer range's rc, and `node-addon-landlock-run` (with its
|
||||
// os/cpu-selected platform package, an OPTIONAL dependency of the entry
|
||||
// — so no `--omit=optional` here) comes from the public registry.
|
||||
writeFileSync(join(consumerDir, 'package.json'), JSON.stringify({ name: 'dsh-packed-consumer', private: true, type: 'module' }))
|
||||
const install = spawnSync('npm', ['install', '--no-audit', '--no-fund', ...tarballs, 'cordis@4.0.0-rc.6'], {
|
||||
cwd: consumerDir,
|
||||
encoding: 'utf8',
|
||||
timeout: 300_000,
|
||||
})
|
||||
expect(install.status, `npm install failed:\n${install.stdout}\n${install.stderr}`).toBe(0)
|
||||
|
||||
// The consumer script runs under PLAIN node against the installed
|
||||
// packages and reports a JSON verdict; every assertion happens back in
|
||||
// the test. bwrap is forced off so the wrap must select the INSTALLED
|
||||
// launcher; a non-enforcing kernel must surface the fail-closed error.
|
||||
writeFileSync(join(consumerDir, 'consumer.mjs'), `
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { Context } from 'cordis'
|
||||
import { launcherPath } from 'node-addon-landlock-run'
|
||||
import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local'
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LocalSandboxProvider, {})
|
||||
const sandbox = ctx.sandbox
|
||||
sandbox.internals = { probeBwrap: () => false }
|
||||
const launcher = launcherPath()
|
||||
const probe = spawnSync(launcher, ['--probe'], { encoding: 'utf8', timeout: 5000 })
|
||||
const out = { launcher, launcherExists: existsSync(launcher), enforcing: probe.status === 0 }
|
||||
const workdir = process.argv[2]
|
||||
if (out.enforcing) {
|
||||
const confined = sandbox.confine(['bash', '-c', \`echo hi > \${workdir}/denied.txt\`], { mode: 'read-only', workspaceRoot: workdir })
|
||||
out.wrapArgv0 = confined.argv[0]
|
||||
out.enforcement = confined.enforcement
|
||||
const run = spawnSync(confined.argv[0], confined.argv.slice(1), { encoding: 'utf8', timeout: 30000 })
|
||||
out.exitCode = run.status
|
||||
out.stderrHasDialect = /permission denied/i.test(run.stderr)
|
||||
} else {
|
||||
try {
|
||||
sandbox.confine(['true'], { mode: 'read-only', workspaceRoot: workdir })
|
||||
out.confineOutcome = 'wrapped'
|
||||
} catch (error) {
|
||||
out.confineOutcome = error?.code === 'SANDBOX_UNAVAILABLE' ? 'fail-closed' : String(error)
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify(out))
|
||||
`)
|
||||
const consumer = spawnSync('node', ['consumer.mjs', workDir], { cwd: consumerDir, encoding: 'utf8', timeout: 60_000 })
|
||||
expect(consumer.status, `consumer script failed:\n${consumer.stdout}\n${consumer.stderr}`).toBe(0)
|
||||
verdict = JSON.parse(consumer.stdout.trim().split('\n').pop() as string) as typeof verdict
|
||||
}, 480_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await Promise.all([consumerDir, workDir].filter(Boolean).map(dir => rm(dir, { recursive: true, force: true })))
|
||||
})
|
||||
|
||||
it('installs the registry launcher for this host: present, EXECUTABLE, right ELF arch', () => {
|
||||
const installed = join(consumerDir, 'node_modules', `node-addon-landlock-run-linux-${process.arch}`, 'bin', 'landlock-run')
|
||||
expect(existsSync(installed), 'platform package missing from the installed tree').toBe(true)
|
||||
// A tarball or extraction step that strips the mode bit would leave the
|
||||
// probe failing exactly like a non-enforcing kernel — assert it apart.
|
||||
expect(() => { accessSync(installed, constants.X_OK) }, 'installed launcher is not executable').not.toThrow()
|
||||
expect(readFileSync(installed).readUInt16LE(18), 'ELF e_machine').toBe(E_MACHINE)
|
||||
})
|
||||
|
||||
it('the installed provider resolves the launcher INSIDE the consumer node_modules platform package', () => {
|
||||
expect(verdict.launcher)
|
||||
.toBe(join(consumerDir, 'node_modules', `node-addon-landlock-run-linux-${process.arch}`, 'bin', 'landlock-run'))
|
||||
})
|
||||
|
||||
it('confines through the installed launcher (enforcing kernel) or fails closed (non-enforcing) — never unconfined', async () => {
|
||||
// Fail-closed is only the acceptable outcome when the installed binary
|
||||
// IS present and executable and the kernel merely does not enforce —
|
||||
// the first test pins that apart, so nothing hides behind this branch.
|
||||
expect(verdict.launcherExists, 'installed launcher missing').toBe(true)
|
||||
if (verdict.enforcing) {
|
||||
expect(verdict.wrapArgv0).toBe(verdict.launcher)
|
||||
expect(['full', 'partial']).toContain(verdict.enforcement)
|
||||
expect(verdict.exitCode).not.toBe(0)
|
||||
expect(verdict.stderrHasDialect, 'kernel denial text must match the advertised dialect').toBe(true)
|
||||
expect(existsSync(join(workDir, 'denied.txt'))).toBe(false)
|
||||
} else {
|
||||
expect(verdict.confineOutcome).toBe('fail-closed')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,120 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { homedir, tmpdir } from 'node:os'
|
||||
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'
|
||||
|
||||
/**
|
||||
* KEYLESS Seatbelt integration proof for the BACKEND: the REAL macOS
|
||||
* `sandbox-exec` confining REAL processes through `confine()` + a direct
|
||||
* spawn of the returned argv, with the Linux rungs forced off so the ladder
|
||||
* lands on Seatbelt. Verifies the WORLD (files exist or don't) and that the
|
||||
* kernel's denial text matches the dialect the wrap advertises; the
|
||||
* through-`ctx.bash` consumer proof lives with `@deepseek-ai/dsh-bash-sandbox`.
|
||||
*
|
||||
* Self-skips wherever the functional probe fails — every non-macOS host, or
|
||||
* a macOS whose `sandbox-exec` refuses the profile.
|
||||
*
|
||||
* Workspaces for the workspace-write tests live under the HOME directory on
|
||||
* purpose: `workspace-write` grants `/tmp` and the per-user temp dir
|
||||
* wholesale (the documented Seatbelt-profile temp areas), so only a
|
||||
* workspace OUTSIDE both proves the workspace-root grant itself.
|
||||
*/
|
||||
|
||||
const probe = spawnSync('sandbox-exec', [...seatbeltProfileArgs({ mode: 'read-only', workspaceRoot: '/' }), '--', 'true'], { timeout: 5_000, stdio: 'ignore' })
|
||||
const seatbeltUsable = probe.status === 0
|
||||
|
||||
let ctx: Context | undefined
|
||||
const tempDirs: string[] = []
|
||||
|
||||
afterEach(async () => {
|
||||
await ctx?.fiber.dispose()
|
||||
ctx = undefined
|
||||
await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
|
||||
})
|
||||
|
||||
async function tempDir(base: string): Promise<string> {
|
||||
const dir = await mkdtemp(join(base, 'dsh-seatbelt-e2e-'))
|
||||
tempDirs.push(dir)
|
||||
return dir
|
||||
}
|
||||
|
||||
async function provider(): Promise<LocalSandboxProvider> {
|
||||
ctx = new Context()
|
||||
await ctx.plugin(LocalSandboxProvider, {})
|
||||
const sandbox = ctx.sandbox as LocalSandboxProvider
|
||||
sandbox.internals = { probeBwrap: () => false, probeLandlock: () => 'unusable' }
|
||||
return sandbox
|
||||
}
|
||||
|
||||
/** Confine a shell command under `policy` and run it for real; returns the spawn result and the wrap's facts. */
|
||||
function runConfined(sandbox: LocalSandboxProvider, command: string, policy: SandboxPolicy) {
|
||||
const confined = sandbox.confine(['bash', '-c', command], policy)
|
||||
const result = spawnSync(confined.argv[0] as string, confined.argv.slice(1), { timeout: 30_000, encoding: 'utf8' })
|
||||
return { result, confined }
|
||||
}
|
||||
|
||||
describe.skipIf(!seatbeltUsable)('sandbox-local: real Seatbelt confinement through sandbox-exec', () => {
|
||||
it('read-only denies a write — the file must NOT exist, and the kernel speaks the advertised dialect', async () => {
|
||||
const workdir = await tempDir(tmpdir())
|
||||
const sandbox = await provider()
|
||||
const { result, confined } = runConfined(sandbox, `echo hi > ${workdir}/denied.txt`, { mode: 'read-only', workspaceRoot: workdir })
|
||||
expect(result.status).not.toBe(0)
|
||||
expect(confined.enforcement).toBe('full')
|
||||
// The wrap's denialSignatures must be what the kernel actually prints.
|
||||
expect(result.stderr.toLowerCase()).toContain('operation not permitted')
|
||||
expect(existsSync(join(workdir, 'denied.txt'))).toBe(false)
|
||||
})
|
||||
|
||||
it('read-only keeps the tree readable/executable and /dev/null writable', async () => {
|
||||
const workdir = await tempDir(tmpdir())
|
||||
const sandbox = await provider()
|
||||
const { result } = runConfined(sandbox, 'ls / > /dev/null && echo dev-ok', { mode: 'read-only', workspaceRoot: workdir })
|
||||
expect(result.status).toBe(0)
|
||||
expect(result.stdout).toBe('dev-ok\n')
|
||||
})
|
||||
|
||||
it('read-only grants no temp area: a write under the user temp dir is denied too', async () => {
|
||||
// The per-user darwin temp dir is a workspace-write grant, not a
|
||||
// read-only one — under read-only the only write-shaped path is /dev/null.
|
||||
const workdir = await tempDir(tmpdir())
|
||||
const sandbox = await provider()
|
||||
const target = join(workdir, 'tmp-denied.txt')
|
||||
const { result } = runConfined(sandbox, `echo hi > ${target}`, { mode: 'read-only', workspaceRoot: await tempDir(homedir()) })
|
||||
expect(result.status).not.toBe(0)
|
||||
expect(existsSync(target)).toBe(false)
|
||||
})
|
||||
|
||||
it('workspace-write lands a write inside the workspace root and still denies one beside it', async () => {
|
||||
const workdir = await tempDir(homedir())
|
||||
const outside = await tempDir(homedir())
|
||||
const sandbox = await provider()
|
||||
|
||||
const inside = runConfined(sandbox, `printf seatbelt-ok > ${workdir}/allowed.txt`, { mode: 'workspace-write', workspaceRoot: workdir })
|
||||
expect(inside.result.status).toBe(0)
|
||||
expect(readFileSync(join(workdir, 'allowed.txt'), 'utf8')).toBe('seatbelt-ok')
|
||||
|
||||
const denied = runConfined(sandbox, `echo hi > ${outside}/denied.txt`, { mode: 'workspace-write', workspaceRoot: workdir })
|
||||
expect(denied.result.status).not.toBe(0)
|
||||
expect(existsSync(join(outside, 'denied.txt'))).toBe(false)
|
||||
})
|
||||
|
||||
it('workspace-write grants /tmp and the user temp dir (the documented Seatbelt-profile temp areas)', async () => {
|
||||
const workdir = await tempDir(homedir())
|
||||
const hostTmp = await tempDir('/tmp')
|
||||
const userTmp = await tempDir(tmpdir())
|
||||
const sandbox = await provider()
|
||||
const { result } = runConfined(
|
||||
sandbox,
|
||||
`printf tmp-ok > ${hostTmp}/scratch.txt && printf user-tmp-ok > ${userTmp}/scratch.txt`,
|
||||
{ mode: 'workspace-write', workspaceRoot: workdir },
|
||||
)
|
||||
expect(result.status).toBe(0)
|
||||
expect(readFileSync(join(hostTmp, 'scratch.txt'), 'utf8')).toBe('tmp-ok')
|
||||
expect(readFileSync(join(userTmp, 'scratch.txt'), 'utf8')).toBe('user-tmp-ok')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user