The foundation · version 3 · superseded
ARBITER: the foundation, stated to be checked
Superseded on 23 October by version 4, which proves G1 to G3 with no bounds; the latest version is here. Kept here as it was published; its model files are kept at /foundation/model/87bc9a1/.
Version 3, 18 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: gate/model/ at 87bc9a1, published at consequenceaisystems.ai/foundation/model/87bc9a1/.
Changed from version 2: the gate's rules are checked by machine (section 4, "Checked by machine"; the evidence for G1 to G3; sections 8 to 10). Changed from version 1: the gap section 4 showed is closed in gate-2, the gate's tests are 124 checks, and the log is frozen at entry 28. Every version is kept: version 1 and version 2, and in commits fb97988 and a2511c2.
Consequence AI Systems · jerrymcclintock@sovereignarbiter.com
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
G1No 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 (section 4).
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 (section 4).
G3Authority 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 (section 4).
G4The 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
S1The 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.
S2Companies 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.
S3Records 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.
S4A 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.
S5Inside 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
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:
- 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.
- 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.
- 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:
- G1: every approval was recorded with the authority of item 2 in force at that moment; every payment instruction went out on the signature of a person the owner chose, made for that very request on the gate's page; nothing is paid to an account the owner did not list.
- G2: a request is approved, and so paid, at most once.
- G3: authority changes only when a change an owner signed is applied for the first time while its signer is an active owner, or at setup, once; the gate always has an active owner after setup; the attacker's passkey and key never gain authority.
| Case | Bounds | Result |
gate-2, payments | Any state the owner's changes can reach; two requests; one change by the owner at any moment while they wait | Every property holds: 7,146,618 distinct states |
gate-2, changes of authority | Two owners, three signed changes, everything granted at the start | Every property holds: 10,789,903 states |
gate-2, from setup | From nothing: setup, registrations, four changes, a request | Every property holds: 10,701,608 states |
gate-1: the key and payee not checked again at approval | As payments | G1 broken in 12 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 transaction | As payments | G1 broken in 11 s: the approver revoked between the two |
| Approval terms without the request's id and hash | As payments | G1 broken in 48 s: root replays a person's approval of one request on another with the same terms, and it pays |
An approval leaves the request pending | As payments | G2 broken in 12 s |
No record of applied changes (used_ops) | As changes of authority | G3 broken in 36 s: root replays an owner's change from the log and undoes a later one |
| The signer not checked again in a change's transaction | As changes of authority | G3 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 is compared by hand; 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); or anything about the cryptography and WebAuthn's own checks, which the model assumes.
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.
-
code 1366ca637 refused, 1 formed, 0 failures
sha256 8b835c1501ef35667ec9a328480948842772c1cf0c7a88c3a456eb796749d748
-
code 4ad364738 refused, 1 formed, 0 other than predicted
sha256 aed031ca02191ea73cfb0fba4dafc8c2fa6c8d8965555e9535a600fecd8c4205
-
code 615c73888 passed, 1 failed (explained)
sha256 89cb11d6877c67d8ac3d44830fdc3299b8e86263f42b6bc99553b7d51bfaf40e
-
code ab0a67d21 as predicted, 1 formed, 0 unexpected
sha256 6d4b8202017f02bf3fb785a126c07db3274454845971f9ee8381be048d6f4c4b
-
sha256 5cf33bb32dbb8ca7eb7f3dde6b27c449093b5ad62ed098846d3da2d1a6e81f6d
-
sha256 f25ed41b9df991ca013ffb737e49695c6266fc06199ce24256a582286eb33586
-
code 87bc9a1
sha256 dad4b21334333e007eb30a99970a0931b708949715c01a33497af9d812d4b8ed
-
code 87bc9a1all nine as expected
sha256 db7236cc442a2d5d53c17d408f6e6c869fcf8164f9a1129b8e9fb135c495f37c
-
code 87bc9a1
sha256 01e4359f384da373027e89cc917d6e43937410ec25ad891d9344eed957b27b74
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:
- Every signature in the gate's log, with the checker.
- The bytes of every run log, with the hashes above.
- Any ARBITER certificate, with the public key.
- S1, S2 and S4 by hand, in a sandbox of your own: demo.sovereignarbiter.com/#sandbox (the gate is not connected to the demo).
- The model check, with Docker: put the four model files in an empty folder and run
bash check.sh (about 26 minutes on one core; bash check.sh gate-1 alone takes seconds). SHA256SUMS beside them lists their hashes.
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.