Files
company-haness/docs/superpowers/specs/2026-07-11-p0-trust-boundary-hardening-design.md
T

8.3 KiB

P0 Trust-Boundary Hardening — Re-review Reflection (2026-07-11)

Context

A re-review judged the harness improved on the happy path but still fail-open on execution integrity: gates are prose-only, ledgers are agent-writable, and there is no proof the harness raises real output quality. This round closes the P0 trust boundaries the review found still open, fills company context with clearly-marked demo data, and replaces the fake benchmark recorder with a real runner+grader.

Prior rounds: p0-execution-integrity, p1-structural-quality, p1-tranche3-state-engine.

The honest trust model (why these fixes and not more)

Claude Code hooks cannot fully sandbox an agent that has Bash — a regex denylist is bypassable by construction (guard_tools says so itself). So the trust model is defense-in-depth + tamper-evidence, not cryptographic unforgeability. The only un-forgeable anchors are the payloads Claude Code itself supplies to hooks:

  • PostToolUse receipts: the real command, exit code, cwd, tool_use_id — the agent never authors these.
  • SubagentStart/Stop identity: agent_id, agent_type, spawn time.

Therefore the design principle for every ledger:

  1. Derive facts from receipt-backed artifacts, never from agent-authored strings.
  2. guard_tools blocks the write/exec paths an agent would use to forge or overwrite a ledger (direct Write/Edit, Bash redirection, python -c, and invoking the ledger scripts by hand).
  3. Validators cross-check agent claims against the trusted receipts (exact match, no substring/basename fuzz).
  4. Human approval is a documented soft-boundary: the harness cannot authenticate a human, so heavy-tier signoff binds to an out-of-band file that guard_tools protects from agent writes, and the limitation is stated openly (no false "human approved").

This is stated so we don't over-claim. The bar moves from "any agent can silently skip every gate" to "skipping a gate requires forging a Claude-Code-supplied receipt, which the wiring makes tamper-evident."

Workstreams

P0-1 — workspace-unset fail-closed

Add require_workspace() to _workspace.py (raises/returns sentinel). Every operational hook exits 2 (block) when workspace is unset instead of degrading to allow: state_engine guard, subagent_register, stop_validate, acceptance_log append, token_ledger mutations. Read-only queries and render paths stay advisory (exit 0) so a missing workspace never breaks reporting. --main Stop stays advisory.

P0-2 — context-package = real spawn gate

guard_tools PreToolUse gains Agent|Task. For an Org OS role/family subagent_type (one with a generated .claude/agents/<type>.md card; helpers like explore, general-purpose, plan exempt), the spawn is denied unless the prompt references a context-package whose file exists, validates, and whose hash matches the embedded reference. subagent_register records package_path + package_sha256 + workflow + role. context_package.py validator is upgraded from "is the field non-empty" to "does the referenced target-repo / must-read file / agent card / acceptance-test exist".

P0-3 / P0-5 — report identity, freshness, typed validation

  • report.schema.json: report-id, workflow-id, role-id, report-type become required; unknown report-type → reject (validator maps type→schema and errors on miss). Fix validate_report.py typed-merge so properties.update() no longer clobbers the common primary-artifacts.items constraint (deep-merge instead).
  • new_report.py --stub writes a valid report-type and identity fields.
  • stop_validate.py: a declared/looked-up report is accepted only if its workflow-id/role-id match the registry record and its created_at (fallback: file mtime) is >= registry started_at (ownership + freshness). This closes "return a peer's or stale report as mine". Priority-1 declared path gets the same ownership/freshness filter Priority-4 already implies.

P0-4 — ledgers as a trust boundary

  • guard_tools denies agent Write/Edit/NotebookEdit and Bash redirection/tee/dd/python -c/open(...,'w') targeting evidence/ledger.jsonl, state/**/workflow.yaml, state/**/state-events.jsonl, state/acceptance-events.jsonl, subagent-registry.jsonl. It also blocks Bash invocation of evidence_ledger.py, acceptance_log.py append, and state_engine.py transition from the agent (these run via hook wiring or the trusted CLI, not hand-typed forgery).
  • state_engine: preconditions derive from receipt-backed artifacts. transition actually checks the selected plan, allowed-by, an authorized actor (privileged HUMAN-* only via the trusted channel, not an agent-passed string), evidence-grade presence on standard+, and heavy plan-signoff on plan→run. De-emphasize the explicit facts override so a hand-written fact can't satisfy a gate absent the real artifact. Add a trusted append CLI (state_engine.py record-artifact) so commands stop editing YAML by hand.
  • acceptance_log append rejects events whose report-id does not resolve to a real file that passes validate_report. Ghost acceptances are refused.

P0-6 — evidence receipt binding

Receipts gain session_id, agent_id, workflow_id (from the Claude-Code payload / env), keep tool_use_id, cwd, ts, and store the artifact full resolved path + current hash. validate_report.py matching becomes exact: command equality (not substring), artifact full-path equality (not basename). Un-parseable exit code is recorded as null, never coerced to 0; an E4/E5 claim needs an explicit exit_code:0 receipt. E3 self-report exit-code:0 without a receipt cannot yield High confidence.

Root-cause R1 — company context (demo) + airtight E3 cap

Fill org-os/01-company/company-context.yaml with status: demo (explicitly not a real company) and a real projects[] manifest for _sandbox. Extend the E3 cap in validate_report.py to also block E3+ citations of company docs by absolute path and of CLAUDE.md, not only relative company paths — so a template can't be laundered into "real company evidence".

Root-cause R2 — skill + tier wiring

gen_agents.py + role-profiles/capability-families emit skills: [build-loop] on implementation families and thread tier→model/effort consistently through the cascade commands (/ground /decide /design /spec /build), not just /run-wave.

Root-cause R3 — golden-task E2E benchmark (real)

Replace benchmark.py arbitrary score-recorder with: (a) ≥3 golden tasks with objective acceptance checks; (b) a runner that invokes the claude CLI headless under two arms (plain vs harness); (c) an automated grader scoring first-pass acceptance / diff applies / tests pass / rework count. Honesty: the machinery is real and wired; a full run consumes API budget, so unrun state is reported as unrun (never fabricated scores).

Root-cause R4 — preview_ui generalization

Drive package-manager / build / out-dir / serve from the project manifest (pnpm/yarn/npm, Next/Vue/Vite) instead of hardcoded npm+vite+dist. Under strict mode, dump-dom and state-route capture failures fail rather than warn.

Secondary + hygiene

doctor.py dependency section-name mismatch (hidden section 3), nonexistent explicit workspace counted OK, and SSOT "comment mention = consumed" false positive; run-wave.md lens wording aligned to the 2-axis lens_cap; run_all.py per-suite timeout; lock / atomic-create on ledger append + report-path issuance. Git: stage all new files, confirm the mass deletions are intentional, re-run the CI entrypoint on a clean checkout.

Testing

Each workstream ships assertions in .claude/tests/ proving the negative case now blocks (forged receipt rejected, peer report rejected, ghost acceptance refused, unset-workspace blocks, uncertified spawn denied). run_all.py stays the single green entrypoint. New file: test_p0_trust_boundary.py.

Non-goals

  • True human authentication (documented soft-boundary).
  • Making regex denylists unbypassable (impossible; we raise the bar + add tamper-evidence).
  • A full paid benchmark run (machinery only; explicit opt-in to actually spend budget).