apteva 0.4.12 → 0.4.15
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/App.jdzxkzm1.js +228 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/auth/middleware.ts +42 -26
- package/src/crypto.ts +2 -2
- package/src/db-tests.ts +174 -0
- package/src/db.ts +302 -5
- package/src/integrations/agentdojo.ts +168 -42
- package/src/mcp-client.ts +15 -9
- package/src/mcp-platform.ts +160 -0
- package/src/openapi.ts +416 -21
- package/src/routes/api/agent-utils.ts +2 -2
- package/src/routes/api/api-keys.ts +95 -0
- package/src/routes/api/integrations.ts +1 -1
- package/src/routes/api/mcp.ts +2 -2
- package/src/routes/api/system.ts +10 -1
- package/src/routes/api/tests.ts +148 -0
- package/src/routes/api.ts +4 -0
- package/src/server.ts +2 -1
- package/src/test-runner.ts +598 -0
- package/src/web/App.tsx +23 -10
- package/src/web/components/agents/AgentPanel.tsx +4 -8
- package/src/web/components/common/Icons.tsx +8 -0
- package/src/web/components/dashboard/Dashboard.tsx +2 -4
- package/src/web/components/index.ts +1 -0
- package/src/web/components/layout/Sidebar.tsx +7 -1
- package/src/web/components/settings/SettingsPage.tsx +288 -5
- package/src/web/components/skills/SkillsPage.tsx +1 -1
- package/src/web/components/tasks/TasksPage.tsx +8 -3
- package/src/web/components/telemetry/TelemetryPage.tsx +2 -5
- package/src/web/components/tests/TestsPage.tsx +580 -0
- package/src/web/context/index.ts +1 -1
- package/src/web/types.ts +1 -1
- package/dist/App.9ph8javh.js +0 -228
package/src/db.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Database } from "bun:sqlite";
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { mkdirSync, existsSync } from "fs";
|
|
4
4
|
import { encrypt, decrypt, encryptObject, decryptObject } from "./crypto";
|
|
5
|
-
import { randomBytes } from "crypto";
|
|
5
|
+
import { randomBytes, createHash } from "crypto";
|
|
6
6
|
|
|
7
7
|
// Types
|
|
8
8
|
export type AgentMode = "coordinator" | "worker";
|
|
@@ -520,6 +520,146 @@ function runMigrations() {
|
|
|
520
520
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_provider_keys_unique ON provider_keys(provider_id, COALESCE(project_id, ''));
|
|
521
521
|
`,
|
|
522
522
|
},
|
|
523
|
+
{
|
|
524
|
+
name: "023_create_test_cases",
|
|
525
|
+
sql: `
|
|
526
|
+
CREATE TABLE IF NOT EXISTS test_cases (
|
|
527
|
+
id TEXT PRIMARY KEY,
|
|
528
|
+
name TEXT NOT NULL,
|
|
529
|
+
description TEXT,
|
|
530
|
+
agent_id TEXT NOT NULL,
|
|
531
|
+
input_message TEXT NOT NULL,
|
|
532
|
+
eval_criteria TEXT NOT NULL,
|
|
533
|
+
timeout_ms INTEGER DEFAULT 60000,
|
|
534
|
+
project_id TEXT,
|
|
535
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
536
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
537
|
+
);
|
|
538
|
+
CREATE INDEX IF NOT EXISTS idx_test_cases_agent ON test_cases(agent_id);
|
|
539
|
+
CREATE INDEX IF NOT EXISTS idx_test_cases_project ON test_cases(project_id);
|
|
540
|
+
`,
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
name: "025_create_api_keys",
|
|
544
|
+
sql: `
|
|
545
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
546
|
+
id TEXT PRIMARY KEY,
|
|
547
|
+
name TEXT NOT NULL,
|
|
548
|
+
key_hash TEXT NOT NULL UNIQUE,
|
|
549
|
+
key_prefix TEXT NOT NULL,
|
|
550
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
551
|
+
expires_at TEXT,
|
|
552
|
+
last_used_at TEXT,
|
|
553
|
+
is_active INTEGER DEFAULT 1,
|
|
554
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
555
|
+
);
|
|
556
|
+
CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash) WHERE is_active = 1;
|
|
557
|
+
CREATE INDEX IF NOT EXISTS idx_api_keys_user ON api_keys(user_id);
|
|
558
|
+
`,
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
name: "026_behavior_tests",
|
|
562
|
+
sql: `
|
|
563
|
+
-- Recreate test_cases with nullable agent_id and input_message
|
|
564
|
+
CREATE TABLE IF NOT EXISTS test_cases_new (
|
|
565
|
+
id TEXT PRIMARY KEY,
|
|
566
|
+
name TEXT NOT NULL,
|
|
567
|
+
description TEXT,
|
|
568
|
+
behavior TEXT,
|
|
569
|
+
agent_id TEXT,
|
|
570
|
+
input_message TEXT,
|
|
571
|
+
eval_criteria TEXT NOT NULL DEFAULT '',
|
|
572
|
+
timeout_ms INTEGER DEFAULT 300000,
|
|
573
|
+
project_id TEXT,
|
|
574
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
575
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
576
|
+
);
|
|
577
|
+
INSERT OR IGNORE INTO test_cases_new (id, name, description, agent_id, input_message, eval_criteria, timeout_ms, project_id, created_at, updated_at)
|
|
578
|
+
SELECT id, name, description, agent_id, input_message, eval_criteria, timeout_ms, project_id, created_at, updated_at FROM test_cases;
|
|
579
|
+
DROP TABLE IF EXISTS test_cases;
|
|
580
|
+
ALTER TABLE test_cases_new RENAME TO test_cases;
|
|
581
|
+
CREATE INDEX IF NOT EXISTS idx_test_cases_agent ON test_cases(agent_id);
|
|
582
|
+
CREATE INDEX IF NOT EXISTS idx_test_cases_project ON test_cases(project_id);
|
|
583
|
+
|
|
584
|
+
-- Add planner columns to test_runs
|
|
585
|
+
ALTER TABLE test_runs ADD COLUMN generated_message TEXT;
|
|
586
|
+
ALTER TABLE test_runs ADD COLUMN selected_agent_id TEXT;
|
|
587
|
+
ALTER TABLE test_runs ADD COLUMN selected_agent_name TEXT;
|
|
588
|
+
ALTER TABLE test_runs ADD COLUMN planner_reasoning TEXT;
|
|
589
|
+
`,
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
name: "027_fix_test_cases_nullable",
|
|
593
|
+
sql: `
|
|
594
|
+
-- Recreate test_cases with nullable agent_id and input_message
|
|
595
|
+
CREATE TABLE IF NOT EXISTS test_cases_new (
|
|
596
|
+
id TEXT PRIMARY KEY,
|
|
597
|
+
name TEXT NOT NULL,
|
|
598
|
+
description TEXT,
|
|
599
|
+
behavior TEXT,
|
|
600
|
+
agent_id TEXT,
|
|
601
|
+
input_message TEXT,
|
|
602
|
+
eval_criteria TEXT NOT NULL DEFAULT '',
|
|
603
|
+
timeout_ms INTEGER DEFAULT 300000,
|
|
604
|
+
project_id TEXT,
|
|
605
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
606
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
607
|
+
);
|
|
608
|
+
INSERT OR IGNORE INTO test_cases_new (id, name, description, behavior, agent_id, input_message, eval_criteria, timeout_ms, project_id, created_at, updated_at)
|
|
609
|
+
SELECT id, name, description, behavior, agent_id, input_message, eval_criteria, timeout_ms, project_id, created_at, updated_at FROM test_cases;
|
|
610
|
+
DROP TABLE IF EXISTS test_cases;
|
|
611
|
+
ALTER TABLE test_cases_new RENAME TO test_cases;
|
|
612
|
+
CREATE INDEX IF NOT EXISTS idx_test_cases_agent ON test_cases(agent_id);
|
|
613
|
+
CREATE INDEX IF NOT EXISTS idx_test_cases_project ON test_cases(project_id);
|
|
614
|
+
`,
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
name: "028_add_test_run_score",
|
|
618
|
+
sql: `ALTER TABLE test_runs ADD COLUMN score INTEGER;`,
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
name: "024_create_test_runs",
|
|
622
|
+
sql: `
|
|
623
|
+
CREATE TABLE IF NOT EXISTS test_runs (
|
|
624
|
+
id TEXT PRIMARY KEY,
|
|
625
|
+
test_case_id TEXT NOT NULL,
|
|
626
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
627
|
+
agent_response TEXT,
|
|
628
|
+
judge_reasoning TEXT,
|
|
629
|
+
duration_ms INTEGER,
|
|
630
|
+
error TEXT,
|
|
631
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
632
|
+
FOREIGN KEY (test_case_id) REFERENCES test_cases(id) ON DELETE CASCADE
|
|
633
|
+
);
|
|
634
|
+
CREATE INDEX IF NOT EXISTS idx_test_runs_test_case ON test_runs(test_case_id);
|
|
635
|
+
CREATE INDEX IF NOT EXISTS idx_test_runs_status ON test_runs(status);
|
|
636
|
+
`,
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
name: "029_fix_provider_keys_unique_constraint",
|
|
640
|
+
sql: `
|
|
641
|
+
-- Recreate provider_keys table without UNIQUE constraint on provider_id alone
|
|
642
|
+
-- This allows multiple keys per provider (one per project)
|
|
643
|
+
CREATE TABLE IF NOT EXISTS provider_keys_new (
|
|
644
|
+
id TEXT PRIMARY KEY,
|
|
645
|
+
provider_id TEXT NOT NULL,
|
|
646
|
+
encrypted_key TEXT NOT NULL,
|
|
647
|
+
key_hint TEXT,
|
|
648
|
+
is_valid INTEGER DEFAULT 1,
|
|
649
|
+
last_tested_at TEXT,
|
|
650
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
651
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
652
|
+
name TEXT
|
|
653
|
+
);
|
|
654
|
+
INSERT OR IGNORE INTO provider_keys_new (id, provider_id, encrypted_key, key_hint, is_valid, last_tested_at, created_at, project_id, name)
|
|
655
|
+
SELECT id, provider_id, encrypted_key, key_hint, is_valid, last_tested_at, created_at, project_id, name FROM provider_keys;
|
|
656
|
+
DROP TABLE IF EXISTS provider_keys;
|
|
657
|
+
ALTER TABLE provider_keys_new RENAME TO provider_keys;
|
|
658
|
+
CREATE INDEX IF NOT EXISTS idx_provider_keys_provider ON provider_keys(provider_id);
|
|
659
|
+
CREATE INDEX IF NOT EXISTS idx_provider_keys_project ON provider_keys(project_id);
|
|
660
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_provider_keys_unique ON provider_keys(provider_id, COALESCE(project_id, ''));
|
|
661
|
+
`,
|
|
662
|
+
},
|
|
523
663
|
];
|
|
524
664
|
|
|
525
665
|
// Check which migrations have been applied
|
|
@@ -599,6 +739,19 @@ function runSchemaUpgrades() {
|
|
|
599
739
|
console.log(`[db] Migrated ${existingUsers.length} user(s). Usernames derived from email addresses.`);
|
|
600
740
|
}
|
|
601
741
|
}
|
|
742
|
+
|
|
743
|
+
// Assign permanent ports to MCP servers that don't have one yet
|
|
744
|
+
// (HTTP-type servers don't need a local proxy port)
|
|
745
|
+
const mcpWithoutPort = db.query("SELECT id FROM mcp_servers WHERE port IS NULL AND type != 'http'").all() as { id: string }[];
|
|
746
|
+
if (mcpWithoutPort.length > 0) {
|
|
747
|
+
const MCP_BASE_PORT = 4500;
|
|
748
|
+
const maxRow = db.query("SELECT MAX(port) as max_port FROM mcp_servers").get() as { max_port: number | null };
|
|
749
|
+
let nextPort = maxRow.max_port !== null ? maxRow.max_port + 1 : MCP_BASE_PORT;
|
|
750
|
+
for (const row of mcpWithoutPort) {
|
|
751
|
+
db.run("UPDATE mcp_servers SET port = ? WHERE id = ?", [nextPort, row.id]);
|
|
752
|
+
nextPort++;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
602
755
|
}
|
|
603
756
|
|
|
604
757
|
// Generate a unique API key for an agent
|
|
@@ -1149,18 +1302,31 @@ function rowToProviderKey(row: ProviderKeyRow): ProviderKey {
|
|
|
1149
1302
|
|
|
1150
1303
|
// MCP Server operations
|
|
1151
1304
|
export const McpServerDB = {
|
|
1305
|
+
// Get the next available port for a new MCP server (starting from 4500)
|
|
1306
|
+
getNextAvailablePort(): number {
|
|
1307
|
+
const BASE_PORT = 4500;
|
|
1308
|
+
const row = db.query("SELECT MAX(port) as max_port FROM mcp_servers").get() as { max_port: number | null };
|
|
1309
|
+
if (row.max_port === null) {
|
|
1310
|
+
return BASE_PORT;
|
|
1311
|
+
}
|
|
1312
|
+
return row.max_port + 1;
|
|
1313
|
+
},
|
|
1314
|
+
|
|
1152
1315
|
create(server: Omit<McpServer, "created_at" | "status" | "port">): McpServer {
|
|
1153
1316
|
const now = new Date().toISOString();
|
|
1154
1317
|
// Encrypt env vars and headers (credentials) before storing
|
|
1155
1318
|
const envEncrypted = encryptObject(server.env || {});
|
|
1156
1319
|
const headersEncrypted = encryptObject(server.headers || {});
|
|
1320
|
+
// Assign port permanently at creation time (like agents)
|
|
1321
|
+
// HTTP-type servers don't need a local proxy port
|
|
1322
|
+
const port = server.type === "http" ? null : this.getNextAvailablePort();
|
|
1157
1323
|
const stmt = db.prepare(`
|
|
1158
1324
|
INSERT INTO mcp_servers (id, name, type, package, pip_module, command, args, env, url, headers, source, project_id, status, port, created_at)
|
|
1159
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'stopped',
|
|
1325
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'stopped', ?, ?)
|
|
1160
1326
|
`);
|
|
1161
1327
|
stmt.run(
|
|
1162
1328
|
server.id, server.name, server.type, server.package, server.pip_module || null, server.command, server.args,
|
|
1163
|
-
envEncrypted, server.url || null, headersEncrypted, server.source || null, server.project_id || null, now
|
|
1329
|
+
envEncrypted, server.url || null, headersEncrypted, server.source || null, server.project_id || null, port, now
|
|
1164
1330
|
);
|
|
1165
1331
|
return this.findById(server.id)!;
|
|
1166
1332
|
},
|
|
@@ -1251,7 +1417,12 @@ export const McpServerDB = {
|
|
|
1251
1417
|
},
|
|
1252
1418
|
|
|
1253
1419
|
setStatus(id: string, status: "stopped" | "running", port?: number): McpServer | null {
|
|
1254
|
-
|
|
1420
|
+
// Port is permanently assigned — only update if explicitly provided
|
|
1421
|
+
const updates: Partial<Omit<McpServer, "id" | "created_at">> = { status };
|
|
1422
|
+
if (port !== undefined) {
|
|
1423
|
+
updates.port = port;
|
|
1424
|
+
}
|
|
1425
|
+
return this.update(id, updates);
|
|
1255
1426
|
},
|
|
1256
1427
|
|
|
1257
1428
|
delete(id: string): boolean {
|
|
@@ -1260,7 +1431,8 @@ export const McpServerDB = {
|
|
|
1260
1431
|
},
|
|
1261
1432
|
|
|
1262
1433
|
resetAllStatus(): void {
|
|
1263
|
-
|
|
1434
|
+
// Keep ports as they're permanently assigned (like agents)
|
|
1435
|
+
db.run("UPDATE mcp_servers SET status = 'stopped'");
|
|
1264
1436
|
},
|
|
1265
1437
|
|
|
1266
1438
|
count(): number {
|
|
@@ -1896,6 +2068,131 @@ function rowToSession(row: SessionRow): Session {
|
|
|
1896
2068
|
};
|
|
1897
2069
|
}
|
|
1898
2070
|
|
|
2071
|
+
// API Key types
|
|
2072
|
+
export interface ApiKey {
|
|
2073
|
+
id: string;
|
|
2074
|
+
name: string;
|
|
2075
|
+
key_hash: string;
|
|
2076
|
+
key_prefix: string;
|
|
2077
|
+
user_id: string;
|
|
2078
|
+
expires_at: string | null;
|
|
2079
|
+
last_used_at: string | null;
|
|
2080
|
+
is_active: boolean;
|
|
2081
|
+
created_at: string;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
interface ApiKeyRow {
|
|
2085
|
+
id: string;
|
|
2086
|
+
name: string;
|
|
2087
|
+
key_hash: string;
|
|
2088
|
+
key_prefix: string;
|
|
2089
|
+
user_id: string;
|
|
2090
|
+
expires_at: string | null;
|
|
2091
|
+
last_used_at: string | null;
|
|
2092
|
+
is_active: number;
|
|
2093
|
+
created_at: string;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
function rowToApiKey(row: ApiKeyRow): ApiKey {
|
|
2097
|
+
return {
|
|
2098
|
+
id: row.id,
|
|
2099
|
+
name: row.name,
|
|
2100
|
+
key_hash: row.key_hash,
|
|
2101
|
+
key_prefix: row.key_prefix,
|
|
2102
|
+
user_id: row.user_id,
|
|
2103
|
+
expires_at: row.expires_at,
|
|
2104
|
+
last_used_at: row.last_used_at,
|
|
2105
|
+
is_active: row.is_active === 1,
|
|
2106
|
+
created_at: row.created_at,
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
// API Key operations
|
|
2111
|
+
export const ApiKeyDB = {
|
|
2112
|
+
// Create a new API key (returns the raw key only at creation time)
|
|
2113
|
+
create(data: { name: string; user_id: string; expires_at?: string | null }): { apiKey: ApiKey; rawKey: string } {
|
|
2114
|
+
const id = generateId();
|
|
2115
|
+
const rawKey = `apt_${randomBytes(24).toString("hex")}`;
|
|
2116
|
+
const keyHash = createHash("sha256").update(rawKey).digest("hex");
|
|
2117
|
+
const keyPrefix = rawKey.slice(0, 10);
|
|
2118
|
+
const now = new Date().toISOString();
|
|
2119
|
+
|
|
2120
|
+
db.run(
|
|
2121
|
+
`INSERT INTO api_keys (id, name, key_hash, key_prefix, user_id, expires_at, created_at)
|
|
2122
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
2123
|
+
[id, data.name, keyHash, keyPrefix, data.user_id, data.expires_at || null, now]
|
|
2124
|
+
);
|
|
2125
|
+
|
|
2126
|
+
return { apiKey: this.findById(id)!, rawKey };
|
|
2127
|
+
},
|
|
2128
|
+
|
|
2129
|
+
// Find by ID
|
|
2130
|
+
findById(id: string): ApiKey | null {
|
|
2131
|
+
const row = db.query("SELECT * FROM api_keys WHERE id = ?").get(id) as ApiKeyRow | null;
|
|
2132
|
+
return row ? rowToApiKey(row) : null;
|
|
2133
|
+
},
|
|
2134
|
+
|
|
2135
|
+
// Validate a raw key - returns the API key record and user if valid
|
|
2136
|
+
validate(rawKey: string): { apiKey: ApiKey; user: User } | null {
|
|
2137
|
+
const keyHash = createHash("sha256").update(rawKey).digest("hex");
|
|
2138
|
+
const row = db.query(
|
|
2139
|
+
"SELECT * FROM api_keys WHERE key_hash = ? AND is_active = 1"
|
|
2140
|
+
).get(keyHash) as ApiKeyRow | null;
|
|
2141
|
+
|
|
2142
|
+
if (!row) return null;
|
|
2143
|
+
|
|
2144
|
+
const apiKey = rowToApiKey(row);
|
|
2145
|
+
|
|
2146
|
+
// Check expiration
|
|
2147
|
+
if (apiKey.expires_at && new Date(apiKey.expires_at) < new Date()) {
|
|
2148
|
+
return null;
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
// Load the user
|
|
2152
|
+
const user = UserDB.findById(apiKey.user_id);
|
|
2153
|
+
if (!user) return null;
|
|
2154
|
+
|
|
2155
|
+
// Update last_used_at
|
|
2156
|
+
db.run("UPDATE api_keys SET last_used_at = ? WHERE id = ?", [new Date().toISOString(), apiKey.id]);
|
|
2157
|
+
|
|
2158
|
+
return { apiKey, user };
|
|
2159
|
+
},
|
|
2160
|
+
|
|
2161
|
+
// List all keys for a user (does not expose hash)
|
|
2162
|
+
findByUser(userId: string): ApiKey[] {
|
|
2163
|
+
const rows = db.query(
|
|
2164
|
+
"SELECT * FROM api_keys WHERE user_id = ? ORDER BY created_at DESC"
|
|
2165
|
+
).all(userId) as ApiKeyRow[];
|
|
2166
|
+
return rows.map(rowToApiKey);
|
|
2167
|
+
},
|
|
2168
|
+
|
|
2169
|
+
// Revoke a key
|
|
2170
|
+
revoke(id: string, userId: string): boolean {
|
|
2171
|
+
const result = db.run(
|
|
2172
|
+
"UPDATE api_keys SET is_active = 0 WHERE id = ? AND user_id = ?",
|
|
2173
|
+
[id, userId]
|
|
2174
|
+
);
|
|
2175
|
+
return result.changes > 0;
|
|
2176
|
+
},
|
|
2177
|
+
|
|
2178
|
+
// Delete a key
|
|
2179
|
+
delete(id: string, userId: string): boolean {
|
|
2180
|
+
const result = db.run(
|
|
2181
|
+
"DELETE FROM api_keys WHERE id = ? AND user_id = ?",
|
|
2182
|
+
[id, userId]
|
|
2183
|
+
);
|
|
2184
|
+
return result.changes > 0;
|
|
2185
|
+
},
|
|
2186
|
+
|
|
2187
|
+
// Count active keys for a user
|
|
2188
|
+
countByUser(userId: string): number {
|
|
2189
|
+
const row = db.query(
|
|
2190
|
+
"SELECT COUNT(*) as count FROM api_keys WHERE user_id = ? AND is_active = 1"
|
|
2191
|
+
).get(userId) as { count: number };
|
|
2192
|
+
return row.count;
|
|
2193
|
+
},
|
|
2194
|
+
};
|
|
2195
|
+
|
|
1899
2196
|
// Skill operations
|
|
1900
2197
|
export const SkillDB = {
|
|
1901
2198
|
// Create a new skill
|
|
@@ -12,44 +12,104 @@ import type {
|
|
|
12
12
|
// AgentDojo MCP API base URL
|
|
13
13
|
const AGENTDOJO_API_BASE = process.env.AGENTDOJO_API_BASE || "https://api.agentdojo.dev";
|
|
14
14
|
|
|
15
|
+
// Map MCP API provider_type to IntegrationApp authSchemes
|
|
16
|
+
function mapAuthSchemes(providerType: string): string[] {
|
|
17
|
+
switch (providerType) {
|
|
18
|
+
case "oauth": return ["OAUTH2"];
|
|
19
|
+
case "api_key": return ["API_KEY"];
|
|
20
|
+
case "basic_auth": return ["BASIC"];
|
|
21
|
+
case "none": return ["NONE"];
|
|
22
|
+
default: return ["API_KEY"];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
15
26
|
export const AgentDojoProvider: IntegrationProvider = {
|
|
16
27
|
id: "agentdojo",
|
|
17
28
|
name: "AgentDojo",
|
|
18
29
|
|
|
19
|
-
// List
|
|
30
|
+
// List toolkits + providers from MCP API, merge so we get both
|
|
31
|
+
// no-auth toolkits AND OAuth/API key providers
|
|
20
32
|
async listApps(apiKey: string): Promise<IntegrationApp[]> {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
},
|
|
33
|
+
const headers = { "X-API-Key": apiKey, "Content-Type": "application/json" };
|
|
34
|
+
|
|
35
|
+
// Fetch both in parallel
|
|
36
|
+
const [toolkitsRes, providersRes] = await Promise.all([
|
|
37
|
+
fetch(`${AGENTDOJO_API_BASE}/toolkits?include_tools=true`, { headers }).catch(() => null),
|
|
38
|
+
fetch(`${AGENTDOJO_API_BASE}/providers?is_active=true`, { headers }).catch(() => null),
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
// Parse toolkits
|
|
42
|
+
let toolkits: any[] = [];
|
|
43
|
+
if (toolkitsRes?.ok) {
|
|
44
|
+
const data = await toolkitsRes.json();
|
|
45
|
+
toolkits = data.toolkits || data.data || [];
|
|
46
|
+
} else if (toolkitsRes) {
|
|
47
|
+
console.error("AgentDojo listApps toolkits error:", toolkitsRes.status);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Parse providers (for auth type info)
|
|
51
|
+
let providers: any[] = [];
|
|
52
|
+
if (providersRes?.ok) {
|
|
53
|
+
const data = await providersRes.json();
|
|
54
|
+
providers = data.providers || data.data || [];
|
|
55
|
+
} else if (providersRes) {
|
|
56
|
+
console.error("AgentDojo listApps providers error:", providersRes.status);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Index providers by name for quick lookup
|
|
60
|
+
const providerByName = new Map<string, any>();
|
|
61
|
+
for (const p of providers) {
|
|
62
|
+
providerByName.set(p.name, p);
|
|
63
|
+
if (p.display_name) providerByName.set(p.display_name.toLowerCase(), p);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Map toolkits to apps, enriching auth info from providers
|
|
67
|
+
const apps: IntegrationApp[] = toolkits.map((toolkit: any) => {
|
|
68
|
+
const name = toolkit.name || toolkit.slug;
|
|
69
|
+
// Try to find matching provider for this toolkit
|
|
70
|
+
const provider = providerByName.get(name) || providerByName.get(name?.toLowerCase());
|
|
71
|
+
|
|
72
|
+
let authSchemes: string[];
|
|
73
|
+
if (provider) {
|
|
74
|
+
authSchemes = mapAuthSchemes(provider.provider_type);
|
|
75
|
+
} else if (toolkit.requires_auth) {
|
|
76
|
+
authSchemes = ["API_KEY"]; // Default if no provider found but auth required
|
|
77
|
+
} else {
|
|
78
|
+
authSchemes = ["NONE"];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
id: String(toolkit.id),
|
|
83
|
+
name: toolkit.display_name || toolkit.name,
|
|
84
|
+
slug: name,
|
|
85
|
+
description: toolkit.description || null,
|
|
86
|
+
logo: provider?.favicon || toolkit.icon_url || null,
|
|
87
|
+
categories: [],
|
|
88
|
+
authSchemes,
|
|
89
|
+
};
|
|
26
90
|
});
|
|
27
91
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
92
|
+
// Also add any providers that don't match a toolkit (standalone OAuth providers)
|
|
93
|
+
const toolkitNames = new Set(toolkits.map((t: any) => t.name));
|
|
94
|
+
for (const p of providers) {
|
|
95
|
+
if (!toolkitNames.has(p.name)) {
|
|
96
|
+
apps.push({
|
|
97
|
+
id: String(p.id),
|
|
98
|
+
name: p.display_name || p.name,
|
|
99
|
+
slug: p.name,
|
|
100
|
+
description: p.description || null,
|
|
101
|
+
logo: p.favicon || p.icon_url || null,
|
|
102
|
+
categories: [],
|
|
103
|
+
authSchemes: mapAuthSchemes(p.provider_type),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
31
106
|
}
|
|
32
107
|
|
|
33
|
-
|
|
34
|
-
const toolkits = data.toolkits || [];
|
|
35
|
-
|
|
36
|
-
return toolkits.map((toolkit: any) => ({
|
|
37
|
-
id: toolkit.id,
|
|
38
|
-
name: toolkit.display_name || toolkit.name,
|
|
39
|
-
slug: toolkit.name,
|
|
40
|
-
description: toolkit.description || null,
|
|
41
|
-
logo: toolkit.icon_url || null,
|
|
42
|
-
categories: [],
|
|
43
|
-
// If toolkit requires auth, it needs API_KEY connection
|
|
44
|
-
authSchemes: toolkit.requires_auth ? ["API_KEY"] : ["NONE"],
|
|
45
|
-
toolsCount: toolkit.tools_count || toolkit.tools?.length || 0,
|
|
46
|
-
tools: toolkit.tools || [],
|
|
47
|
-
}));
|
|
108
|
+
return apps;
|
|
48
109
|
},
|
|
49
110
|
|
|
50
|
-
// List user's credentials
|
|
111
|
+
// List user's credentials
|
|
51
112
|
async listConnectedAccounts(apiKey: string, userId: string): Promise<ConnectedAccount[]> {
|
|
52
|
-
// Get list of credentials from our credentials API
|
|
53
113
|
const res = await fetch(`${AGENTDOJO_API_BASE}/credentials`, {
|
|
54
114
|
headers: {
|
|
55
115
|
"X-API-Key": apiKey,
|
|
@@ -66,18 +126,19 @@ export const AgentDojoProvider: IntegrationProvider = {
|
|
|
66
126
|
const credentials = data.data || data.credentials || [];
|
|
67
127
|
|
|
68
128
|
return credentials.map((cred: any) => ({
|
|
69
|
-
id: cred.id,
|
|
70
|
-
appId: cred.provider_id || cred.toolkit_id || cred.provider_name,
|
|
71
|
-
appName: cred.provider_name || cred.toolkit_name || cred.provider_id,
|
|
72
|
-
status: cred.is_valid !== false ? "active" : "failed",
|
|
129
|
+
id: String(cred.id),
|
|
130
|
+
appId: String(cred.provider_id || cred.toolkit_id || cred.provider_name),
|
|
131
|
+
appName: cred.provider_name || cred.name || cred.toolkit_name || String(cred.provider_id),
|
|
132
|
+
status: (cred.status === "active" || cred.is_valid !== false) ? "active" as const : "failed" as const,
|
|
73
133
|
createdAt: cred.created_at || new Date().toISOString(),
|
|
74
134
|
metadata: {
|
|
75
135
|
keyHint: cred.key_hint,
|
|
136
|
+
credentialType: cred.credential_type,
|
|
76
137
|
},
|
|
77
138
|
}));
|
|
78
139
|
},
|
|
79
140
|
|
|
80
|
-
//
|
|
141
|
+
// Initiate connection — OAuth (popup redirect) or API key (direct store)
|
|
81
142
|
async initiateConnection(
|
|
82
143
|
apiKey: string,
|
|
83
144
|
userId: string,
|
|
@@ -85,11 +146,41 @@ export const AgentDojoProvider: IntegrationProvider = {
|
|
|
85
146
|
redirectUrl: string,
|
|
86
147
|
credentials?: ConnectionCredentials
|
|
87
148
|
): Promise<ConnectionRequest> {
|
|
149
|
+
// OAuth flow: no credentials provided, or explicit OAUTH2 scheme
|
|
88
150
|
if (!credentials?.apiKey) {
|
|
89
|
-
|
|
151
|
+
// Init OAuth via MCP API
|
|
152
|
+
const res = await fetch(`${AGENTDOJO_API_BASE}/oauth/init`, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: {
|
|
155
|
+
"X-API-Key": apiKey,
|
|
156
|
+
"Content-Type": "application/json",
|
|
157
|
+
},
|
|
158
|
+
body: JSON.stringify({
|
|
159
|
+
provider_name: appSlug,
|
|
160
|
+
}),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (!res.ok) {
|
|
164
|
+
const text = await res.text();
|
|
165
|
+
console.error("AgentDojo OAuth init error:", res.status, text);
|
|
166
|
+
throw new Error(`Failed to initiate OAuth: ${text}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const data = await res.json();
|
|
170
|
+
const flowData = data.data || data;
|
|
171
|
+
|
|
172
|
+
if (!flowData.auth_url) {
|
|
173
|
+
throw new Error("No auth URL returned from OAuth init");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
redirectUrl: flowData.auth_url,
|
|
178
|
+
connectionId: flowData.state_token || flowData.state || flowData.flow_id,
|
|
179
|
+
status: "pending",
|
|
180
|
+
};
|
|
90
181
|
}
|
|
91
182
|
|
|
92
|
-
//
|
|
183
|
+
// API key flow: store credential directly
|
|
93
184
|
const res = await fetch(`${AGENTDOJO_API_BASE}/credentials`, {
|
|
94
185
|
method: "POST",
|
|
95
186
|
headers: {
|
|
@@ -99,26 +190,61 @@ export const AgentDojoProvider: IntegrationProvider = {
|
|
|
99
190
|
body: JSON.stringify({
|
|
100
191
|
provider_id: appSlug,
|
|
101
192
|
provider_name: appSlug,
|
|
102
|
-
api_key: credentials.apiKey,
|
|
193
|
+
credential_data: { api_key: credentials.apiKey },
|
|
103
194
|
}),
|
|
104
195
|
});
|
|
105
196
|
|
|
106
197
|
if (!res.ok) {
|
|
107
198
|
const text = await res.text();
|
|
108
|
-
console.error("AgentDojo
|
|
199
|
+
console.error("AgentDojo credential create error:", res.status, text);
|
|
109
200
|
throw new Error(`Failed to store credentials: ${text}`);
|
|
110
201
|
}
|
|
111
202
|
|
|
112
203
|
const data = await res.json();
|
|
113
204
|
|
|
114
205
|
return {
|
|
115
|
-
redirectUrl: null,
|
|
116
|
-
connectionId: data.data?.id || data.id,
|
|
117
|
-
status: "active",
|
|
206
|
+
redirectUrl: null,
|
|
207
|
+
connectionId: String(data.data?.id || data.id),
|
|
208
|
+
status: "active",
|
|
118
209
|
};
|
|
119
210
|
},
|
|
120
211
|
|
|
212
|
+
// Check connection/OAuth flow status
|
|
121
213
|
async getConnectionStatus(apiKey: string, connectionId: string): Promise<ConnectedAccount | null> {
|
|
214
|
+
// First try OAuth status poll (connectionId = state_token)
|
|
215
|
+
const oauthRes = await fetch(`${AGENTDOJO_API_BASE}/oauth/status?state=${encodeURIComponent(connectionId)}`, {
|
|
216
|
+
headers: {
|
|
217
|
+
"X-API-Key": apiKey,
|
|
218
|
+
"Content-Type": "application/json",
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (oauthRes.ok) {
|
|
223
|
+
const oauthData = await oauthRes.json();
|
|
224
|
+
const status = oauthData.data?.status || oauthData.status;
|
|
225
|
+
|
|
226
|
+
if (status === "completed") {
|
|
227
|
+
return {
|
|
228
|
+
id: connectionId,
|
|
229
|
+
appId: oauthData.data?.provider_name || oauthData.data?.provider_id || "",
|
|
230
|
+
appName: oauthData.data?.provider_name || "",
|
|
231
|
+
status: "active",
|
|
232
|
+
createdAt: new Date().toISOString(),
|
|
233
|
+
};
|
|
234
|
+
} else if (status === "failed") {
|
|
235
|
+
return {
|
|
236
|
+
id: connectionId,
|
|
237
|
+
appId: oauthData.data?.provider_name || "",
|
|
238
|
+
appName: oauthData.data?.provider_name || "",
|
|
239
|
+
status: "failed",
|
|
240
|
+
createdAt: new Date().toISOString(),
|
|
241
|
+
};
|
|
242
|
+
} else if (status === "pending") {
|
|
243
|
+
return null; // Still waiting
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Fallback: check credentials list directly (for API key connections)
|
|
122
248
|
const res = await fetch(`${AGENTDOJO_API_BASE}/credentials`, {
|
|
123
249
|
headers: {
|
|
124
250
|
"X-API-Key": apiKey,
|
|
@@ -130,14 +256,14 @@ export const AgentDojoProvider: IntegrationProvider = {
|
|
|
130
256
|
|
|
131
257
|
const data = await res.json();
|
|
132
258
|
const credentials = data.data || data.credentials || [];
|
|
133
|
-
const cred = credentials.find((c: any) => c.id === connectionId);
|
|
259
|
+
const cred = credentials.find((c: any) => String(c.id) === connectionId);
|
|
134
260
|
|
|
135
261
|
if (!cred) return null;
|
|
136
262
|
|
|
137
263
|
return {
|
|
138
|
-
id: cred.id,
|
|
139
|
-
appId: cred.provider_id || cred.toolkit_id,
|
|
140
|
-
appName: cred.provider_name || cred.
|
|
264
|
+
id: String(cred.id),
|
|
265
|
+
appId: String(cred.provider_id || cred.toolkit_id),
|
|
266
|
+
appName: cred.provider_name || cred.name || String(cred.provider_id),
|
|
141
267
|
status: "active",
|
|
142
268
|
createdAt: cred.created_at || new Date().toISOString(),
|
|
143
269
|
};
|
package/src/mcp-client.ts
CHANGED
|
@@ -139,16 +139,22 @@ export async function startMcpProcess(
|
|
|
139
139
|
return { success: false, error: `Process exited: ${stderr || "unknown error"}` };
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
// Start HTTP proxy server if port specified
|
|
142
|
+
// Start HTTP proxy server if port specified (retry once if port busy from previous process)
|
|
143
143
|
if (httpPort) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
144
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
145
|
+
try {
|
|
146
|
+
const httpServer = startHttpProxy(serverId, httpPort);
|
|
147
|
+
entry.httpServer = httpServer;
|
|
148
|
+
entry.httpPort = httpPort;
|
|
149
|
+
console.log(`[MCP] HTTP proxy for ${serverId} started on port ${httpPort}`);
|
|
150
|
+
break;
|
|
151
|
+
} catch (err: any) {
|
|
152
|
+
if (err?.code === "EADDRINUSE" && attempt < 2) {
|
|
153
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
console.error(`[MCP] Failed to start HTTP proxy for ${serverId}:`, err);
|
|
157
|
+
}
|
|
152
158
|
}
|
|
153
159
|
}
|
|
154
160
|
|