Metadata-Version: 2.4
Name: arcaeon-audit
Version: 0.1.5
Summary: Tamper-evident, audit-ready logs for AI agents — ISO 42001 / SOC 2 / EU AI Act evidence. Prove what your agent did.
Author: Arcaeon
License: MIT
Project-URL: Homepage, https://arcaeon.io
Project-URL: Source, https://github.com/Arcaeon-io/arcaeon-audit
Keywords: ai,agent,audit,audit-log,tamper-evident,provenance,eu-ai-act,article-12,compliance,hash-chain,observability
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: arcaeon-ledger>=0.5.9
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: hypothesis; extra == "dev"
Dynamic: license-file

# 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.*

```bash
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`](https://pypi.org/project/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

```python
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`.)

```python
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

```bash
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](https://arcaeon.io) — the evidence layer for AI.
