fix(sandbox): address PR #309 review — TOCTOU direction, denial metadata, shared roots, docs

- fs-sandbox: delegate the mutation with the freshly re-canonicalized target
  (not the stale one), so the checked identity is the mutated identity — a
  symlink swapped in after resolve() can no longer escape workspace-write.
- tool-fs: map a denial to an FsError carrying FS_SANDBOX_DENIED (not a plain
  Error), so ToolRegistry keeps the structured code on result.error for
  retry/observers while the message stays the shared marker.
- sandbox-local: derive the Seatbelt writable set from the shared
  writableRoots() helper, so the profile and the fs fence cannot drift.
- gen-doc-graphs: ctx.sandboxPolicy is owned by dsh-sandbox-policy and read only
  by the sandboxed executor/provider (the tool layers use the pure fold).
- docs: bash-sandbox/bash/permission READMEs and bash.md reflect the relocated
  policy home and the sandbox/mode rename; drop the stale stdout.golden.jsonl.
This commit is contained in:
kingwl
2026-07-20 13:59:18 +08:00
parent 1dd5757897
commit 2530bf8aa3
11 changed files with 65 additions and 107 deletions

View File

@@ -4,9 +4,8 @@
* @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 { writableRoots } from '@deepseek-ai/dsh-sandbox'
import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox'
/**
@@ -36,31 +35,23 @@ export function landlockProfileArgs(policy: SandboxPolicy): string[] {
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.
* Build the sandbox-exec arguments and SBPL profile for one policy. The
* writable roots come from the shared {@link writableRoots} helper (canonical,
* deduplicated) so the Seatbelt grant and the in-process fs fence
* (`@deepseek-ai/dsh-fs-sandbox`) can never drift apart.
* @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))]
const roots = writableRoots(policy)
if (roots.length > 0) {
forms.push(`(allow file-write* ${roots.map(root => `(subpath ${sbplString(root)})`).join(' ')})`)
}
return ['-p', forms.join(' ')]