@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.
- 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/package.json +1 -1
|
@@ -3,6 +3,7 @@ import { buildReadFaceWiring } from "../../read-face-posture.js";
|
|
|
3
3
|
import { resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
4
4
|
import { sendJson, sendError } from "../send.js";
|
|
5
5
|
import { gatedPrincipal, explicitOperatorOk, isLoopbackAddress, verifiedIdentitySource } from "../principal-gate.js";
|
|
6
|
+
import { assertNever } from "../route-ctx.js";
|
|
6
7
|
export function buildServerWiringGates(input) {
|
|
7
8
|
const gate = resolveStreamApprovalGate({
|
|
8
9
|
toolApprovalEnabled: input.toolApprovalEnabled,
|
|
@@ -78,56 +79,53 @@ export function zeroCredentialLoopbackDenied(remoteAddress, config) {
|
|
|
78
79
|
return false;
|
|
79
80
|
return !isLoopbackAddress(remoteAddress);
|
|
80
81
|
}
|
|
81
|
-
export async function handleDiagnostics(req, res,
|
|
82
|
-
const miss = { fell: false };
|
|
83
|
-
await handleDiagnosticsBody(req, res, url, ctx, miss);
|
|
84
|
-
return !miss.fell;
|
|
85
|
-
}
|
|
86
|
-
async function handleDiagnosticsBody(req, res, url, ctx, miss) {
|
|
82
|
+
export async function handleDiagnostics(req, res, match, ctx) {
|
|
87
83
|
const { deps } = ctx;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
84
|
+
switch (match.decl.id) {
|
|
85
|
+
case "diagnostics-wiring": {
|
|
86
|
+
if (deps.config.operatorPrincipals.length > 0 || deps.config.requirePrincipal) {
|
|
87
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
88
|
+
if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
|
|
89
|
+
sendError(res, 403, "auth.operator_only", "wiring diagnostics are operator-only on a multi-tenant worker");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!isLoopbackAddress(req.socket.remoteAddress) && verifiedIdentitySource(req, deps.config) === undefined) {
|
|
93
|
+
sendError(res, 403, "auth.loopback_only", "wiring diagnostics on a zero-credential deployment are readable from loopback only — configure SERVICE_AUTH_TOKEN to read remotely");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
94
96
|
}
|
|
95
|
-
if (
|
|
97
|
+
else if (zeroCredentialLoopbackDenied(req.socket.remoteAddress, deps.config)) {
|
|
96
98
|
sendError(res, 403, "auth.loopback_only", "wiring diagnostics on a zero-credential deployment are readable from loopback only — configure SERVICE_AUTH_TOKEN to read remotely");
|
|
97
99
|
return;
|
|
98
100
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
sendJson(res, 200, {
|
|
102
|
+
static: deps.staticWiring,
|
|
103
|
+
memoryPosture: deps.memoryPosture ?? null,
|
|
104
|
+
sqlEngine: deps.sqlEngineFacts?.() ?? null,
|
|
105
|
+
writeProtection: deps.writeProtection ?? null,
|
|
106
|
+
readFace: buildReadFaceWiring(deps.config),
|
|
107
|
+
configApply: deps.configApplyState?.() ?? null,
|
|
108
|
+
mcpRevocations: deps.mcpRevocationState?.() ?? null,
|
|
109
|
+
serverGates: buildServerWiringGates({
|
|
110
|
+
toolApprovalEnabled: deps.toolApproval !== undefined,
|
|
111
|
+
streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
|
|
112
|
+
backend: deps.backend,
|
|
113
|
+
checkpointStore: deps.checkpointStore,
|
|
114
|
+
durableApproval: deps.config.durableApproval === true,
|
|
115
|
+
durableApprovalSource: deps.config.durableApprovalSource,
|
|
116
|
+
streamAskWindowMs: deps.config.streamApproval?.windowMs ?? 0,
|
|
117
|
+
streamAskWindowMsSource: deps.config.streamAskWindowMsSource,
|
|
118
|
+
sessionAutoTitle: deps.config.sessionAutoTitle === true,
|
|
119
|
+
sessionAutoTitleSource: deps.config.sessionAutoTitleSource,
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
106
122
|
return;
|
|
107
123
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
memoryPosture: deps.memoryPosture ?? null,
|
|
111
|
-
sqlEngine: deps.sqlEngineFacts?.() ?? null,
|
|
112
|
-
writeProtection: deps.writeProtection ?? null,
|
|
113
|
-
readFace: buildReadFaceWiring(deps.config),
|
|
114
|
-
configApply: deps.configApplyState?.() ?? null,
|
|
115
|
-
mcpRevocations: deps.mcpRevocationState?.() ?? null,
|
|
116
|
-
serverGates: buildServerWiringGates({
|
|
117
|
-
toolApprovalEnabled: deps.toolApproval !== undefined,
|
|
118
|
-
streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
|
|
119
|
-
backend: deps.backend,
|
|
120
|
-
checkpointStore: deps.checkpointStore,
|
|
121
|
-
durableApproval: deps.config.durableApproval === true,
|
|
122
|
-
durableApprovalSource: deps.config.durableApprovalSource,
|
|
123
|
-
streamAskWindowMs: deps.config.streamApproval?.windowMs ?? 0,
|
|
124
|
-
streamAskWindowMsSource: deps.config.streamAskWindowMsSource,
|
|
125
|
-
sessionAutoTitle: deps.config.sessionAutoTitle === true,
|
|
126
|
-
sessionAutoTitleSource: deps.config.sessionAutoTitleSource,
|
|
127
|
-
}),
|
|
128
|
-
});
|
|
129
|
-
return;
|
|
124
|
+
default:
|
|
125
|
+
return assertNever(match.decl.id);
|
|
130
126
|
}
|
|
131
|
-
miss.fell = true;
|
|
132
127
|
}
|
|
128
|
+
export const DIAGNOSTICS_ROUTES = [
|
|
129
|
+
{ id: "diagnostics-wiring", path: "/v1/diagnostics/wiring", methods: ["GET"] },
|
|
130
|
+
];
|
|
133
131
|
//# sourceMappingURL=diagnostics.js.map
|
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
* (按 principal 自门,与 workflow 只读路由同列)。本域零可变状态。
|
|
6
6
|
*/
|
|
7
7
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
8
|
-
import type { RouteCtx } from "../route-ctx.js";
|
|
9
|
-
|
|
8
|
+
import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
|
|
9
|
+
/** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
|
|
10
|
+
* = 「本域已应答」;走到函数尾才是「没匹配上」。跨域的东西一律在首行解构出来。 */
|
|
11
|
+
export declare function handleFleet(req: IncomingMessage, res: ServerResponse, match: RouteMatch<FleetRouteId>, ctx: RouteCtx): Promise<void>;
|
|
12
|
+
export declare const FLEET_ROUTES: readonly [{
|
|
13
|
+
readonly id: "fleet-stream";
|
|
14
|
+
readonly path: "/v1/fleet/stream";
|
|
15
|
+
readonly methods: readonly ["GET"];
|
|
16
|
+
}];
|
|
17
|
+
/** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
|
|
18
|
+
export type FleetRouteId = RouteIdsOf<typeof FLEET_ROUTES>;
|
|
10
19
|
//# sourceMappingURL=fleet.d.ts.map
|
|
@@ -3,33 +3,31 @@ import { FLEET_SNAPSHOT_TERMINAL_MAX_ROWS, isTerminalWorkflowRowStatus, recentTe
|
|
|
3
3
|
import { taskNotificationFoldKey } from "../../orchestration/workflow-completion-inbox.js";
|
|
4
4
|
import { sendJson, sendError, sseHeaders } from "../send.js";
|
|
5
5
|
import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
await handleFleetBody(req, res, url, ctx, miss);
|
|
9
|
-
return !miss.fell;
|
|
10
|
-
}
|
|
11
|
-
async function handleFleetBody(req, res, url, ctx, miss) {
|
|
6
|
+
import { assertNever } from "../route-ctx.js";
|
|
7
|
+
export async function handleFleet(req, res, match, ctx) {
|
|
12
8
|
const { deps } = ctx;
|
|
13
9
|
const { sessionOwnerScope } = ctx.helpers;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
10
|
+
switch (match.decl.id) {
|
|
11
|
+
case "fleet-stream": {
|
|
12
|
+
if (!deps.fleetBus) {
|
|
13
|
+
sendError(res, 501, "capability.self_orchestration_required", "fleet stream requires self-orchestration / fleet wiring");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const { fleetWide } = sessionOwnerScope(req);
|
|
17
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
18
|
+
if (deps.config.requirePrincipal && !principal && !fleetWide) {
|
|
19
|
+
sendPrincipalRefusal(res, req, deps.config);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const fleetParams = new URLSearchParams(match.search);
|
|
23
|
+
const callerSession = fleetParams.get("session");
|
|
24
|
+
const observeOnly = fleetParams.get("observe") === "1";
|
|
25
|
+
await streamFleet(req, res, deps.fleetBus, fleetWide ? null : (principal ?? "default"), callerSession || null, observeOnly ? undefined : deps.workflowCompletionInbox, { store: deps.workflowRunStore, scope: fleetWide ? null : (principal ?? "default"), pullScope: principal ?? "default", ready: deps.fleetRecoveryDone });
|
|
24
26
|
return;
|
|
25
27
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const observeOnly = fleetParams.get("observe") === "1";
|
|
29
|
-
await streamFleet(req, res, deps.fleetBus, fleetWide ? null : (principal ?? "default"), callerSession || null, observeOnly ? undefined : deps.workflowCompletionInbox, { store: deps.workflowRunStore, scope: fleetWide ? null : (principal ?? "default"), pullScope: principal ?? "default", ready: deps.fleetRecoveryDone });
|
|
30
|
-
return;
|
|
28
|
+
default:
|
|
29
|
+
return assertNever(match.decl.id);
|
|
31
30
|
}
|
|
32
|
-
miss.fell = true;
|
|
33
31
|
}
|
|
34
32
|
function streamFleet(req, res, bus, callerScope, callerSession = null, completionInbox, terminal) {
|
|
35
33
|
return new Promise((resolve) => {
|
|
@@ -239,4 +237,7 @@ function streamFleet(req, res, bus, callerScope, callerSession = null, completio
|
|
|
239
237
|
})().catch(() => shutdown());
|
|
240
238
|
});
|
|
241
239
|
}
|
|
240
|
+
export const FLEET_ROUTES = [
|
|
241
|
+
{ id: "fleet-stream", path: "/v1/fleet/stream", methods: ["GET"] },
|
|
242
|
+
];
|
|
242
243
|
//# sourceMappingURL=fleet.js.map
|
|
@@ -13,7 +13,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
13
13
|
import { RateLimiter } from "../../observability/rate-limit.js";
|
|
14
14
|
import type { BakeState, BakeErrorCode } from "../../plugins/store-contracts.js";
|
|
15
15
|
import type { FlatServiceDeps } from "../server.js";
|
|
16
|
-
import type { RouteCtx } from "../route-ctx.js";
|
|
16
|
+
import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
|
|
17
17
|
/** 单域独占状态(lens1 §C2 的 12 条之一)。每个 server 实例一份——模块级会把限速状态跨实例串味
|
|
18
18
|
* (一个进程里建几十个 server 的测试会互相污染)。 */
|
|
19
19
|
export interface ImagesLocal {
|
|
@@ -26,12 +26,66 @@ export declare function coarseStatusForState(state: BakeState): "running" | "don
|
|
|
26
26
|
/** build.sh's structured exit code → the center band-composer UI's terminal `errorCode` (§P2.14 #2). null for an
|
|
27
27
|
* uncategorized failure (then `error` free-text + `exitCode` drive the UI). 10=dup-tag, 124=timeout, 507=disk. */
|
|
28
28
|
export declare function errorCodeForExit(exitCode: number | null | undefined, cancelled: boolean): BakeErrorCode;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
export declare
|
|
33
|
-
export declare const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
/** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
|
|
30
|
+
* 的语义原样 =「本域已应答」;走到函数尾才是「没匹配上」,置 `miss.fell` 交回装配器继续下一个域。
|
|
31
|
+
* 跨域的东西(deps / 助手 / 单域状态 / 每请求量)一律在首行解构出来,于是下面每一行都逐字不变。 */
|
|
32
|
+
export declare function handleImages(req: IncomingMessage, res: ServerResponse, match: RouteMatch<ImagesRouteId>, ctx: RouteCtx): Promise<void>;
|
|
33
|
+
export declare const IMAGES_ROUTES: readonly [{
|
|
34
|
+
readonly id: "images-index";
|
|
35
|
+
readonly path: "/v1/images";
|
|
36
|
+
readonly methods: readonly ["GET"];
|
|
37
|
+
}, {
|
|
38
|
+
readonly id: "image-bake-submit";
|
|
39
|
+
readonly path: "/v1/images/bakes";
|
|
40
|
+
readonly methods: readonly ["POST"];
|
|
41
|
+
}, {
|
|
42
|
+
readonly id: "image-bake-claim-next";
|
|
43
|
+
readonly path: "/v1/images/bakes/claim";
|
|
44
|
+
readonly methods: readonly ["POST"];
|
|
45
|
+
}, {
|
|
46
|
+
readonly id: "image-select";
|
|
47
|
+
readonly path: "/v1/images/select";
|
|
48
|
+
readonly methods: readonly ["POST"];
|
|
49
|
+
}, {
|
|
50
|
+
readonly id: "image-register";
|
|
51
|
+
readonly path: "/v1/images/register";
|
|
52
|
+
readonly methods: readonly ["POST"];
|
|
53
|
+
}, {
|
|
54
|
+
readonly id: "image-bake-events";
|
|
55
|
+
readonly pattern: RegExp;
|
|
56
|
+
readonly label: "/v1/images/bakes/:id/events";
|
|
57
|
+
readonly methods: readonly ["GET"];
|
|
58
|
+
}, {
|
|
59
|
+
readonly id: "image-bake-cancel";
|
|
60
|
+
readonly pattern: RegExp;
|
|
61
|
+
readonly label: "/v1/images/bakes/:id/cancel";
|
|
62
|
+
readonly methods: readonly ["POST"];
|
|
63
|
+
}, {
|
|
64
|
+
readonly id: "image-bake-claim";
|
|
65
|
+
readonly pattern: RegExp;
|
|
66
|
+
readonly label: "/v1/images/bakes/:id/claim";
|
|
67
|
+
readonly methods: readonly ["POST"];
|
|
68
|
+
}, {
|
|
69
|
+
readonly id: "image-bake-ingest";
|
|
70
|
+
readonly pattern: RegExp;
|
|
71
|
+
readonly label: "/v1/images/bakes/:id/ingest";
|
|
72
|
+
readonly methods: readonly ["POST"];
|
|
73
|
+
}, {
|
|
74
|
+
readonly id: "image-bake-detail";
|
|
75
|
+
readonly pattern: RegExp;
|
|
76
|
+
readonly label: "/v1/images/bakes/:id";
|
|
77
|
+
readonly methods: readonly ["GET"];
|
|
78
|
+
}, {
|
|
79
|
+
readonly id: "image-by-digest";
|
|
80
|
+
readonly pattern: RegExp;
|
|
81
|
+
readonly label: "/v1/images/digests/:digest";
|
|
82
|
+
readonly methods: readonly ["GET"];
|
|
83
|
+
}, {
|
|
84
|
+
readonly id: "image-by-profile";
|
|
85
|
+
readonly pattern: RegExp;
|
|
86
|
+
readonly label: "/v1/images/:profile";
|
|
87
|
+
readonly methods: readonly ["GET"];
|
|
88
|
+
}];
|
|
89
|
+
/** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
|
|
90
|
+
export type ImagesRouteId = RouteIdsOf<typeof IMAGES_ROUTES>;
|
|
37
91
|
//# sourceMappingURL=images.d.ts.map
|
|
@@ -7,6 +7,7 @@ import { streamSseLog } from "../sse-log.js";
|
|
|
7
7
|
import { sendJson, sendError } from "../send.js";
|
|
8
8
|
import { headerStr, safeEqual, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
9
9
|
import { scopedIdempotencyKey } from "../idempotency.js";
|
|
10
|
+
import { assertNever } from "../route-ctx.js";
|
|
10
11
|
export function createImagesLocal(deps) {
|
|
11
12
|
return { bakeSubmitLimiter: new RateLimiter(deps.config.imageBakes?.submitRateMax ?? 0, (deps.config.imageBakes?.submitRateWindowSec ?? 60) * 1000) };
|
|
12
13
|
}
|
|
@@ -38,24 +39,12 @@ export function errorCodeForExit(exitCode, cancelled) {
|
|
|
38
39
|
}
|
|
39
40
|
const RUNNER_ERROR_CODES = new Set(["band-conflict", "duplicate", "disk", "timeout", "cancelled"]);
|
|
40
41
|
const SANDBOX_BASE_PROFILE = "sandbox-base";
|
|
41
|
-
export
|
|
42
|
-
export const IMAGE_PROFILE_RE = /^\/v1\/images\/([^/]+)$/;
|
|
43
|
-
export const BAKE_ID_RE = /^\/v1\/images\/bakes\/([^/]+)$/;
|
|
44
|
-
export const BAKE_EVENTS_RE = /^\/v1\/images\/bakes\/([^/]+)\/events$/;
|
|
45
|
-
export const BAKE_CANCEL_RE = /^\/v1\/images\/bakes\/([^/]+)\/cancel$/;
|
|
46
|
-
export const BAKE_CLAIM_RE = /^\/v1\/images\/bakes\/([^/]+)\/claim$/;
|
|
47
|
-
export const BAKE_INGEST_RE = /^\/v1\/images\/bakes\/([^/]+)\/ingest$/;
|
|
48
|
-
export async function handleImages(req, res, url, ctx) {
|
|
49
|
-
const miss = { fell: false };
|
|
50
|
-
await handleImagesBody(req, res, url, ctx, miss);
|
|
51
|
-
return !miss.fell;
|
|
52
|
-
}
|
|
53
|
-
async function handleImagesBody(req, res, url, ctx, miss) {
|
|
42
|
+
export async function handleImages(req, res, match, ctx) {
|
|
54
43
|
const { deps } = ctx;
|
|
55
44
|
const { readJson } = ctx.helpers;
|
|
56
45
|
const { source } = ctx.req;
|
|
57
46
|
const { bakeSubmitLimiter } = ctx.local.images;
|
|
58
|
-
if (deps.imageBakes &&
|
|
47
|
+
if (deps.imageBakes && isBakeRouteId(match.decl.id)) {
|
|
59
48
|
const bakes = deps.imageBakes;
|
|
60
49
|
const idx = deps.imageIndex;
|
|
61
50
|
const principal = gatedPrincipal(req, deps.config);
|
|
@@ -63,7 +52,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
63
52
|
const isExplicitOperator = explicitOperatorOk(principal, operators);
|
|
64
53
|
const cfg = deps.config.imageBakes;
|
|
65
54
|
const isRunner = source !== undefined && source !== null && source === cfg.runnerPrincipal;
|
|
66
|
-
if (
|
|
55
|
+
if (match.decl.id === "image-bake-submit") {
|
|
67
56
|
if (!isExplicitOperator) {
|
|
68
57
|
sendError(res, 403, "auth.operator_only", "operator only");
|
|
69
58
|
return;
|
|
@@ -134,13 +123,12 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
134
123
|
sendJson(res, 202, bakeAcceptedBody(bake));
|
|
135
124
|
return;
|
|
136
125
|
}
|
|
137
|
-
|
|
138
|
-
if (evMatch) {
|
|
126
|
+
if (match.decl.id === "image-bake-events") {
|
|
139
127
|
if (!isExplicitOperator) {
|
|
140
128
|
sendError(res, 403, "auth.operator_only", "operator only");
|
|
141
129
|
return;
|
|
142
130
|
}
|
|
143
|
-
const bakeId = decodeURIComponent(
|
|
131
|
+
const bakeId = decodeURIComponent(match.params.bakeId);
|
|
144
132
|
const bake = await bakes.getBake(bakeId);
|
|
145
133
|
if (!bake) {
|
|
146
134
|
sendError(res, 404, "not_found.bake", "bake not found");
|
|
@@ -149,13 +137,12 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
149
137
|
await streamBakeEvents(req, res, bakes, bakeId, cfg.staleMs);
|
|
150
138
|
return;
|
|
151
139
|
}
|
|
152
|
-
|
|
153
|
-
if (cancelMatch) {
|
|
140
|
+
if (match.decl.id === "image-bake-cancel") {
|
|
154
141
|
if (!isExplicitOperator) {
|
|
155
142
|
sendError(res, 403, "auth.operator_only", "operator only");
|
|
156
143
|
return;
|
|
157
144
|
}
|
|
158
|
-
const bakeId = decodeURIComponent(
|
|
145
|
+
const bakeId = decodeURIComponent(match.params.bakeId);
|
|
159
146
|
const bake = await bakes.getBake(bakeId);
|
|
160
147
|
if (!bake) {
|
|
161
148
|
sendError(res, 404, "not_found.bake", "bake not found");
|
|
@@ -165,7 +152,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
165
152
|
sendJson(res, 202, { bakeId, cancelRequested: true, note: flagged ? "cancel requested — the runner kills the build at the next heartbeat" : "bake already terminal — cancel is a no-op" });
|
|
166
153
|
return;
|
|
167
154
|
}
|
|
168
|
-
if (
|
|
155
|
+
if (match.decl.id === "image-bake-claim-next") {
|
|
169
156
|
if (!isRunner) {
|
|
170
157
|
sendError(res, 403, "auth.bake_runner_only", "bake-runner only");
|
|
171
158
|
return;
|
|
@@ -186,13 +173,12 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
186
173
|
sendJson(res, 200, claimResponse(claimed));
|
|
187
174
|
return;
|
|
188
175
|
}
|
|
189
|
-
|
|
190
|
-
if (claimMatch) {
|
|
176
|
+
if (match.decl.id === "image-bake-claim") {
|
|
191
177
|
if (!isRunner) {
|
|
192
178
|
sendError(res, 403, "auth.bake_runner_only", "bake-runner only");
|
|
193
179
|
return;
|
|
194
180
|
}
|
|
195
|
-
const bakeId = decodeURIComponent(
|
|
181
|
+
const bakeId = decodeURIComponent(match.params.bakeId);
|
|
196
182
|
const body = (await readJson(req));
|
|
197
183
|
const runnerId = typeof body.runnerId === "string" && body.runnerId.length > 0 ? body.runnerId : (principal ?? source ?? cfg.runnerPrincipal);
|
|
198
184
|
const claimed = await bakes.claimBake(bakeId, runnerId, cfg.leaseMs);
|
|
@@ -203,13 +189,12 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
203
189
|
sendJson(res, 200, claimResponse(claimed));
|
|
204
190
|
return;
|
|
205
191
|
}
|
|
206
|
-
|
|
207
|
-
if (ingestMatch) {
|
|
192
|
+
if (match.decl.id === "image-bake-ingest") {
|
|
208
193
|
if (!isRunner) {
|
|
209
194
|
sendError(res, 403, "auth.bake_runner_only", "bake-runner only");
|
|
210
195
|
return;
|
|
211
196
|
}
|
|
212
|
-
const bakeId = decodeURIComponent(
|
|
197
|
+
const bakeId = decodeURIComponent(match.params.bakeId);
|
|
213
198
|
const bake = await bakes.getBake(bakeId);
|
|
214
199
|
if (!bake) {
|
|
215
200
|
sendError(res, 404, "not_found.bake", "bake not found");
|
|
@@ -248,13 +233,12 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
248
233
|
sendJson(res, result.status, { ...result.body, cancelRequested: after?.cancelRequested ?? false, leaseValid: true });
|
|
249
234
|
return;
|
|
250
235
|
}
|
|
251
|
-
|
|
252
|
-
if (idMatch) {
|
|
236
|
+
if (match.decl.id === "image-bake-detail") {
|
|
253
237
|
if (!isExplicitOperator) {
|
|
254
238
|
sendError(res, 403, "auth.operator_only", "operator only");
|
|
255
239
|
return;
|
|
256
240
|
}
|
|
257
|
-
const bakeId = decodeURIComponent(
|
|
241
|
+
const bakeId = decodeURIComponent(match.params.bakeId);
|
|
258
242
|
const bake = await bakes.getBake(bakeId);
|
|
259
243
|
if (!bake) {
|
|
260
244
|
sendError(res, 404, "not_found.bake", "bake not found");
|
|
@@ -263,16 +247,15 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
263
247
|
sendJson(res, 200, { bake: bakeView(bake) });
|
|
264
248
|
return;
|
|
265
249
|
}
|
|
266
|
-
|
|
267
|
-
return;
|
|
250
|
+
return assertNever(match.decl.id);
|
|
268
251
|
}
|
|
269
|
-
if (deps.imageIndex &&
|
|
252
|
+
if (deps.imageIndex && isIndexRouteId(match.decl.id)) {
|
|
270
253
|
const idx = deps.imageIndex;
|
|
271
254
|
const principal = gatedPrincipal(req, deps.config);
|
|
272
255
|
const operator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
|
|
273
256
|
const viewer = { operator, tenantId: principal ?? null };
|
|
274
|
-
const q = new
|
|
275
|
-
if (
|
|
257
|
+
const q = new URLSearchParams(match.search);
|
|
258
|
+
if (match.decl.id === "images-index") {
|
|
276
259
|
const limRaw = q.get("limit");
|
|
277
260
|
const { entries, nextCursor } = await idx.list({
|
|
278
261
|
...viewer,
|
|
@@ -286,7 +269,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
286
269
|
sendJson(res, 200, { images: entries, nextCursor });
|
|
287
270
|
return;
|
|
288
271
|
}
|
|
289
|
-
if (
|
|
272
|
+
if (match.decl.id === "image-select") {
|
|
290
273
|
const body = (await readJson(req));
|
|
291
274
|
const profile = typeof body.profile === "string" ? body.profile : "";
|
|
292
275
|
if (!profile) {
|
|
@@ -318,7 +301,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
318
301
|
});
|
|
319
302
|
return;
|
|
320
303
|
}
|
|
321
|
-
if (
|
|
304
|
+
if (match.decl.id === "image-register") {
|
|
322
305
|
if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
|
|
323
306
|
sendError(res, 403, "auth.operator_only", "operator only");
|
|
324
307
|
return;
|
|
@@ -351,9 +334,8 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
351
334
|
sendJson(res, 200, { id });
|
|
352
335
|
return;
|
|
353
336
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const entry = await idx.getByDigest(decodeURIComponent(digMatch[1]));
|
|
337
|
+
if (match.decl.id === "image-by-digest") {
|
|
338
|
+
const entry = await idx.getByDigest(decodeURIComponent(match.params.digest));
|
|
357
339
|
if (!entry || (!viewer.operator && entry.visibility === "tenant" && entry.tenantId !== viewer.tenantId)) {
|
|
358
340
|
sendError(res, 404, "not_found.image", "not found");
|
|
359
341
|
return;
|
|
@@ -361,9 +343,8 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
361
343
|
sendJson(res, 200, { image: entry });
|
|
362
344
|
return;
|
|
363
345
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const entry = await idx.latestPublished(decodeURIComponent(profMatch[1]), viewer);
|
|
346
|
+
if (match.decl.id === "image-by-profile") {
|
|
347
|
+
const entry = await idx.latestPublished(decodeURIComponent(match.params.profile), viewer);
|
|
367
348
|
if (!entry) {
|
|
368
349
|
sendError(res, 404, "not_found.image", "not found");
|
|
369
350
|
return;
|
|
@@ -371,13 +352,25 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
371
352
|
sendJson(res, 200, { image: entry });
|
|
372
353
|
return;
|
|
373
354
|
}
|
|
355
|
+
return assertNever(match.decl.id);
|
|
374
356
|
}
|
|
375
|
-
|
|
357
|
+
{
|
|
376
358
|
sendError(res, 501, "capability.image_index_required", "the sandbox-image-pool requires a SQL store backend (DB_BACKEND=mysql|pg) — the local backend wires no image index");
|
|
377
359
|
return;
|
|
378
360
|
}
|
|
379
|
-
miss.fell = true;
|
|
380
361
|
}
|
|
362
|
+
const BAKE_ROUTE_IDS = [
|
|
363
|
+
"image-bake-submit",
|
|
364
|
+
"image-bake-claim-next",
|
|
365
|
+
"image-bake-events",
|
|
366
|
+
"image-bake-cancel",
|
|
367
|
+
"image-bake-claim",
|
|
368
|
+
"image-bake-ingest",
|
|
369
|
+
"image-bake-detail",
|
|
370
|
+
];
|
|
371
|
+
const BAKE_ROUTE_ID_SET = new Set(BAKE_ROUTE_IDS);
|
|
372
|
+
const isBakeRouteId = (id) => BAKE_ROUTE_ID_SET.has(id);
|
|
373
|
+
const isIndexRouteId = (id) => !BAKE_ROUTE_ID_SET.has(id);
|
|
381
374
|
async function streamBakeEvents(req, res, bakes, bakeId, staleMs) {
|
|
382
375
|
await streamSseLog(req, res, {
|
|
383
376
|
statusOf: async (id) => {
|
|
@@ -576,4 +569,18 @@ async function ingestBakeLine(bakes, idx, bake, line, registry) {
|
|
|
576
569
|
}
|
|
577
570
|
return { status: 200, body: { accepted: true, terminal: affectedRows >= 1, indexId } };
|
|
578
571
|
}
|
|
572
|
+
export const IMAGES_ROUTES = [
|
|
573
|
+
{ id: "images-index", path: "/v1/images", methods: ["GET"] },
|
|
574
|
+
{ id: "image-bake-submit", path: "/v1/images/bakes", methods: ["POST"] },
|
|
575
|
+
{ id: "image-bake-claim-next", path: "/v1/images/bakes/claim", methods: ["POST"] },
|
|
576
|
+
{ id: "image-select", path: "/v1/images/select", methods: ["POST"] },
|
|
577
|
+
{ id: "image-register", path: "/v1/images/register", methods: ["POST"] },
|
|
578
|
+
{ id: "image-bake-events", pattern: /^\/v1\/images\/bakes\/(?<bakeId>[^/]+)\/events$/, label: "/v1/images/bakes/:id/events", methods: ["GET"] },
|
|
579
|
+
{ id: "image-bake-cancel", pattern: /^\/v1\/images\/bakes\/(?<bakeId>[^/]+)\/cancel$/, label: "/v1/images/bakes/:id/cancel", methods: ["POST"] },
|
|
580
|
+
{ id: "image-bake-claim", pattern: /^\/v1\/images\/bakes\/(?<bakeId>[^/]+)\/claim$/, label: "/v1/images/bakes/:id/claim", methods: ["POST"] },
|
|
581
|
+
{ id: "image-bake-ingest", pattern: /^\/v1\/images\/bakes\/(?<bakeId>[^/]+)\/ingest$/, label: "/v1/images/bakes/:id/ingest", methods: ["POST"] },
|
|
582
|
+
{ id: "image-bake-detail", pattern: /^\/v1\/images\/bakes\/(?<bakeId>[^/]+)$/, label: "/v1/images/bakes/:id", methods: ["GET"] },
|
|
583
|
+
{ id: "image-by-digest", pattern: /^\/v1\/images\/digests\/(?<digest>[^/]+)$/, label: "/v1/images/digests/:digest", methods: ["GET"] },
|
|
584
|
+
{ id: "image-by-profile", pattern: /^\/v1\/images\/(?<profile>[^/]+)$/, label: "/v1/images/:profile", methods: ["GET"] },
|
|
585
|
+
];
|
|
579
586
|
//# sourceMappingURL=images.js.map
|
|
@@ -6,6 +6,24 @@
|
|
|
6
6
|
* 任何持服务令牌者能提交/读任意租户的 leader run),owner 用 crypto 验证过的 `gatedPrincipal`。本域零可变状态。
|
|
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()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。
|
|
11
|
+
* 注意本域的落空语义:`deps.leaderEndpoint` 在但 URL 不匹配、或 endpoint 返回 undefined,都要**继续**
|
|
12
|
+
* 往下走(原文的 if 块不 return),故只有真的应答过才算已处理——由末尾的 `miss.fell` 如实表达。 */
|
|
13
|
+
/** 本域两条路径的**单一属主**(下面的声明行与那只接缝的回拼都引它,不再各写一遍字面量)。 */
|
|
14
|
+
export declare const LEADER_SUBMIT_PATH = "/v1/leader";
|
|
15
|
+
export declare function handleLeader(req: IncomingMessage, res: ServerResponse, match: RouteMatch<LeaderRouteId>, ctx: RouteCtx): Promise<void>;
|
|
16
|
+
export declare const LEADER_ROUTES: readonly [{
|
|
17
|
+
readonly id: "leader-submit";
|
|
18
|
+
readonly path: "/v1/leader";
|
|
19
|
+
readonly billable: true;
|
|
20
|
+
readonly methods: readonly ["POST"];
|
|
21
|
+
}, {
|
|
22
|
+
readonly id: "leader-detail";
|
|
23
|
+
readonly pattern: RegExp;
|
|
24
|
+
readonly label: "/v1/leader/:id";
|
|
25
|
+
readonly methods: readonly ["GET"];
|
|
26
|
+
}];
|
|
27
|
+
/** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
|
|
28
|
+
export type LeaderRouteId = RouteIdsOf<typeof LEADER_ROUTES>;
|
|
11
29
|
//# sourceMappingURL=leader.d.ts.map
|
|
@@ -1,48 +1,51 @@
|
|
|
1
1
|
import { sendJson, sendError } from "../send.js";
|
|
2
2
|
import { gatedPrincipal, headerStr, sendPrincipalRefusal } from "../principal-gate.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
import { assertNever } from "../route-ctx.js";
|
|
4
|
+
export const LEADER_SUBMIT_PATH = "/v1/leader";
|
|
5
|
+
export async function handleLeader(req, res, match, ctx) {
|
|
6
|
+
switch (match.decl.id) {
|
|
7
|
+
case "leader-submit":
|
|
8
|
+
case "leader-detail":
|
|
9
|
+
break;
|
|
10
|
+
default:
|
|
11
|
+
return assertNever(match.decl.id);
|
|
12
|
+
}
|
|
9
13
|
const { deps } = ctx;
|
|
10
14
|
const { readJson, rateLimited, quotaExceeded, leaseDenied, usageWindowDenied } = ctx.helpers;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (r) {
|
|
40
|
-
sendJson(res, r.status, r.body);
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
15
|
+
const endpoint = deps.leaderEndpoint;
|
|
16
|
+
if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
|
|
17
|
+
sendPrincipalRefusal(res, req, deps.config);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const requester = gatedPrincipal(req, deps.config) ?? null;
|
|
21
|
+
if (match.decl.id === "leader-submit") {
|
|
22
|
+
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)) || (await usageWindowDenied(req, res)))
|
|
23
|
+
return;
|
|
24
|
+
let body;
|
|
25
|
+
try {
|
|
26
|
+
body = await readJson(req);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
sendError(res, 400, "request.invalid_json", "invalid JSON body");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const r = await endpoint.handle("POST", LEADER_SUBMIT_PATH, body, requester, headerStr(req.headers["idempotency-key"]) ?? null);
|
|
33
|
+
if (r) {
|
|
34
|
+
sendJson(res, r.status, r.body);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const r = await endpoint.handle("GET", `${LEADER_SUBMIT_PATH}/${match.params.id}`, undefined, requester);
|
|
40
|
+
if (r) {
|
|
41
|
+
sendJson(res, r.status, r.body);
|
|
42
|
+
return;
|
|
44
43
|
}
|
|
45
44
|
}
|
|
46
|
-
|
|
45
|
+
throw new Error(`leader endpoint did not claim its own declared route: ${match.decl.id}`);
|
|
47
46
|
}
|
|
47
|
+
export const LEADER_ROUTES = [
|
|
48
|
+
{ id: "leader-submit", path: LEADER_SUBMIT_PATH, billable: true, methods: ["POST"] },
|
|
49
|
+
{ id: "leader-detail", pattern: /^\/v1\/leader\/(?<id>[^/]+)$/, label: "/v1/leader/:id", methods: ["GET"] },
|
|
50
|
+
];
|
|
48
51
|
//# sourceMappingURL=leader.js.map
|