arcaeon-audit
Tamper-evident, audit-ready logs for AI agents. Observability shows you what your agent did. This lets you show it wasn't altered afterward — the integrity layer under the records your ISO 42001 / SOC 2 auditor and your enterprise customers' questionnaires already ask you to keep.
pip install arcaeon-audit
Why this exists
If you ship an AI agent into anything regulated or enterprise-sold, you're already being asked to keep records of what it did and produce them on demand — by your SOC 2 auditor, by ISO/IEC 42001 (Annex A.6.2.8: keep AI event logs across the lifecycle), and by the security/AI-governance questionnaires your enterprise customers send today.
What the law does and does not say, stated carefully because vendors routinely overstate it. The EU AI Act (Article 12) requires automatic recording of events for high-risk systems; it contains no tamper-evidence or integrity requirement, and anyone telling you it mandates tamper-proof logging has not read it. The regulations with teeth on reconstructibility of records are elsewhere: DORA's technical standards (financial entities, in force now) require that ICT-related events be reconstructible after the fact, and the revised Product Liability Directive (applies 9 December 2026) lets courts presume a defect where a defendant fails to produce evidence within its control. Neither mandates this product. What they do is make a records gap expensive in a dispute.
The practical gap is simpler than the law: a plain log file proves nothing, because anyone with write access can edit, delete, or reorder a past record and nothing shows. When a record is challenged, "we logged it" is a self-report. An append-only, hash-chained record whose head is checkable by a party you don't control is the difference between an assertion and evidence.
arcaeon-audit is the small, boring layer that gives you exactly that, in two lines
and a folder. It wraps arcaeon-ledger
(a hash-chained append-only log, zero heavy deps) with an audit-event vocabulary and
an export bundle an auditor can re-verify without trusting you. It is a mechanism,
not a certification: it makes no one compliant with anything, and the export's own
summary says so in writing.
Use
from arcaeon_audit import AuditLog
log = AuditLog("agent-audit.jsonl", system_id="triage-agent-v3", provider="Acme AI")
log.record(event="system_start", agent="triage-agent-v3")
log.record(event="input", agent="triage-agent-v3", inputs={"patient_msg": "chest pain"})
log.record(event="decision", agent="triage-agent-v3", decision="escalate",
outputs={"routed_to": "ER", "priority": 1}, capability_version="v2")
log.verify().ok # True — any edit to history would make this False
log.export_bundle("audit-export/") # regulator-ready folder
export_bundle() writes a self-verifying folder:
| file | what it is |
|---|---|
records.jsonl |
the full hash-chained audit log, verbatim |
integrity.json |
verdict — chain_ok, truncation_checked, truncation_ok, row count, exact first break if any; a witness block naming the witness's kind / identifier / independence + independence_source (who decided that label — a self-declaring witness can no longer earn the strongest one by omission) / self_integrity (did the witness prove its OWN record unedited); and how_to_reverify, a two-step recipe whose second step is a prefilled no-credential call against a public witness — the step that does not require trusting us |
manifest.json |
system id, provider, period covered, counts by event type |
ARTICLE_12_SUMMARY.md |
human-readable mapping to Article 12's requirements |
witness.json |
the external-witness pin + truncation verdict + its nature (only when a witness is consulted) |
The bundle is self-verifying: records.jsonl is hash-chained, so anyone can
re-run arcaeon-ledger's verify_file() and reproduce integrity.json. Tamper
evidence does not depend on trusting this tool or its author — that's the point.
Truncation, and why a chain alone can't catch it
A hash chain proves nobody edited or reordered your records. It provably cannot prove nobody truncated them: delete the most recent rows and the surviving prefix still chains clean. So a log truncated before export would earn a clean pass from chain verification alone — the one gap the integrity check can't close by itself.
The fix is an external witness: an outside record of your log's head
(rows, chain) at a point in time. Pin to it on a cadence; a later truncation has
fewer rows than the witness saw, and is caught. export_bundle() cross-checks
against the witness when one is configured, and the bundle's verdict distinguishes:
PASS— chain intact and the witnessed prefix matches the witness: no truncation up to the witnessed head. The only verdict that claims completeness — but only through the last pin, and only if the witness is controlled independently of whoever can write the log. Records written after the last pin are not covered (pin close to export), and a witness an attacker can also rewrite proves nothing.VERIFIED_MODULO_TRUNCATION— chain intact, but no witness was consulted, so truncation was not checked. Honest non-proof, never a silent clean pass.TRUNCATION_DETECTED/REWRITE_DETECTED— the witness caught missing or re-minted history. A positive detection by the witness outranks every chain-scope verdict below: what the witness saw is evidence, what the local chain could not scan is not.EMPTY_LOG— the file is genuinely empty: zero rows, nothing recorded yet. An absence of evidence, never an accusation. Only for a truly empty file.UNVERIFIED_SCOPE— the log has records, but the chain could not speak for them (they carry no chain links — the normal state of a log adopted from unchained history). Not a pass, not an accusation, an unanswered question. Exits 2, because an unanswered question must never gate CI green.WITNESS_CHECK_FAILED/UNRECOGNIZED_WITNESS_VERDICT:<v>— the witness consultation could not complete, or returned a verdict this version does not know. Reported verbatim rather than mapped onto the nearest accusation.
Exit codes (verify and export share one contract, so the two subcommands
can never disagree about the same file): 0 nothing wrong found · 1 an
accusation · 2 the check could not complete.
witness.self_integrity in integrity.json answers a question separate from
independence: did the witness prove its own record was unedited? verified
only when the witness self-verified its pin chain (arcaeon-ledger 0.5.9+ local
store). Every remote/hosted client exposes only .latest() and therefore reads
unestablished — the comparison still runs, but a PASS resting on it says so
in plain text, because an attacker who can write both the log and the pin can
produce exactly that PASS.
Judge independence yourself. A witness only proves completeness if it is
controlled independently of whoever can write the log. So integrity.json names
what the witness was: a witness block with kind (local_file / remote_url /
opentimestamps / none), its identifier, and an honest independence label
(self_asserted / externally_verifiable / undeclared / none).
And it names who decided that label. independence_source is
established_by_type (we could tell from the object itself — this outranks any
claim), self_declared_by_witness (the witness said so; the note is prefixed
SELF-DECLARED and you should verify the identifier yourself), conservative_default,
or no_witness. A witness that omits independence now reads undeclared, never
the strongest value — an earlier version defaulted the other way, so a shim
wrapping a local file in the log owner's own directory could earn
externally_verifiable without its URL ever being contacted. independence is the
field a regulator weighs a PASS on; it must not be settable by omission. The reference WitnessStore is
a local file — same control domain as the log — so it is labelled self_asserted,
not independent: a PASS backed by it is self-attested, and the block says so
plainly. The point is to let a regulator see the independence question, not to claim
an independence we don't have. (Bundles from ≤0.1.3 predate this block; witness_nature_of()
reads them back as kind: unknown.)
from arcaeon_ledger.witness import WitnessStore, publish_head
from arcaeon_ledger import Ledger
store = WitnessStore("witness.jsonl") # or a hosted witness endpoint
publish_head(store, "acme/triage-v3", Ledger("agent-audit.jsonl")) # pin, on a cadence
log.export_bundle("audit-export/", witness=store, witness_namespace="acme/triage-v3")
CLI
arcaeon-audit verify agent-audit.jsonl
arcaeon-audit pin agent-audit.jsonl witness.jsonl acme/triage-v3
arcaeon-audit export agent-audit.jsonl audit-export/ --system-id triage-v3 --provider "Acme AI" \
--witness witness.jsonl --namespace acme/triage-v3
What this is and isn't
Is: an engineering control that produces automatic, tamper-evident, exportable records — the integrity + export primitive Article 12 leans on.
Isn't: legal advice, and not compliance-in-a-box on its own. Article 12 compliance also depends on what you choose to log and your broader obligations under the Act. This tool gives you the hard part (provable integrity + a clean export); the coverage is yours to define.
How it works
Every record is hash-chained: chain = sha256(prev_chain + canonical(row)). Edit,
delete, or reorder any record and every later link breaks; verify() names the
exact row. Records also carry an authority block (principal + capability version)
so "was this edited?" sharpens to "was this edited and was the writer authorized?"
MIT licensed. Built by Arcaeon — the evidence layer for AI.
Release files for arcaeon-audit 0.1.5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| arcaeon_audit-0.1.5.tar.gz | 27.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| arcaeon_audit-0.1.5-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 53.4 kB
Release files / arcaeon_audit-0.1.5.tar.gz
| Download URL | arcaeon_audit-0.1.5.tar.gz |
|---|---|
| Size | 27.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d8ed26c5ecc22a0899b905a7136cee7041b649b34caf04b04087119e0337d1bc
|
|
BLAKE2b-256 checksum How to use checksums |
9967ee158f51c7bc0958a4b55cbbff644d4bbd5392ee2768ffc2ec444ed3e869
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.3
|
Release files / arcaeon_audit-0.1.5-py3-none-any.whl
| Download URL | arcaeon_audit-0.1.5-py3-none-any.whl |
|---|---|
| Size | 25.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2c6b25642bccbf2968c4d326c924ab42b3f1d3aa57fd28e1646a3f23d981d848
|
|
BLAKE2b-256 checksum How to use checksums |
cba56b00f67bf1eed68ba69c954023ff5cddb8df7cd7a615a4668cec7db0a010
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.3
|