@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.js
CHANGED
|
@@ -17354,6 +17354,125 @@ var init_approval_aggregator_routes = __esm({
|
|
|
17354
17354
|
APPROVAL_INBOX_MAX_LIMIT = 200;
|
|
17355
17355
|
}
|
|
17356
17356
|
});
|
|
17357
|
+
|
|
17358
|
+
// src/sentinel/sentinel-routes.ts
|
|
17359
|
+
function writeJSON5(res, status, payload) {
|
|
17360
|
+
res.writeHead(status, {
|
|
17361
|
+
"Content-Type": "application/json",
|
|
17362
|
+
"Cache-Control": "no-store"
|
|
17363
|
+
});
|
|
17364
|
+
res.end(JSON.stringify(payload));
|
|
17365
|
+
}
|
|
17366
|
+
function isSeverity(value) {
|
|
17367
|
+
return value === "info" || value === "warn" || value === "alert";
|
|
17368
|
+
}
|
|
17369
|
+
function parseLimit3(raw, defaultValue, max) {
|
|
17370
|
+
if (raw === null || raw === "") return defaultValue;
|
|
17371
|
+
const parsed = Number.parseInt(raw, 10);
|
|
17372
|
+
if (Number.isNaN(parsed) || parsed < 0) return defaultValue;
|
|
17373
|
+
return Math.min(parsed, max);
|
|
17374
|
+
}
|
|
17375
|
+
function matchSubscribeRoute(path) {
|
|
17376
|
+
const prefix = `${SENTINEL_API_PREFIX}/`;
|
|
17377
|
+
if (!path.startsWith(prefix)) return null;
|
|
17378
|
+
const rest = path.slice(prefix.length);
|
|
17379
|
+
if (!rest.endsWith("/subscribe")) return null;
|
|
17380
|
+
const sentinelId = rest.slice(0, rest.length - "/subscribe".length);
|
|
17381
|
+
if (sentinelId.length === 0) return null;
|
|
17382
|
+
return { sentinelId: decodeURIComponent(sentinelId) };
|
|
17383
|
+
}
|
|
17384
|
+
async function handleSentinelRoute(deps, req, res) {
|
|
17385
|
+
const host = req.headers.host || "localhost";
|
|
17386
|
+
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
17387
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
17388
|
+
const path = url.pathname;
|
|
17389
|
+
if (path !== SENTINEL_API_PREFIX && !path.startsWith(`${SENTINEL_API_PREFIX}/`)) {
|
|
17390
|
+
return false;
|
|
17391
|
+
}
|
|
17392
|
+
const checkAuth = authMiddleware(deps.authConfig);
|
|
17393
|
+
if (!checkAuth(req, res, url)) return true;
|
|
17394
|
+
const dispatcher = deps.dispatcher;
|
|
17395
|
+
const registry = dispatcher.getRegistry();
|
|
17396
|
+
const findingStore = dispatcher.getFindingStore();
|
|
17397
|
+
try {
|
|
17398
|
+
if (method === "GET" && path === SENTINEL_API_PREFIX) {
|
|
17399
|
+
const catalog = registry.listCatalog();
|
|
17400
|
+
writeJSON5(res, 200, { ok: true, data: { catalog } });
|
|
17401
|
+
return true;
|
|
17402
|
+
}
|
|
17403
|
+
if (method === "GET" && path === `${SENTINEL_API_PREFIX}/subscribed`) {
|
|
17404
|
+
const subscribed = registry.listSubscribed();
|
|
17405
|
+
writeJSON5(res, 200, { ok: true, data: { subscribed } });
|
|
17406
|
+
return true;
|
|
17407
|
+
}
|
|
17408
|
+
if (method === "GET" && path === `${SENTINEL_API_PREFIX}/findings`) {
|
|
17409
|
+
const limit = parseLimit3(
|
|
17410
|
+
url.searchParams.get("limit"),
|
|
17411
|
+
FINDINGS_DEFAULT_LIMIT,
|
|
17412
|
+
FINDINGS_MAX_LIMIT
|
|
17413
|
+
);
|
|
17414
|
+
const since = url.searchParams.get("since") ?? void 0;
|
|
17415
|
+
const severityRaw = url.searchParams.get("severity") ?? void 0;
|
|
17416
|
+
const sentinelIdFilter = url.searchParams.get("sentinel_id") ?? void 0;
|
|
17417
|
+
const agentIdFilter = url.searchParams.get("agent_id") ?? void 0;
|
|
17418
|
+
const severity = severityRaw && isSeverity(severityRaw) ? severityRaw : void 0;
|
|
17419
|
+
const findings = await findingStore.listFindings({
|
|
17420
|
+
limit,
|
|
17421
|
+
...since !== void 0 ? { since } : {},
|
|
17422
|
+
...severity !== void 0 ? { severity } : {},
|
|
17423
|
+
...sentinelIdFilter !== void 0 ? { sentinelId: sentinelIdFilter } : {},
|
|
17424
|
+
...agentIdFilter !== void 0 ? { agentId: agentIdFilter } : {}
|
|
17425
|
+
});
|
|
17426
|
+
writeJSON5(res, 200, { ok: true, data: { findings } });
|
|
17427
|
+
return true;
|
|
17428
|
+
}
|
|
17429
|
+
const subscribeMatch = matchSubscribeRoute(path);
|
|
17430
|
+
if (subscribeMatch) {
|
|
17431
|
+
if (method === "POST") {
|
|
17432
|
+
try {
|
|
17433
|
+
await dispatcher.subscribeSentinel(subscribeMatch.sentinelId);
|
|
17434
|
+
writeJSON5(res, 200, {
|
|
17435
|
+
ok: true,
|
|
17436
|
+
data: { sentinel_id: subscribeMatch.sentinelId, subscribed: true }
|
|
17437
|
+
});
|
|
17438
|
+
} catch (err) {
|
|
17439
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17440
|
+
if (msg.startsWith("sentinel-registry: unknown sentinel")) {
|
|
17441
|
+
writeJSON5(res, 404, { ok: false, error: "not_found" });
|
|
17442
|
+
} else {
|
|
17443
|
+
writeJSON5(res, 500, { ok: false, error: "internal", detail: msg });
|
|
17444
|
+
}
|
|
17445
|
+
}
|
|
17446
|
+
return true;
|
|
17447
|
+
}
|
|
17448
|
+
if (method === "DELETE") {
|
|
17449
|
+
const removed = await dispatcher.unsubscribeSentinel(
|
|
17450
|
+
subscribeMatch.sentinelId
|
|
17451
|
+
);
|
|
17452
|
+
writeJSON5(res, 200, {
|
|
17453
|
+
ok: true,
|
|
17454
|
+
data: { sentinel_id: subscribeMatch.sentinelId, subscribed: false, removed }
|
|
17455
|
+
});
|
|
17456
|
+
return true;
|
|
17457
|
+
}
|
|
17458
|
+
}
|
|
17459
|
+
writeJSON5(res, 404, { ok: false, error: "not_found", path });
|
|
17460
|
+
return true;
|
|
17461
|
+
} catch (err) {
|
|
17462
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17463
|
+
writeJSON5(res, 500, { ok: false, error: "internal", detail: msg });
|
|
17464
|
+
return true;
|
|
17465
|
+
}
|
|
17466
|
+
}
|
|
17467
|
+
var SENTINEL_API_PREFIX, FINDINGS_DEFAULT_LIMIT, FINDINGS_MAX_LIMIT;
|
|
17468
|
+
var init_sentinel_routes = __esm({
|
|
17469
|
+
"src/sentinel/sentinel-routes.ts"() {
|
|
17470
|
+
init_auth_middleware();
|
|
17471
|
+
SENTINEL_API_PREFIX = "/api/sentinels";
|
|
17472
|
+
FINDINGS_DEFAULT_LIMIT = 100;
|
|
17473
|
+
FINDINGS_MAX_LIMIT = 500;
|
|
17474
|
+
}
|
|
17475
|
+
});
|
|
17357
17476
|
function isDashboardViewRoute(method, path) {
|
|
17358
17477
|
if (method !== "GET") return false;
|
|
17359
17478
|
return path === "/" || path === "/dashboard" || path === "/v1.0" || path === "/fortress" || path === "/events";
|
|
@@ -17368,6 +17487,7 @@ var init_dashboard = __esm({
|
|
|
17368
17487
|
init_system_prompt_generator();
|
|
17369
17488
|
init_dispatch();
|
|
17370
17489
|
init_approval_aggregator_routes();
|
|
17490
|
+
init_sentinel_routes();
|
|
17371
17491
|
SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
17372
17492
|
SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
|
|
17373
17493
|
MAX_SESSIONS = 1e3;
|
|
@@ -17435,6 +17555,13 @@ var init_dashboard = __esm({
|
|
|
17435
17555
|
* the operator-facing query / decision surface.
|
|
17436
17556
|
*/
|
|
17437
17557
|
approvalAggregator = null;
|
|
17558
|
+
/**
|
|
17559
|
+
* v1.3 WP-V1.3-1 Phi-1 Sentinel dispatcher. Mounted additively at
|
|
17560
|
+
* `/api/sentinels/*` when set. Sentinel surface is read-only against
|
|
17561
|
+
* the audit log; subscribe/unsubscribe writes flow through the
|
|
17562
|
+
* dispatcher's audited paths.
|
|
17563
|
+
*/
|
|
17564
|
+
sentinelDispatcher = null;
|
|
17438
17565
|
constructor(config) {
|
|
17439
17566
|
this.config = config;
|
|
17440
17567
|
this.authToken = config.auth_token;
|
|
@@ -17494,6 +17621,14 @@ var init_dashboard = __esm({
|
|
|
17494
17621
|
setApprovalAggregator(aggregator) {
|
|
17495
17622
|
this.approvalAggregator = aggregator;
|
|
17496
17623
|
}
|
|
17624
|
+
/**
|
|
17625
|
+
* v1.3 WP-V1.3-1 Phi-1: bind the Sentinel dispatcher. Once set,
|
|
17626
|
+
* requests to `/api/sentinels/*` route through `handleSentinelRoute`.
|
|
17627
|
+
* Pass `null` to detach (used by tests + during shutdown).
|
|
17628
|
+
*/
|
|
17629
|
+
setSentinelDispatcher(dispatcher) {
|
|
17630
|
+
this.sentinelDispatcher = dispatcher;
|
|
17631
|
+
}
|
|
17497
17632
|
/**
|
|
17498
17633
|
* v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
|
|
17499
17634
|
* before the legacy approval route table. Returns true when served.
|
|
@@ -17513,6 +17648,25 @@ var init_dashboard = __esm({
|
|
|
17513
17648
|
res
|
|
17514
17649
|
);
|
|
17515
17650
|
}
|
|
17651
|
+
/**
|
|
17652
|
+
* v1.3 WP-V1.3-1 Phi-1 dispatch entry point. Routes `/api/sentinels/*`
|
|
17653
|
+
* requests through the sentinel router when a dispatcher has been
|
|
17654
|
+
* bound. Returns true when served.
|
|
17655
|
+
*/
|
|
17656
|
+
async dispatchSentinel(req, res) {
|
|
17657
|
+
if (!this.sentinelDispatcher) return false;
|
|
17658
|
+
return handleSentinelRoute(
|
|
17659
|
+
{
|
|
17660
|
+
authConfig: {
|
|
17661
|
+
loopbackAutoAuth: this._autoAuthLocalhost,
|
|
17662
|
+
...this.authToken !== void 0 ? { authToken: this.authToken } : {}
|
|
17663
|
+
},
|
|
17664
|
+
dispatcher: this.sentinelDispatcher
|
|
17665
|
+
},
|
|
17666
|
+
req,
|
|
17667
|
+
res
|
|
17668
|
+
);
|
|
17669
|
+
}
|
|
17516
17670
|
/**
|
|
17517
17671
|
* v1.1 dispatch entry point. Called from `handleRequest` before the
|
|
17518
17672
|
* legacy route table. Returns true when the request was served by v1.1
|
|
@@ -17900,6 +18054,18 @@ var init_dashboard = __esm({
|
|
|
17900
18054
|
});
|
|
17901
18055
|
return;
|
|
17902
18056
|
}
|
|
18057
|
+
if (this.sentinelDispatcher && url.pathname.startsWith(SENTINEL_API_PREFIX)) {
|
|
18058
|
+
this.dispatchSentinel(req, res).then((handled) => {
|
|
18059
|
+
if (handled) return;
|
|
18060
|
+
this.handleLegacyRequest(req, res, url, method);
|
|
18061
|
+
}).catch(() => {
|
|
18062
|
+
if (!res.headersSent) {
|
|
18063
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
18064
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
18065
|
+
}
|
|
18066
|
+
});
|
|
18067
|
+
return;
|
|
18068
|
+
}
|
|
17903
18069
|
if (this.v11Bindings) {
|
|
17904
18070
|
this.dispatchV11(req, res, url, method).then((handled) => {
|
|
17905
18071
|
if (handled) return;
|
|
@@ -21240,47 +21406,1766 @@ var init_aggregator_store = __esm({
|
|
|
21240
21406
|
} catch {
|
|
21241
21407
|
return false;
|
|
21242
21408
|
}
|
|
21243
|
-
return true;
|
|
21409
|
+
return true;
|
|
21410
|
+
}
|
|
21411
|
+
/**
|
|
21412
|
+
* Drop expired payload bundles. Returns the count of bundles pruned.
|
|
21413
|
+
* Caller wires this into the cocoon-unlock initialization path.
|
|
21414
|
+
*/
|
|
21415
|
+
async pruneExpired(now) {
|
|
21416
|
+
const cutoff = (now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
21417
|
+
const entries = await this.storage.list(
|
|
21418
|
+
AGGREGATOR_PAYLOAD_NAMESPACE,
|
|
21419
|
+
AGGREGATOR_PAYLOAD_KEY_PREFIX
|
|
21420
|
+
);
|
|
21421
|
+
let pruned = 0;
|
|
21422
|
+
for (const meta of entries) {
|
|
21423
|
+
const aggregatorId = stripKeyPrefix(meta.key);
|
|
21424
|
+
if (aggregatorId === null) continue;
|
|
21425
|
+
const raw = await this.storage.read(
|
|
21426
|
+
AGGREGATOR_PAYLOAD_NAMESPACE,
|
|
21427
|
+
meta.key
|
|
21428
|
+
);
|
|
21429
|
+
if (!raw) continue;
|
|
21430
|
+
try {
|
|
21431
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
21432
|
+
const aad = stringToBytes(aggregatorId);
|
|
21433
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
21434
|
+
const parsed = JSON.parse(
|
|
21435
|
+
bytesToString(plaintext)
|
|
21436
|
+
);
|
|
21437
|
+
if (parsed.retention_until <= cutoff) {
|
|
21438
|
+
await this.storage.delete(AGGREGATOR_PAYLOAD_NAMESPACE, meta.key);
|
|
21439
|
+
pruned += 1;
|
|
21440
|
+
}
|
|
21441
|
+
} catch {
|
|
21442
|
+
}
|
|
21443
|
+
}
|
|
21444
|
+
return { pruned };
|
|
21445
|
+
}
|
|
21446
|
+
};
|
|
21447
|
+
}
|
|
21448
|
+
});
|
|
21449
|
+
|
|
21450
|
+
// src/sentinel/types.ts
|
|
21451
|
+
function isProxyCallAuditEntry(entry) {
|
|
21452
|
+
return entry.operation.startsWith(
|
|
21453
|
+
SENTINEL_OBSERVED_AUDIT_OPS.PROXY_CALL_PREFIX
|
|
21454
|
+
);
|
|
21455
|
+
}
|
|
21456
|
+
function proxyServerFromAuditEntry(entry) {
|
|
21457
|
+
if (!isProxyCallAuditEntry(entry)) return null;
|
|
21458
|
+
const details = entry.details;
|
|
21459
|
+
if (!details) return null;
|
|
21460
|
+
const server = details["server"];
|
|
21461
|
+
if (typeof server !== "string" || server.length === 0) return null;
|
|
21462
|
+
return server;
|
|
21463
|
+
}
|
|
21464
|
+
var SENTINEL_SUMMARY_MAX_CHARS, SENTINEL_AUDIT_OPS, SENTINEL_OBSERVED_AUDIT_OPS;
|
|
21465
|
+
var init_types3 = __esm({
|
|
21466
|
+
"src/sentinel/types.ts"() {
|
|
21467
|
+
SENTINEL_SUMMARY_MAX_CHARS = 240;
|
|
21468
|
+
SENTINEL_AUDIT_OPS = {
|
|
21469
|
+
SUBSCRIBED: "sentinel_subscribed",
|
|
21470
|
+
UNSUBSCRIBED: "sentinel_unsubscribed",
|
|
21471
|
+
FINDING_EMITTED: "sentinel_finding_emitted",
|
|
21472
|
+
EVALUATION_FAILED: "sentinel_evaluation_failed"
|
|
21473
|
+
};
|
|
21474
|
+
SENTINEL_OBSERVED_AUDIT_OPS = {
|
|
21475
|
+
/** Proxy router emits this on every outbound call (success or failure). */
|
|
21476
|
+
PROXY_CALL_PREFIX: "proxy_call:"
|
|
21477
|
+
};
|
|
21478
|
+
}
|
|
21479
|
+
});
|
|
21480
|
+
|
|
21481
|
+
// src/sentinel/sentinel-finding-store.ts
|
|
21482
|
+
function findingKey(findingId) {
|
|
21483
|
+
return `${SENTINEL_FINDING_KEY_PREFIX}${findingId}`;
|
|
21484
|
+
}
|
|
21485
|
+
function stripKeyPrefix2(key) {
|
|
21486
|
+
if (!key.startsWith(SENTINEL_FINDING_KEY_PREFIX)) return null;
|
|
21487
|
+
return key.slice(SENTINEL_FINDING_KEY_PREFIX.length);
|
|
21488
|
+
}
|
|
21489
|
+
function truncateSummary(summary) {
|
|
21490
|
+
if (summary.length <= SENTINEL_SUMMARY_MAX_CHARS) return summary;
|
|
21491
|
+
return `${summary.slice(0, SENTINEL_SUMMARY_MAX_CHARS - 3)}...`;
|
|
21492
|
+
}
|
|
21493
|
+
var SENTINEL_FINDING_NAMESPACE, SENTINEL_FINDING_KEY_PREFIX, HKDF_INFO2, DEFAULT_SENTINEL_FINDING_RETENTION_DAYS, MAX_FINDING_BYTES, SentinelFindingStore;
|
|
21494
|
+
var init_sentinel_finding_store = __esm({
|
|
21495
|
+
"src/sentinel/sentinel-finding-store.ts"() {
|
|
21496
|
+
init_encryption();
|
|
21497
|
+
init_key_derivation();
|
|
21498
|
+
init_encoding();
|
|
21499
|
+
init_types3();
|
|
21500
|
+
SENTINEL_FINDING_NAMESPACE = "_sentinel_findings";
|
|
21501
|
+
SENTINEL_FINDING_KEY_PREFIX = "finding.";
|
|
21502
|
+
HKDF_INFO2 = "l2-sentinel-finding-v1";
|
|
21503
|
+
DEFAULT_SENTINEL_FINDING_RETENTION_DAYS = 30;
|
|
21504
|
+
MAX_FINDING_BYTES = 256 * 1024;
|
|
21505
|
+
SentinelFindingStore = class {
|
|
21506
|
+
storage;
|
|
21507
|
+
encryptionKey;
|
|
21508
|
+
fortressId;
|
|
21509
|
+
retentionDays;
|
|
21510
|
+
now;
|
|
21511
|
+
constructor(opts) {
|
|
21512
|
+
this.storage = opts.storage;
|
|
21513
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO2);
|
|
21514
|
+
this.fortressId = opts.fortressId;
|
|
21515
|
+
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_SENTINEL_FINDING_RETENTION_DAYS;
|
|
21516
|
+
this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
21517
|
+
}
|
|
21518
|
+
/**
|
|
21519
|
+
* Persist a finding. Truncates the operator-visible summary to
|
|
21520
|
+
* SENTINEL_SUMMARY_MAX_CHARS so the dashboard render stays bounded.
|
|
21521
|
+
* Returns the retention deadline so callers can audit it.
|
|
21522
|
+
*/
|
|
21523
|
+
async saveFinding(finding) {
|
|
21524
|
+
const truncated = {
|
|
21525
|
+
...finding,
|
|
21526
|
+
fortress_id: this.fortressId,
|
|
21527
|
+
summary: truncateSummary(finding.summary)
|
|
21528
|
+
};
|
|
21529
|
+
const retentionMs = this.retentionDays * 24 * 60 * 60 * 1e3;
|
|
21530
|
+
const retentionUntil = new Date(this.now().getTime() + retentionMs);
|
|
21531
|
+
const persisted = {
|
|
21532
|
+
version: 1,
|
|
21533
|
+
finding: truncated,
|
|
21534
|
+
retention_until: retentionUntil.toISOString()
|
|
21535
|
+
};
|
|
21536
|
+
const aad = stringToBytes(finding.finding_id);
|
|
21537
|
+
const plaintext = stringToBytes(JSON.stringify(persisted));
|
|
21538
|
+
const envelope = encrypt(plaintext, this.encryptionKey, aad);
|
|
21539
|
+
await this.storage.write(
|
|
21540
|
+
SENTINEL_FINDING_NAMESPACE,
|
|
21541
|
+
findingKey(finding.finding_id),
|
|
21542
|
+
stringToBytes(JSON.stringify(envelope))
|
|
21543
|
+
);
|
|
21544
|
+
return persisted.retention_until;
|
|
21545
|
+
}
|
|
21546
|
+
/** Load a single finding by id, or null when absent / corrupted. */
|
|
21547
|
+
async loadFinding(findingId) {
|
|
21548
|
+
let raw;
|
|
21549
|
+
try {
|
|
21550
|
+
raw = await this.storage.read(
|
|
21551
|
+
SENTINEL_FINDING_NAMESPACE,
|
|
21552
|
+
findingKey(findingId)
|
|
21553
|
+
);
|
|
21554
|
+
} catch {
|
|
21555
|
+
return null;
|
|
21556
|
+
}
|
|
21557
|
+
if (!raw) return null;
|
|
21558
|
+
if (raw.length > MAX_FINDING_BYTES) return null;
|
|
21559
|
+
return this.decode(findingId, raw);
|
|
21560
|
+
}
|
|
21561
|
+
/**
|
|
21562
|
+
* List findings, newest first. Optional filters: since (ISO 8601),
|
|
21563
|
+
* severity, sentinel_id, agent_id, limit. Default limit 100.
|
|
21564
|
+
*/
|
|
21565
|
+
async listFindings(opts) {
|
|
21566
|
+
const metas = await this.storage.list(
|
|
21567
|
+
SENTINEL_FINDING_NAMESPACE,
|
|
21568
|
+
SENTINEL_FINDING_KEY_PREFIX
|
|
21569
|
+
);
|
|
21570
|
+
const findings = [];
|
|
21571
|
+
for (const meta of metas) {
|
|
21572
|
+
const id = stripKeyPrefix2(meta.key);
|
|
21573
|
+
if (id === null) continue;
|
|
21574
|
+
const raw = await this.storage.read(
|
|
21575
|
+
SENTINEL_FINDING_NAMESPACE,
|
|
21576
|
+
meta.key
|
|
21577
|
+
);
|
|
21578
|
+
if (!raw) continue;
|
|
21579
|
+
if (raw.length > MAX_FINDING_BYTES) continue;
|
|
21580
|
+
const finding = await this.decode(id, raw);
|
|
21581
|
+
if (!finding) continue;
|
|
21582
|
+
if (opts?.since && finding.observed_at < opts.since) continue;
|
|
21583
|
+
if (opts?.severity && finding.severity !== opts.severity) continue;
|
|
21584
|
+
if (opts?.sentinelId && finding.sentinel_id !== opts.sentinelId) continue;
|
|
21585
|
+
if (opts?.agentId && finding.agent_id !== opts.agentId) continue;
|
|
21586
|
+
findings.push(finding);
|
|
21587
|
+
}
|
|
21588
|
+
findings.sort((a, b) => a.observed_at < b.observed_at ? 1 : -1);
|
|
21589
|
+
const limit = opts?.limit ?? 100;
|
|
21590
|
+
return findings.slice(0, limit);
|
|
21591
|
+
}
|
|
21592
|
+
/**
|
|
21593
|
+
* Drop expired findings. Returns the count removed.
|
|
21594
|
+
*/
|
|
21595
|
+
async pruneExpired(now) {
|
|
21596
|
+
const cutoff = (now ?? this.now()).toISOString();
|
|
21597
|
+
const metas = await this.storage.list(
|
|
21598
|
+
SENTINEL_FINDING_NAMESPACE,
|
|
21599
|
+
SENTINEL_FINDING_KEY_PREFIX
|
|
21600
|
+
);
|
|
21601
|
+
let pruned = 0;
|
|
21602
|
+
for (const meta of metas) {
|
|
21603
|
+
const id = stripKeyPrefix2(meta.key);
|
|
21604
|
+
if (id === null) continue;
|
|
21605
|
+
const raw = await this.storage.read(
|
|
21606
|
+
SENTINEL_FINDING_NAMESPACE,
|
|
21607
|
+
meta.key
|
|
21608
|
+
);
|
|
21609
|
+
if (!raw) continue;
|
|
21610
|
+
try {
|
|
21611
|
+
const aad = stringToBytes(id);
|
|
21612
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
21613
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
21614
|
+
const persisted = JSON.parse(
|
|
21615
|
+
bytesToString(plaintext)
|
|
21616
|
+
);
|
|
21617
|
+
if (persisted.retention_until <= cutoff) {
|
|
21618
|
+
await this.storage.delete(SENTINEL_FINDING_NAMESPACE, meta.key);
|
|
21619
|
+
pruned += 1;
|
|
21620
|
+
}
|
|
21621
|
+
} catch {
|
|
21622
|
+
}
|
|
21623
|
+
}
|
|
21624
|
+
return { pruned };
|
|
21625
|
+
}
|
|
21626
|
+
async decode(findingId, raw) {
|
|
21627
|
+
try {
|
|
21628
|
+
const aad = stringToBytes(findingId);
|
|
21629
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
21630
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
21631
|
+
const persisted = JSON.parse(
|
|
21632
|
+
bytesToString(plaintext)
|
|
21633
|
+
);
|
|
21634
|
+
if (persisted.version !== 1) return null;
|
|
21635
|
+
if (persisted.finding.finding_id !== findingId) return null;
|
|
21636
|
+
if (persisted.finding.fortress_id !== this.fortressId) return null;
|
|
21637
|
+
return persisted.finding;
|
|
21638
|
+
} catch {
|
|
21639
|
+
return null;
|
|
21640
|
+
}
|
|
21641
|
+
}
|
|
21642
|
+
};
|
|
21643
|
+
}
|
|
21644
|
+
});
|
|
21645
|
+
|
|
21646
|
+
// src/sentinel/sentinel-registry.ts
|
|
21647
|
+
var SentinelRegistry;
|
|
21648
|
+
var init_sentinel_registry = __esm({
|
|
21649
|
+
"src/sentinel/sentinel-registry.ts"() {
|
|
21650
|
+
SentinelRegistry = class {
|
|
21651
|
+
catalog = /* @__PURE__ */ new Map();
|
|
21652
|
+
subscribed = /* @__PURE__ */ new Map();
|
|
21653
|
+
register(entry) {
|
|
21654
|
+
if (this.catalog.has(entry.sentinelId)) {
|
|
21655
|
+
throw new Error(
|
|
21656
|
+
`sentinel-registry: ${entry.sentinelId} already registered`
|
|
21657
|
+
);
|
|
21658
|
+
}
|
|
21659
|
+
this.catalog.set(entry.sentinelId, entry);
|
|
21660
|
+
}
|
|
21661
|
+
/**
|
|
21662
|
+
* Available sentinels (catalog view). Operator UI lists this so the
|
|
21663
|
+
* operator can pick what to subscribe to.
|
|
21664
|
+
*/
|
|
21665
|
+
listCatalog() {
|
|
21666
|
+
return [...this.catalog.values()].map((entry) => ({
|
|
21667
|
+
sentinelId: entry.sentinelId,
|
|
21668
|
+
description: entry.description
|
|
21669
|
+
}));
|
|
21670
|
+
}
|
|
21671
|
+
/** Currently subscribed sentinel ids. */
|
|
21672
|
+
listSubscribed() {
|
|
21673
|
+
return [...this.subscribed.keys()];
|
|
21674
|
+
}
|
|
21675
|
+
/** Has the fortress opted into this sentinel? */
|
|
21676
|
+
isSubscribed(sentinelId) {
|
|
21677
|
+
return this.subscribed.has(sentinelId);
|
|
21678
|
+
}
|
|
21679
|
+
/**
|
|
21680
|
+
* Subscribe a sentinel to a fortress context. Idempotent: a second
|
|
21681
|
+
* subscribe call on an already-subscribed sentinel returns the
|
|
21682
|
+
* existing instance without re-running `subscribe()`.
|
|
21683
|
+
*/
|
|
21684
|
+
async subscribe(sentinelId, context) {
|
|
21685
|
+
const existing = this.subscribed.get(sentinelId);
|
|
21686
|
+
if (existing) return existing;
|
|
21687
|
+
const entry = this.catalog.get(sentinelId);
|
|
21688
|
+
if (!entry) {
|
|
21689
|
+
throw new Error(`sentinel-registry: unknown sentinel ${sentinelId}`);
|
|
21690
|
+
}
|
|
21691
|
+
const instance = entry.factory();
|
|
21692
|
+
await instance.subscribe(context);
|
|
21693
|
+
this.subscribed.set(sentinelId, instance);
|
|
21694
|
+
return instance;
|
|
21695
|
+
}
|
|
21696
|
+
/**
|
|
21697
|
+
* Unsubscribe. Idempotent: unsubscribing an unsubscribed sentinel
|
|
21698
|
+
* returns false without throwing. Returns true when an active
|
|
21699
|
+
* subscription was torn down.
|
|
21700
|
+
*/
|
|
21701
|
+
async unsubscribe(sentinelId) {
|
|
21702
|
+
const instance = this.subscribed.get(sentinelId);
|
|
21703
|
+
if (!instance) return false;
|
|
21704
|
+
try {
|
|
21705
|
+
await instance.unsubscribe();
|
|
21706
|
+
} finally {
|
|
21707
|
+
this.subscribed.delete(sentinelId);
|
|
21708
|
+
}
|
|
21709
|
+
return true;
|
|
21710
|
+
}
|
|
21711
|
+
/**
|
|
21712
|
+
* Snapshot of subscribed sentinels for the dispatcher's tick path.
|
|
21713
|
+
* Returned as an array so the dispatcher can iterate without holding
|
|
21714
|
+
* the map under modification.
|
|
21715
|
+
*/
|
|
21716
|
+
snapshotSubscribed() {
|
|
21717
|
+
return [...this.subscribed.entries()].map(([sentinelId, sentinel]) => ({
|
|
21718
|
+
sentinelId,
|
|
21719
|
+
sentinel
|
|
21720
|
+
}));
|
|
21721
|
+
}
|
|
21722
|
+
/**
|
|
21723
|
+
* Tear down every subscription. Called by the dispatcher on
|
|
21724
|
+
* fortress-shutdown. Best-effort: a failing unsubscribe does not
|
|
21725
|
+
* abort the rest.
|
|
21726
|
+
*/
|
|
21727
|
+
async unsubscribeAll() {
|
|
21728
|
+
const ids = [...this.subscribed.keys()];
|
|
21729
|
+
for (const id of ids) {
|
|
21730
|
+
try {
|
|
21731
|
+
await this.unsubscribe(id);
|
|
21732
|
+
} catch {
|
|
21733
|
+
}
|
|
21734
|
+
}
|
|
21735
|
+
}
|
|
21736
|
+
};
|
|
21737
|
+
}
|
|
21738
|
+
});
|
|
21739
|
+
var DEFAULT_TICK_INTERVAL_MS, SentinelDispatcher;
|
|
21740
|
+
var init_sentinel_dispatcher = __esm({
|
|
21741
|
+
"src/sentinel/sentinel-dispatcher.ts"() {
|
|
21742
|
+
init_types3();
|
|
21743
|
+
DEFAULT_TICK_INTERVAL_MS = 6e4;
|
|
21744
|
+
SentinelDispatcher = class {
|
|
21745
|
+
registry;
|
|
21746
|
+
findingStore;
|
|
21747
|
+
auditLog;
|
|
21748
|
+
fortressId;
|
|
21749
|
+
identityId;
|
|
21750
|
+
now;
|
|
21751
|
+
tickIntervalMs;
|
|
21752
|
+
listeners = /* @__PURE__ */ new Set();
|
|
21753
|
+
tickTimer = null;
|
|
21754
|
+
tickInFlight = false;
|
|
21755
|
+
constructor(deps) {
|
|
21756
|
+
this.registry = deps.registry;
|
|
21757
|
+
this.findingStore = deps.findingStore;
|
|
21758
|
+
this.auditLog = deps.auditLog;
|
|
21759
|
+
this.fortressId = deps.fortressId;
|
|
21760
|
+
this.identityId = deps.identityId;
|
|
21761
|
+
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
21762
|
+
this.tickIntervalMs = deps.tickIntervalMs ?? DEFAULT_TICK_INTERVAL_MS;
|
|
21763
|
+
}
|
|
21764
|
+
/** Read-only view of the registry. Convenience for route handlers. */
|
|
21765
|
+
getRegistry() {
|
|
21766
|
+
return this.registry;
|
|
21767
|
+
}
|
|
21768
|
+
/** Read-only view of the finding store. Convenience for route handlers. */
|
|
21769
|
+
getFindingStore() {
|
|
21770
|
+
return this.findingStore;
|
|
21771
|
+
}
|
|
21772
|
+
/**
|
|
21773
|
+
* Subscribe an in-process listener. Returns an unsubscribe fn.
|
|
21774
|
+
*/
|
|
21775
|
+
onEvent(listener) {
|
|
21776
|
+
this.listeners.add(listener);
|
|
21777
|
+
return () => this.listeners.delete(listener);
|
|
21778
|
+
}
|
|
21779
|
+
/**
|
|
21780
|
+
* Subscribe a sentinel to this fortress + emit the
|
|
21781
|
+
* `sentinel_subscribed` audit event. Wraps `registry.subscribe()` so
|
|
21782
|
+
* the audit emission lives at the dispatcher boundary (the
|
|
21783
|
+
* fortress-aware site).
|
|
21784
|
+
*/
|
|
21785
|
+
async subscribeSentinel(sentinelId, contextOverrides) {
|
|
21786
|
+
const context = {
|
|
21787
|
+
fortressId: this.fortressId,
|
|
21788
|
+
auditLog: this.auditLog,
|
|
21789
|
+
now: this.now,
|
|
21790
|
+
...contextOverrides ?? {}
|
|
21791
|
+
};
|
|
21792
|
+
const sentinel = await this.registry.subscribe(sentinelId, context);
|
|
21793
|
+
this.auditLog.append(
|
|
21794
|
+
"l2",
|
|
21795
|
+
SENTINEL_AUDIT_OPS.SUBSCRIBED,
|
|
21796
|
+
this.identityId,
|
|
21797
|
+
{ sentinel_id: sentinelId, fortress_id: this.fortressId }
|
|
21798
|
+
);
|
|
21799
|
+
return sentinel;
|
|
21800
|
+
}
|
|
21801
|
+
/**
|
|
21802
|
+
* Unsubscribe + emit `sentinel_unsubscribed`. Returns true when an
|
|
21803
|
+
* active subscription was torn down. Audit fires only on successful
|
|
21804
|
+
* removal.
|
|
21805
|
+
*/
|
|
21806
|
+
async unsubscribeSentinel(sentinelId) {
|
|
21807
|
+
const removed = await this.registry.unsubscribe(sentinelId);
|
|
21808
|
+
if (removed) {
|
|
21809
|
+
this.auditLog.append(
|
|
21810
|
+
"l2",
|
|
21811
|
+
SENTINEL_AUDIT_OPS.UNSUBSCRIBED,
|
|
21812
|
+
this.identityId,
|
|
21813
|
+
{ sentinel_id: sentinelId, fortress_id: this.fortressId }
|
|
21814
|
+
);
|
|
21815
|
+
}
|
|
21816
|
+
return removed;
|
|
21817
|
+
}
|
|
21818
|
+
/**
|
|
21819
|
+
* Run one evaluation pass over every subscribed sentinel. Used by
|
|
21820
|
+
* the auto-tick AND by tests that want a synchronous evaluation
|
|
21821
|
+
* gate. Returns the findings produced this tick (already persisted
|
|
21822
|
+
* + audit-logged + emitted).
|
|
21823
|
+
*/
|
|
21824
|
+
async tick() {
|
|
21825
|
+
if (this.tickInFlight) return [];
|
|
21826
|
+
this.tickInFlight = true;
|
|
21827
|
+
try {
|
|
21828
|
+
const subscribed = this.registry.snapshotSubscribed();
|
|
21829
|
+
const findings = [];
|
|
21830
|
+
for (const { sentinelId, sentinel } of subscribed) {
|
|
21831
|
+
try {
|
|
21832
|
+
const tickFindings = await sentinel.evaluate();
|
|
21833
|
+
for (const finding of tickFindings) {
|
|
21834
|
+
const stamped = await this.routeFinding(sentinelId, finding);
|
|
21835
|
+
findings.push(stamped);
|
|
21836
|
+
}
|
|
21837
|
+
} catch (err) {
|
|
21838
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
21839
|
+
const observedAt = this.now().toISOString();
|
|
21840
|
+
this.auditLog.append(
|
|
21841
|
+
"l2",
|
|
21842
|
+
SENTINEL_AUDIT_OPS.EVALUATION_FAILED,
|
|
21843
|
+
this.identityId,
|
|
21844
|
+
{
|
|
21845
|
+
sentinel_id: sentinelId,
|
|
21846
|
+
fortress_id: this.fortressId,
|
|
21847
|
+
error_message: errorMessage
|
|
21848
|
+
},
|
|
21849
|
+
"failure"
|
|
21850
|
+
);
|
|
21851
|
+
this.emit({
|
|
21852
|
+
type: "evaluation_failed",
|
|
21853
|
+
sentinel_id: sentinelId,
|
|
21854
|
+
error_message: errorMessage,
|
|
21855
|
+
observed_at: observedAt
|
|
21856
|
+
});
|
|
21857
|
+
}
|
|
21858
|
+
}
|
|
21859
|
+
return findings;
|
|
21860
|
+
} finally {
|
|
21861
|
+
this.tickInFlight = false;
|
|
21862
|
+
}
|
|
21863
|
+
}
|
|
21864
|
+
/**
|
|
21865
|
+
* Start the auto-tick loop. No-op when tickIntervalMs is 0 or when
|
|
21866
|
+
* already started. Tests typically leave auto-tick off and call
|
|
21867
|
+
* `tick()` directly.
|
|
21868
|
+
*/
|
|
21869
|
+
start() {
|
|
21870
|
+
if (this.tickTimer !== null) return;
|
|
21871
|
+
if (this.tickIntervalMs <= 0) return;
|
|
21872
|
+
this.tickTimer = setInterval(() => {
|
|
21873
|
+
void this.tick();
|
|
21874
|
+
}, this.tickIntervalMs);
|
|
21875
|
+
if (typeof this.tickTimer.unref === "function") {
|
|
21876
|
+
this.tickTimer.unref();
|
|
21877
|
+
}
|
|
21878
|
+
}
|
|
21879
|
+
/** Stop the auto-tick loop. Idempotent. */
|
|
21880
|
+
stop() {
|
|
21881
|
+
if (this.tickTimer === null) return;
|
|
21882
|
+
clearInterval(this.tickTimer);
|
|
21883
|
+
this.tickTimer = null;
|
|
21884
|
+
}
|
|
21885
|
+
/**
|
|
21886
|
+
* Tear down every subscription + stop the tick loop. Called on
|
|
21887
|
+
* fortress shutdown.
|
|
21888
|
+
*/
|
|
21889
|
+
async dispose() {
|
|
21890
|
+
this.stop();
|
|
21891
|
+
await this.registry.unsubscribeAll();
|
|
21892
|
+
this.listeners.clear();
|
|
21893
|
+
}
|
|
21894
|
+
async routeFinding(sentinelId, raw) {
|
|
21895
|
+
const stamped = {
|
|
21896
|
+
...raw,
|
|
21897
|
+
finding_id: raw.finding_id || randomUUID(),
|
|
21898
|
+
sentinel_id: sentinelId,
|
|
21899
|
+
fortress_id: this.fortressId,
|
|
21900
|
+
observed_at: raw.observed_at || this.now().toISOString()
|
|
21901
|
+
};
|
|
21902
|
+
await this.findingStore.saveFinding(stamped);
|
|
21903
|
+
this.auditLog.append(
|
|
21904
|
+
"l2",
|
|
21905
|
+
SENTINEL_AUDIT_OPS.FINDING_EMITTED,
|
|
21906
|
+
this.identityId,
|
|
21907
|
+
{
|
|
21908
|
+
sentinel_id: sentinelId,
|
|
21909
|
+
finding_id: stamped.finding_id,
|
|
21910
|
+
severity: stamped.severity,
|
|
21911
|
+
...stamped.agent_id !== void 0 ? { agent_id: stamped.agent_id } : {},
|
|
21912
|
+
evidence_audit_ids: stamped.evidence_audit_ids,
|
|
21913
|
+
fortress_id: this.fortressId
|
|
21914
|
+
}
|
|
21915
|
+
);
|
|
21916
|
+
this.emit({ type: "finding", finding: stamped });
|
|
21917
|
+
return stamped;
|
|
21918
|
+
}
|
|
21919
|
+
emit(event) {
|
|
21920
|
+
for (const listener of this.listeners) {
|
|
21921
|
+
try {
|
|
21922
|
+
listener(event);
|
|
21923
|
+
} catch {
|
|
21924
|
+
}
|
|
21925
|
+
}
|
|
21926
|
+
}
|
|
21927
|
+
};
|
|
21928
|
+
}
|
|
21929
|
+
});
|
|
21930
|
+
|
|
21931
|
+
// src/sentinel/sentinel.ts
|
|
21932
|
+
var Sentinel;
|
|
21933
|
+
var init_sentinel = __esm({
|
|
21934
|
+
"src/sentinel/sentinel.ts"() {
|
|
21935
|
+
Sentinel = class {
|
|
21936
|
+
/**
|
|
21937
|
+
* Bind the sentinel to a fortress context. Called once on
|
|
21938
|
+
* subscribe. Default implementation stores the context on `this`;
|
|
21939
|
+
* sentinels that need additional setup (e.g. priming a baseline
|
|
21940
|
+
* cache) override.
|
|
21941
|
+
*/
|
|
21942
|
+
async subscribe(context) {
|
|
21943
|
+
this.context = context;
|
|
21944
|
+
}
|
|
21945
|
+
/**
|
|
21946
|
+
* Tear down. Default implementation clears the context; subclasses
|
|
21947
|
+
* that hold timers or external handles override.
|
|
21948
|
+
*/
|
|
21949
|
+
async unsubscribe() {
|
|
21950
|
+
this.context = void 0;
|
|
21951
|
+
}
|
|
21952
|
+
context;
|
|
21953
|
+
/** Internal helper: assert subscribed before evaluation. */
|
|
21954
|
+
requireContext() {
|
|
21955
|
+
if (!this.context) {
|
|
21956
|
+
throw new Error(
|
|
21957
|
+
`sentinel ${this.sentinelId}: evaluate() called before subscribe()`
|
|
21958
|
+
);
|
|
21959
|
+
}
|
|
21960
|
+
return this.context;
|
|
21961
|
+
}
|
|
21962
|
+
};
|
|
21963
|
+
}
|
|
21964
|
+
});
|
|
21965
|
+
|
|
21966
|
+
// src/sentinel/sentinels/egress-volume-watcher.ts
|
|
21967
|
+
var EGRESS_VOLUME_SENTINEL_ID, WARN_SIGMA, ALERT_SIGMA, BASELINE_WINDOWS, QUERY_LIMIT, EgressVolumeWatcher;
|
|
21968
|
+
var init_egress_volume_watcher = __esm({
|
|
21969
|
+
"src/sentinel/sentinels/egress-volume-watcher.ts"() {
|
|
21970
|
+
init_sentinel();
|
|
21971
|
+
init_types3();
|
|
21972
|
+
EGRESS_VOLUME_SENTINEL_ID = "egress-volume";
|
|
21973
|
+
WARN_SIGMA = 3;
|
|
21974
|
+
ALERT_SIGMA = 6;
|
|
21975
|
+
BASELINE_WINDOWS = 7;
|
|
21976
|
+
QUERY_LIMIT = 1e4;
|
|
21977
|
+
EgressVolumeWatcher = class extends Sentinel {
|
|
21978
|
+
sentinelId = EGRESS_VOLUME_SENTINEL_ID;
|
|
21979
|
+
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.";
|
|
21980
|
+
/** Servers we have already produced an `info` baseline-established finding for. */
|
|
21981
|
+
baselineEstablished = /* @__PURE__ */ new Set();
|
|
21982
|
+
async evaluate() {
|
|
21983
|
+
const ctx = this.requireContext();
|
|
21984
|
+
const now = ctx.now();
|
|
21985
|
+
const windowMs = 24 * 60 * 60 * 1e3;
|
|
21986
|
+
const windowSpanMs = (BASELINE_WINDOWS + 1) * windowMs;
|
|
21987
|
+
const sinceIso = new Date(now.getTime() - windowSpanMs).toISOString();
|
|
21988
|
+
const queryResult = await ctx.auditLog.query({
|
|
21989
|
+
since: sinceIso,
|
|
21990
|
+
layer: "l2",
|
|
21991
|
+
limit: QUERY_LIMIT
|
|
21992
|
+
});
|
|
21993
|
+
const entries = queryResult.entries.filter(isProxyCallAuditEntry);
|
|
21994
|
+
const byServer = /* @__PURE__ */ new Map();
|
|
21995
|
+
for (const entry of entries) {
|
|
21996
|
+
const server = proxyServerFromAuditEntry(entry);
|
|
21997
|
+
if (server === null) continue;
|
|
21998
|
+
const auditAge = now.getTime() - new Date(entry.timestamp).getTime();
|
|
21999
|
+
if (auditAge < 0) continue;
|
|
22000
|
+
const windowIdx = Math.floor(auditAge / windowMs);
|
|
22001
|
+
if (windowIdx > BASELINE_WINDOWS) continue;
|
|
22002
|
+
let snapshot = byServer.get(server);
|
|
22003
|
+
if (!snapshot) {
|
|
22004
|
+
snapshot = { windows: [] };
|
|
22005
|
+
for (let i = 0; i <= BASELINE_WINDOWS; i += 1) {
|
|
22006
|
+
snapshot.windows.push({ count: 0, evidence_audit_ids: [] });
|
|
22007
|
+
}
|
|
22008
|
+
byServer.set(server, snapshot);
|
|
22009
|
+
}
|
|
22010
|
+
const bucket = snapshot.windows[windowIdx];
|
|
22011
|
+
bucket.count += 1;
|
|
22012
|
+
if (windowIdx === 0 && bucket.evidence_audit_ids.length < 50) {
|
|
22013
|
+
bucket.evidence_audit_ids.push(`${entry.timestamp}:${entry.operation}`);
|
|
22014
|
+
}
|
|
22015
|
+
}
|
|
22016
|
+
const findings = [];
|
|
22017
|
+
for (const [server, snapshot] of byServer.entries()) {
|
|
22018
|
+
const finding = this.evaluateServer(server, snapshot, now);
|
|
22019
|
+
if (finding) findings.push(finding);
|
|
22020
|
+
}
|
|
22021
|
+
return findings;
|
|
22022
|
+
}
|
|
22023
|
+
/** Reset baseline-established memoization. Tests use this between runs. */
|
|
22024
|
+
resetBaselineMemo() {
|
|
22025
|
+
this.baselineEstablished.clear();
|
|
22026
|
+
}
|
|
22027
|
+
evaluateServer(server, snapshot, now) {
|
|
22028
|
+
const currentWindow = snapshot.windows[0];
|
|
22029
|
+
const baselineWindows = snapshot.windows.slice(1);
|
|
22030
|
+
const populatedBaselineWindows = baselineWindows.filter((w) => w.count > 0).length;
|
|
22031
|
+
if (populatedBaselineWindows < BASELINE_WINDOWS) {
|
|
22032
|
+
if (this.baselineEstablished.has(server)) return null;
|
|
22033
|
+
if (populatedBaselineWindows === 0 && currentWindow.count === 0) {
|
|
22034
|
+
return null;
|
|
22035
|
+
}
|
|
22036
|
+
return null;
|
|
22037
|
+
}
|
|
22038
|
+
const baselineCounts = baselineWindows.map((w) => w.count);
|
|
22039
|
+
const mean = baselineCounts.reduce((sum, c) => sum + c, 0) / baselineCounts.length;
|
|
22040
|
+
const variance = baselineCounts.reduce((sum, c) => sum + (c - mean) ** 2, 0) / baselineCounts.length;
|
|
22041
|
+
const stddev = Math.sqrt(variance);
|
|
22042
|
+
const wasEstablished = this.baselineEstablished.has(server);
|
|
22043
|
+
this.baselineEstablished.add(server);
|
|
22044
|
+
if (!wasEstablished) {
|
|
22045
|
+
return {
|
|
22046
|
+
finding_id: "",
|
|
22047
|
+
sentinel_id: this.sentinelId,
|
|
22048
|
+
severity: "info",
|
|
22049
|
+
summary: `egress-volume baseline established for ${server}: mean ${mean.toFixed(1)} calls/24h, stddev ${stddev.toFixed(1)} (over ${BASELINE_WINDOWS} prior days).`,
|
|
22050
|
+
details: {
|
|
22051
|
+
server,
|
|
22052
|
+
baseline_mean: mean,
|
|
22053
|
+
baseline_stddev: stddev,
|
|
22054
|
+
baseline_windows: baselineCounts,
|
|
22055
|
+
current_count: currentWindow.count
|
|
22056
|
+
},
|
|
22057
|
+
observed_at: now.toISOString(),
|
|
22058
|
+
evidence_audit_ids: [],
|
|
22059
|
+
fortress_id: ""
|
|
22060
|
+
};
|
|
22061
|
+
}
|
|
22062
|
+
const warnThreshold = mean + WARN_SIGMA * stddev;
|
|
22063
|
+
const alertThreshold = mean + ALERT_SIGMA * stddev;
|
|
22064
|
+
if (currentWindow.count > alertThreshold) {
|
|
22065
|
+
return this.buildAnomalyFinding(
|
|
22066
|
+
server,
|
|
22067
|
+
snapshot,
|
|
22068
|
+
mean,
|
|
22069
|
+
stddev,
|
|
22070
|
+
now,
|
|
22071
|
+
"alert",
|
|
22072
|
+
ALERT_SIGMA
|
|
22073
|
+
);
|
|
22074
|
+
}
|
|
22075
|
+
if (currentWindow.count > warnThreshold) {
|
|
22076
|
+
return this.buildAnomalyFinding(
|
|
22077
|
+
server,
|
|
22078
|
+
snapshot,
|
|
22079
|
+
mean,
|
|
22080
|
+
stddev,
|
|
22081
|
+
now,
|
|
22082
|
+
"warn",
|
|
22083
|
+
WARN_SIGMA
|
|
22084
|
+
);
|
|
22085
|
+
}
|
|
22086
|
+
return null;
|
|
22087
|
+
}
|
|
22088
|
+
buildAnomalyFinding(server, snapshot, mean, stddev, now, severity, sigma) {
|
|
22089
|
+
const currentWindow = snapshot.windows[0];
|
|
22090
|
+
const ratio = mean === 0 ? Infinity : currentWindow.count / mean;
|
|
22091
|
+
const ratioStr = Number.isFinite(ratio) ? `${ratio.toFixed(1)}x normal` : "no prior baseline";
|
|
22092
|
+
const summary = `${server} egress is ${ratioStr}: ${currentWindow.count} calls in last 24h, baseline ${mean.toFixed(1)} (stddev ${stddev.toFixed(1)}). Crossed +${sigma} sigma threshold.`;
|
|
22093
|
+
return {
|
|
22094
|
+
finding_id: "",
|
|
22095
|
+
sentinel_id: this.sentinelId,
|
|
22096
|
+
severity,
|
|
22097
|
+
summary,
|
|
22098
|
+
details: {
|
|
22099
|
+
server,
|
|
22100
|
+
current_count: currentWindow.count,
|
|
22101
|
+
baseline_mean: mean,
|
|
22102
|
+
baseline_stddev: stddev,
|
|
22103
|
+
sigma_threshold: sigma,
|
|
22104
|
+
ratio
|
|
22105
|
+
},
|
|
22106
|
+
observed_at: now.toISOString(),
|
|
22107
|
+
evidence_audit_ids: currentWindow.evidence_audit_ids,
|
|
22108
|
+
fortress_id: ""
|
|
22109
|
+
};
|
|
22110
|
+
}
|
|
22111
|
+
};
|
|
22112
|
+
}
|
|
22113
|
+
});
|
|
22114
|
+
|
|
22115
|
+
// src/sentinel/sentinels/cross-agent-chatter-watcher.ts
|
|
22116
|
+
function pairKey(sender, recipient) {
|
|
22117
|
+
return `${sender}|${recipient}`;
|
|
22118
|
+
}
|
|
22119
|
+
function pairFromKey(key) {
|
|
22120
|
+
const idx = key.indexOf("|");
|
|
22121
|
+
return { sender: key.slice(0, idx), recipient: key.slice(idx + 1) };
|
|
22122
|
+
}
|
|
22123
|
+
function computeNewPartners(byPair) {
|
|
22124
|
+
const currentBySource = /* @__PURE__ */ new Map();
|
|
22125
|
+
const priorBySource = /* @__PURE__ */ new Map();
|
|
22126
|
+
for (const [key, snap] of byPair.entries()) {
|
|
22127
|
+
const { sender, recipient } = pairFromKey(key);
|
|
22128
|
+
if (snap.windows[0] && snap.windows[0].count > 0) {
|
|
22129
|
+
let recipMap = currentBySource.get(sender);
|
|
22130
|
+
if (!recipMap) {
|
|
22131
|
+
recipMap = /* @__PURE__ */ new Map();
|
|
22132
|
+
currentBySource.set(sender, recipMap);
|
|
22133
|
+
}
|
|
22134
|
+
recipMap.set(recipient, snap.windows[0].evidence_audit_ids);
|
|
22135
|
+
}
|
|
22136
|
+
const priorTouched = snap.windows.slice(1).some((w) => w.count > 0);
|
|
22137
|
+
if (priorTouched) {
|
|
22138
|
+
let set = priorBySource.get(sender);
|
|
22139
|
+
if (!set) {
|
|
22140
|
+
set = /* @__PURE__ */ new Set();
|
|
22141
|
+
priorBySource.set(sender, set);
|
|
22142
|
+
}
|
|
22143
|
+
set.add(recipient);
|
|
22144
|
+
}
|
|
22145
|
+
}
|
|
22146
|
+
const out = /* @__PURE__ */ new Map();
|
|
22147
|
+
for (const [sender, recipMap] of currentBySource.entries()) {
|
|
22148
|
+
const prior = priorBySource.get(sender) ?? /* @__PURE__ */ new Set();
|
|
22149
|
+
if (prior.size === 0) {
|
|
22150
|
+
continue;
|
|
22151
|
+
}
|
|
22152
|
+
const newPartners = [];
|
|
22153
|
+
const evidence = [];
|
|
22154
|
+
for (const [recipient, recipEvidence] of recipMap.entries()) {
|
|
22155
|
+
if (!prior.has(recipient)) {
|
|
22156
|
+
newPartners.push(recipient);
|
|
22157
|
+
for (const id of recipEvidence) {
|
|
22158
|
+
if (evidence.length < 50) evidence.push(id);
|
|
22159
|
+
}
|
|
22160
|
+
}
|
|
22161
|
+
}
|
|
22162
|
+
if (newPartners.length === 0) continue;
|
|
22163
|
+
newPartners.sort();
|
|
22164
|
+
out.set(sender, {
|
|
22165
|
+
partners: newPartners,
|
|
22166
|
+
priorPartners: [...prior].sort(),
|
|
22167
|
+
evidenceAuditIds: evidence
|
|
22168
|
+
});
|
|
22169
|
+
}
|
|
22170
|
+
return out;
|
|
22171
|
+
}
|
|
22172
|
+
function extractInterAgentEvents(entries) {
|
|
22173
|
+
const out = [];
|
|
22174
|
+
for (const entry of entries) {
|
|
22175
|
+
const op = entry.operation;
|
|
22176
|
+
if (op === HANDOFF_OP) {
|
|
22177
|
+
const details = entry.details;
|
|
22178
|
+
const sender = optionalString(details, "sender_agent_id");
|
|
22179
|
+
const recipient = optionalString(details, "recipient_agent_id");
|
|
22180
|
+
if (!sender || !recipient || sender === recipient) continue;
|
|
22181
|
+
out.push({
|
|
22182
|
+
sender,
|
|
22183
|
+
recipient,
|
|
22184
|
+
timestampMs: Date.parse(entry.timestamp),
|
|
22185
|
+
auditId: `${entry.timestamp}:${entry.operation}`
|
|
22186
|
+
});
|
|
22187
|
+
continue;
|
|
22188
|
+
}
|
|
22189
|
+
if (CROSS_HARNESS_OPS.has(op)) {
|
|
22190
|
+
const details = entry.details;
|
|
22191
|
+
const sender = optionalString(details, "source_harness") ?? optionalString(details, "source_agent_id");
|
|
22192
|
+
if (!sender) continue;
|
|
22193
|
+
out.push({
|
|
22194
|
+
sender,
|
|
22195
|
+
recipient: OPERATOR_PSEUDO_AGENT,
|
|
22196
|
+
timestampMs: Date.parse(entry.timestamp),
|
|
22197
|
+
auditId: `${entry.timestamp}:${entry.operation}`
|
|
22198
|
+
});
|
|
22199
|
+
}
|
|
22200
|
+
}
|
|
22201
|
+
return out;
|
|
22202
|
+
}
|
|
22203
|
+
function optionalString(details, key) {
|
|
22204
|
+
if (!details) return null;
|
|
22205
|
+
const value = details[key];
|
|
22206
|
+
if (typeof value !== "string" || value.length === 0) return null;
|
|
22207
|
+
return value;
|
|
22208
|
+
}
|
|
22209
|
+
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;
|
|
22210
|
+
var init_cross_agent_chatter_watcher = __esm({
|
|
22211
|
+
"src/sentinel/sentinels/cross-agent-chatter-watcher.ts"() {
|
|
22212
|
+
init_sentinel();
|
|
22213
|
+
CROSS_AGENT_CHATTER_SENTINEL_ID = "cross-agent-chatter";
|
|
22214
|
+
WARN_SIGMA2 = 3;
|
|
22215
|
+
ALERT_SIGMA2 = 6;
|
|
22216
|
+
BASELINE_WINDOWS2 = 7;
|
|
22217
|
+
QUERY_LIMIT2 = 1e4;
|
|
22218
|
+
MULTI_NEW_PARTNER_ALERT_THRESHOLD = 3;
|
|
22219
|
+
OPERATOR_PSEUDO_AGENT = "operator";
|
|
22220
|
+
HANDOFF_OP = "v1.1_local_handoff";
|
|
22221
|
+
CROSS_HARNESS_OPS = /* @__PURE__ */ new Set([
|
|
22222
|
+
"cross_harness_approval_aggregated",
|
|
22223
|
+
"cross_harness_approval_resolved"
|
|
22224
|
+
]);
|
|
22225
|
+
CrossAgentChatterWatcher = class extends Sentinel {
|
|
22226
|
+
sentinelId = CROSS_AGENT_CHATTER_SENTINEL_ID;
|
|
22227
|
+
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).";
|
|
22228
|
+
/** Pair keys we have already produced a baseline-established info finding for. */
|
|
22229
|
+
baselineEstablished = /* @__PURE__ */ new Set();
|
|
22230
|
+
async evaluate() {
|
|
22231
|
+
const ctx = this.requireContext();
|
|
22232
|
+
const now = ctx.now();
|
|
22233
|
+
const windowMs = 24 * 60 * 60 * 1e3;
|
|
22234
|
+
const windowSpanMs = (BASELINE_WINDOWS2 + 1) * windowMs;
|
|
22235
|
+
const sinceIso = new Date(now.getTime() - windowSpanMs).toISOString();
|
|
22236
|
+
const queryResult = await ctx.auditLog.query({
|
|
22237
|
+
since: sinceIso,
|
|
22238
|
+
layer: "l2",
|
|
22239
|
+
limit: QUERY_LIMIT2
|
|
22240
|
+
});
|
|
22241
|
+
const events = extractInterAgentEvents(queryResult.entries);
|
|
22242
|
+
const byPair = /* @__PURE__ */ new Map();
|
|
22243
|
+
for (const event of events) {
|
|
22244
|
+
const auditAgeMs = now.getTime() - event.timestampMs;
|
|
22245
|
+
if (auditAgeMs < 0) continue;
|
|
22246
|
+
const windowIdx = Math.floor(auditAgeMs / windowMs);
|
|
22247
|
+
if (windowIdx > BASELINE_WINDOWS2) continue;
|
|
22248
|
+
const key = pairKey(event.sender, event.recipient);
|
|
22249
|
+
let snap = byPair.get(key);
|
|
22250
|
+
if (!snap) {
|
|
22251
|
+
snap = { windows: [] };
|
|
22252
|
+
for (let i = 0; i <= BASELINE_WINDOWS2; i += 1) {
|
|
22253
|
+
snap.windows.push({ count: 0, evidence_audit_ids: [] });
|
|
22254
|
+
}
|
|
22255
|
+
byPair.set(key, snap);
|
|
22256
|
+
}
|
|
22257
|
+
const bucket = snap.windows[windowIdx];
|
|
22258
|
+
bucket.count += 1;
|
|
22259
|
+
if (windowIdx === 0 && bucket.evidence_audit_ids.length < 50) {
|
|
22260
|
+
bucket.evidence_audit_ids.push(event.auditId);
|
|
22261
|
+
}
|
|
22262
|
+
}
|
|
22263
|
+
const findings = [];
|
|
22264
|
+
for (const [key, snap] of byPair.entries()) {
|
|
22265
|
+
const finding = this.evaluatePair(key, snap, now);
|
|
22266
|
+
if (finding) findings.push(finding);
|
|
22267
|
+
}
|
|
22268
|
+
const newPartnersBySource = computeNewPartners(byPair);
|
|
22269
|
+
for (const [source, partners] of newPartnersBySource.entries()) {
|
|
22270
|
+
const finding = this.buildNewPartnerFinding(source, partners, now);
|
|
22271
|
+
if (finding) findings.push(finding);
|
|
22272
|
+
}
|
|
22273
|
+
return findings;
|
|
22274
|
+
}
|
|
22275
|
+
/** Reset baseline-established memoization. Tests use this between runs. */
|
|
22276
|
+
resetBaselineMemo() {
|
|
22277
|
+
this.baselineEstablished.clear();
|
|
22278
|
+
}
|
|
22279
|
+
evaluatePair(key, snap, now) {
|
|
22280
|
+
const currentWindow = snap.windows[0];
|
|
22281
|
+
const baselineWindows = snap.windows.slice(1);
|
|
22282
|
+
const populated = baselineWindows.filter((w) => w.count > 0).length;
|
|
22283
|
+
if (populated < BASELINE_WINDOWS2) {
|
|
22284
|
+
return null;
|
|
22285
|
+
}
|
|
22286
|
+
const counts = baselineWindows.map((w) => w.count);
|
|
22287
|
+
const mean = counts.reduce((s, c) => s + c, 0) / counts.length;
|
|
22288
|
+
const variance = counts.reduce((s, c) => s + (c - mean) ** 2, 0) / counts.length;
|
|
22289
|
+
const stddev = Math.sqrt(variance);
|
|
22290
|
+
const wasEstablished = this.baselineEstablished.has(key);
|
|
22291
|
+
this.baselineEstablished.add(key);
|
|
22292
|
+
if (!wasEstablished) {
|
|
22293
|
+
const pair = pairFromKey(key);
|
|
22294
|
+
return {
|
|
22295
|
+
finding_id: "",
|
|
22296
|
+
sentinel_id: this.sentinelId,
|
|
22297
|
+
severity: "info",
|
|
22298
|
+
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).`,
|
|
22299
|
+
details: {
|
|
22300
|
+
sender_agent_id: pair.sender,
|
|
22301
|
+
recipient_agent_id: pair.recipient,
|
|
22302
|
+
baseline_mean: mean,
|
|
22303
|
+
baseline_stddev: stddev,
|
|
22304
|
+
baseline_windows: counts,
|
|
22305
|
+
current_count: currentWindow.count
|
|
22306
|
+
},
|
|
22307
|
+
observed_at: now.toISOString(),
|
|
22308
|
+
evidence_audit_ids: [],
|
|
22309
|
+
fortress_id: ""
|
|
22310
|
+
};
|
|
22311
|
+
}
|
|
22312
|
+
const warnThreshold = mean + WARN_SIGMA2 * stddev;
|
|
22313
|
+
const alertThreshold = mean + ALERT_SIGMA2 * stddev;
|
|
22314
|
+
if (currentWindow.count > alertThreshold) {
|
|
22315
|
+
return this.buildRateSpike(key, snap, mean, stddev, now, "alert", ALERT_SIGMA2);
|
|
22316
|
+
}
|
|
22317
|
+
if (currentWindow.count > warnThreshold) {
|
|
22318
|
+
return this.buildRateSpike(key, snap, mean, stddev, now, "warn", WARN_SIGMA2);
|
|
22319
|
+
}
|
|
22320
|
+
return null;
|
|
22321
|
+
}
|
|
22322
|
+
buildRateSpike(key, snap, mean, stddev, now, severity, sigma) {
|
|
22323
|
+
const pair = pairFromKey(key);
|
|
22324
|
+
const cur = snap.windows[0];
|
|
22325
|
+
const ratio = mean === 0 ? Infinity : cur.count / mean;
|
|
22326
|
+
const ratioStr = Number.isFinite(ratio) ? `${ratio.toFixed(1)}x normal` : "no prior baseline";
|
|
22327
|
+
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.`;
|
|
22328
|
+
return {
|
|
22329
|
+
finding_id: "",
|
|
22330
|
+
sentinel_id: this.sentinelId,
|
|
22331
|
+
severity,
|
|
22332
|
+
summary,
|
|
22333
|
+
details: {
|
|
22334
|
+
sender_agent_id: pair.sender,
|
|
22335
|
+
recipient_agent_id: pair.recipient,
|
|
22336
|
+
current_count: cur.count,
|
|
22337
|
+
baseline_mean: mean,
|
|
22338
|
+
baseline_stddev: stddev,
|
|
22339
|
+
sigma_threshold: sigma,
|
|
22340
|
+
ratio
|
|
22341
|
+
},
|
|
22342
|
+
observed_at: now.toISOString(),
|
|
22343
|
+
agent_id: pair.sender,
|
|
22344
|
+
evidence_audit_ids: cur.evidence_audit_ids,
|
|
22345
|
+
fortress_id: ""
|
|
22346
|
+
};
|
|
22347
|
+
}
|
|
22348
|
+
buildNewPartnerFinding(source, info, now) {
|
|
22349
|
+
if (info.partners.length === 0) return null;
|
|
22350
|
+
const severity = info.partners.length >= MULTI_NEW_PARTNER_ALERT_THRESHOLD ? "alert" : "warn";
|
|
22351
|
+
const partnerList = info.partners.join(", ");
|
|
22352
|
+
const baselinePartnerList = info.priorPartners.length === 0 ? "no prior partners" : info.priorPartners.join(", ");
|
|
22353
|
+
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}.`;
|
|
22354
|
+
return {
|
|
22355
|
+
finding_id: "",
|
|
22356
|
+
sentinel_id: this.sentinelId,
|
|
22357
|
+
severity,
|
|
22358
|
+
summary,
|
|
22359
|
+
details: {
|
|
22360
|
+
sender_agent_id: source,
|
|
22361
|
+
new_partners: info.partners,
|
|
22362
|
+
prior_partners: info.priorPartners,
|
|
22363
|
+
new_partner_count: info.partners.length,
|
|
22364
|
+
multi_new_partner_threshold: MULTI_NEW_PARTNER_ALERT_THRESHOLD
|
|
22365
|
+
},
|
|
22366
|
+
observed_at: now.toISOString(),
|
|
22367
|
+
agent_id: source,
|
|
22368
|
+
evidence_audit_ids: info.evidenceAuditIds,
|
|
22369
|
+
fortress_id: ""
|
|
22370
|
+
};
|
|
22371
|
+
}
|
|
22372
|
+
};
|
|
22373
|
+
}
|
|
22374
|
+
});
|
|
22375
|
+
|
|
22376
|
+
// src/sentinel/sentinels/credential-usage-watcher.ts
|
|
22377
|
+
function isCredentialAuditEntry(entry) {
|
|
22378
|
+
if (entry.result !== "success") return false;
|
|
22379
|
+
return entry.operation === BROKER_SECRET_READ_OP || entry.operation === BROKER_TOKEN_ISSUED_OP;
|
|
22380
|
+
}
|
|
22381
|
+
function extractAgentId(entry) {
|
|
22382
|
+
const details = entry.details;
|
|
22383
|
+
if (!details) return null;
|
|
22384
|
+
const agent = details["agent"];
|
|
22385
|
+
return typeof agent === "string" && agent.length > 0 ? agent : null;
|
|
22386
|
+
}
|
|
22387
|
+
function extractSecretId(entry) {
|
|
22388
|
+
const details = entry.details;
|
|
22389
|
+
if (!details) return null;
|
|
22390
|
+
const secret = details["secret"];
|
|
22391
|
+
return typeof secret === "string" && secret.length > 0 ? secret : null;
|
|
22392
|
+
}
|
|
22393
|
+
function enumerateUnorderedPairs(secrets) {
|
|
22394
|
+
const out = /* @__PURE__ */ new Set();
|
|
22395
|
+
const arr = [...secrets].sort();
|
|
22396
|
+
for (let i = 0; i < arr.length; i += 1) {
|
|
22397
|
+
for (let j = i + 1; j < arr.length; j += 1) {
|
|
22398
|
+
out.add(`${arr[i]}\0${arr[j]}`);
|
|
22399
|
+
}
|
|
22400
|
+
}
|
|
22401
|
+
return out;
|
|
22402
|
+
}
|
|
22403
|
+
function buildNewPairSummary(agentId, newPairs) {
|
|
22404
|
+
const first = newPairs[0];
|
|
22405
|
+
if (newPairs.length === 1) {
|
|
22406
|
+
return `${agentId} agent used ${first[0]} and ${first[1]} together for the first time today. This combination does not appear in historical sessions.`;
|
|
22407
|
+
}
|
|
22408
|
+
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.`;
|
|
22409
|
+
}
|
|
22410
|
+
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;
|
|
22411
|
+
var init_credential_usage_watcher = __esm({
|
|
22412
|
+
"src/sentinel/sentinels/credential-usage-watcher.ts"() {
|
|
22413
|
+
init_sentinel();
|
|
22414
|
+
CREDENTIAL_USAGE_SENTINEL_ID = "credential-usage";
|
|
22415
|
+
WARN_SIGMA3 = 3;
|
|
22416
|
+
ALERT_SIGMA3 = 6;
|
|
22417
|
+
NEW_PAIR_ALERT_COUNT = 3;
|
|
22418
|
+
BASELINE_WINDOWS3 = 7;
|
|
22419
|
+
QUERY_LIMIT3 = 2e4;
|
|
22420
|
+
BROKER_SECRET_READ_OP = "broker_secret_read";
|
|
22421
|
+
BROKER_TOKEN_ISSUED_OP = "broker_token_issued";
|
|
22422
|
+
CredentialUsageWatcher = class extends Sentinel {
|
|
22423
|
+
sentinelId = CREDENTIAL_USAGE_SENTINEL_ID;
|
|
22424
|
+
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.";
|
|
22425
|
+
/**
|
|
22426
|
+
* Memoization of (agent, secret) pairs whose baseline has been
|
|
22427
|
+
* established. Same shape Phi-1 uses to avoid re-emitting `info`
|
|
22428
|
+
* findings on every tick after a baseline first establishes.
|
|
22429
|
+
*
|
|
22430
|
+
* Phi-2 deliberately does NOT emit `info` findings: per-pair
|
|
22431
|
+
* baselines on a busy fortress would be too noisy. The memo is
|
|
22432
|
+
* kept here for parity with Phi-1's reset hook so tests can clear
|
|
22433
|
+
* state between runs.
|
|
22434
|
+
*/
|
|
22435
|
+
baselineEstablished = /* @__PURE__ */ new Set();
|
|
22436
|
+
/** Reset memoization. Tests use this between runs. */
|
|
22437
|
+
resetBaselineMemo() {
|
|
22438
|
+
this.baselineEstablished.clear();
|
|
22439
|
+
}
|
|
22440
|
+
async evaluate() {
|
|
22441
|
+
const ctx = this.requireContext();
|
|
22442
|
+
const now = ctx.now();
|
|
22443
|
+
const windowMs = 24 * 60 * 60 * 1e3;
|
|
22444
|
+
const windowSpanMs = (BASELINE_WINDOWS3 + 1) * windowMs;
|
|
22445
|
+
const sinceIso = new Date(now.getTime() - windowSpanMs).toISOString();
|
|
22446
|
+
const queryResult = await ctx.auditLog.query({
|
|
22447
|
+
since: sinceIso,
|
|
22448
|
+
layer: "l3",
|
|
22449
|
+
limit: QUERY_LIMIT3
|
|
22450
|
+
});
|
|
22451
|
+
const entries = queryResult.entries.filter(isCredentialAuditEntry);
|
|
22452
|
+
const byPair = /* @__PURE__ */ new Map();
|
|
22453
|
+
const byAgent = /* @__PURE__ */ new Map();
|
|
22454
|
+
for (const entry of entries) {
|
|
22455
|
+
const agentId = extractAgentId(entry);
|
|
22456
|
+
const secretId = extractSecretId(entry);
|
|
22457
|
+
if (agentId === null || secretId === null) continue;
|
|
22458
|
+
const auditAge = now.getTime() - new Date(entry.timestamp).getTime();
|
|
22459
|
+
if (auditAge < 0) continue;
|
|
22460
|
+
const windowIdx = Math.floor(auditAge / windowMs);
|
|
22461
|
+
if (windowIdx > BASELINE_WINDOWS3) continue;
|
|
22462
|
+
const pairKey2 = `${agentId}\0${secretId}`;
|
|
22463
|
+
let pairSnapshot = byPair.get(pairKey2);
|
|
22464
|
+
if (!pairSnapshot) {
|
|
22465
|
+
pairSnapshot = {
|
|
22466
|
+
windows: Array.from({ length: BASELINE_WINDOWS3 + 1 }, () => ({
|
|
22467
|
+
count: 0,
|
|
22468
|
+
evidence_audit_ids: []
|
|
22469
|
+
}))
|
|
22470
|
+
};
|
|
22471
|
+
byPair.set(pairKey2, pairSnapshot);
|
|
22472
|
+
}
|
|
22473
|
+
const bucket = pairSnapshot.windows[windowIdx];
|
|
22474
|
+
bucket.count += 1;
|
|
22475
|
+
if (windowIdx === 0 && bucket.evidence_audit_ids.length < 50) {
|
|
22476
|
+
bucket.evidence_audit_ids.push(
|
|
22477
|
+
`${entry.timestamp}:${entry.operation}`
|
|
22478
|
+
);
|
|
22479
|
+
}
|
|
22480
|
+
let agentState = byAgent.get(agentId);
|
|
22481
|
+
if (!agentState) {
|
|
22482
|
+
agentState = {
|
|
22483
|
+
currentSecrets: /* @__PURE__ */ new Set(),
|
|
22484
|
+
currentEvidence: [],
|
|
22485
|
+
baselineSecretsByWindow: Array.from(
|
|
22486
|
+
{ length: BASELINE_WINDOWS3 },
|
|
22487
|
+
() => /* @__PURE__ */ new Set()
|
|
22488
|
+
),
|
|
22489
|
+
baselinePopulatedWindows: 0
|
|
22490
|
+
};
|
|
22491
|
+
byAgent.set(agentId, agentState);
|
|
22492
|
+
}
|
|
22493
|
+
if (windowIdx === 0) {
|
|
22494
|
+
agentState.currentSecrets.add(secretId);
|
|
22495
|
+
if (agentState.currentEvidence.length < 50) {
|
|
22496
|
+
agentState.currentEvidence.push(
|
|
22497
|
+
`${entry.timestamp}:${entry.operation}`
|
|
22498
|
+
);
|
|
22499
|
+
}
|
|
22500
|
+
} else {
|
|
22501
|
+
const baselineIdx = windowIdx - 1;
|
|
22502
|
+
agentState.baselineSecretsByWindow[baselineIdx].add(secretId);
|
|
22503
|
+
}
|
|
22504
|
+
}
|
|
22505
|
+
const findings = [];
|
|
22506
|
+
for (const [pairKey2, snapshot] of byPair.entries()) {
|
|
22507
|
+
const [agentId, secretId] = pairKey2.split("\0");
|
|
22508
|
+
const finding = this.evaluateRateSpike(
|
|
22509
|
+
agentId,
|
|
22510
|
+
secretId,
|
|
22511
|
+
snapshot,
|
|
22512
|
+
now
|
|
22513
|
+
);
|
|
22514
|
+
if (finding) findings.push(finding);
|
|
22515
|
+
}
|
|
22516
|
+
for (const [agentId, agentState] of byAgent.entries()) {
|
|
22517
|
+
agentState.baselinePopulatedWindows = agentState.baselineSecretsByWindow.filter((s) => s.size > 0).length;
|
|
22518
|
+
const finding = this.evaluateNewPairs(agentId, agentState, now);
|
|
22519
|
+
if (finding) findings.push(finding);
|
|
22520
|
+
}
|
|
22521
|
+
return findings;
|
|
22522
|
+
}
|
|
22523
|
+
evaluateRateSpike(agentId, secretId, snapshot, now) {
|
|
22524
|
+
const currentWindow = snapshot.windows[0];
|
|
22525
|
+
const baselineWindows = snapshot.windows.slice(1);
|
|
22526
|
+
const populated = baselineWindows.filter((w) => w.count > 0).length;
|
|
22527
|
+
if (populated < BASELINE_WINDOWS3) {
|
|
22528
|
+
return null;
|
|
22529
|
+
}
|
|
22530
|
+
const counts = baselineWindows.map((w) => w.count);
|
|
22531
|
+
const mean = counts.reduce((sum, c) => sum + c, 0) / counts.length;
|
|
22532
|
+
const variance = counts.reduce((sum, c) => sum + (c - mean) ** 2, 0) / counts.length;
|
|
22533
|
+
const stddev = Math.sqrt(variance);
|
|
22534
|
+
const pairKey2 = `${agentId}\0${secretId}`;
|
|
22535
|
+
this.baselineEstablished.add(pairKey2);
|
|
22536
|
+
const warnThreshold = mean + WARN_SIGMA3 * stddev;
|
|
22537
|
+
const alertThreshold = mean + ALERT_SIGMA3 * stddev;
|
|
22538
|
+
if (currentWindow.count > alertThreshold) {
|
|
22539
|
+
return this.buildRateFinding(
|
|
22540
|
+
agentId,
|
|
22541
|
+
secretId,
|
|
22542
|
+
currentWindow,
|
|
22543
|
+
mean,
|
|
22544
|
+
stddev,
|
|
22545
|
+
now,
|
|
22546
|
+
"alert",
|
|
22547
|
+
ALERT_SIGMA3
|
|
22548
|
+
);
|
|
22549
|
+
}
|
|
22550
|
+
if (currentWindow.count > warnThreshold) {
|
|
22551
|
+
return this.buildRateFinding(
|
|
22552
|
+
agentId,
|
|
22553
|
+
secretId,
|
|
22554
|
+
currentWindow,
|
|
22555
|
+
mean,
|
|
22556
|
+
stddev,
|
|
22557
|
+
now,
|
|
22558
|
+
"warn",
|
|
22559
|
+
WARN_SIGMA3
|
|
22560
|
+
);
|
|
22561
|
+
}
|
|
22562
|
+
return null;
|
|
22563
|
+
}
|
|
22564
|
+
buildRateFinding(agentId, secretId, currentWindow, mean, stddev, now, severity, sigma) {
|
|
22565
|
+
const ratio = mean === 0 ? Number.POSITIVE_INFINITY : currentWindow.count / mean;
|
|
22566
|
+
const ratioStr = Number.isFinite(ratio) ? `${ratio.toFixed(1)}x normal` : "no prior baseline";
|
|
22567
|
+
const summary = `${agentId} agent used ${secretId} ${currentWindow.count} times in last 24h, baseline ${mean.toFixed(1)} +/- ${stddev.toFixed(1)} (${ratioStr}). Crossed +${sigma} sigma threshold.`;
|
|
22568
|
+
return {
|
|
22569
|
+
finding_id: "",
|
|
22570
|
+
sentinel_id: this.sentinelId,
|
|
22571
|
+
severity,
|
|
22572
|
+
agent_id: agentId,
|
|
22573
|
+
summary,
|
|
22574
|
+
details: {
|
|
22575
|
+
agent_id: agentId,
|
|
22576
|
+
secret_id: secretId,
|
|
22577
|
+
current_count: currentWindow.count,
|
|
22578
|
+
baseline_mean: mean,
|
|
22579
|
+
baseline_stddev: stddev,
|
|
22580
|
+
sigma_threshold: sigma,
|
|
22581
|
+
ratio: Number.isFinite(ratio) ? ratio : null
|
|
22582
|
+
},
|
|
22583
|
+
observed_at: now.toISOString(),
|
|
22584
|
+
evidence_audit_ids: currentWindow.evidence_audit_ids,
|
|
22585
|
+
fortress_id: ""
|
|
22586
|
+
};
|
|
22587
|
+
}
|
|
22588
|
+
evaluateNewPairs(agentId, state, now) {
|
|
22589
|
+
if (state.baselinePopulatedWindows < BASELINE_WINDOWS3) {
|
|
22590
|
+
return null;
|
|
22591
|
+
}
|
|
22592
|
+
if (state.currentSecrets.size < 2) return null;
|
|
22593
|
+
const currentPairs = enumerateUnorderedPairs(state.currentSecrets);
|
|
22594
|
+
const historicalPairs = /* @__PURE__ */ new Set();
|
|
22595
|
+
for (const secretSet of state.baselineSecretsByWindow) {
|
|
22596
|
+
for (const pair of enumerateUnorderedPairs(secretSet)) {
|
|
22597
|
+
historicalPairs.add(pair);
|
|
22598
|
+
}
|
|
22599
|
+
}
|
|
22600
|
+
const newPairs = [];
|
|
22601
|
+
for (const pair of currentPairs) {
|
|
22602
|
+
if (historicalPairs.has(pair)) continue;
|
|
22603
|
+
const [a, b] = pair.split("\0");
|
|
22604
|
+
newPairs.push([a, b]);
|
|
22605
|
+
}
|
|
22606
|
+
if (newPairs.length === 0) return null;
|
|
22607
|
+
const severity = newPairs.length >= NEW_PAIR_ALERT_COUNT ? "alert" : "warn";
|
|
22608
|
+
const summary = buildNewPairSummary(agentId, newPairs);
|
|
22609
|
+
return {
|
|
22610
|
+
finding_id: "",
|
|
22611
|
+
sentinel_id: this.sentinelId,
|
|
22612
|
+
severity,
|
|
22613
|
+
agent_id: agentId,
|
|
22614
|
+
summary,
|
|
22615
|
+
details: {
|
|
22616
|
+
agent_id: agentId,
|
|
22617
|
+
new_pairs: newPairs,
|
|
22618
|
+
new_pair_count: newPairs.length,
|
|
22619
|
+
historical_pair_count: historicalPairs.size,
|
|
22620
|
+
current_pair_count: currentPairs.size
|
|
22621
|
+
},
|
|
22622
|
+
observed_at: now.toISOString(),
|
|
22623
|
+
evidence_audit_ids: state.currentEvidence,
|
|
22624
|
+
fortress_id: ""
|
|
22625
|
+
};
|
|
22626
|
+
}
|
|
22627
|
+
};
|
|
22628
|
+
}
|
|
22629
|
+
});
|
|
22630
|
+
|
|
22631
|
+
// src/sentinel/sentinels/suspicious-tool-call-detector.ts
|
|
22632
|
+
function countTruncatedValues(args) {
|
|
22633
|
+
let n = 0;
|
|
22634
|
+
for (const v of Object.values(args)) {
|
|
22635
|
+
if (typeof v === "string" && v.endsWith("...")) n += 1;
|
|
22636
|
+
}
|
|
22637
|
+
return n;
|
|
22638
|
+
}
|
|
22639
|
+
function countUrlEncoded(value) {
|
|
22640
|
+
const matches = value.match(/%[0-9a-fA-F]{2}/g);
|
|
22641
|
+
return matches ? matches.length : 0;
|
|
22642
|
+
}
|
|
22643
|
+
function longestBase64Run(value) {
|
|
22644
|
+
const matches = value.match(/[A-Za-z0-9+/=]{40,}/g);
|
|
22645
|
+
if (!matches) return 0;
|
|
22646
|
+
return matches.reduce((max, m) => m.length > max ? m.length : max, 0);
|
|
22647
|
+
}
|
|
22648
|
+
function extractArgsSummary(details) {
|
|
22649
|
+
if (!details) return {};
|
|
22650
|
+
const summary = details["args_summary"];
|
|
22651
|
+
if (summary && typeof summary === "object" && !Array.isArray(summary)) {
|
|
22652
|
+
return summary;
|
|
22653
|
+
}
|
|
22654
|
+
return {};
|
|
22655
|
+
}
|
|
22656
|
+
function truncateSummary2(s) {
|
|
22657
|
+
return s.length > 240 ? s.slice(0, 237) + "..." : s;
|
|
22658
|
+
}
|
|
22659
|
+
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;
|
|
22660
|
+
var init_suspicious_tool_call_detector = __esm({
|
|
22661
|
+
"src/sentinel/sentinels/suspicious-tool-call-detector.ts"() {
|
|
22662
|
+
init_sentinel();
|
|
22663
|
+
SUSPICIOUS_TOOL_CALL_SENTINEL_ID = "suspicious-tool-call";
|
|
22664
|
+
GATE_PREFIXES = [
|
|
22665
|
+
"gate_allow:",
|
|
22666
|
+
"gate_allow_proxy:",
|
|
22667
|
+
"gate_deny:",
|
|
22668
|
+
"gate_unclassified:"
|
|
22669
|
+
];
|
|
22670
|
+
WARN_SIGMA4 = 3;
|
|
22671
|
+
ALERT_SIGMA4 = 6;
|
|
22672
|
+
BASELINE_WINDOWS4 = 7;
|
|
22673
|
+
ALERT_NOVEL_COMBINATIONS = 2;
|
|
22674
|
+
TASK_WINDOW_MS = 60 * 60 * 1e3;
|
|
22675
|
+
TRUNCATION_WARN_THRESHOLD = 5;
|
|
22676
|
+
QUERY_LIMIT4 = 1e4;
|
|
22677
|
+
SIGNATURE_PATTERNS = {
|
|
22678
|
+
/** >=5 percent-encoded sequences in a single visible value. */
|
|
22679
|
+
urlEncodedThreshold: 5,
|
|
22680
|
+
/** >=40 contiguous base64 chars in a single visible value. */
|
|
22681
|
+
base64MinRun: 40,
|
|
22682
|
+
/** Shell metacharacter set. */
|
|
22683
|
+
shellMetacharRegex: /(?:&&|\|\||;|\$\(|`|\|\s)/
|
|
22684
|
+
};
|
|
22685
|
+
SuspiciousToolCallDetector = class extends Sentinel {
|
|
22686
|
+
sentinelId = SUSPICIOUS_TOOL_CALL_SENTINEL_ID;
|
|
22687
|
+
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.";
|
|
22688
|
+
/** Servers we have already produced an `info` baseline-established finding for. */
|
|
22689
|
+
baselineEstablished = /* @__PURE__ */ new Set();
|
|
22690
|
+
/** Memoized known novel-combination keys (sorted-tools-csv). */
|
|
22691
|
+
knownCombinations = /* @__PURE__ */ new Set();
|
|
22692
|
+
/** Tasks observed where a novel combination already produced a finding. */
|
|
22693
|
+
novelCombinationsReported = /* @__PURE__ */ new Set();
|
|
22694
|
+
async evaluate() {
|
|
22695
|
+
const ctx = this.requireContext();
|
|
22696
|
+
const now = ctx.now();
|
|
22697
|
+
const dayMs = 24 * 60 * 60 * 1e3;
|
|
22698
|
+
const windowSpanMs = (BASELINE_WINDOWS4 + 1) * dayMs;
|
|
22699
|
+
const sinceIso = new Date(now.getTime() - windowSpanMs).toISOString();
|
|
22700
|
+
const queryResult = await ctx.auditLog.query({
|
|
22701
|
+
since: sinceIso,
|
|
22702
|
+
layer: "l2",
|
|
22703
|
+
limit: QUERY_LIMIT4
|
|
22704
|
+
});
|
|
22705
|
+
const observations = [];
|
|
22706
|
+
for (const entry of queryResult.entries) {
|
|
22707
|
+
const obs = this.observationFromEntry(entry);
|
|
22708
|
+
if (obs && obs.ts <= now.getTime()) observations.push(obs);
|
|
22709
|
+
}
|
|
22710
|
+
if (observations.length === 0) return [];
|
|
22711
|
+
const findings = [];
|
|
22712
|
+
const layerAFindings = await this.runLayerA(observations, now, ctx);
|
|
22713
|
+
findings.push(...layerAFindings);
|
|
22714
|
+
const layerBFindings = this.runLayerB(observations, now);
|
|
22715
|
+
findings.push(...layerBFindings);
|
|
22716
|
+
const layerCFindings = this.runLayerC(observations, now);
|
|
22717
|
+
findings.push(...layerCFindings);
|
|
22718
|
+
return findings;
|
|
22719
|
+
}
|
|
22720
|
+
/** Reset memoization between test runs. Mirrors Phi-1's reset hook. */
|
|
22721
|
+
resetMemo() {
|
|
22722
|
+
this.baselineEstablished.clear();
|
|
22723
|
+
this.knownCombinations.clear();
|
|
22724
|
+
this.novelCombinationsReported.clear();
|
|
22725
|
+
}
|
|
22726
|
+
// ── Layer A ───────────────────────────────────────────────────────
|
|
22727
|
+
async runLayerA(observations, now, ctx) {
|
|
22728
|
+
const dayMs = 24 * 60 * 60 * 1e3;
|
|
22729
|
+
const recent = observations.filter(
|
|
22730
|
+
(o) => now.getTime() - o.ts <= dayMs
|
|
22731
|
+
);
|
|
22732
|
+
if (recent.length === 0) return [];
|
|
22733
|
+
const findings = [];
|
|
22734
|
+
const historical = observations.filter(
|
|
22735
|
+
(o) => now.getTime() - o.ts > dayMs
|
|
22736
|
+
);
|
|
22737
|
+
const perTool = /* @__PURE__ */ new Map();
|
|
22738
|
+
const ensureTool = (tool) => {
|
|
22739
|
+
let w = perTool.get(tool);
|
|
22740
|
+
if (!w) {
|
|
22741
|
+
w = {
|
|
22742
|
+
windows: Array.from({ length: BASELINE_WINDOWS4 + 1 }, () => ({
|
|
22743
|
+
count: 0,
|
|
22744
|
+
evidenceIds: []
|
|
22745
|
+
})),
|
|
22746
|
+
knownSignatures: /* @__PURE__ */ new Set()
|
|
22747
|
+
};
|
|
22748
|
+
perTool.set(tool, w);
|
|
22749
|
+
}
|
|
22750
|
+
return w;
|
|
22751
|
+
};
|
|
22752
|
+
for (const o of historical) {
|
|
22753
|
+
ensureTool(o.tool).knownSignatures.add(this.signatureOf(o.argsSummary));
|
|
22754
|
+
}
|
|
22755
|
+
const classify = this.classifyHandle(ctx);
|
|
22756
|
+
for (const obs of recent) {
|
|
22757
|
+
const matches = this.matchSignatures(obs);
|
|
22758
|
+
if (matches.length === 0) continue;
|
|
22759
|
+
const ambiguous = matches.every((m) => m === "base64_chunk");
|
|
22760
|
+
if (ambiguous && classify) {
|
|
22761
|
+
const verdict = await this.consultClassifier(classify, obs);
|
|
22762
|
+
if (verdict !== "suspicious") continue;
|
|
22763
|
+
}
|
|
22764
|
+
findings.push(
|
|
22765
|
+
this.buildLayerAFinding(obs, matches, now, classify ? "llm-assist" : "rule-based")
|
|
22766
|
+
);
|
|
22767
|
+
}
|
|
22768
|
+
for (const obs of recent) {
|
|
22769
|
+
const tool = obs.tool;
|
|
22770
|
+
const sig = this.signatureOf(obs.argsSummary);
|
|
22771
|
+
const known = perTool.get(tool)?.knownSignatures;
|
|
22772
|
+
if (known && known.size > 0 && !known.has(sig)) {
|
|
22773
|
+
findings.push(
|
|
22774
|
+
this.buildNovelSignatureFinding(obs, sig, now)
|
|
22775
|
+
);
|
|
22776
|
+
}
|
|
22777
|
+
}
|
|
22778
|
+
return findings;
|
|
22779
|
+
}
|
|
22780
|
+
matchSignatures(obs) {
|
|
22781
|
+
const out = [];
|
|
22782
|
+
const truncCount = countTruncatedValues(obs.argsSummary);
|
|
22783
|
+
if (truncCount >= TRUNCATION_WARN_THRESHOLD) out.push("truncation_burst");
|
|
22784
|
+
let urlBlob = false;
|
|
22785
|
+
let base64Blob = false;
|
|
22786
|
+
let shellChars = false;
|
|
22787
|
+
for (const value of Object.values(obs.argsSummary)) {
|
|
22788
|
+
if (typeof value !== "string") continue;
|
|
22789
|
+
if (countUrlEncoded(value) >= SIGNATURE_PATTERNS.urlEncodedThreshold) {
|
|
22790
|
+
urlBlob = true;
|
|
22791
|
+
}
|
|
22792
|
+
if (longestBase64Run(value) >= SIGNATURE_PATTERNS.base64MinRun) {
|
|
22793
|
+
base64Blob = true;
|
|
22794
|
+
}
|
|
22795
|
+
if (SIGNATURE_PATTERNS.shellMetacharRegex.test(value)) {
|
|
22796
|
+
shellChars = true;
|
|
22797
|
+
}
|
|
22798
|
+
}
|
|
22799
|
+
if (urlBlob) out.push("url_encoded_blob");
|
|
22800
|
+
if (base64Blob) out.push("base64_chunk");
|
|
22801
|
+
if (shellChars) out.push("shell_metachar");
|
|
22802
|
+
return out;
|
|
22803
|
+
}
|
|
22804
|
+
signatureOf(argsSummary) {
|
|
22805
|
+
return Object.keys(argsSummary).sort().join(",");
|
|
22806
|
+
}
|
|
22807
|
+
buildLayerAFinding(obs, matches, now, detectionPath) {
|
|
22808
|
+
const severity = matches.includes("shell_metachar") ? "alert" : "warn";
|
|
22809
|
+
const summary = `${obs.tool}: tool-call argument matches signature ${matches.join(", ")} (${detectionPath}).`;
|
|
22810
|
+
return {
|
|
22811
|
+
finding_id: "",
|
|
22812
|
+
sentinel_id: this.sentinelId,
|
|
22813
|
+
severity,
|
|
22814
|
+
summary: truncateSummary2(summary),
|
|
22815
|
+
details: {
|
|
22816
|
+
layer: "A",
|
|
22817
|
+
tool: obs.tool,
|
|
22818
|
+
proxy: obs.proxy,
|
|
22819
|
+
signatures: matches,
|
|
22820
|
+
detection_path: detectionPath
|
|
22821
|
+
},
|
|
22822
|
+
observed_at: now.toISOString(),
|
|
22823
|
+
evidence_audit_ids: [`${obs.entry.timestamp}:${obs.entry.operation}`],
|
|
22824
|
+
fortress_id: ""
|
|
22825
|
+
};
|
|
22826
|
+
}
|
|
22827
|
+
buildNovelSignatureFinding(obs, signature, now) {
|
|
22828
|
+
return {
|
|
22829
|
+
finding_id: "",
|
|
22830
|
+
sentinel_id: this.sentinelId,
|
|
22831
|
+
severity: "warn",
|
|
22832
|
+
summary: truncateSummary2(
|
|
22833
|
+
`${obs.tool}: novel argument-key signature observed (${signature || "<no-args>"}).`
|
|
22834
|
+
),
|
|
22835
|
+
details: {
|
|
22836
|
+
layer: "A",
|
|
22837
|
+
tool: obs.tool,
|
|
22838
|
+
proxy: obs.proxy,
|
|
22839
|
+
signatures: ["novel_signature"],
|
|
22840
|
+
detection_path: "rule-based",
|
|
22841
|
+
novel_signature: signature
|
|
22842
|
+
},
|
|
22843
|
+
observed_at: now.toISOString(),
|
|
22844
|
+
evidence_audit_ids: [`${obs.entry.timestamp}:${obs.entry.operation}`],
|
|
22845
|
+
fortress_id: ""
|
|
22846
|
+
};
|
|
22847
|
+
}
|
|
22848
|
+
// ── Layer B ───────────────────────────────────────────────────────
|
|
22849
|
+
runLayerB(observations, now) {
|
|
22850
|
+
const dayMs = 24 * 60 * 60 * 1e3;
|
|
22851
|
+
const perTool = /* @__PURE__ */ new Map();
|
|
22852
|
+
for (const obs of observations) {
|
|
22853
|
+
const ageMs = now.getTime() - obs.ts;
|
|
22854
|
+
if (ageMs < 0) continue;
|
|
22855
|
+
const windowIdx = Math.floor(ageMs / dayMs);
|
|
22856
|
+
if (windowIdx > BASELINE_WINDOWS4) continue;
|
|
22857
|
+
let w = perTool.get(obs.tool);
|
|
22858
|
+
if (!w) {
|
|
22859
|
+
w = {
|
|
22860
|
+
windows: Array.from({ length: BASELINE_WINDOWS4 + 1 }, () => ({
|
|
22861
|
+
count: 0,
|
|
22862
|
+
evidenceIds: []
|
|
22863
|
+
})),
|
|
22864
|
+
knownSignatures: /* @__PURE__ */ new Set()
|
|
22865
|
+
};
|
|
22866
|
+
perTool.set(obs.tool, w);
|
|
22867
|
+
}
|
|
22868
|
+
const bucket = w.windows[windowIdx];
|
|
22869
|
+
bucket.count += 1;
|
|
22870
|
+
if (windowIdx === 0 && bucket.evidenceIds.length < 50) {
|
|
22871
|
+
bucket.evidenceIds.push(`${obs.entry.timestamp}:${obs.entry.operation}`);
|
|
22872
|
+
}
|
|
22873
|
+
}
|
|
22874
|
+
const findings = [];
|
|
22875
|
+
for (const [tool, w] of perTool.entries()) {
|
|
22876
|
+
const f = this.evaluateToolFrequency(tool, w, now);
|
|
22877
|
+
if (f) findings.push(f);
|
|
22878
|
+
}
|
|
22879
|
+
return findings;
|
|
22880
|
+
}
|
|
22881
|
+
evaluateToolFrequency(tool, w, now) {
|
|
22882
|
+
const current = w.windows[0];
|
|
22883
|
+
const baseline = w.windows.slice(1);
|
|
22884
|
+
const populated = baseline.filter((b) => b.count > 0).length;
|
|
22885
|
+
if (populated < BASELINE_WINDOWS4) {
|
|
22886
|
+
this.baselineEstablished.add(tool);
|
|
22887
|
+
return null;
|
|
22888
|
+
}
|
|
22889
|
+
const counts = baseline.map((b) => b.count);
|
|
22890
|
+
const mean = counts.reduce((sum, c) => sum + c, 0) / counts.length;
|
|
22891
|
+
const variance = counts.reduce((sum, c) => sum + (c - mean) ** 2, 0) / counts.length;
|
|
22892
|
+
const stddev = Math.sqrt(variance);
|
|
22893
|
+
const wasEstablished = this.baselineEstablished.has(tool);
|
|
22894
|
+
this.baselineEstablished.add(tool);
|
|
22895
|
+
if (!wasEstablished) {
|
|
22896
|
+
return {
|
|
22897
|
+
finding_id: "",
|
|
22898
|
+
sentinel_id: this.sentinelId,
|
|
22899
|
+
severity: "info",
|
|
22900
|
+
summary: truncateSummary2(
|
|
22901
|
+
`${tool}: tool-call baseline established (mean ${mean.toFixed(1)} calls/24h, stddev ${stddev.toFixed(1)} over ${BASELINE_WINDOWS4} prior days).`
|
|
22902
|
+
),
|
|
22903
|
+
details: {
|
|
22904
|
+
layer: "B",
|
|
22905
|
+
tool,
|
|
22906
|
+
baseline_mean: mean,
|
|
22907
|
+
baseline_stddev: stddev,
|
|
22908
|
+
baseline_counts: counts,
|
|
22909
|
+
current_count: current.count
|
|
22910
|
+
},
|
|
22911
|
+
observed_at: now.toISOString(),
|
|
22912
|
+
evidence_audit_ids: [],
|
|
22913
|
+
fortress_id: ""
|
|
22914
|
+
};
|
|
22915
|
+
}
|
|
22916
|
+
const warnT = mean + WARN_SIGMA4 * stddev;
|
|
22917
|
+
const alertT = mean + ALERT_SIGMA4 * stddev;
|
|
22918
|
+
if (current.count > alertT) {
|
|
22919
|
+
return this.buildLayerBAnomaly(
|
|
22920
|
+
tool,
|
|
22921
|
+
current,
|
|
22922
|
+
mean,
|
|
22923
|
+
stddev,
|
|
22924
|
+
ALERT_SIGMA4,
|
|
22925
|
+
"alert",
|
|
22926
|
+
now
|
|
22927
|
+
);
|
|
22928
|
+
}
|
|
22929
|
+
if (current.count > warnT) {
|
|
22930
|
+
return this.buildLayerBAnomaly(
|
|
22931
|
+
tool,
|
|
22932
|
+
current,
|
|
22933
|
+
mean,
|
|
22934
|
+
stddev,
|
|
22935
|
+
WARN_SIGMA4,
|
|
22936
|
+
"warn",
|
|
22937
|
+
now
|
|
22938
|
+
);
|
|
22939
|
+
}
|
|
22940
|
+
return null;
|
|
22941
|
+
}
|
|
22942
|
+
buildLayerBAnomaly(tool, current, mean, stddev, sigma, severity, now) {
|
|
22943
|
+
const ratio = mean === 0 ? Infinity : current.count / mean;
|
|
22944
|
+
const ratioStr = Number.isFinite(ratio) ? `${ratio.toFixed(1)}x normal` : "no prior baseline";
|
|
22945
|
+
return {
|
|
22946
|
+
finding_id: "",
|
|
22947
|
+
sentinel_id: this.sentinelId,
|
|
22948
|
+
severity,
|
|
22949
|
+
summary: truncateSummary2(
|
|
22950
|
+
`${tool}: tool-call rate is ${ratioStr}: ${current.count} calls in last 24h, baseline ${mean.toFixed(1)} (stddev ${stddev.toFixed(1)}). Crossed +${sigma} sigma.`
|
|
22951
|
+
),
|
|
22952
|
+
details: {
|
|
22953
|
+
layer: "B",
|
|
22954
|
+
tool,
|
|
22955
|
+
current_count: current.count,
|
|
22956
|
+
baseline_mean: mean,
|
|
22957
|
+
baseline_stddev: stddev,
|
|
22958
|
+
sigma_threshold: sigma,
|
|
22959
|
+
ratio
|
|
22960
|
+
},
|
|
22961
|
+
observed_at: now.toISOString(),
|
|
22962
|
+
evidence_audit_ids: current.evidenceIds,
|
|
22963
|
+
fortress_id: ""
|
|
22964
|
+
};
|
|
22965
|
+
}
|
|
22966
|
+
// ── Layer C ───────────────────────────────────────────────────────
|
|
22967
|
+
runLayerC(observations, now) {
|
|
22968
|
+
const dayMs = 24 * 60 * 60 * 1e3;
|
|
22969
|
+
const sorted = [...observations].sort((a, b) => a.ts - b.ts);
|
|
22970
|
+
const tasks = [];
|
|
22971
|
+
for (const obs of sorted) {
|
|
22972
|
+
const last = tasks[tasks.length - 1];
|
|
22973
|
+
if (!last || obs.ts - last.startTs > TASK_WINDOW_MS) {
|
|
22974
|
+
tasks.push({ startTs: obs.ts, tools: [obs.tool] });
|
|
22975
|
+
continue;
|
|
22976
|
+
}
|
|
22977
|
+
if (!last.tools.includes(obs.tool)) last.tools.push(obs.tool);
|
|
22978
|
+
}
|
|
22979
|
+
const recentTaskKeys = [];
|
|
22980
|
+
const findings = [];
|
|
22981
|
+
for (const task of tasks) {
|
|
22982
|
+
const ageMs = now.getTime() - task.startTs;
|
|
22983
|
+
const key = task.tools.slice().sort().join(",");
|
|
22984
|
+
if (ageMs > dayMs) {
|
|
22985
|
+
this.knownCombinations.add(key);
|
|
22986
|
+
continue;
|
|
22987
|
+
}
|
|
22988
|
+
if (task.tools.length < 2) continue;
|
|
22989
|
+
if (!this.knownCombinations.has(key)) {
|
|
22990
|
+
this.knownCombinations.add(key);
|
|
22991
|
+
if (!this.novelCombinationsReported.has(key)) {
|
|
22992
|
+
this.novelCombinationsReported.add(key);
|
|
22993
|
+
recentTaskKeys.push(key);
|
|
22994
|
+
findings.push(
|
|
22995
|
+
this.buildLayerCFinding(task, key, "warn", now)
|
|
22996
|
+
);
|
|
22997
|
+
}
|
|
22998
|
+
}
|
|
22999
|
+
}
|
|
23000
|
+
if (recentTaskKeys.length >= ALERT_NOVEL_COMBINATIONS) {
|
|
23001
|
+
const aggregate = {
|
|
23002
|
+
finding_id: "",
|
|
23003
|
+
sentinel_id: this.sentinelId,
|
|
23004
|
+
severity: "alert",
|
|
23005
|
+
summary: truncateSummary2(
|
|
23006
|
+
`multi-novel-combination: ${recentTaskKeys.length} novel tool-permission combinations within last 24h.`
|
|
23007
|
+
),
|
|
23008
|
+
details: {
|
|
23009
|
+
layer: "C",
|
|
23010
|
+
novel_combinations: recentTaskKeys
|
|
23011
|
+
},
|
|
23012
|
+
observed_at: now.toISOString(),
|
|
23013
|
+
evidence_audit_ids: [],
|
|
23014
|
+
fortress_id: ""
|
|
23015
|
+
};
|
|
23016
|
+
findings.push(aggregate);
|
|
23017
|
+
}
|
|
23018
|
+
return findings;
|
|
21244
23019
|
}
|
|
21245
|
-
|
|
21246
|
-
|
|
21247
|
-
|
|
21248
|
-
|
|
21249
|
-
|
|
21250
|
-
|
|
21251
|
-
|
|
21252
|
-
|
|
21253
|
-
|
|
21254
|
-
|
|
21255
|
-
|
|
21256
|
-
|
|
21257
|
-
|
|
21258
|
-
|
|
21259
|
-
|
|
21260
|
-
|
|
21261
|
-
|
|
21262
|
-
|
|
21263
|
-
|
|
23020
|
+
buildLayerCFinding(task, key, severity, now) {
|
|
23021
|
+
return {
|
|
23022
|
+
finding_id: "",
|
|
23023
|
+
sentinel_id: this.sentinelId,
|
|
23024
|
+
severity,
|
|
23025
|
+
summary: truncateSummary2(
|
|
23026
|
+
`novel-permission-combination: tools=[${task.tools.join(",")}] observed in single task burst (${task.tools.length} distinct tools).`
|
|
23027
|
+
),
|
|
23028
|
+
details: {
|
|
23029
|
+
layer: "C",
|
|
23030
|
+
combination_key: key,
|
|
23031
|
+
tools: task.tools,
|
|
23032
|
+
task_started_at: new Date(task.startTs).toISOString()
|
|
23033
|
+
},
|
|
23034
|
+
observed_at: now.toISOString(),
|
|
23035
|
+
evidence_audit_ids: [],
|
|
23036
|
+
fortress_id: ""
|
|
23037
|
+
};
|
|
23038
|
+
}
|
|
23039
|
+
// ── LLM-assist ────────────────────────────────────────────────────
|
|
23040
|
+
classifyHandle(ctx) {
|
|
23041
|
+
const selector = ctx.substrateSelector;
|
|
23042
|
+
if (!selector) return null;
|
|
23043
|
+
const fn = selector.invokeClassify;
|
|
23044
|
+
if (typeof fn !== "function") return null;
|
|
23045
|
+
return async (items) => {
|
|
21264
23046
|
try {
|
|
21265
|
-
const
|
|
21266
|
-
|
|
21267
|
-
|
|
21268
|
-
|
|
21269
|
-
|
|
21270
|
-
);
|
|
21271
|
-
if (
|
|
21272
|
-
|
|
21273
|
-
pruned += 1;
|
|
23047
|
+
const resp = await fn.call(selector, "sentinel-scoring", {
|
|
23048
|
+
kind: "classify",
|
|
23049
|
+
items,
|
|
23050
|
+
categories: ["benign", "suspicious"]
|
|
23051
|
+
});
|
|
23052
|
+
if (resp.failureClass) return { kind: "failure", message: "substrate failure" };
|
|
23053
|
+
if (resp.body.kind === "classify") {
|
|
23054
|
+
return { kind: "classify", results: resp.body.results };
|
|
21274
23055
|
}
|
|
23056
|
+
return { kind: "failure", message: resp.body.message };
|
|
21275
23057
|
} catch {
|
|
23058
|
+
return null;
|
|
23059
|
+
}
|
|
23060
|
+
};
|
|
23061
|
+
}
|
|
23062
|
+
async consultClassifier(classify, obs) {
|
|
23063
|
+
const item = JSON.stringify({
|
|
23064
|
+
tool: obs.tool,
|
|
23065
|
+
proxy: obs.proxy,
|
|
23066
|
+
args_summary: obs.argsSummary
|
|
23067
|
+
});
|
|
23068
|
+
const result = await classify([item]);
|
|
23069
|
+
if (!result || result.kind !== "classify") return "unknown";
|
|
23070
|
+
const top = result.results[0];
|
|
23071
|
+
if (!top) return "unknown";
|
|
23072
|
+
if (top.category === "suspicious" && top.confidence >= 0.5) {
|
|
23073
|
+
return "suspicious";
|
|
23074
|
+
}
|
|
23075
|
+
if (top.category === "benign") return "benign";
|
|
23076
|
+
return "unknown";
|
|
23077
|
+
}
|
|
23078
|
+
// ── helpers ───────────────────────────────────────────────────────
|
|
23079
|
+
observationFromEntry(entry) {
|
|
23080
|
+
const op = entry.operation;
|
|
23081
|
+
let tool = null;
|
|
23082
|
+
let proxy = false;
|
|
23083
|
+
for (const prefix of GATE_PREFIXES) {
|
|
23084
|
+
if (op.startsWith(prefix)) {
|
|
23085
|
+
tool = op.slice(prefix.length);
|
|
23086
|
+
proxy = prefix === "gate_allow_proxy:";
|
|
23087
|
+
break;
|
|
21276
23088
|
}
|
|
21277
23089
|
}
|
|
21278
|
-
|
|
23090
|
+
if (!tool) return null;
|
|
23091
|
+
const ts = Date.parse(entry.timestamp);
|
|
23092
|
+
if (!Number.isFinite(ts)) return null;
|
|
23093
|
+
const argsSummary = extractArgsSummary(entry.details);
|
|
23094
|
+
return { tool, proxy, ts, entry, argsSummary };
|
|
21279
23095
|
}
|
|
21280
23096
|
};
|
|
21281
23097
|
}
|
|
21282
23098
|
});
|
|
21283
23099
|
|
|
23100
|
+
// src/sentinel/sentinels/index.ts
|
|
23101
|
+
var PHI1_BASELINE_CATALOG;
|
|
23102
|
+
var init_sentinels = __esm({
|
|
23103
|
+
"src/sentinel/sentinels/index.ts"() {
|
|
23104
|
+
init_egress_volume_watcher();
|
|
23105
|
+
init_cross_agent_chatter_watcher();
|
|
23106
|
+
init_credential_usage_watcher();
|
|
23107
|
+
init_suspicious_tool_call_detector();
|
|
23108
|
+
PHI1_BASELINE_CATALOG = [
|
|
23109
|
+
{
|
|
23110
|
+
sentinelId: EGRESS_VOLUME_SENTINEL_ID,
|
|
23111
|
+
description: "Watches outbound proxy-call volume per upstream server and surfaces anomalous spikes against a rolling 7-day baseline.",
|
|
23112
|
+
factory: () => new EgressVolumeWatcher()
|
|
23113
|
+
},
|
|
23114
|
+
{
|
|
23115
|
+
sentinelId: CROSS_AGENT_CHATTER_SENTINEL_ID,
|
|
23116
|
+
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.",
|
|
23117
|
+
factory: () => new CrossAgentChatterWatcher()
|
|
23118
|
+
},
|
|
23119
|
+
{
|
|
23120
|
+
sentinelId: CREDENTIAL_USAGE_SENTINEL_ID,
|
|
23121
|
+
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.",
|
|
23122
|
+
factory: () => new CredentialUsageWatcher()
|
|
23123
|
+
},
|
|
23124
|
+
{
|
|
23125
|
+
sentinelId: SUSPICIOUS_TOOL_CALL_SENTINEL_ID,
|
|
23126
|
+
description: "Surfaces tool calls whose argument shape, call frequency, or permission combination looks unusual for the fortress's recent history.",
|
|
23127
|
+
factory: () => new SuspiciousToolCallDetector()
|
|
23128
|
+
}
|
|
23129
|
+
];
|
|
23130
|
+
}
|
|
23131
|
+
});
|
|
23132
|
+
function sentinelSubscriptionsPath(storagePath) {
|
|
23133
|
+
return join(storagePath, "sentinel-subscriptions.json");
|
|
23134
|
+
}
|
|
23135
|
+
async function loadSentinelSubscriptions(storagePath) {
|
|
23136
|
+
const filePath = sentinelSubscriptionsPath(storagePath);
|
|
23137
|
+
try {
|
|
23138
|
+
const raw = await readFile(filePath, "utf8");
|
|
23139
|
+
const parsed = JSON.parse(raw);
|
|
23140
|
+
if (parsed.version !== FILE_VERSION) return /* @__PURE__ */ new Set();
|
|
23141
|
+
if (!Array.isArray(parsed.subscribed)) return /* @__PURE__ */ new Set();
|
|
23142
|
+
const cleaned = parsed.subscribed.filter(
|
|
23143
|
+
(id) => typeof id === "string" && id.length > 0
|
|
23144
|
+
);
|
|
23145
|
+
return new Set(cleaned);
|
|
23146
|
+
} catch {
|
|
23147
|
+
return /* @__PURE__ */ new Set();
|
|
23148
|
+
}
|
|
23149
|
+
}
|
|
23150
|
+
async function saveSentinelSubscriptions(storagePath, subscribed) {
|
|
23151
|
+
const filePath = sentinelSubscriptionsPath(storagePath);
|
|
23152
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
23153
|
+
const payload = {
|
|
23154
|
+
version: FILE_VERSION,
|
|
23155
|
+
subscribed: [...new Set(subscribed)].filter((s) => s.length > 0).sort()
|
|
23156
|
+
};
|
|
23157
|
+
await writeFile(filePath, `${JSON.stringify(payload, null, 2)}
|
|
23158
|
+
`, {
|
|
23159
|
+
mode: 384
|
|
23160
|
+
});
|
|
23161
|
+
}
|
|
23162
|
+
var FILE_VERSION;
|
|
23163
|
+
var init_subscription_store = __esm({
|
|
23164
|
+
"src/sentinel/subscription-store.ts"() {
|
|
23165
|
+
FILE_VERSION = 1;
|
|
23166
|
+
}
|
|
23167
|
+
});
|
|
23168
|
+
|
|
21284
23169
|
// src/principal-policy/tools.ts
|
|
21285
23170
|
function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
21286
23171
|
return [
|
|
@@ -32758,7 +34643,7 @@ var init_recovery_key_disclosure = __esm({
|
|
|
32758
34643
|
});
|
|
32759
34644
|
|
|
32760
34645
|
// src/hub/types.ts
|
|
32761
|
-
var
|
|
34646
|
+
var init_types4 = __esm({
|
|
32762
34647
|
"src/hub/types.ts"() {
|
|
32763
34648
|
}
|
|
32764
34649
|
});
|
|
@@ -33797,7 +35682,7 @@ var init_hub = __esm({
|
|
|
33797
35682
|
"src/hub/index.ts"() {
|
|
33798
35683
|
init_constants3();
|
|
33799
35684
|
init_errors4();
|
|
33800
|
-
|
|
35685
|
+
init_types4();
|
|
33801
35686
|
init_agent_registry();
|
|
33802
35687
|
init_inbox_store();
|
|
33803
35688
|
init_inbox_aggregator();
|
|
@@ -33906,7 +35791,17 @@ var init_operator_chat_audit_events = __esm({
|
|
|
33906
35791
|
* fold. The concierge omits that category and continues; the user-
|
|
33907
35792
|
* facing query is never broken. Body carries category + failure_reason.
|
|
33908
35793
|
*/
|
|
33909
|
-
CONCIERGE_CONTEXT_FETCHER_FAILED: "operator_concierge_context_fetcher_failed"
|
|
35794
|
+
CONCIERGE_CONTEXT_FETCHER_FAILED: "operator_concierge_context_fetcher_failed",
|
|
35795
|
+
/**
|
|
35796
|
+
* Concierge surfaced a proactive starter when a fresh conversation
|
|
35797
|
+
* thread opened (WP-V1.3-9 Tau-5). Emitted once per starter, never on
|
|
35798
|
+
* follow-up turns within the same thread. Body carries `thread_id`,
|
|
35799
|
+
* `trigger` (stable enum), and `triggered_agents_count`. The starter
|
|
35800
|
+
* text body is NOT carried; the trigger enum is sufficient for
|
|
35801
|
+
* dashboard grouping and keeps fortress-internal agent ids off the
|
|
35802
|
+
* audit surface.
|
|
35803
|
+
*/
|
|
35804
|
+
CONCIERGE_PROACTIVE_SUGGESTION_OFFERED: "operator_concierge_proactive_suggestion_offered"
|
|
33910
35805
|
};
|
|
33911
35806
|
}
|
|
33912
35807
|
});
|
|
@@ -34740,9 +36635,108 @@ var init_concierge_query_grammar = __esm({
|
|
|
34740
36635
|
LLM_ASSIST_THRESHOLD = 0.5;
|
|
34741
36636
|
}
|
|
34742
36637
|
});
|
|
36638
|
+
|
|
36639
|
+
// src/chat/agent-context-cache.ts
|
|
34743
36640
|
function approxTokenLen2(text) {
|
|
34744
36641
|
return Math.ceil(text.length / 4);
|
|
34745
36642
|
}
|
|
36643
|
+
function formatSnapshotLine(snapshot) {
|
|
36644
|
+
const flagLabel = snapshot.state_flags.join("+") || "no_flags";
|
|
36645
|
+
const work = snapshot.current_work_summary ? `, last: ${snapshot.current_work_summary}` : "";
|
|
36646
|
+
const verascore = snapshot.recent_verascore_delta_24h !== null ? `, verascore \u0394${snapshot.recent_verascore_delta_24h.toFixed(2)}` : "";
|
|
36647
|
+
return `- ${snapshot.agent_name} (template: ${snapshot.template}): ${flagLabel}, ${snapshot.recent_audit_count_24h} audit/24h, ${snapshot.recent_concordia_receipts_count_24h} receipts${verascore}${work}`;
|
|
36648
|
+
}
|
|
36649
|
+
function urgencyRank(snapshot) {
|
|
36650
|
+
for (let i = 0; i < STATE_FLAG_ORDER.length; i++) {
|
|
36651
|
+
if (snapshot.state_flags.includes(STATE_FLAG_ORDER[i])) {
|
|
36652
|
+
return i;
|
|
36653
|
+
}
|
|
36654
|
+
}
|
|
36655
|
+
return STATE_FLAG_ORDER.length;
|
|
36656
|
+
}
|
|
36657
|
+
function formatCurrentAgentStateSection(snapshots, opts) {
|
|
36658
|
+
if (snapshots.length === 0) return "";
|
|
36659
|
+
const budget = opts?.maxTokens ?? DEFAULT_AGENT_CONTEXT_TOKEN_BUDGET;
|
|
36660
|
+
const sorted = [...snapshots].sort(
|
|
36661
|
+
(a, b) => urgencyRank(a) - urgencyRank(b)
|
|
36662
|
+
);
|
|
36663
|
+
const headerTokens = approxTokenLen2(`${SECTION_HEADER}
|
|
36664
|
+
`);
|
|
36665
|
+
const sepTokens = approxTokenLen2("\n");
|
|
36666
|
+
let runningTokens = headerTokens;
|
|
36667
|
+
const kept = [];
|
|
36668
|
+
for (const snap of sorted) {
|
|
36669
|
+
const line = formatSnapshotLine(snap);
|
|
36670
|
+
const tokens = approxTokenLen2(line) + (kept.length > 0 ? sepTokens : 0);
|
|
36671
|
+
if (kept.length === 0) {
|
|
36672
|
+
kept.push(line);
|
|
36673
|
+
runningTokens += tokens;
|
|
36674
|
+
continue;
|
|
36675
|
+
}
|
|
36676
|
+
if (runningTokens + tokens > budget) break;
|
|
36677
|
+
kept.push(line);
|
|
36678
|
+
runningTokens += tokens;
|
|
36679
|
+
}
|
|
36680
|
+
return `${SECTION_HEADER}
|
|
36681
|
+
${kept.join("\n")}`;
|
|
36682
|
+
}
|
|
36683
|
+
function generateProactiveStarter(snapshots) {
|
|
36684
|
+
if (snapshots.length === 0) return null;
|
|
36685
|
+
const stuck = snapshots.filter((s) => s.state_flags.includes("stuck"));
|
|
36686
|
+
if (stuck.length > 0) {
|
|
36687
|
+
const first = stuck[0];
|
|
36688
|
+
if (first === void 0) return null;
|
|
36689
|
+
const last = first.current_work_summary ? ` (last: ${first.current_work_summary})` : "";
|
|
36690
|
+
return {
|
|
36691
|
+
text: `Your ${first.agent_name} agent looks stuck${last}. Should I check its session state?`,
|
|
36692
|
+
trigger: "stuck_agent",
|
|
36693
|
+
triggered_agents_count: stuck.length
|
|
36694
|
+
};
|
|
36695
|
+
}
|
|
36696
|
+
const pending = snapshots.filter(
|
|
36697
|
+
(s) => s.state_flags.includes("has_pending_approvals")
|
|
36698
|
+
);
|
|
36699
|
+
if (pending.length > 0) {
|
|
36700
|
+
const names = pending.slice(0, 3).map((s) => s.agent_name).join(", ");
|
|
36701
|
+
return {
|
|
36702
|
+
text: `You have pending approvals across ${names}. Want to walk through them?`,
|
|
36703
|
+
trigger: "pending_approvals",
|
|
36704
|
+
triggered_agents_count: pending.length
|
|
36705
|
+
};
|
|
36706
|
+
}
|
|
36707
|
+
const findings = snapshots.filter(
|
|
36708
|
+
(s) => s.state_flags.includes("has_open_findings")
|
|
36709
|
+
);
|
|
36710
|
+
if (findings.length > 0) {
|
|
36711
|
+
return {
|
|
36712
|
+
text: `Sentinel has open findings on ${findings.length} ${findings.length === 1 ? "agent" : "agents"}. Want a summary?`,
|
|
36713
|
+
trigger: "open_findings",
|
|
36714
|
+
triggered_agents_count: findings.length
|
|
36715
|
+
};
|
|
36716
|
+
}
|
|
36717
|
+
return {
|
|
36718
|
+
text: "Your fortress is quiet. Anything you'd like to inspect?",
|
|
36719
|
+
trigger: "all_idle",
|
|
36720
|
+
triggered_agents_count: snapshots.length
|
|
36721
|
+
};
|
|
36722
|
+
}
|
|
36723
|
+
var STATE_FLAG_ORDER, SECTION_HEADER, DEFAULT_AGENT_CONTEXT_TOKEN_BUDGET;
|
|
36724
|
+
var init_agent_context_cache = __esm({
|
|
36725
|
+
"src/chat/agent-context-cache.ts"() {
|
|
36726
|
+
STATE_FLAG_ORDER = [
|
|
36727
|
+
"stuck",
|
|
36728
|
+
"has_pending_approvals",
|
|
36729
|
+
"has_open_findings",
|
|
36730
|
+
"active",
|
|
36731
|
+
"idle"
|
|
36732
|
+
];
|
|
36733
|
+
SECTION_HEADER = "## Current agent state";
|
|
36734
|
+
DEFAULT_AGENT_CONTEXT_TOKEN_BUDGET = 400;
|
|
36735
|
+
}
|
|
36736
|
+
});
|
|
36737
|
+
function approxTokenLen3(text) {
|
|
36738
|
+
return Math.ceil(text.length / 4);
|
|
36739
|
+
}
|
|
34746
36740
|
function makeEventId(prefix) {
|
|
34747
36741
|
return `${prefix}-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
34748
36742
|
}
|
|
@@ -34764,7 +36758,7 @@ function formatPriorTurnLine(turn) {
|
|
|
34764
36758
|
function hashOf(input) {
|
|
34765
36759
|
return hashToString(sha256(stringToBytes(input)));
|
|
34766
36760
|
}
|
|
34767
|
-
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;
|
|
36761
|
+
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;
|
|
34768
36762
|
var init_operator_chat_service = __esm({
|
|
34769
36763
|
"src/chat/operator-chat-service.ts"() {
|
|
34770
36764
|
init_hashing();
|
|
@@ -34773,12 +36767,14 @@ var init_operator_chat_service = __esm({
|
|
|
34773
36767
|
init_operator_chat_types();
|
|
34774
36768
|
init_concierge_context_router();
|
|
34775
36769
|
init_concierge_query_grammar();
|
|
36770
|
+
init_agent_context_cache();
|
|
34776
36771
|
DEFAULT_CONCIERGE_MAX_TOKENS = 512;
|
|
34777
36772
|
DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS = 10;
|
|
34778
36773
|
DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
|
|
34779
36774
|
DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET = 500;
|
|
34780
36775
|
DEFAULT_CONCIERGE_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
34781
36776
|
DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET = 2e3;
|
|
36777
|
+
DEFAULT_CONCIERGE_AGENT_STATE_BUDGET = 400;
|
|
34782
36778
|
SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
|
|
34783
36779
|
1. Castle Wall: OS-boundary egress filter enforced at the kernel level. Blocks unauthorized outbound calls even from prompt-injected agents.
|
|
34784
36780
|
2. Sentinels: internal observation via process introspection. Surfaces anomalies to the operator; does not enforce.
|
|
@@ -34825,6 +36821,15 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
34825
36821
|
dynamicContextBudget;
|
|
34826
36822
|
agentRegistry;
|
|
34827
36823
|
grammarLlmAssist;
|
|
36824
|
+
agentContextCache;
|
|
36825
|
+
agentStateBudget;
|
|
36826
|
+
/**
|
|
36827
|
+
* Per-thread guard so the proactive starter fires at most once per
|
|
36828
|
+
* fresh thread. Tracks the thread_id the starter was last offered
|
|
36829
|
+
* for; subsequent `getProactiveStarter()` calls within the same
|
|
36830
|
+
* thread return null instead of re-emitting.
|
|
36831
|
+
*/
|
|
36832
|
+
starterOfferedForThreadId;
|
|
34828
36833
|
/**
|
|
34829
36834
|
* In-memory thread_id assigned to the active concierge session.
|
|
34830
36835
|
* The first sendConcierge call after construction allocates a fresh
|
|
@@ -34869,6 +36874,10 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
34869
36874
|
if (deps.conciergeGrammarLlmAssist) {
|
|
34870
36875
|
this.grammarLlmAssist = deps.conciergeGrammarLlmAssist;
|
|
34871
36876
|
}
|
|
36877
|
+
if (deps.conciergeAgentContextCache) {
|
|
36878
|
+
this.agentContextCache = deps.conciergeAgentContextCache;
|
|
36879
|
+
}
|
|
36880
|
+
this.agentStateBudget = deps.conciergeAgentStateBudget !== void 0 && deps.conciergeAgentStateBudget > 0 ? deps.conciergeAgentStateBudget : DEFAULT_CONCIERGE_AGENT_STATE_BUDGET;
|
|
34872
36881
|
}
|
|
34873
36882
|
// ── Concierge ─────────────────────────────────────────────────────────
|
|
34874
36883
|
/**
|
|
@@ -34890,6 +36899,7 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
34890
36899
|
const nowMs = this.clock();
|
|
34891
36900
|
if (this.activeMemoryThreadId && this.lastInteractionAt !== void 0 && nowMs - this.lastInteractionAt > this.sessionTtlMs) {
|
|
34892
36901
|
this.activeMemoryThreadId = void 0;
|
|
36902
|
+
this.starterOfferedForThreadId = void 0;
|
|
34893
36903
|
}
|
|
34894
36904
|
const operatorMessage = {
|
|
34895
36905
|
message_id: randomUUID(),
|
|
@@ -34928,6 +36938,11 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
34928
36938
|
});
|
|
34929
36939
|
}
|
|
34930
36940
|
const parsedGrammar = await this.runGrammarParse(filterResult.filtered);
|
|
36941
|
+
const agentSnapshots = this.agentContextCache ? this.agentContextCache.read() : [];
|
|
36942
|
+
const agentStateSection = this.agentContextCache ? formatCurrentAgentStateSection(agentSnapshots, {
|
|
36943
|
+
maxTokens: this.agentStateBudget
|
|
36944
|
+
}) : "";
|
|
36945
|
+
const renderedAgentCount = agentStateSection ? agentSnapshots.length : 0;
|
|
34931
36946
|
const start = Date.now();
|
|
34932
36947
|
let conciergeBody;
|
|
34933
36948
|
let servedBy = "disabled";
|
|
@@ -34952,7 +36967,8 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
34952
36967
|
dynamicCategoriesIncluded = dynamicResult.categoriesIncluded;
|
|
34953
36968
|
const context = await this.assembleConciergeContext(
|
|
34954
36969
|
priorTurns,
|
|
34955
|
-
dynamicResult.section
|
|
36970
|
+
dynamicResult.section,
|
|
36971
|
+
agentStateSection
|
|
34956
36972
|
);
|
|
34957
36973
|
const response = await this.substrateSelector.invokeSummarize(
|
|
34958
36974
|
"concierge",
|
|
@@ -35019,7 +37035,8 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
35019
37035
|
prior_turns_folded: memoryReadFailureReason === null ? priorTurns.length : 0
|
|
35020
37036
|
} : {},
|
|
35021
37037
|
...this.contextFetchers ? { dynamic_context_categories: [...dynamicCategoriesIncluded] } : {},
|
|
35022
|
-
parsed_grammar: auditSafeSummary(parsedGrammar)
|
|
37038
|
+
parsed_grammar: auditSafeSummary(parsedGrammar),
|
|
37039
|
+
...this.agentContextCache !== void 0 ? { agent_context_snapshot_count: renderedAgentCount } : {}
|
|
35023
37040
|
};
|
|
35024
37041
|
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_CHAT, payload, outcome === "ok" ? "success" : "failure");
|
|
35025
37042
|
return {
|
|
@@ -35130,6 +37147,7 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
35130
37147
|
if (!removed) return false;
|
|
35131
37148
|
if (this.activeMemoryThreadId === threadId) {
|
|
35132
37149
|
this.activeMemoryThreadId = void 0;
|
|
37150
|
+
this.starterOfferedForThreadId = void 0;
|
|
35133
37151
|
}
|
|
35134
37152
|
const payload = {
|
|
35135
37153
|
version: "1.2",
|
|
@@ -35148,9 +37166,65 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
35148
37166
|
* Reset the active session memory thread. Subsequent sendConcierge
|
|
35149
37167
|
* calls allocate a fresh thread_id. Surfaced for tests + future "new
|
|
35150
37168
|
* conversation" affordance; not currently called by the dashboard.
|
|
37169
|
+
*
|
|
37170
|
+
* Tau-5: also clears the proactive-starter guard so the next
|
|
37171
|
+
* `getProactiveStarter()` call against the freshly-allocated thread
|
|
37172
|
+
* is eligible to fire.
|
|
35151
37173
|
*/
|
|
35152
37174
|
resetConciergeMemoryThread() {
|
|
35153
37175
|
this.activeMemoryThreadId = void 0;
|
|
37176
|
+
this.starterOfferedForThreadId = void 0;
|
|
37177
|
+
}
|
|
37178
|
+
/**
|
|
37179
|
+
* WP-V1.3-9 Tau-5: surface a proactive starter for the current
|
|
37180
|
+
* concierge session. Intended to be called by the dashboard UI when
|
|
37181
|
+
* the operator opens the chat surface, before any operator typing.
|
|
37182
|
+
*
|
|
37183
|
+
* Returns null when:
|
|
37184
|
+
* - No agent-context cache is wired (Tau-5 disabled).
|
|
37185
|
+
* - No concierge memory store is wired (no thread_id namespace).
|
|
37186
|
+
* - The cache snapshot has no signal (empty fortress).
|
|
37187
|
+
* - A starter has already been offered for the active thread (the
|
|
37188
|
+
* guard ensures one starter per fresh thread).
|
|
37189
|
+
*
|
|
37190
|
+
* Side effects:
|
|
37191
|
+
* - Allocates a fresh thread_id if none is active.
|
|
37192
|
+
* - Emits the `operator_concierge_proactive_suggestion_offered`
|
|
37193
|
+
* audit event with the trigger class + triggered_agents_count.
|
|
37194
|
+
* - Records the offered thread_id so the next call within the same
|
|
37195
|
+
* thread is a no-op.
|
|
37196
|
+
*
|
|
37197
|
+
* The returned starter's `text` is operator-visible copy; the
|
|
37198
|
+
* dashboard renders it as a system-message-style starter the
|
|
37199
|
+
* operator can accept (clicks/types follow-up) or dismiss (types a
|
|
37200
|
+
* new query).
|
|
37201
|
+
*/
|
|
37202
|
+
getProactiveStarter() {
|
|
37203
|
+
if (!this.agentContextCache) return null;
|
|
37204
|
+
if (!this.memory) return null;
|
|
37205
|
+
const threadId = this.ensureActiveMemoryThread();
|
|
37206
|
+
if (this.starterOfferedForThreadId === threadId) return null;
|
|
37207
|
+
const snapshots = this.agentContextCache.read();
|
|
37208
|
+
const starter = generateProactiveStarter(snapshots);
|
|
37209
|
+
if (!starter) return null;
|
|
37210
|
+
const payload = {
|
|
37211
|
+
version: "1.2",
|
|
37212
|
+
event_id: makeEventId("conc-starter"),
|
|
37213
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
37214
|
+
identity_id: this.identityId,
|
|
37215
|
+
kind: "operator_concierge_proactive_suggestion_offered",
|
|
37216
|
+
surface: "concierge",
|
|
37217
|
+
thread_id: threadId,
|
|
37218
|
+
trigger: starter.trigger,
|
|
37219
|
+
triggered_agents_count: starter.triggered_agents_count
|
|
37220
|
+
};
|
|
37221
|
+
this.emit(
|
|
37222
|
+
OPERATOR_CHAT_OPS.CONCIERGE_PROACTIVE_SUGGESTION_OFFERED,
|
|
37223
|
+
payload,
|
|
37224
|
+
"success"
|
|
37225
|
+
);
|
|
37226
|
+
this.starterOfferedForThreadId = threadId;
|
|
37227
|
+
return starter;
|
|
35154
37228
|
}
|
|
35155
37229
|
ensureActiveMemoryThread() {
|
|
35156
37230
|
if (!this.activeMemoryThreadId) {
|
|
@@ -35195,7 +37269,7 @@ Note: this is a static reference block (v1.2.x). Dynamic context injection (live
|
|
|
35195
37269
|
* if available; the v1.2 selector does not expose one, so structured
|
|
35196
37270
|
* serialization is the canonical path for v1.3.
|
|
35197
37271
|
*/
|
|
35198
|
-
async assembleConciergeContext(priorTurns = [], dynamicSection = "") {
|
|
37272
|
+
async assembleConciergeContext(priorTurns = [], dynamicSection = "", agentStateSection = "") {
|
|
35199
37273
|
const ref = `## Sanctuary reference
|
|
35200
37274
|
${SANCTUARY_DOMAIN_REFERENCE}`;
|
|
35201
37275
|
const priorSection = this.formatPriorTurnsSection(priorTurns);
|
|
@@ -35203,6 +37277,7 @@ ${SANCTUARY_DOMAIN_REFERENCE}`;
|
|
|
35203
37277
|
return [
|
|
35204
37278
|
ref,
|
|
35205
37279
|
...dynamicSection ? [dynamicSection] : [],
|
|
37280
|
+
...agentStateSection ? [agentStateSection] : [],
|
|
35206
37281
|
...priorSection ? [priorSection] : [],
|
|
35207
37282
|
"## Recent activity\n(no providers wired)",
|
|
35208
37283
|
"## Wrapped agents\n(no providers wired)",
|
|
@@ -35217,6 +37292,7 @@ ${SANCTUARY_DOMAIN_REFERENCE}`;
|
|
|
35217
37292
|
return [
|
|
35218
37293
|
ref,
|
|
35219
37294
|
...dynamicSection ? [dynamicSection] : [],
|
|
37295
|
+
...agentStateSection ? [agentStateSection] : [],
|
|
35220
37296
|
...priorSection ? [priorSection] : [],
|
|
35221
37297
|
`## Recent activity
|
|
35222
37298
|
${activity}`,
|
|
@@ -35299,14 +37375,14 @@ ${inbox}`
|
|
|
35299
37375
|
if (turns.length === 0) return "";
|
|
35300
37376
|
const HEADER = "## Prior conversation";
|
|
35301
37377
|
const lines = turns.map(formatPriorTurnLine);
|
|
35302
|
-
const headerTokens =
|
|
37378
|
+
const headerTokens = approxTokenLen3(`${HEADER}
|
|
35303
37379
|
`);
|
|
35304
|
-
const sepTokens =
|
|
37380
|
+
const sepTokens = approxTokenLen3("\n");
|
|
35305
37381
|
let runningTokens = headerTokens;
|
|
35306
37382
|
let runningLines = [];
|
|
35307
37383
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
35308
37384
|
const line = lines[i];
|
|
35309
|
-
const tokens =
|
|
37385
|
+
const tokens = approxTokenLen3(line) + (runningLines.length > 0 ? sepTokens : 0);
|
|
35310
37386
|
if (runningTokens + tokens > this.historyTokenBudget) break;
|
|
35311
37387
|
runningTokens += tokens;
|
|
35312
37388
|
runningLines.push(line);
|
|
@@ -35334,7 +37410,7 @@ ${runningLines.join("\n")}`;
|
|
|
35334
37410
|
function chatStorageKey(surface, threadKey) {
|
|
35335
37411
|
return `${surface}.${threadKey}`;
|
|
35336
37412
|
}
|
|
35337
|
-
var OPERATOR_CHAT_NAMESPACE,
|
|
37413
|
+
var OPERATOR_CHAT_NAMESPACE, HKDF_INFO3, OperatorChatStore;
|
|
35338
37414
|
var init_operator_chat_store = __esm({
|
|
35339
37415
|
"src/chat/operator-chat-store.ts"() {
|
|
35340
37416
|
init_encryption();
|
|
@@ -35342,13 +37418,13 @@ var init_operator_chat_store = __esm({
|
|
|
35342
37418
|
init_encoding();
|
|
35343
37419
|
init_operator_chat_types();
|
|
35344
37420
|
OPERATOR_CHAT_NAMESPACE = "_chat";
|
|
35345
|
-
|
|
37421
|
+
HKDF_INFO3 = "operator-chat-store-v1";
|
|
35346
37422
|
OperatorChatStore = class {
|
|
35347
37423
|
storage;
|
|
35348
37424
|
encryptionKey;
|
|
35349
37425
|
constructor(storage, masterKey) {
|
|
35350
37426
|
this.storage = storage;
|
|
35351
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
37427
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO3);
|
|
35352
37428
|
}
|
|
35353
37429
|
/**
|
|
35354
37430
|
* Load a thread. Returns null if no record exists or if the on-disk
|
|
@@ -35433,7 +37509,7 @@ var init_operator_chat_store = __esm({
|
|
|
35433
37509
|
function bundleKey(threadId) {
|
|
35434
37510
|
return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
|
|
35435
37511
|
}
|
|
35436
|
-
function
|
|
37512
|
+
function stripKeyPrefix3(key) {
|
|
35437
37513
|
if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
|
|
35438
37514
|
return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
|
|
35439
37515
|
}
|
|
@@ -35444,7 +37520,7 @@ function lastTurnId(bundle) {
|
|
|
35444
37520
|
}
|
|
35445
37521
|
return max;
|
|
35446
37522
|
}
|
|
35447
|
-
var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX,
|
|
37523
|
+
var CONCIERGE_MEMORY_NAMESPACE, CONCIERGE_MEMORY_KEY_PREFIX, HKDF_INFO4, DEFAULT_CONCIERGE_RETENTION_DAYS, MAX_BUNDLE_BYTES3, ConciergeMemoryStore;
|
|
35448
37524
|
var init_concierge_memory_store = __esm({
|
|
35449
37525
|
"src/chat/concierge-memory-store.ts"() {
|
|
35450
37526
|
init_encryption();
|
|
@@ -35452,7 +37528,7 @@ var init_concierge_memory_store = __esm({
|
|
|
35452
37528
|
init_encoding();
|
|
35453
37529
|
CONCIERGE_MEMORY_NAMESPACE = "_chat";
|
|
35454
37530
|
CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
|
|
35455
|
-
|
|
37531
|
+
HKDF_INFO4 = "concierge-memory-store-v1";
|
|
35456
37532
|
DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
|
|
35457
37533
|
MAX_BUNDLE_BYTES3 = 4 * 1024 * 1024;
|
|
35458
37534
|
ConciergeMemoryStore = class {
|
|
@@ -35463,7 +37539,7 @@ var init_concierge_memory_store = __esm({
|
|
|
35463
37539
|
locks;
|
|
35464
37540
|
constructor(opts) {
|
|
35465
37541
|
this.storage = opts.storage;
|
|
35466
|
-
this.encryptionKey = derivePurposeKey(opts.masterKey,
|
|
37542
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO4);
|
|
35467
37543
|
this.fortressId = opts.fortressId;
|
|
35468
37544
|
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
|
|
35469
37545
|
this.locks = /* @__PURE__ */ new Map();
|
|
@@ -35589,7 +37665,7 @@ var init_concierge_memory_store = __esm({
|
|
|
35589
37665
|
);
|
|
35590
37666
|
const summaries = [];
|
|
35591
37667
|
for (const meta of entries) {
|
|
35592
|
-
const threadId =
|
|
37668
|
+
const threadId = stripKeyPrefix3(meta.key);
|
|
35593
37669
|
if (threadId === null) continue;
|
|
35594
37670
|
const bundle = await this.loadBundle(threadId);
|
|
35595
37671
|
if (!bundle || bundle.turns.length === 0) continue;
|
|
@@ -35642,7 +37718,7 @@ var init_concierge_memory_store = __esm({
|
|
|
35642
37718
|
);
|
|
35643
37719
|
let pruned = 0;
|
|
35644
37720
|
for (const meta of entries) {
|
|
35645
|
-
const threadId =
|
|
37721
|
+
const threadId = stripKeyPrefix3(meta.key);
|
|
35646
37722
|
if (threadId === null) continue;
|
|
35647
37723
|
pruned += await this.withLock(threadId, async () => {
|
|
35648
37724
|
const bundle = await this.loadBundle(threadId);
|
|
@@ -36112,7 +38188,7 @@ var init_defaults = __esm({
|
|
|
36112
38188
|
});
|
|
36113
38189
|
|
|
36114
38190
|
// src/intelligence/policy-store.ts
|
|
36115
|
-
var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY,
|
|
38191
|
+
var INTELLIGENCE_NAMESPACE, SUBSTRATE_CONFIG_KEY, HKDF_INFO5, IntelligenceConfigStore;
|
|
36116
38192
|
var init_policy_store = __esm({
|
|
36117
38193
|
"src/intelligence/policy-store.ts"() {
|
|
36118
38194
|
init_encryption();
|
|
@@ -36121,13 +38197,13 @@ var init_policy_store = __esm({
|
|
|
36121
38197
|
init_defaults();
|
|
36122
38198
|
INTELLIGENCE_NAMESPACE = "_intelligence";
|
|
36123
38199
|
SUBSTRATE_CONFIG_KEY = "substrate-config";
|
|
36124
|
-
|
|
38200
|
+
HKDF_INFO5 = "intelligence-substrate-config";
|
|
36125
38201
|
IntelligenceConfigStore = class {
|
|
36126
38202
|
storage;
|
|
36127
38203
|
encryptionKey;
|
|
36128
38204
|
constructor(storage, masterKey) {
|
|
36129
38205
|
this.storage = storage;
|
|
36130
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
38206
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO5);
|
|
36131
38207
|
}
|
|
36132
38208
|
/**
|
|
36133
38209
|
* Load the operator's substrate config from disk. Returns the config
|
|
@@ -40078,6 +42154,38 @@ ${err.message}
|
|
|
40078
42154
|
if (dashboard) {
|
|
40079
42155
|
dashboard.setApprovalAggregator(approvalAggregator);
|
|
40080
42156
|
}
|
|
42157
|
+
const sentinelFindingStore = new SentinelFindingStore({
|
|
42158
|
+
storage,
|
|
42159
|
+
masterKey,
|
|
42160
|
+
fortressId: fortressIdForAggregator
|
|
42161
|
+
});
|
|
42162
|
+
const sentinelRegistry = new SentinelRegistry();
|
|
42163
|
+
for (const entry of PHI1_BASELINE_CATALOG) {
|
|
42164
|
+
sentinelRegistry.register(entry);
|
|
42165
|
+
}
|
|
42166
|
+
const sentinelDispatcher = new SentinelDispatcher({
|
|
42167
|
+
registry: sentinelRegistry,
|
|
42168
|
+
findingStore: sentinelFindingStore,
|
|
42169
|
+
auditLog,
|
|
42170
|
+
fortressId: fortressIdForAggregator,
|
|
42171
|
+
identityId: aggregatorIdentityId
|
|
42172
|
+
});
|
|
42173
|
+
try {
|
|
42174
|
+
const persistedSubscriptions = await loadSentinelSubscriptions(
|
|
42175
|
+
config.storage_path
|
|
42176
|
+
);
|
|
42177
|
+
for (const sentinelId of persistedSubscriptions) {
|
|
42178
|
+
try {
|
|
42179
|
+
await sentinelDispatcher.subscribeSentinel(sentinelId);
|
|
42180
|
+
} catch {
|
|
42181
|
+
}
|
|
42182
|
+
}
|
|
42183
|
+
} catch {
|
|
42184
|
+
}
|
|
42185
|
+
sentinelDispatcher.start();
|
|
42186
|
+
if (dashboard) {
|
|
42187
|
+
dashboard.setSentinelDispatcher(sentinelDispatcher);
|
|
42188
|
+
}
|
|
40081
42189
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
40082
42190
|
const { tools: sanctuaryMetaTools } = createSanctuaryTools({
|
|
40083
42191
|
config,
|
|
@@ -40271,6 +42379,11 @@ var init_src = __esm({
|
|
|
40271
42379
|
init_approval_aggregator();
|
|
40272
42380
|
init_aggregator_backed_channel();
|
|
40273
42381
|
init_aggregator_store();
|
|
42382
|
+
init_sentinel_finding_store();
|
|
42383
|
+
init_sentinel_registry();
|
|
42384
|
+
init_sentinel_dispatcher();
|
|
42385
|
+
init_sentinels();
|
|
42386
|
+
init_subscription_store();
|
|
40274
42387
|
init_tools4();
|
|
40275
42388
|
init_router();
|
|
40276
42389
|
init_router();
|
|
@@ -45440,6 +47553,232 @@ var init_intelligence = __esm({
|
|
|
45440
47553
|
}
|
|
45441
47554
|
});
|
|
45442
47555
|
|
|
47556
|
+
// src/cli/sentinel.ts
|
|
47557
|
+
var sentinel_exports = {};
|
|
47558
|
+
__export(sentinel_exports, {
|
|
47559
|
+
runSentinelCommand: () => runSentinelCommand
|
|
47560
|
+
});
|
|
47561
|
+
async function runSentinelCommand(args) {
|
|
47562
|
+
const out = args.out ?? process.stdout;
|
|
47563
|
+
const err = args.err ?? process.stderr;
|
|
47564
|
+
const [sub, ...rest] = args.argv;
|
|
47565
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
47566
|
+
printUsage6(out);
|
|
47567
|
+
return 0;
|
|
47568
|
+
}
|
|
47569
|
+
try {
|
|
47570
|
+
switch (sub) {
|
|
47571
|
+
case "list":
|
|
47572
|
+
return cmdList4(out);
|
|
47573
|
+
case "list-subscribed":
|
|
47574
|
+
return await cmdListSubscribed(rest, { out, err, args });
|
|
47575
|
+
case "subscribe":
|
|
47576
|
+
return await cmdSubscribe(rest, { out, err, args });
|
|
47577
|
+
case "unsubscribe":
|
|
47578
|
+
return await cmdUnsubscribe(rest, { out, err, args });
|
|
47579
|
+
case "findings":
|
|
47580
|
+
return await cmdFindings(rest, { out, err, args });
|
|
47581
|
+
default:
|
|
47582
|
+
err.write(`Unknown subcommand: ${sub}
|
|
47583
|
+
`);
|
|
47584
|
+
printUsage6(err);
|
|
47585
|
+
return 2;
|
|
47586
|
+
}
|
|
47587
|
+
} catch (e) {
|
|
47588
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
47589
|
+
err.write(`sanctuary sentinel: ${msg}
|
|
47590
|
+
`);
|
|
47591
|
+
return 1;
|
|
47592
|
+
}
|
|
47593
|
+
}
|
|
47594
|
+
function printUsage6(s) {
|
|
47595
|
+
s.write(`Usage: sanctuary sentinel <command> [args]
|
|
47596
|
+
|
|
47597
|
+
list Show the Phi-1 catalog of available
|
|
47598
|
+
sentinels (egress-volume only at v1.3
|
|
47599
|
+
Phi-1; more land in Phi-2 ... Phi-5).
|
|
47600
|
+
list-subscribed Show which sentinels this fortress has
|
|
47601
|
+
opted into. Loads from
|
|
47602
|
+
<storage>/sentinel-subscriptions.json.
|
|
47603
|
+
subscribe <sentinel-id> Opt in. Writes the subscription file.
|
|
47604
|
+
The server picks it up on next boot.
|
|
47605
|
+
unsubscribe <sentinel-id> Opt out.
|
|
47606
|
+
findings [opts] Read recent findings. Decrypts the
|
|
47607
|
+
sentinel findings store (uses the
|
|
47608
|
+
same passphrase as the cocoon master
|
|
47609
|
+
key).
|
|
47610
|
+
--since <iso> Filter observed_at >= iso.
|
|
47611
|
+
--severity <info|warn|alert> Filter by severity.
|
|
47612
|
+
--sentinel-id <id> Filter by emitting sentinel.
|
|
47613
|
+
--agent-id <id> Filter by agent attribution.
|
|
47614
|
+
--limit <n> Cap result count (default 100).
|
|
47615
|
+
`);
|
|
47616
|
+
}
|
|
47617
|
+
function cmdList4(out) {
|
|
47618
|
+
for (const entry of PHI1_BASELINE_CATALOG) {
|
|
47619
|
+
out.write(`${entry.sentinelId}
|
|
47620
|
+
${entry.description}
|
|
47621
|
+
`);
|
|
47622
|
+
}
|
|
47623
|
+
if (PHI1_BASELINE_CATALOG.length === 0) {
|
|
47624
|
+
out.write("(no sentinels registered)\n");
|
|
47625
|
+
}
|
|
47626
|
+
return 0;
|
|
47627
|
+
}
|
|
47628
|
+
async function cmdListSubscribed(argv, ctx) {
|
|
47629
|
+
const storagePath = await resolveStoragePath2(ctx.args);
|
|
47630
|
+
const subscribed = await loadSentinelSubscriptions(storagePath);
|
|
47631
|
+
if (subscribed.size === 0) {
|
|
47632
|
+
ctx.out.write("(no subscriptions)\n");
|
|
47633
|
+
return 0;
|
|
47634
|
+
}
|
|
47635
|
+
for (const id of [...subscribed].sort()) {
|
|
47636
|
+
ctx.out.write(`${id}
|
|
47637
|
+
`);
|
|
47638
|
+
}
|
|
47639
|
+
return 0;
|
|
47640
|
+
}
|
|
47641
|
+
async function cmdSubscribe(argv, ctx) {
|
|
47642
|
+
const sentinelId = argv[0];
|
|
47643
|
+
if (!sentinelId) {
|
|
47644
|
+
ctx.err.write("subscribe requires a sentinel-id\n");
|
|
47645
|
+
return 2;
|
|
47646
|
+
}
|
|
47647
|
+
const known = PHI1_BASELINE_CATALOG.find(
|
|
47648
|
+
(entry) => entry.sentinelId === sentinelId
|
|
47649
|
+
);
|
|
47650
|
+
if (!known) {
|
|
47651
|
+
ctx.err.write(`Unknown sentinel: ${sentinelId}
|
|
47652
|
+
`);
|
|
47653
|
+
return 2;
|
|
47654
|
+
}
|
|
47655
|
+
const storagePath = await resolveStoragePath2(ctx.args);
|
|
47656
|
+
const subscribed = await loadSentinelSubscriptions(storagePath);
|
|
47657
|
+
if (subscribed.has(sentinelId)) {
|
|
47658
|
+
ctx.out.write(`Already subscribed: ${sentinelId}
|
|
47659
|
+
`);
|
|
47660
|
+
return 0;
|
|
47661
|
+
}
|
|
47662
|
+
subscribed.add(sentinelId);
|
|
47663
|
+
await saveSentinelSubscriptions(storagePath, subscribed);
|
|
47664
|
+
ctx.out.write(
|
|
47665
|
+
`Subscribed: ${sentinelId}
|
|
47666
|
+
Restart Sanctuary or wait for the next dispatcher tick to begin evaluation.
|
|
47667
|
+
`
|
|
47668
|
+
);
|
|
47669
|
+
return 0;
|
|
47670
|
+
}
|
|
47671
|
+
async function cmdUnsubscribe(argv, ctx) {
|
|
47672
|
+
const sentinelId = argv[0];
|
|
47673
|
+
if (!sentinelId) {
|
|
47674
|
+
ctx.err.write("unsubscribe requires a sentinel-id\n");
|
|
47675
|
+
return 2;
|
|
47676
|
+
}
|
|
47677
|
+
const storagePath = await resolveStoragePath2(ctx.args);
|
|
47678
|
+
const subscribed = await loadSentinelSubscriptions(storagePath);
|
|
47679
|
+
if (!subscribed.has(sentinelId)) {
|
|
47680
|
+
ctx.out.write(`Not subscribed: ${sentinelId}
|
|
47681
|
+
`);
|
|
47682
|
+
return 0;
|
|
47683
|
+
}
|
|
47684
|
+
subscribed.delete(sentinelId);
|
|
47685
|
+
await saveSentinelSubscriptions(storagePath, subscribed);
|
|
47686
|
+
ctx.out.write(`Unsubscribed: ${sentinelId}
|
|
47687
|
+
`);
|
|
47688
|
+
return 0;
|
|
47689
|
+
}
|
|
47690
|
+
async function cmdFindings(argv, ctx) {
|
|
47691
|
+
const filters = parseFindingFilters(argv);
|
|
47692
|
+
const storagePath = await resolveStoragePath2(ctx.args);
|
|
47693
|
+
const storage = new FilesystemStorage(`${storagePath}/state`);
|
|
47694
|
+
let passphrase = ctx.args.passphrase ?? process.env["SANCTUARY_PASSPHRASE"];
|
|
47695
|
+
if (!passphrase) {
|
|
47696
|
+
const resolved = await getOrCreatePassphrase();
|
|
47697
|
+
passphrase = resolved.value;
|
|
47698
|
+
}
|
|
47699
|
+
let existingParams;
|
|
47700
|
+
try {
|
|
47701
|
+
const raw = await storage.read("_meta", "key-params");
|
|
47702
|
+
if (raw) existingParams = JSON.parse(bytesToString(raw));
|
|
47703
|
+
} catch {
|
|
47704
|
+
}
|
|
47705
|
+
const { key: masterKey, params } = await deriveMasterKey(
|
|
47706
|
+
passphrase,
|
|
47707
|
+
existingParams
|
|
47708
|
+
);
|
|
47709
|
+
if (!existingParams) {
|
|
47710
|
+
await storage.write(
|
|
47711
|
+
"_meta",
|
|
47712
|
+
"key-params",
|
|
47713
|
+
stringToBytes(JSON.stringify(params))
|
|
47714
|
+
);
|
|
47715
|
+
}
|
|
47716
|
+
const fortressId = fortressIdFromStoragePath(storagePath);
|
|
47717
|
+
const store = new SentinelFindingStore({
|
|
47718
|
+
storage,
|
|
47719
|
+
masterKey,
|
|
47720
|
+
fortressId
|
|
47721
|
+
});
|
|
47722
|
+
const findings = await store.listFindings({
|
|
47723
|
+
limit: filters.limit ?? 100,
|
|
47724
|
+
...filters.since !== void 0 ? { since: filters.since } : {},
|
|
47725
|
+
...filters.severity !== void 0 ? { severity: filters.severity } : {},
|
|
47726
|
+
...filters.sentinelId !== void 0 ? { sentinelId: filters.sentinelId } : {},
|
|
47727
|
+
...filters.agentId !== void 0 ? { agentId: filters.agentId } : {}
|
|
47728
|
+
});
|
|
47729
|
+
if (findings.length === 0) {
|
|
47730
|
+
ctx.out.write("(no findings)\n");
|
|
47731
|
+
return 0;
|
|
47732
|
+
}
|
|
47733
|
+
for (const finding of findings) {
|
|
47734
|
+
ctx.out.write(
|
|
47735
|
+
`[${finding.observed_at}] ${finding.severity.toUpperCase()} ${finding.sentinel_id}${finding.agent_id ? ` (agent ${finding.agent_id})` : ""}: ${finding.summary}
|
|
47736
|
+
`
|
|
47737
|
+
);
|
|
47738
|
+
}
|
|
47739
|
+
return 0;
|
|
47740
|
+
}
|
|
47741
|
+
function parseFindingFilters(argv) {
|
|
47742
|
+
const filters = {};
|
|
47743
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
47744
|
+
const arg = argv[i];
|
|
47745
|
+
if (arg === "--since" && argv[i + 1]) {
|
|
47746
|
+
filters.since = argv[++i];
|
|
47747
|
+
} else if (arg === "--severity" && argv[i + 1]) {
|
|
47748
|
+
const next = argv[++i];
|
|
47749
|
+
if (next === "info" || next === "warn" || next === "alert") {
|
|
47750
|
+
filters.severity = next;
|
|
47751
|
+
}
|
|
47752
|
+
} else if (arg === "--sentinel-id" && argv[i + 1]) {
|
|
47753
|
+
filters.sentinelId = argv[++i];
|
|
47754
|
+
} else if (arg === "--agent-id" && argv[i + 1]) {
|
|
47755
|
+
filters.agentId = argv[++i];
|
|
47756
|
+
} else if (arg === "--limit" && argv[i + 1]) {
|
|
47757
|
+
const n = Number.parseInt(argv[++i], 10);
|
|
47758
|
+
if (!Number.isNaN(n) && n > 0) filters.limit = n;
|
|
47759
|
+
}
|
|
47760
|
+
}
|
|
47761
|
+
return filters;
|
|
47762
|
+
}
|
|
47763
|
+
async function resolveStoragePath2(args) {
|
|
47764
|
+
if (args.storagePath) return args.storagePath;
|
|
47765
|
+
const config = await loadConfig();
|
|
47766
|
+
return config.storage_path;
|
|
47767
|
+
}
|
|
47768
|
+
var init_sentinel2 = __esm({
|
|
47769
|
+
"src/cli/sentinel.ts"() {
|
|
47770
|
+
init_config();
|
|
47771
|
+
init_filesystem();
|
|
47772
|
+
init_key_derivation();
|
|
47773
|
+
init_encoding();
|
|
47774
|
+
init_passphrase();
|
|
47775
|
+
init_wiring();
|
|
47776
|
+
init_sentinel_finding_store();
|
|
47777
|
+
init_subscription_store();
|
|
47778
|
+
init_sentinels();
|
|
47779
|
+
}
|
|
47780
|
+
});
|
|
47781
|
+
|
|
45443
47782
|
// src/mcp/broker-server.ts
|
|
45444
47783
|
var broker_server_exports = {};
|
|
45445
47784
|
__export(broker_server_exports, {
|
|
@@ -45535,7 +47874,7 @@ function createBrokerMcpServer(broker, opts) {
|
|
|
45535
47874
|
case "broker/request_token": {
|
|
45536
47875
|
const skill = requireString(args, "skill");
|
|
45537
47876
|
const secret = requireString(args, "secret");
|
|
45538
|
-
const scopeRaw =
|
|
47877
|
+
const scopeRaw = optionalString2(args, "scope");
|
|
45539
47878
|
const scope = scopeRaw === "rotate" ? "rotate" : scopeRaw === "read" ? "read" : void 0;
|
|
45540
47879
|
const ttl = optionalNumber(args, "ttl_seconds");
|
|
45541
47880
|
const binding = await broker.issueToken({
|
|
@@ -45567,7 +47906,7 @@ function createBrokerMcpServer(broker, opts) {
|
|
|
45567
47906
|
return ok({ grants });
|
|
45568
47907
|
}
|
|
45569
47908
|
case "broker/audit_query": {
|
|
45570
|
-
const since =
|
|
47909
|
+
const since = optionalString2(args, "since");
|
|
45571
47910
|
const limit = optionalNumber(args, "limit");
|
|
45572
47911
|
const summary = await broker.queryAudit({ since, limit });
|
|
45573
47912
|
return ok(summary);
|
|
@@ -45592,7 +47931,7 @@ function requireString(args, key) {
|
|
|
45592
47931
|
}
|
|
45593
47932
|
return v;
|
|
45594
47933
|
}
|
|
45595
|
-
function
|
|
47934
|
+
function optionalString2(args, key) {
|
|
45596
47935
|
const v = args[key];
|
|
45597
47936
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
45598
47937
|
}
|
|
@@ -46299,6 +48638,11 @@ async function main() {
|
|
|
46299
48638
|
const code = await runIntelligenceCommand2({ argv: args.slice(1) });
|
|
46300
48639
|
process.exit(code);
|
|
46301
48640
|
}
|
|
48641
|
+
if (args[0] === "sentinel") {
|
|
48642
|
+
const { runSentinelCommand: runSentinelCommand2 } = await Promise.resolve().then(() => (init_sentinel2(), sentinel_exports));
|
|
48643
|
+
const code = await runSentinelCommand2({ argv: args.slice(1) });
|
|
48644
|
+
process.exit(code);
|
|
48645
|
+
}
|
|
46302
48646
|
if (args[0] === "broker-server") {
|
|
46303
48647
|
const { openBroker: openBroker2 } = await Promise.resolve().then(() => (init_open(), open_exports));
|
|
46304
48648
|
const { createBrokerMcpServer: createBrokerMcpServer2 } = await Promise.resolve().then(() => (init_broker_server(), broker_server_exports));
|