@sema-agent/server 7.148.0 → 7.150.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 (360) hide show
  1. package/MIGRATION.md +551 -9
  2. package/README.md +17 -2
  3. package/README.zh-CN.md +5 -1
  4. package/USAGE.md +126 -68
  5. package/dist/adoption/wire.d.ts +3 -3
  6. package/dist/approval-card.d.ts +33 -33
  7. package/dist/approval-gate-currency.d.ts +1 -1
  8. package/dist/approval-leg-origin.d.ts +33 -0
  9. package/dist/approval-leg-origin.js +4 -0
  10. package/dist/audit.d.ts +1 -1
  11. package/dist/audit.js +2 -2
  12. package/dist/auth-bridge.d.ts +9 -1
  13. package/dist/auth-bridge.js +4 -4
  14. package/dist/auth-keys.d.ts +10 -1
  15. package/dist/auth-keys.js +8 -0
  16. package/dist/background-session-reap.d.ts +1 -1
  17. package/dist/boot/deferred-sandbox-path-env.d.ts +5 -3
  18. package/dist/boot/deferred-sandbox-path-env.js +1 -2
  19. package/dist/boot/execution-env.js +10 -11
  20. package/dist/boot/execution-lane.d.ts +6 -4
  21. package/dist/boot/execution-lane.js +3 -1
  22. package/dist/boot/org-delivery.d.ts +3 -2
  23. package/dist/boot/parked-revive-gate.d.ts +5 -5
  24. package/dist/boot/parked-revive-gate.js +2 -3
  25. package/dist/boot/resolve-spec.d.ts +4 -5
  26. package/dist/boot/resolve-spec.js +66 -28
  27. package/dist/boot/retention-lane.d.ts +1 -1
  28. package/dist/boot/runner-deps.js +3 -1
  29. package/dist/boot/session-faces.js +21 -21
  30. package/dist/boot/stage-01-config.d.ts +1 -0
  31. package/dist/boot/stage-01-config.js +4 -3
  32. package/dist/boot/stage-02-web-admin-pull.d.ts +1 -0
  33. package/dist/boot/stage-02-web-admin-pull.js +10 -2
  34. package/dist/boot/stage-03-store-backend.d.ts +1 -0
  35. package/dist/boot/stage-03-store-backend.js +4 -4
  36. package/dist/boot/stage-04-stores.d.ts +1 -0
  37. package/dist/boot/stage-05-execution-env.d.ts +1 -0
  38. package/dist/boot/stage-06-runners.d.ts +2 -1
  39. package/dist/boot/stage-06-runners.js +1 -2
  40. package/dist/boot/stage-07-capability-layer.d.ts +2 -1
  41. package/dist/boot/stage-07-capability-layer.js +1 -1
  42. package/dist/boot/stage-08-reapers.d.ts +2 -1
  43. package/dist/boot/stage-09-leader.d.ts +2 -1
  44. package/dist/boot/stage-10-http-server.d.ts +35 -2
  45. package/dist/boot/stage-10-http-server.js +9 -4
  46. package/dist/boot/stores.d.ts +3 -0
  47. package/dist/boot/stores.js +22 -19
  48. package/dist/boot/web-admin.d.ts +1 -1
  49. package/dist/capabilities/tool-defer.d.ts +1 -1
  50. package/dist/capability-facts.d.ts +70 -60
  51. package/dist/capability-facts.js +45 -49
  52. package/dist/card-settle-stored-scope.d.ts +56 -0
  53. package/dist/card-settle-stored-scope.js +27 -0
  54. package/dist/card-void.d.ts +7 -12
  55. package/dist/card-void.js +2 -7
  56. package/dist/config-catalog.d.ts +12 -0
  57. package/dist/config-catalog.js +37 -33
  58. package/dist/config-invariants.d.ts +5 -4
  59. package/dist/config-invariants.js +4 -4
  60. package/dist/config-types.d.ts +49 -16
  61. package/dist/config.d.ts +6 -5
  62. package/dist/config.js +35 -24
  63. package/dist/context-window-notice.d.ts +31 -0
  64. package/dist/context-window-notice.js +28 -0
  65. package/dist/deployment-governance.d.ts +7 -28
  66. package/dist/deployment-governance.js +1 -14
  67. package/dist/deployment-shape.d.ts +20 -11
  68. package/dist/deployment-shape.js +1 -0
  69. package/dist/device-ws-hub.d.ts +1 -0
  70. package/dist/device-ws-hub.js +64 -28
  71. package/dist/device-ws-protocol.d.ts +20 -6
  72. package/dist/device-ws-protocol.js +9 -2
  73. package/dist/engine-identity.d.ts +7 -1
  74. package/dist/engine-identity.js +5 -0
  75. package/dist/env-name-allowlist-knobs.d.ts +6 -5
  76. package/dist/env-name-allowlist-knobs.js +5 -0
  77. package/dist/execution-lane-caps.d.ts +20 -5
  78. package/dist/execution-lane-caps.js +18 -4
  79. package/dist/field-conflict.d.ts +2 -2
  80. package/dist/fleet/engine-signal.d.ts +14 -1
  81. package/dist/fleet/engine-signal.js +5 -1
  82. package/dist/fleet/fleet-reconciler.d.ts +1 -1
  83. package/dist/fleet/serving-lease.d.ts +18 -3
  84. package/dist/fleet/serving-lease.js +81 -35
  85. package/dist/fleet-client.d.ts +13 -11
  86. package/dist/fleet-client.js +53 -33
  87. package/dist/history-anchor.js +1 -1
  88. package/dist/hitl-delivery.d.ts +1 -3
  89. package/dist/hitl-delivery.js +1 -1
  90. package/dist/hooks/command-spawn.d.ts +7 -2
  91. package/dist/hooks/command-spawn.js +13 -6
  92. package/dist/hooks/hook-runner.d.ts +5 -0
  93. package/dist/hooks/hook-runner.js +11 -7
  94. package/dist/http/active-run-conflict.d.ts +3 -7
  95. package/dist/http/active-run-conflict.js +2 -2
  96. package/dist/http/admission.js +5 -5
  97. package/dist/http/approval-sweeps.js +5 -3
  98. package/dist/http/caller-trust.d.ts +2 -2
  99. package/dist/http/caller-trust.js +3 -3
  100. package/dist/http/checkpoint-ctx.d.ts +18 -63
  101. package/dist/http/checkpoint-ctx.js +1 -24
  102. package/dist/http/claim-heartbeat.d.ts +6 -6
  103. package/dist/http/claim-heartbeat.js +6 -6
  104. package/dist/http/claimed-run.d.ts +16 -9
  105. package/dist/http/claimed-run.js +8 -0
  106. package/dist/http/dispatch.js +7 -5
  107. package/dist/http/durable-run-leg.d.ts +8 -1
  108. package/dist/http/durable-run-leg.js +34 -15
  109. package/dist/http/execution-context.d.ts +115 -14
  110. package/dist/http/execution-context.js +130 -40
  111. package/dist/http/owner-access.d.ts +246 -21
  112. package/dist/http/owner-access.js +186 -8
  113. package/dist/http/principal-gate.d.ts +18 -11
  114. package/dist/http/principal-gate.js +3 -1
  115. package/dist/http/readiness.d.ts +4 -2
  116. package/dist/http/resume-ctx-read.d.ts +14 -3
  117. package/dist/http/resume-ctx-read.js +24 -3
  118. package/dist/http/resume-legs.d.ts +35 -3
  119. package/dist/http/resume-legs.js +262 -106
  120. package/dist/http/route-ctx.d.ts +29 -11
  121. package/dist/http/routes/a2a-serve.js +17 -14
  122. package/dist/http/routes/approvals-assistant.d.ts +3 -2
  123. package/dist/http/routes/approvals-assistant.js +11 -11
  124. package/dist/http/routes/capabilities.js +5 -4
  125. package/dist/http/routes/config-catalog.d.ts +1 -1
  126. package/dist/http/routes/diagnostics.d.ts +3 -1
  127. package/dist/http/routes/diagnostics.js +30 -5
  128. package/dist/http/routes/mcp-reconnect.js +2 -2
  129. package/dist/http/routes/memory-origin.d.ts +1 -1
  130. package/dist/http/routes/notify-wake.js +2 -3
  131. package/dist/http/routes/principal-inbox.d.ts +3 -1
  132. package/dist/http/routes/principal-inbox.js +3 -2
  133. package/dist/http/routes/projects.d.ts +1 -0
  134. package/dist/http/routes/projects.js +5 -3
  135. package/dist/http/routes/retention-ops.d.ts +1 -1
  136. package/dist/http/routes/run-permission-mode.d.ts +1 -1
  137. package/dist/http/routes/run-permission-mode.js +2 -2
  138. package/dist/http/routes/runs.d.ts +1 -1
  139. package/dist/http/routes/runs.js +33 -37
  140. package/dist/http/routes/session-memory-sync.js +4 -5
  141. package/dist/http/routes/session-sync.d.ts +24 -5
  142. package/dist/http/routes/session-sync.js +45 -37
  143. package/dist/http/routes/sessions-list.d.ts +1 -1
  144. package/dist/http/routes/sessions.d.ts +1 -1
  145. package/dist/http/routes/sessions.js +18 -44
  146. package/dist/http/routes/tasks.js +136 -101
  147. package/dist/http/routes/trace-usage.js +14 -14
  148. package/dist/http/routes/workflows.d.ts +1 -0
  149. package/dist/http/routes/workflows.js +20 -38
  150. package/dist/http/server.d.ts +12 -9
  151. package/dist/http/server.js +35 -6
  152. package/dist/http/stored-body-replay.d.ts +3 -24
  153. package/dist/http/stored-body-replay.js +2 -12
  154. package/dist/http/submit-ctx.d.ts +2 -2
  155. package/dist/http/submit-ctx.js +5 -5
  156. package/dist/http/submit-replay.d.ts +15 -2
  157. package/dist/http/submit-replay.js +10 -4
  158. package/dist/http/wire-types.d.ts +6 -3
  159. package/dist/index.d.ts +3 -1
  160. package/dist/index.js +1 -0
  161. package/dist/leader/endpoint.js +9 -8
  162. package/dist/leg-facts.d.ts +13 -2
  163. package/dist/leg-facts.js +2 -0
  164. package/dist/local-store-declaration.d.ts +54 -0
  165. package/dist/local-store-declaration.js +59 -0
  166. package/dist/mcp-assertion-header.d.ts +1 -1
  167. package/dist/mcp-assertion-header.js +1 -1
  168. package/dist/mcp-authorization-face.d.ts +18 -14
  169. package/dist/mcp-authorization-face.js +3 -2
  170. package/dist/memory-project-layer.js +1 -1
  171. package/dist/memory-scope.d.ts +4 -4
  172. package/dist/memory-scope.js +4 -4
  173. package/dist/memory-store-refusal.d.ts +1 -1
  174. package/dist/memory-sync-client.js +2 -2
  175. package/dist/model-context-window.d.ts +50 -11
  176. package/dist/model-context-window.js +20 -1
  177. package/dist/model-entry-refusal.d.ts +5 -4
  178. package/dist/model-entry-refusal.js +17 -3
  179. package/dist/model-reach-scope.d.ts +2 -1
  180. package/dist/observability/corrupt-read-seat.d.ts +1 -1
  181. package/dist/observability/fail-open.d.ts +19 -7
  182. package/dist/observability/fail-open.js +19 -7
  183. package/dist/observability/logger.d.ts +7 -0
  184. package/dist/observability/logger.js +3 -1
  185. package/dist/observability/metrics.js +1 -1
  186. package/dist/observability/run-terminal.d.ts +1 -1
  187. package/dist/observability/run-terminal.js +1 -0
  188. package/dist/observability/secret-env-scrub.d.ts +1 -1
  189. package/dist/observability/secret-env-scrub.js +1 -1
  190. package/dist/org-delivery-gate.d.ts +31 -6
  191. package/dist/org-delivery-gate.js +7 -4
  192. package/dist/owner-faces.d.ts +30 -5
  193. package/dist/owner-faces.js +2 -0
  194. package/dist/park-row-commit.d.ts +67 -0
  195. package/dist/park-row-commit.js +76 -0
  196. package/dist/parked-decide.d.ts +9 -7
  197. package/dist/parked-decide.js +25 -6
  198. package/dist/peer-directory.d.ts +1 -1
  199. package/dist/peer-seat.d.ts +1 -1
  200. package/dist/peer-seat.js +52 -59
  201. package/dist/plugins/adoption-log-sql.d.ts +3 -3
  202. package/dist/plugins/adoption-log-sql.js +19 -19
  203. package/dist/plugins/approval-ask-store-sql.d.ts +1 -1
  204. package/dist/plugins/approval-ask-store-sql.js +3 -5
  205. package/dist/plugins/approval-exemption-skill-cutoff.d.ts +64 -0
  206. package/dist/plugins/approval-exemption-skill-cutoff.js +122 -0
  207. package/dist/plugins/approval-exemption-skill-m46.d.ts +0 -6
  208. package/dist/plugins/approval-exemption-skill-m46.js +6 -10
  209. package/dist/plugins/approval-exemption-store.d.ts +26 -1
  210. package/dist/plugins/approval-exemption-store.js +28 -19
  211. package/dist/plugins/ensure-schema-lock-policy.d.ts +36 -3
  212. package/dist/plugins/ensure-schema-lock-policy.js +75 -9
  213. package/dist/plugins/exec-path-law.d.ts +34 -0
  214. package/dist/plugins/exec-path-law.js +13 -0
  215. package/dist/plugins/file-run-store.d.ts +22 -9
  216. package/dist/plugins/file-run-store.js +51 -23
  217. package/dist/plugins/local-checkpoint-store.d.ts +8 -1
  218. package/dist/plugins/local-checkpoint-store.js +5 -1
  219. package/dist/plugins/local-data-root-admission.d.ts +52 -0
  220. package/dist/plugins/local-data-root-admission.js +100 -0
  221. package/dist/plugins/local-engine-high-water.d.ts +28 -7
  222. package/dist/plugins/local-engine-high-water.js +27 -22
  223. package/dist/plugins/local-session-store.d.ts +134 -49
  224. package/dist/plugins/local-session-store.js +149 -108
  225. package/dist/plugins/memory-run-store.d.ts +14 -9
  226. package/dist/plugins/memory-run-store.js +48 -22
  227. package/dist/plugins/one-time-migration-marker.d.ts +19 -3
  228. package/dist/plugins/one-time-migration-marker.js +7 -1
  229. package/dist/plugins/one-time-migrations.d.ts +110 -59
  230. package/dist/plugins/one-time-migrations.js +224 -136
  231. package/dist/plugins/org-delivery-quota-store.d.ts +34 -11
  232. package/dist/plugins/org-delivery-quota-store.js +29 -14
  233. package/dist/plugins/permission-rule-store-sql.d.ts +1 -1
  234. package/dist/plugins/permission-rule-store-sql.js +5 -9
  235. package/dist/plugins/pg-pool.d.ts +1 -1
  236. package/dist/plugins/pg-pool.js +14 -12
  237. package/dist/plugins/posix-shell-fs.js +4 -2
  238. package/dist/plugins/remote-env-adb.js +3 -2
  239. package/dist/plugins/remote-env-device.d.ts +3 -2
  240. package/dist/plugins/remote-env-device.js +8 -6
  241. package/dist/plugins/remote-env-e2b.d.ts +4 -8
  242. package/dist/plugins/remote-env-e2b.js +21 -15
  243. package/dist/plugins/remote-env-host.d.ts +51 -10
  244. package/dist/plugins/remote-env-host.js +97 -31
  245. package/dist/plugins/remote-env-k8s.js +7 -4
  246. package/dist/plugins/remote-env-local-docker.d.ts +6 -3
  247. package/dist/plugins/remote-env-local-docker.js +11 -25
  248. package/dist/plugins/remote-env-ssh.js +4 -3
  249. package/dist/plugins/remote-shell.d.ts +3 -1
  250. package/dist/plugins/remote-shell.js +3 -3
  251. package/dist/plugins/replica-registration.d.ts +35 -0
  252. package/dist/plugins/replica-registration.js +48 -0
  253. package/dist/plugins/retention-lane-store-sql.d.ts +1 -1
  254. package/dist/plugins/run-store-sql.d.ts +27 -8
  255. package/dist/plugins/run-store-sql.js +46 -18
  256. package/dist/plugins/scenario-prefix-switch-m42.d.ts +8 -12
  257. package/dist/plugins/scenario-prefix-switch-m42.js +9 -6
  258. package/dist/plugins/schema-consistency.d.ts +91 -5
  259. package/dist/plugins/schema-consistency.js +189 -7
  260. package/dist/plugins/schema-drift-sentence.d.ts +6 -5
  261. package/dist/plugins/schema-drift-sentence.js +13 -5
  262. package/dist/plugins/serving-requirement.d.ts +49 -0
  263. package/dist/plugins/serving-requirement.js +53 -0
  264. package/dist/plugins/session-placement.d.ts +5 -4
  265. package/dist/plugins/shell-sentinel-owner-m48.d.ts +22 -0
  266. package/dist/plugins/shell-sentinel-owner-m48.js +6 -0
  267. package/dist/plugins/stop-then-start-sentence.d.ts +55 -0
  268. package/dist/plugins/stop-then-start-sentence.js +51 -0
  269. package/dist/plugins/store-backend.d.ts +23 -9
  270. package/dist/plugins/store-backend.js +58 -24
  271. package/dist/plugins/store-contracts.d.ts +45 -5
  272. package/dist/plugins/store-contracts.js +19 -0
  273. package/dist/plugins/tidb-pool.d.ts +11 -6
  274. package/dist/plugins/tidb-pool.js +22 -12
  275. package/dist/plugins/workspace-custody.d.ts +44 -0
  276. package/dist/plugins/workspace-custody.js +47 -0
  277. package/dist/process-instance.d.ts +3 -0
  278. package/dist/process-instance.js +7 -0
  279. package/dist/project-context.d.ts +66 -0
  280. package/dist/project-context.js +70 -0
  281. package/dist/project-identity.d.ts +8 -7
  282. package/dist/project-identity.js +40 -17
  283. package/dist/project-layer-key.d.ts +13 -2
  284. package/dist/project-layer-key.js +9 -1
  285. package/dist/question.d.ts +9 -21
  286. package/dist/question.js +28 -19
  287. package/dist/read-deny-floor-notice.d.ts +36 -0
  288. package/dist/read-deny-floor-notice.js +40 -0
  289. package/dist/{plain-string-record.d.ts → request-declaration-disposition.d.ts} +9 -23
  290. package/dist/request-declaration-disposition.js +39 -0
  291. package/dist/run-cancel-card.d.ts +28 -0
  292. package/dist/run-cancel-card.js +20 -0
  293. package/dist/run-cancel-context.d.ts +10 -0
  294. package/dist/run-cancel-context.js +1 -0
  295. package/dist/run-local.js +28 -13
  296. package/dist/runs.d.ts +31 -7
  297. package/dist/runs.js +34 -16
  298. package/dist/security.d.ts +3 -3
  299. package/dist/server-secret-env.d.ts +34 -20
  300. package/dist/server-secret-env.js +61 -18
  301. package/dist/session-agent.d.ts +4 -12
  302. package/dist/session-agent.js +0 -6
  303. package/dist/session-children-retire.d.ts +6 -3
  304. package/dist/session-children-retire.js +2 -5
  305. package/dist/session-incarnation.d.ts +18 -4
  306. package/dist/session-incarnation.js +4 -1
  307. package/dist/session-notify-door.d.ts +4 -4
  308. package/dist/session-notify-door.js +5 -6
  309. package/dist/session-rules-seat.d.ts +4 -3
  310. package/dist/session-rules-seat.js +3 -11
  311. package/dist/session-sync.js +3 -3
  312. package/dist/session-workspace.d.ts +3 -3
  313. package/dist/store-admission.d.ts +33 -0
  314. package/dist/store-admission.js +49 -0
  315. package/dist/store-posture.d.ts +5 -0
  316. package/dist/store-posture.js +18 -1
  317. package/dist/stores-sql/checkpoint-store-sql.d.ts +22 -3
  318. package/dist/stores-sql/checkpoint-store-sql.js +109 -23
  319. package/dist/stores-sql/file-history-store-sql.d.ts +1 -1
  320. package/dist/stores-sql/file-history-store-sql.js +3 -5
  321. package/dist/stores-sql/retention-store-sql.d.ts +1 -1
  322. package/dist/stores-sql/tool-result-store-sql.d.ts +1 -1
  323. package/dist/stores-sql/tool-result-store-sql.js +4 -6
  324. package/dist/task-a2a.d.ts +3 -8
  325. package/dist/task-a2a.js +25 -27
  326. package/dist/task-cwd.d.ts +27 -10
  327. package/dist/task-cwd.js +3 -0
  328. package/dist/task-mcp.d.ts +3 -3
  329. package/dist/task-mcp.js +1 -1
  330. package/dist/task-settings.d.ts +13 -0
  331. package/dist/task-settings.js +14 -7
  332. package/dist/terminal.d.ts +2 -0
  333. package/dist/terminal.js +21 -6
  334. package/dist/tool-approval.d.ts +50 -41
  335. package/dist/tool-approval.js +96 -73
  336. package/dist/trace/engine-notice-wire.d.ts +38 -21
  337. package/dist/trace/engine-notice-wire.js +25 -4
  338. package/dist/trace/ledger-sink.d.ts +3 -1
  339. package/dist/trace/project.d.ts +8 -5
  340. package/dist/trace/project.js +11 -9
  341. package/dist/trace/projection-table.d.ts +33 -2
  342. package/dist/trace/redact.d.ts +12 -0
  343. package/dist/trace/redact.js +5 -1
  344. package/dist/trace/self-minted-wire.d.ts +268 -56
  345. package/dist/trace/self-minted-wire.js +110 -40
  346. package/dist/trace/tool-card.d.ts +3 -0
  347. package/dist/trace/tool-card.js +24 -0
  348. package/dist/trace/tool-output-media.d.ts +2 -1
  349. package/dist/unattended-permission-mode.d.ts +1 -1
  350. package/dist/web-admin-client/apply-effective.d.ts +1 -1
  351. package/dist/web-admin-client/apply-effective.js +22 -9
  352. package/dist/web-admin-client/effective-keys.d.ts +1 -1
  353. package/dist/web-admin-client/skills-mcp.d.ts +1 -1
  354. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +57 -29
  355. package/docs/error-text-lexicon.json +317 -0
  356. package/package.json +4 -2
  357. package/scripts/embed-minimal.mjs +183 -0
  358. package/wire-changes/server@7.149.0.json +372 -0
  359. package/wire-changes/server@7.150.0.json +335 -0
  360. package/dist/plain-string-record.js +0 -70
@@ -40,8 +40,9 @@ export function createServingLeaseClient(o) {
40
40
  const mono = o.clock?.mono ?? (() => performance.now());
41
41
  const epoch = o.clock?.epoch ?? Date.now;
42
42
  const stickyPath = o.dataRoot === undefined ? undefined : join(o.dataRoot, SERVING_LEASE_DENIED_FILE);
43
- let capability;
44
- const enabled = () => capability?.announced === true;
43
+ let fence = { kind: "presumed", since: mono() };
44
+ const enabled = () => fence.kind === "on";
45
+ let viewListener;
45
46
  let denied = false;
46
47
  let stopped = false;
47
48
  let lease;
@@ -49,6 +50,37 @@ export function createServingLeaseClient(o) {
49
50
  let timer;
50
51
  let inFlight;
51
52
  let failStreak = 0;
53
+ const view = () => {
54
+ if (denied)
55
+ return { state: "denied" };
56
+ if (fence.kind === "off")
57
+ return { state: "none" };
58
+ const now = mono();
59
+ if (lease !== undefined && now < lease.expiresAtMono)
60
+ return { state: "granted", leaseUntil: lease.leaseUntil };
61
+ return { state: "expired", pastOutageMax: now - (lease?.expiresAtMono ?? fence.since) >= o.outageMaxMs };
62
+ };
63
+ const changing = (change) => {
64
+ const before = view().state;
65
+ change();
66
+ if (view().state !== before)
67
+ viewListener?.();
68
+ };
69
+ const unconfirmed = (why) => {
70
+ recordFailOpen("server.fleet.serving-lease-unconfirmed", `worker=${o.center.worker} generation=${o.generation} why=${why}`);
71
+ failStreak += 1;
72
+ if (failStreak === 1) {
73
+ o.logger?.warn("fleet_serving_lease_unconfirmed", {
74
+ error: why,
75
+ note: "the fleet center has not confirmed this instance's serving lease (heartbeat or renewal unanswered, refused or unreadable, or the verdict was unknown) — this instance keeps serving until its last grant, or with no grant yet the moment the fence took effect (this process's start, for a member that has not heard the center), is FLEET_LEASE_OUTAGE_MAX_MS behind, then refuses new work (unconfirmed is never a denial)",
76
+ });
77
+ }
78
+ };
79
+ const confirmed = (by) => {
80
+ if (failStreak > 0)
81
+ o.logger?.info("fleet_serving_lease_recovered", { after: failStreak, by });
82
+ failStreak = 0;
83
+ };
52
84
  const clear = () => {
53
85
  if (timer !== undefined)
54
86
  clearTimeout(timer);
@@ -79,6 +111,9 @@ export function createServingLeaseClient(o) {
79
111
  const enterDenied = (denial) => {
80
112
  if (denied)
81
113
  return;
114
+ changing(() => enterDeniedNow(denial));
115
+ };
116
+ const enterDeniedNow = (denial) => {
82
117
  denied = true;
83
118
  lease = undefined;
84
119
  clear();
@@ -118,25 +153,22 @@ export function createServingLeaseClient(o) {
118
153
  why = err instanceof Error ? err.message : String(err);
119
154
  }
120
155
  if (reading === undefined || reading.verdict === "unknown") {
121
- const unconfirmed = reading === undefined ? why : "verdict=unknown (the center holds no lifecycle object for this instance right now)";
122
- recordFailOpen("server.fleet.serving-lease-unconfirmed", `worker=${o.center.worker} generation=${o.generation} why=${unconfirmed}`);
123
- failStreak += 1;
124
- if (failStreak === 1)
125
- o.logger?.warn("fleet_serving_lease_unconfirmed", { error: unconfirmed, note: "the serving lease could not be renewed (the center is unreachable or did not confirm this instance) — this instance keeps serving until the lease expires and then for FLEET_LEASE_OUTAGE_MAX_MS more before it refuses new work (unconfirmed is never a denial)" });
156
+ if (fence.kind === "on")
157
+ unconfirmed(reading === undefined ? why : "verdict=unknown (the center holds no lifecycle object for this instance right now)");
126
158
  schedule(nextInMs);
127
159
  return;
128
160
  }
129
- if (failStreak > 0)
130
- o.logger?.info("fleet_serving_lease_recovered", { after: failStreak });
131
- failStreak = 0;
161
+ confirmed("verdict");
132
162
  switch (reading.verdict) {
133
- case "granted":
163
+ case "granted": {
134
164
  if (denied)
135
165
  return;
136
- lease = { expiresAtMono: sendMono + reading.ttlMs, leaseUntil: sendEpoch + reading.ttlMs };
166
+ const granted = { expiresAtMono: sendMono + reading.ttlMs, leaseUntil: sendEpoch + reading.ttlMs };
167
+ changing(() => (lease = granted));
137
168
  nextInMs = reading.ttlMs / 3;
138
169
  schedule(nextInMs);
139
170
  return;
171
+ }
140
172
  case "denied":
141
173
  enterDenied({ from: "center", reason: reading.reason });
142
174
  return;
@@ -154,34 +186,48 @@ export function createServingLeaseClient(o) {
154
186
  });
155
187
  return inFlight;
156
188
  };
189
+ const noteDeclaration = (caps) => {
190
+ const on = caps.servingLease === true;
191
+ if (!on)
192
+ recordFailOpen("server.fleet.serving-lease-capability-absent", `worker=${o.center.worker} generation=${o.generation} was=${fence.kind}`);
193
+ if (on && fence.kind !== "on") {
194
+ fence = { kind: "on", since: fence.since ?? mono() };
195
+ o.logger?.info("fleet_serving_lease_enabled", { generation: o.generation, worker: o.center.worker });
196
+ void renewNow();
197
+ }
198
+ else if (!on && fence.kind !== "off") {
199
+ const was = fence.kind;
200
+ fence = { kind: "off", since: was === "on" ? fence.since : undefined };
201
+ clear();
202
+ confirmed("not-enforced");
203
+ if (was === "on")
204
+ o.logger?.warn("fleet_serving_lease_disabled", { note: "the fleet center no longer announces the servingLease capability — the lease is out of effect (leaseState none) until it does again; its outage clock is not reset (when the capability returns, the last grant and the moment the fence first took effect in this process still count)" });
205
+ }
206
+ };
157
207
  return {
158
- noteCenterCapabilities(caps) {
208
+ noteHeartbeat(hearing) {
159
209
  if (stopped || denied)
160
210
  return;
161
- const on = caps?.servingLease === true;
162
- if (!on)
163
- recordFailOpen("server.fleet.serving-lease-capability-absent", `worker=${o.center.worker} generation=${o.generation} was=${enabled() ? "enabled" : "none"}`);
164
- if (on && !enabled()) {
165
- capability = { announced: true, firstSeenAtMono: capability?.firstSeenAtMono ?? mono() };
166
- o.logger?.info("fleet_serving_lease_enabled", { generation: o.generation, worker: o.center.worker });
167
- void renewNow();
168
- }
169
- else if (!on && capability?.announced === true) {
170
- capability = { announced: false, firstSeenAtMono: capability.firstSeenAtMono };
171
- clear();
172
- o.logger?.warn("fleet_serving_lease_disabled", { note: "the fleet center no longer announces the servingLease capability — the lease is out of effect (leaseState none) until it does again; its outage clock is not reset (when the capability returns, the last grant and this process's first sighting of the capability still count)" });
211
+ switch (hearing.kind) {
212
+ case "silent":
213
+ if (fence.kind === "presumed")
214
+ unconfirmed(`heartbeat:${hearing.why}`);
215
+ return;
216
+ case "reading":
217
+ if (fence.kind === "presumed")
218
+ changing(() => noteDeclaration(hearing.capabilities));
219
+ else
220
+ noteDeclaration(hearing.capabilities);
221
+ return;
222
+ default: {
223
+ const unhandled = hearing;
224
+ throw new Error(`serving lease: unknown heartbeat hearing ${JSON.stringify(unhandled)}`);
225
+ }
173
226
  }
174
227
  },
175
- view() {
176
- if (denied)
177
- return { state: "denied" };
178
- if (capability === undefined || !capability.announced)
179
- return { state: "none" };
180
- const now = mono();
181
- if (lease !== undefined && now < lease.expiresAtMono)
182
- return { state: "granted", leaseUntil: lease.leaseUntil };
183
- const expiredAt = lease?.expiresAtMono ?? capability.firstSeenAtMono;
184
- return { state: "expired", pastOutageMax: now - expiredAt >= o.outageMaxMs };
228
+ view,
229
+ onViewChange(listener) {
230
+ viewListener = listener;
185
231
  },
186
232
  resumeStickyDenial() {
187
233
  if (stickyPath === undefined || denied)
@@ -18,9 +18,10 @@
18
18
  * - **`modelRefusals?`([ref])**:在场 ⇔ 本副本对最近一次被判的中心候选目录有可报的拒(逐条名 → 码 / 模型面组拒码)—— 取自
19
19
  * `/health.modelRefusals` **同一只**取值口(装配层接 `deployment.modelRefusalsState`),同一个对象,形 = settings-schema
20
20
  * `ModelRefusalsSignal`;缺席即键缺席(同 `restart`)。
21
- * - **自报状态翻转即刻再 announce**(第一时间自摘流 / 自挂流 / 报重启 / 报拒因,比任何观测都快):draining 翻转由收尾链直调
22
- * `announceNow`;roster 落地、restart 信号与模型拒因变化由装配层把 web-admin 的 `onSelfReportChange` 接到 `announceNow`
23
- * (两处 = 两类输入各自的唯一写者,触发都是 `announceNow`)。退出前 DELETE(显式注销,幂等)。
21
+ * - **自报状态翻转即刻再 announce**(第一时间自摘流 / 自挂流 / 报重启 / 报拒因 / 报租约态,比任何观测都快):draining 翻转由收尾链直调
22
+ * `announceNow`;roster 落地、restart 信号与模型拒因变化由装配层把 web-admin 的 `onSelfReportChange` 接到 `announceNow`;租约态由事件引起的
23
+ * 翻转(推定的终结 / 续租判决 / 被拒 —— 不自激的那几样,见 `fleet/serving-lease.ts` 规则 2′)由装配层把服务租约客户端的 `onViewChange` 接到
24
+ * `announceNow`(三处 = 三类输入各自的唯一写者,触发都是 `announceNow`)。退出前 DELETE(显式注销,幂等)。
24
25
  * - **usage 批报**:tracer 的 `brain.call` 已带 ALS principal —— FleetUsageAccumulator 按 principal×model
25
26
  * 累计,每窗口(默认 60s,半开 [windowStartMs, windowEndMs))POST /api/fleet/usage。幂等键
26
27
  * (worker,instanceId,windowStartMs) 重发安全;**发送失败 → merge 回累计器**,并入下一窗(失败窗从未入账,
@@ -30,8 +31,9 @@
30
31
  * 不在下一个人的凭证下上报(不记到别人头上);它留在原箱,那个身份回来时照常报出去,收尾时还压着就响亮一行。
31
32
  * - 全部 fire-and-forget + swallow-guarded:fleet 面是运维旁路,绝不影响任务面;网络错误只 warn 一次/连败类。
32
33
  */
33
- import { UsageReport, type CapabilityReading, type WebAdminCapabilities, type ModelRefusalsSignal } from "@sema-agent/settings-schema/fleet";
34
+ import { UsageReport, type CapabilityReading, type ModelRefusalsSignal } from "@sema-agent/settings-schema/fleet";
34
35
  import { type EngineSignalFacts } from "./fleet/engine-signal.js";
36
+ import type { HeartbeatHearing } from "./fleet/serving-lease.js";
35
37
  import { type CenterEndpoint } from "./web-admin-client/center-request.js";
36
38
  import { type CenterCredentialSource } from "./center-credential.js";
37
39
  /** Bound on distinct principal×model keys per window — a runaway/multi-tenant burst collapses into
@@ -118,10 +120,10 @@ export interface FleetClientOpts {
118
120
  /** [ref]:模型拒因信号(装配层接 `/health` 同一只取值口 `deployment.modelRefusalsState`)。每条公告现读一次;`undefined` ⇒ 公告不带
119
121
  * `modelRefusals` 键。必填:与 `restart` 同 —— 心跳面没有「不知道有没有拒」这一形(老引擎不发这个键,中心按 `version` 判「不可知」)。 */
120
122
  modelRefusals: () => ModelRefusalsSignal | undefined;
121
- /** 编排器片 2:每一次**能力读数**交出去一次(装配层接服务租约客户端的 `noteCenterCapabilities`)。规则见 {@link capabilityReadingOf}:
122
- * 只有 `declared`(交出宣告的位)与 `undeclared`(交 `undefined` = 老中心没宣告)是读数;`unreadable`(体读不出 / 不是对象 /
123
- * `capabilities` 键在场而形坏)不是读数 ⇒ 不调,上一次读数与它的钟原样。失败的公告同样不调(没听到 ≠ 没有)。 */
124
- onCenterCapabilities?: (caps: WebAdminCapabilities | undefined) => void;
123
+ /** 编排器片 2:**每一次公告尝试恰好交一只**听到的东西(装配层接服务租约客户端的 `noteHeartbeat`;只有 fleet 成员接)。`reading` = 中心的显式
124
+ * 声明(规则见 {@link capabilityReadingOf});其余一律 `silent`(`why` 闭集:`HTTP <状态>` / 请求抛出的那句 / `reply-unreadable:<因>` /
125
+ * `not-sent:<那句>`)—— 没听到不是「没有」,租约客户端按沉默处置。没接这一只 ⇒ 不读答复体(非成员的心跳答复不进任何租约臂)。 */
126
+ onHeartbeat?: (hearing: HeartbeatHearing) => void;
125
127
  /** [ref]:按凭证身份分箱的用量账(缺席 = 用量环不装)。 */
126
128
  usage?: FleetUsageBins;
127
129
  heartbeatMs?: number;
@@ -149,13 +151,13 @@ export interface FleetClient {
149
151
  flushFinalUsage: () => Promise<void>;
150
152
  }
151
153
  /**
152
- * 心跳 2xx 答复读不成能力读数的原因 —— 一张闭集:`body` = 答复体本身读不出(空体 / 截断 / 非 JSON),其余两词 = 契约读器
153
- * `readCapabilityReading` 的 `unreadable` 两因(`not-an-object` / `invalid-capabilities`)。
154
+ * 心跳 2xx 答复读不成能力读数的原因 —— 一张闭集:`body` = 答复体本身读不出(空体 / 截断 / 非 JSON),其余三词 = 契约读器
155
+ * `readCapabilityReading` 的 `unreadable` 三因(`not-an-object` / `capabilities-absent` / `invalid-capabilities`)。
154
156
  */
155
157
  export type AnnounceReplyFlaw = "body" | Extract<CapabilityReading, {
156
158
  kind: "unreadable";
157
159
  }>["why"];
158
- /** 本仓对一次心跳答复的读法:契约三态,`unreadable` 的原因扩成 {@link AnnounceReplyFlaw}(`detail` = 读不出体时的那句错误,当数据交出)。 */
160
+ /** 本仓对一次心跳答复的读法:契约两态,`unreadable` 的原因扩成 {@link AnnounceReplyFlaw}(`detail` = 读不出体时的那句错误,当数据交出)。 */
159
161
  export type AnnounceReplyReading = Exclude<CapabilityReading, {
160
162
  kind: "unreadable";
161
163
  }> | {
@@ -110,9 +110,40 @@ export function startFleetClient(opts) {
110
110
  let failStreak = 0;
111
111
  let unreadableStreak = 0;
112
112
  const address = opts.advertiseAddress;
113
+ const announceFailed = (error) => {
114
+ failStreak += 1;
115
+ if (failStreak === 1)
116
+ opts.logger?.warn("fleet_announce_failed", { error });
117
+ };
118
+ const hearReply = async (res, leaseState) => {
119
+ const reading = await capabilityReadingOf(res);
120
+ switch (reading.kind) {
121
+ case "declared":
122
+ unreadableStreak = 0;
123
+ return { kind: "reading", capabilities: reading.capabilities };
124
+ case "unreadable":
125
+ recordFailOpen("server.fleet.announce-reply-unreadable", `why=${reading.why} leaseState=${leaseState}`);
126
+ unreadableStreak += 1;
127
+ if (unreadableStreak === 1) {
128
+ opts.logger?.warn("fleet_announce_reply_unreadable", {
129
+ status: res.status,
130
+ why: reading.why,
131
+ ...(reading.detail !== undefined ? { error: reading.detail } : {}),
132
+ note: "the fleet center's heartbeat reply is not a capability declaration (unreadable body, not an object, no capabilities key, or a malformed capabilities value) — no reading taken; the serving lease keeps its state and outage clock (a member that has heard no declaration stays inside the presumed fence)",
133
+ });
134
+ }
135
+ return { kind: "silent", why: `reply-unreadable:${reading.why}` };
136
+ default: {
137
+ const unhandled = reading;
138
+ throw new Error(`heartbeat reply: unknown capability reading ${JSON.stringify(unhandled)}`);
139
+ }
140
+ }
141
+ };
113
142
  const announceOnce = async () => {
114
143
  if (stopped || address === undefined)
115
144
  return;
145
+ let hearing;
146
+ let sending = false;
116
147
  try {
117
148
  const signal = opts.signal();
118
149
  const engineSignal = buildEngineSignal(signal);
@@ -129,43 +160,32 @@ export function startFleetClient(opts) {
129
160
  ...fleetMembershipKeys(signal),
130
161
  [ENGINE_SIGNAL_KEY]: engineSignal,
131
162
  };
163
+ sending = true;
132
164
  const { res } = await send("fleet-instances", "POST", body);
133
- if (!res.ok)
134
- throw new Error(`announce ${res.status}${await rejectionSnippet(res)}`);
135
- if (opts.onCenterCapabilities !== undefined) {
136
- const reading = await capabilityReadingOf(res);
137
- switch (reading.kind) {
138
- case "declared":
139
- case "undeclared":
140
- unreadableStreak = 0;
141
- opts.onCenterCapabilities(reading.kind === "declared" ? reading.capabilities : undefined);
142
- break;
143
- case "unreadable":
144
- recordFailOpen("server.fleet.announce-reply-unreadable", `why=${reading.why} leaseState=${signal.lease.state}`);
145
- unreadableStreak += 1;
146
- if (unreadableStreak === 1) {
147
- opts.logger?.warn("fleet_announce_reply_unreadable", {
148
- status: res.status,
149
- why: reading.why,
150
- ...(reading.detail !== undefined ? { error: reading.detail } : {}),
151
- note: "the fleet center's heartbeat reply is not a capability reading (unreadable body, not an object, or a malformed capabilities value) — no reading taken; the last one stands (the serving lease keeps its state and outage clock)",
152
- });
153
- }
154
- break;
155
- default: {
156
- const unhandled = reading;
157
- throw new Error(`heartbeat reply: unknown capability reading ${JSON.stringify(unhandled)}`);
158
- }
159
- }
165
+ if (!res.ok) {
166
+ hearing = { kind: "silent", why: `HTTP ${res.status}` };
167
+ announceFailed(`announce ${res.status}${await rejectionSnippet(res)}`);
160
168
  }
161
- if (failStreak > 0)
162
- opts.logger?.info("fleet_announce_recovered", { after: failStreak });
163
- failStreak = 0;
169
+ else {
170
+ if (failStreak > 0)
171
+ opts.logger?.info("fleet_announce_recovered", { after: failStreak });
172
+ failStreak = 0;
173
+ if (opts.onHeartbeat !== undefined)
174
+ hearing = await hearReply(res, signal.lease.state);
175
+ }
176
+ }
177
+ catch (err) {
178
+ const error = err instanceof Error ? err.message : String(err);
179
+ hearing = { kind: "silent", why: sending ? error : `not-sent:${error}` };
180
+ announceFailed(error);
181
+ }
182
+ if (hearing === undefined || opts.onHeartbeat === undefined)
183
+ return;
184
+ try {
185
+ opts.onHeartbeat(hearing);
164
186
  }
165
187
  catch (err) {
166
- failStreak += 1;
167
- if (failStreak === 1)
168
- opts.logger?.warn("fleet_announce_failed", { error: err instanceof Error ? err.message : String(err) });
188
+ opts.logger?.warn("fleet_heartbeat_listener_failed", { error: err instanceof Error ? err.message : String(err), note: "the serving-lease client threw while taking this heartbeat's hearing — this beat did not reach the fence; the announce loop goes on" });
169
189
  }
170
190
  };
171
191
  let inFlight;
@@ -10,7 +10,7 @@ async function unknownAnchorCause(readers, reader, sessionId, handle) {
10
10
  if (reader === undefined)
11
11
  return NO_ANCHOR;
12
12
  try {
13
- const run = await readVisibleRun(reader, readers.runs, handle);
13
+ const run = (await readVisibleRun(reader, readers.runs, handle))?.row;
14
14
  if (run === undefined || run.sessionId !== sessionId)
15
15
  return NO_ANCHOR;
16
16
  return (await readers.anchors.resolve(sessionId, handle, run.owner)) === undefined ? PROMPT_WITHOUT_ANCHOR : NO_ANCHOR;
@@ -29,7 +29,7 @@ export interface HitlSinks {
29
29
  readonly live?: boolean;
30
30
  }
31
31
  /**
32
- * 一条腿的传输(无状态)。`deliver` 与 `reachable` 是**属性**(不是方法签名)—— 帧型参数因此逆变。腿上装出的那一只是全族型(`HitlTransport<HitlFrame>`,
32
+ * 一条腿的传输(无状态)。`deliver` 是**属性**(不是方法签名)—— 帧型参数因此逆变。腿上装出的那一只是全族型(`HitlTransport<HitlFrame>`,
33
33
  * 一腿一只、三族共用);**窄化发生在装进族上下文那一步**:审批上下文的判别联合把传输型钉到本族帧(呈卡族收不进旧族帧),竞速机的目标从族上下文取
34
34
  * ⇒ 混族的 (transport, frame) 对编译不过。直接拿全族型传输喂竞速机的代码不受这道型约束(三只协调器都从族上下文取,`P14` 编译期钉)。
35
35
  */
@@ -39,8 +39,6 @@ export interface HitlTransport<F extends HitlFrame> {
39
39
  * 写抛 / 已死 ⇒ 直写没接下(不碰死连接)。③ 交回每只涉及的 sink 接下与否。**永不 reject。**
40
40
  */
41
41
  readonly deliver: (frame: F, onto?: HitlSinks) => Promise<HitlSinks>;
42
- /** 判决面谓词:有直写 ⇒ 直写此刻活着;只有账本 ⇒ 恒真(账本 tail 随时可被接上)。谓词读不出 ⇒ 假(判决面 fail-closed 到停驻)。 */
43
- readonly reachable: () => boolean;
44
42
  }
45
43
  /** 直写面:一个具名帧写 + 「此刻还活着吗」。 */
46
44
  export interface HitlLiveSink<F extends HitlFrame> {
@@ -66,7 +66,7 @@ export function createHitlTransport(sinks) {
66
66
  }
67
67
  return ledgerAccepted !== undefined ? { ledger: ledgerAccepted, live: liveAccepted } : { live: liveAccepted };
68
68
  };
69
- return { deliver, reachable: () => live === undefined || liveAlive(live) };
69
+ return { deliver };
70
70
  }
71
71
  export function createHitlOpenRace(args) {
72
72
  const opens = args.targets.map((t) => t.transport.deliver(t.frame));
@@ -46,9 +46,14 @@ export type CommandSpawnPlan = {
46
46
  export declare function composeHookEnvLayer(requestEnv: Readonly<Record<string, string>> | undefined, cwd: string, plugin: HookPluginContext | undefined): Record<string, string> & {
47
47
  CLAUDE_PROJECT_DIR: string;
48
48
  };
49
+ /**
50
+ * 这条 hook 的子进程会拿到的环境:有效环境层叠在继承那一半之上(组合属主 = `hook-runner.ts` 的 `hookChildEnv`,本模块不写第二份组合,只问它的读数)。
51
+ * 组合拒(授予形坏 / 放回点到本服务自有凭据名)⇒ 抛,起法按「起不来」答(与 spawn 时组合抛同一个结局)。
52
+ */
53
+ export type HookChildEnvOf = (layer: Readonly<Record<string, string>>) => Readonly<Record<string, string | undefined>>;
49
54
  /**
50
55
  * 一条 command 条目的起法(规则见文件头)。`cwd` = 本次 hook 的工作目录(`CLAUDE_PROJECT_DIR` 的缺省值);`requestEnv` = 请求 `settings.env`(hook 环境的一层;**必传**:没有就显式传 `undefined`,
51
- * 漏传是编译红,不会静默丢掉请求给的 env),合并与读法见 {@link composeHookEnvLayer} / {@link contextVarValue}。有副作用的只有两步,都只在条目带 `plugin` 时发生:读插件根是否是目录、建数据目录(CC 同:注入即建)。
56
+ * 漏传是编译红,不会静默丢掉请求给的 env),合并与读法见 {@link composeHookEnvLayer} / {@link contextVarValue};`childEnvOf` = 组合的读数入口({@link HookChildEnvOf})。有副作用的只有两步,都只在条目带 `plugin` 时发生:读插件根是否是目录、建数据目录(CC 同:注入即建)。
52
57
  */
53
- export declare function planCommandSpawn(entry: RunnableCommandHookEntry, cwd: string, requestEnv: Readonly<Record<string, string>> | undefined): Promise<CommandSpawnPlan>;
58
+ export declare function planCommandSpawn(entry: RunnableCommandHookEntry, cwd: string, requestEnv: Readonly<Record<string, string>> | undefined, childEnvOf: HookChildEnvOf): Promise<CommandSpawnPlan>;
54
59
  //# sourceMappingURL=command-spawn.d.ts.map
@@ -59,17 +59,24 @@ const refOf = (name) => "${" + name + "}";
59
59
  export function composeHookEnvLayer(requestEnv, cwd, plugin) {
60
60
  return { ...requestEnv, CLAUDE_PROJECT_DIR: requestEnv?.CLAUDE_PROJECT_DIR ?? cwd, ...hookPluginEnv(plugin) };
61
61
  }
62
- function contextVarValue(name, layer) {
63
- const v = layer[name];
62
+ function contextVarValue(name, env) {
63
+ const v = env[name];
64
64
  return v !== undefined && v.length > 0 ? v : undefined;
65
65
  }
66
- export async function planCommandSpawn(entry, cwd, requestEnv) {
66
+ export async function planCommandSpawn(entry, cwd, requestEnv, childEnvOf) {
67
67
  const texts = [entry.command, ...(entry.args ?? [])];
68
68
  const p = entry.plugin;
69
69
  const env = composeHookEnvLayer(requestEnv, cwd, p);
70
+ let seen;
71
+ try {
72
+ seen = childEnvOf(env);
73
+ }
74
+ catch (e) {
75
+ return { kind: "refused", error: e instanceof Error ? e.message : String(e) };
76
+ }
70
77
  const values = new Map([["CLAUDE_PROJECT_DIR", env.CLAUDE_PROJECT_DIR]]);
71
78
  for (const [name, ref] of CONTEXT_VARS) {
72
- const value = contextVarValue(name, env);
79
+ const value = contextVarValue(name, seen);
73
80
  if (value !== undefined) {
74
81
  values.set(name, value);
75
82
  continue;
@@ -79,8 +86,8 @@ export async function planCommandSpawn(entry, cwd, requestEnv) {
79
86
  return {
80
87
  kind: "refused",
81
88
  error: p !== undefined
82
- ? `hook command references ${ref} but only ${ROOT_REF} is available for this hook (it carries no plugin data directory and the request settings.env does not set ${name})`
83
- : `hook command references ${ref} but the hook is not associated with a plugin and the request settings.env does not set ${name} (the variable is only available to hooks that carry a plugin context, or to a request that sets it in settings.env)`,
89
+ ? `hook command references ${ref} but only ${ROOT_REF} is available for this hook (it carries no plugin data directory and the hook environment does not set ${name}; a request can set it in settings.env)`
90
+ : `hook command references ${ref} but the hook is not associated with a plugin and the hook environment does not set ${name} (the variable is set for hooks that carry a plugin context; a request can set it in settings.env)`,
84
91
  };
85
92
  }
86
93
  if (p !== undefined) {
@@ -533,6 +533,11 @@ export interface HookRunnerContext {
533
533
  * `[]` = 没有层写名单(不限,与引擎外的参照实现「名单未设 ⇒ 不限」同读)。今天唯一的层来源是请求的 `settings`(`task-settings.ts` 的 `httpHookAllowlist`,源 `flagSettings`)。
534
534
  */
535
535
  httpHookAllowlistLayers: readonly HttpHookAllowlistLayer[];
536
+ /**
537
+ * 部署方点名放回 hook 环境的服务进程变量名(`HOOK_ENV_ALLOW` 的生效名单,`config.hookEnvAllow`;读者 = {@link hookChildEnv} 的两个入口:命令腿 spawn 与 http 插值)。必填:每个装配点都要说 ——
538
+ * `[]` = 不放回(hook 环境 = 十二名基础名单 + 有效环境层)。这是服务进程变量进 hook 环境的**唯一**来源;请求层(条目 `allowedEnvVars`、`settings.httpHookAllowedEnvVars`)只收窄、不授予。
539
+ */
540
+ hookEnvAllow: readonly string[];
536
541
  /**
537
542
  * 部署座在**任务座之下**挂了链成员的站点式样(引擎的准入记录读出,`boot/deployment-mods.ts` 的 `belowTaskChainPatternsOf`)。必填:每个装配点都要说 ——
538
543
  * `[]` = 任务座之下没有部署座的链成员(部署注册只在任务座之上、只有观察者,或没有部署注册)。读者 = PreToolUse 翻译的只许收紧({@link chainsBelowTaskOn};请求钩子两条车道在这些站上只许收紧)。
@@ -36,12 +36,15 @@ const MIN_HOOK_CONTEXT_PARTIAL_CHARS = 256;
36
36
  const MAX_TOOL_RESPONSE_CHARS = 32_768;
37
37
  const MAX_TOOL_INPUT_CHARS = 262_144;
38
38
  const HOOK_ENV_ALLOWLIST = ["PATH", "HOME", "SHELL", "USER", "LOGNAME", "TERM", "TZ", "LANG", "LC_ALL", "LC_CTYPE", "LC_MESSAGES", "TMPDIR"];
39
- function hookChildEnv(layer, spawning) {
40
- const o = { policy: [...HOOK_ENV_ALLOWLIST], allow: [], grants: [layer] };
39
+ function hookChildEnv(layer, putBack, spawning) {
40
+ const o = { policy: [...HOOK_ENV_ALLOWLIST], allow: putBack, grants: [layer] };
41
41
  return spawning ? hostSubprocessEnv({ site: "server.hooks.spawn-env", ...o }) : composeHostSubprocessEnv(o);
42
42
  }
43
+ function hookChildView(ctx) {
44
+ return (layer) => hookChildEnv(layer, ctx.hookEnvAllow, false);
45
+ }
43
46
  function hookEnv(ctx) {
44
- return hookChildEnv(composeHookEnvLayer(ctx.shellEnv, ctx.cwd, undefined), false);
47
+ return hookChildView(ctx)(composeHookEnvLayer(ctx.shellEnv, ctx.cwd, undefined));
45
48
  }
46
49
  export const SETTINGS_HOOK_STATIONS = {
47
50
  PreToolUse: "classic.PreToolUse",
@@ -861,7 +864,7 @@ function readBelowPreToolUse(below) {
861
864
  };
862
865
  }
863
866
  function spawnPlannedCommand(plan, ctx, stdio) {
864
- const env = hookChildEnv(plan.env, true);
867
+ const env = hookChildEnv(plan.env, ctx.hookEnvAllow, true);
865
868
  if (plan.kind === "exec")
866
869
  return spawn(plan.file, [...plan.args], { cwd: ctx.cwd, env, stdio, detached: true });
867
870
  const sh = hostShell();
@@ -926,7 +929,7 @@ function createHookTreeLadder(child, ctx, event) {
926
929
  };
927
930
  }
928
931
  async function runCommandHook(entry, payload, ctx, halt, event) {
929
- const plan = await planCommandSpawn(entry, ctx.cwd, ctx.shellEnv);
932
+ const plan = await planCommandSpawn(entry, ctx.cwd, ctx.shellEnv, hookChildView(ctx));
930
933
  if (plan.kind === "refused")
931
934
  return { code: null, stdout: "", stderr: "", timedOut: false, spawnError: plan.error };
932
935
  if (plan.kind === "shell") {
@@ -1011,7 +1014,7 @@ async function fireAsyncCommandHook(entry, payload, ctx, event, halt) {
1011
1014
  try {
1012
1015
  const rewake = entry.asyncRewake === true;
1013
1016
  const timeoutMs = Math.min(entry.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, MAX_HOOK_TIMEOUT_SECONDS) * 1000;
1014
- const plan = await planCommandSpawn(entry, ctx.cwd, ctx.shellEnv);
1017
+ const plan = await planCommandSpawn(entry, ctx.cwd, ctx.shellEnv, hookChildView(ctx));
1015
1018
  if (plan.kind === "refused")
1016
1019
  throw new Error(plan.error);
1017
1020
  if (plan.kind === "shell")
@@ -1740,7 +1743,8 @@ function createLaneRegistration(lane, config, ctx, budgetMs) {
1740
1743
  asked = alsoAsks(lane, prior.asked ?? prior.unreadable?.message, clipDataText(typeof h.permissionDecisionReason === "string" ? h.permissionDecisionReason : "a PreToolUse hook requires approval", MAX_HOOK_FEEDBACK_CHARS));
1741
1744
  spoke = true;
1742
1745
  }
1743
- if (decision === "allow" || decision === undefined) {
1746
+ const counted = decision === "allow" || decision === "ask" || decision === undefined;
1747
+ if (counted) {
1744
1748
  if (ran.distrust !== undefined && hookLaneOf(ran.source) === "unmanaged" && (decision === "allow" || proposesRewrite)) {
1745
1749
  noteVerdictUntrusted(ctx, { event: "PreToolUse", reason: ran.distrust, entry, source: ran.source, toolName });
1746
1750
  }
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { type GovernancePosture } from "../runtime-governance.js";
17
17
  import type { ShellGateDoctrine } from "@sema-agent/core";
18
- import { type RunRecord } from "../plugins/store-contracts.js";
18
+ import type { RunStore } from "../plugins/store-backend.js";
19
19
  import { type PendingRowBinding } from "../plugins/checkpoint-store-face.js";
20
20
  /** 与 runs.ts/tasks.ts 三个 409 位共享的旧文案(byte-frozen:api-error-text-freeze 门认这句)。 */
21
21
  export declare const ACTIVE_RUN_CONFLICT_BASE_TEXT = "session already has an active run \u2014 POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)";
@@ -113,12 +113,8 @@ interface ConflictProbeDeps<TToken> {
113
113
  /** 结构形入参,但 `status` 取 run 行的**闭词表**(合并码重扫):判 park 要走词表属主
114
114
  * `isParkedRunStatus`,而它的入参是穷举联合 —— 这一格写成开放 `string` 就只能在调用点裸 cast,
115
115
  * 绕开那道编译期执法(core 加一个 park 类成员时本文件不会红)。 */
116
- /** 持有者那一行(只读 `status`;409 体只点名**活着**的持有者 —— 心跳过期的持有者在认领冲突处已被判死、认领已放,见 run 店 `createRun`)。 */
117
- runStore?: {
118
- getRun?: (id: string) => Promise<{
119
- status?: RunRecord["status"];
120
- } | null | undefined>;
121
- } | undefined;
116
+ /** 持有者那一行的状态(不带属主的一格事实,经属主存取器 `runFactsOf` 读;409 体只点名**活着**的持有者 —— 心跳过期的持有者在认领冲突处已被判死、认领已放,见 run 店 `createRun`)。 */
117
+ runStore?: Pick<RunStore, "getRun"> | undefined;
122
118
  checkpointStore?: {
123
119
  peekPendingScope?: (sessionId: string, binding?: PendingRowBinding) => Promise<string | null | undefined>;
124
120
  findPendingTokenBySession?: (sessionId: string, scope?: string, binding?: PendingRowBinding) => Promise<TToken | null | undefined>;
@@ -1,5 +1,6 @@
1
1
  import { governanceOriginOf } from "../runtime-governance.js";
2
2
  import { isParkedRunStatus } from "../plugins/store-contracts.js";
3
+ import { runFactsOf } from "./owner-access.js";
3
4
  import { LIVE_RUN_GATE_KINDS } from "../plugins/checkpoint-store-face.js";
4
5
  import { recordFailOpen } from "../observability/fail-open.js";
5
6
  import { refusalBody } from "./refusal-body.js";
@@ -46,8 +47,7 @@ export async function buildActiveRunConflict(deps, sessionId, activeTaskId) {
46
47
  if (!activeTaskId)
47
48
  return base;
48
49
  try {
49
- const row = await deps.runStore?.getRun?.(activeTaskId);
50
- const status = row?.status;
50
+ const status = deps.runStore === undefined ? undefined : (await runFactsOf(deps.runStore, activeTaskId))?.status;
51
51
  if (status === undefined || (!isParkedRunStatus(status) && status !== "running"))
52
52
  return base;
53
53
  if (status === "running") {
@@ -2,7 +2,7 @@ import { APPROVAL_REACH, IMAGE_ADMISSION, isApprovalReach, isPermissionMode, isT
2
2
  import { isAtMentionable, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../model-select.js";
3
3
  import { echoModelRef, modelUnavailableMessage } from "../model-entry-refusal.js";
4
4
  import { HttpError, sessionIdShapeIssue } from "../security.js";
5
- import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, PERMISSION_MODE_WORDS, autoCompactEnabledOk, parseTaskSettings, readSettingsEnv, settingsEnvComposeIssue, settingsEnvCredentialIssue, settingsPermissionBucketIssue, skillListingBudgetFractionOk, skillListingMaxDescOk, hooksKeyIssue, taskHooksHonored, taskSettingsKeyIssue } from "../task-settings.js";
5
+ import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, appendFold, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, PERMISSION_MODE_WORDS, autoCompactEnabledOk, parseTaskSettings, readSettingsEnv, settingsEnvComposeIssue, settingsEnvCredentialIssue, settingsPermissionBucketIssue, skillListingBudgetFractionOk, skillListingMaxDescOk, hooksKeyIssue, taskHooksHonored, taskSettingsKeyIssue } from "../task-settings.js";
6
6
  import { serverCredentialValueSources } from "../server-secret-env.js";
7
7
  import { tightenOnlySeatIssue } from "../tighten-only-seats.js";
8
8
  import { approvalFrameFamiliesIssue } from "../tool-approval.js";
@@ -25,7 +25,7 @@ import { legFactsOf } from "../leg-facts.js";
25
25
  import { createSubmissionSeat } from "../submission-exit.js";
26
26
  import { assertRequestFormBeforeMint } from "../request-form.js";
27
27
  import { headerStr } from "./principal-gate.js";
28
- import { resolveExecutionContext, stampSpecRoot } from "./execution-context.js";
28
+ import { SUBMISSION_CLAIM_FACE, resolveBeforeClaim, stampSpecRoot } from "./execution-context.js";
29
29
  import { callerTrust, requestTrustRefusal } from "./caller-trust.js";
30
30
  import { createRunAcceptance } from "../run-acceptance.js";
31
31
  import { promptScreenDeclared } from "../prompt-screen.js";
@@ -214,7 +214,7 @@ export function createAdmission(ctx) {
214
214
  sendError(res, 400, "request.field_invalid", `settings.outputStyle must be at most ${MAX_SETTINGS_OUTPUT_STYLE_CHARS} characters`, limitFacts("settings.outputStyle", "characters", st.outputStyle.length, MAX_SETTINGS_OUTPUT_STYLE_CHARS));
215
215
  return null;
216
216
  }
217
- const foldedChars = typeof st.outputStyle === "string" && st.outputStyle.length > 0 && typeof body.appendSystemPrompt === "string" ? body.appendSystemPrompt.length + 2 + st.outputStyle.length : 0;
217
+ const foldedChars = typeof st.outputStyle === "string" && st.outputStyle.length > 0 && typeof body.appendSystemPrompt === "string" ? appendFold([body.appendSystemPrompt, st.outputStyle]).chars : 0;
218
218
  if (foldedChars > MAX_SYSTEM_PROMPT_CHARS) {
219
219
  sendError(res, 400, "request.field_conflict", `appendSystemPrompt + settings.outputStyle fold into one system-prompt block — combined they must be at most ${MAX_SYSTEM_PROMPT_CHARS} characters (including the 2-char joiner)`, fieldConflictFacts("system_prompt_fold", ["appendSystemPrompt", "settings.outputStyle"], { count: foldedChars, cap: MAX_SYSTEM_PROMPT_CHARS }));
220
220
  return null;
@@ -749,12 +749,12 @@ export function createAdmission(ctx) {
749
749
  const seat = createSubmissionSeat({ deployment: deps.config, metrics: deps.metrics, dropSessionScopedOf: deps.dropSessionScopedOf, sessionWatch: deps.sessionWatch });
750
750
  try {
751
751
  const auth = await deps.authorize({ req, body: admitted, onBirth: (birth, face) => seat.noteBirth(birth, face) });
752
- const execution = await resolveExecutionContext(deps, { kind: "fresh", root: auth.sessionId, principal: auth.subject === undefined ? undefined : personOf(recoveredSubject(auth.subject)), judged: auth.head, req, deviceId: admitted.deviceId });
752
+ const execution = await resolveBeforeClaim(deps, { kind: "fresh", root: auth.sessionId, principal: auth.subject === undefined ? undefined : personOf(recoveredSubject(auth.subject)), judged: auth.head, req, deviceId: admitted.deviceId }, SUBMISSION_CLAIM_FACE);
753
753
  const engineNotices = [];
754
754
  const resolved = await deps.resolveSpec(admitted, req, auth, { leg: "fresh", ...(legRoute?.liveStream === true ? { liveStream: true } : {}), onNotice: (n) => engineNotices.push(n), lease, execution });
755
755
  const facts = legFactsOf(resolved);
756
756
  const runner = runnerFor(resolved);
757
- const spec = stampSpecRoot(resolved, execution, admitted);
757
+ const spec = stampSpecRoot(resolved, execution, cascade ? { use: "ladder_host" } : { use: "session", requireExistingSession: admitted.requireExistingSession });
758
758
  const model = modelIdOf(spec.model);
759
759
  const acceptance = createRunAcceptance({ sessionTitler: deps.sessionTitler }, { sessionId: spec.sessionId, objective: spec.objective, screened: promptScreenDeclared([deps.deploymentMods, spec.mods]), ...(model !== undefined ? { model } : {}) });
760
760
  seat.attachAcceptance(acceptance);