@sanctuary-framework/mcp-server 1.2.7 → 1.2.8
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 +14 -0
- package/dist/cli.cjs +2399 -55
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2399 -55
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +2134 -116
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +658 -0
- package/dist/index.d.ts +658 -0
- package/dist/index.js +2134 -116
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -17361,6 +17361,125 @@ var init_approval_aggregator_routes = __esm({
|
|
|
17361
17361
|
APPROVAL_INBOX_MAX_LIMIT = 200;
|
|
17362
17362
|
}
|
|
17363
17363
|
});
|
|
17364
|
+
|
|
17365
|
+
// src/sentinel/sentinel-routes.ts
|
|
17366
|
+
function writeJSON5(res, status, payload) {
|
|
17367
|
+
res.writeHead(status, {
|
|
17368
|
+
"Content-Type": "application/json",
|
|
17369
|
+
"Cache-Control": "no-store"
|
|
17370
|
+
});
|
|
17371
|
+
res.end(JSON.stringify(payload));
|
|
17372
|
+
}
|
|
17373
|
+
function isSeverity(value) {
|
|
17374
|
+
return value === "info" || value === "warn" || value === "alert";
|
|
17375
|
+
}
|
|
17376
|
+
function parseLimit3(raw, defaultValue, max) {
|
|
17377
|
+
if (raw === null || raw === "") return defaultValue;
|
|
17378
|
+
const parsed = Number.parseInt(raw, 10);
|
|
17379
|
+
if (Number.isNaN(parsed) || parsed < 0) return defaultValue;
|
|
17380
|
+
return Math.min(parsed, max);
|
|
17381
|
+
}
|
|
17382
|
+
function matchSubscribeRoute(path) {
|
|
17383
|
+
const prefix = `${SENTINEL_API_PREFIX}/`;
|
|
17384
|
+
if (!path.startsWith(prefix)) return null;
|
|
17385
|
+
const rest = path.slice(prefix.length);
|
|
17386
|
+
if (!rest.endsWith("/subscribe")) return null;
|
|
17387
|
+
const sentinelId = rest.slice(0, rest.length - "/subscribe".length);
|
|
17388
|
+
if (sentinelId.length === 0) return null;
|
|
17389
|
+
return { sentinelId: decodeURIComponent(sentinelId) };
|
|
17390
|
+
}
|
|
17391
|
+
async function handleSentinelRoute(deps, req, res) {
|
|
17392
|
+
const host = req.headers.host || "localhost";
|
|
17393
|
+
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
17394
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
17395
|
+
const path = url.pathname;
|
|
17396
|
+
if (path !== SENTINEL_API_PREFIX && !path.startsWith(`${SENTINEL_API_PREFIX}/`)) {
|
|
17397
|
+
return false;
|
|
17398
|
+
}
|
|
17399
|
+
const checkAuth = authMiddleware(deps.authConfig);
|
|
17400
|
+
if (!checkAuth(req, res, url)) return true;
|
|
17401
|
+
const dispatcher = deps.dispatcher;
|
|
17402
|
+
const registry = dispatcher.getRegistry();
|
|
17403
|
+
const findingStore = dispatcher.getFindingStore();
|
|
17404
|
+
try {
|
|
17405
|
+
if (method === "GET" && path === SENTINEL_API_PREFIX) {
|
|
17406
|
+
const catalog = registry.listCatalog();
|
|
17407
|
+
writeJSON5(res, 200, { ok: true, data: { catalog } });
|
|
17408
|
+
return true;
|
|
17409
|
+
}
|
|
17410
|
+
if (method === "GET" && path === `${SENTINEL_API_PREFIX}/subscribed`) {
|
|
17411
|
+
const subscribed = registry.listSubscribed();
|
|
17412
|
+
writeJSON5(res, 200, { ok: true, data: { subscribed } });
|
|
17413
|
+
return true;
|
|
17414
|
+
}
|
|
17415
|
+
if (method === "GET" && path === `${SENTINEL_API_PREFIX}/findings`) {
|
|
17416
|
+
const limit = parseLimit3(
|
|
17417
|
+
url.searchParams.get("limit"),
|
|
17418
|
+
FINDINGS_DEFAULT_LIMIT,
|
|
17419
|
+
FINDINGS_MAX_LIMIT
|
|
17420
|
+
);
|
|
17421
|
+
const since = url.searchParams.get("since") ?? void 0;
|
|
17422
|
+
const severityRaw = url.searchParams.get("severity") ?? void 0;
|
|
17423
|
+
const sentinelIdFilter = url.searchParams.get("sentinel_id") ?? void 0;
|
|
17424
|
+
const agentIdFilter = url.searchParams.get("agent_id") ?? void 0;
|
|
17425
|
+
const severity = severityRaw && isSeverity(severityRaw) ? severityRaw : void 0;
|
|
17426
|
+
const findings = await findingStore.listFindings({
|
|
17427
|
+
limit,
|
|
17428
|
+
...since !== void 0 ? { since } : {},
|
|
17429
|
+
...severity !== void 0 ? { severity } : {},
|
|
17430
|
+
...sentinelIdFilter !== void 0 ? { sentinelId: sentinelIdFilter } : {},
|
|
17431
|
+
...agentIdFilter !== void 0 ? { agentId: agentIdFilter } : {}
|
|
17432
|
+
});
|
|
17433
|
+
writeJSON5(res, 200, { ok: true, data: { findings } });
|
|
17434
|
+
return true;
|
|
17435
|
+
}
|
|
17436
|
+
const subscribeMatch = matchSubscribeRoute(path);
|
|
17437
|
+
if (subscribeMatch) {
|
|
17438
|
+
if (method === "POST") {
|
|
17439
|
+
try {
|
|
17440
|
+
await dispatcher.subscribeSentinel(subscribeMatch.sentinelId);
|
|
17441
|
+
writeJSON5(res, 200, {
|
|
17442
|
+
ok: true,
|
|
17443
|
+
data: { sentinel_id: subscribeMatch.sentinelId, subscribed: true }
|
|
17444
|
+
});
|
|
17445
|
+
} catch (err) {
|
|
17446
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17447
|
+
if (msg.startsWith("sentinel-registry: unknown sentinel")) {
|
|
17448
|
+
writeJSON5(res, 404, { ok: false, error: "not_found" });
|
|
17449
|
+
} else {
|
|
17450
|
+
writeJSON5(res, 500, { ok: false, error: "internal", detail: msg });
|
|
17451
|
+
}
|
|
17452
|
+
}
|
|
17453
|
+
return true;
|
|
17454
|
+
}
|
|
17455
|
+
if (method === "DELETE") {
|
|
17456
|
+
const removed = await dispatcher.unsubscribeSentinel(
|
|
17457
|
+
subscribeMatch.sentinelId
|
|
17458
|
+
);
|
|
17459
|
+
writeJSON5(res, 200, {
|
|
17460
|
+
ok: true,
|
|
17461
|
+
data: { sentinel_id: subscribeMatch.sentinelId, subscribed: false, removed }
|
|
17462
|
+
});
|
|
17463
|
+
return true;
|
|
17464
|
+
}
|
|
17465
|
+
}
|
|
17466
|
+
writeJSON5(res, 404, { ok: false, error: "not_found", path });
|
|
17467
|
+
return true;
|
|
17468
|
+
} catch (err) {
|
|
17469
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17470
|
+
writeJSON5(res, 500, { ok: false, error: "internal", detail: msg });
|
|
17471
|
+
return true;
|
|
17472
|
+
}
|
|
17473
|
+
}
|
|
17474
|
+
var SENTINEL_API_PREFIX, FINDINGS_DEFAULT_LIMIT, FINDINGS_MAX_LIMIT;
|
|
17475
|
+
var init_sentinel_routes = __esm({
|
|
17476
|
+
"src/sentinel/sentinel-routes.ts"() {
|
|
17477
|
+
init_auth_middleware();
|
|
17478
|
+
SENTINEL_API_PREFIX = "/api/sentinels";
|
|
17479
|
+
FINDINGS_DEFAULT_LIMIT = 100;
|
|
17480
|
+
FINDINGS_MAX_LIMIT = 500;
|
|
17481
|
+
}
|
|
17482
|
+
});
|
|
17364
17483
|
function isDashboardViewRoute(method, path) {
|
|
17365
17484
|
if (method !== "GET") return false;
|
|
17366
17485
|
return path === "/" || path === "/dashboard" || path === "/v1.0" || path === "/fortress" || path === "/events";
|
|
@@ -17375,6 +17494,7 @@ var init_dashboard = __esm({
|
|
|
17375
17494
|
init_system_prompt_generator();
|
|
17376
17495
|
init_dispatch();
|
|
17377
17496
|
init_approval_aggregator_routes();
|
|
17497
|
+
init_sentinel_routes();
|
|
17378
17498
|
SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
17379
17499
|
SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
|
|
17380
17500
|
MAX_SESSIONS = 1e3;
|
|
@@ -17442,6 +17562,13 @@ var init_dashboard = __esm({
|
|
|
17442
17562
|
* the operator-facing query / decision surface.
|
|
17443
17563
|
*/
|
|
17444
17564
|
approvalAggregator = null;
|
|
17565
|
+
/**
|
|
17566
|
+
* v1.3 WP-V1.3-1 Phi-1 Sentinel dispatcher. Mounted additively at
|
|
17567
|
+
* `/api/sentinels/*` when set. Sentinel surface is read-only against
|
|
17568
|
+
* the audit log; subscribe/unsubscribe writes flow through the
|
|
17569
|
+
* dispatcher's audited paths.
|
|
17570
|
+
*/
|
|
17571
|
+
sentinelDispatcher = null;
|
|
17445
17572
|
constructor(config) {
|
|
17446
17573
|
this.config = config;
|
|
17447
17574
|
this.authToken = config.auth_token;
|
|
@@ -17501,6 +17628,14 @@ var init_dashboard = __esm({
|
|
|
17501
17628
|
setApprovalAggregator(aggregator) {
|
|
17502
17629
|
this.approvalAggregator = aggregator;
|
|
17503
17630
|
}
|
|
17631
|
+
/**
|
|
17632
|
+
* v1.3 WP-V1.3-1 Phi-1: bind the Sentinel dispatcher. Once set,
|
|
17633
|
+
* requests to `/api/sentinels/*` route through `handleSentinelRoute`.
|
|
17634
|
+
* Pass `null` to detach (used by tests + during shutdown).
|
|
17635
|
+
*/
|
|
17636
|
+
setSentinelDispatcher(dispatcher) {
|
|
17637
|
+
this.sentinelDispatcher = dispatcher;
|
|
17638
|
+
}
|
|
17504
17639
|
/**
|
|
17505
17640
|
* v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
|
|
17506
17641
|
* before the legacy approval route table. Returns true when served.
|
|
@@ -17520,6 +17655,25 @@ var init_dashboard = __esm({
|
|
|
17520
17655
|
res
|
|
17521
17656
|
);
|
|
17522
17657
|
}
|
|
17658
|
+
/**
|
|
17659
|
+
* v1.3 WP-V1.3-1 Phi-1 dispatch entry point. Routes `/api/sentinels/*`
|
|
17660
|
+
* requests through the sentinel router when a dispatcher has been
|
|
17661
|
+
* bound. Returns true when served.
|
|
17662
|
+
*/
|
|
17663
|
+
async dispatchSentinel(req, res) {
|
|
17664
|
+
if (!this.sentinelDispatcher) return false;
|
|
17665
|
+
return handleSentinelRoute(
|
|
17666
|
+
{
|
|
17667
|
+
authConfig: {
|
|
17668
|
+
loopbackAutoAuth: this._autoAuthLocalhost,
|
|
17669
|
+
...this.authToken !== void 0 ? { authToken: this.authToken } : {}
|
|
17670
|
+
},
|
|
17671
|
+
dispatcher: this.sentinelDispatcher
|
|
17672
|
+
},
|
|
17673
|
+
req,
|
|
17674
|
+
res
|
|
17675
|
+
);
|
|
17676
|
+
}
|
|
17523
17677
|
/**
|
|
17524
17678
|
* v1.1 dispatch entry point. Called from `handleRequest` before the
|
|
17525
17679
|
* legacy route table. Returns true when the request was served by v1.1
|
|
@@ -17907,6 +18061,18 @@ var init_dashboard = __esm({
|
|
|
17907
18061
|
});
|
|
17908
18062
|
return;
|
|
17909
18063
|
}
|
|
18064
|
+
if (this.sentinelDispatcher && url.pathname.startsWith(SENTINEL_API_PREFIX)) {
|
|
18065
|
+
this.dispatchSentinel(req, res).then((handled) => {
|
|
18066
|
+
if (handled) return;
|
|
18067
|
+
this.handleLegacyRequest(req, res, url, method);
|
|
18068
|
+
}).catch(() => {
|
|
18069
|
+
if (!res.headersSent) {
|
|
18070
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
18071
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
18072
|
+
}
|
|
18073
|
+
});
|
|
18074
|
+
return;
|
|
18075
|
+
}
|
|
17910
18076
|
if (this.v11Bindings) {
|
|
17911
18077
|
this.dispatchV11(req, res, url, method).then((handled) => {
|
|
17912
18078
|
if (handled) return;
|
|
@@ -21247,47 +21413,1766 @@ var init_aggregator_store = __esm({
|
|
|
21247
21413
|
} catch {
|
|
21248
21414
|
return false;
|
|
21249
21415
|
}
|
|
21250
|
-
return true;
|
|
21416
|
+
return true;
|
|
21417
|
+
}
|
|
21418
|
+
/**
|
|
21419
|
+
* Drop expired payload bundles. Returns the count of bundles pruned.
|
|
21420
|
+
* Caller wires this into the cocoon-unlock initialization path.
|
|
21421
|
+
*/
|
|
21422
|
+
async pruneExpired(now) {
|
|
21423
|
+
const cutoff = (now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
21424
|
+
const entries = await this.storage.list(
|
|
21425
|
+
AGGREGATOR_PAYLOAD_NAMESPACE,
|
|
21426
|
+
AGGREGATOR_PAYLOAD_KEY_PREFIX
|
|
21427
|
+
);
|
|
21428
|
+
let pruned = 0;
|
|
21429
|
+
for (const meta of entries) {
|
|
21430
|
+
const aggregatorId = stripKeyPrefix(meta.key);
|
|
21431
|
+
if (aggregatorId === null) continue;
|
|
21432
|
+
const raw = await this.storage.read(
|
|
21433
|
+
AGGREGATOR_PAYLOAD_NAMESPACE,
|
|
21434
|
+
meta.key
|
|
21435
|
+
);
|
|
21436
|
+
if (!raw) continue;
|
|
21437
|
+
try {
|
|
21438
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
21439
|
+
const aad = stringToBytes(aggregatorId);
|
|
21440
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
21441
|
+
const parsed = JSON.parse(
|
|
21442
|
+
bytesToString(plaintext)
|
|
21443
|
+
);
|
|
21444
|
+
if (parsed.retention_until <= cutoff) {
|
|
21445
|
+
await this.storage.delete(AGGREGATOR_PAYLOAD_NAMESPACE, meta.key);
|
|
21446
|
+
pruned += 1;
|
|
21447
|
+
}
|
|
21448
|
+
} catch {
|
|
21449
|
+
}
|
|
21450
|
+
}
|
|
21451
|
+
return { pruned };
|
|
21452
|
+
}
|
|
21453
|
+
};
|
|
21454
|
+
}
|
|
21455
|
+
});
|
|
21456
|
+
|
|
21457
|
+
// src/sentinel/types.ts
|
|
21458
|
+
function isProxyCallAuditEntry(entry) {
|
|
21459
|
+
return entry.operation.startsWith(
|
|
21460
|
+
SENTINEL_OBSERVED_AUDIT_OPS.PROXY_CALL_PREFIX
|
|
21461
|
+
);
|
|
21462
|
+
}
|
|
21463
|
+
function proxyServerFromAuditEntry(entry) {
|
|
21464
|
+
if (!isProxyCallAuditEntry(entry)) return null;
|
|
21465
|
+
const details = entry.details;
|
|
21466
|
+
if (!details) return null;
|
|
21467
|
+
const server = details["server"];
|
|
21468
|
+
if (typeof server !== "string" || server.length === 0) return null;
|
|
21469
|
+
return server;
|
|
21470
|
+
}
|
|
21471
|
+
var SENTINEL_SUMMARY_MAX_CHARS, SENTINEL_AUDIT_OPS, SENTINEL_OBSERVED_AUDIT_OPS;
|
|
21472
|
+
var init_types3 = __esm({
|
|
21473
|
+
"src/sentinel/types.ts"() {
|
|
21474
|
+
SENTINEL_SUMMARY_MAX_CHARS = 240;
|
|
21475
|
+
SENTINEL_AUDIT_OPS = {
|
|
21476
|
+
SUBSCRIBED: "sentinel_subscribed",
|
|
21477
|
+
UNSUBSCRIBED: "sentinel_unsubscribed",
|
|
21478
|
+
FINDING_EMITTED: "sentinel_finding_emitted",
|
|
21479
|
+
EVALUATION_FAILED: "sentinel_evaluation_failed"
|
|
21480
|
+
};
|
|
21481
|
+
SENTINEL_OBSERVED_AUDIT_OPS = {
|
|
21482
|
+
/** Proxy router emits this on every outbound call (success or failure). */
|
|
21483
|
+
PROXY_CALL_PREFIX: "proxy_call:"
|
|
21484
|
+
};
|
|
21485
|
+
}
|
|
21486
|
+
});
|
|
21487
|
+
|
|
21488
|
+
// src/sentinel/sentinel-finding-store.ts
|
|
21489
|
+
function findingKey(findingId) {
|
|
21490
|
+
return `${SENTINEL_FINDING_KEY_PREFIX}${findingId}`;
|
|
21491
|
+
}
|
|
21492
|
+
function stripKeyPrefix2(key) {
|
|
21493
|
+
if (!key.startsWith(SENTINEL_FINDING_KEY_PREFIX)) return null;
|
|
21494
|
+
return key.slice(SENTINEL_FINDING_KEY_PREFIX.length);
|
|
21495
|
+
}
|
|
21496
|
+
function truncateSummary(summary) {
|
|
21497
|
+
if (summary.length <= SENTINEL_SUMMARY_MAX_CHARS) return summary;
|
|
21498
|
+
return `${summary.slice(0, SENTINEL_SUMMARY_MAX_CHARS - 3)}...`;
|
|
21499
|
+
}
|
|
21500
|
+
var SENTINEL_FINDING_NAMESPACE, SENTINEL_FINDING_KEY_PREFIX, HKDF_INFO2, DEFAULT_SENTINEL_FINDING_RETENTION_DAYS, MAX_FINDING_BYTES, SentinelFindingStore;
|
|
21501
|
+
var init_sentinel_finding_store = __esm({
|
|
21502
|
+
"src/sentinel/sentinel-finding-store.ts"() {
|
|
21503
|
+
init_encryption();
|
|
21504
|
+
init_key_derivation();
|
|
21505
|
+
init_encoding();
|
|
21506
|
+
init_types3();
|
|
21507
|
+
SENTINEL_FINDING_NAMESPACE = "_sentinel_findings";
|
|
21508
|
+
SENTINEL_FINDING_KEY_PREFIX = "finding.";
|
|
21509
|
+
HKDF_INFO2 = "l2-sentinel-finding-v1";
|
|
21510
|
+
DEFAULT_SENTINEL_FINDING_RETENTION_DAYS = 30;
|
|
21511
|
+
MAX_FINDING_BYTES = 256 * 1024;
|
|
21512
|
+
SentinelFindingStore = class {
|
|
21513
|
+
storage;
|
|
21514
|
+
encryptionKey;
|
|
21515
|
+
fortressId;
|
|
21516
|
+
retentionDays;
|
|
21517
|
+
now;
|
|
21518
|
+
constructor(opts) {
|
|
21519
|
+
this.storage = opts.storage;
|
|
21520
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO2);
|
|
21521
|
+
this.fortressId = opts.fortressId;
|
|
21522
|
+
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_SENTINEL_FINDING_RETENTION_DAYS;
|
|
21523
|
+
this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
21524
|
+
}
|
|
21525
|
+
/**
|
|
21526
|
+
* Persist a finding. Truncates the operator-visible summary to
|
|
21527
|
+
* SENTINEL_SUMMARY_MAX_CHARS so the dashboard render stays bounded.
|
|
21528
|
+
* Returns the retention deadline so callers can audit it.
|
|
21529
|
+
*/
|
|
21530
|
+
async saveFinding(finding) {
|
|
21531
|
+
const truncated = {
|
|
21532
|
+
...finding,
|
|
21533
|
+
fortress_id: this.fortressId,
|
|
21534
|
+
summary: truncateSummary(finding.summary)
|
|
21535
|
+
};
|
|
21536
|
+
const retentionMs = this.retentionDays * 24 * 60 * 60 * 1e3;
|
|
21537
|
+
const retentionUntil = new Date(this.now().getTime() + retentionMs);
|
|
21538
|
+
const persisted = {
|
|
21539
|
+
version: 1,
|
|
21540
|
+
finding: truncated,
|
|
21541
|
+
retention_until: retentionUntil.toISOString()
|
|
21542
|
+
};
|
|
21543
|
+
const aad = stringToBytes(finding.finding_id);
|
|
21544
|
+
const plaintext = stringToBytes(JSON.stringify(persisted));
|
|
21545
|
+
const envelope = encrypt(plaintext, this.encryptionKey, aad);
|
|
21546
|
+
await this.storage.write(
|
|
21547
|
+
SENTINEL_FINDING_NAMESPACE,
|
|
21548
|
+
findingKey(finding.finding_id),
|
|
21549
|
+
stringToBytes(JSON.stringify(envelope))
|
|
21550
|
+
);
|
|
21551
|
+
return persisted.retention_until;
|
|
21552
|
+
}
|
|
21553
|
+
/** Load a single finding by id, or null when absent / corrupted. */
|
|
21554
|
+
async loadFinding(findingId) {
|
|
21555
|
+
let raw;
|
|
21556
|
+
try {
|
|
21557
|
+
raw = await this.storage.read(
|
|
21558
|
+
SENTINEL_FINDING_NAMESPACE,
|
|
21559
|
+
findingKey(findingId)
|
|
21560
|
+
);
|
|
21561
|
+
} catch {
|
|
21562
|
+
return null;
|
|
21563
|
+
}
|
|
21564
|
+
if (!raw) return null;
|
|
21565
|
+
if (raw.length > MAX_FINDING_BYTES) return null;
|
|
21566
|
+
return this.decode(findingId, raw);
|
|
21567
|
+
}
|
|
21568
|
+
/**
|
|
21569
|
+
* List findings, newest first. Optional filters: since (ISO 8601),
|
|
21570
|
+
* severity, sentinel_id, agent_id, limit. Default limit 100.
|
|
21571
|
+
*/
|
|
21572
|
+
async listFindings(opts) {
|
|
21573
|
+
const metas = await this.storage.list(
|
|
21574
|
+
SENTINEL_FINDING_NAMESPACE,
|
|
21575
|
+
SENTINEL_FINDING_KEY_PREFIX
|
|
21576
|
+
);
|
|
21577
|
+
const findings = [];
|
|
21578
|
+
for (const meta of metas) {
|
|
21579
|
+
const id = stripKeyPrefix2(meta.key);
|
|
21580
|
+
if (id === null) continue;
|
|
21581
|
+
const raw = await this.storage.read(
|
|
21582
|
+
SENTINEL_FINDING_NAMESPACE,
|
|
21583
|
+
meta.key
|
|
21584
|
+
);
|
|
21585
|
+
if (!raw) continue;
|
|
21586
|
+
if (raw.length > MAX_FINDING_BYTES) continue;
|
|
21587
|
+
const finding = await this.decode(id, raw);
|
|
21588
|
+
if (!finding) continue;
|
|
21589
|
+
if (opts?.since && finding.observed_at < opts.since) continue;
|
|
21590
|
+
if (opts?.severity && finding.severity !== opts.severity) continue;
|
|
21591
|
+
if (opts?.sentinelId && finding.sentinel_id !== opts.sentinelId) continue;
|
|
21592
|
+
if (opts?.agentId && finding.agent_id !== opts.agentId) continue;
|
|
21593
|
+
findings.push(finding);
|
|
21594
|
+
}
|
|
21595
|
+
findings.sort((a, b) => a.observed_at < b.observed_at ? 1 : -1);
|
|
21596
|
+
const limit = opts?.limit ?? 100;
|
|
21597
|
+
return findings.slice(0, limit);
|
|
21598
|
+
}
|
|
21599
|
+
/**
|
|
21600
|
+
* Drop expired findings. Returns the count removed.
|
|
21601
|
+
*/
|
|
21602
|
+
async pruneExpired(now) {
|
|
21603
|
+
const cutoff = (now ?? this.now()).toISOString();
|
|
21604
|
+
const metas = await this.storage.list(
|
|
21605
|
+
SENTINEL_FINDING_NAMESPACE,
|
|
21606
|
+
SENTINEL_FINDING_KEY_PREFIX
|
|
21607
|
+
);
|
|
21608
|
+
let pruned = 0;
|
|
21609
|
+
for (const meta of metas) {
|
|
21610
|
+
const id = stripKeyPrefix2(meta.key);
|
|
21611
|
+
if (id === null) continue;
|
|
21612
|
+
const raw = await this.storage.read(
|
|
21613
|
+
SENTINEL_FINDING_NAMESPACE,
|
|
21614
|
+
meta.key
|
|
21615
|
+
);
|
|
21616
|
+
if (!raw) continue;
|
|
21617
|
+
try {
|
|
21618
|
+
const aad = stringToBytes(id);
|
|
21619
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
21620
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
21621
|
+
const persisted = JSON.parse(
|
|
21622
|
+
bytesToString(plaintext)
|
|
21623
|
+
);
|
|
21624
|
+
if (persisted.retention_until <= cutoff) {
|
|
21625
|
+
await this.storage.delete(SENTINEL_FINDING_NAMESPACE, meta.key);
|
|
21626
|
+
pruned += 1;
|
|
21627
|
+
}
|
|
21628
|
+
} catch {
|
|
21629
|
+
}
|
|
21630
|
+
}
|
|
21631
|
+
return { pruned };
|
|
21632
|
+
}
|
|
21633
|
+
async decode(findingId, raw) {
|
|
21634
|
+
try {
|
|
21635
|
+
const aad = stringToBytes(findingId);
|
|
21636
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
21637
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
21638
|
+
const persisted = JSON.parse(
|
|
21639
|
+
bytesToString(plaintext)
|
|
21640
|
+
);
|
|
21641
|
+
if (persisted.version !== 1) return null;
|
|
21642
|
+
if (persisted.finding.finding_id !== findingId) return null;
|
|
21643
|
+
if (persisted.finding.fortress_id !== this.fortressId) return null;
|
|
21644
|
+
return persisted.finding;
|
|
21645
|
+
} catch {
|
|
21646
|
+
return null;
|
|
21647
|
+
}
|
|
21648
|
+
}
|
|
21649
|
+
};
|
|
21650
|
+
}
|
|
21651
|
+
});
|
|
21652
|
+
|
|
21653
|
+
// src/sentinel/sentinel-registry.ts
|
|
21654
|
+
var SentinelRegistry;
|
|
21655
|
+
var init_sentinel_registry = __esm({
|
|
21656
|
+
"src/sentinel/sentinel-registry.ts"() {
|
|
21657
|
+
SentinelRegistry = class {
|
|
21658
|
+
catalog = /* @__PURE__ */ new Map();
|
|
21659
|
+
subscribed = /* @__PURE__ */ new Map();
|
|
21660
|
+
register(entry) {
|
|
21661
|
+
if (this.catalog.has(entry.sentinelId)) {
|
|
21662
|
+
throw new Error(
|
|
21663
|
+
`sentinel-registry: ${entry.sentinelId} already registered`
|
|
21664
|
+
);
|
|
21665
|
+
}
|
|
21666
|
+
this.catalog.set(entry.sentinelId, entry);
|
|
21667
|
+
}
|
|
21668
|
+
/**
|
|
21669
|
+
* Available sentinels (catalog view). Operator UI lists this so the
|
|
21670
|
+
* operator can pick what to subscribe to.
|
|
21671
|
+
*/
|
|
21672
|
+
listCatalog() {
|
|
21673
|
+
return [...this.catalog.values()].map((entry) => ({
|
|
21674
|
+
sentinelId: entry.sentinelId,
|
|
21675
|
+
description: entry.description
|
|
21676
|
+
}));
|
|
21677
|
+
}
|
|
21678
|
+
/** Currently subscribed sentinel ids. */
|
|
21679
|
+
listSubscribed() {
|
|
21680
|
+
return [...this.subscribed.keys()];
|
|
21681
|
+
}
|
|
21682
|
+
/** Has the fortress opted into this sentinel? */
|
|
21683
|
+
isSubscribed(sentinelId) {
|
|
21684
|
+
return this.subscribed.has(sentinelId);
|
|
21685
|
+
}
|
|
21686
|
+
/**
|
|
21687
|
+
* Subscribe a sentinel to a fortress context. Idempotent: a second
|
|
21688
|
+
* subscribe call on an already-subscribed sentinel returns the
|
|
21689
|
+
* existing instance without re-running `subscribe()`.
|
|
21690
|
+
*/
|
|
21691
|
+
async subscribe(sentinelId, context) {
|
|
21692
|
+
const existing = this.subscribed.get(sentinelId);
|
|
21693
|
+
if (existing) return existing;
|
|
21694
|
+
const entry = this.catalog.get(sentinelId);
|
|
21695
|
+
if (!entry) {
|
|
21696
|
+
throw new Error(`sentinel-registry: unknown sentinel ${sentinelId}`);
|
|
21697
|
+
}
|
|
21698
|
+
const instance = entry.factory();
|
|
21699
|
+
await instance.subscribe(context);
|
|
21700
|
+
this.subscribed.set(sentinelId, instance);
|
|
21701
|
+
return instance;
|
|
21702
|
+
}
|
|
21703
|
+
/**
|
|
21704
|
+
* Unsubscribe. Idempotent: unsubscribing an unsubscribed sentinel
|
|
21705
|
+
* returns false without throwing. Returns true when an active
|
|
21706
|
+
* subscription was torn down.
|
|
21707
|
+
*/
|
|
21708
|
+
async unsubscribe(sentinelId) {
|
|
21709
|
+
const instance = this.subscribed.get(sentinelId);
|
|
21710
|
+
if (!instance) return false;
|
|
21711
|
+
try {
|
|
21712
|
+
await instance.unsubscribe();
|
|
21713
|
+
} finally {
|
|
21714
|
+
this.subscribed.delete(sentinelId);
|
|
21715
|
+
}
|
|
21716
|
+
return true;
|
|
21717
|
+
}
|
|
21718
|
+
/**
|
|
21719
|
+
* Snapshot of subscribed sentinels for the dispatcher's tick path.
|
|
21720
|
+
* Returned as an array so the dispatcher can iterate without holding
|
|
21721
|
+
* the map under modification.
|
|
21722
|
+
*/
|
|
21723
|
+
snapshotSubscribed() {
|
|
21724
|
+
return [...this.subscribed.entries()].map(([sentinelId, sentinel]) => ({
|
|
21725
|
+
sentinelId,
|
|
21726
|
+
sentinel
|
|
21727
|
+
}));
|
|
21728
|
+
}
|
|
21729
|
+
/**
|
|
21730
|
+
* Tear down every subscription. Called by the dispatcher on
|
|
21731
|
+
* fortress-shutdown. Best-effort: a failing unsubscribe does not
|
|
21732
|
+
* abort the rest.
|
|
21733
|
+
*/
|
|
21734
|
+
async unsubscribeAll() {
|
|
21735
|
+
const ids = [...this.subscribed.keys()];
|
|
21736
|
+
for (const id of ids) {
|
|
21737
|
+
try {
|
|
21738
|
+
await this.unsubscribe(id);
|
|
21739
|
+
} catch {
|
|
21740
|
+
}
|
|
21741
|
+
}
|
|
21742
|
+
}
|
|
21743
|
+
};
|
|
21744
|
+
}
|
|
21745
|
+
});
|
|
21746
|
+
var DEFAULT_TICK_INTERVAL_MS, SentinelDispatcher;
|
|
21747
|
+
var init_sentinel_dispatcher = __esm({
|
|
21748
|
+
"src/sentinel/sentinel-dispatcher.ts"() {
|
|
21749
|
+
init_types3();
|
|
21750
|
+
DEFAULT_TICK_INTERVAL_MS = 6e4;
|
|
21751
|
+
SentinelDispatcher = class {
|
|
21752
|
+
registry;
|
|
21753
|
+
findingStore;
|
|
21754
|
+
auditLog;
|
|
21755
|
+
fortressId;
|
|
21756
|
+
identityId;
|
|
21757
|
+
now;
|
|
21758
|
+
tickIntervalMs;
|
|
21759
|
+
listeners = /* @__PURE__ */ new Set();
|
|
21760
|
+
tickTimer = null;
|
|
21761
|
+
tickInFlight = false;
|
|
21762
|
+
constructor(deps) {
|
|
21763
|
+
this.registry = deps.registry;
|
|
21764
|
+
this.findingStore = deps.findingStore;
|
|
21765
|
+
this.auditLog = deps.auditLog;
|
|
21766
|
+
this.fortressId = deps.fortressId;
|
|
21767
|
+
this.identityId = deps.identityId;
|
|
21768
|
+
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
21769
|
+
this.tickIntervalMs = deps.tickIntervalMs ?? DEFAULT_TICK_INTERVAL_MS;
|
|
21770
|
+
}
|
|
21771
|
+
/** Read-only view of the registry. Convenience for route handlers. */
|
|
21772
|
+
getRegistry() {
|
|
21773
|
+
return this.registry;
|
|
21774
|
+
}
|
|
21775
|
+
/** Read-only view of the finding store. Convenience for route handlers. */
|
|
21776
|
+
getFindingStore() {
|
|
21777
|
+
return this.findingStore;
|
|
21778
|
+
}
|
|
21779
|
+
/**
|
|
21780
|
+
* Subscribe an in-process listener. Returns an unsubscribe fn.
|
|
21781
|
+
*/
|
|
21782
|
+
onEvent(listener) {
|
|
21783
|
+
this.listeners.add(listener);
|
|
21784
|
+
return () => this.listeners.delete(listener);
|
|
21785
|
+
}
|
|
21786
|
+
/**
|
|
21787
|
+
* Subscribe a sentinel to this fortress + emit the
|
|
21788
|
+
* `sentinel_subscribed` audit event. Wraps `registry.subscribe()` so
|
|
21789
|
+
* the audit emission lives at the dispatcher boundary (the
|
|
21790
|
+
* fortress-aware site).
|
|
21791
|
+
*/
|
|
21792
|
+
async subscribeSentinel(sentinelId, contextOverrides) {
|
|
21793
|
+
const context = {
|
|
21794
|
+
fortressId: this.fortressId,
|
|
21795
|
+
auditLog: this.auditLog,
|
|
21796
|
+
now: this.now,
|
|
21797
|
+
...contextOverrides ?? {}
|
|
21798
|
+
};
|
|
21799
|
+
const sentinel = await this.registry.subscribe(sentinelId, context);
|
|
21800
|
+
this.auditLog.append(
|
|
21801
|
+
"l2",
|
|
21802
|
+
SENTINEL_AUDIT_OPS.SUBSCRIBED,
|
|
21803
|
+
this.identityId,
|
|
21804
|
+
{ sentinel_id: sentinelId, fortress_id: this.fortressId }
|
|
21805
|
+
);
|
|
21806
|
+
return sentinel;
|
|
21807
|
+
}
|
|
21808
|
+
/**
|
|
21809
|
+
* Unsubscribe + emit `sentinel_unsubscribed`. Returns true when an
|
|
21810
|
+
* active subscription was torn down. Audit fires only on successful
|
|
21811
|
+
* removal.
|
|
21812
|
+
*/
|
|
21813
|
+
async unsubscribeSentinel(sentinelId) {
|
|
21814
|
+
const removed = await this.registry.unsubscribe(sentinelId);
|
|
21815
|
+
if (removed) {
|
|
21816
|
+
this.auditLog.append(
|
|
21817
|
+
"l2",
|
|
21818
|
+
SENTINEL_AUDIT_OPS.UNSUBSCRIBED,
|
|
21819
|
+
this.identityId,
|
|
21820
|
+
{ sentinel_id: sentinelId, fortress_id: this.fortressId }
|
|
21821
|
+
);
|
|
21822
|
+
}
|
|
21823
|
+
return removed;
|
|
21824
|
+
}
|
|
21825
|
+
/**
|
|
21826
|
+
* Run one evaluation pass over every subscribed sentinel. Used by
|
|
21827
|
+
* the auto-tick AND by tests that want a synchronous evaluation
|
|
21828
|
+
* gate. Returns the findings produced this tick (already persisted
|
|
21829
|
+
* + audit-logged + emitted).
|
|
21830
|
+
*/
|
|
21831
|
+
async tick() {
|
|
21832
|
+
if (this.tickInFlight) return [];
|
|
21833
|
+
this.tickInFlight = true;
|
|
21834
|
+
try {
|
|
21835
|
+
const subscribed = this.registry.snapshotSubscribed();
|
|
21836
|
+
const findings = [];
|
|
21837
|
+
for (const { sentinelId, sentinel } of subscribed) {
|
|
21838
|
+
try {
|
|
21839
|
+
const tickFindings = await sentinel.evaluate();
|
|
21840
|
+
for (const finding of tickFindings) {
|
|
21841
|
+
const stamped = await this.routeFinding(sentinelId, finding);
|
|
21842
|
+
findings.push(stamped);
|
|
21843
|
+
}
|
|
21844
|
+
} catch (err) {
|
|
21845
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
21846
|
+
const observedAt = this.now().toISOString();
|
|
21847
|
+
this.auditLog.append(
|
|
21848
|
+
"l2",
|
|
21849
|
+
SENTINEL_AUDIT_OPS.EVALUATION_FAILED,
|
|
21850
|
+
this.identityId,
|
|
21851
|
+
{
|
|
21852
|
+
sentinel_id: sentinelId,
|
|
21853
|
+
fortress_id: this.fortressId,
|
|
21854
|
+
error_message: errorMessage
|
|
21855
|
+
},
|
|
21856
|
+
"failure"
|
|
21857
|
+
);
|
|
21858
|
+
this.emit({
|
|
21859
|
+
type: "evaluation_failed",
|
|
21860
|
+
sentinel_id: sentinelId,
|
|
21861
|
+
error_message: errorMessage,
|
|
21862
|
+
observed_at: observedAt
|
|
21863
|
+
});
|
|
21864
|
+
}
|
|
21865
|
+
}
|
|
21866
|
+
return findings;
|
|
21867
|
+
} finally {
|
|
21868
|
+
this.tickInFlight = false;
|
|
21869
|
+
}
|
|
21870
|
+
}
|
|
21871
|
+
/**
|
|
21872
|
+
* Start the auto-tick loop. No-op when tickIntervalMs is 0 or when
|
|
21873
|
+
* already started. Tests typically leave auto-tick off and call
|
|
21874
|
+
* `tick()` directly.
|
|
21875
|
+
*/
|
|
21876
|
+
start() {
|
|
21877
|
+
if (this.tickTimer !== null) return;
|
|
21878
|
+
if (this.tickIntervalMs <= 0) return;
|
|
21879
|
+
this.tickTimer = setInterval(() => {
|
|
21880
|
+
void this.tick();
|
|
21881
|
+
}, this.tickIntervalMs);
|
|
21882
|
+
if (typeof this.tickTimer.unref === "function") {
|
|
21883
|
+
this.tickTimer.unref();
|
|
21884
|
+
}
|
|
21885
|
+
}
|
|
21886
|
+
/** Stop the auto-tick loop. Idempotent. */
|
|
21887
|
+
stop() {
|
|
21888
|
+
if (this.tickTimer === null) return;
|
|
21889
|
+
clearInterval(this.tickTimer);
|
|
21890
|
+
this.tickTimer = null;
|
|
21891
|
+
}
|
|
21892
|
+
/**
|
|
21893
|
+
* Tear down every subscription + stop the tick loop. Called on
|
|
21894
|
+
* fortress shutdown.
|
|
21895
|
+
*/
|
|
21896
|
+
async dispose() {
|
|
21897
|
+
this.stop();
|
|
21898
|
+
await this.registry.unsubscribeAll();
|
|
21899
|
+
this.listeners.clear();
|
|
21900
|
+
}
|
|
21901
|
+
async routeFinding(sentinelId, raw) {
|
|
21902
|
+
const stamped = {
|
|
21903
|
+
...raw,
|
|
21904
|
+
finding_id: raw.finding_id || crypto.randomUUID(),
|
|
21905
|
+
sentinel_id: sentinelId,
|
|
21906
|
+
fortress_id: this.fortressId,
|
|
21907
|
+
observed_at: raw.observed_at || this.now().toISOString()
|
|
21908
|
+
};
|
|
21909
|
+
await this.findingStore.saveFinding(stamped);
|
|
21910
|
+
this.auditLog.append(
|
|
21911
|
+
"l2",
|
|
21912
|
+
SENTINEL_AUDIT_OPS.FINDING_EMITTED,
|
|
21913
|
+
this.identityId,
|
|
21914
|
+
{
|
|
21915
|
+
sentinel_id: sentinelId,
|
|
21916
|
+
finding_id: stamped.finding_id,
|
|
21917
|
+
severity: stamped.severity,
|
|
21918
|
+
...stamped.agent_id !== void 0 ? { agent_id: stamped.agent_id } : {},
|
|
21919
|
+
evidence_audit_ids: stamped.evidence_audit_ids,
|
|
21920
|
+
fortress_id: this.fortressId
|
|
21921
|
+
}
|
|
21922
|
+
);
|
|
21923
|
+
this.emit({ type: "finding", finding: stamped });
|
|
21924
|
+
return stamped;
|
|
21925
|
+
}
|
|
21926
|
+
emit(event) {
|
|
21927
|
+
for (const listener of this.listeners) {
|
|
21928
|
+
try {
|
|
21929
|
+
listener(event);
|
|
21930
|
+
} catch {
|
|
21931
|
+
}
|
|
21932
|
+
}
|
|
21933
|
+
}
|
|
21934
|
+
};
|
|
21935
|
+
}
|
|
21936
|
+
});
|
|
21937
|
+
|
|
21938
|
+
// src/sentinel/sentinel.ts
|
|
21939
|
+
var Sentinel;
|
|
21940
|
+
var init_sentinel = __esm({
|
|
21941
|
+
"src/sentinel/sentinel.ts"() {
|
|
21942
|
+
Sentinel = class {
|
|
21943
|
+
/**
|
|
21944
|
+
* Bind the sentinel to a fortress context. Called once on
|
|
21945
|
+
* subscribe. Default implementation stores the context on `this`;
|
|
21946
|
+
* sentinels that need additional setup (e.g. priming a baseline
|
|
21947
|
+
* cache) override.
|
|
21948
|
+
*/
|
|
21949
|
+
async subscribe(context) {
|
|
21950
|
+
this.context = context;
|
|
21951
|
+
}
|
|
21952
|
+
/**
|
|
21953
|
+
* Tear down. Default implementation clears the context; subclasses
|
|
21954
|
+
* that hold timers or external handles override.
|
|
21955
|
+
*/
|
|
21956
|
+
async unsubscribe() {
|
|
21957
|
+
this.context = void 0;
|
|
21958
|
+
}
|
|
21959
|
+
context;
|
|
21960
|
+
/** Internal helper: assert subscribed before evaluation. */
|
|
21961
|
+
requireContext() {
|
|
21962
|
+
if (!this.context) {
|
|
21963
|
+
throw new Error(
|
|
21964
|
+
`sentinel ${this.sentinelId}: evaluate() called before subscribe()`
|
|
21965
|
+
);
|
|
21966
|
+
}
|
|
21967
|
+
return this.context;
|
|
21968
|
+
}
|
|
21969
|
+
};
|
|
21970
|
+
}
|
|
21971
|
+
});
|
|
21972
|
+
|
|
21973
|
+
// src/sentinel/sentinels/egress-volume-watcher.ts
|
|
21974
|
+
var EGRESS_VOLUME_SENTINEL_ID, WARN_SIGMA, ALERT_SIGMA, BASELINE_WINDOWS, QUERY_LIMIT, EgressVolumeWatcher;
|
|
21975
|
+
var init_egress_volume_watcher = __esm({
|
|
21976
|
+
"src/sentinel/sentinels/egress-volume-watcher.ts"() {
|
|
21977
|
+
init_sentinel();
|
|
21978
|
+
init_types3();
|
|
21979
|
+
EGRESS_VOLUME_SENTINEL_ID = "egress-volume";
|
|
21980
|
+
WARN_SIGMA = 3;
|
|
21981
|
+
ALERT_SIGMA = 6;
|
|
21982
|
+
BASELINE_WINDOWS = 7;
|
|
21983
|
+
QUERY_LIMIT = 1e4;
|
|
21984
|
+
EgressVolumeWatcher = class extends Sentinel {
|
|
21985
|
+
sentinelId = EGRESS_VOLUME_SENTINEL_ID;
|
|
21986
|
+
description = "Watches outbound proxy-call volume per upstream server. Emits warn/alert when current 24h volume exceeds the rolling 7-day baseline by 3x or 6x standard deviations.";
|
|
21987
|
+
/** Servers we have already produced an `info` baseline-established finding for. */
|
|
21988
|
+
baselineEstablished = /* @__PURE__ */ new Set();
|
|
21989
|
+
async evaluate() {
|
|
21990
|
+
const ctx = this.requireContext();
|
|
21991
|
+
const now = ctx.now();
|
|
21992
|
+
const windowMs = 24 * 60 * 60 * 1e3;
|
|
21993
|
+
const windowSpanMs = (BASELINE_WINDOWS + 1) * windowMs;
|
|
21994
|
+
const sinceIso = new Date(now.getTime() - windowSpanMs).toISOString();
|
|
21995
|
+
const queryResult = await ctx.auditLog.query({
|
|
21996
|
+
since: sinceIso,
|
|
21997
|
+
layer: "l2",
|
|
21998
|
+
limit: QUERY_LIMIT
|
|
21999
|
+
});
|
|
22000
|
+
const entries = queryResult.entries.filter(isProxyCallAuditEntry);
|
|
22001
|
+
const byServer = /* @__PURE__ */ new Map();
|
|
22002
|
+
for (const entry of entries) {
|
|
22003
|
+
const server = proxyServerFromAuditEntry(entry);
|
|
22004
|
+
if (server === null) continue;
|
|
22005
|
+
const auditAge = now.getTime() - new Date(entry.timestamp).getTime();
|
|
22006
|
+
if (auditAge < 0) continue;
|
|
22007
|
+
const windowIdx = Math.floor(auditAge / windowMs);
|
|
22008
|
+
if (windowIdx > BASELINE_WINDOWS) continue;
|
|
22009
|
+
let snapshot = byServer.get(server);
|
|
22010
|
+
if (!snapshot) {
|
|
22011
|
+
snapshot = { windows: [] };
|
|
22012
|
+
for (let i = 0; i <= BASELINE_WINDOWS; i += 1) {
|
|
22013
|
+
snapshot.windows.push({ count: 0, evidence_audit_ids: [] });
|
|
22014
|
+
}
|
|
22015
|
+
byServer.set(server, snapshot);
|
|
22016
|
+
}
|
|
22017
|
+
const bucket = snapshot.windows[windowIdx];
|
|
22018
|
+
bucket.count += 1;
|
|
22019
|
+
if (windowIdx === 0 && bucket.evidence_audit_ids.length < 50) {
|
|
22020
|
+
bucket.evidence_audit_ids.push(`${entry.timestamp}:${entry.operation}`);
|
|
22021
|
+
}
|
|
22022
|
+
}
|
|
22023
|
+
const findings = [];
|
|
22024
|
+
for (const [server, snapshot] of byServer.entries()) {
|
|
22025
|
+
const finding = this.evaluateServer(server, snapshot, now);
|
|
22026
|
+
if (finding) findings.push(finding);
|
|
22027
|
+
}
|
|
22028
|
+
return findings;
|
|
22029
|
+
}
|
|
22030
|
+
/** Reset baseline-established memoization. Tests use this between runs. */
|
|
22031
|
+
resetBaselineMemo() {
|
|
22032
|
+
this.baselineEstablished.clear();
|
|
22033
|
+
}
|
|
22034
|
+
evaluateServer(server, snapshot, now) {
|
|
22035
|
+
const currentWindow = snapshot.windows[0];
|
|
22036
|
+
const baselineWindows = snapshot.windows.slice(1);
|
|
22037
|
+
const populatedBaselineWindows = baselineWindows.filter((w) => w.count > 0).length;
|
|
22038
|
+
if (populatedBaselineWindows < BASELINE_WINDOWS) {
|
|
22039
|
+
if (this.baselineEstablished.has(server)) return null;
|
|
22040
|
+
if (populatedBaselineWindows === 0 && currentWindow.count === 0) {
|
|
22041
|
+
return null;
|
|
22042
|
+
}
|
|
22043
|
+
return null;
|
|
22044
|
+
}
|
|
22045
|
+
const baselineCounts = baselineWindows.map((w) => w.count);
|
|
22046
|
+
const mean = baselineCounts.reduce((sum, c) => sum + c, 0) / baselineCounts.length;
|
|
22047
|
+
const variance = baselineCounts.reduce((sum, c) => sum + (c - mean) ** 2, 0) / baselineCounts.length;
|
|
22048
|
+
const stddev = Math.sqrt(variance);
|
|
22049
|
+
const wasEstablished = this.baselineEstablished.has(server);
|
|
22050
|
+
this.baselineEstablished.add(server);
|
|
22051
|
+
if (!wasEstablished) {
|
|
22052
|
+
return {
|
|
22053
|
+
finding_id: "",
|
|
22054
|
+
sentinel_id: this.sentinelId,
|
|
22055
|
+
severity: "info",
|
|
22056
|
+
summary: `egress-volume baseline established for ${server}: mean ${mean.toFixed(1)} calls/24h, stddev ${stddev.toFixed(1)} (over ${BASELINE_WINDOWS} prior days).`,
|
|
22057
|
+
details: {
|
|
22058
|
+
server,
|
|
22059
|
+
baseline_mean: mean,
|
|
22060
|
+
baseline_stddev: stddev,
|
|
22061
|
+
baseline_windows: baselineCounts,
|
|
22062
|
+
current_count: currentWindow.count
|
|
22063
|
+
},
|
|
22064
|
+
observed_at: now.toISOString(),
|
|
22065
|
+
evidence_audit_ids: [],
|
|
22066
|
+
fortress_id: ""
|
|
22067
|
+
};
|
|
22068
|
+
}
|
|
22069
|
+
const warnThreshold = mean + WARN_SIGMA * stddev;
|
|
22070
|
+
const alertThreshold = mean + ALERT_SIGMA * stddev;
|
|
22071
|
+
if (currentWindow.count > alertThreshold) {
|
|
22072
|
+
return this.buildAnomalyFinding(
|
|
22073
|
+
server,
|
|
22074
|
+
snapshot,
|
|
22075
|
+
mean,
|
|
22076
|
+
stddev,
|
|
22077
|
+
now,
|
|
22078
|
+
"alert",
|
|
22079
|
+
ALERT_SIGMA
|
|
22080
|
+
);
|
|
22081
|
+
}
|
|
22082
|
+
if (currentWindow.count > warnThreshold) {
|
|
22083
|
+
return this.buildAnomalyFinding(
|
|
22084
|
+
server,
|
|
22085
|
+
snapshot,
|
|
22086
|
+
mean,
|
|
22087
|
+
stddev,
|
|
22088
|
+
now,
|
|
22089
|
+
"warn",
|
|
22090
|
+
WARN_SIGMA
|
|
22091
|
+
);
|
|
22092
|
+
}
|
|
22093
|
+
return null;
|
|
22094
|
+
}
|
|
22095
|
+
buildAnomalyFinding(server, snapshot, mean, stddev, now, severity, sigma) {
|
|
22096
|
+
const currentWindow = snapshot.windows[0];
|
|
22097
|
+
const ratio = mean === 0 ? Infinity : currentWindow.count / mean;
|
|
22098
|
+
const ratioStr = Number.isFinite(ratio) ? `${ratio.toFixed(1)}x normal` : "no prior baseline";
|
|
22099
|
+
const summary = `${server} egress is ${ratioStr}: ${currentWindow.count} calls in last 24h, baseline ${mean.toFixed(1)} (stddev ${stddev.toFixed(1)}). Crossed +${sigma} sigma threshold.`;
|
|
22100
|
+
return {
|
|
22101
|
+
finding_id: "",
|
|
22102
|
+
sentinel_id: this.sentinelId,
|
|
22103
|
+
severity,
|
|
22104
|
+
summary,
|
|
22105
|
+
details: {
|
|
22106
|
+
server,
|
|
22107
|
+
current_count: currentWindow.count,
|
|
22108
|
+
baseline_mean: mean,
|
|
22109
|
+
baseline_stddev: stddev,
|
|
22110
|
+
sigma_threshold: sigma,
|
|
22111
|
+
ratio
|
|
22112
|
+
},
|
|
22113
|
+
observed_at: now.toISOString(),
|
|
22114
|
+
evidence_audit_ids: currentWindow.evidence_audit_ids,
|
|
22115
|
+
fortress_id: ""
|
|
22116
|
+
};
|
|
22117
|
+
}
|
|
22118
|
+
};
|
|
22119
|
+
}
|
|
22120
|
+
});
|
|
22121
|
+
|
|
22122
|
+
// src/sentinel/sentinels/cross-agent-chatter-watcher.ts
|
|
22123
|
+
function pairKey(sender, recipient) {
|
|
22124
|
+
return `${sender}|${recipient}`;
|
|
22125
|
+
}
|
|
22126
|
+
function pairFromKey(key) {
|
|
22127
|
+
const idx = key.indexOf("|");
|
|
22128
|
+
return { sender: key.slice(0, idx), recipient: key.slice(idx + 1) };
|
|
22129
|
+
}
|
|
22130
|
+
function computeNewPartners(byPair) {
|
|
22131
|
+
const currentBySource = /* @__PURE__ */ new Map();
|
|
22132
|
+
const priorBySource = /* @__PURE__ */ new Map();
|
|
22133
|
+
for (const [key, snap] of byPair.entries()) {
|
|
22134
|
+
const { sender, recipient } = pairFromKey(key);
|
|
22135
|
+
if (snap.windows[0] && snap.windows[0].count > 0) {
|
|
22136
|
+
let recipMap = currentBySource.get(sender);
|
|
22137
|
+
if (!recipMap) {
|
|
22138
|
+
recipMap = /* @__PURE__ */ new Map();
|
|
22139
|
+
currentBySource.set(sender, recipMap);
|
|
22140
|
+
}
|
|
22141
|
+
recipMap.set(recipient, snap.windows[0].evidence_audit_ids);
|
|
22142
|
+
}
|
|
22143
|
+
const priorTouched = snap.windows.slice(1).some((w) => w.count > 0);
|
|
22144
|
+
if (priorTouched) {
|
|
22145
|
+
let set = priorBySource.get(sender);
|
|
22146
|
+
if (!set) {
|
|
22147
|
+
set = /* @__PURE__ */ new Set();
|
|
22148
|
+
priorBySource.set(sender, set);
|
|
22149
|
+
}
|
|
22150
|
+
set.add(recipient);
|
|
22151
|
+
}
|
|
22152
|
+
}
|
|
22153
|
+
const out = /* @__PURE__ */ new Map();
|
|
22154
|
+
for (const [sender, recipMap] of currentBySource.entries()) {
|
|
22155
|
+
const prior = priorBySource.get(sender) ?? /* @__PURE__ */ new Set();
|
|
22156
|
+
if (prior.size === 0) {
|
|
22157
|
+
continue;
|
|
22158
|
+
}
|
|
22159
|
+
const newPartners = [];
|
|
22160
|
+
const evidence = [];
|
|
22161
|
+
for (const [recipient, recipEvidence] of recipMap.entries()) {
|
|
22162
|
+
if (!prior.has(recipient)) {
|
|
22163
|
+
newPartners.push(recipient);
|
|
22164
|
+
for (const id of recipEvidence) {
|
|
22165
|
+
if (evidence.length < 50) evidence.push(id);
|
|
22166
|
+
}
|
|
22167
|
+
}
|
|
22168
|
+
}
|
|
22169
|
+
if (newPartners.length === 0) continue;
|
|
22170
|
+
newPartners.sort();
|
|
22171
|
+
out.set(sender, {
|
|
22172
|
+
partners: newPartners,
|
|
22173
|
+
priorPartners: [...prior].sort(),
|
|
22174
|
+
evidenceAuditIds: evidence
|
|
22175
|
+
});
|
|
22176
|
+
}
|
|
22177
|
+
return out;
|
|
22178
|
+
}
|
|
22179
|
+
function extractInterAgentEvents(entries) {
|
|
22180
|
+
const out = [];
|
|
22181
|
+
for (const entry of entries) {
|
|
22182
|
+
const op = entry.operation;
|
|
22183
|
+
if (op === HANDOFF_OP) {
|
|
22184
|
+
const details = entry.details;
|
|
22185
|
+
const sender = optionalString(details, "sender_agent_id");
|
|
22186
|
+
const recipient = optionalString(details, "recipient_agent_id");
|
|
22187
|
+
if (!sender || !recipient || sender === recipient) continue;
|
|
22188
|
+
out.push({
|
|
22189
|
+
sender,
|
|
22190
|
+
recipient,
|
|
22191
|
+
timestampMs: Date.parse(entry.timestamp),
|
|
22192
|
+
auditId: `${entry.timestamp}:${entry.operation}`
|
|
22193
|
+
});
|
|
22194
|
+
continue;
|
|
22195
|
+
}
|
|
22196
|
+
if (CROSS_HARNESS_OPS.has(op)) {
|
|
22197
|
+
const details = entry.details;
|
|
22198
|
+
const sender = optionalString(details, "source_harness") ?? optionalString(details, "source_agent_id");
|
|
22199
|
+
if (!sender) continue;
|
|
22200
|
+
out.push({
|
|
22201
|
+
sender,
|
|
22202
|
+
recipient: OPERATOR_PSEUDO_AGENT,
|
|
22203
|
+
timestampMs: Date.parse(entry.timestamp),
|
|
22204
|
+
auditId: `${entry.timestamp}:${entry.operation}`
|
|
22205
|
+
});
|
|
22206
|
+
}
|
|
22207
|
+
}
|
|
22208
|
+
return out;
|
|
22209
|
+
}
|
|
22210
|
+
function optionalString(details, key) {
|
|
22211
|
+
if (!details) return null;
|
|
22212
|
+
const value = details[key];
|
|
22213
|
+
if (typeof value !== "string" || value.length === 0) return null;
|
|
22214
|
+
return value;
|
|
22215
|
+
}
|
|
22216
|
+
var CROSS_AGENT_CHATTER_SENTINEL_ID, WARN_SIGMA2, ALERT_SIGMA2, BASELINE_WINDOWS2, QUERY_LIMIT2, MULTI_NEW_PARTNER_ALERT_THRESHOLD, OPERATOR_PSEUDO_AGENT, HANDOFF_OP, CROSS_HARNESS_OPS, CrossAgentChatterWatcher;
|
|
22217
|
+
var init_cross_agent_chatter_watcher = __esm({
|
|
22218
|
+
"src/sentinel/sentinels/cross-agent-chatter-watcher.ts"() {
|
|
22219
|
+
init_sentinel();
|
|
22220
|
+
CROSS_AGENT_CHATTER_SENTINEL_ID = "cross-agent-chatter";
|
|
22221
|
+
WARN_SIGMA2 = 3;
|
|
22222
|
+
ALERT_SIGMA2 = 6;
|
|
22223
|
+
BASELINE_WINDOWS2 = 7;
|
|
22224
|
+
QUERY_LIMIT2 = 1e4;
|
|
22225
|
+
MULTI_NEW_PARTNER_ALERT_THRESHOLD = 3;
|
|
22226
|
+
OPERATOR_PSEUDO_AGENT = "operator";
|
|
22227
|
+
HANDOFF_OP = "v1.1_local_handoff";
|
|
22228
|
+
CROSS_HARNESS_OPS = /* @__PURE__ */ new Set([
|
|
22229
|
+
"cross_harness_approval_aggregated",
|
|
22230
|
+
"cross_harness_approval_resolved"
|
|
22231
|
+
]);
|
|
22232
|
+
CrossAgentChatterWatcher = class extends Sentinel {
|
|
22233
|
+
sentinelId = CROSS_AGENT_CHATTER_SENTINEL_ID;
|
|
22234
|
+
description = "Watches inter-agent communication patterns. Surfaces per-pair rate spikes (3 or 6 sigma over the rolling 7-day baseline) and new-partner appearances. Escalates to alert when one source agent picks up 3 or more new partners in 24h (lateral-movement shape).";
|
|
22235
|
+
/** Pair keys we have already produced a baseline-established info finding for. */
|
|
22236
|
+
baselineEstablished = /* @__PURE__ */ new Set();
|
|
22237
|
+
async evaluate() {
|
|
22238
|
+
const ctx = this.requireContext();
|
|
22239
|
+
const now = ctx.now();
|
|
22240
|
+
const windowMs = 24 * 60 * 60 * 1e3;
|
|
22241
|
+
const windowSpanMs = (BASELINE_WINDOWS2 + 1) * windowMs;
|
|
22242
|
+
const sinceIso = new Date(now.getTime() - windowSpanMs).toISOString();
|
|
22243
|
+
const queryResult = await ctx.auditLog.query({
|
|
22244
|
+
since: sinceIso,
|
|
22245
|
+
layer: "l2",
|
|
22246
|
+
limit: QUERY_LIMIT2
|
|
22247
|
+
});
|
|
22248
|
+
const events = extractInterAgentEvents(queryResult.entries);
|
|
22249
|
+
const byPair = /* @__PURE__ */ new Map();
|
|
22250
|
+
for (const event of events) {
|
|
22251
|
+
const auditAgeMs = now.getTime() - event.timestampMs;
|
|
22252
|
+
if (auditAgeMs < 0) continue;
|
|
22253
|
+
const windowIdx = Math.floor(auditAgeMs / windowMs);
|
|
22254
|
+
if (windowIdx > BASELINE_WINDOWS2) continue;
|
|
22255
|
+
const key = pairKey(event.sender, event.recipient);
|
|
22256
|
+
let snap = byPair.get(key);
|
|
22257
|
+
if (!snap) {
|
|
22258
|
+
snap = { windows: [] };
|
|
22259
|
+
for (let i = 0; i <= BASELINE_WINDOWS2; i += 1) {
|
|
22260
|
+
snap.windows.push({ count: 0, evidence_audit_ids: [] });
|
|
22261
|
+
}
|
|
22262
|
+
byPair.set(key, snap);
|
|
22263
|
+
}
|
|
22264
|
+
const bucket = snap.windows[windowIdx];
|
|
22265
|
+
bucket.count += 1;
|
|
22266
|
+
if (windowIdx === 0 && bucket.evidence_audit_ids.length < 50) {
|
|
22267
|
+
bucket.evidence_audit_ids.push(event.auditId);
|
|
22268
|
+
}
|
|
22269
|
+
}
|
|
22270
|
+
const findings = [];
|
|
22271
|
+
for (const [key, snap] of byPair.entries()) {
|
|
22272
|
+
const finding = this.evaluatePair(key, snap, now);
|
|
22273
|
+
if (finding) findings.push(finding);
|
|
22274
|
+
}
|
|
22275
|
+
const newPartnersBySource = computeNewPartners(byPair);
|
|
22276
|
+
for (const [source, partners] of newPartnersBySource.entries()) {
|
|
22277
|
+
const finding = this.buildNewPartnerFinding(source, partners, now);
|
|
22278
|
+
if (finding) findings.push(finding);
|
|
22279
|
+
}
|
|
22280
|
+
return findings;
|
|
22281
|
+
}
|
|
22282
|
+
/** Reset baseline-established memoization. Tests use this between runs. */
|
|
22283
|
+
resetBaselineMemo() {
|
|
22284
|
+
this.baselineEstablished.clear();
|
|
22285
|
+
}
|
|
22286
|
+
evaluatePair(key, snap, now) {
|
|
22287
|
+
const currentWindow = snap.windows[0];
|
|
22288
|
+
const baselineWindows = snap.windows.slice(1);
|
|
22289
|
+
const populated = baselineWindows.filter((w) => w.count > 0).length;
|
|
22290
|
+
if (populated < BASELINE_WINDOWS2) {
|
|
22291
|
+
return null;
|
|
22292
|
+
}
|
|
22293
|
+
const counts = baselineWindows.map((w) => w.count);
|
|
22294
|
+
const mean = counts.reduce((s, c) => s + c, 0) / counts.length;
|
|
22295
|
+
const variance = counts.reduce((s, c) => s + (c - mean) ** 2, 0) / counts.length;
|
|
22296
|
+
const stddev = Math.sqrt(variance);
|
|
22297
|
+
const wasEstablished = this.baselineEstablished.has(key);
|
|
22298
|
+
this.baselineEstablished.add(key);
|
|
22299
|
+
if (!wasEstablished) {
|
|
22300
|
+
const pair = pairFromKey(key);
|
|
22301
|
+
return {
|
|
22302
|
+
finding_id: "",
|
|
22303
|
+
sentinel_id: this.sentinelId,
|
|
22304
|
+
severity: "info",
|
|
22305
|
+
summary: `cross-agent-chatter baseline established for ${pair.sender} -> ${pair.recipient}: mean ${mean.toFixed(1)} msgs/24h, stddev ${stddev.toFixed(1)} (over ${BASELINE_WINDOWS2} prior days).`,
|
|
22306
|
+
details: {
|
|
22307
|
+
sender_agent_id: pair.sender,
|
|
22308
|
+
recipient_agent_id: pair.recipient,
|
|
22309
|
+
baseline_mean: mean,
|
|
22310
|
+
baseline_stddev: stddev,
|
|
22311
|
+
baseline_windows: counts,
|
|
22312
|
+
current_count: currentWindow.count
|
|
22313
|
+
},
|
|
22314
|
+
observed_at: now.toISOString(),
|
|
22315
|
+
evidence_audit_ids: [],
|
|
22316
|
+
fortress_id: ""
|
|
22317
|
+
};
|
|
22318
|
+
}
|
|
22319
|
+
const warnThreshold = mean + WARN_SIGMA2 * stddev;
|
|
22320
|
+
const alertThreshold = mean + ALERT_SIGMA2 * stddev;
|
|
22321
|
+
if (currentWindow.count > alertThreshold) {
|
|
22322
|
+
return this.buildRateSpike(key, snap, mean, stddev, now, "alert", ALERT_SIGMA2);
|
|
22323
|
+
}
|
|
22324
|
+
if (currentWindow.count > warnThreshold) {
|
|
22325
|
+
return this.buildRateSpike(key, snap, mean, stddev, now, "warn", WARN_SIGMA2);
|
|
22326
|
+
}
|
|
22327
|
+
return null;
|
|
22328
|
+
}
|
|
22329
|
+
buildRateSpike(key, snap, mean, stddev, now, severity, sigma) {
|
|
22330
|
+
const pair = pairFromKey(key);
|
|
22331
|
+
const cur = snap.windows[0];
|
|
22332
|
+
const ratio = mean === 0 ? Infinity : cur.count / mean;
|
|
22333
|
+
const ratioStr = Number.isFinite(ratio) ? `${ratio.toFixed(1)}x normal` : "no prior baseline";
|
|
22334
|
+
const summary = `${pair.sender} -> ${pair.recipient} chatter rate is ${ratioStr}: ${cur.count} cross-agent messages in last 24h, baseline ${mean.toFixed(1)} (stddev ${stddev.toFixed(1)}). Crossed +${sigma} sigma threshold.`;
|
|
22335
|
+
return {
|
|
22336
|
+
finding_id: "",
|
|
22337
|
+
sentinel_id: this.sentinelId,
|
|
22338
|
+
severity,
|
|
22339
|
+
summary,
|
|
22340
|
+
details: {
|
|
22341
|
+
sender_agent_id: pair.sender,
|
|
22342
|
+
recipient_agent_id: pair.recipient,
|
|
22343
|
+
current_count: cur.count,
|
|
22344
|
+
baseline_mean: mean,
|
|
22345
|
+
baseline_stddev: stddev,
|
|
22346
|
+
sigma_threshold: sigma,
|
|
22347
|
+
ratio
|
|
22348
|
+
},
|
|
22349
|
+
observed_at: now.toISOString(),
|
|
22350
|
+
agent_id: pair.sender,
|
|
22351
|
+
evidence_audit_ids: cur.evidence_audit_ids,
|
|
22352
|
+
fortress_id: ""
|
|
22353
|
+
};
|
|
22354
|
+
}
|
|
22355
|
+
buildNewPartnerFinding(source, info, now) {
|
|
22356
|
+
if (info.partners.length === 0) return null;
|
|
22357
|
+
const severity = info.partners.length >= MULTI_NEW_PARTNER_ALERT_THRESHOLD ? "alert" : "warn";
|
|
22358
|
+
const partnerList = info.partners.join(", ");
|
|
22359
|
+
const baselinePartnerList = info.priorPartners.length === 0 ? "no prior partners" : info.priorPartners.join(", ");
|
|
22360
|
+
const summary = severity === "alert" ? `${source} began communicating with ${info.partners.length} new partners in 24h (${partnerList}). Prior partners: ${baselinePartnerList}. Multi-new-partner pattern crossed alert threshold (>=${MULTI_NEW_PARTNER_ALERT_THRESHOLD}).` : `${source} began communicating with a new partner today: ${partnerList}. Prior partners: ${baselinePartnerList}.`;
|
|
22361
|
+
return {
|
|
22362
|
+
finding_id: "",
|
|
22363
|
+
sentinel_id: this.sentinelId,
|
|
22364
|
+
severity,
|
|
22365
|
+
summary,
|
|
22366
|
+
details: {
|
|
22367
|
+
sender_agent_id: source,
|
|
22368
|
+
new_partners: info.partners,
|
|
22369
|
+
prior_partners: info.priorPartners,
|
|
22370
|
+
new_partner_count: info.partners.length,
|
|
22371
|
+
multi_new_partner_threshold: MULTI_NEW_PARTNER_ALERT_THRESHOLD
|
|
22372
|
+
},
|
|
22373
|
+
observed_at: now.toISOString(),
|
|
22374
|
+
agent_id: source,
|
|
22375
|
+
evidence_audit_ids: info.evidenceAuditIds,
|
|
22376
|
+
fortress_id: ""
|
|
22377
|
+
};
|
|
22378
|
+
}
|
|
22379
|
+
};
|
|
22380
|
+
}
|
|
22381
|
+
});
|
|
22382
|
+
|
|
22383
|
+
// src/sentinel/sentinels/credential-usage-watcher.ts
|
|
22384
|
+
function isCredentialAuditEntry(entry) {
|
|
22385
|
+
if (entry.result !== "success") return false;
|
|
22386
|
+
return entry.operation === BROKER_SECRET_READ_OP || entry.operation === BROKER_TOKEN_ISSUED_OP;
|
|
22387
|
+
}
|
|
22388
|
+
function extractAgentId(entry) {
|
|
22389
|
+
const details = entry.details;
|
|
22390
|
+
if (!details) return null;
|
|
22391
|
+
const agent = details["agent"];
|
|
22392
|
+
return typeof agent === "string" && agent.length > 0 ? agent : null;
|
|
22393
|
+
}
|
|
22394
|
+
function extractSecretId(entry) {
|
|
22395
|
+
const details = entry.details;
|
|
22396
|
+
if (!details) return null;
|
|
22397
|
+
const secret = details["secret"];
|
|
22398
|
+
return typeof secret === "string" && secret.length > 0 ? secret : null;
|
|
22399
|
+
}
|
|
22400
|
+
function enumerateUnorderedPairs(secrets) {
|
|
22401
|
+
const out = /* @__PURE__ */ new Set();
|
|
22402
|
+
const arr = [...secrets].sort();
|
|
22403
|
+
for (let i = 0; i < arr.length; i += 1) {
|
|
22404
|
+
for (let j = i + 1; j < arr.length; j += 1) {
|
|
22405
|
+
out.add(`${arr[i]}\0${arr[j]}`);
|
|
22406
|
+
}
|
|
22407
|
+
}
|
|
22408
|
+
return out;
|
|
22409
|
+
}
|
|
22410
|
+
function buildNewPairSummary(agentId, newPairs) {
|
|
22411
|
+
const first = newPairs[0];
|
|
22412
|
+
if (newPairs.length === 1) {
|
|
22413
|
+
return `${agentId} agent used ${first[0]} and ${first[1]} together for the first time today. This combination does not appear in historical sessions.`;
|
|
22414
|
+
}
|
|
22415
|
+
return `${agentId} agent introduced ${newPairs.length} unfamiliar credential pairs in the last 24h. First: ${first[0]} + ${first[1]}. This pattern is new for this agent.`;
|
|
22416
|
+
}
|
|
22417
|
+
var CREDENTIAL_USAGE_SENTINEL_ID, WARN_SIGMA3, ALERT_SIGMA3, NEW_PAIR_ALERT_COUNT, BASELINE_WINDOWS3, QUERY_LIMIT3, BROKER_SECRET_READ_OP, BROKER_TOKEN_ISSUED_OP, CredentialUsageWatcher;
|
|
22418
|
+
var init_credential_usage_watcher = __esm({
|
|
22419
|
+
"src/sentinel/sentinels/credential-usage-watcher.ts"() {
|
|
22420
|
+
init_sentinel();
|
|
22421
|
+
CREDENTIAL_USAGE_SENTINEL_ID = "credential-usage";
|
|
22422
|
+
WARN_SIGMA3 = 3;
|
|
22423
|
+
ALERT_SIGMA3 = 6;
|
|
22424
|
+
NEW_PAIR_ALERT_COUNT = 3;
|
|
22425
|
+
BASELINE_WINDOWS3 = 7;
|
|
22426
|
+
QUERY_LIMIT3 = 2e4;
|
|
22427
|
+
BROKER_SECRET_READ_OP = "broker_secret_read";
|
|
22428
|
+
BROKER_TOKEN_ISSUED_OP = "broker_token_issued";
|
|
22429
|
+
CredentialUsageWatcher = class extends Sentinel {
|
|
22430
|
+
sentinelId = CREDENTIAL_USAGE_SENTINEL_ID;
|
|
22431
|
+
description = "Watches per-agent credential reads. Emits warn/alert when a specific (agent, secret) usage rate exceeds the rolling 7-day baseline, or when an agent uses an unfamiliar combination of secrets together in one 24h window.";
|
|
22432
|
+
/**
|
|
22433
|
+
* Memoization of (agent, secret) pairs whose baseline has been
|
|
22434
|
+
* established. Same shape Phi-1 uses to avoid re-emitting `info`
|
|
22435
|
+
* findings on every tick after a baseline first establishes.
|
|
22436
|
+
*
|
|
22437
|
+
* Phi-2 deliberately does NOT emit `info` findings: per-pair
|
|
22438
|
+
* baselines on a busy fortress would be too noisy. The memo is
|
|
22439
|
+
* kept here for parity with Phi-1's reset hook so tests can clear
|
|
22440
|
+
* state between runs.
|
|
22441
|
+
*/
|
|
22442
|
+
baselineEstablished = /* @__PURE__ */ new Set();
|
|
22443
|
+
/** Reset memoization. Tests use this between runs. */
|
|
22444
|
+
resetBaselineMemo() {
|
|
22445
|
+
this.baselineEstablished.clear();
|
|
22446
|
+
}
|
|
22447
|
+
async evaluate() {
|
|
22448
|
+
const ctx = this.requireContext();
|
|
22449
|
+
const now = ctx.now();
|
|
22450
|
+
const windowMs = 24 * 60 * 60 * 1e3;
|
|
22451
|
+
const windowSpanMs = (BASELINE_WINDOWS3 + 1) * windowMs;
|
|
22452
|
+
const sinceIso = new Date(now.getTime() - windowSpanMs).toISOString();
|
|
22453
|
+
const queryResult = await ctx.auditLog.query({
|
|
22454
|
+
since: sinceIso,
|
|
22455
|
+
layer: "l3",
|
|
22456
|
+
limit: QUERY_LIMIT3
|
|
22457
|
+
});
|
|
22458
|
+
const entries = queryResult.entries.filter(isCredentialAuditEntry);
|
|
22459
|
+
const byPair = /* @__PURE__ */ new Map();
|
|
22460
|
+
const byAgent = /* @__PURE__ */ new Map();
|
|
22461
|
+
for (const entry of entries) {
|
|
22462
|
+
const agentId = extractAgentId(entry);
|
|
22463
|
+
const secretId = extractSecretId(entry);
|
|
22464
|
+
if (agentId === null || secretId === null) continue;
|
|
22465
|
+
const auditAge = now.getTime() - new Date(entry.timestamp).getTime();
|
|
22466
|
+
if (auditAge < 0) continue;
|
|
22467
|
+
const windowIdx = Math.floor(auditAge / windowMs);
|
|
22468
|
+
if (windowIdx > BASELINE_WINDOWS3) continue;
|
|
22469
|
+
const pairKey2 = `${agentId}\0${secretId}`;
|
|
22470
|
+
let pairSnapshot = byPair.get(pairKey2);
|
|
22471
|
+
if (!pairSnapshot) {
|
|
22472
|
+
pairSnapshot = {
|
|
22473
|
+
windows: Array.from({ length: BASELINE_WINDOWS3 + 1 }, () => ({
|
|
22474
|
+
count: 0,
|
|
22475
|
+
evidence_audit_ids: []
|
|
22476
|
+
}))
|
|
22477
|
+
};
|
|
22478
|
+
byPair.set(pairKey2, pairSnapshot);
|
|
22479
|
+
}
|
|
22480
|
+
const bucket = pairSnapshot.windows[windowIdx];
|
|
22481
|
+
bucket.count += 1;
|
|
22482
|
+
if (windowIdx === 0 && bucket.evidence_audit_ids.length < 50) {
|
|
22483
|
+
bucket.evidence_audit_ids.push(
|
|
22484
|
+
`${entry.timestamp}:${entry.operation}`
|
|
22485
|
+
);
|
|
22486
|
+
}
|
|
22487
|
+
let agentState = byAgent.get(agentId);
|
|
22488
|
+
if (!agentState) {
|
|
22489
|
+
agentState = {
|
|
22490
|
+
currentSecrets: /* @__PURE__ */ new Set(),
|
|
22491
|
+
currentEvidence: [],
|
|
22492
|
+
baselineSecretsByWindow: Array.from(
|
|
22493
|
+
{ length: BASELINE_WINDOWS3 },
|
|
22494
|
+
() => /* @__PURE__ */ new Set()
|
|
22495
|
+
),
|
|
22496
|
+
baselinePopulatedWindows: 0
|
|
22497
|
+
};
|
|
22498
|
+
byAgent.set(agentId, agentState);
|
|
22499
|
+
}
|
|
22500
|
+
if (windowIdx === 0) {
|
|
22501
|
+
agentState.currentSecrets.add(secretId);
|
|
22502
|
+
if (agentState.currentEvidence.length < 50) {
|
|
22503
|
+
agentState.currentEvidence.push(
|
|
22504
|
+
`${entry.timestamp}:${entry.operation}`
|
|
22505
|
+
);
|
|
22506
|
+
}
|
|
22507
|
+
} else {
|
|
22508
|
+
const baselineIdx = windowIdx - 1;
|
|
22509
|
+
agentState.baselineSecretsByWindow[baselineIdx].add(secretId);
|
|
22510
|
+
}
|
|
22511
|
+
}
|
|
22512
|
+
const findings = [];
|
|
22513
|
+
for (const [pairKey2, snapshot] of byPair.entries()) {
|
|
22514
|
+
const [agentId, secretId] = pairKey2.split("\0");
|
|
22515
|
+
const finding = this.evaluateRateSpike(
|
|
22516
|
+
agentId,
|
|
22517
|
+
secretId,
|
|
22518
|
+
snapshot,
|
|
22519
|
+
now
|
|
22520
|
+
);
|
|
22521
|
+
if (finding) findings.push(finding);
|
|
22522
|
+
}
|
|
22523
|
+
for (const [agentId, agentState] of byAgent.entries()) {
|
|
22524
|
+
agentState.baselinePopulatedWindows = agentState.baselineSecretsByWindow.filter((s) => s.size > 0).length;
|
|
22525
|
+
const finding = this.evaluateNewPairs(agentId, agentState, now);
|
|
22526
|
+
if (finding) findings.push(finding);
|
|
22527
|
+
}
|
|
22528
|
+
return findings;
|
|
22529
|
+
}
|
|
22530
|
+
evaluateRateSpike(agentId, secretId, snapshot, now) {
|
|
22531
|
+
const currentWindow = snapshot.windows[0];
|
|
22532
|
+
const baselineWindows = snapshot.windows.slice(1);
|
|
22533
|
+
const populated = baselineWindows.filter((w) => w.count > 0).length;
|
|
22534
|
+
if (populated < BASELINE_WINDOWS3) {
|
|
22535
|
+
return null;
|
|
22536
|
+
}
|
|
22537
|
+
const counts = baselineWindows.map((w) => w.count);
|
|
22538
|
+
const mean = counts.reduce((sum, c) => sum + c, 0) / counts.length;
|
|
22539
|
+
const variance = counts.reduce((sum, c) => sum + (c - mean) ** 2, 0) / counts.length;
|
|
22540
|
+
const stddev = Math.sqrt(variance);
|
|
22541
|
+
const pairKey2 = `${agentId}\0${secretId}`;
|
|
22542
|
+
this.baselineEstablished.add(pairKey2);
|
|
22543
|
+
const warnThreshold = mean + WARN_SIGMA3 * stddev;
|
|
22544
|
+
const alertThreshold = mean + ALERT_SIGMA3 * stddev;
|
|
22545
|
+
if (currentWindow.count > alertThreshold) {
|
|
22546
|
+
return this.buildRateFinding(
|
|
22547
|
+
agentId,
|
|
22548
|
+
secretId,
|
|
22549
|
+
currentWindow,
|
|
22550
|
+
mean,
|
|
22551
|
+
stddev,
|
|
22552
|
+
now,
|
|
22553
|
+
"alert",
|
|
22554
|
+
ALERT_SIGMA3
|
|
22555
|
+
);
|
|
22556
|
+
}
|
|
22557
|
+
if (currentWindow.count > warnThreshold) {
|
|
22558
|
+
return this.buildRateFinding(
|
|
22559
|
+
agentId,
|
|
22560
|
+
secretId,
|
|
22561
|
+
currentWindow,
|
|
22562
|
+
mean,
|
|
22563
|
+
stddev,
|
|
22564
|
+
now,
|
|
22565
|
+
"warn",
|
|
22566
|
+
WARN_SIGMA3
|
|
22567
|
+
);
|
|
22568
|
+
}
|
|
22569
|
+
return null;
|
|
22570
|
+
}
|
|
22571
|
+
buildRateFinding(agentId, secretId, currentWindow, mean, stddev, now, severity, sigma) {
|
|
22572
|
+
const ratio = mean === 0 ? Number.POSITIVE_INFINITY : currentWindow.count / mean;
|
|
22573
|
+
const ratioStr = Number.isFinite(ratio) ? `${ratio.toFixed(1)}x normal` : "no prior baseline";
|
|
22574
|
+
const summary = `${agentId} agent used ${secretId} ${currentWindow.count} times in last 24h, baseline ${mean.toFixed(1)} +/- ${stddev.toFixed(1)} (${ratioStr}). Crossed +${sigma} sigma threshold.`;
|
|
22575
|
+
return {
|
|
22576
|
+
finding_id: "",
|
|
22577
|
+
sentinel_id: this.sentinelId,
|
|
22578
|
+
severity,
|
|
22579
|
+
agent_id: agentId,
|
|
22580
|
+
summary,
|
|
22581
|
+
details: {
|
|
22582
|
+
agent_id: agentId,
|
|
22583
|
+
secret_id: secretId,
|
|
22584
|
+
current_count: currentWindow.count,
|
|
22585
|
+
baseline_mean: mean,
|
|
22586
|
+
baseline_stddev: stddev,
|
|
22587
|
+
sigma_threshold: sigma,
|
|
22588
|
+
ratio: Number.isFinite(ratio) ? ratio : null
|
|
22589
|
+
},
|
|
22590
|
+
observed_at: now.toISOString(),
|
|
22591
|
+
evidence_audit_ids: currentWindow.evidence_audit_ids,
|
|
22592
|
+
fortress_id: ""
|
|
22593
|
+
};
|
|
22594
|
+
}
|
|
22595
|
+
evaluateNewPairs(agentId, state, now) {
|
|
22596
|
+
if (state.baselinePopulatedWindows < BASELINE_WINDOWS3) {
|
|
22597
|
+
return null;
|
|
22598
|
+
}
|
|
22599
|
+
if (state.currentSecrets.size < 2) return null;
|
|
22600
|
+
const currentPairs = enumerateUnorderedPairs(state.currentSecrets);
|
|
22601
|
+
const historicalPairs = /* @__PURE__ */ new Set();
|
|
22602
|
+
for (const secretSet of state.baselineSecretsByWindow) {
|
|
22603
|
+
for (const pair of enumerateUnorderedPairs(secretSet)) {
|
|
22604
|
+
historicalPairs.add(pair);
|
|
22605
|
+
}
|
|
22606
|
+
}
|
|
22607
|
+
const newPairs = [];
|
|
22608
|
+
for (const pair of currentPairs) {
|
|
22609
|
+
if (historicalPairs.has(pair)) continue;
|
|
22610
|
+
const [a, b] = pair.split("\0");
|
|
22611
|
+
newPairs.push([a, b]);
|
|
22612
|
+
}
|
|
22613
|
+
if (newPairs.length === 0) return null;
|
|
22614
|
+
const severity = newPairs.length >= NEW_PAIR_ALERT_COUNT ? "alert" : "warn";
|
|
22615
|
+
const summary = buildNewPairSummary(agentId, newPairs);
|
|
22616
|
+
return {
|
|
22617
|
+
finding_id: "",
|
|
22618
|
+
sentinel_id: this.sentinelId,
|
|
22619
|
+
severity,
|
|
22620
|
+
agent_id: agentId,
|
|
22621
|
+
summary,
|
|
22622
|
+
details: {
|
|
22623
|
+
agent_id: agentId,
|
|
22624
|
+
new_pairs: newPairs,
|
|
22625
|
+
new_pair_count: newPairs.length,
|
|
22626
|
+
historical_pair_count: historicalPairs.size,
|
|
22627
|
+
current_pair_count: currentPairs.size
|
|
22628
|
+
},
|
|
22629
|
+
observed_at: now.toISOString(),
|
|
22630
|
+
evidence_audit_ids: state.currentEvidence,
|
|
22631
|
+
fortress_id: ""
|
|
22632
|
+
};
|
|
22633
|
+
}
|
|
22634
|
+
};
|
|
22635
|
+
}
|
|
22636
|
+
});
|
|
22637
|
+
|
|
22638
|
+
// src/sentinel/sentinels/suspicious-tool-call-detector.ts
|
|
22639
|
+
function countTruncatedValues(args) {
|
|
22640
|
+
let n = 0;
|
|
22641
|
+
for (const v of Object.values(args)) {
|
|
22642
|
+
if (typeof v === "string" && v.endsWith("...")) n += 1;
|
|
22643
|
+
}
|
|
22644
|
+
return n;
|
|
22645
|
+
}
|
|
22646
|
+
function countUrlEncoded(value) {
|
|
22647
|
+
const matches = value.match(/%[0-9a-fA-F]{2}/g);
|
|
22648
|
+
return matches ? matches.length : 0;
|
|
22649
|
+
}
|
|
22650
|
+
function longestBase64Run(value) {
|
|
22651
|
+
const matches = value.match(/[A-Za-z0-9+/=]{40,}/g);
|
|
22652
|
+
if (!matches) return 0;
|
|
22653
|
+
return matches.reduce((max, m) => m.length > max ? m.length : max, 0);
|
|
22654
|
+
}
|
|
22655
|
+
function extractArgsSummary(details) {
|
|
22656
|
+
if (!details) return {};
|
|
22657
|
+
const summary = details["args_summary"];
|
|
22658
|
+
if (summary && typeof summary === "object" && !Array.isArray(summary)) {
|
|
22659
|
+
return summary;
|
|
22660
|
+
}
|
|
22661
|
+
return {};
|
|
22662
|
+
}
|
|
22663
|
+
function truncateSummary2(s) {
|
|
22664
|
+
return s.length > 240 ? s.slice(0, 237) + "..." : s;
|
|
22665
|
+
}
|
|
22666
|
+
var SUSPICIOUS_TOOL_CALL_SENTINEL_ID, GATE_PREFIXES, WARN_SIGMA4, ALERT_SIGMA4, BASELINE_WINDOWS4, ALERT_NOVEL_COMBINATIONS, TASK_WINDOW_MS, TRUNCATION_WARN_THRESHOLD, QUERY_LIMIT4, SIGNATURE_PATTERNS, SuspiciousToolCallDetector;
|
|
22667
|
+
var init_suspicious_tool_call_detector = __esm({
|
|
22668
|
+
"src/sentinel/sentinels/suspicious-tool-call-detector.ts"() {
|
|
22669
|
+
init_sentinel();
|
|
22670
|
+
SUSPICIOUS_TOOL_CALL_SENTINEL_ID = "suspicious-tool-call";
|
|
22671
|
+
GATE_PREFIXES = [
|
|
22672
|
+
"gate_allow:",
|
|
22673
|
+
"gate_allow_proxy:",
|
|
22674
|
+
"gate_deny:",
|
|
22675
|
+
"gate_unclassified:"
|
|
22676
|
+
];
|
|
22677
|
+
WARN_SIGMA4 = 3;
|
|
22678
|
+
ALERT_SIGMA4 = 6;
|
|
22679
|
+
BASELINE_WINDOWS4 = 7;
|
|
22680
|
+
ALERT_NOVEL_COMBINATIONS = 2;
|
|
22681
|
+
TASK_WINDOW_MS = 60 * 60 * 1e3;
|
|
22682
|
+
TRUNCATION_WARN_THRESHOLD = 5;
|
|
22683
|
+
QUERY_LIMIT4 = 1e4;
|
|
22684
|
+
SIGNATURE_PATTERNS = {
|
|
22685
|
+
/** >=5 percent-encoded sequences in a single visible value. */
|
|
22686
|
+
urlEncodedThreshold: 5,
|
|
22687
|
+
/** >=40 contiguous base64 chars in a single visible value. */
|
|
22688
|
+
base64MinRun: 40,
|
|
22689
|
+
/** Shell metacharacter set. */
|
|
22690
|
+
shellMetacharRegex: /(?:&&|\|\||;|\$\(|`|\|\s)/
|
|
22691
|
+
};
|
|
22692
|
+
SuspiciousToolCallDetector = class extends Sentinel {
|
|
22693
|
+
sentinelId = SUSPICIOUS_TOOL_CALL_SENTINEL_ID;
|
|
22694
|
+
description = "Surfaces tool calls whose argument shape, call frequency, or permission combination looks unusual for the fortress's recent history. Rule-based heuristics with optional LLM-assist on ambiguous matches.";
|
|
22695
|
+
/** Servers we have already produced an `info` baseline-established finding for. */
|
|
22696
|
+
baselineEstablished = /* @__PURE__ */ new Set();
|
|
22697
|
+
/** Memoized known novel-combination keys (sorted-tools-csv). */
|
|
22698
|
+
knownCombinations = /* @__PURE__ */ new Set();
|
|
22699
|
+
/** Tasks observed where a novel combination already produced a finding. */
|
|
22700
|
+
novelCombinationsReported = /* @__PURE__ */ new Set();
|
|
22701
|
+
async evaluate() {
|
|
22702
|
+
const ctx = this.requireContext();
|
|
22703
|
+
const now = ctx.now();
|
|
22704
|
+
const dayMs = 24 * 60 * 60 * 1e3;
|
|
22705
|
+
const windowSpanMs = (BASELINE_WINDOWS4 + 1) * dayMs;
|
|
22706
|
+
const sinceIso = new Date(now.getTime() - windowSpanMs).toISOString();
|
|
22707
|
+
const queryResult = await ctx.auditLog.query({
|
|
22708
|
+
since: sinceIso,
|
|
22709
|
+
layer: "l2",
|
|
22710
|
+
limit: QUERY_LIMIT4
|
|
22711
|
+
});
|
|
22712
|
+
const observations = [];
|
|
22713
|
+
for (const entry of queryResult.entries) {
|
|
22714
|
+
const obs = this.observationFromEntry(entry);
|
|
22715
|
+
if (obs && obs.ts <= now.getTime()) observations.push(obs);
|
|
22716
|
+
}
|
|
22717
|
+
if (observations.length === 0) return [];
|
|
22718
|
+
const findings = [];
|
|
22719
|
+
const layerAFindings = await this.runLayerA(observations, now, ctx);
|
|
22720
|
+
findings.push(...layerAFindings);
|
|
22721
|
+
const layerBFindings = this.runLayerB(observations, now);
|
|
22722
|
+
findings.push(...layerBFindings);
|
|
22723
|
+
const layerCFindings = this.runLayerC(observations, now);
|
|
22724
|
+
findings.push(...layerCFindings);
|
|
22725
|
+
return findings;
|
|
22726
|
+
}
|
|
22727
|
+
/** Reset memoization between test runs. Mirrors Phi-1's reset hook. */
|
|
22728
|
+
resetMemo() {
|
|
22729
|
+
this.baselineEstablished.clear();
|
|
22730
|
+
this.knownCombinations.clear();
|
|
22731
|
+
this.novelCombinationsReported.clear();
|
|
22732
|
+
}
|
|
22733
|
+
// ── Layer A ───────────────────────────────────────────────────────
|
|
22734
|
+
async runLayerA(observations, now, ctx) {
|
|
22735
|
+
const dayMs = 24 * 60 * 60 * 1e3;
|
|
22736
|
+
const recent = observations.filter(
|
|
22737
|
+
(o) => now.getTime() - o.ts <= dayMs
|
|
22738
|
+
);
|
|
22739
|
+
if (recent.length === 0) return [];
|
|
22740
|
+
const findings = [];
|
|
22741
|
+
const historical = observations.filter(
|
|
22742
|
+
(o) => now.getTime() - o.ts > dayMs
|
|
22743
|
+
);
|
|
22744
|
+
const perTool = /* @__PURE__ */ new Map();
|
|
22745
|
+
const ensureTool = (tool) => {
|
|
22746
|
+
let w = perTool.get(tool);
|
|
22747
|
+
if (!w) {
|
|
22748
|
+
w = {
|
|
22749
|
+
windows: Array.from({ length: BASELINE_WINDOWS4 + 1 }, () => ({
|
|
22750
|
+
count: 0,
|
|
22751
|
+
evidenceIds: []
|
|
22752
|
+
})),
|
|
22753
|
+
knownSignatures: /* @__PURE__ */ new Set()
|
|
22754
|
+
};
|
|
22755
|
+
perTool.set(tool, w);
|
|
22756
|
+
}
|
|
22757
|
+
return w;
|
|
22758
|
+
};
|
|
22759
|
+
for (const o of historical) {
|
|
22760
|
+
ensureTool(o.tool).knownSignatures.add(this.signatureOf(o.argsSummary));
|
|
22761
|
+
}
|
|
22762
|
+
const classify = this.classifyHandle(ctx);
|
|
22763
|
+
for (const obs of recent) {
|
|
22764
|
+
const matches = this.matchSignatures(obs);
|
|
22765
|
+
if (matches.length === 0) continue;
|
|
22766
|
+
const ambiguous = matches.every((m) => m === "base64_chunk");
|
|
22767
|
+
if (ambiguous && classify) {
|
|
22768
|
+
const verdict = await this.consultClassifier(classify, obs);
|
|
22769
|
+
if (verdict !== "suspicious") continue;
|
|
22770
|
+
}
|
|
22771
|
+
findings.push(
|
|
22772
|
+
this.buildLayerAFinding(obs, matches, now, classify ? "llm-assist" : "rule-based")
|
|
22773
|
+
);
|
|
22774
|
+
}
|
|
22775
|
+
for (const obs of recent) {
|
|
22776
|
+
const tool = obs.tool;
|
|
22777
|
+
const sig = this.signatureOf(obs.argsSummary);
|
|
22778
|
+
const known = perTool.get(tool)?.knownSignatures;
|
|
22779
|
+
if (known && known.size > 0 && !known.has(sig)) {
|
|
22780
|
+
findings.push(
|
|
22781
|
+
this.buildNovelSignatureFinding(obs, sig, now)
|
|
22782
|
+
);
|
|
22783
|
+
}
|
|
22784
|
+
}
|
|
22785
|
+
return findings;
|
|
22786
|
+
}
|
|
22787
|
+
matchSignatures(obs) {
|
|
22788
|
+
const out = [];
|
|
22789
|
+
const truncCount = countTruncatedValues(obs.argsSummary);
|
|
22790
|
+
if (truncCount >= TRUNCATION_WARN_THRESHOLD) out.push("truncation_burst");
|
|
22791
|
+
let urlBlob = false;
|
|
22792
|
+
let base64Blob = false;
|
|
22793
|
+
let shellChars = false;
|
|
22794
|
+
for (const value of Object.values(obs.argsSummary)) {
|
|
22795
|
+
if (typeof value !== "string") continue;
|
|
22796
|
+
if (countUrlEncoded(value) >= SIGNATURE_PATTERNS.urlEncodedThreshold) {
|
|
22797
|
+
urlBlob = true;
|
|
22798
|
+
}
|
|
22799
|
+
if (longestBase64Run(value) >= SIGNATURE_PATTERNS.base64MinRun) {
|
|
22800
|
+
base64Blob = true;
|
|
22801
|
+
}
|
|
22802
|
+
if (SIGNATURE_PATTERNS.shellMetacharRegex.test(value)) {
|
|
22803
|
+
shellChars = true;
|
|
22804
|
+
}
|
|
22805
|
+
}
|
|
22806
|
+
if (urlBlob) out.push("url_encoded_blob");
|
|
22807
|
+
if (base64Blob) out.push("base64_chunk");
|
|
22808
|
+
if (shellChars) out.push("shell_metachar");
|
|
22809
|
+
return out;
|
|
22810
|
+
}
|
|
22811
|
+
signatureOf(argsSummary) {
|
|
22812
|
+
return Object.keys(argsSummary).sort().join(",");
|
|
22813
|
+
}
|
|
22814
|
+
buildLayerAFinding(obs, matches, now, detectionPath) {
|
|
22815
|
+
const severity = matches.includes("shell_metachar") ? "alert" : "warn";
|
|
22816
|
+
const summary = `${obs.tool}: tool-call argument matches signature ${matches.join(", ")} (${detectionPath}).`;
|
|
22817
|
+
return {
|
|
22818
|
+
finding_id: "",
|
|
22819
|
+
sentinel_id: this.sentinelId,
|
|
22820
|
+
severity,
|
|
22821
|
+
summary: truncateSummary2(summary),
|
|
22822
|
+
details: {
|
|
22823
|
+
layer: "A",
|
|
22824
|
+
tool: obs.tool,
|
|
22825
|
+
proxy: obs.proxy,
|
|
22826
|
+
signatures: matches,
|
|
22827
|
+
detection_path: detectionPath
|
|
22828
|
+
},
|
|
22829
|
+
observed_at: now.toISOString(),
|
|
22830
|
+
evidence_audit_ids: [`${obs.entry.timestamp}:${obs.entry.operation}`],
|
|
22831
|
+
fortress_id: ""
|
|
22832
|
+
};
|
|
22833
|
+
}
|
|
22834
|
+
buildNovelSignatureFinding(obs, signature, now) {
|
|
22835
|
+
return {
|
|
22836
|
+
finding_id: "",
|
|
22837
|
+
sentinel_id: this.sentinelId,
|
|
22838
|
+
severity: "warn",
|
|
22839
|
+
summary: truncateSummary2(
|
|
22840
|
+
`${obs.tool}: novel argument-key signature observed (${signature || "<no-args>"}).`
|
|
22841
|
+
),
|
|
22842
|
+
details: {
|
|
22843
|
+
layer: "A",
|
|
22844
|
+
tool: obs.tool,
|
|
22845
|
+
proxy: obs.proxy,
|
|
22846
|
+
signatures: ["novel_signature"],
|
|
22847
|
+
detection_path: "rule-based",
|
|
22848
|
+
novel_signature: signature
|
|
22849
|
+
},
|
|
22850
|
+
observed_at: now.toISOString(),
|
|
22851
|
+
evidence_audit_ids: [`${obs.entry.timestamp}:${obs.entry.operation}`],
|
|
22852
|
+
fortress_id: ""
|
|
22853
|
+
};
|
|
22854
|
+
}
|
|
22855
|
+
// ── Layer B ───────────────────────────────────────────────────────
|
|
22856
|
+
runLayerB(observations, now) {
|
|
22857
|
+
const dayMs = 24 * 60 * 60 * 1e3;
|
|
22858
|
+
const perTool = /* @__PURE__ */ new Map();
|
|
22859
|
+
for (const obs of observations) {
|
|
22860
|
+
const ageMs = now.getTime() - obs.ts;
|
|
22861
|
+
if (ageMs < 0) continue;
|
|
22862
|
+
const windowIdx = Math.floor(ageMs / dayMs);
|
|
22863
|
+
if (windowIdx > BASELINE_WINDOWS4) continue;
|
|
22864
|
+
let w = perTool.get(obs.tool);
|
|
22865
|
+
if (!w) {
|
|
22866
|
+
w = {
|
|
22867
|
+
windows: Array.from({ length: BASELINE_WINDOWS4 + 1 }, () => ({
|
|
22868
|
+
count: 0,
|
|
22869
|
+
evidenceIds: []
|
|
22870
|
+
})),
|
|
22871
|
+
knownSignatures: /* @__PURE__ */ new Set()
|
|
22872
|
+
};
|
|
22873
|
+
perTool.set(obs.tool, w);
|
|
22874
|
+
}
|
|
22875
|
+
const bucket = w.windows[windowIdx];
|
|
22876
|
+
bucket.count += 1;
|
|
22877
|
+
if (windowIdx === 0 && bucket.evidenceIds.length < 50) {
|
|
22878
|
+
bucket.evidenceIds.push(`${obs.entry.timestamp}:${obs.entry.operation}`);
|
|
22879
|
+
}
|
|
22880
|
+
}
|
|
22881
|
+
const findings = [];
|
|
22882
|
+
for (const [tool, w] of perTool.entries()) {
|
|
22883
|
+
const f = this.evaluateToolFrequency(tool, w, now);
|
|
22884
|
+
if (f) findings.push(f);
|
|
22885
|
+
}
|
|
22886
|
+
return findings;
|
|
22887
|
+
}
|
|
22888
|
+
evaluateToolFrequency(tool, w, now) {
|
|
22889
|
+
const current = w.windows[0];
|
|
22890
|
+
const baseline = w.windows.slice(1);
|
|
22891
|
+
const populated = baseline.filter((b) => b.count > 0).length;
|
|
22892
|
+
if (populated < BASELINE_WINDOWS4) {
|
|
22893
|
+
this.baselineEstablished.add(tool);
|
|
22894
|
+
return null;
|
|
22895
|
+
}
|
|
22896
|
+
const counts = baseline.map((b) => b.count);
|
|
22897
|
+
const mean = counts.reduce((sum, c) => sum + c, 0) / counts.length;
|
|
22898
|
+
const variance = counts.reduce((sum, c) => sum + (c - mean) ** 2, 0) / counts.length;
|
|
22899
|
+
const stddev = Math.sqrt(variance);
|
|
22900
|
+
const wasEstablished = this.baselineEstablished.has(tool);
|
|
22901
|
+
this.baselineEstablished.add(tool);
|
|
22902
|
+
if (!wasEstablished) {
|
|
22903
|
+
return {
|
|
22904
|
+
finding_id: "",
|
|
22905
|
+
sentinel_id: this.sentinelId,
|
|
22906
|
+
severity: "info",
|
|
22907
|
+
summary: truncateSummary2(
|
|
22908
|
+
`${tool}: tool-call baseline established (mean ${mean.toFixed(1)} calls/24h, stddev ${stddev.toFixed(1)} over ${BASELINE_WINDOWS4} prior days).`
|
|
22909
|
+
),
|
|
22910
|
+
details: {
|
|
22911
|
+
layer: "B",
|
|
22912
|
+
tool,
|
|
22913
|
+
baseline_mean: mean,
|
|
22914
|
+
baseline_stddev: stddev,
|
|
22915
|
+
baseline_counts: counts,
|
|
22916
|
+
current_count: current.count
|
|
22917
|
+
},
|
|
22918
|
+
observed_at: now.toISOString(),
|
|
22919
|
+
evidence_audit_ids: [],
|
|
22920
|
+
fortress_id: ""
|
|
22921
|
+
};
|
|
22922
|
+
}
|
|
22923
|
+
const warnT = mean + WARN_SIGMA4 * stddev;
|
|
22924
|
+
const alertT = mean + ALERT_SIGMA4 * stddev;
|
|
22925
|
+
if (current.count > alertT) {
|
|
22926
|
+
return this.buildLayerBAnomaly(
|
|
22927
|
+
tool,
|
|
22928
|
+
current,
|
|
22929
|
+
mean,
|
|
22930
|
+
stddev,
|
|
22931
|
+
ALERT_SIGMA4,
|
|
22932
|
+
"alert",
|
|
22933
|
+
now
|
|
22934
|
+
);
|
|
22935
|
+
}
|
|
22936
|
+
if (current.count > warnT) {
|
|
22937
|
+
return this.buildLayerBAnomaly(
|
|
22938
|
+
tool,
|
|
22939
|
+
current,
|
|
22940
|
+
mean,
|
|
22941
|
+
stddev,
|
|
22942
|
+
WARN_SIGMA4,
|
|
22943
|
+
"warn",
|
|
22944
|
+
now
|
|
22945
|
+
);
|
|
22946
|
+
}
|
|
22947
|
+
return null;
|
|
22948
|
+
}
|
|
22949
|
+
buildLayerBAnomaly(tool, current, mean, stddev, sigma, severity, now) {
|
|
22950
|
+
const ratio = mean === 0 ? Infinity : current.count / mean;
|
|
22951
|
+
const ratioStr = Number.isFinite(ratio) ? `${ratio.toFixed(1)}x normal` : "no prior baseline";
|
|
22952
|
+
return {
|
|
22953
|
+
finding_id: "",
|
|
22954
|
+
sentinel_id: this.sentinelId,
|
|
22955
|
+
severity,
|
|
22956
|
+
summary: truncateSummary2(
|
|
22957
|
+
`${tool}: tool-call rate is ${ratioStr}: ${current.count} calls in last 24h, baseline ${mean.toFixed(1)} (stddev ${stddev.toFixed(1)}). Crossed +${sigma} sigma.`
|
|
22958
|
+
),
|
|
22959
|
+
details: {
|
|
22960
|
+
layer: "B",
|
|
22961
|
+
tool,
|
|
22962
|
+
current_count: current.count,
|
|
22963
|
+
baseline_mean: mean,
|
|
22964
|
+
baseline_stddev: stddev,
|
|
22965
|
+
sigma_threshold: sigma,
|
|
22966
|
+
ratio
|
|
22967
|
+
},
|
|
22968
|
+
observed_at: now.toISOString(),
|
|
22969
|
+
evidence_audit_ids: current.evidenceIds,
|
|
22970
|
+
fortress_id: ""
|
|
22971
|
+
};
|
|
22972
|
+
}
|
|
22973
|
+
// ── Layer C ───────────────────────────────────────────────────────
|
|
22974
|
+
runLayerC(observations, now) {
|
|
22975
|
+
const dayMs = 24 * 60 * 60 * 1e3;
|
|
22976
|
+
const sorted = [...observations].sort((a, b) => a.ts - b.ts);
|
|
22977
|
+
const tasks = [];
|
|
22978
|
+
for (const obs of sorted) {
|
|
22979
|
+
const last = tasks[tasks.length - 1];
|
|
22980
|
+
if (!last || obs.ts - last.startTs > TASK_WINDOW_MS) {
|
|
22981
|
+
tasks.push({ startTs: obs.ts, tools: [obs.tool] });
|
|
22982
|
+
continue;
|
|
22983
|
+
}
|
|
22984
|
+
if (!last.tools.includes(obs.tool)) last.tools.push(obs.tool);
|
|
22985
|
+
}
|
|
22986
|
+
const recentTaskKeys = [];
|
|
22987
|
+
const findings = [];
|
|
22988
|
+
for (const task of tasks) {
|
|
22989
|
+
const ageMs = now.getTime() - task.startTs;
|
|
22990
|
+
const key = task.tools.slice().sort().join(",");
|
|
22991
|
+
if (ageMs > dayMs) {
|
|
22992
|
+
this.knownCombinations.add(key);
|
|
22993
|
+
continue;
|
|
22994
|
+
}
|
|
22995
|
+
if (task.tools.length < 2) continue;
|
|
22996
|
+
if (!this.knownCombinations.has(key)) {
|
|
22997
|
+
this.knownCombinations.add(key);
|
|
22998
|
+
if (!this.novelCombinationsReported.has(key)) {
|
|
22999
|
+
this.novelCombinationsReported.add(key);
|
|
23000
|
+
recentTaskKeys.push(key);
|
|
23001
|
+
findings.push(
|
|
23002
|
+
this.buildLayerCFinding(task, key, "warn", now)
|
|
23003
|
+
);
|
|
23004
|
+
}
|
|
23005
|
+
}
|
|
23006
|
+
}
|
|
23007
|
+
if (recentTaskKeys.length >= ALERT_NOVEL_COMBINATIONS) {
|
|
23008
|
+
const aggregate = {
|
|
23009
|
+
finding_id: "",
|
|
23010
|
+
sentinel_id: this.sentinelId,
|
|
23011
|
+
severity: "alert",
|
|
23012
|
+
summary: truncateSummary2(
|
|
23013
|
+
`multi-novel-combination: ${recentTaskKeys.length} novel tool-permission combinations within last 24h.`
|
|
23014
|
+
),
|
|
23015
|
+
details: {
|
|
23016
|
+
layer: "C",
|
|
23017
|
+
novel_combinations: recentTaskKeys
|
|
23018
|
+
},
|
|
23019
|
+
observed_at: now.toISOString(),
|
|
23020
|
+
evidence_audit_ids: [],
|
|
23021
|
+
fortress_id: ""
|
|
23022
|
+
};
|
|
23023
|
+
findings.push(aggregate);
|
|
23024
|
+
}
|
|
23025
|
+
return findings;
|
|
21251
23026
|
}
|
|
21252
|
-
|
|
21253
|
-
|
|
21254
|
-
|
|
21255
|
-
|
|
21256
|
-
|
|
21257
|
-
|
|
21258
|
-
|
|
21259
|
-
|
|
21260
|
-
|
|
21261
|
-
|
|
21262
|
-
|
|
21263
|
-
|
|
21264
|
-
|
|
21265
|
-
|
|
21266
|
-
|
|
21267
|
-
|
|
21268
|
-
|
|
21269
|
-
|
|
21270
|
-
|
|
23027
|
+
buildLayerCFinding(task, key, severity, now) {
|
|
23028
|
+
return {
|
|
23029
|
+
finding_id: "",
|
|
23030
|
+
sentinel_id: this.sentinelId,
|
|
23031
|
+
severity,
|
|
23032
|
+
summary: truncateSummary2(
|
|
23033
|
+
`novel-permission-combination: tools=[${task.tools.join(",")}] observed in single task burst (${task.tools.length} distinct tools).`
|
|
23034
|
+
),
|
|
23035
|
+
details: {
|
|
23036
|
+
layer: "C",
|
|
23037
|
+
combination_key: key,
|
|
23038
|
+
tools: task.tools,
|
|
23039
|
+
task_started_at: new Date(task.startTs).toISOString()
|
|
23040
|
+
},
|
|
23041
|
+
observed_at: now.toISOString(),
|
|
23042
|
+
evidence_audit_ids: [],
|
|
23043
|
+
fortress_id: ""
|
|
23044
|
+
};
|
|
23045
|
+
}
|
|
23046
|
+
// ── LLM-assist ────────────────────────────────────────────────────
|
|
23047
|
+
classifyHandle(ctx) {
|
|
23048
|
+
const selector = ctx.substrateSelector;
|
|
23049
|
+
if (!selector) return null;
|
|
23050
|
+
const fn = selector.invokeClassify;
|
|
23051
|
+
if (typeof fn !== "function") return null;
|
|
23052
|
+
return async (items) => {
|
|
21271
23053
|
try {
|
|
21272
|
-
const
|
|
21273
|
-
|
|
21274
|
-
|
|
21275
|
-
|
|
21276
|
-
|
|
21277
|
-
);
|
|
21278
|
-
if (
|
|
21279
|
-
|
|
21280
|
-
pruned += 1;
|
|
23054
|
+
const resp = await fn.call(selector, "sentinel-scoring", {
|
|
23055
|
+
kind: "classify",
|
|
23056
|
+
items,
|
|
23057
|
+
categories: ["benign", "suspicious"]
|
|
23058
|
+
});
|
|
23059
|
+
if (resp.failureClass) return { kind: "failure", message: "substrate failure" };
|
|
23060
|
+
if (resp.body.kind === "classify") {
|
|
23061
|
+
return { kind: "classify", results: resp.body.results };
|
|
21281
23062
|
}
|
|
23063
|
+
return { kind: "failure", message: resp.body.message };
|
|
21282
23064
|
} catch {
|
|
23065
|
+
return null;
|
|
23066
|
+
}
|
|
23067
|
+
};
|
|
23068
|
+
}
|
|
23069
|
+
async consultClassifier(classify, obs) {
|
|
23070
|
+
const item = JSON.stringify({
|
|
23071
|
+
tool: obs.tool,
|
|
23072
|
+
proxy: obs.proxy,
|
|
23073
|
+
args_summary: obs.argsSummary
|
|
23074
|
+
});
|
|
23075
|
+
const result = await classify([item]);
|
|
23076
|
+
if (!result || result.kind !== "classify") return "unknown";
|
|
23077
|
+
const top = result.results[0];
|
|
23078
|
+
if (!top) return "unknown";
|
|
23079
|
+
if (top.category === "suspicious" && top.confidence >= 0.5) {
|
|
23080
|
+
return "suspicious";
|
|
23081
|
+
}
|
|
23082
|
+
if (top.category === "benign") return "benign";
|
|
23083
|
+
return "unknown";
|
|
23084
|
+
}
|
|
23085
|
+
// ── helpers ───────────────────────────────────────────────────────
|
|
23086
|
+
observationFromEntry(entry) {
|
|
23087
|
+
const op = entry.operation;
|
|
23088
|
+
let tool = null;
|
|
23089
|
+
let proxy = false;
|
|
23090
|
+
for (const prefix of GATE_PREFIXES) {
|
|
23091
|
+
if (op.startsWith(prefix)) {
|
|
23092
|
+
tool = op.slice(prefix.length);
|
|
23093
|
+
proxy = prefix === "gate_allow_proxy:";
|
|
23094
|
+
break;
|
|
21283
23095
|
}
|
|
21284
23096
|
}
|
|
21285
|
-
|
|
23097
|
+
if (!tool) return null;
|
|
23098
|
+
const ts = Date.parse(entry.timestamp);
|
|
23099
|
+
if (!Number.isFinite(ts)) return null;
|
|
23100
|
+
const argsSummary = extractArgsSummary(entry.details);
|
|
23101
|
+
return { tool, proxy, ts, entry, argsSummary };
|
|
21286
23102
|
}
|
|
21287
23103
|
};
|
|
21288
23104
|
}
|
|
21289
23105
|
});
|
|
21290
23106
|
|
|
23107
|
+
// src/sentinel/sentinels/index.ts
|
|
23108
|
+
var PHI1_BASELINE_CATALOG;
|
|
23109
|
+
var init_sentinels = __esm({
|
|
23110
|
+
"src/sentinel/sentinels/index.ts"() {
|
|
23111
|
+
init_egress_volume_watcher();
|
|
23112
|
+
init_cross_agent_chatter_watcher();
|
|
23113
|
+
init_credential_usage_watcher();
|
|
23114
|
+
init_suspicious_tool_call_detector();
|
|
23115
|
+
PHI1_BASELINE_CATALOG = [
|
|
23116
|
+
{
|
|
23117
|
+
sentinelId: EGRESS_VOLUME_SENTINEL_ID,
|
|
23118
|
+
description: "Watches outbound proxy-call volume per upstream server and surfaces anomalous spikes against a rolling 7-day baseline.",
|
|
23119
|
+
factory: () => new EgressVolumeWatcher()
|
|
23120
|
+
},
|
|
23121
|
+
{
|
|
23122
|
+
sentinelId: CROSS_AGENT_CHATTER_SENTINEL_ID,
|
|
23123
|
+
description: "Watches inter-agent communication patterns. Surfaces per-pair rate spikes (3 or 6 sigma over the rolling 7-day baseline) and new-partner appearances. Escalates to alert when one source agent picks up 3 or more new partners in 24h.",
|
|
23124
|
+
factory: () => new CrossAgentChatterWatcher()
|
|
23125
|
+
},
|
|
23126
|
+
{
|
|
23127
|
+
sentinelId: CREDENTIAL_USAGE_SENTINEL_ID,
|
|
23128
|
+
description: "Watches per-agent credential reads. Surfaces (agent, secret) usage that exceeds a rolling 7-day baseline, and unfamiliar secret combinations the agent uses for the first time in one 24h window.",
|
|
23129
|
+
factory: () => new CredentialUsageWatcher()
|
|
23130
|
+
},
|
|
23131
|
+
{
|
|
23132
|
+
sentinelId: SUSPICIOUS_TOOL_CALL_SENTINEL_ID,
|
|
23133
|
+
description: "Surfaces tool calls whose argument shape, call frequency, or permission combination looks unusual for the fortress's recent history.",
|
|
23134
|
+
factory: () => new SuspiciousToolCallDetector()
|
|
23135
|
+
}
|
|
23136
|
+
];
|
|
23137
|
+
}
|
|
23138
|
+
});
|
|
23139
|
+
function sentinelSubscriptionsPath(storagePath) {
|
|
23140
|
+
return path.join(storagePath, "sentinel-subscriptions.json");
|
|
23141
|
+
}
|
|
23142
|
+
async function loadSentinelSubscriptions(storagePath) {
|
|
23143
|
+
const filePath = sentinelSubscriptionsPath(storagePath);
|
|
23144
|
+
try {
|
|
23145
|
+
const raw = await promises.readFile(filePath, "utf8");
|
|
23146
|
+
const parsed = JSON.parse(raw);
|
|
23147
|
+
if (parsed.version !== FILE_VERSION) return /* @__PURE__ */ new Set();
|
|
23148
|
+
if (!Array.isArray(parsed.subscribed)) return /* @__PURE__ */ new Set();
|
|
23149
|
+
const cleaned = parsed.subscribed.filter(
|
|
23150
|
+
(id) => typeof id === "string" && id.length > 0
|
|
23151
|
+
);
|
|
23152
|
+
return new Set(cleaned);
|
|
23153
|
+
} catch {
|
|
23154
|
+
return /* @__PURE__ */ new Set();
|
|
23155
|
+
}
|
|
23156
|
+
}
|
|
23157
|
+
async function saveSentinelSubscriptions(storagePath, subscribed) {
|
|
23158
|
+
const filePath = sentinelSubscriptionsPath(storagePath);
|
|
23159
|
+
await promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
23160
|
+
const payload = {
|
|
23161
|
+
version: FILE_VERSION,
|
|
23162
|
+
subscribed: [...new Set(subscribed)].filter((s) => s.length > 0).sort()
|
|
23163
|
+
};
|
|
23164
|
+
await promises.writeFile(filePath, `${JSON.stringify(payload, null, 2)}
|
|
23165
|
+
`, {
|
|
23166
|
+
mode: 384
|
|
23167
|
+
});
|
|
23168
|
+
}
|
|
23169
|
+
var FILE_VERSION;
|
|
23170
|
+
var init_subscription_store = __esm({
|
|
23171
|
+
"src/sentinel/subscription-store.ts"() {
|
|
23172
|
+
FILE_VERSION = 1;
|
|
23173
|
+
}
|
|
23174
|
+
});
|
|
23175
|
+
|
|
21291
23176
|
// src/principal-policy/tools.ts
|
|
21292
23177
|
function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
21293
23178
|
return [
|
|
@@ -32765,7 +34650,7 @@ var init_recovery_key_disclosure = __esm({
|
|
|
32765
34650
|
});
|
|
32766
34651
|
|
|
32767
34652
|
// src/hub/types.ts
|
|
32768
|
-
var
|
|
34653
|
+
var init_types4 = __esm({
|
|
32769
34654
|
"src/hub/types.ts"() {
|
|
32770
34655
|
}
|
|
32771
34656
|
});
|
|
@@ -33804,7 +35689,7 @@ var init_hub = __esm({
|
|
|
33804
35689
|
"src/hub/index.ts"() {
|
|
33805
35690
|
init_constants3();
|
|
33806
35691
|
init_errors4();
|
|
33807
|
-
|
|
35692
|
+
init_types4();
|
|
33808
35693
|
init_agent_registry();
|
|
33809
35694
|
init_inbox_store();
|
|
33810
35695
|
init_inbox_aggregator();
|
|
@@ -33913,7 +35798,17 @@ var init_operator_chat_audit_events = __esm({
|
|
|
33913
35798
|
* fold. The concierge omits that category and continues; the user-
|
|
33914
35799
|
* facing query is never broken. Body carries category + failure_reason.
|
|
33915
35800
|
*/
|
|
33916
|
-
CONCIERGE_CONTEXT_FETCHER_FAILED: "operator_concierge_context_fetcher_failed"
|
|
35801
|
+
CONCIERGE_CONTEXT_FETCHER_FAILED: "operator_concierge_context_fetcher_failed",
|
|
35802
|
+
/**
|
|
35803
|
+
* Concierge surfaced a proactive starter when a fresh conversation
|
|
35804
|
+
* thread opened (WP-V1.3-9 Tau-5). Emitted once per starter, never on
|
|
35805
|
+
* follow-up turns within the same thread. Body carries `thread_id`,
|
|
35806
|
+
* `trigger` (stable enum), and `triggered_agents_count`. The starter
|
|
35807
|
+
* text body is NOT carried; the trigger enum is sufficient for
|
|
35808
|
+
* dashboard grouping and keeps fortress-internal agent ids off the
|
|
35809
|
+
* audit surface.
|
|
35810
|
+
*/
|
|
35811
|
+
CONCIERGE_PROACTIVE_SUGGESTION_OFFERED: "operator_concierge_proactive_suggestion_offered"
|
|
33917
35812
|
};
|
|
33918
35813
|
}
|
|
33919
35814
|
});
|
|
@@ -34747,9 +36642,108 @@ var init_concierge_query_grammar = __esm({
|
|
|
34747
36642
|
LLM_ASSIST_THRESHOLD = 0.5;
|
|
34748
36643
|
}
|
|
34749
36644
|
});
|
|
36645
|
+
|
|
36646
|
+
// src/chat/agent-context-cache.ts
|
|
34750
36647
|
function approxTokenLen2(text) {
|
|
34751
36648
|
return Math.ceil(text.length / 4);
|
|
34752
36649
|
}
|
|
36650
|
+
function formatSnapshotLine(snapshot) {
|
|
36651
|
+
const flagLabel = snapshot.state_flags.join("+") || "no_flags";
|
|
36652
|
+
const work = snapshot.current_work_summary ? `, last: ${snapshot.current_work_summary}` : "";
|
|
36653
|
+
const verascore = snapshot.recent_verascore_delta_24h !== null ? `, verascore \u0394${snapshot.recent_verascore_delta_24h.toFixed(2)}` : "";
|
|
36654
|
+
return `- ${snapshot.agent_name} (template: ${snapshot.template}): ${flagLabel}, ${snapshot.recent_audit_count_24h} audit/24h, ${snapshot.recent_concordia_receipts_count_24h} receipts${verascore}${work}`;
|
|
36655
|
+
}
|
|
36656
|
+
function urgencyRank(snapshot) {
|
|
36657
|
+
for (let i = 0; i < STATE_FLAG_ORDER.length; i++) {
|
|
36658
|
+
if (snapshot.state_flags.includes(STATE_FLAG_ORDER[i])) {
|
|
36659
|
+
return i;
|
|
36660
|
+
}
|
|
36661
|
+
}
|
|
36662
|
+
return STATE_FLAG_ORDER.length;
|
|
36663
|
+
}
|
|
36664
|
+
function formatCurrentAgentStateSection(snapshots, opts) {
|
|
36665
|
+
if (snapshots.length === 0) return "";
|
|
36666
|
+
const budget = opts?.maxTokens ?? DEFAULT_AGENT_CONTEXT_TOKEN_BUDGET;
|
|
36667
|
+
const sorted = [...snapshots].sort(
|
|
36668
|
+
(a, b) => urgencyRank(a) - urgencyRank(b)
|
|
36669
|
+
);
|
|
36670
|
+
const headerTokens = approxTokenLen2(`${SECTION_HEADER}
|
|
36671
|
+
`);
|
|
36672
|
+
const sepTokens = approxTokenLen2("\n");
|
|
36673
|
+
let runningTokens = headerTokens;
|
|
36674
|
+
const kept = [];
|
|
36675
|
+
for (const snap of sorted) {
|
|
36676
|
+
const line = formatSnapshotLine(snap);
|
|
36677
|
+
const tokens = approxTokenLen2(line) + (kept.length > 0 ? sepTokens : 0);
|
|
36678
|
+
if (kept.length === 0) {
|
|
36679
|
+
kept.push(line);
|
|
36680
|
+
runningTokens += tokens;
|
|
36681
|
+
continue;
|
|
36682
|
+
}
|
|
36683
|
+
if (runningTokens + tokens > budget) break;
|
|
36684
|
+
kept.push(line);
|
|
36685
|
+
runningTokens += tokens;
|
|
36686
|
+
}
|
|
36687
|
+
return `${SECTION_HEADER}
|
|
36688
|
+
${kept.join("\n")}`;
|
|
36689
|
+
}
|
|
36690
|
+
function generateProactiveStarter(snapshots) {
|
|
36691
|
+
if (snapshots.length === 0) return null;
|
|
36692
|
+
const stuck = snapshots.filter((s) => s.state_flags.includes("stuck"));
|
|
36693
|
+
if (stuck.length > 0) {
|
|
36694
|
+
const first = stuck[0];
|
|
36695
|
+
if (first === void 0) return null;
|
|
36696
|
+
const last = first.current_work_summary ? ` (last: ${first.current_work_summary})` : "";
|
|
36697
|
+
return {
|
|
36698
|
+
text: `Your ${first.agent_name} agent looks stuck${last}. Should I check its session state?`,
|
|
36699
|
+
trigger: "stuck_agent",
|
|
36700
|
+
triggered_agents_count: stuck.length
|
|
36701
|
+
};
|
|
36702
|
+
}
|
|
36703
|
+
const pending = snapshots.filter(
|
|
36704
|
+
(s) => s.state_flags.includes("has_pending_approvals")
|
|
36705
|
+
);
|
|
36706
|
+
if (pending.length > 0) {
|
|
36707
|
+
const names = pending.slice(0, 3).map((s) => s.agent_name).join(", ");
|
|
36708
|
+
return {
|
|
36709
|
+
text: `You have pending approvals across ${names}. Want to walk through them?`,
|
|
36710
|
+
trigger: "pending_approvals",
|
|
36711
|
+
triggered_agents_count: pending.length
|
|
36712
|
+
};
|
|
36713
|
+
}
|
|
36714
|
+
const findings = snapshots.filter(
|
|
36715
|
+
(s) => s.state_flags.includes("has_open_findings")
|
|
36716
|
+
);
|
|
36717
|
+
if (findings.length > 0) {
|
|
36718
|
+
return {
|
|
36719
|
+
text: `Sentinel has open findings on ${findings.length} ${findings.length === 1 ? "agent" : "agents"}. Want a summary?`,
|
|
36720
|
+
trigger: "open_findings",
|
|
36721
|
+
triggered_agents_count: findings.length
|
|
36722
|
+
};
|
|
36723
|
+
}
|
|
36724
|
+
return {
|
|
36725
|
+
text: "Your fortress is quiet. Anything you'd like to inspect?",
|
|
36726
|
+
trigger: "all_idle",
|
|
36727
|
+
triggered_agents_count: snapshots.length
|
|
36728
|
+
};
|
|
36729
|
+
}
|
|
36730
|
+
var STATE_FLAG_ORDER, SECTION_HEADER, DEFAULT_AGENT_CONTEXT_TOKEN_BUDGET;
|
|
36731
|
+
var init_agent_context_cache = __esm({
|
|
36732
|
+
"src/chat/agent-context-cache.ts"() {
|
|
36733
|
+
STATE_FLAG_ORDER = [
|
|
36734
|
+
"stuck",
|
|
36735
|
+
"has_pending_approvals",
|
|
36736
|
+
"has_open_findings",
|
|
36737
|
+
"active",
|
|
36738
|
+
"idle"
|
|
36739
|
+
];
|
|
36740
|
+
SECTION_HEADER = "## Current agent state";
|
|
36741
|
+
DEFAULT_AGENT_CONTEXT_TOKEN_BUDGET = 400;
|
|
36742
|
+
}
|
|
36743
|
+
});
|
|
36744
|
+
function approxTokenLen3(text) {
|
|
36745
|
+
return Math.ceil(text.length / 4);
|
|
36746
|
+
}
|
|
34753
36747
|
function makeEventId(prefix) {
|
|
34754
36748
|
return `${prefix}-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
|
|
34755
36749
|
}
|
|
@@ -34771,7 +36765,7 @@ function formatPriorTurnLine(turn) {
|
|
|
34771
36765
|
function hashOf(input) {
|
|
34772
36766
|
return hashToString(sha256.sha256(stringToBytes(input)));
|
|
34773
36767
|
}
|
|
34774
|
-
var DEFAULT_CONCIERGE_MAX_TOKENS, DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS, DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS, DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET, DEFAULT_CONCIERGE_SESSION_TTL_MS, DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET, SANCTUARY_DOMAIN_REFERENCE, OperatorChatService;
|
|
36768
|
+
var DEFAULT_CONCIERGE_MAX_TOKENS, DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS, DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS, DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET, DEFAULT_CONCIERGE_SESSION_TTL_MS, DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET, DEFAULT_CONCIERGE_AGENT_STATE_BUDGET, SANCTUARY_DOMAIN_REFERENCE, OperatorChatService;
|
|
34775
36769
|
var init_operator_chat_service = __esm({
|
|
34776
36770
|
"src/chat/operator-chat-service.ts"() {
|
|
34777
36771
|
init_hashing();
|
|
@@ -34780,12 +36774,14 @@ var init_operator_chat_service = __esm({
|
|
|
34780
36774
|
init_operator_chat_types();
|
|
34781
36775
|
init_concierge_context_router();
|
|
34782
36776
|
init_concierge_query_grammar();
|
|
36777
|
+
init_agent_context_cache();
|
|
34783
36778
|
DEFAULT_CONCIERGE_MAX_TOKENS = 512;
|
|
34784
36779
|
DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS = 10;
|
|
34785
36780
|
DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
|
|
34786
36781
|
DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET = 500;
|
|
34787
36782
|
DEFAULT_CONCIERGE_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
34788
36783
|
DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET = 2e3;
|
|
36784
|
+
DEFAULT_CONCIERGE_AGENT_STATE_BUDGET = 400;
|
|
34789
36785
|
SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
|
|
34790
36786
|
1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
|
|
34791
36787
|
2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
|
|
@@ -34832,6 +36828,15 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
34832
36828
|
dynamicContextBudget;
|
|
34833
36829
|
agentRegistry;
|
|
34834
36830
|
grammarLlmAssist;
|
|
36831
|
+
agentContextCache;
|
|
36832
|
+
agentStateBudget;
|
|
36833
|
+
/**
|
|
36834
|
+
* Per-thread guard so the proactive starter fires at most once per
|
|
36835
|
+
* fresh thread. Tracks the thread_id the starter was last offered
|
|
36836
|
+
* for; subsequent `getProactiveStarter()` calls within the same
|
|
36837
|
+
* thread return null instead of re-emitting.
|
|
36838
|
+
*/
|
|
36839
|
+
starterOfferedForThreadId;
|
|
34835
36840
|
/**
|
|
34836
36841
|
* In-memory thread_id assigned to the active concierge session.
|
|
34837
36842
|
* The first sendConcierge call after construction allocates a fresh
|
|
@@ -34876,6 +36881,10 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
34876
36881
|
if (deps.conciergeGrammarLlmAssist) {
|
|
34877
36882
|
this.grammarLlmAssist = deps.conciergeGrammarLlmAssist;
|
|
34878
36883
|
}
|
|
36884
|
+
if (deps.conciergeAgentContextCache) {
|
|
36885
|
+
this.agentContextCache = deps.conciergeAgentContextCache;
|
|
36886
|
+
}
|
|
36887
|
+
this.agentStateBudget = deps.conciergeAgentStateBudget !== void 0 && deps.conciergeAgentStateBudget > 0 ? deps.conciergeAgentStateBudget : DEFAULT_CONCIERGE_AGENT_STATE_BUDGET;
|
|
34879
36888
|
}
|
|
34880
36889
|
// ── Concierge ─────────────────────────────────────────────────────────
|
|
34881
36890
|
/**
|
|
@@ -34897,6 +36906,7 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
34897
36906
|
const nowMs = this.clock();
|
|
34898
36907
|
if (this.activeMemoryThreadId && this.lastInteractionAt !== void 0 && nowMs - this.lastInteractionAt > this.sessionTtlMs) {
|
|
34899
36908
|
this.activeMemoryThreadId = void 0;
|
|
36909
|
+
this.starterOfferedForThreadId = void 0;
|
|
34900
36910
|
}
|
|
34901
36911
|
const operatorMessage = {
|
|
34902
36912
|
message_id: crypto.randomUUID(),
|
|
@@ -34935,6 +36945,11 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
34935
36945
|
});
|
|
34936
36946
|
}
|
|
34937
36947
|
const parsedGrammar = await this.runGrammarParse(filterResult.filtered);
|
|
36948
|
+
const agentSnapshots = this.agentContextCache ? this.agentContextCache.read() : [];
|
|
36949
|
+
const agentStateSection = this.agentContextCache ? formatCurrentAgentStateSection(agentSnapshots, {
|
|
36950
|
+
maxTokens: this.agentStateBudget
|
|
36951
|
+
}) : "";
|
|
36952
|
+
const renderedAgentCount = agentStateSection ? agentSnapshots.length : 0;
|
|
34938
36953
|
const start = Date.now();
|
|
34939
36954
|
let conciergeBody;
|
|
34940
36955
|
let servedBy = "disabled";
|
|
@@ -34959,7 +36974,8 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
34959
36974
|
dynamicCategoriesIncluded = dynamicResult.categoriesIncluded;
|
|
34960
36975
|
const context = await this.assembleConciergeContext(
|
|
34961
36976
|
priorTurns,
|
|
34962
|
-
dynamicResult.section
|
|
36977
|
+
dynamicResult.section,
|
|
36978
|
+
agentStateSection
|
|
34963
36979
|
);
|
|
34964
36980
|
const response = await this.substrateSelector.invokeSummarize(
|
|
34965
36981
|
"concierge",
|
|
@@ -35026,7 +37042,8 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
35026
37042
|
prior_turns_folded: memoryReadFailureReason === null ? priorTurns.length : 0
|
|
35027
37043
|
} : {},
|
|
35028
37044
|
...this.contextFetchers ? { dynamic_context_categories: [...dynamicCategoriesIncluded] } : {},
|
|
35029
|
-
parsed_grammar: auditSafeSummary(parsedGrammar)
|
|
37045
|
+
parsed_grammar: auditSafeSummary(parsedGrammar),
|
|
37046
|
+
...this.agentContextCache !== void 0 ? { agent_context_snapshot_count: renderedAgentCount } : {}
|
|
35030
37047
|
};
|
|
35031
37048
|
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_CHAT, payload, outcome === "ok" ? "success" : "failure");
|
|
35032
37049
|
return {
|
|
@@ -35137,6 +37154,7 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
35137
37154
|
if (!removed) return false;
|
|
35138
37155
|
if (this.activeMemoryThreadId === threadId) {
|
|
35139
37156
|
this.activeMemoryThreadId = void 0;
|
|
37157
|
+
this.starterOfferedForThreadId = void 0;
|
|
35140
37158
|
}
|
|
35141
37159
|
const payload = {
|
|
35142
37160
|
version: "1.2",
|
|
@@ -35155,9 +37173,65 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
35155
37173
|
* Reset the active session memory thread. Subsequent sendConcierge
|
|
35156
37174
|
* calls allocate a fresh thread_id. Surfaced for tests + future "new
|
|
35157
37175
|
* conversation" affordance; not currently called by the dashboard.
|
|
37176
|
+
*
|
|
37177
|
+
* Tau-5: also clears the proactive-starter guard so the next
|
|
37178
|
+
* `getProactiveStarter()` call against the freshly-allocated thread
|
|
37179
|
+
* is eligible to fire.
|
|
35158
37180
|
*/
|
|
35159
37181
|
resetConciergeMemoryThread() {
|
|
35160
37182
|
this.activeMemoryThreadId = void 0;
|
|
37183
|
+
this.starterOfferedForThreadId = void 0;
|
|
37184
|
+
}
|
|
37185
|
+
/**
|
|
37186
|
+
* WP-V1.3-9 Tau-5: surface a proactive starter for the current
|
|
37187
|
+
* concierge session. Intended to be called by the dashboard UI when
|
|
37188
|
+
* the operator opens the chat surface, before any operator typing.
|
|
37189
|
+
*
|
|
37190
|
+
* Returns null when:
|
|
37191
|
+
* - No agent-context cache is wired (Tau-5 disabled).
|
|
37192
|
+
* - No concierge memory store is wired (no thread_id namespace).
|
|
37193
|
+
* - The cache snapshot has no signal (empty fortress).
|
|
37194
|
+
* - A starter has already been offered for the active thread (the
|
|
37195
|
+
* guard ensures one starter per fresh thread).
|
|
37196
|
+
*
|
|
37197
|
+
* Side effects:
|
|
37198
|
+
* - Allocates a fresh thread_id if none is active.
|
|
37199
|
+
* - Emits the `operator_concierge_proactive_suggestion_offered`
|
|
37200
|
+
* audit event with the trigger class + triggered_agents_count.
|
|
37201
|
+
* - Records the offered thread_id so the next call within the same
|
|
37202
|
+
* thread is a no-op.
|
|
37203
|
+
*
|
|
37204
|
+
* The returned starter's `text` is operator-visible copy; the
|
|
37205
|
+
* dashboard renders it as a system-message-style starter the
|
|
37206
|
+
* operator can accept (clicks/types follow-up) or dismiss (types a
|
|
37207
|
+
* new query).
|
|
37208
|
+
*/
|
|
37209
|
+
getProactiveStarter() {
|
|
37210
|
+
if (!this.agentContextCache) return null;
|
|
37211
|
+
if (!this.memory) return null;
|
|
37212
|
+
const threadId = this.ensureActiveMemoryThread();
|
|
37213
|
+
if (this.starterOfferedForThreadId === threadId) return null;
|
|
37214
|
+
const snapshots = this.agentContextCache.read();
|
|
37215
|
+
const starter = generateProactiveStarter(snapshots);
|
|
37216
|
+
if (!starter) return null;
|
|
37217
|
+
const payload = {
|
|
37218
|
+
version: "1.2",
|
|
37219
|
+
event_id: makeEventId("conc-starter"),
|
|
37220
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
37221
|
+
identity_id: this.identityId,
|
|
37222
|
+
kind: "operator_concierge_proactive_suggestion_offered",
|
|
37223
|
+
surface: "concierge",
|
|
37224
|
+
thread_id: threadId,
|
|
37225
|
+
trigger: starter.trigger,
|
|
37226
|
+
triggered_agents_count: starter.triggered_agents_count
|
|
37227
|
+
};
|
|
37228
|
+
this.emit(
|
|
37229
|
+
OPERATOR_CHAT_OPS.CONCIERGE_PROACTIVE_SUGGESTION_OFFERED,
|
|
37230
|
+
payload,
|
|
37231
|
+
"success"
|
|
37232
|
+
);
|
|
37233
|
+
this.starterOfferedForThreadId = threadId;
|
|
37234
|
+
return starter;
|
|
35161
37235
|
}
|
|
35162
37236
|
ensureActiveMemoryThread() {
|
|
35163
37237
|
if (!this.activeMemoryThreadId) {
|
|
@@ -35202,7 +37276,7 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
35202
37276
|
* if available; the v1.2 selector does not expose one, so structured
|
|
35203
37277
|
* serialization is the canonical path for v1.3.
|
|
35204
37278
|
*/
|
|
35205
|
-
async assembleConciergeContext(priorTurns = [], dynamicSection = "") {
|
|
37279
|
+
async assembleConciergeContext(priorTurns = [], dynamicSection = "", agentStateSection = "") {
|
|
35206
37280
|
const ref = `## Sanctuary reference
|
|
35207
37281
|
${SANCTUARY_DOMAIN_REFERENCE}`;
|
|
35208
37282
|
const priorSection = this.formatPriorTurnsSection(priorTurns);
|
|
@@ -35210,6 +37284,7 @@ ${SANCTUARY_DOMAIN_REFERENCE}`;
|
|
|
35210
37284
|
return [
|
|
35211
37285
|
ref,
|
|
35212
37286
|
...dynamicSection ? [dynamicSection] : [],
|
|
37287
|
+
...agentStateSection ? [agentStateSection] : [],
|
|
35213
37288
|
...priorSection ? [priorSection] : [],
|
|
35214
37289
|
"## Recent activity\n(no providers wired)",
|
|
35215
37290
|
"## Wrapped agents\n(no providers wired)",
|
|
@@ -35224,6 +37299,7 @@ ${SANCTUARY_DOMAIN_REFERENCE}`;
|
|
|
35224
37299
|
return [
|
|
35225
37300
|
ref,
|
|
35226
37301
|
...dynamicSection ? [dynamicSection] : [],
|
|
37302
|
+
...agentStateSection ? [agentStateSection] : [],
|
|
35227
37303
|
...priorSection ? [priorSection] : [],
|
|
35228
37304
|
`## Recent activity
|
|
35229
37305
|
${activity}`,
|
|
@@ -35306,14 +37382,14 @@ ${inbox}`
|
|
|
35306
37382
|
if (turns.length === 0) return "";
|
|
35307
37383
|
const HEADER = "## Prior conversation";
|
|
35308
37384
|
const lines = turns.map(formatPriorTurnLine);
|
|
35309
|
-
const headerTokens =
|
|
37385
|
+
const headerTokens = approxTokenLen3(`${HEADER}
|
|
35310
37386
|
`);
|
|
35311
|
-
const sepTokens =
|
|
37387
|
+
const sepTokens = approxTokenLen3("\n");
|
|
35312
37388
|
let runningTokens = headerTokens;
|
|
35313
37389
|
let runningLines = [];
|
|
35314
37390
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
35315
37391
|
const line = lines[i];
|
|
35316
|
-
const tokens =
|
|
37392
|
+
const tokens = approxTokenLen3(line) + (runningLines.length > 0 ? sepTokens : 0);
|
|
35317
37393
|
if (runningTokens + tokens > this.historyTokenBudget) break;
|
|
35318
37394
|
runningTokens += tokens;
|
|
35319
37395
|
runningLines.push(line);
|
|
@@ -35341,7 +37417,7 @@ ${runningLines.join("\n")}`;
|
|
|
35341
37417
|
function chatStorageKey(surface, threadKey) {
|
|
35342
37418
|
return `${surface}.${threadKey}`;
|
|
35343
37419
|
}
|
|
35344
|
-
var OPERATOR_CHAT_NAMESPACE,
|
|
37420
|
+
var OPERATOR_CHAT_NAMESPACE, HKDF_INFO3, OperatorChatStore;
|
|
35345
37421
|
var init_operator_chat_store = __esm({
|
|
35346
37422
|
"src/chat/operator-chat-store.ts"() {
|
|
35347
37423
|
init_encryption();
|
|
@@ -35349,13 +37425,13 @@ var init_operator_chat_store = __esm({
|
|
|
35349
37425
|
init_encoding();
|
|
35350
37426
|
init_operator_chat_types();
|
|
35351
37427
|
OPERATOR_CHAT_NAMESPACE = "_chat";
|
|
35352
|
-
|
|
37428
|
+
HKDF_INFO3 = "operator-chat-store-v1";
|
|
35353
37429
|
OperatorChatStore = class {
|
|
35354
37430
|
storage;
|
|
35355
37431
|
encryptionKey;
|
|
35356
37432
|
constructor(storage, masterKey) {
|
|
35357
37433
|
this.storage = storage;
|
|
35358
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
37434
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO3);
|
|
35359
37435
|
}
|
|
35360
37436
|
/**
|
|
35361
37437
|
* Load a thread. Returns null if no record exists or if the on-disk
|
|
@@ -35440,7 +37516,7 @@ var init_operator_chat_store = __esm({
|
|
|
35440
37516
|
function bundleKey(threadId) {
|
|
35441
37517
|
return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
|
|
35442
37518
|
}
|
|
35443
|
-
function
|
|
37519
|
+
function stripKeyPrefix3(key) {
|
|
35444
37520
|
if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
|
|
35445
37521
|
return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
|
|
35446
37522
|
}
|
|
@@ -35451,7 +37527,7 @@ function lastTurnId(bundle) {
|
|
|
35451
37527
|
}
|
|
35452
37528
|
return max;
|
|
35453
37529
|
}
|
|
35454
|
-
var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX,
|
|
37530
|
+
var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX, HKDF_INFO4, DEFAULT_CONCIERGE_RETENTION_DAYS, MAX_BUNDLE_BYTES3, ConciergeMemoryStore;
|
|
35455
37531
|
var init_concierge_memory_store = __esm({
|
|
35456
37532
|
"src/chat/concierge-memory-store.ts"() {
|
|
35457
37533
|
init_encryption();
|
|
@@ -35459,7 +37535,7 @@ var init_concierge_memory_store = __esm({
|
|
|
35459
37535
|
init_encoding();
|
|
35460
37536
|
CONCIERGE_MEMORY_NAMESPACE = "_chat";
|
|
35461
37537
|
CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
|
|
35462
|
-
|
|
37538
|
+
HKDF_INFO4 = "concierge-memory-store-v1";
|
|
35463
37539
|
DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
|
|
35464
37540
|
MAX_BUNDLE_BYTES3 = 4 * 1024 * 1024;
|
|
35465
37541
|
ConciergeMemoryStore = class {
|
|
@@ -35470,7 +37546,7 @@ var init_concierge_memory_store = __esm({
|
|
|
35470
37546
|
locks;
|
|
35471
37547
|
constructor(opts) {
|
|
35472
37548
|
this.storage = opts.storage;
|
|
35473
|
-
this.encryptionKey = derivePurposeKey(opts.masterKey,
|
|
37549
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO4);
|
|
35474
37550
|
this.fortressId = opts.fortressId;
|
|
35475
37551
|
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
|
|
35476
37552
|
this.locks = /* @__PURE__ */ new Map();
|
|
@@ -35596,7 +37672,7 @@ var init_concierge_memory_store = __esm({
|
|
|
35596
37672
|
);
|
|
35597
37673
|
const summaries = [];
|
|
35598
37674
|
for (const meta of entries) {
|
|
35599
|
-
const threadId =
|
|
37675
|
+
const threadId = stripKeyPrefix3(meta.key);
|
|
35600
37676
|
if (threadId === null) continue;
|
|
35601
37677
|
const bundle = await this.loadBundle(threadId);
|
|
35602
37678
|
if (!bundle || bundle.turns.length === 0) continue;
|
|
@@ -35649,7 +37725,7 @@ var init_concierge_memory_store = __esm({
|
|
|
35649
37725
|
);
|
|
35650
37726
|
let pruned = 0;
|
|
35651
37727
|
for (const meta of entries) {
|
|
35652
|
-
const threadId =
|
|
37728
|
+
const threadId = stripKeyPrefix3(meta.key);
|
|
35653
37729
|
if (threadId === null) continue;
|
|
35654
37730
|
pruned += await this.withLock(threadId, async () => {
|
|
35655
37731
|
const bundle = await this.loadBundle(threadId);
|
|
@@ -36119,7 +38195,7 @@ var init_defaults = __esm({
|
|
|
36119
38195
|
});
|
|
36120
38196
|
|
|
36121
38197
|
// src/intelligence/policy-store.ts
|
|
36122
|
-
var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY,
|
|
38198
|
+
var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY, HKDF_INFO5, IntelligenceConfigStore;
|
|
36123
38199
|
var init_policy_store = __esm({
|
|
36124
38200
|
"src/intelligence/policy-store.ts"() {
|
|
36125
38201
|
init_encryption();
|
|
@@ -36128,13 +38204,13 @@ var init_policy_store = __esm({
|
|
|
36128
38204
|
init_defaults();
|
|
36129
38205
|
INTELLIGENCE_NAMESPACE = "_intelligence";
|
|
36130
38206
|
SUBSTRATE_CONFIG_KEY = "substrate-config";
|
|
36131
|
-
|
|
38207
|
+
HKDF_INFO5 = "intelligence-substrate-config";
|
|
36132
38208
|
IntelligenceConfigStore = class {
|
|
36133
38209
|
storage;
|
|
36134
38210
|
encryptionKey;
|
|
36135
38211
|
constructor(storage, masterKey) {
|
|
36136
38212
|
this.storage = storage;
|
|
36137
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
38213
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO5);
|
|
36138
38214
|
}
|
|
36139
38215
|
/**
|
|
36140
38216
|
* Load the operator's substrate config from disk. Returns the config
|
|
@@ -40085,6 +42161,38 @@ ${err.message}
|
|
|
40085
42161
|
if (dashboard) {
|
|
40086
42162
|
dashboard.setApprovalAggregator(approvalAggregator);
|
|
40087
42163
|
}
|
|
42164
|
+
const sentinelFindingStore = new SentinelFindingStore({
|
|
42165
|
+
storage,
|
|
42166
|
+
masterKey,
|
|
42167
|
+
fortressId: fortressIdForAggregator
|
|
42168
|
+
});
|
|
42169
|
+
const sentinelRegistry = new SentinelRegistry();
|
|
42170
|
+
for (const entry of PHI1_BASELINE_CATALOG) {
|
|
42171
|
+
sentinelRegistry.register(entry);
|
|
42172
|
+
}
|
|
42173
|
+
const sentinelDispatcher = new SentinelDispatcher({
|
|
42174
|
+
registry: sentinelRegistry,
|
|
42175
|
+
findingStore: sentinelFindingStore,
|
|
42176
|
+
auditLog,
|
|
42177
|
+
fortressId: fortressIdForAggregator,
|
|
42178
|
+
identityId: aggregatorIdentityId
|
|
42179
|
+
});
|
|
42180
|
+
try {
|
|
42181
|
+
const persistedSubscriptions = await loadSentinelSubscriptions(
|
|
42182
|
+
config.storage_path
|
|
42183
|
+
);
|
|
42184
|
+
for (const sentinelId of persistedSubscriptions) {
|
|
42185
|
+
try {
|
|
42186
|
+
await sentinelDispatcher.subscribeSentinel(sentinelId);
|
|
42187
|
+
} catch {
|
|
42188
|
+
}
|
|
42189
|
+
}
|
|
42190
|
+
} catch {
|
|
42191
|
+
}
|
|
42192
|
+
sentinelDispatcher.start();
|
|
42193
|
+
if (dashboard) {
|
|
42194
|
+
dashboard.setSentinelDispatcher(sentinelDispatcher);
|
|
42195
|
+
}
|
|
40088
42196
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
40089
42197
|
const { tools: sanctuaryMetaTools } = createSanctuaryTools({
|
|
40090
42198
|
config,
|
|
@@ -40278,6 +42386,11 @@ var init_src = __esm({
|
|
|
40278
42386
|
init_approval_aggregator();
|
|
40279
42387
|
init_aggregator_backed_channel();
|
|
40280
42388
|
init_aggregator_store();
|
|
42389
|
+
init_sentinel_finding_store();
|
|
42390
|
+
init_sentinel_registry();
|
|
42391
|
+
init_sentinel_dispatcher();
|
|
42392
|
+
init_sentinels();
|
|
42393
|
+
init_subscription_store();
|
|
40281
42394
|
init_tools4();
|
|
40282
42395
|
init_router();
|
|
40283
42396
|
init_router();
|
|
@@ -45447,6 +47560,232 @@ var init_intelligence = __esm({
|
|
|
45447
47560
|
}
|
|
45448
47561
|
});
|
|
45449
47562
|
|
|
47563
|
+
// src/cli/sentinel.ts
|
|
47564
|
+
var sentinel_exports = {};
|
|
47565
|
+
__export(sentinel_exports, {
|
|
47566
|
+
runSentinelCommand: () => runSentinelCommand
|
|
47567
|
+
});
|
|
47568
|
+
async function runSentinelCommand(args) {
|
|
47569
|
+
const out = args.out ?? process.stdout;
|
|
47570
|
+
const err = args.err ?? process.stderr;
|
|
47571
|
+
const [sub, ...rest] = args.argv;
|
|
47572
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
47573
|
+
printUsage6(out);
|
|
47574
|
+
return 0;
|
|
47575
|
+
}
|
|
47576
|
+
try {
|
|
47577
|
+
switch (sub) {
|
|
47578
|
+
case "list":
|
|
47579
|
+
return cmdList4(out);
|
|
47580
|
+
case "list-subscribed":
|
|
47581
|
+
return await cmdListSubscribed(rest, { out, err, args });
|
|
47582
|
+
case "subscribe":
|
|
47583
|
+
return await cmdSubscribe(rest, { out, err, args });
|
|
47584
|
+
case "unsubscribe":
|
|
47585
|
+
return await cmdUnsubscribe(rest, { out, err, args });
|
|
47586
|
+
case "findings":
|
|
47587
|
+
return await cmdFindings(rest, { out, err, args });
|
|
47588
|
+
default:
|
|
47589
|
+
err.write(`Unknown subcommand: ${sub}
|
|
47590
|
+
`);
|
|
47591
|
+
printUsage6(err);
|
|
47592
|
+
return 2;
|
|
47593
|
+
}
|
|
47594
|
+
} catch (e) {
|
|
47595
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
47596
|
+
err.write(`sanctuary sentinel: ${msg}
|
|
47597
|
+
`);
|
|
47598
|
+
return 1;
|
|
47599
|
+
}
|
|
47600
|
+
}
|
|
47601
|
+
function printUsage6(s) {
|
|
47602
|
+
s.write(`Usage: sanctuary sentinel <command> [args]
|
|
47603
|
+
|
|
47604
|
+
list Show the Phi-1 catalog of available
|
|
47605
|
+
sentinels (egress-volume only at v1.3
|
|
47606
|
+
Phi-1; more land in Phi-2 ... Phi-5).
|
|
47607
|
+
list-subscribed Show which sentinels this fortress has
|
|
47608
|
+
opted into. Loads from
|
|
47609
|
+
<storage>/sentinel-subscriptions.json.
|
|
47610
|
+
subscribe <sentinel-id> Opt in. Writes the subscription file.
|
|
47611
|
+
The server picks it up on next boot.
|
|
47612
|
+
unsubscribe <sentinel-id> Opt out.
|
|
47613
|
+
findings [opts] Read recent findings. Decrypts the
|
|
47614
|
+
sentinel findings store (uses the
|
|
47615
|
+
same passphrase as the cocoon master
|
|
47616
|
+
key).
|
|
47617
|
+
--since <iso> Filter observed_at >= iso.
|
|
47618
|
+
--severity <info|warn|alert> Filter by severity.
|
|
47619
|
+
--sentinel-id <id> Filter by emitting sentinel.
|
|
47620
|
+
--agent-id <id> Filter by agent attribution.
|
|
47621
|
+
--limit <n> Cap result count (default 100).
|
|
47622
|
+
`);
|
|
47623
|
+
}
|
|
47624
|
+
function cmdList4(out) {
|
|
47625
|
+
for (const entry of PHI1_BASELINE_CATALOG) {
|
|
47626
|
+
out.write(`${entry.sentinelId}
|
|
47627
|
+
${entry.description}
|
|
47628
|
+
`);
|
|
47629
|
+
}
|
|
47630
|
+
if (PHI1_BASELINE_CATALOG.length === 0) {
|
|
47631
|
+
out.write("(no sentinels registered)\n");
|
|
47632
|
+
}
|
|
47633
|
+
return 0;
|
|
47634
|
+
}
|
|
47635
|
+
async function cmdListSubscribed(argv, ctx) {
|
|
47636
|
+
const storagePath = await resolveStoragePath2(ctx.args);
|
|
47637
|
+
const subscribed = await loadSentinelSubscriptions(storagePath);
|
|
47638
|
+
if (subscribed.size === 0) {
|
|
47639
|
+
ctx.out.write("(no subscriptions)\n");
|
|
47640
|
+
return 0;
|
|
47641
|
+
}
|
|
47642
|
+
for (const id of [...subscribed].sort()) {
|
|
47643
|
+
ctx.out.write(`${id}
|
|
47644
|
+
`);
|
|
47645
|
+
}
|
|
47646
|
+
return 0;
|
|
47647
|
+
}
|
|
47648
|
+
async function cmdSubscribe(argv, ctx) {
|
|
47649
|
+
const sentinelId = argv[0];
|
|
47650
|
+
if (!sentinelId) {
|
|
47651
|
+
ctx.err.write("subscribe requires a sentinel-id\n");
|
|
47652
|
+
return 2;
|
|
47653
|
+
}
|
|
47654
|
+
const known = PHI1_BASELINE_CATALOG.find(
|
|
47655
|
+
(entry) => entry.sentinelId === sentinelId
|
|
47656
|
+
);
|
|
47657
|
+
if (!known) {
|
|
47658
|
+
ctx.err.write(`Unknown sentinel: ${sentinelId}
|
|
47659
|
+
`);
|
|
47660
|
+
return 2;
|
|
47661
|
+
}
|
|
47662
|
+
const storagePath = await resolveStoragePath2(ctx.args);
|
|
47663
|
+
const subscribed = await loadSentinelSubscriptions(storagePath);
|
|
47664
|
+
if (subscribed.has(sentinelId)) {
|
|
47665
|
+
ctx.out.write(`Already subscribed: ${sentinelId}
|
|
47666
|
+
`);
|
|
47667
|
+
return 0;
|
|
47668
|
+
}
|
|
47669
|
+
subscribed.add(sentinelId);
|
|
47670
|
+
await saveSentinelSubscriptions(storagePath, subscribed);
|
|
47671
|
+
ctx.out.write(
|
|
47672
|
+
`Subscribed: ${sentinelId}
|
|
47673
|
+
Restart Sanctuary or wait for the next dispatcher tick to begin evaluation.
|
|
47674
|
+
`
|
|
47675
|
+
);
|
|
47676
|
+
return 0;
|
|
47677
|
+
}
|
|
47678
|
+
async function cmdUnsubscribe(argv, ctx) {
|
|
47679
|
+
const sentinelId = argv[0];
|
|
47680
|
+
if (!sentinelId) {
|
|
47681
|
+
ctx.err.write("unsubscribe requires a sentinel-id\n");
|
|
47682
|
+
return 2;
|
|
47683
|
+
}
|
|
47684
|
+
const storagePath = await resolveStoragePath2(ctx.args);
|
|
47685
|
+
const subscribed = await loadSentinelSubscriptions(storagePath);
|
|
47686
|
+
if (!subscribed.has(sentinelId)) {
|
|
47687
|
+
ctx.out.write(`Not subscribed: ${sentinelId}
|
|
47688
|
+
`);
|
|
47689
|
+
return 0;
|
|
47690
|
+
}
|
|
47691
|
+
subscribed.delete(sentinelId);
|
|
47692
|
+
await saveSentinelSubscriptions(storagePath, subscribed);
|
|
47693
|
+
ctx.out.write(`Unsubscribed: ${sentinelId}
|
|
47694
|
+
`);
|
|
47695
|
+
return 0;
|
|
47696
|
+
}
|
|
47697
|
+
async function cmdFindings(argv, ctx) {
|
|
47698
|
+
const filters = parseFindingFilters(argv);
|
|
47699
|
+
const storagePath = await resolveStoragePath2(ctx.args);
|
|
47700
|
+
const storage = new FilesystemStorage(`${storagePath}/state`);
|
|
47701
|
+
let passphrase = ctx.args.passphrase ?? process.env["SANCTUARY_PASSPHRASE"];
|
|
47702
|
+
if (!passphrase) {
|
|
47703
|
+
const resolved = await getOrCreatePassphrase();
|
|
47704
|
+
passphrase = resolved.value;
|
|
47705
|
+
}
|
|
47706
|
+
let existingParams;
|
|
47707
|
+
try {
|
|
47708
|
+
const raw = await storage.read("_meta", "key-params");
|
|
47709
|
+
if (raw) existingParams = JSON.parse(bytesToString(raw));
|
|
47710
|
+
} catch {
|
|
47711
|
+
}
|
|
47712
|
+
const { key: masterKey, params } = await deriveMasterKey(
|
|
47713
|
+
passphrase,
|
|
47714
|
+
existingParams
|
|
47715
|
+
);
|
|
47716
|
+
if (!existingParams) {
|
|
47717
|
+
await storage.write(
|
|
47718
|
+
"_meta",
|
|
47719
|
+
"key-params",
|
|
47720
|
+
stringToBytes(JSON.stringify(params))
|
|
47721
|
+
);
|
|
47722
|
+
}
|
|
47723
|
+
const fortressId = fortressIdFromStoragePath(storagePath);
|
|
47724
|
+
const store = new SentinelFindingStore({
|
|
47725
|
+
storage,
|
|
47726
|
+
masterKey,
|
|
47727
|
+
fortressId
|
|
47728
|
+
});
|
|
47729
|
+
const findings = await store.listFindings({
|
|
47730
|
+
limit: filters.limit ?? 100,
|
|
47731
|
+
...filters.since !== void 0 ? { since: filters.since } : {},
|
|
47732
|
+
...filters.severity !== void 0 ? { severity: filters.severity } : {},
|
|
47733
|
+
...filters.sentinelId !== void 0 ? { sentinelId: filters.sentinelId } : {},
|
|
47734
|
+
...filters.agentId !== void 0 ? { agentId: filters.agentId } : {}
|
|
47735
|
+
});
|
|
47736
|
+
if (findings.length === 0) {
|
|
47737
|
+
ctx.out.write("(no findings)\n");
|
|
47738
|
+
return 0;
|
|
47739
|
+
}
|
|
47740
|
+
for (const finding of findings) {
|
|
47741
|
+
ctx.out.write(
|
|
47742
|
+
`[${finding.observed_at}] ${finding.severity.toUpperCase()} ${finding.sentinel_id}${finding.agent_id ? ` (agent ${finding.agent_id})` : ""}: ${finding.summary}
|
|
47743
|
+
`
|
|
47744
|
+
);
|
|
47745
|
+
}
|
|
47746
|
+
return 0;
|
|
47747
|
+
}
|
|
47748
|
+
function parseFindingFilters(argv) {
|
|
47749
|
+
const filters = {};
|
|
47750
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
47751
|
+
const arg = argv[i];
|
|
47752
|
+
if (arg === "--since" && argv[i + 1]) {
|
|
47753
|
+
filters.since = argv[++i];
|
|
47754
|
+
} else if (arg === "--severity" && argv[i + 1]) {
|
|
47755
|
+
const next = argv[++i];
|
|
47756
|
+
if (next === "info" || next === "warn" || next === "alert") {
|
|
47757
|
+
filters.severity = next;
|
|
47758
|
+
}
|
|
47759
|
+
} else if (arg === "--sentinel-id" && argv[i + 1]) {
|
|
47760
|
+
filters.sentinelId = argv[++i];
|
|
47761
|
+
} else if (arg === "--agent-id" && argv[i + 1]) {
|
|
47762
|
+
filters.agentId = argv[++i];
|
|
47763
|
+
} else if (arg === "--limit" && argv[i + 1]) {
|
|
47764
|
+
const n = Number.parseInt(argv[++i], 10);
|
|
47765
|
+
if (!Number.isNaN(n) && n > 0) filters.limit = n;
|
|
47766
|
+
}
|
|
47767
|
+
}
|
|
47768
|
+
return filters;
|
|
47769
|
+
}
|
|
47770
|
+
async function resolveStoragePath2(args) {
|
|
47771
|
+
if (args.storagePath) return args.storagePath;
|
|
47772
|
+
const config = await loadConfig();
|
|
47773
|
+
return config.storage_path;
|
|
47774
|
+
}
|
|
47775
|
+
var init_sentinel2 = __esm({
|
|
47776
|
+
"src/cli/sentinel.ts"() {
|
|
47777
|
+
init_config();
|
|
47778
|
+
init_filesystem();
|
|
47779
|
+
init_key_derivation();
|
|
47780
|
+
init_encoding();
|
|
47781
|
+
init_passphrase();
|
|
47782
|
+
init_wiring();
|
|
47783
|
+
init_sentinel_finding_store();
|
|
47784
|
+
init_subscription_store();
|
|
47785
|
+
init_sentinels();
|
|
47786
|
+
}
|
|
47787
|
+
});
|
|
47788
|
+
|
|
45450
47789
|
// src/mcp/broker-server.ts
|
|
45451
47790
|
var broker_server_exports = {};
|
|
45452
47791
|
__export(broker_server_exports, {
|
|
@@ -45542,7 +47881,7 @@ function createBrokerMcpServer(broker, opts) {
|
|
|
45542
47881
|
case "broker/request_token": {
|
|
45543
47882
|
const skill = requireString(args, "skill");
|
|
45544
47883
|
const secret = requireString(args, "secret");
|
|
45545
|
-
const scopeRaw =
|
|
47884
|
+
const scopeRaw = optionalString2(args, "scope");
|
|
45546
47885
|
const scope = scopeRaw === "rotate" ? "rotate" : scopeRaw === "read" ? "read" : void 0;
|
|
45547
47886
|
const ttl = optionalNumber(args, "ttl_seconds");
|
|
45548
47887
|
const binding = await broker.issueToken({
|
|
@@ -45574,7 +47913,7 @@ function createBrokerMcpServer(broker, opts) {
|
|
|
45574
47913
|
return ok({ grants });
|
|
45575
47914
|
}
|
|
45576
47915
|
case "broker/audit_query": {
|
|
45577
|
-
const since =
|
|
47916
|
+
const since = optionalString2(args, "since");
|
|
45578
47917
|
const limit = optionalNumber(args, "limit");
|
|
45579
47918
|
const summary = await broker.queryAudit({ since, limit });
|
|
45580
47919
|
return ok(summary);
|
|
@@ -45599,7 +47938,7 @@ function requireString(args, key) {
|
|
|
45599
47938
|
}
|
|
45600
47939
|
return v;
|
|
45601
47940
|
}
|
|
45602
|
-
function
|
|
47941
|
+
function optionalString2(args, key) {
|
|
45603
47942
|
const v = args[key];
|
|
45604
47943
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
45605
47944
|
}
|
|
@@ -46306,6 +48645,11 @@ async function main() {
|
|
|
46306
48645
|
const code = await runIntelligenceCommand2({ argv: args.slice(1) });
|
|
46307
48646
|
process.exit(code);
|
|
46308
48647
|
}
|
|
48648
|
+
if (args[0] === "sentinel") {
|
|
48649
|
+
const { runSentinelCommand: runSentinelCommand2 } = await Promise.resolve().then(() => (init_sentinel2(), sentinel_exports));
|
|
48650
|
+
const code = await runSentinelCommand2({ argv: args.slice(1) });
|
|
48651
|
+
process.exit(code);
|
|
48652
|
+
}
|
|
46309
48653
|
if (args[0] === "broker-server") {
|
|
46310
48654
|
const { openBroker: openBroker2 } = await Promise.resolve().then(() => (init_open(), open_exports));
|
|
46311
48655
|
const { createBrokerMcpServer: createBrokerMcpServer2 } = await Promise.resolve().then(() => (init_broker_server(), broker_server_exports));
|