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
| Attack | Exit | Caught at | How |
|---|---|---|---|
| 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:
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.
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.
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
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:
{"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.