@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
package/dist/security.js CHANGED
@@ -1,33 +1,6 @@
1
1
  import { uuidv7, PROJECT_ID_REGEX } from "@sema-agent/core";
2
- // design/158 A4: the env→key-set decoders (parseApprovalHmacKeys / parsePrincipalJwks + their row types) moved to
3
- // the leaf ./auth-keys.js so config.ts's env-load no longer value-imports this whole auth module (lens2 §F). The
4
- // verifiers that consume the parsed key-sets now live in ./approval-hmac.js / ./principal-jwt.js (design/158 A13);
5
- // import all four names from ./auth-keys.js directly ([2354] 兼容面全清 — the compat re-export here is gone).
6
- import { memoryScopeFor } from "./memory-scope.js"; // design/158 N17: moved to ./memory-scope.js (this file's createAuthorizer still calls it)
7
- // design/158 A13: the approval-envelope HMAC (integrity) group and the principal-JWT/direct-door (identity)
8
- // group moved VERBATIM to ./approval-hmac.js and ./principal-jwt.js respectively — this file's own
9
- // `verifiedPrincipal` still calls verifyPrincipalJwt for the direct-door branch (value import below); every
10
- // symbol from both groups is re-exported near the bottom of this file so existing importers keep working.
2
+ import { memoryScopeFor } from "./memory-scope.js";
11
3
  import { verifyPrincipalJwt } from "./principal-jwt.js";
12
- /** A SessionStore that may additionally expose tenant-ownership ops (the TiDB store does) AND the
13
- * `SessionRepo` enumeration/lifecycle seam (§0.5: list/fork/delete homed on the session abstraction's own
14
- * tables, not the `task_run` runs ledger). All optional — an in-memory dev store implements none. */
15
- /**
16
- * E6 fail-closed 守卫的辖域判据:**这个请求是不是一次「破坏性会话写」**。
17
- *
18
- * 🔴 2026-08-01 复审逮到:守卫原本是**逐条路由白名单**(`PUT …/policy`、`POST …/sync/import`),
19
- * 而 `DELETE /v1/sessions/:id`(删整条会话)不在其中 —— 同一威胁形、爆炸半径**更大**,却没门。
20
- *
21
- * 威胁形逐字同源(守卫自己的注释):「无 service token + 未开 `ALLOW_UNAUTHED_WRITES`」这一形下
22
- * `sessionOwnerScopeForWrite` 判 fleet-wide ⇒ 集群内**任意**调用方可对**任意** session 动手。
23
- * policy 腿担心的是「forged principal 能 tighten/DoS 一个会话的工具」;DELETE 直接把会话删掉。
24
- *
25
- * ⇒ 枚举改**族判定**:新增一条破坏性会话路由时自动落进门内,不必记得回来加白名单。
26
- * 这是本仓「同一条论证只在枚举到的路径上执行」这一族的第 N 次,修法统一为把判据提成函数。
27
- *
28
- * 辖域**只含**对**既有会话**的破坏性写:改规则 / 整段替换 / 删除 / 分叉(读源会话全历史)。
29
- * 不含读面,也不含 `POST /v1/tasks` 那条提交腿 —— 它有自己的同族门(别重复拦,双拦会让错误归因变糊)。
30
- */
31
4
  export function isDestructiveSessionWrite(method, url) {
32
5
  if (method === "DELETE")
33
6
  return /^\/v1\/sessions\/[^/]+$/.test(url);
@@ -37,88 +10,21 @@ export function isDestructiveSessionWrite(method, url) {
37
10
  return /^\/v1\/sessions\/[^/]+\/(sync\/import|fork)$/.test(url);
38
11
  return false;
39
12
  }
40
- /** [4198] 窄互认第二臂的租户键字面量 —— 单机形下 core 写侧 `spec.principal ?? "default"` 铸出的那一个,
41
- * 也是壳 [4195]G1 翻面后随请求带的那一个。**不是哨兵**([4183] 措辞):它是单机形的真租户键。 */
42
13
  export const LEGACY_UNOWNED_AMNESTY_TENANT = "default";
43
- /**
44
- * [4198] 窄互认**第二臂**(clay 裁 A / [4225] 下达;cli [4238] 表态即发)—— `legacyShellOwner`
45
- * (本文件 createAuthorizer 内 + http/server.ts `runOwnerOk`)的对称孪生。
46
- *
47
- * 病灶:7.8.0 第一臂承接的是 `owner === "anon:shell-live"` 哨兵行;此后壳把无主表示法归一为
48
- * **缺席**,于是 1.0.82 及更早留下的存量会话 owner 列是 **null**。壳 1.0.83 起随请求带
49
- * `default` 主([4195] G1 闸口),`null !== "default"` ⇒ 存量会话在 session 面
50
- * **列表不出现 / fork 404 / delete 静默 {deleted:false} / workspace 404**(三面矩阵 [4198] 面3;
51
- * fleet 面与 run 读面天然连续,面1/面2 零施工)。
52
- *
53
- * 🔒 **三条件缺一不放行**(与第一臂同源,不是兼容臂):
54
- * ① 单机形 `!requirePrincipal` —— 多租部署本臂**死路**(header 必到、键必具名);
55
- * ② 存量行恰为无主 `owner === null` —— 任何具名 owner 的行不受影响;
56
- * ③ 调用方恰为 `default` 主 —— 非 default 的具名主对无主行仍走原门(404 / deleted:false)。
57
- * 反向风险有界:①限定的单机形本就单用户,把无主行判给 default 主不产生跨租泄露。
58
- *
59
- * 🪦 墓碑注(与第一臂同源):这是两个表示法之间的**过渡承接**。存量随会话生命周期消亡后,本臂
60
- * 连同 {@link LEGACY_UNOWNED_AMNESTY_TENANT} 与几处调用点可**整删**,不留兼容层。
61
- *
62
- * ── 逐面判过、**刻意不放行**的三处(免得后人当遗漏再补一次)────────────────────────────────
63
- * · `runOwnerOk` 三读面(head / audit 单读 / events):`owner === null` 那一支**本来就放行**
64
- * (判据是 `owner !== null && principal !== owner`),不需要本臂。[4198] 帖文里「单读 404」一句
65
- * 经亲读实现行**不成立**,已加现状钉(session-head-probe.test.ts)锁住这个事实。
66
- * · `PUT /v1/sessions/:id/policy` 的 `owner === null ⇒ 409 conflict.session_ownerless`:那是**有意的
67
- * fail-closed** —— 无主会话没有稳定的 run-principal,存下去的规则读不到、静默失效。放宽它等于制造
68
- * 一条 fail-open,不在本臂辖域。
69
- * · `…/sync/*`(PULL/PUSH/import):PULL 虽是只读、形同 workspace,但这一族**带一条破坏性 import 腿**
70
- * (`isDestructiveSessionWrite` 明列),而 [4198]/[4225] 裁的是会话**读面与生命周期**四门。放宽一个
71
- * 写面族超出裁定射程 —— 要做另立一次表态,不在本臂里顺手带过。
72
- *
73
- * @param requirePrincipal `config.requirePrincipal`(条件①)
74
- * @param gateOwner `sessionOwnerScope(ForWrite)` 判出的**已验证**主(条件③;fleet-wide 面不经本臂)
75
- * @param owner 存量行的 owner 列(条件②)
76
- */
77
14
  export function legacyUnownedSessionOk(requirePrincipal, gateOwner, owner) {
78
15
  return owner === null && legacyUnownedAmnestyOpen(requirePrincipal, gateOwner);
79
16
  }
80
- /**
81
- * {@link legacyUnownedSessionOk} 的**条件①③**半支 —— 只判「这个部署形 × 这个调用方」是否落在窄臂辖域内。
82
- *
83
- * 🔴 单独存在的唯一理由是**列表面**:`GET /v1/sessions` 的第三条件(`owner === null`)不在 JS 里判,
84
- * 它是 store 侧 `owner = ? OR owner IS NULL` 这一句 SQL 谓词 —— 三条件仍然齐,只是第三条件由 SQL 承载
85
- * (`listSessions({ owner, includeUnowned })` 的 `includeUnowned` 位即本函数的返回值)。
86
- * 单实体读/写点(fork/delete/workspace)**一律**用三条件齐的 {@link legacyUnownedSessionOk},不要用本函数。
87
- */
88
17
  export function legacyUnownedAmnestyOpen(requirePrincipal, gateOwner) {
89
18
  return !requirePrincipal && gateOwner === LEGACY_UNOWNED_AMNESTY_TENANT;
90
19
  }
91
- /** The shape core's `uuidv7()` mints: canonical 8-4-4-4-12 lowercase hex with version nibble `7` and the RFC-4122
92
- * variant (`8`/`9`/`a`/`b`). Used to shape-validate a caller-supplied session id on the §0.5 fork/delete routes so
93
- * a crafted id (LIKE metacharacters, over-long, non-canonical) is rejected up front — a SECOND layer behind the
94
- * tool-result LIKE-escape, not the only one. NB: deliberately NOT enforced on the task-submission `body.sessionId`
95
- * path (the authorizer) — historical callers/tests pass arbitrary short ids there, and the LIKE-escape +
96
- * VARCHAR(64) column already neutralize the metacharacter risk; tightening that path is a separate, breaking change.
97
- * ASYMMETRY (GAP #15, INTENTIONAL): a client MAY create a session via submit with an arbitrary id (e.g. run-local
98
- * `--session foo`, dev anonymous-share ids) yet then CANNOT fork/delete it (those routes 400 a non-uuidv7 id). This
99
- * is by design — the wire contract types sessionId as an opaque string, so submit stays permissive; fork/delete add
100
- * the uuidv7 gate only as defense-in-depth for store ops on a caller-supplied `:id`. Do NOT add a uuidv7 check to
101
- * submit (breaks the contract + run-local + single-tenant dev). The ONE submit-side guard that IS safe and present
102
- * is a LENGTH cap (≤64, the column width) in prepareSpec — see server.ts `body.sessionId` check — which prevents a
103
- * 500/truncation at the INSERT without constraining the id shape. */
104
20
  const UUIDV7_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
105
- /** True iff `s` matches the canonical uuidv7 shape core mints (see {@link UUIDV7_RE}). */
106
21
  export function isUuidV7(s) {
107
22
  return UUIDV7_RE.test(s);
108
23
  }
109
- /** Canonical uuid of ANY version (v4 legacy shell sessions included) — the SYNC
110
- * face's shape gate. Sync stays stricter than submit (arbitrary ≤64 ids) because it is a bulk write
111
- * face: the shape still shuts out crafted ids (LIKE metacharacters, over-long), but no longer strands a
112
- * v4-keyed legacy session as unsyncable (it was CREATABLE via the lenient submit yet REJECTED by the v7-only
113
- * sync gate — a real contradiction). fork/purge/policy keep the v7-only gate (ids we mint ourselves). */
114
24
  const UUID_ANY_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
115
25
  export function isUuidShape(s) {
116
26
  return UUID_ANY_RE.test(s);
117
27
  }
118
- /** An HTTP error the server maps to a status code (instead of a generic 500). `code` (optional) is a stable
119
- * machine-readable identifier the HTTP layer echoes as `{code}` beside the human `{error}` message — the
120
- * "typed rejection" posture (structured code on the wire, prose stays with the shell/web).
121
- * `extra` (optional) is folded into the error body verbatim (e.g. the scenario allowlist on a reject). */
122
28
  export class HttpError extends Error {
123
29
  status;
124
30
  code;
@@ -133,46 +39,12 @@ export class HttpError extends Error {
133
39
  this.extra = opts.extra;
134
40
  }
135
41
  }
136
- /**
137
- * S6 — close the "sessionId / scope are bearer capabilities" hole.
138
- *
139
- * The principal is read from a trusted header set by the authenticated caller (e.g. the OA backend
140
- * that already holds the service token). It is NOT taken from the request body, so a caller cannot
141
- * forge identity by passing `scope`/`sessionId` for another tenant.
142
- *
143
- * Then:
144
- * - a supplied `sessionId` is ownership-checked against `session_meta.owner` (403 on mismatch);
145
- * an unknown id is claimed for this principal; a missing id mints a fresh owned session;
146
- * - the memory scope is derived from the principal.
147
- *
148
- * Returns a function shaped for `ServiceDeps.authorize`. When `sessionStore` is not the TiDB store
149
- * (e.g. in-memory dev backend) ownership can't be enforced, so it only derives principal + sessionId.
150
- */
151
42
  export function createAuthorizer(config, sessionStore) {
152
43
  return async ({ req, body }) => {
153
- // F-fix (direct-door principal spoofing, core-steered): the authoritative identity for governance + cost +
154
- // ownership + memory + checkpoint scope is the VERIFIED principal, never the spoofable header. This swap covers
155
- // every consumer that flows THROUGH the authorizer (spec.principal + cost/owner/memory/checkpoint scope); the
156
- // non-authorizer owner/cost sites (runOwnerOk / quotaExceeded / leader / idemKey / the run-route + worker-global
157
- // presence gates) call `gatedPrincipal` directly, same invariant. BFF/gated = unchanged (no-op there).
158
44
  const principal = verifiedPrincipal(req, config);
159
45
  if (config.requirePrincipal && !principal) {
160
46
  throw new HttpError(401, `missing principal header '${config.principalHeader}'`);
161
47
  }
162
- // 142-S4 projectId 线程化(design/142 §2 行为面):body.projectId 是「哪个项目」的选择器,不是身份 —
163
- // scope 的 tenant 段永远来自 verified principal(上面),所以它过 SHAPE 门即可。不合形状 = 422 typed
164
- // fail-loud(注入串/任意路径段绝不进 scope 键或 config.projects 查表)。RESUME 腿不过此门
165
- // (checkpoint 重建 auth 不跑 authorizer)— 持久化 body 在原始提交时已验过,形状不变。
166
- //
167
- // 🔴 A-033.1(#256)单源换装 + **宽读严写**两半:
168
- // ① 形状词法改从 core 取(`PROJECT_ID_REGEX`,core 自陈「projectId 格式单源」[637]①)。此前取自
169
- // registry-core 的**自持镜像**,而那份镜像与 core 已实证漂移:core 带 `/i`、registry-core 不带 ⇒
170
- // 一个 projectId 能过 core 的 scope 面(`parseProjectMarker`/`resolveProjectId` 都认大小写)却被
171
- // 本仓的门 422。registry-core 自己的 `types.d.ts` 就挂着「core 公开导出后即改从 core 取」的 TODO。
172
- // ② 收下之后**立即折小写**再往下走 —— core 的铸造面同款纪律(`formatProjectMarker` 亲读:验形用
173
- // 带 `/i` 的同一条正则,写盘 `projectId.toLowerCase()`)。这一折是承重的:`formatProjScope` 不做
174
- // 任何归一化,不折的话 `proj:<t>/ABC…` 与 `proj:<t>/abc…` 是**两只不同的记忆盘**,同一个项目按
175
- // 大小写分家(而 registry-core 的 `ProjectsConfig` 只收小写键 ⇒ 大写形还会在 config.projects 恒 miss)。
176
48
  let projectId;
177
49
  if (body.projectId !== undefined) {
178
50
  if (typeof body.projectId !== "string" || !PROJECT_ID_REGEX.test(body.projectId)) {
@@ -180,69 +52,26 @@ export function createAuthorizer(config, sessionStore) {
180
52
  }
181
53
  projectId = body.projectId.toLowerCase();
182
54
  }
183
- // design/114 Phase3 (reuse-path warm-resume): the caller requires the session to ALREADY exist. The authorizer
184
- // must NOT claim-create a missing id — that would materialize an empty session_meta row and DEFEAT core's
185
- // acquire({requireExisting}) fail-loud, silently turning a resume of a PURGED/EXPIRED session into a fresh empty
186
- // run (the exact "looks warm, actually fresh" bug the flag prevents). So with it set: a missing requested id
187
- // FAILS LOUD with a clean 404 HERE (eager — before any SSE headers, so BOTH the sync and stream legs get a real
188
- // status, not an SSE-buried error) and is NEVER claim-created; a request with NO sessionId is a caller error (the
189
- // authorizer would otherwise mint a fresh id — there is no existing session to resume). This is the SERVICE-side
190
- // complement to core's store-level fail-loud (which still covers run-local / no-ownerOf backends / direct core):
191
- // without it the store never SEES a missing session (the authorizer pre-registers it), so store honoring alone is
192
- // inert on the HTTP path.
193
55
  const requireExisting = body.requireExistingSession === true;
194
56
  const ownerOf = sessionStore.ownerOf?.bind(sessionStore);
195
57
  const register = sessionStore.register?.bind(sessionStore);
196
58
  const requested = typeof body.sessionId === "string" && body.sessionId ? body.sessionId : undefined;
197
59
  let sessionId;
198
60
  if (requested) {
199
- // 🔴 [2195]:两个能力**解耦判**(旧码是 `if (ownerOf && register)` 一把抓)。它们回答的是两个
200
- // 不同的问题:`ownerOf` = 「这个 session 归谁」(判归属),`register` = 「把新 id 记在谁名下」
201
- // (能记账)。旧码把两者绑成一个开关,于是 `ownerOf` 在、`register` 缺时,**明明判得了归属却
202
- // 整条不判** —— 仓里有一条特征化测试逐字钉着这个结果(principal 直接叫 `user:intruder`,
203
- // 注释写「would be a mismatch IF enforced」),它 describe 名却是 "in-memory dev backend",
204
- // 而配的是 `requirePrincipal: true`(多租)。描述与执行面脱钩,读标题的人以为那是 dev-only 的事。
205
61
  if (ownerOf) {
206
62
  let owner = await ownerOf(requested);
207
63
  if (owner === undefined) {
208
64
  if (requireExisting) {
209
- // design/114 Phase3: the session is genuinely missing (no owner row) AND the caller requires it to
210
- // EXIST → FAIL LOUD with a clean HTTP 404 on BOTH the sync AND the streaming leg. The authorizer runs
211
- // (in prepareSpec) BEFORE any SSE headers are committed, so the stream leg gets a real status code — not
212
- // a `resume.session_not_found` buried in an SSE `done` payload, which a 200-committed stream cannot
213
- // upgrade. We must NOT claim-create it either (that would defeat the fail-loud). The durable stores ALSO
214
- // honor `{requireExisting}` (run-local / a no-ownerOf memory backend / direct core), so the fail-loud
215
- // still holds on the paths that never reach this authorizer.
216
65
  throw new HttpError(404, "requireExistingSession: session does not exist (resume.session_not_found)");
217
66
  }
218
67
  if (register) {
219
- await register(requested, principal ?? null); // unknown id → claim for this principal
220
- // Re-read the authoritative stored owner: register is an idempotent upsert that keeps the
221
- // FIRST owner, so a concurrent first-claim of the same id by another principal could have won
222
- // the race. Without this re-check the loser would silently attach to the winner's session.
68
+ await register(requested, principal ?? null);
223
69
  owner = await ownerOf(requested);
224
70
  }
225
71
  else if (config.requirePrincipal) {
226
- // 🔴 [2195] 的第二半:能判归属但**记不了账**。放行的话这个 id 的归属永远不会被写下,
227
- // 于是下一个租户带同一个 id 来,同样看到「不存在」、同样被放行 —— 两个租户共用一条会话,
228
- // 第二个人读得到第一个人的对话。多租下这与「归属不匹配」是同一类越权,同样拒。
229
72
  throw new HttpError(403, "cannot claim a caller-supplied sessionId on a multi-tenant deployment: this worker's session store cannot record session ownership, so the id would stay unowned and the next caller could attach to it. Configure a durable session backend (SESSION_BACKEND), or omit sessionId to start a new session.");
230
73
  }
231
74
  }
232
- // Enforce ownership whenever the session HAS an owner — including against callers that present
233
- // no principal (audit B — the fix IS this guard, right here in `createAuthorizer`; no line-number
234
- // anchor on purpose, the old `security.ts:77` pointer had drifted onto an unrelated JSDoc):
235
- // with the old `principal &&` guard, a caller that
236
- // omitted the header under requirePrincipal=false could attach to ANY owned session by id.
237
- // Anonymous callers may still attach to anonymous (owner=null) sessions, so single-tenant dev
238
- // (no principals anywhere) is unaffected; flipping the requirePrincipal default is NOT needed.
239
- //
240
- // [3262] 存量哨兵窄互认([3260] cli live 取证→本仓裁 (a)):cli ≤1.0.70 对本地单用户自发
241
- // `anon:shell-live` header,存量 session 的 owner 列全是这一个哨兵;cli 停发(表示法归一为
242
- // 「缺席」,[3239] 复裁)后若仍逐字比对,缺席请求 attach 存量会话恒 401 —— 用户全部既存
243
- // resume 即断。互认=同一主体(本地单用户)两个表示法的过渡承接,不是兼容臂:三条件缺一
244
- // 不可(单机形 ∧ owner 恰为哨兵字面量 ∧ 请求 principal 缺席),带任何具名 principal 的请求
245
- // 对哨兵会话仍走下方 403;多租形 header 必到,本臂不可达。存量随会话生命周期消亡后本臂可整删。
246
75
  const legacyShellOwner = !config.requirePrincipal && owner === "anon:shell-live" && principal === undefined;
247
76
  if (!legacyShellOwner && owner != null && owner !== principal) {
248
77
  throw principal
@@ -251,31 +80,11 @@ export function createAuthorizer(config, sessionStore) {
251
80
  }
252
81
  }
253
82
  else if (config.requirePrincipal) {
254
- // 🔴 [2195](cli 七版实测:bob 续聊 alice 的 session,期望 403 实测 200)的根因修。
255
- //
256
- // 上面整段 owner 门挂在 `ownerOf && register` 上,而 core 的**默认内存店 `TtlSessionStore`
257
- // 这两个方法都没有**。能力缺席时,旧码不是「拒绝」而是让门**整条消失** —— caller 自报的
258
- // sessionId 被原样受理,于是多租部署里 bob 直接续聊 alice 的会话。
259
- //
260
- // 这也解释了 cli 观察到的 GET/POST 不对称:GET 走 `routes/sessions.ts` 的
261
- // `deps.sessionStorage.ownerOf`(durable session 面,**另一个对象**)⇒ 判别在场 ⇒ 404;
262
- // POST 走这里的 runner sessionStore ⇒ 门蒸发。也解释了为什么按路由 diff 定位不到:
263
- // **路由代码没变**,变的是装配里塞进来的 store 实现有没有这两个方法。
264
- //
265
- // 判据改成 fail-closed:多租形下拿不到归属判别能力,就不能允许复用一个 caller 自报的
266
- // sessionId(拒绝的是「复用」,不是「提交」—— 不带 sessionId 的新会话照常受理)。
267
- // 单租/dev 形(requirePrincipal=false)零行为变化:那里没有租户边界可越。
268
- // ⚠️ 消息必须点名「连本 worker 自己刚铸的 id 也一样」:无 `register` 的 store 上,新会话的
269
- // 归属**根本没被记下**,所以第二个 turn 带回引擎自己给的 id 时同样落到这里。不说清楚的话,
270
- // 这个 403 从消费端看是纯粹的反直觉(「这 id 是你给我的,你现在说我无权用」)——
271
- // 实测:客户端「首 turn 不发 sessionId、后续 turn 带回铸值」这一常见形状,第二个 turn 起必拒。
272
83
  throw new HttpError(403, "session reuse needs an owner-aware session store on a multi-tenant deployment: this worker's session store cannot attest who owns a session, so a sessionId cannot be authorized — including one this worker itself minted a moment ago (without a register seam it was never recorded as yours). Configure a durable session backend (SESSION_BACKEND), or omit sessionId on every turn to start fresh.");
273
84
  }
274
85
  sessionId = requested;
275
86
  }
276
87
  else {
277
- // design/114 Phase3 (M2 parity): requireExistingSession with NO sessionId is a caller error — the authorizer
278
- // mints a fresh id otherwise, so core's own no-sessionId M2 check can never fire on the service path.
279
88
  if (requireExisting) {
280
89
  throw new HttpError(422, "requireExistingSession requires a sessionId — there is no existing session to resume (resume.session_not_found)");
281
90
  }
@@ -286,113 +95,36 @@ export function createAuthorizer(config, sessionStore) {
286
95
  return { principal, sessionId, memoryScope: memoryScopeFor(config, principal, projectId), resolvedProjectId: projectId };
287
96
  };
288
97
  }
289
- /** F1/F3 (codex scope-ledger audit 2026-07-14) — the auth-boundary principal SHAPE gate, applied to every
290
- * verified identity source (trusted header, SSO-bridge sub, direct-door JWT sub) so no downstream consumer
291
- * ever sees an ambiguous or over-wide principal:
292
- * - RESERVED internal identifiers are REJECTED (修1, 三路复审 A 组 HIGH — see {@link RESERVED_PRINCIPALS} /
293
- * {@link RESERVED_PRINCIPAL_PREFIXES} below): a REAL tenant whose principal string-equals an internal scope
294
- * sentinel would pass the plain string-compare owner/scope gates downstream and merge with internal rows —
295
- * e.g. `"_"` merges with the anonymous scope family (the checkpoint scope column `auth?.principal ?? "_"`
296
- * at suspend, and the decide owner-gate treats `cp.scope === "_"` as anonymous-OPEN, server.ts ~L3710; the
297
- * SendUserFile ledger scope + the /v1/sendfile-links fleet-wide default; the resource-suspend isolation
298
- * key), `"_leader"` merges with the leader durable-approval owner scope (a tenant so named could list/
299
- * decide internal leader approvals), `"_leader-…"` with the leader resource-suspend scope family, and
300
- * `"__none__"` with the missing-principal placeholder scope on the decide/pending gates.
301
- * - length > 190 is REJECTED: every scope/owner column the principal lands in VERBATIM is VARCHAR(190)
302
- * (sendfile_link.scope, approval.owner/scope, memory-engine scope, rate-limit keys, …) — an over-long
303
- * principal would truncate/500 at the INSERT and split one identity into two.
304
- * Throws a typed 400 `HttpError` (the HTTP layer maps it); the message names the rule (a reserved identifier /
305
- * the length cap), never a secret and never the internal scope's semantics. */
306
98
  export const PRINCIPAL_MAX_LENGTH = 190;
307
- /** 修1 — the SINGLE registry of internal sentinel/scope identifiers a tenant principal must never equal.
308
- * 🔴 新增内部 sentinel scope 必须在此登记(精确值加 RESERVED_PRINCIPALS,前缀族加
309
- * RESERVED_PRINCIPAL_PREFIXES),否则 assertPrincipalShape 会放行同名租户 = 与内部 scope 行合流的越权面。
310
- * Current entries (where each is minted): `"_"` = single-user/anonymous sentinel (checkpoint/sendfile/
311
- * resource-suspend scope family); `"__none__"` = missing-principal placeholder on the decide/pending gates
312
- * (http/server.ts `principal ?? "__none__"`); `"_leader"` = leader durable-approval scope
313
- * (src/leader/wire.ts); prefix `"_leader-"` = the leader resource-suspend scope family
314
- * (`_leader-resource-<workerId>`, src/leader/wire.ts). NB the prefix REQUIRES the hyphen — a tenant named
315
- * `_leaderX` collides with nothing internal and stays valid (boundary pinned by test). */
316
99
  export const RESERVED_PRINCIPALS = ["_", "__none__", "_leader"];
317
100
  export const RESERVED_PRINCIPAL_PREFIXES = ["_leader-"];
318
101
  export function assertPrincipalShape(principal) {
319
102
  if (RESERVED_PRINCIPALS.includes(principal) || RESERVED_PRINCIPAL_PREFIXES.some((p) => principal.startsWith(p))) {
320
- // Wording deliberately names the RULE (reserved internal scope sentinel), not what the sentinel does.
321
103
  throw new HttpError(400, `principal ${JSON.stringify(principal)} is reserved (an internal scope sentinel) — authenticate with a real tenant identifier`, { code: "principal_reserved" });
322
104
  }
323
105
  if (principal.length > PRINCIPAL_MAX_LENGTH) {
324
106
  throw new HttpError(400, `principal exceeds ${PRINCIPAL_MAX_LENGTH} characters (the scope-column width every store pins, e.g. sendfile_link.scope VARCHAR(190)); got ${principal.length}`, { code: "principal_too_long" });
325
107
  }
326
108
  }
327
- /** The checkpoint `scope` column's single-user/anonymous sentinel — the same "_" documented above
328
- * ({@link RESERVED_PRINCIPALS}) as the checkpoint/sendfile/resource-suspend scope family's shared value.
329
- * Single attested constant + codec pair (naming precedent: {@link SENDFILE_SINGLE_USER_SCOPE} in
330
- * ./plugins/send-file-ledger.js) so every checkpoint scope read/write goes through one place instead of
331
- * each call site re-deriving the "_" ⇄ no-principal mapping inline. */
332
109
  export const CHECKPOINT_PUBLIC_SCOPE = "_";
333
- /**
334
- * 🔴 A-057.44(2026-08-19)—— **后台子代注册簿的匿名租户键**,与上面那只 checkpoint 哨兵是
335
- * 「同一件事(没有 principal)的两套写法」。
336
- *
337
- * 写侧不是本仓能单方面改的:core 的 `treeScope = reviveClaim?.row.scope ?? ctx.principal ??
338
- * opts.background?.scope` 同时喂**进程内** `defaultTaskRegistry` 与 **durable** 行,而本仓/`core` 全域的
339
- * 读侧是一整族 `principal ?? "default"`(`runs.ts` markStopSourceForOwner、`boot/session-faces.ts`
340
- * reapSessionBackground、`http/routes/runs.ts` 的 subagent output/stream 三口、`agents-roster.ts`、
341
- * `fleet.ts`)。⇒ **把铸点改成 `"_"` 会把这一整族读者一起打瞎**(本车实测:`subagent-tail-content-supply`
342
- * 的 tail 建连当场 404)。所以归一只能发生在**读侧**:checkpoint 侧拿着 `"_"` 去查 bg 分区时,
343
- * 显式把这两个键当同一格。
344
- */
345
110
  export const BACKGROUND_REGISTRY_ANON_SCOPE = "default";
346
- /**
347
- * checkpoint 的 scope → 该在哪些 **bg 注册簿分区**里找它的 parked 子代(A-057.44)。
348
- *
349
- * 匿名(`"_"`)⇒ 两个都查:`"_"`(将来若统一了写侧)与 `"default"`(今天 core 真写下去的那个)。
350
- * 其余 ⇒ 原样单查:有 principal 时两套约定折出**同一个**串(`encodeCheckpointScope` 只在 null/undefined
351
- * 时才替换,core 那侧也只在 `ctx.principal` 缺席时才落兜底),不存在第二个候选;而多租户部署里一个
352
- * **真名叫 `default`** 的租户必须只匹配自己那格 —— 所以别把别名做成双向。
353
- */
354
111
  export function backgroundScopesForCheckpointScope(scope) {
355
112
  return scope === CHECKPOINT_PUBLIC_SCOPE ? [CHECKPOINT_PUBLIC_SCOPE, BACKGROUND_REGISTRY_ANON_SCOPE] : [scope];
356
113
  }
357
- /** Encode a principal (or none) as the checkpoint `scope` column value. */
358
114
  export const encodeCheckpointScope = (principal) => principal ?? CHECKPOINT_PUBLIC_SCOPE;
359
- /** Decode a checkpoint `scope` column value back to a principal — {@link CHECKPOINT_PUBLIC_SCOPE} maps to
360
- * `undefined` (no principal), everything else passes through verbatim. */
361
115
  export const decodeCheckpointScope = (scope) => (scope === CHECKPOINT_PUBLIC_SCOPE ? undefined : scope);
362
- /** Read the authenticated principal from the trusted header. Never from the body. */
363
116
  export function principalFrom(req, config) {
364
117
  if (!config.principalHeader)
365
- return undefined; // no header configured → no principal (defensive; real config always sets it)
118
+ return undefined;
366
119
  const raw = req.headers[config.principalHeader.toLowerCase()];
367
120
  const principal = (Array.isArray(raw) ? raw[0] : raw)?.trim() || undefined;
368
121
  if (principal !== undefined)
369
- assertPrincipalShape(principal); // F1/F3: reserved-sentinel + length gate at the boundary
122
+ assertPrincipalShape(principal);
370
123
  return principal;
371
124
  }
372
- /** Header carrying the crypto-verified principal JWT on a direct-door worker (the issuer signs it; the worker only
373
- * verifies). The trusted twin of `config.principalHeader` (the latter is client-spoofable on a direct door). */
374
125
  export const PRINCIPAL_TOKEN_HEADER = "x-approval-principal-token";
375
- /**
376
- * direct-door 决策证明三件套的另外两个头名(JWT 在上面那个常量里)。
377
- *
378
- * 为什么也具名(2026-08-08,#176 二轮扫描 finding):这三个头名此前在
379
- * `http/routes/approvals-assistant.ts` 的两个 `verifyDirectDoorProof` 调用点、以及 `http/server.ts` 的
380
- * CORS `access-control-allow-headers` 串里**各手抄一份**——而 CORS 那一行同一句里已经在用
381
- * `${principalHeader}` 插值另一个头名,证明具名写法在该处本就可用。改任一头名时漏改 CORS 串的后果
382
- * 是**静默的**:浏览器预检把该头挡下,请求到不了 handler ⇒ 表现成「direct-door 上审批面莫名失效」,
383
- * 而不是任何一处报错。低危(改头名本身即 wire BREAKING、不太可能单边发生),但零成本可消。
384
- */
385
126
  export const APPROVAL_MAC_HEADER = "x-approval-mac";
386
127
  export const APPROVAL_MAC_KID_HEADER = "x-approval-mac-kid";
387
- /** The DIRECT-DOOR-SAFE principal — the VERIFIED identity for governance + cost + isolation. On a BFF/gated deployment
388
- * the header is already verified upstream (return it); on a direct-door worker the `principalHeader` is client-
389
- * spoofable, so identity comes from the crypto-verified `x-approval-principal-token` JWT (the issuer's private key
390
- * never lives on a worker). `spec.principal` MUST be this, NEVER `principalFrom` — keying a gate or a bill on a
391
- * spoofable header is a multi-tenant governance/cost bypass (core's TaskSpec.principal contract). Single source for
392
- * `createAuthorizer` AND every `gatedPrincipal` call site (F-fix: direct-door principal spoofing, core-steered). */
393
- /** 鉴权桥:请求入口 async 预认证把验过的 registry-JWT principal 挂在这个 symbol 上——它是
394
- * request-local 真源(来自签名,非自报头),优先于任何头读取。setSsoPrincipal 在 server.ts 的入口块设,
395
- * verifiedPrincipal 在此读。 */
396
128
  const SSO_PRINCIPAL = Symbol.for("sema.ssoVerifiedPrincipal");
397
129
  export function setSsoPrincipal(req, principal) {
398
130
  req[SSO_PRINCIPAL] = principal;
@@ -401,9 +133,6 @@ export function ssoVerifiedPrincipal(req) {
401
133
  const v = req[SSO_PRINCIPAL];
402
134
  return typeof v === "string" && v.length > 0 ? v : undefined;
403
135
  }
404
- /** 鉴权桥 scope 透传:registry-JWT 的 `scope` claim(多租户活动租户,registry-core 0.3.0+
405
- * `VerifiedIdentity.scope`)也挂 request-local——与 principal 同真源(来自签名,非自报头)。用途=可观测/审计
406
- * 归因,以及未来 scope 级 quota/过滤的读取地基(单一真源)。ABSENT = pre-scope/默认 token,语义"global"。 */
407
136
  const SSO_SCOPE = Symbol.for("sema.ssoVerifiedScope");
408
137
  export function setSsoScope(req, scope) {
409
138
  req[SSO_SCOPE] = scope;
@@ -413,15 +142,13 @@ export function ssoVerifiedScope(req) {
413
142
  return typeof v === "string" && v.length > 0 ? v : undefined;
414
143
  }
415
144
  export function verifiedPrincipal(req, config) {
416
- // 鉴权桥优先:registry-JWT 验过的 principal 来自签名,在 directDoor 与 BFF/gated 两种部署都胜出
417
- // (directDoor 下原走 x-approval-principal-token,SSO principal 在其之前返回,成为下游的可信身份)。
418
145
  const sso = ssoVerifiedPrincipal(req);
419
146
  if (sso) {
420
- assertPrincipalShape(sso); // F1/F3: the SSO-bridge sub is a verified identity source too — same boundary gate
147
+ assertPrincipalShape(sso);
421
148
  return sso;
422
149
  }
423
150
  if (!config.directDoorActive)
424
- return principalFrom(req, config); // BFF/gated: the header is already verified upstream
151
+ return principalFrom(req, config);
425
152
  const h = req.headers[PRINCIPAL_TOKEN_HEADER];
426
153
  const token = (Array.isArray(h) ? h[0] : h) ?? "";
427
154
  const sub = verifyPrincipalJwt(token, config.principalJwtPubkeys, {
@@ -429,20 +156,8 @@ export function verifiedPrincipal(req, config) {
429
156
  aud: config.principalJwtAud ?? "\0",
430
157
  maxTtlSec: config.principalJwtMaxTtlSec,
431
158
  })?.sub;
432
- // F1/F3: gate the direct-door JWT sub as well — a typed 400 naming the rule beats a silent-null 401 (the
433
- // minter must not be able to issue a "_"-sub token that merges with the anonymous scope family downstream).
434
159
  if (sub !== undefined)
435
160
  assertPrincipalShape(sub);
436
161
  return sub;
437
162
  }
438
- // design/158 N17: memoryScopeFor / memoryEngineRemoteLanePosture / memoryEngineBackendFor / memorySpecForRequest
439
- // moved VERBATIM to the leaf ./memory-scope.js (security.ts's name says auth; these four resolve memory-plane
440
- // config, unrelated). Import them from ./memory-scope.js directly ([2354] 兼容面全清 — the compat re-export here
441
- // is gone); this file's own createAuthorizer below still calls memoryScopeFor.
442
- // design/158 A13: approval-envelope HMAC (integrity, verifyApprovalHmac/approvalHmacMessage/
443
- // MAX_APPROVAL_REASON_CHARS) and principal-JWT/direct-door proof (identity, verifyPrincipalJwt/
444
- // approvalBnd/verifyDirectDoorProof) moved VERBATIM to ./approval-hmac.js and ./principal-jwt.js — this file
445
- // keeps only the authenticator/principal-shape/SSO-mount group (createAuthorizer, verifiedPrincipal,
446
- // assertPrincipalShape, principalFrom, setSsoPrincipal/setSsoScope). Import the moved names from their
447
- // new homes directly ([2354] 兼容面全清 — the compat re-exports here are gone).
448
163
  //# sourceMappingURL=security.js.map
@@ -1,44 +1,12 @@
1
- /**
2
- * Session leaf-advance bus — the S2 fast path of the session-level SSE subscription
3
- * ([1196]/[1208]③: an append that lands on THIS replica fans out to watchers synchronously,
4
- * zero latency on single-replica deployments; cross-replica watchers still ride the probe lane).
5
- *
6
- * Deliberately a REPLICA-LOCAL singleton (module state), not a constructor-threaded dependency:
7
- * the leaf write sits at the bottom of the per-session storage stack (TiDB/Pg `persist()` — a
8
- * static-factory + N-arg constructor chain away from main.ts), while the one consumer
9
- * (SessionWatchRegistry.notifyLocal) is replica-local by design. A bus keeps the wiring one line
10
- * per write site and one line in main; anything smarter re-plumbs four layers for the same effect.
11
- *
12
- * Contract:
13
- * - `emitLeafAdvance` MUST be fire-and-forget from the writer's perspective: it never throws
14
- * (listener errors are swallowed) and does no async work — a broken watcher must not fail or
15
- * slow a session write.
16
- * - Emit AFTER the storage commit succeeds (a rolled-back CAS must not fan out a leaf that never
17
- * became durable).
18
- * - Owner IS carried and MUST be the COMMIT-TIME row owner (codex two rounds: without owner the
19
- * generation fence is skipped; a wake-time snapshot is worse — stale-A over a reclaimed row false-
20
- * matches an old A entry and leaks the new tenant's leaf into the old stream). Zero-cost source:
21
- * the persist transaction's own leaf_seq SELECT carries the owner column — the emitted owner is the
22
- * row's tenant at the moment the write committed.
23
- * - `leafSeq`(the row's committed leaf_seq)rides every emit: DB commits serialize, but the
24
- * post-commit continuations don't — without an ordering token a delayed leaf-N delivery after
25
- * leaf-N+1 would walk the registry BACKWARD (and a stale notification could false-drop a newer
26
- * generation on owner mismatch). The registry keeps the highest accepted seq and ignores lower.
27
- * - The local (file/in-mem) lane does not emit: those sessions persist inside core's storage
28
- * backend with no server-side write hook; the probe lane covers them at its normal cadence.
29
- */
30
1
  let listener;
31
- /** main.ts wires this once at boot (→ SessionWatchRegistry.notifyLocal). Last write wins; tests reset with undefined. */
32
2
  export function setLeafAdvanceListener(fn) {
33
3
  listener = fn;
34
4
  }
35
- /** Called by the SQL session-storage write sites post-commit. Never throws, never awaits. */
36
5
  export function emitLeafAdvance(sessionId, leafId, owner, leafSeq) {
37
6
  try {
38
7
  listener?.(sessionId, leafId, owner, leafSeq);
39
8
  }
40
9
  catch {
41
- /* a broken watcher must never fail a session write */
42
10
  }
43
11
  }
44
12
  //# sourceMappingURL=session-leaf-bus.js.map
@@ -1,70 +1,9 @@
1
- /**
2
- * session-sync 的**内容判据**——回答一个 id-集合分类器回答不了的问题:
3
- * 「两条 entry id 集合相等的日志,**内容**是否也相同?」
4
- *
5
- * ── 为什么需要它(深挖第一轮 + 2026-07-26 顺着往下查出的第二半)────────────────────────────────────
6
- * §7 的 `classifySyncRelationshipByIds` 只比 **id 集合**,而 entry id 是 uuidv7、**不是内容寻址** ⇒
7
- * 「同 id、异载荷」结构上完全可能(wire 本身就收调用方给的 entries)。第一半的后果已知:Phase A 判 `identical`
8
- * ⇒ 整段跳过、调用方拿 200 而目的端仍是自己那份内容(真 HTTP 复现过)。
9
- *
10
- * 🔴 **第二半是我一开始判错的地方**:我在黑板 [1701] 说「server 单方面修不了」。那句只对 **Phase A** 成立
11
- * (那时 server 手上没有源端内容,必须靠调用方送摘要)。但 **commit 时 server 同时握着两份日志**
12
- * (staged + dst),所以内容比较**根本不需要调用方配合** —— 而 4 处 commit/import 路径当时都写着
13
- * 「`relation === "identical"` ⇒ 跳过 entries 写入」,把「id 集合相等」当成了「内容相同」。
14
- * 后果:一个**已经把整条日志上传完**的调用方(没送摘要、或送了但不可比),在 commit 处仍然拿到 no-op。
15
- * ⇒ 两半都要修:Phase A 需要调用方的 `logDigest`(省掉无谓上传),commit 这一半是 server 自己的活。
16
- *
17
- * ── 判据来源:core 1.414 导出的 `sessionLogDigest` ──────────────────────────────────────────────────
18
- * 覆盖每条 entry 的**全部字段**(id / parentId / type / timestamp / 载荷)且**顺序显著**,逐条带长度前缀
19
- * (所以两份日志不可能被重新切分成同样的字节)。core 明确它可以两端各算 —— 与 `boundInputHash` 不同,
20
- * 因为这里**分叉的代价是多同步一趟,不是错答案、也不是拒绝**(审批路径上分叉会拒掉一个合法的人类决定,
21
- * 方向相反)。
22
- *
23
- * ── ⚠️ v1 摘要下本判据的**退化行为**(core 黑板 [1709];1.415.0 已修,现行下限已远高于此 ——
24
- * 承重下限与理由见 `test/core-dependency-floor.test.ts`,别在这里记版本号)────────────────
25
- * v1 的摘要区分 `{a: undefined}` 与 `{}`,而 core 自己成规模产 own-key=undefined 的 entry ⇒ 同一份日志的
26
- * 内存形与 JSON 往返形摘要必然不同。本函数在 commit 处比的是 **staged**(刚从 wire 解析,JSON 形)与 **dst**
27
- * (从存储读回,也是 JSON 形)—— 两侧同形,所以**这一处**不受那条影响;但若某个后端把 staged 留在内存形,
28
- * 判据会退化成「恒不相等 ⇒ 恒改写」。
29
- * 🔴 那个退化方向是**安全**的(多写一遍,不会丢数据),这是刻意选的:判据不确定时**宁可改写**,
30
- * 因为反方向的错(误判"内容相同"而跳过写入)正是本函数要修的那个缺陷。
31
- * ⇒ 所以这里**不加 scheme 门**。(Phase A 那条路曾加过 scheme **名字门**,后被 tsc 判恒真并撤掉,
32
- * 换成「依赖下限 + 行为门」—— 名字门只认识旧名字,守不住这条性质;见 server.ts Phase A 段注释与
33
- * `test/sync-content-digest.test.ts`。)
34
- *
35
- * ⚠️ 抽成共享函数而不是在 4 处各写一遍:core [1689] RB-74 的教训(宣布"同族类修完成"而三个实现只落了一个),
36
- * 本仓今天也刚在 33 处 exec→FileError 包装上吃过同款。判据收成一处,才谈得上机器看守。
37
- */
38
1
  import { sessionLogDigest } from "@sema-agent/core";
39
- /**
40
- * id 集合已判 `identical` 的两条日志,**内容是否真的相同**。
41
- *
42
- * `true` ⇒ 内容也相同,跳过 entries 写入是**对的**(只需 owner 重戳)。
43
- * `false` ⇒ **同 id 异内容**:必须真正改写目的端,否则调用方拿到的是一句"已同步"的谎。
44
- *
45
- * 🔴 这里**不抛错、不拒绝**:core 定的消费口径是「digest 不符永远不得变成 error / 409 / 拒绝同步」——
46
- * 不符只意味着"多干一趟活",而不是"这次同步非法"。
47
- */
48
2
  export function identicalIdsAlsoIdenticalContent(staged, dst) {
49
3
  if (dst === null)
50
- return false; // 目的端没有日志 ⇒ 谈不上"内容相同",按需改写
4
+ return false;
51
5
  return sessionLogDigest(staged) === sessionLogDigest(dst);
52
6
  }
53
- /**
54
- * `fast_forward` 的**内容判据**([2557] 审计 A5/F2,2-i)——id 分类器的另一格盲区:
55
- * `dst ⊆ src` 只说明 **id 集合**是子集,共享 id 的**载荷**可能已分叉(目的端把某条 entry
56
- * 补写/纠正过、id 不变,源端拿着旧版本 + 新追加)。此时按「clean append」整段换装,
57
- * 目的端那几条纠正会被**静默销毁** —— §7「会丢目的端历史必须 409(除非显式 overwrite-dst)」
58
- * 在载荷层同样成立。与 `identical` 格的口径**方向不同**是刻意的:identical 格 core 已定
59
- * 「不符=多干一趟活、照常改写」(那里 src 按 id 完整覆盖 dst,push 语义=以源为准);本格
60
- * dst 的共享版本与 src 的追加互为对方没有的东西,是真分叉,归 409 那一族。
61
- *
62
- * 入参:`stagedShared` = staged 里 id ∈ dst 集合的那部分(**staged 顺序**);`dst` = 目的端全量
63
- * (**dst 顺序**)。两侧都保留各自顺序 —— 摘要顺序显著,共享段的**乱序**同样是分叉。
64
- *
65
- * 返回 `null` = 无分叉(安全 append);返回非空数组 = 分叉的 entry id(dst 顺序):
66
- * 逐条单 entry 摘要不等的 id;若逐条全等而整段摘要不等(纯乱序),列出位置错开的 id。
67
- */
68
7
  export function fastForwardSharedContentDiverged(stagedShared, dst) {
69
8
  if (sessionLogDigest([...stagedShared]) === sessionLogDigest([...dst]))
70
9
  return null;
@@ -82,13 +21,6 @@ export function fastForwardSharedContentDiverged(stagedShared, dst) {
82
21
  }
83
22
  return diverged;
84
23
  }
85
- /**
86
- * 把 fast_forward 的内容级分叉铸成 §7 冲突载荷(SyncConflictError.relation,wire 上是 409 的
87
- * `{relation}` 详情)。用现有 `fork` 成员而不是扩词表:内容层它**就是** fork(双方各握着对方
88
- * 没有的东西),且 SDK/客户端的关系词表是闭集,扩成员=BREAKING。字段口径(内容级,与 id 级
89
- * 注释的措辞差异在此登记):`srcExclusive` = 源端要追加的新 id;`dstExclusive` = 目的端将被
90
- * 销毁的共享版本的 id;`commonAncestor` = null(未逐条定位分叉点 —— 定位成本与收益不配)。
91
- */
92
24
  export function contentForkRelation(divergedIds, newEntryIds) {
93
25
  return { relation: "fork", commonAncestor: null, srcExclusive: [...newEntryIds], dstExclusive: [...divergedIds] };
94
26
  }