Skip to main content

Open, tamper-evident runtime records for AI agents: the Halo Runtime Record format, with a dependency-free conformance verifier.

Project description

halo-record

Tamper-evident runtime records for AI agents: the audit trail the vendor runs but cannot silently edit.

Every action your agent takes (tool calls, model calls, data access, approvals) becomes one record in an append-structured, hash-chained log. Any party holding a checkpoint of the chain can verify the records behind it were never altered, without trusting whoever produced them. When a customer's security team asks "what did your agent do with our data?", you hand them a link instead of a paragraph. Security reviews already ask AI questions next to the SOC 2 checklist, and today a written assurance still passes. The bet behind this project is that it won't for long.

The record format is open and free to implement. This package is the reference implementation: recorder, verifier, witness client, and report server.

Using halo-record, or thinking about it? Tell me who you are and what for → Who's using halo-record?

Why you can trust this code

You are being asked to put a recorder inside your agent. You should not take that on faith:

  • Zero runtime dependencies. Standard library only. pip install halo-record installs exactly one package.
  • No network calls, except the witness, which is opt-in and receives only a record count and a chain fingerprint. Record contents never leave your infrastructure.
  • Full payloads never enter a record. Arguments are hashed and stored only as a short redacted summary — the complete raw value is never written, though a summary can carry fragments of it. Redaction is best-effort (regex over common secret and PII formats plus an entropy catch-all): treat it as defense-in-depth, not a guarantee.
  • Small enough to audit. ~4,800 lines of Python. Read all of it in an afternoon.
  • Apache-2.0.

60-second demo

No agent required. With uv, nothing to install:

uvx --from halo-record halo demo --serve

or the classic way:

pip install halo-record
halo demo --serve

Either one scaffolds a fictional support-agent vendor with two customers, witnesses the chains, serves their gated Runtime Reports, and opens the operator console in your browser. Then try the tamper test: delete a line from one of the .jsonl files and reload. The report catches it.

Record your own agent

One line at the boundary:

from halo import trace

agent = trace(run_my_agent, profile="my-agent", log="audit.jsonl")   # wraps your entrypoint; records every tool call to ./audit.jsonl

Without log=, records go to ~/.halo/my-agent.jsonl (one chain per agent). Or use the adapter for what you already run (see the matrix below). Then render the report:

halo report audit.jsonl -o report.html    # one chain -> self-verifying HTML
halo serve ./records --port 8721          # all tenants, gated per customer

The quickstart ends when you are looking at your own agent's Runtime Report in a browser. If you got a JSONL file and no report, something is wrong: open an issue.

Connect to what you already run

Captured at the boundary Ingested from existing telemetry
Native recorder (from halo import trace) OpenTelemetry GenAI spans
MCP interceptor LiteLLM callbacks
LangChain / LangGraph callback Langfuse export
OpenAI Agents SDK hooks Any gateway / reverse-proxy log
Claude Code / Claude Agent SDK hook

Framework adapters and ingestion paths stamp each record with a source tag, so the report discloses how each piece of evidence was collected. Captured and ingested records live in the same chain.

For LangChain / LangGraph, it is a callback handler:

from halo_record import Recorder
from halo_record.integrations.langchain import HaloCallbackHandler

recorder = Recorder("audit.jsonl")
result = my_chain.invoke(inputs, config={"callbacks": [HaloCallbackHandler(recorder)]})   # every tool call becomes a record

Anything that emits OpenTelemetry GenAI spans (CrewAI, LlamaIndex, and most agent frameworks with OTel instrumentation) lands in the chain through the OTel adapter, and the TypeScript package ships native adapters for the Vercel AI SDK and the JS agent ecosystem. Missing an adapter for your stack? Open an issue. Most adapters are about a hundred lines.

Record your coding agent

Claude Code fires a PostToolUse hook after every tool call. Point it at halo hook and each action — file writes, shell commands, MCP connector calls — becomes a record in a local chain. No code changes; one settings entry:

{
  "hooks": {
    "PostToolUse": [
      {"matcher": "*", "hooks": [{"type": "command", "command": "halo hook"}]}
    ]
  }
}

Add that to ~/.claude/settings.json and records land in ~/.halo/audit.jsonl (override with $HALO_LOG). Pure-orchestration tools that touch no data, network, or external state are skipped — the chain records trust-boundary actions, not thinking. Set HALO_HASH_ONLY=1 to record content hashes without summaries. Set HALO_AGENT_VERSION (and optionally HALO_AGENT_MODEL) to bind every record to the agent build that produced it — when an auditor asks about the version that was running in a given window, the export answers by column instead of by recollection.

If you need the report to answer "under what rules did this run happen?", set HALO_AUTHORITY_FILE to a JSON snapshot of the effective authority for the session. Keep it privacy-safe: hashes and refs, not raw prompts, private policy text, secrets, or full tool schemas.

{
  "snapshot_id": "auth_2026_07_08T1100Z",
  "captured_at": "2026-07-08T11:00:00Z",
  "scope": "session",
  "workspace": {"path_hash": "sha256:...", "git_commit": "abc1234"},
  "refs": [
    {"kind": "project_rules", "id": "CLAUDE.md", "hash": "sha256:...", "loaded": true, "truncated": false},
    {"kind": "mcp_tool_registry", "id": "filesystem", "hash": "sha256:..."}
  ],
  "omissions": [{"kind": "private_policy", "reason": "customer_secret", "hash": "sha256:..."}],
  "stale_if": ["project_rules_hash_changed", "mcp_tool_registry_hash_changed"]
}
HALO_AUTHORITY_FILE=./authority.json halo hook

The snapshot is sealed into the same hash chain as the action records. A good default is one session-level snapshot at start, plus a new snapshot when rules, Skills, hooks, MCP tool registries, or compaction policy change. To keep long sessions lean, consecutive records with the same authority.snapshot_id are compacted after the first full snapshot: later records keep only {"snapshot_id": "...", "same_as_previous": true}. The pointer stays hash-chained, but the bulky refs/omissions/stale-if block is not repeated on every action. Then, the usual:

halo verify ~/.halo/audit.jsonl
halo report ~/.halo/audit.jsonl -o report.html

Any agent runtime that exposes a post-action hook can feed the same command — the hook reads one event as JSON on stdin and appends one record.

When recording fails

The two integration styles fail in opposite directions, on purpose — pick the one whose failure you can live with:

  • Framework adapters (LangChain, hooks via callback managers) fail open. If a record cannot be written (disk full, permissions), the agent's action completes normally and the record is lost. The LangChain handler prints a loud warning to stderr and counts the loss (handler.lost_records), but nothing in the chain itself can show a record that was never written — a stalled chain still verifies. Witness checkpoints on a cadence are what make a stalled chain visible: an expected checkpoint that never arrives is the alarm.
  • The native trace() wrapper fails closed. If the record cannot be written, the exception propagates into the agent's action — no evidence, no action. Stricter, and it can interrupt your agent.

Neither default is right for everyone; know which one you are running.

Integrity vs. completeness (read this part)

Be precise about what each layer proves — because they are different claims, and the differences are the point:

A self-held chain proves integrity relative to an established head: given a chain head someone already holds, any edit, reordering, or deletion in the records behind it becomes detectable. By itself — before anyone outside the operator has seen a head — a chain proves internal consistency, not history: an operator could drop a record and re-seal, and the new file would verify. The chain becomes historically committed the moment its head leaves the operator's control.

That is the witness: a party outside the operator holding periodic fingerprints of the chain (a count and a head hash, nothing else). Checkpoints make rewriting committed history detectable, and a missed checkpoint is itself a visible event:

halo anchor audit.jsonl witness.jsonl           # anchor a checkpoint to a local witness
halo anchor audit.jsonl witness.jsonl --check   # completeness verdict against it

One more boundary, stated plainly: neither the chain nor the witness proves that every real-world action passed through the recorder. That is capture completeness — a property of where the recorder sits in the stack (native instrumentation, hooks, gateway ingestion), not of any hash. Records carry a source tag for exactly this reason.

Claim Self-held chain + External checkpoints + Trusted capture
Detect edits to an established artifact
Detect rewriting of committed history
Detect missing/late checkpoints ✔ (agreed cadence)
Prove every action was recorded depends on capture boundary

Anyone can run a witness. A witness you run yourself commits history to you; committing it to your customer requires a witness they have reason to trust. The protocol is open either way.

A hosted, recognized witness is how this project will sustain itself. Early access: bkuan001@gmail.com.

Where this sits in a compliance stack

halo-record is an evidence layer, not a certification. It produces the artifact that assessment frameworks keep asking for in different words:

  • Security questionnaires and SOC 2 reviews: answer the AI sections with a verifiable Runtime Report instead of screenshots and prose.
  • AIUC-1: produces the tamper-evident logging (E015.4) and full-execution-chain records with authorization events (E015.2) the standard's Accountability controls call for — continuous runtime evidence, not reconstructed at audit time.
  • OWASP (GenAI Security Project): the runtime evidence behind the agent-behavior risks in the OWASP Top 10 for Agentic Applications 2026 and the LLM Top 10 — goal hijack, tool misuse, identity and privilege abuse — recorded as what the agent actually did, with which tools and data. See OWASP.md.
  • AARM (CSA): produces the tamper-evident action receipt AARM specifies (R5/R6) — chained and independently witnessed. halo-record is the receipt layer; pair it with an enforcement gateway for a full AARM system. See AARM.md.
  • Agentic Trust Controls: the runtime records behind the ATC's evidence controls — tamper-evident action logging (RBM-03) and authority attestation (AID-05) in one chained record, with the witness layer beyond both. See ATC.md.
  • EU AI Act: logging and record-keeping obligations for high-risk AI systems.
  • ISO 42001 / NIST AI RMF: the operational evidence behind management-system controls.

None of this certifies anything by itself. It gives your assessor something verifiable to look at. The boundaries — what halo-record deliberately does not do, and what to say when a reviewer asks — are documented in LIMITS.md.

Getting the evidence into your GRC platform

Most GRC platforms (Vanta, Drata, and similar) accept uploaded files as custom evidence against a control. halo-record's export is built to drop into that flow:

halo export audit.jsonl --from 2026-06-01 --to 2026-06-30 -o evidence.csv

This writes two files for the audit window: the CSV (one row per recorded action — the agent build and model that produced each, the identity it ran on behalf of, the record it was caused by, its authorization decision, and any personal-data categories or ingested threat flags) and a manifest (evidence.csv.manifest.json) that ties the CSV to its source — the chain's head hash links it to the verifiable log it came from, and csv_sha256 is the exported file's own hash, so a CSV edited after export no longer matches its manifest. Upload both against your logging or monitoring control; attach the Runtime Report HTML when a reviewer wants to verify the chain themselves. The export refuses to run on a chain that fails verification.

A native push integration — evidence landing in your platform automatically — is on the roadmap. The file path above works today with any platform that accepts uploaded evidence.

CLI

halo verify   validate schema + hash chain (exit 1 broken, 3 empty chain; CI-friendly)
halo report   render a chain as a self-verifying HTML Runtime Report
              (--from/--to: a date-windowed report covering only the review period)
halo policy   corroborate a chain against a declarative policy pack
              (per-rule pass / violation / evidence-gap; exit 1 violated, 3 nothing in scope)
halo serve    serve per-tenant reports over HTTP, access-scoped per customer
halo grant    designate a report recipient (email or domain)
halo viewers  list who has unlocked a gated report
halo anchor   witness a chain head, or --check completeness (exit 1 incomplete, 3 unwitnessed)
halo witness-serve  run a witness over HTTP: vendors anchor chain heads, viewers fetch checkpoints
halo demo     scaffold the full vendor demo (record -> witness -> gated report)
halo export   date-bounded evidence export: CSV + manifest tied to the chain head
halo sample   emit a valid example log
halo hash     canonical sha256 of a JSON value
halo hook     Claude Code PostToolUse hook

Integrity model

To compute a record's hash: take the record excluding integrity.hash, with integrity.prev_hash set to the previous record's hash; canonicalize with RFC 8785 (JSON Canonicalization Scheme); SHA-256 the bytes. The first record's prev_hash is 64 zeros. Verification recomputes every hash and checks every link. No secret required; that is the point.

Think you can tamper with a chain without the verifier noticing? Attempts and results live here.

Full field reference: halo-record.schema.json.

TypeScript

The same recorder ships for Node: halo-record-ts. Same chain format, same witness protocol. Records written in either language verify with either verifier.

Contributing

Issues, discussions, and pull requests welcome — see CONTRIBUTING.md for the ground rules (short version: tests required, small PRs, schema changes get discussed first).

License

Apache-2.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

halo_record-0.2.22.tar.gz (110.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

halo_record-0.2.22-py3-none-any.whl (97.7 kB view details)

Uploaded Python 3

File details

Details for the file halo_record-0.2.22.tar.gz.

File metadata

  • Download URL: halo_record-0.2.22.tar.gz
  • Upload date:
  • Size: 110.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for halo_record-0.2.22.tar.gz
Algorithm Hash digest
SHA256 fc77c95c642b614a9572f864a6a78ba32d1461732b002cc0b205f5bb60171554
MD5 52123283b9645601cd8d8d3608863648
BLAKE2b-256 aec6ab8c3f3d3c077866242b37996ac706478ccc71704e62669e11ea663c66f9

See more details on using hashes here.

File details

Details for the file halo_record-0.2.22-py3-none-any.whl.

File metadata

  • Download URL: halo_record-0.2.22-py3-none-any.whl
  • Upload date:
  • Size: 97.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for halo_record-0.2.22-py3-none-any.whl
Algorithm Hash digest
SHA256 e594a62520105cbcb1d73917e4e92367996e7c71f77e858413fa546eec4f717c
MD5 a95cb776f1dd991330b79a61023771fa
BLAKE2b-256 d59274e8210debc9dde2032a67badc02e8c10d401bf5e06389637d0039f76ab6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page