@agent-e/server 1.8.0 → 1.8.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/README.md CHANGED
@@ -232,6 +232,60 @@ Connect to the same port via WebSocket upgrade.
232
232
 
233
233
  Heartbeat: Server pings every 30 seconds.
234
234
 
235
+ ## Authentication
236
+
237
+ Protect mutation routes and the dashboard with an API key:
238
+
239
+ ```ts
240
+ const server = new AgentEServer({
241
+ apiKey: process.env.AGENTE_API_KEY,
242
+ });
243
+ ```
244
+
245
+ When `apiKey` is set:
246
+
247
+ - **POST routes** (`/tick`, `/config`, `/approve`, `/reject`, `/diagnose`) require `Authorization: Bearer <key>`.
248
+ - **Sensitive GET routes** (`/decisions`, `/metrics`, `/metrics/personas`, `/pending`) also require the header.
249
+ - **Dashboard** (`GET /`) accepts either the `Authorization` header or a `?token=<key>` query parameter.
250
+ - **WebSocket** accepts the key via `Authorization` header or `?token=<key>` on the upgrade request.
251
+ - **Open routes** (`/health`, `/principles`) remain unauthenticated for health-check probes.
252
+
253
+ All key comparisons use `crypto.timingSafeEqual()` to prevent timing side-channel attacks.
254
+
255
+ ## Security
256
+
257
+ The server includes multiple layers of defense-in-depth:
258
+
259
+ ### Input Validation
260
+
261
+ - **State validation** — all incoming economy state is validated before processing. Invalid state returns detailed errors with field paths.
262
+ - **Event validation** — events are checked for required fields (`type`, `actor`, `timestamp`) and a valid `type` value before ingestion. Malformed events are silently dropped (HTTP) or return an error (WebSocket).
263
+ - **Prototype pollution protection** — `__proto__`, `constructor`, and `prototype` keys are recursively stripped from all parsed JSON bodies.
264
+ - **Body size limits** — HTTP request bodies are capped at 1 MB with a 30-second read timeout to mitigate slow-loris attacks.
265
+ - **Array caps** — configuration arrays (lock/unlock/constrain) are capped at 1,000 entries.
266
+
267
+ ### Rate Limiting
268
+
269
+ - **Per-connection** — each WebSocket connection is limited to one tick per 100 ms.
270
+ - **Global** — a server-wide rate limiter caps ticks at 20/sec across all WebSocket connections to prevent CPU saturation.
271
+ - **Connection limit** — maximum 50 concurrent WebSocket connections; excess connections are closed with code 1013.
272
+
273
+ ### Transport Security
274
+
275
+ - **CORS** — configurable origin restriction via `corsOrigin` (default: `http://localhost:3100`). WebSocket connections from disallowed origins are closed with code 1008.
276
+ - **HSTS** — `Strict-Transport-Security: max-age=31536000; includeSubDomains` header on all responses.
277
+ - **Nonce-based CSP** — the dashboard uses a per-request cryptographic nonce for `script-src`, eliminating `'unsafe-inline'` scripts.
278
+ - **SRI** — the CDN-loaded Chart.js library includes a `integrity` hash and `crossorigin="anonymous"` attribute.
279
+ - **Security headers** — `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, and `Cache-Control: no-cache, private` on all responses.
280
+
281
+ ### Concurrency
282
+
283
+ - **Tick serialization** — `processTick()` uses a Promise-based mutex so concurrent HTTP + WebSocket ticks cannot corrupt shared adjustment queues.
284
+
285
+ ### Data Exposure
286
+
287
+ - **metricsSnapshot stripping** — the `/decisions` endpoint strips full metrics snapshots from decision records to avoid leaking large internal state objects.
288
+
235
289
  ## State Validation
236
290
 
237
291
  All incoming state is validated before processing. Invalid state returns detailed errors with paths:
@@ -0,0 +1,7 @@
1
+ import {
2
+ AgentEServer
3
+ } from "./chunk-3HGNFK4A.mjs";
4
+ export {
5
+ AgentEServer
6
+ };
7
+ //# sourceMappingURL=AgentEServer-A7K6426K.mjs.map
@@ -9,11 +9,12 @@ import {
9
9
  } from "@agent-e/core";
10
10
 
11
11
  // src/routes.ts
12
- import { timingSafeEqual } from "crypto";
12
+ import { timingSafeEqual, randomBytes } from "crypto";
13
13
  import { validateEconomyState } from "@agent-e/core";
14
14
 
15
15
  // src/dashboard.ts
16
- function getDashboardHtml() {
16
+ function getDashboardHtml(nonce) {
17
+ const nonceAttr = nonce ? ` nonce="${nonce}"` : "";
17
18
  return `<!DOCTYPE html>
18
19
  <html lang="en">
19
20
  <head>
@@ -23,7 +24,7 @@ function getDashboardHtml() {
23
24
  <link rel="preconnect" href="https://fonts.googleapis.com">
24
25
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
25
26
  <link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400;14..32,500;14..32,600;14..32,700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
26
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script>
27
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js" integrity="sha384-jb8JQMbMoBUzgWatfe6COACi2ljcDdZQ2OxczGA3bGNeWe+6DChMTBJemed7ZnvJ" crossorigin="anonymous"` + nonceAttr + `></script>
27
28
  <style>
28
29
  *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
29
30
 
@@ -321,6 +322,15 @@ function getDashboardHtml() {
321
322
  .t-pending-icon { color: var(--warning); }
322
323
  .t-pending-val { color: var(--warning); font-variant-numeric: tabular-nums; }
323
324
 
325
+ /* -- LLM line colors (V1.8.1) -- */
326
+ .t-narration-icon { color: #a78bfa; }
327
+ .t-narration { color: #c4b5fd; }
328
+ .t-explanation-icon { color: #60a5fa; }
329
+ .t-explanation { color: #93c5fd; }
330
+ .t-anomaly-icon { color: #f59e0b; }
331
+ .t-anomaly-label { color: #fbbf24; }
332
+ .t-anomaly { color: #fcd34d; }
333
+
324
334
  /* -- Advisor Inline Buttons -- */
325
335
  .advisor-btn {
326
336
  display: none;
@@ -699,7 +709,7 @@ function getDashboardHtml() {
699
709
  <header class="header" id="header">
700
710
  <div class="header-left">
701
711
  <span class="header-logo">AgentE</span>
702
- <span class="header-version">v1.8.0</span>
712
+ <span class="header-version">v1.8.1</span>
703
713
  </div>
704
714
  <div class="header-right">
705
715
  <div class="kpi-pill">
@@ -835,7 +845,7 @@ function getDashboardHtml() {
835
845
 
836
846
  </main>
837
847
 
838
- <script>
848
+ <script` + nonceAttr + `>
839
849
  (function() {
840
850
  'use strict';
841
851
 
@@ -882,6 +892,9 @@ function getDashboardHtml() {
882
892
  var $dashboardRoot = document.getElementById('dashboard-root');
883
893
 
884
894
  // -- Helpers --
895
+ // SECURITY-CRITICAL: esc() prevents XSS by escaping all HTML-significant characters.
896
+ // Every user-derived or WebSocket-derived value rendered via innerHTML MUST pass through esc().
897
+ // If adding new terminal renderers or DOM builders, always use esc() on dynamic data.
885
898
  function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;').replace(/\\\\/g,'&#92;'); }
886
899
  function pad(n, w) { return String(n).padStart(w || 4, ' '); }
887
900
  function fmt(n) { return typeof n === 'number' ? n.toFixed(3) : '\\u2014'; }
@@ -1037,6 +1050,31 @@ function getDashboardHtml() {
1037
1050
  + advisorBtns;
1038
1051
  }
1039
1052
 
1053
+ // -- LLM line renderers (V1.8.1) --
1054
+ function narrationToTerminal(msg) {
1055
+ return '<span class="t-tick">[Tick ' + pad(msg.tick) + ']</span> '
1056
+ + '<span class="t-narration-icon">\\u{1F9E0} </span>'
1057
+ + '<span class="t-narration">"' + esc(msg.text) + '"</span>';
1058
+ }
1059
+
1060
+ function explanationToTerminal(msg) {
1061
+ return '<span class="t-tick">[Tick ' + pad(msg.tick) + ']</span> '
1062
+ + '<span class="t-explanation-icon">\\u{1F4A1} </span>'
1063
+ + '<span class="t-explanation">"' + esc(msg.text) + '"</span>';
1064
+ }
1065
+
1066
+ function anomalyToTerminal(msg) {
1067
+ var metricsStr = '';
1068
+ if (msg.metrics && msg.metrics.length > 0) {
1069
+ var m = msg.metrics[0];
1070
+ metricsStr = m.name + ' ' + m.deviation.toFixed(1) + '\\u03C3 \u2014 ';
1071
+ }
1072
+ return '<span class="t-tick">[Tick ' + pad(msg.tick) + ']</span> '
1073
+ + '<span class="t-anomaly-icon">\\u{1F50D} </span>'
1074
+ + '<span class="t-anomaly-label">Anomaly detected: </span>'
1075
+ + '<span class="t-anomaly">' + esc(metricsStr) + '"' + esc(msg.text) + '"</span>';
1076
+ }
1077
+
1040
1078
  // -- Alerts --
1041
1079
  function renderAlerts(alerts) {
1042
1080
  if (!alerts || alerts.length === 0) {
@@ -1261,14 +1299,25 @@ function getDashboardHtml() {
1261
1299
  }
1262
1300
 
1263
1301
  // -- API calls --
1302
+ // Read token from URL query param \u2014 never embed the API key in the HTML body.
1303
+ var _authKey = new URLSearchParams(location.search).get('token');
1304
+
1305
+ function authHeaders() {
1306
+ var h = {};
1307
+ if (_authKey) h['Authorization'] = 'Bearer ' + _authKey;
1308
+ return h;
1309
+ }
1310
+
1264
1311
  function fetchJSON(path) {
1265
- return fetch(path).then(function(r) { return r.json(); });
1312
+ return fetch(path, { headers: authHeaders() }).then(function(r) { return r.json(); });
1266
1313
  }
1267
1314
 
1268
1315
  function postJSON(path, body) {
1316
+ var h = authHeaders();
1317
+ h['Content-Type'] = 'application/json';
1269
1318
  return fetch(path, {
1270
1319
  method: 'POST',
1271
- headers: { 'Content-Type': 'application/json' },
1320
+ headers: h,
1272
1321
  body: JSON.stringify(body),
1273
1322
  }).then(function(r) { return r.json(); });
1274
1323
  }
@@ -1330,7 +1379,9 @@ function getDashboardHtml() {
1330
1379
  // -- WebSocket --
1331
1380
  function connectWS() {
1332
1381
  var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
1333
- ws = new WebSocket(proto + '//' + location.host);
1382
+ var wsUrl = proto + '//' + location.host;
1383
+ if (_authKey) wsUrl += '?token=' + encodeURIComponent(_authKey);
1384
+ ws = new WebSocket(wsUrl);
1334
1385
 
1335
1386
  ws.onopen = function() {
1336
1387
  reconnectDelay = 1000;
@@ -1376,6 +1427,19 @@ function getDashboardHtml() {
1376
1427
  $hPending.textContent = pendingDecisions.length;
1377
1428
  }
1378
1429
  break;
1430
+
1431
+ // V1.8.1: LLM feed events
1432
+ case 'narration':
1433
+ addTerminalLine(narrationToTerminal(msg));
1434
+ break;
1435
+
1436
+ case 'explanation':
1437
+ addTerminalLine(explanationToTerminal(msg));
1438
+ break;
1439
+
1440
+ case 'anomaly':
1441
+ addTerminalLine(anomalyToTerminal(msg));
1442
+ break;
1379
1443
  }
1380
1444
  };
1381
1445
  }
@@ -1467,11 +1531,30 @@ function getDashboardHtml() {
1467
1531
  </html>`;
1468
1532
  }
1469
1533
 
1534
+ // src/validation.ts
1535
+ var VALID_EVENT_TYPES = /* @__PURE__ */ new Set([
1536
+ "trade",
1537
+ "mint",
1538
+ "burn",
1539
+ "transfer",
1540
+ "produce",
1541
+ "consume",
1542
+ "role_change",
1543
+ "enter",
1544
+ "churn"
1545
+ ]);
1546
+ function validateEvent(e) {
1547
+ if (!e || typeof e !== "object") return false;
1548
+ const ev = e;
1549
+ return typeof ev["type"] === "string" && VALID_EVENT_TYPES.has(ev["type"]) && typeof ev["timestamp"] === "number" && typeof ev["actor"] === "string";
1550
+ }
1551
+
1470
1552
  // src/routes.ts
1471
1553
  function setSecurityHeaders(res) {
1472
1554
  res.setHeader("X-Content-Type-Options", "nosniff");
1473
1555
  res.setHeader("X-Frame-Options", "DENY");
1474
1556
  res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
1557
+ res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
1475
1558
  }
1476
1559
  function setCorsHeaders(res, allowedOrigin, requestOrigin) {
1477
1560
  setSecurityHeaders(res);
@@ -1513,21 +1596,34 @@ function json(res, status, data, origin, reqOrigin) {
1513
1596
  res.end(JSON.stringify(data));
1514
1597
  }
1515
1598
  var MAX_BODY_BYTES = 1048576;
1599
+ var READ_BODY_TIMEOUT_MS = 3e4;
1600
+ var MAX_CONFIG_ARRAY = 1e3;
1516
1601
  function readBody(req) {
1517
1602
  return new Promise((resolve, reject) => {
1518
1603
  const chunks = [];
1519
1604
  let totalBytes = 0;
1605
+ const timeout = setTimeout(() => {
1606
+ req.destroy();
1607
+ reject(new Error("Request body read timeout"));
1608
+ }, READ_BODY_TIMEOUT_MS);
1520
1609
  req.on("data", (chunk) => {
1521
1610
  totalBytes += chunk.length;
1522
1611
  if (totalBytes > MAX_BODY_BYTES) {
1612
+ clearTimeout(timeout);
1523
1613
  req.destroy();
1524
1614
  reject(new Error("Request body too large"));
1525
1615
  return;
1526
1616
  }
1527
1617
  chunks.push(chunk);
1528
1618
  });
1529
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
1530
- req.on("error", reject);
1619
+ req.on("end", () => {
1620
+ clearTimeout(timeout);
1621
+ resolve(Buffer.concat(chunks).toString("utf-8"));
1622
+ });
1623
+ req.on("error", (err) => {
1624
+ clearTimeout(timeout);
1625
+ reject(err);
1626
+ });
1531
1627
  });
1532
1628
  }
1533
1629
  function createRouteHandler(server) {
@@ -1574,9 +1670,10 @@ function createRouteHandler(server) {
1574
1670
  });
1575
1671
  return;
1576
1672
  }
1673
+ const validEvents = Array.isArray(events) ? events.filter(validateEvent) : void 0;
1577
1674
  const result = await server.processTick(
1578
1675
  state,
1579
- Array.isArray(events) ? events : void 0
1676
+ validEvents
1580
1677
  );
1581
1678
  const warnings = validation?.warnings ?? [];
1582
1679
  respond(200, {
@@ -1606,6 +1703,10 @@ function createRouteHandler(server) {
1606
1703
  return;
1607
1704
  }
1608
1705
  if (path === "/decisions" && method === "GET") {
1706
+ if (!checkAuth(req, apiKey)) {
1707
+ respond(401, { error: "Unauthorized" });
1708
+ return;
1709
+ }
1609
1710
  const rawLimit = parseInt(url.searchParams.get("limit") ?? "100", 10);
1610
1711
  const limit = Math.min(Math.max(Number.isNaN(rawLimit) ? 100 : rawLimit, 1), 1e3);
1611
1712
  const sinceParam = url.searchParams.get("since");
@@ -1621,7 +1722,11 @@ function createRouteHandler(server) {
1621
1722
  } else {
1622
1723
  decisions = agentE.log.latest(limit);
1623
1724
  }
1624
- respond(200, { decisions });
1725
+ const sanitized = decisions.map((d) => {
1726
+ const { metricsSnapshot: _, ...rest } = d;
1727
+ return rest;
1728
+ });
1729
+ respond(200, { decisions: sanitized });
1625
1730
  return;
1626
1731
  }
1627
1732
  if (path === "/config" && method === "POST") {
@@ -1639,18 +1744,18 @@ function createRouteHandler(server) {
1639
1744
  }
1640
1745
  const config = parsed;
1641
1746
  if (Array.isArray(config["lock"])) {
1642
- for (const param of config["lock"]) {
1747
+ for (const param of config["lock"].slice(0, MAX_CONFIG_ARRAY)) {
1643
1748
  if (typeof param === "string") server.lock(param);
1644
1749
  }
1645
1750
  }
1646
1751
  if (Array.isArray(config["unlock"])) {
1647
- for (const param of config["unlock"]) {
1752
+ for (const param of config["unlock"].slice(0, MAX_CONFIG_ARRAY)) {
1648
1753
  if (typeof param === "string") server.unlock(param);
1649
1754
  }
1650
1755
  }
1651
1756
  if (Array.isArray(config["constrain"])) {
1652
1757
  const validated = [];
1653
- for (const c of config["constrain"]) {
1758
+ for (const c of config["constrain"].slice(0, MAX_CONFIG_ARRAY)) {
1654
1759
  if (c && typeof c === "object" && typeof c["param"] === "string" && typeof c["min"] === "number" && typeof c["max"] === "number") {
1655
1760
  const constraint = c;
1656
1761
  if (!Number.isFinite(constraint.min) || !Number.isFinite(constraint.max)) {
@@ -1723,14 +1828,28 @@ function createRouteHandler(server) {
1723
1828
  return;
1724
1829
  }
1725
1830
  if (path === "/" && method === "GET" && server.serveDashboard) {
1831
+ if (apiKey) {
1832
+ const dashToken = url.searchParams.get("token") ?? "";
1833
+ const hasBearer = checkAuth(req, apiKey);
1834
+ const hasQueryToken = dashToken.length === apiKey.length && timingSafeEqual(Buffer.from(dashToken), Buffer.from(apiKey));
1835
+ if (!hasBearer && !hasQueryToken) {
1836
+ respond(401, { error: "Unauthorized \u2014 use ?token=<apiKey> or Authorization header" });
1837
+ return;
1838
+ }
1839
+ }
1726
1840
  setCorsHeaders(res, cors, reqOrigin);
1727
- 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:");
1728
- res.setHeader("Cache-Control", "public, max-age=60");
1841
+ const nonce = randomBytes(16).toString("base64");
1842
+ res.setHeader("Content-Security-Policy", `default-src 'self'; script-src 'nonce-${nonce}' 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:`);
1843
+ res.setHeader("Cache-Control", "no-cache, private");
1729
1844
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1730
- res.end(getDashboardHtml());
1845
+ res.end(getDashboardHtml(nonce));
1731
1846
  return;
1732
1847
  }
1733
1848
  if (path === "/metrics" && method === "GET") {
1849
+ if (!checkAuth(req, apiKey)) {
1850
+ respond(401, { error: "Unauthorized" });
1851
+ return;
1852
+ }
1734
1853
  const agentE = server.getAgentE();
1735
1854
  const latest = agentE.store.latest();
1736
1855
  const history = agentE.store.recentHistory(100);
@@ -1738,6 +1857,10 @@ function createRouteHandler(server) {
1738
1857
  return;
1739
1858
  }
1740
1859
  if (path === "/metrics/personas" && method === "GET") {
1860
+ if (!checkAuth(req, apiKey)) {
1861
+ respond(401, { error: "Unauthorized" });
1862
+ return;
1863
+ }
1741
1864
  const agentE = server.getAgentE();
1742
1865
  const latest = agentE.store.latest();
1743
1866
  const dist = latest.personaDistribution || {};
@@ -1828,6 +1951,10 @@ function createRouteHandler(server) {
1828
1951
  return;
1829
1952
  }
1830
1953
  if (path === "/pending" && method === "GET") {
1954
+ if (!checkAuth(req, apiKey)) {
1955
+ respond(401, { error: "Unauthorized" });
1956
+ return;
1957
+ }
1831
1958
  const agentE = server.getAgentE();
1832
1959
  const pending = agentE.log.query({ result: "skipped_override" });
1833
1960
  respond(200, {
@@ -1857,6 +1984,7 @@ function send(ws, data) {
1857
1984
  var MAX_WS_PAYLOAD = 1048576;
1858
1985
  var MAX_WS_CONNECTIONS = 100;
1859
1986
  var MIN_TICK_INTERVAL_MS = 100;
1987
+ var GLOBAL_MIN_TICK_INTERVAL_MS = 50;
1860
1988
  function sanitizeJson2(obj) {
1861
1989
  if (obj === null || typeof obj !== "object") return obj;
1862
1990
  if (Array.isArray(obj)) return obj.map(sanitizeJson2);
@@ -1869,6 +1997,7 @@ function sanitizeJson2(obj) {
1869
1997
  }
1870
1998
  function createWebSocketHandler(httpServer, server) {
1871
1999
  const wss = new WebSocketServer({ server: httpServer, maxPayload: MAX_WS_PAYLOAD });
2000
+ let globalLastTickTime = 0;
1872
2001
  const aliveMap = /* @__PURE__ */ new WeakMap();
1873
2002
  const heartbeatInterval = setInterval(() => {
1874
2003
  for (const ws of wss.clients) {
@@ -1896,7 +2025,8 @@ function createWebSocketHandler(httpServer, server) {
1896
2025
  }
1897
2026
  if (server.apiKey) {
1898
2027
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
1899
- const token = url.searchParams.get("token") ?? req.headers["authorization"]?.replace("Bearer ", "");
2028
+ const authHeader = req.headers["authorization"];
2029
+ const token = (authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : void 0) ?? url.searchParams.get("token");
1900
2030
  if (!token || token.length !== server.apiKey.length || !timingSafeEqual2(Buffer.from(token), Buffer.from(server.apiKey))) {
1901
2031
  ws.close(1008, "Unauthorized");
1902
2032
  return;
@@ -1930,7 +2060,12 @@ function createWebSocketHandler(httpServer, server) {
1930
2060
  send(ws, { type: "error", message: "Rate limited \u2014 min 100ms between ticks" });
1931
2061
  break;
1932
2062
  }
2063
+ if (now - globalLastTickTime < GLOBAL_MIN_TICK_INTERVAL_MS) {
2064
+ send(ws, { type: "error", message: "Rate limited \u2014 server tick capacity exceeded" });
2065
+ break;
2066
+ }
1933
2067
  lastTickTime = now;
2068
+ globalLastTickTime = now;
1934
2069
  const state = msg["state"];
1935
2070
  const events = msg["events"];
1936
2071
  if (server.validateState) {
@@ -1944,9 +2079,10 @@ function createWebSocketHandler(httpServer, server) {
1944
2079
  }
1945
2080
  }
1946
2081
  try {
2082
+ const validEvents = Array.isArray(events) ? events.filter(validateEvent) : void 0;
1947
2083
  const result = await server.processTick(
1948
2084
  state,
1949
- Array.isArray(events) ? events : void 0
2085
+ validEvents
1950
2086
  );
1951
2087
  send(ws, {
1952
2088
  type: "tick_result",
@@ -1966,13 +2102,17 @@ function createWebSocketHandler(httpServer, server) {
1966
2102
  break;
1967
2103
  }
1968
2104
  case "event": {
1969
- const event = msg["event"];
1970
- if (event) {
1971
- server.getAgentE().ingest(event);
1972
- send(ws, { type: "event_ack" });
1973
- } else {
2105
+ const rawEvent = msg["event"];
2106
+ if (!rawEvent) {
1974
2107
  send(ws, { type: "error", message: 'Missing "event" field' });
2108
+ break;
1975
2109
  }
2110
+ if (!validateEvent(rawEvent)) {
2111
+ send(ws, { type: "error", message: "Invalid event \u2014 requires type (valid event type), timestamp (number), and actor (string)" });
2112
+ break;
2113
+ }
2114
+ server.getAgentE().ingest(rawEvent);
2115
+ send(ws, { type: "event_ack" });
1976
2116
  break;
1977
2117
  }
1978
2118
  case "health": {
@@ -2037,6 +2177,8 @@ var AgentEServer = class {
2037
2177
  this.lastState = null;
2038
2178
  this.adjustmentQueue = [];
2039
2179
  this.alerts = [];
2180
+ /** Serialization lock for processTick — prevents concurrent ticks from corrupting shared state. */
2181
+ this.tickLock = Promise.resolve();
2040
2182
  this.startedAt = Date.now();
2041
2183
  this.wsHandle = null;
2042
2184
  this.port = config.port ?? 3100;
@@ -2088,6 +2230,44 @@ var AgentEServer = class {
2088
2230
  this.agentE.on("alert", (diagnosis) => {
2089
2231
  this.alerts.push(diagnosis);
2090
2232
  });
2233
+ this.agentE.on("narration", (n) => {
2234
+ const narration = n;
2235
+ const tick = this.lastState?.tick ?? 0;
2236
+ this.broadcast({
2237
+ type: "narration",
2238
+ tick,
2239
+ text: narration.narration,
2240
+ principle: narration.diagnosis.principle.name,
2241
+ severity: narration.diagnosis.violation.severity,
2242
+ confidence: narration.confidence
2243
+ });
2244
+ });
2245
+ this.agentE.on("explanation", (e) => {
2246
+ const explanation = e;
2247
+ this.broadcast({
2248
+ type: "explanation",
2249
+ tick: this.lastState?.tick ?? 0,
2250
+ text: explanation.explanation,
2251
+ parameter: explanation.plan.parameter,
2252
+ direction: explanation.plan.targetValue > explanation.plan.currentValue ? "increase" : "decrease",
2253
+ expectedOutcome: explanation.expectedOutcome,
2254
+ risks: explanation.risks
2255
+ });
2256
+ });
2257
+ this.agentE.on("anomaly", (a) => {
2258
+ const anomaly = a;
2259
+ this.broadcast({
2260
+ type: "anomaly",
2261
+ tick: anomaly.tick,
2262
+ text: anomaly.interpretation,
2263
+ metrics: anomaly.anomalies.map((m) => ({
2264
+ name: m.metric,
2265
+ deviation: m.deviation,
2266
+ currentValue: m.currentValue
2267
+ })),
2268
+ severity: anomaly.severity
2269
+ });
2270
+ });
2091
2271
  this.agentE.connect(adapter).start();
2092
2272
  const routeHandler = createRouteHandler(this);
2093
2273
  this.server = http.createServer(routeHandler);
@@ -2135,36 +2315,46 @@ var AgentEServer = class {
2135
2315
  * 6. Return response
2136
2316
  */
2137
2317
  async processTick(state, events) {
2138
- this.adjustmentQueue = [];
2139
- this.alerts = [];
2140
- this.lastState = state;
2141
- if (events) {
2142
- for (const event of events) {
2143
- this.agentE.ingest(event);
2318
+ const prev = this.tickLock;
2319
+ let unlock;
2320
+ this.tickLock = new Promise((resolve) => {
2321
+ unlock = resolve;
2322
+ });
2323
+ await prev;
2324
+ try {
2325
+ this.adjustmentQueue = [];
2326
+ this.alerts = [];
2327
+ this.lastState = state;
2328
+ if (events) {
2329
+ for (const event of events) {
2330
+ this.agentE.ingest(event);
2331
+ }
2144
2332
  }
2145
- }
2146
- await this.agentE.tick(state);
2147
- const rawAdj = [...this.adjustmentQueue];
2148
- this.adjustmentQueue = [];
2149
- const decisions = this.agentE.getDecisions({ since: state.tick, until: state.tick });
2150
- const adjustments = rawAdj.map((adj) => {
2151
- const decision = decisions.find(
2152
- (d) => d.plan.parameter === adj.key && d.result === "applied"
2153
- );
2333
+ await this.agentE.tick(state);
2334
+ const rawAdj = [...this.adjustmentQueue];
2335
+ this.adjustmentQueue = [];
2336
+ const decisions = this.agentE.getDecisions({ since: state.tick, until: state.tick });
2337
+ const adjustments = rawAdj.map((adj) => {
2338
+ const decision = decisions.find(
2339
+ (d) => d.plan.parameter === adj.key && d.result === "applied"
2340
+ );
2341
+ return {
2342
+ parameter: adj.key,
2343
+ value: adj.value,
2344
+ ...adj.scope ? { scope: adj.scope } : {},
2345
+ reasoning: decision?.diagnosis.violation.suggestedAction.reasoning ?? ""
2346
+ };
2347
+ });
2154
2348
  return {
2155
- parameter: adj.key,
2156
- value: adj.value,
2157
- ...adj.scope ? { scope: adj.scope } : {},
2158
- reasoning: decision?.diagnosis.violation.suggestedAction.reasoning ?? ""
2349
+ adjustments,
2350
+ alerts: [...this.alerts],
2351
+ health: this.agentE.getHealth(),
2352
+ tick: state.tick,
2353
+ decisions
2159
2354
  };
2160
- });
2161
- return {
2162
- adjustments,
2163
- alerts: [...this.alerts],
2164
- health: this.agentE.getHealth(),
2165
- tick: state.tick,
2166
- decisions
2167
- };
2355
+ } finally {
2356
+ unlock();
2357
+ }
2168
2358
  }
2169
2359
  /**
2170
2360
  * Run Observer + Diagnoser on the given state without side effects (no execution).
@@ -2206,4 +2396,4 @@ var AgentEServer = class {
2206
2396
  export {
2207
2397
  AgentEServer
2208
2398
  };
2209
- //# sourceMappingURL=chunk-XFI27OM4.mjs.map
2399
+ //# sourceMappingURL=chunk-3HGNFK4A.mjs.map