DPE 投递协议规范 v1(dpe-push/1)
外部数据源系统向 Robot Memory 投递 DPE 文档的通信协议规范正文。
!!! note "状态" 起草中(v1)· 2026-09-18 · 归属 TFRS-379
Hash 算法契约见 [hash-contract-v1.md](hash-contract-v1.md)。协议总览与设计基线见 [index.md](index.md)。
1. 协议全景
协议分为三个阶段。Phase 0 可缓存、可跳过;Phase 1 是纯只读操作;Phase 2 是唯一的写入点。
Phase 0 能力发现 GET /.well-known/dpe-push 无需认证,可缓存
Phase 1 协商 POST /v1/memory/dpe:negotiate manifest → gap(只读)
Phase 2 投递 POST /v1/memory/dpe:commit manifest + contents → result
设计决策
| 决策 | 选择 | 理由 |
|---|---|---|
| 协商粒度 | 批量(一次请求携带完整 manifest) | 文档动辄数百 element。若效仿 Docker Registry 逐 blob HEAD,将产生数百次往返。此处采用 Git fetch 式批量协商(一次 have / want),而非 Docker 式逐对象探测 |
| 接口形态 | AIP-136 自定义方法后缀 :negotiate / :commit |
Google AIP-136 是 RPC-over-HTTP 的标准形态,语义自明,且不与未来可能新增的子资源路径冲突 |
| 版本表达 | media type:application/vnd.tfrs.dpe.v1+json |
OCI / Docker 既有实践;使内容协商与协议版本协商合一,避免 /v2/ 式路径版本污染资源层级 |
快速路径
若客户端确信服务端已持有目标内容(例如刚完成一次成功投递且本地未再变更),可跳过 Phase 0 与 Phase 1,直接进入 Phase 2。此时若内容不足,服务端返回 DPE_INSUFFICIENT_CONTENT(见 §7 错误模型),客户端据此补传即可——幂等键保证该重试安全。
2. Phase 0 — 能力发现
GET /.well-known/dpe-push
无需认证。与项目既有的 GET /.well-known/oauth-protected-resource(RFC 9728)同规格。该文档全局一致、不含任何 robot 专属信息,泄露面为零。
{
"protocol_versions": ["1"],
"accepted_hash_strategies": [
{
"uri": "hash-strategy://default?algo_version=v1",
"status": "stable",
"deprecated_at": null
}
],
"default_hash_strategy_uri": "hash-strategy://default?algo_version=v1",
"max_payload_bytes": 52428800,
"content_encodings": ["gzip", "identity"],
"delta_supported": true,
"endpoints": {
"negotiate": "/v1/memory/dpe:negotiate",
"commit": "/v1/memory/dpe:commit"
}
}
accepted_hash_strategies 是服务端算法版本闸门:客户端从中选择一个双方都支持的 hash_strategy_uri。
!!! warning "能力文档不是信任来源"
能力文档可被缓存,客户端可能在服务端收紧 allowlist 之后仍持有旧副本。服务端必须在写入路径上重新校验一次 hash_strategy_uri 是否仍在 allowlist 内,不匹配则拒绝,不得依赖 Phase 0 的结果。
配置项:dpe_push.accepted_hash_strategies(Robot 配置)。默认仅放行 v1。运维可用它单独关停某个算法版本而无需改代码。
3. Phase 1 — 协商
POST /v1/memory/dpe:negotiate
Content-Type: application/vnd.tfrs.dpe.v1+json
Content-Encoding: gzip
Authorization: Bearer <RS256 JWT>
X-TFRS-Robot-Id: <rid>
X-TFRS-Robot-Id 可选,仅用于防误路由校验(不匹配时返回 RobotIdMismatchError 语义的错误),不是路由键——见 index.md 的「设计基线」章节。
请求体(manifest)
{
"hash_strategy_uri": "hash-strategy://default?algo_version=v1",
"file_uri": "dpe://acme-crm/handbook",
"file_type": "markdown",
"doc_metadata": {},
"doc_hash": "a1b2c3…",
"base_doc_hash": "0f9e8d…",
"pages": [
{
"number": 0,
"title": "第一章",
"page_hash": "c3d4e5…",
"elements": [
{ "content_hash": "e5f6a7…" },
{ "content_hash": "b8c9d0…" }
]
}
]
}
manifest 的三条规格决定
| 决定 | 理由 |
|---|---|
必须含 page.number |
内核 LCS 按 page.number 配对新旧页(unified_memory.py 的 old_by_num / new_by_num),doc_hash 亦按 number 排序取 page_hash 序列。缺此字段则无法重建页身份 |
不含 seq_in_page |
HashStrategy.attach() 在装配阶段按元素数组顺序 enumerate 赋 0..N-1。数组顺序即阅读顺序——不设冗余字段,即从结构上排除「顺序与 seq 不一致」这类歧义 |
page_hash / doc_hash 是提示,不是断言 |
见 §5 权威模型 |
!!! warning "客户端必须维持稳定的 page 编号"
由于页按 number 配对,在文档头部插入一页会使后续所有 number 位移,导致配对全部失配——LCS 将退化为「全部重建」。
增量收益依赖编号稳定。客户端应为每页分配**稳定且持久**的编号(例如以页内容指纹派生,或维护本地编号映射),而非每次投递重新从 0 顺序编号。
响应
{
"status": "gap",
"missing_content_hashes": ["e5f6a7…"],
"server_doc_exists": true,
"server_doc_hash": "0f9e8d…",
"base_doc_hash_matched": true
}
status 取值:
| 值 | 含义 | 客户端动作 |
|---|---|---|
unchanged |
server_doc_hash == manifest.doc_hash |
无需 Phase 2,投递完成 |
gap |
存在缺失内容 | 携带 contents 进入 Phase 2 |
new |
服务端无此文档(首次投递) | 补传全部内容进入 Phase 2 |
缺口的作用域
!!! danger "安全硬约束:缺口池必须按 file_uri 限定"
missing = manifest 的 hash 集合 − **本文档旧树**的 hash 集合
**绝不可**改用全库 hash 池。若服务端跨文档解析 hash,客户端即可通过构造 manifest 引用**它无权访问的其它文档**的内容,并借由「服务端补齐」把该内容写入自己的文档,随后正常读取——这是一条**越权内容外泄路径**。
实现上不需要内核新增原语。内核 get_doc_by_file_uri() 已能拉取旧树全树,在内存中做集合差即可。不要直连内核表编写 SQL 查询 hash——DPE schema 由内核拥有,绕过其 API 会形成脆弱的隐式耦合。
4. Phase 2 — 投递
POST /v1/memory/dpe:commit
Content-Type: application/vnd.tfrs.dpe.v1+json
Content-Encoding: gzip
Authorization: Bearer <RS256 JWT>
Idempotency-Key: <client-uuid>
X-TFRS-Robot-Id: <rid>
{
"manifest": { "…同 §3…" },
"contents": [
{
"content_hash": "e5f6a7…",
"element": { "…完整 element payload(含 category / text / ele_metadata)…" }
}
]
}
contents[].content_hash 有两个用途:① 与 manifest 中的槽位匹配;② 作为早失败断言(见 §5 权威模型)。
响应
{
"status": "applied",
"doc_hash": "f7a8b9…",
"hash_strategy_uri": "hash-strategy://default?algo_version=v1",
"counts": {
"elements_added": 3,
"elements_removed": 1,
"elements_synced": 0,
"elements_unchanged": 996,
"llm_calls_saved": 1992
}
}
status 取值 created / unchanged / updated / rebuilt / applied,counts 直接映射内核 UpsertResult。
!!! tip "返回的 doc_hash 是服务端权威值"
客户端应存储该值作为下次投递的 base_doc_hash。协议因此不需要另建版本号系统——内容寻址 hash 本身就是现成的乐观并发令牌。
5. 权威模型:服务端重算
这是本协议最重要的设计决策。
| 数据 | 权威方 | 客户端提供的值用于 |
|---|---|---|
| element 内容 | 客户端 | 直接写入 |
content_hash |
服务端(attach() 重算) |
① 匹配 manifest 槽位 ② 早失败断言 |
page_hash / doc_hash |
服务端 | 仅 Phase 1 协商,永不入库 |
服务端收到 Phase 2 请求后的处理:
- 构造
Document,传入由hash_strategy_uri解析出的 strategy 实例。内核Document的构造校验器会据此自动调用HashStrategy.attach(),重算三层 hash 与seq_in_page。 - 调用内核
upsert_doc()—— 唯一的 reconcile 入口。
由此得到的性质
!!! tip "协议是 fail-safe 的" 客户端 hash 算错、算偏、甚至伪造,都不会污染数据库。最坏情况是协商未命中、多传输了一些内容,服务端最终仍会算出正确答案。
这是「服务端重算」相对于「信任客户端 hash + 校验后拒绝」的关键优势:后者需要定义失败恢复路径,前者不需要。
唯一的 fail-loud 点
contents[].content_hash 与服务端重算结果不符 → 立即拒绝(DPE_CONTENT_HASH_MISMATCH)。
理由:这是 SDK 实现存在缺陷的信号,必须在边界上拦截,绝不能落库。一旦错误的 hash 进入数据库,Merkle 不变量即告失守,此后所有同步都会出错。
第 0 条校验规则
!!! danger "必须显式拒绝客户端提交 hash 字段"
接口必须拒绝客户端将 doc_hash / page_hash / content_hash / seq_in_page 作为文档字段提交。hash_strategy_uri 只允许出现在 manifest 顶层作为协商参数。
内核 `Document` 的构造校验器存在**第二条路径**:若 data 自带 `hash_strategy_uri` + `doc_hash`,将跳过 `attach()` 直接通过校验(DB 读回路径)。外部数据不可信,必须钉死走 attach 路径,否则客户端可自证 hash 混入数据库。
6. 认证与授权
复用项目既有的 RS256 JWT + scope 体系。建议新增 scope dpe:push。
!!! warning "不要复用 config:write"
复用会让外部集成方获得改写机器人配置(LLM 密钥、工具、链)的权限——这是提权。外部集成方只应能投递内容。
新增 scope 的成本需计入:scope 词表 SoT 在 TFRSManager(M0/TFRM-164),需跨项目协调,并需 bump CONTRACT_VERSION、更新 CLAUDE.md 的 Contract Registry 表。
路由挂载需在 main.py 挂 enforce_scope,并在 security/scope_contract.py 的 _RULES 中登记(默认拒绝 CI 会拦截未分类路由)。
7. 错误模型
协议错误寄生在项目既有的 {code, message, data} 信封内(TFRS-481 契约,唯一构造点为 error_envelope.py::error_response)——不引入 RFC 9457 第二套错误体,那会与前端 BFF 的解析逻辑冲突。
code 承载协议错误码,data 承载机器可读细节。
| HTTP | code |
可恢复 | data 内容 |
|---|---|---|---|
| 400 | DPE_MANIFEST_INVALID |
否 | violations[] |
| 400 | DPE_CONTENT_HASH_MISMATCH |
否 | expected / actual / element_index |
| 400 | DPE_HASH_STRATEGY_UNSUPPORTED |
否 | accepted[](服务端 allowlist) |
| 409 | DPE_INSUFFICIENT_CONTENT |
是 | missing_content_hashes[] |
| 413 | DPE_PAYLOAD_TOO_LARGE |
否 | max_payload_bytes |
| 415 | DPE_UNSUPPORTED_PROTOCOL_VERSION |
否 | supported[] |
| 429 | DPE_RATE_LIMITED |
是 | 附 Retry-After |
| 503 | DPE_ROBOT_UNAVAILABLE |
是 | 附 Retry-After |
!!! warning "两条硬约束"
1. 绝不使用 422 —— 项目的 422 被 RequestValidationError 占用且有意保留 pydantic 原生形状(TFRS-385)。协议错误一律用 400。
2. 绝不动 RequestValidationError 处理器 —— 两个错误契约不要「顺手统一」。
8. 幂等与重试
| 机制 | 做法 |
|---|---|
| 幂等键 | Idempotency-Key: <client-uuid> 头。服务端按 (scope, rid, key) 去重,并回放首次响应 |
| 并发语义 | last-writer-wins,整文档覆盖。base_doc_hash 仅诊断;不匹配时在响应中回 base_doc_hash_matched: false 告警,不报错 |
| 重试 | negotiate 天然幂等(只读);commit 依赖幂等键。429 / 503 按 Retry-After 退避 |
!!! note "并发语义为何是「无合并」" 服务端以 manifest 的结构 + 补传内容整体重建文档,与全量推送语义完全一致。协议不引入任何新的并发语义——这是它能保持简单、且相对全量推送「零行为差异」的保证。
9. 版本治理与废弃
| 场景 | 做法 |
|---|---|
| 新增可选字段 | 同协议版本内直接添加。接收方 must-ignore 未知字段 |
| 破坏性变更 | 新 media type application/vnd.tfrs.dpe.v2+json;旧版至少保留 2 个发布周期 |
| 算法版本废弃 | 能力文档标 deprecated_at;协商时服务端回 Deprecation / Sunset 响应头(RFC 8594) |
| 已存文档使用旧算法 | 不需要迁移 —— 内核 hash_strategy_uri 不匹配时自动走 _full_rebuild。协议只需在响应中如实回传 hash_strategy_uri,客户端自行发现版本变化 |
!!! warning "must-ignore 与 must-understand 必须逐字段明确" 一刀切 must-ignore 对数据完整性协议是危险的(静默丢字段);一刀切 must-understand 则断掉前向兼容。
规则:**结构承载字段 must-understand(未知即拒绝),`x-` 前缀扩展位 must-ignore。**
10. 明确不支持
以下项在 v1 中明确不支持,写进规范以避免第三方猜测:
| 项 | 原因 |
|---|---|
image-source 策略 |
该策略的 doc_hash = source_file_hash,需要源文件字节;而 DPE-only 投递没有源字节。这是唯一可能需要向 TFROB 提能力需求的地方——若业务必须支持,需内核允许客户端自带 source_file_hash |
image_path 图片通道 |
现有链路强制图片相对路径以 {robot_id}/ 开头(隔离规则)。新端点不复用该检查,隔离规则需另行设计。v1 直接禁止 image_path,要求使用 image_base64 |
file_uri 命名空间冲突 |
file_uri 在 PG 中 UNIQUE。外部系统 URI 必须走保留 scheme(推荐 dpe://{tenant}/{path})与内部 feishu / cos 源隔离,否则可能撞键 |
| 部分提交 / 断点续传 | 内核 upsert_doc 是整文档事务,无法部分提交。靠幂等键 + gzip 缓解;续传列为 v2 候选 |
11. 相关文档
- DPE 投递协议总览
- Hash 算法 wire 契约
- A2A 结构化进展信封 —— 另一份面向外部系统的协议规范,错误信封与版本治理惯例可对照