@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.
- package/dist/boot/late-binds.d.ts +35 -0
- package/dist/boot/late-binds.js +20 -0
- package/dist/boot/stage-01-config.d.ts +7 -0
- package/dist/boot/stage-01-config.js +60 -0
- package/dist/boot/stage-02-registry-pull.d.ts +10 -0
- package/dist/boot/stage-02-registry-pull.js +11 -0
- package/dist/boot/stage-03-store-backend.d.ts +32 -0
- package/dist/boot/stage-03-store-backend.js +11 -0
- package/dist/boot/stage-04-stores.d.ts +69 -0
- package/dist/boot/stage-04-stores.js +49 -0
- package/dist/boot/stage-05-execution-env.d.ts +82 -0
- package/dist/boot/stage-05-execution-env.js +20 -0
- package/dist/boot/stage-06-runners.d.ts +143 -0
- package/dist/boot/stage-06-runners.js +207 -0
- package/dist/boot/stage-07-capability-layer.d.ts +157 -0
- package/dist/boot/stage-07-capability-layer.js +162 -0
- package/dist/boot/stage-08-reapers.d.ts +172 -0
- package/dist/boot/stage-08-reapers.js +78 -0
- package/dist/boot/stage-09-leader.d.ts +204 -0
- package/dist/boot/stage-09-leader.js +47 -0
- package/dist/boot/stage-10-http-server.d.ts +204 -0
- package/dist/boot/stage-10-http-server.js +289 -0
- package/dist/config-invariants.d.ts +9 -0
- package/dist/config-invariants.js +7 -1
- package/dist/config-types.d.ts +1 -1
- package/dist/http/dispatch.js +28 -8
- package/dist/http/route-ctx.d.ts +101 -4
- package/dist/http/route-ctx.js +4 -0
- package/dist/http/route-table.d.ts +71 -43
- package/dist/http/route-table.js +111 -187
- package/dist/http/routes/a2a-serve.d.ts +18 -3
- package/dist/http/routes/a2a-serve.js +25 -23
- package/dist/http/routes/admin-config-refresh.d.ts +9 -2
- package/dist/http/routes/admin-config-refresh.js +32 -31
- package/dist/http/routes/admin-drain.d.ts +9 -2
- package/dist/http/routes/admin-drain.js +32 -31
- package/dist/http/routes/admin-memory-optout.d.ts +28 -2
- package/dist/http/routes/admin-memory-optout.js +26 -39
- package/dist/http/routes/adoption.d.ts +15 -2
- package/dist/http/routes/adoption.js +52 -52
- package/dist/http/routes/agents-roster.d.ts +9 -2
- package/dist/http/routes/agents-roster.js +82 -80
- package/dist/http/routes/approvals-assistant.d.ts +58 -5
- package/dist/http/routes/approvals-assistant.js +37 -38
- package/dist/http/routes/attachments.d.ts +22 -2
- package/dist/http/routes/attachments.js +81 -72
- package/dist/http/routes/capabilities.d.ts +21 -2
- package/dist/http/routes/capabilities.js +174 -170
- package/dist/http/routes/config-catalog.d.ts +9 -2
- package/dist/http/routes/config-catalog.js +28 -31
- package/dist/http/routes/devices.d.ts +24 -2
- package/dist/http/routes/devices.js +19 -21
- package/dist/http/routes/diagnostics.d.ts +9 -2
- package/dist/http/routes/diagnostics.js +41 -43
- package/dist/http/routes/fleet.d.ts +11 -2
- package/dist/http/routes/fleet.js +23 -22
- package/dist/http/routes/images.d.ts +63 -9
- package/dist/http/routes/images.js +53 -46
- package/dist/http/routes/leader.d.ts +20 -2
- package/dist/http/routes/leader.js +43 -40
- package/dist/http/routes/memory-bundle.d.ts +15 -2
- package/dist/http/routes/memory-bundle.js +38 -38
- package/dist/http/routes/memory-compliance.d.ts +16 -4
- package/dist/http/routes/memory-compliance.js +44 -45
- package/dist/http/routes/memory-consolidation.d.ts +14 -2
- package/dist/http/routes/memory-consolidation.js +66 -66
- package/dist/http/routes/memory-origin.d.ts +23 -4
- package/dist/http/routes/memory-origin.js +65 -65
- package/dist/http/routes/memory-policy.d.ts +21 -2
- package/dist/http/routes/memory-policy.js +119 -115
- package/dist/http/routes/notify-wake.d.ts +21 -11
- package/dist/http/routes/notify-wake.js +118 -116
- package/dist/http/routes/observability.d.ts +16 -2
- package/dist/http/routes/observability.js +54 -51
- package/dist/http/routes/retention-ops.d.ts +19 -2
- package/dist/http/routes/retention-ops.js +20 -22
- package/dist/http/routes/rules.d.ts +36 -2
- package/dist/http/routes/rules.js +22 -24
- package/dist/http/routes/run-memory-capture.d.ts +10 -3
- package/dist/http/routes/run-memory-capture.js +84 -84
- package/dist/http/routes/runs.d.ts +102 -22
- package/dist/http/routes/runs.js +71 -88
- package/dist/http/routes/session-sync.d.ts +41 -2
- package/dist/http/routes/session-sync.js +25 -23
- package/dist/http/routes/sessions-list.d.ts +16 -2
- package/dist/http/routes/sessions-list.js +78 -75
- package/dist/http/routes/sessions.d.ts +77 -2
- package/dist/http/routes/sessions.js +50 -47
- package/dist/http/routes/shared-memory.d.ts +19 -2
- package/dist/http/routes/shared-memory.js +65 -63
- package/dist/http/routes/side-query.d.ts +11 -2
- package/dist/http/routes/side-query.js +81 -79
- package/dist/http/routes/tasks.d.ts +17 -2
- package/dist/http/routes/tasks.js +18 -11
- package/dist/http/routes/trace-usage.d.ts +40 -2
- package/dist/http/routes/trace-usage.js +46 -35
- package/dist/http/routes/workflows.d.ts +35 -3
- package/dist/http/routes/workflows.js +29 -29
- package/dist/main.js +20 -870
- package/dist/service-version.d.ts +5 -0
- package/dist/service-version.js +12 -0
- package/dist/trace/redact.d.ts +7 -6
- package/dist/trace/redact.js +419 -229
- 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
|
-
|
|
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,
|
|
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
|
-
|
|
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 (
|
|
142
|
+
if (match.decl.id === "approvals-stream") {
|
|
150
143
|
const scope = operator
|
|
151
|
-
? (new
|
|
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 (
|
|
149
|
+
if (match.decl.id === "approvals-list") {
|
|
157
150
|
const scope = operator
|
|
158
|
-
? (new
|
|
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
|
-
|
|
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(
|
|
179
|
-
exToolName =
|
|
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 (
|
|
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 (
|
|
217
|
-
const ownerParam = new
|
|
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 (
|
|
244
|
-
const ownerParam = new
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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 =
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|