@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.
Files changed (50) hide show
  1. package/README.md +9 -0
  2. package/README.zh-CN.md +7 -0
  3. package/dist/config-types.d.ts +1 -1
  4. package/dist/config.d.ts +14 -0
  5. package/dist/config.js +148 -45
  6. package/dist/elicitation.js +2 -2
  7. package/dist/http/route-ctx.d.ts +51 -2
  8. package/dist/http/routes/approvals-assistant.d.ts +11 -0
  9. package/dist/http/routes/approvals-assistant.js +530 -0
  10. package/dist/http/routes/attachments.js +7 -7
  11. package/dist/http/routes/fleet.d.ts +4 -0
  12. package/dist/http/routes/fleet.js +147 -0
  13. package/dist/http/routes/images.js +29 -29
  14. package/dist/http/routes/leader.d.ts +4 -0
  15. package/dist/http/routes/leader.js +48 -0
  16. package/dist/http/routes/memory-policy.js +10 -10
  17. package/dist/http/routes/notify-wake.d.ts +4 -0
  18. package/dist/http/routes/notify-wake.js +133 -0
  19. package/dist/http/routes/observability.js +5 -5
  20. package/dist/http/routes/runs.d.ts +19 -0
  21. package/dist/http/routes/runs.js +967 -0
  22. package/dist/http/routes/session-sync.js +28 -28
  23. package/dist/http/routes/sessions-list.js +8 -8
  24. package/dist/http/routes/sessions.js +47 -47
  25. package/dist/http/routes/side-query.d.ts +4 -0
  26. package/dist/http/routes/side-query.js +88 -0
  27. package/dist/http/routes/tasks.d.ts +4 -0
  28. package/dist/http/routes/tasks.js +632 -0
  29. package/dist/http/routes/trace-usage.d.ts +4 -0
  30. package/dist/http/routes/trace-usage.js +239 -0
  31. package/dist/http/routes/workflows.d.ts +5 -0
  32. package/dist/http/routes/workflows.js +337 -0
  33. package/dist/http/run-meta.d.ts +11 -0
  34. package/dist/http/run-meta.js +16 -0
  35. package/dist/http/send.d.ts +1 -0
  36. package/dist/http/send.js +15 -0
  37. package/dist/http/server.d.ts +6 -5
  38. package/dist/http/server.js +241 -3166
  39. package/dist/http/sse-log.js +2 -2
  40. package/dist/http/wire-types.d.ts +6 -0
  41. package/dist/leader/endpoint.js +4 -4
  42. package/dist/main.js +5 -5
  43. package/dist/plugins/remote-env-host.js +4 -3
  44. package/dist/question.js +2 -2
  45. package/dist/run-local.js +1 -1
  46. package/dist/tool-approval.js +2 -2
  47. package/dist/trace/ledger-sink.js +1 -1
  48. package/dist/trace/project.d.ts +1 -0
  49. package/dist/trace/project.js +3 -0
  50. 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
- sendJson(res, 501, { error: "session sync requires a durable session store" });
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
- sendJson(res, 401, { error: "unauthorized" });
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
- sendJson(res, 400, { error: "invalid session id (must be a canonical uuid)" });
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
- sendJson(res, 404, { error: "session not found" });
60
+ sendError(res, 404, "not_found.session", "session not found");
61
61
  return false;
62
62
  }
63
63
  if (!ownsOrFleet(owner)) {
64
- sendJson(res, 404, { error: "session not found" });
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
- sendJson(res, 404, { error: "session not found" });
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
- sendJson(res, 404, { error: "session not found" });
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
- sendJson(res, 400, { error: "afterSeq must be a non-negative integer" });
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
- sendJson(res, 501, { error: "session entry streaming requires a durable session store" });
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
- sendJson(res, 404, { error: "session not found" });
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
- sendJson(res, 400, { error: "invalid snapshot key or blob hash" });
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
- sendJson(res, 400, { error: "blob hash must be 64 lowercase-hex chars" });
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
- sendJson(res, 404, { error: "blob not found" });
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
- sendJson(res, 404, { error: "blob not found" });
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
- sendJson(res, 400, { error: "blob hash must be 64 lowercase-hex chars" });
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
- sendJson(res, 501, { error: "blob upload requires a durable snapshot store" });
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
- sendJson(res, 400, { error: "blob hash mismatch" });
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
- sendJson(res, 501, { error: "staged session import requires a durable session store" });
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
- sendJson(res, 400, { error: "invalid JSON body" });
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
- sendJson(res, 400, { error: "entryIds must be an array of strings" });
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
- sendJson(res, 400, { error: "malformed import metadata" });
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
- sendJson(res, 400, { error: "resolution must be 'overwrite-dst'" });
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
- sendJson(res, 400, { error: "invalid staging id" });
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
- sendJson(res, 404, { error: "staging not found" });
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
- sendJson(res, 404, { error: "staging not found" });
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
- sendJson(res, 400, { error: "invalid JSON body" });
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
- sendJson(res, 400, { error: "entryIds must be an array of strings" });
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
- sendJson(res, 404, { error: "not found" });
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
- sendJson(res, 401, { error: "unauthorized" });
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
- sendJson(res, 501, { error: "session list requires a durable session store (SESSION_BACKEND=tidb)" });
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
- sendJson(res, 401, { error: "unauthorized" });
28
+ sendError(res, 401, "auth.unauthorized", "unauthorized");
29
29
  return;
30
30
  }
31
31
  if (!deps.sendFileLedger) {
32
- sendJson(res, 501, { error: "sendfile ledger requires a store backend (DB_BACKEND=tidb|pg|local) and the SendUserFile issuer configured" });
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
- sendJson(res, 400, { error: "limit must be a decimal integer in 1..100" });
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
- sendJson(res, 400, { error: "before must be a uuidv7 link id (the `id`/`nextBefore` of a previous page)" });
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
- sendJson(res, 400, { error: "scope must be a non-blank tenant principal of at most 190 characters (the sendfile_link.scope column width)" });
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
- sendJson(res, 401, { error: `missing principal header '${deps.config.principalHeader}'` });
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
- sendJson(res, 401, { error: `missing principal header '${deps.config.principalHeader}'` });
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
- sendJson(res, 401, { error: `missing principal header '${deps.config.principalHeader}'` });
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
- sendJson(res, 501, { error: "session head probe requires a durable session store (SESSION_BACKEND=tidb|pg|local)" });
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
- sendJson(res, 404, { error: "session not found" });
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
- sendJson(res, 404, { error: "session not found" });
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
- sendJson(res, 501, { error: "session events require a durable session store (SESSION_BACKEND=tidb|pg|local)" });
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
- sendJson(res, 404, { error: "session not found" });
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
- sendJson(res, 500, { error: `session events failed: ${e instanceof Error ? e.message : "unknown"}` });
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
- sendJson(res, 404, { error: "session not found" });
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
- sendJson(res, 404, { error: "message index out of range", total: messages.length });
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
- sendJson(res, 501, { error: "session fork requires a durable session store (SESSION_BACKEND=tidb)" });
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
- sendJson(res, 401, { error: "unauthorized" });
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
- sendJson(res, 400, { error: "invalid session id (must be a uuidv7)" });
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
- sendJson(res, 404, { error: "session not found" });
419
+ sendError(res, 404, "not_found.session", "session not found");
420
420
  return;
421
421
  }
422
422
  if (!scope.fleetWide && owner !== scope.gateOwner) {
423
- sendJson(res, 404, { error: "session not found" });
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
- sendJson(res, 404, { error: "session not found" });
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
- sendJson(res, 501, { error: "session delete requires a durable session store (SESSION_BACKEND=tidb)" });
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
- sendJson(res, 401, { error: "unauthorized" });
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
- sendJson(res, 400, { error: "invalid session id (must be a uuidv7)" });
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
- sendJson(res, 409, { error: "session has an active run; cancel it before deleting", activeTaskId });
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
- sendJson(res, 409, { error: "session has an active run; cancel it before deleting", activeTaskId: result.active });
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
- sendJson(res, 501, { error: "session policy requires a session-store backend" });
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
- sendJson(res, 401, { error: "unauthorized" });
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
- sendJson(res, 400, { error: "invalid session id (must be a uuidv7)" });
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
- sendJson(res, 404, { error: "session not found" });
499
+ sendError(res, 404, "not_found.session", "session not found");
500
500
  return;
501
501
  }
502
502
  if (owner === null) {
503
- sendJson(res, 409, { error: "session policy requires an owned session (set REQUIRE_PRINCIPAL / attach a principal); this session is ownerless" });
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
- sendJson(res, 404, { error: "session not found" });
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
- sendJson(res, 400, { error: "invalid JSON body" });
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
- sendJson(res, 400, { error: "rules must be a SessionPermissionRules object" });
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
- sendJson(res, 400, { error: "expectedRev must be a non-negative integer" });
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
- sendJson(res, 400, { error: `${f} must be an array of strings` });
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
- sendJson(res, 501, { error: "workspace browse requires a durable snapshot store (rewindFiles wiring) and a durable session store" });
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
- sendJson(res, 401, { error: "unauthorized" });
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
- sendJson(res, 400, { error: "invalid session id (must be a canonical uuid)" });
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
- sendJson(res, 404, { error: "session not found" });
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
- sendJson(res, 400, { error: "limit must be a decimal integer in 1..100" });
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
- sendJson(res, 400, { error: "malformed snapshot key" });
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
- sendJson(res, 404, { error: "snapshot not found" });
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
- sendJson(res, 400, { error: "limit must be a decimal integer in 1..2000" });
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
- sendJson(res, 400, { error: "offset must be a non-negative integer" });
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
- sendJson(res, 400, { error: "missing ?path=<relPath from the tree>" });
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
- sendJson(res, 404, { error: "no such file in this snapshot (paths are the tree's relPaths, verbatim)" });
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
- sendJson(res, 404, { error: "file content no longer available (snapshot raced a reap)" });
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
- sendJson(res, 422, { error: `snapshot contains ${bad.length} path(s) not representable in ustar (>255 bytes or unsplittable)`, paths: bad.slice(0, 5) });
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
- sendJson(res, 502, { error: `snapshot is missing ${missing} blob(s) (raced a reap or a partial import) — retry, or pick another snapshot` });
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
- sendJson(res, 404, { error: "unknown workspace verb — use /tree, /file?path=, or /archive" });
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