@sema-agent/server 7.151.0 → 7.152.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 (286) hide show
  1. package/MIGRATION.md +266 -0
  2. package/README.md +15 -3
  3. package/README.zh-CN.md +10 -2
  4. package/USAGE.md +93 -64
  5. package/deploy/sema-up/README.md +5 -4
  6. package/deploy/sema-up/cluster-up.sh +6 -6
  7. package/deploy/sema-up/kube-up.sh +5 -5
  8. package/deploy/sema-up/lib/flagshape.sh +36 -8
  9. package/deploy/sema-up/machines.example.yaml +3 -1
  10. package/deploy/sema-up/sema-up.sh +22 -16
  11. package/dist/approval-ask-audit-store.d.ts +1 -1
  12. package/dist/approval-ask-machine.d.ts +4 -10
  13. package/dist/approval-ask-retention.d.ts +1 -1
  14. package/dist/approval-card.d.ts +10 -12
  15. package/dist/approval-gate-currency.d.ts +8 -8
  16. package/dist/approval-gate-currency.js +3 -4
  17. package/dist/approval-hmac.d.ts +17 -1
  18. package/dist/approval-hmac.js +7 -1
  19. package/dist/approval-queue-kind.d.ts +1 -1
  20. package/dist/approval.d.ts +2 -2
  21. package/dist/approval.js +2 -2
  22. package/dist/bind-host.d.ts +1 -3
  23. package/dist/bind-host.js +2 -3
  24. package/dist/boot/coordinators.d.ts +1 -1
  25. package/dist/boot/coordinators.js +9 -10
  26. package/dist/boot/device-lane.js +2 -0
  27. package/dist/boot/execution-env.d.ts +1 -0
  28. package/dist/boot/execution-env.js +31 -5
  29. package/dist/boot/leader.d.ts +31 -30
  30. package/dist/boot/leader.js +38 -28
  31. package/dist/boot/leg-path-adjudication.d.ts +40 -0
  32. package/dist/boot/leg-path-adjudication.js +18 -0
  33. package/dist/boot/parked-revive-gate.js +3 -15
  34. package/dist/boot/reapers.d.ts +1 -1
  35. package/dist/boot/reapers.js +1 -1
  36. package/dist/boot/resolve-spec.js +12 -26
  37. package/dist/boot/runner-deps.d.ts +39 -35
  38. package/dist/boot/runner-deps.js +92 -89
  39. package/dist/boot/shutdown.d.ts +6 -0
  40. package/dist/boot/shutdown.js +10 -4
  41. package/dist/boot/stage-02-web-admin-pull.js +4 -5
  42. package/dist/boot/stage-05-execution-env.js +2 -1
  43. package/dist/boot/stage-06-runners.d.ts +2 -0
  44. package/dist/boot/stage-06-runners.js +10 -4
  45. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  46. package/dist/boot/stage-07-capability-layer.js +1 -1
  47. package/dist/boot/stage-08-reapers.d.ts +1 -0
  48. package/dist/boot/stage-09-leader.d.ts +1 -0
  49. package/dist/boot/stage-09-leader.js +2 -2
  50. package/dist/boot/stage-10-http-server.d.ts +8 -0
  51. package/dist/boot/stage-10-http-server.js +11 -15
  52. package/dist/boot/stores.js +7 -4
  53. package/dist/boot/web-admin.js +2 -7
  54. package/dist/capabilities/approvals-push.d.ts +3 -3
  55. package/dist/capabilities/scenarios.d.ts +28 -13
  56. package/dist/capabilities/scenarios.js +2 -2
  57. package/dist/capability-facts.d.ts +22 -20
  58. package/dist/capability-facts.js +18 -34
  59. package/dist/card-rule-offers.d.ts +32 -0
  60. package/dist/card-rule-offers.js +130 -0
  61. package/dist/config-catalog.js +12 -11
  62. package/dist/config-dir-trust.js +4 -2
  63. package/dist/config-invariants.d.ts +18 -7
  64. package/dist/config-invariants.js +51 -8
  65. package/dist/config-lkg.d.ts +5 -19
  66. package/dist/config-lkg.js +8 -62
  67. package/dist/config-provider.d.ts +1 -1
  68. package/dist/config-root-migration.d.ts +2 -2
  69. package/dist/config-types.d.ts +28 -38
  70. package/dist/config.d.ts +7 -5
  71. package/dist/config.js +55 -16
  72. package/dist/control-socket.d.ts +51 -0
  73. package/dist/control-socket.js +115 -0
  74. package/dist/deployment-governance.d.ts +2 -1
  75. package/dist/device-ws-hub.d.ts +7 -0
  76. package/dist/device-ws-hub.js +7 -1
  77. package/dist/elicitation.js +2 -2
  78. package/dist/entry-problems.d.ts +40 -3
  79. package/dist/entry-problems.js +3 -1
  80. package/dist/execution-env-refusal.d.ts +8 -2
  81. package/dist/execution-env-refusal.js +3 -0
  82. package/dist/fleet/engine-signal.d.ts +12 -29
  83. package/dist/fleet/engine-signal.js +1 -7
  84. package/dist/fleet-client.js +2 -5
  85. package/dist/gate-settlement.d.ts +1 -8
  86. package/dist/gate-settlement.js +0 -4
  87. package/dist/history-anchor.js +1 -1
  88. package/dist/hitl-delivery.d.ts +2 -3
  89. package/dist/hitl-delivery.js +0 -2
  90. package/dist/hooks/hook-llm.js +4 -2
  91. package/dist/hooks/hook-runner.d.ts +3 -2
  92. package/dist/hooks/hook-runner.js +16 -1
  93. package/dist/host-decision.d.ts +11 -11
  94. package/dist/http/active-run-conflict.d.ts +35 -2
  95. package/dist/http/active-run-conflict.js +48 -19
  96. package/dist/http/approval-sweeps.d.ts +1 -1
  97. package/dist/http/approval-sweeps.js +5 -2
  98. package/dist/http/checkpoint-ctx.d.ts +2 -0
  99. package/dist/http/checkpoint-ctx.js +1 -0
  100. package/dist/http/dispatch.d.ts +6 -2
  101. package/dist/http/dispatch.js +25 -5
  102. package/dist/http/durable-run-leg.js +0 -1
  103. package/dist/http/health-reader.d.ts +16 -9
  104. package/dist/http/health-reader.js +2 -1
  105. package/dist/http/health-self-report.js +1 -1
  106. package/dist/http/owner-access.d.ts +2 -3
  107. package/dist/http/owner-access.js +3 -4
  108. package/dist/http/principal-gate.d.ts +49 -21
  109. package/dist/http/principal-gate.js +61 -11
  110. package/dist/http/request-channel.d.ts +23 -0
  111. package/dist/http/request-channel.js +9 -0
  112. package/dist/http/request-guards.js +17 -2
  113. package/dist/http/resume-ctx-read.js +20 -3
  114. package/dist/http/resume-legs.d.ts +2 -2
  115. package/dist/http/resume-legs.js +111 -41
  116. package/dist/http/route-ctx.d.ts +73 -7
  117. package/dist/http/route-ctx.js +17 -0
  118. package/dist/http/route-table.d.ts +4 -0
  119. package/dist/http/route-table.js +4 -0
  120. package/dist/http/routes/admin-config-refresh.js +3 -3
  121. package/dist/http/routes/admin-drain.d.ts +3 -0
  122. package/dist/http/routes/admin-drain.js +7 -7
  123. package/dist/http/routes/admin-memory-optout.js +3 -3
  124. package/dist/http/routes/adoption.js +3 -5
  125. package/dist/http/routes/approvals-assistant.d.ts +2 -1
  126. package/dist/http/routes/approvals-assistant.js +95 -48
  127. package/dist/http/routes/capabilities.d.ts +4 -0
  128. package/dist/http/routes/capabilities.js +30 -5
  129. package/dist/http/routes/config-catalog.js +2 -2
  130. package/dist/http/routes/devices.js +1 -1
  131. package/dist/http/routes/diagnostics.d.ts +3 -3
  132. package/dist/http/routes/diagnostics.js +19 -7
  133. package/dist/http/routes/images.js +10 -10
  134. package/dist/http/routes/memory-bundle.js +3 -3
  135. package/dist/http/routes/memory-compliance.js +3 -3
  136. package/dist/http/routes/memory-consolidation.js +2 -2
  137. package/dist/http/routes/memory-origin.js +3 -3
  138. package/dist/http/routes/memory-policy.js +2 -2
  139. package/dist/http/routes/observability.js +3 -3
  140. package/dist/http/routes/retention-ops.js +3 -3
  141. package/dist/http/routes/rules.js +5 -5
  142. package/dist/http/routes/run-permission-mode.d.ts +2 -1
  143. package/dist/http/routes/run-permission-mode.js +2 -2
  144. package/dist/http/routes/runs.d.ts +4 -9
  145. package/dist/http/routes/runs.js +73 -33
  146. package/dist/http/routes/session-sync.d.ts +18 -6
  147. package/dist/http/routes/session-sync.js +38 -36
  148. package/dist/http/routes/sessions.js +20 -9
  149. package/dist/http/routes/tasks.js +28 -37
  150. package/dist/http/routes/workflows.js +1 -1
  151. package/dist/http/server.d.ts +9 -3
  152. package/dist/http/server.js +15 -3
  153. package/dist/http/submit-ctx.d.ts +1 -1
  154. package/dist/http/submit-ctx.js +2 -0
  155. package/dist/human-facing-args.d.ts +33 -0
  156. package/dist/human-facing-args.js +33 -0
  157. package/dist/index.d.ts +1 -1
  158. package/dist/k8s-api-credentials.d.ts +29 -0
  159. package/dist/k8s-api-credentials.js +25 -0
  160. package/dist/lane-home-probe.d.ts +1 -1
  161. package/dist/lane-home-probe.js +2 -1
  162. package/dist/leader/diffup.d.ts +19 -0
  163. package/dist/leader/diffup.js +7 -0
  164. package/dist/leader/leader.d.ts +114 -10
  165. package/dist/leader/leader.js +69 -5
  166. package/dist/leader/repair-wire.js +2 -2
  167. package/dist/leader/run-entry.d.ts +2 -1
  168. package/dist/leader/run-entry.js +1 -1
  169. package/dist/leader/wire.d.ts +42 -132
  170. package/dist/leader/wire.js +81 -67
  171. package/dist/memory-scope.d.ts +20 -9
  172. package/dist/memory-scope.js +15 -3
  173. package/dist/model-entry-headers.d.ts +9 -10
  174. package/dist/model-entry-headers.js +0 -4
  175. package/dist/model-entry-refusal.d.ts +7 -4
  176. package/dist/model-entry-refusal.js +11 -5
  177. package/dist/model-plane-face.d.ts +1 -1
  178. package/dist/model-plane-face.js +3 -1
  179. package/dist/model-route-endpoint.d.ts +13 -7
  180. package/dist/model-route-endpoint.js +14 -11
  181. package/dist/model-select.d.ts +22 -0
  182. package/dist/model-select.js +9 -0
  183. package/dist/model-unreachable-notice.d.ts +11 -0
  184. package/dist/model-unreachable-notice.js +4 -4
  185. package/dist/object-store-buckets.d.ts +640 -6
  186. package/dist/object-store-buckets.js +166 -0
  187. package/dist/observability/fail-open.d.ts +20 -16
  188. package/dist/observability/fail-open.js +20 -16
  189. package/dist/observability/logger.js +3 -1
  190. package/dist/operator-roster.d.ts +68 -0
  191. package/dist/operator-roster.js +32 -0
  192. package/dist/orchestration/workflow-park-decision.d.ts +1 -1
  193. package/dist/outbound-dispatcher.d.ts +71 -8
  194. package/dist/outbound-dispatcher.js +48 -7
  195. package/dist/owner-faces.d.ts +1 -1
  196. package/dist/parked-decide.d.ts +2 -2
  197. package/dist/permission-mode-admission.d.ts +59 -26
  198. package/dist/permission-mode-admission.js +37 -10
  199. package/dist/plugins/approval-ask-store-file.d.ts +4 -0
  200. package/dist/plugins/approval-ask-store-file.js +3 -0
  201. package/dist/plugins/approval-ask-store-memory.d.ts +4 -0
  202. package/dist/plugins/approval-ask-store-memory.js +7 -1
  203. package/dist/plugins/approval-ask-store-sql.d.ts +26 -2
  204. package/dist/plugins/approval-ask-store-sql.js +12 -1
  205. package/dist/plugins/blob-backend.d.ts +83 -7
  206. package/dist/plugins/blob-backend.js +57 -6
  207. package/dist/plugins/checkpoint-index-columns.d.ts +20 -16
  208. package/dist/plugins/checkpoint-index-columns.js +3 -6
  209. package/dist/plugins/checkpoint-store-face.d.ts +119 -61
  210. package/dist/plugins/checkpoint-store-face.js +134 -55
  211. package/dist/plugins/landed-attendants.d.ts +2 -2
  212. package/dist/plugins/local-checkpoint-store.d.ts +13 -3
  213. package/dist/plugins/local-checkpoint-store.js +38 -49
  214. package/dist/plugins/one-time-migrations.d.ts +51 -21
  215. package/dist/plugins/one-time-migrations.js +67 -45
  216. package/dist/plugins/pg-pool.js +11 -9
  217. package/dist/plugins/remote-env-host.d.ts +29 -10
  218. package/dist/plugins/remote-env-host.js +99 -19
  219. package/dist/plugins/remote-env-k8s.d.ts +68 -2
  220. package/dist/plugins/remote-env-k8s.js +107 -24
  221. package/dist/plugins/replica-registration.d.ts +1 -1
  222. package/dist/plugins/replica-registration.js +1 -1
  223. package/dist/plugins/s3-presign.d.ts +23 -0
  224. package/dist/plugins/s3-presign.js +37 -3
  225. package/dist/plugins/scheduler-support.d.ts +2 -2
  226. package/dist/plugins/scheduler-support.js +0 -3
  227. package/dist/plugins/schema-indexes.js +2 -0
  228. package/dist/plugins/send-file-ledger.d.ts +2 -2
  229. package/dist/plugins/send-file-ledger.js +2 -2
  230. package/dist/plugins/send-user-file.d.ts +3 -4
  231. package/dist/plugins/send-user-file.js +56 -52
  232. package/dist/plugins/session-blob-claim-token.d.ts +15 -0
  233. package/dist/plugins/session-blob-claim-token.js +20 -0
  234. package/dist/plugins/session-blob-namespace.d.ts +46 -0
  235. package/dist/plugins/session-blob-namespace.js +116 -0
  236. package/dist/plugins/snapshot-bucket-presence.d.ts +25 -2
  237. package/dist/plugins/snapshot-bucket-presence.js +8 -2
  238. package/dist/plugins/sql-json-column.d.ts +1 -1
  239. package/dist/plugins/store-backend.d.ts +44 -2
  240. package/dist/plugins/store-backend.js +75 -53
  241. package/dist/plugins/task-attachment-store.d.ts +2 -1
  242. package/dist/plugins/task-attachment-store.js +5 -1
  243. package/dist/plugins/tidb-pool.js +19 -24
  244. package/dist/plugins/workspace-custody.d.ts +4 -3
  245. package/dist/question.d.ts +2 -1
  246. package/dist/question.js +5 -13
  247. package/dist/request-form.d.ts +6 -1
  248. package/dist/request-form.js +4 -1
  249. package/dist/rule-issue-reading.d.ts +1 -1
  250. package/dist/rules-consent.d.ts +1 -1
  251. package/dist/run-local.js +8 -9
  252. package/dist/runs.d.ts +17 -8
  253. package/dist/runs.js +15 -2
  254. package/dist/runtime-governance.d.ts +2 -2
  255. package/dist/sema-config-dir.d.ts +2 -1
  256. package/dist/session-sync.d.ts +27 -11
  257. package/dist/session-sync.js +15 -7
  258. package/dist/stores-sql/blob-index-sql.d.ts +30 -9
  259. package/dist/stores-sql/blob-index-sql.js +40 -21
  260. package/dist/stores-sql/checkpoint-store-sql.d.ts +54 -27
  261. package/dist/stores-sql/checkpoint-store-sql.js +70 -146
  262. package/dist/stores-sql/file-history-store-sql.d.ts +64 -16
  263. package/dist/stores-sql/file-history-store-sql.js +100 -28
  264. package/dist/stores-sql/retention-store-sql.js +3 -9
  265. package/dist/task-settings.d.ts +6 -5
  266. package/dist/task-settings.js +3 -3
  267. package/dist/tool-approval.d.ts +342 -518
  268. package/dist/tool-approval.js +498 -1207
  269. package/dist/trace/core-keyset-guard.d.ts +1 -1
  270. package/dist/trace/ledger-events.d.ts +3 -3
  271. package/dist/trace/project.d.ts +2 -3
  272. package/dist/trace/redact.js +3 -2
  273. package/dist/trace/self-minted-wire.d.ts +287 -157
  274. package/dist/trace/self-minted-wire.js +93 -48
  275. package/dist/web-admin-client/apply-effective.js +2 -2
  276. package/dist/web-admin-client/read-warnings.d.ts +2 -2
  277. package/dist/web-admin-client/read-warnings.js +1 -1
  278. package/dist/web-admin-client/types.d.ts +1 -1
  279. package/dist/workspace-space.d.ts +10 -4
  280. package/dist/workspace-space.js +4 -1
  281. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +1 -1
  282. package/package.json +6 -2
  283. package/scripts/embed-minimal.mjs +3 -3
  284. package/wire-changes/server@7.152.0.json +900 -0
  285. package/dist/config-d-migration.d.ts +0 -37
  286. package/dist/config-d-migration.js +0 -60
@@ -398,20 +398,16 @@ export interface A2aServeConfig {
398
398
  * off (the control plane never mounts). The runner-facing facts (the runner principal, the build-host CACHE_BASE,
399
399
  * the default base ref) live here so image-api injects only VETTED, server-fixed values into the build.sh argv —
400
400
  * operator free-text never reaches the runner. */
401
- /** [ref] 车3([ref] 流内审批协议)的配置面。总开关默认 **true**(clay 裁 2026-08-08,[ref] 翻真验证后;
402
- * 树上已生效,发布线落在 7.4.0 之后的下一个发布);从属旋钮**除 `windowMs` 外**只在开关为真时生效
403
- * ([ref]:`STREAM_ASK_WINDOW_MS` 是活卡窗长,与协议上不上场解耦 —— 逐条辖域见各字段域注)
401
+ /** [ref] 车3([ref] 流内审批协议)的配置面。协议上不上场只由装配在场性定(审批席 ∧ store backend ∧ park 设施,
402
+ * `resolveStreamApprovalGate`)—— 没有总开关:审批只有呈卡族一个帧族,开关的「关」(回退发旧族活卡)随旧族退役。
403
+ * 从属旋钮**除 `windowMs` 外**只在协议上场时生效([ref]:`STREAM_ASK_WINDOW_MS` 与协议上不上场解耦 —— 逐条辖域见各字段域注)
404
404
  * (语义与「谁需要 / 谁被伤 / 什么补偿」三问见 `ServiceConfigFlat.streamApproval` 的注)。 */
405
405
  export interface StreamApprovalConfig {
406
- /** 协议总开关。`STREAM_APPROVAL_ENABLED`,默认 **true**(clay 裁 2026-08-08,[ref] 翻真验证后;7.5.0 起)。显式 false ⇒ 全链逐字 7.3.0 前行为(唯一干净还原键)。
407
- * 翻真要等回决端点(车4)与对账收敛器(车5)到位——在此之前是个不完整协议(有卡无正规回决口、
408
- * 有 PARKING 无收敛器、跨副本回决无唤醒路径)。 */
409
- enabled: boolean;
410
406
  /** `ToolApprovalCoordinator` 的 `ttlMs` —— **一只活卡等人多久**([ref] §3.3 的「配置窗默认」)。
411
407
  * `STREAM_ASK_WINDOW_MS`,默认 **300000**(同裁与 sync 腿既有 5min 活卡窗对齐)。
412
408
  *
413
409
  * 🔴 **辖域([ref] 起):无条件** —— 本值在**每一条**呈活卡的腿上生效,与流内协议上不上场无关
414
- * (`DB_BACKEND=local` / 无 park 设施 / `STREAM_APPROVAL_ENABLED=false` 的部署同样按它计窗)。
410
+ * (无 park 设施的部署上它管的是「没有卡、每只问当场交政策口」之前的那一格 —— 那里没有窗可计)。
415
411
  * 旧文「开关为假 ⇒ 本值不参与,窗保持既有 `DEFAULT_APPROVAL_TTL_MS`(5min)」是**已修的病**,不是
416
412
  * 设计:那让一根部署级旋钮的生效与否挂在装配在场性上,运维配了它、没有任何一行说它没生效
417
413
  * (clay 令:部署级旋钮禁挂在场性派生腿)。缺省值与那个常量同为 300000 ⇒ **没配过这根 env 的部署
@@ -1238,14 +1234,9 @@ export interface ServiceConfigFlat {
1238
1234
  /** plugins 域 clone host 白名单(`PLUGINS_ALLOW_HOSTS`,缺省 github.com;契约 [ref]① 双保险面,
1239
1235
  * 消费在 capabilities/center-plugins.ts)。 */
1240
1236
  pluginsAllowHosts: string[];
1241
- /** 监听绑址([ref] cli 实锤:此前 `HOST` **零消费**、`listen(port)` 恒绑所有接口)。
1242
- * `BIND_HOST`(兼容 `HOST`)。显式值恒生效;缺省见 {@link resolveBindHost}——**无鉴权写面
1237
+ /** 监听绑址:唯一来源 = `BIND_HOST`(`HOST` 不读 —— zsh 恒设 HOST=机器名,读它等于被 shell 环境暗配 bind 面)。显式值恒生效;缺省见 {@link resolveBindHost}——**无鉴权写面
1243
1238
  * (allowUnauthedWrites 且零凭证)自动收窄到 127.0.0.1**,其余维持全接口(既有部署零影响)。 */
1244
1239
  bindHost?: string;
1245
- /** [ref]③ `bindHost` 的取值来源(BIND_HOST 显式 / HOST 兼容名 / 未设)。`"HOST"` 时 main.ts 打一行
1246
- * 启动告警:zsh 常把 HOST 设成机器名,`{...process.env}` 起底的壳会静默继承给引擎——bind 面被 shell
1247
- * 环境暗配而部署者不知情(HOST=机器名解析到 LAN 口时=静默扩暴露面)。 */
1248
- bindHostSource?: "BIND_HOST" | "HOST";
1249
1240
  /** D-1 附件孤儿对象 GC 的 grace(毫秒;`ATTACHMENT_ORPHAN_GRACE_MS`,默认 1h,**0=关掉本腿**)。
1250
1241
  * 复审 F10:同 tick 邻居(workflowOrphanGraceMs/attachmentUnboundTtlMs/scratchpadSweepTtlMs)都有
1251
1242
  * 旋钮——存量对象巨大的部署要调慢/停掉,不该被迫取消 MinIO 配置(那会连带关掉整个附件面)。 */
@@ -1266,7 +1257,8 @@ export interface ServiceConfigFlat {
1266
1257
  * the unguessable-key capability URLs be enumerated); the private presigned track shares
1267
1258
  * SESSION_SNAPSHOT_BUCKET (default "session-snapshots") under a "sendfile/" prefix.
1268
1259
  * SEND_USER_FILE_URL_TTL = default ttl seconds when the tool call omits one; 0 = permanent (the settled
1269
- * default). Unset/non-integer/negative fail-SAFE to 0 — never NaN into a signature. */
1260
+ * default). Unset / empty ⇒ 0; anything but canonical decimal digits does not load (read as 0 it would turn a link meant to expire into a
1261
+ * permanent public one), nor does a value over the SigV4 ceiling (`config.ts` `sendUserFileUrlTtlKnob`). */
1270
1262
  sendUserFile?: {
1271
1263
  endpoint: string;
1272
1264
  publicEndpoint?: string;
@@ -1387,6 +1379,11 @@ export interface ServiceConfigFlat {
1387
1379
  * "empty = everyone on the approval queue" reading is gone). A single-user deployment must answer "who operates it" — a named list here or {@link operatorSingleUser}; neither ⇒ refusal
1388
1380
  * (`operator-declaration-required`: the server process refuses at boot stage 1, and `createHttpServer` refuses for every assembler, embedders included). */
1389
1381
  operatorPrincipals: string[];
1382
+ /** `OPERATOR_DRAIN_PRINCIPALS` —— 名册的**排空档**(服务主体名单;档表 `src/operator-roster.ts` 的 `drain` 行):名单内的验过主体只持 `drain` 一只运维权力、不是人 ——
1383
+ * 它的身份只在声明了 `drain` 权力的路由(`POST /v1/admin/drain`)上算数,其余每条路由整次请求 403 `auth.principal_confined`,`GET /health` 按没出示凭据分档。
1384
+ * 缺席 ≡ 空 = 这一档没人(少给权力,fail-closed;`loadConfig` 恒写)。与 `OPERATOR_PRINCIPALS` 同一个名字 / 不是主体名的条目 / 不是名字列表 ⇒ 拒启(`operator-roster-well-formed`);与 `OPERATOR_SINGLE_USER=true` 同在 ⇒ 拒启
1385
+ * (`operator-single-user`)。env 独有(没有中心写位),起服冻结。不算运维声明:单用户形只写它 ⇒ 照旧 `operator-declaration-required` 拒。 */
1386
+ operatorDrainPrincipals?: readonly string[];
1390
1387
  /** `OPERATOR_SINGLE_USER=true` —— 单用户形显式声明「这台部署的每个调用方都是运维」([ref],[ref] ① 裁 A)。缺席 = 不声明(没人是,除非在名单里)。
1391
1388
  * 只在单用户形(`REQUIRE_PRINCIPAL` 未开)且名单为空时成立 —— 否则拒启(`config-invariants.ts` 的 `operator-single-user`:一个事实一个来源)。
1392
1389
  * 单用户形上它与具名名单二者必有其一,都没有 ⇒ 拒启(服务进程起服第 1 段;嵌入方在 `createHttpServer`;`operator-declaration-required`,[ref];`false` 与缺席同判)。
@@ -2027,10 +2024,12 @@ export interface ServiceConfigFlat {
2027
2024
  * {@link import("./question.js").DEFAULT_QUESTION_THROTTLE}(与本旋钮族默认逐字同值)。 */
2028
2025
  questionThrottle?: QuestionThrottle;
2029
2026
  /** [ref]/[ref]② live tool-approval HITL — mount the ToolApprovalCoordinator on `RunnerDeps.onAsk` + the
2030
- * `POST /v1/tool-approvals/:id/respond` route so a policy `ask` (the fs-write gate, ask-lists, non-durable safety
2027
+ * ask decision door (`POST /v1/tasks/:taskId/asks/:askId/decision`) so a policy `ask` (the fs-write gate, ask-lists, non-durable safety
2031
2028
  * asks) routes to the live human as the CC three-choice card on the run's stream (core 1.290 sync-ask leg). A CC
2032
2029
  * cap → posture-gated like askQuestion (single-user turnkey → ON; multi-tenant opt-in);
2033
- * `TOOL_APPROVAL_ENABLED=true/false` overrides. Absent ⇒ core's headless auto-deny stands ([ref]⑤ fail-closed). */
2030
+ * `TOOL_APPROVAL_ENABLED=true/false` overrides. Absent ⇒ core's headless auto-deny stands ([ref]⑤ fail-closed).
2031
+ * Necessary, not sufficient: the coordinator mounts only when this worker presents approval cards (this knob ∧ a store backend ∧ a park
2032
+ * facility — `resolveStreamApprovalGate`); on a worker without them the knob has nothing to mount and the headless auto-deny stands too. */
2034
2033
  toolApprovalEnabled: boolean;
2035
2034
  /**
2036
2035
  * [ref] 车二(core [ref] 持久化权限规则「不再询问」车道)**总开关**。`PERMISSION_RULES_ENABLED`,
@@ -2061,17 +2060,11 @@ export interface ServiceConfigFlat {
2061
2060
  * 旋钮本身缺省对现行为零影响(D1)。`STREAM_ASK_WINDOW_MARGIN_MS`,默认 10000(10s)。ttl 概念沿用既有
2062
2061
  * `approvalTimeoutSec` 族,本旋钮不新增第二个 TTL 概念。 */
2063
2062
  streamAskWindowMarginMs: number;
2064
- /** [ref] 车3([ref] 流内审批协议)总开关 + 四个从属旋钮。
2063
+ /** [ref] 车3([ref] 流内审批协议)的从属旋钮(没有总开关:协议上不上场只由装配在场性定,见 {@link StreamApprovalConfig} 顶注)。
2065
2064
  *
2066
- * **现行默认(clay 裁 2026-08-08 走 (a),[ref] 翻真验证后)**:`STREAM_APPROVAL_ENABLED` 默认 **true**
2067
- * = 协议整体默认 **ON**;`STREAM_ASK_WINDOW_MS` 默认 **300000**(5min,与 sync 腿既有活卡窗、与
2068
- * `DEFAULT_APPROVAL_TTL_MS` 对齐——「开协议」不再顺带把窗砍短)。⚠️ 版本坐标:翻转**已在树上生效**,
2069
- * 发布线上落在 7.4.0 之后的下一个发布,所以一台自报 7.4.0 的**已发布** worker 仍是 OFF,别只按
2070
- * `/health` 的版本号推默认。显式 `STREAM_APPROVAL_ENABLED=false` 是**唯一干净还原键** ⇒ 全链逐字回到
2071
- * 翻转前行为:不发 `approval_request` 帧、不落 `approval_ask` 行、不起收敛器腿、`askStore` 不注入
2072
- * 协调器。⚠️ **窗不在这份还原清单里了([ref])**:`STREAM_ASK_WINDOW_MS` 自此在关协议的部署上照样
2073
- * 生效(缺省 300000 == `DEFAULT_APPROVAL_TTL_MS` ⇒ 没配过它的部署仍然字节不变),理由见
2074
- * `StreamApprovalConfig.windowMs` 的辖域段。
2065
+ * **总开关 `STREAM_APPROVAL_ENABLED` 已退役**:它的「关」= 回退发旧族活卡(`tool_approval`),旧族退役后没有可关的东西;
2066
+ * 部署 env 里还留着这一键 ⇒ 起服拒(退役墓碑,不论值)。`STREAM_ASK_WINDOW_MS` 默认 **300000**(5min,与
2067
+ * `DEFAULT_APPROVAL_TTL_MS` 对齐),理由见 `StreamApprovalConfig.windowMs` 的辖域段。
2075
2068
  *
2076
2069
  * **旋钮为什么存在(2026-08-08 翻转前的成文前提,已被上面的裁定取代,留作沿革)**:那一版默认 false +
2077
2070
  * 60s 窗,三问记的是那个世界——
@@ -2084,7 +2077,7 @@ export interface ServiceConfigFlat {
2084
2077
  * ⇒ 翻真时「窗变短」这一项被从翻转里摘出(窗默认抬回 300000),被伤面①②因此消失;60s 只在运维显式设
2085
2078
  * `STREAM_ASK_WINDOW_MS=60000` 时才回来。
2086
2079
  *
2087
- * `STREAM_APPROVAL_ENABLED` / `STREAM_ASK_WINDOW_MS` / `STREAM_APPROVAL_REPLAY_MAX` /
2080
+ * 键:`STREAM_ASK_WINDOW_MS` / `STREAM_APPROVAL_REPLAY_MAX` /
2088
2081
  * `STREAM_APPROVAL_ADMIT_MAX_PER_TASK` / `STREAM_APPROVAL_ADMIT_MAX_PER_OWNER` /
2089
2082
  * `STREAM_APPROVAL_RECONCILE_BATCH` / `STREAM_APPROVAL_PENDING_GRACE_MS` /
2090
2083
  * `STREAM_APPROVAL_ADHOC_GRACE_MS` / `STREAM_APPROVAL_ORPHAN_TTL_MS`(后四键 = 车5 收敛器)。 */
@@ -2103,7 +2096,7 @@ export interface ServiceConfigFlat {
2103
2096
  *
2104
2097
  * 施加点 = `ToolApprovalCoordinator` 的构造参 `unattendedPolicy`,五条「无人可答」的臂读同一
2105
2098
  * 个值;对账收敛器的同名构造参([ref],`boot/reapers.ts` 恒传同一个部署值)只供它的孤儿段打同一只终态 claim ——
2106
- * 两处读同一个值,于是崩溃恢复与 live 腿同答(设计稿 §1 的表:(a) 有店窗到期赢 CAS /(b) 无店窗到期 /(c) 店报错 catch /(d) windowZero 与
2099
+ * 两处读同一个值,于是崩溃恢复与 live 腿同答(设计稿 §1 的表:(a) 有店窗到期赢 CAS /(b) 无店窗到期(7.152.0 起随无账协调器退役)/(c) 店报错 catch /(d) windowZero 与
2107
2100
  * headless 腿 /(e) 断连强转 + emit 全灭 + 写侧准入超限 + **投递集合入口活性过滤清空**([ref] 归
2108
2101
  * 位:与「查无活流」同一件事实的两个时刻,详见 `askBroadcast` 的该臂注))。`"timeout"` 宿主自报只留在 (b)(c) ——
2109
2102
  * 结束等待的确实是本仓的窗;其余三臂结束等待的是容量/连接/装配,自报恒缺席。
@@ -2115,14 +2108,11 @@ export interface ServiceConfigFlat {
2115
2108
  * `"timeout"` 自报(`approval_window_expired`)。
2116
2109
  *
2117
2110
  * 🔴 **射程边界(成文,不是遗漏)**:
2118
- * · 唯一让本旋钮**无处施加**的键是 `TOOL_APPROVAL_ENABLED=false`(协调器整体不构造,core 走它自己的
2119
- * headless 姿态)。**不造推断腿**:旋钮只在协调器在场时有意义,这一句是成文而不是代码分支。
2120
- * · ⚠️ **与 `STREAM_APPROVAL_ENABLED` 正交**(codex 交叉复审 R2-F2 验真后纠正 —— 本注上一版把两者
2121
- * 写成了一回事,是错的):协议开关关掉只是不注入 `askStore`/协议窗,协调器照常在场,于是 R-13 的
2122
- * D1 窗到期臂**照样**走 park 路由。这不是漏网:设计稿 §2 明写 park 的承载是 **core checkpoint**
2123
- * 而不是 server 的 ask 行,「无 askStore 但有 durable 设施」正是 R-13 点名的**主受益部署**。
2124
- * 要回滚 R-13 的行为面,键是 `UNATTENDED_APPROVAL_POLICY=deny`,**不是**协议开关。
2125
- * 射程钉:`test/unattended-approval-policy.test.ts` 的 codex R2-F2 两格。
2111
+ * · 本旋钮只在审批协调器在场时有处施加;协调器只在呈卡协议上场时构造(`TOOL_APPROVAL_ENABLED` ∧ store backend ∧ park 设施,
2112
+ * `boot/coordinators.ts`)。不上场的三形各自的终局与本旋钮无关:开关关 ⇒ core 自己的姿态(有 durable 设施时照常 park);
2113
+ * 开关开而无 store backend / 无 park 设施 ⇒ 没有审批席也没有停驻处,core 的无审批席臂当场拒。**不造推断腿**:这一句是成文而不是代码分支。
2114
+ * · 总开关 `STREAM_APPROVAL_ENABLED` 7.152.0 起退役;要回滚 R-13 的行为面,键是 `UNATTENDED_APPROVAL_POLICY=deny`。
2115
+ * 射程钉:`test/unattended-approval-policy.test.ts` 的「呈卡协议不上场与本旋钮的关系」一组。
2126
2116
  * · APPROVAL_REQUIRE 名单工具:**要不要 ask** 由 `createApprovalBaselinePolicy` 的 durable gate 类裁决,那一层
2127
2117
  * 结构上读不到本旋钮;但那只 ask 的**答法**在协调器在场时与治理 ask 同走流内臂、同政策(引擎先问活席
2128
2118
  * `onAsk`,答 `"unavailable"` 才 park)⇒ `deny` 部署对名单工具同样当场拒。旧注「不经流内臂、不受本旋钮
@@ -2415,7 +2405,7 @@ export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled"
2415
2405
  * 三者都是 operator 在配置面写下的**治理声明**(与 autonomy/commandPolicy 同族),不是存储旋钮
2416
2406
  * ——`retentionPolicy` 尤其容易被误归 store 组:它的消费点是 core 的启动期能力校验与将来的调度面,
2417
2407
  * config 这一层它只是一句声明。 */
2418
- export type ServiceAuthConfig = Pick<ServiceConfigFlat, "authToken" | "authTokens" | "allowUnauthedWrites" | "corsOrigins" | "principalHeader" | "requirePrincipal" | "autonomy" | "commandPolicy" | "compliancePosture" | "lockedConfigKeys" | "configCatalogEnabled" | "retentionPolicy" | "retentionSweep" | "operatorPrincipals" | "operatorSingleUser" | "serviceTokenActsForDeployment" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "authBridgeIssuer" | "principalJwtMaxTtlSec" | "bindHost" | "bindHostSource">;
2408
+ export type ServiceAuthConfig = Pick<ServiceConfigFlat, "authToken" | "authTokens" | "allowUnauthedWrites" | "corsOrigins" | "principalHeader" | "requirePrincipal" | "autonomy" | "commandPolicy" | "compliancePosture" | "lockedConfigKeys" | "configCatalogEnabled" | "retentionPolicy" | "retentionSweep" | "operatorPrincipals" | "operatorDrainPrincipals" | "operatorSingleUser" | "serviceTokenActsForDeployment" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "authBridgeIssuer" | "principalJwtMaxTtlSec" | "bindHost">;
2419
2409
  /** 组:orchestration(编排 + 执行车道 + 沙箱面 + workflow/后台 agent 存留)。 */
2420
2410
  export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "sandboxGit" | "remoteExec" | "remoteExecHomeDir" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerSessionLifetime" | "schedulerStorePath" | "planModeEnabled" | "hooksTimeoutMs" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "usageWindows" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentStallMs" | "backgroundAgentParkClaimStaleMs" | "delegationEntryCaps" | "rosterRetentionMs" | "leaderRunStaleMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sandboxPkgSourceCustom" | "sessionAutoTitle" | "sessionAutoTitleSource" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspHostEnabled" | "lspEnvAllow" | "hookEnvAllow" | "autoBackgroundOnTimeout" | "imageBakes" | "readFace" | "readFaceSource" | "taskWriteFaceEscalation" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude" | "readDenyFloorRequired" | "sandboxReadFloor" | "agentConfigDir" | "configLkgPath" | "configLkgEnabled">;
2421
2411
  /** 组:limitsHttp(HTTP 面 + 各类上限/配额/回收窗)。 */
package/dist/config.d.ts CHANGED
@@ -3,6 +3,7 @@ import { type ConfigProviderWord } from "./config-types.js";
3
3
  import { type CenterEndpoint } from "./web-admin-client/center-request.js";
4
4
  import { type ExecutionLane } from "./execution-lane-caps.js";
5
5
  import { type Autonomy } from "./runtime-governance.js";
6
+ import { type EntryProblemReason } from "./entry-problems.js";
6
7
  import { type RunnerEnvNameAllowlistKnob, type ServerEnvNameAllowlistKnob } from "./env-name-allowlist-knobs.js";
7
8
  import type { ServiceConfig, LoadedConfig, RemoteExecConfig, ServiceConfigFlat, ServiceConfigGroups } from "./config-types.js";
8
9
  /** [ref] A1: the config TYPE face lives in the leaf module config-types.ts; re-exported here so
@@ -61,9 +62,9 @@ export declare function parseNumOrFail(name: string, raw: string | undefined): n
61
62
  */
62
63
  export declare function parseNumOrFailNonNegative(name: string, raw: string | undefined): number;
63
64
  /** Optional FAIL-SAFE numeric env for an OPTIONAL feature knob: a non-numeric / non-positive value yields `undefined`
64
- * (the caller falls back to its own default) rather than NaN OR a hard startup throw. Used for SESSION_SNAPSHOT_TTL_SEC
65
- * (finding 20): a typo like "600s" must NOT sign every presigned URL with X-Amz-Expires=NaN (total snapshot outage),
66
- * but ALSO must not crash boot of an unrelated subsystem — degrade to the MinioBlobBackend default. */
65
+ * (the caller falls back to its own default) rather than NaN OR a hard startup throw. A presigned-URL lifetime is not such a
66
+ * knob: its spelling is judged by `presignLifetimeSpelled` and a non-decimal one does not load (a typo there must neither sign
67
+ * `X-Amz-Expires=NaN` nor a `Number()` reading of what the operator wrote). */
67
68
  /** S20 (SILENT-FALLBACK P1): soft-knob parse rejects, collected here because config parsing runs BEFORE the
68
69
  * logger exists. main.ts drains this once after createLogger and warns per entry ("configured X=raw is
69
70
  * invalid, using the default"). Module-level on purpose; drain clears it (repeat loadConfig in tests is fine). */
@@ -387,9 +388,10 @@ export interface UnmatchableToolName {
387
388
  guidance: string;
388
389
  /**
389
390
  * 这一条为什么永远不生效 —— 机读位的 `reason`(`entry-problems.ts`):引擎名字判官的判词 `retired` / `unmintable_segment` / `unprefixed_namespace`、引擎规则文法的码原样(`invalid.*` /
390
- * `unsupported.*`)、近似名 `near_miss`、名表消费者的 CC 规则形 `cc_rule_form`。句子里的 `guidance` 给人读,本字给机器读;两者在同一处同时铸。
391
+ * `unsupported.*`)、近似名 `near_miss`、名表消费者的 CC 规则形 `cc_rule_form`(本仓自铸,取闭集 `CC_RULE_FORM`)。句子里的 `guidance` 给人读,本字给机器读;两者在同一处同时铸。
392
+ * 型 = 条目问题 `reason` 的全部词表之并({@link EntryProblemReason}):这里写一个哪张表都没有的词 = tsc 红。
391
393
  */
392
- reason: string;
394
+ reason: EntryProblemReason;
393
395
  }
394
396
  /**
395
397
  * [ref](core 7.9.0 [ref])—— **这条名单条目喂给谁**,决定「永不匹配」这句话怎么判。三个消费者形态:
package/dist/config.js CHANGED
@@ -32,11 +32,11 @@ import { MODEL_THINKING_LEVELS, THINKING_LEVELS, THINKING_FORMATS, buildModelCom
32
32
  import {} from "./posture-source.js";
33
33
  import { COST_QUOTA_WINDOW_SEC_RANGE, costQuotaWindowSecInRange } from "./observability/cost-quota.js";
34
34
  import { NAME_TABLE_BEHAVIOR, TOOL_NAME_ROSTER, describeUnmatchableRuleName, isRetiredLeftOut, retiredLeftOutEntries, ruleEntryToolName, unmatchableRuleNameOf } from "./namespaced-rule-name.js";
35
- import { bakeOperatorPrincipalsRefusal, bakeRunnerCredentialRefusal, centerIdentityRefusal, halfOpenDirectDoorRefusal, leaderDurableStoreRefusal, operatorPrincipalsRequiredRefusal, operatorSingleUserRefusal, remoteConfigProviderUrlRefusal, retentionLaneRefusal, workflowParkDecisionDomainRefusal } from "./config-invariants.js";
35
+ import { bakeOperatorPrincipalsRefusal, bakeRunnerCredentialRefusal, centerIdentityRefusal, halfOpenDirectDoorRefusal, leaderDurableStoreRefusal, operatorPrincipalsRequiredRefusal, operatorSingleUserRefusal, operatorRosterWellFormedRefusal, remoteConfigProviderUrlRefusal, retentionLaneRefusal, workflowParkDecisionDomainRefusal } from "./config-invariants.js";
36
36
  import { declaredSemaConfigDir } from "./sema-config-dir.js";
37
37
  import { lkgPathOverrideOf } from "./config-lkg.js";
38
38
  import { K8S_WORKSPACE_MOUNT_DEFAULT, WORKSPACE_PERSIST_DEFAULT, WORKSPACE_PERSIST_WORDS, WORKSPACE_SNAPSHOT_KEEP_DEFAULT, WORKSPACE_SNAPSHOT_KEEP_RANGE, WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT, WORKSPACE_SNAPSHOT_TIMEOUT_MS_RANGE, parseWorkspaceSnapshotExclude, parseWorkspaceSnapshotPrefix } from "./workspace-space.js";
39
- import { SESSION_SNAPSHOT_BUCKET_DEFAULT, WORKSPACE_SNAPSHOT_BUCKET_DEFAULT } from "./object-store-buckets.js";
39
+ import { S3_PUBLIC_BUCKET_DEFAULT, SENDFILE_PRIVATE_PREFIX, SESSION_SNAPSHOT_BUCKET_DEFAULT, SIGV4_PRESIGN_MAX_SEC, WORKSPACE_SNAPSHOT_BUCKET_DEFAULT, sessionSnapshotPrefixProblem } from "./object-store-buckets.js";
40
40
  import { laneLivenessOf } from "./exec-bounds.js";
41
41
  import { SANDBOX_IPV6_DEFAULT, SANDBOX_IPV6_WORDS } from "./sandbox-egress-policy.js";
42
42
  import { WORKSPACE_CACHE_DEFAULT, WORKSPACE_CACHE_MOUNT_DEFAULT, WORKSPACE_CACHE_MOUNT_RESERVED, WORKSPACE_CACHE_SIZE_DEFAULT, WORKSPACE_CACHE_WORDS, isK8sQuantity, mountsOverlap, reservedMountCollision } from "./workspace-cache.js";
@@ -46,6 +46,7 @@ import { sandboxForwardedEnvOf } from "./execution-config-root.js";
46
46
  import { SANDBOX_READ_FLOOR_DEFAULT, SANDBOX_READ_FLOOR_WORDS, laneMissingEnv, readDeclaredLaneTable } from "./execution-lane-caps.js";
47
47
  import { MEMORY_ENGINE_BACKENDS, SESSION_BACKENDS, STORE_BACKENDS } from "@sema-agent/settings-schema/execution-lanes";
48
48
  import { AUTONOMY_MODES } from "./runtime-governance.js";
49
+ import { CC_RULE_FORM } from "./entry-problems.js";
49
50
  import { deploymentRuleRefusal } from "./rule-issue-reading.js";
50
51
  import { MAX_NAMED_INERT_ENTRIES, allowLayerEntryCountIssue, describeInertRuleEntries, inertRuleEntriesOf, inertRuleEntryGuidance, ruleEntryListShapeOk } from "./rule-entry-list.js";
51
52
  import { readReadDenyEntry } from "./read-deny-entry.js";
@@ -494,6 +495,41 @@ function parseDeviceTrustedProxyCidrsEnv(raw) {
494
495
  `(e.g. 10.0.0.0/8,fd00::/8); unset it to read no proxy header at all (the cap is then keyed on the socket peer).`);
495
496
  });
496
497
  }
498
+ function sessionSnapshotPrefixKnob(prefix) {
499
+ const problem = sessionSnapshotPrefixProblem(prefix);
500
+ if (problem !== undefined)
501
+ throw new Error(`SESSION_SNAPSHOT_PREFIX=${JSON.stringify(prefix)} is refused: ${problem} — give each engine on the bucket a prefix that does not end in a domain segment (e.g. "engine-a/")`);
502
+ return prefix;
503
+ }
504
+ function presignLifetimeKnob(knob, sec, alternative) {
505
+ if (sec > SIGV4_PRESIGN_MAX_SEC) {
506
+ throw new Error(`${knob}=${sec} is longer than a presigned URL can live (at most ${SIGV4_PRESIGN_MAX_SEC} s (7 days)) — set ${knob} to at most ${SIGV4_PRESIGN_MAX_SEC}${alternative ? `, ${alternative}` : ""}`);
507
+ }
508
+ return sec;
509
+ }
510
+ function presignLifetimeSpelled(knob, raw, legal, alternative) {
511
+ const spelled = raw?.trim() ?? "";
512
+ if (spelled === "")
513
+ return undefined;
514
+ if (!/^(0|[1-9]\d*)$/.test(spelled)) {
515
+ const shown = raw.length > 40 ? `${raw.slice(0, 40)}…` : raw;
516
+ throw new Error(`${knob}=${JSON.stringify(shown)} is not ${legal}`);
517
+ }
518
+ return presignLifetimeKnob(knob, Number(spelled), alternative);
519
+ }
520
+ function sendUserFileUrlTtlKnob(raw) {
521
+ return (presignLifetimeSpelled("SEND_USER_FILE_URL_TTL", raw, `a link lifetime — set 0 (or leave it unset) for a permanent public link, or a whole number of seconds from 1 to ${SIGV4_PRESIGN_MAX_SEC} (7 days) for a private presigned link that expires`, "or 0 for a permanent link") ?? 0);
522
+ }
523
+ function sessionSnapshotTtlKnob(raw) {
524
+ const sec = presignLifetimeSpelled("SESSION_SNAPSHOT_TTL_SEC", raw, `a presigned-URL lifetime — leave it unset for the default, or set a whole number of seconds from 1 to ${SIGV4_PRESIGN_MAX_SEC} (7 days) in decimal digits`);
525
+ if (sec !== 0)
526
+ return sec;
527
+ CONFIG_WARNINGS.push({ env: "SESSION_SNAPSHOT_TTL_SEC", raw: raw });
528
+ return undefined;
529
+ }
530
+ function workspaceSnapshotTtlKnob(raw) {
531
+ return presignLifetimeSpelled("WORKSPACE_SNAPSHOT_TTL_SEC", raw, `a presigned-URL lifetime — leave it unset (or 0) for the default, or set a whole number of seconds from 1 to ${SIGV4_PRESIGN_MAX_SEC} (7 days) in decimal digits`);
532
+ }
497
533
  function parseFileHistoryRetention() {
498
534
  const raw = process.env.FILE_HISTORY_RETENTION_KEEP;
499
535
  if (raw === undefined || raw === "")
@@ -1036,8 +1072,8 @@ function parseStoreDomain(ctx) {
1036
1072
  accessKey: process.env.MINIO_ACCESS_KEY,
1037
1073
  secretKey: process.env.MINIO_SECRET_KEY,
1038
1074
  ...(process.env.MINIO_REGION ? { region: process.env.MINIO_REGION } : {}),
1039
- ...(process.env.SESSION_SNAPSHOT_PREFIX ? { keyPrefix: process.env.SESSION_SNAPSHOT_PREFIX } : {}),
1040
- ...((ttl) => (ttl !== undefined ? { presignTtlSec: ttl } : {}))(optFinitePositiveEnv("SESSION_SNAPSHOT_TTL_SEC")),
1075
+ ...(process.env.SESSION_SNAPSHOT_PREFIX ? { keyPrefix: sessionSnapshotPrefixKnob(process.env.SESSION_SNAPSHOT_PREFIX) } : {}),
1076
+ ...((ttl) => (ttl !== undefined ? { presignTtlSec: ttl } : {}))(sessionSnapshotTtlKnob(process.env.SESSION_SNAPSHOT_TTL_SEC)),
1041
1077
  }
1042
1078
  : undefined,
1043
1079
  snapshotBlobSqlMaxBytes: optFinitePositiveEnv("SNAPSHOT_BLOB_SQL_MAX_BYTES"),
@@ -1050,14 +1086,14 @@ function parseStoreDomain(ctx) {
1050
1086
  ...(process.env.S3_PUBLIC_ENDPOINT || process.env.S3_ENDPOINT
1051
1087
  ? { publicEndpoint: (process.env.S3_PUBLIC_ENDPOINT || process.env.S3_ENDPOINT) }
1052
1088
  : {}),
1053
- publicBucket: process.env.S3_PUBLIC_BUCKET ?? "sema-public",
1089
+ publicBucket: process.env.S3_PUBLIC_BUCKET ?? S3_PUBLIC_BUCKET_DEFAULT,
1054
1090
  privateBucket: process.env.SESSION_SNAPSHOT_BUCKET ?? SESSION_SNAPSHOT_BUCKET_DEFAULT,
1055
- privateKeyPrefix: "sendfile/",
1091
+ privateKeyPrefix: SENDFILE_PRIVATE_PREFIX,
1056
1092
  ...(process.env.SEND_USER_FILE_SANDBOX_PUT_ENDPOINT ? { sandboxPutEndpoint: process.env.SEND_USER_FILE_SANDBOX_PUT_ENDPOINT } : {}),
1057
1093
  accessKey: process.env.MINIO_ACCESS_KEY,
1058
1094
  secretKey: process.env.MINIO_SECRET_KEY,
1059
1095
  ...(process.env.MINIO_REGION ? { region: process.env.MINIO_REGION } : {}),
1060
- defaultTtlSec: ((v) => (v !== undefined && Number.isInteger(v) && v >= 0 ? v : 0))(process.env.SEND_USER_FILE_URL_TTL !== undefined ? Number(process.env.SEND_USER_FILE_URL_TTL) : undefined),
1096
+ defaultTtlSec: sendUserFileUrlTtlKnob(process.env.SEND_USER_FILE_URL_TTL),
1061
1097
  }
1062
1098
  : undefined,
1063
1099
  };
@@ -1363,7 +1399,7 @@ function findUnmatchableToolNames(names, opts) {
1363
1399
  if (opts.consumer === "tool-name-table") {
1364
1400
  const ccGuidance = ccRuleFormGuidance(name);
1365
1401
  if (ccGuidance !== undefined) {
1366
- out.push({ name, guidance: ccGuidance, reason: "cc_rule_form" });
1402
+ out.push({ name, guidance: ccGuidance, reason: CC_RULE_FORM });
1367
1403
  continue;
1368
1404
  }
1369
1405
  }
@@ -1419,7 +1455,6 @@ export function approvalGrantRefusal(names) {
1419
1455
  }
1420
1456
  function streamApprovalConfig(gates) {
1421
1457
  const cfg = {
1422
- enabled: boolEnv("STREAM_APPROVAL_ENABLED", true),
1423
1458
  windowMs: numEnv("STREAM_ASK_WINDOW_MS", gates.windowMsSource === "posture" ? "86400000" : "300000"),
1424
1459
  replayMax: numEnv("STREAM_APPROVAL_REPLAY_MAX", "50"),
1425
1460
  admitMaxPerTask: numEnv("STREAM_APPROVAL_ADMIT_MAX_PER_TASK", "32"),
@@ -1443,7 +1478,7 @@ function streamApprovalConfig(gates) {
1443
1478
  if (cfg.windowMs + cfg.adhocGraceMs >= cfg.orphanTtlMs) {
1444
1479
  throw new Error(`env STREAM_ASK_WINDOW_MS=${cfg.windowMs} + STREAM_APPROVAL_ADHOC_GRACE_MS=${cfg.adhocGraceMs} must be STRICTLY LESS than STREAM_APPROVAL_ORPHAN_TTL_MS=${cfg.orphanTtlMs} — the adhoc arm fires at (createdAt + window + grace) while the orphan backstop fires at (createdAt + ttl); if the backstop wins, every adhoc-leg row is attributed "orphan_ttl_exceeded" instead of "adhoc_leg_no_durable_domain" (attribution honesty, design/172 §6.1)`);
1445
1480
  }
1446
- const suspendedRowsPossible = cfg.enabled && gates.toolApprovalEnabled && gates.unattendedPolicy !== "deny";
1481
+ const suspendedRowsPossible = gates.toolApprovalEnabled && gates.unattendedPolicy !== "deny";
1447
1482
  if (suspendedRowsPossible && cfg.unreachedTtlMs + cfg.adhocGraceMs >= cfg.orphanTtlMs) {
1448
1483
  throw new Error(`env UNREACHED_ASK_TTL_MS=${cfg.unreachedTtlMs} + STREAM_APPROVAL_ADHOC_GRACE_MS=${cfg.adhocGraceMs} must be STRICTLY LESS than STREAM_APPROVAL_ORPHAN_TTL_MS=${cfg.orphanTtlMs} — an ask minted with NO live stream (case A: a workflow child ask) waits UNREACHED_ASK_TTL_MS, so its row expires at (createdAt + that window); the orphan backstop fires at (createdAt + ttl). If the backstop wins, every suspended row is attributed "orphan_ttl_exceeded" instead of its real cause (attribution honesty, same rule as the STREAM_ASK_WINDOW_MS line above)`);
1449
1484
  }
@@ -2021,7 +2056,6 @@ function parseAuthDomain(ctx) {
2021
2056
  const authTokens = parseServiceAuthTokens(process.env.SERVICE_AUTH_TOKENS);
2022
2057
  return {
2023
2058
  bindHost: process.env.BIND_HOST || undefined,
2024
- bindHostSource: process.env.BIND_HOST ? "BIND_HOST" : undefined,
2025
2059
  authToken: process.env.SERVICE_AUTH_TOKEN,
2026
2060
  authTokens,
2027
2061
  allowUnauthedWrites: boolEnv("ALLOW_UNAUTHED_WRITES", false),
@@ -2031,6 +2065,7 @@ function parseAuthDomain(ctx) {
2031
2065
  autonomy: parseAutonomy(process.env.AUTONOMY),
2032
2066
  ...parseGovernanceDeclarations(),
2033
2067
  operatorPrincipals: csv("OPERATOR_PRINCIPALS"),
2068
+ operatorDrainPrincipals: csv("OPERATOR_DRAIN_PRINCIPALS"),
2034
2069
  operatorSingleUser: boolEnv("OPERATOR_SINGLE_USER", false),
2035
2070
  serviceTokenActsForDeployment: boolEnv("SERVICE_TOKEN_ACTS_FOR_DEPLOYMENT", true),
2036
2071
  configCatalogEnabled: boolEnv("CONFIG_CATALOG_ENABLED", true),
@@ -2662,8 +2697,8 @@ const MEMORY_GROUP_KEYS = [
2662
2697
  const AUTH_GROUP_KEYS = [
2663
2698
  "authToken", "authTokens", "allowUnauthedWrites", "corsOrigins", "principalHeader", "requirePrincipal", "autonomy",
2664
2699
  "commandPolicy", "compliancePosture", "lockedConfigKeys", "retentionPolicy", "retentionSweep",
2665
- "operatorPrincipals", "operatorSingleUser", "serviceTokenActsForDeployment", "configCatalogEnabled", "principalJwtPubkeys", "principalJwtIss", "principalJwtAud",
2666
- "principalJwtMaxTtlSec", "authBridgeIssuer", "bindHost", "bindHostSource",
2700
+ "operatorPrincipals", "operatorDrainPrincipals", "operatorSingleUser", "serviceTokenActsForDeployment", "configCatalogEnabled", "principalJwtPubkeys", "principalJwtIss", "principalJwtAud",
2701
+ "principalJwtMaxTtlSec", "authBridgeIssuer", "bindHost",
2667
2702
  ];
2668
2703
  const ORCHESTRATION_GROUP_KEYS = [
2669
2704
  "sandboxGit", "remoteExec", "remoteExecHomeDir", "worktreeIsolation", "leaderEnabled", "leaderFanoutEnabled", "routerEnabled", "selfOrchestrationEnabled",
@@ -2761,7 +2796,7 @@ function k8sWorkspaceStore() {
2761
2796
  secretKey,
2762
2797
  ...(process.env.MINIO_REGION ? { region: process.env.MINIO_REGION } : {}),
2763
2798
  ...(keyPrefix !== undefined ? { keyPrefix } : {}),
2764
- ...((n) => (n !== undefined ? { presignTtlSec: n } : {}))(optNumEnvStrict("WORKSPACE_SNAPSHOT_TTL_SEC")),
2799
+ ...((n) => (n !== undefined ? { presignTtlSec: n } : {}))(workspaceSnapshotTtlKnob(process.env.WORKSPACE_SNAPSHOT_TTL_SEC)),
2765
2800
  },
2766
2801
  };
2767
2802
  }
@@ -2836,7 +2871,7 @@ export function loadConfig() {
2836
2871
  throw new Error(retentionRefusal);
2837
2872
  }
2838
2873
  const { directApprovalDoor, durableApproval: dgDurable, approvalHmacKeys: dgHmacKeys } = approval;
2839
- const { principalJwtPubkeys, principalJwtIss, principalJwtAud, operatorPrincipals: dgOperators, operatorSingleUser, authTokens } = auth;
2874
+ const { principalJwtPubkeys, principalJwtIss, principalJwtAud, operatorPrincipals: dgOperators, operatorDrainPrincipals, operatorSingleUser, authTokens } = auth;
2840
2875
  const directDoorActive = directApprovalDoor && dgDurable && dgHmacKeys.length > 0 && principalJwtPubkeys.length > 0 && !!principalJwtIss && !!principalJwtAud;
2841
2876
  const doorRefusal = halfOpenDirectDoorRefusal({
2842
2877
  directApprovalDoor,
@@ -2855,10 +2890,14 @@ export function loadConfig() {
2855
2890
  if (operatorsRefusal !== undefined) {
2856
2891
  throw new Error(operatorsRefusal);
2857
2892
  }
2858
- const singleUserOperatorRefusal = operatorSingleUserRefusal({ operatorSingleUser, requirePrincipal, operatorPrincipals: dgOperators });
2893
+ const singleUserOperatorRefusal = operatorSingleUserRefusal({ operatorSingleUser, requirePrincipal, operatorPrincipals: dgOperators, operatorDrainPrincipals });
2859
2894
  if (singleUserOperatorRefusal !== undefined) {
2860
2895
  throw new Error(singleUserOperatorRefusal);
2861
2896
  }
2897
+ const rosterRefusal = operatorRosterWellFormedRefusal({ operatorPrincipals: dgOperators, operatorDrainPrincipals });
2898
+ if (rosterRefusal !== undefined) {
2899
+ throw new Error(rosterRefusal);
2900
+ }
2862
2901
  const bakeOperatorsRefusal = bakeOperatorPrincipalsRefusal({ imageBakes: orchestration.imageBakes, operatorPrincipals: dgOperators });
2863
2902
  if (bakeOperatorsRefusal !== undefined) {
2864
2903
  throw new Error(bakeOperatorsRefusal);
@@ -0,0 +1,51 @@
1
+ import type { Server } from "node:http";
2
+ /** 开不了口的原因词(闭集;`listening` 行的 `controlSocketUnavailable` 取值;顺序 = 开口序)。 */
3
+ export declare const CONTROL_SOCKET_UNAVAILABLE_REASONS: readonly ["unsupported_platform", "data_root_not_exclusive", "path_too_long", "dir_not_private", "path_occupied", "bind_failed"];
4
+ export type ControlSocketUnavailableReason = (typeof CONTROL_SOCKET_UNAVAILABLE_REASONS)[number];
5
+ /** 开口的结局:在听(绝对路径)或缺席(原因词)—— 恰一形。 */
6
+ export type ControlSocketOpening = {
7
+ readonly listening: true;
8
+ readonly path: string;
9
+ } | {
10
+ readonly listening: false;
11
+ readonly reason: ControlSocketUnavailableReason;
12
+ };
13
+ /** 数据根(锁所守的物理路径)→ 控制 socket 的绝对路径。唯一推导:`listening` 行报的、开口判的、附着方连的是同一只。 */
14
+ export declare function controlSocketPathOf(physicalDataRoot: string): string;
15
+ /** 私有判的读数(`lstat`,不跟符号链接)。`absent` = 不在;`unreadable` = 读不出(按不私有处置)。 */
16
+ export type ControlPathReading = {
17
+ readonly kind: "absent";
18
+ } | {
19
+ readonly kind: "unreadable";
20
+ } | {
21
+ readonly kind: "directory" | "socket" | "symlink" | "other";
22
+ readonly uid: number;
23
+ readonly mode: number;
24
+ };
25
+ /** 读一只路径(`lstat`)。不在 ⇒ `absent`;别的读错 ⇒ `unreadable`。 */
26
+ export declare function readControlPath(path: string): ControlPathReading;
27
+ /** `control/` 私有吗:是目录(不是符号链接)∧ 属主 = `euid` ∧ 无组 / 他人位。任何别的读数 ⇒ 否。 */
28
+ export declare function controlDirIsPrivate(reading: ControlPathReading, euid: number): boolean;
29
+ /** `engine.sock` 是不是本用户的 socket:是 socket(不是符号链接)∧ 属主 = `euid`。任何别的读数 ⇒ 否。 */
30
+ export declare function controlSocketIsOwned(reading: ControlPathReading, euid: number): boolean;
31
+ export interface ControlSocketOpenOptions {
32
+ /** 本进程持锁独占的物理数据根(`StoreBackend.exclusiveDataRoot()`);缺席 ⇒ 不开口。 */
33
+ readonly exclusiveDataRoot: string | undefined;
34
+ /** 平台读数(缺省 = 全仓唯一平台读点 `HOST_PLATFORM`)。 */
35
+ readonly platform?: string;
36
+ /** 属主判对照的 euid(缺省 `process.geteuid()`)。 */
37
+ readonly euid?: number;
38
+ }
39
+ /**
40
+ * 在 `server`(控制口那只 `http.Server`,`createHttpServer` 交出的 `controlSocketServer`)上开控制 socket —— 判序见头注。
41
+ * 结局恰一形;`listening: false` 时 `server` 不在听(绑上又判不过的那一形已先关)。调用点:TCP listen 成功之后、`listening` 行之前(起服第 10 段)。
42
+ */
43
+ export declare function openControlSocket(server: Server, opts: ControlSocketOpenOptions): Promise<ControlSocketOpening>;
44
+ /** `listening` 行的两键 —— 同一次开口结局的投影,恰一键在场(`controlSocket` ⟺ 在听;`controlSocketUnavailable` ⟺ 缺席)。 */
45
+ export interface ControlSocketLineKeys {
46
+ readonly controlSocket?: string;
47
+ readonly controlSocketUnavailable?: ControlSocketUnavailableReason;
48
+ }
49
+ /** 开口结局 → `listening` 行两键(唯一投影)。 */
50
+ export declare function controlSocketLineKeysOf(opening: ControlSocketOpening): ControlSocketLineKeys;
51
+ //# sourceMappingURL=control-socket.d.ts.map
@@ -0,0 +1,115 @@
1
+ import { lstatSync, unlinkSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { ensureDir } from "@sema-agent/core";
4
+ import { HOST_PLATFORM } from "./plugins/host-platform.js";
5
+ export const CONTROL_SOCKET_UNAVAILABLE_REASONS = ["unsupported_platform", "data_root_not_exclusive", "path_too_long", "dir_not_private", "path_occupied", "bind_failed"];
6
+ const SUN_PATH_MAX_BYTES = {
7
+ darwin: 104,
8
+ linux: 107,
9
+ aix: "unsupported",
10
+ android: "unsupported",
11
+ freebsd: "unsupported",
12
+ haiku: "unsupported",
13
+ openbsd: "unsupported",
14
+ sunos: "unsupported",
15
+ win32: "unsupported",
16
+ cygwin: "unsupported",
17
+ netbsd: "unsupported",
18
+ };
19
+ export function controlSocketPathOf(physicalDataRoot) {
20
+ return join(physicalDataRoot, "control", "engine.sock");
21
+ }
22
+ function readingOf(stats) {
23
+ const kind = stats.isSymbolicLink() ? "symlink" : stats.isDirectory() ? "directory" : stats.isSocket() ? "socket" : "other";
24
+ return { kind, uid: stats.uid, mode: stats.mode };
25
+ }
26
+ export function readControlPath(path) {
27
+ try {
28
+ return readingOf(lstatSync(path));
29
+ }
30
+ catch (err) {
31
+ return err.code === "ENOENT" ? { kind: "absent" } : { kind: "unreadable" };
32
+ }
33
+ }
34
+ export function controlDirIsPrivate(reading, euid) {
35
+ return reading.kind === "directory" && reading.uid === euid && (reading.mode & 0o077) === 0;
36
+ }
37
+ export function controlSocketIsOwned(reading, euid) {
38
+ return reading.kind === "socket" && reading.uid === euid;
39
+ }
40
+ function sunPathMaxBytesOf(platform) {
41
+ if (!Object.hasOwn(SUN_PATH_MAX_BYTES, platform))
42
+ return undefined;
43
+ const limit = SUN_PATH_MAX_BYTES[platform];
44
+ return typeof limit === "number" ? limit : undefined;
45
+ }
46
+ const unavailable = (reason) => ({ listening: false, reason });
47
+ async function ioStep(act) {
48
+ try {
49
+ await act();
50
+ return { ok: true };
51
+ }
52
+ catch (error) {
53
+ return { ok: false, error };
54
+ }
55
+ }
56
+ function listenOn(server, path) {
57
+ return new Promise((resolve, reject) => {
58
+ const onError = (err) => reject(err);
59
+ server.once("error", onError);
60
+ server.listen(path, () => {
61
+ server.removeListener("error", onError);
62
+ resolve();
63
+ });
64
+ });
65
+ }
66
+ export async function openControlSocket(server, opts) {
67
+ const maxBytes = sunPathMaxBytesOf(opts.platform ?? HOST_PLATFORM);
68
+ if (maxBytes === undefined)
69
+ return unavailable("unsupported_platform");
70
+ const euid = opts.euid ?? process.geteuid?.();
71
+ if (euid === undefined)
72
+ return unavailable("unsupported_platform");
73
+ const root = opts.exclusiveDataRoot;
74
+ if (root === undefined)
75
+ return unavailable("data_root_not_exclusive");
76
+ const path = controlSocketPathOf(root);
77
+ if (Buffer.byteLength(path, "utf8") > maxBytes)
78
+ return unavailable("path_too_long");
79
+ const dir = dirname(path);
80
+ await ioStep(() => ensureDir(dir));
81
+ if (!controlDirIsPrivate(readControlPath(dir), euid))
82
+ return unavailable("dir_not_private");
83
+ const existing = readControlPath(path);
84
+ switch (existing.kind) {
85
+ case "absent":
86
+ break;
87
+ case "socket":
88
+ if (!controlSocketIsOwned(existing, euid))
89
+ return unavailable("path_occupied");
90
+ if (!(await ioStep(() => unlinkSync(path))).ok)
91
+ return unavailable("bind_failed");
92
+ break;
93
+ case "directory":
94
+ case "symlink":
95
+ case "other":
96
+ return unavailable("path_occupied");
97
+ case "unreadable":
98
+ return unavailable("bind_failed");
99
+ default: {
100
+ const unknownReading = existing;
101
+ throw new Error(`control socket: unknown path reading ${JSON.stringify(unknownReading)}`);
102
+ }
103
+ }
104
+ if (!(await ioStep(() => listenOn(server, path))).ok)
105
+ return unavailable("bind_failed");
106
+ if (!controlSocketIsOwned(readControlPath(path), euid) || !controlDirIsPrivate(readControlPath(dir), euid)) {
107
+ await new Promise((resolve) => server.close(() => resolve()));
108
+ return unavailable("bind_failed");
109
+ }
110
+ return { listening: true, path };
111
+ }
112
+ export function controlSocketLineKeysOf(opening) {
113
+ return opening.listening ? { controlSocket: opening.path } : { controlSocketUnavailable: opening.reason };
114
+ }
115
+ //# sourceMappingURL=control-socket.js.map
@@ -64,7 +64,8 @@ export interface PathAdjudication {
64
64
  * · `durable` —— checkpoint suspend/resume 服 ask(HTTP 腿 durable 开、park 赎回腿)。core 的问答策略
65
65
  * (`createDurableQuestionPolicy()`,原形逐字)**只**叠在这一形上:它对引擎的问题工具答 `ask`,落点归 core 的同步优先
66
66
  * (有问题席 ⇒ 先投卡、席在窗内拿到答即交答;没拿到 ⇒ 停驻 —— 卡由活面呈出而窗走完的那一问除外:席抛,引擎的投递失败臂不停驻)。
67
- * · `live` —— Runner 的活 onAsk 席当场服 ask(run-local 的 `createLocalApprover`:TTY 问 y/N / 无 TTY 当场拒;
67
+ * · `live` —— Runner 的活 onAsk 席当场服 ask(run-local 的 `createLocalApprover`:TTY 问 y/N / 无 TTY 当场拒;leader 车道不耐久的腿
68
+ * (`leader/leader.ts` `leaderLegDurability` 判为不耐久:各修复腿、没递停驻设施或续跑上下文没落的 worker)的无人值守层:答集内的类按策略答,其余交回 core 的缺席路 —— 这条腿没有停驻设施 ⇒ 拒;
68
69
  * core `isLiveApproverSeat(onAsk)` 为真时 `suspendAsk` 早退,ask 落回调)。三张名单与 durable 席**同一只**策略。
69
70
  * · `absent` —— 没有谁服 ask ⇒ adjudicated allow-all;**只**服务零名单(HTTP 腿「durable 关 ∧ 单用户零意图」)。
70
71
  * 带名单进来 = 响亮拒:名单被一条没有席的腿静默吞掉,正是 [ref] 的病形。
@@ -40,6 +40,7 @@ import { type DeviceOwner, type DeviceStore } from "./device-store.js";
40
40
  import type { DeviceEnrollment, DeviceRejection } from "./device-enrollment.js";
41
41
  import { type CidrSet } from "./net-address.js";
42
42
  import { type DeviceChunkKind, type DeviceInstructionArgsByKind, type DeviceInstructionKind, type DeviceInstructionOutcome } from "./device-ws-protocol.js";
43
+ import type { NonPersonTier } from "./operator-roster.js";
43
44
  /** 交给调用方的一片上行流(`data` 已从 base64 解回字节)。 */
44
45
  export interface DeviceChunkDelivery {
45
46
  streamSeq: number;
@@ -129,6 +130,12 @@ export interface DeviceWsHubOptions {
129
130
  enrollment: DeviceEnrollment;
130
131
  /** 本副本 id(写进连接租约行,跨副本排障用)。 */
131
132
  replicaId: string;
133
+ /**
134
+ * hello 准入的属主圈禁读者:这一台设备行的属主此刻落在名册的哪一档**非人档**(圈禁,`undefined` = 不圈禁)。答「是」⇒ `helloReject: auth_failed`,不夺租约、不顶替旧连接。
135
+ * 汇聚端自己不 import 名册、不判档位:判官只有一只(`http/principal-gate.ts` 的 `confinedTierOf`,设备读点 `deviceOwnerConfinedTier`),装配方(`boot/device-lane.ts`)把它连进来。
136
+ * **必填**:可选会让漏接静默(没接 = 不圈禁,安全轴上的静默放宽)—— 漏接编译红。每次 hello 现读(名册是现态,不拍快照);读者抛 ⇒ 握手失败、连接关(`device_hello_failed`),不放行。
137
+ */
138
+ ownerConfinedTier: (owner: DeviceOwner) => NonPersonTier | undefined;
132
139
  logger?: DeviceWsHubLogger;
133
140
  metrics?: {
134
141
  inc(name: string, labels?: Record<string, string>): void;
@@ -121,7 +121,7 @@ function forwardedHopAddress(hop) {
121
121
  return canonicalAddress(t);
122
122
  }
123
123
  export function createDeviceWsHub(options) {
124
- const { store, enrollment, replicaId, logger, metrics } = options;
124
+ const { store, enrollment, replicaId, ownerConfinedTier, logger, metrics } = options;
125
125
  const heartbeatIntervalMs = options.heartbeatIntervalMs ?? 15_000;
126
126
  const heartbeatMisses = options.heartbeatMisses ?? 3;
127
127
  const leaseSeconds = options.leaseSeconds ?? 45;
@@ -438,6 +438,12 @@ export function createDeviceWsHub(options) {
438
438
  }
439
439
  if (row.status === "revoked")
440
440
  return rejectHello(conn, "revoked");
441
+ const confined = ownerConfinedTier(row.owner);
442
+ if (confined !== undefined) {
443
+ logger?.warn("device_hello_owner_confined", { deviceId: row.deviceId, tier: confined });
444
+ metrics?.inc("device_hello_rejected_total", { code: "auth_failed" });
445
+ return rejectHello(conn, "auth_failed");
446
+ }
441
447
  const existing = conns.get(row.deviceId);
442
448
  if (existing && existing !== conn) {
443
449
  const fresh = Date.now() - existing.lastFrameAt < heartbeatIntervalMs * heartbeatMisses;
@@ -6,7 +6,7 @@ import { refusalBody } from "./http/refusal-body.js";
6
6
  import { keyAdmits, ownerCellOf, recoveredSubject } from "./owner-faces.js";
7
7
  import { createHitlOpenRace } from "./hitl-delivery.js";
8
8
  const MAX_ELICIT_MESSAGE_CHARS = 4096;
9
- const MAX_ELICIT_SCHEMA_BYTES = 8192;
9
+ const MAX_ELICIT_SCHEMA_CHARS = 8192;
10
10
  const RELEASE_ACTION = { refused: "decline", unanswered: "cancel" };
11
11
  export const DEFAULT_ELICITATION_THROTTLE = {
12
12
  maxConcurrentPerRun: 2,
@@ -42,7 +42,7 @@ function boundSchema(schema) {
42
42
  return undefined;
43
43
  const redacted = redactDeep(schema);
44
44
  try {
45
- if (JSON.stringify(redacted).length > MAX_ELICIT_SCHEMA_BYTES)
45
+ if (JSON.stringify(redacted).length > MAX_ELICIT_SCHEMA_CHARS)
46
46
  return undefined;
47
47
  }
48
48
  catch {