@sema-agent/server 7.135.0 → 7.136.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/MIGRATION.md +64 -2
  2. package/USAGE.md +62 -25
  3. package/deploy/sema-up/README.md +1 -1
  4. package/deploy/sema-up/chart/templates/NOTES.txt +1 -1
  5. package/deploy/sema-up/chart/values.yaml +1 -1
  6. package/dist/approval-card.d.ts +60 -4
  7. package/dist/approval-card.js +18 -1
  8. package/dist/approval-content-kind.d.ts +1 -0
  9. package/dist/approval-content-kind.js +1 -1
  10. package/dist/auth-bridge.d.ts +34 -5
  11. package/dist/auth-bridge.js +20 -3
  12. package/dist/boot/budget-tracing.d.ts +1 -2
  13. package/dist/boot/budget-tracing.js +2 -2
  14. package/dist/boot/org-memory.js +1 -1
  15. package/dist/boot/resolve-spec.js +10 -7
  16. package/dist/boot/runner-deps.js +4 -2
  17. package/dist/boot/stage-04-stores.d.ts +1 -1
  18. package/dist/boot/stage-05-execution-env.d.ts +1 -1
  19. package/dist/boot/stage-06-runners.d.ts +1 -1
  20. package/dist/boot/stage-07-capability-layer.d.ts +1 -1
  21. package/dist/boot/stage-08-reapers.d.ts +1 -1
  22. package/dist/boot/stage-09-leader.d.ts +1 -1
  23. package/dist/boot/stage-10-http-server.d.ts +1 -1
  24. package/dist/boot/stores.js +2 -3
  25. package/dist/boot/web-admin.js +10 -17
  26. package/dist/brain.js +12 -3
  27. package/dist/budget.d.ts +3 -3
  28. package/dist/budget.js +4 -0
  29. package/dist/capabilities/mcp-probe.d.ts +5 -4
  30. package/dist/capability-facts.d.ts +3 -7
  31. package/dist/capability-facts.js +3 -5
  32. package/dist/center-credential.d.ts +18 -4
  33. package/dist/center-credential.js +18 -3
  34. package/dist/config-catalog.js +12 -10
  35. package/dist/config-d-migration.js +1 -1
  36. package/dist/config-invariants.d.ts +1 -1
  37. package/dist/config-invariants.js +8 -0
  38. package/dist/config-lkg.d.ts +0 -38
  39. package/dist/config-lkg.js +1 -83
  40. package/dist/config-types.d.ts +12 -4
  41. package/dist/config.js +26 -8
  42. package/dist/deployment-shape.d.ts +1 -1
  43. package/dist/elicitation.d.ts +8 -7
  44. package/dist/elicitation.js +9 -8
  45. package/dist/file-history-disclosure.d.ts +6 -4
  46. package/dist/file-history-disclosure.js +22 -6
  47. package/dist/fleet-client.d.ts +32 -1
  48. package/dist/fleet-client.js +24 -4
  49. package/dist/fleet-lease.d.ts +7 -3
  50. package/dist/fleet-lease.js +31 -23
  51. package/dist/hooks/hook-llm.js +14 -8
  52. package/dist/hooks/hook-runner.d.ts +4 -4
  53. package/dist/hooks/hook-runner.js +2 -2
  54. package/dist/http/checkpoint-ctx.d.ts +28 -9
  55. package/dist/http/checkpoint-ctx.js +5 -1
  56. package/dist/http/dispatch.js +9 -9
  57. package/dist/http/resume-ctx-read.d.ts +2 -16
  58. package/dist/http/resume-ctx-read.js +2 -3
  59. package/dist/http/routes/approvals-assistant.d.ts +0 -2
  60. package/dist/http/routes/approvals-assistant.js +47 -12
  61. package/dist/http/routes/runs.js +2 -2
  62. package/dist/http/routes/session-sync.js +31 -9
  63. package/dist/http/routes/sessions.js +18 -14
  64. package/dist/http/submit-ctx.js +1 -1
  65. package/dist/http/wire-types.d.ts +5 -3
  66. package/dist/index.d.ts +1 -0
  67. package/dist/index.js +1 -0
  68. package/dist/model-route-gap.d.ts +49 -4
  69. package/dist/model-route-gap.js +34 -1
  70. package/dist/object-store-buckets.d.ts +13 -0
  71. package/dist/object-store-buckets.js +3 -0
  72. package/dist/observability/fail-open.d.ts +6 -6
  73. package/dist/observability/fail-open.js +6 -6
  74. package/dist/observability/metrics.js +1 -1
  75. package/dist/observability/principal-context.d.ts +4 -1
  76. package/dist/observability/principal-context.js +6 -1
  77. package/dist/parked-decide.d.ts +4 -3
  78. package/dist/parked-decide.js +26 -7
  79. package/dist/peer-directory.d.ts +2 -1
  80. package/dist/peer-directory.js +5 -5
  81. package/dist/plugins/approval-ask-store-sql.d.ts +8 -0
  82. package/dist/plugins/approval-ask-store-sql.js +3 -0
  83. package/dist/plugins/blob-backend.js +2 -1
  84. package/dist/plugins/checkpoint-index-columns.d.ts +50 -1
  85. package/dist/plugins/checkpoint-index-columns.js +28 -0
  86. package/dist/plugins/checkpoint-store-sql.d.ts +27 -3
  87. package/dist/plugins/checkpoint-store-sql.js +58 -29
  88. package/dist/plugins/file-history-store-sql.d.ts +6 -0
  89. package/dist/plugins/file-history-store-sql.js +30 -23
  90. package/dist/plugins/local-checkpoint-store.js +4 -3
  91. package/dist/plugins/one-time-migrations.d.ts +21 -28
  92. package/dist/plugins/one-time-migrations.js +39 -123
  93. package/dist/plugins/remote-env-k8s.d.ts +18 -5
  94. package/dist/plugins/remote-env-k8s.js +24 -7
  95. package/dist/plugins/s3-answer.d.ts +15 -0
  96. package/dist/plugins/s3-answer.js +5 -0
  97. package/dist/plugins/snapshot-bucket-presence.d.ts +39 -4
  98. package/dist/plugins/snapshot-bucket-presence.js +35 -7
  99. package/dist/plugins/sql-errors.d.ts +39 -0
  100. package/dist/plugins/sql-errors.js +49 -1
  101. package/dist/plugins/store-fault.d.ts +4 -0
  102. package/dist/plugins/store-fault.js +8 -0
  103. package/dist/plugins/tidb-pool.js +1 -1
  104. package/dist/plugins/web-search.d.ts +1 -1
  105. package/dist/plugins/web-search.js +2 -2
  106. package/dist/request-declaration-refusal.d.ts +94 -0
  107. package/dist/request-declaration-refusal.js +78 -0
  108. package/dist/request-image-urls.d.ts +6 -0
  109. package/dist/request-image-urls.js +27 -0
  110. package/dist/request-key-closure.d.ts +5 -0
  111. package/dist/request-key-closure.js +4 -1
  112. package/dist/runtime-caps-resolver.js +1 -1
  113. package/dist/sandbox-pkg-source.d.ts +28 -1
  114. package/dist/sandbox-pkg-source.js +1 -2
  115. package/dist/sealed-key.d.ts +19 -0
  116. package/dist/sealed-key.js +47 -4
  117. package/dist/session-incarnation.d.ts +16 -5
  118. package/dist/session-incarnation.js +7 -4
  119. package/dist/session-row.d.ts +3 -17
  120. package/dist/session-row.js +4 -0
  121. package/dist/session-sync.d.ts +15 -3
  122. package/dist/session-sync.js +5 -2
  123. package/dist/task-a2a.d.ts +52 -81
  124. package/dist/task-a2a.js +219 -89
  125. package/dist/task-mcp.d.ts +6 -14
  126. package/dist/task-mcp.js +16 -51
  127. package/dist/tool-approval.d.ts +23 -10
  128. package/dist/tool-approval.js +51 -36
  129. package/dist/trace/core-keyset-guard.d.ts +2 -2
  130. package/dist/trace/exit-seat.js +37 -13
  131. package/dist/trace/redact.d.ts +28 -12
  132. package/dist/trace/redact.js +233 -94
  133. package/dist/trace/self-minted-wire.d.ts +267 -0
  134. package/dist/trace/self-minted-wire.js +79 -0
  135. package/dist/url-credential-refusal.d.ts +52 -0
  136. package/dist/url-credential-refusal.js +14 -0
  137. package/dist/url-refusal.d.ts +13 -4
  138. package/dist/url-refusal.js +8 -2
  139. package/dist/workspace-space.d.ts +27 -0
  140. package/dist/workspace-space.js +26 -0
  141. package/package.json +3 -2
  142. package/scripts/drain-check-local.mjs +2 -1
  143. package/scripts/drain-check.mysql.sql +3 -1
  144. package/scripts/drain-check.pg.sql +3 -1
  145. package/skills/schedule.md +1 -1
  146. package/wire-changes/server@7.104.0.json +134 -0
  147. package/wire-changes/server@7.105.0.json +53 -0
  148. package/wire-changes/server@7.106.0.json +475 -0
  149. package/wire-changes/server@7.107.0.json +64 -0
  150. package/wire-changes/server@7.108.0.json +153 -0
  151. package/wire-changes/server@7.109.0.json +46 -0
  152. package/wire-changes/server@7.110.0.json +90 -0
  153. package/wire-changes/server@7.111.0.json +86 -0
  154. package/wire-changes/server@7.112.0.json +60 -0
  155. package/wire-changes/server@7.113.0.json +26 -0
  156. package/wire-changes/server@7.114.0.json +26 -0
  157. package/wire-changes/server@7.115.0.json +97 -0
  158. package/wire-changes/server@7.116.0.json +218 -0
  159. package/wire-changes/server@7.116.1.json +26 -0
  160. package/wire-changes/server@7.117.0.json +65 -0
  161. package/wire-changes/server@7.118.0.json +129 -0
  162. package/wire-changes/server@7.119.0.json +65 -0
  163. package/wire-changes/server@7.120.0.json +89 -0
  164. package/wire-changes/server@7.121.0.json +219 -0
  165. package/wire-changes/server@7.122.0.json +54 -0
  166. package/wire-changes/server@7.123.0.json +26 -0
  167. package/wire-changes/server@7.124.0.json +336 -0
  168. package/wire-changes/server@7.124.1.json +26 -0
  169. package/wire-changes/server@7.125.0.json +111 -0
  170. package/wire-changes/server@7.126.0.json +285 -0
  171. package/wire-changes/server@7.127.0.json +77 -0
  172. package/wire-changes/server@7.127.1.json +26 -0
  173. package/wire-changes/server@7.127.2.json +53 -0
  174. package/wire-changes/server@7.128.0.json +26 -0
  175. package/wire-changes/server@7.129.0.json +106 -0
  176. package/wire-changes/server@7.129.1.json +26 -0
  177. package/wire-changes/server@7.130.0.json +26 -0
  178. package/wire-changes/server@7.131.0.json +118 -0
  179. package/wire-changes/server@7.132.0.json +226 -0
  180. package/wire-changes/server@7.133.0.json +1155 -0
  181. package/wire-changes/server@7.134.0.json +107 -0
  182. package/wire-changes/server@7.135.0.json +174 -0
  183. package/wire-changes/server@7.136.0.json +448 -0
  184. package/dist/url-credential.d.ts +0 -52
  185. package/dist/url-credential.js +0 -49
package/MIGRATION.md CHANGED
@@ -7,6 +7,68 @@
7
7
  > ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
8
8
  > 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
9
9
 
10
+ ## 7.136.0 —— 中心登录令牌必须带 `scope`,不带的令牌验证不过(安全;行为面收紧,BREAKING;另含嵌入方 `webAdminJwtVerifier` 类型面) · anthropic 车道两把凭据同设起服拒(`ANTHROPIC_API_KEY` 与 `ANTHROPIC_AUTH_TOKEN`;行为收紧) · 一次性迁移 M5 退役(持久数据面;行为变更;嵌入方 `OneTimeMigrationReport` 去两键〔BREAKING,类型面〕;跳过 7.104.0–7.135.x 直升的库补列与铸令牌要自己做) · `GET /v1/assistant/inbox` 行的 `objective` 改成 run 行的目标预览(wire 值变更) · 审批卡信封多一个键 `approvalCard`(整队一次滚完;回滚到 7.135.x 时待决卡按 fail-safe 出口收场) · k8s 工作区快照桶起服时核存在性:没建桶的部署起服拒(行为收紧;升级前确认桶在) · `WORKSPACE_SNAPSHOT_PREFIX` 成为 `/` 分隔的闭集路径:不合形起服拒、未配快照存储也核(行为收紧;同时放宽到多段) · 子进程的命令行停驻时只以封签落盘(安全;零动作,升级序与回滚要留意:先停光 7.135.x 副本再起第一个 7.136.0 副本)
11
+
12
+ 1. **中心登录令牌必须带 `scope` —— 不带的令牌自本版起验证不过(安全;行为面收紧,BREAKING;另含嵌入方类型面)**:鉴权桥(`SEMA_WEB_ADMIN_URL` 在位默认开,或 `AUTH_BRIDGE_ISSUER`)
13
+ 验过签名之后按 settings-schema 的唯一读法 `readTokenScope` 读令牌的 `scope`:缺、空串、不是字符串 ⇒ 拒,拒因 `scope_claim_missing`(日志 `auth_bridge_rejected` 的 `error`、
14
+ 计数 `auth_bridge_verify_total` 的 `outcome`),处置与验签不过同一条 —— 不设任何身份,配了 service 凭据的部署答 `401 auth.unauthorized`(通用体,不说拒因)。不读成缺省 scope `global`。
15
+ 此前这类令牌被当作已验证身份受理、角色照读。
16
+ **升级序**:先把全部签发点(web-admin 各副本;split 部署里 `AUTH_BRIDGE_ISSUER` 指向的签发方)升到每一枚访问令牌都带 `scope` 的版本,**再过 ≥ 1 小时**
17
+ (访问令牌寿命上限 3600 秒,存量的无 `scope` 令牌到期自愈),然后升本服务。做不到就接受:窗口里拿着旧令牌的调用方验证不过,刷新或重登即恢复(刷新换出的访问令牌恒带 `scope`)。
18
+ 没有过渡旋钮。升级后看 `auth_bridge_verify_total{outcome="scope_claim_missing"}`:非零 = 仍有签发点不打 `scope`,或调用方拿着旧令牌没刷新。
19
+ **嵌入方**(只影响自己给 `ServiceDeps.webAdminJwtVerifier` 写验证器的;部署零动作):`verify` 成功返回的 `identity.scope` 必填(非空串),失败返回的 `error` 多一个词
20
+ `scope_claim_missing` —— 你的验证器读不出 scope 就答失败;tsc 红即是提示,零别名。交出 `scope` 为空串(JS 侧:缺省 / 非串)的成功身份
21
+ 在装身份之前同样被 `readTokenScope` 拒,与验证失败同答。
22
+
23
+ 2. **anthropic 车道两把凭据同设 ⇒ 起服拒(行为收紧;另含子路径端口的行为)**:`ANTHROPIC_API_KEY` 与 `ANTHROPIC_AUTH_TOKEN` 都非空的部署(7.135.x 照起、只发 `Authorization: Bearer`,
24
+ `x-api-key` 被丢弃)升级后起服拒,拒句带 `[config.anthropic_credential_conflict]`、点名两键、不回显值。**升级前做**:二选一 —— 端点要 Bearer 就只留 `ANTHROPIC_AUTH_TOKEN`
25
+ (与 7.135.x 实际发出的头相同,线上字节不变),原生 Anthropic API 只留 `ANTHROPIC_API_KEY`;空串仍算未设(compose 透传的 `${VAR:-}` 不算同设)。
26
+ 自拼 `ServiceConfig` 的嵌入方:`anthropic` 同时带非空 `apiKey` 与 `authToken` ⇒ `createBrain` 构造即抛同一句(此前 Bearer 胜出)。读子路径 `@sema-agent/server/model-route-env` 的
27
+ `laneAndEndpointFor`:两把同设的 env 上抛同一句(此前答 `apiKeyEnvName: "ANTHROPIC_AUTH_TOKEN"`)—— 在引擎起之前调它的壳先按上面二选一收好 env,或接住这一抛。
28
+
29
+ 3. **一次性迁移 M5 退役:会话表的化身列不再由起服补加、空化身的行不再由起服铸令牌(持久数据面;行为变更;嵌入方 BREAKING,类型面)**:7.104.0 引入的启动期迁移 ——
30
+ 给 `session_meta` 补 `incarnation` 列、给每一行空化身(`''`)铸一只互不相同的随机令牌 —— 窗(发布日 + 30 天)于 2026-10-28 关,本版提前于窗关删了它的码(窗内从 ≤ 7.103.x 直跳的库起服拒:先升到 7.104.0–7.135.x 任一版补列、起服一次,再升本版;「谁受伤」与「做法」写了顺序),
31
+ 以及起服日志行 `one_time_migrations_applied` 里的两个读数字段 `sessionIncarnationColumn` / `sessionIncarnationsBackfilled`(同一行里 `removableAfter` 读数的 `M5` 键随登记行消失,`note` 不再叙述它)。
32
+ 按这两个字段做告警或看板的规则要删。新库照旧由建表语句建出这一列,`DB_BACKEND=local` 的令牌在数据根边档里,这条迁移从来不碰。
33
+ - **嵌入方(类型面)**:`StoreBackend.runOneTimeMigrations()` 的返回型 `OneTimeMigrationReport`(经包根导出的 `StoreBackend` 可达,本身不在包根导出名里)去掉 `sessionIncarnationColumn` 与 `sessionIncarnationsBackfilled`;
34
+ 读它们的代码、把值标成 `OneTimeMigrationReport` 再写这两键的对象字面量 tsc 红;自实现该方法、未标注类型就直接返回含这两键的字面量的代码 tsc 不报(多出的键没有人读),也请一并删掉。
35
+ - **谁受伤**:从 ≤ 7.103.x 直接跳到本版、中间没在 7.104.0–7.135.x 任一版起过服的 SQL 库(含升级后仍有 ≤ 7.103.x 旧副本混跑、写下空化身行的库)。两种形,都是响亮的,没有静默错答:
36
+ ① `session_meta` 没有 `incarnation` 列 ⇒ 起服列册对账拒启 `config.schema_mismatch`,拒句里的 `ALTER TABLE session_meta ADD COLUMN incarnation …` 由建表语句生成,照句补列即可起服 ——
37
+ **补列之后立刻会落进第 ② 种**(新列对每个存量行都是缺省空串);② 列在、行上是空化身 ⇒ 那条会话取不出来(`acquire` 响亮拒 `session.incarnation_missing`),要一位活住客的读者(parked 决定腿)同拒,记忆座退回按 id 绑定并 warn;
38
+ `session.incarnation_missing` 是内部错误码,日志可见,wire 上是通用 500。起过 7.104.0–7.135.x 任一版的库、新库、`DB_BACKEND=local` 与内存形零影响。
39
+ - **做法**(只在受伤时,二选一):**先在 7.104.0–7.135.x 任一版上对这只库起服一次**(那几版的起服迁移补列并逐行铸令牌),再升到本版;
40
+ 或在本版上按 ① 的拒句补列后,给每个空化身的行填一只互不相同的随机令牌(不透明串,至多 64 个字符,UUID 即可;只改 `incarnation = ''` 的行,已有令牌的行一个字节不动),或删掉那些会话。
41
+ 令牌不上 wire、不进模型上下文、日志只打前 8 位。
42
+
43
+ 4. **`GET /v1/assistant/inbox` 行的 `objective` 改成 run 行的目标预览(wire 值变更;键名与可空性不变)**:此前是停驻上下文里存着的任务目标**原文**(不截断、不脱敏),
44
+ 本版起是门所属 run 的 `objectivePreview`(写入时已脱敏、约 120 字符,与 `GET /v1/assistant/tasks` 同一列;委派子代的门读派它的那条 run);没有 run 账本的部署 ⇒ `null`。
45
+ **消费方动作**:把 inbox `objective` 当完整目标用的(全文展示、按全文匹配),改读 run 详情面;盯运维计数 `server.assistant-inbox.objective-ctx-corrupt` 的看板改盯
46
+ `server.assistant-inbox.objective-preview-unreadable`(旧 tag 退役,不再计数)。
47
+ 5. **审批卡信封多一个键 `approvalCard`(滚动升级 / 回滚)**:本版写下的待决审批卡(`approval_ask.card_json`)带卡词,7.135.x 及更早的二进制读不动这类行
48
+ ⇒ 那张卡在旧副本上开流重放时跳过、同一只问幂等重入改走耐久停驻(人仍可经耐久决断口补批;不会被当成拒绝)。**动作**:整队一次滚完,不留长期混版窗;
49
+ 回滚到 7.135.x 时,7.136.0 写下的待决卡按上面那条 fail-safe 出口收场,升回即自愈。
50
+
51
+ 6. **k8s 工作区快照桶起服时核存在性 —— 没建桶的部署起服拒(行为收紧;运维前置)**:`REMOTE_EXEC=k8s` 且配了对象存储(`MINIO_ENDPOINT` / `MINIO_ACCESS_KEY` / `MINIO_SECRET_KEY`)的部署,
52
+ 起服时 server 对 `WORKSPACE_SNAPSHOT_BUCKET`(缺省 `workspaces`)指的桶做一次列举探针 —— 与会话快照桶(`SESSION_SNAPSHOT_BUCKET`)同一只探针、同一条规则:`404 + NoSuchBucket` ⇒ 在 `STORE_LOCK_WAIT_MS` 窗内重试、
53
+ 到窗**起服拒**,拒句点名桶名、`WORKSPACE_SNAPSHOT_BUCKET` 与 `MINIO_ENDPOINT` 的显示安全形(不带密钥)。**server 不建桶**。7.135.x 上这种部署照起,之后每条 run 读 `latest` 得 404,
54
+ 被当成「空间第一条 run」从空目录起,run 末保存失败(`workspace_snapshot_failed`)。连不上 / 403 / 5xx 这类证明不了缺桶的答复**不**拒启,走已登记的 fail-open 兜底口 `server.snapshot-bucket.presence-unproven`。
55
+ 起服之后桶才没的(被删):读 `latest` 得 `404 NoSuchBucket` ⇒ 该 run 以 `connect_failed` 拒,句子与运维日志行 `workspace_snapshot_bucket_missing` 的 `code` 都是 `snapshot_bucket_missing`,不再读成新空间。
56
+ **升级前做**:确认 `workspaces` 桶(或你的 `WORKSPACE_SNAPSHOT_BUCKET`)存在 —— `mc mb <别名>/workspaces`,或把 `WORKSPACE_SNAPSHOT_BUCKET` 指向已有的桶。sema-up 的三条路径一直建 `workspaces`,不在受伤面。
57
+ 不用工作区快照的 k8s 部署也在此列:对象存储三键一配,这只桶就是停驻挂起与跨轮保留共用的店,拒启的出路只有建桶 / 改指,没有关掉这次探针的旋钮。建桶晚于 server 起的编排:`STORE_LOCK_WAIT_MS` 窗内会重试。
58
+
59
+ 7. **`WORKSPACE_SNAPSHOT_PREFIX` 成为 `/` 分隔的闭集路径 —— 不合形起服拒(行为收紧;同时放宽到多段)**:前缀是一段或多段普通 key 段(`kata`、`libA/ws`),每段以字母或数字开头,其后字母、数字、`.` `_` `-`;
60
+ 空段(前导 / 尾随 / 连续的 `/`)、`.` / `..`、其它字符 ⇒ **起服拒**,码 `config.workspace_snapshot_prefix_invalid`,句子点名第几段。🔴 **未配快照存储(`MINIO_*` 缺)也核**:此前这个值没有存储时根本不读,
61
+ 拼错的值一直潜伏;现在 `REMOTE_EXEC=k8s` 的部署不论配没配存储,起服即核(别的车道不读这个值,杂散的值不拒)。配了存储的部署:7.135.x 对不合形的值在第一次连沙箱时才以
62
+ `unsafe snapshot key segment (keyPrefix)` 拒(只落在那一条 run 上),现在起服就拒;多段值(`libA/ws`)是新放宽,7.135.x 上同样在运行期被拒。合法的单段旧值行为不变。
63
+ **升级前做**:核 `WORKSPACE_SNAPSHOT_PREFIX` 的值,不合形的改成合形的;改前缀 = 换了一个 key 空间,旧前缀下的存量快照与 `latest` 不再被读 —— 要保留已有的快照就别改合形的旧值。
64
+ 8. **子进程的命令行停驻时只以封签落盘(安全;零动作,升级序与回滚要留意)**:stdio MCP 传输与 command hook 条目的 `command` / `args`(`args` 整只数组一个值)进凭据位,
65
+ 与 env 同一条判据;续跑开封后逐字交执行器,调用方零改动。升级前停驻过的这类任务,命令行明文已经在库 / 盘(及其备份)里 —— 起服的一次性迁移在窗内(至 2026-11-20)把仍在的行原地封好,
66
+ 已落过盘的字节改不回来:**轮换**这些命令行里出现过的密钥。
67
+ **多副本升级**:会停驻带 stdio MCP 或 command hook 任务的部署,先停光 7.135.x 副本再起第一个 7.136.0 副本(两版混跑时:7.135.x 副本续跑 7.136.0 停驻的这类任务 ⇒
68
+ 整行 `409 conflict.checkpoint_ctx_unsealable`(`reason:"unrecognized_seal"`,`field:"credentialSlotGeneration"`);7.135.x 副本新写的这类行在 7.136.0 上续跑 ⇒ `409`(`reason:"plaintext"`),
69
+ 直到下一次 7.136.0 起服的一次性迁移把它封好 —— 只在窗内)。**回滚到 7.135.x 之前**:用随包排空检查的 `sealed-credentials-rollback` 判据把带封签的停驻任务决掉或取消(这一类在候选里)。
70
+ 三只拒句(`conflict.checkpoint_ctx_unsealable` 两句与装配期拒启)改成规则形,按句子字面匹配的脚本改按 `errorCode` / `field` / `reason` 读。
71
+
10
72
  ## 7.135.0 —— 请求自带 MCP 的 `transport.url` 不得带凭据(新提交 400;安全) · `SANDBOX_PKG_SOURCE=custom` 的地址带凭据名参数起服拒(安全) · 嵌入方:技能的来处必填(`loadSkills` 第二参 `{ source }` 与 `LoadedSkill.source`;BREAKING,类型面) · 引擎 7.55.0:会话规则行的升级序(先升全部读者再放写) · 整协议名 `mcp__*` 在限制表上读成全部工具 · 属主收回自己加的会话规则条目 · 订正 7.134.0 节第 4 条:审批 `feedback` 拒改答 `resume_outcome_invalid`(原 `approval_binding_mismatch`) · 会话店接口只剩头读 `headOf`(嵌入方 BREAKING) · 文件历史读不全 ⇒ `503 state.file_history_unreadable` · journal 坏行 ⇒ `500 workflow.journal_incompatible` · MCP 工具结果两只新码 · `WebFetch(domain:<host>)` 的 deny / ask 按主机生效、参数不属于该工具的 deny / ask 规则受理口 422 / 续跑 409(安全;行为收窄) · `settings.hooks` 组 / 条目上读者不读的键新提交 `400 request.body_shape` · command hook 条目的 `plugin.options` 停驻时只以封签落盘(安全;回滚到 7.134.x 前先结掉带选项的停驻任务) · 耐久事件流开流不再读续跑输入判审批帧族(7.133.x 写下的在飞待决卡照重放;从 7.133.x 直升先停光旧副本) · SQL 后端 `permission_rule` 加一列 `grammar`(🔴 升级前先 `ALTER TABLE` 加列;持久数据,不能删表重建) · 会话 fork 不带在飞的 assistant 轮 · 技能条目自带的 `allowedTools` / `disallowedTools` 开始生效(升级序:先升全部读者,再在管理面写名单;放行免问是放宽方向,先审已有条目) · 请求自带的 `a2aPeers` 对端 / http 钩子 / 搜索后端地址带凭据(新提交 400;安全) · 设备车道有效工作目录非绝对路径的拒带终局码 `config.device_cwd_invalid`(续跑失败终局码多一词) · SQL 后端 `checkpoint` 加一列 `approval_card`(🔴 升级前先 `ALTER TABLE` 加列;表里是在飞的停驻,不能删表重建;嵌入方 `OneTimeMigrationReport` 多三键〔BREAKING,类型面〕) · 一次性迁移 M4 退役(行为变更;起服日志行 `one_time_migrations_applied` 少一个读数字段;嵌入方 `OneTimeMigrationReport` 去一键〔BREAKING,类型面〕;跳版升级的库含旧审计字节的那一页读不出,手工语句见第 22 条) · 嵌入方:审批决议的店接口 `ApprovalAskStore.decideAsk` 第三位插入必填 `owner`(BREAKING,类型面;部署零动作)
11
73
 
12
74
  1. **`mcpServers[i].transport.url` 带凭据 ⇒ 新提交 `400 request.field_invalid`**(安全;行为面收紧):url 带 userinfo,或带名字在凭据名表里的 query / fragment 参数
@@ -115,7 +177,7 @@
115
177
  **升级前做**:`ALTER TABLE checkpoint ADD COLUMN approval_card TEXT NULL;`(PostgreSQL 写 `ALTER TABLE checkpoint ADD COLUMN approval_card TEXT COLLATE "C";`)。
116
178
  升级前停驻、升级时还挂着的行由起服的一次性迁移按同一只派生补上这一列(待决的审批门行;文档读不出的行留空,起服日志 `one_time_migrations_applied` 的 `parkedApprovalCardsBackfilled` /
117
179
  `parkedApprovalCardRowsUnreadable`(条数),并一行 warn `parked_approval_card_row_unreadable` 点名会话;卡词超过列容量 —— MySQL 协议 `TEXT` 65,535 **字节**、PG 无上限 —— 的行同一个报法:留空、计数在 `parkedApprovalCardRowsTooLong`,并一行 warn `parked_approval_card_row_too_long` 点名会话(另带字节数),起服不挡:引擎铸的卡词是短词,超长只会是手改的行)。**顺序**:先升库(加列)、再升副本 —— 加完列之后,仍是 7.134.x 的副本**再启动**会对多出的列拒启;同一只库不混跑两版(与第 4 / 16 条同一升级序):
118
- 已在跑的 7.134.x 副本写下的问答停驻行不带这一列,在下一次本版起服之前到期会进 SLA 拒扫(引擎拒收,行停到绝对兜底)而不是作废。
180
+ 已在跑的 7.134.x 副本写下的问答停驻行不带这一列,在下一次本版起服之前到期会进 SLA 拒扫(引擎拒收,行停到绝对兜底)而不是作废;7.136.0 起这些行在拒扫选行里排在带卡词的审批行之后,不再挤占后者的取行窗(卡列同为空的审批行之间仍按到期序,先停光旧副本再起新仍是规避)。
119
181
  **不要**删表重建 `checkpoint`:表里是在飞的停驻(待批的卡、待审的计划)。`DB_BACKEND=local` 与内存后端零动作(停驻行在引擎自己的文件店里,卡从文档读)。
120
182
  **回滚到 7.134.x:只回退二进制不可行**(MySQL 协议与 PG 皆然)—— 加过列的库上,7.134.x 的起服列册对账把多出的 `approval_card` 读成幽灵列、起服拒(`config.schema_mismatch`,拒句点名 `UNKNOWN column(s) approval_card`、不给 ALTER)。
121
183
  回滚 = ① 先停光全部 7.135.0 副本(它们的停驻写与待批队列读点名这一列,列没了而它们还在跑会报错);② 手工 `ALTER TABLE checkpoint DROP COLUMN approval_card;`(两方言同一句);③ 再起 7.134.x —— **本版还加了 `permission_rule.grammar`(第 16 条)这第二枚幽灵列**:只摘这一枚,7.134.x 仍被另一枚拒启;两条回滚段的 ①② 合起来做一遍(停光副本一次、两句 `DROP COLUMN` 都执行)再起旧版。
@@ -156,7 +218,7 @@
156
218
  - **升级序:7.133.x 与 7.134.0 不能混跑**。**先停掉全部 7.133.x 副本,再起第一个 7.134.0 副本**(一次性迁移 M28 在它起服时跑,把升级前停驻行里的明文原地封好:不删行、不动续跑时钟;
157
219
  起服 info 一行 `checkpoint_ctx_credentials_sealed`;M28 的窗 = 发布日 + 30 天,窗外不扫)。多副本:先起一个 7.134.0 副本、等它起好再起其余副本(耐久审批开着时它起好即打出上面那一行)——
158
220
  停驻封签的钥匙在新子目录 `sealed-key/checkpoint-ctx-keys/` 里,升级后第一个起服的 7.134.0 副本生成它(`sealed-key/` 早已共享好也一样:那是模型钥匙的那一层);
159
- 同时起多个副本会各建一把,互相答 `key_not_held`,直到各自重启一次(钥匙只在起服时读目录里的全部文件)。混跑时 7.133.x 副本续跑一条已封签的行(M28 封过的,或 7.134.0 副本新写的)会把凭据位当空值 ——
221
+ 同时起多个副本可能各建一把:钥匙创建是先读目录、没读到可用的钥匙文件才生成并写入,没有互斥 —— 只有两个副本在同一个目录上同时首启、都没读到可用的钥匙文件时才各建一把;各建了就互相答 `key_not_held`,直到各自重启一次(钥匙只在起服时读目录里的全部文件);等第一个副本起好再起其余,其余副本读到的就是它写下的那一把。混跑时 7.133.x 副本续跑一条已封签的行(M28 封过的,或 7.134.0 副本新写的)会把凭据位当空值 ——
160
222
  MCP 的 `headers` / `env` 整键不交(server 照挂)、A2A 对端不挂、`settings.env` 不采纳、WebSearch 不挂、`settings.hooks` 整份不采纳(同一份里的 command hook 一起)—— 任务无凭据照跑、不报错
161
223
  (只有运维日志各一行 warn);7.133.x 的续跑读者没有对封签值的拒臂,新版本改不到它。反过来,7.133.x 副本新写的明文行在 7.134.0 副本上续跑答 409 `reason:"plaintext"`。
162
224
  - **回滚**:降级到 7.133.x 之前先把带凭据的停驻行结掉(决定或取消);已封签的行在 7.133.x 上同样是无凭据续跑、不报错。随包排空检查的 `sealed-credentials-rollback` 判据列出它们:
package/USAGE.md CHANGED
@@ -123,6 +123,8 @@ ANTHROPIC_API_KEY=sk-ant-… # 可选:ANTHROPIC_BASE_URL / ANTHROPIC_VERSION /
123
123
  中心 / config.d 下发的候选目录里有解析不到的条目 ⇒ 候选的模型面这一组被拒、上一代模型面继续服务(同候选其余组照落)。
124
124
  - **目录与 `MODEL_ID` 并存**:目录一旦被采纳就整张换掉 env 表 —— 缺省模型取目录缺省,env 的 `MODEL_ID` 不服务。此时起服**不拒**(哪怕没设网关:目录条目自带 `baseUrl` 即可),
125
125
  而是说一行 warn `config_env_set_but_ineffective`(`env: "MODEL_ID"`,点名目录缺省是谁),让你知道改 `MODEL_ID` 没有效果 —— 目录第一次被采纳的那一拍说,恰好一次(启动时就采纳、或目录晚到 / 刷新时才第一次采纳都一样)。`MODEL_ID` 仍是目录缺席时的兜底(目录迟到且无上一份 / 候选被整拒 / 没有启用条目时由它服务,届时按 env 表判)。
126
+ - **anthropic 车道两把凭据一名一义**:`ANTHROPIC_API_KEY` ⇒ 只发 `x-api-key` 头;`ANTHROPIC_AUTH_TOKEN` ⇒ 只发 `Authorization: Bearer` 头;两把都非空 ⇒ 起服拒(`[config.anthropic_credential_conflict]`,
127
+ 点名两键、不回显值)—— 只留你的端点要的那一把。空串 = 未设(compose 透传的 `${VAR:-}` 不算同设)。会话标题 / hook 的直连腿与主请求读同一只判断,发同一种头。
126
128
  - **凭据什么时候生效**:网关 key `MODEL_API_KEY`(以及 `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN`)在启动时读一次,**换值 = 重启本服务**
127
129
  —— 没有热应用通道(以前的版本同样如此:这几把 key 只来自启动时的环境变量;引擎 7.51.0 起模型路由的构造配置也只读一次)。
128
130
  目录条目**自己的**凭据(`apiKeyEnv` 点名的变量、`sealedApiKey`)跟着中心 / config.d 的模型面热应用走,不用重启:新起的任务与调用
@@ -151,7 +153,7 @@ ANTHROPIC_API_KEY=sk-ant-… # 可选:ANTHROPIC_BASE_URL / ANTHROPIC_VERSION /
151
153
  引擎(core)不读 `MODEL_*` / `ANTHROPIC_*` 这些键,键表和读法属本服务;壳首启、客户端运行时要在引擎起之前问「这个环境模型走哪条车道、端点 / 凭据取哪一对键」,读这一只,不各抄键名。
152
154
  `MODEL_ROUTE_ENV_KEYS` 是九把路由键的闭表(网关端点 / 备用端点 / 网关凭据、provider、主 / cheap 模型 id、三把 Anthropic 键),`readModelRouteEnv(env)` 是读口(`env` 交整张 `process.env` 或等价快照;空串 = 未设),
153
155
  `laneAndEndpointFor(env, model, routes)` 是值级端口:`routes` 是引擎路由表的原型(`anthropic` 键在场 ⇔ anthropic 路由装上),答 `{ lane, endpointEnvName, endpointDeclared, apiKeyEnvName }` ——
154
- **只有键名与在场性,没有任何 env 值**(端点与凭据的值调用方自己从 env 取)。凭据取值序与引擎装配同序(`ANTHROPIC_AUTH_TOKEN` 压过 `ANTHROPIC_API_KEY`);网关凭据 `MODEL_API_KEY` 只与网关端点配对,
156
+ **只有键名与在场性,没有任何 env 值**(端点与凭据的值调用方自己从 env 取)。anthropic 凭据键与引擎装配读同一只判断(一名一义:设了哪一把就报哪一把;两把同设 ⇒ 抛起服的那一句拒句,不替你挑一把);网关凭据 `MODEL_API_KEY` 只与网关端点配对,
155
157
  网关端点没设就不报凭据键(与起服不装载那把 key 同向)。
156
158
 
157
159
  **可选 — 韧性栈:分级超时 + 断路器(core 1.38,叠在 failover 之下)**
@@ -724,6 +726,7 @@ SEND_USER_FILE_SANDBOX_PUT_ENDPOINT=… # 可选:沙箱直传 PUT 的端
724
726
  - **词表优先级**:内网端点 `MINIO_ENDPOINT` 优先,`S3_ENDPOINT` 是外接 S3 兼容位(内网/公网同址场景可只给它,同时充当公网面);公网端点 `S3_PUBLIC_ENDPOINT` 优先,缺席回落 `S3_ENDPOINT`。空串一律按未设处理。
725
727
  - **🔴 云形(`DB_BACKEND=mysql|pg`)快照 blob 强制对象存储(1.295+,clay 拍)**:缺 MinIO 三键 ⇒ **boot 拒启**——单行 SQL blob 写会撞 TiDB `txn-entry-size-limit`(默认 6MiB,真库实测矮墙)/ mysql 协议 `max_allowed_packet`,大字节归对象存储(与 D-1 附件面同裁定)。单机/测试台显式逃生:`SNAPSHOT_BLOB_ALLOW_SQL_BYTES=true`(SQL 店此时带 per-blob 帽,MySQL 协议(InnoDB 与 TiDB)默认 = **写入那条数据库连接**报的两堵已知墙取最小:单行上限减 1 KiB(blob 行自己的余量:哈希键 + 长度 + 时间戳;TiDB 才有)与包墙 `max_allowed_packet` 扣 1 KiB 语句余量后折半(⌊(包墙 − 1 KiB) ÷ 2⌋:文本协议把 blob 写成十六进制,语句至少是它的 2 倍);任一读数读不出时按该库族文档缺省并留一行运维日志 `fail_open`(tag `server.sql.packet-cap-unread` / `server.sql.tidb-row-cap-unread`),超限 PUT 413 `blob_too_large_for_sql`,句子按帽的出处分三形(运维旋钮 / 库报的墙 / 读不出而按文档缺省假定的墙),机读位 `wallSource` 同词 `operator` / `reported` / `assumed`。包墙是每条连接自己的会话值(7.133.0 起按连接判):DBA 调大 `max_allowed_packet` 之后,池里的旧连接在它第一次按包墙拒 blob 时自换(运维日志一行 `sql_connection_packet_wall_stale`,那一笔在新连接上重判),不必重启副本;`GET /v1/diagnostics/wiring` 的 `sqlEngine.maxRowBytes` / `sqlEngine.maxPacketBytes` 是最近一次初始化的那条连接的读数(见部署前置清单 DEPLOY-PREREQS(仓内)「数据库」条);帽可用 `SNAPSHOT_BLOB_SQL_MAX_BYTES` 按部署真实限值覆写,两方言生效;pg 默认无帽)。
726
728
  - **per-file rewind 历史的边界上限**(与 workspace 浏览面同一个整树快照纪元退役批留下的保留条款):`FILE_HISTORY_RETENTION_KEEP` —— 每个 scope(会话)存活的**最新** boundary 数,**每次 boundary 提交后自剪**(最旧者先亡,刚提交者恒存活;失引用的版本行级联删,blob 字节走异步 grace 窗 GC;v1 基线永存)。未设=core 缺省 **100**(由 core 的 `resolveFileHistoryRetention` 兑现,server 不复制那个数);`unbounded` 是**唯一**的「不剪」拼法(整店自剪关闭,GC 归部署自排 `reap`);`0`/负数/小数/非数 ⇒ **boot 拒启**(码 `config.retention_policy_invalid`,与 core 内置后端同码同判——**省略这个键不等于不剪**,它选的是缺省 100)。三条车道(TiDB / PG / local 文件店)同旋钮同判。⚠️ 升级即生效:存量超 100 boundary 的 scope 在下一次提交时被剪到 100;要保全量先写 `unbounded`。被剪 entry 的键**已花掉**(再次 snapshot 同 entry 典型拒,不复铸)。排序=DB 分配的 per-scope 发布序号 `file_history_boundary.publish_seq`(不是墙钟):**自 7.50.x 有存量 `file_history_boundary` 的部署升级本版会在 boot 期被 `assertFileHistorySchema` 拒启并指明 `DROP TABLE file_history_boundary`**(本仓无 ALTER;丢的是 per-file rewind 便利态,会话与其他持久面不动)。
729
+ - **会话同步重推(同图)要读 blob**(7.136.0 起):`POST /v1/sessions/:id/sync/import` 的 identical 重推臂,对云端「已握着同一张文件历史图」的目标,也先把信封引用的每一枚 blob 经云端自己的店读一遍、核哈希(不回写),再答成功 —— 对象存储里的对象若在图发布之后丢了,这一腿答 `422 missing_blob`(`PUT` 缺的 blob 再推;图不动),不再答 `200 identical`。代价:无变化会话的频繁重推多付对象存储读流量(每枚引用的对象一次下载,正比于引用的 blob 字节);没有旋钮。
727
730
  - **文件历史捕获总开关**(7.97.0 立,7.104.0 改语义):`FILE_HISTORY_CAPTURE`(缺省 `true`;布尔词表 true/false/1/0/yes/no/on/off,未知词 ⇒ boot 拒启;TiDB / PG / local 三条车道同义)。`false` ⇒ **只关捕获**:历史店照接,引擎拿到部署捕获位 `RunnerDeps.fileHistoryCapture: "off"`,改动文件的首次快照**一个都不捕获**、不铸回合边界(`wiring_manifest.fileHistory` 报 `"disabled"`);**已存历史照常可用** —— `resumeAt`+`restoreFiles` / `rewindFilesTo` 照常还原、2c session-sync 照常导出 / 导入。`GET /v1/capabilities` 的 `fileHistoryCapture` 报 `"off"`,而 `rewindFiles` / `restoreFiles` / `rewindFilesTo` / `sessionSync` 仍为 `true`(缺省部署报 `"on-optional"` = 捕获中且请求可按 run 关掉,见上一段)。🔴 **7.97.0–7.103.x 的旧语义**:`false` = 不把店交给引擎,连已存历史也读不了(还原走 `rewind.store_unconfigured`、sync 501)—— 把本旋钮当成「整个文件历史功能关掉」来用的部署,升级后已存历史**重新可以被还原 / 同步**。要让已存历史也不可用,今天的出路只有**逐会话** `DELETE /v1/sessions/:id`(擦除面不受本旋钮管)与 SQL 后端的留存清扫(`RETENTION_MODE=enforce` + `RETENTION_MAX_AGE_DAYS` + `RETENTION_SWEEP_INTERVAL_SEC`,按会话年龄删,local 车道没有留存车道);**没有**「整库历史一键不可用」的旋钮。⚠️ **关掉 ≠ 删掉**:翻旋钮不批量删除存量历史;local 车道上删会话走 `reap` —— 边界删光、v1 基线按 core 契约留存,既有残余。关着时 `FILE_HISTORY_RETENTION_KEEP` 没有效果(不再有边界提交,自剪不会触发),值仍照常校验。没有存储后端的部署(env-only worker)本来就没有历史店,旋钮在那里无事可关、不下发捕获位(引擎对「off 但没店」拒 prepare)。
728
731
  - **🔴 公桶策略必须 GetObject-only**:`mc anonymous set download` 会**连带打开 ListBucket**——匿名 `GET /<bucket>/?list-type=2` 能枚举全部不可猜 key,能力链接设计即告失效。正确姿势=`mc anonymous set-json`,policy 只含 `Action:["s3:GetObject"]` on `arn:aws:s3:::<bucket>/*`(验证:对象 GET 200、桶 LIST 403)。
729
732
  - **两条轨**:ttl=0(默认)→ 匿名 GET 公桶下 `uuidv7/<name>` 不可猜 key,链接永久、可回收(删对象);0<ttl≤7 天 → 私桶 + SigV4 限时签名链接(7 天是 SigV4 物理上限,更久用 ttl=0)。
@@ -978,6 +981,7 @@ UNREACHED_ASK_TTL_MS=3600000
978
981
  (代价是超限的 ask 走 park,要有人去审批队列捞);库压大 → 调小 `STREAM_APPROVAL_RECONCILE_BATCH`
979
982
  (代价是收敛变慢,靠队列轮转保证下轮接着扫)。
980
983
  - **停驻行最长活多久(local 文件后端与 SQL 店同一公式)**:一张 pending 的 checkpoint 行在 `max(createdAt + APPROVAL_TERMINAL_BACKSTOP_MS, deadline + 1h)` 到点后由后台 reaper(每 `REAP_INTERVAL_SEC`,缺省 60 s 一拍)expire;`deadline` 只在配了审批 SLA / TTL 时有,没配就是 `createdAt + 后备上限`。旋钮缺省 30 天、下钳 60 s、非有限值落缺省。到点的行先 expire,挂起的 run 行随后由 reaper 翻成 `failed`(`errorCode: approval.expired`)。所以「老停驻行在 local 后端最长活多久」= 后备上限(缺省 30 天)+ 最多一拍;要缩短就调 `APPROVAL_TERMINAL_BACKSTOP_MS`(两店同辖)。
984
+ - **SQL 店写停驻行的预判与 SLA 拒扫的取行序(7.136.0 起)**:`put` 先看这份检查点放不放得进索引列 —— 没有 `gate` / `pendingAction` 对象,或门上卡词的 UTF-8 **字节**数超过 `approval_card` 列的容量(MySQL 协议 `TEXT` 65,535 字节;PG 无上限),抛 `CheckpointIndexRefusedError`(`err.refusal.kind` 为 `unreadable` / `card_too_long`),一句 SQL 都不发 —— 严格与非严格 sql_mode 的库都一样,不会落成半只卡词;引擎自己铸的检查点不会触发。SLA 拒扫每拍至多取 `LIMIT`(缺省 100)行,取行序是「卡列有词的先取,其后按到期时刻」:混跑窗里旧副本停驻的问答行(卡列为空)排在带卡词的真到期行之后。
981
985
  - **审批账本保留多久(`APPROVAL_ASK_RETENTION_MS`,缺省 `2592000000` = 30 天;local 文件后端与 SQL 店同一条律)**:审批在盘上有三本账 —— 问账(SQL 的 `approval_ask` / `approval_batch`,local 的
982
986
  `<数据根>/approval-asks/asks.jsonl`)与 local 审计账(`<数据根>/approval-ask-audit/ledger.jsonl`,`GET /v1/approvals` 的 `crashConverged` 读它)。一问**结了**且它最后一次动静早于
983
987
  `now − APPROVAL_ASK_RETENTION_MS`,它在三本账里的全部记录由维护 tick 整问删掉。「结了」= 该问在它那本账里到了终局,**且它挂靠的东西都不再能动**:挂靠 = 发起它的 run、宿主会话、
@@ -1364,6 +1368,8 @@ MCP_ELICITATION_TTL_MS=300000 # 一张没人填的表单挂多久后释放(
1364
1368
  - 四个节流钮都是 `numEnvBounded`:**越界启动期响亮拒,不静默夹取**(一个手滑的多小时 TTL 会让表单实际上永不过期)。
1365
1369
  - 总开关关着时四个节流钮解析照跑但无消费者——不设=行为不变。帧格式见
1366
1370
  [`docs/ASSISTANT-WIRE-CONTRACT.md` §4a-quater](docs/ASSISTANT-WIRE-CONTRACT.md)(`elicitation` / `elicitation_complete`)。
1371
+ - **回给 MCP server 的词**(一事一词):没人应答的释放 —— 答题窗到期、run 中止、腿退出、表单没送达 —— 回 `cancel`;本部署按规则当场拒(没有 run 上下文 / url 形 / 节流越界)回 `decline`;
1372
+ 用户经 respond 答的词原样落地。7.135.x 及以前,答题窗到期与表单没送达回 `decline`。判法与 helper 该怎么读见契约 §4a-quater 的「终局词律」。
1367
1373
  - 两帧同样**随 run 账本回放**(与上面的问题卡同一条规则:`/v1/tasks/stream` 腿有 durable run 行时先落账再直写,`/v1/runs` 腿只有账本),重载后经 `GET /v1/runs/:id/events` 取回表单卡;
1368
1374
  `elicitation` 开帧带 `expiresInMs` / `expiresAtMs`,待答判法与问题卡同一把。**答题口不变**:`POST /v1/elicitations/:id/respond` 只在受理那一张表单的副本内、窗内有效。
1369
1375
 
@@ -1868,10 +1874,10 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
1868
1874
  | 端点 | 用途 |
1869
1875
  |---|---|
1870
1876
  | `GET /v1/sessions/<id>` | 审计回溯:当前上下文 + 摘要(owner 校验);消息里模型 / 工具产出的文本与 live 帧、账本帧同一只脱敏(密钥形换占位,其余字节不变);Read 图像 / PDF 的媒体卡(工具结果消息的 `details`)的 `file.base64` 是字节编码,逐字。**委派子代的转录**也走这一口:`<id>` = 子代帧 / fleet 子代行上的 `transcriptId`(未压缩过的子代首条用户消息是委派 prompt;子代跑过的工具是 assistant 消息里的 `toolCall` 段 + 同 `toolCallId` 的 `toolResult` 行;整段逐字条目走 `…/sync/entries`)。体顶层加性键 `agent: { id?, parentSessionId?, rootSessionId? }`(**7.133.0+**)说这是哪只子代:只投影会话行上存下来的委派元组,`id` 与 fleet 行的 `id`、`GET /v1/agents/roster` 的 `handle` 同一个串;键缺席 = 普通会话 / 老 server / 会话店不存元组(local 后端),读作「没存」、回落 fleet 行 / 名册上的 id。子代会话的属主是铸行那一刻它所在委派树的根会话的属主(**7.133.0+**,写下那一行时按当时读到的写入;此前 SQL 后端上子代会话无主、谁都读得到)⇒ 用发起子代的那位主体读;别的主体与不存在的 id 同一个 `404 not_found.session`。属主为空的委派子代行(根会话本就无主,或升级前写下、还没回填)只有 fleet 调用方读得到(出示 service 凭据、不带主体头;dev-open 部署不钉人),任何具名主体、没有 service 凭据的无身份调用方同样 404(存量无主会话互认臂 —— 单用户形下 `default` 主对无主的普通会话放行,fork / 删除 / `/sync/*`(同步族自 7.134.0 起)同走这一只 —— 对这些 placed 行同样不放行,审批豁免、notify 同样不放行;fork / 删除 / `/sync/*` 的 fleet 调用方与此处同一个判据:出示 service 凭据、不带主体头,带主体头的 service 凭据请求是那位主体,7.134.0 起);SQL 后端每次起服回填这些行(起服读数行 `placedSessionOwnersRemaining` 非零 = 还有没定下的,再起直到为 0,或等它们下一次停驻)。推不出属主的子代写成属主 `_orphan:<id>`,只有 fleet-wide / operator 读得到:SQL 后端的子代行根会话已删时按它记录的父会话,父也删了才是 `_orphan:`;停驻时拷进宿主店的那一份(local 后端)只按根判,根会话已删 ⇒ `_orphan:`;子代被派出 / 批准之后、再次停驻读根之前根会话被删并由别人以同一 id 重新登记 ⇒ 其下的子代再次停驻被否决(读过根之后才换的,写进去的仍是读根那一刻的属主) |
1871
- | `GET /v1/approvals?owner=user:42` | 高危写审批(durable,需 `DURABLE_APPROVAL=true`):**operator** 看待办队列(可按 owner 过滤);非 operator 只看自己的。队列里某一行的存储体读不出来、重试不会好时,答 `500` + `errorCode:"approvals.read_corrupt"`(**7.134.0+**;与订阅流同码、同一只判官;运维处置那一行后再读,别原样重试);此刻读不出队列(连接 / 超时 / 店抖动)⇒ `503` + `errorCode:"approvals.read_unavailable"`(**7.135.0+**,此前 500 `internal.error`;稍后重发,这一答不表示队列为空)。`GET /v1/assistant/inbox` / `GET /v1/assistant/tasks` 读同一个队列,两种读失败同码同状态(**7.135.0+**) |
1872
- | `GET /v1/approvals/stream` | 同一队列的 SSE 推送(范围同上):首拍快照 → `pending` / `resolved` 增量(开了工具审批时另有 `live_pending` / `live_resolved`),空闲时心跳,15 分钟上限(`STREAM_MAX_DURATION` 帧后重连)。**心跳只表示流活着,不表示这一拍读过队列**:队列读不出时每次读失败发一帧 `error`(`approvals.read_unavailable`,流留着、下一拍再读,恢复后同一条流继续);某一行的存储体读不出来、重试不会好时发 `approvals.read_corrupt` 并收流(运维处置那一行后再重连)。15 分钟上限对读失败的拍同样算数 |
1873
- | `POST /v1/approvals/<sessionId>/decide` `{"decision":"approve"|"deny","reason":"…"}` | 批/否并恢复挂起任务(CAS,重复决议 409);**仅 operator**(非 operator → 403)。5.0.0 起旧轮询腿 `POST /v1/approvals/<id>` 已退役 |
1874
- | `GET /v1/tool-approvals/<approvalId>` | 按问读口(**7.133.0+**;与 `POST …/respond` 同门同身份,不是你的 ask 与查无同形 404):一只审批问此刻在哪一态,`state` ∈ `pending` / `decided` / `parked` / `void`(闭集)。停驻卡被答过 ⇒ `decided`、停驻 run 被取消 ⇒ `void`(**7.134.0+**;7.133.x 恒答 `parked`);停驻行的门已结算却读不出可用的决议(含坏行:状态词 / 种类 / 当事方不在闭集、结算记录过不了引擎的不变量筛、缺必填成员、时刻 / 方向读不出)⇒ 404 `tool_approval.not_pending`,运维日志一行 error(`defect` 首条缺陷句 · `defectCount`)。停驻行的 `decided` 可回到 `parked`:续跑腿在被批准的动作开跑前失败时,引擎重开同一张门 —— 读口答 `parked` + `decidePath` 并另带 `reopened`(**7.135.0+**):`reason` 是引擎的重开因由词(`env_failed` 按原决议经 `decidePath` 再交一次 —— 本副本还留着首次受理回执时(缺省 1 小时内)`…/respond` 回放那张回执、不驱续跑;`tool_unavailable` / `constraint_rejected` 重新决;`config_refused` 改好配置再交或重新决),`decision` / `decidedAtMs` / `decisionNote?` 是重开之前的那次决议(读不出 ⇒ 只带 `reason`);再决之后回到 `decided`。续跑被取消 ⇒ 重开的门被收掉,读作 `void`。全文见契约 §4a-bis-read |
1877
+ | `GET /v1/approvals?owner=user:42` | 高危写审批(durable,需 `DURABLE_APPROVAL=true`):**operator** 看待办队列(可按 owner 过滤);非 operator 只看自己的。队列里某一行的存储体读不出来、重试不会好时,答 `500` + `errorCode:"approvals.read_corrupt"`(**7.134.0+**;与订阅流同码、同一只判官;运维处置那一行后再读,别原样重试);此刻读不出队列(连接 / 超时 / 店抖动)⇒ `503` + `errorCode:"approvals.read_unavailable"`(**7.135.0+**,此前 500 `internal.error`;稍后重发,这一答不表示队列为空)。`GET /v1/assistant/inbox` / `GET /v1/assistant/tasks` 读同一个队列,两种读失败同码同状态(**7.135.0+**)。停驻的**技能卡**(对引擎 `Skill` 工具的调用)的行带 `boundGrantDigest` / `boundGrants`(批准它还授予的东西:技能名 + 两只工具清单;清单只在哈希等于摘要时才有,是部署文本,渲染要按上下文转义),分类器没给判词的卡带 `classifierNoRuling`(`error` / `timeout` / `parse_error`)—— **7.136.0+**,与 `GET /v1/assistant/inbox` 行逐字同值(此前只有收件箱行与活卡帧带);普通卡三位都缺席。`GET /v1/assistant/inbox` 行的 `objective` 是门所属 run 的**目标预览**(写入时已脱敏、截到约 120 字符,与 `GET /v1/assistant/tasks` 读同一列;委派子代的门读派它的那条 run;没有 run 账本 ⇒ `null`;**7.136.0+**,此前原样回显存着的任务目标,运维 `?owner=` 跨看也是原文) |
1878
+ | `GET /v1/approvals/stream` | 同一队列的 SSE 推送(范围同上):首拍快照 → `pending` / `resolved` 增量(开了工具审批时另有 `live_pending` / `live_resolved`),空闲时心跳,15 分钟上限(`STREAM_MAX_DURATION` 帧后重连)。**心跳只表示流活着,不表示这一拍读过队列**:队列读不出时每次读失败发一帧 `error`(`approvals.read_unavailable`,流留着、下一拍再读,恢复后同一条流继续);某一行的存储体读不出来、重试不会好时发 `approvals.read_corrupt` 并收流(运维处置那一行后再重连)。15 分钟上限对读失败的拍同样算数。`pending` 帧带的行与 `GET /v1/approvals` 同一只投影(含上面的技能卡三位,**7.136.0+**) |
1879
+ | `POST /v1/approvals/<sessionId>/decide` `{"decision":"approve"|"deny","reason":"…"}` | 批/否并恢复挂起任务(CAS,重复决议 409);**仅 operator**(非 operator → 403)。5.0.0 起旧轮询腿 `POST /v1/approvals/<id>` 已退役。停驻的后台子代**复活不了**(行上没有 agent 名 —— 今天未命名的后台子代就是这一形 —— 或没有根会话锚)⇒ `deny` 作废这张卡:`200 {taskId, status:"failed", decision:"deny"}`,卡离开待决队列、子代终局 `failed`;`approve` 答 `409 decide.row_unrevivable`、拒句指路 deny(**7.136.0+**;此前两个决定都 409,卡处理不掉) |
1880
+ | `GET /v1/tool-approvals/<approvalId>` | 按问读口(**7.133.0+**;与 `POST …/respond` 同门同身份,不是你的 ask 与查无同形 404;**7.136.0+** 两扇决议口 —— `POST …/respond` 与耐久决断口 `POST /v1/tasks/<taskId>/asks/<askId>/decision` —— 与本口同一只属主判据:有行的问读行上当下的属主,收编之后新属主三口都认、前属主三口都 404;本口在店读不出时只对本副本自己铸的活登记回落登记(登记属主认你 ⇒ `pending`),起服接管的登记不回落、答 404 —— 与同一刻的应答口同答,此前本副本上的活卡读的是铸卡那一刻记下的属主):一只审批问此刻在哪一态,`state` ∈ `pending` / `decided` / `parked` / `void`(闭集)。停驻卡被答过 ⇒ `decided`、停驻 run 被取消 ⇒ `void`(**7.134.0+**;7.133.x 恒答 `parked`);停驻行的门已结算却读不出可用的决议(含坏行:状态词 / 种类 / 当事方不在闭集、结算记录过不了引擎的不变量筛、缺必填成员、时刻 / 方向读不出)⇒ 404 `tool_approval.not_pending`,运维日志一行 error(`defect` 首条缺陷句 · `defectCount`)。停驻行的 `decided` 可回到 `parked`:续跑腿在被批准的动作开跑前失败时,引擎重开同一张门 —— 读口答 `parked` + `decidePath` 并另带 `reopened`(**7.135.0+**):`reason` 是引擎的重开因由词(`env_failed` 按原决议经 `decidePath` 再交一次 —— 本副本还留着首次受理回执时(缺省 1 小时内)`…/respond` 回放那张回执、不驱续跑;`tool_unavailable` / `constraint_rejected` 重新决;`config_refused` 改好配置再交或重新决),`decision` / `decidedAtMs` / `decisionNote?` 是重开之前的那次决议(读不出 ⇒ 只带 `reason`);再决之后回到 `decided`。续跑被取消 ⇒ 重开的门被收掉,读作 `void`。全文见契约 §4a-bis-read |
1875
1881
  | `GET /v1/agents/roster` | 后台 agent **名册**(7.29 起,需 durable background-agent store,否则 501 `capability.background_agent_store_required`):调用方自己 scope 的 `a…` 行,新→旧。行只带 `handle`/`name`/`agentType`/`status`/`spawnedAt`/`updatedAt`/`settledAt`(content-free;子代产物正文走 `GET /v1/runs/<id>/subagents/<handle>/output`)。查询:`?limit=`(1..100,默认 20)、`?before=`(上一页的 `nextBefore` 游标)、`?status=`(running\|parked\|completed\|failed\|killed)、`?session=`(按会话树窄化)、`?scope=`(**仅** fleet 调用方:不带主体头的 service 凭据 / 只读 `TRACE_TOKEN`(只在没配 service 凭据的部署上够得着这一口)/ dev-open;带主体头的调用方 —— 含带 service 凭据的 —— 锁定自己)。回体 `{agents,total,nextBefore?,truncated?}`;`truncated:true` = 租户行数超过名册窗(500),`total` 此时是窗内数。非属主的行**不出现也不计数**(无存在性 oracle) |
1876
1882
  | `GET /v1/capabilities/scenarios/<name>` | 单个场景的只读详情:工具面、提示词概览、**本部署现在跑不跑得动**、附件供给、**技能清单**(见下) |
1877
1883
  | `GET /v1/capabilities/mcp[?scenario=<场景名>]` | **无需先跑 run 的逐台 MCP 状态**(现连、列工具、关掉;按调用方限速 + 30 秒复用)。答的是**某个场景**会挂哪些中心声明的 MCP 服务器:不带参数 = 这位主体 run 时的场景(**7.131.0+**:中心按主体指派的场景胜出,否则部署缺省场景;此前恒读部署缺省场景;指派悬空 ⇒ `503 state.scenario_assignment_unknown`,与提交任务时同一句;`GET /v1/sessions/:id/mcp` 与 `…/a2a` 两张会话面板同读这位主体 run 时的场景:a2a 面板悬空时同样整面 503,而 mcp 面板另有「这个会话上一次真挂上了什么」那一半(`lastLegMcp`),悬空时答 `200`、`lastLegMcp` 照答、`servers` 缺席、拒因在 `scenarioRefusal: { errorCode, error, detail }`(与状态面整面 503 的体是同一份成员,`detail` 是门拒的机读事实 `{ scenario, source }`;`servers` 缺席 = 场景这一半没答,`servers: []` = 该场景没配 MCP));`?scenario=<名>`(**7.130.0+**)= 点名场景,可见集与同场景任务 run 时是同一只解析、过同一只场景门 —— 不在册 `400 scenario_unknown`(附在册词表;空串 `?scenario=` 同此,与提交 `scenario: ""` 同答)、这位 principal 的场景白名单集外 `400 scenario_not_allowed`(附 allowlist),两者的体与提交任务时的拒逐字同形;重复给出参数 `400 request.field_invalid`(提交腿没有这一形)。场景门接缝 `scenarioGate` 是 `createHttpServer` 的必填件:自建 `ServiceDeps` 的嵌入方用包根导出的 `createScenarioGate` 铸它(`"peek"` 腿给读面),没递就是 tsc 红。老 server 忽略这个参数、回缺省场景那一份,按 `GET /v1/capabilities` 的 `version` 判。`REQUIRE_PRINCIPAL` 下无身份 `401`(先于场景门)。每一行(两条口同形)在 §G.7 的行形之外多两个可选键(**7.133.0+**):`toolNames`(这台会挂上的工具名,引擎命名空间形 `mcp__<server>__<tool>`,读数序)+ `toolNamesTruncated: true`(名单超过 200 只时只截表,`toolCount` 仍是真数;读数用 `toolCount`,别用 `toolNames.length`);该台读数里没有名单 ⇒ 两键缺席(不编 `[]`),没连上的台是在场的 `[]`;老 server 没有这两键,按缺席读。自带 `.mcp.json` 的探测口是 `POST /v1/capabilities/mcp/probe` |
@@ -1906,9 +1912,10 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
1906
1912
  > 单用户 + 名单空 + 没声明 + 有审批底座 ⇒ 起服 warn `operator_undeclared_single_user`(不拒启)。
1907
1913
  > ⚠️ **多租户形拒启**:`DURABLE_APPROVAL=true` + `REQUIRE_PRINCIPAL=true` 而 `OPERATOR_PRINCIPALS` 空 ⇒ 进程启动失败并点名修法(空名单下没人能当这台多租 worker 审批队列的 operator)。设名单,或确属单租户则不设 `REQUIRE_PRINCIPAL`。
1908
1914
  > 🔒 **停驻任务续跑上下文里的凭据是密封的(7.134.0 起)**:任务停驻时存下的提交体里,请求自带的凭据(`mcpServers[].transport.headers` / `.env` 的值、`a2aPeers[].headers` 的值、`settings.env` 的值、
1909
- > `settings.webSearch.apiKey`、http hook 条目的静态头 `settings.hooks.<事件>[].hooks[].headers` 的值、command hook 条目的插件选项 `settings.hooks.<事件>[].hooks[].plugin.options` 的值〔7.135.0 起〕)只以封签落盘(对着这一域自己的保管钥匙 `<数据根>/sealed-key/checkpoint-ctx-keys/` 最新那把公钥封、
1915
+ > `settings.webSearch.apiKey`、http hook 条目的静态头 `settings.hooks.<事件>[].hooks[].headers` 的值、command hook 条目的插件选项 `settings.hooks.<事件>[].hooks[].plugin.options` 的值〔7.135.0 起〕、子进程的命令行 —— stdio 传输的 `mcpServers[].transport.command` / `.args` 与 command hook 条目的 `settings.hooks.<事件>[].hooks[].command` / `.args`〔7.136.0 起;`args` 整只数组封成一个值〕)只以封签落盘(对着这一域自己的保管钥匙 `<数据根>/sealed-key/checkpoint-ctx-keys/` 最新那把公钥封、
1910
1916
  > 绑着这一行的会话;与 sealed 模型钥匙那对钥匙不共用),续跑时开封;开不开 ⇒ `409 conflict.checkpoint_ctx_unsealable`(`field` + `reason`)。
1911
- > URL 一类自由文本不入封签位:体里「值是 URL」的四个座位(MCP `transport.url` · A2A 对端 `url` / `cardUrl` · http 钩子 `url` · 搜索后端 `endpoint`)改由受理口拒绝带凭据的 URL(7.135.0 起,见 9.5g)。
1917
+ > 入封签位的判据只有一条:本服务替调用方原样交出去的配置值(子进程的命令行与 env、发往对端的静态请求头、搜索钥匙),值是不是秘密本服务不判、整格封 —— 所以命令行里的 `--token=…` 不必挪进 env。
1918
+ > URL 一类自由文本不入封签位:体里「值是 URL」的座位(MCP `transport.url` · A2A 对端 `url` / `cardUrl` · http 钩子 `url` · 搜索后端 `endpoint`;图片 `images[i].url` 7.136.0 起)改由受理口拒绝带凭据的 URL(见 9.5g)。
1912
1919
  > 因此耐久审批在场的部署(`DURABLE_APPROVAL`;单用户 + 有耐久店时缺省开)要能建保管目录(建不了 ⇒ 起服拒;不要停驻就显式 `DURABLE_APPROVAL=false`),`<数据根>/sealed-key/` 要跨重启持久
1913
1920
  > (SQL 后端也一样:钥匙不进库;不持久 ⇒ 每次重启换钥匙,重启前停驻的任务续跑答 `reason:"key_not_held"`),多副本要让每个副本的 `<数据根>/sealed-key/` 是同一份(§9.10 ⑦)。升级与回滚序见 §9.10 ⑦ 与 MIGRATION 7.134.0 第 1 条。
1914
1921
  >
@@ -2257,7 +2264,7 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
2257
2264
  | HTTP `429` + `errorCode:"limit.cost_quota_exceeded"` | per-principal 累计成本配额越顶(`MAX_PRINCIPAL_COST_USD`;**进场门**,不打断在跑的 run) | 看 `Retry-After` / 体 `retryAfterSec` 退避(体 `windowResetsAt` = 当代窗滚过的时刻);窗滚过或调高上限后放行 |
2258
2265
  | HTTP `400` + `errorCode:"request.id_invalid"` | 调用方给的 id 形不对。**会话 id 的形门只有一只** = 任意版本 canonical uuid(壳预设的 v4 也过门):会话口 `:id`(fork / 抹除 / 会话策略 / 2c 同步)与提交口(`POST /v1/tasks` / `/v1/tasks/stream` / `/v1/runs` 体里的 `sessionId`,A2A 的 `contextId` 同)同判,提交口的句子在句首之后点名「改用 uuid、自己记映射」;`GET /v1/sendfile-links?before=` 游标与 `body.taskId` 幂等键要求 uuidv7(时间序 / 幂等键契约) | 按 `errorCode` 分支;句子各自点名要求的形(`must be a canonical uuid` / `must be a uuidv7 …`),只作展示、别锚文案 |
2259
2266
  | HTTP `400` + `errorCode:"request.invalid_json"` + `reason:"not_json"` \| `"not_object"`(`reason` 7.133.0 起) | 请求体不是合法 JSON(`not_json`;所有读体的口同一个体 —— 含 `notify` / `wake` / 权限规则这几口,它们 7.132.x 及以前答 `request.body_shape`,其余多数口答 `request.rejected`),或合法 JSON 却不是这一口要的对象(`not_object`,`POST /v1/runs/:id/compact`)。读体超 8 MiB 是 `413 request.payload_too_large`(`field:"body"` / `reason:"too_large"` / `cap`,没有 `count`),不是本码 | 修请求体;按 `errorCode` + `reason` 分支,句子只作展示 |
2260
- | HTTP `400` + `errorCode:"request.field_invalid"`(`PATCH /v1/sessions/:id` / 2c 导入的 `title`) | 标题不合形:1–120 个 Unicode 字符、**首尾不得有空白**(服务端不代为 trim,先于 C0 判)、不得含 C0 控制字符;`title: null` 清回自动标题;空串是 `request.body_shape` | 客户端送出前自行 trim;按 `errorCode` 分支,句子只作展示 |
2267
+ | HTTP `400` + `errorCode:"request.field_invalid"`(`PATCH /v1/sessions/:id` / 2c 导入的 `title`) | 标题不合形:1–120 个 Unicode 字符、**首尾不得有空白**(服务端不代为 trim,先于 C0 判)、不得含 C0 控制字符、**至少有一个可见字符**(只由零宽 / 方向控制符等不可见码点构成的串被拒,句子 `title must contain at least one visible character`;夹在可见字符之间的照收);`title: null` 清回自动标题;空串是 `request.body_shape` | 客户端送出前自行 trim、拒空显示的标题;按 `errorCode` 分支,句子只作展示 |
2261
2268
  | HTTP `400` + `errorCode:"request.field_invalid"`(撞 413 的帽见 `request.payload_too_large`)+ `reason:"over_cap"` / `"too_long"` / `"bad_entry"`(7.132.0 起:工具名清单、子代定义、两张宿主路径清单;7.133.0 起提交体上其余的帽同样带:`jobId` · `systemPrompt` · `appendSystemPrompt` · `settings.outputStyle` · `settings.env` · `clientContext.*` · `sandboxImageProfile` · `outputSchema` · `resumeAt` · 顶层技能 `content` / `baseDir`;子代定义里技能自己的 `content` / `baseDir` 同样带,坐标 `agents[i].skills[j].content`,句子仍是形句) | 撞了帽:`field` = 撞帽那一处的体内坐标(`skills[1].allowedTools` · `agents[0].denyTools[3]` · `agents` · `additionalDirectories` · `systemPrompt` · `settings.env.<变量名>` …),`count` = 送来的条数(`over_cap`)或字符数(`too_long`),`cap` = 帽;`bad_entry` = 清单第 `index` 条坏了(`field` = 那张清单)。7.132.0 起覆盖的面句子头一句恒为 `<field> has <count> entries\|characters — at most <cap> are accepted`;7.133.0 起补上的面句子不变、只加这一组键。同一个字段的形错(不是串、空串)只有句子 | 按 `field` 改那一处(合并 / 拆小条目、缩短文字、修那一条路径);句子只作展示 |
2262
2269
  | HTTP `400` + `errorCode:"request.field_conflict"`(7.133.0 起带 `detail`;`requireExistingSession` 缺 `sessionId` 一处是 422) | 请求里的字段互斥或互为前置(`verify` × `cascade`、`capabilitiesNeeded` 没有 `sandboxImageProfile`、`disableSkills` 同体带技能、决定口的 `remember` / `answer` / `editedPlan` 错门 …)。体带 `detail: { kind, fields }`:`kind` 是闭集词(`cascade_verify` · `image_profile` · `resume_anchor` · `system_prompt_fold` · `file_history_restore` · `skills_disabled` · `existing_session` · `scratchpad_root` · `ask_answer` · `policy_person_ask` · `plan_edit` · `plan_mode_after` · `bound_call_door` · `remember_decision`,全表见契约附录 A),`fields` = 冲突涉及的请求体坐标(句子主语在前;互斥的类只点请求里真带了的键,「X 要求 Y」的类 Y 是缺席的前置、也在里面);`system_prompt_fold` 另带 `count` / `cap`(折叠后的字符数与帽) | 按 `detail.kind` 分支(例:`cascade_verify` ⇒ 去掉 `verify` / `cascade` 重提),按 `detail.fields` 定位到那几个键;句子只作展示、别锚文案 |
2263
2270
  | HTTP `400` + `errorCode:"config.tool_face_control"`(7.126.0 起;流式 / 后台腿的引擎侧拒是 `failed` 终局带同码,受理口拒三条腿都是提交时 400) | 请求的 `allowTools` / `excludeTools` 里有一个**读成空**的条目:任何工具名都不可能有的拼法(空白、括号不配、空名)、`allowTools` 写了 `mcp__*`(引擎拒)、`excludeTools` 写了 `*`,或(7.131.0 起,受理口拒)**近似名**(大小写孪生 `BASH`;放行侧 `allowTools` 与限制侧 `excludeTools` 都拒 —— 想放的什么也没放 / 想禁的照用)与**限制侧的退役名**(`excludeTools: ["Task"]`)。放行侧 `allowTools` 的**内容形**(`Agent(Explore)`)与没有后继的退役名(`MultiEdit`)照 7.130.0 受理、交引擎既有通告(运维行 `runner_config_advisory`);**有后继的退役名**(`Task` · `bash`)7.132.0 起由引擎开跑前拒(`config.tool_face_control`,同步腿 400)。**真未知名**(拼写合法、这台部署没有工具叫它,懒挂载的合法形)7.129.0 起照跑:条目原样留在表里,运维行告知一次。选择器只取工具名那一段判名字(限制侧 `Bash(echo $HOME)` 不拒),`allowTools:["*"]` 与限制侧 `mcp__*` / `a2a__*`(整个协议)合法。清单超 1000 条 ⇒ 整只 `400 request.field_invalid`(`count` / `cap`),帽内拒句只点名前 5 条 + `; and K more`。拒时:主模型零调用、标题器零调用(7.127.0 起;7.126.x 上标题器仍会为这次提交调一次模型)。这次提交留下的会话无标题、零消息 | 改成句子里 `write` 后的现名(或删掉条目);只列这台部署挂得出的工具名 |
@@ -2270,7 +2277,7 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
2270
2277
  | HTTP `429` + `errorCode:"usage.window_exhausted"` | 部署级治理窗耗尽(`USAGE_WINDOWS`,token 或 $ 天花板先满者) | 看响应体 `retryAfterSec` 退避;窗滑动/桶到期后放行 |
2271
2278
  | `terminal:{kind:"failed", code:"usage.window_exhausted"}` | 已受理的 run 在 **turn 边界**撞上治理窗且无法 durable 挂起 | 同上退避后重投;配好 checkpoint 基建则改为 `suspended` 等窗自动续跑 |
2272
2279
  | HTTP `501`(`/v1/runs`) | 内存模式不支持异步 | 配持久存储(`DB_BACKEND=mysql` / `pg` + 坐标;7.106.0 起给了坐标必须点名 `DB_BACKEND`) |
2273
- | HTTP `500` + `errorCode:"corrupt_session"`(7.104.0 起;流式口在应答头之后则是 `done` 帧的 `result.terminal.code`,后台 run 是 run 行的 `errorCode`) | 这只会话的**存档读不出**、唤不醒(文件后端:会话日志没有会话头;SQL 后端:一行事件读不出)。这一轮没跑,一个 token 没烧。直接读这只会话的口(头探针 / 会话视图 / 导出 / 分叉 / 2c 同步)同答(契约 §17) | **别重试**(确定性失败)。**文件后端**:运维看 `fail_open_total{tag="server.session-repo.corrupt-record-skipped"}` 的计数、stderr 契约行 `[sema] fail-open: server.session-repo.corrupt-record-skipped` 与随带的一条 `fail_open` warn(后两者每进程每 tag 只一次,warn 带首条明细);逐条明细(文件路径、会话 id、行号、成因)每命中一行进探针文件 —— 起服前设 `SEMA_FAILOPEN_PROBE=<文件路径>`(会话头那一行本身读不出时,它就是其中一条);修或移开那份存档。**SQL 后端**不经这只座:坏的那一行由错误句子点名(`session_event.payload` + 会话 id + 序号 `seq`)。要继续工作就换一个新会话 |
2280
+ | HTTP `500` + `errorCode:"corrupt_session"`(7.104.0 起;流式口在应答头之后则是 `done` 帧的 `result.terminal.code`,后台 run 是 run 行的 `errorCode`) | 这只会话的**存档读不出**、唤不醒(文件后端:会话日志没有会话头;SQL 后端:一行事件读不出)。这一轮没跑,一个 token 没烧。直接读这只会话的口(头探针 / 事件订阅 / 会话视图 / 改名 / 导出 / 分叉 / 2c 同步)同答(契约 §17;文件后端上事件订阅与改名 7.136.0 起同答,此前一个 200 起流却零帧、一个 404) | **别重试**(确定性失败)。**文件后端**:运维看 `fail_open_total{tag="server.session-repo.corrupt-record-skipped"}` 的计数、stderr 契约行 `[sema] fail-open: server.session-repo.corrupt-record-skipped` 与随带的一条 `fail_open` warn(后两者每进程每 tag 只一次,warn 带首条明细);逐条明细(文件路径、会话 id、行号、成因)每命中一行进探针文件 —— 起服前设 `SEMA_FAILOPEN_PROBE=<文件路径>`(会话头那一行本身读不出时,它就是其中一条);修或移开那份存档。**SQL 后端**不经这只座:坏的那一行由错误句子点名(`session_event.payload` + 会话 id + 序号 `seq`)。要继续工作就换一个新会话 |
2274
2281
  | 流里一帧 `engine_notice` `code:"session.records_skipped"`,`detail:{ dropped, entriesKept, sessionId }`(7.104.0 起,只在文件后端) | 这只会话的存档打开时有 `dropped` 条记录读不出、被跳过了;这一轮**照跑**,但模型读到的历史只剩 `entriesKept` 条。存档没修好之前,每次真正打开这只会话都会再说一次(会话缓存命中的轮次不打开、不说;`SESSION_CACHE_TTL_SEC=0` 时每一轮都说) | 告诉用户「这段对话的一部分历史丢了」;运维按 `fail_open_total{tag="server.session-repo.corrupt-record-skipped"}` 的计数与探针文件(`SEMA_FAILOPEN_PROBE=<文件路径>`,每命中一行)里的逐条明细(文件路径、会话 id、行号、成因)查那份文件。非流式 `POST /v1/tasks` 收不到通告帧(只有运维面的计数与探针) |
2275
2282
  | 流里一帧 `engine_notice` `code:"route.finish_reason_unknown"`,`detail:{ finishReason, sessionId, runId }`(7.126.0 起) | 提供方用一个引擎读不懂的结束原因结束了这一轮:回答按模型留下的样子保留,但可能被提供方截短了 | 告诉用户「这一段回答可能不完整」;同一个词同一 run 只说一次。运维按 `finishReason` 查是哪家网关 / 模型发的 |
2276
2283
  | 流里一帧 `engine_notice` `code:"route.stream_end_ignored"`,`detail:{ statement, word, frames, sessionId, runId }`(7.126.0 起) | 提供方已经结束了这一轮,它的流却又发来了东西(又一个结束 / 一个错误 / 内容);第一个结束算数,之后的没进这一轮 | 一般无需处置;频繁出现 ⇒ 运维查那条网关的流式实现(`statement` / `word` 说的是迟到的第一帧) |
@@ -2386,7 +2393,7 @@ k8s 沙箱(pod `activeDeadlineSeconds` 真硬死线)向引擎声明 `lifetimeMs`
2386
2393
 
2387
2394
  k8s 车道每条 run 一只新 pod(pod 寿命 = 泄漏护栏,不跨 run 复用)。配了快照存储时,`/workspace` **缺省跨轮保留**(`WORKSPACE_PERSIST` 缺省 `space`):每条 run 结束把工作区快照进本会话住客的空间,下一条 run 开始恢复最近一份。要回到每轮空目录:`WORKSPACE_PERSIST=off`。
2388
2395
 
2389
- - **空间 = 会话的这一位住客**(会话化身:会话建行时铸的不透明随机串,删会话随行消失)。同一会话的各条 run 共享一个空间;不同会话互不可见;会话删掉后同一个 id 被重新登记(不论谁)= 新住客,从空目录开始,前一位住客的对象不再被读。前缀 `WORKSPACE_SNAPSHOT_PREFIX` 缺省 `kata`,桶 `WORKSPACE_SNAPSHOT_BUCKET` 缺省 `workspaces`。
2396
+ - **空间 = 会话的这一位住客**(会话化身:会话建行时铸的不透明随机串,删会话随行消失)。同一会话的各条 run 共享一个空间;不同会话互不可见;会话删掉后同一个 id 被重新登记(不论谁)= 新住客,从空目录开始,前一位住客的对象不再被读。前缀 `WORKSPACE_SNAPSHOT_PREFIX` 缺省 `kata`,可以是**多段路径**(`libA/ws`:一只桶按属主分区;7.136.0 起,单段旧值照常合法),桶 `WORKSPACE_SNAPSHOT_BUCKET` 缺省 `workspaces`(**server 不建桶**:起服时 server 核这只桶在不在,缺 ⇒ 拒启,见下方失败形)。
2390
2397
  - **两条车道,两种分区**:
2391
2398
  - **跨轮保留**(本节):归档 `<前缀>/<化身>/space/<snapshotId>.tar.gz`,指针 `<前缀>/<化身>/space/latest`;只有这条车道被列举、被删旧,且只在 `space/` 里。按住客分区的理由:读它的是「这只会话的下一条 run」,分区就决定了谁读得到 —— 按会话 id 分,删后重登记的另一位就会恢复前一位的文件。
2392
2399
  - **停驻挂起 / 续跑**(引擎驱动的 durable suspend):归档 `<前缀>/<会话 id>/<snapshotId>.tar.gz`(key 形与 7.125.0 之前同形,存量停驻快照照常可读,零迁移;7.126.0 起新写的 `snapshotId` 形为 `kata-<归属记号>-<时间>-<随机>`,归属记号 = 住客化身的不可逆摘要头 16 位,没有已知住客的 run 记 `none`);从不被列举、从不被删旧 —— 一条停驻中的 run 的快照不会被同会话后来的 run 挤掉。按会话 id 分区的理由:读它的只有那条停驻 run 自己的检查点(snapshotId 记在检查点里,删会话连检查点一起删),分区只做命名空间、不决定谁读。
@@ -2406,11 +2413,14 @@ k8s 车道每条 run 一只新 pod(pod 寿命 = 泄漏护栏,不跨 run 复用)
2406
2413
  | `WORKSPACE_SNAPSHOT_KEEP` | `3` | 每个空间保留的快照份数,整数 `[1, 50]`;域外 / 小数 ⇒ 拒启 |
2407
2414
  | `WORKSPACE_SNAPSHOT_TIMEOUT_MS` | `300000` | 快照时限(毫秒,整数 `[30000, 3600000]`;域外 / 小数 ⇒ 拒启):**一个数管四处** —— run 末打包上传的墙钟上界、连沙箱时恢复的墙钟上界、空间结束时回收座**一次调用**的绝对截止(一次调用一份:会话删除 = 一个空间,托管留存 sweep = 每个域每拍一次、该域这一拍结束的全部空间;**卷回收也在内**)、本副本写者给自己每次围栏期限用的数(终态 / 开始 / 发布三次续期各一界;下一条 run 等的是**上一条 run 的写者**所配的数,不按它自己副本的数 —— 各副本同配时就是同一个数);大文件读写的墙钟上界也是它(卡住不出字节的传输由命令无进展上界先切)。构建目录 GB 级的工作区撞 5 分钟 ⇒ 调大它,或配排除表 |
2408
2415
  | `WORKSPACE_SNAPSHOT_EXCLUDE` | 未设 | 快照排除表(见上「排除表」);未设 = 打包命令与旧版逐字同 |
2416
+ | `WORKSPACE_SNAPSHOT_BUCKET` | `workspaces` | 快照桶。起服核它在不在(与会话快照桶 `SESSION_SNAPSHOT_BUCKET` 同一只探针、同一条规则);桶必须预先存在 |
2417
+ | `WORKSPACE_SNAPSHOT_PREFIX` | `kata` | 快照 key 前缀:`/` 分隔的**一段或多段**普通 key 段(`kata`、`libA/ws`);每段以字母或数字开头,其后字母、数字、`.` `_` `-`;空段(首尾或连续的 `/`)/ `.` / `..` / 其它字符 ⇒ 拒启(码 `config.workspace_snapshot_prefix_invalid`,点名第几段),k8s 车道上不论配没配快照存储都核。改前缀 = 新 key 空间,旧前缀下的存量快照不再被读 |
2409
2418
 
2410
2419
  **失败形(都是响亮的)**:
2411
2420
 
2412
2421
  - **没配快照存储**(`MINIO_ENDPOINT` / `MINIO_ACCESS_KEY` / `MINIO_SECRET_KEY` 缺任一)⇒ 工作区每轮为空,`GET /v1/capabilities` 不带 `workspaceSync`;起服一行 `workspace_persist_unavailable`(`missing` 点名缺的键;缺省 `space` 也出,设 `WORKSPACE_PERSIST=off` 即不出)。
2413
2422
  - **没有存储后端**(`DB_BACKEND=memory`;配了 SQL 的 `DB_BACKEND` 却显式 `SESSION_BACKEND=memory` 时库不开,同一形)× `space`(含缺省,且配了快照存储)⇒ **拒启**,拒句一行点名 `WORKSPACE_PERSIST=space`、缺的存储后端、会话店种类(与 `/health.sessionBackend` 同一读法)与可用的后端。出路:`DB_BACKEND=local`,或 SQL(`DB_BACKEND=mysql|pg`,`SESSION_BACKEND` 不设 `memory`),或设 `WORKSPACE_PERSIST=off`。`SESSION_BACKEND=memory` 配 `DB_BACKEND=local` 不在此列:被收编成 local 店,照起。拒启而不是降级:否则能力位报 `space`、每条 run 却一份都不存。
2423
+ - **快照桶不存在**(`WORKSPACE_SNAPSHOT_BUCKET` 指的桶没建):**起服拒启**(k8s 车道配了快照存储时,server 在打开任何店之前核这只桶;拒句点名桶名、`MINIO_ENDPOINT` 的显示安全形与出路 —— 建桶,或把 `WORKSPACE_SNAPSHOT_BUCKET` 指向已有的桶;建桶晚于 server 起是编排里的常态,所以缺桶在 `STORE_LOCK_WAIT_MS` 窗内重试、到窗才拒);连不上 / 403 / 5xx 这些证明不了桶缺的答复不拒启,走 fail-open 兜底口 `server.snapshot-bucket.presence-unproven`。起服之后桶才没的(被删),或起服核证明不了的部署:读 `latest` 得 `404 NoSuchBucket` ⇒ 这条 run **连沙箱失败**(`connect_failed`,句子带 `snapshot_bucket_missing`、桶名与出路),一只 pod 都不建,另有一行 `workspace_snapshot_bucket_missing`(`space` / `bucket` / `code` / `effect`),每只执行环境至多一次;**不再**读成「空间第一条 run」而给一个空目录、每轮只留下一条不点名桶的存失败。无 `NoSuchBucket` 的 404(桶在、键不在;代理的 404)仍是「空间第一条 run」。
2414
2424
  - **读 `latest` 失败**(非 404 的 HTTP 错、网络错)、**读或弃提交围栏那一行失败**或**读会话住客失败**(会话店报错、答出的不是住客令牌,或没在 `rpcTimeoutMs`〔30 秒,与读围栏行、读 `latest` 同一只墙〕内答 —— 挂住的会话店不再让 run 的起步永挂)⇒ 这条 run 连沙箱失败(`connect_failed`,工具调用报沙箱不可用;停驻 run 续跑时的恢复同样失败),**不会**悄悄给一个空目录、也不猜「没有提交在飞」—— 「以为恢复了其实没有」比失败更坏。读住客失败时一只 pod 都不建,另有一行 `workspace_space_unreadable_occupant`(`sessionId` / `code`:会话店错误的码,不带正文;读超时的码是 `incarnation_read_timeout`),每条 run 至多一次。修好存储 / 数据库后重跑,或设 `WORKSPACE_PERSIST=off` 即恢复。
2415
2425
  - **`latest` 指向的快照对象不在了**(被删 / 被只盖 `.tar.gz` 的生命周期规则清掉)⇒ 沙箱**照常起**:一行 `workspace_latest_dangling`(`space` / `key` / `deleted`),删掉悬空的 `latest`,这条 run 从空目录开始,它的 run 末快照成为新的 `latest`(数据已被运维规则清走:响亮、可继续)。指针删不掉 ⇒ 同一行 `deleted: false` + `error`,下一条 run 再说一次。
2416
2426
  - **`latest` 指向的快照在、却拿不回来**(桶答了却不是 200 / 解压失败 / 超过快照时限 / 传输中断)⇒ 这条 run **仍然连沙箱失败**(`connect_failed`)、**不**退到空目录 —— 用户的改动还在桶里,静默空目录 = 让人以为丢了。**一条 run 只恢复一次**(同一只执行环境上并发的连沙箱 —— 显式 `connect()` 与首个工具调用的懒连接 —— 共用同一次建连:一只 pod、一次恢复、一个终局,被拒也是同一个拒;恢复还没完成时才到的工具调用和 `reconnect` 也等这一次建连 —— pod 先于它的工作区存在,不在半恢复的 `/workspace` 上先跑):瞬时类(`timeout` 超过快照时限 / `transport` 传输中断:执行通道断了,或 pod 取包的 GET 没拿到完整应答 —— 连不上、被重置、传到一半被截)在恢复这一步里换一只新 pod 再试一次(约 1 秒后),再试成了 ⇒ 照常起、不出行;`store_busy`(桶答 `408` / `429` / `500` / `502` / `503` / `504` —— 「现在不行」,同一请求再发可能换答案)同属瞬时类;非瞬时类(`download` 桶答了、答的是其余非 200 —— 如 `401` / `403` / `501`;`untar` 包完整下来了却解不开)不重试。每次没成的尝试,新起的 pod 当场删掉(残缺目录既不给工具用,也不会在 run 末被快照回去覆盖 `latest`)。恢复终局是拒 ⇒ **恰一行** `workspace_restore_failed`(`space` / `snapshotId` / `class` = `store_busy` \| `download` \| `untar` \| `timeout` \| `transport` / `attempts` = 这次恢复的尝试次数(`1` 或 `2`)/ `boundMs` = 每次尝试的时限 / `error`,预签 URL 已脱敏),这条 run 之后的每次沙箱操作都答**同一句**拒(不再建 pod、不再读 `latest`、不再出行);建 pod 本身失败不是恢复的结局(不出这行,下一次操作重来)。拒句写明类别与尝试次数,点名两条出路:**调大 `WORKSPACE_SNAPSHOT_TIMEOUT_MS`**(大工作区撞上界),或 **`WORKSPACE_PERSIST=off` 跑一轮**(空目录起,不删任何快照;改回 `space` 后下一条 run 照常恢复那一份)。没有「删指针」的接口:丢弃这份快照 = 丢弃用户数据,要做就由运维在对象存储侧有意识地删 `<前缀>/<化身>/space/latest`(下一条 run 从空目录起)。与上一条「悬空」分开:那是对象**不在**(没东西可恢复 ⇒ 空目录 + 响亮),这是对象**在**而拿不回来(⇒ 拒)。
@@ -2805,7 +2815,7 @@ WebSearch 工具的 `tool_result` 错误文本,回到模型的对话里,**不是
2805
2815
 
2806
2816
  条目的受理键 = `name` · `transport` · `allowTools` · `toolAxes` · `toolFaces` · `contentOrigin` · `elicitation` · `source` · `startupTimeoutMs`(7.132.0 起)(`transport` 的子键:`kind` · `command` · `args` · `env`(stdio),`kind` · `url` · `headers` · `principalHeader` · `assertionHeader`(http))。**新提交上每个键只有一只判官,判官在受理口,写坏的答案只有一种**(下面第一条:400)。`startupTimeoutMs`(这一只 server 自己的启动界,毫秒整数,范围与引擎 env 旋钮 `MCP_TIMEOUT` 同一段)7.134.0 起同样在受理口判 —— 判官是引擎包根导出的 `mcpStartupTimeoutMs`(启动界的唯一读者):数字串 / 小数 / 越界 / `null` ⇒ `400 request.field_invalid`,原因句是引擎的那一句(7.132.0–7.133.x:受理口不判、请求照常 200,引擎拨号时只拒这一只 server);耐久 resume 重放存量请求不判(原样交引擎,坏值在拨号时只拒这一只 server —— `config.mcp_startup_timeout_invalid`,兄弟照挂)。
2807
2817
  - **新提交**(`POST /v1/tasks` · `/v1/tasks/stream` · `/v1/runs`):键在场却判不出形 ⇒ `400 request.field_invalid`,拒句形如 `mcpServers[2].allowTools (server "crm") is not a usable declaration — must be an array of strings (omit the key to leave the server's tools unrestricted) — got a string of 4 characters. …`:点名 `mcpServers[i].<键路径>`、server 名与该键判官的原因句,**整份请求不收** —— 不挂一半、不丢声明、不默认。原因句不回显声明的值,有几处例外,回显只有一只口径(剥控制与双向字符、截到 64 字、超出加 `…`):server 名(含写坏的 `name` 值,另报送来的原长度)、`contentOrigin` 词表外的值;`toolAxes` / `toolFaces` 的原因句是引擎写的(7.134.0 起 `toolFaces` 的判官也是引擎包根导出的 `readToolFaces`):点名工具名(引擎截 80 字)与成员,`toolFaces` 还会点名闭词表拒的那个词,整句详情由引擎截到 240 字、本服务再剥控制与双向字符 —— 不回显显示文本、不回显合规的列表;`transport.url` 的值从不回显(带凭据的 url 被拒时,原因句只点名命中的那一段与凭据名表里的那个词)。可选键**缺席**永远合法(真「没声明」),必填的 `name` / `transport` 缺席同样 400;在场的 `null` 不是缺席。与单用户 / 多租户无关。列表本身同一条律(7.132.0 起,先于条目判):`mcpServers` 在场却不是数组、超过 32 条(整份拒,不挂前 32 条)、某一条不是对象 ⇒ `400 request.field_invalid`,拒句点名 `mcpServers` 或 `mcpServers[i]` 与形的种类;条目层或 `transport` 层出现词表外的键 ⇒ `400 request.body_shape`,拒体带 `unknownKeys`(全路径、至多 20 条)。`mcpServers` 缺席或为 `null` = 没声明,照常受理。
2808
- - **`transport.url` 不得带凭据(7.135.0 起)**:http 传输的 `url` 带 userinfo(`https://user:pass@host/mcp`,只有用户名也算),或带名字在凭据名表里的 query / fragment 参数(`?api_key=` · `?key=` · `?token=` · `#access_token=` …;表 = 本服务日志脱敏器认作凭据名的那一张,全表见契约附录 A `request.field_invalid` 行;名字大小写不敏感,按解一次百分号编码读)⇒ 新提交 `400 request.field_invalid`,体另带 `field: "mcpServers[i].transport.url"` 与 `reason`(`url_userinfo` | `url_credential_param`),原因句点名出路:**凭据改写进 `transport.headers`**(如 `"Authorization": "Bearer …"`)。为什么:任务停驻时提交体整份进续跑上下文,`transport.headers` 的值在那里只以封签存在,`url` 照原样存(日志与读面同理)。值从不回显;解析不了的 url 不在这一问里判(拨号时失败)。7.134.x 及以前照收、原样落停驻上下文;带 userinfo 的 url 本就连不上(fetch 对带凭据的 URL 构造即抛),带名表参数的升级后要改。同一条规则在 A2A 对端 / http 钩子 / 搜索后端上的三座见 9.5g。
2818
+ - **`transport.url` 不得带凭据(7.135.0 起)**:http 传输的 `url` 带 userinfo(`https://user:pass@host/mcp`,只有用户名也算),或带名字在凭据名表里的 query / fragment 参数(`?api_key=` · `?key=` · `?token=` · `#access_token=` …;表 = 本服务日志脱敏器认作凭据名的那一张,全表见契约附录 A `request.field_invalid` 行;名字大小写不敏感,按解一次百分号编码读)⇒ 新提交 `400 request.field_invalid`,体另带 `field: "mcpServers[i].transport.url"` 与 `reason`(`url_userinfo` | `url_credential_param`),原因句点名出路:**凭据改写进 `transport.headers`**(如 `"Authorization": "Bearer …"`)。为什么:任务停驻时提交体整份进续跑上下文,`transport.headers` 的值在那里只以封签存在,`url` 照原样存(日志与读面同理)。值从不回显;解析不了的 url 不在这一问里判(拨号时失败)。7.134.x 及以前照收、原样落停驻上下文;带 userinfo 的 url 本就连不上(fetch 对带凭据的 URL 构造即抛),带名表参数的升级后要改。同一条规则在 A2A 对端 / http 钩子 / 搜索后端 / 图片 URL 上的几座见 9.5g。
2809
2819
  - **耐久 resume 重放存量请求**:不拒(拒会把升级前 park 的任务永久卡死;`transport.url` 带凭据的存量条目照挂、url 逐字交引擎)。写坏的 `name` · `transport` 的 `kind`/`command`/`url` · `allowTools` · `toolAxes` · `toolFaces` ⇒ 这一条 server 整条不进,运维 warn `mcp_injection_dropped`(`reason: malformed_entry`,`field` 点名)并投用户面通告 `mcp.injection_dropped`(其中 `toolAxes` 在一次性迁移 M19 的窗开着时先按升级前读法重读一次,过了就挂,见上文 `toolAxes` 一节);写坏的 `contentOrigin` · `elicitation` · `source` · `transport` 的 `args`/`principalHeader` 与词表外的键 ⇒ 丢掉那一个键、server 照挂;`transport` 的 `env`/`headers`(名 → 串记录)对 JSON 存体按 7.131.x 的读法交:有值不是串的成员 ⇒ 整键不交,只有 `__proto__` 名的成员坏 ⇒ 剔掉它、其余照交;server 照挂,一行运维 warn `mcp_transport_members_dropped` 点名(见 9.5c-bis 同段);条目不是对象 ⇒ 这一条不进(`malformed_entry`);超过 32 条 ⇒ 第 33 条起只记 `over_cap`;整只不是数组 ⇒ 整只忽略 + 运维 warn `mcp_injection_ignored`。
2810
2820
  - `POST /v1/capabilities/mcp/probe` 问的是新提交那**同一只判官**:上面每一形同码同句(含 `unknownKeys`,与 url 带凭据那一形的 `field` / `reason`),拒之前零拨号;判官受理的申报,探测口拨的就是一条任务提交会交给引擎的那一份(判官的合法归一照收)。它多出的两形:本口的体就是这份列表,`mcpServers` 缺席或为 `null` ⇒ `400 request.body_shape`(缺必需字段);声明了 `transport.assertionHeader` 的条目 ⇒ `400 request.field_invalid`(见 9.5d:本版承载、不发送,新提交照挂,探测不拨一份少了断言头的配置 —— 同一只谓词,后果按门分)。7.131.x 上本口另有一只判法(申报原文与读器产物逐字比),列表不是数组 / 条目不是对象答 `request.body_shape`。
2811
2821
  - **`toolFaces` 的判官是引擎的 `readToolFaces`(7.134.0 起;此前本服务自带一只只判 JSON 形的读器)**:它在门口就判声明本身能说的一切 —— 成员与类型、五张闭词表、上界、跨成员律 ——
@@ -2814,6 +2824,8 @@ WebSearch 工具的 `tool_result` 错误文本,回到模型的对话里,**不是
2814
2824
  `patternParam` 只在 `absent: "base"` 旁有意义,单独写是一份读不到的声明(inert)、同样整条拒;未知成员(笔误)、安全轴写成面成员(`effect` 属于 `toolAxes`)、非布尔旗、错型成员。
2815
2825
  部署腿(中心 / `config.d`)的坏声明若绕过本服务直接到引擎,引擎在准备门**整任务**拒(`config.tool_face_invalid`)—— 一处笔误整任务起不来,写之前对一下这三条。
2816
2826
  要看物化 schema 才判得了的那一半(`param` 是不是工具的顶层参数、审批卡要的键在不在)与 `resultCards` 落在别的命名空间,仍由引擎在列举处**按工具**判(那一只工具不挂,通告 `config.tool_face_invalid`,兄弟照挂)。
2827
+ - **列表上限的拒句(7.136.0 起换成全仓同一形)**:`mcpServers` 超过 32 条 ⇒ `400 request.field_invalid`,句子头一句是 `mcpServers has 33 entries — at most 32 are accepted`,体另带撞帽机读位一组键(`field:"mcpServers"` · `reason:"over_cap"` · `count` · `cap`),
2828
+ 与 `a2aPeers` 超界、`images` 超界同一形;7.135.0 及以前的头一句是 `mcpServers carries 33 declarations, above this deployment's ceiling of 32`、体没有机读位。码与状态(400 `request.field_invalid`)不变,探测口同一句同一组机读位。**按 `errorCode` 与 `field` / `reason` 分支,不要匹配句子。**
2817
2829
  - 7.130.x 及更早:`allowTools` / `toolFaces` / `name` / `transport` 写坏 ⇒ 200 + 这条 server 不挂;`elicitation` / `source` / `transport` 的可选子键写坏 ⇒ 200 + 悄悄丢键。7.131.x 及更早:列表整只不是数组 ⇒ 200、整只忽略;条目不是对象 ⇒ 200 + 点名丢弃;超过 32 条 ⇒ 200、挂前 32 条;词表外的键 ⇒ 悄悄不读。升级后这些请求变 400:改对那个键(或删掉它)、把列表改成数组、删掉第 33 条起的条目。
2818
2830
 
2819
2831
  ### 9.5f 中心提示层:一种 wire 形、按场景挂段、品牌行与「无持久记忆」披露(7.132.0 起)
@@ -2834,10 +2846,10 @@ WebSearch 工具的 `tool_result` 错误文本,回到模型的对话里,**不是
2834
2846
  - **「本会话没有持久记忆」**:引擎判、引擎渲染(记忆面没挂 ∧ 名册不能持久 ∧ 不是委派子代)。本部署有引擎推断看不见的持久通道(MCP 记忆服务器、自定义写工具)⇒ `MEMORY_PERSISTENCE_CAPABLE=true`
2835
2847
  撤回这段披露;host 车道自动声明。
2836
2848
 
2837
- ### 9.5g 请求体里的 URL 不得带凭据:MCP 传输 · A2A 对端 · http 钩子 · 搜索后端(7.135.0 起,新提交 400)
2849
+ ### 9.5g 请求体里的 URL 不得带凭据:MCP 传输 · A2A 对端 · http 钩子 · 搜索后端(7.135.0 起)· 图片 URL(7.136.0 起)(新提交 400)
2838
2850
 
2839
2851
  任务停驻时提交体整份进续跑上下文,请求自带的凭据位(`mcpServers[i].transport.headers` · `a2aPeers[i].headers` · `settings.hooks.<事件>[g].hooks[h].headers` · `settings.webSearch.apiKey`)在那里只以封签存在;
2840
- 而「值是 URL」的字段不在封签位里 —— URL 里的凭据会原样落库 / 落盘(日志与读面同理)。所以同一条规则落在四个座位上,**一只判官、一个拒形**:
2852
+ 而「值是 URL」的字段不在封签位里 —— URL 里的凭据会原样落库 / 落盘(日志与读面同理)。所以同一条规则落在五类座位上,**一只判官、一个拒形**:
2841
2853
 
2842
2854
  | 座位(体内坐标) | 凭据该写在哪(已封签的位) |
2843
2855
  |---|---|
@@ -2845,13 +2857,29 @@ WebSearch 工具的 `tool_result` 错误文本,回到模型的对话里,**不是
2845
2857
  | `a2aPeers[i].url` · `a2aPeers[i].cardUrl` | 同对端的 `headers` |
2846
2858
  | `settings.hooks.<事件>[g].hooks[h].url`(`type:"http"` 条目) | 同条目的 `headers`(`$NAME` + `allowedEnvVars`,配置本身不带密钥值,见 9.5c) |
2847
2859
  | `settings.webSearch.endpoint` | 同段的 `apiKey`(brave / tavily 发成请求头;searxng 腿不发鉴权头,要凭据的实例在部署面声明 `WEB_SEARCH_ENDPOINT`) |
2860
+ | `images[i].url`(`{ url }` 臂;7.136.0 起) | 没有封签的头位 —— 图片字节改走内联 `{ data, mimeType }`(7.136.0 起) |
2848
2861
 
2849
2862
  - **判据**:URL 带 userinfo(`https://user:pass@host/…`,只有用户名也算),或带名字在凭据名表里的 query / fragment 参数(`?api_key=` · `?key=` · `?token=` · `#access_token=` …;表 = 本服务日志脱敏器认作凭据名的那一张,全表见契约附录 A `request.field_invalid` 行;名字大小写不敏感,解一次百分号编码读,整名相等)。
2850
2863
  - **答案**:新提交(`POST /v1/tasks` · `/v1/tasks/stream` · `/v1/runs`)`400 request.field_invalid`,体另带 `field`(上表的体内全坐标)与 `reason`(`url_userinfo` | `url_credential_param`);句子点名坐标、命中的那一段与表里的词,**不回显 URL 的值**,并指出路。MCP 座另有探测口(`POST /v1/capabilities/mcp/probe`,同码同句同机读位)。
2851
- - **A2A 座**逐条扫整只数组(含读器会丢弃的条目 —— 字节一样落盘);`a2aPeers` 超过 32 条(读器的挂载上限)整份 `400 request.field_invalid`(`field:"a2aPeers"` · `reason:"over_cap"` · `count` · `cap`):界外的尾项读器本就不挂,但提交体整份落盘 —— 判官要么看过每一条对端的 `url` / `cardUrl`,要么这份体不收。判的范围是条目上**串值**的 `url` / `cardUrl`:非串值的键、条目上多余的键、`a2aPeers` 不是数组仍是读器既有的读法(这条对端丢弃 / 整只忽略 + 运维 warn),它们的字节照落盘 —— A2A 请求面还没有 MCP 那样的条目判官表。7.134.x 及以前超过 32 条是 200、第 33 条起丢弃(运维 warn 点名条数)。**搜索后端座**:userinfo 形此前已是 400(句子逐字不变,本版起体另带 `field` / `reason`),凭据名参数是本版新拒的。
2852
- - **不判**:续跑重放存量请求(拒会把升级前停驻的任务永久卡死;已落盘的字节不因拒而消失 —— 介意的部署把这些任务结掉后轮换那几枚凭据);解析不了的 url(拨号期失败);部署面(中心 / `config.d` / 部署 env)声明的地址 —— 那是运维的配置,不进任何请求的停驻上下文。
2864
+ - **A2A 座**并进 `a2aPeers` 的条目判官表(7.136.0 起,与 `mcpServers` 同一副拒绝骨架,见下):`url` / `cardUrl` 先判是 http(s) 串、再判凭据;`a2aPeers` 超过 32 条(挂载上限)整份 `400 request.field_invalid`(`field:"a2aPeers"` · `reason:"over_cap"` · `count` · `cap`),
2865
+ 判官要么看过每一条对端的 `url` / `cardUrl`,要么这份体不收。7.134.x 及以前超过 32 条是 200、第 33 条起丢弃(运维 warn 点名条数)。**搜索后端座**:userinfo 形此前已是 400(句子逐字不变,本版起体另带 `field` / `reason`),凭据名参数是本版新拒的。
2866
+ - **图片座**(7.136.0 起):`images[i]` 的 `{ url }` 臂过同一只判官;内联臂 `{ data, mimeType }` 不落 URL、不归本座。个数帽(20,`413 request.payload_too_large`)与内联字节帽排在它前面。
2867
+ **谁受伤**:用带签名的临时地址传图的调用方 —— `?sig=` · `?signature=` · `?token=` · `?key=` · `#access_token=` 这类名字在表里的参数(Azure SAS 的 `sig`、S3 预签名的 `X-Amz-Signature` / `X-Amz-Credential`、OSS / COS 的 `Signature`、带 `?key=` 的图床),7.135.0 及以前照收、取图时把令牌发给图床,本版起 400。
2868
+ **补偿**:图片字节读下来改走内联 `{ data, mimeType }`(内联图有自己的字节帽,不落 URL);或换一个不靠这些参数名鉴权的地址(路径上的签名、公开地址)。
2869
+ - **不判**:续跑重放存量请求(拒会把升级前停驻的任务永久卡死;已落盘的字节不因拒而消失 —— 介意的部署把这些任务结掉后轮换那几枚凭据);解析不了的 url(拨号 / 取图期失败);部署面(中心 / `config.d` / 部署 env)声明的地址 —— 那是运维的配置,不进任何请求的停驻上下文。
2853
2870
  - 7.134.x 及以前照收、原样落停驻上下文。升级后用凭据名参数鉴权的对端 / 托管 MCP / 搜索后端要改:凭据挪进上表的位;只认 query 鉴权、不收请求头的服务端,改在部署面声明。
2854
2871
 
2872
+ ### 9.5h 请求体 `a2aPeers` 的列表与条目键:写坏,整份新提交 400(7.136.0 起)
2873
+
2874
+ 与 9.5e(`mcpServers`)**同一副拒绝骨架、同一个拒形**。条目的受理键 = `name` · `url` · `cardUrl` · `headers` · `principalHeader` · `allowSkills` · `toolAxes`(与引擎的 A2A 对端声明逐键对应)。新提交(`POST /v1/tasks` · `/v1/tasks/stream` · `/v1/runs`)上:
2875
+ - `a2aPeers` 在场却不是数组 ⇒ `400 request.field_invalid`(`a2aPeers is a string of 4 characters, not an array of A2A peer declarations. …`);超过 32 条 ⇒ `400 request.field_invalid` + `field:"a2aPeers"` · `reason:"over_cap"` · `count` · `cap`;某一条不是对象 ⇒ `400 request.field_invalid`,点名 `a2aPeers[i]`。缺席 / `null` 是「没声明」,空数组受理。
2876
+ - 条目上词表外的键 ⇒ `400 request.body_shape`,体带 `unknownKeys`(全路径形,如 `a2aPeers[0].allowSkill`)。
2877
+ - 键在场却判不出形 ⇒ `400 request.field_invalid`,拒句形如 `a2aPeers[1].url (peer "billing") is not a usable declaration — must be a string starting with "http://" or "https://" (never file://, never a bare host) — got a string of 15 characters. …`:点名 `a2aPeers[i].<键路径>`、peer 名与该键判官的原因句,**整份请求不收**,原因句不回显值。判官就是读器用的那一只:`name`(非空串、线长界、无首尾空白)· `url`(必填,http(s) 串)· `cardUrl`(可缺席,在场必须是 http(s) 串)· `headers`(「名 → 串」的纯数据记录,无 `__proto__` 名成员)· `principalHeader`(串)· `allowSkills`(串数组)· `toolAxes`(引擎的判官,A2A 一面,请求侧不收 `boundaryFlow`)。`url` / `cardUrl` 带凭据另见 9.5g(体另带 `field` / `reason`)。
2878
+ - 判序:先列表、后条目;条目里先词表外的键、后键值(`name` → `url` → `cardUrl` → `headers` → `principalHeader` → `allowSkills` → `toolAxes`),第一处坏形即答。纯值域、与租户车道无关(多租户部署整只 `a2aPeers` 本会被忽略,告诉调用方「这样写不行」只多不少)。
2879
+ - **耐久 resume 重放存量请求**不拒(拒会把升级前 park 的任务永久卡死):写坏的条目不挂、运维 warn `a2a_injection_dropped` 点名 —— `dropped` 的每个条目是 `{ peer, reason, field }`:`reason` 是上面判官对那个键的原因句(与受理口 400 拒句里的同一句,不回显值;只有一臂例外:一次性迁移的重读入参克隆不了 —— 访问器成员 / BigInt / Proxy / 环 / 非纯数据 —— 时带前缀 `declaration not cloneable (<because>) — the one-time migration re-reads the stored declaration as plain JSON data and this one is not; `,**其后**才是判官的那一句),`field` 是 `a2aPeers[i].<键路径>`(与 MCP 面 `mcp_injection_dropped` 的条目同键名,名字键 `server` ↔ `peer` 除外);整只不是数组 ⇒ 整只忽略 + `a2a_injection_ignored`。
2880
+ - 7.135.0 及以前:这些形全部 200 —— 坏形的对端悄悄不挂(只在运维 warn 里点名)、词表外的键悄悄不读(`allowSkills` 拼成 `allowSkill` 的对端照样挂上远端卡声明的全部技能)。升级后这些请求变 400:改对那个键(或删掉它)、把列表改成数组、删掉第 33 条起的条目。
2881
+ - A2A 没有运维锁、没有合规能力位:引擎的锁键与合规能力两个闭集里都没有 A2A 的词,所以 `LOCKED_CONFIG_KEYS` 写不进 `a2a`、合规档位也禁不了 A2A(7.135.0 及以前的代码里有一条永远不触发的「锁了就整拒」分支,本版删掉)。`GET /v1/capabilities` 的 `a2aInjection` 只回答「单用户」这一问。
2882
+
2855
2883
  ### 9.6 trace 脱敏的射程:与出站请求体无关
2856
2884
 
2857
2885
  🔴 **脱敏 ≠ 出站控制**:trace 脱敏(账本 / SSE / turns / 审批预览里的 `«redacted»` 标记)只作用于
@@ -3018,9 +3046,9 @@ web-admin 拉取 / fleet 公告 / 凭证续签、WebSearch、对象存储、OTLP
3018
3046
  `tag`(本地 tag 在、tag 树的版本对)→ `precheck`(registry 上本版在不在;读不出 ⇒ 红,不盲发)→ `token` → `clone`(干净 tag 树 =
3019
3047
  `git clone --depth 1 --branch v<v> file://<仓>`)→ `npm-ci`(**k3s8**:tag 树上船、容器里 `npm ci`)→ `prepublish`(**k3s8**:`npm run prepublishOnly`)→
3020
3048
  `pack`(**k3s8** 上 `npm pack --ignore-scripts`,远端打 sha1 → 取回、sha1 核同 → 解进本机 tag 树 → 本机 `npm pack --ignore-scripts` 复包 → 两只解压后的
3021
- tar 流逐字节同才往下走;读数 `remote-shasum=… tar=identical`)→ `publish`(**在 tag 树目录里** `npm publish --ignore-scripts`)→ `readback-wait`(轮询 registry 至多 20 min)→ `readback-shasum`(dist.shasum = 本地 tarball 的 sha1)→
3049
+ tar 流逐字节同才往下走;读数 `remote-shasum=… tar=identical`)→ `publish`(**在 tag 树目录里** `timeout -k 10 "${PUBLISH_TIMEOUT_S:-300}" npm publish --ignore-scripts`)→ `readback-wait`(轮询 registry 至多 20 min)→ `readback-shasum`(dist.shasum = 本地 tarball 的 sha1)→
3022
3050
  `readback-bytes`(curl 回读 tarball,与本地 `cmp` 逐字节同;**传输失败**在 `readback-wait` 用的同一只窗里重试、窗满才红,**字节差**不重试、立即红,两种红句分开说)→ `githead`(registry 的 gitHead = tag commit)。脚本钉 `umask 022`(文件权限进 tar 头,
3023
- 换 umask 重跑会 pack 出另一份字节);每次 registry 请求有硬上限(60 s),回读等待另有 20 min 墙钟截止,registry 挂住不会挂死链。成功尾行
3051
+ 换 umask 重跑会 pack 出另一份字节);每次 registry 请求有硬上限(60 s),回读等待另有 20 min 墙钟截止,`npm publish` 进程自己有进程级墙钟(`PUBLISH_TIMEOUT_S`,缺省 300 秒,到点杀、宽限 10 s 后强杀 ⇒ 退出码 5 + `PUBLISH-FAIL … step=publish`,红句说「超时」;包可能已上,重跑即可,幂等边判),registry 挂住不会挂死链。成功尾行
3024
3052
  `PUBLISH v=<v> shasum=<sha1> gitHead=<sha> bytes=<n> readback=identical`(发车帖 / CHANGELOG 回填 shasum 与 gitHead 从这里取);任一步红 ⇒
3025
3053
  `PUBLISH-FAIL v=<v> step=<步骤>` + 非零退出 + 一句点名步骤的红因(红因与步骤行同走 stdout,链日志里步骤行之后紧跟)。
3026
3054
  - **幂等**:registry 上已有本版 ⇒ 不重发,只做回读比对 —— 与本地 pack 逐字节同且 gitHead 对 ⇒ 绿并说「已在」;不同 ⇒ 红(别人发的字节,版本不可覆盖)。
@@ -3040,8 +3068,8 @@ web-admin 拉取 / fleet 公告 / 凭证续签、WebSearch、对象存储、OTLP
3040
3068
  链日志尾行 `PUBLISH-DRYRUN v=<v> stop=pack`(不写 `PUBLISH v=…`)。
3041
3069
  - **凭据**:**需要环境变量 `NPM_TOKEN`**(只在要发的时候查;发车机 `~/.npmrc` 那一行本身就是 `//registry.npmjs.org/:_authToken=${NPM_TOKEN}`
3042
3070
  引用)。脚本不读 `~/.npmrc`:每条 npm 调用带临时 `--userconfig`,临时 npmrc 只写 `${NPM_TOKEN}` 引用、不写值;开头封口,`remote-gate.sh`
3043
- 与它起的 ssh / 容器(`npm ci` 的安装脚本 / 构建 / pack-hygiene 测试)、本机复包拿不到任何凭据,只有发车机上 `npm publish` 那一个进程拿到
3044
- `NPM_TOKEN`(且带 `--ignore-scripts`)—— 令牌不出发车机。令牌从不打印。
3071
+ 与它起的 ssh / 容器(`npm ci` 的安装脚本 / 构建 / pack-hygiene 测试)、本机复包拿不到任何凭据,只有发车机上 `timeout` 与它起的这一个 `npm publish`(同一条进程链)拿到
3072
+ `NPM_TOKEN`(且带 `--ignore-scripts`)—— 令牌不出发车机。令牌从不打印。发车机需有 GNU `timeout`(coreutils;macOS 上 `brew install coreutils`),缺它在最开头红(退出码 2),不碰网络。
3045
3073
  - **门 4 不再等 CI**(同一裁定):`scripts/post-publish-gates.sh` 的门 4 = 一个 45 min 的 npm 窗(公开 packument 上 `versions[<v>]` 在且
3046
3074
  `dist-tags.latest` 追平)。判决行 `GATES456 v=… rc4=… rc5=… rc6=… shasum=… elapsed=… ciTag=… ciMain=… tagRun=… mainRun=… why4=… chain=…`:既有十一键形状不变,末尾追加第十二键 `chain=<本轮标识>`(见下一条起链启动器);
3047
3075
  CI 两腿(`ciTag` / `ciMain` / `tagRun` / `mainRun`)是**信息格**,不进 rc4、不进 why4;`why4` 只剩 `propagation_timeout`(rc4 非零)与 `-`(绿)。
@@ -3161,7 +3189,7 @@ web-admin 拉取 / fleet 公告 / 凭证续签、WebSearch、对象存储、OTLP
3161
3189
  做**键级**比较,与面的增删用同一张表、同一只比较器:删一个键或改了键的值形 / 容器 / 嵌套面 / 由必选变可选 ⇒ `removed` / `narrowed`(`breaking`),
3162
3190
  新增键 ⇒ `added`,由可选变必选 ⇒ `changed`;该面此前没有键声明(首次登记)时既有的键进 `registered`,不当本版新上 wire。其余帧面的键集仍由 `coreType`
3163
3191
  在安装态 core 里展开,生成器看不见它们的键级增删(core 升版带来的键变化由提货批的读面 × core 清单求交门与各面的 `_Guard*` 编译钉盯)。随 release commit 提交;查账门(门 1.6)第三腿 `--check` 核它在仓且与生成输出逐字节同,缺席 / 陈旧 ⇒ 红。
3164
- `wire-changes/server@7.104.0.json` 是回填的第一份。
3192
+ `wire-changes/server@7.104.0.json` 是回填的第一份。7.136.0 起 `wire-changes/` 随 npm 包出:装包的人在包内 `wire-changes/server@<ver>.json` 就能读到每一版的清单(生成时清单里的 `text` 过发布面内部编号词表,命中的编号换成 `[ref]`;`sources` 里点名的契约文档不随包)。
3165
3193
  - **台账 `refs:` 列建议**:`node scripts/ledger-refs.mjs [--comms-dir <dir>] [--json]` 只读 sema-comms 的 `debts/DEBTS-server.md` /
3166
3194
  `PLANS.md`(server 段)/ `bugs/B-*.md`(文件头 `owner:` 格),按「一方点了另一方的号」双向互链,输出每行的 `refs:` 建议值(号按数值认,
3167
3195
  零填充与不填充的拼法算同一只号;悬空号 stderr 点名)。不改 sema-comms,落账由主会话做。
@@ -3184,6 +3212,8 @@ web-admin 拉取 / fleet 公告 / 凭证续签、WebSearch、对象存储、OTLP
3184
3212
 
3185
3213
  - `alg`:恒为 `libsodium-sealedbox-x25519`(当前唯一取值);`publicKeyId`:写进密文条目的那个 id;`publicKey`:X25519 公钥,标准 base64(32 字节)。
3186
3214
  - 值是**本进程**保管店里最新那把钥匙(`createdAt` 最新)。只有公钥:私钥、私钥文件路径、本机还持有的旧钥匙 id 都不在这个面上。
3215
+ - 答出的公钥恒过一只判断(7.136.0 起):规范的标准 base64、32 字节、不是 X25519 小阶点。保管目录里公钥过不了的钥匙文件不装载(warn `sealed_key_file_unreadable` 带 `flaw`),也不向中心登记;
3216
+ 你这一侧封之前同样该拒小阶点(libsodium 的 `crypto_box_seal` 对小阶公钥本身就抛;换用别的库时先确认它也拒)。
3187
3217
  - **键缺席**有两种含义:引擎早于 7.104.0(不认识这一位),或这台引擎今天没有保管店(起服时建店失败,日志 `sealed_key_store_unavailable`)。
3188
3218
  同一个体里的 `version` 能区分这两种;不论哪种,调用方的动作相同 —— **不要封**:改用 `apiKeyEnv`,或修好保管目录后重启再取。
3189
3219
 
@@ -3219,10 +3249,17 @@ const ciphertext = Buffer.from(sodium.crypto_box_seal(sodium.from_string(apiKey)
3219
3249
  | 失败词 | 什么时候出 | 怎么修 |
3220
3250
  |---|---|---|
3221
3251
  | `sealed_decrypt_failed` | 本进程**持有**这个 `publicKeyId` 的私钥,但开不开:密文不是对着这把公钥封的、被截断或改坏(短于 48 字节)、缺了应有的 `=` 补齐。warn `web_admin_model_sealed_key_open_failed` | 对着 caps 当前给的公钥重封,写回条目 |
3222
- | `stale_ciphertext` | 本进程**没有**这个 `publicKeyId` 的私钥:密文是对着别的进程 / 别的宿主 / 别的副本的公钥封的,或那把钥匙的私钥文件被删、读不了。warn `web_admin_model_sealed_key_no_private_key`(带本机持有的 id) | 对着**这个进程** caps 给的公钥重封;或把那个 id 的私钥文件放回保管目录后重启 |
3252
+ | `stale_ciphertext` | 本进程**没有**这个 `publicKeyId` 的私钥:密文是对着别的进程 / 别的宿主 / 别的副本的公钥封的(含读同一份 `config.d`、保管目录不同的另一台引擎),或那把钥匙的私钥文件被删、读不了。warn `web_admin_model_sealed_key_no_private_key`(带本机持有的 id) | 对着**这个进程** caps 给的公钥重封;或把那个 id 的私钥文件放回保管目录后重启;多台引擎共用一份 `config.d` 时让它们共用保管目录(`SEALED_KEY_DIR`,见 ⑥) |
3223
3253
  | `no_key_store` | 本进程没有保管店:起服时保管目录建不了、改不了权限(只读挂载 / 权限不够 / 数据根不是目录),caps 没有 `sealedKey` 段。warn `web_admin_model_sealed_key_no_store` | 修好保管目录(进程要能建它、每次起服把它设成 `0700`)后重启 |
3224
3254
 
3225
- **⑥ 保管目录与轮换。** 保管目录 = `<数据根>/sealed-key/`(数据根 = `LOCAL_DATA_ROOT` → `CONFIG_LOCAL_DIR` → `AGENT_DATA_DIR` → `~/.ai-agent`),
3255
+ **sealed 条目绑定的是钥匙身份,不是数据根。** 一条 sealed 条目只对持有封它的那把钥匙(`publicKeyId` → 钥匙对)的进程可开;共享同一份 `config.d` 的**每一台**引擎都必须持有那把钥匙,
3256
+ 否则那只模型在那台引擎上响亮失败、不回落(缺这个 id ⇒ `stale_ciphertext` · 同 id 而私钥不对 ⇒ `sealed_decrypt_failed` · 没有保管店 ⇒ `no_key_store`)。持钥的办法两条:共用保管目录(`SEALED_KEY_DIR`,下一段),
3257
+ 或把同一把钥匙文件放进每台引擎各自的保管目录。典型形:终端与桌面端读同一份 `config.d`,而桌面端自起引擎的数据根固定在应用数据目录下 —— 不共用保管目录时,一端封进去的条目在另一端是 `stale_ciphertext`。
3258
+
3259
+ **⑥ 保管目录与轮换。** 保管目录 = `SEALED_KEY_DIR`(7.136.0 起;绝对路径),缺席时 = `<数据根>/sealed-key/`(数据根 = `LOCAL_DATA_ROOT` → `CONFIG_LOCAL_DIR` → `AGENT_DATA_DIR` → `~/.ai-agent`)。
3260
+ 设了 `SEALED_KEY_DIR`:它是唯一保管目录(停驻任务凭据封签的子目录 `checkpoint-ctx-keys/` 跟着它走);相对路径起服拒;起服在它上面建不了保管店(建不了 / 设不成 `0700` / 只读挂载)⇒ **起服拒**
3261
+ (不会以「没有保管店」的状态起来);多台引擎(同一个 OS 用户)把它设成同一个路径 ⇒ 持有同一把钥匙。缺席时建不了只告警(`sealed_key_store_unavailable`,与 7.135.x 相同)。
3262
+ 已有部署切到 `SEALED_KEY_DIR` 时先把原保管目录(`<数据根>/sealed-key/`)里的钥匙文件连同子目录 `checkpoint-ctx-keys/` 整个搬进新目录(停服时搬),再设旋钮起服 —— 否则新目录是空目录,起服生成一把新钥:对着旧钥封的 sealed 模型全部 `stale_ciphertext`,停驻、带请求凭据的任务续跑答 `409 conflict.checkpoint_ctx_unsealable`(`key_not_held`)。
3226
3263
  每把钥匙一个文件 `<publicKeyId>.json`(文件 `0600`,目录 `0700`)。**私钥只在这里**:丢了,对着它封的密文全部变成 `stale_ciphertext`,
3227
3264
  只能重封 —— 备份时按私钥对待。7.134.0 起它下面还有一个子目录 `checkpoint-ctx-keys/`:停驻任务续跑上下文里请求凭据的封签钥匙(一个域一对钥匙,与模型钥匙不共用;
3228
3265
  同样的文件形、同样的「空目录生成一把」;不向中心登记、不出现在 caps 上)。它丢了,封在停驻行里的凭据就开不开(续跑答 `409 conflict.checkpoint_ctx_unsealable` `reason:"key_not_held"`)——
@@ -3239,11 +3276,11 @@ const ciphertext = Buffer.from(sodium.crypto_box_seal(sodium.from_string(apiKey)
3239
3276
  —— 在负载均衡后面取 caps,取到哪台就是哪台的钥匙。出路两条:
3240
3277
 
3241
3278
  - **共享保管目录**:让每个副本的 `<数据根>/sealed-key/` 是同一份钥匙文件(例如把同一个卷挂到这个路径)。先把钥匙备好(先起一个副本让它生成,
3242
- 或放好钥匙文件)再起其余副本 —— 空目录上并发首启会各自生成一把,各副本只持有自己那一把,直到下次重启。目录要对每个副本**可写**(每次起服
3279
+ 或放好钥匙文件)再起其余副本 —— 钥匙创建是先读后写、没有互斥:两个副本在同一个目录上同时首启、都没读到可用的钥匙文件时,各副本可能各自生成一把、只持有自己那一把,直到下次重启。目录要对每个副本**可写**(每次起服
3243
3280
  会把它设成 `0700`;只读挂载 ⇒ 建店失败 ⇒ `no_key_store`)。⚠️ 只共享 `sealed-key/` 这一个子目录:`DB_BACKEND=local` 的整个数据根禁止跨容器 /
3244
3281
  跨主机共享(`docs/DEPLOY-PREREQS.md`)。
3245
3282
  照随包 `deploy/sema-up` 图表部署时这一步图表替你做了:`sealedKey.persistence` 块(缺省开)把整个 `sealed-key/` 目录挂成一只 PVC,访问模式缺省单副本 `ReadWriteOnce`、多副本 `ReadWriteMany`
3246
- (要存储类支持 RWX;空卷首装后 `rollout restart` 一次,让并发首启各生成的钥匙互相都持有);显式关掉它(`sealedKey.persistence.enabled: false`)图表会同时给 worker 设 `DURABLE_APPROVAL=false`,并拒绝再配 `server.durableApproval: true`。
3283
+ (要存储类支持 RWX;空卷首装后 `rollout restart` 一次,让并发首启时可能各生成的钥匙互相都持有);显式关掉它(`sealedKey.persistence.enabled: false`)图表会同时给 worker 设 `DURABLE_APPROVAL=false`,并拒绝再配 `server.durableApproval: true`。
3247
3284
  - **走中心形**:接 web-admin,执行侧公钥登记在中心(起服时带管理令牌自动上报,否则把公钥打印出来供手工登记),web 面对着登记的公钥封、中心存密文。
3248
3285
  调用方不必逐台去问 caps;但同一个 worker 的多个副本各有各的保管店这件事中心形不改变 —— 要它们都开得了同一份密文,仍要共享保管目录。
3249
3286
 
@@ -3251,7 +3288,7 @@ const ciphertext = Buffer.from(sodium.crypto_box_seal(sodium.from_string(apiKey)
3251
3288
  在 B 副本上续跑答 `409 conflict.checkpoint_ctx_unsealable` `reason:"key_not_held"`;单副本也要让这个目录跨重启持久。**7.133.x 与 7.134.0 不能混跑**:先停掉全部 7.133.x 副本,再起第一个 7.134.0 副本
3252
3289
  (一次性迁移 M28 在它起服时把升级前停驻行里的明文封好);混跑时 7.133.x 副本续跑一条已封签的行会把凭据位当空值、任务无凭据照跑、不报错。降级到 7.133.x 之前先把带凭据的停驻行结掉(决定或取消)。
3253
3290
  多副本升级时先起一个 7.134.0 副本、等它起好再起其余副本(耐久审批开着时它起好即打出 `checkpoint_ctx_credentials_sealed` 一行):停驻封签的钥匙在新子目录 `checkpoint-ctx-keys/` 里,
3254
- 升级后第一个起服的副本生成它(`sealed-key/` 早已共享好也一样);同时起多个副本会各建一把、互相答 `key_not_held`,直到各自重启一次(钥匙只在起服时读目录里的全部文件)。
3291
+ 升级后第一个起服的副本生成它(`sealed-key/` 早已共享好也一样);同时起多个副本可能各建一把、互相答 `key_not_held`,直到各自重启一次(钥匙只在起服时读目录里的全部文件;各建只发生在两个副本在同一个目录上同时首启、都没读到可用的钥匙文件时)。
3255
3292
 
3256
3293
  ### 9.11 会话规则店:抹除封键与显式恢复(7.106.0 起)
3257
3294
 
@@ -166,7 +166,7 @@ server 的 `<数据根>/sealed-key/` 存着停驻任务续跑上下文里请求
166
166
  有效访问模式是 `ReadWriteOnce` 时 Deployment 用 `Recreate`(RWO 卷同时只能被一个节点挂,滚动更新的新 Pod 落在别的节点就永远等不到卷;代价是升级窗口内短暂没有副本)。
167
167
  - **没有 RWX 的集群**:`--set server.replicas=1`,或单节点多副本盖 RWO(上一条),或关持久 `--set sealedKey.persistence.enabled=false` —— 关掉时图表**显式**给每只 worker 设 `DURABLE_APPROVAL=false`
168
168
  (耐久审批整条关,不产生续不了的停驻行;`helm install` 打印的安装说明里也有这一句),且同时配 `server.durableApproval=true` 渲染即拒。
169
- - **首装是空卷**:多个 pod 并发首启会各自生成一把钥匙、各持自己那一把;装好后 `kubectl -n <ns> rollout restart deploy/sema-server`(启用 `serverV4pro` 时再加 `deploy/sema-server-v4pro`)一次,之后每个 pod 读到全部钥匙。
169
+ - **首装是空卷**:多个 pod 并发首启可能各建一把钥匙、各持自己那一把(钥匙创建没有互斥,只有两个副本在同一个目录上同时首启、都没读到可用的钥匙文件时才发生,不是每次都分叉);装好后不论有没有分叉都执行 `kubectl -n <ns> rollout restart deploy/sema-server`(启用 `serverV4pro` 时再加 `deploy/sema-server-v4pro`)一次,之后每个 pod 读到全部钥匙。
170
170
  - PVC 的 `accessModes` 建好后不可改:副本数跨过 1 的边界时 `helm upgrade` 会撞「spec is immutable」;卷里是私钥,换卷前先结掉停驻行或备份钥匙文件。
171
171
  PVC 带 `helm.sh/resource-policy: keep`:`helm uninstall` 与把 `enabled` 改成 `false` 的 upgrade 都不删它(确认不要了再 `kubectl -n <ns> delete pvc sema-server-sealed-key`)。
172
172
 
@@ -8,7 +8,7 @@
8
8
  多个 pod 共用这只卷,存储类必须支持 ReadWriteMany(PVC 一直 Pending 就是它不支持:出路 server.replicas=1,或 sealedKey.persistence.enabled=false 并接受耐久审批随之关)。
9
9
  {{- end }}
10
10
  {{- if gt (int (include "sema.sealedKey.podCount" .)) 1 }}
11
- 首装是空卷:多个副本并发首启会各自生成一把钥匙、各持自己那一把 —— 装好后执行一次 kubectl -n {{ .Release.Namespace }} rollout restart deploy/sema-server{{ if .Values.serverV4pro.enabled }} deploy/sema-server-v4pro{{ end }},此后每个副本读到目录里的全部钥匙。
11
+ 首装是空卷:多个副本并发首启可能各建一把钥匙、各持自己那一把(只有两个副本在同一个目录上同时首启、都没读到可用的钥匙文件时才发生,不是每次都分叉)—— 装好后不论有没有分叉都执行一次 kubectl -n {{ .Release.Namespace }} rollout restart deploy/sema-server{{ if .Values.serverV4pro.enabled }} deploy/sema-server-v4pro{{ end }},此后每个副本读到目录里的全部钥匙。
12
12
  {{- end }}
13
13
  {{- else }}
14
14
  停驻封签钥匙:sealedKey.persistence.enabled=false —— 不建卷,图表已给每只 worker 显式设上 DURABLE_APPROVAL=false(耐久审批整条关,不产生续不了的停驻行;代价见 values.yaml 的 sealedKey 块;要重新打开,把它改回 true)。关持久 + 提交体没声明 settings.approvalFrameFamilies 的客户端 ⇒ 受门工具当场拒(没有耐久店,无处停驻;7.134.0 起审批卡只发给声明认得这一族的客户端)。
@@ -86,7 +86,7 @@ server:
86
86
  # 这只卷里是私钥,不要为了过这个错直接删 PVC:先把停驻行结掉(随包排空检查脚本数一数)或备份钥匙文件,再换卷。
87
87
  # PVC 带 helm.sh/resource-policy: keep:`helm uninstall` 与把 enabled 改成 false 的 upgrade 都不删它(钥匙留着;确认不要了再 `kubectl -n <ns> delete pvc sema-server-sealed-key`);
88
88
  # 同名 release 重装会接管这只留下的 PVC。
89
- # ⚠ 空卷上多副本并发首启:每个副本看到空目录各自生成一把钥匙,各副本只持有自己那一把,直到下次重启 —— 首装后 `kubectl -n <ns> rollout restart deploy/sema-server`
89
+ # ⚠ 空卷上多副本并发首启:钥匙创建没有互斥,两个副本在同一个目录上同时首启、都没读到可用的钥匙文件时,各副本可能各建一把钥匙、只持有自己那一把,直到下次重启(不是每次都分叉)—— 首装后不论有没有分叉都执行 `kubectl -n <ns> rollout restart deploy/sema-server`
90
90
  # 一次(启用 serverV4pro 时再加 deploy/sema-server-v4pro;NOTES 也说这句),重启之后每个副本读到目录里的全部钥匙,从此一致。
91
91
  sealedKey:
92
92
  persistence: