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