This document is the “from nothing” path: start from a clean clone of the repo
and end at a passing make test, with the ability to produce kernel-derived
extracts (via Ghidra) that feed the repo’s contracts.
If you are running inside a sandboxed harness, disambiguate harness restraint from policy denial early (system python is fine before the venv exists):
PYTHONPATH="$PWD" python3 tools/inside/inside.py --jsonBefore running make cold-start, verify prerequisites are in place. This
checklist catches missing dependencies before they surface as cryptic errors.
# Run from repo root after cloning
# === Toolchain ===
swift --version # Swift 5.9+
cmake --version # CMake 3.9+
python3 --version # Python 3.10+
# === Homebrew packages ===
brew list nlohmann-json >/dev/null 2>&1 && echo "nlohmann-json: ok" || echo "nlohmann-json: MISSING"
brew list argp-standalone >/dev/null 2>&1 && echo "argp-standalone: ok" || echo "argp-standalone: MISSING"
brew list dyld-shared-cache-extractor >/dev/null 2>&1 && echo "dyld-shared-cache-extractor: ok" || echo "dyld-shared-cache-extractor: MISSING"
# === Venv ===
[ -x .venv/bin/python ] && echo "venv: ok" || echo "venv: MISSING (run: python3 -m venv .venv)"
# === Venv packages (after venv exists) ===
.venv/bin/python -c "import pytest" 2>/dev/null && echo "pytest: ok" || echo "pytest: MISSING"
.venv/bin/python -c "import frida" 2>/dev/null && echo "frida: ok" || echo "frida: MISSING"
.venv/bin/python -c "import lief" 2>/dev/null && echo "lief: ok" || echo "lief: MISSING"
.venv/bin/python -c "import mcp" 2>/dev/null && echo "mcp: ok" || echo "mcp: MISSING"
# === Ghidra (required for rebaselining; extracts staged later) ===
brew list ghidra >/dev/null 2>&1 && echo "ghidra: ok" || echo "ghidra: MISSING"
[ -x /opt/homebrew/opt/ghidra/libexec/support/analyzeHeadless ] && echo "analyzeHeadless: ok" || echo "analyzeHeadless: MISSING (check GHIDRA_HEADLESS path)"
java -version 2>&1 | grep -q "21\." && echo "java 21: ok" || echo "java 21: MISSING"
# === Optional ===
which duckdb >/dev/null 2>&1 && echo "duckdb: ok" || echo "duckdb: MISSING (optional, for frida queries)"If all required items show "ok", proceed with make cold-start.
- macOS (Seatbelt tooling is macOS-specific).
- A baseline world exists under
world/and is selected viaworld/registry.json.
Optional but recommended baseline check:
PYTHONPATH="$PWD" python3 tools/doctor/doctor.py --world <world_ref> --out /tmp/pawl_doctor- Xcode Command Line Tools (for
swift, codesign tooling, and general developer utilities).- Verify:
swift --version - Provides:
clang(native helpers),codesign,xcrun,nm,otool, and other build staples.
- Verify:
Some workflows require additional CLIs that are commonly installed via Homebrew:
Build dependencies (forward compiler):
cmake(buildspawl/forward/src/).nlohmann-json(JSON parsing for CARTON vocabulary).argp-standalone(CLI argument parsing on macOS).
Analysis tooling:
duckdb(required fortools/fridaquery/index; seetools/frida/README.md).ghidra(providesanalyzeHeadless; used bytools/ghidra).temurin@21(or another Java 21 distribution; used for headless Ghidra runs).node/npm(required for building TypeScript hooks undertools/frida/hooks_ts/).
Kernel extraction:
dyld-shared-cache-extractor(extracts dyld shared cache for Ghidra analysis):brew tap keith/formulae brew install dyld-shared-cache-extractor
Optional:
graphviz(generates publication figures underprojections/figures/).
- Python 3.10+.
venv+pip.
This repo assumes explicit invocation (no shell activation dependency):
PYTHONPATH="$PWD" ./.venv/bin/python …
- Swift 5.9+ (
integration/carton/runtime/graph/Package.swiftisswift-tools-version: 5.9). - No network fetch is expected for the Swift graph package (no SwiftPM deps).
Ghidra is required for rebaselining on a new macOS version. The actual extraction is staged later (section 3) because outputs are large and host-specific, but the tooling must be installed upfront.
Kernel extraction runs headlessly and writes to integration/evidence/ghidra/local/ (gitignored).
Required:
- Java 21 (Temurin 21 is used in repo examples; see
tools/ghidra/README.md). - Ghidra with a usable
analyzeHeadlesspath (Homebrew default used in repo examples). dyld-shared-cache-extractorinPATH(used bytools.ghidra.core.stage; override via--dyld-extractor).
Runtime probes execute via PolicyWitness under runtime/. Built-in runner
works immediately for SBPL-only probes; entitlement-bearing probes require
installed external runners:
- BYOXPC (
PWRunner.byoxpc.xpc) - MachMe (
PWRunneras a Mach service)
These require a GUI session (launchd bootstrap) and a signing path (it can be ad hoc signing).
The install commands are in runtime/PolicyWitness.md and the safe template path is
described in runtime/external/README.md.
From repo root:
python3 -m venv .venv
PYTHONPATH="$PWD" ./.venv/bin/python -m pip install -U pipInstall the minimum Python dependencies for make test:
PYTHONPATH="$PWD" ./.venv/bin/python -m pip install \
pytest==7.4.3 \
frida \
lief \
mcpNotes:
pytest==7.4.3is a known-good version in this repo.fridais required fortools/frida(spawn/attach capture).liefis required forpawl/reverse/ops_extract.pyMach‑O scanning.mcpis required for the convergence MCP server (orchestration/mcp_server.py).
From repo root (recommended for cold starts):
make cold-startThis runs venv-check, builds the forward compiler, then runs the full test harness.
Alternatively, if you've already built the forward compiler:
make testWhat make test does (high level):
- Validates CARTON (
PYTHONPATH="$PWD" ./.venv/bin/python -m integration.carton build) - Runs pytest (
PYTHONPATH="$PWD" ./.venv/bin/python -m pytest) - Builds the Swift graph package (
integration/carton/runtime/graph/swift_build.py)
If tests skip due to missing private Ghidra outputs, that’s expected on a fresh checkout; the next section describes how to generate the minimal extracts that turn those skips into passes.
These are not required for make test, but they are part of the repo’s
instrumentation surface.
Build a minimal libsandbox compile probe:
PYTHONPATH="$PWD" ./.venv/bin/python -m pawl.structure compile integration/fixtures/pawl/sample.sb --out /tmp/sample.sb.binBuild and ad-hoc sign the sandbox_check() validator tools:
./tools/check/build.sh
./tools/check/hold_open/build.shThe forward lane (pawl/forward/) contains container_profile, a C tool that
reconstructs SBPL from container metadata. make cold-start builds this
automatically; this section is for manual builds or troubleshooting.
Prerequisites (Homebrew):
brew install cmake nlohmann-json argp-standaloneManual build:
make forward-buildOr directly:
cd pawl/forward/src
mkdir -p ../build && cd ../build
cmake ../src && makeThe resulting binary lands in pawl/forward/build/bin/container_profile.
container_profile loads operation and filter vocabulary from the CARTON bundle
(integration/carton/contract/bundle/.../vocab/). There are no frozen
per-release tables—the tool only supports --platforms host (CARTON).
If you are building on a macOS version that differs from the repo's pinned
baseline (see world/registry.json), the CARTON vocabulary may be stale:
| Symptom | Likely cause |
|---|---|
| Unknown operation errors | New ops added in your macOS version |
| Filter argument mismatches | Filter signature changed |
| Scheme alias failures | Surface name mappings drifted |
To update CARTON vocabulary for a new host:
- Run
make carton-refreshto regenerate mappings - If ops/filters changed, re-extract from Ghidra (section 3)
- Re-run
make testto verify
If you're on an unreleased macOS version, expect vocabulary drift until you complete the extraction workflow in section 3.
This section is the “repo learns from the kernel” path. It is intentionally separate from the core test harness: local extracts are host-bound and gitignored, and public packs are sanitized outputs used to prove extraction fidelity and to seed downstream work.
Start with the connector guide:
tools/ghidra/README.md
Always set Java explicitly. PyGhidra headless execution is sensitive to Java discovery; missing or implicit Java can produce flaky, non-reproducible behavior and stale outputs. All automation invoking Ghidra must pass a known JDK explicitly.
# Required: explicit Java and Ghidra paths
export JAVA_HOME="$(/usr/libexec/java_home -v 21)"
export GHIDRA_HEADLESS="/opt/homebrew/opt/ghidra/libexec/support/analyzeHeadless"Verify Java is correctly set before running Ghidra tasks:
echo "JAVA_HOME=$JAVA_HOME"
$JAVA_HOME/bin/java -versionPyGhidra requirement: The Ghidra scripts in this repo are Python scripts. If
headless runs fail with Ghidra was not started with PyGhidra. Python is not available, you need to run Ghidra through PyGhidra instead of plain
analyzeHeadless. Create a wrapper script:
#!/bin/bash
# Save as e.g. /usr/local/bin/pyghidra_headless
exec /opt/homebrew/opt/ghidra/libexec/support/pyghidraRun -H "$@"Then set GHIDRA_HEADLESS to point at this wrapper, or invoke it directly.
Post-run verification: After Ghidra runs, verify expected output files actually changed. A "successful" run that produces stale or empty outputs is a common failure mode when Java or paths are misconfigured.
Staging materializes host inputs (KC, libs, profiles) under the private tree. Do not commit staged inputs; they are intentionally gitignored.
PYTHONPATH="$PWD" ./.venv/bin/python -m tools.ghidra.core.stage --build-id <build_id>Some tests are wired to skip unless specific private outputs exist (see
integration/support.py).
Run the following tasks (examples; adapt build id as needed):
PYTHONPATH="$PWD" ./.venv/bin/python tools/ghidra/run_task.py kernel-imports --build <build_id> --exec
PYTHONPATH="$PWD" ./.venv/bin/python tools/ghidra/run_task.py sandbox-kext-conf-scan --build <build_id> --exec
PYTHONPATH="$PWD" ./.venv/bin/python -m tools.ghidra.commands.refresh_canonical --name offset_inst_scan_0xc0_write_classify
PYTHONPATH="$PWD" ./.venv/bin/python -m tools.ghidra.commands.refresh_canonical --name kernel_collection_symbols_canaryExport writes a public pack under integration/evidence/ghidra/public/<build_id>/
containing sanitized metadata only.
PYTHONPATH="$PWD" ./.venv/bin/python -m tools.ghidra.commands.export_extract --build-id <build_id>make testIf you staged/extracted for a build id that differs from the baseline fixtures, you may still see skips. At that point you're in "rebaseline" territory: see the next section for how to detect and understand drift.
When you move to a new macOS version or libsandbox changes, PAWL's pinned
evidence may become stale. The forward rebaseline command helps you
understand what drifted.
When to run:
- After a macOS upgrade
- When tests fail with evidence staleness errors
- To characterize a new host before classifying artifacts
Run the rebaseline check:
PYTHONPATH="$PWD" ./.venv/bin/python -m pawl.pawl forward rebaselineWhat the output tells you:
| Drift type | What it means |
|---|---|
| Probes changed | Compiled blob bytes differ — libsandbox compilation behavior changed |
| Groups changed | Operation membership in group-ops changed — e.g., a new op falls under file-read* |
| Fallbacks changed | The _operation_info fallback chain structure changed |
The command writes a JSON report to .../op_table/sb/rebaseline/rebaseline_report.json
with full details. Use --force to recompile even if outputs exist.
What to do with drift:
This is a checkpoint, not a gate. The first time you rebaseline on a new host will be a learning process — you're discovering what changed, not fixing it automatically.
- Review the drift report to understand the scope of changes
- If probes changed: the pinned blobs need updating (re-run atlas builders)
- If groups changed: Source-Evaluate assumptions may need updating
- If fallbacks changed: re-extract from Ghidra, update canonical tables
For the full technical story (authority hierarchy, group derivation, fallback
chains), see pawl/forward/README.md section "Forward Rebaseline".
For world-based artifact management plans and theories, see
world/BASELINING.md.
Start with:
runtime/README.mdruntime/PolicyWitness.md
Upstream:
This repo vendors a pinned bundle at runtime/bin/PolicyWitness.app (symlinked from runtime/PolicyWitness.app for compatibility).
PYTHONPATH="$PWD" ./.venv/bin/python -m runtime.core.statusFollow the authoritative install flows in runtime/PolicyWitness.md:
- “Install a BYOXPC runner”
- “Install a MachMe runner”
When installed, verify:
PW="$PWD/runtime/bin/PolicyWitness.app/Contents/MacOS/policy-witness"
$PW runner verify --service-name <service_name> --timeout-ms 2000Start here:
orchestration/api.py(plan/run facade)orchestration/probe_plan_builder.py(probe-plan generation)runtime/core/executor.py(runtime execution contract)
Build a plan from a casefile and persist it under a local output root:
PYTHONPATH="$PWD" ./.venv/bin/python - <<'PY'
from orchestration.probe_plan_builder import build_probe_plan_for_casefile
result = build_probe_plan_for_casefile(
"path/to/casefile",
persist_root="/tmp/pawl_plan/example_plan",
)
print(result.plan_ref)
PYBuild a full orchestration plan envelope from the same casefile:
PYTHONPATH="$PWD" ./.venv/bin/python - <<'PY'
from orchestration import plan_casefile
plan = plan_casefile(
"path/to/casefile",
persist_root="/tmp/pawl_plan/example_orchestration",
)
print(plan.probe_plan_path)
print(plan.transport_status)
PYFrom repo root:
make testIf make test fails:
- Re-run
tools/insideto confirm you are not interpreting harness restraint as policy denial. - Re-run
PYTHONPATH="$PWD" ./.venv/bin/python -m runtime.core.statusto confirm runtime readiness. - Re-run
integration/carton builddirectly to isolate CARTON drift from pytest failures:PYTHONPATH="$PWD" ./.venv/bin/python -m integration.carton build