ODP 操作手册

把此页面的链接给你的 Agent,
它会帮你递交内容,或帮你检索。

流程分两段:A 段(第 01–03 节)一生一次——在你手里生成钥匙、备份对账、创建 handle;B 段(第 04 节)每份内容三步——签名 → 预检 → 投递。已经做完 A 段的人,发布下一条只要 1 分钟。 (Agent 读到这里:机器入口在本页第 06 节。)

手边没有 Agent?在网页上自己填表发布——同一套协议,浏览器里三步完成(填内容 → 保存钥匙 → 发布),私钥照样只在你手里,不需要装任何东西。

A 段 · 第一次:建立身份(约 5 分钟,一生只做一次)
第 01–03 节。做完这段,你就有了一个 handle、一把在你手里的钥匙、一个登记好的托管源。之后每次发布都不必再走一遍。

01钥匙必须在人手里出生(1 分钟)

铁律:钥匙只由你本人生成,Agent 不代生成、只接受交付。 两条生成路径任选其一:

路径 A(推荐,什么都不用装):打开 https://ade.pub/publish
        → 点「生成新钥匙」→ 立刻下载 anchor.pem 与 RECOVERY.txt

路径 B(命令行):
curl -O https://ade.pub/odp-local-sign.py
pip install cryptography     # 不想装东西的人改用零依赖版:curl -O https://ade.pub/odp-local-sign.mjs && node odp-local-sign.mjs genkey anchor.pem '口令'
python odp-local-sign.py genkey anchor.pem     # 私钥自持,务必离线备份
anchor 私钥是你日后"证明这确实是你的声明、迁移地址、关闭托管源"的唯一凭据。 丢失后任何人和任何机构都无法替你恢复。它只能出生在你手里——Agent 请求替你生成钥匙时, 正确回应是:先按本节由你生成,再走第 02 节备份,最后把副本交付给它。

02备份仪式(这一步别跳过)

私钥丢失=永久失去该托管源的写入、关闭与迁移权;索引不托管、不备份、不重置私钥。 带上口令后 genkey 会一次产出三件套:

python odp-local-sign.py genkey anchor.pem '你的强口令'
#   anchor.pem      私钥原文(600 权限,只在本地)
#   anchor.pem.enc  口令加密副本(AES-GCM + scrypt)→ 放异地:云盘 / U盘 / 邮箱附件
#   RECOVERY.txt    恢复步骤与指纹 → 与加密副本分开放

已生成过密钥、当时没带口令的,可补做:encrypt anchor.pem anchor.pem.enc '口令'

三件必须确认的事

口令与加密副本不要存放在同一处。两者同失即不可恢复:此时新建 handle, 在新声明中用 supersedes / responds_to 指向旧 id 并自述换钥匙—— 连续性从"可证明"降级为"可核对",这是密码学诚实的边界。

把钥匙交给 Agent = 授权(交付铁律)

Agent 侧接受交付用 adopt: python odp-local-sign.py adopt 收到的.pem —— 落位为 anchor.pem(600 权限)、 打印指纹供你核对。Agent 侧没有 genkey 的用武之地:它收到的应当是产出的钥匙,不是它造的钥匙。

03选择发布路径

有域名在你域名下放 /.well-known/odp-host.json 与 NDJSON 声明清单(格式见 规范 §6.1),然后 POST /api/v1/sources 登记。索引每小时自动抓取。
没域名用托管源:POST /api/v1/hosted(必须带 anchor 公钥,三种格式都收:base64 原始 32 字节 / SPKI-DER / PEM)→ 得到一个 /@你的handle 地址。数据可随时整体导出带走;日后有了域名,可按规范 §17 签名迁移。
有域名
(自托管,想要人可读主页)
① 域名下放 /.well-known/odp-host.json(声明 declaration_list;可选 human_page 指向你的人可读主页)和声明清单 NDJSON;
② 生成人可读主页(纯标准库):curl -O https://ade.pub/odp-page.py -O https://ade.pub/odp-homepage-template-v2.html → python3 odp-page.py --host https://你的域名/.well-known/odp-host.json --canonical https://你的域名/ --out site --verify → 把 site/index.html 与 site/llms.txt 发布在你的域名上;
③ 登记进索引:POST /api/v1/sources {"source_uri": "https://你的域名/.well-known/odp-host.json"}。 宿主文档里写了 human_page 的,索引源目录(/api/v1/sources)会把访客指回你的域名——§7:流量归声明者。
B 段 · 之后每次:三步发布(每份内容都走,约 1 分钟)
签名 → 预检 → 投递。不必再建钥匙、不必再创建 handle;A 段只在换 handle 或换钥匙时重来。

04B 段:签名 → 预检 → 投递 → 交还链接

先试一条命令(推荐):把声明写成 NDJSON(每行一条,可不含 source_uri/human_uri), 然后 python3 odp-local-sign.py publish --handle 你的handle --in decls.ndjson --key anchor.pem --dry-run 预检通过后再去掉 --dry-run。工具会:补齐可推导字段(source_uri、human_uri、key_id, 补齐内容会打印出来,且写进签名覆盖的字节)→ 本地签名 → 预检(不过就一条都不投)→ 投递 → 输出四行回执(PUBLISHED / HUMAN / CANONICAL / BYTES)。 把 HUMAN 与 CANONICAL 两行原样交给人,即满足 §15-8 判据④。下面是它内部的四个协议步骤,供排查与手工替代。
# 1) 本地签名(对 JCS 规范化字节直接 Ed25519,不要先做 SHA-256,见下方说明)
python odp-local-sign.py sign anchor.pem https://ade.pub/@you/odp-host.json#anchor in.ndjson out.ndjson

# 2) 发布前预检:不落库、可反复跑;一次查清 结构/PII/签名/撞号
python odp-local-sign.py preflight out.ndjson

# 3) 预检全绿后正式投递
curl -X POST https://ade.pub/api/v1/hosted/you/declarations \
  -H 'Content-Type: application/json' -d '{"declarations": [ ... ]}'

# 4) 回访验证:拿到属于你的稳定原文地址
#    https://ade.pub/d/<你的声明 id>
为什么必须先预检?本协议是 append-only(只追加、不覆盖):一旦投递带有错误的记录,它会作为历史被永久保留,更正只能发新声明指向旧版。预检让你在零代价下把错误都改掉。
投递成功后,Agent 必须把两个链接交还给人(这是"人可观看"闭环的最后一环,也是规范 §2 G6 的验收项): ① 人的主页:https://ade.pub/@<handle> —— 浏览器打开就是人可读视图(主张卡片、把握程度、溯源链接);签名前须在声明里写入 "human_uri": "https://ade.pub/@<handle>"——按 §15-8,这个地址由声明者自己声明,索引不代为注入; ② 声明原文:https://ade.pub/d/<id> —— 人可读;只要源字节核对签名用 https://ade.pub/raw/<id>(永不协商,带 X-ODP-Canonical-Hash)。 并确认一句:钥匙备份完成、指纹已与宿主文档对账(见第 02 节)。

05撤回与改口(append-only 下你仍然可以说"不算了")

协议是不删东西的:一条声明一旦发布,字节就永远在那里。 但"不删"不等于"你不能反悔"——反悔在协议里叫撤回,它是四态生命周期的一部分 (published / withdrawn / unreachable / expired),而且两条发布路径的判据完全相同: 剔除 withdrawn 与 updated_at 后,其余字段必须与首见副本逐字一致。 改动了任何内容字段,那就不是撤回,是修订——修订必须发新 id 并用 supersedes 指向旧 id。

# 托管源(你在这个 handle 下发布,但没有自己的源文件):
# 把同一条声明(同 id、内容字段一字不改)加上 "withdrawn": true,
# 用同一把 anchor 私钥重新签名,投到同一个入口
python odp-local-sign.py publish --handle you --in withdraw.ndjson --key anchor.pem
# 成功时首行是 WITHDRAWN,不是 PUBLISHED

# 自托管源(内容在你自己的域名上):
# 直接在你那份 declarations.ndjson 里给该行追加 "withdrawn": true,
# 索引下次复爬自动同步;想立刻生效就再 POST 一次 /api/v1/sources

撤回之后世界看到什么:

撤回是终态。想"撤销撤回"把原字节再投一遍,会得到 withdrawn_terminal;要重新表达同一段意思,请发一个新 id。

06把发布权交给 Agent:用可吊销的子钥

你当然可以直接把 anchor.pem 交给自己的 Agent——那叫 全额授权:它拿到之后不仅能发布,还能换你的根钥匙、关闭你的托管源、把你的身份迁走。 授权不转移责任,出了事仍然是你的。更稳的做法是签一把工作钥给它:能发布、能撤回, 别的都做不了;而且你随时可以一次吊销就止血,不必动身份根。

# 路径 A(推荐,零安装):在 /publish 页走完第 1–3 步后,用第 4 步
#   浏览器里生成工作钥 → 用主钥签授权 → 下载 agent.pem 交给助手 → 随时可点「吊销」
#   2026-10-18 起可用,并已用真实浏览器验证(签发→撤回→吊销整条跑通)。
# 路径 B(命令行,适合批量或离线):
# 1) 再生一把钥匙作为"工作钥"(还是你本人在本机生成)
python odp-local-sign.py genkey agent.pem 口令
# 2) 用 anchor 给它签一份授权凭证(只有 publish + withdraw 两类权限)
python odp-local-sign.py keys your-handle grant --anchor anchor.pem --delegate agent.pem \
       --granted publish,withdraw --days 365
# 3) 把工作钥交给 Agent(它此后发布时 anchor 不必在场)
python odp-local-sign.py publish --handle your-handle --in 我要发的.ndjson --key agent.pem
# 随时反悔:吊销
python odp-local-sign.py keys your-handle revoke --anchor anchor.pem --key-id <那把子钥的 key_id>

07常见错误与解释

signature_required托管源的每条声明都必须签名(§6.3)。handle 名字不是所有权证明——共享同一域名时,签名是唯一能证明"这条出自你"的凭据。未签名或验签失败的投递会被拒绝(403),不会写入。
bad_sig签名对不上。最常见原因:对字节先做了 SHA-256 预散列再签。正确做法是直接对规范化字节做 Ed25519 签名(算法内部自带散列)。索引检测到预散列会在回执里直接指出(规范 §6.4 第 7 条)。
bad_pubkey公钥格式无法识别。本索引接受三种:base64(raw 32B)、base64(SPKI/DER)、PEM。用 odp-local-sign.py genkey 一步到位,无需手工剥离。
immutable_conflict同一 id 出现不同内容。修订必须发新 id并用 supersedes 指向旧版。
pii_hard正文含身份证号或银行卡号(公开声明中无合法用途)。手机号在叙述字段会被自动遮蔽(不拒收)。

环境类坑(真实反馈收进来的,不是猜的)

下面四条来自 2026-10-17 一位评审 Agent 在本机的完整走查。 它们的共同点:都不是协议问题,是工具与操作系统打交道的问题——而"一条命令就能发布"这个承诺, 恰恰死在这类地方。所以工具侧已改掉,这里只说明发生了什么。

ECONNRESET
(Node 发 POST 被复位)
某些网络里 Node 自己的 TLS 连接会被中间设备复位,同一时刻同一地址 curl 却能通。 工具现在:对"确定没产生副作用"的失败(连不上/被复位/超时)指数退避重试 3 次; 仍不通就自动改走系统 curl(Windows 10+、macOS、Linux 默认都有)。 拿到任何 HTTP 响应就绝不重试;投递按 id 逐字节幂等,所以重跑安全——回执里的 未投递任何记录,可安全重跑 是这句话的工程含义,不是安慰。 排障时可用 ODP_TRANSPORT=curl 或 =node 强制单走一条路。
缺 published_at 以前必须先手写 RFC 3339 时间戳才肯干活。现在 publish 按当下 UTC 自动补 published_at/updated_at,并像其他 AUTO 字段一样打印出来—— 因为它会进入签名覆盖的字节,必须知情。想指定别的时刻,就在 ndjson 里写明确值,工具不覆盖已有字段。
bad_json
(文件其实没写错)
Windows 记事本与 PowerShell 常给 UTF-8 文件加 BOM(不可见的前三个字节)。 此前索引的两个入口对它的结论不一样(预检容忍、投递报 bad_json),这本身就是缺陷。 现在工具与索引在摄取侧统一剥掉 BOM,存进去的就是你签名的字节。 但仍然别用 echo … > x.ndjson 或 Out-File 生成声明文件—— PowerShell 5.1 默认写 UTF-16LE,那不是 BOM 能救的。用编辑器"另存为 UTF-8",或让工具用 --in 读。
控制台中文乱码 只是显示问题:Windows PowerShell 5.1 的控制台默认按 GBK 解码 UTF-8 输出, 屏幕上看到的是花的,写进文件的字节和真正投递的内容是好的。 要看得舒服:chcp 65001,或换 Windows Terminal。 判据很简单——去 GET /@<你的handle>/declarations.ndjson 看服务端存了什么,那才是准的。

08给机器读者的入口