@agentlensai/server 0.13.0 → 0.14.0

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 (82) hide show
  1. package/dist/app.d.ts +27 -0
  2. package/dist/app.d.ts.map +1 -0
  3. package/dist/app.js +178 -0
  4. package/dist/app.js.map +1 -0
  5. package/dist/config.d.ts +2 -6
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +13 -10
  8. package/dist/config.js.map +1 -1
  9. package/dist/health.d.ts +22 -0
  10. package/dist/health.d.ts.map +1 -0
  11. package/dist/health.js +34 -0
  12. package/dist/health.js.map +1 -0
  13. package/dist/index.d.ts +17 -31
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +28 -502
  16. package/dist/index.js.map +1 -1
  17. package/dist/lib/api-schema.d.ts +126 -0
  18. package/dist/lib/api-schema.d.ts.map +1 -0
  19. package/dist/lib/api-schema.js +69 -0
  20. package/dist/lib/api-schema.js.map +1 -0
  21. package/dist/lib/api-version.d.ts +21 -0
  22. package/dist/lib/api-version.d.ts.map +1 -0
  23. package/dist/lib/api-version.js +36 -0
  24. package/dist/lib/api-version.js.map +1 -0
  25. package/dist/lib/lore-client.d.ts +64 -112
  26. package/dist/lib/lore-client.d.ts.map +1 -1
  27. package/dist/lib/lore-client.js +120 -155
  28. package/dist/lib/lore-client.js.map +1 -1
  29. package/dist/routes/agents.d.ts.map +1 -1
  30. package/dist/routes/agents.js +73 -0
  31. package/dist/routes/agents.js.map +1 -1
  32. package/dist/routes/alerts.d.ts.map +1 -1
  33. package/dist/routes/alerts.js +13 -36
  34. package/dist/routes/alerts.js.map +1 -1
  35. package/dist/routes/analytics.d.ts.map +1 -1
  36. package/dist/routes/analytics.js +93 -0
  37. package/dist/routes/analytics.js.map +1 -1
  38. package/dist/routes/api-version.d.ts +9 -0
  39. package/dist/routes/api-version.d.ts.map +1 -0
  40. package/dist/routes/api-version.js +19 -0
  41. package/dist/routes/api-version.js.map +1 -0
  42. package/dist/routes/audit-verify.d.ts +3 -2
  43. package/dist/routes/audit-verify.d.ts.map +1 -1
  44. package/dist/routes/audit-verify.js +91 -27
  45. package/dist/routes/audit-verify.js.map +1 -1
  46. package/dist/routes/cost-budgets.d.ts.map +1 -1
  47. package/dist/routes/cost-budgets.js +19 -36
  48. package/dist/routes/cost-budgets.js.map +1 -1
  49. package/dist/routes/guardrails.d.ts.map +1 -1
  50. package/dist/routes/guardrails.js +121 -37
  51. package/dist/routes/guardrails.js.map +1 -1
  52. package/dist/routes/helpers.d.ts +27 -0
  53. package/dist/routes/helpers.d.ts.map +1 -0
  54. package/dist/routes/helpers.js +46 -0
  55. package/dist/routes/helpers.js.map +1 -0
  56. package/dist/routes/lore-proxy.d.ts +8 -6
  57. package/dist/routes/lore-proxy.d.ts.map +1 -1
  58. package/dist/routes/lore-proxy.js +39 -193
  59. package/dist/routes/lore-proxy.js.map +1 -1
  60. package/dist/routes/mcp-policies.d.ts +40 -0
  61. package/dist/routes/mcp-policies.d.ts.map +1 -0
  62. package/dist/routes/mcp-policies.js +200 -0
  63. package/dist/routes/mcp-policies.js.map +1 -0
  64. package/dist/routes/optimization-advisor.d.ts +13 -0
  65. package/dist/routes/optimization-advisor.d.ts.map +1 -0
  66. package/dist/routes/optimization-advisor.js +42 -0
  67. package/dist/routes/optimization-advisor.js.map +1 -0
  68. package/dist/routes/recall.d.ts.map +1 -1
  69. package/dist/routes/recall.js +7 -3
  70. package/dist/routes/recall.js.map +1 -1
  71. package/dist/routes/registration.d.ts +27 -0
  72. package/dist/routes/registration.d.ts.map +1 -0
  73. package/dist/routes/registration.js +311 -0
  74. package/dist/routes/registration.js.map +1 -0
  75. package/dist/routes/replay.d.ts.map +1 -1
  76. package/dist/routes/replay.js +51 -0
  77. package/dist/routes/replay.js.map +1 -1
  78. package/dist/services/optimization-advisor.d.ts +37 -0
  79. package/dist/services/optimization-advisor.d.ts.map +1 -0
  80. package/dist/services/optimization-advisor.js +239 -0
  81. package/dist/services/optimization-advisor.js.map +1 -0
  82. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,92 +1,32 @@
1
1
  /**
2
2
  * @agentlensai/server — Hono HTTP API server and event storage
3
3
  *
4
- * Exports:
5
- * - createApp(store, config?) factory that returns a configured Hono app
6
- * - startServer() standalone entry point that creates DB + starts listening
4
+ * Thin entry point: re-exports createApp from app.ts and startServer below.
5
+ * The heavy lifting lives in:
6
+ * - app.ts Hono app creation, middleware setup
7
+ * - routes/registration.ts — route mounting
8
+ * - health.ts — inline health check endpoint
7
9
  */
8
- import { Hono } from 'hono';
9
- import { OpenAPIHono } from '@hono/zod-openapi';
10
- import { apiReference } from '@scalar/hono-api-reference';
11
- import { cors } from 'hono/cors';
12
- import { logger } from 'hono/logger';
13
10
  import { serve } from '@hono/node-server';
14
- import { serveStatic } from '@hono/node-server/serve-static';
15
- import { readFileSync, existsSync } from 'node:fs';
16
- import { resolve, dirname } from 'node:path';
17
- import { fileURLToPath } from 'node:url';
18
- import { BearerAuthScheme } from './schemas/common.js';
19
11
  import { getConfig, validateConfig } from './config.js';
20
- import { authMiddleware } from './middleware/auth.js';
21
- import { unifiedAuthMiddleware } from './middleware/unified-auth.js';
22
- import { requireCategory, requireMethodCategory, requireCategoryByMethod } from './middleware/rbac.js';
23
- import { otlpAuthRequired as otlpAuthRequiredError, otlpInvalidToken } from './middleware/auth-errors.js';
24
- import { securityHeadersMiddleware } from './middleware/security-headers.js';
25
- import { sanitizeErrorMessage, getErrorStatus } from './lib/error-sanitizer.js';
26
- import { buildCorsOptions } from './middleware/cors-config.js';
27
- import { apiKeysRoutes } from './routes/api-keys.js';
28
- import { eventsRoutes } from './routes/events.js';
29
- import { sessionsRoutes } from './routes/sessions.js';
30
- import { agentsRoutes } from './routes/agents.js';
31
- import { statsRoutes } from './routes/stats.js';
32
- import { configRoutes } from './routes/config.js';
33
- import { alertsRoutes } from './routes/alerts.js';
34
- import { notificationRoutes } from './routes/notifications.js';
35
- import { NotificationChannelRepository } from './db/repositories/notification-channel-repository.js';
36
- import { NotificationRouter } from './lib/notifications/router.js';
37
- import { ingestRoutes } from './routes/ingest.js';
38
- import { analyticsRoutes } from './routes/analytics.js';
39
- import { streamRoutes } from './routes/stream.js';
40
- import { reflectRoutes } from './routes/reflect.js';
41
- import { recallRoutes } from './routes/recall.js';
42
- import { contextRoutes } from './routes/context.js';
43
- import { optimizeRoutes } from './routes/optimize.js';
44
- import { healthRoutes } from './routes/health.js';
45
- import { diagnoseRoutes } from './routes/diagnose.js';
46
- import { registerReplayRoutes } from './routes/replay.js';
47
- import { benchmarkRoutes } from './routes/benchmarks.js';
48
- import { promptRoutes } from './routes/prompts.js';
49
- import { guardrailRoutes } from './routes/guardrails.js';
50
- import { evalRoutes } from './routes/eval.js';
51
- import { capabilityRoutes } from './routes/capabilities.js';
52
- import { capabilityTopRoutes } from './routes/capabilities-top.js';
53
- import { discoveryRoutes } from './routes/discovery.js';
54
- import { delegationRoutes } from './routes/delegation.js';
55
- import { delegationTopRoutes } from './routes/delegations-top.js';
56
- import { trustRoutes } from './routes/trust.js';
57
- import { LocalPoolTransport } from './services/delegation-service.js';
58
- import { loreProxyRoutes, loreCommunityProxyRoutes } from './routes/lore-proxy.js';
59
- import { createLoreAdapter } from './lib/lore-client.js';
60
- import { meshProxyRoutes } from './routes/mesh-proxy.js';
61
- import { RemoteMeshAdapter } from './lib/mesh-client.js';
62
- import { otlpRoutes } from './routes/otlp.js';
63
- import { authRoutes } from './routes/auth.js';
64
- import { authRateLimit, apiRateLimit } from './middleware/rate-limit.js';
65
- import { apiBodyLimit } from './middleware/body-limit.js';
66
- import { auditRoutes } from './routes/audit.js';
67
- import { cloudOrgRoutes } from './cloud/routes/index.js';
68
- import { auditVerifyRoutes } from './routes/audit-verify.js';
69
- import { complianceRoutes } from './routes/compliance.js';
70
- import { createAuditLogger, cleanupAuditLogs } from './lib/audit.js';
71
- import { auditMiddleware } from './middleware/audit.js';
72
- import { GuardrailEngine } from './lib/guardrails/engine.js';
73
- import { GuardrailStore } from './db/guardrail-store.js';
74
- import { ContentGuardrailEngine } from './lib/guardrails/content-engine.js';
75
- import { setAgentStore, setNotificationRouter } from './lib/guardrails/actions.js';
76
- import { BudgetEngine } from './lib/budget-engine.js';
77
- import { CostAnomalyDetector } from './lib/cost-anomaly-detector.js';
78
- import { costBudgetRoutes } from './routes/cost-budgets.js';
79
12
  import { createDb } from './db/index.js';
80
13
  import { runMigrations } from './db/migrate.js';
81
14
  import { SqliteEventStore } from './db/sqlite-store.js';
82
15
  import { AlertEngine } from './lib/alert-engine.js';
83
- import { eventBus } from './lib/event-bus.js';
84
16
  import { EmbeddingWorker } from './lib/embeddings/worker.js';
85
17
  import { EmbeddingStore } from './db/embedding-store.js';
86
- import { SessionSummaryStore } from './db/session-summary-store.js';
18
+ import { NotificationChannelRepository } from './db/repositories/notification-channel-repository.js';
19
+ import { NotificationRouter } from './lib/notifications/router.js';
20
+ import { GuardrailEngine } from './lib/guardrails/engine.js';
21
+ import { setAgentStore, setNotificationRouter } from './lib/guardrails/actions.js';
22
+ import { BudgetEngine } from './lib/budget-engine.js';
23
+ import { CostAnomalyDetector } from './lib/cost-anomaly-detector.js';
24
+ import { cleanupAuditLogs } from './lib/audit.js';
87
25
  import { createLogger } from './lib/logger.js';
26
+ // Re-export createApp from the new module
27
+ export { createApp } from './app.js';
88
28
  const log = createLogger('Server');
89
- // Re-export everything consumers may need
29
+ // ─── Re-exports (preserve public API) ──────────────────────
90
30
  export { getConfig, validateConfig } from './config.js';
91
31
  export { authMiddleware, hashApiKey } from './middleware/auth.js';
92
32
  export { buildCorsOptions } from './middleware/cors-config.js';
@@ -122,431 +62,24 @@ export { apiBodyLimit } from './middleware/body-limit.js';
122
62
  export { auditMiddleware } from './middleware/audit.js';
123
63
  export { healthRoutes, registerHealthRoutes } from './routes/health.js';
124
64
  export { ContextRetriever } from './lib/context/retrieval.js';
125
- export { loreProxyRoutes, loreCommunityProxyRoutes } from './routes/lore-proxy.js';
126
- export { createLoreAdapter, RemoteLoreAdapter, LocalLoreAdapter, LoreError } from './lib/lore-client.js';
65
+ export { loreProxyRoutes } from './routes/lore-proxy.js';
66
+ export { createLoreAdapter, LoreReadAdapter, LoreError } from './lib/lore-client.js';
127
67
  export { meshProxyRoutes } from './routes/mesh-proxy.js';
128
68
  export { RemoteMeshAdapter, MeshError } from './lib/mesh-client.js';
129
69
  export { otlpRoutes } from './routes/otlp.js';
130
70
  export { guardrailRoutes } from './routes/guardrails.js';
71
+ export { mcpPolicyRoutes } from './routes/mcp-policies.js';
131
72
  export { GuardrailEngine } from './lib/guardrails/engine.js';
132
73
  export { GuardrailStore } from './db/guardrail-store.js';
133
74
  export { BudgetEngine } from './lib/budget-engine.js';
134
75
  export { CostAnomalyDetector } from './lib/cost-anomaly-detector.js';
135
76
  export { CostBudgetStore } from './db/cost-budget-store.js';
136
77
  export { costBudgetRoutes } from './routes/cost-budgets.js';
137
- // ─── Dashboard SPA helpers ───────────────────────────────────
138
- /**
139
- * Resolve the dashboard dist/ directory path.
140
- * Looks for the built dashboard relative to this file's package:
141
- * ../dashboard/dist/ (monorepo sibling)
142
- *
143
- * Returns relative path suitable for serveStatic root, or null if not found.
144
- */
145
- function getDashboardRoot() {
146
- const candidates = [
147
- // When running from packages/server/dist/ or packages/server/src/
148
- resolve(dirname(fileURLToPath(import.meta.url)), '../../dashboard/dist'),
149
- // Fallback: env var override
150
- process.env['DASHBOARD_PATH'] ?? '',
151
- ].filter(Boolean);
152
- for (const candidate of candidates) {
153
- if (existsSync(resolve(candidate, 'index.html'))) {
154
- return candidate;
155
- }
156
- }
157
- return null;
158
- }
159
- /**
160
- * Read the dashboard index.html for SPA fallback (non-API routes).
161
- * Cached after first read.
162
- */
163
- let cachedIndexHtml;
164
- function getDashboardIndexHtml() {
165
- if (cachedIndexHtml !== undefined)
166
- return cachedIndexHtml;
167
- const root = getDashboardRoot();
168
- if (!root) {
169
- cachedIndexHtml = null;
170
- return null;
171
- }
172
- const indexPath = resolve(root, 'index.html');
173
- try {
174
- cachedIndexHtml = readFileSync(indexPath, 'utf-8');
175
- return cachedIndexHtml;
176
- }
177
- catch {
178
- cachedIndexHtml = null;
179
- return null;
180
- }
181
- }
182
- /**
183
- * Create a configured Hono app with all routes and middleware.
184
- *
185
- * @param store - IEventStore implementation for data access
186
- * @param config - Optional partial config override (defaults from env)
187
- */
188
- export async function createApp(store, config) {
189
- const resolvedConfig = { ...getConfig(), ...config };
190
- const app = new OpenAPIHono({
191
- defaultHook: (result, c) => {
192
- if (!result.success) {
193
- return c.json({
194
- error: 'Validation failed',
195
- status: 400,
196
- details: result.error.issues.map((i) => ({
197
- path: i.path.map(String).join('.'),
198
- message: i.message,
199
- })),
200
- }, 400);
201
- }
202
- },
203
- });
204
- // Register Bearer auth security scheme for OpenAPI [F13-S1]
205
- app.openAPIRegistry.registerComponent('securitySchemes', 'Bearer', BearerAuthScheme);
206
- // ─── Security headers (position 1 — must be first) ────
207
- app.use('*', securityHeadersMiddleware());
208
- // ─── Global error handler ──────────────────────────────
209
- app.onError((err, c) => {
210
- const status = getErrorStatus(err);
211
- if (status >= 500) {
212
- log.error('Unhandled error', { error: err instanceof Error ? err.message : String(err) });
213
- }
214
- const message = sanitizeErrorMessage(err);
215
- return c.json({ error: message, status }, status);
216
- });
217
- // ─── 404 handler — API routes return JSON, others get SPA fallback ──
218
- app.notFound((c) => {
219
- const path = new URL(c.req.url).pathname;
220
- if (path.startsWith('/api/')) {
221
- return c.json({ error: 'Not found', status: 404 }, 404);
222
- }
223
- // Static asset requests (paths with file extensions) should 404,
224
- // not fall through to SPA index.html
225
- if (/\.\w{1,10}$/.test(path)) {
226
- return c.json({ error: 'Not found', status: 404 }, 404);
227
- }
228
- // SPA fallback: serve index.html for client-side routing
229
- const indexHtml = getDashboardIndexHtml();
230
- if (indexHtml) {
231
- return c.html(indexHtml);
232
- }
233
- return c.json({ error: 'Not found', status: 404 }, 404);
234
- });
235
- // ─── Middleware on /api/* ──────────────────────────────
236
- app.use('/api/*', cors(buildCorsOptions({
237
- corsOrigins: resolvedConfig.corsOrigins ?? resolvedConfig.corsOrigin,
238
- nodeEnv: process.env['NODE_ENV'],
239
- })));
240
- app.use('/api/*', logger());
241
- // ─── SH-3: Body size limit (1MB default) ────────────────
242
- app.use('/api/*', apiBodyLimit);
243
- // ─── Rate limiting: API endpoints ──────────────────────
244
- app.use('/api/*', apiRateLimit);
245
- // ─── Health check (no auth) ────────────────────────────
246
- app.get('/api/health', async (c) => {
247
- const result = { status: 'ok', version: '0.1.0' };
248
- // DB health check — works for both SQLite and Postgres
249
- if (config?.pgSql) {
250
- const { postgresHealthCheck } = await import('./db/index.js');
251
- result.db = await postgresHealthCheck(config.pgSql);
252
- }
253
- else if (config?.db) {
254
- // SQLite health check
255
- const start = performance.now();
256
- try {
257
- config.db.run((await import('drizzle-orm')).sql `SELECT 1`);
258
- result.db = { ok: true, latencyMs: Math.round(performance.now() - start) };
259
- }
260
- catch {
261
- result.db = { ok: false, latencyMs: Math.round(performance.now() - start) };
262
- }
263
- }
264
- return c.json(result);
265
- });
266
- // ─── Feature flags (no auth — dashboard needs before login) ──
267
- app.get('/api/config/features', (c) => {
268
- return c.json({ lore: resolvedConfig.loreEnabled, mesh: resolvedConfig.meshEnabled });
269
- });
270
- // ─── SSE stream (authenticates via Bearer header or ?token= query param) ──
271
- // Mounted before auth middleware — handles its own auth internally for EventSource compat.
272
- app.route('/api/stream', streamRoutes(config?.apiKeyLookup, resolvedConfig.authDisabled));
273
- // ─── Webhook ingest (no API key auth — uses HMAC signature verification) ──
274
- app.route('/api/events/ingest', ingestRoutes(store, {
275
- agentgateWebhookSecret: process.env['AGENTGATE_WEBHOOK_SECRET'],
276
- formbridgeWebhookSecret: process.env['FORMBRIDGE_WEBHOOK_SECRET'],
277
- }));
278
- // ─── Rate limiting: auth endpoints ─────────────────────
279
- app.use('/auth/*', authRateLimit);
280
- // ─── OIDC Auth routes (no API key auth — handles own auth) ──
281
- {
282
- const authDb = config?.db;
283
- if (authDb) {
284
- const { loadOidcConfig } = await import('agentkit-auth');
285
- const oidcConfig = loadOidcConfig();
286
- if (oidcConfig) {
287
- const jwtSecret = process.env['JWT_SECRET'];
288
- if (!jwtSecret && process.env['NODE_ENV'] === 'production') {
289
- throw new Error('JWT_SECRET must be set in production. Refusing to start with default secret.');
290
- }
291
- if (!jwtSecret) {
292
- log.warn('JWT_SECRET not set — using insecure default. Do NOT use in production.');
293
- }
294
- app.route('/auth', authRoutes(authDb, {
295
- oidcConfig,
296
- authConfig: {
297
- oidc: null,
298
- jwt: {
299
- secret: jwtSecret ?? 'dev-secret-change-me',
300
- accessTokenTtlSeconds: Number(process.env['JWT_ACCESS_TTL'] ?? 900),
301
- refreshTokenTtlSeconds: Number(process.env['JWT_REFRESH_TTL'] ?? 604800),
302
- },
303
- authDisabled: resolvedConfig.authDisabled,
304
- },
305
- }));
306
- }
307
- }
308
- }
309
- // ─── Fallback auth endpoints when auth is disabled ─────
310
- if (resolvedConfig.authDisabled) {
311
- app.get('/auth/me', (c) => c.json({ authMode: 'api-key-only' }, 200));
312
- }
313
- // ─── Auth middleware on protected routes [F2-S3] ───────
314
- // Fail-closed: single catch-all for /api/* with public routes registered above.
315
- const db = config?.db;
316
- if (!db && !resolvedConfig.authDisabled) {
317
- throw new Error('createApp() requires a `db` option when auth is enabled. ' +
318
- 'Either provide a database or set authDisabled: true.');
319
- }
320
- {
321
- const authLookup = config?.apiKeyLookup ?? db ?? null;
322
- const authConfig = {
323
- authDisabled: resolvedConfig.authDisabled,
324
- jwtSecret: process.env['JWT_SECRET'],
325
- };
326
- // ── Unified auth catch-all (replaces 40+ individual app.use calls) ──
327
- app.use('/api/*', unifiedAuthMiddleware(authLookup, authConfig));
328
- // ── RBAC enforcement per architecture §3.3 ──────────
329
- // Manage-level routes (owner, admin only)
330
- const manageGuard = requireCategory('manage');
331
- app.use('/api/keys/*', manageGuard);
332
- app.use('/api/keys', manageGuard);
333
- app.use('/api/audit/*', manageGuard);
334
- app.use('/api/audit', manageGuard);
335
- app.use('/api/compliance/*', manageGuard);
336
- app.use('/api/compliance', manageGuard);
337
- const configGuard = requireCategoryByMethod({ GET: 'read', PUT: 'manage', PATCH: 'manage' });
338
- app.use('/api/config/*', configGuard);
339
- app.use('/api/config', configGuard);
340
- const guardrailGuard = requireCategoryByMethod({ GET: 'read', POST: 'manage', PUT: 'manage', DELETE: 'manage' });
341
- app.use('/api/guardrails/*', guardrailGuard);
342
- app.use('/api/guardrails', guardrailGuard);
343
- // Default safety net: GET = read (all roles), mutations = write (member+)
344
- app.use('/api/*', requireMethodCategory());
345
- // ── Audit middleware (after auth — has access to auth context) ──
346
- if (db) {
347
- const auditLogger = createAuditLogger(db);
348
- app.use('/api/*', auditMiddleware(auditLogger));
349
- }
350
- }
351
- // ─── Routes ────────────────────────────────────────────
352
- if (db) {
353
- app.route('/api/keys', apiKeysRoutes(db));
354
- }
355
- app.route('/api/events', eventsRoutes(store, {
356
- embeddingWorker: config?.embeddingWorker ?? null,
357
- sessionSummaryStore: db ? new SessionSummaryStore(db) : null,
358
- }));
359
- // Replay route registered directly on main app BEFORE sessions sub-app
360
- // (otherwise the sessions sub-app catches /api/sessions/* first)
361
- registerReplayRoutes(app, store);
362
- app.route('/api/sessions', sessionsRoutes(store));
363
- // Health routes [F13-S2] — factory pattern, mounted at /api
364
- app.route('/api', healthRoutes(store, db));
365
- if (db) {
366
- const { app: discApp } = discoveryRoutes(db);
367
- app.route('/api/agents', discApp);
368
- app.route('/api/agents', capabilityRoutes(store, db));
369
- const poolTransport = new LocalPoolTransport();
370
- const { app: delApp } = delegationRoutes(db, poolTransport);
371
- app.route('/api/agents', delApp);
372
- const { app: trustApp } = trustRoutes(db);
373
- app.route('/api/agents', trustApp);
374
- }
375
- app.route('/api/agents', agentsRoutes(store));
376
- app.route('/api/stats', statsRoutes(store));
377
- if (db) {
378
- app.route('/api/config', configRoutes(db));
379
- app.route('/api/analytics', analyticsRoutes(store, db, config?.pgDb));
380
- }
381
- app.route('/api/alerts', alertsRoutes(store));
382
- // Feature 12: Notification channels
383
- const notifRepo = db ? new NotificationChannelRepository(db) : null;
384
- const notifRouter = notifRepo ? new NotificationRouter(notifRepo) : null;
385
- if (notifRepo && notifRouter) {
386
- app.route('/api/notifications', notificationRoutes(notifRepo, notifRouter));
387
- }
388
- let loreAdapter = null;
389
- if (resolvedConfig.loreEnabled) {
390
- try {
391
- loreAdapter = createLoreAdapter(resolvedConfig);
392
- }
393
- catch (err) {
394
- log.warn(`Lore adapter init failed: ${err instanceof Error ? err.message : err}`);
395
- }
396
- }
397
- if (loreAdapter) {
398
- app.route('/api/lessons', loreProxyRoutes(loreAdapter));
399
- }
400
- // ─── AI Diagnostics (Feature 18) ───────────────────────
401
- app.route('/api', diagnoseRoutes(store));
402
- // ─── Reflect / Pattern Analysis ────────────────────────
403
- app.route('/api/reflect', reflectRoutes(store));
404
- // ─── Optimize / Cost Recommendations ──────────────────
405
- app.route('/api/optimize', optimizeRoutes(store));
406
- // ─── Benchmarks / A/B Testing ─────────────────────────
407
- if (db) {
408
- app.route('/api/benchmarks', benchmarkRoutes(store, db));
409
- app.route('/api/prompts', promptRoutes(db));
410
- app.route('/api/eval', evalRoutes(db));
411
- }
412
- // ─── Guardrails / Proactive Guardrails ────────────────
413
- if (db) {
414
- const gStore = new GuardrailStore(db);
415
- const contentEngine = new ContentGuardrailEngine(gStore);
416
- app.route('/api/guardrails', guardrailRoutes(gStore, contentEngine));
417
- }
418
- // ─── Cost Budgets (Feature 5) ─────────────────────────
419
- if (db) {
420
- const cBudgetEngine = new BudgetEngine(store, db);
421
- const budgetStore = cBudgetEngine.getStore();
422
- app.route('/api/cost-budgets', costBudgetRoutes(budgetStore, store, cBudgetEngine));
423
- }
424
- // ─── Recall / Semantic Search ─────────────────────────
425
- {
426
- const embeddingService = config?.embeddingService ?? null;
427
- const embeddingStore = db ? new EmbeddingStore(db) : null;
428
- app.route('/api/recall', recallRoutes({ embeddingService, embeddingStore, eventStore: store }));
429
- // ─── Context / Cross-Session Retrieval ──────────────
430
- if (db) {
431
- app.route('/api/context', contextRoutes(store, {
432
- db,
433
- embeddingService,
434
- embeddingStore,
435
- }));
436
- }
437
- }
438
- // ─── Top-level Capabilities & Delegations (dashboard-facing) ──
439
- if (db) {
440
- app.route('/api/capabilities', capabilityTopRoutes(store, db));
441
- app.route('/api/delegations', delegationTopRoutes(db));
442
- // Discovery top-level (for /api/discovery?taskType=...)
443
- const { app: discTopApp } = discoveryRoutes(db);
444
- app.route('/api/discovery', discTopApp);
445
- }
446
- // ─── Audit Log (SH-2) ──────────────────────────────────
447
- if (db) {
448
- app.route('/api/audit', auditRoutes(db));
449
- app.route('/api/audit/verify', auditVerifyRoutes(db, resolvedConfig.auditSigningKey));
450
- app.route('/api/compliance', complianceRoutes(db, resolvedConfig.auditSigningKey, {
451
- retentionDays: resolvedConfig.retentionDays,
452
- }));
453
- }
454
- // ─── Cloud org routes with org access validation [F6-fix] ──
455
- if (config?.pgSql) {
456
- const cloudDb = {
457
- async query(sql, params) {
458
- const result = await config.pgSql.unsafe(sql, params);
459
- return { rows: Array.from(result) };
460
- },
461
- };
462
- app.route('/api/cloud/orgs', cloudOrgRoutes({ db: cloudDb }));
463
- }
464
- // ─── Community Sharing (Stories 4.1–4.3) ────────────────
465
- // Auth is handled by the unified catch-all above.
466
- if (loreAdapter) {
467
- app.route('/api/community', loreCommunityProxyRoutes(loreAdapter));
468
- }
469
- // ─── Mesh Proxy (agentkit-mesh) ─────────────────────────
470
- // Auth is handled by the unified catch-all above.
471
- if (resolvedConfig.meshEnabled && resolvedConfig.meshUrl) {
472
- const meshAdapter = new RemoteMeshAdapter(resolvedConfig.meshUrl);
473
- app.route('/api/mesh', meshProxyRoutes(meshAdapter));
474
- }
475
- // ─── OTLP HTTP Receiver [F2-S5] ─────────────────────────
476
- // Default: no auth (standard OTel convention). Opt-in via env vars.
477
- if (resolvedConfig.otlpAuthRequired) {
478
- // Full unified auth on OTLP endpoints
479
- const authLookup = config?.apiKeyLookup ?? db ?? null;
480
- app.use('/v1/*', unifiedAuthMiddleware(authLookup, {
481
- authDisabled: resolvedConfig.authDisabled,
482
- jwtSecret: process.env['JWT_SECRET'],
483
- }));
484
- }
485
- else if (resolvedConfig.otlpAuthToken) {
486
- // Simple bearer token check
487
- const { createMiddleware } = await import('hono/factory');
488
- app.use('/v1/*', createMiddleware(async (c, next) => {
489
- const authHeader = c.req.header('Authorization');
490
- if (!authHeader?.startsWith('Bearer ')) {
491
- return otlpAuthRequiredError(c);
492
- }
493
- const token = authHeader.slice(7);
494
- if (token !== resolvedConfig.otlpAuthToken) {
495
- return otlpInvalidToken(c);
496
- }
497
- return next();
498
- }));
499
- }
500
- app.route('/v1', otlpRoutes(store, resolvedConfig));
501
- // ─── Server Info (Feature 10, Story 10.1) ─────────────
502
- {
503
- const features = [
504
- 'sessions', 'agents', 'alerts', 'analytics', 'stats',
505
- 'recall', 'reflect', 'optimize', 'context', 'health',
506
- 'replay', 'benchmarks', 'guardrails', 'discovery', 'delegation',
507
- 'cost-budgets', 'trust', 'lessons',
508
- ];
509
- const { serverInfoRoutes } = await import('./routes/server-info.js');
510
- app.route('/api/server-info', serverInfoRoutes(features));
511
- }
512
- // ─── OpenAPI Spec & Documentation [F13-S1] ────────────
513
- app.doc('/api/openapi.json', {
514
- openapi: '3.1.0',
515
- info: {
516
- title: 'AgentLens API',
517
- version: '0.12.1',
518
- description: 'Observability, governance, and orchestration for AI agents.',
519
- license: { name: 'MIT' },
520
- },
521
- servers: [
522
- { url: 'http://localhost:3000', description: 'Local development' },
523
- ],
524
- security: [{ Bearer: [] }],
525
- tags: [
526
- { name: 'Sessions', description: 'Agent session lifecycle and queries' },
527
- { name: 'Events', description: 'Event ingestion and retrieval' },
528
- { name: 'Agents', description: 'Agent management and health' },
529
- { name: 'Auth', description: 'Authentication and API keys' },
530
- { name: 'Analytics', description: 'Metrics, costs, and statistics' },
531
- { name: 'Alerts', description: 'Alert rules and history' },
532
- { name: 'Intelligence', description: 'Reflect, recall, context, optimize' },
533
- { name: 'Trust & Governance', description: 'Trust scores, guardrails, cost budgets' },
534
- { name: 'Multi-Agent', description: 'Discovery, delegation, capabilities, mesh' },
535
- { name: 'Observability', description: 'Health, benchmarks, audit' },
536
- { name: 'Platform', description: 'Config, OTLP, streaming, webhooks' },
537
- ],
538
- });
539
- app.get('/api/docs', apiReference({
540
- url: '/api/openapi.json',
541
- theme: 'kepler',
542
- }));
543
- // ─── Dashboard SPA static assets ──────────────────────
544
- const dashboardRoot = getDashboardRoot();
545
- if (dashboardRoot) {
546
- app.use('/*', serveStatic({ root: dashboardRoot }));
547
- }
548
- return app;
549
- }
78
+ export { apiVersionMiddleware, CURRENT_API_VERSION, SUPPORTED_API_VERSIONS } from './lib/api-version.js';
79
+ export { apiVersionRoutes } from './routes/api-version.js';
80
+ export { optimizationAdvisorRoutes } from './routes/optimization-advisor.js';
81
+ export { getOptimizationSuggestions, getOptimizationSummary } from './services/optimization-advisor.js';
82
+ // ─── startServer ─────────────────────────────────────────────
550
83
  /**
551
84
  * Start the server as a standalone process.
552
85
  * Creates the database, runs migrations, and starts listening.
@@ -558,26 +91,23 @@ export async function startServer() {
558
91
  const config = getConfig();
559
92
  validateConfig(config);
560
93
  // Create and initialize database
561
- // For Postgres, we need the raw sql client for shutdown & health checks
562
94
  let pgSql;
563
95
  let pgDb;
564
96
  let store;
565
97
  let db;
566
- // SQLite is always created for auxiliary features (api_keys, audit, guardrails, etc.)
567
- // Even when PG is the primary event/embedding store
98
+ // SQLite is always created for auxiliary features
568
99
  db = createDb({ databasePath: config.dbPath });
569
100
  runMigrations(db);
570
101
  if (config.storageBackend === 'postgres') {
571
102
  const { createPostgresConnection, verifyPostgresConnection } = await import('./db/connection.postgres.js');
572
103
  const conn = createPostgresConnection();
573
- await verifyPostgresConnection(conn.sql); // fail fast if unreachable
104
+ await verifyPostgresConnection(conn.sql);
574
105
  pgSql = conn.sql;
575
106
  pgDb = conn.db;
576
107
  const { runPostgresMigrations } = await import('./db/migrate.postgres.js');
577
108
  await runPostgresMigrations(pgDb);
578
109
  const { PostgresEventStore } = await import('./db/postgres-store.js');
579
110
  store = new PostgresEventStore(pgDb);
580
- // Warn about silent SQLite → PG switch for existing Docker Compose users
581
111
  log.warn('STORAGE_BACKEND=postgres is now active. Previous SQLite data at ' +
582
112
  `${config.dbPath} is not automatically migrated.`);
583
113
  log.info('Database: PostgreSQL');
@@ -615,9 +145,9 @@ export async function startServer() {
615
145
  }
616
146
  }
617
147
  // Create app with db reference for auth
618
- // Create API key lookup for auth (uses SQLite for auxiliary features in both modes)
619
148
  const { SqliteApiKeyLookup } = await import('./db/api-key-lookup.js');
620
149
  const apiKeyLookup = new SqliteApiKeyLookup(db);
150
+ const { createApp } = await import('./app.js');
621
151
  const app = await createApp(store, { ...config, db, apiKeyLookup, embeddingService, embeddingWorker, pgSql, pgDb });
622
152
  // Start listening
623
153
  log.info(`AgentLens server starting on port ${config.port}`);
@@ -638,13 +168,12 @@ export async function startServer() {
638
168
  }
639
169
  }
640
170
  }
641
- // Start alert evaluation engine — wire notification router if db is available
171
+ // Start alert evaluation engine
642
172
  const notifRepoForEngine = db ? new NotificationChannelRepository(db) : null;
643
173
  const notifRouterForEngine = notifRepoForEngine ? new NotificationRouter(notifRepoForEngine) : null;
644
174
  const alertEngine = new AlertEngine(store, { notificationRouter: notifRouterForEngine ?? undefined });
645
175
  alertEngine.start();
646
176
  // Start guardrail evaluation engine (v0.8.0)
647
- // Wire the agent store so pause_agent/downgrade_model actions can UPDATE the agents table (B1)
648
177
  setAgentStore(store);
649
178
  if (notifRouterForEngine)
650
179
  setNotificationRouter(notifRouterForEngine);
@@ -657,7 +186,7 @@ export async function startServer() {
657
186
  const anomalyDetector = new CostAnomalyDetector(store, budgetEngine.getStore());
658
187
  anomalyDetector.start();
659
188
  log.info('Cost budgets & anomaly detection: enabled');
660
- // M-11 FIX: Graceful shutdown for engines, workers, HTTP server, and PG pool
189
+ // M-11 FIX: Graceful shutdown
661
190
  let httpServer;
662
191
  let shuttingDown = false;
663
192
  const shutdown = async () => {
@@ -665,16 +194,13 @@ export async function startServer() {
665
194
  return;
666
195
  shuttingDown = true;
667
196
  log.info('Shutting down...');
668
- // 1. Stop accepting new requests
669
197
  if (httpServer) {
670
198
  httpServer.close(() => log.info('HTTP server closed'));
671
199
  }
672
- // 2. Stop engines and workers
673
200
  alertEngine.stop();
674
201
  guardrailEngine.stop();
675
202
  if (embeddingWorker)
676
203
  embeddingWorker.stop();
677
- // 3. Drain PG pool (5s timeout)
678
204
  if (pgSql) {
679
205
  try {
680
206
  log.info('Draining PostgreSQL connection pool...');