The price
Ceremony
Arming is a ceremony, not a config flip. Dual custody: host-level staging plus live hardware attestation.
One armable thing, and one only
The gauntlet has exactly one armable seam: the Watch producer. That is the seam that promotes a recorded fire toward the dispatcher, paid Council deliberation, and a signed Outbox directive. Everything else you can switch on is just posture.
Not the ceremony, no matter how official it feels:
- Starting Gateway arms nothing.
- Enabling Gateway enrolls nothing.
- Enrolling a credential arms nothing.
- Loading or enabling a Sentinel arms nothing.
- Enabling Governed routing arms nothing.
- Enabling the dispatcher alone arms nothing.
- The producer boot gate alone arms nothing.
The control ladder
The ceremony from the app: Touch ID, no terminal
On IRIN.app the full ceremony runs from Settings, beside the Enable Gateway control. Same stage, same challenge, same signature, same audit path; the terminal never appears. Arming is authorization for consequential behavior: the armed Watch producer can cause paid Council deliberation and a signed Outbox directive. It is not a convenience toggle.
Enroll
Once, from Settings.
Enroll the Mac's Touch ID credential. Enrolling is not arming: enrollment writes only the public credential record, and the app pins the bundled helper's SHA-256 and the registry keyset digest. A helper swap or a registry mismatch forces explicit re-enrollment.
Arm
Stage, tap, confirm.
The app stages the challenge, you tap Touch ID, then confirm. The result is the same signed active_arm row the source ceremony writes, and spend fails closed without it.
Disarm, then arm again
On your clock.
Disarm works any time, stops new claims, and never asks for a second factor. It cannot cancel provider work already in flight. To extend an arm, disarm and arm again: a fresh tap writes a fresh lease.
The control tells the truth about its state
"Touch ID ready" means enrolled and disarmed. "Rehearsal passed — not armed" is a distinct state after a rehearsal: the same stage, challenge, signature, and audit path ran, and the producer did not start. "Armed until" shows the live lease deadline while armed. Rehearsal is recommended before a real arm, and an unexpected prompt, a changed credential hash, or an expired stage is a stop condition, not a retry.
What enabling Gateway does not do
Enabling Gateway is not enrolling, and enrolling is not arming. Nothing runs at launch, and enabling Gateway arms nothing: the default enrollment registry is empty ([]) and loads UNLOADED, and the producer and dispatcher stay off at boot in every pack path; only the completed ceremony arms the producer. The app exposes the ceremony routes on loopback only inside its own Gateway Pack; every other deployment answers 404 and the ceremony stays on the management socket.
The ceremony from source, step by step
Six steps. This is the source-operator path; on the app, the Settings control above runs the same ceremony. Host-level staging first, live hardware attestation second. A stage that expires, a hash that changed, or a prompt you did not expect stops everything.
Step 1: Configure a principal
GW_ARM_PRINCIPALS=sovereign-op:<token>
SourceSet this in ~/.config/irin/gateway.env (permissions 0600), then restart Gateway. The token is never a CLI argument.
Step 2: Enroll hardware
gateway/bin/arm-enroll # Touch ID / Secure Enclave gateway/bin/arm-enroll-fido2 # or a FIDO2 key
SourceOnly the public credential record is written. Then verify the keyset hash with gateway/bin/verify-attest-keyset-hash against the sidecar boot log.
Step 3: Rehearse
gateway/bin/arm --rehearse
SourceExpect rehearsal-ok. The producer does not start. Stop conditions: an unexpected biometric prompt, a changed credential hash, an expired stage, or a writer-claim conflict.
Step 4: Pre-arm checklist
- Council, Gateway, and Watch are healthy.
- The intended tenant and Sentinel profile are confirmed.
- Explicit spend ceilings are set.
- The dispatcher credential is present.
- The last rehearsal passed on the current build and keyset.
- No other writer holds the same
watch.db.
Step 5: Arm
gateway/bin/arm
SourceExpect armed. The flow: stage, then an SE-P256 or FIDO2-ES256 hardware confirm bound to the content, cap, and window, then a signed active_arm row. Stages expire (default 120 s, ARM_STAGE_TTL_MS) and cannot be reused.
Step 6: Verify before any test fire
- Producer state is armed as expected.
- The writer claim is held by this runtime.
- The arm audit row exists.
- Spend metrics are visible and sane.
Ceremony routes
Disarm: the fast exit
Arming always blocks on the second factor. Emergency disarm never does. Any authorized principal can pull the plug, over the management socket, never the public listener.
gateway/bin/disarm
SourceRuns over the management socket, never the public listener. Any authorized principal may disarm, and it never requires a second factor. An unexpected hardware prompt during disarm is an alarm, not a feature.
WATCH_PRODUCER_ENABLED=false
FallbackIf the binary path is unavailable: stop the runtime, set this, restart. The producer boot gate stays down.
During an incident
- Never delete the state volume. Never run
docker compose down -v: the volume holds diagnostic evidence. - After resolution: verify DB and ledger integrity, restart, and pass a fresh rehearsal before any later arm.
Spend ceilings: the price, capped
Thought is rare, so it gets a budget. These are the numbers the ceremony signs up for, quoted exactly.
min(daily_ceiling, charge_unit * claims_per_tick + in_flight_at_disarm). The worst case is arithmetic, not vibes.The boot triple-gate is not an arm
WATCH_PRODUCER_ENABLED + EXECUTION_MODE=LIVE + WATCH_DISPATCHER_GATEWAY_KEY starts the producer but never writes active_arm. Spend reserves fail closed without the ceremony.
The verify lane: proof without price
One command proves the whole closed loop from a fresh clone to a signed directive, with zero spend and zero hardware. This is an engineering lane that runs from a source checkout, not part of the app.
make verify make verify-down
ProofFresh clone to signed directive in one command. Prints the signed directive_outbox row and the elapsed time; the under-120-second clock starts after image availability.
Guarantees
- No real provider keys. Throwaway
openssl randdev secrets go into.env.demo; it never touches.env. - No Touch ID or hardware arm. A software P-256 key stands in, same as CI.
- Full isolation: compose project
irin-demo, ports 28080 and 28765 (never 18080), its own volumes, and an ephemeral ledger key under.demo-state/. Safe to run alongside live stacks. - Everything else runs the real production code path.
- It never builds unattended and never pulls Docker Hub by default.
- Requirements: git, make, docker with compose v2, openssl, python3, jq.
Knobs
The honest limits
- The verify lane proves the closed loop with a software key, not the hardware path. The real Touch ID arm is Day-2 operator setup.
- Quarantine and probation:
fails_to_trigger=2, backoff 60/300/1800/3600 s, hard-kill after 5 cycles in 1 h. - Disarm is not a cancel: in-flight provider work may finish and settles after disarm.
- Backup requires BOTH the state volume and the signing key. One without the other is not a backup.