@agent-e/server 1.6.0 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AgentEServer-DT6ETPR6.mjs +7 -0
- package/dist/{chunk-OALXQFKY.mjs → chunk-53EPMEWX.mjs} +55 -31
- package/dist/chunk-53EPMEWX.mjs.map +1 -0
- package/dist/cli.js +54 -30
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/index.js +55 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -2
- package/package.json +2 -2
- package/dist/AgentEServer-H3FWDCYM.mjs +0 -7
- package/dist/chunk-OALXQFKY.mjs.map +0 -1
- /package/dist/{AgentEServer-H3FWDCYM.mjs.map → AgentEServer-DT6ETPR6.mjs.map} +0 -0
|
@@ -550,6 +550,7 @@ function getDashboardHtml() {
|
|
|
550
550
|
const $app = document.getElementById('app');
|
|
551
551
|
|
|
552
552
|
// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
553
|
+
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
|
553
554
|
function pad(n, w) { return String(n).padStart(w || 4, ' '); }
|
|
554
555
|
function fmt(n) { return typeof n === 'number' ? n.toFixed(3) : '\u2014'; }
|
|
555
556
|
function pct(n) { return typeof n === 'number' ? (n * 100).toFixed(0) + '%' : '\u2014'; }
|
|
@@ -655,15 +656,15 @@ function getDashboardHtml() {
|
|
|
655
656
|
let advisorBtns = '';
|
|
656
657
|
if (isAdvisor && d.result === 'skipped_override') {
|
|
657
658
|
advisorBtns = '<span class="advisor-actions">'
|
|
658
|
-
+ '<button class="advisor-btn approve" onclick="window._approve(\\'' + d.id + '\\')">[Approve]</button>'
|
|
659
|
-
+ '<button class="advisor-btn reject" onclick="window._reject(\\'' + d.id + '\\')">[Reject]</button>'
|
|
659
|
+
+ '<button class="advisor-btn approve" onclick="window._approve(\\'' + esc(d.id) + '\\')">[Approve]</button>'
|
|
660
|
+
+ '<button class="advisor-btn reject" onclick="window._reject(\\'' + esc(d.id) + '\\')">[Reject]</button>'
|
|
660
661
|
+ '</span>';
|
|
661
662
|
}
|
|
662
663
|
|
|
663
664
|
return '<span class="t-tick">[Tick ' + pad(d.tick) + ']</span> '
|
|
664
665
|
+ resultIcon
|
|
665
|
-
+ '<span class="t-principle">[' + (principle.id || '?') + '] ' + (principle.name || '') + ':</span> '
|
|
666
|
-
+ '<span class="t-param">' + (plan.parameter || '\u2014') + ' </span>'
|
|
666
|
+
+ '<span class="t-principle">[' + esc(principle.id || '?') + '] ' + esc(principle.name || '') + ':</span> '
|
|
667
|
+
+ '<span class="t-param">' + esc(plan.parameter || '\u2014') + ' </span>'
|
|
667
668
|
+ '<span class="t-old">' + fmt(plan.currentValue) + '</span>'
|
|
668
669
|
+ '<span class="t-arrow"> \\u2192 </span>'
|
|
669
670
|
+ '<span class="t-new">' + fmt(plan.targetValue) + '</span>'
|
|
@@ -687,8 +688,8 @@ function getDashboardHtml() {
|
|
|
687
688
|
return '<div class="alert-card">'
|
|
688
689
|
+ '<span class="alert-severity ' + sc + '">' + sev + '/10</span>'
|
|
689
690
|
+ '<div class="alert-body">'
|
|
690
|
-
+ '<div class="alert-principle">[' + pid + '] ' + name + '</div>'
|
|
691
|
-
+ '<div class="alert-reason">' + reason + '</div>'
|
|
691
|
+
+ '<div class="alert-principle">[' + esc(pid) + '] ' + esc(name) + '</div>'
|
|
692
|
+
+ '<div class="alert-reason">' + esc(reason) + '</div>'
|
|
692
693
|
+ '</div></div>';
|
|
693
694
|
}).join('');
|
|
694
695
|
}
|
|
@@ -716,10 +717,10 @@ function getDashboardHtml() {
|
|
|
716
717
|
$violationsBody.innerHTML = sorted.map(function(v) {
|
|
717
718
|
return '<tr>'
|
|
718
719
|
+ '<td>' + v.tick + '</td>'
|
|
719
|
-
+ '<td style="color:var(--text-primary);font-family:var(--font-sans)">' + v.principle + '</td>'
|
|
720
|
+
+ '<td style="color:var(--text-primary);font-family:var(--font-sans)">' + esc(v.principle) + '</td>'
|
|
720
721
|
+ '<td><span class="alert-severity ' + sevClass(v.severity) + '">' + v.severity + '</span></td>'
|
|
721
|
-
+ '<td>' + v.parameter + '</td>'
|
|
722
|
-
+ '<td>' + v.result + '</td>'
|
|
722
|
+
+ '<td>' + esc(v.parameter) + '</td>'
|
|
723
|
+
+ '<td>' + esc(v.result) + '</td>'
|
|
723
724
|
+ '</tr>';
|
|
724
725
|
}).join('');
|
|
725
726
|
}
|
|
@@ -745,7 +746,7 @@ function getDashboardHtml() {
|
|
|
745
746
|
$personaBars.innerHTML = entries.map(function(e) {
|
|
746
747
|
const pctVal = total > 0 ? (e[1] / total * 100) : 0;
|
|
747
748
|
return '<div class="persona-row">'
|
|
748
|
-
+ '<div class="persona-label">' + e[0] + '</div>'
|
|
749
|
+
+ '<div class="persona-label">' + esc(e[0]) + '</div>'
|
|
749
750
|
+ '<div class="persona-bar-track"><div class="persona-bar-fill" style="width:' + pctVal + '%"></div></div>'
|
|
750
751
|
+ '<div class="persona-pct">' + pctVal.toFixed(0) + '%</div>'
|
|
751
752
|
+ '</div>';
|
|
@@ -758,12 +759,10 @@ function getDashboardHtml() {
|
|
|
758
759
|
$registryList.innerHTML = '<div class="empty-state">No parameters registered</div>';
|
|
759
760
|
return;
|
|
760
761
|
}
|
|
761
|
-
// Show unique parameters from principles
|
|
762
|
-
const params = new Set();
|
|
763
762
|
$registryList.innerHTML = principles.slice(0, 30).map(function(p) {
|
|
764
763
|
return '<div class="registry-item">'
|
|
765
|
-
+ '<span class="registry-key">[' + p.id + ']</span>'
|
|
766
|
-
+ '<span class="registry-val">' + p.name + '</span>'
|
|
764
|
+
+ '<span class="registry-key">[' + esc(p.id) + ']</span>'
|
|
765
|
+
+ '<span class="registry-val">' + esc(p.name) + '</span>'
|
|
767
766
|
+ '</div>';
|
|
768
767
|
}).join('');
|
|
769
768
|
}
|
|
@@ -949,7 +948,13 @@ function getDashboardHtml() {
|
|
|
949
948
|
}
|
|
950
949
|
|
|
951
950
|
// src/routes.ts
|
|
951
|
+
function setSecurityHeaders(res) {
|
|
952
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
953
|
+
res.setHeader("X-Frame-Options", "DENY");
|
|
954
|
+
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
955
|
+
}
|
|
952
956
|
function setCorsHeaders(res, origin) {
|
|
957
|
+
setSecurityHeaders(res);
|
|
953
958
|
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
954
959
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
955
960
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
@@ -1006,21 +1011,19 @@ function createRouteHandler(server) {
|
|
|
1006
1011
|
const payload = parsed;
|
|
1007
1012
|
const state = payload["state"] ?? parsed;
|
|
1008
1013
|
const events = payload["events"];
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
return;
|
|
1017
|
-
}
|
|
1014
|
+
const validation = server.validateState ? validateEconomyState(state) : null;
|
|
1015
|
+
if (validation && !validation.valid) {
|
|
1016
|
+
json(res, 400, {
|
|
1017
|
+
error: "invalid_state",
|
|
1018
|
+
validationErrors: validation.errors
|
|
1019
|
+
}, cors);
|
|
1020
|
+
return;
|
|
1018
1021
|
}
|
|
1019
1022
|
const result = await server.processTick(
|
|
1020
1023
|
state,
|
|
1021
1024
|
Array.isArray(events) ? events : void 0
|
|
1022
1025
|
);
|
|
1023
|
-
const warnings =
|
|
1026
|
+
const warnings = validation?.warnings ?? [];
|
|
1024
1027
|
json(res, 200, {
|
|
1025
1028
|
adjustments: result.adjustments,
|
|
1026
1029
|
alerts: result.alerts.map((a) => ({
|
|
@@ -1048,12 +1051,18 @@ function createRouteHandler(server) {
|
|
|
1048
1051
|
return;
|
|
1049
1052
|
}
|
|
1050
1053
|
if (path === "/decisions" && method === "GET") {
|
|
1051
|
-
const
|
|
1052
|
-
const
|
|
1054
|
+
const rawLimit = parseInt(url.searchParams.get("limit") ?? "100", 10);
|
|
1055
|
+
const limit = Math.min(Math.max(isNaN(rawLimit) ? 100 : rawLimit, 1), 1e3);
|
|
1056
|
+
const sinceParam = url.searchParams.get("since");
|
|
1053
1057
|
const agentE = server.getAgentE();
|
|
1054
1058
|
let decisions;
|
|
1055
|
-
if (
|
|
1056
|
-
|
|
1059
|
+
if (sinceParam) {
|
|
1060
|
+
const since = parseInt(sinceParam, 10);
|
|
1061
|
+
if (isNaN(since)) {
|
|
1062
|
+
json(res, 400, { error: 'Invalid "since" parameter \u2014 must be a number' }, cors);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
decisions = agentE.getDecisions({ since });
|
|
1057
1066
|
} else {
|
|
1058
1067
|
decisions = agentE.log.latest(limit);
|
|
1059
1068
|
}
|
|
@@ -1084,6 +1093,14 @@ function createRouteHandler(server) {
|
|
|
1084
1093
|
for (const c of config["constrain"]) {
|
|
1085
1094
|
if (c && typeof c === "object" && typeof c["param"] === "string" && typeof c["min"] === "number" && typeof c["max"] === "number") {
|
|
1086
1095
|
const constraint = c;
|
|
1096
|
+
if (!isFinite(constraint.min) || !isFinite(constraint.max)) {
|
|
1097
|
+
json(res, 400, { error: "Constraint bounds must be finite numbers" }, cors);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
if (constraint.min > constraint.max) {
|
|
1101
|
+
json(res, 400, { error: "Constraint min cannot exceed max" }, cors);
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1087
1104
|
server.constrain(constraint.param, { min: constraint.min, max: constraint.max });
|
|
1088
1105
|
}
|
|
1089
1106
|
}
|
|
@@ -1140,6 +1157,7 @@ function createRouteHandler(server) {
|
|
|
1140
1157
|
}
|
|
1141
1158
|
if (path === "/" && method === "GET" && server.serveDashboard) {
|
|
1142
1159
|
setCorsHeaders(res, cors);
|
|
1160
|
+
res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; connect-src 'self' ws: wss:; img-src 'self' data:");
|
|
1143
1161
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1144
1162
|
res.end(getDashboardHtml());
|
|
1145
1163
|
return;
|
|
@@ -1259,8 +1277,10 @@ function send(ws, data) {
|
|
|
1259
1277
|
ws.send(JSON.stringify(data));
|
|
1260
1278
|
}
|
|
1261
1279
|
}
|
|
1280
|
+
var MAX_WS_PAYLOAD = 1048576;
|
|
1281
|
+
var MAX_WS_CONNECTIONS = 100;
|
|
1262
1282
|
function createWebSocketHandler(httpServer, server) {
|
|
1263
|
-
const wss = new WebSocketServer({ server: httpServer });
|
|
1283
|
+
const wss = new WebSocketServer({ server: httpServer, maxPayload: MAX_WS_PAYLOAD });
|
|
1264
1284
|
const aliveMap = /* @__PURE__ */ new WeakMap();
|
|
1265
1285
|
const heartbeatInterval = setInterval(() => {
|
|
1266
1286
|
for (const ws of wss.clients) {
|
|
@@ -1275,6 +1295,10 @@ function createWebSocketHandler(httpServer, server) {
|
|
|
1275
1295
|
}
|
|
1276
1296
|
}, 3e4);
|
|
1277
1297
|
wss.on("connection", (ws) => {
|
|
1298
|
+
if (wss.clients.size > MAX_WS_CONNECTIONS) {
|
|
1299
|
+
ws.close(1013, "Server at capacity");
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1278
1302
|
console.log("[AgentE Server] Client connected");
|
|
1279
1303
|
aliveMap.set(ws, true);
|
|
1280
1304
|
ws.on("pong", () => {
|
|
@@ -1408,7 +1432,7 @@ var AgentEServer = class {
|
|
|
1408
1432
|
this.port = config.port ?? 3100;
|
|
1409
1433
|
this.host = config.host ?? "0.0.0.0";
|
|
1410
1434
|
this.validateState = config.validateState ?? true;
|
|
1411
|
-
this.corsOrigin = config.corsOrigin ?? "
|
|
1435
|
+
this.corsOrigin = config.corsOrigin ?? "http://localhost:3100";
|
|
1412
1436
|
this.serveDashboard = config.serveDashboard ?? true;
|
|
1413
1437
|
const adapter = {
|
|
1414
1438
|
getState: () => {
|
|
@@ -1571,4 +1595,4 @@ var AgentEServer = class {
|
|
|
1571
1595
|
export {
|
|
1572
1596
|
AgentEServer
|
|
1573
1597
|
};
|
|
1574
|
-
//# sourceMappingURL=chunk-
|
|
1598
|
+
//# sourceMappingURL=chunk-53EPMEWX.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/AgentEServer.ts","../src/routes.ts","../src/dashboard.ts","../src/websocket.ts"],"sourcesContent":["// AgentEServer — HTTP + WebSocket transport for AgentE\n\nimport * as http from 'node:http';\nimport {\n AgentE,\n Observer,\n Diagnoser,\n ALL_PRINCIPLES,\n DEFAULT_THRESHOLDS,\n validateEconomyState,\n type AgentEConfig,\n type EconomyAdapter,\n type EconomyState,\n type EconomicEvent,\n type Diagnosis,\n type AgentEMode,\n type Thresholds,\n} from '@agent-e/core';\nimport { createRouteHandler } from './routes.js';\nimport { createWebSocketHandler, type WebSocketHandle } from './websocket.js';\n\nexport interface ServerConfig {\n port?: number;\n host?: string;\n agentE?: Partial<Omit<AgentEConfig, 'adapter'>>;\n validateState?: boolean;\n corsOrigin?: string;\n serveDashboard?: boolean;\n}\n\nexport interface EnrichedAdjustment {\n parameter: string;\n value: number;\n scope?: import('@agent-e/core').ParameterScope;\n reasoning: string;\n}\n\ninterface QueuedAdjustment {\n key: string;\n value: number;\n scope: import('@agent-e/core').ParameterScope | undefined;\n}\n\nexport class AgentEServer {\n private readonly agentE: AgentE;\n private readonly server: http.Server;\n private lastState: EconomyState | null = null;\n private adjustmentQueue: QueuedAdjustment[] = [];\n private alerts: Diagnosis[] = [];\n readonly port: number;\n private readonly host: string;\n private readonly thresholds: Thresholds;\n private readonly startedAt = Date.now();\n private wsHandle: WebSocketHandle | null = null;\n readonly validateState: boolean;\n readonly corsOrigin: string;\n readonly serveDashboard: boolean;\n\n constructor(config: ServerConfig = {}) {\n this.port = config.port ?? 3100;\n this.host = config.host ?? '0.0.0.0';\n this.validateState = config.validateState ?? true;\n this.corsOrigin = config.corsOrigin ?? 'http://localhost:3100';\n this.serveDashboard = config.serveDashboard ?? true;\n\n // Build a \"remote\" adapter — state comes from HTTP/WS, not polled\n const adapter: EconomyAdapter = {\n getState: () => {\n if (!this.lastState) {\n return {\n tick: 0,\n roles: [],\n resources: [],\n currencies: ['default'],\n agentBalances: {},\n agentRoles: {},\n agentInventories: {},\n marketPrices: {},\n recentTransactions: [],\n };\n }\n return this.lastState;\n },\n setParam: (key: string, value: number, scope?: import('@agent-e/core').ParameterScope) => {\n this.adjustmentQueue.push({ key, value, scope });\n },\n };\n\n const agentECfg = config.agentE ?? {};\n const agentEConfig: AgentEConfig = {\n adapter,\n mode: agentECfg.mode ?? 'autonomous',\n gracePeriod: agentECfg.gracePeriod ?? 0,\n checkInterval: agentECfg.checkInterval ?? 1,\n ...(agentECfg.dominantRoles ? { dominantRoles: agentECfg.dominantRoles } : {}),\n ...(agentECfg.idealDistribution ? { idealDistribution: agentECfg.idealDistribution } : {}),\n ...(agentECfg.maxAdjustmentPercent !== undefined ? { maxAdjustmentPercent: agentECfg.maxAdjustmentPercent } : {}),\n ...(agentECfg.cooldownTicks !== undefined ? { cooldownTicks: agentECfg.cooldownTicks } : {}),\n ...(agentECfg.thresholds ? { thresholds: agentECfg.thresholds } : {}),\n };\n\n this.thresholds = {\n ...DEFAULT_THRESHOLDS,\n ...(agentECfg.thresholds ?? {}),\n ...(agentECfg.maxAdjustmentPercent !== undefined ? { maxAdjustmentPercent: agentECfg.maxAdjustmentPercent } : {}),\n ...(agentECfg.cooldownTicks !== undefined ? { cooldownTicks: agentECfg.cooldownTicks } : {}),\n };\n this.agentE = new AgentE(agentEConfig);\n\n // Capture alerts during tick\n this.agentE.on('alert', (diagnosis: unknown) => {\n this.alerts.push(diagnosis as Diagnosis);\n });\n\n this.agentE.connect(adapter).start();\n\n // Create HTTP server\n const routeHandler = createRouteHandler(this);\n this.server = http.createServer(routeHandler);\n }\n\n async start(): Promise<void> {\n // Wire up WebSocket upgrade\n this.wsHandle = createWebSocketHandler(this.server, this);\n\n return new Promise((resolve) => {\n this.server.listen(this.port, this.host, () => {\n const addr = this.getAddress();\n console.log(`[AgentE Server] Listening on http://${addr.host}:${addr.port}`);\n resolve();\n });\n });\n }\n\n async stop(): Promise<void> {\n this.agentE.stop();\n if (this.wsHandle) this.wsHandle.cleanup();\n return new Promise((resolve, reject) => {\n this.server.close((err) => {\n if (err) reject(err);\n else resolve();\n });\n });\n }\n\n getAgentE(): AgentE {\n return this.agentE;\n }\n\n getAddress(): { port: number; host: string } {\n const addr = this.server.address();\n if (addr && typeof addr === 'object') {\n return { port: addr.port, host: addr.address };\n }\n return { port: this.port, host: this.host };\n }\n\n getUptime(): number {\n return Date.now() - this.startedAt;\n }\n\n /**\n * Process a tick with the given state.\n * 1. Clear adjustment queue\n * 2. Set state\n * 3. Ingest events\n * 4. Run agentE.tick(state)\n * 5. Drain adjustment queue, enrich with reasoning from decisions\n * 6. Return response\n */\n async processTick(\n state: EconomyState,\n events?: EconomicEvent[],\n ): Promise<{\n adjustments: EnrichedAdjustment[];\n alerts: Diagnosis[];\n health: number;\n tick: number;\n decisions: ReturnType<AgentE['getDecisions']>;\n }> {\n // Clear queues\n this.adjustmentQueue = [];\n this.alerts = [];\n\n // Set state\n this.lastState = state;\n\n // Ingest events\n if (events) {\n for (const event of events) {\n this.agentE.ingest(event);\n }\n }\n\n // Run tick\n await this.agentE.tick(state);\n\n // Drain adjustments\n const rawAdj = [...this.adjustmentQueue];\n this.adjustmentQueue = [];\n\n // Cross-reference with decision log to attach reasoning\n const decisions = this.agentE.getDecisions({ since: state.tick, until: state.tick });\n\n const adjustments: EnrichedAdjustment[] = rawAdj.map(adj => {\n const decision = decisions.find(d =>\n d.plan.parameter === adj.key && d.result === 'applied',\n );\n return {\n parameter: adj.key,\n value: adj.value,\n ...(adj.scope ? { scope: adj.scope } : {}),\n reasoning: decision?.diagnosis.violation.suggestedAction.reasoning ?? '',\n };\n });\n\n return {\n adjustments,\n alerts: [...this.alerts],\n health: this.agentE.getHealth(),\n tick: state.tick,\n decisions,\n };\n }\n\n /**\n * Run Observer + Diagnoser on the given state without side effects (no execution).\n * Computes fresh metrics from the state rather than reading stored metrics.\n */\n diagnoseOnly(state: EconomyState): {\n diagnoses: ReturnType<AgentE['diagnoseNow']>;\n health: number;\n } {\n const observer = new Observer();\n const diagnoser = new Diagnoser(ALL_PRINCIPLES);\n const metrics = observer.compute(state, []);\n const diagnoses = diagnoser.diagnose(metrics, this.thresholds);\n\n // Mirrors AgentE.getHealth() — keep in sync if that logic changes\n let health = 100;\n if (metrics.avgSatisfaction < 65) health -= 15;\n if (metrics.avgSatisfaction < 50) health -= 10;\n if (metrics.giniCoefficient > 0.45) health -= 15;\n if (metrics.giniCoefficient > 0.60) health -= 10;\n if (Math.abs(metrics.netFlow) > 10) health -= 15;\n if (Math.abs(metrics.netFlow) > 20) health -= 10;\n if (metrics.churnRate > 0.05) health -= 15;\n health = Math.max(0, Math.min(100, health));\n\n return { diagnoses, health };\n }\n\n setMode(mode: AgentEMode): void {\n this.agentE.setMode(mode);\n }\n\n lock(param: string): void {\n this.agentE.lock(param);\n }\n\n unlock(param: string): void {\n this.agentE.unlock(param);\n }\n\n constrain(param: string, bounds: { min: number; max: number }): void {\n this.agentE.constrain(param, bounds);\n }\n\n broadcast(data: Record<string, unknown>): void {\n if (this.wsHandle) this.wsHandle.broadcast(data);\n }\n}\n","// HTTP routes for AgentE Server\n// Node http module with manual body parsing. CORS on all responses.\n\nimport type * as http from 'node:http';\nimport { validateEconomyState } from '@agent-e/core';\nimport type { AgentEServer } from './AgentEServer.js';\nimport { getDashboardHtml } from './dashboard.js';\n\nfunction setSecurityHeaders(res: http.ServerResponse): void {\n res.setHeader('X-Content-Type-Options', 'nosniff');\n res.setHeader('X-Frame-Options', 'DENY');\n res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');\n}\n\nfunction setCorsHeaders(res: http.ServerResponse, origin: string): void {\n setSecurityHeaders(res);\n res.setHeader('Access-Control-Allow-Origin', origin);\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\n}\n\nfunction json(res: http.ServerResponse, status: number, data: unknown, origin: string): void {\n setCorsHeaders(res, origin);\n res.writeHead(status, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(data));\n}\n\nconst MAX_BODY_BYTES = 1_048_576; // 1 MB\n\nfunction readBody(req: http.IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let totalBytes = 0;\n req.on('data', (chunk: Buffer) => {\n totalBytes += chunk.length;\n if (totalBytes > MAX_BODY_BYTES) {\n req.destroy();\n reject(new Error('Request body too large'));\n return;\n }\n chunks.push(chunk);\n });\n req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));\n req.on('error', reject);\n });\n}\n\nexport function createRouteHandler(\n server: AgentEServer,\n): (req: http.IncomingMessage, res: http.ServerResponse) => void {\n const cors = server.corsOrigin;\n\n return async (req, res) => {\n const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);\n const path = url.pathname;\n const method = req.method?.toUpperCase() ?? 'GET';\n\n // CORS preflight\n if (method === 'OPTIONS') {\n setCorsHeaders(res, cors);\n res.writeHead(204);\n res.end();\n return;\n }\n\n try {\n // POST /tick — validate state, run tick, return adjustments/alerts/health\n if (path === '/tick' && method === 'POST') {\n const body = await readBody(req);\n let parsed: unknown;\n try {\n parsed = JSON.parse(body);\n } catch {\n json(res, 400, { error: 'Invalid JSON' }, cors);\n return;\n }\n\n if (!parsed || typeof parsed !== 'object') {\n json(res, 400, { error: 'Body must be a JSON object' }, cors);\n return;\n }\n\n const payload = parsed as Record<string, unknown>;\n const state = payload['state'] ?? parsed;\n const events = payload['events'];\n\n // Validate state (if enabled)\n const validation = server.validateState ? validateEconomyState(state) : null;\n if (validation && !validation.valid) {\n json(res, 400, {\n error: 'invalid_state',\n validationErrors: validation.errors,\n }, cors);\n return;\n }\n\n const result = await server.processTick(\n state as import('@agent-e/core').EconomyState,\n Array.isArray(events) ? events as import('@agent-e/core').EconomicEvent[] : undefined,\n );\n\n const warnings = validation?.warnings ?? [];\n\n json(res, 200, {\n adjustments: result.adjustments,\n alerts: result.alerts.map(a => ({\n principleId: a.principle.id,\n principleName: a.principle.name,\n severity: a.violation.severity,\n evidence: a.violation.evidence,\n reasoning: a.violation.suggestedAction.reasoning,\n })),\n health: result.health,\n tick: result.tick,\n ...(warnings.length > 0 ? { validationWarnings: warnings } : {}),\n }, cors);\n return;\n }\n\n // GET /health — health, tick, mode, activePlans, uptime\n if (path === '/health' && method === 'GET') {\n const agentE = server.getAgentE();\n json(res, 200, {\n health: agentE.getHealth(),\n tick: agentE.metrics.latest()?.tick ?? 0,\n mode: agentE.getMode(),\n activePlans: agentE.getActivePlans().length,\n uptime: server.getUptime(),\n }, cors);\n return;\n }\n\n // GET /decisions — decision log with optional ?limit and ?since\n if (path === '/decisions' && method === 'GET') {\n const rawLimit = parseInt(url.searchParams.get('limit') ?? '100', 10);\n const limit = Math.min(Math.max(isNaN(rawLimit) ? 100 : rawLimit, 1), 1000);\n const sinceParam = url.searchParams.get('since');\n const agentE = server.getAgentE();\n\n let decisions;\n if (sinceParam) {\n const since = parseInt(sinceParam, 10);\n if (isNaN(since)) {\n json(res, 400, { error: 'Invalid \"since\" parameter — must be a number' }, cors);\n return;\n }\n decisions = agentE.getDecisions({ since });\n } else {\n decisions = agentE.log.latest(limit);\n }\n\n json(res, 200, { decisions }, cors);\n return;\n }\n\n // POST /config — batch lock/unlock/constrain/mode\n if (path === '/config' && method === 'POST') {\n const body = await readBody(req);\n let parsed: unknown;\n try {\n parsed = JSON.parse(body);\n } catch {\n json(res, 400, { error: 'Invalid JSON' }, cors);\n return;\n }\n\n const config = parsed as Record<string, unknown>;\n\n // Lock parameters\n if (Array.isArray(config['lock'])) {\n for (const param of config['lock']) {\n if (typeof param === 'string') server.lock(param);\n }\n }\n\n // Unlock parameters\n if (Array.isArray(config['unlock'])) {\n for (const param of config['unlock']) {\n if (typeof param === 'string') server.unlock(param);\n }\n }\n\n // Constrain parameters\n if (Array.isArray(config['constrain'])) {\n for (const c of config['constrain'] as unknown[]) {\n if (\n c && typeof c === 'object' &&\n typeof (c as Record<string, unknown>)['param'] === 'string' &&\n typeof (c as Record<string, unknown>)['min'] === 'number' &&\n typeof (c as Record<string, unknown>)['max'] === 'number'\n ) {\n const constraint = c as { param: string; min: number; max: number };\n if (!isFinite(constraint.min) || !isFinite(constraint.max)) {\n json(res, 400, { error: 'Constraint bounds must be finite numbers' }, cors);\n return;\n }\n if (constraint.min > constraint.max) {\n json(res, 400, { error: 'Constraint min cannot exceed max' }, cors);\n return;\n }\n server.constrain(constraint.param, { min: constraint.min, max: constraint.max });\n }\n }\n }\n\n // Mode switch\n if (config['mode'] === 'autonomous' || config['mode'] === 'advisor') {\n server.setMode(config['mode']);\n }\n\n json(res, 200, { ok: true }, cors);\n return;\n }\n\n // GET /principles — list all principles\n if (path === '/principles' && method === 'GET') {\n const principles = server.getAgentE().getPrinciples();\n json(res, 200, {\n count: principles.length,\n principles: principles.map(p => ({\n id: p.id,\n name: p.name,\n category: p.category,\n description: p.description,\n })),\n }, cors);\n return;\n }\n\n // POST /diagnose — standalone Observer+Diagnoser (no side effects)\n if (path === '/diagnose' && method === 'POST') {\n const body = await readBody(req);\n let parsed: unknown;\n try {\n parsed = JSON.parse(body);\n } catch {\n json(res, 400, { error: 'Invalid JSON' }, cors);\n return;\n }\n\n const payload = parsed as Record<string, unknown>;\n const state = payload['state'] ?? parsed;\n\n if (server.validateState) {\n const validation = validateEconomyState(state);\n if (!validation.valid) {\n json(res, 400, { error: 'invalid_state', validationErrors: validation.errors }, cors);\n return;\n }\n }\n\n const result = server.diagnoseOnly(state as import('@agent-e/core').EconomyState);\n\n json(res, 200, {\n health: result.health,\n diagnoses: result.diagnoses.map(d => ({\n principleId: d.principle.id,\n principleName: d.principle.name,\n severity: d.violation.severity,\n evidence: d.violation.evidence,\n suggestedAction: d.violation.suggestedAction,\n })),\n }, cors);\n return;\n }\n\n // GET / — Dashboard HTML\n if (path === '/' && method === 'GET' && server.serveDashboard) {\n setCorsHeaders(res, cors);\n res.setHeader('Content-Security-Policy', \"default-src 'self'; script-src 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; connect-src 'self' ws: wss:; img-src 'self' data:\");\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\n res.end(getDashboardHtml());\n return;\n }\n\n // GET /metrics — Latest metrics + history for dashboard charts\n if (path === '/metrics' && method === 'GET') {\n const agentE = server.getAgentE();\n const latest = agentE.store.latest();\n const history = agentE.store.recentHistory(100);\n json(res, 200, { latest, history }, cors);\n return;\n }\n\n // GET /metrics/personas — Persona distribution\n if (path === '/metrics/personas' && method === 'GET') {\n const agentE = server.getAgentE();\n const latest = agentE.store.latest();\n const dist = latest.personaDistribution || {};\n const total = Object.values(dist).reduce((s: number, v) => s + (v as number), 0);\n json(res, 200, { distribution: dist, total }, cors);\n return;\n }\n\n // POST /approve — Approve advisor recommendation\n if (path === '/approve' && method === 'POST') {\n const body = await readBody(req);\n let parsed: unknown;\n try { parsed = JSON.parse(body); } catch {\n json(res, 400, { error: 'Invalid JSON' }, cors);\n return;\n }\n const payload = parsed as Record<string, unknown>;\n const decisionId = payload['decisionId'] as string;\n if (!decisionId) {\n json(res, 400, { error: 'missing_decision_id' }, cors);\n return;\n }\n\n const agentE = server.getAgentE();\n if (agentE.getMode() !== 'advisor') {\n json(res, 400, { error: 'not_in_advisor_mode' }, cors);\n return;\n }\n\n const entry = agentE.log.getById(decisionId);\n if (!entry) {\n json(res, 404, { error: 'decision_not_found' }, cors);\n return;\n }\n if (entry.result !== 'skipped_override') {\n json(res, 409, { error: 'decision_not_pending', currentResult: entry.result }, cors);\n return;\n }\n\n await agentE.apply(entry.plan);\n agentE.log.updateResult(decisionId, 'applied');\n server.broadcast({ type: 'advisor_action', action: 'approved', decisionId });\n json(res, 200, {\n ok: true,\n parameter: entry.plan.parameter,\n value: entry.plan.targetValue,\n }, cors);\n return;\n }\n\n // POST /reject — Reject advisor recommendation\n if (path === '/reject' && method === 'POST') {\n const body = await readBody(req);\n let parsed: unknown;\n try { parsed = JSON.parse(body); } catch {\n json(res, 400, { error: 'Invalid JSON' }, cors);\n return;\n }\n const payload = parsed as Record<string, unknown>;\n const decisionId = payload['decisionId'] as string;\n const reason = (payload['reason'] as string) || undefined;\n if (!decisionId) {\n json(res, 400, { error: 'missing_decision_id' }, cors);\n return;\n }\n\n const agentE = server.getAgentE();\n if (agentE.getMode() !== 'advisor') {\n json(res, 400, { error: 'not_in_advisor_mode' }, cors);\n return;\n }\n\n const entry = agentE.log.getById(decisionId);\n if (!entry) {\n json(res, 404, { error: 'decision_not_found' }, cors);\n return;\n }\n if (entry.result !== 'skipped_override') {\n json(res, 409, { error: 'decision_not_pending', currentResult: entry.result }, cors);\n return;\n }\n\n agentE.log.updateResult(decisionId, 'rejected', reason);\n server.broadcast({ type: 'advisor_action', action: 'rejected', decisionId, reason });\n json(res, 200, { ok: true, decisionId }, cors);\n return;\n }\n\n // GET /pending — List pending advisor recommendations\n if (path === '/pending' && method === 'GET') {\n const agentE = server.getAgentE();\n const pending = agentE.log.query({ result: 'skipped_override' });\n json(res, 200, {\n mode: agentE.getMode(),\n pending,\n count: pending.length,\n }, cors);\n return;\n }\n\n // 404\n json(res, 404, { error: 'Not found' }, cors);\n } catch (err) {\n console.error('[AgentE Server] Unhandled route error:', err);\n json(res, 500, { error: 'Internal server error' }, cors);\n }\n };\n}\n","// Dashboard HTML — self-contained single-page dashboard served at GET /\n// Inline CSS + Chart.js CDN + WebSocket real-time updates\n\nexport function getDashboardHtml(): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>AgentE Dashboard</title>\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap\" rel=\"stylesheet\">\n<script src=\"https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js\"></script>\n<style>\n :root {\n --bg-root: #09090b;\n --bg-panel: #18181b;\n --bg-panel-hover: #1f1f23;\n --border: #27272a;\n --border-light: #3f3f46;\n --text-primary: #f4f4f5;\n --text-secondary: #a1a1aa;\n --text-muted: #71717a;\n --text-dim: #52525b;\n --accent: #22c55e;\n --accent-dim: #166534;\n --warning: #eab308;\n --warning-dim: #854d0e;\n --danger: #ef4444;\n --danger-dim: #991b1b;\n --blue: #3b82f6;\n --font-sans: 'IBM Plex Sans', system-ui, sans-serif;\n --font-mono: 'JetBrains Mono', monospace;\n }\n\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n body {\n background: var(--bg-root);\n color: var(--text-primary);\n font-family: var(--font-sans);\n font-size: 14px;\n line-height: 1.5;\n overflow-x: hidden;\n }\n\n /* ── Header ─────────────────────────────────────── */\n .header {\n position: sticky;\n top: 0;\n z-index: 100;\n background: var(--bg-root);\n border-bottom: 1px solid var(--border);\n padding: 12px 24px;\n display: flex;\n align-items: center;\n gap: 24px;\n backdrop-filter: blur(8px);\n }\n\n .header-brand {\n font-weight: 600;\n font-size: 16px;\n color: var(--text-primary);\n white-space: nowrap;\n }\n\n .header-brand span { color: var(--accent); }\n\n .kpi-row {\n display: flex;\n gap: 20px;\n flex-wrap: wrap;\n align-items: center;\n margin-left: auto;\n }\n\n .kpi {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 13px;\n color: var(--text-secondary);\n }\n\n .kpi-value {\n font-family: var(--font-mono);\n font-weight: 500;\n color: var(--text-primary);\n font-size: 13px;\n }\n\n .kpi-value.health-good { color: var(--accent); }\n .kpi-value.health-warn { color: var(--warning); }\n .kpi-value.health-bad { color: var(--danger); }\n\n .live-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--accent);\n animation: pulse 2s ease-in-out infinite;\n }\n\n .live-dot.disconnected {\n background: var(--danger);\n animation: none;\n }\n\n @keyframes pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.4; }\n }\n\n /* ── Layout ─────────────────────────────────────── */\n .container {\n max-width: 1440px;\n margin: 0 auto;\n padding: 20px 24px;\n display: flex;\n flex-direction: column;\n gap: 16px;\n }\n\n .panel {\n background: var(--bg-panel);\n border: 1px solid var(--border);\n border-radius: 8px;\n padding: 16px;\n }\n\n .panel-title {\n font-size: 13px;\n font-weight: 600;\n color: var(--text-secondary);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin-bottom: 12px;\n }\n\n /* ── Charts grid ────────────────────────────────── */\n .charts-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));\n gap: 16px;\n }\n\n .chart-box {\n background: var(--bg-panel);\n border: 1px solid var(--border);\n border-radius: 8px;\n padding: 16px;\n }\n\n .chart-box canvas { width: 100% !important; height: 160px !important; }\n\n .chart-label {\n font-size: 12px;\n color: var(--text-muted);\n font-weight: 500;\n margin-bottom: 8px;\n }\n\n .chart-value {\n font-family: var(--font-mono);\n font-size: 22px;\n font-weight: 500;\n color: var(--text-primary);\n margin-bottom: 8px;\n }\n\n /* ── Terminal (Decision Feed) ───────────────────── */\n .terminal {\n background: var(--bg-root);\n border: 1px solid var(--border);\n border-radius: 8px;\n height: 380px;\n overflow-y: auto;\n font-family: var(--font-mono);\n font-size: 12px;\n line-height: 1.7;\n padding: 12px 16px;\n }\n\n .terminal::-webkit-scrollbar { width: 6px; }\n .terminal::-webkit-scrollbar-track { background: transparent; }\n .terminal::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 3px; }\n\n .term-line {\n white-space: nowrap;\n opacity: 0;\n transform: translateY(4px);\n animation: termIn 0.3s ease-out forwards;\n }\n\n @keyframes termIn {\n to { opacity: 1; transform: translateY(0); }\n }\n\n .t-tick { color: var(--text-dim); }\n .t-ok { color: var(--accent); }\n .t-skip { color: var(--warning); }\n .t-fail { color: var(--danger); }\n .t-principle { color: var(--text-primary); font-weight: 500; }\n .t-param { color: var(--text-secondary); }\n .t-old { color: #d4d4d8; font-variant-numeric: tabular-nums; }\n .t-arrow { color: var(--text-dim); }\n .t-new { color: var(--accent); font-variant-numeric: tabular-nums; }\n .t-meta { color: var(--text-dim); }\n\n /* ── Alerts ─────────────────────────────────────── */\n .alerts-container {\n display: flex;\n flex-direction: column;\n gap: 8px;\n max-height: 320px;\n overflow-y: auto;\n }\n\n .alert-card {\n display: flex;\n align-items: flex-start;\n gap: 12px;\n padding: 12px;\n border-radius: 6px;\n border: 1px solid var(--border);\n background: var(--bg-panel);\n transition: opacity 0.3s, transform 0.3s;\n }\n\n .alert-card.fade-out {\n opacity: 0;\n transform: translateX(20px);\n }\n\n .alert-severity {\n font-family: var(--font-mono);\n font-weight: 600;\n font-size: 13px;\n padding: 2px 8px;\n border-radius: 4px;\n white-space: nowrap;\n }\n\n .sev-high { background: var(--danger-dim); color: var(--danger); }\n .sev-med { background: var(--warning-dim); color: var(--warning); }\n .sev-low { background: var(--accent-dim); color: var(--accent); }\n\n .alert-body { flex: 1; }\n .alert-principle { font-weight: 500; font-size: 13px; }\n .alert-reason { color: var(--text-secondary); font-size: 12px; margin-top: 2px; }\n\n /* ── Violations table ──────────────────────────── */\n .violations-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 12px;\n }\n\n .violations-table th {\n text-align: left;\n color: var(--text-muted);\n font-weight: 500;\n padding: 6px 10px;\n border-bottom: 1px solid var(--border);\n cursor: pointer;\n user-select: none;\n }\n\n .violations-table th:hover { color: var(--text-secondary); }\n\n .violations-table td {\n padding: 6px 10px;\n border-bottom: 1px solid var(--border);\n color: var(--text-secondary);\n font-family: var(--font-mono);\n font-size: 11px;\n }\n\n .violations-table tr:hover td { background: var(--bg-panel-hover); }\n\n /* ── Split row ─────────────────────────────────── */\n .split-row {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 16px;\n }\n\n @media (max-width: 800px) {\n .split-row { grid-template-columns: 1fr; }\n }\n\n /* ── Persona bar chart ─────────────────────────── */\n .persona-bars { display: flex; flex-direction: column; gap: 6px; }\n\n .persona-row {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n }\n\n .persona-label {\n width: 100px;\n text-align: right;\n color: var(--text-secondary);\n font-size: 11px;\n flex-shrink: 0;\n }\n\n .persona-bar-track {\n flex: 1;\n height: 16px;\n background: var(--bg-root);\n border-radius: 3px;\n overflow: hidden;\n }\n\n .persona-bar-fill {\n height: 100%;\n background: var(--accent);\n border-radius: 3px;\n transition: width 0.5s ease;\n }\n\n .persona-pct {\n width: 40px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-muted);\n }\n\n /* ── Registry list ─────────────────────────────── */\n .registry-list { display: flex; flex-direction: column; gap: 4px; }\n\n .registry-item {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 6px 10px;\n border-radius: 4px;\n font-size: 12px;\n }\n\n .registry-item:nth-child(odd) { background: rgba(255,255,255,0.02); }\n .registry-key { color: var(--text-secondary); font-family: var(--font-mono); }\n .registry-val { color: var(--accent); font-family: var(--font-mono); font-weight: 500; }\n\n /* ── Advisor mode ──────────────────────────────── */\n .advisor-banner {\n display: none;\n background: var(--warning-dim);\n border: 1px solid var(--warning);\n color: var(--warning);\n padding: 8px 16px;\n border-radius: 6px;\n font-size: 13px;\n font-weight: 500;\n align-items: center;\n gap: 8px;\n }\n\n .advisor-mode .advisor-banner { display: flex; }\n\n .pending-pill {\n background: var(--warning);\n color: var(--bg-root);\n font-size: 11px;\n font-weight: 600;\n padding: 1px 8px;\n border-radius: 10px;\n font-family: var(--font-mono);\n }\n\n .advisor-btn {\n font-family: var(--font-mono);\n font-size: 11px;\n padding: 2px 10px;\n border-radius: 4px;\n border: none;\n cursor: pointer;\n font-weight: 500;\n transition: opacity 0.15s;\n }\n\n .advisor-btn:hover { opacity: 0.85; }\n .advisor-btn.approve { background: var(--accent); color: var(--bg-root); }\n .advisor-btn.reject { background: var(--danger); color: #fff; }\n\n .advisor-actions { display: none; gap: 6px; margin-left: 8px; }\n .advisor-mode .advisor-actions { display: inline-flex; }\n\n /* ── Empty state ───────────────────────────────── */\n .empty-state {\n color: var(--text-dim);\n font-size: 13px;\n text-align: center;\n padding: 40px 20px;\n }\n\n /* ── Reduced motion ────────────────────────────── */\n @media (prefers-reduced-motion: reduce) {\n .term-line { animation: none; opacity: 1; transform: none; }\n .live-dot { animation: none; }\n .persona-bar-fill { transition: none; }\n }\n</style>\n</head>\n<body>\n\n<!-- Header -->\n<div class=\"header\" id=\"header\">\n <div class=\"header-brand\">Agent<span>E</span> v1.6</div>\n <div class=\"kpi-row\">\n <div class=\"kpi\">Health <span class=\"kpi-value health-good\" id=\"kpi-health\">--</span></div>\n <div class=\"kpi\">Mode <span class=\"kpi-value\" id=\"kpi-mode\">--</span></div>\n <div class=\"kpi\">Tick <span class=\"kpi-value\" id=\"kpi-tick\">0</span></div>\n <div class=\"kpi\">Uptime <span class=\"kpi-value\" id=\"kpi-uptime\">0s</span></div>\n <div class=\"kpi\">Plans <span class=\"kpi-value\" id=\"kpi-plans\">0</span></div>\n <div class=\"live-dot\" id=\"live-dot\" title=\"WebSocket connected\"></div>\n </div>\n</div>\n\n<div class=\"container\" id=\"app\">\n <!-- Advisor banner -->\n <div class=\"advisor-banner\" id=\"advisor-banner\">\n ADVISOR MODE — Recommendations require manual approval\n <span class=\"pending-pill\" id=\"pending-count\">0</span> pending\n </div>\n\n <!-- Charts -->\n <div class=\"charts-grid\">\n <div class=\"chart-box\">\n <div class=\"chart-label\">Economy Health</div>\n <div class=\"chart-value\" id=\"cv-health\">--</div>\n <canvas id=\"chart-health\"></canvas>\n </div>\n <div class=\"chart-box\">\n <div class=\"chart-label\">Gini Coefficient</div>\n <div class=\"chart-value\" id=\"cv-gini\">--</div>\n <canvas id=\"chart-gini\"></canvas>\n </div>\n <div class=\"chart-box\">\n <div class=\"chart-label\">Net Flow</div>\n <div class=\"chart-value\" id=\"cv-netflow\">--</div>\n <canvas id=\"chart-netflow\"></canvas>\n </div>\n <div class=\"chart-box\">\n <div class=\"chart-label\">Avg Satisfaction</div>\n <div class=\"chart-value\" id=\"cv-satisfaction\">--</div>\n <canvas id=\"chart-satisfaction\"></canvas>\n </div>\n </div>\n\n <!-- Decision Feed -->\n <div class=\"panel\">\n <div class=\"panel-title\">Decision Feed</div>\n <div class=\"terminal\" id=\"terminal\"></div>\n </div>\n\n <!-- Active Alerts -->\n <div class=\"panel\">\n <div class=\"panel-title\">Active Alerts</div>\n <div class=\"alerts-container\" id=\"alerts-container\">\n <div class=\"empty-state\" id=\"alerts-empty\">No active violations</div>\n </div>\n </div>\n\n <!-- Violation History -->\n <div class=\"panel\">\n <div class=\"panel-title\">Violation History</div>\n <div style=\"max-height:320px;overflow-y:auto\">\n <table class=\"violations-table\" id=\"violations-table\">\n <thead>\n <tr>\n <th data-sort=\"tick\">Tick</th>\n <th data-sort=\"principle\">Principle</th>\n <th data-sort=\"severity\">Severity</th>\n <th data-sort=\"parameter\">Parameter</th>\n <th data-sort=\"result\">Result</th>\n </tr>\n </thead>\n <tbody id=\"violations-body\"></tbody>\n </table>\n </div>\n </div>\n\n <!-- Split: Personas + Registry -->\n <div class=\"split-row\">\n <div class=\"panel\">\n <div class=\"panel-title\">Persona Distribution</div>\n <div class=\"persona-bars\" id=\"persona-bars\">\n <div class=\"empty-state\">No persona data yet</div>\n </div>\n </div>\n <div class=\"panel\">\n <div class=\"panel-title\">Parameter Registry</div>\n <div class=\"registry-list\" id=\"registry-list\">\n <div class=\"empty-state\">No parameters registered</div>\n </div>\n </div>\n </div>\n</div>\n\n<script>\n(function() {\n 'use strict';\n\n // ── State ────────────────────────────────────────\n let ws = null;\n let reconnectDelay = 1000;\n const MAX_RECONNECT = 30000;\n let isAdvisor = false;\n let pendingDecisions = [];\n const MAX_TERMINAL_LINES = 80;\n const MAX_VIOLATIONS = 100;\n let violationSortKey = 'tick';\n let violationSortAsc = false;\n let violations = [];\n\n // Chart instances\n let chartHealth, chartGini, chartNetflow, chartSatisfaction;\n\n // ── DOM refs ─────────────────────────────────────\n const $kpiHealth = document.getElementById('kpi-health');\n const $kpiMode = document.getElementById('kpi-mode');\n const $kpiTick = document.getElementById('kpi-tick');\n const $kpiUptime = document.getElementById('kpi-uptime');\n const $kpiPlans = document.getElementById('kpi-plans');\n const $liveDot = document.getElementById('live-dot');\n const $terminal = document.getElementById('terminal');\n const $alertsContainer = document.getElementById('alerts-container');\n const $alertsEmpty = document.getElementById('alerts-empty');\n const $violationsBody = document.getElementById('violations-body');\n const $personaBars = document.getElementById('persona-bars');\n const $registryList = document.getElementById('registry-list');\n const $pendingCount = document.getElementById('pending-count');\n const $app = document.getElementById('app');\n\n // ── Helpers ──────────────────────────────────────\n function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\"/g,'"').replace(/'/g,'''); }\n function pad(n, w) { return String(n).padStart(w || 4, ' '); }\n function fmt(n) { return typeof n === 'number' ? n.toFixed(3) : '—'; }\n function pct(n) { return typeof n === 'number' ? (n * 100).toFixed(0) + '%' : '—'; }\n\n function formatUptime(ms) {\n const s = Math.floor(ms / 1000);\n if (s < 60) return s + 's';\n if (s < 3600) return Math.floor(s / 60) + 'm ' + (s % 60) + 's';\n const h = Math.floor(s / 3600);\n return h + 'h ' + Math.floor((s % 3600) / 60) + 'm';\n }\n\n function healthClass(h) {\n if (h >= 70) return 'health-good';\n if (h >= 40) return 'health-warn';\n return 'health-bad';\n }\n\n function sevClass(s) {\n if (s >= 7) return 'sev-high';\n if (s >= 4) return 'sev-med';\n return 'sev-low';\n }\n\n // ── Chart setup ──────────────────────────────────\n const chartOpts = {\n responsive: true,\n maintainAspectRatio: false,\n animation: { duration: 300 },\n plugins: { legend: { display: false } },\n scales: {\n x: { display: false },\n y: {\n ticks: { color: '#71717a', font: { family: \"'JetBrains Mono'\", size: 10 } },\n grid: { color: 'rgba(63,63,70,0.3)' },\n border: { display: false },\n }\n },\n elements: {\n point: { radius: 0 },\n line: { borderWidth: 1.5, tension: 0.3 },\n }\n };\n\n function makeChart(id, color, minY, maxY) {\n const ctx = document.getElementById(id).getContext('2d');\n const opts = JSON.parse(JSON.stringify(chartOpts));\n if (minY !== undefined) opts.scales.y.min = minY;\n if (maxY !== undefined) opts.scales.y.max = maxY;\n return new Chart(ctx, {\n type: 'line',\n data: {\n labels: [],\n datasets: [{\n data: [],\n borderColor: color,\n backgroundColor: color + '18',\n fill: true,\n }]\n },\n options: opts,\n });\n }\n\n function initCharts() {\n chartHealth = makeChart('chart-health', '#22c55e', 0, 100);\n chartGini = makeChart('chart-gini', '#eab308', 0, 1);\n chartNetflow = makeChart('chart-netflow', '#3b82f6');\n chartSatisfaction = makeChart('chart-satisfaction', '#22c55e', 0, 100);\n }\n\n function updateChart(chart, labels, data) {\n chart.data.labels = labels;\n chart.data.datasets[0].data = data;\n chart.update('none');\n }\n\n // ── Terminal ─────────────────────────────────────\n function addTerminalLine(html) {\n const el = document.createElement('div');\n el.className = 'term-line';\n el.innerHTML = html;\n $terminal.appendChild(el);\n while ($terminal.children.length > MAX_TERMINAL_LINES) {\n $terminal.removeChild($terminal.firstChild);\n }\n $terminal.scrollTop = $terminal.scrollHeight;\n }\n\n function decisionToTerminal(d) {\n const resultIcon = d.result === 'applied'\n ? '<span class=\"t-ok\">\\\\u2705 </span>'\n : d.result === 'rejected'\n ? '<span class=\"t-fail\">\\\\u274c </span>'\n : '<span class=\"t-skip\">\\\\u23f8 </span>';\n\n const principle = d.diagnosis?.principle || {};\n const plan = d.plan || {};\n const severity = d.diagnosis?.violation?.severity ?? '?';\n const confidence = d.diagnosis?.violation?.confidence;\n const confStr = confidence != null ? (confidence * 100).toFixed(0) + '%' : '?';\n\n let advisorBtns = '';\n if (isAdvisor && d.result === 'skipped_override') {\n advisorBtns = '<span class=\"advisor-actions\">'\n + '<button class=\"advisor-btn approve\" onclick=\"window._approve(\\\\'' + esc(d.id) + '\\\\')\">[Approve]</button>'\n + '<button class=\"advisor-btn reject\" onclick=\"window._reject(\\\\'' + esc(d.id) + '\\\\')\">[Reject]</button>'\n + '</span>';\n }\n\n return '<span class=\"t-tick\">[Tick ' + pad(d.tick) + ']</span> '\n + resultIcon\n + '<span class=\"t-principle\">[' + esc(principle.id || '?') + '] ' + esc(principle.name || '') + ':</span> '\n + '<span class=\"t-param\">' + esc(plan.parameter || '—') + ' </span>'\n + '<span class=\"t-old\">' + fmt(plan.currentValue) + '</span>'\n + '<span class=\"t-arrow\"> \\\\u2192 </span>'\n + '<span class=\"t-new\">' + fmt(plan.targetValue) + '</span>'\n + '<span class=\"t-meta\"> severity ' + severity + '/10, confidence ' + confStr + '</span>'\n + advisorBtns;\n }\n\n // ── Alerts ───────────────────────────────────────\n function renderAlerts(alerts) {\n if (!alerts || alerts.length === 0) {\n $alertsContainer.innerHTML = '<div class=\"empty-state\">No active violations</div>';\n return;\n }\n const sorted = [...alerts].sort((a, b) => (b.severity || 0) - (a.severity || 0));\n $alertsContainer.innerHTML = sorted.map(function(a) {\n const sev = a.severity || a.violation?.severity || 0;\n const sc = sevClass(sev);\n const name = a.principleName || a.principle?.name || '?';\n const pid = a.principleId || a.principle?.id || '?';\n const reason = a.reasoning || a.violation?.suggestedAction?.reasoning || '';\n return '<div class=\"alert-card\">'\n + '<span class=\"alert-severity ' + sc + '\">' + sev + '/10</span>'\n + '<div class=\"alert-body\">'\n + '<div class=\"alert-principle\">[' + esc(pid) + '] ' + esc(name) + '</div>'\n + '<div class=\"alert-reason\">' + esc(reason) + '</div>'\n + '</div></div>';\n }).join('');\n }\n\n // ── Violations table ─────────────────────────────\n function addViolation(d) {\n violations.push({\n tick: d.tick,\n principle: (d.diagnosis?.principle?.id || '?') + ' ' + (d.diagnosis?.principle?.name || ''),\n severity: d.diagnosis?.violation?.severity || 0,\n parameter: d.plan?.parameter || '—',\n result: d.result,\n });\n if (violations.length > MAX_VIOLATIONS) violations.shift();\n renderViolations();\n }\n\n function renderViolations() {\n const sorted = [...violations].sort(function(a, b) {\n const va = a[violationSortKey], vb = b[violationSortKey];\n if (va < vb) return violationSortAsc ? -1 : 1;\n if (va > vb) return violationSortAsc ? 1 : -1;\n return 0;\n });\n $violationsBody.innerHTML = sorted.map(function(v) {\n return '<tr>'\n + '<td>' + v.tick + '</td>'\n + '<td style=\"color:var(--text-primary);font-family:var(--font-sans)\">' + esc(v.principle) + '</td>'\n + '<td><span class=\"alert-severity ' + sevClass(v.severity) + '\">' + v.severity + '</span></td>'\n + '<td>' + esc(v.parameter) + '</td>'\n + '<td>' + esc(v.result) + '</td>'\n + '</tr>';\n }).join('');\n }\n\n // Table sorting\n document.querySelectorAll('.violations-table th').forEach(function(th) {\n th.addEventListener('click', function() {\n const key = th.dataset.sort;\n if (violationSortKey === key) violationSortAsc = !violationSortAsc;\n else { violationSortKey = key; violationSortAsc = true; }\n renderViolations();\n });\n });\n\n // ── Personas ─────────────────────────────────────\n function renderPersonas(dist) {\n if (!dist || Object.keys(dist).length === 0) {\n $personaBars.innerHTML = '<div class=\"empty-state\">No persona data yet</div>';\n return;\n }\n const total = Object.values(dist).reduce(function(s, v) { return s + v; }, 0);\n const entries = Object.entries(dist).sort(function(a, b) { return b[1] - a[1]; });\n $personaBars.innerHTML = entries.map(function(e) {\n const pctVal = total > 0 ? (e[1] / total * 100) : 0;\n return '<div class=\"persona-row\">'\n + '<div class=\"persona-label\">' + esc(e[0]) + '</div>'\n + '<div class=\"persona-bar-track\"><div class=\"persona-bar-fill\" style=\"width:' + pctVal + '%\"></div></div>'\n + '<div class=\"persona-pct\">' + pctVal.toFixed(0) + '%</div>'\n + '</div>';\n }).join('');\n }\n\n // ── Registry ─────────────────────────────────────\n function renderRegistry(principles) {\n if (!principles || principles.length === 0) {\n $registryList.innerHTML = '<div class=\"empty-state\">No parameters registered</div>';\n return;\n }\n $registryList.innerHTML = principles.slice(0, 30).map(function(p) {\n return '<div class=\"registry-item\">'\n + '<span class=\"registry-key\">[' + esc(p.id) + ']</span>'\n + '<span class=\"registry-val\">' + esc(p.name) + '</span>'\n + '</div>';\n }).join('');\n }\n\n // ── KPI update ───────────────────────────────────\n function updateKPIs(data) {\n if (data.health != null) {\n $kpiHealth.textContent = data.health + '/100';\n $kpiHealth.className = 'kpi-value ' + healthClass(data.health);\n document.getElementById('cv-health').textContent = data.health + '/100';\n }\n if (data.mode != null) {\n $kpiMode.textContent = data.mode;\n isAdvisor = data.mode === 'advisor';\n $app.classList.toggle('advisor-mode', isAdvisor);\n }\n if (data.tick != null) $kpiTick.textContent = data.tick;\n if (data.uptime != null) $kpiUptime.textContent = formatUptime(data.uptime);\n if (data.activePlans != null) $kpiPlans.textContent = data.activePlans;\n }\n\n // ── Metrics history ──────────────────────────────\n function updateChartsFromHistory(history) {\n if (!history || history.length === 0) return;\n const ticks = history.map(function(h) { return h.tick; });\n updateChart(chartHealth, ticks, history.map(function(h) { return h.health; }));\n updateChart(chartGini, ticks, history.map(function(h) { return h.giniCoefficient; }));\n updateChart(chartNetflow, ticks, history.map(function(h) { return h.netFlow; }));\n updateChart(chartSatisfaction, ticks, history.map(function(h) { return h.avgSatisfaction; }));\n\n const last = history[history.length - 1];\n document.getElementById('cv-gini').textContent = last.giniCoefficient.toFixed(3);\n document.getElementById('cv-netflow').textContent = last.netFlow.toFixed(1);\n document.getElementById('cv-satisfaction').textContent = last.avgSatisfaction.toFixed(0) + '/100';\n }\n\n // ── API calls ────────────────────────────────────\n function fetchJSON(path) {\n return fetch(path).then(function(r) { return r.json(); });\n }\n\n function postJSON(path, body) {\n return fetch(path, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n }).then(function(r) { return r.json(); });\n }\n\n function loadInitialData() {\n fetchJSON('/health').then(function(data) {\n updateKPIs(data);\n }).catch(function() {});\n\n fetchJSON('/decisions?limit=50').then(function(data) {\n if (data.decisions) {\n data.decisions.reverse().forEach(function(d) {\n addTerminalLine(decisionToTerminal(d));\n addViolation(d);\n });\n }\n }).catch(function() {});\n\n fetchJSON('/metrics').then(function(data) {\n if (data.history) updateChartsFromHistory(data.history);\n if (data.latest) {\n renderPersonas(data.latest.personaDistribution);\n }\n }).catch(function() {});\n\n fetchJSON('/principles').then(function(data) {\n if (data.principles) renderRegistry(data.principles);\n }).catch(function() {});\n\n fetchJSON('/pending').then(function(data) {\n if (data.pending) {\n pendingDecisions = data.pending;\n $pendingCount.textContent = data.count || 0;\n }\n }).catch(function() {});\n }\n\n // ── Polling fallback ─────────────────────────────\n let pollInterval = null;\n\n function startPolling() {\n if (pollInterval) return;\n pollInterval = setInterval(function() {\n fetchJSON('/health').then(updateKPIs).catch(function() {});\n fetchJSON('/metrics').then(function(data) {\n if (data.history) updateChartsFromHistory(data.history);\n if (data.latest) renderPersonas(data.latest.personaDistribution);\n }).catch(function() {});\n }, 5000);\n }\n\n function stopPolling() {\n if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }\n }\n\n // ── WebSocket ────────────────────────────────────\n function connectWS() {\n const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';\n ws = new WebSocket(proto + '//' + location.host);\n\n ws.onopen = function() {\n reconnectDelay = 1000;\n $liveDot.classList.remove('disconnected');\n $liveDot.title = 'WebSocket connected';\n stopPolling();\n // Request fresh health\n ws.send(JSON.stringify({ type: 'health' }));\n };\n\n ws.onclose = function() {\n $liveDot.classList.add('disconnected');\n $liveDot.title = 'WebSocket disconnected — reconnecting...';\n startPolling();\n setTimeout(connectWS, reconnectDelay);\n reconnectDelay = Math.min(reconnectDelay * 1.5, MAX_RECONNECT);\n };\n\n ws.onerror = function() { ws.close(); };\n\n ws.onmessage = function(ev) {\n var msg;\n try { msg = JSON.parse(ev.data); } catch(e) { return; }\n\n switch (msg.type) {\n case 'tick_result':\n updateKPIs({ health: msg.health, tick: msg.tick });\n if (msg.alerts) renderAlerts(msg.alerts);\n // Refresh charts\n fetchJSON('/metrics').then(function(data) {\n if (data.history) updateChartsFromHistory(data.history);\n if (data.latest) renderPersonas(data.latest.personaDistribution);\n }).catch(function() {});\n break;\n\n case 'health_result':\n updateKPIs(msg);\n break;\n\n case 'advisor_action':\n if (msg.action === 'approved' || msg.action === 'rejected') {\n pendingDecisions = pendingDecisions.filter(function(d) {\n return d.id !== msg.decisionId;\n });\n $pendingCount.textContent = pendingDecisions.length;\n }\n break;\n }\n };\n }\n\n // ── Advisor actions ──────────────────────────────\n window._approve = function(id) {\n postJSON('/approve', { decisionId: id }).then(function(data) {\n if (data.ok) {\n addTerminalLine('<span class=\"t-tick\">[Advisor]</span> <span class=\"t-ok\">\\\\u2705 Approved ' + id + '</span>');\n }\n }).catch(function() {});\n };\n\n window._reject = function(id) {\n var reason = prompt('Rejection reason (optional):');\n postJSON('/reject', { decisionId: id, reason: reason || undefined }).then(function(data) {\n if (data.ok) {\n addTerminalLine('<span class=\"t-tick\">[Advisor]</span> <span class=\"t-fail\">\\\\u274c Rejected ' + id + '</span>');\n }\n }).catch(function() {});\n };\n\n // ── Init ─────────────────────────────────────────\n initCharts();\n loadInitialData();\n connectWS();\n\n})();\n</script>\n</body>\n</html>`;\n}\n","// WebSocket handler for AgentE Server\n// Same port via HTTP upgrade. JSON messages with `type` field.\n\nimport type * as http from 'node:http';\nimport { WebSocketServer, WebSocket } from 'ws';\nimport { validateEconomyState, type EconomyState, type EconomicEvent } from '@agent-e/core';\nimport type { AgentEServer } from './AgentEServer.js';\n\ninterface IncomingMessage {\n type: string;\n [key: string]: unknown;\n}\n\nfunction send(ws: WebSocket, data: Record<string, unknown>): void {\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify(data));\n }\n}\n\nexport interface WebSocketHandle {\n cleanup: () => void;\n broadcast: (data: Record<string, unknown>) => void;\n}\n\nconst MAX_WS_PAYLOAD = 1_048_576; // 1 MB\nconst MAX_WS_CONNECTIONS = 100;\n\nexport function createWebSocketHandler(\n httpServer: http.Server,\n server: AgentEServer,\n): WebSocketHandle {\n const wss = new WebSocketServer({ server: httpServer, maxPayload: MAX_WS_PAYLOAD });\n\n // Heartbeat: ping every 30s, disconnect if no pong within 10s\n const aliveMap = new WeakMap<WebSocket, boolean>();\n\n const heartbeatInterval = setInterval(() => {\n for (const ws of wss.clients) {\n if (ws.readyState === WebSocket.OPEN) {\n if (aliveMap.get(ws) === false) {\n // No pong received since last ping — terminate\n ws.terminate();\n continue;\n }\n aliveMap.set(ws, false);\n ws.ping();\n }\n }\n }, 30_000);\n\n wss.on('connection', (ws) => {\n if (wss.clients.size > MAX_WS_CONNECTIONS) {\n ws.close(1013, 'Server at capacity');\n return;\n }\n console.log('[AgentE Server] Client connected');\n aliveMap.set(ws, true);\n\n ws.on('pong', () => {\n aliveMap.set(ws, true);\n });\n\n ws.on('close', () => {\n console.log('[AgentE Server] Client disconnected');\n });\n\n ws.on('message', async (raw) => {\n let msg: IncomingMessage;\n try {\n msg = JSON.parse(raw.toString()) as IncomingMessage;\n } catch {\n send(ws, { type: 'error', message: 'Malformed JSON' });\n return;\n }\n\n if (!msg.type || typeof msg.type !== 'string') {\n send(ws, { type: 'error', message: 'Missing \"type\" field' });\n return;\n }\n\n switch (msg.type) {\n case 'tick': {\n const state = msg['state'];\n const events = msg['events'];\n\n if (server.validateState) {\n const validation = validateEconomyState(state);\n if (!validation.valid) {\n send(ws, { type: 'validation_error', validationErrors: validation.errors });\n return;\n }\n\n // Forward warnings even if valid\n if (validation.warnings.length > 0) {\n send(ws, { type: 'validation_warning', validationWarnings: validation.warnings });\n }\n }\n\n try {\n const result = await server.processTick(\n state as EconomyState,\n Array.isArray(events) ? events as EconomicEvent[] : undefined,\n );\n\n send(ws, {\n type: 'tick_result',\n adjustments: result.adjustments,\n alerts: result.alerts.map(a => ({\n principleId: a.principle.id,\n principleName: a.principle.name,\n severity: a.violation.severity,\n reasoning: a.violation.suggestedAction.reasoning,\n })),\n health: result.health,\n tick: result.tick,\n });\n } catch (err) {\n send(ws, { type: 'error', message: 'Tick processing failed' });\n }\n break;\n }\n\n case 'event': {\n const event = msg['event'] as EconomicEvent | undefined;\n if (event) {\n server.getAgentE().ingest(event);\n send(ws, { type: 'event_ack' });\n } else {\n send(ws, { type: 'error', message: 'Missing \"event\" field' });\n }\n break;\n }\n\n case 'health': {\n const agentE = server.getAgentE();\n send(ws, {\n type: 'health_result',\n health: agentE.getHealth(),\n tick: agentE.metrics.latest()?.tick ?? 0,\n mode: agentE.getMode(),\n activePlans: agentE.getActivePlans().length,\n uptime: server.getUptime(),\n });\n break;\n }\n\n case 'diagnose': {\n const state = msg['state'];\n\n if (server.validateState) {\n const validation = validateEconomyState(state);\n if (!validation.valid) {\n send(ws, { type: 'validation_error', validationErrors: validation.errors });\n return;\n }\n }\n\n const result = server.diagnoseOnly(state as EconomyState);\n send(ws, {\n type: 'diagnose_result',\n health: result.health,\n diagnoses: result.diagnoses.map(d => ({\n principleId: d.principle.id,\n principleName: d.principle.name,\n severity: d.violation.severity,\n suggestedAction: d.violation.suggestedAction,\n })),\n });\n break;\n }\n\n default:\n send(ws, { type: 'error', message: `Unknown message type: \"${msg.type}\"` });\n }\n });\n });\n\n function broadcast(data: Record<string, unknown>): void {\n const payload = JSON.stringify(data);\n for (const ws of wss.clients) {\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(payload);\n }\n }\n }\n\n return {\n cleanup: () => {\n clearInterval(heartbeatInterval);\n wss.close();\n },\n broadcast,\n };\n}\n"],"mappings":";AAEA,YAAY,UAAU;AACtB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OASK;;;ACbP,SAAS,4BAA4B;;;ACD9B,SAAS,mBAA2B;AACzC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAo6BT;;;ADh6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","names":["validateEconomyState"]}
|
package/dist/cli.js
CHANGED
|
@@ -569,6 +569,7 @@ function getDashboardHtml() {
|
|
|
569
569
|
const $app = document.getElementById('app');
|
|
570
570
|
|
|
571
571
|
// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
572
|
+
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
|
572
573
|
function pad(n, w) { return String(n).padStart(w || 4, ' '); }
|
|
573
574
|
function fmt(n) { return typeof n === 'number' ? n.toFixed(3) : '\u2014'; }
|
|
574
575
|
function pct(n) { return typeof n === 'number' ? (n * 100).toFixed(0) + '%' : '\u2014'; }
|
|
@@ -674,15 +675,15 @@ function getDashboardHtml() {
|
|
|
674
675
|
let advisorBtns = '';
|
|
675
676
|
if (isAdvisor && d.result === 'skipped_override') {
|
|
676
677
|
advisorBtns = '<span class="advisor-actions">'
|
|
677
|
-
+ '<button class="advisor-btn approve" onclick="window._approve(\\'' + d.id + '\\')">[Approve]</button>'
|
|
678
|
-
+ '<button class="advisor-btn reject" onclick="window._reject(\\'' + d.id + '\\')">[Reject]</button>'
|
|
678
|
+
+ '<button class="advisor-btn approve" onclick="window._approve(\\'' + esc(d.id) + '\\')">[Approve]</button>'
|
|
679
|
+
+ '<button class="advisor-btn reject" onclick="window._reject(\\'' + esc(d.id) + '\\')">[Reject]</button>'
|
|
679
680
|
+ '</span>';
|
|
680
681
|
}
|
|
681
682
|
|
|
682
683
|
return '<span class="t-tick">[Tick ' + pad(d.tick) + ']</span> '
|
|
683
684
|
+ resultIcon
|
|
684
|
-
+ '<span class="t-principle">[' + (principle.id || '?') + '] ' + (principle.name || '') + ':</span> '
|
|
685
|
-
+ '<span class="t-param">' + (plan.parameter || '\u2014') + ' </span>'
|
|
685
|
+
+ '<span class="t-principle">[' + esc(principle.id || '?') + '] ' + esc(principle.name || '') + ':</span> '
|
|
686
|
+
+ '<span class="t-param">' + esc(plan.parameter || '\u2014') + ' </span>'
|
|
686
687
|
+ '<span class="t-old">' + fmt(plan.currentValue) + '</span>'
|
|
687
688
|
+ '<span class="t-arrow"> \\u2192 </span>'
|
|
688
689
|
+ '<span class="t-new">' + fmt(plan.targetValue) + '</span>'
|
|
@@ -706,8 +707,8 @@ function getDashboardHtml() {
|
|
|
706
707
|
return '<div class="alert-card">'
|
|
707
708
|
+ '<span class="alert-severity ' + sc + '">' + sev + '/10</span>'
|
|
708
709
|
+ '<div class="alert-body">'
|
|
709
|
-
+ '<div class="alert-principle">[' + pid + '] ' + name + '</div>'
|
|
710
|
-
+ '<div class="alert-reason">' + reason + '</div>'
|
|
710
|
+
+ '<div class="alert-principle">[' + esc(pid) + '] ' + esc(name) + '</div>'
|
|
711
|
+
+ '<div class="alert-reason">' + esc(reason) + '</div>'
|
|
711
712
|
+ '</div></div>';
|
|
712
713
|
}).join('');
|
|
713
714
|
}
|
|
@@ -735,10 +736,10 @@ function getDashboardHtml() {
|
|
|
735
736
|
$violationsBody.innerHTML = sorted.map(function(v) {
|
|
736
737
|
return '<tr>'
|
|
737
738
|
+ '<td>' + v.tick + '</td>'
|
|
738
|
-
+ '<td style="color:var(--text-primary);font-family:var(--font-sans)">' + v.principle + '</td>'
|
|
739
|
+
+ '<td style="color:var(--text-primary);font-family:var(--font-sans)">' + esc(v.principle) + '</td>'
|
|
739
740
|
+ '<td><span class="alert-severity ' + sevClass(v.severity) + '">' + v.severity + '</span></td>'
|
|
740
|
-
+ '<td>' + v.parameter + '</td>'
|
|
741
|
-
+ '<td>' + v.result + '</td>'
|
|
741
|
+
+ '<td>' + esc(v.parameter) + '</td>'
|
|
742
|
+
+ '<td>' + esc(v.result) + '</td>'
|
|
742
743
|
+ '</tr>';
|
|
743
744
|
}).join('');
|
|
744
745
|
}
|
|
@@ -764,7 +765,7 @@ function getDashboardHtml() {
|
|
|
764
765
|
$personaBars.innerHTML = entries.map(function(e) {
|
|
765
766
|
const pctVal = total > 0 ? (e[1] / total * 100) : 0;
|
|
766
767
|
return '<div class="persona-row">'
|
|
767
|
-
+ '<div class="persona-label">' + e[0] + '</div>'
|
|
768
|
+
+ '<div class="persona-label">' + esc(e[0]) + '</div>'
|
|
768
769
|
+ '<div class="persona-bar-track"><div class="persona-bar-fill" style="width:' + pctVal + '%"></div></div>'
|
|
769
770
|
+ '<div class="persona-pct">' + pctVal.toFixed(0) + '%</div>'
|
|
770
771
|
+ '</div>';
|
|
@@ -777,12 +778,10 @@ function getDashboardHtml() {
|
|
|
777
778
|
$registryList.innerHTML = '<div class="empty-state">No parameters registered</div>';
|
|
778
779
|
return;
|
|
779
780
|
}
|
|
780
|
-
// Show unique parameters from principles
|
|
781
|
-
const params = new Set();
|
|
782
781
|
$registryList.innerHTML = principles.slice(0, 30).map(function(p) {
|
|
783
782
|
return '<div class="registry-item">'
|
|
784
|
-
+ '<span class="registry-key">[' + p.id + ']</span>'
|
|
785
|
-
+ '<span class="registry-val">' + p.name + '</span>'
|
|
783
|
+
+ '<span class="registry-key">[' + esc(p.id) + ']</span>'
|
|
784
|
+
+ '<span class="registry-val">' + esc(p.name) + '</span>'
|
|
786
785
|
+ '</div>';
|
|
787
786
|
}).join('');
|
|
788
787
|
}
|
|
@@ -968,7 +967,13 @@ function getDashboardHtml() {
|
|
|
968
967
|
}
|
|
969
968
|
|
|
970
969
|
// src/routes.ts
|
|
970
|
+
function setSecurityHeaders(res) {
|
|
971
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
972
|
+
res.setHeader("X-Frame-Options", "DENY");
|
|
973
|
+
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
974
|
+
}
|
|
971
975
|
function setCorsHeaders(res, origin) {
|
|
976
|
+
setSecurityHeaders(res);
|
|
972
977
|
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
973
978
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
974
979
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
@@ -1025,21 +1030,19 @@ function createRouteHandler(server2) {
|
|
|
1025
1030
|
const payload = parsed;
|
|
1026
1031
|
const state = payload["state"] ?? parsed;
|
|
1027
1032
|
const events = payload["events"];
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
return;
|
|
1036
|
-
}
|
|
1033
|
+
const validation = server2.validateState ? (0, import_core.validateEconomyState)(state) : null;
|
|
1034
|
+
if (validation && !validation.valid) {
|
|
1035
|
+
json(res, 400, {
|
|
1036
|
+
error: "invalid_state",
|
|
1037
|
+
validationErrors: validation.errors
|
|
1038
|
+
}, cors);
|
|
1039
|
+
return;
|
|
1037
1040
|
}
|
|
1038
1041
|
const result = await server2.processTick(
|
|
1039
1042
|
state,
|
|
1040
1043
|
Array.isArray(events) ? events : void 0
|
|
1041
1044
|
);
|
|
1042
|
-
const warnings =
|
|
1045
|
+
const warnings = validation?.warnings ?? [];
|
|
1043
1046
|
json(res, 200, {
|
|
1044
1047
|
adjustments: result.adjustments,
|
|
1045
1048
|
alerts: result.alerts.map((a) => ({
|
|
@@ -1067,12 +1070,18 @@ function createRouteHandler(server2) {
|
|
|
1067
1070
|
return;
|
|
1068
1071
|
}
|
|
1069
1072
|
if (path === "/decisions" && method === "GET") {
|
|
1070
|
-
const
|
|
1071
|
-
const
|
|
1073
|
+
const rawLimit = parseInt(url.searchParams.get("limit") ?? "100", 10);
|
|
1074
|
+
const limit = Math.min(Math.max(isNaN(rawLimit) ? 100 : rawLimit, 1), 1e3);
|
|
1075
|
+
const sinceParam = url.searchParams.get("since");
|
|
1072
1076
|
const agentE = server2.getAgentE();
|
|
1073
1077
|
let decisions;
|
|
1074
|
-
if (
|
|
1075
|
-
|
|
1078
|
+
if (sinceParam) {
|
|
1079
|
+
const since = parseInt(sinceParam, 10);
|
|
1080
|
+
if (isNaN(since)) {
|
|
1081
|
+
json(res, 400, { error: 'Invalid "since" parameter \u2014 must be a number' }, cors);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
decisions = agentE.getDecisions({ since });
|
|
1076
1085
|
} else {
|
|
1077
1086
|
decisions = agentE.log.latest(limit);
|
|
1078
1087
|
}
|
|
@@ -1103,6 +1112,14 @@ function createRouteHandler(server2) {
|
|
|
1103
1112
|
for (const c of config["constrain"]) {
|
|
1104
1113
|
if (c && typeof c === "object" && typeof c["param"] === "string" && typeof c["min"] === "number" && typeof c["max"] === "number") {
|
|
1105
1114
|
const constraint = c;
|
|
1115
|
+
if (!isFinite(constraint.min) || !isFinite(constraint.max)) {
|
|
1116
|
+
json(res, 400, { error: "Constraint bounds must be finite numbers" }, cors);
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
if (constraint.min > constraint.max) {
|
|
1120
|
+
json(res, 400, { error: "Constraint min cannot exceed max" }, cors);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1106
1123
|
server2.constrain(constraint.param, { min: constraint.min, max: constraint.max });
|
|
1107
1124
|
}
|
|
1108
1125
|
}
|
|
@@ -1159,6 +1176,7 @@ function createRouteHandler(server2) {
|
|
|
1159
1176
|
}
|
|
1160
1177
|
if (path === "/" && method === "GET" && server2.serveDashboard) {
|
|
1161
1178
|
setCorsHeaders(res, cors);
|
|
1179
|
+
res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; connect-src 'self' ws: wss:; img-src 'self' data:");
|
|
1162
1180
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1163
1181
|
res.end(getDashboardHtml());
|
|
1164
1182
|
return;
|
|
@@ -1278,8 +1296,10 @@ function send(ws, data) {
|
|
|
1278
1296
|
ws.send(JSON.stringify(data));
|
|
1279
1297
|
}
|
|
1280
1298
|
}
|
|
1299
|
+
var MAX_WS_PAYLOAD = 1048576;
|
|
1300
|
+
var MAX_WS_CONNECTIONS = 100;
|
|
1281
1301
|
function createWebSocketHandler(httpServer, server2) {
|
|
1282
|
-
const wss = new import_ws.WebSocketServer({ server: httpServer });
|
|
1302
|
+
const wss = new import_ws.WebSocketServer({ server: httpServer, maxPayload: MAX_WS_PAYLOAD });
|
|
1283
1303
|
const aliveMap = /* @__PURE__ */ new WeakMap();
|
|
1284
1304
|
const heartbeatInterval = setInterval(() => {
|
|
1285
1305
|
for (const ws of wss.clients) {
|
|
@@ -1294,6 +1314,10 @@ function createWebSocketHandler(httpServer, server2) {
|
|
|
1294
1314
|
}
|
|
1295
1315
|
}, 3e4);
|
|
1296
1316
|
wss.on("connection", (ws) => {
|
|
1317
|
+
if (wss.clients.size > MAX_WS_CONNECTIONS) {
|
|
1318
|
+
ws.close(1013, "Server at capacity");
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1297
1321
|
console.log("[AgentE Server] Client connected");
|
|
1298
1322
|
aliveMap.set(ws, true);
|
|
1299
1323
|
ws.on("pong", () => {
|
|
@@ -1427,7 +1451,7 @@ var AgentEServer = class {
|
|
|
1427
1451
|
this.port = config.port ?? 3100;
|
|
1428
1452
|
this.host = config.host ?? "0.0.0.0";
|
|
1429
1453
|
this.validateState = config.validateState ?? true;
|
|
1430
|
-
this.corsOrigin = config.corsOrigin ?? "
|
|
1454
|
+
this.corsOrigin = config.corsOrigin ?? "http://localhost:3100";
|
|
1431
1455
|
this.serveDashboard = config.serveDashboard ?? true;
|
|
1432
1456
|
const adapter = {
|
|
1433
1457
|
getState: () => {
|