@sanctuary-framework/mcp-server 1.2.6 → 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 +3131 -54
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3131 -54
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +2787 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +970 -10
- package/dist/index.d.ts +970 -10
- package/dist/index.js +2787 -43
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -4922,7 +4922,8 @@ var SIGNATURE_SCHEME_V1 = "ed25519-v1";
|
|
|
4922
4922
|
var RESERVED_EVENT_TYPE_PREFIXES = [
|
|
4923
4923
|
"EXTENSION_",
|
|
4924
4924
|
"cross_fortress_",
|
|
4925
|
-
"multi_master_"
|
|
4925
|
+
"multi_master_",
|
|
4926
|
+
"cross_harness_approval_"
|
|
4926
4927
|
];
|
|
4927
4928
|
function isReservedEventType(s) {
|
|
4928
4929
|
return RESERVED_EVENT_TYPE_PREFIXES.some((p) => s.startsWith(p));
|
|
@@ -9102,9 +9103,9 @@ function fingerprintDID(did) {
|
|
|
9102
9103
|
return `${raw.slice(0, 6)}\u2026${raw.slice(-6)}`;
|
|
9103
9104
|
}
|
|
9104
9105
|
function countInjectionsToday(audit) {
|
|
9105
|
-
const
|
|
9106
|
-
|
|
9107
|
-
const cutoff =
|
|
9106
|
+
const startOfDay2 = /* @__PURE__ */ new Date();
|
|
9107
|
+
startOfDay2.setHours(0, 0, 0, 0);
|
|
9108
|
+
const cutoff = startOfDay2.getTime();
|
|
9108
9109
|
return audit.filter((e) => {
|
|
9109
9110
|
const ts = new Date(e.timestamp).getTime();
|
|
9110
9111
|
if (isNaN(ts) || ts < cutoff) return false;
|
|
@@ -9118,9 +9119,9 @@ var PROOF_CREATION_OPS = /* @__PURE__ */ new Set([
|
|
|
9118
9119
|
"proof_commitment"
|
|
9119
9120
|
]);
|
|
9120
9121
|
function countProofsToday(audit) {
|
|
9121
|
-
const
|
|
9122
|
-
|
|
9123
|
-
const cutoff =
|
|
9122
|
+
const startOfDay2 = /* @__PURE__ */ new Date();
|
|
9123
|
+
startOfDay2.setHours(0, 0, 0, 0);
|
|
9124
|
+
const cutoff = startOfDay2.getTime();
|
|
9124
9125
|
return audit.filter((e) => {
|
|
9125
9126
|
if (e.layer !== "l3") return false;
|
|
9126
9127
|
if (!PROOF_CREATION_OPS.has(e.operation)) return false;
|
|
@@ -16319,6 +16320,24 @@ async function handleApprovalInboxRoute(deps, req, res) {
|
|
|
16319
16320
|
await handleStream2(deps, res);
|
|
16320
16321
|
return true;
|
|
16321
16322
|
}
|
|
16323
|
+
if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/revision`) {
|
|
16324
|
+
const revision = await deps.aggregator.getRevision();
|
|
16325
|
+
writeJSON4(res, 200, { ok: true, data: { revision } });
|
|
16326
|
+
return true;
|
|
16327
|
+
}
|
|
16328
|
+
if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/sync`) {
|
|
16329
|
+
const sinceRaw = url.searchParams.get("since_revision");
|
|
16330
|
+
const sinceParsed = sinceRaw === null ? 0 : Number.parseInt(sinceRaw, 10);
|
|
16331
|
+
const sinceRevision = Number.isFinite(sinceParsed) && sinceParsed >= 0 ? sinceParsed : 0;
|
|
16332
|
+
const limit = parseLimit2(
|
|
16333
|
+
url.searchParams.get("limit"),
|
|
16334
|
+
APPROVAL_INBOX_DEFAULT_LIMIT,
|
|
16335
|
+
APPROVAL_INBOX_MAX_LIMIT
|
|
16336
|
+
);
|
|
16337
|
+
const delta = await deps.aggregator.getSync({ sinceRevision, limit });
|
|
16338
|
+
writeJSON4(res, 200, { ok: true, data: delta });
|
|
16339
|
+
return true;
|
|
16340
|
+
}
|
|
16322
16341
|
if (method === "GET" && path === `${APPROVAL_INBOX_API_PREFIX}/history`) {
|
|
16323
16342
|
const limit = parseLimit2(
|
|
16324
16343
|
url.searchParams.get("limit"),
|
|
@@ -16434,6 +16453,119 @@ async function handleApprovalInboxRoute(deps, req, res) {
|
|
|
16434
16453
|
}
|
|
16435
16454
|
}
|
|
16436
16455
|
|
|
16456
|
+
// src/sentinel/sentinel-routes.ts
|
|
16457
|
+
var SENTINEL_API_PREFIX = "/api/sentinels";
|
|
16458
|
+
var FINDINGS_DEFAULT_LIMIT = 100;
|
|
16459
|
+
var FINDINGS_MAX_LIMIT = 500;
|
|
16460
|
+
function writeJSON5(res, status, payload) {
|
|
16461
|
+
res.writeHead(status, {
|
|
16462
|
+
"Content-Type": "application/json",
|
|
16463
|
+
"Cache-Control": "no-store"
|
|
16464
|
+
});
|
|
16465
|
+
res.end(JSON.stringify(payload));
|
|
16466
|
+
}
|
|
16467
|
+
function isSeverity(value) {
|
|
16468
|
+
return value === "info" || value === "warn" || value === "alert";
|
|
16469
|
+
}
|
|
16470
|
+
function parseLimit3(raw, defaultValue, max) {
|
|
16471
|
+
if (raw === null || raw === "") return defaultValue;
|
|
16472
|
+
const parsed = Number.parseInt(raw, 10);
|
|
16473
|
+
if (Number.isNaN(parsed) || parsed < 0) return defaultValue;
|
|
16474
|
+
return Math.min(parsed, max);
|
|
16475
|
+
}
|
|
16476
|
+
function matchSubscribeRoute(path) {
|
|
16477
|
+
const prefix = `${SENTINEL_API_PREFIX}/`;
|
|
16478
|
+
if (!path.startsWith(prefix)) return null;
|
|
16479
|
+
const rest = path.slice(prefix.length);
|
|
16480
|
+
if (!rest.endsWith("/subscribe")) return null;
|
|
16481
|
+
const sentinelId = rest.slice(0, rest.length - "/subscribe".length);
|
|
16482
|
+
if (sentinelId.length === 0) return null;
|
|
16483
|
+
return { sentinelId: decodeURIComponent(sentinelId) };
|
|
16484
|
+
}
|
|
16485
|
+
async function handleSentinelRoute(deps, req, res) {
|
|
16486
|
+
const host = req.headers.host || "localhost";
|
|
16487
|
+
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
16488
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
16489
|
+
const path = url.pathname;
|
|
16490
|
+
if (path !== SENTINEL_API_PREFIX && !path.startsWith(`${SENTINEL_API_PREFIX}/`)) {
|
|
16491
|
+
return false;
|
|
16492
|
+
}
|
|
16493
|
+
const checkAuth = authMiddleware(deps.authConfig);
|
|
16494
|
+
if (!checkAuth(req, res, url)) return true;
|
|
16495
|
+
const dispatcher = deps.dispatcher;
|
|
16496
|
+
const registry = dispatcher.getRegistry();
|
|
16497
|
+
const findingStore = dispatcher.getFindingStore();
|
|
16498
|
+
try {
|
|
16499
|
+
if (method === "GET" && path === SENTINEL_API_PREFIX) {
|
|
16500
|
+
const catalog = registry.listCatalog();
|
|
16501
|
+
writeJSON5(res, 200, { ok: true, data: { catalog } });
|
|
16502
|
+
return true;
|
|
16503
|
+
}
|
|
16504
|
+
if (method === "GET" && path === `${SENTINEL_API_PREFIX}/subscribed`) {
|
|
16505
|
+
const subscribed = registry.listSubscribed();
|
|
16506
|
+
writeJSON5(res, 200, { ok: true, data: { subscribed } });
|
|
16507
|
+
return true;
|
|
16508
|
+
}
|
|
16509
|
+
if (method === "GET" && path === `${SENTINEL_API_PREFIX}/findings`) {
|
|
16510
|
+
const limit = parseLimit3(
|
|
16511
|
+
url.searchParams.get("limit"),
|
|
16512
|
+
FINDINGS_DEFAULT_LIMIT,
|
|
16513
|
+
FINDINGS_MAX_LIMIT
|
|
16514
|
+
);
|
|
16515
|
+
const since = url.searchParams.get("since") ?? void 0;
|
|
16516
|
+
const severityRaw = url.searchParams.get("severity") ?? void 0;
|
|
16517
|
+
const sentinelIdFilter = url.searchParams.get("sentinel_id") ?? void 0;
|
|
16518
|
+
const agentIdFilter = url.searchParams.get("agent_id") ?? void 0;
|
|
16519
|
+
const severity = severityRaw && isSeverity(severityRaw) ? severityRaw : void 0;
|
|
16520
|
+
const findings = await findingStore.listFindings({
|
|
16521
|
+
limit,
|
|
16522
|
+
...since !== void 0 ? { since } : {},
|
|
16523
|
+
...severity !== void 0 ? { severity } : {},
|
|
16524
|
+
...sentinelIdFilter !== void 0 ? { sentinelId: sentinelIdFilter } : {},
|
|
16525
|
+
...agentIdFilter !== void 0 ? { agentId: agentIdFilter } : {}
|
|
16526
|
+
});
|
|
16527
|
+
writeJSON5(res, 200, { ok: true, data: { findings } });
|
|
16528
|
+
return true;
|
|
16529
|
+
}
|
|
16530
|
+
const subscribeMatch = matchSubscribeRoute(path);
|
|
16531
|
+
if (subscribeMatch) {
|
|
16532
|
+
if (method === "POST") {
|
|
16533
|
+
try {
|
|
16534
|
+
await dispatcher.subscribeSentinel(subscribeMatch.sentinelId);
|
|
16535
|
+
writeJSON5(res, 200, {
|
|
16536
|
+
ok: true,
|
|
16537
|
+
data: { sentinel_id: subscribeMatch.sentinelId, subscribed: true }
|
|
16538
|
+
});
|
|
16539
|
+
} catch (err) {
|
|
16540
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
16541
|
+
if (msg.startsWith("sentinel-registry: unknown sentinel")) {
|
|
16542
|
+
writeJSON5(res, 404, { ok: false, error: "not_found" });
|
|
16543
|
+
} else {
|
|
16544
|
+
writeJSON5(res, 500, { ok: false, error: "internal", detail: msg });
|
|
16545
|
+
}
|
|
16546
|
+
}
|
|
16547
|
+
return true;
|
|
16548
|
+
}
|
|
16549
|
+
if (method === "DELETE") {
|
|
16550
|
+
const removed = await dispatcher.unsubscribeSentinel(
|
|
16551
|
+
subscribeMatch.sentinelId
|
|
16552
|
+
);
|
|
16553
|
+
writeJSON5(res, 200, {
|
|
16554
|
+
ok: true,
|
|
16555
|
+
data: { sentinel_id: subscribeMatch.sentinelId, subscribed: false, removed }
|
|
16556
|
+
});
|
|
16557
|
+
return true;
|
|
16558
|
+
}
|
|
16559
|
+
}
|
|
16560
|
+
writeJSON5(res, 404, { ok: false, error: "not_found", path });
|
|
16561
|
+
return true;
|
|
16562
|
+
} catch (err) {
|
|
16563
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
16564
|
+
writeJSON5(res, 500, { ok: false, error: "internal", detail: msg });
|
|
16565
|
+
return true;
|
|
16566
|
+
}
|
|
16567
|
+
}
|
|
16568
|
+
|
|
16437
16569
|
// src/principal-policy/dashboard.ts
|
|
16438
16570
|
var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
16439
16571
|
var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -16506,6 +16638,13 @@ var DashboardApprovalChannel = class {
|
|
|
16506
16638
|
* the operator-facing query / decision surface.
|
|
16507
16639
|
*/
|
|
16508
16640
|
approvalAggregator = null;
|
|
16641
|
+
/**
|
|
16642
|
+
* v1.3 WP-V1.3-1 Phi-1 Sentinel dispatcher. Mounted additively at
|
|
16643
|
+
* `/api/sentinels/*` when set. Sentinel surface is read-only against
|
|
16644
|
+
* the audit log; subscribe/unsubscribe writes flow through the
|
|
16645
|
+
* dispatcher's audited paths.
|
|
16646
|
+
*/
|
|
16647
|
+
sentinelDispatcher = null;
|
|
16509
16648
|
constructor(config) {
|
|
16510
16649
|
this.config = config;
|
|
16511
16650
|
this.authToken = config.auth_token;
|
|
@@ -16565,6 +16704,14 @@ var DashboardApprovalChannel = class {
|
|
|
16565
16704
|
setApprovalAggregator(aggregator) {
|
|
16566
16705
|
this.approvalAggregator = aggregator;
|
|
16567
16706
|
}
|
|
16707
|
+
/**
|
|
16708
|
+
* v1.3 WP-V1.3-1 Phi-1: bind the Sentinel dispatcher. Once set,
|
|
16709
|
+
* requests to `/api/sentinels/*` route through `handleSentinelRoute`.
|
|
16710
|
+
* Pass `null` to detach (used by tests + during shutdown).
|
|
16711
|
+
*/
|
|
16712
|
+
setSentinelDispatcher(dispatcher) {
|
|
16713
|
+
this.sentinelDispatcher = dispatcher;
|
|
16714
|
+
}
|
|
16568
16715
|
/**
|
|
16569
16716
|
* v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
|
|
16570
16717
|
* before the legacy approval route table. Returns true when served.
|
|
@@ -16584,6 +16731,25 @@ var DashboardApprovalChannel = class {
|
|
|
16584
16731
|
res
|
|
16585
16732
|
);
|
|
16586
16733
|
}
|
|
16734
|
+
/**
|
|
16735
|
+
* v1.3 WP-V1.3-1 Phi-1 dispatch entry point. Routes `/api/sentinels/*`
|
|
16736
|
+
* requests through the sentinel router when a dispatcher has been
|
|
16737
|
+
* bound. Returns true when served.
|
|
16738
|
+
*/
|
|
16739
|
+
async dispatchSentinel(req, res) {
|
|
16740
|
+
if (!this.sentinelDispatcher) return false;
|
|
16741
|
+
return handleSentinelRoute(
|
|
16742
|
+
{
|
|
16743
|
+
authConfig: {
|
|
16744
|
+
loopbackAutoAuth: this._autoAuthLocalhost,
|
|
16745
|
+
...this.authToken !== void 0 ? { authToken: this.authToken } : {}
|
|
16746
|
+
},
|
|
16747
|
+
dispatcher: this.sentinelDispatcher
|
|
16748
|
+
},
|
|
16749
|
+
req,
|
|
16750
|
+
res
|
|
16751
|
+
);
|
|
16752
|
+
}
|
|
16587
16753
|
/**
|
|
16588
16754
|
* v1.1 dispatch entry point. Called from `handleRequest` before the
|
|
16589
16755
|
* legacy route table. Returns true when the request was served by v1.1
|
|
@@ -16971,6 +17137,18 @@ var DashboardApprovalChannel = class {
|
|
|
16971
17137
|
});
|
|
16972
17138
|
return;
|
|
16973
17139
|
}
|
|
17140
|
+
if (this.sentinelDispatcher && url.pathname.startsWith(SENTINEL_API_PREFIX)) {
|
|
17141
|
+
this.dispatchSentinel(req, res).then((handled) => {
|
|
17142
|
+
if (handled) return;
|
|
17143
|
+
this.handleLegacyRequest(req, res, url, method);
|
|
17144
|
+
}).catch(() => {
|
|
17145
|
+
if (!res.headersSent) {
|
|
17146
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
17147
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
17148
|
+
}
|
|
17149
|
+
});
|
|
17150
|
+
return;
|
|
17151
|
+
}
|
|
16974
17152
|
if (this.v11Bindings) {
|
|
16975
17153
|
this.dispatchV11(req, res, url, method).then((handled) => {
|
|
16976
17154
|
if (handled) return;
|
|
@@ -19414,6 +19592,20 @@ var ApprovalAggregator = class {
|
|
|
19414
19592
|
hydrated = false;
|
|
19415
19593
|
/** Active SSE listeners. */
|
|
19416
19594
|
listeners = /* @__PURE__ */ new Set();
|
|
19595
|
+
/**
|
|
19596
|
+
* Monotonic revision counter, bumped on every mutation (ingest of new
|
|
19597
|
+
* entry, resolve, expire, delete). Hydrated from max(last_modified_revision)
|
|
19598
|
+
* across persisted entries on first read; in-memory after that. v1.3
|
|
19599
|
+
* Upsilon-4.
|
|
19600
|
+
*/
|
|
19601
|
+
currentRevision = 0;
|
|
19602
|
+
/**
|
|
19603
|
+
* Removal tombstones: aggregator_id -> revision at removal. Used by the
|
|
19604
|
+
* sync API to surface "removed" entries to mobile consumers between
|
|
19605
|
+
* polls. In-memory only; server restart clears tombstones (mobile
|
|
19606
|
+
* bootstraps via `list()` on reconnect). v1.3 Upsilon-4.
|
|
19607
|
+
*/
|
|
19608
|
+
removedTombstones = /* @__PURE__ */ new Map();
|
|
19417
19609
|
constructor(deps) {
|
|
19418
19610
|
this.storage = deps.storage;
|
|
19419
19611
|
this.encryptionKey = derivePurposeKey(
|
|
@@ -19448,6 +19640,113 @@ var ApprovalAggregator = class {
|
|
|
19448
19640
|
this.listeners.add(listener);
|
|
19449
19641
|
return () => this.listeners.delete(listener);
|
|
19450
19642
|
}
|
|
19643
|
+
/**
|
|
19644
|
+
* Current aggregator revision. v1.3 Upsilon-4. Mobile companions
|
|
19645
|
+
* poll the lightweight `/revision` route to detect that something
|
|
19646
|
+
* changed before fetching a full sync delta.
|
|
19647
|
+
*/
|
|
19648
|
+
async getRevision() {
|
|
19649
|
+
await this.hydrate();
|
|
19650
|
+
return this.currentRevision;
|
|
19651
|
+
}
|
|
19652
|
+
/**
|
|
19653
|
+
* Compute a delta since `sinceRevision`. v1.3 Upsilon-4. Mobile
|
|
19654
|
+
* clients poll this for cheap state-sync. Behavior:
|
|
19655
|
+
* - `added`: entries whose `created_at_revision > sinceRevision`.
|
|
19656
|
+
* - `changed`: entries that existed at `sinceRevision` but had a
|
|
19657
|
+
* status transition (resolve, expire) since.
|
|
19658
|
+
* - `removed`: aggregator_ids deleted after `sinceRevision`.
|
|
19659
|
+
* - `revision`: current aggregator revision; pass this back as
|
|
19660
|
+
* `sinceRevision` on the next call.
|
|
19661
|
+
*
|
|
19662
|
+
* `limit` caps the total count returned across all three lists,
|
|
19663
|
+
* prioritized as added -> changed -> removed (newer-state first).
|
|
19664
|
+
* When more changes exist than fit, the next call with the returned
|
|
19665
|
+
* revision will pick up the rest because each entry's
|
|
19666
|
+
* last_modified_revision is unchanged by truncation.
|
|
19667
|
+
*/
|
|
19668
|
+
async getSync(opts) {
|
|
19669
|
+
await this.hydrate();
|
|
19670
|
+
await this.expireStale();
|
|
19671
|
+
const sinceRevision = opts?.sinceRevision ?? 0;
|
|
19672
|
+
const cap = Math.min(
|
|
19673
|
+
opts?.limit ?? DEFAULT_LIST_PAGE_SIZE,
|
|
19674
|
+
this.maxListLimit
|
|
19675
|
+
);
|
|
19676
|
+
const added = [];
|
|
19677
|
+
const changed = [];
|
|
19678
|
+
for (const entry of this.entries.values()) {
|
|
19679
|
+
const lastMod = entry.last_modified_revision ?? 0;
|
|
19680
|
+
if (lastMod <= sinceRevision) continue;
|
|
19681
|
+
const createdRev = entry.created_at_revision ?? 0;
|
|
19682
|
+
if (createdRev > sinceRevision) {
|
|
19683
|
+
added.push(entry);
|
|
19684
|
+
} else {
|
|
19685
|
+
changed.push(entry);
|
|
19686
|
+
}
|
|
19687
|
+
}
|
|
19688
|
+
const removed = [];
|
|
19689
|
+
for (const [id, rev] of this.removedTombstones) {
|
|
19690
|
+
if (rev > sinceRevision) removed.push(id);
|
|
19691
|
+
}
|
|
19692
|
+
added.sort(
|
|
19693
|
+
(a, b) => (a.last_modified_revision ?? 0) - (b.last_modified_revision ?? 0)
|
|
19694
|
+
);
|
|
19695
|
+
changed.sort(
|
|
19696
|
+
(a, b) => (a.last_modified_revision ?? 0) - (b.last_modified_revision ?? 0)
|
|
19697
|
+
);
|
|
19698
|
+
let remaining = cap;
|
|
19699
|
+
const addedOut = added.slice(0, Math.max(0, remaining));
|
|
19700
|
+
remaining -= addedOut.length;
|
|
19701
|
+
const changedOut = changed.slice(0, Math.max(0, remaining));
|
|
19702
|
+
remaining -= changedOut.length;
|
|
19703
|
+
const removedOut = removed.slice(0, Math.max(0, remaining));
|
|
19704
|
+
return {
|
|
19705
|
+
revision: this.currentRevision,
|
|
19706
|
+
added: addedOut,
|
|
19707
|
+
changed: changedOut,
|
|
19708
|
+
removed: removedOut
|
|
19709
|
+
};
|
|
19710
|
+
}
|
|
19711
|
+
/**
|
|
19712
|
+
* Delete an entry. Drops the in-memory record, the persisted bundle,
|
|
19713
|
+
* and the at-rest payload (if a payload store is wired). Records a
|
|
19714
|
+
* tombstone with the new revision so sync-API consumers see a
|
|
19715
|
+
* `removed` delta. Returns true when an entry was deleted, false on
|
|
19716
|
+
* unknown id. v1.3 Upsilon-4. Reserved for v1.4+ retention housekeeping;
|
|
19717
|
+
* Upsilon-4 ships the surface so mobile sync-API tests can exercise the
|
|
19718
|
+
* removal path.
|
|
19719
|
+
*/
|
|
19720
|
+
async deleteEntry(aggregatorId) {
|
|
19721
|
+
await this.hydrate();
|
|
19722
|
+
const entry = this.entries.get(aggregatorId);
|
|
19723
|
+
if (!entry) return false;
|
|
19724
|
+
const dedupKey = `${entry.source_harness}|${entry.source_agent_id}|${entry.audit_log_entry_id}`;
|
|
19725
|
+
this.entries.delete(aggregatorId);
|
|
19726
|
+
this.dedupIndex.delete(dedupKey);
|
|
19727
|
+
this.fullPayloads.delete(aggregatorId);
|
|
19728
|
+
for (const [corr, id] of this.correlationIndex) {
|
|
19729
|
+
if (id === aggregatorId) this.correlationIndex.delete(corr);
|
|
19730
|
+
}
|
|
19731
|
+
try {
|
|
19732
|
+
await this.storage.delete(APPROVAL_AGGREGATOR_NAMESPACE, aggregatorId);
|
|
19733
|
+
} catch {
|
|
19734
|
+
}
|
|
19735
|
+
if (this.payloadStore) {
|
|
19736
|
+
try {
|
|
19737
|
+
await this.payloadStore.deletePayload(aggregatorId);
|
|
19738
|
+
} catch {
|
|
19739
|
+
}
|
|
19740
|
+
}
|
|
19741
|
+
const revision = this.nextRevision();
|
|
19742
|
+
this.removedTombstones.set(aggregatorId, revision);
|
|
19743
|
+
this.emit({ type: "removed", entry: { ...entry } });
|
|
19744
|
+
return true;
|
|
19745
|
+
}
|
|
19746
|
+
nextRevision() {
|
|
19747
|
+
this.currentRevision += 1;
|
|
19748
|
+
return this.currentRevision;
|
|
19749
|
+
}
|
|
19451
19750
|
/**
|
|
19452
19751
|
* Ingest a gate event. Returns the aggregator entry on first sight,
|
|
19453
19752
|
* `null` when deduped. Resolution events update the existing record;
|
|
@@ -19658,6 +19957,7 @@ var ApprovalAggregator = class {
|
|
|
19658
19957
|
entry.status = decision;
|
|
19659
19958
|
entry.resolved_at = this.now().toISOString();
|
|
19660
19959
|
entry.resolved_by = operatorId;
|
|
19960
|
+
entry.last_modified_revision = this.nextRevision();
|
|
19661
19961
|
await this.persist(entry);
|
|
19662
19962
|
this.auditLog.append(
|
|
19663
19963
|
"l2",
|
|
@@ -19709,6 +20009,7 @@ var ApprovalAggregator = class {
|
|
|
19709
20009
|
const expires = new Date(now.getTime() + this.pendingTtlMs);
|
|
19710
20010
|
const hubInboxId = this.resolveHubInboxItemId(event);
|
|
19711
20011
|
const enforcementChain = this.resolveEnforcementChain(event);
|
|
20012
|
+
const revision = this.nextRevision();
|
|
19712
20013
|
const entry = {
|
|
19713
20014
|
aggregator_id: id,
|
|
19714
20015
|
source_harness: ctx.source_harness,
|
|
@@ -19720,6 +20021,8 @@ var ApprovalAggregator = class {
|
|
|
19720
20021
|
status: "pending",
|
|
19721
20022
|
created_at: now.toISOString(),
|
|
19722
20023
|
expires_at: expires.toISOString(),
|
|
20024
|
+
created_at_revision: revision,
|
|
20025
|
+
last_modified_revision: revision,
|
|
19723
20026
|
...hubInboxId !== void 0 ? { hub_inbox_item_id: hubInboxId } : {},
|
|
19724
20027
|
...enforcementChain.length > 0 ? { enforcement_chain: enforcementChain } : {}
|
|
19725
20028
|
};
|
|
@@ -19762,6 +20065,7 @@ var ApprovalAggregator = class {
|
|
|
19762
20065
|
entry.status = status;
|
|
19763
20066
|
entry.resolved_at = event.resolution.decided_at;
|
|
19764
20067
|
entry.resolved_by = event.resolution.decided_by;
|
|
20068
|
+
entry.last_modified_revision = this.nextRevision();
|
|
19765
20069
|
await this.persist(entry);
|
|
19766
20070
|
this.auditLog.append(
|
|
19767
20071
|
"l2",
|
|
@@ -19824,6 +20128,7 @@ var ApprovalAggregator = class {
|
|
|
19824
20128
|
entry.status = "expired";
|
|
19825
20129
|
entry.resolved_at = this.now().toISOString();
|
|
19826
20130
|
entry.resolved_by = "system_ttl";
|
|
20131
|
+
entry.last_modified_revision = this.nextRevision();
|
|
19827
20132
|
await this.persist(entry);
|
|
19828
20133
|
this.auditLog.append(
|
|
19829
20134
|
"l2",
|
|
@@ -19870,6 +20175,10 @@ var ApprovalAggregator = class {
|
|
|
19870
20175
|
this.entries.set(entry.aggregator_id, entry);
|
|
19871
20176
|
const dedupKey = `${entry.source_harness}|${entry.source_agent_id}|${entry.audit_log_entry_id}`;
|
|
19872
20177
|
this.dedupIndex.set(dedupKey, entry.aggregator_id);
|
|
20178
|
+
const lastMod = entry.last_modified_revision ?? 0;
|
|
20179
|
+
if (lastMod > this.currentRevision) {
|
|
20180
|
+
this.currentRevision = lastMod;
|
|
20181
|
+
}
|
|
19873
20182
|
} catch {
|
|
19874
20183
|
}
|
|
19875
20184
|
}
|
|
@@ -20179,6 +20488,1648 @@ function stripKeyPrefix(key) {
|
|
|
20179
20488
|
return key.slice(AGGREGATOR_PAYLOAD_KEY_PREFIX.length);
|
|
20180
20489
|
}
|
|
20181
20490
|
|
|
20491
|
+
// src/sentinel/sentinel-finding-store.ts
|
|
20492
|
+
init_encryption();
|
|
20493
|
+
init_encoding();
|
|
20494
|
+
|
|
20495
|
+
// src/sentinel/types.ts
|
|
20496
|
+
var SENTINEL_SUMMARY_MAX_CHARS = 240;
|
|
20497
|
+
var SENTINEL_AUDIT_OPS = {
|
|
20498
|
+
SUBSCRIBED: "sentinel_subscribed",
|
|
20499
|
+
UNSUBSCRIBED: "sentinel_unsubscribed",
|
|
20500
|
+
FINDING_EMITTED: "sentinel_finding_emitted",
|
|
20501
|
+
EVALUATION_FAILED: "sentinel_evaluation_failed"
|
|
20502
|
+
};
|
|
20503
|
+
var SENTINEL_OBSERVED_AUDIT_OPS = {
|
|
20504
|
+
/** Proxy router emits this on every outbound call (success or failure). */
|
|
20505
|
+
PROXY_CALL_PREFIX: "proxy_call:"
|
|
20506
|
+
};
|
|
20507
|
+
function isProxyCallAuditEntry(entry) {
|
|
20508
|
+
return entry.operation.startsWith(
|
|
20509
|
+
SENTINEL_OBSERVED_AUDIT_OPS.PROXY_CALL_PREFIX
|
|
20510
|
+
);
|
|
20511
|
+
}
|
|
20512
|
+
function proxyServerFromAuditEntry(entry) {
|
|
20513
|
+
if (!isProxyCallAuditEntry(entry)) return null;
|
|
20514
|
+
const details = entry.details;
|
|
20515
|
+
if (!details) return null;
|
|
20516
|
+
const server = details["server"];
|
|
20517
|
+
if (typeof server !== "string" || server.length === 0) return null;
|
|
20518
|
+
return server;
|
|
20519
|
+
}
|
|
20520
|
+
|
|
20521
|
+
// src/sentinel/sentinel-finding-store.ts
|
|
20522
|
+
var SENTINEL_FINDING_NAMESPACE = "_sentinel_findings";
|
|
20523
|
+
var SENTINEL_FINDING_KEY_PREFIX = "finding.";
|
|
20524
|
+
var HKDF_INFO2 = "l2-sentinel-finding-v1";
|
|
20525
|
+
var DEFAULT_SENTINEL_FINDING_RETENTION_DAYS = 30;
|
|
20526
|
+
var MAX_FINDING_BYTES = 256 * 1024;
|
|
20527
|
+
var SentinelFindingStore = class {
|
|
20528
|
+
storage;
|
|
20529
|
+
encryptionKey;
|
|
20530
|
+
fortressId;
|
|
20531
|
+
retentionDays;
|
|
20532
|
+
now;
|
|
20533
|
+
constructor(opts) {
|
|
20534
|
+
this.storage = opts.storage;
|
|
20535
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO2);
|
|
20536
|
+
this.fortressId = opts.fortressId;
|
|
20537
|
+
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_SENTINEL_FINDING_RETENTION_DAYS;
|
|
20538
|
+
this.now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
20539
|
+
}
|
|
20540
|
+
/**
|
|
20541
|
+
* Persist a finding. Truncates the operator-visible summary to
|
|
20542
|
+
* SENTINEL_SUMMARY_MAX_CHARS so the dashboard render stays bounded.
|
|
20543
|
+
* Returns the retention deadline so callers can audit it.
|
|
20544
|
+
*/
|
|
20545
|
+
async saveFinding(finding) {
|
|
20546
|
+
const truncated = {
|
|
20547
|
+
...finding,
|
|
20548
|
+
fortress_id: this.fortressId,
|
|
20549
|
+
summary: truncateSummary(finding.summary)
|
|
20550
|
+
};
|
|
20551
|
+
const retentionMs = this.retentionDays * 24 * 60 * 60 * 1e3;
|
|
20552
|
+
const retentionUntil = new Date(this.now().getTime() + retentionMs);
|
|
20553
|
+
const persisted = {
|
|
20554
|
+
version: 1,
|
|
20555
|
+
finding: truncated,
|
|
20556
|
+
retention_until: retentionUntil.toISOString()
|
|
20557
|
+
};
|
|
20558
|
+
const aad = stringToBytes(finding.finding_id);
|
|
20559
|
+
const plaintext = stringToBytes(JSON.stringify(persisted));
|
|
20560
|
+
const envelope = encrypt(plaintext, this.encryptionKey, aad);
|
|
20561
|
+
await this.storage.write(
|
|
20562
|
+
SENTINEL_FINDING_NAMESPACE,
|
|
20563
|
+
findingKey(finding.finding_id),
|
|
20564
|
+
stringToBytes(JSON.stringify(envelope))
|
|
20565
|
+
);
|
|
20566
|
+
return persisted.retention_until;
|
|
20567
|
+
}
|
|
20568
|
+
/** Load a single finding by id, or null when absent / corrupted. */
|
|
20569
|
+
async loadFinding(findingId) {
|
|
20570
|
+
let raw;
|
|
20571
|
+
try {
|
|
20572
|
+
raw = await this.storage.read(
|
|
20573
|
+
SENTINEL_FINDING_NAMESPACE,
|
|
20574
|
+
findingKey(findingId)
|
|
20575
|
+
);
|
|
20576
|
+
} catch {
|
|
20577
|
+
return null;
|
|
20578
|
+
}
|
|
20579
|
+
if (!raw) return null;
|
|
20580
|
+
if (raw.length > MAX_FINDING_BYTES) return null;
|
|
20581
|
+
return this.decode(findingId, raw);
|
|
20582
|
+
}
|
|
20583
|
+
/**
|
|
20584
|
+
* List findings, newest first. Optional filters: since (ISO 8601),
|
|
20585
|
+
* severity, sentinel_id, agent_id, limit. Default limit 100.
|
|
20586
|
+
*/
|
|
20587
|
+
async listFindings(opts) {
|
|
20588
|
+
const metas = await this.storage.list(
|
|
20589
|
+
SENTINEL_FINDING_NAMESPACE,
|
|
20590
|
+
SENTINEL_FINDING_KEY_PREFIX
|
|
20591
|
+
);
|
|
20592
|
+
const findings = [];
|
|
20593
|
+
for (const meta of metas) {
|
|
20594
|
+
const id = stripKeyPrefix2(meta.key);
|
|
20595
|
+
if (id === null) continue;
|
|
20596
|
+
const raw = await this.storage.read(
|
|
20597
|
+
SENTINEL_FINDING_NAMESPACE,
|
|
20598
|
+
meta.key
|
|
20599
|
+
);
|
|
20600
|
+
if (!raw) continue;
|
|
20601
|
+
if (raw.length > MAX_FINDING_BYTES) continue;
|
|
20602
|
+
const finding = await this.decode(id, raw);
|
|
20603
|
+
if (!finding) continue;
|
|
20604
|
+
if (opts?.since && finding.observed_at < opts.since) continue;
|
|
20605
|
+
if (opts?.severity && finding.severity !== opts.severity) continue;
|
|
20606
|
+
if (opts?.sentinelId && finding.sentinel_id !== opts.sentinelId) continue;
|
|
20607
|
+
if (opts?.agentId && finding.agent_id !== opts.agentId) continue;
|
|
20608
|
+
findings.push(finding);
|
|
20609
|
+
}
|
|
20610
|
+
findings.sort((a, b) => a.observed_at < b.observed_at ? 1 : -1);
|
|
20611
|
+
const limit = opts?.limit ?? 100;
|
|
20612
|
+
return findings.slice(0, limit);
|
|
20613
|
+
}
|
|
20614
|
+
/**
|
|
20615
|
+
* Drop expired findings. Returns the count removed.
|
|
20616
|
+
*/
|
|
20617
|
+
async pruneExpired(now) {
|
|
20618
|
+
const cutoff = (now ?? this.now()).toISOString();
|
|
20619
|
+
const metas = await this.storage.list(
|
|
20620
|
+
SENTINEL_FINDING_NAMESPACE,
|
|
20621
|
+
SENTINEL_FINDING_KEY_PREFIX
|
|
20622
|
+
);
|
|
20623
|
+
let pruned = 0;
|
|
20624
|
+
for (const meta of metas) {
|
|
20625
|
+
const id = stripKeyPrefix2(meta.key);
|
|
20626
|
+
if (id === null) continue;
|
|
20627
|
+
const raw = await this.storage.read(
|
|
20628
|
+
SENTINEL_FINDING_NAMESPACE,
|
|
20629
|
+
meta.key
|
|
20630
|
+
);
|
|
20631
|
+
if (!raw) continue;
|
|
20632
|
+
try {
|
|
20633
|
+
const aad = stringToBytes(id);
|
|
20634
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
20635
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
20636
|
+
const persisted = JSON.parse(
|
|
20637
|
+
bytesToString(plaintext)
|
|
20638
|
+
);
|
|
20639
|
+
if (persisted.retention_until <= cutoff) {
|
|
20640
|
+
await this.storage.delete(SENTINEL_FINDING_NAMESPACE, meta.key);
|
|
20641
|
+
pruned += 1;
|
|
20642
|
+
}
|
|
20643
|
+
} catch {
|
|
20644
|
+
}
|
|
20645
|
+
}
|
|
20646
|
+
return { pruned };
|
|
20647
|
+
}
|
|
20648
|
+
async decode(findingId, raw) {
|
|
20649
|
+
try {
|
|
20650
|
+
const aad = stringToBytes(findingId);
|
|
20651
|
+
const envelope = JSON.parse(bytesToString(raw));
|
|
20652
|
+
const plaintext = decrypt(envelope, this.encryptionKey, aad);
|
|
20653
|
+
const persisted = JSON.parse(
|
|
20654
|
+
bytesToString(plaintext)
|
|
20655
|
+
);
|
|
20656
|
+
if (persisted.version !== 1) return null;
|
|
20657
|
+
if (persisted.finding.finding_id !== findingId) return null;
|
|
20658
|
+
if (persisted.finding.fortress_id !== this.fortressId) return null;
|
|
20659
|
+
return persisted.finding;
|
|
20660
|
+
} catch {
|
|
20661
|
+
return null;
|
|
20662
|
+
}
|
|
20663
|
+
}
|
|
20664
|
+
};
|
|
20665
|
+
function findingKey(findingId) {
|
|
20666
|
+
return `${SENTINEL_FINDING_KEY_PREFIX}${findingId}`;
|
|
20667
|
+
}
|
|
20668
|
+
function stripKeyPrefix2(key) {
|
|
20669
|
+
if (!key.startsWith(SENTINEL_FINDING_KEY_PREFIX)) return null;
|
|
20670
|
+
return key.slice(SENTINEL_FINDING_KEY_PREFIX.length);
|
|
20671
|
+
}
|
|
20672
|
+
function truncateSummary(summary) {
|
|
20673
|
+
if (summary.length <= SENTINEL_SUMMARY_MAX_CHARS) return summary;
|
|
20674
|
+
return `${summary.slice(0, SENTINEL_SUMMARY_MAX_CHARS - 3)}...`;
|
|
20675
|
+
}
|
|
20676
|
+
|
|
20677
|
+
// src/sentinel/sentinel-registry.ts
|
|
20678
|
+
var SentinelRegistry = class {
|
|
20679
|
+
catalog = /* @__PURE__ */ new Map();
|
|
20680
|
+
subscribed = /* @__PURE__ */ new Map();
|
|
20681
|
+
register(entry) {
|
|
20682
|
+
if (this.catalog.has(entry.sentinelId)) {
|
|
20683
|
+
throw new Error(
|
|
20684
|
+
`sentinel-registry: ${entry.sentinelId} already registered`
|
|
20685
|
+
);
|
|
20686
|
+
}
|
|
20687
|
+
this.catalog.set(entry.sentinelId, entry);
|
|
20688
|
+
}
|
|
20689
|
+
/**
|
|
20690
|
+
* Available sentinels (catalog view). Operator UI lists this so the
|
|
20691
|
+
* operator can pick what to subscribe to.
|
|
20692
|
+
*/
|
|
20693
|
+
listCatalog() {
|
|
20694
|
+
return [...this.catalog.values()].map((entry) => ({
|
|
20695
|
+
sentinelId: entry.sentinelId,
|
|
20696
|
+
description: entry.description
|
|
20697
|
+
}));
|
|
20698
|
+
}
|
|
20699
|
+
/** Currently subscribed sentinel ids. */
|
|
20700
|
+
listSubscribed() {
|
|
20701
|
+
return [...this.subscribed.keys()];
|
|
20702
|
+
}
|
|
20703
|
+
/** Has the fortress opted into this sentinel? */
|
|
20704
|
+
isSubscribed(sentinelId) {
|
|
20705
|
+
return this.subscribed.has(sentinelId);
|
|
20706
|
+
}
|
|
20707
|
+
/**
|
|
20708
|
+
* Subscribe a sentinel to a fortress context. Idempotent: a second
|
|
20709
|
+
* subscribe call on an already-subscribed sentinel returns the
|
|
20710
|
+
* existing instance without re-running `subscribe()`.
|
|
20711
|
+
*/
|
|
20712
|
+
async subscribe(sentinelId, context) {
|
|
20713
|
+
const existing = this.subscribed.get(sentinelId);
|
|
20714
|
+
if (existing) return existing;
|
|
20715
|
+
const entry = this.catalog.get(sentinelId);
|
|
20716
|
+
if (!entry) {
|
|
20717
|
+
throw new Error(`sentinel-registry: unknown sentinel ${sentinelId}`);
|
|
20718
|
+
}
|
|
20719
|
+
const instance = entry.factory();
|
|
20720
|
+
await instance.subscribe(context);
|
|
20721
|
+
this.subscribed.set(sentinelId, instance);
|
|
20722
|
+
return instance;
|
|
20723
|
+
}
|
|
20724
|
+
/**
|
|
20725
|
+
* Unsubscribe. Idempotent: unsubscribing an unsubscribed sentinel
|
|
20726
|
+
* returns false without throwing. Returns true when an active
|
|
20727
|
+
* subscription was torn down.
|
|
20728
|
+
*/
|
|
20729
|
+
async unsubscribe(sentinelId) {
|
|
20730
|
+
const instance = this.subscribed.get(sentinelId);
|
|
20731
|
+
if (!instance) return false;
|
|
20732
|
+
try {
|
|
20733
|
+
await instance.unsubscribe();
|
|
20734
|
+
} finally {
|
|
20735
|
+
this.subscribed.delete(sentinelId);
|
|
20736
|
+
}
|
|
20737
|
+
return true;
|
|
20738
|
+
}
|
|
20739
|
+
/**
|
|
20740
|
+
* Snapshot of subscribed sentinels for the dispatcher's tick path.
|
|
20741
|
+
* Returned as an array so the dispatcher can iterate without holding
|
|
20742
|
+
* the map under modification.
|
|
20743
|
+
*/
|
|
20744
|
+
snapshotSubscribed() {
|
|
20745
|
+
return [...this.subscribed.entries()].map(([sentinelId, sentinel]) => ({
|
|
20746
|
+
sentinelId,
|
|
20747
|
+
sentinel
|
|
20748
|
+
}));
|
|
20749
|
+
}
|
|
20750
|
+
/**
|
|
20751
|
+
* Tear down every subscription. Called by the dispatcher on
|
|
20752
|
+
* fortress-shutdown. Best-effort: a failing unsubscribe does not
|
|
20753
|
+
* abort the rest.
|
|
20754
|
+
*/
|
|
20755
|
+
async unsubscribeAll() {
|
|
20756
|
+
const ids = [...this.subscribed.keys()];
|
|
20757
|
+
for (const id of ids) {
|
|
20758
|
+
try {
|
|
20759
|
+
await this.unsubscribe(id);
|
|
20760
|
+
} catch {
|
|
20761
|
+
}
|
|
20762
|
+
}
|
|
20763
|
+
}
|
|
20764
|
+
};
|
|
20765
|
+
var DEFAULT_TICK_INTERVAL_MS = 6e4;
|
|
20766
|
+
var SentinelDispatcher = class {
|
|
20767
|
+
registry;
|
|
20768
|
+
findingStore;
|
|
20769
|
+
auditLog;
|
|
20770
|
+
fortressId;
|
|
20771
|
+
identityId;
|
|
20772
|
+
now;
|
|
20773
|
+
tickIntervalMs;
|
|
20774
|
+
listeners = /* @__PURE__ */ new Set();
|
|
20775
|
+
tickTimer = null;
|
|
20776
|
+
tickInFlight = false;
|
|
20777
|
+
constructor(deps) {
|
|
20778
|
+
this.registry = deps.registry;
|
|
20779
|
+
this.findingStore = deps.findingStore;
|
|
20780
|
+
this.auditLog = deps.auditLog;
|
|
20781
|
+
this.fortressId = deps.fortressId;
|
|
20782
|
+
this.identityId = deps.identityId;
|
|
20783
|
+
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
20784
|
+
this.tickIntervalMs = deps.tickIntervalMs ?? DEFAULT_TICK_INTERVAL_MS;
|
|
20785
|
+
}
|
|
20786
|
+
/** Read-only view of the registry. Convenience for route handlers. */
|
|
20787
|
+
getRegistry() {
|
|
20788
|
+
return this.registry;
|
|
20789
|
+
}
|
|
20790
|
+
/** Read-only view of the finding store. Convenience for route handlers. */
|
|
20791
|
+
getFindingStore() {
|
|
20792
|
+
return this.findingStore;
|
|
20793
|
+
}
|
|
20794
|
+
/**
|
|
20795
|
+
* Subscribe an in-process listener. Returns an unsubscribe fn.
|
|
20796
|
+
*/
|
|
20797
|
+
onEvent(listener) {
|
|
20798
|
+
this.listeners.add(listener);
|
|
20799
|
+
return () => this.listeners.delete(listener);
|
|
20800
|
+
}
|
|
20801
|
+
/**
|
|
20802
|
+
* Subscribe a sentinel to this fortress + emit the
|
|
20803
|
+
* `sentinel_subscribed` audit event. Wraps `registry.subscribe()` so
|
|
20804
|
+
* the audit emission lives at the dispatcher boundary (the
|
|
20805
|
+
* fortress-aware site).
|
|
20806
|
+
*/
|
|
20807
|
+
async subscribeSentinel(sentinelId, contextOverrides) {
|
|
20808
|
+
const context = {
|
|
20809
|
+
fortressId: this.fortressId,
|
|
20810
|
+
auditLog: this.auditLog,
|
|
20811
|
+
now: this.now,
|
|
20812
|
+
...contextOverrides ?? {}
|
|
20813
|
+
};
|
|
20814
|
+
const sentinel = await this.registry.subscribe(sentinelId, context);
|
|
20815
|
+
this.auditLog.append(
|
|
20816
|
+
"l2",
|
|
20817
|
+
SENTINEL_AUDIT_OPS.SUBSCRIBED,
|
|
20818
|
+
this.identityId,
|
|
20819
|
+
{ sentinel_id: sentinelId, fortress_id: this.fortressId }
|
|
20820
|
+
);
|
|
20821
|
+
return sentinel;
|
|
20822
|
+
}
|
|
20823
|
+
/**
|
|
20824
|
+
* Unsubscribe + emit `sentinel_unsubscribed`. Returns true when an
|
|
20825
|
+
* active subscription was torn down. Audit fires only on successful
|
|
20826
|
+
* removal.
|
|
20827
|
+
*/
|
|
20828
|
+
async unsubscribeSentinel(sentinelId) {
|
|
20829
|
+
const removed = await this.registry.unsubscribe(sentinelId);
|
|
20830
|
+
if (removed) {
|
|
20831
|
+
this.auditLog.append(
|
|
20832
|
+
"l2",
|
|
20833
|
+
SENTINEL_AUDIT_OPS.UNSUBSCRIBED,
|
|
20834
|
+
this.identityId,
|
|
20835
|
+
{ sentinel_id: sentinelId, fortress_id: this.fortressId }
|
|
20836
|
+
);
|
|
20837
|
+
}
|
|
20838
|
+
return removed;
|
|
20839
|
+
}
|
|
20840
|
+
/**
|
|
20841
|
+
* Run one evaluation pass over every subscribed sentinel. Used by
|
|
20842
|
+
* the auto-tick AND by tests that want a synchronous evaluation
|
|
20843
|
+
* gate. Returns the findings produced this tick (already persisted
|
|
20844
|
+
* + audit-logged + emitted).
|
|
20845
|
+
*/
|
|
20846
|
+
async tick() {
|
|
20847
|
+
if (this.tickInFlight) return [];
|
|
20848
|
+
this.tickInFlight = true;
|
|
20849
|
+
try {
|
|
20850
|
+
const subscribed = this.registry.snapshotSubscribed();
|
|
20851
|
+
const findings = [];
|
|
20852
|
+
for (const { sentinelId, sentinel } of subscribed) {
|
|
20853
|
+
try {
|
|
20854
|
+
const tickFindings = await sentinel.evaluate();
|
|
20855
|
+
for (const finding of tickFindings) {
|
|
20856
|
+
const stamped = await this.routeFinding(sentinelId, finding);
|
|
20857
|
+
findings.push(stamped);
|
|
20858
|
+
}
|
|
20859
|
+
} catch (err) {
|
|
20860
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
20861
|
+
const observedAt = this.now().toISOString();
|
|
20862
|
+
this.auditLog.append(
|
|
20863
|
+
"l2",
|
|
20864
|
+
SENTINEL_AUDIT_OPS.EVALUATION_FAILED,
|
|
20865
|
+
this.identityId,
|
|
20866
|
+
{
|
|
20867
|
+
sentinel_id: sentinelId,
|
|
20868
|
+
fortress_id: this.fortressId,
|
|
20869
|
+
error_message: errorMessage
|
|
20870
|
+
},
|
|
20871
|
+
"failure"
|
|
20872
|
+
);
|
|
20873
|
+
this.emit({
|
|
20874
|
+
type: "evaluation_failed",
|
|
20875
|
+
sentinel_id: sentinelId,
|
|
20876
|
+
error_message: errorMessage,
|
|
20877
|
+
observed_at: observedAt
|
|
20878
|
+
});
|
|
20879
|
+
}
|
|
20880
|
+
}
|
|
20881
|
+
return findings;
|
|
20882
|
+
} finally {
|
|
20883
|
+
this.tickInFlight = false;
|
|
20884
|
+
}
|
|
20885
|
+
}
|
|
20886
|
+
/**
|
|
20887
|
+
* Start the auto-tick loop. No-op when tickIntervalMs is 0 or when
|
|
20888
|
+
* already started. Tests typically leave auto-tick off and call
|
|
20889
|
+
* `tick()` directly.
|
|
20890
|
+
*/
|
|
20891
|
+
start() {
|
|
20892
|
+
if (this.tickTimer !== null) return;
|
|
20893
|
+
if (this.tickIntervalMs <= 0) return;
|
|
20894
|
+
this.tickTimer = setInterval(() => {
|
|
20895
|
+
void this.tick();
|
|
20896
|
+
}, this.tickIntervalMs);
|
|
20897
|
+
if (typeof this.tickTimer.unref === "function") {
|
|
20898
|
+
this.tickTimer.unref();
|
|
20899
|
+
}
|
|
20900
|
+
}
|
|
20901
|
+
/** Stop the auto-tick loop. Idempotent. */
|
|
20902
|
+
stop() {
|
|
20903
|
+
if (this.tickTimer === null) return;
|
|
20904
|
+
clearInterval(this.tickTimer);
|
|
20905
|
+
this.tickTimer = null;
|
|
20906
|
+
}
|
|
20907
|
+
/**
|
|
20908
|
+
* Tear down every subscription + stop the tick loop. Called on
|
|
20909
|
+
* fortress shutdown.
|
|
20910
|
+
*/
|
|
20911
|
+
async dispose() {
|
|
20912
|
+
this.stop();
|
|
20913
|
+
await this.registry.unsubscribeAll();
|
|
20914
|
+
this.listeners.clear();
|
|
20915
|
+
}
|
|
20916
|
+
async routeFinding(sentinelId, raw) {
|
|
20917
|
+
const stamped = {
|
|
20918
|
+
...raw,
|
|
20919
|
+
finding_id: raw.finding_id || crypto.randomUUID(),
|
|
20920
|
+
sentinel_id: sentinelId,
|
|
20921
|
+
fortress_id: this.fortressId,
|
|
20922
|
+
observed_at: raw.observed_at || this.now().toISOString()
|
|
20923
|
+
};
|
|
20924
|
+
await this.findingStore.saveFinding(stamped);
|
|
20925
|
+
this.auditLog.append(
|
|
20926
|
+
"l2",
|
|
20927
|
+
SENTINEL_AUDIT_OPS.FINDING_EMITTED,
|
|
20928
|
+
this.identityId,
|
|
20929
|
+
{
|
|
20930
|
+
sentinel_id: sentinelId,
|
|
20931
|
+
finding_id: stamped.finding_id,
|
|
20932
|
+
severity: stamped.severity,
|
|
20933
|
+
...stamped.agent_id !== void 0 ? { agent_id: stamped.agent_id } : {},
|
|
20934
|
+
evidence_audit_ids: stamped.evidence_audit_ids,
|
|
20935
|
+
fortress_id: this.fortressId
|
|
20936
|
+
}
|
|
20937
|
+
);
|
|
20938
|
+
this.emit({ type: "finding", finding: stamped });
|
|
20939
|
+
return stamped;
|
|
20940
|
+
}
|
|
20941
|
+
emit(event) {
|
|
20942
|
+
for (const listener of this.listeners) {
|
|
20943
|
+
try {
|
|
20944
|
+
listener(event);
|
|
20945
|
+
} catch {
|
|
20946
|
+
}
|
|
20947
|
+
}
|
|
20948
|
+
}
|
|
20949
|
+
};
|
|
20950
|
+
|
|
20951
|
+
// src/sentinel/sentinel.ts
|
|
20952
|
+
var Sentinel = class {
|
|
20953
|
+
/**
|
|
20954
|
+
* Bind the sentinel to a fortress context. Called once on
|
|
20955
|
+
* subscribe. Default implementation stores the context on `this`;
|
|
20956
|
+
* sentinels that need additional setup (e.g. priming a baseline
|
|
20957
|
+
* cache) override.
|
|
20958
|
+
*/
|
|
20959
|
+
async subscribe(context) {
|
|
20960
|
+
this.context = context;
|
|
20961
|
+
}
|
|
20962
|
+
/**
|
|
20963
|
+
* Tear down. Default implementation clears the context; subclasses
|
|
20964
|
+
* that hold timers or external handles override.
|
|
20965
|
+
*/
|
|
20966
|
+
async unsubscribe() {
|
|
20967
|
+
this.context = void 0;
|
|
20968
|
+
}
|
|
20969
|
+
context;
|
|
20970
|
+
/** Internal helper: assert subscribed before evaluation. */
|
|
20971
|
+
requireContext() {
|
|
20972
|
+
if (!this.context) {
|
|
20973
|
+
throw new Error(
|
|
20974
|
+
`sentinel ${this.sentinelId}: evaluate() called before subscribe()`
|
|
20975
|
+
);
|
|
20976
|
+
}
|
|
20977
|
+
return this.context;
|
|
20978
|
+
}
|
|
20979
|
+
};
|
|
20980
|
+
|
|
20981
|
+
// src/sentinel/sentinels/egress-volume-watcher.ts
|
|
20982
|
+
var EGRESS_VOLUME_SENTINEL_ID = "egress-volume";
|
|
20983
|
+
var WARN_SIGMA = 3;
|
|
20984
|
+
var ALERT_SIGMA = 6;
|
|
20985
|
+
var BASELINE_WINDOWS = 7;
|
|
20986
|
+
var QUERY_LIMIT = 1e4;
|
|
20987
|
+
var EgressVolumeWatcher = class extends Sentinel {
|
|
20988
|
+
sentinelId = EGRESS_VOLUME_SENTINEL_ID;
|
|
20989
|
+
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.";
|
|
20990
|
+
/** Servers we have already produced an `info` baseline-established finding for. */
|
|
20991
|
+
baselineEstablished = /* @__PURE__ */ new Set();
|
|
20992
|
+
async evaluate() {
|
|
20993
|
+
const ctx = this.requireContext();
|
|
20994
|
+
const now = ctx.now();
|
|
20995
|
+
const windowMs = 24 * 60 * 60 * 1e3;
|
|
20996
|
+
const windowSpanMs = (BASELINE_WINDOWS + 1) * windowMs;
|
|
20997
|
+
const sinceIso = new Date(now.getTime() - windowSpanMs).toISOString();
|
|
20998
|
+
const queryResult = await ctx.auditLog.query({
|
|
20999
|
+
since: sinceIso,
|
|
21000
|
+
layer: "l2",
|
|
21001
|
+
limit: QUERY_LIMIT
|
|
21002
|
+
});
|
|
21003
|
+
const entries = queryResult.entries.filter(isProxyCallAuditEntry);
|
|
21004
|
+
const byServer = /* @__PURE__ */ new Map();
|
|
21005
|
+
for (const entry of entries) {
|
|
21006
|
+
const server = proxyServerFromAuditEntry(entry);
|
|
21007
|
+
if (server === null) continue;
|
|
21008
|
+
const auditAge = now.getTime() - new Date(entry.timestamp).getTime();
|
|
21009
|
+
if (auditAge < 0) continue;
|
|
21010
|
+
const windowIdx = Math.floor(auditAge / windowMs);
|
|
21011
|
+
if (windowIdx > BASELINE_WINDOWS) continue;
|
|
21012
|
+
let snapshot = byServer.get(server);
|
|
21013
|
+
if (!snapshot) {
|
|
21014
|
+
snapshot = { windows: [] };
|
|
21015
|
+
for (let i = 0; i <= BASELINE_WINDOWS; i += 1) {
|
|
21016
|
+
snapshot.windows.push({ count: 0, evidence_audit_ids: [] });
|
|
21017
|
+
}
|
|
21018
|
+
byServer.set(server, snapshot);
|
|
21019
|
+
}
|
|
21020
|
+
const bucket = snapshot.windows[windowIdx];
|
|
21021
|
+
bucket.count += 1;
|
|
21022
|
+
if (windowIdx === 0 && bucket.evidence_audit_ids.length < 50) {
|
|
21023
|
+
bucket.evidence_audit_ids.push(`${entry.timestamp}:${entry.operation}`);
|
|
21024
|
+
}
|
|
21025
|
+
}
|
|
21026
|
+
const findings = [];
|
|
21027
|
+
for (const [server, snapshot] of byServer.entries()) {
|
|
21028
|
+
const finding = this.evaluateServer(server, snapshot, now);
|
|
21029
|
+
if (finding) findings.push(finding);
|
|
21030
|
+
}
|
|
21031
|
+
return findings;
|
|
21032
|
+
}
|
|
21033
|
+
/** Reset baseline-established memoization. Tests use this between runs. */
|
|
21034
|
+
resetBaselineMemo() {
|
|
21035
|
+
this.baselineEstablished.clear();
|
|
21036
|
+
}
|
|
21037
|
+
evaluateServer(server, snapshot, now) {
|
|
21038
|
+
const currentWindow = snapshot.windows[0];
|
|
21039
|
+
const baselineWindows = snapshot.windows.slice(1);
|
|
21040
|
+
const populatedBaselineWindows = baselineWindows.filter((w) => w.count > 0).length;
|
|
21041
|
+
if (populatedBaselineWindows < BASELINE_WINDOWS) {
|
|
21042
|
+
if (this.baselineEstablished.has(server)) return null;
|
|
21043
|
+
if (populatedBaselineWindows === 0 && currentWindow.count === 0) {
|
|
21044
|
+
return null;
|
|
21045
|
+
}
|
|
21046
|
+
return null;
|
|
21047
|
+
}
|
|
21048
|
+
const baselineCounts = baselineWindows.map((w) => w.count);
|
|
21049
|
+
const mean = baselineCounts.reduce((sum, c) => sum + c, 0) / baselineCounts.length;
|
|
21050
|
+
const variance = baselineCounts.reduce((sum, c) => sum + (c - mean) ** 2, 0) / baselineCounts.length;
|
|
21051
|
+
const stddev = Math.sqrt(variance);
|
|
21052
|
+
const wasEstablished = this.baselineEstablished.has(server);
|
|
21053
|
+
this.baselineEstablished.add(server);
|
|
21054
|
+
if (!wasEstablished) {
|
|
21055
|
+
return {
|
|
21056
|
+
finding_id: "",
|
|
21057
|
+
sentinel_id: this.sentinelId,
|
|
21058
|
+
severity: "info",
|
|
21059
|
+
summary: `egress-volume baseline established for ${server}: mean ${mean.toFixed(1)} calls/24h, stddev ${stddev.toFixed(1)} (over ${BASELINE_WINDOWS} prior days).`,
|
|
21060
|
+
details: {
|
|
21061
|
+
server,
|
|
21062
|
+
baseline_mean: mean,
|
|
21063
|
+
baseline_stddev: stddev,
|
|
21064
|
+
baseline_windows: baselineCounts,
|
|
21065
|
+
current_count: currentWindow.count
|
|
21066
|
+
},
|
|
21067
|
+
observed_at: now.toISOString(),
|
|
21068
|
+
evidence_audit_ids: [],
|
|
21069
|
+
fortress_id: ""
|
|
21070
|
+
};
|
|
21071
|
+
}
|
|
21072
|
+
const warnThreshold = mean + WARN_SIGMA * stddev;
|
|
21073
|
+
const alertThreshold = mean + ALERT_SIGMA * stddev;
|
|
21074
|
+
if (currentWindow.count > alertThreshold) {
|
|
21075
|
+
return this.buildAnomalyFinding(
|
|
21076
|
+
server,
|
|
21077
|
+
snapshot,
|
|
21078
|
+
mean,
|
|
21079
|
+
stddev,
|
|
21080
|
+
now,
|
|
21081
|
+
"alert",
|
|
21082
|
+
ALERT_SIGMA
|
|
21083
|
+
);
|
|
21084
|
+
}
|
|
21085
|
+
if (currentWindow.count > warnThreshold) {
|
|
21086
|
+
return this.buildAnomalyFinding(
|
|
21087
|
+
server,
|
|
21088
|
+
snapshot,
|
|
21089
|
+
mean,
|
|
21090
|
+
stddev,
|
|
21091
|
+
now,
|
|
21092
|
+
"warn",
|
|
21093
|
+
WARN_SIGMA
|
|
21094
|
+
);
|
|
21095
|
+
}
|
|
21096
|
+
return null;
|
|
21097
|
+
}
|
|
21098
|
+
buildAnomalyFinding(server, snapshot, mean, stddev, now, severity, sigma) {
|
|
21099
|
+
const currentWindow = snapshot.windows[0];
|
|
21100
|
+
const ratio = mean === 0 ? Infinity : currentWindow.count / mean;
|
|
21101
|
+
const ratioStr = Number.isFinite(ratio) ? `${ratio.toFixed(1)}x normal` : "no prior baseline";
|
|
21102
|
+
const summary = `${server} egress is ${ratioStr}: ${currentWindow.count} calls in last 24h, baseline ${mean.toFixed(1)} (stddev ${stddev.toFixed(1)}). Crossed +${sigma} sigma threshold.`;
|
|
21103
|
+
return {
|
|
21104
|
+
finding_id: "",
|
|
21105
|
+
sentinel_id: this.sentinelId,
|
|
21106
|
+
severity,
|
|
21107
|
+
summary,
|
|
21108
|
+
details: {
|
|
21109
|
+
server,
|
|
21110
|
+
current_count: currentWindow.count,
|
|
21111
|
+
baseline_mean: mean,
|
|
21112
|
+
baseline_stddev: stddev,
|
|
21113
|
+
sigma_threshold: sigma,
|
|
21114
|
+
ratio
|
|
21115
|
+
},
|
|
21116
|
+
observed_at: now.toISOString(),
|
|
21117
|
+
evidence_audit_ids: currentWindow.evidence_audit_ids,
|
|
21118
|
+
fortress_id: ""
|
|
21119
|
+
};
|
|
21120
|
+
}
|
|
21121
|
+
};
|
|
21122
|
+
|
|
21123
|
+
// src/sentinel/sentinels/cross-agent-chatter-watcher.ts
|
|
21124
|
+
var CROSS_AGENT_CHATTER_SENTINEL_ID = "cross-agent-chatter";
|
|
21125
|
+
var WARN_SIGMA2 = 3;
|
|
21126
|
+
var ALERT_SIGMA2 = 6;
|
|
21127
|
+
var BASELINE_WINDOWS2 = 7;
|
|
21128
|
+
var QUERY_LIMIT2 = 1e4;
|
|
21129
|
+
var MULTI_NEW_PARTNER_ALERT_THRESHOLD = 3;
|
|
21130
|
+
var OPERATOR_PSEUDO_AGENT = "operator";
|
|
21131
|
+
var HANDOFF_OP = "v1.1_local_handoff";
|
|
21132
|
+
var CROSS_HARNESS_OPS = /* @__PURE__ */ new Set([
|
|
21133
|
+
"cross_harness_approval_aggregated",
|
|
21134
|
+
"cross_harness_approval_resolved"
|
|
21135
|
+
]);
|
|
21136
|
+
function pairKey(sender, recipient) {
|
|
21137
|
+
return `${sender}|${recipient}`;
|
|
21138
|
+
}
|
|
21139
|
+
function pairFromKey(key) {
|
|
21140
|
+
const idx = key.indexOf("|");
|
|
21141
|
+
return { sender: key.slice(0, idx), recipient: key.slice(idx + 1) };
|
|
21142
|
+
}
|
|
21143
|
+
var CrossAgentChatterWatcher = class extends Sentinel {
|
|
21144
|
+
sentinelId = CROSS_AGENT_CHATTER_SENTINEL_ID;
|
|
21145
|
+
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).";
|
|
21146
|
+
/** Pair keys we have already produced a baseline-established info finding for. */
|
|
21147
|
+
baselineEstablished = /* @__PURE__ */ new Set();
|
|
21148
|
+
async evaluate() {
|
|
21149
|
+
const ctx = this.requireContext();
|
|
21150
|
+
const now = ctx.now();
|
|
21151
|
+
const windowMs = 24 * 60 * 60 * 1e3;
|
|
21152
|
+
const windowSpanMs = (BASELINE_WINDOWS2 + 1) * windowMs;
|
|
21153
|
+
const sinceIso = new Date(now.getTime() - windowSpanMs).toISOString();
|
|
21154
|
+
const queryResult = await ctx.auditLog.query({
|
|
21155
|
+
since: sinceIso,
|
|
21156
|
+
layer: "l2",
|
|
21157
|
+
limit: QUERY_LIMIT2
|
|
21158
|
+
});
|
|
21159
|
+
const events = extractInterAgentEvents(queryResult.entries);
|
|
21160
|
+
const byPair = /* @__PURE__ */ new Map();
|
|
21161
|
+
for (const event of events) {
|
|
21162
|
+
const auditAgeMs = now.getTime() - event.timestampMs;
|
|
21163
|
+
if (auditAgeMs < 0) continue;
|
|
21164
|
+
const windowIdx = Math.floor(auditAgeMs / windowMs);
|
|
21165
|
+
if (windowIdx > BASELINE_WINDOWS2) continue;
|
|
21166
|
+
const key = pairKey(event.sender, event.recipient);
|
|
21167
|
+
let snap = byPair.get(key);
|
|
21168
|
+
if (!snap) {
|
|
21169
|
+
snap = { windows: [] };
|
|
21170
|
+
for (let i = 0; i <= BASELINE_WINDOWS2; i += 1) {
|
|
21171
|
+
snap.windows.push({ count: 0, evidence_audit_ids: [] });
|
|
21172
|
+
}
|
|
21173
|
+
byPair.set(key, snap);
|
|
21174
|
+
}
|
|
21175
|
+
const bucket = snap.windows[windowIdx];
|
|
21176
|
+
bucket.count += 1;
|
|
21177
|
+
if (windowIdx === 0 && bucket.evidence_audit_ids.length < 50) {
|
|
21178
|
+
bucket.evidence_audit_ids.push(event.auditId);
|
|
21179
|
+
}
|
|
21180
|
+
}
|
|
21181
|
+
const findings = [];
|
|
21182
|
+
for (const [key, snap] of byPair.entries()) {
|
|
21183
|
+
const finding = this.evaluatePair(key, snap, now);
|
|
21184
|
+
if (finding) findings.push(finding);
|
|
21185
|
+
}
|
|
21186
|
+
const newPartnersBySource = computeNewPartners(byPair);
|
|
21187
|
+
for (const [source, partners] of newPartnersBySource.entries()) {
|
|
21188
|
+
const finding = this.buildNewPartnerFinding(source, partners, now);
|
|
21189
|
+
if (finding) findings.push(finding);
|
|
21190
|
+
}
|
|
21191
|
+
return findings;
|
|
21192
|
+
}
|
|
21193
|
+
/** Reset baseline-established memoization. Tests use this between runs. */
|
|
21194
|
+
resetBaselineMemo() {
|
|
21195
|
+
this.baselineEstablished.clear();
|
|
21196
|
+
}
|
|
21197
|
+
evaluatePair(key, snap, now) {
|
|
21198
|
+
const currentWindow = snap.windows[0];
|
|
21199
|
+
const baselineWindows = snap.windows.slice(1);
|
|
21200
|
+
const populated = baselineWindows.filter((w) => w.count > 0).length;
|
|
21201
|
+
if (populated < BASELINE_WINDOWS2) {
|
|
21202
|
+
return null;
|
|
21203
|
+
}
|
|
21204
|
+
const counts = baselineWindows.map((w) => w.count);
|
|
21205
|
+
const mean = counts.reduce((s, c) => s + c, 0) / counts.length;
|
|
21206
|
+
const variance = counts.reduce((s, c) => s + (c - mean) ** 2, 0) / counts.length;
|
|
21207
|
+
const stddev = Math.sqrt(variance);
|
|
21208
|
+
const wasEstablished = this.baselineEstablished.has(key);
|
|
21209
|
+
this.baselineEstablished.add(key);
|
|
21210
|
+
if (!wasEstablished) {
|
|
21211
|
+
const pair = pairFromKey(key);
|
|
21212
|
+
return {
|
|
21213
|
+
finding_id: "",
|
|
21214
|
+
sentinel_id: this.sentinelId,
|
|
21215
|
+
severity: "info",
|
|
21216
|
+
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).`,
|
|
21217
|
+
details: {
|
|
21218
|
+
sender_agent_id: pair.sender,
|
|
21219
|
+
recipient_agent_id: pair.recipient,
|
|
21220
|
+
baseline_mean: mean,
|
|
21221
|
+
baseline_stddev: stddev,
|
|
21222
|
+
baseline_windows: counts,
|
|
21223
|
+
current_count: currentWindow.count
|
|
21224
|
+
},
|
|
21225
|
+
observed_at: now.toISOString(),
|
|
21226
|
+
evidence_audit_ids: [],
|
|
21227
|
+
fortress_id: ""
|
|
21228
|
+
};
|
|
21229
|
+
}
|
|
21230
|
+
const warnThreshold = mean + WARN_SIGMA2 * stddev;
|
|
21231
|
+
const alertThreshold = mean + ALERT_SIGMA2 * stddev;
|
|
21232
|
+
if (currentWindow.count > alertThreshold) {
|
|
21233
|
+
return this.buildRateSpike(key, snap, mean, stddev, now, "alert", ALERT_SIGMA2);
|
|
21234
|
+
}
|
|
21235
|
+
if (currentWindow.count > warnThreshold) {
|
|
21236
|
+
return this.buildRateSpike(key, snap, mean, stddev, now, "warn", WARN_SIGMA2);
|
|
21237
|
+
}
|
|
21238
|
+
return null;
|
|
21239
|
+
}
|
|
21240
|
+
buildRateSpike(key, snap, mean, stddev, now, severity, sigma) {
|
|
21241
|
+
const pair = pairFromKey(key);
|
|
21242
|
+
const cur = snap.windows[0];
|
|
21243
|
+
const ratio = mean === 0 ? Infinity : cur.count / mean;
|
|
21244
|
+
const ratioStr = Number.isFinite(ratio) ? `${ratio.toFixed(1)}x normal` : "no prior baseline";
|
|
21245
|
+
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.`;
|
|
21246
|
+
return {
|
|
21247
|
+
finding_id: "",
|
|
21248
|
+
sentinel_id: this.sentinelId,
|
|
21249
|
+
severity,
|
|
21250
|
+
summary,
|
|
21251
|
+
details: {
|
|
21252
|
+
sender_agent_id: pair.sender,
|
|
21253
|
+
recipient_agent_id: pair.recipient,
|
|
21254
|
+
current_count: cur.count,
|
|
21255
|
+
baseline_mean: mean,
|
|
21256
|
+
baseline_stddev: stddev,
|
|
21257
|
+
sigma_threshold: sigma,
|
|
21258
|
+
ratio
|
|
21259
|
+
},
|
|
21260
|
+
observed_at: now.toISOString(),
|
|
21261
|
+
agent_id: pair.sender,
|
|
21262
|
+
evidence_audit_ids: cur.evidence_audit_ids,
|
|
21263
|
+
fortress_id: ""
|
|
21264
|
+
};
|
|
21265
|
+
}
|
|
21266
|
+
buildNewPartnerFinding(source, info, now) {
|
|
21267
|
+
if (info.partners.length === 0) return null;
|
|
21268
|
+
const severity = info.partners.length >= MULTI_NEW_PARTNER_ALERT_THRESHOLD ? "alert" : "warn";
|
|
21269
|
+
const partnerList = info.partners.join(", ");
|
|
21270
|
+
const baselinePartnerList = info.priorPartners.length === 0 ? "no prior partners" : info.priorPartners.join(", ");
|
|
21271
|
+
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}.`;
|
|
21272
|
+
return {
|
|
21273
|
+
finding_id: "",
|
|
21274
|
+
sentinel_id: this.sentinelId,
|
|
21275
|
+
severity,
|
|
21276
|
+
summary,
|
|
21277
|
+
details: {
|
|
21278
|
+
sender_agent_id: source,
|
|
21279
|
+
new_partners: info.partners,
|
|
21280
|
+
prior_partners: info.priorPartners,
|
|
21281
|
+
new_partner_count: info.partners.length,
|
|
21282
|
+
multi_new_partner_threshold: MULTI_NEW_PARTNER_ALERT_THRESHOLD
|
|
21283
|
+
},
|
|
21284
|
+
observed_at: now.toISOString(),
|
|
21285
|
+
agent_id: source,
|
|
21286
|
+
evidence_audit_ids: info.evidenceAuditIds,
|
|
21287
|
+
fortress_id: ""
|
|
21288
|
+
};
|
|
21289
|
+
}
|
|
21290
|
+
};
|
|
21291
|
+
function computeNewPartners(byPair) {
|
|
21292
|
+
const currentBySource = /* @__PURE__ */ new Map();
|
|
21293
|
+
const priorBySource = /* @__PURE__ */ new Map();
|
|
21294
|
+
for (const [key, snap] of byPair.entries()) {
|
|
21295
|
+
const { sender, recipient } = pairFromKey(key);
|
|
21296
|
+
if (snap.windows[0] && snap.windows[0].count > 0) {
|
|
21297
|
+
let recipMap = currentBySource.get(sender);
|
|
21298
|
+
if (!recipMap) {
|
|
21299
|
+
recipMap = /* @__PURE__ */ new Map();
|
|
21300
|
+
currentBySource.set(sender, recipMap);
|
|
21301
|
+
}
|
|
21302
|
+
recipMap.set(recipient, snap.windows[0].evidence_audit_ids);
|
|
21303
|
+
}
|
|
21304
|
+
const priorTouched = snap.windows.slice(1).some((w) => w.count > 0);
|
|
21305
|
+
if (priorTouched) {
|
|
21306
|
+
let set = priorBySource.get(sender);
|
|
21307
|
+
if (!set) {
|
|
21308
|
+
set = /* @__PURE__ */ new Set();
|
|
21309
|
+
priorBySource.set(sender, set);
|
|
21310
|
+
}
|
|
21311
|
+
set.add(recipient);
|
|
21312
|
+
}
|
|
21313
|
+
}
|
|
21314
|
+
const out = /* @__PURE__ */ new Map();
|
|
21315
|
+
for (const [sender, recipMap] of currentBySource.entries()) {
|
|
21316
|
+
const prior = priorBySource.get(sender) ?? /* @__PURE__ */ new Set();
|
|
21317
|
+
if (prior.size === 0) {
|
|
21318
|
+
continue;
|
|
21319
|
+
}
|
|
21320
|
+
const newPartners = [];
|
|
21321
|
+
const evidence = [];
|
|
21322
|
+
for (const [recipient, recipEvidence] of recipMap.entries()) {
|
|
21323
|
+
if (!prior.has(recipient)) {
|
|
21324
|
+
newPartners.push(recipient);
|
|
21325
|
+
for (const id of recipEvidence) {
|
|
21326
|
+
if (evidence.length < 50) evidence.push(id);
|
|
21327
|
+
}
|
|
21328
|
+
}
|
|
21329
|
+
}
|
|
21330
|
+
if (newPartners.length === 0) continue;
|
|
21331
|
+
newPartners.sort();
|
|
21332
|
+
out.set(sender, {
|
|
21333
|
+
partners: newPartners,
|
|
21334
|
+
priorPartners: [...prior].sort(),
|
|
21335
|
+
evidenceAuditIds: evidence
|
|
21336
|
+
});
|
|
21337
|
+
}
|
|
21338
|
+
return out;
|
|
21339
|
+
}
|
|
21340
|
+
function extractInterAgentEvents(entries) {
|
|
21341
|
+
const out = [];
|
|
21342
|
+
for (const entry of entries) {
|
|
21343
|
+
const op = entry.operation;
|
|
21344
|
+
if (op === HANDOFF_OP) {
|
|
21345
|
+
const details = entry.details;
|
|
21346
|
+
const sender = optionalString(details, "sender_agent_id");
|
|
21347
|
+
const recipient = optionalString(details, "recipient_agent_id");
|
|
21348
|
+
if (!sender || !recipient || sender === recipient) continue;
|
|
21349
|
+
out.push({
|
|
21350
|
+
sender,
|
|
21351
|
+
recipient,
|
|
21352
|
+
timestampMs: Date.parse(entry.timestamp),
|
|
21353
|
+
auditId: `${entry.timestamp}:${entry.operation}`
|
|
21354
|
+
});
|
|
21355
|
+
continue;
|
|
21356
|
+
}
|
|
21357
|
+
if (CROSS_HARNESS_OPS.has(op)) {
|
|
21358
|
+
const details = entry.details;
|
|
21359
|
+
const sender = optionalString(details, "source_harness") ?? optionalString(details, "source_agent_id");
|
|
21360
|
+
if (!sender) continue;
|
|
21361
|
+
out.push({
|
|
21362
|
+
sender,
|
|
21363
|
+
recipient: OPERATOR_PSEUDO_AGENT,
|
|
21364
|
+
timestampMs: Date.parse(entry.timestamp),
|
|
21365
|
+
auditId: `${entry.timestamp}:${entry.operation}`
|
|
21366
|
+
});
|
|
21367
|
+
}
|
|
21368
|
+
}
|
|
21369
|
+
return out;
|
|
21370
|
+
}
|
|
21371
|
+
function optionalString(details, key) {
|
|
21372
|
+
if (!details) return null;
|
|
21373
|
+
const value = details[key];
|
|
21374
|
+
if (typeof value !== "string" || value.length === 0) return null;
|
|
21375
|
+
return value;
|
|
21376
|
+
}
|
|
21377
|
+
|
|
21378
|
+
// src/sentinel/sentinels/credential-usage-watcher.ts
|
|
21379
|
+
var CREDENTIAL_USAGE_SENTINEL_ID = "credential-usage";
|
|
21380
|
+
var WARN_SIGMA3 = 3;
|
|
21381
|
+
var ALERT_SIGMA3 = 6;
|
|
21382
|
+
var NEW_PAIR_ALERT_COUNT = 3;
|
|
21383
|
+
var BASELINE_WINDOWS3 = 7;
|
|
21384
|
+
var QUERY_LIMIT3 = 2e4;
|
|
21385
|
+
var BROKER_SECRET_READ_OP = "broker_secret_read";
|
|
21386
|
+
var BROKER_TOKEN_ISSUED_OP = "broker_token_issued";
|
|
21387
|
+
var CredentialUsageWatcher = class extends Sentinel {
|
|
21388
|
+
sentinelId = CREDENTIAL_USAGE_SENTINEL_ID;
|
|
21389
|
+
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.";
|
|
21390
|
+
/**
|
|
21391
|
+
* Memoization of (agent, secret) pairs whose baseline has been
|
|
21392
|
+
* established. Same shape Phi-1 uses to avoid re-emitting `info`
|
|
21393
|
+
* findings on every tick after a baseline first establishes.
|
|
21394
|
+
*
|
|
21395
|
+
* Phi-2 deliberately does NOT emit `info` findings: per-pair
|
|
21396
|
+
* baselines on a busy fortress would be too noisy. The memo is
|
|
21397
|
+
* kept here for parity with Phi-1's reset hook so tests can clear
|
|
21398
|
+
* state between runs.
|
|
21399
|
+
*/
|
|
21400
|
+
baselineEstablished = /* @__PURE__ */ new Set();
|
|
21401
|
+
/** Reset memoization. Tests use this between runs. */
|
|
21402
|
+
resetBaselineMemo() {
|
|
21403
|
+
this.baselineEstablished.clear();
|
|
21404
|
+
}
|
|
21405
|
+
async evaluate() {
|
|
21406
|
+
const ctx = this.requireContext();
|
|
21407
|
+
const now = ctx.now();
|
|
21408
|
+
const windowMs = 24 * 60 * 60 * 1e3;
|
|
21409
|
+
const windowSpanMs = (BASELINE_WINDOWS3 + 1) * windowMs;
|
|
21410
|
+
const sinceIso = new Date(now.getTime() - windowSpanMs).toISOString();
|
|
21411
|
+
const queryResult = await ctx.auditLog.query({
|
|
21412
|
+
since: sinceIso,
|
|
21413
|
+
layer: "l3",
|
|
21414
|
+
limit: QUERY_LIMIT3
|
|
21415
|
+
});
|
|
21416
|
+
const entries = queryResult.entries.filter(isCredentialAuditEntry);
|
|
21417
|
+
const byPair = /* @__PURE__ */ new Map();
|
|
21418
|
+
const byAgent = /* @__PURE__ */ new Map();
|
|
21419
|
+
for (const entry of entries) {
|
|
21420
|
+
const agentId = extractAgentId(entry);
|
|
21421
|
+
const secretId = extractSecretId(entry);
|
|
21422
|
+
if (agentId === null || secretId === null) continue;
|
|
21423
|
+
const auditAge = now.getTime() - new Date(entry.timestamp).getTime();
|
|
21424
|
+
if (auditAge < 0) continue;
|
|
21425
|
+
const windowIdx = Math.floor(auditAge / windowMs);
|
|
21426
|
+
if (windowIdx > BASELINE_WINDOWS3) continue;
|
|
21427
|
+
const pairKey2 = `${agentId}\0${secretId}`;
|
|
21428
|
+
let pairSnapshot = byPair.get(pairKey2);
|
|
21429
|
+
if (!pairSnapshot) {
|
|
21430
|
+
pairSnapshot = {
|
|
21431
|
+
windows: Array.from({ length: BASELINE_WINDOWS3 + 1 }, () => ({
|
|
21432
|
+
count: 0,
|
|
21433
|
+
evidence_audit_ids: []
|
|
21434
|
+
}))
|
|
21435
|
+
};
|
|
21436
|
+
byPair.set(pairKey2, pairSnapshot);
|
|
21437
|
+
}
|
|
21438
|
+
const bucket = pairSnapshot.windows[windowIdx];
|
|
21439
|
+
bucket.count += 1;
|
|
21440
|
+
if (windowIdx === 0 && bucket.evidence_audit_ids.length < 50) {
|
|
21441
|
+
bucket.evidence_audit_ids.push(
|
|
21442
|
+
`${entry.timestamp}:${entry.operation}`
|
|
21443
|
+
);
|
|
21444
|
+
}
|
|
21445
|
+
let agentState = byAgent.get(agentId);
|
|
21446
|
+
if (!agentState) {
|
|
21447
|
+
agentState = {
|
|
21448
|
+
currentSecrets: /* @__PURE__ */ new Set(),
|
|
21449
|
+
currentEvidence: [],
|
|
21450
|
+
baselineSecretsByWindow: Array.from(
|
|
21451
|
+
{ length: BASELINE_WINDOWS3 },
|
|
21452
|
+
() => /* @__PURE__ */ new Set()
|
|
21453
|
+
),
|
|
21454
|
+
baselinePopulatedWindows: 0
|
|
21455
|
+
};
|
|
21456
|
+
byAgent.set(agentId, agentState);
|
|
21457
|
+
}
|
|
21458
|
+
if (windowIdx === 0) {
|
|
21459
|
+
agentState.currentSecrets.add(secretId);
|
|
21460
|
+
if (agentState.currentEvidence.length < 50) {
|
|
21461
|
+
agentState.currentEvidence.push(
|
|
21462
|
+
`${entry.timestamp}:${entry.operation}`
|
|
21463
|
+
);
|
|
21464
|
+
}
|
|
21465
|
+
} else {
|
|
21466
|
+
const baselineIdx = windowIdx - 1;
|
|
21467
|
+
agentState.baselineSecretsByWindow[baselineIdx].add(secretId);
|
|
21468
|
+
}
|
|
21469
|
+
}
|
|
21470
|
+
const findings = [];
|
|
21471
|
+
for (const [pairKey2, snapshot] of byPair.entries()) {
|
|
21472
|
+
const [agentId, secretId] = pairKey2.split("\0");
|
|
21473
|
+
const finding = this.evaluateRateSpike(
|
|
21474
|
+
agentId,
|
|
21475
|
+
secretId,
|
|
21476
|
+
snapshot,
|
|
21477
|
+
now
|
|
21478
|
+
);
|
|
21479
|
+
if (finding) findings.push(finding);
|
|
21480
|
+
}
|
|
21481
|
+
for (const [agentId, agentState] of byAgent.entries()) {
|
|
21482
|
+
agentState.baselinePopulatedWindows = agentState.baselineSecretsByWindow.filter((s) => s.size > 0).length;
|
|
21483
|
+
const finding = this.evaluateNewPairs(agentId, agentState, now);
|
|
21484
|
+
if (finding) findings.push(finding);
|
|
21485
|
+
}
|
|
21486
|
+
return findings;
|
|
21487
|
+
}
|
|
21488
|
+
evaluateRateSpike(agentId, secretId, snapshot, now) {
|
|
21489
|
+
const currentWindow = snapshot.windows[0];
|
|
21490
|
+
const baselineWindows = snapshot.windows.slice(1);
|
|
21491
|
+
const populated = baselineWindows.filter((w) => w.count > 0).length;
|
|
21492
|
+
if (populated < BASELINE_WINDOWS3) {
|
|
21493
|
+
return null;
|
|
21494
|
+
}
|
|
21495
|
+
const counts = baselineWindows.map((w) => w.count);
|
|
21496
|
+
const mean = counts.reduce((sum, c) => sum + c, 0) / counts.length;
|
|
21497
|
+
const variance = counts.reduce((sum, c) => sum + (c - mean) ** 2, 0) / counts.length;
|
|
21498
|
+
const stddev = Math.sqrt(variance);
|
|
21499
|
+
const pairKey2 = `${agentId}\0${secretId}`;
|
|
21500
|
+
this.baselineEstablished.add(pairKey2);
|
|
21501
|
+
const warnThreshold = mean + WARN_SIGMA3 * stddev;
|
|
21502
|
+
const alertThreshold = mean + ALERT_SIGMA3 * stddev;
|
|
21503
|
+
if (currentWindow.count > alertThreshold) {
|
|
21504
|
+
return this.buildRateFinding(
|
|
21505
|
+
agentId,
|
|
21506
|
+
secretId,
|
|
21507
|
+
currentWindow,
|
|
21508
|
+
mean,
|
|
21509
|
+
stddev,
|
|
21510
|
+
now,
|
|
21511
|
+
"alert",
|
|
21512
|
+
ALERT_SIGMA3
|
|
21513
|
+
);
|
|
21514
|
+
}
|
|
21515
|
+
if (currentWindow.count > warnThreshold) {
|
|
21516
|
+
return this.buildRateFinding(
|
|
21517
|
+
agentId,
|
|
21518
|
+
secretId,
|
|
21519
|
+
currentWindow,
|
|
21520
|
+
mean,
|
|
21521
|
+
stddev,
|
|
21522
|
+
now,
|
|
21523
|
+
"warn",
|
|
21524
|
+
WARN_SIGMA3
|
|
21525
|
+
);
|
|
21526
|
+
}
|
|
21527
|
+
return null;
|
|
21528
|
+
}
|
|
21529
|
+
buildRateFinding(agentId, secretId, currentWindow, mean, stddev, now, severity, sigma) {
|
|
21530
|
+
const ratio = mean === 0 ? Number.POSITIVE_INFINITY : currentWindow.count / mean;
|
|
21531
|
+
const ratioStr = Number.isFinite(ratio) ? `${ratio.toFixed(1)}x normal` : "no prior baseline";
|
|
21532
|
+
const summary = `${agentId} agent used ${secretId} ${currentWindow.count} times in last 24h, baseline ${mean.toFixed(1)} +/- ${stddev.toFixed(1)} (${ratioStr}). Crossed +${sigma} sigma threshold.`;
|
|
21533
|
+
return {
|
|
21534
|
+
finding_id: "",
|
|
21535
|
+
sentinel_id: this.sentinelId,
|
|
21536
|
+
severity,
|
|
21537
|
+
agent_id: agentId,
|
|
21538
|
+
summary,
|
|
21539
|
+
details: {
|
|
21540
|
+
agent_id: agentId,
|
|
21541
|
+
secret_id: secretId,
|
|
21542
|
+
current_count: currentWindow.count,
|
|
21543
|
+
baseline_mean: mean,
|
|
21544
|
+
baseline_stddev: stddev,
|
|
21545
|
+
sigma_threshold: sigma,
|
|
21546
|
+
ratio: Number.isFinite(ratio) ? ratio : null
|
|
21547
|
+
},
|
|
21548
|
+
observed_at: now.toISOString(),
|
|
21549
|
+
evidence_audit_ids: currentWindow.evidence_audit_ids,
|
|
21550
|
+
fortress_id: ""
|
|
21551
|
+
};
|
|
21552
|
+
}
|
|
21553
|
+
evaluateNewPairs(agentId, state, now) {
|
|
21554
|
+
if (state.baselinePopulatedWindows < BASELINE_WINDOWS3) {
|
|
21555
|
+
return null;
|
|
21556
|
+
}
|
|
21557
|
+
if (state.currentSecrets.size < 2) return null;
|
|
21558
|
+
const currentPairs = enumerateUnorderedPairs(state.currentSecrets);
|
|
21559
|
+
const historicalPairs = /* @__PURE__ */ new Set();
|
|
21560
|
+
for (const secretSet of state.baselineSecretsByWindow) {
|
|
21561
|
+
for (const pair of enumerateUnorderedPairs(secretSet)) {
|
|
21562
|
+
historicalPairs.add(pair);
|
|
21563
|
+
}
|
|
21564
|
+
}
|
|
21565
|
+
const newPairs = [];
|
|
21566
|
+
for (const pair of currentPairs) {
|
|
21567
|
+
if (historicalPairs.has(pair)) continue;
|
|
21568
|
+
const [a, b] = pair.split("\0");
|
|
21569
|
+
newPairs.push([a, b]);
|
|
21570
|
+
}
|
|
21571
|
+
if (newPairs.length === 0) return null;
|
|
21572
|
+
const severity = newPairs.length >= NEW_PAIR_ALERT_COUNT ? "alert" : "warn";
|
|
21573
|
+
const summary = buildNewPairSummary(agentId, newPairs);
|
|
21574
|
+
return {
|
|
21575
|
+
finding_id: "",
|
|
21576
|
+
sentinel_id: this.sentinelId,
|
|
21577
|
+
severity,
|
|
21578
|
+
agent_id: agentId,
|
|
21579
|
+
summary,
|
|
21580
|
+
details: {
|
|
21581
|
+
agent_id: agentId,
|
|
21582
|
+
new_pairs: newPairs,
|
|
21583
|
+
new_pair_count: newPairs.length,
|
|
21584
|
+
historical_pair_count: historicalPairs.size,
|
|
21585
|
+
current_pair_count: currentPairs.size
|
|
21586
|
+
},
|
|
21587
|
+
observed_at: now.toISOString(),
|
|
21588
|
+
evidence_audit_ids: state.currentEvidence,
|
|
21589
|
+
fortress_id: ""
|
|
21590
|
+
};
|
|
21591
|
+
}
|
|
21592
|
+
};
|
|
21593
|
+
function isCredentialAuditEntry(entry) {
|
|
21594
|
+
if (entry.result !== "success") return false;
|
|
21595
|
+
return entry.operation === BROKER_SECRET_READ_OP || entry.operation === BROKER_TOKEN_ISSUED_OP;
|
|
21596
|
+
}
|
|
21597
|
+
function extractAgentId(entry) {
|
|
21598
|
+
const details = entry.details;
|
|
21599
|
+
if (!details) return null;
|
|
21600
|
+
const agent = details["agent"];
|
|
21601
|
+
return typeof agent === "string" && agent.length > 0 ? agent : null;
|
|
21602
|
+
}
|
|
21603
|
+
function extractSecretId(entry) {
|
|
21604
|
+
const details = entry.details;
|
|
21605
|
+
if (!details) return null;
|
|
21606
|
+
const secret = details["secret"];
|
|
21607
|
+
return typeof secret === "string" && secret.length > 0 ? secret : null;
|
|
21608
|
+
}
|
|
21609
|
+
function enumerateUnorderedPairs(secrets) {
|
|
21610
|
+
const out = /* @__PURE__ */ new Set();
|
|
21611
|
+
const arr = [...secrets].sort();
|
|
21612
|
+
for (let i = 0; i < arr.length; i += 1) {
|
|
21613
|
+
for (let j = i + 1; j < arr.length; j += 1) {
|
|
21614
|
+
out.add(`${arr[i]}\0${arr[j]}`);
|
|
21615
|
+
}
|
|
21616
|
+
}
|
|
21617
|
+
return out;
|
|
21618
|
+
}
|
|
21619
|
+
function buildNewPairSummary(agentId, newPairs) {
|
|
21620
|
+
const first = newPairs[0];
|
|
21621
|
+
if (newPairs.length === 1) {
|
|
21622
|
+
return `${agentId} agent used ${first[0]} and ${first[1]} together for the first time today. This combination does not appear in historical sessions.`;
|
|
21623
|
+
}
|
|
21624
|
+
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.`;
|
|
21625
|
+
}
|
|
21626
|
+
|
|
21627
|
+
// src/sentinel/sentinels/suspicious-tool-call-detector.ts
|
|
21628
|
+
var SUSPICIOUS_TOOL_CALL_SENTINEL_ID = "suspicious-tool-call";
|
|
21629
|
+
var GATE_PREFIXES = [
|
|
21630
|
+
"gate_allow:",
|
|
21631
|
+
"gate_allow_proxy:",
|
|
21632
|
+
"gate_deny:",
|
|
21633
|
+
"gate_unclassified:"
|
|
21634
|
+
];
|
|
21635
|
+
var WARN_SIGMA4 = 3;
|
|
21636
|
+
var ALERT_SIGMA4 = 6;
|
|
21637
|
+
var BASELINE_WINDOWS4 = 7;
|
|
21638
|
+
var ALERT_NOVEL_COMBINATIONS = 2;
|
|
21639
|
+
var TASK_WINDOW_MS = 60 * 60 * 1e3;
|
|
21640
|
+
var TRUNCATION_WARN_THRESHOLD = 5;
|
|
21641
|
+
var QUERY_LIMIT4 = 1e4;
|
|
21642
|
+
var SIGNATURE_PATTERNS = {
|
|
21643
|
+
/** >=5 percent-encoded sequences in a single visible value. */
|
|
21644
|
+
urlEncodedThreshold: 5,
|
|
21645
|
+
/** >=40 contiguous base64 chars in a single visible value. */
|
|
21646
|
+
base64MinRun: 40,
|
|
21647
|
+
/** Shell metacharacter set. */
|
|
21648
|
+
shellMetacharRegex: /(?:&&|\|\||;|\$\(|`|\|\s)/
|
|
21649
|
+
};
|
|
21650
|
+
var SuspiciousToolCallDetector = class extends Sentinel {
|
|
21651
|
+
sentinelId = SUSPICIOUS_TOOL_CALL_SENTINEL_ID;
|
|
21652
|
+
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.";
|
|
21653
|
+
/** Servers we have already produced an `info` baseline-established finding for. */
|
|
21654
|
+
baselineEstablished = /* @__PURE__ */ new Set();
|
|
21655
|
+
/** Memoized known novel-combination keys (sorted-tools-csv). */
|
|
21656
|
+
knownCombinations = /* @__PURE__ */ new Set();
|
|
21657
|
+
/** Tasks observed where a novel combination already produced a finding. */
|
|
21658
|
+
novelCombinationsReported = /* @__PURE__ */ new Set();
|
|
21659
|
+
async evaluate() {
|
|
21660
|
+
const ctx = this.requireContext();
|
|
21661
|
+
const now = ctx.now();
|
|
21662
|
+
const dayMs = 24 * 60 * 60 * 1e3;
|
|
21663
|
+
const windowSpanMs = (BASELINE_WINDOWS4 + 1) * dayMs;
|
|
21664
|
+
const sinceIso = new Date(now.getTime() - windowSpanMs).toISOString();
|
|
21665
|
+
const queryResult = await ctx.auditLog.query({
|
|
21666
|
+
since: sinceIso,
|
|
21667
|
+
layer: "l2",
|
|
21668
|
+
limit: QUERY_LIMIT4
|
|
21669
|
+
});
|
|
21670
|
+
const observations = [];
|
|
21671
|
+
for (const entry of queryResult.entries) {
|
|
21672
|
+
const obs = this.observationFromEntry(entry);
|
|
21673
|
+
if (obs && obs.ts <= now.getTime()) observations.push(obs);
|
|
21674
|
+
}
|
|
21675
|
+
if (observations.length === 0) return [];
|
|
21676
|
+
const findings = [];
|
|
21677
|
+
const layerAFindings = await this.runLayerA(observations, now, ctx);
|
|
21678
|
+
findings.push(...layerAFindings);
|
|
21679
|
+
const layerBFindings = this.runLayerB(observations, now);
|
|
21680
|
+
findings.push(...layerBFindings);
|
|
21681
|
+
const layerCFindings = this.runLayerC(observations, now);
|
|
21682
|
+
findings.push(...layerCFindings);
|
|
21683
|
+
return findings;
|
|
21684
|
+
}
|
|
21685
|
+
/** Reset memoization between test runs. Mirrors Phi-1's reset hook. */
|
|
21686
|
+
resetMemo() {
|
|
21687
|
+
this.baselineEstablished.clear();
|
|
21688
|
+
this.knownCombinations.clear();
|
|
21689
|
+
this.novelCombinationsReported.clear();
|
|
21690
|
+
}
|
|
21691
|
+
// ── Layer A ───────────────────────────────────────────────────────
|
|
21692
|
+
async runLayerA(observations, now, ctx) {
|
|
21693
|
+
const dayMs = 24 * 60 * 60 * 1e3;
|
|
21694
|
+
const recent = observations.filter(
|
|
21695
|
+
(o) => now.getTime() - o.ts <= dayMs
|
|
21696
|
+
);
|
|
21697
|
+
if (recent.length === 0) return [];
|
|
21698
|
+
const findings = [];
|
|
21699
|
+
const historical = observations.filter(
|
|
21700
|
+
(o) => now.getTime() - o.ts > dayMs
|
|
21701
|
+
);
|
|
21702
|
+
const perTool = /* @__PURE__ */ new Map();
|
|
21703
|
+
const ensureTool = (tool) => {
|
|
21704
|
+
let w = perTool.get(tool);
|
|
21705
|
+
if (!w) {
|
|
21706
|
+
w = {
|
|
21707
|
+
windows: Array.from({ length: BASELINE_WINDOWS4 + 1 }, () => ({
|
|
21708
|
+
count: 0,
|
|
21709
|
+
evidenceIds: []
|
|
21710
|
+
})),
|
|
21711
|
+
knownSignatures: /* @__PURE__ */ new Set()
|
|
21712
|
+
};
|
|
21713
|
+
perTool.set(tool, w);
|
|
21714
|
+
}
|
|
21715
|
+
return w;
|
|
21716
|
+
};
|
|
21717
|
+
for (const o of historical) {
|
|
21718
|
+
ensureTool(o.tool).knownSignatures.add(this.signatureOf(o.argsSummary));
|
|
21719
|
+
}
|
|
21720
|
+
const classify = this.classifyHandle(ctx);
|
|
21721
|
+
for (const obs of recent) {
|
|
21722
|
+
const matches = this.matchSignatures(obs);
|
|
21723
|
+
if (matches.length === 0) continue;
|
|
21724
|
+
const ambiguous = matches.every((m) => m === "base64_chunk");
|
|
21725
|
+
if (ambiguous && classify) {
|
|
21726
|
+
const verdict = await this.consultClassifier(classify, obs);
|
|
21727
|
+
if (verdict !== "suspicious") continue;
|
|
21728
|
+
}
|
|
21729
|
+
findings.push(
|
|
21730
|
+
this.buildLayerAFinding(obs, matches, now, classify ? "llm-assist" : "rule-based")
|
|
21731
|
+
);
|
|
21732
|
+
}
|
|
21733
|
+
for (const obs of recent) {
|
|
21734
|
+
const tool = obs.tool;
|
|
21735
|
+
const sig = this.signatureOf(obs.argsSummary);
|
|
21736
|
+
const known = perTool.get(tool)?.knownSignatures;
|
|
21737
|
+
if (known && known.size > 0 && !known.has(sig)) {
|
|
21738
|
+
findings.push(
|
|
21739
|
+
this.buildNovelSignatureFinding(obs, sig, now)
|
|
21740
|
+
);
|
|
21741
|
+
}
|
|
21742
|
+
}
|
|
21743
|
+
return findings;
|
|
21744
|
+
}
|
|
21745
|
+
matchSignatures(obs) {
|
|
21746
|
+
const out = [];
|
|
21747
|
+
const truncCount = countTruncatedValues(obs.argsSummary);
|
|
21748
|
+
if (truncCount >= TRUNCATION_WARN_THRESHOLD) out.push("truncation_burst");
|
|
21749
|
+
let urlBlob = false;
|
|
21750
|
+
let base64Blob = false;
|
|
21751
|
+
let shellChars = false;
|
|
21752
|
+
for (const value of Object.values(obs.argsSummary)) {
|
|
21753
|
+
if (typeof value !== "string") continue;
|
|
21754
|
+
if (countUrlEncoded(value) >= SIGNATURE_PATTERNS.urlEncodedThreshold) {
|
|
21755
|
+
urlBlob = true;
|
|
21756
|
+
}
|
|
21757
|
+
if (longestBase64Run(value) >= SIGNATURE_PATTERNS.base64MinRun) {
|
|
21758
|
+
base64Blob = true;
|
|
21759
|
+
}
|
|
21760
|
+
if (SIGNATURE_PATTERNS.shellMetacharRegex.test(value)) {
|
|
21761
|
+
shellChars = true;
|
|
21762
|
+
}
|
|
21763
|
+
}
|
|
21764
|
+
if (urlBlob) out.push("url_encoded_blob");
|
|
21765
|
+
if (base64Blob) out.push("base64_chunk");
|
|
21766
|
+
if (shellChars) out.push("shell_metachar");
|
|
21767
|
+
return out;
|
|
21768
|
+
}
|
|
21769
|
+
signatureOf(argsSummary) {
|
|
21770
|
+
return Object.keys(argsSummary).sort().join(",");
|
|
21771
|
+
}
|
|
21772
|
+
buildLayerAFinding(obs, matches, now, detectionPath) {
|
|
21773
|
+
const severity = matches.includes("shell_metachar") ? "alert" : "warn";
|
|
21774
|
+
const summary = `${obs.tool}: tool-call argument matches signature ${matches.join(", ")} (${detectionPath}).`;
|
|
21775
|
+
return {
|
|
21776
|
+
finding_id: "",
|
|
21777
|
+
sentinel_id: this.sentinelId,
|
|
21778
|
+
severity,
|
|
21779
|
+
summary: truncateSummary2(summary),
|
|
21780
|
+
details: {
|
|
21781
|
+
layer: "A",
|
|
21782
|
+
tool: obs.tool,
|
|
21783
|
+
proxy: obs.proxy,
|
|
21784
|
+
signatures: matches,
|
|
21785
|
+
detection_path: detectionPath
|
|
21786
|
+
},
|
|
21787
|
+
observed_at: now.toISOString(),
|
|
21788
|
+
evidence_audit_ids: [`${obs.entry.timestamp}:${obs.entry.operation}`],
|
|
21789
|
+
fortress_id: ""
|
|
21790
|
+
};
|
|
21791
|
+
}
|
|
21792
|
+
buildNovelSignatureFinding(obs, signature, now) {
|
|
21793
|
+
return {
|
|
21794
|
+
finding_id: "",
|
|
21795
|
+
sentinel_id: this.sentinelId,
|
|
21796
|
+
severity: "warn",
|
|
21797
|
+
summary: truncateSummary2(
|
|
21798
|
+
`${obs.tool}: novel argument-key signature observed (${signature || "<no-args>"}).`
|
|
21799
|
+
),
|
|
21800
|
+
details: {
|
|
21801
|
+
layer: "A",
|
|
21802
|
+
tool: obs.tool,
|
|
21803
|
+
proxy: obs.proxy,
|
|
21804
|
+
signatures: ["novel_signature"],
|
|
21805
|
+
detection_path: "rule-based",
|
|
21806
|
+
novel_signature: signature
|
|
21807
|
+
},
|
|
21808
|
+
observed_at: now.toISOString(),
|
|
21809
|
+
evidence_audit_ids: [`${obs.entry.timestamp}:${obs.entry.operation}`],
|
|
21810
|
+
fortress_id: ""
|
|
21811
|
+
};
|
|
21812
|
+
}
|
|
21813
|
+
// ── Layer B ───────────────────────────────────────────────────────
|
|
21814
|
+
runLayerB(observations, now) {
|
|
21815
|
+
const dayMs = 24 * 60 * 60 * 1e3;
|
|
21816
|
+
const perTool = /* @__PURE__ */ new Map();
|
|
21817
|
+
for (const obs of observations) {
|
|
21818
|
+
const ageMs = now.getTime() - obs.ts;
|
|
21819
|
+
if (ageMs < 0) continue;
|
|
21820
|
+
const windowIdx = Math.floor(ageMs / dayMs);
|
|
21821
|
+
if (windowIdx > BASELINE_WINDOWS4) continue;
|
|
21822
|
+
let w = perTool.get(obs.tool);
|
|
21823
|
+
if (!w) {
|
|
21824
|
+
w = {
|
|
21825
|
+
windows: Array.from({ length: BASELINE_WINDOWS4 + 1 }, () => ({
|
|
21826
|
+
count: 0,
|
|
21827
|
+
evidenceIds: []
|
|
21828
|
+
})),
|
|
21829
|
+
knownSignatures: /* @__PURE__ */ new Set()
|
|
21830
|
+
};
|
|
21831
|
+
perTool.set(obs.tool, w);
|
|
21832
|
+
}
|
|
21833
|
+
const bucket = w.windows[windowIdx];
|
|
21834
|
+
bucket.count += 1;
|
|
21835
|
+
if (windowIdx === 0 && bucket.evidenceIds.length < 50) {
|
|
21836
|
+
bucket.evidenceIds.push(`${obs.entry.timestamp}:${obs.entry.operation}`);
|
|
21837
|
+
}
|
|
21838
|
+
}
|
|
21839
|
+
const findings = [];
|
|
21840
|
+
for (const [tool, w] of perTool.entries()) {
|
|
21841
|
+
const f = this.evaluateToolFrequency(tool, w, now);
|
|
21842
|
+
if (f) findings.push(f);
|
|
21843
|
+
}
|
|
21844
|
+
return findings;
|
|
21845
|
+
}
|
|
21846
|
+
evaluateToolFrequency(tool, w, now) {
|
|
21847
|
+
const current = w.windows[0];
|
|
21848
|
+
const baseline = w.windows.slice(1);
|
|
21849
|
+
const populated = baseline.filter((b) => b.count > 0).length;
|
|
21850
|
+
if (populated < BASELINE_WINDOWS4) {
|
|
21851
|
+
this.baselineEstablished.add(tool);
|
|
21852
|
+
return null;
|
|
21853
|
+
}
|
|
21854
|
+
const counts = baseline.map((b) => b.count);
|
|
21855
|
+
const mean = counts.reduce((sum, c) => sum + c, 0) / counts.length;
|
|
21856
|
+
const variance = counts.reduce((sum, c) => sum + (c - mean) ** 2, 0) / counts.length;
|
|
21857
|
+
const stddev = Math.sqrt(variance);
|
|
21858
|
+
const wasEstablished = this.baselineEstablished.has(tool);
|
|
21859
|
+
this.baselineEstablished.add(tool);
|
|
21860
|
+
if (!wasEstablished) {
|
|
21861
|
+
return {
|
|
21862
|
+
finding_id: "",
|
|
21863
|
+
sentinel_id: this.sentinelId,
|
|
21864
|
+
severity: "info",
|
|
21865
|
+
summary: truncateSummary2(
|
|
21866
|
+
`${tool}: tool-call baseline established (mean ${mean.toFixed(1)} calls/24h, stddev ${stddev.toFixed(1)} over ${BASELINE_WINDOWS4} prior days).`
|
|
21867
|
+
),
|
|
21868
|
+
details: {
|
|
21869
|
+
layer: "B",
|
|
21870
|
+
tool,
|
|
21871
|
+
baseline_mean: mean,
|
|
21872
|
+
baseline_stddev: stddev,
|
|
21873
|
+
baseline_counts: counts,
|
|
21874
|
+
current_count: current.count
|
|
21875
|
+
},
|
|
21876
|
+
observed_at: now.toISOString(),
|
|
21877
|
+
evidence_audit_ids: [],
|
|
21878
|
+
fortress_id: ""
|
|
21879
|
+
};
|
|
21880
|
+
}
|
|
21881
|
+
const warnT = mean + WARN_SIGMA4 * stddev;
|
|
21882
|
+
const alertT = mean + ALERT_SIGMA4 * stddev;
|
|
21883
|
+
if (current.count > alertT) {
|
|
21884
|
+
return this.buildLayerBAnomaly(
|
|
21885
|
+
tool,
|
|
21886
|
+
current,
|
|
21887
|
+
mean,
|
|
21888
|
+
stddev,
|
|
21889
|
+
ALERT_SIGMA4,
|
|
21890
|
+
"alert",
|
|
21891
|
+
now
|
|
21892
|
+
);
|
|
21893
|
+
}
|
|
21894
|
+
if (current.count > warnT) {
|
|
21895
|
+
return this.buildLayerBAnomaly(
|
|
21896
|
+
tool,
|
|
21897
|
+
current,
|
|
21898
|
+
mean,
|
|
21899
|
+
stddev,
|
|
21900
|
+
WARN_SIGMA4,
|
|
21901
|
+
"warn",
|
|
21902
|
+
now
|
|
21903
|
+
);
|
|
21904
|
+
}
|
|
21905
|
+
return null;
|
|
21906
|
+
}
|
|
21907
|
+
buildLayerBAnomaly(tool, current, mean, stddev, sigma, severity, now) {
|
|
21908
|
+
const ratio = mean === 0 ? Infinity : current.count / mean;
|
|
21909
|
+
const ratioStr = Number.isFinite(ratio) ? `${ratio.toFixed(1)}x normal` : "no prior baseline";
|
|
21910
|
+
return {
|
|
21911
|
+
finding_id: "",
|
|
21912
|
+
sentinel_id: this.sentinelId,
|
|
21913
|
+
severity,
|
|
21914
|
+
summary: truncateSummary2(
|
|
21915
|
+
`${tool}: tool-call rate is ${ratioStr}: ${current.count} calls in last 24h, baseline ${mean.toFixed(1)} (stddev ${stddev.toFixed(1)}). Crossed +${sigma} sigma.`
|
|
21916
|
+
),
|
|
21917
|
+
details: {
|
|
21918
|
+
layer: "B",
|
|
21919
|
+
tool,
|
|
21920
|
+
current_count: current.count,
|
|
21921
|
+
baseline_mean: mean,
|
|
21922
|
+
baseline_stddev: stddev,
|
|
21923
|
+
sigma_threshold: sigma,
|
|
21924
|
+
ratio
|
|
21925
|
+
},
|
|
21926
|
+
observed_at: now.toISOString(),
|
|
21927
|
+
evidence_audit_ids: current.evidenceIds,
|
|
21928
|
+
fortress_id: ""
|
|
21929
|
+
};
|
|
21930
|
+
}
|
|
21931
|
+
// ── Layer C ───────────────────────────────────────────────────────
|
|
21932
|
+
runLayerC(observations, now) {
|
|
21933
|
+
const dayMs = 24 * 60 * 60 * 1e3;
|
|
21934
|
+
const sorted = [...observations].sort((a, b) => a.ts - b.ts);
|
|
21935
|
+
const tasks = [];
|
|
21936
|
+
for (const obs of sorted) {
|
|
21937
|
+
const last = tasks[tasks.length - 1];
|
|
21938
|
+
if (!last || obs.ts - last.startTs > TASK_WINDOW_MS) {
|
|
21939
|
+
tasks.push({ startTs: obs.ts, tools: [obs.tool] });
|
|
21940
|
+
continue;
|
|
21941
|
+
}
|
|
21942
|
+
if (!last.tools.includes(obs.tool)) last.tools.push(obs.tool);
|
|
21943
|
+
}
|
|
21944
|
+
const recentTaskKeys = [];
|
|
21945
|
+
const findings = [];
|
|
21946
|
+
for (const task of tasks) {
|
|
21947
|
+
const ageMs = now.getTime() - task.startTs;
|
|
21948
|
+
const key = task.tools.slice().sort().join(",");
|
|
21949
|
+
if (ageMs > dayMs) {
|
|
21950
|
+
this.knownCombinations.add(key);
|
|
21951
|
+
continue;
|
|
21952
|
+
}
|
|
21953
|
+
if (task.tools.length < 2) continue;
|
|
21954
|
+
if (!this.knownCombinations.has(key)) {
|
|
21955
|
+
this.knownCombinations.add(key);
|
|
21956
|
+
if (!this.novelCombinationsReported.has(key)) {
|
|
21957
|
+
this.novelCombinationsReported.add(key);
|
|
21958
|
+
recentTaskKeys.push(key);
|
|
21959
|
+
findings.push(
|
|
21960
|
+
this.buildLayerCFinding(task, key, "warn", now)
|
|
21961
|
+
);
|
|
21962
|
+
}
|
|
21963
|
+
}
|
|
21964
|
+
}
|
|
21965
|
+
if (recentTaskKeys.length >= ALERT_NOVEL_COMBINATIONS) {
|
|
21966
|
+
const aggregate = {
|
|
21967
|
+
finding_id: "",
|
|
21968
|
+
sentinel_id: this.sentinelId,
|
|
21969
|
+
severity: "alert",
|
|
21970
|
+
summary: truncateSummary2(
|
|
21971
|
+
`multi-novel-combination: ${recentTaskKeys.length} novel tool-permission combinations within last 24h.`
|
|
21972
|
+
),
|
|
21973
|
+
details: {
|
|
21974
|
+
layer: "C",
|
|
21975
|
+
novel_combinations: recentTaskKeys
|
|
21976
|
+
},
|
|
21977
|
+
observed_at: now.toISOString(),
|
|
21978
|
+
evidence_audit_ids: [],
|
|
21979
|
+
fortress_id: ""
|
|
21980
|
+
};
|
|
21981
|
+
findings.push(aggregate);
|
|
21982
|
+
}
|
|
21983
|
+
return findings;
|
|
21984
|
+
}
|
|
21985
|
+
buildLayerCFinding(task, key, severity, now) {
|
|
21986
|
+
return {
|
|
21987
|
+
finding_id: "",
|
|
21988
|
+
sentinel_id: this.sentinelId,
|
|
21989
|
+
severity,
|
|
21990
|
+
summary: truncateSummary2(
|
|
21991
|
+
`novel-permission-combination: tools=[${task.tools.join(",")}] observed in single task burst (${task.tools.length} distinct tools).`
|
|
21992
|
+
),
|
|
21993
|
+
details: {
|
|
21994
|
+
layer: "C",
|
|
21995
|
+
combination_key: key,
|
|
21996
|
+
tools: task.tools,
|
|
21997
|
+
task_started_at: new Date(task.startTs).toISOString()
|
|
21998
|
+
},
|
|
21999
|
+
observed_at: now.toISOString(),
|
|
22000
|
+
evidence_audit_ids: [],
|
|
22001
|
+
fortress_id: ""
|
|
22002
|
+
};
|
|
22003
|
+
}
|
|
22004
|
+
// ── LLM-assist ────────────────────────────────────────────────────
|
|
22005
|
+
classifyHandle(ctx) {
|
|
22006
|
+
const selector = ctx.substrateSelector;
|
|
22007
|
+
if (!selector) return null;
|
|
22008
|
+
const fn = selector.invokeClassify;
|
|
22009
|
+
if (typeof fn !== "function") return null;
|
|
22010
|
+
return async (items) => {
|
|
22011
|
+
try {
|
|
22012
|
+
const resp = await fn.call(selector, "sentinel-scoring", {
|
|
22013
|
+
kind: "classify",
|
|
22014
|
+
items,
|
|
22015
|
+
categories: ["benign", "suspicious"]
|
|
22016
|
+
});
|
|
22017
|
+
if (resp.failureClass) return { kind: "failure", message: "substrate failure" };
|
|
22018
|
+
if (resp.body.kind === "classify") {
|
|
22019
|
+
return { kind: "classify", results: resp.body.results };
|
|
22020
|
+
}
|
|
22021
|
+
return { kind: "failure", message: resp.body.message };
|
|
22022
|
+
} catch {
|
|
22023
|
+
return null;
|
|
22024
|
+
}
|
|
22025
|
+
};
|
|
22026
|
+
}
|
|
22027
|
+
async consultClassifier(classify, obs) {
|
|
22028
|
+
const item = JSON.stringify({
|
|
22029
|
+
tool: obs.tool,
|
|
22030
|
+
proxy: obs.proxy,
|
|
22031
|
+
args_summary: obs.argsSummary
|
|
22032
|
+
});
|
|
22033
|
+
const result = await classify([item]);
|
|
22034
|
+
if (!result || result.kind !== "classify") return "unknown";
|
|
22035
|
+
const top = result.results[0];
|
|
22036
|
+
if (!top) return "unknown";
|
|
22037
|
+
if (top.category === "suspicious" && top.confidence >= 0.5) {
|
|
22038
|
+
return "suspicious";
|
|
22039
|
+
}
|
|
22040
|
+
if (top.category === "benign") return "benign";
|
|
22041
|
+
return "unknown";
|
|
22042
|
+
}
|
|
22043
|
+
// ── helpers ───────────────────────────────────────────────────────
|
|
22044
|
+
observationFromEntry(entry) {
|
|
22045
|
+
const op = entry.operation;
|
|
22046
|
+
let tool = null;
|
|
22047
|
+
let proxy = false;
|
|
22048
|
+
for (const prefix of GATE_PREFIXES) {
|
|
22049
|
+
if (op.startsWith(prefix)) {
|
|
22050
|
+
tool = op.slice(prefix.length);
|
|
22051
|
+
proxy = prefix === "gate_allow_proxy:";
|
|
22052
|
+
break;
|
|
22053
|
+
}
|
|
22054
|
+
}
|
|
22055
|
+
if (!tool) return null;
|
|
22056
|
+
const ts = Date.parse(entry.timestamp);
|
|
22057
|
+
if (!Number.isFinite(ts)) return null;
|
|
22058
|
+
const argsSummary = extractArgsSummary(entry.details);
|
|
22059
|
+
return { tool, proxy, ts, entry, argsSummary };
|
|
22060
|
+
}
|
|
22061
|
+
};
|
|
22062
|
+
function countTruncatedValues(args) {
|
|
22063
|
+
let n = 0;
|
|
22064
|
+
for (const v of Object.values(args)) {
|
|
22065
|
+
if (typeof v === "string" && v.endsWith("...")) n += 1;
|
|
22066
|
+
}
|
|
22067
|
+
return n;
|
|
22068
|
+
}
|
|
22069
|
+
function countUrlEncoded(value) {
|
|
22070
|
+
const matches = value.match(/%[0-9a-fA-F]{2}/g);
|
|
22071
|
+
return matches ? matches.length : 0;
|
|
22072
|
+
}
|
|
22073
|
+
function longestBase64Run(value) {
|
|
22074
|
+
const matches = value.match(/[A-Za-z0-9+/=]{40,}/g);
|
|
22075
|
+
if (!matches) return 0;
|
|
22076
|
+
return matches.reduce((max, m) => m.length > max ? m.length : max, 0);
|
|
22077
|
+
}
|
|
22078
|
+
function extractArgsSummary(details) {
|
|
22079
|
+
if (!details) return {};
|
|
22080
|
+
const summary = details["args_summary"];
|
|
22081
|
+
if (summary && typeof summary === "object" && !Array.isArray(summary)) {
|
|
22082
|
+
return summary;
|
|
22083
|
+
}
|
|
22084
|
+
return {};
|
|
22085
|
+
}
|
|
22086
|
+
function truncateSummary2(s) {
|
|
22087
|
+
return s.length > 240 ? s.slice(0, 237) + "..." : s;
|
|
22088
|
+
}
|
|
22089
|
+
|
|
22090
|
+
// src/sentinel/sentinels/index.ts
|
|
22091
|
+
var PHI1_BASELINE_CATALOG = [
|
|
22092
|
+
{
|
|
22093
|
+
sentinelId: EGRESS_VOLUME_SENTINEL_ID,
|
|
22094
|
+
description: "Watches outbound proxy-call volume per upstream server and surfaces anomalous spikes against a rolling 7-day baseline.",
|
|
22095
|
+
factory: () => new EgressVolumeWatcher()
|
|
22096
|
+
},
|
|
22097
|
+
{
|
|
22098
|
+
sentinelId: CROSS_AGENT_CHATTER_SENTINEL_ID,
|
|
22099
|
+
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.",
|
|
22100
|
+
factory: () => new CrossAgentChatterWatcher()
|
|
22101
|
+
},
|
|
22102
|
+
{
|
|
22103
|
+
sentinelId: CREDENTIAL_USAGE_SENTINEL_ID,
|
|
22104
|
+
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.",
|
|
22105
|
+
factory: () => new CredentialUsageWatcher()
|
|
22106
|
+
},
|
|
22107
|
+
{
|
|
22108
|
+
sentinelId: SUSPICIOUS_TOOL_CALL_SENTINEL_ID,
|
|
22109
|
+
description: "Surfaces tool calls whose argument shape, call frequency, or permission combination looks unusual for the fortress's recent history.",
|
|
22110
|
+
factory: () => new SuspiciousToolCallDetector()
|
|
22111
|
+
}
|
|
22112
|
+
];
|
|
22113
|
+
var FILE_VERSION = 1;
|
|
22114
|
+
function sentinelSubscriptionsPath(storagePath) {
|
|
22115
|
+
return path.join(storagePath, "sentinel-subscriptions.json");
|
|
22116
|
+
}
|
|
22117
|
+
async function loadSentinelSubscriptions(storagePath) {
|
|
22118
|
+
const filePath = sentinelSubscriptionsPath(storagePath);
|
|
22119
|
+
try {
|
|
22120
|
+
const raw = await promises.readFile(filePath, "utf8");
|
|
22121
|
+
const parsed = JSON.parse(raw);
|
|
22122
|
+
if (parsed.version !== FILE_VERSION) return /* @__PURE__ */ new Set();
|
|
22123
|
+
if (!Array.isArray(parsed.subscribed)) return /* @__PURE__ */ new Set();
|
|
22124
|
+
const cleaned = parsed.subscribed.filter(
|
|
22125
|
+
(id) => typeof id === "string" && id.length > 0
|
|
22126
|
+
);
|
|
22127
|
+
return new Set(cleaned);
|
|
22128
|
+
} catch {
|
|
22129
|
+
return /* @__PURE__ */ new Set();
|
|
22130
|
+
}
|
|
22131
|
+
}
|
|
22132
|
+
|
|
20182
22133
|
// src/principal-policy/tools.ts
|
|
20183
22134
|
function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
20184
22135
|
return [
|
|
@@ -32320,6 +34271,13 @@ init_encoding();
|
|
|
32320
34271
|
// src/chat/operator-chat-audit-events.ts
|
|
32321
34272
|
var OPERATOR_CHAT_OPS = {
|
|
32322
34273
|
CONCIERGE_CHAT: "operator_concierge_chat",
|
|
34274
|
+
/**
|
|
34275
|
+
* Click-to-inspect panel opened on an agent row. Repurposed from the
|
|
34276
|
+
* direct-agent session-open audit event in the v1.2 reshape; the click
|
|
34277
|
+
* affordance now opens an inspect/approve panel (recent activity +
|
|
34278
|
+
* pending approvals + policy summary) instead of a chat session.
|
|
34279
|
+
*/
|
|
34280
|
+
AGENT_INSPECT_PANEL_OPENED: "agent_inspect_panel_opened",
|
|
32323
34281
|
/**
|
|
32324
34282
|
* Operator viewed concierge thread history (WP-V1.3-9 Tau-1). Emitted
|
|
32325
34283
|
* when the operator hits the list-threads or read-thread route. Body
|
|
@@ -32346,7 +34304,17 @@ var OPERATOR_CHAT_OPS = {
|
|
|
32346
34304
|
* fold. The concierge omits that category and continues; the user-
|
|
32347
34305
|
* facing query is never broken. Body carries category + failure_reason.
|
|
32348
34306
|
*/
|
|
32349
|
-
CONCIERGE_CONTEXT_FETCHER_FAILED: "operator_concierge_context_fetcher_failed"
|
|
34307
|
+
CONCIERGE_CONTEXT_FETCHER_FAILED: "operator_concierge_context_fetcher_failed",
|
|
34308
|
+
/**
|
|
34309
|
+
* Concierge surfaced a proactive starter when a fresh conversation
|
|
34310
|
+
* thread opened (WP-V1.3-9 Tau-5). Emitted once per starter, never on
|
|
34311
|
+
* follow-up turns within the same thread. Body carries `thread_id`,
|
|
34312
|
+
* `trigger` (stable enum), and `triggered_agents_count`. The starter
|
|
34313
|
+
* text body is NOT carried; the trigger enum is sufficient for
|
|
34314
|
+
* dashboard grouping and keeps fortress-internal agent ids off the
|
|
34315
|
+
* audit surface.
|
|
34316
|
+
*/
|
|
34317
|
+
CONCIERGE_PROACTIVE_SUGGESTION_OFFERED: "operator_concierge_proactive_suggestion_offered"
|
|
32350
34318
|
};
|
|
32351
34319
|
|
|
32352
34320
|
// src/chat/operator-chat-types.ts
|
|
@@ -32488,9 +34456,10 @@ function isTrivialQuery(query) {
|
|
|
32488
34456
|
if (norm.length < 8) return true;
|
|
32489
34457
|
return TRIVIAL_GREETINGS.has(norm);
|
|
32490
34458
|
}
|
|
32491
|
-
function classifyQuery(query) {
|
|
34459
|
+
function classifyQuery(query, parsedGrammar) {
|
|
32492
34460
|
const normalized = query.toLowerCase();
|
|
32493
34461
|
const matches = [];
|
|
34462
|
+
const grammarAgent = parsedGrammar && parsedGrammar.agent_names.length > 0 ? parsedGrammar.agent_names[0] ?? null : null;
|
|
32494
34463
|
for (const spec of CATEGORY_KEYWORDS) {
|
|
32495
34464
|
const matchedPhrases = [];
|
|
32496
34465
|
for (const pattern of spec.patterns) {
|
|
@@ -32502,11 +34471,14 @@ function classifyQuery(query) {
|
|
|
32502
34471
|
}
|
|
32503
34472
|
if (matchedPhrases.length === 0) continue;
|
|
32504
34473
|
const confidence = Math.min(1, 0.4 + 0.3 * matchedPhrases.length);
|
|
34474
|
+
const wantsAgentHint = spec.category === "agent_state" || spec.category === "agent_activity";
|
|
34475
|
+
const agent_name_hint = wantsAgentHint ? grammarAgent ?? extractAgentNameHint(query) : null;
|
|
32505
34476
|
matches.push({
|
|
32506
34477
|
category: spec.category,
|
|
32507
34478
|
confidence,
|
|
32508
34479
|
matched_keywords: matchedPhrases,
|
|
32509
|
-
agent_name_hint
|
|
34480
|
+
agent_name_hint,
|
|
34481
|
+
...parsedGrammar !== void 0 ? { query_grammar: parsedGrammar } : {}
|
|
32510
34482
|
});
|
|
32511
34483
|
}
|
|
32512
34484
|
matches.sort((a, b) => {
|
|
@@ -32515,6 +34487,25 @@ function classifyQuery(query) {
|
|
|
32515
34487
|
});
|
|
32516
34488
|
return matches;
|
|
32517
34489
|
}
|
|
34490
|
+
function fetcherHintsFromGrammar(parsed) {
|
|
34491
|
+
if (!parsed) return void 0;
|
|
34492
|
+
const hasTime = parsed.time_range !== null;
|
|
34493
|
+
const hasAgents = parsed.agent_names.length > 0;
|
|
34494
|
+
const hasEvents = parsed.event_types.length > 0;
|
|
34495
|
+
if (!hasTime && !hasAgents && !hasEvents) return void 0;
|
|
34496
|
+
const hints = {};
|
|
34497
|
+
if (parsed.time_range) {
|
|
34498
|
+
const range = parsed.time_range;
|
|
34499
|
+
hints.time_range = {
|
|
34500
|
+
start: range.start,
|
|
34501
|
+
end: range.end,
|
|
34502
|
+
...range.relative_label !== void 0 ? { relative_label: range.relative_label } : {}
|
|
34503
|
+
};
|
|
34504
|
+
}
|
|
34505
|
+
if (hasAgents) hints.agent_names = parsed.agent_names;
|
|
34506
|
+
if (hasEvents) hints.event_types = parsed.event_types;
|
|
34507
|
+
return hints;
|
|
34508
|
+
}
|
|
32518
34509
|
function approxTokenLen(text) {
|
|
32519
34510
|
return Math.ceil(text.length / APPROX_CHARS_PER_TOKEN);
|
|
32520
34511
|
}
|
|
@@ -32528,42 +34519,45 @@ var CATEGORY_LABELS = {
|
|
|
32528
34519
|
recent_receipts: "Recent receipts",
|
|
32529
34520
|
verascore_deltas: "Verascore deltas"
|
|
32530
34521
|
};
|
|
32531
|
-
async function runFetcher(match, fetchers) {
|
|
34522
|
+
async function runFetcher(match, fetchers, hints) {
|
|
32532
34523
|
switch (match.category) {
|
|
32533
34524
|
case "templates":
|
|
32534
|
-
return fetchers.templates();
|
|
34525
|
+
return fetchers.templates(hints);
|
|
32535
34526
|
case "agent_state":
|
|
32536
|
-
return fetchers.agent_state(match.agent_name_hint);
|
|
34527
|
+
return fetchers.agent_state(match.agent_name_hint, hints);
|
|
32537
34528
|
case "agent_activity":
|
|
32538
|
-
return fetchers.agent_activity(match.agent_name_hint);
|
|
34529
|
+
return fetchers.agent_activity(match.agent_name_hint, hints);
|
|
32539
34530
|
case "audit_log":
|
|
32540
|
-
return fetchers.audit_log();
|
|
34531
|
+
return fetchers.audit_log(hints);
|
|
32541
34532
|
case "sentinel_findings":
|
|
32542
|
-
return fetchers.sentinel_findings();
|
|
34533
|
+
return fetchers.sentinel_findings(hints);
|
|
32543
34534
|
case "anomaly_alerts":
|
|
32544
|
-
return fetchers.anomaly_alerts();
|
|
34535
|
+
return fetchers.anomaly_alerts(hints);
|
|
32545
34536
|
case "recent_receipts":
|
|
32546
|
-
return fetchers.recent_receipts();
|
|
34537
|
+
return fetchers.recent_receipts(hints);
|
|
32547
34538
|
case "verascore_deltas":
|
|
32548
|
-
return fetchers.verascore_deltas();
|
|
34539
|
+
return fetchers.verascore_deltas(hints);
|
|
32549
34540
|
}
|
|
32550
34541
|
}
|
|
32551
|
-
function trivialMatch(category) {
|
|
34542
|
+
function trivialMatch(category, parsedGrammar) {
|
|
32552
34543
|
return {
|
|
32553
34544
|
category,
|
|
32554
34545
|
confidence: 0.5,
|
|
32555
34546
|
matched_keywords: ["llm-assist"],
|
|
32556
|
-
agent_name_hint: null
|
|
34547
|
+
agent_name_hint: parsedGrammar && parsedGrammar.agent_names.length > 0 ? parsedGrammar.agent_names[0] ?? null : null,
|
|
34548
|
+
...parsedGrammar !== void 0 ? { query_grammar: parsedGrammar } : {}
|
|
32557
34549
|
};
|
|
32558
34550
|
}
|
|
32559
34551
|
async function foldContext(query, fetchers, opts) {
|
|
32560
34552
|
const budget = opts?.maxTokens ?? DEFAULT_DYNAMIC_CONTEXT_TOKEN_BUDGET;
|
|
32561
|
-
|
|
34553
|
+
const parsed = opts?.parsed ?? null;
|
|
34554
|
+
const hints = fetcherHintsFromGrammar(parsed);
|
|
34555
|
+
let matches = classifyQuery(query, parsed);
|
|
32562
34556
|
if (matches.length === 0 && !isTrivialQuery(query) && opts?.llmAssistClassify) {
|
|
32563
34557
|
try {
|
|
32564
34558
|
const picked = await opts.llmAssistClassify(query, CONTEXT_CATEGORIES);
|
|
32565
34559
|
if (picked !== "none" && CONTEXT_CATEGORIES.includes(picked)) {
|
|
32566
|
-
matches = [trivialMatch(picked)];
|
|
34560
|
+
matches = [trivialMatch(picked, parsed)];
|
|
32567
34561
|
}
|
|
32568
34562
|
} catch {
|
|
32569
34563
|
}
|
|
@@ -32574,7 +34568,7 @@ async function foldContext(query, fetchers, opts) {
|
|
|
32574
34568
|
const attempts = [];
|
|
32575
34569
|
for (const match of matches) {
|
|
32576
34570
|
try {
|
|
32577
|
-
const text = await runFetcher(match, fetchers);
|
|
34571
|
+
const text = await runFetcher(match, fetchers, hints);
|
|
32578
34572
|
const trimmed = text.trim();
|
|
32579
34573
|
if (trimmed.length > 0) {
|
|
32580
34574
|
attempts.push({ category: match.category, text: trimmed });
|
|
@@ -32616,6 +34610,614 @@ ${blocks.join("\n\n")}`;
|
|
|
32616
34610
|
};
|
|
32617
34611
|
}
|
|
32618
34612
|
|
|
34613
|
+
// src/composition/constants.ts
|
|
34614
|
+
var COMPOSITION_EVENT_TYPES = [
|
|
34615
|
+
"composition_receipt_packed",
|
|
34616
|
+
"composition_receipt_verified",
|
|
34617
|
+
"composition_mandate_verified",
|
|
34618
|
+
"composition_verascore_published",
|
|
34619
|
+
"composition_sidecar_spawned",
|
|
34620
|
+
"composition_sidecar_crashed",
|
|
34621
|
+
"composition_sidecar_recovered",
|
|
34622
|
+
"composition_degraded",
|
|
34623
|
+
"composition_recovered"
|
|
34624
|
+
];
|
|
34625
|
+
|
|
34626
|
+
// src/chat/concierge-query-grammar.ts
|
|
34627
|
+
var CANONICAL_AUDIT_EVENT_CLASSES = [
|
|
34628
|
+
// Lifecycle / policy
|
|
34629
|
+
"policy_change",
|
|
34630
|
+
"approval_request",
|
|
34631
|
+
"audit_truncate",
|
|
34632
|
+
"lockdown",
|
|
34633
|
+
"unwrap",
|
|
34634
|
+
// Exit bundle (Tier 1)
|
|
34635
|
+
"exit_bundle_export",
|
|
34636
|
+
"exit_bundle_import_activate",
|
|
34637
|
+
"exit_bundle_rekey",
|
|
34638
|
+
// Cross-harness approval aggregator
|
|
34639
|
+
"cross_harness_approval_aggregated",
|
|
34640
|
+
"cross_harness_approval_resolved",
|
|
34641
|
+
"cross_harness_approval_deduped",
|
|
34642
|
+
"cross_harness_approval_payload_decrypted",
|
|
34643
|
+
"cross_harness_approval_audit_trail_viewed",
|
|
34644
|
+
"cross_harness_approval_replayed",
|
|
34645
|
+
// Composition (full set from constants.ts)
|
|
34646
|
+
...COMPOSITION_EVENT_TYPES,
|
|
34647
|
+
// Operator chat / concierge (full set from OPERATOR_CHAT_OPS)
|
|
34648
|
+
OPERATOR_CHAT_OPS.CONCIERGE_CHAT,
|
|
34649
|
+
OPERATOR_CHAT_OPS.AGENT_INSPECT_PANEL_OPENED,
|
|
34650
|
+
OPERATOR_CHAT_OPS.CONCIERGE_HISTORY_READ,
|
|
34651
|
+
OPERATOR_CHAT_OPS.CONCIERGE_THREAD_DELETED,
|
|
34652
|
+
OPERATOR_CHAT_OPS.CONCIERGE_MEMORY_READ_FAILED,
|
|
34653
|
+
OPERATOR_CHAT_OPS.CONCIERGE_CONTEXT_FETCHER_FAILED,
|
|
34654
|
+
// Bridge / commitment
|
|
34655
|
+
"bridge_commit",
|
|
34656
|
+
"bridge_verify",
|
|
34657
|
+
"bridge_attest",
|
|
34658
|
+
"proof_commitment",
|
|
34659
|
+
"proof_reveal",
|
|
34660
|
+
// Reputation
|
|
34661
|
+
"reputation_export",
|
|
34662
|
+
"reputation_import",
|
|
34663
|
+
"reputation_publish",
|
|
34664
|
+
"reputation_record",
|
|
34665
|
+
"reputation_query"
|
|
34666
|
+
];
|
|
34667
|
+
var EVENT_SYNONYMS = [
|
|
34668
|
+
{ phrase: "approvals", canonical: ["approval_request", "cross_harness_approval_aggregated", "cross_harness_approval_resolved"] },
|
|
34669
|
+
{ phrase: "approval", canonical: ["approval_request"] },
|
|
34670
|
+
{ phrase: "policy changes", canonical: ["policy_change"] },
|
|
34671
|
+
{ phrase: "policy change", canonical: ["policy_change"] },
|
|
34672
|
+
{ phrase: "policy edits", canonical: ["policy_change"] },
|
|
34673
|
+
{ phrase: "lockdowns", canonical: ["lockdown"] },
|
|
34674
|
+
{ phrase: "exit bundles", canonical: ["exit_bundle_export", "exit_bundle_import_activate"] },
|
|
34675
|
+
{ phrase: "exit bundle", canonical: ["exit_bundle_export"] },
|
|
34676
|
+
{ phrase: "audit truncations", canonical: ["audit_truncate"] },
|
|
34677
|
+
{ phrase: "audit truncation", canonical: ["audit_truncate"] },
|
|
34678
|
+
{ phrase: "compositions", canonical: ["composition_receipt_packed", "composition_receipt_verified"] },
|
|
34679
|
+
{ phrase: "receipts", canonical: ["composition_receipt_packed", "composition_receipt_verified"] },
|
|
34680
|
+
{ phrase: "receipt verifications", canonical: ["composition_receipt_verified"] },
|
|
34681
|
+
{ phrase: "concierge chats", canonical: [OPERATOR_CHAT_OPS.CONCIERGE_CHAT] },
|
|
34682
|
+
{ phrase: "cross harness approvals", canonical: ["cross_harness_approval_aggregated", "cross_harness_approval_resolved"] }
|
|
34683
|
+
];
|
|
34684
|
+
var MS_PER_HOUR = 60 * 60 * 1e3;
|
|
34685
|
+
var MS_PER_DAY = 24 * MS_PER_HOUR;
|
|
34686
|
+
var NUMBER_WORDS = {
|
|
34687
|
+
a: 1,
|
|
34688
|
+
an: 1,
|
|
34689
|
+
one: 1,
|
|
34690
|
+
two: 2,
|
|
34691
|
+
three: 3,
|
|
34692
|
+
four: 4,
|
|
34693
|
+
five: 5,
|
|
34694
|
+
six: 6,
|
|
34695
|
+
seven: 7,
|
|
34696
|
+
eight: 8,
|
|
34697
|
+
nine: 9,
|
|
34698
|
+
ten: 10,
|
|
34699
|
+
twelve: 12,
|
|
34700
|
+
twentyfour: 24
|
|
34701
|
+
};
|
|
34702
|
+
function resolveTimeRange(query, now) {
|
|
34703
|
+
const normalized = query.trim();
|
|
34704
|
+
const lower = normalized.toLowerCase();
|
|
34705
|
+
const fromTo = lower.match(
|
|
34706
|
+
/\b(?:from|between)\s+(.+?)\s+(?:to|and|-|until)\s+([\w:.\-+t /]+)/i
|
|
34707
|
+
);
|
|
34708
|
+
if (fromTo) {
|
|
34709
|
+
const aSlice = fromTo[1];
|
|
34710
|
+
const bSlice = fromTo[2];
|
|
34711
|
+
if (aSlice !== void 0 && bSlice !== void 0) {
|
|
34712
|
+
const a = parseInstant(aSlice, now);
|
|
34713
|
+
const b = parseInstant(bSlice, now);
|
|
34714
|
+
if (a && b) {
|
|
34715
|
+
const start = a.getTime() <= b.getTime() ? a : b;
|
|
34716
|
+
const end = a.getTime() <= b.getTime() ? b : a;
|
|
34717
|
+
return {
|
|
34718
|
+
range: { start, end },
|
|
34719
|
+
matchedSubstring: fromTo[0]
|
|
34720
|
+
};
|
|
34721
|
+
}
|
|
34722
|
+
}
|
|
34723
|
+
}
|
|
34724
|
+
const sinceMatch = lower.match(/\bsince\s+([\w:.\-+t /]+)/i);
|
|
34725
|
+
if (sinceMatch) {
|
|
34726
|
+
const slice = sinceMatch[1];
|
|
34727
|
+
if (slice !== void 0) {
|
|
34728
|
+
const start = parseInstant(slice, now);
|
|
34729
|
+
if (start) {
|
|
34730
|
+
return {
|
|
34731
|
+
range: { start, end: now },
|
|
34732
|
+
matchedSubstring: sinceMatch[0]
|
|
34733
|
+
};
|
|
34734
|
+
}
|
|
34735
|
+
}
|
|
34736
|
+
}
|
|
34737
|
+
if (/\byesterday\b/.test(lower)) {
|
|
34738
|
+
const startOfToday = startOfDay(now);
|
|
34739
|
+
const start = new Date(startOfToday.getTime() - MS_PER_DAY);
|
|
34740
|
+
const end = new Date(startOfToday.getTime() - 1);
|
|
34741
|
+
return {
|
|
34742
|
+
range: { start, end, relative_label: "yesterday" },
|
|
34743
|
+
matchedSubstring: "yesterday"
|
|
34744
|
+
};
|
|
34745
|
+
}
|
|
34746
|
+
if (/\btoday\b/.test(lower)) {
|
|
34747
|
+
return {
|
|
34748
|
+
range: {
|
|
34749
|
+
start: startOfDay(now),
|
|
34750
|
+
end: now,
|
|
34751
|
+
relative_label: "today"
|
|
34752
|
+
},
|
|
34753
|
+
matchedSubstring: "today"
|
|
34754
|
+
};
|
|
34755
|
+
}
|
|
34756
|
+
const compactHours = lower.match(/\blast\s+(\d+)\s*h\b/i);
|
|
34757
|
+
if (compactHours) {
|
|
34758
|
+
const tok = compactHours[1];
|
|
34759
|
+
if (tok !== void 0) {
|
|
34760
|
+
const n = Number.parseInt(tok, 10);
|
|
34761
|
+
if (Number.isFinite(n) && n > 0) {
|
|
34762
|
+
const start = new Date(now.getTime() - n * MS_PER_HOUR);
|
|
34763
|
+
return {
|
|
34764
|
+
range: { start, end: now, relative_label: `last ${n}h` },
|
|
34765
|
+
matchedSubstring: compactHours[0]
|
|
34766
|
+
};
|
|
34767
|
+
}
|
|
34768
|
+
}
|
|
34769
|
+
}
|
|
34770
|
+
const hoursMatch = lower.match(
|
|
34771
|
+
/\b(?:past|last)\s+([\w]+|\d+)\s*(?:hr\b|hrs\b|hour|hours)/i
|
|
34772
|
+
);
|
|
34773
|
+
if (hoursMatch) {
|
|
34774
|
+
const tok = hoursMatch[1];
|
|
34775
|
+
if (tok !== void 0) {
|
|
34776
|
+
const n = parseCount(tok);
|
|
34777
|
+
if (n !== null && n > 0) {
|
|
34778
|
+
const start = new Date(now.getTime() - n * MS_PER_HOUR);
|
|
34779
|
+
return {
|
|
34780
|
+
range: { start, end: now, relative_label: `past ${n} hour${n === 1 ? "" : "s"}` },
|
|
34781
|
+
matchedSubstring: hoursMatch[0]
|
|
34782
|
+
};
|
|
34783
|
+
}
|
|
34784
|
+
}
|
|
34785
|
+
}
|
|
34786
|
+
if (/\b(?:past|last)\s+hour\b/.test(lower)) {
|
|
34787
|
+
const start = new Date(now.getTime() - MS_PER_HOUR);
|
|
34788
|
+
return {
|
|
34789
|
+
range: { start, end: now, relative_label: "past hour" },
|
|
34790
|
+
matchedSubstring: lower.match(/\b(?:past|last)\s+hour\b/i)[0]
|
|
34791
|
+
};
|
|
34792
|
+
}
|
|
34793
|
+
const daysMatch = lower.match(
|
|
34794
|
+
/\b(?:past|last)\s+([\w]+|\d+)\s*(?:d\b|day|days)/i
|
|
34795
|
+
);
|
|
34796
|
+
if (daysMatch) {
|
|
34797
|
+
const tok = daysMatch[1];
|
|
34798
|
+
if (tok !== void 0) {
|
|
34799
|
+
const n = parseCount(tok);
|
|
34800
|
+
if (n !== null && n > 0) {
|
|
34801
|
+
const start = new Date(now.getTime() - n * MS_PER_DAY);
|
|
34802
|
+
return {
|
|
34803
|
+
range: { start, end: now, relative_label: `past ${n} day${n === 1 ? "" : "s"}` },
|
|
34804
|
+
matchedSubstring: daysMatch[0]
|
|
34805
|
+
};
|
|
34806
|
+
}
|
|
34807
|
+
}
|
|
34808
|
+
}
|
|
34809
|
+
if (/\b(?:past|last)\s+day\b/.test(lower)) {
|
|
34810
|
+
const start = new Date(now.getTime() - MS_PER_DAY);
|
|
34811
|
+
return {
|
|
34812
|
+
range: { start, end: now, relative_label: "past day" },
|
|
34813
|
+
matchedSubstring: lower.match(/\b(?:past|last)\s+day\b/i)[0]
|
|
34814
|
+
};
|
|
34815
|
+
}
|
|
34816
|
+
if (/\bthis\s+week\b/.test(lower)) {
|
|
34817
|
+
const start = startOfWeek(now);
|
|
34818
|
+
return {
|
|
34819
|
+
range: { start, end: now, relative_label: "this week" },
|
|
34820
|
+
matchedSubstring: lower.match(/\bthis\s+week\b/i)[0]
|
|
34821
|
+
};
|
|
34822
|
+
}
|
|
34823
|
+
if (/\b(?:past|last)\s+week\b/.test(lower)) {
|
|
34824
|
+
const start = new Date(now.getTime() - 7 * MS_PER_DAY);
|
|
34825
|
+
return {
|
|
34826
|
+
range: { start, end: now, relative_label: "past week" },
|
|
34827
|
+
matchedSubstring: lower.match(/\b(?:past|last)\s+week\b/i)[0]
|
|
34828
|
+
};
|
|
34829
|
+
}
|
|
34830
|
+
const isoMatch = normalized.match(
|
|
34831
|
+
/\b(\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2})?(?:Z|[+-]\d{2}:\d{2})?)?)\b/
|
|
34832
|
+
);
|
|
34833
|
+
if (isoMatch) {
|
|
34834
|
+
const tok = isoMatch[1];
|
|
34835
|
+
if (tok !== void 0) {
|
|
34836
|
+
const parsed = parseInstant(tok, now);
|
|
34837
|
+
if (parsed) {
|
|
34838
|
+
const isDateOnly = /^\d{4}-\d{2}-\d{2}$/.test(tok);
|
|
34839
|
+
if (isDateOnly) {
|
|
34840
|
+
return {
|
|
34841
|
+
range: {
|
|
34842
|
+
start: parsed,
|
|
34843
|
+
end: new Date(parsed.getTime() + MS_PER_DAY - 1)
|
|
34844
|
+
},
|
|
34845
|
+
matchedSubstring: tok
|
|
34846
|
+
};
|
|
34847
|
+
}
|
|
34848
|
+
return {
|
|
34849
|
+
range: {
|
|
34850
|
+
start: new Date(parsed.getTime() - 30 * 60 * 1e3),
|
|
34851
|
+
end: new Date(parsed.getTime() + 30 * 60 * 1e3)
|
|
34852
|
+
},
|
|
34853
|
+
matchedSubstring: tok
|
|
34854
|
+
};
|
|
34855
|
+
}
|
|
34856
|
+
}
|
|
34857
|
+
}
|
|
34858
|
+
return null;
|
|
34859
|
+
}
|
|
34860
|
+
function parseInstant(token, now) {
|
|
34861
|
+
const trimmed = token.trim().replace(/[,.!?;]+$/g, "");
|
|
34862
|
+
if (!trimmed) return null;
|
|
34863
|
+
const lower = trimmed.toLowerCase();
|
|
34864
|
+
if (lower === "now") return now;
|
|
34865
|
+
if (lower === "today") return startOfDay(now);
|
|
34866
|
+
if (lower === "yesterday") {
|
|
34867
|
+
return new Date(startOfDay(now).getTime() - MS_PER_DAY);
|
|
34868
|
+
}
|
|
34869
|
+
const isoLike = trimmed.replace(" ", "T");
|
|
34870
|
+
const parsed = new Date(isoLike);
|
|
34871
|
+
if (!Number.isNaN(parsed.getTime())) return parsed;
|
|
34872
|
+
return null;
|
|
34873
|
+
}
|
|
34874
|
+
function parseCount(token) {
|
|
34875
|
+
const lower = token.toLowerCase();
|
|
34876
|
+
if (/^\d+$/.test(lower)) {
|
|
34877
|
+
const n = Number.parseInt(lower, 10);
|
|
34878
|
+
return Number.isFinite(n) ? n : null;
|
|
34879
|
+
}
|
|
34880
|
+
return NUMBER_WORDS[lower] ?? null;
|
|
34881
|
+
}
|
|
34882
|
+
function startOfDay(d) {
|
|
34883
|
+
const out = new Date(d);
|
|
34884
|
+
out.setHours(0, 0, 0, 0);
|
|
34885
|
+
return out;
|
|
34886
|
+
}
|
|
34887
|
+
function startOfWeek(d) {
|
|
34888
|
+
const out = startOfDay(d);
|
|
34889
|
+
const dayOfWeek = out.getDay();
|
|
34890
|
+
const offsetToMonday = (dayOfWeek + 6) % 7;
|
|
34891
|
+
out.setDate(out.getDate() - offsetToMonday);
|
|
34892
|
+
return out;
|
|
34893
|
+
}
|
|
34894
|
+
function listFromRegistry(registry) {
|
|
34895
|
+
if (!registry) return [];
|
|
34896
|
+
if (Array.isArray(registry)) return registry;
|
|
34897
|
+
if (typeof registry.list === "function") {
|
|
34898
|
+
return registry.list();
|
|
34899
|
+
}
|
|
34900
|
+
return [];
|
|
34901
|
+
}
|
|
34902
|
+
function extractAgentNames(query, registry) {
|
|
34903
|
+
const records = listFromRegistry(registry);
|
|
34904
|
+
if (records.length === 0) return { matched: [], flagged: false };
|
|
34905
|
+
const lowerQuery = query.toLowerCase();
|
|
34906
|
+
const compactQuery = lowerQuery.replace(/[\s_-]+/g, "");
|
|
34907
|
+
const matched = [];
|
|
34908
|
+
const seen = /* @__PURE__ */ new Set();
|
|
34909
|
+
for (const rec of records) {
|
|
34910
|
+
const id = rec.agent_id;
|
|
34911
|
+
if (!id || seen.has(id)) continue;
|
|
34912
|
+
const idLower = id.toLowerCase();
|
|
34913
|
+
if (idLower.length < 3) continue;
|
|
34914
|
+
const idCompact = idLower.replace(/[\s_-]+/g, "");
|
|
34915
|
+
const wordRe = new RegExp(
|
|
34916
|
+
`\\b${idLower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
|
|
34917
|
+
"i"
|
|
34918
|
+
);
|
|
34919
|
+
if (wordRe.test(query)) {
|
|
34920
|
+
matched.push(id);
|
|
34921
|
+
seen.add(id);
|
|
34922
|
+
continue;
|
|
34923
|
+
}
|
|
34924
|
+
if (idCompact.length >= 4 && compactQuery.includes(idCompact)) {
|
|
34925
|
+
matched.push(id);
|
|
34926
|
+
seen.add(id);
|
|
34927
|
+
}
|
|
34928
|
+
}
|
|
34929
|
+
const agentMention = lowerQuery.match(/\bagent\s+([a-z0-9_-]{3,40})/i);
|
|
34930
|
+
const flagged = matched.length === 0 && agentMention !== null && agentMention[1] !== void 0 && !records.some((r) => r.agent_id.toLowerCase() === agentMention[1]?.toLowerCase());
|
|
34931
|
+
return { matched, flagged };
|
|
34932
|
+
}
|
|
34933
|
+
function escapeRegex(s) {
|
|
34934
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
34935
|
+
}
|
|
34936
|
+
function extractEventTypes(query, enumValues) {
|
|
34937
|
+
const lower = query.toLowerCase();
|
|
34938
|
+
const matched = [];
|
|
34939
|
+
const seen = /* @__PURE__ */ new Set();
|
|
34940
|
+
for (const ev of enumValues) {
|
|
34941
|
+
if (seen.has(ev)) continue;
|
|
34942
|
+
const re = new RegExp(`\\b${escapeRegex(ev)}\\b`, "i");
|
|
34943
|
+
if (re.test(query)) {
|
|
34944
|
+
matched.push(ev);
|
|
34945
|
+
seen.add(ev);
|
|
34946
|
+
}
|
|
34947
|
+
}
|
|
34948
|
+
for (const syn of EVENT_SYNONYMS) {
|
|
34949
|
+
const re = new RegExp(
|
|
34950
|
+
`\\b${syn.phrase.split(/\s+/).map(escapeRegex).join("\\s+")}\\b`,
|
|
34951
|
+
"i"
|
|
34952
|
+
);
|
|
34953
|
+
if (re.test(query)) {
|
|
34954
|
+
for (const c of syn.canonical) {
|
|
34955
|
+
if (seen.has(c)) continue;
|
|
34956
|
+
if (!enumValues.includes(c)) continue;
|
|
34957
|
+
matched.push(c);
|
|
34958
|
+
seen.add(c);
|
|
34959
|
+
}
|
|
34960
|
+
}
|
|
34961
|
+
}
|
|
34962
|
+
const globMatches = lower.match(/\b([a-z_]+)_\*/g) ?? [];
|
|
34963
|
+
for (const glob of globMatches) {
|
|
34964
|
+
const prefix = glob.slice(0, -2);
|
|
34965
|
+
for (const ev of enumValues) {
|
|
34966
|
+
if (seen.has(ev)) continue;
|
|
34967
|
+
if (ev.startsWith(prefix)) {
|
|
34968
|
+
matched.push(ev);
|
|
34969
|
+
seen.add(ev);
|
|
34970
|
+
}
|
|
34971
|
+
}
|
|
34972
|
+
}
|
|
34973
|
+
const eventNounMention = /\b(?:event|events|class|classes)\b/i.test(query) && matched.length === 0;
|
|
34974
|
+
return { matched, flagged: eventNounMention };
|
|
34975
|
+
}
|
|
34976
|
+
function deriveIntentPhrase(query, stripTokens) {
|
|
34977
|
+
let out = query;
|
|
34978
|
+
for (const tok of stripTokens) {
|
|
34979
|
+
if (!tok) continue;
|
|
34980
|
+
const re = new RegExp(escapeRegex(tok), "gi");
|
|
34981
|
+
out = out.replace(re, " ");
|
|
34982
|
+
}
|
|
34983
|
+
return out.replace(/\s+/g, " ").trim();
|
|
34984
|
+
}
|
|
34985
|
+
function computeConfidence(parsed) {
|
|
34986
|
+
const dims = [
|
|
34987
|
+
{ present: parsed.hasTimeMention, resolved: parsed.timeResolved },
|
|
34988
|
+
{ present: parsed.hasAgentMention, resolved: parsed.agentResolved },
|
|
34989
|
+
{ present: parsed.hasEventMention, resolved: parsed.eventResolved }
|
|
34990
|
+
];
|
|
34991
|
+
const present = dims.filter((d) => d.present);
|
|
34992
|
+
let base;
|
|
34993
|
+
if (present.length === 0) {
|
|
34994
|
+
base = parsed.intentEmpty ? 0 : 0.3;
|
|
34995
|
+
} else {
|
|
34996
|
+
const resolved = present.filter((d) => d.resolved).length;
|
|
34997
|
+
base = resolved / present.length;
|
|
34998
|
+
}
|
|
34999
|
+
const adjusted = base - 0.15 * parsed.ambiguityCount;
|
|
35000
|
+
if (adjusted < 0) return 0;
|
|
35001
|
+
if (adjusted > 1) return 1;
|
|
35002
|
+
return adjusted;
|
|
35003
|
+
}
|
|
35004
|
+
var TIME_MENTION_PROBE = /\b(yesterday|today|now|past|last|this\s+week|this\s+month|since|from|between|\d{4}-\d{2}-\d{2})\b/i;
|
|
35005
|
+
var AGENT_MENTION_PROBE = /\bagent[s]?\b/i;
|
|
35006
|
+
var EVENT_MENTION_PROBE = /\b(event|events|class|classes|approvals?|policy)\b/i;
|
|
35007
|
+
function parseQuery(query, opts) {
|
|
35008
|
+
const now = opts?.now ?? /* @__PURE__ */ new Date();
|
|
35009
|
+
const enumValues = opts?.eventClassEnum ?? CANONICAL_AUDIT_EVENT_CLASSES;
|
|
35010
|
+
const original = query ?? "";
|
|
35011
|
+
const trimmed = original.trim();
|
|
35012
|
+
if (trimmed.length === 0) {
|
|
35013
|
+
return {
|
|
35014
|
+
time_range: null,
|
|
35015
|
+
agent_names: [],
|
|
35016
|
+
event_types: [],
|
|
35017
|
+
intent_phrase: "",
|
|
35018
|
+
ambiguity_flags: ["no_signal_extracted"],
|
|
35019
|
+
parse_confidence: 0
|
|
35020
|
+
};
|
|
35021
|
+
}
|
|
35022
|
+
const ambiguity_flags = /* @__PURE__ */ new Set();
|
|
35023
|
+
const timeMatch = resolveTimeRange(trimmed, now);
|
|
35024
|
+
const hasTimeMention = TIME_MENTION_PROBE.test(trimmed);
|
|
35025
|
+
if (hasTimeMention && !timeMatch) {
|
|
35026
|
+
ambiguity_flags.add("unknown_time_token");
|
|
35027
|
+
}
|
|
35028
|
+
const agentResult = extractAgentNames(trimmed, opts?.registry);
|
|
35029
|
+
if (agentResult.flagged) {
|
|
35030
|
+
ambiguity_flags.add("unknown_agent_token");
|
|
35031
|
+
}
|
|
35032
|
+
const hasAgentMention = AGENT_MENTION_PROBE.test(trimmed);
|
|
35033
|
+
const eventResult = extractEventTypes(trimmed, enumValues);
|
|
35034
|
+
const hasEventMention = EVENT_MENTION_PROBE.test(trimmed);
|
|
35035
|
+
if (eventResult.flagged) {
|
|
35036
|
+
ambiguity_flags.add("unknown_event_token");
|
|
35037
|
+
}
|
|
35038
|
+
const stripTokens = [];
|
|
35039
|
+
if (timeMatch) stripTokens.push(timeMatch.matchedSubstring);
|
|
35040
|
+
for (const name of agentResult.matched) stripTokens.push(name);
|
|
35041
|
+
for (const ev of eventResult.matched) {
|
|
35042
|
+
if (trimmed.toLowerCase().includes(ev.toLowerCase())) {
|
|
35043
|
+
stripTokens.push(ev);
|
|
35044
|
+
}
|
|
35045
|
+
}
|
|
35046
|
+
const intent_phrase = deriveIntentPhrase(trimmed, stripTokens);
|
|
35047
|
+
const parse_confidence = computeConfidence({
|
|
35048
|
+
hasTimeMention,
|
|
35049
|
+
timeResolved: timeMatch !== null,
|
|
35050
|
+
hasAgentMention,
|
|
35051
|
+
agentResolved: agentResult.matched.length > 0,
|
|
35052
|
+
hasEventMention,
|
|
35053
|
+
eventResolved: eventResult.matched.length > 0,
|
|
35054
|
+
intentEmpty: intent_phrase.length === 0,
|
|
35055
|
+
ambiguityCount: ambiguity_flags.size
|
|
35056
|
+
});
|
|
35057
|
+
if (timeMatch === null && agentResult.matched.length === 0 && eventResult.matched.length === 0 && intent_phrase.length === 0) {
|
|
35058
|
+
ambiguity_flags.add("no_signal_extracted");
|
|
35059
|
+
}
|
|
35060
|
+
return {
|
|
35061
|
+
time_range: timeMatch ? timeMatch.range : null,
|
|
35062
|
+
agent_names: agentResult.matched,
|
|
35063
|
+
event_types: eventResult.matched,
|
|
35064
|
+
intent_phrase,
|
|
35065
|
+
ambiguity_flags: Array.from(ambiguity_flags),
|
|
35066
|
+
parse_confidence
|
|
35067
|
+
};
|
|
35068
|
+
}
|
|
35069
|
+
var LLM_ASSIST_THRESHOLD = 0.5;
|
|
35070
|
+
function isLowConfidence(parsed) {
|
|
35071
|
+
return parsed.parse_confidence < LLM_ASSIST_THRESHOLD;
|
|
35072
|
+
}
|
|
35073
|
+
async function parseQueryWithLlmAssist(query, llmAssist, opts) {
|
|
35074
|
+
const parsed = parseQuery(query, opts);
|
|
35075
|
+
if (!llmAssist || !isLowConfidence(parsed)) return parsed;
|
|
35076
|
+
let completion;
|
|
35077
|
+
try {
|
|
35078
|
+
completion = await llmAssist(query, parsed);
|
|
35079
|
+
} catch {
|
|
35080
|
+
return parsed;
|
|
35081
|
+
}
|
|
35082
|
+
if (!completion || typeof completion !== "object") return parsed;
|
|
35083
|
+
const merged = { ...parsed };
|
|
35084
|
+
if (parsed.time_range === null && completion.time_range) {
|
|
35085
|
+
merged.time_range = completion.time_range;
|
|
35086
|
+
}
|
|
35087
|
+
if (parsed.agent_names.length === 0 && Array.isArray(completion.agent_names)) {
|
|
35088
|
+
merged.agent_names = completion.agent_names.filter(
|
|
35089
|
+
(s) => typeof s === "string" && s.length > 0
|
|
35090
|
+
);
|
|
35091
|
+
}
|
|
35092
|
+
if (parsed.event_types.length === 0 && Array.isArray(completion.event_types)) {
|
|
35093
|
+
const allowed = new Set(opts?.eventClassEnum ?? CANONICAL_AUDIT_EVENT_CLASSES);
|
|
35094
|
+
merged.event_types = completion.event_types.filter(
|
|
35095
|
+
(s) => typeof s === "string" && allowed.has(s)
|
|
35096
|
+
);
|
|
35097
|
+
}
|
|
35098
|
+
merged.parse_confidence = Math.max(
|
|
35099
|
+
parsed.parse_confidence,
|
|
35100
|
+
computeConfidence({
|
|
35101
|
+
hasTimeMention: TIME_MENTION_PROBE.test(query),
|
|
35102
|
+
timeResolved: merged.time_range !== null,
|
|
35103
|
+
hasAgentMention: AGENT_MENTION_PROBE.test(query),
|
|
35104
|
+
agentResolved: merged.agent_names.length > 0,
|
|
35105
|
+
hasEventMention: EVENT_MENTION_PROBE.test(query),
|
|
35106
|
+
eventResolved: merged.event_types.length > 0,
|
|
35107
|
+
intentEmpty: merged.intent_phrase.length === 0,
|
|
35108
|
+
ambiguityCount: merged.ambiguity_flags.length
|
|
35109
|
+
})
|
|
35110
|
+
);
|
|
35111
|
+
return merged;
|
|
35112
|
+
}
|
|
35113
|
+
function auditSafeSummary(parsed) {
|
|
35114
|
+
return {
|
|
35115
|
+
time_range: parsed.time_range ? {
|
|
35116
|
+
start_iso: parsed.time_range.start.toISOString(),
|
|
35117
|
+
end_iso: parsed.time_range.end.toISOString(),
|
|
35118
|
+
...parsed.time_range.relative_label !== void 0 ? { relative_label: parsed.time_range.relative_label } : {}
|
|
35119
|
+
} : null,
|
|
35120
|
+
agent_names: [...parsed.agent_names],
|
|
35121
|
+
event_types: [...parsed.event_types],
|
|
35122
|
+
ambiguity_flags: [...parsed.ambiguity_flags],
|
|
35123
|
+
parse_confidence: parsed.parse_confidence
|
|
35124
|
+
};
|
|
35125
|
+
}
|
|
35126
|
+
|
|
35127
|
+
// src/chat/agent-context-cache.ts
|
|
35128
|
+
var STATE_FLAG_ORDER = [
|
|
35129
|
+
"stuck",
|
|
35130
|
+
"has_pending_approvals",
|
|
35131
|
+
"has_open_findings",
|
|
35132
|
+
"active",
|
|
35133
|
+
"idle"
|
|
35134
|
+
];
|
|
35135
|
+
var SECTION_HEADER = "## Current agent state";
|
|
35136
|
+
function approxTokenLen2(text) {
|
|
35137
|
+
return Math.ceil(text.length / 4);
|
|
35138
|
+
}
|
|
35139
|
+
var DEFAULT_AGENT_CONTEXT_TOKEN_BUDGET = 400;
|
|
35140
|
+
function formatSnapshotLine(snapshot) {
|
|
35141
|
+
const flagLabel = snapshot.state_flags.join("+") || "no_flags";
|
|
35142
|
+
const work = snapshot.current_work_summary ? `, last: ${snapshot.current_work_summary}` : "";
|
|
35143
|
+
const verascore = snapshot.recent_verascore_delta_24h !== null ? `, verascore \u0394${snapshot.recent_verascore_delta_24h.toFixed(2)}` : "";
|
|
35144
|
+
return `- ${snapshot.agent_name} (template: ${snapshot.template}): ${flagLabel}, ${snapshot.recent_audit_count_24h} audit/24h, ${snapshot.recent_concordia_receipts_count_24h} receipts${verascore}${work}`;
|
|
35145
|
+
}
|
|
35146
|
+
function urgencyRank(snapshot) {
|
|
35147
|
+
for (let i = 0; i < STATE_FLAG_ORDER.length; i++) {
|
|
35148
|
+
if (snapshot.state_flags.includes(STATE_FLAG_ORDER[i])) {
|
|
35149
|
+
return i;
|
|
35150
|
+
}
|
|
35151
|
+
}
|
|
35152
|
+
return STATE_FLAG_ORDER.length;
|
|
35153
|
+
}
|
|
35154
|
+
function formatCurrentAgentStateSection(snapshots, opts) {
|
|
35155
|
+
if (snapshots.length === 0) return "";
|
|
35156
|
+
const budget = opts?.maxTokens ?? DEFAULT_AGENT_CONTEXT_TOKEN_BUDGET;
|
|
35157
|
+
const sorted = [...snapshots].sort(
|
|
35158
|
+
(a, b) => urgencyRank(a) - urgencyRank(b)
|
|
35159
|
+
);
|
|
35160
|
+
const headerTokens = approxTokenLen2(`${SECTION_HEADER}
|
|
35161
|
+
`);
|
|
35162
|
+
const sepTokens = approxTokenLen2("\n");
|
|
35163
|
+
let runningTokens = headerTokens;
|
|
35164
|
+
const kept = [];
|
|
35165
|
+
for (const snap of sorted) {
|
|
35166
|
+
const line = formatSnapshotLine(snap);
|
|
35167
|
+
const tokens = approxTokenLen2(line) + (kept.length > 0 ? sepTokens : 0);
|
|
35168
|
+
if (kept.length === 0) {
|
|
35169
|
+
kept.push(line);
|
|
35170
|
+
runningTokens += tokens;
|
|
35171
|
+
continue;
|
|
35172
|
+
}
|
|
35173
|
+
if (runningTokens + tokens > budget) break;
|
|
35174
|
+
kept.push(line);
|
|
35175
|
+
runningTokens += tokens;
|
|
35176
|
+
}
|
|
35177
|
+
return `${SECTION_HEADER}
|
|
35178
|
+
${kept.join("\n")}`;
|
|
35179
|
+
}
|
|
35180
|
+
function generateProactiveStarter(snapshots) {
|
|
35181
|
+
if (snapshots.length === 0) return null;
|
|
35182
|
+
const stuck = snapshots.filter((s) => s.state_flags.includes("stuck"));
|
|
35183
|
+
if (stuck.length > 0) {
|
|
35184
|
+
const first = stuck[0];
|
|
35185
|
+
if (first === void 0) return null;
|
|
35186
|
+
const last = first.current_work_summary ? ` (last: ${first.current_work_summary})` : "";
|
|
35187
|
+
return {
|
|
35188
|
+
text: `Your ${first.agent_name} agent looks stuck${last}. Should I check its session state?`,
|
|
35189
|
+
trigger: "stuck_agent",
|
|
35190
|
+
triggered_agents_count: stuck.length
|
|
35191
|
+
};
|
|
35192
|
+
}
|
|
35193
|
+
const pending = snapshots.filter(
|
|
35194
|
+
(s) => s.state_flags.includes("has_pending_approvals")
|
|
35195
|
+
);
|
|
35196
|
+
if (pending.length > 0) {
|
|
35197
|
+
const names = pending.slice(0, 3).map((s) => s.agent_name).join(", ");
|
|
35198
|
+
return {
|
|
35199
|
+
text: `You have pending approvals across ${names}. Want to walk through them?`,
|
|
35200
|
+
trigger: "pending_approvals",
|
|
35201
|
+
triggered_agents_count: pending.length
|
|
35202
|
+
};
|
|
35203
|
+
}
|
|
35204
|
+
const findings = snapshots.filter(
|
|
35205
|
+
(s) => s.state_flags.includes("has_open_findings")
|
|
35206
|
+
);
|
|
35207
|
+
if (findings.length > 0) {
|
|
35208
|
+
return {
|
|
35209
|
+
text: `Sentinel has open findings on ${findings.length} ${findings.length === 1 ? "agent" : "agents"}. Want a summary?`,
|
|
35210
|
+
trigger: "open_findings",
|
|
35211
|
+
triggered_agents_count: findings.length
|
|
35212
|
+
};
|
|
35213
|
+
}
|
|
35214
|
+
return {
|
|
35215
|
+
text: "Your fortress is quiet. Anything you'd like to inspect?",
|
|
35216
|
+
trigger: "all_idle",
|
|
35217
|
+
triggered_agents_count: snapshots.length
|
|
35218
|
+
};
|
|
35219
|
+
}
|
|
35220
|
+
|
|
32619
35221
|
// src/chat/operator-chat-service.ts
|
|
32620
35222
|
var DEFAULT_CONCIERGE_MAX_TOKENS = 512;
|
|
32621
35223
|
var DEFAULT_CONCIERGE_HISTORY_WINDOW_TURNS = 10;
|
|
@@ -32623,7 +35225,8 @@ var DEFAULT_CONCIERGE_HISTORY_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
|
|
|
32623
35225
|
var DEFAULT_CONCIERGE_HISTORY_TOKEN_BUDGET = 500;
|
|
32624
35226
|
var DEFAULT_CONCIERGE_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
32625
35227
|
var DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET = 2e3;
|
|
32626
|
-
|
|
35228
|
+
var DEFAULT_CONCIERGE_AGENT_STATE_BUDGET = 400;
|
|
35229
|
+
function approxTokenLen3(text) {
|
|
32627
35230
|
return Math.ceil(text.length / 4);
|
|
32628
35231
|
}
|
|
32629
35232
|
var SANCTUARY_DOMAIN_REFERENCE = `Castle Architecture (four enforcement layers):
|
|
@@ -32670,6 +35273,17 @@ var OperatorChatService = class {
|
|
|
32670
35273
|
contextFetchers;
|
|
32671
35274
|
contextLlmAssist;
|
|
32672
35275
|
dynamicContextBudget;
|
|
35276
|
+
agentRegistry;
|
|
35277
|
+
grammarLlmAssist;
|
|
35278
|
+
agentContextCache;
|
|
35279
|
+
agentStateBudget;
|
|
35280
|
+
/**
|
|
35281
|
+
* Per-thread guard so the proactive starter fires at most once per
|
|
35282
|
+
* fresh thread. Tracks the thread_id the starter was last offered
|
|
35283
|
+
* for; subsequent `getProactiveStarter()` calls within the same
|
|
35284
|
+
* thread return null instead of re-emitting.
|
|
35285
|
+
*/
|
|
35286
|
+
starterOfferedForThreadId;
|
|
32673
35287
|
/**
|
|
32674
35288
|
* In-memory thread_id assigned to the active concierge session.
|
|
32675
35289
|
* The first sendConcierge call after construction allocates a fresh
|
|
@@ -32708,6 +35322,16 @@ var OperatorChatService = class {
|
|
|
32708
35322
|
this.contextLlmAssist = deps.conciergeContextLlmAssist;
|
|
32709
35323
|
}
|
|
32710
35324
|
this.dynamicContextBudget = deps.conciergeDynamicContextBudget !== void 0 && deps.conciergeDynamicContextBudget > 0 ? deps.conciergeDynamicContextBudget : DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET;
|
|
35325
|
+
if (deps.conciergeAgentRegistry) {
|
|
35326
|
+
this.agentRegistry = deps.conciergeAgentRegistry;
|
|
35327
|
+
}
|
|
35328
|
+
if (deps.conciergeGrammarLlmAssist) {
|
|
35329
|
+
this.grammarLlmAssist = deps.conciergeGrammarLlmAssist;
|
|
35330
|
+
}
|
|
35331
|
+
if (deps.conciergeAgentContextCache) {
|
|
35332
|
+
this.agentContextCache = deps.conciergeAgentContextCache;
|
|
35333
|
+
}
|
|
35334
|
+
this.agentStateBudget = deps.conciergeAgentStateBudget !== void 0 && deps.conciergeAgentStateBudget > 0 ? deps.conciergeAgentStateBudget : DEFAULT_CONCIERGE_AGENT_STATE_BUDGET;
|
|
32711
35335
|
}
|
|
32712
35336
|
// ── Concierge ─────────────────────────────────────────────────────────
|
|
32713
35337
|
/**
|
|
@@ -32729,6 +35353,7 @@ var OperatorChatService = class {
|
|
|
32729
35353
|
const nowMs = this.clock();
|
|
32730
35354
|
if (this.activeMemoryThreadId && this.lastInteractionAt !== void 0 && nowMs - this.lastInteractionAt > this.sessionTtlMs) {
|
|
32731
35355
|
this.activeMemoryThreadId = void 0;
|
|
35356
|
+
this.starterOfferedForThreadId = void 0;
|
|
32732
35357
|
}
|
|
32733
35358
|
const operatorMessage = {
|
|
32734
35359
|
message_id: crypto.randomUUID(),
|
|
@@ -32766,6 +35391,12 @@ var OperatorChatService = class {
|
|
|
32766
35391
|
await this.memory.appendTurn(threadId, "user", trimmed).catch(() => {
|
|
32767
35392
|
});
|
|
32768
35393
|
}
|
|
35394
|
+
const parsedGrammar = await this.runGrammarParse(filterResult.filtered);
|
|
35395
|
+
const agentSnapshots = this.agentContextCache ? this.agentContextCache.read() : [];
|
|
35396
|
+
const agentStateSection = this.agentContextCache ? formatCurrentAgentStateSection(agentSnapshots, {
|
|
35397
|
+
maxTokens: this.agentStateBudget
|
|
35398
|
+
}) : "";
|
|
35399
|
+
const renderedAgentCount = agentStateSection ? agentSnapshots.length : 0;
|
|
32769
35400
|
const start = Date.now();
|
|
32770
35401
|
let conciergeBody;
|
|
32771
35402
|
let servedBy = "disabled";
|
|
@@ -32784,12 +35415,14 @@ var OperatorChatService = class {
|
|
|
32784
35415
|
outcome = "substrate_disabled";
|
|
32785
35416
|
} else {
|
|
32786
35417
|
const dynamicResult = await this.runDynamicContextFold(
|
|
32787
|
-
filterResult.filtered
|
|
35418
|
+
filterResult.filtered,
|
|
35419
|
+
parsedGrammar
|
|
32788
35420
|
);
|
|
32789
35421
|
dynamicCategoriesIncluded = dynamicResult.categoriesIncluded;
|
|
32790
35422
|
const context = await this.assembleConciergeContext(
|
|
32791
35423
|
priorTurns,
|
|
32792
|
-
dynamicResult.section
|
|
35424
|
+
dynamicResult.section,
|
|
35425
|
+
agentStateSection
|
|
32793
35426
|
);
|
|
32794
35427
|
const response = await this.substrateSelector.invokeSummarize(
|
|
32795
35428
|
"concierge",
|
|
@@ -32855,7 +35488,9 @@ var OperatorChatService = class {
|
|
|
32855
35488
|
...this.memory ? {
|
|
32856
35489
|
prior_turns_folded: memoryReadFailureReason === null ? priorTurns.length : 0
|
|
32857
35490
|
} : {},
|
|
32858
|
-
...this.contextFetchers ? { dynamic_context_categories: [...dynamicCategoriesIncluded] } : {}
|
|
35491
|
+
...this.contextFetchers ? { dynamic_context_categories: [...dynamicCategoriesIncluded] } : {},
|
|
35492
|
+
parsed_grammar: auditSafeSummary(parsedGrammar),
|
|
35493
|
+
...this.agentContextCache !== void 0 ? { agent_context_snapshot_count: renderedAgentCount } : {}
|
|
32859
35494
|
};
|
|
32860
35495
|
this.emit(OPERATOR_CHAT_OPS.CONCIERGE_CHAT, payload, outcome === "ok" ? "success" : "failure");
|
|
32861
35496
|
return {
|
|
@@ -32966,6 +35601,7 @@ var OperatorChatService = class {
|
|
|
32966
35601
|
if (!removed) return false;
|
|
32967
35602
|
if (this.activeMemoryThreadId === threadId) {
|
|
32968
35603
|
this.activeMemoryThreadId = void 0;
|
|
35604
|
+
this.starterOfferedForThreadId = void 0;
|
|
32969
35605
|
}
|
|
32970
35606
|
const payload = {
|
|
32971
35607
|
version: "1.2",
|
|
@@ -32984,9 +35620,65 @@ var OperatorChatService = class {
|
|
|
32984
35620
|
* Reset the active session memory thread. Subsequent sendConcierge
|
|
32985
35621
|
* calls allocate a fresh thread_id. Surfaced for tests + future "new
|
|
32986
35622
|
* conversation" affordance; not currently called by the dashboard.
|
|
35623
|
+
*
|
|
35624
|
+
* Tau-5: also clears the proactive-starter guard so the next
|
|
35625
|
+
* `getProactiveStarter()` call against the freshly-allocated thread
|
|
35626
|
+
* is eligible to fire.
|
|
32987
35627
|
*/
|
|
32988
35628
|
resetConciergeMemoryThread() {
|
|
32989
35629
|
this.activeMemoryThreadId = void 0;
|
|
35630
|
+
this.starterOfferedForThreadId = void 0;
|
|
35631
|
+
}
|
|
35632
|
+
/**
|
|
35633
|
+
* WP-V1.3-9 Tau-5: surface a proactive starter for the current
|
|
35634
|
+
* concierge session. Intended to be called by the dashboard UI when
|
|
35635
|
+
* the operator opens the chat surface, before any operator typing.
|
|
35636
|
+
*
|
|
35637
|
+
* Returns null when:
|
|
35638
|
+
* - No agent-context cache is wired (Tau-5 disabled).
|
|
35639
|
+
* - No concierge memory store is wired (no thread_id namespace).
|
|
35640
|
+
* - The cache snapshot has no signal (empty fortress).
|
|
35641
|
+
* - A starter has already been offered for the active thread (the
|
|
35642
|
+
* guard ensures one starter per fresh thread).
|
|
35643
|
+
*
|
|
35644
|
+
* Side effects:
|
|
35645
|
+
* - Allocates a fresh thread_id if none is active.
|
|
35646
|
+
* - Emits the `operator_concierge_proactive_suggestion_offered`
|
|
35647
|
+
* audit event with the trigger class + triggered_agents_count.
|
|
35648
|
+
* - Records the offered thread_id so the next call within the same
|
|
35649
|
+
* thread is a no-op.
|
|
35650
|
+
*
|
|
35651
|
+
* The returned starter's `text` is operator-visible copy; the
|
|
35652
|
+
* dashboard renders it as a system-message-style starter the
|
|
35653
|
+
* operator can accept (clicks/types follow-up) or dismiss (types a
|
|
35654
|
+
* new query).
|
|
35655
|
+
*/
|
|
35656
|
+
getProactiveStarter() {
|
|
35657
|
+
if (!this.agentContextCache) return null;
|
|
35658
|
+
if (!this.memory) return null;
|
|
35659
|
+
const threadId = this.ensureActiveMemoryThread();
|
|
35660
|
+
if (this.starterOfferedForThreadId === threadId) return null;
|
|
35661
|
+
const snapshots = this.agentContextCache.read();
|
|
35662
|
+
const starter = generateProactiveStarter(snapshots);
|
|
35663
|
+
if (!starter) return null;
|
|
35664
|
+
const payload = {
|
|
35665
|
+
version: "1.2",
|
|
35666
|
+
event_id: makeEventId("conc-starter"),
|
|
35667
|
+
emitted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
35668
|
+
identity_id: this.identityId,
|
|
35669
|
+
kind: "operator_concierge_proactive_suggestion_offered",
|
|
35670
|
+
surface: "concierge",
|
|
35671
|
+
thread_id: threadId,
|
|
35672
|
+
trigger: starter.trigger,
|
|
35673
|
+
triggered_agents_count: starter.triggered_agents_count
|
|
35674
|
+
};
|
|
35675
|
+
this.emit(
|
|
35676
|
+
OPERATOR_CHAT_OPS.CONCIERGE_PROACTIVE_SUGGESTION_OFFERED,
|
|
35677
|
+
payload,
|
|
35678
|
+
"success"
|
|
35679
|
+
);
|
|
35680
|
+
this.starterOfferedForThreadId = threadId;
|
|
35681
|
+
return starter;
|
|
32990
35682
|
}
|
|
32991
35683
|
ensureActiveMemoryThread() {
|
|
32992
35684
|
if (!this.activeMemoryThreadId) {
|
|
@@ -33031,7 +35723,7 @@ var OperatorChatService = class {
|
|
|
33031
35723
|
* if available; the v1.2 selector does not expose one, so structured
|
|
33032
35724
|
* serialization is the canonical path for v1.3.
|
|
33033
35725
|
*/
|
|
33034
|
-
async assembleConciergeContext(priorTurns = [], dynamicSection = "") {
|
|
35726
|
+
async assembleConciergeContext(priorTurns = [], dynamicSection = "", agentStateSection = "") {
|
|
33035
35727
|
const ref = `## Sanctuary reference
|
|
33036
35728
|
${SANCTUARY_DOMAIN_REFERENCE}`;
|
|
33037
35729
|
const priorSection = this.formatPriorTurnsSection(priorTurns);
|
|
@@ -33039,6 +35731,7 @@ ${SANCTUARY_DOMAIN_REFERENCE}`;
|
|
|
33039
35731
|
return [
|
|
33040
35732
|
ref,
|
|
33041
35733
|
...dynamicSection ? [dynamicSection] : [],
|
|
35734
|
+
...agentStateSection ? [agentStateSection] : [],
|
|
33042
35735
|
...priorSection ? [priorSection] : [],
|
|
33043
35736
|
"## Recent activity\n(no providers wired)",
|
|
33044
35737
|
"## Wrapped agents\n(no providers wired)",
|
|
@@ -33053,6 +35746,7 @@ ${SANCTUARY_DOMAIN_REFERENCE}`;
|
|
|
33053
35746
|
return [
|
|
33054
35747
|
ref,
|
|
33055
35748
|
...dynamicSection ? [dynamicSection] : [],
|
|
35749
|
+
...agentStateSection ? [agentStateSection] : [],
|
|
33056
35750
|
...priorSection ? [priorSection] : [],
|
|
33057
35751
|
`## Recent activity
|
|
33058
35752
|
${activity}`,
|
|
@@ -33070,8 +35764,12 @@ ${inbox}`
|
|
|
33070
35764
|
* proceeds with no fold. Returns the rendered section + the list of
|
|
33071
35765
|
* categories whose data made it into the section (used for the
|
|
33072
35766
|
* round-trip audit emission).
|
|
35767
|
+
*
|
|
35768
|
+
* Tau-4: receives the pre-parsed `ParsedQuery` and forwards it as the
|
|
35769
|
+
* `parsed` opt to `foldContext`, so fetchers see the structured
|
|
35770
|
+
* `FetcherHints` derived from it.
|
|
33073
35771
|
*/
|
|
33074
|
-
async runDynamicContextFold(query) {
|
|
35772
|
+
async runDynamicContextFold(query, parsedGrammar) {
|
|
33075
35773
|
if (!this.contextFetchers) {
|
|
33076
35774
|
return { section: "", categoriesIncluded: [] };
|
|
33077
35775
|
}
|
|
@@ -33080,10 +35778,24 @@ ${inbox}`
|
|
|
33080
35778
|
...this.contextLlmAssist ? { llmAssistClassify: this.contextLlmAssist } : {},
|
|
33081
35779
|
onFetcherFailure: (category, error) => {
|
|
33082
35780
|
this.emitContextFetcherFailed(category, classifyFetcherError(error));
|
|
33083
|
-
}
|
|
35781
|
+
},
|
|
35782
|
+
parsed: parsedGrammar
|
|
33084
35783
|
});
|
|
33085
35784
|
return result;
|
|
33086
35785
|
}
|
|
35786
|
+
/**
|
|
35787
|
+
* WP-V1.3-9 Tau-4: parse the (PII-filtered) operator query into a
|
|
35788
|
+
* `ParsedQuery`. Routes through the LLM-assist completion hook when
|
|
35789
|
+
* configured and the rule-based parse is below
|
|
35790
|
+
* `LLM_ASSIST_THRESHOLD`. Always returns a parse object (never
|
|
35791
|
+
* throws) so the audit emission can carry the result unconditionally.
|
|
35792
|
+
*/
|
|
35793
|
+
async runGrammarParse(query) {
|
|
35794
|
+
return parseQueryWithLlmAssist(query, this.grammarLlmAssist, {
|
|
35795
|
+
...this.agentRegistry !== void 0 ? { registry: this.agentRegistry } : {},
|
|
35796
|
+
eventClassEnum: CANONICAL_AUDIT_EVENT_CLASSES
|
|
35797
|
+
});
|
|
35798
|
+
}
|
|
33087
35799
|
/**
|
|
33088
35800
|
* Emit the WP-V1.3-9 Tau-3 fetcher-failure audit event. Pulled out
|
|
33089
35801
|
* of the fold path so the dynamic-context handler stays readable.
|
|
@@ -33117,14 +35829,14 @@ ${inbox}`
|
|
|
33117
35829
|
if (turns.length === 0) return "";
|
|
33118
35830
|
const HEADER = "## Prior conversation";
|
|
33119
35831
|
const lines = turns.map(formatPriorTurnLine);
|
|
33120
|
-
const headerTokens =
|
|
35832
|
+
const headerTokens = approxTokenLen3(`${HEADER}
|
|
33121
35833
|
`);
|
|
33122
|
-
const sepTokens =
|
|
35834
|
+
const sepTokens = approxTokenLen3("\n");
|
|
33123
35835
|
let runningTokens = headerTokens;
|
|
33124
35836
|
let runningLines = [];
|
|
33125
35837
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
33126
35838
|
const line = lines[i];
|
|
33127
|
-
const tokens =
|
|
35839
|
+
const tokens = approxTokenLen3(line) + (runningLines.length > 0 ? sepTokens : 0);
|
|
33128
35840
|
if (runningTokens + tokens > this.historyTokenBudget) break;
|
|
33129
35841
|
runningTokens += tokens;
|
|
33130
35842
|
runningLines.push(line);
|
|
@@ -33171,7 +35883,7 @@ function hashOf(input) {
|
|
|
33171
35883
|
init_encryption();
|
|
33172
35884
|
init_encoding();
|
|
33173
35885
|
var OPERATOR_CHAT_NAMESPACE = "_chat";
|
|
33174
|
-
var
|
|
35886
|
+
var HKDF_INFO3 = "operator-chat-store-v1";
|
|
33175
35887
|
function chatStorageKey(surface, threadKey) {
|
|
33176
35888
|
return `${surface}.${threadKey}`;
|
|
33177
35889
|
}
|
|
@@ -33180,7 +35892,7 @@ var OperatorChatStore = class {
|
|
|
33180
35892
|
encryptionKey;
|
|
33181
35893
|
constructor(storage, masterKey) {
|
|
33182
35894
|
this.storage = storage;
|
|
33183
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
35895
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO3);
|
|
33184
35896
|
}
|
|
33185
35897
|
/**
|
|
33186
35898
|
* Load a thread. Returns null if no record exists or if the on-disk
|
|
@@ -33264,7 +35976,7 @@ init_encryption();
|
|
|
33264
35976
|
init_encoding();
|
|
33265
35977
|
var CONCIERGE_MEMORY_NAMESPACE = "_chat";
|
|
33266
35978
|
var CONCIERGE_MEMORY_KEY_PREFIX = "concierge_memory.";
|
|
33267
|
-
var
|
|
35979
|
+
var HKDF_INFO4 = "concierge-memory-store-v1";
|
|
33268
35980
|
var DEFAULT_CONCIERGE_RETENTION_DAYS = 30;
|
|
33269
35981
|
var MAX_BUNDLE_BYTES3 = 4 * 1024 * 1024;
|
|
33270
35982
|
var ConciergeMemoryStore = class {
|
|
@@ -33275,7 +35987,7 @@ var ConciergeMemoryStore = class {
|
|
|
33275
35987
|
locks;
|
|
33276
35988
|
constructor(opts) {
|
|
33277
35989
|
this.storage = opts.storage;
|
|
33278
|
-
this.encryptionKey = derivePurposeKey(opts.masterKey,
|
|
35990
|
+
this.encryptionKey = derivePurposeKey(opts.masterKey, HKDF_INFO4);
|
|
33279
35991
|
this.fortressId = opts.fortressId;
|
|
33280
35992
|
this.retentionDays = opts.retentionDays !== void 0 && opts.retentionDays > 0 ? opts.retentionDays : DEFAULT_CONCIERGE_RETENTION_DAYS;
|
|
33281
35993
|
this.locks = /* @__PURE__ */ new Map();
|
|
@@ -33401,7 +36113,7 @@ var ConciergeMemoryStore = class {
|
|
|
33401
36113
|
);
|
|
33402
36114
|
const summaries = [];
|
|
33403
36115
|
for (const meta of entries) {
|
|
33404
|
-
const threadId =
|
|
36116
|
+
const threadId = stripKeyPrefix3(meta.key);
|
|
33405
36117
|
if (threadId === null) continue;
|
|
33406
36118
|
const bundle = await this.loadBundle(threadId);
|
|
33407
36119
|
if (!bundle || bundle.turns.length === 0) continue;
|
|
@@ -33454,7 +36166,7 @@ var ConciergeMemoryStore = class {
|
|
|
33454
36166
|
);
|
|
33455
36167
|
let pruned = 0;
|
|
33456
36168
|
for (const meta of entries) {
|
|
33457
|
-
const threadId =
|
|
36169
|
+
const threadId = stripKeyPrefix3(meta.key);
|
|
33458
36170
|
if (threadId === null) continue;
|
|
33459
36171
|
pruned += await this.withLock(threadId, async () => {
|
|
33460
36172
|
const bundle = await this.loadBundle(threadId);
|
|
@@ -33538,7 +36250,7 @@ var ConciergeMemoryStore = class {
|
|
|
33538
36250
|
function bundleKey(threadId) {
|
|
33539
36251
|
return `${CONCIERGE_MEMORY_KEY_PREFIX}${threadId}`;
|
|
33540
36252
|
}
|
|
33541
|
-
function
|
|
36253
|
+
function stripKeyPrefix3(key) {
|
|
33542
36254
|
if (!key.startsWith(CONCIERGE_MEMORY_KEY_PREFIX)) return null;
|
|
33543
36255
|
return key.slice(CONCIERGE_MEMORY_KEY_PREFIX.length);
|
|
33544
36256
|
}
|
|
@@ -33911,13 +36623,13 @@ init_encryption();
|
|
|
33911
36623
|
init_encoding();
|
|
33912
36624
|
var INTELLIGENCE_NAMESPACE = "_intelligence";
|
|
33913
36625
|
var SUBSTRATE_CONFIG_KEY = "substrate-config";
|
|
33914
|
-
var
|
|
36626
|
+
var HKDF_INFO5 = "intelligence-substrate-config";
|
|
33915
36627
|
var IntelligenceConfigStore = class {
|
|
33916
36628
|
storage;
|
|
33917
36629
|
encryptionKey;
|
|
33918
36630
|
constructor(storage, masterKey) {
|
|
33919
36631
|
this.storage = storage;
|
|
33920
|
-
this.encryptionKey = derivePurposeKey(masterKey,
|
|
36632
|
+
this.encryptionKey = derivePurposeKey(masterKey, HKDF_INFO5);
|
|
33921
36633
|
}
|
|
33922
36634
|
/**
|
|
33923
36635
|
* Load the operator's substrate config from disk. Returns the config
|
|
@@ -37896,6 +40608,38 @@ ${err.message}
|
|
|
37896
40608
|
if (dashboard) {
|
|
37897
40609
|
dashboard.setApprovalAggregator(approvalAggregator);
|
|
37898
40610
|
}
|
|
40611
|
+
const sentinelFindingStore = new SentinelFindingStore({
|
|
40612
|
+
storage,
|
|
40613
|
+
masterKey,
|
|
40614
|
+
fortressId: fortressIdForAggregator
|
|
40615
|
+
});
|
|
40616
|
+
const sentinelRegistry = new SentinelRegistry();
|
|
40617
|
+
for (const entry of PHI1_BASELINE_CATALOG) {
|
|
40618
|
+
sentinelRegistry.register(entry);
|
|
40619
|
+
}
|
|
40620
|
+
const sentinelDispatcher = new SentinelDispatcher({
|
|
40621
|
+
registry: sentinelRegistry,
|
|
40622
|
+
findingStore: sentinelFindingStore,
|
|
40623
|
+
auditLog,
|
|
40624
|
+
fortressId: fortressIdForAggregator,
|
|
40625
|
+
identityId: aggregatorIdentityId
|
|
40626
|
+
});
|
|
40627
|
+
try {
|
|
40628
|
+
const persistedSubscriptions = await loadSentinelSubscriptions(
|
|
40629
|
+
config.storage_path
|
|
40630
|
+
);
|
|
40631
|
+
for (const sentinelId of persistedSubscriptions) {
|
|
40632
|
+
try {
|
|
40633
|
+
await sentinelDispatcher.subscribeSentinel(sentinelId);
|
|
40634
|
+
} catch {
|
|
40635
|
+
}
|
|
40636
|
+
}
|
|
40637
|
+
} catch {
|
|
40638
|
+
}
|
|
40639
|
+
sentinelDispatcher.start();
|
|
40640
|
+
if (dashboard) {
|
|
40641
|
+
dashboard.setSentinelDispatcher(sentinelDispatcher);
|
|
40642
|
+
}
|
|
37899
40643
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
37900
40644
|
const { tools: sanctuaryMetaTools } = createSanctuaryTools({
|
|
37901
40645
|
config,
|