@sema-agent/server 7.144.0 → 7.146.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 (305) hide show
  1. package/MIGRATION.md +621 -0
  2. package/README.md +3 -0
  3. package/README.zh-CN.md +1 -0
  4. package/USAGE.md +233 -54
  5. package/deploy/facts/CHANGES.json +25 -0
  6. package/deploy/facts/README.md +57 -0
  7. package/deploy/facts/lanes.json +54 -0
  8. package/deploy/facts/lanes.schema.json +57 -0
  9. package/deploy/facts/sandbox-rbac.json +113 -0
  10. package/deploy/facts/sandbox-rbac.schema.json +74 -0
  11. package/deploy/facts/sealed-key.json +9 -0
  12. package/deploy/facts/sealed-key.schema.json +52 -0
  13. package/deploy/sema-up/chart/templates/_helpers.tpl +19 -0
  14. package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +47 -7
  15. package/deploy/sema-up/chart/templates/server-v4pro.yaml +4 -0
  16. package/deploy/sema-up/chart/templates/server.yaml +4 -0
  17. package/deploy/sema-up/chart/values.yaml +7 -0
  18. package/deploy/sema-up/stack/sema-stack-cluster.yaml +30 -14
  19. package/dist/adoption/import-limits.d.ts +6 -0
  20. package/dist/adoption/import-limits.js +7 -0
  21. package/dist/adoption/import.d.ts +62 -0
  22. package/dist/adoption/import.js +107 -0
  23. package/dist/adoption/wire.d.ts +52 -0
  24. package/dist/adoption/wire.js +9 -0
  25. package/dist/approval-ask-machine.d.ts +2 -2
  26. package/dist/approval-card.d.ts +3 -12
  27. package/dist/approval-card.js +0 -4
  28. package/dist/approval-gate-currency.d.ts +1 -1
  29. package/dist/approval-gate-currency.js +1 -1
  30. package/dist/approval-reconciler.d.ts +1 -1
  31. package/dist/audit.d.ts +2 -21
  32. package/dist/audit.js +0 -9
  33. package/dist/boot/deployment-mods.d.ts +11 -1
  34. package/dist/boot/deployment-mods.js +15 -2
  35. package/dist/boot/device-lane.js +2 -0
  36. package/dist/boot/engine-lease.d.ts +193 -21
  37. package/dist/boot/engine-lease.js +146 -20
  38. package/dist/boot/execution-env.d.ts +4 -0
  39. package/dist/boot/execution-env.js +12 -0
  40. package/dist/boot/exit-boundary.d.ts +50 -0
  41. package/dist/boot/exit-boundary.js +32 -0
  42. package/dist/boot/exit-last.d.ts +20 -4
  43. package/dist/boot/exit-last.js +21 -4
  44. package/dist/boot/governance-seams.js +1 -0
  45. package/dist/boot/memory-consolidation.d.ts +29 -17
  46. package/dist/boot/memory-consolidation.js +26 -15
  47. package/dist/boot/reapers.d.ts +12 -4
  48. package/dist/boot/reapers.js +39 -15
  49. package/dist/boot/resolve-spec.d.ts +2 -0
  50. package/dist/boot/resolve-spec.js +38 -15
  51. package/dist/boot/runner-deps.js +1 -1
  52. package/dist/boot/session-faces.d.ts +1 -1
  53. package/dist/boot/session-faces.js +6 -4
  54. package/dist/boot/shutdown.js +27 -9
  55. package/dist/boot/stage-01-config.js +1 -1
  56. package/dist/boot/stage-03-store-backend.d.ts +1 -0
  57. package/dist/boot/stage-03-store-backend.js +2 -2
  58. package/dist/boot/stage-04-stores.d.ts +1 -0
  59. package/dist/boot/stage-05-execution-env.d.ts +3 -0
  60. package/dist/boot/stage-05-execution-env.js +3 -3
  61. package/dist/boot/stage-06-runners.d.ts +3 -0
  62. package/dist/boot/stage-06-runners.js +11 -10
  63. package/dist/boot/stage-07-capability-layer.d.ts +3 -0
  64. package/dist/boot/stage-07-capability-layer.js +7 -2
  65. package/dist/boot/stage-08-reapers.d.ts +3 -0
  66. package/dist/boot/stage-08-reapers.js +3 -3
  67. package/dist/boot/stage-09-leader.d.ts +4 -1
  68. package/dist/boot/stage-10-http-server.d.ts +4 -1
  69. package/dist/boot/stage-10-http-server.js +4 -3
  70. package/dist/boot/stores.d.ts +1 -0
  71. package/dist/boot/stores.js +35 -19
  72. package/dist/boot/workflow-orchestration.d.ts +1 -1
  73. package/dist/boot/workflow-orchestration.js +2 -1
  74. package/dist/capabilities/composite-members.d.ts +3 -1
  75. package/dist/capabilities/composite-members.js +3 -1
  76. package/dist/capabilities/scenarios.d.ts +2 -2
  77. package/dist/capability-facts.d.ts +18 -1
  78. package/dist/capability-facts.js +25 -7
  79. package/dist/checkpoint-ctx-seal-m28.d.ts +1 -1
  80. package/dist/config-catalog.js +13 -4
  81. package/dist/config-d-migration.d.ts +3 -2
  82. package/dist/config-provider.d.ts +2 -2
  83. package/dist/config-types.d.ts +43 -4
  84. package/dist/config.d.ts +6 -1
  85. package/dist/config.js +67 -11
  86. package/dist/deleted-session-ids.d.ts +19 -0
  87. package/dist/deleted-session-ids.js +26 -0
  88. package/dist/deploy-facts.d.ts +155 -0
  89. package/dist/deploy-facts.js +94 -0
  90. package/dist/device-ws-hub.d.ts +48 -3
  91. package/dist/device-ws-hub.js +48 -2
  92. package/dist/error-code-family.d.ts +35 -0
  93. package/dist/error-code-family.js +26 -0
  94. package/dist/fleet/drain-entry.d.ts +39 -3
  95. package/dist/fleet/drain-entry.js +6 -0
  96. package/dist/fleet/fleet-bus.d.ts +15 -4
  97. package/dist/fleet/fleet-bus.js +17 -3
  98. package/dist/hooks/branch-transcript.d.ts +1 -1
  99. package/dist/hooks/branch-transcript.js +1 -0
  100. package/dist/hooks/classic-stations.d.ts +25 -8
  101. package/dist/hooks/classic-stations.js +29 -3
  102. package/dist/hooks/hook-runner.d.ts +33 -16
  103. package/dist/hooks/hook-runner.js +166 -73
  104. package/dist/hooks/hook-source.d.ts +14 -2
  105. package/dist/hooks/hook-source.js +8 -0
  106. package/dist/http/active-run-conflict.d.ts +1 -1
  107. package/dist/http/active-run-conflict.js +1 -1
  108. package/dist/http/admission.js +13 -2
  109. package/dist/http/checkpoint-ctx.d.ts +60 -71
  110. package/dist/http/checkpoint-ctx.js +35 -40
  111. package/dist/http/dispatch.js +18 -2
  112. package/dist/http/durable-run-leg.d.ts +5 -0
  113. package/dist/http/durable-run-leg.js +8 -6
  114. package/dist/http/execution-context.d.ts +4 -0
  115. package/dist/http/execution-context.js +9 -0
  116. package/dist/http/health-self-report.d.ts +6 -2
  117. package/dist/http/health-self-report.js +3 -2
  118. package/dist/http/memory-compliance-failure-reply.d.ts +1 -1
  119. package/dist/http/memory-compliance-failure-reply.js +2 -15
  120. package/dist/http/memory-control-plane-corrupt-reply.d.ts +25 -0
  121. package/dist/http/memory-control-plane-corrupt-reply.js +19 -0
  122. package/dist/http/principal-gate.d.ts +35 -12
  123. package/dist/http/principal-gate.js +16 -12
  124. package/dist/http/request-guards.d.ts +1 -1
  125. package/dist/http/request-guards.js +5 -5
  126. package/dist/http/resume-legs.d.ts +22 -0
  127. package/dist/http/resume-legs.js +31 -20
  128. package/dist/http/route-ctx.d.ts +26 -10
  129. package/dist/http/route-table.js +2 -0
  130. package/dist/http/routes/adoption.d.ts +11 -0
  131. package/dist/http/routes/adoption.js +80 -6
  132. package/dist/http/routes/agents-roster.js +1 -0
  133. package/dist/http/routes/approvals-assistant.d.ts +20 -1
  134. package/dist/http/routes/approvals-assistant.js +29 -7
  135. package/dist/http/routes/background.js +1 -0
  136. package/dist/http/routes/capabilities.js +4 -1
  137. package/dist/http/routes/diagnostics.d.ts +9 -14
  138. package/dist/http/routes/diagnostics.js +10 -20
  139. package/dist/http/routes/engine.d.ts +31 -0
  140. package/dist/http/routes/engine.js +81 -0
  141. package/dist/http/routes/memory-bundle.js +3 -0
  142. package/dist/http/routes/memory-consolidation.js +3 -5
  143. package/dist/http/routes/memory-origin.d.ts +1 -1
  144. package/dist/http/routes/memory-origin.js +2 -4
  145. package/dist/http/routes/memory-policy.js +5 -1
  146. package/dist/http/routes/observability.js +3 -6
  147. package/dist/http/routes/rules.d.ts +1 -1
  148. package/dist/http/routes/rules.js +5 -2
  149. package/dist/http/routes/runs.js +7 -3
  150. package/dist/http/routes/session-sync.js +24 -25
  151. package/dist/http/routes/sessions-list.d.ts +1 -1
  152. package/dist/http/routes/sessions.js +4 -0
  153. package/dist/http/routes/tasks.js +35 -14
  154. package/dist/http/routes/trace-usage.js +1 -1
  155. package/dist/http/run-concurrency-cap.d.ts +86 -0
  156. package/dist/http/run-concurrency-cap.js +54 -0
  157. package/dist/http/run-meta.d.ts +14 -7
  158. package/dist/http/run-meta.js +5 -2
  159. package/dist/http/server.d.ts +27 -16
  160. package/dist/http/server.js +21 -3
  161. package/dist/http/stored-body-replay.d.ts +9 -6
  162. package/dist/http/stored-body-replay.js +19 -5
  163. package/dist/http/submit-ctx.d.ts +12 -0
  164. package/dist/http/submit-ctx.js +23 -0
  165. package/dist/http/wire-types.d.ts +8 -3
  166. package/dist/json-body-limit.d.ts +8 -0
  167. package/dist/json-body-limit.js +2 -0
  168. package/dist/leader/diffout.d.ts +1 -1
  169. package/dist/local-data-root.d.ts +2 -2
  170. package/dist/memory-operator-faces.d.ts +9 -4
  171. package/dist/memory-scope-stores.d.ts +9 -7
  172. package/dist/memory-scope-stores.js +3 -14
  173. package/dist/memory-scope.d.ts +36 -0
  174. package/dist/memory-scope.js +32 -1
  175. package/dist/net-address.d.ts +53 -0
  176. package/dist/net-address.js +111 -0
  177. package/dist/observability/fail-open.d.ts +28 -12
  178. package/dist/observability/fail-open.js +28 -12
  179. package/dist/orchestration/workflow-notify-journal.js +2 -3
  180. package/dist/org-delivery-gate.js +1 -1
  181. package/dist/peer-directory.d.ts +6 -12
  182. package/dist/peer-directory.js +5 -1
  183. package/dist/plugins/approval-ask-store-file.d.ts +16 -2
  184. package/dist/plugins/approval-ask-store-file.js +21 -1
  185. package/dist/plugins/approval-ask-store-memory.d.ts +8 -2
  186. package/dist/plugins/approval-ask-store-memory.js +11 -8
  187. package/dist/plugins/caching-session-store.d.ts +6 -1
  188. package/dist/plugins/caching-session-store.js +6 -0
  189. package/dist/plugins/checkpoint-store-face.d.ts +426 -0
  190. package/dist/plugins/checkpoint-store-face.js +152 -0
  191. package/dist/plugins/file-run-store.d.ts +13 -7
  192. package/dist/plugins/file-run-store.js +12 -13
  193. package/dist/plugins/fork-routing-session-store.d.ts +6 -2
  194. package/dist/plugins/fork-routing-session-store.js +2 -2
  195. package/dist/plugins/imported-layer-root-retirement.d.ts +10 -8
  196. package/dist/plugins/local-checkpoint-store.d.ts +16 -5
  197. package/dist/plugins/local-checkpoint-store.js +25 -6
  198. package/dist/plugins/local-session-store.d.ts +80 -21
  199. package/dist/plugins/local-session-store.js +79 -22
  200. package/dist/plugins/memory-engine-pg.d.ts +29 -6
  201. package/dist/plugins/memory-engine-pg.js +96 -214
  202. package/dist/plugins/memory-engine-sql-apply.d.ts +121 -0
  203. package/dist/plugins/memory-engine-sql-apply.js +358 -0
  204. package/dist/plugins/memory-engine-tidb.d.ts +35 -9
  205. package/dist/plugins/memory-engine-tidb.js +97 -227
  206. package/dist/plugins/memory-key-guards.d.ts +1 -1
  207. package/dist/plugins/memory-origin-law.d.ts +3 -3
  208. package/dist/plugins/memory-run-store.d.ts +2 -0
  209. package/dist/plugins/memory-run-store.js +6 -0
  210. package/dist/plugins/one-time-migrations.d.ts +32 -56
  211. package/dist/plugins/one-time-migrations.js +27 -119
  212. package/dist/plugins/permission-rule-store-file.d.ts +17 -3
  213. package/dist/plugins/permission-rule-store-file.js +8 -1
  214. package/dist/plugins/pg-pool.js +1 -1
  215. package/dist/plugins/pg-session-storage.d.ts +17 -4
  216. package/dist/plugins/pg-session-storage.js +8 -4
  217. package/dist/plugins/run-store-sql.d.ts +4 -0
  218. package/dist/plugins/run-store-sql.js +5 -2
  219. package/dist/plugins/schema-ensure-legs.js +2 -2
  220. package/dist/plugins/session-fork.d.ts +31 -9
  221. package/dist/plugins/session-fork.js +3 -2
  222. package/dist/plugins/session-placement.d.ts +4 -2
  223. package/dist/plugins/session-store.d.ts +1 -1
  224. package/dist/plugins/sql-json-column.d.ts +5 -1
  225. package/dist/plugins/store-backend.d.ts +11 -8
  226. package/dist/plugins/store-backend.js +8 -7
  227. package/dist/plugins/store-contracts.d.ts +1 -11
  228. package/dist/plugins/store-contracts.js +0 -9
  229. package/dist/plugins/store-sweep.d.ts +63 -0
  230. package/dist/plugins/store-sweep.js +48 -0
  231. package/dist/plugins/tidb-pool.js +1 -1
  232. package/dist/plugins/tidb-session-store.d.ts +16 -3
  233. package/dist/plugins/tidb-session-store.js +8 -4
  234. package/dist/prompt-screen.d.ts +18 -0
  235. package/dist/prompt-screen.js +5 -0
  236. package/dist/rules-consent.d.ts +56 -51
  237. package/dist/rules-consent.js +86 -14
  238. package/dist/run-acceptance.d.ts +40 -10
  239. package/dist/run-acceptance.js +38 -5
  240. package/dist/run-cancel-context.d.ts +4 -1
  241. package/dist/run-cancel-context.js +2 -1
  242. package/dist/runs.js +2 -2
  243. package/dist/sandbox-egress-policy.js +6 -11
  244. package/dist/sandbox-rbac-rules.d.ts +88 -0
  245. package/dist/sandbox-rbac-rules.js +80 -0
  246. package/dist/sealed-key-layout.d.ts +17 -0
  247. package/dist/sealed-key-layout.js +4 -0
  248. package/dist/sealed-key.js +5 -5
  249. package/dist/security.d.ts +48 -9
  250. package/dist/security.js +9 -2
  251. package/dist/session-agent.d.ts +39 -0
  252. package/dist/session-agent.js +16 -0
  253. package/dist/session-error-wire.d.ts +4 -4
  254. package/dist/session-notify-door.d.ts +5 -3
  255. package/dist/session-resident-background.d.ts +57 -0
  256. package/dist/session-resident-background.js +20 -0
  257. package/dist/session-sync-kernel.d.ts +53 -14
  258. package/dist/session-sync-kernel.js +70 -1
  259. package/dist/session-sync.d.ts +5 -3
  260. package/dist/session-workspace.d.ts +1 -0
  261. package/dist/session-workspace.js +1 -0
  262. package/dist/sighup-idle.d.ts +9 -7
  263. package/dist/sighup-idle.js +4 -5
  264. package/dist/spec-fields.d.ts +16 -0
  265. package/dist/spec-fields.js +7 -0
  266. package/dist/stored-request-env-credentials-m38.d.ts +3 -0
  267. package/dist/stored-request-env-credentials-m38.js +8 -0
  268. package/dist/stored-request-prompt-profile-m30.d.ts +1 -1
  269. package/dist/{plugins → stores-sql}/checkpoint-store-sql.d.ts +33 -429
  270. package/dist/{plugins → stores-sql}/checkpoint-store-sql.js +70 -209
  271. package/dist/{plugins → stores-sql}/mailbox-store-sql.d.ts +2 -2
  272. package/dist/{plugins → stores-sql}/mailbox-store-sql.js +4 -4
  273. package/dist/{plugins → stores-sql}/roster-store-sql.d.ts +2 -2
  274. package/dist/{plugins → stores-sql}/roster-store-sql.js +1 -1
  275. package/dist/{plugins → stores-sql}/tool-result-store-sql.d.ts +2 -2
  276. package/dist/{plugins → stores-sql}/tool-result-store-sql.js +6 -6
  277. package/dist/{plugins → stores-sql}/workflow-journal-store-sql.d.ts +7 -12
  278. package/dist/{plugins → stores-sql}/workflow-journal-store-sql.js +3 -8
  279. package/dist/task-settings.d.ts +59 -4
  280. package/dist/task-settings.js +50 -4
  281. package/dist/tool-approval.d.ts +2 -2
  282. package/dist/trace/core-keyset-guard.d.ts +8 -7
  283. package/dist/trace/engine-notice-wire.d.ts +1 -1
  284. package/dist/trace/engine-notice-wire.js +2 -0
  285. package/dist/trace/exit-seat.d.ts +6 -0
  286. package/dist/trace/exit-seat.js +1 -0
  287. package/dist/trace/model-segment.d.ts +3 -2
  288. package/dist/trace/model-segment.js +16 -8
  289. package/dist/trace/project.js +9 -0
  290. package/dist/trace/redact.d.ts +79 -4
  291. package/dist/trace/redact.js +523 -123
  292. package/dist/trace/self-minted-wire.d.ts +391 -1
  293. package/dist/trace/self-minted-wire.js +145 -0
  294. package/dist/trace/wire-projection-faces.d.ts +1 -1
  295. package/dist/trace/wire-projection-faces.js +1 -1
  296. package/dist/usage-analytics.d.ts +4 -0
  297. package/package.json +12 -4
  298. package/scripts/drain-check-local.mjs +8 -29
  299. package/scripts/drain-check.mysql.sql +3 -24
  300. package/scripts/drain-check.pg.sql +3 -24
  301. package/skills/update-config.md +1 -1
  302. package/wire-changes/server@7.145.0.json +146 -0
  303. package/wire-changes/server@7.146.0.json +3221 -0
  304. package/dist/approval-batch-census.d.ts +0 -27
  305. package/dist/approval-batch-census.js +0 -27
package/USAGE.md CHANGED
@@ -74,6 +74,10 @@ docker compose up --build --scale service=4 # 4 个无状态 worker,nginx
74
74
  > | `device`(每人自己的设备) | ✅ | ✅ | 单用户 `open`(来源 `posture`);多租户 不铸 = 引擎默认 `roots` |
75
75
  > | 未点名(云形进程内 stub,零手带工具、非手带工具照常执行) | ✅(`REQUIRE_EXECUTION_LANE=true` 可改成拒启) | ✅(同左) | 单用户 `open`(来源 `posture`);多租户 不铸 = 引擎默认 `roots` |
76
76
 
77
+ > 🔴 **硬断预告(7.147.0 起)**:每个部署都要显式声明自己最终生效的执行车道 —— env `REMOTE_EXEC`(`CONFIG_PROVIDER=local` 形下 `config.d/remote-exec.json` 也算);没有声明 ⇒ 拒启(`CONFIG_PROVIDER=local` 不再隐含 `host`,非 local 未设也不再静默落进「不挂手」的进程内桩)。「不给智能体的工具提供执行环境」改写成车道词 `none`,取代词表外的状态名 `in-process`(`GET /v1/capabilities` 的 `executionLane.provider`、`GET /v1/sessions/:id/execution-placement` 的 `lane`、409 的 `boundLane`、403 `execution_lane_not_allowed` 的 `extra.lane` 换值);`REQUIRE_EXECUTION_LANE` 退役(设了任何非空值 ⇒ 拒启,拒句指向 `REMOTE_EXEC`),自己拼的清单或拷过随包部署产物的 env 里写着它的,升级前要去掉。库里或数据根里已有记着 `in-process` 的会话 / 审批行的部署(跑过未点名车道的进程内桩的),须停全部旧副本、跑一次性迁移、再起 7.147.0,不可滚动混跑;web-admin 渲染 env 的中心 worker,须先停渲 `REQUIRE_EXECUTION_LANE`,再升到 7.147.0。照旧行为的一行修法:7.146.0 里非 local 且未设车道的 ⇒ 7.147.0 写 `REMOTE_EXEC=none`,`CONFIG_PROVIDER=local` 且未设的 ⇒ 写 `REMOTE_EXEC=host`(7.146.0 及以前不认 `none` 这个词,别提前写)。7.146.0 本身零行为变化。
78
+
79
+ > **设备 WS 未鉴权握手的 per-remote 帽(`DEVICE_MAX_PREAUTH_PER_REMOTE`,缺省 8)按真实对端计**:缺省按 socket 对端计 —— 引擎在反代 / 托管入口 / 公司 NAT 之后时,全员同一个 socket 对端,共用这 8 个名额。把**自己的**反代写进 `DEVICE_TRUSTED_PROXY_CIDRS`(CIDR,逗号分隔;缺省空 = 不读任何代理头):socket 对端落在名单内才读 `X-Forwarded-For`,取自右向左第一个不在名单内的地址作帽键;名单外的对端一律不采代理头;请求头行数到顶(100 行)/ 头缺席 / 坏形 / 全在名单内 ⇒ 退回 socket 对端 + 一行 warn `device_preauth_forwarded_unusable`。名单每项是 IP 地址或 CIDR(裸地址 = 单台);名单里的来源能改帽键,所以只放自己控制的反代;前缀 0、主机位非零、不是 IP 地址或 CIDR 的项、空项拒启。不读 `Forwarded` / `X-Real-IP`,不支持 PROXY protocol。全局帽 64 不变。这根旋钮的语义行与缺省句也由 `GET /v1/config/catalog` 报出(域 `orchestration`,安全轴)。
80
+
77
81
  > 🔴 **`REMOTE_EXEC=device` 必须声明设备入口 `DEVICE_RELAY_URL`(无缺省)**。两种合法值:设备连不到引擎自己的地址(引擎在中继 / 网关之后)时设备该拨的那一个 `ws://` / `wss://` 绝对地址(逐字上 `GET /v1/capabilities` 的 `deviceExecutor.relayUrl`);或字面 `none`(没有中继,设备按引擎地址 + `deviceExecutor.wsPath` 拨号,能力面不带 `relayUrl`)。缺席 / 空串 / 只含空白 ⇒ 起服拒,拒在车道必需 env 门(与 `K8S_NAMESPACE` 等其余车道必需键同一句:`required env DEVICE_RELAY_URL is unset, empty or whitespace-only`);写了却不合文法 ⇒ 起服拒,拒句点名两种合法值(规则文字取自契约包)。`none` 而根会话上还有设备绑定 ⇒ 起服 warn `device_relay_withdrawn_with_bindings`(`bindings` = 绑定条数,不带会话 id)。撤中继声明的回滚序:先恢复引擎自己的入口,再把值改成 `none`。
78
82
 
79
83
  > 🔴 **7.104.0 起 `CONFIG_PROVIDER` 是闭集**:只认 `local` / `remote` / 不设(空串 = 不设),**逐字**比较 —— `Local`、` local`、`remote ` 这类写法**拒启**,拒因句点名旋钮并列出合法词。此前认不出的词照起,却被配置源当成 `local`、被隐式 host 车道当成「不是 local」,local 配置源的根还落回进程 cwd(与车道文件覆写读的本地根分家)。本地配置根只有一条链:`CONFIG_LOCAL_DIR` → `AGENT_DATA_DIR` → `~/.ai-agent`,配置源与 `config.d/remote-exec.json` 读同一个。
@@ -173,6 +177,11 @@ ANTHROPIC_API_KEY=sk-ant-… # 可选:ANTHROPIC_BASE_URL / ANTHROPIC_VERSION /
173
177
  引擎注入的 `<system-reminder>` / `<task-notification>` / `<attachment>` 块,以及壳铸进用户轮的**本地命令回显**(`<command-name>` / `<command-message>` / `<command-args>`、`<local-command-stdout>` / `<local-command-stderr>` / `<local-command-caveat>`、
174
178
  `<bash-input>` / `<bash-stdout>` / `<bash-stderr>` / `<bash-exit-code>`、`<user-memory-input>`)都不算你写的:`/compact` 一类本地命令之后的下一条提交,预览与标题取的是随后那句话,不再是回显;只有回显、没有你的话 ⇒ run / leader 行的预览是空串(任务名折成 `(notification turn)`),会话列表的预览回落到更早的一句、都没有则缺席(`null`)。
175
179
  代价:正文里真的写了这些开标签字面的那一段在预览 / 标题里被剥(只是**提到**这些词的原样保留),原文在会话历史读面完整可见。
180
+ - **装了 prompt 屏的部署,预览与标题只取引擎原样交付的目标**(7.145.0+):部署座的 mod 包或请求的 `settings.hooks` 声明了 `prompt.submit` / `classic.UserPromptSubmit` 的腿,建 run 行时不写预览、续跑输入里的目标留空、fleet 行不带名;
181
+ 目标的受理帧原样送达(不带 `rewrittenBy`)⇒ 那一拍补上预览、fleet 行名并起标题;屏改写过 / 丢弃 / 抛错 ⇒ 这些面一直缺席,标题不自动起 —— 屏前文本不落进 run 行、续跑输入,也不送进标题模型。
182
+ 续跑输入里的目标在这类腿上一直留空,所以 wake 新建的 run 行没有预览、续跑腿的 fleet 行不带名(原样交下也一样)。订 `prompt.*` / `classic.*` 通配的只观察型运维 mod 也被算成屏(判据读声明、与引擎的通配语义一致),这类部署上只回结果的腿(`verify` / `cascade`)与 wake / 续跑腿同样缺席,其余腿在受理那一拍补上。
183
+ leader 车道 durable worker 的子目标续跑输入不在这条律内:它不经受理座,屏前原文照旧落盘(另票)。
184
+ `verify` 形提交的续跑输入照旧存请求原文(停驻续跑要把它交给验证器;不进 wake 新行的预览与 fleet 行名);目标里的内联 `@名` 选中的模型(请求没带 `model` 时)记在续跑输入里,续跑照旧跑它;只回结果的 `cascade` 提交有屏时预览与标题缺席。嵌入方:`createHttpServer` 的 `deploymentMods` 必填,传交给 Runner 的同一只 `mods` 数组(没有传 `[]`)。
176
185
  - **缺省推断**:`MODEL_PROVIDER` **未设**、但显式配了 Anthropic 协议 base URL(`ANTHROPIC_BASE_URL`)**和**对应凭证(`ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN`)时,缺省自动判 `anthropic` 并打一行启动 warn(`model_provider_inferred`)——纯净机直连 Anthropic 兼容上游不再需要显式第七个键。显式 `MODEL_PROVIDER` 恒赢;只给 base URL 或只给凭证不推断;两者皆无 = `gateway`,与以前逐字节一致。
177
186
  - 启动日志的 `brain` 字段会回显当前组合(`failover` / `anthropicRoute` / `anthropicCacheBreakpoints`)。
178
187
  - **在服务进程之外读同一套 env 路由**(子路径 `@sema-agent/server/model-route-env`;纯数据 + 纯函数,import 无副作用,值依赖只有 `@sema-agent/core` 与 `@sema-agent/settings-schema`):
@@ -387,6 +396,18 @@ MODEL_DEGRADE_ON=rate_limit,breaker_open # 可选,反应式触发器子集;词
387
396
  是引擎账本 —— 别拿它做自动重试/取消的机器判据。
388
397
  - **per-principal 累计配额**:用 `AsyncLocalStorage` 把 principal 透传到 cost tracer,所以**委派子任务的花费也算到发起人头上**。**配了 `SESSION_BACKEND=mysql` 时自动跨副本共享**(`cost_quota` 表,写后聚合的**原子自增** `micro=micro+delta`,对齐固定窗,最终一致——多副本花费 SUM 到一起、不丢增量);否则 in-memory per-replica 计数(单副本兜底;窗形与 SQL 腿同一个时钟对齐桶)。启动日志 `costQuota` 字段回显 `shared(mysql)`/`in-process`/`off`。跨副本是最终一致(flush 间隔内峰值可能略超,由**硬 per-task `maxCostUsd` 兜底**)。
389
398
  - **`RATE_LIMIT_RPM` 请求限流同样自动跨副本**(`SESSION_BACKEND=mysql` 时,`rate_limit` 表,与配额共用 `WriteBehindCounter`;启动日志 `rateLimit` 回显)。注意:写后聚合 = **软限流**(边界上短暂略超 OK,适合公平/热调用方防护);要**硬合规上限**得另走 CAS/原子计数,不靠写后聚合——和断路器跨副本同款权衡。
399
+ - **在飞 run 并发帽 `RUN_MAX_CONCURRENT` / `RUN_MAX_CONCURRENT_PER_PRINCIPAL`**(整数,缺省 `0` = 关,升级零变化;负数 / 小数 / 非数 boot 拒启)。`RATE_LIMIT_RPM` 限的是「每分钟提交几条」,不是「此刻有几条在跑」;一条 run 可以跑几十分钟,
400
+ 共享一台 GPU 或一只小模型网关的部署(调用方自己限并发不可靠)要的是后一个量:
401
+ - **数什么**:**本副本**此刻在飞的 run —— 与排空读数(`drainState.inflight` 里的 run 部分)同一份在飞集:后台 run、同步 / 流式腿,以及有 run 行的续跑腿都占一格。副本之间不共享,各副本各数各的。
402
+ `RUN_MAX_CONCURRENT` 是全局帽;`RUN_MAX_CONCURRENT_PER_PRINCIPAL` 数同一**已验提交主体**(与成本配额门同键 —— 自报头不进键)的在飞数。
403
+ - **判哪几扇门**:**新提交**的四个口 —— `POST /v1/runs`、A2A `message/send`、`POST /v1/tasks`、`POST /v1/tasks/stream`。排在幂等重放**之后**:同 `Idempotency-Key` / 自带 `taskId` 的重试拿回自己的回执,不吃新 429。
404
+ 续跑、唤醒、审批 / 问答作答**不判**(它们占格但不被拒 —— 拒一张已经在等人的卡的作答只会把它卡死),所以帽闸的是**新提交**,不是在飞总数的硬上限:一条停驻的 run 续跑回来时,在飞数可以越过帽。
405
+ `POST /v1/leader` 的后台 run 不在这份在飞集里,本帽不数它也不判它。同键的并发重复提交若赶在首发还在受理(尚未落行、尚未合流)的窗里到达,按新提交判:帽满时与速率 / 配额门同形,可能拿到一次 429,按 `Retry-After` 重投即拿回首发的回执。
406
+ - **超帽的形**:`429`,`errorCode: "limit.rate_exceeded"`(沿用现有限流码,不新铸一枚)+ 体里机读 `reason: "run_concurrency_cap"` + `retryAfterSec: 5` + 头 `Retry-After: 5`(头体同值;是退避提示,不是承诺 ——
407
+ 在飞的 run 可能再跑很久)。拒在建行、认领会话与 SSE 头之前(流式口的 `429` 也是干净的 JSON);`POST /v1/runs` 与 A2A 口的帽排在受理漏斗之后(与 lease / 治理窗的 `429` 同位),拒时漏斗已为新会话做的登记不撤。消费端把它当「稍后再来」,要区分来源就读 `reason`(其余 `limit.rate_exceeded` 的产地不带 `reason`)。
408
+ 每次拒打一行 `run_concurrency_cap_refused`(`scope` = `global` | `principal`,在飞数与帽)。
409
+ - **无主体请求只受全局帽**:无头 service 凭据、`REQUIRE_PRINCIPAL=false` 的单用户部署没有主体可分账,按主体帽对它不判。
410
+ - **并发一阵提交不越帽**:放行即占一枚席位、run 登记进在飞集那一拍放席位,同一瞬间到的 N 条提交恰放行帽允许的条数。没有 run store 的部署上同步 / 流式腿不进在飞集,它们的席位活到请求结束,帽照样数得到。
390
411
 
391
412
  **可选 — 生成调参透传 + 退化打捞(core 1.59/1.60)**
392
413
  ```bash
@@ -465,6 +486,10 @@ MODEL_CODE_ROLES=default,subagent # 不设=全中立;仅这些角色在「
465
486
  ⚠️ **口径别混**:「受限」是**会话级的这一个声明**;请求键 `memoryWrite:false` 铸出的只读**平面**(以及 org 层默认只读、双根非写面)**保持原样的 adopt-on-read**——它只是不 harvest,照旧看得见盘上的新内容。
466
487
  ⚠️ **core 5.26.0 起,remote 执行车道上的 `# Memory` 写指令被撤**(沙箱手带够不着 host 记忆根;读与注入不变)。**判据是「`REMOTE_EXEC` 有没有设」,不是那个旗**——凡设了 `REMOTE_EXEC`(含 `host`)且这次任务真有记忆面,写指令就被撤。因此被打到的**不止**显式 `MEMORY_ENGINE_REMOTE_LANE=allow` 的部署,还包括:① `MEMORY_ENGINE_BACKEND=pg|mysql` 的 DB 记忆面(它不受那个旗管辖——旗只管 file 引擎,库是持久真身,任何车道上都照常点亮)= 最常见的云形;② `REMOTE_EXEC=host` + file 引擎(文件平面就在本机,但 core 的执行环境判决仍是 remote)。若该车道确实够得着记忆根,`MEMORY_PERSISTENCE_CAPABLE=true` 是官方恢复路径(启动日志 `memory_remote_lane_write_instruction_dropped` 会点名这一条);够不着就表 `false`,让只读状态对模型说明白。
467
488
  另:记忆**没有写工具**,模型是用普通文件工具往记忆根写的——所以记忆根必须落在任务的 fs 授权边界内。结构性够不着时启动会 warn 并给出三条改法(把根挪进 workspace / 经 `additionalDirectories` 授权 / 声明 `MEMORY_PERSISTENCE_CAPABLE=false` 让披露诚实)。⚠️ **「挪根」用哪个旋钮按记忆后端分腿**:file 引擎是 `MEMORY_ENGINE_DIR`;`MEMORY_ENGINE_BACKEND=pg|mysql` 的根是 `<数据根>/memory-work`(数据根 = `LOCAL_DATA_ROOT`/`AGENT_DATA_DIR` 族),那两条腿**不读** `MEMORY_ENGINE_DIR`——启动 warn 的文案自己会按腿指对旋钮。
489
+ ⚠️ **两条 SQL 记忆腿声明 core 的 `admitsLandings`(7.146.0 起)**:把它们交给 core 记忆引擎、自己驱动记忆整合的嵌入方,整合 apply 把写闸判决(`admit`)与整合计划的持有者核对(`confirm`)交给后端
490
+ (本服务自己的整合阀门 `MEMORY_CONSOLIDATION_DRIVER` 在这两条腿上照旧拒启,见下文该旋钮);后端在写任何一条之前按最终拼写(含撞名后缀、改名 / 移动留下的旧拼、删除移走的拼)一次问全,所有的问都在第一笔写之前(拒 ⇒ 一字不写;最多 4 次 —— 轮数上限与超限的码是引擎的问询圈,与 file 腿同一只 —— 仍在动 ⇒ `memory.landing_unstable`,一字未写);开始写之后落点才被别的副本变成没问过的拼写的那一条报冲突不写、下次 apply 再问;`confirm` 在每条写语句之前同步调。
491
+ **已知限制**:SQL 记忆腿的 confirm 在写语句前同步调,不持事务锁;要事务内验 fence 等包里的腿(7.64.0 核心存储包)。
492
+ 🔴 **会话的记忆写家(7.146.0 起,`GET /v1/sessions/:id/memory-status` 的可选键 `writeHome` / `planes`)**:会话最近写入的写域的 canonical 绝对目录 —— 引擎给的、字节原样(壳不要自拼:名字带哈希后缀;本服务不拼不折不择一)。`writeHome` 是**店绑定**的键:带 `?scope=<层键>` 时是那一层住的店的引擎答案、顶层 `writeHome`;不带 `?scope=` 时顶层 `writeHome` 是引擎在店之间择好的会话级一个家(点名家的店里最近写入的那只的家;任一店的写家读不出、两店同一时刻点名、或没有店点名 ⇒ 缺席,引擎不铸替身;单店部署 = 那只店自己的),逐店明细里每只店引擎答的值原样在 `planes: [{ plane, writeHome? }]`(每只店一行)。**只在单用户 `host` 车道上来**(= `GET /v1/capabilities` 的 `projectContext` 为真:手在本 worker 的盘上、部署只有一个主人);托管 / 沙箱 / 设备 / 无手车道两键都不来(那是 worker 宿主的路径,不是调用方的文件系统)。缺席的成因不止一个:部署不兑现(`projectContext` 为假)、引擎不答(没有源命名写域 / 源读不出 / 记忆库布局还不能派生家,此时行里没有 `writeHome`)、不带层座时引擎不择(另一只店的写家读不出 / 两店同刻)—— 都不是「老 server」。
468
493
  - **记忆店根搬了家 ⇒ 拒启(7.126.0 / 引擎 7.49.0 起,file 记忆后端)**:店根的物理路径记在店的控制面里(`root-anchor.json`),每次打开都比对。目录被搬走、
469
494
  或换成指向别处的链接之后,起服**拒**(码 `memory.target_outside_store`;不降级成「记忆暗」—— 店还在,只是搬了家,静默降级会让这台部署在没人知道的情况下丢掉全部记忆)。
470
495
  拒句点名控制面记下的路径与现在解析到的路径,并给出两条出路:**① 把目录放回原处**;**② 在本服务的目录里跑一次重锚**:
@@ -607,9 +632,10 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
607
632
  新身份的桶里还没有这一代)。在那只身份的桶里确认即缺席。**没有耐久声明的部署恒缺席**:那里「重启靠中心重拉」是部署形本身(配置时声明的事实),不是事故;没接中心 / 本地腿
608
633
  也缺席。`version` = 应用着的那一代,`since` = 这一代开始没有确认接力的时刻。与 `restart.withheld`(要重启但扣着)是两个事实两张脸,可同在。7.120.x 的 `modelPlaneDeferred` 已删。
609
634
  - **自报位(7.121.0+)**:`/health` 恒带三键 `auth` / `operator` / `center`(缺席 = 老引擎),给壳在**带凭据之前**判出路用,
610
- 全部是装配事实的投影、不是另立的规则:`auth = { required, kinds, identityAttach }` —— `required` = 全局凭据门武装(配了
635
+ 全部是装配事实的投影、不是另立的规则:`auth = { required, kinds, identityAttach, serviceTokenActsForDeployment }` —— `required` = 全局凭据门武装(配了
611
636
  `SERVICE_AUTH_TOKEN(S)`),`kinds` ⊆ `service_token` / `sso`(装了鉴权桥:登记中心的登录令牌)/ `direct_door_jwt`(直连门),
612
- `identityAttach` = `available`(kinds 里有 `sso` 或 `direct_door_jwt`)/ `unavailable`;`operator` = `present`(`OPERATOR_PRINCIPALS`
637
+ `identityAttach` = `available`(kinds 里有 `sso` 或 `direct_door_jwt`)/ `unavailable`;`serviceTokenActsForDeployment`(**7.146.0+**,恒在场)=
638
+ 不带主体头的 service token 在这台部署上代不代部署本人 = `required` ∧ `SERVICE_TOKEN_ACTS_FOR_DEPLOYMENT`(缺省 `true`;没配 service token ⇒ `false`;见 §「不报主体的 service token 是谁」);`operator` = `present`(`OPERATOR_PRINCIPALS`
613
639
  非空,或单用户声明 `OPERATOR_SINGLE_USER=true`)/ `absent`;`center = { issuer?, webBaseUrl?, sessionsVisible }` —— `issuer` = 鉴权桥的 issuer(`AUTH_BRIDGE_ISSUER`,缺席回退
614
640
  `SEMA_WEB_ADMIN_URL`;没有桥 ⇒ 缺席),`webBaseUrl` = `SEMA_WEB_ADMIN_WEB_URL` 原样(缺席即缺席),`sessionsVisible` = `true` /
615
641
  `false` / `"unknown"`:中心车道配齐 ∧ `FLEET_ADVERTISE_ADDRESS` 在 ∧ 鉴权桥在 ∧ 会话枚举面在,缺一 ⇒ `false`;齐了而多租户 ⇒ `true`,
@@ -652,7 +678,7 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
652
678
  行与编号高水位一并抹掉(同 anchors/附件/快照的级联)——会话 id 可被重新认领,残留就等于把上一位
653
679
  主人的任务标题与编号进度交给下一个化身。
654
680
  - **会话的行事实只住在数据根的 `session-incarnations.json`**(每只会话一格:属主 / 化身 / 执行放置 / 建档时刻 / fork 谱系);`sessions/*.jsonl` 是匿名的对话内容。
655
- 备份 / 迁数据根时两样一起搬;更早版本写下的带属主戳的存档由起服一次性迁移改写(改不了的点名,fork 与改日志推送答 409 `conflict.session_archive_stamped`),快照目录 `session-archive-m33/` 非空且消化不了 ⇒ 拒启(MIGRATION 写了出路)。
681
+ 备份 / 迁数据根时两样一起搬;更早版本写下的带属主戳的存档由起服一次性迁移改写(改不了的点名,fork 与改日志推送答 409 `conflict.session_archive_stamped`;会话同步导入的第一阶段对 fork / stale 且没带同意的推送也直接答它),快照目录 `session-archive-m33/` 非空且消化不了 ⇒ 拒启(MIGRATION 写了出路)。
656
682
  `session-incarnations.json` 本构建读不出(更新的引擎写过、格里多出不认得的键;或文件坏了 / 读不了)⇒ 起服拒 `config.schema_mismatch`(与 SQL 后端列册对账同一个码,拒句点名文件与键;
657
683
  出路 = 部署写下它的那一版,或恢复升级前的数据根备份;读不了 ⇒ 修好权限 / 盘再起);起服之后才读不出(跳过了 `ensureSchema()` 的嵌入方;或违规共享的数据根上另一只引擎写了它)⇒ 同码拒,
658
684
  形读不出则那只边档的每一读每一写拒到重启,盘上字节不被盖掉。
@@ -696,6 +722,8 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
696
722
  「退回内存并 warn」降级臂(`store_db_unreachable_fallback_memory`),所以第二只实例此前**当场**降级、
697
723
  现在要**等满窗**才降级 —— 方向仍是好的(窗内拿到就用上真数据根,而不是静默丢掉持久面),但它是一次
698
724
  真实的启动时长变化。显式 `DB_BACKEND=local` 不走降级臂(拒启),`0` 关掉两形的等待。
725
+ 🔴 **新引擎的锁等待窗应 ≥ 前任的排空窗 + 15 s 终局 + 一拍**(前任的锁要到它收尾链尾部才还回来;前任最晚何时退出见 `DRAIN_GRACE_MS` 下面的「停机上界(契约句)」):
726
+ 壳 / 桌面按**自己的**排空窗给新引擎进程注入这个值 —— 那是配置渠道的本意;server 不改缺省(缺省 `60000`:改缺省会让所有部署的起服失败延后)。
699
727
  三问成文见 `docs/DEPLOY-PREREQS.md`。
700
728
  - **跳版升级的旧库(退役表 `snapshot_manifest` / 退役列 `checkpoint.pending_steer` 还在)起服即拒 `config.schema_mismatch`,而且拒启之前库里一张表都没建**:
701
729
  连上库后**先探这两件、再建表** —— 探针先于第一条 `CREATE`(两个 SQL 方言同一只判据),所以按拒句指路去 7.125.x 起服一次时,库还是你交给本版之前的样子,
@@ -749,7 +777,7 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
749
777
  (`e2b`/`k8s`/`local-docker`),那里的工作区是沙箱自己的,`cwd` 被忽略。
750
778
  - **读面(7.86.0 起)**:`GET /v1/capabilities` 的 `executionLane` 把本部署的车道广告成
751
779
  `{provider:<执行车道八词>, toolsOnThisHost:boolean}`。7.97.0 起 `REMOTE_EXEC` 未设 ⇒
752
- `provider:"in-process"`、`toolsOnThisHost:false`(本部署没有执行车道 = 无手;改前折报 `"host"`/`true`)。
780
+ `provider:"in-process"`、`toolsOnThisHost:false`(本部署没有执行车道 = 无手;改前折报 `"host"`/`true`;7.147.0 起这一形拒启、这个状态名改叫车道词 `none`,见上文硬断预告)。
753
781
  要本机有手,显式设 `REMOTE_EXEC=host`(sema-up 产物已显式写车道)。
754
782
  壳/客户端判「工具跑不跑在本机」**读这一位**,不要再从自己这一侧的 `REMOTE_EXEC` 猜 —— 连远端引擎时
755
783
  那个 env 说的是**壳自己这台**的事。⚠️ 它是**文件系统同一性**位,**不是**权限位:`cwd` 兑不兑现仍由
@@ -824,7 +852,7 @@ SEND_USER_FILE_SANDBOX_PUT_ENDPOINT=… # 可选:沙箱直传 PUT 的端
824
852
  - **🔴 云形(`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 默认无帽)。
825
853
  - **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` 拒启,拒因打印这张表缺的列的 `ALTER TABLE … ADD COLUMN …`,由部署者自己执行**(列集对账与拒启句只打印语句、永不改库;per-file rewind 历史照留,会话与其他持久面不动)。⚠️ 打印的语句取自建表语句原文:`publish_seq` 是 `NOT NULL` 且无缺省的列,在已有行的 PostgreSQL 表上那句原文会被引擎拒(列含空值)—— 这种存量表要部署者自己按 per-scope 的发布顺序补值后再改写执行;这是起服对账拒启句同一生成器的既有性质。
826
854
  - **会话同步重推(同图)要读 blob**(7.136.0 起):`POST /v1/sessions/:id/sync/import` 的 identical 重推臂,对云端「已握着同一张文件历史图」的目标,也先把信封引用的每一枚 blob 经云端自己的店读一遍、核哈希(不回写),再答成功 —— 对象存储里的对象若在图发布之后丢了,这一腿答 `422 missing_blob`(`PUT` 缺的 blob 再推;图不动),不再答 `200 identical`。代价:无变化会话的频繁重推多付对象存储读流量(每枚引用的对象一次下载,正比于引用的 blob 字节);没有旋钮。
827
- - **文件历史捕获总开关**(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)。
855
+ - **文件历史捕获总开关**(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 车道自 7.146.0 / 引擎 7.64.0 起也走整会话清理动词 —— 边界、版本记录、v1 基线与只被这只会话引用的 blob 一起删;此前 local 走 `reap`,v1 基线留存)。关着时 `FILE_HISTORY_RETENTION_KEEP` 没有效果(不再有边界提交,自剪不会触发),值仍照常校验。没有存储后端的部署(env-only worker)本来就没有历史店,旋钮在那里无事可关、不下发捕获位(引擎对「off 但没店」拒 prepare)。
828
856
  - **🔴 公桶策略必须 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)。
829
857
  - **两条轨**:ttl=0(默认)→ 匿名 GET 公桶下 `uuidv7/<name>` 不可猜 key,链接永久、可回收(删对象);0<ttl≤7 天 → 私桶 + SigV4 限时签名链接(7 天是 SigV4 物理上限,更久用 ttl=0)。
830
858
  - **执行 lane 与租户门**:e2b/k8s 沙箱 lane 任意租户可用(沙箱文件系统=租户边界,沙箱内 `curl -T` 直传、字节不中转、凭据不进沙箱);host/ssh lane 仅单用户部署(`REQUIRE_PRINCIPAL` 未开)时挂载。
@@ -1094,6 +1122,9 @@ UNREACHED_ASK_TTL_MS=3600000
1094
1122
  非终态的问行(`STREAM_PENDING` / `PARKING`)永不按龄删 —— 它们归对账器与起服收敛。节律:首拍即清扫一次,之后每小时至多一次;每次先从 run 账、后台子代账与 checkpoint 店列出还能动的东西(任一读不出 ⇒ 这次不删),
1095
1123
  问账一批至多 500 行、一次清扫删到不满一批为止。读数:起服一行 `approval_ask_retention`(`windowMs`;`0` = 关),真删了东西时一行 `approval_ask_retention_swept`
1096
1124
  (`cutoffMs` / `asks` / `batches` / `auditAsks` / `auditRecords`;半途失败时已经删掉的照样打,带 `incomplete: true`)+ 计数器 `approval_ask_retention_rows_deleted_total{ledger=asks|batches|audit}`。
1125
+ local 的问账日志上,任何真删了行的调用(留存清扫的两只动词与 `deleteByTask`)的那一拍,同步把整本原子重写成存活行的快照 —— `asks.jsonl` 里同拍不再有被删的问的字节(含 `Write` / `Edit` 这类工具实参的字面);没删东西的调用不落记录、不重写;
1126
+ 压实失败不影响调用(行照删,计 `fail_open_total{tag="server.approval-ask-journal.compaction-failed"}`,日志里留着删行记录,下一次写或下一次起服再压实)。
1127
+ 起服时因撕尾 / 坏行隔离出去的原件 `asks.jsonl.corrupt-<ts>` 是取证材料,**没有清扫腿**,里面仍有当时的字节,要清由人来(正常部署恒零)。
1097
1128
  **关法**:`APPROVAL_ASK_RETENTION_MS=0` = 永久保留(要在升级后的第一次清扫之前设 —— 首拍即扫);正值低于 1 天钳到 1 天并在启动告警里点名(刚结束的 run 的客户端还可能对它的卡重试 / 晚答,
1098
1129
  一天吸收这一段);负值 / 非整数 / 非数拒启。run 结束超过窗之后才对它的卡作答 ⇒ 决断口与流内审批口答 404。
1099
1130
 
@@ -1416,6 +1447,12 @@ TASK_WRITE_FACE_ESCALATION=allow # 词表 allow|refuse;坏词启动期响亮
1416
1447
  4. **请求体自带的技能**(`skills[]`):只补这一 run **没被占的名字** —— 与部署的技能同名的那条被丢(一行 `user_skills_dropped`),不覆盖;
1417
1448
  `disableSkills: true` ⇒ 这一 run 一只技能都不装(部署的、请求的都不装)。
1418
1449
 
1450
+ **把部署的场景面收窄到只剩自己的(部署自配,没有新语法)**:出厂技能默认进每个场景(`scenarios` 为空),内建的 `code-review` / `scan` 也一直在册。一台只想服务某个场景(例:`twin`)的部署这样收:
1451
+ - 技能:`SKILLS_DIR` 指向一个**空目录**(整替换,出厂技能清零;目录空 = 零出厂技能,不报错),再在 `config.d/skills.json`(或中心的 skills 域)里给自己的每条技能写 `"scenarios": ["twin"]` —— 它们只进 `twin` 这一个场景,不进内建场景。
1452
+ - 内建场景:`code-review` / `scan` 本身没有「隐藏」写法,但可以在 `config.d/scenarios.json`(或中心的 scenarios 域)里写同名条目 `{ "name": "code-review", "toolset": "none" }`,把它**遮蔽**成只有 `Now` 工具、不挂手的空场景
1453
+ (起服日志 `web_admin_scenarios` 的 `shadowsBuiltin` 点名被遮蔽的内建名);`"enabled": false` 的条目是**跳过**,不遮蔽 —— 内建场景照在。
1454
+ - 常驻面各有现成开关:`LSP_HOST_ENABLED=false` 关 host 车道的语言服务器,`SCHEDULER_ENABLED=false` 关自唤醒调度。
1455
+
1419
1456
  **看一个场景装着哪些技能(7.135.0 起)**:`GET /v1/capabilities/scenarios/<name>` 的 `skills` = 上面 1–3 叠出来、按这个场景过滤后的那一组(第 4 条请求自带的不在内 —— 那是调用方自己的),
1420
1457
  每条 `{ name, description, source }`:`source` 是 `"factory"`(第 1 条,本部署的技能目录)或 `"center"`(第 2、3 条,配置源交来的;盖掉出厂同名的也是 `"center"`),不带正文。
1421
1458
  与这个场景的 run 交给引擎的是**同一只过滤**:卡上列的就是本服务交给这一 run 的部署技能,点名(提交体 `userInvokedSkills`)用它的 `name`;没有技能 ⇒ `skills: []`。
@@ -1505,9 +1542,9 @@ A2A_SERVE_BLOCKING_WAIT_MS=45000 # blocking:true 的服务端等待
1505
1542
  本设计唯一点名禁止的事),只决定点名这条 skill 的请求跑哪个场景。
1506
1543
  - 两条路由的**鉴权位置刻意不同**:`GET /.well-known/agent-card.json` 在服务凭据门**外**(公网匿名可
1507
1544
  发现,零存量披露);`POST /v1/a2a` 在门**内**(与本仓其余 API 同一套凭据)。
1508
- - `message/send` / `tasks/get` 还需要 **durable run store**(`DB_BACKEND=mysql|pg`)——缺它时两个方法
1509
- 回具名 JSON-RPC `-32004`(卡照发)。开关打开时 boot 日志有一行 `a2a_serve_enabled`,里面的
1510
- `tasksUsable` 就是这件事。
1545
+ - `message/send` / `tasks/get` 都站在 **durable run store** 上(能力位 `a2aServe` 与这条是同一只判据)—— 缺它时这台 worker 整个**不作为 A2A agent 对外存在**:
1546
+ 卡与 `POST /v1/a2a` 一起 404(7.116.0 起;此前卡照发、两个方法回 JSON-RPC `-32004`,现在不再出现这个码)。开关打开时 boot 日志有一行 info `a2a_serve_enabled`
1547
+ (键:`card` · `rpc` · `advertisedUrl` · `skills` · `blockingWaitMs`);开关打开而没有 run store 时改打一行 warn `a2a_serve_not_served`(`reason` + `detail`,`detail` 点名出路)。
1511
1548
  - 完整 wire 契约(方法表、错误码全表、Task 形与任务态投影、反枚举语义、版本槽)见
1512
1549
  [`docs/ASSISTANT-WIRE-CONTRACT.md` 附录 C](docs/ASSISTANT-WIRE-CONTRACT.md)。
1513
1550
 
@@ -1551,10 +1588,19 @@ ENGINE_ORPHAN_LINGER_MS=2000 # 可选,默认 2s:成孤儿之后还要静多久
1551
1588
  取值(半夜一次滚动重启,宁可等长一点也不要砍掉一条在跑的 turn)。而**壳自 spawn 的引擎会被壳注入更短
1552
1589
  的窗**:sema-cli 在装子进程 env 时把它填成一个分钟级的值(2026-09-22 亲读壳侧实现:`60000`;那是**壳的**
1553
1590
  常量,以你手上那版壳为准 —— 单机形下这个窗直接等于"退出后多久不能重开",见上一条的撞锁退避),
1554
- **用户/运维显式设的值原样尊重、壳不覆盖**。⇒ 排壳形的关机/重开预算时,以**进程实际
1555
- 生效值**为准:`GET /v1/config/catalog` 里 `DRAIN_GRACE_MS` 那一行的 `effectiveValue` 就是本进程真在用的
1556
- 数(它读的是活 config,不是这份文档里的默认值),启动期的夹取/警告行同源。本文档其余各处谈这条排空腿
1557
- 时都不带数字,数字只有这一处 —— 照抄默认值去算壳形预算会差一个数量级。
1591
+ **用户/运维显式设的值原样尊重、壳不覆盖**。⇒ 排壳形的关机/重开预算时,窗 g 取**壳自己注入的那个值**
1592
+ (壳读不到运维面的配置目录:`GET /v1/config/catalog` 是 operator-only,没有运维名册时答 `501`,不是 `403`);
1593
+ 一次排空**开始之后**,本次排空定死的到期点在 `GET /health` 的 `engineSignal.draining.deadlineAt`
1594
+ (= 进入排空那一拍 + 窗;之后热改排空窗不动它;排空前这一格不在场)。启动期的夹取/警告行报的是夹取后真在用的数。
1595
+ 本文档其余各处谈这条排空腿时都不带数字,数字只有这一处 —— 照抄默认值去算壳形预算会差一个数量级。
1596
+ **从停止信号到进程退出的上界**见下一条「停机上界(契约句)」。
1597
+ - **停机上界(契约句)**:从收到停止信号到进程退出,**单发 `SIGTERM`** 的上界 = `engineSignal.draining.deadlineAt`(信号进的排空 = 第一发被处理那一拍 + 排空窗,入口定死)
1598
+ + ≤ 1 s(排空驱动一拍)+ ≤ 15 s 终局;**两发形**(第二发与第一发相隔 g ≥ `DRAIN_SECOND_SIGNAL_MIN_GAP_MS`,缺省 250 ms;t0 = 第一发被处理的那一拍)= t0 + g + ≤ 15 s。
1599
+ 硬停里 10 s 在飞宽限在 15 s 之内,不相加(宽限 = 强断残余连接、审批面收尾、检查点店收尾共读的那一个窗;终局闸从硬停开始那一拍起算,到点无条件退出)。
1600
+ 卡上要写「最多等多久」的壳,诚实的上界是它自己的梯:窗 g + 它愿意给的宽限 H(到 H 壳自己 `SIGKILL`);H 取 ≥ 16 s(= 15 s 终局 + ≤ 1 s 一拍),否则会在 server 还没到自己的终局闸时把它杀掉。以上是按定时器算的名义上界(排空窗与终局闸;不含进程被同步工作卡住的时间),真正强制的上界是壳自己梯子里的 `SIGKILL`。
1601
+ 排空事实里的动作号(`GET /health` 的 `engineSignal.draining.actionId`):信号致排空的**闭集** = `signal:SIGTERM` / `signal:SIGHUP`;
1602
+ 经运维排空口 `POST /v1/admin/drain` 进的排空 = 该请求自带的 id;硬停(未经排空)已在进行时,排空口回执里的动作号 = 固定的 `hard-stop`。
1603
+ 另两类引擎自铸的值:附着租约自退 `self-exit:attachment`、服务租约被拒 `lease:<代际>`。
1558
1604
  - **排空期的第二发信号(`DRAIN_SECOND_SIGNAL_MIN_GAP_MS`,缺省 `250`,7.102.0+)**:排空中再来一发 `SIGTERM` /
1559
1605
  `SIGINT`,只有与**本次排空的第一发停止信号**相隔 ≥ 这个毫秒数才算「别等了,硬停」;更近的是同一拍被投递了两遍(壳心跳误判、
1560
1606
  编排器重试),打一行 `drain_duplicate_signal_ignored`(带 `signal` / `sinceFirstMs` / `minGapMs`)后照常排空。SIGTERM 进的排空,
@@ -1563,15 +1609,19 @@ ENGINE_ORPHAN_LINGER_MS=2000 # 可选,默认 2s:成孤儿之后还要静多久
1563
1609
  修前那一发被读成第二发)。k8s 例:`preStop` 调排空口 → kubelet 发一发 SIGTERM(吸收)→ `terminationGracePeriodSeconds` 到点 SIGKILL。
1564
1610
  真要立刻停:隔过这个间隔再发一次,或 `SIGKILL`;设 `0` 回到「每个第二发都硬停」的旧行为。合法域 `[0, 10000]` 整数毫秒
1565
1611
  (上界 = `DRAIN_GRACE_MS` 的地板),坏值拒启。不在排空时 `SIGINT` 仍是立即硬停。
1566
- - **自退留痕 `<数据根>/exit-last.json`**:自退在 **drain 开始那一刻**就把判据写进数据根(`orphan` /
1567
- `reason` / `idleForMs` / `attached` / `inflight` / `lingerMs` + `version` / `at` / `pid`)。壳的 exit
1612
+ - **自退留痕 `<数据根>/exit-last.json`**:自退在 **drain 开始那一刻**就把判据写进数据根(`lane` /
1613
+ `reason` / `idleForMs` / `attached` / `inflight` / `lingerMs`,孤儿还带 `orphanedForMs` + `format` / `version` / `at` / `pid`)。壳的 exit
1568
1614
  watch 往往在流断之后才跑,只看退出码分不清"它自己退的"与"它崩了";读到这个文件就能把这个 pid 归入
1569
1615
  **预期退出**。退出码仍是 **0**,不另立专用码。引擎下次在同一数据根启动时读到它 ⇒ 一行 `exit_last_found`
1570
1616
  然后**删掉**(在场 ⇔ **上一次**退出是自退;它与 `crash-last.json` 是对偶的两只判别位)。
1617
+ 文件带格式号 `format`(当前为 `3`):读到别的格式号(缺键 = 7.145.x 及以前写的)⇒ 一行 **`warn`** `exit_last_stale_format`
1618
+ (`fileFormat` / `buildFormat`)后删掉,**不**投影成 `exit_last_found` —— 只丢那一次自退的留痕,不迁移、不挡启动。
1619
+ 格式 3 的 `lane` 是自退那一拍的车道(`"handover"` / `"orphan"` / `"awaitingFirstAttach"` / `"owned"`,见下一节「交接车道」):7.145.x 的 `orphan:true` ⟺ `lane:"orphan"`;
1620
+ 候首触窗到期 ⟺ `lane:"awaitingFirstAttach"`;`reason !== "idle"` ⟺ `lane === "orphan"`。
1571
1621
  - **拍频不开旋钮**(2s 写死)。有真需求再议 —— 这里刻意不增殖一个只有一个人会调的键。
1572
1622
  - 日志:装配时一行 `parent_watch_armed`(`info`,带 `pid` / `ppidAtArm` / `intervalMs`),成孤儿时一行
1573
1623
  `parent_watch_orphaned`(**`warn`** —— 非计划内的生命周期事件,带 `reason: "reparented" | "esrch"`),
1574
- 真退的时候才是 `engine_lease_auto_exit`(带 `orphan: true` + 同一个 `reason`)+ `draining_started`。
1624
+ 真退的时候才是 `engine_lease_auto_exit`(带 `lane: "orphan"` + 同一个 `reason`)+ `draining_started`。
1575
1625
  - 坏值(`0` / 负数 / 小数 / 非数字 / 超过 `2147483647`)**拒启并指路**:`0` 和负数在 `process.kill` 里的
1576
1626
  语义是**进程组**,超出 int32 的值 `process.kill` 根本收不下(会变成"armed 了但永远探不动")。要关掉就
1577
1627
  **不设**这个键。探活若给出既不是 `ESRCH` 也不是 `EPERM` 的 errno,会打一条(仅一条)**`warn` 档**
@@ -1593,6 +1643,8 @@ peer 还在用。这条腿问的是那个真正的问题 —— **还有没有
1593
1643
  ENGINE_AUTO_EXIT=true # 壳 spawn 引擎时注入;服务器部署**不要**设
1594
1644
  ENGINE_LINGER_MS=60000 # 可选,默认 60s,下界 1000(父还活着时的窗)
1595
1645
  ENGINE_ORPHAN_LINGER_MS=2000 # 可选,默认 2s(父没了之后的窗;见上一节)
1646
+ ENGINE_FIRST_ATTACH_MS=180000 # 可选,默认 180s(7.146.0 起;还没有过任何非探针请求时的窗,取 max(本值, ENGINE_LINGER_MS);0 = 不给额外宽限)
1647
+ ENGINE_HANDOVER_LINGER_MS=1800000 # 可选,默认 30 min,下界 60000(壳经 POST /v1/engine/handover 交接之后的窗;见下文「交接车道」)
1596
1648
  ```
1597
1649
 
1598
1650
  - **默认 `false` = 不自退**,存量部署逐字零变化。一台 k8s 上的 replica 半夜没人用是**正常低谷**,
@@ -1600,16 +1652,41 @@ ENGINE_ORPHAN_LINGER_MS=2000 # 可选,默认 2s(父没了之后的窗;见上一
1600
1652
  🔴 自退腿在「本键为真 **或** `SEMA_PARENT_PID` 在场」时装配(两件并成一件)。本键关着而
1601
1653
  `SEMA_PARENT_PID` 设了 ⇒ 父还活着期间**永不自退**(窗 = ∞,与以前逐字同),父没了才用孤儿窗。
1602
1654
  两键都缺席 ⇒ 整件不装配,零定时器。
1603
- - **自退判据 = 三合取,且必须连续满**「当前窗」(父还活着 = `ENGINE_LINGER_MS`;父没了 =
1604
- `ENGINE_ORPHAN_LINGER_MS`。判据一条,只有窗长两态):① 没有附着的 SSE 流;② 没有在飞的 run
1605
- (与 `/health` 的 `inflight` **同一个读数**);③ 没有非探针 HTTP 触点(`/health`、`/readyz`、`/metrics*` 不算 ——
1655
+ - **自退判据 = 三合取,且必须连续满**「当前窗」(窗长由车道定,从上到下第一条成立者:交接 = `ENGINE_HANDOVER_LINGER_MS`;父没了 =
1656
+ `ENGINE_ORPHAN_LINGER_MS`;还没被碰过 = `max(ENGINE_LINGER_MS, ENGINE_FIRST_ATTACH_MS)`;其余(父还活着、已被碰过)= `ENGINE_LINGER_MS`;孤儿窗见上一节,「首次附着前的宽限」与「交接车道」见下面各自一条。判据一条,窗长按车道取):① 没有附着的 SSE 流;② 没有在飞的 run
1657
+ (读的是**阻断集** `http + leader`,与排空等的、SIGHUP 空闲窗同一个读数;`/health` 的 `inflight` 是它再加会话驻留的后台 shell / monitor 的**总数**,见下一条);③ 没有非探针 HTTP 触点(`/health`、`/readyz`、`/metrics*` 不算 ——
1606
1658
  探针不是「有人在用」),**且**最后一条附着流断开也已满窗。任何一条不满足即把计时**清零**。
1607
1659
  (最后那半句不是修辞:一条活得比 15s 拍频还短的流整个生命周期都落在两拍之间,自查腿从没看见过它 ——
1608
1660
  只有在**关闭点**记一笔离场时刻,「已经没人附着满 N 秒」才是精确的。)
1661
+ - **`/health.inflight` 的两个事实(7.146.0 起)**:模型在会话里 `run_in_background` 起的 shell / monitor 活过起它的那一条腿 —— 此前 `/health.inflight` 对它们读 0,读 `/health` 判「引擎是否空闲」的判官只好另拼谓词。
1662
+ 现在 `/health.inflight` 是**总数**:这台引擎此刻托管着多少活,把仍在跑的、会话驻留的、**没声明 retain** 的后台 shell / monitor 一并数进去;分项在 `/health.inflightBreakdown { http, leader, sessionResident }`,`inflight` 恒等于三项之和。
1663
+ **判停读的是另一个和 —— 阻断集 `http + leader`**(还有没有必须等完的活):自退、排空、SIGHUP 空闲窗都读它,驻留 shell 只报、不阻断,所以**判停行为与 7.145.x 逐字同**
1664
+ (壳没了且没人附着时引擎照退,驻留进程随受控退被收掉;一只不退出的 `while true` 不会让孤儿引擎永生,也不会让排空等满窗)。
1665
+ **谁受影响**:把 `inflight` 当「HTTP 腿数」读的监控面(数值会比 7.145.x 大,原口径 = `inflight − inflightBreakdown.sessionResident` = `http + leader`)。**补偿**:分项键;要不要因为引擎托管着驻留 shell 而推迟换代 / 回收由判官自己定。
1666
+ 读数只会少数、不会多数:枚举的会话集是本进程提交过的会话(LRU 上界 4096、删会话即清),被挤出的会话、不经提交链的内部车道起的驻留行、删会话级联中途失败的会话不可见。
1667
+ 某个会话的驻留行读失败时按 1 只在跑计(读不出 ⇒ 非空闲)并记一行 `session_resident_background_unreadable`(每会话一次),读口本身不抛。
1609
1668
  - **「run 在跑就不退」是结构性保护,没有旋钮可以关掉它** —— 最后一个壳关掉、后台 run 还在烧的场景里,
1610
1669
  引擎必须活到那条 run 落地。
1611
1670
  - **`ENGINE_LINGER_MS` 防的是壳重启窗**:壳崩了/重启的 1-3s 里附着数确实是 0,窗太短就在用户眼皮底下
1612
1671
  把引擎杀了。窗内任何一次重连即清零重新起算。
1672
+ - **首次附着前的宽限(`ENGINE_FIRST_ATTACH_MS`,7.146.0 起)**:引擎起服以来**还没有过任何非探针 HTTP 请求**(打开 SSE 流算;
1673
+ `/health` `/readyz` `/metrics*` 不算)时,「当前窗」是 `max(ENGINE_LINGER_MS, ENGINE_FIRST_ATTACH_MS)`(缺省 180s);首个非探针请求一到就回到
1674
+ `ENGINE_LINGER_MS`,**不再回来**;父没了照旧是孤儿窗(孤儿优先)。防的是一次性 `-p` 壳在 spawn 与第一个真请求之间被饿住(高载宿主 / CI)
1675
+ 超过 60s —— 以前引擎会在从没被碰过的时候就退掉,壳醒来只剩起步失败。在飞的 run 本身不算首个请求(它在跑时本来就不退,跑完仍按这个窗计)。
1676
+ 判据与采样拍都没变,只是窗长多了一态(「还没被碰过」;连同下一条的交接车道,窗长共四条车道:交接 → 孤儿 → 还没被碰过 → 已被碰过)。**代价**:spawn 之后从没被碰过、父却一直活着的引擎要多占
1677
+ 端口 / 数据根锁 / 库连接到 180s;`ENGINE_FIRST_ATTACH_MS=0`(或任何不大于 `ENGINE_LINGER_MS` 的值)逐字回到以前的行为。
1678
+ `ENGINE_AUTO_EXIT` 关着时这一态与父活着的窗一样「永不」。
1679
+ - **交接车道(「Move to background and exit」;两口 `POST /v1/engine/handover` / `POST /v1/engine/attach`,能力位 `engine.handover`)**:
1680
+ 壳退出时想让后台 shell 接着跑完,就先 `POST /v1/engine/handover`,体恰为 `{ "reason": "background_exit" }`,然后自己退出、不停引擎;下一只壳起来时
1681
+ `POST /v1/engine/attach`(体 `{}` 或空)收回。附着租约因此有四条车道,一只派生函数,从上到下第一条成立者:**交接**(调过交接口、之后还没有 attach)>
1682
+ **孤儿**(父没了)> **还没被碰过** > **已被碰过**。交接车道的窗是 `ENGINE_HANDOVER_LINGER_MS`(缺省 30 min),**且只在这条车道里**引擎此刻一退就会被退出边界硬收的
1683
+ 后台 shell(在跑、整张注册表、不含 retain 行)算工作 —— 后台 `sleep 2400` 在跑时引擎不退,跑完再静默满 30 min 才退;别的车道照旧(后台 shell 不算)。判据、采样拍、出口(同一条排空腿 + 留痕)都只有一份。
1684
+ 这只数与 `/health.inflightBreakdown.sessionResident` 是两问:后者只数经过提交的会话的会话驻留行,交接数还含 run 腿上的行与没经过提交的会话的行,读得出时 ≥ 驻留项;
1685
+ 读不出(某行 env 的 retain 声明读抛)⇒ 按 1 计(不退)、一行 `warn` `engine_handover_shell_count_unreadable`(每次起服至多一行)。
1686
+ 交接口幂等(再调回同一个 `since`);attach 在不交接时照回此刻车道。交接位只由这两口改:壳退出、父死、首触都不动它。终端关掉的 SIGHUP 在交接车道里**不计时**
1687
+ (寿命只归附着租约;下一只壳 attach 之后 SIGHUP 的空闲窗照旧)。两口不可用 ⇒ 501 `capability.engine_handover_unavailable`:自退腿没装(两键都没设)、
1688
+ `REQUIRE_PRINCIPAL=true`,或正在停机;体不合形 ⇒ 400 `request.field_invalid`,什么都不改。**代价**:交接之后引擎多占端口 /
1689
+ 数据根锁 / 库连接到后台 shell 跑完再加交接窗;要短就调 `ENGINE_HANDOVER_LINGER_MS`(下界 60s —— 比壳重启窗还短的交接窗等于选了停引擎),要立刻停就 `SIGTERM`。
1613
1690
  - **实际等待 = `[窗, 窗 + 一拍)`**:最后一条租约灭在两拍之间时要到下一拍才被看见。方向是**保守**的
1614
1691
  (只会等更久)。**拍频由窗推导、不是第二个旋钮**:`min(15s, 生效得到的每一个窗)` —— 没装父监视的
1615
1692
  服务器形仍是 15s 一拍(一拍都没多跳),装了父监视的壳形按 2s 采样(否则「孤儿 2s 内退」这句承诺会被
@@ -1617,18 +1694,31 @@ ENGINE_ORPHAN_LINGER_MS=2000 # 可选,默认 2s(父没了之后的窗;见上一
1617
1694
  - 判死后走的是**和 `SIGTERM` 逐字同一条**优雅排空腿:拒新提交(503 + `Retry-After`)→ 候在途腿跑完 →
1618
1695
  停机。in-flight 的 run 照常结算 / park,**不是**裸退出;而且因为是受控退,`crash-last.json` 会被清掉
1619
1696
  ⇒ 下次启动**不会**把这次自退误读成崩溃。
1620
- - 日志:装配时一行 `engine_lease_armed`(带 `lingerMs` / `orphanLingerMs` / `tickMs`;`lingerMs: null`
1621
- = 这一档窗永不到期),自退时一行 `engine_lease_auto_exit`(带 `orphan` / `reason` / `attached` /
1697
+ - 日志:装配时一行 `engine_lease_armed`(带 `lingerMs` / `orphanLingerMs` / `firstAttachMs`(旋钮值)/ `firstAttachWindowMs`(生效值,
1698
+ 两者不同 = 旋钮被 `ENGINE_LINGER_MS` 托到了它)/ `handoverLingerMs`(`null` = 没装交接车道)/ `tickMs`;`lingerMs: null` / `firstAttachWindowMs: null`
1699
+ = 这一档窗永不到期),自退时一行 `engine_lease_auto_exit`(带 `lane`(那一拍的车道)/ `reason` / `attached` /
1622
1700
  `inflight` / `idleForMs` / `lingerMs`,孤儿还带 `orphanedForMs`),随后就是那条 `draining_started`。
1623
- `reason` 是闭集:`"idle"`(没人用了)/ `"reparented"` / `"esrch"`(后两者 = 父没了那一路,`orphan:true`)。
1701
+ `reason` 是闭集:`"idle"`(没人用了)/ `"reparented"` / `"esrch"`(后两者 = 父没了那一路,`lane:"orphan"`)。
1702
+ 车道经两口变化时一行 `engine_lane_changed`(带 `from` / `to` / `reason`(`"handover_requested"` | `"attached"`)/ `since`)。SIGHUP 空闲窗起表时那一行
1703
+ `sighup_idle_watch_armed` 带 `work`(此刻有没有工作,与附着租约同一只判据)/ `idleGraceMs`。
1624
1704
  同一份读数会写进 `<数据根>/exit-last.json`(见上一节)。
1625
1705
  - **租约窗可读(`GET /health` 的 `engineLease`,7.141.0 起)**:装了自退腿的引擎在 `/health`(无鉴权)上多一个对象键 `engineLease: { lingerMs, tickMs }` —— `lingerMs` = 自退腿会执行的**最短**静默窗(`ENGINE_LINGER_MS` 与 `ENGINE_ORPHAN_LINGER_MS`〔装了父监视才有〕取小),`tickMs` = 采样拍(自退发生在**生效窗**满之后、至多再晚一拍;父还活着时生效的是较长的父活着的窗,所以实际自退可以比 `lingerMs` 晚得多 —— `lingerMs` 是安全节拍的上界(触点比它密,在任何状态下都先于自退),不是自退时刻的预言)。壳持租约的触点节拍据此定:比 `lingerMs` 密,在任何状态下都留得住引擎(附着着 SSE 流时本来就留得住,约束的是没有 SSE 的纯轮询壳),不必再按交给 spawn 的那份推 —— 别处起的短窗引擎、父死之后只剩孤儿窗的引擎也读得到。**取最短、不取「此刻生效」**:此刻生效的窗在父死那一拍从长窗掉到孤儿窗,按旧读数定节拍的壳会在下一次读 `/health` 之前被拆台。两个数与上一条 `engine_lease_armed` 日志是同一组窗。没装自退腿(两键都没设)、或腿已因排空 / 自退触发而收摊 ⇒ 键缺席(不补 0)。
1706
+ **7.146.0 起同一格多一个 `firstAttach: { windowMs, since, attachedAt }`**:`windowMs` = 生效的首次附着窗(`max(ENGINE_LINGER_MS, ENGINE_FIRST_ATTACH_MS)`,
1707
+ 与 `engine_lease_armed.firstAttachWindowMs` 同数),`since` = 窗开始(装配那一拍,epoch ms),`attachedAt` = 首个非探针请求的时刻(epoch ms;`null` = 还没有过,
1708
+ 非 `null` ⇒ 这一窗已永久关闭)。父一直活着且一直没人碰时,`since + windowMs` 是「最早何时自退」的**下界**,不是预言(父死之后孤儿窗优先、可以更早退 —— 那一态看同一格的 `lingerMs`;期间的在飞会让它后移,另有至多一拍滞后;
1709
+ 两个时刻是装配那一拍的挂钟锚加单调差,系统改钟之后会与真实期限漂开)。在场 ⇔ `engineLease` 在场 ∧ `ENGINE_AUTO_EXIT` 开;缺席 = 老 worker
1710
+ 或只靠父监视(那一态永不到期)。一次性 `-p` 的壳据它处理起步段。
1711
+ 同一格还有 `lane`(此刻的车道,`engineLease` 在场即在场)与 `handover: { since, reason, lingerMs, backgroundShells }`(只在交接车道在场:
1712
+ 进入时刻、缘由、交接窗、引擎此刻一退就会被硬收的后台 shell 数;在飞数读同一份应答的 `inflightBreakdown`);装了交接车道时最短窗 `lingerMs` 把交接窗算进去(缺省 30 min,不改默认读数)。
1626
1713
  - **坏值拒启**:`ENGINE_LINGER_MS` 必须是落在 **`[1000, 86400000]`(1s…24h)** 的整数毫秒,**两端都承重**:
1627
1714
  下界防 `ENGINE_LINGER_MS=60`(以为单位是秒)—— 60ms 的窗等于把整条防误杀腿静默摘掉;上界防 `1e100`
1628
1715
  这类值 —— 它能过朴素的「是正整数吗」检查,而计时**永远追不上它**,于是配置看着好好的、自退其实整件
1629
1716
  失效。两端一律**拒启并指路**而不是夹取。本键**没有**「关掉」的取值 —— 关自退的唯一开关是
1630
1717
  `ENGINE_AUTO_EXIT=false`。校验**不看** `ENGINE_AUTO_EXIT` 的表态:否则手滑值可以在一台今天关着自退的
1631
1718
  机器上一直躺着,等哪天有人打开时当场生效。
1719
+ `ENGINE_FIRST_ATTACH_MS` 与它**同一只校验器、同一对上下界**,只多放行字面 `0`(= 不给额外宽限,生效窗落成 `ENGINE_LINGER_MS`,不会更早退;`-0` / `0.0` / `1e-400` 这类零形照常拒);
1720
+ `1..999`(秒当毫秒)与其余坏值同样拒启,同样不看 `ENGINE_AUTO_EXIT` / `SEMA_PARENT_PID` 的表态。
1721
+ `ENGINE_HANDOVER_LINGER_MS` 同一只校验器、同一个上界,下界 `60000`(没有「关」的取值;不交接的壳从不用它),解析同样无条件。
1632
1722
  - 🔴 **已知收窄(成文,现版不收)**:**parked-待赎回** 的 run 不在判据里,只看 running。数 parked 要发
1633
1723
  SQL,而这是一只每 15s 无限期跑下去的定时器 —— 不值得为它引入周期性 SQL 轮询。parked 行是 **durable**
1634
1724
  的,引擎退掉不丢:下次起来(壳的下一次 spawn)仍可赎回,伤害面是**赎回被推迟**而不是丢失。
@@ -1724,7 +1814,7 @@ invalid_using_default` 警告 + 退回缺省值」这条臂随词表放宽一并
1724
1814
  继承整份用户环境(CC 交互形,用户自己的凭据形变量在内)—— 🔴 **但不含本服务自己的凭据**(7.129.0 起,引擎 7.51.1):配置目录全部 `type:"secret"` 名
1725
1815
  (`SERVICE_AUTH_TOKEN(S)` / `APPROVAL_HMAC_KEYS` / `MODEL_API_KEY` …)与名单型旋钮名起服已向引擎声明为本进程自有凭据,生效配置里每条模型条目的 `apiKeyEnv` 也在名单里
1726
1816
  (中心热应用新增 / 改名条目时在模型目录换代前重声明,名单只随落地的世代增长 —— 改名后旧名照扣到重启,没落地的候选不留痕;条目的 `apiKeyEnv` 是引擎扣不住的名,或它点到的名正被某只名单型旋钮交给沙箱 / 语言服务器(见下文沙箱名单段)⇒ 该候选整拒),`"all"` = 整份环境**减**它们,任何继承词都不把它们
1727
- 交给模型 shell(能力位 `inherit_env.own_credentials_withheld` = 最近一次声明 ⊇ 当前生效配置应声明的集合;真要让模型 shell 持某枚令牌 ⇒ 另放一个不在配置目录、也不是任何模型条目 `apiKeyEnv` 的名,或经 `settings.env` 显式授予);这些变量此刻的**值**也交给引擎按值扣(同值换名的副本同样进不了模型 shell),引擎判某个值「不是凭据值」(空白 / 占位词 / 一个字符重复 / 不足 8 字符)⇒
1817
+ 交给模型 shell(能力位 `inherit_env.own_credentials_withheld` = 最近一次声明 ⊇ 当前生效配置应声明的集合;真要让模型 shell 持某枚令牌 ⇒ 放在引擎进程环境里一个不在配置目录、也不是任何模型条目 `apiKeyEnv` 的名下,配 `inheritEnv:"all"`;7.146.0 起请求 `settings.env` 不收凭据,见 9.5j);这些变量此刻的**值**也交给引擎按值扣(同值换名的副本同样进不了模型 shell),引擎判某个值「不是凭据值」(空白 / 占位词 / 一个字符重复 / 不足 8 字符)⇒
1728
1818
  那一个值不按值扣(名照扣、不拒启,能力位 `inherit_env.own_credential_values_withheld` 答 false),起服与模型目录换代时每个这样的变量出**一条** warn 结构化行 `own_credential_value_not_withheld { name, reason }`
1729
1819
  (`reason` 是引擎的拒句原文,从不回显值;起服第一步 logger 还没建时,这一行当拍以同一行形写 stderr,不缓冲;logger 接上之后——模型目录换代并进的条目的值被拒——这一行受 `LOG_LEVEL` 约束,`error` 级读不到,机读事实读上面那个能力位)—— 按事件名告警,给该变量一个真值即可。值是**网关密钥**(`MODEL_API_KEY` / `ANTHROPIC_API_KEY`,起服交给模型适配器的那一个)时,进程输出里在这一行之外还有**引擎自己**的一行裸文本(`a credential the engine was handed is …`):引擎在适配器构造口收下密钥的那一刻对同一个值说一次,这个收值口没有通告席,落引擎自己的 `console.warn`(引擎已知限制;本服务不是它的写者,也不改写它);给密钥一个真值两行一起消失。`"scrub"` / 缺席 = 上面的缺省剥除。🔴 **`"all"` 只在单用户部署兑现**:`REQUIRE_PRINCIPAL=true`
1730
1820
  ⇒ 422 `request.host_posture_refused`(射程与写容纳面那条腿同一只谓词,`TASK_WRITE_FACE_ESCALATION` 不管它);非 host 车道带 `"all"` ⇒ 400。词表外的值(`"ALL"` · `""` · 名单 `["PATH"]` · 布尔 · `null` …)、非 host 车道带 `"all"`、部署不接 authorizer 时带 `"all"` 这三只都是 `400 request.field_invalid`,7.137.0 起体另带 `field: "inheritEnv"` 与 `reason`(`"unknown_word"` = 是字符串但不在词表;`"not_a_string"` = 根本不是字符串;`"lane_not_host"` = `"all"` 落在非 host 车道;`"no_authorizer_session"` = `"all"` 而部署不接 authorizer),壳按这两位认拒因、不解析句子(句子 / 码 / 状态不变;续跑腿同判,折叠 409 随体带同一组)。托管部署的 `422 request.host_posture_refused` 不在这组:它说的是「这台部署的姿态不给」,不是这个键写错,按 `errorCode` 认、不带这两位。部署方
@@ -1813,6 +1903,25 @@ bake-runner 要用 ⇒ `LSP_ENV_ALLOW` / `BAKE_ENV_ALLOW` 点名放回。
1813
1903
  > 不带任何凭据的列表 `401 auth.unauthorized`、`/metrics` 族 `401`(要 `METRICS_TOKEN`)。仍成立的边界:没配 service token 的部署上,不带登录令牌的请求自报的主体头仍按身份读
1814
1904
  > (与 `REQUIRE_PRINCIPAL` 无 token 形同读)—— 要让「没令牌就没有身份」成立,配 `SERVICE_AUTH_TOKEN`(全局凭据门:每个请求要么带 service token、要么带登录令牌)。
1815
1905
 
1906
+ > **不报主体的 service token 是谁(`SERVICE_TOKEN_ACTS_FOR_DEPLOYMENT`,7.146.0+)**:出示这台部署的 service token 而**不带主体头**的请求,缺省(不设 = `true`)代部署本人说话 ——
1907
+ > fleet 读面(舰队流 / 会话列表 / 发件链接 / 名册 / trace / 用量)看全舰队、运维聚合口(`/metrics*`、`GET /v1/tasks/source-summary`)照放、删除 / fork / 同步对任何会话照开。
1908
+ > 设 `SERVICE_TOKEN_ACTS_FOR_DEPLOYMENT=false` ⇒ 它是**无身份调用方**,各面按既有的「过了凭据门、没有主体」那一臂答:fleet 读面与写档(删除 / fork / 同步)`401 auth.unauthorized`(identity 句)、
1909
+ > 运维聚合口 `401`(operator 句)、舰队流在多租户部署上 `401 auth.principal_required`、在单用户部署上落 `default` 那一族的行;无主的委派子代会话在单用户形(未开 `REQUIRE_PRINCIPAL`)上与别人的行同一个 `404`、在多租形上由会话读面的主体门先答 `401 auth.principal_required`(缺省形同样如此,本位不改这一面)。
1910
+ > 带主体头的请求两形都是那位主体(不变);建 run、run 面(`/v1/runs/:id` 族)、A2A `tasks/get` 不读本位;没配 service token 的部署上设 `false` 不改变任何东西。
1911
+ > 给谁:单 service token + 多主体的网关部署(网关替每个用户写主体头),关掉「漏带头 = 看全部」。**它不解决「网关不可信」**:持 service token 的一方在头里写谁就是谁 —— 鉴权桥不改这一点(桥只给带登录令牌的请求验身份,带 service token 的请求照旧信主体头);要防这一层,别把 service token 交给不可信的一方(不可信的调用方走鉴权桥的登录令牌),或开直连门(主体只认签名的 principal 令牌,自报的主体头不再是身份)。
1912
+ > 词表布尔(`true/false/1/0/yes/no/on/off`,词表外拒启),只有 env 一个座,起服读一次(改了要重启);全部副本设同一个值(滚动重启完成之前收窄不完整);
1913
+ > 起服打一行 `service_token_posture { actsForDeployment: false }`;`GET /health` 的 `auth.serviceTokenActsForDeployment` 报这台部署的答案(上面「自报位」)。
1914
+ > 开 `false` 前把运维脚本 / 面板按端点换凭据(「位关了」与「token 错了」答同一个 `401`,区分就是探测面):
1915
+ >
1916
+ > | 端点 | 开 `false` 之后用什么读 |
1917
+ > |---|---|
1918
+ > | trace 五口(`GET /v1/tasks[/:id/turns\|stream\|artifacts\|tool-results/:ref]`)/ 用量三口(`GET /v1/usage/summary\|series\|breakdown`)/ `GET /v1/tasks/source-summary` / 舰队流 | 只读 `TRACE_TOKEN`(全舰队只读,在全局凭据门之前判) |
1919
+ > | `/metrics*` | 只读 `METRICS_TOKEN` |
1920
+ > | `GET /v1/sessions` / `GET /v1/sendfile-links` / `GET /v1/agents/roster` | **没有全量替换**:配了 service token 时全局凭据门先拒只读 `TRACE_TOKEN`;带具名主体头,只看到那位主体的 |
1921
+ > | 他人会话的删除 / fork / `/sync/*` | **没有替换**(写面不认只读凭据);带属主的主体头 |
1922
+ >
1923
+ > 回到 `true`(或删掉这一行)重启即复原。
1924
+
1816
1925
  | env 键 | 缺省 | 说明 |
1817
1926
  |---|---|---|
1818
1927
  | `PRINCIPAL_HEADER` | `x-agent-principal` | 上面那个身份头的**名字**。🔴 **缺省值是跨仓 wire 常量,不是给部署方自定义的**:这个名字**不在任何 wire 面上广播**(`/v1/capabilities`、`/health` 都没有它),所以没有下游能在运行期问 server「你叫它什么」——SDK 有一个 `principalHeader` 逃生舱但 cli/client-core 都没接线,浏览器端 BFF 是逐字硬编码,`mcp-oa` 侧车自己读同名 env(各读各的)。本旋钮只服务于「入口网关已经把身份写进别的头名」这一种特殊部署(让 server 迁就既有网关),不是给部署方起新名字用的。**改名即断下游**,但后果分四形(逐条对过代码):`REQUIRE_PRINCIPAL=true` 下提交腿(`POST /v1/tasks|/v1/runs`)是 `401` + **粗码 `auth.unauthorized`**(authorizer 抛的无 code HttpError 走状态码兜底映射),属主寻址的读/动词面是 `401 auth.principal_required`(路由级细码);两者的文案都逐字回显**你配的头名**,那是现场唯一能指认改名的线索。`REQUIRE_PRINCIPAL` 未开时更坏:**新会话照常成功**、每一条被当成**匿名**(session 归属 / 记忆隔离 / 规则车道 / operator 判定静默走无身份分支,无任何错误码),而**接续一条已属主的会话**会 `401`(`session is principal-owned; missing principal header …`)。⚠️ **SSO 与 direct-door 两条身份来源不经过这个头**(`verifiedPrincipal`),改名对它们无影响 —— 混合形部署因此会出现「一半调用方有身份、一半静默变匿名」的混着长。改了后果自负,且必须同批把每一家下游改到同名。取值须是**单个**合法 header 名(带空格/逗号 ⇒ 启动期拒启:它会撕裂 CORS `allow-headers`)。成文契约见 [`docs/ASSISTANT-WIRE-CONTRACT.md` §0.5](docs/ASSISTANT-WIRE-CONTRACT.md) |
@@ -1822,7 +1931,7 @@ bake-runner 要用 ⇒ `LSP_ENV_ALLOW` / `BAKE_ENV_ALLOW` 点名放回。
1822
1931
  > **`401 auth.unauthorized` 的 `error` 是一句出路(7.104.0 起;此前只有一个词 `unauthorized`)**,按这道门收什么分三句,码都是 `auth.unauthorized`、都**不含任何值**,
1823
1932
  > 同一道门上「没带」与「带错」逐字同一句(区分就是探测面):① 全局凭据门 —— 说「带这台部署的 service token 作 Bearer」,并指 sema-up 部署的
1824
1933
  > 令牌在 `~/.sema-up/<project>/.env` 的 `SERVICE_AUTH_TOKEN`;② 运维口(`/metrics*`、`GET /v1/tasks/source-summary`)—— 说要运维凭据
1825
- > (不带主体头的 service token —— 带主体头的 service token 请求代那位主体、不是运维凭据,7.134.0 起;指标口另认只读的 `METRICS_TOKEN`,`source-summary` 另认只读的 `TRACE_TOKEN`);③ 过了凭据门、但这一口要调用方身份(属主寻址读写、规则、记忆)—— 说要一个验过的
1934
+ > (不带主体头的 service token —— 在部署让它代自己说话时:`/health` 的 `auth.serviceTokenActsForDeployment` 为 `true`,7.146.0 起句子带这一条件;带主体头的 service token 请求代那位主体、不是运维凭据,7.134.0 起;指标口另认只读的 `METRICS_TOKEN`,`source-summary` 另认只读的 `TRACE_TOKEN`);③ 过了凭据门、但这一口要调用方身份(属主寻址读写、规则、记忆)—— 说要一个验过的
1826
1935
  > 调用方身份,**不**叫你再去带 service token。逐字句子见 `docs/ASSISTANT-WIRE-CONTRACT.md` 附录 A.1 码表。壳(sema CLI)接 sema-up 引擎的
1827
1936
  > 令牌交法见 `deploy/sema-up/README.md`「壳接入」。
1828
1937
 
@@ -1855,6 +1964,7 @@ PostgreSQL 硬上限 ⇒ 只有缩小),运维日志一行 `submit_ctx_row_too_la
1855
1964
  | `projectMemory` | — | 7.135.0+(能力位 `projectMemory`):`"disabled"` = 这一条 run 不读部署的项目上下文 —— 引擎不调部署的项目上下文座,项目指令文件(`CLAUDE.md` / `AGENTS.md` 链)与项目层都不进上下文,这条腿的 `wiring_manifest` 答 `projectMemory: "disabled"`、本服务的 `instructions` 段缺席;引擎自己的 git 状态、记忆、技能 / hooks / MCP 不受影响,委派与后台子代随父继承。唯一的词是 `"disabled"`(缺席 = 照常读);别的值 `400 request.field_invalid`(与 `fileHistory` 同一道拼写门)。没接项目上下文座的部署照样受理(清单答 `"absent"`) |
1856
1965
  | `outputStyle` / `settings.outputStyle` | — | 输出风格**两个座位、两件事**:风格的**正文**(指南全文)走 `settings.outputStyle`(折进系统提示的追加段);**每轮一句**走顶层 `outputStyle: { name, turnReminder? }`(7.121.0+,能力位 `outputStyle`)—— 原样交引擎,引擎每个回合用自己的标记帧说「`<name> output style is active. <turnReminder>`」(`turnReminder` 缺席用引擎缺省句),流上回声 `steering_injected` `source:"output_style"`。只在非缺省风格时声明顶层键;它的值形由引擎判(`name` 非空、≤256 字;`turnReminder` 在场即非空串;不许别的键),坏值这一轮终态 `failed`、码 `config.output_style_invalid` |
1857
1966
  | `model` | — | 本次用哪个模型:只能点服务端**已配置目录**里的名字 / 档位词 / 模型 id(目录见 `GET /v1/models`;请求体带不进新模型)。未知 ⇒ 400 `request.unknown_reference`(带 `available` 列表);目录里有、但本部署服务不了 ⇒ 400 `request.model_unavailable`;空串 / 非字符串 ⇒ 400 `request.field_invalid` |
1967
+ | `settings.autoCompactEnabled` | — | 7.146.0+(能力位 `taskSettings.autoCompactEnabled`;老 server 对这个键 `400 request.body_shape` —— 见位才发):**布尔**,这条 run 的自动压缩开关(CC settings 同名键),逐字交给引擎的自动压缩座 `compaction.autoCompact`。`false` = 关引擎自起的摘要压缩;显式 `true` = 开(压过部署席的关);缺席 = 部署席的缺省(本服务自己的装配不设部署席 ⇒ 开);非布尔 ⇒ `400 request.field_invalid`。逐字交给引擎,与顶层 `compaction.clampTolerance` 是同一个 `compaction` 对象里的两个成员(开关只有这一个座位,顶层 `compaction.autoCompact` 不被读)。壳的 `DISABLE_AUTO_COMPACT` / `DISABLE_COMPACT` 环境变量由壳折成这个布尔再发(`DISABLE_COMPACT` 多出的「手动也关」由壳自己不发 `/compact`),本服务不读环境变量。🔴 **`false` 关掉的是引擎自起的三遍**:窗阈值那一遍、服务商拒绝过长请求后的恢复压缩、任务尾摘要;对这条 run 的手动压缩(`POST /v1/runs/:id/compact`)**不受影响**(归引擎的另一只座 `manualCompact`,本服务不写它)—— 与参照实现同读;请求投影层的旧工具结果清除与媒体裁剪不受它管。关掉之后撞上窗口上限的 run:守卫线之上引擎先清旧工具结果、仍超线则只保留尾部(较早的消息从该次请求里省掉,会话历史不动);服务商仍 4xx 拒 ⇒ `terminal.kind:"failed"`,拒绝被归为超长输入时 `terminal.detail.errorKind:"input_too_long"`;代价落在发 `false` 的那条 run 自己(更长历史每回合重发,每任务预算上限照常封顶)。读法全文见契约 `settings.autoCompactEnabled` 一节 |
1858
1968
  | ~~tools~~ | 🚫 | **不是受理键**(传了 ⇒ 400 `request.body_shape`,`unknownKeys` 点名)——工具面走场景装配;按请求只能**减**:`allowTools`(7.126.0 起;引擎的可用性选择器,只留列出的,原样透传)/ `excludeTools` / `settings.permissions` 的 deny · ask(`allow` 7.115.0 起是 allow 规则:给出「有人表态」,回答没人表态的服务器工具 / A2A 调用;不加工具、不放掉任何一层自己的问) |
1859
1969
 
1860
1970
  **`TaskResult`(同步 / `done` 事件里拿到的;7.64.0 起终局是**一条** `terminal` 因由,不再有 `status`/`errorCode`/`errorMessage`/`blockedReason` 四个平面键):**
@@ -1872,8 +1982,16 @@ PostgreSQL 硬上限 ⇒ 只有缩小),运维日志一行 `submit_ctx_row_too_la
1872
1982
  | `sessionId` | 续聊用——下次带回 |
1873
1983
  | `terminal.kind` | `completed` / `blocked` / `failed` / `paused`(闭集;`timeout` 已于 core 5.8.0/server 6.0.0 退役——见 §9) |
1874
1984
  | `terminal.code` | `failed` 臂的程序化分支:如 `"conflict"`(乐观锁丢失,可重试) |
1875
- | `terminal.reason` | `blocked` 臂:为什么做不了(缺信息/权限) |
1876
- | `stats.toolCalls` | 工具调用的**尝试**次数,不是「真执行了几次」:模型每发出一次工具调用请求就计一次,计在工具查找与入参校验**之前** —— 请求了没挂载的工具(例:执行车道未点名时的 `Bash`)照样计 `1`;同一条 `Bash` 请求在真执行车道(文件落盘)与未点名车道(没挂、文件不存在)两臂同读 `1`。所以**不能**拿它判「任务真跑了工具」:判「真跑了没有」不能只看一个位:`tool_end.isError` 是逐调用的**工具结果错误标记**(工具信封报错才 `true`),`false` 只说结果没被标错 —— **不**说命令退出成功(Bash 跑完退出码非 0 照样 `false`,退出码看 `structured.exitCode`),`true` 也不证明工具体跑过;「没跑」的正证据是 `notExecuted`;逐腿「有没有手」看 `wiring_manifest.hands`,不看这个数。它是**本腿**计数(续跑腿只计本腿)。 |
1985
+ | `terminal.reason` | `blocked` 臂的**模型面**句:`terminal.detail` 在场时是引擎给模型的 reminder 框句(别渲给人),没有 `detail` 时是 agent 自己报的卡住原因(缺信息/权限,可渲);见表下「blocked 终局的用户面」 |
1986
+ | `terminal.detail` | `blocked` 臂的用户面 / 机读面(只在部署的 `userPromptSubmit` hook 拦下主提交时在场):`{ kind, cause, hookReason? }`;见表下「blocked 终局的用户面」 |
1987
+ | `stats.toolCalls` | 工具调用的**尝试**次数,不是「真执行了几次」:模型每发出一次工具调用请求就计一次,计在工具查找与入参校验**之前** —— 请求了没挂载的工具(例:执行车道未点名时的 `Bash`)照样计 `1`;同一条 `Bash` 请求在真执行车道(文件落盘)与未点名车道(没挂、文件不存在)两臂同读 `1`。所以**不能**拿它判「任务真跑了工具」:判「真跑了没有」不能只看一个位:`tool_end.isError` 是逐调用的**工具结果错误标记**(工具信封报错才 `true`;委派调用 `Agent` 的子代以 `failed` 收场也是 `true`,同步卡 / fork 卡与后台轮询同一判,以 `blocked` 截断收场的同步子代不是),`false` 只说结果没被标错 —— **不**说命令退出成功(Bash 跑完退出码非 0 照样 `false`,退出码看 `structured.exitCode`),`true` 也不证明工具体跑过;「没跑」的正证据是 `notExecuted`;逐腿「有没有手」看 `wiring_manifest.hands`,不看这个数。它是**本腿**计数(续跑腿只计本腿)。 |
1988
+
1989
+ **blocked 终局的用户面是 `terminal.detail`,不是 `terminal.reason`**:`reason` 是**模型面**句 —— `detail` 在场(部署的 `userPromptSubmit` hook 拦下了这次主提交)时,它是引擎给模型的 reminder 框句
1990
+ (`<system-reminder …>` 框 + 会话标记,hook 的话在框里),直接渲给人会把标记露在屏上;没有 `detail` 时它是 agent 自己用 `ReportBlocked` 报的卡住原因,可以渲。
1991
+ 用户面 / 机读面 = `detail { kind: "prompt_blocked_by_hook", cause, hookReason? }`:`cause` ∈ `block` / `crashed` / `timeout` / `cancelled`(hook 的四种收场;提示没有被提交,改了再发有意义),
1992
+ `hookReason` 只在 `cause:"block"` 且 hook 给了话时在场 —— hook 作者的原话(折一行、限 240 字、引擎信封标签去势,本服务与 `reason` 过同一只脱敏器),按不可信文本渲。
1993
+ **消费方渲 `detail`,不渲 `reason`**;`reason` 只在没有 `detail` 时才是可渲的句子。按 `detail.kind` 分支的消费方必须留缺省臂(新的 `kind` 随引擎 minor 加入)。
1994
+ 同一块出现在同步 200 体、流 `done.result`、`GET /v1/runs/:id` 的 `result` 与 `GET /v1/runs/:id/events` 的 `done` 行。
1877
1995
 
1878
1996
  > ⚠️ **任务成败以 `terminal.kind`/`terminal.code` 为准,勿以 HTTP 状态码判**:同步腿 200=提交受理成功(任务可能
1879
1997
  > `terminal.kind:"failed"`+`code`);`POST /v1/runs` 202=已受理(执行期结局落在终态 run 记录/`done` 帧)。执行期
@@ -1988,13 +2106,19 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
1988
2106
  > 本流上同样逐字(不过文本脱敏,卡上其余成员照常脱敏),积压判据把它与 `output` 块的字节同一帧一起计、降级时一并去掉;账本面(账本行与 `/events`、`GET /v1/tasks/:id/turns`、`GET /v1/tasks/:id/stream` 的回放)里卡上**没有** `file.base64`
1989
2107
  > (与引擎对超 48K 的卡自己略去该键的形一致,`originalSize` / `type` 仍在)。7.127.0 / 7.127.1 上这份字节(≤48K 字符)仍随账本行落库并回放,7.127.2 起不带。
1990
2108
  > 会话存档读面同律:`GET /v1/sessions/:id` 的 `messages[]` 里,工具结果消息的 `details` 就是这张卡 —— 媒体卡的 `file.base64` 逐字(不去键、不过文本脱敏),卡上其余成员与非媒体卡照常脱敏(7.128.0 起;此前这一位也被文本脱敏改写)。
2109
+ >
2110
+ > **后台子代的当前状态 `agentState`**(引擎 7.41.0 起过境;本服务零新键,读法成文)`{ status, readAt, row, lastActivityAt?, approval?: { state, checkpointId?, deadline? } }`(时刻 epoch ms;`row` 是 `"live_row"` 或 `"durable_row"`;`approval` 只在 `status` 为 `"parked"` 时在场)。
2111
+ > 带它的应答:本流 / 子代 tail 流 / 账本回放里 `tool_end.structured` 的两张卡 —— `AgentTranscript` 的步骤答话(`structured.type` 为 `agent-transcript`:`structured.agentState`,`steps[i].at` / `inFlight`)与 `ListAgents`(`list-agents`:每个后台子代行 `rows[i].agentState`);
2112
+ > 本服务对卡只换串叶里的凭据形、不枚举卡内的键,`agentState` 逐键原样过(卡在不在由引擎定:类型在卡集里、整卡序列化不超过 65,536 字符才有)。读口 `GET /v1/runs/:id/subagents/:handle/output` 与 `GET /v1/runs/:id/tasks/:tid/output` 的体里 `output.agentState` 与 `content` 里的 `state:` 行(别的副本写下的持久行同读:
2113
+ > 已结行 `lastActivityAt` 是落定时刻,停驻行是停驻时刻,别的副本还标 running 的行答 `not_local` 且无 `lastActivityAt`)。**不带它的应答**(缺席 = 引擎没给):拒答(引擎取得了状态的那几类,首行 `State: …` 在 `output` 文本上;未知或无权限的 id 的拒答两者都没有)、`ListAgents` 里本进程没有活句柄的名册行(`state:"unknown"`)、
2114
+ > 后台子代任务 poll 的 `details`(不进 `structured`;引擎的 `TaskOutput` 工具已退役,这份 poll 如今只有这两个读口)。读口不交检查点座 ⇒ **停驻行(行上有审批 token)的审批卡读 `unreadable`**(无 `checkpointId` / `deadline`;行上没有 token 的读 `missing`),等的那张卡看 `GET /v1/approvals` 的 `pending[]`。名册行 `GET /v1/agents/roster` **不带** `agentState`:行只带持久行自己的列(`updatedAt` 对仍标 running 的行是写者的租约续期、不是活动),活动时刻与审批态读该子代的 output 读口。
1991
2115
 
1992
2116
  ## 7. 其它接口(按需)
1993
2117
  | 端点 | 用途 |
1994
2118
  |---|---|
1995
- | `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(`DB_BACKEND=local` 到 7.140.x 也是)/ 会话店不存元组(`DB_BACKEND=memory`;嵌入方自建、不声明委派分区的店),读作「没存」、回落 fleet 行 / 名册上的 id。子代会话的属主是引擎判的委派树属主(**7.144.0+**:没人派生的 run 读自己那一行的属主、按化身绑一次,原样带给每一层子代;三只持久会话店〔MySQL 协议 / PostgreSQL / local〕建行那一笔原样记引擎交来的属主,`null` = 根匿名;停驻挂点只比对宿主行属主与引擎交来的属主,不认领、不推断。**7.133.0–7.143.x** 是本服务在建行那一笔按根 / 父推断:根会话的行在 ⇒ 它的属主,根行已不在 ⇒ 它记录的父会话的,都不在 ⇒ 哨兵 `_orphan:<id>`;7.132.x 及更早的 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 后端两店每次起服由一次性迁移 M23 在它的窗内回填升级前写下的无主 placed 行(按 7.133.0 起的读法:根的属主,根行不在取记录的父的,都不在写 `_orphan:<id>`;起服读数行 `placedSessionOwnersRemaining` 非零 = 还有没定下的,再起直到为 0),local 后端没有回填臂。属主 `_orphan:<id>` 由 7.133.0–7.143.x 的建行推断与 M23 回填写下(7.144.0 起本服务不再推哨兵,建行原样记引擎交来的属主 —— 已有的哨兵属主随引擎的树属主原样带到它下面新建的子代行上):任何主体都不匹配,只有 fleet-wide / operator 读得到,前缀永久保留。停驻时宿主行属主与引擎交来的不等 ⇒ 停驻否决(那张待批卡作废、那一只子代以失败收尾) |
1996
- | `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=` 跨看也是原文) |
1997
- | `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+**) |
2119
+ | `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(`DB_BACKEND=local` 到 7.140.x 也是)/ 会话店不存元组(`DB_BACKEND=memory`;嵌入方自建、不声明委派分区的店),读作「没存」、回落 fleet 行 / 名册上的 id。子代会话的属主是引擎判的委派树属主(**7.144.0+**:没人派生的 run 读自己那一行的属主、按化身绑一次,原样带给每一层子代;三只持久会话店〔MySQL 协议 / PostgreSQL / local〕建行那一笔原样记引擎交来的属主,`null` = 根匿名;停驻挂点只比对宿主行属主与引擎交来的属主,不认领、不推断。**7.133.0–7.143.x** 是本服务在建行那一笔按根 / 父推断:根会话的行在 ⇒ 它的属主,根行已不在 ⇒ 它记录的父会话的,都不在 ⇒ 哨兵 `_orphan:<id>`;7.132.x 及更早的 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 后端两店每次起服由一次性迁移 M23 在它的窗内回填升级前写下的无主 placed 行(按 7.133.0 起的读法:根的属主,根行不在取记录的父的,都不在写 `_orphan:<id>`;起服读数行 `placedSessionOwnersRemaining` 非零 = 还有没定下的,再起直到为 0),local 后端没有回填臂(**7.145.0+** local 后端建子代行先记属主、再建转录存档,进程崩在两笔之间留下的是一只没有转录的属主格 —— 不是子代行,不再新生无主子代行;更早版本崩在两笔之间留下的无主子代行照旧无主、不回填,出路是取消 / 删掉那只后台子代)。属主 `_orphan:<id>` 由 7.133.0–7.143.x 的建行推断与 M23 回填写下(7.144.0 起本服务不再推哨兵,建行原样记引擎交来的属主 —— 已有的哨兵属主随引擎的树属主原样带到它下面新建的子代行上):任何主体都不匹配,只有 fleet-wide / operator 读得到,前缀永久保留。停驻时宿主行属主与引擎交来的不等 ⇒ 停驻否决(那张待批卡作废、那一只子代以失败收尾) |
2120
+ | `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+**)。一张停驻行的存储体读不出摘要(手改成 `{}`、整段缺 `state` 等)只坏它自己那一行(**7.145.0+**,MySQL 协议 / PostgreSQL 后端):收件箱与运维队列照列它、带引擎判官的 `rowVerdict`(只缺 `pendingAction` 的行判官答 `ok`,引擎侧已知缺口),任务视图照列那条 run,三个面都不再整张答 500 / 503;别的卡的决断照常,这张卡自己的决断答 409 带码拒,`error` 以行上 `rowVerdict.sentence` 开头(**7.146.0+**;此前存储体 `{}` 的卡被指向 plan_review 的口、存储体读不成检查点的卡只有店的诊断);`DB_BACKEND=local` 上列表面本版仍整张 503(决断已逐行)。停驻的**技能卡**(对引擎 `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=` 跨看也是原文)。停驻行带 `agent`(**7.145.0+**):行的会话在委派树里的位置 `{ id?, parentSessionId?, rootSessionId? }`,与 `GET /v1/sessions/<sessionId>` 体顶层的 `agent` 同一只读口、逐值同答 —— 后台委派子代停驻的行(`taskId` 为 `null`、`sessionId` 是子代自己的会话)凭它连回发起委派的会话(`rootSessionId` / `parentSessionId` = 本端会话 ⇒ 本端的卡);缺席 = 这台 server 没有说(宿主会话自己的停驻、同步委派的子代、`subagent_type: "fork"` 的后台子代、不存委派元组的店、那一读没读成 —— 只这一行缺键、列表照出),按原读法判,不读作「不是子代」 |
2121
+ | `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+**;含 `agent`,新订阅首拍补给的帧与之后推出的帧同值,**7.145.0+**) |
1998
2122
  | `POST /v1/approvals/<sessionId>/decide` `{"decision":"approve"|"deny","reason":"…"}` | 批/否并恢复挂起任务(CAS,重复决议 409);**仅 operator**(非 operator → 403)。5.0.0 起旧轮询腿 `POST /v1/approvals/<id>` 已退役。停驻的后台子代**复活不了**(行上没有根会话锚;未起名的后台子代不在此列 —— 引擎 7.57.0 起每只子代都有名字,升级前的无名行读作派生名,照常复活)⇒ `deny` 作废这张卡:`200 {taskId, status:"failed", decision:"deny"}`,卡离开待决队列、子代终局 `failed`;`approve` 答 `409 decide.row_unrevivable`、拒句指路 deny(**7.136.0+**;此前两个决定都 409,卡处理不掉)。有根会话锚的停驻子代卡上,`decide` 与删根会话 / 卡作废并发:写口放行之后卡已被作废 ⇒ `422 parked_resume.startup_failed`(卡已作废、什么都没复活;与 `409 decide.gate_moved` 并列为并发终局码,调用方须同时处理 409 / 422) |
1999
2123
  | `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 |
2000
2124
  | `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) |
@@ -2003,6 +2127,7 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
2003
2127
  | `POST /v1/admin/drain` `{"actionId":"…","worker":"…","deadlineAt":…}` | **排空口**(7.121.0 起体 = settings-schema `DrainRequest`;operator-only —— principal 须在 `OPERATOR_PRINCIPALS` 内,或单用户部署声明了 `OPERATOR_SINGLE_USER=true`;两条出路都没有〔名单为空且没声明〕⇒ 501)。体 `{ actionId, worker, deadlineAt, scope?, expectedGeneration?, instanceId?, term? }`:`actionId` 幂等键(人手形自取,如 `manual:<uuid>`)、`worker` 本引擎的 worker 名(`SEMA_WEB_ADMIN_WORKER`)、`deadlineAt` 到期自停的 epoch ms、`scope` 本引擎的租户 scope(`FLEET_SCOPE`;不带 = 缺省 scope `global`,与没设 `FLEET_SCOPE` 的引擎相符 —— 多租引擎要带它自己的)、`expectedGeneration` fleet 成员的代际(`FLEET_GENERATION`;非成员不带)、`instanceId` 本进程身份(`GET /health` 读;不带则不比)。身份逐字段比(`worker` / `expectedGeneration` 缺席对缺席),不符 ⇒ `409 conflict.fleet_identity`(什么都不变);首次 ⇒ `200 {actionId, since, deadlineAt, state:"draining"}` 并**进入排空**(拒新提交、本副本可抢占的在飞腿当拍让出、活的审批卡改路耐久停驻、在飞归零或到期限即停);再来 ⇒ `200 … state:"already_draining"` 回显第一次的 `since` / `deadlineAt`,不重置。体坏 ⇒ `400 request.field_invalid`;`deadlineAt` 离本引擎此刻超过 `FLEET_LEASE_OUTAGE_MAX_MS`(缺省 24 h,一台实例活着而不服务的最长时长;7.122.0 起)⇒ 同码 400(什么都不变;拒句点名字段与上界的值;只在这一发会进入排空时判 —— 已在排空或硬停已在进行时任何重发都回 `already_draining`),已过的期限照收(进入即到期);嵌入宿主(无收尾链)⇒ `501 capability.drain_state_required`。没有 worker 身份的引擎(没配 `SEMA_WEB_ADMIN_WORKER`)恒 409 —— 用 SIGTERM。经本口进的排空之后,平台随后发的第一发 SIGTERM 被吸收、不缩短期限(第二发判据见「排空期的第二发信号」)。7.120.x 的 `{"reason":"…"}` 声明形与 `/health.drainReason` 已退役。 |
2004
2128
  | `GET /v1/config/catalog` | **配置目录自描述**(operator-only —— `explicitOperatorOk`:没有运维名册〔`OPERATOR_PRINCIPALS` 为空且没有单用户声明 `OPERATOR_SINGLE_USER=true`〕⇒ 501 `capability.operator_principals_required`,有名册而调用方不是运维 ⇒ 403)。逐 env 键一行:`{name, domain, type, semantics, writeLanes, effectiveLane, envHeld, envTiming, staticDefault?/derivedDefaultNote?, effectiveValue, valueClass, danger, center?{domain,path,precedence,timing,restartSlice?,managedNow,effectiveNow}}`(🔴 7.106.0 起 `center.domainExists` 删除:座位在加载期对契约 `DOMAIN_SCHEMAS` 核过域 + 路径,核不过即拒启 ⇒ 有 `center` 块 ⇔ 中心可写;readFace 四键 / `PERMISSIONS_DISABLE_AUTO_MODE` / `MODEL_DEGRADE_ON` / `_REACTIVE` 七行的座位随之删除);顶层带 `restartSlices`(重启片闭集)与 `centerDomains`(settings-schema 现行域表)。**两轴不混**:`writeLanes`=谁能写,`effectiveLane`=现在谁在生效(env-wins 族 env 占位 ⇒ center 腿 `effectiveNow:false`)。secret 类值**永不回传**(只回在场位;opaque 回在场位+长度;URL 剥 userinfo)。完备性由仓内 AST 双向对账门执法(分母=全 `src/` 生产 env 读取点)。旋钮 `CONFIG_CATALOG_ENABLED=false`(合规部署)⇒ 501 `feature.config_catalog_disabled`(路径恒挂载)+ 能力位 `configCatalog:false`。响应 `Cache-Control: no-store`。**7.57.0 起**:`enum` 行的 `effectiveValue` **恒落在该行自己的 `enumValues` 闭集内**(仓内机器门执法)——此前 `SESSION_BACKEND` 回的是**内部标签** `tidb`(自 5.0.0 起是**退役公名**,写回去 boot 拒),现归一成公名 `mysql`;`MODEL_DEFAULT_THINKING` 补上一直被解析器接受、却漏在目录闭集外的 `xhigh`,并改为回**实算值**(`off`/未设 ⇒ `null`)而不是 env 原文。 |
2005
2129
  | `POST /v1/admin/config/refresh` | **配置手动刷新**(operator-only,与上一行同门:没有运维名册〔`OPERATOR_PRINCIPALS` 为空且没有单用户声明 `OPERATOR_SINGLE_USER=true`〕⇒ 501;名单非空而调用方不在名单上 ⇒ 403)。改完中心配置 / `config.d` 之后不想等 60s 轮询就调它:触发**一次既有**的 refresh 拍(在飞则汇入那一拍,绝不并发双拍),返回时该拍已落地。200 体 `{triggered:true,targetVersion,appliedVersion}` —— 两个世代号与 `/health` 的 `configTargetVersion`/`configAppliedVersion` 同源。候选被判官整拒(坏域 / 坏 prompts / 带事实的退役域 —— 7.126.0 起 / 本机 `config.d` 里读不出的文档,门控域在内)是这一拍的结论,不是失败:照样 200,`targetVersion ≠ appliedVersion`,拒因在 `config_candidate_rejected` 日志行;拍真的失败(配置源不可达 / 读不了 / 应用抛错)⇒ `502 internal.config_refresh_failed`。本部署没有配置管道(纯 env worker:既没配 `CONFIG_CENTER`、也不是 `CONFIG_PROVIDER=local`)⇒ 409 `config.refresh_unavailable` 指路,不会回一个「触发了但什么都没发生」的 200。 |
2130
+ | `POST /v1/adoption/<adoptionId>/sessions` `{"sessions":[{"clientSessionId":"…","entries":[…]}]}` | **收编批量导入**(**7.146.0+**;能力位 `adoptionImport`,按调用方):收编的**源主体本人**把本机会话导进那条收编的目的主体名下(operator 不能代,请求体不能点名目的地);导进来的是只读镜像,在云端接着跑先 fork。`adoptionId` 从 `GET /v1/capabilities` 的 `adoptionImport.adoptionId` 读(只对源主体本人在场,另带两只帽 `maxSessions` / `maxSessionBytes`)。云端会话 id 由 `(adoptionId, clientSessionId)` 确定性派生 ⇒ 重投安全。200 + 逐元素结局 `imported` / `already_imported` / `rejected{code: too_large \| invalid_envelope \| duplicate_client_session_id}`;信封恰好两键,多一个键(如 `cwd`)⇒ 这个元素 `invalid_envelope`。整请求拒:401 / 501 `capability.adoption_store_required` / 404 `not_found.adoption` / 403 `auth.adoption_source_only` / 409 `adoption.not_adopted` / 400(体形、元素数超帽)。帽 = `ADOPTION_IMPORT_MAX_SESSIONS`(缺省 100)/ `ADOPTION_IMPORT_MAX_SESSION_BYTES`(缺省 6 MiB)。契约 §26 |
2006
2131
  | `GET /health` | 健康(无需鉴权)。**7.67.0+** 恒带 `startedAt`(引擎进程起点 epoch ms,一次铸;与 `pid` 并列的换代锚——值变了就是换了一条命)。7.37.0+ 配置管道在场时带世代账键:`configTargetVersion`(本副本**最后见到**的配置版本,身份标)、`configAppliedVersion`(最后一次真落地的版本)、`configTargetOrdinal`/`configAppliedOrdinal`(7.38.0-rc.2 起,**收货序数**——`CONFIG_PROVIDER=local` 下 version 是内容哈希不承诺序,「改了没生效」看序数对不对齐)、`configApplyStaleMs`(target≠applied **持续**时长;收敛时键缺席)—— 编排器据此摘掉「配置持续落后」的副本 |
2007
2132
  | `GET /v1/sessions/:id/background` · `POST /v1/sessions/:id/background/stop` `{"includeRetained":true\|false}` | **会话后台任务的读口与停止口**(**7.102.0+**;能力位 `background.listFace` / `background.exitFaces`,老 server 404)。读口列**本会话**、**本副本**上的后台任务行(`{ rows: [{ id, description?, type, status, createdAt, sessionScoped, retained?, sessionId? }] }`,`retained:true` = 声明了保留的活服务,正常退出不会停它;`sessionId` = 后台 agent 行的子代会话 id,绑定之后才在);停止口「软 → 等一次 `HOST_BG_KILL_GRACE_MS` → 硬」收掉本会话的后台 shell(SIGTERM 处理器先有一次宽限跑完,之后组 SIGKILL,屏蔽 SIGTERM 的循环也活不过去;有后台行时应答慢一次宽限)并逐字返回受据 `{ receipts: [{ id, outcome }] }`:`killed` / `already_gone` / `retained_skipped`(`includeRetained:false` 留下的活服务)/ `no_pgid`(远端 / 沙箱车道没有本机进程组可发,已派普通 kill,「可能还在跑」;host 车道的 `already_gone` 只由观察得出 —— 引擎已收割组长或内核答组已空,7.103.0 起;darwin 上梯子那一枪先落地、引擎还没收割的那一拍如实答 `no_pgid`,判「收掉没有」读进程)。`includeRetained` 必填:`true` = 连活服务一起停(壳的「Exit and stop tasks」),`false` = 活服务照留(`sema kill`)。鉴权 = 会话属主或显式 operator(未知与他人同 404);停止口在无凭据写门内(没配 service 凭证且未开 `ALLOW_UNAUTHED_WRITES` ⇒ 503 `auth.service_token_required`)。`DELETE /v1/sessions/:id` 走同一只收敛(`includeRetained:false`);run 取消(Esc)**不**收后台 shell。契约 §16 |
2008
2133
  | `GET /readyz` | **就绪探针**(**7.102.0+**,无需鉴权;7.121.0 起是唯一的探针路径)。就绪 ⇒ `200 {"ready":true}`;未就绪 ⇒ `503 {"ready":false,"reasons":[…]}` + `Retry-After: 5`,`reasons` ⊆ {`draining`, `model_roster_pending`}(按优先序)。与 `/health` 的 `ready` / `readyReason` / `reasons` 同一只判;库连不上**不算**未就绪(`storeLive` 只在 `/health` 披露)。给只认状态码的**摘流**探针用(k8s `readinessProbe.httpGet`、LB 健康检查、镜像 `HEALTHCHECK`);存活探针用 TCP 探端口,不打 HTTP,见 §0「探针」 |
@@ -2028,7 +2153,8 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
2028
2153
  > **名单为空 = 没有人是 operator**(每个调用方只看自己的待批 —— 不带主体的调用方看到的是匿名卡〔不带主体提交的任务停下的那几张,即决断口对它开放的那几张〕,`?owner=` 不生效;7.133.0 之前审批四个读面在名单为空时把每个调用方当 operator,那条读法已删)。
2029
2154
  > **单用户部署当自己的 operator**:`OPERATOR_SINGLE_USER=true` = 「这台部署的每个调用方都是 operator」,对全部运维面生效;只在 `REQUIRE_PRINCIPAL` 未开且 `OPERATOR_PRINCIPALS` 为空时成立(与任一同在 ⇒ 拒启);
2030
2155
  > 没配服务凭据的部署上能连上端口的都是 operator;不带主体的 operator 没有身份可记,记「谁改的」的写面(`PUT /v1/admin/memory-optout[/:principal]`)要带主体(否则 401)。
2031
- > 单用户 + 名单空 + 没声明 + 有审批底座 ⇒ 起服 warn `operator_undeclared_single_user`(不拒启)。
2156
+ > 部署形不是输入:单用户部署上 `GET /v1/diagnostics/wiring` 与 `GET /v1/outcomes` 也只认运维 —— 名单空且没声明 ⇒ 两口 501 `capability.operator_principals_required`,具名名单在单用户部署上同样生效(此前诊断面在名单为空的单用户形上、outcomes 在任何单用户形上都不问名单)。
2157
+ > 单用户 + 名单空 + 没声明 ⇒ 起服 warn `operator_undeclared_single_user`(不拒启;句子点名这两口与审批队列,出路 = 一行 `OPERATOR_SINGLE_USER=true`)。
2032
2158
  > ⚠️ **多租户形拒启**:`DURABLE_APPROVAL=true` + `REQUIRE_PRINCIPAL=true` 而 `OPERATOR_PRINCIPALS` 空 ⇒ 进程启动失败并点名修法(空名单下没人能当这台多租 worker 审批队列的 operator)。设名单,或确属单租户则不设 `REQUIRE_PRINCIPAL`。
2033
2159
  > 🔒 **停驻任务续跑上下文里的凭据是密封的(7.134.0 起)**:任务停驻时存下的提交体里,请求自带的凭据(`mcpServers[].transport.headers` / `.env` 的值、`a2aPeers[].headers` 的值、`settings.env` 的值、
2034
2160
  > `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/` 最新那把公钥封、
@@ -2040,10 +2166,10 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
2040
2166
  >
2041
2167
  > **部署治理三声明(7.21.0 起)**——三根都是**配置期**旋钮,「谁能改它们」≡「谁能改
2042
2168
  > 部署配置」,不引入新鉴权面;三根**都不设 = 现行为逐字不变**,坏值一律**拒启**(安全轴上的旋钮不静默失效)。
2043
- > - `COMPLIANCE_PROFILE=hipaa|zdr`(+ 可选 `COMPLIANCE_ADDITIONAL_DENIES=mcp_servers,workflows,web_fetch,org_memory_mount,a2a_peers`,
2169
+ > - `COMPLIANCE_PROFILE=hipaa|zdr`(+ 可选 `COMPLIANCE_ADDITIONAL_DENIES=mcp_servers,workflows,web_fetch,org_memory_mount,a2a_peers,memory_consolidation`,
2044
2170
  > **只能在档位内建 floor 上加禁,不能减**):合规档位否决。档位在场时被禁能力不挂载,任务**显式请求**被禁能力
2045
- > ⇒ 响亮拒(prepare 期终态 `config.compliance_denied`)。hipaa 内建禁 `web_fetch`+`org_memory_mount`,zdr 内建禁
2046
- > `org_memory_mount`。⚠️ 档位是**部署级**(对本部署所有 principal 同一份)——per-principal 档位需要中心侧下发面,
2171
+ > ⇒ 响亮拒(prepare 期终态 `config.compliance_denied`)。hipaa 内建禁 `web_fetch`+`org_memory_mount`+`memory_consolidation`,zdr 内建禁
2172
+ > `org_memory_mount`+`memory_consolidation`(`memory_consolidation` 自 7.146.0 / 引擎 7.63.0 起:否决的是记忆整理,不是任务面 —— 判在整理阀门上,见 `MEMORY_CONSOLIDATION_DRIVER`)。⚠️ 档位是**部署级**(对本部署所有 principal 同一份)——per-principal 档位需要中心侧下发面,
2047
2173
  > 那一面尚未建;拼错档位名或能力名一律拒启(拼错=「标着 hipaa 却根本没在执行」)。
2048
2174
  > 🔴 **装配相容性拒启**(不是配置洁癖,是保护):被禁能力若在本部署的装配里**恒在场**,引擎会**整拒**
2049
2175
  > 每一条腿(不是静默不挂载),所以这两形在启动期就拒:①禁 `mcp_servers` 而部署自己配了 MCP 服务器;
@@ -2097,7 +2223,7 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
2097
2223
  > - **未设 + 没有便宜模型**(`MODEL_CHEAP_ID` 未设时 summarize 落主模型)⇒ **关** + 起服一行 warn
2098
2224
  > `memory_consolidation_default_off_no_cheap_model`(点名 `MODEL_CHEAP_ID`;显式写 `off` 即静音)。理由:一轮整库
2099
2225
  > 折叠读 ~1e5 prompt tokens,落主模型就是按最贵的价记账。
2100
- > - **未设 + 结构上跑不起来**(下面 ①②③ 三形:多租户 / `MEMORY_ENGINE=off` / pg·mysql 记忆后端 / `MEMORY_PROVENANCE=off`)
2226
+ > - **未设 + 结构上跑不起来**(下面 ①②③⑤ 四形:多租户 / `MEMORY_ENGINE=off` / pg·mysql 记忆后端 / `MEMORY_PROVENANCE=off` / 合规档否决整理)
2101
2227
  > ⇒ **恒关、零提示**(配不配便宜模型它都开不了;缺省不造新拒启)。
2102
2228
  > - **显式 `on` / `off` 恒赢**。显式 `on` 而座落主模型 ⇒ 照开,起服一行 warn `memory_consolidation_on_main_model`
2103
2229
  > 说清按主模型计费(部署方显式承担)。
@@ -2110,13 +2236,15 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
2110
2236
  > 便宜模型在场 ⇒ 派生开,不再误拒)。
2111
2237
  > ⚠️ **闭集两词,别的一律拒启**——本旋钮是新的,不继承 `MEMORY_ENGINE` 的 `true/1/yes` 放宽:
2112
2238
  > 一个被静默读成 `off` 的开词买到的是「运维以为在折叠、其实一次都没跑过」,而这条面的失败恰恰看不出来。
2113
- > 🔴 **`on` 而这台机器结构上跑不了 ⇒ 拒启**(四形各有点名文案):①没接记忆引擎(`MEMORY_ENGINE=off`,
2114
- > 或多租户形——记忆引擎只在单用户部署上装配);②记忆后端不自带引擎控制面归属(`MEMORY_ENGINE_BACKEND=file`
2115
- > 有,`pg`/`tidb` 没有 —— core 会把控制面落到副本本地盘,而一条 run 的续跑账死在 pod 重建上等于下次整库
2116
- > 重新蒸馏、真金白银);③`MEMORY_PROVENANCE=off`(折叠法必须铸得出 origin 标记,否则产物会不带标记提交);
2239
+ > 🔴 **`on` 而这台机器结构上跑不了 ⇒ 拒启**(五形各有点名文案):①没接记忆引擎(`MEMORY_ENGINE=off`,
2240
+ > 或多租户形——记忆引擎只在单用户部署上装配);②整理状态不随记忆后端走(引擎对这只后端答 `"replica-local"`:
2241
+ > `MEMORY_ENGINE_BACKEND=file` 答 `"backend"`,`pg` / `mysql`(MySQL 协议;TiDB 走这一词)答 `"replica-local"` —— 状态会落到副本本地盘,而一条 run 的续跑账死在
2242
+ > pod 重建上等于下次整库重新蒸馏、真金白银);③`MEMORY_PROVENANCE=off`(折叠法必须铸得出 origin 标记,否则产物会不带标记提交);
2117
2243
  > ④**模型座位解析不出来**——既没有显式 chat 席,也没有 `roles.consolidate` / `roles.summarize`(含档位表的
2118
2244
  > `flash` 绑定)。core **刻意不回落主模型**(整库蒸馏不该悄悄骑最贵的席位),本服务照抄那条判断,不在下游
2119
- > 补一个兜底席。
2245
+ > 补一个兜底席;⑤**合规档否决记忆整理**(7.146.0 起):`COMPLIANCE_PROFILE=hipaa|zdr`(内建否决含 `memory_consolidation`),或
2246
+ > `COMPLIANCE_ADDITIONAL_DENIES` 含该词 —— 拒句点名三只旋钮;未设 `MEMORY_CONSOLIDATION_DRIVER` ⇒ 缺省派生关,手动整理口答
2247
+ > `501 feature.memory_consolidation_disabled`(句子说阀门关,不点名合规档)。档位否决时阀门结构上开不起来,引擎那一只 `409 memory.consolidation_refused` 在本服务的装配下不可达。
2120
2248
  > - `MEMORY_AUTO_CONSOLIDATION=on|off`(**未设 = 读引擎缺省**,见下):记忆**自动整理**的部署席
2121
2249
  > (core 7.21.0 的 `RunnerDeps.autoRunOnRecommendation` 席)。武装 ⇒ **每个产生了整理建议的任务
2122
2250
  > 之后**,为该 scope 起**一次**整库整理跑 —— 🔴 **记忆内容自动外流到配置的整理模型**。
@@ -2266,8 +2394,8 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
2266
2394
  > adb 经设备 shell、e2b 是建 / 恢复沙箱时的一次 `commands.run`),host 读它自己组合给子进程的那份 env 里的 `HOME`(会话 `settings.env` 改了 `HOME` 也算);
2267
2395
  > 与主张(`REMOTE_EXEC_HOME_DIR`,或 host 的本进程家目录 / e2b 的 `/home/user`)不一致(去掉尾斜杠后逐字比)或问不出 ⇒ 那只执行环境不开,拒句点名两个值与改法
2268
2396
  > (把 `REMOTE_EXEC_HOME_DIR` 设成实测值、删掉错的声明,或让环境的 `HOME` 读主张那个值)。host 车道的读数在铸环境时就到手 ⇒ 环境不铸,run 在开跑前失败
2269
- > (码 `env.home_unconfirmed`,同步腿 409;一条命令没跑、模型一次没调,命令带不带路径都是这一句;7.144.0 起,此前拒句落在工具结果里);远端五车道的读数要环境开起来才有 ⇒
2270
- > 拒句落在工具结果里,环境答了的不一致记在那只环境上(此后每次回同一句,不再起容器 / pod / 沙箱、不再问),问不出的那一次不记、下一次重问。e2b 恢复出来的沙箱对不上时重新挂起(快照留着),不杀。
2397
+ > (码 `env.home_unconfirmed`,同步腿 409;一条命令没跑、模型一次没调,命令带不带路径都是这一句;7.144.0 起,此前拒句落在工具结果里);远端五车道的环境也在开跑前打开、拒在那一拍判出,但 run 照样开跑 ⇒
2398
+ > 拒句落在工具结果里(带路径的命令可能先撞读边界、ssh / adb 上带附件的会话开跑前就失败、从快照恢复的续跑腿答 `resume.env_failed`,逐形见 §9 的 `env.home_unconfirmed` 行),环境答了的不一致记在那只环境上(此后每次回同一句,不再起容器 / pod / 沙箱、不再问),问不出的那一次不记、下一次重问。e2b 恢复出来的沙箱对不上时尝试重新挂起(挂起成功快照留着;挂起失败只记一行 warn,沙箱按自己的寿命到期),不杀。
2271
2399
  > e2b 的出厂答案 `/home/user` 已于 2026-10-29 在真沙箱上实测一致(缺省模板:用户 `user`、`$HOME` = `/home/user`);自定义模板换了用户的,`$HOME` 若不是它,没声明 `REMOTE_EXEC_HOME_DIR` 的 e2b 部署的每只沙箱建起来就拒,把声明设成沙箱里 `echo $HOME` 的结果(声明同样被对证)。
2272
2400
  > device 车道读不了家目录:声明即座,起服响一行 `execution_lane_home_unmeasured`(点名声明值与「未实测」)。
2273
2401
  >
@@ -2360,6 +2488,7 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
2360
2488
 
2361
2489
  **A. OA 系统(服务到服务后端)**
2362
2490
  - OA 后端持有 `SERVICE_AUTH_TOKEN`,每个请求按当前终端用户设 `x-agent-principal: user:<id>`。带了主体头的请求只看得到、动得到那位用户的(7.134.0 起 fleet 面同读);后台自己的全舰队运维视图**不带主体头**(或用只读 `TRACE_TOKEN`)。
2491
+ 部署设了 `SERVICE_TOKEN_ACTS_FOR_DEPLOYMENT=false` 时不带主体头的 service token 不再是全舰队视图(按端点换凭据,见 §「不报主体的 service token 是谁」)。
2363
2492
  - 短问答 → `/v1/tasks`(同步);长任务/要进度条 → `/v1/runs` + `/events`(异步)。
2364
2493
  - 续聊:把上次 `sessionId` 存在 OA 会话里,下次带回。
2365
2494
 
@@ -2377,7 +2506,7 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
2377
2506
  | 你看到 | 含义 | 怎么办 |
2378
2507
  |---|---|---|
2379
2508
  | HTTP `200` + `terminal.kind:"completed"` | 成功 | 取 `result` |
2380
- | `terminal.kind:"blocked"` | agent 主动报卡住 | 看 `terminal.reason`,补信息再发 |
2509
+ | `terminal.kind:"blocked"` | agent 主动报卡住(`terminal.detail` 缺席),或部署的 `userPromptSubmit` hook 拦下了这次主提交(`terminal.detail` 在场) | 有 `terminal.detail` 就渲它(`cause` / `hookReason`),没有才看 `terminal.reason`;补信息 / 改提示再发 |
2381
2510
  | HTTP `200` + `terminal.kind:"paused"` + 顶层 `status:"suspended"\|"needs_review"`(同步 `POST /v1/tasks`) | run **停在门上**(工具审批 / 计划审阅 / 资源分片),没跑完。🔴 **7.104.0 起**这张体就是那份结果本身(`terminal.gate` 说停在哪扇门、`checkpointId` 是对账位、`result`/`stats` 是停下来那一刻的读数),另叠 `taskId`/`sessionId`/`status` 三键 poll 回执 —— 修前只有这三键。恢复凭据**从不**在体上 | 按 `taskId` 轮询 `GET /v1/runs/:taskId`(park 中那一行同样带这份 `result`);审批走 `/v1/approvals`。判读用 `terminal.kind`,旧的 `status` 读法照旧成立。契约 §5.0c |
2382
2511
  | `terminal:{kind:"failed", code:"conflict"}` | 同一会话的两条写同时落在同一处(真并发:两处同时写一只会话),按叶的条件写只成一条;输的那一条以本码结束,撞上之后的内容没写进会话。多副本按任意路由轮流服务同一会话**不会**产生它(会话暖缓存命中前先核店里的当前叶,见 `SESSION_CACHE_TTL_SEC`) | 直接重试(重发接在会话当前的叶之后) |
2383
2512
  | `terminal:{kind:"failed", code:"limits.max_walltime_exceeded"}` | 墙钟到限(6.0.0 起 `status:"timeout"` 退役;7.64.0 起平面 `status` 键退役)。🔴 **7.71.0 / 引擎 7.13.0 收窄**:上限若在最后一轮 turn **已经给出干净答案之后**才响,run 记 `completed`、答案就是 `result`,**不铸本码**——那次停机的唯一痕迹是 `engine_notice` 的 `task.interrupt_unconsumed{origin:"walltime"}`(附录 D.3)。本码此后只在**工作被真切断**时出现。⚠️ 按码计数的运维面要把 `origin ∈ walltime\|turns` 的那一帧计入才守恒(server 侧对偶计量 `limit_ceiling_after_answer_total{origin}`,与 `budget_exceeded_total{code}` **相加**才是撞顶总量) | 拆小任务 / 提高 `limits.maxWalltimeMs`(毫秒) |
@@ -2386,14 +2515,18 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
2386
2515
  | HTTP `403` / `404`(session/run) | 不是该 principal 的资源 | 用正确身份 |
2387
2516
  | HTTP `409`(`/v1/runs`) | 同 session 已有活跃 run | 等它完成 / 用返回的 `activeTaskId` |
2388
2517
  | HTTP `409` + `errorCode:"conflict.idempotency_payload_mismatch"`(7.126.0 起;`POST /v1/tasks` · `/v1/tasks/stream` · `/v1/runs`) | `Idempotency-Key` 已绑定在一条你读得到的 run 上,而绑定它的那次请求的体与这次不同(规范化:按键排序、去掉 `taskId`;契约 §0.7「同一请求 = 同一份体」)—— 不回放、不新建、那一条不动,体不回显键与那一行;流式腿在开流之后才撞上时,是终帧 `done.result` 带同一 `errorCode` 的 `failed`。A2A 不会遇到 | 重试就重发同一份体;不同的请求换一把新键。与 `conflict.run_exists` 不同:后者是那一行你读不到、或带的 `taskId` 不是那一行 |
2518
+ | HTTP `409` + `errorCode:"conflict.session_id_retired"`(7.146.0 起;`POST /v1/tasks` · `/v1/tasks/stream` · `/v1/runs` 体里的 `sessionId`;续跑 / 唤醒 / 停驻赎回上是 `409 resume_blocked_by_policy` + `blockedBy:"conflict.session_id_retired"`;A2A 入口照它既有的拒形,JSON-RPC `-32602`) | 这只会话 id 属于一只**在本服务进程里被删掉**的会话,而这只会话现在的属主不是它删前的属主(无主与无主算同一位;同一只 id 在本进程被不同的属主各删过一次 ⇒ 谁都不是)—— 什么都没跑。新提交被拒时这只 id 已登记在你名下(一只空会话)。删前属主本人照常;判定只在删掉它的那个服务进程里、进程重启即消 | **换一个新的会话 id**(不要原样重发);想要好记的名字,自己记 name → uuid 的映射 |
2389
2519
  | HTTP `429` | 限流 | 看 `Retry-After` 退避 |
2390
- | HTTP `409` + `errorCode:"conflict.session_archive_stamped"`(7.144.0 起;`DB_BACKEND=local`;`POST /v1/sessions/:id/fork` 与会话同步导入的提交 `POST …/sync/import/:stagingId/entries`;7.141.0–7.143.0 答的码是 `conflict`) | 这只会话的存档仍带更早版本写下的属主戳(一次性迁移改写不了的,或跳过带迁移的版本升上来的数据根;起服 warn `local_archive_kept_stamped` 点名):不在它上面造新存档、不原地换它的日志(干净追加 / 载荷分叉 / fork / stale 各形的推送同答),什么都没写;读 / 续跑 / 拉取照常,「id 与内容都相同」的重推照常落定 | 按体里的固定句办:删掉这只会话(`DELETE /v1/sessions/:id`)再推。与暂存提交时属主变了的 `errorCode:"conflict"`(出路相反:从第一阶段重来)按码分开 |
2520
+ | 任一 4xx / 5xx 的 `errorCode`(7.146.0 起查契约附录 A 的**码族**列) | 目录每个码恰属五族之一:`transient`(原样重发等一等可能成)· `deterministic`(重发改不了判,别自动重发)· `store_judged`(逐次由店判,读体键 `permanence`)· `pending_decision`(卡仍待决,读体键 `retriable`:它说「卡还在」,不说「重发会成功」)· `retention_expired`(留存已被收走,新开) | 「要不要重试」「要不要重读待决列表」「要不要新开」按这一列读,不自己抄码表(契约文档附录 A.1g;MIGRATION 第 17 条)。表外码(core 原码镜像)没有族,照旧看状态与前缀 |
2521
+ | HTTP `429` + `errorCode:"limit.rate_exceeded"` + `reason:"run_concurrency_cap"` | 本副本在飞 run 已到并发帽(`RUN_MAX_CONCURRENT` 全局,或 `RUN_MAX_CONCURRENT_PER_PRINCIPAL` 按提交主体;缺省关;新提交的四个口,幂等重放不吃) | 看 `Retry-After`(5 秒)/ 体 `retryAfterSec` 退避后重投;或等在飞的 run 收尾 / 调高帽 |
2522
+ | HTTP `409` + `errorCode:"conflict.session_archive_stamped"`(7.144.0 起;`DB_BACKEND=local`;`POST /v1/sessions/:id/fork` 与会话同步导入的提交 `POST …/sync/import/:stagingId/entries`;7.145.0 起第一阶段 `POST …/sync/import` 对按 id 分类出 fork / stale 且没带同意的推送也直接答它,此前先答通用 `conflict`;7.141.0–7.143.0 答的码是 `conflict`) | 这只会话的存档仍带更早版本写下的属主戳(一次性迁移改写不了的,或跳过带迁移的版本升上来的数据根;起服 warn `local_archive_kept_stamped` 点名):不在它上面造新存档、不原地换它的日志(干净追加 / 载荷分叉 / fork / stale 各形的推送同答),什么都没写;读 / 续跑 / 拉取照常,「id 与内容都相同」的重推照常落定 | 按体里的固定句办:删掉这只会话(`DELETE /v1/sessions/:id`)再推。与暂存提交时属主变了的 `errorCode:"conflict"`(出路相反:从第一阶段重来)按码分开 |
2391
2523
  | HTTP `429` + `errorCode:"limit.cost_quota_exceeded"` | per-principal 累计成本配额越顶(`MAX_PRINCIPAL_COST_USD`;**进场门**,不打断在跑的 run) | 看 `Retry-After` / 体 `retryAfterSec` 退避(体 `windowResetsAt` = 当代窗滚过的时刻);窗滚过或调高上限后放行 |
2392
2524
  | 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 …`),只作展示、别锚文案 |
2393
2525
  | 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` 分支,句子只作展示 |
2394
2526
  | 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` 分支,句子只作展示 |
2395
2527
  | 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` 改那一处(合并 / 拆小条目、缩短文字、修那一条路径);句子只作展示 |
2396
2528
  | 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` 定位到那几个键;句子只作展示、别锚文案 |
2529
+ | HTTP `400` + `errorCode:"request.env_credential_refused"`(7.146.0 起) | 请求 `settings.env` 里有凭据:名字是本引擎的凭据变量(配置目录 `secret` 行 ∪ 模型条目 `apiKeyEnv`)或值是秘密形。体带 `field: "settings.env"` 与 `entries: [{ key, match }]`,值不回显。把这些条目挪进引擎进程的环境后重发(见 9.5j);停驻任务窗关后的续跑答 409 `resume_blocked_by_policy`(`blockedBy` = 本码) |
2397
2530
  | HTTP `400` + `errorCode:"config.tool_face_control"`(7.126.0 起;流式 / 后台腿的引擎侧拒是 `failed` 终局带同码,受理口拒三条腿都是提交时 400) | 请求的 `allowTools` / `excludeTools` 里有一个**读成空**的条目:任何工具名都不可能有的拼法(空白、括号不配、空名)、`allowTools` 写了 `mcp__*`(引擎拒)、`excludeTools` 够着了这一腿依赖的协议工具(`*` / 前缀 glob / 直接点名都算,原因词 `protocol_denied`;`excludeTools` 写 `*` 本身不拒 —— 7.137.0 起限制侧名内的 `*` 是整名 glob,卸每一只工具),或(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` 后的现名(或删掉条目);只列这台部署挂得出的工具名 |
2398
2531
  | HTTP `400` + `errorCode:"config.agent.unknown_tool"`(7.131.0 起) | 请求 `agents[i].denyTools`(限制侧)里有读成空的条目:近似名(`BASH`)、退役名(`bash`)、任何工具名都不可能有的拼法;`agents[i].allowTools`(放行侧)里的**近似名**。开跑前拒、主模型零调用、一个 token 没烧,句子点名条目、原因词与现名。限制侧 `Tool(content)` 的内容不读(读成整只工具)、名内的 `*` 是整名 glob(7.137.0 起,孤 `*` = 每一只工具)、限制侧整个协议的拒 `mcp__*` / `a2a__*`、真未知名(`NotMounted`,照跑、告运维一次)不拒;放行侧的内容形(`Agent(Explore)`)与没有后继的退役名(`MultiEdit`)照 7.130.0 受理、交引擎既有通告,选择器清单里**有后继的退役名**(`Task` · `bash`)7.132.0 起由引擎开跑前拒;文法读不出的放行条目授不出去 = 收窄而不是放行:规则表(`permissions.allow` / 技能 `allowedTools`)上受理口不拒、引擎开跑时告运维;选择器清单(根 / 子代 `allowTools`)上受理口不拒、引擎开跑前自己拒(子代 ⇒ `config.agent.unknown_tool`、根 ⇒ `config.tool_face_control`,同步腿都是 `400`〔7.132.0 起;此前子代这一形答 HTTP 200 + failed 终局体〕;异步 / 活流腿 HTTP 2xx、同码落在终态 run 行) | 改成句子里 `write` 后的现名,或删掉条目 |
2399
2532
  | HTTP `400` + `errorCode:"prompt.assembled_role_refused"`(7.132.0 起;流式 / 后台腿是 `failed` 终局带同码) | 请求的 `systemPrompt` / `appendSystemPrompt`(含 `settings.outputStyle`)逐字含引擎提示里的一段锁定文本(宪法、安全段、`# Harness` 这样的标题行)—— 一份已组装的提示。开跑前拒、零调用,句子点名字段与命中段 | 只写角色层:删掉抄进来的引擎段(引擎自己组宪法与行为段;追加段恒排在宪法之后) |
@@ -2404,14 +2537,14 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
2404
2537
  | HTTP `422`(`request settings cannot be applied as written: …`)+ `detail.problems[].reason:"home_unresolvable"`,句子说 `user-settings path rule rooted at the configuration directory`(7.141.0 起) | 请求 `settings.permissions.deny` / `ask` 里一条 `layer:"userSettings"` 的 `/` 形路径规则,而这条沙箱车道既没把 `SEMA_CONFIG_DIR` 转进沙箱、也没有可知的 home(k8s / local-docker / ssh / adb / device 且未声明 `REMOTE_EXEC_HOME_DIR`)—— 配置根读不出,不猜目录(续跑同体 409 同句) | 往沙箱 env 名单里加 `SEMA_CONFIG_DIR`,或声明 `REMOTE_EXEC_HOME_DIR`,或把规则写成 `//` 绝对形 / 放进项目层(读在任务根);见 9.4d |
2405
2538
  | HTTP `429` + `errorCode:"usage.window_exhausted"` | 部署级治理窗耗尽(`USAGE_WINDOWS`,token 或 $ 天花板先满者) | 看响应体 `retryAfterSec` 退避;窗滑动/桶到期后放行 |
2406
2539
  | `terminal:{kind:"failed", code:"usage.window_exhausted"}` | 已受理的 run 在 **turn 边界**撞上治理窗且无法 durable 挂起 | 同上退避后重投;配好 checkpoint 基建则改为 `suspended` 等窗自动续跑 |
2407
- | HTTP `409` + `errorCode:"env.home_unconfirmed"`(同步 `POST /v1/tasks`;流式 / 后台腿是 `terminal:{kind:"failed", code:"env.home_unconfirmed"}`;7.144.0 起) | 执行环境的家目录主张证不了:部署给环境的家目录(`REMOTE_EXEC_HOME_DIR`,没设时 host 车道 = 服务进程的家目录)与环境里命令看到的 `HOME`(含会话 `settings.env` 里写的 `HOME`)对不上 / 读不出 / 不是绝对路径 ⇒ 环境在开跑前就不铸:一条命令没跑、模型一次没调、一个 token 没烧,不管模型要跑的命令带不带路径都是这一句(句子点名两个家目录与改法)。今天只有 host 车道(含 `run-local`)这样答;远端五车道照旧在开环境那一拍拒、同一句落在工具结果里(读边界要先读环境才判得了的命令 —— 递归读,adb 上连带路径的普通读 —— 在那里先被读边界拦下,句子不点名家目录) | 把 `REMOTE_EXEC_HOME_DIR` 设成环境里 `echo $HOME` 的结果(或删掉它),或让环境的 `HOME` 等于声明,或去掉会话 `settings.env` 里的 `HOME`;原样重试不改判 |
2540
+ | HTTP `409` + `errorCode:"env.home_unconfirmed"`(同步 `POST /v1/tasks`;流式 / 后台腿是 `terminal:{kind:"failed", code:"env.home_unconfirmed"}`;7.144.0 起) | 执行环境的家目录主张证不了:部署给环境的家目录(`REMOTE_EXEC_HOME_DIR`,没设时 host 车道 = 服务进程的家目录)与环境里命令看到的 `HOME`(含会话 `settings.env` 里写的 `HOME`)对不上 / 读不出 / 不是绝对路径 ⇒ 环境在开跑前就不铸:一条命令没跑、模型一次没调、一个 token 没烧,不管模型要跑的命令带不带路径都是这一句(句子点名两个家目录与改法)。今天只有 host 车道(含 `run-local`)这样答;远端五车道的环境也在开跑前打开、拒在那一拍判出,但 run 照样开跑:不带路径的命令拿到同一句(工具结果里);读边界要先读环境才判得了的命令先被读边界拦下、句子不点名家目录(路径不在读拒表上时;全 shell 面要人批:`~/` 下的递归读五条车道都是,`~/` 下的普通读在 ssh / adb,工作区内相对路径的普通读在 adb;`AUTONOMY=read-only` 上 adb 的工作区相对路径读被拒);ssh / adb 上会话绑了附件 ⇒ 开跑前 `failed`、终局不带码(句子以 `attachment materialization failed` 起头);从快照恢复的续跑腿(e2b;配了快照存储的 k8s)⇒ `resume.env_failed` | 把 `REMOTE_EXEC_HOME_DIR` 设成环境里 `echo $HOME` 的结果(或删掉它 —— host / e2b 删掉后主张回落车道的出厂答案,仍要与 `HOME` 一致),或让环境的 `HOME` 等于声明,或去掉会话 `settings.env` 里的 `HOME`;原样重试不改判 |
2408
2541
  | HTTP `501`(`/v1/runs`) | 内存模式不支持异步 | 配持久存储(`DB_BACKEND=mysql` / `pg` + 坐标;7.106.0 起给了坐标必须点名 `DB_BACKEND`) |
2409
2542
  | HTTP `500` + `errorCode:"corrupt_session"`(7.104.0 起;流式口在应答头之后则是 `done` 帧的 `result.terminal.code`,后台 run 是 run 行的 `errorCode`) | 这只会话的**存档读不出**、唤不醒(文件后端:会话日志没有会话头;SQL 后端:一行事件读不出);或(7.144.0 起)存着的日志读得出、**走不成一棵树**:某条目的父不在日志里而回放没丢记录(悬空)、条目是自己的祖先(自指 / 成环)、叶子不在日志里(7.137.0–7.143.0 这一形答 `invalid_session`;句子点名条目 id,读整条分支的口 —— 会话视图 `GET /v1/sessions/:id` / `…/memory-status` —— 同答,事件订阅与头探针只读会话头的叶子,照旧 200 / 起流)。这一轮没跑,一个 token 没烧。直接读这只会话的口(头探针 / 事件订阅 / 会话视图 / 改名 / 导出 / 分叉 / 2c 同步)同答(契约 §17;文件后端上事件订阅与改名 7.136.0 起同答,此前一个 200 起流却零帧、一个 404)。会话列表 `GET /v1/sessions` 里它仍是一行,行带 `unreadable: { errorCode }`,码就是这些口对它答的那一个(文件后端,7.142.0 起;此前它从列表里消失;存档文件本身读不出 —— 权限 / I/O —— 时码是 `internal.error`) | **别重试**(确定性失败)。**文件后端**:运维看 `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`)。要继续工作就换一个新会话 |
2410
2543
  | 流里一帧 `engine_notice` `code:"session.lineage_rerooted"`,`detail:{ break, recordsDropped, recordsTotal, leaf, entriesOnPath, sessionId, runId }`(7.144.0 起,只在文件后端;7.104.0–7.143.0 这件事由 `session.records_skipped` 说,已退役) | 这只会话的日志回放时有 `recordsDropped` 条记录读不出、被跳过了;这一轮**照跑**,模型读到的历史是 `entriesOnPath` 条。`break: true` = 被跳过的是分支上的祖先(`missingParentId`),这一轮从点名它作父的那一条(`rootedAt`)续上、`entriesCut` 条更早的条目不在上下文里(7.143.0 及以前这一形整轮失败);`break: false` = 分支完整,从 `leaf` 续上(`leaf: null` = 一条都没剩,从空对话起跑)。日志没修好之前每一轮都会再说一次(会话缓存命中的轮次照样说);进程已持有这只会话时盘上才坏的行,这一轮读的是内存里那份完整历史、不说,重启后的第一轮才说 | 告诉用户「这段对话的一部分历史丢了」;运维按 `fail_open_total{tag="server.session-repo.corrupt-record-skipped"}` 的计数与探针文件(`SEMA_FAILOPEN_PROBE=<文件路径>`,每命中一行)里的逐条明细(文件路径、会话 id、行号、成因)查那份文件。非流式 `POST /v1/tasks` 收不到通告帧(只有运维面的计数与探针) |
2411
2544
  | 流里一帧 `engine_notice` `code:"route.finish_reason_unknown"`,`detail:{ finishReason, sessionId, runId }`(7.126.0 起) | 提供方用一个引擎读不懂的结束原因结束了这一轮:回答按模型留下的样子保留,但可能被提供方截短了 | 告诉用户「这一段回答可能不完整」;同一个词同一 run 只说一次。运维按 `finishReason` 查是哪家网关 / 模型发的 |
2412
2545
  | 流里一帧 `engine_notice` `code:"route.stream_end_ignored"`,`detail:{ statement, word, frames, sessionId, runId }`(7.126.0 起) | 提供方已经结束了这一轮,它的流却又发来了东西(又一个结束 / 一个错误 / 内容);第一个结束算数,之后的没进这一轮 | 一般无需处置;频繁出现 ⇒ 运维查那条网关的流式实现(`statement` / `word` 说的是迟到的第一帧) |
2413
2546
 
2414
- 会话流 `engine_notice` 帧的**全部**可投码 = 随包 `dist/trace/engine-notice-wire.d.ts` 里的 `ENGINE_NOTICE_WIRE_CODES`(码表本身,只列码名;每一帧自带 `message` 与 `detail`);逐版增删见 `wire-changes/server@<版本>.json` 的 `closedSetMembers` 里 `set` 为 `engineNotice` 的那一项(该版没有增删就没有这一项);本表只列运维最常问的几条。其中 `delegation.ask_unresolvable` = 一只调用被最终拒而**没有任何人裁决过它**(`detail.cause` / `parkLaneExisted` / `remedies` 分开读),委派子代上的这一条今天不上父会话流。
2547
+ 会话流 `engine_notice` 帧的**全部**可投码 = 随包 `dist/trace/engine-notice-wire.d.ts` 里的 `ENGINE_NOTICE_WIRE_CODES`(码表本身,只列码名;每一帧自带 `message` 与 `detail`);逐版增删见 `wire-changes/server@<版本>.json` 的 `closedSetMembers` 里 `set` 为 `engineNotice` 的那一项(该版没有增删就没有这一项);本表只列运维最常问的几条。其中 `delegation.ask_unresolvable` = 一只调用被最终拒而**没有任何人裁决过它**(`detail.cause` / `parkLaneExisted` / `remedies` 分开读),委派子代上的这一条投委派树根会话(父会话的流),被拒那只调用所在的子代会话在 `detail.sourceSessionId`、代理 id 在 `detail.agentId`;判「是谁的调用被拒」读 `detail.sourceSessionId ?? detail.sessionId`。
2415
2548
 
2416
2549
  **机器码**:每个 4xx/5xx 响应体都带一个 `errorCode`(与人类文案 `error` 并列),这是**唯一**该拿来做
2417
2550
  程序分支的字段——**别锚 `error` 文案**。前缀族固定:`auth.` / `request.` / `not_found.` / `conflict.` /
@@ -2585,7 +2718,7 @@ k8s 车道每条 run 一只新 pod(pod 寿命 = 泄漏护栏,不跨 run 复用)
2585
2718
  **不回落**成把值写进 pod spec。它与 `SANDBOX_PKG_SOURCE` 推导出的包源变量在装配处分座一次,**同名时名单赢**(派生的那条不出现;部署方的刻意覆盖);
2586
2719
  适配器不把值拼进执行命令串、不写进 `/workspace`,因而不随跨轮保留 / 挂起的快照进对象存储;不进事件日志(起服行只报键名)。
2587
2720
  pod 里的进程读得到这些值 —— 那正是转发的目的;进程自己若把值写进 `/workspace`,它就会随快照走。
2588
- - **RBAC**:沙箱命名空间里服务账号要有 `secrets` 的 `create`(Role 规则 `- apiGroups: [""] resources: [secrets] verbs: [create]`;随包 `deploy/sema-up` 的 Role 已带)。
2721
+ - **RBAC**:沙箱命名空间里服务账号要有 `secrets` 的 `create`(Role 规则 `- apiGroups: [""] resources: [secrets] verbs: [create]`;随包 chart 在 `sandbox.sandboxEnv` 非空时带这一条〔同一个值同时写引擎的 `K8S_SANDBOX_ENV`〕,cluster 静态清单里它是注释块、给 server 设了 `K8S_SANDBOX_ENV` 时取消注释)。
2589
2722
  它恰是服务为 env Secret 真发的调用;不要给 `get`(服务从不读回,给了就读得到命名空间里每一只 Secret)与 `delete`(Secret 随属主 pod 由集群回收)。租约 Secret 的 `patch` 只在 §9.3h 的平面打开时要。
2590
2723
  名单非空时起服各问一次 SelfSubjectAccessReview:答了「不许」⇒ **拒启**,拒句带 `[sandbox-secret-rbac]`、点名缺的动词与这条规则;问不到(API 连不上 / 审查本身被拒)⇒ 照常起服,
2591
2724
  一行 `sandbox_secret_seat_unverified` + fail-open 计数 `server.sandbox-secret.seat-unverified` —— 那时若 Role 真缺,第一个要转发的 run 在连接时被拒、它的 pod 被删。
@@ -2633,7 +2766,7 @@ k8s 车道每条 run 一只新 pod(pod 寿命 = 泄漏护栏,不跨 run 复用)
2633
2766
  | `WORKSPACE_CACHE_SIZE` | `20Gi` | 卷的申请量(k8s quantity 形,如 `20Gi` / `500Mi`;形不合 ⇒ 拒启) |
2634
2767
  | `WORKSPACE_CACHE_MOUNT` | `/cache` | 卷在 pod 里的挂载点(绝对路径;与工作区挂载 `K8S_MOUNT_PATH` 相同 / 互相嵌套 ⇒ 拒启;落在保留的镜像目录闭集 —— `/bin /boot /dev /etc /home/sandbox /lib /lib64 /opt/sema /proc /root /run /sbin /sys /tmp /usr /var` —— 上 / 之下 / 之上 ⇒ 拒启并点名那一项:卷挂在那里会遮住镜像在那条路径下放的东西) |
2635
2768
 
2636
- **RBAC**:沙箱 Role 要有 `persistentvolumeclaims` 的 `get` / `create` / `delete` / `deletecollection`(回收按标签删 = `deletecollection`;chart 与 cluster 静态清单 7.126.0 起已带)。
2769
+ **RBAC**:沙箱 Role 要有 `persistentvolumeclaims` 的 `get` / `create` / `delete` / `deletecollection`(回收按标签删 = `deletecollection`;随包 chart 在 `sandbox.workspaceCacheVolume: true` 时带〔同一个值同时写引擎的 `WORKSPACE_CACHE=volume`〕,cluster 静态清单里它们是注释块、给 server 设了 `WORKSPACE_CACHE=volume` 时取消注释;没开缓存卷的部署 Role 里没有这四个动词)。
2637
2770
  **能力位**:`GET /v1/capabilities.workspaceSync.cache = { mount, size }`,present-iff k8s 车道 ∧ `WORKSPACE_CACHE=volume`(与快照存储无关:没配存储而开了卷的部署,
2638
2771
  `workspaceSync` 只有 `cache` 一个成员 —— 读跨轮保留一律读 `persist === "space"`)。
2639
2772
 
@@ -3026,10 +3159,10 @@ WebSearch 工具的 `tool_result` 错误文本,回到模型的对话里,**不是
3026
3159
  `http` 条目 header **值**里的 `$NAME` / `${NAME}` 按条目的 `allowedEnvVars` 展开,读法是契约包 settings-schema 的 `interpolateHookEnvVars`(壳与本服务同调,同一份钩子定义到端点的字节相同):
3027
3160
  - 名字只认大写 `[A-Z_][A-Z0-9_]*`;小写名(`$my_token`)是普通文字,原样发出。
3028
3161
  - 列在 `allowedEnvVars` 里 ⇒ 展开成钩子环境里的值(钩子环境 = 净化后的进程环境 allowlist + 同请求 `settings.env`;服务进程的密钥不在其中);钩子环境里没有这个名字 ⇒ 空串。
3029
- - 名单外的引用 ⇒ 空串;凭据名表里的名字(契约包 `HOOK_ENV_CREDENTIAL_NAMES`:`CLAUDE_CODE_OAUTH_TOKEN` 一类 27 个名字 × 裸名与 `INPUT_` 前缀;**不含** `ANTHROPIC_API_KEY` / `MODEL_API_KEY` 这类模型凭据名)一律空串,点进名单也一样。本服务自己的模型凭据不靠这张表护:它们从不进钩子环境(钩子环境 = 净化后的进程环境 allowlist + 同请求 `settings.env`,服务进程的密钥名在扣留名单里),所以 `${ANTHROPIC_API_KEY}` 在本服务上展开成空串是因为钩子环境里没有它,不是因为它在凭据名表里;调用方自己在 `settings.env` 里给的同名变量会照常展开。
3162
+ - 名单外的引用 ⇒ 空串;凭据名表里的名字(契约包 `HOOK_ENV_CREDENTIAL_NAMES`:`CLAUDE_CODE_OAUTH_TOKEN` 一类 27 个名字 × 裸名与 `INPUT_` 前缀;**不含** `ANTHROPIC_API_KEY` / `MODEL_API_KEY` 这类模型凭据名)一律空串,点进名单也一样。本服务自己的模型凭据不靠这张表护:它们从不进钩子环境(钩子环境 = 净化后的进程环境 allowlist + 同请求 `settings.env`,服务进程的密钥名在扣留名单里),所以 `${ANTHROPIC_API_KEY}` 在本服务上展开成空串是因为钩子环境里没有它,不是因为它在凭据名表里;7.146.0 起调用方在 `settings.env` 里给这类名字(或任何秘密形的值)新提交即 `400 request.env_credential_refused`(9.5j)。
3030
3163
  - 每个 header 值剥 CR / LF / NUL;header 名不展开。
3031
3164
  - 置空(名单外 / 凭据名)时日志一行 `hook_http_header_env_blanked`(名字与原因,不带值)。
3032
- 例:`"headers": { "Authorization": "Bearer ${HOOK_TOKEN}" }`、`"allowedEnvVars": ["HOOK_TOKEN"]`,并在请求 `settings.env` 里给 `HOOK_TOKEN`。7.125.x 及更早的读法不同(升级见 MIGRATION)。
3165
+ 例:`"headers": { "Authorization": "Bearer ${HOOK_TOKEN}" }`、`"allowedEnvVars": ["HOOK_TOKEN"]`,并在请求 `settings.env` 里给 `HOOK_TOKEN`(7.146.0 起值是秘密形的 token 不能经 `settings.env` 给,今天没有替代通道 —— 9.5j 已知限制)。7.125.x 及更早的读法不同(升级见 MIGRATION)。
3033
3166
  **两张名单 `settings.allowedHttpHookUrls` / `settings.httpHookAllowedEnvVars`(7.142.0 起;CC settings 同名键)**:读法与判决都在契约包 settings-schema,本服务只在 http 条目**拨出之前**问一次(能力位 `taskSettings.httpHookAllowlists`,单用户车道为真):
3034
3167
  - `allowedHttpHookUrls`(URL 模式的串数组,例 `["https://hooks.example.com/*"]`)在场 ⇒ 条目 `url` 必须命中其中一条才发出;不命中 / 名单是 `[]` / 名单不是串数组 ⇒ **一个字节都不发**。非托管组:这次 hook 算非阻断故障(fleet 流 `hook_notice` `url_not_allowed`,运维 warn `hook_http_withheld`);
3035
3168
  托管组(`source: managedPolicy` / `managedPlugin:*`):失效关闭,`PreToolUse` 在没有否决时改问人。句子点名 `settings.hooks.<事件>` 与规则(`url` / `empty` / `malformed`)。出路:把地址的模式加进名单,或改 hook 的 `url`。
@@ -3039,14 +3172,18 @@ WebSearch 工具的 `tool_result` 错误文本,回到模型的对话里,**不是
3039
3172
 
3040
3173
  ### 9.5c-bis `settings.hooks` 的执行模型与时限(7.132.0 起)
3041
3174
 
3042
- - 被采纳的 `settings.hooks`(单用户车道)在引擎里是**一张**扩展注册,名字 `settings.hooks`(`wiring_manifest.mods[]` 里看得到它;
3043
- 审批卡 `allowRuleShadowed` 的 `hook` 成员、钩子失败时模型读到的拒句都点名它)。本部署自己另挂两张只观察的注册:恒挂的拒绝计量
3044
- `server.permission-denied-meter`,`TOOL_TRACE=true` 时的 `server.tool-trace`(品牌部署另有一张提示装配站上的 `server.brand-identity`,只答品牌行 overlay,不在工具门上)。观察者的判词不采纳,链里唯一的答者是请求的钩子。
3175
+ - 被采纳的 `settings.hooks`(单用户车道)在引擎里是**每条车道一张**扩展注册:非托管车道 `settings.hooks`;有托管组(`source` 为 `managedPolicy` / `managedPlugin:*`,见 9.5c-quater)时
3176
+ 另一张托管车道 `settings.hooks.managed`,排在前面(7.146.0 起;此前恒一张)。`wiring_manifest.mods[]` 里看得到它们;审批卡 `allowRuleShadowed` 的 `hook` 成员、问因项、钩子失败时模型读到的拒句都点名作答 / 失败的那一张。本部署自己另挂两张只观察的注册:恒挂的拒绝计量
3177
+ `server.permission-denied-meter`,`TOOL_TRACE=true` 时的 `server.tool-trace`(品牌部署另有一张提示装配站上的 `server.brand-identity`,只答品牌行 overlay,不在工具门上)。观察者的判词不采纳;本服务自铸的注册在工具门(`classic.*` 站)上没有链成员(品牌行那一张在提示装配站 `prompt.compose` 上是链成员,不碰工具门;运维声明的 mod 包可以有,见 9.5c-quater 末条)。
3045
3178
  - 每个事件的语义与此前相同:`Stop` 条目在主任务结束与委派子代结束时都跑;`PermissionDenied` / `SubagentStart` / `SubagentStop` / 两个 model-switch 事件属壳,服务端不跑。
3046
- - **`HOOKS_TIMEOUT_MS`** = 这张注册的 `budgetMs`。引擎对它的定义是 "Each handler's own time" —— 每个 handler 自己的时间(一个事件跑一遍本请求的钩子条目;
3179
+ - **`HOOKS_TIMEOUT_MS`** = 每张车道注册的 `budgetMs`(两张各一份同值;一个事件最坏 2 倍)。引擎对它的定义是 "Each handler's own time" —— 每个 handler 自己的时间(一个事件跑一遍这条车道的钩子条目;
3047
3180
  引擎的钟,handler 等 `next` / `$` 调用的那段暂停),**不是**钩子子进程的墙钟。**未设 = 600000 ms**(= 单条命令超时的上限;引擎的钟在 handler 起跑时就开始走,
3048
3181
  命令的 `timeout` 钟要等 shell 解析和起进程之后,所以 `timeout` 配到 600 s 的命令跑满时,预算会先到);`0` 按字面(零预算)。超预算:`PreToolUse` 上该调用被拒(fail-closed,拒句点名 `extension "settings.hooks"`)、
3049
- `UserPromptSubmit` 上该提交被拒,其余事件跳过这一轮钩子。子进程自己的墙钟另有两只,照旧:单条命令的 `timeout`(上限 600 s)与一个事件 120 s 的总墙钟(到点不再起下一条)。
3182
+ `UserPromptSubmit` 上该提交被拒,其余事件跳过这一轮这条车道的钩子。托管车道是外层(先等非托管答完,等的那段引擎停它的表):非托管条目吃光的只是非托管那张的预算,
3183
+ 其余事件上托管钩子照跑;托管车道自己的预算用尽 ⇒ 撞上的托管条目发 `hook_notice` / `hook_managed_unrun{reason:"budget_exhausted"}`(`detail` 点名引擎预算;每个事件恰一帧,任务取消不记)。
3184
+ `Stop` / `SubagentStop` 上每条车道的 `continue:false` 只撤本车道的推回,别的车道与部署座链成员的推回照收(两车道意见相左 ⇒ 这一轮不收场);
3185
+ 观察站(`PostCompact` / `StopFailure`)上托管车道先跑、非托管后跑。
3186
+ 子进程自己的墙钟另有两只,照旧:单条命令的 `timeout`(上限 600 s)与每条车道一个事件 120 s 的总墙钟(到点不再起下一条)。
3050
3187
  - **预算到、或任务被取消 ⇒ 这个 handler 里在跑的命令钩子被叫停**(7.133.0 起;此前它要跑到自己的 `timeout` 才停,取消之后还可能接着写文件):进程组先收 SIGTERM,
3051
3188
  1 秒到点组长还在就补一发组 SIGKILL(组长在宽限内退了就不再发 —— 退出的进程号可能已被回收;代价:组长一退,组里屏蔽 SIGTERM 且不攥管道的后代不再被追杀),
3052
3189
  进程组长退出才算收走;这个 handler 里还没起的后续条目不再起。结局不变(上一条的 fail-closed 拒 / 跳过),运维日志多一行
@@ -3062,6 +3199,26 @@ WebSearch 工具的 `tool_result` 错误文本,回到模型的对话里,**不是
3062
3199
  出路:删掉那个键,或改成读者读的拼法;拒体里的坐标就是要动的那一处。
3063
3200
  - 停驻任务续跑时不判这一条(存量请求里的这些键照旧被剥掉、其余钩子照采纳)。
3064
3201
 
3202
+ ### 9.5c-quater `settings.hooks` 的托管标签:调用方自报,服务端只校闭集、不验身份,失败方向只收紧(7.133.0 起)
3203
+
3204
+ - **托管标签是调用方自报的,服务端只校闭集、不验身份**:组上的 `source` —— 托管 = `managedPolicy` / `managedPlugin:*`,非托管 = `user` / `project` / `local` / `flag` / `plugin:*` 与缺席 ——
3205
+ 由请求体里的调用方写,服务端只判它在不在闭集里(集外词 / 形错 ⇒ `400 request.field_invalid`,`field: "settings.hooks"`),**不验身份**,也不按调用方的档位剥掉、改写或拒绝托管词。
3206
+ 本服务没有用户 / 项目 / 本地 / 托管层的设置文件:请求体 `settings` 是启动方(壳 / 嵌入方)端合并之后交来的那一份,所以「托管」今天的唯一来源就是壳 / 嵌入方的自报;
3207
+ `settings.hooks` 只在单用户车道被采纳(多租户车道整只忽略),能自报的就是持有这台部署凭据的调用方(壳 / 嵌入方)。不要把它读成「组织策略来源」的证明。
3208
+ 标签只改两件事,失败方向**只收紧**:① 位置 —— 非托管相 → 托管插件相 → 托管策略相(终判相),托管条目在非托管改写之后的最终输入上判,托管相自己改写了输入则整相按新输入重判一遍;
3209
+ 所以托管条目的 `allow` 绑定的是最终要执行的那份输入(一条非托管 `allow` 若排在另一条非托管改写之前,会因输入被改而作废;托管的不会 —— 它是改写之后才判的,效果等同于调用方把同一条钩子不带标签写在数组末尾);
3210
+ ② 失败方向 —— 托管条目没跑成、或判词不可信,`PreToolUse` 折 `ask`(非托管相里没跑成的条目照旧只是无意见;判词是闭集外的词则两相都折 `ask`);引擎 handler 预算、事件墙钟、prompt 条数、上下文份额各车道独立计量(每条车道一张注册)。
3211
+ **`allow` 不看相**:放行只看「判词是 `allow` ∧ 判词可信 ∧ 它判过的就是最终输入」,出口对象不带车道 —— 同一条钩子、同一个判词、同一份输入,自报 `managedPolicy` 拿不到任何额外放行(任务座之下有部署座链成员的站上同样如此 —— 见本节末条)。
3212
+ 所以调用方给自己的钩子写 `source: "managedPolicy"` 不是抬权口,而是「请按失效关闭跑这一组、并让它最后判」的开关;不要这层严格,去掉 `source` 即可(缺席 = 非托管)。
3213
+ 过渡(7.146.0 起,至引擎为钩子提供「请求收场」席位止):同一请求同时带托管与非托管的 Stop 钩组、且两车道意见相左(一边 `continue:false`、一边 `block`)时,`continue:false` 只撤本车道的推回,另一车道的 `block` 作数 —— 这是托管标签在 Stop 上会改变的第三件事。目标形 = 任一 `continue:false` 即请求钩子整体无意见(7.145.0 形,与 CC 一致),席位到货当版恢复并删本句。
3214
+ - **任务座之下有部署座链成员的站上,托管条目只许收紧**(7.146.0 起):运维声明的 mod 包(`MODS_ROOT` / `MODS_PACKAGES`)在 `classic.PreToolUse` 上以 `append` 档(排在请求钩子所在的任务座之后)挂了链成员
3215
+ (观察者不算;读引擎的准入记录)⇒ 托管车道照旧最后判,但那一站上只许收紧:否决 / 问(含托管条目没跑成折的问)/ 上下文照收;托管的 `allow` ⇒ 这次调用拒(与不带标签的 `allow` 被引擎座信任律拒同一个结局,不落到常规门);
3216
+ 托管的 `updatedInput`(带 `ask` 的也算)给不进运维 handler(它只判过改写之前的那一份)⇒ 这次调用拒(失效关闭);运维 handler 要真人批的问,托管作答之后照旧要真人批。
3217
+ 否则自报的托管组会等运维 handler 判完原输入、再在它之上放行或改写 —— 改写后的输入运维 handler 从没见过。在装了运维 mod 的部署上,覆盖站上的标签换不来任何额外放行(与不带标签同;与 7.145.x 同)。
3218
+ 挂法不随站点变:同座的非托管车道与运维 handler 恒先判、否决恒跑 —— 运维 mod 只管某些工具(带匹配器)时,别的工具的调用上同样如此,那些调用上托管条目也按这一条收紧(准入记录不带匹配器;只多收紧,不跳过否决)。
3219
+ 不带托管标签的组照旧:运维 handler 在这一次调用的链上时,它们的非否决答复被引擎的座信任律拒。
3220
+ 只挂观察者的部署(本服务自铸的拒绝计量 / 工具追踪)、链成员只在任务座之上(`prepend` 档,或同档 `user` —— 同档里部署座在前)的部署不收紧:那里运维 handler 已先判、并在答回来的结果上再判。
3221
+
3065
3222
  ### 9.5d MCP 服务器 http 传输的身份头:`principalHeader` 与 `assertionHeader` 二选一(请求体与部署声明同一种读法)
3066
3223
 
3067
3224
  `mcpServers[].transport`(`kind:"http"`)上有两个「用哪个头送身份」的可选键,**只能设一个**:`principalHeader`(runner 把该任务已认证的终端用户 principal 注入这个头)与 `assertionHeader`(托管连接器断言所用的头**名**)。
@@ -3169,6 +3326,22 @@ WebSearch 工具的 `tool_result` 错误文本,回到模型的对话里,**不是
3169
3326
  仍会被座拒(`credential_unavailable` + 一行 `peer_seat_refused`,`reason: handle_not_held`)的窄形:被取消的父腿里一次工具调用起的会话级子代,在那条腿已放之后才取得持有(取消的孤儿窗);
3170
3327
  一条永不落定的准备腿永不放(条目随之常驻,`peer_registry_holders` 高于基线不回落即此形)。
3171
3328
 
3329
+ ### 9.5j 请求的 `settings.env` 不收凭据(7.146.0 起,新提交 400)
3330
+
3331
+ **律**:凭据进引擎只经引擎进程启动时的环境,从不经请求。`settings.env` 来自一份模型读得到的设置文件,而本服务把它交给会话的 shell(单用户 host / device 车道原样叠进模型 shell 的环境 ——
3332
+ 调用方显式授予的变量不按本服务的自有凭据表扣)与 settings hooks(命令钩子的环境、http 钩子的 `$NAME` 插值),所以这一块不许有凭据。
3333
+ - **判据一只**,读会交到子进程的那几条(非串值、超帽的键 / 值本来就到不了子进程):名字是本引擎的凭据变量 —— 配置目录(`GET /v1/config/catalog`)里 `secret` 型的全部行
3334
+ (`MODEL_API_KEY` / `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` / `SERVICE_AUTH_TOKEN(S)` …)∪ 生效配置每条模型条目的 `apiKeyEnv`,比名不分大小写(这一层落到 host 与 device 车道,Windows 上 `model_api_key` 就是同一个变量)——
3335
+ **或**值是秘密形(契约包 settings-schema 的 `containsSecretToken`:`sk-…`、`ghp_…`、`AKIA…`、`Bearer …`、JWT、私钥头 …;worker `env` 与各自由文本写口同一只扫描器)。
3336
+ - **命中** ⇒ `400 request.env_credential_refused`,体带 `field: "settings.env"` 与 `entries: [{ key, match }]`(按体内序列出每一条;`match` = `"name"` / `"value_shape"`,两者都中报 `"name"`),**值从不回显**;
3337
+ 不剥、不降级(剥掉 = 调用方以为交出去的东西静默没到)。不分车道:`settings.env` 被忽略的多租户 / 云车道同拒(凭据同样不该随请求到引擎)。三处同一只判据:提交受理门、装配(嵌入方直调同拒)、停驻任务续跑前的预判。
3338
+ - **出路**:把凭据挪进引擎进程的环境 —— 壳起的本机引擎由壳的凭据文件带入(还把凭据写进 `settings.json` 的老壳先升级),服务形部署放进进程环境 / 编排器的 secret 注入。
3339
+ 要让模型 shell 持一枚令牌(`GH_TOKEN` 一类):放在引擎进程环境里一个不在配置目录、也不是任何模型条目 `apiKeyEnv` 的名下,配请求级 `inheritEnv:"all"`(单用户 host 车道)。
3340
+ 值形扫描是粗判:一串不是凭据、却被判成秘密形的值同样要挪出 `settings.env`(改名不救值形)。
3341
+ - **已知限制**:http 钩子 header 的 `$NAME` 插值只读钩子环境(十二名基础变量 + `settings.env`,9.5c)—— 秘密形的 webhook token 今天在本服务的钩子上**没有替代通道**;不像秘密的 token 照旧经 `settings.env` 给。
3342
+ - **续跑**:升级前受理、停驻的任务,存体里带这类条目时,一次性迁移 M38 的窗(本版发布日 + 30 天;窗口见登记表)内续跑把它们从 shell / hooks 的环境里剥掉、任务照续
3343
+ (运维 warn `settings_env_credentials_dropped`:`migration` / `sessionId` / `dropped: { name, value_shape }` —— 只计数,不带键名与值);窗关后续跑口答 409 `resume_blocked_by_policy`(`blockedBy: "request.env_credential_refused"`,同一组机读位),出路是取消那条 run 后挪走凭据重提。
3344
+
3172
3345
  ### 9.6 trace 脱敏的射程:与出站请求体无关
3173
3346
 
3174
3347
  🔴 **脱敏 ≠ 出站控制**:trace 脱敏(账本 / SSE / turns / 审批预览里的 `«redacted»` 标记)只作用于
@@ -3195,7 +3368,7 @@ WebSearch 工具的 `tool_result` 错误文本,回到模型的对话里,**不是
3195
3368
 
3196
3369
  **日志不是原形面**:运维结构化日志里 `engine_notice` 行的 `message` 与会话流上同一条通知的 `message` 过同一只脱敏(认出凭据形才换成 `«redacted…»`,没有凭据形的句子逐字节原样;会话流行另有 1000 字上限,日志行不截),`detail` 里的规则文本(`permission.allowed_tool_not_granted` 的 `entries[].entry`、`permission.rule_row_dropped` 的 `rows[].rule`)是条目的脱敏原值、`detail` 其余字段逐字 —— 同一条规则文本在 `message` 与 `detail` 里的脱敏是同一只脱敏器;已知边界:引擎把条目里的零宽字符写成 `\u200b` 转义,夹零宽字符的凭据在 `message` 里(日志行与会话流行同)认不出,机读位的规则文本是在原串上认的、不受此限。
3197
3370
 
3198
- **自由文本的强锚读法**(7.143.0 起;此前下面 ① ② 两形漏遮,见 MIGRATION「自由文本脱敏的名字面」):① 复合凭据名(`api_key` / `apikey` / `api-key` / `access_token` / `refresh_token` / `id_token` / `private_key` / `x-amz-signature` / `x-amz-security-token` / `x-amz-credential`)与 `password` / `secret` / `client_secret` 同档 ——在自由文本 `名=值` / `名: 值` 的**声明形**(名字带引号、值带引号,或赋值符两侧零空白)上是名字锚,值是以字母数字起头的字面量时遮(`$VAR` / `%s` / `<占位>` / 表达式 / `None` 一类不算值;`passwd` / `pwd` 任何赋值形都是锚);赋值符两侧带空白的不带引号形(`api_key = abc`)不是声明形、不算锚。`token` / `key` / `auth` / `sig` / `signature` / `sas` 在自由文本里是常用词,只在 JSON 字段与 URL 参数位上是名字锚,自由文本里只有值自身形状被别的臂认出(`ghp_…` / `sk-…` / `AKIA…` 这类)时才遮。代价:关键字参数透传(`f(api_key=api_key)`)的变量名、源码里对象字面量的值(`apiKey: "…"`,名字大小写不敏感)也换成记号;这些名字后面本来由值自身形状认出的值(`API_KEY=sk-…`)记号是强锚值的 `«redacted»`,不再是 `«redacted:<种类>»`(与 `password=` 同读)。
3371
+ **自由文本的强锚读法**(7.143.0 起;此前下面 ① ② 两形漏遮,见 MIGRATION「自由文本脱敏的名字面」):① 复合凭据名(`api_key` / `apikey` / `api-key` / `access_token` / `refresh_token` / `id_token` / `private_key` / `x-amz-signature` / `x-amz-security-token` / `x-amz-credential`)与 `password` / `secret` / `client_secret` 同档 ——在自由文本 `名=值` / `名: 值` 的**声明形**(名字带引号、值带引号、赋值符两侧零空白,或 7.146.0 起的**行首键形**:YAML / INI / compose 的配置行 —— 名字在行首〔可带缩进与 `- ` 列表项记号〕、赋值符右边至少一个空白、不带引号的值是那一行余下的全部〔之后只许行尾或 ` #` 注释〕)上是名字锚,值是以字母数字起头的字面量时遮(`$VAR` / `%s` / `<占位>` / 表达式 / `None` 一类不算值;行首键形上值是类型名〔`str` / `string` / `SecretStr` …〕也不算值;`passwd` / `pwd` 任何赋值形都是锚);行中赋值符两侧带空白的不带引号形(`… api_key = abc`)与值之后行上还有东西的行(`password: str = ""` / ` password: pw,`)不是声明形、不算锚。`token` 只在**赋值那一半的零空白形**上是名字锚(`f(token=tok1)` / `curl -d 'token=…'` / `TOKEN=…`:`=` 两侧零空白、名字不带引号;值文法同上),冒号形(`token: …` / `{token:12345}`)与带空白的赋值(`token = …`)里它是常用词;`key` / `auth` / `sig` / `signature` / `sas` 在自由文本里是常用词,只在 JSON 字段与 URL 参数位上是名字锚,自由文本里只有值自身形状被别的臂认出(`ghp_…` / `sk-…` / `AKIA…` 这类)时才遮。代价:关键字参数透传(`f(api_key=api_key)` / `requests.get(url, token=token)`)的变量名、源码里对象字面量的值(`apiKey: "…"`,名字大小写不敏感)、行首键形同形的文书示例与代码行(`password: xxx` / 对象字面末项 ` password: pw` / `password = user_input` / `Password: required`)也换成记号;这些名字后面本来由值自身形状认出的值(`API_KEY=sk-…`)记号是强锚值的 `«redacted»`,不再是 `«redacted:<种类>»`(与 `password=` 同读)。
3199
3372
  ② 名字的左界止于前一枚已认出的凭据(或可读的结构块)的终点:强锚名紧贴在前一枚凭据的尾巴上(`-----END PRIVATE KEY-----password=…`;PGP 收尾栅栏、PuTTY 的 `Private-MAC:` 行、AWS 键 id 同形,中间无分隔)时**首遍即遮**,再过一遍读数不变 ——所以各出口同读,不看出口过几遍脱敏:流式 `done` 帧、同步体、运维日志行、活流 `text_delta` / `reasoning_delta` 逐块帧与 `text_end` / `reasoning_end`(活流与 `done` 帧同一只脱敏器、同一遍,逐块拼接 = 段末帧,契约 §4a-nov-bis)、`GET /v1/runs/:id` 的 run 行、`GET /v1/runs/:id/events` 回放里的 `text` / `reasoning` / `done` / `tool_end` 行。
3200
3373
  ③ 不带引号的值里面的赋值照问:`api_key=abc,password="my secret value"` 里两个值都遮(此前外层不带引号的值把里面的赋值藏住,里面那一只不认)。
3201
3374
  **升级前已落库的行**:回放读面对 run 行与 `text` / `reasoning` / `done` 行再过一遍脱敏 ⇒ 按本读法遮;`tool_end` 行回放读面不再过(只剥媒体字节)⇒ 升级前写下的 ① ② 两形照旧明文,升级后新写的行照遮。
@@ -3699,6 +3872,12 @@ UPDATE session_policy_rev_mark SET rev = 0, gen = gen + 1 WHERE policy_key = '<w
3699
3872
  - **路径 2 · npm 包**(只有 npm 访问权;包是公开的,匿名可装):`npm install @sema-agent/server@<版本>`。基镜像 `node:24-slim`(包的 `engines` 是 `^20.19.0 || >=22.12.0`);
3700
3873
  入口 `node node_modules/@sema-agent/server/dist/main.js`。包的 `bin` 里没有 HTTP 服务(`run-local` 是单机一次性 runner、`sema-up` 是部署引导脚本),要起服务必须直接跑这个 `dist/main.js`。
3701
3874
 
3875
+ **随包部署事实(7.146.0 起)**:包里的 `deploy/facts/` 是引擎自己的部署要求的机器可读形,给安装器(chart / 清单 / 安装脚本)读 —— `lanes.json`(每条执行车道的租户档、隔离位、必需键;行集 = 所依 settings-schema 的全部车道词)、
3876
+ `sandbox-rbac.json`(k8s 沙箱服务账号的 RBAC 规则:何时授、引擎真调还是兼容保留)、`sealed-key.json`(钥匙保管目录的安装契约:显式绝对 `SEALED_KEY_DIR`,整个目录跨重启持久、各副本共享、引擎可写),
3877
+ 各配一份同包 `*.schema.json`,另有形变登记 `CHANGES.json`。取用 = npm 子路径 `@sema-agent/server/deploy/facts/<文件>`(子路径 = 包内文件路径:`require.resolve` 得到它,tarball 里是 `package/deploy/facts/<文件>`);
3878
+ 读方用同包 Schema 校验、不认识的 `schemaVersion` 整份拒读、同一版本内的未知键忽略。加性形变只有「新增可选属性」一种,其余形变都抬 `schemaVersion` 并登记;规则与本版还没覆盖的要求见包内 `deploy/facts/README.md`。
3879
+ **硬断预告(安装器退役)**:server 包里的 `deploy/sema-up/` 安装器、`bin.sema-up`(`npx -p @sema-agent/server sema-up`)与 npm 子路径 `./deploy/sema-up/lanes.json` 将在 sema-deploy 的安装器(读本包 `deploy/facts/`)与 cli 都已发出、且对绑定好的这组发行物跑完接管验收之后的下一个 server minor 退役(最早 7.148.0;按触发、不按日历;删除,零别名、零转发)。现在就改读:车道、沙箱 RBAC、钥匙保管目录的事实读 npm 子路径 `@sema-agent/server/deploy/facts/<文件>`,安装器取 sema-deploy 的发行物。7.146.0 里旧入口一字未变。
3880
+
3702
3881
  **构建(二选一)**
3703
3882
 
3704
3883
  路径 1: