56 lines
2.7 KiB
TypeScript
56 lines
2.7 KiB
TypeScript
/**
|
|
* The writable-root derivation shared by every enforcement dialect that
|
|
* expresses a mode as a canonical allow-list: `workspace-write` means "the
|
|
* workspace root plus the platform temp areas", and this module is that
|
|
* meaning's one home. The Seatbelt profile
|
|
* (`@deepseek-ai/dsh-sandbox-local`) and the in-process filesystem fence
|
|
* (`@deepseek-ai/dsh-fs-sandbox`) both derive their allow-list here, so "the
|
|
* write tool cannot write /tmp but bash can" asymmetries cannot arise between
|
|
* them. The bwrap and Landlock dialects keep their own grant spellings (an
|
|
* ephemeral `/tmp` mount, launcher-owned flags) — the honest per-runner
|
|
* differences recorded in the sandbox RFC — with parity pinned by test.
|
|
*
|
|
* @module dsh-sandbox/roots
|
|
*/
|
|
|
|
import { realpathSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import type { SandboxExecutionPolicy } from './index.ts'
|
|
|
|
/**
|
|
* Resolve a granted root to the path the enforcement layer actually compares:
|
|
* canonical (symlinks resolved), because both Seatbelt filters and the fs
|
|
* fence's containment check match resolved paths — `/tmp` IS `/private/tmp`
|
|
* on darwin, and an as-spelled grant would match nothing.
|
|
* @param path - the root as configured or platform-reported.
|
|
* @returns the canonical path, or the spelling as-is when resolution fails
|
|
* (a missing root matches nothing until it exists — the conservative
|
|
* outcome; inventing a fallback would grant a path the caller never named).
|
|
*/
|
|
export function canonicalPath(path: string): string {
|
|
try {
|
|
// Node's JavaScript realpath implementation lexically collapses `..`
|
|
// before resolving a preceding symlink on some platforms. The native
|
|
// implementation follows the filesystem's component-by-component lookup,
|
|
// matching chdir/spawn and the enforcement layers this identity feeds.
|
|
return realpathSync.native(path)
|
|
} catch {
|
|
// realpathSync.native failed: the path (or a prefix) is missing or unreadable.
|
|
return path
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The roots one confined execution may WRITE under — the mode's meaning as a
|
|
* canonical, deduplicated allow-list. `read-only` allows nothing;
|
|
* `workspace-write` allows the policy's workspace root, the host `/tmp`, and
|
|
* the per-user platform temp dir (`os.tmpdir()` — the real temp area for
|
|
* mkstemp-family tools; omitting it would deny what the mode promises).
|
|
* @param policy - the file-effect policy to derive the allow-list from.
|
|
* @returns the canonical writable roots; empty exactly under `read-only`.
|
|
*/
|
|
export function writableRoots(policy: SandboxExecutionPolicy): string[] {
|
|
if (policy.mode !== 'workspace-write') return []
|
|
return [...new Set([policy.workspaceRoot, '/tmp', tmpdir()].map(canonicalPath))]
|
|
}
|