@agent-e/server 1.8.1 → 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/dist/cli.js CHANGED
@@ -32,7 +32,8 @@ var import_node_crypto = require("crypto");
32
32
  var import_core = require("@agent-e/core");
33
33
 
34
34
  // src/dashboard.ts
35
- function getDashboardHtml() {
35
+ function getDashboardHtml(nonce) {
36
+ const nonceAttr = nonce ? ` nonce="${nonce}"` : "";
36
37
  return `<!DOCTYPE html>
37
38
  <html lang="en">
38
39
  <head>
@@ -42,7 +43,7 @@ function getDashboardHtml() {
42
43
  <link rel="preconnect" href="https://fonts.googleapis.com">
43
44
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
44
45
  <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">
45
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script>
46
+ <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>
46
47
  <style>
47
48
  *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
48
49
 
@@ -863,7 +864,7 @@ function getDashboardHtml() {
863
864
 
864
865
  </main>
865
866
 
866
- <script>
867
+ <script` + nonceAttr + `>
867
868
  (function() {
868
869
  'use strict';
869
870
 
@@ -910,6 +911,9 @@ function getDashboardHtml() {
910
911
  var $dashboardRoot = document.getElementById('dashboard-root');
911
912
 
912
913
  // -- Helpers --
914
+ // SECURITY-CRITICAL: esc() prevents XSS by escaping all HTML-significant characters.
915
+ // Every user-derived or WebSocket-derived value rendered via innerHTML MUST pass through esc().
916
+ // If adding new terminal renderers or DOM builders, always use esc() on dynamic data.
913
917
  function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;').replace(/\\\\/g,'&#92;'); }
914
918
  function pad(n, w) { return String(n).padStart(w || 4, ' '); }
915
919
  function fmt(n) { return typeof n === 'number' ? n.toFixed(3) : '\\u2014'; }
@@ -1314,14 +1318,25 @@ function getDashboardHtml() {
1314
1318
  }
1315
1319
 
1316
1320
  // -- API calls --
1321
+ // Read token from URL query param \u2014 never embed the API key in the HTML body.
1322
+ var _authKey = new URLSearchParams(location.search).get('token');
1323
+
1324
+ function authHeaders() {
1325
+ var h = {};
1326
+ if (_authKey) h['Authorization'] = 'Bearer ' + _authKey;
1327
+ return h;
1328
+ }
1329
+
1317
1330
  function fetchJSON(path) {
1318
- return fetch(path).then(function(r) { return r.json(); });
1331
+ return fetch(path, { headers: authHeaders() }).then(function(r) { return r.json(); });
1319
1332
  }
1320
1333
 
1321
1334
  function postJSON(path, body) {
1335
+ var h = authHeaders();
1336
+ h['Content-Type'] = 'application/json';
1322
1337
  return fetch(path, {
1323
1338
  method: 'POST',
1324
- headers: { 'Content-Type': 'application/json' },
1339
+ headers: h,
1325
1340
  body: JSON.stringify(body),
1326
1341
  }).then(function(r) { return r.json(); });
1327
1342
  }
@@ -1383,7 +1398,9 @@ function getDashboardHtml() {
1383
1398
  // -- WebSocket --
1384
1399
  function connectWS() {
1385
1400
  var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
1386
- ws = new WebSocket(proto + '//' + location.host);
1401
+ var wsUrl = proto + '//' + location.host;
1402
+ if (_authKey) wsUrl += '?token=' + encodeURIComponent(_authKey);
1403
+ ws = new WebSocket(wsUrl);
1387
1404
 
1388
1405
  ws.onopen = function() {
1389
1406
  reconnectDelay = 1000;
@@ -1533,11 +1550,30 @@ function getDashboardHtml() {
1533
1550
  </html>`;
1534
1551
  }
1535
1552
 
1553
+ // src/validation.ts
1554
+ var VALID_EVENT_TYPES = /* @__PURE__ */ new Set([
1555
+ "trade",
1556
+ "mint",
1557
+ "burn",
1558
+ "transfer",
1559
+ "produce",
1560
+ "consume",
1561
+ "role_change",
1562
+ "enter",
1563
+ "churn"
1564
+ ]);
1565
+ function validateEvent(e) {
1566
+ if (!e || typeof e !== "object") return false;
1567
+ const ev = e;
1568
+ return typeof ev["type"] === "string" && VALID_EVENT_TYPES.has(ev["type"]) && typeof ev["timestamp"] === "number" && typeof ev["actor"] === "string";
1569
+ }
1570
+
1536
1571
  // src/routes.ts
1537
1572
  function setSecurityHeaders(res) {
1538
1573
  res.setHeader("X-Content-Type-Options", "nosniff");
1539
1574
  res.setHeader("X-Frame-Options", "DENY");
1540
1575
  res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
1576
+ res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
1541
1577
  }
1542
1578
  function setCorsHeaders(res, allowedOrigin, requestOrigin) {
1543
1579
  setSecurityHeaders(res);
@@ -1579,21 +1615,34 @@ function json(res, status, data, origin, reqOrigin) {
1579
1615
  res.end(JSON.stringify(data));
1580
1616
  }
1581
1617
  var MAX_BODY_BYTES = 1048576;
1618
+ var READ_BODY_TIMEOUT_MS = 3e4;
1619
+ var MAX_CONFIG_ARRAY = 1e3;
1582
1620
  function readBody(req) {
1583
1621
  return new Promise((resolve, reject) => {
1584
1622
  const chunks = [];
1585
1623
  let totalBytes = 0;
1624
+ const timeout = setTimeout(() => {
1625
+ req.destroy();
1626
+ reject(new Error("Request body read timeout"));
1627
+ }, READ_BODY_TIMEOUT_MS);
1586
1628
  req.on("data", (chunk) => {
1587
1629
  totalBytes += chunk.length;
1588
1630
  if (totalBytes > MAX_BODY_BYTES) {
1631
+ clearTimeout(timeout);
1589
1632
  req.destroy();
1590
1633
  reject(new Error("Request body too large"));
1591
1634
  return;
1592
1635
  }
1593
1636
  chunks.push(chunk);
1594
1637
  });
1595
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
1596
- req.on("error", reject);
1638
+ req.on("end", () => {
1639
+ clearTimeout(timeout);
1640
+ resolve(Buffer.concat(chunks).toString("utf-8"));
1641
+ });
1642
+ req.on("error", (err) => {
1643
+ clearTimeout(timeout);
1644
+ reject(err);
1645
+ });
1597
1646
  });
1598
1647
  }
1599
1648
  function createRouteHandler(server2) {
@@ -1640,9 +1689,10 @@ function createRouteHandler(server2) {
1640
1689
  });
1641
1690
  return;
1642
1691
  }
1692
+ const validEvents = Array.isArray(events) ? events.filter(validateEvent) : void 0;
1643
1693
  const result = await server2.processTick(
1644
1694
  state,
1645
- Array.isArray(events) ? events : void 0
1695
+ validEvents
1646
1696
  );
1647
1697
  const warnings = validation?.warnings ?? [];
1648
1698
  respond(200, {
@@ -1672,6 +1722,10 @@ function createRouteHandler(server2) {
1672
1722
  return;
1673
1723
  }
1674
1724
  if (path === "/decisions" && method === "GET") {
1725
+ if (!checkAuth(req, apiKey)) {
1726
+ respond(401, { error: "Unauthorized" });
1727
+ return;
1728
+ }
1675
1729
  const rawLimit = parseInt(url.searchParams.get("limit") ?? "100", 10);
1676
1730
  const limit = Math.min(Math.max(Number.isNaN(rawLimit) ? 100 : rawLimit, 1), 1e3);
1677
1731
  const sinceParam = url.searchParams.get("since");
@@ -1687,7 +1741,11 @@ function createRouteHandler(server2) {
1687
1741
  } else {
1688
1742
  decisions = agentE.log.latest(limit);
1689
1743
  }
1690
- respond(200, { decisions });
1744
+ const sanitized = decisions.map((d) => {
1745
+ const { metricsSnapshot: _, ...rest } = d;
1746
+ return rest;
1747
+ });
1748
+ respond(200, { decisions: sanitized });
1691
1749
  return;
1692
1750
  }
1693
1751
  if (path === "/config" && method === "POST") {
@@ -1705,18 +1763,18 @@ function createRouteHandler(server2) {
1705
1763
  }
1706
1764
  const config = parsed;
1707
1765
  if (Array.isArray(config["lock"])) {
1708
- for (const param of config["lock"]) {
1766
+ for (const param of config["lock"].slice(0, MAX_CONFIG_ARRAY)) {
1709
1767
  if (typeof param === "string") server2.lock(param);
1710
1768
  }
1711
1769
  }
1712
1770
  if (Array.isArray(config["unlock"])) {
1713
- for (const param of config["unlock"]) {
1771
+ for (const param of config["unlock"].slice(0, MAX_CONFIG_ARRAY)) {
1714
1772
  if (typeof param === "string") server2.unlock(param);
1715
1773
  }
1716
1774
  }
1717
1775
  if (Array.isArray(config["constrain"])) {
1718
1776
  const validated = [];
1719
- for (const c of config["constrain"]) {
1777
+ for (const c of config["constrain"].slice(0, MAX_CONFIG_ARRAY)) {
1720
1778
  if (c && typeof c === "object" && typeof c["param"] === "string" && typeof c["min"] === "number" && typeof c["max"] === "number") {
1721
1779
  const constraint = c;
1722
1780
  if (!Number.isFinite(constraint.min) || !Number.isFinite(constraint.max)) {
@@ -1789,14 +1847,28 @@ function createRouteHandler(server2) {
1789
1847
  return;
1790
1848
  }
1791
1849
  if (path === "/" && method === "GET" && server2.serveDashboard) {
1850
+ if (apiKey) {
1851
+ const dashToken = url.searchParams.get("token") ?? "";
1852
+ const hasBearer = checkAuth(req, apiKey);
1853
+ const hasQueryToken = dashToken.length === apiKey.length && (0, import_node_crypto.timingSafeEqual)(Buffer.from(dashToken), Buffer.from(apiKey));
1854
+ if (!hasBearer && !hasQueryToken) {
1855
+ respond(401, { error: "Unauthorized \u2014 use ?token=<apiKey> or Authorization header" });
1856
+ return;
1857
+ }
1858
+ }
1792
1859
  setCorsHeaders(res, cors, reqOrigin);
1793
- 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:");
1794
- res.setHeader("Cache-Control", "public, max-age=60");
1860
+ const nonce = (0, import_node_crypto.randomBytes)(16).toString("base64");
1861
+ 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:`);
1862
+ res.setHeader("Cache-Control", "no-cache, private");
1795
1863
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1796
- res.end(getDashboardHtml());
1864
+ res.end(getDashboardHtml(nonce));
1797
1865
  return;
1798
1866
  }
1799
1867
  if (path === "/metrics" && method === "GET") {
1868
+ if (!checkAuth(req, apiKey)) {
1869
+ respond(401, { error: "Unauthorized" });
1870
+ return;
1871
+ }
1800
1872
  const agentE = server2.getAgentE();
1801
1873
  const latest = agentE.store.latest();
1802
1874
  const history = agentE.store.recentHistory(100);
@@ -1804,6 +1876,10 @@ function createRouteHandler(server2) {
1804
1876
  return;
1805
1877
  }
1806
1878
  if (path === "/metrics/personas" && method === "GET") {
1879
+ if (!checkAuth(req, apiKey)) {
1880
+ respond(401, { error: "Unauthorized" });
1881
+ return;
1882
+ }
1807
1883
  const agentE = server2.getAgentE();
1808
1884
  const latest = agentE.store.latest();
1809
1885
  const dist = latest.personaDistribution || {};
@@ -1894,6 +1970,10 @@ function createRouteHandler(server2) {
1894
1970
  return;
1895
1971
  }
1896
1972
  if (path === "/pending" && method === "GET") {
1973
+ if (!checkAuth(req, apiKey)) {
1974
+ respond(401, { error: "Unauthorized" });
1975
+ return;
1976
+ }
1897
1977
  const agentE = server2.getAgentE();
1898
1978
  const pending = agentE.log.query({ result: "skipped_override" });
1899
1979
  respond(200, {
@@ -1923,6 +2003,7 @@ function send(ws, data) {
1923
2003
  var MAX_WS_PAYLOAD = 1048576;
1924
2004
  var MAX_WS_CONNECTIONS = 100;
1925
2005
  var MIN_TICK_INTERVAL_MS = 100;
2006
+ var GLOBAL_MIN_TICK_INTERVAL_MS = 50;
1926
2007
  function sanitizeJson2(obj) {
1927
2008
  if (obj === null || typeof obj !== "object") return obj;
1928
2009
  if (Array.isArray(obj)) return obj.map(sanitizeJson2);
@@ -1935,6 +2016,7 @@ function sanitizeJson2(obj) {
1935
2016
  }
1936
2017
  function createWebSocketHandler(httpServer, server2) {
1937
2018
  const wss = new import_ws.WebSocketServer({ server: httpServer, maxPayload: MAX_WS_PAYLOAD });
2019
+ let globalLastTickTime = 0;
1938
2020
  const aliveMap = /* @__PURE__ */ new WeakMap();
1939
2021
  const heartbeatInterval = setInterval(() => {
1940
2022
  for (const ws of wss.clients) {
@@ -1962,7 +2044,8 @@ function createWebSocketHandler(httpServer, server2) {
1962
2044
  }
1963
2045
  if (server2.apiKey) {
1964
2046
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
1965
- const token = url.searchParams.get("token") ?? req.headers["authorization"]?.replace("Bearer ", "");
2047
+ const authHeader = req.headers["authorization"];
2048
+ const token = (authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : void 0) ?? url.searchParams.get("token");
1966
2049
  if (!token || token.length !== server2.apiKey.length || !(0, import_node_crypto2.timingSafeEqual)(Buffer.from(token), Buffer.from(server2.apiKey))) {
1967
2050
  ws.close(1008, "Unauthorized");
1968
2051
  return;
@@ -1996,7 +2079,12 @@ function createWebSocketHandler(httpServer, server2) {
1996
2079
  send(ws, { type: "error", message: "Rate limited \u2014 min 100ms between ticks" });
1997
2080
  break;
1998
2081
  }
2082
+ if (now - globalLastTickTime < GLOBAL_MIN_TICK_INTERVAL_MS) {
2083
+ send(ws, { type: "error", message: "Rate limited \u2014 server tick capacity exceeded" });
2084
+ break;
2085
+ }
1999
2086
  lastTickTime = now;
2087
+ globalLastTickTime = now;
2000
2088
  const state = msg["state"];
2001
2089
  const events = msg["events"];
2002
2090
  if (server2.validateState) {
@@ -2010,9 +2098,10 @@ function createWebSocketHandler(httpServer, server2) {
2010
2098
  }
2011
2099
  }
2012
2100
  try {
2101
+ const validEvents = Array.isArray(events) ? events.filter(validateEvent) : void 0;
2013
2102
  const result = await server2.processTick(
2014
2103
  state,
2015
- Array.isArray(events) ? events : void 0
2104
+ validEvents
2016
2105
  );
2017
2106
  send(ws, {
2018
2107
  type: "tick_result",
@@ -2032,13 +2121,17 @@ function createWebSocketHandler(httpServer, server2) {
2032
2121
  break;
2033
2122
  }
2034
2123
  case "event": {
2035
- const event = msg["event"];
2036
- if (event) {
2037
- server2.getAgentE().ingest(event);
2038
- send(ws, { type: "event_ack" });
2039
- } else {
2124
+ const rawEvent = msg["event"];
2125
+ if (!rawEvent) {
2040
2126
  send(ws, { type: "error", message: 'Missing "event" field' });
2127
+ break;
2041
2128
  }
2129
+ if (!validateEvent(rawEvent)) {
2130
+ send(ws, { type: "error", message: "Invalid event \u2014 requires type (valid event type), timestamp (number), and actor (string)" });
2131
+ break;
2132
+ }
2133
+ server2.getAgentE().ingest(rawEvent);
2134
+ send(ws, { type: "event_ack" });
2042
2135
  break;
2043
2136
  }
2044
2137
  case "health": {
@@ -2103,6 +2196,8 @@ var AgentEServer = class {
2103
2196
  this.lastState = null;
2104
2197
  this.adjustmentQueue = [];
2105
2198
  this.alerts = [];
2199
+ /** Serialization lock for processTick — prevents concurrent ticks from corrupting shared state. */
2200
+ this.tickLock = Promise.resolve();
2106
2201
  this.startedAt = Date.now();
2107
2202
  this.wsHandle = null;
2108
2203
  this.port = config.port ?? 3100;
@@ -2239,36 +2334,46 @@ var AgentEServer = class {
2239
2334
  * 6. Return response
2240
2335
  */
2241
2336
  async processTick(state, events) {
2242
- this.adjustmentQueue = [];
2243
- this.alerts = [];
2244
- this.lastState = state;
2245
- if (events) {
2246
- for (const event of events) {
2247
- this.agentE.ingest(event);
2337
+ const prev = this.tickLock;
2338
+ let unlock;
2339
+ this.tickLock = new Promise((resolve) => {
2340
+ unlock = resolve;
2341
+ });
2342
+ await prev;
2343
+ try {
2344
+ this.adjustmentQueue = [];
2345
+ this.alerts = [];
2346
+ this.lastState = state;
2347
+ if (events) {
2348
+ for (const event of events) {
2349
+ this.agentE.ingest(event);
2350
+ }
2248
2351
  }
2249
- }
2250
- await this.agentE.tick(state);
2251
- const rawAdj = [...this.adjustmentQueue];
2252
- this.adjustmentQueue = [];
2253
- const decisions = this.agentE.getDecisions({ since: state.tick, until: state.tick });
2254
- const adjustments = rawAdj.map((adj) => {
2255
- const decision = decisions.find(
2256
- (d) => d.plan.parameter === adj.key && d.result === "applied"
2257
- );
2352
+ await this.agentE.tick(state);
2353
+ const rawAdj = [...this.adjustmentQueue];
2354
+ this.adjustmentQueue = [];
2355
+ const decisions = this.agentE.getDecisions({ since: state.tick, until: state.tick });
2356
+ const adjustments = rawAdj.map((adj) => {
2357
+ const decision = decisions.find(
2358
+ (d) => d.plan.parameter === adj.key && d.result === "applied"
2359
+ );
2360
+ return {
2361
+ parameter: adj.key,
2362
+ value: adj.value,
2363
+ ...adj.scope ? { scope: adj.scope } : {},
2364
+ reasoning: decision?.diagnosis.violation.suggestedAction.reasoning ?? ""
2365
+ };
2366
+ });
2258
2367
  return {
2259
- parameter: adj.key,
2260
- value: adj.value,
2261
- ...adj.scope ? { scope: adj.scope } : {},
2262
- reasoning: decision?.diagnosis.violation.suggestedAction.reasoning ?? ""
2368
+ adjustments,
2369
+ alerts: [...this.alerts],
2370
+ health: this.agentE.getHealth(),
2371
+ tick: state.tick,
2372
+ decisions
2263
2373
  };
2264
- });
2265
- return {
2266
- adjustments,
2267
- alerts: [...this.alerts],
2268
- health: this.agentE.getHealth(),
2269
- tick: state.tick,
2270
- decisions
2271
- };
2374
+ } finally {
2375
+ unlock();
2376
+ }
2272
2377
  }
2273
2378
  /**
2274
2379
  * Run Observer + Diagnoser on the given state without side effects (no execution).