DEV Community

andreysparish
andreysparish

Posted on

How to verify an LLM agent's audit log without the decryption key

If an agent's log says it called a tool on a given day, how does someone outside your team check that, without you handing over the key that decrypts every prompt in it?
Below is the check we built into Palimpsests (PALA-1), an Apache-2.0 runtime for local LLM inference. You can run all of it offline in about a minute.
What the verifier looks at
Every record in the log has a header and an optional body. The header stores the SHA-256 of its own body and the hash of the record before it. The verifier walks the headers one by one and compares each body with the digest stored for it. It never decrypts a body, so it never needs the key.
Step 1: write a small audited run
pip install palimpsests
palimpsests demo

The demo uses a deterministic backend, so nothing gets downloaded. It serves one turn, lets the model call a tool and writes the result to palimpsests-demo.pala. With 0.12.0 the output ends like this:

Audit chain written: palimpsests-demo.pala
Verifying with the production reader (what an auditor runs)
records: 7 chain_ok: True head: a3682a36b3a9b703…
recorded events: MODEL_LOAD, TOOL_CALL, TOOL_RESULT

Your head hash will be different, because the headers include boot ids and timestamps.
Step 2: verify the file on its own

palimpsests pala verify palimpsests-demo.pala
_
Text
_consistency: 7 records, chain intact
completeness: NOT CHECKED — no --anchor supplied, so tail truncation or wholesale replacement would not have been detected
witness: no WITNESS records — existence at a point in time is not attested

The exit code is 2.
With nothing but the file, the verifier can confirm that no record inside it was changed, dropped or moved. It can't tell whether someone deleted the last few records, or swapped the whole file for another chain that is consistent with itself. So it says that it didn't check, and exits with a code your CI can tell apart from success.
Step 3: give it an anchor
An anchor is the latest head hash, stored somewhere outside the file. In a real deployment that's an anchor file, a PKCS#11 token or a transparency service. For the demo, take the head from the JSON output and pass it by hand:
HEAD=$(palimpsests pala verify palimpsests-demo.pala --json | python3 -c "import json,sys; print(json.load(sys.stdin)['head'])")
palimpsests pala verify palimpsests-demo.pala --anchor "$HEAD"

Exit code 0. No key was used at any point.
Step 4: break it
Flip one bit at byte 200 and check again with the same anchor:

cp palimpsests-demo.pala tampered.pala
python3 -c "d=bytearray(open('tampered.pala','rb').read()); d[200]^=1; open('tampered.pala','wb').write(d)"
palimpsests pala verify tampered.pala --anchor "$HEAD"

Exit code 1.
The two lines contradict each other only at first sight. The edit was in an early record, so the last record and its hash are untouched and still match the anchor. The link between records 1 and 2 no longer holds, and the verifier names where. Someone investigating can tell an edit in the middle from a cut at the end, because the tool reports them separately.
Limits
The demo backend only stands in for a model. Real runs use llama.cpp or Ollama on your own hardware.
The log implementation has not been through an independent penetration test yet.
Why bother with any of this
For high-risk AI systems in the EU, Article 12 of the AI Act asks for automatic event logs, and deployers have to keep them for at least six months under Article 26(6). An auditor will usually start with the tool calls, since that's where an agent acts on the world. I wrote the legal side up separately, with dates and the actual fine levels: EU AI Act Article 12 for self-hosted LLMs.
The records use PALA-1 (Portable Append-only Log for Audit). The format was frozen at v1.0 in August 2026, the spec and test vectors are CC0, and five independent implementations have reproduced it from the spec alone. If you'd rather write your own verifier than trust ours, start at palimpsests.dev/pala-1.

Top comments (0)