The price

Ceremony

Arming is a ceremony, not a config flip. Dual custody: host-level staging plus live hardware attestation.

Stage TTL 120 s default Arm window 24 h default Daily cap $50 per UTC day Disarm no second factor, ever installed app runs from Settings, Touch ID

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

Level
What it allows
Sentinel loaded
Observe and record only. The cheapest tier of the gauntlet.
Dispatcher enabledWATCH_DISPATCHER_ENABLED
Claims pending escalations and may spend. Still not the producer.
Producer boot gateWATCH_PRODUCER_ENABLED
A recovery and test surface. It is a gate, not an arm.
Hardware-backed producer arm
The real ceremony. The only level that lets the producer promote fires toward paid deliberation.
Disarm
Stops new claims only. In-flight provider work may finish and settles after disarm.

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

Route
Contract
POST /watch/admin/producer/arm
410 Gone. The legacy route, kept only to refuse.
POST /watch/admin/producer/arm/stage
Issues the stage challenge (v3, JCS).
POST /watch/admin/producer/arm/pending
Crash-resume for an in-flight stage.
POST /watch/admin/producer/arm/confirm
Attest and commit in a single DB transaction. Fail-closed.
POST /watch/admin/producer/disarm
Admin token OR any arm principal.

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.

Ceiling
Rule
Daily ceiling: $50 per UTC dayDAILY_SPEND_CAP_USD
May only be lowered. Raising it refuses boot.
Fanout reserve: $5WATCH_MAX_FANOUT_COST_USD
Reserved against each fanout before spend.
Signed arm window: 24 h defaultGW_ARM_WINDOW_MS
Signed into the challenge itself. Not env-extendable after the tap.
Max loss
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 rand dev 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

Knob
Effect
DEMO_GW_PORTdefault 28080
Gateway port for the isolated demo stack.
DEMO_COUNCIL_PORTdefault 28765
Council port for the isolated demo stack.
DEMO_POLL_TIMEOUTdefault 90
How long the lane waits for the loop to close.
DEMO_ALLOW_BUILD=1
Builds from this checkout. Needs about 8 GB for Docker plus BuildKit.
DEMO_PULL=1
Pulls Docker Hub tags instead. Hub tags may lag tip.

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.