AI Factory · the audit record

Our own evidence overclaimed. Here is what we found and what we changed.

On 2026-08-26 four attacks were run against AI Factory's audit chain in the production database of a deployed service, from a process that had written none of the entries. All four were detected. Two of them could not have been run the week before — the columns they attack were stored and not hashed, and the earlier attack that established this ledger's reputation had certified those columns by association without anyone noticing. That is the subject of this page.

What AI Factory is, before any of this

AI Factory is a build-and-review loop for work produced by AI agents. A Prime Builder produces a change; an independent Loyal Opposition is bound by a versioned role constitution that licenses and obligates specific, severity-tagged dissent; and a deterministic gate — ordinary Rust, no model in the arbitration path — turns the dissent set into one of GO, REVISE, NO_GO or HUMAN_ESCALATED. A human holds the override. The product page sets out the loop.

Every cycle, decision and override is written to a hash-linked table — the audit chain this page is about. It is what makes the gate's record worth anything: a deterministic gate that nobody can prove was not edited afterwards decides nothing.

Scope. This is AI Factory's own audit chain and this page is about that one record. It is not a claim about any other component's ledger, and nothing here should be read across to one.

What was attacked

Each entry commits to the one before it — a SHA-256 over a preimage of the entry's columns, recomputed from genesis whenever the chain is verified. The fields are length-prefixed rather than joined on a delimiter, so two different column tuples cannot collide into one preimage. details, the queryable JSONB copy of the payload, is not hashed directly: verification asserts that it decodes to the same JSON value as details_canon, which is. Which columns that preimage covers is the subject of the section after next, and it is what changed on 2026-08-26.

The entries were written by the aif CLI running inside the VPC as a separate process, into the deployed service's production database. They were verified by a different container, started afterwards, whose filesystem held none of the writer's state. Each attack proved its mutation had landed before any claim was made about detection, and each restore was verified before the next attack ran.

The four attacks

AttackExitCaught atHow
Mutate a hashed column event_type edited directly in the database at seq 2. 4 seq 2 entry_hash mismatch — this entry was mutated after it was written
Delete an entry from the middle One row removed from the interior of the chain, at seq 3. 4 seq 4 prev_hash does not match the previous entry — deleted or reordered
Re-point session_id, alone gap closed the same morning One entry reassigned to a different session at seq 7. Nothing else touched. 4 seq 7 entry_hash mismatch — this entry was mutated after it was written
Mutate the details JSONB, alone gap closed the same morning The queryable copy of the payload edited at seq 6, leaving the hashed copy intact. 4 seq 6 details diverges from details_canon — mutated outside the hash

Four detections are four detections. They are not coverage, and the two sections below are the reason that sentence is here rather than at the bottom.

The attack that made this ledger's name could not have found two of these

Before that morning, session_id and details were stored and not hashed. Either could be changed in the database and verification would still return ok. The two attacks marked above are those two columns, and they are new because the gaps were.

The gaps survived a real, executed, passing attack. One line of the tamper test mutated two columns in a single statement:

The line · crates/aif-infrastructure/tests/audit_chain_pg.rs:252
verify_chain_detects_history_forgery
UPDATE aif_audit_events
   SET details_canon = '{"i": 999}', details = '{"i": 999}'::jsonb
 WHERE seq = 2

One statement, two columns. details_canon is hashed; details was not. The test passes on the strength of details_canon alone.

The shape A test that mutates several columns at once proves only that the combination is detected. Which member did the detecting is unrecoverable from a passing result, and the columns that contributed nothing are certified by association. Nobody re-reads an UPDATE to check which columns it touched, because the result was a pass.

Two different failure routes came out of that one line. details was certified by association — it was in the UPDATE, the UPDATE was caught, so it read as covered. session_id was certified by omission — it was never touched, and its absence from the results is indistinguishable from its absence from the threat model. The association route is the sharper of the two: an untested column at least looks untested to anyone who checks, while a column mutated inside a passing multi-column attack looks tested and clear.

The attack was real. It ran. It passed. What outran its scope was the conclusion, not the rigour. The defence is one attack per column: each column gets a test that mutates that column alone, so a pass is attributable — the only thing that changed is the only thing that could have broken it. Combination tests stay, because real tampering edits several columns at once; they stay in addition to the per-column set, never instead of it.

And the honest limit of that defence The per-column set is enumerated by hand. It closes today's gap; it does not close tomorrow's, because a column added without being added to the preimage would be silently mutable again and no test would go red. Deriving the column list from the schema at runtime is what closes that, and it is not done here. Two further columns, agent_id and created_at, have no single-column attack of their own and are not claimed on this page.

Tail truncation is untested in either direction

Not tested · not addressed Removing the newest entries and leaving a shorter, internally consistent chain is not addressed by any attack in this set. Verification recomputes from genesis, and a prefix of a valid chain is exactly what a valid chain looks like — every remaining entry still hashes correctly and still links to its predecessor. No re-verification from genesis can detect it.

AI Factory has no signed head and no external anchor. Nothing above demonstrates that a truncated tail is detected, and nothing above demonstrates that it is not. It is stated because untested is not the same as covered, and four loud detections make that easy to assume.

Crucible — a different product, with a different ledger — has an external anchor, and publishes what it does and does not catch. AI Factory does not have one. That is a difference in this ledger's favour nowhere.

What else it cannot do

Stated here rather than at the bottom, because "attacked and detected" must not be read as more than it is.

No review cycle can be run over the deployed endpoint

aif_submit — the tool that starts a cycle — fails on the deployed service:

Observed · the deployed endpoint, authenticated
aif_submit
{"error":{"code":-32603,
  "message":"role keys: invalid input: AIF_PRIME_VAID must be set"}}

Role keys are not provisioned on the service. Status, handoff and constitution reads work. Every entry in the ledger was written by the CLI, in-VPC, as a separate process — which is what made it a legitimate second process for the durability proof, and is also why nothing on this page is evidence about the HTTP surface.

Two custody domains, and nothing reconciling them

The role-key directory is a local file on an ephemeral filesystem, and the service scales to two instances. That is two custody domains, and environment variables do not fix it: the identifiers are configuration and the key directory is a filesystem.

This is custody divergence, not ledger divergence, and that is evidenced rather than argued. Four concurrent containers with four separate key directories and four distinct identities produced one chain, zero forks, one genesis — no prev_hash used twice, no entry_hash reused, exactly one row descending from GENESIS. The lock that serialises appends lives in the database, not in the process, so instance count is irrelevant to chain integrity. It degrades who-signed-what. It cannot fork the chain.

A gap in the sequence is not a signal

seq is a BIGSERIAL, so a legitimate rolled-back write burns a sequence value and leaves a legitimate gap. There is one, at seq 5, left by a deliberately crashed writer. The verifier therefore keys on prev_hash linkage only, and during the mid-chain deletion above it correctly reported the broken linkage while ignoring the gap. This ledger has one integrity signal where a contiguity-checking chain has two, and that is a deliberate consequence of the column choice, not an oversight.

Deployed from an unmerged branch

The service runs a branch that is not merged to main, and it has to: main lacks the preimage fix, so deploying main would deploy a ledger in which session_id and details are silently mutable. Both container images are pinned by digest, not by tag, and the branch is on a remote — but a deployed service whose source is not on main is a deployed service whose source can be rewritten. It closes when the branch merges, and it has not merged yet.

What you can check, and what you cannot

You cannot reproduce this run. These attacks ran against a production database from inside a private network, against a service with no public hostname, and the AI Factory repository is not public. What is published here is the record of what was done and what it returned — including the gap that no attack on this page addresses — not a check you can run for yourself.

That distinction matters on this site, so it is stated rather than left to be inferred. The claims we ask you to verify yourself, rather than take from us, are the open standard's: reproduce the frozen conformance vectors from the published packages, or verify a signed action in your browser and watch a tampered one reject.

Ask us what else we found against ourselves.

The findings behind this page — including the ones still open — go to anyone evaluating this seriously.

Talk to us