@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
@@ -3,9 +3,9 @@ import { ControlPlaneCorruptError } from "@sema-agent/core";
3
3
  import { sendJson, sendError } from "../send.js";
4
4
  import { buildOriginFence, readOriginAware } from "../../memory-origin-fence.js";
5
5
  import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
6
+ import { assertNever } from "../route-ctx.js";
6
7
  export const MEMORY_ORIGIN_EXTERNAL_PATH = "/v1/memory/origin/external";
7
8
  export const MEMORY_ORIGIN_CLEARANCES_PATH = "/v1/memory/origin/clearances";
8
- export const MEMORY_ORIGIN_CLEAR_RE = /^\/v1\/memory\/origin\/entries\/([^/]+)\/clear$/;
9
9
  const ClearRequestSchema = z.object({
10
10
  requestId: z.string().min(1),
11
11
  reason: z.string(),
@@ -106,21 +106,8 @@ function parseScopes(raw) {
106
106
  }
107
107
  return out.length > 0 ? out : null;
108
108
  }
109
- export async function handleMemoryOrigin(req, res, url, ctx) {
110
- const miss = { fell: false };
111
- await handleMemoryOriginBody(req, res, url, ctx, miss);
112
- return !miss.fell;
113
- }
114
- async function handleMemoryOriginBody(req, res, url, ctx, miss) {
109
+ export async function handleMemoryOrigin(req, res, match, ctx) {
115
110
  const { deps } = ctx;
116
- const path = url.split("?")[0] ?? url;
117
- const isExternal = req.method === "GET" && path === MEMORY_ORIGIN_EXTERNAL_PATH;
118
- const isClearances = req.method === "GET" && path === MEMORY_ORIGIN_CLEARANCES_PATH;
119
- const clearMatch = req.method === "POST" ? MEMORY_ORIGIN_CLEAR_RE.exec(path) : null;
120
- if (!isExternal && !isClearances && clearMatch === null) {
121
- miss.fell = true;
122
- return;
123
- }
124
111
  const principal = gatedPrincipal(req, deps.config);
125
112
  if (deps.config.requirePrincipal && !principal) {
126
113
  sendPrincipalRefusal(res, req, deps.config);
@@ -135,61 +122,74 @@ async function handleMemoryOriginBody(req, res, url, ctx, miss) {
135
122
  sendError(res, 501, "capability.memory_engine_required", "memory origin marking and clearance are not available on this deployment — they require a wired memory engine (MEMORY_ENGINE=on) whose backend owns the engine control plane (controlPlaneRoot); the clearance ledger and its write-ahead custody rows live in that plane, and on a backend without it they would land on one replica's local disk");
136
123
  return;
137
124
  }
138
- if (isExternal) {
139
- const scopes = parseScopes(new URL(req.url ?? "", "http://x").searchParams.getAll("scopes"));
140
- if (scopes === null) {
141
- sendError(res, 400, "request.query_invalid", "`scopes` is required and must name at least one memory scope (comma-separated, e.g. `?scopes=user:alice,org:acme`) — this deployment has no scope-enumeration read face yet, so name the same scope keys you use on `GET /v1/memory/export?scope=`; an answer here covers ONLY the scopes you named and an empty result must never be read as \"the store is clean\"");
125
+ switch (match.decl.id) {
126
+ case "memory-origin-external": {
127
+ const scopes = parseScopes(new URLSearchParams(match.search).getAll("scopes"));
128
+ if (scopes === null) {
129
+ sendError(res, 400, "request.query_invalid", "`scopes` is required and must name at least one memory scope (comma-separated, e.g. `?scopes=user:alice,org:acme`) — this deployment has no scope-enumeration read face yet, so name the same scope keys you use on `GET /v1/memory/export?scope=`; an answer here covers ONLY the scopes you named and an empty result must never be read as \"the store is clean\"");
130
+ return;
131
+ }
132
+ try {
133
+ const fence = buildOriginFence(await faces.listExternal(scopes), readOriginAware(new URLSearchParams(match.search)));
134
+ sendJson(res, 200, { scopes, entries: fence.entries, ...(fence.withheldOriginTagged !== undefined ? { withheldOriginTagged: fence.withheldOriginTagged } : {}) });
135
+ }
136
+ catch (err) {
137
+ if (!sentClassifiedFailure(res, ctx, "external", err))
138
+ sendError(res, 500, "internal.error", "memory external-origin listing failed");
139
+ }
142
140
  return;
143
141
  }
144
- try {
145
- const fence = buildOriginFence(await faces.listExternal(scopes), readOriginAware(new URL(req.url ?? "", "http://x").searchParams));
146
- sendJson(res, 200, { scopes, entries: fence.entries, ...(fence.withheldOriginTagged !== undefined ? { withheldOriginTagged: fence.withheldOriginTagged } : {}) });
147
- }
148
- catch (err) {
149
- if (!sentClassifiedFailure(res, ctx, "external", err))
150
- sendError(res, 500, "internal.error", "memory external-origin listing failed");
151
- }
152
- return;
153
- }
154
- if (isClearances) {
155
- try {
156
- sendJson(res, 200, { clearances: faces.listClearances().map(buildClearanceRowWire) });
157
- }
158
- catch (err) {
159
- if (!sentClassifiedFailure(res, ctx, "clearances", err))
160
- sendError(res, 500, "internal.error", "memory origin clearance listing failed");
142
+ case "memory-origin-clearances": {
143
+ try {
144
+ sendJson(res, 200, { clearances: faces.listClearances().map(buildClearanceRowWire) });
145
+ }
146
+ catch (err) {
147
+ if (!sentClassifiedFailure(res, ctx, "clearances", err))
148
+ sendError(res, 500, "internal.error", "memory origin clearance listing failed");
149
+ }
150
+ return;
161
151
  }
162
- return;
163
- }
164
- const entryId = ctx.helpers.safeDecode(clearMatch[1]);
165
- if (entryId === null) {
166
- sendError(res, 400, "request.id_invalid", "invalid memory entry id");
167
- return;
168
- }
169
- const raw = await ctx.helpers.readJson(req);
170
- const parsed = ClearRequestSchema.safeParse(raw);
171
- if (!parsed.success) {
172
- if (parsed.error.issues.some((i) => i.path.length > 0 && i.path[0] === "requestId")) {
173
- sendError(res, 400, "request.request_id_required", "memory origin clearance requires a non-empty `requestId` string — it is the audit attribution of WHO cleared this marker and the server never mints one for you");
152
+ case "memory-origin-entry-clear": {
153
+ const entryId = ctx.helpers.safeDecode(match.params.entryId);
154
+ if (entryId === null) {
155
+ sendError(res, 400, "request.id_invalid", "invalid memory entry id");
156
+ return;
157
+ }
158
+ const raw = await ctx.helpers.readJson(req);
159
+ const parsed = ClearRequestSchema.safeParse(raw);
160
+ if (!parsed.success) {
161
+ if (parsed.error.issues.some((i) => i.path.length > 0 && i.path[0] === "requestId")) {
162
+ sendError(res, 400, "request.request_id_required", "memory origin clearance requires a non-empty `requestId` string — it is the audit attribution of WHO cleared this marker and the server never mints one for you");
163
+ return;
164
+ }
165
+ sendError(res, 400, "request.body_shape", "invalid memory origin clearance request — expected an object with a non-empty `requestId` and a `reason` string (whether an EMPTY reason is acceptable is the engine's judgment, not this layer's)");
166
+ return;
167
+ }
168
+ try {
169
+ const receipt = await faces.clear(entryId, { requestId: parsed.data.requestId, reason: parsed.data.reason });
170
+ ctx.deps.logger?.info?.("memory_origin_cleared", {
171
+ principal: principal ?? null,
172
+ entryId,
173
+ clearanceId: receipt.clearanceId,
174
+ claimedRequestId: parsed.data.requestId,
175
+ landedSlug: receipt.landedSlug,
176
+ });
177
+ sendJson(res, 200, receipt);
178
+ }
179
+ catch (err) {
180
+ if (!sentClassifiedFailure(res, ctx, "clear", err))
181
+ sendError(res, 500, "internal.error", "memory origin clearance failed");
182
+ }
174
183
  return;
175
184
  }
176
- sendError(res, 400, "request.body_shape", "invalid memory origin clearance request — expected an object with a non-empty `requestId` and a `reason` string (whether an EMPTY reason is acceptable is the engine's judgment, not this layer's)");
177
- return;
178
- }
179
- try {
180
- const receipt = await faces.clear(entryId, { requestId: parsed.data.requestId, reason: parsed.data.reason });
181
- ctx.deps.logger?.info?.("memory_origin_cleared", {
182
- principal: principal ?? null,
183
- entryId,
184
- clearanceId: receipt.clearanceId,
185
- claimedRequestId: parsed.data.requestId,
186
- landedSlug: receipt.landedSlug,
187
- });
188
- sendJson(res, 200, receipt);
189
- }
190
- catch (err) {
191
- if (!sentClassifiedFailure(res, ctx, "clear", err))
192
- sendError(res, 500, "internal.error", "memory origin clearance failed");
185
+ default:
186
+ return assertNever(match.decl.id);
193
187
  }
194
188
  }
189
+ export const MEMORY_ORIGIN_ROUTES = [
190
+ { id: "memory-origin-external", path: MEMORY_ORIGIN_EXTERNAL_PATH, credentialGated: ["GET"], methods: ["GET"] },
191
+ { id: "memory-origin-clearances", path: MEMORY_ORIGIN_CLEARANCES_PATH, credentialGated: ["GET"], methods: ["GET"] },
192
+ { id: "memory-origin-entry-clear", pattern: /^\/v1\/memory\/origin\/entries\/(?<entryId>[^/]+)\/clear$/, label: "/v1/memory/origin/entries/:entryId/clear", methods: ["POST"] },
193
+ { prefix: "/v1/memory/origin/entries/", credentialGated: ["POST"] },
194
+ ];
195
195
  //# sourceMappingURL=memory-origin.js.map
@@ -6,7 +6,7 @@
6
6
  * 越界一律 404(零存在性 oracle)。本域零可变状态。
7
7
  */
8
8
  import type { IncomingMessage, ServerResponse } from "node:http";
9
- import type { RouteCtx } from "../route-ctx.js";
9
+ import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
10
10
  /**
11
11
  * [ref] 件3([ref] A2 车立的欠账,[ref] 我方认领)—— `GET /v1/memory/export` 的 keyset 分页上限。
12
12
  *
@@ -20,5 +20,24 @@ import type { RouteCtx } from "../route-ctx.js";
20
20
  * 本形解决,库侧的读放大另案。
21
21
  */
22
22
  export declare const MEMORY_EXPORT_MAX_LIMIT = 500;
23
- export declare function handleMemoryPolicy(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
23
+ /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
24
+ * 的语义原样 =「本域已应答」;走到函数尾才是「没匹配上」,置 `miss.fell` 交回装配器继续下一个域。
25
+ * 跨域的东西(deps / 助手 / 单域状态)一律在首行解构出来,于是下面每一行都逐字不变。 */
26
+ export declare function handleMemoryPolicy(req: IncomingMessage, res: ServerResponse, match: RouteMatch<MemoryPolicyRouteId>, ctx: RouteCtx): Promise<void>;
27
+ export declare const MEMORY_POLICY_ROUTES: readonly [{
28
+ readonly id: "memory-export";
29
+ readonly path: "/v1/memory/export";
30
+ readonly methods: readonly ["GET"];
31
+ }, {
32
+ readonly id: "policy";
33
+ readonly path: "/v1/policy";
34
+ readonly methods: readonly ["GET"];
35
+ }, {
36
+ readonly id: "memory-sync-scope";
37
+ readonly pattern: RegExp;
38
+ readonly label: "/v1/memory/sync/:scope";
39
+ readonly methods: readonly ["POST"];
40
+ }];
41
+ /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
42
+ export type MemoryPolicyRouteId = RouteIdsOf<typeof MEMORY_POLICY_ROUTES>;
24
43
  //# sourceMappingURL=memory-policy.d.ts.map
@@ -3,6 +3,7 @@ import { parseMemorySyncRequest } from "../../memory-sync.js";
3
3
  import { sendJson, sendError } from "../send.js";
4
4
  import { buildOriginFence, readOriginAware } from "../../memory-origin-fence.js";
5
5
  import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
6
+ import { assertNever } from "../route-ctx.js";
6
7
  const ORG_PREFIX = "org:";
7
8
  export const MEMORY_EXPORT_MAX_LIMIT = 500;
8
9
  function encodeExportCursor(c) {
@@ -60,133 +61,136 @@ function sendScopeGateRefusal(res, gate) {
60
61
  sendError(res, 404, "not_found.memory_scope", "not found");
61
62
  return true;
62
63
  }
63
- export async function handleMemoryPolicy(req, res, url, ctx) {
64
- const miss = { fell: false };
65
- await handleMemoryPolicyBody(req, res, url, ctx, miss);
66
- return !miss.fell;
67
- }
68
- async function handleMemoryPolicyBody(req, res, url, ctx, miss) {
64
+ export async function handleMemoryPolicy(req, res, match, ctx) {
69
65
  const { deps } = ctx;
70
66
  const { readJson } = ctx.helpers;
71
- if (url === "/v1/memory/export" && req.method === "GET") {
72
- const principal = gatedPrincipal(req, deps.config);
73
- if (!principal) {
74
- sendError(res, 401, "auth.unauthorized", "unauthorized");
75
- return;
76
- }
77
- if (deps.config.memoryEngineBackend === "file" || !deps.memoryExport) {
78
- sendError(res, 501, "capability.memory_store_required", "memory export requires a DB memory backend (MEMORY_ENGINE_BACKEND=pg|tidb)");
79
- return;
80
- }
81
- const q = new URL(req.url ?? "", "http://x").searchParams;
82
- const scope = q.get("scope");
83
- if (!scope) {
84
- sendError(res, 400, "request.query_invalid", "missing ?scope=<key>");
85
- return;
86
- }
87
- if (sendScopeGateRefusal(res, await gateMemoryScope(deps, principal, scope, "read")))
88
- return;
89
- const limitRaw = q.get("limit");
90
- if (limitRaw !== null && !(/^[0-9]+$/.test(limitRaw) && Number(limitRaw) >= 1 && Number(limitRaw) <= MEMORY_EXPORT_MAX_LIMIT)) {
91
- sendError(res, 400, "request.query_invalid", `limit must be a decimal integer in 1..${MEMORY_EXPORT_MAX_LIMIT}`);
92
- return;
93
- }
94
- const beforeRaw = q.get("before");
95
- if (beforeRaw !== null && limitRaw === null) {
96
- sendError(res, 400, "request.query_invalid", "before requires an explicit limit — without one this endpoint returns the whole scope by contract");
67
+ switch (match.decl.id) {
68
+ case "memory-export": {
69
+ const principal = gatedPrincipal(req, deps.config);
70
+ if (!principal) {
71
+ sendError(res, 401, "auth.unauthorized", "unauthorized");
72
+ return;
73
+ }
74
+ if (deps.config.memoryEngineBackend === "file" || !deps.memoryExport) {
75
+ sendError(res, 501, "capability.memory_store_required", "memory export requires a DB memory backend (MEMORY_ENGINE_BACKEND=pg|tidb)");
76
+ return;
77
+ }
78
+ const q = new URLSearchParams(match.search);
79
+ const scope = q.get("scope");
80
+ if (!scope) {
81
+ sendError(res, 400, "request.query_invalid", "missing ?scope=<key>");
82
+ return;
83
+ }
84
+ if (sendScopeGateRefusal(res, await gateMemoryScope(deps, principal, scope, "read")))
85
+ return;
86
+ const limitRaw = q.get("limit");
87
+ if (limitRaw !== null && !(/^[0-9]+$/.test(limitRaw) && Number(limitRaw) >= 1 && Number(limitRaw) <= MEMORY_EXPORT_MAX_LIMIT)) {
88
+ sendError(res, 400, "request.query_invalid", `limit must be a decimal integer in 1..${MEMORY_EXPORT_MAX_LIMIT}`);
89
+ return;
90
+ }
91
+ const beforeRaw = q.get("before");
92
+ if (beforeRaw !== null && limitRaw === null) {
93
+ sendError(res, 400, "request.query_invalid", "before requires an explicit limit — without one this endpoint returns the whole scope by contract");
94
+ return;
95
+ }
96
+ const decoded = beforeRaw === null ? undefined : decodeExportCursor(beforeRaw);
97
+ if (decoded !== undefined && !decoded.ok) {
98
+ sendError(res, 400, "request.query_invalid", "before must be the opaque `nextBefore` cursor from a previous page of this endpoint", { detail: decoded.why });
99
+ return;
100
+ }
101
+ try {
102
+ const fence = buildOriginFence(await deps.memoryExport(scope), readOriginAware(new URLSearchParams(match.search)));
103
+ const entries = fence.entries;
104
+ const withheld = fence.withheldOriginTagged !== undefined ? { withheldOriginTagged: fence.withheldOriginTagged } : {};
105
+ if (limitRaw === null) {
106
+ sendJson(res, 200, { scope, exportedAt: new Date().toISOString(), entries, ...withheld });
107
+ return;
108
+ }
109
+ const sorted = [...entries].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
110
+ const after = decoded?.ok === true ? sorted.filter((e) => e.id > decoded.cursor.id) : sorted;
111
+ const page = after.slice(0, Number(limitRaw));
112
+ const last = page[page.length - 1];
113
+ const hasMore = last !== undefined && after.length > page.length;
114
+ sendJson(res, 200, {
115
+ scope,
116
+ exportedAt: new Date().toISOString(),
117
+ entries: page,
118
+ ...(hasMore ? { truncated: true, nextBefore: encodeExportCursor({ id: last.id }) } : {}),
119
+ ...withheld,
120
+ });
121
+ }
122
+ catch (err) {
123
+ deps.logger?.warn?.("memory_export_failed", { scope, err: String(err) });
124
+ sendError(res, 500, "internal.error", "memory export failed");
125
+ }
97
126
  return;
98
127
  }
99
- const decoded = beforeRaw === null ? undefined : decodeExportCursor(beforeRaw);
100
- if (decoded !== undefined && !decoded.ok) {
101
- sendError(res, 400, "request.query_invalid", "before must be the opaque `nextBefore` cursor from a previous page of this endpoint", { detail: decoded.why });
128
+ case "memory-sync-scope": {
129
+ const principal = gatedPrincipal(req, deps.config);
130
+ if (!principal) {
131
+ sendError(res, 401, "auth.unauthorized", "unauthorized");
132
+ return;
133
+ }
134
+ if (deps.config.memoryEngineBackend === "file" || !deps.memorySync) {
135
+ sendError(res, 501, "capability.memory_store_required", "memory sync requires a DB memory backend (MEMORY_ENGINE_BACKEND=pg|tidb)");
136
+ return;
137
+ }
138
+ let scope;
139
+ try {
140
+ scope = decodeURIComponent(match.params.scope);
141
+ }
142
+ catch {
143
+ sendError(res, 400, "memory_sync_invalid_scope", "malformed percent-encoding in :scope");
144
+ return;
145
+ }
146
+ if (scope.length === 0) {
147
+ sendError(res, 400, "memory_sync_invalid_scope", "missing :scope");
148
+ return;
149
+ }
150
+ if (sendScopeGateRefusal(res, await gateMemoryScope(deps, principal, scope, "write")))
151
+ return;
152
+ const body = await readJson(req);
153
+ const parsed = parseMemorySyncRequest(body, scope);
154
+ if (!parsed.ok) {
155
+ sendError(res, 422, "memory_sync_invalid_body", parsed.error);
156
+ return;
157
+ }
158
+ try {
159
+ const out = await deps.memorySync(scope, parsed.value);
160
+ sendJson(res, 200, out);
161
+ }
162
+ catch (err) {
163
+ deps.logger?.warn?.("memory_sync_failed", { scope, peer: parsed.value.peer, err: String(err) });
164
+ sendError(res, 500, "internal.error", "memory sync failed");
165
+ }
102
166
  return;
103
167
  }
104
- try {
105
- const fence = buildOriginFence(await deps.memoryExport(scope), readOriginAware(new URL(req.url ?? "", "http://x").searchParams));
106
- const entries = fence.entries;
107
- const withheld = fence.withheldOriginTagged !== undefined ? { withheldOriginTagged: fence.withheldOriginTagged } : {};
108
- if (limitRaw === null) {
109
- sendJson(res, 200, { scope, exportedAt: new Date().toISOString(), entries, ...withheld });
168
+ case "policy": {
169
+ const principal = gatedPrincipal(req, deps.config);
170
+ if (deps.config.requirePrincipal && !principal) {
171
+ sendPrincipalRefusal(res, req, deps.config);
110
172
  return;
111
173
  }
112
- const sorted = [...entries].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
113
- const after = decoded?.ok === true ? sorted.filter((e) => e.id > decoded.cursor.id) : sorted;
114
- const page = after.slice(0, Number(limitRaw));
115
- const last = page[page.length - 1];
116
- const hasMore = last !== undefined && after.length > page.length;
117
174
  sendJson(res, 200, {
118
- scope,
119
- exportedAt: new Date().toISOString(),
120
- entries: page,
121
- ...(hasMore ? { truncated: true, nextBefore: encodeExportCursor({ id: last.id }) } : {}),
122
- ...withheld,
175
+ autonomy: deps.config.autonomy ?? null,
176
+ commandPolicy: deps.config.commandPolicy ?? [],
177
+ approvalRequire: deps.config.approvalRequire,
178
+ limits: {
179
+ maxTaskCostUsd: deps.config.maxTaskCostUsd,
180
+ maxTaskTokens: deps.config.maxTaskTokens,
181
+ maxPrincipalCostUsd: deps.config.maxPrincipalCostUsd,
182
+ costQuotaWindowSec: deps.config.costQuotaWindowSec,
183
+ },
123
184
  });
124
- }
125
- catch (err) {
126
- deps.logger?.warn?.("memory_export_failed", { scope, err: String(err) });
127
- sendError(res, 500, "internal.error", "memory export failed");
128
- }
129
- return;
130
- }
131
- if (req.method === "POST" && url.startsWith("/v1/memory/sync/")) {
132
- const principal = gatedPrincipal(req, deps.config);
133
- if (!principal) {
134
- sendError(res, 401, "auth.unauthorized", "unauthorized");
135
- return;
136
- }
137
- if (deps.config.memoryEngineBackend === "file" || !deps.memorySync) {
138
- sendError(res, 501, "capability.memory_store_required", "memory sync requires a DB memory backend (MEMORY_ENGINE_BACKEND=pg|tidb)");
139
- return;
140
- }
141
- let scope;
142
- try {
143
- scope = decodeURIComponent(url.slice("/v1/memory/sync/".length));
144
- }
145
- catch {
146
- sendError(res, 400, "memory_sync_invalid_scope", "malformed percent-encoding in :scope");
147
- return;
148
- }
149
- if (scope.length === 0) {
150
- sendError(res, 400, "memory_sync_invalid_scope", "missing :scope");
151
- return;
152
- }
153
- if (sendScopeGateRefusal(res, await gateMemoryScope(deps, principal, scope, "write")))
154
- return;
155
- const body = await readJson(req);
156
- const parsed = parseMemorySyncRequest(body, scope);
157
- if (!parsed.ok) {
158
- sendError(res, 422, "memory_sync_invalid_body", parsed.error);
159
- return;
160
- }
161
- try {
162
- const out = await deps.memorySync(scope, parsed.value);
163
- sendJson(res, 200, out);
164
- }
165
- catch (err) {
166
- deps.logger?.warn?.("memory_sync_failed", { scope, peer: parsed.value.peer, err: String(err) });
167
- sendError(res, 500, "internal.error", "memory sync failed");
168
- }
169
- return;
170
- }
171
- if (url === "/v1/policy" && req.method === "GET") {
172
- const principal = gatedPrincipal(req, deps.config);
173
- if (deps.config.requirePrincipal && !principal) {
174
- sendPrincipalRefusal(res, req, deps.config);
175
185
  return;
176
186
  }
177
- sendJson(res, 200, {
178
- autonomy: deps.config.autonomy ?? null,
179
- commandPolicy: deps.config.commandPolicy ?? [],
180
- approvalRequire: deps.config.approvalRequire,
181
- limits: {
182
- maxTaskCostUsd: deps.config.maxTaskCostUsd,
183
- maxTaskTokens: deps.config.maxTaskTokens,
184
- maxPrincipalCostUsd: deps.config.maxPrincipalCostUsd,
185
- costQuotaWindowSec: deps.config.costQuotaWindowSec,
186
- },
187
- });
188
- return;
187
+ default:
188
+ return assertNever(match.decl.id);
189
189
  }
190
- miss.fell = true;
191
190
  }
191
+ export const MEMORY_POLICY_ROUTES = [
192
+ { id: "memory-export", path: "/v1/memory/export", methods: ["GET"] },
193
+ { id: "policy", path: "/v1/policy", methods: ["GET"] },
194
+ { id: "memory-sync-scope", pattern: /^\/v1\/memory\/sync\/(?<scope>.*)$/, label: "/v1/memory/sync/:scope", methods: ["POST"] },
195
+ ];
192
196
  //# sourceMappingURL=memory-policy.js.map
@@ -9,17 +9,27 @@
9
9
  * 都在腿里),而不是在本域自建第二条驱动路径。本域零可变状态。
10
10
  */
11
11
  import type { IncomingMessage, ServerResponse } from "node:http";
12
- import { type RouteCtx } from "../route-ctx.js";
12
+ import { type RouteCtx, type RouteMatch, type RouteIdsOf } from "../route-ctx.js";
13
13
  /**
14
- * `POST /v1/sessions/:id/wake` 的路由形 —— **单源导出**,因为它有两个消费者:本模块的分发,
15
- * 和 `server.ts` 的 `isBillableSubmitPath`(wake 会烧模型,必须吃 drain / model-roster-pending /
16
- * 无 service-token 三道提交门)。
17
- *
18
- * 🔴 为什么提成常量:2026-07-31 的缝合审查出这两处**曾经不同步** —— 匹配形写死在本文件里,
19
- * 名单在 server.ts 里,wake 从来没进过名单,于是同一条 `driveResumeIntoRunLog` 模型腿
20
- * 从 `/v1/assistant/tasks/:id/resume` 进来受三道门管、从 wake 进来一道都不受。
21
- * 共用一个常量之后,再想漏就得两处一起改。
14
+ * 🔴 `POST /v1/sessions/:id/wake` 的匹配形与它的 `billable` 位**同住一行声明**(下面的
15
+ * `NOTIFY_WAKE_ROUTES`)。历史:2026-07-31 的缝合审查出这两件事**曾经不同步** —— 匹配形写死在本文件的
16
+ * 判路代码里、`isBillableSubmitPath` 的名单写在 `server.ts` 里,wake 从来没进过名单,于是同一条
17
+ * `driveResumeIntoRunLog` 模型腿从 `/v1/assistant/tasks/:id/resume` 进来受三道提交门管、从 wake 进来
18
+ * 一道都不受。S-146 让两件事变成同一行的两列,再想漏就得把一行拆成两处才做得到。
22
19
  */
23
- export declare const SESSION_WAKE_RE: RegExp;
24
- export declare function handleNotifyWake(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
20
+ export declare function handleNotifyWake(req: IncomingMessage, res: ServerResponse, match: RouteMatch<NotifyWakeRouteId>, ctx: RouteCtx): Promise<void>;
21
+ export declare const NOTIFY_WAKE_ROUTES: readonly [{
22
+ readonly id: "session-wake";
23
+ readonly pattern: RegExp;
24
+ readonly label: "/v1/sessions/:id/$verb";
25
+ readonly billable: true;
26
+ readonly methods: readonly ["POST"];
27
+ }, {
28
+ readonly id: "session-notify";
29
+ readonly pattern: RegExp;
30
+ readonly label: "/v1/sessions/:id/$verb";
31
+ readonly methods: readonly ["POST"];
32
+ }];
33
+ /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
34
+ export type NotifyWakeRouteId = RouteIdsOf<typeof NOTIFY_WAKE_ROUTES>;
25
35
  //# sourceMappingURL=notify-wake.d.ts.map