@sema-agent/server 7.79.3 → 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 (74) hide show
  1. package/dist/http/dispatch.js +28 -8
  2. package/dist/http/route-ctx.d.ts +101 -4
  3. package/dist/http/route-ctx.js +4 -0
  4. package/dist/http/route-table.d.ts +71 -43
  5. package/dist/http/route-table.js +111 -187
  6. package/dist/http/routes/a2a-serve.d.ts +18 -3
  7. package/dist/http/routes/a2a-serve.js +25 -23
  8. package/dist/http/routes/admin-config-refresh.d.ts +9 -2
  9. package/dist/http/routes/admin-config-refresh.js +32 -31
  10. package/dist/http/routes/admin-drain.d.ts +9 -2
  11. package/dist/http/routes/admin-drain.js +32 -31
  12. package/dist/http/routes/admin-memory-optout.d.ts +28 -2
  13. package/dist/http/routes/admin-memory-optout.js +26 -39
  14. package/dist/http/routes/adoption.d.ts +15 -2
  15. package/dist/http/routes/adoption.js +52 -52
  16. package/dist/http/routes/agents-roster.d.ts +9 -2
  17. package/dist/http/routes/agents-roster.js +82 -80
  18. package/dist/http/routes/approvals-assistant.d.ts +58 -5
  19. package/dist/http/routes/approvals-assistant.js +37 -38
  20. package/dist/http/routes/attachments.d.ts +22 -2
  21. package/dist/http/routes/attachments.js +81 -72
  22. package/dist/http/routes/capabilities.d.ts +21 -2
  23. package/dist/http/routes/capabilities.js +174 -170
  24. package/dist/http/routes/config-catalog.d.ts +9 -2
  25. package/dist/http/routes/config-catalog.js +28 -31
  26. package/dist/http/routes/devices.d.ts +24 -2
  27. package/dist/http/routes/devices.js +19 -21
  28. package/dist/http/routes/diagnostics.d.ts +9 -2
  29. package/dist/http/routes/diagnostics.js +41 -43
  30. package/dist/http/routes/fleet.d.ts +11 -2
  31. package/dist/http/routes/fleet.js +23 -22
  32. package/dist/http/routes/images.d.ts +63 -9
  33. package/dist/http/routes/images.js +53 -46
  34. package/dist/http/routes/leader.d.ts +20 -2
  35. package/dist/http/routes/leader.js +43 -40
  36. package/dist/http/routes/memory-bundle.d.ts +15 -2
  37. package/dist/http/routes/memory-bundle.js +38 -38
  38. package/dist/http/routes/memory-compliance.d.ts +16 -4
  39. package/dist/http/routes/memory-compliance.js +44 -45
  40. package/dist/http/routes/memory-consolidation.d.ts +14 -2
  41. package/dist/http/routes/memory-consolidation.js +66 -66
  42. package/dist/http/routes/memory-origin.d.ts +23 -4
  43. package/dist/http/routes/memory-origin.js +65 -65
  44. package/dist/http/routes/memory-policy.d.ts +21 -2
  45. package/dist/http/routes/memory-policy.js +119 -115
  46. package/dist/http/routes/notify-wake.d.ts +21 -11
  47. package/dist/http/routes/notify-wake.js +118 -116
  48. package/dist/http/routes/observability.d.ts +16 -2
  49. package/dist/http/routes/observability.js +54 -51
  50. package/dist/http/routes/retention-ops.d.ts +19 -2
  51. package/dist/http/routes/retention-ops.js +20 -22
  52. package/dist/http/routes/rules.d.ts +36 -2
  53. package/dist/http/routes/rules.js +22 -24
  54. package/dist/http/routes/run-memory-capture.d.ts +10 -3
  55. package/dist/http/routes/run-memory-capture.js +84 -84
  56. package/dist/http/routes/runs.d.ts +102 -22
  57. package/dist/http/routes/runs.js +71 -88
  58. package/dist/http/routes/session-sync.d.ts +41 -2
  59. package/dist/http/routes/session-sync.js +25 -23
  60. package/dist/http/routes/sessions-list.d.ts +16 -2
  61. package/dist/http/routes/sessions-list.js +78 -75
  62. package/dist/http/routes/sessions.d.ts +77 -2
  63. package/dist/http/routes/sessions.js +50 -47
  64. package/dist/http/routes/shared-memory.d.ts +19 -2
  65. package/dist/http/routes/shared-memory.js +65 -63
  66. package/dist/http/routes/side-query.d.ts +11 -2
  67. package/dist/http/routes/side-query.js +81 -79
  68. package/dist/http/routes/tasks.d.ts +17 -2
  69. package/dist/http/routes/tasks.js +18 -11
  70. package/dist/http/routes/trace-usage.d.ts +40 -2
  71. package/dist/http/routes/trace-usage.js +46 -35
  72. package/dist/http/routes/workflows.d.ts +35 -3
  73. package/dist/http/routes/workflows.js +29 -29
  74. package/package.json +1 -1
@@ -2,136 +2,138 @@ import { taskNotificationInboxEntry } from "../../orchestration/workflow-complet
2
2
  import { sendJson, sendError } from "../send.js";
3
3
  import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
4
4
  import { sendResumeOutcome } from "../route-ctx.js";
5
- export const SESSION_WAKE_RE = /^\/v1\/sessions\/[^/]+\/wake$/;
6
- export async function handleNotifyWake(req, res, url, ctx) {
7
- const miss = { fell: false };
8
- await handleNotifyWakeBody(req, res, url, ctx, miss);
9
- return !miss.fell;
10
- }
11
- async function handleNotifyWakeBody(req, res, url, ctx, miss) {
5
+ import { assertNever } from "../route-ctx.js";
6
+ export async function handleNotifyWake(req, res, match, ctx) {
12
7
  const { deps } = ctx;
13
8
  const { steerableRuns } = ctx.registry;
14
9
  const { readJson, rateLimited, quotaExceeded, leaseDenied } = ctx.helpers;
15
10
  const { resumeWake } = ctx.legs;
16
- if (req.method === "POST" && /^\/v1\/sessions\/[^/]+\/notify$/.test(url)) {
17
- if (rateLimited(req, res) || quotaExceeded(req, res))
18
- return;
19
- const principal = gatedPrincipal(req, deps.config);
20
- if (deps.config.requirePrincipal && principal === undefined) {
21
- sendPrincipalRefusal(res, req, deps.config);
22
- return;
23
- }
24
- const notifySession = decodeURIComponent(url.split("/")[3]);
25
- let nb;
26
- try {
27
- nb = (await readJson(req));
28
- }
29
- catch {
30
- sendError(res, 400, "request.body_shape", "body must be JSON: { task_id, status, summary, result?, seq?, source? }");
31
- return;
32
- }
33
- if (typeof nb.task_id !== "string" || nb.task_id.trim().length === 0 || nb.task_id.length > 190) {
34
- sendError(res, 400, "request.field_invalid", "task_id must be a non-empty string of at most 190 characters");
35
- return;
36
- }
37
- const NOTIFY_STATUSES = ["completed", "failed", "killed", "cancelled", "event"];
38
- const _notifyExhaustive = true;
39
- void _notifyExhaustive;
40
- if (typeof nb.status !== "string" || !NOTIFY_STATUSES.includes(nb.status)) {
41
- sendError(res, 400, "request.field_invalid", `status must be one of ${NOTIFY_STATUSES.join("/")}`);
42
- return;
43
- }
44
- if (typeof nb.summary !== "string" || nb.summary.trim().length === 0) {
45
- sendError(res, 400, "request.field_invalid", "summary must be a non-empty string");
46
- return;
47
- }
48
- if (nb.result !== undefined && typeof nb.result !== "string") {
49
- sendError(res, 400, "request.field_invalid", "result must be a string when present");
50
- return;
51
- }
52
- if (nb.seq !== undefined && (typeof nb.seq !== "number" || !Number.isInteger(nb.seq) || nb.seq < 1)) {
53
- sendError(res, 400, "request.field_invalid", "seq must be a positive integer when present");
54
- return;
55
- }
56
- if (nb.source !== undefined && (typeof nb.source !== "string" || nb.source.length > 190)) {
57
- sendError(res, 400, "request.field_invalid", "source must be a string of at most 190 characters when present");
58
- return;
59
- }
60
- if (!deps.sessionStorage?.ownerOf) {
61
- sendError(res, 501, "capability.session_ownership_required", "external notify requires the session-ownership face (sessionStorage.ownerOf)");
62
- return;
63
- }
64
- const notifyOwner = await deps.sessionStorage.ownerOf(notifySession);
65
- if (notifyOwner === undefined) {
66
- sendError(res, 404, "not_found.session", "session not found");
67
- return;
68
- }
69
- const notifyOperator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
70
- if (!notifyOperator && notifyOwner !== null && notifyOwner !== principal) {
71
- sendError(res, 404, "not_found.session", "session not found");
72
- return;
73
- }
74
- const payload = {
75
- task_id: nb.task_id,
76
- status: nb.status,
77
- summary: nb.summary,
78
- ...(nb.result !== undefined ? { result: nb.result } : {}),
79
- ...(nb.seq !== undefined ? { seq: nb.seq } : {}),
80
- ...(nb.source !== undefined ? { source: nb.source } : {}),
81
- };
82
- const liveTaskId = await deps.runStore?.getActiveTaskId?.(notifySession).catch(() => undefined);
83
- const liveStream = liveTaskId !== undefined && liveTaskId !== null ? steerableRuns.get(liveTaskId) : undefined;
84
- if (liveStream) {
11
+ switch (match.decl.id) {
12
+ case "session-notify": {
13
+ if (rateLimited(req, res) || quotaExceeded(req, res))
14
+ return;
15
+ const principal = gatedPrincipal(req, deps.config);
16
+ if (deps.config.requirePrincipal && principal === undefined) {
17
+ sendPrincipalRefusal(res, req, deps.config);
18
+ return;
19
+ }
20
+ const notifySession = decodeURIComponent(match.params.sessionId);
21
+ let nb;
85
22
  try {
86
- await liveStream.notify(payload, { priority: "next" });
87
- sendJson(res, 200, { sessionId: notifySession, delivery: "live" });
23
+ nb = (await readJson(req));
24
+ }
25
+ catch {
26
+ sendError(res, 400, "request.body_shape", "body must be JSON: { task_id, status, summary, result?, seq?, source? }");
88
27
  return;
89
28
  }
90
- catch (e) {
91
- const code = e.code;
92
- if (typeof code === "string" && code.startsWith("notify.")) {
93
- sendError(res, 400, code, e instanceof Error ? e.message : "invalid notification");
29
+ if (typeof nb.task_id !== "string" || nb.task_id.trim().length === 0 || nb.task_id.length > 190) {
30
+ sendError(res, 400, "request.field_invalid", "task_id must be a non-empty string of at most 190 characters");
31
+ return;
32
+ }
33
+ const NOTIFY_STATUSES = ["completed", "failed", "killed", "cancelled", "event"];
34
+ const _notifyExhaustive = true;
35
+ void _notifyExhaustive;
36
+ if (typeof nb.status !== "string" || !NOTIFY_STATUSES.includes(nb.status)) {
37
+ sendError(res, 400, "request.field_invalid", `status must be one of ${NOTIFY_STATUSES.join("/")}`);
38
+ return;
39
+ }
40
+ if (typeof nb.summary !== "string" || nb.summary.trim().length === 0) {
41
+ sendError(res, 400, "request.field_invalid", "summary must be a non-empty string");
42
+ return;
43
+ }
44
+ if (nb.result !== undefined && typeof nb.result !== "string") {
45
+ sendError(res, 400, "request.field_invalid", "result must be a string when present");
46
+ return;
47
+ }
48
+ if (nb.seq !== undefined && (typeof nb.seq !== "number" || !Number.isInteger(nb.seq) || nb.seq < 1)) {
49
+ sendError(res, 400, "request.field_invalid", "seq must be a positive integer when present");
50
+ return;
51
+ }
52
+ if (nb.source !== undefined && (typeof nb.source !== "string" || nb.source.length > 190)) {
53
+ sendError(res, 400, "request.field_invalid", "source must be a string of at most 190 characters when present");
54
+ return;
55
+ }
56
+ if (!deps.sessionStorage?.ownerOf) {
57
+ sendError(res, 501, "capability.session_ownership_required", "external notify requires the session-ownership face (sessionStorage.ownerOf)");
58
+ return;
59
+ }
60
+ const notifyOwner = await deps.sessionStorage.ownerOf(notifySession);
61
+ if (notifyOwner === undefined) {
62
+ sendError(res, 404, "not_found.session", "session not found");
63
+ return;
64
+ }
65
+ const notifyOperator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
66
+ if (!notifyOperator && notifyOwner !== null && notifyOwner !== principal) {
67
+ sendError(res, 404, "not_found.session", "session not found");
68
+ return;
69
+ }
70
+ const payload = {
71
+ task_id: nb.task_id,
72
+ status: nb.status,
73
+ summary: nb.summary,
74
+ ...(nb.result !== undefined ? { result: nb.result } : {}),
75
+ ...(nb.seq !== undefined ? { seq: nb.seq } : {}),
76
+ ...(nb.source !== undefined ? { source: nb.source } : {}),
77
+ };
78
+ const liveTaskId = await deps.runStore?.getActiveTaskId?.(notifySession).catch(() => undefined);
79
+ const liveStream = liveTaskId !== undefined && liveTaskId !== null ? steerableRuns.get(liveTaskId) : undefined;
80
+ if (liveStream) {
81
+ try {
82
+ await liveStream.notify(payload, { priority: "next" });
83
+ sendJson(res, 200, { sessionId: notifySession, delivery: "live" });
94
84
  return;
95
85
  }
86
+ catch (e) {
87
+ const code = e.code;
88
+ if (typeof code === "string" && code.startsWith("notify.")) {
89
+ sendError(res, 400, code, e instanceof Error ? e.message : "invalid notification");
90
+ return;
91
+ }
92
+ }
96
93
  }
97
- }
98
- if (!deps.workflowCompletionInbox) {
99
- sendError(res, 501, "capability.workflow_store_required", "external notify park requires the workflow completion inbox (WORKFLOW_RUN_STORE)");
100
- return;
101
- }
102
- await deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(notifySession, notifyOwner ?? principal ?? null, { task_id: payload.task_id, task_type: "external", status: payload.status, summary: payload.summary, ...(payload.result !== undefined ? { result: payload.result } : {}), ...(payload.seq !== undefined ? { seq: payload.seq } : {}), ...(payload.source !== undefined ? { source: payload.source } : {}) }, Date.now()));
103
- sendJson(res, 202, { sessionId: notifySession, delivery: "parked", note: "no live stream — parked in the session inbox; drained as a task_notification on the next stream open" });
104
- return;
105
- }
106
- if (req.method === "POST" && SESSION_WAKE_RE.test(url)) {
107
- if (!deps.checkpointStore) {
108
- sendError(res, 501, "capability.checkpoint_store_required", "wake requires the checkpoint store");
109
- return;
110
- }
111
- if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
112
- return;
113
- const principal = gatedPrincipal(req, deps.config);
114
- if (deps.config.requirePrincipal && principal === undefined) {
115
- sendPrincipalRefusal(res, req, deps.config);
116
- return;
117
- }
118
- const wakeSession = decodeURIComponent(url.split("/")[3]);
119
- let wakeBody;
120
- try {
121
- wakeBody = (await readJson(req));
122
- }
123
- catch {
124
- sendError(res, 400, "request.body_shape", "body must be JSON: { message?: string }");
94
+ if (!deps.workflowCompletionInbox) {
95
+ sendError(res, 501, "capability.workflow_store_required", "external notify park requires the workflow completion inbox (WORKFLOW_RUN_STORE)");
96
+ return;
97
+ }
98
+ await deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(notifySession, notifyOwner ?? principal ?? null, { task_id: payload.task_id, task_type: "external", status: payload.status, summary: payload.summary, ...(payload.result !== undefined ? { result: payload.result } : {}), ...(payload.seq !== undefined ? { seq: payload.seq } : {}), ...(payload.source !== undefined ? { source: payload.source } : {}) }, Date.now()));
99
+ sendJson(res, 202, { sessionId: notifySession, delivery: "parked", note: "no live stream — parked in the session inbox; drained as a task_notification on the next stream open" });
125
100
  return;
126
101
  }
127
- if (wakeBody.message !== undefined && (typeof wakeBody.message !== "string" || wakeBody.message.length === 0)) {
128
- sendError(res, 400, "request.field_invalid", "message must be a non-empty string when present");
102
+ case "session-wake": {
103
+ if (!deps.checkpointStore) {
104
+ sendError(res, 501, "capability.checkpoint_store_required", "wake requires the checkpoint store");
105
+ return;
106
+ }
107
+ if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
108
+ return;
109
+ const principal = gatedPrincipal(req, deps.config);
110
+ if (deps.config.requirePrincipal && principal === undefined) {
111
+ sendPrincipalRefusal(res, req, deps.config);
112
+ return;
113
+ }
114
+ const wakeSession = decodeURIComponent(match.params.sessionId);
115
+ let wakeBody;
116
+ try {
117
+ wakeBody = (await readJson(req));
118
+ }
119
+ catch {
120
+ sendError(res, 400, "request.body_shape", "body must be JSON: { message?: string }");
121
+ return;
122
+ }
123
+ if (wakeBody.message !== undefined && (typeof wakeBody.message !== "string" || wakeBody.message.length === 0)) {
124
+ sendError(res, 400, "request.field_invalid", "message must be a non-empty string when present");
125
+ return;
126
+ }
127
+ const out = await resumeWake(wakeSession, wakeBody.message, principal, req, true);
128
+ sendResumeOutcome(res, out, deps.logger);
129
129
  return;
130
130
  }
131
- const out = await resumeWake(wakeSession, wakeBody.message, principal, req, true);
132
- sendResumeOutcome(res, out, deps.logger);
133
- return;
131
+ default:
132
+ return assertNever(match.decl.id);
134
133
  }
135
- miss.fell = true;
136
134
  }
135
+ export const NOTIFY_WAKE_ROUTES = [
136
+ { id: "session-wake", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/wake$/, label: "/v1/sessions/:id/$verb", billable: true, methods: ["POST"] },
137
+ { id: "session-notify", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/notify$/, label: "/v1/sessions/:id/$verb", methods: ["POST"] },
138
+ ];
137
139
  //# sourceMappingURL=notify-wake.js.map
@@ -5,6 +5,20 @@
5
5
  * 与 quotaExceeded 门同键、只读不消费)。本域零可变状态。
6
6
  */
7
7
  import type { IncomingMessage, ServerResponse } from "node:http";
8
- import type { RouteCtx } from "../route-ctx.js";
9
- export declare function handleObservability(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
8
+ import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
9
+ /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
10
+ * 的语义原样 =「本域已应答」;走到函数尾才是「没匹配上」,置 `miss.fell` 交回装配器继续下一个域。
11
+ * 跨域的东西(deps / 助手 / 单域状态)一律在首行解构出来,于是下面每一行都逐字不变。 */
12
+ export declare function handleObservability(req: IncomingMessage, res: ServerResponse, match: RouteMatch<ObservabilityRouteId>, ctx: RouteCtx): Promise<void>;
13
+ export declare const OBSERVABILITY_ROUTES: readonly [{
14
+ readonly id: "outcomes";
15
+ readonly path: "/v1/outcomes";
16
+ readonly methods: readonly ["GET"];
17
+ }, {
18
+ readonly id: "usage";
19
+ readonly path: "/v1/usage";
20
+ readonly methods: readonly ["GET"];
21
+ }];
22
+ /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
23
+ export type ObservabilityRouteId = RouteIdsOf<typeof OBSERVABILITY_ROUTES>;
10
24
  //# sourceMappingURL=observability.d.ts.map
@@ -1,63 +1,66 @@
1
1
  import { sendJson, sendError } from "../send.js";
2
2
  import { gatedPrincipal, isOperator, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
3
- export async function handleObservability(req, res, url, ctx) {
4
- const miss = { fell: false };
5
- await handleObservabilityBody(req, res, url, ctx, miss);
6
- return !miss.fell;
7
- }
8
- async function handleObservabilityBody(req, res, url, ctx, miss) {
3
+ import { assertNever } from "../route-ctx.js";
4
+ export async function handleObservability(req, res, match, ctx) {
9
5
  const { deps } = ctx;
10
- if (url === "/v1/outcomes" && req.method === "GET") {
11
- if (deps.config.requirePrincipal) {
12
- const principal = gatedPrincipal(req, deps.config);
13
- if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
14
- sendError(res, 403, "auth.operator_only", "outcomes view is operator-only on a multi-tenant worker");
6
+ switch (match.decl.id) {
7
+ case "outcomes": {
8
+ if (deps.config.requirePrincipal) {
9
+ const principal = gatedPrincipal(req, deps.config);
10
+ if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
11
+ sendError(res, 403, "auth.operator_only", "outcomes view is operator-only on a multi-tenant worker");
12
+ return;
13
+ }
14
+ }
15
+ if (!deps.outcomeSink?.summary) {
16
+ sendError(res, 501, "capability.outcome_ledger_required", "no queryable outcome ledger on this worker (File sink — read the JSONL dataset directly)");
15
17
  return;
16
18
  }
17
- }
18
- if (!deps.outcomeSink?.summary) {
19
- sendError(res, 501, "capability.outcome_ledger_required", "no queryable outcome ledger on this worker (File sink — read the JSONL dataset directly)");
20
- return;
21
- }
22
- const q = new URL(req.url ?? "/", "http://localhost").searchParams;
23
- const sig = q.get("signature") ?? undefined;
24
- try {
25
- const rows = await deps.outcomeSink.summary({ ...(sig ? { taskSignature: sig } : {}), includeAborted: q.get("includeAborted") === "true" });
26
- sendJson(res, 200, { outcomes: rows });
27
- }
28
- catch (err) {
29
- deps.logger?.warn?.("outcomes_summary_failed", { err: String(err) });
30
- sendError(res, 500, "internal.error", "outcome summary failed");
31
- }
32
- return;
33
- }
34
- if (url === "/v1/usage" && req.method === "GET") {
35
- const principal = gatedPrincipal(req, deps.config);
36
- if (!principal) {
37
- sendPrincipalRefusal(res, req, deps.config);
19
+ const q = new URLSearchParams(match.search);
20
+ const sig = q.get("signature") ?? undefined;
21
+ try {
22
+ const rows = await deps.outcomeSink.summary({ ...(sig ? { taskSignature: sig } : {}), includeAborted: q.get("includeAborted") === "true" });
23
+ sendJson(res, 200, { outcomes: rows });
24
+ }
25
+ catch (err) {
26
+ deps.logger?.warn?.("outcomes_summary_failed", { err: String(err) });
27
+ sendError(res, 500, "internal.error", "outcome summary failed");
28
+ }
38
29
  return;
39
30
  }
40
- const windowSec = deps.config.costQuotaWindowSec;
41
- if (!deps.costQuota) {
42
- sendJson(res, 200, { enabled: false, windowSec, maxTaskCostUsd: deps.config.maxTaskCostUsd, maxTaskTokens: deps.config.maxTaskTokens });
31
+ case "usage": {
32
+ const principal = gatedPrincipal(req, deps.config);
33
+ if (!principal) {
34
+ sendPrincipalRefusal(res, req, deps.config);
35
+ return;
36
+ }
37
+ const windowSec = deps.config.costQuotaWindowSec;
38
+ if (!deps.costQuota) {
39
+ sendJson(res, 200, { enabled: false, windowSec, maxTaskCostUsd: deps.config.maxTaskCostUsd, maxTaskTokens: deps.config.maxTaskTokens });
40
+ return;
41
+ }
42
+ const d = deps.costQuota.check(principal);
43
+ sendJson(res, 200, {
44
+ enabled: true,
45
+ windowSec,
46
+ usedMicroUsd: d.usedMicroUsd,
47
+ usedUsd: d.usedMicroUsd / 1_000_000,
48
+ limitMicroUsd: d.limitMicroUsd,
49
+ limitUsd: d.limitMicroUsd / 1_000_000,
50
+ remainingMicroUsd: Math.max(0, d.limitMicroUsd - d.usedMicroUsd),
51
+ overLimit: !d.allowed,
52
+ retryAfterSec: d.retryAfterSec,
53
+ maxTaskCostUsd: deps.config.maxTaskCostUsd,
54
+ maxTaskTokens: deps.config.maxTaskTokens,
55
+ });
43
56
  return;
44
57
  }
45
- const d = deps.costQuota.check(principal);
46
- sendJson(res, 200, {
47
- enabled: true,
48
- windowSec,
49
- usedMicroUsd: d.usedMicroUsd,
50
- usedUsd: d.usedMicroUsd / 1_000_000,
51
- limitMicroUsd: d.limitMicroUsd,
52
- limitUsd: d.limitMicroUsd / 1_000_000,
53
- remainingMicroUsd: Math.max(0, d.limitMicroUsd - d.usedMicroUsd),
54
- overLimit: !d.allowed,
55
- retryAfterSec: d.retryAfterSec,
56
- maxTaskCostUsd: deps.config.maxTaskCostUsd,
57
- maxTaskTokens: deps.config.maxTaskTokens,
58
- });
59
- return;
58
+ default:
59
+ return assertNever(match.decl.id);
60
60
  }
61
- miss.fell = true;
62
61
  }
62
+ export const OBSERVABILITY_ROUTES = [
63
+ { id: "outcomes", path: "/v1/outcomes", methods: ["GET"] },
64
+ { id: "usage", path: "/v1/usage", methods: ["GET"] },
65
+ ];
63
66
  //# sourceMappingURL=observability.js.map
@@ -31,6 +31,23 @@
31
31
  * 分层:本模块不值 import `server.ts`(那条边闭合运行时装载环),只 `import type`。
32
32
  */
33
33
  import type { IncomingMessage, ServerResponse } from "node:http";
34
- import type { RouteCtx } from "../route-ctx.js";
35
- export declare function handleRetention(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
34
+ import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
35
+ export declare function handleRetention(req: IncomingMessage, res: ServerResponse, match: RouteMatch<RetentionRouteId>, ctx: RouteCtx): Promise<void>;
36
+ export declare const RETENTION_ROUTES: readonly [{
37
+ readonly id: "retention-audit";
38
+ readonly path: "/v1/ops/retention/audit";
39
+ readonly methods: readonly ["GET"];
40
+ }, {
41
+ readonly id: "retention-hold-place";
42
+ readonly pattern: RegExp;
43
+ readonly label: "/v1/ops/retention/holds/:domain";
44
+ readonly methods: readonly ["PUT"];
45
+ }, {
46
+ readonly id: "retention-hold-release";
47
+ readonly pattern: RegExp;
48
+ readonly label: "/v1/ops/retention/holds/:domain";
49
+ readonly methods: readonly ["DELETE"];
50
+ }];
51
+ /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
52
+ export type RetentionRouteId = RouteIdsOf<typeof RETENTION_ROUTES>;
36
53
  //# sourceMappingURL=retention-ops.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import { sendJson, sendError } from "../send.js";
2
2
  import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
3
+ import { assertNever } from "../route-ctx.js";
3
4
  import { RETENTION_AUDIT_DEFAULT_LIMIT, RETENTION_AUDIT_MAX_LIMIT } from "../../plugins/retention-lane-store-sql.js";
4
- const HOLD_RE = /^\/v1\/ops\/retention\/holds\/([^/]*)$/;
5
5
  const AUDIT_PATH = "/v1/ops/retention/audit";
6
6
  const DOMAIN_MAX_CHARS = 190;
7
7
  const NOTE_MAX_CHARS = 4000;
@@ -24,20 +24,8 @@ function decodeCursor(raw) {
24
24
  return { ok: false, why: "cursor payload lacks a non-negative integer `id`" };
25
25
  return { ok: true, id };
26
26
  }
27
- export async function handleRetention(req, res, url, ctx) {
28
- const miss = { fell: false };
29
- await handleRetentionBody(req, res, url, ctx, miss);
30
- return !miss.fell;
31
- }
32
- async function handleRetentionBody(req, res, url, ctx, miss) {
27
+ export async function handleRetention(req, res, match, ctx) {
33
28
  const { deps } = ctx;
34
- const path = url.split("?")[0] ?? url;
35
- const holdMatch = req.method === "PUT" || req.method === "DELETE" ? HOLD_RE.exec(path) : null;
36
- const isAudit = req.method === "GET" && path === AUDIT_PATH;
37
- if (!holdMatch && !isAudit) {
38
- miss.fell = true;
39
- return;
40
- }
41
29
  const principal = gatedPrincipal(req, deps.config);
42
30
  if (deps.config.requirePrincipal && !principal) {
43
31
  sendPrincipalRefusal(res, req, deps.config);
@@ -52,13 +40,19 @@ async function handleRetentionBody(req, res, url, ctx, miss) {
52
40
  sendError(res, 501, "capability.retention_store_required", "managed retention requires a SQL store backend (DB_BACKEND=mysql|pg) — the local/file backend declares retention:'none' and holds nothing to govern");
53
41
  return;
54
42
  }
55
- if (holdMatch) {
56
- await handleHold(req, res, ctx, store, holdMatch[1] ?? "", principal);
57
- return;
43
+ switch (match.decl.id) {
44
+ case "retention-hold-place":
45
+ case "retention-hold-release":
46
+ await handleHold(req, res, ctx, store, match.params.domain ?? "", principal, match.decl.id === "retention-hold-place");
47
+ return;
48
+ case "retention-audit":
49
+ await handleAuditRead(res, store, match.search);
50
+ return;
51
+ default:
52
+ return assertNever(match.decl.id);
58
53
  }
59
- await handleAuditRead(req, res, store);
60
54
  }
61
- async function handleHold(req, res, ctx, store, rawDomain, principal) {
55
+ async function handleHold(req, res, ctx, store, rawDomain, principal, held) {
62
56
  const domain = ctx.helpers.safeDecode(rawDomain);
63
57
  if (domain === null) {
64
58
  sendError(res, 400, "request.id_invalid", "invalid retention domain segment");
@@ -70,7 +64,6 @@ async function handleHold(req, res, ctx, store, rawDomain, principal) {
70
64
  });
71
65
  return;
72
66
  }
73
- const held = req.method === "PUT";
74
67
  let note;
75
68
  if (held) {
76
69
  let raw;
@@ -123,8 +116,8 @@ async function handleHold(req, res, ctx, store, rawDomain, principal) {
123
116
  });
124
117
  sendJson(res, 200, held ? { domain, held: true, placedBy, placedAtMs: nowMs } : { domain, held: false });
125
118
  }
126
- async function handleAuditRead(req, res, store) {
127
- const q = new URL(req.url ?? "", "http://x").searchParams;
119
+ async function handleAuditRead(res, store, search) {
120
+ const q = new URLSearchParams(search);
128
121
  const limitRaw = q.get("limit");
129
122
  if (limitRaw !== null && !(/^[0-9]+$/.test(limitRaw) && Number(limitRaw) >= 1 && Number(limitRaw) <= RETENTION_AUDIT_MAX_LIMIT)) {
130
123
  sendError(res, 400, "request.query_invalid", `limit must be a decimal integer in 1..${RETENTION_AUDIT_MAX_LIMIT}`);
@@ -155,4 +148,9 @@ async function handleAuditRead(req, res, store) {
155
148
  ...(hasMore && last !== undefined ? { nextBefore: encodeCursor({ id: last.id }) } : {}),
156
149
  });
157
150
  }
151
+ export const RETENTION_ROUTES = [
152
+ { id: "retention-audit", path: AUDIT_PATH, methods: ["GET"] },
153
+ { id: "retention-hold-place", pattern: /^\/v1\/ops\/retention\/holds\/(?<domain>[^/]*)$/, label: "/v1/ops/retention/holds/:domain", methods: ["PUT"] },
154
+ { id: "retention-hold-release", pattern: /^\/v1\/ops\/retention\/holds\/(?<domain>[^/]*)$/, label: "/v1/ops/retention/holds/:domain", methods: ["DELETE"] },
155
+ ];
158
156
  //# sourceMappingURL=retention-ops.js.map
@@ -26,7 +26,7 @@
26
26
  * 日志,不上 wire。同族先例 = run/trace 属主门与 `POST /v1/tasks/:id/asks/:askId/decision` 的 404 姿势。
27
27
  */
28
28
  import type { IncomingMessage, ServerResponse } from "node:http";
29
- import type { RouteCtx } from "../route-ctx.js";
29
+ import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
30
30
  export declare const RULES_CC_IMPORT_PREPARE_PATH = "/v1/rules/cc-import/prepare";
31
31
  export declare const RULES_CC_IMPORT_REDEEM_PATH = "/v1/rules/cc-import/redeem";
32
32
  /** [ref] A7:File 规则直通票口两条(与 cc-import 两口并列,票语义分家见 `rules-consent.ts` 的 A7 段)。 */
@@ -34,5 +34,39 @@ export declare const RULES_LOCAL_IMPORT_PREPARE_PATH = "/v1/rules/local-import/p
34
34
  export declare const RULES_LOCAL_IMPORT_REDEEM_PATH = "/v1/rules/local-import/redeem";
35
35
  /** 撤销面两口共用的字面路径(GET 列举 / DELETE 撤销)。 */
36
36
  export declare const RULES_PATH = "/v1/rules";
37
- export declare function handleRules(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
37
+ export declare function handleRules(req: IncomingMessage, res: ServerResponse, match: RouteMatch<RulesRouteId>, ctx: RouteCtx): Promise<void>;
38
+ export declare const RULES_ROUTES: readonly [{
39
+ readonly id: "rules-cc-import-prepare";
40
+ readonly path: "/v1/rules/cc-import/prepare";
41
+ readonly methods: readonly ["POST"];
42
+ }, {
43
+ readonly id: "rules-cc-import-redeem";
44
+ readonly path: "/v1/rules/cc-import/redeem";
45
+ readonly methods: readonly ["POST"];
46
+ }, {
47
+ readonly id: "rules-local-import-prepare";
48
+ readonly path: "/v1/rules/local-import/prepare";
49
+ readonly methods: readonly ["POST"];
50
+ }, {
51
+ readonly id: "rules-local-import-redeem";
52
+ readonly path: "/v1/rules/local-import/redeem";
53
+ readonly methods: readonly ["POST"];
54
+ }, {
55
+ readonly id: "rules-list";
56
+ readonly path: "/v1/rules";
57
+ readonly methods: readonly ["GET"];
58
+ }, {
59
+ readonly id: "rules-revoke";
60
+ readonly path: "/v1/rules";
61
+ readonly credentialGated: readonly ["DELETE"];
62
+ readonly methods: readonly ["DELETE"];
63
+ }, {
64
+ readonly prefix: "/v1/rules/cc-import/";
65
+ readonly credentialGated: readonly ["POST"];
66
+ }, {
67
+ readonly prefix: "/v1/rules/local-import/";
68
+ readonly credentialGated: readonly ["POST"];
69
+ }];
70
+ /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
71
+ export type RulesRouteId = RouteIdsOf<typeof RULES_ROUTES>;
38
72
  //# sourceMappingURL=rules.d.ts.map