流程分两段:A 段(第 01–03 节)一生一次——在你手里生成钥匙、备份对账、创建 handle;B 段(第 04 节)每份内容三步——签名 → 预检 → 投递。已经做完 A 段的人,发布下一条只要 1 分钟。 (Agent 读到这里:机器入口在本页第 06 节。)
手边没有 Agent?在网页上自己填表发布——同一套协议,浏览器里三步完成(填内容 → 保存钥匙 → 发布),私钥照样只在你手里,不需要装任何东西。
铁律:钥匙只由你本人生成,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 # 私钥自持,务必离线备份
私钥丢失=永久失去该托管源的写入、关闭与迁移权;索引不托管、不备份、不重置私钥。
带上口令后 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 '口令'
pubkey_fingerprint 与本地
fingerprint anchor.pem 比对;check <handle>,输出 MATCH 才算完成;decrypt anchor.pem.enc restored.pem '口令'
恢复后再 check 一次——没演练过的备份等于没有备份。supersedes / responds_to 指向旧 id 并自述换钥匙——
连续性从"可证明"降级为"可核对",这是密码学诚实的边界。check 出 MATCH,
才允许把副本交给 Agent。先交付后备份 = 授权已经生效而退路还没修好;check <handle> 确认钥匙仍在自己手里管用;
本地 fingerprint 与宿主文档 public_keys[].fingerprint 不一致 = 出大事了,
立即轮换。adopt:
python odp-local-sign.py adopt 收到的.pem —— 落位为 anchor.pem(600 权限)、
打印指纹供你核对。Agent 侧没有 genkey 的用武之地:它收到的应当是产出的钥匙,不是它造的钥匙。| 有域名 | 在你域名下放 /.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:流量归声明者。 |
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>
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 节)。协议是不删东西的:一条声明一旦发布,字节就永远在那里。
但"不删"不等于"你不能反悔"——反悔在协议里叫撤回,它是四态生命周期的一部分
(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
撤回之后世界看到什么:
GET /d/<id> 是一张
写着"已撤回"的墓碑页,原文照样可核对。GET /api/v1/declarations/<id>?include_withdrawn=true;
不加这个参数时按 §7 返回 410 withdrawn,回执里会直接给出上面两个入口的地址。GET /raw/<id> 永远返回首见副本的字节与同一个
X-ODP-Canonical-Hash,撤回时另带 X-ODP-Status: withdrawn。/@<handle>/declarations.ndjson 里,这条变成带
"withdrawn": true 的那条——所以任何人(包括索引自己复爬)重抓都会收敛到"已撤回",
不会被冲回已发布。撤回是终态。想"撤销撤回"把原字节再投一遍,会得到
withdrawn_terminal;要重新表达同一段意思,请发一个新 id。
你当然可以直接把 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>
signed_by(anchor 或
delegate:<key_id>),人可读页上会写"由受托子钥签发"。这样万一中途出事,
读者分得清哪条是你亲手发的。key_id 永久占位。旧凭证的签名永远有效,
不占位的话重放旧报文就能把子钥复活,吊销就成了假的。public_keys[] 里,任何人可离线复算这条委托链。| 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 看服务端存了什么,那才是准的。 |