@agenticmail/enterprise 0.5.263 → 0.5.265
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/agent-heartbeat-555FI2S4.js +510 -0
- package/dist/agent-heartbeat-OI5NCSVK.js +510 -0
- package/dist/chunk-D4SZ5O2E.js +1224 -0
- package/dist/chunk-ENUTKURJ.js +1224 -0
- package/dist/chunk-F5NPN7BG.js +3778 -0
- package/dist/chunk-G5GAUJXJ.js +4732 -0
- package/dist/chunk-JOEBIVIE.js +4732 -0
- package/dist/chunk-LMB7DQL3.js +3778 -0
- package/dist/cli-agent-4OC2FXMQ.js +1768 -0
- package/dist/cli-agent-JIJ4TZPF.js +1768 -0
- package/dist/cli-serve-KCMTALAR.js +114 -0
- package/dist/cli-serve-PO47LUNK.js +114 -0
- package/dist/cli.js +3 -3
- package/dist/index.js +3 -3
- package/dist/routes-B74M2BGU.js +14652 -0
- package/dist/routes-KUVZIIZI.js +14673 -0
- package/dist/runtime-C4N65ZI4.js +45 -0
- package/dist/runtime-KFA4Y37B.js +45 -0
- package/dist/server-BCLS6J2T.js +15 -0
- package/dist/server-J6BO4HOX.js +15 -0
- package/dist/setup-IVOR5YBA.js +20 -0
- package/dist/setup-XR5VCQQK.js +20 -0
- package/package.json +1 -1
- package/src/database-access/agent-tools.ts +178 -0
- package/src/database-access/connection-manager.ts +970 -0
- package/src/database-access/index.ts +21 -0
- package/src/database-access/query-sanitizer.ts +220 -0
- package/src/database-access/routes.ts +193 -0
- package/src/database-access/types.ts +224 -0
- package/src/engine/routes.ts +7 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Access System — Public API
|
|
3
|
+
*
|
|
4
|
+
* Enterprise-grade database connectivity for AI agents.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { DatabaseConnectionManager } from './connection-manager.js';
|
|
8
|
+
export { createDatabaseAccessRoutes } from './routes.js';
|
|
9
|
+
export { createDatabaseTools } from './agent-tools.js';
|
|
10
|
+
export { sanitizeQuery, classifyQuery, sanitizeForLogging } from './query-sanitizer.js';
|
|
11
|
+
export type {
|
|
12
|
+
DatabaseType,
|
|
13
|
+
DatabaseConnectionConfig,
|
|
14
|
+
AgentDatabaseAccess,
|
|
15
|
+
DatabasePermission,
|
|
16
|
+
DatabaseQuery,
|
|
17
|
+
QueryResult,
|
|
18
|
+
DatabaseAuditEntry,
|
|
19
|
+
ConnectionPoolStats,
|
|
20
|
+
} from './types.js';
|
|
21
|
+
export { DATABASE_LABELS, DATABASE_CATEGORIES } from './types.js';
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Sanitizer — Enterprise Security Layer
|
|
3
|
+
*
|
|
4
|
+
* Validates and sanitizes all database queries before execution.
|
|
5
|
+
* Prevents SQL injection, enforces schema restrictions, blocks dangerous operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { DatabasePermission, DatabaseConnectionConfig, AgentDatabaseAccess } from './types.js';
|
|
9
|
+
|
|
10
|
+
// ─── Dangerous Patterns ──────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
/** SQL patterns that are ALWAYS blocked regardless of permissions */
|
|
13
|
+
const BLOCKED_PATTERNS = [
|
|
14
|
+
/;\s*DROP\s+/i,
|
|
15
|
+
/;\s*TRUNCATE\s+/i,
|
|
16
|
+
/;\s*ALTER\s+/i,
|
|
17
|
+
/;\s*CREATE\s+/i,
|
|
18
|
+
/;\s*GRANT\s+/i,
|
|
19
|
+
/;\s*REVOKE\s+/i,
|
|
20
|
+
/;\s*SET\s+ROLE/i,
|
|
21
|
+
/LOAD_FILE\s*\(/i,
|
|
22
|
+
/INTO\s+OUTFILE/i,
|
|
23
|
+
/INTO\s+DUMPFILE/i,
|
|
24
|
+
/INFORMATION_SCHEMA/i,
|
|
25
|
+
/pg_catalog\./i,
|
|
26
|
+
/sys\.\w+/i,
|
|
27
|
+
/xp_cmdshell/i,
|
|
28
|
+
/sp_execute/i,
|
|
29
|
+
/EXEC\s*\(/i,
|
|
30
|
+
/EXECUTE\s+IMMEDIATE/i,
|
|
31
|
+
/--\s*$/m, // SQL comments at end of line (common injection)
|
|
32
|
+
/\/\*[\s\S]*?\*\//, // Block comments (hiding malicious code)
|
|
33
|
+
/SLEEP\s*\(/i,
|
|
34
|
+
/BENCHMARK\s*\(/i,
|
|
35
|
+
/WAITFOR\s+DELAY/i,
|
|
36
|
+
/pg_sleep/i,
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** Patterns that require 'execute' permission */
|
|
40
|
+
const EXECUTE_PATTERNS = [
|
|
41
|
+
/\bCALL\s+/i,
|
|
42
|
+
/\bEXEC\s+/i,
|
|
43
|
+
/\bEXECUTE\s+/i,
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// ─── Query Classification ────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
export type QueryOperation = 'read' | 'write' | 'delete' | 'schema' | 'execute' | 'blocked';
|
|
49
|
+
|
|
50
|
+
export function classifyQuery(sql: string): QueryOperation {
|
|
51
|
+
const trimmed = sql.trim().toUpperCase();
|
|
52
|
+
|
|
53
|
+
// Check blocked patterns first
|
|
54
|
+
for (const pattern of BLOCKED_PATTERNS) {
|
|
55
|
+
if (pattern.test(sql)) return 'blocked';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check execute patterns
|
|
59
|
+
for (const pattern of EXECUTE_PATTERNS) {
|
|
60
|
+
if (pattern.test(sql)) return 'execute';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (trimmed.startsWith('SELECT') || trimmed.startsWith('WITH') || trimmed.startsWith('EXPLAIN')) return 'read';
|
|
64
|
+
if (trimmed.startsWith('INSERT') || trimmed.startsWith('UPDATE') || trimmed.startsWith('UPSERT') || trimmed.startsWith('MERGE')) return 'write';
|
|
65
|
+
if (trimmed.startsWith('DELETE')) return 'delete';
|
|
66
|
+
if (trimmed.startsWith('CREATE') || trimmed.startsWith('ALTER') || trimmed.startsWith('DROP')) return 'schema';
|
|
67
|
+
|
|
68
|
+
// Default to blocked for unrecognized patterns
|
|
69
|
+
return 'blocked';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Permission Check ────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
export interface SanitizeResult {
|
|
75
|
+
allowed: boolean;
|
|
76
|
+
operation: QueryOperation;
|
|
77
|
+
reason?: string;
|
|
78
|
+
sanitizedQuery?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function sanitizeQuery(
|
|
82
|
+
sql: string,
|
|
83
|
+
permissions: DatabasePermission[],
|
|
84
|
+
connectionConfig: DatabaseConnectionConfig,
|
|
85
|
+
agentAccess: AgentDatabaseAccess,
|
|
86
|
+
): SanitizeResult {
|
|
87
|
+
// 1. Classify the query
|
|
88
|
+
const operation = classifyQuery(sql);
|
|
89
|
+
|
|
90
|
+
if (operation === 'blocked') {
|
|
91
|
+
return { allowed: false, operation, reason: 'Query contains blocked patterns (potential injection or dangerous operations)' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 2. Check agent has required permission
|
|
95
|
+
if (!permissions.includes(operation as DatabasePermission)) {
|
|
96
|
+
return { allowed: false, operation, reason: `Agent lacks '${operation}' permission on this database` };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 3. Check table access
|
|
100
|
+
const tables = extractTableNames(sql);
|
|
101
|
+
const mergedBlocked = mergeBlockedTables(connectionConfig, agentAccess);
|
|
102
|
+
const mergedAllowed = mergeAllowedTables(connectionConfig, agentAccess);
|
|
103
|
+
|
|
104
|
+
for (const table of tables) {
|
|
105
|
+
if (mergedBlocked.has(table.toLowerCase())) {
|
|
106
|
+
return { allowed: false, operation, reason: `Access to table '${table}' is blocked` };
|
|
107
|
+
}
|
|
108
|
+
if (mergedAllowed.size > 0 && !mergedAllowed.has(table.toLowerCase())) {
|
|
109
|
+
return { allowed: false, operation, reason: `Table '${table}' is not in the allowed tables list` };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 4. Check column access (for SELECT, INSERT, UPDATE)
|
|
114
|
+
const columns = extractColumnNames(sql);
|
|
115
|
+
const blockedColumns = new Set([
|
|
116
|
+
...(connectionConfig.schemaAccess?.blockedColumns || []).map(c => c.toLowerCase()),
|
|
117
|
+
...(agentAccess.schemaAccess?.blockedColumns || []).map(c => c.toLowerCase()),
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
for (const col of columns) {
|
|
121
|
+
if (blockedColumns.has(col.toLowerCase())) {
|
|
122
|
+
return { allowed: false, operation, reason: `Access to column '${col}' is blocked (sensitive data)` };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 5. Add safety limits for read queries
|
|
127
|
+
let sanitizedQuery = sql.trim();
|
|
128
|
+
if (operation === 'read' && !sanitizedQuery.toUpperCase().includes('LIMIT')) {
|
|
129
|
+
const maxRows = agentAccess.queryLimits?.maxRowsRead
|
|
130
|
+
?? connectionConfig.queryLimits?.maxRowsRead
|
|
131
|
+
?? 10000;
|
|
132
|
+
sanitizedQuery = `${sanitizedQuery.replace(/;?\s*$/, '')} LIMIT ${maxRows}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { allowed: true, operation, sanitizedQuery };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── Table/Column Extraction ─────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
function extractTableNames(sql: string): string[] {
|
|
141
|
+
const tables = new Set<string>();
|
|
142
|
+
|
|
143
|
+
// FROM clause
|
|
144
|
+
const fromMatch = sql.match(/\bFROM\s+([`"[\]]?\w+[`"\]]?(?:\s*\.\s*[`"[\]]?\w+[`"\]]?)?)/gi);
|
|
145
|
+
if (fromMatch) {
|
|
146
|
+
for (const m of fromMatch) {
|
|
147
|
+
const name = m.replace(/^FROM\s+/i, '').replace(/[`"[\]]/g, '').trim();
|
|
148
|
+
tables.add(name.split('.').pop() || name);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// JOIN clause
|
|
153
|
+
const joinMatch = sql.match(/\bJOIN\s+([`"[\]]?\w+[`"\]]?(?:\s*\.\s*[`"[\]]?\w+[`"\]]?)?)/gi);
|
|
154
|
+
if (joinMatch) {
|
|
155
|
+
for (const m of joinMatch) {
|
|
156
|
+
const name = m.replace(/^JOIN\s+/i, '').replace(/[`"[\]]/g, '').trim();
|
|
157
|
+
tables.add(name.split('.').pop() || name);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// INSERT INTO
|
|
162
|
+
const insertMatch = sql.match(/\bINSERT\s+INTO\s+([`"[\]]?\w+[`"\]]?)/i);
|
|
163
|
+
if (insertMatch) tables.add(insertMatch[1].replace(/[`"[\]]/g, ''));
|
|
164
|
+
|
|
165
|
+
// UPDATE
|
|
166
|
+
const updateMatch = sql.match(/\bUPDATE\s+([`"[\]]?\w+[`"\]]?)/i);
|
|
167
|
+
if (updateMatch) tables.add(updateMatch[1].replace(/[`"[\]]/g, ''));
|
|
168
|
+
|
|
169
|
+
// DELETE FROM
|
|
170
|
+
const deleteMatch = sql.match(/\bDELETE\s+FROM\s+([`"[\]]?\w+[`"\]]?)/i);
|
|
171
|
+
if (deleteMatch) tables.add(deleteMatch[1].replace(/[`"[\]]/g, ''));
|
|
172
|
+
|
|
173
|
+
return [...tables];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function extractColumnNames(sql: string): string[] {
|
|
177
|
+
// Extract column names from SELECT, INSERT, UPDATE SET clauses
|
|
178
|
+
// This is best-effort — complex queries may not parse perfectly
|
|
179
|
+
const columns = new Set<string>();
|
|
180
|
+
|
|
181
|
+
// SELECT columns (between SELECT and FROM)
|
|
182
|
+
const selectMatch = sql.match(/\bSELECT\s+(.*?)\bFROM\b/is);
|
|
183
|
+
if (selectMatch && !selectMatch[1].includes('*')) {
|
|
184
|
+
const parts = selectMatch[1].split(',');
|
|
185
|
+
for (const p of parts) {
|
|
186
|
+
const col = p.trim().split(/\s+AS\s+/i)[0].trim().split('.').pop();
|
|
187
|
+
if (col && /^\w+$/.test(col)) columns.add(col);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return [...columns];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function mergeBlockedTables(config: DatabaseConnectionConfig, access: AgentDatabaseAccess): Set<string> {
|
|
195
|
+
return new Set([
|
|
196
|
+
...(config.schemaAccess?.blockedTables || []).map(t => t.toLowerCase()),
|
|
197
|
+
...(access.schemaAccess?.blockedTables || []).map(t => t.toLowerCase()),
|
|
198
|
+
]);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function mergeAllowedTables(config: DatabaseConnectionConfig, access: AgentDatabaseAccess): Set<string> {
|
|
202
|
+
const connAllowed = config.schemaAccess?.allowedTables || [];
|
|
203
|
+
const agentAllowed = access.schemaAccess?.allowedTables || [];
|
|
204
|
+
|
|
205
|
+
// If agent has specific allowed tables, use those (most restrictive)
|
|
206
|
+
if (agentAllowed.length > 0) return new Set(agentAllowed.map(t => t.toLowerCase()));
|
|
207
|
+
if (connAllowed.length > 0) return new Set(connAllowed.map(t => t.toLowerCase()));
|
|
208
|
+
return new Set(); // Empty = all tables allowed
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Sanitize a query string for safe logging (remove parameter values).
|
|
213
|
+
*/
|
|
214
|
+
export function sanitizeForLogging(sql: string): string {
|
|
215
|
+
// Replace string literals
|
|
216
|
+
let sanitized = sql.replace(/'[^']*'/g, "'?'");
|
|
217
|
+
// Replace numeric literals after = or IN
|
|
218
|
+
sanitized = sanitized.replace(/=\s*\d+/g, '= ?');
|
|
219
|
+
return sanitized;
|
|
220
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Access — API Routes
|
|
3
|
+
*
|
|
4
|
+
* REST API for managing database connections, agent access, and query execution.
|
|
5
|
+
* All routes require authentication and org-level authorization.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Hono } from 'hono';
|
|
9
|
+
import type { DatabaseConnectionManager } from './connection-manager.js';
|
|
10
|
+
import type { DatabasePermission } from './types.js';
|
|
11
|
+
|
|
12
|
+
export function createDatabaseAccessRoutes(manager: DatabaseConnectionManager) {
|
|
13
|
+
const router = new Hono();
|
|
14
|
+
|
|
15
|
+
// ─── Connections ─────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/** List all database connections for the org */
|
|
18
|
+
router.get('/connections', async (c) => {
|
|
19
|
+
const orgId = (c as any).get?.('orgId') || 'default';
|
|
20
|
+
const connections = manager.listConnections(orgId);
|
|
21
|
+
// Strip sensitive fields
|
|
22
|
+
const safe = connections.map(conn => ({
|
|
23
|
+
...conn,
|
|
24
|
+
sslCaCert: conn.sslCaCert ? '***' : undefined,
|
|
25
|
+
sslClientCert: conn.sslClientCert ? '***' : undefined,
|
|
26
|
+
sslClientKey: conn.sslClientKey ? '***' : undefined,
|
|
27
|
+
}));
|
|
28
|
+
return c.json(safe);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/** Get single connection */
|
|
32
|
+
router.get('/connections/:id', async (c) => {
|
|
33
|
+
const conn = manager.getConnection(c.req.param('id'));
|
|
34
|
+
if (!conn) return c.json({ error: 'Connection not found' }, 404);
|
|
35
|
+
return c.json(conn);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/** Create a new database connection */
|
|
39
|
+
router.post('/connections', async (c) => {
|
|
40
|
+
const body = await c.req.json();
|
|
41
|
+
const orgId = (c as any).get?.('orgId') || 'default';
|
|
42
|
+
|
|
43
|
+
if (!body.name || !body.type) {
|
|
44
|
+
return c.json({ error: 'name and type are required' }, 400);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const connection = await manager.createConnection(
|
|
48
|
+
{
|
|
49
|
+
orgId,
|
|
50
|
+
name: body.name,
|
|
51
|
+
type: body.type,
|
|
52
|
+
host: body.host,
|
|
53
|
+
port: body.port,
|
|
54
|
+
database: body.database,
|
|
55
|
+
username: body.username,
|
|
56
|
+
ssl: body.ssl,
|
|
57
|
+
sslRejectUnauthorized: body.sslRejectUnauthorized,
|
|
58
|
+
sshTunnel: body.sshTunnel,
|
|
59
|
+
pool: body.pool,
|
|
60
|
+
queryLimits: body.queryLimits,
|
|
61
|
+
schemaAccess: body.schemaAccess,
|
|
62
|
+
description: body.description,
|
|
63
|
+
tags: body.tags,
|
|
64
|
+
status: 'inactive',
|
|
65
|
+
},
|
|
66
|
+
{ password: body.password, connectionString: body.connectionString },
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return c.json(connection, 201);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/** Update a database connection */
|
|
73
|
+
router.put('/connections/:id', async (c) => {
|
|
74
|
+
const body = await c.req.json();
|
|
75
|
+
const updated = await manager.updateConnection(
|
|
76
|
+
c.req.param('id'),
|
|
77
|
+
body,
|
|
78
|
+
{ password: body.password, connectionString: body.connectionString },
|
|
79
|
+
);
|
|
80
|
+
if (!updated) return c.json({ error: 'Connection not found' }, 404);
|
|
81
|
+
return c.json(updated);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/** Delete a database connection */
|
|
85
|
+
router.delete('/connections/:id', async (c) => {
|
|
86
|
+
const deleted = await manager.deleteConnection(c.req.param('id'));
|
|
87
|
+
if (!deleted) return c.json({ error: 'Connection not found' }, 404);
|
|
88
|
+
return c.json({ ok: true });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/** Test a database connection */
|
|
92
|
+
router.post('/connections/:id/test', async (c) => {
|
|
93
|
+
const result = await manager.testConnection(c.req.param('id'));
|
|
94
|
+
return c.json(result);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/** Get connection pool stats */
|
|
98
|
+
router.get('/connections/:id/stats', async (c) => {
|
|
99
|
+
const stats = manager.getPoolStats(c.req.param('id'));
|
|
100
|
+
return c.json(stats);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ─── Agent Access ──────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
/** List all agents with access to a connection */
|
|
106
|
+
router.get('/connections/:id/agents', async (c) => {
|
|
107
|
+
const agents = manager.getConnectionAgents(c.req.param('id'));
|
|
108
|
+
return c.json(agents);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/** Grant agent access to a connection */
|
|
112
|
+
router.post('/connections/:id/agents', async (c) => {
|
|
113
|
+
const body = await c.req.json();
|
|
114
|
+
const orgId = (c as any).get?.('orgId') || 'default';
|
|
115
|
+
|
|
116
|
+
if (!body.agentId) return c.json({ error: 'agentId is required' }, 400);
|
|
117
|
+
|
|
118
|
+
const access = await manager.grantAccess({
|
|
119
|
+
orgId,
|
|
120
|
+
agentId: body.agentId,
|
|
121
|
+
connectionId: c.req.param('id'),
|
|
122
|
+
permissions: body.permissions || ['read'],
|
|
123
|
+
queryLimits: body.queryLimits,
|
|
124
|
+
schemaAccess: body.schemaAccess,
|
|
125
|
+
logAllQueries: body.logAllQueries ?? false,
|
|
126
|
+
requireApproval: body.requireApproval ?? false,
|
|
127
|
+
enabled: true,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return c.json(access, 201);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/** Update agent access */
|
|
134
|
+
router.put('/connections/:connId/agents/:agentId', async (c) => {
|
|
135
|
+
const body = await c.req.json();
|
|
136
|
+
const updated = await manager.updateAccess(c.req.param('agentId'), c.req.param('connId'), body);
|
|
137
|
+
if (!updated) return c.json({ error: 'Access grant not found' }, 404);
|
|
138
|
+
return c.json(updated);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/** Revoke agent access */
|
|
142
|
+
router.delete('/connections/:connId/agents/:agentId', async (c) => {
|
|
143
|
+
await manager.revokeAccess(c.req.param('agentId'), c.req.param('connId'));
|
|
144
|
+
return c.json({ ok: true });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
/** List all connections an agent has access to */
|
|
148
|
+
router.get('/agents/:agentId/connections', async (c) => {
|
|
149
|
+
const accessList = manager.getAgentAccess(c.req.param('agentId'));
|
|
150
|
+
const result = accessList.map(a => ({
|
|
151
|
+
...a,
|
|
152
|
+
connection: manager.getConnection(a.connectionId),
|
|
153
|
+
}));
|
|
154
|
+
return c.json(result);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ─── Query Execution ──────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
/** Execute a query as an agent */
|
|
160
|
+
router.post('/query', async (c) => {
|
|
161
|
+
const body = await c.req.json();
|
|
162
|
+
|
|
163
|
+
if (!body.connectionId || !body.agentId || !body.sql) {
|
|
164
|
+
return c.json({ error: 'connectionId, agentId, and sql are required' }, 400);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const result = await manager.executeQuery({
|
|
168
|
+
connectionId: body.connectionId,
|
|
169
|
+
agentId: body.agentId,
|
|
170
|
+
operation: body.operation || 'read',
|
|
171
|
+
sql: body.sql,
|
|
172
|
+
params: body.params,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return c.json(result, result.success ? 200 : 403);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ─── Audit Log ─────────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
/** Get audit log */
|
|
181
|
+
router.get('/audit', async (c) => {
|
|
182
|
+
const orgId = (c as any).get?.('orgId') || 'default';
|
|
183
|
+
const agentId = c.req.query('agentId');
|
|
184
|
+
const connectionId = c.req.query('connectionId');
|
|
185
|
+
const limit = parseInt(c.req.query('limit') || '100');
|
|
186
|
+
const offset = parseInt(c.req.query('offset') || '0');
|
|
187
|
+
|
|
188
|
+
const logs = await manager.getAuditLog({ orgId, agentId, connectionId, limit, offset });
|
|
189
|
+
return c.json(logs);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return router;
|
|
193
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Access System — Types
|
|
3
|
+
*
|
|
4
|
+
* Enterprise-grade database connectivity for AI agents.
|
|
5
|
+
* Supports: PostgreSQL, MySQL/MariaDB, SQLite, MongoDB, Redis,
|
|
6
|
+
* Microsoft SQL Server, Oracle, CockroachDB, PlanetScale, Turso/LibSQL,
|
|
7
|
+
* DynamoDB, Supabase, Neon, and any ODBC-compatible database.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ─── Database Types ──────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export type DatabaseType =
|
|
13
|
+
| 'postgresql'
|
|
14
|
+
| 'mysql'
|
|
15
|
+
| 'mariadb'
|
|
16
|
+
| 'sqlite'
|
|
17
|
+
| 'mongodb'
|
|
18
|
+
| 'redis'
|
|
19
|
+
| 'mssql'
|
|
20
|
+
| 'oracle'
|
|
21
|
+
| 'cockroachdb'
|
|
22
|
+
| 'planetscale'
|
|
23
|
+
| 'turso'
|
|
24
|
+
| 'dynamodb'
|
|
25
|
+
| 'supabase'
|
|
26
|
+
| 'neon';
|
|
27
|
+
|
|
28
|
+
export const DATABASE_LABELS: Record<DatabaseType, string> = {
|
|
29
|
+
postgresql: 'PostgreSQL',
|
|
30
|
+
mysql: 'MySQL',
|
|
31
|
+
mariadb: 'MariaDB',
|
|
32
|
+
sqlite: 'SQLite',
|
|
33
|
+
mongodb: 'MongoDB',
|
|
34
|
+
redis: 'Redis',
|
|
35
|
+
mssql: 'Microsoft SQL Server',
|
|
36
|
+
oracle: 'Oracle',
|
|
37
|
+
cockroachdb: 'CockroachDB',
|
|
38
|
+
planetscale: 'PlanetScale',
|
|
39
|
+
turso: 'Turso / LibSQL',
|
|
40
|
+
dynamodb: 'AWS DynamoDB',
|
|
41
|
+
supabase: 'Supabase',
|
|
42
|
+
neon: 'Neon',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const DATABASE_CATEGORIES: Record<string, DatabaseType[]> = {
|
|
46
|
+
'Relational (SQL)': ['postgresql', 'mysql', 'mariadb', 'mssql', 'oracle', 'sqlite'],
|
|
47
|
+
'Cloud-Native SQL': ['supabase', 'neon', 'planetscale', 'cockroachdb', 'turso'],
|
|
48
|
+
'NoSQL / Key-Value': ['mongodb', 'redis', 'dynamodb'],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// ─── Connection Configuration ────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export interface DatabaseConnectionConfig {
|
|
54
|
+
id: string;
|
|
55
|
+
orgId: string;
|
|
56
|
+
name: string; // Human-friendly name: "Production DB", "Analytics Warehouse"
|
|
57
|
+
type: DatabaseType;
|
|
58
|
+
|
|
59
|
+
// Connection details (stored encrypted in vault)
|
|
60
|
+
host?: string;
|
|
61
|
+
port?: number;
|
|
62
|
+
database?: string;
|
|
63
|
+
username?: string;
|
|
64
|
+
// password stored in vault, never in this config
|
|
65
|
+
|
|
66
|
+
// Alternative: connection string (stored encrypted in vault)
|
|
67
|
+
// connectionString stored in vault
|
|
68
|
+
|
|
69
|
+
// SSL/TLS
|
|
70
|
+
ssl?: boolean;
|
|
71
|
+
sslCaCert?: string; // Vault reference
|
|
72
|
+
sslClientCert?: string; // Vault reference
|
|
73
|
+
sslClientKey?: string; // Vault reference
|
|
74
|
+
sslRejectUnauthorized?: boolean;
|
|
75
|
+
|
|
76
|
+
// SSH Tunnel
|
|
77
|
+
sshTunnel?: {
|
|
78
|
+
enabled: boolean;
|
|
79
|
+
host: string;
|
|
80
|
+
port?: number;
|
|
81
|
+
username: string;
|
|
82
|
+
// privateKey stored in vault
|
|
83
|
+
localPort?: number;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Connection Pool
|
|
87
|
+
pool?: {
|
|
88
|
+
min?: number;
|
|
89
|
+
max?: number;
|
|
90
|
+
idleTimeoutMs?: number;
|
|
91
|
+
acquireTimeoutMs?: number;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Query Safety
|
|
95
|
+
queryLimits?: {
|
|
96
|
+
maxRowsRead?: number; // Default: 10000
|
|
97
|
+
maxRowsWrite?: number; // Default: 1000
|
|
98
|
+
maxRowsDelete?: number; // Default: 100
|
|
99
|
+
queryTimeoutMs?: number; // Default: 30000
|
|
100
|
+
maxConcurrentQueries?: number; // Default: 5
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Schema restrictions
|
|
104
|
+
schemaAccess?: {
|
|
105
|
+
allowedSchemas?: string[]; // Empty = all schemas
|
|
106
|
+
allowedTables?: string[]; // Empty = all tables
|
|
107
|
+
blockedTables?: string[]; // Takes precedence over allowed
|
|
108
|
+
blockedColumns?: string[]; // Sensitive columns: "ssn", "password", etc.
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Metadata
|
|
112
|
+
description?: string;
|
|
113
|
+
tags?: string[];
|
|
114
|
+
status: 'active' | 'inactive' | 'error';
|
|
115
|
+
lastTestedAt?: string;
|
|
116
|
+
lastError?: string;
|
|
117
|
+
createdAt: string;
|
|
118
|
+
updatedAt: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── Agent Access Control ────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
export type DatabasePermission = 'read' | 'write' | 'delete' | 'schema' | 'execute';
|
|
124
|
+
|
|
125
|
+
export interface AgentDatabaseAccess {
|
|
126
|
+
id: string;
|
|
127
|
+
orgId: string;
|
|
128
|
+
agentId: string;
|
|
129
|
+
connectionId: string;
|
|
130
|
+
|
|
131
|
+
// Granular permissions
|
|
132
|
+
permissions: DatabasePermission[];
|
|
133
|
+
|
|
134
|
+
// Per-agent overrides (stricter than connection defaults)
|
|
135
|
+
queryLimits?: {
|
|
136
|
+
maxRowsRead?: number;
|
|
137
|
+
maxRowsWrite?: number;
|
|
138
|
+
maxRowsDelete?: number;
|
|
139
|
+
queryTimeoutMs?: number;
|
|
140
|
+
maxConcurrentQueries?: number;
|
|
141
|
+
maxQueriesPerMinute?: number; // Rate limiting
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Per-agent schema restrictions (further restricts connection-level)
|
|
145
|
+
schemaAccess?: {
|
|
146
|
+
allowedTables?: string[];
|
|
147
|
+
blockedTables?: string[];
|
|
148
|
+
blockedColumns?: string[];
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Audit settings
|
|
152
|
+
logAllQueries?: boolean; // Default: true for write/delete, false for read
|
|
153
|
+
requireApproval?: boolean; // Require human approval before write/delete
|
|
154
|
+
|
|
155
|
+
enabled: boolean;
|
|
156
|
+
createdAt: string;
|
|
157
|
+
updatedAt: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ─── Query Types ─────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
export interface DatabaseQuery {
|
|
163
|
+
connectionId: string;
|
|
164
|
+
agentId: string;
|
|
165
|
+
operation: 'read' | 'write' | 'delete' | 'schema' | 'execute';
|
|
166
|
+
sql?: string;
|
|
167
|
+
params?: any[];
|
|
168
|
+
|
|
169
|
+
// For NoSQL
|
|
170
|
+
collection?: string;
|
|
171
|
+
filter?: Record<string, any>;
|
|
172
|
+
update?: Record<string, any>;
|
|
173
|
+
document?: Record<string, any>;
|
|
174
|
+
|
|
175
|
+
// For Redis
|
|
176
|
+
command?: string;
|
|
177
|
+
args?: string[];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface QueryResult {
|
|
181
|
+
success: boolean;
|
|
182
|
+
rows?: any[];
|
|
183
|
+
rowCount?: number;
|
|
184
|
+
affectedRows?: number;
|
|
185
|
+
fields?: { name: string; type: string }[];
|
|
186
|
+
error?: string;
|
|
187
|
+
executionTimeMs: number;
|
|
188
|
+
truncated?: boolean; // True if rows were limited
|
|
189
|
+
queryId: string; // For audit trail
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─── Audit Log ───────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
export interface DatabaseAuditEntry {
|
|
195
|
+
id: string;
|
|
196
|
+
orgId: string;
|
|
197
|
+
agentId: string;
|
|
198
|
+
agentName?: string;
|
|
199
|
+
connectionId: string;
|
|
200
|
+
connectionName?: string;
|
|
201
|
+
operation: DatabasePermission;
|
|
202
|
+
query: string; // Sanitized query (no values)
|
|
203
|
+
paramCount: number;
|
|
204
|
+
rowsAffected: number;
|
|
205
|
+
executionTimeMs: number;
|
|
206
|
+
success: boolean;
|
|
207
|
+
error?: string;
|
|
208
|
+
ipAddress?: string;
|
|
209
|
+
timestamp: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── Connection Pool Stats ───────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
export interface ConnectionPoolStats {
|
|
215
|
+
connectionId: string;
|
|
216
|
+
totalConnections: number;
|
|
217
|
+
activeConnections: number;
|
|
218
|
+
idleConnections: number;
|
|
219
|
+
waitingRequests: number;
|
|
220
|
+
queriesExecuted: number;
|
|
221
|
+
averageQueryTimeMs: number;
|
|
222
|
+
errorCount: number;
|
|
223
|
+
lastActivityAt: string;
|
|
224
|
+
}
|
package/src/engine/routes.ts
CHANGED
|
@@ -295,6 +295,11 @@ engine.route('/knowledge-import', createKnowledgeImportRoutes(knowledgeImport));
|
|
|
295
295
|
engine.route('/skill-updates', createSkillUpdaterRoutes(skillUpdater));
|
|
296
296
|
engine.route('/oauth', createOAuthConnectRoutes(vault, lifecycle));
|
|
297
297
|
|
|
298
|
+
// Database Access system
|
|
299
|
+
import { DatabaseConnectionManager, createDatabaseAccessRoutes } from '../database-access/index.js';
|
|
300
|
+
const databaseManager = new DatabaseConnectionManager({ vault });
|
|
301
|
+
engine.route('/database', createDatabaseAccessRoutes(databaseManager));
|
|
302
|
+
|
|
298
303
|
// ─── Hierarchy / Management API ─────────────────────────
|
|
299
304
|
engine.get('/hierarchy/org-chart', async (c) => {
|
|
300
305
|
if (!hierarchyManager) return c.json({ error: 'Hierarchy not initialized' }, 503);
|
|
@@ -752,6 +757,7 @@ export async function setEngineDb(
|
|
|
752
757
|
storageManager.setDb(db),
|
|
753
758
|
policyImporter.setDb(db),
|
|
754
759
|
(async () => { (taskQueue as any).db = (db as any)?.db || db; await taskQueue.init(); })(),
|
|
760
|
+
databaseManager.setDb((db as any)?.db || db),
|
|
755
761
|
]);
|
|
756
762
|
// Initialize hierarchy manager + start background task monitor
|
|
757
763
|
hierarchyManager = new AgentHierarchyManager(db);
|
|
@@ -1026,4 +1032,4 @@ export function setRuntime(runtime: any): void {
|
|
|
1026
1032
|
}
|
|
1027
1033
|
|
|
1028
1034
|
export { engine as engineRoutes };
|
|
1029
|
-
export { permissionEngine, configGen, deployer, approvals, lifecycle, knowledgeBase, tenants, activity, dlp, commBus, guardrails, journal, compliance, communityRegistry, workforce, policyEngine, memoryManager, onboarding, vault, storageManager, policyImporter, knowledgeContribution, skillUpdater, agentStatus, hierarchyManager };
|
|
1035
|
+
export { permissionEngine, configGen, deployer, approvals, lifecycle, knowledgeBase, tenants, activity, dlp, commBus, guardrails, journal, compliance, communityRegistry, workforce, policyEngine, memoryManager, onboarding, vault, storageManager, policyImporter, knowledgeContribution, skillUpdater, agentStatus, hierarchyManager, databaseManager };
|