@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.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  AgentEServer
4
- } from "./chunk-ALGME445.mjs";
4
+ } from "./chunk-3HGNFK4A.mjs";
5
5
 
6
6
  // src/cli.ts
7
7
  var port = parseInt(process.env["AGENTE_PORT"] ?? "3100", 10);
package/dist/index.d.mts CHANGED
@@ -23,6 +23,8 @@ declare class AgentEServer {
23
23
  private lastState;
24
24
  private adjustmentQueue;
25
25
  private alerts;
26
+ /** Serialization lock for processTick — prevents concurrent ticks from corrupting shared state. */
27
+ private tickLock;
26
28
  readonly port: number;
27
29
  private readonly host;
28
30
  private readonly thresholds;
package/dist/index.d.ts CHANGED
@@ -23,6 +23,8 @@ declare class AgentEServer {
23
23
  private lastState;
24
24
  private adjustmentQueue;
25
25
  private alerts;
26
+ /** Serialization lock for processTick — prevents concurrent ticks from corrupting shared state. */
27
+ private tickLock;
26
28
  readonly port: number;
27
29
  private readonly host;
28
30
  private readonly thresholds;
package/dist/index.js CHANGED
@@ -31,7 +31,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
33
  // src/dashboard.ts
34
- function getDashboardHtml() {
34
+ function getDashboardHtml(nonce) {
35
+ const nonceAttr = nonce ? ` nonce="${nonce}"` : "";
35
36
  return `<!DOCTYPE html>
36
37
  <html lang="en">
37
38
  <head>
@@ -41,7 +42,7 @@ function getDashboardHtml() {
41
42
  <link rel="preconnect" href="https://fonts.googleapis.com">
42
43
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
43
44
  <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">
44
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script>
45
+ <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>
45
46
  <style>
46
47
  *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
47
48
 
@@ -862,7 +863,7 @@ function getDashboardHtml() {
862
863
 
863
864
  </main>
864
865
 
865
- <script>
866
+ <script` + nonceAttr + `>
866
867
  (function() {
867
868
  'use strict';
868
869
 
@@ -909,6 +910,9 @@ function getDashboardHtml() {
909
910
  var $dashboardRoot = document.getElementById('dashboard-root');
910
911
 
911
912
  // -- Helpers --
913
+ // SECURITY-CRITICAL: esc() prevents XSS by escaping all HTML-significant characters.
914
+ // Every user-derived or WebSocket-derived value rendered via innerHTML MUST pass through esc().
915
+ // If adding new terminal renderers or DOM builders, always use esc() on dynamic data.
912
916
  function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;').replace(/\\\\/g,'&#92;'); }
913
917
  function pad(n, w) { return String(n).padStart(w || 4, ' '); }
914
918
  function fmt(n) { return typeof n === 'number' ? n.toFixed(3) : '\\u2014'; }
@@ -1313,14 +1317,25 @@ function getDashboardHtml() {
1313
1317
  }
1314
1318
 
1315
1319
  // -- API calls --
1320
+ // Read token from URL query param \u2014 never embed the API key in the HTML body.
1321
+ var _authKey = new URLSearchParams(location.search).get('token');
1322
+
1323
+ function authHeaders() {
1324
+ var h = {};
1325
+ if (_authKey) h['Authorization'] = 'Bearer ' + _authKey;
1326
+ return h;
1327
+ }
1328
+
1316
1329
  function fetchJSON(path) {
1317
- return fetch(path).then(function(r) { return r.json(); });
1330
+ return fetch(path, { headers: authHeaders() }).then(function(r) { return r.json(); });
1318
1331
  }
1319
1332
 
1320
1333
  function postJSON(path, body) {
1334
+ var h = authHeaders();
1335
+ h['Content-Type'] = 'application/json';
1321
1336
  return fetch(path, {
1322
1337
  method: 'POST',
1323
- headers: { 'Content-Type': 'application/json' },
1338
+ headers: h,
1324
1339
  body: JSON.stringify(body),
1325
1340
  }).then(function(r) { return r.json(); });
1326
1341
  }
@@ -1382,7 +1397,9 @@ function getDashboardHtml() {
1382
1397
  // -- WebSocket --
1383
1398
  function connectWS() {
1384
1399
  var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
1385
- ws = new WebSocket(proto + '//' + location.host);
1400
+ var wsUrl = proto + '//' + location.host;
1401
+ if (_authKey) wsUrl += '?token=' + encodeURIComponent(_authKey);
1402
+ ws = new WebSocket(wsUrl);
1386
1403
 
1387
1404
  ws.onopen = function() {
1388
1405
  reconnectDelay = 1000;
@@ -1537,11 +1554,36 @@ var init_dashboard = __esm({
1537
1554
  }
1538
1555
  });
1539
1556
 
1557
+ // src/validation.ts
1558
+ function validateEvent(e) {
1559
+ if (!e || typeof e !== "object") return false;
1560
+ const ev = e;
1561
+ return typeof ev["type"] === "string" && VALID_EVENT_TYPES.has(ev["type"]) && typeof ev["timestamp"] === "number" && typeof ev["actor"] === "string";
1562
+ }
1563
+ var VALID_EVENT_TYPES;
1564
+ var init_validation = __esm({
1565
+ "src/validation.ts"() {
1566
+ "use strict";
1567
+ VALID_EVENT_TYPES = /* @__PURE__ */ new Set([
1568
+ "trade",
1569
+ "mint",
1570
+ "burn",
1571
+ "transfer",
1572
+ "produce",
1573
+ "consume",
1574
+ "role_change",
1575
+ "enter",
1576
+ "churn"
1577
+ ]);
1578
+ }
1579
+ });
1580
+
1540
1581
  // src/routes.ts
1541
1582
  function setSecurityHeaders(res) {
1542
1583
  res.setHeader("X-Content-Type-Options", "nosniff");
1543
1584
  res.setHeader("X-Frame-Options", "DENY");
1544
1585
  res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
1586
+ res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
1545
1587
  }
1546
1588
  function setCorsHeaders(res, allowedOrigin, requestOrigin) {
1547
1589
  setSecurityHeaders(res);
@@ -1586,17 +1628,28 @@ function readBody(req) {
1586
1628
  return new Promise((resolve, reject) => {
1587
1629
  const chunks = [];
1588
1630
  let totalBytes = 0;
1631
+ const timeout = setTimeout(() => {
1632
+ req.destroy();
1633
+ reject(new Error("Request body read timeout"));
1634
+ }, READ_BODY_TIMEOUT_MS);
1589
1635
  req.on("data", (chunk) => {
1590
1636
  totalBytes += chunk.length;
1591
1637
  if (totalBytes > MAX_BODY_BYTES) {
1638
+ clearTimeout(timeout);
1592
1639
  req.destroy();
1593
1640
  reject(new Error("Request body too large"));
1594
1641
  return;
1595
1642
  }
1596
1643
  chunks.push(chunk);
1597
1644
  });
1598
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
1599
- req.on("error", reject);
1645
+ req.on("end", () => {
1646
+ clearTimeout(timeout);
1647
+ resolve(Buffer.concat(chunks).toString("utf-8"));
1648
+ });
1649
+ req.on("error", (err) => {
1650
+ clearTimeout(timeout);
1651
+ reject(err);
1652
+ });
1600
1653
  });
1601
1654
  }
1602
1655
  function createRouteHandler(server) {
@@ -1643,9 +1696,10 @@ function createRouteHandler(server) {
1643
1696
  });
1644
1697
  return;
1645
1698
  }
1699
+ const validEvents = Array.isArray(events) ? events.filter(validateEvent) : void 0;
1646
1700
  const result = await server.processTick(
1647
1701
  state,
1648
- Array.isArray(events) ? events : void 0
1702
+ validEvents
1649
1703
  );
1650
1704
  const warnings = validation?.warnings ?? [];
1651
1705
  respond(200, {
@@ -1675,6 +1729,10 @@ function createRouteHandler(server) {
1675
1729
  return;
1676
1730
  }
1677
1731
  if (path === "/decisions" && method === "GET") {
1732
+ if (!checkAuth(req, apiKey)) {
1733
+ respond(401, { error: "Unauthorized" });
1734
+ return;
1735
+ }
1678
1736
  const rawLimit = parseInt(url.searchParams.get("limit") ?? "100", 10);
1679
1737
  const limit = Math.min(Math.max(Number.isNaN(rawLimit) ? 100 : rawLimit, 1), 1e3);
1680
1738
  const sinceParam = url.searchParams.get("since");
@@ -1690,7 +1748,11 @@ function createRouteHandler(server) {
1690
1748
  } else {
1691
1749
  decisions = agentE.log.latest(limit);
1692
1750
  }
1693
- respond(200, { decisions });
1751
+ const sanitized = decisions.map((d) => {
1752
+ const { metricsSnapshot: _, ...rest } = d;
1753
+ return rest;
1754
+ });
1755
+ respond(200, { decisions: sanitized });
1694
1756
  return;
1695
1757
  }
1696
1758
  if (path === "/config" && method === "POST") {
@@ -1708,18 +1770,18 @@ function createRouteHandler(server) {
1708
1770
  }
1709
1771
  const config = parsed;
1710
1772
  if (Array.isArray(config["lock"])) {
1711
- for (const param of config["lock"]) {
1773
+ for (const param of config["lock"].slice(0, MAX_CONFIG_ARRAY)) {
1712
1774
  if (typeof param === "string") server.lock(param);
1713
1775
  }
1714
1776
  }
1715
1777
  if (Array.isArray(config["unlock"])) {
1716
- for (const param of config["unlock"]) {
1778
+ for (const param of config["unlock"].slice(0, MAX_CONFIG_ARRAY)) {
1717
1779
  if (typeof param === "string") server.unlock(param);
1718
1780
  }
1719
1781
  }
1720
1782
  if (Array.isArray(config["constrain"])) {
1721
1783
  const validated = [];
1722
- for (const c of config["constrain"]) {
1784
+ for (const c of config["constrain"].slice(0, MAX_CONFIG_ARRAY)) {
1723
1785
  if (c && typeof c === "object" && typeof c["param"] === "string" && typeof c["min"] === "number" && typeof c["max"] === "number") {
1724
1786
  const constraint = c;
1725
1787
  if (!Number.isFinite(constraint.min) || !Number.isFinite(constraint.max)) {
@@ -1792,14 +1854,28 @@ function createRouteHandler(server) {
1792
1854
  return;
1793
1855
  }
1794
1856
  if (path === "/" && method === "GET" && server.serveDashboard) {
1857
+ if (apiKey) {
1858
+ const dashToken = url.searchParams.get("token") ?? "";
1859
+ const hasBearer = checkAuth(req, apiKey);
1860
+ const hasQueryToken = dashToken.length === apiKey.length && (0, import_node_crypto.timingSafeEqual)(Buffer.from(dashToken), Buffer.from(apiKey));
1861
+ if (!hasBearer && !hasQueryToken) {
1862
+ respond(401, { error: "Unauthorized \u2014 use ?token=<apiKey> or Authorization header" });
1863
+ return;
1864
+ }
1865
+ }
1795
1866
  setCorsHeaders(res, cors, reqOrigin);
1796
- 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:");
1797
- res.setHeader("Cache-Control", "public, max-age=60");
1867
+ const nonce = (0, import_node_crypto.randomBytes)(16).toString("base64");
1868
+ 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:`);
1869
+ res.setHeader("Cache-Control", "no-cache, private");
1798
1870
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1799
- res.end(getDashboardHtml());
1871
+ res.end(getDashboardHtml(nonce));
1800
1872
  return;
1801
1873
  }
1802
1874
  if (path === "/metrics" && method === "GET") {
1875
+ if (!checkAuth(req, apiKey)) {
1876
+ respond(401, { error: "Unauthorized" });
1877
+ return;
1878
+ }
1803
1879
  const agentE = server.getAgentE();
1804
1880
  const latest = agentE.store.latest();
1805
1881
  const history = agentE.store.recentHistory(100);
@@ -1807,6 +1883,10 @@ function createRouteHandler(server) {
1807
1883
  return;
1808
1884
  }
1809
1885
  if (path === "/metrics/personas" && method === "GET") {
1886
+ if (!checkAuth(req, apiKey)) {
1887
+ respond(401, { error: "Unauthorized" });
1888
+ return;
1889
+ }
1810
1890
  const agentE = server.getAgentE();
1811
1891
  const latest = agentE.store.latest();
1812
1892
  const dist = latest.personaDistribution || {};
@@ -1897,6 +1977,10 @@ function createRouteHandler(server) {
1897
1977
  return;
1898
1978
  }
1899
1979
  if (path === "/pending" && method === "GET") {
1980
+ if (!checkAuth(req, apiKey)) {
1981
+ respond(401, { error: "Unauthorized" });
1982
+ return;
1983
+ }
1900
1984
  const agentE = server.getAgentE();
1901
1985
  const pending = agentE.log.query({ result: "skipped_override" });
1902
1986
  respond(200, {
@@ -1913,14 +1997,17 @@ function createRouteHandler(server) {
1913
1997
  }
1914
1998
  };
1915
1999
  }
1916
- var import_node_crypto, import_core, MAX_BODY_BYTES;
2000
+ var import_node_crypto, import_core, MAX_BODY_BYTES, READ_BODY_TIMEOUT_MS, MAX_CONFIG_ARRAY;
1917
2001
  var init_routes = __esm({
1918
2002
  "src/routes.ts"() {
1919
2003
  "use strict";
1920
2004
  import_node_crypto = require("crypto");
1921
2005
  import_core = require("@agent-e/core");
1922
2006
  init_dashboard();
2007
+ init_validation();
1923
2008
  MAX_BODY_BYTES = 1048576;
2009
+ READ_BODY_TIMEOUT_MS = 3e4;
2010
+ MAX_CONFIG_ARRAY = 1e3;
1924
2011
  }
1925
2012
  });
1926
2013
 
@@ -1942,6 +2029,7 @@ function sanitizeJson2(obj) {
1942
2029
  }
1943
2030
  function createWebSocketHandler(httpServer, server) {
1944
2031
  const wss = new import_ws.WebSocketServer({ server: httpServer, maxPayload: MAX_WS_PAYLOAD });
2032
+ let globalLastTickTime = 0;
1945
2033
  const aliveMap = /* @__PURE__ */ new WeakMap();
1946
2034
  const heartbeatInterval = setInterval(() => {
1947
2035
  for (const ws of wss.clients) {
@@ -1969,7 +2057,8 @@ function createWebSocketHandler(httpServer, server) {
1969
2057
  }
1970
2058
  if (server.apiKey) {
1971
2059
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
1972
- const token = url.searchParams.get("token") ?? req.headers["authorization"]?.replace("Bearer ", "");
2060
+ const authHeader = req.headers["authorization"];
2061
+ const token = (authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : void 0) ?? url.searchParams.get("token");
1973
2062
  if (!token || token.length !== server.apiKey.length || !(0, import_node_crypto2.timingSafeEqual)(Buffer.from(token), Buffer.from(server.apiKey))) {
1974
2063
  ws.close(1008, "Unauthorized");
1975
2064
  return;
@@ -2003,7 +2092,12 @@ function createWebSocketHandler(httpServer, server) {
2003
2092
  send(ws, { type: "error", message: "Rate limited \u2014 min 100ms between ticks" });
2004
2093
  break;
2005
2094
  }
2095
+ if (now - globalLastTickTime < GLOBAL_MIN_TICK_INTERVAL_MS) {
2096
+ send(ws, { type: "error", message: "Rate limited \u2014 server tick capacity exceeded" });
2097
+ break;
2098
+ }
2006
2099
  lastTickTime = now;
2100
+ globalLastTickTime = now;
2007
2101
  const state = msg["state"];
2008
2102
  const events = msg["events"];
2009
2103
  if (server.validateState) {
@@ -2017,9 +2111,10 @@ function createWebSocketHandler(httpServer, server) {
2017
2111
  }
2018
2112
  }
2019
2113
  try {
2114
+ const validEvents = Array.isArray(events) ? events.filter(validateEvent) : void 0;
2020
2115
  const result = await server.processTick(
2021
2116
  state,
2022
- Array.isArray(events) ? events : void 0
2117
+ validEvents
2023
2118
  );
2024
2119
  send(ws, {
2025
2120
  type: "tick_result",
@@ -2039,13 +2134,17 @@ function createWebSocketHandler(httpServer, server) {
2039
2134
  break;
2040
2135
  }
2041
2136
  case "event": {
2042
- const event = msg["event"];
2043
- if (event) {
2044
- server.getAgentE().ingest(event);
2045
- send(ws, { type: "event_ack" });
2046
- } else {
2137
+ const rawEvent = msg["event"];
2138
+ if (!rawEvent) {
2047
2139
  send(ws, { type: "error", message: 'Missing "event" field' });
2140
+ break;
2048
2141
  }
2142
+ if (!validateEvent(rawEvent)) {
2143
+ send(ws, { type: "error", message: "Invalid event \u2014 requires type (valid event type), timestamp (number), and actor (string)" });
2144
+ break;
2145
+ }
2146
+ server.getAgentE().ingest(rawEvent);
2147
+ send(ws, { type: "event_ack" });
2049
2148
  break;
2050
2149
  }
2051
2150
  case "health": {
@@ -2103,16 +2202,18 @@ function createWebSocketHandler(httpServer, server) {
2103
2202
  broadcast
2104
2203
  };
2105
2204
  }
2106
- var import_node_crypto2, import_ws, import_core2, MAX_WS_PAYLOAD, MAX_WS_CONNECTIONS, MIN_TICK_INTERVAL_MS;
2205
+ var import_node_crypto2, import_ws, import_core2, MAX_WS_PAYLOAD, MAX_WS_CONNECTIONS, MIN_TICK_INTERVAL_MS, GLOBAL_MIN_TICK_INTERVAL_MS;
2107
2206
  var init_websocket = __esm({
2108
2207
  "src/websocket.ts"() {
2109
2208
  "use strict";
2110
2209
  import_node_crypto2 = require("crypto");
2111
2210
  import_ws = require("ws");
2112
2211
  import_core2 = require("@agent-e/core");
2212
+ init_validation();
2113
2213
  MAX_WS_PAYLOAD = 1048576;
2114
2214
  MAX_WS_CONNECTIONS = 100;
2115
2215
  MIN_TICK_INTERVAL_MS = 100;
2216
+ GLOBAL_MIN_TICK_INTERVAL_MS = 50;
2116
2217
  }
2117
2218
  });
2118
2219
 
@@ -2134,6 +2235,8 @@ var init_AgentEServer = __esm({
2134
2235
  this.lastState = null;
2135
2236
  this.adjustmentQueue = [];
2136
2237
  this.alerts = [];
2238
+ /** Serialization lock for processTick — prevents concurrent ticks from corrupting shared state. */
2239
+ this.tickLock = Promise.resolve();
2137
2240
  this.startedAt = Date.now();
2138
2241
  this.wsHandle = null;
2139
2242
  this.port = config.port ?? 3100;
@@ -2270,36 +2373,46 @@ var init_AgentEServer = __esm({
2270
2373
  * 6. Return response
2271
2374
  */
2272
2375
  async processTick(state, events) {
2273
- this.adjustmentQueue = [];
2274
- this.alerts = [];
2275
- this.lastState = state;
2276
- if (events) {
2277
- for (const event of events) {
2278
- this.agentE.ingest(event);
2376
+ const prev = this.tickLock;
2377
+ let unlock;
2378
+ this.tickLock = new Promise((resolve) => {
2379
+ unlock = resolve;
2380
+ });
2381
+ await prev;
2382
+ try {
2383
+ this.adjustmentQueue = [];
2384
+ this.alerts = [];
2385
+ this.lastState = state;
2386
+ if (events) {
2387
+ for (const event of events) {
2388
+ this.agentE.ingest(event);
2389
+ }
2279
2390
  }
2280
- }
2281
- await this.agentE.tick(state);
2282
- const rawAdj = [...this.adjustmentQueue];
2283
- this.adjustmentQueue = [];
2284
- const decisions = this.agentE.getDecisions({ since: state.tick, until: state.tick });
2285
- const adjustments = rawAdj.map((adj) => {
2286
- const decision = decisions.find(
2287
- (d) => d.plan.parameter === adj.key && d.result === "applied"
2288
- );
2391
+ await this.agentE.tick(state);
2392
+ const rawAdj = [...this.adjustmentQueue];
2393
+ this.adjustmentQueue = [];
2394
+ const decisions = this.agentE.getDecisions({ since: state.tick, until: state.tick });
2395
+ const adjustments = rawAdj.map((adj) => {
2396
+ const decision = decisions.find(
2397
+ (d) => d.plan.parameter === adj.key && d.result === "applied"
2398
+ );
2399
+ return {
2400
+ parameter: adj.key,
2401
+ value: adj.value,
2402
+ ...adj.scope ? { scope: adj.scope } : {},
2403
+ reasoning: decision?.diagnosis.violation.suggestedAction.reasoning ?? ""
2404
+ };
2405
+ });
2289
2406
  return {
2290
- parameter: adj.key,
2291
- value: adj.value,
2292
- ...adj.scope ? { scope: adj.scope } : {},
2293
- reasoning: decision?.diagnosis.violation.suggestedAction.reasoning ?? ""
2407
+ adjustments,
2408
+ alerts: [...this.alerts],
2409
+ health: this.agentE.getHealth(),
2410
+ tick: state.tick,
2411
+ decisions
2294
2412
  };
2295
- });
2296
- return {
2297
- adjustments,
2298
- alerts: [...this.alerts],
2299
- health: this.agentE.getHealth(),
2300
- tick: state.tick,
2301
- decisions
2302
- };
2413
+ } finally {
2414
+ unlock();
2415
+ }
2303
2416
  }
2304
2417
  /**
2305
2418
  * Run Observer + Diagnoser on the given state without side effects (no execution).