agent-relay-server 0.9.0 → 0.10.1
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 +1 -1
- package/src/connectors.ts +29 -9
- package/src/daemon.ts +1 -0
- package/src/db.ts +241 -34
- package/src/events.ts +33 -0
- package/src/index.ts +94 -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 +649 -155
- package/src/security.ts +128 -2
- package/src/sse.ts +42 -31
- package/src/token-db.ts +96 -0
- package/src/types.ts +1 -493
- 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,
|
|
@@ -63,6 +64,10 @@ import {
|
|
|
63
64
|
evaluatePoolBindings,
|
|
64
65
|
ValidationError,
|
|
65
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";
|
|
66
71
|
import {
|
|
67
72
|
getConnector,
|
|
68
73
|
listConnectors,
|
|
@@ -71,9 +76,21 @@ import {
|
|
|
71
76
|
runConnectorAction,
|
|
72
77
|
writeConnectorConfig,
|
|
73
78
|
} from "./connectors";
|
|
74
|
-
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";
|
|
75
90
|
import { getIntegrationTokens, INTEGRATION_RATE_LIMIT_PER_MINUTE, MAX_BODY_BYTES } from "./config";
|
|
76
|
-
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";
|
|
77
94
|
import {
|
|
78
95
|
getIntegrationAuth,
|
|
79
96
|
hasIntegrationScope,
|
|
@@ -93,6 +110,7 @@ import {
|
|
|
93
110
|
emitOrchestratorRemoved,
|
|
94
111
|
emitPoolBindingChanged,
|
|
95
112
|
} from "./sse";
|
|
113
|
+
import { emitRelayEvent } from "./events";
|
|
96
114
|
|
|
97
115
|
type Handler = (
|
|
98
116
|
req: Request,
|
|
@@ -175,16 +193,17 @@ function parseQueryInt(
|
|
|
175
193
|
return n;
|
|
176
194
|
}
|
|
177
195
|
|
|
178
|
-
const VALID_AGENT_STATUSES = ["online", "idle", "busy", "offline"] as const;
|
|
196
|
+
const VALID_AGENT_STATUSES = ["online", "idle", "busy", "stale", "offline"] as const;
|
|
179
197
|
const VALID_AGENT_KINDS = ["provider", "channel", "orchestrator", "system", "user"] as const;
|
|
180
198
|
const VALID_CHANNEL_BINDING_TARGET_TYPES = ["agent", "label", "tag", "capability", "broadcast", "orchestrator", "pool"] as const;
|
|
181
|
-
const VALID_CHANNEL_BINDING_MODES = ["exclusive", "
|
|
199
|
+
const VALID_CHANNEL_BINDING_MODES = ["exclusive", "broadcast"] as const;
|
|
182
200
|
const VALID_AGENT_ACTIONS = ["restart", "shutdown"] as const;
|
|
183
201
|
const VALID_AGENT_SPAWN_PROVIDERS = ["codex"] as const;
|
|
184
202
|
const VALID_CODEX_SPAWN_APPROVALS = ["open", "guarded", "read-only"] as const;
|
|
185
203
|
const VALID_CONNECTOR_ACTIONS = ["install", "uninstall", "enable", "disable", "start", "stop", "restart", "status", "doctor"] as const;
|
|
186
204
|
const VALID_TASK_SEVERITIES = ["info", "warning", "critical"] as const;
|
|
187
|
-
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;
|
|
188
207
|
const VALID_PAIR_STATUSES = ["pending", "active", "ended", "rejected", "expired"] as const;
|
|
189
208
|
const VALID_ACTIVITY_KINDS = ["message", "reply", "question", "operator", "pair", "task", "state"] as const;
|
|
190
209
|
const integrationRateBuckets = new Map<string, { windowStart: number; count: number }>();
|
|
@@ -232,6 +251,13 @@ function cleanMeta(value: unknown): Record<string, unknown> | undefined {
|
|
|
232
251
|
return value;
|
|
233
252
|
}
|
|
234
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
|
+
|
|
235
261
|
function cleanEnum<T extends readonly string[]>(
|
|
236
262
|
value: unknown,
|
|
237
263
|
field: string,
|
|
@@ -357,6 +383,11 @@ function normalizeMessageInput(body: unknown): SendMessageInput {
|
|
|
357
383
|
return input;
|
|
358
384
|
}
|
|
359
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
|
+
|
|
360
391
|
function applyReplyRouting(input: SendMessageInput): void {
|
|
361
392
|
if (input.to || !input.replyTo) return;
|
|
362
393
|
const parent = getMessage(input.replyTo);
|
|
@@ -557,6 +588,53 @@ function normalizeIntegrationEvent(body: unknown): IntegrationEventInput {
|
|
|
557
588
|
};
|
|
558
589
|
}
|
|
559
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
|
+
|
|
560
638
|
function normalizeTaskStatusInput(body: unknown): TaskStatusInput {
|
|
561
639
|
if (!isRecord(body)) throw new ValidationError("JSON object body required");
|
|
562
640
|
const status = cleanEnum(body.status, "status", VALID_TASK_STATUSES);
|
|
@@ -761,10 +839,11 @@ function agentToChannel(agent: AgentCard): ChannelSummary {
|
|
|
761
839
|
async function dispatchTaskCallbacks(taskId: number, eventType: string): Promise<void> {
|
|
762
840
|
const task = getTask(taskId);
|
|
763
841
|
if (!task) return;
|
|
842
|
+
const requestedTarget = typeof task.metadata?.relayRequestedTarget === "string" ? task.metadata.relayRequestedTarget : undefined;
|
|
764
843
|
const integrations = getIntegrationTokens()
|
|
765
844
|
.filter((integration) => integration.name === task.source)
|
|
766
845
|
.filter((integration) => integration.callbackUrl)
|
|
767
|
-
.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)))
|
|
768
847
|
.filter((integration) => !integration.channels?.length || !task.channel || integration.channels.includes(task.channel));
|
|
769
848
|
|
|
770
849
|
for (const integration of integrations) {
|
|
@@ -832,15 +911,6 @@ function auditAgentStateTransition(agentId: string, before: AgentCard | null | u
|
|
|
832
911
|
});
|
|
833
912
|
}
|
|
834
913
|
|
|
835
|
-
function orchestratorControlMeta(control: Record<string, unknown>): Record<string, unknown> {
|
|
836
|
-
return {
|
|
837
|
-
delivery: "interrupt",
|
|
838
|
-
priority: "urgent",
|
|
839
|
-
// Legacy orchestrators read control details from message meta.
|
|
840
|
-
orchestratorControl: control,
|
|
841
|
-
};
|
|
842
|
-
}
|
|
843
|
-
|
|
844
914
|
// --- Agent routes ---
|
|
845
915
|
|
|
846
916
|
const postAgent: Handler = async (req) => {
|
|
@@ -896,14 +966,14 @@ const patchAgentStatus: Handler = async (req, params) => {
|
|
|
896
966
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
897
967
|
const body = parsed.body;
|
|
898
968
|
if (!body?.status) return error("status required");
|
|
899
|
-
const valid =
|
|
900
|
-
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(", ")}`);
|
|
901
971
|
try {
|
|
902
972
|
const guard = normalizeAgentSessionGuard(req, body);
|
|
903
973
|
const session = validateAgentSession(params.id!, guard);
|
|
904
974
|
if (!session.ok) return error(session.error!, agentSessionStatus(session.error));
|
|
905
975
|
const before = getAgent(params.id!);
|
|
906
|
-
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);
|
|
907
977
|
auditAgentStateTransition(params.id!, before, getAgent(params.id!));
|
|
908
978
|
} catch (e) {
|
|
909
979
|
if (e instanceof ValidationError) return error(e.message, 400);
|
|
@@ -932,8 +1002,23 @@ const patchAgentLabel: Handler = async (req, params) => {
|
|
|
932
1002
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
933
1003
|
const body = parsed.body;
|
|
934
1004
|
if (body === null || !("label" in body)) return error("label field required (string or null)");
|
|
1005
|
+
const before = getAgent(params.id!);
|
|
935
1006
|
if (!setLabel(params.id!, body.label)) return error("agent not found", 404);
|
|
936
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
|
+
}
|
|
937
1022
|
return json({ ok: true });
|
|
938
1023
|
};
|
|
939
1024
|
|
|
@@ -943,9 +1028,23 @@ const patchAgentTags: Handler = async (req, params) => {
|
|
|
943
1028
|
try {
|
|
944
1029
|
const tags = isRecord(parsed.body) ? cleanStringArray(parsed.body.tags, "tags") : undefined;
|
|
945
1030
|
if (!tags) return error("tags field required");
|
|
1031
|
+
const before = getAgent(params.id!);
|
|
946
1032
|
const agent = setTags(params.id!, tags);
|
|
947
1033
|
if (!agent) return error("agent not found", 404);
|
|
948
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
|
+
}
|
|
949
1048
|
return json(agent);
|
|
950
1049
|
} catch (e) {
|
|
951
1050
|
if (e instanceof ValidationError) return error(e.message, 400);
|
|
@@ -987,7 +1086,8 @@ function agentCanReceiveControlAction(agent: AgentCard): boolean {
|
|
|
987
1086
|
return agent.id !== "user" &&
|
|
988
1087
|
agent.id !== "system" &&
|
|
989
1088
|
agent.meta?.kind !== "channel" &&
|
|
990
|
-
!agent.tags.includes("channel")
|
|
1089
|
+
!agent.tags.includes("channel") &&
|
|
1090
|
+
agent.meta?.runnerManaged === true;
|
|
991
1091
|
}
|
|
992
1092
|
|
|
993
1093
|
const postAgentAction: Handler = async (req, params) => {
|
|
@@ -1002,40 +1102,25 @@ const postAgentAction: Handler = async (req, params) => {
|
|
|
1002
1102
|
if (!agentCanReceiveControlAction(agent)) return error("agent does not support dashboard control actions", 400);
|
|
1003
1103
|
|
|
1004
1104
|
const title = action === "restart" ? "Agent restart requested" : "Agent shutdown requested";
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
body: action === "restart"
|
|
1011
|
-
? "Dashboard requested that this agent restart its relay-managed session now."
|
|
1012
|
-
: "Dashboard requested that this agent shut down its relay-managed session now.",
|
|
1013
|
-
payload: {
|
|
1014
|
-
agentControl: {
|
|
1015
|
-
action,
|
|
1016
|
-
requestedBy: "dashboard",
|
|
1017
|
-
requestedAt: Date.now(),
|
|
1018
|
-
},
|
|
1019
|
-
},
|
|
1020
|
-
meta: {
|
|
1021
|
-
delivery: "interrupt",
|
|
1022
|
-
priority: "urgent",
|
|
1023
|
-
},
|
|
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() },
|
|
1024
1110
|
});
|
|
1025
|
-
|
|
1111
|
+
emitCommand(command);
|
|
1026
1112
|
auditEvent({
|
|
1027
|
-
clientId: "server-agent-" + agent.id + "-action-" + action + "-" +
|
|
1113
|
+
clientId: "server-agent-" + agent.id + "-action-" + action + "-" + command.id,
|
|
1028
1114
|
kind: "state",
|
|
1029
1115
|
title,
|
|
1030
1116
|
body: action,
|
|
1031
1117
|
meta: agent.id,
|
|
1032
1118
|
icon: action === "restart" ? "ti-refresh" : "ti-power",
|
|
1033
1119
|
view: "agents",
|
|
1034
|
-
messageId: msg.id,
|
|
1035
1120
|
agentId: agent.id,
|
|
1036
|
-
metadata: { action },
|
|
1121
|
+
metadata: { action, commandId: command.id },
|
|
1037
1122
|
});
|
|
1038
|
-
return json({ ok: true, action,
|
|
1123
|
+
return json({ ok: true, action, command }, 202);
|
|
1039
1124
|
} catch (e) {
|
|
1040
1125
|
if (e instanceof ValidationError) return error(e.message, 400);
|
|
1041
1126
|
throw e;
|
|
@@ -1049,70 +1134,33 @@ const postAgentSpawn: Handler = async (req) => {
|
|
|
1049
1134
|
if (!isRecord(parsed.body)) return error("provider required");
|
|
1050
1135
|
const provider = cleanEnum(parsed.body.provider, "provider", [...VALID_AGENT_SPAWN_PROVIDERS, "claude"] as const);
|
|
1051
1136
|
if (!provider) return error("provider required");
|
|
1052
|
-
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;
|
|
1053
1138
|
const cwd = cleanString(parsed.body.cwd, "cwd", { max: 500 });
|
|
1054
1139
|
const label = cleanString(parsed.body.label, "label", { max: 120 });
|
|
1055
1140
|
|
|
1056
|
-
// Check for an online orchestrator that supports this provider
|
|
1057
1141
|
const orchestrators = listOrchestrators().filter(
|
|
1058
1142
|
(o) => o.status === "online" && o.providers.includes(provider as SpawnProvider),
|
|
1059
1143
|
);
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
kind: "control",
|
|
1068
|
-
subject: "Spawn agent",
|
|
1069
|
-
body: `Spawn ${provider} agent${label ? ` (${label})` : ""}`,
|
|
1070
|
-
payload: { orchestratorControl: control },
|
|
1071
|
-
meta: orchestratorControlMeta(control),
|
|
1072
|
-
});
|
|
1073
|
-
emitNewMessage(msg);
|
|
1074
|
-
auditEvent({
|
|
1075
|
-
clientId: "server-agent-spawn-" + provider + "-" + Date.now(),
|
|
1076
|
-
kind: "state",
|
|
1077
|
-
title: `${provider} agent spawn requested (via ${orch.id})`,
|
|
1078
|
-
body: cwd || orch.baseDir,
|
|
1079
|
-
meta: orch.id,
|
|
1080
|
-
icon: "ti-plus",
|
|
1081
|
-
view: "agents",
|
|
1082
|
-
metadata: { provider, orchestratorId: orch.id, approvalMode },
|
|
1083
|
-
});
|
|
1084
|
-
return json({ ok: true, orchestratorId: orch.id, provider, message: msg }, 202);
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
// Fallback: direct spawn for codex only (no orchestrator)
|
|
1088
|
-
if (provider !== "codex") return error("no orchestrator available for provider: " + provider);
|
|
1089
|
-
const relayUrl = process.env.AGENT_RELAY_SPAWN_RELAY_URL || process.env.AGENT_RELAY_URL || `http://127.0.0.1:${process.env.PORT || "4850"}`;
|
|
1090
|
-
const token = req.headers.get("X-Agent-Relay-Token") ?? req.headers.get("Authorization")?.replace(/^Bearer\s+/i, "");
|
|
1091
|
-
const result = spawnCodexAgent({
|
|
1092
|
-
cwd,
|
|
1093
|
-
approvalMode,
|
|
1094
|
-
label,
|
|
1095
|
-
relayUrl,
|
|
1096
|
-
token: token || undefined,
|
|
1097
|
-
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() },
|
|
1098
1151
|
});
|
|
1152
|
+
emitCommand(command);
|
|
1099
1153
|
auditEvent({
|
|
1100
|
-
clientId: "server-agent-spawn-
|
|
1154
|
+
clientId: "server-agent-spawn-" + provider + "-" + Date.now(),
|
|
1101
1155
|
kind: "state",
|
|
1102
|
-
title:
|
|
1103
|
-
body:
|
|
1104
|
-
meta:
|
|
1156
|
+
title: `${provider} agent spawn requested (via ${orch.id})`,
|
|
1157
|
+
body: cwd || orch.baseDir,
|
|
1158
|
+
meta: orch.id,
|
|
1105
1159
|
icon: "ti-plus",
|
|
1106
1160
|
view: "agents",
|
|
1107
|
-
metadata: {
|
|
1108
|
-
provider: result.provider,
|
|
1109
|
-
approvalMode: result.approvalMode,
|
|
1110
|
-
pid: result.pid ?? null,
|
|
1111
|
-
logPath: result.logPath,
|
|
1112
|
-
dryRun: result.dryRun === true,
|
|
1113
|
-
},
|
|
1161
|
+
metadata: { provider, orchestratorId: orch.id, approvalMode, commandId: command.id },
|
|
1114
1162
|
});
|
|
1115
|
-
return json(
|
|
1163
|
+
return json({ ok: true, orchestratorId: orch.id, provider, command }, 202);
|
|
1116
1164
|
} catch (e) {
|
|
1117
1165
|
if (e instanceof ValidationError) return error(e.message, 400);
|
|
1118
1166
|
if (e instanceof Error) return error(e.message, 400);
|
|
@@ -1132,11 +1180,137 @@ const getHostDirectories: Handler = (req) => {
|
|
|
1132
1180
|
}
|
|
1133
1181
|
};
|
|
1134
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
|
+
|
|
1135
1303
|
// --- Orchestrator routes ---
|
|
1136
1304
|
|
|
1137
1305
|
const VALID_ORCHESTRATOR_PROVIDERS = ["claude", "codex"] as const;
|
|
1138
1306
|
const VALID_SPAWN_APPROVALS = ["open", "guarded", "read-only"] as const;
|
|
1139
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
|
+
|
|
1140
1314
|
const postOrchestrator: Handler = async (req) => {
|
|
1141
1315
|
const parsed = await parseBody<unknown>(req);
|
|
1142
1316
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
@@ -1156,12 +1330,7 @@ const postOrchestrator: Handler = async (req) => {
|
|
|
1156
1330
|
const envKeys = cleanStringArray(parsed.body.envKeys, "envKeys");
|
|
1157
1331
|
const version = cleanString(parsed.body.version, "version", { max: 80 });
|
|
1158
1332
|
const gitSha = cleanString(parsed.body.gitSha, "gitSha", { max: 80 });
|
|
1159
|
-
const
|
|
1160
|
-
const protocolVersion = protocolVersionRaw === undefined
|
|
1161
|
-
? undefined
|
|
1162
|
-
: typeof protocolVersionRaw === "number" && Number.isInteger(protocolVersionRaw) && protocolVersionRaw > 0
|
|
1163
|
-
? protocolVersionRaw
|
|
1164
|
-
: (() => { throw new ValidationError("protocolVersion must be a positive integer"); })();
|
|
1333
|
+
const protocolVersion = cleanProtocolVersion(parsed.body.protocolVersion);
|
|
1165
1334
|
const meta = cleanMeta(parsed.body.meta);
|
|
1166
1335
|
const orch = upsertOrchestrator({ id, hostname, providers: providers ?? ["claude", "codex"], baseDir, envKeys, version, protocolVersion, gitSha, meta });
|
|
1167
1336
|
auditEvent({
|
|
@@ -1191,10 +1360,23 @@ const getOrchestratorById: Handler = (_req, params) => {
|
|
|
1191
1360
|
return json(orch);
|
|
1192
1361
|
};
|
|
1193
1362
|
|
|
1194
|
-
const postOrchestratorHeartbeat: Handler = (
|
|
1195
|
-
const
|
|
1196
|
-
if (!
|
|
1197
|
-
|
|
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
|
+
}
|
|
1198
1380
|
};
|
|
1199
1381
|
|
|
1200
1382
|
const patchOrchestratorAgents: Handler = async (req, params) => {
|
|
@@ -1249,40 +1431,33 @@ const postOrchestratorSpawn: Handler = async (req, params) => {
|
|
|
1249
1431
|
const approvalMode = cleanEnum(parsed.body.approvalMode, "approvalMode", VALID_SPAWN_APPROVALS, "guarded") as SpawnApprovalMode;
|
|
1250
1432
|
const prompt = cleanString(parsed.body.prompt, "prompt", { max: 4000 });
|
|
1251
1433
|
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
to: orch.agentId,
|
|
1266
|
-
kind: "control",
|
|
1267
|
-
subject: "Spawn agent",
|
|
1268
|
-
body: `Spawn ${provider} agent${label ? ` (${label})` : ""}`,
|
|
1269
|
-
payload: {
|
|
1270
|
-
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(),
|
|
1271
1447
|
},
|
|
1272
|
-
meta: orchestratorControlMeta(control),
|
|
1273
1448
|
});
|
|
1274
|
-
|
|
1449
|
+
emitCommand(command);
|
|
1275
1450
|
auditEvent({
|
|
1276
|
-
clientId: "server-orchestrator-spawn-" + orch.id + "-" +
|
|
1451
|
+
clientId: "server-orchestrator-spawn-" + orch.id + "-" + command.id,
|
|
1277
1452
|
kind: "state",
|
|
1278
1453
|
title: `Spawn ${provider} agent requested`,
|
|
1279
1454
|
body: cwd || orch.baseDir,
|
|
1280
1455
|
meta: orch.id,
|
|
1281
1456
|
icon: "ti-plus",
|
|
1282
1457
|
view: "orchestrators",
|
|
1283
|
-
metadata: { orchestratorId: orch.id, provider, approvalMode, label },
|
|
1458
|
+
metadata: { orchestratorId: orch.id, provider, approvalMode, label, commandId: command.id },
|
|
1284
1459
|
});
|
|
1285
|
-
return json({ ok: true, orchestratorId: orch.id,
|
|
1460
|
+
return json({ ok: true, orchestratorId: orch.id, command }, 202);
|
|
1286
1461
|
} catch (e) {
|
|
1287
1462
|
if (e instanceof ValidationError) return error(e.message, 400);
|
|
1288
1463
|
throw e;
|
|
@@ -1301,47 +1476,231 @@ const postOrchestratorAction: Handler = async (req, params) => {
|
|
|
1301
1476
|
if (!action) return error("action required");
|
|
1302
1477
|
const agentId = cleanString(parsed.body.agentId, "agentId", { max: 240 });
|
|
1303
1478
|
|
|
1304
|
-
const
|
|
1305
|
-
action,
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
requestedAt: Date.now(),
|
|
1309
|
-
};
|
|
1310
|
-
const msg = sendMessage({
|
|
1311
|
-
from: "system",
|
|
1312
|
-
to: orch.agentId,
|
|
1313
|
-
kind: "control",
|
|
1314
|
-
subject: action === "restart" ? "Restart agent" : "Shutdown agent",
|
|
1315
|
-
body: agentId ? `${action} ${agentId}` : `${action} all managed agents`,
|
|
1316
|
-
payload: {
|
|
1317
|
-
orchestratorControl: control,
|
|
1318
|
-
},
|
|
1319
|
-
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 },
|
|
1320
1484
|
});
|
|
1321
|
-
|
|
1485
|
+
emitCommand(command);
|
|
1322
1486
|
auditEvent({
|
|
1323
|
-
clientId: "server-orchestrator-action-" + orch.id + "-" + action + "-" +
|
|
1487
|
+
clientId: "server-orchestrator-action-" + orch.id + "-" + action + "-" + command.id,
|
|
1324
1488
|
kind: "state",
|
|
1325
1489
|
title: `Agent ${action} requested`,
|
|
1326
1490
|
body: agentId || "all",
|
|
1327
1491
|
meta: orch.id,
|
|
1328
1492
|
icon: action === "restart" ? "ti-refresh" : "ti-power",
|
|
1329
1493
|
view: "orchestrators",
|
|
1330
|
-
metadata: { orchestratorId: orch.id, action, agentId },
|
|
1494
|
+
metadata: { orchestratorId: orch.id, action, agentId, commandId: command.id },
|
|
1331
1495
|
});
|
|
1332
|
-
return json({ ok: true, action,
|
|
1496
|
+
return json({ ok: true, action, command }, 202);
|
|
1333
1497
|
} catch (e) {
|
|
1334
1498
|
if (e instanceof ValidationError) return error(e.message, 400);
|
|
1335
1499
|
throw e;
|
|
1336
1500
|
}
|
|
1337
1501
|
};
|
|
1338
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
|
+
|
|
1339
1520
|
const deleteOrchestratorById: Handler = (_req, params) => {
|
|
1340
1521
|
const deleted = deleteOrchestrator(params.id!);
|
|
1341
1522
|
if (!deleted) return error("orchestrator not found", 404);
|
|
1342
1523
|
return json({ ok: true });
|
|
1343
1524
|
};
|
|
1344
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
|
+
|
|
1345
1704
|
// --- Message routes ---
|
|
1346
1705
|
|
|
1347
1706
|
const VALID_MSG_KINDS = ["chat", "channel.event", "task", "pair", "control", "system"];
|
|
@@ -1356,6 +1715,13 @@ const postMessage: Handler = async (req) => {
|
|
|
1356
1715
|
}
|
|
1357
1716
|
applyReplyRouting(input);
|
|
1358
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
|
+
}
|
|
1359
1725
|
const result = sendMessageWithResult(input);
|
|
1360
1726
|
if (result.created) {
|
|
1361
1727
|
emitNewMessage(result.message);
|
|
@@ -1647,7 +2013,7 @@ const postConnectorAction: Handler = async (req, params) => {
|
|
|
1647
2013
|
try {
|
|
1648
2014
|
if (!isRecord(parsed.body)) throw new ValidationError("JSON object body required");
|
|
1649
2015
|
const action = cleanEnum(parsed.body.action, "action", VALID_CONNECTOR_ACTIONS)!;
|
|
1650
|
-
const result = runConnectorAction(params.id!, action);
|
|
2016
|
+
const result = await runConnectorAction(params.id!, action);
|
|
1651
2017
|
return json(result, result.ok ? 200 : 502);
|
|
1652
2018
|
} catch (e) {
|
|
1653
2019
|
if (e instanceof ValidationError) return error(e.message, e.message.includes("not found") ? 404 : 400);
|
|
@@ -1719,6 +2085,83 @@ const getIntegrations: Handler = () => {
|
|
|
1719
2085
|
return json(integrations);
|
|
1720
2086
|
};
|
|
1721
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
|
+
|
|
1722
2165
|
const getChannels: Handler = () => {
|
|
1723
2166
|
return json(listChannels());
|
|
1724
2167
|
};
|
|
@@ -1775,7 +2218,7 @@ const postChannelEvent: Handler = async (req, params) => {
|
|
|
1775
2218
|
body: input.body,
|
|
1776
2219
|
payload: input.payload,
|
|
1777
2220
|
idempotencyKey: scopedChannelIdempotencyKey(input.idempotencyKey, binding),
|
|
1778
|
-
claimable:
|
|
2221
|
+
claimable: false,
|
|
1779
2222
|
}));
|
|
1780
2223
|
for (const result of results) {
|
|
1781
2224
|
if (result.created) emitNewMessage(result.message);
|
|
@@ -1831,10 +2274,12 @@ const postIntegrationEvent: Handler = async (req) => {
|
|
|
1831
2274
|
const parsed = await parseBody<unknown>(req);
|
|
1832
2275
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
1833
2276
|
try {
|
|
1834
|
-
const
|
|
1835
|
-
|
|
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 })) {
|
|
1836
2280
|
return error("integration token cannot target this task", 403);
|
|
1837
2281
|
}
|
|
2282
|
+
const input = resolveIntegrationEventTarget(normalized);
|
|
1838
2283
|
const result = ingestIntegrationEvent(input, auth.name);
|
|
1839
2284
|
if (result.message) emitNewMessage(result.message);
|
|
1840
2285
|
emitTaskChanged(result.task, result.created ? "task.created" : "task.updated");
|
|
@@ -2124,11 +2569,30 @@ const getHealthRoute: Handler = () => json(getHealth());
|
|
|
2124
2569
|
|
|
2125
2570
|
const postSystemReap: Handler = () => {
|
|
2126
2571
|
const released = releaseExpiredClaims();
|
|
2572
|
+
const releasedOrphans = releaseOrphanedTasks();
|
|
2573
|
+
const expiredCommands = expireCommands();
|
|
2127
2574
|
const reapedAgentIds = reapStaleAgents();
|
|
2128
2575
|
const reapedOrchestratorIds = reapStaleOrchestrators();
|
|
2129
2576
|
for (const id of released.messageIds) emitMessageClaimReleased(id);
|
|
2130
2577
|
for (const task of released.tasks) emitTaskChanged(task, "task.updated");
|
|
2131
|
-
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
|
+
}
|
|
2132
2596
|
for (const id of reapedOrchestratorIds) emitOrchestratorStatus(id);
|
|
2133
2597
|
const poolChanges = evaluatePoolBindings();
|
|
2134
2598
|
for (const change of poolChanges) emitPoolBindingChanged(change.bindingId, change.channelId, change.previousAgentId, change.newAgentId);
|
|
@@ -2136,7 +2600,7 @@ const postSystemReap: Handler = () => {
|
|
|
2136
2600
|
clientId: "server-system-reap-" + Date.now(),
|
|
2137
2601
|
kind: "state",
|
|
2138
2602
|
title: "Maintenance reaper run",
|
|
2139
|
-
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)`,
|
|
2140
2604
|
icon: "ti-broom",
|
|
2141
2605
|
view: "activity",
|
|
2142
2606
|
});
|
|
@@ -2146,6 +2610,8 @@ const postSystemReap: Handler = () => {
|
|
|
2146
2610
|
reapedOrchestratorIds,
|
|
2147
2611
|
releasedMessageIds: released.messageIds,
|
|
2148
2612
|
releasedTaskIds: released.tasks.map((task) => task.id),
|
|
2613
|
+
releasedOrphanedTaskIds: releasedOrphans.map((task) => task.id),
|
|
2614
|
+
expiredCommandIds: expiredCommands.map((command) => command.id),
|
|
2149
2615
|
});
|
|
2150
2616
|
};
|
|
2151
2617
|
|
|
@@ -2193,9 +2659,30 @@ const routes: Route[] = [
|
|
|
2193
2659
|
route("PATCH", "/api/orchestrators/:id/agents", patchOrchestratorAgents),
|
|
2194
2660
|
route("POST", "/api/orchestrators/:id/spawn", postOrchestratorSpawn),
|
|
2195
2661
|
route("POST", "/api/orchestrators/:id/actions", postOrchestratorAction),
|
|
2662
|
+
route("GET", "/api/orchestrators/:id/directories", getOrchestratorDirectories),
|
|
2196
2663
|
route("DELETE", "/api/orchestrators/:id", deleteOrchestratorById),
|
|
2197
2664
|
|
|
2198
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
|
+
|
|
2199
2686
|
route("POST", "/api/messages", postMessage),
|
|
2200
2687
|
route("GET", "/api/messages", getMessages),
|
|
2201
2688
|
route("GET", "/api/messages/cursor", getCursorRoute),
|
|
@@ -2228,6 +2715,13 @@ const routes: Route[] = [
|
|
|
2228
2715
|
route("POST", "/api/channel-bindings", postChannelBinding),
|
|
2229
2716
|
route("GET", "/api/integrations", getIntegrations),
|
|
2230
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),
|
|
2231
2725
|
route("GET", "/api/tasks", getTasks),
|
|
2232
2726
|
route("GET", "/api/tasks/:id", getTaskById),
|
|
2233
2727
|
route("GET", "/api/tasks/:id/events", getTaskEvents),
|