# halo-record
防篡改的 **AI 代理审计追踪** —— 哈希链式运行时记录,渲染为运行时报告,你的客户可以自行核验。
你的插桩捕获的每一个操作(工具调用、模型调用、数据访问、审批)都会成为仅追加、哈希链式日志中的一条 **运行时记录**;**运行时报告** 是将该链渲染为一个 HTML 页面,在读者的浏览器中重新计算哈希链,并根据其验证过的记录渲染所显示的内容(对于不依赖页面自身代码的检查,可对 JSONL 运行 `halo verify`)。任何持有链检查点的一方都可以验证截至该检查点的记录自检查点生成以来未被篡改,而无需信任生成记录的一方——该检查点是承重部件:仅凭链本身,除了操作记录器的一方之外,对所有人都是防篡改的([LIMITS.md §1](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md))。当客户的安全团队问“你的代理对我们的数据做了什么?”时,你递给他们一个链接,而不是一段文字。安全审查已经在 SOC 2 检查清单旁边提出 AI 相关问题——而且越来越多的问题来自 ISO 42001、欧盟 AI 法案的记录保存条款,以及客户自己的问卷。如今,一份书面保证仍然能通过。这个项目背后的赌注是,这种情况不会持续太久。
**被 [Help Net Security](https://www.helpnetsecurity.com/2026/08/31/halo-record-open-source-ai-agent-audit-trail/) 报道**(2026 年 8 月)。
记录格式是开放的,可自由实现。本包是参考实现:记录器、验证器、见证客户端和报告服务器。
> **正在使用 halo-record,或正在考虑?** 告诉我你是谁、用途是什么 → [谁在使用 halo-record?](https://github.com/bkuan001/halo-record/discussions/7)
## 自行核验
有人要求你在你的代理中放入一个记录器。你不应盲目相信:
- **零运行时依赖。** 仅使用标准库。`pip install halo-record` 只安装一个包。
- **无网络调用**,除了四个可选调用——锚定到见证方(发送主体 ID、记录数量和两个链指纹——链头和链根),读回见证方的检查点(发送主体 ID),RFC 3161 时间戳(仅将检查点的状态哈希发送给时间戳机构),以及 `halo serve --verify-email`(通过你在 `$HALO_SMTP_HOST` 中配置的服务器,以 SMTP 方式将一次性验证码和报告的主体名称发送到报告查看者输入的地址)。除非你主动调用,否则这些功能均关闭;没有其他记录内容离开你的基础设施。
- **原始工具参数经过哈希处理,并附带脱敏摘要。** 参数存储为规范哈希加摘要:参数文本中已知的密钥和 PII 模式被掩码,上限为 200 个字符。不匹配任何模式的短输入会完整出现在摘要中;仅哈希模式(`summaries=False`)完全不保留摘要。脱敏是尽力而为(对常见密钥和 PII 格式的正则表达式,外加熵兜底):将其视为纵深防御,而非保证。你提供的 `summary` 之外的结果字段会以相同模式扫描和掩码,但不会截断到 200 个字符(LIMITS §13)。
- **小到足以审计。** 约 8,700 行 Python,包括报告页面内嵌的 JavaScript 和 CSS(代码行数,不计空行、注释和文档字符串)。一天内可全部读完。
- **Apache-2.0。**
- **文档是一等公民。** [LIMITS.md](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md)(链无法证明什么)、[PRIVACY.md](https://github.com/bkuan001/halo-record/blob/main/PRIVACY.md)(记录包含什么以及什么会离开你的机器)、[RETENTION.md](https://github.com/bkuan001/halo-record/blob/main/RETENTION.md)(在保留策略下运行),以及 [REVIEWERS.md](https://github.com/bkuan001/halo-record/blob/main/REVIEWERS.md)——四命令独立检查,外加审查发现的引用格式。
每一层证明什么——本项目中的承重区分([LIMITS.md §1](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md)):你自己持有的链证明记录未被**编辑**,相对于某人已持有的链头;只有操作者外部持有的检查点才能证明没有记录被**移除**;没有任何哈希能证明每个操作都被**捕获**。
| 主张 | 自持链 | + 外部检查点 | + 可信捕获 |
|---|---|---|---|
| 检测对已建立产物的编辑 | ✔ | ✔ | ✔ |
| 检测对已提交历史的改写 | — | ✔ | ✔ |
| 检测缺失/延迟的检查点 | — | ✔(约定节奏) | ✔ |
| 证明每个操作都被记录 | — | — | 取决于捕获边界 |
**安装前先看一个:** [示例运行时报告](https://bkuan001.github.io/halo-record/sample-report.html)——虚构数据、真实链,打开时其脚本会在你的浏览器中重新计算内嵌记录的哈希链。
## 60 秒演示
无需代理。使用 [uv](https://docs.astral.sh/uv/),无需安装任何东西:
```
uvx --from halo-record halo demo --serve
```
或经典方式:
```
pip install halo-record
halo demo --serve
```
要么搭建一个虚构的支持代理供应商,包含两个客户,见证这些链(用一个本地见证文件代表操作者之外的一方——参见 [LIMITS.md §1](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md)),提供他们受门控的 Runtime Reports,并在你的浏览器中打开操作员控制台。然后尝试篡改测试:从其中一个 `.jsonl` 文件中删除一行并重新加载。报告会捕获它。
## 记录你自己的代理
在边界处一行:
```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
```
`from halo import ...` 便捷垫片也会随附提供——但 PyPI 上的 `halo` 名称属于一个无关的终端加载动画包,如果该包已安装,它会在导入时胜出。`halo_record` 则没有歧义,因此示例使用它。
如果没有 `log=`,记录会写入 `~/.halo/my-agent.jsonl`(每个 agent 一条链)。包装器封存运行边界;证据存在于每次调用的记录中。使用框架适配器(见下方矩阵)捕获这些记录——或者显式捕获,这也能展示委托链接是如何建立的:
```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)
```
然后渲染报告:
```
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
```
升级一个最初由 0.2.45 提供服务的目录?`halo serve` 会在启动时重新读取每个先前的授权。对某个文件的授权,如果其记录不携带 subject id(例如 `agent.jsonl` 这样的普通 `Recorder` 文件),会被隔离,并在每次启动时打印出来,同时附带重新签发它的 `halo grant` 命令——参见 [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)。
当你在浏览器中看到自己 agent 的 Runtime Report 时,快速入门就结束了。如果你得到了一个 JSONL 文件却没有报告,那说明出了问题:请提交一个 issue。
### 验证块
如果某个护栏或策略层检查了该操作,其裁决可以随记录一起传递——这是一个可选块,记录门控做出的决定,并像其他所有字段一样被封入哈希链:
```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"})
```
一个命名说明:该包导出了一个 `record` *函数*(装饰器),它会在包对象上遮蔽 `halo_record.record` *模块*。当你需要其内部实现时,请直接从模块路径导入——`from halo_record.record import build`——而不是 `import halo_record.record as record`。
其封存进记录后为:
```json
"verification": {"status": "allowed", "verifier": "gate/1.2", "policy_ref": "sha256:1f3a...", "checked_at": "2026-08-01T12:00:00Z"}
```
`record_call(...)` 接受相同的 `verification=` 关键字参数。`status` 在块内是必需的;`verifier`、`policy_ref` 和 `checked_at` 是可选的。每个状态的含义如下:
| 状态 | 门禁报告的内容 | 操作是否执行? |
|---|---|---|
| `allowed` | 它允许了该操作 | 是 — 操作继续进行 |
| `blocked` | 它拒绝了该操作 | 由集成决定,而非此字段 — 记录仍可能携带结果,且阻止本身并不证明未执行 |
| `modified` | 它在执行前修改了该操作 — `action.input` 描述的是**已执行**的操作,即修改后的形式 | 是,以修改后的形式 |
| `unverified` | 它运行了(或被咨询了)但未做出判定 — 与缺失的块不同,后者意味着根本没有做出验证声明 | 是 — 操作在没有裁决的情况下继续进行 |
该块由操作者的集成代码提供,并记录其报告的门禁所述内容 — 与 `principal` 相同的信任姿态(参见 [LIMITS](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md#11-verification-status-is-the-gates-report-not-halos-finding))。密封证明状态在事后未被编辑;它并不证明检查确实发生、裁决正确,或已阻止的操作未执行。这不是独立验证。
要使 `policy_ref` 可用作证据,请使用规则集的内容哈希并保留规则集工件 — 无法解析的标签会使该字段沦为装饰。
## 连接到您已在运行的内容
| 在边界处捕获 | 从现有遥测中摄取 |
|---|---|
| 原生记录器(`from halo_record import trace`) | OpenTelemetry GenAI spans |
| MCP 拦截器 | LiteLLM 回调 |
| LangChain / LangGraph 回调 | Langfuse 导出 |
| OpenAI Agents SDK 钩子 | 任何网关 / 反向代理日志 |
| Claude Agent SDK 钩子 | Claude Code 和 Codex CLI 的 `PostToolUse` 钩子(在工具运行后触发) |
框架适配器和摄取路径会为每条记录打上 `source` 标签,因此报告会披露每条证据是如何收集的。捕获的记录和摄取的记录存在于同一条链中。
对于 LangChain / LangGraph,它是一个回调处理器:
```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
```
对于 MCP,一次调用即可包装客户端会话——随后*任何*使用 MCP 的 agent 都会为每次工具调用发出记录,无论它由哪个框架驱动:
```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
```
对包装客户端上的每次 `call_tool()` 调用都会被记录,返回错误结果的工具会被标记为 `error`(适配器会同时读取 `isError` 和 mcp 2.x 的 `is_error`)。在 mcp 1.x 上,传入已初始化的 `ClientSession` 而不是 `Client`;包装器是相同的。
对于网关或代理日志(Cloudflare AI Gateway、Portkey、模型前面的 nginx),将日志行映射到链中——明确标记为已摄取,而非边界捕获:
```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"})
```
任何发出 OpenTelemetry GenAI span 的内容(CrewAI、LlamaIndex,以及大多数带有 OTel 插桩的 agent 框架)都会通过 OTel 适配器进入链中,而 [TypeScript 包](https://github.com/bkuan001/halo-record-ts) 为 Vercel AI SDK 和 JS agent 生态系统提供了原生适配器。缺少适用于你的技术栈的适配器?提交一个 issue。大多数适配器大约只有一百行代码。
## 记录你的编码 agent(Claude Code 或 Codex)
Claude Code 在每次工具调用后都会触发一个 `PostToolUse` 钩子。将其指向 `halo hook`,每个操作——文件写入、shell 命令、MCP 连接器调用——都会成为本地链中的一条记录。无需修改代码;只需一项设置条目:
```json
{
"hooks": {
"PostToolUse": [
{"matcher": "*", "hooks": [{"type": "command", "command": "halo hook"}]}
]
}
}
```
将其添加到 `~/.claude/settings.json`,记录会写入 `~/.halo/audit.jsonl`(可通过 `$HALO_LOG` 覆盖)。不接触任何数据、网络或外部状态的纯编排工具会被跳过——该链记录的是信任边界操作,而非思考过程。设置 `HALO_HASH_ONLY=1` 可仅记录内容哈希而不记录摘要。设置 `HALO_AGENT_VERSION`(以及可选的 `HALO_AGENT_MODEL`)可将每条记录绑定到产生它的 agent 构建版本——当审计人员询问某个时间窗口内运行的是哪个版本时,导出结果会按列给出答案,而非凭记忆回答。
Codex CLI 提供相同的生命周期钩子,且事件结构相同(钩子默认开启)。将其添加到 `~/.codex/hooks.json`,Codex 的 shell 命令、`apply_patch` 编辑和 MCP 调用就会写入同一条链:
```json
{
"hooks": {
"PostToolUse": [
{"matcher": ".*", "hooks": [{"type": "command", "command": "halo hook"}]}
]
}
}
```
hook 从事件本身区分两者(Codex 会添加 `turn_id` 和 `model`),并将每条记录标记为 `claude-code` 或 `codex`;设置 `HALO_HOOK_AGENT` 可强制指定其一。两者都属于摄取层:`PostToolUse` hook 在工具运行后触发,因此记录是根据 harness 所报告的内容构建的。
如果你需要报告回答“这次运行是在什么规则下发生的?”,请将 `HALO_AUTHORITY_FILE` 设置为该会话有效权限的 JSON 快照。保持隐私安全:使用哈希和引用,而非原始提示、私有策略文本、机密或完整工具 schema——已知的机密和个人数据格式会在封存时被遮蔽,但哈希和引用会原样通过,自由格式文本不会被检测(参见 LIMITS §6)。仅当底层权限未发生变化时才复用 `snapshot_id`;具有相同 id 且内容未变的连续记录会被压缩——若复用的 id 对应内容已变更,则会完整存储,并附带一条通知。
```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
```
快照被密封在与操作记录相同的哈希链中。一个良好的默认设置是在开始时创建一个会话级快照,并在规则、Skills、hooks、MCP 工具注册表或压缩策略发生变化时创建新快照。为保持长会话精简,具有相同 `authority.snapshot_id` 的连续记录在第一个完整快照之后会被压缩:后续记录仅保留 `{"snapshot_id": "...", "same_as_previous": true}`。指针仍保持哈希链式连接,但庞大的 refs/omissions/stale-if 块不会在每个操作上重复。(压缩是按记录器进程进行的:hook 式捕获为每次工具调用生成一个进程,当上一个完整快照不是尾部记录时,会重新存储完整主体,因此短生命周期进程以链大小为代价换取重用保护。)
SDK 用户直接附加相同的块——`build(..., authority={...})` 或 `record_call(..., authority={...})`;仅哈希捕获是相同的接口(在任意一个上设置 `summaries=False`):
```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")
```
然后,照常:
```
halo verify ~/.halo/audit.jsonl
halo report ~/.halo/audit.jsonl -o report.html
```
任何暴露 post-action hook 的 agent 运行时都可以喂入同一条命令——该 hook 从 stdin 读取一个 JSON 事件,并追加一条记录。
一条链,同一时间只有一个写入者。链是一个链表:两个写入者读取同一个 head 并都进行追加,就会使其分叉(两条记录声称拥有同一个前驱),验证过程会指出受影响的记录。`Recorder` 通过 sidecar 锁(此处为 POSIX `flock`;TypeScript 包中为锁目录)串行化自身的追加操作,而 `halo hook` 通过 `Recorder` 进行追加,因此上述 hook 设置已被覆盖。任何直接写入链文件的操作——手写的 hook、并行 worker、日志转发器——都必须在“读取 head 然后追加”这一序列期间持有等效的排他锁,或者写入按进程划分的链。[LIMITS.md 第 9 节](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md#9-single-writer-chains)完整涵盖了这一点,包括跨语言边界。
## 记录失败时
记录在动作运行之后写入。当记录无法写入时,Halo 不会阻止该动作;不提供在动作之前写入记录的选项。
- **默认:动作的结果原样通过。** 如果记录无法写入(磁盘已满、权限问题、锁超时),`trace()`、`record_call`、`@record`、框架适配器(OpenAI Agents、LangChain、LiteLLM、OpenTelemetry)、MCP wrapper 和 `halo hook` 会原样返回动作自身的结果,或者如果动作失败则重新抛出动作自身的异常。stderr 上的警告会指出缺失的记录:`halo-record: FAILED to record '<tool>' (<error>); the action ran, but its record is NOT in the evidence log. ...`(如果没有可用的 stderr,则写入 `halo_record` logger)。LangChain handler 和 OTel exporter 还会统计丢失次数(`lost_records`)。链中无法显示从未写入的记录,因此其余记录仍然可以通过验证。关注 witness checkpoint 节奏的审查者会看到一条已经停滞的链;后续记录之间单条缺失的记录只会体现在 stderr 行中。
- **严格模式也会抛出异常。** 传入 `strict_recording=True`(给 `trace`、`record_call`、`@record`、`instrument_client_session` 或适配器的构造函数),或设置 `HALO_STRICT_RECORDING=1`,会写入相同的 stderr 行,然后抛出记录错误。它会从 `trace`、`record_call`、`@record`、MCP wrapper、OpenAI Agents hooks 和 LangChain handler(后者会设置 LangChain 的 `raise_error`)到达调用者。它无法从 OTel exporter 或 LiteLLM logger 到达调用者:这些框架会捕获并记录 exporter 或 callback 抛出的内容,因此在那里 stderr 行是两种模式下的信号(LIMITS.md 第 18 节)。抛出错误时动作已经运行,因此如果调用者将其视为“动作失败”并重试,就会重复动作的副作用(退款被发放两次)。仅在缺失记录比该风险更严重的情况下使用严格模式。
- 没有 recorder 的 `record_call` 和 `@record`(未传入,也未由 `trace` 绑定)会在被包装的代码运行之前抛出 `RuntimeError`。
- `@record` 和 `trace()` 接受 `async def` 函数。wrapper 本身也是 `async def`:它会 await 该函数,然后记录其返回或抛出的内容(取消会被记录为错误)。`trace()` 在函数体被 await 期间保持其 recorder 绑定,因此异步 agent 内部的 `record_call` 或 `@record` 工具会落到被追踪的链上,并且并发 agent 各自保持自己的链。返回 awaitable 的普通函数会在其返回时被记录。异步生成器函数会在被调用时被记录,其结果就是生成器对象;它 yield 的内容不会被记录。
## 完整性 vs. 完备性(请阅读这一部分)
要准确理解每一层证明了什么——因为它们是不同的主张,而这些差异正是关键所在:
自持有的链证明的是**相对于已确立 head 的完整性**:给定某人已经持有的链 head,其后方记录中的任何编辑、重排序或删除都会变得可检测。就其自身而言——在操作者之外的任何人看到 head 之前——链证明的是内部一致性,而不是历史:操作者可以丢弃一条记录并重新密封,而新文件仍会通过验证。链在其 head 离开操作者控制的那一刻就变得**历史性地已提交**。
这就是 witness:操作者之外的一方持有链的周期性 checkpoint——subject id、记录数量以及两个链指纹(head 和 chain root);确切的有效载荷,仅此而已。Checkpoint 使重写已提交的历史变得可检测,而错过的 checkpoint 本身就是一个可见事件:
```
halo anchor audit.jsonl witness.jsonl # anchor a checkpoint to a local witness
halo anchor audit.jsonl witness.jsonl --check # completeness verdict against it
```
对于*时间*而言,外部 RFC 3161 时间戳用来自运营方控制范围之外的时间戳机构(Timestamp Authority)的证明,取代检查点自我声明的时间——“该链最迟在 T 时刻达到此头部”,可由第三方在无需托管基础设施的情况下验证。默认 TSA 是免费的 freetsa.org(适合评估用途);生产环境中,应将 `--tsa` 指向运营方控制范围之外的 TSA(例如 DigiCert / Sectigo,或依赖方指定的 TSA):
```
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` 确认令牌绑定此链状态并读取其证明时间,但它**不**验证 TSA 的签名——这是有意留给标准工具处理,以便审查者无需信任我们的任何代码。要独立验证时间(这正是你交给安全审查者的内容):
```
# 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"
```
还有一个边界,直白地说:链和见证者都不能证明每一个真实世界的操作都经过了记录器。那是**捕获完整性**——取决于记录器在栈中的位置(原生插桩、钩子、网关摄取),而不是任何哈希。记录带有 `source` 标签正是出于这个原因。本页顶部“自己检查”下的声明表就是这三层的总结。
任何人都可以运行一个见证者。你自己运行的见证者将历史提交给*你*;将其提交给*你的客户*则需要一个他们有理由信任的见证者。无论哪种方式,协议都是开放的。
一个托管的、被认可的见证者是这个项目维持自身的方式。早期访问:[email protected]。
## 链中的个人数据
链是仅追加的:任何被封入记录的内容都会留在那里,因为
删除它会破坏其后所有内容的验证。工具参数已经
处理好了——存储为哈希加上一个被脱敏的摘要,上限为 200 个字符
(仅哈希模式不保留摘要;在 id 字段中,它会像在摘要中一样将已知秘密格式的令牌掩码,并将任何其他秘密形状的令牌替换为 `sha256:` 加上 16 个十六进制数字——除了 `subject.id`,它会把记录路由到其租户的链)。
注意那句话中的限制:*脱敏*,而非移除。[LIMITS.md](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md)
第 6 节明确指出,姓名或邮寄地址没有可靠的模式,因此
两者都不会被检测到,也都不会被掩码。而且 `subject` 并不是唯一携带你提供的文本的字段——`principal`、`approver`、`session_id`、`agent`、
`authority`、`data` 以及所有摘要都携带。
行之有效的模式:在链中放入一个稳定的假名 id,并将
到任何个人的映射保存在一个你可以从中删除的系统中。这样,删除请求
就可以通过删除该映射来满足。让 `subject` 指向租户
组织,而不是个人:
```python
from halo_record import build
build("tool_call", "privacy", subject={"id": "acme", "name": "Acme Corp"})
```
没有任何设置强制这一点——这是你调用记录器时的一种纪律。
它使擦除变得可行;这不是匿名化,而且目前还没有内置的
保留或清理机制。
[LIMITS.md 第 13 节](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md#13-personal-data-and-erasure) 包含完整的字段
列表,解释了为什么即使映射已消失,存储的输入指纹仍能确认一个可猜测的值,
并以审查者应提出的问题作为结尾。
记录一次模型调用(买方的第一个问题:“哪个模型看到了我的数据?”):
```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
```
模型同样被封装为 `agent.model`,保留条款封装为 `data.zdr`,
因此导出的模型列和策略规则可以读取它们,且两者在
`summaries=False` 时依然保留。传入 `messages=` 后,记录的输入会携带消息数量——
只是一个计数,绝不包含其内容。
## 在合规体系中的定位
halo-record 是一个证据层,而非认证。它产出的工件正是评估框架反复以不同措辞索要的东西。有一条范围说明统领以下每一条:这些都是关于记录的完整性声明;要针对运营方实现完整性,需要由外部见证方持有检查点([LIMITS.md §1](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md))。
- **安全问卷和 SOC 2 审查:** 用可验证的 Runtime Report 回答 AI 相关部分,而不是截图和文字描述。
- **AIUC-1:** 产出防篡改日志证据(E015.4)以及带授权事件的执行链记录(E015.2——存在一个已声明的缺口:推理轨迹未被捕获),这正是该标准问责控制 E015 所要求的。E015 本身是强制性的;E015.2 和 E015.4 是其补充层级:并非通过所必需,而是当客户或监管机构要求时供应商才会采纳的部分。一旦链条锚定到依赖方有理由信任的见证方——运营方自己运行的见证方不提供这一点——那就是一条持续受见证的链,而非在审计时才重建的链(进入链的内容仍受捕获面限制)。逐控制的证据映射,包括有意排除在范围之外的内容,见 [`AIUC.md`](https://github.com/bkuan001/halo-record/blob/main/AIUC.md)。
- **OWASP Top 10 for Agentic Applications 2026:** 十项威胁中有八项映射为对记录的确定性策略规则,两项被标记为超出范围并附有原因,且该规则包随包发布,可按名称运行(`halo policy chain.jsonl owasp-starter`)。这是社区近似映射,并非 OWASP 官方工件。见 [`OWASP.md`](https://github.com/bkuan001/halo-record/blob/main/OWASP.md)。
- **AARM (CSA):** 产出 AARM 规定的防篡改操作回执——R5,以及 R6 的封装部分(身份被封装进哈希,而非经过密码学认证)。halo-record 是回执层;将其与执行网关配对以构成完整的 AARM 系统。见 [`AARM.md`](https://github.com/bkuan001/halo-record/blob/main/AARM.md)。
- **Agentic Trust Controls:** ATC 证据控制背后的运行时记录——防篡改操作日志(RBM-03)以及权限证明的记录部分(AID-05;执行部分属于网关)——合并在一条链式记录中。见 [`ATC.md`](https://github.com/bkuan001/halo-record/blob/main/ATC.md)。
- **CSA AI Controls Matrix (AICM) / STAR for AI:** LOG 域证据——生成的审计记录、防未检测修改的封装、输入和输出事件日志——逐控制映射见 [`AICM.md`](https://github.com/bkuan001/halo-record/blob/main/AICM.md)。CSA 自己的 v1.1 对照表将该域与 AIUC-1 E015 关联。
- **MITRE ATLAS:** 智能体遥测缓解措施(AML.M0024)的实现带有 ATLAS 本身并未要求的完整性属性——日志可由运营方之外的某人验证。见 [`ATLAS.md`](https://github.com/bkuan001/halo-record/blob/main/ATLAS.md)。
- **EU AI Act / ISO 42001 / NIST AI RMF:** 这些框架所描述的记录保存和日志义务属于同一类工件——保守映射见 [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) 和 [NIST-AI-RMF.md](https://github.com/bkuan001/halo-record/blob/main/NIST-AI-RMF.md)。
这些本身都不构成任何认证。它只是给你的评估者提供可验证的查看对象。边界——halo-record 有意不做什么,以及当审查者提问时该如何回答——记录在 [`LIMITS.md`](https://github.com/bkuan001/halo-record/blob/main/LIMITS.md) 中。
### 将证据导入你的 GRC 平台
大多数 GRC 平台(Vanta、Drata 及类似平台)接受上传文件作为针对某项控制的自定义证据。halo-record 的导出正是为融入该流程而构建:
```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
```
这会为审计窗口写入两个文件:CSV(每个记录的操作一行,从左到右分组为*何时 → 发生了什么 → 谁 → 依据什么权限 → 标记了什么 → 来源 → 如何验证*,包括对调用及其结果的脱敏通俗语言摘要、产生每次调用的 agent 构建和模型、其代表运行的身份、导致它的记录、其授权决策和范围,以及任何个人数据类别或摄入的威胁标记)和一个清单(`evidence.csv.manifest.json`),将 CSV 与其来源绑定——链的头哈希将其链接到它所来自的可验证日志,而 `csv_sha256` 是导出文件自身的哈希,因此导出后被编辑的 CSV 不再与其清单匹配。当某个控制仅覆盖特定操作时,用 `--tool` 缩小范围;清单会记录该过滤器,因此限定范围的导出会披露它是一个子集,而不会被读作整个总体。将两者上传到你的日志记录或监控控制项;当审查者想要自行验证链时,附上 Runtime Report HTML。该导出拒绝在验证失败的链上运行。使用 `--witness` / `--witness-url` 时,它还会检查完整性:与见证冲突(INCOMPLETE)或携带的主体不是 `--expect-subject` 的链会被拒绝,无法访问的见证会被拒绝,并且清单会记录主体 id、见证以及完整性状态(未指定见证时为 `NOT_CHECKED`)。记录时间戳是记录主机的时钟;清单会说明这一点(`chain.ts_source`)。
---
[Read more](https://github.com/bkuan001/halo-record)