@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/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
|
|
|
@@ -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.
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''').replace(/\\\\/g,'\'); }
|
|
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:
|
|
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
|
-
|
|
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", () =>
|
|
1530
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1728
|
-
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");
|
|
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
|
|
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
|
-
|
|
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
|
|
1970
|
-
if (
|
|
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
|
-
|
|
2139
|
-
|
|
2140
|
-
this.
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
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
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
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
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2349
|
+
adjustments,
|
|
2350
|
+
alerts: [...this.alerts],
|
|
2351
|
+
health: this.agentE.getHealth(),
|
|
2352
|
+
tick: state.tick,
|
|
2353
|
+
decisions
|
|
2159
2354
|
};
|
|
2160
|
-
}
|
|
2161
|
-
|
|
2162
|
-
|
|
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-
|
|
2399
|
+
//# sourceMappingURL=chunk-3HGNFK4A.mjs.map
|