@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 +54 -0
- package/dist/AgentEServer-A7K6426K.mjs +7 -0
- package/dist/{chunk-XFI27OM4.mjs → chunk-3HGNFK4A.mjs} +243 -53
- package/dist/chunk-3HGNFK4A.mjs.map +1 -0
- package/dist/cli.js +241 -51
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +251 -53
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
- package/dist/AgentEServer-O3DNPERG.mjs +0 -7
- package/dist/chunk-XFI27OM4.mjs.map +0 -1
- /package/dist/{AgentEServer-O3DNPERG.mjs.map → AgentEServer-A7K6426K.mjs.map} +0 -0
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"
|
|
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
|
|
|
@@ -340,6 +341,15 @@ function getDashboardHtml() {
|
|
|
340
341
|
.t-pending-icon { color: var(--warning); }
|
|
341
342
|
.t-pending-val { color: var(--warning); font-variant-numeric: tabular-nums; }
|
|
342
343
|
|
|
344
|
+
/* -- LLM line colors (V1.8.1) -- */
|
|
345
|
+
.t-narration-icon { color: #a78bfa; }
|
|
346
|
+
.t-narration { color: #c4b5fd; }
|
|
347
|
+
.t-explanation-icon { color: #60a5fa; }
|
|
348
|
+
.t-explanation { color: #93c5fd; }
|
|
349
|
+
.t-anomaly-icon { color: #f59e0b; }
|
|
350
|
+
.t-anomaly-label { color: #fbbf24; }
|
|
351
|
+
.t-anomaly { color: #fcd34d; }
|
|
352
|
+
|
|
343
353
|
/* -- Advisor Inline Buttons -- */
|
|
344
354
|
.advisor-btn {
|
|
345
355
|
display: none;
|
|
@@ -718,7 +728,7 @@ function getDashboardHtml() {
|
|
|
718
728
|
<header class="header" id="header">
|
|
719
729
|
<div class="header-left">
|
|
720
730
|
<span class="header-logo">AgentE</span>
|
|
721
|
-
<span class="header-version">v1.8.
|
|
731
|
+
<span class="header-version">v1.8.1</span>
|
|
722
732
|
</div>
|
|
723
733
|
<div class="header-right">
|
|
724
734
|
<div class="kpi-pill">
|
|
@@ -854,7 +864,7 @@ function getDashboardHtml() {
|
|
|
854
864
|
|
|
855
865
|
</main>
|
|
856
866
|
|
|
857
|
-
<script
|
|
867
|
+
<script` + nonceAttr + `>
|
|
858
868
|
(function() {
|
|
859
869
|
'use strict';
|
|
860
870
|
|
|
@@ -901,6 +911,9 @@ function getDashboardHtml() {
|
|
|
901
911
|
var $dashboardRoot = document.getElementById('dashboard-root');
|
|
902
912
|
|
|
903
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.
|
|
904
917
|
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''').replace(/\\\\/g,'\'); }
|
|
905
918
|
function pad(n, w) { return String(n).padStart(w || 4, ' '); }
|
|
906
919
|
function fmt(n) { return typeof n === 'number' ? n.toFixed(3) : '\\u2014'; }
|
|
@@ -1056,6 +1069,31 @@ function getDashboardHtml() {
|
|
|
1056
1069
|
+ advisorBtns;
|
|
1057
1070
|
}
|
|
1058
1071
|
|
|
1072
|
+
// -- LLM line renderers (V1.8.1) --
|
|
1073
|
+
function narrationToTerminal(msg) {
|
|
1074
|
+
return '<span class="t-tick">[Tick ' + pad(msg.tick) + ']</span> '
|
|
1075
|
+
+ '<span class="t-narration-icon">\\u{1F9E0} </span>'
|
|
1076
|
+
+ '<span class="t-narration">"' + esc(msg.text) + '"</span>';
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function explanationToTerminal(msg) {
|
|
1080
|
+
return '<span class="t-tick">[Tick ' + pad(msg.tick) + ']</span> '
|
|
1081
|
+
+ '<span class="t-explanation-icon">\\u{1F4A1} </span>'
|
|
1082
|
+
+ '<span class="t-explanation">"' + esc(msg.text) + '"</span>';
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function anomalyToTerminal(msg) {
|
|
1086
|
+
var metricsStr = '';
|
|
1087
|
+
if (msg.metrics && msg.metrics.length > 0) {
|
|
1088
|
+
var m = msg.metrics[0];
|
|
1089
|
+
metricsStr = m.name + ' ' + m.deviation.toFixed(1) + '\\u03C3 \u2014 ';
|
|
1090
|
+
}
|
|
1091
|
+
return '<span class="t-tick">[Tick ' + pad(msg.tick) + ']</span> '
|
|
1092
|
+
+ '<span class="t-anomaly-icon">\\u{1F50D} </span>'
|
|
1093
|
+
+ '<span class="t-anomaly-label">Anomaly detected: </span>'
|
|
1094
|
+
+ '<span class="t-anomaly">' + esc(metricsStr) + '"' + esc(msg.text) + '"</span>';
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1059
1097
|
// -- Alerts --
|
|
1060
1098
|
function renderAlerts(alerts) {
|
|
1061
1099
|
if (!alerts || alerts.length === 0) {
|
|
@@ -1280,14 +1318,25 @@ function getDashboardHtml() {
|
|
|
1280
1318
|
}
|
|
1281
1319
|
|
|
1282
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
|
+
|
|
1283
1330
|
function fetchJSON(path) {
|
|
1284
|
-
return fetch(path).then(function(r) { return r.json(); });
|
|
1331
|
+
return fetch(path, { headers: authHeaders() }).then(function(r) { return r.json(); });
|
|
1285
1332
|
}
|
|
1286
1333
|
|
|
1287
1334
|
function postJSON(path, body) {
|
|
1335
|
+
var h = authHeaders();
|
|
1336
|
+
h['Content-Type'] = 'application/json';
|
|
1288
1337
|
return fetch(path, {
|
|
1289
1338
|
method: 'POST',
|
|
1290
|
-
headers:
|
|
1339
|
+
headers: h,
|
|
1291
1340
|
body: JSON.stringify(body),
|
|
1292
1341
|
}).then(function(r) { return r.json(); });
|
|
1293
1342
|
}
|
|
@@ -1349,7 +1398,9 @@ function getDashboardHtml() {
|
|
|
1349
1398
|
// -- WebSocket --
|
|
1350
1399
|
function connectWS() {
|
|
1351
1400
|
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
1352
|
-
|
|
1401
|
+
var wsUrl = proto + '//' + location.host;
|
|
1402
|
+
if (_authKey) wsUrl += '?token=' + encodeURIComponent(_authKey);
|
|
1403
|
+
ws = new WebSocket(wsUrl);
|
|
1353
1404
|
|
|
1354
1405
|
ws.onopen = function() {
|
|
1355
1406
|
reconnectDelay = 1000;
|
|
@@ -1395,6 +1446,19 @@ function getDashboardHtml() {
|
|
|
1395
1446
|
$hPending.textContent = pendingDecisions.length;
|
|
1396
1447
|
}
|
|
1397
1448
|
break;
|
|
1449
|
+
|
|
1450
|
+
// V1.8.1: LLM feed events
|
|
1451
|
+
case 'narration':
|
|
1452
|
+
addTerminalLine(narrationToTerminal(msg));
|
|
1453
|
+
break;
|
|
1454
|
+
|
|
1455
|
+
case 'explanation':
|
|
1456
|
+
addTerminalLine(explanationToTerminal(msg));
|
|
1457
|
+
break;
|
|
1458
|
+
|
|
1459
|
+
case 'anomaly':
|
|
1460
|
+
addTerminalLine(anomalyToTerminal(msg));
|
|
1461
|
+
break;
|
|
1398
1462
|
}
|
|
1399
1463
|
};
|
|
1400
1464
|
}
|
|
@@ -1486,11 +1550,30 @@ function getDashboardHtml() {
|
|
|
1486
1550
|
</html>`;
|
|
1487
1551
|
}
|
|
1488
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
|
+
|
|
1489
1571
|
// src/routes.ts
|
|
1490
1572
|
function setSecurityHeaders(res) {
|
|
1491
1573
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
1492
1574
|
res.setHeader("X-Frame-Options", "DENY");
|
|
1493
1575
|
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
1576
|
+
res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
|
1494
1577
|
}
|
|
1495
1578
|
function setCorsHeaders(res, allowedOrigin, requestOrigin) {
|
|
1496
1579
|
setSecurityHeaders(res);
|
|
@@ -1532,21 +1615,34 @@ function json(res, status, data, origin, reqOrigin) {
|
|
|
1532
1615
|
res.end(JSON.stringify(data));
|
|
1533
1616
|
}
|
|
1534
1617
|
var MAX_BODY_BYTES = 1048576;
|
|
1618
|
+
var READ_BODY_TIMEOUT_MS = 3e4;
|
|
1619
|
+
var MAX_CONFIG_ARRAY = 1e3;
|
|
1535
1620
|
function readBody(req) {
|
|
1536
1621
|
return new Promise((resolve, reject) => {
|
|
1537
1622
|
const chunks = [];
|
|
1538
1623
|
let totalBytes = 0;
|
|
1624
|
+
const timeout = setTimeout(() => {
|
|
1625
|
+
req.destroy();
|
|
1626
|
+
reject(new Error("Request body read timeout"));
|
|
1627
|
+
}, READ_BODY_TIMEOUT_MS);
|
|
1539
1628
|
req.on("data", (chunk) => {
|
|
1540
1629
|
totalBytes += chunk.length;
|
|
1541
1630
|
if (totalBytes > MAX_BODY_BYTES) {
|
|
1631
|
+
clearTimeout(timeout);
|
|
1542
1632
|
req.destroy();
|
|
1543
1633
|
reject(new Error("Request body too large"));
|
|
1544
1634
|
return;
|
|
1545
1635
|
}
|
|
1546
1636
|
chunks.push(chunk);
|
|
1547
1637
|
});
|
|
1548
|
-
req.on("end", () =>
|
|
1549
|
-
|
|
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
|
+
});
|
|
1550
1646
|
});
|
|
1551
1647
|
}
|
|
1552
1648
|
function createRouteHandler(server2) {
|
|
@@ -1593,9 +1689,10 @@ function createRouteHandler(server2) {
|
|
|
1593
1689
|
});
|
|
1594
1690
|
return;
|
|
1595
1691
|
}
|
|
1692
|
+
const validEvents = Array.isArray(events) ? events.filter(validateEvent) : void 0;
|
|
1596
1693
|
const result = await server2.processTick(
|
|
1597
1694
|
state,
|
|
1598
|
-
|
|
1695
|
+
validEvents
|
|
1599
1696
|
);
|
|
1600
1697
|
const warnings = validation?.warnings ?? [];
|
|
1601
1698
|
respond(200, {
|
|
@@ -1625,6 +1722,10 @@ function createRouteHandler(server2) {
|
|
|
1625
1722
|
return;
|
|
1626
1723
|
}
|
|
1627
1724
|
if (path === "/decisions" && method === "GET") {
|
|
1725
|
+
if (!checkAuth(req, apiKey)) {
|
|
1726
|
+
respond(401, { error: "Unauthorized" });
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1628
1729
|
const rawLimit = parseInt(url.searchParams.get("limit") ?? "100", 10);
|
|
1629
1730
|
const limit = Math.min(Math.max(Number.isNaN(rawLimit) ? 100 : rawLimit, 1), 1e3);
|
|
1630
1731
|
const sinceParam = url.searchParams.get("since");
|
|
@@ -1640,7 +1741,11 @@ function createRouteHandler(server2) {
|
|
|
1640
1741
|
} else {
|
|
1641
1742
|
decisions = agentE.log.latest(limit);
|
|
1642
1743
|
}
|
|
1643
|
-
|
|
1744
|
+
const sanitized = decisions.map((d) => {
|
|
1745
|
+
const { metricsSnapshot: _, ...rest } = d;
|
|
1746
|
+
return rest;
|
|
1747
|
+
});
|
|
1748
|
+
respond(200, { decisions: sanitized });
|
|
1644
1749
|
return;
|
|
1645
1750
|
}
|
|
1646
1751
|
if (path === "/config" && method === "POST") {
|
|
@@ -1658,18 +1763,18 @@ function createRouteHandler(server2) {
|
|
|
1658
1763
|
}
|
|
1659
1764
|
const config = parsed;
|
|
1660
1765
|
if (Array.isArray(config["lock"])) {
|
|
1661
|
-
for (const param of config["lock"]) {
|
|
1766
|
+
for (const param of config["lock"].slice(0, MAX_CONFIG_ARRAY)) {
|
|
1662
1767
|
if (typeof param === "string") server2.lock(param);
|
|
1663
1768
|
}
|
|
1664
1769
|
}
|
|
1665
1770
|
if (Array.isArray(config["unlock"])) {
|
|
1666
|
-
for (const param of config["unlock"]) {
|
|
1771
|
+
for (const param of config["unlock"].slice(0, MAX_CONFIG_ARRAY)) {
|
|
1667
1772
|
if (typeof param === "string") server2.unlock(param);
|
|
1668
1773
|
}
|
|
1669
1774
|
}
|
|
1670
1775
|
if (Array.isArray(config["constrain"])) {
|
|
1671
1776
|
const validated = [];
|
|
1672
|
-
for (const c of config["constrain"]) {
|
|
1777
|
+
for (const c of config["constrain"].slice(0, MAX_CONFIG_ARRAY)) {
|
|
1673
1778
|
if (c && typeof c === "object" && typeof c["param"] === "string" && typeof c["min"] === "number" && typeof c["max"] === "number") {
|
|
1674
1779
|
const constraint = c;
|
|
1675
1780
|
if (!Number.isFinite(constraint.min) || !Number.isFinite(constraint.max)) {
|
|
@@ -1742,14 +1847,28 @@ function createRouteHandler(server2) {
|
|
|
1742
1847
|
return;
|
|
1743
1848
|
}
|
|
1744
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
|
+
}
|
|
1745
1859
|
setCorsHeaders(res, cors, reqOrigin);
|
|
1746
|
-
|
|
1747
|
-
res.setHeader("
|
|
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");
|
|
1748
1863
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1749
|
-
res.end(getDashboardHtml());
|
|
1864
|
+
res.end(getDashboardHtml(nonce));
|
|
1750
1865
|
return;
|
|
1751
1866
|
}
|
|
1752
1867
|
if (path === "/metrics" && method === "GET") {
|
|
1868
|
+
if (!checkAuth(req, apiKey)) {
|
|
1869
|
+
respond(401, { error: "Unauthorized" });
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1753
1872
|
const agentE = server2.getAgentE();
|
|
1754
1873
|
const latest = agentE.store.latest();
|
|
1755
1874
|
const history = agentE.store.recentHistory(100);
|
|
@@ -1757,6 +1876,10 @@ function createRouteHandler(server2) {
|
|
|
1757
1876
|
return;
|
|
1758
1877
|
}
|
|
1759
1878
|
if (path === "/metrics/personas" && method === "GET") {
|
|
1879
|
+
if (!checkAuth(req, apiKey)) {
|
|
1880
|
+
respond(401, { error: "Unauthorized" });
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1760
1883
|
const agentE = server2.getAgentE();
|
|
1761
1884
|
const latest = agentE.store.latest();
|
|
1762
1885
|
const dist = latest.personaDistribution || {};
|
|
@@ -1847,6 +1970,10 @@ function createRouteHandler(server2) {
|
|
|
1847
1970
|
return;
|
|
1848
1971
|
}
|
|
1849
1972
|
if (path === "/pending" && method === "GET") {
|
|
1973
|
+
if (!checkAuth(req, apiKey)) {
|
|
1974
|
+
respond(401, { error: "Unauthorized" });
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1850
1977
|
const agentE = server2.getAgentE();
|
|
1851
1978
|
const pending = agentE.log.query({ result: "skipped_override" });
|
|
1852
1979
|
respond(200, {
|
|
@@ -1876,6 +2003,7 @@ function send(ws, data) {
|
|
|
1876
2003
|
var MAX_WS_PAYLOAD = 1048576;
|
|
1877
2004
|
var MAX_WS_CONNECTIONS = 100;
|
|
1878
2005
|
var MIN_TICK_INTERVAL_MS = 100;
|
|
2006
|
+
var GLOBAL_MIN_TICK_INTERVAL_MS = 50;
|
|
1879
2007
|
function sanitizeJson2(obj) {
|
|
1880
2008
|
if (obj === null || typeof obj !== "object") return obj;
|
|
1881
2009
|
if (Array.isArray(obj)) return obj.map(sanitizeJson2);
|
|
@@ -1888,6 +2016,7 @@ function sanitizeJson2(obj) {
|
|
|
1888
2016
|
}
|
|
1889
2017
|
function createWebSocketHandler(httpServer, server2) {
|
|
1890
2018
|
const wss = new import_ws.WebSocketServer({ server: httpServer, maxPayload: MAX_WS_PAYLOAD });
|
|
2019
|
+
let globalLastTickTime = 0;
|
|
1891
2020
|
const aliveMap = /* @__PURE__ */ new WeakMap();
|
|
1892
2021
|
const heartbeatInterval = setInterval(() => {
|
|
1893
2022
|
for (const ws of wss.clients) {
|
|
@@ -1915,7 +2044,8 @@ function createWebSocketHandler(httpServer, server2) {
|
|
|
1915
2044
|
}
|
|
1916
2045
|
if (server2.apiKey) {
|
|
1917
2046
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
1918
|
-
const
|
|
2047
|
+
const authHeader = req.headers["authorization"];
|
|
2048
|
+
const token = (authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : void 0) ?? url.searchParams.get("token");
|
|
1919
2049
|
if (!token || token.length !== server2.apiKey.length || !(0, import_node_crypto2.timingSafeEqual)(Buffer.from(token), Buffer.from(server2.apiKey))) {
|
|
1920
2050
|
ws.close(1008, "Unauthorized");
|
|
1921
2051
|
return;
|
|
@@ -1949,7 +2079,12 @@ function createWebSocketHandler(httpServer, server2) {
|
|
|
1949
2079
|
send(ws, { type: "error", message: "Rate limited \u2014 min 100ms between ticks" });
|
|
1950
2080
|
break;
|
|
1951
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
|
+
}
|
|
1952
2086
|
lastTickTime = now;
|
|
2087
|
+
globalLastTickTime = now;
|
|
1953
2088
|
const state = msg["state"];
|
|
1954
2089
|
const events = msg["events"];
|
|
1955
2090
|
if (server2.validateState) {
|
|
@@ -1963,9 +2098,10 @@ function createWebSocketHandler(httpServer, server2) {
|
|
|
1963
2098
|
}
|
|
1964
2099
|
}
|
|
1965
2100
|
try {
|
|
2101
|
+
const validEvents = Array.isArray(events) ? events.filter(validateEvent) : void 0;
|
|
1966
2102
|
const result = await server2.processTick(
|
|
1967
2103
|
state,
|
|
1968
|
-
|
|
2104
|
+
validEvents
|
|
1969
2105
|
);
|
|
1970
2106
|
send(ws, {
|
|
1971
2107
|
type: "tick_result",
|
|
@@ -1985,13 +2121,17 @@ function createWebSocketHandler(httpServer, server2) {
|
|
|
1985
2121
|
break;
|
|
1986
2122
|
}
|
|
1987
2123
|
case "event": {
|
|
1988
|
-
const
|
|
1989
|
-
if (
|
|
1990
|
-
server2.getAgentE().ingest(event);
|
|
1991
|
-
send(ws, { type: "event_ack" });
|
|
1992
|
-
} else {
|
|
2124
|
+
const rawEvent = msg["event"];
|
|
2125
|
+
if (!rawEvent) {
|
|
1993
2126
|
send(ws, { type: "error", message: 'Missing "event" field' });
|
|
2127
|
+
break;
|
|
1994
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" });
|
|
1995
2135
|
break;
|
|
1996
2136
|
}
|
|
1997
2137
|
case "health": {
|
|
@@ -2056,6 +2196,8 @@ var AgentEServer = class {
|
|
|
2056
2196
|
this.lastState = null;
|
|
2057
2197
|
this.adjustmentQueue = [];
|
|
2058
2198
|
this.alerts = [];
|
|
2199
|
+
/** Serialization lock for processTick — prevents concurrent ticks from corrupting shared state. */
|
|
2200
|
+
this.tickLock = Promise.resolve();
|
|
2059
2201
|
this.startedAt = Date.now();
|
|
2060
2202
|
this.wsHandle = null;
|
|
2061
2203
|
this.port = config.port ?? 3100;
|
|
@@ -2107,6 +2249,44 @@ var AgentEServer = class {
|
|
|
2107
2249
|
this.agentE.on("alert", (diagnosis) => {
|
|
2108
2250
|
this.alerts.push(diagnosis);
|
|
2109
2251
|
});
|
|
2252
|
+
this.agentE.on("narration", (n) => {
|
|
2253
|
+
const narration = n;
|
|
2254
|
+
const tick = this.lastState?.tick ?? 0;
|
|
2255
|
+
this.broadcast({
|
|
2256
|
+
type: "narration",
|
|
2257
|
+
tick,
|
|
2258
|
+
text: narration.narration,
|
|
2259
|
+
principle: narration.diagnosis.principle.name,
|
|
2260
|
+
severity: narration.diagnosis.violation.severity,
|
|
2261
|
+
confidence: narration.confidence
|
|
2262
|
+
});
|
|
2263
|
+
});
|
|
2264
|
+
this.agentE.on("explanation", (e) => {
|
|
2265
|
+
const explanation = e;
|
|
2266
|
+
this.broadcast({
|
|
2267
|
+
type: "explanation",
|
|
2268
|
+
tick: this.lastState?.tick ?? 0,
|
|
2269
|
+
text: explanation.explanation,
|
|
2270
|
+
parameter: explanation.plan.parameter,
|
|
2271
|
+
direction: explanation.plan.targetValue > explanation.plan.currentValue ? "increase" : "decrease",
|
|
2272
|
+
expectedOutcome: explanation.expectedOutcome,
|
|
2273
|
+
risks: explanation.risks
|
|
2274
|
+
});
|
|
2275
|
+
});
|
|
2276
|
+
this.agentE.on("anomaly", (a) => {
|
|
2277
|
+
const anomaly = a;
|
|
2278
|
+
this.broadcast({
|
|
2279
|
+
type: "anomaly",
|
|
2280
|
+
tick: anomaly.tick,
|
|
2281
|
+
text: anomaly.interpretation,
|
|
2282
|
+
metrics: anomaly.anomalies.map((m) => ({
|
|
2283
|
+
name: m.metric,
|
|
2284
|
+
deviation: m.deviation,
|
|
2285
|
+
currentValue: m.currentValue
|
|
2286
|
+
})),
|
|
2287
|
+
severity: anomaly.severity
|
|
2288
|
+
});
|
|
2289
|
+
});
|
|
2110
2290
|
this.agentE.connect(adapter).start();
|
|
2111
2291
|
const routeHandler = createRouteHandler(this);
|
|
2112
2292
|
this.server = http.createServer(routeHandler);
|
|
@@ -2154,36 +2334,46 @@ var AgentEServer = class {
|
|
|
2154
2334
|
* 6. Return response
|
|
2155
2335
|
*/
|
|
2156
2336
|
async processTick(state, events) {
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
this.
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
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
|
+
}
|
|
2163
2351
|
}
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
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
|
+
});
|
|
2173
2367
|
return {
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2368
|
+
adjustments,
|
|
2369
|
+
alerts: [...this.alerts],
|
|
2370
|
+
health: this.agentE.getHealth(),
|
|
2371
|
+
tick: state.tick,
|
|
2372
|
+
decisions
|
|
2178
2373
|
};
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
alerts: [...this.alerts],
|
|
2183
|
-
health: this.agentE.getHealth(),
|
|
2184
|
-
tick: state.tick,
|
|
2185
|
-
decisions
|
|
2186
|
-
};
|
|
2374
|
+
} finally {
|
|
2375
|
+
unlock();
|
|
2376
|
+
}
|
|
2187
2377
|
}
|
|
2188
2378
|
/**
|
|
2189
2379
|
* Run Observer + Diagnoser on the given state without side effects (no execution).
|