CC-2_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-2. Branch: audit/cc-2-tenant-durable-execution.
Work orders: WO-17, WO-18, WO-19, WO-20, WO-32. All P0 except WO-32 (P2). Lane: ENG + SEC.
You own: services/service-action-runner/execution_adapters/{base,portfolio,meta_ads,registry,flags,
ratelimit,inventory_gate}.py, the shared tenant resolver — CX-1 resolved it: `contracts/mizoki_contracts/auth.py` (resolver) and
`contracts/mizoki_contracts/tenancy.py` (registry); strict settings are TENANT_STRICT / MIZOKI_TENANT_STRICT.
NOTE: CC-1 (PR #1016) already touched contracts/mizoki_contracts/{validators,decision_objects,holdouts}.py — base your
branch on main AFTER #1016 merges, or on the #1016 head, and do not edit those three files, and
services/service-policy-engine/pacing_veto.py. Do NOT edit google_ads.py or credentials.py (CX-3) or
service-action-runner/main.py (CC-1 — PR #1016 rewrote its promotion/dispatch path; read that version). CC-1 exposed a holdout
resolver in main.py for you; `execution_adapters/base.py::check_holdout` is still presence-only — wire it to that resolver (WO-04 handoff). If DCP main.py must change for WO-18, make the smallest
possible change and flag it in the report — CC-1 is editing that file concurrently.
Why this pack exists: the audit showed exposure caps and freezes living in process memory (two $70
checks pass a $100 cap → $140 exposure), a tenant resolver that accepts any tenant when its registry
read fails, treasury checked from one source at policy time and another at execution time, and an
execute() exception path that skips the freeze handler.
WO-17 Fail closed on empty/unavailable tenant registry
Step 0: mock the registry read to raise; assert resolve_tenant("anything") currently succeeds.
Fix: distinguish RegistryUnavailable from RegistryEmpty. In strict mode (make strict the default
for all serving paths; allow non-strict only under an explicit env flag documented in the module
docstring) unknown tenant → refused; unavailable → refused unless a last-good cache entry exists
with age < TENANT_REGISTRY_CACHE_TTL_S (default 300) — and log that the cache was used.
Add ownership enforcement: DCP reads of stored decisions and runner execute/rollback/outcome
writes must check record.tenant == caller tenant.
Accept: raise → refused; empty+strict → refused; cache within TTL → allowed with audit log;
cache past TTL → refused; cross-tenant read of a stored decision → 404; cross-tenant
rollback/outcome write → 403.
WO-18 One versioned constraint state, Decide → settlement
Step 0: show that a proposal can pass the policy-engine treasury check while DCP/Act holds no
reservation (the startup global-file path is empty/optional).
Fix: a single ConstraintResolver(tenant) returning {version, currency, treasury_cap, exposure_cap,
horizon, fetched_at} sourced from the tenant onboarding vault (the same source policy uses).
Admission, reservation, execution and settlement all call it and record constraint_version on
the action. Execution refuses if constraint_version != the version the reservation was made
under, or if fetched_at is older than CONSTRAINT_MAX_AGE_S. Delete or hard-deprecate the
optional global startup file path (leave a loud error if the env var is still set).
Accept: policy pass + no reservation → execution refused; version drift → refused; stale → refused;
matching → allowed and all four stages log the same version.
WO-19 Persistent atomic reservations and freezes
Files: execution_adapters/portfolio.py, base.py.
Step 0: two threads reserve $70 each against a $100 cap using the current dict → both pass.
Fix: reservations and freezes move to a transactional store behind a small interface
(ReservationStore with reserve(tenant, account, amount, cap) → ok|refused atomically,
release(), freeze(tenant, account, reason), is_frozen()). Provide an in-memory transactional
fake for tests and a Firestore implementation (transactions/conditional writes) — do NOT run it
against a live project; unit-test the Firestore implementation with the emulator or a mock
that enforces transaction semantics.
Accept: 2 threads/2 processes $70+$70 vs $100 → exactly one passes; restart mid-reservation
(drop the process-local object, re-instantiate) → reservation still held; freeze set in
one instance is visible in another.
WO-20 Cover the whole mutation-and-verification interval
Files: execution_adapters/base.py (and meta_ads.py as the reference adapter).
Step 0: raise an ambiguous exception (e.g., timeout after the provider call) inside adapter.execute
and show the freeze handler is not entered.
Fix: one guarded span: dispatch → provider call → read-back → verify, with the action state machine
proposed → validated → authorized → dispatched → confirmed | uncertain | failed →
compensated | closed persisted at every transition. Any exception after dispatch → state
uncertain + freeze + reconcile-before-retry; retry is only permitted from a reconciled state.
Accept: fault-injection tests for: provider success + client timeout; crash after provider success;
duplicate delivery of the same authorization; each → no second provider mutation, freeze
recorded, state = uncertain until reconcile marks confirmed/failed.
WO-32 Certification evaluator enforced on every promotion
Files: execution_adapters/portfolio.py; the certification evaluator referenced from
decision_meter.py (read-only for you; if enforcement must live in DCP, write the call site
in portfolio/registry and report the DCP hook needed).
Fix: promotion calls the evaluator per tenant/account/action_class; no bypass path or flag.
Accept: promotion without a certification record → refused; with a record for another action_class → refused.
Gates: pytest services/service-action-runner services/service-policy-engine tests/governance;
skill_sync.py --audit if present. PR title: "Audit pack C (backend) — tenant boundaries + durable
execution (WO-17..20, 32)".