CC-1_CLAUDE_CODE.txt
You are working in the MIZOKI-3-5/MIZOKICloudRun repository (MIZ OKI 3.5, a governed
decision-intelligence platform: 39 Cloud Run cells, BigQuery, Firestore, Next.js command-center UI).
Read in this order before touching code: CLAUDE.md, CONSTITUTION.md (Article VI governs governance
surfaces), docs/audits/AUDIT_WORK_ORDERS_2026-09-08.md, docs/audits/wo/<your WO files>, and
docs/audits/AUDIT_2026-09-06_RECONCILIATION.md if it exists (CX-1 writes it).
Ground rules
- Audit findings are HYPOTHESES pinned at commit fc8b03f9. main has moved. Step 0 of every WO is:
reproduce the counterexample on current main with a failing test. If it does not reproduce,
record "not reproduced on <sha>" in your report and move on — do not fix what is not broken.
- Fail closed. Every fix must make a refusal path explicit and tested. Never widen access to make a test pass.
- No new architecture. Reuse the existing modules named in the WO. If a WO says "reuse X", reuse X.
- Tests are the deliverable. Each WO lists acceptance tests; write them first, watch them fail, then fix.
Name them test_wo<nn>_<what>. Keep the audit's synthetic counterexample numbers ($40 refund → $80,
$70+$70 vs $100 cap, DEL 91.7, etc.) as fixtures so the regression is recognizable.
- Branch: audit/<PROMPT-ID>-<slug>. NEVER use a claude/* branch (they auto-merge to main in seconds).
Commit per WO with message "WO-nn: <title>". Open ONE PR for the prompt when done. Do not merge.
- Protected paths (.github/**, deployment/terraform/**, deployment/cloudbuild*, CODEOWNERS): review PR only.
- Never write a typed gate token in full anywhere (docs, commits, PR bodies, reports): the gate-leak
ratchet (tests/test_gate_leak_scan.py) turns main red. Refer to it only as APPROVED: [MERGE].
- Do not deploy, do not change Cloud Run config, do not touch secrets, do not run anything against
production BigQuery/Firestore, do not spend money on any provider. Local + test fixtures only.
- Coordination: before starting, run
python scripts/claude_memory.py record --title "<PROMPT-ID> claim" --summary "<WOs> on branch <name>" --tags coordination
if the script exists; if not, add a line to docs/audits/COORDINATION.md.
- Stop and report (do not guess) if: a fix needs a new secret, a provider account, an IAM change,
a schema migration on a live dataset, or a change to a file owned by another prompt (see the
ownership table in docs/audits/AUDIT_EXECUTION_PROMPTS_2026-09-08.md).
Final report — write docs/audits/reports/<PROMPT-ID>_REPORT_<date>.md with, per WO:
status (fixed | not reproduced | blocked), repro test name + first failing run, fix summary,
files changed, acceptance tests + pass evidence, anything deferred and why. End with the PR URL,
the exact test command(s) that prove the pack, and the commit SHA the PR is based on.
PROMPT-ID: CC-1. Branch: audit/cc-1-spending-gates.
Work orders: WO-01, WO-02, WO-03, WO-04, WO-05, WO-06, WO-07, plus WO-47 (added by the v1.1 review). All P0. Lane: ENG + SEC.
You own: services/service-policy-engine/**, services/service-validation-orchestrator/**,
services/service-decision-control-plane/**, services/service-approval-routing/**,
services/service-action-runner/main.py, tests/governance/test_decision_control_plane.py,
tests/governance/test_action_runner.py. Do not edit services/service-action-runner/execution_adapters/**
(CC-2/CX-3 own it) — if a fix needs it, write the interface you need in main.py and report the gap.
Why this pack exists: the audit showed that a proposal can be authorized and spend money while a hard
economic/consent/identity check has failed, with unbound evidence, an asserted approver, and a
double-redeemed approval. This pack makes every one of those a terminal refusal.
WO-01 Hard-gate failures are terminal
Files: services/service-policy-engine/main.py (pass-rate / DEL eligibility),
services/service-validation-orchestrator/main.py (six-check media validator; incremental_profit check).
Step 0: build a fixture where the incremental-profit check FAILS and the other five checks pass at
100%; assert policy currently returns ELIGIBLE (audit: DEL 91.7). That is the failing test.
Fix: introduce an explicit check taxonomy: HARD = {economic (incremental profit, treasury), integrity,
consent, policy}, RANK = everything else. Compute eligibility as: if any HARD check failed →
INELIGIBLE with reason codes, before DEL is computed; DEL only ranks candidates that passed.
Do not implement this as a weight tweak. The validator must emit per-check {name, class, passed}.
Accept: (a) each HARD check failing alone, all others 100% → INELIGIBLE; (b) hypothesis/property test:
for any vector of RANK scores, a HARD failure never flips to ELIGIBLE; (c) existing eligible
fixtures still pass (no regression in the happy path).
WO-02 Bounded exploration class
Files: services/service-policy-engine/main.py.
Fix: eligibility_class ∈ {standard, exploration}. exploration requires envelope_id (approved
exploration budget record), cap, and a logged assignment probability; it is NOT exempt from HARD
checks. Store the class on the decision record.
Accept: exploration candidate without envelope_id → refused; with envelope over cap → refused;
HARD failure under exploration → refused.
WO-03 Bind evidence passports to the decision
Files: services/service-decision-control-plane/main.py, decision_meter.py.
Step 0: reproduce: supply a passport for a different tenant with an invalid seal and stage the actuator
registry so Stage-4 is reachable; assert a signed authorization is currently issued.
Fix: resolve the passport as an immutable record by id; verify seal; require passport.tenant ==
decision.tenant, passport.action_fingerprint == fingerprint(decision.action), passport.model_version
and horizon present, validity window covers now. Put those bound fields INSIDE the signed
authorization payload so a later reader can re-verify. Distinct reason codes:
PASSPORT_FOREIGN_TENANT, PASSPORT_SEAL_INVALID, PASSPORT_STALE, PASSPORT_ACTION_MISMATCH, PASSPORT_ALTERED.
Accept: one test per reason code → refused; a valid bound passport → authorized and the signature
verifies over the binding fields; tampering any bound field after signing → verification fails.
WO-04 Holdout registration is proved, not asserted
Files: services/service-decision-control-plane/main.py (experiment sufficiency), and the interface
services/service-action-runner/main.py uses to check holdouts. If the check lives in
execution_adapters/base.py, do NOT edit it — expose a resolver in main.py and report.
Fix: sufficiency = registry lookup of holdout_id returning {tenant, registered_at, salt_version}
with registered_at < first_exposure_at and tenant match. The proposer boolean is ignored.
Accept: unregistered id, post-exposure registration, other-tenant registration → refused;
properly registered → passes.
WO-05 Approver identity from authentication only
Files: services/service-approval-routing/main.py + the principal-auth module it imports.
Step 0: HTTP test: service principal S sends approval with body.actor = "some human"; assert it currently succeeds.
Fix: approver identity and role come from the verified principal only. Body actor fields are either
ignored or must equal the principal (mismatch → 400). A service principal can never satisfy a
HUMAN approval requirement; a human principal without the required role → 403.
Accept: three HTTP tests: service+body-human → 403; human-wrong-role → 403; human-right-role → 200
and the stored approval carries the principal's verified id, not the body string.
WO-06 Rollback proof artifact before promotion
Files: services/service-action-runner/main.py (promotion path), tests/governance/test_action_runner.py.
Fix: promotion to any executing stage requires a stored RollbackProof {drill_id, tenant, account,
action_class, executed_at, outcome=success, evidence_ref} matching the exact tenant/account/action
class. Registration's rollback_demonstrated flag becomes advisory metadata only.
Accept: flag=True + no proof → refused; proof for a different action_class → refused;
matching proof → promotion allowed. ops/remediation/live_proof.py may be READ for the
proof shape; do not modify it.
WO-07 Single-use approval under concurrency
Files: services/service-decision-control-plane/main.py (redemption), its Firestore/DB access layer.
Step 0: with a fake transactional store, redeem the same approval from two threads; assert two
distinct authorization ids are issued today.
Fix: atomic claim (transaction/conditional write) on the approval record; authorization_id =
deterministic hash(approval_id, decision_fingerprint, tenant); the stored authorization is
returned verbatim on any retry, including after a simulated crash between claim and persist
(two-phase: claim → persist → mark complete; a retry that finds claim-without-persist completes it).
Accept: 50-way concurrent redemption → exactly one authorization id and 49 identical replays;
crash-after-claim retry → same id; crash-after-persist retry → same id; contention at every
write boundary covered by a fault-injection test.
WO-47 DCP get_decision returns any tenant's DecisionProof (from docs/audits/wo/WO-47.md)
Finding (confirmed on main 635318712): services/service-decision-control-plane/main.py get_decision (~lines 504-508)
returns any DecisionProof by id to any allow-listed caller with no resolve_tenant call, while list_decisions
and decisions_summary do resolve tenant. Passport assembly reads through this route.
Step 0: HTTP test: tenant B caller requests tenant A's decision id; assert it currently returns the document.
Fix: resolve the caller's tenant and refuse with 404 (never a 403 that confirms existence) when doc["tenant_id"]
does not match; apply the same rule to /decision/{id}/chain and the passport GET. Pairs with WO-03 and WO-05.
Accept: tenant-B → 404 with no body fields on all three routes; tenant-A positive path unchanged.
Gates before the PR: full pytest for the four services + tests/governance; skill_sync.py --audit if
present (mizoki_canon.py --check is a no-op — do not cite it as a gate); ruff/black if configured.
PR title: "Audit pack B — spending admission hard gates (WO-01..07, WO-47)". Include the report path.