This directory contains the Swift implementation of the ephemeral sandbox runner shipped inside dist/PolicyWitness.app.
PolicyWitness is specimen-first:
- The controller (
policy-witness) starts a fresh XPC runner instance per specimen. - The XPC host stays unsandboxed and spawns two short-lived children:
pw-probe-runner— the sandboxed C worker that applies the specimen policy (SBPL source + params) exactly once to itself and runs the probe plan's attempts.sb_api_validator --batch— queriessandbox_checkfor each probe against the worker's PID.
- The host joins both children's outputs into a single
PWRunnerRunResultenvelope and replies. Because the host never applies the specimen policy, default-deny policies can't block the XPC reply path.
The runner follows SwiftPM-convention layout: the core library sources
compiled into PWRunner.xpc live under runner/Sources/PWRunnerCore/,
each C shim under its own runner/Sources/<Shim>/ (with include/), the
thin XPC client under runner/Clients/PWRunnerClient/, and the service
bundle scaffolding under runner/Services/PWRunner/.
Sources/PWRunnerCore/PWRunnerAPI.swiftPWRunnerProtocol(runSpecimen(Data) -> Data)- Codable JSON types:
PWRunnerRunSpec,PWRunnerPolicySpec,PWRunnerProbeStep, and the returnedPWRunnerRunResult
Sources/PWRunnerCore/SandboxLib.swift- Explicit
dlopen+dlsymbindings for libsandbox. SandboxLib.load(path:)defaults to/usr/lib/libsandbox.dylib; re-routed by_test_overrides.libsandbox_path(see "Test seam" below).
- Explicit
Sources/PWRunnerCore/SandboxApply.swift- Policy hashing and single-shot
sandbox_applypath.
- Policy hashing and single-shot
Sources/PWRunnerCore/ProbeRunner.swiftsandbox_checkhelpers and shared prediction-unavailable metadata.
Sources/PWRunnerCore/PathUtils.swift- Path normalization and fd-based observation helpers.
Sources/PWRunnerCore/Signals.swift- Deny-signal handler and counters.
Sources/PWRunnerCore/CWorker.swift- Host-side driver for
pw-probe-runner: shm_open + mmap + posix_spawn, sentinel polling, and the post-apply hook.
- Host-side driver for
Sources/PWRunnerCore/ValidatorClient.swift- Host-side driver for
sb_api_validator --batch: concurrent stdin/stdout viapoll()to avoid pipe deadlock, partial-evidence failure result.
- Host-side driver for
Sources/PWRunnerCore/CWorkerOrchestrator.swift- Joins the C worker and the validator child into a single
PWRunnerRunResult. Owns probe-plan validation,prediction_unavailablehost mirror, classification, and drift.
- Joins the C worker and the validator child into a single
Sources/PWRunnerCore/PWRunnerService.swift- Orchestrates the host flow (decode → validate → drive C worker + validator → reply).
- The host enforces caller authorization, loads libsandbox once to fail
fast on missing dynamic loaders, computes
policy_sha256, and never callssandbox_applyon itself.
Clients/PWRunnerClient/main.swift- Builds
dist/PolicyWitness.app/Contents/MacOS/pw-runner-client: a thinNSXPCConnectionwrapper that forwards JSON bytes and prints the runner's JSON reply.
- Builds
Services/PWRunner/Info.plist,Entitlements.plist,main.swiftfor the standard runner XPC service bundle. Debug-attach inspection goes through BYOXPC.
External to runner/ but conceptually part of the runner:
controller/tools/pw_probe_runner/pw_probe_runner.c(+pw_probe_runner_abi.h) — the C worker that owns the post-apply syscall surface. Built once and embedded inside each XPC service bundle as…/Contents/MacOS/pw-probe-runner(not the app's top-levelContents/MacOS/) so built-in and BYOXPC runners both resolve the binary relative to their own bundle. Driven byCWorker.swift; therunner_c_worker_harnesssuite exercises it in isolation as a regression pin.controller/tools/sb_api_validator/sb_api_validator.c— the batch validator. Same bundle-local embedding story; driven byValidatorClient.swift; wire contract pinned bytests/suites/validator_batch_mode/.
Package.swift declares a test-only SwiftPM layout that mirrors the
source set build.sh ships in PWRunner.xpc. The runner_unit suite
runs the PWRunnerCoreTests executable via:
swift run --package-path runner PWRunnerCoreTestsThe executable hand-rolls a small XCTest-shaped harness so the test
target works on Command Line Tools alone (full Xcode not required).
PWRunnerCore is built with -enable-testing so tests can
@testable import it. Production builds keep going through build.sh;
SwiftPM's .build/ tree is gitignored.
See runner/AGENTS.md → "Swift runner unit tests (SwiftPM)" for the
contract, when to reach for a unit test vs an e2e suite, the rules around
stubbing @convention(c) C function pointers, and how to add a new test file.
The request JSON accepts an optional _test_overrides block that
re-routes narrow boundaries through real production code so the test
suite can reach failure outcomes (libsandbox_unavailable,
worker_spawn_failed, runner_timeout) without stubbing returns.
Every honored override is mirrored back into
data.runner_result.test_overrides; production runs leave that field
unset.
| Key | Default | Re-routed boundary | Outcome it lets you reach |
|---|---|---|---|
libsandbox_path |
/usr/lib/libsandbox.dylib |
dlopen in SandboxLib.load(path:) (host pre-spawn check) |
libsandbox_unavailable |
worker_executable_path |
bundle-local pw-probe-runner |
posix_spawn path in CWorker.spawn |
worker_spawn_failed |
worker_timeout_ms |
60000 (floored at 50) | Host-side sentinel deadline in CWorker.run |
runner_timeout |
validator_executable_path |
bundle-local sb_api_validator |
posix_spawn path in ValidatorClient.runValidator |
validator_spawn_failed |
worker_post_apply_hang_ms |
0 (disabled) | --post-apply-hang-ms argv to pw-probe-runner |
runner_timeout |
worker_post_apply_kill_signal |
0 (disabled) | --post-apply-kill-signal argv to pw-probe-runner (worker self-signals after applied, before done) |
runner_sandbox_denied |
See runner/AGENTS.md → "Testing normalized_outcome failure paths via
_test_overrides" for the full contract, the four-assertion test
recipe, and the rules for adding a new override.
The runner consumes a PWRunnerRunSpec which contains:
policy:sbplsource (with optionalparamsandaugments)probe_plan: ordered probe steps (sandbox_check + attempt)
policy.augments is resolved upstream by the controller (the runner
itself is augment-agnostic — by the time a request reaches
PWRunnerService.runSpecimen, the field has been stripped and any
named augment contents have been spliced onto policy.sbpl_source).
See PolicyWitness.md → Augments for the wire surface and the
single shipped augment (exec_baseline — three allows that let a
libSystem-dynamic helper spawn under (deny default)).
Top-level fields:
pidis the sandboxed C worker's PID whenrunner_subprocessis present; use it for unified-log correlation.runner_subprocesscarries the worker's{ exit_code, term_signal, partial_steps }. The classifier inCWorkerOrchestratormaps the sentinel state + waitpid outcome tonormalized_outcome:donesentinel flipped + clean exit →ok; signal-before-done →runner_sandbox_denied(the precise signal stays interm_signal); host SIGKILL after sentinel timeout →runner_timeout; failure toposix_spawn→worker_spawn_failed.validator_subprocesscarries the validator child's{ pid, exit_code, term_signal }, or isnullwhen no validator ran (every probe was in the prediction-unavailable set, or spawning the validator failed).
Per-step fields under steps[]:
sandbox_checkincludesscope(post_sandbox) plus the originalfilter_valueand a best-efforteffective_filter_value(forpathfilters, the runner's normalized path). It also reportspid,operation,filter_type_id, anderrno/errorwhen the check call fails.attemptalways includesexit_codeandsyscall_errno(explicitnullwhen not applicable).requested_pathechoes the attempt target for every attempt kind;normalized_pathandobserved_pathare file-path diagnostics and arenullfor non-file attempts. Thercanderrnofields are retained for compatibility.driftis a bool when both verdicts are available and comparable, ornullwhen no comparison is possible (validator didn't run for the step, op+filter is in the prediction-unavailable set, or the attempt didn't produce a verdict).
The standard built-in runner ships with minimal entitlements. External (BYOXPC) runners can carry additional hardened-runtime exceptions for inspection and controlled extensibility (debug attach / dynamic loading / dyld env / executable memory). These do not make sandbox policy “dynamic”.
Built-in runners can require a signed caller before accepting XPC connections. The check is controlled via Info.plist keys:
PWRunnerRequireSignedCaller(bool)PWRunnerAllowedIdentifiers(optional array of code signing identifiers)
When enabled, the runner compares the caller’s Team ID to its own Team ID and optionally enforces the allowlist. External runners are unaffected unless they opt in by adding the same keys.
Sandbox policy variation is driven by the specimen itself:
- the controller supplies SBPL,
pw-probe-runnerapplies it once to itself before running probes,- the runner's witness pairs the attempt result with the validator's
sandbox_checkverdict for each probe and surfaces disagreement assteps[].drift.
PolicyWitness can target external runner services when entitlements are
required. An external runner is the same PWRunner implementation, but signed
with user-selected entitlements and registered with launchd as a BYOXPC
runner: a signed .xpc bundle, addressed by CFBundleIdentifier.
Invariants:
- The protocol is unchanged (
PWRunnerProtocolJSON-over-Data). - One specimen -> one runner process; the runner applies the sandbox once and exits.
- Evidence schema remains identical; the controller records runner provenance.
The controller provides a policy-witness runner manager to install/register
these services and to enforce entitlements supersets before dispatch.
Some development harnesses run tools inside an OS sandbox. In those environments:
- XPC lookup can fail early with
NSCocoaErrorDomain4099 / error 159"Sandbox restriction"(before the service launches). - Unified Logging access can also be restricted, making deny-evidence capture impossible from inside the harness.
Treat this as an environment constraint, not a PolicyWitness regression.
If you suspect you are running under a sandboxed automation harness, re-run from a normal Terminal (or with escalation) before debugging PolicyWitness itself.