Anchor an agent audit log
Agent audit libraries such as halo-record and agentproofs write each action to a hash chain on your own disk. A chain you hold proves records were not edited after someone outside saw its head. Until then, whoever runs the recorder can drop a record and re-seal the chain, and it still verifies. Anchoring closes that gap: on a timer, post the chain's size and head hash to the registry as a checkpoint record.
The registry keeps every head in its append-only log and refuses, per publisher and log, a smaller size (a rollback) or the same size with another head (a fork). Checkpoint records are free and never count against your quota. Only three values leave your machine: a log id you choose, the entry count, and the head hash. The records themselves stay with you.
Before you start
- A publisher key and client credentials: register with
POST /v1/publishers(publish shows the three calls). Once the CLI is on npm,aisot init --id <slug> --name <name>writes them toaisot.json. - The helper:
anchorChainin@aisourceoftruth/sdk,anchor_chainin theaisotPython package, oraisot anchorin the CLI. All three behave the same. They come with the 0.2.0 packages, which are not released yet; until then post the checkpoint record below over the HTTP API.
What the helper does on each tick
- Reads the head with your
getHead():{ size, headHash }, or null while the chain is empty. - Posts nothing when the head has not moved.
- Otherwise builds a checkpoint record (
algorithm: "sha256-chain"), signs it and posts it with an Idempotency-Key derived from the log id, the size and the head hash. A timeout, 429 or 5xx is retried with the same bytes and key, so a retry never adds a second record. Retry-After is honored. - On a rollback or fork refusal it calls your alarm callback once and stops for good. It never retries one.
halo-record
halo-record writes one JSON record per line. The chain size is the record count, and the head is the last record's integrity.hash (SHA-256 hex over the RFC 8785 form of the record).
import { readFileSync } from "node:fs";
import { anchorChain } from "@aisourceoftruth/sdk";
function haloHead(path) {
const lines = readFileSync(path, "utf8").split("\n").filter((l) => l.trim());
if (!lines.length) return null;
return { size: lines.length, headHash: JSON.parse(lines.at(-1)).integrity.hash };
}
anchorChain({
logId: "halo:my-agent",
getHead: () => haloHead("audit.jsonl"),
signer: { keyId: cfg.key_id, privateJwk: cfg.private_jwk },
clientId: cfg.client_id,
clientSecret: cfg.client_secret,
intervalMs: 5 * 60 * 1000,
onAlarm: (a) => page(`audit log ${a.logId}: ${a.kind} at size ${a.size}`),
});
The same in Python, beside the recorder:
import json
from aisot import AisotClient, anchor_chain
def halo_head(path):
with open(path, encoding="utf-8") as f:
lines = [l for l in f if l.strip()]
if not lines:
return None
return {"size": len(lines), "head_hash": json.loads(lines[-1])["integrity"]["hash"]}
client = AisotClient(key_id=cfg["key_id"], private_jwk=cfg["private_jwk"], timeout=30)
client.authenticate(cfg["client_id"], cfg["client_secret"])
anchor_chain(get_head=lambda: halo_head("audit.jsonl"), client=client, log_id="halo:my-agent",
client_id=cfg["client_id"], client_secret=cfg["client_secret"],
interval_s=300, on_alarm=alert, start=True)
agentproofs
agentproofs keeps signed, hash-linked entries in JSONL segments under its data directory. Its readChainState export returns the last entry's sequence (the entry count, from 1) and lastHash (SHA-256 hex); sequence is 0 before the first entry.
import { readChainState } from "agentproofs";
import { anchorChain } from "@aisourceoftruth/sdk";
anchorChain({
logId: "agentproofs:default",
getHead: async () => {
const s = await readChainState(process.env.AGENTPROOFS_DATA_DIR, "default");
return s.sequence === 0 ? null : { size: s.sequence, headHash: s.lastHash };
},
signer: { keyId: cfg.key_id, privateJwk: cfg.private_jwk },
clientId: cfg.client_id,
clientSecret: cfg.client_secret,
onAlarm: (a) => page(`proof chain ${a.kind} at size ${a.size}`),
});
Any other chain, from the CLI
aisot anchor runs a command on a timer. The command prints the head as JSON {"size": N, "headHash": "<hex>"} or as <size> <hash>, and prints nothing while the chain is empty. On an alarm the CLI exits with code 2.
// halo-head.mjs: print "<size> <head hash>" for a halo-record JSONL file
import { readFileSync } from "node:fs";
const lines = readFileSync(process.argv[2], "utf8").split("\n").filter((l) => l.trim());
if (lines.length) console.log(lines.length, JSON.parse(lines.at(-1)).integrity.hash);
aisot anchor --log-id halo:my-agent --every 5m --head-cmd "node halo-head.mjs audit.jsonl"
Check a local chain against the anchored heads
Anyone holding the chain file can check it, with no token. List your checkpoint records for the log, then confirm that the entry at each anchored size has the anchored head hash. A record that was removed or rewritten after an anchor makes the check fail.
const origin = "https://aisourceoftruth.com";
const lines = readFileSync("audit.jsonl", "utf8").split("\n").filter((l) => l.trim());
const list = await (await fetch(`${origin}/v1/records?profile=checkpoint&publisher=<you>&ref=halo:my-agent&limit=100`)).json();
for (const r of list.records) {
const { record } = await (await fetch(r.url, { headers: { accept: "application/json" } })).json();
const { tree_size, root_hash } = record.checkpoint;
const local = lines[tree_size - 1] && JSON.parse(lines[tree_size - 1]).integrity.hash;
console.log(tree_size, local === root_hash ? "matches" : "DOES NOT MATCH");
}
Recompute the chain itself first with the library's own verifier (halo verify, npx agentproofs verify), so you know each stored hash is right. To check a checkpoint record's signature as well, call verify(digest) in the SDK.
What an anchor does not prove
It proves the chain had this size and head no later than the time the record was appended, and that no record before that point changed since. It does not prove that every action reached the recorder, and it says nothing about records written after the last anchor. Anchor often enough that a gap between heads is itself a signal.