agent-relay-server 0.8.1 → 0.10.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/README.md +12 -14
- package/package.json +18 -1
- package/public/index.html +979 -2575
- package/public/manifest.webmanifest +6 -6
- package/public/sw.js +16 -10
- package/recipes/code-review.yaml +26 -0
- package/recipes/debug.yaml +20 -0
- package/recipes/feature.yaml +26 -0
- package/recipes/refactor.yaml +20 -0
- package/recipes/test.yaml +20 -0
- package/runner/src/adapter.ts +69 -0
- package/runner/src/config.ts +144 -0
- package/scripts/orchestrator-spawn-smoke.ts +2 -9
- package/src/agent-spawn.ts +2 -94
- package/src/automations.ts +774 -0
- package/src/bus-outbox.ts +75 -0
- package/src/bus.ts +439 -0
- package/src/cli.ts +251 -5
- package/src/commands-db.ts +160 -0
- package/src/config.ts +2 -1
- package/src/connectors.ts +29 -9
- package/src/daemon.ts +1 -0
- package/src/db.ts +363 -36
- package/src/events.ts +33 -0
- package/src/index.ts +100 -5
- package/src/recipe-db.ts +163 -0
- package/src/recipe-loader.ts +100 -0
- package/src/recipe-runner.ts +206 -0
- package/src/recipe-validator.ts +85 -0
- package/src/routes.ts +661 -158
- package/src/security.ts +128 -2
- package/src/sse.ts +45 -28
- package/src/token-db.ts +96 -0
- package/src/types.ts +1 -488
- package/src/upgrade.ts +14 -28
- package/public/dashboard/actions.js +0 -819
- package/public/dashboard/api.js +0 -336
- package/public/dashboard/app.js +0 -34
- package/public/dashboard/charts.js +0 -128
- package/public/dashboard/computed.js +0 -693
- package/public/dashboard/constants.js +0 -28
- package/public/dashboard/display.js +0 -345
- package/public/dashboard/state.js +0 -129
- package/public/dashboard/utils.js +0 -207
package/src/routes.ts
CHANGED
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
reapStaleAgents,
|
|
54
54
|
reapStaleOrchestrators,
|
|
55
55
|
releaseExpiredClaims,
|
|
56
|
+
releaseOrphanedTasks,
|
|
56
57
|
validateAgentSession,
|
|
57
58
|
upsertOrchestrator,
|
|
58
59
|
getOrchestrator,
|
|
@@ -60,8 +61,13 @@ import {
|
|
|
60
61
|
orchestratorHeartbeat,
|
|
61
62
|
updateManagedAgents,
|
|
62
63
|
deleteOrchestrator,
|
|
64
|
+
evaluatePoolBindings,
|
|
63
65
|
ValidationError,
|
|
64
66
|
} from "./db";
|
|
67
|
+
import { createCommand, deleteCommand, expireCommands, getCommand, listCommands, updateCommand } from "./commands-db";
|
|
68
|
+
import { getRecipe, listRecipes } from "./recipe-loader";
|
|
69
|
+
import { applyCommandToRecipe, getRecipeInstance, listRecipeInstances, startRecipe, stopRecipe } from "./recipe-runner";
|
|
70
|
+
import { createToken, getToken, listTokens, revokeToken } from "./token-db";
|
|
65
71
|
import {
|
|
66
72
|
getConnector,
|
|
67
73
|
listConnectors,
|
|
@@ -70,9 +76,21 @@ import {
|
|
|
70
76
|
runConnectorAction,
|
|
71
77
|
writeConnectorConfig,
|
|
72
78
|
} from "./connectors";
|
|
73
|
-
import
|
|
79
|
+
import {
|
|
80
|
+
createAutomation,
|
|
81
|
+
deleteAutomation,
|
|
82
|
+
getAutomation,
|
|
83
|
+
listAutomationRuns,
|
|
84
|
+
listAutomations,
|
|
85
|
+
runAutomationNow,
|
|
86
|
+
updateAutomation,
|
|
87
|
+
type AutomationDispatchResult,
|
|
88
|
+
} from "./automations";
|
|
89
|
+
import type { ActivityEventInput, ActivityKind, AgentCard, AgentKind, AgentSessionGuard, ChannelBinding, ChannelBindingMode, ChannelDirection, ChannelRouteTarget, ChannelSummary, Command, CommandStatus, CreateCommandInput, CreatePairInput, IntegrationEventInput, IntegrationSummary, ManagedAgent, OrchestratorRuntimeInput, PairActionInput, PairMessageInput, PairStatus, RegisterAgentInput, RegisterOrchestratorInput, SendMessageInput, SpawnApprovalMode, SpawnProvider, TaskStatus, TaskStatusInput } from "./types";
|
|
74
90
|
import { getIntegrationTokens, INTEGRATION_RATE_LIMIT_PER_MINUTE, MAX_BODY_BYTES } from "./config";
|
|
75
|
-
import { listHostDirectories
|
|
91
|
+
import { listHostDirectories } from "./agent-spawn";
|
|
92
|
+
import { defaultProviderConfig, loadProviderConfig, providerConfigPublic, writeProviderConfig } from "../runner/src/config";
|
|
93
|
+
import type { ProviderConfig } from "../runner/src/adapter";
|
|
76
94
|
import {
|
|
77
95
|
getIntegrationAuth,
|
|
78
96
|
hasIntegrationScope,
|
|
@@ -90,7 +108,9 @@ import {
|
|
|
90
108
|
emitChannelActivity,
|
|
91
109
|
emitOrchestratorStatus,
|
|
92
110
|
emitOrchestratorRemoved,
|
|
111
|
+
emitPoolBindingChanged,
|
|
93
112
|
} from "./sse";
|
|
113
|
+
import { emitRelayEvent } from "./events";
|
|
94
114
|
|
|
95
115
|
type Handler = (
|
|
96
116
|
req: Request,
|
|
@@ -173,16 +193,17 @@ function parseQueryInt(
|
|
|
173
193
|
return n;
|
|
174
194
|
}
|
|
175
195
|
|
|
176
|
-
const VALID_AGENT_STATUSES = ["online", "idle", "busy", "offline"] as const;
|
|
196
|
+
const VALID_AGENT_STATUSES = ["online", "idle", "busy", "stale", "offline"] as const;
|
|
177
197
|
const VALID_AGENT_KINDS = ["provider", "channel", "orchestrator", "system", "user"] as const;
|
|
178
|
-
const VALID_CHANNEL_BINDING_TARGET_TYPES = ["agent", "label", "tag", "capability", "broadcast", "orchestrator"] as const;
|
|
179
|
-
const VALID_CHANNEL_BINDING_MODES = ["exclusive", "
|
|
198
|
+
const VALID_CHANNEL_BINDING_TARGET_TYPES = ["agent", "label", "tag", "capability", "broadcast", "orchestrator", "pool"] as const;
|
|
199
|
+
const VALID_CHANNEL_BINDING_MODES = ["exclusive", "broadcast"] as const;
|
|
180
200
|
const VALID_AGENT_ACTIONS = ["restart", "shutdown"] as const;
|
|
181
201
|
const VALID_AGENT_SPAWN_PROVIDERS = ["codex"] as const;
|
|
182
202
|
const VALID_CODEX_SPAWN_APPROVALS = ["open", "guarded", "read-only"] as const;
|
|
183
203
|
const VALID_CONNECTOR_ACTIONS = ["install", "uninstall", "enable", "disable", "start", "stop", "restart", "status", "doctor"] as const;
|
|
184
204
|
const VALID_TASK_SEVERITIES = ["info", "warning", "critical"] as const;
|
|
185
|
-
const VALID_TASK_STATUSES = ["open", "claimed", "in_progress", "blocked", "done", "failed", "canceled"] as const;
|
|
205
|
+
const VALID_TASK_STATUSES = ["open", "claimed", "in_progress", "blocked", "orphaned", "done", "failed", "canceled"] as const;
|
|
206
|
+
const VALID_COMMAND_STATUSES = ["pending", "accepted", "running", "succeeded", "failed", "timed_out", "rejected", "canceled"] as const;
|
|
186
207
|
const VALID_PAIR_STATUSES = ["pending", "active", "ended", "rejected", "expired"] as const;
|
|
187
208
|
const VALID_ACTIVITY_KINDS = ["message", "reply", "question", "operator", "pair", "task", "state"] as const;
|
|
188
209
|
const integrationRateBuckets = new Map<string, { windowStart: number; count: number }>();
|
|
@@ -230,6 +251,13 @@ function cleanMeta(value: unknown): Record<string, unknown> | undefined {
|
|
|
230
251
|
return value;
|
|
231
252
|
}
|
|
232
253
|
|
|
254
|
+
function cleanParams(value: unknown, field = "params"): Record<string, unknown> | undefined {
|
|
255
|
+
if (value === undefined || value === null) return undefined;
|
|
256
|
+
if (!isRecord(value)) throw new ValidationError(`${field} must be an object`);
|
|
257
|
+
if (JSON.stringify(value).length > 65_536) throw new ValidationError(`${field} is too large`);
|
|
258
|
+
return value;
|
|
259
|
+
}
|
|
260
|
+
|
|
233
261
|
function cleanEnum<T extends readonly string[]>(
|
|
234
262
|
value: unknown,
|
|
235
263
|
field: string,
|
|
@@ -355,6 +383,11 @@ function normalizeMessageInput(body: unknown): SendMessageInput {
|
|
|
355
383
|
return input;
|
|
356
384
|
}
|
|
357
385
|
|
|
386
|
+
const FANOUT_PREFIXES = ["tag:", "cap:", "label:"];
|
|
387
|
+
function isDirectTarget(to: string): boolean {
|
|
388
|
+
return to !== "broadcast" && !FANOUT_PREFIXES.some((p) => to.startsWith(p));
|
|
389
|
+
}
|
|
390
|
+
|
|
358
391
|
function applyReplyRouting(input: SendMessageInput): void {
|
|
359
392
|
if (input.to || !input.replyTo) return;
|
|
360
393
|
const parent = getMessage(input.replyTo);
|
|
@@ -404,6 +437,7 @@ function normalizeChannelBindingInput(body: unknown): {
|
|
|
404
437
|
}
|
|
405
438
|
|
|
406
439
|
function routeTargetFromAddress(target: string): ChannelRouteTarget {
|
|
440
|
+
if (target.startsWith("pool:")) return { type: "pool", id: target.slice("pool:".length) };
|
|
407
441
|
if (target.startsWith("label:")) return { type: "label", id: target.slice("label:".length) };
|
|
408
442
|
if (target.startsWith("tag:")) return { type: "tag", id: target.slice("tag:".length) };
|
|
409
443
|
if (target.startsWith("cap:")) return { type: "capability", id: target.slice("cap:".length) };
|
|
@@ -412,8 +446,12 @@ function routeTargetFromAddress(target: string): ChannelRouteTarget {
|
|
|
412
446
|
return { type: "agent", id: target };
|
|
413
447
|
}
|
|
414
448
|
|
|
415
|
-
function messageTargetForChannelTarget(target: ChannelRouteTarget): string {
|
|
449
|
+
function messageTargetForChannelTarget(target: ChannelRouteTarget, binding?: ChannelBinding): string {
|
|
416
450
|
if (target.type === "orchestrator") throw new ValidationError("orchestrator channel targets are not supported yet");
|
|
451
|
+
if (target.type === "pool") {
|
|
452
|
+
if (!binding?.poolAgentId) throw new ValidationError("pool slot is unclaimed — no eligible agent available");
|
|
453
|
+
return binding.poolAgentId;
|
|
454
|
+
}
|
|
417
455
|
if (target.type === "label") return `label:${target.id}`;
|
|
418
456
|
if (target.type === "tag") return `tag:${target.id}`;
|
|
419
457
|
if (target.type === "capability") return `cap:${target.id}`;
|
|
@@ -550,6 +588,53 @@ function normalizeIntegrationEvent(body: unknown): IntegrationEventInput {
|
|
|
550
588
|
};
|
|
551
589
|
}
|
|
552
590
|
|
|
591
|
+
function channelIdFromIntegrationTarget(target: string): string | undefined {
|
|
592
|
+
if (!target.startsWith("channel:")) return undefined;
|
|
593
|
+
const channelId = target.slice("channel:".length).trim();
|
|
594
|
+
if (!channelId) throw new ValidationError("channel target id required");
|
|
595
|
+
return channelId;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function metadataWithResolvedChannelTarget(
|
|
599
|
+
metadata: Record<string, unknown> | undefined,
|
|
600
|
+
originalTarget: string,
|
|
601
|
+
channelId: string,
|
|
602
|
+
binding: ChannelBinding,
|
|
603
|
+
): Record<string, unknown> {
|
|
604
|
+
return {
|
|
605
|
+
...(metadata ?? {}),
|
|
606
|
+
relayRequestedTarget: originalTarget,
|
|
607
|
+
relayResolvedChannelId: channelId,
|
|
608
|
+
relayResolvedChannelBindingId: binding.id,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function resolveIntegrationEventTarget(input: IntegrationEventInput): IntegrationEventInput {
|
|
613
|
+
const channelId = channelIdFromIntegrationTarget(input.target);
|
|
614
|
+
if (!channelId) return input;
|
|
615
|
+
|
|
616
|
+
evaluatePoolBindings();
|
|
617
|
+
const channel = getChannel(channelId);
|
|
618
|
+
if (!channel) throw new ValidationError(`channel ${channelId} not found`);
|
|
619
|
+
|
|
620
|
+
const bindings = resolveChannelRoutes(channelId);
|
|
621
|
+
if (bindings.length === 0) throw new ValidationError(`channel ${channelId} has no binding`);
|
|
622
|
+
if (bindings.length > 1) throw new ValidationError(`channel ${channelId} has multiple bindings; integration channel targets must resolve to one backend agent`);
|
|
623
|
+
|
|
624
|
+
const binding = bindings[0]!;
|
|
625
|
+
const resolvedTarget = messageTargetForChannelTarget(binding.target, binding);
|
|
626
|
+
if (!getAgent(resolvedTarget)) {
|
|
627
|
+
throw new ValidationError(`channel ${channelId} binding resolves to ${resolvedTarget}; integration channel targets must resolve to one backend agent`);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return {
|
|
631
|
+
...input,
|
|
632
|
+
target: resolvedTarget,
|
|
633
|
+
channel: input.channel ?? channelId,
|
|
634
|
+
metadata: metadataWithResolvedChannelTarget(input.metadata, input.target, channelId, binding),
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
553
638
|
function normalizeTaskStatusInput(body: unknown): TaskStatusInput {
|
|
554
639
|
if (!isRecord(body)) throw new ValidationError("JSON object body required");
|
|
555
640
|
const status = cleanEnum(body.status, "status", VALID_TASK_STATUSES);
|
|
@@ -754,10 +839,11 @@ function agentToChannel(agent: AgentCard): ChannelSummary {
|
|
|
754
839
|
async function dispatchTaskCallbacks(taskId: number, eventType: string): Promise<void> {
|
|
755
840
|
const task = getTask(taskId);
|
|
756
841
|
if (!task) return;
|
|
842
|
+
const requestedTarget = typeof task.metadata?.relayRequestedTarget === "string" ? task.metadata.relayRequestedTarget : undefined;
|
|
757
843
|
const integrations = getIntegrationTokens()
|
|
758
844
|
.filter((integration) => integration.name === task.source)
|
|
759
845
|
.filter((integration) => integration.callbackUrl)
|
|
760
|
-
.filter((integration) => !integration.targets?.length || integration.targets.includes(task.target))
|
|
846
|
+
.filter((integration) => !integration.targets?.length || integration.targets.includes(task.target) || Boolean(requestedTarget && integration.targets.includes(requestedTarget)))
|
|
761
847
|
.filter((integration) => !integration.channels?.length || !task.channel || integration.channels.includes(task.channel));
|
|
762
848
|
|
|
763
849
|
for (const integration of integrations) {
|
|
@@ -825,15 +911,6 @@ function auditAgentStateTransition(agentId: string, before: AgentCard | null | u
|
|
|
825
911
|
});
|
|
826
912
|
}
|
|
827
913
|
|
|
828
|
-
function orchestratorControlMeta(control: Record<string, unknown>): Record<string, unknown> {
|
|
829
|
-
return {
|
|
830
|
-
delivery: "interrupt",
|
|
831
|
-
priority: "urgent",
|
|
832
|
-
// Legacy orchestrators read control details from message meta.
|
|
833
|
-
orchestratorControl: control,
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
|
|
837
914
|
// --- Agent routes ---
|
|
838
915
|
|
|
839
916
|
const postAgent: Handler = async (req) => {
|
|
@@ -889,14 +966,14 @@ const patchAgentStatus: Handler = async (req, params) => {
|
|
|
889
966
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
890
967
|
const body = parsed.body;
|
|
891
968
|
if (!body?.status) return error("status required");
|
|
892
|
-
const valid =
|
|
893
|
-
if (!valid.includes(body.status)) return error(`status must be one of: ${valid.join(", ")}`);
|
|
969
|
+
const valid = VALID_AGENT_STATUSES;
|
|
970
|
+
if (!valid.includes(body.status as any)) return error(`status must be one of: ${valid.join(", ")}`);
|
|
894
971
|
try {
|
|
895
972
|
const guard = normalizeAgentSessionGuard(req, body);
|
|
896
973
|
const session = validateAgentSession(params.id!, guard);
|
|
897
974
|
if (!session.ok) return error(session.error!, agentSessionStatus(session.error));
|
|
898
975
|
const before = getAgent(params.id!);
|
|
899
|
-
if (!setStatus(params.id!, body.status as
|
|
976
|
+
if (!setStatus(params.id!, body.status as (typeof VALID_AGENT_STATUSES)[number], guard)) return error("agent not found", 404);
|
|
900
977
|
auditAgentStateTransition(params.id!, before, getAgent(params.id!));
|
|
901
978
|
} catch (e) {
|
|
902
979
|
if (e instanceof ValidationError) return error(e.message, 400);
|
|
@@ -925,8 +1002,23 @@ const patchAgentLabel: Handler = async (req, params) => {
|
|
|
925
1002
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
926
1003
|
const body = parsed.body;
|
|
927
1004
|
if (body === null || !("label" in body)) return error("label field required (string or null)");
|
|
1005
|
+
const before = getAgent(params.id!);
|
|
928
1006
|
if (!setLabel(params.id!, body.label)) return error("agent not found", 404);
|
|
929
1007
|
emitAgentStatus(params.id!);
|
|
1008
|
+
const after = getAgent(params.id!);
|
|
1009
|
+
if ((before?.label ?? null) !== (after?.label ?? null)) {
|
|
1010
|
+
auditEvent({
|
|
1011
|
+
clientId: "server-agent-" + params.id! + "-label-" + Date.now(),
|
|
1012
|
+
kind: "state",
|
|
1013
|
+
title: after?.label ? "Agent label set" : "Agent label cleared",
|
|
1014
|
+
body: after?.label ?? before?.label ?? "",
|
|
1015
|
+
meta: params.id!,
|
|
1016
|
+
icon: "ti-tag",
|
|
1017
|
+
view: "chat",
|
|
1018
|
+
agentId: params.id!,
|
|
1019
|
+
metadata: { field: "label", previous: before?.label ?? null, next: after?.label ?? null },
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
930
1022
|
return json({ ok: true });
|
|
931
1023
|
};
|
|
932
1024
|
|
|
@@ -936,9 +1028,23 @@ const patchAgentTags: Handler = async (req, params) => {
|
|
|
936
1028
|
try {
|
|
937
1029
|
const tags = isRecord(parsed.body) ? cleanStringArray(parsed.body.tags, "tags") : undefined;
|
|
938
1030
|
if (!tags) return error("tags field required");
|
|
1031
|
+
const before = getAgent(params.id!);
|
|
939
1032
|
const agent = setTags(params.id!, tags);
|
|
940
1033
|
if (!agent) return error("agent not found", 404);
|
|
941
1034
|
emitAgentStatus(params.id!);
|
|
1035
|
+
if (JSON.stringify(before?.tags ?? []) !== JSON.stringify(agent.tags ?? [])) {
|
|
1036
|
+
auditEvent({
|
|
1037
|
+
clientId: "server-agent-" + params.id! + "-tags-" + Date.now(),
|
|
1038
|
+
kind: "state",
|
|
1039
|
+
title: "Agent tags updated",
|
|
1040
|
+
body: (agent.tags || []).join(", "),
|
|
1041
|
+
meta: params.id!,
|
|
1042
|
+
icon: "ti-tags",
|
|
1043
|
+
view: "chat",
|
|
1044
|
+
agentId: params.id!,
|
|
1045
|
+
metadata: { field: "tags", previous: before?.tags ?? [], next: agent.tags ?? [] },
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
942
1048
|
return json(agent);
|
|
943
1049
|
} catch (e) {
|
|
944
1050
|
if (e instanceof ValidationError) return error(e.message, 400);
|
|
@@ -980,7 +1086,8 @@ function agentCanReceiveControlAction(agent: AgentCard): boolean {
|
|
|
980
1086
|
return agent.id !== "user" &&
|
|
981
1087
|
agent.id !== "system" &&
|
|
982
1088
|
agent.meta?.kind !== "channel" &&
|
|
983
|
-
!agent.tags.includes("channel")
|
|
1089
|
+
!agent.tags.includes("channel") &&
|
|
1090
|
+
agent.meta?.runnerManaged === true;
|
|
984
1091
|
}
|
|
985
1092
|
|
|
986
1093
|
const postAgentAction: Handler = async (req, params) => {
|
|
@@ -995,40 +1102,25 @@ const postAgentAction: Handler = async (req, params) => {
|
|
|
995
1102
|
if (!agentCanReceiveControlAction(agent)) return error("agent does not support dashboard control actions", 400);
|
|
996
1103
|
|
|
997
1104
|
const title = action === "restart" ? "Agent restart requested" : "Agent shutdown requested";
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
body: action === "restart"
|
|
1004
|
-
? "Dashboard requested that this agent restart its relay-managed session now."
|
|
1005
|
-
: "Dashboard requested that this agent shut down its relay-managed session now.",
|
|
1006
|
-
payload: {
|
|
1007
|
-
agentControl: {
|
|
1008
|
-
action,
|
|
1009
|
-
requestedBy: "dashboard",
|
|
1010
|
-
requestedAt: Date.now(),
|
|
1011
|
-
},
|
|
1012
|
-
},
|
|
1013
|
-
meta: {
|
|
1014
|
-
delivery: "interrupt",
|
|
1015
|
-
priority: "urgent",
|
|
1016
|
-
},
|
|
1105
|
+
const command = createCommand({
|
|
1106
|
+
type: action === "restart" ? "agent.restart" : "agent.shutdown",
|
|
1107
|
+
source: "system",
|
|
1108
|
+
target: agent.id,
|
|
1109
|
+
params: { action, agentId: agent.id, requestedBy: "dashboard", requestedAt: Date.now() },
|
|
1017
1110
|
});
|
|
1018
|
-
|
|
1111
|
+
emitCommand(command);
|
|
1019
1112
|
auditEvent({
|
|
1020
|
-
clientId: "server-agent-" + agent.id + "-action-" + action + "-" +
|
|
1113
|
+
clientId: "server-agent-" + agent.id + "-action-" + action + "-" + command.id,
|
|
1021
1114
|
kind: "state",
|
|
1022
1115
|
title,
|
|
1023
1116
|
body: action,
|
|
1024
1117
|
meta: agent.id,
|
|
1025
1118
|
icon: action === "restart" ? "ti-refresh" : "ti-power",
|
|
1026
1119
|
view: "agents",
|
|
1027
|
-
messageId: msg.id,
|
|
1028
1120
|
agentId: agent.id,
|
|
1029
|
-
metadata: { action },
|
|
1121
|
+
metadata: { action, commandId: command.id },
|
|
1030
1122
|
});
|
|
1031
|
-
return json({ ok: true, action,
|
|
1123
|
+
return json({ ok: true, action, command }, 202);
|
|
1032
1124
|
} catch (e) {
|
|
1033
1125
|
if (e instanceof ValidationError) return error(e.message, 400);
|
|
1034
1126
|
throw e;
|
|
@@ -1042,70 +1134,33 @@ const postAgentSpawn: Handler = async (req) => {
|
|
|
1042
1134
|
if (!isRecord(parsed.body)) return error("provider required");
|
|
1043
1135
|
const provider = cleanEnum(parsed.body.provider, "provider", [...VALID_AGENT_SPAWN_PROVIDERS, "claude"] as const);
|
|
1044
1136
|
if (!provider) return error("provider required");
|
|
1045
|
-
const approvalMode = cleanEnum(parsed.body.approvalMode, "approvalMode", VALID_CODEX_SPAWN_APPROVALS, "guarded") as
|
|
1137
|
+
const approvalMode = cleanEnum(parsed.body.approvalMode, "approvalMode", VALID_CODEX_SPAWN_APPROVALS, "guarded") as SpawnApprovalMode;
|
|
1046
1138
|
const cwd = cleanString(parsed.body.cwd, "cwd", { max: 500 });
|
|
1047
1139
|
const label = cleanString(parsed.body.label, "label", { max: 120 });
|
|
1048
1140
|
|
|
1049
|
-
// Check for an online orchestrator that supports this provider
|
|
1050
1141
|
const orchestrators = listOrchestrators().filter(
|
|
1051
1142
|
(o) => o.status === "online" && o.providers.includes(provider as SpawnProvider),
|
|
1052
1143
|
);
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
kind: "control",
|
|
1061
|
-
subject: "Spawn agent",
|
|
1062
|
-
body: `Spawn ${provider} agent${label ? ` (${label})` : ""}`,
|
|
1063
|
-
payload: { orchestratorControl: control },
|
|
1064
|
-
meta: orchestratorControlMeta(control),
|
|
1065
|
-
});
|
|
1066
|
-
emitNewMessage(msg);
|
|
1067
|
-
auditEvent({
|
|
1068
|
-
clientId: "server-agent-spawn-" + provider + "-" + Date.now(),
|
|
1069
|
-
kind: "state",
|
|
1070
|
-
title: `${provider} agent spawn requested (via ${orch.id})`,
|
|
1071
|
-
body: cwd || orch.baseDir,
|
|
1072
|
-
meta: orch.id,
|
|
1073
|
-
icon: "ti-plus",
|
|
1074
|
-
view: "agents",
|
|
1075
|
-
metadata: { provider, orchestratorId: orch.id, approvalMode },
|
|
1076
|
-
});
|
|
1077
|
-
return json({ ok: true, orchestratorId: orch.id, provider, message: msg }, 202);
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
// Fallback: direct spawn for codex only (no orchestrator)
|
|
1081
|
-
if (provider !== "codex") return error("no orchestrator available for provider: " + provider);
|
|
1082
|
-
const relayUrl = process.env.AGENT_RELAY_SPAWN_RELAY_URL || process.env.AGENT_RELAY_URL || `http://127.0.0.1:${process.env.PORT || "4850"}`;
|
|
1083
|
-
const token = req.headers.get("X-Agent-Relay-Token") ?? req.headers.get("Authorization")?.replace(/^Bearer\s+/i, "");
|
|
1084
|
-
const result = spawnCodexAgent({
|
|
1085
|
-
cwd,
|
|
1086
|
-
approvalMode,
|
|
1087
|
-
label,
|
|
1088
|
-
relayUrl,
|
|
1089
|
-
token: token || undefined,
|
|
1090
|
-
dryRun: process.env.AGENT_RELAY_SPAWN_DRY_RUN === "1",
|
|
1144
|
+
const orch = orchestrators[0];
|
|
1145
|
+
if (!orch) return error("no orchestrator available for provider: " + provider);
|
|
1146
|
+
const command = createCommand({
|
|
1147
|
+
type: "agent.spawn",
|
|
1148
|
+
source: "system",
|
|
1149
|
+
target: orch.agentId,
|
|
1150
|
+
params: { action: "spawn", provider, cwd, label, approvalMode, requestedBy: "dashboard", requestedAt: Date.now() },
|
|
1091
1151
|
});
|
|
1152
|
+
emitCommand(command);
|
|
1092
1153
|
auditEvent({
|
|
1093
|
-
clientId: "server-agent-spawn-
|
|
1154
|
+
clientId: "server-agent-spawn-" + provider + "-" + Date.now(),
|
|
1094
1155
|
kind: "state",
|
|
1095
|
-
title:
|
|
1096
|
-
body:
|
|
1097
|
-
meta:
|
|
1156
|
+
title: `${provider} agent spawn requested (via ${orch.id})`,
|
|
1157
|
+
body: cwd || orch.baseDir,
|
|
1158
|
+
meta: orch.id,
|
|
1098
1159
|
icon: "ti-plus",
|
|
1099
1160
|
view: "agents",
|
|
1100
|
-
metadata: {
|
|
1101
|
-
provider: result.provider,
|
|
1102
|
-
approvalMode: result.approvalMode,
|
|
1103
|
-
pid: result.pid ?? null,
|
|
1104
|
-
logPath: result.logPath,
|
|
1105
|
-
dryRun: result.dryRun === true,
|
|
1106
|
-
},
|
|
1161
|
+
metadata: { provider, orchestratorId: orch.id, approvalMode, commandId: command.id },
|
|
1107
1162
|
});
|
|
1108
|
-
return json(
|
|
1163
|
+
return json({ ok: true, orchestratorId: orch.id, provider, command }, 202);
|
|
1109
1164
|
} catch (e) {
|
|
1110
1165
|
if (e instanceof ValidationError) return error(e.message, 400);
|
|
1111
1166
|
if (e instanceof Error) return error(e.message, 400);
|
|
@@ -1125,11 +1180,137 @@ const getHostDirectories: Handler = (req) => {
|
|
|
1125
1180
|
}
|
|
1126
1181
|
};
|
|
1127
1182
|
|
|
1183
|
+
// --- Recipe routes ---
|
|
1184
|
+
|
|
1185
|
+
const getRecipes: Handler = () => {
|
|
1186
|
+
return json(listRecipes().map((loaded) => ({
|
|
1187
|
+
name: loaded.name,
|
|
1188
|
+
source: loaded.source,
|
|
1189
|
+
path: loaded.path,
|
|
1190
|
+
recipe: loaded.recipe,
|
|
1191
|
+
})));
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
const getRecipeByName: Handler = (_req, params) => {
|
|
1195
|
+
const recipe = getRecipe(params.name!);
|
|
1196
|
+
return recipe ? json(recipe) : error("recipe not found", 404);
|
|
1197
|
+
};
|
|
1198
|
+
|
|
1199
|
+
const getRecipeInstances: Handler = (req) => {
|
|
1200
|
+
const url = new URL(req.url);
|
|
1201
|
+
const status = url.searchParams.get("status") ?? undefined;
|
|
1202
|
+
return json(listRecipeInstances(status as any));
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
const getRecipeInstanceById: Handler = (_req, params) => {
|
|
1206
|
+
const instance = getRecipeInstance(params.id!);
|
|
1207
|
+
return instance ? json(instance) : error("recipe instance not found", 404);
|
|
1208
|
+
};
|
|
1209
|
+
|
|
1210
|
+
const postRecipeStart: Handler = async (req) => {
|
|
1211
|
+
const parsed = await parseBody<unknown>(req);
|
|
1212
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
1213
|
+
try {
|
|
1214
|
+
if (!isRecord(parsed.body)) return error("recipe name required");
|
|
1215
|
+
const name = cleanString(parsed.body.name ?? parsed.body.recipe, "name", { required: true, max: 120 })!;
|
|
1216
|
+
const cwd = cleanString(parsed.body.cwd, "cwd", { max: 500 });
|
|
1217
|
+
const orchestratorId = cleanString(parsed.body.orchestratorId, "orchestratorId", { max: 120 });
|
|
1218
|
+
const startedBy = cleanString(parsed.body.startedBy, "startedBy", { max: 120 }) ?? "api";
|
|
1219
|
+
const result = startRecipe({ name, cwd, orchestratorId, startedBy });
|
|
1220
|
+
for (const command of result.commands) emitCommand(command);
|
|
1221
|
+
auditEvent({
|
|
1222
|
+
clientId: "server-recipe-start-" + result.instance.id,
|
|
1223
|
+
kind: "state",
|
|
1224
|
+
title: "Recipe started",
|
|
1225
|
+
body: result.instance.recipeName,
|
|
1226
|
+
meta: result.instance.id,
|
|
1227
|
+
icon: "ti-play",
|
|
1228
|
+
view: "activity",
|
|
1229
|
+
metadata: { recipeInstanceId: result.instance.id, commands: result.commands.map((command) => command.id) },
|
|
1230
|
+
});
|
|
1231
|
+
return json(result, 202);
|
|
1232
|
+
} catch (e) {
|
|
1233
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
1234
|
+
if (e instanceof Error) return error(e.message, 400);
|
|
1235
|
+
throw e;
|
|
1236
|
+
}
|
|
1237
|
+
};
|
|
1238
|
+
|
|
1239
|
+
const postRecipeStop: Handler = async (req, params) => {
|
|
1240
|
+
const parsed = await parseBody<unknown>(req);
|
|
1241
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
1242
|
+
try {
|
|
1243
|
+
const stoppedBy = isRecord(parsed.body)
|
|
1244
|
+
? cleanString(parsed.body.stoppedBy, "stoppedBy", { max: 120 }) ?? "api"
|
|
1245
|
+
: "api";
|
|
1246
|
+
const result = stopRecipe(params.id!, stoppedBy);
|
|
1247
|
+
for (const command of result.commands) emitCommand(command);
|
|
1248
|
+
auditEvent({
|
|
1249
|
+
clientId: "server-recipe-stop-" + result.instance.id + "-" + Date.now(),
|
|
1250
|
+
kind: "state",
|
|
1251
|
+
title: "Recipe stopped",
|
|
1252
|
+
body: result.instance.recipeName,
|
|
1253
|
+
meta: result.instance.id,
|
|
1254
|
+
icon: "ti-player-stop",
|
|
1255
|
+
view: "activity",
|
|
1256
|
+
metadata: { recipeInstanceId: result.instance.id, commands: result.commands.map((command) => command.id) },
|
|
1257
|
+
});
|
|
1258
|
+
return json(result, 202);
|
|
1259
|
+
} catch (e) {
|
|
1260
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
1261
|
+
if (e instanceof Error) return error(e.message, e.message.includes("not found") ? 404 : 400);
|
|
1262
|
+
throw e;
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1266
|
+
// --- Token routes ---
|
|
1267
|
+
|
|
1268
|
+
const getTokens: Handler = () => json(listTokens());
|
|
1269
|
+
|
|
1270
|
+
const getTokenById: Handler = (_req, params) => {
|
|
1271
|
+
const token = getToken(params.jti!);
|
|
1272
|
+
return token ? json(token) : error("token not found", 404);
|
|
1273
|
+
};
|
|
1274
|
+
|
|
1275
|
+
const postToken: Handler = async (req) => {
|
|
1276
|
+
const parsed = await parseBody<unknown>(req);
|
|
1277
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
1278
|
+
try {
|
|
1279
|
+
if (!isRecord(parsed.body)) return error("token body required");
|
|
1280
|
+
const role = cleanString(parsed.body.role, "role", { required: true, max: 80 })!;
|
|
1281
|
+
const sub = cleanString(parsed.body.sub, "sub", { max: 160 }) ?? role;
|
|
1282
|
+
const scope = cleanStringArray(parsed.body.scope ?? parsed.body.scopes, "scope");
|
|
1283
|
+
const createdBy = cleanString(parsed.body.createdBy, "createdBy", { max: 120 });
|
|
1284
|
+
let ttlSeconds: number | undefined;
|
|
1285
|
+
if (parsed.body.ttlSeconds !== undefined) {
|
|
1286
|
+
if (typeof parsed.body.ttlSeconds !== "number" || !Number.isSafeInteger(parsed.body.ttlSeconds) || parsed.body.ttlSeconds <= 0) {
|
|
1287
|
+
throw new ValidationError("ttlSeconds must be a positive integer");
|
|
1288
|
+
}
|
|
1289
|
+
ttlSeconds = parsed.body.ttlSeconds;
|
|
1290
|
+
}
|
|
1291
|
+
return json(createToken({ sub, role, scope, ttlSeconds, createdBy }), 201);
|
|
1292
|
+
} catch (e) {
|
|
1293
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
1294
|
+
throw e;
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
|
|
1298
|
+
const postTokenRevoke: Handler = (_req, params) => {
|
|
1299
|
+
if (!revokeToken(params.jti!)) return error("token not found or already revoked", 404);
|
|
1300
|
+
return json({ ok: true });
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1128
1303
|
// --- Orchestrator routes ---
|
|
1129
1304
|
|
|
1130
1305
|
const VALID_ORCHESTRATOR_PROVIDERS = ["claude", "codex"] as const;
|
|
1131
1306
|
const VALID_SPAWN_APPROVALS = ["open", "guarded", "read-only"] as const;
|
|
1132
1307
|
|
|
1308
|
+
function cleanProtocolVersion(value: unknown): number | undefined {
|
|
1309
|
+
if (value === undefined) return undefined;
|
|
1310
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
|
|
1311
|
+
throw new ValidationError("protocolVersion must be a positive integer");
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1133
1314
|
const postOrchestrator: Handler = async (req) => {
|
|
1134
1315
|
const parsed = await parseBody<unknown>(req);
|
|
1135
1316
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
@@ -1149,12 +1330,7 @@ const postOrchestrator: Handler = async (req) => {
|
|
|
1149
1330
|
const envKeys = cleanStringArray(parsed.body.envKeys, "envKeys");
|
|
1150
1331
|
const version = cleanString(parsed.body.version, "version", { max: 80 });
|
|
1151
1332
|
const gitSha = cleanString(parsed.body.gitSha, "gitSha", { max: 80 });
|
|
1152
|
-
const
|
|
1153
|
-
const protocolVersion = protocolVersionRaw === undefined
|
|
1154
|
-
? undefined
|
|
1155
|
-
: typeof protocolVersionRaw === "number" && Number.isInteger(protocolVersionRaw) && protocolVersionRaw > 0
|
|
1156
|
-
? protocolVersionRaw
|
|
1157
|
-
: (() => { throw new ValidationError("protocolVersion must be a positive integer"); })();
|
|
1333
|
+
const protocolVersion = cleanProtocolVersion(parsed.body.protocolVersion);
|
|
1158
1334
|
const meta = cleanMeta(parsed.body.meta);
|
|
1159
1335
|
const orch = upsertOrchestrator({ id, hostname, providers: providers ?? ["claude", "codex"], baseDir, envKeys, version, protocolVersion, gitSha, meta });
|
|
1160
1336
|
auditEvent({
|
|
@@ -1184,10 +1360,23 @@ const getOrchestratorById: Handler = (_req, params) => {
|
|
|
1184
1360
|
return json(orch);
|
|
1185
1361
|
};
|
|
1186
1362
|
|
|
1187
|
-
const postOrchestratorHeartbeat: Handler = (
|
|
1188
|
-
const
|
|
1189
|
-
if (!
|
|
1190
|
-
|
|
1363
|
+
const postOrchestratorHeartbeat: Handler = async (req, params) => {
|
|
1364
|
+
const parsed = await parseBody<unknown>(req);
|
|
1365
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
1366
|
+
try {
|
|
1367
|
+
const body = isRecord(parsed.body) ? parsed.body : {};
|
|
1368
|
+
const runtime: OrchestratorRuntimeInput = {
|
|
1369
|
+
version: cleanString(body.version, "version", { max: 80 }),
|
|
1370
|
+
protocolVersion: cleanProtocolVersion(body.protocolVersion),
|
|
1371
|
+
gitSha: cleanString(body.gitSha, "gitSha", { max: 80 }),
|
|
1372
|
+
};
|
|
1373
|
+
const orch = orchestratorHeartbeat(params.id!, runtime);
|
|
1374
|
+
if (!orch) return error("orchestrator not found", 404);
|
|
1375
|
+
return json({ ok: true });
|
|
1376
|
+
} catch (e) {
|
|
1377
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
1378
|
+
throw e;
|
|
1379
|
+
}
|
|
1191
1380
|
};
|
|
1192
1381
|
|
|
1193
1382
|
const patchOrchestratorAgents: Handler = async (req, params) => {
|
|
@@ -1242,40 +1431,33 @@ const postOrchestratorSpawn: Handler = async (req, params) => {
|
|
|
1242
1431
|
const approvalMode = cleanEnum(parsed.body.approvalMode, "approvalMode", VALID_SPAWN_APPROVALS, "guarded") as SpawnApprovalMode;
|
|
1243
1432
|
const prompt = cleanString(parsed.body.prompt, "prompt", { max: 4000 });
|
|
1244
1433
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
to: orch.agentId,
|
|
1259
|
-
kind: "control",
|
|
1260
|
-
subject: "Spawn agent",
|
|
1261
|
-
body: `Spawn ${provider} agent${label ? ` (${label})` : ""}`,
|
|
1262
|
-
payload: {
|
|
1263
|
-
orchestratorControl: control,
|
|
1434
|
+
const command = createCommand({
|
|
1435
|
+
type: "agent.spawn",
|
|
1436
|
+
source: "system",
|
|
1437
|
+
target: orch.agentId,
|
|
1438
|
+
params: {
|
|
1439
|
+
action: "spawn",
|
|
1440
|
+
provider,
|
|
1441
|
+
cwd: cwd || orch.baseDir,
|
|
1442
|
+
label,
|
|
1443
|
+
approvalMode,
|
|
1444
|
+
prompt,
|
|
1445
|
+
requestedBy: "dashboard",
|
|
1446
|
+
requestedAt: Date.now(),
|
|
1264
1447
|
},
|
|
1265
|
-
meta: orchestratorControlMeta(control),
|
|
1266
1448
|
});
|
|
1267
|
-
|
|
1449
|
+
emitCommand(command);
|
|
1268
1450
|
auditEvent({
|
|
1269
|
-
clientId: "server-orchestrator-spawn-" + orch.id + "-" +
|
|
1451
|
+
clientId: "server-orchestrator-spawn-" + orch.id + "-" + command.id,
|
|
1270
1452
|
kind: "state",
|
|
1271
1453
|
title: `Spawn ${provider} agent requested`,
|
|
1272
1454
|
body: cwd || orch.baseDir,
|
|
1273
1455
|
meta: orch.id,
|
|
1274
1456
|
icon: "ti-plus",
|
|
1275
1457
|
view: "orchestrators",
|
|
1276
|
-
metadata: { orchestratorId: orch.id, provider, approvalMode, label },
|
|
1458
|
+
metadata: { orchestratorId: orch.id, provider, approvalMode, label, commandId: command.id },
|
|
1277
1459
|
});
|
|
1278
|
-
return json({ ok: true, orchestratorId: orch.id,
|
|
1460
|
+
return json({ ok: true, orchestratorId: orch.id, command }, 202);
|
|
1279
1461
|
} catch (e) {
|
|
1280
1462
|
if (e instanceof ValidationError) return error(e.message, 400);
|
|
1281
1463
|
throw e;
|
|
@@ -1294,47 +1476,231 @@ const postOrchestratorAction: Handler = async (req, params) => {
|
|
|
1294
1476
|
if (!action) return error("action required");
|
|
1295
1477
|
const agentId = cleanString(parsed.body.agentId, "agentId", { max: 240 });
|
|
1296
1478
|
|
|
1297
|
-
const
|
|
1298
|
-
action,
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
requestedAt: Date.now(),
|
|
1302
|
-
};
|
|
1303
|
-
const msg = sendMessage({
|
|
1304
|
-
from: "system",
|
|
1305
|
-
to: orch.agentId,
|
|
1306
|
-
kind: "control",
|
|
1307
|
-
subject: action === "restart" ? "Restart agent" : "Shutdown agent",
|
|
1308
|
-
body: agentId ? `${action} ${agentId}` : `${action} all managed agents`,
|
|
1309
|
-
payload: {
|
|
1310
|
-
orchestratorControl: control,
|
|
1311
|
-
},
|
|
1312
|
-
meta: orchestratorControlMeta(control),
|
|
1479
|
+
const command = createCommand({
|
|
1480
|
+
type: action === "restart" ? "agent.restart" : "agent.shutdown",
|
|
1481
|
+
source: "system",
|
|
1482
|
+
target: agentId || orch.agentId,
|
|
1483
|
+
params: { action, agentId, requestedBy: "dashboard", requestedAt: Date.now(), orchestratorId: orch.id },
|
|
1313
1484
|
});
|
|
1314
|
-
|
|
1485
|
+
emitCommand(command);
|
|
1315
1486
|
auditEvent({
|
|
1316
|
-
clientId: "server-orchestrator-action-" + orch.id + "-" + action + "-" +
|
|
1487
|
+
clientId: "server-orchestrator-action-" + orch.id + "-" + action + "-" + command.id,
|
|
1317
1488
|
kind: "state",
|
|
1318
1489
|
title: `Agent ${action} requested`,
|
|
1319
1490
|
body: agentId || "all",
|
|
1320
1491
|
meta: orch.id,
|
|
1321
1492
|
icon: action === "restart" ? "ti-refresh" : "ti-power",
|
|
1322
1493
|
view: "orchestrators",
|
|
1323
|
-
metadata: { orchestratorId: orch.id, action, agentId },
|
|
1494
|
+
metadata: { orchestratorId: orch.id, action, agentId, commandId: command.id },
|
|
1324
1495
|
});
|
|
1325
|
-
return json({ ok: true, action,
|
|
1496
|
+
return json({ ok: true, action, command }, 202);
|
|
1326
1497
|
} catch (e) {
|
|
1327
1498
|
if (e instanceof ValidationError) return error(e.message, 400);
|
|
1328
1499
|
throw e;
|
|
1329
1500
|
}
|
|
1330
1501
|
};
|
|
1331
1502
|
|
|
1503
|
+
const getOrchestratorDirectories: Handler = async (req, params) => {
|
|
1504
|
+
const orch = getOrchestrator(params.id!);
|
|
1505
|
+
if (!orch) return error("orchestrator not found", 404);
|
|
1506
|
+
if (!orch.apiUrl) return error("orchestrator does not expose an API", 422);
|
|
1507
|
+
if (orch.status !== "online") return error("orchestrator is offline", 422);
|
|
1508
|
+
const url = new URL(req.url);
|
|
1509
|
+
const path = url.searchParams.get("path") || "";
|
|
1510
|
+
const proxyUrl = `${orch.apiUrl}/api/directories${path ? `?path=${encodeURIComponent(path)}` : ""}`;
|
|
1511
|
+
try {
|
|
1512
|
+
const res = await fetch(proxyUrl, { signal: AbortSignal.timeout(5_000) });
|
|
1513
|
+
const body = await res.json();
|
|
1514
|
+
return json(body, res.status);
|
|
1515
|
+
} catch (e) {
|
|
1516
|
+
return error(`Failed to reach orchestrator API: ${(e as Error).message}`, 502);
|
|
1517
|
+
}
|
|
1518
|
+
};
|
|
1519
|
+
|
|
1332
1520
|
const deleteOrchestratorById: Handler = (_req, params) => {
|
|
1333
1521
|
const deleted = deleteOrchestrator(params.id!);
|
|
1334
1522
|
if (!deleted) return error("orchestrator not found", 404);
|
|
1335
1523
|
return json({ ok: true });
|
|
1336
1524
|
};
|
|
1337
1525
|
|
|
1526
|
+
// --- Command routes ---
|
|
1527
|
+
|
|
1528
|
+
function normalizeCommandInput(body: unknown): CreateCommandInput {
|
|
1529
|
+
if (!isRecord(body)) throw new ValidationError("command body must be an object");
|
|
1530
|
+
const type = cleanString(body.type, "type", { required: true, max: 120 })!;
|
|
1531
|
+
const source = cleanString(body.source, "source", { required: true, max: 240 })!;
|
|
1532
|
+
const target = cleanString(body.target, "target", { required: true, max: 240 })!;
|
|
1533
|
+
const params = cleanParams(body.params) ?? {};
|
|
1534
|
+
const correlationId = cleanString(body.correlationId, "correlationId", { max: 240 });
|
|
1535
|
+
let ttlMs: number | undefined;
|
|
1536
|
+
if (body.ttlMs !== undefined) {
|
|
1537
|
+
if (typeof body.ttlMs !== "number" || !Number.isSafeInteger(body.ttlMs) || body.ttlMs <= 0) {
|
|
1538
|
+
throw new ValidationError("ttlMs must be a positive integer");
|
|
1539
|
+
}
|
|
1540
|
+
ttlMs = body.ttlMs;
|
|
1541
|
+
}
|
|
1542
|
+
return { type, source, target, params, correlationId, ttlMs };
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
function commandEventType(command: Command): string {
|
|
1546
|
+
if (command.status === "pending") return "command.requested";
|
|
1547
|
+
return `command.${command.status}`;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function emitCommand(command: Command): void {
|
|
1551
|
+
emitRelayEvent({
|
|
1552
|
+
type: commandEventType(command),
|
|
1553
|
+
source: command.source,
|
|
1554
|
+
subject: command.id,
|
|
1555
|
+
data: { command },
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
const postCommand: Handler = async (req) => {
|
|
1560
|
+
const parsed = await parseBody<unknown>(req);
|
|
1561
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
1562
|
+
try {
|
|
1563
|
+
const command = createCommand(normalizeCommandInput(parsed.body));
|
|
1564
|
+
emitCommand(command);
|
|
1565
|
+
return json(command, 201);
|
|
1566
|
+
} catch (e) {
|
|
1567
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
1568
|
+
throw e;
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
|
|
1572
|
+
const getCommands: Handler = (req) => {
|
|
1573
|
+
const url = new URL(req.url);
|
|
1574
|
+
const limitRaw = parseQueryInt(url.searchParams.get("limit"), { min: 1, max: 500 });
|
|
1575
|
+
if (Number.isNaN(limitRaw)) return error("limit must be an integer between 1 and 500");
|
|
1576
|
+
const sinceRaw = parseQueryInt(url.searchParams.get("since"), { min: 0, max: Number.MAX_SAFE_INTEGER });
|
|
1577
|
+
if (Number.isNaN(sinceRaw)) return error("since must be a non-negative integer");
|
|
1578
|
+
const status = url.searchParams.get("status") ?? undefined;
|
|
1579
|
+
if (status && !VALID_COMMAND_STATUSES.includes(status as any)) {
|
|
1580
|
+
return error(`status must be one of: ${VALID_COMMAND_STATUSES.join(", ")}`);
|
|
1581
|
+
}
|
|
1582
|
+
return json(listCommands({
|
|
1583
|
+
target: url.searchParams.get("target") ?? undefined,
|
|
1584
|
+
status: status as CommandStatus | undefined,
|
|
1585
|
+
type: url.searchParams.get("type") ?? undefined,
|
|
1586
|
+
since: sinceRaw ?? undefined,
|
|
1587
|
+
limit: limitRaw ?? undefined,
|
|
1588
|
+
}));
|
|
1589
|
+
};
|
|
1590
|
+
|
|
1591
|
+
const getCommandById: Handler = (_req, params) => {
|
|
1592
|
+
const command = getCommand(params.id!);
|
|
1593
|
+
return command ? json(command) : error("command not found", 404);
|
|
1594
|
+
};
|
|
1595
|
+
|
|
1596
|
+
const patchCommand: Handler = async (req, params) => {
|
|
1597
|
+
const parsed = await parseBody<unknown>(req);
|
|
1598
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
1599
|
+
if (!isRecord(parsed.body)) return error("command body must be an object");
|
|
1600
|
+
try {
|
|
1601
|
+
const status = cleanEnum(parsed.body.status, "status", VALID_COMMAND_STATUSES);
|
|
1602
|
+
const result = cleanParams(parsed.body.result, "result");
|
|
1603
|
+
const err = cleanString(parsed.body.error, "error", { max: 4000 });
|
|
1604
|
+
const command = updateCommand(params.id!, { status: status as CommandStatus | undefined, result, error: err });
|
|
1605
|
+
if (!command) return error("command not found", 404);
|
|
1606
|
+
applyCommandToRecipe(command);
|
|
1607
|
+
emitCommand(command);
|
|
1608
|
+
return json(command);
|
|
1609
|
+
} catch (e) {
|
|
1610
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
1611
|
+
throw e;
|
|
1612
|
+
}
|
|
1613
|
+
};
|
|
1614
|
+
|
|
1615
|
+
// --- Provider config routes ---
|
|
1616
|
+
|
|
1617
|
+
const VALID_PROVIDER_CONFIGS = ["claude", "codex"] as const;
|
|
1618
|
+
|
|
1619
|
+
const getProviderConfigsRoute: Handler = () => {
|
|
1620
|
+
return json(Object.fromEntries(VALID_PROVIDER_CONFIGS.map((provider) => {
|
|
1621
|
+
const config = loadProviderConfig(provider);
|
|
1622
|
+
return [provider, providerConfigPublic(config)];
|
|
1623
|
+
})));
|
|
1624
|
+
};
|
|
1625
|
+
|
|
1626
|
+
const getProviderConfigRoute: Handler = (_req, params) => {
|
|
1627
|
+
const provider = cleanEnum(params.provider, "provider", VALID_PROVIDER_CONFIGS);
|
|
1628
|
+
if (!provider) return error("provider required");
|
|
1629
|
+
return json(providerConfigPublic(loadProviderConfig(provider)));
|
|
1630
|
+
};
|
|
1631
|
+
|
|
1632
|
+
const putProviderConfigRoute: Handler = async (req, params) => {
|
|
1633
|
+
const provider = cleanEnum(params.provider, "provider", VALID_PROVIDER_CONFIGS);
|
|
1634
|
+
if (!provider) return error("provider required");
|
|
1635
|
+
const parsed = await parseBody<unknown>(req);
|
|
1636
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
1637
|
+
try {
|
|
1638
|
+
if (!isRecord(parsed.body)) return error("provider config body must be an object");
|
|
1639
|
+
const defaults = defaultProviderConfig(provider);
|
|
1640
|
+
const headless = isRecord(parsed.body.headless) ? parsed.body.headless : {};
|
|
1641
|
+
const config: ProviderConfig = {
|
|
1642
|
+
command: cleanString(parsed.body.command, "command", { required: true, max: 500 })!,
|
|
1643
|
+
defaultArgs: cleanStringArray(parsed.body.defaultArgs, "defaultArgs") ?? defaults.defaultArgs,
|
|
1644
|
+
env: cleanEnvRecord(parsed.body.env),
|
|
1645
|
+
pluginDirs: cleanStringArray(parsed.body.pluginDirs, "pluginDirs") ?? defaults.pluginDirs,
|
|
1646
|
+
defaultCapabilities: cleanStringArray(parsed.body.defaultCapabilities, "defaultCapabilities") ?? defaults.defaultCapabilities,
|
|
1647
|
+
defaultApprovalMode: cleanString(parsed.body.defaultApprovalMode, "defaultApprovalMode", { max: 80 }) ?? defaults.defaultApprovalMode,
|
|
1648
|
+
defaultTags: cleanStringArray(parsed.body.defaultTags, "defaultTags") ?? defaults.defaultTags,
|
|
1649
|
+
headless: {
|
|
1650
|
+
tmuxPrefix: cleanString(headless.tmuxPrefix, "headless.tmuxPrefix", { max: 120 }) ?? defaults.headless.tmuxPrefix,
|
|
1651
|
+
shutdownTimeoutMs: cleanPositiveInt(headless.shutdownTimeoutMs, "headless.shutdownTimeoutMs") ?? defaults.headless.shutdownTimeoutMs,
|
|
1652
|
+
},
|
|
1653
|
+
};
|
|
1654
|
+
return json(providerConfigPublic(writeProviderConfig(provider, config)));
|
|
1655
|
+
} catch (e) {
|
|
1656
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
1657
|
+
throw e;
|
|
1658
|
+
}
|
|
1659
|
+
};
|
|
1660
|
+
|
|
1661
|
+
const postProviderConfigTestRoute: Handler = async (_req, params) => {
|
|
1662
|
+
const provider = cleanEnum(params.provider, "provider", VALID_PROVIDER_CONFIGS);
|
|
1663
|
+
if (!provider) return error("provider required");
|
|
1664
|
+
const config = loadProviderConfig(provider);
|
|
1665
|
+
const proc = Bun.spawn(["bash", "-lc", `command -v "$1"`, "bash", config.command], {
|
|
1666
|
+
stdout: "pipe",
|
|
1667
|
+
stderr: "pipe",
|
|
1668
|
+
});
|
|
1669
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
1670
|
+
proc.exited,
|
|
1671
|
+
new Response(proc.stdout).text(),
|
|
1672
|
+
new Response(proc.stderr).text(),
|
|
1673
|
+
]);
|
|
1674
|
+
return json({ ok: exitCode === 0, command: config.command, path: stdout.trim(), error: stderr.trim() }, exitCode === 0 ? 200 : 422);
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
function cleanEnvRecord(value: unknown): Record<string, string> {
|
|
1678
|
+
if (value === undefined) return {};
|
|
1679
|
+
if (!isRecord(value)) throw new ValidationError("env must be an object");
|
|
1680
|
+
const env: Record<string, string> = {};
|
|
1681
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1682
|
+
if (typeof item !== "string") throw new ValidationError(`env.${key} must be a string`);
|
|
1683
|
+
env[key] = item;
|
|
1684
|
+
}
|
|
1685
|
+
return env;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
function cleanPositiveInt(value: unknown, field: string): number | undefined {
|
|
1689
|
+
if (value === undefined) return undefined;
|
|
1690
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new ValidationError(`${field} must be a positive integer`);
|
|
1691
|
+
return value;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
const deleteCommandById: Handler = (_req, params) => {
|
|
1695
|
+
if (!deleteCommand(params.id!)) return error("command not found or cannot be canceled", 404);
|
|
1696
|
+
const command = getCommand(params.id!);
|
|
1697
|
+
if (command) {
|
|
1698
|
+
applyCommandToRecipe(command);
|
|
1699
|
+
emitCommand(command);
|
|
1700
|
+
}
|
|
1701
|
+
return json({ ok: true });
|
|
1702
|
+
};
|
|
1703
|
+
|
|
1338
1704
|
// --- Message routes ---
|
|
1339
1705
|
|
|
1340
1706
|
const VALID_MSG_KINDS = ["chat", "channel.event", "task", "pair", "control", "system"];
|
|
@@ -1349,6 +1715,13 @@ const postMessage: Handler = async (req) => {
|
|
|
1349
1715
|
}
|
|
1350
1716
|
applyReplyRouting(input);
|
|
1351
1717
|
if (!input.to) return error("to is required (or provide replyTo to auto-route)");
|
|
1718
|
+
const bypassKinds = ["system", "control"];
|
|
1719
|
+
if (isDirectTarget(input.to) && !bypassKinds.includes(input.kind ?? "")) {
|
|
1720
|
+
const target = getAgent(input.to);
|
|
1721
|
+
if (target && target.status === "offline") {
|
|
1722
|
+
return error(`agent "${input.to}" is offline`, 422);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1352
1725
|
const result = sendMessageWithResult(input);
|
|
1353
1726
|
if (result.created) {
|
|
1354
1727
|
emitNewMessage(result.message);
|
|
@@ -1640,7 +2013,7 @@ const postConnectorAction: Handler = async (req, params) => {
|
|
|
1640
2013
|
try {
|
|
1641
2014
|
if (!isRecord(parsed.body)) throw new ValidationError("JSON object body required");
|
|
1642
2015
|
const action = cleanEnum(parsed.body.action, "action", VALID_CONNECTOR_ACTIONS)!;
|
|
1643
|
-
const result = runConnectorAction(params.id!, action);
|
|
2016
|
+
const result = await runConnectorAction(params.id!, action);
|
|
1644
2017
|
return json(result, result.ok ? 200 : 502);
|
|
1645
2018
|
} catch (e) {
|
|
1646
2019
|
if (e instanceof ValidationError) return error(e.message, e.message.includes("not found") ? 404 : 400);
|
|
@@ -1712,6 +2085,83 @@ const getIntegrations: Handler = () => {
|
|
|
1712
2085
|
return json(integrations);
|
|
1713
2086
|
};
|
|
1714
2087
|
|
|
2088
|
+
function emitAutomationDispatch(result: AutomationDispatchResult): void {
|
|
2089
|
+
if (result.command) emitCommand(result.command);
|
|
2090
|
+
if (result.message) emitNewMessage(result.message);
|
|
2091
|
+
if (result.task) emitTaskChanged(result.task, "task.created");
|
|
2092
|
+
emitRelayEvent({
|
|
2093
|
+
type: "automation.run",
|
|
2094
|
+
source: "server",
|
|
2095
|
+
subject: result.run.id,
|
|
2096
|
+
data: { automation: result.automation, run: result.run, task: result.task },
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
const getAutomations: Handler = () => {
|
|
2101
|
+
return json(listAutomations());
|
|
2102
|
+
};
|
|
2103
|
+
|
|
2104
|
+
const postAutomation: Handler = async (req) => {
|
|
2105
|
+
const parsed = await parseBody<unknown>(req);
|
|
2106
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
2107
|
+
try {
|
|
2108
|
+
const automation = createAutomation(parsed.body as any);
|
|
2109
|
+
emitRelayEvent({ type: "automation.created", source: "server", subject: automation.id, data: { automation } });
|
|
2110
|
+
return json(automation, 201);
|
|
2111
|
+
} catch (e) {
|
|
2112
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
2113
|
+
throw e;
|
|
2114
|
+
}
|
|
2115
|
+
};
|
|
2116
|
+
|
|
2117
|
+
const getAutomationById: Handler = (_req, params) => {
|
|
2118
|
+
const automation = getAutomation(params.id!);
|
|
2119
|
+
return automation ? json(automation) : error("automation not found", 404);
|
|
2120
|
+
};
|
|
2121
|
+
|
|
2122
|
+
const patchAutomation: Handler = async (req, params) => {
|
|
2123
|
+
const parsed = await parseBody<unknown>(req);
|
|
2124
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
2125
|
+
try {
|
|
2126
|
+
const automation = updateAutomation(params.id!, parsed.body as any);
|
|
2127
|
+
if (!automation) return error("automation not found", 404);
|
|
2128
|
+
emitRelayEvent({ type: "automation.updated", source: "server", subject: automation.id, data: { automation } });
|
|
2129
|
+
return json(automation);
|
|
2130
|
+
} catch (e) {
|
|
2131
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
2132
|
+
throw e;
|
|
2133
|
+
}
|
|
2134
|
+
};
|
|
2135
|
+
|
|
2136
|
+
const deleteAutomationById: Handler = (_req, params) => {
|
|
2137
|
+
if (!deleteAutomation(params.id!)) return error("automation not found", 404);
|
|
2138
|
+
emitRelayEvent({ type: "automation.deleted", source: "server", subject: params.id!, data: { automationId: params.id! } });
|
|
2139
|
+
return json({ ok: true });
|
|
2140
|
+
};
|
|
2141
|
+
|
|
2142
|
+
const postAutomationRun: Handler = (_req, params) => {
|
|
2143
|
+
try {
|
|
2144
|
+
const result = runAutomationNow(params.id!);
|
|
2145
|
+
if (!result) return error("automation not found", 404);
|
|
2146
|
+
emitAutomationDispatch(result);
|
|
2147
|
+
return json(result, 202);
|
|
2148
|
+
} catch (e) {
|
|
2149
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
2150
|
+
throw e;
|
|
2151
|
+
}
|
|
2152
|
+
};
|
|
2153
|
+
|
|
2154
|
+
const getAutomationRuns: Handler = (req) => {
|
|
2155
|
+
const url = new URL(req.url);
|
|
2156
|
+
const limitRaw = parseQueryInt(url.searchParams.get("limit"), { min: 1, max: 500 });
|
|
2157
|
+
if (Number.isNaN(limitRaw)) return error("limit must be an integer between 1 and 500");
|
|
2158
|
+
return json(listAutomationRuns({
|
|
2159
|
+
automationId: url.searchParams.get("automationId") ?? undefined,
|
|
2160
|
+
status: url.searchParams.get("status") ?? undefined,
|
|
2161
|
+
limit: limitRaw ?? 100,
|
|
2162
|
+
}));
|
|
2163
|
+
};
|
|
2164
|
+
|
|
1715
2165
|
const getChannels: Handler = () => {
|
|
1716
2166
|
return json(listChannels());
|
|
1717
2167
|
};
|
|
@@ -1762,13 +2212,13 @@ const postChannelEvent: Handler = async (req, params) => {
|
|
|
1762
2212
|
if (!bindings.length) return error("channel has no binding", 409);
|
|
1763
2213
|
const results = bindings.map((binding) => sendMessageWithResult({
|
|
1764
2214
|
from: channel.agentId,
|
|
1765
|
-
to: messageTargetForChannelTarget(binding.target),
|
|
2215
|
+
to: messageTargetForChannelTarget(binding.target, binding),
|
|
1766
2216
|
kind: "channel.event",
|
|
1767
2217
|
channel: channel.id,
|
|
1768
2218
|
body: input.body,
|
|
1769
2219
|
payload: input.payload,
|
|
1770
2220
|
idempotencyKey: scopedChannelIdempotencyKey(input.idempotencyKey, binding),
|
|
1771
|
-
claimable:
|
|
2221
|
+
claimable: false,
|
|
1772
2222
|
}));
|
|
1773
2223
|
for (const result of results) {
|
|
1774
2224
|
if (result.created) emitNewMessage(result.message);
|
|
@@ -1824,10 +2274,12 @@ const postIntegrationEvent: Handler = async (req) => {
|
|
|
1824
2274
|
const parsed = await parseBody<unknown>(req);
|
|
1825
2275
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
1826
2276
|
try {
|
|
1827
|
-
const
|
|
1828
|
-
|
|
2277
|
+
const normalized = { ...normalizeIntegrationEvent(parsed.body), source: auth.name };
|
|
2278
|
+
const requestedChannelId = channelIdFromIntegrationTarget(normalized.target);
|
|
2279
|
+
if (!isIntegrationAllowed(auth, { target: normalized.target, channel: normalized.channel ?? requestedChannelId })) {
|
|
1829
2280
|
return error("integration token cannot target this task", 403);
|
|
1830
2281
|
}
|
|
2282
|
+
const input = resolveIntegrationEventTarget(normalized);
|
|
1831
2283
|
const result = ingestIntegrationEvent(input, auth.name);
|
|
1832
2284
|
if (result.message) emitNewMessage(result.message);
|
|
1833
2285
|
emitTaskChanged(result.task, result.created ? "task.created" : "task.updated");
|
|
@@ -2117,17 +2569,38 @@ const getHealthRoute: Handler = () => json(getHealth());
|
|
|
2117
2569
|
|
|
2118
2570
|
const postSystemReap: Handler = () => {
|
|
2119
2571
|
const released = releaseExpiredClaims();
|
|
2572
|
+
const releasedOrphans = releaseOrphanedTasks();
|
|
2573
|
+
const expiredCommands = expireCommands();
|
|
2120
2574
|
const reapedAgentIds = reapStaleAgents();
|
|
2121
2575
|
const reapedOrchestratorIds = reapStaleOrchestrators();
|
|
2122
2576
|
for (const id of released.messageIds) emitMessageClaimReleased(id);
|
|
2123
2577
|
for (const task of released.tasks) emitTaskChanged(task, "task.updated");
|
|
2124
|
-
for (const
|
|
2578
|
+
for (const task of releasedOrphans) emitTaskChanged(task, "task.updated");
|
|
2579
|
+
for (const command of expiredCommands) {
|
|
2580
|
+
applyCommandToRecipe(command);
|
|
2581
|
+
emitCommand(command);
|
|
2582
|
+
}
|
|
2583
|
+
for (const id of reapedAgentIds) {
|
|
2584
|
+
emitAgentStatus(id);
|
|
2585
|
+
auditEvent({
|
|
2586
|
+
clientId: "server-agent-" + id + "-heartbeat-lost-" + Date.now(),
|
|
2587
|
+
kind: "state",
|
|
2588
|
+
title: "Heartbeat lost",
|
|
2589
|
+
body: "Agent marked offline",
|
|
2590
|
+
meta: id,
|
|
2591
|
+
icon: "ti-heart-off",
|
|
2592
|
+
view: "chat",
|
|
2593
|
+
agentId: id,
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2125
2596
|
for (const id of reapedOrchestratorIds) emitOrchestratorStatus(id);
|
|
2597
|
+
const poolChanges = evaluatePoolBindings();
|
|
2598
|
+
for (const change of poolChanges) emitPoolBindingChanged(change.bindingId, change.channelId, change.previousAgentId, change.newAgentId);
|
|
2126
2599
|
auditEvent({
|
|
2127
2600
|
clientId: "server-system-reap-" + Date.now(),
|
|
2128
2601
|
kind: "state",
|
|
2129
2602
|
title: "Maintenance reaper run",
|
|
2130
|
-
body: `${reapedAgentIds.length} stale agent(s), ${reapedOrchestratorIds.length} stale orchestrator(s), ${released.messageIds.length} message claim(s), ${released.tasks.length} task claim(s)`,
|
|
2603
|
+
body: `${reapedAgentIds.length} stale agent(s), ${reapedOrchestratorIds.length} stale orchestrator(s), ${released.messageIds.length} message claim(s), ${released.tasks.length} task claim(s), ${expiredCommands.length} command(s)`,
|
|
2131
2604
|
icon: "ti-broom",
|
|
2132
2605
|
view: "activity",
|
|
2133
2606
|
});
|
|
@@ -2137,6 +2610,8 @@ const postSystemReap: Handler = () => {
|
|
|
2137
2610
|
reapedOrchestratorIds,
|
|
2138
2611
|
releasedMessageIds: released.messageIds,
|
|
2139
2612
|
releasedTaskIds: released.tasks.map((task) => task.id),
|
|
2613
|
+
releasedOrphanedTaskIds: releasedOrphans.map((task) => task.id),
|
|
2614
|
+
expiredCommandIds: expiredCommands.map((command) => command.id),
|
|
2140
2615
|
});
|
|
2141
2616
|
};
|
|
2142
2617
|
|
|
@@ -2184,9 +2659,30 @@ const routes: Route[] = [
|
|
|
2184
2659
|
route("PATCH", "/api/orchestrators/:id/agents", patchOrchestratorAgents),
|
|
2185
2660
|
route("POST", "/api/orchestrators/:id/spawn", postOrchestratorSpawn),
|
|
2186
2661
|
route("POST", "/api/orchestrators/:id/actions", postOrchestratorAction),
|
|
2662
|
+
route("GET", "/api/orchestrators/:id/directories", getOrchestratorDirectories),
|
|
2187
2663
|
route("DELETE", "/api/orchestrators/:id", deleteOrchestratorById),
|
|
2188
2664
|
|
|
2189
2665
|
route("POST", "/api/system/broadcast", postSystemBroadcast),
|
|
2666
|
+
route("GET", "/api/recipes", getRecipes),
|
|
2667
|
+
route("POST", "/api/recipes/start", postRecipeStart),
|
|
2668
|
+
route("GET", "/api/recipes/instances", getRecipeInstances),
|
|
2669
|
+
route("GET", "/api/recipes/instances/:id", getRecipeInstanceById),
|
|
2670
|
+
route("POST", "/api/recipes/instances/:id/stop", postRecipeStop),
|
|
2671
|
+
route("GET", "/api/recipes/:name", getRecipeByName),
|
|
2672
|
+
route("GET", "/api/tokens", getTokens),
|
|
2673
|
+
route("POST", "/api/tokens", postToken),
|
|
2674
|
+
route("GET", "/api/tokens/:jti", getTokenById),
|
|
2675
|
+
route("POST", "/api/tokens/:jti/revoke", postTokenRevoke),
|
|
2676
|
+
route("POST", "/api/commands", postCommand),
|
|
2677
|
+
route("GET", "/api/commands", getCommands),
|
|
2678
|
+
route("GET", "/api/commands/:id", getCommandById),
|
|
2679
|
+
route("PATCH", "/api/commands/:id", patchCommand),
|
|
2680
|
+
route("DELETE", "/api/commands/:id", deleteCommandById),
|
|
2681
|
+
route("GET", "/api/providers/config", getProviderConfigsRoute),
|
|
2682
|
+
route("GET", "/api/providers/:provider/config", getProviderConfigRoute),
|
|
2683
|
+
route("PUT", "/api/providers/:provider/config", putProviderConfigRoute),
|
|
2684
|
+
route("POST", "/api/providers/:provider/config/test", postProviderConfigTestRoute),
|
|
2685
|
+
|
|
2190
2686
|
route("POST", "/api/messages", postMessage),
|
|
2191
2687
|
route("GET", "/api/messages", getMessages),
|
|
2192
2688
|
route("GET", "/api/messages/cursor", getCursorRoute),
|
|
@@ -2219,6 +2715,13 @@ const routes: Route[] = [
|
|
|
2219
2715
|
route("POST", "/api/channel-bindings", postChannelBinding),
|
|
2220
2716
|
route("GET", "/api/integrations", getIntegrations),
|
|
2221
2717
|
route("POST", "/api/integrations/events", postIntegrationEvent),
|
|
2718
|
+
route("GET", "/api/automations", getAutomations),
|
|
2719
|
+
route("POST", "/api/automations", postAutomation),
|
|
2720
|
+
route("GET", "/api/automation-runs", getAutomationRuns),
|
|
2721
|
+
route("GET", "/api/automations/:id", getAutomationById),
|
|
2722
|
+
route("PATCH", "/api/automations/:id", patchAutomation),
|
|
2723
|
+
route("DELETE", "/api/automations/:id", deleteAutomationById),
|
|
2724
|
+
route("POST", "/api/automations/:id/run", postAutomationRun),
|
|
2222
2725
|
route("GET", "/api/tasks", getTasks),
|
|
2223
2726
|
route("GET", "/api/tasks/:id", getTaskById),
|
|
2224
2727
|
route("GET", "/api/tasks/:id/events", getTaskEvents),
|