Skip to content

Repository files navigation

dsh-macos

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

license ci topic: dsh-plugin topic: dsh

Why this exists

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)

The sandbox anatomy

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).

What you get

  • 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 identityps -o lstart start-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 alignmentEXIT:<code>, lock = pid:startSec, O_EXCL race semantics identical to the two published backends; the registry switches platforms without noticing.
  • macOS-specific hardeningchmod 444 same-user write protection (EXP-1 measured); Windows/Linux have no such free lunch.

Quick start

# 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>

Acceptance evidence

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).

Experiments

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).

Components

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)

Honest boundaries

  • 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 /tmp symlink trap: the deny subpath must use the realpath'd path, otherwise the /tmp → /private/tmp mismatch 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.

Development

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).

License

Apache-2.0

About

macOS backend for dsh-witness/dsh-anchor: uchg + sandbox-exec sandbox recipes. Every claim carries an experiment number.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages