@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
@@ -1,28 +1,3 @@
1
- /**
2
- * src/plugins/scheduler-support.ts — the server-side `SchedulerCapability` backend (R7 self-wake).
3
- *
4
- * core 1.165.0 ships the `SchedulerCapability` seam (design/105): core 出接口 + 工具壳 (现名 CronCreate/CronDelete/
5
- * CronList——L8 订正:CronCancel 旧名、Sleep 已 design/136 撤除、挂载在 core prepare-task 非 createHandsToolkit,
6
- * `hasScheduler(env)` 门不变) + the `hasScheduler` gate. **持久化 /
7
- * cron 时钟 / idle 触发 / 起新 task 全在 backend** — core is request-driven, not resident. R7 splits the backend:
8
- * - THIS file (server) = the SchedulerCapability impl: the WRITE side — schedule/cancel/list, persisted to the
9
- * SHARED on-disk store. Injected into `RemoteHostExecutionEnv` (the TOC host lane). deps `@sema-agent/core` (fine
10
- * server-side).
11
- * - The TOC shell `SchedulerDaemon` (shell-host, design R7 part3b) = the READ side: a resident process that
12
- * polls the SAME store, resolves cron, fires due intents via the SDK (principal pinned), and reaps.
13
- *
14
- * The shared on-disk FORMAT is the dep-free `@sema-agent/registry-core/scheduler-store` contract; the fs binding is
15
- * `@sema-agent/registry-core/node`. server (writer) + shell daemon (reader) share ONE impl ⇒ zero format drift.
16
- *
17
- * Logic ported from the shell-host's canonical `scheduler/schedulerBackend.ts` (design doc
18
- * `R7-scheduler-rewind.md` §1: the server reuses the same schedulerBackend logic verbatim).
19
- *
20
- * 🔴 红线 (design/105): `principal` is `ctx` (Runner-held) NOT `intent` (model-controllable) → pinned into the
21
- * record, passed through to runTask unchanged by the daemon (no privilege escalation). cron via core
22
- * `isValidCronExpr` (no shell-exec / RCE). scope isolation: cancel/list touch only the caller's scope. durable:
23
- * the store is on disk (survives a daemon restart). upsert: same (scope, when, label) overwrites (idempotent on a
24
- * tool retry).
25
- */
26
1
  import { join } from "node:path";
27
2
  import { homedir } from "node:os";
28
3
  import { randomBytes, createHash } from "node:crypto";
@@ -30,27 +5,15 @@ import { SchedulerError, isValidCronExpr } from "@sema-agent/core";
30
5
  import { loadSchedulerStore, saveSchedulerStore } from "@sema-agent/registry-core/node";
31
6
  const ok = (value) => ({ ok: true, value });
32
7
  const err = (error) => ({ ok: false, error });
33
- /** Default caps (fail-closed bounds; design/105 §3.9). Override per deployment. */
34
8
  export const DEFAULT_SCHEDULER_CAPS = {
35
9
  supported: true,
36
10
  maxScheduledPerScope: 32,
37
11
  minDelaySec: 10,
38
12
  minCronIntervalSec: 60,
39
- maxScheduledHorizonSec: 60 * 60 * 24 * 30, // 30d
40
- // core 1.301 CronCreate durable/session 分层 ([950]): session-scoped jobs are persisted with
41
- // lifetime:"session" and reaped by the resident shell daemon when the session ends. True here because
42
- // this backend faithfully persists the lifetime field (schedule() below) — without the flag core's
43
- // CronCreate refuses durable:false up front (fail-closed at the tool layer).
13
+ maxScheduledHorizonSec: 60 * 60 * 24 * 30,
44
14
  supportsSessionLifetime: true,
45
- // core 1.309 ([1000]③ ScheduleWakeup 治本位): true = a resident daemon reads this store and fires
46
- // session-wakeup records for its own sessions (the normal co-resident TOC shape). The DEPLOYMENT flips
47
- // this to false via SCHEDULER_SESSION_WAKEUP=false when the spawning shell knows no daemon will honor
48
- // them (headless spawn without a resident daemon) — core's ScheduleWakeup then refuses up front instead
49
- // of parking a wakeup that never fires. Capability = instance-lifetime snapshot (core TOCTOU 契约条款),
50
- // so the signal rides construction, never per-call.
51
15
  supportsSessionWakeup: true,
52
16
  };
53
- /** Human-readable `when` summary for list(). */
54
17
  function whenSummary(when) {
55
18
  if (when.kind === "cron")
56
19
  return `cron ${when.expr}`;
@@ -58,30 +21,16 @@ function whenSummary(when) {
58
21
  return `at ${new Date(when.atMs).toISOString()}`;
59
22
  return `delay ${when.delaySec}s`;
60
23
  }
61
- /** Upsert dedup key — same (scope, LANE, when, label) overwrites (idempotent on tool retry).
62
- * The lane term is the lifetime identity (adversarial review F1): without it a session job and a durable
63
- * job — or two different sessions' jobs — with the same (when, label) collide, and the daemon's
64
- * session-end reap would then silently delete what USED to be a durable/other-session job. core's cron
65
- * shorthand defaults to session lifetime with no label, so the collision is normal usage, not an edge. */
66
24
  function dedupKey(scope, intent, sessionId) {
67
25
  const lane = intent.lifetime === "session" ? `session:${sessionId ?? ""}` : "durable";
68
26
  return createHash("sha256").update(JSON.stringify([scope, lane, intent.when, intent.label ?? ""])).digest("hex").slice(0, 16);
69
27
  }
70
- /**
71
- * PURE validation of an intent against caps + now. Returns a SchedulerError or null (ok). No IO. Testable.
72
- * cron via core `isValidCronExpr` (RCE-safe, no shell-exec); delay/at bounded by minDelaySec + horizon.
73
- */
74
28
  export function validateIntent(intent, caps, nowMs) {
75
29
  if (!intent.prompt || typeof intent.prompt !== "string")
76
30
  return new SchedulerError("invalid_schedule", "prompt is required");
77
31
  const w = intent.when;
78
32
  const horizonMs = caps.maxScheduledHorizonSec * 1000;
79
33
  if (w.kind === "cron") {
80
- // Validate the cron is WELL-FORMED (RCE-safe — the expr never reaches a shell). The `minCronIntervalSec` FLOOR is
81
- // NOT enforced here: realizing a cron's actual fire interval requires cron-next, which the design assigns to the
82
- // resident daemon (it computes nextRunMs + can skip/floor a sub-interval fire). A 5-field cron's minimum interval
83
- // is 60s, so the default 60s floor is naturally satisfied; a deployment that TIGHTENS minCronIntervalSec relies on
84
- // the daemon to honor it. (Adversarial-review LOW: the advertised cap is daemon-enforced, not writer-enforced.)
85
34
  if (!isValidCronExpr(w.expr))
86
35
  return new SchedulerError("invalid_schedule", `invalid cron expression: ${w.expr}`);
87
36
  }
@@ -103,7 +52,6 @@ export function validateIntent(intent, caps, nowMs) {
103
52
  }
104
53
  return null;
105
54
  }
106
- /** nextRunMs for at/delay (direct); cron left undefined here (the resident daemon computes via a cron lib). */
107
55
  function computeNextRunMs(when, nowMs) {
108
56
  if (when.kind === "at")
109
57
  return when.atMs;
@@ -111,15 +59,9 @@ function computeNextRunMs(when, nowMs) {
111
59
  return nowMs + when.delaySec * 1000;
112
60
  return undefined;
113
61
  }
114
- /** The default shared store path: `SEMA_CONFIG_DIR` → `~/.sema`, file `scheduled_tasks.json` (server + shell agree). */
115
62
  export function defaultSchedulerStorePath() {
116
63
  return join(process.env.SEMA_CONFIG_DIR ?? join(homedir(), ".sema"), "scheduled_tasks.json");
117
64
  }
118
- /**
119
- * File-backed SchedulerCapability (durable across daemon restarts). Store = a single JSON file (atomic write via
120
- * the registry-core /node binding). The resident shell daemon reads the same store to fire intents. `now`
121
- * injectable for tests.
122
- */
123
65
  export class FileSchedulerBackend {
124
66
  schedulerCapabilities;
125
67
  storePath;
@@ -140,14 +82,9 @@ export class FileSchedulerBackend {
140
82
  const bad = validateIntent(intent, this.schedulerCapabilities, nowMs);
141
83
  if (bad)
142
84
  return err(bad);
143
- // A session-scoped job with no session to bind to could never be reaped (a zombie the daemon would
144
- // fire forever) — core's CronCreate refuses this at the tool layer; refuse here too for non-tool callers.
145
85
  if (intent.lifetime === "session" && !ctx.sessionId) {
146
86
  return err(new SchedulerError("invalid_schedule", 'lifetime:"session" requires a session to bind to (ctx.sessionId missing)'));
147
87
  }
148
- // [2436] 纵深(5.6.0):能力面已声明无 daemon 收割 session-lifetime(headless 部署形,
149
- // SCHEDULER_SESSION_LIFETIME=false)⇒ 落盘一条即必然孤儿([2425] 外溢病)。core 的 CronCreate 读
150
- // 能力面在工具层前门拒是第一道;这里是第二道——错装配宿主绕过工具层直调也不许落盘。
151
88
  if (intent.lifetime === "session" && this.schedulerCapabilities.supportsSessionLifetime === false) {
152
89
  return err(new SchedulerError("invalid_schedule", 'lifetime:"session" is not supported on this deployment (no resident daemon reaps session-lifetime records — the row would be an orphan)'));
153
90
  }
@@ -157,7 +94,6 @@ export class FileSchedulerBackend {
157
94
  const rr = r;
158
95
  return r.scope === ctx.scope && dedupKey(r.scope, { when: r.when, label: r.label, lifetime: rr.lifetime }, r.sessionId) === key;
159
96
  });
160
- // Per-scope cap (excluding the upsert target — overwriting an existing one doesn't grow the count).
161
97
  const scopeCount = records.filter((r) => r.scope === ctx.scope).length;
162
98
  if (existingIdx < 0 && scopeCount >= this.schedulerCapabilities.maxScheduledPerScope) {
163
99
  return err(new SchedulerError("limit_exceeded", `scope at cap (${this.schedulerCapabilities.maxScheduledPerScope})`));
@@ -166,20 +102,12 @@ export class FileSchedulerBackend {
166
102
  const rec = {
167
103
  id,
168
104
  scope: ctx.scope,
169
- ...(ctx.principal ? { principal: ctx.principal } : {}), // 🔴 pinned from ctx (Runner-held), never intent
105
+ ...(ctx.principal ? { principal: ctx.principal } : {}),
170
106
  ...(ctx.sessionId ? { sessionId: ctx.sessionId } : {}),
171
107
  prompt: intent.prompt,
172
108
  when: intent.when,
173
109
  ...(intent.label ? { label: intent.label } : {}),
174
- // core 1.265 ScheduledIntent.mode(缺省 "task" 向后兼容):persist the execution shape —
175
- // ScheduleWakeup's stop:true keys off the LIST face's mode echo (CC 206 D9); a dropped mode forced the
176
- // shell onto its label fallback. registry-core SchedulerRecord optional fields are additive by contract.
177
110
  ...(intent.mode ? { mode: intent.mode } : {}),
178
- // core 1.301 lifetime/recurring ([950]/[954]): persist BOTH so the daemon can reap session-scoped
179
- // jobs and delete a recurring:false cron after its first firing. Dropping them here was [954]-(b):
180
- // the tool answered "once" while the record said nothing → the daemon fired forever. Additive
181
- // SchedulerRecord fields (registry-core contract), same posture as `mode` above. recurring is a
182
- // tri-state (undefined = cron default "fire every match") — persist only when explicitly set.
183
111
  ...(intent.lifetime ? { lifetime: intent.lifetime } : {}),
184
112
  ...(intent.recurring !== undefined ? { recurring: intent.recurring } : {}),
185
113
  createdMs: nowMs,
@@ -200,9 +128,6 @@ export class FileSchedulerBackend {
200
128
  async cancel(id, ctx) {
201
129
  const records = this.load();
202
130
  const idx = records.findIndex((r) => r.id === id);
203
- // 🔴 scope isolation + 假肯定病修([2425]/[2209],5.6.0):missing 与 non-own-scope **同折**
204
- // err(not_found)——存在性仍不泄露(错 id/别人的任务/已删的重试三条到达路径一个答案),但不再
205
- // ok(undefined) 谎报「删掉了」。旧形的代价是真实的:模型删错 id 得到成功回执,真任务继续按点起跑。
206
131
  if (idx < 0 || records[idx].scope !== ctx.scope) {
207
132
  return err(new SchedulerError("not_found", `no scheduled task ${id} in your scope (wrong id, already cancelled, or not yours)`));
208
133
  }
@@ -217,15 +142,13 @@ export class FileSchedulerBackend {
217
142
  }
218
143
  async list(ctx) {
219
144
  const summaries = this.load()
220
- .filter((r) => r.scope === ctx.scope) // 🔴 scope-scoped, not global
145
+ .filter((r) => r.scope === ctx.scope)
221
146
  .map((r) => ({
222
147
  id: r.id,
223
148
  when: whenSummary(r.when),
224
149
  ...(r.label ? { label: r.label } : {}),
225
150
  ...(r.nextRunMs !== undefined ? { nextRunMs: r.nextRunMs } : {}),
226
- // echo the persisted mode (additive; pre-1.159.x records lack it → undefined = shell label fallback).
227
151
  ...(r.mode ? { mode: r.mode } : {}),
228
- // echo lifetime/recurring (ScheduledTaskSummary 1.302 fields; absent on pre-1.215 records).
229
152
  ...(r.lifetime ? { lifetime: r.lifetime } : {}),
230
153
  ...(r.recurring !== undefined ? { recurring: r.recurring } : {}),
231
154
  }));
@@ -1,30 +1,6 @@
1
- /**
2
- * SendUserFile server-side LEDGER (multi-tenant governance face, clay ruling 2026-07-14 — the second half of
3
- * [768]②a: the object key carries a HASHED scope segment, so the store alone can't answer "which tenant sent
4
- * what". This ledger is the scope↔object mapping the hash deliberately hides from URLs): one row per issued
5
- * link, keyed by a uuidv7 `id` (time-ordered → keyset pagination), indexed by `scope` (the VERIFIED principal
6
- * verbatim; "_" = the single-user sentinel, same as the checkpoint scope column).
7
- *
8
- * WRITE SEMANTICS (enforced by {@link withLedgerRecording}, wrapped around BOTH send lanes in main.ts): record
9
- * AFTER upload+verify succeed and BEFORE the file_link frame is emitted. A record failure is FAIL-LOUD for that
10
- * file (the governance face MUST have the row — a link the ledger can't see can't be listed or revoked) and the
11
- * already-uploaded object is DELETED with one retry (a public-track object with no ledger row would be an
12
- * unmanageable orphan); the thrown message reports the TRUE reclaim outcome (reclaimed vs orphan) but — 修4 —
13
- * NEVER the coordinates or the cause (it is model-visible via the tool result; a bucket/key leak there would
14
- * let the model rebuild the capability URL): those go to the operator log under a shared incident id.
15
- *
16
- * The ledger does NOT store URLs: the public-track URL is reconstructible from bucket+key, and a presigned-track
17
- * URL is a decaying capability that would be expired by the time anyone lists it. Reads return metadata + key.
18
- *
19
- * Four twins behind one interface (the approval-exemption pattern): TiDB/PG = durable cross-replica SQL rows
20
- * (`sendfile_link` table, DDL in tidb-pool.ts / pg-pool.ts); File = append-only JSONL hydrated on boot
21
- * (fsync — a torn tail must not fabricate a row); Memory = tests / ephemeral.
22
- */
23
1
  import { join } from "node:path";
24
2
  import { AppendLog, ensureDir, readJsonlRecords, uuidv7 } from "@sema-agent/core";
25
- /** Ledger scope sentinel for single-user issuance (no principal) — mirrors scopeSegment's "_" key segment. */
26
3
  export const SENDFILE_SINGLE_USER_SCOPE = "_";
27
- // ─── TiDB / MySQL twin ───────────────────────────────────────────────────────────────────────────────────
28
4
  export class TiDBSendFileLedger {
29
5
  pool;
30
6
  constructor(pool) {
@@ -32,8 +8,7 @@ export class TiDBSendFileLedger {
32
8
  }
33
9
  async record(entry) {
34
10
  await this.pool.query("INSERT INTO sendfile_link (id, scope, bucket, object_key, track, filename, byte_len, ttl_sec, task_id, session_id, created_at) " +
35
- "VALUES (?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE id = id", // idempotent on the uuidv7 PK (retry-safe no-op)
36
- [entry.id, entry.scope, entry.bucket, entry.objectKey, entry.track, entry.filename, entry.size, entry.ttlSec, entry.taskId ?? null, entry.sessionId ?? null, new Date(entry.createdAt)]);
11
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE id = id", [entry.id, entry.scope, entry.bucket, entry.objectKey, entry.track, entry.filename, entry.size, entry.ttlSec, entry.taskId ?? null, entry.sessionId ?? null, new Date(entry.createdAt)]);
37
12
  }
38
13
  async listByScope(scope, limit, beforeId) {
39
14
  const [rows] = await this.pool.query("SELECT id, scope, bucket, object_key, track, filename, byte_len, ttl_sec, task_id, session_id, created_at FROM sendfile_link " +
@@ -41,7 +16,6 @@ export class TiDBSendFileLedger {
41
16
  return rows.map((r) => rowToEntry(r));
42
17
  }
43
18
  }
44
- // ─── PG twin (dialect twin — $n placeholders + ON CONFLICT) ──────────────────────────────────────────────
45
19
  export class PgSendFileLedger {
46
20
  pool;
47
21
  constructor(pool) {
@@ -72,7 +46,6 @@ function rowToEntry(r) {
72
46
  createdAt: new Date(r.created_at).getTime(),
73
47
  };
74
48
  }
75
- // ─── Memory twin (tests / ephemeral) ─────────────────────────────────────────────────────────────────────
76
49
  export class MemorySendFileLedger {
77
50
  byScope = new Map();
78
51
  async record(entry) {
@@ -82,7 +55,7 @@ export class MemorySendFileLedger {
82
55
  this.byScope.set(entry.scope, m);
83
56
  }
84
57
  if (!m.has(entry.id))
85
- m.set(entry.id, { ...entry }); // idempotent on id
58
+ m.set(entry.id, { ...entry });
86
59
  }
87
60
  async listByScope(scope, limit, beforeId) {
88
61
  const m = this.byScope.get(scope);
@@ -95,7 +68,6 @@ export class MemorySendFileLedger {
95
68
  .map((e) => ({ ...e }));
96
69
  }
97
70
  }
98
- // ─── File twin (local lane — single append-only JSONL + boot hydrate, torn-tail-safe) ───────────────────
99
71
  export class FileSendFileLedger {
100
72
  path;
101
73
  mem = new MemorySendFileLedger();
@@ -105,7 +77,6 @@ export class FileSendFileLedger {
105
77
  ensureDir(dir);
106
78
  this.path = join(dir, "links.jsonl");
107
79
  for (const r of readJsonlRecords(this.path)) {
108
- // hydrate defensively: a record without the identity triple can't be listed/revoked — skip, don't fabricate
109
80
  if (!r || typeof r.id !== "string" || typeof r.scope !== "string" || typeof r.objectKey !== "string")
110
81
  continue;
111
82
  void this.mem.record(r);
@@ -113,29 +84,21 @@ export class FileSendFileLedger {
113
84
  }
114
85
  async record(entry) {
115
86
  this.log ??= new AppendLog(this.path);
116
- this.log.append(entry, true); // fsync — the governance row must survive a crash
87
+ this.log.append(entry, true);
117
88
  await this.mem.record(entry);
118
89
  }
119
90
  listByScope(scope, limit, beforeId) {
120
91
  return this.mem.listByScope(scope, limit, beforeId);
121
92
  }
122
- /** Release the append-log fd (LocalBackend.close → graceful-restart re-open). */
123
93
  dispose() {
124
94
  try {
125
95
  this.log?.close();
126
96
  }
127
97
  catch {
128
- /* best-effort */
129
98
  }
130
99
  this.log = undefined;
131
100
  }
132
101
  }
133
- /**
134
- * Wrap a lane `send` with the ledger write point: upload+verify succeed (the inner send resolved) → record →
135
- * only then return (the tool emits the file_link frame strictly after). Record failure ⇒ fail-loud for THIS
136
- * file + delete of the uploaded object with ONE retry; the throw is the contract either way, and its message
137
- * states the true reclaim outcome (F2: a failed reclaim = permanent orphan, warned with coordinates).
138
- */
139
102
  export function withLedgerRecording(send, deps) {
140
103
  return async (path, ctx) => {
141
104
  const link = await send(path, ctx);
@@ -155,18 +118,6 @@ export function withLedgerRecording(send, deps) {
155
118
  });
156
119
  }
157
120
  catch (e) {
158
- // Governance face MUST have the row — reclaim the object (an unledgered public object is unmanageable),
159
- // then fail loud for this file. F2 (codex scope-ledger audit): the DELETE gets ONE retry (S3 DELETE is
160
- // idempotent, so a transient blip degrades to a second attempt), and the thrown message reports the TRUE
161
- // reclaim outcome — a failed reclaim is a PERMANENT ORPHAN (record failed + object still live) and must
162
- // never be described as "reclaimed"; the warns carry the coordinates for manual cleanup.
163
- // 修4 (三路复审): the THROWN message is MODEL-VISIBLE (the tool splices per-file errors into its result,
164
- // and capabilities already expose the public endpoint) — bucket/key/underlying cause in it would let the
165
- // model reconstruct the permanent capability URL of an object the governance face can't see. The message
166
- // therefore carries only a fixed error code + a short incident id; coordinates and cause go to the
167
- // OPERATOR log (warn) keyed by the same incident id.
168
- // Backlog (deliberately not built here): a durable reclaim queue that retries beyond this request's
169
- // lifetime — today the orphan is surfaced loudly (incident-tagged warns) and cleaned up by hand.
170
121
  const incident = (deps.newId ? deps.newId() : uuidv7()).replace(/-/g, "").slice(-12);
171
122
  let reclaimed = true;
172
123
  try {
@@ -174,7 +125,7 @@ export function withLedgerRecording(send, deps) {
174
125
  await deps.deleteObject(link.bucket, link.key);
175
126
  }
176
127
  catch {
177
- await deps.deleteObject(link.bucket, link.key); // one retry: transient-fault degrade
128
+ await deps.deleteObject(link.bucket, link.key);
178
129
  }
179
130
  }
180
131
  catch (de) {
@@ -1,74 +1,19 @@
1
- /**
2
- * SendUserFile — the dual-track public-link ISSUER (design: sema-deploy docs/design/S3-PUBLIC-LINKS.md,
3
- * settled cross-repo 2026-07-14; tool name SendUserFile is final).
4
- *
5
- * The LLM hands a user a file over the web session as a URL the browser fetches DIRECTLY from S3/MinIO
6
- * (server relay was rejected: double bandwidth on third-party S3 + loses CDN). Two tracks, keyed on ttl:
7
- *
8
- * - ttl == 0 (PERMANENT, the default): upload to the anonymous-GET public bucket under an unguessable
9
- * `<scopeSeg>/<uuidv7>/<name>` key (capability URL — GET-only bucket policy, List disabled, so the prefix IS
10
- * the secret; scopeSeg = hashed tenant segment, see {@link scopeSegment} — multi-tenant list/revoke handle,
11
- * 1.190.0 [768]②a). URL = plain `<publicEndpoint>/<bucket>/<key>` — no signature, never expires, CDN-cacheable.
12
- * Revocation = delete the object.
13
- * ⚠️ Bucket-policy footgun (live-verified 2026-07-14): `mc anonymous set download` ALSO enables
14
- * ListBucket — an anonymous `GET /<bucket>/?list-type=2` then enumerates every uuidv7 key and the
15
- * capability design collapses. Deployments MUST use a GetObject-only JSON policy
16
- * (`mc anonymous set-json` with Action=["s3:GetObject"] on `arn:aws:s3:::<bucket>/*`): GET 200, LIST 403.
17
- * - 0 < ttl ≤ 7d: upload to the PRIVATE bucket and return a SigV4 presigned GET. The signature is
18
- * computed against the PUBLIC endpoint (SigV4 binds Host — signing with the internal host would 403
19
- * the user's browser; the reverse proxy must pass Host through).
20
- * - ttl > 7d: fail-loud — SigV4 caps presign at 7 days (physical limit); the caller should use ttl=0.
21
- *
22
- * Uploads always go through the INTERNAL endpoint (server-side PUT, its own signed request — Host binding
23
- * is per-request, so internal-for-write + public-for-read coexist). Zero-dependency posture: presignS3Url
24
- * + fetch, same as MinioBlobBackend / the k8s adapter.
25
- */
26
1
  import { createHmac } from "node:crypto";
27
2
  import { uuidv7 } from "@sema-agent/core";
28
3
  import { presignS3CopyUrl, presignS3Url } from "./s3-presign.js";
29
4
  import { OBJECT_STORE_IO_TIMEOUT_MS } from "./blob-backend.js";
30
- /** SigV4 physical ceiling (AWS: X-Amz-Expires max 604800s = 7 days). */
31
5
  export const PRESIGN_MAX_TTL_SEC = 604_800;
32
- /**
33
- * Multi-tenant governance (clay ruling 2026-07-14, blackboard [768]② variant a with a HASHED segment): every
34
- * object key carries a per-tenant SCOPE SEGMENT so tenants can be listed/revoked by prefix on the store itself,
35
- * WITHOUT leaking the principal into the (user-visible, forwardable) URL. The segment is
36
- * `HMAC-SHA256(secretKey, "sendfile-scope:" + scope)` hex, truncated to 32 chars:
37
- * - keyed on the S3 SECRET KEY — server-held and stable, so the mapping is deterministic per deployment but
38
- * unforgeable/unenumerable from outside (a URL holder cannot test principal guesses without the key);
39
- * - the "sendfile-scope:" DOMAIN SEPARATOR pins this HMAC use — the same secret signs SigV4 requests, and a
40
- * cross-protocol collision between "scope segment" and any other HMAC-of-attacker-controlled-string use is
41
- * exactly what domain separation exists to prevent;
42
- * - 32 hex chars (128 bits) is a NAMESPACE label, not a secret: the cross-tenant BIRTHDAY bound at even a
43
- * billion tenants is ~n²/2^129 ≈ 2^-69 — negligible. (The earlier 16-hex/64-bit cut had a ~2.7% birthday
44
- * collision probability at that scale — n²/2^65, NOT the per-pair 2^-32 the old note claimed; codex audit
45
- * F4, widened pre-release at zero cost.) Capability secrecy still lives in the uuidv7 segment. Key budget
46
- * stays comfortable: worst-case private-track key = 9 ("sendfile/") + 32 + 1 + 36 + 1 + 128 = 207 chars,
47
- * under both sendfile_link.object_key VARCHAR(512) and S3's 1024-byte key ceiling.
48
- * scope undefined/"" (single-user deployments have no principal) → the literal "_" sentinel (same sentinel the
49
- * checkpoint scope column uses for anonymous), so the key SHAPE is uniform across deployments.
50
- * ⚠️ Objects issued before 1.190.0 have NO scope segment (`<uuidv7>/<name>`); they stay valid — the segment is
51
- * purely additive (URLs are capability-complete either way), zero migration.
52
- */
53
6
  export function scopeSegment(secretKey, scope) {
54
7
  if (scope === undefined || scope === "")
55
8
  return "_";
56
9
  return createHmac("sha256", secretKey).update(`sendfile-scope:${scope}`).digest("hex").slice(0, 32);
57
10
  }
58
- /**
59
- * Collapse an arbitrary client-supplied name to ONE safe path segment: strip directories (both
60
- * separators), control/bidi characters (the session-title sanitize class), leading dots (no hidden
61
- * files / no "." ".."), and cap the length (S3 key segment budget; keep the extension when truncating).
62
- * Empty in ⇒ "file".
63
- */
64
11
  export function sanitizeSendFileName(name) {
65
12
  const base = (name ?? "").split(/[/\\]/).pop() ?? "";
66
13
  let clean = base.replace(/[\u0000-\u001f\u007f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "").replace(/^\.+/, "").trim();
67
14
  if (clean.length > 128) {
68
15
  const dot = clean.lastIndexOf(".");
69
16
  const ext = dot > 0 && clean.length - dot <= 16 ? clean.slice(dot) : "";
70
- // code-point 安全截断:UTF-16 slice 会切断代理对,留下 lone surrogate → encodeURIComponent 抛
71
- // URIError(此时对象已上传,链接却铸不出来)。按 code point 截,预算=128-ext 的 UTF-16 units。
72
17
  const budget = 128 - ext.length;
73
18
  let head = "";
74
19
  for (const ch of clean) {
@@ -81,8 +26,6 @@ export function sanitizeSendFileName(name) {
81
26
  return clean.length > 0 ? clean : "file";
82
27
  }
83
28
  export function createSendUserFileIssuer(cfg, deps) {
84
- // C3:六站点统一 abort 上界(半开 S3 端点此前把用户可见的工具调用挂满 undici ~300s,
85
- // reclaim 也挂住 ⇒ staging 孤儿+误报「reclaim FAILED」)。上界常量与 blob-backend 同源。
86
29
  const f = deps?.fetchImpl ?? fetch;
87
30
  const bounded = () => ({ signal: AbortSignal.timeout(OBJECT_STORE_IO_TIMEOUT_MS) });
88
31
  const upload = deps?.upload ??
@@ -111,11 +54,9 @@ export function createSendUserFileIssuer(cfg, deps) {
111
54
  throw new Error(`SendUserFile ttl must be a non-negative INTEGER of seconds (0 = permanent; SigV4 X-Amz-Expires is integer-only); got ${ttl}`);
112
55
  const name = sanitizeSendFileName(filename);
113
56
  const publicBase = cfg.publicEndpoint.replace(/\/+$/, "");
114
- const seg = scopeSegment(cfg.secretKey, scope); // hashed tenant segment ("_" single-user) — see scopeSegment doc
57
+ const seg = scopeSegment(cfg.secretKey, scope);
115
58
  const scopeField = scope !== undefined && scope !== "" ? { scope } : {};
116
59
  if (ttl === 0) {
117
- // Permanent track: capability URL on the anonymous-GET bucket. Key = <scopeSeg>/<uuidv7>/<name>
118
- // (pre-1.190 objects have no scope segment — additive, both shapes stay valid).
119
60
  const key = `${seg}/${uuidv7()}/${name}`;
120
61
  await upload(cfg.publicBucket, key, bytes);
121
62
  const url = `${publicBase}/${cfg.publicBucket}/${key.split("/").map(encodeURIComponent).join("/")}`;
@@ -124,7 +65,6 @@ export function createSendUserFileIssuer(cfg, deps) {
124
65
  if (ttl > PRESIGN_MAX_TTL_SEC) {
125
66
  throw new Error(`SendUserFile ttl ${ttl}s exceeds the SigV4 presign ceiling (${PRESIGN_MAX_TTL_SEC}s = 7 days) — use ttl=0 for a permanent link`);
126
67
  }
127
- // Presigned track: private bucket upload, GET signed against the PUBLIC endpoint (Host-bound).
128
68
  const key = `${cfg.privateKeyPrefix}${seg}/${uuidv7()}/${name}`;
129
69
  await upload(cfg.privateBucket, key, bytes);
130
70
  const url = presignS3Url({
@@ -140,9 +80,6 @@ export function createSendUserFileIssuer(cfg, deps) {
140
80
  });
141
81
  return { url, filename: name, size: bytes.byteLength, ttlSec: ttl, track: "presigned", bucket: cfg.privateBucket, key, ...scopeField };
142
82
  };
143
- // v2 (sandbox direct-upload): same key/track/URL algebra as issue(), split into sign-then-finalize so
144
- // the BYTES move sandbox→S3 directly (server never relays; creds never enter the sandbox — only a
145
- // single-object, method-bound, 120s PUT capability on a STAGING key does; see PreparedDirectUpload).
146
83
  const prepareDirectUpload = (filename, ttlSec, scope) => {
147
84
  if (cfg.publicEndpoint === undefined) {
148
85
  throw new Error("SendUserFile is not configured for public links: set S3_PUBLIC_ENDPOINT (the S3/MinIO base URL users can reach; falls back to S3_ENDPOINT when that is already public)");
@@ -157,21 +94,11 @@ export function createSendUserFileIssuer(cfg, deps) {
157
94
  const publicBase = cfg.publicEndpoint.replace(/\/+$/, "");
158
95
  const putBase = (cfg.sandboxPutEndpoint ?? cfg.publicEndpoint).replace(/\/+$/, "");
159
96
  const bucket = ttl === 0 ? cfg.publicBucket : cfg.privateBucket;
160
- // Same key algebra as issue() incl. the hashed tenant segment — prepare/finalize share ONE key, so the
161
- // segment is fixed here (the PUT capability is already scoped to it).
162
97
  const seg = scopeSegment(cfg.secretKey, scope);
163
98
  const scopeField = scope !== undefined && scope !== "" ? { scope } : {};
164
99
  const key = ttl === 0 ? `${seg}/${uuidv7()}/${name}` : `${cfg.privateKeyPrefix}${seg}/${uuidv7()}/${name}`;
165
- // 修3 staging two-phase: the sandbox-visible PUT capability is scoped to a STAGING key in the PRIVATE
166
- // bucket (never anonymous-readable, never handed out as a URL) — the final key above gets its bytes via
167
- // a server-side CopyObject only, so no /proc reader of the exec argv can ever rewrite what the user's
168
- // link points at. The staging key keeps the tenant segment (orphan attribution on manual cleanup).
169
100
  const stagingBucket = cfg.privateBucket;
170
101
  const stagingKey = `${cfg.privateKeyPrefix}staging/${seg}/${uuidv7()}`;
171
- // TTL 120s, not the uploader default 600: the URL rides the sandbox exec argv (visible to the sandbox's
172
- // own processes / possibly an apiserver audit log) — anyone who reads it gains nothing they don't already
173
- // have DURING the upload (the sandbox owns the file), and since 修3 the writable object is only the
174
- // staging key anyway. Keep the window as short as a slow 100MiB upload allows.
175
102
  const putUrl = presignS3Url({
176
103
  endpoint: putBase,
177
104
  bucket: stagingBucket,
@@ -196,21 +123,16 @@ export function createSendUserFileIssuer(cfg, deps) {
196
123
  ...(cfg.region !== undefined ? { region: cfg.region } : {}),
197
124
  expiresSec: 60,
198
125
  });
199
- // 修2: best-effort reclaim — any failure AFTER the object landed must delete it before throwing
200
- // (a transient HEAD 5xx used to strand an unledgered, undelivered orphan in the bucket). Returns the
201
- // TRUE outcome so error messages never claim "reclaimed" for a delete that itself failed (F2 posture).
202
126
  const reclaim = async (b, k) => {
203
127
  try {
204
128
  const r = await f(sign("DELETE", b, k), { method: "DELETE", ...bounded() });
205
- return r.ok || r.status === 404; // S3 DELETE is idempotent — a missing key IS reclaimed
129
+ return r.ok || r.status === 404;
206
130
  }
207
131
  catch {
208
132
  return false;
209
133
  }
210
134
  };
211
135
  const outcome = (ok, what) => (ok ? `${what} reclaimed` : `${what} reclaim FAILED (orphan; operator cleanup needed)`);
212
- // ① HEAD the STAGING object (internal endpoint, adapter creds): existence + a cheap early cap gate
213
- // (fail before copying a huge object). Advisory only for size — the authoritative reading is ④.
214
136
  let head;
215
137
  try {
216
138
  head = await f(sign("HEAD", stagingBucket, stagingKey), { method: "HEAD", ...bounded() });
@@ -222,7 +144,7 @@ export function createSendUserFileIssuer(cfg, deps) {
222
144
  if (head.status === 404)
223
145
  throw new Error("SendUserFile upload verification failed: object not found after upload (HEAD 404)");
224
146
  if (!head.ok) {
225
- const r = await reclaim(stagingBucket, stagingKey); // transient 5xx ≠ missing — the object may be live; reclaim it
147
+ const r = await reclaim(stagingBucket, stagingKey);
226
148
  throw new Error(`SendUserFile upload verification failed: store error (HEAD ${head.status}) — ${outcome(r, "staged object")}`);
227
149
  }
228
150
  const staged = Number.parseInt(head.headers.get("content-length") ?? "", 10);
@@ -234,9 +156,6 @@ export function createSendUserFileIssuer(cfg, deps) {
234
156
  await reclaim(stagingBucket, stagingKey);
235
157
  throw new Error(`file exceeds the ${maxBytes >= 1024 * 1024 ? `${Math.floor(maxBytes / (1024 * 1024))} MiB` : `${maxBytes} byte`} per-file cap (uploaded object was ${staged} bytes — deleted)`);
236
158
  }
237
- // ② Server-side CopyObject staging→final (internal endpoint, signed x-amz-copy-source): the ONLY
238
- // write the final key ever receives. NB: S3 can return 200 with an error BODY on slow copies — require
239
- // the CopyObjectResult marker, not just the status.
240
159
  const copy = presignS3CopyUrl({
241
160
  endpoint: cfg.endpoint,
242
161
  bucket,
@@ -257,15 +176,10 @@ export function createSendUserFileIssuer(cfg, deps) {
257
176
  }
258
177
  catch (e) {
259
178
  const r1 = await reclaim(stagingBucket, stagingKey);
260
- const r2 = await reclaim(bucket, key); // the copy may have half-landed — reclaim both sides
179
+ const r2 = await reclaim(bucket, key);
261
180
  throw new Error(`SendUserFile upload verification failed: staging promotion failed (${e instanceof Error ? e.message : String(e)}) — ${outcome(r1 && r2, "uploaded object")}`);
262
181
  }
263
- // ③ Staging object served its purpose — reclaim (best-effort: a leftover is a private-bucket blob
264
- // whose write capability expires in ≤120s; it can't become a public link).
265
182
  await reclaim(stagingBucket, stagingKey);
266
- // ④ Authoritative HEAD of the FINAL object — immutable to the sandbox, so THIS size is what the copy
267
- // pinned and what the card must carry (re-checked against the cap: a /proc reader could have re-PUT
268
- // staging between ① and ②, swapping in different bytes).
269
183
  let fin;
270
184
  try {
271
185
  fin = await f(sign("HEAD", bucket, key), { method: "HEAD", ...bounded() });
@@ -309,10 +223,6 @@ export function createSendUserFileIssuer(cfg, deps) {
309
223
  };
310
224
  return { putUrl, verifySize, finalize };
311
225
  };
312
- // Ledger reclaim (fail-loud record path): delete an already-uploaded object whose ledger record failed —
313
- // a public-track object with no ledger row would be unmanageable (no per-tenant list/revoke handle). Signed
314
- // DELETE against the INTERNAL endpoint (adapter creds, same posture as verifySize). S3 DELETE is idempotent
315
- // (204 on a missing key), so callers may retry safely.
316
226
  const deleteObject = async (bucket, key) => {
317
227
  if (deps?.remove)
318
228
  return deps.remove(bucket, key);