@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/README.md +54 -0
- package/dist/AgentEServer-A7K6426K.mjs +7 -0
- package/dist/{chunk-ALGME445.mjs → chunk-3HGNFK4A.mjs} +157 -52
- package/dist/chunk-3HGNFK4A.mjs.map +1 -0
- package/dist/cli.js +155 -50
- 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 +165 -52
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
- package/dist/AgentEServer-KIQBHRUS.mjs +0 -7
- package/dist/chunk-ALGME445.mjs.map +0 -1
- /package/dist/{AgentEServer-KIQBHRUS.mjs.map → AgentEServer-A7K6426K.mjs.map} +0 -0
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:
|
|
@@ -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"
|
|
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
|
|
|
@@ -844,7 +845,7 @@ function getDashboardHtml() {
|
|
|
844
845
|
|
|
845
846
|
</main>
|
|
846
847
|
|
|
847
|
-
<script
|
|
848
|
+
<script` + nonceAttr + `>
|
|
848
849
|
(function() {
|
|
849
850
|
'use strict';
|
|
850
851
|
|
|
@@ -891,6 +892,9 @@ function getDashboardHtml() {
|
|
|
891
892
|
var $dashboardRoot = document.getElementById('dashboard-root');
|
|
892
893
|
|
|
893
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.
|
|
894
898
|
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''').replace(/\\\\/g,'\'); }
|
|
895
899
|
function pad(n, w) { return String(n).padStart(w || 4, ' '); }
|
|
896
900
|
function fmt(n) { return typeof n === 'number' ? n.toFixed(3) : '\\u2014'; }
|
|
@@ -1295,14 +1299,25 @@ function getDashboardHtml() {
|
|
|
1295
1299
|
}
|
|
1296
1300
|
|
|
1297
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
|
+
|
|
1298
1311
|
function fetchJSON(path) {
|
|
1299
|
-
return fetch(path).then(function(r) { return r.json(); });
|
|
1312
|
+
return fetch(path, { headers: authHeaders() }).then(function(r) { return r.json(); });
|
|
1300
1313
|
}
|
|
1301
1314
|
|
|
1302
1315
|
function postJSON(path, body) {
|
|
1316
|
+
var h = authHeaders();
|
|
1317
|
+
h['Content-Type'] = 'application/json';
|
|
1303
1318
|
return fetch(path, {
|
|
1304
1319
|
method: 'POST',
|
|
1305
|
-
headers:
|
|
1320
|
+
headers: h,
|
|
1306
1321
|
body: JSON.stringify(body),
|
|
1307
1322
|
}).then(function(r) { return r.json(); });
|
|
1308
1323
|
}
|
|
@@ -1364,7 +1379,9 @@ function getDashboardHtml() {
|
|
|
1364
1379
|
// -- WebSocket --
|
|
1365
1380
|
function connectWS() {
|
|
1366
1381
|
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
1367
|
-
|
|
1382
|
+
var wsUrl = proto + '//' + location.host;
|
|
1383
|
+
if (_authKey) wsUrl += '?token=' + encodeURIComponent(_authKey);
|
|
1384
|
+
ws = new WebSocket(wsUrl);
|
|
1368
1385
|
|
|
1369
1386
|
ws.onopen = function() {
|
|
1370
1387
|
reconnectDelay = 1000;
|
|
@@ -1514,11 +1531,30 @@ function getDashboardHtml() {
|
|
|
1514
1531
|
</html>`;
|
|
1515
1532
|
}
|
|
1516
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
|
+
|
|
1517
1552
|
// src/routes.ts
|
|
1518
1553
|
function setSecurityHeaders(res) {
|
|
1519
1554
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
1520
1555
|
res.setHeader("X-Frame-Options", "DENY");
|
|
1521
1556
|
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
1557
|
+
res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
|
1522
1558
|
}
|
|
1523
1559
|
function setCorsHeaders(res, allowedOrigin, requestOrigin) {
|
|
1524
1560
|
setSecurityHeaders(res);
|
|
@@ -1560,21 +1596,34 @@ function json(res, status, data, origin, reqOrigin) {
|
|
|
1560
1596
|
res.end(JSON.stringify(data));
|
|
1561
1597
|
}
|
|
1562
1598
|
var MAX_BODY_BYTES = 1048576;
|
|
1599
|
+
var READ_BODY_TIMEOUT_MS = 3e4;
|
|
1600
|
+
var MAX_CONFIG_ARRAY = 1e3;
|
|
1563
1601
|
function readBody(req) {
|
|
1564
1602
|
return new Promise((resolve, reject) => {
|
|
1565
1603
|
const chunks = [];
|
|
1566
1604
|
let totalBytes = 0;
|
|
1605
|
+
const timeout = setTimeout(() => {
|
|
1606
|
+
req.destroy();
|
|
1607
|
+
reject(new Error("Request body read timeout"));
|
|
1608
|
+
}, READ_BODY_TIMEOUT_MS);
|
|
1567
1609
|
req.on("data", (chunk) => {
|
|
1568
1610
|
totalBytes += chunk.length;
|
|
1569
1611
|
if (totalBytes > MAX_BODY_BYTES) {
|
|
1612
|
+
clearTimeout(timeout);
|
|
1570
1613
|
req.destroy();
|
|
1571
1614
|
reject(new Error("Request body too large"));
|
|
1572
1615
|
return;
|
|
1573
1616
|
}
|
|
1574
1617
|
chunks.push(chunk);
|
|
1575
1618
|
});
|
|
1576
|
-
req.on("end", () =>
|
|
1577
|
-
|
|
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
|
+
});
|
|
1578
1627
|
});
|
|
1579
1628
|
}
|
|
1580
1629
|
function createRouteHandler(server) {
|
|
@@ -1621,9 +1670,10 @@ function createRouteHandler(server) {
|
|
|
1621
1670
|
});
|
|
1622
1671
|
return;
|
|
1623
1672
|
}
|
|
1673
|
+
const validEvents = Array.isArray(events) ? events.filter(validateEvent) : void 0;
|
|
1624
1674
|
const result = await server.processTick(
|
|
1625
1675
|
state,
|
|
1626
|
-
|
|
1676
|
+
validEvents
|
|
1627
1677
|
);
|
|
1628
1678
|
const warnings = validation?.warnings ?? [];
|
|
1629
1679
|
respond(200, {
|
|
@@ -1653,6 +1703,10 @@ function createRouteHandler(server) {
|
|
|
1653
1703
|
return;
|
|
1654
1704
|
}
|
|
1655
1705
|
if (path === "/decisions" && method === "GET") {
|
|
1706
|
+
if (!checkAuth(req, apiKey)) {
|
|
1707
|
+
respond(401, { error: "Unauthorized" });
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1656
1710
|
const rawLimit = parseInt(url.searchParams.get("limit") ?? "100", 10);
|
|
1657
1711
|
const limit = Math.min(Math.max(Number.isNaN(rawLimit) ? 100 : rawLimit, 1), 1e3);
|
|
1658
1712
|
const sinceParam = url.searchParams.get("since");
|
|
@@ -1668,7 +1722,11 @@ function createRouteHandler(server) {
|
|
|
1668
1722
|
} else {
|
|
1669
1723
|
decisions = agentE.log.latest(limit);
|
|
1670
1724
|
}
|
|
1671
|
-
|
|
1725
|
+
const sanitized = decisions.map((d) => {
|
|
1726
|
+
const { metricsSnapshot: _, ...rest } = d;
|
|
1727
|
+
return rest;
|
|
1728
|
+
});
|
|
1729
|
+
respond(200, { decisions: sanitized });
|
|
1672
1730
|
return;
|
|
1673
1731
|
}
|
|
1674
1732
|
if (path === "/config" && method === "POST") {
|
|
@@ -1686,18 +1744,18 @@ function createRouteHandler(server) {
|
|
|
1686
1744
|
}
|
|
1687
1745
|
const config = parsed;
|
|
1688
1746
|
if (Array.isArray(config["lock"])) {
|
|
1689
|
-
for (const param of config["lock"]) {
|
|
1747
|
+
for (const param of config["lock"].slice(0, MAX_CONFIG_ARRAY)) {
|
|
1690
1748
|
if (typeof param === "string") server.lock(param);
|
|
1691
1749
|
}
|
|
1692
1750
|
}
|
|
1693
1751
|
if (Array.isArray(config["unlock"])) {
|
|
1694
|
-
for (const param of config["unlock"]) {
|
|
1752
|
+
for (const param of config["unlock"].slice(0, MAX_CONFIG_ARRAY)) {
|
|
1695
1753
|
if (typeof param === "string") server.unlock(param);
|
|
1696
1754
|
}
|
|
1697
1755
|
}
|
|
1698
1756
|
if (Array.isArray(config["constrain"])) {
|
|
1699
1757
|
const validated = [];
|
|
1700
|
-
for (const c of config["constrain"]) {
|
|
1758
|
+
for (const c of config["constrain"].slice(0, MAX_CONFIG_ARRAY)) {
|
|
1701
1759
|
if (c && typeof c === "object" && typeof c["param"] === "string" && typeof c["min"] === "number" && typeof c["max"] === "number") {
|
|
1702
1760
|
const constraint = c;
|
|
1703
1761
|
if (!Number.isFinite(constraint.min) || !Number.isFinite(constraint.max)) {
|
|
@@ -1770,14 +1828,28 @@ function createRouteHandler(server) {
|
|
|
1770
1828
|
return;
|
|
1771
1829
|
}
|
|
1772
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
|
+
}
|
|
1773
1840
|
setCorsHeaders(res, cors, reqOrigin);
|
|
1774
|
-
|
|
1775
|
-
res.setHeader("
|
|
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");
|
|
1776
1844
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1777
|
-
res.end(getDashboardHtml());
|
|
1845
|
+
res.end(getDashboardHtml(nonce));
|
|
1778
1846
|
return;
|
|
1779
1847
|
}
|
|
1780
1848
|
if (path === "/metrics" && method === "GET") {
|
|
1849
|
+
if (!checkAuth(req, apiKey)) {
|
|
1850
|
+
respond(401, { error: "Unauthorized" });
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1781
1853
|
const agentE = server.getAgentE();
|
|
1782
1854
|
const latest = agentE.store.latest();
|
|
1783
1855
|
const history = agentE.store.recentHistory(100);
|
|
@@ -1785,6 +1857,10 @@ function createRouteHandler(server) {
|
|
|
1785
1857
|
return;
|
|
1786
1858
|
}
|
|
1787
1859
|
if (path === "/metrics/personas" && method === "GET") {
|
|
1860
|
+
if (!checkAuth(req, apiKey)) {
|
|
1861
|
+
respond(401, { error: "Unauthorized" });
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1788
1864
|
const agentE = server.getAgentE();
|
|
1789
1865
|
const latest = agentE.store.latest();
|
|
1790
1866
|
const dist = latest.personaDistribution || {};
|
|
@@ -1875,6 +1951,10 @@ function createRouteHandler(server) {
|
|
|
1875
1951
|
return;
|
|
1876
1952
|
}
|
|
1877
1953
|
if (path === "/pending" && method === "GET") {
|
|
1954
|
+
if (!checkAuth(req, apiKey)) {
|
|
1955
|
+
respond(401, { error: "Unauthorized" });
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1878
1958
|
const agentE = server.getAgentE();
|
|
1879
1959
|
const pending = agentE.log.query({ result: "skipped_override" });
|
|
1880
1960
|
respond(200, {
|
|
@@ -1904,6 +1984,7 @@ function send(ws, data) {
|
|
|
1904
1984
|
var MAX_WS_PAYLOAD = 1048576;
|
|
1905
1985
|
var MAX_WS_CONNECTIONS = 100;
|
|
1906
1986
|
var MIN_TICK_INTERVAL_MS = 100;
|
|
1987
|
+
var GLOBAL_MIN_TICK_INTERVAL_MS = 50;
|
|
1907
1988
|
function sanitizeJson2(obj) {
|
|
1908
1989
|
if (obj === null || typeof obj !== "object") return obj;
|
|
1909
1990
|
if (Array.isArray(obj)) return obj.map(sanitizeJson2);
|
|
@@ -1916,6 +1997,7 @@ function sanitizeJson2(obj) {
|
|
|
1916
1997
|
}
|
|
1917
1998
|
function createWebSocketHandler(httpServer, server) {
|
|
1918
1999
|
const wss = new WebSocketServer({ server: httpServer, maxPayload: MAX_WS_PAYLOAD });
|
|
2000
|
+
let globalLastTickTime = 0;
|
|
1919
2001
|
const aliveMap = /* @__PURE__ */ new WeakMap();
|
|
1920
2002
|
const heartbeatInterval = setInterval(() => {
|
|
1921
2003
|
for (const ws of wss.clients) {
|
|
@@ -1943,7 +2025,8 @@ function createWebSocketHandler(httpServer, server) {
|
|
|
1943
2025
|
}
|
|
1944
2026
|
if (server.apiKey) {
|
|
1945
2027
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
1946
|
-
const
|
|
2028
|
+
const authHeader = req.headers["authorization"];
|
|
2029
|
+
const token = (authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : void 0) ?? url.searchParams.get("token");
|
|
1947
2030
|
if (!token || token.length !== server.apiKey.length || !timingSafeEqual2(Buffer.from(token), Buffer.from(server.apiKey))) {
|
|
1948
2031
|
ws.close(1008, "Unauthorized");
|
|
1949
2032
|
return;
|
|
@@ -1977,7 +2060,12 @@ function createWebSocketHandler(httpServer, server) {
|
|
|
1977
2060
|
send(ws, { type: "error", message: "Rate limited \u2014 min 100ms between ticks" });
|
|
1978
2061
|
break;
|
|
1979
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
|
+
}
|
|
1980
2067
|
lastTickTime = now;
|
|
2068
|
+
globalLastTickTime = now;
|
|
1981
2069
|
const state = msg["state"];
|
|
1982
2070
|
const events = msg["events"];
|
|
1983
2071
|
if (server.validateState) {
|
|
@@ -1991,9 +2079,10 @@ function createWebSocketHandler(httpServer, server) {
|
|
|
1991
2079
|
}
|
|
1992
2080
|
}
|
|
1993
2081
|
try {
|
|
2082
|
+
const validEvents = Array.isArray(events) ? events.filter(validateEvent) : void 0;
|
|
1994
2083
|
const result = await server.processTick(
|
|
1995
2084
|
state,
|
|
1996
|
-
|
|
2085
|
+
validEvents
|
|
1997
2086
|
);
|
|
1998
2087
|
send(ws, {
|
|
1999
2088
|
type: "tick_result",
|
|
@@ -2013,13 +2102,17 @@ function createWebSocketHandler(httpServer, server) {
|
|
|
2013
2102
|
break;
|
|
2014
2103
|
}
|
|
2015
2104
|
case "event": {
|
|
2016
|
-
const
|
|
2017
|
-
if (
|
|
2018
|
-
server.getAgentE().ingest(event);
|
|
2019
|
-
send(ws, { type: "event_ack" });
|
|
2020
|
-
} else {
|
|
2105
|
+
const rawEvent = msg["event"];
|
|
2106
|
+
if (!rawEvent) {
|
|
2021
2107
|
send(ws, { type: "error", message: 'Missing "event" field' });
|
|
2108
|
+
break;
|
|
2022
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" });
|
|
2023
2116
|
break;
|
|
2024
2117
|
}
|
|
2025
2118
|
case "health": {
|
|
@@ -2084,6 +2177,8 @@ var AgentEServer = class {
|
|
|
2084
2177
|
this.lastState = null;
|
|
2085
2178
|
this.adjustmentQueue = [];
|
|
2086
2179
|
this.alerts = [];
|
|
2180
|
+
/** Serialization lock for processTick — prevents concurrent ticks from corrupting shared state. */
|
|
2181
|
+
this.tickLock = Promise.resolve();
|
|
2087
2182
|
this.startedAt = Date.now();
|
|
2088
2183
|
this.wsHandle = null;
|
|
2089
2184
|
this.port = config.port ?? 3100;
|
|
@@ -2220,36 +2315,46 @@ var AgentEServer = class {
|
|
|
2220
2315
|
* 6. Return response
|
|
2221
2316
|
*/
|
|
2222
2317
|
async processTick(state, events) {
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
this.
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
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
|
+
}
|
|
2229
2332
|
}
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
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
|
+
});
|
|
2239
2348
|
return {
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2349
|
+
adjustments,
|
|
2350
|
+
alerts: [...this.alerts],
|
|
2351
|
+
health: this.agentE.getHealth(),
|
|
2352
|
+
tick: state.tick,
|
|
2353
|
+
decisions
|
|
2244
2354
|
};
|
|
2245
|
-
}
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
alerts: [...this.alerts],
|
|
2249
|
-
health: this.agentE.getHealth(),
|
|
2250
|
-
tick: state.tick,
|
|
2251
|
-
decisions
|
|
2252
|
-
};
|
|
2355
|
+
} finally {
|
|
2356
|
+
unlock();
|
|
2357
|
+
}
|
|
2253
2358
|
}
|
|
2254
2359
|
/**
|
|
2255
2360
|
* Run Observer + Diagnoser on the given state without side effects (no execution).
|
|
@@ -2291,4 +2396,4 @@ var AgentEServer = class {
|
|
|
2291
2396
|
export {
|
|
2292
2397
|
AgentEServer
|
|
2293
2398
|
};
|
|
2294
|
-
//# sourceMappingURL=chunk-
|
|
2399
|
+
//# sourceMappingURL=chunk-3HGNFK4A.mjs.map
|