@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
|
@@ -1,40 +1,41 @@
|
|
|
1
1
|
import { sendJson, sendError } from "../send.js";
|
|
2
2
|
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
3
|
+
import { assertNever } from "../route-ctx.js";
|
|
3
4
|
export const ADMIN_DRAIN_PATH = "/v1/admin/drain";
|
|
4
5
|
export const MAX_DRAIN_REASON_CHARS = 256;
|
|
5
6
|
const OPERATOR_ONLY_MESSAGE = "drain declaration is operator-only: the caller principal is not in OPERATOR_PRINCIPALS (an empty list means NO one is an operator — configure it to enable this endpoint)";
|
|
6
|
-
export async function handleAdminDrain(req, res,
|
|
7
|
-
const miss = { fell: false };
|
|
8
|
-
await handleAdminDrainBody(req, res, url, ctx, miss);
|
|
9
|
-
return !miss.fell;
|
|
10
|
-
}
|
|
11
|
-
async function handleAdminDrainBody(req, res, url, ctx, miss) {
|
|
7
|
+
export async function handleAdminDrain(req, res, match, ctx) {
|
|
12
8
|
const { deps } = ctx;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
9
|
+
switch (match.decl.id) {
|
|
10
|
+
case "admin-drain": {
|
|
11
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
12
|
+
if (deps.config.requirePrincipal && !principal) {
|
|
13
|
+
sendPrincipalRefusal(res, req, deps.config);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
|
|
17
|
+
sendError(res, 403, "auth.operator_only", OPERATOR_ONLY_MESSAGE);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const body = (await ctx.helpers.readJson(req));
|
|
21
|
+
const reason = body?.reason;
|
|
22
|
+
if (typeof reason !== "string" || reason.trim().length === 0 || reason.length > MAX_DRAIN_REASON_CHARS) {
|
|
23
|
+
sendError(res, 400, "request.field_invalid", `the drain reason is surfaced verbatim on the UNAUTHENTICATED /health endpoint (and in 503 draining bodies), so anyone who can reach this port can read it — do not put ticket ids, hostnames, or internal identifiers in it. body must be { reason: string }: a non-empty string of at most ${MAX_DRAIN_REASON_CHARS} characters`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (!deps.drainState) {
|
|
27
|
+
sendError(res, 501, "capability.drain_state_required", "this deployment has no drain state wired (embedded host?) — the declared reason would have nowhere to surface");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
deps.drainState.reason = reason.trim();
|
|
31
|
+
sendJson(res, 200, { ok: true });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
default:
|
|
35
|
+
return assertNever(match.decl.id);
|
|
36
36
|
}
|
|
37
|
-
deps.drainState.reason = reason.trim();
|
|
38
|
-
sendJson(res, 200, { ok: true });
|
|
39
37
|
}
|
|
38
|
+
export const ADMIN_DRAIN_ROUTES = [
|
|
39
|
+
{ id: "admin-drain", path: ADMIN_DRAIN_PATH, methods: ["POST"] },
|
|
40
|
+
];
|
|
40
41
|
//# sourceMappingURL=admin-drain.js.map
|
|
@@ -40,9 +40,35 @@
|
|
|
40
40
|
* prepare 即读到新 verdict(撤权后新声明具名拒;已声明会话 record wins,core 判,server 零码)。
|
|
41
41
|
*/
|
|
42
42
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
43
|
-
import type { RouteCtx } from "../route-ctx.js";
|
|
43
|
+
import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
|
|
44
44
|
export declare const ADMIN_MEMORY_OPTOUT_PATH = "/v1/admin/memory-optout";
|
|
45
45
|
/** 带参口(`:principal`)⇒ 路由声明表(`http/route-table.ts`)里走 `pattern` 行,不是 `path` 行。 */
|
|
46
46
|
export declare const ADMIN_MEMORY_OPTOUT_OVERRIDE_RE: RegExp;
|
|
47
|
-
export declare function handleMemoryOptOut(req: IncomingMessage, res: ServerResponse,
|
|
47
|
+
export declare function handleMemoryOptOut(req: IncomingMessage, res: ServerResponse, match: RouteMatch<MemoryOptOutRouteId>, ctx: RouteCtx): Promise<void>;
|
|
48
|
+
export declare const MEMORY_OPTOUT_ROUTES: readonly [{
|
|
49
|
+
readonly id: "memory-optout-grants-list";
|
|
50
|
+
readonly path: "/v1/admin/memory-optout";
|
|
51
|
+
readonly credentialGated: readonly ["GET"];
|
|
52
|
+
readonly methods: readonly ["GET"];
|
|
53
|
+
}, {
|
|
54
|
+
readonly id: "memory-optout-put-default";
|
|
55
|
+
readonly path: "/v1/admin/memory-optout";
|
|
56
|
+
readonly credentialGated: readonly ["PUT"];
|
|
57
|
+
readonly methods: readonly ["PUT"];
|
|
58
|
+
}, {
|
|
59
|
+
readonly id: "memory-optout-put-override";
|
|
60
|
+
readonly pattern: RegExp;
|
|
61
|
+
readonly label: "/v1/admin/memory-optout/:principal";
|
|
62
|
+
readonly methods: readonly ["PUT"];
|
|
63
|
+
}, {
|
|
64
|
+
readonly id: "memory-optout-delete-override";
|
|
65
|
+
readonly pattern: RegExp;
|
|
66
|
+
readonly label: "/v1/admin/memory-optout/:principal";
|
|
67
|
+
readonly methods: readonly ["DELETE"];
|
|
68
|
+
}, {
|
|
69
|
+
readonly prefix: "/v1/admin/memory-optout/";
|
|
70
|
+
readonly credentialGated: readonly ["PUT", "DELETE"];
|
|
71
|
+
}];
|
|
72
|
+
/** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
|
|
73
|
+
export type MemoryOptOutRouteId = RouteIdsOf<typeof MEMORY_OPTOUT_ROUTES>;
|
|
48
74
|
//# sourceMappingURL=admin-memory-optout.d.ts.map
|
|
@@ -1,41 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { sendJson, sendError } from "../send.js";
|
|
3
3
|
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
4
|
+
import { assertNever } from "../route-ctx.js";
|
|
4
5
|
import { memoryOptOutPrincipalShapeProblem } from "../../plugins/memory-optout-grant-store-sql.js";
|
|
5
6
|
export const ADMIN_MEMORY_OPTOUT_PATH = "/v1/admin/memory-optout";
|
|
6
7
|
export const ADMIN_MEMORY_OPTOUT_OVERRIDE_RE = /^\/v1\/admin\/memory-optout\/([^/]+)$/;
|
|
7
8
|
const PutBodySchema = z.object({ allow: z.boolean(), note: z.string().min(1).max(500).optional() }).strict();
|
|
8
|
-
export async function handleMemoryOptOut(req, res,
|
|
9
|
-
const miss = { fell: false };
|
|
10
|
-
await handleBody(req, res, url, ctx, miss);
|
|
11
|
-
return !miss.fell;
|
|
12
|
-
}
|
|
13
|
-
function decideVerb(method, path, safeDecode) {
|
|
14
|
-
if (path === ADMIN_MEMORY_OPTOUT_PATH) {
|
|
15
|
-
if (method === "GET")
|
|
16
|
-
return { kind: "list" };
|
|
17
|
-
if (method === "PUT")
|
|
18
|
-
return { kind: "putDefault" };
|
|
19
|
-
return undefined;
|
|
20
|
-
}
|
|
21
|
-
const m = ADMIN_MEMORY_OPTOUT_OVERRIDE_RE.exec(path);
|
|
22
|
-
if (m === null)
|
|
23
|
-
return undefined;
|
|
24
|
-
if (method !== "PUT" && method !== "DELETE")
|
|
25
|
-
return undefined;
|
|
26
|
-
const principal = safeDecode(m[1]);
|
|
27
|
-
if (principal === null)
|
|
28
|
-
return "malformed";
|
|
29
|
-
return method === "PUT" ? { kind: "putOverride", principal } : { kind: "deleteOverride", principal };
|
|
30
|
-
}
|
|
31
|
-
async function handleBody(req, res, url, ctx, miss) {
|
|
9
|
+
export async function handleMemoryOptOut(req, res, match, ctx) {
|
|
32
10
|
const { deps } = ctx;
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
miss.fell = true;
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
11
|
+
const target = match.decl.id === "memory-optout-grants-list" || match.decl.id === "memory-optout-put-default"
|
|
12
|
+
? undefined
|
|
13
|
+
: ctx.helpers.safeDecode(match.params.principal);
|
|
39
14
|
const principal = gatedPrincipal(req, deps.config);
|
|
40
15
|
if (deps.config.requirePrincipal && !principal) {
|
|
41
16
|
sendPrincipalRefusal(res, req, deps.config);
|
|
@@ -51,17 +26,22 @@ async function handleBody(req, res, url, ctx, miss) {
|
|
|
51
26
|
sendError(res, 501, "capability.memory_optout_grant_required", "this deployment has no memory opt-out grant table: the per-principal memory-capture opt-out authorization store lives on a SQL store backend (DB_BACKEND=mysql|pg) and the local backend deliberately keeps no forgetting twin — wire a SQL backend to administer opt-out grants");
|
|
52
27
|
return;
|
|
53
28
|
}
|
|
54
|
-
if (
|
|
29
|
+
if (target !== undefined && (target === null || memoryOptOutPrincipalShapeProblem(target) !== undefined)) {
|
|
55
30
|
sendError(res, 400, "request.path_malformed", "invalid memory opt-out grant principal — the path segment must percent-decode to a non-empty principal of at most 190 chars with no leading/trailing whitespace or control characters (the deployment default row is addressed by PUT on the collection root, not by a path segment)");
|
|
56
31
|
return;
|
|
57
32
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
33
|
+
switch (match.decl.id) {
|
|
34
|
+
case "memory-optout-grants-list":
|
|
35
|
+
sendJson(res, 200, await store.list());
|
|
36
|
+
return;
|
|
37
|
+
case "memory-optout-delete-override":
|
|
38
|
+
sendJson(res, 200, { principal: target, deleted: await store.deleteOverride(target) });
|
|
39
|
+
return;
|
|
40
|
+
case "memory-optout-put-default":
|
|
41
|
+
case "memory-optout-put-override":
|
|
42
|
+
break;
|
|
43
|
+
default:
|
|
44
|
+
return assertNever(match.decl.id);
|
|
65
45
|
}
|
|
66
46
|
const parsed = PutBodySchema.safeParse(await ctx.helpers.readJson(req));
|
|
67
47
|
if (!parsed.success) {
|
|
@@ -69,7 +49,14 @@ async function handleBody(req, res, url, ctx, miss) {
|
|
|
69
49
|
return;
|
|
70
50
|
}
|
|
71
51
|
const input = { allow: parsed.data.allow, updatedBy: operator, ...(parsed.data.note !== undefined ? { note: parsed.data.note } : {}) };
|
|
72
|
-
const row =
|
|
52
|
+
const row = match.decl.id === "memory-optout-put-default" ? await store.putDefault(input) : await store.putOverride(target, input);
|
|
73
53
|
sendJson(res, 200, row);
|
|
74
54
|
}
|
|
55
|
+
export const MEMORY_OPTOUT_ROUTES = [
|
|
56
|
+
{ id: "memory-optout-grants-list", path: ADMIN_MEMORY_OPTOUT_PATH, credentialGated: ["GET"], methods: ["GET"] },
|
|
57
|
+
{ id: "memory-optout-put-default", path: ADMIN_MEMORY_OPTOUT_PATH, credentialGated: ["PUT"], methods: ["PUT"] },
|
|
58
|
+
{ id: "memory-optout-put-override", pattern: /^\/v1\/admin\/memory-optout\/(?<principal>[^/]+)$/, label: "/v1/admin/memory-optout/:principal", methods: ["PUT"] },
|
|
59
|
+
{ id: "memory-optout-delete-override", pattern: /^\/v1\/admin\/memory-optout\/(?<principal>[^/]+)$/, label: "/v1/admin/memory-optout/:principal", methods: ["DELETE"] },
|
|
60
|
+
{ prefix: "/v1/admin/memory-optout/", credentialGated: ["PUT", "DELETE"] },
|
|
61
|
+
];
|
|
75
62
|
//# sourceMappingURL=admin-memory-optout.js.map
|
|
@@ -21,6 +21,19 @@
|
|
|
21
21
|
* 分层:本模块不值 import `server.ts`(那条边闭合运行时装载环),只 `import type`。
|
|
22
22
|
*/
|
|
23
23
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
24
|
-
import type { RouteCtx } from "../route-ctx.js";
|
|
25
|
-
export declare function handleAdoption(req: IncomingMessage, res: ServerResponse,
|
|
24
|
+
import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
|
|
25
|
+
export declare function handleAdoption(req: IncomingMessage, res: ServerResponse, match: RouteMatch<AdoptionRouteId>, ctx: RouteCtx): Promise<void>;
|
|
26
|
+
export declare const ADOPTION_ROUTES: readonly [{
|
|
27
|
+
readonly id: "adoption-submit";
|
|
28
|
+
readonly path: "/v1/adoption";
|
|
29
|
+
readonly credentialGated: readonly ["POST"];
|
|
30
|
+
readonly methods: readonly ["POST"];
|
|
31
|
+
}, {
|
|
32
|
+
readonly id: "adoption-detail";
|
|
33
|
+
readonly pattern: RegExp;
|
|
34
|
+
readonly label: "/v1/adoption/:id";
|
|
35
|
+
readonly methods: readonly ["GET"];
|
|
36
|
+
}];
|
|
37
|
+
/** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
|
|
38
|
+
export type AdoptionRouteId = RouteIdsOf<typeof ADOPTION_ROUTES>;
|
|
26
39
|
//# sourceMappingURL=adoption.d.ts.map
|
|
@@ -3,7 +3,7 @@ import { AdoptionRequestSchema } from "../../adoption/wire.js";
|
|
|
3
3
|
import { checkAdoptionPrincipalShape, checkAdoptionWidths } from "../../adoption/plan.js";
|
|
4
4
|
import { sendJson, sendError } from "../send.js";
|
|
5
5
|
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
6
|
-
|
|
6
|
+
import { assertNever } from "../route-ctx.js";
|
|
7
7
|
function sendOutcome(res, out) {
|
|
8
8
|
switch (out.kind) {
|
|
9
9
|
case "receipt":
|
|
@@ -38,20 +38,9 @@ function sendOutcome(res, out) {
|
|
|
38
38
|
function assertNoRejectCodeLeft(unhandled) {
|
|
39
39
|
throw new Error(`adoption: unhandled reject code ${JSON.stringify(unhandled)} — the wire contract's closed set and this switch have drifted apart`);
|
|
40
40
|
}
|
|
41
|
-
export async function handleAdoption(req, res,
|
|
42
|
-
const miss = { fell: false };
|
|
43
|
-
await handleAdoptionBody(req, res, url, ctx, miss);
|
|
44
|
-
return !miss.fell;
|
|
45
|
-
}
|
|
46
|
-
async function handleAdoptionBody(req, res, url, ctx, miss) {
|
|
41
|
+
export async function handleAdoption(req, res, match, ctx) {
|
|
47
42
|
const { deps } = ctx;
|
|
48
43
|
const { readJson } = ctx.helpers;
|
|
49
|
-
const isPost = req.method === "POST" && url === "/v1/adoption";
|
|
50
|
-
const idMatch = req.method === "GET" ? ADOPTION_ID_RE.exec(url) : null;
|
|
51
|
-
if (!isPost && !idMatch) {
|
|
52
|
-
miss.fell = true;
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
44
|
const principal = gatedPrincipal(req, deps.config);
|
|
56
45
|
if (deps.config.requirePrincipal && !principal) {
|
|
57
46
|
sendPrincipalRefusal(res, req, deps.config);
|
|
@@ -67,49 +56,60 @@ async function handleAdoptionBody(req, res, url, ctx, miss) {
|
|
|
67
56
|
return;
|
|
68
57
|
}
|
|
69
58
|
const runner = createAdoptionRunner({ store, dialect: deps.backend?.kind === "pg" ? "pg" : "tidb", now: Date.now, logger: deps.logger });
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
59
|
+
switch (match.decl.id) {
|
|
60
|
+
case "adoption-detail": {
|
|
61
|
+
const id = ctx.helpers.safeDecode(match.params.id);
|
|
62
|
+
if (id === null) {
|
|
63
|
+
sendError(res, 400, "request.id_invalid", "invalid adoption id");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const out = await runner.get(id);
|
|
67
|
+
if (!out) {
|
|
68
|
+
sendError(res, 404, "not_found.adoption", "adoption not found");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
sendOutcome(res, out);
|
|
74
72
|
return;
|
|
75
73
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
case "adoption-submit": {
|
|
75
|
+
let raw;
|
|
76
|
+
try {
|
|
77
|
+
raw = await readJson(req);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
sendError(res, 400, "request.invalid_json", "invalid JSON body");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const parsed = AdoptionRequestSchema.safeParse(raw);
|
|
84
|
+
if (!parsed.success) {
|
|
85
|
+
sendError(res, 400, "request.body_shape", "invalid adoption body — expected {fromPrincipal, toPrincipal} as non-empty strings");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const { fromPrincipal, toPrincipal } = parsed.data;
|
|
89
|
+
const badShape = checkAdoptionPrincipalShape(fromPrincipal, toPrincipal);
|
|
90
|
+
if (badShape !== undefined) {
|
|
91
|
+
sendError(res, 400, "request.field_invalid", "the adoption principals are not in a portable identity shape", { detail: badShape });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const tooWide = checkAdoptionWidths(fromPrincipal, toPrincipal);
|
|
95
|
+
if (tooWide !== undefined) {
|
|
96
|
+
sendError(res, 400, "request.field_invalid", "the adoption principals do not fit every identity column and derived key this adoption must write", { detail: tooWide });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (fromPrincipal === toPrincipal) {
|
|
100
|
+
sendError(res, 400, "request.field_invalid", "fromPrincipal and toPrincipal are the same — nothing to adopt");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const out = await runner.start(fromPrincipal, toPrincipal);
|
|
104
|
+
sendOutcome(res, out);
|
|
79
105
|
return;
|
|
80
106
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
let raw;
|
|
85
|
-
try {
|
|
86
|
-
raw = await readJson(req);
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
sendError(res, 400, "request.invalid_json", "invalid JSON body");
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
const parsed = AdoptionRequestSchema.safeParse(raw);
|
|
93
|
-
if (!parsed.success) {
|
|
94
|
-
sendError(res, 400, "request.body_shape", "invalid adoption body — expected {fromPrincipal, toPrincipal} as non-empty strings");
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
const { fromPrincipal, toPrincipal } = parsed.data;
|
|
98
|
-
const badShape = checkAdoptionPrincipalShape(fromPrincipal, toPrincipal);
|
|
99
|
-
if (badShape !== undefined) {
|
|
100
|
-
sendError(res, 400, "request.field_invalid", "the adoption principals are not in a portable identity shape", { detail: badShape });
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
const tooWide = checkAdoptionWidths(fromPrincipal, toPrincipal);
|
|
104
|
-
if (tooWide !== undefined) {
|
|
105
|
-
sendError(res, 400, "request.field_invalid", "the adoption principals do not fit every identity column and derived key this adoption must write", { detail: tooWide });
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
if (fromPrincipal === toPrincipal) {
|
|
109
|
-
sendError(res, 400, "request.field_invalid", "fromPrincipal and toPrincipal are the same — nothing to adopt");
|
|
110
|
-
return;
|
|
107
|
+
default:
|
|
108
|
+
return assertNever(match.decl.id);
|
|
111
109
|
}
|
|
112
|
-
const out = await runner.start(fromPrincipal, toPrincipal);
|
|
113
|
-
sendOutcome(res, out);
|
|
114
110
|
}
|
|
111
|
+
export const ADOPTION_ROUTES = [
|
|
112
|
+
{ id: "adoption-submit", path: "/v1/adoption", credentialGated: ["POST"], methods: ["POST"] },
|
|
113
|
+
{ id: "adoption-detail", pattern: /^\/v1\/adoption\/(?<id>[^/]+)$/, label: "/v1/adoption/:id", methods: ["GET"] },
|
|
114
|
+
];
|
|
115
115
|
//# sourceMappingURL=adoption.js.map
|
|
@@ -25,9 +25,16 @@
|
|
|
25
25
|
* · **能力位同源**:`capabilities.agentRoster` 与这里的 501 谓词是同一个 `deps.backgroundAgentStore`。
|
|
26
26
|
*/
|
|
27
27
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
28
|
-
import type { RouteCtx } from "../route-ctx.js";
|
|
28
|
+
import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
|
|
29
29
|
/** 名册窗的扫描上限(与 fleet 的终态窗同姿势:读面要有界)。窗满时回体带 `truncated:true`,
|
|
30
30
|
* `total` 于是诚实地是「窗内行数」而不是租户全量 —— 一个悄悄封顶的 total 比没有 total 更坏。 */
|
|
31
31
|
export declare const ROSTER_SCAN_LIMIT = 500;
|
|
32
|
-
export declare function handleAgentRoster(req: IncomingMessage, res: ServerResponse,
|
|
32
|
+
export declare function handleAgentRoster(req: IncomingMessage, res: ServerResponse, match: RouteMatch<AgentRosterRouteId>, ctx: RouteCtx): Promise<void>;
|
|
33
|
+
export declare const AGENT_ROSTER_ROUTES: readonly [{
|
|
34
|
+
readonly id: "agents-roster";
|
|
35
|
+
readonly path: "/v1/agents/roster";
|
|
36
|
+
readonly methods: readonly ["GET"];
|
|
37
|
+
}];
|
|
38
|
+
/** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
|
|
39
|
+
export type AgentRosterRouteId = RouteIdsOf<typeof AGENT_ROSTER_ROUTES>;
|
|
33
40
|
//# sourceMappingURL=agents-roster.d.ts.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sendJson, sendError } from "../send.js";
|
|
2
|
+
import { assertNever } from "../route-ctx.js";
|
|
2
3
|
export const ROSTER_SCAN_LIMIT = 500;
|
|
3
4
|
const ROSTER_STATUSES = ["running", "parked", "completed", "failed", "killed"];
|
|
4
5
|
const ROSTER_KEY_DISPOSITION = {
|
|
@@ -48,91 +49,92 @@ function decodeCursor(raw) {
|
|
|
48
49
|
return { ok: false, why: "cursor payload lacks a non-empty `handle`" };
|
|
49
50
|
return { ok: true, cursor: { spawnedAt, handle } };
|
|
50
51
|
}
|
|
51
|
-
export async function handleAgentRoster(req, res,
|
|
52
|
-
const miss = { fell: false };
|
|
53
|
-
await handleAgentRosterBody(req, res, url, ctx, miss);
|
|
54
|
-
return !miss.fell;
|
|
55
|
-
}
|
|
56
|
-
async function handleAgentRosterBody(req, res, url, ctx, miss) {
|
|
52
|
+
export async function handleAgentRoster(req, res, match, ctx) {
|
|
57
53
|
const { deps } = ctx;
|
|
58
54
|
const { sessionOwnerScope } = ctx.helpers;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
55
|
+
switch (match.decl.id) {
|
|
56
|
+
case "agents-roster": {
|
|
57
|
+
const { fleetWide, gateOwner } = sessionOwnerScope(req);
|
|
58
|
+
if (!fleetWide && gateOwner === null) {
|
|
59
|
+
sendError(res, 401, "auth.unauthorized", "unauthorized");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const store = deps.backgroundAgentStore;
|
|
63
|
+
if (!store) {
|
|
64
|
+
sendError(res, 501, "capability.background_agent_store_required", "the agent roster requires a durable background-agent store (DB_BACKEND=mysql|pg, or the local file backend)");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const q = new URLSearchParams(match.search);
|
|
68
|
+
const limitRaw = q.get("limit");
|
|
69
|
+
if (limitRaw !== null && !(/^[0-9]+$/.test(limitRaw) && Number(limitRaw) >= 1 && Number(limitRaw) <= 100)) {
|
|
70
|
+
sendError(res, 400, "request.query_invalid", "limit must be a decimal integer in 1..100");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const limit = limitRaw === null ? 20 : Number(limitRaw);
|
|
74
|
+
const beforeRaw = q.get("before");
|
|
75
|
+
const decoded = beforeRaw === null ? undefined : decodeCursor(beforeRaw);
|
|
76
|
+
if (decoded !== undefined && !decoded.ok) {
|
|
77
|
+
sendError(res, 400, "request.query_invalid", "before must be the opaque `nextBefore` cursor from a previous page of this endpoint", { detail: decoded.why });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const cursor = decoded?.ok === true ? decoded.cursor : undefined;
|
|
81
|
+
const statusRaw = q.get("status");
|
|
82
|
+
if (statusRaw !== null && !ROSTER_STATUSES.includes(statusRaw)) {
|
|
83
|
+
sendError(res, 400, "request.query_invalid", `status must be one of: ${ROSTER_STATUSES.join(", ")}`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const status = statusRaw === null ? undefined : statusRaw;
|
|
87
|
+
let scope;
|
|
88
|
+
if (gateOwner !== null) {
|
|
89
|
+
scope = gateOwner;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const raw = q.get("scope");
|
|
93
|
+
if (raw === null) {
|
|
94
|
+
scope = "default";
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
const s = raw.trim();
|
|
98
|
+
if (s === "" || s.length > 190) {
|
|
99
|
+
sendError(res, 400, "request.query_invalid", "scope must be a non-blank tenant principal of at most 190 characters");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
scope = s;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const session = q.get("session");
|
|
106
|
+
if (session !== null && session.trim() === "") {
|
|
107
|
+
sendError(res, 400, "request.query_invalid", "session must be a non-blank session id when present");
|
|
106
108
|
return;
|
|
107
109
|
}
|
|
108
|
-
|
|
110
|
+
const opts = { ...(status !== undefined ? { status } : {}), limit: ROSTER_SCAN_LIMIT + 1 };
|
|
111
|
+
const scanned = session !== null ? await store.listBySession(scope, session, opts) : await store.listByScope(scope, opts);
|
|
112
|
+
const truncated = scanned.length > ROSTER_SCAN_LIMIT;
|
|
113
|
+
let window = truncated ? scanned.slice(0, ROSTER_SCAN_LIMIT) : scanned;
|
|
114
|
+
if (truncated && window.length > 0 && scanned[ROSTER_SCAN_LIMIT].spawnedAt === window[window.length - 1].spawnedAt) {
|
|
115
|
+
const cut = window[window.length - 1].spawnedAt;
|
|
116
|
+
const clean = window.filter((r) => r.spawnedAt > cut);
|
|
117
|
+
if (clean.length > 0)
|
|
118
|
+
window = clean;
|
|
119
|
+
}
|
|
120
|
+
const sorted = [...window].sort((a, b) => (b.spawnedAt - a.spawnedAt) || (a.handle < b.handle ? 1 : a.handle > b.handle ? -1 : 0));
|
|
121
|
+
const scoped = cursor === undefined ? sorted : sorted.filter((r) => r.spawnedAt < cursor.spawnedAt || (r.spawnedAt === cursor.spawnedAt && r.handle < cursor.handle));
|
|
122
|
+
const page = scoped.slice(0, limit);
|
|
123
|
+
const last = page[page.length - 1];
|
|
124
|
+
const hasMore = last !== undefined && scoped.length > page.length;
|
|
125
|
+
sendJson(res, 200, {
|
|
126
|
+
agents: page.map(projectRow),
|
|
127
|
+
total: window.length,
|
|
128
|
+
...(truncated ? { truncated: true } : {}),
|
|
129
|
+
...(hasMore ? { nextBefore: encodeCursor({ spawnedAt: last.spawnedAt, handle: last.handle }) } : {}),
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
109
132
|
}
|
|
133
|
+
default:
|
|
134
|
+
return assertNever(match.decl.id);
|
|
110
135
|
}
|
|
111
|
-
const session = q.get("session");
|
|
112
|
-
if (session !== null && session.trim() === "") {
|
|
113
|
-
sendError(res, 400, "request.query_invalid", "session must be a non-blank session id when present");
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
const opts = { ...(status !== undefined ? { status } : {}), limit: ROSTER_SCAN_LIMIT + 1 };
|
|
117
|
-
const scanned = session !== null ? await store.listBySession(scope, session, opts) : await store.listByScope(scope, opts);
|
|
118
|
-
const truncated = scanned.length > ROSTER_SCAN_LIMIT;
|
|
119
|
-
let window = truncated ? scanned.slice(0, ROSTER_SCAN_LIMIT) : scanned;
|
|
120
|
-
if (truncated && window.length > 0 && scanned[ROSTER_SCAN_LIMIT].spawnedAt === window[window.length - 1].spawnedAt) {
|
|
121
|
-
const cut = window[window.length - 1].spawnedAt;
|
|
122
|
-
const clean = window.filter((r) => r.spawnedAt > cut);
|
|
123
|
-
if (clean.length > 0)
|
|
124
|
-
window = clean;
|
|
125
|
-
}
|
|
126
|
-
const sorted = [...window].sort((a, b) => (b.spawnedAt - a.spawnedAt) || (a.handle < b.handle ? 1 : a.handle > b.handle ? -1 : 0));
|
|
127
|
-
const scoped = cursor === undefined ? sorted : sorted.filter((r) => r.spawnedAt < cursor.spawnedAt || (r.spawnedAt === cursor.spawnedAt && r.handle < cursor.handle));
|
|
128
|
-
const page = scoped.slice(0, limit);
|
|
129
|
-
const last = page[page.length - 1];
|
|
130
|
-
const hasMore = last !== undefined && scoped.length > page.length;
|
|
131
|
-
sendJson(res, 200, {
|
|
132
|
-
agents: page.map(projectRow),
|
|
133
|
-
total: window.length,
|
|
134
|
-
...(truncated ? { truncated: true } : {}),
|
|
135
|
-
...(hasMore ? { nextBefore: encodeCursor({ spawnedAt: last.spawnedAt, handle: last.handle }) } : {}),
|
|
136
|
-
});
|
|
137
136
|
}
|
|
137
|
+
export const AGENT_ROSTER_ROUTES = [
|
|
138
|
+
{ id: "agents-roster", path: "/v1/agents/roster", methods: ["GET"] },
|
|
139
|
+
];
|
|
138
140
|
//# sourceMappingURL=agents-roster.js.map
|