AISOURCEOFTRUTH.COM

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

What the helper does on each tick

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.

$ curl -H 'Accept: text/markdown' https://aisourceoftruth.com/guides/anchor-agent-logs # this page is a rendering of the API. Reproduce it as data.