项目介绍
Agent 配套工具 · 2026-11-09 更新 · 本站尚未实测
halo-record 是一个面向 AI Agent 的运行审计(Runtime Record)记录与校验工具。它把 Agent 在运行过程中被采集到的动作——工具调用、模型调用、数据访问、审批等——写成一条只追加、以哈希链串联的记录链,再把这条记录链渲染成一个 HTML 形式的 Runtime Report,读者在浏览器打开时页面会重新计算哈希链,并根据校验通过的记录来呈现内容。
按 README 的定位,它是一个“证据层”,而不是合规认证工具;它产出的是安全问卷、供应商评审、AI 治理框架所索要的那类可核查材料。项目同时提供了记录器、校验器、见证(witness)客户端和报告服务端作为参考实现。
需要先说明其边界:链本身能证明记录相对某个已被他人掌握的链头未被编辑,只有由运营方之外的一方持有检查点(checkpoint)时,才能发现已提交历史被重写或删除;而任何哈希都不能证明“每个真实动作都被采集到”,这取决于记录器在系统中的位置(原生埋点、钩子、网关接入等),记录会带 source 标签标明采集方式。
核心能力
点击能力名称查看输入、输出和原文依据。
- 哈希链记录:每条记录写入只追加的哈希链;相对外部持有的检查点可发现后续篡改,但不能阻止记录器运营方重写未对外锚定的历史。
- 自行校验的报告:
halo report生成 HTML,页面在读者浏览器中重算哈希链;不依赖页面代码时可对 JSONL 运行halo verify。 - 外部见证与时间证明:
halo anchor可将检查点锚定到见证方;可选附加 RFC 3161 时间戳,把链状态的时间交由外部时间戳机构证明。 - 多种接入方式:原生
trace/record_call、MCP 客户端包装、LangChain/LangGraph 回调、OpenAI Agents SDK 钩子、Claude Agent SDK 钩子,以及从 OpenTelemetry GenAI span、LiteLLM、Langfuse、网关/反向代理日志接入。 - 编码 Agent 记录:为 Claude Code 与 Codex CLI 提供
PostToolUse钩子接入,把文件写入、Shell 命令、MCP 调用写入同一条记录链。 - 买方侧核对:
halo door可读取买家自有平台日志(如 GitHub 安全日志、Salesforce Event Monitoring 导出)为外部 Agent 生成报告页。 - 可选的校验块与权限快照:记录可携带守门策略的判定结果(allowed/blocked/modified/unverified)以及会话权威快照。README 明确说明该判定块记录的是“守门方报告了什么”,不等于独立验证。
实际应用场景
适用场景
- 客户安全团队询问“你们的 Agent 对我们的数据做了什么”时,提供一个可自行校验的报告链接。
- 应对安全问卷、供应商评审以及与 ISO 42001、EU AI Act 记录保存条款相关的材料需求(README 明确其为证据层,不构成认证)。
- 开发者在本地或评测环境中记录编码 Agent 的工具行为,便于事后复核。
- 需要向外部交付记录完整性证明的团队,配合外部见证检查点使用。
安装与接入
README 给出的官方入口是 PyPI 包 halo-record,并强调零运行时依赖、仅用标准库。
快速体验(README 原文,需 uv):
uvx --from halo-record halo demo --serve
经典方式:
pip install halo-record
halo demo --serve
在 Agent 入口处包装一行即可开始记录,单个工具边界可用 record_call() 或框架适配器捕获;不传 log= 时记录写入 ~/.halo/<agent>.jsonl(每个 agent 一条链)。渲染报告与本地服务的命令为:
halo report audit.jsonl -o report.html # one chain -> HTML that re-verifies in the browser
halo serve ./records --port 8721 # all tenants, gated per customer
Claude Code 与 Codex CLI 通过各自设置文件中的 PostToolUse 钩子调用 halo hook 接入,记录默认落在 ~/.halo/audit.jsonl(可用 $HALO_LOG 覆盖)。具体 JSON 配置请按 README 原文填写,本页不复述完整配置。
注意:PyPI 上 halo 这个名称属于一个无关的终端加载动画包,若已安装该包会抢占 import halo,因此示例统一使用 halo_record。另外一条链同时只能有一个写者,直接写链文件的程序需自行持有排他锁或分进程写链。
使用条件与限制
- 是否需要部署
- 需自行准备运行环境,具体要求见正文
- 是否需要注册
- 账号和认证要求见正文;未明确说明的项目仍待核验
- 是否需要 API Key
- 待核验
- 费用与额度
- 费用边界见正文,项目许可不代表模型或托管服务免费
- 所需权限
- 待核验
- 许可证与服务条款
- 待核验
权限费用与限制
- 记录失败不阻断动作:记录在动作执行后写入;写记录失败时默认原样返回动作结果并输出警告,可选严格模式(
strict_recording=True或HALO_STRICT_RECORDING=1)抛错,但此时动作已执行,调用方若重试会重复副作用。 - 网络调用:README 声明除四项需主动触发的操作(锚定见证方、读取见证方检查点、RFC 3161 时间戳、
halo serve --verify-email)外不做网络调用,上述操作均默认关闭。 - 数据内容:工具参数以规范化哈希加脱敏摘要保存,摘要上限 200 字符;脱敏为尽力而为的正则匹配,姓名、邮寄地址等无可靠模式的内容不会被识别或遮蔽;链只追加,写入后无法删除,且目前没有内置保留或清理机制,需用可删除的映射表设计来应对删除请求。
- 位置风险:写入 Agent 自身可编辑的设置文件,只适用于不刻意规避记录的评测或开发者本机;需要对方采信的场合,应把记录器放在 Agent 触及不到的位置。
- 费用:README 未给出任何价格;托管见证方以“早期访问”形式提及,具体费用与商务条款需向官方核验。默认时间戳机构 freetsa.org 仅被说明为适合评估,生产环境建议换用受信任的 TSA。
- 项目未声明已归档或停止维护(截至所给资料)。
国内使用情况
网友核验 用户经验分享
与站长实测独立,不代表官方认证或本站已实测。
最近核验:暂无有效核验。参与人数按当前项目有效身份去重;不同核验项分别统计,不合并成用户总结果。仅统计审核通过且未过期的记录。
分项统计、有效期与样本量
| 核验项目 | 有效人数 | 成功 / 部分 / 失败 / 无法判断 | 成功率 | 最近核验 / 有效期 |
|---|---|---|---|---|
| 官方网站访问 | 0 | 0 / 0 / 0 / 0 | 样本不足 | 暂无 / 7 天 |
| GitHub 仓库访问 | 0 | 0 / 0 / 0 / 0 | 样本不足 | 暂无 / 7 天 |
| 安装与运行 | 0 | 0 / 0 / 0 / 0 | 样本不足 | 暂无 / 30 天 |
| 官方服务国内连接 / MCP 连接 | 0 | 0 / 0 / 0 / 0 | 样本不足 | 暂无 / 30 天 |
| 国内网络情况 | 0 | 0 / 0 / 0 / 0 | 样本不足 | 暂无 / 7 天 |
| 国内模型兼容性 | 0 | 0 / 0 / 0 / 0 | 样本不足 | 暂无 / 30 天 |
| 自定义 API | 0 | 0 / 0 / 0 / 0 | 样本不足 | 暂无 / 30 天 |
| 海外服务依赖 | 0 | 0 / 0 / 0 / 0 | 样本不足 | 暂无 / 30 天 |
| 海外账号要求 | 0 | 0 / 0 / 0 / 0 | 样本不足 | 暂无 / 30 天 |
| 中文界面 | 0 | 0 / 0 / 0 / 0 | 样本不足 | 暂无 / 30 天 |
| 客户端兼容性 | 0 | 0 / 0 / 0 / 0 | 样本不足 | 暂无 / 30 天 |
| 账号及费用信息 | 0 | 0 / 0 / 0 / 0 | 样本不足 | 暂无 / 30 天 |
成功率 = 成功 ÷(成功 + 部分成功 + 失败);无法判断不计入分母。至少 5 份可判断结果才显示成功率。账号、费用、海外依赖与语言等事实项,成功表示核查结论与资料吻合,具体观察另行统计;不表示免费或没有海外依赖。不同网络、客户端和版本的结果不能直接等同。
最近有效核验
暂无审核通过且未过期的网友核验。欢迎提交真实使用记录。
查看全部有效核验 →国内实测状态:国内可用性待核验。协议兼容和官方资料说明不等于国内网络可用。
- 官方网站访问
- 尚未实测 待核验
- GitHub 仓库访问
- 尚未实测 待核验
- 官方服务国内连接
- 尚未实测 待核验
- 国内网络情况
- 尚未实测 待核验
- 国内模型兼容性
- 尚未实测 待核验
- 自定义 API
- 尚未实测 待核验
- 海外服务依赖
- 尚未实测 待核验
- 海外账号要求
- 尚未实测 待核验
- 中文界面
- 尚未实测 待核验
- 国内最后核验时间
- 尚未实测
许可与国内使用
仓库 LICENSE 为 Apache License 2.0(Copyright 2026 Brian Kuan),适用于本仓库代码;记录格式本身 README 称开放且可自由实现。第三方时间戳机构、托管见证服务、SMTP 邮件服务等不属于该许可证范围,需另行核验其条款与费用。
中国大陆网络环境下能否稳定访问 PyPI、freetsa.org、GitHub Pages 示例页及邮件服务,资料未作说明,需实际核验。README 未声明对国内模型或国内云平台的专门适配;它支持从 OpenTelemetry、网关日志等通用路径接入,但具体云厂商兼容性待核验。
另需注意:所给 README 文本在“Where this sits in a compliance stack”一节末尾被截断,该节后续内容与任何收费/商业模块说明可能不完整,应以原仓库为准核验。
项目资料与来源
字段依据与待核验事项
access:来源确认 · pip install halo-record
project_type:来源确认 · Tamper-evident **audit trails for AI agents** — hash-chained Runtime Records, rendered as a Runtime Report your customers can check themselves.
原始 README(折叠技术参考)
# halo-record
Tamper-evident **audit trails for AI agents** — hash-chained Runtime Records, rendered as a Runtime Report your customers can check themselves.
Every action your instrumentation captures (tool calls, model calls, data access, approvals) becomes one **Runtime Record** in an append-only, hash-chained log; the **Runtime Report** is that chain rendered as an HTML page that re-computes the hash chain in the reader's browser and renders what it displays from the records it verified (for a check that does not rely on the page's own code, run `halo verify` on the JSONL). Any party holding a checkpoint of the chain can verify that the records up to that checkpoint have not been altered since it was taken, without trusting whoever produced them — that checkpoint is the load-bearing piece: the chain alone is tamper-evident against everyone except the party operating the recorder ([LIMITS.md §1](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md)). 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 increasingly those questions come from ISO 42001, the EU AI Act's record-keeping articles, and customers' own questionnaires. Today a written assurance still passes. The bet behind this project is that it won't for long.
> "Twenty years in forensics teaches you that evidence is only evidence if the other side can check it... in a field full of AI products asking for trust, [Halo] is the rare one designed to make trust unnecessary."
>
> Johannes Stillig, Founder & CEO of Tidebreak AI and Group CISO at a private investment office
Featured in [Help Net Security](https://www.helpnetsecurity.com/2026/08/31/halo-record-open-source-ai-agent-audit-trail/) · Top 4% of Show HN launches that week · Listed on [Agentic Trust Controls](https://trustcontrols.ai/vendor/halo) against five controls
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?](https://github.com/bkuan001/halo-record/discussions/7)
## Check it yourself
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 four opt-in ones — anchoring to a witness (sends the subject id, a record count, and two chain fingerprints — the head and the chain root), reading a witness's checkpoints back (sends the subject id), the RFC 3161 timestamp (sends only a checkpoint's state hash to a Timestamp Authority), and `halo serve --verify-email` (sends a one-time code and the report's subject name, by SMTP through the server you configure in `$HALO_SMTP_HOST`, to the address a report viewer enters). All are off unless you invoke them; no other record contents leave your infrastructure.
- **Raw tool arguments are hashed, with a redacted summary alongside.** Arguments are stored as a canonical hash plus a summary: the argument text with known secret and PII patterns masked, capped at 200 characters. A short input that matches no pattern appears in the summary in full; hash-only mode (`summaries=False`) keeps no summary at all. 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. Outcome fields you supply beyond `summary` are scanned and masked with the same patterns, but not cut to 200 characters (LIMITS §13).
- **Small enough to audit.** ~8,700 lines of Python, including the report page's embedded JavaScript and CSS (code lines, not counting blanks, comments, and docstrings). Read all of it in a day.
- **Apache-2.0.**
- **The paperwork is first-class.** [LIMITS.md](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md) (what the chain can't prove), [PRIVACY.md](https://github.com/bkuan001/halo-record/blob/main/PRIVACY.md) (what records contain and what leaves your machine), [RETENTION.md](https://github.com/bkuan001/halo-record/blob/main/RETENTION.md) (operating under a retention policy), and [REVIEWERS.md](https://github.com/bkuan001/halo-record/blob/main/REVIEWERS.md) — the four-command independent check plus a citation format for review findings.
What each layer proves — the load-bearing distinction in this project ([LIMITS.md §1](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md)): a chain you hold yourself proves records were not **edited**, relative to a head someone already holds; only checkpoints held outside the operator prove none were **removed**; and no hash proves every action was **captured**.
| 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 |
**See one before you install:** [a sample Runtime Report](https://bkuan001.github.io/halo-record/sample-report.html) — fictional data, real chain, and its script re-computes the embedded records' hash chain in your browser when you open it.
## 60-second demo
No agent required. With [uv](https://docs.astral.sh/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 (with a local witness file standing in for one outside the operator — see [LIMITS.md §1](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md)), 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:
```python
from halo_record import trace
agent = trace(run_my_agent, profile="my-agent", log="audit.jsonl") # wraps your entrypoint; records the run boundary to ./audit.jsonl — add record_call() or a framework adapter at each tool boundary to capture individual calls
```
A `from halo import ...` convenience shim also ships — but the `halo` name on PyPI belongs to an unrelated terminal-spinner package, and if that package is installed it wins the import. `halo_record` is unambiguous, so the examples use it.
Without `log=`, records go to `~/.halo/my-agent.jsonl` (one chain per agent). The wrapper seals the run boundary; the evidence lives in the per-call records. Capture those with a framework adapter (matrix below) — or explicitly, which also shows how delegation links:
```python
from halo_record import Recorder, record_call
rec = Recorder("audit.jsonl")
with record_call(rec, "crm.lookup", {"account": "acct-9"}) as call: # one sealed record per tool call
call.result = crm.lookup("acct-9")
with record_call(rec, "payments.refund", {"amount": 120},
parent_id=rec.last_record_id()) as call: # child links to the action that spawned it
call.result = payments.refund(120)
```
Then render the report:
```
halo report audit.jsonl -o report.html # one chain -> HTML that re-verifies in the browser
halo serve ./records --port 8721 # all tenants, gated per customer
```
Upgrading a directory first served by 0.2.45? `halo serve` re-reads each earlier grant at startup. A grant on a file whose records carry no subject id (a plain `Recorder` file such as `agent.jsonl`) is quarantined and printed at every startup with the `halo grant` command that re-issues it — see [LIMITS.md §17, "Serving a directory first served by 0.2.45"](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md#17-one-chain-file-per-subject).
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.
### The verification block
If a guardrail or policy layer checked the action, its verdict can ride on the record — an optional block recording what the gate decided, sealed into the hash chain like every other field:
```python
from halo_record import build
build("tool_call", "security", tool="payments.refund",
verification={"status": "allowed", "verifier": "gate/1.2",
"policy_ref": "sha256:1f3a...",
"checked_at": "2026-08-01T12:00:00Z"})
```
One naming note: the package exports a `record` *function* (the decorator), which shadows the `halo_record.record` *module* on the package object. Import from the module path directly when you want its internals — `from halo_record.record import build` — rather than `import halo_record.record as record`.
which seals into the record as:
```json
"verification": {"status": "allowed", "verifier": "gate/1.2", "policy_ref": "sha256:1f3a...", "checked_at": "2026-08-01T12:00:00Z"}
```
`record_call(...)` accepts the same `verification=` keyword. `status` is required within the block; `verifier`, `policy_ref`, and `checked_at` are optional. What each status means:
| Status | What the gate reports | Did the action execute? |
|---|---|---|
| `allowed` | it permitted the action | yes — the action proceeded |
| `blocked` | it denied the action | determined by the integration, not by this field — a record may still carry an outcome, and a block does not by itself prove non-execution |
| `modified` | it altered the action before execution — `action.input` describes the action **as executed**, post-modification | yes, in the altered form |
| `unverified` | it ran (or was consulted) but made no determination — distinct from an absent block, which means no verification claim was made at all | yes — the action proceeded without a verdict |
The block is supplied by the operator's integration code and records what it reports the gate said — the same trust posture as `principal` (see [LIMITS](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md#11-verification-status-is-the-gates-report-not-halos-finding)). Sealing proves the status was not edited after the fact; it does not prove the check occurred, that the verdict was correct, or that a blocked action did not execute. This is not independent verification.
For `policy_ref` to be usable as evidence, use a content hash of the ruleset and retain the ruleset artifact — an unresolvable label makes the field decorative.
## Connect to what you already run
| Captured at the boundary | Ingested from existing telemetry |
|---|---|
| Native recorder (`from halo_record import trace`) | OpenTelemetry GenAI spans |
| MCP interceptor | LiteLLM callbacks |
| LangChain / LangGraph callback | Langfuse export |
| OpenAI Agents SDK hooks | Any gateway / reverse-proxy log |
| Claude Agent SDK hook | Claude Code and Codex CLI `PostToolUse` hooks (fire after the tool ran) |
| | Platform log exports read buyer-side by `halo door` (GitHub security log, Salesforce Event Monitoring) |
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:
```python
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
```
For MCP, one call wraps the client session — and then *any* MCP-using agent emits records for every tool call, regardless of which framework drives it:
```python
from mcp import Client # mcp 2.x
from halo_record import Recorder
from halo_record.integrations.mcp import instrument_client_session
async def run(server): # an MCPServer, a server URL, or stdio parameters
async with Client(server) as client:
instrument_client_session(client, Recorder("audit.jsonl"), server="stripe")
await client.call_tool("charge", {"customer": "cus_123", "amount": 500}) # recorded
```
Every `call_tool()` on the wrapped client is recorded, and a tool that returns an error result is sealed as `error` (the adapter reads both `isError` and the mcp 2.x `is_error`). On mcp 1.x, pass the initialized `ClientSession` instead of `Client`; the wrapper is the same.
For gateway or proxy logs (Cloudflare AI Gateway, Portkey, nginx in front of the model), map a log row into the chain — explicitly tagged as ingested, not boundary-captured:
```python
from halo_record.integrations.gateway import record_log
record_log(Recorder("audit.jsonl"), {"tool": "gen_ai:gpt-4o", "model": "gpt-4o", "status": 200, "subject": "acme-corp"})
```
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](https://github.com/bkuan001/halo-record-ts) 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 or Codex)
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:
```json
{
"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.
That settings file is one the agent's own tool calls can edit, so this placement records an agent that is not trying to avoid being recorded; it fits evaluation and a developer's own machine. For evidence a counterparty will rely on, put the recorder where the agent cannot reach it: managed settings the agent's account cannot write, a gateway its traffic must pass through, or the platform's own log sealed with `halo door` (reconciled against the chain for GitHub exports; shown without comparison for other sources). [LIMITS.md §4](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md) ranks the three placements by who can switch the recorder off.
Codex CLI ships the same lifecycle hooks with the same event shape (hooks are on by default). Add this to `~/.codex/hooks.json` and Codex's shell commands, `apply_patch` edits, and MCP calls land in the same chain:
```json
{
"hooks": {
"PostToolUse": [
{"matcher": ".*", "hooks": [{"type": "command", "command": "halo hook"}]}
]
}
}
```
The hook tells the two apart from the event itself (Codex adds `turn_id` and `model`) and labels each record `claude-code` or `codex`; set `HALO_HOOK_AGENT` to force one. Both are the ingested tier: a `PostToolUse` hook fires after the tool ran, so the record is built from what the harness reports.
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 — known secret and personal-data formats are masked at seal time, but hashes and refs pass through untouched and free-form text is not detected (see LIMITS §6). Reuse a `snapshot_id` only while the underlying authority is unchanged; consecutive records with the same id and unchanged content are compacted — a reused id over changed content is stored in full, with a notice.
```json
{
"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"]
}
```
```sh
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. (Compaction is per recorder process: hook-style capture spawning one process per tool call re-stores the full body whenever the previous full snapshot isn't the tail record, so short-lived processes trade chain size for the reuse guard.)
SDK users attach the same block directly — `build(..., authority={...})` or `record_call(..., authority={...})`; hash-only capture is the same surface (`summaries=False` on any of them):
```python
from halo_record import Recorder, record_call
rec = Recorder("audit.jsonl")
with record_call(rec, "crm.lookup", {"account": "acct-9"},
authority={"snapshot_id": "auth_1", "rules_hash": "sha256:..."},
summaries=False) as call: # hash-only: no summaries, no excerpts
call.result = crm.lookup("acct-9")
```
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.
One chain, one writer at a time. A chain is a linked list: two writers that read the same head and both append will fork it (two records claiming the same predecessor), and verification will name the affected records. `Recorder` serializes its own appends with a sidecar lock (POSIX `flock` here; a lock directory in the TypeScript package), and `halo hook` appends through `Recorder`, so the hook setup above is covered. Anything that writes the chain file directly — a hand-rolled hook, parallel workers, a log shipper — must hold an equivalent exclusive lock across the read-head-then-append sequence, or write to per-process chains. [LIMITS.md section 9](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md#9-single-writer-chains) covers this in full, including the cross-language boundary.
## Buyer-side door
Everything above is the vendor's record of its own agent. `halo door` is the other side: the buyer's own platform log, sealed and read for one outside agent, without the vendor's help. A GitHub account security log or a Salesforce Event Monitoring export (EventLogFile CSVs) becomes an ingested-tier chain; one page names what that agent did, which rules it broke, how it signed in, where it reached, and what the export cannot show.
```bash
halo door ./export # list the clients acting in the export
halo door ./export --agent "app:<client>" --allow-ip <cidr> --out door.html --chain door.jsonl
halo door --sample --agent "app:bot:0XxSAMPLE00000001A" # the bundled synthetic Agentforce sample
```
`--allow-ip` names the address ranges the integration may sign in from; without it the sign-in rule is not evaluated and the page says so (the bundled sample carries its own range, so `--sample` shows the rule firing). `--rules` takes a bundled pack (`github-default`, `salesforce-default`, `salesforce-agentforce`) or your own file; `--own` lines the rows up against the agent's own Halo log where the source supports it (GitHub today). The page embeds the whole sealed chain and re-verifies it in the browser; `halo verify door.jsonl` checks it without the page.
## When recording fails
Records are written after the action runs. Halo does not block an action when its record cannot be written; writing the record before the action is not offered.
- **Default: the action's result passes through.** If a record cannot be written (disk full, permissions, a lock timeout), `trace()`, `record_call`, `@record`, the framework adapters (OpenAI Agents, LangChain, LiteLLM, OpenTelemetry), the MCP wrapper and `halo hook` return the action's own result unchanged, or re-raise the action's own exception if it failed. A warning on stderr names the missing record: `halo-record: FAILED to record '<tool>' (<error>); the action ran, but its record is NOT in the evidence log. ...` (with no usable stderr, it goes to the `halo_record` logger). The LangChain handler and the OTel exporter also count losses (`lost_records`). Nothing in the chain can show a record that was never written, so the remaining records still verify. A reviewer watching the witness checkpoint cadence sees a chain that has stalled; a single missing record between later ones shows only in the stderr line.
- **Strict mode raises as well.** Pass `strict_recording=True` (to `trace`, `record_call`, `@record`, `instrument_client_session`, or an adapter's constructor), or set `HALO_STRICT_RECORDING=1`, and the same stderr line is written, then the recording error is raised. It reaches the caller from `trace`, `record_call`, `@record`, the MCP wrapper, the OpenAI Agents hooks and the LangChain handler (which sets LangChain's `raise_error`). It cannot reach the caller from the OTel exporter or the LiteLLM logger: those frameworks catch and log what an exporter or callback raises, so there the stderr line is the signal in both modes (LIMITS.md section 18). The action has already run when the error is raised, so a caller that treats it as "the action failed" and retries will repeat the action's side effects (a refund issued twice). Use strict mode only where a missing record is worse than that risk.
- `record_call` and `@record` with no recorder (none passed, none bound by `trace`) raise `RuntimeError` before the wrapped code runs.
- `@record` and `trace()` accept `async def` functions. The wrapper is itself `async def`: it awaits the function, then records what it returned or raised (a cancellation is recorded as an error). `trace()` keeps its recorder bound while the body is awaited, so a `record_call` or `@record` tool inside an async agent lands on the traced chain, and concurrent agents each keep their own. A plain function that returns an awaitable is recorded when it returns. An async generator function is recorded when it is called, with the generator object as its result; what it yields is not recorded.
## 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 checkpoints of the chain — the subject id, a record count, and two chain fingerprints (the head and the chain root); the exact payload, and 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
```
For *time* specifically, an external RFC 3161 timestamp replaces the checkpoint's self-asserted clock with a proof from a Timestamp Authority outside the operator's control — "this chain reached this head no later than T", verifiable by a third party with no hosted infrastructure. The default TSA is the free freetsa.org (fine for evaluation); for production, point `--tsa` at a TSA outside the operator's control (e.g. DigiCert / Sectigo, or one a relying party designates):
```
halo anchor audit.jsonl witness.jsonl --timestamp # attach a TSA time proof to the checkpoint
halo anchor audit.jsonl witness.jsonl --check # reads the token's claimed time
```
`--check` confirms the token binds this chain state and reads its attested time, but it does **not** validate the TSA's signature — that is deliberately left to a standard tool so a reviewer trusts no code of ours. To verify the time independently (this is what you hand a security reviewer):
```
# tsa.token_b64 lives in the witness log; decode the latest one to a standard .tsr file
python3 -c 'import json,base64; cps=[json.loads(l) for l in open("witness.jsonl") if l.strip()]; t=[c["tsa"] for c in cps if c.get("tsa")][-1]; open("token.tsr","wb").write(base64.b64decode(t["token_b64"])); print(t["digest"])'
curl -s -o tsa-ca.pem https://freetsa.org/files/cacert.pem # CA for the default TSA (a commercial TSA publishes its own)
openssl ts -verify -digest <the digest printed above> -in token.tsr -CAfile tsa-ca.pem # → "Verification: OK"
```
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. The claims table under “Check it yourself” at the top of this page is the summary of these three layers.
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: brian@briankuan.com.
## Personal data in the chain
The chain is append-only: anything sealed into a record stays there, because
removing it would break verification for everything after it. Tool arguments are
already handled — stored as a hash plus a redacted summary capped at 200 characters
(hash-only mode keeps no summary; in id fields it masks a token in a known secret
format as in summaries and replaces any other secret-shaped token with `sha256:`
plus 16 hex digits — all but `subject.id`, which routes the record to its tenant's chain).
Note the limit in that sentence: *redacted*, not removed. [LIMITS.md](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md)
section 6 is explicit that a name or a postal address has no reliable pattern, so
neither is detected and neither is masked. And `subject` is not the only field that
carries text you supply — `principal`, `approver`, `session_id`, `agent`,
`authority`, `data` and the summaries all do.
The pattern that works: put a stable pseudonymous id in the chain and hold the
mapping to any individual in a system you can delete from. An erasure request is
then satisfied by deleting the mapping. Keep `subject` pointing at the tenant
organization, not a person:
```python
from halo_record import build
build("tool_call", "privacy", subject={"id": "acme", "name": "Acme Corp"})
```
No setting enforces this — it is a discipline in how you call the recorder.
It makes erasure tractable; it is not anonymization, and there is no built-in
retention or pruning yet.
[LIMITS.md section 13](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md#13-personal-data-and-erasure) has the full field
list, explains why the stored input fingerprint can confirm a guessable value even
after the mapping is gone, and ends with questions a reviewer should ask.
Record a model call (the buyer's first question: "which model saw my data?"):
```python
from halo_record import Recorder, record_model_call
rec = Recorder("audit.jsonl")
record_model_call(rec, provider="anthropic", model="claude-sonnet-4-6",
zdr=True, purpose="draft support reply",
subject="acme") # tool=model.generate, scope=model:anthropic
```
The model is also sealed as `agent.model` and the retention terms as `data.zdr`,
so the export's model column and policy rules can read them, and both survive
`summaries=False`. Pass `messages=` and the record's input carries how many there
were — a count, never their content.
## 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. One scope note that governs every bullet below: these are integrity claims about the record; completeness against the operator requires an external witness holding checkpoints ([LIMITS.md §1](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md)).
- **Security questionnaires and vendor reviews:** answer the AI-agent questions with a verifiable Runtime Report instead of screenshots and prose.
- **AIUC-1:** produces the tamper-evident logging evidence (E015.4) and execution-chain records with authorization events (E015.2 — with a stated gap: reasoning traces are not captured) that the standard's Accountability control E015 names. E015 itself is mandatory; E015.2 and E015.4 are its supplemental tier: not required to pass, the one a vendor adopts when a customer or regulator asks for it. Once the chain is anchored to a witness the relying party has reason to trust — a witness the operator runs itself does not provide this — that's a continuously witnessed chain rather than one reconstructed at audit time (what entered the chain is still bounded by the capture surface). A control-by-control evidence mapping, including what's deliberately out of scope, is in [`AIUC.md`](https://github.com/bkuan001/halo-record/blob/main/AIUC.md).
- **OWASP Top 10 for Agentic Applications 2026:** eight of the ten threats map to deterministic policy rules over the record, two are marked out of scope with reasons, and the pack ships in the package, runnable by name (`halo policy chain.jsonl owasp-starter`). An approximate community mapping, not an official OWASP artifact. See [`OWASP.md`](https://github.com/bkuan001/halo-record/blob/main/OWASP.md).
- **AARM (CSA):** produces the tamper-evident action receipt AARM specifies — R5, and the sealing half of R6 (identity is sealed into the hash, not cryptographically authenticated). halo-record is the receipt layer; pair it with an enforcement gateway for a full AARM system. See [`AARM.md`](https://github.com/bkuan001/halo-record/blob/main/AARM.md).
- **Agentic Trust Controls:** the runtime records behind the ATC's evidence controls — tamper-evident action logging (RBM-03) and the record half of authority attestation (AID-05; the enforcement half belongs to the gate) in one chained record. See [`ATC.md`](https://github.com/bkuan001/halo-record/blob/main/ATC.md).
- **CSA AI Controls Matrix (AICM) / STAR for AI:** the LOG-domain evidence — audit records generated, sealed against undetected modification, input and output events logged — mapped control-by-control in [`AICM.md`](https://github.com/bkuan001/halo-record/blob/main/AICM.md). CSA's own v1.1 crosswalk ties that domain to AIUC-1 E015.
- **MITRE ATLAS:** the agent-telemetry mitigation (AML.M0024) implemented with an integrity property ATLAS itself doesn't ask for — the log is verifiable by someone outside the operator. See [`ATLAS.md`](https://github.com/bkuan001/halo-record/blob/main/ATLAS.md).
- **EU AI Act / ISO 42001 / NIST AI RMF:** the record-keeping and logging obligations these frameworks describe are the same artifact class — mapped conservatively in [EU-AI-ACT.md](https://github.com/bkuan001/halo-record/blob/main/EU-AI-ACT.md), [ISO42001.md](https://github.com/bkuan001/halo-record/blob/main/ISO42001.md), and [NIST-AI-RMF.md](https://github.com/bkuan001/halo-record/blob/main/NIST-AI-RMF.md).
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`](https://github.com/bkuan001/halo-record/blob/main/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:
```bash
halo export audit.jsonl --from 2026-06-01 --to 2026-06-30 -o evidence.csv
# scope the export to the actions a control covers
halo export audit.jsonl --from 2026-06-01 --to 2026-06-30 --tool email.send --tool db.query -o evidence.csv
# check completeness against a witness, under the customer id you expect
halo export audit.jsonl --from 2026-06-01 --to 2026-06-30 --witness-url https://<witness> --expect-subject acme -o evidence.csv
```
This writes two files for the audit window: the CSV (one row per recorded action, grouped left to right as *when → what happened → who → under what authority → what was flagged → provenance → how to verify*, including a redacted plain-language summary of the call and its result, 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 scope, 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. Narrow the population with `--tool` when a control only covers certain actions; the manifest records the filter, so a scoped export discloses that it is a subset rather than reading as the whole population. 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. With `--witness` / `--witness-url` it also checks completeness: a chain that conflicts with the witness (INCOMPLETE) or carries a subject other than `--expect-subject` is refused, an unreachable witness is refused, and the manifest records the subject ids, the witness, and the completeness status (`NOT_CHECKED` when no witness was named). Record timestamps are the recording host's clock, or the platform's own event time for a chain ingested from a log export; the manifest says which (`chain.ts_source`).
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 an HTML Runtime Report that re-verifies its hash chain in the browser (`halo verify` also checks the record format)
(--from/--to: a date-windowed report covering only the review period;
-w/--witness or --witness-url: completeness checked at generation;
exit 1 INCOMPLETE or refused — chain (or window) does not verify, unexpected
subject, or unreachable witness — 2 local witness file missing or damaged,
3 unwitnessed or an empty chain (a --from/--to window holding no records
exits 0); --expect-subject pins the customer id)
halo policy corroborate a chain against a declarative policy pack
(per-rule pass / violation / evidence-gap; exit 1 violated, 2 refused — invalid
policy or unexpected subject — or non-attestable — the chain does not verify;
the rules are printed under a NON-ATTESTABLE header when the records can be read — 3 nothing in scope;
--from/--to/--expect-subject; bundled packs run by name; --help shows the format)
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, 2 local witness
file missing or damaged, 3 unwitnessed; --remote --check needs no vendor key)
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
(--witness/--witness-url/--expect-subject: completeness in the manifest;
exit 1 refused — chain fails verification, INCOMPLETE, unexpected subject,
or unreachable witness — 2 unreadable chain, local witness file missing or
damaged, or output directory missing — 3 empty chain or unwitnessed; a
--from/--to window holding no records exits 0)
halo door buyer-side page of what one outside agent did, from a platform log export you
already hold (GitHub security log, or Salesforce Event Monitoring CSVs; --sample
runs the bundled synthetic export; exit 2 unreadable input, 3 agent not in the export)
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](https://github.com/bkuan001/halo-record/discussions/2).
Full field reference: [`halo-record.schema.json`](src/halo_record/halo-record.schema.json).
## TypeScript
The same recorder ships for Node: [`halo-record-ts`](https://github.com/bkuan001/halo-record-ts). Same chain format, same witness protocol. Records written in either language verify with either verifier.
## Community Examples
[trail-halo-poc](https://github.com/AmeyParle/trail-halo-poc) — community proof of concept binding a Halo record's principal authority to TRAIL credentials: a reciprocal org–agent binding and org-signed scope grants recorded into a Halo chain, with an adversarial verification suite.
## Contributing
Issues, discussions, and pull requests welcome — see [CONTRIBUTING.md](https://github.com/bkuan001/halo-record/blob/main/CONTRIBUTING.md) for the ground rules (short version: tests required, small PRs, schema changes get discussed first).
## License
Apache-2.0
历史前置条件说明(补充参考)
Python 环境与 pip;演示可用可选工具 uv;按需使用 MCP 2.x(或 1.x 的 ClientSession)、LangChain/LangGraph、OpenAI Agents SDK、Claude Code / Codex CLI 等对应框架;使用外部见证、RFC 3161 时间戳或 --verify-email 时需相应网络与外部服务;默认 TSA freetsa.org 仅为评估用,生产需自选 TSA;托管见证方费用与条款待核验;国内网络可达性与国内模型适配待核验。
项目来源与清单版权许可
https://github.com/bkuan001/halo-record
整理来源:https://github.com/slavakurilyak/awesome-ai-agents
清单版权与许可:
MIT License
Copyright (c) 2024 Slava Kurilyak
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.