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