@agenticmail/enterprise 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/ARCHITECTURE.md +183 -0
  2. package/agenticmail-enterprise.db +0 -0
  3. package/dashboards/README.md +120 -0
  4. package/dashboards/dotnet/Program.cs +261 -0
  5. package/dashboards/express/app.js +146 -0
  6. package/dashboards/go/main.go +513 -0
  7. package/dashboards/html/index.html +535 -0
  8. package/dashboards/java/AgenticMailDashboard.java +376 -0
  9. package/dashboards/php/index.php +414 -0
  10. package/dashboards/python/app.py +273 -0
  11. package/dashboards/ruby/app.rb +195 -0
  12. package/dist/chunk-77IDQJL3.js +7 -0
  13. package/dist/chunk-7RGCCHIT.js +115 -0
  14. package/dist/chunk-DXNKR3TG.js +1355 -0
  15. package/dist/chunk-IQWA44WT.js +970 -0
  16. package/dist/chunk-LCUZGIDH.js +965 -0
  17. package/dist/chunk-N2JVTNNJ.js +2553 -0
  18. package/dist/chunk-O462UJBH.js +363 -0
  19. package/dist/chunk-PNKVD2UK.js +26 -0
  20. package/dist/cli.js +218 -0
  21. package/dist/dashboard/index.html +558 -0
  22. package/dist/db-adapter-DEWEFNIV.js +7 -0
  23. package/dist/dynamodb-CCGL2E77.js +426 -0
  24. package/dist/engine/index.js +1261 -0
  25. package/dist/index.js +522 -0
  26. package/dist/mongodb-ODTXIVPV.js +319 -0
  27. package/dist/mysql-RM3S2FV5.js +521 -0
  28. package/dist/postgres-LN7A6MGQ.js +518 -0
  29. package/dist/routes-2JEPIIKC.js +441 -0
  30. package/dist/routes-74ZLKJKP.js +399 -0
  31. package/dist/server.js +7 -0
  32. package/dist/sqlite-3K5YOZ4K.js +439 -0
  33. package/dist/turso-LDWODSDI.js +442 -0
  34. package/package.json +49 -0
  35. package/src/admin/routes.ts +331 -0
  36. package/src/auth/routes.ts +130 -0
  37. package/src/cli.ts +260 -0
  38. package/src/dashboard/index.html +558 -0
  39. package/src/db/adapter.ts +230 -0
  40. package/src/db/dynamodb.ts +456 -0
  41. package/src/db/factory.ts +51 -0
  42. package/src/db/mongodb.ts +360 -0
  43. package/src/db/mysql.ts +472 -0
  44. package/src/db/postgres.ts +479 -0
  45. package/src/db/sql-schema.ts +123 -0
  46. package/src/db/sqlite.ts +391 -0
  47. package/src/db/turso.ts +411 -0
  48. package/src/deploy/fly.ts +368 -0
  49. package/src/deploy/managed.ts +213 -0
  50. package/src/engine/activity.ts +474 -0
  51. package/src/engine/agent-config.ts +429 -0
  52. package/src/engine/agenticmail-bridge.ts +296 -0
  53. package/src/engine/approvals.ts +278 -0
  54. package/src/engine/db-adapter.ts +682 -0
  55. package/src/engine/db-schema.ts +335 -0
  56. package/src/engine/deployer.ts +595 -0
  57. package/src/engine/index.ts +134 -0
  58. package/src/engine/knowledge.ts +486 -0
  59. package/src/engine/lifecycle.ts +635 -0
  60. package/src/engine/openclaw-hook.ts +371 -0
  61. package/src/engine/routes.ts +528 -0
  62. package/src/engine/skills.ts +473 -0
  63. package/src/engine/tenant.ts +345 -0
  64. package/src/engine/tool-catalog.ts +189 -0
  65. package/src/index.ts +64 -0
  66. package/src/lib/resilience.ts +326 -0
  67. package/src/middleware/index.ts +286 -0
  68. package/src/server.ts +310 -0
  69. package/tsconfig.json +14 -0
@@ -0,0 +1,528 @@
1
+ /**
2
+ * Engine API Routes
3
+ *
4
+ * REST endpoints for the complete enterprise engine.
5
+ * Mounted at /api/engine/* on the enterprise server.
6
+ */
7
+
8
+ import { Hono } from 'hono';
9
+ import { PermissionEngine, BUILTIN_SKILLS, PRESET_PROFILES } from './skills.js';
10
+ import { AgentConfigGenerator, type AgentConfig } from './agent-config.js';
11
+ import { DeploymentEngine } from './deployer.js';
12
+ import { ApprovalEngine } from './approvals.js';
13
+ import { AgentLifecycleManager } from './lifecycle.js';
14
+ import { KnowledgeBaseEngine } from './knowledge.js';
15
+ import { TenantManager } from './tenant.js';
16
+ import { ActivityTracker } from './activity.js';
17
+
18
+ const engine = new Hono();
19
+
20
+ // ─── Shared Instances ───────────────────────────────────
21
+
22
+ const permissionEngine = new PermissionEngine();
23
+ const configGen = new AgentConfigGenerator();
24
+ const deployer = new DeploymentEngine();
25
+ const approvals = new ApprovalEngine();
26
+ const lifecycle = new AgentLifecycleManager({ permissions: permissionEngine });
27
+ const knowledgeBase = new KnowledgeBaseEngine();
28
+ const tenants = new TenantManager();
29
+ const activity = new ActivityTracker();
30
+
31
+ // Wire lifecycle events into activity tracker
32
+ lifecycle.onEvent((event) => {
33
+ activity.record({
34
+ agentId: event.agentId,
35
+ orgId: event.orgId,
36
+ type: event.type as any,
37
+ data: event.data,
38
+ });
39
+ });
40
+
41
+ // ─── Skills Catalog ─────────────────────────────────────
42
+
43
+ engine.get('/skills', (c) => {
44
+ return c.json({ skills: BUILTIN_SKILLS, categories: [...new Set(BUILTIN_SKILLS.map(s => s.category))], total: BUILTIN_SKILLS.length });
45
+ });
46
+
47
+ engine.get('/skills/by-category', (c) => {
48
+ const grouped: Record<string, typeof BUILTIN_SKILLS> = {};
49
+ for (const skill of BUILTIN_SKILLS) {
50
+ if (!grouped[skill.category]) grouped[skill.category] = [];
51
+ grouped[skill.category].push(skill);
52
+ }
53
+ return c.json({ categories: grouped });
54
+ });
55
+
56
+ engine.get('/skills/:id', (c) => {
57
+ const skill = BUILTIN_SKILLS.find(s => s.id === c.req.param('id'));
58
+ if (!skill) return c.json({ error: 'Skill not found' }, 404);
59
+ return c.json({ skill });
60
+ });
61
+
62
+ // ─── Permission Profiles ────────────────────────────────
63
+
64
+ engine.get('/profiles/presets', (c) => c.json({ presets: PRESET_PROFILES }));
65
+
66
+ engine.get('/profiles/:agentId', (c) => {
67
+ const profile = permissionEngine.getProfile(c.req.param('agentId'));
68
+ if (!profile) return c.json({ error: 'No profile assigned' }, 404);
69
+ return c.json({ profile });
70
+ });
71
+
72
+ engine.put('/profiles/:agentId', async (c) => {
73
+ const agentId = c.req.param('agentId');
74
+ const profile = await c.req.json();
75
+ profile.id = profile.id || agentId;
76
+ profile.updatedAt = new Date().toISOString();
77
+ if (!profile.createdAt) profile.createdAt = profile.updatedAt;
78
+ permissionEngine.setProfile(agentId, profile);
79
+ return c.json({ success: true, profile });
80
+ });
81
+
82
+ engine.post('/profiles/:agentId/apply-preset', async (c) => {
83
+ const agentId = c.req.param('agentId');
84
+ const { presetName } = await c.req.json();
85
+ const preset = PRESET_PROFILES.find(p => p.name === presetName);
86
+ if (!preset) return c.json({ error: 'Preset not found' }, 404);
87
+ const profile = { ...preset, id: agentId, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
88
+ permissionEngine.setProfile(agentId, profile as any);
89
+ return c.json({ success: true, profile });
90
+ });
91
+
92
+ engine.post('/permissions/check', async (c) => {
93
+ const { agentId, toolId } = await c.req.json();
94
+ return c.json(permissionEngine.checkPermission(agentId, toolId));
95
+ });
96
+
97
+ engine.get('/permissions/:agentId/tools', (c) => {
98
+ const tools = permissionEngine.getAvailableTools(c.req.param('agentId'));
99
+ return c.json({ tools, total: tools.length });
100
+ });
101
+
102
+ engine.get('/permissions/:agentId/policy', (c) => {
103
+ return c.json(permissionEngine.generateToolPolicy(c.req.param('agentId')));
104
+ });
105
+
106
+ // ─── Agent Lifecycle ────────────────────────────────────
107
+
108
+ engine.post('/agents', async (c) => {
109
+ const { orgId, config, createdBy } = await c.req.json();
110
+ try {
111
+ const agent = await lifecycle.createAgent(orgId, config, createdBy);
112
+ return c.json({ agent }, 201);
113
+ } catch (e: any) {
114
+ return c.json({ error: e.message }, 400);
115
+ }
116
+ });
117
+
118
+ engine.get('/agents', (c) => {
119
+ const orgId = c.req.query('orgId');
120
+ if (!orgId) return c.json({ error: 'orgId required' }, 400);
121
+ const agents = lifecycle.getAgentsByOrg(orgId);
122
+ return c.json({ agents, total: agents.length });
123
+ });
124
+
125
+ engine.get('/agents/:id', (c) => {
126
+ const agent = lifecycle.getAgent(c.req.param('id'));
127
+ if (!agent) return c.json({ error: 'Agent not found' }, 404);
128
+ return c.json({ agent });
129
+ });
130
+
131
+ engine.patch('/agents/:id/config', async (c) => {
132
+ const { updates, updatedBy } = await c.req.json();
133
+ try {
134
+ const agent = await lifecycle.updateConfig(c.req.param('id'), updates, updatedBy);
135
+ return c.json({ agent });
136
+ } catch (e: any) {
137
+ return c.json({ error: e.message }, 400);
138
+ }
139
+ });
140
+
141
+ engine.post('/agents/:id/deploy', async (c) => {
142
+ const { deployedBy } = await c.req.json();
143
+ try {
144
+ const agent = await lifecycle.deploy(c.req.param('id'), deployedBy);
145
+ return c.json({ agent });
146
+ } catch (e: any) {
147
+ return c.json({ error: e.message }, 400);
148
+ }
149
+ });
150
+
151
+ engine.post('/agents/:id/stop', async (c) => {
152
+ const { stoppedBy, reason } = await c.req.json();
153
+ try {
154
+ const agent = await lifecycle.stop(c.req.param('id'), stoppedBy, reason);
155
+ return c.json({ agent });
156
+ } catch (e: any) {
157
+ return c.json({ error: e.message }, 400);
158
+ }
159
+ });
160
+
161
+ engine.post('/agents/:id/restart', async (c) => {
162
+ const { restartedBy } = await c.req.json();
163
+ try {
164
+ const agent = await lifecycle.restart(c.req.param('id'), restartedBy);
165
+ return c.json({ agent });
166
+ } catch (e: any) {
167
+ return c.json({ error: e.message }, 400);
168
+ }
169
+ });
170
+
171
+ engine.post('/agents/:id/hot-update', async (c) => {
172
+ const { updates, updatedBy } = await c.req.json();
173
+ try {
174
+ const agent = await lifecycle.hotUpdate(c.req.param('id'), updates, updatedBy);
175
+ return c.json({ agent });
176
+ } catch (e: any) {
177
+ return c.json({ error: e.message }, 400);
178
+ }
179
+ });
180
+
181
+ engine.delete('/agents/:id', async (c) => {
182
+ const { destroyedBy } = await c.req.json().catch(() => ({ destroyedBy: 'unknown' }));
183
+ try {
184
+ await lifecycle.destroy(c.req.param('id'), destroyedBy);
185
+ return c.json({ success: true });
186
+ } catch (e: any) {
187
+ return c.json({ error: e.message }, 400);
188
+ }
189
+ });
190
+
191
+ engine.get('/agents/:id/usage', (c) => {
192
+ const agent = lifecycle.getAgent(c.req.param('id'));
193
+ if (!agent) return c.json({ error: 'Agent not found' }, 404);
194
+ return c.json({ usage: agent.usage, health: agent.health, state: agent.state });
195
+ });
196
+
197
+ engine.get('/usage/:orgId', (c) => {
198
+ return c.json(lifecycle.getOrgUsage(c.req.param('orgId')));
199
+ });
200
+
201
+ // ─── Config Generation ──────────────────────────────────
202
+
203
+ engine.post('/config/workspace', async (c) => {
204
+ const config: AgentConfig = await c.req.json();
205
+ return c.json({ files: configGen.generateWorkspace(config) });
206
+ });
207
+
208
+ engine.post('/config/gateway', async (c) => {
209
+ const config: AgentConfig = await c.req.json();
210
+ return c.json({ config: configGen.generateGatewayConfig(config) });
211
+ });
212
+
213
+ engine.post('/config/docker-compose', async (c) => {
214
+ const config: AgentConfig = await c.req.json();
215
+ return c.json({ compose: configGen.generateDockerCompose(config) });
216
+ });
217
+
218
+ engine.post('/config/systemd', async (c) => {
219
+ const config: AgentConfig = await c.req.json();
220
+ return c.json({ unit: configGen.generateSystemdUnit(config) });
221
+ });
222
+
223
+ engine.post('/config/deploy-script', async (c) => {
224
+ const config: AgentConfig = await c.req.json();
225
+ return c.json({ script: configGen.generateVPSDeployScript(config) });
226
+ });
227
+
228
+ // ─── Knowledge Base ─────────────────────────────────────
229
+
230
+ engine.post('/knowledge-bases', async (c) => {
231
+ const { orgId, name, description, agentIds, config } = await c.req.json();
232
+ const kb = knowledgeBase.createKnowledgeBase(orgId, { name, description, agentIds, config });
233
+ return c.json({ knowledgeBase: kb }, 201);
234
+ });
235
+
236
+ engine.get('/knowledge-bases', (c) => {
237
+ const orgId = c.req.query('orgId');
238
+ const agentId = c.req.query('agentId');
239
+ if (agentId) return c.json({ knowledgeBases: knowledgeBase.getKnowledgeBasesForAgent(agentId) });
240
+ if (orgId) return c.json({ knowledgeBases: knowledgeBase.getKnowledgeBasesByOrg(orgId) });
241
+ return c.json({ error: 'orgId or agentId required' }, 400);
242
+ });
243
+
244
+ engine.get('/knowledge-bases/:id', (c) => {
245
+ const kb = knowledgeBase.getKnowledgeBase(c.req.param('id'));
246
+ if (!kb) return c.json({ error: 'Knowledge base not found' }, 404);
247
+ return c.json({ knowledgeBase: kb });
248
+ });
249
+
250
+ engine.post('/knowledge-bases/:id/documents', async (c) => {
251
+ const { name, content, sourceType, sourceUrl, mimeType, metadata } = await c.req.json();
252
+ try {
253
+ const doc = await knowledgeBase.ingestDocument(c.req.param('id'), { name, content, sourceType, sourceUrl, mimeType, metadata });
254
+ return c.json({ document: doc }, 201);
255
+ } catch (e: any) {
256
+ return c.json({ error: e.message }, 400);
257
+ }
258
+ });
259
+
260
+ engine.delete('/knowledge-bases/:kbId/documents/:docId', (c) => {
261
+ const ok = knowledgeBase.deleteDocument(c.req.param('kbId'), c.req.param('docId'));
262
+ return ok ? c.json({ success: true }) : c.json({ error: 'Not found' }, 404);
263
+ });
264
+
265
+ engine.post('/knowledge-bases/search', async (c) => {
266
+ const { agentId, query, kbIds, maxResults, minScore } = await c.req.json();
267
+ const results = await knowledgeBase.search(agentId, query, { kbIds, maxResults, minScore });
268
+ return c.json({ results, total: results.length });
269
+ });
270
+
271
+ engine.post('/knowledge-bases/context', async (c) => {
272
+ const { agentId, query, maxTokens } = await c.req.json();
273
+ const context = await knowledgeBase.getContext(agentId, query, maxTokens);
274
+ return c.json({ context });
275
+ });
276
+
277
+ engine.delete('/knowledge-bases/:id', (c) => {
278
+ const ok = knowledgeBase.deleteKnowledgeBase(c.req.param('id'));
279
+ return ok ? c.json({ success: true }) : c.json({ error: 'Not found' }, 404);
280
+ });
281
+
282
+ // ─── Organizations (Tenants) ────────────────────────────
283
+
284
+ engine.post('/orgs', async (c) => {
285
+ const { name, slug, plan, adminEmail, settings } = await c.req.json();
286
+ try {
287
+ const org = tenants.createOrg({ name, slug, plan: plan || 'free', adminEmail, settings });
288
+ return c.json({ org }, 201);
289
+ } catch (e: any) {
290
+ return c.json({ error: e.message }, 400);
291
+ }
292
+ });
293
+
294
+ engine.get('/orgs', (c) => c.json({ orgs: tenants.listOrgs() }));
295
+
296
+ engine.get('/orgs/:id', (c) => {
297
+ const org = tenants.getOrg(c.req.param('id'));
298
+ if (!org) return c.json({ error: 'Org not found' }, 404);
299
+ return c.json({ org });
300
+ });
301
+
302
+ engine.get('/orgs/slug/:slug', (c) => {
303
+ const org = tenants.getOrgBySlug(c.req.param('slug'));
304
+ if (!org) return c.json({ error: 'Org not found' }, 404);
305
+ return c.json({ org });
306
+ });
307
+
308
+ engine.post('/orgs/:id/check-limit', async (c) => {
309
+ const { resource, currentCount } = await c.req.json();
310
+ try {
311
+ return c.json(tenants.checkLimit(c.req.param('id'), resource, currentCount));
312
+ } catch (e: any) {
313
+ return c.json({ error: e.message }, 400);
314
+ }
315
+ });
316
+
317
+ engine.post('/orgs/:id/check-feature', async (c) => {
318
+ const { feature } = await c.req.json();
319
+ return c.json({ allowed: tenants.hasFeature(c.req.param('id'), feature) });
320
+ });
321
+
322
+ engine.post('/orgs/:id/change-plan', async (c) => {
323
+ const { plan } = await c.req.json();
324
+ try {
325
+ const org = tenants.changePlan(c.req.param('id'), plan);
326
+ return c.json({ org });
327
+ } catch (e: any) {
328
+ return c.json({ error: e.message }, 400);
329
+ }
330
+ });
331
+
332
+ // ─── Approvals ──────────────────────────────────────────
333
+
334
+ engine.get('/approvals/pending', (c) => {
335
+ const agentId = c.req.query('agentId');
336
+ const requests = approvals.getPendingRequests(agentId || undefined);
337
+ return c.json({ requests, total: requests.length });
338
+ });
339
+
340
+ engine.get('/approvals/history', (c) => {
341
+ const agentId = c.req.query('agentId');
342
+ const limit = parseInt(c.req.query('limit') || '25');
343
+ const offset = parseInt(c.req.query('offset') || '0');
344
+ return c.json(approvals.getHistory({ agentId: agentId || undefined, limit, offset }));
345
+ });
346
+
347
+ engine.get('/approvals/:id', (c) => {
348
+ const request = approvals.getRequest(c.req.param('id'));
349
+ if (!request) return c.json({ error: 'Not found' }, 404);
350
+ return c.json({ request });
351
+ });
352
+
353
+ engine.post('/approvals/:id/decide', async (c) => {
354
+ const { action, reason, by } = await c.req.json();
355
+ const result = approvals.decide(c.req.param('id'), { action, reason, by });
356
+ if (!result) return c.json({ error: 'Not found or already decided' }, 404);
357
+ return c.json({ request: result });
358
+ });
359
+
360
+ engine.get('/approvals/policies', (c) => c.json({ policies: approvals.getPolicies() }));
361
+
362
+ engine.post('/approvals/policies', async (c) => {
363
+ const policy = await c.req.json();
364
+ policy.id = policy.id || crypto.randomUUID();
365
+ approvals.addPolicy(policy);
366
+ return c.json({ success: true, policy });
367
+ });
368
+
369
+ engine.delete('/approvals/policies/:id', (c) => {
370
+ approvals.removePolicy(c.req.param('id'));
371
+ return c.json({ success: true });
372
+ });
373
+
374
+ // ─── Activity & Monitoring ──────────────────────────────
375
+
376
+ engine.get('/activity/events', (c) => {
377
+ const events = activity.getEvents({
378
+ agentId: c.req.query('agentId') || undefined,
379
+ orgId: c.req.query('orgId') || undefined,
380
+ since: c.req.query('since') || undefined,
381
+ limit: parseInt(c.req.query('limit') || '50'),
382
+ });
383
+ return c.json({ events, total: events.length });
384
+ });
385
+
386
+ engine.get('/activity/tool-calls', (c) => {
387
+ const calls = activity.getToolCalls({
388
+ agentId: c.req.query('agentId') || undefined,
389
+ orgId: c.req.query('orgId') || undefined,
390
+ toolId: c.req.query('toolId') || undefined,
391
+ limit: parseInt(c.req.query('limit') || '50'),
392
+ });
393
+ return c.json({ toolCalls: calls, total: calls.length });
394
+ });
395
+
396
+ engine.get('/activity/conversation/:sessionId', (c) => {
397
+ const entries = activity.getConversation(c.req.param('sessionId'));
398
+ return c.json({ entries, total: entries.length });
399
+ });
400
+
401
+ engine.get('/activity/timeline/:agentId/:date', (c) => {
402
+ const timeline = activity.getTimeline(c.req.param('agentId'), c.req.param('date'));
403
+ return c.json({ timeline });
404
+ });
405
+
406
+ engine.get('/activity/stats', (c) => {
407
+ const orgId = c.req.query('orgId');
408
+ return c.json(activity.getStats(orgId || undefined));
409
+ });
410
+
411
+ // SSE endpoint for real-time events
412
+ engine.get('/activity/stream', (c) => {
413
+ const orgId = c.req.query('orgId');
414
+ const agentId = c.req.query('agentId');
415
+
416
+ const stream = new ReadableStream({
417
+ start(controller) {
418
+ const encoder = new TextEncoder();
419
+ const send = (data: string) => {
420
+ try { controller.enqueue(encoder.encode(`data: ${data}\n\n`)); }
421
+ catch { unsubscribe(); }
422
+ };
423
+
424
+ // Send heartbeat every 30s
425
+ const heartbeat = setInterval(() => send(JSON.stringify({ type: 'heartbeat' })), 30_000);
426
+
427
+ const unsubscribe = activity.subscribe((event) => {
428
+ if (orgId && event.orgId !== orgId) return;
429
+ if (agentId && event.agentId !== agentId) return;
430
+ send(JSON.stringify(event));
431
+ });
432
+
433
+ // Cleanup on close
434
+ c.req.raw.signal.addEventListener('abort', () => {
435
+ unsubscribe();
436
+ clearInterval(heartbeat);
437
+ });
438
+ },
439
+ });
440
+
441
+ return new Response(stream, {
442
+ headers: {
443
+ 'Content-Type': 'text/event-stream',
444
+ 'Cache-Control': 'no-cache',
445
+ 'Connection': 'keep-alive',
446
+ },
447
+ });
448
+ });
449
+
450
+ // ─── Engine Stats (Dashboard Overview) ──────────────────
451
+
452
+ engine.get('/stats/:orgId', (c) => {
453
+ const orgId = c.req.param('orgId');
454
+ const org = tenants.getOrg(orgId);
455
+ const agents = lifecycle.getAgentsByOrg(orgId);
456
+ const orgUsage = lifecycle.getOrgUsage(orgId);
457
+ const realTimeStats = activity.getStats(orgId);
458
+
459
+ return c.json({
460
+ org: org ? { name: org.name, plan: org.plan, limits: org.limits, usage: org.usage } : null,
461
+ agents: {
462
+ total: agents.length,
463
+ byState: agents.reduce((acc, a) => { acc[a.state] = (acc[a.state] || 0) + 1; return acc; }, {} as Record<string, number>),
464
+ },
465
+ usage: orgUsage,
466
+ realTime: realTimeStats,
467
+ });
468
+ });
469
+
470
+ // ─── Dynamic Tables ────────────────────────────────────
471
+
472
+ /**
473
+ * POST /schema/tables — Create a dynamic table at runtime.
474
+ * Body: { name, sql, postgres?, mysql?, indexes? }
475
+ * Tables are prefixed with `ext_` automatically.
476
+ *
477
+ * Requires an EngineDatabase instance wired in via setEngineDb().
478
+ */
479
+ let _engineDb: import('./db-adapter.js').EngineDatabase | null = null;
480
+
481
+ export function setEngineDb(db: import('./db-adapter.js').EngineDatabase) {
482
+ _engineDb = db;
483
+ }
484
+
485
+ engine.post('/schema/tables', async (c) => {
486
+ if (!_engineDb) return c.json({ error: 'Engine database not initialized' }, 503);
487
+ try {
488
+ const def = await c.req.json();
489
+ if (!def.name || !def.sql) return c.json({ error: 'name and sql are required' }, 400);
490
+ await _engineDb.createDynamicTable(def);
491
+ const prefixed = def.name.startsWith('ext_') ? def.name : `ext_${def.name}`;
492
+ return c.json({ ok: true, table: prefixed });
493
+ } catch (e: any) {
494
+ return c.json({ error: e.message }, 500);
495
+ }
496
+ });
497
+
498
+ engine.get('/schema/tables', async (c) => {
499
+ if (!_engineDb) return c.json({ error: 'Engine database not initialized' }, 503);
500
+ const tables = await _engineDb.listDynamicTables();
501
+ return c.json({ tables });
502
+ });
503
+
504
+ engine.post('/schema/query', async (c) => {
505
+ if (!_engineDb) return c.json({ error: 'Engine database not initialized' }, 503);
506
+ try {
507
+ const { sql, params } = await c.req.json();
508
+ if (!sql) return c.json({ error: 'sql is required' }, 400);
509
+ // Only allow SELECT on ext_ tables for safety
510
+ const trimmed = sql.trim().toUpperCase();
511
+ if (trimmed.startsWith('SELECT')) {
512
+ const rows = await _engineDb.query(sql, params);
513
+ return c.json({ rows });
514
+ } else {
515
+ // INSERT/UPDATE/DELETE — verify it targets ext_ tables
516
+ if (!trimmed.includes('EXT_')) {
517
+ return c.json({ error: 'Mutations only allowed on ext_* tables' }, 403);
518
+ }
519
+ await _engineDb.execute(sql, params);
520
+ return c.json({ ok: true });
521
+ }
522
+ } catch (e: any) {
523
+ return c.json({ error: e.message }, 500);
524
+ }
525
+ });
526
+
527
+ export { engine as engineRoutes };
528
+ export { permissionEngine, configGen, deployer, approvals, lifecycle, knowledgeBase, tenants, activity };