@sema-agent/server 1.316.0 → 1.317.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/README.md +9 -0
- package/README.zh-CN.md +7 -0
- package/dist/config-types.d.ts +1 -1
- package/dist/config.d.ts +14 -0
- package/dist/config.js +148 -45
- package/dist/elicitation.js +2 -2
- package/dist/http/route-ctx.d.ts +51 -2
- package/dist/http/routes/approvals-assistant.d.ts +11 -0
- package/dist/http/routes/approvals-assistant.js +530 -0
- package/dist/http/routes/attachments.js +7 -7
- package/dist/http/routes/fleet.d.ts +4 -0
- package/dist/http/routes/fleet.js +147 -0
- package/dist/http/routes/images.js +29 -29
- package/dist/http/routes/leader.d.ts +4 -0
- package/dist/http/routes/leader.js +48 -0
- package/dist/http/routes/memory-policy.js +10 -10
- package/dist/http/routes/notify-wake.d.ts +4 -0
- package/dist/http/routes/notify-wake.js +133 -0
- package/dist/http/routes/observability.js +5 -5
- package/dist/http/routes/runs.d.ts +19 -0
- package/dist/http/routes/runs.js +967 -0
- package/dist/http/routes/session-sync.js +28 -28
- package/dist/http/routes/sessions-list.js +8 -8
- package/dist/http/routes/sessions.js +47 -47
- package/dist/http/routes/side-query.d.ts +4 -0
- package/dist/http/routes/side-query.js +88 -0
- package/dist/http/routes/tasks.d.ts +4 -0
- package/dist/http/routes/tasks.js +632 -0
- package/dist/http/routes/trace-usage.d.ts +4 -0
- package/dist/http/routes/trace-usage.js +239 -0
- package/dist/http/routes/workflows.d.ts +5 -0
- package/dist/http/routes/workflows.js +337 -0
- package/dist/http/run-meta.d.ts +11 -0
- package/dist/http/run-meta.js +16 -0
- package/dist/http/send.d.ts +1 -0
- package/dist/http/send.js +15 -0
- package/dist/http/server.d.ts +6 -5
- package/dist/http/server.js +241 -3166
- package/dist/http/sse-log.js +2 -2
- package/dist/http/wire-types.d.ts +6 -0
- package/dist/leader/endpoint.js +4 -4
- package/dist/main.js +5 -5
- package/dist/plugins/remote-env-host.js +4 -3
- package/dist/question.js +2 -2
- package/dist/run-local.js +1 -1
- package/dist/tool-approval.js +2 -2
- package/dist/trace/ledger-sink.js +1 -1
- package/dist/trace/project.d.ts +1 -0
- package/dist/trace/project.js +3 -0
- package/package.json +1 -1
|
@@ -40,35 +40,35 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
40
40
|
if (sm) {
|
|
41
41
|
const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
|
|
42
42
|
if (!deps.backend || !ownerOf) {
|
|
43
|
-
|
|
43
|
+
sendError(res, 501, "capability.session_store_required", "session sync requires a durable session store");
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
46
|
const scope = sessionOwnerScopeForWrite(req);
|
|
47
47
|
if (!scope.fleetWide && scope.gateOwner === null) {
|
|
48
|
-
|
|
48
|
+
sendError(res, 401, "auth.unauthorized", "unauthorized");
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
const sessionId = safeDecode(sm[1]);
|
|
52
52
|
if (sessionId === null || !isUuidShape(sessionId)) {
|
|
53
|
-
|
|
53
|
+
sendError(res, 400, "request.id_invalid", "invalid session id (must be a canonical uuid)");
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
56
|
const owner = await ownerOf(sessionId);
|
|
57
57
|
const ownsOrFleet = (o) => scope.fleetWide || o === scope.gateOwner;
|
|
58
58
|
const pullGateOk = () => {
|
|
59
59
|
if (owner === undefined) {
|
|
60
|
-
|
|
60
|
+
sendError(res, 404, "not_found.session", "session not found");
|
|
61
61
|
return false;
|
|
62
62
|
}
|
|
63
63
|
if (!ownsOrFleet(owner)) {
|
|
64
|
-
|
|
64
|
+
sendError(res, 404, "not_found.session", "session not found");
|
|
65
65
|
return false;
|
|
66
66
|
}
|
|
67
67
|
return true;
|
|
68
68
|
};
|
|
69
69
|
const pushGateOk = () => {
|
|
70
70
|
if (owner !== undefined && !ownsOrFleet(owner)) {
|
|
71
|
-
|
|
71
|
+
sendError(res, 404, "not_found.session", "session not found");
|
|
72
72
|
return false;
|
|
73
73
|
}
|
|
74
74
|
return true;
|
|
@@ -78,7 +78,7 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
78
78
|
return;
|
|
79
79
|
const out = await exportSessionManifest(sessionId, deps.backend);
|
|
80
80
|
if (out === null) {
|
|
81
|
-
|
|
81
|
+
sendError(res, 404, "not_found.session", "session not found");
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
84
|
sendJson(res, 200, { manifest: out.manifest });
|
|
@@ -92,19 +92,19 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
92
92
|
if (afterSeqRaw !== null) {
|
|
93
93
|
const n = Number(afterSeqRaw);
|
|
94
94
|
if (!Number.isInteger(n) || n < 0) {
|
|
95
|
-
|
|
95
|
+
sendError(res, 400, "request.query_invalid", "afterSeq must be a non-negative integer");
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
98
|
afterSeq = n;
|
|
99
99
|
}
|
|
100
100
|
const stream = deps.sessionStorage?.exportEntriesStream?.bind(deps.sessionStorage);
|
|
101
101
|
if (!stream) {
|
|
102
|
-
|
|
102
|
+
sendError(res, 501, "capability.session_store_required", "session entry streaming requires a durable session store");
|
|
103
103
|
return;
|
|
104
104
|
}
|
|
105
105
|
const it = await stream(sessionId, afterSeq === undefined ? undefined : { afterSeq });
|
|
106
106
|
if (it === null) {
|
|
107
|
-
|
|
107
|
+
sendError(res, 404, "not_found.session", "session not found");
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
110
|
res.writeHead(200, { "content-type": "application/x-ndjson", "transfer-encoding": "chunked" });
|
|
@@ -132,22 +132,22 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
132
132
|
const key = safeDecode(sm[4]);
|
|
133
133
|
const hash = safeDecode(sm[5]);
|
|
134
134
|
if (key === null || hash === null) {
|
|
135
|
-
|
|
135
|
+
sendError(res, 400, "request.path_malformed", "invalid snapshot key or blob hash");
|
|
136
136
|
return;
|
|
137
137
|
}
|
|
138
138
|
if (!SHA256_HEX_RE.test(hash)) {
|
|
139
|
-
|
|
139
|
+
sendError(res, 400, "request.id_invalid", "blob hash must be 64 lowercase-hex chars");
|
|
140
140
|
return;
|
|
141
141
|
}
|
|
142
142
|
const fs = deps.fileSnapshotStore;
|
|
143
143
|
const manifest = await fs?.exportManifest?.(sessionId, key);
|
|
144
144
|
if (!manifest || ![...manifest.values()].includes(hash)) {
|
|
145
|
-
|
|
145
|
+
sendError(res, 404, "not_found.blob", "blob not found");
|
|
146
146
|
return;
|
|
147
147
|
}
|
|
148
148
|
const bytes = await fs?.getBlob?.(hash);
|
|
149
149
|
if (!bytes) {
|
|
150
|
-
|
|
150
|
+
sendError(res, 404, "not_found.blob", "blob not found");
|
|
151
151
|
return;
|
|
152
152
|
}
|
|
153
153
|
res.writeHead(200, { "content-type": "application/octet-stream" });
|
|
@@ -159,12 +159,12 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
159
159
|
return;
|
|
160
160
|
const hash = safeDecode(sm[3]);
|
|
161
161
|
if (hash === null || !SHA256_HEX_RE.test(hash)) {
|
|
162
|
-
|
|
162
|
+
sendError(res, 400, "request.id_invalid", "blob hash must be 64 lowercase-hex chars");
|
|
163
163
|
return;
|
|
164
164
|
}
|
|
165
165
|
const fs = deps.fileSnapshotStore;
|
|
166
166
|
if (typeof fs?.putBlob !== "function") {
|
|
167
|
-
|
|
167
|
+
sendError(res, 501, "capability.snapshot_store_required", "blob upload requires a durable snapshot store");
|
|
168
168
|
return;
|
|
169
169
|
}
|
|
170
170
|
const body = await readRawBody(req, SYNC_BLOB_MAX_BYTES);
|
|
@@ -173,7 +173,7 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
173
173
|
return;
|
|
174
174
|
}
|
|
175
175
|
if (createHash("sha256").update(body).digest("hex") !== hash) {
|
|
176
|
-
|
|
176
|
+
sendError(res, 400, "request.hash_mismatch", "blob hash mismatch");
|
|
177
177
|
return;
|
|
178
178
|
}
|
|
179
179
|
const put = await fs.putBlob(hash, new Uint8Array(body));
|
|
@@ -197,7 +197,7 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
197
197
|
}).catch(() => { });
|
|
198
198
|
}
|
|
199
199
|
if (!deps.sessionStorage?.beginImportStaging) {
|
|
200
|
-
|
|
200
|
+
sendError(res, 501, "capability.session_store_required", "staged session import requires a durable session store");
|
|
201
201
|
return;
|
|
202
202
|
}
|
|
203
203
|
let body;
|
|
@@ -205,20 +205,20 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
205
205
|
body = (await readJson(req));
|
|
206
206
|
}
|
|
207
207
|
catch {
|
|
208
|
-
|
|
208
|
+
sendError(res, 400, "request.invalid_json", "invalid JSON body");
|
|
209
209
|
return;
|
|
210
210
|
}
|
|
211
211
|
if (!Array.isArray(body.entryIds) || !body.entryIds.every((x) => typeof x === "string")) {
|
|
212
|
-
|
|
212
|
+
sendError(res, 400, "request.field_invalid", "entryIds must be an array of strings");
|
|
213
213
|
return;
|
|
214
214
|
}
|
|
215
215
|
const snapshotsOk = Array.isArray(body.snapshots) && body.snapshots.every((s) => typeof s === "object" && s !== null && typeof s.key === "string" && Array.isArray(s.manifest));
|
|
216
216
|
if (!snapshotsOk || !Array.isArray(body.policy) || !Array.isArray(body.anchors)) {
|
|
217
|
-
|
|
217
|
+
sendError(res, 400, "request.body_shape", "malformed import metadata");
|
|
218
218
|
return;
|
|
219
219
|
}
|
|
220
220
|
if (body.resolution !== undefined && body.resolution !== "overwrite-dst") {
|
|
221
|
-
|
|
221
|
+
sendError(res, 400, "request.field_invalid", "resolution must be 'overwrite-dst'");
|
|
222
222
|
return;
|
|
223
223
|
}
|
|
224
224
|
const resolution = body.resolution;
|
|
@@ -338,16 +338,16 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
338
338
|
if (sm[6] !== undefined && req.method === "POST") {
|
|
339
339
|
const stagingId = safeDecode(sm[6]);
|
|
340
340
|
if (stagingId === null) {
|
|
341
|
-
|
|
341
|
+
sendError(res, 400, "request.id_invalid", "invalid staging id");
|
|
342
342
|
return;
|
|
343
343
|
}
|
|
344
344
|
const pending = pendingStagings.get(stagingId);
|
|
345
345
|
if (!pending || pending.realSessionId !== sessionId) {
|
|
346
|
-
|
|
346
|
+
sendError(res, 404, "not_found.staging", "staging not found");
|
|
347
347
|
return;
|
|
348
348
|
}
|
|
349
349
|
if (!pending.openedFleetWide && !scope.fleetWide && pending.openedBy !== scope.gateOwner) {
|
|
350
|
-
|
|
350
|
+
sendError(res, 404, "not_found.staging", "staging not found");
|
|
351
351
|
return;
|
|
352
352
|
}
|
|
353
353
|
const getBlob = (h) => deps.fileSnapshotStore.getBlob(h);
|
|
@@ -492,11 +492,11 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
492
492
|
body = (await readJson(req));
|
|
493
493
|
}
|
|
494
494
|
catch {
|
|
495
|
-
|
|
495
|
+
sendError(res, 400, "request.invalid_json", "invalid JSON body");
|
|
496
496
|
return;
|
|
497
497
|
}
|
|
498
498
|
if (!Array.isArray(body.entryIds) || !body.entryIds.every((x) => typeof x === "string")) {
|
|
499
|
-
|
|
499
|
+
sendError(res, 400, "request.field_invalid", "entryIds must be an array of strings");
|
|
500
500
|
return;
|
|
501
501
|
}
|
|
502
502
|
const dst = await deps.sessionStorage?.exportEntries?.(sessionId);
|
|
@@ -504,7 +504,7 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
504
504
|
sendJson(res, 200, relation);
|
|
505
505
|
return;
|
|
506
506
|
}
|
|
507
|
-
|
|
507
|
+
sendError(res, 404, "not_found.route", "not found");
|
|
508
508
|
return;
|
|
509
509
|
}
|
|
510
510
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isUuidV7 } from "../../security.js";
|
|
2
|
-
import { sendJson } from "../send.js";
|
|
2
|
+
import { sendJson, sendError } from "../send.js";
|
|
3
3
|
export async function handleSessionsList(req, res, url, ctx) {
|
|
4
4
|
const miss = { fell: false };
|
|
5
5
|
await handleSessionsListBody(req, res, url, ctx, miss);
|
|
@@ -11,12 +11,12 @@ async function handleSessionsListBody(req, res, url, ctx, miss) {
|
|
|
11
11
|
if (req.method === "GET" && url === "/v1/sessions") {
|
|
12
12
|
const { fleetWide, gateOwner } = sessionOwnerScope(req);
|
|
13
13
|
if (!fleetWide && gateOwner === null) {
|
|
14
|
-
|
|
14
|
+
sendError(res, 401, "auth.unauthorized", "unauthorized");
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
17
|
const lister = deps.sessionStorage?.listSessions?.bind(deps.sessionStorage) ?? deps.runStore?.listSessions?.bind(deps.runStore);
|
|
18
18
|
if (!lister) {
|
|
19
|
-
|
|
19
|
+
sendError(res, 501, "capability.session_store_required", "session list requires a durable session store (SESSION_BACKEND=tidb)");
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
await handleSessionList(res, lister, new URL(req.url ?? "", "http://x").searchParams, gateOwner);
|
|
@@ -25,23 +25,23 @@ async function handleSessionsListBody(req, res, url, ctx, miss) {
|
|
|
25
25
|
if (req.method === "GET" && url === "/v1/sendfile-links") {
|
|
26
26
|
const { fleetWide, gateOwner } = sessionOwnerScope(req);
|
|
27
27
|
if (!fleetWide && gateOwner === null) {
|
|
28
|
-
|
|
28
|
+
sendError(res, 401, "auth.unauthorized", "unauthorized");
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
31
|
if (!deps.sendFileLedger) {
|
|
32
|
-
|
|
32
|
+
sendError(res, 501, "capability.store_required", "sendfile ledger requires a store backend (DB_BACKEND=tidb|pg|local) and the SendUserFile issuer configured");
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
35
|
const query = new URL(req.url ?? "", "http://x").searchParams;
|
|
36
36
|
const limitRaw = query.get("limit");
|
|
37
37
|
if (limitRaw !== null && !(/^[0-9]+$/.test(limitRaw) && Number(limitRaw) >= 1 && Number(limitRaw) <= 100)) {
|
|
38
|
-
|
|
38
|
+
sendError(res, 400, "request.query_invalid", "limit must be a decimal integer in 1..100");
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
41
|
const limit = limitRaw === null ? 50 : Number(limitRaw);
|
|
42
42
|
const beforeRaw = query.get("before");
|
|
43
43
|
if (beforeRaw !== null && !isUuidV7(beforeRaw)) {
|
|
44
|
-
|
|
44
|
+
sendError(res, 400, "request.id_invalid", "before must be a uuidv7 link id (the `id`/`nextBefore` of a previous page)");
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
47
|
const before = beforeRaw ?? undefined;
|
|
@@ -57,7 +57,7 @@ async function handleSessionsListBody(req, res, url, ctx, miss) {
|
|
|
57
57
|
else {
|
|
58
58
|
const s = scopeRaw.trim();
|
|
59
59
|
if (s === "" || s.length > 190) {
|
|
60
|
-
|
|
60
|
+
sendError(res, 400, "request.query_invalid", "scope must be a non-blank tenant principal of at most 190 characters (the sendfile_link.scope column width)");
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
63
|
scope = s;
|
|
@@ -32,7 +32,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
32
32
|
if (im && req.method === "GET") {
|
|
33
33
|
const principal = gatedPrincipal(req, deps.config);
|
|
34
34
|
if (deps.config.requirePrincipal && !principal) {
|
|
35
|
-
|
|
35
|
+
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
38
|
sendJson(res, 200, {
|
|
@@ -48,7 +48,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
48
48
|
if (stm && req.method === "GET") {
|
|
49
49
|
const principal = gatedPrincipal(req, deps.config);
|
|
50
50
|
if (deps.config.requirePrincipal && !principal) {
|
|
51
|
-
|
|
51
|
+
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
sendJson(res, 200, {
|
|
@@ -68,7 +68,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
68
68
|
const mm = /^\/v1\/sessions\/([^/]+)\/mcp$/.exec(url);
|
|
69
69
|
if (mm && req.method === "GET") {
|
|
70
70
|
if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
|
|
71
|
-
|
|
71
|
+
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
74
|
const specs = mcpForScenario(deps.config.mcpServers, deps.config.defaultScenario);
|
|
@@ -120,7 +120,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
120
120
|
const getLeafId = deps.sessionStorage?.getLeafId?.bind(deps.sessionStorage);
|
|
121
121
|
const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
|
|
122
122
|
if (!getLeafId || !ownerOf) {
|
|
123
|
-
|
|
123
|
+
sendError(res, 501, "capability.session_store_required", "session head probe requires a durable session store (SESSION_BACKEND=tidb|pg|local)");
|
|
124
124
|
return;
|
|
125
125
|
}
|
|
126
126
|
const headSession = decodeURIComponent(hm[1]);
|
|
@@ -130,7 +130,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
130
130
|
if (getHead) {
|
|
131
131
|
const head = await getHead(headSession);
|
|
132
132
|
if (head === undefined) {
|
|
133
|
-
|
|
133
|
+
sendError(res, 404, "not_found.session", "session not found");
|
|
134
134
|
return;
|
|
135
135
|
}
|
|
136
136
|
owner = head.owner;
|
|
@@ -141,7 +141,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
141
141
|
else {
|
|
142
142
|
owner = await ownerOf(headSession);
|
|
143
143
|
if (owner === undefined) {
|
|
144
|
-
|
|
144
|
+
sendError(res, 404, "not_found.session", "session not found");
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
147
|
if (!runOwnerOk(req, res, owner ?? null))
|
|
@@ -164,7 +164,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
164
164
|
const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
|
|
165
165
|
const watch = deps.sessionWatch;
|
|
166
166
|
if (!deps.sessionStorage?.getLeafId || !ownerOf || !watch) {
|
|
167
|
-
|
|
167
|
+
sendError(res, 501, "capability.session_store_required", "session events require a durable session store (SESSION_BACKEND=tidb|pg|local)");
|
|
168
168
|
return;
|
|
169
169
|
}
|
|
170
170
|
const evSession = decodeURIComponent(em[1]);
|
|
@@ -221,7 +221,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
221
221
|
}
|
|
222
222
|
if (owner === undefined) {
|
|
223
223
|
cleanup();
|
|
224
|
-
|
|
224
|
+
sendError(res, 404, "not_found.session", "session not found");
|
|
225
225
|
return;
|
|
226
226
|
}
|
|
227
227
|
if (!runOwnerOk(req, res, owner ?? null)) {
|
|
@@ -337,7 +337,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
337
337
|
catch (e) {
|
|
338
338
|
cleanup();
|
|
339
339
|
if (!res.headersSent)
|
|
340
|
-
|
|
340
|
+
sendError(res, 500, "internal.error", `session events failed: ${e instanceof Error ? e.message : "unknown"}`);
|
|
341
341
|
else
|
|
342
342
|
try {
|
|
343
343
|
res.end();
|
|
@@ -352,7 +352,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
352
352
|
if (sm) {
|
|
353
353
|
const audit = await deps.sessionAudit(sm[1]);
|
|
354
354
|
if (!audit) {
|
|
355
|
-
|
|
355
|
+
sendError(res, 404, "not_found.session", "session not found");
|
|
356
356
|
return;
|
|
357
357
|
}
|
|
358
358
|
if (!runOwnerOk(req, res, audit.owner))
|
|
@@ -369,7 +369,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
369
369
|
if (msgIndex !== undefined) {
|
|
370
370
|
const messages = audit.messages;
|
|
371
371
|
if (msgIndex >= messages.length) {
|
|
372
|
-
|
|
372
|
+
sendError(res, 404, "not_found.message_index", "message index out of range", { total: messages.length });
|
|
373
373
|
return;
|
|
374
374
|
}
|
|
375
375
|
sendJson(res, 200, { sessionId: audit.sessionId, floorEntryId: audit.floorEntryId, index: msgIndex, message: messages[msgIndex] });
|
|
@@ -401,32 +401,32 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
401
401
|
const fork = deps.sessionStorage?.fork?.bind(deps.sessionStorage);
|
|
402
402
|
const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
|
|
403
403
|
if (!fork || !ownerOf) {
|
|
404
|
-
|
|
404
|
+
sendError(res, 501, "capability.session_store_required", "session fork requires a durable session store (SESSION_BACKEND=tidb)");
|
|
405
405
|
return;
|
|
406
406
|
}
|
|
407
407
|
const scope = sessionOwnerScopeForWrite(req);
|
|
408
408
|
if (!scope.fleetWide && scope.gateOwner === null) {
|
|
409
|
-
|
|
409
|
+
sendError(res, 401, "auth.unauthorized", "unauthorized");
|
|
410
410
|
return;
|
|
411
411
|
}
|
|
412
412
|
const sourceId = safeDecode(fm[1]);
|
|
413
413
|
if (sourceId === null || !isUuidV7(sourceId)) {
|
|
414
|
-
|
|
414
|
+
sendError(res, 400, "request.id_invalid", "invalid session id (must be a uuidv7)");
|
|
415
415
|
return;
|
|
416
416
|
}
|
|
417
417
|
const owner = await ownerOf(sourceId);
|
|
418
418
|
if (owner === undefined) {
|
|
419
|
-
|
|
419
|
+
sendError(res, 404, "not_found.session", "session not found");
|
|
420
420
|
return;
|
|
421
421
|
}
|
|
422
422
|
if (!scope.fleetWide && owner !== scope.gateOwner) {
|
|
423
|
-
|
|
423
|
+
sendError(res, 404, "not_found.session", "session not found");
|
|
424
424
|
return;
|
|
425
425
|
}
|
|
426
426
|
const newOwner = scope.fleetWide ? (owner ?? null) : scope.gateOwner;
|
|
427
427
|
const newId = await fork(sourceId, newOwner);
|
|
428
428
|
if (newId === null) {
|
|
429
|
-
|
|
429
|
+
sendError(res, 404, "not_found.session", "session not found");
|
|
430
430
|
return;
|
|
431
431
|
}
|
|
432
432
|
sendJson(res, 200, { sessionId: newId });
|
|
@@ -439,17 +439,17 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
439
439
|
const purge = deps.purgeSession;
|
|
440
440
|
const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
|
|
441
441
|
if (!purge || !ownerOf) {
|
|
442
|
-
|
|
442
|
+
sendError(res, 501, "capability.session_store_required", "session delete requires a durable session store (SESSION_BACKEND=tidb)");
|
|
443
443
|
return;
|
|
444
444
|
}
|
|
445
445
|
const scope = sessionOwnerScopeForWrite(req);
|
|
446
446
|
if (!scope.fleetWide && scope.gateOwner === null) {
|
|
447
|
-
|
|
447
|
+
sendError(res, 401, "auth.unauthorized", "unauthorized");
|
|
448
448
|
return;
|
|
449
449
|
}
|
|
450
450
|
const sessionId = safeDecode(dm[1]);
|
|
451
451
|
if (sessionId === null || !isUuidV7(sessionId)) {
|
|
452
|
-
|
|
452
|
+
sendError(res, 400, "request.id_invalid", "invalid session id (must be a uuidv7)");
|
|
453
453
|
return;
|
|
454
454
|
}
|
|
455
455
|
const owner = await ownerOf(sessionId);
|
|
@@ -463,12 +463,12 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
463
463
|
}
|
|
464
464
|
const activeTaskId = await deps.runStore?.getActiveTaskId(sessionId);
|
|
465
465
|
if (activeTaskId) {
|
|
466
|
-
|
|
466
|
+
sendError(res, 409, "conflict.session_active_run", "session has an active run; cancel it before deleting", { activeTaskId });
|
|
467
467
|
return;
|
|
468
468
|
}
|
|
469
469
|
const result = await purge(sessionId, owner);
|
|
470
470
|
if ("active" in result) {
|
|
471
|
-
|
|
471
|
+
sendError(res, 409, "conflict.session_active_run", "session has an active run; cancel it before deleting", { activeTaskId: result.active });
|
|
472
472
|
return;
|
|
473
473
|
}
|
|
474
474
|
sendJson(res, 200, result);
|
|
@@ -481,31 +481,31 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
481
481
|
const store = deps.sessionPolicyStore;
|
|
482
482
|
const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
|
|
483
483
|
if (!store || !ownerOf) {
|
|
484
|
-
|
|
484
|
+
sendError(res, 501, "capability.session_store_required", "session policy requires a session-store backend");
|
|
485
485
|
return;
|
|
486
486
|
}
|
|
487
487
|
const principal = gatedPrincipal(req, deps.config);
|
|
488
488
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
489
|
-
|
|
489
|
+
sendError(res, 401, "auth.unauthorized", "unauthorized");
|
|
490
490
|
return;
|
|
491
491
|
}
|
|
492
492
|
const sessionId = safeDecode(pm[1]);
|
|
493
493
|
if (sessionId === null || !isUuidV7(sessionId)) {
|
|
494
|
-
|
|
494
|
+
sendError(res, 400, "request.id_invalid", "invalid session id (must be a uuidv7)");
|
|
495
495
|
return;
|
|
496
496
|
}
|
|
497
497
|
const owner = await ownerOf(sessionId);
|
|
498
498
|
if (owner === undefined) {
|
|
499
|
-
|
|
499
|
+
sendError(res, 404, "not_found.session", "session not found");
|
|
500
500
|
return;
|
|
501
501
|
}
|
|
502
502
|
if (owner === null) {
|
|
503
|
-
|
|
503
|
+
sendError(res, 409, "conflict.session_ownerless", "session policy requires an owned session (set REQUIRE_PRINCIPAL / attach a principal); this session is ownerless");
|
|
504
504
|
return;
|
|
505
505
|
}
|
|
506
506
|
const operator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
|
|
507
507
|
if (!operator && owner !== (principal ?? null)) {
|
|
508
|
-
|
|
508
|
+
sendError(res, 404, "not_found.session", "session not found");
|
|
509
509
|
return;
|
|
510
510
|
}
|
|
511
511
|
const target = owner;
|
|
@@ -518,15 +518,15 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
518
518
|
body = (await readJson(req));
|
|
519
519
|
}
|
|
520
520
|
catch {
|
|
521
|
-
|
|
521
|
+
sendError(res, 400, "request.invalid_json", "invalid JSON body");
|
|
522
522
|
return;
|
|
523
523
|
}
|
|
524
524
|
if (typeof body.rules !== "object" || body.rules === null || Array.isArray(body.rules)) {
|
|
525
|
-
|
|
525
|
+
sendError(res, 400, "request.body_shape", "rules must be a SessionPermissionRules object");
|
|
526
526
|
return;
|
|
527
527
|
}
|
|
528
528
|
if (body.expectedRev !== undefined && (typeof body.expectedRev !== "number" || !Number.isInteger(body.expectedRev) || body.expectedRev < 0)) {
|
|
529
|
-
|
|
529
|
+
sendError(res, 400, "request.field_invalid", "expectedRev must be a non-negative integer");
|
|
530
530
|
return;
|
|
531
531
|
}
|
|
532
532
|
const raw = body.rules;
|
|
@@ -535,7 +535,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
535
535
|
if (raw[f] === undefined)
|
|
536
536
|
continue;
|
|
537
537
|
if (!Array.isArray(raw[f]) || !raw[f].every((x) => typeof x === "string")) {
|
|
538
|
-
|
|
538
|
+
sendError(res, 400, "request.field_invalid", `${f} must be an array of strings`);
|
|
539
539
|
return;
|
|
540
540
|
}
|
|
541
541
|
rules[f] = raw[f];
|
|
@@ -560,22 +560,22 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
560
560
|
const fs = deps.fileSnapshotStore;
|
|
561
561
|
const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
|
|
562
562
|
if (!fs || typeof fs.listKeys !== "function" || typeof fs.exportManifest !== "function" || typeof fs.getBlob !== "function" || !ownerOf) {
|
|
563
|
-
|
|
563
|
+
sendError(res, 501, "capability.snapshot_store_required", "workspace browse requires a durable snapshot store (rewindFiles wiring) and a durable session store");
|
|
564
564
|
return;
|
|
565
565
|
}
|
|
566
566
|
const scope = sessionOwnerScope(req);
|
|
567
567
|
if (!scope.fleetWide && scope.gateOwner === null) {
|
|
568
|
-
|
|
568
|
+
sendError(res, 401, "auth.unauthorized", "unauthorized");
|
|
569
569
|
return;
|
|
570
570
|
}
|
|
571
571
|
const wsSession = safeDecode(wm[1]);
|
|
572
572
|
if (wsSession === null || !isUuidShape(wsSession)) {
|
|
573
|
-
|
|
573
|
+
sendError(res, 400, "request.id_invalid", "invalid session id (must be a canonical uuid)");
|
|
574
574
|
return;
|
|
575
575
|
}
|
|
576
576
|
const wsOwner = await ownerOf(wsSession);
|
|
577
577
|
if (wsOwner === undefined || (!scope.fleetWide && wsOwner !== scope.gateOwner)) {
|
|
578
|
-
|
|
578
|
+
sendError(res, 404, "not_found.session", "session not found");
|
|
579
579
|
return;
|
|
580
580
|
}
|
|
581
581
|
const q = new URL(req.url ?? "", "http://x").searchParams;
|
|
@@ -584,7 +584,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
584
584
|
if (wm[2] === undefined) {
|
|
585
585
|
const limRaw = q.get("limit");
|
|
586
586
|
if (limRaw !== null && !(/^[0-9]+$/.test(limRaw) && Number(limRaw) >= 1 && Number(limRaw) <= 100)) {
|
|
587
|
-
|
|
587
|
+
sendError(res, 400, "request.query_invalid", "limit must be a decimal integer in 1..100");
|
|
588
588
|
return;
|
|
589
589
|
}
|
|
590
590
|
const lim = limRaw === null ? 20 : Number(limRaw);
|
|
@@ -613,24 +613,24 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
613
613
|
}
|
|
614
614
|
const keyRaw = safeDecode(wm[2]);
|
|
615
615
|
if (keyRaw === null) {
|
|
616
|
-
|
|
616
|
+
sendError(res, 400, "request.path_malformed", "malformed snapshot key");
|
|
617
617
|
return;
|
|
618
618
|
}
|
|
619
619
|
const key = keyRaw === "latest" ? keys[0] : keyRaw;
|
|
620
620
|
const manifest = key !== undefined ? await fs.exportManifest(wsSession, key) : null;
|
|
621
621
|
if (key === undefined || manifest === null) {
|
|
622
|
-
|
|
622
|
+
sendError(res, 404, "not_found.snapshot", "snapshot not found");
|
|
623
623
|
return;
|
|
624
624
|
}
|
|
625
625
|
if (sub === "tree") {
|
|
626
626
|
const limRaw = q.get("limit");
|
|
627
627
|
if (limRaw !== null && !(/^[0-9]+$/.test(limRaw) && Number(limRaw) >= 1 && Number(limRaw) <= 2000)) {
|
|
628
|
-
|
|
628
|
+
sendError(res, 400, "request.query_invalid", "limit must be a decimal integer in 1..2000");
|
|
629
629
|
return;
|
|
630
630
|
}
|
|
631
631
|
const offRaw = q.get("offset");
|
|
632
632
|
if (offRaw !== null && !/^[0-9]+$/.test(offRaw)) {
|
|
633
|
-
|
|
633
|
+
sendError(res, 400, "request.query_invalid", "offset must be a non-negative integer");
|
|
634
634
|
return;
|
|
635
635
|
}
|
|
636
636
|
const lim = limRaw === null ? 500 : Number(limRaw);
|
|
@@ -650,12 +650,12 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
650
650
|
if (sub === "file") {
|
|
651
651
|
const pathParam = q.get("path");
|
|
652
652
|
if (!pathParam) {
|
|
653
|
-
|
|
653
|
+
sendError(res, 400, "request.query_invalid", "missing ?path=<relPath from the tree>");
|
|
654
654
|
return;
|
|
655
655
|
}
|
|
656
656
|
const hash = manifest.get(pathParam);
|
|
657
657
|
if (hash === undefined) {
|
|
658
|
-
|
|
658
|
+
sendError(res, 404, "not_found.file", "no such file in this snapshot (paths are the tree's relPaths, verbatim)");
|
|
659
659
|
return;
|
|
660
660
|
}
|
|
661
661
|
const cap = deps.config.workspaceFileMaxBytes ?? 8 * 1024 * 1024;
|
|
@@ -668,7 +668,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
668
668
|
}
|
|
669
669
|
const bytes = await fs.getBlob(hash);
|
|
670
670
|
if (bytes === undefined) {
|
|
671
|
-
|
|
671
|
+
sendError(res, 404, "not_found.file", "file content no longer available (snapshot raced a reap)");
|
|
672
672
|
return;
|
|
673
673
|
}
|
|
674
674
|
if (bytes.byteLength > cap) {
|
|
@@ -690,14 +690,14 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
690
690
|
const entries = [...manifest.entries()].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
691
691
|
const bad = entries.filter(([p]) => splitTarPath(p) === null).map(([p]) => p);
|
|
692
692
|
if (bad.length > 0) {
|
|
693
|
-
|
|
693
|
+
sendError(res, 422, "request.content_unrepresentable", `snapshot contains ${bad.length} path(s) not representable in ustar (>255 bytes or unsplittable)`, { paths: bad.slice(0, 5) });
|
|
694
694
|
return;
|
|
695
695
|
}
|
|
696
696
|
if (typeof fs.hasBlobs === "function") {
|
|
697
697
|
const present = await fs.hasBlobs([...new Set(entries.map(([, h]) => h))]);
|
|
698
698
|
const missing = entries.filter(([, h]) => !present.has(h)).length;
|
|
699
699
|
if (missing > 0) {
|
|
700
|
-
|
|
700
|
+
sendError(res, 502, "internal.snapshot_blobs_missing", `snapshot is missing ${missing} blob(s) (raced a reap or a partial import) — retry, or pick another snapshot`);
|
|
701
701
|
return;
|
|
702
702
|
}
|
|
703
703
|
}
|
|
@@ -737,7 +737,7 @@ async function handleSessionsBody(req, res, url, ctx, miss) {
|
|
|
737
737
|
}
|
|
738
738
|
return;
|
|
739
739
|
}
|
|
740
|
-
|
|
740
|
+
sendError(res, 404, "request.route_unsupported", "unknown workspace verb — use /tree, /file?path=, or /archive");
|
|
741
741
|
return;
|
|
742
742
|
}
|
|
743
743
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { RouteCtx } from "../route-ctx.js";
|
|
3
|
+
export declare function handleSideQuery(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
|
|
4
|
+
//# sourceMappingURL=side-query.d.ts.map
|