@aiassesstech/grillo 0.1.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 (121) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/LICENSE +21 -0
  3. package/README.md +512 -0
  4. package/SKILL.md +87 -0
  5. package/dist/api/server.d.ts +68 -0
  6. package/dist/api/server.d.ts.map +1 -0
  7. package/dist/api/server.js +596 -0
  8. package/dist/api/server.js.map +1 -0
  9. package/dist/audit/audit-log.d.ts +88 -0
  10. package/dist/audit/audit-log.d.ts.map +1 -0
  11. package/dist/audit/audit-log.js +195 -0
  12. package/dist/audit/audit-log.js.map +1 -0
  13. package/dist/certification/certificate.d.ts +80 -0
  14. package/dist/certification/certificate.d.ts.map +1 -0
  15. package/dist/certification/certificate.js +176 -0
  16. package/dist/certification/certificate.js.map +1 -0
  17. package/dist/cli/bin.d.ts +8 -0
  18. package/dist/cli/bin.d.ts.map +1 -0
  19. package/dist/cli/bin.js +12 -0
  20. package/dist/cli/bin.js.map +1 -0
  21. package/dist/cli/config-loader.d.ts +66 -0
  22. package/dist/cli/config-loader.d.ts.map +1 -0
  23. package/dist/cli/config-loader.js +243 -0
  24. package/dist/cli/config-loader.js.map +1 -0
  25. package/dist/cli/runner.d.ts +27 -0
  26. package/dist/cli/runner.d.ts.map +1 -0
  27. package/dist/cli/runner.js +388 -0
  28. package/dist/cli/runner.js.map +1 -0
  29. package/dist/commands/grillo-commands.d.ts +50 -0
  30. package/dist/commands/grillo-commands.d.ts.map +1 -0
  31. package/dist/commands/grillo-commands.js +752 -0
  32. package/dist/commands/grillo-commands.js.map +1 -0
  33. package/dist/commands/inline-commands.d.ts +16 -0
  34. package/dist/commands/inline-commands.d.ts.map +1 -0
  35. package/dist/commands/inline-commands.js +277 -0
  36. package/dist/commands/inline-commands.js.map +1 -0
  37. package/dist/commands/router.d.ts +56 -0
  38. package/dist/commands/router.d.ts.map +1 -0
  39. package/dist/commands/router.js +154 -0
  40. package/dist/commands/router.js.map +1 -0
  41. package/dist/config/defaults.d.ts +9 -0
  42. package/dist/config/defaults.d.ts.map +1 -0
  43. package/dist/config/defaults.js +78 -0
  44. package/dist/config/defaults.js.map +1 -0
  45. package/dist/config/schema.d.ts +573 -0
  46. package/dist/config/schema.d.ts.map +1 -0
  47. package/dist/config/schema.js +142 -0
  48. package/dist/config/schema.js.map +1 -0
  49. package/dist/dashboard/metrics.d.ts +100 -0
  50. package/dist/dashboard/metrics.d.ts.map +1 -0
  51. package/dist/dashboard/metrics.js +282 -0
  52. package/dist/dashboard/metrics.js.map +1 -0
  53. package/dist/dashboard/ui.d.ts +19 -0
  54. package/dist/dashboard/ui.d.ts.map +1 -0
  55. package/dist/dashboard/ui.js +951 -0
  56. package/dist/dashboard/ui.js.map +1 -0
  57. package/dist/discovery/discovery-adapter.d.ts +94 -0
  58. package/dist/discovery/discovery-adapter.d.ts.map +1 -0
  59. package/dist/discovery/discovery-adapter.js +114 -0
  60. package/dist/discovery/discovery-adapter.js.map +1 -0
  61. package/dist/discovery/discovery-service.d.ts +77 -0
  62. package/dist/discovery/discovery-service.d.ts.map +1 -0
  63. package/dist/discovery/discovery-service.js +240 -0
  64. package/dist/discovery/discovery-service.js.map +1 -0
  65. package/dist/drift/detector.d.ts +51 -0
  66. package/dist/drift/detector.d.ts.map +1 -0
  67. package/dist/drift/detector.js +148 -0
  68. package/dist/drift/detector.js.map +1 -0
  69. package/dist/drift/fleet-anomaly.d.ts +28 -0
  70. package/dist/drift/fleet-anomaly.d.ts.map +1 -0
  71. package/dist/drift/fleet-anomaly.js +186 -0
  72. package/dist/drift/fleet-anomaly.js.map +1 -0
  73. package/dist/events/event-bus.d.ts +209 -0
  74. package/dist/events/event-bus.d.ts.map +1 -0
  75. package/dist/events/event-bus.js +184 -0
  76. package/dist/events/event-bus.js.map +1 -0
  77. package/dist/frameworks/framework-registry.d.ts +116 -0
  78. package/dist/frameworks/framework-registry.d.ts.map +1 -0
  79. package/dist/frameworks/framework-registry.js +241 -0
  80. package/dist/frameworks/framework-registry.js.map +1 -0
  81. package/dist/index.d.ts +94 -0
  82. package/dist/index.d.ts.map +1 -0
  83. package/dist/index.js +254 -0
  84. package/dist/index.js.map +1 -0
  85. package/dist/monitoring/continuous-monitor.d.ts +61 -0
  86. package/dist/monitoring/continuous-monitor.d.ts.map +1 -0
  87. package/dist/monitoring/continuous-monitor.js +191 -0
  88. package/dist/monitoring/continuous-monitor.js.map +1 -0
  89. package/dist/notifications/notifier.d.ts +21 -0
  90. package/dist/notifications/notifier.d.ts.map +1 -0
  91. package/dist/notifications/notifier.js +119 -0
  92. package/dist/notifications/notifier.js.map +1 -0
  93. package/dist/notifications/templates.d.ts +14 -0
  94. package/dist/notifications/templates.d.ts.map +1 -0
  95. package/dist/notifications/templates.js +105 -0
  96. package/dist/notifications/templates.js.map +1 -0
  97. package/dist/orchestration/orchestrator.d.ts +99 -0
  98. package/dist/orchestration/orchestrator.d.ts.map +1 -0
  99. package/dist/orchestration/orchestrator.js +426 -0
  100. package/dist/orchestration/orchestrator.js.map +1 -0
  101. package/dist/orchestration/queue.d.ts +17 -0
  102. package/dist/orchestration/queue.d.ts.map +1 -0
  103. package/dist/orchestration/queue.js +121 -0
  104. package/dist/orchestration/queue.js.map +1 -0
  105. package/dist/orchestration/scheduler.d.ts +26 -0
  106. package/dist/orchestration/scheduler.d.ts.map +1 -0
  107. package/dist/orchestration/scheduler.js +110 -0
  108. package/dist/orchestration/scheduler.js.map +1 -0
  109. package/dist/registry/agent-registry.d.ts +106 -0
  110. package/dist/registry/agent-registry.d.ts.map +1 -0
  111. package/dist/registry/agent-registry.js +349 -0
  112. package/dist/registry/agent-registry.js.map +1 -0
  113. package/dist/registry/types.d.ts +158 -0
  114. package/dist/registry/types.d.ts.map +1 -0
  115. package/dist/registry/types.js +44 -0
  116. package/dist/registry/types.js.map +1 -0
  117. package/dist/reports/compliance-report.d.ts +66 -0
  118. package/dist/reports/compliance-report.d.ts.map +1 -0
  119. package/dist/reports/compliance-report.js +208 -0
  120. package/dist/reports/compliance-report.js.map +1 -0
  121. package/package.json +67 -0
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Grillo Cricket — REST API Server
3
+ *
4
+ * Lightweight HTTP server exposing Grillo's fleet management API.
5
+ * Uses Node.js built-in http module (zero external dependencies).
6
+ *
7
+ * Endpoints map 1:1 to the spec in SPEC-GRILLO-CRICKET section 10:
8
+ * /api/grillo/agents/* — Agent management
9
+ * /api/grillo/assess/* — Assessment operations
10
+ * /api/grillo/certifications/* — Certification & compliance
11
+ * /api/grillo/drift/* — Drift detection
12
+ */
13
+ import type { AgentRegistry } from "../registry/agent-registry.js";
14
+ import type { GrilloOrchestrator } from "../orchestration/orchestrator.js";
15
+ import type { DriftDetector } from "../drift/detector.js";
16
+ import type { FleetAnomalyDetector } from "../drift/fleet-anomaly.js";
17
+ import type { AuditLog } from "../audit/audit-log.js";
18
+ import type { ComplianceReportGenerator } from "../reports/compliance-report.js";
19
+ import type { GrilloConfig } from "../config/schema.js";
20
+ import type { DiscoveryService } from "../discovery/discovery-service.js";
21
+ import type { GrilloEventBus } from "../events/event-bus.js";
22
+ import type { ContinuousMonitor } from "../monitoring/continuous-monitor.js";
23
+ import type { DashboardMetrics } from "../dashboard/metrics.js";
24
+ export interface GrilloAPIServerOptions {
25
+ registry: AgentRegistry;
26
+ orchestrator: GrilloOrchestrator;
27
+ driftDetector: DriftDetector;
28
+ fleetAnomalyDetector: FleetAnomalyDetector;
29
+ auditLog: AuditLog;
30
+ reportGenerator: ComplianceReportGenerator;
31
+ config: GrilloConfig;
32
+ /** Phase 4 additions */
33
+ discoveryService?: DiscoveryService;
34
+ eventBus?: GrilloEventBus;
35
+ continuousMonitor?: ContinuousMonitor;
36
+ /** Phase 7: Dashboard */
37
+ dashboardMetrics?: DashboardMetrics;
38
+ port?: number;
39
+ host?: string;
40
+ }
41
+ export declare class GrilloAPIServer {
42
+ private readonly opts;
43
+ private server;
44
+ private readonly startedAt;
45
+ private activeConnections;
46
+ private _shuttingDown;
47
+ /** Phase 7: Cached dashboard HTML (regenerated on config change) */
48
+ private dashboardHTML;
49
+ constructor(opts: GrilloAPIServerOptions);
50
+ /**
51
+ * Start the API server.
52
+ */
53
+ start(): Promise<void>;
54
+ /**
55
+ * Graceful shutdown: stop accepting new connections,
56
+ * drain active connections, then close.
57
+ */
58
+ stop(options?: {
59
+ timeoutMs?: number;
60
+ }): Promise<void>;
61
+ /**
62
+ * Whether the server is in shutdown mode.
63
+ */
64
+ get shuttingDown(): boolean;
65
+ private handleRequest;
66
+ private json;
67
+ }
68
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/api/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,iCAAiC,CAAC;AACjF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qCAAqC,CAAC;AAC7E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAOhE,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,aAAa,CAAC;IACxB,YAAY,EAAE,kBAAkB,CAAC;IACjC,aAAa,EAAE,aAAa,CAAC;IAC7B,oBAAoB,EAAE,oBAAoB,CAAC;IAC3C,QAAQ,EAAE,QAAQ,CAAC;IACnB,eAAe,EAAE,yBAAyB,CAAC;IAC3C,MAAM,EAAE,YAAY,CAAC;IACrB,wBAAwB;IACxB,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC,yBAAyB;IACzB,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyB;IAC9C,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoB;IAC9C,OAAO,CAAC,iBAAiB,CAAuC;IAChE,OAAO,CAAC,aAAa,CAAS;IAC9B,oEAAoE;IACpE,OAAO,CAAC,aAAa,CAAS;gBAElB,IAAI,EAAE,sBAAsB;IAKxC;;OAEG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAkBtB;;;OAGG;IACG,IAAI,CAAC,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAgC3D;;OAEG;IACH,IAAI,YAAY,IAAI,OAAO,CAE1B;YAMa,aAAa;IAmgB3B,OAAO,CAAC,IAAI;CAIb"}
@@ -0,0 +1,596 @@
1
+ /**
2
+ * Grillo Cricket — REST API Server
3
+ *
4
+ * Lightweight HTTP server exposing Grillo's fleet management API.
5
+ * Uses Node.js built-in http module (zero external dependencies).
6
+ *
7
+ * Endpoints map 1:1 to the spec in SPEC-GRILLO-CRICKET section 10:
8
+ * /api/grillo/agents/* — Agent management
9
+ * /api/grillo/assess/* — Assessment operations
10
+ * /api/grillo/certifications/* — Certification & compliance
11
+ * /api/grillo/drift/* — Drift detection
12
+ */
13
+ import * as http from "node:http";
14
+ import { AgentCategory, RiskTier } from "../registry/types.js";
15
+ import { generateDashboardHTML } from "../dashboard/ui.js";
16
+ export class GrilloAPIServer {
17
+ opts;
18
+ server = null;
19
+ startedAt = new Date();
20
+ activeConnections = new Set();
21
+ _shuttingDown = false;
22
+ /** Phase 7: Cached dashboard HTML (regenerated on config change) */
23
+ dashboardHTML;
24
+ constructor(opts) {
25
+ this.opts = opts;
26
+ this.dashboardHTML = generateDashboardHTML(opts.config);
27
+ }
28
+ /**
29
+ * Start the API server.
30
+ */
31
+ start() {
32
+ const port = this.opts.port ?? 18800;
33
+ const host = this.opts.host ?? "127.0.0.1";
34
+ return new Promise((resolve) => {
35
+ this.server = http.createServer((req, res) => {
36
+ // Track active connections for graceful shutdown
37
+ this.activeConnections.add(res);
38
+ res.on("close", () => this.activeConnections.delete(res));
39
+ this.handleRequest(req, res);
40
+ });
41
+ this.server.listen(port, host, () => {
42
+ console.log(`[grillo:api] Listening on http://${host}:${port}`);
43
+ resolve();
44
+ });
45
+ });
46
+ }
47
+ /**
48
+ * Graceful shutdown: stop accepting new connections,
49
+ * drain active connections, then close.
50
+ */
51
+ async stop(options) {
52
+ if (this._shuttingDown)
53
+ return;
54
+ this._shuttingDown = true;
55
+ const timeoutMs = options?.timeoutMs ?? 10000;
56
+ console.log(`[grillo:api] Shutting down gracefully (${timeoutMs}ms timeout, ` +
57
+ `${this.activeConnections.size} active connections)...`);
58
+ return new Promise((resolve) => {
59
+ // Force-resolve after timeout
60
+ const forceTimer = setTimeout(() => {
61
+ console.log("[grillo:api] Shutdown timeout reached, forcing close.");
62
+ this.server?.close();
63
+ resolve();
64
+ }, timeoutMs);
65
+ if (this.server) {
66
+ // Stop accepting new connections
67
+ this.server.close(() => {
68
+ clearTimeout(forceTimer);
69
+ console.log("[grillo:api] All connections drained. Shutdown complete.");
70
+ resolve();
71
+ });
72
+ }
73
+ else {
74
+ clearTimeout(forceTimer);
75
+ resolve();
76
+ }
77
+ });
78
+ }
79
+ /**
80
+ * Whether the server is in shutdown mode.
81
+ */
82
+ get shuttingDown() {
83
+ return this._shuttingDown;
84
+ }
85
+ // ============================================================
86
+ // Request Router
87
+ // ============================================================
88
+ async handleRequest(req, res) {
89
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
90
+ const method = req.method ?? "GET";
91
+ const pathname = url.pathname;
92
+ // CORS headers
93
+ res.setHeader("Access-Control-Allow-Origin", "*");
94
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
95
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
96
+ if (method === "OPTIONS") {
97
+ res.writeHead(204);
98
+ res.end();
99
+ return;
100
+ }
101
+ try {
102
+ // Reject new requests during shutdown
103
+ if (this._shuttingDown) {
104
+ res.setHeader("Connection", "close");
105
+ return this.json(res, 503, { error: "Server is shutting down" });
106
+ }
107
+ // ---- Phase 7: Dashboard Web UI ----
108
+ if (pathname === "/dashboard" || pathname === "/dashboard/") {
109
+ res.writeHead(200, {
110
+ "Content-Type": "text/html; charset=utf-8",
111
+ "Cache-Control": "no-cache",
112
+ });
113
+ res.end(this.dashboardHTML);
114
+ return;
115
+ }
116
+ // Dashboard data API — aggregated metrics
117
+ if (pathname === "/api/grillo/dashboard/overview" && method === "GET") {
118
+ if (!this.opts.dashboardMetrics) {
119
+ return this.json(res, 501, { error: "Dashboard metrics not configured" });
120
+ }
121
+ const overview = this.opts.dashboardMetrics.getFleetOverview();
122
+ return this.json(res, 200, overview);
123
+ }
124
+ if (pathname === "/api/grillo/dashboard/hierarchy" && method === "GET") {
125
+ if (!this.opts.dashboardMetrics) {
126
+ return this.json(res, 501, { error: "Dashboard metrics not configured" });
127
+ }
128
+ const hierarchy = this.opts.dashboardMetrics.getHierarchyOverview();
129
+ return this.json(res, 200, hierarchy);
130
+ }
131
+ if (pathname === "/api/grillo/dashboard/trends" && method === "GET") {
132
+ if (!this.opts.dashboardMetrics) {
133
+ return this.json(res, 501, { error: "Dashboard metrics not configured" });
134
+ }
135
+ const days = Number(url.searchParams.get("days") ?? 30);
136
+ const trends = this.opts.dashboardMetrics.getFleetTrend(days);
137
+ return this.json(res, 200, trends);
138
+ }
139
+ const agentTrendMatch = pathname.match(/^\/api\/grillo\/dashboard\/agent\/([^/]+)$/);
140
+ if (agentTrendMatch && method === "GET") {
141
+ if (!this.opts.dashboardMetrics) {
142
+ return this.json(res, 501, { error: "Dashboard metrics not configured" });
143
+ }
144
+ const agentId = agentTrendMatch[1];
145
+ const trend = this.opts.dashboardMetrics.getAgentTrend(agentId);
146
+ if (!trend)
147
+ return this.json(res, 404, { error: `Agent not found: ${agentId}` });
148
+ return this.json(res, 200, trend);
149
+ }
150
+ // Health check (enhanced in Phase 4)
151
+ if (pathname === "/api/grillo/health") {
152
+ const uptimeMs = Date.now() - this.startedAt.getTime();
153
+ return this.json(res, 200, {
154
+ status: "ok",
155
+ version: "0.1.0",
156
+ uptime: {
157
+ ms: uptimeMs,
158
+ human: formatUptime(uptimeMs),
159
+ },
160
+ services: {
161
+ registry: { healthy: true, agents: this.opts.registry.size },
162
+ auditLog: { healthy: true, entries: this.opts.auditLog.size },
163
+ orchestrator: { healthy: true, running: this.opts.orchestrator.isRunning },
164
+ monitor: {
165
+ healthy: true,
166
+ running: this.opts.continuousMonitor?.running ?? false,
167
+ },
168
+ discovery: {
169
+ healthy: true,
170
+ adapters: this.opts.discoveryService?.listAdapters().length ?? 0,
171
+ },
172
+ eventBus: {
173
+ healthy: true,
174
+ webhooks: this.opts.eventBus?.listWebhooks().length ?? 0,
175
+ },
176
+ },
177
+ startedAt: this.startedAt.toISOString(),
178
+ activeConnections: this.activeConnections.size,
179
+ });
180
+ }
181
+ // Readiness probe (Kubernetes-style)
182
+ if (pathname === "/api/grillo/ready") {
183
+ return this.json(res, 200, { ready: true });
184
+ }
185
+ // Liveness probe
186
+ if (pathname === "/api/grillo/live") {
187
+ return this.json(res, 200, { alive: true });
188
+ }
189
+ // ---- Discovery (Phase 4) ----
190
+ if (pathname === "/api/grillo/discover" && method === "POST") {
191
+ if (!this.opts.discoveryService) {
192
+ return this.json(res, 501, { error: "Discovery service not configured" });
193
+ }
194
+ const result = await this.opts.discoveryService.discoverAndReconcile();
195
+ this.opts.eventBus?.emit("discovery:scan_completed", {
196
+ source: "api",
197
+ newAgents: result.registered.length,
198
+ existingAgents: result.existing.length,
199
+ orphanedAgents: result.orphaned.length,
200
+ durationMs: result.durationMs,
201
+ });
202
+ return this.json(res, 200, result);
203
+ }
204
+ if (pathname === "/api/grillo/discover/health" && method === "GET") {
205
+ if (!this.opts.discoveryService) {
206
+ return this.json(res, 501, { error: "Discovery service not configured" });
207
+ }
208
+ const health = await this.opts.discoveryService.healthCheck();
209
+ return this.json(res, 200, { adapters: health });
210
+ }
211
+ // ---- Events (Phase 4) ----
212
+ if (pathname === "/api/grillo/events" && method === "GET") {
213
+ if (!this.opts.eventBus) {
214
+ return this.json(res, 501, { error: "Event bus not configured" });
215
+ }
216
+ const limitParam = url.searchParams.get("limit");
217
+ const eventFilter = url.searchParams.get("event");
218
+ const limit = limitParam ? parseInt(limitParam, 10) : 50;
219
+ const history = this.opts.eventBus.history({
220
+ event: eventFilter,
221
+ limit,
222
+ });
223
+ return this.json(res, 200, { events: history, total: history.length });
224
+ }
225
+ if (pathname === "/api/grillo/events/webhooks" && method === "GET") {
226
+ if (!this.opts.eventBus) {
227
+ return this.json(res, 501, { error: "Event bus not configured" });
228
+ }
229
+ return this.json(res, 200, { webhooks: this.opts.eventBus.listWebhooks() });
230
+ }
231
+ if (pathname === "/api/grillo/events/webhooks" && method === "POST") {
232
+ if (!this.opts.eventBus) {
233
+ return this.json(res, 501, { error: "Event bus not configured" });
234
+ }
235
+ const body = await readBody(req);
236
+ const webhookUrl = String(body.url ?? "");
237
+ const events = body.events ?? ["*"];
238
+ if (!webhookUrl) {
239
+ return this.json(res, 400, { error: "Missing url" });
240
+ }
241
+ this.opts.eventBus.registerWebhook(webhookUrl, events);
242
+ return this.json(res, 201, { registered: true, url: webhookUrl, events });
243
+ }
244
+ // ---- Agent Management ----
245
+ if (pathname === "/api/grillo/agents" && method === "GET") {
246
+ return this.json(res, 200, {
247
+ agents: this.opts.registry.all(),
248
+ total: this.opts.registry.size,
249
+ });
250
+ }
251
+ if (pathname === "/api/grillo/agents/register" && method === "POST") {
252
+ const body = await readBody(req);
253
+ const agent = this.opts.registry.register({
254
+ agentId: String(body.agentId ?? ""),
255
+ agentName: String(body.agentName ?? body.agentId ?? ""),
256
+ agentType: String(body.agentType ?? "agent"),
257
+ category: body.category ?? AgentCategory.INTERNAL_UTILITY,
258
+ provider: String(body.provider ?? ""),
259
+ model: String(body.model ?? ""),
260
+ riskTier: body.riskTier ?? RiskTier.MEDIUM,
261
+ tags: Array.isArray(body.tags) ? body.tags : [],
262
+ metadata: body.metadata ?? {},
263
+ });
264
+ this.opts.auditLog.append({
265
+ action: "agent_registered",
266
+ actor: "api",
267
+ agentId: agent.agentId,
268
+ description: `Agent ${agent.agentName} registered via API`,
269
+ });
270
+ return this.json(res, 201, agent);
271
+ }
272
+ if (pathname === "/api/grillo/agents/discover" && method === "POST") {
273
+ return this.json(res, 200, {
274
+ message: "Discovery scan complete",
275
+ registeredAgents: this.opts.registry.size,
276
+ note: "Full auto-discovery integration coming in Phase 3",
277
+ });
278
+ }
279
+ // Agent by ID routes
280
+ const agentMatch = pathname.match(/^\/api\/grillo\/agents\/([^/]+)$/);
281
+ if (agentMatch) {
282
+ const agentId = agentMatch[1];
283
+ if (method === "GET") {
284
+ const agent = this.opts.registry.get(agentId);
285
+ if (!agent)
286
+ return this.json(res, 404, { error: `Agent not found: ${agentId}` });
287
+ return this.json(res, 200, agent);
288
+ }
289
+ if (method === "DELETE") {
290
+ const removed = this.opts.registry.deregister(agentId);
291
+ if (!removed)
292
+ return this.json(res, 404, { error: `Agent not found: ${agentId}` });
293
+ this.opts.auditLog.append({
294
+ action: "agent_deregistered",
295
+ actor: "api",
296
+ agentId,
297
+ description: `Agent ${agentId} deregistered via API`,
298
+ });
299
+ return this.json(res, 200, { message: `Agent ${agentId} deregistered` });
300
+ }
301
+ }
302
+ // Agent category update
303
+ const categoryMatch = pathname.match(/^\/api\/grillo\/agents\/([^/]+)\/category$/);
304
+ if (categoryMatch && method === "PUT") {
305
+ const agentId = categoryMatch[1];
306
+ const agent = this.opts.registry.get(agentId);
307
+ if (!agent)
308
+ return this.json(res, 404, { error: `Agent not found: ${agentId}` });
309
+ const body = await readBody(req);
310
+ // Re-register with new category
311
+ this.opts.registry.register({
312
+ agentId,
313
+ agentName: agent.agentName,
314
+ agentType: agent.agentType,
315
+ category: body.category,
316
+ provider: agent.provider,
317
+ model: agent.model,
318
+ riskTier: body.riskTier ?? agent.riskTier,
319
+ });
320
+ return this.json(res, 200, this.opts.registry.get(agentId));
321
+ }
322
+ // ---- Assessment Operations ----
323
+ const assessMatch = pathname.match(/^\/api\/grillo\/assess\/([^/]+)$/);
324
+ if (assessMatch && method === "POST") {
325
+ const target = assessMatch[1];
326
+ if (target === "fleet") {
327
+ const body = await readBody(req);
328
+ const result = await this.opts.orchestrator.assessFleet({
329
+ dryRun: body.dryRun === true,
330
+ });
331
+ return this.json(res, 200, result);
332
+ }
333
+ const agent = this.opts.registry.get(target);
334
+ if (!agent)
335
+ return this.json(res, 404, { error: `Agent not found: ${target}` });
336
+ const body = await readBody(req);
337
+ const result = await this.opts.orchestrator.assessAgent(agent, {
338
+ framework: body.framework ? String(body.framework) : undefined,
339
+ dryRun: body.dryRun === true,
340
+ });
341
+ return this.json(res, 200, result);
342
+ }
343
+ // ---- Assessment Bypass (Phase 6) ----
344
+ const bypassMatch = pathname.match(/^\/api\/grillo\/assess\/([^/]+)\/bypass$/);
345
+ if (bypassMatch && method === "POST") {
346
+ const agentId = bypassMatch[1];
347
+ const agent = this.opts.registry.get(agentId);
348
+ if (!agent)
349
+ return this.json(res, 404, { error: `Agent not found: ${agentId}` });
350
+ const body = await readBody(req);
351
+ const reason = String(body.reason ?? "");
352
+ const authorizedBy = String(body.authorizedBy ?? "");
353
+ const expiresInDays = Number(body.expiresInDays ?? 30);
354
+ if (!reason || !authorizedBy) {
355
+ return this.json(res, 400, {
356
+ error: "Both 'reason' and 'authorizedBy' are required for bypass authorization",
357
+ });
358
+ }
359
+ // Grant bypass certification
360
+ const now = new Date();
361
+ const expiresAt = new Date(now.getTime() + expiresInDays * 24 * 60 * 60 * 1000);
362
+ const bypassRecord = {
363
+ runId: `bypass-${Date.now()}-${agentId}`,
364
+ framework: "bypass",
365
+ level: 0,
366
+ scores: {},
367
+ passed: true,
368
+ classification: "BYPASS — Manual Authorization",
369
+ assessedAt: now,
370
+ expiresAt,
371
+ verifyUrl: "",
372
+ metadata: {
373
+ bypass: true,
374
+ reason,
375
+ authorizedBy,
376
+ expiresInDays,
377
+ },
378
+ };
379
+ this.opts.registry.recordAssessment(agentId, bypassRecord);
380
+ // Force status to CERTIFIED (bypass overrides PROBATION gate)
381
+ const { CertificationStatus } = await import("../registry/types.js");
382
+ this.opts.registry.updateStatus(agentId, CertificationStatus.CERTIFIED);
383
+ this.opts.auditLog.append({
384
+ action: "bypass_requested",
385
+ actor: authorizedBy,
386
+ agentId,
387
+ description: `Assessment bypass granted for ${agent.agentName}. Reason: ${reason}. Expires: ${expiresAt.toISOString()}`,
388
+ data: { reason, authorizedBy, expiresInDays, expiresAt: expiresAt.toISOString() },
389
+ });
390
+ this.opts.eventBus?.emit("assessment:bypass_granted", {
391
+ agentId,
392
+ agentName: agent.agentName,
393
+ reason,
394
+ authorizedBy,
395
+ expiresAt: expiresAt.toISOString(),
396
+ });
397
+ return this.json(res, 200, {
398
+ message: `Bypass granted for ${agent.agentName}`,
399
+ agentId,
400
+ status: "certified",
401
+ bypassRecord,
402
+ expiresAt: expiresAt.toISOString(),
403
+ });
404
+ }
405
+ const assessStatusMatch = pathname.match(/^\/api\/grillo\/assess\/([^/]+)\/status$/);
406
+ if (assessStatusMatch && method === "GET") {
407
+ const agentId = assessStatusMatch[1];
408
+ const agent = this.opts.registry.get(agentId);
409
+ if (!agent)
410
+ return this.json(res, 404, { error: `Agent not found: ${agentId}` });
411
+ return this.json(res, 200, {
412
+ agentId: agent.agentId,
413
+ certificationStatus: agent.certificationStatus,
414
+ lastAssessedAt: agent.lastAssessedAt,
415
+ nextAssessmentDue: agent.nextAssessmentDue,
416
+ });
417
+ }
418
+ const assessHistoryMatch = pathname.match(/^\/api\/grillo\/assess\/([^/]+)\/history$/);
419
+ if (assessHistoryMatch && method === "GET") {
420
+ const agentId = assessHistoryMatch[1];
421
+ const agent = this.opts.registry.get(agentId);
422
+ if (!agent)
423
+ return this.json(res, 404, { error: `Agent not found: ${agentId}` });
424
+ return this.json(res, 200, {
425
+ agentId: agent.agentId,
426
+ history: agent.certificationHistory,
427
+ });
428
+ }
429
+ if (pathname === "/api/grillo/assess/queue" && method === "GET") {
430
+ const { buildAssessmentQueue } = await import("../orchestration/queue.js");
431
+ const queue = buildAssessmentQueue(this.opts.registry.all(), this.opts.config);
432
+ return this.json(res, 200, { queue: queue.map((q) => ({
433
+ agentId: q.agent.agentId,
434
+ agentName: q.agent.agentName,
435
+ priority: q.priority,
436
+ reason: q.reason,
437
+ framework: q.framework,
438
+ level: q.level,
439
+ })) });
440
+ }
441
+ // ---- Certifications & Compliance ----
442
+ if (pathname === "/api/grillo/certifications" && method === "GET") {
443
+ return this.json(res, 200, this.opts.registry.summary());
444
+ }
445
+ const certMatch = pathname.match(/^\/api\/grillo\/certifications\/([^/]+)$/);
446
+ if (certMatch && method === "GET") {
447
+ const agentId = certMatch[1];
448
+ const agent = this.opts.registry.get(agentId);
449
+ if (!agent)
450
+ return this.json(res, 404, { error: `Agent not found: ${agentId}` });
451
+ const lastCert = agent.certificationHistory[agent.certificationHistory.length - 1];
452
+ return this.json(res, 200, {
453
+ agentId: agent.agentId,
454
+ status: agent.certificationStatus,
455
+ hierarchicalProgress: agent.hierarchicalProgress,
456
+ latestCertification: lastCert ?? null,
457
+ totalAssessments: agent.certificationHistory.length,
458
+ });
459
+ }
460
+ const suspendMatch = pathname.match(/^\/api\/grillo\/certifications\/([^/]+)\/suspend$/);
461
+ if (suspendMatch && method === "POST") {
462
+ const agentId = suspendMatch[1];
463
+ try {
464
+ const agent = this.opts.registry.updateStatus(agentId, "suspended");
465
+ this.opts.auditLog.append({
466
+ action: "certification_revoked",
467
+ actor: "api",
468
+ agentId,
469
+ description: `Agent ${agentId} suspended via API`,
470
+ });
471
+ return this.json(res, 200, agent);
472
+ }
473
+ catch {
474
+ return this.json(res, 404, { error: `Agent not found: ${agentId}` });
475
+ }
476
+ }
477
+ const reinstateMatch = pathname.match(/^\/api\/grillo\/certifications\/([^/]+)\/reinstate$/);
478
+ if (reinstateMatch && method === "POST") {
479
+ const agentId = reinstateMatch[1];
480
+ try {
481
+ const agent = this.opts.registry.updateStatus(agentId, "expired");
482
+ this.opts.auditLog.append({
483
+ action: "agent_reinstated",
484
+ actor: "api",
485
+ agentId,
486
+ description: `Agent ${agentId} reinstated via API`,
487
+ });
488
+ return this.json(res, 200, agent);
489
+ }
490
+ catch {
491
+ return this.json(res, 404, { error: `Agent not found: ${agentId}` });
492
+ }
493
+ }
494
+ if (pathname === "/api/grillo/compliance/report" && method === "GET") {
495
+ const format = url.searchParams.get("format") ?? "json";
496
+ const period = url.searchParams.get("period") ?? "30d";
497
+ const report = this.opts.reportGenerator.generate({ format, period });
498
+ if (format === "json") {
499
+ return this.json(res, 200, JSON.parse(report));
500
+ }
501
+ res.writeHead(200, { "Content-Type": format === "csv" ? "text/csv" : "text/markdown" });
502
+ res.end(report);
503
+ return;
504
+ }
505
+ // ---- Drift ----
506
+ const driftAgentMatch = pathname.match(/^\/api\/grillo\/drift\/([^/]+)$/);
507
+ if (driftAgentMatch && method === "GET") {
508
+ const target = driftAgentMatch[1];
509
+ if (target === "fleet") {
510
+ const agents = this.opts.registry.all().filter((a) => a.driftBaseline !== null);
511
+ const reports = agents.map((a) => {
512
+ const lastCert = a.certificationHistory[a.certificationHistory.length - 1];
513
+ return this.opts.driftDetector.analyze(a, lastCert?.scores ?? {});
514
+ });
515
+ const anomalies = this.opts.fleetAnomalyDetector.analyze(reports, agents);
516
+ return this.json(res, 200, { reports, anomalies });
517
+ }
518
+ const agent = this.opts.registry.get(target);
519
+ if (!agent)
520
+ return this.json(res, 404, { error: `Agent not found: ${target}` });
521
+ if (!agent.driftBaseline)
522
+ return this.json(res, 200, { message: "No baseline established" });
523
+ const lastCert = agent.certificationHistory[agent.certificationHistory.length - 1];
524
+ const report = this.opts.driftDetector.analyze(agent, lastCert?.scores ?? {});
525
+ return this.json(res, 200, report);
526
+ }
527
+ // ---- Audit ----
528
+ if (pathname === "/api/grillo/audit" && method === "GET") {
529
+ const agentId = url.searchParams.get("agentId");
530
+ const entries = agentId
531
+ ? this.opts.auditLog.forAgent(agentId)
532
+ : this.opts.auditLog.recent(100);
533
+ const verification = this.opts.auditLog.verifyChain();
534
+ return this.json(res, 200, { entries, verification });
535
+ }
536
+ // ---- 404 ----
537
+ return this.json(res, 404, {
538
+ error: "Not Found",
539
+ path: pathname,
540
+ method,
541
+ });
542
+ }
543
+ catch (error) {
544
+ console.error("[grillo:api] Error:", error);
545
+ return this.json(res, 500, {
546
+ error: "Internal Server Error",
547
+ message: error instanceof Error ? error.message : String(error),
548
+ });
549
+ }
550
+ }
551
+ // ============================================================
552
+ // Helpers
553
+ // ============================================================
554
+ json(res, status, data) {
555
+ res.writeHead(status, { "Content-Type": "application/json" });
556
+ res.end(JSON.stringify(data, dateReplacer, 2));
557
+ }
558
+ }
559
+ // ================================================================
560
+ // Utilities
561
+ // ================================================================
562
+ function readBody(req) {
563
+ return new Promise((resolve, reject) => {
564
+ const chunks = [];
565
+ req.on("data", (chunk) => chunks.push(chunk));
566
+ req.on("end", () => {
567
+ try {
568
+ const raw = Buffer.concat(chunks).toString("utf-8");
569
+ resolve(raw ? JSON.parse(raw) : {});
570
+ }
571
+ catch {
572
+ resolve({});
573
+ }
574
+ });
575
+ req.on("error", reject);
576
+ });
577
+ }
578
+ function dateReplacer(_key, value) {
579
+ if (value instanceof Date)
580
+ return value.toISOString();
581
+ return value;
582
+ }
583
+ function formatUptime(ms) {
584
+ const seconds = Math.floor(ms / 1000);
585
+ const minutes = Math.floor(seconds / 60);
586
+ const hours = Math.floor(minutes / 60);
587
+ const days = Math.floor(hours / 24);
588
+ if (days > 0)
589
+ return `${days}d ${hours % 24}h ${minutes % 60}m`;
590
+ if (hours > 0)
591
+ return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
592
+ if (minutes > 0)
593
+ return `${minutes}m ${seconds % 60}s`;
594
+ return `${seconds}s`;
595
+ }
596
+ //# sourceMappingURL=server.js.map