@sema-agent/server 7.43.0 → 7.44.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 (345) hide show
  1. package/USAGE.md +18 -0
  2. package/dist/adoption/plan.js +31 -192
  3. package/dist/adoption/quiesce.js +3 -86
  4. package/dist/adoption/runner.js +3 -140
  5. package/dist/adoption/sql.js +0 -74
  6. package/dist/adoption/wire.js +0 -80
  7. package/dist/approval-ask-machine.js +0 -75
  8. package/dist/approval-card.js +0 -323
  9. package/dist/approval-deny-reasons.js +0 -42
  10. package/dist/approval-hmac.js +0 -35
  11. package/dist/approval-reconciler.js +8 -166
  12. package/dist/approval.js +4 -67
  13. package/dist/audit.js +1 -44
  14. package/dist/auth-bridge.js +4 -47
  15. package/dist/auth-keys.js +0 -23
  16. package/dist/bake-runner/main.js +4 -65
  17. package/dist/bake-runner/protocol.js +3 -58
  18. package/dist/bake-runner/runner.js +5 -91
  19. package/dist/bench/l8/artifact.js +2 -61
  20. package/dist/bench/l8/escape.js +0 -25
  21. package/dist/bench/l8/index.js +0 -14
  22. package/dist/bench/l8/probes.js +0 -60
  23. package/dist/bench/l8/run-probes.js +2 -45
  24. package/dist/bench/s1/arms.js +14 -189
  25. package/dist/bench/s1/live-deps.js +11 -253
  26. package/dist/bench/s1/oracle.js +1 -36
  27. package/dist/bench/s1/repair-oracle-adapter.js +0 -30
  28. package/dist/bench/s1/reviewer.js +0 -34
  29. package/dist/bench/s1/row.js +0 -62
  30. package/dist/bench/s1/run-firm.js +4 -70
  31. package/dist/bench/s1/runner-ctx.js +0 -40
  32. package/dist/bench/s1/tasks.js +0 -115
  33. package/dist/boot/adoption.js +0 -20
  34. package/dist/boot/budget-tracing.js +3 -55
  35. package/dist/boot/config-center.js +43 -551
  36. package/dist/boot/coordinators.js +1 -69
  37. package/dist/boot/crash-last.js +0 -18
  38. package/dist/boot/deferred-sandbox-path-env.js +3 -119
  39. package/dist/boot/execution-env.js +5 -171
  40. package/dist/boot/governance-seams.js +0 -92
  41. package/dist/boot/leader.js +0 -69
  42. package/dist/boot/limit-sync.js +0 -2
  43. package/dist/boot/memory-boundary.js +3 -91
  44. package/dist/boot/org-memory.js +1 -25
  45. package/dist/boot/parked-revive-gate.js +0 -150
  46. package/dist/boot/permission-rules-audit.js +1 -56
  47. package/dist/boot/reapers.js +15 -273
  48. package/dist/boot/resolve-spec.js +9 -768
  49. package/dist/boot/retention-lane.js +4 -87
  50. package/dist/boot/runner-deps.js +3 -297
  51. package/dist/boot/runtime-caps.js +0 -40
  52. package/dist/boot/session-faces.js +2 -133
  53. package/dist/boot/shutdown.js +9 -83
  54. package/dist/boot/side-query-lane.js +2 -137
  55. package/dist/boot/stores.js +15 -215
  56. package/dist/boot/task-list-lane.js +0 -18
  57. package/dist/boot/webfetch-summarize-lane.js +0 -47
  58. package/dist/boot/workflow-orchestration.js +3 -99
  59. package/dist/boot-reclaim.js +0 -20
  60. package/dist/bounded-session-map.js +0 -19
  61. package/dist/brain.js +2 -139
  62. package/dist/budget.js +9 -244
  63. package/dist/capabilities/builtin-tools.js +0 -3
  64. package/dist/capabilities/center-plugins.js +6 -51
  65. package/dist/capabilities/center-prompts.js +6 -71
  66. package/dist/capabilities/code-review-council.js +7 -40
  67. package/dist/capabilities/collab-workflows.js +1 -44
  68. package/dist/capabilities/hands-lane.js +0 -65
  69. package/dist/capabilities/memory-notice.js +0 -70
  70. package/dist/capabilities/prompt.js +0 -14
  71. package/dist/capabilities/prompts/code-review.js +0 -14
  72. package/dist/capabilities/prompts/identity.js +0 -6
  73. package/dist/capabilities/prompts/team.js +0 -4
  74. package/dist/capabilities/repo-tools.js +0 -34
  75. package/dist/capabilities/sandbox-file-send.js +3 -92
  76. package/dist/capabilities/scenarios.d.ts +0 -1
  77. package/dist/capabilities/scenarios.js +18 -279
  78. package/dist/capabilities/select-environment-tool.js +0 -32
  79. package/dist/capabilities/send-user-file-tool.js +2 -66
  80. package/dist/capabilities/skills.d.ts +1 -2
  81. package/dist/capabilities/skills.js +3 -21
  82. package/dist/capabilities/team.d.ts +3 -11
  83. package/dist/capabilities/team.js +3 -51
  84. package/dist/capabilities/tool-defer.js +0 -3
  85. package/dist/config-center/apply-effective.js +15 -330
  86. package/dist/config-center/apply-ledger.js +5 -38
  87. package/dist/config-center/facade.js +0 -41
  88. package/dist/config-center/hot-keys-registry.js +0 -20
  89. package/dist/config-center/http-client.js +2 -125
  90. package/dist/config-center/mcp-revocation.js +2 -34
  91. package/dist/config-center/read-face.js +0 -59
  92. package/dist/config-center/restart-signal.js +1 -85
  93. package/dist/config-center/skills-mcp.d.ts +1 -1
  94. package/dist/config-center/skills-mcp.js +9 -76
  95. package/dist/config-center/stage-limits.js +8 -35
  96. package/dist/config-invariants.js +0 -16
  97. package/dist/config-lkg.js +0 -42
  98. package/dist/config-provider.js +3 -186
  99. package/dist/config-types.js +0 -5
  100. package/dist/config.js +107 -1145
  101. package/dist/degenerate-instrument.js +3 -67
  102. package/dist/deployment-governance.js +0 -124
  103. package/dist/digest-form.js +0 -12
  104. package/dist/elicitation.js +3 -86
  105. package/dist/env-facts.js +7 -75
  106. package/dist/fleet/fleet-bus.js +34 -507
  107. package/dist/fleet/fleet-reconciler.js +9 -149
  108. package/dist/fleet/fleet-terminal-window.js +10 -178
  109. package/dist/fleet/subagent-tail-bus.js +3 -72
  110. package/dist/fleet-client.js +10 -70
  111. package/dist/fleet-lease.js +5 -79
  112. package/dist/git-api-kind.js +0 -3
  113. package/dist/governance-ask-marks.js +2 -78
  114. package/dist/hooks/branch-transcript.js +0 -74
  115. package/dist/hooks/cc-agent-hook-prompt.js +0 -29
  116. package/dist/hooks/cc-stop-prompt.js +1 -46
  117. package/dist/hooks/hook-llm.js +1 -53
  118. package/dist/hooks/hook-runner.js +20 -414
  119. package/dist/http/active-run-conflict.js +4 -129
  120. package/dist/http/cursor-fingerprint.d.ts +5 -0
  121. package/dist/http/cursor-fingerprint.js +5 -0
  122. package/dist/http/idempotency.js +0 -37
  123. package/dist/http/principal-gate.js +3 -40
  124. package/dist/http/route-ctx.js +0 -9
  125. package/dist/http/routes/a2a-serve.js +7 -319
  126. package/dist/http/routes/admin-config-refresh.js +0 -4
  127. package/dist/http/routes/admin-drain.js +0 -10
  128. package/dist/http/routes/adoption.js +1 -29
  129. package/dist/http/routes/agents-roster.js +1 -47
  130. package/dist/http/routes/approvals-assistant.js +32 -395
  131. package/dist/http/routes/attachments.js +4 -22
  132. package/dist/http/routes/capabilities.js +3 -420
  133. package/dist/http/routes/diagnostics.js +1 -81
  134. package/dist/http/routes/fleet.js +9 -185
  135. package/dist/http/routes/images.js +14 -239
  136. package/dist/http/routes/leader.js +0 -13
  137. package/dist/http/routes/memory-bundle.js +1 -61
  138. package/dist/http/routes/memory-policy.js +9 -99
  139. package/dist/http/routes/notify-wake.js +3 -37
  140. package/dist/http/routes/observability.js +2 -19
  141. package/dist/http/routes/retention-ops.js +2 -34
  142. package/dist/http/routes/rules.js +1 -93
  143. package/dist/http/routes/runs.js +64 -873
  144. package/dist/http/routes/session-sync.js +19 -258
  145. package/dist/http/routes/sessions-list.js +17 -43
  146. package/dist/http/routes/sessions.js +34 -226
  147. package/dist/http/routes/shared-memory.js +5 -36
  148. package/dist/http/routes/side-query.js +1 -87
  149. package/dist/http/routes/tasks.js +52 -722
  150. package/dist/http/routes/trace-usage.js +32 -195
  151. package/dist/http/routes/workflows.js +19 -195
  152. package/dist/http/run-meta.js +0 -6
  153. package/dist/http/send.js +0 -32
  154. package/dist/http/server.js +92 -1597
  155. package/dist/http/sse-lifecycle.js +2 -13
  156. package/dist/http/sse-log.js +3 -48
  157. package/dist/http/tar.js +5 -21
  158. package/dist/http/verify-rounds.js +0 -5
  159. package/dist/http/wire-gate.js +0 -9
  160. package/dist/http/workspace-content.js +0 -10
  161. package/dist/images/bake-validate.js +1 -70
  162. package/dist/images/manifest.js +1 -6
  163. package/dist/index.js +0 -21
  164. package/dist/key-resolver.js +2 -17
  165. package/dist/leader/diffout.js +1 -20
  166. package/dist/leader/diffup.js +0 -47
  167. package/dist/leader/endpoint.js +2 -57
  168. package/dist/leader/fanout.js +3 -45
  169. package/dist/leader/grader-env-factory.js +3 -72
  170. package/dist/leader/leader.js +5 -156
  171. package/dist/leader/merge.js +8 -115
  172. package/dist/leader/planner.js +3 -54
  173. package/dist/leader/repair-oracle.js +1 -60
  174. package/dist/leader/repair-wire.js +2 -79
  175. package/dist/leader/wire.js +8 -307
  176. package/dist/lsp/e2b-bridge.js +4 -64
  177. package/dist/lsp/e2b-manager.js +6 -94
  178. package/dist/lsp/lsp-frames.js +0 -12
  179. package/dist/lsp/manager.js +4 -96
  180. package/dist/lsp/ws-transport.js +5 -55
  181. package/dist/lsp-evict.js +1 -15
  182. package/dist/main.js +57 -765
  183. package/dist/memory-bundle-engine.js +0 -55
  184. package/dist/memory-export.js +0 -4
  185. package/dist/memory-posture.js +1 -15
  186. package/dist/memory-scope.js +11 -148
  187. package/dist/memory-sync-client.js +2 -44
  188. package/dist/memory-sync.js +1 -80
  189. package/dist/model-select.js +3 -80
  190. package/dist/observability/cost-quota.js +1 -17
  191. package/dist/observability/cost-taxonomy.js +0 -34
  192. package/dist/observability/fail-open.js +7 -86
  193. package/dist/observability/logger.js +0 -6
  194. package/dist/observability/metrics.js +0 -94
  195. package/dist/observability/otel-exporter.js +3 -13
  196. package/dist/observability/principal-context.js +0 -9
  197. package/dist/observability/prompt-manifest.js +1 -37
  198. package/dist/observability/rate-limit.js +0 -4
  199. package/dist/observability/secret-env-scrub.js +2 -56
  200. package/dist/observability/tool-trace.js +1 -70
  201. package/dist/orchestration/hardened-vm-runner.js +4 -118
  202. package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
  203. package/dist/orchestration/hardened-vm-worker.js +0 -27
  204. package/dist/orchestration/subagent-steer.js +1 -45
  205. package/dist/orchestration/workflow-agent-steer.js +1 -80
  206. package/dist/orchestration/workflow-completion-inbox.js +32 -285
  207. package/dist/orchestration/workflow-notify-journal.js +16 -259
  208. package/dist/org-memory-admission.js +3 -47
  209. package/dist/parent-watch.js +2 -48
  210. package/dist/parked-decide.js +1 -109
  211. package/dist/per-task-image.js +0 -57
  212. package/dist/plan-cache-probe.js +3 -27
  213. package/dist/plugins/adoption-log-sql.js +2 -119
  214. package/dist/plugins/approval-ask-store-memory.js +3 -38
  215. package/dist/plugins/approval-ask-store-sql.js +6 -188
  216. package/dist/plugins/approval-exemption-store.js +2 -28
  217. package/dist/plugins/background-agent-store-sql.js +4 -105
  218. package/dist/plugins/background-shell-support.js +14 -122
  219. package/dist/plugins/blob-backend.js +6 -169
  220. package/dist/plugins/breaker-state-sql.js +8 -46
  221. package/dist/plugins/caching-session-store.js +4 -106
  222. package/dist/plugins/checkpoint-store-sql.js +18 -505
  223. package/dist/plugins/e2b-orphan-reclaim.js +0 -45
  224. package/dist/plugins/file-outcome-sink.js +0 -9
  225. package/dist/plugins/file-resume-anchor-store.js +4 -43
  226. package/dist/plugins/file-run-store.js +26 -364
  227. package/dist/plugins/file-snapshot-store-sql.js +10 -181
  228. package/dist/plugins/fork-routing-session-store.js +8 -111
  229. package/dist/plugins/host-platform.js +2 -91
  230. package/dist/plugins/image-bake-store-sql.js +7 -250
  231. package/dist/plugins/image-index-sql.js +4 -123
  232. package/dist/plugins/k8s-bg-scripts.js +4 -88
  233. package/dist/plugins/k8s-exec-protocol.js +0 -41
  234. package/dist/plugins/leader-run-store-sql.js +0 -101
  235. package/dist/plugins/local-checkpoint-store.js +8 -128
  236. package/dist/plugins/local-session-store.js +34 -296
  237. package/dist/plugins/local-task-attachment-store.js +2 -16
  238. package/dist/plugins/mailbox-store-sql.js +8 -65
  239. package/dist/plugins/memory-embedder-fingerprint.js +5 -166
  240. package/dist/plugins/memory-embedder.js +4 -70
  241. package/dist/plugins/memory-engine-pg.js +6 -170
  242. package/dist/plugins/memory-engine-tidb.js +7 -155
  243. package/dist/plugins/memory-engine-vector-util.js +0 -10
  244. package/dist/plugins/memory-key-guards.js +0 -34
  245. package/dist/plugins/memory-origin-law.js +0 -187
  246. package/dist/plugins/memory-resume-anchor-store.js +0 -17
  247. package/dist/plugins/memory-run-store.js +12 -89
  248. package/dist/plugins/memory-session-policy-store.js +0 -17
  249. package/dist/plugins/memory-sync-store-pg.js +4 -49
  250. package/dist/plugins/memory-sync-store-tidb.js +3 -35
  251. package/dist/plugins/outcome-ledger-sql.js +3 -97
  252. package/dist/plugins/permission-rule-store-file.js +5 -133
  253. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  254. package/dist/plugins/permission-rule-store-sql.js +50 -307
  255. package/dist/plugins/pg-cost-quota.js +0 -7
  256. package/dist/plugins/pg-pool.js +0 -92
  257. package/dist/plugins/pg-rate-limiter.js +2 -13
  258. package/dist/plugins/pg-safe-json.js +4 -40
  259. package/dist/plugins/pg-session-storage.js +25 -189
  260. package/dist/plugins/posix-shell-fs.js +1 -31
  261. package/dist/plugins/remote-env-adb.js +12 -101
  262. package/dist/plugins/remote-env-e2b.js +40 -371
  263. package/dist/plugins/remote-env-file-error.js +0 -37
  264. package/dist/plugins/remote-env-host.js +63 -483
  265. package/dist/plugins/remote-env-k8s.js +32 -326
  266. package/dist/plugins/remote-env-local-docker.js +24 -157
  267. package/dist/plugins/remote-env-ssh.js +20 -128
  268. package/dist/plugins/remote-scratchpad.js +2 -32
  269. package/dist/plugins/remote-shell.js +1 -32
  270. package/dist/plugins/resume-anchor-store-sql.js +0 -11
  271. package/dist/plugins/retention-lane-store-sql.js +0 -108
  272. package/dist/plugins/retention-store-sql.js +4 -383
  273. package/dist/plugins/roster-store-sql.js +0 -55
  274. package/dist/plugins/run-store-sql.js +14 -251
  275. package/dist/plugins/s3-presign.js +2 -49
  276. package/dist/plugins/scheduler-support.js +3 -80
  277. package/dist/plugins/send-file-ledger.js +4 -53
  278. package/dist/plugins/send-user-file.js +4 -94
  279. package/dist/plugins/session-placement.js +1 -89
  280. package/dist/plugins/session-policy-store-sql.js +3 -81
  281. package/dist/plugins/session-store.js +0 -57
  282. package/dist/plugins/shared-memory-store-sql.js +4 -167
  283. package/dist/plugins/sql-driver.js +0 -17
  284. package/dist/plugins/sql-errors.js +0 -7
  285. package/dist/plugins/sql-escape.js +0 -8
  286. package/dist/plugins/sql-row-helpers.js +0 -25
  287. package/dist/plugins/store-backend.js +42 -222
  288. package/dist/plugins/store-contracts.js +2 -46
  289. package/dist/plugins/task-attachment-store.js +3 -49
  290. package/dist/plugins/task-list-store-sql.js +0 -79
  291. package/dist/plugins/tidb-cost-quota.js +1 -4
  292. package/dist/plugins/tidb-pool.js +1 -207
  293. package/dist/plugins/tidb-rate-limiter.js +3 -9
  294. package/dist/plugins/tidb-session-storage.js +4 -70
  295. package/dist/plugins/tidb-session-store.js +19 -341
  296. package/dist/plugins/tool-result-store-sql.js +4 -179
  297. package/dist/plugins/usage-window-store-sql.js +0 -8
  298. package/dist/plugins/web-search.js +10 -132
  299. package/dist/plugins/workflow-journal-store-sql.js +2 -58
  300. package/dist/plugins/workflow-run-store-sql.js +6 -91
  301. package/dist/plugins/worktree-isolation.js +6 -126
  302. package/dist/plugins/write-behind-counter.js +16 -75
  303. package/dist/principal-jwt.js +5 -60
  304. package/dist/project-memory.js +15 -146
  305. package/dist/prompts-domain-validate.js +1 -51
  306. package/dist/question.js +2 -111
  307. package/dist/resource-suspend.js +0 -18
  308. package/dist/router/route-orchestration.js +0 -77
  309. package/dist/rules-consent.d.ts +44 -5
  310. package/dist/rules-consent.js +75 -215
  311. package/dist/run-local.js +14 -381
  312. package/dist/runs.js +29 -617
  313. package/dist/runtime-caps-resolver.js +6 -132
  314. package/dist/runtime-governance.js +1 -232
  315. package/dist/sandbox-pkg-source.js +0 -37
  316. package/dist/sealed-key.js +3 -68
  317. package/dist/security.js +6 -291
  318. package/dist/session-leaf-bus.js +0 -32
  319. package/dist/session-sync-content.js +1 -69
  320. package/dist/session-sync-kernel.js +3 -60
  321. package/dist/session-sync.js +3 -66
  322. package/dist/session-titler.js +7 -41
  323. package/dist/session-watch.js +9 -73
  324. package/dist/shared-memory-scope-authorizer.js +0 -18
  325. package/dist/sighup-idle.js +1 -12
  326. package/dist/spec-fields.js +4 -143
  327. package/dist/store-live-probe.js +3 -49
  328. package/dist/task-a2a.js +1 -126
  329. package/dist/task-cwd.js +1 -103
  330. package/dist/task-mcp.js +1 -89
  331. package/dist/task-settings.js +10 -300
  332. package/dist/task-workflow.js +8 -75
  333. package/dist/tool-approval.d.ts +28 -1
  334. package/dist/tool-approval.js +70 -1430
  335. package/dist/trace/artifacts.js +5 -20
  336. package/dist/trace/engine-notice-wire.js +6 -138
  337. package/dist/trace/ledger-sink.js +7 -96
  338. package/dist/trace/project.js +4 -448
  339. package/dist/trace/redact.js +9 -77
  340. package/dist/turn-activity.js +1 -27
  341. package/dist/usage-analytics.js +4 -34
  342. package/dist/wall-clock-jump-guard.js +1 -68
  343. package/package.json +3 -3
  344. package/dist/capabilities/scenario-alias.d.ts +0 -27
  345. package/dist/capabilities/scenario-alias.js +0 -61
@@ -10,29 +10,14 @@ import { sendJson, sendError, sseHeaders } from "../send.js";
10
10
  import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
11
11
  const MCP_STATUS_TTL_MS = 5_000;
12
12
  const MCP_MATERIALIZE_TIMEOUT_MS = 10_000;
13
- // DESIGN-269 件4:A2A 读面的两枚同族时限。**刻意与 MCP 分开命名而不复用同一个常量**:两条腿的物化
14
- // 成本不同族(MCP 可能 spawn 子进程/开长连;A2A 只是几个 HTTPS 卡请求),将来任一侧要调窗时不该被
15
- // 另一侧的名字绑架。数值今天相同,语义各自独立。
16
13
  const A2A_STATUS_TTL_MS = 5_000;
17
14
  const A2A_MATERIALIZE_TIMEOUT_MS = 10_000;
18
- /** 面板上每个 peer 最多回多少条工具名(codex finding 5)。超出只截**表**,`toolCount` 仍是真数、
19
- * `toolNamesTruncated:true` 显式说明 —— 消费端读得出「还有更多」,而不是拿到一份看着完整的假名单。 */
20
15
  const A2A_PANEL_MAX_TOOL_NAMES = 200;
21
16
  export function createSessionsLocal() {
22
17
  return {
23
- // [1196] session-events SSE 活跃连接计数(连接帽谓词;帽=deps.sessionEventsMaxConnections ?? 256)。
24
18
  sseConnections: 0,
25
- // 六轮复审:初始 owner 查探的**存储侧**并发许可——连接坑在 race 超时即还,但底层查询可能仍悬着
26
- // (驱动级取消不可得);重连风暴下未决查询必须独立有界(64),超限=零存储触碰直接 503。
27
19
  sseOwnerLookups: 0,
28
- // E9 (shell-host contract) — short-TTL single-flight cache for GET /v1/sessions/:id/mcp. materializeMcpTools connects to (or
29
- // spawns) MCP servers; without this a burst of /mcp polls would re-connect every time (adversarial-review MEDIUM:
30
- // resource-exhaustion on an unbounded panel-poll path). Keyed by (scenario, principal) so per-user RBAC stays
31
- // correct; stores the in-flight PROMISE so concurrent first-polls collapse to ONE materialize. Bounded by TTL +
32
- // a hard size cap (cleared on overflow). The cached value is the PROJECTED panel (no live connections held).
33
20
  mcpStatusCache: new Map(),
34
- // DESIGN-269 件4 —— 同上那套(TTL + single-flight + 硬条数帽 + 按 (scenario, principal) 分键,因为
35
- // `principalHeader` 注入让不同调用方看到的 peer 授权面可能不同),A2A 半场。
36
21
  a2aStatusCache: new Map(),
37
22
  };
38
23
  }
@@ -41,22 +26,15 @@ export async function handleSessions(req, res, url, ctx) {
41
26
  await handleSessionsBody(req, res, url, ctx, miss);
42
27
  return !miss.fell;
43
28
  }
44
- /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(去缩进两格;sessions 另有一处见文件头说明)。
45
- * 裸 `return;` 的语义原样 =「本域已应答」;走到函数尾才是「没匹配上」,置 `miss.fell` 交回装配器继续下一个域。 */
46
29
  async function handleSessionsBody(req, res, url, ctx, miss) {
47
30
  const { deps } = ctx;
48
31
  const { readJson, safeDecode, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite } = ctx.helpers;
49
32
  const sse = ctx.local.sessions;
50
33
  const { mcpStatusCache, a2aStatusCache } = ctx.local.sessions;
51
- // E11 (shell-host contract): GET /v1/sessions/:id/init — the one-call startup bundle, so the shell renders its
52
- // banner from ONE request instead of fanning out to /v1/models + /v1/policy + /v1/capabilities. Worker-global
53
- // config surface (same posture as /v1/policy): principal OPTIONAL, enforced only under REQUIRE_PRINCIPAL.
54
- // ADDITIVE — `model` + `mode` are the load-bearing banner fields; `tools`/`agents`/`commands`/`cwd` are omitted
55
- // today (tolerate-absent — the shell falls back to its mock-fill). `cwd` is live-only (E13) and never durable here.
56
34
  {
57
35
  const im = /^\/v1\/sessions\/([^/]+)\/init$/.exec(url);
58
36
  if (im && req.method === "GET") {
59
- const principal = gatedPrincipal(req, deps.config); // F-fix: VERIFIED presence gate (direct-door JWT, not the spoofable header)
37
+ const principal = gatedPrincipal(req, deps.config);
60
38
  if (deps.config.requirePrincipal && !principal) {
61
39
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
62
40
  return;
@@ -69,15 +47,10 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
69
47
  return;
70
48
  }
71
49
  }
72
- // E24 (shell-host contract): GET /v1/sessions/:id/settings — the effective merged settings view (CC /config read
73
- // side). Worker-global config surface (principal optional unless REQUIRE_PRINCIPAL), same posture as /v1/policy +
74
- // the E11 init bundle. `effective` = the resolved worker config the run obeys; `applied` echoes the resolved
75
- // model. The PATCH half (runtime settings write) + per-source layering need a core seam (SERVICE-GAP-DOSSIER E24)
76
- // — omitted today, so no `settings` write capability is advertised (honest: read route only).
77
50
  {
78
51
  const stm = /^\/v1\/sessions\/([^/]+)\/settings$/.exec(url);
79
52
  if (stm && req.method === "GET") {
80
- const principal = gatedPrincipal(req, deps.config); // F-fix: VERIFIED presence gate (direct-door JWT, not the spoofable header)
53
+ const principal = gatedPrincipal(req, deps.config);
81
54
  if (deps.config.requirePrincipal && !principal) {
82
55
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
83
56
  return;
@@ -95,38 +68,25 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
95
68
  return;
96
69
  }
97
70
  }
98
- // E9 (shell-host contract, core 1.124.0): GET /v1/sessions/:id/mcp — per-server MCP status for the `/mcp` panel.
99
- // 🔴 core does NOT persist MCP connections (task-scoped), so there is no stored session MCP health. This
100
- // MATERIALIZES the worker's configured MCP servers ON-DEMAND (a fresh connect → list → dispose, fail-open) and
101
- // returns the projected statuses with `asOf` = THIS materialization moment. It is materialization-time status,
102
- // NOT live session health (core §E9) — the shell renders "as of <asOf>". Worker-global config surface
103
- // (principal-optional like /policy); the caller's gatedPrincipal is threaded for per-user MCP RBAC
104
- // (principalHeader injection). Uses the DEFAULT scenario's MCP set (per-session scenario tracking is a follow-up).
105
71
  {
106
72
  const mm = /^\/v1\/sessions\/([^/]+)\/mcp$/.exec(url);
107
73
  if (mm && req.method === "GET") {
108
- // F-fix: VERIFIED presence gate (direct-door JWT, not the spoofable header); per-user MCP RBAC already uses gatedPrincipal.
109
74
  if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
110
75
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
111
76
  return;
112
77
  }
113
78
  const specs = mcpForScenario(deps.config.mcpServers, deps.config.defaultScenario);
114
79
  if (!specs || specs.length === 0) {
115
- sendJson(res, 200, { servers: [], asOf: new Date().toISOString() }); // no MCP configured → honest empty panel
80
+ sendJson(res, 200, { servers: [], asOf: new Date().toISOString() });
116
81
  return;
117
82
  }
118
- const principal = gatedPrincipal(req, deps.config); // threaded for per-user MCP RBAC (principalHeader injection)
83
+ const principal = gatedPrincipal(req, deps.config);
119
84
  const cacheKey = `${deps.config.defaultScenario}::${principal ?? ""}`;
120
85
  const cached = mcpStatusCache.get(cacheKey);
121
86
  if (!cached || Date.now() - cached.at > MCP_STATUS_TTL_MS) {
122
87
  if (mcpStatusCache.size > 1024)
123
- mcpStatusCache.clear(); // hard size cap (per-principal keys) — crude but bounded
124
- // Single-flight: store the in-flight promise so a burst of polls collapses to ONE materialize.
88
+ mcpStatusCache.clear();
125
89
  const panel = (async () => {
126
- // Bound the connect/list so a hanging MCP server can't pin the request; dispose the late resolver if it
127
- // eventually completes after the timeout (no connection leak). materializeMcpTools is fail-open per server.
128
- // #324:第 6 参接撤销台账 —— 展示面与引擎执行面同一权威(被撤服务器不再进面板;core 契约:
129
- // probe 抛错 fail-open + onProbeFailure 回调,这里缺席=沿 core 的 once-per-materialization notice)。
130
90
  const matP = materializeMcpTools(specs, principal, undefined, undefined, undefined, deps.mcpRevocations);
131
91
  let timer;
132
92
  const mat = await Promise.race([matP.catch(() => null), new Promise((r) => { timer = setTimeout(() => r(null), MCP_MATERIALIZE_TIMEOUT_MS); })]);
@@ -134,7 +94,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
134
94
  clearTimeout(timer);
135
95
  if (!mat) {
136
96
  void matP.then((m) => m?.dispose().catch(() => undefined)).catch(() => undefined);
137
- return { servers: [], asOf: new Date().toISOString(), degraded: true }; // timed out / failed to materialize
97
+ return { servers: [], asOf: new Date().toISOString(), degraded: true };
138
98
  }
139
99
  try {
140
100
  return {
@@ -144,8 +104,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
144
104
  status: s.status,
145
105
  ...(s.serverInfo ? { serverInfo: s.serverInfo } : {}),
146
106
  ...(s.toolNames ? { toolNames: s.toolNames } : {}),
147
- // E9 (review LOW): redact secret-shaped strings + bound length — raw MCP connect errors may carry
148
- // internal paths/config the shell needn't see (same redaction posture as tool output).
149
107
  ...(s.error ? { error: redactSecrets(String(s.error)).slice(0, 200) } : {}),
150
108
  })),
151
109
  };
@@ -160,17 +118,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
160
118
  return;
161
119
  }
162
120
  }
163
- // [DESIGN-269 车1 件4] GET /v1/sessions/:id/a2a — per-peer A2A status for the shell's agent panel.
164
- // 逐点对称上面的 /mcp 面(按需物化 → 投影 → TTL single-flight 缓存 → 超时竞速 + 迟到 dispose),
165
- // 差别只在**投影形状**,而那是被裁定过的(设计稿 v1.2 §2.5,cli [4107] 采纳级意见):
166
- //
167
- // 🔴 **不发裸 `status`**。core 的 `A2aPeerStatus.status:"ready"` 的真语义是「本次探测时,卡取到了、
168
- // skills 铸出来了」——A2A **不持连接**(每次调用都是独立 HTTPS 请求),所以它永远不是「peer 现在活着」。
169
- // 一个叫 `status:"ready"` 的键发给三个消费端,必有一端渲成「在线」,而那句话我们证不了。所以投影的
170
- // 键名自己承重语义:`cardFetched: boolean`(卡取到没有)+ `probedAt: number`(**本次**探测时刻)+
171
- // 可选 `error`。「在线/离线」这个词在本读面结构性不存在,不是靠文档约束不许渲。
172
- // `asOf` 与 `probedAt` 并存不是冗余:前者是**面板**的物化时刻(ISO,与 /mcp 同形,壳渲 "as of …"),
173
- // 后者是**每个 peer** 的探测时刻戳(机器可读,消费端算新鲜度用)。
174
121
  {
175
122
  const am = /^\/v1\/sessions\/([^/]+)\/a2a$/.exec(url);
176
123
  if (am && req.method === "GET") {
@@ -180,19 +127,16 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
180
127
  }
181
128
  const specs = a2aForScenario(deps.config.a2aPeers, deps.config.defaultScenario);
182
129
  if (!specs || specs.length === 0) {
183
- sendJson(res, 200, { peers: [], asOf: new Date().toISOString() }); // no peers configured → honest empty panel
130
+ sendJson(res, 200, { peers: [], asOf: new Date().toISOString() });
184
131
  return;
185
132
  }
186
- const principal = gatedPrincipal(req, deps.config); // threaded for per-user peer authz (principalHeader injection)
133
+ const principal = gatedPrincipal(req, deps.config);
187
134
  const cacheKey = `${deps.config.defaultScenario}::${principal ?? ""}`;
188
135
  const cached = a2aStatusCache.get(cacheKey);
189
136
  if (!cached || Date.now() - cached.at > A2A_STATUS_TTL_MS) {
190
137
  if (a2aStatusCache.size > 1024)
191
- a2aStatusCache.clear(); // hard size cap (per-principal keys)
138
+ a2aStatusCache.clear();
192
139
  const panel = (async () => {
193
- // Bound the card fetches so a hanging peer can't pin the request; dispose the late resolver if it
194
- // completes after the timeout (materializeA2aTools holds no socket, but its dispose IS the lifetime
195
- // statement — a tool that outlives this probe must fail loudly instead of calling a remote agent).
196
140
  const matP = materializeA2aTools(specs, principal);
197
141
  let timer;
198
142
  const mat = await Promise.race([matP.catch(() => null), new Promise((r) => { timer = setTimeout(() => r(null), A2A_MATERIALIZE_TIMEOUT_MS); })]);
@@ -201,28 +145,18 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
201
145
  const probedAt = Date.now();
202
146
  if (!mat) {
203
147
  void matP.then((m) => m?.dispose().catch(() => undefined)).catch(() => undefined);
204
- return { peers: [], asOf: new Date(probedAt).toISOString(), degraded: true }; // timed out / failed to materialize
148
+ return { peers: [], asOf: new Date(probedAt).toISOString(), degraded: true };
205
149
  }
206
150
  try {
207
151
  return {
208
152
  asOf: new Date(probedAt).toISOString(),
209
153
  peers: mat.statuses.map((s) => ({
210
154
  name: s.name,
211
- // 🔴 翻译点(唯一):core 的闭集 status → 承重键名。`"ready"` = 卡取到了 + skills 铸出来了。
212
155
  cardFetched: s.status === "ready",
213
156
  probedAt,
214
- // 卡自述的两串是 UNTRUSTED(core 已中性化+限长);端点是协商结果,诊断用。
215
157
  ...(s.agentName ? { agentName: s.agentName } : {}),
216
158
  ...(s.agentDescription ? { agentDescription: s.agentDescription } : {}),
217
- // 🔒 自审第三镜(泄密面):`endpoint` 是**卡自述**的协商结果 —— 远端可控文本,而且 URL 形
218
- // 天然能带 userinfo(`https://user:pass@host/rpc`,core 的 `new URL()` 原样保留)。core 对
219
- // name/description 有中性化+限长,对 endpoint 只验协议(http/https),所以脱敏与限长是**本
220
- // 读面自己的活**(投影是它的输出,不是 core 的)。2048 = 通用 URL 上限量级,越界的是病态值。
221
159
  ...(s.endpoint ? { endpoint: redactSecrets(String(s.endpoint)).slice(0, 2048) } : {}),
222
- // 🔴 codex 对抗复审 finding 5(medium,验真后采纳):技能**条数**是远端可控且无界的
223
- // (core 限单个名字的长度,不限卡里的条数;卡体上限 8MiB),而这条只读 GET 按
224
- // (scenario, principal) 分键缓存 ⇒ 多身份可放大。发**有界表 + 真计数 + 显式旗**,
225
- // 而不是静默截断:一个会撒谎的读面比一个大的读面更贵。
226
160
  ...(s.toolNames
227
161
  ? {
228
162
  toolCount: s.toolNames.length,
@@ -230,8 +164,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
230
164
  ...(s.toolNames.length > A2A_PANEL_MAX_TOOL_NAMES ? { toolNamesTruncated: true } : {}),
231
165
  }
232
166
  : {}),
233
- // 与 /mcp 同规:原始错误可能带内部路径/凭据形 —— 脱敏 + 截断。
234
- // 🔒 `headers`/`principalHeader` 在本投影里**根本没有出口**(白名单式铸键,不是删黑名单)。
235
167
  ...(s.error ? { error: redactSecrets(String(s.error)).slice(0, 200) } : {}),
236
168
  })),
237
169
  };
@@ -246,16 +178,9 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
246
178
  return;
247
179
  }
248
180
  }
249
- // [1075]② 会话追随轻探针:GET /v1/sessions/:id/head → { leafId }(+ ETag/304)。leafId=会话树叶
250
- // 指针,任何 append 都变=天然版本号——web 空闲期低成本轮询(If-None-Match 命中=304 零 body),
251
- // 变了才拉 tail 增量。刻意不做 session 级 SSE(真订阅需跨副本通知面,写可发生在任意副本而流挂在
252
- // 某一副本——设计件另立;探针轮询 1-2s 一次的 304 成本≈零,先解「必须刷新」的主诉)。
253
- // 顶层路由(只依赖 sessionStorage,不依赖 sessionAudit——audit 缺席的部署探针照常)。
254
181
  {
255
182
  const hm = req.method === "GET" ? /^\/v1\/sessions\/([^/]+)\/head$/.exec(url) : null;
256
183
  if (hm) {
257
- // 二轮复审([1196] S1 邻案连修):方法必须 bind 宿主——SESSION_CACHE_TTL_SEC=0 时 sessionStorage
258
- // 是裸 PG/TiDB 类实例,getLeafId 内用 this.pool,裸引用调用=TypeError 500。
259
184
  const getLeafId = deps.sessionStorage?.getLeafId?.bind(deps.sessionStorage);
260
185
  const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
261
186
  if (!getLeafId || !ownerOf) {
@@ -263,8 +188,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
263
188
  return;
264
189
  }
265
190
  const headSession = decodeURIComponent(hm[1]);
266
- // 七轮复审:store 有 getHead 时用单行原子快照(owner 鉴权与 leafId 同拍——两独立读在
267
- // delete/reclaim 缝里可鉴旧主发新叶);无 getHead 的旧 store 保持两读旧形(退化面照旧)。
268
191
  const getHead = deps.sessionStorage?.getHead?.bind(deps.sessionStorage);
269
192
  let owner;
270
193
  let leafId;
@@ -299,16 +222,9 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
299
222
  return;
300
223
  }
301
224
  }
302
- // [1196] session 级 SSE 真订阅:GET /v1/sessions/:id/events —— `event: head` 推送(latest-wins),
303
- // `?lastLeafId=` 为游标(等价 If-None-Match:相同则无初始帧),25s 心跳注释行。鉴权/存储门与 /head
304
- // 完全同门;连接帽超限=503+Retry-After(消费方回落 /head 轮询,capabilities.sessionEvents 广告)。
305
225
  {
306
226
  const em = req.method === "GET" ? /^\/v1\/sessions\/([^/]+)\/events$/.exec(url) : null;
307
227
  if (em) {
308
- // 二轮复审单源化:本路由**不再自读基线**——所有帧(含首帧)由 registry 按 rev 单调序 fan-out
309
- // (replay-on-subscribe 给已知条目即时首帧;新条目首帧=首拍探针 ≤hotMs)。双源(自读基线 vs
310
- // registry 缓冲)必然产生不可判序的回退帧,单源后该类竞序结构性不存在。getLeafId 由 registry
311
- // 内部使用(main.ts 注入时已绑定宿主);路由只需 ownerOf 门。
312
228
  const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
313
229
  const watch = deps.sessionWatch;
314
230
  if (!deps.sessionStorage?.getLeafId || !ownerOf || !watch) {
@@ -316,17 +232,12 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
316
232
  return;
317
233
  }
318
234
  const evSession = decodeURIComponent(em[1]);
319
- // codex 一轮生命周期状态机:①连接坑与 close 守卫在任何 await 之前同步建立(读存储期间断连不再
320
- // 漏清/漏还坑);②每个 await 之后查 closed;③所有拒绝/异常路径经同一 release。
321
235
  if (sse.sseConnections >= (deps.sessionEventsMaxConnections ?? 256)) {
322
- // 🔴 机器码而非裸 writeHead(2026-07-31 缝合审):这条 503 与 drain 的 503 走同一条流,
323
- // 而 drain 的带 `errorCode:"draining"`(冻结契约)。没有码,消费端只能靠自由文本字面区分
324
- // 「连接帽 ⇒ 回落 /head 轮询(自愈,保持连接策略)」与「该重连替换实例」—— 正是本仓要根除的姿势。
325
236
  res.setHeader("retry-after", "5");
326
237
  sendError(res, 503, "state.sse_connection_cap", "session-events connection cap reached — fall back to /head polling");
327
238
  return;
328
239
  }
329
- sse.sseConnections++; // 同步占坑(查+占同拍,慢读并发不再超卖);此后一切路径必经 cleanup 还坑
240
+ sse.sseConnections++;
330
241
  let hb;
331
242
  let unsubscribe;
332
243
  let closed = false;
@@ -342,10 +253,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
342
253
  res.on("close", cleanup);
343
254
  req.on("close", cleanup);
344
255
  try {
345
- // 五轮复审:初始 ownerOf 也受 stall 界(存储悬死时不许无限占坑——256 坑被悬死请求耗尽=合法
346
- // 客户端全 503);超时=可重试失败(503+Retry-After),坑经 cleanup 归还。六轮复审:查询本体的
347
- // 并发另行有界(sseOwnerLookups,许可在底层 settle 时才还)——race 只还连接坑不还查询许可,
348
- // 重连风暴不得无界堆积未决存储查询。
349
256
  const stallBound = deps.sessionEventsStallMs ?? 60_000;
350
257
  if (sse.sseOwnerLookups >= 64) {
351
258
  cleanup();
@@ -355,8 +262,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
355
262
  }
356
263
  sse.sseOwnerLookups++;
357
264
  const ownerP = ownerOf(evSession);
358
- void ownerP.catch(() => undefined).then(() => { sse.sseOwnerLookups--; }); // 许可随底层 settle 归还
359
- // 七轮复审:race 计时器 settle 即清——高频开合不得每次拖 60s 计时器+闭包(unref 只保进程退出面)。
265
+ void ownerP.catch(() => undefined).then(() => { sse.sseOwnerLookups--; });
360
266
  let stallTimer;
361
267
  let owner;
362
268
  try {
@@ -370,7 +276,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
370
276
  clearTimeout(stallTimer);
371
277
  }
372
278
  if (closed)
373
- return; // 读期断连:close 守卫已还坑,别再写已死响应
279
+ return;
374
280
  if (owner === "__stall__") {
375
281
  cleanup();
376
282
  res.setHeader("retry-after", "5");
@@ -387,20 +293,14 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
387
293
  return;
388
294
  }
389
295
  const q = new URL(req.url ?? url, "http://x").searchParams;
390
- const lastLeafId = q.get("lastLeafId"); // null=未带 ⇒ 恒发首帧;带了且首帧值相等 ⇒ 抑制(304 同义)
391
- // 单源发射:全部帧来自 registry(rev 单调序),连接级 lastSent 去重;首帧抑制=首值命中
392
- // lastLeafId 时只记不发(后续同值仍去重,更新值照发)。
296
+ const lastLeafId = q.get("lastLeafId");
393
297
  let first = true;
394
- let lastAccepted; // 去重基准(单源 rev 序,最新接受值)
298
+ let lastAccepted;
395
299
  let lastWritten;
396
- // 三轮复审背压:res.write 返回 false 即停写,只保「最新一格」待冲(latest-wins 恰是本契约),
397
- // drain 冲洗;阻塞超时(缺省 60s)=消费者死/僵——断流(client 重连自愈),绝不无界积压。
398
300
  let writeBlocked = false;
399
301
  let blockedAt = 0;
400
302
  let pendingHead;
401
303
  const stallMs = deps.sessionEventsStallMs ?? 60_000;
402
- // 四轮复审:**全部**写(connected/心跳注释/head 帧)走同一背压感知写手——任何一次 false 都
403
- // 进 blocked 态(心跳先塞住的流同样受 stall 断流约束,不再有旁路缓冲增长)。
404
304
  const writeRaw = (chunk) => {
405
305
  if (closed)
406
306
  return;
@@ -418,7 +318,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
418
318
  });
419
319
  }
420
320
  }
421
- catch { /* torn socket: close 守卫清理 */ }
321
+ catch { }
422
322
  };
423
323
  const doWrite = (leafId) => {
424
324
  lastWritten = leafId;
@@ -433,22 +333,20 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
433
333
  if (suppress) {
434
334
  lastWritten = leafId;
435
335
  return;
436
- } // 视作已发:重复抑制,真变更仍会推
336
+ }
437
337
  if (writeBlocked) {
438
- pendingHead = { v: leafId }; // latest-wins 单格
338
+ pendingHead = { v: leafId };
439
339
  if (Date.now() - blockedAt > stallMs) {
440
340
  cleanup();
441
341
  try {
442
342
  res.end();
443
343
  }
444
- catch { /* noop */ }
344
+ catch { }
445
345
  }
446
346
  return;
447
347
  }
448
348
  doWrite(leafId);
449
349
  };
450
- // 订阅先于 SSE headers(帽拒绝要保住干净 503)——headers 前到达的 replay/live 帧存一格
451
- // pre 缓冲(latest-wins 覆写,单源序不破),headers 后冲洗。
452
350
  let started = false;
453
351
  let preVal;
454
352
  const onHead = (leafId) => {
@@ -457,13 +355,11 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
457
355
  else
458
356
  emit(leafId);
459
357
  };
460
- // dropSession(session 删除,三轮复审租户轮转围栏)→ 即刻断流:旧 owner 的订阅绝不跨越
461
- // delete/reclaim 边界;跨副本删除由下方心跳期 owner 复核兜底(≤心跳一拍)。
462
358
  const onEnd = () => { cleanup(); try {
463
359
  res.end();
464
360
  }
465
- catch { /* noop */ } };
466
- unsubscribe = watch.subscribe(evSession, owner ?? null, onHead, onEnd); // owner 铸入条目(四轮跨副本围栏);已知条目=同步 replay 首帧;新条目=首拍探针 ≤hotMs
361
+ catch { } };
362
+ unsubscribe = watch.subscribe(evSession, owner ?? null, onHead, onEnd);
467
363
  if (!unsubscribe) {
468
364
  cleanup();
469
365
  res.setHeader("retry-after", "5");
@@ -476,8 +372,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
476
372
  if (preVal !== undefined)
477
373
  emit(preVal.v);
478
374
  const hbMs = deps.sessionEventsHeartbeatMs ?? 25_000;
479
- // 四轮复审:owner 复核 single-flight——上拍未归不再起新查(存储卡顿时 256 连接不得堆积
480
- // 未决查询);未决超过 stallMs=复核面失效,fail-closed 断流(长活流不许在盲区里续命)。
481
375
  let ownerCheckPending = false;
482
376
  let ownerCheckStartedAt = 0;
483
377
  hb = setInterval(() => {
@@ -486,22 +380,21 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
486
380
  if (writeBlocked && Date.now() - blockedAt > stallMs) {
487
381
  onEnd();
488
382
  return;
489
- } // 僵消费者:无新帧也到点断
383
+ }
490
384
  if (!writeBlocked)
491
385
  writeRaw(": hb\n\n");
492
386
  if (ownerCheckPending) {
493
387
  if (Date.now() - ownerCheckStartedAt > stallMs)
494
- onEnd(); // 复核悬死=盲区,fail-closed
388
+ onEnd();
495
389
  return;
496
390
  }
497
- // 三轮复审跨副本围栏:心跳拍复核 owner——session 被删(undefined)或换主(≠连接时 owner)即断流。
498
391
  ownerCheckPending = true;
499
392
  ownerCheckStartedAt = Date.now();
500
393
  void ownerOf(evSession).then((o) => {
501
394
  ownerCheckPending = false;
502
395
  if (!closed && o !== owner)
503
396
  onEnd();
504
- }).catch(() => { ownerCheckPending = false; /* 瞬时读障不断流,下一拍再核 */ });
397
+ }).catch(() => { ownerCheckPending = false; });
505
398
  }, hbMs);
506
399
  hb.unref?.();
507
400
  }
@@ -513,7 +406,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
513
406
  try {
514
407
  res.end();
515
408
  }
516
- catch { /* noop */ }
409
+ catch { }
517
410
  }
518
411
  return;
519
412
  }
@@ -528,13 +421,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
528
421
  }
529
422
  if (!runOwnerOk(req, res, audit.owner))
530
423
  return;
531
- // web 会话区性能(additive — parameterless requests stay BYTE-IDENTICAL):
532
- // ?tail=N → messages = last N (+ window {offset,total})
533
- // ?before=<off>&limit=N → the page above a prior window (offsets stable within one floorEntryId
534
- // generation — a compaction changes floorEntryId, the web refetches)
535
- // ?blobCap=<bytes> → oversized string bodies truncated to a preview + message-level truncated:true
536
- // (expand = refetch that ONE message via ?message=<index>, full size)
537
- // ?message=<index> → SINGLE full message {message, index, floorEntryId} (the expand face)
538
424
  const q = new URL(req.url ?? url, "http://x").searchParams;
539
425
  const qi = (k) => {
540
426
  const v = q.get(k);
@@ -557,7 +443,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
557
443
  const before = qi("before");
558
444
  const blobCap = qi("blobCap");
559
445
  if (tail === undefined && before === undefined && blobCap === undefined) {
560
- sendJson(res, 200, audit); // additive contract: no params = the exact legacy payload
446
+ sendJson(res, 200, audit);
561
447
  return;
562
448
  }
563
449
  let messages = audit.messages;
@@ -573,8 +459,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
573
459
  return;
574
460
  }
575
461
  }
576
- // E17 (§0.5) — POST /v1/sessions/:id/fork → {sessionId}. Branch a NEW session off the source's WHOLE history
577
- // via the SessionRepo seam the service owns (sessionStorage.fork), owner-scoped exactly like GET /v1/sessions.
578
462
  {
579
463
  const fm = req.method === "POST" ? /^\/v1\/sessions\/([^/]+)\/fork$/.exec(url) : null;
580
464
  if (fm) {
@@ -590,9 +474,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
590
474
  return;
591
475
  }
592
476
  const sourceId = safeDecode(fm[1]);
593
- // §0.5 — shape-validate the session id (uuidv7) so a crafted id can't reach the store ops as anything but
594
- // a well-formed key (a second layer behind the tool-result LIKE-escape). Malformed (bad %-seq or non-uuidv7)
595
- // ⇒ 400, never a store hit.
596
477
  if (sourceId === null || !isUuidV7(sourceId)) {
597
478
  sendError(res, 400, "request.id_invalid", "invalid session id (must be a uuidv7)");
598
479
  return;
@@ -602,38 +483,20 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
602
483
  sendError(res, 404, "not_found.session", "session not found");
603
484
  return;
604
485
  }
605
- // Owner gate (no existence oracle for non-owners — 404, parity with runOwnerOk): a principal caller may
606
- // only fork its OWN session; a fleet-wide (system/trace) caller may fork any (ops).
607
- // [4198] 窄互认第二臂:单机形下 default 主对**存量无主行**放行(三条件齐,见 legacyUnownedSessionOk)。
608
486
  if (!scope.fleetWide && owner !== scope.gateOwner && !legacyUnownedSessionOk(deps.config.requirePrincipal, scope.gateOwner, owner)) {
609
487
  sendError(res, 404, "not_found.session", "session not found");
610
488
  return;
611
489
  }
612
- // New-session ownership — DELIBERATE (no security issue either way; documented per adversarial-review):
613
- // • principal caller → the NEW session is owned by the CALLER (scope.gateOwner): you fork your own session,
614
- // you own the copy.
615
- // • [4198] 窄臂放行的 default 主 → 走 principal 支 ⇒ 副本铸给 `default`(具名行)。刻意如此:副本
616
- // 离开无主池,本臂**不被 fork 传播延寿**,墓碑注(存量随生命周期消亡后可整删)仍然成立。
617
- // • fleet-wide (ops, fork-on-behalf) → the NEW session INHERITS the SOURCE owner (`owner ?? null`) so the
618
- // fork is TENANT-owned (an ops/system fork-on-behalf hands the copy to the same tenant as the source,
619
- // NOT to the ownerless/system pool) — a later attach is then governed by the normal authorizer for that
620
- // tenant. (Chosen over null/system-owned: an ops fork is "give this tenant a branched copy", so tenant
621
- // attribution is the intent; a system-submitted *run* forks ownerless, but a *session* fork-on-behalf
622
- // is explicitly on the source tenant's behalf.)
623
490
  const newOwner = scope.fleetWide ? (owner ?? null) : scope.gateOwner;
624
491
  const newId = await fork(sourceId, newOwner);
625
492
  if (newId === null) {
626
493
  sendError(res, 404, "not_found.session", "session not found");
627
494
  return;
628
- } // raced delete between ownerOf and fork
495
+ }
629
496
  sendJson(res, 200, { sessionId: newId });
630
497
  return;
631
498
  }
632
499
  }
633
- // E21 (§0.5) — DELETE /v1/sessions/:id → {deleted}. Purge the session's conversation history (session_meta/
634
- // session_event) AND the service-owned run-ledger/checkpoint/tool-result rows for it. Auth-FIRST (the scope is
635
- // computed before any store touch, mirroring GET to avoid a 501-before-auth existence oracle). Owner-scoped,
636
- // idempotent ({deleted:false} when nothing existed). active-run guard → 409 (don't orphan the session lock).
637
500
  {
638
501
  const dm = req.method === "DELETE" ? /^\/v1\/sessions\/([^/]+)$/.exec(url) : null;
639
502
  if (dm) {
@@ -649,8 +512,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
649
512
  return;
650
513
  }
651
514
  const sessionId = safeDecode(dm[1]);
652
- // §0.5 — shape-validate the session id (uuidv7) so a crafted id can't reach the purge ops (a second layer
653
- // behind the tool-result LIKE-escape). Malformed (bad %-seq or non-uuidv7) ⇒ 400, never a store hit.
654
515
  if (sessionId === null || !isUuidV7(sessionId)) {
655
516
  sendError(res, 400, "request.id_invalid", "invalid session id (must be a uuidv7)");
656
517
  return;
@@ -659,21 +520,11 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
659
520
  if (owner === undefined) {
660
521
  sendJson(res, 200, { deleted: false });
661
522
  return;
662
- } // idempotent: already gone
663
- // Foreign-owner ⇒ the SAME response as missing (a 404 here vs 200 above was an existence
664
- // oracle — a prober could distinguish "absent" from "another tenant's"). deleted:false is also honest
665
- // (nothing was deleted) and keeps the route idempotent-shaped for both cases.
666
- // [4198] 窄互认第二臂:不放行时,存量无主会话对 default 主是**静默假成功**(用户永远删不掉自己的历史)。
667
- // 放行后 purge 的 owner 守卫仍传**行的真 owner**(下面的 `owner`,即 null)——SQL 侧 null-safe 属主门不被放宽。
523
+ }
668
524
  if (!scope.fleetWide && owner !== scope.gateOwner && !legacyUnownedSessionOk(deps.config.requirePrincipal, scope.gateOwner, owner)) {
669
525
  sendJson(res, 200, { deleted: false });
670
526
  return;
671
527
  }
672
- // Don't orphan the single-active-run lock: refuse while a run is live on this session (409 over a
673
- // half-purge that strands a task_active claim). A terminal/suspended/parked session deletes cleanly. This
674
- // is a cheap PRE-check; the AUTHORITATIVE no-active-run gate is re-asserted transactionally inside the
675
- // runs-ledger purge (FOR UPDATE on task_active), which closes the check→purge TOCTOU and also returns
676
- // `{ active }` to 409 if a createRun raced in between.
677
528
  const activeTaskId = await deps.runStore?.getActiveTaskId(sessionId);
678
529
  if (activeTaskId) {
679
530
  sendError(res, 409, "conflict.session_active_run", "session has an active run; cancel it before deleting", { activeTaskId });
@@ -688,12 +539,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
688
539
  return;
689
540
  }
690
541
  }
691
- // E6 — GET/PUT /v1/sessions/:id/policy: read / write operator-tightened per-session tool-permission rules (core
692
- // reads them at prepare-time, SUBTRACT-only). Owner-gated + the operator flag FROZEN from the verified principal
693
- // (explicitOperatorOk(gatedPrincipal), NEVER the spoofable header — the steer.trusted privilege-escalation rule):
694
- // a non-operator may only TIGHTEN its OWN session (a loosen → loosen_forbidden); an operator may loosen + act
695
- // cross-tenant. The target key = the session OWNER (the principal core reads rules under). SessionPolicyError →
696
- // conflict:409 (stale expectedRev) / loosen_forbidden:403. 404 (not 403) on a non-owner — no existence oracle.
697
542
  {
698
543
  const pm = req.method === "GET" || req.method === "PUT" ? /^\/v1\/sessions\/([^/]+)\/policy$/.exec(url) : null;
699
544
  if (pm) {
@@ -717,11 +562,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
717
562
  if (owner === undefined) {
718
563
  sendError(res, 404, "not_found.session", "session not found");
719
564
  return;
720
- } // idempotent / no oracle
721
- // E6 (review): rules MUST be keyed by the principal core reads at run time (getRules(sessionId, spec.principal)).
722
- // For an OWNED session that is the owner (a task on it runs as the owner). An OWNERLESS session (owner=null,
723
- // REQUIRE_PRINCIPAL=false) has no stable run-principal — a task attaching any principal would read a DIFFERENT
724
- // key, silently bypassing the rule (fail-OPEN). Refuse rather than store a rule that won't apply.
565
+ }
725
566
  if (owner === null) {
726
567
  sendError(res, 409, "conflict.session_ownerless", "session policy requires an owned session (set REQUIRE_PRINCIPAL / attach a principal); this session is ownerless");
727
568
  return;
@@ -730,16 +571,12 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
730
571
  if (!operator && owner !== (principal ?? null)) {
731
572
  sendError(res, 404, "not_found.session", "session not found");
732
573
  return;
733
- } // non-owner → 404, no existence oracle
734
- const target = owner; // the key core reads rules under = the session's owner (= spec.principal for its tasks)
574
+ }
575
+ const target = owner;
735
576
  if (req.method === "GET") {
736
- // BC-3 (shell §I): the 200 is ALWAYS a non-null StoredSessionRules with a load-bearing `rev` (the PUT CAS
737
- // baseline). An owned session with NO policy yet → getRules null → return the empty-at-rev-0 record (the
738
- // store reads a missing row as rev 0 = expectedRev:0 create-once), never `{rules:null}`.
739
577
  sendJson(res, 200, { rules: (await store.getRules(sessionId, target)) ?? { rev: 0 } });
740
578
  return;
741
579
  }
742
- // PUT — validate + pick the 5 known string[] rule fields (reject malformed fail-loud); CAS + tighten gate in the store.
743
580
  let body;
744
581
  try {
745
582
  body = (await readJson(req));
@@ -781,11 +618,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
781
618
  return;
782
619
  }
783
620
  }
784
- // ── workspace 浏览面(#3,docs/DESIGN-workspace-browse.md;[1882] 征询 → [1894] cli 表态四点细化)────────
785
- // E19 快照店的只读投影:list/tree/file/archive 四端点,零新 store。owner 门=sync 面 PULL 同款
786
- // (session 必须存在且属调用方,404 无 oracle);501 谓词=快照店三读面(listKeys/exportManifest/
787
- // getBlob)+ ownerOf 齐(capabilities.workspace 同源)。`:key=latest` 别名=最新快照(键=entryId,
788
- // uuidv7 时间序 ⇒ 字典序最大即最新)。
789
621
  {
790
622
  const wm = req.method === "GET" ? /^\/v1\/sessions\/([^/]+)\/workspace(?:\/([^/]+)(?:\/(tree|file|archive))?)?$/.exec(url) : null;
791
623
  if (wm) {
@@ -806,16 +638,13 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
806
638
  return;
807
639
  }
808
640
  const wsOwner = await ownerOf(wsSession);
809
- // PULL 门:必须存在且属己,无 oracle。[4198] 窄互认第二臂同样覆盖本读面(存量会话的工作区浏览)。
810
641
  if (wsOwner === undefined || (!scope.fleetWide && wsOwner !== scope.gateOwner && !legacyUnownedSessionOk(deps.config.requirePrincipal, scope.gateOwner, wsOwner))) {
811
642
  sendError(res, 404, "not_found.session", "session not found");
812
643
  return;
813
644
  }
814
645
  const q = new URL(req.url ?? "", "http://x").searchParams;
815
- const keys = (await fs.listKeys(wsSession)).sort().reverse(); // uuidv7 entryId:字典序 desc = 时间序 新→旧
646
+ const keys = (await fs.listKeys(wsSession)).sort().reverse();
816
647
  const sub = wm[3];
817
- // GET …/workspace — 列快照(新→旧;?limit 1..100 默认 20)。files=manifest 行数;bytes 仅在
818
- // blobSizes 面在场时给(local 店缺席=诚实省略,[1894]② additive 口径)。
819
648
  if (wm[2] === undefined) {
820
649
  const limRaw = q.get("limit");
821
650
  if (limRaw !== null && !(/^[0-9]+$/.test(limRaw) && Number(limRaw) >= 1 && Number(limRaw) <= 100)) {
@@ -823,12 +652,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
823
652
  return;
824
653
  }
825
654
  const lim = limRaw === null ? 20 : Number(limRaw);
826
- // [2400] CAPS-OPS-6(clay 裁 2026-08-03,最优设计不留旧语义):
827
- // · `?before=<key>`(exclusive)游标——keys 是 uuidv7 字典序 desc(新→旧),before 取更旧段;
828
- // · `total` = listKeys 口径的**快照总数**(与 tree 腿的 total 同名同义;此前=页内行数,同名反义
829
- // 且超过 limit 的快照永远取不到)。竞态窗口内正被 reap 的 ghost 键会短暂多计 1,下一拍自愈;
830
- // · `nextBefore` 仅在还有更旧的 key 时给(=本页最后一个 key);
831
- // · 复审 A-MED 的保护不变:latest 与**展示行**同源(ghost 不当 latest)、页行掉 ghost 不空报。
832
655
  const beforeRaw = q.get("before");
833
656
  const scoped = beforeRaw !== null ? keys.filter((k) => k < beforeRaw) : keys;
834
657
  const page = scoped.slice(0, lim);
@@ -836,7 +659,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
836
659
  for (const k of page) {
837
660
  const m = await fs.exportManifest(wsSession, k);
838
661
  if (m === null)
839
- continue; // listKeys 与 manifest 读之间被 reap 的键:掉行不空报
662
+ continue;
840
663
  let bytes;
841
664
  if (typeof fs.blobSizes === "function") {
842
665
  const sizes = await fs.blobSizes([...new Set(m.values())]);
@@ -857,7 +680,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
857
680
  });
858
681
  return;
859
682
  }
860
- // :key 解析(latest 别名);快照必须真存在(manifest 非 null)。
861
683
  const keyRaw = safeDecode(wm[2]);
862
684
  if (keyRaw === null) {
863
685
  sendError(res, 400, "request.path_malformed", "malformed snapshot key");
@@ -870,7 +692,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
870
692
  return;
871
693
  }
872
694
  if (sub === "tree") {
873
- // [1894]②:entries 带 {path, size?, hash};?limit 默认 500 cap 2000、?offset 分页;path 序稳定。
874
695
  const limRaw = q.get("limit");
875
696
  if (limRaw !== null && !(/^[0-9]+$/.test(limRaw) && Number(limRaw) >= 1 && Number(limRaw) <= 2000)) {
876
697
  sendError(res, 400, "request.query_invalid", "limit must be a decimal integer in 1..2000");
@@ -907,8 +728,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
907
728
  return;
908
729
  }
909
730
  const cap = deps.config.workspaceFileMaxBytes ?? 8 * 1024 * 1024;
910
- // [1894]①:超限=413 结构化信封(web 渲「文件过大,下载查看」卡)。size 可先知(SQL 索引)时
911
- // 先拒后拉——绝不为判超限吸整块字节。
912
731
  if (typeof fs.blobSizes === "function") {
913
732
  const known = (await fs.blobSizes([hash])).get(hash);
914
733
  if (known !== undefined && known > cap) {
@@ -925,8 +744,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
925
744
  sendError(res, 413, "workspace_file_too_large", "file exceeds the single-file read limit — download it via the archive endpoint", { sizeBytes: bytes.byteLength, limit: cap });
926
745
  return;
927
746
  }
928
- // [1894]③:content-type 按扩展名(保守表,未知=octet-stream)+ 二进制判别头(首 8KiB NUL/控制
929
- // 字节嗅探)。nosniff 恒带(内容是 agent 产物=不可信,绝不让浏览器自嗅成可执行类型)。
930
747
  const ct = workspaceContentType(pathParam);
931
748
  const bin = looksBinary(bytes);
932
749
  res.writeHead(200, {
@@ -939,10 +756,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
939
756
  return;
940
757
  }
941
758
  if (sub === "archive") {
942
- // 流式 tar(ustar,src/http/tar.ts 零依赖)。开流前三道预检(头没发,还能诚实报错):
943
- // ① 路径全部可表示(>255B/不可拆 ⇒ 422 点名);② blob 全在(hasBlobs 单查;缺 ⇒ 502 可重试);
944
- // ③ 之后逐文件 getBlob 流式写+背压。中途 blob 消失(reap 竞态)⇒ 已在流中,destroy 连接
945
- // (诚实截断——tar 尾块缺失,消费端可判损)。
946
759
  const entries = [...manifest.entries()].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
947
760
  const bad = entries.filter(([p]) => splitTarPath(p) === null).map(([p]) => p);
948
761
  if (bad.length > 0) {
@@ -965,10 +778,6 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
965
778
  const write = async (buf) => {
966
779
  if (res.writableEnded || res.destroyed)
967
780
  throw new Error("client gone");
968
- // 🔴 复审 F5:drain 必须与 close/error **竞速**——socket 被销毁后 `drain` 永不再来,只等
969
- // 它会让本协程永久 pending(入口的 destroyed 守卫挡不住「已经卡在 await 里」这一态)。
970
- // 今天的缓和事实是引用图随连接一起变垃圾、admittedInflight 由 res.on("close") 独立释放;
971
- // 但任何将来加在这条腿上的 finally 清理都会静默不执行 —— 一行竞速根除这个类别。
972
781
  if (!res.write(buf)) {
973
782
  await new Promise((resolve, reject) => {
974
783
  const onDrain = () => { cleanup(); resolve(); };
@@ -993,11 +802,10 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
993
802
  res.end();
994
803
  }
995
804
  catch {
996
- res.destroy(); // 头已发:诚实截断(缺 tar 尾块,消费端可判损),绝不补一个假完整尾
805
+ res.destroy();
997
806
  }
998
807
  return;
999
808
  }
1000
- // 裸 :key 无动词 ⇒ 404 指路(不猜语义)。
1001
809
  sendError(res, 404, "request.route_unsupported", "unknown workspace verb — use /tree, /file?path=, or /archive");
1002
810
  return;
1003
811
  }