{"protocol":"odp/1","spec":"https://ade.pub/odp-spec.md","endpoints":["/ (人类落地页; ?format=json 得机器摘要)","/api/v1/declarations","/api/v1/declarations/{id}","/d/{id} (原文稳定地址)","/api/v1/stats","/api/v1/conflicts","/api/v1/dump","/api/v1/sources (POST)","/api/v1/preflight (POST 发布前预检, 不落库)","/api/v1/appeal (POST)","/mcp","/llms.txt","/tos.md","/odp-local-sign.py (本地签名工具)","/api/v1/hosted (GET 清单 / POST 创建托管源)","/api/v1/hosted/{handle}/declarations (POST 发布)","/api/v1/hosted/{handle}/export (GET 免费全量导出)","/api/v1/hosted/{handle}/graduate (POST 升学待签字节)","/api/v1/hosted/{handle}/moved (POST 登记已签迁移记录)","/@{handle}/odp-host.json","/@{handle}/declarations.ndjson","DELETE /api/v1/hosted/{handle} (需 anchor 签名)"],"hosted_source_requirements":{"anchor_public_key":"required（无账号体系：所有权由 anchor 私钥签名证明）","export":"GET /api/v1/hosted/<handle>/export 免费、单请求、无需授权","graduation":"新域名放同公钥宿主文档 → 本地签 odp-moved.json → POST /moved","no_privilege":"托管内容不因托管关系获得任何检索优待（§10）"},"sort_policy":{"modes":["published_desc","updated_desc","id_asc"],"default":"published_desc","note":"§10：仅确定性可复现排序；相关性排序若提供必须公开算法与版本","relevance_ranking":"not_implemented_in_mvp","human_view":"/browse（同一份查询代码渲染的人可读页，无热度无推荐）"},"error_hints":{"bad_handle":"handle 需匹配 ^[a-z0-9][a-z0-9-]{1,31}$（小写字母/数字/连字符，2–32 字符，不以连字符开头结尾），见 §6.3 规则 6","reserved_handle":"换一个不冲突的 handle；保留字见 GET /api/v1/meta 的 hosted_create","handle_taken":"handle 先到先得（§6.3 规则 6）：换名，或用已登记该 handle 的 anchor 私钥签名证明所有权","tos_required":"请求体加 \"accepted_tos\": true；条款全文 GET /tos.md","anchor_key_required":"public_key 需为 {key_id, alg:\"ed25519\", value}；value 接受 base64(raw32) / SPKI-DER / PEM，示例见 GET /api/v1/meta 的 hosted_create","bad_pubkey":"改用 odp-local-sign.py genkey 的输出（一步得到合法公钥），或直接贴 PEM","bad_shape":"预检/发布请求体形如 {\"declaration\": {…}} 或 {\"declarations\": [ {…} ]}","bad_json":"Content-Type: application/json，正文为 UTF-8 JSON","bad_body":"接受 {\"declarations\":[…]} 或 Content-Type: application/x-ndjson（每行一条）","missing_field":"补齐 details.field 指明的字段；完整必填集见规范 §5.3","bad_type":"type ∈ idea | intent | notice | capability（§5.3）","bad_identity":"identity ∈ verified | signed | verified_signed | unverified_mirror（§6.4）","bad_sort":"sort 取值见 GET /api/v1/meta 的 sort_policy.modes","bad_cursor":"游标不透明：原样使用上一次响应的 next_cursor，不要手工构造（§7）","bad_uri":"source_uri 需为可解析的绝对 URL","empty":"至少提供一条声明","immutable_conflict":"同 id 不得改内容字节（§5.4）。两类合法出路：改内容→发新 id 并 supersedes 指向旧 id；**撤回**→带 withdrawn:true 且其余字段与首见副本逐字一致（§5.4 细则第 5 条，托管投递同样接受）","withdrawn_terminal":"撤回是终态：不能靠再投一次原字节把它改回已发布。要重新表达请发新 id（§5.4）","withdrawn":"该声明已被声明者撤回；默认口径不返回，显式 ?include_withdrawn=true 取墓碑，字节始终在 /raw/{id}（§5.4/§7）","same_key_id":"delegate.key_id 与在册（含已退役/已吊销）某把钥匙重名：一个 key_id 只能对应一把公钥，否则历史声明分不清该用哪把验。续期请换新 key_id（§6.4-20）","key_material_revoked":"这把公钥曾被授权又已被吊销，不能换个 key_id 重新登记——那等于把泄露过的私钥接回来。请新生成一把密钥（§6.4-20）","stale_grant":"授权时间戳不比既有记录更新，看起来是重放的旧凭证。旧凭证的签名永远有效，所以这里靠时间戳单调性挡。请用当前时刻重签一份、并用新 key_id（§6.4-20）","delegation_invalid":"委托凭证自身的 anchor 签名验不过，委托链不成立。检查是否先做了 SHA-256 预散列（应直签 JCS 字节）、凭证是否被改过","delegation_revoked":"签名来自一把已吊销的子钥：吊销是面向未来的止血，它此前发布的声明不受影响。要让 Agent 继续发，请由 anchor 重新签发一把新 key_id 的子钥（§6.4-20）","delegation_expired":"签名来自一把已过有效期的子钥。续期 = anchor 重签一份新 key_id 的授权；存量声明的身份不会因到期被追溯改判（§6.4-20）","delegation_not_yet_valid":"这把子钥的 not_before 还没到。等生效，或让 anchor 重签一份当期有效的授权","insufficient_grant":"这把子钥没有被授予本次操作所需权限（本期只有 publish / withdraw）。换根、关闭、迁移、管理钥只能由 anchor 直签（§6.4-20）","unknown_grant":"granted 里出现索引未实现的权限值。宁可整条拒收，也不能「凭证说有、实现当没有」地静默降级（§6.4-20）","bad_grant":"granted 只能取 publish / withdraw；manage_keys/close/migrate/rotate_anchor 永不授予子钥（§6.4-20）","anchor_mismatch":"本期只接受由**当前 anchor** 签发的凭证（多级委托不在范围内）","already_revoked":"这把子钥已经吊销过了；吊销记录只置位不删除，可在宿主文档核对","unknown_key":"revoke 的 key_id 不在该 handle 名下。已知子钥清单见 /@<handle>/odp-host.json","delegate_quota":"每个 handle 的活跃子钥数量已达上限（宿主文档要留在体积闸内）","too_large":"压缩内容或分批投递（阈值见 meta.capacity）","host_doc_unreachable":"先确认宿主文档 URL 公网可访问并返回 JSON（§6.1），再重试登记","list_unreachable":"清单须为 NDJSON 且公网可读（§5.1/§6.1）；源已登记，索引仍会每小时重试","no_such_handle":"先 POST /api/v1/hosted 创建；已迁移/关闭的 handle 不再接受写入（§6.3）","no_local_list":"先创建 handle 并投递，或用 POST /api/v1/sources 登记外部清单","no_move_record":"先 POST /api/v1/hosted/{handle}/graduate 取待签字节，本地签名后 POST /moved","unknown_endpoint":"端点清单见 GET /api/v1/meta 的 endpoints","signature_required":"托管源每条声明必须由该 handle 登记的 anchor 公钥签名（§6.3）：odp-local-sign.py sign 后再投递；未签名不会入库","bad_signature":"签名与该 handle 登记的公钥不匹配：确认同一把私钥、key_id 指向本 handle 宿主文档、且未做 SHA-256 预散列（§12 / §6.4-7）","write_rate_limited":"创建/发布频率受限：按 meta.capacity 阈值退避重试；批量请合并为一次投递"},"error_hints_note":"键=错误码（与错误响应的 error 字段一致），值=一句人话＋下一步动作；人可读版见 /browse 与 /howto，机器版就是这个对象","capacity":{"max_declaration_bytes":262144,"max_active_per_source":10000,"read_req_per_min":60,"write_req_per_hour":10,"write_limits":{"source_register_per_ip_per_hour":10,"hosted_create_per_ip_per_hour":5,"hosted_publish_per_handle_per_hour":60,"note":"超限返回 403/429 write_rate_limited，附退避提示；测试请在独立窗口内进行，勿共享同一出口 IP"},"crawl_interval_sec":3600,"unreachable_after_hours":48},"pii_policy":{"id_card":"hard_reject","bank_card":"hard_reject","phone_in_narrative_fields":"mask_in_index_copy","contact_field":"exempt_by_design(§16)"},"identity_semantics":{"verified":"域名一致：控制背书，但未签名，内容完整性依赖传输","signed":"签名有效：证明完整性与密钥连续性，**不**证明 id 中 host 的所有权","verified_signed":"两者兼备（协议的理想态）","unverified_mirror":"域名不一致且未签名：按镜像收录，如实标注，不拒收","sig_invalid":"签名存在但验证失败或公钥取不到：不得当作 `verified`（评审 P1-2 修正，2026-10-16）；完整性声称不成立，过滤参数 identity=sig_invalid 可单独筛","signature_alg":"Ed25519 over the raw RFC8785 JCS bytes of (record minus signature). NO separate SHA-256 pre-hash — Ed25519 hashes internally; pre-hashing fails verification (see spec §6.4-7).","ranking_note":"签名可作过滤参数（signed=true / identity=），永不作排序权重（§12/§10）"},"corpus_scopes":{"/api/v1/dump":"索引全量转储（含自有与托管源），剔除已撤回记录","/declarations.ndjson":"仅本索引运营者自己一个源的清单（§6.1 declaration_list）；不含托管源，保留撤回记录并带 withdrawn 标记——不是全量","/api/v1/stats.declarations":"计数包含已撤回记录，故大于 dump 行数","/api/v1/declarations?exclude_test=1":"排除测试数据（三层判据见 exclude_test_rule 与 /api/v1/test-registry）；默认不排除，索引不做隐性编辑（§10）","exclude_test_rule":"声明被判定为测试数据的依据有三层，全部可机读复现：① 归属 handle 的 handles.test=1（e2eweb*/demo* 前缀，含导入/迁移用的临时句柄）；② 声明自身带 `\"test\": true` 字段或 `odp-test` 标签（**声明级**标记，不依赖 handle 命名——测试记录混入真实 handle 时靠它兜底）；③ GET /api/v1/test-registry 逐条登记（append-only 下已签名的字节改不了，只能由索引登记并公开理由）。②③ 存在的理由是一条实测教训：把\"测试\"绑在 handle 前缀上，等于假设测试永远只发生在测试账号里——2026-10-17 一天内破了两次。","/api/v1/test-registry":"exclude_test=1 排除了什么、依据哪一层、谁登记的理由（HTML 与 JSON 同一地址）","/api/v1/dump.meta.ndjson":"按 id 连接的索引判定侧车（identity_status / signature_status / index_canonical_hash / source_status / pii_masked）。dump 保持源字节原样（§6.2），fork 者不必重新实现验签即可复现分类。","/examples.ndjson":"规范 §19 的可直接使用示例集（原与运营者清单同名，已改名消歧）","human_uri":"声明级可选字段（§5.3）：该条声明的人类可读形态地址。面向人的引用目标 = human_uri（若有），否则 source_uri（§7）。静态托管源靠它满足可读性，因为它既不能协商也不能发响应头","link_header":"机器清单（/declarations.ndjson、/@<handle>/declarations.ndjson）均带 Link: <人可读页>; rel=\"alternate\"; type=\"text/html\"（§7 分支 c）"},"signing_tools":{"python":{"url":"https://ade.pub/odp-local-sign.py","deps":"python3 + cryptography","role":"审计基准（canonical_json 与索引同源）"},"node":{"url":"https://ade.pub/odp-local-sign.mjs","deps":"Node ≥18，零依赖（内置 crypto）","role":"消掉 pip 这一步；子命令与 Python 版一一对应"},"equivalence_proof":"2000 条随机声明规范化逐字节全等 + 同钥同记录签名逐字节相同 + 三件套双向互解密；两版都内嵌规范 §12.1 测试向量，自证闸不过即拒绝签名","numeric_domain":"数字限非整数 1e-4≤|v|<1e16、整数 |v|<1e15；越界一律整条拒绝（Python 按 int/float 类型分支序列化，JS 无法区分该字面量，宁拒绝不产出不一致字节）——见 §6.4-16"},"verification_log":[{"subject":"/publish 浏览器内签名与表单点击流","state":"verified_in_real_browser","runs":[{"date":"2026-10-17","how":"Chrome headless + CDP 驱动真实点击流，两次 ok:true；顺带查出『页面根本没签名』与 key_id 写死 #anchor 两处缺陷","harness":"run_cdp.ps1 + cdp_flow.js"},{"date":"2026-10-18","how":"同日文案与服务端渲染改动后复验：ok:true，并新增 5 项页面断言（主页链接带 @、误句已删、?handle= 即时写入、错误速查表服务端已填、第 2 步指引交工作钥）均通过"}],"supersedes_note":"capability-002 / capability-003 里『尚未经真实浏览器端到端验证』一句已被本条取代；因签名内容不可改写，需由 anchor 持有人发布一条 supersedes 新声明正式更正"},{"subject":"两版 CLI 等价性","state":"verified","runs":[{"date":"2026-10-18","how":"2000 条差分零不一致 + 跨实现签名逐字节相同 + 三件套互解 + 越界数字 10/10 拒绝"}]},{"subject":"浏览器内签发工作钥与撤回（/publish 第 4、5 步）","state":"verified_in_real_browser","runs":[{"date":"2026-10-18","how":"真实 Chrome + CDP 走完整点击流：发布 → grant 一把 publish/withdraw 工作钥 → 用页面撤回该条（回读 410 / include_withdrawn 200 / /raw 仍 200 且带 X-ODP-Status） → 吊销子钥（宿主文档 delegate_status=revoked）。委托链内嵌于宿主文档可离线复算"}],"how_to_verify":"对 /publish 重跑一次 CDP 点击流（run_cdp.ps1 + cdp_flow.js），任一步 ok:false 即推翻本条"},{"subject":"撤回与委托子钥","state":"verified_on_test_handles_only","runs":[{"date":"2026-10-18","how":"撤回端到端 31/31、委托子钥 53/53；现网**全部**委托记录都落在测试 handle 上，还没有一位真人采用者用过子钥"}],"how_to_verify":"GET /api/v1/hosted 每个 item 带 test 与 delegates[]；真人 handle（test=0）名下无未吊销子钥 = 本条为真的判定条件","open":"真人未用过子钥；@daluo 那把被第三方 Agent 摸过的 anchor 仍待轮换"}],"hosted_create":{"endpoint":"POST /api/v1/hosted","content_type":"application/json","min_body_example":{"handle":"alice","name":"爱丽丝","kind":"person","contact":"mailto:alice@example.org","accepted_tos":true,"public_key":{"key_id":"anchor","alg":"ed25519","value":"<odp-local-sign.py genkey 的输出>"},"declarations":[]},"required_fields":["handle","accepted_tos(=true)","public_key.key_id","public_key.value"],"optional_fields":["name","kind","contact","declarations","declared_home（会被忽略，见 identity_rules）"],"handle_rules":{"pattern":"^[a-z0-9][a-z0-9-]{1,31}$","reserved":[".well-known","admin","api","declarations.ndjson","favicon.ico","index.html","llms.txt","mcp","o","odp-spec.md","robots.txt","self","sitemap.xml","static","www"]},"withdrawal":{"why":"撤回是 §5.4 四态之一，也是「地址属于自己、内容由自己掌控」的最后一环。规范细则第 5 条（2026-10-12）写的判据是「在源上追加 withdrawn:true」——自托管源改得了自己的文件，而托管源的声明者**碰不到源文件**，唯一写入口是投递端点。此前那段豁免只装在爬取侧，于是托管路径把撤回判成 immutable_conflict（实现没跟上规范）。","how_hosted":"向 POST /api/v1/hosted/<handle>/declarations 再投递**同一个 id**：内容字段与首见副本逐字一致，只加 \"withdrawn\": true 并前移 updated_at，用当前登记的 anchor 私钥重新签名","how_self_hosted":"在自己的源清单里给该行追加 \"withdrawn\": true（索引下次复爬生效，或 POST /api/v1/sources 立即触发）","judgement":"唯一的判据，两条路共用同一个函数：剔除 signature/withdrawn/updated_at 后，其余字段的 JCS 规范化哈希与首见副本相同。任何内容字段变动仍记为冲突。","what_index_keeps":"内容字节 = 首见副本，永不改写；状态转 withdrawn；本 handle 清单里这条变为带 withdrawn:true 的撤回记录——这样任何爬取方（含索引自己复爬）重抓都收敛到「已撤回」，不会把撤回状态冲回去","read_back":{"默认口径":"GET /api/v1/declarations 不返回（加 include_withdrawn=true 取回）","单条机器形态":"GET /api/v1/declarations/{id} → 410 withdrawn，回执内给出取墓碑与取字节的确切入口（不再是死路）","带内容的墓碑":"?include_withdrawn=true → 200 + withdrawn_note","字节":"GET /raw/{id} 始终返回首见副本字节 + X-ODP-Canonical-Hash，撤回时另带 X-ODP-Status: withdrawn","人可读":"GET /d/{id} 呈现为撤回墓碑页（明示已撤回，原文仍可核对）"},"terminal":"撤回是终态：不能再投一次原字节改回已发布（错误码 withdrawn_terminal）；要重新表达同一段意思请发新 id","receipt":"publish 撤回成功时首行打印 WITHDRAWN（不是 PUBLISHED），并附 STATE 行说明状态转换"},"delegation":{"why":"此前把钥匙交给 Agent 等于全额授权——它能发布，也能换根/关闭/迁移。真实走查里发生过「第三方 Agent 摸过采用者真钥」。子钥把最坏后果钉死在「乱发/乱撤声明」这一级，且一次 revoke 即止血，不动身份根。","endpoint":"POST /api/v1/hosted/<handle>/keys","operations":{"grant":"anchor 签发子钥（提交 anchor 双签：凭证本体 + 管理动作）","revoke":"anchor 吊销子钥；记录只置位不删除，吊销历史可对账"},"grantable":["publish","withdraw"],"never_grantable":["manage_keys","close","migrate","rotate_anchor"],"grant_shape":{"type":"odp-key-delegation","handle":"<必填，须与 URL 一致>","ts":"<RFC3339，重放判定依据>","anchor_key_id":"anchor","delegate":{"key_id":"publish-2026-10-17","alg":"ed25519","value":"<base64 raw32>"},"granted":["publish","withdraw"],"not_before":"<可选>","not_after":"<可选>","signature":"对『剔除 signature 后整对象的 JCS 字节』直签 Ed25519"},"verify":"宿主文档 public_keys[] 的子钥条目内嵌**完整凭证对象**（delegation 字段）；任何第三方剔除其 signature、做 JCS 规范化、用 anchor 公钥即可离线复算委托链","time_semantics":"吊销与到期**只约束新写入**；已发布声明的 sig_state / identity_status 不追溯改变（否则索引每次复爬会把有效期内合法发布的存量声明判废——§6.4-20 明令禁止）","attribution":"读侧字段 signed_by 三态：\"anchor\"（当前 anchor 直签）｜\"delegate:<key_id>\"（受托子钥签）｜**缺席**＝索引未记录该条归因（自托管源记录，或本列上线前的历史写入；托管记录已在启动时按公钥验签回填）。出现规则：只要索引知道就写，不知道才省略——不会只对子钥写、对 anchor 省。落点：GET /api/v1/declarations 的每条、单条读取、/raw 与 /d 的 X-ODP-Signed-By 响应头、/api/v1/dump.meta.ndjson 每行；/d 的 HTML 另在**子钥**签名时加一枚可见徽章（给人看的降噪，不是数据差异——机读处仍按上列规则）。","revoked_key_id":"永久占位：续期必须换新 key_id。旧凭证的 anchor 签名永远有效，不占位就等于吊销后重放旧报文即可复活子钥","errors":["signature_required","bad_signature","handle_mismatch","bad_type","bad_key_id","bad_grant","unknown_grant","bad_pubkey","anchor_mismatch","same_key","same_key_id","stale_grant","key_material_revoked","delegate_quota","delegation_invalid","delegation_revoked","delegation_expired","delegation_not_yet_valid","insufficient_grant","already_revoked","unknown_key"],"tool":"python3 odp-local-sign.py keys <handle> grant --anchor a.pem --delegate sub.pem；revoke 同端点。两把私钥都只在本地签名，永不上传","not_in_mvp":"多级委托（子钥再签孙钥）与 MCP 发布通道都不进本期——后者会挪动「人知情地签一次」这一步的位置，而其便利 CLI 已提供"},"identity_setup_once":{"when":"每个声明者一生一次（换 handle 或换钥匙时重来）","steps":["1) 钥匙由声明者本人生成（/publish 网页或本人 CLI；Agent 只接受交付，不代生成）","2) 备份三件套并 check 输出 MATCH","3) POST /api/v1/hosted（带公钥，先登记）"]},"per_publish_steps":{"when":"每份内容都要走，约 1 分钟；不需要重复 identity_setup_once","one_command":"python3 odp-local-sign.py publish --handle <h> --in decls.ndjson --key anchor.pem [--dry-run]   （内部执行下面 4 步；预检不过则一条都不投递；成功输出四行回执）","receipt_fields":["PUBLISHED <id>（撤回时首行为 WITHDRAWN <id> + STATE 行）","HUMAN <base>/@<handle>","CANONICAL <base>/d/<id>","BYTES <base>/raw/<id> + X-ODP-Canonical-Hash"],"steps":["1) 本地 sign（key_id、human_uri/source_uri 与 published_at/updated_at 都由工具推导并在签名前打印；显式写了值则以你写的为准）","2) POST /api/v1/preflight {declaration, handle}（不落库，可反复）","3) POST /api/v1/hosted/<handle>/declarations","4) 把 HUMAN（/@handle）与 CANONICAL（/d/<id>）两个链接交还给人（§15-8 判据④）"]},"sequence":["identity_setup_once 之后才可执行 per_publish_steps；两段内部顺序均不可颠倒"],"note":"顺序不可颠倒：本地 verify 与验签都要先有已登记的公钥（identity_setup_once 第 3 步早于任何 sign）","backup_ritual":{"where":"钥匙只由声明者本人生成（/publish 浏览器生成后下载，或本人运行 odp-local-sign.py genkey 产出三件套：anchor.pem / anchor.enc 口令加密副本 / RECOVERY.txt）。交给 AI Agent 代发布 = 交付授权：先完成备份与 check 对账，再交付副本；Agent 侧以 `adopt` 接受（不代生成）；收回授权 = §17.4 轮换","verify_with":"回执中的 pubkey_fingerprint 与宿主文档 public_keys[].fingerprint；再用 `check <handle>` 确认本地钥匙可接管（输出 MATCH）","loss_policy":"索引不托管、不重置私钥；丢失＝永久失去写入/关闭/迁移权（/tos.md 第 8 条）"}},"publishing_via_mcp":{"supported":false,"is_a_technical_limit":false,"why":"价值选择而非技术限制：MCP 加一个转发已签名声明的工具并不触碰私钥。我们刻意不提供这条捷径——备份仪式必须先于便利；跳过 §02 的三件套与 check 对账，等于把身份交给一次不可恢复的偶然。","instead":"发布走 HTTP + /odp-local-sign.py（或零依赖 .mjs）；纯 MCP 环境请让人先用 /publish 在浏览器内生成并保管钥匙","revisit":"若将来引入 delegation key（§17.4 提案：anchor 签发可吊销子钥），MCP 发布通道可在'子钥可吊销'的前提下重议——这才是降低风险的正解，而不是删掉摩擦"},"export_format":{"endpoint":"GET /api/v1/hosted/<handle>/export","default_json":{"exported_at":"RFC3339","host_doc":"{宿主文档对象}","count":"N","declarations":"[原始声明对象数组，字段未改动]"},"ndjson":"?format=ndjson → 每行一条源字节原文（可直接作为自托管清单使用）","auth":"不需要任何授权或私钥（§6.3 规则 1）"},"no_registration_required_for_publishing":true,"sources":1,"declarations":176}