@sema-agent/server 7.103.1 → 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 (177) hide show
  1. package/MIGRATION.md +362 -3
  2. package/README.md +6 -1
  3. package/README.zh-CN.md +5 -1
  4. package/USAGE.md +279 -18
  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/host-lsp-manager.d.ts +1 -1
  66. package/dist/http/admission.d.ts +11 -4
  67. package/dist/http/admission.js +5 -1
  68. package/dist/http/dispatch.js +3 -3
  69. package/dist/http/principal-gate.d.ts +22 -0
  70. package/dist/http/principal-gate.js +20 -0
  71. package/dist/http/resume-legs.js +9 -5
  72. package/dist/http/routes/agents-roster.d.ts +7 -0
  73. package/dist/http/routes/agents-roster.js +6 -2
  74. package/dist/http/routes/approvals-assistant.d.ts +48 -19
  75. package/dist/http/routes/approvals-assistant.js +117 -50
  76. package/dist/http/routes/capabilities.js +3 -2
  77. package/dist/http/routes/config-catalog.js +1 -0
  78. package/dist/http/routes/memory-consolidation.d.ts +3 -2
  79. package/dist/http/routes/memory-policy.js +3 -3
  80. package/dist/http/routes/rules.js +2 -2
  81. package/dist/http/routes/runs.js +27 -2
  82. package/dist/http/routes/session-sync.js +8 -14
  83. package/dist/http/routes/sessions-list.js +3 -2
  84. package/dist/http/routes/sessions.js +44 -23
  85. package/dist/http/routes/shared-memory.js +2 -2
  86. package/dist/http/routes/tasks.js +22 -19
  87. package/dist/http/routes/trace-usage.js +4 -4
  88. package/dist/http/send.d.ts +12 -0
  89. package/dist/http/send.js +12 -1
  90. package/dist/http/server.d.ts +21 -4
  91. package/dist/http/server.js +7 -4
  92. package/dist/http/sync-submit-body.d.ts +43 -0
  93. package/dist/http/sync-submit-body.js +4 -0
  94. package/dist/http/wire-types.d.ts +23 -5
  95. package/dist/index.d.ts +1 -0
  96. package/dist/index.js +1 -0
  97. package/dist/leader/merge.d.ts +8 -3
  98. package/dist/memory-incident-sink.js +2 -1
  99. package/dist/model-select.d.ts +9 -0
  100. package/dist/model-select.js +5 -1
  101. package/dist/observability/corrupt-read-seat.d.ts +151 -16
  102. package/dist/observability/corrupt-read-seat.js +158 -15
  103. package/dist/observability/fail-open.d.ts +12 -0
  104. package/dist/observability/fail-open.js +12 -0
  105. package/dist/observability/run-terminal.d.ts +15 -1
  106. package/dist/observability/run-terminal.js +4 -0
  107. package/dist/parked-decide.d.ts +8 -3
  108. package/dist/parked-decide.js +56 -20
  109. package/dist/parked-revive-surface.d.ts +14 -7
  110. package/dist/parked-revive-surface.js +5 -1
  111. package/dist/plugins/approval-ask-store-file.d.ts +4 -1
  112. package/dist/plugins/approval-ask-store-file.js +9 -12
  113. package/dist/plugins/approval-ask-store-sql.d.ts +4 -0
  114. package/dist/plugins/approval-ask-store-sql.js +4 -1
  115. package/dist/plugins/approval-exemption-store.d.ts +21 -1
  116. package/dist/plugins/approval-exemption-store.js +36 -7
  117. package/dist/plugins/approval-nonce-store.d.ts +16 -2
  118. package/dist/plugins/approval-nonce-store.js +17 -7
  119. package/dist/plugins/caching-session-store.d.ts +5 -0
  120. package/dist/plugins/caching-session-store.js +3 -0
  121. package/dist/plugins/file-resume-anchor-store.d.ts +7 -2
  122. package/dist/plugins/file-resume-anchor-store.js +19 -5
  123. package/dist/plugins/file-run-store.d.ts +21 -8
  124. package/dist/plugins/file-run-store.js +60 -38
  125. package/dist/plugins/local-session-store.d.ts +22 -0
  126. package/dist/plugins/local-session-store.js +74 -11
  127. package/dist/plugins/memory-run-store.d.ts +7 -5
  128. package/dist/plugins/memory-run-store.js +11 -4
  129. package/dist/plugins/one-time-migrations.d.ts +38 -1
  130. package/dist/plugins/one-time-migrations.js +143 -15
  131. package/dist/plugins/permission-rule-store-file.d.ts +9 -2
  132. package/dist/plugins/permission-rule-store-file.js +37 -51
  133. package/dist/plugins/pg-session-storage.d.ts +3 -0
  134. package/dist/plugins/pg-session-storage.js +19 -10
  135. package/dist/plugins/run-store-sql.d.ts +12 -4
  136. package/dist/plugins/run-store-sql.js +15 -13
  137. package/dist/plugins/send-file-ledger.d.ts +5 -1
  138. package/dist/plugins/send-file-ledger.js +11 -6
  139. package/dist/plugins/session-event-payload.d.ts +14 -4
  140. package/dist/plugins/session-event-payload.js +5 -2
  141. package/dist/plugins/store-backend.d.ts +7 -3
  142. package/dist/plugins/store-backend.js +10 -6
  143. package/dist/plugins/store-contracts.d.ts +7 -2
  144. package/dist/plugins/tidb-pool.js +8 -2
  145. package/dist/plugins/tidb-session-store.d.ts +6 -1
  146. package/dist/plugins/tidb-session-store.js +17 -10
  147. package/dist/project-identity.d.ts +4 -6
  148. package/dist/request-key-closure.js +1 -1
  149. package/dist/request-value-closure.d.ts +72 -3
  150. package/dist/request-value-closure.js +12 -4
  151. package/dist/roster-bound-http-status.d.ts +6 -0
  152. package/dist/roster-bound-http-status.js +6 -0
  153. package/dist/run-local.js +22 -27
  154. package/dist/runs.d.ts +22 -2
  155. package/dist/runs.js +29 -7
  156. package/dist/sealed-key.d.ts +31 -4
  157. package/dist/sealed-key.js +9 -0
  158. package/dist/security.d.ts +5 -1
  159. package/dist/server-secret-env.d.ts +2 -2
  160. package/dist/session-children-retire.d.ts +92 -0
  161. package/dist/session-children-retire.js +85 -0
  162. package/dist/session-error-wire.d.ts +58 -0
  163. package/dist/session-error-wire.js +38 -0
  164. package/dist/session-incarnation.d.ts +61 -0
  165. package/dist/session-incarnation.js +43 -0
  166. package/dist/session-records-skipped-notice.d.ts +20 -0
  167. package/dist/session-records-skipped-notice.js +27 -0
  168. package/dist/tool-approval.d.ts +65 -0
  169. package/dist/tool-approval.js +36 -9
  170. package/dist/trace/core-keyset-guard.d.ts +8 -7
  171. package/dist/trace/engine-notice-wire.d.ts +3 -1
  172. package/dist/trace/engine-notice-wire.js +2 -0
  173. package/dist/trace/project.d.ts +1 -1
  174. package/dist/trace/project.js +9 -0
  175. package/dist/trace/wire-projection-faces.d.ts +10 -3
  176. package/dist/trace/wire-projection-faces.js +8 -2
  177. package/package.json +3 -3
@@ -47,6 +47,13 @@ export declare const TASK_WRITE_FACE_ESCALATIONS: readonly ["allow", "refuse"];
47
47
  export type TaskWriteFaceEscalation = (typeof TASK_WRITE_FACE_ESCALATIONS)[number];
48
48
  export declare const RETENTION_MODES: readonly ["audit-only", "enforce"];
49
49
  export type RetentionMode = (typeof RETENTION_MODES)[number];
50
+ /**
51
+ * S-841 —— `CONFIG_PROVIDER` 的词表闭集(**属主在此**:`loadConfig` 的唯一读点、config-center 工厂、执行车道合成步、
52
+ * 配置目录的 enum 行共读这一份)。缺席 / 空串 = 未表态(配了 `SEMA_REGISTRY_URL` ⇒ 中心,否则纯 env);两词之外 ⇒ 拒启。
53
+ * 修前是开放词:认不出的词被配置源判官当 `local`、被隐式 host 臂当「不是 local」,一个旋钮在一个进程里有两种读法。
54
+ */
55
+ export declare const CONFIG_PROVIDER_WORDS: readonly ["local", "remote"];
56
+ export type ConfigProviderWord = (typeof CONFIG_PROVIDER_WORDS)[number];
50
57
  /** [ref] §3/§4 —— sweep lane 的节律与灰度档(语义与三问见 {@link ServiceConfigFlat.retentionSweep})。 */
51
58
  export interface RetentionSweepConfig {
52
59
  /** `audit-only` = 照跑照判、只记 `audit_only` 行,**不调任何破坏性方法**;`enforce` = 真删。默认前者。 */
@@ -193,9 +200,17 @@ export declare function buildDefaultResilience(): ServiceConfigFlat["resilience"
193
200
  * {@link ServiceConfigFlat.memoryConsolidationDriver})。纯数据。
194
201
  */
195
202
  export interface MemoryConsolidationDriverConfig {
196
- /** 阀门(env `MEMORY_CONSOLIDATION_DRIVER=on|off`,缺省 `off`)。`false` = 零装配零探针:
197
- * 座不解析、引擎不构造、两个 admin 口整个不挂(404 族,**不是** 501)、能力位翻假。 */
198
- enabled: boolean;
203
+ /**
204
+ * 阀门的**部署表态**(env `MEMORY_CONSOLIDATION_DRIVER=on|off`,原词)。**缺席 = 没表态,不是 off**。
205
+ *
206
+ * 🔴 S-824 / clay **[ref]**(取代 [ref] ① 的「出厂 off」实况):出厂缺省 = **开,但只在整理座不落主模型时生效**
207
+ * (`MODEL_CHEAP_ID` 在场,或目录给 consolidate / summarize / flash 档位绑了别的模型);否则不开并在起服时提示一行。
208
+ * 这件事要等**目录应用之后**、记忆引擎接线之后才判得出来,所以本字段只记表态,开不开由 boot 第 6 段的
209
+ * `resolveConsolidationValve`(`boot/memory-consolidation.ts`)一次判定 —— 那是阀门开关**唯一**的属主。
210
+ * 显式 `on` / `off` 恒赢;阀门关 = 零装配零探针:座不解析、引擎不构造、两个 admin 口整个不挂(404 族,
211
+ * **不是** 501)、能力位翻假。
212
+ */
213
+ valve?: "on" | "off";
199
214
  /**
200
215
  * 周期腿节律(秒;env `MEMORY_CONSOLIDATION_INTERVAL_SEC`,缺省 `0` = 不周期跑)。
201
216
  * 🔴 **本 build 里任何正值都拒启**:结构上没有周期腿(没有可用的真互斥基建 —— 全文与偏离登记见
@@ -225,7 +240,8 @@ export interface MemoryConsolidationDriverConfig {
225
240
  * · `true`(`=on`)= 武装。**武装是一条出口决定,不是性能旋钮**:每个建议任务之后会有一次整库整理跑,
226
241
  * 记忆内容自动外流到配置的整理模型 —— 设它之前请读 `boot/memory-consolidation.ts` 的 EGRESS 段。
227
242
  *
228
- * ⚠️ **前置 = `MEMORY_CONSOLIDATION_DRIVER=on`**(且记忆引擎接了线)。阀门开着 ⇒ 引擎那半场的整理两席
243
+ * ⚠️ **前置 = 整理阀门开着**(`MEMORY_CONSOLIDATION_DRIVER=on`,或未设而 [ref] 出厂缺省派生成开 —— 见 {@link valve};
244
+ * 且记忆引擎接了线)。阀门开着 ⇒ 引擎那半场的整理两席
229
245
  * (`RunnerDeps.memoryConsolidation` + `memoryConsolidationDriver`)由**同一只已解析座**接上
230
246
  * (S-399,`boot/runner-deps.ts` 的 `ctx.consolidationSeat` 那一段)⇒ 本旋钮真的生效;阀门关着 ⇒ 两席
231
247
  * 一个键都不写,而 core 对「武装但没接整理席」的处置是**每一次 prepare 都抛** `config.auto_consolidation`
@@ -761,12 +777,12 @@ export interface ServiceConfigFlat {
761
777
  /**
762
778
  * [ref](core 5.58 [ref] 片②)—— 记忆 **consolidation driver 阀门**的三根旋钮。
763
779
  * **恒在场**(不是可选块,与 {@link retentionSweep} 同姿态):三根都有文档化的缺省,而缺省本身
764
- * 是一条要能被读出来的姿态 —— `{enabled:false, intervalSec:0, scopes:[]}` = 「阀门关着」。
780
+ * 是一条要能被读出来的姿态 —— `{intervalSec:0, scopes:[]}`(`valve` 键缺席)= 「阀门没表态,出厂缺省在 boot 期派生」。
765
781
  *
766
782
  * 三问(设计稿 §消长账):谁需要 = 记忆库只涨不折的 TOB 部署(检索质量随库龄衰减);谁被伤 =
767
783
  * 误开 + 错 role 的部署(一轮 consolidation 读全库 ~1e5 prompt tokens,静默升级到最贵 default
768
- * 就是把一次维护跑成一笔意外账单);补偿 = 阀门默认关 + 座解析失败拒启 + D3 投影把每轮花了什么
769
- * 摆出来 + v1 只有**手动**阀门(不信任周期腿之前可人工控节奏)。
784
+ * 就是把一次维护跑成一笔意外账单);补偿 = 出厂缺省只在整理座**不落主模型**时开([ref];落主模型 ⇒ 不开 +
785
+ * 起服提示)+ 座解析失败拒启 + D3 投影把每轮花了什么摆出来 + v1 只有**手动**阀门(不信任周期腿之前可人工控节奏)。
770
786
  *
771
787
  * 语义与解析见 {@link MemoryConsolidationDriverConfig} 与 config.ts 的 `parseMemoryDomain`。
772
788
  */
@@ -1051,11 +1067,12 @@ export interface ServiceConfigFlat {
1051
1067
  fileHistory?: {
1052
1068
  retention?: FileHistoryRetentionPolicy;
1053
1069
  };
1054
- /** S-589:文件历史**捕获**总开关(`FILE_HISTORY_CAPTURE`,布尔词表,缺省 true,未知词拒启)。三条车道
1055
- * (local / mysql / pg)同一语义:`false` ⇒ 装配点不把历史店交给 core(`RunnerDeps.fileHistoryStore` 缺席 ⇒
1056
- * 首次触碰捕获结构上不发生),同时没有 rewind / restore / `rewindFilesTo` / 2c session-sync(那几位能力位与
1057
- * 能力词 `fileHistoryCapture:"off"` 同源翻关)。**只管写入,不管擦除**:已捕获的历史照旧随会话删除 / 留存
1058
- * 规则清理,翻旋钮本身不做批量删除。判定的唯一属主是 `file-history-capture.ts` 的 `assembleFileHistory`。
1070
+ /** S-589 / S-724:文件历史**捕获**总开关(`FILE_HISTORY_CAPTURE`,布尔词表,缺省 true,未知词拒启)。三条车道
1071
+ * (local / mysql / pg)同一语义:`false` ⇒ 装配点照接历史店,但给 core 的部署捕获位 `RunnerDeps.fileHistoryCapture: "off"`
1072
+ * (首次触碰备份 / 回合边界 / fork 携带结构上不发生;能力词 `fileHistoryCapture:"off"` 同一只判定派生)。**只关捕获**:
1073
+ * 已存历史照常服务 restore / rewind / 2c session-sync(S-589 时旋钮关 = 摘店,连已存历史也读不了;S-724 改落 core 的捕获位)。
1074
+ * **不管擦除**:已捕获的历史照旧随会话删除 / 留存规则清理,翻旋钮本身不做批量删除。没有店的部署形不下发捕获位
1075
+ * (core 对「off 但没店」拒 prepare,而没店本来就不捕获)。判定的唯一属主是 `file-history-capture.ts`。
1059
1076
  * 刻意**不**并进上面的 `fileHistory` 键:那一格是 core 店构造选项的同域键形,原样透传给 core。 */
1060
1077
  fileHistoryCapture: boolean;
1061
1078
  /** plugins 域 clone host 白名单(`PLUGINS_ALLOW_HOSTS`,缺省 github.com;契约 [ref]① 双保险面,
@@ -1526,8 +1543,11 @@ export interface ServiceConfigFlat {
1526
1543
  selectEnvironmentTool: boolean;
1527
1544
  /** RFC A1: inject structured sandbox envFacts into the task (`TaskSpec.envFacts` — core 1.240.0 whitelist
1528
1545
  * field; rendered as the `# Environment` block, sanitized+bounded core-side). Default ON —
1529
- * facts only ride when the deployment actually knows them (image binding/pkgSource/egress), absent = block
1530
- * unchanged. `SANDBOX_ENV_FACTS=false` opts out. */
1546
+ * facts only ride when the deployment actually knows them (image binding/pkgSource/egress/resume facts), absent =
1547
+ * block unchanged. `SANDBOX_ENV_FACTS=false` opts out of the SANDBOX facts only.
1548
+ * 🔴 S-842:the session scratchpad declaration (`envFacts.scratchpadDir`) is NOT under this knob — it is the write
1549
+ * surface's declaration (core opens the root-fence hole for it and teaches it by the same verdict), not a display
1550
+ * fact; gating it would close the scratchpad together with the display. */
1531
1551
  envFactsEnabled: boolean;
1532
1552
  /** ③ sensitive-path write DENY set (core 1.295 `createSensitivePathPolicy`). The PATTERN SET is core's call
1533
1553
  * (RECOMMENDED_SENSITIVE_PATTERNS — .env/.ssh/keys/.git hooks+config/cloud creds/histories, rationale
@@ -2054,8 +2074,9 @@ export interface ServiceConfigFlat {
2054
2074
  centerCredentialRenewLeadMs: number;
2055
2075
  /** Dual-mode config SOURCE (DUAL-MODE-DESIGN §4): "remote" (sema-registry HTTP) | "local" (.env + config.d
2056
2076
  * via @sema-agent/settings-schema FileConfigStore) | unset → remote if SEMA_REGISTRY_URL (legacy CONFIG_CENTER_URL now boot-rejects, no dual-read) is set, else pure-env.
2057
- * Env: CONFIG_PROVIDER. The same resolver/EffectiveConfig either way (can't drift). */
2058
- configProvider?: string;
2077
+ * Env: CONFIG_PROVIDER —— S-841 起闭集 {@link CONFIG_PROVIDER_WORDS}(认不出的词拒启;空串 = 未设)。
2078
+ * The same resolver/EffectiveConfig either way (can't drift). */
2079
+ configProvider?: ConfigProviderWord;
2059
2080
  /** Root dir holding `config.d/<domain>.json` for the local config source. Env: CONFIG_LOCAL_DIR. */
2060
2081
  configLocalDir?: string;
2061
2082
  }
@@ -1,6 +1,7 @@
1
1
  import { DRAIN_GRACE_FLOOR_MS } from "@sema-agent/settings-schema/fleet";
2
2
  export const TASK_WRITE_FACE_ESCALATIONS = ["allow", "refuse"];
3
3
  export const RETENTION_MODES = ["audit-only", "enforce"];
4
+ export const CONFIG_PROVIDER_WORDS = ["local", "remote"];
4
5
  export const MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC = 300;
5
6
  export const MAX_TIMER_DELAY_MS = 2_147_483_647;
6
7
  export const AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS = { minMs: 1_000, maxMs: MAX_TIMER_DELAY_MS };
package/dist/config.d.ts CHANGED
@@ -75,8 +75,9 @@ export declare function parseNumOrFailNonNegative(name: string, raw: string | un
75
75
  * 采用值也被报成夹取、而钥集条目**被丢弃**(没有任何采用值、也不该印 raw)却被报成「坏值回默认」。
76
76
  * 告警规则是按事件名分流的,报错的种类就是报错的事实本身;可选字段的在场性是实现细节,不该被当契约用。
77
77
  *
78
- * `invalid-default` 是**缺省种类**(不写 `kind` 即是它)—— 历史形 `{env,raw}` 的三个存量生产者
79
- * (`optFinitePositiveEnv` / `boolEnv` / `boolEnvWithLegacyNegated`)因此逐字不动,它们的断言也不破。
78
+ * `invalid-default` 是**缺省种类**(不写 `kind` 即是它)—— 历史形 `{env,raw}` 的存量生产者(`optFinitePositiveEnv`)
79
+ * 因此逐字不动,它的断言也不破。(布尔族 `boolEnv` 自 [ref]-rc2 起未知词在 `parseBoolWord` 里拒启,不再往这里 push;
80
+ * S-588 起旧负名走墓碑表 —— 两者都不是本种类的生产者。)
80
81
  */
81
82
  type ConfigWarningKind = "invalid-default" | "clamped" | "empty-preserved" | "entry-rejected" | "set-rejected" | "ineffective";
82
83
  interface ConfigWarning {
@@ -169,10 +170,10 @@ export declare function buildUnpricedCeilingNotice(models: Record<string, Model>
169
170
  * stated, so it was re-invented per line and the suffix stopped predicting anything.
170
171
  *
171
172
  * Semantics are the two dialects' semantics VERBATIM — an opt-in knob is `boolEnv(x, false)`, an opt-out knob is
172
- * `boolEnv(x, true)` — so this migration is value-preserving on every input. The one addition: a value that is
173
- * neither literal (`1`, `yes`, `TRUE`) still lands on the knob's default (as before) but now raises a config
174
- * warning instead of being swallowed, which is the whole failure mode operators kept hitting (`LSP_ENABLED=1`
175
- * reads as "on" to a human and as "off" to `=== "true"`).
173
+ * `boolEnv(x, true)` — for the two literals and for unset. Anything else goes through `parseBoolWord` ([ref]-rc2,
174
+ * [ref]): `1`/`0`/`yes`/`no`/`on`/`off` in any case are read as the operator meant them (`LSP_ENABLED=1` reads as
175
+ * "on" to a human, and now to the parser too), and an unknown word REFUSES TO BOOT — it no longer lands on the
176
+ * default with a warning (that was this helper's first form, retired with [ref]-rc2; S-588 corrected this note).
176
177
  *
177
178
  * Note the deliberate non-migrations: the posture family (`postureOn`) keeps its tri-state — its unset case is
178
179
  * neither `true` nor `false` but "single-user ∧ infra-ready" — and the sentinel-valued knobs (`MEMORY_ENGINE`,
@@ -183,7 +184,7 @@ export declare function buildUnpricedCeilingNotice(models: Record<string, Model>
183
184
  * 用",而实际设了就拒启——诊断面撒的谎比没有诊断更贵(它会让人先去试那个必然拒启的开关)。 */
184
185
  type ConfigKnobSource = "env" | "posture" | "default";
185
186
  export interface ConfigKnobRecord {
186
- /** The env name. 3.0.0 起只有这一个名字 —— 负名别名已是墓碑(见 `boolEnvWithLegacyNegated`)。 */
187
+ /** The env name. 3.0.0 起只有这一个名字 —— 负名别名已是墓碑(`RETIRED_ENVS` 的 replaced 行,S-588 起在场即拒)。 */
187
188
  name: string;
188
189
  /** `opt-in` = default OFF, `opt-out` = default ON, `posture` = default derived from REQUIRE_PRINCIPAL ∧ infra. */
189
190
  polarity: "opt-in" | "opt-out" | "posture";
@@ -192,28 +193,12 @@ export interface ConfigKnobRecord {
192
193
  }
193
194
  /** The registered boolean knobs, sorted by name — the data behind `logConfigDiagnostics`'s polarity table. */
194
195
  export declare function configKnobTable(): ConfigKnobRecord[];
195
- /** [ref] N8 (D-family) + server 3.0.0 墓碑 —— a knob whose CANONICAL name is positive (`X_ENABLED`,
196
- * default ON) and whose retired negative name (`X_DISABLED`) is now a **fail-loud tombstone**.
197
- *
198
- * Why the flip at all: `X_DISABLED !== "true"` is a double negative at the read site, so the four knobs that
199
- * used it (`PROJECT_MEMORY`, `CONFIG_LKG`, `HOST_BG`, `HOST_EXEC_SPOOL`) could not be read for their default
200
- * without inverting twice in your head, and `projectMemoryDisabled` was the ONE negative boolean on a
201
- * `ServiceConfig` that otherwise carries 16 positive `*Enabled`/`*Allowed` fields.
202
- *
203
- * 🔴 3.0.0([ref],clay 令「不做兼容」):旧名不再有兼容窗——**设了就拒启**。这个函数保留 legacy 形参
204
- * 不是为了继续读它,而正是为了**认出它在场并拒绝启动**(把墓碑放在唯一知道这对名字关系的地方)。
205
- *
206
- * Resolution order:
207
- * 1. `X_ENABLED` set to a literal ⇒ that.
208
- * 2. `X_ENABLED` set to a non-literal ⇒ default + warning (the tombstone is NOT consulted — a typo in the
209
- * new name must not turn into a boot refusal about an env the operator never set).
210
- * 3. `X_ENABLED` unset ∧ `X_DISABLED` set(**任何值**,含 `"false"`)⇒ throw,文案指路新名。选拒启不选
211
- * 静默忽略:静默会让升级部署的旧开关名义在、实际归默认——比拒启危险,且下游冒烟测试测不出来。
212
- * 4. neither ⇒ default. */
213
- export declare function boolEnvWithLegacyNegated(name: string, legacyNegatedName: string, def: boolean): boolean;
214
- /** LKG (last-known-good) persistence of the remote effective-config fetch. Read LAZILY at both main.ts sites
196
+ /** [ref] B4 ②:D 族四枚(`PROJECT_MEMORY` / `CONFIG_LKG` / `HOST_BG` / `HOST_EXEC_SPOOL`)的正名 —— 缺省开,读点只读正名。
197
+ * 3.0.0 之前它们叫 `X_DISABLED`(读点上的双重否定);那四个旧负名是墓碑,在 `RETIRED_ENVS` 里(S-588 起在场即拒启,不论正名在不在)。
198
+ *
199
+ * LKG (last-known-good) persistence of the remote effective-config fetch. Read LAZILY at both main.ts sites
215
200
  * rather than lifted onto `ServiceConfig`, so this stays a pure env knob; `loadConfig` pre-registers it (below)
216
- * so its polarity row and any deprecation notice appear at boot with everything else. */
201
+ * so its polarity row appears at boot with everything else. */
217
202
  export declare const configLkgEnabled: () => boolean;
218
203
  /** The `host` exec lane's background-shell capability kill switch (read per capability probe in remote-env-host.ts). */
219
204
  export declare const hostBackgroundShellEnabled: () => boolean;
package/dist/config.js CHANGED
@@ -10,7 +10,7 @@ import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
10
10
  import { isV2ScopeKey, memoryReplicationCoverage } from "./memory-scope.js";
11
11
  import { DEFAULT_QUESTION_THROTTLE } from "./question.js";
12
12
  import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js";
13
- import { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS, MAX_TIMER_DELAY_MS, AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, DRAIN_GRACE_FLOOR_MS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS } from "./config-types.js";
13
+ import { CONFIG_PROVIDER_WORDS, RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS, MAX_TIMER_DELAY_MS, AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, DRAIN_GRACE_FLOOR_MS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS } from "./config-types.js";
14
14
  import { buildWriteProtectionPosture } from "./write-protection.js";
15
15
  import { normalizeSshFingerprint } from "./ssh-host-key.js";
16
16
  import { readFaceForPosture } from "./read-face-posture.js";
@@ -130,6 +130,10 @@ const RETIRED_ENVS = [
130
130
  "widened retry; 0 = no engine clock on that segment and the transport's header / body clocks off too — the TCP + TLS connect " +
131
131
  "keeps the transport's own clock)",
132
132
  ],
133
+ ["PROJECT_MEMORY_DISABLED", "PROJECT_MEMORY_ENABLED", "replaced", "negative-name alias retired in server 3.0.0 — unset it; the polarity is inverted: PROJECT_MEMORY_DISABLED=true is PROJECT_MEMORY_ENABLED=false (the positive name defaults on)"],
134
+ ["CONFIG_LKG_DISABLED", "CONFIG_LKG_ENABLED", "replaced", "negative-name alias retired in server 3.0.0 — unset it; the polarity is inverted: CONFIG_LKG_DISABLED=true is CONFIG_LKG_ENABLED=false (the positive name defaults on)"],
135
+ ["HOST_BG_DISABLED", "HOST_BG_ENABLED", "replaced", "negative-name alias retired in server 3.0.0 — unset it; the polarity is inverted: HOST_BG_DISABLED=true is HOST_BG_ENABLED=false (the positive name defaults on)"],
136
+ ["HOST_EXEC_SPOOL_DISABLED", "HOST_EXEC_SPOOL_ENABLED", "replaced", "negative-name alias retired in server 3.0.0 — unset it; the polarity is inverted: HOST_EXEC_SPOOL_DISABLED=true is HOST_EXEC_SPOOL_ENABLED=false (the positive name defaults on)"],
133
137
  ];
134
138
  const RETIRED_ENV_SENTENCE = {
135
139
  renamed: (hits) => `${hits.map(([old, to]) => `${old} → use ${to}`).join("; ")} — rename the env key(s); the value semantics are unchanged`,
@@ -147,7 +151,7 @@ function rejectRetiredEnvs() {
147
151
  if (hit.length === 0)
148
152
  return;
149
153
  const parts = Object.keys(RETIRED_ENV_SENTENCE).flatMap((kind) => retiredEnvSentence(kind, hit));
150
- throw new Error(`retired env name(s) set [${RETIRED_ENV_REFUSAL_CODE}] ([2354] 兼容面全清,5.0.0 起拒启): ${parts.join(" | ")}.`);
154
+ throw new Error(`retired env name(s) set [${RETIRED_ENV_REFUSAL_CODE}] — refused while present, whether or not the replacement is also set: ${parts.join(" | ")}.`);
151
155
  }
152
156
  export function numEnv(name, fallback) {
153
157
  return parseNumOrFail(name, env(name, fallback));
@@ -201,13 +205,24 @@ export function drainConfigNotices() {
201
205
  return CONFIG_NOTICES.splice(0, CONFIG_NOTICES.length);
202
206
  }
203
207
  let EXEC_LANE_NOTICE;
204
- function buildExecLaneNotice(remoteExec) {
208
+ function buildExecLaneNotice(remoteExec, configProvider) {
205
209
  const explicit = process.env.REMOTE_EXEC !== undefined && process.env.REMOTE_EXEC !== "";
206
210
  return {
207
211
  lane: executionLaneOf(remoteExec?.provider),
208
- source: explicit ? "explicit-env" : process.env.CONFIG_PROVIDER === "local" ? "default-local-provider" : "default-unset",
212
+ source: explicit ? "explicit-env" : configProvider === "local" ? "default-local-provider" : "default-unset",
209
213
  };
210
214
  }
215
+ function parseConfigProvider() {
216
+ const raw = process.env.CONFIG_PROVIDER;
217
+ if (raw === undefined || raw === "")
218
+ return undefined;
219
+ const word = CONFIG_PROVIDER_WORDS.find((w) => w === raw);
220
+ if (word === undefined) {
221
+ throw new Error(`env CONFIG_PROVIDER="${raw.slice(0, 120)}" must be one of: ${CONFIG_PROVIDER_WORDS.join(", ")} — or leave it unset (then SEMA_REGISTRY_URL, if set, selects the center as the config source; otherwise this worker is env-only). ` +
222
+ `The word is matched exactly (no trimming, no case folding). Refusing to boot rather than guessing: the config source, the execution-lane default and the local config root all key on this one word.`);
223
+ }
224
+ return word;
225
+ }
211
226
  export function logConfigDiagnostics(logger) {
212
227
  for (const w of drainConfigWarnings()) {
213
228
  const kind = w.kind ?? "invalid-default";
@@ -433,30 +448,9 @@ function boolEnv(name, def) {
433
448
  registerKnob({ name, polarity: def ? "opt-out" : "opt-in", value, source });
434
449
  return value;
435
450
  }
436
- export function boolEnvWithLegacyNegated(name, legacyNegatedName, def) {
437
- const raw = process.env[name];
438
- const legacy = process.env[legacyNegatedName];
439
- let value;
440
- let source;
441
- const parsedCanonical = parseBoolWord(name, raw);
442
- if (parsedCanonical !== undefined) {
443
- value = parsedCanonical;
444
- source = "env";
445
- }
446
- else if (legacy !== undefined && legacy !== "") {
447
- throw new Error(`${legacyNegatedName} was removed in server 3.0.0 — set ${name}=${legacy !== "true"} instead ` +
448
- `(positive-name knob, default ${def ? "on" : "off"}; migration table in USAGE.md)`);
449
- }
450
- else {
451
- value = def;
452
- source = "default";
453
- }
454
- registerKnob({ name, polarity: def ? "opt-out" : "opt-in", value, source });
455
- return value;
456
- }
457
- export const configLkgEnabled = () => boolEnvWithLegacyNegated("CONFIG_LKG_ENABLED", "CONFIG_LKG_DISABLED", true);
458
- export const hostBackgroundShellEnabled = () => boolEnvWithLegacyNegated("HOST_BG_ENABLED", "HOST_BG_DISABLED", true);
459
- export const hostExecSpoolEnabled = () => boolEnvWithLegacyNegated("HOST_EXEC_SPOOL_ENABLED", "HOST_EXEC_SPOOL_DISABLED", true);
451
+ export const configLkgEnabled = () => boolEnv("CONFIG_LKG_ENABLED", true);
452
+ export const hostBackgroundShellEnabled = () => boolEnv("HOST_BG_ENABLED", true);
453
+ export const hostExecSpoolEnabled = () => boolEnv("HOST_EXEC_SPOOL_ENABLED", true);
460
454
  export const REQUIRED_ENVS_BY_LANE = {
461
455
  e2b: ["E2B_API_KEY"],
462
456
  k8s: ["K8S_SANDBOX_IMAGE"],
@@ -977,7 +971,22 @@ function parseModelDomain() {
977
971
  fields: { hint: "MODEL_CHEAP_ID is set but MODEL_ID is not — the env cheap slot derives from the env main model, so it is not minted; the catalog (config.d/models.json or the config center) serves the roles. Set MODEL_ID to use the env slots, or unset MODEL_CHEAP_ID to silence this line." },
978
972
  });
979
973
  }
980
- const cheapModel = cheapId && modelId !== ""
974
+ const cheapIsMain = cheapId !== undefined && cheapId !== "" && modelId !== "" && cheapId === modelId;
975
+ if (cheapIsMain) {
976
+ CONFIG_NOTICES.push({
977
+ event: "config_env_entry_cheap_same_as_main",
978
+ fields: {
979
+ env: "MODEL_CHEAP_ID",
980
+ model: modelId,
981
+ hint: `MODEL_CHEAP_ID names the same model as MODEL_ID ("${modelId}") — the env cheap slot is the main gateway under a different id, ` +
982
+ `so the same id IS the main model: no cheap slot is minted and subagent / summarize stay on the main model (roles -> default); ` +
983
+ `any other MODEL_CHEAP_* key set here describes that slot and is not applied. Set MODEL_CHEAP_ID to a different (cheaper) model ` +
984
+ `id to run those roles cheap, or declare two differently-named entries in config.d/models.json to give one upstream id a second ` +
985
+ `shape (price / reasoning / window); unset MODEL_CHEAP_ID to silence this line.`,
986
+ },
987
+ });
988
+ }
989
+ const cheapModel = cheapId && modelId !== "" && !cheapIsMain
981
990
  ? {
982
991
  ...model,
983
992
  id: cheapId,
@@ -1526,7 +1535,7 @@ function parseMemoryConsolidationDriver() {
1526
1535
  `who believes the valve is armed gets a 404 rather than a diagnosis). This knob is new in server 7.46 and has no legacy ` +
1527
1536
  `spellings to preserve, unlike MEMORY_ENGINE's two-family word table.`);
1528
1537
  }
1529
- const enabled = word === "on";
1538
+ const valve = word === "on" || word === "off" ? word : undefined;
1530
1539
  const autoWord = requireNonBlank("MEMORY_AUTO_CONSOLIDATION", process.env.MEMORY_AUTO_CONSOLIDATION);
1531
1540
  if (autoWord !== undefined && autoWord !== "on" && autoWord !== "off") {
1532
1541
  throw new Error(`MEMORY_AUTO_CONSOLIDATION must be exactly "on" or "off", got ${JSON.stringify(autoWord)} — this knob decides whether memory content ` +
@@ -1558,12 +1567,12 @@ function parseMemoryConsolidationDriver() {
1558
1567
  }
1559
1568
  const scopesRaw = requireNonBlank("MEMORY_CONSOLIDATION_SCOPES", process.env.MEMORY_CONSOLIDATION_SCOPES);
1560
1569
  const scopes = scopesRaw !== undefined ? csv("MEMORY_CONSOLIDATION_SCOPES") : [];
1561
- if (!enabled && (intervalSec > 0 || scopes.length > 0)) {
1562
- throw new Error(`MEMORY_CONSOLIDATION_INTERVAL_SEC / MEMORY_CONSOLIDATION_SCOPES are configured but MEMORY_CONSOLIDATION_DRIVER is not "on" — ` +
1563
- `the consolidation valve is OFF, so neither knob reaches anything (the two admin endpoints are not even mounted). ` +
1564
- `Set MEMORY_CONSOLIDATION_DRIVER=on, or unset the other two.`);
1565
- }
1566
- return { enabled, intervalSec, scopes, ...(autoRunOnRecommendation !== undefined ? { autoRunOnRecommendation } : {}) };
1570
+ return {
1571
+ ...(valve !== undefined ? { valve } : {}),
1572
+ intervalSec,
1573
+ scopes,
1574
+ ...(autoRunOnRecommendation !== undefined ? { autoRunOnRecommendation } : {}),
1575
+ };
1567
1576
  }
1568
1577
  function parseMemoryDomain(ctx) {
1569
1578
  const { requirePrincipal } = ctx;
@@ -1615,7 +1624,7 @@ function parseMemoryDomain(ctx) {
1615
1624
  },
1616
1625
  });
1617
1626
  }
1618
- const projectMemoryEnabled = boolEnvWithLegacyNegated("PROJECT_MEMORY_ENABLED", "PROJECT_MEMORY_DISABLED", true);
1627
+ const projectMemoryEnabled = boolEnv("PROJECT_MEMORY_ENABLED", true);
1619
1628
  const memoryOrgAdmissionMode = enumEnv("MEMORY_ORG_ADMISSION_MODE", "enforce", ["audit", "enforce"]);
1620
1629
  const memoryOrgDirectoryJson = process.env.MEMORY_ORG_DIRECTORY_JSON || undefined;
1621
1630
  const memoryOrgGrantTtlMs = numEnvBounded("MEMORY_ORG_GRANT_TTL_MS", "60000", 1000, 3_600_000);
@@ -1910,7 +1919,7 @@ function parseOrchestrationDomain(ctx) {
1910
1919
  ...(process.env.DEVICE_SUPERSEDE ? { supersede: enumEnv("DEVICE_SUPERSEDE", "stale_only", ["stale_only", "deny"]) } : {}),
1911
1920
  }
1912
1921
  :
1913
- process.env.CONFIG_PROVIDER === "local"
1922
+ ctx.configProvider === "local"
1914
1923
  ? { provider: "host" }
1915
1924
  : undefined,
1916
1925
  worktreeIsolation: boolEnv("WORKTREE_ISOLATION_ENABLED", false) && process.env.WORKTREE_REPO_ROOT
@@ -2081,7 +2090,7 @@ function parseGitApiKind(raw, baseUrl) {
2081
2090
  return kind;
2082
2091
  }
2083
2092
  function parseIntegrationsDomain(ctx) {
2084
- const { requirePrincipal } = ctx;
2093
+ const { requirePrincipal, configProvider } = ctx;
2085
2094
  return {
2086
2095
  pluginsAllowHosts: (process.env.PLUGINS_ALLOW_HOSTS ?? "github.com").split(",").map((h) => h.trim()).filter(Boolean),
2087
2096
  configBootFetchBudgetMs: clampEnvWithWarn("CONFIG_BOOT_FETCH_BUDGET_MS", "1500", 0),
@@ -2092,7 +2101,7 @@ function parseIntegrationsDomain(ctx) {
2092
2101
  skillsDir: env("SKILLS_DIR", defaultSkillsDir()),
2093
2102
  centerCredentialRenewLeadMs: parseCenterCredentialRenewLeadMs(process.env.CENTER_CREDENTIAL_RENEW_LEAD_MS),
2094
2103
  configCenter: process.env.SEMA_REGISTRY_URL ? parseConfigCenter(env("SEMA_REGISTRY_URL"), requirePrincipal) : undefined,
2095
- configProvider: process.env.CONFIG_PROVIDER || undefined,
2104
+ ...(configProvider !== undefined ? { configProvider } : {}),
2096
2105
  configLocalDir: process.env.CONFIG_LOCAL_DIR || undefined,
2097
2106
  a2aServe: parseA2aServe(),
2098
2107
  };
@@ -2281,6 +2290,7 @@ export function loadConfig() {
2281
2290
  CONFIG_KNOBS.clear();
2282
2291
  const requirePrincipal = boolEnv("REQUIRE_PRINCIPAL", false);
2283
2292
  const singleUserTurnkey = !requirePrincipal;
2293
+ const configProvider = parseConfigProvider();
2284
2294
  const postureOn = (name, infraReady = true) => {
2285
2295
  const explicit = parseBoolWord(name, process.env[name]);
2286
2296
  const value = explicit ?? (singleUserTurnkey && infraReady);
@@ -2293,10 +2303,10 @@ export function loadConfig() {
2293
2303
  const approval = parseApprovalDomain({ postureOn, singleUserTurnkey, infraReady: durableStoreReady });
2294
2304
  const memory = parseMemoryDomain({ requirePrincipal });
2295
2305
  const auth = parseAuthDomain({ requirePrincipal });
2296
- const orchestration = parseOrchestrationDomain({ postureOn, singleUserTurnkey });
2306
+ const orchestration = parseOrchestrationDomain({ postureOn, singleUserTurnkey, configProvider });
2297
2307
  const limitsHttp = parseLimitsHttpDomain();
2298
2308
  const observability = parseObservabilityDomain();
2299
- const integrations = parseIntegrationsDomain({ requirePrincipal });
2309
+ const integrations = parseIntegrationsDomain({ requirePrincipal, configProvider });
2300
2310
  const { directApprovalDoor, durableApproval: dgDurable, approvalHmacKeys: dgHmacKeys } = approval;
2301
2311
  const { principalJwtPubkeys, principalJwtIss, principalJwtAud, operatorPrincipals: dgOperators, authTokens } = auth;
2302
2312
  const { enabled: imageBakesEnabled, runnerPrincipal: bakeRunnerPrincipal } = orchestration.imageBakes;
@@ -2330,14 +2340,6 @@ export function loadConfig() {
2330
2340
  if (imageBakesEnabled && !Object.values(authTokens).includes(bakeRunnerPrincipal)) {
2331
2341
  throw new Error(`IMAGE_BAKES_ENABLED=true but no SERVICE_AUTH_TOKENS entry maps to the runner principal '${bakeRunnerPrincipal}' — the bake-runner authenticates by its credential (token-derived source), so a '<BAKE_RUNNER_TOKEN>=${bakeRunnerPrincipal}' SERVICE_AUTH_TOKENS entry is required or claim/ingest/heartbeat all 403 (IMAGE-API-DESIGN.md §P2.12 boot invariant)`);
2332
2342
  }
2333
- if (orchestration.leaderEnabled && orchestration.remoteExec?.provider === "host") {
2334
- throw new Error("LEADER_ENABLED=true + REMOTE_EXEC=host is refused: the leader wire pins ONE absolute workspace path " +
2335
- "(LEADER_WORKSPACE / the /home/user default) for every sub-worker, while the host adapter gives each env a " +
2336
- "DIFFERENT random directory — so N concurrent host workers would share one physical git worktree and " +
2337
- "corrupt each other's state SILENTLY (per-worker workspace is not wired on this lane yet). Run the leader " +
2338
- "on an isolated lane (REMOTE_EXEC=e2b or k8s), or turn the leader off (unset LEADER_ENABLED) " +
2339
- "(#193 车4 件2 / staleness ledger P1-10 boot invariant)");
2340
- }
2341
2343
  if (orchestration.leaderEnabled && store.dbBackend !== "mysql" && store.dbBackend !== "pg") {
2342
2344
  throw new Error(`LEADER_ENABLED=true but DB_BACKEND=${store.dbBackend} (no external SQL store) is refused: a leader run is an ` +
2343
2345
  "N-worker fan-out lasting up to 24h that ends in a REAL git push, and without a durable store its registry is " +
@@ -2345,25 +2347,6 @@ export function loadConfig() {
2345
2347
  "push. Point the service at a durable store (DB_BACKEND=mysql|pg), or turn the leader off (unset LEADER_ENABLED) " +
2346
2348
  "(#193 车4 件2 / staleness ledger P1-9+P1-10 boot invariant)");
2347
2349
  }
2348
- {
2349
- const deviceLane = orchestration.remoteExec?.provider === "device";
2350
- const deviceSqlStore = (store.dbBackend === "mysql" && store.tidb !== undefined) || (store.dbBackend === "pg" && store.pg !== undefined);
2351
- if (deviceLane && !deviceSqlStore) {
2352
- throw new Error(`REMOTE_EXEC=device with DB_BACKEND=${store.dbBackend} and no SQL connection coordinates built is refused ` +
2353
- "[device-durable-store]: the device registry, the session↔device binding and the device audit trail are all " +
2354
- "SQL-backed — without a real SQL store the binding lives in this process only, so a restart forgets WHICH device " +
2355
- "a parked run must resume onto and a second replica cannot see it at all. Note DB_BACKEND alone is not enough: " +
2356
- "SESSION_BACKEND=memory (or auto with no MYSQL_HOST/PG_HOST) builds no coordinates at all. Configure a durable " +
2357
- "store (DB_BACKEND=mysql|pg + its *_HOST/*_USER/*_DATABASE), or unset REMOTE_EXEC " +
2358
- "(device-executor-lane-v2 §4.7 boot invariant)");
2359
- }
2360
- if (deviceLane && !dgDurable) {
2361
- throw new Error("REMOTE_EXEC=device without DURABLE_APPROVAL=true is refused [device-durable-approval]: a personal device goes " +
2362
- "offline (lid closed, network drop) at arbitrary points, so an in-flight approval that outlives the window MUST " +
2363
- "have a park exit — without the durable approval store there is none and the run is stuck with nowhere to persist. " +
2364
- "Set DURABLE_APPROVAL=true, or unset REMOTE_EXEC (device-executor-lane-v2 §4.7 boot invariant)");
2365
- }
2366
- }
2367
2350
  if (orchestration.requireExecutionLane && orchestration.remoteExec === undefined) {
2368
2351
  throw new Error(`REQUIRE_EXECUTION_LANE=true but NO execution lane is named on this deployment — refusing to start. ` +
2369
2352
  `With no lane the engine falls back to its in-process stub environment, where at least the Bash tool is ` +
@@ -2387,7 +2370,7 @@ export function loadConfig() {
2387
2370
  throw new Error(refusal);
2388
2371
  }
2389
2372
  }
2390
- EXEC_LANE_NOTICE = buildExecLaneNotice(orchestration.remoteExec);
2373
+ EXEC_LANE_NOTICE = buildExecLaneNotice(orchestration.remoteExec, configProvider);
2391
2374
  return attachConfigGroups({
2392
2375
  ...store,
2393
2376
  ...modelPlane,
@@ -30,6 +30,14 @@ export interface SandboxEnvFacts {
30
30
  note?: string;
31
31
  };
32
32
  }
33
+ /**
34
+ * 一条 scratchpad 路径能不能被**原样**宣告(core 的渲染上限 {@link MAX_PATH} 之内)—— scratchpad 的**铸造条件**之一。
35
+ * 两个铸点(server 自算 {@link ensureScratchpadDir}、壳供 {@link screenShellScratchpadDir})读这同一只:渲染不下的路径
36
+ * **根本不铸**(不建目录、不进写门豁免、不宣告 —— 宣告是根围栏那个洞的唯一写者,S-842,于是也不开洞)。
37
+ * 🔴 S-842 codex r1 [medium]:删 [ref] 折叠之后,只在宣告那一步丢长路径会留下「建了、豁免了、却不宣告也不开洞」的目录;
38
+ * 规则挪到铸点,四件同有同无。(远程车道的 `/tmp/scratchpad/<sessionId>` 恒在上限内:sessionId ≤ 64,见 prepareSpec。)
39
+ */
40
+ export declare function scratchpadPathRenderable(path: string): boolean;
33
41
  /** Compose envFacts from what the deployment knows at spec-prep. Returns undefined when nothing is known
34
42
  * (no fact beats a vacuous one — an empty block would still spend prompt tokens). */
35
43
  export declare function buildEnvFacts(opts: {
@@ -68,12 +76,14 @@ export declare function scratchpadDirFor(localDataRoot: string, sessionId: strin
68
76
  * egress/pkgSource "don't declare what you don't know"). Lifecycle: the reaper primitives live below —
69
77
  * {@link purgeScratchpadDir}(session delete)+ {@link sweepStaleScratchpads}(mtime 周期扫),keyed by the
70
78
  * same scratchpadDirFor convention;接线(E21 DELETE purge + 周期 sweep)在 main.ts 侧(D 组)。 */
71
- export declare function ensureScratchpadDir(localDataRoot: string, sessionId: string): Promise<string | undefined>;
79
+ export declare function ensureScratchpadDir(localDataRoot: string, sessionId: string,
80
+ /** 渲染不下(铸造条件不成立)时的那一行 —— 调用方交日志口(S-842 codex r1:不铸必须说出来,否则「本会话没有 scratchpad」无从排查)。 */
81
+ warn: (msg: string, meta?: object) => void): Promise<string | undefined>;
72
82
  /** G3([ref] 壳侧承诺③/[ref]③,[ref]§四接单):**壳供** scratchpad 路径的验收门。
73
83
  *
74
84
  * 壳(cli)vendored getScratchpadDir 算出 per-session scratchpad 路径随 `TaskRequest.scratchpadDir` 发来;
75
- * 接受后它**优先于** server 自算(ensureScratchpadDir),喂 main.ts 的同一单点(envFacts 事实 +
76
- * fs-write gate exemptDirs + additionalDirectories 根围栏)。「没到货引擎忽略 = 无害 additive」。
85
+ * 接受后它**优先于** server 自算(ensureScratchpadDir),喂 resolve-spec 的同一单点(envFacts 宣告 ——
86
+ * core 据它开根围栏的洞,S-842 起是那个洞的唯一写者 —— + fs-write gate exemptDirs)。「没到货引擎忽略 = 无害 additive」。
77
87
  *
78
88
  * 信任边界:这是 exemptDirs **放宽**输入,验收 fail-closed(任何拒收形回落自算,不 fault 任务):
79
89
  * - 多租户(requirePrincipal)一律拒——共享 worker 盘上租户自选豁免目录=超范围面;
package/dist/env-facts.js CHANGED
@@ -5,6 +5,9 @@ import { dirCoversPath, isValidCwd, normalizeAbsolutePathLexically } from "./tas
5
5
  const MAX_STR = 64;
6
6
  const MAX_CAPS = 16;
7
7
  const MAX_PATH = 160;
8
+ export function scratchpadPathRenderable(path) {
9
+ return path.length <= MAX_PATH;
10
+ }
8
11
  const bound = (s) => (s.length > MAX_STR ? s.slice(0, MAX_STR) : s);
9
12
  const bound2 = (s) => (s.length > MAX_PATH ? s.slice(0, MAX_PATH) : s);
10
13
  export function buildEnvFacts(opts) {
@@ -17,7 +20,7 @@ export function buildEnvFacts(opts) {
17
20
  ...(caps.length > 0 ? { capabilities: caps } : {}),
18
21
  ...(opts.pkgSource ? { pkgSource: bound(opts.pkgSource) } : {}),
19
22
  ...(opts.egress ? { egress: opts.egress } : {}),
20
- ...(opts.scratchpadDir && opts.scratchpadDir.length <= MAX_PATH ? { scratchpadDir: opts.scratchpadDir } : {}),
23
+ ...(opts.scratchpadDir && scratchpadPathRenderable(opts.scratchpadDir) ? { scratchpadDir: opts.scratchpadDir } : {}),
21
24
  ...(opts.resumeFacts && Object.keys(opts.resumeFacts).length > 0
22
25
  ? { resumeFacts: { ...opts.resumeFacts, ...(opts.resumeFacts.note ? { note: bound2(opts.resumeFacts.note) } : {}) } }
23
26
  : {}),
@@ -51,8 +54,17 @@ export function scratchpadSessionSegment(sessionId) {
51
54
  export function scratchpadDirFor(localDataRoot, sessionId) {
52
55
  return join(localDataRoot, "scratchpad", scratchpadSessionSegment(sessionId));
53
56
  }
54
- export async function ensureScratchpadDir(localDataRoot, sessionId) {
57
+ export async function ensureScratchpadDir(localDataRoot, sessionId, warn) {
55
58
  const dir = scratchpadDirFor(localDataRoot, sessionId);
59
+ if (!scratchpadPathRenderable(dir)) {
60
+ warn("scratchpad_not_minted", {
61
+ reason: "too_long",
62
+ length: dir.length,
63
+ max: MAX_PATH,
64
+ note: "the session scratchpad path is longer than the engine renders verbatim, so it is not created, not exempted and not declared (a truncated path would be a fake one) — shorten LOCAL_DATA_ROOT to get a scratchpad back",
65
+ });
66
+ return undefined;
67
+ }
56
68
  try {
57
69
  await mkdir(dir, { recursive: true });
58
70
  return dir;
@@ -89,7 +101,7 @@ export function screenShellScratchpadDir(raw, opts) {
89
101
  opts.warn("shell_scratchpad_ignored", { reason: "not_absolute" });
90
102
  return undefined;
91
103
  }
92
- if (raw.length > MAX_PATH) {
104
+ if (!scratchpadPathRenderable(raw)) {
93
105
  opts.warn("shell_scratchpad_ignored", { reason: "too_long", length: raw.length, max: MAX_PATH });
94
106
  return undefined;
95
107
  }
@@ -70,6 +70,21 @@ export interface ExecutionLaneCaps {
70
70
  * 静默继承一个没人表过态的缺席。
71
71
  */
72
72
  readonly canonicalPathAuthority: LaneCapability;
73
+ /**
74
+ * 车PI 修复轮 ②(S-775 / clay C-R91A ②,主会话裁「按表不按词」;裁定语义 = **租户不共宿主**)—— 这条车道**分不分租户**。
75
+ * · `supported` = 每个租户的命令落在各自的目标上(每任务一只沙箱:e2b / k8s / local-docker;发起者自己的设备:device,
76
+ * 归属由 deviceId↔principal 绑定验过),或根本没有手(in-process:没有可共享的执行目标);
77
+ * · `single-user-only` = 所有任务落在**同一个共享目标**上(worker 宿主:host;一台常驻 SSH 对端:ssh;一台 ADB 设备:adb)
78
+ * ⇒ 只许单用户部署用。
79
+ * 本位**没有** `unsupported` 这一档(那会读成「谁都不许用」,不是车道事实)—— 型上收窄掉,每条车道必须在两档里表态。
80
+ * 🔴 **本位即执法源**(车PI 验修轮 F8,如实登记):唯一消费点是 `config-invariants.ts` 的 `multi-tenant-shared-lane`(经
81
+ * {@link laneCapabilityHolds} 落成布尔),能力表对账格里本位的「判别式」读的就是这张表 —— 自指,只证明消费点读的是本位,不证明本位
82
+ * 的值对。本位值的**独立作者** = 部署面 `deploy/sema-up/lib/lanes.json` 的 users 列(只有 host / e2b / k8s 三行)+ 行为格
83
+ * (`test/multi-tenant-shared-lane.test.ts`:ssh / adb 的工厂给两个租户的任务铸出的 env 报同一个 `workspaceHandle().sandboxId`)。
84
+ * local-docker / device / in-process 的 `supported` 今天没有独立预言机(e2b / local-docker 的句柄要连上才有,k8s 连前恒 `(unconnected)`,
85
+ * 都不是零成本格)—— 改这几行时按适配器事实自查。
86
+ */
87
+ readonly tenantSeparation: Exclude<LaneCapability, "unsupported">;
73
88
  }
74
89
  /**
75
90
  * per-lane 能力表。**穷举 switch,禁 default 臂**(hands-lane.ts:39-46 先例)。
@@ -20,6 +20,7 @@ export function executionLaneCaps(lane) {
20
20
  hostFilePlane: YES,
21
21
  coreRemoteExecutionEnv: NO,
22
22
  canonicalPathAuthority: NO,
23
+ tenantSeparation: YES,
23
24
  };
24
25
  case "host":
25
26
  return {
@@ -37,6 +38,7 @@ export function executionLaneCaps(lane) {
37
38
  hostFilePlane: YES,
38
39
  coreRemoteExecutionEnv: YES,
39
40
  canonicalPathAuthority: YES,
41
+ tenantSeparation: SU,
40
42
  };
41
43
  case "e2b":
42
44
  return {
@@ -54,6 +56,7 @@ export function executionLaneCaps(lane) {
54
56
  hostFilePlane: NO,
55
57
  coreRemoteExecutionEnv: YES,
56
58
  canonicalPathAuthority: NO,
59
+ tenantSeparation: YES,
57
60
  };
58
61
  case "k8s":
59
62
  return {
@@ -71,6 +74,7 @@ export function executionLaneCaps(lane) {
71
74
  hostFilePlane: NO,
72
75
  coreRemoteExecutionEnv: YES,
73
76
  canonicalPathAuthority: YES,
77
+ tenantSeparation: YES,
74
78
  };
75
79
  case "ssh":
76
80
  return {
@@ -88,6 +92,7 @@ export function executionLaneCaps(lane) {
88
92
  hostFilePlane: NO,
89
93
  coreRemoteExecutionEnv: YES,
90
94
  canonicalPathAuthority: YES,
95
+ tenantSeparation: SU,
91
96
  };
92
97
  case "adb":
93
98
  return {
@@ -105,6 +110,7 @@ export function executionLaneCaps(lane) {
105
110
  hostFilePlane: NO,
106
111
  coreRemoteExecutionEnv: YES,
107
112
  canonicalPathAuthority: YES,
113
+ tenantSeparation: SU,
108
114
  };
109
115
  case "local-docker":
110
116
  return {
@@ -122,6 +128,7 @@ export function executionLaneCaps(lane) {
122
128
  hostFilePlane: NO,
123
129
  coreRemoteExecutionEnv: YES,
124
130
  canonicalPathAuthority: NO,
131
+ tenantSeparation: YES,
125
132
  };
126
133
  case "device":
127
134
  return {
@@ -139,6 +146,7 @@ export function executionLaneCaps(lane) {
139
146
  hostFilePlane: NO,
140
147
  coreRemoteExecutionEnv: YES,
141
148
  canonicalPathAuthority: YES,
149
+ tenantSeparation: YES,
142
150
  };
143
151
  }
144
152
  }