ADR-0012: Amend ALLOWED_BINARIES with npm, bwrap, sandbox-exec, jq (amends Phase 2 ADR-0001)¶
Status: Accepted Date: 2026-05-17 Tags: subprocess-discipline · allowed-binaries · amendment · supply-chain Related: 0006, 0007, Phase 2 ADR-0001, production ADRs
Context¶
Phase 2 ADR-0001 (the omnibus subprocess-discipline ADR for Phase 2) established ALLOWED_BINARIES as a closed frozenset in src/codegenie/exec/__init__.py: every external CLI invocation must route through run_allowlisted / run_external_cli, and the allowlist is amendment-only. The forbidden-patterns pre-commit hook bans subprocess.run(..., shell=True), os.system, os.popen, eval(, exec(, __import__(, and pickle.loads repo-wide.
Phase 3 needs four binaries Phase 2 did not enable:
npm— for the recipe engine'snpm install --package-lock-only, the Stage-6 validate'snpm install+npm test. Without it, the roadmap exit criterion is unmeetable.bwrap— the LinuxSubprocessJailadapter (per ADR-0006). The jail spawns child processes underbwrap --unshare-all --new-session --die-with-parent ....sandbox-exec— the macOSSubprocessJailadapter (per ADR-0006). Even though it's deprecation-flagged by Apple, it's the only built-in macOS sandbox primitive Phase 3 can use without Phase 5's Lima/DinD infrastructure.jq— operator-tooling adjunct used by debugging helpers (codegenie audit verify | jq ...patterns documented in the runbook); occasionally invoked by integration tests to inspect JSON event streams.
Phase 0/2 ADRs require an explicit ADR amendment to extend the allowlist. The architecture spec calls this out explicitly (phase-arch-design.md §Goal G6, §Agentic best practices, §Path to production §Phase 3 ADRs #P3-008).
Options considered¶
- Option A — Don't amend; route
npmthrough a wrapper that calls Python'srequestsdirectly to the npm registry. Re-implements npm. Unbounded scope, breaks at every npm semantic change. Pattern: wheel reinvention. - Option B — Amend
ALLOWED_BINARIESwith all four binaries, with a one-line justification per addition and the smallest possible privilege envelope per use. Pattern: Allowlist extension with documented rationale. - Option C — Bypass the allowlist with a Phase-3-specific
run_jailedfunction that does NOT go throughrun_allowlisted. Splits the discipline; creates a parallel path subprocess invocations can hide in. Pattern: Subprocess discipline violation.
Decision¶
Adopt Option B. Amend src/codegenie/exec/__init__.py::ALLOWED_BINARIES to add: npm, bwrap, sandbox-exec, jq. Each addition is justified inline + cross-referenced to this ADR. The forbidden-patterns pre-commit hook continues to ban subprocess.run(..., shell=True), os.system, os.popen, eval(, exec(, __import__(, pickle.loads repo-wide — this amendment changes the allowlist, not the forbiddenlist.
The SubprocessJail adapters (BwrapAdapter, SandboxExecAdapter) wrap bwrap / sandbox-exec via run_external_cli — they do NOT bypass the chokepoint.
java is NOT added in Phase 3 (the OpenRewriteRecipeEngine is scaffolded but not invoked by Phase-3 workflows per ADR-0009). Phase 7 amends to add java when it enables OpenRewrite for distroless workflows.
Tradeoffs¶
| Gain | Cost |
|---|---|
Single chokepoint preserved — every subprocess goes through run_external_cli, even the jail adapters |
Four new binaries the Phase-0 tool_readiness startup check must verify on every operator/CI machine |
| One ADR captures every Phase-3 amendment — easy diff at Phase-3-arrival time; easy audit at Phase-7-arrival time | ADR maintenance: any future binary addition must amend the allowlist + cross-reference this ADR + a new ADR |
npm is a published binary with reasonable provenance (npm registry, GitHub Releases); jail isolation handles untrusted-script risk per ADR-0006 |
npm itself has had supply-chain incidents historically; --ignore-scripts at both CLI and env (per ADR-0006) mitigates |
bwrap is a well-audited containment primitive; sandbox-exec is deprecation-flagged but still functional |
macOS migration to Lima/DinD is on Phase 5; Phase 3 carries the deprecation risk explicitly |
jq is an operator-tooling convenience; integration tests use it to inspect JSON streams |
One more binary in the allowlist; very small attack surface (jq is read-only over stdin) |
| The amendment-only allowlist discipline survives — Phase 3 doesn't open a hole for ad-hoc subprocess use | New plugins must amend the allowlist explicitly + ADR; no silent additions |
Pattern fit¶
Implements Adapter pattern at the subprocess boundary (toolkit §Behavioral patterns) — run_external_cli is the single Adapter wrapping every binary; the allowlist is the closed kernel feature. Composes with Hexagonal Port (ADR-0006) — the SubprocessJail Adapters use run_external_cli internally, never subprocess.run directly. Honors Open/Closed at the kernel boundary — adding a new binary is an additive ADR, not a kernel rewrite.
Consequences¶
src/codegenie/exec/__init__.py::ALLOWED_BINARIESadds 4 entries with one-line per-entry comments referencing this ADR.tests/unit/exec/test_allowlist.pyasserts the exact contents (any drift fails CI).- Phase 0's
tool_readinesscheck extends to verifynpm,bwrap(Linux only),sandbox-exec(macOS only),jqare on$PATH; missing → structured warning at orchestrator init. BwrapAdapterandSandboxExecAdapter(ADR-0006) wrap their respective binaries viarun_external_cli— nosubprocess.rundirect calls.- The architecture spec's Goal G6 ("Zero edits to Phase 0/1/2") is explicitly satisfied: the only Phase-0/1/2 edits permitted are extending
ALLOWED_BINARIES(via this ADR) and addingimport-lintercontracts. - Phase 7 amends
ALLOWED_BINARIESwithjava(forOpenRewriteRecipeEngineinvocation); the precedent established here is the model. forbidden-patternshook is unchanged — the forbidden list grows independently of the allowlist.- New invariant: any new binary requires an ADR + the one-line allowlist amendment + comment cross-reference.
Reversibility¶
High. Removing a binary from the allowlist is a one-line code change + a fence test update. The forward-direction (adding) and reverse-direction (removing) are equally cheap. The constraint that matters is the social one: don't add a binary without an ADR.
Evidence / sources¶
../phase-arch-design.md §Goal G6("Zero edits to Phase 0/1/2 — the only permitted edits: extending ALLOWED_BINARIES"), §Agentic best practices, §Path to production §Phase 3 ADRs #P3-008../final-design.md §Roadmap coherence check — Phase 0 row (allowlist extension), §Synthesis ledger- Phase 2 ADR-0001 — subprocess-discipline omnibus (the document this amends)
src/codegenie/exec/__init__.py::ALLOWED_BINARIES(the closed frozenset this amendment edits)