The foundation · version 5

ARBITER: the foundation, stated to be checked

Version 5, 23 October 2026. Code 8b66202 (production and the demo). The gate runs gate/worker.js as committed at 44575d1, deployed on 18 October (version gate-2, SHA-256 668568f0a8be5e252ebdad67680f8cd774ecd4c154deda272d736da752de1216). The model of its rules, their proof, and the replay that ties the code to the model: gate/model/ at 695b933, published at consequenceaisystems.ai/foundation/model/695b933/.

Note, 24 October 2026: the proof's prover, tlapm 1.6.0-pre, is a rolling pre-release, and its GitHub address now serves a later build, so Dockerfile.tlaps at 695b933 no longer builds. The same twelve files, with Dockerfile.tlaps fetching the exact build the proof was checked with from this site, are at consequenceaisystems.ai/foundation/model/e05ad81/ (only that file differs). The later build gives the same result.

Note, 25 October 2026: the gate now runs gate/worker.js as committed at d9aed52 (version gate-3, SHA-256 cffffb2766b204cdbe204ee576b481e12a57a7cd990b280c483f6b6728f43c65), pasted into Cloudflare's editor that day (R2), and ARBITER's production server runs d9aed52. It changes who may read a request. This note also corrects what G4 and section 4 say the log records. Both are set out in "Since version 5", at the end. G1 to G3, the model and the proof do not change.

Note, 26 October 2026: the gate now runs gate/worker.js as committed at 1e5553a (version gate-4, SHA-256 fcbc5e0fcf619c2a941152a745b8e5b571a60fa9fca8020a2b483af322e419d0), pasted into Cloudflare's editor that day (R2). It changes which refusals the log records, in whose name and under what allowances, so parts of the 25 October correction to G4 no longer hold. This is set out in "Since version 5", at the end. ARBITER's production server is unchanged. G1 to G3, the model and the proof do not change.

Note, 27 October 2026: a full dress rehearsal ran ARBITER's production code with a test gate that reports the same gate code (gate-4), and a passkey registered from a phone decided two of its payments: 115 checks passed, 0 failed. Its run log, and the copy of the test gate's log it checked, are listed in "Since version 5", at the end. The production gate and ARBITER's production server are unchanged.

Note, 1 November 2026: the production gate's owners are now two hardware security keys, and a new passkey on the founder's phone may only approve. The passkey made at setup, a passkey synced through the founder's Google account, is revoked. From the same day a job on GitHub's machines checks every hour that Cloudflare runs exactly the gate our repository records. The gate's code (gate-4), ARBITER's production server, G1 to G3, the model and the proof do not change. Both are set out in "Since version 5", at the end, which also says what they replace in sections 6 and 9.

Changed from version 4: the gate's code is tied to its model. The gate's own log of every change it makes is replayed against the model, step by step: the log of the gate's tests on every change to its code, and the live gate's whole log every day (section 4, "The code against the model"). Changed from version 3: G1 to G3 are proved for every behaviour of the model, with no bounds. Changed from version 2: the rules are checked by machine within bounds. Changed from version 1: the gap section 4 showed is closed in gate-2. Every version is kept: version 1, version 2, version 3 and version 4, and in commits fb97988, a2511c2, 3857290 and 1839f2b.

Consequence AI Systems · jerrymcclintock@sovereignarbiter.com

1What this is

Our run logs show how ARBITER behaved when it was attacked. A passing test shows that the routes it tried were closed. It does not show that no other route exists, and it does not say which rules the system is built to keep.

This document says that first. It states:

  1. who ARBITER must hold against (section 2);
  2. the rules it keeps, the things that must never happen, each with what enforces it and the evidence (section 3), and the gate's rule written out exactly, checked by machine and proved (section 4);
  3. every route by which the protected effect, a payment, could form, and what closes each one (section 5);
  4. what is trusted, and what is not claimed (sections 6 and 7);
  5. the evidence, frozen with hashes, and how anyone can re-check it (section 8);
  6. what is not done yet (section 9).

If a rule here is wrong, if a route is missing from section 5, or if the evidence does not say what we say it says, that is a finding, and we want it: jerrymcclintock@sovereignarbiter.com.

2Who it must hold against

AdversaryWhat they hold
A1An outsiderNothing: the public internet
A2Another company on the same ARBITERIts own users and API keys
A3The AI model, what it is fed, and the agent's toolsWhatever text it returns; the tool's own database login
A4Code running inside ARBITER's executorThe executor's environment, database login and signing keys: the part that reads untrusted input and calls the model, so the part most likely to be made to run an attacker's code
A5Root on the ARBITER serverEverything on that server: the database as its owner, every container, every key and credential kept there

What is trusted, against all five, is in section 6.

3The rules

Each rule says what must never happen, who it holds against, what enforces it, and where the evidence is. Rules G1 to G4 are kept by the gate, off the ARBITER server. Rules S1 to S5 are kept on the server.

Kept by the gateoff the ARBITER server
G1

No payment without the whole chain of authority

A payment forms only when ARBITER asks, under a key an owner granted for that company, to pay a payee an owner listed for that company, AND a person whose passkey an owner activated and granted for that company approves the exact terms on the gate's own page, within the limit the owner set for that passkey.

Holds against
A1 to A5
Enforced by
The gate, a Cloudflare Worker in an account the ARBITER server holds no credential for (gate/worker.js: intakeChecked, decideChecked, verifyAssertion). It holds the only credential that pays.
Evidence
The 16 October run (section 8). Root on the server, holding the database and the broker's own key, made 15 attempts on the gate: 13 were refused outright, the passkey it registered stayed pending and could do nothing, and the $49,999.99 it asked for waited for a person, who rejected it. One payment formed: the one the owner approved on their phone. 21 outcomes as predicted, 0 unexpected. The gate's own tests (124 checks) and the end-to-end test (32 checks) run on every change. The model checker finds no order of events, within its bounds, in which a payment forms without the whole chain, or goes to anyone but a person's chosen request, and the proof shows there is none at all (section 4).
Exactly
Written out in section 4.
G2

At most once

A request pays at most once, and a failed payment is never retried: ARBITER must ask again, and a person must approve again.

Holds against
A1 to A5
Enforced by
An approval moves the request from pending to executing in the same transaction that records the approval, before the payment is attempted; a second approval finds it no longer pending (decideChecked).
Evidence
The gate's tests send ten approvals of one request at once: one executes, nine are refused. The model checker finds no order of events, within its bounds, in which a request is approved twice; with the move to executing taken out, it finds one in 12 seconds. The proof shows there is none at all (section 4).
G3

Authority changes only by an owner's signature

Who may approve, for which company, up to what limit, which payees, and which ARBITER keys may ask: each of these changes only by an owner's passkey signature over the exact change, made within the last five minutes, applied once.

Holds against
A1 to A5
Enforced by
opStart, opFinish, applyOp. The first owner was made once, with a setup code that only the Cloudflare account can set; setup then closed for good, even if that owner is later revoked. The last active owner cannot be revoked. A passkey anyone registers is pending and can do nothing until an owner activates it.
Evidence
The 16 October run: root tried to make itself owner through setup (refused: closed for good) and to make its own passkey an owner, signing with that passkey (refused: pending, not active). The model checker finds no order of events, within its bounds, in which authority changes other than by an owner's signed change applied once while its signer is an active owner, with root replaying every signed change in the log, and the proof shows there is none at all (section 4).
G4

The gate's record is public, and anyone can check every signature in it

Every event at the gate (setup, registrations, owner changes with their signatures, requests with ARBITER's signature, approvals and rejections with the person's signature, refusals, payments) is appended to a hash chain that anyone can read. Every final outcome is a receipt signed by the gate's own Ed25519 key.

Holds against
A1 to A5. Nothing on the ARBITER server can write to it.
Enforced by
append (each entry's hash covers the one before it) and receipt.
Evidence
verify_gate_log.py, one file of about 330 lines that needs only Python and the cryptography package, replays the whole log from its first entry: the chain, every owner change, request, approval and receipt. Section 8 freezes the log's head as of this version.
Kept on the serverARBITER's own layers
S1

The company's rules decide before the AI acts

No model is called for a request until the company's rules have decided it. A block stops it. A review holds it until a person decides. Only "proceed" reaches the model. A rule the evaluator cannot read, or a payload value of the wrong shape, sends the request to a person, never through. Every model answer is checked against the platform's and the company's principles before it is released; one that fails is blocked or held for review, and a check that runs out of time counts as a failure.

Holds against
A1 to A3
Does not hold
A4 and A5, which control the code that applies the rules.
Enforced by
The executor's pipeline (executor/main.py: the decision commits before any model call), executor/jsonlogic.py (strict mode), executor/cle_engine.py.
Evidence
The 13 October dress rehearsal (88 of 89 checks; the one failure is explained in its log); the sandbox, where anyone can write a rule and test it.
S2

Companies are walled off by the database

A request authenticated for one company can neither read nor write another company's records.

Holds against
A1 to A3
Does not hold
A4 and A5. The executor can take the database role that crosses companies, which it needs for background work.
Enforced by
PostgreSQL row-level security, enabled and forced, on every table that holds a company's records. The schema contract fails if a new such table lacks it. Three tables are exempt, each named with its reason in test_arbiter.sh check 1: the sign-in accounts, which a super-admin lists across companies by design; usage counts, read only with the company named in the query; and one table nothing reads. Request connections use a role that cannot bypass it; the role that can is reached only through one function, which requires a reason and logs it. A static check fails the build if any other way to the database appears.
Evidence
The guided demo's step 4 and every sandbox: one company's key asks for another's record and gets "not found". The dress rehearsal's isolation checks.
S3

Records are add-only, chained, and copied off the server

The audit log, the execution certificates and the effect receipts are add-only for every application role. Each is a hash chain. Within about five minutes, each chain's head is copied to write-once storage (S3 Object Lock in compliance mode, which not even that storage account's own root can shorten) in a separate cloud account. The copies are checked hourly on the server, and every six hours from GitHub with read-only access and no stored key.

Holds against
A1 to A4. The executor's login cannot rewrite any of it.
Against A5
Detected, not prevented. Root can rewrite the database. It cannot change the copies already made, so a rewrite of anything already copied shows up in the next check. A record is unprotected for the seconds before it is sealed.
Enforced by
Migrations 30 and 47, the anchor sidecar, verify_receipts, .github/workflows/receipt-anchors.yml.
S4

A certificate cannot be changed without its check failing

Each execution gets one certificate, issued once and never reissued, signed with Ed25519. It records who asked (as a role), what was asked (a hash), which rule decided and the hash of the whole rule set, the model and the hash of its instructions, the principles checked, the reviewer's role if a person decided, and what became of it. Anyone can check one with the public key at /.well-known/arbiter-certificate-keys: any change to any field makes the signature fail.

Holds against
A1 to A3
Does not hold
A4 and A5, which hold the signing key and can sign a false certificate. Whether a certificate is the instance's own record is answered by POST /certificates/verify (on_record) and by S3. Against the server itself, the evidence is the gate's receipts (G4), not ARBITER's certificates.
S5

Inside the server, neither the tool nor the executor can pay on its own

This is the server's own layer for payments made through it rather than through the gate (the demonstration ledger). The tool's database login cannot write the ledger or create a permit. A permit comes only from the effect broker, after it has verified a person's passkey signature over the exact terms, by a passkey the operator confirmed; each permit pays once.

Holds against
A1 to A4
Does not hold
A5, or a shell in the database or the broker. That is why the gate (G1 to G4) exists. A payment bound to the gate does not rely on S5.
Evidence
The 13 October run from inside the executor: 38 attempts refused, 1 effect formed (the sanctioned one), 0 outcomes other than predicted.
Who each rule holds against

A1 outsider · A2 other company · A3 model, tool · A4 executor code · A5 root on server

Rule A1outsider A2other company A3model, tool A4executor code A5root on server
G1authority to payholdsholdsholdsholdsholds
G2at most onceholdsholdsholdsholdsholds
G3owner-signed changesholdsholdsholdsholdsholds
G4public, checkable recordholdsholdsholdsholdsholds
S1rules before the AIholdsholdsholdsnono
S2company wallsholdsholdsholdsnono
S3add-only, copied offholdsholdsholdsholdsdetected
S4certificatesholdsholdsholdsnono
S5no payment alone, on the serverholdsholdsholdsholdsno

4G1 written out exactly

For every payment instruction the gate's credential sends, there is exactly one request r and one approval a such that:

  1. When the gate received r:
    • its text has exactly the fields account, amount, currency, expires_at, gate, id, intent, issued_at, kind, tenant, v, names this gate, and is signed (Ed25519) by an ARBITER key whose status is active and whose companies include tenant(r);
    • account(r) is a payee listed for tenant(r);
    • amount(r) is greater than zero, issued_at is within five minutes of the gate's clock, and expires_at is in the future and at most 24 hours after issued_at.
  2. When a was recorded:
    • r was pending and had not expired;
    • the ARBITER key that signed r was still active and granted for tenant(r), and account(r) was still a payee listed for tenant(r);
    • a is a WebAuthn signature by a passkey p whose status is active and which is granted for tenant(r) with a limit at least amount(r);
    • the signature is over SHA-256 of the exact terms {v, kind: "approve", gate, request: id(r), request_sha256: SHA-256 of r's text, tenant, account, amount, currency};
    • it was made for the gate's own origin, not from inside another site, by an authenticator that reports the person present and verified (PIN or biometric), with a signature counter that did not go backwards;
    • in the same transaction, r became executing.
  3. The sets in 1 and 2 (active ARBITER keys and their companies, payees, active passkeys, grants and limits) change only by an owner operation (G3).

The credential that pays (EFFECT_SLACK_URL) is read in one place in the gate's code, the function effect, and that function is called in one place, after the transaction in 2.

That is the whole claim for G1, and it is the one a reviewer should try to break.

The gap version 1 showed, closed in gate-2

Until gate-2 the key's grant and the payee were checked when the request arrived, not again at approval, so a request already waiting could still be approved after an owner unlisted its payee or revoked ARBITER's key, until it expired (24 hours at most). Since gate-2 (18 October) the approval's transaction checks both again (standingOk, in decideChecked), so withdrawing either also stops the requests already waiting. A rejection needs neither: refusing to pay is always allowed. On the gate-1 code, 9 of the gate's 124 checks fail.

Checked by machine

The rules above are also a model in TLA+, Gate.tla, and the model checker TLC tries every order of events up to stated bounds. The model follows gate/worker.js step by step, each action named for the function it stands for, and each check the code makes before its transaction is a step of its own, so anything may happen in between. The attacker is A5: it holds ARBITER's real key and one of its own, has its own passkey registered, asks for any payment, signs anything with its own passkey, and replays every signature that ever reached the public log, at any moment, for any request. The people an owner chose may approve anything the gate shows them: the model relies on the gate's checks, not on their judgement. It checks, in every state it reaches:

CaseBoundsResult
gate-2, paymentsAny state the owner's changes can reach; two requests; one change by the owner at any moment while they waitEvery property holds: 8,290,140 distinct states
gate-2, changes of authorityTwo owners, three signed changes, everything granted at the startEvery property holds: 15,052,721 states
gate-2, from setupFrom nothing: setup, registrations, four changes, a requestEvery property holds: 28,481,992 states
gate-1: the key and payee not checked again at approvalAs paymentsG1 broken in 13 s: a payee unlisted between a request's check and its recording, then the request approved. This document's own gap, found by machine.
The approval's checks made only before its transactionAs paymentsG1 broken in 13 s: the approver revoked between the two
Approval terms without the request's id and hashAs paymentsG1 broken in 58 s: root replays a person's approval of one request on another with the same terms, and it pays
An approval leaves the request pendingAs paymentsG2 broken in 12 s
No record of applied changes (used_ops)As changes of authorityG3 broken in 49 s: root replays an owner's change from the log and undoes a later one
The signer not checked again in a change's transactionAs changes of authorityG3 broken in 6 s: a change signed by an owner revoked before it applied

The six known-bad designs show that each property can fail, and what each check is for. They caught a flaw in the model itself: in its first full run, two approvals of one request made one identical payment record, so G2 could not fail. G2 now counts recorded approvals, as the gate sends a payment after each one.

What this does not show: that worker.js matches the model, which the replay below checks for the runs that happened; anything beyond the bounds (two companies, two requests, amounts and limits of 1 or 2, a clock of three ticks, one to four owner changes), which the proof below covers; or anything about the cryptography and WebAuthn's own checks, which the model assumes.

Proved, with no bounds

TLC tries every order of events within its bounds; a proof covers them all. GateProof.tla proves, for every value of the constants (any number of companies, requests, owners, approvers, payees, keys and changes, any number of passkeys and keys held by the attacker, any amounts and limits, a clock that runs as long as you like), that every behaviour of the model keeps G1 to G3, the same properties TLC checks. The method is an invariant that holds at the start and that every step keeps, whose parts say why each property holds:

The proof system TLAPS checks every step of it: all 629, in about five minutes on one core. The proof assumes only that the gate runs as modelled (no check removed) and that the people and keys are distinct: the attacker's passkeys are neither owners' nor approvers', and its keys are not ARBITER's. TLC's configurations satisfy these, so they cannot contradict each other (if they did, anything would follow). Run on gate-1's design, the proof fails at exactly one of its 629 steps: the one that says an approval is recorded with the authority in force, the gap version 1 showed.

What the proof does not change: it is about the model, not worker.js (the replay below ties the two), and the provers are trusted (tlapm, which divides the proof among Z3, Zenon, Isabelle and LS4).

The code against the model

The model and its proof are about Gate.tla; the gate runs worker.js. They are tied by the gate's own log, which records every change the gate makes to its records, signed and chained (G4). trace.sh turns a log into the steps of the model its entries stand for, with the logged values (who signed, which change, which request, account and amount, at what time), and TLC takes exactly those steps, in order, from an empty gate. Either the model takes every entry, so every change the code made is one the model allows at that point, or TLC stops at the first entry it does not allow, and the replay names it. Since version 4 the model has sets of people and keys where it had one of each, so that it can stand for any log: an owner who also approves, several approvers, several passkeys and keys that were registered and never given authority.

LogResult
The gate's own tests (gate.test.mjs) on gate-2The model takes all 101 entries
The live gate, from its first entryThe model takes all 28 entries
The same tests on gate-1's codeStops at entry 73: an approval recorded after entry 72 unlisted the request's payee, the gap version 1 showed

Every change to the gate's code must pass this before it can land: the tests' log is replayed as part of the gate's CI job. Every day, the live gate's whole public log is replayed against the model on GitHub's machines. Anyone can run the same replay on the live gate (section 8).

What the replay does not show: it checks what the code did, not what it refused (a refusal changes nothing in either, so it is skipped, and that the model would refuse it too is not checked); it covers the runs that happened, not every run the code could make; and it reads the log, so it checks the code's own account of what it did. G4 lets anyone check that account is signed and unbroken, not that it is complete. The one change the model leaves out, adding a company, is skipped: the model starts with every company added.

5Every route to a payment, and what closes it

The protected effect is a payment instruction sent with the gate's credential. Today that credential is a Slack webhook address, standing in for a bank, and the instruction is a message posted to a channel kept for it. These are all the ways we know for such an instruction to form:

#RouteWhat closes itWhat remains
R1Use the credential directlyIt exists only in the gate's Cloudflare settings. It is not on the ARBITER server or in any of its containers (checked in the 16 October run), and the server holds no Cloudflare credential.Whoever holds the Cloudflare login, or can read the webhook's settings in Slack (section 6)
R2Change the gate's codeOnly through the Cloudflare account. The code is one file, run as written with no build step. The automatic build that first deployed it was disconnected, and the branch it built from deleted, so nothing pushed to our repository reaches it. A new version is pasted into Cloudflare's editor by hand (gate-2, 18 October).The Cloudflare login (section 6)
R3Ask without ARBITER's keyRefused: the request must be signed by an active ARBITER key granted for that company. A key root makes for itself stays pending until an owner grants it.
R4Ask with ARBITER's real key (root holds it)Allowed: asking is not paying. Every request still needs R5 to R8. Root asked in the 16 October run; nothing paid until a person approved, and the person rejected root's request. An owner who revokes the key also stops the requests it already made (since gate-2).Root can ask, delay or withhold requests
R5Pay an account of the attacker'sRefused: the payee must be listed for that company, which only an owner can change (G3), both when the request arrives and when it is approved
R6Approve without a granted personRefused: the passkey must be active and granted for that company. A passkey root registers stays pending. A signature made with another key, or for another site, does not verify.
R7Replay a real approvalRefused: an approval signs the exact terms, including the request's id and the hash of its text, so it fits one request, and that request is no longer pending once used
R8Exceed the limitRefused: the amount must be within the limit the owner set for that passkey and company. A request above every approver's limit is refused on arrival.
R9Change the authority (add a passkey, raise a limit, add a payee)Refused: only by an owner's signature over the exact change (G3). Setup is closed for good.The owner's device (section 6)
R10Pay twiceRefused: G2
R11Trick the approverThe terms are shown by the gate's own page, from the gate's own records, not by the ARBITER server.The approver's judgement, bounded by listed payees and the limit
R12Break the gate's code itselfIts input checks; its tests (124 checks); its design checked by machine and proved (section 4)A bug we have not found, or a difference between the code and the model. This is what an outside review is for (section 9).
R13Compromise Cloudflare, or the owner's or an approver's deviceNot closed: trusted (section 6)

6What is trusted

7What is not claimed

8The evidence, frozen

Each run log states where it started (the code and the setup), what was changed (the attacks), the protected effect, the receipts, the routes tried, and the results. It is published as it printed. A file's SHA-256 here lets anyone confirm they have the same bytes.

The gate's log, frozen at this version

Entry 28, hash

24cf12e47312d58f2ef3d0b89572b79aefd60e8e758a2834a8448ccf085f08ed

The log only grows. Run the checker against it, then confirm that entry 28 still has this hash (the hash of /v1/log?after=27&limit=1): then entries 1 to 28 are the ones this document describes. Entries 1 to 27 are version 1's (entry 27: 60ac6b5b51a2397159c135b545a1988acf5062b4d847f38b07329d5725a89803). Entry 28 is the owner revoking 0d0e33e02e9dfafa, the ARBITER key root registered in the 16 October run (entry 21), which had stayed pending and could ask for nothing.

What anyone can reproduce today, without trusting us:

  1. Every signature in the gate's log, with the checker.
  2. The bytes of every run log, with the hashes above.
  3. Any ARBITER certificate, with the public key.
  4. S1, S2 and S4 by hand, in a sandbox of your own: demo.sovereignarbiter.com/#sandbox (the gate is not connected to the demo).
  5. The model check and the proof, with Docker: put the twelve files SHA256SUMS lists in an empty folder and run bash check.sh (about 45 minutes on one core; bash check.sh gate-1 alone takes seconds) and bash prove.sh (about 11 minutes, with the run on gate-1's design).
  6. The live gate's log against the model, in the same folder (Docker, Python 3 and curl): bash trace.sh https://arbiter-gate.jerrymcclintock1.workers.dev (under a minute). The log only grows, and every entry it gains is replayed too.

What needs our code: re-running the attack scripts themselves. The source is private today. No change reaches production until all 11 of its CI jobs pass on that exact commit.

9Not done yet

10Where it is in the code

For a reviewer with access.

Since version 5

25 October 2026: who may read a request (gate-3)

What changed. Until gate-3, anyone who held a request's id could read the request and its terms from the gate (GET /v1/requests/<id> and /terms). An id is not a secret: every one is in this gate's public log. Now the gate shows them only to:

Anyone else gets the same answer whether or not the request exists: a refusal before the request is looked up when there is no sign-in or valid signed read, and otherwise the refusal an unknown id gets. A refused read writes nothing. A sign-in lets the person read, nothing more: approving still needs their passkey's signature over the exact terms (G1). It lasts up to eight hours. Signing out clears it from that browser; only revoking the passkey ends it at the gate, so a copy of it keeps working until then. Sign-in changed with it: its challenge is now a value the gate checks with a secret only it holds (HMAC-SHA256), not a stored row, so it has no gate-wide hourly limit for anyone to use up, and each challenge is accepted once.

What it holds against. A1 to A4. Not A5, nor a shell in the broker: both hold ARBITER's key (R4), so they can read every request of the companies that key is granted for; ARBITER keeps these requests in its own records too. The rule is not one of G1 to G4. It is not in the model or the proof, and the replay cannot see it: no entry records a read or who made it, and an allowed read logs only what reads logged before (an expiry, an expired request's receipt). Its evidence is the gate's tests.

What stays public on this gate. The log is public (G4), so each request's text, each decision's terms and each payment's receipt can still be read there by anyone. What the log does not hold is now private: the gate's signed receipt for a rejection. An expired request's receipt goes into the log when it is made, which is now when an owner, an approver or ARBITER first reads the request; until gate-3, anyone's read made it. On a gate whose log only its owners can read, G4's "anyone can read" does not hold, and this change keeps its requests' terms private, though not whether they exist or how they ended.

Limits that remain.

A correction to G4 and section 4. G4 says every refusal is appended to the log. Only four kinds are: a wrong setup code (while setup is open, within its ten tries an hour), an owner change whose signature does not verify or is not an owner's, a refused payment request, and a refused decision; and only once the call's body has been read, as a JSON object of at most 16 KiB. Every other refusal is answered and never logged, among them an owner change already applied or too old, any setup attempt once setup has closed, and refused sign-ins, registrations and reads. The four logged kinds share one allowance of 600 entries per clock hour, which limits how fast anyone can add to the log. Anyone can use it up with unsigned calls, and those refusals then go unlogged until the hour ends. Section 4 says the log records every change the gate makes to its records. It records every change of authority (G3), and every request's arrival, decision, expiry and payment outcome, but one: a payment interrupted after it was sent, which the log shows only as its approval, with the request left executing. It does not record a rejection's receipt, which is kept with the request, or the gate's own bookkeeping: challenges, used sign-ins, the signature counter a sign-in moves on, and the allowance's counts. All of this was so in gate-2 too. It hides no change of authority, no payment is sent before its approval is logged, and G1 to G3 do not depend on it. Since 26 October (gate-4) some of this no longer holds as written. An owner change that a passkey the gate knows signed is now also logged when it is refused as already applied, as too old, or because the gate refuses the change itself (revoking the last owner, for example). And the 600 is now one of three allowances, so using it up no longer stops a key that setup or an owner made active from having its refusals logged in its name, replays aside. See "26 October 2026: refusals in the signer's name (gate-4)", below.

The numbers now. The gate's tests are 170 checks and the end-to-end test 40. G1's evidence says 124 and 32 and R12 says 124; section 4's "9 of the gate's 124 checks" is about that older suite. The tests' log is 109 entries, and the model takes all of them. The live log is unchanged: entry 28, with the hash in section 8. Gate.tla, GateProof.tla and the checker are byte for byte the files section 8 lists. trace.sh in our repository has changed since (4cb2fe0: it retries a dropped read of a live gate and stops if it read only part of the log); the published copy still works.

26 October 2026: refusals in the signer's name (gate-4)

What changed. A logged refusal now names the key that signed the refused attempt, as by, when that signature verifies with the registered public key the attempt names (a passkey's credential id, an ARBITER key's fingerprint). For a passkey, that means an assertion over the attempt's own challenge (an owner change's or a decision's) that passes every check the gate makes of it, up to and including the signature; a counter that went backwards is checked after that, and its refusal names the key too. For an ARBITER key it is a signature over the request's text. The key may be active, waiting for an owner, or revoked.

Anyone can register a key to wait for an owner, and registering one shows nothing about who holds its private half (an ARBITER key is registered with its public key alone, and the gate checks no signature when a passkey registers), so by says which registered key made the signature, not who sent the attempt. A signature that does not verify names no one, so naming someone else's key costs that key nothing. Refusals the gate makes before it has the named key's signature to check name no one: a request with no text, text over 4,096 characters, or no key named; a key it does not know; a decision that is neither approve nor reject, or one on a request that does not exist or is no longer waiting. The gate's own receipt key is never named. A refused request's key is still the fingerprint the request claimed, signed or not.

How much is logged. Each clock hour:

Once an allowance is used up, the refusals it covers get the same answer and go unlogged for the rest of the clock hour, even if another allowance has room.

A replay is a request the gate already holds from that key with exactly that text, or an owner change whose id was already applied, whatever its terms. Anyone can copy those from the public log. A replay still names its key and is marked replay, but it counts against the 600, never against a key's own 20 or the 400. For a request, replay says the log holds that key's signature over that very text; for an owner change, only that a change with that id was applied, not that these were its terms. Neither says who sent it.

At most 600 + 400 refusals, plus 20 for each key that setup or an owner has ever made active, are logged in one clock hour: 1,040 on the live gate today, whose two such keys are the owner's passkey, made by setup, and ARBITER's key. Setup has closed, so only an owner's signed change can raise that number.

Newly logged. An owner change signed by a passkey the gate knows, refused because its terms are too old (logged with op null when the change in them is not a valid one), because it was already applied, because the gate refuses the change itself (revoking the last owner, for example), or because its signer stopped being an active owner after its signature was checked. gate-3 logged none of these. No test reaches the last case: nothing else runs at the gate while it checks a signature, and the re-check does not rely on that. Nor does any test send terms too old whose change is not a valid one.

What did not change. Every refusal gets gate-3's answer, word for word, and so does everything the gate accepts. For a key waiting for an owner or revoked, and for owner-change terms too old, the gate checks the signature only after deciding the refusal, and only to say who signed. Only the version the gate reports and the log differ, and with the log what reports on it: the log head at /.well-known/arbiter-gate, and a receipt's place in the log (decision_seq). Refusals are logged as the same four kinds of entry, and only once the call's body has been read, as a JSON object of at most 16,384 characters as JavaScript counts them (UTF-16 code units, so an emoji counts as two; the 25 October correction calls this 16 KiB, but it is not bytes). Still not logged: refused sign-ins, registrations and reads; any setup attempt once setup has closed; owner-change terms that are not this gate's; terms too old that no passkey the gate knows signed; and terms not too old whose change is not a valid one (an unknown kind of change, a malformed field), even when signed.

Of the 25 October correction to G4, then, three things no longer hold: that one allowance of 600 covers every logged refusal; that the only owner changes logged are those whose signature does not verify or is not an owner's; and that an owner change already applied or too old is never logged. The rest of it stands, as does its correction to section 4, with two more pieces of the gate's own bookkeeping: the three allowances' counts, and its list of the keys made active, which it builds from its own log.

What it holds against. For each passkey that setup or an owner made active, the allowance holds against A1 to A5, short of the fingerprint match under Limits: nothing they send uses that passkey's 20, so the first 20 refusals in its name in an hour, replays aside, are logged. None of them can make its signatures, and those it makes go from the person's device to the gate over TLS. ARBITER's key is different. Root on the ARBITER server and a shell in the broker hold it (R4), and it signs whatever ARBITER is made to ask: the broker asks the gate to pay every completed intent of an action paid through the gate, with the account and amount the intent carries. So code in the executor (A4); the agent's tool (A3), which reaches the broker, if it has the id of a completed intent the gate refused; and a company's users or API keys (A2), for intents their company's rules let through, can all make it sign requests the gate refuses. Any of these can spend its 20. Those 20 are logged in its name; that key's later refusals that hour, those of the requests ARBITER sends in its normal work included, are not, unless they are replays. ARBITER uses one key for every company, so its 20 cover them all together.

Limits that remain.

The numbers now. The gate's tests are 209 checks: 39 added since gate-3, 29 of which fail on gate-3's code (the other ten hold what gate-3 already did). The end-to-end test is still 40 checks. The tests' log is 1146 entries, 1072 of them refusals, which the replay skips, as it skips adding a company (section 4); the model allows each of the 65 steps it takes from the rest. The live log is unchanged by the deploy: entry 28, with the hash in section 8. gate-4 changes none of Gate.tla, GateProof.tla, the replay (trace.sh and the trace.py it runs) or the checker.

27 October 2026: a dress rehearsal with a test gate

What ran. The full dress rehearsal (as on 13 October, section 8), with the gate added. On a disposable copy of production, with the executor, broker and database code production runs and the real AI models, six made-up companies used ARBITER, and four of them paid through a test gate, rehearsal-gate (gate id rehearsal-gate-1, rehearsal-gate.jerrymcclintock1.workers.dev), which reports version gate-4, with its own public log and its own Slack channel. The run attacked the gate as an outsider, as another company, through the AI model, from a shell in ARBITER's executor and as root on ARBITER's server. ARBITER's founder decided two of its payments with a passkey registered from a phone: approved one, rejected the other. 115 checks passed, 0 failed. The test gate's log as the run checked it, 2,144 entries, holds with the checker in section 8 and replays against the model (section 4).

What it does not show. On the test gate, the owner's passkey and every approver's but the phone's are software keys on ARBITER's server, the server the run attacks as root, and an approver granted for a company could have approved root's own request. The attack never uses those keys: it shows what root can do without them, which is root's position against the production gate, whose only owner and approver is a passkey on the founder's phone. The run log sets out this and its other limits.

1 November 2026: two security keys own the gate, and its deployments are watched

What changed. Two security keys (YubiKey 5C NFC), each with its own PIN, were registered and made owners by changes the first owner signed: log entries 30 to 34 (entry 29, a first attempt from one key, was revoked at 33). Then, each signed by the first key: a new passkey on the founder's phone, registered without asking to be an owner (35), was activated as an approver only (36) and allowed to approve for the one company the gate serves, the test company "Boundary proof (test)", up to 50,000.00 (37); and the first owner, the passkey made at setup on 16 October, was revoked (38). The gate's description now reports two owners.

Why. A synced passkey has a copy wherever its account is signed in, and an owner can change everything at the gate: add an owner, a payee, a key or a higher limit (G3, R9). A security key's passkey never leaves the key, and each use needs the key in hand, its PIN and a touch. The phone keeps what it needs from day to day: an approval, which pays only what ARBITER asked, to a listed payee, under the limit (R5 to R8).

How to check it. The checker, as published in section 8, holds on the log's 38 entries and names the owners now: aa5f86cc42992f34 and abefa4c6e7776cd1. Every owner change carries the signer's authenticator data. Its flags byte is 0x05 in entries 36 to 38: the person was present and verified, and the credential cannot be backed up, so it cannot be synced or copied off the key. In every change the first owner signed it is 0x1d: verified, and backed up, a synced passkey. The second key has signed no change yet. All 38 entries replay against the model (section 4). Entry 38 has hash 90daa3be09d216b9a139e5ea8874983328ac3c3a60755fc6b4494090236a50dd: confirm it as in section 8, with after=37.

The deployment watch (R2). Cloudflare sends no notice when a Worker is deployed. Every hour a job on GitHub's machines reads what Cloudflare runs as the gate, with an API token that can only read the gate's Worker, and compares it with what our repository records: the serving deployment must run exactly the recorded version (now 509ac9cc), no newer version may be waiting, and its code must be gate/worker.js as committed at 1e5553a, byte for byte but for the newline that ends the file, which Cloudflare's editor drops (SHA-256 9bdd3eca5c318ae2207904ab4daf9995c51ebc518c1f9b51b4ffa47105ef3ddb). Anything else alerts us, a changed secret included, and so does a watch that stops running. It sees a change after the fact, within the hour; it cannot stop one. (Commit 9b7c4fd.)

What it replaces. In section 6: the owners are the two security keys, not a passkey on the founder's phone, and the only approver is the phone's new passkey (the owners approve nothing). As of 1 November the Cloudflare account has one member, one API token (the watch's, which can only read the gate's Worker), and two-factor sign-in that also takes the two security keys. In section 9, the second owner is done. In R13, the owner's device is now the two keys and their PINs. The 27 October section's "the production gate, whose only owner and approver is a passkey on the founder's phone" describes that day.

What it does not change. The gate's code, ARBITER's server, G1 to G3, the model and the proof. If both keys were lost, no owner change could ever be made again (setup is closed), though the approver could still approve; they are kept apart. Whoever holds the Cloudflare login can still replace the gate's code: the watch makes that seen, not impossible. Root on ARBITER's server holds our repository's deploy key, so it could record a new version for the watch, in plain sight in the repository's history.