Part of the DSH plugin suite — six Apache-2.0 plugins for DeepSeek Harness.
macOS backend for dsh-witness and dsh-anchor: uchg immutable flags + a sandbox-exec deny view give macOS the same six-dimension evidence protection the other platforms have. Every capability claim carries an experiment number and a control group.
中文版见 README.zh-CN.md。
dsh-witness's task evidence chain needs a sandbox at the same level on all three platforms. Windows uses a six-dimension NTFS ACL closure, Linux uses chattr +i + bubblewrap — macOS is the last piece of the puzzle:
| Capability | Windows (dsh-witness) | Linux (dsh-cross-platform) | macOS (this package) |
|---|---|---|---|
| Overwrite-proof | NTFS ACL deny | chattr +i + bwrap read-only view | sandbox-exec deny view |
| Delete-proof | NTFS ACL deny | chattr +i | chflags uchg |
| Forge-proof lock/exit.txt | ACL + guard handle | chattr +i | chflags uchg |
| Write isolation view | Restricted token | bubblewrap (EROFS) | sandbox-exec (allow default) + surgical deny |
| Process identity | PowerShell StartTime | /proc//stat + btime | ps -o lstart |
| Exit protocol | EXIT:<code> |
EXIT:<code> |
EXIT:<code> (fully aligned) |
Each task is supervised by the runner with the following sequence (aligned with the Linux architecture verdict):
node detach-runner-macos.cjs <jobDir> <outFile> <exitFile> <commandBase64>
① lock (wx exclusive) lock ← "pid:startSec" (ps -o lstart, EXP-3 measured format)
② Pre-create protocol files exit.txt (truncate-rewritten per lifecycle) + out.log (append)
③ uchg only on protocol files chmod 444 → chflags uchg: exit.txt / lock (the runner no longer writes them)
④ sandbox-exec wraps the task (allow default) (deny file-write* (subpath "<realpath(jobDir)>"))
—— network available, outside-directory writable, in-directory writes all blocked (EXP-3 P1 three-case control winner)
⑤ Output pipe capture stdout/stderr → out.log (out.log not uchg'd — the deny view protects it)
⑥ Exit nouchg → write EXIT:<code> → delete lock → process exits
Two key verdicts (EXP-2): on macOS 26, sandbox-exec with no explicit default implicitly defaults to deny — a task missing (allow file-read*) gets silently SIGKILLed by the kernel; /bin/bash must use an absolute path (execvp's PATH lookup inside the sandbox is refused). The deny subpath must use the realpath'd path (/tmp → /private/tmp symlink mismatch silently disables the sandbox).
- Overwrite/delete/forge-proof — writes, deletes, and forgeries of task-directory files inside the sandbox view all fail (smoke 9/9 attack items blocked).
- Three-evidence process identity —
ps -o lstartstart-time formula (EXP-3 PS-LSTART measured + smoke ±5s); PID-reuse protection at the same level as Windows/Linux. - Capability parity without shrinkage — network available inside the sandbox, writable outside the task directory, same capabilities as the bubblewrap view (EXP-3 same-battery control).
- Full protocol alignment —
EXIT:<code>,lock = pid:startSec, O_EXCL race semantics identical to the two published backends; the registry switches platforms without noticing. - macOS-specific hardening —
chmod 444same-user write protection (EXP-1 measured); Windows/Linux have no such free lunch.
# Install (git source, pinned tag — the pre-npm installation path)
dsh plugin --profile <name> add "github:Wang-Lin-Chang/dsh-macos#v0.1.0"import { MacosSandboxBackend } from 'dsh-macos'
const backend = new MacosSandboxBackend('/path/to/job-dir')
backend.apply() // chflags uchg evidence files (before task spawn)
backend.verify() // fail-closed self-check (aligned EXIT:-998 semantics)
backend.restore() // restore after task death (registry terminal-state window)Direct runner usage (same protocol as detach-runner.cjs / detach-runner-linux.cjs):
node detach-runner-macos.cjs <jobDir> <outFile> <exitFile> <commandBase64>test/witness-final-macos-test.ts — 12 scenarios / 34 assertions, 34/34 ×3 stable (GitHub Actions macos-latest · macOS 26.5.2 arm64 · Node 25.9). Plus: runner smoke 9/9.
| Category | Scenario | Assertions |
|---|---|---|
| Persistence A | Survives restart / zombie recovery (SIGKILL) / cursor-based output read / ID collision-free | 4 |
| Adoption coordination B | 50-process O_EXCL race yields exactly one terminal state / cross-session adoption / silent-task protection / PID-reuse guard | 4 |
| Event sourcing C | Event log complete and ordered / autopsy report generated | 2 |
| Sandbox boundary D | Overwrite-proof / delete-proof (sandbox-exec deny view + uchg) | 2 |
Run it yourself: npm test (node --experimental-strip-types test/witness-final-macos-test.ts).
All experiments live in EXPERIMENTS.md: 4 verdicts, each with a control group. Three decisive results:
- EXP-1: uchg full-control true pass (before +uchg writable and deletable → after +uchg overwrite/delete/rm -f all BLOCKED → after restore writable again).
- EXP-2: the silent-SIGKILL mystery — implicit default deny + missing
(allow file-read*), consistent across three reruns. - EXP-3: profile three-case same-battery control —
(allow default)wins (IN-WRITE-BLOCKED / OUT-WRITE-ALLOWED / network 200).
src/
├── detach-runner-macos.cjs # task runner (sandbox-exec wrap + uchg, protocol fully aligned with Windows/Linux)
├── macos-backend.mjs # sandbox backend (uchg apply/verify/restore + capability self-report)
└── macos-utils.mjs # ps lstart start time + process liveness + exit-code protocol
vendor/
├── detach-runner-macos.cjs # self-contained runner copy (for registry integration)
└── lib/ # WitnessJobRegistry compiled output (darwin branch)
- macOS measured environment: GitHub Actions macos-latest (macOS 26.5.2 arm64) + Node 25.9. No local Mac — all verdicts come from the CI lab, and no other macOS version has been measured.
- sandbox-exec is deprecated by Apple: it still works on macOS 26 and its behavior is measured; if a future version removes it, this package's capability claims expire with the experiments (see EXPERIMENTS.md experiment 1, item 6, the sandbox_init probe record).
- sandbox-exec implicit default = deny (EXP-2 verdict): the profile must explicitly write
(allow default)or the full allow set, otherwise the task is SIGKILLed. - The
/tmpsymlink trap: the deny subpath must use the realpath'd path, otherwise the/tmp → /private/tmpmismatch silently disables the sandbox (the runner has realpath built in). - Offline applicability: all sandbox mechanisms are local syscalls (chflags/sandbox-exec), no network components; multi-day offline runs are not measured, not claimed.
- uchg timing aligned with the Linux lesson: uchg only on exit.txt/lock (the runner doesn't write them); out.log relies on the deny view.
npm test # 12 acceptance scenarios, macOS edition (needs macOS; see EXPERIMENTS.md lab notes)Requires: Node ≥ 22.6 (node:sqlite, measured on 25.9), macOS (sandbox-exec / chflags).
Apache-2.0