@sema-agent/server 1.314.0 → 1.316.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 (146) hide show
  1. package/dist/approval-hmac.d.ts +17 -0
  2. package/dist/approval-hmac.js +27 -0
  3. package/dist/config-center/apply-effective.d.ts +22 -0
  4. package/dist/config-center/apply-effective.js +283 -0
  5. package/dist/config-center/http-client.d.ts +23 -0
  6. package/dist/config-center/http-client.js +109 -0
  7. package/dist/config-center/restart-signal.d.ts +12 -0
  8. package/dist/config-center/restart-signal.js +70 -0
  9. package/dist/config-center/skills-mcp.d.ts +13 -0
  10. package/dist/config-center/skills-mcp.js +113 -0
  11. package/dist/config-center/types.d.ts +143 -0
  12. package/dist/config-center/types.js +2 -0
  13. package/dist/fleet/fleet-bus.js +92 -83
  14. package/dist/hooks/hook-runner.js +18 -9
  15. package/dist/http/idempotency.d.ts +1 -0
  16. package/dist/http/idempotency.js +3 -0
  17. package/dist/http/principal-gate.d.ts +12 -0
  18. package/dist/http/principal-gate.js +52 -0
  19. package/dist/http/route-ctx.d.ts +67 -0
  20. package/dist/http/route-ctx.js +2 -0
  21. package/dist/http/routes/attachments.d.ts +4 -0
  22. package/dist/http/routes/attachments.js +106 -0
  23. package/dist/http/routes/capabilities.d.ts +4 -0
  24. package/dist/http/routes/capabilities.js +111 -0
  25. package/dist/http/routes/images.d.ts +20 -0
  26. package/dist/http/routes/images.js +572 -0
  27. package/dist/http/routes/memory-policy.d.ts +4 -0
  28. package/dist/http/routes/memory-policy.js +105 -0
  29. package/dist/http/routes/observability.d.ts +4 -0
  30. package/dist/http/routes/observability.js +63 -0
  31. package/dist/http/routes/session-sync.d.ts +36 -0
  32. package/dist/http/routes/session-sync.js +513 -0
  33. package/dist/http/routes/sessions-list.d.ts +4 -0
  34. package/dist/http/routes/sessions-list.js +117 -0
  35. package/dist/http/routes/sessions.d.ts +17 -0
  36. package/dist/http/routes/sessions.js +746 -0
  37. package/dist/http/send.d.ts +6 -0
  38. package/dist/http/send.js +19 -0
  39. package/dist/http/server.d.ts +101 -76
  40. package/dist/http/server.js +129 -2410
  41. package/dist/http/sse-log.d.ts +25 -0
  42. package/dist/http/sse-log.js +59 -0
  43. package/dist/http/workspace-content.d.ts +3 -0
  44. package/dist/http/workspace-content.js +20 -0
  45. package/dist/index.d.ts +1 -1
  46. package/dist/leader/leader.js +5 -2
  47. package/dist/leader/wire.js +169 -165
  48. package/dist/main.js +462 -451
  49. package/dist/plugins/approval-store-sql.d.ts +52 -0
  50. package/dist/plugins/approval-store-sql.js +119 -0
  51. package/dist/plugins/breaker-state-sql.d.ts +29 -0
  52. package/dist/plugins/breaker-state-sql.js +104 -0
  53. package/dist/plugins/checkpoint-store-sql.d.ts +67 -0
  54. package/dist/plugins/checkpoint-store-sql.js +224 -0
  55. package/dist/plugins/file-snapshot-store-sql.d.ts +38 -0
  56. package/dist/plugins/file-snapshot-store-sql.js +210 -0
  57. package/dist/plugins/image-bake-store-sql.d.ts +53 -0
  58. package/dist/plugins/image-bake-store-sql.js +463 -0
  59. package/dist/plugins/image-index-sql.d.ts +33 -0
  60. package/dist/plugins/image-index-sql.js +328 -0
  61. package/dist/plugins/k8s-bg-scripts.d.ts +19 -0
  62. package/dist/plugins/k8s-bg-scripts.js +129 -0
  63. package/dist/plugins/k8s-exec-protocol.d.ts +20 -0
  64. package/dist/plugins/k8s-exec-protocol.js +87 -0
  65. package/dist/plugins/outcome-ledger-sql.d.ts +70 -0
  66. package/dist/plugins/outcome-ledger-sql.js +158 -0
  67. package/dist/plugins/pg-approval-store.d.ts +1 -27
  68. package/dist/plugins/pg-approval-store.js +1 -83
  69. package/dist/plugins/pg-breaker-state.d.ts +1 -19
  70. package/dist/plugins/pg-breaker-state.js +1 -88
  71. package/dist/plugins/pg-checkpoint-store.d.ts +1 -33
  72. package/dist/plugins/pg-checkpoint-store.js +1 -189
  73. package/dist/plugins/pg-cost-quota.js +3 -143
  74. package/dist/plugins/pg-file-snapshot-store.d.ts +1 -27
  75. package/dist/plugins/pg-file-snapshot-store.js +1 -190
  76. package/dist/plugins/pg-image-bake.d.ts +1 -40
  77. package/dist/plugins/pg-image-bake.js +1 -420
  78. package/dist/plugins/pg-image-index.d.ts +1 -21
  79. package/dist/plugins/pg-image-index.js +1 -217
  80. package/dist/plugins/pg-outcome-ledger.d.ts +2 -22
  81. package/dist/plugins/pg-outcome-ledger.js +1 -104
  82. package/dist/plugins/pg-rate-limiter.d.ts +2 -32
  83. package/dist/plugins/pg-rate-limiter.js +4 -147
  84. package/dist/plugins/pg-resume-anchor-store.d.ts +1 -13
  85. package/dist/plugins/pg-resume-anchor-store.js +1 -22
  86. package/dist/plugins/pg-run-store.d.ts +1 -75
  87. package/dist/plugins/pg-run-store.js +1 -287
  88. package/dist/plugins/pg-session-policy-store.d.ts +1 -11
  89. package/dist/plugins/pg-session-policy-store.js +1 -90
  90. package/dist/plugins/pg-tool-result-store.d.ts +1 -15
  91. package/dist/plugins/pg-tool-result-store.js +1 -52
  92. package/dist/plugins/pg-workflow-journal-store.d.ts +1 -20
  93. package/dist/plugins/pg-workflow-journal-store.js +1 -34
  94. package/dist/plugins/pg-workflow-run-store.d.ts +1 -55
  95. package/dist/plugins/pg-workflow-run-store.js +1 -220
  96. package/dist/plugins/remote-env-k8s.d.ts +5 -38
  97. package/dist/plugins/remote-env-k8s.js +7 -213
  98. package/dist/plugins/resume-anchor-store-sql.d.ts +23 -0
  99. package/dist/plugins/resume-anchor-store-sql.js +38 -0
  100. package/dist/plugins/run-store-sql.d.ts +89 -0
  101. package/dist/plugins/run-store-sql.js +397 -0
  102. package/dist/plugins/session-policy-store-sql.d.ts +22 -0
  103. package/dist/plugins/session-policy-store-sql.js +109 -0
  104. package/dist/plugins/sql-driver.d.ts +25 -0
  105. package/dist/plugins/sql-driver.js +59 -0
  106. package/dist/plugins/tidb-approval-store.d.ts +1 -39
  107. package/dist/plugins/tidb-approval-store.js +1 -71
  108. package/dist/plugins/tidb-breaker-state.d.ts +1 -17
  109. package/dist/plugins/tidb-breaker-state.js +1 -74
  110. package/dist/plugins/tidb-checkpoint-store.d.ts +1 -49
  111. package/dist/plugins/tidb-checkpoint-store.js +1 -191
  112. package/dist/plugins/tidb-file-snapshot-store.d.ts +1 -27
  113. package/dist/plugins/tidb-file-snapshot-store.js +1 -192
  114. package/dist/plugins/tidb-image-bake.d.ts +1 -38
  115. package/dist/plugins/tidb-image-bake.js +1 -348
  116. package/dist/plugins/tidb-image-index.d.ts +1 -19
  117. package/dist/plugins/tidb-image-index.js +1 -149
  118. package/dist/plugins/tidb-outcome-ledger.d.ts +1 -57
  119. package/dist/plugins/tidb-outcome-ledger.js +1 -97
  120. package/dist/plugins/tidb-resume-anchor-store.d.ts +1 -13
  121. package/dist/plugins/tidb-resume-anchor-store.js +1 -22
  122. package/dist/plugins/tidb-run-store.d.ts +1 -75
  123. package/dist/plugins/tidb-run-store.js +1 -306
  124. package/dist/plugins/tidb-session-policy-store.d.ts +1 -11
  125. package/dist/plugins/tidb-session-policy-store.js +1 -86
  126. package/dist/plugins/tidb-tool-result-store.d.ts +1 -13
  127. package/dist/plugins/tidb-tool-result-store.js +1 -38
  128. package/dist/plugins/tidb-workflow-journal-store.d.ts +1 -20
  129. package/dist/plugins/tidb-workflow-journal-store.js +1 -34
  130. package/dist/plugins/tidb-workflow-run-store.d.ts +1 -61
  131. package/dist/plugins/tidb-workflow-run-store.js +1 -231
  132. package/dist/plugins/tool-result-store-sql.d.ts +25 -0
  133. package/dist/plugins/tool-result-store-sql.js +69 -0
  134. package/dist/plugins/workflow-journal-store-sql.d.ts +30 -0
  135. package/dist/plugins/workflow-journal-store-sql.js +53 -0
  136. package/dist/plugins/workflow-run-store-sql.d.ts +85 -0
  137. package/dist/plugins/workflow-run-store-sql.js +299 -0
  138. package/dist/plugins/write-behind-counter.d.ts +14 -3
  139. package/dist/plugins/write-behind-counter.js +39 -12
  140. package/dist/principal-jwt.d.ts +44 -0
  141. package/dist/principal-jwt.js +95 -0
  142. package/dist/security.d.ts +2 -58
  143. package/dist/security.js +3 -118
  144. package/dist/sema-registry.d.ts +5 -200
  145. package/dist/sema-registry.js +4 -567
  146. package/package.json +1 -1
@@ -33,8 +33,28 @@ import { projectArtifacts } from "../trace/artifacts.js";
33
33
  import { composeSupervisorCost, infraCost, infraUsageFromEvents, hasInfraPricing } from "../observability/cost-taxonomy.js";
34
34
  import { redactSecrets, redactDeep, redactedPreview } from "../trace/redact.js";
35
35
  import { usageSummary, usageSeries, usageBreakdown } from "../usage-analytics.js";
36
- import { IdempotencyCache } from "./idempotency.js";
36
+ import { IdempotencyCache, scopedIdempotencyKey } from "./idempotency.js";
37
+ export { scopedIdempotencyKey };
38
+ import { streamSseLog, sleep } from "./sse-log.js";
39
+ import { handleCapabilities } from "./routes/capabilities.js";
40
+ import { handleObservability } from "./routes/observability.js";
41
+ import { handleMemoryPolicy } from "./routes/memory-policy.js";
42
+ import { handleSessionsList } from "./routes/sessions-list.js";
43
+ import { handleSessions, createSessionsLocal } from "./routes/sessions.js";
44
+ import { handleSessionSync, createSessionSyncLocal } from "./routes/session-sync.js";
45
+ import { handleAttachments } from "./routes/attachments.js";
46
+ import { handleImages, createImagesLocal, coarseStatusForState, errorCodeForExit, IMAGE_DIGEST_RE, IMAGE_PROFILE_RE, BAKE_ID_RE, BAKE_EVENTS_RE, BAKE_CANCEL_RE, BAKE_CLAIM_RE, BAKE_INGEST_RE } from "./routes/images.js";
47
+ export { coarseStatusForState, errorCodeForExit };
48
+ import { sendJson, sendError, msg, sseHeaders } from "./send.js";
49
+ import { headerStr, safeEqual, authorized, systemFor, bearerPresentedButUnverified, gatedPrincipal, explicitOperatorOk, isOperator, explicitOperator } from "./principal-gate.js";
50
+ export { explicitOperatorOk, isOperator, explicitOperator };
37
51
  import { pgHasUnstorable } from "../plugins/pg-safe-json.js";
52
+ export function flattenServiceDeps(deps) {
53
+ const { stores, coordinators, seams, observability, governance, deployment, knobs, ...flat } = deps;
54
+ if (!(stores || coordinators || seams || observability || governance || deployment || knobs))
55
+ return deps;
56
+ return { ...stores, ...coordinators, ...seams, ...observability, ...governance, ...deployment, ...knobs, ...flat };
57
+ }
38
58
  export const MAX_USER_SKILLS = 10;
39
59
  export const MAX_SKILL_CONTENT_CHARS = 1_048_576;
40
60
  export const MAX_SYSTEM_PROMPT_CHARS = 16_384;
@@ -66,42 +86,6 @@ export function validateUserSkills(skills) {
66
86
  }
67
87
  return null;
68
88
  }
69
- export function explicitOperatorOk(principal, operatorPrincipals) {
70
- return operatorPrincipals.length > 0 && principal !== undefined && operatorPrincipals.includes(principal);
71
- }
72
- export function coarseStatusForState(state) {
73
- if (state === "COMPLETE")
74
- return "done";
75
- if (state === "FAILED" || state === "CANCELLED")
76
- return "failed";
77
- return "running";
78
- }
79
- export function errorCodeForExit(exitCode, cancelled) {
80
- if (cancelled)
81
- return "cancelled";
82
- switch (exitCode) {
83
- case 9:
84
- return "band-conflict";
85
- case 10:
86
- return "duplicate";
87
- case 124:
88
- return "timeout";
89
- case 507:
90
- return "disk";
91
- default:
92
- return null;
93
- }
94
- }
95
- const RUNNER_ERROR_CODES = new Set(["band-conflict", "duplicate", "disk", "timeout", "cancelled"]);
96
- const SANDBOX_BASE_PROFILE = "sandbox-base";
97
- export function isOperator(principal, operatorPrincipals) {
98
- if (operatorPrincipals.length === 0)
99
- return true;
100
- return principal !== undefined && operatorPrincipals.includes(principal);
101
- }
102
- export function explicitOperator(principal, operatorPrincipals) {
103
- return operatorPrincipals.length > 0 && principal !== undefined && operatorPrincipals.includes(principal);
104
- }
105
89
  export function cascadeConfig(ladder, maxCostUsd) {
106
90
  return {
107
91
  ladder: ladder.map((model) => ({ model })),
@@ -112,9 +96,6 @@ export function clampVerifyRounds(v) {
112
96
  const n = typeof v === "number" && Number.isFinite(v) ? Math.floor(v) : 2;
113
97
  return Math.min(5, Math.max(1, n));
114
98
  }
115
- export function scopedIdempotencyKey(raw, source, principal) {
116
- return raw ? `${source ?? ""}\x1f${principal ?? ""}\x1f${raw}` : undefined;
117
- }
118
99
  export function isQuestionAnswer(v) {
119
100
  if (!v || typeof v !== "object" || Array.isArray(v))
120
101
  return false;
@@ -135,10 +116,6 @@ export function isQuestionAnswer(v) {
135
116
  const MAX_BODY = 8 * 1024 * 1024;
136
117
  const MAX_IMAGES_PER_REQUEST = 20;
137
118
  const MAX_IMAGE_BASE64_BYTES = 6 * 1024 * 1024;
138
- const SYNC_BLOB_MAX_BYTES = 64 * 1024 * 1024;
139
- const SYNC_NDJSON_LINE_MAX_BYTES = 8 * 1024 * 1024;
140
- const SYNC_IMPORT_BATCH = 500;
141
- const SHA256_HEX_RE = /^[0-9a-f]{64}$/;
142
119
  const RUN_ID_RE = /^\/v1\/runs\/([^/]+)(\/events)?$/;
143
120
  const RUN_CANCEL_RE = /^\/v1\/runs\/([^/]+)\/cancel$/;
144
121
  const RUN_STEER_RE = /^\/v1\/runs\/([^/]+)\/steer$/;
@@ -158,13 +135,6 @@ const TOOL_APPROVAL_RESPOND_RE = /^\/v1\/tool-approvals\/([^/]+)\/respond$/;
158
135
  const ASSISTANT_PREEMPT_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/preempt$/;
159
136
  const ASSISTANT_RESUME_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/resume$/;
160
137
  const ASSISTANT_PLAN_REVIEW_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/plan_review$/;
161
- const IMAGE_DIGEST_RE = /^\/v1\/images\/digests\/([^/]+)$/;
162
- const IMAGE_PROFILE_RE = /^\/v1\/images\/([^/]+)$/;
163
- const BAKE_ID_RE = /^\/v1\/images\/bakes\/([^/]+)$/;
164
- const BAKE_EVENTS_RE = /^\/v1\/images\/bakes\/([^/]+)\/events$/;
165
- const BAKE_CANCEL_RE = /^\/v1\/images\/bakes\/([^/]+)\/cancel$/;
166
- const BAKE_CLAIM_RE = /^\/v1\/images\/bakes\/([^/]+)\/claim$/;
167
- const BAKE_INGEST_RE = /^\/v1\/images\/bakes\/([^/]+)\/ingest$/;
168
138
  const TRACE_RE = /^\/v1\/tasks(?:\/([^/]+)(\/turns|\/stream|\/artifacts))?$/;
169
139
  const WORKFLOWS_RE = /^\/v1\/workflows(?:\/([^/]+)(\/stream|\/journal)?)?$/;
170
140
  const WORKFLOW_AGENT_STEER_RE = /^\/v1\/workflows\/([^/]+)\/agents\/([^/]+)\/steer$/;
@@ -178,37 +148,28 @@ const HAND_TOOL_NAMES = Object.keys(HAND_TOOL_EFFECTS);
178
148
  function isCheckpointReopenedFailure(r) {
179
149
  return r.status === "failed" && (r.errorCode === "resume.env_failed" || r.errorCode === "resume.tool_unavailable");
180
150
  }
181
- function gatedPrincipal(req, config) {
182
- if (bearerPresentedButUnverified(req, config))
183
- return undefined;
184
- return verifiedPrincipal(req, config);
185
- }
186
- function bearerPresentedButUnverified(req, config) {
187
- const h = req.headers.authorization;
188
- if (typeof h !== "string" || !/^(Bearer|token) /i.test(h))
189
- return false;
190
- if (ssoVerifiedPrincipal(req))
191
- return false;
192
- if (systemFor(req, config) !== undefined)
193
- return false;
194
- if (config.traceToken && authorized(req, config.traceToken))
195
- return false;
196
- return true;
197
- }
198
- export function createHttpServer(deps) {
151
+ const ROUTE_DOMAINS = [
152
+ handleCapabilities,
153
+ handleImages,
154
+ handleObservability,
155
+ handleMemoryPolicy,
156
+ handleSessionsList,
157
+ handleSessions,
158
+ handleSessionSync,
159
+ handleAttachments,
160
+ ];
161
+ void ROUTE_DOMAINS;
162
+ export function createHttpServer(rawDeps) {
163
+ const deps = flattenServiceDeps(rawDeps);
199
164
  const idemCache = new IdempotencyCache();
200
- let sseConnections = 0;
201
- let sseOwnerLookups = 0;
202
- const bakeSubmitLimiter = new RateLimiter(deps.config.imageBakes?.submitRateMax ?? 0, (deps.config.imageBakes?.submitRateWindowSec ?? 60) * 1000);
203
165
  const inflightRuns = new Map();
204
166
  const cancelledViaVerb = new Set();
205
167
  const preemptableRuns = new Map();
206
168
  const steerableRuns = new Map();
207
169
  const wakeParkMints = new Map();
208
- let uncountedBillableInflight = 0;
209
- let admittedInflight = 0;
170
+ const counters = { uncountedBillableInflight: 0, admittedInflight: 0 };
210
171
  if (deps.drainState)
211
- deps.drainState.inflight = () => new Set([...inflightRuns.keys(), ...steerableRuns.keys()]).size + uncountedBillableInflight + admittedInflight + (deps.leaderEndpoint?.inflight?.() ?? 0);
172
+ deps.drainState.inflight = () => new Set([...inflightRuns.keys(), ...steerableRuns.keys()]).size + counters.uncountedBillableInflight + counters.admittedInflight + (deps.leaderEndpoint?.inflight?.() ?? 0);
212
173
  let lastBillableActivityAt = performance.now();
213
174
  const noteActivity = (url) => {
214
175
  if (url === "/health" || url.startsWith("/metrics"))
@@ -254,22 +215,6 @@ export function createHttpServer(deps) {
254
215
  }
255
216
  };
256
217
  }
257
- const mcpStatusCache = new Map();
258
- const MCP_STATUS_TTL_MS = 5_000;
259
- const MCP_MATERIALIZE_TIMEOUT_MS = 10_000;
260
- const pendingStagings = new Map();
261
- const importLeases = new Map();
262
- const touchImportLease = (sessionId) => {
263
- const cur = importLeases.get(sessionId);
264
- if (cur)
265
- importLeases.set(sessionId, { ...cur, at: Date.now() });
266
- };
267
- const cleanupStaging = (stagingId) => {
268
- const p = pendingStagings.get(stagingId);
269
- if (p)
270
- importLeases.delete(p.realSessionId);
271
- pendingStagings.delete(stagingId);
272
- };
273
218
  const server = http.createServer((req, res) => {
274
219
  void handle(req, res).catch((err) => {
275
220
  if (err instanceof HttpError) {
@@ -286,6 +231,12 @@ export function createHttpServer(deps) {
286
231
  res.end();
287
232
  });
288
233
  });
234
+ const routeCtxBase = {
235
+ deps,
236
+ registry: { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters },
237
+ helpers: { readJson, readRawBody, rateLimited, quotaExceeded, leaseDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
238
+ local: { images: createImagesLocal(deps), sessions: createSessionsLocal(), sessionSync: createSessionSyncLocal() },
239
+ };
289
240
  async function handle(req, res) {
290
241
  const startedAt = Date.now();
291
242
  const method = req.method ?? "GET";
@@ -293,16 +244,16 @@ export function createHttpServer(deps) {
293
244
  noteActivity(url);
294
245
  const admitted = method !== "OPTIONS" && url !== "/health" && !url.startsWith("/metrics");
295
246
  if (admitted)
296
- admittedInflight++;
247
+ counters.admittedInflight++;
297
248
  let logged = false;
298
- let streamTaskId;
299
- let streamDetached = false;
249
+ const reqState = { streamTaskId: undefined, streamDetached: false, source: null };
250
+ const ctx = { deps: routeCtxBase.deps, registry: routeCtxBase.registry, helpers: routeCtxBase.helpers, local: routeCtxBase.local, req: reqState };
300
251
  const record = (viaClose) => {
301
252
  if (logged)
302
253
  return;
303
254
  logged = true;
304
255
  if (admitted)
305
- admittedInflight--;
256
+ counters.admittedInflight--;
306
257
  const route = routeLabel(method, url);
307
258
  const seconds = (Date.now() - startedAt) / 1000;
308
259
  const aborted = viaClose && !res.writableEnded;
@@ -310,7 +261,7 @@ export function createHttpServer(deps) {
310
261
  deps.metrics?.inc("http_requests_total", { route, method, status });
311
262
  deps.metrics?.observe("http_request_duration_seconds", seconds, { route });
312
263
  const log = route === "/health" || route === "/metrics" ? deps.logger?.debug : deps.logger?.info;
313
- log?.call(deps.logger, "request", { method, route, status, ms: Date.now() - startedAt, ...(aborted ? { aborted: true, ...(streamTaskId ? { taskId: streamTaskId } : {}), ...(streamDetached ? { detached: true } : {}) } : {}) });
264
+ log?.call(deps.logger, "request", { method, route, status, ms: Date.now() - startedAt, ...(aborted ? { aborted: true, ...(reqState.streamTaskId ? { taskId: reqState.streamTaskId } : {}), ...(reqState.streamDetached ? { detached: true } : {}) } : {}) });
314
265
  };
315
266
  res.on("finish", () => record(false));
316
267
  res.on("close", () => record(true));
@@ -623,6 +574,7 @@ export function createHttpServer(deps) {
623
574
  source = sys;
624
575
  }
625
576
  }
577
+ reqState.source = source;
626
578
  if (req.method === "POST" &&
627
579
  !anyServiceAuth &&
628
580
  !deps.config.allowUnauthedWrites &&
@@ -651,103 +603,8 @@ export function createHttpServer(deps) {
651
603
  });
652
604
  return;
653
605
  }
654
- if (req.method === "GET" && url.startsWith("/v1/capabilities/scenarios/")) {
655
- const name = url.slice("/v1/capabilities/scenarios/".length).split("?")[0];
656
- const detail = deps.scenarioDetails?.[name];
657
- if (!detail) {
658
- sendError(res, 404, "scenario_not_found", "scenario not found");
659
- return;
660
- }
661
- sendJson(res, 200, detail);
662
- return;
663
- }
664
- if (req.method === "GET" && url === "/v1/capabilities") {
665
- sendJson(res, 200, {
666
- ...deps.capabilities,
667
- asyncRuns: Boolean(deps.runStore),
668
- artifacts: Boolean(deps.runStore),
669
- approvals: Boolean(deps.checkpointStore),
670
- sessions: Boolean(deps.sessionAudit),
671
- sessionEvents: Boolean(deps.sessionWatch && deps.sessionStorage?.getLeafId && deps.sessionStorage?.ownerOf),
672
- fleet: deps.fleetBus
673
- ? { stream: true, sessionScope: true, observe: true, resume: "snapshot", bgNotifyFailClosed: true, steerPriority: "advisory" }
674
- : false,
675
- workspace: (() => {
676
- const wfs = deps.fileSnapshotStore;
677
- return wfs && typeof wfs.listKeys === "function" && typeof wfs.exportManifest === "function" && typeof wfs.getBlob === "function" && Boolean(deps.sessionStorage?.ownerOf)
678
- ? { browse: true, archive: true, maxFileBytes: deps.config.workspaceFileMaxBytes ?? 8 * 1024 * 1024 }
679
- : false;
680
- })(),
681
- leader: Boolean(deps.leaderEndpoint),
682
- memory: false,
683
- memoryWrite: false,
684
- policy: true,
685
- usage: Boolean(deps.costQuota),
686
- permissionModeWrite: false,
687
- modelSelection: true,
688
- effortSelection: true,
689
- toolOutput: true,
690
- messageIdentity: true,
691
- forwardSubagentEvents: true,
692
- scratchpadDir: true,
693
- subagentSteer: Boolean(deps.subagentSteerRegistry) && Boolean(deps.runStore),
694
- subagentResume: Boolean(deps.subagentSteerRegistry) && Boolean(deps.runStore),
695
- taskSettings: { permissions: true, permissionMode: true, model: true, outputStyle: true, env: false, hooks: deps.config.requirePrincipal !== true },
696
- appendSystemPrompt: true,
697
- compactionModel: true,
698
- subagentOutput: Boolean(deps.subagentTaskOutput) && Boolean(deps.runStore),
699
- subagentStream: Boolean(deps.subagentTaskOutput) && Boolean(deps.runStore),
700
- taskHandles: Boolean(deps.taskHandleOutput) && Boolean(deps.taskHandleStop) && Boolean(deps.runStore),
701
- taskAgents: deps.config.requirePrincipal !== true,
702
- retainBackgroundProcesses: deps.config.requirePrincipal !== true,
703
- interactiveTools: true,
704
- projectContext: cwdHonored(deps.config),
705
- mcpInjection: mcpInjectionHonored(deps.config),
706
- modelUsage: Boolean(deps.runStore && deps.modelUsage),
707
- sessionInit: true,
708
- mcp: true,
709
- mcpElicitation: Boolean(deps.elicitation),
710
- askUserQuestion: Boolean(deps.question),
711
- toolApproval: Boolean(deps.toolApproval),
712
- sideQuery: true,
713
- sessionList: Boolean(deps.sessionStorage?.listSessions ?? deps.runStore?.listSessions),
714
- sessionSearch: Boolean(deps.sessionStorage?.listSessions ?? deps.runStore?.listSessions),
715
- sessionFork: Boolean(deps.sessionStorage?.fork),
716
- sessionDelete: Boolean(deps.purgeSession),
717
- resumeAt: Boolean(deps.resumeAnchorStore && deps.sessionStorage?.getLeafId),
718
- promptSuggestions: true,
719
- sessionPolicy: Boolean(deps.sessionPolicyStore),
720
- rewindFiles: Boolean(deps.fileSnapshotStore),
721
- rewindFilesTo: Boolean(deps.fileSnapshotStore && deps.resumeAnchorStore && deps.sessionStorage?.getLeafId),
722
- scheduler: Boolean(deps.config.schedulerEnabled && deps.config.requirePrincipal !== true && deps.config.remoteExec?.provider === "host"),
723
- sessionSync: Boolean(deps.backend && deps.sessionStorage?.exportEntries && deps.fileSnapshotStore),
724
- sendUserFile: Boolean(deps.sendUserFile && deps.config.sendUserFile?.publicEndpoint),
725
- s3PublicEndpoint: deps.config.sendUserFile?.publicEndpoint ?? null,
726
- sendUserFileLedger: Boolean(deps.sendFileLedger),
727
- workflows: deps.workflowsCapable ?? Boolean(deps.workflowRunStore),
728
- workflowsList: Boolean(deps.workflowRunStore),
729
- manualCompact: Boolean(deps.runStore),
730
- });
731
- return;
732
- }
733
- if (req.method === "GET" && url === "/v1/models") {
734
- const models = Object.entries(deps.config.models)
735
- .filter(([name]) => name !== "default")
736
- .map(([name, m]) => ({
737
- name,
738
- id: m.id,
739
- provider: m.provider,
740
- reasoning: m.reasoning,
741
- vision: Array.isArray(m.input) ? m.input.includes("image") : false,
742
- ...(m.contextWindow ? { contextWindow: m.contextWindow } : {}),
743
- ...(m.maxTokens ? { maxOutputTokens: m.maxTokens } : {}),
744
- ...(m.reasoning ? { supportedEffortLevels: [...DEFAULT_EFFORT_LEVELS] } : {}),
745
- }));
746
- const nonAlias = Object.entries(deps.config.models).filter(([n]) => n !== "default");
747
- const defaultName = (nonAlias.find(([, m]) => m === deps.config.model) ?? nonAlias.find(([, m]) => m.id === deps.config.model.id))?.[0];
748
- sendJson(res, 200, { models, default: deps.config.model.id, defaultModel: { id: deps.config.model.id, ...(defaultName ? { name: defaultName } : {}) } });
606
+ if (await handleCapabilities(req, res, url, ctx))
749
607
  return;
750
- }
751
608
  if (req.method === "POST" && url === "/v1/side-query") {
752
609
  const ac = new AbortController();
753
610
  const onClose = () => ac.abort();
@@ -863,9 +720,9 @@ export function createHttpServer(deps) {
863
720
  sendJson(res, 400, { error: "x-detach-on-disconnect requires a durable run (a run store + sessionId) — without one the detached result would be unqueryable" });
864
721
  return;
865
722
  }
866
- streamTaskId = earlyDurableTid;
723
+ reqState.streamTaskId = earlyDurableTid;
867
724
  let usageKey;
868
- streamDetached = detachOnDisconnect;
725
+ reqState.streamDetached = detachOnDisconnect;
869
726
  sseHeaders(res, earlyDurableTid ? { "x-task-id": earlyDurableTid } : undefined);
870
727
  if (earlyDurableTid) {
871
728
  res.write(`event: meta\ndata: ${JSON.stringify({ type: "meta", taskId: earlyDurableTid, ...(prepared.spec.sessionId ? { sessionId: prepared.spec.sessionId } : {}) })}\n\n`);
@@ -900,7 +757,7 @@ export function createHttpServer(deps) {
900
757
  }, 15_000);
901
758
  let ranLive = false;
902
759
  let resp;
903
- uncountedBillableInflight++;
760
+ counters.uncountedBillableInflight++;
904
761
  try {
905
762
  resp = await idemCache.run(idemKey, async () => {
906
763
  ranLive = true;
@@ -1322,7 +1179,7 @@ export function createHttpServer(deps) {
1322
1179
  throw e;
1323
1180
  }
1324
1181
  finally {
1325
- uncountedBillableInflight--;
1182
+ counters.uncountedBillableInflight--;
1326
1183
  clearInterval(hb);
1327
1184
  if (usageKey)
1328
1185
  deps.modelUsage?.clear(usageKey);
@@ -1376,7 +1233,7 @@ export function createHttpServer(deps) {
1376
1233
  const specWithSignal = { ...prepared.spec, signal: syncCancelCtrl.signal };
1377
1234
  let result;
1378
1235
  let cancelVerbLabel = false;
1379
- uncountedBillableInflight++;
1236
+ counters.uncountedBillableInflight++;
1380
1237
  try {
1381
1238
  result = await withPrincipal(principal, () => prepared.verify
1382
1239
  ? runWithVerification(deps.runner, specWithSignal, prepared.verify)
@@ -1385,7 +1242,7 @@ export function createHttpServer(deps) {
1385
1242
  : deps.runner.runTask(specWithSignal));
1386
1243
  }
1387
1244
  finally {
1388
- uncountedBillableInflight--;
1245
+ counters.uncountedBillableInflight--;
1389
1246
  if (durableHeartbeat)
1390
1247
  clearInterval(durableHeartbeat);
1391
1248
  if (durableTaskId && inflightRuns.get(durableTaskId) === syncCancelCtrl) {
@@ -2426,323 +2283,8 @@ export function createHttpServer(deps) {
2426
2283
  }
2427
2284
  }
2428
2285
  }
2429
- if (deps.imageBakes && url.startsWith("/v1/images/bakes")) {
2430
- const bakes = deps.imageBakes;
2431
- const idx = deps.imageIndex;
2432
- const principal = gatedPrincipal(req, deps.config);
2433
- const operators = deps.config.operatorPrincipals;
2434
- const explicitOperator = explicitOperatorOk(principal, operators);
2435
- const cfg = deps.config.imageBakes;
2436
- const isRunner = source !== undefined && source !== null && source === cfg.runnerPrincipal;
2437
- if (req.method === "POST" && url === "/v1/images/bakes") {
2438
- if (!explicitOperator) {
2439
- sendJson(res, 403, { error: "operator only" });
2440
- return;
2441
- }
2442
- const rl = bakeSubmitLimiter.check(`bake:${principal}`);
2443
- if (!rl.allowed) {
2444
- res.setHeader("retry-after", String(rl.retryAfterSec));
2445
- sendJson(res, 429, { error: "bake submission rate exceeded", retryAfterSec: rl.retryAfterSec });
2446
- return;
2447
- }
2448
- const body = (await readJson(req));
2449
- const v = validateBake(body);
2450
- if (!v.ok) {
2451
- sendJson(res, v.status, { error: v.error, ...(v.conflictBands ? { errorCode: "band-conflict", conflictBands: v.conflictBands } : {}) });
2452
- return;
2453
- }
2454
- let effectiveBaseRef;
2455
- if (v.baseRef) {
2456
- const baseEntry = idx ? await idx.getByDigest(v.baseRef) : null;
2457
- if (!baseEntry || baseEntry.status !== "published" || baseEntry.profile !== SANDBOX_BASE_PROFILE) {
2458
- sendJson(res, 400, { error: "baseRef is not a known published sandbox-base digest" });
2459
- return;
2460
- }
2461
- effectiveBaseRef = v.baseRef;
2462
- }
2463
- else {
2464
- effectiveBaseRef = cfg.defaultBaseRef ?? (idx ? (await idx.latestPublished(SANDBOX_BASE_PROFILE, { operator: true }))?.digest ?? null : null);
2465
- }
2466
- if (!v.dryRun && !effectiveBaseRef) {
2467
- sendJson(res, 503, { error: "no sandbox-base available to build from (set SANDBOX_BASE_REF or publish a sandbox-base image)" });
2468
- return;
2469
- }
2470
- const argv = buildBakeArgv(v, { baseRef: effectiveBaseRef, cacheBase: cfg.cacheBase });
2471
- const rawIdem = headerStr(req.headers["idempotency-key"]) ?? (typeof body.idempotencyKey === "string" ? body.idempotencyKey : undefined);
2472
- const idemKey = rawIdem ? scopedIdempotencyKey(rawIdem, source, principal) ?? null : null;
2473
- const createInput = {
2474
- profile: v.profile,
2475
- bands: v.bands,
2476
- baseRef: effectiveBaseRef,
2477
- push: v.push,
2478
- dryRun: v.dryRun,
2479
- logs: v.logs,
2480
- argv,
2481
- idemKey,
2482
- requestedBy: principal ?? null,
2483
- };
2484
- if (!v.dryRun) {
2485
- const sameArgv = await bakes.findActiveByArgv(argv);
2486
- if (sameArgv) {
2487
- sendJson(res, 202, { bakeId: sameArgv.bakeId, eventsUrl: `/v1/images/bakes/${sameArgv.bakeId}/events`, status: sameArgv.status, state: sameArgv.state });
2488
- return;
2489
- }
2490
- const r = await bakes.createBakeIfIdle(createInput);
2491
- if (!r.created) {
2492
- const sameNow = await bakes.findActiveByArgv(argv);
2493
- if (sameNow) {
2494
- sendJson(res, 202, { bakeId: sameNow.bakeId, eventsUrl: `/v1/images/bakes/${sameNow.bakeId}/events`, status: sameNow.status, state: sameNow.state });
2495
- return;
2496
- }
2497
- const busy = await bakes.findActiveAny();
2498
- sendJson(res, 409, { error: "a bake is already in progress", ...(busy ? { activeBakeId: busy.bakeId, eventsUrl: `/v1/images/bakes/${busy.bakeId}/events` } : {}) });
2499
- return;
2500
- }
2501
- sendJson(res, 202, { bakeId: r.bake.bakeId, eventsUrl: `/v1/images/bakes/${r.bake.bakeId}/events`, status: r.bake.status, state: r.bake.state });
2502
- return;
2503
- }
2504
- const { bake } = await bakes.createBake(createInput);
2505
- sendJson(res, 202, { bakeId: bake.bakeId, eventsUrl: `/v1/images/bakes/${bake.bakeId}/events`, status: bake.status, state: bake.state });
2506
- return;
2507
- }
2508
- const evMatch = req.method === "GET" ? BAKE_EVENTS_RE.exec(url) : null;
2509
- if (evMatch) {
2510
- if (!explicitOperator) {
2511
- sendJson(res, 403, { error: "operator only" });
2512
- return;
2513
- }
2514
- const bakeId = decodeURIComponent(evMatch[1]);
2515
- const bake = await bakes.getBake(bakeId);
2516
- if (!bake) {
2517
- sendJson(res, 404, { error: "bake not found" });
2518
- return;
2519
- }
2520
- await streamBakeEvents(req, res, bakes, bakeId, cfg.staleMs);
2521
- return;
2522
- }
2523
- const cancelMatch = req.method === "POST" ? BAKE_CANCEL_RE.exec(url) : null;
2524
- if (cancelMatch) {
2525
- if (!explicitOperator) {
2526
- sendJson(res, 403, { error: "operator only" });
2527
- return;
2528
- }
2529
- const bakeId = decodeURIComponent(cancelMatch[1]);
2530
- const bake = await bakes.getBake(bakeId);
2531
- if (!bake) {
2532
- sendJson(res, 404, { error: "bake not found" });
2533
- return;
2534
- }
2535
- const flagged = await bakes.requestCancel(bakeId);
2536
- sendJson(res, 202, { bakeId, cancelRequested: true, note: flagged ? "cancel requested — the runner kills the build at the next heartbeat" : "bake already terminal — cancel is a no-op" });
2537
- return;
2538
- }
2539
- if (req.method === "POST" && url === "/v1/images/bakes/claim") {
2540
- if (!isRunner) {
2541
- sendJson(res, 403, { error: "bake-runner only" });
2542
- return;
2543
- }
2544
- const body = (await readJson(req));
2545
- const runnerId = typeof body.runnerId === "string" && body.runnerId.length > 0 ? body.runnerId : (principal ?? source ?? cfg.runnerPrincipal);
2546
- let claimed = null;
2547
- for (let attempt = 0; attempt < 4 && !claimed; attempt++) {
2548
- const nextId = await bakes.findNextQueuedId();
2549
- if (!nextId)
2550
- break;
2551
- claimed = await bakes.claimBake(nextId, runnerId, cfg.leaseMs);
2552
- }
2553
- if (!claimed) {
2554
- res.writeHead(204).end();
2555
- return;
2556
- }
2557
- sendJson(res, 200, claimResponse(claimed));
2558
- return;
2559
- }
2560
- const claimMatch = req.method === "POST" ? BAKE_CLAIM_RE.exec(url) : null;
2561
- if (claimMatch) {
2562
- if (!isRunner) {
2563
- sendJson(res, 403, { error: "bake-runner only" });
2564
- return;
2565
- }
2566
- const bakeId = decodeURIComponent(claimMatch[1]);
2567
- const body = (await readJson(req));
2568
- const runnerId = typeof body.runnerId === "string" && body.runnerId.length > 0 ? body.runnerId : (principal ?? source ?? cfg.runnerPrincipal);
2569
- const claimed = await bakes.claimBake(bakeId, runnerId, cfg.leaseMs);
2570
- if (!claimed) {
2571
- sendJson(res, 409, { error: "bake not claimable (already claimed or the build lease is held)" });
2572
- return;
2573
- }
2574
- sendJson(res, 200, claimResponse(claimed));
2575
- return;
2576
- }
2577
- const ingestMatch = req.method === "POST" ? BAKE_INGEST_RE.exec(url) : null;
2578
- if (ingestMatch) {
2579
- if (!isRunner) {
2580
- sendJson(res, 403, { error: "bake-runner only" });
2581
- return;
2582
- }
2583
- const bakeId = decodeURIComponent(ingestMatch[1]);
2584
- const bake = await bakes.getBake(bakeId);
2585
- if (!bake) {
2586
- sendJson(res, 404, { error: "bake not found" });
2587
- return;
2588
- }
2589
- const presented = headerStr(req.headers["x-bake-ingest-secret"]);
2590
- if (!bake.ingestSecret || !presented || !safeEqual(presented, bake.ingestSecret)) {
2591
- sendJson(res, 403, { error: "invalid ingest secret" });
2592
- return;
2593
- }
2594
- const line = (await readJson(req));
2595
- if (line.event === "heartbeat") {
2596
- const fresh = await bakes.getBake(bakeId);
2597
- if (!fresh || fresh.status !== "running") {
2598
- sendJson(res, 409, { accepted: false, leaseValid: false, cancelRequested: fresh?.cancelRequested ?? false });
2599
- return;
2600
- }
2601
- const ok = bake.runnerId ? await bakes.heartbeatLease(bakeId, bake.runnerId, cfg.leaseMs) : false;
2602
- if (!ok) {
2603
- sendJson(res, 409, { accepted: false, leaseValid: false, cancelRequested: fresh.cancelRequested });
2604
- return;
2605
- }
2606
- sendJson(res, 200, { accepted: true, leaseValid: true, cancelRequested: fresh.cancelRequested });
2607
- return;
2608
- }
2609
- const fresh = await bakes.getBake(bakeId);
2610
- if (!fresh || fresh.status !== "running") {
2611
- sendJson(res, 409, { accepted: false, leaseValid: false, cancelRequested: fresh?.cancelRequested ?? false });
2612
- return;
2613
- }
2614
- if (fresh.runnerId) {
2615
- await bakes.heartbeatLease(bakeId, fresh.runnerId, cfg.leaseMs).catch(() => undefined);
2616
- }
2617
- const result = await ingestBakeLine(bakes, idx, fresh, line, cfg.registry);
2618
- const after = await bakes.getBake(bakeId);
2619
- sendJson(res, result.status, { ...result.body, cancelRequested: after?.cancelRequested ?? false, leaseValid: true });
2620
- return;
2621
- }
2622
- const idMatch = req.method === "GET" ? BAKE_ID_RE.exec(url) : null;
2623
- if (idMatch) {
2624
- if (!explicitOperator) {
2625
- sendJson(res, 403, { error: "operator only" });
2626
- return;
2627
- }
2628
- const bakeId = decodeURIComponent(idMatch[1]);
2629
- const bake = await bakes.getBake(bakeId);
2630
- if (!bake) {
2631
- sendJson(res, 404, { error: "bake not found" });
2632
- return;
2633
- }
2634
- sendJson(res, 200, { bake: bakeView(bake) });
2635
- return;
2636
- }
2637
- sendJson(res, 404, { error: "not found" });
2286
+ if (await handleImages(req, res, url, ctx))
2638
2287
  return;
2639
- }
2640
- if (deps.imageIndex && url.startsWith("/v1/images")) {
2641
- const idx = deps.imageIndex;
2642
- const principal = gatedPrincipal(req, deps.config);
2643
- const operator = explicitOperator(principal, deps.config.operatorPrincipals);
2644
- const viewer = { operator, tenantId: principal ?? null };
2645
- const q = new URL(req.url ?? "", "http://x").searchParams;
2646
- if (req.method === "GET" && url === "/v1/images") {
2647
- const limRaw = q.get("limit");
2648
- const { entries, nextCursor } = await idx.list({
2649
- ...viewer,
2650
- profile: q.get("profile") ?? undefined,
2651
- status: q.get("status") ?? undefined,
2652
- capability: q.get("capability") ?? undefined,
2653
- latestOnly: q.get("latest") === "true",
2654
- limit: limRaw ? Number(limRaw) : undefined,
2655
- cursor: q.get("cursor") ?? undefined,
2656
- });
2657
- sendJson(res, 200, { images: entries, nextCursor });
2658
- return;
2659
- }
2660
- if (req.method === "POST" && url === "/v1/images/select") {
2661
- const body = (await readJson(req));
2662
- const profile = typeof body.profile === "string" ? body.profile : "";
2663
- if (!profile) {
2664
- sendJson(res, 400, { error: "profile is required" });
2665
- return;
2666
- }
2667
- const entry = await idx.latestPublished(profile, viewer);
2668
- if (!entry) {
2669
- sendJson(res, 404, { error: `no published image for profile '${profile}' visible to this principal` });
2670
- return;
2671
- }
2672
- const needed = Array.isArray(body.capabilitiesNeeded)
2673
- ? body.capabilitiesNeeded.filter((c) => typeof c === "string")
2674
- : [];
2675
- const caps = entry.capabilities;
2676
- const missing = needed.filter((c) => !caps[c]);
2677
- if (missing.length > 0) {
2678
- sendJson(res, 409, { error: "capabilities not satisfied by the image", missing });
2679
- return;
2680
- }
2681
- sendJson(res, 200, {
2682
- profile: entry.profile,
2683
- digest: entry.digest,
2684
- repo: entry.repo,
2685
- ref: `${entry.repo}@${entry.digest}`,
2686
- podContract: entry.podContract,
2687
- capabilities: entry.capabilities,
2688
- manifestSha: entry.manifestSha,
2689
- });
2690
- return;
2691
- }
2692
- if (req.method === "POST" && url === "/v1/images/register") {
2693
- if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
2694
- sendJson(res, 403, { error: "operator only" });
2695
- return;
2696
- }
2697
- const b = (await readJson(req));
2698
- if (typeof b.profile !== "string" || typeof b.repo !== "string" || typeof b.digest !== "string") {
2699
- sendJson(res, 400, { error: "profile, repo, digest are required" });
2700
- return;
2701
- }
2702
- const id = await idx.registerBuilding({
2703
- profile: b.profile,
2704
- bands: Array.isArray(b.bands) ? b.bands : [],
2705
- repo: b.repo,
2706
- tag: typeof b.tag === "string" ? b.tag : "",
2707
- digest: b.digest,
2708
- toolchainVersions: b.toolchainVersions ?? {},
2709
- capabilities: b.capabilities ?? {},
2710
- podContract: b.podContract ?? {},
2711
- sizeBytes: typeof b.sizeBytes === "number" ? b.sizeBytes : null,
2712
- status: (typeof b.status === "string" ? b.status : "building"),
2713
- visibility: b.visibility === "tenant" ? "tenant" : "public",
2714
- tenantId: typeof b.tenantId === "string" ? b.tenantId : null,
2715
- manifestSha: typeof b.manifestSha === "string" ? b.manifestSha : null,
2716
- recipeGitSha: typeof b.recipeGitSha === "string" ? b.recipeGitSha : null,
2717
- generatorVersion: typeof b.generatorVersion === "string" ? b.generatorVersion : null,
2718
- buildDate: typeof b.buildDate === "string" ? b.buildDate : null,
2719
- supersedes: typeof b.supersedes === "string" ? b.supersedes : null,
2720
- signed: b.signed === true,
2721
- });
2722
- sendJson(res, 200, { id });
2723
- return;
2724
- }
2725
- const digMatch = req.method === "GET" ? IMAGE_DIGEST_RE.exec(url) : null;
2726
- if (digMatch) {
2727
- const entry = await idx.getByDigest(decodeURIComponent(digMatch[1]));
2728
- if (!entry || (!viewer.operator && entry.visibility === "tenant" && entry.tenantId !== viewer.tenantId)) {
2729
- sendJson(res, 404, { error: "not found" });
2730
- return;
2731
- }
2732
- sendJson(res, 200, { image: entry });
2733
- return;
2734
- }
2735
- const profMatch = req.method === "GET" ? IMAGE_PROFILE_RE.exec(url) : null;
2736
- if (profMatch) {
2737
- const entry = await idx.latestPublished(decodeURIComponent(profMatch[1]), viewer);
2738
- if (!entry) {
2739
- sendJson(res, 404, { error: "not found" });
2740
- return;
2741
- }
2742
- sendJson(res, 200, { image: entry });
2743
- return;
2744
- }
2745
- }
2746
2288
  if (deps.checkpointStore && (url.startsWith("/v1/approvals") || url.startsWith("/v1/assistant"))) {
2747
2289
  const cs = deps.checkpointStore;
2748
2290
  const principal = gatedPrincipal(req, deps.config);
@@ -3183,1593 +2725,112 @@ export function createHttpServer(deps) {
3183
2725
  sendJson(res, 404, { error: "not found" });
3184
2726
  return;
3185
2727
  }
3186
- if (url === "/v1/outcomes" && req.method === "GET") {
3187
- if (deps.config.requirePrincipal) {
3188
- const principal = gatedPrincipal(req, deps.config);
3189
- if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
3190
- sendJson(res, 403, { error: "outcomes view is operator-only on a multi-tenant worker" });
3191
- return;
3192
- }
3193
- }
3194
- if (!deps.outcomeSink?.summary) {
3195
- sendJson(res, 501, { error: "no queryable outcome ledger on this worker (File sink — read the JSONL dataset directly)" });
3196
- return;
3197
- }
3198
- const q = new URL(req.url ?? "/", "http://localhost").searchParams;
3199
- const sig = q.get("signature") ?? undefined;
3200
- try {
3201
- const rows = await deps.outcomeSink.summary({ ...(sig ? { taskSignature: sig } : {}), includeAborted: q.get("includeAborted") === "true" });
3202
- sendJson(res, 200, { outcomes: rows });
3203
- }
3204
- catch (err) {
3205
- deps.logger?.warn?.("outcomes_summary_failed", { err: String(err) });
3206
- sendJson(res, 500, { error: "outcome summary failed" });
3207
- }
2728
+ if (await handleObservability(req, res, url, ctx))
3208
2729
  return;
3209
- }
3210
- if (url === "/v1/usage" && req.method === "GET") {
2730
+ if (await handleMemoryPolicy(req, res, url, ctx))
2731
+ return;
2732
+ if (await handleSessionsList(req, res, url, ctx))
2733
+ return;
2734
+ if (await handleSessions(req, res, url, ctx))
2735
+ return;
2736
+ if (await handleSessionSync(req, res, url, ctx))
2737
+ return;
2738
+ if (await handleAttachments(req, res, url, ctx))
2739
+ return;
2740
+ if (req.method === "POST" && /^\/v1\/sessions\/[^/]+\/notify$/.test(url)) {
2741
+ if (rateLimited(req, res) || quotaExceeded(req, res))
2742
+ return;
3211
2743
  const principal = gatedPrincipal(req, deps.config);
3212
- if (!principal) {
2744
+ if (deps.config.requirePrincipal && principal === undefined) {
3213
2745
  sendJson(res, 401, { error: `missing principal header '${deps.config.principalHeader}'` });
3214
2746
  return;
3215
2747
  }
3216
- const windowSec = deps.config.costQuotaWindowSec;
3217
- if (!deps.costQuota) {
3218
- sendJson(res, 200, { enabled: false, windowSec, maxTaskCostUsd: deps.config.maxTaskCostUsd, maxTaskTokens: deps.config.maxTaskTokens });
3219
- return;
2748
+ const notifySession = decodeURIComponent(url.split("/")[3]);
2749
+ let nb;
2750
+ try {
2751
+ nb = (await readJson(req));
3220
2752
  }
3221
- const d = deps.costQuota.check(principal);
3222
- sendJson(res, 200, {
3223
- enabled: true,
3224
- windowSec,
3225
- usedMicroUsd: d.usedMicroUsd,
3226
- usedUsd: d.usedMicroUsd / 1_000_000,
3227
- limitMicroUsd: d.limitMicroUsd,
3228
- limitUsd: d.limitMicroUsd / 1_000_000,
3229
- remainingMicroUsd: Math.max(0, d.limitMicroUsd - d.usedMicroUsd),
3230
- overLimit: !d.allowed,
3231
- retryAfterSec: d.retryAfterSec,
3232
- maxTaskCostUsd: deps.config.maxTaskCostUsd,
3233
- maxTaskTokens: deps.config.maxTaskTokens,
3234
- });
3235
- return;
3236
- }
3237
- if (url === "/v1/memory/export" && req.method === "GET") {
3238
- const principal = gatedPrincipal(req, deps.config);
3239
- if (!principal) {
3240
- sendJson(res, 401, { error: "unauthorized" });
2753
+ catch {
2754
+ sendJson(res, 400, { error: "body must be JSON: { task_id, status, summary, result?, seq?, source? }" });
3241
2755
  return;
3242
2756
  }
3243
- if (deps.config.memoryEngineBackend === "file" || !deps.memoryExport) {
3244
- sendJson(res, 501, { error: "memory export requires a DB memory backend (MEMORY_ENGINE_BACKEND=pg|tidb)" });
2757
+ if (typeof nb.task_id !== "string" || nb.task_id.trim().length === 0 || nb.task_id.length > 190) {
2758
+ sendJson(res, 400, { error: "task_id must be a non-empty string of at most 190 characters" });
3245
2759
  return;
3246
2760
  }
3247
- const scope = new URL(req.url ?? "", "http://x").searchParams.get("scope");
3248
- if (!scope) {
3249
- sendJson(res, 400, { error: "missing ?scope=<key>" });
2761
+ const NOTIFY_STATUSES = ["completed", "failed", "killed", "cancelled", "event"];
2762
+ if (typeof nb.status !== "string" || !NOTIFY_STATUSES.includes(nb.status)) {
2763
+ sendJson(res, 400, { error: `status must be one of ${NOTIFY_STATUSES.join("/")}` });
3250
2764
  return;
3251
2765
  }
3252
- if (scope !== formatUserScope(principal) && !explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
3253
- sendJson(res, 404, { error: "not found" });
2766
+ if (typeof nb.summary !== "string" || nb.summary.trim().length === 0) {
2767
+ sendJson(res, 400, { error: "summary must be a non-empty string" });
3254
2768
  return;
3255
2769
  }
3256
- try {
3257
- const entries = await deps.memoryExport(scope);
3258
- sendJson(res, 200, { scope, exportedAt: new Date().toISOString(), entries });
3259
- }
3260
- catch (err) {
3261
- deps.logger?.warn?.("memory_export_failed", { scope, err: String(err) });
3262
- sendJson(res, 500, { error: "memory export failed" });
3263
- }
3264
- return;
3265
- }
3266
- if (req.method === "POST" && url.startsWith("/v1/memory/sync/")) {
3267
- const principal = gatedPrincipal(req, deps.config);
3268
- if (!principal) {
3269
- sendJson(res, 401, { error: "unauthorized" });
2770
+ if (nb.result !== undefined && typeof nb.result !== "string") {
2771
+ sendJson(res, 400, { error: "result must be a string when present" });
3270
2772
  return;
3271
2773
  }
3272
- if (deps.config.memoryEngineBackend === "file" || !deps.memorySync) {
3273
- sendJson(res, 501, { error: "memory sync requires a DB memory backend (MEMORY_ENGINE_BACKEND=pg|tidb)" });
2774
+ if (nb.seq !== undefined && (typeof nb.seq !== "number" || !Number.isInteger(nb.seq) || nb.seq < 1)) {
2775
+ sendJson(res, 400, { error: "seq must be a positive integer when present" });
3274
2776
  return;
3275
2777
  }
3276
- let scope;
3277
- try {
3278
- scope = decodeURIComponent(url.slice("/v1/memory/sync/".length));
3279
- }
3280
- catch {
3281
- sendError(res, 400, "memory_sync_invalid_scope", "malformed percent-encoding in :scope", { code: "memory_sync_invalid_scope" });
2778
+ if (nb.source !== undefined && (typeof nb.source !== "string" || nb.source.length > 190)) {
2779
+ sendJson(res, 400, { error: "source must be a string of at most 190 characters when present" });
3282
2780
  return;
3283
2781
  }
3284
- if (scope.length === 0) {
3285
- sendError(res, 400, "memory_sync_invalid_scope", "missing :scope", { code: "memory_sync_invalid_scope" });
2782
+ if (!deps.sessionStorage?.ownerOf) {
2783
+ sendJson(res, 501, { error: "external notify requires the session-ownership face (sessionStorage.ownerOf)" });
3286
2784
  return;
3287
2785
  }
3288
- if (scope !== formatUserScope(principal) && !explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
3289
- sendJson(res, 404, { error: "not found" });
2786
+ const notifyOwner = await deps.sessionStorage.ownerOf(notifySession);
2787
+ if (notifyOwner === undefined) {
2788
+ sendJson(res, 404, { error: "session not found" });
3290
2789
  return;
3291
2790
  }
3292
- const body = await readJson(req);
3293
- const parsed = parseMemorySyncRequest(body, scope);
3294
- if (!parsed.ok) {
3295
- sendError(res, 422, "memory_sync_invalid_body", parsed.error, { code: "memory_sync_invalid_body" });
2791
+ const notifyOperator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
2792
+ if (!notifyOperator && notifyOwner !== null && notifyOwner !== principal) {
2793
+ sendJson(res, 404, { error: "session not found" });
3296
2794
  return;
3297
2795
  }
3298
- try {
3299
- const out = await deps.memorySync(scope, parsed.value);
3300
- sendJson(res, 200, out);
3301
- }
3302
- catch (err) {
3303
- deps.logger?.warn?.("memory_sync_failed", { scope, peer: parsed.value.peer, err: String(err) });
3304
- sendJson(res, 500, { error: "memory sync failed" });
2796
+ const payload = {
2797
+ task_id: nb.task_id,
2798
+ status: nb.status,
2799
+ summary: nb.summary,
2800
+ ...(nb.result !== undefined ? { result: nb.result } : {}),
2801
+ ...(nb.seq !== undefined ? { seq: nb.seq } : {}),
2802
+ ...(nb.source !== undefined ? { source: nb.source } : {}),
2803
+ };
2804
+ const liveTaskId = await deps.runStore?.getActiveTaskId?.(notifySession).catch(() => undefined);
2805
+ const liveStream = liveTaskId !== undefined && liveTaskId !== null ? steerableRuns.get(liveTaskId) : undefined;
2806
+ if (liveStream) {
2807
+ try {
2808
+ await liveStream.notify(payload);
2809
+ sendJson(res, 200, { sessionId: notifySession, delivery: "live" });
2810
+ return;
2811
+ }
2812
+ catch (e) {
2813
+ const code = e.code;
2814
+ if (typeof code === "string" && code.startsWith("notify.")) {
2815
+ sendError(res, 400, code, e instanceof Error ? e.message : "invalid notification");
2816
+ return;
2817
+ }
2818
+ }
3305
2819
  }
3306
- return;
3307
- }
3308
- if (url === "/v1/policy" && req.method === "GET") {
3309
- const principal = gatedPrincipal(req, deps.config);
3310
- if (deps.config.requirePrincipal && !principal) {
3311
- sendJson(res, 401, { error: `missing principal header '${deps.config.principalHeader}'` });
2820
+ if (!deps.workflowCompletionInbox) {
2821
+ sendJson(res, 501, { error: "external notify park requires the workflow completion inbox (WORKFLOW_RUN_STORE)" });
3312
2822
  return;
3313
2823
  }
3314
- sendJson(res, 200, {
3315
- autonomy: deps.config.autonomy ?? null,
3316
- commandPolicy: deps.config.commandPolicy ?? [],
3317
- approvalRequire: deps.config.approvalRequire,
3318
- limits: {
3319
- maxTaskCostUsd: deps.config.maxTaskCostUsd,
3320
- maxTaskTokens: deps.config.maxTaskTokens,
3321
- maxPrincipalCostUsd: deps.config.maxPrincipalCostUsd,
3322
- costQuotaWindowSec: deps.config.costQuotaWindowSec,
3323
- },
3324
- });
2824
+ await deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(notifySession, notifyOwner ?? principal ?? null, { task_id: payload.task_id, task_type: "external", status: payload.status, summary: payload.summary, ...(payload.result !== undefined ? { result: payload.result } : {}), ...(payload.seq !== undefined ? { seq: payload.seq } : {}), ...(payload.source !== undefined ? { source: payload.source } : {}) }, Date.now()));
2825
+ sendJson(res, 202, { sessionId: notifySession, delivery: "parked", note: "no live stream — parked in the session inbox; drained as a task_notification on the next stream open" });
3325
2826
  return;
3326
2827
  }
3327
- if (req.method === "GET" && url === "/v1/sessions") {
3328
- const { fleetWide, gateOwner } = sessionOwnerScope(req);
3329
- if (!fleetWide && gateOwner === null) {
3330
- sendJson(res, 401, { error: "unauthorized" });
2828
+ if (req.method === "POST" && /^\/v1\/sessions\/[^/]+\/wake$/.test(url)) {
2829
+ if (!deps.checkpointStore) {
2830
+ sendJson(res, 501, { error: "wake requires the checkpoint store" });
3331
2831
  return;
3332
2832
  }
3333
- const lister = deps.sessionStorage?.listSessions?.bind(deps.sessionStorage) ?? deps.runStore?.listSessions?.bind(deps.runStore);
3334
- if (!lister) {
3335
- sendJson(res, 501, { error: "session list requires a durable session store (SESSION_BACKEND=tidb)" });
3336
- return;
3337
- }
3338
- await handleSessionList(res, lister, new URL(req.url ?? "", "http://x").searchParams, gateOwner);
3339
- return;
3340
- }
3341
- if (req.method === "GET" && url === "/v1/sendfile-links") {
3342
- const { fleetWide, gateOwner } = sessionOwnerScope(req);
3343
- if (!fleetWide && gateOwner === null) {
3344
- sendJson(res, 401, { error: "unauthorized" });
3345
- return;
3346
- }
3347
- if (!deps.sendFileLedger) {
3348
- sendJson(res, 501, { error: "sendfile ledger requires a store backend (DB_BACKEND=tidb|pg|local) and the SendUserFile issuer configured" });
3349
- return;
3350
- }
3351
- const query = new URL(req.url ?? "", "http://x").searchParams;
3352
- const limitRaw = query.get("limit");
3353
- if (limitRaw !== null && !(/^[0-9]+$/.test(limitRaw) && Number(limitRaw) >= 1 && Number(limitRaw) <= 100)) {
3354
- sendJson(res, 400, { error: "limit must be a decimal integer in 1..100" });
3355
- return;
3356
- }
3357
- const limit = limitRaw === null ? 50 : Number(limitRaw);
3358
- const beforeRaw = query.get("before");
3359
- if (beforeRaw !== null && !isUuidV7(beforeRaw)) {
3360
- sendJson(res, 400, { error: "before must be a uuidv7 link id (the `id`/`nextBefore` of a previous page)" });
3361
- return;
3362
- }
3363
- const before = beforeRaw ?? undefined;
3364
- let scope;
3365
- if (gateOwner !== null) {
3366
- scope = gateOwner;
3367
- }
3368
- else {
3369
- const scopeRaw = query.get("scope");
3370
- if (scopeRaw === null) {
3371
- scope = "_";
3372
- }
3373
- else {
3374
- const s = scopeRaw.trim();
3375
- if (s === "" || s.length > 190) {
3376
- sendJson(res, 400, { error: "scope must be a non-blank tenant principal of at most 190 characters (the sendfile_link.scope column width)" });
3377
- return;
3378
- }
3379
- scope = s;
3380
- }
3381
- }
3382
- const rows = await deps.sendFileLedger.listByScope(scope, limit + 1, before);
3383
- const hasMore = rows.length > limit;
3384
- const page = hasMore ? rows.slice(0, limit) : rows;
3385
- sendJson(res, 200, {
3386
- links: page.map((r) => ({
3387
- id: r.id,
3388
- filename: r.filename,
3389
- size: r.size,
3390
- ttlSec: r.ttlSec,
3391
- track: r.track,
3392
- bucket: r.bucket,
3393
- key: r.objectKey,
3394
- createdAt: new Date(r.createdAt).toISOString(),
3395
- ...(r.taskId !== undefined ? { taskId: r.taskId } : {}),
3396
- ...(r.sessionId !== undefined ? { sessionId: r.sessionId } : {}),
3397
- })),
3398
- ...(hasMore ? { nextBefore: page[page.length - 1].id } : {}),
3399
- });
3400
- return;
3401
- }
3402
- {
3403
- const im = /^\/v1\/sessions\/([^/]+)\/init$/.exec(url);
3404
- if (im && req.method === "GET") {
3405
- const principal = gatedPrincipal(req, deps.config);
3406
- if (deps.config.requirePrincipal && !principal) {
3407
- sendJson(res, 401, { error: `missing principal header '${deps.config.principalHeader}'` });
3408
- return;
3409
- }
3410
- sendJson(res, 200, {
3411
- sessionId: im[1],
3412
- model: { provider: deps.config.model.provider, modelId: deps.config.model.id },
3413
- ...(deps.config.autonomy ? { mode: deps.config.autonomy } : {}),
3414
- });
3415
- return;
3416
- }
3417
- }
3418
- {
3419
- const stm = /^\/v1\/sessions\/([^/]+)\/settings$/.exec(url);
3420
- if (stm && req.method === "GET") {
3421
- const principal = gatedPrincipal(req, deps.config);
3422
- if (deps.config.requirePrincipal && !principal) {
3423
- sendJson(res, 401, { error: `missing principal header '${deps.config.principalHeader}'` });
3424
- return;
3425
- }
3426
- sendJson(res, 200, {
3427
- effective: {
3428
- model: deps.config.model.id,
3429
- ...(deps.config.autonomy ? { mode: deps.config.autonomy } : {}),
3430
- approvalRequire: deps.config.approvalRequire,
3431
- commandPolicy: deps.config.commandPolicy ?? [],
3432
- limits: { maxTaskCostUsd: deps.config.maxTaskCostUsd, maxTaskTokens: deps.config.maxTaskTokens },
3433
- },
3434
- applied: { model: deps.config.model.id },
3435
- });
3436
- return;
3437
- }
3438
- }
3439
- {
3440
- const mm = /^\/v1\/sessions\/([^/]+)\/mcp$/.exec(url);
3441
- if (mm && req.method === "GET") {
3442
- if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
3443
- sendJson(res, 401, { error: `missing principal header '${deps.config.principalHeader}'` });
3444
- return;
3445
- }
3446
- const specs = mcpForScenario(deps.config.mcpServers, deps.config.defaultScenario);
3447
- if (!specs || specs.length === 0) {
3448
- sendJson(res, 200, { servers: [], asOf: new Date().toISOString() });
3449
- return;
3450
- }
3451
- const principal = gatedPrincipal(req, deps.config);
3452
- const cacheKey = `${deps.config.defaultScenario}::${principal ?? ""}`;
3453
- const cached = mcpStatusCache.get(cacheKey);
3454
- if (!cached || Date.now() - cached.at > MCP_STATUS_TTL_MS) {
3455
- if (mcpStatusCache.size > 1024)
3456
- mcpStatusCache.clear();
3457
- const panel = (async () => {
3458
- const matP = materializeMcpTools(specs, principal);
3459
- let timer;
3460
- const mat = await Promise.race([matP.catch(() => null), new Promise((r) => { timer = setTimeout(() => r(null), MCP_MATERIALIZE_TIMEOUT_MS); })]);
3461
- if (timer)
3462
- clearTimeout(timer);
3463
- if (!mat) {
3464
- void matP.then((m) => m?.dispose().catch(() => undefined)).catch(() => undefined);
3465
- return { servers: [], asOf: new Date().toISOString(), degraded: true };
3466
- }
3467
- try {
3468
- return {
3469
- asOf: new Date().toISOString(),
3470
- servers: mat.statuses.map((s) => ({
3471
- name: s.name,
3472
- status: s.status,
3473
- ...(s.serverInfo ? { serverInfo: s.serverInfo } : {}),
3474
- ...(s.toolNames ? { toolNames: s.toolNames } : {}),
3475
- ...(s.error ? { error: redactSecrets(String(s.error)).slice(0, 200) } : {}),
3476
- })),
3477
- };
3478
- }
3479
- finally {
3480
- await mat.dispose().catch(() => undefined);
3481
- }
3482
- })();
3483
- mcpStatusCache.set(cacheKey, { at: Date.now(), panel });
3484
- }
3485
- sendJson(res, 200, await mcpStatusCache.get(cacheKey).panel);
3486
- return;
3487
- }
3488
- }
3489
- {
3490
- const hm = req.method === "GET" ? /^\/v1\/sessions\/([^/]+)\/head$/.exec(url) : null;
3491
- if (hm) {
3492
- const getLeafId = deps.sessionStorage?.getLeafId?.bind(deps.sessionStorage);
3493
- const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
3494
- if (!getLeafId || !ownerOf) {
3495
- sendJson(res, 501, { error: "session head probe requires a durable session store (SESSION_BACKEND=tidb|pg|local)" });
3496
- return;
3497
- }
3498
- const headSession = decodeURIComponent(hm[1]);
3499
- const getHead = deps.sessionStorage?.getHead?.bind(deps.sessionStorage);
3500
- let owner;
3501
- let leafId;
3502
- if (getHead) {
3503
- const head = await getHead(headSession);
3504
- if (head === undefined) {
3505
- sendJson(res, 404, { error: "session not found" });
3506
- return;
3507
- }
3508
- owner = head.owner;
3509
- if (!runOwnerOk(req, res, owner ?? null))
3510
- return;
3511
- leafId = head.leafId;
3512
- }
3513
- else {
3514
- owner = await ownerOf(headSession);
3515
- if (owner === undefined) {
3516
- sendJson(res, 404, { error: "session not found" });
3517
- return;
3518
- }
3519
- if (!runOwnerOk(req, res, owner ?? null))
3520
- return;
3521
- leafId = await getLeafId(headSession);
3522
- }
3523
- const etag = `"${leafId ?? "empty"}"`;
3524
- if (req.headers["if-none-match"] === etag) {
3525
- res.writeHead(304, { etag }).end();
3526
- return;
3527
- }
3528
- res.setHeader("etag", etag);
3529
- sendJson(res, 200, { sessionId: headSession, leafId });
3530
- return;
3531
- }
3532
- }
3533
- {
3534
- const em = req.method === "GET" ? /^\/v1\/sessions\/([^/]+)\/events$/.exec(url) : null;
3535
- if (em) {
3536
- const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
3537
- const watch = deps.sessionWatch;
3538
- if (!deps.sessionStorage?.getLeafId || !ownerOf || !watch) {
3539
- sendJson(res, 501, { error: "session events require a durable session store (SESSION_BACKEND=tidb|pg|local)" });
3540
- return;
3541
- }
3542
- const evSession = decodeURIComponent(em[1]);
3543
- if (sseConnections >= (deps.sessionEventsMaxConnections ?? 256)) {
3544
- res.writeHead(503, { "retry-after": "5", "content-type": "application/json" });
3545
- res.end(JSON.stringify({ error: "session-events connection cap reached — fall back to /head polling" }));
3546
- return;
3547
- }
3548
- sseConnections++;
3549
- let hb;
3550
- let unsubscribe;
3551
- let closed = false;
3552
- const cleanup = () => {
3553
- if (closed)
3554
- return;
3555
- closed = true;
3556
- sseConnections--;
3557
- if (hb)
3558
- clearInterval(hb);
3559
- unsubscribe?.();
3560
- };
3561
- res.on("close", cleanup);
3562
- req.on("close", cleanup);
3563
- try {
3564
- const stallBound = deps.sessionEventsStallMs ?? 60_000;
3565
- if (sseOwnerLookups >= 64) {
3566
- cleanup();
3567
- res.writeHead(503, { "retry-after": "5", "content-type": "application/json" });
3568
- res.end(JSON.stringify({ error: "session-events owner-lookup concurrency cap — retry" }));
3569
- return;
3570
- }
3571
- sseOwnerLookups++;
3572
- const ownerP = ownerOf(evSession);
3573
- void ownerP.catch(() => undefined).then(() => { sseOwnerLookups--; });
3574
- let stallTimer;
3575
- let owner;
3576
- try {
3577
- owner = await Promise.race([
3578
- ownerP,
3579
- new Promise((r) => { stallTimer = setTimeout(() => r("__stall__"), stallBound); stallTimer.unref?.(); }),
3580
- ]);
3581
- }
3582
- finally {
3583
- if (stallTimer)
3584
- clearTimeout(stallTimer);
3585
- }
3586
- if (closed)
3587
- return;
3588
- if (owner === "__stall__") {
3589
- cleanup();
3590
- res.writeHead(503, { "retry-after": "5", "content-type": "application/json" });
3591
- res.end(JSON.stringify({ error: "session-events owner lookup stalled — retry" }));
3592
- return;
3593
- }
3594
- if (owner === undefined) {
3595
- cleanup();
3596
- sendJson(res, 404, { error: "session not found" });
3597
- return;
3598
- }
3599
- if (!runOwnerOk(req, res, owner ?? null)) {
3600
- cleanup();
3601
- return;
3602
- }
3603
- const q = new URL(req.url ?? url, "http://x").searchParams;
3604
- const lastLeafId = q.get("lastLeafId");
3605
- let first = true;
3606
- let lastAccepted;
3607
- let lastWritten;
3608
- let writeBlocked = false;
3609
- let blockedAt = 0;
3610
- let pendingHead;
3611
- const stallMs = deps.sessionEventsStallMs ?? 60_000;
3612
- const writeRaw = (chunk) => {
3613
- if (closed)
3614
- return;
3615
- try {
3616
- const okW = res.write(chunk);
3617
- if (!okW && !writeBlocked) {
3618
- writeBlocked = true;
3619
- blockedAt = Date.now();
3620
- res.once("drain", () => {
3621
- writeBlocked = false;
3622
- const p = pendingHead;
3623
- pendingHead = undefined;
3624
- if (p && !closed && p.v !== lastWritten)
3625
- doWrite(p.v);
3626
- });
3627
- }
3628
- }
3629
- catch { }
3630
- };
3631
- const doWrite = (leafId) => {
3632
- lastWritten = leafId;
3633
- writeRaw(`event: head\ndata: ${JSON.stringify({ sessionId: evSession, leafId })}\n\n`);
3634
- };
3635
- const emit = (leafId) => {
3636
- if (closed || leafId === lastAccepted)
3637
- return;
3638
- lastAccepted = leafId;
3639
- const suppress = first && lastLeafId !== null && lastLeafId === (leafId ?? "null");
3640
- first = false;
3641
- if (suppress) {
3642
- lastWritten = leafId;
3643
- return;
3644
- }
3645
- if (writeBlocked) {
3646
- pendingHead = { v: leafId };
3647
- if (Date.now() - blockedAt > stallMs) {
3648
- cleanup();
3649
- try {
3650
- res.end();
3651
- }
3652
- catch { }
3653
- }
3654
- return;
3655
- }
3656
- doWrite(leafId);
3657
- };
3658
- let started = false;
3659
- let preVal;
3660
- const onHead = (leafId) => {
3661
- if (!started)
3662
- preVal = { v: leafId };
3663
- else
3664
- emit(leafId);
3665
- };
3666
- const onEnd = () => { cleanup(); try {
3667
- res.end();
3668
- }
3669
- catch { } };
3670
- unsubscribe = watch.subscribe(evSession, owner ?? null, onHead, onEnd);
3671
- if (!unsubscribe) {
3672
- cleanup();
3673
- res.writeHead(503, { "retry-after": "5", "content-type": "application/json" });
3674
- res.end(JSON.stringify({ error: "session-events probe cap reached — fall back to /head polling" }));
3675
- return;
3676
- }
3677
- res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache, no-transform", connection: "keep-alive", "x-accel-buffering": "no" });
3678
- writeRaw(": connected\n\n");
3679
- started = true;
3680
- if (preVal !== undefined)
3681
- emit(preVal.v);
3682
- const hbMs = deps.sessionEventsHeartbeatMs ?? 25_000;
3683
- let ownerCheckPending = false;
3684
- let ownerCheckStartedAt = 0;
3685
- hb = setInterval(() => {
3686
- if (closed)
3687
- return;
3688
- if (writeBlocked && Date.now() - blockedAt > stallMs) {
3689
- onEnd();
3690
- return;
3691
- }
3692
- if (!writeBlocked)
3693
- writeRaw(": hb\n\n");
3694
- if (ownerCheckPending) {
3695
- if (Date.now() - ownerCheckStartedAt > stallMs)
3696
- onEnd();
3697
- return;
3698
- }
3699
- ownerCheckPending = true;
3700
- ownerCheckStartedAt = Date.now();
3701
- void ownerOf(evSession).then((o) => {
3702
- ownerCheckPending = false;
3703
- if (!closed && o !== owner)
3704
- onEnd();
3705
- }).catch(() => { ownerCheckPending = false; });
3706
- }, hbMs);
3707
- hb.unref?.();
3708
- }
3709
- catch (e) {
3710
- cleanup();
3711
- if (!res.headersSent)
3712
- sendJson(res, 500, { error: `session events failed: ${e instanceof Error ? e.message : "unknown"}` });
3713
- else
3714
- try {
3715
- res.end();
3716
- }
3717
- catch { }
3718
- }
3719
- return;
3720
- }
3721
- }
3722
- if (deps.sessionAudit && req.method === "GET") {
3723
- const sm = /^\/v1\/sessions\/([^/]+)$/.exec(url);
3724
- if (sm) {
3725
- const audit = await deps.sessionAudit(sm[1]);
3726
- if (!audit) {
3727
- sendJson(res, 404, { error: "session not found" });
3728
- return;
3729
- }
3730
- if (!runOwnerOk(req, res, audit.owner))
3731
- return;
3732
- const q = new URL(req.url ?? url, "http://x").searchParams;
3733
- const qi = (k) => {
3734
- const v = q.get(k);
3735
- if (v === null)
3736
- return undefined;
3737
- const n = Number(v);
3738
- return Number.isFinite(n) && n >= 0 ? n : undefined;
3739
- };
3740
- const msgIndex = qi("message");
3741
- if (msgIndex !== undefined) {
3742
- const messages = audit.messages;
3743
- if (msgIndex >= messages.length) {
3744
- sendJson(res, 404, { error: "message index out of range", total: messages.length });
3745
- return;
3746
- }
3747
- sendJson(res, 200, { sessionId: audit.sessionId, floorEntryId: audit.floorEntryId, index: msgIndex, message: messages[msgIndex] });
3748
- return;
3749
- }
3750
- const tail = qi("tail");
3751
- const before = qi("before");
3752
- const blobCap = qi("blobCap");
3753
- if (tail === undefined && before === undefined && blobCap === undefined) {
3754
- sendJson(res, 200, audit);
3755
- return;
3756
- }
3757
- let messages = audit.messages;
3758
- let window;
3759
- if (tail !== undefined || before !== undefined) {
3760
- const w = windowMessages(messages, { ...(tail !== undefined ? { tail } : {}), ...(before !== undefined ? { before } : {}), ...(qi("limit") !== undefined ? { limit: qi("limit") } : {}) });
3761
- messages = w.messages;
3762
- window = w.window;
3763
- }
3764
- if (blobCap !== undefined && blobCap > 0)
3765
- messages = truncateMessageBlobs(messages, blobCap);
3766
- sendJson(res, 200, { ...audit, messages, ...(window ? { window } : {}) });
3767
- return;
3768
- }
3769
- }
3770
- {
3771
- const fm = req.method === "POST" ? /^\/v1\/sessions\/([^/]+)\/fork$/.exec(url) : null;
3772
- if (fm) {
3773
- const fork = deps.sessionStorage?.fork?.bind(deps.sessionStorage);
3774
- const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
3775
- if (!fork || !ownerOf) {
3776
- sendJson(res, 501, { error: "session fork requires a durable session store (SESSION_BACKEND=tidb)" });
3777
- return;
3778
- }
3779
- const scope = sessionOwnerScopeForWrite(req);
3780
- if (!scope.fleetWide && scope.gateOwner === null) {
3781
- sendJson(res, 401, { error: "unauthorized" });
3782
- return;
3783
- }
3784
- const sourceId = safeDecode(fm[1]);
3785
- if (sourceId === null || !isUuidV7(sourceId)) {
3786
- sendJson(res, 400, { error: "invalid session id (must be a uuidv7)" });
3787
- return;
3788
- }
3789
- const owner = await ownerOf(sourceId);
3790
- if (owner === undefined) {
3791
- sendJson(res, 404, { error: "session not found" });
3792
- return;
3793
- }
3794
- if (!scope.fleetWide && owner !== scope.gateOwner) {
3795
- sendJson(res, 404, { error: "session not found" });
3796
- return;
3797
- }
3798
- const newOwner = scope.fleetWide ? (owner ?? null) : scope.gateOwner;
3799
- const newId = await fork(sourceId, newOwner);
3800
- if (newId === null) {
3801
- sendJson(res, 404, { error: "session not found" });
3802
- return;
3803
- }
3804
- sendJson(res, 200, { sessionId: newId });
3805
- return;
3806
- }
3807
- }
3808
- {
3809
- const dm = req.method === "DELETE" ? /^\/v1\/sessions\/([^/]+)$/.exec(url) : null;
3810
- if (dm) {
3811
- const purge = deps.purgeSession;
3812
- const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
3813
- if (!purge || !ownerOf) {
3814
- sendJson(res, 501, { error: "session delete requires a durable session store (SESSION_BACKEND=tidb)" });
3815
- return;
3816
- }
3817
- const scope = sessionOwnerScopeForWrite(req);
3818
- if (!scope.fleetWide && scope.gateOwner === null) {
3819
- sendJson(res, 401, { error: "unauthorized" });
3820
- return;
3821
- }
3822
- const sessionId = safeDecode(dm[1]);
3823
- if (sessionId === null || !isUuidV7(sessionId)) {
3824
- sendJson(res, 400, { error: "invalid session id (must be a uuidv7)" });
3825
- return;
3826
- }
3827
- const owner = await ownerOf(sessionId);
3828
- if (owner === undefined) {
3829
- sendJson(res, 200, { deleted: false });
3830
- return;
3831
- }
3832
- if (!scope.fleetWide && owner !== scope.gateOwner) {
3833
- sendJson(res, 200, { deleted: false });
3834
- return;
3835
- }
3836
- const activeTaskId = await deps.runStore?.getActiveTaskId(sessionId);
3837
- if (activeTaskId) {
3838
- sendJson(res, 409, { error: "session has an active run; cancel it before deleting", activeTaskId });
3839
- return;
3840
- }
3841
- const result = await purge(sessionId, owner);
3842
- if ("active" in result) {
3843
- sendJson(res, 409, { error: "session has an active run; cancel it before deleting", activeTaskId: result.active });
3844
- return;
3845
- }
3846
- sendJson(res, 200, result);
3847
- return;
3848
- }
3849
- }
3850
- {
3851
- const pm = req.method === "GET" || req.method === "PUT" ? /^\/v1\/sessions\/([^/]+)\/policy$/.exec(url) : null;
3852
- if (pm) {
3853
- const store = deps.sessionPolicyStore;
3854
- const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
3855
- if (!store || !ownerOf) {
3856
- sendJson(res, 501, { error: "session policy requires a session-store backend" });
3857
- return;
3858
- }
3859
- const principal = gatedPrincipal(req, deps.config);
3860
- if (deps.config.requirePrincipal && principal === undefined) {
3861
- sendJson(res, 401, { error: "unauthorized" });
3862
- return;
3863
- }
3864
- const sessionId = safeDecode(pm[1]);
3865
- if (sessionId === null || !isUuidV7(sessionId)) {
3866
- sendJson(res, 400, { error: "invalid session id (must be a uuidv7)" });
3867
- return;
3868
- }
3869
- const owner = await ownerOf(sessionId);
3870
- if (owner === undefined) {
3871
- sendJson(res, 404, { error: "session not found" });
3872
- return;
3873
- }
3874
- if (owner === null) {
3875
- sendJson(res, 409, { error: "session policy requires an owned session (set REQUIRE_PRINCIPAL / attach a principal); this session is ownerless" });
3876
- return;
3877
- }
3878
- const operator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
3879
- if (!operator && owner !== (principal ?? null)) {
3880
- sendJson(res, 404, { error: "session not found" });
3881
- return;
3882
- }
3883
- const target = owner;
3884
- if (req.method === "GET") {
3885
- sendJson(res, 200, { rules: (await store.getRules(sessionId, target)) ?? { rev: 0 } });
3886
- return;
3887
- }
3888
- let body;
3889
- try {
3890
- body = (await readJson(req));
3891
- }
3892
- catch {
3893
- sendJson(res, 400, { error: "invalid JSON body" });
3894
- return;
3895
- }
3896
- if (typeof body.rules !== "object" || body.rules === null || Array.isArray(body.rules)) {
3897
- sendJson(res, 400, { error: "rules must be a SessionPermissionRules object" });
3898
- return;
3899
- }
3900
- if (body.expectedRev !== undefined && (typeof body.expectedRev !== "number" || !Number.isInteger(body.expectedRev) || body.expectedRev < 0)) {
3901
- sendJson(res, 400, { error: "expectedRev must be a non-negative integer" });
3902
- return;
3903
- }
3904
- const raw = body.rules;
3905
- const rules = {};
3906
- for (const f of ["toolAllow", "toolDeny", "allowDirs", "commandAllow", "commandDeny"]) {
3907
- if (raw[f] === undefined)
3908
- continue;
3909
- if (!Array.isArray(raw[f]) || !raw[f].every((x) => typeof x === "string")) {
3910
- sendJson(res, 400, { error: `${f} must be an array of strings` });
3911
- return;
3912
- }
3913
- rules[f] = raw[f];
3914
- }
3915
- try {
3916
- const stored = await store.putRules(sessionId, target, rules, { ...(typeof body.expectedRev === "number" ? { expectedRev: body.expectedRev } : {}), operator });
3917
- sendJson(res, 200, { rules: stored });
3918
- }
3919
- catch (e) {
3920
- if (e instanceof SessionPolicyError) {
3921
- sendError(res, e.code === "conflict" ? 409 : 403, e.code, e.message);
3922
- return;
3923
- }
3924
- throw e;
3925
- }
3926
- return;
3927
- }
3928
- }
3929
- {
3930
- const wm = req.method === "GET" ? /^\/v1\/sessions\/([^/]+)\/workspace(?:\/([^/]+)(?:\/(tree|file|archive))?)?$/.exec(url) : null;
3931
- if (wm) {
3932
- const fs = deps.fileSnapshotStore;
3933
- const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
3934
- if (!fs || typeof fs.listKeys !== "function" || typeof fs.exportManifest !== "function" || typeof fs.getBlob !== "function" || !ownerOf) {
3935
- sendJson(res, 501, { error: "workspace browse requires a durable snapshot store (rewindFiles wiring) and a durable session store" });
3936
- return;
3937
- }
3938
- const scope = sessionOwnerScope(req);
3939
- if (!scope.fleetWide && scope.gateOwner === null) {
3940
- sendJson(res, 401, { error: "unauthorized" });
3941
- return;
3942
- }
3943
- const wsSession = safeDecode(wm[1]);
3944
- if (wsSession === null || !isUuidShape(wsSession)) {
3945
- sendJson(res, 400, { error: "invalid session id (must be a canonical uuid)" });
3946
- return;
3947
- }
3948
- const wsOwner = await ownerOf(wsSession);
3949
- if (wsOwner === undefined || (!scope.fleetWide && wsOwner !== scope.gateOwner)) {
3950
- sendJson(res, 404, { error: "session not found" });
3951
- return;
3952
- }
3953
- const q = new URL(req.url ?? "", "http://x").searchParams;
3954
- const keys = (await fs.listKeys(wsSession)).sort().reverse();
3955
- const sub = wm[3];
3956
- if (wm[2] === undefined) {
3957
- const limRaw = q.get("limit");
3958
- if (limRaw !== null && !(/^[0-9]+$/.test(limRaw) && Number(limRaw) >= 1 && Number(limRaw) <= 100)) {
3959
- sendJson(res, 400, { error: "limit must be a decimal integer in 1..100" });
3960
- return;
3961
- }
3962
- const lim = limRaw === null ? 20 : Number(limRaw);
3963
- const page = keys.slice(0, lim);
3964
- const snapshots = [];
3965
- for (const k of page) {
3966
- const m = await fs.exportManifest(wsSession, k);
3967
- if (m === null)
3968
- continue;
3969
- let bytes;
3970
- if (typeof fs.blobSizes === "function") {
3971
- const sizes = await fs.blobSizes([...new Set(m.values())]);
3972
- bytes = 0;
3973
- for (const h of m.values())
3974
- bytes += sizes.get(h) ?? 0;
3975
- }
3976
- snapshots.push({ key: k, files: m.size, ...(bytes !== undefined ? { bytes } : {}) });
3977
- }
3978
- sendJson(res, 200, {
3979
- sessionId: wsSession,
3980
- snapshots,
3981
- ...(snapshots.length > 0 ? { latest: snapshots[0].key } : {}),
3982
- total: snapshots.length,
3983
- });
3984
- return;
3985
- }
3986
- const keyRaw = safeDecode(wm[2]);
3987
- if (keyRaw === null) {
3988
- sendJson(res, 400, { error: "malformed snapshot key" });
3989
- return;
3990
- }
3991
- const key = keyRaw === "latest" ? keys[0] : keyRaw;
3992
- const manifest = key !== undefined ? await fs.exportManifest(wsSession, key) : null;
3993
- if (key === undefined || manifest === null) {
3994
- sendJson(res, 404, { error: "snapshot not found" });
3995
- return;
3996
- }
3997
- if (sub === "tree") {
3998
- const limRaw = q.get("limit");
3999
- if (limRaw !== null && !(/^[0-9]+$/.test(limRaw) && Number(limRaw) >= 1 && Number(limRaw) <= 2000)) {
4000
- sendJson(res, 400, { error: "limit must be a decimal integer in 1..2000" });
4001
- return;
4002
- }
4003
- const offRaw = q.get("offset");
4004
- if (offRaw !== null && !/^[0-9]+$/.test(offRaw)) {
4005
- sendJson(res, 400, { error: "offset must be a non-negative integer" });
4006
- return;
4007
- }
4008
- const lim = limRaw === null ? 500 : Number(limRaw);
4009
- const off = offRaw === null ? 0 : Number(offRaw);
4010
- const all = [...manifest.entries()].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
4011
- const page = all.slice(off, off + lim);
4012
- const sizes = typeof fs.blobSizes === "function" ? await fs.blobSizes([...new Set(page.map(([, h]) => h))]) : undefined;
4013
- sendJson(res, 200, {
4014
- sessionId: wsSession,
4015
- key,
4016
- entries: page.map(([path, hash]) => ({ path, hash, ...(sizes?.get(hash) !== undefined ? { size: sizes.get(hash) } : {}) })),
4017
- total: all.length,
4018
- ...(off + lim < all.length ? { nextOffset: off + lim } : {}),
4019
- });
4020
- return;
4021
- }
4022
- if (sub === "file") {
4023
- const pathParam = q.get("path");
4024
- if (!pathParam) {
4025
- sendJson(res, 400, { error: "missing ?path=<relPath from the tree>" });
4026
- return;
4027
- }
4028
- const hash = manifest.get(pathParam);
4029
- if (hash === undefined) {
4030
- sendJson(res, 404, { error: "no such file in this snapshot (paths are the tree's relPaths, verbatim)" });
4031
- return;
4032
- }
4033
- const cap = deps.config.workspaceFileMaxBytes ?? 8 * 1024 * 1024;
4034
- if (typeof fs.blobSizes === "function") {
4035
- const known = (await fs.blobSizes([hash])).get(hash);
4036
- if (known !== undefined && known > cap) {
4037
- sendError(res, 413, "workspace_file_too_large", "file exceeds the single-file read limit — download it via the archive endpoint", { code: "workspace_file_too_large", sizeBytes: known, limit: cap });
4038
- return;
4039
- }
4040
- }
4041
- const bytes = await fs.getBlob(hash);
4042
- if (bytes === undefined) {
4043
- sendJson(res, 404, { error: "file content no longer available (snapshot raced a reap)" });
4044
- return;
4045
- }
4046
- if (bytes.byteLength > cap) {
4047
- sendError(res, 413, "workspace_file_too_large", "file exceeds the single-file read limit — download it via the archive endpoint", { code: "workspace_file_too_large", sizeBytes: bytes.byteLength, limit: cap });
4048
- return;
4049
- }
4050
- const ct = workspaceContentType(pathParam);
4051
- const bin = looksBinary(bytes);
4052
- res.writeHead(200, {
4053
- "content-type": ct,
4054
- "content-length": bytes.byteLength,
4055
- "x-content-type-options": "nosniff",
4056
- "x-sema-content-binary": bin ? "true" : "false",
4057
- });
4058
- res.end(Buffer.from(bytes));
4059
- return;
4060
- }
4061
- if (sub === "archive") {
4062
- const entries = [...manifest.entries()].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
4063
- const bad = entries.filter(([p]) => splitTarPath(p) === null).map(([p]) => p);
4064
- if (bad.length > 0) {
4065
- sendJson(res, 422, { error: `snapshot contains ${bad.length} path(s) not representable in ustar (>255 bytes or unsplittable)`, paths: bad.slice(0, 5) });
4066
- return;
4067
- }
4068
- if (typeof fs.hasBlobs === "function") {
4069
- const present = await fs.hasBlobs([...new Set(entries.map(([, h]) => h))]);
4070
- const missing = entries.filter(([, h]) => !present.has(h)).length;
4071
- if (missing > 0) {
4072
- sendJson(res, 502, { error: `snapshot is missing ${missing} blob(s) (raced a reap or a partial import) — retry, or pick another snapshot` });
4073
- return;
4074
- }
4075
- }
4076
- res.writeHead(200, {
4077
- "content-type": "application/x-tar",
4078
- "content-disposition": `attachment; filename="workspace-${key}.tar"`,
4079
- "x-content-type-options": "nosniff",
4080
- });
4081
- const write = async (buf) => {
4082
- if (res.writableEnded || res.destroyed)
4083
- throw new Error("client gone");
4084
- if (!res.write(buf)) {
4085
- await new Promise((resolve, reject) => {
4086
- const onDrain = () => { cleanup(); resolve(); };
4087
- const onGone = () => { cleanup(); reject(new Error("client gone (closed while draining)")); };
4088
- const cleanup = () => { res.off("drain", onDrain); res.off("close", onGone); res.off("error", onGone); };
4089
- res.once("drain", onDrain);
4090
- res.once("close", onGone);
4091
- res.once("error", onGone);
4092
- });
4093
- }
4094
- };
4095
- try {
4096
- for (const [p, h] of entries) {
4097
- const bytes = await fs.getBlob(h);
4098
- if (bytes === undefined)
4099
- throw new Error(`blob vanished mid-stream: ${h}`);
4100
- await write(tarHeader(p, bytes.byteLength, 0));
4101
- await write(Buffer.from(bytes));
4102
- await write(tarPadding(bytes.byteLength));
4103
- }
4104
- await write(TAR_END);
4105
- res.end();
4106
- }
4107
- catch {
4108
- res.destroy();
4109
- }
4110
- return;
4111
- }
4112
- sendJson(res, 404, { error: "unknown workspace verb — use /tree, /file?path=, or /archive" });
4113
- return;
4114
- }
4115
- }
4116
- {
4117
- const sm = /^\/v1\/sessions\/([^/]+)\/sync\/(manifest|entries|import|plan|blobs(?:\/([^/]+))?|snapshots\/([^/]+)\/blobs\/([^/]+)|import\/([^/]+)\/entries)$/.exec(url);
4118
- if (sm) {
4119
- const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
4120
- if (!deps.backend || !ownerOf) {
4121
- sendJson(res, 501, { error: "session sync requires a durable session store" });
4122
- return;
4123
- }
4124
- const scope = sessionOwnerScopeForWrite(req);
4125
- if (!scope.fleetWide && scope.gateOwner === null) {
4126
- sendJson(res, 401, { error: "unauthorized" });
4127
- return;
4128
- }
4129
- const sessionId = safeDecode(sm[1]);
4130
- if (sessionId === null || !isUuidShape(sessionId)) {
4131
- sendJson(res, 400, { error: "invalid session id (must be a canonical uuid)" });
4132
- return;
4133
- }
4134
- const owner = await ownerOf(sessionId);
4135
- const ownsOrFleet = (o) => scope.fleetWide || o === scope.gateOwner;
4136
- const pullGateOk = () => {
4137
- if (owner === undefined) {
4138
- sendJson(res, 404, { error: "session not found" });
4139
- return false;
4140
- }
4141
- if (!ownsOrFleet(owner)) {
4142
- sendJson(res, 404, { error: "session not found" });
4143
- return false;
4144
- }
4145
- return true;
4146
- };
4147
- const pushGateOk = () => {
4148
- if (owner !== undefined && !ownsOrFleet(owner)) {
4149
- sendJson(res, 404, { error: "session not found" });
4150
- return false;
4151
- }
4152
- return true;
4153
- };
4154
- if (sm[2] === "manifest" && req.method === "GET") {
4155
- if (!pullGateOk())
4156
- return;
4157
- const out = await exportSessionManifest(sessionId, deps.backend);
4158
- if (out === null) {
4159
- sendJson(res, 404, { error: "session not found" });
4160
- return;
4161
- }
4162
- sendJson(res, 200, { manifest: out.manifest });
4163
- return;
4164
- }
4165
- if (sm[2] === "entries" && req.method === "GET") {
4166
- if (!pullGateOk())
4167
- return;
4168
- const afterSeqRaw = new URL(req.url ?? "", "http://x").searchParams.get("afterSeq");
4169
- let afterSeq;
4170
- if (afterSeqRaw !== null) {
4171
- const n = Number(afterSeqRaw);
4172
- if (!Number.isInteger(n) || n < 0) {
4173
- sendJson(res, 400, { error: "afterSeq must be a non-negative integer" });
4174
- return;
4175
- }
4176
- afterSeq = n;
4177
- }
4178
- const stream = deps.sessionStorage?.exportEntriesStream?.bind(deps.sessionStorage);
4179
- if (!stream) {
4180
- sendJson(res, 501, { error: "session entry streaming requires a durable session store" });
4181
- return;
4182
- }
4183
- const it = await stream(sessionId, afterSeq === undefined ? undefined : { afterSeq });
4184
- if (it === null) {
4185
- sendJson(res, 404, { error: "session not found" });
4186
- return;
4187
- }
4188
- res.writeHead(200, { "content-type": "application/x-ndjson", "transfer-encoding": "chunked" });
4189
- const write = async (line) => { if (!res.write(line))
4190
- await once(res, "drain"); };
4191
- let count = 0;
4192
- try {
4193
- await write(`${JSON.stringify({ __sync: "begin", sessionId })}\n`);
4194
- for await (const entry of it) {
4195
- await write(`${JSON.stringify(entry)}\n`);
4196
- count++;
4197
- }
4198
- await write(`${JSON.stringify({ __sync: "end", count })}\n`);
4199
- res.end();
4200
- }
4201
- catch (e) {
4202
- deps.logger?.error?.("sync entries stream failed", { sessionId, count, error: msg(e) });
4203
- res.end();
4204
- }
4205
- return;
4206
- }
4207
- if (sm[4] !== undefined && req.method === "GET") {
4208
- if (!pullGateOk())
4209
- return;
4210
- const key = safeDecode(sm[4]);
4211
- const hash = safeDecode(sm[5]);
4212
- if (key === null || hash === null) {
4213
- sendJson(res, 400, { error: "invalid snapshot key or blob hash" });
4214
- return;
4215
- }
4216
- if (!SHA256_HEX_RE.test(hash)) {
4217
- sendJson(res, 400, { error: "blob hash must be 64 lowercase-hex chars" });
4218
- return;
4219
- }
4220
- const fs = deps.fileSnapshotStore;
4221
- const manifest = await fs?.exportManifest?.(sessionId, key);
4222
- if (!manifest || ![...manifest.values()].includes(hash)) {
4223
- sendJson(res, 404, { error: "blob not found" });
4224
- return;
4225
- }
4226
- const bytes = await fs?.getBlob?.(hash);
4227
- if (!bytes) {
4228
- sendJson(res, 404, { error: "blob not found" });
4229
- return;
4230
- }
4231
- res.writeHead(200, { "content-type": "application/octet-stream" });
4232
- res.end(Buffer.from(bytes));
4233
- return;
4234
- }
4235
- if (sm[3] !== undefined && req.method === "PUT") {
4236
- if (!pushGateOk())
4237
- return;
4238
- const hash = safeDecode(sm[3]);
4239
- if (hash === null || !SHA256_HEX_RE.test(hash)) {
4240
- sendJson(res, 400, { error: "blob hash must be 64 lowercase-hex chars" });
4241
- return;
4242
- }
4243
- const fs = deps.fileSnapshotStore;
4244
- if (typeof fs?.putBlob !== "function") {
4245
- sendJson(res, 501, { error: "blob upload requires a durable snapshot store" });
4246
- return;
4247
- }
4248
- const body = await readRawBody(req, SYNC_BLOB_MAX_BYTES);
4249
- if (deps.snapshotBlobSqlCapBytes !== undefined && body.byteLength > deps.snapshotBlobSqlCapBytes) {
4250
- sendError(res, 413, "blob_too_large_for_sql", `blob of ${body.byteLength} bytes exceeds this deployment's SQL snapshot-blob cap (${deps.snapshotBlobSqlCapBytes} bytes — TiDB's txn-entry-size-limit is the low wall at 6MiB by default; the mysql-protocol packet limit sits above it) — configure MinIO object storage (MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY) for large snapshot blobs, or raise SNAPSHOT_BLOB_SQL_MAX_BYTES if your deployment lifted those limits`, { code: "blob_too_large_for_sql" });
4251
- return;
4252
- }
4253
- if (createHash("sha256").update(body).digest("hex") !== hash) {
4254
- sendJson(res, 400, { error: "blob hash mismatch" });
4255
- return;
4256
- }
4257
- const put = await fs.putBlob(hash, new Uint8Array(body));
4258
- if (!put.ok) {
4259
- sendError(res, 502, put.error.code, "blob store write failed", { code: put.error.code });
4260
- return;
4261
- }
4262
- res.writeHead(204).end();
4263
- return;
4264
- }
4265
- if (sm[2] === "import" && req.method === "POST") {
4266
- if (!pushGateOk())
4267
- return;
4268
- if (deps.sessionMirrorRuling && !scope.fleetWide && scope.gateOwner !== null) {
4269
- const auditPrincipal = scope.gateOwner;
4270
- void deps.sessionMirrorRuling(auditPrincipal).then((mirror) => {
4271
- if (mirror?.required) {
4272
- deps.metrics?.inc("session_mirror_required_push_total");
4273
- deps.logger?.info("session_mirror_required_push_observed", { principal: auditPrincipal, engineUrl: mirror.engineUrl, sessionId });
4274
- }
4275
- }).catch(() => { });
4276
- }
4277
- if (!deps.sessionStorage?.beginImportStaging) {
4278
- sendJson(res, 501, { error: "staged session import requires a durable session store" });
4279
- return;
4280
- }
4281
- let body;
4282
- try {
4283
- body = (await readJson(req));
4284
- }
4285
- catch {
4286
- sendJson(res, 400, { error: "invalid JSON body" });
4287
- return;
4288
- }
4289
- if (!Array.isArray(body.entryIds) || !body.entryIds.every((x) => typeof x === "string")) {
4290
- sendJson(res, 400, { error: "entryIds must be an array of strings" });
4291
- return;
4292
- }
4293
- const snapshotsOk = Array.isArray(body.snapshots) && body.snapshots.every((s) => typeof s === "object" && s !== null && typeof s.key === "string" && Array.isArray(s.manifest));
4294
- if (!snapshotsOk || !Array.isArray(body.policy) || !Array.isArray(body.anchors)) {
4295
- sendJson(res, 400, { error: "malformed import metadata" });
4296
- return;
4297
- }
4298
- if (body.resolution !== undefined && body.resolution !== "overwrite-dst") {
4299
- sendJson(res, 400, { error: "resolution must be 'overwrite-dst'" });
4300
- return;
4301
- }
4302
- const resolution = body.resolution;
4303
- const srcLogDigest = typeof body.logDigest === "string" && body.logDigest.length > 0 ? body.logDigest : undefined;
4304
- const entryIds = body.entryIds;
4305
- const snapshots = body.snapshots;
4306
- const policy = body.policy;
4307
- const anchors = body.anchors;
4308
- const dstIds = (await deps.sessionStorage.listEntryIds?.(sessionId)) ?? null;
4309
- const relA = classifySyncRelationshipByIds(entryIds, dstIds);
4310
- if ((relA.relation === "fork" || relA.relation === "stale") && resolution !== "overwrite-dst") {
4311
- sendError(res, 409, "conflict", "sync conflict", { relation: relA });
4312
- return;
4313
- }
4314
- let payloadVerified = false;
4315
- let digestSaysDiffer = false;
4316
- if (relA.relation === "identical" && srcLogDigest !== undefined) {
4317
- const dstEntries = await deps.sessionStorage.exportEntries?.(sessionId).catch(() => null) ?? null;
4318
- const dstDigest = dstEntries === null ? undefined : sessionLogDigest(dstEntries);
4319
- if (sessionLogDigestsComparable(srcLogDigest, dstDigest)) {
4320
- if (srcLogDigest === dstDigest)
4321
- payloadVerified = true;
4322
- else
4323
- digestSaysDiffer = true;
4324
- }
4325
- if (!payloadVerified)
4326
- digestSaysDiffer = true;
4327
- }
4328
- if (relA.relation === "identical" && !digestSaysDiffer) {
4329
- const fss = deps.fileSnapshotStore;
4330
- if (fss?.importManifest && fss.getBlob && snapshots.length > 0) {
4331
- const getBlobA = (h) => fss.getBlob(h);
4332
- for (const snap of snapshots) {
4333
- const r = await fss.importManifest(sessionId, snap.key, new Map(snap.manifest), getBlobA);
4334
- if (!r.ok) {
4335
- sendError(res, 422, r.error.code === "restore_failed" ? "restore_failed" : "missing_blob", `snapshot import failed (${r.error.code})`);
4336
- return;
4337
- }
4338
- }
4339
- }
4340
- const opOk = explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals);
4341
- if (deps.sessionPolicyStore)
4342
- for (const rec of policy)
4343
- await deps.sessionPolicyStore.putRules(sessionId, rec.principal, rec.rules, { operator: opOk });
4344
- const importingP = scope.fleetWide ? (owner ?? null) : scope.gateOwner;
4345
- if (deps.resumeAnchorStore)
4346
- for (const a of anchors)
4347
- await deps.resumeAnchorStore.put(sessionId, a.eventId, a.entryId, importingP);
4348
- sendJson(res, 200, {
4349
- relation: "identical",
4350
- basis: payloadVerified ? "entry-ids+digest" : "entry-ids",
4351
- payloadVerified,
4352
- });
4353
- return;
4354
- }
4355
- const activeTaskId = await deps.runStore?.getActiveTaskId(sessionId);
4356
- if (activeTaskId) {
4357
- sendError(res, 409, "session_active", "session has an active run; sync after it settles", { activeTaskId });
4358
- return;
4359
- }
4360
- const heldLease = importLeases.get(sessionId);
4361
- if (heldLease) {
4362
- const staleSecRaw = deps.config.syncImportLeaseStaleSec;
4363
- const staleSec = typeof staleSecRaw === "number" && Number.isFinite(staleSecRaw) ? staleSecRaw : 600;
4364
- const staleMs = staleSec * 1000;
4365
- const ageMs = Date.now() - heldLease.at;
4366
- if (staleMs > 0 && ageMs >= staleMs) {
4367
- deps.logger?.warn?.("sync_import_lease_taken_over", { sessionId, ageMs, staleMs, stagingId: heldLease.stagingId });
4368
- cleanupStaging(heldLease.stagingId);
4369
- importLeases.delete(sessionId);
4370
- }
4371
- else {
4372
- const retryAfterSec = staleMs > 0 ? Math.max(1, Math.ceil((staleMs - ageMs) / 1000)) : undefined;
4373
- sendError(res, 409, "import_in_flight", "an import is already in flight for this session"
4374
- + (retryAfterSec !== undefined ? ` (a stalled holder is taken over after ${staleSec}s of inactivity; retry in ~${retryAfterSec}s)` : ""), retryAfterSec !== undefined ? { retryAfterSec } : undefined);
4375
- return;
4376
- }
4377
- }
4378
- const fss = deps.fileSnapshotStore;
4379
- const canImportSnapshots = typeof fss?.importManifest === "function";
4380
- if (canImportSnapshots) {
4381
- const distinct = new Set();
4382
- for (const snap of snapshots)
4383
- for (const [, hash] of snap.manifest)
4384
- distinct.add(hash);
4385
- const all = [...distinct];
4386
- if (typeof fss?.hasBlobs === "function") {
4387
- const present = await fss.hasBlobs(all);
4388
- const missing = all.find((h) => !present.has(h));
4389
- if (missing !== undefined) {
4390
- sendError(res, 422, "missing_blob", "missing blob; PUT all referenced blobs before import");
4391
- return;
4392
- }
4393
- }
4394
- else {
4395
- for (const hash of all) {
4396
- if (!(await deps.fileSnapshotStore.getBlob(hash))) {
4397
- sendError(res, 422, "missing_blob", "missing blob; PUT all referenced blobs before import");
4398
- return;
4399
- }
4400
- }
4401
- }
4402
- }
4403
- const importingPrincipal = scope.fleetWide ? (owner ?? null) : scope.gateOwner;
4404
- const operatorOk = explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals);
4405
- const token = randomBytes(8).toString("hex");
4406
- const handle = deps.sessionStorage.beginImportStaging(sessionId, token);
4407
- const stagingId = handle.stagingId;
4408
- importLeases.set(sessionId, { at: Date.now(), stagingId });
4409
- pendingStagings.set(stagingId, {
4410
- handle, realSessionId: sessionId, owner: importingPrincipal, ...(resolution ? { resolution } : {}), operatorOk,
4411
- snapshots, policy, anchors, declaredEntryIds: entryIds, openedBy: scope.gateOwner, openedFleetWide: scope.fleetWide,
4412
- });
4413
- sendJson(res, 200, { stagingId, relation: relA.relation });
4414
- return;
4415
- }
4416
- if (sm[6] !== undefined && req.method === "POST") {
4417
- const stagingId = safeDecode(sm[6]);
4418
- if (stagingId === null) {
4419
- sendJson(res, 400, { error: "invalid staging id" });
4420
- return;
4421
- }
4422
- const pending = pendingStagings.get(stagingId);
4423
- if (!pending || pending.realSessionId !== sessionId) {
4424
- sendJson(res, 404, { error: "staging not found" });
4425
- return;
4426
- }
4427
- if (!pending.openedFleetWide && !scope.fleetWide && pending.openedBy !== scope.gateOwner) {
4428
- sendJson(res, 404, { error: "staging not found" });
4429
- return;
4430
- }
4431
- const getBlob = (h) => deps.fileSnapshotStore.getBlob(h);
4432
- const canImportSnapshots = typeof deps.fileSnapshotStore?.importManifest === "function";
4433
- const validator = new StreamingImportValidator();
4434
- let lineBuf = "";
4435
- let pendingBatch = [];
4436
- const flush = async () => { if (pendingBatch.length) {
4437
- await pending.handle.appendBatch(pendingBatch);
4438
- pendingBatch = [];
4439
- } };
4440
- class PrecheckError extends Error {
4441
- status;
4442
- constructor(status, msg) {
4443
- super(msg);
4444
- this.status = status;
4445
- }
4446
- }
4447
- const streamedIds = new Set();
4448
- const onEntry = (e) => {
4449
- if (!e || typeof e !== "object" || typeof e.id !== "string")
4450
- throw new PrecheckError(422, "malformed entry (missing id)");
4451
- streamedIds.add(e.id);
4452
- validator.step(e);
4453
- };
4454
- const utf8 = new StringDecoder("utf8");
4455
- try {
4456
- for await (const chunk of req) {
4457
- touchImportLease(pending.realSessionId);
4458
- lineBuf += utf8.write(chunk);
4459
- if (Buffer.byteLength(lineBuf, "utf8") > SYNC_NDJSON_LINE_MAX_BYTES)
4460
- throw new PrecheckError(413, "NDJSON line too large");
4461
- let nl;
4462
- while ((nl = lineBuf.indexOf("\n")) >= 0) {
4463
- const line = lineBuf.slice(0, nl).trim();
4464
- lineBuf = lineBuf.slice(nl + 1);
4465
- if (line.length === 0)
4466
- continue;
4467
- let e;
4468
- try {
4469
- e = JSON.parse(line);
4470
- }
4471
- catch {
4472
- throw new PrecheckError(400, "malformed NDJSON line");
4473
- }
4474
- onEntry(e);
4475
- pendingBatch.push(e);
4476
- if (pendingBatch.length >= SYNC_IMPORT_BATCH)
4477
- await flush();
4478
- }
4479
- }
4480
- lineBuf += utf8.end();
4481
- const tail = lineBuf.trim();
4482
- if (tail.length > 0) {
4483
- let e;
4484
- try {
4485
- e = JSON.parse(tail);
4486
- }
4487
- catch {
4488
- throw new PrecheckError(400, "malformed NDJSON line");
4489
- }
4490
- onEntry(e);
4491
- pendingBatch.push(e);
4492
- }
4493
- await flush();
4494
- validator.finish();
4495
- if (streamedIds.size !== pending.declaredEntryIds.length || !pending.declaredEntryIds.every((id) => streamedIds.has(id))) {
4496
- throw new PrecheckError(422, "streamed entries do not match the Phase-A declared entryIds");
4497
- }
4498
- }
4499
- catch (e) {
4500
- if (e instanceof PrecheckError) {
4501
- await pending.handle.abort().catch(() => undefined);
4502
- cleanupStaging(stagingId);
4503
- sendError(res, e.status, "invalid_entries", e.message);
4504
- return;
4505
- }
4506
- if (e instanceof SessionError && e.code === "invalid_session") {
4507
- await pending.handle.abort().catch(() => undefined);
4508
- cleanupStaging(stagingId);
4509
- sendError(res, 422, "invalid_entries", "staged session log failed validation");
4510
- return;
4511
- }
4512
- await pending.handle.abort().catch(() => undefined);
4513
- cleanupStaging(stagingId);
4514
- throw e;
4515
- }
4516
- const activeAtCommit = await deps.runStore?.getActiveTaskId(sessionId);
4517
- if (activeAtCommit) {
4518
- await pending.handle.abort().catch(() => undefined);
4519
- cleanupStaging(stagingId);
4520
- sendError(res, 409, "session_active", "session started an active run while the import streamed; sync after it settles", { activeTaskId: activeAtCommit });
4521
- return;
4522
- }
4523
- try {
4524
- const committed = await pending.handle.commit(pending.owner, pending.resolution ? { resolution: pending.resolution } : undefined);
4525
- if (canImportSnapshots) {
4526
- const importManifest = deps.fileSnapshotStore.importManifest.bind(deps.fileSnapshotStore);
4527
- for (const snap of pending.snapshots) {
4528
- const r = await importManifest(sessionId, snap.key, new Map(snap.manifest), getBlob);
4529
- if (!r.ok) {
4530
- sendError(res, 422, r.error.code === "restore_failed" ? "restore_failed" : "missing_blob", `snapshot import failed (${r.error.code})`);
4531
- cleanupStaging(stagingId);
4532
- return;
4533
- }
4534
- }
4535
- }
4536
- const policyStore = deps.sessionPolicyStore;
4537
- if (policyStore)
4538
- for (const rec of pending.policy)
4539
- await policyStore.putRules(sessionId, rec.principal, rec.rules, { operator: pending.operatorOk });
4540
- const anchorStore = deps.resumeAnchorStore;
4541
- if (anchorStore)
4542
- for (const a of pending.anchors)
4543
- await anchorStore.put(sessionId, a.eventId, a.entryId, pending.owner);
4544
- deps.sessionStorage.evict?.(sessionId);
4545
- cleanupStaging(stagingId);
4546
- sendJson(res, 200, { relation: committed.relation });
4547
- }
4548
- catch (e) {
4549
- if (e instanceof SyncConflictError) {
4550
- await pending.handle.abort().catch(() => undefined);
4551
- cleanupStaging(stagingId);
4552
- sendError(res, 409, "conflict", "sync conflict", { relation: e.relation });
4553
- return;
4554
- }
4555
- if (e instanceof SessionPolicyError) {
4556
- cleanupStaging(stagingId);
4557
- sendError(res, e.code === "loosen_forbidden" ? 403 : 409, e.code, e.message);
4558
- return;
4559
- }
4560
- cleanupStaging(stagingId);
4561
- throw e;
4562
- }
4563
- return;
4564
- }
4565
- if (sm[2] === "plan" && req.method === "POST") {
4566
- if (!pushGateOk())
4567
- return;
4568
- let body;
4569
- try {
4570
- body = (await readJson(req));
4571
- }
4572
- catch {
4573
- sendJson(res, 400, { error: "invalid JSON body" });
4574
- return;
4575
- }
4576
- if (!Array.isArray(body.entryIds) || !body.entryIds.every((x) => typeof x === "string")) {
4577
- sendJson(res, 400, { error: "entryIds must be an array of strings" });
4578
- return;
4579
- }
4580
- const dst = await deps.sessionStorage?.exportEntries?.(sessionId);
4581
- const relation = classifySyncRelationshipByIds(body.entryIds, dst ? dst.map((e) => e.id) : null);
4582
- sendJson(res, 200, relation);
4583
- return;
4584
- }
4585
- sendJson(res, 404, { error: "not found" });
4586
- return;
4587
- }
4588
- }
4589
- {
4590
- const am = /^\/v1\/attachments(?:\/([^/?]+))?$/.exec(url);
4591
- if (am) {
4592
- if (rateLimited(req, res) || quotaExceeded(req, res))
4593
- return;
4594
- const store = deps.taskAttachmentStore;
4595
- if (!store) {
4596
- sendJson(res, 501, { error: "attachment store not configured" });
4597
- return;
4598
- }
4599
- const principal = gatedPrincipal(req, deps.config);
4600
- if (deps.config.requirePrincipal && principal === undefined) {
4601
- sendJson(res, 401, { error: `missing principal header '${deps.config.principalHeader}'` });
4602
- return;
4603
- }
4604
- const owner = principal ?? "default";
4605
- const attId = am[1] !== undefined ? safeDecode(am[1]) : undefined;
4606
- if (attId === undefined && req.method === "POST") {
4607
- const q = new URL(req.url ?? "", "http://x").searchParams;
4608
- const rawName = q.get("name");
4609
- const base = ((rawName ?? "").split(/[/\\]/).filter(Boolean).pop() ?? "").slice(0, 128);
4610
- if (!rawName || base === "" || base === "." || base === ".." || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(base)) {
4611
- sendJson(res, 400, { error: "query param `name` is required and must reduce to a safe basename ([A-Za-z0-9._-], not starting with a dot)" });
4612
- return;
4613
- }
4614
- const name = sanitizePathComponent(base);
4615
- const mime = String(req.headers["content-type"] ?? "").split(";")[0].trim().toLowerCase();
4616
- if (!/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(mime) || mime.length > 128) {
4617
- sendJson(res, 400, { error: "content-type must be a well-formed mime type (type/subtype)" });
4618
- return;
4619
- }
4620
- const allow = deps.config.attachmentMimeAllowlist;
4621
- if (allow && !allow.some((a) => a === mime || (a.endsWith("/*") && mime.startsWith(a.slice(0, -1))))) {
4622
- sendError(res, 415, "attachment.mime_not_allowed", `mime '${mime}' is not in this deployment's allowlist`, { code: "attachment.mime_not_allowed", allowed: allow });
4623
- return;
4624
- }
4625
- const max = deps.config.attachmentMaxBytes ?? 32 * 1024 * 1024;
4626
- let body;
4627
- try {
4628
- body = await readRawBody(req, max);
4629
- }
4630
- catch (err) {
4631
- if (err instanceof HttpError && err.status === 413) {
4632
- sendError(res, 413, "attachment.too_large", `attachment exceeds the per-file limit (${max} bytes)`, { code: "attachment.too_large", maxBytes: max });
4633
- return;
4634
- }
4635
- throw err;
4636
- }
4637
- const rec = {
4638
- id: uuidv7(),
4639
- owner,
4640
- name,
4641
- mime,
4642
- sha256: createHash("sha256").update(body).digest("hex"),
4643
- sizeBytes: body.length,
4644
- createdAtMs: Date.now(),
4645
- };
4646
- await store.put({ ...rec, content: new Uint8Array(body) });
4647
- deps.metrics?.inc("attachment_uploads_total");
4648
- sendJson(res, 201, { id: rec.id, name: rec.name, mime: rec.mime, sha256: rec.sha256, sizeBytes: rec.sizeBytes });
4649
- return;
4650
- }
4651
- if (attId !== undefined && attId !== null && req.method === "GET") {
4652
- const meta = await store.get(owner, attId);
4653
- const bytes = meta ? await store.getContent(owner, attId) : null;
4654
- if (!meta || !bytes) {
4655
- sendJson(res, 404, { error: "not found" });
4656
- return;
4657
- }
4658
- res.writeHead(200, {
4659
- "content-type": meta.mime,
4660
- "content-length": String(bytes.byteLength),
4661
- "content-disposition": `attachment; filename*=UTF-8''${encodeURIComponent(meta.name)}`,
4662
- });
4663
- res.end(Buffer.from(bytes));
4664
- return;
4665
- }
4666
- if (attId !== undefined && attId !== null && req.method === "DELETE") {
4667
- const ok = await store.delete(owner, attId);
4668
- if (!ok) {
4669
- sendJson(res, 404, { error: "not found" });
4670
- return;
4671
- }
4672
- res.writeHead(204).end();
4673
- return;
4674
- }
4675
- sendJson(res, 404, { error: "not found" });
4676
- return;
4677
- }
4678
- }
4679
- if (req.method === "POST" && /^\/v1\/sessions\/[^/]+\/notify$/.test(url)) {
4680
- if (rateLimited(req, res) || quotaExceeded(req, res))
4681
- return;
4682
- const principal = gatedPrincipal(req, deps.config);
4683
- if (deps.config.requirePrincipal && principal === undefined) {
4684
- sendJson(res, 401, { error: `missing principal header '${deps.config.principalHeader}'` });
4685
- return;
4686
- }
4687
- const notifySession = decodeURIComponent(url.split("/")[3]);
4688
- let nb;
4689
- try {
4690
- nb = (await readJson(req));
4691
- }
4692
- catch {
4693
- sendJson(res, 400, { error: "body must be JSON: { task_id, status, summary, result?, seq?, source? }" });
4694
- return;
4695
- }
4696
- if (typeof nb.task_id !== "string" || nb.task_id.trim().length === 0 || nb.task_id.length > 190) {
4697
- sendJson(res, 400, { error: "task_id must be a non-empty string of at most 190 characters" });
4698
- return;
4699
- }
4700
- const NOTIFY_STATUSES = ["completed", "failed", "killed", "cancelled", "event"];
4701
- if (typeof nb.status !== "string" || !NOTIFY_STATUSES.includes(nb.status)) {
4702
- sendJson(res, 400, { error: `status must be one of ${NOTIFY_STATUSES.join("/")}` });
4703
- return;
4704
- }
4705
- if (typeof nb.summary !== "string" || nb.summary.trim().length === 0) {
4706
- sendJson(res, 400, { error: "summary must be a non-empty string" });
4707
- return;
4708
- }
4709
- if (nb.result !== undefined && typeof nb.result !== "string") {
4710
- sendJson(res, 400, { error: "result must be a string when present" });
4711
- return;
4712
- }
4713
- if (nb.seq !== undefined && (typeof nb.seq !== "number" || !Number.isInteger(nb.seq) || nb.seq < 1)) {
4714
- sendJson(res, 400, { error: "seq must be a positive integer when present" });
4715
- return;
4716
- }
4717
- if (nb.source !== undefined && (typeof nb.source !== "string" || nb.source.length > 190)) {
4718
- sendJson(res, 400, { error: "source must be a string of at most 190 characters when present" });
4719
- return;
4720
- }
4721
- if (!deps.sessionStorage?.ownerOf) {
4722
- sendJson(res, 501, { error: "external notify requires the session-ownership face (sessionStorage.ownerOf)" });
4723
- return;
4724
- }
4725
- const notifyOwner = await deps.sessionStorage.ownerOf(notifySession);
4726
- if (notifyOwner === undefined) {
4727
- sendJson(res, 404, { error: "session not found" });
4728
- return;
4729
- }
4730
- const notifyOperator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
4731
- if (!notifyOperator && notifyOwner !== null && notifyOwner !== principal) {
4732
- sendJson(res, 404, { error: "session not found" });
4733
- return;
4734
- }
4735
- const payload = {
4736
- task_id: nb.task_id,
4737
- status: nb.status,
4738
- summary: nb.summary,
4739
- ...(nb.result !== undefined ? { result: nb.result } : {}),
4740
- ...(nb.seq !== undefined ? { seq: nb.seq } : {}),
4741
- ...(nb.source !== undefined ? { source: nb.source } : {}),
4742
- };
4743
- const liveTaskId = await deps.runStore?.getActiveTaskId?.(notifySession).catch(() => undefined);
4744
- const liveStream = liveTaskId !== undefined && liveTaskId !== null ? steerableRuns.get(liveTaskId) : undefined;
4745
- if (liveStream) {
4746
- try {
4747
- await liveStream.notify(payload);
4748
- sendJson(res, 200, { sessionId: notifySession, delivery: "live" });
4749
- return;
4750
- }
4751
- catch (e) {
4752
- const code = e.code;
4753
- if (typeof code === "string" && code.startsWith("notify.")) {
4754
- sendError(res, 400, code, e instanceof Error ? e.message : "invalid notification");
4755
- return;
4756
- }
4757
- }
4758
- }
4759
- if (!deps.workflowCompletionInbox) {
4760
- sendJson(res, 501, { error: "external notify park requires the workflow completion inbox (WORKFLOW_RUN_STORE)" });
4761
- return;
4762
- }
4763
- await deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(notifySession, notifyOwner ?? principal ?? null, { task_id: payload.task_id, task_type: "external", status: payload.status, summary: payload.summary, ...(payload.result !== undefined ? { result: payload.result } : {}), ...(payload.seq !== undefined ? { seq: payload.seq } : {}), ...(payload.source !== undefined ? { source: payload.source } : {}) }, Date.now()));
4764
- sendJson(res, 202, { sessionId: notifySession, delivery: "parked", note: "no live stream — parked in the session inbox; drained as a task_notification on the next stream open" });
4765
- return;
4766
- }
4767
- if (req.method === "POST" && /^\/v1\/sessions\/[^/]+\/wake$/.test(url)) {
4768
- if (!deps.checkpointStore) {
4769
- sendJson(res, 501, { error: "wake requires the checkpoint store" });
4770
- return;
4771
- }
4772
- if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
2833
+ if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
4773
2834
  return;
4774
2835
  const principal = gatedPrincipal(req, deps.config);
4775
2836
  if (deps.config.requirePrincipal && principal === undefined) {
@@ -6045,35 +4106,6 @@ async function handleTaskList(res, runStore, query, forceOwner) {
6045
4106
  const nextCursor = hasMore && last ? encodeCursor({ createdAt: last.createdAt, taskId: last.taskId }) : undefined;
6046
4107
  sendJson(res, 200, { tasks, ...(nextCursor ? { nextCursor } : {}) });
6047
4108
  }
6048
- function encodeSessionCursor(c) {
6049
- return Buffer.from(JSON.stringify(c)).toString("base64url");
6050
- }
6051
- function decodeSessionCursor(s) {
6052
- if (!s)
6053
- return undefined;
6054
- try {
6055
- const o = JSON.parse(Buffer.from(s, "base64url").toString());
6056
- return o.lastActivityAt && o.sessionId && !Number.isNaN(Date.parse(o.lastActivityAt))
6057
- ? { lastActivityAt: o.lastActivityAt, sessionId: o.sessionId }
6058
- : undefined;
6059
- }
6060
- catch {
6061
- return undefined;
6062
- }
6063
- }
6064
- async function handleSessionList(res, lister, query, forceOwner) {
6065
- const owner = forceOwner != null ? forceOwner : (query.get("owner") ?? undefined);
6066
- const limit = Math.min(100, Math.max(1, Number(query.get("limit") ?? 50) || 50));
6067
- const cursor = decodeSessionCursor(query.get("cursor"));
6068
- const qRaw = (query.get("q") ?? "").trim();
6069
- const q = qRaw ? qRaw.slice(0, 256) : undefined;
6070
- const rows = await lister({ ...(owner ? { owner } : {}), ...(cursor ? { cursor } : {}), limit: limit + 1, ...(q ? { q } : {}) });
6071
- const hasMore = rows.length > limit;
6072
- const page = hasMore ? rows.slice(0, limit) : rows;
6073
- const last = page[page.length - 1];
6074
- const nextCursor = hasMore && last ? encodeSessionCursor({ lastActivityAt: last.lastActivityAt, sessionId: last.sessionId }) : undefined;
6075
- sendJson(res, 200, { sessions: page, ...(nextCursor ? { nextCursor } : {}) });
6076
- }
6077
4109
  async function handleTaskArtifacts(res, runStore, taskId) {
6078
4110
  const run = await runStore.getRun(taskId);
6079
4111
  if (!run) {
@@ -6210,60 +4242,6 @@ export async function streamApprovals(req, res, cs, scope, pollMs = APPROVALS_ST
6210
4242
  }
6211
4243
  res.end();
6212
4244
  }
6213
- async function streamSseLog(req, res, provider, id, staleMs) {
6214
- const lastId = Number(req.headers["last-event-id"] ?? new URL(req.url ?? "", "http://x").searchParams.get("from") ?? 0);
6215
- let from = Number.isFinite(lastId) ? lastId : 0;
6216
- if (from > 0) {
6217
- const retainedFrom = await provider.retainedFrom(id);
6218
- if (retainedFrom > from + 1) {
6219
- sendJson(res, 416, { error: "resume point evicted past retention", retainedFrom });
6220
- return;
6221
- }
6222
- }
6223
- sseHeaders(res);
6224
- let closed = false;
6225
- req.on("close", () => {
6226
- closed = true;
6227
- });
6228
- const start = Date.now();
6229
- const MAX_MS = 15 * 60 * 1000;
6230
- let lastBeat = Date.now();
6231
- const write = (events) => {
6232
- for (const ev of events) {
6233
- const f = provider.formatEvent(ev);
6234
- res.write(`id: ${f.id}\nevent: ${f.event}\ndata: ${JSON.stringify(f.data)}\n\n`);
6235
- from = ev.seq;
6236
- }
6237
- };
6238
- while (!closed) {
6239
- const [events, row] = await Promise.all([provider.getEvents(id, from), provider.statusOf(id)]);
6240
- write(events);
6241
- if (!row)
6242
- break;
6243
- if (row.status !== "running") {
6244
- write(await provider.getEvents(id, from));
6245
- break;
6246
- }
6247
- const stale = Date.now() - new Date(row.updatedAt).getTime() > staleMs;
6248
- if (stale && events.length === 0) {
6249
- const sf = provider.staleFrame();
6250
- res.write(`event: ${sf.event}\ndata: ${JSON.stringify(sf.data)}\n\n`);
6251
- break;
6252
- }
6253
- if (Date.now() - start > MAX_MS) {
6254
- res.write(`event: error\ndata: ${JSON.stringify({ type: "error", code: "STREAM_MAX_DURATION", message: "stream reached its 15-minute cap — reconnect with Last-Event-ID to continue (the run is still active)" })}\n\n`);
6255
- break;
6256
- }
6257
- if (events.length === 0) {
6258
- if (Date.now() - lastBeat > 15_000) {
6259
- res.write(`event: heartbeat\ndata: {}\n\n`);
6260
- lastBeat = Date.now();
6261
- }
6262
- await sleep(250);
6263
- }
6264
- }
6265
- res.end();
6266
- }
6267
4245
  async function streamRunEvents(req, res, runStore, taskId, staleMs) {
6268
4246
  await streamSseLog(req, res, {
6269
4247
  statusOf: async (id) => {
@@ -6276,210 +4254,6 @@ async function streamRunEvents(req, res, runStore, taskId, staleMs) {
6276
4254
  staleFrame: () => ({ event: "failed", data: { type: "failed", errorMessage: "run stalled (instance lost?)" } }),
6277
4255
  }, taskId, staleMs);
6278
4256
  }
6279
- async function streamBakeEvents(req, res, bakes, bakeId, staleMs) {
6280
- await streamSseLog(req, res, {
6281
- statusOf: async (id) => {
6282
- const bake = await bakes.getBake(id);
6283
- return bake ? { status: bake.status, updatedAt: bake.updatedAt } : undefined;
6284
- },
6285
- getEvents: (id, after) => bakes.getEvents(id, after),
6286
- retainedFrom: (id) => bakes.retainedFrom(id),
6287
- formatEvent: (ev) => ({
6288
- id: ev.seq,
6289
- event: ev.kind,
6290
- data: { kind: ev.kind, seq: ev.seq, ts: ev.ts, ...(ev.level ? { level: ev.level } : {}), ...(ev.data ?? {}) },
6291
- }),
6292
- staleFrame: () => ({ event: "done", data: { kind: "done", status: "FAILED", error: "bake-runner lost (no heartbeat)", digest: null, indexId: null, repo: null, ref: null } }),
6293
- }, bakeId, staleMs);
6294
- }
6295
- function bakeView(b) {
6296
- return {
6297
- bakeId: b.bakeId,
6298
- status: b.status,
6299
- state: b.state,
6300
- profile: b.profile,
6301
- bands: b.bands,
6302
- baseRef: b.baseRef,
6303
- push: b.push,
6304
- dryRun: b.dryRun,
6305
- logs: b.logs,
6306
- digest: b.digest,
6307
- repo: b.repo,
6308
- ref: b.ref,
6309
- indexId: b.indexId,
6310
- exitCode: b.exitCode,
6311
- error: b.error,
6312
- errorCode: b.errorCode,
6313
- manifestSha: b.manifestSha,
6314
- tag: b.tag,
6315
- requestedBy: b.requestedBy,
6316
- createdAt: b.createdAt,
6317
- updatedAt: b.updatedAt,
6318
- };
6319
- }
6320
- function claimResponse(claimed) {
6321
- return {
6322
- bakeId: claimed.bakeId,
6323
- argv: claimed.argv,
6324
- push: claimed.push,
6325
- dryRun: claimed.dryRun,
6326
- logs: claimed.logs,
6327
- profile: claimed.profile,
6328
- bands: claimed.bands,
6329
- ingestSecret: claimed.ingestSecret,
6330
- leaseUntil: claimed.leaseUntil,
6331
- };
6332
- }
6333
- async function ingestBakeLine(bakes, idx, bake, line, registry) {
6334
- const bakeId = bake.bakeId;
6335
- const event = typeof line.event === "string" ? line.event : "";
6336
- const lineOrd = typeof line.lineOrd === "number" ? line.lineOrd : typeof line.line_ord === "number" ? line.line_ord : null;
6337
- if (event !== "done") {
6338
- if (event === "state") {
6339
- const state = typeof line.state === "string" ? line.state : null;
6340
- if (state)
6341
- await bakes.setState(bakeId, state);
6342
- const seq = await bakes.appendEvent(bakeId, "state", { state }, { lineOrd });
6343
- return { status: 200, body: { accepted: true, seq } };
6344
- }
6345
- const kind = event === "manifest" ? "manifest" : event;
6346
- const level = event === "log" && line.level === "raw" ? "raw" : null;
6347
- const { event: _e, lineOrd: _lo, line_ord: _li, ...data } = line;
6348
- const seq = await bakes.appendEvent(bakeId, kind, redactDeep(data), { lineOrd, level });
6349
- return { status: 200, body: { accepted: true, seq } };
6350
- }
6351
- const doneStatus = typeof line.status === "string" ? line.status : "FAILED";
6352
- const exitCode = typeof line.exitCode === "number" ? line.exitCode : doneStatus === "COMPLETE" ? 0 : null;
6353
- const cancelled = doneStatus === "CANCELLED";
6354
- const errorText = typeof line.error === "string" ? redactSecrets(line.error) : null;
6355
- const tag = typeof line.tag === "string" ? line.tag : bake.tag;
6356
- let digest = typeof line.digest === "string" && line.digest.length > 0 ? line.digest : null;
6357
- let indexId = bake.indexId;
6358
- let repo = bake.repo;
6359
- let ref = bake.ref;
6360
- let manifestSha = bake.manifestSha;
6361
- let finalStatus = doneStatus;
6362
- let finalError = errorText;
6363
- let finalExit = exitCode;
6364
- let registerDowngraded = false;
6365
- const registerSteps = [];
6366
- if (doneStatus === "COMPLETE" && digest && idx) {
6367
- const manifestBody = (line.manifestBody ?? line.manifest);
6368
- const fresh = await bakes.getBake(bakeId);
6369
- const nonTerminal = !!fresh && (fresh.status === "queued" || fresh.status === "running");
6370
- if (manifestBody && typeof manifestBody === "object" && nonTerminal) {
6371
- const profile = bake.profile && bake.profile !== "custom" ? bake.profile : (manifestBody.profile && manifestBody.profile !== "custom" ? manifestBody.profile : "custom");
6372
- repo = registry ? `${registry}/${profile}` : `sema-images/${profile}`;
6373
- manifestSha = typeof line.manifestSha === "string" ? line.manifestSha : (typeof line.manifest?.sha256 === "string" ? String(line.manifest.sha256) : bake.manifestSha);
6374
- if (pgHasUnstorable(digest) || pgHasUnstorable(repo) || pgHasUnstorable(manifestSha)) {
6375
- indexId = null;
6376
- registerSteps.push({ step: "register", status: "skipped", reason: "identity fields contained unstorable bytes" });
6377
- }
6378
- else {
6379
- registerSteps.push({ step: "register", status: "running" });
6380
- try {
6381
- const entry = manifestToIndexEntry(manifestBody, { repo, digest, status: "building", manifestSha, sizeBytes: manifestBody.sizeBytes ?? null, signed: false });
6382
- entry.visibility = "public";
6383
- entry.tenantId = null;
6384
- indexId = await idx.registerBuilding(entry);
6385
- ref = `${repo}@${digest}`;
6386
- const bound = await bakes.setIndexId(bakeId, indexId);
6387
- if (!bound)
6388
- indexId = null;
6389
- registerSteps.push({ step: "register", status: "ok" });
6390
- }
6391
- catch (err) {
6392
- indexId = null;
6393
- ref = `${repo}@${digest}`;
6394
- finalStatus = "FAILED";
6395
- registerDowngraded = true;
6396
- finalExit = exitCode ?? 0;
6397
- finalError = redactSecrets(`index register failed after a COMPLETE build (built-but-unregistered partial): ${String(err?.message ?? err)}`);
6398
- registerSteps.push({ step: "register", status: "fail", reason: "index register failed (TiDB?)" });
6399
- }
6400
- }
6401
- }
6402
- }
6403
- const suppliedCode = typeof line.errorCode === "string" && RUNNER_ERROR_CODES.has(line.errorCode)
6404
- ? line.errorCode
6405
- : undefined;
6406
- const errorCode = cancelled
6407
- ? "cancelled"
6408
- : suppliedCode !== undefined && !registerDowngraded
6409
- ? suppliedCode
6410
- : errorCodeForExit(finalExit, cancelled);
6411
- const conflictBands = errorCode === "band-conflict" && Array.isArray(line.conflictBands)
6412
- ? line.conflictBands.filter((b) => typeof b === "string")
6413
- : null;
6414
- const identityInvalid = (typeof digest === "string" && pgHasUnstorable(digest)) ||
6415
- (typeof repo === "string" && pgHasUnstorable(repo)) ||
6416
- (typeof ref === "string" && pgHasUnstorable(ref)) ||
6417
- (typeof manifestSha === "string" && pgHasUnstorable(manifestSha));
6418
- if (identityInvalid) {
6419
- finalStatus = "FAILED";
6420
- digest = null;
6421
- repo = null;
6422
- ref = null;
6423
- manifestSha = null;
6424
- indexId = null;
6425
- finalError = finalError ? `${finalError}; identity fields contained unstorable bytes` : "terminal frame identity fields contained unstorable bytes";
6426
- }
6427
- const finalState = (finalStatus === "COMPLETE" ? "COMPLETE" : finalStatus === "CANCELLED" ? "CANCELLED" : "FAILED");
6428
- const coarse = coarseStatusForState(finalState);
6429
- let affectedRows = 0;
6430
- const terminal = {
6431
- status: coarse === "done" ? "done" : "failed",
6432
- state: finalState,
6433
- digest,
6434
- repo,
6435
- ref,
6436
- indexId,
6437
- exitCode: finalExit,
6438
- error: finalError,
6439
- errorCode,
6440
- manifestSha,
6441
- tag,
6442
- };
6443
- let lastErr;
6444
- for (let attempt = 0; attempt < 3; attempt += 1) {
6445
- try {
6446
- affectedRows = await bakes.setTerminal(bakeId, terminal);
6447
- lastErr = undefined;
6448
- break;
6449
- }
6450
- catch (err) {
6451
- lastErr = err;
6452
- }
6453
- }
6454
- if (lastErr !== undefined) {
6455
- return { status: 200, body: { accepted: true, terminal: false, needsFlip: true } };
6456
- }
6457
- if (affectedRows >= 1) {
6458
- for (const s of registerSteps)
6459
- await bakes.appendEvent(bakeId, "step", s);
6460
- await bakes.appendEvent(bakeId, "done", {
6461
- status: finalStatus,
6462
- digest,
6463
- repo,
6464
- ref,
6465
- indexId,
6466
- tag,
6467
- manifestSha,
6468
- sizeBytes: typeof line.sizeBytes === "number" ? line.sizeBytes : null,
6469
- exitCode: finalExit,
6470
- error: finalError,
6471
- errorCode,
6472
- ...(conflictBands ? { conflictBands } : {}),
6473
- }, { lineOrd });
6474
- }
6475
- return { status: 200, body: { accepted: true, terminal: affectedRows >= 1, indexId } };
6476
- }
6477
- function sleep(ms) {
6478
- return new Promise((r) => setTimeout(r, ms));
6479
- }
6480
- function headerStr(v) {
6481
- return Array.isArray(v) ? v[0] : v;
6482
- }
6483
4257
  function isBillableSubmitPath(url) {
6484
4258
  return (url === "/v1/side-query" ||
6485
4259
  url === "/v1/tasks" ||
@@ -6745,14 +4519,6 @@ function streamFleet(req, res, bus, callerScope, callerSession = null, completio
6745
4519
  res.on("close", shutdown);
6746
4520
  });
6747
4521
  }
6748
- function sseHeaders(res, extra) {
6749
- res.writeHead(200, {
6750
- "content-type": "text/event-stream",
6751
- "cache-control": "no-cache",
6752
- connection: "keep-alive",
6753
- ...extra,
6754
- });
6755
- }
6756
4522
  function routeLabel(_method, url) {
6757
4523
  if (RUN_STEER_RE.test(url))
6758
4524
  return "/v1/runs/:id/steer";
@@ -6799,43 +4565,6 @@ function routeLabel(_method, url) {
6799
4565
  return url;
6800
4566
  return "other";
6801
4567
  }
6802
- function workspaceContentType(relPath) {
6803
- const base = relPath.slice(relPath.lastIndexOf("/") + 1);
6804
- const dot = base.lastIndexOf(".");
6805
- const ext = dot > 0 ? base.slice(dot + 1).toLowerCase() : "";
6806
- const TABLE = {
6807
- txt: "text/plain; charset=utf-8", md: "text/plain; charset=utf-8", log: "text/plain; charset=utf-8",
6808
- json: "application/json; charset=utf-8", csv: "text/csv; charset=utf-8",
6809
- png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp",
6810
- pdf: "application/pdf",
6811
- };
6812
- return TABLE[ext] ?? "application/octet-stream";
6813
- }
6814
- function looksBinary(bytes) {
6815
- const n = Math.min(bytes.byteLength, 8192);
6816
- for (let i = 0; i < n; i++)
6817
- if (bytes[i] === 0)
6818
- return true;
6819
- return false;
6820
- }
6821
- function authorized(req, token) {
6822
- const h = req.headers.authorization ?? "";
6823
- return safeEqual(h, `Bearer ${token}`) || safeEqual(h, `token ${token}`);
6824
- }
6825
- function systemFor(req, config) {
6826
- for (const [tok, system] of Object.entries(config.authTokens ?? {})) {
6827
- if (authorized(req, tok))
6828
- return system;
6829
- }
6830
- if (config.authToken && authorized(req, config.authToken))
6831
- return null;
6832
- return undefined;
6833
- }
6834
- function safeEqual(a, b) {
6835
- const ab = Buffer.from(a, "utf8");
6836
- const bb = Buffer.from(b, "utf8");
6837
- return ab.length === bb.length && timingSafeEqual(ab, bb);
6838
- }
6839
4568
  function cors(res, req, origins, principalHeader) {
6840
4569
  if (!origins || origins.length === 0)
6841
4570
  return;
@@ -6854,14 +4583,4 @@ function cors(res, req, origins, principalHeader) {
6854
4583
  res.setHeader("access-control-allow-methods", "GET, POST, OPTIONS");
6855
4584
  res.setHeader("access-control-allow-headers", `content-type, authorization, last-event-id, ${principalHeader}`);
6856
4585
  }
6857
- function sendJson(res, status, body) {
6858
- res.writeHead(status, { "content-type": "application/json" });
6859
- res.end(JSON.stringify(body));
6860
- }
6861
- function sendError(res, status, errorCode, message, extra) {
6862
- sendJson(res, status, { error: message, errorCode, ...extra });
6863
- }
6864
- function msg(e) {
6865
- return e instanceof Error ? e.message : String(e);
6866
- }
6867
4586
  //# sourceMappingURL=server.js.map