@sema-agent/server 7.103.2 → 7.104.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 (175) hide show
  1. package/MIGRATION.md +346 -2
  2. package/README.md +6 -1
  3. package/README.zh-CN.md +5 -1
  4. package/USAGE.md +271 -16
  5. package/deploy/sema-up/README.md +57 -7
  6. package/deploy/sema-up/cluster-up.sh +18 -4
  7. package/deploy/sema-up/kube-up.sh +16 -0
  8. package/deploy/sema-up/lib/dataroot.sh +142 -0
  9. package/deploy/sema-up/lib/flagshape.sh +69 -0
  10. package/deploy/sema-up/lib/lanes.py +14 -4
  11. package/deploy/sema-up/lib/shell-access.sh +41 -0
  12. package/deploy/sema-up/preflight.sh +14 -1
  13. package/deploy/sema-up/sema-up.sh +134 -37
  14. package/deploy/sema-up/smoke.sh +5 -0
  15. package/dist/auto-mode-face.js +3 -4
  16. package/dist/boot/budget-tracing.js +1 -1
  17. package/dist/boot/config-center.d.ts +6 -2
  18. package/dist/boot/config-center.js +5 -59
  19. package/dist/boot/execution-env.d.ts +11 -0
  20. package/dist/boot/execution-env.js +14 -0
  21. package/dist/boot/execution-lane.d.ts +25 -0
  22. package/dist/boot/execution-lane.js +95 -0
  23. package/dist/boot/memory-consolidation.d.ts +185 -9
  24. package/dist/boot/memory-consolidation.js +192 -32
  25. package/dist/boot/resolve-spec.d.ts +2 -0
  26. package/dist/boot/resolve-spec.js +28 -24
  27. package/dist/boot/runner-deps.d.ts +6 -2
  28. package/dist/boot/runner-deps.js +4 -0
  29. package/dist/boot/session-faces.d.ts +5 -0
  30. package/dist/boot/session-faces.js +18 -2
  31. package/dist/boot/session-incarnation-seat.d.ts +41 -0
  32. package/dist/boot/session-incarnation-seat.js +69 -0
  33. package/dist/boot/stage-01-config.js +2 -2
  34. package/dist/boot/stage-02-registry-pull.js +3 -1
  35. package/dist/boot/stage-04-stores.d.ts +2 -0
  36. package/dist/boot/stage-04-stores.js +3 -1
  37. package/dist/boot/stage-05-execution-env.d.ts +4 -0
  38. package/dist/boot/stage-05-execution-env.js +2 -2
  39. package/dist/boot/stage-06-runners.d.ts +4 -1
  40. package/dist/boot/stage-06-runners.js +7 -14
  41. package/dist/boot/stage-07-capability-layer.d.ts +4 -1
  42. package/dist/boot/stage-08-reapers.d.ts +4 -1
  43. package/dist/boot/stage-09-leader.d.ts +6 -1
  44. package/dist/boot/stage-09-leader.js +2 -1
  45. package/dist/boot/stage-10-http-server.d.ts +6 -1
  46. package/dist/boot/stage-10-http-server.js +4 -1
  47. package/dist/brain.d.ts +1 -1
  48. package/dist/config-catalog.d.ts +16 -1
  49. package/dist/config-catalog.js +9 -9
  50. package/dist/config-center/restart-signal.d.ts +4 -4
  51. package/dist/config-invariants.d.ts +7 -1
  52. package/dist/config-invariants.js +37 -6
  53. package/dist/config-provider.d.ts +42 -10
  54. package/dist/config-provider.js +18 -16
  55. package/dist/config-types.d.ts +37 -16
  56. package/dist/config-types.js +1 -0
  57. package/dist/config.d.ts +13 -28
  58. package/dist/config.js +53 -70
  59. package/dist/env-facts.d.ts +13 -3
  60. package/dist/env-facts.js +15 -3
  61. package/dist/execution-lane-caps.d.ts +15 -0
  62. package/dist/execution-lane-caps.js +8 -0
  63. package/dist/file-history-capture.d.ts +42 -35
  64. package/dist/file-history-capture.js +15 -10
  65. package/dist/http/admission.d.ts +11 -4
  66. package/dist/http/admission.js +5 -1
  67. package/dist/http/dispatch.js +3 -3
  68. package/dist/http/principal-gate.d.ts +22 -0
  69. package/dist/http/principal-gate.js +20 -0
  70. package/dist/http/resume-legs.js +9 -5
  71. package/dist/http/routes/agents-roster.d.ts +7 -0
  72. package/dist/http/routes/agents-roster.js +6 -2
  73. package/dist/http/routes/approvals-assistant.d.ts +48 -19
  74. package/dist/http/routes/approvals-assistant.js +117 -50
  75. package/dist/http/routes/capabilities.js +3 -2
  76. package/dist/http/routes/config-catalog.js +1 -0
  77. package/dist/http/routes/memory-consolidation.d.ts +3 -2
  78. package/dist/http/routes/memory-policy.js +3 -3
  79. package/dist/http/routes/rules.js +2 -2
  80. package/dist/http/routes/runs.js +27 -2
  81. package/dist/http/routes/session-sync.js +8 -14
  82. package/dist/http/routes/sessions-list.js +3 -2
  83. package/dist/http/routes/sessions.js +44 -23
  84. package/dist/http/routes/shared-memory.js +2 -2
  85. package/dist/http/routes/tasks.js +22 -19
  86. package/dist/http/routes/trace-usage.js +4 -4
  87. package/dist/http/send.d.ts +12 -0
  88. package/dist/http/send.js +12 -1
  89. package/dist/http/server.d.ts +21 -4
  90. package/dist/http/server.js +7 -4
  91. package/dist/http/sync-submit-body.d.ts +43 -0
  92. package/dist/http/sync-submit-body.js +4 -0
  93. package/dist/http/wire-types.d.ts +23 -5
  94. package/dist/index.d.ts +1 -0
  95. package/dist/index.js +1 -0
  96. package/dist/leader/merge.d.ts +8 -3
  97. package/dist/memory-incident-sink.js +2 -1
  98. package/dist/model-select.d.ts +9 -0
  99. package/dist/model-select.js +5 -1
  100. package/dist/observability/corrupt-read-seat.d.ts +151 -16
  101. package/dist/observability/corrupt-read-seat.js +158 -15
  102. package/dist/observability/fail-open.d.ts +12 -0
  103. package/dist/observability/fail-open.js +12 -0
  104. package/dist/observability/run-terminal.d.ts +15 -1
  105. package/dist/observability/run-terminal.js +4 -0
  106. package/dist/parked-decide.d.ts +8 -3
  107. package/dist/parked-decide.js +56 -20
  108. package/dist/parked-revive-surface.d.ts +14 -7
  109. package/dist/parked-revive-surface.js +5 -1
  110. package/dist/plugins/approval-ask-store-file.d.ts +4 -1
  111. package/dist/plugins/approval-ask-store-file.js +9 -12
  112. package/dist/plugins/approval-ask-store-sql.d.ts +4 -0
  113. package/dist/plugins/approval-ask-store-sql.js +4 -1
  114. package/dist/plugins/approval-exemption-store.d.ts +21 -1
  115. package/dist/plugins/approval-exemption-store.js +36 -7
  116. package/dist/plugins/approval-nonce-store.d.ts +16 -2
  117. package/dist/plugins/approval-nonce-store.js +17 -7
  118. package/dist/plugins/caching-session-store.d.ts +5 -0
  119. package/dist/plugins/caching-session-store.js +3 -0
  120. package/dist/plugins/file-resume-anchor-store.d.ts +7 -2
  121. package/dist/plugins/file-resume-anchor-store.js +19 -5
  122. package/dist/plugins/file-run-store.d.ts +21 -8
  123. package/dist/plugins/file-run-store.js +60 -38
  124. package/dist/plugins/local-session-store.d.ts +22 -0
  125. package/dist/plugins/local-session-store.js +74 -11
  126. package/dist/plugins/memory-run-store.d.ts +7 -5
  127. package/dist/plugins/memory-run-store.js +11 -4
  128. package/dist/plugins/one-time-migrations.d.ts +38 -1
  129. package/dist/plugins/one-time-migrations.js +143 -15
  130. package/dist/plugins/permission-rule-store-file.d.ts +9 -2
  131. package/dist/plugins/permission-rule-store-file.js +37 -51
  132. package/dist/plugins/pg-session-storage.d.ts +3 -0
  133. package/dist/plugins/pg-session-storage.js +19 -10
  134. package/dist/plugins/run-store-sql.d.ts +12 -4
  135. package/dist/plugins/run-store-sql.js +15 -13
  136. package/dist/plugins/send-file-ledger.d.ts +5 -1
  137. package/dist/plugins/send-file-ledger.js +11 -6
  138. package/dist/plugins/session-event-payload.d.ts +14 -4
  139. package/dist/plugins/session-event-payload.js +5 -2
  140. package/dist/plugins/store-backend.d.ts +7 -3
  141. package/dist/plugins/store-backend.js +10 -6
  142. package/dist/plugins/store-contracts.d.ts +7 -2
  143. package/dist/plugins/tidb-pool.js +8 -2
  144. package/dist/plugins/tidb-session-store.d.ts +6 -1
  145. package/dist/plugins/tidb-session-store.js +17 -10
  146. package/dist/project-identity.d.ts +4 -6
  147. package/dist/request-key-closure.js +1 -1
  148. package/dist/request-value-closure.d.ts +72 -3
  149. package/dist/request-value-closure.js +12 -4
  150. package/dist/roster-bound-http-status.d.ts +6 -0
  151. package/dist/roster-bound-http-status.js +6 -0
  152. package/dist/run-local.js +22 -27
  153. package/dist/runs.d.ts +22 -2
  154. package/dist/runs.js +29 -7
  155. package/dist/sealed-key.d.ts +31 -4
  156. package/dist/sealed-key.js +9 -0
  157. package/dist/security.d.ts +5 -1
  158. package/dist/session-children-retire.d.ts +92 -0
  159. package/dist/session-children-retire.js +85 -0
  160. package/dist/session-error-wire.d.ts +58 -0
  161. package/dist/session-error-wire.js +38 -0
  162. package/dist/session-incarnation.d.ts +61 -0
  163. package/dist/session-incarnation.js +43 -0
  164. package/dist/session-records-skipped-notice.d.ts +20 -0
  165. package/dist/session-records-skipped-notice.js +27 -0
  166. package/dist/tool-approval.d.ts +65 -0
  167. package/dist/tool-approval.js +36 -9
  168. package/dist/trace/core-keyset-guard.d.ts +8 -7
  169. package/dist/trace/engine-notice-wire.d.ts +3 -1
  170. package/dist/trace/engine-notice-wire.js +2 -0
  171. package/dist/trace/project.d.ts +1 -1
  172. package/dist/trace/project.js +9 -0
  173. package/dist/trace/wire-projection-faces.d.ts +10 -3
  174. package/dist/trace/wire-projection-faces.js +8 -2
  175. package/package.json +2 -2
package/USAGE.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # 调用方手册(OA / 智能体 / 你自己)
2
2
 
3
3
  > 一句话心智模型:**你只对一个地址发普通 HTTP 请求——LB `http://<host>:8090`;请求体里只写"要做什么"。**
4
- > model / tools / prompt / 安全策略由服务端 `resolveSpec` 注入,**永远不要也不能从请求体传**(凭据不进模型/请求体)。
4
+ > 工具面与安全策略由服务端 `resolveSpec` 注入,请求体**替换不了**它们(`tools` 不是受理键,传了 400;按请求只能**减**:`excludeTools` / `settings.permissions`)。
5
+ > `model` 与 `systemPrompt` / `appendSystemPrompt` 是受理键,但 `model` 只能点服务端已配置目录里的名字 / 档位词 / 模型 id(未知 ⇒ 400)。凭据永远不进模型 / 请求体。
5
6
  > 你不"调度 worker",你只发任务;LB + TiDB 自己协调。每个字段都来自当前真实代码。
6
7
 
7
8
  ---
@@ -23,10 +24,11 @@ npm run dev # 或 bun run src/main.ts → http://<
23
24
  **路线 B — docker(最小,单 worker,内存模式,不需要数据库;只支持同步 `/v1/tasks`)**
24
25
  ```bash
25
26
  cd sema-server
27
+ IMAGE_REGISTRY=local \
26
28
  MODEL_GATEWAY_BASEURL=https://<your-gateway>/v1 MODEL_ID=<your-model-name> \
27
29
  SESSION_BACKEND=memory REMOTE_EXEC=host \
28
30
  docker compose up --build
29
- # 起来后 OA 只认 http://<host>:8090(LB)
31
+ # IMAGE_REGISTRY 是 compose 的必填插值;--build 就地构建时它只给镜像起名(local/sema-server:latest),不去拉取
30
32
  ```
31
33
 
32
34
  **生产(N worker 集群,需 MySQL 协议库——MySQL(≥8.0)/ MariaDB(≥10.5)/ TiDB 均可;支持异步 `/v1/runs` + 断线重连)**
@@ -46,6 +48,21 @@ docker compose up --build --scale service=4 # 4 个无状态 worker,nginx
46
48
 
47
49
  > 🔴 7.96.0 起根 `docker-compose.yml` 的 `REMOTE_EXEC`(执行车道)是**必填**:不设 ⇒ `docker compose config` 当场失败(此前静默起成一个没有手的 worker)。单人自用 `REMOTE_EXEC=host`(手落在 worker 容器里);多人 / 生产选隔离车道 `e2b`(配 `E2B_API_KEY`)或 `k8s`(配 `K8S_SANDBOX_IMAGE`)。
48
50
 
51
+ > 🔴 **7.104.0 起多租户姿态不许跑不分租户的执行车道**:`REQUIRE_PRINCIPAL=true` ∧ 最终生效的车道在执行车道能力表上不分租户 ⇒ **拒启**。按表不按词,今天覆盖 `host` / `ssh` / `adb`(所有任务落同一个共享目标:worker 宿主 / 一台 SSH 对端 / 一台 ADB 设备)。判的是合成后的车道(env 的 `REMOTE_EXEC`、`CONFIG_PROVIDER=local` 而 `REMOTE_EXEC` 未设的隐式 host、local 形 `config.d/remote-exec.json` 的覆写,在中心拉取与任何配置应用之前合成)。拒因(逐字,以 host 为例):`REQUIRE_PRINCIPAL=true (multi-tenant posture) with the "host" execution lane runs every tenant's Bash on one shared target — this lane does not separate tenants; set REMOTE_EXEC to e2b / k8s / local-docker / device, or unset REQUIRE_PRINCIPAL for a single-user box`。车道 × 租户形:
52
+ >
53
+ > | 车道 | 单用户 | 多租户(`REQUIRE_PRINCIPAL=true`) |
54
+ > |---|---|---|
55
+ > | `host`(显式 / `CONFIG_PROVIDER=local` 隐式 / 文件覆写) | ✅ | ❌ 拒启(7.104.0 起) |
56
+ > | `ssh`(一台常驻对端) | ✅ | ❌ 拒启(7.104.0 起) |
57
+ > | `adb`(一台设备) | ✅ | ❌ 拒启(7.104.0 起) |
58
+ > | `e2b` / `k8s` / `local-docker`(每任务一只沙箱) | ✅ | ✅ |
59
+ > | `device`(每人自己的设备) | ✅ | ✅ |
60
+ > | 未点名(云形进程内 stub,无手) | ✅(`REQUIRE_EXECUTION_LANE=true` 可改成拒启) | ✅(同左) |
61
+
62
+ > 🔴 **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` 读同一个。
63
+
64
+ > **会话 scratchpad 与 `SANDBOX_ENV_FACTS`(7.104.0 起)**:`SANDBOX_ENV_FACTS=false` 只关沙箱事实(profile / capabilities / pkgSource / egress / resume);会话 scratchpad 的宣告不归它管 —— 宣告是 Write / Edit 能写进 scratchpad 的唯一来源(引擎据它开根围栏的洞,并用同一次裁决在提示词里写出 `# Scratchpad Directory` 一节)。缺省部署此前每轮一句 `scratchpad advertised writable directory skipped` 的噪音随之消失。
65
+
49
66
  > 拓扑:LB(入口)→ N 个一样的 worker → 共享 SQL 库(数据中心;MySQL 协议或 PG)+ 模型网关。无中央调度器,跨 worker 靠 DB 协调。
50
67
 
51
68
  **探针:存活看 `/health`,就绪看 `/readyz`(7.102.0+)**。`/health` 恒 200(liveness:进程在答);只认状态码的**摘流**
@@ -320,6 +337,9 @@ MODEL_ID=<your-model-name> MODEL_CHEAP_ID=<your-cheap-model-name>
320
337
  - 调用方可在 `objective` 里 `@<模型名>` 选模型,**只认已配置的名单**(`MODEL_ID` / `MODEL_CHEAP_ID`,或目录条目)——注入不了 `baseUrl`/`apiKey`;无 `@` → 默认模型;`@` 点中一条已发布但本部署不可用的目录条目 ⇒ 400 `request.model_unavailable`(S-704)。
321
338
  - **`GET /v1/models`**(带 Bearer)→ `{models:[{name,id,provider,reasoning,vision}],default}`,只含名字/能力、**不含密钥** —— 消费方(如 OA)拿它做 `@` 自动补全下拉的数据源。
322
339
  - 不设 `MODEL_CHEAP_ID` → 所有角色 = 主模型(行为不变)。
340
+ - `MODEL_CHEAP_ID` 写成与 `MODEL_ID` **同一个 id**(7.104.0 起)→ 那就是主模型本身:不铸 cheap 槽,所有角色 = 主模型,起服一行
341
+ `config_env_entry_cheap_same_as_main`(同时设的其余 `MODEL_CHEAP_*` 键一并不生效)。要给同一只上游 id 第二种形(另一个价 / 思考 /
342
+ 窗),在 `config.d/models.json` 写两条不同名的条目。
323
343
  - **1M 双窗**(`autoCompactTokens`):`967000` 只发给 **claude-sonnet-5 系 id ∧ 窗 ≥1M**(CC 的 per-model 值,不是窗宽几何)。其他 ≥1M 模型要双窗触发点请显式配 —— 主槽 `MODEL_AUTO_COMPACT_TOKENS=<tokens>`、cheap 槽 `MODEL_CHEAP_AUTO_COMPACT_TOKENS=<tokens>`(**两槽各配各的**:主槽的值按主模型 id/窗声明,不会外溢到 cheap)。不配 → core 按整窗的 `W-33000` 平几何触发,boot 期发 `auto_compact_window_not_derived_1m` warn 点名槽位(`slot=main|cheap`)。⚠️ cheap 槽不设 `MODEL_CHEAP_CONTEXT_WINDOW` 时**继承主模型的窗**,主模型是 1M 则 cheap 也按 1M 计。取值范围:**必须低于该槽的物理窗**(CC 的形是 `窗-33000`)—— 高于物理窗时 **core 在取数处把触发窗夹回物理窗**(`resolveTriggerWindow`,自 core 5.60.0 起),即**这根旋钮变成 no-op**、几何回落成没配它的样子,并由引擎逐任务铸一条 `config.autocompact_window_clamped` 通告(受众 operator)。server 侧**不夹也不拒**(夹紧归 core 的取数处,一处一次),但会在 **boot** 就发 `auto_compact_tokens_above_context_window` warn 点名槽位、越窗值与 **`clampedTo`(core 会夹到的那个数)**—— 不必先派一条任务才知道旋钮白配了。⚠️ 7.57.0 之前这一段写的是「autocompact 事实上关掉、改由破坏性 guard 砍历史」,那是 core 5.60.0 之前的行为,已订正。
324
344
 
325
345
  **可选 — 开发者模式 building blocks(core 1.43,默认中立)**
@@ -391,12 +411,17 @@ MODEL_CODE_ROLES=default,subagent # 不设=全中立;仅这些角色在「
391
411
  MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheapest→strongest;不设=cascade 不可用
392
412
  ```
393
413
  - 结果带 `cascadeOutcome:"passed"|"exhausted"` + `escalated`/`finalRung`/`attempts[]`。`/metrics` 加 `cascade_total{outcome}`。
394
- - **每档用自己的上游 key**:从配置控制面模型的 `apiKeyEnv`(env 变量**名**,非密钥值)解析——同一网关下不同模型/账号各用各的 key,没配 `apiKeyEnv` 的模型回落到网关 key(`MODEL_API_KEY`)。`baseUrl` 仍由 brain 层统一持有(非目标)。启动日志 `perModelKeys=N` 报有几个模型带了自己的 key。
414
+ - **每档用自己的上游 key**:从配置控制面模型的 `apiKeyEnv`(env 变量**名**,非密钥值)或 `sealedApiKey`(密文,见 §9.10)解析——同一网关下不同模型/账号各用各的 key,两者都没配的模型回落到网关 key(`MODEL_API_KEY`;sealed 开不了的模型**不**回落,见 §9.10)。`baseUrl` 仍由 brain 层统一持有(非目标)。启动日志 `perModelKeys=N` 报有几个模型带了自己的 key。
395
415
 
396
416
  **嵌入形契约(1.309+;桌面宿主把引擎作为依赖内嵌启动的正门)**
397
417
  - 入口:`import "@sema-agent/server/main"`(exports 正门;此前宿主只能用 node_modules 路径字符串定位
398
418
  `dist/main.js`,那是未承诺的内部路径)。语义承诺三条:**import 即 boot**(副作用模块形,无需调用);
399
419
  **不读 `process.argv`**(宿主可用 argv 传自己的标记,`ps` args 判据不受干扰);`./package.json` 可读。
420
+ - **库形嵌入**(包根 `.` 导出:`loadConfig` + `createHttpServer` + 自建 `StoreBackend` / `ServiceDeps`):`loadConfig()` 之后调
421
+ `composeExecutionLane(config, ctx)`(HTTP 服务与 `run-local` 都这么做;7.104.0+)。执行车道的四条拒启(`leader-host-lane` /
422
+ `multi-tenant-shared-lane` / `device-durable-store` / `device-durable-approval`)自 7.104.0 起判在这一步,**不在** `loadConfig()` 里 ——
423
+ 只调 `loadConfig()` 的装配对这几形照起。`ctx.configSource === "local"` 时它先读 `config.d/remote-exec.json` 覆写车道(`drivesFromFile`
424
+ = 你的装配驱动得了的文件车道),再判;不走文件覆写的装配直接调同一份判据 `assertExecutionLaneInvariants(config)`。
400
425
  - `/health` 身份字段承诺:`pid`(= 引擎进程)、`startedAt`(**7.67.0+**,= 引擎进程起点的 epoch ms,
401
426
  一次铸)与 `dataRoot`(= 生效数据根,解析恒回退 `~/.ai-agent`,与 DB_BACKEND 无关)**恒在**——宿主用
402
427
  它们验证「这个端口上的 /health 是不是我起的那个引擎」。`startedAt` 变了 = 这个端口后面换了一条命
@@ -493,6 +518,18 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
493
518
  射程 = boot 期的**全部盘面**:`runs/`+`runs/active/` 的建目录、两次目录列举,加上逐个
494
519
  `run.json` / `events.jsonl` / claim 文件的读取。目录里本来就没有的东西(`.DS_Store` 一类杂物、
495
520
  尚未落盘的目录)照旧跳过 —— 拒的是「读不出来」,不是「没有」。
521
+ ⚠️ 射程**不含**「文件读得出、里面有一条坏记录」:那一形在两档下都是跳过 + 披露 + 计数(下一条)。
522
+ - **local 车道 JSONL 店的坏记录(7.104.0 起,S-582)**:`DB_BACKEND=local` 的八本 JSONL 账(run 店三面、续跑锚、
523
+ 发文件账本、流内审批 ask 账本、直连门 nonce 账、「本会话不再询问」豁免账、规则审批记录 / 导入票两本)重放时,
524
+ **中间**一条记录读不出(一行 `42`、缺本店必填键的对象、半截 JSON)不再静默:每条打一行 warn(事件名
525
+ `<店>_corrupt_read`,带 `path` / `line` / `form` 与本次重放的 `kept` / `dropped` / `total`),方向按店分两类 ——
526
+ ① **拒用**(安全轴):nonce 账坏 ⇒ 这台机器上直连门的 plan_review 全部 `500 internal.error`(`/decide` 不受影响),
527
+ 直到有人处置那份文件(`<数据根>/approval-nonces/consumed.jsonl`;移开它会让其中每条消费在其证明到期前可被重放,
528
+ 所以等过 `PRINCIPAL_JWT_MAX_TTL_SEC` 加容差再移、再重启);某会话的豁免账坏 ⇒ **那个会话**的每个工具重新问人、
529
+ 它的豁免列表 / 撤销答 500、`remember:"session"` 不生效(`rememberApplied:false`),删掉该会话即恢复;规则两本日志坏 ⇒
530
+ 规则车道整面拒(与此前同,拒绝句自此带码)。不拒启。② **跳过 + 计数**(可用性轴):run 店 / 续跑锚 / 发文件账本 /
531
+ ask 账本,计数 `fail_open_total{tag="server.<店>.corrupt-record-skipped"}` + stderr 契约行。逐店后果见
532
+ `docs/FAIL-OPEN-CENSUS.md` 第 75、93–98 行。崩在半行的**尾**行不算坏记录(那是非优雅退出的正常落盘形)。
496
533
 
497
534
  **监听绑址(1.306+)—— 桌面/单机形请注意**
498
535
  - `BIND_HOST` = 监听地址;显式设置**恒生效**(要在无鉴权下对外暴露,显式写 `BIND_HOST=0.0.0.0` 即可)。
@@ -512,7 +549,7 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
512
549
  要本机有手,显式设 `REMOTE_EXEC=host`(sema-up 产物已显式写车道)。
513
550
  壳/客户端判「工具跑不跑在本机」**读这一位**,不要再从自己这一侧的 `REMOTE_EXEC` 猜 —— 连远端引擎时
514
551
  那个 env 说的是**壳自己这台**的事。⚠️ 它是**文件系统同一性**位,**不是**权限位:`cwd` 兑不兑现仍由
515
- `projectContext` / `callerCwd` 回答(那两位的判据多一条单用户约束,多租户 host 部署上两者会分叉)。
552
+ `projectContext` / `callerCwd` 回答(那两位的判据多一条单用户约束;多租户 ∧ host 部署自 7.104.0 起拒启,这一形的分叉不再可达)。
516
553
  端点/凭据/镜像/`serial` 一个字都不上这条 wire,那些仍只在 operator 面 `GET /v1/config/catalog`。
517
554
 
518
555
  **workspace 浏览面 —— 已随整树快照纪元退役**
@@ -524,6 +561,11 @@ MODEL_CASCADE_LADDER=deepseek-flash,deepseek-pro # 目录里的模型名,cheap
524
561
  引擎把**本会话跟踪集**收敛到该消息时刻的边界;`acceptPartialRestore: true` 容忍部分恢复
525
562
  (缺省=响亮失败带逐文件账本)。旧键 `rewindFiles` 的 capture 义=容忍 no-op(首触跟踪恒开),
526
563
  restore 义(与 resumeAt 同发)=typed 拒并指路 `restoreFiles`(迁移显式化是 core 裁定)。
564
+ - **按 run 关掉捕获**(S-724,7.104.0):提交体带 `fileHistory: "disabled"`(唯一词;坏值 400
565
+ `request.field_invalid`)⇒ 这条 run 的编辑照常落盘,但不做首次触碰备份、不铸回合边界,委派出去的子代继承;
566
+ `wiring_manifest.fileHistory` 报 `"disabled"`。同一提交再带还原键(`restoreFiles` / `rewindFilesTo` / `rewindFiles`)⇒ 受理时
567
+ 400 `request.field_conflict`(会话与文件都不动)。
568
+ 能力词 `fileHistoryCapture:"on-optional"` 表示本部署认这个词;要「本会话不捕获」就在每个提交上都带它。
527
569
 
528
570
  **可选 — SendUserFile 文件直链(把沙箱/主机里的文件发给用户,S3/MinIO 双轨)**
529
571
  ```bash
@@ -541,7 +583,7 @@ SEND_USER_FILE_SANDBOX_PUT_ENDPOINT=… # 可选:沙箱直传 PUT 的端
541
583
  - **词表优先级**:内网端点 `MINIO_ENDPOINT` 优先,`S3_ENDPOINT` 是外接 S3 兼容位(内网/公网同址场景可只给它,同时充当公网面);公网端点 `S3_PUBLIC_ENDPOINT` 优先,缺席回落 `S3_ENDPOINT`。空串一律按未设处理。
542
584
  - **🔴 云形(`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 帽,tidb 默认 6MiB,超限 PUT 413 `blob_too_large_for_sql`;帽可用 `SNAPSHOT_BLOB_SQL_MAX_BYTES` 按部署真实限值覆写,两方言生效;pg 默认无帽)。
543
585
  - **per-file rewind 历史的边界上限**(与 workspace 浏览面同一个整树快照纪元退役批留下的保留条款):`FILE_HISTORY_RETENTION_KEEP` —— 每个 scope(会话)存活的**最新** boundary 数,**每次 boundary 提交后自剪**(最旧者先亡,刚提交者恒存活;失引用的版本行级联删,blob 字节走异步 grace 窗 GC;v1 基线永存)。未设=core 缺省 **100**(由 core 的 `resolveFileHistoryRetention` 兑现,server 不复制那个数);`unbounded` 是**唯一**的「不剪」拼法(整店自剪关闭,GC 归部署自排 `reap`);`0`/负数/小数/非数 ⇒ **boot 拒启**(码 `config.retention_policy_invalid`,与 core 内置后端同码同判——**省略这个键不等于不剪**,它选的是缺省 100)。三条车道(TiDB / PG / local 文件店)同旋钮同判。⚠️ 升级即生效:存量超 100 boundary 的 scope 在下一次提交时被剪到 100;要保全量先写 `unbounded`。被剪 entry 的键**已花掉**(再次 snapshot 同 entry 典型拒,不复铸)。排序=DB 分配的 per-scope 发布序号 `file_history_boundary.publish_seq`(不是墙钟):**自 7.50.x 有存量 `file_history_boundary` 的部署升级本版会在 boot 期被 `assertFileHistorySchema` 拒启并指明 `DROP TABLE file_history_boundary`**(本仓无 ALTER;丢的是 per-file rewind 便利态,会话与其他持久面不动)。
544
- - **文件历史捕获总开关**(S-589):`FILE_HISTORY_CAPTURE`(缺省 `true`;布尔词表 true/false/1/0/yes/no/on/off,未知词 ⇒ boot 拒启;TiDB / PG / local 三条车道同义)。`false` ⇒ 不把历史店交给引擎:改动文件的首次快照**一个都不捕获**;交换是同时没有 rewind(`resumeAt`+`restoreFiles` / `rewindFilesTo` 的 run 以 `rewind.store_unconfigured` 失败,文件不动)与 2c session-sync(整族 `501 capability.snapshot_store_required`)。`GET /v1/capabilities` 的 `fileHistoryCapture` 报 `"off"`,`rewindFiles` / `restoreFiles` / `rewindFilesTo` / `sessionSync` 同源翻 `false`(缺省部署报 `"on-always"` = 捕获中且无法按会话关闭)。⚠️ **关掉 ≠ 删掉**:翻旋钮不批量删除存量历史,存量也不再被读用;它照旧随 `DELETE /v1/sessions/:id` 与留存车道清理(擦除面不受本旋钮管;留存车道只在 SQL 后端,local 车道上删会话走 `reap` —— 边界删光、v1 基线按 core 契约留存,既有残余)—— 要清存量走会话删除或留存旋钮。关着时 `FILE_HISTORY_RETENTION_KEEP` 没有效果(不再有边界提交,自剪不会触发),值仍照常校验。翻旋钮后每个进程首个带手的 run 会有一条引擎的 `runner_config_advisory` warn(「file history is not live for this run」),是预期。
586
+ - **文件历史捕获总开关**(S-589 立,S-724 / 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)。
545
587
  - **🔴 公桶策略必须 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)。
546
588
  - **两条轨**:ttl=0(默认)→ 匿名 GET 公桶下 `uuidv7/<name>` 不可猜 key,链接永久、可回收(删对象);0<ttl≤7 天 → 私桶 + SigV4 限时签名链接(7 天是 SigV4 物理上限,更久用 ttl=0)。
547
589
  - **执行 lane 与租户门**:e2b/k8s 沙箱 lane 任意租户可用(沙箱文件系统=租户边界,沙箱内 `curl -T` 直传、字节不中转、凭据不进沙箱);host/ssh lane 仅单用户部署(`REQUIRE_PRINCIPAL` 未开)时挂载。
@@ -598,7 +640,7 @@ FLEET_ADVERTISE_ADDRESS=http://<本机可达IP>:8090 # 可选:设了才启 flee
598
640
  | plugins(插件引用) | 重启生效 | 插件只在启动期物化;改了之后 `/health` 报 `restartRequired`,`restart.reasons` 含 `skills`(7.100.0 前这一改动**不报**,要等一次无关的重启) |
599
641
  | skills / mcp / a2a / scenarios(技能 / MCP / A2A / 场景) | 重启生效 | 只在启动期物化;中心改了 ⇒ `/health` 报 `restartRequired`,`restart.reasons` 各含其名。**换账号**(凭证身份变化)同理:这些面仍是上一身份的直到重启(候 S-667) |
600
642
  | 审批名单(`governance.approvalRequire`) | **热**(下一 refresh 拍) | 7.100.0 起:下一个任务调名单里的工具即停车等审批;撤键回 env 底(`APPROVAL_REQUIRE`)。**没有 checkpoint 店**的部署(`DURABLE_APPROVAL` 关 / 后端无 checkpoint 面)发非空名单 ⇒ 审批组**整组拒**(旧名单继续服务、`configAppliedVersion` 停旧代、拒因见日志 `sema_registry_approval_require_invalid` 与诊断端点 `configApply.groups.approvalGate`)—— 与启动期「名单已配却无 durable 门 ⇒ 拒启」同一条判据。此前 `/health` 报 `restartRequired`(`runtime-gates`,该理由退役) |
601
- | 记忆整理驱动席(仅 `MEMORY_CONSOLIDATION_DRIVER=on`) | 重启生效 | 整理跑用的模型在启动期解析一次;中心改了 roles / 档位 / 该条目录后 `/health` 报 `restartRequired`,`restart.reasons` 含 `memory-consolidation`(7.100.0 新理由;此前不报) |
643
+ | 记忆整理阀门判决 + 驱动席(`MEMORY_CONSOLIDATION_DRIVER=on`,或未设而出厂缺省按目录派生 —— 7.104.0 起) | 重启生效 | 阀门开关与整理跑用的模型都在启动期定一次;中心改了 roles / 档位 / 目录条目后,**判决会翻**(派生开 ↔ 关,含只把主模型挪到整理座那一条上)或开着而座漂了 ⇒ `/health` 报 `restartRequired`,`restart.reasons` 含 `memory-consolidation`(7.100.0 新理由;7.104.0 起派生关的那台同样报) |
602
644
 
603
645
  生效与否的观测口=`/health` 世代账键(`configTargetVersion`/`configAppliedVersion`+两 ordinal+`configApplyStaleMs`,见 API 表 `/health` 行)——target≠applied 持续=「改了没生效」的机读信号。
604
646
  - **`run-local`(一次性本机 CLI)上三张审批名单同样执法**(7.101.0 起,TTY 问 / 无 TTY 拒):`APPROVAL_DENY` 当场拒;`APPROVAL_REQUIRE` / `APPROVAL_NEVER_AUTO` 命中 ⇒ stdin 是 TTY 就 `y/N` 问人,否则当场拒并在 stderr 点名三张名单键;`APPROVAL_AUTO_BUDGET` 照常生效(never-auto 不吃预算);TTY 问句带上这次调用的参数,拒答而停下的 run 退 1。这条腿没有 checkpoint 店也**不**拒启 —— 它有一只当场作答的席,名单的 ask 不需要 park(上表「无 checkpoint 店 ⇒ 整组拒」只说服务腿)。此前三张名单在这条腿上无声不生效。
@@ -1110,8 +1152,8 @@ invalid_using_default` 警告 + 退回缺省值」这条臂随词表放宽一并
1110
1152
  | `opt-out` | **开** | 显式 `=false` 才关(缺省即最佳实践,给逃生舱) |
1111
1153
  | `posture` | 取决于 `REQUIRE_PRINCIPAL` | 单用户 turnkey(未设 `REQUIRE_PRINCIPAL=true`)且基建就绪 ⇒ 开;多租户 ⇒ 关。显式字面量永远压过姿势推导 |
1112
1154
 
1113
- `source` 说明这个值从哪来:`env`(显式设了)/ `default`(缺省)/ `posture`(姿势推导)/ `legacy-env`(命中了
1114
- 下面的兼容旧名)。表覆盖的是**本次加载中活着的旋钮**:挂在未激活通道/形状上的旋钮(非 k8s 通道下的
1155
+ `source` 说明这个值从哪来:`env`(显式设了)/ `default`(缺省)/ `posture`(姿势推导)。(没有「来自旧名」这一档:
1156
+ 下面那些旧名设了就拒启,从来不是取值来源。)表覆盖的是**本次加载中活着的旋钮**:挂在未激活通道/形状上的旋钮(非 k8s 通道下的
1115
1157
  `K8S_INSECURE_TLS`、未设 `MODEL_DEGRADE_TO` 时的 `MODEL_DEGRADE_REACTIVE` 等)不出行——没有行=「在这份
1116
1158
  配置里不生效」,不是「关」。
1117
1159
 
@@ -1128,7 +1170,10 @@ invalid_using_default` 警告 + 退回缺省值」这条臂随词表放宽一并
1128
1170
 
1129
1171
  四个旧名都是"负名"(`X_DISABLED`),读的时候要双重否定;新名一律正向 + 缺省显式。
1130
1172
  🪦 **墓碑语义**:旧名**在场即拒启**——值是 `true` 还是 `false` 都一样(设 `false` 的人同样以为它还生效),
1131
- 错误文案直接给出该改成什么。所以不存在"两个名字同时设"的状态,也不存在"旧名还在悄悄生效"的状态。
1173
+ **正名设没设也一样**(7.104.0 起;此前正名设了时旧名被静默忽略 —— 迁移到一半、两个名字都留着的部署以为旧名还在生效)。
1174
+ 拒句与其余退役 env 名同一句、同一个码 `[config.env_key_retired]`,点名旧名、正名与换算(`X_DISABLED=true` 即 `X_ENABLED=false`)。
1175
+ 所以不存在"两个名字同时设"而能起服的状态,也不存在"旧名还在悄悄生效"的状态。墓碑**不设到期**:退役名的拒启是永久的,
1176
+ 只有某个名字要被赋予新含义时才删它的墓碑(届时 MIGRATION 写明)。
1132
1177
 
1133
1178
  **`LSP_ENABLED` 一分为二**:它原来同时驱动两条腿,而且两腿缺省相反——沙箱腿缺省**关**(要烤好的
1134
1179
  `ai-agent-code-lsp` 模板;⚠️ 该模板**只烤 TS/JS**——Python/Go 烤在 `dev-base`,dart/kotlin/java/swift
@@ -1153,6 +1198,14 @@ invalid_using_default` 警告 + 退回缺省值」这条臂随词表放宽一并
1153
1198
  放回的键不算剥除(不在 `keys` 里、不进计数 `secret_env_scrubbed_total`);同一个键每进程只报一次;没有剥除 ⇒ 不发。
1154
1199
  另两条剥除现场(模型可驱动的 host shell、project-memory 的 git 子进程)没有部署级放行旋钮,它们的同名行照旧是 `info`、不带 `allowed` / `hint`。
1155
1200
 
1201
+ **🔴 模型可驱动的 host shell 没有点名放回旋钮(刻意,7.104.0 写明)**:凭据形变量不交给模型能驱动的 shell —— 这条腿恒走缺省剥除
1202
+ (本服务不给它传点名名单),部署配置里没有任何键能改它的继承模式;`inheritEnv:[names]` 是**嵌入方**用本包构造执行环境时的选项
1203
+ (下一段),不是 sema-server 部署方的旋钮(引擎 CHANGELOG 里把它说成部署方出路的那一句,对 sema-server 部署不成立)。被剥变量的
1204
+ 出路**按用途分**:git 凭据走 git 配置文件 / credential helper(隔离全局配置的两条 git 腿 —— project-memory 读仓、center-plugins 克隆 ——
1205
+ 另见 `docs/DEPLOY-PREREQS.md` 7.103.2 那一节的「按腿分」);带口令的出网代理走不带口令的形(`~/.gitconfig` 里只带用户名的 `http.proxy` + credential helper,见
1206
+ `docs/DEPLOY-PREREQS.md`「带口令的出网代理」);语言服务器 / bake-runner 走各自的名单 `LSP_ENV_ALLOW` / `BAKE_ENV_ALLOW`;沙箱车道走
1207
+ `E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV`。要不要给 host shell 加部署级名单属于能力面变更(安全轴),不在文档里顺手给。
1208
+
1156
1209
  **形状规则的精确名豁免(7.101.0 起,core 7.31.0 #1081)**:十个预算名(`MAX_NEW_TOKENS` `MAX_COMPLETION_TOKENS` `AIDER_MAP_TOKENS`
1157
1210
  `AIDER_MAX_CHAT_HISTORY_TOKENS` `MAX_BATCH_PREFILL_TOKENS` `MAX_WAITING_TOKENS` `MAX_TOP_N_TOKENS` `RATE_LIMIT_TOKENS` `PROMPT_TOKENS`
1158
1211
  `CONTEXT_TOKENS`)与两个开关(`DB_FOREIGN_KEYS` `JSON_SORT_KEYS`)不再被剥,三条子进程腿同时生效(7.100.0 上这五个 7.30.1 误剥的名
@@ -1168,7 +1221,7 @@ oracle 门子进程,core 7.33.8 #1181):oracle 门若先于工具 shell 剥,通
1168
1221
  bake-runner 要用 ⇒ `LSP_ENV_ALLOW` / `BAKE_ENV_ALLOW` 点名放回。
1169
1222
 
1170
1223
  **「点名放行」只有一种意思**:本服务里凡是「一张名单点到哪些环境变量键」的地方(这里的 `LSP_ENV_ALLOW`、沙箱车道的
1171
- `E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV`(见下表),与嵌入方构造 host shell 时的 `inheritEnv:[names]`)用的是同一只谓词 ——
1224
+ `E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV`(见下表),与**嵌入方**构造 host shell 时的 `inheritEnv:[names]` —— 后者不是部署旋钮,见上一段)用的是同一只谓词 ——
1172
1225
  点到的键从服务进程环境原样交出去(空串值照交),密钥形也照交;源里没有的名什么也不产生。三只名单型旋钮(`LSP_ENV_ALLOW` /
1173
1226
  `E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV`)另走同一只名单解析:形错拒启点名第几段、不回显原值,点名任一只名单型旋钮拒启,
1174
1227
  本进程环境里没有的名按序号一行 `config_env_entry_rejected`,旋钮原文按凭据类对待(配置目录只回在场位、不进任何子进程)。
@@ -1215,6 +1268,13 @@ bake-runner 要用 ⇒ `LSP_ENV_ALLOW` / `BAKE_ENV_ALLOW` 点名放回。
1215
1268
  | `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) |
1216
1269
 
1217
1270
  > ⚠️ **Bearer 是"每个请求",不只是 POST。** 配了 `SERVICE_AUTH_TOKEN` 后,**`GET /v1/runs/:id`、`GET /v1/runs/:id/events`(SSE)、`/metrics`** 等除 `/health` / `/readyz` 两只探针外的路由都要带 `Authorization: Bearer`——漏带一律 `401`。下面示例为简洁**省略了 Bearer**,真实调用请逐个补上。
1271
+ >
1272
+ > **`401 auth.unauthorized` 的 `error` 是一句出路(7.104.0 起;此前只有一个词 `unauthorized`)**,按这道门收什么分三句,码都是 `auth.unauthorized`、都**不含任何值**,
1273
+ > 同一道门上「没带」与「带错」逐字同一句(区分就是探测面):① 全局凭据门 —— 说「带这台部署的 service token 作 Bearer」,并指 sema-up 部署的
1274
+ > 令牌在 `~/.sema-up/<project>/.env` 的 `SERVICE_AUTH_TOKEN`;② 运维口(`/metrics*`、`GET /v1/tasks/source-summary`)—— 说要运维凭据
1275
+ > (service token;指标口另认只读的 `METRICS_TOKEN`);③ 过了凭据门、但这一口要调用方身份(属主寻址读写、规则、记忆)—— 说要一个验过的
1276
+ > 调用方身份,**不**叫你再去带 service token。逐字句子见 `docs/ASSISTANT-WIRE-CONTRACT.md` 附录 A.1 码表。壳(sema CLI)接 sema-up 引擎的
1277
+ > 令牌交法见 `deploy/sema-up/README.md`「壳接入」。
1218
1278
 
1219
1279
  **请求体字段(常用;受理**闭集**以 `docs/ASSISTANT-WIRE-CONTRACT.md` §1 为准 —— 本表不再重抄一份,S-710):**
1220
1280
  | 字段 | 必填 | 说明 |
@@ -1226,7 +1286,8 @@ bake-runner 要用 ⇒ `LSP_ENV_ALLOW` / `BAKE_ENV_ALLOW` 点名放回。
1226
1286
  | `scenario` | — | 省略时取本部署的 `DEFAULT_SCENARIO`(工厂缺省场景名 = **`code`**——CC 编码 persona 蒸馏版,default 全量工具面,终验旋钮独立;`default` 场景**已非出厂缺省**,要用它须显式传 `scenario:"default"`,或部署方显式设 `DEFAULT_SCENARIO=default`)。可选内建名:`default`(通用/助理场景,域中性 persona,CC agent loop 全量工具面)/ `code-review`(见 §5)/ `scan`(同 §5 的 repo 只读工具但**中性无框架提示词**——objective+中心下发 skill 全权主导输出,OA 扫描类用;与 `code-review`/`discuss` 同属 clone-free 无执行环境场景,见 §5 末)/ **配置控制面可声明任意新场景**(`{name, toolset: none\|repo-readonly, prompt?}`,组合即配置、能力写死在部署;restart-to-apply;center 可覆盖内建名,boot 日志 `config_center_scenarios.shadowsBuiltin` 可审计) |
1227
1287
  | `repo` / `council` / `debate` | — | `repo` 为 `code-review`/`scan` 必填;`council`/`debate` 仅 `code-review`,见 §5 |
1228
1288
  | `systemPrompt` / `appendSystemPrompt` | — | 调用方自带的系统提示 / 追加段(各 ≤16384 字符,超限 400 `request.field_invalid`;`appendSystemPrompt` 必须非空串)—— 任务自带时优先于角色 / 全局缺省(见 §模型角色) |
1229
- | ~~model / tools~~ | 🚫 | **不接受**——服务端注入(模型走目录 / 角色,工具走场景装配;按请求只能**减**:`excludeTools` / `settings.permissions`) |
1289
+ | `model` | — | 本次用哪个模型:只能点服务端**已配置目录**里的名字 / 档位词 / 模型 id(目录见 `GET /v1/models`;请求体带不进新模型)。未知 ⇒ 400 `request.unknown_reference`(带 `available` 列表);目录里有、但本部署服务不了 ⇒ 400 `request.model_unavailable`;空串 / 非字符串 ⇒ 400 `request.field_invalid` |
1290
+ | ~~tools~~ | 🚫 | **不是受理键**(传了 ⇒ 400 `request.body_shape`,`unknownKeys` 点名)——工具面走场景装配;按请求只能**减**:`excludeTools` / `settings.permissions` |
1230
1291
 
1231
1292
  **`TaskResult`(同步 / `done` 事件里拿到的;7.64.0 起终局是**一条** `terminal` 因由,不再有 `status`/`errorCode`/`errorMessage`/`blockedReason` 四个平面键):**
1232
1293
  ```json
@@ -1290,6 +1351,7 @@ curl -N http://<host>:8090/v1/runs/<taskId>/events \
1290
1351
 
1291
1352
  # ③ 或者轮询最终结果(同样带 principal)
1292
1353
  curl -s http://<host>:8090/v1/runs/<taskId> -H 'x-agent-principal: user:42' # → {status, result, error?}
1354
+ # 停在门上(status: suspended / needs_review)时 result 是停下来那一份结果(7.104.0 起;修前缺席),续跑后被终局结果整块替换
1293
1355
  ```
1294
1356
  > 红利:②③ 经 LB 落到**任意一个 worker** 都能拿到同一个 run 的事件(数据全在 TiDB)。OA 不用关心是哪个 worker。
1295
1357
 
@@ -1412,9 +1474,29 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
1412
1474
  > `SUBAGENT_TRANSCRIPT_RETENTION_DAYS_DEFAULT`(30 天)是共享词表常量,**本服务未按它跑**;天数形专用
1413
1475
  > 旋钮要等 core 在 reap policy 开出转录独立年龄座后再铸(届时名 `SUBAGENT_TRANSCRIPT_RETENTION_DAYS`)。
1414
1476
  > 生效值上 boot 日志:`wiring_static.subagentTranscriptRetentionMs`。
1415
- > - `MEMORY_CONSOLIDATION_DRIVER=on|off`(默认 `off`):记忆**折叠(consolidation)阀门**。一个从未折叠过的
1416
- > 记忆库只涨不折,检索质量随库龄衰减;开这根旋钮就给 worker 装上两个 operator 口,让人在自己选的节奏上
1417
- > 跑折叠。⚠️ **闭集两词,别的一律拒启**——本旋钮是新的,不继承 `MEMORY_ENGINE` 的 `true/1/yes` 放宽:
1477
+ > - `MEMORY_CONSOLIDATION_DRIVER=on|off`(**未设 = 出厂缺省派生**,7.104.0 起,见下):记忆
1478
+ > **折叠(consolidation)阀门**。一个从未折叠过的记忆库只涨不折,检索质量随库龄衰减;开着 ⇒ worker 装上两个
1479
+ > operator 口(人在自己选的节奏上跑折叠),引擎面整理两席接上(下一根旋钮的前置)。
1480
+ > 🔴 **出厂缺省 = 开,但只在整理座不落主模型时生效**:
1481
+ > - **未设 + 便宜模型在场**(`MODEL_CHEAP_ID` 设了,或目录 `config.d/models.json` / 配置中心给 `roles.consolidate` /
1482
+ > `roles.summarize` / `flash` 档位绑了主模型之外的条目)⇒ **开**。判据按**装配后**的整理座(引擎自己的解析器:
1483
+ > consolidate → summarize,含 flash 档位缺省)解到的**目录条目**是不是主模型那一条(按条目身份,同 id 异网关的两条算两只;
1484
+ > `MODEL_CHEAP_ID` 与 `MODEL_ID` 写成同一个 id 则不铸 cheap 槽 —— 那就是主模型,算「没有便宜模型」),不按 env 拼写。
1485
+ > - **未设 + 没有便宜模型**(`MODEL_CHEAP_ID` 未设时 summarize 落主模型)⇒ **关** + 起服一行 warn
1486
+ > `memory_consolidation_default_off_no_cheap_model`(点名 `MODEL_CHEAP_ID`;显式写 `off` 即静音)。理由:一轮整库
1487
+ > 折叠读 ~1e5 prompt tokens,落主模型就是按最贵的价记账。
1488
+ > - **未设 + 结构上跑不起来**(下面 ①②③ 三形:多租户 / `MEMORY_ENGINE=off` / pg·tidb 记忆后端 / `MEMORY_PROVENANCE=off`)
1489
+ > ⇒ **恒关、零提示**(配不配便宜模型它都开不了;缺省不造新拒启)。
1490
+ > - **显式 `on` / `off` 恒赢**。显式 `on` 而座落主模型 ⇒ 照开,起服一行 warn `memory_consolidation_on_main_model`
1491
+ > 说清按主模型计费(部署方显式承担)。
1492
+ > - 起服**判一次**:之后配置中心热改 roles / 档位(或 boot 拉取超窗、目录迟到),阀门不跟着翻,但翻的需要由
1493
+ > `restart.reasons ∋ "memory-consolidation"` 报出来 —— 判决会翻(派生开 ↔ 关,含只把主模型挪到整理座那一条上)或开着而座漂了 ⇒ 报;
1494
+ > 判决不翻的目录变化不报。
1495
+ > 启动日志 `memory_consolidation_driver_armed` 带一位 `valve`(`explicit` / `derived`)与模型 id。配置目录这一行
1496
+ > (`GET /v1/config/catalog`)的 `effectiveValue` 答**真生效值** `on` / `off`(起服判决;派生说明在 `derivedDefaultNote`)。
1497
+ > - `MEMORY_CONSOLIDATION_SCOPES` 配了而阀门**最终没开** ⇒ 拒启(半配;7.104.0 起判在阀门定形处 —— 未设 + scope +
1498
+ > 便宜模型在场 ⇒ 派生开,不再误拒)。
1499
+ > ⚠️ **闭集两词,别的一律拒启**——本旋钮是新的,不继承 `MEMORY_ENGINE` 的 `true/1/yes` 放宽:
1418
1500
  > 一个被静默读成 `off` 的开词买到的是「运维以为在折叠、其实一次都没跑过」,而这条面的失败恰恰看不出来。
1419
1501
  > 🔴 **`on` 而这台机器结构上跑不了 ⇒ 拒启**(四形各有点名文案):①没接记忆引擎(`MEMORY_ENGINE=off`,
1420
1502
  > 或多租户形——记忆引擎只在单用户部署上装配);②记忆后端不自带引擎控制面归属(`MEMORY_ENGINE_BACKEND=file`
@@ -1436,8 +1518,12 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
1436
1518
  > (~1e5 prompt tokens)并把内容交给整理模型,既是钱更是**数据出境**。
1437
1519
  > ⚠️ **闭集两词,别的一律拒启**(与上一根同一条纪律,方向在这根上更要紧:一个被读成「未设」的拼错关词
1438
1520
  > 会让那台部署跟着 core 的新缺省**静默武装**,而运维手里握着一份自以为关掉了的配置)。
1439
- > 🔴 **前置 = 上一根旋钮**:引擎面的整理两席(协议席 + 模型席)在场 **iff** `MEMORY_CONSOLIDATION_DRIVER=on`
1440
- > 且记忆引擎接了线 —— 两席与那两个 operator 口**共用同一只已解析座**(不做第二份装配)。
1521
+ > 🔴 **前置 = 上一根旋钮**:引擎面的整理两席(协议席 + 模型席)在场 **iff** 整理阀门开着(`MEMORY_CONSOLIDATION_DRIVER=on`,
1522
+ > 或 7.104.0 起未设而出厂缺省派生开)且记忆引擎接了线 —— 两席与那两个 operator 口**共用同一只已解析座**(不做第二份装配)。
1523
+ > 🔴🔴 **7.104.0 升级影响**:配了便宜模型(`MODEL_CHEAP_ID`)、记忆引擎接线(单用户 + file 记忆后端)而**没表态**
1524
+ > `MEMORY_CONSOLIDATION_DRIVER` 的部署,升级后阀门派生开 ⇒ 本旋钮未设 = 引擎缺省开 ⇒ **每个产生了整理建议的任务之后自动跑一轮
1525
+ > 整库整理,记忆内容外流到便宜模型、按便宜模型计费**。不想要:`MEMORY_AUTO_CONSOLIDATION=off`(只关自动跑,手动口保留)或
1526
+ > `MEMORY_CONSOLIDATION_DRIVER=off`(整条关)。
1441
1527
  > · 阀门关着 ⇒ 两席都不写键,引擎那半场**逐字节不变**;此时 `MEMORY_AUTO_CONSOLIDATION=on`
1442
1528
  > **启动期响亮拒**(core 对「武装但没接整理席」是**每一次 prepare 都抛** `config.auto_consolidation`,
1443
1529
  > 留到运行期就是一台**启动成功、每个任务都死**的机器)。
@@ -1642,6 +1728,7 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
1642
1728
  |---|---|---|
1643
1729
  | HTTP `200` + `terminal.kind:"completed"` | 成功 | 取 `result` |
1644
1730
  | `terminal.kind:"blocked"` | agent 主动报卡住 | 看 `terminal.reason`,补信息再发 |
1731
+ | HTTP `200` + `terminal.kind:"paused"` + 顶层 `status:"suspended"\|"needs_review"`(同步 `POST /v1/tasks`) | run **停在门上**(工具审批 / 计划审阅 / 资源分片),没跑完。🔴 **7.104.0 起(S-173)**这张体就是那份结果本身(`terminal.gate` 说停在哪扇门、`checkpointId` 是对账位、`result`/`stats` 是停下来那一刻的读数),另叠 `taskId`/`sessionId`/`status` 三键 poll 回执 —— 修前只有这三键。恢复凭据**从不**在体上 | 按 `taskId` 轮询 `GET /v1/runs/:taskId`(park 中那一行同样带这份 `result`);审批走 `/v1/approvals`。判读用 `terminal.kind`,旧的 `status` 读法照旧成立。契约 §5.0c |
1645
1732
  | `terminal:{kind:"failed", code:"conflict"}` | 跨实例乐观锁丢失 | 直接重试(幂等) |
1646
1733
  | `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`(毫秒) |
1647
1734
  | `terminal:{kind:"failed", code:"limits.max_turns_exceeded"}` | 轮数到帽。🔴 **同上收窄**(引擎 7.13.0):帽子若落在**那一轮答案自己**身上 ⇒ `completed` + `task.interrupt_unconsumed{origin:"turns"}`,不铸本码 | 提高 `limits.maxTurns` / 拆小任务 |
@@ -1653,6 +1740,8 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
1653
1740
  | HTTP `429` + `errorCode:"usage.window_exhausted"` | 部署级治理窗耗尽(`USAGE_WINDOWS`,token 或 $ 天花板先满者) | 看响应体 `retryAfterSec` 退避;窗滑动/桶到期后放行 |
1654
1741
  | `terminal:{kind:"failed", code:"usage.window_exhausted"}` | 已受理的 run 在 **turn 边界**撞上治理窗且无法 durable 挂起 | 同上退避后重投;配好 checkpoint 基建则改为 `suspended` 等窗自动续跑 |
1655
1742
  | HTTP `501`(`/v1/runs`) | 内存模式不支持异步 | 配 MySQL 协议存储(`SESSION_BACKEND=mysql`) |
1743
+ | HTTP `500` + `errorCode:"corrupt_session"`(7.104.0 起;流式口在应答头之后则是 `done` 帧的 `result.terminal.code`,后台 run 是 run 行的 `errorCode`) | 这只会话的**存档读不出**、唤不醒(文件后端:会话日志没有会话头;SQL 后端:一行事件读不出)。这一轮没跑,一个 token 没烧。直接读这只会话的口(头探针 / 会话视图 / 导出 / 分叉 / 2c 同步)同答(契约 §17) | **别重试**(确定性失败)。运维看日志 `session_repo_corrupt_read`(带文件路径与跳过行号)修或移开那份存档;要继续工作就换一个新会话 |
1744
+ | 流里一帧 `engine_notice` `code:"session.records_skipped"`,`detail:{ dropped, entriesKept, sessionId }`(7.104.0 起,只在文件后端) | 这只会话的存档打开时有 `dropped` 条记录读不出、被跳过了;这一轮**照跑**,但模型读到的历史只剩 `entriesKept` 条。存档没修好之前,每次真正打开这只会话都会再说一次(会话缓存命中的轮次不打开、不说;`SESSION_CACHE_TTL_SEC=0` 时每一轮都说) | 告诉用户「这段对话的一部分历史丢了」;运维按日志 `session_repo_corrupt_read` 查那份文件。非流式 `POST /v1/tasks` 收不到通告帧(只有日志) |
1656
1745
 
1657
1746
  **机器码**:每个 4xx/5xx 响应体都带一个 `errorCode`(与人类文案 `error` 并列),这是**唯一**该拿来做
1658
1747
  程序分支的字段——**别锚 `error` 文案**。前缀族固定:`auth.` / `request.` / `not_found.` / `conflict.` /
@@ -2010,3 +2099,169 @@ server 进程(HTTP 服务与 `run-local`)**所有走 undici 的出网跳**都认
2010
2099
  (`node:https` + `ws`)、LSP 的 `ws` 传输、ssh 车道、MySQL / PG 驱动(TCP)、`bake-runner`(独立进程)。宿主上起的 host shell 与 git 子进程 env 里
2011
2100
  照常带着代理变量(不被剥名),由 git / curl 这类工具自己认它们;**MCP stdio 服务器不继承**(MCP SDK 只给子进程 `HOME` / `LOGNAME` / `PATH` /
2012
2101
  `SHELL` / `TERM` / `USER` 加上该服务器声明的 env)—— 要它走代理,在它的 env 声明里写上代理变量。
2102
+
2103
+ ### 9.9 发车链(维护者):本地两阶段发包 · CI 健康读数 · 远端门上船清单 · 提货门区间 · core RC 探针 · 读表坐标 · 兄弟仓改名 · 门 × 凭据 · tag 跑复用 main 结论(7.104.0 起)
2104
+
2105
+ 这几件是**仓库维护者**的工具面,不影响服务运行时。
2106
+
2107
+ - **npm 发包 = 本地两阶段**(维护者裁定,2026-09-28,S-873;**永久**取代「发包走 tag → CI」):`scripts/release.sh` 门 3 推 tag、核远端 tag 之后,
2108
+ 门 3.5 调 `bash scripts/publish-local.sh <version>`,绿了才起门 4/5/6。起链时 `NPM_TOKEN` 就要在、而且要有效:起链准备段在只带它的子进程里对
2109
+ npmjs 跑一次 `npm whoami`(临时 npmrc 只写 `${NPM_TOKEN}` 引用,60 s、不重试)—— 缺席 / E401(令牌无效)/ E403(无权)/ 其它(网络不可达)⇒
2110
+ 第一道门之前停链,红句不打印令牌与用户名;通过 ⇒ 链日志一行 `NPM-WHOAMI ok`(它证「npmjs 认得这把令牌」,证不了对本包的发布权 / OTP,那一半仍在
2111
+ 门 3.5)。门 1 在洁净树之后 `npm ci`(门 2 测的依赖 = 锁文件 = 门 3.5 的 tag 树装的那一份);**node_modules 是软链的树不能起链**(npm ci 清空的是
2112
+ 软链所指那棵树的依赖,软链到同一处的别的树会在脚下丢依赖)—— 门 1 在 `npm ci` 之前停链、点名软链指向哪里,在 node_modules 是真目录的树里起链。
2113
+ 门 1 记下起链提交;门 3 打 tag 之前断言 HEAD、本地已有的同名 tag 都还是它、树仍干净(链跑的这段时间发车树里有人提交 / 留着一只过期的本地 tag ⇒
2114
+ 停链,不给没测过的提交打 tag)。GitHub Actions 不再发 npmjs 包(`ci.yml` 的 `npmjs-publish` job 已删;
2115
+ 镜像 job 与 GH Packages 镜像位照旧随 tag 跑)。脚本的步骤,每步一行 `PUBLISH-STEP v=… step=…` 进链日志 `scratchpad/release-gates-<v>.log`:
2116
+ `tag`(本地 tag 在、tag 树的版本对)→ `precheck`(registry 上本版在不在;读不出 ⇒ 红,不盲发)→ `token` → `clone`(干净 tag 树 =
2117
+ `git clone --depth 1 --branch v<v> file://<仓>`)→ `npm-ci` → `prepublish`(`npm run prepublishOnly`)→ `pack` → `publish`(**在 tag 树目录里**
2118
+ `npm publish --ignore-scripts`)→ `readback-wait`(轮询 registry 至多 20 min)→ `readback-shasum`(dist.shasum = 本地 tarball 的 sha1)→
2119
+ `readback-bytes`(curl 回读 tarball,与本地 `cmp` 逐字节同)→ `githead`(registry 的 gitHead = tag commit)。脚本钉 `umask 022`(文件权限进 tar 头,
2120
+ 换 umask 重跑会 pack 出另一份字节);每次 registry 请求有硬上限(60 s),回读等待另有 20 min 墙钟截止,registry 挂住不会挂死链。成功尾行
2121
+ `PUBLISH v=<v> shasum=<sha1> gitHead=<sha> bytes=<n> readback=identical`(发车帖 / CHANGELOG 回填 shasum 与 gitHead 从这里取);任一步红 ⇒
2122
+ `PUBLISH-FAIL v=<v> step=<步骤>` + 非零退出 + 一句点名步骤的红因。
2123
+ - **幂等**:registry 上已有本版 ⇒ 不重发,只做回读比对 —— 与本地 pack 逐字节同且 gitHead 对 ⇒ 绿并说「已在」;不同 ⇒ 红(别人发的字节,版本不可覆盖)。
2124
+ 门 3.5 红了(tag 已推):修好后单独重跑 `bash scripts/publish-local.sh <v>`,再前台跑 `bash scripts/post-publish-gates.sh <v>`。
2125
+ **手跑的前置**:两只脚本开头都封口,封口要 import `@sema-agent/core`(封口的判据 = core `scrubSecretEnv`)⇒ 在本树装着依赖的前提下跑
2126
+ (别的机器上新 clone 的树 / `npm ci` 半途断掉的树先 `npm ci --no-audit --no-fund`;缺了它封口红,红句带这条出路)。
2127
+ - **为什么是 clone 而不是 git worktree**:npm 从目录发布时读 `<git 根>/.git/HEAD` 填 gitHead;worktree 的 `.git` 是文件 ⇒ gitHead 为空
2128
+ (与 7.103.1 从 tarball 文件发布同一个结果)。
2129
+ - **凭据**:**需要环境变量 `NPM_TOKEN`**(只在要发的时候查;发车机 `~/.npmrc` 那一行本身就是 `//registry.npmjs.org/:_authToken=${NPM_TOKEN}`
2130
+ 引用)。脚本不读 `~/.npmrc`:每条 npm 调用带临时 `--userconfig`,临时 npmrc 只写 `${NPM_TOKEN}` 引用、不写值;开头封口,`npm ci` 的安装脚本 /
2131
+ 构建 / pack-hygiene 测试拿不到任何凭据,只有 `npm publish` 那一个进程拿到 `NPM_TOKEN`(且带 `--ignore-scripts`)。令牌从不打印。
2132
+ - **门 4 不再等 CI**(同一裁定):`scripts/post-publish-gates.sh` 的门 4 = 一个 45 min 的 npm 窗(公开 packument 上 `versions[<v>]` 在且
2133
+ `dist-tags.latest` 追平)。判决行 `GATES456 v=… rc4=… rc5=… rc6=… shasum=… elapsed=… ciTag=… ciMain=… tagRun=… mainRun=… why4=…` 十一键形状不变;
2134
+ CI 两腿(`ciTag` / `ciMain` / `tagRun` / `mainRun`)是**信息格**,不进 rc4、不进 why4;`why4` 只剩 `propagation_timeout`(rc4 非零)与 `-`(绿)。
2135
+ 自此门 4 常态首拍即绿,CI 两腿多半还在跑 ⇒ 常落 `unchecked:still-running`,如实。
2136
+ - **CI 健康读数**(S-846):`GH_TOKEN=… node scripts/ci-health-brief.mjs` 读 GitHub 上 `ci.yml` 在 `main` 的最近一次 run,一行
2137
+ `CI-HEALTH main=… test=… steps=… run=… verdict=ok|red why=…`(rc 0 = ok,1 = red,2 = 用法错)—— 每小时巡查用。`test` job success,或在跑且已起步骤
2138
+ ⇒ ok;completed 非 success(零步骤 = CI 没起,如 7.103.1 的账单真案 / 有步骤 = CI 红)、排队零步骤、读不出 ⇒ red(读不到 ≠ 健康)。
2139
+ 它**不再是发车链的一道门**:门 1.11 随同一裁定撤出(它的前提「CI 起不来 ⇒ tag 推了也发不了包」已不成立),起链不再需要 `GH_TOKEN`,
2140
+ 放行旋钮一并删除。CI 起不来今天挡的是镜像与 main 上的全套,不挡发包。
2141
+ - **`scripts/remote-gate.sh ship`** 只把 `git ls-files --cached --others --exclude-standard` 列出的文件打包上 k3s8(已跟踪 + 未跟踪而未被 ignore;
2142
+ 工作区未提交的改动照带),被 gitignore 的本地探针测试 / scratchpad 读数不再上船。清单生成不出来(不在 git 工作树里 / 空树)⇒ 在碰远端目录之前停。
2143
+ - **core 提货门**(`node scripts/core-read-table-gate.mjs`)求交的是 **(当前钉, 目标]** 区间里 core 的**每一版**清单:从目标清单沿 `from`
2144
+ 回溯到钉的那条链,每一环的清单都从目标 tag 的树里读(`git -C ../sema-core show v<目标>:wire-changes/core@<ver>.json`),取并集 —— 跳版提货
2145
+ 不再只读目标那一份;链断了(某环清单读不出、本地 tag 树缺了那一版)⇒ exit 2 点名。门会先 `git fetch --tags` 那个 core 克隆(不切换 checkout)。
2146
+ 当前钉缺省取本仓上一发版 tag 的 `package.json`,可用 `--from <x.y.z>` 显式给;`--core-repo` 必须是 core 仓的 git 根
2147
+ (安装树 `node_modules/@sema-agent/core` 不再接受)。
2148
+ - **core RC 探针**(维护者裁定,2026-09-28,S-878):core 出 RC tarball 时直讯本仓(`npm pack` 产物路径 + shasum + `wire-changes/core@<ver>.json` 的 commit),
2149
+ 当拍跑:
2150
+ ```bash
2151
+ git -C ../sema-core show <commit>:wire-changes/core@<ver>.json > /tmp/rc/core@<ver>.json # 只读邻仓,不切换 checkout
2152
+ GATE_HOST=root@<k3s8> bash scripts/core-rc-probe.sh <tarball> --wire-changes /tmp/rc/core@<ver>.json [--lane <名>]
2153
+ # --dry-run:只做前四步(不上 k3s8);--core-repo <core 仓的 git 根>(缺省 ../sema-core)
2154
+ ```
2155
+ 脚本从本仓 **HEAD** 建临时 worktree(主树一字不动),六步各落一行 `CORE-RC-STEP v=<ver> step=<名> …` 进 `scratchpad/core-rc-probe-<ver>.log`
2156
+ (原始输出在同目录的 `core-rc-probe-<ver>.XXXX/`):`tarball`(sha1 = npm 的 dist.shasum,**只打读数,与 core 直讯的 shasum 由人对**;tarball 自报版本
2157
+ 必须 = 清单文件名 `core@<ver>.json` 的版本,不一致 ⇒ 红句点名两者、不建临时树)→ `install`(npm ci → `npm i <tarball> --save-exact`,声明改成精确版本
2158
+ = 真提货形)→ `tsc` → `read-table`(重生成读表 → 提货门 `--wire-changes <文件>` `--from <HEAD 的 core 钉>`:同一只门,目标清单读自文件 —— RC 还没有
2159
+ tag;清单文件的文件名与它自报的 `repo` / `version` 都要是 core@<ver>,不然门判输入不可信)→ `probes`(k3s8 车道 `/root/ci/sema-server-<lane>`,
2160
+ 缺省 lane = `RC-<ver>-<本次运行的随机后缀>`,起用前原子占位 —— 已存在 ⇒ 红、不接手;secret-env 族 + 四只门钉)→ `default`(k3s8 全量,`--dsn ""`)。
2161
+ 任一步红 ⇒ `CORE-RC-FAIL v=<ver> step=<名>`,后续照跑,最后非零退出;全绿才删临时树与本次占到的 k3s8 车道目录(先核没有容器挂着 —— 读不出也不删),
2162
+ 红则留盘、路径在红句里。
2163
+ **读哪一行**:日志最后一行(尾行)
2164
+ `CORE-RC v=<ver> shasum=<sha1> tsc=<rc> gate=<0|N|untrusted> probes=<pass>/<fail> default=<pass>/<fail> regressions=<N> failed=<文件,…|->`
2165
+ —— 缺读数一律 `-`;`regressions` 只在 probes 与 default 两步都有读数时才是数(`-` ≠ 0);`gate=N` = 门命中的不同键数(`read-table` 步骤行的
2166
+ `keyNames=` 逐键),`untrusted` = 门判输入不可信。🚧 `it.fails` 翻绿单列 `CORE-RC-FLIP file=… it=…`(core 修好了本仓钉着的缺陷,翻钉文件本身不算
2167
+ 回归,回帖要报)。但翻钉会让 vitest 的 rc 为 1,而 JSON 报告不记全局未处理错误与 globalSetup 收尾的红 —— 探针证明不了 rc 里没藏别的,所以「rc 1 而零
2168
+ 回归文件」恒记一只 `<步>:rc1-unattributed`;读 `<步>.out` 的 vitest 汇总,只有翻钉(没有 `Errors` 行、没有 teardown 报错)才由人判「无回归」并在回帖里
2169
+ 写明。rc 不在 {0,1}(ssh 断 / 被杀)同样记 `<步>:rc<n>-unattributed`,那一跑作废重跑。
2170
+ **回帖格式**(发给 core):`无回归` 或 `回归 N 条`,接尾行原文 + 每条 `CORE-RC-FLIP` 行 + `gate≠0` 时的逐键;回归逐文件一句归因。
2171
+ ⚠️ 版本配对类的钉(`core-dependency-floor` / `gate-negative-controls` 的 `CORE_FLOOR` 声明、`core-consumption-ledger-gate` 的台账 `coreVersion`、
2172
+ `store-contract-kit` 的 `KIT_REVIEWED_AGAINST` …)在任何新版本的 RC 上**按构造红**:它们是提货批的记账义务,不是 core 的回归 —— 回帖前逐个认出来、
2173
+ 从回归数里剔掉并写明。
2174
+ - `ci-health-brief.mjs` 调 `gh` 时主机钉 `github.com`、只带 `GH_TOKEN`(环境里的 `GH_HOST` / 企业凭据 / `GITHUB_TOKEN` 不会影响读数)。
2175
+ - **core 读表坐标不含行号**(S-593):`docs/core-consumption/READ-TABLE.json` 的每条坐标 = `<文件>#<名路径>`(导入读点 = 本地绑定名;闭表读点 =
2176
+ `<闭表名>.<键>` / `<闭表名>[]`;同文件同名路径读了 N 次尾缀 ` ×N`)。门 1.8(`node scripts/core-read-table.mjs --check`)因此只在**读点集合真变了**
2177
+ 时红(多读 / 少读一个 core 名、闭表增删键、读点换了宿主);只挪行号的编辑不再要求重生成读表。提货门命中行打印同形坐标,找行拿名路径 grep。
2178
+ - **按名字找兄弟仓的门:改名 ⇒ 红**(S-597):门 1.9(`scripts/settings-schema-duty.mjs`)与测试侧按名字找兄弟仓的门一律经
2179
+ `scripts/lib/sibling-repo.mjs` 三态定位 —— 名字缺席而改名映射里的对位名在场(本机 checkout 还叫旧名 `sema-web` / `sema-registry-core`,或门的名单
2180
+ 还写旧名)⇒ **红**并指路(把目录改名 / 重新 clone 成新名),门 1.9 上持有方与非持有方一样;两个名字都不在 ⇒ 照旧(门 1.9:持有方包仓红、非持有方
2181
+ 提示;测试侧:skip,skip 句点名找过的名字与 org 根)。改仓名 / 改包名的同窗动作清单在 `docs/PIPELINE-DISCIPLINE.md`。
2182
+ - **门 × 凭据**(S-631):起链环境里的凭据(`GITHUB_SEMA_ORG_API_TOKEN` / `NPM_TOKEN` / `GH_TOKEN` / `DEEPSEEK_API_KEY` / 三条 `*_TEST_URL` …)照旧由链启动器
2183
+ 注入,但**每一道门的子进程只拿到 `scripts/lib/gate-credentials.mjs` 表里它那一行**;表外的凭据形变量(core `scrubSecretEnv` 的判据 + 本链
2184
+ DSN 名)一律不交。`release.sh` / `post-publish-gates.sh` 开头封口,每道门先换门;链日志 `scratchpad/release-gates-<v>.log` 每门一行
2185
+ `GATE-ENV gate=<门> handed=<名,…|-> withheld=<名,…|->`(只有名字)。
2186
+ - 加一道新门 / 某门要一个新变量:在表里登记那一行(不要凭据就登记空行),脚本在那一门的 `step` 之后 `gate_creds_enter <门>`。漏登记 ⇒ 那一门
2187
+ 拿不到变量而红,退出处有一句「停在门 X,这一门被剥掉的凭据形变量:…」。测试新读一条起链环境里的凭据:授予读它的门 2 组,或登记进
2188
+ `GATE2_WITHHELD_TEST_CREDENTIALS` 写明为何不给(钉会逼这一步)。
2189
+ - 起链环境里有名字不是 shell 标识符的凭据形变量,或带凭据条目的 git env 配置记录(`GIT_CONFIG_COUNT` / `GIT_CONFIG_KEY_<i>` …)⇒ 封口拒绝起链
2190
+ (点名不打值);起链前 unset 掉。
2191
+ - `scripts/remote-gate.sh run --dsn "<名…>" <cmd>` 只把点名的 DSN(`TIDB_TEST_URL` / `PG_TEST_URL` / `INNODB_TEST_URL`,值取 `GATE_*_TEST_URL`
2192
+ 或缺省)交给容器,`--dsn ""` = 一条都不给;**不给 `--dsn`(手跑)照旧三条**。`ship` 的容器不带 DSN。
2193
+ - **tag 跑复用 main 上同一提交的 test 结论**(S-159,`.github/workflows/ci.yml`):tag run 的 `test` job 先跑 `node scripts/ci-tag-test-reuse.mjs`,
2194
+ 按 headSha 读 main 上同一提交那次 push run 的 `test` job —— success ⇒ 跳过全套;在跑 ⇒ 等(上限 45 min,复用步 55 min 时限之内);
2195
+ 失败 / 查不到 / 读不出 ⇒ 照跑全套。结论一行 `TAG-TEST-REUSE decision=reuse|run why=… mainRun=… waited=…s sha=…` 在该 job 的日志与 step summary 里。
2196
+ ⚠️ 这条还没在真 CI 上跑过(组织账单恢复后的第一次发车核它:tag run 的 test job 里复用步那一行应是 `decision=reuse why=main_success`)。
2197
+
2198
+ ### 9.10 模型钥匙的 sealed 形(本地车道)(7.104.0 起)
2199
+
2200
+ 把模型 key 以**密文**写进 `config.d/models.json`:文件里只有密文,明文只在引擎进程的内存里出现。本节写本地车道(`CONFIG_PROVIDER=local`
2201
+ 的 HTTP 服务进程);接配置中心的部署走中心形(web 面贴 key、中心存密文),开箱是同一条路径。⚠️ `run-local` 一次性 CLI **不开保管店**:
2202
+ 同一份 `config.d` 里的 sealed 条目在 `run-local` 下是 `no_key_store`(见下表),响亮失败,不会退回别的 key。
2203
+
2204
+ **① 取公钥。** `GET /v1/capabilities`(与其它能力位同门,在服务凭据门之后)多一段 `sealedKey`,三键:
2205
+
2206
+ ```json
2207
+ "sealedKey": { "alg": "libsodium-sealedbox-x25519", "publicKeyId": "exec-<12 位 hex>", "publicKey": "<44 字符 base64>" }
2208
+ ```
2209
+
2210
+ - `alg`:恒为 `libsodium-sealedbox-x25519`(当前唯一取值);`publicKeyId`:写进密文条目的那个 id;`publicKey`:X25519 公钥,标准 base64(32 字节)。
2211
+ - 值是**本进程**保管店里最新那把钥匙(`createdAt` 最新)。只有公钥:私钥、私钥文件路径、本机还持有的旧钥匙 id 都不在这个面上。
2212
+ - **键缺席**有两种含义:引擎早于 7.104.0(不认识这一位),或这台引擎今天没有保管店(起服时建店失败,日志 `sealed_key_store_unavailable`)。
2213
+ 同一个体里的 `version` 能区分这两种;不论哪种,调用方的动作相同 —— **不要封**:改用 `apiKeyEnv`,或修好保管目录后重启再取。
2214
+
2215
+ **② 封。** libsodium `crypto_box_seal(明文 key 的 UTF-8 字节, 公钥的原始 32 字节)`,结果转**标准 base64**(字母表 `A–Z a–z 0–9 + /`,
2216
+ 带 `=` 补齐,长度是 4 的倍数)。URL-safe 字母表(`-` / `_`)⇒ 条目判形失败、整条不可用;缺了应有的 `=` ⇒ `sealed_decrypt_failed`。Node 例:
2217
+
2218
+ ```js
2219
+ import sodium from "libsodium-wrappers";
2220
+ await sodium.ready;
2221
+ const pk = new Uint8Array(Buffer.from(caps.sealedKey.publicKey, "base64"));
2222
+ const ciphertext = Buffer.from(sodium.crypto_box_seal(sodium.from_string(apiKey), pk)).toString("base64");
2223
+ ```
2224
+
2225
+ **③ 写。** `config.d/models.json` 的条目带 `sealedApiKey`(形由 `@sema-agent/settings-schema` 的 `ModelEntry.sealedApiKey` 定;`setAt` 可选,手写可省):
2226
+
2227
+ ```json
2228
+ { "models": [ { "name": "my-model", "id": "<上游模型 id>", "provider": "gateway", "api": "openai-completions",
2229
+ "sealedApiKey": { "alg": "libsodium-sealedbox-x25519", "publicKeyId": "<取自 caps>", "ciphertext": "<上一步的 base64>" } } ] }
2230
+ ```
2231
+
2232
+ - 写入后下一拍刷新生效(60 s 轮询,或 `POST /v1/admin/config/refresh` 立即触发),不用重启。
2233
+ - 形不对(`alg` 不是上面那个词、`ciphertext` 含标准 base64 字母表以外的字符)⇒ 这一条目整条不可用,与其它判形失败的条目走同一条路径;
2234
+ 不是下面三种失败词之一。
2235
+
2236
+ **④ 与 `apiKeyEnv` 的关系(按代码行为)。** 同一条目两个都写 ⇒ **sealed 优先,`apiKeyEnv` 整条不看**:开箱成功就用开出来的 key;开箱失败 ⇒
2237
+ 这只模型的每一次模型调用都失败(用它的任务响亮失败),**不退回 `apiKeyEnv`,也不退回网关 `MODEL_API_KEY`**。对照:只写 `apiKeyEnv` 而那个
2238
+ env 变量没设 ⇒ 这只模型退回网关 key(配置应用时 warn `sema_registry_model_key_env_missing`)—— 两种写法「坏了之后」的方向相反。
2239
+
2240
+ **⑤ 三种失败词**(配置应用那一拍每只模型一行 warn;用这只模型的任务在模型调用处失败,错误句里带失败词与 `publicKeyId`):
2241
+
2242
+ | 失败词 | 什么时候出 | 怎么修 |
2243
+ |---|---|---|
2244
+ | `sealed_decrypt_failed` | 本进程**持有**这个 `publicKeyId` 的私钥,但开不开:密文不是对着这把公钥封的、被截断或改坏(短于 48 字节)、缺了应有的 `=` 补齐。warn `sema_registry_model_sealed_key_open_failed` | 对着 caps 当前给的公钥重封,写回条目 |
2245
+ | `stale_ciphertext` | 本进程**没有**这个 `publicKeyId` 的私钥:密文是对着别的进程 / 别的宿主 / 别的副本的公钥封的,或那把钥匙的私钥文件被删、读不了。warn `sema_registry_model_sealed_key_no_private_key`(带本机持有的 id) | 对着**这个进程** caps 给的公钥重封;或把那个 id 的私钥文件放回保管目录后重启 |
2246
+ | `no_key_store` | 本进程没有保管店:起服时保管目录建不了、改不了权限(只读挂载 / 权限不够 / 数据根不是目录),caps 没有 `sealedKey` 段。warn `sema_registry_model_sealed_key_no_store` | 修好保管目录(进程要能建它、每次起服把它设成 `0700`)后重启 |
2247
+
2248
+ **⑥ 保管目录与轮换。** 保管目录 = `<数据根>/sealed-key/`(数据根 = `LOCAL_DATA_ROOT` → `CONFIG_LOCAL_DIR` → `AGENT_DATA_DIR` → `~/.ai-agent`),
2249
+ 每把钥匙一个文件 `<publicKeyId>.json`(文件 `0600`,目录 `0700`)。**私钥只在这里**:丢了,对着它封的密文全部变成 `stale_ciphertext`,
2250
+ 只能重封 —— 备份时按私钥对待。
2251
+
2252
+ - 起服时读目录里的全部钥匙文件;一个可用的都没有 ⇒ 生成一把新的。读不了的文件(坏 JSON / 不认识的格式)跳过、**不删**,warn
2253
+ `sealed_key_file_unreadable`。文件只在起服时读:运行中放进来的钥匙文件要重启才认。
2254
+ - 轮换 = 新公钥 + 旧私钥文件留存:① 停服,把现有钥匙文件挪出目录;② 起服一次,目录为空 ⇒ 生成新的一把(这期间旧密文是 `stale_ciphertext`);
2255
+ ③ 停服,把旧文件挪回;④ 起服 —— 新旧都持有,caps 答新的那把,旧密文照常能开;⑤ 逐条对着新公钥重封;⑥ 没有条目再引用旧 id 之后才删旧文件
2256
+ (删早了 ⇒ 那些条目 `stale_ciphertext`)。
2257
+
2258
+ **⑦ 多副本:每台宿主一份。** `sealedKey` 段说的是「这个进程持有哪把钥匙」,在任何部署形下都是真话。保管店在各自的数据根下,所以**每台宿主一份**:
2259
+ 多个副本不共享保管目录时,各副本答出的公钥不同,对着 A 副本的公钥封的密文在 B 副本上是 `stale_ciphertext`(逐模型响亮失败,不退回任何共享 key)
2260
+ —— 在负载均衡后面取 caps,取到哪台就是哪台的钥匙。出路两条:
2261
+
2262
+ - **共享保管目录**:让每个副本的 `<数据根>/sealed-key/` 是同一份钥匙文件(例如把同一个卷挂到这个路径)。先把钥匙备好(先起一个副本让它生成,
2263
+ 或放好钥匙文件)再起其余副本 —— 空目录上并发首启会各自生成一把,各副本只持有自己那一把,直到下次重启。目录要对每个副本**可写**(每次起服
2264
+ 会把它设成 `0700`;只读挂载 ⇒ 建店失败 ⇒ `no_key_store`)。⚠️ 只共享 `sealed-key/` 这一个子目录:`DB_BACKEND=local` 的整个数据根禁止跨容器 /
2265
+ 跨主机共享(`docs/DEPLOY-PREREQS.md`)。
2266
+ - **走中心形**:接配置中心,执行侧公钥登记在中心(起服时带管理令牌自动上报,否则把公钥打印出来供手工登记),web 面对着登记的公钥封、中心存密文。
2267
+ 调用方不必逐台去问 caps;但同一个 worker 的多个副本各有各的保管店这件事中心形不改变 —— 要它们都开得了同一份密文,仍要共享保管目录。