@sema-agent/server 7.138.0 → 7.139.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 (215) hide show
  1. package/MIGRATION.md +276 -7
  2. package/README.md +7 -4
  3. package/README.zh-CN.md +6 -4
  4. package/USAGE.md +73 -29
  5. package/deploy/sema-up/README.md +1 -1
  6. package/deploy/sema-up/lib/lanes.json +3 -2
  7. package/dist/adoption/wire.d.ts +3 -3
  8. package/dist/approval-card.d.ts +20 -20
  9. package/dist/bench/s1/arms.d.ts +1 -1
  10. package/dist/bench/s1/arms.js +1 -3
  11. package/dist/bench/s1/run-firm.js +2 -2
  12. package/dist/boot/device-lane.d.ts +8 -3
  13. package/dist/boot/device-lane.js +7 -2
  14. package/dist/boot/execution-env.d.ts +4 -2
  15. package/dist/boot/execution-env.js +16 -24
  16. package/dist/boot/execution-lane.d.ts +1 -1
  17. package/dist/boot/governance-seams.d.ts +3 -2
  18. package/dist/boot/governance-seams.js +9 -11
  19. package/dist/boot/memory-consolidation.js +2 -2
  20. package/dist/boot/resolve-spec.d.ts +1 -1
  21. package/dist/boot/resolve-spec.js +8 -9
  22. package/dist/boot/retention-lane.d.ts +5 -5
  23. package/dist/boot/retention-lane.js +0 -6
  24. package/dist/boot/runner-deps.d.ts +3 -1
  25. package/dist/boot/runner-deps.js +2 -1
  26. package/dist/boot/session-faces.d.ts +0 -3
  27. package/dist/boot/session-faces.js +15 -8
  28. package/dist/boot/stage-02-web-admin-pull.js +2 -2
  29. package/dist/boot/stage-05-execution-env.js +2 -2
  30. package/dist/boot/stage-06-runners.js +0 -1
  31. package/dist/boot/stage-09-leader.js +1 -2
  32. package/dist/boot/stage-10-http-server.js +2 -2
  33. package/dist/boot/stores.js +4 -4
  34. package/dist/boot/web-admin.js +2 -2
  35. package/dist/capabilities/mcp-probe.js +1 -1
  36. package/dist/capabilities/scenarios.d.ts +1 -1
  37. package/dist/capability-facts.d.ts +15 -7
  38. package/dist/capability-facts.js +8 -3
  39. package/dist/config-catalog.js +7 -7
  40. package/dist/config-invariants.d.ts +23 -3
  41. package/dist/config-invariants.js +42 -0
  42. package/dist/config-provider.d.ts +19 -24
  43. package/dist/config-provider.js +27 -12
  44. package/dist/config-types.d.ts +10 -5
  45. package/dist/config.d.ts +33 -0
  46. package/dist/config.js +44 -15
  47. package/dist/delegated-session-owner.d.ts +2 -2
  48. package/dist/deployment-shape.d.ts +1 -1
  49. package/dist/device-enrollment.d.ts +41 -2
  50. package/dist/device-enrollment.js +27 -12
  51. package/dist/device-store.d.ts +58 -85
  52. package/dist/device-store.js +19 -49
  53. package/dist/device-ws-hub.js +2 -1
  54. package/dist/env-name-allowlist-knobs.d.ts +5 -4
  55. package/dist/exec-cut.d.ts +60 -0
  56. package/dist/exec-cut.js +36 -0
  57. package/dist/execution-lane-caps.d.ts +19 -0
  58. package/dist/execution-lane-caps.js +10 -0
  59. package/dist/execution-placement.d.ts +259 -0
  60. package/dist/execution-placement.js +199 -0
  61. package/dist/field-conflict.d.ts +3 -2
  62. package/dist/field-conflict.js +1 -0
  63. package/dist/hosted-posture.d.ts +2 -2
  64. package/dist/http/admission.js +8 -19
  65. package/dist/http/checkpoint-ctx.d.ts +21 -12
  66. package/dist/http/checkpoint-ctx.js +4 -2
  67. package/dist/http/claimed-run.d.ts +1 -1
  68. package/dist/http/durable-run-leg.js +9 -5
  69. package/dist/http/execution-context.d.ts +218 -0
  70. package/dist/http/execution-context.js +330 -0
  71. package/dist/http/resume-legs.js +50 -20
  72. package/dist/http/route-ctx.d.ts +14 -8
  73. package/dist/http/routes/background.d.ts +29 -1
  74. package/dist/http/routes/background.js +29 -2
  75. package/dist/http/routes/devices.d.ts +13 -19
  76. package/dist/http/routes/devices.js +21 -26
  77. package/dist/http/routes/session-sync.js +110 -15
  78. package/dist/http/routes/sessions.d.ts +6 -0
  79. package/dist/http/routes/sessions.js +115 -11
  80. package/dist/http/routes/side-query.d.ts +12 -4
  81. package/dist/http/routes/side-query.js +4 -5
  82. package/dist/http/routes/tasks.js +37 -31
  83. package/dist/http/server.d.ts +35 -19
  84. package/dist/http/server.js +11 -0
  85. package/dist/http/session-inherit-env.d.ts +3 -4
  86. package/dist/http/session-inherit-env.js +1 -1
  87. package/dist/http/wire-types.d.ts +8 -12
  88. package/dist/index.d.ts +1 -0
  89. package/dist/memory-sync-client.d.ts +53 -14
  90. package/dist/memory-sync-client.js +196 -116
  91. package/dist/memory-sync.d.ts +1 -1
  92. package/dist/memory-sync.js +3 -2
  93. package/dist/observability/cost-taxonomy.d.ts +2 -2
  94. package/dist/observability/fail-open.d.ts +12 -0
  95. package/dist/observability/fail-open.js +12 -0
  96. package/dist/observability/run-terminal.js +0 -1
  97. package/dist/orchestration/workflow-notify-journal.js +4 -4
  98. package/dist/org-memory-admission.d.ts +4 -3
  99. package/dist/org-memory-admission.js +6 -6
  100. package/dist/parked-decide.d.ts +34 -9
  101. package/dist/parked-decide.js +12 -19
  102. package/dist/parked-revive-surface.d.ts +12 -15
  103. package/dist/parked-revive-surface.js +3 -6
  104. package/dist/peer-seat.d.ts +13 -7
  105. package/dist/peer-seat.js +37 -3
  106. package/dist/plugins/approval-ask-store-file.d.ts +7 -0
  107. package/dist/plugins/approval-ask-store-file.js +11 -0
  108. package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
  109. package/dist/plugins/approval-ask-store-memory.js +12 -0
  110. package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
  111. package/dist/plugins/approval-ask-store-sql.js +30 -12
  112. package/dist/plugins/caching-session-store.d.ts +6 -2
  113. package/dist/plugins/caching-session-store.js +9 -3
  114. package/dist/plugins/checkpoint-store-sql.d.ts +6 -5
  115. package/dist/plugins/checkpoint-store-sql.js +5 -7
  116. package/dist/plugins/device-store-sql.d.ts +25 -51
  117. package/dist/plugins/device-store-sql.js +49 -103
  118. package/dist/plugins/execution-placement-sql.d.ts +151 -0
  119. package/dist/plugins/execution-placement-sql.js +223 -0
  120. package/dist/plugins/file-history-store-sql.d.ts +7 -6
  121. package/dist/plugins/file-history-store-sql.js +5 -5
  122. package/dist/plugins/file-run-store.d.ts +6 -1
  123. package/dist/plugins/file-run-store.js +4 -0
  124. package/dist/plugins/fork-routing-session-store.d.ts +6 -0
  125. package/dist/plugins/fork-routing-session-store.js +16 -0
  126. package/dist/plugins/local-session-store.d.ts +149 -50
  127. package/dist/plugins/local-session-store.js +382 -107
  128. package/dist/plugins/memory-key-guards.d.ts +4 -0
  129. package/dist/plugins/memory-key-guards.js +1 -0
  130. package/dist/plugins/memory-run-store.d.ts +1 -1
  131. package/dist/plugins/one-time-migrations.d.ts +111 -3
  132. package/dist/plugins/one-time-migrations.js +380 -10
  133. package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
  134. package/dist/plugins/permission-rule-store-sql.d.ts +13 -15
  135. package/dist/plugins/permission-rule-store-sql.js +13 -16
  136. package/dist/plugins/pg-pool.d.ts +7 -3
  137. package/dist/plugins/pg-session-storage.d.ts +31 -20
  138. package/dist/plugins/pg-session-storage.js +120 -52
  139. package/dist/plugins/remote-env-adb.d.ts +20 -4
  140. package/dist/plugins/remote-env-adb.js +70 -62
  141. package/dist/plugins/remote-env-device.d.ts +30 -12
  142. package/dist/plugins/remote-env-device.js +18 -17
  143. package/dist/plugins/remote-env-e2b.d.ts +18 -4
  144. package/dist/plugins/remote-env-e2b.js +41 -42
  145. package/dist/plugins/remote-env-file-error.d.ts +2 -0
  146. package/dist/plugins/remote-env-file-error.js +1 -1
  147. package/dist/plugins/remote-env-host.d.ts +21 -8
  148. package/dist/plugins/remote-env-host.js +66 -70
  149. package/dist/plugins/remote-env-k8s.d.ts +11 -16
  150. package/dist/plugins/remote-env-k8s.js +61 -54
  151. package/dist/plugins/remote-env-local-docker.d.ts +29 -6
  152. package/dist/plugins/remote-env-local-docker.js +46 -59
  153. package/dist/plugins/remote-env-ssh.d.ts +16 -4
  154. package/dist/plugins/remote-env-ssh.js +72 -56
  155. package/dist/plugins/remote-shell.d.ts +100 -3
  156. package/dist/plugins/remote-shell.js +166 -2
  157. package/dist/plugins/retention-store-sql.d.ts +9 -6
  158. package/dist/plugins/retention-store-sql.js +7 -1
  159. package/dist/plugins/run-store-sql.d.ts +13 -3
  160. package/dist/plugins/run-store-sql.js +5 -0
  161. package/dist/plugins/schema-consistency.d.ts +15 -5
  162. package/dist/plugins/schema-consistency.js +14 -8
  163. package/dist/plugins/schema-drift-sentence.d.ts +27 -0
  164. package/dist/plugins/schema-drift-sentence.js +38 -0
  165. package/dist/plugins/session-fork.d.ts +54 -0
  166. package/dist/plugins/session-fork.js +38 -1
  167. package/dist/plugins/session-row-sql.d.ts +22 -0
  168. package/dist/plugins/session-row-sql.js +81 -0
  169. package/dist/plugins/session-store.d.ts +1 -1
  170. package/dist/plugins/store-backend.d.ts +1 -1
  171. package/dist/plugins/store-backend.js +19 -7
  172. package/dist/plugins/store-contracts.d.ts +25 -5
  173. package/dist/plugins/tidb-pool.d.ts +12 -7
  174. package/dist/plugins/tidb-pool.js +9 -3
  175. package/dist/plugins/tidb-session-store.d.ts +57 -26
  176. package/dist/plugins/tidb-session-store.js +118 -50
  177. package/dist/plugins/tool-result-store-sql.d.ts +3 -2
  178. package/dist/plugins/tool-result-store-sql.js +18 -10
  179. package/dist/project-memory.d.ts +9 -15
  180. package/dist/project-memory.js +5 -4
  181. package/dist/request-key-closure.js +1 -1
  182. package/dist/request-value-closure.d.ts +6 -7
  183. package/dist/request-value-closure.js +1 -3
  184. package/dist/run-local.d.ts +6 -0
  185. package/dist/run-local.js +24 -20
  186. package/dist/runs.d.ts +2 -1
  187. package/dist/runs.js +3 -1
  188. package/dist/security.d.ts +38 -12
  189. package/dist/security.js +8 -2
  190. package/dist/server-secret-env.d.ts +9 -5
  191. package/dist/session-children-retire.d.ts +27 -9
  192. package/dist/session-children-retire.js +9 -5
  193. package/dist/session-incarnation.d.ts +19 -9
  194. package/dist/session-incarnation.js +4 -2
  195. package/dist/session-row.d.ts +114 -5
  196. package/dist/session-row.js +55 -5
  197. package/dist/session-titler.d.ts +2 -2
  198. package/dist/task-a2a.d.ts +1 -1
  199. package/dist/task-cwd.d.ts +6 -1
  200. package/dist/task-mcp.d.ts +3 -3
  201. package/dist/tool-approval.d.ts +6 -0
  202. package/dist/tool-approval.js +8 -1
  203. package/dist/trace/project.d.ts +1 -0
  204. package/dist/trace/project.js +1 -0
  205. package/dist/trace/self-minted-wire.d.ts +111 -2
  206. package/dist/trace/self-minted-wire.js +38 -1
  207. package/dist/url-refusal.d.ts +1 -1
  208. package/dist/web-admin-client/apply-effective.d.ts +17 -3
  209. package/dist/web-admin-client/apply-effective.js +20 -10
  210. package/dist/web-admin-client/skills-mcp.js +3 -2
  211. package/dist/web-admin-client/types.d.ts +6 -0
  212. package/package.json +3 -3
  213. package/wire-changes/server@7.139.0.json +348 -0
  214. package/dist/http/device-placement.d.ts +0 -175
  215. package/dist/http/device-placement.js +0 -145
package/MIGRATION.md CHANGED
@@ -7,6 +7,269 @@
7
7
  > ⚠️ 完整清单在仓库根 `CHANGELOG.md`——它**不随 npm tarball 出包**(本文件随包)。看完整迁移窗的
8
8
  > 权威姿势是源码 tag diff:`git diff v<旧>..v<新>`(每版都推 `v<版本>` tag);npm 包页也镜像 CHANGELOG。
9
9
 
10
+ ## 7.139.0 —— 执行放置成为根会话行上的一格:🔴 SQL 后端 `session_meta` 加八列、`device_session` 表退役、`approval_ask` 加一列 `execution_lane`(本服务起服时自己改表;升级前备份、先停光全部旧副本再起本版;回滚只有恢复备份)· 路由 `GET /v1/devices/sessions/:id` 删除(BREAKING,wire;换读 `GET /v1/sessions/:id/execution-placement`)· 新口 `POST /v1/sessions/:id/children/retire` · fork 可带键重试 `{forkId, forkNonce}`(wire additive)· 四只新 409(`conflict.session_mirror` / `conflict.session_writer` / `conflict.fork_*` / 活子代)与拒体新顶层键 `forkPath` / `boundLane` / `children` / `retirePath`(行为面)· 会话行条件写 `expectedRowTag` ⇒ `409 conflict.session_row_changed`(wire 加性)· 🔴 记忆同步:升级后每一层停同步直到运维对该层游标做一步 · 请求键 `rewindFiles` 退役(wire BREAKING;新提交 400)· `POST /v1/rules/cc-import/prepare` 的 200 体少键 `translated`(wire BREAKING,读侧)· 后台行多可选键 `sessionId`(wire 加性)· 旁问路由 400 只给 `config.unknown_role`(行为变更)· 声明判官的拒句不再回显调用方写的名(运维面)· 部署 mod 包内核契约号 6(`contract: 5` 起服拒)· 四只新运维通告码不上用户流 · 沙箱带外 env 名单点到本服务自有凭据名 ⇒ 起服拒(`E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV` / `K8S_SANDBOX_ENV`;安全,行为收紧)· k8s 车道 `K8S_NAMESPACE` 必设(部署面,行为收紧)· 模型目录 `apiKeyEnv` 点到名单型旋钮正在交出的名 ⇒ 该候选整拒(安全)· `CONFIG_PROVIDER=remote` 无中心地址 / `RETENTION_MODE=enforce` 缺天数或节律 ⇒ 起服拒(行为收紧,fail-closed)· 会话规则 / 持久规则桶文法戳 7(升级序:先升全部读者再放写)· 执行环境写入改「暂存 → 一次提交」(七条车道一条规则;行为面)· 本机宿主车道 workflow 子代 `isolation: "worktree"` 的已知变化(E10,引擎 7.61.0 收)· 嵌入方类型面 BREAKING 多处(`RunnerDeps.peerSeat` 三员对象 · `createHttpServer` 的 `authorize` 席必填 · `resolveSpec` 不再写 `sessionId` · 设备店绑定四法换两只放置写 · 停驻赎回腿的会话店必须带执行席、`root` → `rootOccupant` · `StoreBackend` 问店行与迁移报告多键)· 契约包 settings-schema 15.0.0 · 引擎 7.60.0
11
+
12
+ 1. **🔴 记忆同步(`MEMORY_SYNC_URL` 设了的 file 记忆部署):升级后每一层都停同步,直到运维对那一层的游标做一步**:
13
+ 本版起同步对端 = `MEMORY_SYNC_URL` 的规范化形(协议与主机小写、缺省端口省掉、路径去尾斜杠;起服日志 `memory_sync_enabled` 的 `peer` 就是它,该行不再带 `url`),
14
+ 每层一只游标 `<记忆根>/sync-cursors/<scope 摘要>.json`,游标的对端 / scope / 店绑定只由引擎判。第一轮会把上一版的分区游标(`sync-cursors/central/…`)与更早的根下游标
15
+ (`sync-cursor-central.json`)搬到各自那一层的路径(只搬、字节不变;日志 `memory_sync_cursor_relocated`,目标路径已有游标或读不出它属于哪一层 ⇒ 原地不动、一行 `memory_sync_cursor_not_relocated`)。
16
+ 搬过来的游标绑的对端名是 `central`,与新身份不符 ⇒ 引擎在对端轴上拒、这一层零请求,`memory_sync_layer_failed` 带引擎的拒句(两条出路)与游标路径;其余层照跑,下一轮同样拒,直到处置。
17
+ **处置(每一层一次,二选一)**:① 中心没换(同一只中心,只是换了身份写法)⇒ 把那只游标文件里的 `"peer"` 改成 `memory_sync_enabled` 日志里的 `peer` 值,其余字节不动,下一轮照常增量同步 —— 例外:游标里没有 `"store"` 键的(7.138.0 之前写下的 `sync-cursors/central/…` 与更早根下的 `sync-cursor-central.json` 都没有),改完 `peer` 后下一轮引擎在店轴再拒一次
18
+ (三轴按 scope → peer → store 的顺序判,peer 过了才轮到 store),`memory_sync_layer_failed` 的拒句点名这一轮的店身份:把它作为 `"store"` 键写进那只游标,再下一轮照常增量同步(或走处置 ②);
19
+ ② 删掉那只游标 = 接受空基线重收敛:两边都改过的条目按冲突上报,自上一轮以来一侧删掉的条目会从另一侧回来。中心换过了只能走 ②。执行器不替运维选:读不了 / 读不成 / 被拒的游标一律原地不动。
20
+ **起服判 `MEMORY_SYNC_URL`**(引擎关着也判):不是层级式 http(s)、带 userinfo / query / fragment(含空的 `?` / `#`)、路径段里带令牌形、或规范化后超过中心 `peer` 列宽 ⇒ 拒启(拒句值只到 origin,不回显地址其余部分);
21
+ 令牌只走 `MEMORY_SYNC_TOKEN`。上一版照收的这些形升级当天起服即拒 —— 升级前把地址写成基址本身。
22
+ 两道判各有属主,本服务不另抄:地址的形归「后面要拼路径的基址」座位的判官 `pathBaseUrlRefusal`;身份里有没有凭据(路径段的令牌形、任何 userinfo)归引擎包根导出的 `carriesCredential` ——
23
+ 引擎同步客户端判对端身份的同一只,起服对规范化后的身份调它,所以引擎在第一轮会拒的地址起服就拒,一个请求都不发。
24
+
25
+ 2. **请求键 `rewindFiles` 退役(wire BREAKING;零别名)**:新提交(`POST /v1/tasks` · `/v1/tasks/stream` · `/v1/runs`)带它 ⇒ `400 request.body_shape`、`unknownKeys: ["rewindFiles"]`,不建 run;
26
+ 续跑重放盘上旧体里的它零读者(不进引擎 —— 引擎本版对它按名拒,带它存过的任务因此照常续跑)。客户端改用 `restoreFiles` 或 `rewindFilesTo`。
27
+
28
+ 3. **旁问路由(`POST /v1/side-query`)的 400 只给引擎的 `config.unknown_role`(行为变更)**:角色族四臂(角色词不在词表 / 不是模型引用 / 指到目录外 / 角色缺模型)带码 400;
29
+ 没有引擎码的错误一律 500(上一版按英文前缀表把同形句子判 400)。调用方按 `errorCode` 判,不按句子。续跑侧:停驻卡因角色缺模型(`no_model_for_role`)被拒时与目录缺条目同判可兑 —— 配好角色表后同一张卡可续。
30
+
31
+ 4. **声明判官的拒句不再回显调用方写的名(行为变更;运维面)**:请求面 `mcpServers[].toolAxes` / `toolFaces` / `sessionMetaKey` 与 A2A `toolAxes` 的 400 拒句里,调用方写的工具名 / 键 / 词换成
32
+ `[redacted] (N characters)`(N = 码位数)。部署腿(`config.d` / 中心下发)的 `web_admin_mcp_tool_axes_invalid` / `web_admin_mcp_tool_faces_invalid` 的 `problem` 同样只给码位数 —— server 名照点,
33
+ 哪只工具坏了要按 server 名去配置里逐个查。码与状态码不变。
34
+
35
+ 5. **部署 mod 包的内核契约号是 6**:`contract: 5` 的包起服拒 `config.mods_invalid`(拒句点名两号;不静默装、不跳过)。6 = 四个新扩展事件开站(`agent.offer` / `agent.spawn` / `turn.complete` / `session.end`)
36
+ + `tool.call` 失败列 `refuse`(调用跑过之后扩展失败 ⇒ 结果扣下)。升级前把包的 `contract` 改成 6 并核处理器对新列的假设。
37
+
38
+ 6. **`POST /v1/rules/cc-import/prepare` 的 200 体少一个键 `translated`(wire BREAKING,读侧)**:引擎删了这个恒空数组,本服务原样投。读它的客户端改为不读(它从来是空的)。
39
+
40
+ 7. **`GET /v1/sessions/:id/background` 的行多可选键 `sessionId`(wire 加性)**:后台 agent 行在子会话绑定之后带它(跳到子会话用;那只会话照常过会话面的属主门)。键缺席 = 没有 / 还没绑定。
41
+
42
+ 8. **运维面:四只新通告码受众是运维,不上用户流**:`mcp.credential_echoed`(对端把连接凭据回显进它的自述 —— 点名对端与位置,不带值)、`config.read_deny_floor_unkept`
43
+ (执行车道不守读拒表的字节地板 —— 本服务七只车道都声明不守,读拒表非空的每条带车道的 run 都会有一行)、`classifier.seat_rate_limited`(自动模式分类座的模型连续被限流,
44
+ 这条 run 余下的门改由主模型判;引擎缺省连续 3 次,本服务不接这只引擎旋钮 —— 要一直问分类座就设 `AUTO_MODE_CLASSIFIER_FALLBACK=none`,那时它答不出整条拒、从不退到主模型)与 `config.image_processor_unavailable`(这条 run 没有图片处理器、一张超软目标的图原样发出;
45
+ 本服务把 sharp 列为可选依赖,装上即到达不了)。看板 / 告警按 `audience` 过滤。
46
+ `/metrics` 的 `peer_registry_holders` 本版起 = 宿主租约 + 引擎持有之和(run 在飞时读数会高于名册条目数,落定后回到租约数)。
47
+
48
+ 9. **嵌入方(类型面 BREAKING;部署零动作)**:`RunnerDeps.peerSeat` 是 `{ answer, hold, release }` 三员对象(函数形构造即拒 `config.invalid_argument`;本服务的 desk 已是对象;
49
+ desk 对它没铸过的引用 —— `env` / `vault` / `assertion` 或闭集外的种类 —— `hold` / `release` 抛,引擎报一次运维面 `peer-seat-hold`、不记持有);
50
+ 自写执行车道的 `capabilities` 必须声明引擎八键(`hostLocalPaths` / `externalContentTarget` / `io` / `keepsReadFloor` 新必填,设备车道顶层两只散布尔已并入块);
51
+ `loadProjectMemory` 只答 `{ content, instructionSources? }`(无可注入 = `{ content: null }`;裸串 / `null` 引擎不读)。
52
+
53
+ 10. **沙箱带外 env 名单点到本服务自有凭据名 ⇒ 起服拒(`E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV` / `K8S_SANDBOX_ENV`;安全,行为收紧)**:
54
+ 三只名单点到的变量从服务进程环境原样装进沙箱(e2b VM env / `docker exec -e` / k8s pod 自己的 Secret),沙箱里模型驱动的命令读得到。本版起它们与
55
+ `LSP_ENV_ALLOW` / `BAKE_ENV_ALLOW` 同一条律:点到本服务自有凭据名 —— 配置目录里全部 secret 型变量(如 `SERVICE_AUTH_TOKEN(S)` / `APPROVAL_HMAC_KEYS` /
56
+ `MODEL_API_KEY` / `E2B_API_KEY` / `K8S_TOKEN` / `GIT_API_TOKEN`)与名单型旋钮名本身 —— ⇒ 起服拒,拒句点名旋钮与第几段
57
+ (`env E2B_SANDBOX_ENV: entry #2 of 2 is this deployment's own credential — a sandbox list puts the named variable, as written, into a sandbox the model can read; …`),
58
+ 不回显值;多租户姿态(`REQUIRE_PRINCIPAL=true`)下同样拒。
59
+ **谁受伤**:靠这三只名单把本服务自有凭据交进沙箱过活的部署(典型:`E2B_SANDBOX_ENV=MODEL_API_KEY` 让沙箱里的工具直调模型网关、`K8S_SANDBOX_ENV=GIT_API_TOKEN`
60
+ 让沙箱里的 git 用本服务的 Git host 凭据)—— 升级后起服失败。
61
+ **升级前做(出路)**:给沙箱铸一把独立凭据 —— 名与值都不同于本服务的任何凭据(例如在网关 / Git host 上为沙箱另发一把限权、可单独吊销的 key)—— 放进服务进程环境,
62
+ 名单改点那个名:`SANDBOX_GIT_TOKEN=<另一把令牌>`、`K8S_SANDBOX_ENV=SANDBOX_GIT_TOKEN`,沙箱里的工具改读新名。别把同一个值换个名字再点
63
+ (`SANDBOX_GIT_TOKEN=$GIT_API_TOKEN`):那仍是本服务的凭据进了模型可读的沙箱,只是换了名字。
64
+ 名单只点部署方给沙箱的令牌(`GIT_TOKEN`、`NPM_TOKEN` 这类不在配置目录里的名)的部署零动作。**回滚**:回到上一版即恢复旧行为(名单照装),无数据面改动。
65
+
66
+ 11. **k8s 车道:`K8S_NAMESPACE` 必须设,缺席 / 空白起服拒(部署面,行为收紧;嵌入方类型面 BREAKING)**:
67
+ 沙箱 pod 所在的命名空间是部署事实(沙箱 Role / RoleBinding / NetworkPolicy 与回收器都认部署声明的那一个),服务不再带内置缺省。
68
+ `REMOTE_EXEC=k8s` 而 `K8S_NAMESPACE` 缺席、空串或只含空白 ⇒ 起服拒,拒句点名旋钮与出路:它是 k8s 车道的必需件,拒的是所有车道共用的那道必需 env 门
69
+ (`REMOTE_EXEC=k8s but required env K8S_NAMESPACE is unset, empty or whitespace-only — it counts as absent: …`;门的判法见第 18 条)。其它车道不读这根旋钮,缺席照常起。
70
+ **谁受伤**:`REMOTE_EXEC=k8s` 且没设 `K8S_NAMESPACE` 的部署 —— 升级前沙箱 pod 落在 `sandboxes`;典型是不走随包 chart 的自管集群与本地联调。
71
+ 嵌入方:`ServiceConfig` 的 k8s 车道臂里 `namespace` 由可选改必填(类型面,编译红即消费点清单,没有别名)。
72
+ **升级前做(出路)**:在服务进程环境里设 `K8S_NAMESPACE=<沙箱 pod 所在的命名空间>`;原来没设的部署设成 `K8S_NAMESPACE=sandboxes`,即与升级前逐字同。
73
+ 随包 chart(`deploy/sema-up`,取 `sandbox.namespace`)与单文件栈清单已显式设,零动作。**回滚**:回到上一版即恢复旧缺省,无数据面改动。
74
+
75
+ 12. **模型目录条目的 `apiKeyEnv` 点到名单型旋钮正在交出去的名 ⇒ 该候选整拒(安全,行为收紧)**:
76
+ 中心 / config.d 模型目录条目的 `apiKeyEnv` 在模型目录换代时并进本服务自有凭据名。本版起这一刻对活配置里的名单(`E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV` /
77
+ `K8S_SANDBOX_ENV` / `LSP_ENV_ALLOW`)此刻交出去的名重判一次:新并进的名正被某只名单交给沙箱 / 语言服务器 ⇒ 这份候选整拒、上一代照常服务(起服时落 LKG / env;
78
+ 刷新拍 `web_admin_refresh_failed` 每拍重判,`/health` 目标 ≠ 已应用),拒句点名旋钮、名与条目
79
+ (`env K8S_SANDBOX_ENV hands on GATEWAY_KEY, which this candidate makes this deployment's own credential — …`),不回显值。
80
+ **谁受伤**:worker 的名单点着某个变量、中心又把同一个变量写成某条模型条目的 `apiKeyEnv`(典型:`K8S_SANDBOX_ENV=GATEWAY_KEY` 让沙箱里的工具直调网关,中心又给一条模型配
81
+ `apiKeyEnv: GATEWAY_KEY`)—— 升级后中心那份发布在这台 worker 上整份不生效;修前同一个值既是本服务的模型凭据,又在每只沙箱里。
82
+ **升级前做(出路,二选一)**:给沙箱 / 语言服务器另铸一把凭据(名与值都不同),名单改点新名并重启 worker;或在中心把那条模型条目的 `apiKeyEnv` 改指另一个变量(下一拍自愈,不用重启)。
83
+ 名单与模型条目 `apiKeyEnv` 不相交的部署零动作。**回滚**:回到上一版即恢复旧行为,无数据面改动。
84
+
85
+ 13. **嵌入方(类型面 + 构造期,BREAKING):`createHttpServer` 的 `authorize` 席必填;`resolveSpec` 不再写 `sessionId`**:
86
+ 标准起服零动作 —— 它恒装 authorizer(`run-local` 不起 HTTP 面,同样零动作)。自建 `createHttpServer(deps)` 的嵌入方:
87
+ ① deps 加一行 `authorize: createAuthorizer(config, runner.sessions)`(包根导出;店没有属主读时退化为只派 principal 与会话 id,单用户嵌入与升级前同义);
88
+ 这一键从 `governance` 组移到顶层必填席,写在 `governance: { authorize }` 里的改写到顶层。省掉 ⇒ 编译错;JS 嵌入方省掉 ⇒ `createHttpServer` 构造期抛,拒句点名这一行。
89
+ ② 自建 `resolveSpec` 删掉返回值里的 `sessionId`(返回型 `ResolvedTaskSpec` 上它是 `never`);要用这条腿的根会话就读必填的 `opts.execution.root`。
90
+ 根由 server 在装配之后盖进 spec;JS 形装配器返回另一只会话 ⇒ 提交腿 `400 request.precondition_unmet`、零 run 行(续跑腿折成 `409 resume_blocked_by_policy`,`blockedBy` 同码)。
91
+ 要续已有会话,让请求体带它的 id(过 authorizer 的属主门)。
92
+ ③ 注入了 `runStore` 而解析器不写 `sessionId` 的嵌入:同步 / 流式提交从此起认领 + run 行(与 durable 腿同形)。不想记 run 账就不注入 `runStore`(`/v1/runs` 501)。
93
+ ④ SQL run 店的认领事务先锁根会话行:根会话行不在(受理之后会话被删)⇒ 四个建行点答 `404 resume.session_not_found`。自建 deps 把 SQL run 店与非 SQL 会话店配在一起的装配(生产起服不会这样配)认领永远读到零行 —— 两只店要同一个 SQL 后端。
94
+
95
+ 14. **SQL 后端:执行放置切换 —— `session_meta` 加八列、`device_session` 表退役、`approval_ask` 加一列 `execution_lane`(🔴 本服务起服时自己改表;升级前备份、先停光全部旧副本再起本版;回滚只有恢复备份一条路)**:
96
+ 「这只会话在本部署上由谁写」成了根会话行上的一格(`absent` / `writer` / `mirror`,`DB_BACKEND=local` 另有 `forking`);device 车道的设备绑定就是 `writer` 那一格里的设备。
97
+ 本条**不用手跑 `ALTER`**:起服时先只读探一次库形,再在一次性迁移锁内依次 —— 加列(`execution_role` · `execution_lane` · `execution_device_id` · `execution_device_owner_tenant` ·
98
+ `execution_device_owner_subject` · `execution_placed_at` · `execution_rev` · `fork_key`,全部可空;`approval_ask.execution_lane` `NOT NULL DEFAULT ''`)→ `device_session` 的绑定整行搬进根会话行
99
+ (`rev` 原值)→ 有 run 行而还没有放置的会话回填成本处写者(本部署的车道)→ 问行回填车道 → 删掉旧表。中途被打断,下次起服从断点续;新副本在迁移走完之前不服务。库账号要有 DDL 权限(起服建表本就要求)。
100
+ **升级步**:
101
+ ① **备份**整个库(回滚的唯一前提)。`DB_BACKEND=local`:备份整个数据根(会话元数据边档 `session-incarnations.json` 与问账日志 `approval-asks/asks.jsonl` 会被改写)。
102
+ ② **先停光全部旧副本,再起第一个本版副本** —— 滚动升级在本版不成立:切换开始之后仍在跑的旧副本还会往 `device_session` 写首绑,那些行会让下一次起服撞围栏(见 ④)。
103
+ 多副本 k8s 本次按 `Recreate` 做:先把副本数缩到 0、等旧 pod 退光,再升级、再放回副本数(各安装路径的缩容做法与 7.137.0 节第 4 条「升级前先缩光副本」三段相同)。
104
+ ③ 首启时 `REMOTE_EXEC` 照旧取一个值(回填按这一条车道写)。起服日志行 `one_time_migrations_applied` 的七个读数键说这次做了什么:`executionPlacementColumnsAdded`(补了几列)·
105
+ `deviceBindingsMoved`(搬了几条绑定)· `deviceBindingsOrphaned`(指向已不在的会话、被丢弃的绑定数;非零另有一行 warn `device_bindings_orphaned`)· `deviceBindingTablesDropped`(删了几张绑定表)·
106
+ `executionPlacementsBackfilled`(回填了几只会话)· `askExecutionLanesBackfilled`(回填了几只问)· `executionPlacementSessionsUnreadable`(只 `DB_BACKEND=local`:被回填跳过的读不出的旧会话数)。
107
+ `DB_BACKEND=local` 数据根里「有 run、会话元数据格缺、存档读不出」的老会话被回填**跳过并告警**、起服不挡:每只一行 warn `execution_placement_session_unreadable`(`sessionId` 与 `fault`:`corrupt_session` / `invalid_session` / `storage`;
108
+ `storage` = 读存档文件时抛了别的故障〔I/O 级:权限不够、磁盘 I/O 错、存档大到读不进内存〕,此时多一个字段 `errno` 带系统码 `EACCES` / `EIO` / `ERR_STRING_TOO_LONG` ……),**I/O 级读故障同样跳过并点名**(fork 创建的格而存档读不出的会话同样);
109
+ 每次起服都再报;该会话的请求照旧答存档的故障。出路 = 把它的存档文件(数据根 `sessions/` 目录下)从留存副本恢复(`storage`:修好文件权限 / 磁盘),重起后补它的放置;移走存档文件则不再告警、也不补放置(这只会话在本服务上不再存在,它的 run 行留着;fork 创建的会话例外:它的格还在,读作 fork 未落定,出路是同键重试那次 fork 或删除该会话)。
110
+ device 车道上升级前有 run、绑定表里却没有它的会话回填成「本处写者、device 车道、设备未知」:不带 `deviceId` 的提交答 `409 device.not_bound`,带 `deviceId` 即首绑。
111
+ ④ **围栏**:切换已开始(`session_meta.execution_role` 列在,或中途表 `device_session_retired` 在)而 `device_session` 里又有行 ⇒ 起服拒 `config.execution_placement_switch_refused`
112
+ (`reason: fence`),拒在任何 DDL 之前、库零写,拒句列出这些行的根会话(至多 20 只)。先停光旧副本,再二选一,然后重起本版:
113
+ **采旧**(这些行在下一次起服时覆盖它们根会话行上的设备放置)—— 中途表不在时,(MySQL 协议)`RENAME TABLE device_session TO device_session_retired;`
114
+ (PostgreSQL)`ALTER TABLE device_session RENAME TO device_session_retired;`;中途表已在时(两方言同一串):
115
+ `DELETE FROM device_session_retired WHERE root_session_id IN (SELECT root_session_id FROM device_session); INSERT INTO device_session_retired (root_session_id, device_id, owner_tenant, owner_subject, bound_at, rev) SELECT root_session_id, device_id, owner_tenant, owner_subject, bound_at, rev FROM device_session; DELETE FROM device_session;`
116
+ **弃旧**(会话行保留现有的放置):`DELETE FROM device_session;`(只清空旧绑定表;这张空表由本版自己的迁移在下一次起服删掉,不用你动手)。
117
+ 另一拒因 `backfill_ambiguous`(声明的车道不止一条而还有要回填的会话 / 问行)在 `REMOTE_EXEC` 只取一个值的本版不可达;拒句给的出路是「先以单一车道起一次服,让切换走完」。
118
+ ⑤ **回滚**:唯一支持的形 = 恢复 ① 的备份(代价:切换之后的写全丢)。上一版起在已切换的库上会建回一张空 `device_session`,随后起服列册对账把执行列看成未知列而拒启
119
+ (`config.schema_mismatch`),不写行;再起本版,迁移删掉那张空表。`DB_BACKEND=local` 上,上一版读到带执行位的会话元数据格时整份拒那一只边档(首次触及时,不是起服时)—— 同样恢复备份。
120
+ ⑥ **窗**:这段迁移窗内每次起服都跑(切换走完之后零动作),窗 = 本版发布日起 30 天;窗关之后的版本删掉它,并让还带 `device_session` / `device_session_retired` 的库起服拒启 ——
121
+ 跳过本版直升的库,先在本版上起一次服。
122
+ `DB_BACKEND=memory`(引擎缺省的内存会话店)零动作:那种部署不记执行放置。
123
+
124
+ 15. **路由:`GET /v1/devices/sessions/:id` 删除(BREAKING,wire;零转发),换读 `GET /v1/sessions/:id/execution-placement`;新口 `POST /v1/sessions/:id/children/retire`;`POST /v1/sessions/:id/fork` 收可选体 `{forkId, forkNonce}`(wire additive)**:
125
+ ① 旧读面答全局 `404 not_found.route`。新读面 200 体 = `{role:"absent"}` | `{role:"writer", lane, device?:{deviceId, rev}}` | `{role:"mirror"}` | `{role:"forking"}`(`lane` = 执行车道词,
126
+ `REMOTE_EXEC` 的取值,未设 = `in-process`)。换绑的材料:旧体 `boundDevice.deviceId` / `boundDevice.rev` ⇒ 新体 `device.deviceId` / `device.rev`(`rev` 仍是换绑口 `expectedRev` 要回显的前态);
127
+ `boundAt` 没有了;旧体的 `rootSessionId` 就是路径里的 id。「没有绑定」此前答 `404 not_found.device`,现在是 200 —— 格不是 `writer`,或没有 `device` 键;他人会话与不在的会话同答
128
+ `404 not_found.session`(此前 `not_found.device`)。**可见性的门换了**:旧读面按设备域判(要有 tenant claim,且是该设备绑定的同属主,否则 403 / 404;operator 不受此限),新读面按会话判 ——
129
+ 会话面的同一只属主谓词(有主的会话行 = 会话属主;没有身份边界的开发部署与无主的普通会话行对调用方开放,与会话面同读)∨ explicit operator。对**非 operator** 的调用方,在带身份边界的部署、有主会话上:
130
+ 会话属主没有 tenant claim 时,旧读面答 403,现在读得到自己会话的 `device`(绑定);会话属主带的 tenant claim 与该设备绑定的属主不同时,旧读面答 404,现在读得到;
131
+ 设备属主既不是会话属主、也不是 operator 时,旧读面读得到,现在答 `404 not_found.session`。operator 两边都读得到,无变化。非 device 部署上它也在(旧读面在那里整族 `501 capability.device_lane_required`);
132
+ 会话店不带执行席 ⇒ `501 capability.session_store_required`。没有能力位:老 server 对新路径答 `404 not_found.route`。
133
+ ② 结清口(无体)⇒ `200 {children: string[]}`(首轮观察到、且已全部结清的后台子代,每项与 `GET /v1/agents/roster` 行上的 id 同串)/ `409 conflict.session_children_alive`
134
+ (体 `remaining`,有本副本结束不了的子代,一只都没动)/ `404 not_found.session`;鉴权、限流、无凭据写门与 `POST /v1/sessions/:id/background/stop` 同。停止口本身语义不变
135
+ (停驻子代与它的待决卡照留);要「停一切」= 停止口 + 结清口两次调用。
136
+ ③ fork 口:`forkId`(调用方铸的规范 uuid,= 新会话的 id)+ `forkNonce`(1–255 个字符,重试送同一个),同在同缺。首次 `200 {sessionId}`;同键重试 `200 {sessionId, replayed: true}`;
137
+ `forkId` 已是你的另一只会话 ⇒ `409 conflict.fork_id_taken`(终局,换一组新键);已是别人的会话 ⇒ `404 not_found.session`;缺一键 ⇒ `400 request.field_conflict`(`detail.kind: "fork_target"`);
138
+ `forkId` 不是规范 uuid ⇒ `400 request.id_invalid`;`forkId` 等于源 ⇒ `400 request.field_invalid`;`forkNonce` 不合形 ⇒ `400 request.field_invalid`;
139
+ 两次撞上同一目标的别的写者 ⇒ `503 state.fork_target_unsettled`(`permanence: "transient"`,原样重试)。不带体 ⇒ 与上一版同(现铸 id);零字节体与字面 `null` 同义于不带体。
140
+ **体现在被解析了**(上一版这个口忽略体,任何体都照常现铸一只):畸形 JSON ⇒ `400 request.invalid_json`;超过体帽(8 MiB)⇒ `413 request.payload_too_large`;
141
+ JSON 数组或标量(`[]`、`"x"`、`7`)⇒ `400 request.field_invalid`。三种都拒在 fork 之前(什么都没建)。
142
+ 对账读新读面:`GET /v1/sessions/<forkId>/execution-placement` 答 `absent` / `writer` ⇒ 已 fork;`forking` ⇒ 没拷完,同键重试补齐;404 ⇒ 没 fork。
143
+
144
+ 16. **壳 / 客户端的读法:四只新 409 与拒体新顶层键 `forkPath` / `boundLane` / `children` / `retirePath`;同一份日志重推到本处执行过的会话从 200 变 409(行为面)**:
145
+ · `409 conflict.session_mirror`(体 `forkPath`):会话是从别处导入的只读副本 —— 要在本处继续,`POST <forkPath>`(带 `{forkId, forkNonce}` 可重试)后在新会话上执行。
146
+ · `409 conflict.session_writer`:推送(`POST /v1/sessions/:id/sync/import` 与 `…/sync/import/:stagingId/entries`)打到本处写者 —— 本处执行过的会话,升级时有 run 行的会话都被回填成写者。
147
+ **内容完全相同的重推也是 409**(此前 `200 relation:"identical"`)。改为拉取(`GET …/sync/manifest` + `…/sync/entries`),或 fork 后推自己的副本;推之前可读一次执行放置读面(`writer` ⇒ 别推)。
148
+ · `409 conflict.fork_in_progress`(只 `DB_BACKEND=local`):目标是一次没拷完的 fork —— 用同一组 `forkId` / `forkNonce` 重试那次 fork,或删掉这只会话。
149
+ · `409 execution_lane.not_served`(体 `boundLane`):会话放在本部署已不再运行的车道上(升级之后运维换了 `REMOTE_EXEC`)—— fork 出新会话,新会话首跑落在当前车道。
150
+ · 子代半:换绑 `409 device.rebind_active_run`、推送 `409 session_active` 的体可以是 `{children, retirePath}`(根下有活后台子代)而不是 `{activeTaskId}`(根 run 在跑)—— 按键分支;
151
+ `POST <retirePath>` 结清后重试。
152
+ · 续跑家族(`/decide` · `/answer` · `/plan_review` · `/wake` 等)上,这些拒在认领之前一律折成 `409 resume_blocked_by_policy`,`blockedBy` = 原码,原拒体的键随行。
153
+ · 会话在受理与执行之间被删、或删后同 id 重登记:提交腿 `404 resume.session_not_found`(零 run 行);续跑腿认领前 `409 resume_blocked_by_policy`(`blockedBy: "resume.session_not_found"`)、
154
+ 认领后 `404 resume.session_not_found`(那条停驻的 run 以 `failed` 收尾);停驻赎回仍答 `409 decide.not_parked`。
155
+ · device 车道:带 `resumeAt` 的提交,设备准入的拒(`device.*`)先于解锚的拒(`resume_at.*`)—— 锚错又点错设备答 `403 device.identity_mismatch`(此前 `404 resume_at.unknown_event`);
156
+ 提交时读根会话行出故障答 500(此前 `409 device.not_bound`;设备注册簿读故障仍是 `409 device.not_bound`)。
157
+
158
+ 17. **嵌入方(类型面,BREAKING;零别名):设备店的绑定四法换成两只放置写;自建 `StoreBackend` 的问店行与迁移报告多键;会话店、authorizer、`ServiceDeps` 各多可选位**:
159
+ 标准起服零动作。自建装配的嵌入方:
160
+ ① 经 `ServiceDeps.deviceStore` / `StoreBackend.device()` 交出的设备店:删 `getSessionBinding` / `bindSession` / `rebindSession` / `deleteByRootSession`,实现 `placeDeviceExecution`
161
+ (首绑:锁设备行 → 根会话行那一格的条件写)与 `rebindDeviceExecution`(换绑:锁设备行 → 同一事务锁根会话行、读认领,有认领答 `active_run` → 按住客 + 格上属主 + `rev` 的 CAS → 审计行)。
162
+ 绑定不再住在设备店里:执行放置的唯一载体是会话店的执行席。经 `ServiceDeps.deviceEnrollment` 交出的准入链实现 `readPlacement`,`admitSession` 判调用方交来的那一读。
163
+ ② 自建 `StoreBackend`:`approvalAsk()` 的问店铸行时写 `executionLane`、读回带它(必填;幂等命中别人插的行不改);`runOneTimeMigrations()` 的报告多七键
164
+ `executionPlacementColumnsAdded` / `deviceBindingsMoved` / `deviceBindingsOrphaned` / `deviceBindingTablesDropped` / `executionPlacementsBackfilled` / `askExecutionLanesBackfilled` /
165
+ `executionPlacementSessionsUnreadable`(点名行数组 `{ sessionId, fault, errno? }`,`fault` 是闭集 `corrupt_session` / `invalid_session` / `storage`,`errno` 只在 `storage` 时在场;只 local 非空;不跑这段迁移的后端答 0 / 空数组)。
166
+ ③ `OwnerAwareSessionStore` 多两只可选席:`executionPlacement`(`read` / `place` / `markMirror`)与 `forkTo(sourceId, owner, { id, key? })`。不带执行席 ⇒ 不记放置(执行放置读面 501,
167
+ 不判 mirror / writer);不带 `forkTo` ⇒ 带键的 fork 501。包根的 `createStoreBackend()` 造出的会话店都带。
168
+ ④ 自建 authorizer:`AuthContext` 多可选 `head`(门判过的那一读);交了它,执行起点拿它与行上的住客比,不交就不比。包根的 `createAuthorizer` 交它。
169
+ ⑤ `ServiceDeps.executionRuling`(可选,治理组):主体的执行车道裁定,执行起点的唯一输入;缺席 = 不判。
170
+ ⑥ 包根导出型 `ExecutionContext` 的带根两形多 `occupant` / `placement` / `lane` / `device?`,服务形多 `lane` / `placement`;自建 `resolveSpec` 收的 `opts.execution` 仍只有 `{ kind, root }`(第 13 条)。
171
+ ⑦ 直调 `DB_BACKEND=local` 会话店(`createStoreBackend(...).session()`)的 `importEntries` 只建新会话(与两只 SQL 后端同判):id 上已有行或格缺而存档在 ⇒ `SessionError("conflict")`、
172
+ 一个字节不写(上一版 local 店在属主本人的既有行上原地换了日志;SQL 后端一直只建新行,撞已有行答 `storage`,不变)。改写既有会话用 `replaceEntries` 或暂存提交;HTTP 会话同步只走暂存提交,零动作。
173
+
174
+ 18. **契约包 settings-schema 15.0.0 提货(部署面,行为收紧;零数据面改动,回滚 = 回上一版)**:
175
+ ① **车道必需 env 的「缺」判法统一:未设、空串、只含空白一律按缺(所有车道、所有必需键同判)**。此前起服门只判真值,`SSH_HOST=" "` / `E2B_API_KEY=" "` 这类纯空白被当成写了、起服放行、到第一次用才失败;
176
+ 现在 `REMOTE_EXEC=<车道>` 点了名而该车道的任一必需键(`E2B_API_KEY` / `K8S_SANDBOX_IMAGE` / `K8S_NAMESPACE` / `SSH_HOST` / `SSH_USER` / `SSH_PRIVATE_KEY` / `ADB_SERIAL` / `DOCKER_IMAGE`)是这三形之一 ⇒ 起服拒,
177
+ 拒句点名缺的键并说 `unset, empty or whitespace-only — it counts as absent`(与契约包写闸对同一件事的拒句同一句话)。前后带空白但有内容的值原样放行(不 trim)。
178
+ **谁受伤**:必需键写成纯空白的部署(病态输入;典型是模板渲染留下的 `" "`、或只有换行的 secret 文件内容)—— 升级后起服拒;**出路**:填真值,或不点名该车道(`unset REMOTE_EXEC`)。
179
+ ② **compose 用户:`K8S_NAMESPACE` 随根 `docker-compose.yml` / `docker-compose.review.yml` 透传,空串透传 = 按缺**。车道表把 `K8S_NAMESPACE` 列进 k8s 车道的必需栏,两份根 compose 的必需键透传区跟着多一行 `K8S_NAMESPACE: ${K8S_NAMESPACE:-}`;
180
+ `REMOTE_EXEC=k8s` 的 compose 用户必须在宿主环境或 `.env` 里填它(此前 compose 只透传镜像,命名空间要在 override 里自己加;本版起标准透传)。原来没设的部署设成 `sandboxes` 即与升级前逐字同(第 11 条)。
181
+ 其它车道的 compose 用户零动作;随包 chart 与单文件栈清单本来就显式设了,零动作。`deploy/sema-up/lanes.json` 的 k8s 行 `env` 多一键 `K8S_NAMESPACE`(值恒 `null`;读这张表的 cli / web-admin 随表重算)。
182
+ ③ **单机形 `config.d/remote-exec.json` 的 k8s 臂 `namespace` 必填(`CONFIG_PROVIDER=local`)**:契约包读侧对缺它的文件抛,起服沿既有「文件存在而坏 ⇒ 响亮拒」(不静默退回 host),拒句点名 `namespace`。
183
+ **谁受伤**:文件里写了 `"provider":"k8s"` 而没写 `namespace` 的单机部署;**出路**:补 `"namespace": "<沙箱 pod 所在的命名空间>"`(此前靠内置缺省的写 `sandboxes` 即与升级前同)。
184
+ ④ **MCP 条目的 `sessionMetaKey` / `startupTimeoutMs` 从 `config.d/mcp.json` 与中心下发一路承运到引擎(行为面)**:此前这两把键在契约包读腿被剥掉并报 `unknown-keys-dropped`(没有效果),现在原样交给引擎。
185
+ 存量里已经写着它们的条目:合格的值自此生效(引擎在发给这台服务器的每个 `tools/call` 的 `_meta` 里放会话 id / 这台服务器的握手与首次工具列举各带这个启动界);
186
+ 不合格的:`sessionMetaKey` 坏形 ⇒ 这一条 server 整条不挂 + 一行 `web_admin_mcp_session_meta_key_invalid` 点名(兄弟照挂);`startupTimeoutMs` 坏值 ⇒ 引擎在拨号时只拒这一台、点名 `config.mcp_startup_timeout_invalid`(兄弟照挂)。
187
+ **出路**:删掉这把键,或改成引擎收的值(键名格式 / 整数毫秒的判官在引擎,本服务不另判)。
188
+ ⑤ **中心形部署:升级本服务前,中心里存量 workers 文档的 k8s 行先补 `K8S_NAMESPACE`(契约包的部署序:先补键、后升级)**。本服务不消费 workers 域,但拉取腿的契约读法会解析线上载荷里带的 workers 域(整域读不进即抛);
189
+ 中心给 worker 的 `/effective` 视图是否带这个域取决于中心,带的话缺命名空间的 k8s 行让拉取失败、本服务回落 LKG / env。**出路**:中心侧给每个 k8s 行补 `coords.K8S_NAMESPACE`(值 = 沙箱 pod 所在的命名空间)。
190
+ 嵌入方(类型面,加性):`CenterMcpServer` 多可选键 `startupTimeoutMs`(`unknown` 承载形);自建装配零动作。
191
+ 随 15.1.0 判官改 import,行为不变:① 里「空串 / 只含空白」的判法改调契约包根导出的 `isBlankCoordinate`,本仓只留「未设 = 缺」一臂;拒句、放行形、可选键同前,部署者零动作。
192
+
193
+ 19. **嵌入方(构造期 + 类型面,BREAKING;零别名):接了停驻赎回腿的会话店必须带执行席;自写赎回面工厂的入参 `root` → `rootOccupant`**:
194
+ 标准起服零动作 —— 接这条腿的部署(停驻底座在场)用的会话店一律带执行席。自建 `createHttpServer(deps)` 的嵌入方:
195
+ ① 交了 `parkedReviveSurface` 的,`sessionStorage` 必须带执行席 `executionPlacement`:用包根 `createStoreBackend()` 交出的会话店(`backend.session()`;缓存 / 分叉路由包装原样转发)。
196
+ 缺 ⇒ `createHttpServer` 构造期抛,拒句点名这一席。不接停驻赎回腿的,不交 `parkedReviveSurface`,会话店带不带席照旧。
197
+ ② 自写赎回面工厂(`ServiceDeps.parkedReviveSurface`)的第二参从 `{ principal, root?: { before, after } }` 改成 `{ principal, rootOccupant }`:`rootOccupant`(`{ owner, incarnation }`,恒在)
198
+ = 认领之后写口放行的那一位根住客。根住客换没换已由写口判过(换了 ⇒ 决定答 `409 decide.not_parked`,工厂不被调用),工厂只判属主是不是卡的 principal。读旧名 ⇒ tsc 红。
199
+ ③ 删一只有后台子代的根会话走服务端删除路径 `DELETE /v1/sessions/:id`(它先作废 / 停掉以该会话为根的子代,做完才删会话行)。直调会话店的 `deleteSession` 只删行、不结清子代:
200
+ 子代的待决卡仍挂着,同一个 id 再登记之后,对旧卡的决定会复活到新住客的会话上。
201
+
202
+ 20. **会话行条件写(wire 加性;行为变更两处;嵌入方类型面 BREAKING,零别名)**:
203
+ 列表行(`GET /v1/sessions` 每一行、`PATCH /v1/sessions/:id` 的 200 行)多键 `rowTag`(不透明串,只原样回传);`PATCH` 体可带 `expectedRowTag: <读到的 rowTag>` ——
204
+ 行仍是读到的那一份才写,否则 `409 conflict.session_row_changed`、一个字节不写,体 `currentRow` = 此刻的列表行(含新 `rowTag`)。不中一律 409:`currentRow` 的可写位已等于要写的值 ⇒ 视为已同步;
205
+ 否则按它重新决定,再带它的 `rowTag` 发,**不要原样重发**。键形不对 ⇒ `400 request.field_invalid`;只带它、没有 `title` / `pinned` ⇒ `400 request.body_shape`。
206
+ **行为变更**:① 只读镜像会话(从别处导入、写者在别处)上的 `PATCH` 由 200 变 `409 conflict.session_mirror`(体 `forkPath`,与执行面同码同体;带不带键都一样)—— 要在本处改名,先 fork;
207
+ ② 不带键的 `PATCH` 在处理中这只会话的属主被改(同一会话被认领 / 同步重戳)⇒ 404、不写(上一发布版照写)。
208
+ **自动重放的客户端**:带上 `expectedRowTag`;滚动升级窗里落到老副本 ⇒ `400 request.body_shape`(什么都没写)—— 待同步项留着稍后再发,不要去掉键重发。交互式「此刻改名」可以不带(仍是无条件写)。
209
+ **嵌入方**:会话店的行写口 `patchSessionMeta(id, patch, occupant)` 删除,换成 `writeSessionRowCells(id, cells, how)`:`how = { writer: "client", row: <core 行期望>, rowTag? }` 的结局是
210
+ `written`(带行)/ `changed`(带此刻的行)/ `mirror` / `absent`;`{ writer: "replay", row }`(会话同步的行重放)只有 `written` / `absent`。自实现会话店按新签名实现(判序:行期望 → 写者身份 → 标签 → 写,
211
+ 判与写同锁);列表行型 `SessionListItem` / `SessionSummary` 多可选键 `rowTag`。用包根 `createStoreBackend()` 造的三种会话店都已带。没有数据面改动、没有迁移;回滚到上一发布版即恢复无条件写。
212
+
213
+ 21. **配置源与留存扫车道的两条起服拒(行为收紧,fail-closed;零数据面改动;回滚 = 回上一版)**:
214
+ ① `CONFIG_PROVIDER=remote` 而没设 `SEMA_WEB_ADMIN_URL` ⇒ 起服拒,拒句点名两键与出路
215
+ (`CONFIG_PROVIDER=remote names the web-admin center as this worker's config source, but SEMA_WEB_ADMIN_URL is not set — …`)。上一发布版这一形照起:HTTP 服务不建配置源、按纯 env 跑,
216
+ `run-local` 读本地 `config.d`,两处都零句。**升级前自查**:写了 `CONFIG_PROVIDER=remote` 的部署(含按多租 profile 渲出来的)确认 `SEMA_WEB_ADMIN_URL` 在场 —— 要中心就补地址;
217
+ 本就靠 env / 本地 `config.d` 跑的,改成不设 `CONFIG_PROVIDER`(纯 env)或 `CONFIG_PROVIDER=local`。`run-local`(`sema` CLI)只装本地配置源:写了 `CONFIG_PROVIDER=remote`(配没配中心地址都一样)
218
+ ⇒ 一行拒句、退 2;共享 `.env` 里带这个词的,给 CLI 去掉它或写 `local`(没写词时 CLI 自己写 `local`)。
219
+ ② 被要的留存扫车道必须齐:`RETENTION_MODE=enforce` 或 `RETENTION_SWEEP_INTERVAL_SEC > 0` 都是在要车道,要了就得有 `RETENTION_MAX_AGE_DAYS` 且节律 `> 0`;缺一 ⇒ 起服拒,拒句点名缺的键与出路。
220
+ 上一发布版:enforce 而节律缺 / 为 0 照起、什么都不删、`capabilities.retention` 为 `null`(给了天数的只有一行 warn —— 7.138.0 节第 13 条「关着、配了没锁的策略」那一形,enforce 档下改为拒);
221
+ 节律在而天数缺在装配第 6 段(一次性迁移之后)拒,本版在第 1 段拒、拒句换成本条的句子,拒的那一次起服不碰库。**升级前自查**:写了 `RETENTION_MODE=enforce` 的部署确认天数与 `> 0` 的节律都在
222
+ (车道跑在 SQL 后端上);本就不删的改 `RETENTION_MODE=audit-only` 或不设(节律 `> 0` 而没给天数的,同时把节律设 0)。`audit-only` 下给了天数而车道关着照起,warn `retention_policy_not_executed` 多两个字段 `mode` / `retentionSweepIntervalSec`,
223
+ 说明句只说「节律缺或为 0」这个真因 —— 按旧说明句匹配告警的改读字段。
224
+ 运营目录 `GET /v1/config/catalog` 的 `CONFIG_PROVIDER` / `RETENTION_MODE` / `RETENTION_SWEEP_INTERVAL_SEC` 三行 `semantics` 写出这两条拒启条件。
225
+
226
+ 22. **会话规则 / 持久规则桶的文法戳 7(升级序:先升全部读者,再放写;回滚前见下)**:本版写下的会话规则行、SQL 后端 `permission_rule` 的规则桶与规则同步状态都盖文法戳 `7`
227
+ (零 DDL:`grammar` 列早已在)。意思动在拒绝 / 询问桶里身上带 `__` 的 MCP 名:`mcp__a__b` 同时拒 server `a` 的工具 `b` 与 server `a__b` 的每一只工具(限制只会变宽,放行不变;
228
+ 放行表里的条目:本轮工具名单里没有这只精确名、却有另一种 server 拼法下的工具时,按引擎的句子点名、不放任何一只 —— 写成铸出的拼法 `mcp__a_b__<工具>`;名单里有这只精确名的工具则照放这一只)。戳 6 及更早的**会话规则行**保留写者的意思(精确名只拒那一只工具),
229
+ 每层通告一次 `config.session_rules_earlier_grammar`;**持久规则桶**不留旧读法:戳 6 的桶里的整工具 `mcp__a__b` 按文法 7 读(更宽)。7.138.x 及更早的副本读不了戳 7 的行 / 桶 ——
230
+ 整只拒,不按旧文法逐条读(会话行:那条会话每一次调用都拒;规则桶:载零条、拒每一次写,报 `permission_rules.later_grammar`;规则同步:整轮拒)。本版本就要求先停光旧副本再起(SQL 执行放置切换),
231
+ 所以混跑窗口不出现;回滚到 7.138.x 只有恢复备份一条路(第 14 条),**不要**在库里把戳改小。
232
+
233
+ 23. **执行环境的写入改成「暂存 → 一次提交」(行为面;七条车道一条规则;零配置)**:`writeFile` 是整文件替换 —— 整旧或整新,取消 ⇒ 目标未碰;
234
+ host 车道委托引擎的整文件替换,ssh / adb / local-docker / k8s / e2b 先在目标目录下建一只生来 0700 的私有目录 `.<名>.stage-<12 位十六进制>/`、
235
+ 把整份传进去,再在目标机上一次 `mv -f`;`appendFile` 只暂存要加的字节、提交是一次 `cat >>`(inode 不换)。
236
+ **暂存自创建起不比目标更可读**:替换一只 0600 的文件时,新内容在传输期间只有写入方(与 root)读得到(私有目录别的用户进不去;
237
+ 本机侧 adb 推送前落的临时件同样在私有目录里);host 车道的这一保证在引擎原语里:引擎 7.60.0 起目标存在时暂存件以 0600 打开。
238
+ 准备目录、上传、提交、清理是同一个身份:**local-docker 的上传改由容器内以镜像的用户经 stdin 收字节**,不再由守护进程的 `docker cp` 以 root 落盘
239
+ —— 镜像用户 ≠ root 时,写文件与代理的 shell 同一套权限:镜像用户写不了的路径照实拒 `permission_denied`(7.138.x 的 `docker cp` 以 root 写得进)。
240
+ 部署零动作;以下几处语义变了,依赖它们的先核:
241
+ ① **硬链接被断开**:替换之后另一条硬链接留旧内容(与引擎 / 参照实现同形);
242
+ ② **没有写权限位的文件在可写目录里会被替换**(权限位保留;修前就地写被拒 `permission_denied`)—— 防代理改文件靠写门 / 写保护规则,不靠权限位;
243
+ ③ 替换后的文件**属于写入方**(属主不随原件);
244
+ ④ **host 车道上名字超过约 230 字节(随服务进程号的位数浮动)的文件写不进**(引擎的暂存名超出 255 字节上限 ⇒ `unknown` / `ENAMETOOLONG`;远端五车道不受影响)—— 已知缺口,修在引擎;
245
+ ⑤ 目标目录里可能见到一只没清掉的隐藏目录 `.<名>.stage-…/`,可以直接删:一是中断之后清理那一次往返也失败(失败的写的错误句会点名它);二是写已经成功、
246
+ 却因文件系统为仍打开着的文件留了隐藏项(NFS 的 `.nfsXXXX`、FUSE 的 `.fuse_hidden*`、SMB 的待删)而删不掉目录 —— 写照常成功,只在服务的遥测里记一条
247
+ `server.remote-write.staging-dir-left`(不进错误句);
248
+ ⑥ 远端车道的写多一次往返(提交;e2b 多两次:建私有目录 + 提交);
249
+ ⑦ local-docker:镜像用户 ≠ root 的部署,代理以前靠 `docker cp` 的 root 身份写得进、镜像用户自己写不了的路径(例如 root 的目录里不归镜像用户写的文件)
250
+ 现在答 `permission_denied` —— 要写那些路径,让镜像用户有权限(或镜像就以 root 跑);local-docker 新建的文件属镜像用户、权限位按容器内的 umask(以前属 root)。
251
+ **最常见的一处是工作区根本身**:镜像里没有这只目录时,容器启动(`docker run -w <工作区>`)以 root 建出它(root 所有、0755)——
252
+ 镜像用户 ≠ root 的部署,写工作区里的文件从本版起答 `permission_denied`(代理的 shell 在 7.138.x 就写不进那里,只有写文件经 `docker cp` 写得进)。
253
+ **升级前做**:镜像里预建工作区目录并归镜像用户(例如 `RUN mkdir /workspace && chown <镜像用户> /workspace`;已有的目录启动时不改属主),或让镜像以 root 跑。
254
+ ⑧ **私有暂存目录收不成私有时退回就地写,不再静默暂存**:准备那一步把刚建的目录收成 0700 并读回核实(模式、属主);核不过 —— `chmod` 被拒、文件系统不认权限位、
255
+ 父目录的缺省 ACL 掩码收不下来、目标机上没有 `stat` —— 这一次写**不暂存**,退回就地写目标(与「目录不收新名字」同一条退路:非原子;取消落在写目标那一步开始之前(读旧也在这之前)⇒ `aborted`、目标未碰;写目标那一步因取消中断 ⇒ `unknown`、目标可能只写了一半(那一步自己的通道准备也算在内,保守);那一步不收取消(ssh 的 sftp)⇒ 照样写完、答成功;
256
+ ssh / local-docker / k8s 写进原 inode、权限位不变,e2b 的 `files.write` 与 adb 的 `adb push` 整份写目标、inode / 权限位由对端定),并在遥测里记 `server.remote-write.stage-seal-fallback`(计数非零 = 这个目录上的写都在走非原子退路,去看那个目录)。
257
+ **adb 两面在私有目录收不成私有时退回 7.138.x 的形**(写 = 整份 push 到目标;追加 = 读旧 + 拼 + 整份 push;非原子,中途断目标可能没了或只剩半份;`adb push` 不保 inode / 权限位,与 7.138.x 同形)
258
+ 并计 `stage-seal-fallback`。Android 共享存储(`/sdcard` 一类:权限位由挂载派生、不认 `chmod`)上 adb 的写按 sdcardfs / MediaProvider FUSE 的权限语义**推断**每一次都走这一形(未上真机,真机读数候 test)—— 把 adb 的工作区放在 `/data/local/tmp` 一类认权限位的目录即照常暂存。
259
+ 以前这一步的失败被吞掉、字节照样放进一只没核成私有的目录。
260
+ 已知缺口:目标机是 macOS、且工作区父目录带继承的**原生 ACL 允许条目**时,模式位挡不住那个条目(读回 `700` 也一样);FreeBSD / illumos ZFS 的 NFSv4 ACL(`aclmode=passthrough`)同类 ——
261
+ 这类目录上暂存件对那个条目可见,替换之后目标带的是继承来的条目、不是原件自己的 ACL(比 7.138.x 的就地直写宽:就地写不换 inode,原件的 ACL 不变);本版不另造探测臂。
262
+ 回滚到 7.138.x:零数据面动作(暂存名是瞬时的)。
263
+
264
+ 24. **本机宿主车道(`REMOTE_EXEC=host`)上 workflow 子代的 `isolation: "worktree"`(已知变化;行为面)**:本版的引擎(7.60.0)在这条车道上按宿主读,worktree 子代按物理路径比对、「它自己的目录」即收;
265
+ 上一发布版这条车道上这一形一律拒(`isolation.unavailable`)。本版的读法:
266
+ ① **配了包装**(`WORKTREE_ISOLATION_ENABLED=true` 且 `WORKTREE_REPO_ROOT` 指向一个 git 仓):包装铸的 worktree 被收,workflow 子代 `agent(spec, { isolation: "worktree" })` 照跑
267
+ (读已提交副本、调用方检出零写)—— 上一发布版这一形仍拒;
268
+ ② **没配包装**(或仓根不存在 / 里面没有 `.git`、包装被起服关掉 —— 起服日志 `worktree_isolation_disabled_bad_repo_root`;`.git` 在而仓坏的,包装照开、铸树时 git 自己报错):本服务的 host 工厂在铸环境之前拒,码 `isolation.unavailable`,句子点名两只开关,子代零轮 ——
269
+ **与上一发布版同码同句形**。不拒的话引擎的新读法会收下一只本服务新铸的空临时目录:子代读不到项目里任何文件,照烧 token,结果照交回。要在这条车道上用 workflow 的 worktree 隔离,配包装;
270
+ ③ 引擎 7.61.0 起这一形由引擎自己拒,本服务的这一臂到那一版随提货删,届时码与句子随引擎。
271
+ 委派工具形(`Agent({ isolation: "worktree" })` / `agents[].isolation`)不受这一拒影响:worktree 由引擎自己铸,子代的根经 `parentCwd` 交给工厂,工厂收不到 `isolation` 提示。部署零动作。
272
+
10
273
  ## 7.138.0 —— MCP / A2A 的凭据件改由部署的对端座在发送时作答(行为面;件的形更严;嵌入方 `ServiceCoreDeps.peerDesk` 必填,BREAKING)· 🔴 SQL 后端 `session_meta` 加 `workspace` 列、`mailbox_message` 加 `record_id` 列(升级前先加列)· 请求 `projectId` 与检出记号不等 ⇒ 409 · 记忆同步:升级前写的旧游标 ⇒ 该层停同步直到运维一步(两条出路)· settings hook 子环境维持十二名边界 · 抹除授权集与收执 v2 · 已知缺口:会话级子代在父腿作用域结束后首拨 / 再问被座拒 · 引擎 7.59.0
11
274
 
12
275
  1. **SQL 后端:`mailbox_message` 表加一列 `decision`(🔴 升级前先加列;持久数据,不能删表重建;引擎 7.57.0 的邮箱留置决定)**:
@@ -56,6 +319,9 @@
56
319
  `POST /v1/sessions/:id/wake` 与 `/decide` 交给宿主的回执(workflow 子代的审批)不带。**读法**:「已提交」只读 `decisionCommitted === true`,缺席按「没提交」读;
57
320
  它与 `errorCode:"cancelled"`、`retriable:true` 并存时照样成立,不说明卡的当前状态;`retriable:true` 只说「这一跑已重新停驻,请重读待决列表」。
58
321
  经 `POST /v1/tool-approvals/:id/respond` 迟到兑现的停驻卡:同向重试的回放带两键,反向重试的回放(回放首次那一向)不带。
322
+ **同一张卡同向的并发 / 重发请求**:拿到 200 回放体(`idempotent: true`)的,是受理者之后到、而回执(或在飞的赎回)还在的那些 —— 迟到兑现的卡上回执只在受理它的那一副本上、有界(换副本 / 重启 / 被逐 / 过期之后再来的是新一次赎回,答 `409`,终局不变、不会双兑);
323
+ `POST /v1/approvals/:id/decide` 上,首决已落行之后到的同向重发答回放体,与首决真正重叠的输家答 `409`(`conflict.not_resumable` 或 `checkpoint.already_resolved`)。拿到回放体时:迟到兑现的卡上它带着受理体的成员(`status: "resuming"`、`taskId`、两键),
324
+ 所以分清受理者**只看 `idempotent` 是否在场**(缺席 = 受理者),别拿 `status` / `taskId` 数「起了几跑」(回放不起续跑);`/decide` 的幂等回放体则不带 `taskId` / `status` / `checkpointId`(带 `decisionCommitted: true`),要结果面读 `GET /v1/runs/:id`。
59
325
  **竞态收紧**:决一张后台子代的停驻卡时,若这只子代在你决断的同一刻被别处续过、又在新的卡上停驻,本版答 `409 decide.gate_moved`(此前把决定交给了那张新卡)
60
326
  —— 重拉待决列表、对新卡再决。
61
327
  **升级前做**:用封闭 schema 校验这些 200 体的调用方(多余键即拒)把两键加进可选键;其余零动作。**升级后看**:选中那一行的门身份读不出时,既有计数
@@ -82,7 +348,7 @@
82
348
  此后也挂部署座 —— 拒绝计量计入这些腿上的拒绝(拒绝计数的看板会多出 leader 腿的那一份);`TOOL_TRACE=true` 时它们的工具调用也写追踪行、系统提示多出引擎的钩子段
83
349
  (与主车道同配置的腿同读;此前这条车道漏挂)。
84
350
  **要装包的部署**:① 把包放进只读物料树(镜像层 / 只读挂载),`MODS_ROOT` 指它;② 包是**受信的部署代码**,在本进程里跑、可触全部租户与进程凭据 —— 多租户部署上声明的包对全体租户生效,
85
- 准入不是沙箱,只装你审过的包;③ 包按「自包含」构建(执行代码全在 `hooks/register.mjs`,不另 import 包内文件、不读运行时资源),`register` 确定、可重复、不留进程副作用(引擎在每条腿再调它);
351
+ 准入不是沙箱,只装你审过的包;包若在 `PreToolUse` 上答 `allow`(可带 `updatedInput`),引擎仍按最终参数裁决,运维旋钮要问或要拒的(`APPROVAL_REQUIRE` / `APPROVAL_NEVER_AUTO` / `MANUAL_MODE_SHELL_GATE=always` / `governance.commandPolicy` 的问档与拒档等)与引擎强制的问句不因它改变,但**未被强制的**缺省审批问句它能清掉 —— 装包就是把这一类问句的决定权交给包作者;要某类调用无论包怎么答都问人,用这些旋钮声明;③ 包按「自包含」构建(执行代码全在 `hooks/register.mjs`,不另 import 包内文件、不读运行时资源),`register` 确定、可重复、不留进程副作用(引擎在每条腿再调它);
86
352
  ④ 改声明或换包要重启(配置刷新不重载,也没有自动重启信号);滚动升级期间新旧副本可以并存 —— 声明只在设了它的新副本上生效,停驻的任务在哪只副本续跑就按那只副本当下的部署座再准入。
87
353
  **升级后看**:起服日志每包一行 `mods_package_loaded { name, tier, contract, digest }`(`digest` 是引擎读包者读的三件文件的字节摘要,认得出装的是哪几份字节,不是代码完整性凭据);`/health.mods.deploymentPackages` 与能力位 `modsDeploymentPackages`;每条腿 `wiring_manifest.mods[]` 有那只包一行(`seat: "deployment"`、`provenance: "operator"`;本服务自铸的注册那几行带 `provenance: "server"`)。
88
354
  **起服拒句换形(运维面,非 wire)**:部署座准入被拒此前是 `deployment extension registration refused: <引擎句>`,现在是 `deployment mods refused: admission_refused[(<引擎码>)]: <引擎句>` ——
@@ -167,7 +433,7 @@
167
433
  **Who is hurt**(对上一发布版;① ② ③ ⑤ ⑧ 是「那一台不再连上、兄弟照挂、提交照收、运行期才见」:连接期 `wiring_manifest.mcp[].errorCode` / 调用期 `tool_end.errorCode` = `credential_unavailable`,A2A 一侧只有文本;因件的形被引擎读器拒的那一台,服务端没有对应的拒行,看这些码 / 文本;`peer_seat_refused` 只在本服务的座自己拒时才有 —— 引用不在表里 / 绑定不符 / 引用种类座不答):
168
434
  ① stdio 件值含 CR / LF / NUL(多行 PEM、尾换行的 Secret);② stdio 件名不是标识符形(`ODD.NAME`、`MY-VAR`);③ MCP http 的件头名是传输自己写的头(`content-type` · `accept` · `mcp-session-id` · `mcp-protocol-version` · `last-event-id`);
169
435
  ④ MCP 或 A2A 条目的 `principalHeader` 取引擎的断言头名 `connector-assertion`(任何大小写)—— **新提交 `400 request.field_invalid`**(此前受理:上一发布版的引擎只查身份头名与静态头重名);⑤ stdio 的秘密形件(变量名被引擎认作凭据名)的值恰等于一只每个子进程都会继承的变量的值;
170
- ⑥ 嵌入方(下一条);⑦ MCP 条目的 `principalHeader` 取传输自己写的头名(`mcp-session-id` · `mcp-protocol-version` · `content-type` · `accept` · `last-event-id`,任何大小写)—— **入口 400**,与 ④ 同一道门、同一个码,点名 `mcpServers[i].transport.principalHeader`,拒句不回显写的串
436
+ ⑥ 嵌入方(下一条);⑦ MCP 条目的 `principalHeader` 取传输自己写的头名(`mcp-session-id` · `mcp-protocol-version` · `content-type` · `accept` · `last-event-id`,任何大小写)—— **入口 400**,与 ④ 同一道门、同一个码,点名 `mcpServers[i].transport.principalHeader`,拒句里的头名是引擎固定表里的规范化(小写)名,不回显调用方写的原串 / 大小写形(调用方写的恰是小写时,句里的名与所写相同)
171
437
  (A2A 没有传输自有头名表,A2A 只中 ④);⑧ http 件值首尾带 CR / LF(Secret 文件的尾换行;此前由发送时的头规范化吞掉,现在读器拒)。④ ⑦ 的耐久 resume 重放存量体不 4xx:MCP 照挂(引擎拨号时拒那一台),A2A 那一条对端整条不进(warn `a2a_injection_dropped` 点名)。
172
438
  另:⑨ 会话级子代(http / stdio)的已知缺口见第 25 条;⑩ **扣留次序**:引擎把座答里认作凭据的 http 头值与 stdio 秘密形件值记为自己持有的凭据值(服务进程环境里同值的变量不再交给模型可驱动的 host shell 子进程;MCP stdio 子进程只继承 MCP SDK 的少数默认变量〔`HOME` · `LOGNAME` · `PATH` · `SHELL` · `TERM` · `USER`〕与座给的件,拿不到服务进程环境的其余变量),
173
439
  次序是先过读器、放行了才记 —— 被读器拒的形(件值含 CR / LF / NUL、头名不是 HTTP token、两个头名只差大小写、变量名不是标识符形)的值不进扣留表:那一台失败,环境里同值的变量仍交给 host shell 子进程(上一发布版先记后判,同样的形值已在表里);MCP 传输自己写的头名(③)在读器之后的协议层才拒,值此时已在表里。补偿:名册 `headerRefs` 指向的变量值去掉尾换行;件值别与长期留在服务进程环境里的变量同值而又带读器拒的形。
@@ -3654,7 +3920,7 @@ wire 面(同步提交撞 park 的 200 体、`GET /v1/runs/:id` 行形、同一
3654
3920
  - **记忆抹除面的 409 `memory.layer_unwritable` 撤销(随 core 7.31.0 提货;wire 码删除 + 行为面)**:`POST /v1/memory/erase`、`POST /v1/memory/origin/entries/:entryId/clear`、`POST /v1/sessions/:id/memory/erase` 不再在引擎之前做盘上可写预检,这个码不再出现(7.100.0 那条「按 409 体 `chmod u+w <dir>` 后重试」的指路随之撤)—— core 7.31.0 的挂载**一个模式位都不改**(只读层是会话策略:往会话不写的层里写在写点被拒,绕过工具写进去的字节永不被采纳)。7.100.0 上被 409 盖住的三种答复回到 core 自己的读数(非法选择子 400 `config.memory_erasure_request`、清标空理由 422 `memory.origin_clear_invalid`、已提交抹除同 `requestId` 重放的 200 收执),锁住的是别的层时抹除可写层条目照常 200。**升级(一次性)**:存量已锁目录在**最后一个 ≤7.30.x 会话结束后**(= 7.100.0 及更早的 server 副本全部停掉之后)执行 `chmod -R u+w <memoryDir>`(file 记忆引擎的 `<数据根>/memory`)—— 混跑期旧引擎每次只读挂载都会再锁一次,7.31 不替你解。**谁受伤**:没做这一步、又去删一条住在锁住目录里的条目的部署 ⇒ `500`(日志带 core 的同一条 `chmod -R u+w` 补救句),journal 留盘、下次起服失败,照补救句做完即自愈(`docs/DEPLOY-PREREQS.md` 同名段)。**滚动窗内这一形会反复出现**:同一数据根上还有 7.100.0 及更早的副本在跑时,它每次只读挂载一层都会把那一层再锁一次 —— 窗内做过的 `chmod` 会被撤回,7.101.0 副本在那一层上删条目照样 `500` + journal 卡盘。7.101.0 起服时对记忆挂载面探一次:有锁住的层 ⇒ 一条 warn(`engine_notice`,码 `memory.layer_locked_legacy`,`detail.layers` = 层名、`detail.remedy` = 同一句 `chmod -R u+w '<memoryDir>'`),**照常起服**(不拒启:窗内每次 boot 都可能刚被旧副本锁过);看到这一行就先别在那几层上抹除 / 清标,等旧副本全停后执行那一句。按码分支 `memory.layer_unwritable` 的消费方(sdk 错误码目录)删掉那一臂。
3655
3921
  - **settings 层里残留的 `crossSessionInbound: "accept"` ⇒ 跨会话消息 hold(core 7.31.0,行为面)—— 本服务不受影响(只 env 层接线)**:本服务三层来源里只有 env 管理层 `CROSS_SESSION_INBOUND` 接线(user / repo 两层不读),而 env 写 `accept` 自 7.100.0 起就拒启 ⇒ 下面这一形只波及**自持 settings 层的嵌入方**。7.30.x 把这个退役词读成 `next-turn` 并每腿通告一次(7.100.0 本节那句「由引擎自己读成 `next-turn`」随之过期);7.31.0 起它与任何未知词同路 —— 该层进 `invalidLayers`、fail-closed 落 `hold`(每条跨会话消息以 `invalid-setting` 因由扣住,`peer.inbound_disposition` 披露 `held`)。env `CROSS_SESSION_INBOUND=accept` 自 7.100.0 起就拒启,不受影响;`@sema-agent/settings-schema` 6.0.0 的文件层本就拒这个词,所以只有绕过 schema 写进去的 settings 文件会撞上。**迁移**(仅嵌入方):把每一层的 `accept` 改写成 `next-turn`。
3656
3922
  - **子进程剥名收窄(core 7.31.0,运行面;7.100.0 本节「子进程按名剥凭据形变量」一条的修正)**:十个预算名(`MAX_NEW_TOKENS` `MAX_COMPLETION_TOKENS` `AIDER_MAP_TOKENS` `AIDER_MAX_CHAT_HISTORY_TOKENS` `MAX_BATCH_PREFILL_TOKENS` `MAX_WAITING_TOKENS` `MAX_TOP_N_TOKENS` `RATE_LIMIT_TOKENS` `PROMPT_TOKENS` `CONTEXT_TOKENS`)与两个开关(`DB_FOREIGN_KEYS` `JSON_SORT_KEYS`)按**精确名**豁免,重新到得了工具子 shell、git 子进程与本地语言服务器;装饰过的变体(`USERS_MAX_NEW_TOKENS`、`MAX_NEW_TOKENS_2`)仍剥。为这五名改过名的部署可以撤回那一手(留着也无害);`inheritEnv:[names]` 是嵌入方构造执行环境时的选项,不是部署级放回旋钮(7.104.0 起文档写明)。
3657
- - **SQL 后端:`session_meta` 新列 `instruction_source`(存储面)**:存量 MySQL 协议 / PostgreSQL 库升级后**拒启**(`config.schema_mismatch`,拒因里逐字给出 `ALTER TABLE session_meta ADD COLUMN instruction_source VARCHAR(64) NULL;`,PG 为 `… VARCHAR(64) COLLATE "C";`),照它加列或删库重建;滚动序恒为「**先升库、再升副本**」(加列之后,旧副本重启会把它看成幽灵列而拒启)。`DB_BACKEND=local` 与内存形零动作。见上方「SQL 存储面 BREAKING」表。
3923
+ - **SQL 后端:`session_meta` 新列 `instruction_source`(存储面)**:存量 MySQL 协议 / PostgreSQL 库升级后**拒启**(`config.schema_mismatch`,拒因里逐字给出 `ALTER TABLE session_meta ADD COLUMN instruction_source VARCHAR(64) NULL;`,PG 为 `… VARCHAR(64) COLLATE "C";`),照它加列(由部署者自己执行;列集对账与拒启句永不改库);滚动序恒为「**先升库、再升副本**」(加列之后,旧副本重启会把它看成幽灵列而拒启)。`DB_BACKEND=local` 与内存形零动作。见上方「SQL 存储面 BREAKING」表。
3658
3924
  - **沙箱名单形错拒启 + 空串值照注入(行为面)**:`E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV` 写了空段、尾逗号(`A,`)、带 `-` / `.` 的键名(`MY-TOKEN`)或点名名单型旋钮(自己 / 另一只沙箱名单 / `LSP_ENV_ALLOW`)⇒ **拒启**(拒句点名键与第几段,不回显原值;修前静默吞或照转发)。点到的键值为空串(compose `X=${X:-}` 在宿主没设时的常态)⇒ **照注入空串**(修前不注入):local-docker 上 `-e X=` 盖掉镜像里的同名缺省值,e2b 上盖掉 `SANDBOX_PKG_SOURCE` 推导出的同名包源变量。**迁移**:按拒句修名单;不想盖缺省值就把那个名从名单里删掉。
3659
3925
  - **`WEB_SEARCH_ENDPOINT` 带 userinfo ⇒ 拒启(行为面)**:`WEB_SEARCH_PROVIDER` 点名了后端、`WEB_SEARCH_ENDPOINT` 形如 `https://user:pw@host/…`(或只带 `user@`)的部署升级后拒启(`config.web_search_invalid`);每请求 `settings.webSearch.endpoint` 带这一形从 202 变 `400 request.field_invalid`(修前这一形本来就用不了:fetch 每次都抛,错误文本还带出口令)。**迁移**:brave / tavily 的凭据走 `WEB_SEARCH_API_KEY`(或 `settings.webSearch.apiKey`);带 basic-auth 的 SearXNG 在前面放一层注入鉴权的反代。
3660
3926
  - **配置目录两行 `csv` → `secret`(wire 形变)**:`GET /v1/config/catalog` 的 `E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV` 两行 `type` `csv` → `secret`、`valueClass` `plain` → `secret`、`effectiveValue` 从名字数组变 `{ present }`,`summary.secret` 计数 +2。读这两行名单内容的消费方改读起服行 `remote_exec_enabled.sandboxEnvKeys`(只键名);这两只旋钮的原文从此也不进 host shell / git 子进程 / 语言服务器(在这些子进程里读它们的脚本收不到了)。
@@ -3680,16 +3946,19 @@ wire 面(同步提交撞 park 的 200 体、`GET /v1/runs/:id` 行形、同一
3680
3946
 
3681
3947
  ## SQL 存储面 BREAKING(3.0.0 之后;列增批自 7.101.0 起入表)
3682
3948
 
3683
- **常设口径**:本仓**不出 `ALTER TABLE` 增量迁移**(成文裁定:预生产期零存量用户窗口,schema 变更
3684
- 一律删表/删库重建);boot 对旧形 schema **拒启并带恢复动作文案**,不会静默跑在错形表上。只用
3685
- file/local 存储形(未配 SQL 后端)的部署不受本节任何条目影响。
3949
+ **常设口径**:代码是 schema 的唯一真源,启动 DDL 里没有 `ALTER TABLE` 增量 seam;列集对账与拒启句永不改库 —— boot 对旧形
3950
+ schema **拒启并打印要执行的 `ALTER TABLE … ADD COLUMN …`**,不会静默跑在错形表上。进程自己对已存在的表发的 DDL 只有两种:幂等加索引(只动索引、不碰列)与
3951
+ 登记在册的一次性迁移(每条有窗、有登记行);加列 / 改列 / 改名 / 删表只有一次性迁移做。出路是闭集三条,没有销毁性的一条:
3952
+ 部署与这个库匹配的构建;库比构建旧(缺列)⇒ 部署者自己执行拒启句打印的 `ALTER`;库比构建新(多列)⇒ 部署持有那些列的构建(先升库、再升副本)。
3953
+ 下表前三行是预生产期(零存量用户窗口)各版当年的升级动作,保留作历史;今天的升级动作以拒启句为准。
3954
+ 只用 file/local 存储形(未配 SQL 后端)的部署不受本节任何条目影响。
3686
3955
 
3687
3956
  | 窗 | 变更 | 升级动作 |
3688
3957
  |---|---|---|
3689
3958
  | **7.6.0**(2026-08-08) | SQL 双端归一化第一刀:隔离键排序规则钉死(MySQL 腿表级 `COLLATE utf8mb4_bin`/PG 腿逐列 `COLLATE "C"`)+索引名 36 条+列名/宽度收窄 | **删库重建**(两方言) |
3690
3959
  | **7.8.0**(2026-08-09) | SQL 命名三轴归一化第二刀:9 张表名单数化、epoch 毫秒列补 `_ms` 后缀 5 列、approval 两表 `version→rev`;wire 面零变化 | **删库重建**(两方言) |
3691
3960
  | **7.14.0**(2026-08-12) | `tool_result` 换代(随 core 5.26.0 的 ref 格式换代):ref 四段单射形、主键 `VARCHAR(190)→518`、新增出处两列 `owner_session_id`/`owner_task_id` | 升级前两方言 **`DROP TABLE tool_result`**(不删=boot 拒启;offload 产物是可恢复窗缓存,转录内联预览不受影响) |
3692
- | **7.101.0** | `session_meta` 新列 `instruction_source VARCHAR(64) NULL`(PG `COLLATE "C"`;项目指令换源通告的基线) | 两方言按拒启句里给出的 `ALTER TABLE session_meta ADD COLUMN …` 加列,或删库重建;**先升库、再升副本**(加列后旧副本重启见幽灵列拒启) |
3961
+ | **7.101.0** | `session_meta` 新列 `instruction_source VARCHAR(64) NULL`(PG `COLLATE "C"`;项目指令换源通告的基线) | 两方言按拒启句里给出的 `ALTER TABLE session_meta ADD COLUMN …` 由部署者自己加列;**先升库、再升副本**(加列后旧副本重启见幽灵列拒启) |
3693
3962
 
3694
3963
  > 列增批此前只记在 CHANGELOG / `docs/DEPLOY-PREREQS.md`(7.92.0 `session_policy`、7.93.4 `approval_ask.settled_by` 各加过一列),自 7.101.0 起同样入本表。7.101.0 这一列的拒启句自带可粘贴的 `ALTER`,本仓仍不替你执行。
3695
3964
 
package/README.md CHANGED
@@ -133,6 +133,9 @@ curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>"
133
133
  7.104.0 the execution-lane start refusals (`LEADER_ENABLED` on the host lane, a multi-tenant posture on a
134
134
  shared lane, the device lane without its durable stores) are judged there, not in `loadConfig()`;
135
135
  `assertExecutionLaneInvariants(config)` is the same judgment without the `config.d/remote-exec.json` override.
136
+ `createHttpServer` requires the `authorize` seat — the only resolver of a submission's root session (the server stamps
137
+ that root onto every spec it runs; `resolveSpec` returns no `sessionId` and reads `opts.execution.root` when it needs it).
138
+ A single-user embedding passes `createAuthorizer(config, runner.sessions)` (package root); omitting the seat refuses at construction.
136
139
  Build-time tools (e.g. a catalog editor) read two allowlists from the package root instead of hand-copying them: `RETIRED_ENVS` (the retired-env
137
140
  tombstones) and, since 7.110.0, `CATALOG_ENTRY_HEADER_LANES` / `CATALOG_ENTRY_HEADER_NAMES` (+ type `CatalogEntryHeaderName`) — the closed allowlist of
138
141
  model-catalog entry `headers` and the API line that reads each name. Both are frozen, the very objects the server judges with.
@@ -147,7 +150,7 @@ curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>"
147
150
  result and exits — no HTTP server, no submission auth gate.
148
151
  - **The deployment governance knobs apply here too** (since 7.5.0 — before that this leg assembled its
149
152
  task with the governance chain absent, so the knobs were silently inert): `AUTONOMY`,
150
- `runtime.commandPolicy` (from `config.d/governance.json`) and `SENSITIVE_WRITE_PATTERNS` are compiled
153
+ `governance.commandPolicy` (from `config.d/governance.json`) and `SENSITIVE_WRITE_PATTERNS` are compiled
151
154
  onto the local task exactly as they are on the HTTP leg, tighten-only, anchored at the
152
155
  `--workspace` directory.
153
156
  - **Off switches**: `SENSITIVE_WRITE_PATTERNS=off` **or** an empty value (`SENSITIVE_WRITE_PATTERNS=`)
@@ -196,11 +199,11 @@ The server is configured entirely through environment variables. The most import
196
199
  | `SERVICE_AUTH_TOKEN` | — | Callers must send `Authorization: Bearer <token>` |
197
200
  | `DB_BACKEND` | `local`* | `mysql` (any MySQL-protocol DB: MySQL/TiDB/MariaDB) / `pg` (PostgreSQL) / `local` (file-backed, no DB) / `memory` (explicit in-memory: nothing survives a restart, durable-runs faces 501). *Bare boot (no DB env at all) defaults to `local` so a single-user machine keeps its runs across restarts; any SQL signal (`SESSION_BACKEND` or `MYSQL_/PG_HOST`) keeps the `mysql` engine default, and `REQUIRE_PRINCIPAL=true` bare boots stay `memory`. (`TIDB_HOST`/`_PORT`/`_USER`/`_PASSWORD`/`_DATABASE`/`_POOL_SIZE` are retired names, not an alias — since 5.0.0 setting any of them refuses to boot, pointing at the `MYSQL_*` replacement; TiDB itself connects fine through `MYSQL_*`, since it speaks the MySQL protocol.) (the local file store has no tenant isolation — a warning says so). A DEFAULT-derived `local` that cannot create its data root degrades to memory with a warning + the `store_backend_degraded` gauge; an EXPLICIT `DB_BACKEND=local` fails loud instead. Setting `mysql`/`pg` explicitly also switches sessions to durable |
198
201
  | `SESSION_BACKEND` | `memory`* | `memory` / `mysql` (durable session center) / `auto`. *Defaults to durable when `DB_BACKEND` is explicitly `mysql`/`pg`. (`tidb` is a RETIRED public name — setting it refuses to boot; since 7.57.0 `GET /v1/config/catalog` also echoes the public word `mysql` instead of the internal `tidb` label) |
199
- | `REMOTE_EXEC` | unset | Sandbox execution lane: `host` / `local-docker` / `e2b` / `k8s` / `ssh` / `adb`; unset = in-process stub (with `CONFIG_PROVIDER=local` the default becomes `host`). Naming a lane without its required env (e.g. `e2b` without `E2B_API_KEY`) or an unrecognized value **refuses to start** — no silent downgrade to the host/in-process lane (fail-closed). Under `CONFIG_PROVIDER=local` a `config.d/remote-exec.json` **wins over** this env (deliberate: the file is the single-machine source of truth). Since 7.57.0 that preemption is **loud** — `remote_exec_lane_preempted_by_file` names the preempted lane, the effective one, and whether it is an **isolation downgrade** (an isolated lane replaced by a non-isolated one); a file naming a not-yet-wired isolated lane (`e2b`/`k8s`/`ssh`/`adb`) leaves the env lane in force and says so |
202
+ | `REMOTE_EXEC` | unset | Sandbox execution lane: `host` / `local-docker` / `e2b` / `k8s` / `ssh` / `adb`; unset = in-process stub (with `CONFIG_PROVIDER=local` the default becomes `host`). Naming a lane without its required env (e.g. `e2b` without `E2B_API_KEY`; an unset, empty or whitespace-only value counts as absent, for every lane and every required key) or an unrecognized value **refuses to start** — no silent downgrade to the host/in-process lane (fail-closed). `k8s` needs `K8S_SANDBOX_IMAGE` **and** `K8S_NAMESPACE` — the sandbox pods' namespace has no built-in default (the deployment declares it; unset, empty or blank refuses to start; the bundled chart sets it from `sandbox.namespace`). Under `CONFIG_PROVIDER=local` a `config.d/remote-exec.json` **wins over** this env (deliberate: the file is the single-machine source of truth). Since 7.57.0 that preemption is **loud** — `remote_exec_lane_preempted_by_file` names the preempted lane, the effective one, and whether it is an **isolation downgrade** (an isolated lane replaced by a non-isolated one); a file naming a not-yet-wired isolated lane (`e2b`/`k8s`/`ssh`/`adb`) leaves the env lane in force and says so |
200
203
  | `SSH_HOST_FINGERPRINT` / `SSH_KNOWN_HOSTS` | unset | **SSH lane host-key verification** (`REMOTE_EXEC=ssh` only). `SSH_HOST_FINGERPRINT` = the host key's SHA256 base64 fingerprint (`SHA256:` prefix and `=` padding both optional); `SSH_KNOWN_HOSTS` = a path to a `known_hosts` file, matched literally on `host` / `[host]:port`. **Either one present ⇒ strict verification, a mismatch REFUSES the connection** (the error carries both fingerprints plus an `ssh-keyscan` line); both set ⇒ both must pass. **Both unset ⇒ a loud per-connection warning (`ssh_host_key_unverified`, man-in-the-middle risk) and the connection proceeds** — the deliberate posture for the batch-deploy lane, registered as a P-DEBT fail-open (`docs/FAIL-OPEN-CENSUS.md`). A malformed fingerprint or an unreadable `known_hosts` path **refuses to start**. Not supported in `known_hosts`, on purpose: hashed (`\|1\|…`) lines, wildcard patterns, `@cert-authority` — all skipped, so a host whose only entries are those is REFUSED (`@revoked` is honored and wins over any plain line, whatever the order) |
201
204
  | `HOOKS_TIMEOUT_MS` | unset (600000) | Time budget of each handler of the request `settings.hooks` registration (one event's run of the request's hook entries). Unset = 600000 ms — this is the engine's `budgetMs` for the registration (each handler's own time, one event's run of the request's hook entries); when the budget is spent, or the task is cancelled, the command hooks still running in that handler are stopped (SIGTERM, a short grace, then SIGKILL) — they no longer run on to their own `timeout`; the engine's default for an extension (10 s) is not used. `0` is honored as written (a zero budget). The server's own two observer registrations (deny meter, tool trace) do not read it. A non-integer, negative, or above-`setTimeout`-ceiling (2147483647) value **refuses to start** |
202
- | `MODS_ROOT` / `MODS_PACKAGES` | unset | **Deployment mod packages** loaded into the deployment seat. `MODS_PACKAGES` = a JSON array of `{ "name", "dir", "tier", "options"? }`: `name` is the package's own name (its `.claude-plugin/plugin.json` `name`, spelled exactly); `dir` an absolute path whose real path must sit strictly under the real path of `MODS_ROOT` (an absolute path; compared by path components — a symlink out of the root, a look-alike sibling such as `/mods-evil`, a `..` traversal, or the root itself all refuse to start); `tier` is required (`prepend` / `user` / `append` — the host declares it, a package never does); `options` is handed to the package's `register` as is. The engine's package reader is the only reader of the package files; this server never reads them. Every declared package is loaded once at boot and the whole list goes through the engine's admission: any failure (bad declaration, outside the root, the reader refuses the package, a name mismatch, the admission refuses) **refuses to start** with one single-line sentence naming the slot or the package — the server's own sentence never echoes declared values (paths, `options`); the reader's / admission's own words are passed through (credential shapes redacted, control characters stripped) and may name the package directory. Loaded ⇒ one `mods_package_loaded` log line per package (carrying the engine reader's `digest` — a digest of the bytes of the three package files it read, which tells you which bytes were loaded and is not a code-integrity credential: code the module loads on its own is not in it), `/health.mods.deploymentPackages` and the capability `modsDeploymentPackages: true`; unset (or `[]`) ⇒ both faces stay exactly as without these knobs. A package is **trusted deployment code** — it runs in this process with access to every tenant and the process credentials, the admission is not a sandbox — so install it only from a read-only artifact tree you control. Changing the declaration or a package needs a restart (no automatic restart signal). The config catalog shows `MODS_PACKAGES` as present / length only. `options` is not a credential channel: this server does not withhold package credentials — `MODS_PACKAGES` itself and any variable a package reads are inherited by the model's child processes on the host lane under `inheritEnv: "all"` (only this server's declared own credentials are withheld), so deliver package credentials through a channel the host-lane model shell cannot read |
203
- | `CONFIG_PROVIDER` | unset | Config source: `local` (file-backed `config.d/`, single machine) / `remote` (registry control plane). Closed set, matched exactly (7.104.0+): any other value (wrong case, surrounding spaces) refuses to boot; empty = unset |
205
+ | `MODS_ROOT` / `MODS_PACKAGES` | unset | **Deployment mod packages** loaded into the deployment seat. `MODS_PACKAGES` = a JSON array of `{ "name", "dir", "tier", "options"? }`: `name` is the package's own name (its `.claude-plugin/plugin.json` `name`, spelled exactly); `dir` an absolute path whose real path must sit strictly under the real path of `MODS_ROOT` (an absolute path; compared by path components — a symlink out of the root, a look-alike sibling such as `/mods-evil`, a `..` traversal, or the root itself all refuse to start); `tier` is required (`prepend` / `user` / `append` — the host declares it, a package never does); `options` is handed to the package's `register` as is. The engine's package reader is the only reader of the package files; this server never reads them. Every declared package is loaded once at boot and the whole list goes through the engine's admission: any failure (bad declaration, outside the root, the reader refuses the package, a name mismatch, the admission refuses) **refuses to start** with one single-line sentence naming the slot or the package — the server's own sentence never echoes declared values (paths, `options`); the reader's / admission's own words are passed through (credential shapes redacted, control characters stripped) and may name the package directory. Loaded ⇒ one `mods_package_loaded` log line per package (carrying the engine reader's `digest` — a digest of the bytes of the three package files it read, which tells you which bytes were loaded and is not a code-integrity credential: code the module loads on its own is not in it), `/health.mods.deploymentPackages` and the capability `modsDeploymentPackages: true`; unset (or `[]`) ⇒ both faces stay exactly as without these knobs. A package is **trusted deployment code** — it runs in this process with access to every tenant and the process credentials, the admission is not a sandbox — so install it only from a read-only artifact tree you control. If a package answers `allow` on `PreToolUse` (optionally with `updatedInput`), the engine still adjudicates the final arguments, and what an operator knob asks or refuses (`APPROVAL_REQUIRE`, `APPROVAL_NEVER_AUTO`, `MANUAL_MODE_SHELL_GATE=always`, the ask and deny tiers of `governance.commandPolicy`, …) or the engine mandates does not change because of it — but it can clear an **unmandated** default approval question, so installing a package hands that class of questions to the package author; to have a call asked whatever a package answers, declare it with those knobs. Changing the declaration or a package needs a restart (no automatic restart signal). The config catalog shows `MODS_PACKAGES` as present / length only. `options` is not a credential channel: this server does not withhold package credentials — `MODS_PACKAGES` itself and any variable a package reads are inherited by the model's child processes on the host lane under `inheritEnv: "all"` (only this server's declared own credentials are withheld), so deliver package credentials through a channel the host-lane model shell cannot read |
206
+ | `CONFIG_PROVIDER` | unset | Config source: `local` (file-backed `config.d/`, single machine) / `remote` (registry control plane at `SEMA_WEB_ADMIN_URL` — `remote` without that URL refuses to boot). Unset = the center if `SEMA_WEB_ADMIN_URL` is set, else env only. Closed set, matched exactly (7.104.0+): any other value (wrong case, surrounding spaces) refuses to boot; empty = unset |
204
207
  | `DEFAULT_SCENARIO` | `code` | Default scenario when the request body names none; must be a scenario this deployment has (boot refuses otherwise) |
205
208
  | `SANDBOX_PKG_SOURCE` | `global` | Package sources inside sandboxes: `global` (official upstreams) / `cn` (China mirrors) / `custom` / `none` |
206
209
  | `SENSITIVE_WRITE_PATTERNS` | core's recommended set | Sensitive-path write deny list; comma-separated value replaces the set, `off` **or an empty value** disables. Applied unconditionally at the governance layer (independent of client permission mode, lane or settings presence) — including on the `run-local` leg. A value that cannot compile into a guard set (e.g. `/`, a pattern with no path segment) refuses to start |
package/README.zh-CN.md CHANGED
@@ -115,6 +115,8 @@ curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>"
115
115
  `composeExecutionLane(config, ctx)` —— HTTP 服务与 `run-local` 都这么做。7.104.0 起执行车道的几条拒启(host 车道上开
116
116
  `LEADER_ENABLED`、多租户姿态落在共享车道、device 车道缺耐久店)判在这一步,不在 `loadConfig()` 里;
117
117
  `assertExecutionLaneInvariants(config)` 是同一份判据,不做 `config.d/remote-exec.json` 覆写。
118
+ `createHttpServer` 的 `authorize` 席是必填:它是一次提交的根会话唯一的来源(server 把这只根盖进它执行的每一只 spec;`resolveSpec`
119
+ 不返回 `sessionId`,要用根时读 `opts.execution.root`)。单用户嵌入传 `createAuthorizer(config, runner.sessions)`(包根导出);省掉即构造期拒。
118
120
  构建期工具(如目录编辑页)从包根取两张表、不手抄:`RETIRED_ENVS`(退役 env 墓碑)与 7.110.0 起的 `CATALOG_ENTRY_HEADER_LANES` /
119
121
  `CATALOG_ENTRY_HEADER_NAMES`(+ 型 `CatalogEntryHeaderName`)—— 模型目录条目 `headers` 的闭集白名单及读每个名字的线路。两者都冻结,就是服务端自己判据用的那一只。
120
122
  壳要知道一台在跑的引擎服务的是哪张模型表、目录采纳了没有、起服路由判官怎么说,读 `GET /v1/models` 的 `modelPlane`
@@ -126,7 +128,7 @@ curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>"
126
128
  - `run-local` 在进程内启同一套引擎,把命令行给的目标跑完一次、打印结果、退出——没有 HTTP 服务,
127
129
  也没有提交鉴权门。
128
130
  - **部署治理旋钮在这条腿上同样生效**(7.5.0 起;此前这条腿自建任务时整条治理链缺席,旋钮静默失效):
129
- `AUTONOMY`、`runtime.commandPolicy`(来自 `config.d/governance.json`)与 `SENSITIVE_WRITE_PATTERNS`
131
+ `AUTONOMY`、`governance.commandPolicy`(来自 `config.d/governance.json`)与 `SENSITIVE_WRITE_PATTERNS`
130
132
  与 HTTP 腿同样地 tighten-only 编译进本地任务,裁决锚在 `--workspace` 目录。
131
133
  - **关闸通道**:`SENSITIVE_WRITE_PATTERNS=off` **或**留空(`SENSITIVE_WRITE_PATTERNS=`)都关掉守卫集;
132
134
  `AUTONOMY` 不设(或设 `auto`)= 不额外收紧。守卫集若给了**编译不出来**的值(例如 `/` 这种不含任何
@@ -166,11 +168,11 @@ curl -s localhost:8090/v1/tasks -H "Authorization: Bearer <SERVICE_AUTH_TOKEN>"
166
168
  | `SERVICE_AUTH_TOKEN` | — | 调用方需带 `Authorization: Bearer <token>` |
167
169
  | `DB_BACKEND` | `local`* | SQL 引擎:`mysql`(任何 MySQL 协议库:MySQL/TiDB/MariaDB —— TiDB 走这个值,**没有** `tidb` 别名,写它启动即拒)/ `pg`(PostgreSQL)/ `local`(免 DB 文件持久化)/ `memory`(显式纯内存)。显式设置 `mysql`/`pg` 时 session 自动转 durable。*单租户裸 boot 缺省 `local`;`REQUIRE_PRINCIPAL=true` 的多租户裸 boot 缺省 `memory`(local 与多租户互斥) |
168
170
  | `SESSION_BACKEND` | `memory`* | `memory` / `mysql`(durable 会话中心)/ `auto`。*显式 `DB_BACKEND=mysql/pg` 时默认转 durable。(`tidb` 是**退役公名**,设了拒启;7.57.0 起 `GET /v1/config/catalog` 的回显也归一成公名 `mysql`,不再吐内部标签 `tidb`) |
169
- | `REMOTE_EXEC` | 未设 | 沙箱执行通道:`host` / `local-docker` / `e2b` / `k8s` / `ssh` / `adb`;未设 = 进程内 stub(`CONFIG_PROVIDER=local` 时缺省转 `host`)。点名了通道但必需 env 不全(如 `e2b` 缺 `E2B_API_KEY`)或值不在闭集内 ⇒ **启动即拒**——不再静默降级到 host/进程内通道(fail-closed)。`CONFIG_PROVIDER=local` 下 `config.d/remote-exec.json` **压过**本 env(刻意:单机形的真源是那份文件);7.57.0 起这次抢占**响亮**——`remote_exec_lane_preempted_by_file` 点名被抢占的 lane、最终生效的 lane,以及它是不是一次**隔离降级**(隔离 lane 被换成不隔离的);文件点了尚未接线的隔离 lane(`e2b`/`k8s`/`ssh`/`adb`)时 env 腿照旧生效并说明 |
171
+ | `REMOTE_EXEC` | 未设 | 沙箱执行通道:`host` / `local-docker` / `e2b` / `k8s` / `ssh` / `adb`;未设 = 进程内 stub(`CONFIG_PROVIDER=local` 时缺省转 `host`)。点名了通道但必需 env 不全(如 `e2b` 缺 `E2B_API_KEY`;未设、空串或只含空白一律按缺,所有通道、所有必需键同判)或值不在闭集内 ⇒ **启动即拒**——不再静默降级到 host/进程内通道(fail-closed)。`k8s` 必需 `K8S_SANDBOX_IMAGE` **和** `K8S_NAMESPACE` —— 沙箱 pod 所在的命名空间没有内置缺省(部署自己声明;缺席、空串或只含空白 ⇒ 启动即拒;随包 chart 取 `sandbox.namespace` 显式设)。`CONFIG_PROVIDER=local` 下 `config.d/remote-exec.json` **压过**本 env(刻意:单机形的真源是那份文件);7.57.0 起这次抢占**响亮**——`remote_exec_lane_preempted_by_file` 点名被抢占的 lane、最终生效的 lane,以及它是不是一次**隔离降级**(隔离 lane 被换成不隔离的);文件点了尚未接线的隔离 lane(`e2b`/`k8s`/`ssh`/`adb`)时 env 腿照旧生效并说明 |
170
172
  | `SSH_HOST_FINGERPRINT` / `SSH_KNOWN_HOSTS` | 未设 | **SSH 通道主机密钥校验**(仅 `REMOTE_EXEC=ssh`)。前者 = 主机公钥的 SHA256 base64 指纹(`SHA256:` 前缀与 `=` 填充都可选);后者 = `known_hosts` 文件路径,按 `host` / `[host]:port` **逐字**匹配行。**任一在场 ⇒ 严格校验,不符即拒连**(拒因带两半指纹 + `ssh-keyscan` 取指纹指路一行);两只同时在场 ⇒ 合取(都要过)。**两只全缺席 ⇒ 每连接一条响亮 warn(`ssh_host_key_unverified`,中间人风险)后照常连** —— 批量部署通道的既定姿态,已登记为 P-DEBT fail-open(`docs/FAIL-OPEN-CENSUS.md`)。指纹拼错 / `known_hosts` 路径读不到 ⇒ **启动即拒**。`known_hosts` 里**刻意不支持**:hashed(`\|1\|…`)行、通配符 pattern、`@cert-authority` —— 一律跳过,于是只有这些条目的主机会被**拒连**(`@revoked` 照常生效,且无论写在哪一行都优先于普通匹配行) |
171
173
  | `HOOKS_TIMEOUT_MS` | 未设(600000) | 请求 `settings.hooks` 那张扩展注册**每个 handler** 的时间预算(一个事件跑一遍本请求的钩子条目)。未设 = 600000 ms —— 这是引擎对这张注册的 `budgetMs`(每个 handler 自己的时间:一个事件跑一遍本请求的钩子条目);预算到、或任务被取消,这个 handler 里还在跑的命令钩子会被叫停(SIGTERM → 短宽限 → SIGKILL),不再跑到自己的 `timeout`;不用引擎给扩展的缺省(10 s)。`0` **按字面生效**(零预算)。本服务自己的两张观察注册(拒绝计量 / 工具追踪)不读它。非整数 / 负数 / 超 `setTimeout` 上限(2147483647)⇒ **启动即拒** |
172
- | `MODS_ROOT` / `MODS_PACKAGES` | 未设 | **部署座装载的 mod 包**。`MODS_PACKAGES` = JSON 数组,每项 `{ "name", "dir", "tier", "options"? }`:`name` = 包自己的名(`.claude-plugin/plugin.json` 的 `name`,逐字同);`dir` 绝对路径,真实路径须严格落在 `MODS_ROOT`(绝对路径)的真实路径之下(按路径分量判 —— 符号链接出根、`/mods-evil` 这类前缀相似的兄弟目录、`..` 遍历、根本身都拒启);`tier` 必填(`prepend` / `user` / `append`,宿主声明,包不自报);`options` 原样交包的 `register`。包里的文件只有引擎的读包者一只读者,本服务不读。每只声明的包起服装一次、整列交引擎准入:任一失败(声明坏、越根、读包者拒包、名不同、准入拒)⇒ **拒启**,一行拒句点名哪一席或哪只包 —— 本服务自己写的句子不回显声明的值(路径、`options`);读包者 / 准入交回的话原样透传(凭据形换记号、剥控制字符),其中可能点名包目录。装上了 ⇒ 每包一行 `mods_package_loaded` 日志(带引擎读包者交回的 `digest` —— 装载件三件字节摘要,认得出装的是哪几份字节,非完整性凭据:模块自己再加载的代码不在其内)、`/health.mods.deploymentPackages` 与能力位 `modsDeploymentPackages: true`;未设(或 `[]`)⇒ 两面与没有这两根旋钮时逐字节同。包是**受信的部署代码** —— 在本进程里跑、可触全部租户与进程凭据,准入不是沙箱 —— 只从你控制的只读物料树装。改声明或换包要重启(没有自动重启信号)。配置目录对 `MODS_PACKAGES` 只回在场位与长度。`options` 不是凭据通道:本服务不替包扣凭据 —— `MODS_PACKAGES` 本身与包自己读的任何变量,在宿主车道 `inheritEnv: "all"` 下都会被模型的子进程继承(只有本服务声明的自有凭据被扣),包要的凭据请经宿主车道模型 shell 读不到的渠道交付 |
173
- | `CONFIG_PROVIDER` | 未设 | 配置来源:`local`(单机文件 `config.d/`)/ `remote`(registry 控制面)。闭集、逐字比较(7.104.0 起):其余任何值(大小写不同、带首尾空白)拒启;空串 = 未设 |
174
+ | `MODS_ROOT` / `MODS_PACKAGES` | 未设 | **部署座装载的 mod 包**。`MODS_PACKAGES` = JSON 数组,每项 `{ "name", "dir", "tier", "options"? }`:`name` = 包自己的名(`.claude-plugin/plugin.json` 的 `name`,逐字同);`dir` 绝对路径,真实路径须严格落在 `MODS_ROOT`(绝对路径)的真实路径之下(按路径分量判 —— 符号链接出根、`/mods-evil` 这类前缀相似的兄弟目录、`..` 遍历、根本身都拒启);`tier` 必填(`prepend` / `user` / `append`,宿主声明,包不自报);`options` 原样交包的 `register`。包里的文件只有引擎的读包者一只读者,本服务不读。每只声明的包起服装一次、整列交引擎准入:任一失败(声明坏、越根、读包者拒包、名不同、准入拒)⇒ **拒启**,一行拒句点名哪一席或哪只包 —— 本服务自己写的句子不回显声明的值(路径、`options`);读包者 / 准入交回的话原样透传(凭据形换记号、剥控制字符),其中可能点名包目录。装上了 ⇒ 每包一行 `mods_package_loaded` 日志(带引擎读包者交回的 `digest` —— 装载件三件字节摘要,认得出装的是哪几份字节,非完整性凭据:模块自己再加载的代码不在其内)、`/health.mods.deploymentPackages` 与能力位 `modsDeploymentPackages: true`;未设(或 `[]`)⇒ 两面与没有这两根旋钮时逐字节同。包是**受信的部署代码** —— 在本进程里跑、可触全部租户与进程凭据,准入不是沙箱 —— 只从你控制的只读物料树装。包若在 `PreToolUse` 上答 `allow`(可带 `updatedInput`),引擎仍按最终参数裁决,运维旋钮要问或要拒的(`APPROVAL_REQUIRE` / `APPROVAL_NEVER_AUTO` / `MANUAL_MODE_SHELL_GATE=always` / `governance.commandPolicy` 的问档与拒档等)与引擎强制的问句不因它改变,但**未被强制的**缺省审批问句它能清掉 —— 装包就是把这一类问句的决定权交给包作者;要某类调用无论包怎么答都问人,用这些旋钮声明。改声明或换包要重启(没有自动重启信号)。配置目录对 `MODS_PACKAGES` 只回在场位与长度。`options` 不是凭据通道:本服务不替包扣凭据 —— `MODS_PACKAGES` 本身与包自己读的任何变量,在宿主车道 `inheritEnv: "all"` 下都会被模型的子进程继承(只有本服务声明的自有凭据被扣),包要的凭据请经宿主车道模型 shell 读不到的渠道交付 |
175
+ | `CONFIG_PROVIDER` | 未设 | 配置来源:`local`(单机文件 `config.d/`)/ `remote`(`SEMA_WEB_ADMIN_URL` 那只 registry 控制面 —— 写了 `remote` 而没设这个地址拒启)。未设 = 设了 `SEMA_WEB_ADMIN_URL` 即中心,否则纯 env。闭集、逐字比较(7.104.0 起):其余任何值(大小写不同、带首尾空白)拒启;空串 = 未设 |
174
176
  | `DEFAULT_SCENARIO` | `code` | 请求体未指定场景时的缺省场景;必须是本部署在册的场景名,否则拒启 |
175
177
  | `SANDBOX_PKG_SOURCE` | `global` | 沙箱内装包源:`global`(官方源)/ `cn`(国内镜像)/ `custom` / `none` |
176
178
  | `SENSITIVE_WRITE_PATTERNS` | core 推荐集 | 敏感路径写拒集;逗号分隔值为整体替换,`off` **或留空**关闭。在治理层无条件施加(与客户端权限模式/lane/settings 在场性无关),`run-local` 腿同样生效;编译不出守卫集的值(如 `/`)启动即拒 |