关于 Arcaeon Ledger
Arcaeon Ledger 是用于记录 Agent 操作的哈希链账本,提供追加和验证能力,适合需要检查记录完整性的开发者。它使用单个 JSONL 文件,并能指出链中断裂的位置。
主要功能
- 01追加操作记录
- 02验证哈希链完整性
- 03定位链断裂行
- 04命令行验证与追加
- 05记录操作者及权限信息
适用场景
- 01验证 Agent 操作日志是否被修改
- 02在 CI 或发布前检查账本
- 03将操作者和权限信息纳入记录
MCP 相关
README 未说明该项目与 MCP 的连接方式或 MCP 服务所提供的能力。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
arcaeon-ledger
可观测性工具能展示 Agent 做了什么。arcaeon-ledger 能让你_证明_这一点。
每条记录都会与前一条记录进行哈希链式连接。修改一行、删除一行或
调整历史顺序,都会导致之后的每条链接失效——verify 会指出确切行号。
记录由你掌控,你可以证明它未被篡改。零依赖,一个 JSONL 文件,两个动词。
pip install arcaeon-ledger # then: from arcaeon_ledger import Ledger
from arcaeon_ledger import Ledger
log = Ledger("agent.log.jsonl")
log.append({"tool": "web.search", "query": "weather in LA", "result_ok": True})
log.append({"tool": "payment", "amount": "49.00", "currency": "USD"})
log.verify() # VerifyResult(ok=True, rows=2, chained=2, ...)
篡改会被发现,而不是寄望于它不会发生:
# someone edits row 2's amount in the file by hand...
log.verify() # VerifyResult(ok=False, first_break="line 2: chain mismatch")
CLI(可将其接入 CI 或发版前检查——篡改过的日志会以非零状态码退出;而只能获得部分可信证明的日志,也不再像完全验证通过的日志那样退出):
python -m arcaeon_ledger.cli append agent.log.jsonl '{"tool":"search","ok":true}'
python -m arcaeon_ledger.cli verify agent.log.jsonl
python -m arcaeon_ledger.cli verify --strict agent.log.jsonl
verify 退出码(0.5.7):
| 退出码 | 含义 |
|---|---|
0 | 完全验证通过——已检查每一行,链完整(ok: true) |
1 | 已损坏——发现断链(ok: false),或用法错误 |
3 | 仅在限定范围内验证(所有情况下均为 ok: null):未发现断链,但未链接的 prechain 行被跳过且未验证(verified_scope: "bounded_prechain_skipped");或唯一的断链是已声明的断链("bounded_declared_break");或文件没有任何行("empty")。伪造的“legacy”前置内容会得到此状态码,而不是 0。传入 --strict 可将前两种情况改为硬性错误 1。 |
CI 检查应仅将 0 视为通过:
python -m arcaeon_ledger.cli verify agent.log.jsonl
case $? in
0) echo "fully verified" ;;
3) echo "chain intact but prechain rows skipped unverified — inspect, or use --strict" ; exit 1 ;;
*) echo "ledger broken" ; exit 1 ;;
esac
证明谁执行了操作,而不只是操作顺序
哈希链能证明序列完整性——但无法证明是谁写入了每条记录,也无法证明他们是否有权这样做。附加一个 authority 块,将执行者及其权限范围绑定到链式连接的(防篡改)行中:
from arcaeon_ledger import Ledger, authority
log = Ledger("agent.log.jsonl")
log.append(
{"tool": "payment", "amount": "49.00"},
authority=authority(
"agent://billing-7",
capability_version="v3", # what they were allowed to do
tool_schema={"name": "payment", "args": ["amount"]}, # hashed, not just named
time_source="ntp", # trust surface of the clock
),
)
现在,审计问题从*“这是否被编辑过?”进一步变为“这是否被编辑过,并且写入者是否获得授权?”*——编辑主体、能力或架构哈希,都会像其他篡改一样破坏链。这将防篡改与权限重放结合起来。(根据上线后社区反馈推出。)
为何打造此工具
2026 年 Agent 构建者最迫切却尚未解决的痛点,是可靠性与审计之间的落差:
Agent“完成”了一项任务,但结果悄然出错,而你无法重现——或证明——实际发生了什么。可观测性平台会跟踪运行过程;但没有一个能提供防篡改、可移植、由你掌控的记录。监管要求也正在到来:欧盟《人工智能法案》要求高风险系统在其生命周期内具备自动记录事件的技术能力(第 12(1) 条),并要求提供者和部署者在其控制范围内至少保存这些日志六个月(第 19(1) 条、第 26(6) 条)。该法案规定了记录和留存要求——防篡改并非它使用的措辞,而是我们提出的要求:当有人询问留存的日志是否仍是原始日志时,答案必须比信任更有力。arcaeon-ledger 提供了最精简、诚实的方案:一份加密链式操作日志,可直接接入、由你掌控并验证。
链的工作原理
chain = sha256(prev_chain + json.dumps(row_without_chain, sort_keys=True, ensure_ascii=False))[:32]
请注意分隔符:链主体使用 Python 默认的 ", " / ": " 间距,而不是制品摘要采用的紧凑 json-c14n:v1 格式。跨语言验证器必须精确复现这种间距。
链值采用**truncated_sha256_128**——SHA-256 的前 32 个十六进制字符(128 位),而非完整摘要。此命名是为了避免有人将它称为完整的 SHA-256:128 位足以检测编辑或意外更改;但如果你希望重写后让链本身难以被暴力破解,它就略显不足(感谢 atomic-raven 的评审)。
每行都会对之前的完整历史作出承诺。第一行从固定的 "genesis" 种子开始链接。只有在第一条链式记录之前,才允许存在没有 chain 字段的行(方便你将其用于现有日志);链开始后出现的未链接行本身会被标记。发生不匹配时,verify 会继续使用记录中声称的值进行验证,从而如实统计之后的损坏,而不是让一个断链级联成一堆噪声。
它能证明什么——以及它不能证明的五件事
在这里,准确表述本身就是产品,而不是免责声明。哈希链能证明每一行记录的内容在写入后没有被就地篡改:修改文件中间部分、删除和重排都会破坏哈希链,而 verify 会指出对应的行。
0.5.8 中,这句话里有一个词发生了变化,原因正是本节存在的意义。以前写的是“记录的字节”,这比哈希链实际能证明的更多。哈希链是基于从文件中解析回来的每一行计算的,而读取器会规范化无法解码的字节序列——因此,此类区域内的两种不同字节串会被读取为相同内容,并产生相同的验证结果。受保护的是每一行的含义,而不是文件的精确字节内容。如果你需要字节级保全,请同时对文件本身进行哈希。
它不能单独证明另外五件事:
1. 截断。 去掉最新的几行,剩余内容仍能通过验证——仅靠只追加的哈希链无法发现这一点。可以通过定期将链头发布到你无法控制的地方来弥补:
pin = log.head().as_pin()
# -> "arcaeon-ledger head chain=9f3c… rows=204 as_of=2026-08-13T17:40:00Z"
# post `pin` to a git commit / public comment / notarization anchor.
# a reader compares a fresh head() against the last pin; a truncated or
# re-minted history disagrees. the MAX gap between pins is your security
# parameter, not the average — an attacker picks the gap.
2. 真实性。 哈希链会为写入的任何内容作公证——带有防篡改记录的幻觉,仍然是带校验和的幻觉。若要让一行记录能证明现实情况,请对可重新获取的凭证(URL+字节、快照、工具标准输出)进行哈希,并将摘要存入该行,这样第三方就可以重新获取并进行比对。
3. 署名。 authority()(见上文)记录了谁声称了什么,但它只是行中的数据,不是签名——从创世记录重新铸造哈希链的人,也会一并重新铸造它。外部锚定链头(#1)才是重新铸造者无法推进的部分。
4. 伪造的旧记录前置。 第一条带链记录之前没有 chain 字段的行会被容许——这是有意设计的,这样你就能在现有日志之上启用哈希链,而无需改写历史记录。但跳过的行是未经验证的行,验证器无法区分真实的旧记录历史与伪造的前置记录。因此(0.5.7 起),非严格验证如果跳过了任何行,就绝不会给出绿色结果:ok 为 None——表示“未发现破坏,在限定范围内已验证”——这是一个假值;范围会以内嵌字段的形式标明(verified_scope: "bounded_prechain_skipped"),跳过数量则记录在 prechain 中;CLI 的退出码为 3,而不是 0。只有检查了每一行的扫描才会返回 ok=True。如果你的日志从创世记录起就启用了哈希链,且不应存在任何合理的旧记录行,请传入 verify(strict=True) / --strict——这会将任何未链接的行视为破坏,直接标记为红色。(在哈希链开始后插入的未链接行,在任何模式下都会被标记。)
5. 完整性。 这是最重要的一点,而且属于结构性问题:由 Agent 决定调用 append 记录什么。防篡改日志记录的只是 Agent 选择报告的调用,它仍然是自我报告。这个库内的任何机制都无法解决这一点,因为 Agent 可以拒绝调用自己能调用的任何东西。
要解决这个问题,就得把记录权从 Agent 手中移开——改为在交界处记录,由独立的 OS 进程完成;Agent 不拥有该进程,无法跳过它,也看不到它:
pip install arcaeon-adapter
python -m arcaeon_adapter --ledger seam.log.jsonl -- <your mcp server command...>
arcaeon-adapter 是一个 stdio 代理,在 MCP 客户端与服务端之间逐字节转发 JSON-RPC,并针对每次 tools/call 向自己的账本写入一条带哈希链的记录。将它包裹在这个库自己的 MCP 服务外部进行测试,得到了最能说明问题的数字:服务自己的日志记下了 0 条,而交界处的日志捕获了 5 条。系统自我报告的内容与交界处实际观察到的内容之间的差距,正是值得衡量的东西。
准确限定其作用后,这个基础机制可以表述为*“这个文件没有被就地改写”*——范围有限、真实可信且可测试。更上一层的机制(通过 head() 进行外部锚定、绑定凭证、签署署名、在交界处记录)则能帮助你将其扩展为完整的证据声明。
缺失或为空的账本上的 verify()
这两种情况看起来一样(“没有数据”),而 verify() 会有意将它们区分开来:
Ledger("never/written.jsonl").verify()
# VerifyResult(ok=False, rows=0, first_break="unreadable: [Errno 2] No such file...")
open("touched/empty.jsonl", "w").close()
Ledger("touched/empty.jsonl").verify()
# VerifyResult(ok=None, rows=0, chained=0, first_break=None, verified_scope="empty")
从未创建过的路径无法提供可信保证——ok=False,状态为“unreadable”,与其他读取失败相同。路径存在且确实为空时,没有可篡改的行,但检查了 0 行也不代表通过(自 0.5.8 起):ok=None, rows=0, verified_scope="empty",结果为假,CLI 退出码为 3。根据 verify().ok 分支处理的自动化流程,会将缺失文件判为失败,将空文件判为未通过;读取 first_break 和 verified_scope,即可根据名称区分这两种情况。
日志是在程序外写入时:声明断点,不要重新伪造
迟早会有东西绕过 append(),直接向你的 JSONL 写入内容——可能是脚本、事故,或是使用编辑器的人。链条会在此处断裂,并且一直保持断裂,因为这才是真实记录。两个显而易见的做法都不好:要么接受永久的失败状态,让读者无从得知原因;要么重新计算链条,让文件变为通过——这等于伪造记录,而一条可以被悄悄重新伪造的链条不具备任何证明力。
declare_break 是第三种选择。它会追加一行,说明断点:
from arcaeon_ledger import declare_break, verify_file
declare_break("agent.log.jsonl", 25,
"Written out of band 2026-08-15 by a session hand-appending JSON "
"instead of calling append(). Content is true and preserved verbatim; "
"no chain value was ever computed for it, so none can honestly be supplied.")
r = verify_file("agent.log.jsonl")
r.ok # None — bounded, NOT True. Falsy.
r.verified_scope # "bounded_declared_break"
r.breaks # 0
r.declared # ["line 25: declared break (Written out of band 2026-08-15 ...)"]
断点会永远保留在 declared 中。变化之处在于,已知且有说明的断点不再伪装成未说明的断点——孤立行的确切字节会由 sha256 固定下来,因此之后编辑这一行会让文件再次判为失败。它永远不会返回 ok=True。 只有检查过每一行的扫描才会如此,而获准免责的行并未经过检查。verify(strict=True) 会完全忽略声明。
直白地说,这项功能并非密码学工具,而是一种记录机制。 任何能写入文件的人都能写入声明,因此对于已经拥有写入权限的攻击者,它不会提高任何门槛。它防范的是遗忘,而不是篡改。它无法区分诚实的程序外追加与恶意追加——why 是未经验证的人类描述。而且它只能声明 verify() 已发现的断点;对于无人察觉的断点,它无能为力。请用它让诚实记录的事故清晰可读,绝不要用它让账本变为通过。
绑定 Agent 实际读取的内容(artefact-binding)
链条能证明某一行没有被编辑。它不能证明该行曾经属实——
它会和记录事实一样如实地为幻觉做公证。对于能够指向可重新获取来源的情况,bind_artefact 可以弥补这一缺口:对 Agent 实际读取的字节进行哈希,并将摘要存入该行,以便第三方重新获取来源并进行比对。
from arcaeon_ledger import Ledger, bind_artefact
log = Ledger("agent.log.jsonl")
art = bind_artefact("https://example.com/pricing") # or bytes, a file path, or a dict
log.append({"tool": "web.read", "url": "https://example.com/pricing", "artefact": art})
# art -> {"subject": {"name": "...", "digest": {"sha256": "..."}},
# "recipe": "sha256:raw-bytes:v1",
# "digest": "sha256:raw-bytes:v1:<hex>", "bound_at": "...", "source_meta": {...}}
摘要是自描述的——绝不能只是裸十六进制哈希。每个摘要都采用
sha256:<recipe>:<version>:<hex> 格式,并携带自身的配方,让陌生人仅凭该字符串就能重现摘要:raw-bytes:v1(按读取时的原样处理不透明字节)或 json-c14n:v1
(固定且有文档说明的 JSON 规范化方式——键排序、紧凑格式、UTF-8)。配方会冻结,并按只追加的方式进行版本管理,因此旧行会永久保留其配方,规则变更也不会让历史记录看起来像被篡改。
如实验证:
from arcaeon_ledger import verify_artefact
verify_artefact(art) # recipe reproducible + string self-consistent
verify_artefact(art, refetch=True) # for a URL: re-fetch and compare
# -> {"verdict": "live_match", # <- THE answer; read this field
# "digest_ok": True, "reason": None,
# "refetch": "match" | "mismatch" | "unavailable" | "skipped", "notes": [...]}
读取 verdict,而不只是 digest_ok(0.5.7)。 digest_ok 仅表示离线环节——配方可重现,字符串自洽;即使实时重新获取的内容不一致,它仍会保持 True。顶层的 verdict 标签会用一个字段给出完整结果:"digest_consistent"(离线环节通过,未进行实时比对)、"live_match"、"live_mismatch"(实时内容不再匹配——可能已更改或遭到篡改,无法判定)、"live_unavailable"(无法执行所请求的实时检查),或者离线环节失败时的具体失败原因。过去,在 refetch=True 后使用 if out["digest_ok"],即使实时比对不一致也会显示通过;而 out["verdict"] == "live_match" 不会如此。
当前构建无法重现的标签属于明确的失败,而不是通过。 如果摘要指定的算法、配方或配方版本不在支持的注册表中,verify_artefact 会返回 digest_ok=False,并给出机器可读的 reason——取值之一为 unknown_algorithm、unknown_recipe、unknown_recipe_version、malformed_digest、subject_digest_mismatch——且绝不会进入重新获取阶段,因此无法验证的配方不会被判为 "match"。无法重新计算的摘要,就意味着我们没有检查该摘要;而“没有检查”绝不能被报告为“已验证”。新增配方版本时,只要将旧版本继续保留在 SUPPORTED_RECIPE_VERSIONS 中,旧版本就仍可验证;这样既能兑现配方只追加的承诺,也能避免验证器放行它从未随版本发布过的标签。
必须明确说明的诚实边界,因为这正是关键所在: 重新获取结果为 mismatch,表示内容已更改或遭到篡改——无法判定。绝不会将其报告为篡改证据。网页内容会变化,也可能返回 404、设置付费墙或因人而异;绑定只能证明*“这是 Agent 声称自己在时间 T 读取的字节的摘要”,不能证明更多。若要使用中立的捕获结果,而非自行获取的内容,请将来源交由公证快照处理;若要证明在 T 之前已存在*,请在外部锚定摘要。每一项都是额外添加的一层——明确说明,而非暗示。
外部校验:一位外部见证者
链本身无法发现截断——只要删掉最新的几行,剩下的内容仍会通过校验(如上文“它无法证明什么”所述)。解决办法是引入一位见证者:由你控制范围之外的记录者,按一定频率保存你的头部 (rows, chain)。见证者从时间 T 起有了一个固定记录后,被截断的日志行数会少于见证者看到的行数;被改写的日志则会在见证行处呈现不同的链。两者都无法隐藏。
from arcaeon_ledger import Ledger, WitnessStore, publish_head, verify_against_witness
log = Ledger("agent.log.jsonl")
witness = WitnessStore("witness_pins.jsonl") # ideally on a host you don't control
publish_head(witness, "billing-agent", log) # record the current head — do this on a cadence
# later — did the log survive intact?
v = verify_against_witness(witness, "billing-agent", log)
v.verdict # "consistent" | "truncated" | "rewritten" | "no_record" | "witness_broken" | "local_broken"
bool(v) # truthy ONLY on "consistent" — a missing pin is no_record, never a false ok
# READ THE VERDICT WITH ITS QUALIFIERS, never the bare string alone:
v.witness_self_integrity # "verified" | "unestablished" | "broken"
单独一个 "consistent" 并不能说明全部情况。 判定结果还包含 witness_self_integrity:见证存储是否能够证明其自身的固定记录链完整无损。只公开 latest() 的托管客户端无法自我验证,因此其判定结果会显示为 unestablished——比较过程是可信的,但如果该存储提供了伪造的固定记录,比较结果仍会通过。verified 表示已重新计算存储自身的链;broken 表示校验失败。若使用者只根据 v.verdict == "consistent" 分支,而不读取 witness_self_integrity,那么他们对存储是否诚实的信任程度,就和对日志是否诚实的信任程度完全相同——而见证者的存在,正是为了取代这种安排。(此问题发现于 2026-08-23 的邀请前审计,C14;添加该字段是为了确保“未检查”绝不会显示成“已检查且无异常”。)
WitnessStore 是参考见证存储:一个只追加的 JSONL 固定记录文件。托管见证者只是对这个对象的轻量 HTTP 封装;在本地运行它,就能拥有一个完整、离线、零成本且完全由你控制的见证者(显而易见的注意事项是,你控制的见证者,其独立性取决于它的主机)。
这究竟能证明什么。 见证者只能证明你的日志未被截断或改写,且仅相对于见证者看到的内容,并且只截至最近一次固定记录的时间。最近一次固定记录后追加的行,在下一次固定记录之前都不受保护——因此,固定记录之间的最大间隔才是你真正的安全参数,而不是平均间隔,因为攻击者会选择间隔。 而且,这无法证明记录的内容是否属实——那是 artefact-binding 的职责(见上文);见证者只保护历史记录的结构。
见证者保存什么。 只有指纹——(namespace, rows, chain, time)——绝不保存你的日志内容。密码从不存储,这是设计如此:即使见证者遭到入侵,也没有敏感信息可窃取,只有脱离原始日志便毫无用处的哈希。
将其接入任意 MCP Agent
arcaeon-ledger 提供零依赖的 MCP 服务,因此任何 MCP 客户端(Claude Code、
等)都能无需编写代码,就让其 Agent 获得防篡改日志记录能力。接入方法:
{
"mcpServers": {
"ledger": {
"command": "python",
"args": ["-m", "arcaeon_ledger.mcp_server", "--log", "agent.log.jsonl"]
}
}
}
随后,Agent 就拥有五个工具。其中两个是针对单个文件的操作员工具:
ledger_append(record) 用于记录操作(返回其链哈希),ledger_verify(strict?) 用于证明日志完好无损(或返回被篡改行的准确内容)。验证结果有三种,和库中的定义相同:
ok: true = 每一行都已验证;ok: null = 链完好,但跳过了未验证的无链
prechain 行
(verified_scope: "bounded_prechain_skipped" —— 这不算通过);ok: false =
链已损坏。传入 strict: true,即可将任何无链行视为硬性失败。
另外三个是Agent 工具(0.7.0),适用于输出将交给他人的场景——无论是想要证据的主体,还是正在判断是否信任你的对等方:
| 工具 | 用途 | 返回值 |
|---|---|---|
prove_my_conduct(namespace, events) | 批量记录刚完成的操作,并向主体提供一个哈希 | {rows, head_hash, chain_verified} |
verify_peer_ledger(jsonl_text, strict?) | 仅凭文本判断另一 Agent 导出的日志 | {ok, rows, first_break, declared_breaks} |
declare_break(namespace, reason) | 日志已损坏时,声明损坏原因,而非重新铸造一条链 | {declared_line, declared_breaks, ...} |
prove_my_conduct 会在追加后重新验证,因此,日志遭到篡改的 Agent 会得到 chain_verified: false,而不是带着绿色通过标记的头哈希。verify_peer_ledger 返回的 first_break 是一个整数行号(或 null),调用它的 Agent 可以据此指出确切的异常行——它绝不会触碰你的日志(导出内容会通过临时文件验证,而调用本身和其他工具调用一样,会在 <log>.calls.jsonl 中留下单独一行);如果导出内容没有可解析的行,则返回 ok: null(verified_scope: "empty"),因为空无一物却声称通过,是成本最低的伪造。declare_break 在没有损坏时会拒绝执行,并且绝不会恢复通过标记。
Agent 日志按命名空间分别存放,每个命名空间一个文件,位于 --ns-dir 下(默认为 --log 旁的 ledgers/)。命名空间是名称,而非路径:仅允许 [A-Za-z0-9._-],遇到路径遍历会直接拒绝,而不会进行清理。
MCP 使用基于 stdio 的 JSON-RPC,本服务直接实现了该协议——无需 SDK,也无需额外安装。
状态
核心库、CLI 和可直接接入的 MCP 服务均已完成测试:库通过了针对编辑、删除和重排篡改的测试(test_ledger.py);MCP 服务通过请求处理程序完成了 tools/list → append → verify 流程测试,包括篡改检测;Agent 工具通过了命名空间路径遍历、按准确行号检测对等方导出内容篡改,以及已声明损坏的测试(test_agent_tools.py)。该项目从一个已在生产环境运行的哈希链式操作日志中提取而来。外部锚定通过 head() 实现(自行发布固定值),也可使用参考见证存储(上文的 WitnessStore);托管见证服务层(保留、自动固定频率、合规导出)是下一阶段。
MIT。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。