agent-relay-server 0.32.1 → 0.32.3
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/docs/openapi.json +57 -127
- package/package.json +1 -1
- package/public/assets/{activity-C6nbfryG.js → activity-DT1JGHnp.js} +2 -2
- package/public/assets/{activity-C6nbfryG.js.map → activity-DT1JGHnp.js.map} +1 -1
- package/public/assets/{agent-profiles-FEITAgHs.js → agent-profiles-CrMemMkZ.js} +2 -2
- package/public/assets/{agent-profiles-FEITAgHs.js.map → agent-profiles-CrMemMkZ.js.map} +1 -1
- package/public/assets/{agents-D4S0yIbe.js → agents-Bl-rrgOy.js} +2 -2
- package/public/assets/{agents-D4S0yIbe.js.map → agents-Bl-rrgOy.js.map} +1 -1
- package/public/assets/{analytics-DM2g62T_.js → analytics-a663ak56.js} +2 -2
- package/public/assets/{analytics-DM2g62T_.js.map → analytics-a663ak56.js.map} +1 -1
- package/public/assets/{automation-3D2pQa1C.js → automation-CiaLThdO.js} +2 -2
- package/public/assets/{automation-3D2pQa1C.js.map → automation-CiaLThdO.js.map} +1 -1
- package/public/assets/{branch-state-badge-Bi4IbkOZ.js → branch-state-badge-D4ur3m3_.js} +2 -2
- package/public/assets/{branch-state-badge-Bi4IbkOZ.js.map → branch-state-badge-D4ur3m3_.js.map} +1 -1
- package/public/assets/{channels-QNp7zmA_.js → channels-o9KLTHoK.js} +2 -2
- package/public/assets/{channels-QNp7zmA_.js.map → channels-o9KLTHoK.js.map} +1 -1
- package/public/assets/{chat-jeXt_SFs.js → chat-5hvHZcAe.js} +2 -2
- package/public/assets/{chat-jeXt_SFs.js.map → chat-5hvHZcAe.js.map} +1 -1
- package/public/assets/{connectors-BGJARDui.js → connectors-CdC806mA.js} +2 -2
- package/public/assets/{connectors-BGJARDui.js.map → connectors-CdC806mA.js.map} +1 -1
- package/public/assets/{formatted-body-impl-B7FgqkYL.js → formatted-body-impl-Ca74OAEH.js} +2 -2
- package/public/assets/{formatted-body-impl-B7FgqkYL.js.map → formatted-body-impl-Ca74OAEH.js.map} +1 -1
- package/public/assets/{index-2m9mT8kV.js → index-C_33ymaw.js} +6 -6
- package/public/assets/{index-2m9mT8kV.js.map → index-C_33ymaw.js.map} +1 -1
- package/public/assets/{integrations-CJm8-FcG.js → integrations-1nxMizDY.js} +2 -2
- package/public/assets/{integrations-CJm8-FcG.js.map → integrations-1nxMizDY.js.map} +1 -1
- package/public/assets/{maintenance-CBvZrVAG.js → maintenance-DiFNzNPN.js} +2 -2
- package/public/assets/{maintenance-CBvZrVAG.js.map → maintenance-DiFNzNPN.js.map} +1 -1
- package/public/assets/{managed-agents-Dcmm8YKt.js → managed-agents-Do3dKvfj.js} +2 -2
- package/public/assets/{managed-agents-Dcmm8YKt.js.map → managed-agents-Do3dKvfj.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-7xjqdiEu.js → markdown-preview-impl-CLA0J255.js} +2 -2
- package/public/assets/{markdown-preview-impl-7xjqdiEu.js.map → markdown-preview-impl-CLA0J255.js.map} +1 -1
- package/public/assets/{memory-BmGNW61h.js → memory-IjwqFzBd.js} +2 -2
- package/public/assets/{memory-BmGNW61h.js.map → memory-IjwqFzBd.js.map} +1 -1
- package/public/assets/{messages-BvMMhoy-.js → messages-DjvWqHyn.js} +2 -2
- package/public/assets/{messages-BvMMhoy-.js.map → messages-DjvWqHyn.js.map} +1 -1
- package/public/assets/{orchestrators-DsstaupT.js → orchestrators-D2IqDxDT.js} +2 -2
- package/public/assets/{orchestrators-DsstaupT.js.map → orchestrators-D2IqDxDT.js.map} +1 -1
- package/public/assets/{overview-kK6PTce3.js → overview-DKC3TbAh.js} +2 -2
- package/public/assets/{overview-kK6PTce3.js.map → overview-DKC3TbAh.js.map} +1 -1
- package/public/assets/{pairs-BEFvTW6X.js → pairs-WpKCPE1n.js} +2 -2
- package/public/assets/{pairs-BEFvTW6X.js.map → pairs-WpKCPE1n.js.map} +1 -1
- package/public/assets/{security-Dc5wZwv0.js → security-BF7ZtPQe.js} +2 -2
- package/public/assets/{security-Dc5wZwv0.js.map → security-BF7ZtPQe.js.map} +1 -1
- package/public/assets/{settings-CEtJrORa.js → settings-CQnjrTa-.js} +2 -2
- package/public/assets/{settings-CEtJrORa.js.map → settings-CQnjrTa-.js.map} +1 -1
- package/public/assets/{store-DkmReBlH.js → store-C9VcSo05.js} +2 -2
- package/public/assets/{store-DkmReBlH.js.map → store-C9VcSo05.js.map} +1 -1
- package/public/assets/{tasks-pQKtxqeV.js → tasks-CbN_GSSb.js} +2 -2
- package/public/assets/{tasks-pQKtxqeV.js.map → tasks-CbN_GSSb.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-Cc769mYy.js → terminal-viewer-impl-BJRohThT.js} +2 -2
- package/public/assets/{terminal-viewer-impl-Cc769mYy.js.map → terminal-viewer-impl-BJRohThT.js.map} +1 -1
- package/public/assets/{work-queue-DjAanr02.js → work-queue-C5xLBLmm.js} +2 -2
- package/public/assets/{work-queue-DjAanr02.js.map → work-queue-C5xLBLmm.js.map} +1 -1
- package/public/assets/{workspaces-DLBNyR4k.js → workspaces-D91H3wDX.js} +2 -2
- package/public/assets/{workspaces-DLBNyR4k.js.map → workspaces-D91H3wDX.js.map} +1 -1
- package/public/index.html +2 -2
- package/scripts/orchestrator-spawn-smoke.ts +2 -1
- package/src/automations.ts +2 -4
- package/src/managed-policy.ts +2 -4
- package/src/mcp.ts +3 -3
- package/src/ratchet-files.ts +37 -0
- package/src/routes/_shared.ts +376 -0
- package/src/routes/activity.ts +61 -0
- package/src/routes/agent-profiles.ts +47 -0
- package/src/routes/agent-sessions.ts +488 -0
- package/src/routes/agents-spawn.ts +274 -0
- package/src/routes/agents.ts +251 -0
- package/src/routes/artifacts.ts +226 -0
- package/src/routes/automations.ts +83 -0
- package/src/routes/commands.ts +317 -0
- package/src/routes/config.ts +66 -0
- package/src/routes/connectors.ts +108 -0
- package/src/routes/inbox.ts +142 -0
- package/src/routes/index.ts +293 -0
- package/src/routes/insights.ts +81 -0
- package/src/routes/integrations.ts +592 -0
- package/src/routes/memory.ts +337 -0
- package/src/routes/messages.ts +529 -0
- package/src/routes/orchestrator-bootstrap.ts +100 -0
- package/src/routes/orchestrator-proxy.ts +160 -0
- package/src/routes/orchestrator.ts +490 -0
- package/src/routes/pairs.ts +197 -0
- package/src/routes/provider-config.ts +112 -0
- package/src/routes/recipes.ts +113 -0
- package/src/routes/spawn-policy.ts +231 -0
- package/src/routes/spec.ts +54 -0
- package/src/routes/sse.ts +9 -0
- package/src/routes/stats.ts +32 -0
- package/src/routes/steward.ts +45 -0
- package/src/routes/tasks.ts +174 -0
- package/src/routes/tokens.ts +311 -0
- package/src/routes/workspaces.ts +355 -0
- package/src/routes.ts +3 -6892
- package/src/runtime-tokens.ts +17 -8
- package/src/security.ts +0 -2
- package/src/validation.ts +134 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Auto-split from routes.ts (#299). Domain: spec.
|
|
2
|
+
import { error, type Handler } from "./_shared";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
let cachedSpec: { data: unknown; etag: string } | null = null;
|
|
7
|
+
|
|
8
|
+
function loadSpec(): { data: unknown; etag: string } | null {
|
|
9
|
+
try {
|
|
10
|
+
const specPath = resolve(import.meta.dir, "../docs/openapi.json");
|
|
11
|
+
const raw = readFileSync(specPath, "utf8");
|
|
12
|
+
const data = JSON.parse(raw);
|
|
13
|
+
const etag = `"${Bun.hash(raw).toString(36)}"`;
|
|
14
|
+
return { data, etag };
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const getApiSpec: Handler = (req) => {
|
|
21
|
+
if (!cachedSpec) cachedSpec = loadSpec();
|
|
22
|
+
if (!cachedSpec) return error("API spec not generated. Run: bun run docs:api", 404);
|
|
23
|
+
const ifNoneMatch = req.headers.get("if-none-match");
|
|
24
|
+
if (ifNoneMatch === cachedSpec.etag) {
|
|
25
|
+
return new Response(null, { status: 304, headers: { ETag: cachedSpec.etag } });
|
|
26
|
+
}
|
|
27
|
+
return Response.json(cachedSpec.data, {
|
|
28
|
+
headers: {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
ETag: cachedSpec.etag,
|
|
31
|
+
"Cache-Control": "public, max-age=60",
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const getApiDocs: Handler = () => {
|
|
37
|
+
if (!cachedSpec) cachedSpec = loadSpec();
|
|
38
|
+
const specUrl = cachedSpec ? "./spec" : "https://petstore3.swagger.io/api/v3/openapi.json";
|
|
39
|
+
const html = `<!doctype html>
|
|
40
|
+
<html>
|
|
41
|
+
<head>
|
|
42
|
+
<title>Agent Relay API</title>
|
|
43
|
+
<meta charset="utf-8" />
|
|
44
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
45
|
+
</head>
|
|
46
|
+
<body>
|
|
47
|
+
<script id="api-reference" data-url="${specUrl}"></script>
|
|
48
|
+
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
|
49
|
+
</body>
|
|
50
|
+
</html>`;
|
|
51
|
+
return new Response(html, {
|
|
52
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
53
|
+
});
|
|
54
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Auto-split from routes.ts (#299). Domain: sse.
|
|
2
|
+
import { createSSEStream } from "../sse";
|
|
3
|
+
import { type Handler } from "./_shared";
|
|
4
|
+
|
|
5
|
+
export const getEvents: Handler = (req) => {
|
|
6
|
+
const url = new URL(req.url);
|
|
7
|
+
const agentId = url.searchParams.get("for") || null;
|
|
8
|
+
return createSSEStream(agentId);
|
|
9
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Auto-split from routes.ts (#299). Domain: stats.
|
|
2
|
+
import { ANALYTICS_PERIODS, getAnalytics, getHealth, getStats, type AnalyticsPeriod } from "../db";
|
|
3
|
+
import { error, json, type Handler } from "./_shared";
|
|
4
|
+
import { listMaintenanceJobs, runLegacyMaintenanceReaper, runMaintenanceJobNow } from "../maintenance";
|
|
5
|
+
|
|
6
|
+
export const getStatsRoute: Handler = () => json(getStats());
|
|
7
|
+
|
|
8
|
+
export const getAnalyticsRoute: Handler = (req) => {
|
|
9
|
+
const period = new URL(req.url).searchParams.get("period") ?? "24h";
|
|
10
|
+
if (!(period in ANALYTICS_PERIODS)) {
|
|
11
|
+
return error(`period must be one of ${Object.keys(ANALYTICS_PERIODS).join(", ")}`);
|
|
12
|
+
}
|
|
13
|
+
return json(getAnalytics(period as AnalyticsPeriod));
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const getHealthRoute: Handler = () => json(getHealth());
|
|
17
|
+
|
|
18
|
+
export const getMaintenanceJobs: Handler = () => json(listMaintenanceJobs());
|
|
19
|
+
|
|
20
|
+
export const postMaintenanceJobRun: Handler = async (_req, params) => {
|
|
21
|
+
try {
|
|
22
|
+
const run = await runMaintenanceJobNow(params.id!);
|
|
23
|
+
return json({ run, jobs: listMaintenanceJobs() }, run.status === "failed" ? 500 : 202);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
if (e instanceof Error && e.message.includes("not found")) return error(e.message, 404);
|
|
26
|
+
throw e;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const postSystemReap: Handler = async () => {
|
|
31
|
+
return json(await runLegacyMaintenanceReaper());
|
|
32
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Auto-split from routes.ts (#299). Domain: steward.
|
|
2
|
+
import { ValidationError } from "../db";
|
|
3
|
+
import { cleanString } from "../validation";
|
|
4
|
+
import { emitConfigChanged } from "../sse";
|
|
5
|
+
import { error, json, parseBody, type Handler } from "./_shared";
|
|
6
|
+
import { getStewardConfigEntry, getWorkspaceConfigEntry, setStewardConfig, setWorkspaceConfig } from "../config-store";
|
|
7
|
+
import { isRecord } from "agent-relay-sdk";
|
|
8
|
+
|
|
9
|
+
export const getStewardConfigRoute: Handler = () => json(getStewardConfigEntry());
|
|
10
|
+
|
|
11
|
+
export const putStewardConfigRoute: Handler = async (req) => {
|
|
12
|
+
const parsed = await parseBody<unknown>(req);
|
|
13
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
14
|
+
try {
|
|
15
|
+
const value = isRecord(parsed.body) && Object.prototype.hasOwnProperty.call(parsed.body, "value")
|
|
16
|
+
? parsed.body.value
|
|
17
|
+
: parsed.body;
|
|
18
|
+
const updatedBy = isRecord(parsed.body) ? cleanString(parsed.body.updatedBy, "updatedBy", { max: 200 }) : undefined;
|
|
19
|
+
const entry = setStewardConfig(value, updatedBy);
|
|
20
|
+
emitConfigChanged(entry.namespace, entry.key, entry.version);
|
|
21
|
+
return json(entry, entry.version === 1 ? 201 : 200);
|
|
22
|
+
} catch (e) {
|
|
23
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
24
|
+
throw e;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const getWorkspaceConfigRoute: Handler = () => json(getWorkspaceConfigEntry());
|
|
29
|
+
|
|
30
|
+
export const putWorkspaceConfigRoute: Handler = async (req) => {
|
|
31
|
+
const parsed = await parseBody<unknown>(req);
|
|
32
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
33
|
+
try {
|
|
34
|
+
const value = isRecord(parsed.body) && Object.prototype.hasOwnProperty.call(parsed.body, "value")
|
|
35
|
+
? parsed.body.value
|
|
36
|
+
: parsed.body;
|
|
37
|
+
const updatedBy = isRecord(parsed.body) ? cleanString(parsed.body.updatedBy, "updatedBy", { max: 200 }) : undefined;
|
|
38
|
+
const entry = setWorkspaceConfig(value, updatedBy);
|
|
39
|
+
emitConfigChanged(entry.namespace, entry.key, entry.version);
|
|
40
|
+
return json(entry, entry.version === 1 ? 201 : 200);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
43
|
+
throw e;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Auto-split from routes.ts (#299). Domain: tasks.
|
|
2
|
+
import { MAX_BODY_BYTES } from "../config";
|
|
3
|
+
import { VALID_TASK_STATUSES, agentSessionStatus, auditEvent, dispatchTaskCallbacks, emitCommand, error, json, normalizeAgentSessionGuard, parseBody, parseId, parseQueryInt, type Handler } from "./_shared";
|
|
4
|
+
import { ValidationError, claimTask, getMessage, getTask, listTaskEvents, listTasks, recordTaskEvent, renewTaskClaim, updateTaskStatus } from "../db";
|
|
5
|
+
import { captureTaskResultMemory, injectMemoryForTaskClaim } from "../memory-service";
|
|
6
|
+
import { cleanEpoch, cleanMeta, cleanString, optionalEnum } from "../validation";
|
|
7
|
+
import { emitMessageClaimed, emitTaskChanged } from "../sse";
|
|
8
|
+
import { errMessage, isRecord } from "agent-relay-sdk";
|
|
9
|
+
import { type AgentSessionGuard, type TaskStatus, type TaskStatusInput } from "../types";
|
|
10
|
+
|
|
11
|
+
function normalizeTaskStatusInput(body: unknown): TaskStatusInput {
|
|
12
|
+
if (!isRecord(body)) throw new ValidationError("JSON object body required");
|
|
13
|
+
const status = optionalEnum(body.status, "status", VALID_TASK_STATUSES);
|
|
14
|
+
if (!status) throw new ValidationError("status required");
|
|
15
|
+
return {
|
|
16
|
+
status: status as TaskStatus,
|
|
17
|
+
agentId: cleanString(body.agentId, "agentId", { max: 200 }),
|
|
18
|
+
instanceId: cleanString(body.instanceId, "instanceId", { max: 200 }),
|
|
19
|
+
epoch: cleanEpoch(body.epoch, "epoch"),
|
|
20
|
+
result: cleanString(body.result, "result", { max: MAX_BODY_BYTES }),
|
|
21
|
+
body: cleanString(body.body, "body", { max: MAX_BODY_BYTES }),
|
|
22
|
+
metadata: cleanMeta(body.metadata),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const getTasks: Handler = (req) => {
|
|
27
|
+
const url = new URL(req.url);
|
|
28
|
+
const limitRaw = parseQueryInt(url.searchParams.get("limit"), { min: 1, max: 500 });
|
|
29
|
+
if (Number.isNaN(limitRaw)) return error("limit must be an integer between 1 and 500");
|
|
30
|
+
return json(listTasks({
|
|
31
|
+
status: url.searchParams.get("status") ?? undefined,
|
|
32
|
+
source: url.searchParams.get("source") ?? undefined,
|
|
33
|
+
target: url.searchParams.get("target") ?? undefined,
|
|
34
|
+
limit: limitRaw ?? 100,
|
|
35
|
+
}));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const getTaskById: Handler = (_req, params) => {
|
|
39
|
+
const id = parseId(params.id);
|
|
40
|
+
if (id === null) return error("invalid task id");
|
|
41
|
+
const task = getTask(id);
|
|
42
|
+
return task ? json(task) : error("task not found", 404);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const getTaskEvents: Handler = (_req, params) => {
|
|
46
|
+
const id = parseId(params.id);
|
|
47
|
+
if (id === null) return error("invalid task id");
|
|
48
|
+
if (!getTask(id)) return error("task not found", 404);
|
|
49
|
+
return json(listTaskEvents(id));
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const postClaimTask: Handler = async (req, params) => {
|
|
53
|
+
const id = parseId(params.id);
|
|
54
|
+
if (id === null) return error("invalid task id");
|
|
55
|
+
const parsed = await parseBody<{ agentId: string }>(req);
|
|
56
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
57
|
+
const agentId = parsed.body?.agentId;
|
|
58
|
+
if (!agentId) return error("agentId required");
|
|
59
|
+
let guard: AgentSessionGuard | undefined;
|
|
60
|
+
try {
|
|
61
|
+
guard = normalizeAgentSessionGuard(req, parsed.body);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
64
|
+
throw e;
|
|
65
|
+
}
|
|
66
|
+
const result = claimTask(id, agentId, guard);
|
|
67
|
+
if (!result.ok) {
|
|
68
|
+
const status = result.error === "task not found" ? 404 : result.error?.includes("race") || result.error === "stale agent instance" ? 409 : 400;
|
|
69
|
+
return error(result.error!, status);
|
|
70
|
+
}
|
|
71
|
+
emitTaskChanged(result.task!, "task.claimed");
|
|
72
|
+
auditEvent({
|
|
73
|
+
clientId: "server-task-" + id + "-claimed-" + agentId,
|
|
74
|
+
kind: "task",
|
|
75
|
+
title: "Task claimed",
|
|
76
|
+
body: result.task!.title,
|
|
77
|
+
meta: "by " + agentId,
|
|
78
|
+
icon: "ti-user-check",
|
|
79
|
+
view: "work",
|
|
80
|
+
taskId: id,
|
|
81
|
+
agentId,
|
|
82
|
+
});
|
|
83
|
+
if (result.task!.messageId) {
|
|
84
|
+
emitMessageClaimed(result.task!.messageId, agentId, getMessage(result.task!.messageId)?.claimExpiresAt);
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const memoryInjection = await injectMemoryForTaskClaim(result.task!, agentId);
|
|
88
|
+
if (memoryInjection) emitCommand(memoryInjection.command);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.warn(`[memory] automatic task context assembly failed: ${errMessage(e)}`);
|
|
91
|
+
}
|
|
92
|
+
void dispatchTaskCallbacks(id, "task.claimed");
|
|
93
|
+
return json(result.task);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const postRenewTaskClaim: Handler = async (req, params) => {
|
|
97
|
+
const id = parseId(params.id);
|
|
98
|
+
if (id === null) return error("invalid task id");
|
|
99
|
+
const parsed = await parseBody<{ agentId: string }>(req);
|
|
100
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
101
|
+
const agentId = parsed.body?.agentId;
|
|
102
|
+
if (!agentId) return error("agentId required");
|
|
103
|
+
let guard: AgentSessionGuard | undefined;
|
|
104
|
+
try {
|
|
105
|
+
guard = normalizeAgentSessionGuard(req, parsed.body);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
108
|
+
throw e;
|
|
109
|
+
}
|
|
110
|
+
const result = renewTaskClaim(id, agentId, guard);
|
|
111
|
+
if (!result.ok) {
|
|
112
|
+
const status = result.error === "task not found" ? 404 : 409;
|
|
113
|
+
return error(result.error!, status);
|
|
114
|
+
}
|
|
115
|
+
emitTaskChanged(result.task!, "task.updated");
|
|
116
|
+
if (result.task!.messageId) {
|
|
117
|
+
emitMessageClaimed(result.task!.messageId, agentId, getMessage(result.task!.messageId)?.claimExpiresAt);
|
|
118
|
+
}
|
|
119
|
+
return json(result.task);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const patchTaskStatus: Handler = async (req, params) => {
|
|
123
|
+
const id = parseId(params.id);
|
|
124
|
+
if (id === null) return error("invalid task id");
|
|
125
|
+
const parsed = await parseBody<unknown>(req);
|
|
126
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
127
|
+
try {
|
|
128
|
+
const result = updateTaskStatus(id, normalizeTaskStatusInput(parsed.body));
|
|
129
|
+
if (!result.ok) return error(result.error!, result.error === "task not found" ? 404 : agentSessionStatus(result.error));
|
|
130
|
+
emitTaskChanged(result.task!, "task.status");
|
|
131
|
+
const capturedMemory = result.task!.status === "done"
|
|
132
|
+
? await captureTaskResultMemory(result.task!, result.task!.result)
|
|
133
|
+
: null;
|
|
134
|
+
const shouldRecordMemoryEvent = capturedMemory && !listTaskEvents(id).some((event) =>
|
|
135
|
+
event.type === "memory.captured" && event.metadata?.memoryId === capturedMemory.id);
|
|
136
|
+
const memoryEvent = shouldRecordMemoryEvent
|
|
137
|
+
? recordTaskEvent(id, {
|
|
138
|
+
source: "memory-broker",
|
|
139
|
+
type: "memory.captured",
|
|
140
|
+
severity: result.task!.severity,
|
|
141
|
+
title: "Memory captured",
|
|
142
|
+
body: capturedMemory.title,
|
|
143
|
+
metadata: {
|
|
144
|
+
memoryId: capturedMemory.id,
|
|
145
|
+
memory: {
|
|
146
|
+
id: capturedMemory.id,
|
|
147
|
+
type: capturedMemory.type,
|
|
148
|
+
scope: capturedMemory.scope,
|
|
149
|
+
title: capturedMemory.title,
|
|
150
|
+
tags: capturedMemory.tags,
|
|
151
|
+
sensitivity: capturedMemory.sensitivity,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
: null;
|
|
156
|
+
auditEvent({
|
|
157
|
+
clientId: "server-task-" + id + "-status-" + result.task!.status + "-" + Date.now(),
|
|
158
|
+
kind: "task",
|
|
159
|
+
title: "Task " + result.task!.status,
|
|
160
|
+
body: result.task!.title,
|
|
161
|
+
meta: capturedMemory ? `captured ${capturedMemory.id}` : result.task!.claimedBy ? "by " + result.task!.claimedBy : result.task!.target,
|
|
162
|
+
icon: "ti-arrows-exchange",
|
|
163
|
+
view: "work",
|
|
164
|
+
taskId: id,
|
|
165
|
+
agentId: result.task!.claimedBy,
|
|
166
|
+
metadata: capturedMemory ? { memoryId: capturedMemory.id } : undefined,
|
|
167
|
+
});
|
|
168
|
+
void dispatchTaskCallbacks(id, "task.status");
|
|
169
|
+
return json({ task: result.task, event: result.event, ...(capturedMemory ? { memory: capturedMemory, memoryEvent } : {}) });
|
|
170
|
+
} catch (e) {
|
|
171
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
172
|
+
throw e;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// Auto-split from routes.ts (#299). Domain: tokens.
|
|
2
|
+
import { SPAWN_PROVIDERS, isRecord } from "agent-relay-sdk";
|
|
3
|
+
import { ValidationError, getOrchestrator, upsertIntegrationRegistry } from "../db";
|
|
4
|
+
import { auditEvent, authAuditMetadata, authorizeRoute, error, isRootCredentialRequest, json, parseBody, type Handler } from "./_shared";
|
|
5
|
+
import { cleanString, cleanStringArray, cleanTokenConstraints, optionalEnum } from "../validation";
|
|
6
|
+
import { createToken, deleteTokenProfile, getToken, getTokenProfile, listTokenProfiles, listTokens, renewToken, revokeToken, updateTokenProfile, upsertTokenProfile } from "../token-db";
|
|
7
|
+
import { getComponentAuth } from "../security";
|
|
8
|
+
import { issueIntegrationRuntimeToken, issueInteractiveRunnerRuntimeToken, issueMcpRuntimeToken, reissueRunnerRuntimeToken } from "../runtime-tokens";
|
|
9
|
+
import { type SpawnProvider } from "../types";
|
|
10
|
+
|
|
11
|
+
export const getTokens: Handler = () => json(listTokens());
|
|
12
|
+
|
|
13
|
+
function cleanTtlSeconds(value: unknown): number | undefined {
|
|
14
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
15
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
16
|
+
throw new ValidationError("ttlSeconds must be a positive integer");
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function cleanTokenProfileInput(body: unknown, partial = false) {
|
|
22
|
+
if (!isRecord(body)) throw new ValidationError("token profile body required");
|
|
23
|
+
const id = partial ? cleanString(body.id, "id", { max: 120 }) : cleanString(body.id, "id", { max: 120 });
|
|
24
|
+
const name = cleanString(body.name, "name", { required: !partial, max: 120 });
|
|
25
|
+
const description = cleanString(body.description, "description", { max: 500 });
|
|
26
|
+
const role = cleanString(body.role, "role", { required: !partial, max: 80 });
|
|
27
|
+
const scope = cleanStringArray(body.scope ?? body.scopes, "scope", { itemMax: 80, maxItems: 50 });
|
|
28
|
+
if (!partial && !scope?.length) throw new ValidationError("scope must contain at least one scope");
|
|
29
|
+
const constraints = body.constraints === null ? undefined : cleanTokenConstraints(body.constraints);
|
|
30
|
+
const ttlSeconds = cleanTtlSeconds(body.ttlSeconds);
|
|
31
|
+
const createdBy = cleanString(body.createdBy, "createdBy", { max: 120 });
|
|
32
|
+
return { id, name, description, role, scope, constraints, ttlSeconds, createdBy };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const getTokenById: Handler = (_req, params) => {
|
|
36
|
+
const token = getToken(params.jti!);
|
|
37
|
+
return token ? json(token) : error("token not found", 404);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const postToken: Handler = async (req) => {
|
|
41
|
+
const parsed = await parseBody<unknown>(req);
|
|
42
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
43
|
+
try {
|
|
44
|
+
if (!isRecord(parsed.body)) return error("token body required");
|
|
45
|
+
const profileId = cleanString(parsed.body.profileId ?? parsed.body.profile, "profileId", { max: 120 });
|
|
46
|
+
const profile = profileId ? getTokenProfile(profileId) : null;
|
|
47
|
+
if (profileId && !profile) return error("token profile not found", 404);
|
|
48
|
+
const role = cleanString(parsed.body.role, "role", { max: 80 }) ?? profile?.role;
|
|
49
|
+
if (!role) return error("role required");
|
|
50
|
+
const sub = cleanString(parsed.body.sub, "sub", { max: 160 }) ?? role;
|
|
51
|
+
const scope = cleanStringArray(parsed.body.scope ?? parsed.body.scopes, "scope", { itemMax: 80, maxItems: 50 });
|
|
52
|
+
const constraints = cleanTokenConstraints(parsed.body.constraints);
|
|
53
|
+
const createdBy = cleanString(parsed.body.createdBy, "createdBy", { max: 120 });
|
|
54
|
+
const ttlSeconds = cleanTtlSeconds(parsed.body.ttlSeconds);
|
|
55
|
+
return json(createToken({ sub, role, scope, constraints, ttlSeconds, createdBy, profileId }), 201);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
58
|
+
throw e;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const postRuntimeTokenRenew: Handler = async (req) => {
|
|
63
|
+
const auth = getComponentAuth(req);
|
|
64
|
+
if (!auth?.jti) return error("scoped runtime token required", 403);
|
|
65
|
+
if (auth.role !== "provider") return error("provider runtime token required", 403);
|
|
66
|
+
const renewed = renewToken({
|
|
67
|
+
jti: auth.jti,
|
|
68
|
+
allowedProfileIds: ["provider-agent", "provider-interactive"],
|
|
69
|
+
createdBy: `renew:${auth.jti}`,
|
|
70
|
+
});
|
|
71
|
+
if (!renewed) return error("runtime token cannot be renewed", 403);
|
|
72
|
+
auditEvent({
|
|
73
|
+
clientId: "server-runtime-token-renew-" + auth.jti + "-" + Date.now(),
|
|
74
|
+
kind: "state",
|
|
75
|
+
title: "Runtime token renewed",
|
|
76
|
+
body: renewed.record.profileId ?? auth.role,
|
|
77
|
+
meta: renewed.record.jti,
|
|
78
|
+
icon: "ti-key",
|
|
79
|
+
view: "security",
|
|
80
|
+
metadata: {
|
|
81
|
+
previousJti: renewed.previous.jti,
|
|
82
|
+
previousExpiresAt: renewed.previous.expiresAt,
|
|
83
|
+
profileId: renewed.record.profileId,
|
|
84
|
+
jti: renewed.record.jti,
|
|
85
|
+
sub: renewed.record.sub,
|
|
86
|
+
...authAuditMetadata(req),
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
return json({
|
|
90
|
+
token: renewed.token,
|
|
91
|
+
record: renewed.record,
|
|
92
|
+
previous: {
|
|
93
|
+
jti: renewed.previous.jti,
|
|
94
|
+
expiresAt: renewed.previous.expiresAt,
|
|
95
|
+
},
|
|
96
|
+
}, 201);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const postOrchestratorRunnerToken: Handler = async (req, params) => {
|
|
100
|
+
const orch = getOrchestrator(params.id!);
|
|
101
|
+
if (!orch) return error("orchestrator not found", 404);
|
|
102
|
+
const denied = authorizeRoute(req, { scope: "command:write", resource: { orchestratorId: orch.id } });
|
|
103
|
+
if (denied) return denied;
|
|
104
|
+
const parsed = await parseBody<unknown>(req);
|
|
105
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
106
|
+
if (!isRecord(parsed.body)) return error("body required");
|
|
107
|
+
const token = cleanString(parsed.body.token, "token", { required: true, max: 8192 })!;
|
|
108
|
+
const result = reissueRunnerRuntimeToken({
|
|
109
|
+
expiredToken: token,
|
|
110
|
+
orchestratorId: orch.id,
|
|
111
|
+
createdBy: `orchestrator-remint:${orch.id}`,
|
|
112
|
+
});
|
|
113
|
+
if ("error" in result) return error(result.error, 403);
|
|
114
|
+
auditEvent({
|
|
115
|
+
clientId: "server-runner-token-remint-" + result.record.jti + "-" + Date.now(),
|
|
116
|
+
kind: "state",
|
|
117
|
+
title: "Runner token re-minted",
|
|
118
|
+
body: result.record.sub,
|
|
119
|
+
meta: result.record.jti,
|
|
120
|
+
icon: "ti-key",
|
|
121
|
+
view: "security",
|
|
122
|
+
metadata: { orchestratorId: orch.id, jti: result.record.jti, sub: result.record.sub, ...authAuditMetadata(req) },
|
|
123
|
+
});
|
|
124
|
+
return json(result, 201);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const postInteractiveRunnerRuntimeToken: Handler = async (req) => {
|
|
128
|
+
if (!isRootCredentialRequest(req)) return error("root credential required for runtime token exchange", 403);
|
|
129
|
+
const parsed = await parseBody<unknown>(req);
|
|
130
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
131
|
+
try {
|
|
132
|
+
if (!isRecord(parsed.body)) return error("runtime token body required");
|
|
133
|
+
const provider = optionalEnum(parsed.body.provider, "provider", SPAWN_PROVIDERS)! as SpawnProvider;
|
|
134
|
+
const cwd = cleanString(parsed.body.cwd, "cwd", { required: true, max: 500 })!;
|
|
135
|
+
const runnerId = cleanString(parsed.body.runnerId, "runnerId", { required: true, max: 240 })!;
|
|
136
|
+
const agentId = cleanString(parsed.body.agentId, "agentId", { max: 240 });
|
|
137
|
+
const label = cleanString(parsed.body.label, "label", { max: 120 });
|
|
138
|
+
const host = cleanString(parsed.body.host, "host", { max: 120 });
|
|
139
|
+
const runtimeToken = issueInteractiveRunnerRuntimeToken({
|
|
140
|
+
provider,
|
|
141
|
+
cwd,
|
|
142
|
+
runnerId,
|
|
143
|
+
agentId,
|
|
144
|
+
label,
|
|
145
|
+
host,
|
|
146
|
+
createdBy: `interactive:${runnerId}`,
|
|
147
|
+
});
|
|
148
|
+
auditEvent({
|
|
149
|
+
clientId: "server-runtime-token-interactive-" + runnerId + "-" + Date.now(),
|
|
150
|
+
kind: "state",
|
|
151
|
+
title: "Interactive runner token issued",
|
|
152
|
+
body: provider,
|
|
153
|
+
meta: runnerId,
|
|
154
|
+
icon: "ti-key",
|
|
155
|
+
view: "security",
|
|
156
|
+
metadata: {
|
|
157
|
+
provider,
|
|
158
|
+
runnerId,
|
|
159
|
+
agentId: agentId ?? runnerId,
|
|
160
|
+
profileId: runtimeToken.record.profileId,
|
|
161
|
+
jti: runtimeToken.record.jti,
|
|
162
|
+
...authAuditMetadata(req),
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
return json(runtimeToken, 201);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
168
|
+
throw e;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const postMcpRuntimeToken: Handler = async (req) => {
|
|
173
|
+
if (!isRootCredentialRequest(req)) return error("root credential required for runtime token exchange", 403);
|
|
174
|
+
const parsed = await parseBody<unknown>(req);
|
|
175
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
176
|
+
try {
|
|
177
|
+
if (!isRecord(parsed.body)) return error("runtime token body required");
|
|
178
|
+
const sessionId = cleanString(parsed.body.sessionId, "sessionId", { required: true, max: 240 })!;
|
|
179
|
+
const agentId = cleanString(parsed.body.agentId, "agentId", { max: 240 });
|
|
180
|
+
const targets = cleanStringArray(parsed.body.targets, "targets", { itemMax: 80, maxItems: 50 });
|
|
181
|
+
const channels = cleanStringArray(parsed.body.channels, "channels", { itemMax: 80, maxItems: 50 });
|
|
182
|
+
const memoryScopes = cleanStringArray(parsed.body.memoryScopes, "memoryScopes", { itemMax: 80, maxItems: 50 });
|
|
183
|
+
const runtimeToken = issueMcpRuntimeToken({
|
|
184
|
+
sessionId,
|
|
185
|
+
agentId,
|
|
186
|
+
targets,
|
|
187
|
+
channels,
|
|
188
|
+
memoryScopes,
|
|
189
|
+
createdBy: `mcp:${sessionId}`,
|
|
190
|
+
});
|
|
191
|
+
auditEvent({
|
|
192
|
+
clientId: "server-runtime-token-mcp-" + sessionId + "-" + Date.now(),
|
|
193
|
+
kind: "state",
|
|
194
|
+
title: "MCP session token issued",
|
|
195
|
+
body: agentId ?? sessionId,
|
|
196
|
+
meta: sessionId,
|
|
197
|
+
icon: "ti-key",
|
|
198
|
+
view: "security",
|
|
199
|
+
metadata: {
|
|
200
|
+
sessionId,
|
|
201
|
+
agentId,
|
|
202
|
+
profileId: runtimeToken.record.profileId,
|
|
203
|
+
jti: runtimeToken.record.jti,
|
|
204
|
+
...authAuditMetadata(req),
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
return json(runtimeToken, 201);
|
|
208
|
+
} catch (e) {
|
|
209
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
210
|
+
throw e;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export const postIntegrationRuntimeToken: Handler = async (req) => {
|
|
215
|
+
if (!isRootCredentialRequest(req)) return error("root credential required for runtime token exchange", 403);
|
|
216
|
+
const parsed = await parseBody<unknown>(req);
|
|
217
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
218
|
+
try {
|
|
219
|
+
if (!isRecord(parsed.body)) return error("runtime token body required");
|
|
220
|
+
const name = cleanString(parsed.body.name, "name", { required: true, max: 120 })!;
|
|
221
|
+
const targets = cleanStringArray(parsed.body.targets, "targets", { itemMax: 80, maxItems: 50 });
|
|
222
|
+
const channels = cleanStringArray(parsed.body.channels, "channels", { itemMax: 80, maxItems: 50 });
|
|
223
|
+
const runtimeToken = issueIntegrationRuntimeToken({
|
|
224
|
+
name,
|
|
225
|
+
targets,
|
|
226
|
+
channels,
|
|
227
|
+
createdBy: `integration:${name}`,
|
|
228
|
+
});
|
|
229
|
+
upsertIntegrationRegistry({
|
|
230
|
+
name,
|
|
231
|
+
scopes: runtimeToken.record.scope,
|
|
232
|
+
targets,
|
|
233
|
+
channels,
|
|
234
|
+
source: "api",
|
|
235
|
+
});
|
|
236
|
+
auditEvent({
|
|
237
|
+
clientId: "server-runtime-token-integration-" + name + "-" + Date.now(),
|
|
238
|
+
kind: "state",
|
|
239
|
+
title: "Integration token issued",
|
|
240
|
+
body: name,
|
|
241
|
+
meta: name,
|
|
242
|
+
icon: "ti-key",
|
|
243
|
+
view: "security",
|
|
244
|
+
metadata: {
|
|
245
|
+
integration: name,
|
|
246
|
+
profileId: runtimeToken.record.profileId,
|
|
247
|
+
jti: runtimeToken.record.jti,
|
|
248
|
+
...authAuditMetadata(req),
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
return json(runtimeToken, 201);
|
|
252
|
+
} catch (e) {
|
|
253
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
254
|
+
throw e;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export const getTokenProfiles: Handler = () => json(listTokenProfiles());
|
|
259
|
+
|
|
260
|
+
export const postTokenProfile: Handler = async (req) => {
|
|
261
|
+
const parsed = await parseBody<unknown>(req);
|
|
262
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
263
|
+
try {
|
|
264
|
+
const input = cleanTokenProfileInput(parsed.body);
|
|
265
|
+
return json(upsertTokenProfile({
|
|
266
|
+
id: input.id,
|
|
267
|
+
name: input.name!,
|
|
268
|
+
description: input.description,
|
|
269
|
+
role: input.role!,
|
|
270
|
+
scope: input.scope!,
|
|
271
|
+
constraints: input.constraints,
|
|
272
|
+
ttlSeconds: input.ttlSeconds,
|
|
273
|
+
createdBy: input.createdBy,
|
|
274
|
+
}), 201);
|
|
275
|
+
} catch (e) {
|
|
276
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
277
|
+
if (e instanceof Error) return error(e.message, 400);
|
|
278
|
+
throw e;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
export const patchTokenProfile: Handler = async (req, params) => {
|
|
283
|
+
const parsed = await parseBody<unknown>(req);
|
|
284
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
285
|
+
try {
|
|
286
|
+
const patch = cleanTokenProfileInput(parsed.body, true);
|
|
287
|
+
const updated = updateTokenProfile(params.id!, {
|
|
288
|
+
name: patch.name,
|
|
289
|
+
description: patch.description,
|
|
290
|
+
role: patch.role,
|
|
291
|
+
scope: patch.scope,
|
|
292
|
+
constraints: patch.constraints,
|
|
293
|
+
ttlSeconds: patch.ttlSeconds,
|
|
294
|
+
createdBy: patch.createdBy,
|
|
295
|
+
});
|
|
296
|
+
return updated ? json(updated) : error("token profile not found", 404);
|
|
297
|
+
} catch (e) {
|
|
298
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
299
|
+
if (e instanceof Error) return error(e.message, e.message.includes("built-in") ? 409 : 400);
|
|
300
|
+
throw e;
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
export const deleteTokenProfileRoute: Handler = (_req, params) => {
|
|
305
|
+
return deleteTokenProfile(params.id!) ? json({ ok: true }) : error("token profile not found or built-in", 404);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
export const postTokenRevoke: Handler = (_req, params) => {
|
|
309
|
+
if (!revokeToken(params.jti!)) return error("token not found or already revoked", 404);
|
|
310
|
+
return json({ ok: true });
|
|
311
|
+
};
|