DPE Connector 契约¶
状态:定稿——§6「运行边界」由 Issue #34 定稿(经 Issue #39 补充 §6.3 / §6.5 的 uri_prefix 规范化不动点,经 Issue #105 补充 §6.3 的插件进程组清理),§1–§5、§7–§8 由 Issue #41 定稿(经 Issue #80 补充 §4.1.1 的 config_schema 正则子集,经 Issue #95 同步 §6.6 的
file_type语法措辞,经 Issue #94 将 §4.1 / §4.4 的 JSON 嵌套深度统一为 core.md §2.8 的计数口径);全文关键词 MUST / MUST NOT / SHOULD / SHOULD NOT / MAY 按 RFC 2119 理解。 依据:docs/plan/v1-plan.md §11。 这是独立的中立规范,不属于 Core:核心投递协议完全不知道 connector 的存在(类比 git 与 remote-helper)。 Issue #2 已关闭:平台不引入 connector 运行环境,connector 由用户自行开发、自行部署;托管运行若将来需要,另立独立产品。v1 官方 connector 只有 Git connector(本仓connectors/git/)。
0. 定位¶
- 运行器负责推送,插件只负责产出。connector(插件)产出变更、Document、删除意图、移动意图;协商、暂存、CAS、状态缓存与 DPE 凭证全部由运行器掌握,插件拿不到 remote 凭证。
- 同一个 connector 既可由用户用 SDK 自带的独立运行器(
dpe-run)自行部署,也可托管在某个宿主中运行——宿主只是本契约的另一个运行器实现。 - v1 不开放租户自定义 connector 在托管环境运行,但契约设计必须保证将来开放时只需扩展、不需重构。
1. 术语与角色¶
- 插件(connector):产出源内容的独立进程(§6.1)。它持有数据源凭证,按 §2 产出条目,不接触 DPE remote。
- 运行器(runner):启动并驱动插件、校验其产出、计算 hash,并经 DPE 协议(core.md、http.md)向 remote 投递的一方。它持有 remote 凭证、实例定义与本地游标缓存。
- 宿主(host):以服务形态管理多个实例的运行器(如将来的托管产品)。对本契约而言宿主就是运行器;它的多租户、计费与权限模型不在本契约内(§9)。
- 独立运行器(standalone runner):以命令行进程形态运行、每次调用至多执行一轮的运行器,如 SDK 自带的
dpe-run。独立运行器除满足适用于全部运行器的条文外,还 MUST 满足标注「独立运行器规范档」的条文(§4.4、§5.2、§7.4–§7.6),使同一份实例定义能被不同语言的独立运行器执行,其输出能被同一套工具消费。 - 内容源实例(实例):一个插件、一份配置、一组凭证引用、一个 URI 前缀与一个 remote 的绑定(§4.4)。一个实例对应一个插件进程(§6.1),以及一份本地游标缓存(§6.8)与同步状态(§7.2)。实例的内容身份由 remote URL、URI 前缀、实例配置(按 RFC 8785 JCS 比较)与插件的
name/version(§4.1)组成;任何一项变化,运行器 MUST 重启插件并丢弃该实例的本地游标缓存(§6.3)。凭证(引用与取值)、启动命令、冲突策略与运行器调优参数不属于内容身份。 - remote:实例投递的 DPE 服务端,以基 URL 标识(http.md §1)。
- 轮(round):一次完整的按批枚举与投递(§6.4)。
- 清单(manifest):插件随分发附带的静态声明文件(§4.1)。
2. 插件产出模型¶
本节规定产出的语义;线格式见 §6.5–§6.7,游标见 §6.8。
- 插件产出四类条目:文档、删除意图、移动意图与错误(§6.5)。
- 文档复用 core.md §2 的内容模型。DPE 只表达内容面:插件产出的每个字段都是源内容,都进 hash(契约 1 §2);治理属性(如访问控制)与服务端衍生物(如抽取产物、关键词)MUST NOT 出现在任何字段中。
- 易变字段:插件 SHOULD NOT 把不随内容变化的易变源字段(浏览计数、最近访问时间、在线状态、同步时间戳等)放进 metadata,否则每轮同步都会制造内容变化(契约 1 §2);同理 SHOULD NOT 在元素 metadata 中重复骨架已表达的位置(页码、页内序号),否则插入一页会让后续元素对象全部重传(core.md §2.4)。
- blob 字节:可携带 blob 的元素(契约 1 §4.1)的字节由插件用自己持有的数据源凭证取得,以内联字节或句柄产出(§6.6、§6.7),运行器分块读取后自行计算
sha256:引用并上传。运行器 MUST NOT 自行解引用数据源 url(契约 1 §4.2)。字节确实无法取得时blob留空,url 等源信息照常放在元素 metadata 中。 - 删除与移动 MUST 以显式的
delete/move条目产出;运行器 MUST NOT 以「未出现即删除」推断(§6.5)。 - 枚举方式:全量枚举,或基于源端增量游标的增量枚举(§6.8)。跨轮的差只经游标表达,游标只存于运行器本地缓存;插件 MAY 持有加速用的本地缓存(如源数据的本地副本),但该缓存丢失时 MUST 仍产出正确结果,MUST NOT 依赖进程外的自有持久状态来表达删除与移动。
- 确定性:插件 SHOULD 保证同一源状态的两次枚举产出相同的内容与顺序(§6.5);这是收敛判据(§8.3)的前提。
3. URI 前缀绑定¶
- 每个实例 MUST 绑定一个 URI 前缀,在创建实例时由宿主分配或由实例定义(§4.4)给出。前缀 MUST 是符合 core.md §1.1 文法的 URI,且是规范化不动点(§6.3)。
- 运行器 MUST 拒绝越界的文档、删除与移动条目并如实上报(判定规则见 §6.5)。服务端的前缀授权(core.md §5.1)是独立的第二道防线,运行器 MUST NOT 以它代替本地判定。
- 前缀不重叠:同一运行器管理的多个实例(独立运行器:同一状态目录中登记的实例,§4.4)投递到同一 remote(实例定义中
remote.url相同)时,任意两个实例的前缀 MUST NOT 互为前缀(含相等);运行器 MUST 在创建或启动实例时检查并拒绝(§7.1 实例级)。否则一个实例的删除或移动会落入另一个实例的范围。跨运行器的重叠无法由运行器察觉,由服务端前缀授权兜底。 - 前缀变更:前缀属于实例的内容身份(§1)。前缀变化后,原前缀下已投递的文档 MUST NOT 被隐式删除或迁移;需要清理或迁移时,由使用者显式执行(core.md §4 的 delete / move)。
4. 配置与凭证¶
4.1 清单¶
插件 MUST 随分发附带清单:名为 dpe-connector.json 的 UTF-8 JSON 文件(I-JSON,同 core.md §2.8 第 0 步)。清单是静态声明,宿主与运行器 MUST 能在不启动插件的前提下读取它(渲染配置表单、校验配置、确定所需凭证);取得清单 MUST NOT 依赖执行插件。
{ "manifest_version": 1,
"name": "git-connector",
"version": "0.1.0",
"description": "以 Git 仓库为数据源",
"protocol_versions": ["dpe-connector/1"],
"config_schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"repo": { "type": "string" },
"ref": { "type": "string", "default": "main" } },
"required": ["repo"],
"additionalProperties": false },
"secrets": [
{ "name": "GIT_TOKEN", "description": "读取私有仓库的访问令牌", "required": false } ] }
| 字段 | 必填 | 说明 |
|---|---|---|
manifest_version |
是 | 整数;本规范定义 1。运行器遇到不认识的版本 MUST 拒绝 |
name |
是 | 非空字符串;MUST 与 initialize 响应的 plugin.name 相同 |
version |
是 | 非空字符串;MUST 与 initialize 响应的 plugin.version 相同 |
description |
否 | 字符串,供展示 |
protocol_versions |
是 | 非空字符串数组:插件支持的线协议版本(§6.2) |
config_schema |
是 | JSON Schema(draft 2020-12)对象,根 MUST 为 "type": "object";插件无配置时为 {"type": "object", "additionalProperties": false} |
secrets |
否 | 数据源凭证声明数组,缺省为 [];每项为 {name, description?, required?},见下 |
- 清单文件的 JSON 嵌套深度(计数口径见 core.md §2.8 第 0 步,全协议只有这一处定义)MUST ≤ 64;超出即拒绝(
manifest_invalid,§7.4),在读取阶段判定、不进入任何语义校验。上界使合法文件在各实现的默认 JSON 解析限额(如 serde_json 的递归限额 128)内也能被读取。 - 清单是封闭 schema:出现未定义的成员(含
secrets项内)或成员类型不符时,运行器 MUST 拒绝该清单(manifest_invalid,§7.4)。 config_schemaMUST 自包含:$ref只能引用本文档内的位置,运行器 MUST NOT 解析外部引用(不得因校验配置而访问网络或文件);format只作注解,运行器 MUST NOT 据此判定校验失败——使不同实现对同一配置得出相同的校验结论。插件 SHOULD 在根上设"additionalProperties": false。它 MUST NOT 声明凭证字段:实例配置经线协议原样交给插件(§6.3),不是凭证通道;凭证一律经secrets声明、按 §4.3 注入。config_schema是封闭 schema(与 DPE 的三层对象同理):只允许 §4.1.1 列出的关键字,$ref只允许 §4.1.1 规定的唯一形式;pattern的值与patternProperties的每个键 MUST 属于 §4.1.1 的正则子集。content 词(contentEncoding、contentMediaType)与format一样只作注解:运行器 MUST NOT 据此判定校验失败;contentSchema不允许出现(见 §4.1.1)。secrets每项:name(必填):注入插件进程的环境变量名,MUST 匹配^[A-Z][A-Z0-9_]*$,在清单内唯一;MUST NOT 以DPE_开头(该前缀保留给运行器);MUST NOT 与 §4.3 基础环境中的变量同名。description(可选):字符串,供宿主渲染表单。required(可选):布尔,缺省 true。- 身份核对:initialize 成功后,运行器 MUST 核对
plugin.name/plugin.version与清单相同、选定的protocol_version在清单的protocol_versions中;不符即终止实例(plugin_mismatch)。清单与实际可执行文件漂移时,依据清单完成的配置校验与凭证注入都不再可信。
4.1.1 config_schema 的正则子集¶
pattern 的值与 patternProperties 的每个键 MUST 属于下述可移植子集——判定只依据本节的文法与结构上界,与本地正则引擎能否编译无关;不属于(含语法错误、超出上界)时运行器 MUST 拒绝该清单(manifest_invalid,§7.4)。子集以 RFC 9485(I-Regexp,为跨引擎互操作定义的正则子集,JSONPath(RFC 9535)即采用它)为底稿,与 §4.1 其余部分同理,目的是使不同实现对同一配置得出相同的校验结论。
与 RFC 9485 的差异:
| 项 | RFC 9485 | 本子集 |
|---|---|---|
| 锚点 | 无(^、$ 是普通字符) |
^ 只允许在顶层每个分支的开头、$ 只允许在顶层每个分支的结尾;其他位置 MUST 用 \^、\$ 转义 |
| 多字符转义 | 无 | 支持 \d \D \w \W \s \S(ASCII 语义,见语义表) |
\xHH |
无 | 支持;H 为十六进制(大小写均可),值为 U+0000–U+00FF |
(?:…) |
不属于文法 | 允许,与 (…) 同义 |
\p{…}、\P{…} |
支持 | MUST NOT 使用:各实现内嵌的 Unicode 版本不同步,展开为码点集合也会随版本漂移 |
| 类内集合序列 | [&&]、[--]、[~~] 等均为合法类 |
MUST NOT 出现未被转义的 &&、--、~~:部分引擎把它们当类集合运算,需保证同一文本在任何实现下同义 |
| 匹配语义 | 整串匹配(XSD 语义) | 未锚定(search):串中存在一处匹配即通过;^、$ 是作者获得整串约束的手段 |
. 的语义 |
由 §5.3 映射为 [^\n\r] |
[^\n\r](与该映射一致) |
文法(ABNF;其中的「字符」是 Unicode 标量值):
pattern = top-branch *("|" top-branch)
top-branch = ["^"] *piece ["$"] ; 锚点只允许出现在顶层分支的首/尾
branch = *piece ; 分组内
piece = atom [quantifier]
atom = "." / literal / escape / shorthand / class / "(" branch *("|" branch) ")"
/ "(?:" branch *("|" branch) ")"
quantifier = "*" / "+" / "?" / "{" 1*DIGIT ["," [1*DIGIT]] "}"
shorthand = "\" ( "d" / "D" / "w" / "W" / "s" / "S" )
escape = "\" ( "(" / ")" / "*" / "+" / "-" / "." / "?" / "[" / "\" / "]" / "^"
/ "{" / "|" / "}" / "$" / "n" / "r" / "t" ) / "\x" HEXDIG HEXDIG
class = "[" ["^"] ("-" / class-item) *class-item ["-"] "]"
class-item = class-char ["-" class-char] / shorthand
class-char = ( %x00-2C / %x2E-5A / %x5E-D7FF / %xE000-10FFFF ) / escape ; 除 -、[、\、] 外的标量值,或转义
其中 literal(用于原子位置)是除元字符(\、.、^、$、|、?、*、+、(、)、[、]、{、})外的任意 Unicode 标量值;class-char(用于类内)是除 [、]、-、\ 外的任意 Unicode 标量值——类内元字符(.、^、$、|、?、*、+、(、)、{、})没有特殊含义,照字面书写即可([.]、[a*b]、[^a$] 都合法)。两者都不可以是孤立代理项 U+D800–U+DFFF(不是标量值,出现即越界——清单文件经 I-JSON 本已不允许,此处对在内存中构造的清单同样成立);类内另加:
- 字面
[、]MUST 转义(\[、\]),\MUST 转义(\\); - MUST NOT 出现相邻的
&&、--、~~三组字符(按字面文本判定:紧邻的两个同字符一律违例,无论前一个是否被转义——[a\--b]也违例);写出相邻字符时用\xHH(如[a\x26\x26b]、[\x2D\x2D]合法;&、~不在转义白名单里,不能写\&);-只能是类首/类尾的字面字符(或\-、\x2D)或区间分隔符; - 区间的两个端点 MUST 是单字符(字面或转义,含
\xHH),MUST NOT 是简写类;端点 MUST 满足 c1 ≤ c2(按码点序); ^仅在类内首位表示否定,其他位置是字面字符。
封闭关键字子集:config_schema(含 $defs 中的子 schema 与所有嵌套位置)MUST 只使用下表列出的关键字;出现未列出的关键字($id、$anchor、$dynamicAnchor、$dynamicRef、definitions、dependencies、additionalItems、prefixItems、contains、minContains、maxContains、dependentSchemas、unevaluatedItems、unevaluatedProperties、contentSchema、multipleOf 等,无论出现在哪个位置)一律拒绝该清单(manifest_invalid,§7.4)。与 DPE 的三层对象一样,config_schema 是封闭 schema:只用表达力够用的一小组关键字,未定义的关键字不猜语义——这正是「不同实现对同一配置得出相同结论」的前提。允许 schema 的位置一律允许布尔 schema(true / false,如 "additionalProperties": false)。
| 组 | 允许的关键字 |
|---|---|
| 结构与组合 | type、properties、patternProperties、additionalProperties、required、propertyNames、items(仅单个 schema,数组形式拒绝)、allOf、anyOf、oneOf、not、if、then、else、$defs(仅根,见下) |
| 引用 | $ref(唯一形式见下) |
| 方言 | $schema(仅根;MUST 缺省或为 draft 2020-12,…/2020-12/schema 与带空片段 …/2020-12/schema# 等价) |
| 通用 | enum、const |
| 字符串 | pattern(属于本节的正则子集)、minLength、maxLength |
| 数值 | minimum、maximum、exclusiveMinimum、exclusiveMaximum |
| 数组 | minItems、maxItems、uniqueItems |
| 对象 | minProperties、maxProperties、dependentRequired |
| 注解 | title、description、default、examples、deprecated、readOnly、writeOnly、format、contentEncoding、contentMediaType、$comment |
$defsMUST 只出现在根上;其值 MUST 是 schema,其中的关键字同样只允许本表。$refMUST 恰为"#/$defs/<名字>"一层:前缀 MUST 是字面的#/(不得写成%2F等编码形式),其后按 RFC 6901 §6 处理(先百分号解码、再按/拆分、最后处理~1、~0,顺序不可换);含非法百分号序列(%后不是两位十六进制)或~后不是0/1的片段 MUST 拒绝;解码后的指针 MUST 恰为$defs与<名字>两段(名字非空);#、锚点(#名字)、深于一层(#/$defs/a/b)、其他位置(#/properties/x)与外部 URI 一律拒绝;名字 MUST 在根的$defs中存在。$defs各条之间的引用关系 MUST 构成有向无环图(对$defs做一次拓扑检查即可)——环会在求值时无限递归,行为随实现而异;这同时排除了递归 schema(含经properties等位置回到自身的写法),这是有意的取舍:配置 schema 不需要递归。展开深度 MUST ≤ 64:根计 1,每进入一个 schema 位置再 +1,每经过一次$ref跳转再 +1 并沿引用进入目标继续计,取所有路径的最大值($defs已是 DAG,记忆化后线性可算)。未被引用的$defs条目不参与展开深度(它们从不被求值),深度一律从根计起。上界保证合法清单在任何实现的默认递归限额下都能完成求值:同时守住 JSON 深度与展开深度的最紧形状(63 层additionalProperties内联链、布尔叶子——两个上界恰好同时顶格)在解析期的元模式校验约需 500–525 帧(Python 默认限额 1000,留约两倍余量;求值阶段本身约 250–300 帧)。multipleOf与contentSchemaMUST NOT 使用:前者的整除判定在浮点与大整数上跨实现不一致(实测0.3对multipleOf: 0.1被判非整数倍、大整数受浮点舍入误判),后者在 JSON Schema 里是 schema 位置、各库会按 schema crawl,留作字段即是分歧面;需要倍率表达时用enum/const或上下界。- 注解词只作注解:运行器 MUST NOT 据此判定校验失败;
default、examples、enum、const等处的同名键是数据,MUST NOT 按关键字解析。
文法之外另有约束:
- 量词 MUST 紧跟一个原子,且每个原子至多一个量词;
*a、a**、a{2}{3}、{,3}都不合法({、}作字面 MUST 转义);{m,n}形式 MUST 满足 m ≤ n。 ^、$只允许出现在顶层分支的首/尾;a^b、(^a)、(a$)、(?:^a)都不合法,其他位置作字面 MUST 转义。- schema 自身 MUST 通过 draft 2020-12 元模式校验(本表之外的元层违例——如
type取值不认识、properties不是对象——同样拒绝该清单); - 结构上界:pattern 的长度 MUST ≤ 1024 个 Unicode 标量值;分组嵌套深度 MUST ≤ 64;展开规模 MUST ≤ 4096。分组嵌套深度按括号层数计:最外层分组计 1,不含分组的 pattern 计 0(量词与字符类不计入;但下述上界对它们另有约束)。展开规模按书写形式逐项、递归地计数(不去重、不做集合归并——归并结果取决于各实现的集合算法,会在边界上分歧):字面按码点计 1(
é计 1,不论其 UTF-8 字节数);转义、.、简写各计 1;字符类的取反^不计,其余按写出的每一项计 1([aaaa]计 4、[a-cb]计 2,类内简写各计 1);锚点计 0、不参与量词;分组至少计 1(空组计 1——否则「零尺寸原子 × 大计数」会绕过本上界);分组为组内之和与 1 的较大者;连接、选择为各子项之和;量词为「因子 × 被量化原子的展开规模」,因子为:{m}记 m、{m,n}记 n、{m,}记 m、*、+、?记 1;嵌套时外层因子乘内层规模,如(a{2}){3}为 6。深度取 64 是因为各引擎的解析器嵌套限额(如 RE2 系的 250)除分组外还计入量词与字符类:最坏形状(每层分组带量词)的解析嵌套约为分组层数的两倍、实测在 125 层即触及默认限额,64 层取其约一半。
语义:
| 构造 | 语义 |
|---|---|
. |
除 U+000A、U+000D 外的任意单个 Unicode 标量值 |
\d / \D |
[0-9] / 其补(在全体 Unicode 标量值上取补,下同) |
\w / \W |
[0-9A-Za-z_] / 其补 |
\s / \S |
[\t\n\v\f\r ](U+0009–U+000D 与 U+0020)/ 其补 |
^ / $ |
文本的开头 / 结尾;无多行语义,$ MUST NOT 匹配末尾换行之前 |
\xHH |
值为 U+00HH 的单字符 |
匹配方式:未锚定(search)——在串的任意位置找到一处匹配即通过;patternProperties 的键以同一方式匹配属性名。区分大小写、按码点(不是字节或 UTF-16 码元)、不使用任何标志;匹配的字符宇宙是 Unicode 标量值(类区间跨越代理区时不含代理码位)。
结构上界使编译规模有确定上界:子集内的任何 pattern MUST 被接受并正确匹配,实现 MUST NOT 以本地引擎的资源上限为由拒绝(实现方式不限,例如先转译为显式字符类/码点区间再交给本地引擎)。这是可用性约束,不是安全边界——清单来自插件,而插件本来就能执行任意代码;配置来自使用者。
4.2 实例配置¶
- 实例配置是一个 JSON 对象,经 initialize 的
instance.config原样交给插件(§6.3)。 - 启动插件前,运行器 MUST 按清单的
config_schema(draft 2020-12 语义)校验实例配置;不通过时 MUST NOT 启动插件,按实例级失败上报(config_schema_violation,§7.1)。 - 运行器 MUST 原样传递配置,MUST NOT 填充
default或做任何改写——default等注解只供宿主渲染表单,缺省值由插件自行解释。这样配置的比较(§1 内容身份)与插件看到的值始终一致。 - schema 无法表达的语义约束(如仓库不存在、ref 无效)由插件在 initialize 时以
-32005(invalid_config)拒绝(§6.3)。
4.3 凭证¶
两类凭证严格分属:remote 凭证只属于运行器,用于向 remote 鉴权(http.md §6);数据源凭证只属于插件,用于读取内容源与 blob 字节。
来源引用:实例定义、宿主存储等配置载体中,凭证 MUST 以来源引用表示,MUST NOT 含凭证值本身——声明可以提交进版本库,凭证值不可以。独立运行器 MUST 支持以下两种引用(宿主 MAY 以自己的密钥存储替代):
{"env": "NAME"}:取运行器进程环境变量NAME的值;{"file": "path"}:取文件的全部内容(UTF-8),去掉末尾的一个 LF(或 CRLF);相对路径的基准见 §4.4。
引用对象是封闭 schema,恰含上述成员之一。解析结果为空字符串视同无法解析。
解析时机:运行器 MUST 在每次启动插件进程时重新解析数据源凭证(remote 凭证同理,在每次需要时从引用解析),MUST NOT 持久化解析值,MUST NOT 跨进程启动缓存。凭证轮换因此只需在轮边界重启插件(§6.3)即生效;凭证不属于实例的内容身份(§1),轮换 MUST NOT 导致丢弃游标。
缺失:required 为 true 的凭证无法解析(引用缺失、变量未设、文件不可读、值为空)时,运行器 MUST NOT 启动插件,按实例级失败上报(secret_missing),报告只含凭证名。required 为 false 的凭证无法解析时,运行器 MUST NOT 注入该变量(MUST NOT 以空字符串顶替),由插件按缺失处理。
注入:数据源凭证 MUST 且只能以环境变量注入插件进程,变量名为清单声明的 name;运行器 MUST NOT 经命令行参数、实例配置、协议消息或工作目录文件传递数据源凭证。插件进程的环境 MUST 恰由以下三部分组成,运行器 MUST NOT 让插件继承自身的其余环境变量(运行器环境中可能有 remote 凭证,§6.1):
- 基础环境:运行器自身环境中存在的
PATH、HOME、LANG、LC_*、TZ、TMPDIR;Windows 上另加SYSTEMROOT、TEMP、TMP、USERPROFILE、PATHEXT、COMSPEC; - 已解析的数据源凭证;
- 实例定义的
plugin.env(非秘密的字面量,§4.4)。
remote 凭证不可达:数据源凭证的引用 MUST NOT 与 remote 凭证的引用指向同一来源(同名 env,或解析为同一路径的 file);运行器 MUST 检测并拒绝(definition_invalid)。连同上面的环境构造,运行器不经任何通道把 remote 凭证交给插件。v1 不要求进程隔离(§6.1):插件与运行器以同一用户运行时,file 型 remote 凭证能否被插件读取取决于文件权限;需要更强的隔离时,由部署方或宿主在进程之外提供(不同用户、容器等)。
不外泄:凭证值 MUST NOT 出现在运行器的日志、轮报告、错误消息与诊断中(§7)。运行器无法识别插件自行写出的凭证,因此插件 MUST NOT 把凭证值写入 stdout、stderr 或任何产出;运行器转发插件 stderr 前 SHOULD 把已知的凭证值替换为固定标记。
4.4 实例定义(独立运行器规范档)¶
独立运行器 MUST 接受以下格式的实例定义:一个 UTF-8 JSON 文件(I-JSON),描述一个实例。定义文件(含 config)的 JSON 嵌套深度(计数口径见 core.md §2.8 第 0 步,全协议只有这一处定义)MUST ≤ 64;超出时按 definition_invalid 拒绝(§7.4),在读取阶段判定。宿主 MAY 以自己的存储表达同样的信息,但 MUST 满足 §3–§4.3 的要求。
{ "definition_version": 1,
"id": "acme-repo",
"remote": { "url": "https://dpe.example.com/remotes/acme",
"credential": { "env": "ACME_DPE_AUTHORIZATION" } },
"uri_prefix": "git://acme/repo/",
"plugin": { "manifest": "plugins/git/dpe-connector.json",
"command": ["dpe-git-connector"],
"env": { "GIT_SSL_CAINFO": "/etc/ssl/certs/ca.pem" } },
"config": { "repo": "https://example.com/acme/repo.git" },
"secrets": { "GIT_TOKEN": { "file": "/run/secrets/git_token" } } }
| 字段 | 必填 | 说明 |
|---|---|---|
definition_version |
是 | 整数;本规范定义 1。不认识的版本 MUST 拒绝 |
id |
是 | 实例的本地标识,匹配 ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$,在同一状态目录内唯一;是本地状态(游标缓存、同步状态、内容身份指纹、排他锁)与日志(§7.5)的隔离键 |
remote.url |
是 | remote 的基 URL(http.md §1);MUST NOT 含 userinfo |
remote.credential |
否 | 来源引用(§4.3)。解析值是完整的 Authorization 请求头值(如 Bearer …),运行器原样发送、不解释方案(http.md §6);缺省表示不发送 Authorization。Authorization 头无法承载的方案(如 mTLS)由运行器实现经 runner 扩展 |
uri_prefix |
是 | 实例 URI 前缀(§3) |
plugin.manifest |
是 | 清单路径(§4.1) |
plugin.command |
是 | 非空字符串数组,启动插件的 argv。argv[0] 含路径分隔符时按路径解析,否则在 PATH 中查找;运行器 MUST 直接执行,MUST NOT 经 shell 解释 |
plugin.env |
否 | 对象,字符串到字符串:注入插件的非秘密环境变量(§4.3)。变量名 MUST NOT 以 DPE_ 开头,MUST NOT 与基础环境或已声明的凭证同名 |
config |
是 | 实例配置(§4.2),可为 {} |
secrets |
否 | 对象:键为清单声明的凭证名,值为来源引用(§4.3)。出现清单未声明的名字 MUST 拒绝 |
conflict_policy |
否 | 对象 {commit?, delete?}:写入冲突的策略(§7.2)。commit 取 "source_wins"(缺省)或 "report";delete 取 "report"(缺省)或 "source_wins" |
runner |
否 | 对象,留给运行器实现的调优参数(超时、批大小、重试上限等);运行器 MUST 忽略不认识的键,其内容 MUST NOT 改变本契约规定的行为 |
- 实例定义是封闭 schema(
runner内部除外):出现未定义的成员或成员类型不符时,运行器 MUST NOT 启动插件,按实例级失败上报(definition_invalid)。 - 相对路径(
plugin.manifest、file引用,以及含路径分隔符的argv[0])以实例定义文件所在目录为基准。 - 实例定义 MUST NOT 含凭证值(§4.3);
remote.url中的 userinfo、plugin.env与config都不是凭证通道。 - 状态目录:独立运行器 MUST 把每个实例的本地状态(游标缓存、同步状态与意图日志(§7.2)、内容身份指纹与排他锁)按
id存放在一个状态目录中;状态目录的位置由运行器实现决定,SHOULD 可由调用方指定。同一状态目录中登记过的实例就是该独立运行器「管理的实例」:id在其中唯一,§3 的前缀不重叠也在其中检查——启动实例时,运行器 MUST 把它的remote.url与uri_prefix同状态目录中其余实例的登记比较,重叠即拒绝(definition_invalid)。登记与重叠检查 MUST 原子完成(如在状态目录级的锁内进行),以免两个前缀重叠的新实例同时首次启动时都通过检查。 - 内容身份指纹:运行器 MUST 把游标与产生它的实例内容身份(§1)一并持久化。启动时内容身份与已存的不同(如沿用同一
id改了config或uri_prefix),MUST 视为缓存丢失:丢弃游标与同步状态,以全量轮开始(§6.8),并更新登记。
5. 生命周期与调度¶
5.1 实例操作¶
| 操作 | 语义 |
|---|---|
| 创建 | 绑定前缀(§3),校验清单(§4.1)、实例配置(§4.2)与凭证引用(§4.3)。不开始轮 |
| 校验 | 启动插件、完成 initialize 与身份核对(§4.1),获取 remote 的 capabilities(http.md §4.1),随后 shutdown(§6.3)。MUST NOT 发出 scan,不产生任何写入 |
| 同步一轮 | §6.4–§6.8 |
| 暂停 | 不再开始新的一轮;进行中的轮 MAY 完成或中止(中止在提交前无副作用,§6.4) |
| 删除 | 丢弃本地游标缓存(独立运行器:从状态目录中移除该实例的状态与登记,§4.4)。MUST NOT 隐式删除或迁移 remote 上已投递的文档——清理是使用者的显式操作(core.md §4) |
5.2 调度、并发与重试¶
- 调度不属于本契约:何时开始一轮由运行器(宿主)决定。独立运行器每次调用至多执行一轮后退出;周期性或事件驱动的触发(CI、版本库钩子、webhook 处理器、外部定时器)由调用方承担。
- 单轮约束跨进程成立(§6.4):独立运行器(独立运行器规范档)解析实例定义得到
id后,MUST 先取得该实例的排他锁,再做其余实例级校验(清单、配置、凭证、前缀登记)与启动插件,并持有至本轮结束(含游标持久化);取不到锁时 MUST NOT 启动插件,以instance_busy结束(§7.6)。这是暂时状态而非实例级失败,调用方稍后重试即可。锁 MUST 在持有进程异常退出时自动释放(如操作系统提供的文件锁),以免残留锁永久阻塞实例。 - 重试:轮失败后的重试 SHOULD 采用有上限、带抖动的指数退避;remote 返回
Retry-After时 MUST 遵守(core.md §6)。实例级失败(§7.1)不改动实例定义、清单或凭证就无法恢复,MUST NOT 自动重试。独立运行器 MAY 在单次调用内有限次重试,也 MAY 直接以非零退出码返回、交给调用方重试。永久失败不得空转(§6.8)。
6. 运行边界¶
本节由 Issue #34 定稿。它定义运行器与插件之间的进程边界与语言无关线协议:任何语言实现的插件,只要遵循本节,就能被任何遵循本契约的运行器驱动。插件只产出源内容(§2),线协议上不出现 DPE 的 hash、契约版本与 remote 细节;校验、hash、协商、暂存、CAS、重试与凭证全部由运行器负责。
6.1 进程边界与信任模型¶
- 插件 MUST 是独立进程:由运行器启动,一个内容源实例对应一个插件进程;同一进程 MUST NOT 服务多个实例。语言无关性由此保证——插件不链接 SDK,也不要求与运行器同语言。
- 标准流分工:插件从 stdin 读协议消息、向 stdout 写协议消息;stdout MUST 只包含协议消息(不得混入日志、调试输出或 BOM),诊断日志写 stderr。运行器 MUST 持续排空 stderr(或把它导向自身 stderr),否则插件可能因管道写满而阻塞。
- 请求方向:v1 中所有请求都由运行器发起,插件只应答;插件 MUST NOT 主动发起请求或通知。单管道因此不会死锁;无长请求时,插件实现就是「读一行、处理、写一行」的循环,处理长请求期间仍 MUST 继续读取并处理控制消息(§6.7 的取消要求)。
- 信任模型:插件产出是不可信输入。运行器 MUST 全量校验(§6.6)并自行计算一切 hash,MUST NOT 把产出字段直接当作已校验的 DPE 对象;MUST NOT 因任意插件产出而崩溃(协议级无法继续的情形除外,§6.2)。
- 凭证边界:运行器 MUST NOT 经任何通道(命令行参数、环境变量、工作目录文件、协议消息)把 DPE remote 凭证交给插件;运行器 MUST NOT 自行解引用数据源 url(契约 1 §4.2)。数据源凭证由插件持有、只以环境变量注入插件(§4.3);凭证 MUST NOT 经线协议传递,运行器除按 §4.3 解析与注入外 MUST NOT 使用或记录数据源凭证。
- v1 隔离立场:自部署场景下插件以用户自身权限运行于用户自己的环境;本契约不要求运行器提供沙箱、资源配额或网络出口限制。将来托管产品由宿主在进程之外施加这些(本契约的进程边界与凭证边界不变),因此只需扩展、不需重构。
6.2 传输与分帧¶
- 线协议 MUST 是 JSON-RPC 2.0 over stdio:传输为插件的 stdin/stdout 一对有序字节流。
- 分帧 MUST 是 NDJSON:一条消息 = 一行紧凑序列化的 JSON(UTF-8、LF 结尾)。JSON 字符串内的换行按 JSON 规则转义,因此一行必然是完整消息。MUST NOT 输出多行或缩进格式;每条消息 MUST 立即冲刷;MUST NOT 发送空行;MUST NOT 使用 JSON-RPC 批量数组——一条消息 MUST 是单个 JSON-RPC 对象。
- 报文 MUST 是 I-JSON(同 core.md §2.8 第 0 步:无重复键、无孤立代理项)。空行或无法解析的行是协议错误,运行器 MUST 终止实例并如实上报,MUST NOT 跳过该行继续。
- 消息上限
max_message_bytes:由运行器在 initialize 中给出(单位 UTF-8 字节、含行尾 LF),进程存活期内不变。双方 MUST NOT 发送超过上限的消息;运行器收到超限消息 MUST 终止实例(无法安全解析,不得截断或跳过);插件收到超限请求 MUST 以-32600(invalid request)拒绝并尽力丢弃该行。运行器已知远端 capabilities 的max_payload_bytes时,上限 MUST NOT 小于它(使远端可接受的单个元素在 JSON 转义后仍能成行),并 SHOULD 留出充分余量。注意与max_payload_bytes的区分:后者是 DPE 服务端的请求体上限(http.md §4.1),两者独立。 - 协议版本为
dpe-connector/1,与 DPE 协议版本(dpe/1)、hash 契约版本(dpe1)相互独立。 - 报文字段:方法参数与结果中,未标注「可选 / 可缺省」的字段 MUST 出现且不得为
null;null仅在文中明确给出语义处有效(如 §6.4 的next、cursor,§6.5 的file_uri),其余情形按非法值拒绝。
6.3 初始化与进程生命周期¶
initialize MUST 是插件进程启动后运行器发出的第一条消息:
{ "jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {
"protocol_versions": ["dpe-connector/1"],
"max_message_bytes": 33554432,
"instance": {
"uri_prefix": "git://acme/repo/",
"config": { "repo": "https://example.com/acme/repo.git" }
},
"runner": { "name": "dpe-run", "version": "0.1.0" },
"remote_limits": { "max_payload_bytes": 8388608, "page_max_bytes": 268435456, "blob_max_bytes": 104857600 }
} }
| params 字段 | 必填 | 说明 |
|---|---|---|
protocol_versions |
是 | 运行器支持的线协议版本,按优先级排列 |
max_message_bytes |
是 | 单条消息上限(§6.2) |
instance.uri_prefix |
是 | 实例 URI 前缀,MUST 是符合 core.md §1.1 文法的合法 URI,且已是规范化不动点(normalize_file_uri(p) == p);运行器在启动实例前 MUST 校验,不满足即拒绝启动并如实上报。插件 SHOULD 据此构造 file_uri(§6.5) |
instance.config |
是 | 实例配置(§4.2:按清单的 config_schema 校验、原样传递;MUST NOT 含凭证),可为 {} |
runner |
否 | {name, version},供插件日志 |
remote_limits |
否 | 远端限额 {max_payload_bytes, page_max_bytes, blob_max_bytes}(http.md §4.1);运行器获取过 capabilities 时 SHOULD 提供,供插件做源→文档映射的切分决策(core.md §3.2) |
响应:
{ "jsonrpc": "2.0", "id": 1,
"result": {
"protocol_version": "dpe-connector/1",
"plugin": { "name": "git-connector", "version": "0.1.0" },
"capabilities": { "supports_cursor": true }
} }
| result 字段 | 必填 | 说明 |
|---|---|---|
protocol_version |
是 | 从 protocol_versions 中选定的版本,原样返回 |
plugin |
是 | {name, version},插件自报(可观测性) |
capabilities |
否 | v1 定义 supports_cursor(布尔,缺省 false,§6.8);运行器 MUST 忽略不认识的键 |
- 插件 MUST 从
protocol_versions中选择自己支持的一个;无一支持时 MUST 以-32004(version_unsupported)失败。运行器收到不在自己列表中的版本时 MUST 终止实例,MUST NOT 静默降级。 - 配置不可用时插件 MUST 以
-32005(invalid_config)失败。 - 握手先行:initialize 成功完成前,插件 MUST NOT 产出任何内容,MUST 以
-32600拒绝其他请求。
进程生命周期:
- 启动:运行器负责启动插件进程;插件进程的环境 MUST 按 §4.3 构造,独立运行器的启动命令见 §4.4;宿主取得可执行文件的方式属于宿主实现。插件 MUST NOT 依赖交互式终端(TTY)。
- 重启边界:运行器 MAY 在轮边界重启插件(含空闲期);MUST NOT 在一轮内重启,故障恢复除外(此时该轮作废,§6.4)。实例的内容身份(§1:remote、uri_prefix、实例配置、插件 name / version)变化时,运行器 MUST 重启插件并丢弃本地游标缓存(§6.8)。
- 正常关闭:运行器发送
shutdown请求;插件 MUST 停止当前工作、以结果响应,并尽快以退出码 0 退出。运行器随后关闭 stdin;宽限期内未退出则终止进程(先 TERM、后 KILL,作用于进程组)。 - stdin EOF:插件读到 stdin EOF 时 MUST 尽快退出(不得悬挂),这是运行器崩溃后不残留孤儿进程的保证。运行器 MUST 在关闭 stdin 后同时监视进程退出,MUST NOT 只等流 EOF(插件可能留有子进程持有 stdout)。
- 崩溃:插件异常退出(非零退出码或信号)时本轮作废,运行器 MAY 按自身策略重启并重试,并 MUST 如实上报。已提交的文档不受影响——投递是逐文档原子的(core.md §3.3),扫描轮本身幂等。
- 进程组清理:插件进程组终结时——实例结束时(正常关闭、插件崩溃后的收尾,或运行器中止本轮,§6.4),或插件进程按「重启边界」重启时——运行器 MUST 对插件进程组执行一次清理:先 TERM,宽限期后 KILL;不以管道是否仍被持有为条件——插件已退出、但留下不持有管道的残余进程时,清理同样发生。(理由:插件退出不代表它派生的进程已退出,这些进程仍占用系统资源、可能仍在访问数据源;不清理则每次插件重启都会把上一代的残余再泄漏一次。业界参照 systemd 的
KillMode=control-group与容器运行时的 PID 命名空间,回收的粒度都是实例整体而非主进程。) - 子进程与进程组:插件派生的子进程 SHOULD 留在插件的进程组内(§6.1:一个内容源实例对应一个插件进程),使终止与进程组清理作用于整个实例。主动脱离进程组(如自行
setsid)的进程不在运行器的清理保证范围内,其回收由插件负责。 - 分发与签名(v1 最低要求):自部署场景下插件由用户自行选择与部署,本契约不要求运行器做来源校验或签名验证。插件的打包、审核与签名属分发方(以及将来宿主产品)的责任,在协议之外完成(校验发生在启动前),因此托管产品也不需要改动线协议。
6.4 扫描轮、批次与背压¶
- 轮(round):一次按批拉取的序列构成一轮——从运行器发出轮首
scan开始,到插件在批次响应中给出新cursor(不再有next)结束。一个实例同时至多一轮;运行器 MUST NOT 在上一轮的产出全部落地(§6.8)之前开始下一轮。 - 轮首请求:
{"cursor": "…"}(省略或null表示全量枚举);MUST NOT 带next。 - 续批请求:
{"next": "…"},取上一响应给出的 token;MUST NOT 带cursor。 max_items:运行器给出的建议批大小上限(可选整数),插件 SHOULD 遵守。无论批次大小,单条消息 MUST NOT 超过max_message_bytes(§6.2)——批次内条目数由插件按序列化后的大小自行控制。
// 轮首("cursor" 为上一轮结束批给出的游标;省略或为 null 表示全量枚举)
{ "jsonrpc": "2.0", "id": 7, "method": "scan",
"params": { "cursor": "c-42", "max_items": 100 } }
// 续批("next" 为上一条响应给出的 token)
{ "jsonrpc": "2.0", "id": 8, "method": "scan",
"params": { "next": "n-2" } }
// 结束批:本轮结束,给出下一轮游标;"incremental" 标明本轮是否为增量枚举(§6.4)
{ "jsonrpc": "2.0", "id": 8,
"result": { "items": [ …产出项… ], "cursor": "c-43", "incremental": true } }
- 响应:
{"items": [ …产出项… ], "next": "…"?, "cursor": …?, "incremental": true|false}。 next为字符串 = 还有后续批,运行器以下一个scan带该 token 继续;缺省或null= 本轮结束(插件 SHOULD NOT 发送null)。中间批 MUST NOT 带cursor与incremental。- 结束批 MUST 带
cursor(字符串 = 新游标,§6.8;null= 本轮不提供游标)与incremental(布尔)。空批合法(items: [])。 incremental:本轮是否基于运行器给出的 cursor 增量枚举——运行器给了 cursor 且插件实际基于它枚举 ⇒ true;运行器未给 cursor(首轮、缓存丢失)或插件无法使用而回退为全量枚举(§6.8)⇒ false。incremental: false表示本轮只保证现存内容的完整枚举,此前的删除/移动不在表达范围内(插件 MAY 仍产出它能确定的删除/移动)。运行器收到incremental: falseMUST 如实上报并可观测,MUST NOT 视同删除/移动已同步。- 插件对未知或过期的
nextMUST 以-32602(invalid params)拒绝;运行器收到后 MUST 中止本轮,并以原 cursor 重开(MUST NOT 把 token 当作可忽略的提示)。 scan收到error响应(如-32003)时本轮失败:运行器 MUST 如实上报、MUST NOT 推进游标(§6.8),退避后重试。- 运行器 MAY 对进行中的
scan设置超时并在超时后cancel(策略属运行器实现,§5.2);插件对cancel的义务见 §6.7。 - 背压:插件只在收到
scan时产出,运行器处理完一批再拉下一批;背压由拉模型天然给出,插件 MUST NOT 依赖任何自行推送的通道。 - 中止:运行器可随时丢弃本轮(不再发
scan,或终止进程);轮的产出在运行器提交之前没有任何副作用。进行中的请求可用cancel取消(§6.7)。 - 顺序:产出项按其在流中的顺序生效;同一 file_uri 的多次出现按序后写覆盖(core.md §2.4 的同级写入);跨 URI 可并发处理(运行器内)。判断 URI 同一性前 MUST 先按 core.md §1.1 规范化。
一轮的典型流程:初始化 → scan(轮首)→ 插件返回一批条目与 next → 运行器逐条处理(其间按需 read_blob 读取 blob,§6.7)→ 继续 scan(续批)→ … → 插件返回结束批与新 cursor → 运行器完成本轮全部落地后持久化游标(§6.8)。
6.5 产出项与文档分段¶
产出项是 scan 响应 items 数组的元素,四种 kind,均为封闭 schema——出现未定义的字段或未知 kind 时该条目失败(§6.5 末),运行器 MUST NOT 静默忽略。
document:一篇文档(core.md §2.1–§2.3 的内容模型),页以数组顺序内联给出。
{ "kind": "document", "file_uri": "git://acme/repo/docs/a.md",
"document": { "file_type": "md", "title": "A", "doc_metadata": { "author": "…" } },
"pages": [
{ "page_metadata": {},
"elements": [ { "category": "NarrativeText", "text": "…" } ] }
] }
file_uri:文档身份(URI,文法与规范化见 core.md §1.1)。document:文档对象,字段与 core.md §2.1 一致,但不含pages——页由本条目给出。首段 MUST 带document,续段 MUST NOT 带;违例时该文档整篇失败。pages:页对象数组,字段与 core.md §2.2 一致;elements为内联元素对象(线格式见 §6.6)。页数可为 0。- 插件 MUST 保证每条消息(含内联元素与 base64 字节)不超过
max_message_bytes;为此 SHOULD 依据max_message_bytes与remote_limits决定页与元素的切分。确实无法在限制内表达的内容 MUST 以条目级error(content_invalid)如实上报,MUST NOT 发送超限消息。 continued:布尔,缺省 false;为 true 表示本段之后还有同一file_uri的段(非末段 MUST 为 true)。- 分段规则:只在页边界切分,页数组按流中顺序跨段拼接;同一
file_uri的各段 MUST 连续出现(中间不得插入其他条目),一轮内同一时刻至多一个未闭合的分段文档;末段 MUST 省略continued或为 false。轮结束(或轮中止)时仍有未闭合的文档 → 该文档整篇失败(§6.8),MUST NOT 只提交已收到的页。 - 插件 SHOULD 保证同一源两次枚举的页序与元素序稳定(同一内容产出相同顺序)——这是收敛判据(§8.3)的前提。
- 同一轮内同一
file_uri出现多次完整的 document / delete / move 时按顺序生效(后写覆盖)。
delete:{"kind": "delete", "file_uri": "…"}——按 URI 删除整篇文档(core.md §4)。
move:{"kind": "move", "from_uri": "…", "to_uri": "…"}——原子改名(core.md §4)。
- 删除与移动 MUST 是显式条目:运行器 MUST NOT 用「未出现即删除」做集合差分——增量枚举下未出现不等于已删除,推断会造成静默误删。
- 前缀强制:
file_uri/from_uri/to_uriMUST 是符合 core.md §1.1 的 URI;运行器 MUST 拒绝并如实上报不在instance.uri_prefix内的条目,MUST NOT 投递。判定按 core.md §1.1 规范化后的条目与该前缀做前缀匹配(同 core.md §3list的规则:码点匹配,不识别 path 段边界;需要按段匹配时前缀末尾自带分隔符)。前缀本身是规范化不动点(§6.3),条目规范化后仍以它开头,判定不因规范化而失效。
error:插件对无法产出的条目如实上报,MUST NOT 静默跳过。
{ "kind": "error", "file_uri": "git://acme/repo/docs/broken.md",
"code": "source_unavailable", "message": "…", "retryable": true }
code、message、retryable必填;file_uri可选(为null或缺省,错误不针对单篇文档时)。code为封闭枚举:source_unavailable(源端暂时不可达或读取失败)、content_invalid(源内容无法映射为合法 DPE 内容,或无法在max_message_bytes限制内表达)、internal(插件内部错误)。retryable表示「同样的扫描重跑可能成功」,语义同 core.md §6 的retryable。- 条目级错误不终止本轮,但阻止游标推进(§6.8),运行器 MUST 如实上报。
条目的失败归属:
- 条目级失败(封闭 schema 违例、未知
kind、内容校验失败、blob 读取失败、越界):该条目失败,本轮继续处理其余条目,游标不得推进(§6.8),运行器 MUST 如实上报并在报告中区分来源。 - 协议级失败(分帧 / JSON-RPC 违例、消息超限、未知 method):按 §6.2、§6.3 处理,运行器终止实例。initialize 的失败(无共同版本、插件拒绝配置、选定版本不在运行器列表中)同样终止实例,按 §7.1 归为实例级——不改动实例定义或插件就无法恢复。
请求级错误用 JSON-RPC error 响应;错误码是闭集:
{ "jsonrpc": "2.0", "id": 21,
"error": { "code": -32003, "message": "source read failed",
"data": { "retryable": true } } }
| code | 名称 | 语义 |
|---|---|---|
-32700 / -32600 / -32601 / -32602 / -32603 |
JSON-RPC 标准码 | 解析错误 / 非法请求 / 未知 method / 参数非法 / 内部错误 |
-32001 |
cancelled |
请求被 cancel 取消(§6.7) |
-32002 |
unknown_handle |
句柄未知或已失效(§6.7) |
-32003 |
source_failed |
源端失败(scan 或 read_blob);data.retryable 标注可否原样重试 |
-32004 |
version_unsupported |
initialize 无共同版本(§6.3) |
-32005 |
invalid_config |
initialize 的实例配置不被插件接受(§6.3) |
data 为扩展位(如 retryable)。运行器遇到未知的错误码 MUST 按不可重试处理并如实上报,MUST NOT 崩溃。
6.6 元素线格式与校验流水线¶
线元素与 core.md §2.3 的元素对象一致,唯一区别是 blob 字段的取值:线协议上给出字节的来源,运行器取得字节后把它替换为 sha256: 引用。
- 线协议上的
blobMUST 是下面的字节来源对象,MUST NOT 是sha256:引用字符串——插件不计算、也不得伪造引用;运行器取得字节后自行写入引用并上传。 - 字节来源互斥二选一:
{"inline": "<base64>"}:内联字节(RFC 4648 §4 标准字母表,带填充)。只适合小对象,受max_message_bytes约束(§6.2)。{"handle": "…", "size": 12345}:不透明句柄(§6.7);size为字节数(整数,≤ 2^53−1),可选,插件 SHOULD 提供已知的大小。blob缺省或为null表示源确实没有字节(合法;运行器最终不产出blob)。blob对象中inline与handle同时出现或同时缺失(但blob对象存在)→ 条目失败。- category 不允许携带 blob 的元素 MUST NOT 出现
blob与mime_type(与 core.md §2.3 同样的封闭规则,契约 1 §4.1)。
{ "category": "Image", "text": "架构图", "mime_type": "image/png",
"blob": { "handle": "blob-1", "size": 20480 } }
运行器对每个文件条目按以下流水线校验,遇错即该条目失败(MUST NOT 跳过出错的元素或只提交其余部分):
- 条目封闭 schema(§6.5);
- 页对象与元素对象按 core.md §2 校验,顺序同 core.md §2.8:形状 → category 封闭枚举 → 封闭 schema(含 category 允许的字段)→ 逐字段(类型、数值界限 §2.6、metadata 全量、base64 与
size合法性)。其中blob按本节的字节来源对象校验,替代 core.md §2.8 对 blob 引用格式的校验;第 3 步替换为引用之后,元素才是 core.md §2.3 的元素对象; - blob 解析(§6.7):内联解码或句柄读取,取得字节、算出
sha256:引用; -
分段合并为完整文档后,整体再按 core.md §2 的文档层规则校验(含
file_type语法),并由运行器重算三层 hash。 -
本地校验的错误码沿用 core.md §2.8 的口径(
DPE_VALIDATION/DPE_CATEGORY_UNKNOWN),但这是运行器对插件产出的本地校验,与远端返回的 DPE 错误是两个来源,报告时 MUST 区分。 - hash 一律由运行器用规范的 hash 契约计算;插件产出中没有、也不需要 hash。
6.7 blob 读取与句柄生命周期¶
read_blob(运行器 → 插件):
{ "jsonrpc": "2.0", "id": 21, "method": "read_blob",
"params": { "handle": "blob-1", "offset": 0, "max_bytes": 1048576 } }
offset:本次读取的起始字节偏移(≥ 0,缺省 0);max_bytes:本次返回的最大原始字节数(> 0)。插件 MUST 支持句柄范围内的任意偏移读取(内部可重开 / 重取数据源,方式不限),使运行器能从断点重试。data(必填)为原始字节的 base64(RFC 4648 §4,带填充),原始长度 MUST NOT 超过max_bytes;eof(必填,布尔)为 true 表示本次返回的块到达字节流末尾,false 表示之后还有字节。返回空data时 MUST 带eof: true(防止运行器空转);offset 等于或超过实际长度时返回空块 +eof: true。- 插件对未知或已失效的句柄 MUST 以
-32002(unknown_handle)拒绝;源端读取失败 MUST 以-32003(source_failed)拒绝,并在data.retryable中标注可否原样重试。
句柄:
- 句柄是不透明字符串,含义由插件定义;运行器 MUST NOT 解析。
- 快照一致性:插件 MUST 保证句柄指向产出它时那份字节的确定快照——轮内任意时刻、任意偏移读到的字节都来自同一份字节;同一偏移重复读取结果一致。
- 生命周期:句柄自产出起、至运行器发出下一轮的轮首
scan(或shutdown)为止有效;此后插件 MAY 失效。运行器 MUST 在下一轮开始前完成上一轮全部句柄的读取(§6.4 的轮约束);同一句柄 MAY 多次读取(重试)。 size一致性:size已声明时,读取全部完成后的实际总长 MUST 等于它;不符(或提前eof)时运行器 MUST 判该条目失败。
运行器的读取职责:
- MUST 以分块方式流式读取(块大小由运行器决定、受
max_message_bytes约束),MUST NOT 要求单条消息承载完整 blob。 - 读取完成后运行器才得到
sha256:与总长;blob 的分块上传在读取完成之后进行——上传路径含sha256、Content-Range含 total(http.md §4.7),两者都必须先确定。运行器 MAY 在本地暂存字节(内存或临时文件)以衔接读取与上传,MUST NOT 在摘要与总长确定前开始上传。 - 声明
size超过远端blob_max_bytes时 SHOULD 立即判该条目失败,不必读取。 - 失败语义:读取失败(含取消、超时、
size不符、句柄失效)使该条目整篇失败:运行器 MUST NOT 提交该文档(MUST NOT 只提交其余元素,MUST NOT 用空blob顶替——那会写出与源不等价的内容),MUST 如实上报;本轮游标不得推进(§6.8)。运行器 MAY 在本轮内重试读取。 - 同一轮内相同内容的 blob SHOULD 只读取与上传一次(去重属运行器实现)。
取消与超时:
- 运行器 MUST 为每个
read_blob设置超时(有界、可配置)。超时或轮中止时,运行器 MAY 发送取消通知:
cancel是通知(无响应);它适用于任何进行中的请求(scan与read_blob均可取消)。插件 MUST 尽快中止对应请求,并以-32001(cancelled)错误响应;为此插件 MUST 在处理长请求期间继续读取并处理控制消息。- 取消或超时后该次读取视为失败,但句柄不因此失效(可从任意偏移重试)。运行器 MUST 忽略已放弃请求迟到的响应。
- 运行器对不响应取消的插件 MAY 在超时后终止插件进程(该轮作废,§6.3)。
6.8 游标与增量¶
- 游标:插件在轮结束时给出的不透明字符串(§6.4),代表「本轮枚举到的源状态位置」。运行器在下一轮把它作为轮首
scan的cursor传回;插件据此只产出该位置之后的变更。 - 性质:MUST 不透明(运行器不得解析);MUST 自包含——插件进程重启后仍可解释,解释不了时插件 MUST 回退为全量枚举(结束批标记
incremental: false,§6.4),MUST NOT 因此报错;运行器 MUST 只在本地缓存、按实例隔离(§2「游标只存于运行器本地缓存」),MUST NOT 把它写进任何 DPE 字段或发往服务端;缓存丢失或首轮没有游标 = 全量枚举。 - 覆盖范围(增量轮):
incremental: true的轮 MUST 产出自所给游标以来的一切变化,包括删除与移动(显式条目)与内容变更;插件 MUST NOT 让新游标越过本次未能成功产出的变更。 - 全量轮:
incremental: false的轮只保证现存内容的完整枚举——此前的删除与移动无法由协议表达:它们是相对上次枚举的差,而跨轮状态只有游标本身(§2「游标只存于运行器本地缓存」;游标不可得时这个差不存在)。插件 MAY 产出它自己能确定的删除与移动(如源端提供删除事件)。运行器 MUST 按 §6.4 如实上报这类轮,MUST NOT 视同删除与移动已同步;全量轮的新游标同样受 fail-closed 约束——整轮成功落地后持久化,此后恢复为增量轮。 - 不支持游标:
capabilities.supports_cursor为 false 时插件 MUST NOT 给出游标(结束批的cursor为null、incremental恒为 false),运行器 MUST NOT 缓存游标——此后每轮都是全量轮(推送侧的增量由 doc_hash 筛选承担,core.md §3.2),删除与移动只能靠插件自己确定并显式产出。 - 持久化(fail-closed):运行器 MUST 仅在整轮全部条目成功落地之后才持久化新游标——即无条目错误、无校验与越界失败、全部 blob 读取成功、全部文档以
created/updated/unchanged提交成功、全部删除与移动成功或已达成目标状态(core.md §5.2)。任何未解决失败 → MUST 保留旧游标、MUST 如实上报、退避后重试。 - 重扫是安全的:commit 由内容幂等(core.md §5.2),delete 重试得到
DPE_NOT_FOUND视为成功(core.md §5.2)。恢复路径就是稳定重跑整轮,MUST NOT 尝试只重放失败项(此禁令针对跨轮恢复;§7.2 的轮内冲突重读不在此列)。 - 永久失败不得空转:越界产出、schema 违例、超出远端限额等插件缺陷会让游标被持续扣住。运行器 MUST 如实上报(含 file_uri 与条目定位)并可观测,MUST NOT 静默跳过,也 MUST NOT 无退避地空转;
retryable: false的条目同样不得被当作「跳过」。重试节奏与上限属运行器策略(§5.2),上报形式见 §7。
7. 错误与可观测¶
本节把 §6 分散规定的失败处置归为统一分层,并为独立运行器规定机器可读的输出。线协议上的错误承载见 §6.5,游标推进规则见 §6.8;本节细化失败的归属与上报,不改变它们。
7.1 失败分层¶
| 层级 | 情形 | 运行器处置 | 游标 |
|---|---|---|---|
| 协议级 | 分帧或 JSON-RPC 违例、消息超限、未知 method、initialize 错误响应(实例级,见下行)之外的握手违例(如 initialize 结果缺少必填字段、握手完成前发来响应以外的消息)(§6.2、§6.3、§6.5) | 终止实例,本轮作废 | 不推进 |
| 实例级 | 清单不合法、实例定义不合法(含前缀非法或重叠)、配置未通过 schema、必填凭证缺失、无共同协议版本(-32004)、插件拒绝配置(-32005)、选定版本不在运行器列表中、插件身份与清单不符(§3、§4、§6.3);remote 拒绝凭证(HTTP 401,http.md §6)、remote 不支持运行器可用的任何 hash 契约(capabilities,http.md §4.1) |
不启动插件或终止插件,不开始轮;MUST NOT 自动重试(§5.2) | 不推进 |
| 轮级 | 实例已有轮在进行(instance_busy,未启动插件,§5.2);scan 的 error 响应、未知或过期的 next(§6.4)、插件异常退出(§6.3);scan 超时,或任何请求超时后插件不响应 cancel(§6.7);remote 不可用:网络失败,或可重试的 DPE 错误(DPE_UNAVAILABLE / DPE_RATE_LIMITED)按 Retry-After 重试耗尽 |
本轮作废并上报,退避后重试(§5.2) | 不推进 |
| 条目级 | 见下表 | 该条目失败,本轮继续处理其余条目(§6.5) | 不推进(§6.8) |
条目级失败按来源区分(§6.5 要求报告区分来源):
| source | 情形 | code |
|---|---|---|
plugin |
插件产出的 error 条目(§6.5) |
插件给出的 source_unavailable / content_invalid / internal |
validation |
运行器对插件产出的本地校验失败(§6.6),含分段在轮结束时仍未闭合(§6.5) | DPE_VALIDATION / DPE_CATEGORY_UNKNOWN / segment_unclosed |
prefix |
条目越出实例前缀(§6.5) | out_of_prefix |
blob |
blob 读取失败(含取消、超时、句柄失效)、size 不符、超过远端限额(§6.7) |
blob_read_failed / blob_size_mismatch / blob_too_large |
remote |
remote 对该条目的写入返回不可重试的 DPE 错误(core.md §6);可重试的错误不落到条目级,重试耗尽即按轮级中止本轮 | DPE 错误码原样 |
retryable的含义同 §6.5:同样的轮重跑可能成功。plugin来源取插件给出的值;blob_read_failed取插件-32003的data.retryable(取消、超时视为 true,句柄失效视为 false);remote来源取 core.md §6 的值,唯一的例外是:按 §7.2 本可自动重新判定的冲突(source_wins下的 commit / delete、源被改写的 move)因超过重试上限而失败时取 true;其余运行器判定的条目级失败均为 false。- 运行器对条目的处理 MUST NOT 因其中某个条目失败而崩溃或放弃本轮其余条目(§6.1)。
7.2 写入冲突¶
同步状态:运行器 MUST 为每个实例维护本地同步状态——每个文档最近一次确认的 remote doc_hash(类比 Git 的 remote-tracking ref),键为 core.md §1.1 规范化后的 file_uri(独立运行器存放于状态目录,§4.4)。它与游标一样只作缓存:丢失时结果仍然正确,只是冲突保护退化(见下)。
- 以下结果确认并更新同步状态:
created/updated/unchanged、已达成(remote 已等于本地内容),以及按source_wins重新提交成功;delete 成功或已达成时删除该条目;move 成功或已达成时把条目从from_uri迁到to_uri。冲突上报时不更新。 - 意图日志:写入请求发出前,运行器 MUST 先持久化本次写入预期的 remote 状态(意图),确认结果后再并入同步状态。意图的取值是 doc_hash 或「不存在」:commit 记
file_uri → N,delete 记file_uri → 不存在,move 同时记两侧from_uri → 不存在、to_uri → 本次 move 的 base_hash。判定时,R 等于该 URI 的待定意图值(含「不存在」)即视为本实例自己已生效的写入:按意图更新同步状态(「不存在」即删除条目),再按判定表继续。否则进程在写入生效、记录确认之前崩溃,下一轮会把自己的写入误报为他人改写或删除。写入结果一旦确定(成功或失败,含冲突),对应 URI 的待定意图 MUST 清除,只有结果未知(崩溃、响应丢失)时才保留——否则残留的意图会把之后他人的同值写入误当作自己的。意图 MAY 整批持久化后再并发发出写入;持久化只要求对进程崩溃持久(不要求对掉电持久)——意图日志丢失时退化为误报冲突并如实上报,不会造成错误写入。 - 契约:比较 MUST 在同一 hash 契约下进行。同步状态中的值带契约前缀;服务端仍接受该契约时可直接作前置条件(契约 1 §6、core.md §5.1),不再接受或运行器改用其他契约时,这些条目按缺失处理。
前置条件与冲突判定:写入前运行器以 batch_head / head 读取 remote 值 R,同步状态中的值为 L,本地新内容的 doc_hash 为 N:
| 情形 | 判定 |
|---|---|
| R == N(commit),或目标状态已达成(delete / move,core.md §5.2) | 已达成,跳过并确认 |
| 无 L,R 不存在 | commit 以 if_absent 新建 |
| 无 L,R 存在 | 以 R 为 base_hash。这是同步状态缺失时的退化:两轮之间他人的写入无法识别,冲突保护只覆盖读写之间的竞态窗口 |
| 有 L,R == L | 以 L 为 base_hash 正常写入 |
| 有 L,R 存在且 ≠ L | 他人改写:冲突 |
| 有 L,R 不存在 | 他人删除(或移走):冲突 |
delete 与 move 用同一张表(move 取 from_uri 的 L 与 R)。此外,以前置条件发出的写入仍可能因读写之间的竞态得到 DPE_PRECONDITION_FAILED、DPE_ALREADY_EXISTS 或不属于「已达成」的 DPE_NOT_FOUND(core.md §5.2),同样是冲突。
冲突处置:运行器就是 core.md §5.2 所说的「上层」。它 MUST NOT 改用 force(move 本就不支持 force,core.md §4),MUST 重新读取冲突 URI 的当前状态后按下表决定,运行器实现的其他按 URI 缓存(若有)MUST NOT 影响本节的判定。重新提交以重读到的 doc_hash 为前置条件,仍经 CAS:同级写入之间经 CAS 的后写覆盖不属于静默覆盖(core.md §7 第 4 条)。
策略由实例的 conflict_policy 给出(§4.4;宿主以自己的方式配置,缺省值相同):commit 取 source_wins(缺省)或 report,delete 取 report(缺省)或 source_wins。delete 不可逆,会连带丢掉他人的新内容与学习产物,因此缺省不以源为准。
| 意图 | 重读结果 | 处置 |
|---|---|---|
| commit | remote 的 doc_hash 等于本次提交内容 | 已达成,计 unchanged |
| commit | 文档存在、内容不同 | source_wins:以重读到的 doc_hash 为 base_hash 重新提交;report:条目失败 |
| commit | 文档不存在(他人已删除或移走) | source_wins:以 if_absent 重建(复活,core.md §4);report:条目失败 |
| delete | 文档不存在 | 已达成,计 deleted |
| delete | 文档存在、被他人改写 | report:条目失败;source_wins:以重读到的 doc_hash 为 base_hash 重新删除 |
| move | 源存在、被他人改写 | 受 commit 策略约束——source_wins:以重读到的 doc_hash 为 base_hash 重试 move(move 保留内容与学习产物,他人的写入随之移到目标);report:条目失败 |
| move | 目标已被占用(DPE_ALREADY_EXISTS) |
条目失败 |
| move | 源已不存在,目标未达成(DPE_NOT_FOUND) |
条目失败 |
- 重试上限:本轮内对同一条目的重读与重新提交 MUST 有次数上限(由运行器决定,SHOULD 不超过 2 次),以免与并发写入者互相饿死;超过即该条目失败(
retryable: true,§7.1),游标不推进(§6.8);report策略下的冲突与 move 的两类失败重跑不会自行消失,取 false。 source_wins的副作用:它会覆盖他人在实例前缀内的同级写入,并复活他人删除的文档,两者都会在 remote 侧引起重新学习(plan §0.1 P3);两个写入者都以自己为准时,同一文档会被反复改写。因此服务端与其他写入者 SHOULD NOT 写入 connector 实例的前缀;需要在 remote 上保持删除的文档,应先从源中删除,或对该实例使用report。- 永久冲突:move 的后两类失败与
report策略下的冲突,重跑不会自行消失,按 §6.8「永久失败不得空转」处理。恢复由使用者完成:在 remote 上消除冲突后重跑;或丢弃该实例的本地游标,以全量轮恢复现存内容(全量轮不表达此前的删除与移动,§6.8)。运行器 MUST NOT 把失败的 move 自动退化为「在目标上 commit、再删除源」——那会丢失学习产物并覆盖占用目标的文档。
7.3 轮结果¶
运行器 MUST 以下列三态之一上报每一轮,并附 incremental:
| outcome | 含义 |
|---|---|
succeeded |
枚举完整结束,全部条目成功落地(§6.8 的持久化条件);新游标已持久化,不支持游标时同样以此表示整轮成功 |
partial |
枚举完整结束(收到结束批),但存在条目级失败;游标不推进 |
failed |
实例级、轮级(含 instance_busy)或协议级失败,轮未完整结束或未开始;游标不推进 |
incremental取自结束批(§6.4);轮未收到结束批时为null。incremental: false的succeeded轮表示此前的删除与移动未被表达(§6.8),运行器 MUST 让它可观测(§7.5),MUST NOT 把它与增量轮混同。
7.4 轮报告(独立运行器规范档)¶
每次调用结束时,独立运行器 MUST 向 stdout 写出恰好一个轮报告——单行紧凑 JSON 对象(UTF-8,LF 结尾)——stdout 不输出其他内容。未能开始轮(实例级失败、instance_busy)时同样输出报告,outcome 为 failed、incremental 为 null、counts 各项为 0。唯一的例外是命令行用法错误:此时以退出码 64 结束、不输出报告(§7.6)。
{ "report_version": 1,
"instance": { "id": "acme-repo", "uri_prefix": "git://acme/repo/" },
"remote_url": "https://dpe.example.com/remotes/acme",
"plugin": { "name": "git-connector", "version": "0.1.0" },
"protocol_version": "dpe-connector/1",
"started_at": "2026-10-09T08:00:00Z", "ended_at": "2026-10-09T08:00:42Z",
"outcome": "partial", "incremental": true, "cursor_persisted": false,
"cursor_held_since": "2026-10-09T08:00:00Z",
"counts": { "created": 3, "updated": 1, "unchanged": 0, "deleted": 1, "moved": 0, "failed": 1 },
"failures": [
{ "level": "item", "source": "prefix", "code": "out_of_prefix",
"message": "file_uri outside instance prefix",
"file_uri": "git://other/x.md", "retryable": false } ] }
| 字段 | 说明 |
|---|---|
report_version |
整数;本规范定义 1 |
instance |
{id, uri_prefix},取自实例定义;实例定义无法解析或缺少该成员时,对应成员为 null |
remote_url |
实例定义的 remote.url;无法取得时为 null |
plugin |
{name, version};插件未完成 initialize 时为 null |
protocol_version |
选定的线协议版本;未完成 initialize 时为 null |
started_at / ended_at |
RFC 3339 UTC 时间戳 |
outcome / incremental |
§7.3 |
cursor_persisted |
布尔:本轮是否持久化了新游标 |
cursor_held_since |
游标被扣住的起始时刻:自该实例上一次 succeeded 的轮以来,第一个已取得排他锁(§5.2)且结果不是 succeeded 的轮的 started_at(RFC 3339 UTC,记录于状态目录,§4.4)。本轮为 succeeded、此前没有这样的轮,或状态目录中尚无该实例时为 null;未取得锁的调用(instance_busy)不读写该记录,报告中取 null。supports_cursor: false 的实例以同一定义计算(只看 outcome,不看是否有游标)。供调用方对长期卡住的实例告警 |
counts |
各项计数(整数,均必填):created / updated / unchanged 为 commit 结果(core.md §3.3);deleted / moved 含按 core.md §5.2 视为已达成的情形;failed 为条目级失败的条目数 |
failures |
失败数组,元素见下;无失败时为 [] |
failures_truncated |
可缺省:失败过多时运行器 MAY 截断 failures,此字段为被省略的条数 |
failures 的元素:{level, source, code, message, retryable, file_uri?, from_uri?, to_uri?}。
level:protocol/instance/round/item(§7.1)。source:plugin(插件报告的错误,含 JSON-RPCerror响应)/validation/prefix/blob/remote/runner(运行器自身判定的非条目级失败)。code:字符串。插件error条目原样取其code;插件的 JSON-RPC 错误取 §6.5 错误码表的名称(cancelled、unknown_handle、source_failed、version_unsupported、invalid_config),JSON-RPC 标准码依次记为parse_error、invalid_request、method_not_found、invalid_params、internal_error,未知错误码记为unknown_error(按不可重试处理,§6.5);remote 错误取 DPE 错误码原样;运行器自身的判定取下表。message:人类可读的说明,MUST NOT 含凭证值(§4.3)。file_uri/from_uri/to_uri:条目级失败 MUST 给出所涉 URI(规范化后,core.md §1.1)。
运行器自有错误码(闭集):
| code | 层级 | 含义 |
|---|---|---|
manifest_invalid |
instance | 清单不可读或不合法(§4.1) |
definition_invalid |
instance | 实例定义不合法,含前缀非法或重叠、凭证引用指向 remote 凭证(§3、§4.3、§4.4) |
config_schema_violation |
instance | 实例配置未通过 config_schema(§4.2) |
secret_missing |
instance | 必填凭证无法解析(§4.3) |
version_unsupported |
instance | 插件选定的版本不在运行器的列表中(§6.3) |
plugin_mismatch |
instance | 插件自报身份与清单不符(§4.1) |
remote_unauthorized |
instance | remote 拒绝凭证(HTTP 401,http.md §6) |
remote_incompatible |
instance | remote 不支持运行器可用的任何 hash 契约(http.md §4.1) |
instance_busy |
round | 同一实例已有轮在进行(§5.2);retryable: true |
protocol_error |
protocol | 分帧、JSON-RPC 或握手违例,消息超限(§6.2、§6.3) |
plugin_crashed |
round | 插件异常退出(§6.3) |
timeout |
round | scan 超时,或请求超时后插件不响应 cancel(§6.7) |
remote_unavailable |
round | remote 整体不可用(§7.1) |
out_of_prefix |
item | 越界条目(§6.5) |
segment_unclosed |
item | 分段文档在轮结束时未闭合(§6.5) |
blob_read_failed |
item | blob 读取失败(§6.7) |
blob_size_mismatch |
item | 读取总长与声明的 size 不符(§6.7) |
blob_too_large |
item | blob 超过 remote 的 blob_max_bytes(§6.7) |
7.5 结构化日志(独立运行器规范档)¶
- 独立运行器 MUST 把日志写到 stderr,格式为 NDJSON:每行一个紧凑 JSON 对象。每条日志 MUST 含
ts(RFC 3339 UTC)、level(debug/info/warn/error)、event、instance(实例id)与round(运行器为每轮生成的不透明标识,轮外事件为null)。 - 运行器 MUST 至少输出以下事件:
| event | 附加字段 | 时机 |
|---|---|---|
plugin.started |
pid |
插件进程启动后 |
plugin.exited |
exit_code、signal(可为 null) |
插件进程退出后 |
round.started |
cursor_given(布尔:轮首 scan 是否带了游标) |
发出轮首 scan 前 |
item.failed |
同 §7.4 failures 元素的字段 |
每个失败发生时(任何层级) |
round.finished |
outcome、incremental、cursor_persisted、cursor_held_since、counts |
轮结束时 |
plugin.stderr |
line、truncated(布尔) |
插件 stderr 每输出一行 |
- 运行器给出了游标而插件回退为全量轮(
incremental: false,§6.8)时,round.finishedMUST 为warn级;outcome为partial或failed时 MUST 为warn或error级。 plugin.stderr:插件 stderr 按行转发;单行超过运行器的长度上限(SHOULD 不小于 8 KiB)时截断并标truncated: true;非 UTF-8 字节以 U+FFFD 替换。转发前按 §4.3 脱敏。- 日志 MUST NOT 含凭证值与
Authorization头(§4.3),SHOULD NOT 含 blob 字节与完整的元素内容。 - 实现 MAY 增加事件与字段;日志的消费者 MUST 忽略不认识的事件与字段。
7.6 退出码(独立运行器规范档)¶
| 退出码 | 含义 |
|---|---|
| 0 | succeeded(含全量轮;需要区分时读报告的 incremental) |
| 1 | partial |
| 2 | failed:轮级(instance_busy 除外)或协议级失败,重跑可能成功 |
| 3 | failed:实例级失败,不修改实例定义、清单或凭证就无法恢复 |
| 4 | failed:instance_busy,稍后重试即可 |
- 命令行用法错误(如未给出实例定义)MUST 返回 64(同 BSD
EX_USAGE),且不输出轮报告(§7.4);其余退出码都伴随一个轮报告。其他非零退出码表示运行器自身异常(如被信号终止),调用方 SHOULD 按 2 处理。
8. 一致性要求¶
8.1 插件¶
插件 MUST:
- 附带合法的清单,initialize 自报的
name/version与清单相同(§4.1); - 作为独立进程,经 stdio 以 NDJSON 分帧的 JSON-RPC 2.0 通信;stdout 只含协议消息,不主动发起请求(§6.1、§6.2);
- 先握手后产出;版本协商不静默降级,配置不可用时以
-32005拒绝(§6.3); - 响应
shutdown后以退出码 0 退出,读到 stdin EOF 时尽快退出(§6.3); - 产出封闭 schema 的条目,删除与移动以显式条目表达,无法产出的内容以
error条目如实上报(§2、§6.5); - 不计算、不伪造
sha256:引用;句柄快照一致,支持句柄范围内任意偏移的读取(§6.6、§6.7); - 处理长请求期间继续处理
cancel(§6.7); - 游标不透明且自包含;增量轮覆盖自游标以来的一切变化;解释不了游标时回退全量并标记
incremental: false(§6.8); - 只从环境变量取得凭证,不把凭证值写入任何输出(§4.3)。
插件 SHOULD 保证枚举确定(§2),不产出易变字段与位置序号(§2)。
8.2 运行器¶
运行器 MUST:
- 不经任何通道把 remote 凭证交给插件,按 §4.3 构造插件环境(§4.3、§6.1);
- 启动插件前校验清单、实例配置与凭证,initialize 后核对插件身份(§4.1–§4.3);
- 全量校验插件产出,自行计算全部 hash(§6.1、§6.6);
- 强制 URI 前缀,拒绝越界条目,拒绝重叠的前缀(§3、§6.5);
- 保证每个实例同时至多一轮(§5.2、§6.4);
- blob 读全后再上传,读取失败时该文档整篇失败(§6.7);
- 游标 fail-closed:只在整轮全部落地后持久化(§6.8);
- 绝不自动 force,按 §7.2 处理写入冲突;
- 以 §7.3 的三态上报每一轮,全量轮可观测;
- 不在日志、报告与错误中写出凭证值(§4.3、§7)。
独立运行器另 MUST 满足 §4.4、§5.2、§7.4–§7.6。
8.3 收敛判据¶
记号:对源状态 S,F(S) 为一个全量轮在 S 上的产出经运行器计算后的映射 file_uri → doc_hash(file_uri 按 core.md §1.1 规范化);R 为 remote 上实例前缀内的文档映射(core.md §3 的 list)。以下判据以「前缀内没有其他写入者、所述各轮均为 succeeded」为前提;声明符合本契约的插件与运行器组合 MUST 满足:
- 确定性:同一 S 上的两个全量轮得到相同的 F(S)。
- 全量轮:全量轮落地后,F(S) 中每个 URI 在 R 中存在且 doc_hash 相同;此前已删除或移出的文档可能残留在 R 中(§6.8)。
- 增量收敛:从空前缀开始,一个全量轮在 S0 上落地(R = F(S0));此后源依次变为 S1, …, Sn,每次变化后运行一个
incremental: true的轮。最终 R = F(Sn)——包括删除与移动的结果。 - 失败不破坏收敛:判据 3 的序列中任一轮为
partial或failed时,原样重跑直至succeeded,结论不变(游标 fail-closed,§6.8)。
判据 3 即 plan §14 M4 的验收口径(增量推送、删除与移动的结果与全量推送收敛后一致)。
8.4 验证方法¶
本节说明如何验证 §8.1–§8.3,不规定测试代码的组织。
线协议夹具插件(验证运行器):一个脚本化的插件,由实例配置选择场景(如 {"scenario": "multi_batch"}),覆盖全部线协议消息与失败路径。由于插件是独立进程,同一个夹具可供任何语言的运行器使用。运行器针对每个场景 MUST 得到下表的结果:
| 场景 | 插件行为 | 运行器预期 |
|---|---|---|
empty |
正常握手;一批空结束批,cursor: null、incremental: false |
succeeded,退出码 0,全量轮可观测 |
version_mismatch |
initialize 以 -32004 拒绝 |
实例级 version_unsupported,退出码 3 |
invalid_config |
initialize 以 -32005 拒绝 |
实例级,source plugin、code invalid_config,退出码 3 |
identity_mismatch |
自报的 version 与清单不符 |
plugin_mismatch,退出码 3 |
multi_batch |
三批(next 续批),含 document、delete、move |
succeeded,remote 状态符合产出,游标已持久化 |
segmented |
一篇文档分三段(continued) |
合并为一篇提交 |
unclosed_segment |
轮结束时分段未闭合 | partial,segment_unclosed,该文档未提交,游标未推进,退出码 1 |
error_item |
error 条目(source_unavailable,retryable: true) |
partial,source plugin |
out_of_prefix |
越界的 document,以及 to_uri 越界的 move |
partial,out_of_prefix,均未投递 |
invalid_item |
未知 kind、未定义字段、未知 category |
partial,DPE_VALIDATION / DPE_CATEGORY_UNKNOWN |
blobs |
内联 blob 与需多块读取的句柄 blob | succeeded,blob 已上传,引用等于字节的 sha256 |
blob_size_mismatch |
句柄实际长度与 size 不符 |
partial,blob_size_mismatch,文档未提交 |
unknown_handle |
read_blob 返回 -32002 |
partial,blob_read_failed |
cancel |
read_blob 挂起,收到 cancel 后以 -32001 响应 |
运行器超时后发出 cancel;partial,blob_read_failed |
crash |
第二批之前以非零码退出 | failed,plugin_crashed,游标未推进,退出码 2 |
oversize |
输出超过 max_message_bytes 的行 |
failed,protocol_error,退出码 2 |
bad_framing |
输出空行或非 JSON 行 | failed,protocol_error,退出码 2 |
scan_error |
scan 以 -32003 失败 |
failed,source_failed,退出码 2 |
fallback_full |
收到游标但回退全量(incremental: false) |
succeeded,round.finished 为 warn 级 |
no_cursor |
supports_cursor: false |
不缓存游标,下一轮不带游标 |
env_probe |
声明一个必填凭证;把自身环境变量的名字(不含值)作为文档内容产出 | 环境恰为 §4.3 的三部分;凭证缺失时 secret_missing,退出码 3 |
另有不依赖插件行为的运行器用例:同一实例并发运行两次,其一以 instance_busy(退出码 4)结束;清单或实例定义违例得到 manifest_invalid / definition_invalid,配置未通过 schema 得到 config_schema_violation;清单的 pattern / patternProperties 键超出 §4.1.1 的子集或结构上界(如 lookaround、反向引用、\p{…}、超展开规模)得到 manifest_invalid;同一状态目录中前缀重叠的第二个实例被拒绝(definition_invalid);沿用同一 id 修改 config 后,下一轮不带游标(全量轮);remote 上他人改写某文档、源端随后也修改该文档(使插件再次产出它)后,commit 冲突按 source_wins 重新提交、report 下判条目失败(retryable: false,cursor_held_since 非 null);同步状态与意图日志:写入生效后、确认前终止运行器进程,重跑不得报告冲突(含 delete 后源端重建同一 URI、move 后向原 URI 写入);move 目标被占用时为 partial,cursor_held_since 保持为首次失败轮的开始时刻。
插件测试驱动(验证插件):驱动插件进程并检查 §8.1——分帧与握手、条目的封闭 schema、句柄的快照一致性(同一句柄在不同偏移重复读取)、shutdown / stdin EOF 后按时退出、cancel 的响应,以及确定性(判据 1)。
收敛测试(验证判据 2–4):以可脚本化变更的源夹具(新增、修改、文档中间插入一页、删除、改名、二进制内容)驱动插件与运行器,对参考服务端核对 §8.3;官方 connector 另以真实服务端完成 plan §14 M4 的端到端验收。
9. 非目标¶
- 不定义任何具体数据源(官方 Git connector 以插件形式另行交付);
- 不定义宿主的多租户、计费、权限模型;
- 不定义调度策略(§5.2);
- 不复制 Core 的投递语义——运行器直接遵循 core.md 与 HTTP 绑定。