@sema-agent/server 7.79.2 → 7.80.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 (104) hide show
  1. package/dist/boot/late-binds.d.ts +35 -0
  2. package/dist/boot/late-binds.js +20 -0
  3. package/dist/boot/stage-01-config.d.ts +7 -0
  4. package/dist/boot/stage-01-config.js +60 -0
  5. package/dist/boot/stage-02-registry-pull.d.ts +10 -0
  6. package/dist/boot/stage-02-registry-pull.js +11 -0
  7. package/dist/boot/stage-03-store-backend.d.ts +32 -0
  8. package/dist/boot/stage-03-store-backend.js +11 -0
  9. package/dist/boot/stage-04-stores.d.ts +69 -0
  10. package/dist/boot/stage-04-stores.js +49 -0
  11. package/dist/boot/stage-05-execution-env.d.ts +82 -0
  12. package/dist/boot/stage-05-execution-env.js +20 -0
  13. package/dist/boot/stage-06-runners.d.ts +143 -0
  14. package/dist/boot/stage-06-runners.js +207 -0
  15. package/dist/boot/stage-07-capability-layer.d.ts +157 -0
  16. package/dist/boot/stage-07-capability-layer.js +162 -0
  17. package/dist/boot/stage-08-reapers.d.ts +172 -0
  18. package/dist/boot/stage-08-reapers.js +78 -0
  19. package/dist/boot/stage-09-leader.d.ts +204 -0
  20. package/dist/boot/stage-09-leader.js +47 -0
  21. package/dist/boot/stage-10-http-server.d.ts +204 -0
  22. package/dist/boot/stage-10-http-server.js +289 -0
  23. package/dist/config-invariants.d.ts +9 -0
  24. package/dist/config-invariants.js +7 -1
  25. package/dist/config-types.d.ts +1 -1
  26. package/dist/http/dispatch.js +28 -8
  27. package/dist/http/route-ctx.d.ts +101 -4
  28. package/dist/http/route-ctx.js +4 -0
  29. package/dist/http/route-table.d.ts +71 -43
  30. package/dist/http/route-table.js +111 -187
  31. package/dist/http/routes/a2a-serve.d.ts +18 -3
  32. package/dist/http/routes/a2a-serve.js +25 -23
  33. package/dist/http/routes/admin-config-refresh.d.ts +9 -2
  34. package/dist/http/routes/admin-config-refresh.js +32 -31
  35. package/dist/http/routes/admin-drain.d.ts +9 -2
  36. package/dist/http/routes/admin-drain.js +32 -31
  37. package/dist/http/routes/admin-memory-optout.d.ts +28 -2
  38. package/dist/http/routes/admin-memory-optout.js +26 -39
  39. package/dist/http/routes/adoption.d.ts +15 -2
  40. package/dist/http/routes/adoption.js +52 -52
  41. package/dist/http/routes/agents-roster.d.ts +9 -2
  42. package/dist/http/routes/agents-roster.js +82 -80
  43. package/dist/http/routes/approvals-assistant.d.ts +58 -5
  44. package/dist/http/routes/approvals-assistant.js +37 -38
  45. package/dist/http/routes/attachments.d.ts +22 -2
  46. package/dist/http/routes/attachments.js +81 -72
  47. package/dist/http/routes/capabilities.d.ts +21 -2
  48. package/dist/http/routes/capabilities.js +174 -170
  49. package/dist/http/routes/config-catalog.d.ts +9 -2
  50. package/dist/http/routes/config-catalog.js +28 -31
  51. package/dist/http/routes/devices.d.ts +24 -2
  52. package/dist/http/routes/devices.js +19 -21
  53. package/dist/http/routes/diagnostics.d.ts +9 -2
  54. package/dist/http/routes/diagnostics.js +41 -43
  55. package/dist/http/routes/fleet.d.ts +11 -2
  56. package/dist/http/routes/fleet.js +23 -22
  57. package/dist/http/routes/images.d.ts +63 -9
  58. package/dist/http/routes/images.js +53 -46
  59. package/dist/http/routes/leader.d.ts +20 -2
  60. package/dist/http/routes/leader.js +43 -40
  61. package/dist/http/routes/memory-bundle.d.ts +15 -2
  62. package/dist/http/routes/memory-bundle.js +38 -38
  63. package/dist/http/routes/memory-compliance.d.ts +16 -4
  64. package/dist/http/routes/memory-compliance.js +44 -45
  65. package/dist/http/routes/memory-consolidation.d.ts +14 -2
  66. package/dist/http/routes/memory-consolidation.js +66 -66
  67. package/dist/http/routes/memory-origin.d.ts +23 -4
  68. package/dist/http/routes/memory-origin.js +65 -65
  69. package/dist/http/routes/memory-policy.d.ts +21 -2
  70. package/dist/http/routes/memory-policy.js +119 -115
  71. package/dist/http/routes/notify-wake.d.ts +21 -11
  72. package/dist/http/routes/notify-wake.js +118 -116
  73. package/dist/http/routes/observability.d.ts +16 -2
  74. package/dist/http/routes/observability.js +54 -51
  75. package/dist/http/routes/retention-ops.d.ts +19 -2
  76. package/dist/http/routes/retention-ops.js +20 -22
  77. package/dist/http/routes/rules.d.ts +36 -2
  78. package/dist/http/routes/rules.js +22 -24
  79. package/dist/http/routes/run-memory-capture.d.ts +10 -3
  80. package/dist/http/routes/run-memory-capture.js +84 -84
  81. package/dist/http/routes/runs.d.ts +102 -22
  82. package/dist/http/routes/runs.js +71 -88
  83. package/dist/http/routes/session-sync.d.ts +41 -2
  84. package/dist/http/routes/session-sync.js +25 -23
  85. package/dist/http/routes/sessions-list.d.ts +16 -2
  86. package/dist/http/routes/sessions-list.js +78 -75
  87. package/dist/http/routes/sessions.d.ts +77 -2
  88. package/dist/http/routes/sessions.js +50 -47
  89. package/dist/http/routes/shared-memory.d.ts +19 -2
  90. package/dist/http/routes/shared-memory.js +65 -63
  91. package/dist/http/routes/side-query.d.ts +11 -2
  92. package/dist/http/routes/side-query.js +81 -79
  93. package/dist/http/routes/tasks.d.ts +17 -2
  94. package/dist/http/routes/tasks.js +18 -11
  95. package/dist/http/routes/trace-usage.d.ts +40 -2
  96. package/dist/http/routes/trace-usage.js +46 -35
  97. package/dist/http/routes/workflows.d.ts +35 -3
  98. package/dist/http/routes/workflows.js +29 -29
  99. package/dist/main.js +20 -870
  100. package/dist/service-version.d.ts +5 -0
  101. package/dist/service-version.js +12 -0
  102. package/dist/trace/redact.d.ts +7 -6
  103. package/dist/trace/redact.js +419 -229
  104. package/package.json +1 -1
@@ -2,7 +2,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import type { QuestionAnswer } from "@sema-agent/core";
3
3
  import type { CheckpointStoreFull } from "../../plugins/store-backend.js";
4
4
  import { type GovernancePosture } from "../active-run-conflict.js";
5
- import { type RouteCtx } from "../route-ctx.js";
5
+ import { type RouteCtx, type RouteMatch, type RouteIdsOf } from "../route-ctx.js";
6
6
  import { type KeyClosureIssue } from "../../request-key-closure.js";
7
7
  /**
8
8
  * FRESH 决裁面的**顶层**键闭集裁决:受理集之外的键 ⇒ 返回拒体(调用方 400 `request.body_shape`);
@@ -16,9 +16,6 @@ import { type KeyClosureIssue } from "../../request-key-closure.js";
16
16
  * 的两种兑现方式:**门只站在人**(或客户端)**现写的那份字节**上。
17
17
  */
18
18
  export declare function decideBodyKeyIssue(raw: unknown): KeyClosureIssue | null;
19
- export declare const ASSISTANT_PREEMPT_RE: RegExp;
20
- export declare const ASSISTANT_RESUME_RE: RegExp;
21
- export declare const ASSISTANT_PLAN_REVIEW_RE: RegExp;
22
19
  /**
23
20
  * S-342 —— 「空串的理由 = 没有理由 = 这是一次**裸拒**」。这条语义只适用于**本仓自铸**的那一个域。
24
21
  *
@@ -45,7 +42,9 @@ export declare function decisionReasonOf(body: {
45
42
  * load-bearing shape — `answers[]` non-empty, each `{header: string, selected: string[], note?: string}` —
46
43
  * so a typo'd payload fails the request instead of resuming the task with an answer the tool can't use. */
47
44
  export declare function isQuestionAnswer(v: unknown): v is QuestionAnswer;
48
- export declare function handleApprovalsAssistant(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
45
+ /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
46
+ * = 「本域已应答」;走到函数尾才是「没匹配上」。跨域的东西一律在首行解构出来。 */
47
+ export declare function handleApprovalsAssistant(req: IncomingMessage, res: ServerResponse, match: RouteMatch<ApprovalsAssistantRouteId>, ctx: RouteCtx): Promise<void>;
49
48
  /**
50
49
  * GET /v1/approvals/stream ([ref] native push): SSE — pushes pending-approval deltas so the portal
51
50
  * SUBSCRIBES ONCE instead of polling GET /v1/approvals every ~10s (better UX: near-real-time + no client poll
@@ -71,4 +70,58 @@ export declare function streamApprovals(req: IncomingMessage, res: ServerRespons
71
70
  /** [ref]②:出身归因的部署侧一半(缺席 ⇒ 归不出治理出身 ⇒ 键缺席,与 best-effort 同方向)。**位置在
72
71
  * `pollMs` 之前**是刻意的:`pollMs` 只有测试传,而本参数是每个生产调用点都必须给的那一个。 */
73
72
  governance?: GovernancePosture, pollMs?: number): Promise<void>;
73
+ export declare const APPROVALS_ASSISTANT_ROUTES: readonly [{
74
+ readonly id: "approvals-list";
75
+ readonly path: "/v1/approvals";
76
+ readonly methods: readonly ["GET"];
77
+ }, {
78
+ readonly id: "approvals-stream";
79
+ readonly path: "/v1/approvals/stream";
80
+ readonly methods: readonly ["GET"];
81
+ }, {
82
+ readonly id: "assistant-inbox";
83
+ readonly path: "/v1/assistant/inbox";
84
+ readonly methods: readonly ["GET"];
85
+ }, {
86
+ readonly id: "assistant-tasks";
87
+ readonly path: "/v1/assistant/tasks";
88
+ readonly methods: readonly ["GET"];
89
+ }, {
90
+ readonly id: "approval-decide";
91
+ readonly pattern: RegExp;
92
+ readonly label: "/v1/approvals/:id/decide";
93
+ readonly methods: readonly ["POST"];
94
+ }, {
95
+ readonly id: "approval-exemptions-list";
96
+ readonly pattern: RegExp;
97
+ readonly label: "/v1/approvals/:id/exemptions";
98
+ readonly methods: readonly ["GET"];
99
+ }, {
100
+ readonly id: "approval-exemption-delete";
101
+ readonly pattern: RegExp;
102
+ readonly label: "/v1/approvals/:id/exemptions";
103
+ readonly methods: readonly ["DELETE"];
104
+ }, {
105
+ readonly id: "assistant-task-preempt";
106
+ readonly pattern: RegExp;
107
+ readonly label: "/v1/assistant/tasks/:id/preempt";
108
+ readonly methods: readonly ["POST"];
109
+ }, {
110
+ readonly id: "assistant-task-resume";
111
+ readonly pattern: RegExp;
112
+ readonly label: "/v1/assistant/tasks/:id/resume";
113
+ readonly billable: true;
114
+ readonly methods: readonly ["POST"];
115
+ }, {
116
+ readonly id: "assistant-task-plan-review";
117
+ readonly pattern: RegExp;
118
+ readonly label: "/v1/assistant/tasks/:id/plan_review";
119
+ readonly billable: true;
120
+ readonly methods: readonly ["POST"];
121
+ }, {
122
+ readonly prefix: "/v1/approvals";
123
+ readonly billable: true;
124
+ }];
125
+ /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
126
+ export type ApprovalsAssistantRouteId = RouteIdsOf<typeof APPROVALS_ASSISTANT_ROUTES>;
74
127
  //# sourceMappingURL=approvals-assistant.d.ts.map
@@ -17,6 +17,7 @@ import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_
17
17
  import { gatedPrincipal, explicitOperatorOk, isOperator, sendPrincipalRefusal } from "../principal-gate.js";
18
18
  import { governanceOriginOf } from "../active-run-conflict.js";
19
19
  import { sendResumeOutcome } from "../route-ctx.js";
20
+ import { assertNever } from "../route-ctx.js";
20
21
  import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "../../request-key-closure.js";
21
22
  const DECIDE_BODY_DECLARED_KEYS = [
22
23
  "answer", "boundCallId", "boundInputHash", "checkpointToken", "decision", "reason", "remember", "updatedInput",
@@ -45,9 +46,6 @@ export function decideBodyKeyIssue(raw) {
45
46
  unsupportedKeys: buckets.unsupported.slice(0, MAX_KEY_ECHO),
46
47
  };
47
48
  }
48
- export const ASSISTANT_PREEMPT_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/preempt$/;
49
- export const ASSISTANT_RESUME_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/resume$/;
50
- export const ASSISTANT_PLAN_REVIEW_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/plan_review$/;
51
49
  export function decisionReasonOf(body) {
52
50
  return typeof body.reason === "string" && body.reason !== "" ? body.reason : undefined;
53
51
  }
@@ -132,30 +130,25 @@ async function decideIdempotentReplay(cs, args) {
132
130
  },
133
131
  };
134
132
  }
135
- export async function handleApprovalsAssistant(req, res, url, ctx) {
136
- const miss = { fell: false };
137
- await handleApprovalsAssistantBody(req, res, url, ctx, miss);
138
- return !miss.fell;
139
- }
140
- async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
133
+ export async function handleApprovalsAssistant(req, res, match, ctx) {
141
134
  const { deps } = ctx;
142
135
  const { preemptableRuns } = ctx.registry;
143
136
  const { readJson, rateLimited, quotaExceeded } = ctx.helpers;
144
137
  const { resumeCheckpoint, resumePreempted, resumePlanReview } = ctx.legs;
145
- if (deps.checkpointStore && (url.startsWith("/v1/approvals") || url.startsWith("/v1/assistant"))) {
138
+ {
146
139
  const cs = deps.checkpointStore;
147
140
  const principal = gatedPrincipal(req, deps.config);
148
141
  const operator = isOperator(principal, deps.config.operatorPrincipals);
149
- if (req.method === "GET" && url === "/v1/approvals/stream") {
142
+ if (match.decl.id === "approvals-stream") {
150
143
  const scope = operator
151
- ? (new URL(req.url ?? "", "http://x").searchParams.get("owner") ?? undefined)
144
+ ? (new URLSearchParams(match.search).get("owner") ?? undefined)
152
145
  : (principal ?? "__none__");
153
146
  await streamApprovals(req, res, cs, scope, deps.config);
154
147
  return;
155
148
  }
156
- if (req.method === "GET" && url === "/v1/approvals") {
149
+ if (match.decl.id === "approvals-list") {
157
150
  const scope = operator
158
- ? (new URL(req.url ?? "", "http://x").searchParams.get("owner") ?? undefined)
151
+ ? (new URLSearchParams(match.search).get("owner") ?? undefined)
159
152
  : (principal ?? "__none__");
160
153
  sendJson(res, 200, {
161
154
  pending: (await cs.listPending(scope)).map((r) => projectPendingForWire(r, deps.config)),
@@ -166,8 +159,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
166
159
  return;
167
160
  }
168
161
  {
169
- const em = /^\/v1\/approvals\/([^/]+)\/exemptions(?:\/([^/]+))?$/.exec(url);
170
- if (em && (req.method === "GET" || req.method === "DELETE")) {
162
+ if (match.decl.id === "approval-exemptions-list" || match.decl.id === "approval-exemption-delete") {
171
163
  if (!deps.approvalExemptionStore) {
172
164
  sendError(res, 501, "capability.store_required", "approval exemptions need a store backend");
173
165
  return;
@@ -175,8 +167,8 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
175
167
  let exSessionId;
176
168
  let exToolName;
177
169
  try {
178
- exSessionId = decodeURIComponent(em[1]);
179
- exToolName = em[2] !== undefined ? decodeURIComponent(em[2]) : undefined;
170
+ exSessionId = decodeURIComponent(match.params.sessionId);
171
+ exToolName = match.params.tool !== undefined ? decodeURIComponent(match.params.tool) : undefined;
180
172
  }
181
173
  catch {
182
174
  sendError(res, 400, "request.path_malformed", "malformed id (invalid percent-encoding)");
@@ -196,7 +188,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
196
188
  return;
197
189
  }
198
190
  }
199
- if (req.method === "GET") {
191
+ if (match.decl.id === "approval-exemptions-list") {
200
192
  if (exToolName !== undefined) {
201
193
  sendError(res, 404, "request.route_unsupported", "not found");
202
194
  return;
@@ -213,8 +205,8 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
213
205
  return;
214
206
  }
215
207
  }
216
- if (req.method === "GET" && url === "/v1/assistant/inbox") {
217
- const ownerParam = new URL(req.url ?? "", "http://x").searchParams.get("owner") || undefined;
208
+ if (match.decl.id === "assistant-inbox") {
209
+ const ownerParam = new URLSearchParams(match.search).get("owner") || undefined;
218
210
  const scope = operator && ownerParam ? ownerParam : (principal ?? "__none__");
219
211
  const summaries = await cs.listByScope(scope);
220
212
  const inbox = (await Promise.all(summaries.map(async (s) => {
@@ -240,8 +232,8 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
240
232
  sendJson(res, 200, { inbox });
241
233
  return;
242
234
  }
243
- if (req.method === "GET" && url === "/v1/assistant/tasks") {
244
- const ownerParam = new URL(req.url ?? "", "http://x").searchParams.get("owner") || undefined;
235
+ if (match.decl.id === "assistant-tasks") {
236
+ const ownerParam = new URLSearchParams(match.search).get("owner") || undefined;
245
237
  const scope = operator && ownerParam ? ownerParam : (principal ?? "__none__");
246
238
  if (!deps.runStore) {
247
239
  sendJson(res, 200, { tasks: [] });
@@ -268,8 +260,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
268
260
  sendJson(res, 200, { tasks });
269
261
  return;
270
262
  }
271
- const preemptMatch = req.method === "POST" ? ASSISTANT_PREEMPT_RE.exec(url) : null;
272
- if (preemptMatch) {
263
+ if (match.decl.id === "assistant-task-preempt") {
273
264
  if (rateLimited(req, res) || quotaExceeded(req, res))
274
265
  return;
275
266
  if (!deps.runStore) {
@@ -284,7 +275,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
284
275
  sendError(res, 501, "feature.preemption_disabled", "preemption not enabled on this worker (needs RESOURCE_SUSPEND=true + a durable checkpoint store)");
285
276
  return;
286
277
  }
287
- const taskId = preemptMatch[1];
278
+ const taskId = match.params.taskId;
288
279
  const run = await deps.runStore.getRun(taskId);
289
280
  if (!run) {
290
281
  sendError(res, 404, "not_found.run", "task not found");
@@ -332,8 +323,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
332
323
  }
333
324
  return;
334
325
  }
335
- const resumeMatch = req.method === "POST" ? ASSISTANT_RESUME_RE.exec(url) : null;
336
- if (resumeMatch) {
326
+ if (match.decl.id === "assistant-task-resume") {
337
327
  if (rateLimited(req, res) || quotaExceeded(req, res))
338
328
  return;
339
329
  if (!deps.runStore) {
@@ -344,7 +334,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
344
334
  sendPrincipalRefusal(res, req, deps.config);
345
335
  return;
346
336
  }
347
- const taskId = resumeMatch[1];
337
+ const taskId = match.params.taskId;
348
338
  const run = await deps.runStore.getRun(taskId);
349
339
  if (!run) {
350
340
  sendError(res, 404, "not_found.run", "task not found");
@@ -360,8 +350,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
360
350
  sendResumeOutcome(res, out, deps.logger);
361
351
  return;
362
352
  }
363
- const planReviewMatch = req.method === "POST" ? ASSISTANT_PLAN_REVIEW_RE.exec(url) : null;
364
- if (planReviewMatch) {
353
+ if (match.decl.id === "assistant-task-plan-review") {
365
354
  if (rateLimited(req, res) || quotaExceeded(req, res))
366
355
  return;
367
356
  if (!deps.runStore) {
@@ -372,7 +361,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
372
361
  sendPrincipalRefusal(res, req, deps.config);
373
362
  return;
374
363
  }
375
- const taskId = planReviewMatch[1];
364
+ const taskId = match.params.taskId;
376
365
  const body = (await readJson(req));
377
366
  const decision = body?.decision === "approve" ? "approve" : body?.decision === "edit" ? "edit" : body?.decision === "reject" ? "reject" : undefined;
378
367
  if (!decision) {
@@ -465,13 +454,12 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
465
454
  sendResumeOutcome(res, out, deps.logger);
466
455
  return;
467
456
  }
468
- const m = /^\/v1\/approvals\/([^/]+)\/decide$/.exec(url);
469
- if (m && req.method === "POST") {
457
+ if (match.decl.id === "approval-decide") {
470
458
  if (rateLimited(req, res) || quotaExceeded(req, res))
471
459
  return;
472
460
  let sessionId;
473
461
  try {
474
- sessionId = decodeURIComponent(m[1]);
462
+ sessionId = decodeURIComponent(match.params.sessionId);
475
463
  }
476
464
  catch {
477
465
  sendError(res, 400, "request.path_malformed", "malformed approval id (invalid percent-encoding)");
@@ -615,10 +603,8 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
615
603
  sendResumeOutcome(res, out, deps.logger, remember && deps.approvalExemptionStore ? { rememberApplied } : undefined);
616
604
  return;
617
605
  }
618
- sendError(res, 404, "not_found.route", "not found");
619
- return;
620
606
  }
621
- miss.fell = true;
607
+ return assertNever(match.decl.id);
622
608
  }
623
609
  const APPROVALS_STREAM_POLL_MS = 3000;
624
610
  function redactPendingDisclosures(row) {
@@ -686,4 +672,17 @@ export async function streamApprovals(req, res, cs, scope, governance, pollMs =
686
672
  }
687
673
  res.end();
688
674
  }
675
+ export const APPROVALS_ASSISTANT_ROUTES = [
676
+ { id: "approvals-list", path: "/v1/approvals", methods: ["GET"] },
677
+ { id: "approvals-stream", path: "/v1/approvals/stream", methods: ["GET"] },
678
+ { id: "assistant-inbox", path: "/v1/assistant/inbox", methods: ["GET"] },
679
+ { id: "assistant-tasks", path: "/v1/assistant/tasks", methods: ["GET"] },
680
+ { id: "approval-decide", pattern: /^\/v1\/approvals\/(?<sessionId>[^/]+)\/decide$/, label: "/v1/approvals/:id/decide", methods: ["POST"] },
681
+ { id: "approval-exemptions-list", pattern: /^\/v1\/approvals\/(?<sessionId>[^/]+)\/exemptions(?:\/(?<tool>[^/]+))?$/, label: "/v1/approvals/:id/exemptions", methods: ["GET"] },
682
+ { id: "approval-exemption-delete", pattern: /^\/v1\/approvals\/(?<sessionId>[^/]+)\/exemptions(?:\/(?<tool>[^/]+))?$/, label: "/v1/approvals/:id/exemptions", methods: ["DELETE"] },
683
+ { id: "assistant-task-preempt", pattern: /^\/v1\/assistant\/tasks\/(?<taskId>[^/]+)\/preempt$/, label: "/v1/assistant/tasks/:id/preempt", methods: ["POST"] },
684
+ { id: "assistant-task-resume", pattern: /^\/v1\/assistant\/tasks\/(?<taskId>[^/]+)\/resume$/, label: "/v1/assistant/tasks/:id/resume", billable: true, methods: ["POST"] },
685
+ { id: "assistant-task-plan-review", pattern: /^\/v1\/assistant\/tasks\/(?<taskId>[^/]+)\/plan_review$/, label: "/v1/assistant/tasks/:id/plan_review", billable: true, methods: ["POST"] },
686
+ { prefix: "/v1/approvals", billable: true },
687
+ ];
689
688
  //# sourceMappingURL=approvals-assistant.js.map
@@ -1,4 +1,24 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
- import type { RouteCtx } from "../route-ctx.js";
3
- export declare function handleAttachments(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
2
+ import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
3
+ /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
4
+ * 的语义原样 =「本域已应答」;走到函数尾才是「没匹配上」,置 `miss.fell` 交回装配器继续下一个域。
5
+ * 跨域的东西(deps / 助手 / 单域状态 / 每请求量)一律在首行解构出来,于是下面每一行都逐字不变。 */
6
+ export declare function handleAttachments(req: IncomingMessage, res: ServerResponse, match: RouteMatch<AttachmentsRouteId>, ctx: RouteCtx): Promise<void>;
7
+ export declare const ATTACHMENTS_ROUTES: readonly [{
8
+ readonly id: "attachment-upload";
9
+ readonly path: "/v1/attachments";
10
+ readonly methods: readonly ["POST"];
11
+ }, {
12
+ readonly id: "attachment-fetch";
13
+ readonly pattern: RegExp;
14
+ readonly label: "/v1/attachments/:id";
15
+ readonly methods: readonly ["GET"];
16
+ }, {
17
+ readonly id: "attachment-delete";
18
+ readonly pattern: RegExp;
19
+ readonly label: "/v1/attachments/:id";
20
+ readonly methods: readonly ["DELETE"];
21
+ }];
22
+ /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
23
+ export type AttachmentsRouteId = RouteIdsOf<typeof ATTACHMENTS_ROUTES>;
4
24
  //# sourceMappingURL=attachments.d.ts.map
@@ -3,17 +3,12 @@ import { uuidv7, sanitizePathComponent } from "@sema-agent/core";
3
3
  import { HttpError } from "../../security.js";
4
4
  import { sendJson, sendError } from "../send.js";
5
5
  import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
6
- export async function handleAttachments(req, res, url, ctx) {
7
- const miss = { fell: false };
8
- await handleAttachmentsBody(req, res, url, ctx, miss);
9
- return !miss.fell;
10
- }
11
- async function handleAttachmentsBody(req, res, url, ctx, miss) {
6
+ import { assertNever } from "../route-ctx.js";
7
+ export async function handleAttachments(req, res, match, ctx) {
12
8
  const { deps } = ctx;
13
9
  const { rateLimited, quotaExceeded, safeDecode, readRawBody } = ctx.helpers;
14
10
  {
15
- const am = /^\/v1\/attachments(?:\/([^/?]+))?$/.exec(url);
16
- if (am) {
11
+ {
17
12
  if (rateLimited(req, res) || quotaExceeded(req, res))
18
13
  return;
19
14
  const store = deps.taskAttachmentStore;
@@ -27,80 +22,94 @@ async function handleAttachmentsBody(req, res, url, ctx, miss) {
27
22
  return;
28
23
  }
29
24
  const owner = principal ?? "default";
30
- const attId = am[1] !== undefined ? safeDecode(am[1]) : undefined;
31
- if (attId === undefined && req.method === "POST") {
32
- const q = new URL(req.url ?? "", "http://x").searchParams;
33
- const rawName = q.get("name");
34
- const base = ((rawName ?? "").split(/[/\\]/).filter(Boolean).pop() ?? "").slice(0, 128);
35
- if (!rawName || base === "" || base === "." || base === ".." || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(base)) {
36
- sendError(res, 400, "request.query_invalid", "query param `name` is required and must reduce to a safe basename ([A-Za-z0-9._-], not starting with a dot)");
37
- return;
38
- }
39
- const name = sanitizePathComponent(base);
40
- const mime = String(req.headers["content-type"] ?? "").split(";")[0].trim().toLowerCase();
41
- if (!/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(mime) || mime.length > 128) {
42
- sendError(res, 400, "request.query_invalid", "content-type must be a well-formed mime type (type/subtype)");
43
- return;
44
- }
45
- const allow = deps.config.attachmentMimeAllowlist;
46
- if (allow && !allow.some((a) => a === mime || (a.endsWith("/*") && mime.startsWith(a.slice(0, -1))))) {
47
- sendError(res, 415, "attachment.mime_not_allowed", `mime '${mime}' is not in this deployment's allowlist`, { allowed: allow });
25
+ const attId = match.decl.id === "attachment-upload" ? null : safeDecode(match.params.id);
26
+ switch (match.decl.id) {
27
+ case "attachment-upload": {
28
+ const q = new URLSearchParams(match.search);
29
+ const rawName = q.get("name");
30
+ const base = ((rawName ?? "").split(/[/\\]/).filter(Boolean).pop() ?? "").slice(0, 128);
31
+ if (!rawName || base === "" || base === "." || base === ".." || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(base)) {
32
+ sendError(res, 400, "request.query_invalid", "query param `name` is required and must reduce to a safe basename ([A-Za-z0-9._-], not starting with a dot)");
33
+ return;
34
+ }
35
+ const name = sanitizePathComponent(base);
36
+ const mime = String(req.headers["content-type"] ?? "").split(";")[0].trim().toLowerCase();
37
+ if (!/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(mime) || mime.length > 128) {
38
+ sendError(res, 400, "request.query_invalid", "content-type must be a well-formed mime type (type/subtype)");
39
+ return;
40
+ }
41
+ const allow = deps.config.attachmentMimeAllowlist;
42
+ if (allow && !allow.some((a) => a === mime || (a.endsWith("/*") && mime.startsWith(a.slice(0, -1))))) {
43
+ sendError(res, 415, "attachment.mime_not_allowed", `mime '${mime}' is not in this deployment's allowlist`, { allowed: allow });
44
+ return;
45
+ }
46
+ const max = deps.config.attachmentMaxBytes ?? 32 * 1024 * 1024;
47
+ let body;
48
+ try {
49
+ body = await readRawBody(req, max);
50
+ }
51
+ catch (err) {
52
+ if (err instanceof HttpError && err.status === 413) {
53
+ sendError(res, 413, "attachment.too_large", `attachment exceeds the per-file limit (${max} bytes)`, { maxBytes: max });
54
+ return;
55
+ }
56
+ throw err;
57
+ }
58
+ const rec = {
59
+ id: uuidv7(),
60
+ owner,
61
+ name,
62
+ mime,
63
+ sha256: createHash("sha256").update(body).digest("hex"),
64
+ sizeBytes: body.length,
65
+ createdAtMs: Date.now(),
66
+ };
67
+ await store.put({ ...rec, content: new Uint8Array(body) });
68
+ deps.metrics?.inc("attachment_uploads_total");
69
+ sendJson(res, 201, { id: rec.id, name: rec.name, mime: rec.mime, sha256: rec.sha256, sizeBytes: rec.sizeBytes });
48
70
  return;
49
71
  }
50
- const max = deps.config.attachmentMaxBytes ?? 32 * 1024 * 1024;
51
- let body;
52
- try {
53
- body = await readRawBody(req, max);
54
- }
55
- catch (err) {
56
- if (err instanceof HttpError && err.status === 413) {
57
- sendError(res, 413, "attachment.too_large", `attachment exceeds the per-file limit (${max} bytes)`, { maxBytes: max });
72
+ case "attachment-fetch": {
73
+ if (attId === null) {
74
+ sendError(res, 404, "not_found.route", "not found");
58
75
  return;
59
76
  }
60
- throw err;
61
- }
62
- const rec = {
63
- id: uuidv7(),
64
- owner,
65
- name,
66
- mime,
67
- sha256: createHash("sha256").update(body).digest("hex"),
68
- sizeBytes: body.length,
69
- createdAtMs: Date.now(),
70
- };
71
- await store.put({ ...rec, content: new Uint8Array(body) });
72
- deps.metrics?.inc("attachment_uploads_total");
73
- sendJson(res, 201, { id: rec.id, name: rec.name, mime: rec.mime, sha256: rec.sha256, sizeBytes: rec.sizeBytes });
74
- return;
75
- }
76
- if (attId !== undefined && attId !== null && req.method === "GET") {
77
- const meta = await store.get(owner, attId);
78
- const bytes = meta ? await store.getContent(owner, attId) : null;
79
- if (!meta || !bytes) {
80
- sendError(res, 404, "not_found.attachment", "not found");
77
+ const meta = await store.get(owner, attId);
78
+ const bytes = meta ? await store.getContent(owner, attId) : null;
79
+ if (!meta || !bytes) {
80
+ sendError(res, 404, "not_found.attachment", "not found");
81
+ return;
82
+ }
83
+ res.writeHead(200, {
84
+ "content-type": meta.mime,
85
+ "content-length": String(bytes.byteLength),
86
+ "content-disposition": `attachment; filename*=UTF-8''${encodeURIComponent(meta.name)}`,
87
+ });
88
+ res.end(Buffer.from(bytes));
81
89
  return;
82
90
  }
83
- res.writeHead(200, {
84
- "content-type": meta.mime,
85
- "content-length": String(bytes.byteLength),
86
- "content-disposition": `attachment; filename*=UTF-8''${encodeURIComponent(meta.name)}`,
87
- });
88
- res.end(Buffer.from(bytes));
89
- return;
90
- }
91
- if (attId !== undefined && attId !== null && req.method === "DELETE") {
92
- const ok = await store.delete(owner, attId);
93
- if (!ok) {
94
- sendError(res, 404, "not_found.attachment", "not found");
91
+ case "attachment-delete": {
92
+ if (attId === null) {
93
+ sendError(res, 404, "not_found.route", "not found");
94
+ return;
95
+ }
96
+ const ok = await store.delete(owner, attId);
97
+ if (!ok) {
98
+ sendError(res, 404, "not_found.attachment", "not found");
99
+ return;
100
+ }
101
+ res.writeHead(204).end();
95
102
  return;
96
103
  }
97
- res.writeHead(204).end();
98
- return;
104
+ default:
105
+ return assertNever(match.decl.id);
99
106
  }
100
- sendError(res, 404, "not_found.route", "not found");
101
- return;
102
107
  }
103
108
  }
104
- miss.fell = true;
105
109
  }
110
+ export const ATTACHMENTS_ROUTES = [
111
+ { id: "attachment-upload", path: "/v1/attachments", methods: ["POST"] },
112
+ { id: "attachment-fetch", pattern: /^\/v1\/attachments\/(?<id>[^/]+)$/, label: "/v1/attachments/:id", methods: ["GET"] },
113
+ { id: "attachment-delete", pattern: /^\/v1\/attachments\/(?<id>[^/]+)$/, label: "/v1/attachments/:id", methods: ["DELETE"] },
114
+ ];
106
115
  //# sourceMappingURL=attachments.js.map
@@ -6,6 +6,25 @@
6
6
  * 本域零可变状态;deps 面宽(能力布尔逐条对应各路由的 501 门,单源不许漂移)但全是存在性只读。
7
7
  */
8
8
  import type { IncomingMessage, ServerResponse } from "node:http";
9
- import type { RouteCtx } from "../route-ctx.js";
10
- export declare function handleCapabilities(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
9
+ import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
10
+ /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
11
+ * 的语义原样 =「本域已应答」;走到函数尾才是「没匹配上」,置 `miss.fell` 交回装配器继续下一个域。
12
+ * 跨域的东西(deps / 助手 / 单域状态 / 每请求量)一律在首行解构出来,于是下面每一行都逐字不变。 */
13
+ export declare function handleCapabilities(req: IncomingMessage, res: ServerResponse, match: RouteMatch<CapabilitiesRouteId>, ctx: RouteCtx): Promise<void>;
14
+ export declare const CAPABILITIES_ROUTES: readonly [{
15
+ readonly id: "models";
16
+ readonly path: "/v1/models";
17
+ readonly methods: readonly ["GET"];
18
+ }, {
19
+ readonly id: "capabilities";
20
+ readonly path: "/v1/capabilities";
21
+ readonly methods: readonly ["GET"];
22
+ }, {
23
+ readonly id: "capability-scenario";
24
+ readonly pattern: RegExp;
25
+ readonly label: "/v1/capabilities/scenarios/:name";
26
+ readonly methods: readonly ["GET"];
27
+ }];
28
+ /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
29
+ export type CapabilitiesRouteId = RouteIdsOf<typeof CAPABILITIES_ROUTES>;
11
30
  //# sourceMappingURL=capabilities.d.ts.map