apteva 0.4.17 → 0.4.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ActivityPage.yv28a2vj.js +3 -0
- package/dist/ApiDocsPage.4ccwjjbk.js +4 -0
- package/dist/App.155wke5v.js +4 -0
- package/dist/App.2e19nvn4.js +13 -0
- package/dist/App.2ye1b5n0.js +4 -0
- package/dist/App.4da4ycbe.js +4 -0
- package/dist/App.b6wtzd1j.js +4 -0
- package/dist/App.fjrh28tf.js +4 -0
- package/dist/App.htc36cy8.js +4 -0
- package/dist/App.me6reaa6.js +4 -0
- package/dist/App.n5q6p960.js +4 -0
- package/dist/App.nft7h9jt.js +4 -0
- package/dist/App.np463xvy.js +4 -0
- package/dist/App.nps62kvt.js +4 -0
- package/dist/App.q8ws33cc.js +181 -0
- package/dist/App.tb0y0jmt.js +40 -0
- package/dist/ConnectionsPage.52evzrp7.js +3 -0
- package/dist/McpPage.bjqrp0n2.js +3 -0
- package/dist/SettingsPage.es76hnj2.js +3 -0
- package/dist/SkillsPage.06h8yf0h.js +3 -0
- package/dist/TasksPage.99df66mk.js +3 -0
- package/dist/TelemetryPage.bmdnxhq7.js +3 -0
- package/dist/TestsPage.denxrg8c.js +3 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/auth/middleware.ts +2 -0
- package/src/db.ts +162 -11
- package/src/mcp-platform.ts +41 -1
- package/src/routes/api/agent-utils.ts +38 -2
- package/src/routes/api/agents.ts +65 -2
- package/src/routes/api/projects.ts +19 -2
- package/src/routes/api/system.ts +26 -12
- package/src/routes/api/triggers.ts +458 -0
- package/src/routes/api/webhooks.ts +171 -0
- package/src/routes/api.ts +4 -0
- package/src/routes/static.ts +12 -3
- package/src/server.ts +4 -2
- package/src/triggers/agentdojo.ts +248 -0
- package/src/triggers/composio.ts +264 -0
- package/src/triggers/index.ts +71 -0
- package/src/web/App.tsx +17 -10
- package/src/web/components/agents/AgentCard.tsx +14 -7
- package/src/web/components/agents/AgentPanel.tsx +105 -115
- package/src/web/components/common/Icons.tsx +8 -0
- package/src/web/components/common/index.ts +1 -0
- package/src/web/components/connections/ConnectionsPage.tsx +54 -0
- package/src/web/components/connections/IntegrationsTab.tsx +144 -0
- package/src/web/components/connections/OverviewTab.tsx +183 -0
- package/src/web/components/connections/TriggersTab.tsx +690 -0
- package/src/web/components/index.ts +1 -0
- package/src/web/components/layout/Sidebar.tsx +7 -1
- package/src/web/components/mcp/IntegrationsPanel.tsx +19 -3
- package/src/web/components/settings/SettingsPage.tsx +96 -2
- package/src/web/components/tasks/TasksPage.tsx +2 -2
- package/src/web/components/tests/TestsPage.tsx +1 -2
- package/src/web/hooks/useAgents.ts +15 -11
- package/src/web/types.ts +1 -1
- package/dist/App.fq4xbpcz.js +0 -228
package/src/server.ts
CHANGED
|
@@ -167,7 +167,9 @@ export const agentProcesses: Map<string, { proc: Subprocess; port: number }> = n
|
|
|
167
167
|
// Track agents currently being started (to prevent race conditions)
|
|
168
168
|
export const agentsStarting: Set<string> = new Set();
|
|
169
169
|
|
|
170
|
-
// Graceful shutdown handler - stop all
|
|
170
|
+
// Graceful shutdown handler - stop all agent processes when server exits
|
|
171
|
+
// NOTE: We intentionally do NOT update DB status here — agents marked "running"
|
|
172
|
+
// in the DB will be auto-restarted on next boot via findRunning() + resetAllStatus().
|
|
171
173
|
async function shutdownAllAgents() {
|
|
172
174
|
if (agentProcesses.size === 0) return;
|
|
173
175
|
|
|
@@ -182,7 +184,6 @@ async function shutdownAllAgents() {
|
|
|
182
184
|
}).catch(() => {});
|
|
183
185
|
|
|
184
186
|
proc.kill();
|
|
185
|
-
AgentDB.setStatus(agentId, "stopped");
|
|
186
187
|
} catch {
|
|
187
188
|
// Ignore errors during shutdown
|
|
188
189
|
}
|
|
@@ -192,6 +193,7 @@ async function shutdownAllAgents() {
|
|
|
192
193
|
|
|
193
194
|
// Handle process termination signals
|
|
194
195
|
let shuttingDown = false;
|
|
196
|
+
export function isShuttingDown(): boolean { return shuttingDown; }
|
|
195
197
|
process.on("SIGINT", async () => {
|
|
196
198
|
if (shuttingDown) return;
|
|
197
199
|
shuttingDown = true;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// AgentDojo Trigger Provider
|
|
2
|
+
// Uses our MCP API's subscription and trigger system
|
|
3
|
+
// Docs: POST /mcp/subscribe, GET /mcp/subscriptions, GET /mcp/triggers
|
|
4
|
+
|
|
5
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
6
|
+
import type { TriggerProvider, TriggerType, TriggerInstance } from "./index";
|
|
7
|
+
|
|
8
|
+
const AGENTDOJO_API_BASE = process.env.AGENTDOJO_API_BASE || "https://api.agentdojo.dev";
|
|
9
|
+
|
|
10
|
+
function headers(apiKey: string) {
|
|
11
|
+
return { "X-API-Key": apiKey, "Content-Type": "application/json" };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const AgentDojoTriggerProvider: TriggerProvider = {
|
|
15
|
+
id: "agentdojo",
|
|
16
|
+
name: "AgentDojo",
|
|
17
|
+
|
|
18
|
+
async listTriggerTypes(apiKey: string, toolkitSlugs?: string[]): Promise<TriggerType[]> {
|
|
19
|
+
const params = new URLSearchParams({ is_active: "true", limit: "200" });
|
|
20
|
+
if (toolkitSlugs?.length) {
|
|
21
|
+
// Filter by toolkit name(s) — API supports one at a time, so fetch each
|
|
22
|
+
const allItems: any[] = [];
|
|
23
|
+
for (const slug of toolkitSlugs) {
|
|
24
|
+
const res = await fetch(
|
|
25
|
+
`${AGENTDOJO_API_BASE}/mcp/triggers?${new URLSearchParams({ toolkit_name: slug, is_active: "true", limit: "200" })}`,
|
|
26
|
+
{ headers: headers(apiKey) },
|
|
27
|
+
);
|
|
28
|
+
if (res.ok) {
|
|
29
|
+
const data = await res.json();
|
|
30
|
+
const items = data.data || data.triggers || [];
|
|
31
|
+
allItems.push(...items);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return mapTriggerTypes(allItems);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const res = await fetch(`${AGENTDOJO_API_BASE}/mcp/triggers?${params}`, {
|
|
38
|
+
headers: headers(apiKey),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
console.error("AgentDojo listTriggerTypes error:", res.status, await res.text());
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const data = await res.json();
|
|
47
|
+
const items = data.data || data.triggers || [];
|
|
48
|
+
return mapTriggerTypes(items);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
async getTriggerType(apiKey: string, slug: string): Promise<TriggerType | null> {
|
|
52
|
+
const res = await fetch(
|
|
53
|
+
`${AGENTDOJO_API_BASE}/mcp/triggers/${encodeURIComponent(slug)}`,
|
|
54
|
+
{ headers: headers(apiKey) },
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
if (res.status === 404) return null;
|
|
59
|
+
console.error("AgentDojo getTriggerType error:", res.status, await res.text());
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const data = await res.json();
|
|
64
|
+
const item = data.data || data;
|
|
65
|
+
return {
|
|
66
|
+
slug: item.slug,
|
|
67
|
+
name: item.display_name || item.event_name || item.slug,
|
|
68
|
+
description: item.description || "",
|
|
69
|
+
type: (item.mechanism as "webhook" | "poll") || "webhook",
|
|
70
|
+
toolkit_slug: item.toolkit?.name || item.toolkit_name || "",
|
|
71
|
+
toolkit_name: item.toolkit?.display_name || item.toolkit_display_name || "",
|
|
72
|
+
logo: item.toolkit?.icon_url || null,
|
|
73
|
+
config_schema: item.config_schema || {},
|
|
74
|
+
payload_schema: item.payload_schema || {},
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
async createTrigger(
|
|
79
|
+
apiKey: string,
|
|
80
|
+
slug: string,
|
|
81
|
+
connectedAccountId: string,
|
|
82
|
+
config?: Record<string, unknown>,
|
|
83
|
+
): Promise<{ triggerId: string }> {
|
|
84
|
+
// AgentDojo uses subscriptions — we create a subscription for this trigger
|
|
85
|
+
// The callback_url should be set in config or from stored webhook URL
|
|
86
|
+
const callbackUrl = (config?.callback_url as string) || "";
|
|
87
|
+
if (!callbackUrl) {
|
|
88
|
+
throw new Error("callback_url is required in config for AgentDojo triggers");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const body: Record<string, unknown> = {
|
|
92
|
+
trigger_type_slug: slug,
|
|
93
|
+
credential_id: connectedAccountId,
|
|
94
|
+
callback_url: callbackUrl,
|
|
95
|
+
title: (config?.title as string) || `Trigger: ${slug}`,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (config?.events) {
|
|
99
|
+
body.events = config.events;
|
|
100
|
+
}
|
|
101
|
+
if (config?.server) {
|
|
102
|
+
body.server = config.server;
|
|
103
|
+
}
|
|
104
|
+
if (config?.prompt) {
|
|
105
|
+
body.prompt = config.prompt;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const res = await fetch(`${AGENTDOJO_API_BASE}/mcp/subscribe`, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: headers(apiKey),
|
|
111
|
+
body: JSON.stringify(body),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
const errText = await res.text();
|
|
116
|
+
console.error("AgentDojo createTrigger error:", res.status, errText);
|
|
117
|
+
throw new Error(`Failed to create AgentDojo subscription: ${errText}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const data = await res.json();
|
|
121
|
+
return { triggerId: String(data.subscription_id || data.id) };
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
async listTriggers(apiKey: string): Promise<TriggerInstance[]> {
|
|
125
|
+
const res = await fetch(`${AGENTDOJO_API_BASE}/mcp/subscriptions?status=active`, {
|
|
126
|
+
headers: headers(apiKey),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (!res.ok) {
|
|
130
|
+
console.error("AgentDojo listTriggers error:", res.status, await res.text());
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const data = await res.json();
|
|
135
|
+
const items = data.subscriptions || data.data || [];
|
|
136
|
+
|
|
137
|
+
return items.map((item: any) => ({
|
|
138
|
+
id: String(item.id || item.subscription_id),
|
|
139
|
+
trigger_slug: item.title || item.server || "",
|
|
140
|
+
connected_account_id: item.credential_id || null,
|
|
141
|
+
status: item.status === "active" ? "active" as const : "disabled" as const,
|
|
142
|
+
config: {
|
|
143
|
+
server: item.server,
|
|
144
|
+
events: item.events,
|
|
145
|
+
callback_url: item.callback_url,
|
|
146
|
+
prompt: item.prompt,
|
|
147
|
+
title: item.title,
|
|
148
|
+
},
|
|
149
|
+
created_at: item.created_at || "",
|
|
150
|
+
}));
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
async enableTrigger(apiKey: string, triggerId: string): Promise<boolean> {
|
|
154
|
+
const res = await fetch(`${AGENTDOJO_API_BASE}/mcp/subscription/update`, {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers: headers(apiKey),
|
|
157
|
+
body: JSON.stringify({ subscription_id: parseInt(triggerId), status: "active" }),
|
|
158
|
+
});
|
|
159
|
+
return res.ok;
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
async disableTrigger(apiKey: string, triggerId: string): Promise<boolean> {
|
|
163
|
+
const res = await fetch(`${AGENTDOJO_API_BASE}/mcp/subscription/update`, {
|
|
164
|
+
method: "POST",
|
|
165
|
+
headers: headers(apiKey),
|
|
166
|
+
body: JSON.stringify({ subscription_id: parseInt(triggerId), status: "disabled" }),
|
|
167
|
+
});
|
|
168
|
+
return res.ok;
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
async deleteTrigger(apiKey: string, triggerId: string): Promise<boolean> {
|
|
172
|
+
const res = await fetch(`${AGENTDOJO_API_BASE}/mcp/unsubscribe`, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: headers(apiKey),
|
|
175
|
+
body: JSON.stringify({ subscription_id: parseInt(triggerId) }),
|
|
176
|
+
});
|
|
177
|
+
return res.ok;
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
async setupWebhook(apiKey: string, webhookUrl: string): Promise<{ secret?: string }> {
|
|
181
|
+
// AgentDojo uses per-subscription callback URLs, not a global webhook.
|
|
182
|
+
// We just store the base URL locally — it will be passed when creating subscriptions.
|
|
183
|
+
// No remote API call needed.
|
|
184
|
+
return {};
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
async getWebhookConfig(apiKey: string): Promise<{ url: string | null; secret: string | null }> {
|
|
188
|
+
// AgentDojo doesn't have a global webhook config — each subscription has its own callback_url.
|
|
189
|
+
// Return null to indicate per-subscription mode.
|
|
190
|
+
return { url: null, secret: null };
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
verifyWebhook(req: Request, body: string, secret: string): boolean {
|
|
194
|
+
// AgentDojo forwards webhooks with an HMAC signature
|
|
195
|
+
const signature = req.headers.get("x-webhook-signature") || "";
|
|
196
|
+
if (!signature) {
|
|
197
|
+
// If no signature header, check for a simple shared token
|
|
198
|
+
const token = req.headers.get("x-webhook-token") || "";
|
|
199
|
+
return token === secret;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// HMAC-SHA256 verification: signature is "sha256=<hex>"
|
|
203
|
+
const sig = signature.startsWith("sha256=") ? signature.slice(7) : signature;
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const expected = createHmac("sha256", secret).update(body).digest("hex");
|
|
207
|
+
return timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"));
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
parseWebhookPayload(body: Record<string, unknown>): {
|
|
214
|
+
triggerSlug: string;
|
|
215
|
+
triggerInstanceId: string | null;
|
|
216
|
+
payload: Record<string, unknown>;
|
|
217
|
+
} {
|
|
218
|
+
// AgentDojo forwarded payload format:
|
|
219
|
+
// { type: "webhook", server: "stripe-payments", event: "payment_intent.succeeded",
|
|
220
|
+
// data: { ... }, prompt: "...", subscription_id: 123, timestamp: "..." }
|
|
221
|
+
|
|
222
|
+
const server = (body.server as string) || "";
|
|
223
|
+
const event = (body.event as string) || "";
|
|
224
|
+
const triggerSlug = event ? `${server}:${event}` : server || (body.type as string) || "unknown";
|
|
225
|
+
|
|
226
|
+
const triggerInstanceId = body.subscription_id
|
|
227
|
+
? String(body.subscription_id)
|
|
228
|
+
: null;
|
|
229
|
+
|
|
230
|
+
const payload = (body.data as Record<string, unknown>) || {};
|
|
231
|
+
|
|
232
|
+
return { triggerSlug, triggerInstanceId, payload };
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
function mapTriggerTypes(items: any[]): TriggerType[] {
|
|
237
|
+
return items.map((item: any) => ({
|
|
238
|
+
slug: item.slug,
|
|
239
|
+
name: item.display_name || item.event_name || item.slug,
|
|
240
|
+
description: item.description || "",
|
|
241
|
+
type: (item.mechanism as "webhook" | "poll") || "webhook",
|
|
242
|
+
toolkit_slug: item.toolkit?.name || item.toolkit_name || "",
|
|
243
|
+
toolkit_name: item.toolkit?.display_name || item.toolkit_display_name || "",
|
|
244
|
+
logo: item.toolkit?.icon_url || null,
|
|
245
|
+
config_schema: item.config_schema || {},
|
|
246
|
+
payload_schema: item.payload_schema || {},
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// Composio Trigger Provider
|
|
2
|
+
// https://docs.composio.dev/api-reference/triggers
|
|
3
|
+
|
|
4
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
5
|
+
import type { TriggerProvider, TriggerType, TriggerInstance } from "./index";
|
|
6
|
+
|
|
7
|
+
const COMPOSIO_API_BASE = "https://backend.composio.dev";
|
|
8
|
+
|
|
9
|
+
function headers(apiKey: string) {
|
|
10
|
+
return { "x-api-key": apiKey, "Content-Type": "application/json" };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ComposioTriggerProvider: TriggerProvider = {
|
|
14
|
+
id: "composio",
|
|
15
|
+
name: "Composio",
|
|
16
|
+
|
|
17
|
+
async listTriggerTypes(apiKey: string, toolkitSlugs?: string[]): Promise<TriggerType[]> {
|
|
18
|
+
const params = new URLSearchParams();
|
|
19
|
+
if (toolkitSlugs?.length) {
|
|
20
|
+
params.set("toolkit_slugs", toolkitSlugs.join(","));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const res = await fetch(`${COMPOSIO_API_BASE}/api/v3/triggers_types?${params}`, {
|
|
24
|
+
headers: headers(apiKey),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
console.error("Composio listTriggerTypes error:", res.status, await res.text());
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const data = await res.json();
|
|
33
|
+
const items = data.items || data || [];
|
|
34
|
+
|
|
35
|
+
return items.map((item: any) => ({
|
|
36
|
+
slug: item.slug || item.enum,
|
|
37
|
+
name: item.name || item.slug,
|
|
38
|
+
description: item.description || "",
|
|
39
|
+
type: item.type || "webhook",
|
|
40
|
+
toolkit_slug: item.toolkit?.slug || item.app_slug || "",
|
|
41
|
+
toolkit_name: item.toolkit?.name || item.app_name || "",
|
|
42
|
+
logo: item.toolkit?.logo || item.logo || null,
|
|
43
|
+
config_schema: item.config || {},
|
|
44
|
+
payload_schema: item.payload || {},
|
|
45
|
+
}));
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async getTriggerType(apiKey: string, slug: string): Promise<TriggerType | null> {
|
|
49
|
+
const res = await fetch(`${COMPOSIO_API_BASE}/api/v3/triggers_types/${encodeURIComponent(slug)}`, {
|
|
50
|
+
headers: headers(apiKey),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
if (res.status === 404) return null;
|
|
55
|
+
console.error("Composio getTriggerType error:", res.status, await res.text());
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const item = await res.json();
|
|
60
|
+
return {
|
|
61
|
+
slug: item.slug || item.enum,
|
|
62
|
+
name: item.name || item.slug,
|
|
63
|
+
description: item.description || "",
|
|
64
|
+
type: item.type || "webhook",
|
|
65
|
+
toolkit_slug: item.toolkit?.slug || item.app_slug || "",
|
|
66
|
+
toolkit_name: item.toolkit?.name || item.app_name || "",
|
|
67
|
+
logo: item.toolkit?.logo || item.logo || null,
|
|
68
|
+
config_schema: item.config || {},
|
|
69
|
+
payload_schema: item.payload || {},
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async createTrigger(
|
|
74
|
+
apiKey: string,
|
|
75
|
+
slug: string,
|
|
76
|
+
connectedAccountId: string,
|
|
77
|
+
config?: Record<string, unknown>,
|
|
78
|
+
): Promise<{ triggerId: string }> {
|
|
79
|
+
const body: any = {
|
|
80
|
+
connected_account_id: connectedAccountId,
|
|
81
|
+
};
|
|
82
|
+
if (config) {
|
|
83
|
+
body.trigger_config = config;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const res = await fetch(
|
|
87
|
+
`${COMPOSIO_API_BASE}/api/v3/trigger_instances/${encodeURIComponent(slug)}/upsert`,
|
|
88
|
+
{
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: headers(apiKey),
|
|
91
|
+
body: JSON.stringify(body),
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
const errText = await res.text();
|
|
97
|
+
console.error("Composio createTrigger error:", res.status, errText);
|
|
98
|
+
throw new Error(`Failed to create trigger: ${errText}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const data = await res.json();
|
|
102
|
+
return { triggerId: data.trigger_id || data.deprecated?.uuid || data.id };
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
async listTriggers(apiKey: string): Promise<TriggerInstance[]> {
|
|
106
|
+
const res = await fetch(`${COMPOSIO_API_BASE}/api/v3/trigger_instances/active`, {
|
|
107
|
+
headers: headers(apiKey),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
console.error("Composio listTriggers error:", res.status, await res.text());
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const data = await res.json();
|
|
116
|
+
const items = data.items || data.triggers || data || [];
|
|
117
|
+
|
|
118
|
+
return items.map((item: any) => ({
|
|
119
|
+
id: item.id || item.trigger_id,
|
|
120
|
+
trigger_slug: item.trigger_name || item.trigger_slug || item.slug || "",
|
|
121
|
+
connected_account_id: item.connected_account_id || item.connectedAccountId || null,
|
|
122
|
+
status: item.disabled ? "disabled" : "active",
|
|
123
|
+
config: item.trigger_config || item.triggerConfig || {},
|
|
124
|
+
created_at: item.created_at || item.createdAt || "",
|
|
125
|
+
}));
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
async enableTrigger(apiKey: string, triggerId: string): Promise<boolean> {
|
|
129
|
+
const res = await fetch(
|
|
130
|
+
`${COMPOSIO_API_BASE}/api/v3/trigger_instances/manage/${encodeURIComponent(triggerId)}`,
|
|
131
|
+
{
|
|
132
|
+
method: "PATCH",
|
|
133
|
+
headers: headers(apiKey),
|
|
134
|
+
body: JSON.stringify({ status: "enable" }),
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
return res.ok;
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
async disableTrigger(apiKey: string, triggerId: string): Promise<boolean> {
|
|
141
|
+
const res = await fetch(
|
|
142
|
+
`${COMPOSIO_API_BASE}/api/v3/trigger_instances/manage/${encodeURIComponent(triggerId)}`,
|
|
143
|
+
{
|
|
144
|
+
method: "PATCH",
|
|
145
|
+
headers: headers(apiKey),
|
|
146
|
+
body: JSON.stringify({ status: "disable" }),
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
return res.ok;
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
async deleteTrigger(apiKey: string, triggerId: string): Promise<boolean> {
|
|
153
|
+
const res = await fetch(
|
|
154
|
+
`${COMPOSIO_API_BASE}/api/v3/trigger_instances/manage/${encodeURIComponent(triggerId)}`,
|
|
155
|
+
{
|
|
156
|
+
method: "DELETE",
|
|
157
|
+
headers: headers(apiKey),
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
return res.ok;
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
async setupWebhook(apiKey: string, webhookUrl: string): Promise<{ secret?: string }> {
|
|
164
|
+
// Use webhook_subscriptions API (requires HTTPS)
|
|
165
|
+
const res = await fetch(`${COMPOSIO_API_BASE}/api/v3/webhook_subscriptions`, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: headers(apiKey),
|
|
168
|
+
body: JSON.stringify({
|
|
169
|
+
webhook_url: webhookUrl,
|
|
170
|
+
enabled_events: ["composio.trigger.message"],
|
|
171
|
+
version: "V3",
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (!res.ok) {
|
|
176
|
+
const errText = await res.text();
|
|
177
|
+
console.error("Composio setupWebhook error:", res.status, errText);
|
|
178
|
+
throw new Error(`Failed to set webhook URL: ${errText}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const data = await res.json();
|
|
182
|
+
const secret = data.secret || data.webhook_secret || undefined;
|
|
183
|
+
return { secret };
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
async getWebhookConfig(apiKey: string): Promise<{ url: string | null; secret: string | null }> {
|
|
187
|
+
const res = await fetch(`${COMPOSIO_API_BASE}/api/v3/webhook_subscriptions`, {
|
|
188
|
+
headers: headers(apiKey),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (!res.ok) {
|
|
192
|
+
console.error("Composio getWebhookConfig error:", res.status, await res.text());
|
|
193
|
+
return { url: null, secret: null };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const data = await res.json();
|
|
197
|
+
const items = data.items || data.subscriptions || (Array.isArray(data) ? data : [data]);
|
|
198
|
+
const sub = items[0];
|
|
199
|
+
return {
|
|
200
|
+
url: sub?.webhook_url || sub?.callback_url || sub?.url || null,
|
|
201
|
+
secret: sub?.secret || sub?.webhook_secret || null,
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
verifyWebhook(req: Request, body: string, secret: string): boolean {
|
|
206
|
+
const signature = req.headers.get("webhook-signature") || "";
|
|
207
|
+
const webhookId = req.headers.get("webhook-id") || "";
|
|
208
|
+
const timestamp = req.headers.get("webhook-timestamp") || "";
|
|
209
|
+
|
|
210
|
+
if (!signature || !webhookId || !timestamp) return false;
|
|
211
|
+
|
|
212
|
+
// Check timestamp freshness (5-minute window to prevent replay)
|
|
213
|
+
const now = Math.floor(Date.now() / 1000);
|
|
214
|
+
const ts = parseInt(timestamp, 10);
|
|
215
|
+
if (isNaN(ts) || Math.abs(now - ts) > 300) return false;
|
|
216
|
+
|
|
217
|
+
// Composio signature: HMAC-SHA256("{webhook-id}.{timestamp}.{body}", secret)
|
|
218
|
+
const signedContent = `${webhookId}.${timestamp}.${body}`;
|
|
219
|
+
const expectedSignature = createHmac("sha256", secret)
|
|
220
|
+
.update(signedContent)
|
|
221
|
+
.digest("base64");
|
|
222
|
+
|
|
223
|
+
// Signature header format: "v1,<base64>"
|
|
224
|
+
const sig = signature.startsWith("v1,") ? signature.slice(3) : signature;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
return timingSafeEqual(
|
|
228
|
+
Buffer.from(sig, "base64"),
|
|
229
|
+
Buffer.from(expectedSignature, "base64"),
|
|
230
|
+
);
|
|
231
|
+
} catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
parseWebhookPayload(body: Record<string, unknown>): {
|
|
237
|
+
triggerSlug: string;
|
|
238
|
+
triggerInstanceId: string | null;
|
|
239
|
+
payload: Record<string, unknown>;
|
|
240
|
+
} {
|
|
241
|
+
// V3 format: { type: "composio.trigger.message", metadata: { trigger_slug, trigger_id, ... }, data: { ... } }
|
|
242
|
+
// V2/V1 format: { trigger_name, trigger_id, payload: { ... } }
|
|
243
|
+
const metadata = (body.metadata as Record<string, unknown>) || {};
|
|
244
|
+
const data = (body.data as Record<string, unknown>) || {};
|
|
245
|
+
|
|
246
|
+
const triggerSlug =
|
|
247
|
+
(metadata.trigger_slug as string) ||
|
|
248
|
+
(body.trigger_name as string) ||
|
|
249
|
+
((body.type as string) !== "composio.trigger.message" ? (body.type as string) : null) ||
|
|
250
|
+
"unknown";
|
|
251
|
+
|
|
252
|
+
const triggerInstanceId =
|
|
253
|
+
(metadata.trigger_id as string) ||
|
|
254
|
+
(body.trigger_id as string) ||
|
|
255
|
+
(body.triggerId as string) ||
|
|
256
|
+
null;
|
|
257
|
+
|
|
258
|
+
const payload =
|
|
259
|
+
(body.payload as Record<string, unknown>) ||
|
|
260
|
+
data;
|
|
261
|
+
|
|
262
|
+
return { triggerSlug, triggerInstanceId, payload };
|
|
263
|
+
},
|
|
264
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Generic Trigger Provider Interface
|
|
2
|
+
// Allows multiple providers (Composio, AgentDojo, local, etc.) to offer trigger/webhook integrations
|
|
3
|
+
|
|
4
|
+
export interface TriggerType {
|
|
5
|
+
slug: string;
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
type: "webhook" | "poll";
|
|
9
|
+
toolkit_slug: string;
|
|
10
|
+
toolkit_name: string;
|
|
11
|
+
logo: string | null;
|
|
12
|
+
config_schema: Record<string, unknown>;
|
|
13
|
+
payload_schema: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TriggerInstance {
|
|
17
|
+
id: string;
|
|
18
|
+
trigger_slug: string;
|
|
19
|
+
connected_account_id: string | null;
|
|
20
|
+
status: "active" | "disabled";
|
|
21
|
+
config: Record<string, unknown>;
|
|
22
|
+
created_at: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TriggerProvider {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
|
|
29
|
+
// Browse available trigger types
|
|
30
|
+
listTriggerTypes(apiKey: string, toolkitSlugs?: string[]): Promise<TriggerType[]>;
|
|
31
|
+
getTriggerType(apiKey: string, slug: string): Promise<TriggerType | null>;
|
|
32
|
+
|
|
33
|
+
// CRUD trigger instances (all remote)
|
|
34
|
+
createTrigger(
|
|
35
|
+
apiKey: string,
|
|
36
|
+
slug: string,
|
|
37
|
+
connectedAccountId: string,
|
|
38
|
+
config?: Record<string, unknown>,
|
|
39
|
+
): Promise<{ triggerId: string }>;
|
|
40
|
+
listTriggers(apiKey: string): Promise<TriggerInstance[]>;
|
|
41
|
+
enableTrigger(apiKey: string, triggerId: string): Promise<boolean>;
|
|
42
|
+
disableTrigger(apiKey: string, triggerId: string): Promise<boolean>;
|
|
43
|
+
deleteTrigger(apiKey: string, triggerId: string): Promise<boolean>;
|
|
44
|
+
|
|
45
|
+
// Webhook configuration
|
|
46
|
+
setupWebhook(apiKey: string, webhookUrl: string): Promise<{ secret?: string }>;
|
|
47
|
+
getWebhookConfig(apiKey: string): Promise<{ url: string | null; secret: string | null }>;
|
|
48
|
+
|
|
49
|
+
// Webhook verification and parsing (each provider signs differently)
|
|
50
|
+
verifyWebhook(req: Request, body: string, secret: string): boolean;
|
|
51
|
+
parseWebhookPayload(body: Record<string, unknown>): {
|
|
52
|
+
triggerSlug: string;
|
|
53
|
+
triggerInstanceId: string | null;
|
|
54
|
+
payload: Record<string, unknown>;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Provider registry
|
|
59
|
+
const triggerProviders: Map<string, TriggerProvider> = new Map();
|
|
60
|
+
|
|
61
|
+
export function registerTriggerProvider(provider: TriggerProvider) {
|
|
62
|
+
triggerProviders.set(provider.id, provider);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getTriggerProvider(id: string): TriggerProvider | undefined {
|
|
66
|
+
return triggerProviders.get(id);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getTriggerProviderIds(): string[] {
|
|
70
|
+
return Array.from(triggerProviders.keys());
|
|
71
|
+
}
|
package/src/web/App.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect, useMemo } from "react";
|
|
1
|
+
import React, { useState, useEffect, useMemo, lazy, Suspense } from "react";
|
|
2
2
|
import { createRoot } from "react-dom/client";
|
|
3
3
|
import "@apteva/apteva-kit/styles.css";
|
|
4
4
|
|
|
@@ -12,28 +12,31 @@ import { TelemetryProvider, AuthProvider, ProjectProvider, useAuth, useProjects,
|
|
|
12
12
|
// Hooks
|
|
13
13
|
import { useAgents, useProviders, useOnboarding } from "./hooks";
|
|
14
14
|
|
|
15
|
-
//
|
|
15
|
+
// Core components (always needed)
|
|
16
16
|
import {
|
|
17
17
|
LoadingSpinner,
|
|
18
18
|
Header,
|
|
19
19
|
Sidebar,
|
|
20
20
|
ErrorBanner,
|
|
21
21
|
OnboardingWizard,
|
|
22
|
-
SettingsPage,
|
|
23
22
|
CreateAgentModal,
|
|
24
23
|
AgentsView,
|
|
25
24
|
Dashboard,
|
|
26
|
-
ActivityPage,
|
|
27
|
-
TasksPage,
|
|
28
|
-
McpPage,
|
|
29
|
-
SkillsPage,
|
|
30
|
-
TestsPage,
|
|
31
|
-
TelemetryPage,
|
|
32
25
|
LoginPage,
|
|
33
26
|
} from "./components";
|
|
34
|
-
import { ApiDocsPage } from "./components/api/ApiDocsPage";
|
|
35
27
|
import { MetaAgentProvider, MetaAgentPanel } from "./components/meta-agent/MetaAgent";
|
|
36
28
|
|
|
29
|
+
// Lazy-loaded page components (only loaded when navigated to)
|
|
30
|
+
const SettingsPage = lazy(() => import("./components/settings/SettingsPage").then(m => ({ default: m.SettingsPage })));
|
|
31
|
+
const ActivityPage = lazy(() => import("./components/activity/ActivityPage").then(m => ({ default: m.ActivityPage })));
|
|
32
|
+
const TasksPage = lazy(() => import("./components/tasks/TasksPage").then(m => ({ default: m.TasksPage })));
|
|
33
|
+
const McpPage = lazy(() => import("./components/mcp/McpPage").then(m => ({ default: m.McpPage })));
|
|
34
|
+
const SkillsPage = lazy(() => import("./components/skills/SkillsPage").then(m => ({ default: m.SkillsPage })));
|
|
35
|
+
const TestsPage = lazy(() => import("./components/tests/TestsPage").then(m => ({ default: m.TestsPage })));
|
|
36
|
+
const TelemetryPage = lazy(() => import("./components/telemetry/TelemetryPage").then(m => ({ default: m.TelemetryPage })));
|
|
37
|
+
const ConnectionsPage = lazy(() => import("./components/connections/ConnectionsPage").then(m => ({ default: m.ConnectionsPage })));
|
|
38
|
+
const ApiDocsPage = lazy(() => import("./components/api/ApiDocsPage").then(m => ({ default: m.ApiDocsPage })));
|
|
39
|
+
|
|
37
40
|
function AppContent() {
|
|
38
41
|
// Auth state
|
|
39
42
|
const { isAuthenticated, isLoading: authLoading, hasUsers, accessToken, checkAuth } = useAuth();
|
|
@@ -254,6 +257,7 @@ function AppContent() {
|
|
|
254
257
|
/>
|
|
255
258
|
|
|
256
259
|
<main className="flex-1 overflow-hidden flex">
|
|
260
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
257
261
|
{route === "settings" && <SettingsPage />}
|
|
258
262
|
|
|
259
263
|
{route === "activity" && (
|
|
@@ -292,6 +296,8 @@ function AppContent() {
|
|
|
292
296
|
|
|
293
297
|
{route === "tasks" && <TasksPage />}
|
|
294
298
|
|
|
299
|
+
{route === "connections" && <ConnectionsPage />}
|
|
300
|
+
|
|
295
301
|
{route === "mcp" && <McpPage />}
|
|
296
302
|
|
|
297
303
|
{route === "skills" && <SkillsPage />}
|
|
@@ -301,6 +307,7 @@ function AppContent() {
|
|
|
301
307
|
{route === "telemetry" && <TelemetryPage />}
|
|
302
308
|
|
|
303
309
|
{route === "api" && <ApiDocsPage />}
|
|
310
|
+
</Suspense>
|
|
304
311
|
</main>
|
|
305
312
|
</div>
|
|
306
313
|
|