@sema-agent/server 7.125.1 → 7.127.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 (275) hide show
  1. package/MIGRATION.md +229 -2
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/USAGE.md +346 -145
  5. package/deploy/sema-up/README.md +1 -1
  6. package/deploy/sema-up/chart/templates/networkpolicy-sandbox.yaml +57 -0
  7. package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +32 -1
  8. package/deploy/sema-up/chart/templates/server-v4pro.yaml +3 -2
  9. package/deploy/sema-up/chart/templates/server.yaml +2 -1
  10. package/deploy/sema-up/chart/values.yaml +22 -0
  11. package/deploy/sema-up/cluster-up.sh +10 -10
  12. package/deploy/sema-up/kube-up.sh +20 -13
  13. package/deploy/sema-up/lib/dataroot.sh +1 -1
  14. package/deploy/sema-up/lib/envf.sh +5 -5
  15. package/deploy/sema-up/lib/flagshape.sh +5 -5
  16. package/deploy/sema-up/lib/lanes.py +1 -1
  17. package/deploy/sema-up/lib/node-addrs.sh +42 -0
  18. package/deploy/sema-up/lib/parse-machines.py +2 -2
  19. package/deploy/sema-up/lib/shell-access.sh +1 -1
  20. package/deploy/sema-up/preflight.sh +4 -4
  21. package/deploy/sema-up/sema-up.sh +33 -33
  22. package/deploy/sema-up/smoke.sh +3 -3
  23. package/deploy/sema-up/stack/compose.yaml +1 -1
  24. package/deploy/sema-up/stack/sema-stack-cluster.yaml +26 -3
  25. package/dist/approval-card.d.ts +20 -4
  26. package/dist/approval-content-kind.d.ts +38 -22
  27. package/dist/approval-content-kind.js +20 -4
  28. package/dist/audit.js +3 -0
  29. package/dist/bench/s1/arms.d.ts +9 -37
  30. package/dist/bench/s1/arms.js +4 -61
  31. package/dist/bench/s1/live-deps.d.ts +4 -10
  32. package/dist/bench/s1/live-deps.js +1 -222
  33. package/dist/bench/s1/oracle.d.ts +1 -1
  34. package/dist/bench/s1/reviewer.d.ts +1 -1
  35. package/dist/bench/s1/row.d.ts +5 -7
  36. package/dist/bench/s1/row.js +0 -2
  37. package/dist/bench/s1/run-firm.d.ts +2 -2
  38. package/dist/bench/s1/run-firm.js +1 -4
  39. package/dist/bench/s1/runner-ctx.d.ts +17 -29
  40. package/dist/bench/s1/runner-ctx.js +3 -7
  41. package/dist/bench/s1/tasks.d.ts +9 -46
  42. package/dist/bench/s1/tasks.js +4 -63
  43. package/dist/boot/execution-env.d.ts +38 -26
  44. package/dist/boot/execution-env.js +87 -41
  45. package/dist/boot/leader.d.ts +2 -1
  46. package/dist/boot/memory-boundary.d.ts +1 -1
  47. package/dist/boot/memory-consolidation.d.ts +37 -3
  48. package/dist/boot/memory-consolidation.js +18 -2
  49. package/dist/boot/parked-revive-gate.d.ts +0 -3
  50. package/dist/boot/parked-revive-gate.js +2 -2
  51. package/dist/boot/resolve-spec.d.ts +0 -2
  52. package/dist/boot/resolve-spec.js +12 -17
  53. package/dist/boot/retention-lane.d.ts +12 -1
  54. package/dist/boot/retention-lane.js +2 -0
  55. package/dist/boot/runner-deps.d.ts +7 -6
  56. package/dist/boot/runner-deps.js +5 -16
  57. package/dist/boot/session-faces.d.ts +7 -2
  58. package/dist/boot/session-faces.js +37 -1
  59. package/dist/boot/stage-03-store-backend.d.ts +1 -0
  60. package/dist/boot/stage-03-store-backend.js +2 -2
  61. package/dist/boot/stage-04-stores.d.ts +1 -0
  62. package/dist/boot/stage-05-execution-env.d.ts +2 -2
  63. package/dist/boot/stage-05-execution-env.js +7 -4
  64. package/dist/boot/stage-06-runners.d.ts +2 -3
  65. package/dist/boot/stage-06-runners.js +13 -9
  66. package/dist/boot/stage-07-capability-layer.d.ts +2 -3
  67. package/dist/boot/stage-07-capability-layer.js +3 -4
  68. package/dist/boot/stage-08-reapers.d.ts +2 -3
  69. package/dist/boot/stage-08-reapers.js +2 -2
  70. package/dist/boot/stage-09-leader.d.ts +2 -3
  71. package/dist/boot/stage-09-leader.js +2 -1
  72. package/dist/boot/stage-10-http-server.d.ts +2 -3
  73. package/dist/boot/stage-10-http-server.js +2 -2
  74. package/dist/boot/stores.d.ts +1 -0
  75. package/dist/boot/stores.js +4 -3
  76. package/dist/boot/web-admin.d.ts +5 -0
  77. package/dist/boot/web-admin.js +28 -14
  78. package/dist/brain.d.ts +2 -2
  79. package/dist/brain.js +1 -1
  80. package/dist/capabilities/builtin-tools.d.ts +2 -1
  81. package/dist/capabilities/composite-members.d.ts +1 -1
  82. package/dist/capabilities/composite-members.js +1 -1
  83. package/dist/capabilities/hands-lane.d.ts +9 -6
  84. package/dist/capabilities/hands-lane.js +2 -2
  85. package/dist/capabilities/memory-notice.d.ts +27 -22
  86. package/dist/capabilities/memory-notice.js +15 -17
  87. package/dist/capabilities/scenarios.d.ts +9 -14
  88. package/dist/capabilities/scenarios.js +5 -50
  89. package/dist/capability-facts.d.ts +22 -11
  90. package/dist/capability-facts.js +13 -6
  91. package/dist/config-catalog.js +14 -5
  92. package/dist/config-provider.d.ts +5 -1
  93. package/dist/config-provider.js +23 -4
  94. package/dist/config-types.d.ts +19 -2
  95. package/dist/config.js +41 -6
  96. package/dist/deployment-governance.d.ts +19 -21
  97. package/dist/deployment-governance.js +7 -8
  98. package/dist/env-name-allowlist-knobs.d.ts +5 -5
  99. package/dist/env-name-allowlist-knobs.js +1 -0
  100. package/dist/execution-lane-caps.d.ts +12 -0
  101. package/dist/execution-lane-caps.js +18 -4
  102. package/dist/fleet/subagent-tail-bus.js +15 -5
  103. package/dist/hooks/hook-runner.d.ts +84 -30
  104. package/dist/hooks/hook-runner.js +74 -41
  105. package/dist/http/admission.js +17 -10
  106. package/dist/http/durable-run-leg.d.ts +5 -4
  107. package/dist/http/durable-run-leg.js +2 -4
  108. package/dist/http/idempotency.d.ts +28 -6
  109. package/dist/http/idempotency.js +10 -2
  110. package/dist/http/principal-gate.d.ts +14 -0
  111. package/dist/http/principal-gate.js +7 -0
  112. package/dist/http/request-guards.d.ts +2 -1
  113. package/dist/http/request-guards.js +14 -14
  114. package/dist/http/resume-legs.d.ts +2 -2
  115. package/dist/http/resume-legs.js +20 -10
  116. package/dist/http/route-ctx.d.ts +15 -7
  117. package/dist/http/routes/a2a-serve.js +11 -7
  118. package/dist/http/routes/approvals-assistant.js +18 -3
  119. package/dist/http/routes/background.d.ts +1 -1
  120. package/dist/http/routes/background.js +1 -1
  121. package/dist/http/routes/capabilities.js +10 -2
  122. package/dist/http/routes/mcp-reconnect.js +1 -1
  123. package/dist/http/routes/runs.d.ts +4 -3
  124. package/dist/http/routes/runs.js +35 -14
  125. package/dist/http/routes/session-sync.js +14 -11
  126. package/dist/http/routes/sessions.js +9 -9
  127. package/dist/http/routes/side-query.js +6 -19
  128. package/dist/http/routes/tasks.js +29 -19
  129. package/dist/http/routes/workflows.js +8 -0
  130. package/dist/http/server.d.ts +4 -0
  131. package/dist/http/server.js +3 -5
  132. package/dist/http/submit-replay.d.ts +15 -8
  133. package/dist/http/submit-replay.js +6 -0
  134. package/dist/http/wire-types.d.ts +7 -9
  135. package/dist/index.d.ts +1 -1
  136. package/dist/index.js +1 -1
  137. package/dist/internal-run-env.d.ts +30 -0
  138. package/dist/internal-run-env.js +11 -0
  139. package/dist/leader/wire.js +3 -2
  140. package/dist/lsp/manager.js +1 -1
  141. package/dist/memory-scope.d.ts +19 -7
  142. package/dist/memory-scope.js +25 -8
  143. package/dist/observability/fail-open.d.ts +31 -3
  144. package/dist/observability/fail-open.js +31 -3
  145. package/dist/observability/metrics.d.ts +0 -1
  146. package/dist/observability/metrics.js +1 -4
  147. package/dist/observability/run-terminal.js +72 -4
  148. package/dist/permission-rule-vocab.d.ts +3 -2
  149. package/dist/permission-rule-vocab.js +1 -1
  150. package/dist/plan-cache-probe.d.ts +3 -2
  151. package/dist/plugins/approval-ask-store-sql.d.ts +1 -1
  152. package/dist/plugins/caching-session-store.d.ts +42 -22
  153. package/dist/plugins/caching-session-store.js +19 -9
  154. package/dist/plugins/checkpoint-store-sql.d.ts +42 -28
  155. package/dist/plugins/checkpoint-store-sql.js +57 -11
  156. package/dist/plugins/file-run-store.d.ts +25 -21
  157. package/dist/plugins/file-run-store.js +70 -34
  158. package/dist/plugins/k8s-exec-protocol.js +3 -0
  159. package/dist/plugins/local-checkpoint-store.d.ts +8 -5
  160. package/dist/plugins/local-checkpoint-store.js +5 -15
  161. package/dist/plugins/local-session-store.d.ts +66 -7
  162. package/dist/plugins/local-session-store.js +168 -101
  163. package/dist/plugins/mailbox-recipient-lifecycle.d.ts +1 -1
  164. package/dist/plugins/memory-run-store.d.ts +19 -3
  165. package/dist/plugins/memory-run-store.js +38 -3
  166. package/dist/plugins/one-time-migrations.d.ts +165 -39
  167. package/dist/plugins/one-time-migrations.js +51 -144
  168. package/dist/plugins/pg-pool.js +11 -1
  169. package/dist/plugins/pg-session-storage.d.ts +2 -0
  170. package/dist/plugins/pg-session-storage.js +9 -5
  171. package/dist/plugins/remote-env-adb.d.ts +3 -0
  172. package/dist/plugins/remote-env-adb.js +1 -0
  173. package/dist/plugins/remote-env-device.d.ts +3 -0
  174. package/dist/plugins/remote-env-device.js +1 -0
  175. package/dist/plugins/remote-env-e2b.d.ts +4 -0
  176. package/dist/plugins/remote-env-e2b.js +2 -1
  177. package/dist/plugins/remote-env-host.d.ts +23 -1
  178. package/dist/plugins/remote-env-host.js +20 -2
  179. package/dist/plugins/remote-env-k8s.d.ts +264 -41
  180. package/dist/plugins/remote-env-k8s.js +551 -82
  181. package/dist/plugins/remote-env-local-docker.d.ts +4 -0
  182. package/dist/plugins/remote-env-local-docker.js +2 -1
  183. package/dist/plugins/remote-env-ssh.d.ts +3 -0
  184. package/dist/plugins/remote-env-ssh.js +1 -0
  185. package/dist/plugins/retention-store-sql.d.ts +4 -1
  186. package/dist/plugins/retention-store-sql.js +11 -9
  187. package/dist/plugins/run-store-sql.d.ts +44 -6
  188. package/dist/plugins/run-store-sql.js +92 -27
  189. package/dist/plugins/session-capture-record-store-sql.d.ts +2 -3
  190. package/dist/plugins/session-store.d.ts +4 -3
  191. package/dist/plugins/session-store.js +1 -1
  192. package/dist/plugins/sql-driver.d.ts +2 -1
  193. package/dist/plugins/store-backend.d.ts +12 -5
  194. package/dist/plugins/store-backend.js +13 -4
  195. package/dist/plugins/store-contracts.d.ts +20 -3
  196. package/dist/plugins/store-open-wait.js +3 -0
  197. package/dist/plugins/tidb-pool.js +15 -3
  198. package/dist/plugins/tidb-session-storage.js +3 -2
  199. package/dist/plugins/tidb-session-store.d.ts +2 -0
  200. package/dist/plugins/tidb-session-store.js +6 -2
  201. package/dist/request-key-closure.d.ts +32 -7
  202. package/dist/request-key-closure.js +25 -9
  203. package/dist/router/route-orchestration.d.ts +3 -11
  204. package/dist/router/route-orchestration.js +0 -4
  205. package/dist/rule-entry-list.js +0 -4
  206. package/dist/rules-consent.js +3 -0
  207. package/dist/run-acceptance.d.ts +51 -0
  208. package/dist/run-acceptance.js +27 -0
  209. package/dist/run-local.d.ts +5 -4
  210. package/dist/run-local.js +23 -17
  211. package/dist/runs.d.ts +7 -11
  212. package/dist/runs.js +6 -16
  213. package/dist/sandbox-egress-policy.d.ts +61 -0
  214. package/dist/sandbox-egress-policy.js +235 -0
  215. package/dist/security.d.ts +7 -0
  216. package/dist/security.js +5 -0
  217. package/dist/server-secret-env.d.ts +10 -4
  218. package/dist/server-secret-env.js +4 -1
  219. package/dist/session-error-wire.d.ts +2 -2
  220. package/dist/session-incarnation.d.ts +46 -2
  221. package/dist/session-incarnation.js +14 -3
  222. package/dist/session-sync-kernel.d.ts +10 -3
  223. package/dist/session-titler.d.ts +5 -3
  224. package/dist/spec-fields.d.ts +24 -8
  225. package/dist/spec-fields.js +21 -2
  226. package/dist/task-workflow.d.ts +6 -4
  227. package/dist/task-workflow.js +3 -2
  228. package/dist/tool-approval.d.ts +78 -6
  229. package/dist/tool-approval.js +7 -3
  230. package/dist/trace/core-keyset-guard.d.ts +6 -5
  231. package/dist/trace/engine-notice-wire.d.ts +26 -1
  232. package/dist/trace/engine-notice-wire.js +7 -2
  233. package/dist/trace/ledger-events.d.ts +9 -3
  234. package/dist/trace/ledger-sink.d.ts +7 -0
  235. package/dist/trace/ledger-sink.js +14 -4
  236. package/dist/trace/project.d.ts +57 -11
  237. package/dist/trace/project.js +54 -8
  238. package/dist/trace/self-minted-wire.d.ts +94 -2
  239. package/dist/trace/self-minted-wire.js +28 -0
  240. package/dist/trace/tool-output-media.d.ts +26 -0
  241. package/dist/trace/tool-output-media.js +57 -0
  242. package/dist/trace/wire-projection-faces.d.ts +7 -1
  243. package/dist/trace/wire-projection-faces.js +2 -1
  244. package/dist/web-admin-client/apply-effective.d.ts +13 -7
  245. package/dist/web-admin-client/apply-effective.js +11 -26
  246. package/dist/web-admin-client/apply-ledger.d.ts +2 -2
  247. package/dist/web-admin-client/candidate-gate.d.ts +39 -4
  248. package/dist/web-admin-client/candidate-gate.js +9 -2
  249. package/dist/web-admin-client/facade.d.ts +2 -2
  250. package/dist/web-admin-client/http-client.d.ts +3 -1
  251. package/dist/web-admin-client/http-client.js +4 -3
  252. package/dist/web-admin-client/read-warnings.d.ts +33 -5
  253. package/dist/web-admin-client/read-warnings.js +41 -6
  254. package/dist/web-admin-client/types.d.ts +11 -5
  255. package/dist/workspace-cache.d.ts +45 -0
  256. package/dist/workspace-cache.js +30 -0
  257. package/dist/workspace-space-commit.d.ts +116 -0
  258. package/dist/workspace-space-commit.js +117 -0
  259. package/dist/workspace-space.d.ts +75 -9
  260. package/dist/workspace-space.js +52 -1
  261. package/package.json +4 -4
  262. package/dist/boot/session-incarnation-seat.d.ts +0 -41
  263. package/dist/boot/session-incarnation-seat.js +0 -69
  264. package/dist/capabilities/code-review-council.d.ts +0 -49
  265. package/dist/capabilities/code-review-council.js +0 -111
  266. package/dist/capabilities/collab-wire.d.ts +0 -36
  267. package/dist/capabilities/collab-wire.js +0 -2
  268. package/dist/capabilities/collab-workflows.d.ts +0 -47
  269. package/dist/capabilities/collab-workflows.js +0 -90
  270. package/dist/capabilities/prompts/code-review.d.ts +0 -25
  271. package/dist/capabilities/prompts/code-review.js +0 -39
  272. package/dist/capabilities/prompts/team.d.ts +0 -10
  273. package/dist/capabilities/prompts/team.js +0 -6
  274. package/dist/capabilities/team.d.ts +0 -113
  275. package/dist/capabilities/team.js +0 -70
package/dist/runs.js CHANGED
@@ -64,13 +64,6 @@ export function markChildrenStoppedByUserOnAbort(signal, taskId, principal) {
64
64
  }
65
65
  signal.addEventListener("abort", mark, { once: true });
66
66
  }
67
- export function isSessionConflictResult(result) {
68
- if (result.terminal.kind !== "failed")
69
- return false;
70
- if (result.terminal.code)
71
- return result.terminal.code === "conflict";
72
- return /stale write|concurrent write|conflict/i.test(result.terminal.message ?? "");
73
- }
74
67
  const PREPARE_REFUSAL_HTTP_STATUS = {
75
68
  "resume.session_not_found": 404,
76
69
  "rewind_snapshot.unresolvable": 422,
@@ -83,6 +76,7 @@ const PREPARE_REFUSAL_HTTP_STATUS = {
83
76
  "config.skill_listing_budget_invalid": 400,
84
77
  "config.user_invoked_skill_unknown": 400,
85
78
  "config.image_invalid": 400,
79
+ "config.tool_face_control": 400,
86
80
  "config.memory_capture_unsupported": 409,
87
81
  };
88
82
  export function resumeAtHttpStatus(result) {
@@ -169,12 +163,7 @@ export async function graftParkToolCallId(result, cs) {
169
163
  const id = await parkToolCallId(result, cs);
170
164
  return id === undefined ? result : { ...result, toolCallId: id };
171
165
  }
172
- export function evictIfConflict(runner, sessionId, result) {
173
- if (sessionId && isSessionConflictResult(result)) {
174
- runner.sessions.evict?.(sessionId);
175
- }
176
- }
177
- export async function runInBackground(runner, spec, runStore, taskId, metrics, principal, verify, cascade, instrumentDegenerate, planCacheProbe, persistThinking = false, inflight, preemptable, steerable, modelUsage, elicitation, elicitOwner, captureTurnAnchor, fleetPublisher, captureUserMessageAnchor, question, workflowCompletionInbox, subagentSteer, completionDiagLog, sendUserFile, promptManifests, approval, parkCheckpointReader, pendingEngineNotices, trustedInternals, userInputLedger) {
166
+ export async function runInBackground(runner, spec, runStore, taskId, metrics, principal, verify, cascade, instrumentDegenerate, planCacheProbe, persistThinking = false, inflight, preemptable, steerable, modelUsage, elicitation, elicitOwner, captureTurnAnchor, fleetPublisher, captureUserMessageAnchor, question, workflowCompletionInbox, subagentSteer, completionDiagLog, sendUserFile, promptManifests, approval, parkCheckpointReader, pendingEngineNotices, trustedInternals, userInputLedger, acceptance) {
178
167
  const startedAt = Date.now();
179
168
  metrics?.addGauge("runs_active", 1);
180
169
  fleetPublisher?.onStart();
@@ -337,7 +326,6 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
337
326
  if (verify !== undefined) {
338
327
  const result = await runWithVerification(runner, { ...spec, taskId, signal: cancelCtrl.signal }, verify, { ...fwdInternals, ...statusSeat });
339
328
  await flush();
340
- evictIfConflict(runner, spec.sessionId, result);
341
329
  await flushPromptManifest();
342
330
  const safe = await withModelUsage(sealResultForExit(result));
343
331
  await append("done", { result: safe });
@@ -345,13 +333,13 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
345
333
  const plane = outwardPlane(safe.terminal);
346
334
  await runStore.setTerminal(taskId, plane.status, safe, plane.errorMessage ?? null);
347
335
  await releaseKeyIfZeroWorkRefusal(runStore, taskId, safe);
336
+ acceptance?.settle(safe);
348
337
  recordDone(safe, plane.status);
349
338
  return;
350
339
  }
351
340
  if (cascade !== undefined) {
352
341
  const result = await runCascade(runner, { ...spec, taskId, signal: cancelCtrl.signal }, cascade, { ...fwdInternals, ...statusSeat });
353
342
  await flush();
354
- evictIfConflict(runner, spec.sessionId, result);
355
343
  await flushPromptManifest();
356
344
  const safe = await withModelUsage(sealResultForExit(result));
357
345
  await append("done", { result: safe });
@@ -359,6 +347,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
359
347
  const plane = outwardPlane(safe.terminal);
360
348
  await runStore.setTerminal(taskId, plane.status, safe, plane.errorMessage ?? null);
361
349
  await releaseKeyIfZeroWorkRefusal(runStore, taskId, safe);
350
+ acceptance?.settle(safe);
362
351
  recordDone(safe, plane.status);
363
352
  return;
364
353
  }
@@ -372,6 +361,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
372
361
  }
373
362
  fleetPublisher?.onEvent(ev);
374
363
  await userInputLedger?.observeLegEvent(stream, ev);
364
+ acceptance?.observe(ev);
375
365
  await flushPromptManifest();
376
366
  switch (ev.type) {
377
367
  case "text_delta":
@@ -405,7 +395,6 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
405
395
  await parkNeedsReview(ev.result);
406
396
  continue;
407
397
  }
408
- evictIfConflict(runner, spec.sessionId, ev.result);
409
398
  {
410
399
  const settled = causeAtDone !== undefined ? cancelledResult(causeAtDone, ev.result) : ev.result;
411
400
  settledAsCancel = failureCodeOf(settled.terminal) === CANCELLED_CODE;
@@ -490,6 +479,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
490
479
  settledAsCancel = failureCodeOf(reached.result.terminal) === CANCELLED_CODE;
491
480
  await runStore.setTerminal(taskId, plane.status, reached.result, plane.errorMessage ?? null);
492
481
  await releaseKeyIfZeroWorkRefusal(runStore, taskId, reached.result);
482
+ acceptance?.settle(reached.result);
493
483
  }
494
484
  else if (reached?.kind === "suspended") {
495
485
  await runStore.setSuspended(taskId, reached.result);
@@ -0,0 +1,61 @@
1
+ /** `K8S_SANDBOX_IPV6` 闭集:`enabled` = pod 保留平台给的 IPv6(pod spec 与未引入本旋钮时逐字同形);`disabled` = pod 级 sysctl 关掉 IPv6。 */
2
+ export declare const SANDBOX_IPV6_WORDS: readonly ["enabled", "disabled"];
3
+ export type SandboxIpv6 = (typeof SANDBOX_IPV6_WORDS)[number];
4
+ /** 旋钮缺席时的姿态(缺省不动 pod:关 v6 要每个节点的 kubelet 放行这两只 sysctl,没放行的集群 pod 起不来)。 */
5
+ export declare const SANDBOX_IPV6_DEFAULT: SandboxIpv6;
6
+ /** 一只 pod 级 sysctl(k8s `PodSecurityContext.sysctls[]` 的形)。 */
7
+ export interface PodSysctl {
8
+ readonly name: string;
9
+ readonly value: string;
10
+ }
11
+ /**
12
+ * 沙箱 pod 的 sysctl 只有这一个属主:`disabled` ⇒ `all`(已有接口)与 `default`(之后建的接口)都关 IPv6;`enabled` ⇒ 不铸。
13
+ * 两只都不在 k8s 的 safe sysctl 表里 ⇒ 每个节点 kubelet 要 `--allowed-unsafe-sysctls` 点名它们,否则 pod 起不来(车道连沙箱失败,响亮)。
14
+ */
15
+ export declare function sandboxPodSysctls(ipv6: SandboxIpv6): readonly PodSysctl[] | undefined;
16
+ /** 自检读的两张表(车道侧各一只 GET)。 */
17
+ export type EgressPolicyRead = "nodes" | "networkpolicies";
18
+ /** 节点地址里自检判的两类(`Hostname` / `InternalDNS` / `ExternalDNS` 不是地址)。 */
19
+ type NodeAddressType = "InternalIP" | "ExternalIP";
20
+ export type SandboxEgressGap = {
21
+ readonly kind: "node_address_open";
22
+ readonly node: string;
23
+ readonly addressType: NodeAddressType;
24
+ readonly address: string;
25
+ readonly policy: string;
26
+ readonly ports: string;
27
+ } | {
28
+ readonly kind: "policy_absent";
29
+ } | {
30
+ readonly kind: "ipv6_enabled";
31
+ } | {
32
+ readonly kind: "unreadable";
33
+ readonly what: EgressPolicyRead;
34
+ readonly detail: string;
35
+ };
36
+ /** 一只 GET 的读数:答了 2xx ⇒ 体;否则一句为什么读不到(HTTP 码 / 错误句)。 */
37
+ export type ApiRead = {
38
+ readonly ok: true;
39
+ readonly json: unknown;
40
+ } | {
41
+ readonly ok: false;
42
+ readonly detail: string;
43
+ };
44
+ export interface EgressPolicyInputs {
45
+ readonly nodes: ApiRead;
46
+ readonly policies: ApiRead;
47
+ /** 沙箱 pod 的标签(车道建 pod 时铸的那一份)。 */
48
+ readonly podLabels: Readonly<Record<string, string>>;
49
+ readonly ipv6: SandboxIpv6;
50
+ }
51
+ /**
52
+ * 读数 → 缺口(纯函数、全函数:不抛;顺序:读不到的表 → 策略缺席 → 逐节点逐地址 → IPv6)。一张表读不到,只跳过要它的那一判;另一张照判。
53
+ */
54
+ export declare function sandboxEgressGaps(inputs: EgressPolicyInputs): SandboxEgressGap[];
55
+ /** 读数的说法:每个缺口一行 warn `sandbox_egress_policy_gap` + 一次 `recordFailOpen`(P-DEBT);零缺口一行 info `sandbox_egress_policy_checked`。 */
56
+ export declare function sayEgressPolicyReading(gaps: readonly SandboxEgressGap[], logger: {
57
+ info(msg: string, fields?: Record<string, unknown>): void;
58
+ warn(msg: string, fields?: Record<string, unknown>): void;
59
+ } | undefined, namespace: string): void;
60
+ export {};
61
+ //# sourceMappingURL=sandbox-egress-policy.d.ts.map
@@ -0,0 +1,235 @@
1
+ import { BlockList, isIP } from "node:net";
2
+ import { recordFailOpen } from "./observability/fail-open.js";
3
+ export const SANDBOX_IPV6_WORDS = ["enabled", "disabled"];
4
+ export const SANDBOX_IPV6_DEFAULT = "enabled";
5
+ export function sandboxPodSysctls(ipv6) {
6
+ switch (ipv6) {
7
+ case "enabled":
8
+ return undefined;
9
+ case "disabled":
10
+ return [
11
+ { name: "net.ipv6.conf.all.disable_ipv6", value: "1" },
12
+ { name: "net.ipv6.conf.default.disable_ipv6", value: "1" },
13
+ ];
14
+ }
15
+ }
16
+ class Unjudgeable extends Error {
17
+ }
18
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
19
+ function recordAt(v, where) {
20
+ if (!isRecord(v))
21
+ throw new Unjudgeable(`${where} is not an object`);
22
+ return v;
23
+ }
24
+ function optRecordAt(v, where) {
25
+ return v === undefined || v === null ? undefined : recordAt(v, where);
26
+ }
27
+ function arrayAt(v, where) {
28
+ if (!Array.isArray(v))
29
+ throw new Unjudgeable(`${where} is not a list`);
30
+ return v;
31
+ }
32
+ function optArrayAt(v, where) {
33
+ return v === undefined || v === null ? undefined : arrayAt(v, where);
34
+ }
35
+ function stringAt(v, where) {
36
+ if (typeof v !== "string")
37
+ throw new Unjudgeable(`${where} is not a string`);
38
+ return v;
39
+ }
40
+ function listItems(json, what) {
41
+ const items = isRecord(json) ? json.items : undefined;
42
+ if (!Array.isArray(items))
43
+ throw new Unjudgeable(`${what} answer carries no items list`);
44
+ return items;
45
+ }
46
+ const SELECTOR_OPERATORS = ["In", "NotIn", "Exists", "DoesNotExist"];
47
+ function readSelector(v, where) {
48
+ const sel = optRecordAt(v, where) ?? {};
49
+ const labels = optRecordAt(sel.matchLabels, `${where}.matchLabels`) ?? {};
50
+ const matchLabels = {};
51
+ for (const [k, val] of Object.entries(labels))
52
+ matchLabels[k] = stringAt(val, `${where}.matchLabels.${k}`);
53
+ const matchExpressions = (optArrayAt(sel.matchExpressions, `${where}.matchExpressions`) ?? []).map((e, i) => {
54
+ const x = recordAt(e, `${where}.matchExpressions[${i}]`);
55
+ const operator = stringAt(x.operator, `${where}.matchExpressions[${i}].operator`);
56
+ if (!SELECTOR_OPERATORS.includes(operator))
57
+ throw new Unjudgeable(`${where} operator ${JSON.stringify(operator)} is not one of ${SELECTOR_OPERATORS.join(" / ")}`);
58
+ return { key: stringAt(x.key, `${where}.matchExpressions[${i}].key`), operator, values: (optArrayAt(x.values, `${where}.matchExpressions[${i}].values`) ?? []).map((y, j) => stringAt(y, `${where}.matchExpressions[${i}].values[${j}]`)) };
59
+ });
60
+ return { matchLabels, matchExpressions };
61
+ }
62
+ function selects(sel, labels) {
63
+ for (const [k, v] of Object.entries(sel.matchLabels))
64
+ if (labels[k] !== v)
65
+ return false;
66
+ for (const e of sel.matchExpressions) {
67
+ const has = Object.prototype.hasOwnProperty.call(labels, e.key);
68
+ switch (e.operator) {
69
+ case "In":
70
+ if (!has || !e.values.includes(labels[e.key]))
71
+ return false;
72
+ break;
73
+ case "NotIn":
74
+ if (has && e.values.includes(labels[e.key]))
75
+ return false;
76
+ break;
77
+ case "Exists":
78
+ if (!has)
79
+ return false;
80
+ break;
81
+ case "DoesNotExist":
82
+ if (has)
83
+ return false;
84
+ break;
85
+ default:
86
+ throw new Unjudgeable(`podSelector operator ${JSON.stringify(e.operator)} is not one of ${SELECTOR_OPERATORS.join(" / ")}`);
87
+ }
88
+ }
89
+ return true;
90
+ }
91
+ function readCidr(v, where) {
92
+ const cidr = stringAt(v, where);
93
+ parseCidr(cidr);
94
+ return cidr;
95
+ }
96
+ function readRule(v, where) {
97
+ const r = recordAt(v, where);
98
+ const to = optArrayAt(r.to, `${where}.to`);
99
+ const blocks = [];
100
+ for (const [i, peer] of (to ?? []).entries()) {
101
+ const ip = optRecordAt(recordAt(peer, `${where}.to[${i}]`).ipBlock, `${where}.to[${i}].ipBlock`);
102
+ if (ip === undefined)
103
+ continue;
104
+ blocks.push({ cidr: readCidr(ip.cidr, `${where}.to[${i}].ipBlock.cidr`), except: (optArrayAt(ip.except, `${where}.to[${i}].ipBlock.except`) ?? []).map((x, j) => readCidr(x, `${where}.to[${i}].ipBlock.except[${j}]`)) });
105
+ }
106
+ const ports = optArrayAt(r.ports, `${where}.ports`);
107
+ const portText = ports === undefined || ports.length === 0
108
+ ? "all"
109
+ : ports
110
+ .map((p, i) => {
111
+ const x = recordAt(p, `${where}.ports[${i}]`);
112
+ return `${typeof x.protocol === "string" ? x.protocol : "TCP"}/${typeof x.port === "number" || typeof x.port === "string" ? x.port : "*"}${typeof x.endPort === "number" ? `-${x.endPort}` : ""}`;
113
+ })
114
+ .join(",");
115
+ return { to: to === undefined || to.length === 0 ? undefined : blocks, ports: portText };
116
+ }
117
+ function isolatesEgress(spec, where) {
118
+ const types = optArrayAt(spec.policyTypes, `${where}.policyTypes`);
119
+ return types !== undefined ? types.map((t, i) => stringAt(t, `${where}.policyTypes[${i}]`)).includes("Egress") : spec.egress !== undefined && spec.egress !== null;
120
+ }
121
+ function egressPolicies(json, podLabels) {
122
+ const out = [];
123
+ for (const [i, item] of listItems(json, "networkpolicies").entries()) {
124
+ const p = recordAt(item, `networkpolicies[${i}]`);
125
+ const name = stringAt(recordAt(p.metadata, `networkpolicies[${i}].metadata`).name, `networkpolicies[${i}].metadata.name`);
126
+ const spec = recordAt(p.spec, `networkpolicy ${name} spec`);
127
+ if (!isolatesEgress(spec, `networkpolicy ${name}`) || !selects(readSelector(spec.podSelector, `networkpolicy ${name} podSelector`), podLabels))
128
+ continue;
129
+ out.push({ name, rules: (optArrayAt(spec.egress, `networkpolicy ${name} egress`) ?? []).map((r, j) => readRule(r, `networkpolicy ${name} egress[${j}]`)) });
130
+ }
131
+ return out;
132
+ }
133
+ function nodeAddresses(json) {
134
+ const out = [];
135
+ for (const [i, item] of listItems(json, "nodes").entries()) {
136
+ const n = recordAt(item, `nodes[${i}]`);
137
+ const node = stringAt(recordAt(n.metadata, `nodes[${i}].metadata`).name, `nodes[${i}].metadata.name`);
138
+ const addresses = arrayAt(recordAt(n.status, `node ${node} status`).addresses, `node ${node} status.addresses`);
139
+ let seen = 0;
140
+ for (const [j, a] of addresses.entries()) {
141
+ const x = recordAt(a, `node ${node} status.addresses[${j}]`);
142
+ const type = stringAt(x.type, `node ${node} status.addresses[${j}].type`);
143
+ if (type !== "InternalIP" && type !== "ExternalIP")
144
+ continue;
145
+ const address = stringAt(x.address, `node ${node} status.addresses[${j}].address`);
146
+ if (isIP(address) === 0)
147
+ throw new Unjudgeable(`node ${node} ${type} ${JSON.stringify(address)} is not an IP address`);
148
+ out.push({ node, addressType: type, address });
149
+ seen++;
150
+ }
151
+ if (seen === 0)
152
+ throw new Unjudgeable(`node ${node} reports no InternalIP / ExternalIP`);
153
+ }
154
+ return out;
155
+ }
156
+ function parseCidr(cidr) {
157
+ const slash = cidr.indexOf("/");
158
+ const net = slash < 0 ? cidr : cidr.slice(0, slash);
159
+ const fam = isIP(net);
160
+ const max = fam === 6 ? 128 : 32;
161
+ const prefix = slash < 0 ? max : Number(cidr.slice(slash + 1));
162
+ if (fam === 0 || !/^\d+$/.test(slash < 0 ? String(max) : cidr.slice(slash + 1)) || prefix > max)
163
+ throw new Unjudgeable(`ipBlock cidr ${JSON.stringify(cidr)} does not parse`);
164
+ return { net, prefix, family: fam === 6 ? "ipv6" : "ipv4" };
165
+ }
166
+ function inCidr(address, cidr) {
167
+ const c = parseCidr(cidr);
168
+ if ((isIP(address) === 6 ? "ipv6" : "ipv4") !== c.family)
169
+ return false;
170
+ const list = new BlockList();
171
+ list.addSubnet(c.net, c.prefix, c.family);
172
+ return list.check(address, c.family);
173
+ }
174
+ function ruleAllows(rule, address) {
175
+ if (rule.to === undefined)
176
+ return true;
177
+ return rule.to.some((b) => inCidr(address, b.cidr) && !b.except.some((x) => inCidr(address, x)));
178
+ }
179
+ function readTable(read, what, parse, gaps) {
180
+ if (!read.ok) {
181
+ gaps.push({ kind: "unreadable", what, detail: read.detail });
182
+ return undefined;
183
+ }
184
+ try {
185
+ return parse(read.json);
186
+ }
187
+ catch (e) {
188
+ gaps.push({ kind: "unreadable", what, detail: e instanceof Unjudgeable ? e.message : `answer not understood: ${e instanceof Error ? e.message : String(e)}` });
189
+ return undefined;
190
+ }
191
+ }
192
+ export function sandboxEgressGaps(inputs) {
193
+ const gaps = [];
194
+ const policies = readTable(inputs.policies, "networkpolicies", (json) => egressPolicies(json, inputs.podLabels), gaps);
195
+ const addresses = readTable(inputs.nodes, "nodes", nodeAddresses, gaps);
196
+ if (policies !== undefined && policies.length === 0)
197
+ gaps.push({ kind: "policy_absent" });
198
+ if (policies !== undefined && policies.length > 0 && addresses !== undefined) {
199
+ for (const a of addresses) {
200
+ for (const p of policies) {
201
+ const rule = p.rules.find((r) => ruleAllows(r, a.address));
202
+ if (rule !== undefined) {
203
+ gaps.push({ kind: "node_address_open", node: a.node, addressType: a.addressType, address: a.address, policy: p.name, ports: rule.ports });
204
+ break;
205
+ }
206
+ }
207
+ }
208
+ }
209
+ if (inputs.ipv6 === "enabled")
210
+ gaps.push({ kind: "ipv6_enabled" });
211
+ return gaps;
212
+ }
213
+ function effectOf(gap) {
214
+ switch (gap.kind) {
215
+ case "node_address_open":
216
+ return "sandbox pods can reach this node address through the egress policy — add it to sandbox.networkPolicy.nodeCidrs (kube-up lists every node's addresses) or narrow the allowing rule";
217
+ case "policy_absent":
218
+ return "no NetworkPolicy selects the sandbox pods for egress — the policy layer does not restrict sandbox egress (chart sandbox.networkPolicy.enabled, or the CNI's own policy)";
219
+ case "ipv6_enabled":
220
+ return "sandbox pods keep IPv6 — an enforcement layer that filters only IPv4 leaves the pods' IPv6 (link-local included) unfiltered; K8S_SANDBOX_IPV6=disabled turns it off in the pod (needs the two sysctls allowed by every node's kubelet)";
221
+ case "unreadable":
222
+ return `the ${gap.what} could not be read — this part of the egress check did not run (the server's k8s account needs ${gap.what === "nodes" ? "get/list on nodes (cluster scope)" : "get/list on networkpolicies in the sandbox namespace"})`;
223
+ }
224
+ }
225
+ export function sayEgressPolicyReading(gaps, logger, namespace) {
226
+ if (gaps.length === 0) {
227
+ logger?.info("sandbox_egress_policy_checked", { namespace, gaps: 0 });
228
+ return;
229
+ }
230
+ for (const gap of gaps) {
231
+ recordFailOpen("server.sandbox-egress.policy-gap", `kind=${gap.kind}${gap.kind === "unreadable" ? ` what=${gap.what}` : ""}`);
232
+ logger?.warn("sandbox_egress_policy_gap", { namespace, ...gap, effect: effectOf(gap) });
233
+ }
234
+ }
235
+ //# sourceMappingURL=sandbox-egress-policy.js.map
@@ -195,6 +195,13 @@ export type OwnerAwareSessionStore = SessionStore & {
195
195
  /** True iff `s` matches the canonical uuidv7 shape core mints (see {@link UUIDV7_RE}). */
196
196
  export declare function isUuidV7(s: string): boolean;
197
197
  export declare function isUuidShape(s: string): boolean;
198
+ /** The submit door's refusal of a caller-supplied session id that is not a canonical uuid, or `undefined` when the value
199
+ * is absent / not a string (the authorizer only ever reads a string `sessionId`; a non-string never enters the session
200
+ * slot) / a canonical uuid. The sentence opens with the store verbs' sentence (`invalid session id (must be a canonical
201
+ * uuid)`, same `request.id_invalid` code) and then names the remedy — a self-chosen name is not a session id; the caller
202
+ * keeps its own name → uuid mapping. HTTP admission answers it as `400 request.id_invalid`; `run-local --session` prints
203
+ * it as a usage error: ONE judge, ONE sentence, no second door-specific rule. */
204
+ export declare function sessionIdShapeIssue(sessionId: unknown): string | undefined;
198
205
  /** An HTTP error the server maps to a status code (instead of a generic 500). `code` (optional) is a stable
199
206
  * machine-readable identifier the HTTP layer echoes as `{code}` beside the human `{error}` message — the
200
207
  * "typed rejection" posture (structured code on the wire, prose stays with the shell/web).
package/dist/security.js CHANGED
@@ -25,6 +25,11 @@ const UUID_ANY_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}
25
25
  export function isUuidShape(s) {
26
26
  return UUID_ANY_RE.test(s);
27
27
  }
28
+ export function sessionIdShapeIssue(sessionId) {
29
+ if (typeof sessionId !== "string" || isUuidShape(sessionId))
30
+ return undefined;
31
+ return "invalid session id (must be a canonical uuid); a self-chosen session name is not accepted — use a uuid (any version) and keep your own name-to-uuid mapping";
32
+ }
28
33
  export class HttpError extends Error {
29
34
  status;
30
35
  code;
@@ -19,7 +19,7 @@
19
19
  * {@link scrubServerSecrets} = core `scrubSecretEnv`(按形)∘ 本仓按名两臂:
20
20
  * · 目录臂 —— 配置目录(`config-catalog.ts`)里 `type:"secret"` 的每一行,从目录机器派生,不手抄第二份名单;
21
21
  * · 名单型旋钮臂 —— `ENV_NAME_ALLOWLIST_KNOBS` 的每一只(`env-name-allowlist-knobs.ts`,表键派生):名单**原文**可能夹着一枚被错展开进来
22
- * 的令牌,按凭据类对待。服务侧三只(`LSP_ENV_ALLOW` S-699 / `E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV` S-713)同时是目录 secret 行,
22
+ * 的令牌,按凭据类对待。服务侧四只(`LSP_ENV_ALLOW` S-699 / `E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV` S-713 / `K8S_SANDBOX_ENV`)同时是目录 secret 行,
23
23
  * 目录臂先认;本臂接住的是**不在服务目录里**的那一只 —— bake-runner 的 `BAKE_ENV_ALLOW`(它不是服务进程的配置)。
24
24
  * core 形状规则的缺口由 core 源头修 —— 7.30.1 #1072 补 `KEYS|TOKENS|PASSWORDS`(S-616),7.33.7 #1180 按名认 OTLP 头变量一族(7.103.2 车PM 提货;目录臂照旧点名
25
25
  * `OTEL_EXPORTER_OTLP_HEADERS` = 双保险,互证钉在 `test/secret-env-scrub-disclosure.test.ts`),目录臂独担的只剩名单型旋钮(值是变量名单,core 本来就不该认它们)。
@@ -32,10 +32,10 @@
32
32
  * ## 点名放行(S-699 / S-713,一只谓词各处用)
33
33
  * {@link allowlistedEnv} 是本仓「一张名单点到哪些 env 键,就把它们从源 env **原样**交给子进程」的唯一实现。调用:
34
34
  * host shell 的 `inheritEnv:[names]`(`plugins/remote-env-host.ts`,hermetic 形)、{@link hostSubprocessEnv}(剥后放回形:本地语言服务器的
35
- * `LSP_ENV_ALLOW`、bake-runner 的 `BAKE_ENV_ALLOW`)、沙箱带外 env 的 `E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV`(`config.ts`,S-713 起)。
35
+ * `LSP_ENV_ALLOW`、bake-runner 的 `BAKE_ENV_ALLOW`)、沙箱带外 env 的 `E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV` / `K8S_SANDBOX_ENV`(`config.ts`,S-713 起)。
36
36
  * 名单型旋钮的**名单**另由 `config.ts` 的同一只解析器 `parseEnvNameAllowlist` 判形(形错拒启 / 源里没有的名按序号 warn),本函数只管取值。
37
37
  * 「点名」在各处同义:点到的键原样带值(空串值照带),密钥形也照过(点名 = 部署方显式承担);源里没有的名不产生条目。
38
- * 沙箱两腿不继承本进程环境:进沙箱的本进程值只有点到的键(与 host shell 的 hermetic 形同一侧)。
38
+ * 沙箱三腿不继承本进程环境:进沙箱的本进程值只有点到的键(与 host shell 的 hermetic 形同一侧)。
39
39
  * host shell 与「剥后放回」形**只在没点到的键上**不同,这是有意保留的差别:host shell 的 `string[]` 形是 hermetic 白名单(与 core
40
40
  * `NodeExecutionEnv` 同名选项同义,没点到的一律不继承);语言服务器 / bake-runner 在 {@link scrubServerSecrets} 的结果之上放回点名键
41
41
  * (没点到的照缺省剥,`PATH` / `HOME` 这类非密钥键照常在 —— hermetic 形会让子进程当场找不到自己的工具链)。
@@ -70,7 +70,7 @@ export declare const CATALOG_SECRET_ENV_NAMES: ReadonlySet<string>;
70
70
  * 各腿不直接调它 —— 经 {@link hostSubprocessEnv}(剥 + 放回 + 留痕一只口)。
71
71
  *
72
72
  * **只为宿主子进程组合**。本包跑在别的平台上的子进程(ssh / k8s / adb / e2b / docker 车道)的 env 不经本函数:那些车道不继承本进程 env,只交
73
- * 调用方显式给的 env 与部署方点名的键(`E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV`,经 {@link allowlistedEnv})。(core 7.33.6 #1159 的平台尾参随
73
+ * 调用方显式给的 env 与部署方点名的键(`E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV` / `K8S_SANDBOX_ENV`,经 {@link allowlistedEnv})。(core 7.33.6 #1159 的平台尾参随
74
74
  * core 7.35.0 #1201 删除 —— git 的 env 配置族成了豁免族,不再有「按子进程平台读记录」这件事,S-877。)
75
75
  */
76
76
  export declare function scrubServerSecrets(env: NodeJS.ProcessEnv, findings: ServerSecretEnvFinding[]): NodeJS.ProcessEnv;
@@ -100,6 +100,12 @@ export declare function hostSubprocessEnv(o: {
100
100
  site: SecretEnvScrubSite;
101
101
  allow: readonly string[];
102
102
  }, sourceEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
103
+ /**
104
+ * {@link hostSubprocessEnv} 的**组合本体**(不披露、不计数)—— 同一只组合,两个入口:真要交给子进程的现场经 `hostSubprocessEnv`(组合 + 披露);
105
+ * 只想知道「子进程**会**拿到什么」的读者(core 7.49.0 #1554:host 车道的 `ExecutionEnv.cdPath` 要报子进程真拿到的 `CDPATH`)读这一只 ——
106
+ * 既不另写一份组合(第二份会漂),也不把一次没发生的 spawn 记进剥除计数。
107
+ */
108
+ export declare function composeHostSubprocessEnv(allow: readonly string[], sourceEnv?: NodeJS.ProcessEnv, findings?: ServerSecretEnvFinding[]): NodeJS.ProcessEnv;
103
109
  /** git 子进程腿的现场(闭集里名字带 `.git-` 的那几只 —— 编译期筛;它们都没有点名放行面)。 */
104
110
  export type GitSubprocessSite = Extract<SecretEnvScrubSite, `${string}.git-${string}`>;
105
111
  /**
@@ -24,10 +24,13 @@ export function scrubServerSecrets(env, findings) {
24
24
  }
25
25
  export function hostSubprocessEnv(o, sourceEnv = process.env) {
26
26
  const scrubFindings = [];
27
- const composed = { ...scrubServerSecrets(sourceEnv, scrubFindings), ...allowlistedEnv(o.allow, sourceEnv) };
27
+ const composed = composeHostSubprocessEnv(o.allow, sourceEnv, scrubFindings);
28
28
  recordSecretEnvScrub(o.site, scrubFindings.filter((f) => composed[f.key] !== sourceEnv[f.key]), o.allow);
29
29
  return composed;
30
30
  }
31
+ export function composeHostSubprocessEnv(allow, sourceEnv = process.env, findings = []) {
32
+ return { ...scrubServerSecrets(sourceEnv, findings), ...allowlistedEnv(allow, sourceEnv) };
33
+ }
31
34
  export function gitSubprocessEnv(o) {
32
35
  return {
33
36
  ...hostSubprocessEnv({ site: o.site, allow: [] }),
@@ -29,8 +29,8 @@
29
29
  * 列表里的缺席),码位各不相同,不由本表统一。
30
30
  * · `invalid_entry` / `invalid_fork_target` / `placement_refused` —— 留白:本仓今天没有一条 wire 口把它们作为自己的
31
31
  * 应答(它们在 core 的追加 / 分叉 / 放置面上抛,落进各站点既有的处置或通用 500)。给它们起名是另一件事、另一张票。
32
- * · `storage` / `conflict` / `unknown` —— 留白:`conflict` 由提交腿的 CAS 冲突臂按结果处置(`runs.ts`
33
- * `isSessionConflictResult`),`storage` / `unknown` 是故障类,通用 500 就是它们今天的应答。
32
+ * · `storage` / `conflict` / `unknown` —— 留白:`conflict` 是 run 的写撞上真并发写,落成那条 run 的 `failed` 终局
33
+ * (`terminal.code: "conflict"`),提交腿不另作处置;`storage` / `unknown` 是故障类,通用 500 就是它们今天的应答。
34
34
  *
35
35
  * core 给闭集加一个词 ⇒ 下面的 `switch` 编译红,由人裁定它在两面上的答复,而不是让它静默落进某个恰好存在的臂
36
36
  * ([ref] 词表纪律;同形先例 `session-policy-wire.ts` 的 `sessionPolicyErrorHttp`)。
@@ -1,3 +1,33 @@
1
+ /**
2
+ * S-790 / S-506([ref] §2)—— **会话化身**:一个会话 id 是座位,化身是这一位住客。
3
+ *
4
+ * 会话 id 由调用方自选,删掉之后同一个 id 可以被别人(或同一个人)重新登记(`boot/session-faces.ts` 的级联删除注自认)。
5
+ * 本仓大量持久状态只按会话 id 分区,于是「上一位住客留下的东西」会在下一位名下继续起作用。化身令牌给「是不是同一位住客」
6
+ * 一个可比的答案 —— 今天只能比属主,同一个人删了重建就比不出来。
7
+ *
8
+ * ## 规则(全仓唯一处写)
9
+ * · **铸点只在新建行**:会话店的 `register` / 无 id 创建 / fork / session-sync 的导入新行。已存在的行一个字节都不动
10
+ * (与 `owner` 同一条 first-write 律);session-sync 的整段换装(replace / staged commit)换的是**同一位住客**的日志,
11
+ * 化身沿用。删除随行消失,同 id 重新登记铸新的。
12
+ * · **值是不透明随机串**(`randomUUID()`,v4:没有时间戳、没有机器信息 —— 刻意不用 uuidv7,那一形带铸造时刻)。只拿来比较。
13
+ * · **一格、一个解码**:住客令牌是会话**行**上的一格 —— SQL `session_meta.incarnation`(两方言)、local 后端的会话元数据边档那一格
14
+ * (`plugins/local-session-store.ts` 的 `session-incarnations.json`)。每只店对它只有**一次读**(`sessionFaceOf`),行上的值只经
15
+ * {@link incarnationFromCell} 解码;别的读法都从这一次读派生(下一节),没有第二份。
16
+ * · **读口唯一**:会话店的 `sessionFaceOf(sessionId) → { owner, incarnation } | undefined`,一次读同时给两样
17
+ * (两次独立读在删除 / 重登记的缝里能拼出混世代组合,与 `getHead` 同一条理由)。`undefined` = 没有这只会话。
18
+ * · **读到空化身 ⇒ 响亮拒**({@link SessionIncarnationMissingError}):一次性迁移(`plugins/one-time-migrations.ts` 登记表 M5)
19
+ * 之后,空值只可能来自没铸化身的写腿或人为改库 —— 把它当「通配」就是让任何人都能和这一行比成同一位住客。
20
+ * · **不上 wire、不进模型上下文、不进日志正文**:日志里要点名时只打 {@link incarnationLogPrefix}(前 8 位)。
21
+ *
22
+ * ## 谁读它(全部派生自那一格)
23
+ * · `/decide` 的 parked 赎回腿(`parked-decide.ts`):claim 前读一次、claim 后再读一次,会话不在或化身变了 ⇒ 回滚 claim、
24
+ * 答既有的 `409 decide.not_parked`;赎回面(`parked-revive-surface.ts`)只在属主相等**且**两次读是同一位住客时交继承座。
25
+ * · core 的记忆引擎:Runner 读**它取到的那只会话**的 `getMetadata().incarnation`(SQL 两店的 wake 带出这一列;local 店交出的会话
26
+ * 在元数据上答边档那一格),不再有宿主座。
27
+ * · 按 id 读的消费方(没取会话的那一侧 —— k8s 工作区空间的分区,`boot/execution-env.ts`):core 的 `SessionStore.incarnationOf`,
28
+ * 三只店都实装为 `sessionFaceOf` 的化身那一半;缓存包装层原样转发。「店有没有这只读」只判一处:{@link incarnationReadOf}。
29
+ */
30
+ import type { SessionStore } from "@sema-agent/core";
1
31
  /** 会话元数据的一次读:属主 + 化身(同一行、同一次读)。 */
2
32
  export interface SessionFace {
3
33
  /** `null` = 登记为无主的会话。 */
@@ -31,10 +61,24 @@ export declare class SessionIncarnationMissingError extends Error {
31
61
  constructor(sessionId: string);
32
62
  }
33
63
  /**
34
- * 店把一行原始列读成 {@link SessionFace} 的唯一口(三只店共用 —— 空值判据只写这一处)。
35
- * `incarnation` 不是非空字符串 ⇒ {@link SessionIncarnationMissingError}。
64
+ * 行上化身那一格的**唯一解码**(三只店的 `sessionFaceOf` 与 SQL 两店 wake 带出的会话元数据都经它 —— 空值判据只写这一处)。
65
+ * 不是非空字符串 ⇒ {@link SessionIncarnationMissingError}。
36
66
  */
67
+ export declare function incarnationFromCell(sessionId: string, cell: unknown): string;
68
+ /** 店把一行原始列读成 {@link SessionFace} 的唯一口(三只店共用;化身那一格经 {@link incarnationFromCell})。 */
37
69
  export declare function sessionFaceFromRow(sessionId: string, owner: string | null, incarnation: unknown): SessionFace;
70
+ /** 按 id 读住客(core `SessionStore.incarnationOf` 的形):`undefined` = 没有这只会话;读抛 = 读故障,不是答案。 */
71
+ export type IncarnationRead = (sessionId: string) => Promise<string | undefined>;
72
+ /**
73
+ * 「这只会话店能不能按 id 答住客」—— 全仓**只判这一处**:起服门(`assertWorkspaceSpaceReadable`)与 k8s 工作区空间的每 run 读
74
+ * (`createK8sRunEnvFactory`)读的是同一只返回值(装配第 5 段判一次)。缺席 = 店没有实装 `incarnationOf`。
75
+ */
76
+ export declare function incarnationReadOf(store: Pick<SessionStore, "incarnationOf"> | undefined): IncarnationRead | undefined;
77
+ /**
78
+ * 「这只会话店能不能按 id 答住客脸(属主 + 化身一次读)」—— 与 {@link incarnationReadOf} 同一处判法、同一个「能不能」:
79
+ * 空间缓存卷的回收用它一次拿两位(属主要与这次删除验过的那位相符,化身是空间键)。店没有这只读口 ⇒ `undefined`(回收不做,不响)。
80
+ */
81
+ export declare function sessionFaceReadOf(store: object | undefined): SessionFaceReader | undefined;
38
82
  /**
39
83
  * session-sync **整段换装**(replace)时化身的去留 —— 三只店共用的一句判据:换装删掉再插回的是**同一位住客**的那一行
40
84
  * (删除带属主门),所以行在、属主相同、化身非空 ⇒ 沿用;行不在(这是一次新建)或还是空的存量形 ⇒ 铸一只新的。
@@ -24,10 +24,21 @@ export class SessionIncarnationMissingError extends Error {
24
24
  this.name = "SessionIncarnationMissingError";
25
25
  }
26
26
  }
27
- export function sessionFaceFromRow(sessionId, owner, incarnation) {
28
- if (typeof incarnation !== "string" || incarnation === "")
27
+ export function incarnationFromCell(sessionId, cell) {
28
+ if (typeof cell !== "string" || cell === "")
29
29
  throw new SessionIncarnationMissingError(sessionId);
30
- return { owner, incarnation };
30
+ return cell;
31
+ }
32
+ export function sessionFaceFromRow(sessionId, owner, incarnation) {
33
+ return { owner, incarnation: incarnationFromCell(sessionId, incarnation) };
34
+ }
35
+ export function incarnationReadOf(store) {
36
+ const read = store?.incarnationOf;
37
+ return typeof read === "function" ? read.bind(store) : undefined;
38
+ }
39
+ export function sessionFaceReadOf(store) {
40
+ const read = store?.sessionFaceOf;
41
+ return typeof read === "function" ? read.bind(store) : undefined;
31
42
  }
32
43
  export function carriedIncarnation(prior, owner) {
33
44
  if (prior !== undefined && (prior.owner ?? null) === owner && typeof prior.incarnation === "string" && prior.incarnation !== "") {
@@ -5,11 +5,12 @@
5
5
  * importSession/planSync) so the three DB session adapters (local/pg/tidb) and security.ts can depend on these
6
6
  * symbols WITHOUT touching the orchestration module — session-sync.ts value-imports StoreBackend and type-imports
7
7
  * security's OwnerAwareSessionStore, so importing it from security.ts formed the repo's only true type cycle
8
- * (security ↔ session-sync; lens2 §B/§E). This file imports NOTHING from the repo (core types only) — a leaf,
9
- * same shape as session-sync-content.ts. session-sync.ts re-exports everything here, so existing import sites
8
+ * (security ↔ session-sync; lens2 §B/§E). This file imports nothing from the repo but TYPES from leaves (core types, and
9
+ * the session-face type from `session-incarnation.ts`, itself a leaf) — a leaf, same shape as session-sync-content.ts. session-sync.ts re-exports everything here, so existing import sites
10
10
  * keep working unchanged.
11
11
  */
12
12
  import type { SessionTreeEntry } from "@sema-agent/core";
13
+ import type { SessionFace } from "./session-incarnation.js";
13
14
  /**
14
15
  * P1d-β (PUSH streaming, sema-internal server/docs/DESIGN-session-sync.md §15 + the persistence synthesis) — a TWO-PHASE STAGED import
15
16
  * handle. The whole-bundle `importSession` (session-sync.ts) buffers every entry in one JSON body (server memory ∝ session size);
@@ -39,11 +40,17 @@ export interface StagingHandle {
39
40
  appendBatch(entries: SessionTreeEntry[]): Promise<void>;
40
41
  /** ATOMICALLY swap the staged rows into the real session id (ONE txn): in-txn re-classify (TOCTOU close) → purge dst
41
42
  * → row-key rename staging→real → session_meta LAST (the commit point). `owner` is re-stamped (§9). Throws
42
- * {@link SyncConflictError} on an unconsented fork/stale flip. Returns the committed relation. */
43
+ * {@link SyncConflictError} on an unconsented fork/stale flip. Returns the committed relation **and the settled session
44
+ * face** — the occupant (owner + incarnation) the commit left in the real session id, read INSIDE the same txn (the local
45
+ * store: the same hold of the session's write lock), never re-read afterwards. The post-commit replays conditioned on the
46
+ * occupant (the session row's two bits) write on exactly this face — a session deleted and re-created after the commit,
47
+ * even by the same owner, is a different occupant and is not written. A row without an incarnation is refused at that read
48
+ * (`sessionFaceFromRow`, the same law as every face read). */
43
49
  commit(owner: string | null, opts?: {
44
50
  resolution?: "overwrite-dst";
45
51
  }): Promise<{
46
52
  relation: SyncRelation["relation"];
53
+ face: SessionFace;
47
54
  }>;
48
55
  /** Drop the staging rows (Phase B aborted before commit) — DELETE the staging-id session_event rows. Idempotent. */
49
56
  abort(): Promise<void>;
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Auto-generated session titles.
3
3
  *
4
- * SHAPE: fire-and-forget at a task submit whose session has NO title (the objective is known then — no need to wait
5
- * for the run to finish, and the web Recents gets the title sooner). Fills a NULL title only (`setTitleIfNull`,
4
+ * SHAPE: fire-and-forget once the ENGINE ACCEPTS a submitted run whose session has NO title — the one caller is the
5
+ * submission's acceptance seat (`run-acceptance.ts`: the objective entered the conversation, or a result-only verify /
6
+ * cascade leg came back having done work). A run the engine refuses before acceptance never reaches the titler (no
7
+ * model call for a refused input). No need to wait for the run to finish, and the web Recents gets the title sooner. Fills a NULL title only (`setTitleIfNull`,
6
8
  * store-side `WHERE title IS NULL`) — the user writes the SAME column unconditionally (`PATCH /v1/sessions/:id` →
7
9
  * `patchSessionMeta`): a user-set title is never overwritten (the probe below reads "titled" and skips the model
8
10
  * call), and a title the user cleared (`title: null` ⇒ NULL) is filled again at the next submit (law: `session-row.ts`).
@@ -25,7 +27,7 @@ export interface SessionTitlerDeps {
25
27
  setTitleIfNull(sessionId: string, title: string): Promise<boolean>;
26
28
  /** Cheap pre-LLM probe: "titled" = skip (a restart/evicted-seen/another replica
27
29
  * already paid — don't re-spend an LLM call); "untitled" = generate; "none" = the session_meta row hasn't
28
- * been REGISTERED yet (createRun fires before the Runner's session acquire) — retry after a grace window,
30
+ * been REGISTERED (or was deleted meanwhile) — retry after a grace window,
29
31
  * and if still absent, release the seen slot so the NEXT submit retries (never a permanently-untitled
30
32
  * session via a swallowed early UPDATE-0). Local lane has no row-existence dimension → never "none". */
31
33
  probeTitle(sessionId: string): Promise<"none" | "untitled" | "titled">;