@sanctuary-framework/mcp-server 1.2.9 → 1.2.11
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/dist/cli.cjs +1242 -23
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1242 -23
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +822 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +243 -81
- package/dist/index.d.ts +243 -81
- package/dist/index.js +822 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -16565,6 +16565,306 @@ async function handleSentinelRoute(deps, req, res) {
|
|
|
16565
16565
|
return true;
|
|
16566
16566
|
}
|
|
16567
16567
|
}
|
|
16568
|
+
var HANDOFF_LOG_OBSERVED_OPS = {
|
|
16569
|
+
/** Tau-3 in-process coordination handoff. */
|
|
16570
|
+
LOCAL_HANDOFF: "v1.1_local_handoff",
|
|
16571
|
+
/** Upsilon-1 cross-harness approval (wrapped-agent -> operator). */
|
|
16572
|
+
CROSS_HARNESS_APPROVAL: "cross_harness_approval_aggregated"
|
|
16573
|
+
};
|
|
16574
|
+
var OBSERVED_OP_LIST = [
|
|
16575
|
+
HANDOFF_LOG_OBSERVED_OPS.LOCAL_HANDOFF,
|
|
16576
|
+
HANDOFF_LOG_OBSERVED_OPS.CROSS_HARNESS_APPROVAL
|
|
16577
|
+
];
|
|
16578
|
+
var OPERATOR_PSEUDO_AGENT = "operator";
|
|
16579
|
+
function makeEntryId(auditEventId) {
|
|
16580
|
+
return crypto.createHash("sha256").update(auditEventId).digest("hex").slice(0, 32);
|
|
16581
|
+
}
|
|
16582
|
+
var DEFAULT_LIMIT = 50;
|
|
16583
|
+
var MAX_LIMIT = 500;
|
|
16584
|
+
var AUDIT_QUERY_LIMIT = 1e4;
|
|
16585
|
+
var HandoffLog = class {
|
|
16586
|
+
auditLog;
|
|
16587
|
+
fortressId;
|
|
16588
|
+
constructor(opts) {
|
|
16589
|
+
this.auditLog = opts.auditLog;
|
|
16590
|
+
this.fortressId = opts.fortressId;
|
|
16591
|
+
}
|
|
16592
|
+
/** Stable fortress id this HandoffLog reads. */
|
|
16593
|
+
getFortressId() {
|
|
16594
|
+
return this.fortressId;
|
|
16595
|
+
}
|
|
16596
|
+
/**
|
|
16597
|
+
* Query handoffs in chronological-newest-first order. Filters
|
|
16598
|
+
* applied after normalization so the per-event-class shape
|
|
16599
|
+
* differences (sender field name, recipient inference) are handled
|
|
16600
|
+
* once.
|
|
16601
|
+
*/
|
|
16602
|
+
async query(opts) {
|
|
16603
|
+
const limit = Math.min(opts.limit ?? DEFAULT_LIMIT, MAX_LIMIT);
|
|
16604
|
+
const queryResult = await this.auditLog.query({
|
|
16605
|
+
...opts.since !== void 0 ? { since: opts.since } : {},
|
|
16606
|
+
layer: "l2",
|
|
16607
|
+
limit: AUDIT_QUERY_LIMIT
|
|
16608
|
+
});
|
|
16609
|
+
const normalized = [];
|
|
16610
|
+
for (const entry of queryResult.entries) {
|
|
16611
|
+
if (!OBSERVED_OP_LIST.includes(entry.operation)) continue;
|
|
16612
|
+
const handoff = this.normalize(entry);
|
|
16613
|
+
if (!handoff) continue;
|
|
16614
|
+
if (opts.until && handoff.observed_at > opts.until) continue;
|
|
16615
|
+
if (opts.since && handoff.observed_at < opts.since) continue;
|
|
16616
|
+
if (opts.agent_id) {
|
|
16617
|
+
if (handoff.source_agent_id !== opts.agent_id && handoff.target_agent_id !== opts.agent_id) {
|
|
16618
|
+
continue;
|
|
16619
|
+
}
|
|
16620
|
+
}
|
|
16621
|
+
normalized.push(handoff);
|
|
16622
|
+
}
|
|
16623
|
+
normalized.sort((a, b) => a.observed_at < b.observed_at ? 1 : -1);
|
|
16624
|
+
return normalized.slice(0, limit);
|
|
16625
|
+
}
|
|
16626
|
+
/**
|
|
16627
|
+
* Look up a single entry by id. Returns the normalized entry +
|
|
16628
|
+
* the source audit payload for operator-facing detail rendering.
|
|
16629
|
+
* Returns null when no audit entry maps to the given id.
|
|
16630
|
+
*/
|
|
16631
|
+
async getEntry(entryId) {
|
|
16632
|
+
const queryResult = await this.auditLog.query({
|
|
16633
|
+
layer: "l2",
|
|
16634
|
+
limit: AUDIT_QUERY_LIMIT
|
|
16635
|
+
});
|
|
16636
|
+
for (const audit of queryResult.entries) {
|
|
16637
|
+
if (!OBSERVED_OP_LIST.includes(audit.operation)) continue;
|
|
16638
|
+
const handoff = this.normalize(audit);
|
|
16639
|
+
if (!handoff) continue;
|
|
16640
|
+
if (handoff.entry_id === entryId) {
|
|
16641
|
+
return { entry: handoff, source_audit_entry: audit };
|
|
16642
|
+
}
|
|
16643
|
+
}
|
|
16644
|
+
return null;
|
|
16645
|
+
}
|
|
16646
|
+
normalize(audit) {
|
|
16647
|
+
const details = audit.details;
|
|
16648
|
+
if (audit.operation === HANDOFF_LOG_OBSERVED_OPS.LOCAL_HANDOFF) {
|
|
16649
|
+
const sender = optString(details, "sender_agent_id");
|
|
16650
|
+
const recipient = optString(details, "recipient_agent_id");
|
|
16651
|
+
if (!sender || !recipient || sender === recipient) return null;
|
|
16652
|
+
const auditEventId = optString(details, "event_id") ?? auditEventIdFallback(audit);
|
|
16653
|
+
return {
|
|
16654
|
+
entry_id: makeEntryId(auditEventId),
|
|
16655
|
+
audit_event_id: auditEventId,
|
|
16656
|
+
source_agent_id: sender,
|
|
16657
|
+
target_agent_id: recipient,
|
|
16658
|
+
observed_at: audit.timestamp,
|
|
16659
|
+
event_class: audit.operation,
|
|
16660
|
+
context_transfer_summary: localHandoffSummary(details, sender, recipient),
|
|
16661
|
+
workflow_link: null
|
|
16662
|
+
};
|
|
16663
|
+
}
|
|
16664
|
+
if (audit.operation === HANDOFF_LOG_OBSERVED_OPS.CROSS_HARNESS_APPROVAL) {
|
|
16665
|
+
const sender = optString(details, "source_harness") ?? optString(details, "source_agent_id");
|
|
16666
|
+
if (!sender) return null;
|
|
16667
|
+
const auditEventId = optString(details, "aggregator_id") ?? auditEventIdFallback(audit);
|
|
16668
|
+
return {
|
|
16669
|
+
entry_id: makeEntryId(auditEventId),
|
|
16670
|
+
audit_event_id: auditEventId,
|
|
16671
|
+
source_agent_id: sender,
|
|
16672
|
+
target_agent_id: OPERATOR_PSEUDO_AGENT,
|
|
16673
|
+
observed_at: audit.timestamp,
|
|
16674
|
+
event_class: audit.operation,
|
|
16675
|
+
context_transfer_summary: crossHarnessSummary(details, sender),
|
|
16676
|
+
workflow_link: null
|
|
16677
|
+
};
|
|
16678
|
+
}
|
|
16679
|
+
return null;
|
|
16680
|
+
}
|
|
16681
|
+
};
|
|
16682
|
+
function optString(details, key) {
|
|
16683
|
+
if (!details) return null;
|
|
16684
|
+
const value = details[key];
|
|
16685
|
+
if (typeof value !== "string" || value.length === 0) return null;
|
|
16686
|
+
return value;
|
|
16687
|
+
}
|
|
16688
|
+
function auditEventIdFallback(audit) {
|
|
16689
|
+
return `${audit.timestamp}:${audit.operation}`;
|
|
16690
|
+
}
|
|
16691
|
+
function localHandoffSummary(details, sender, recipient) {
|
|
16692
|
+
const taskScope = optString(details, "task_scope");
|
|
16693
|
+
const reasonClass = optString(details, "reason_class");
|
|
16694
|
+
if (taskScope) {
|
|
16695
|
+
return `${sender} -> ${recipient} handoff: ${taskScope}`;
|
|
16696
|
+
}
|
|
16697
|
+
if (reasonClass) {
|
|
16698
|
+
return `${sender} -> ${recipient} handoff (${reasonClass})`;
|
|
16699
|
+
}
|
|
16700
|
+
return `${sender} -> ${recipient} handoff`;
|
|
16701
|
+
}
|
|
16702
|
+
function crossHarnessSummary(details, sender) {
|
|
16703
|
+
const policyRule = optString(details, "policy_rule_id");
|
|
16704
|
+
if (policyRule) {
|
|
16705
|
+
return `${sender} -> operator approval (${policyRule})`;
|
|
16706
|
+
}
|
|
16707
|
+
return `${sender} -> operator approval`;
|
|
16708
|
+
}
|
|
16709
|
+
var COORDINATION_VIEW_AUDIT_OPS = {
|
|
16710
|
+
VIEW_OPENED: "operator_coordination_view_opened",
|
|
16711
|
+
ENTRY_DRILLED: "operator_handoff_entry_drilled"
|
|
16712
|
+
};
|
|
16713
|
+
|
|
16714
|
+
// src/coordination/handoff-routes.ts
|
|
16715
|
+
var COORDINATION_API_PREFIX = "/api/coordination";
|
|
16716
|
+
var COORDINATION_HANDOFFS_PREFIX = "/api/coordination/handoffs";
|
|
16717
|
+
var COORDINATION_LIST_DEFAULT_LIMIT = 50;
|
|
16718
|
+
var COORDINATION_LIST_MAX_LIMIT = 500;
|
|
16719
|
+
var HandoffEventBridge = class {
|
|
16720
|
+
listeners = /* @__PURE__ */ new Set();
|
|
16721
|
+
subscribe(listener) {
|
|
16722
|
+
this.listeners.add(listener);
|
|
16723
|
+
return () => this.listeners.delete(listener);
|
|
16724
|
+
}
|
|
16725
|
+
emit(entry) {
|
|
16726
|
+
for (const listener of this.listeners) {
|
|
16727
|
+
try {
|
|
16728
|
+
listener(entry);
|
|
16729
|
+
} catch {
|
|
16730
|
+
}
|
|
16731
|
+
}
|
|
16732
|
+
}
|
|
16733
|
+
};
|
|
16734
|
+
function writeJSON6(res, status, payload) {
|
|
16735
|
+
res.writeHead(status, {
|
|
16736
|
+
"Content-Type": "application/json",
|
|
16737
|
+
"Cache-Control": "no-store"
|
|
16738
|
+
});
|
|
16739
|
+
res.end(JSON.stringify(payload));
|
|
16740
|
+
}
|
|
16741
|
+
function parseLimit4(raw, defaultValue, max) {
|
|
16742
|
+
if (raw === null || raw === "") return defaultValue;
|
|
16743
|
+
const parsed = Number.parseInt(raw, 10);
|
|
16744
|
+
if (Number.isNaN(parsed) || parsed < 0) return defaultValue;
|
|
16745
|
+
return Math.min(parsed, max);
|
|
16746
|
+
}
|
|
16747
|
+
function matchEntryRoute2(path) {
|
|
16748
|
+
const prefix = `${COORDINATION_HANDOFFS_PREFIX}/`;
|
|
16749
|
+
if (!path.startsWith(prefix)) return null;
|
|
16750
|
+
const rest = path.slice(prefix.length);
|
|
16751
|
+
if (rest.length === 0 || rest === "stream") return null;
|
|
16752
|
+
if (rest.includes("/")) return null;
|
|
16753
|
+
return { entryId: decodeURIComponent(rest) };
|
|
16754
|
+
}
|
|
16755
|
+
async function handleStream3(deps, res) {
|
|
16756
|
+
res.writeHead(200, {
|
|
16757
|
+
"Content-Type": "text/event-stream",
|
|
16758
|
+
"Cache-Control": "no-cache, no-transform",
|
|
16759
|
+
Connection: "keep-alive",
|
|
16760
|
+
"X-Accel-Buffering": "no"
|
|
16761
|
+
});
|
|
16762
|
+
const snapshot = await deps.handoffLog.query({ limit: 50 });
|
|
16763
|
+
res.write(
|
|
16764
|
+
`event: handoff_snapshot
|
|
16765
|
+
data: ${JSON.stringify({ entries: snapshot })}
|
|
16766
|
+
|
|
16767
|
+
`
|
|
16768
|
+
);
|
|
16769
|
+
const unsubscribe = deps.events.subscribe((entry) => {
|
|
16770
|
+
try {
|
|
16771
|
+
res.write(
|
|
16772
|
+
`event: handoff_added
|
|
16773
|
+
data: ${JSON.stringify(entry)}
|
|
16774
|
+
|
|
16775
|
+
`
|
|
16776
|
+
);
|
|
16777
|
+
} catch {
|
|
16778
|
+
}
|
|
16779
|
+
});
|
|
16780
|
+
const keepAlive = setInterval(() => {
|
|
16781
|
+
try {
|
|
16782
|
+
res.write(": keepalive\n\n");
|
|
16783
|
+
} catch {
|
|
16784
|
+
}
|
|
16785
|
+
}, 25e3);
|
|
16786
|
+
const cleanup = () => {
|
|
16787
|
+
clearInterval(keepAlive);
|
|
16788
|
+
unsubscribe();
|
|
16789
|
+
};
|
|
16790
|
+
res.on("close", cleanup);
|
|
16791
|
+
res.on("error", cleanup);
|
|
16792
|
+
}
|
|
16793
|
+
async function handleCoordinationRoute(deps, req, res) {
|
|
16794
|
+
const host = req.headers.host || "localhost";
|
|
16795
|
+
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
16796
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
16797
|
+
const path = url.pathname;
|
|
16798
|
+
if (path !== COORDINATION_API_PREFIX && !path.startsWith(`${COORDINATION_API_PREFIX}/`)) {
|
|
16799
|
+
return false;
|
|
16800
|
+
}
|
|
16801
|
+
const checkAuth = authMiddleware(deps.authConfig);
|
|
16802
|
+
if (!checkAuth(req, res, url)) return true;
|
|
16803
|
+
try {
|
|
16804
|
+
if (method === "GET" && path === `${COORDINATION_HANDOFFS_PREFIX}/stream`) {
|
|
16805
|
+
await handleStream3(deps, res);
|
|
16806
|
+
return true;
|
|
16807
|
+
}
|
|
16808
|
+
if (method === "GET" && path === COORDINATION_HANDOFFS_PREFIX) {
|
|
16809
|
+
const limit = parseLimit4(
|
|
16810
|
+
url.searchParams.get("limit"),
|
|
16811
|
+
COORDINATION_LIST_DEFAULT_LIMIT,
|
|
16812
|
+
COORDINATION_LIST_MAX_LIMIT
|
|
16813
|
+
);
|
|
16814
|
+
const since = url.searchParams.get("since") ?? void 0;
|
|
16815
|
+
const until = url.searchParams.get("until") ?? void 0;
|
|
16816
|
+
const agentId = url.searchParams.get("agent_id") ?? void 0;
|
|
16817
|
+
const entries = await deps.handoffLog.query({
|
|
16818
|
+
limit,
|
|
16819
|
+
...since !== void 0 ? { since } : {},
|
|
16820
|
+
...until !== void 0 ? { until } : {},
|
|
16821
|
+
...agentId !== void 0 ? { agent_id: agentId } : {}
|
|
16822
|
+
});
|
|
16823
|
+
deps.auditLog.append(
|
|
16824
|
+
"l2",
|
|
16825
|
+
COORDINATION_VIEW_AUDIT_OPS.VIEW_OPENED,
|
|
16826
|
+
deps.operatorId,
|
|
16827
|
+
{
|
|
16828
|
+
fortress_id: deps.handoffLog.getFortressId(),
|
|
16829
|
+
result_count: entries.length,
|
|
16830
|
+
...since !== void 0 ? { since } : {},
|
|
16831
|
+
...until !== void 0 ? { until } : {},
|
|
16832
|
+
...agentId !== void 0 ? { agent_id: agentId } : {}
|
|
16833
|
+
}
|
|
16834
|
+
);
|
|
16835
|
+
writeJSON6(res, 200, { ok: true, data: { entries } });
|
|
16836
|
+
return true;
|
|
16837
|
+
}
|
|
16838
|
+
const entryMatch = matchEntryRoute2(path);
|
|
16839
|
+
if (method === "GET" && entryMatch) {
|
|
16840
|
+
const detail = await deps.handoffLog.getEntry(entryMatch.entryId);
|
|
16841
|
+
if (!detail) {
|
|
16842
|
+
writeJSON6(res, 404, { ok: false, error: "not_found" });
|
|
16843
|
+
return true;
|
|
16844
|
+
}
|
|
16845
|
+
deps.auditLog.append(
|
|
16846
|
+
"l2",
|
|
16847
|
+
COORDINATION_VIEW_AUDIT_OPS.ENTRY_DRILLED,
|
|
16848
|
+
deps.operatorId,
|
|
16849
|
+
{
|
|
16850
|
+
fortress_id: deps.handoffLog.getFortressId(),
|
|
16851
|
+
entry_id: detail.entry.entry_id,
|
|
16852
|
+
event_class: detail.entry.event_class,
|
|
16853
|
+
source_agent_id: detail.entry.source_agent_id,
|
|
16854
|
+
target_agent_id: detail.entry.target_agent_id
|
|
16855
|
+
}
|
|
16856
|
+
);
|
|
16857
|
+
writeJSON6(res, 200, { ok: true, data: detail });
|
|
16858
|
+
return true;
|
|
16859
|
+
}
|
|
16860
|
+
writeJSON6(res, 404, { ok: false, error: "not_found", path });
|
|
16861
|
+
return true;
|
|
16862
|
+
} catch (err) {
|
|
16863
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
16864
|
+
writeJSON6(res, 500, { ok: false, error: "internal", detail: msg });
|
|
16865
|
+
return true;
|
|
16866
|
+
}
|
|
16867
|
+
}
|
|
16568
16868
|
|
|
16569
16869
|
// src/principal-policy/dashboard.ts
|
|
16570
16870
|
var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
@@ -16645,6 +16945,17 @@ var DashboardApprovalChannel = class {
|
|
|
16645
16945
|
* dispatcher's audited paths.
|
|
16646
16946
|
*/
|
|
16647
16947
|
sentinelDispatcher = null;
|
|
16948
|
+
/**
|
|
16949
|
+
* v1.3 WP-V1.3-3 Omega-1 Coordination Handoff Visualization.
|
|
16950
|
+
* Mounted additively at `/api/coordination/*` when set. Read-only
|
|
16951
|
+
* against the audit log; the only writes are operator-action audit
|
|
16952
|
+
* events (operator_coordination_view_opened,
|
|
16953
|
+
* operator_handoff_entry_drilled).
|
|
16954
|
+
*/
|
|
16955
|
+
handoffLog = null;
|
|
16956
|
+
handoffEventBridge = null;
|
|
16957
|
+
handoffAuditLog = null;
|
|
16958
|
+
handoffOperatorId = null;
|
|
16648
16959
|
constructor(config) {
|
|
16649
16960
|
this.config = config;
|
|
16650
16961
|
this.authToken = config.auth_token;
|
|
@@ -16712,6 +17023,18 @@ var DashboardApprovalChannel = class {
|
|
|
16712
17023
|
setSentinelDispatcher(dispatcher) {
|
|
16713
17024
|
this.sentinelDispatcher = dispatcher;
|
|
16714
17025
|
}
|
|
17026
|
+
/**
|
|
17027
|
+
* v1.3 WP-V1.3-3 Omega-1: bind the Coordination handoff log +
|
|
17028
|
+
* event bridge + audit log + operator id. Once set, requests to
|
|
17029
|
+
* `/api/coordination/*` route through `handleCoordinationRoute`.
|
|
17030
|
+
* Pass `null` for any field to detach.
|
|
17031
|
+
*/
|
|
17032
|
+
setHandoffLog(opts) {
|
|
17033
|
+
this.handoffLog = opts.handoffLog;
|
|
17034
|
+
this.handoffEventBridge = opts.eventBridge ?? null;
|
|
17035
|
+
this.handoffAuditLog = opts.auditLog ?? null;
|
|
17036
|
+
this.handoffOperatorId = opts.operatorId ?? null;
|
|
17037
|
+
}
|
|
16715
17038
|
/**
|
|
16716
17039
|
* v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
|
|
16717
17040
|
* before the legacy approval route table. Returns true when served.
|
|
@@ -16750,6 +17073,30 @@ var DashboardApprovalChannel = class {
|
|
|
16750
17073
|
res
|
|
16751
17074
|
);
|
|
16752
17075
|
}
|
|
17076
|
+
/**
|
|
17077
|
+
* v1.3 WP-V1.3-3 Omega-1 dispatch entry point. Routes
|
|
17078
|
+
* `/api/coordination/*` requests through the coordination router
|
|
17079
|
+
* when a HandoffLog has been bound. Returns true when served.
|
|
17080
|
+
*/
|
|
17081
|
+
async dispatchCoordination(req, res) {
|
|
17082
|
+
if (!this.handoffLog || !this.handoffEventBridge || !this.handoffAuditLog) {
|
|
17083
|
+
return false;
|
|
17084
|
+
}
|
|
17085
|
+
return handleCoordinationRoute(
|
|
17086
|
+
{
|
|
17087
|
+
authConfig: {
|
|
17088
|
+
loopbackAutoAuth: this._autoAuthLocalhost,
|
|
17089
|
+
...this.authToken !== void 0 ? { authToken: this.authToken } : {}
|
|
17090
|
+
},
|
|
17091
|
+
handoffLog: this.handoffLog,
|
|
17092
|
+
auditLog: this.handoffAuditLog,
|
|
17093
|
+
operatorId: this.handoffOperatorId ?? this.identityManager?.getPrimaryIdentityId() ?? "operator_dashboard",
|
|
17094
|
+
events: this.handoffEventBridge
|
|
17095
|
+
},
|
|
17096
|
+
req,
|
|
17097
|
+
res
|
|
17098
|
+
);
|
|
17099
|
+
}
|
|
16753
17100
|
/**
|
|
16754
17101
|
* v1.1 dispatch entry point. Called from `handleRequest` before the
|
|
16755
17102
|
* legacy route table. Returns true when the request was served by v1.1
|
|
@@ -17149,6 +17496,18 @@ var DashboardApprovalChannel = class {
|
|
|
17149
17496
|
});
|
|
17150
17497
|
return;
|
|
17151
17498
|
}
|
|
17499
|
+
if (this.handoffLog && url.pathname.startsWith(COORDINATION_API_PREFIX)) {
|
|
17500
|
+
this.dispatchCoordination(req, res).then((handled) => {
|
|
17501
|
+
if (handled) return;
|
|
17502
|
+
this.handleLegacyRequest(req, res, url, method);
|
|
17503
|
+
}).catch(() => {
|
|
17504
|
+
if (!res.headersSent) {
|
|
17505
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
17506
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
17507
|
+
}
|
|
17508
|
+
});
|
|
17509
|
+
return;
|
|
17510
|
+
}
|
|
17152
17511
|
if (this.v11Bindings) {
|
|
17153
17512
|
this.dispatchV11(req, res, url, method).then((handled) => {
|
|
17154
17513
|
if (handled) return;
|
|
@@ -20809,6 +21168,11 @@ var SentinelDispatcher = class {
|
|
|
20809
21168
|
fortressId: this.fortressId,
|
|
20810
21169
|
auditLog: this.auditLog,
|
|
20811
21170
|
now: this.now,
|
|
21171
|
+
// Phi-5 meta-sentinel reads the per-fortress finding store to
|
|
21172
|
+
// detect patterns across other sentinels' findings. First-order
|
|
21173
|
+
// sentinels ignore the field; the dispatcher always attaches it
|
|
21174
|
+
// because the store is already in scope here.
|
|
21175
|
+
findingStore: this.findingStore,
|
|
20812
21176
|
...contextOverrides ?? {}
|
|
20813
21177
|
};
|
|
20814
21178
|
const sentinel = await this.registry.subscribe(sentinelId, context);
|
|
@@ -20947,6 +21311,218 @@ var SentinelDispatcher = class {
|
|
|
20947
21311
|
}
|
|
20948
21312
|
}
|
|
20949
21313
|
};
|
|
21314
|
+
var ANOMALY_AUDIT_OPS = {
|
|
21315
|
+
DETECTOR_REGISTERED: "anomaly_detector_registered",
|
|
21316
|
+
DETECTOR_UNREGISTERED: "anomaly_detector_unregistered",
|
|
21317
|
+
FINDING_EMITTED: "anomaly_finding_emitted",
|
|
21318
|
+
EVALUATION_FAILED: "anomaly_evaluation_failed",
|
|
21319
|
+
TRAINING_COMPLETED: "anomaly_training_completed",
|
|
21320
|
+
TRAINING_FAILED: "anomaly_training_failed"
|
|
21321
|
+
};
|
|
21322
|
+
var DEFAULT_TICK_INTERVAL_MS2 = 6e4;
|
|
21323
|
+
var AnomalyPipelineDispatcher = class {
|
|
21324
|
+
findingStore;
|
|
21325
|
+
auditLog;
|
|
21326
|
+
storage;
|
|
21327
|
+
masterKey;
|
|
21328
|
+
fortressId;
|
|
21329
|
+
identityId;
|
|
21330
|
+
now;
|
|
21331
|
+
tickIntervalMs;
|
|
21332
|
+
detectors = /* @__PURE__ */ new Map();
|
|
21333
|
+
listeners = /* @__PURE__ */ new Set();
|
|
21334
|
+
tickTimer = null;
|
|
21335
|
+
tickInFlight = false;
|
|
21336
|
+
constructor(deps) {
|
|
21337
|
+
this.findingStore = deps.findingStore;
|
|
21338
|
+
this.auditLog = deps.auditLog;
|
|
21339
|
+
this.storage = deps.storage;
|
|
21340
|
+
this.masterKey = deps.masterKey;
|
|
21341
|
+
this.fortressId = deps.fortressId;
|
|
21342
|
+
this.identityId = deps.identityId;
|
|
21343
|
+
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
21344
|
+
this.tickIntervalMs = deps.tickIntervalMs ?? DEFAULT_TICK_INTERVAL_MS2;
|
|
21345
|
+
}
|
|
21346
|
+
onEvent(listener) {
|
|
21347
|
+
this.listeners.add(listener);
|
|
21348
|
+
return () => this.listeners.delete(listener);
|
|
21349
|
+
}
|
|
21350
|
+
/**
|
|
21351
|
+
* Register + subscribe a detector to this fortress. Idempotent: a
|
|
21352
|
+
* second call with the same detectorId returns the already-
|
|
21353
|
+
* registered instance without re-subscribing.
|
|
21354
|
+
*/
|
|
21355
|
+
async registerDetector(detector) {
|
|
21356
|
+
const existing = this.detectors.get(detector.detectorId);
|
|
21357
|
+
if (existing) return existing;
|
|
21358
|
+
const context = {
|
|
21359
|
+
fortressId: this.fortressId,
|
|
21360
|
+
auditLog: this.auditLog,
|
|
21361
|
+
storage: this.storage,
|
|
21362
|
+
masterKey: this.masterKey,
|
|
21363
|
+
now: this.now
|
|
21364
|
+
};
|
|
21365
|
+
await detector.subscribe(context);
|
|
21366
|
+
this.detectors.set(detector.detectorId, detector);
|
|
21367
|
+
this.auditLog.append(
|
|
21368
|
+
"l2",
|
|
21369
|
+
ANOMALY_AUDIT_OPS.DETECTOR_REGISTERED,
|
|
21370
|
+
this.identityId,
|
|
21371
|
+
{ detector_id: detector.detectorId, fortress_id: this.fortressId }
|
|
21372
|
+
);
|
|
21373
|
+
return detector;
|
|
21374
|
+
}
|
|
21375
|
+
/**
|
|
21376
|
+
* Unregister + tear down a detector. Idempotent. Returns true when
|
|
21377
|
+
* an active registration was removed.
|
|
21378
|
+
*/
|
|
21379
|
+
async unregisterDetector(detectorId) {
|
|
21380
|
+
const detector = this.detectors.get(detectorId);
|
|
21381
|
+
if (!detector) return false;
|
|
21382
|
+
try {
|
|
21383
|
+
await detector.unsubscribe();
|
|
21384
|
+
} finally {
|
|
21385
|
+
this.detectors.delete(detectorId);
|
|
21386
|
+
}
|
|
21387
|
+
this.auditLog.append(
|
|
21388
|
+
"l2",
|
|
21389
|
+
ANOMALY_AUDIT_OPS.DETECTOR_UNREGISTERED,
|
|
21390
|
+
this.identityId,
|
|
21391
|
+
{ detector_id: detectorId, fortress_id: this.fortressId }
|
|
21392
|
+
);
|
|
21393
|
+
return true;
|
|
21394
|
+
}
|
|
21395
|
+
listDetectors() {
|
|
21396
|
+
return [...this.detectors.keys()];
|
|
21397
|
+
}
|
|
21398
|
+
/** Run one evaluation pass over every registered detector. */
|
|
21399
|
+
async tick() {
|
|
21400
|
+
if (this.tickInFlight) return [];
|
|
21401
|
+
this.tickInFlight = true;
|
|
21402
|
+
try {
|
|
21403
|
+
const findings = [];
|
|
21404
|
+
for (const [detectorId, detector] of this.detectors.entries()) {
|
|
21405
|
+
try {
|
|
21406
|
+
const detectorFindings = await detector.evaluate();
|
|
21407
|
+
for (const raw of detectorFindings) {
|
|
21408
|
+
const stamped = await this.routeFinding(detectorId, raw);
|
|
21409
|
+
findings.push(stamped);
|
|
21410
|
+
}
|
|
21411
|
+
try {
|
|
21412
|
+
const trainingResult = await detector.classifier.train();
|
|
21413
|
+
this.auditLog.append(
|
|
21414
|
+
"l2",
|
|
21415
|
+
ANOMALY_AUDIT_OPS.TRAINING_COMPLETED,
|
|
21416
|
+
this.identityId,
|
|
21417
|
+
{
|
|
21418
|
+
detector_id: detectorId,
|
|
21419
|
+
classifier_id: detector.classifier.classifierId,
|
|
21420
|
+
trained_at: trainingResult.trained_at,
|
|
21421
|
+
sample_count: trainingResult.sample_count,
|
|
21422
|
+
agent_count: trainingResult.agent_count,
|
|
21423
|
+
fortress_id: this.fortressId
|
|
21424
|
+
}
|
|
21425
|
+
);
|
|
21426
|
+
} catch (trainErr) {
|
|
21427
|
+
const message = trainErr instanceof Error ? trainErr.message : String(trainErr);
|
|
21428
|
+
this.auditLog.append(
|
|
21429
|
+
"l2",
|
|
21430
|
+
ANOMALY_AUDIT_OPS.TRAINING_FAILED,
|
|
21431
|
+
this.identityId,
|
|
21432
|
+
{
|
|
21433
|
+
detector_id: detectorId,
|
|
21434
|
+
error_message: message,
|
|
21435
|
+
fortress_id: this.fortressId
|
|
21436
|
+
},
|
|
21437
|
+
"failure"
|
|
21438
|
+
);
|
|
21439
|
+
}
|
|
21440
|
+
} catch (err) {
|
|
21441
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
21442
|
+
const observedAt = this.now().toISOString();
|
|
21443
|
+
this.auditLog.append(
|
|
21444
|
+
"l2",
|
|
21445
|
+
ANOMALY_AUDIT_OPS.EVALUATION_FAILED,
|
|
21446
|
+
this.identityId,
|
|
21447
|
+
{
|
|
21448
|
+
detector_id: detectorId,
|
|
21449
|
+
error_message: message,
|
|
21450
|
+
fortress_id: this.fortressId
|
|
21451
|
+
},
|
|
21452
|
+
"failure"
|
|
21453
|
+
);
|
|
21454
|
+
this.emit({
|
|
21455
|
+
type: "evaluation_failed",
|
|
21456
|
+
detector_id: detectorId,
|
|
21457
|
+
error_message: message,
|
|
21458
|
+
observed_at: observedAt
|
|
21459
|
+
});
|
|
21460
|
+
}
|
|
21461
|
+
}
|
|
21462
|
+
return findings;
|
|
21463
|
+
} finally {
|
|
21464
|
+
this.tickInFlight = false;
|
|
21465
|
+
}
|
|
21466
|
+
}
|
|
21467
|
+
start() {
|
|
21468
|
+
if (this.tickTimer !== null) return;
|
|
21469
|
+
if (this.tickIntervalMs <= 0) return;
|
|
21470
|
+
this.tickTimer = setInterval(() => {
|
|
21471
|
+
void this.tick();
|
|
21472
|
+
}, this.tickIntervalMs);
|
|
21473
|
+
if (typeof this.tickTimer.unref === "function") {
|
|
21474
|
+
this.tickTimer.unref();
|
|
21475
|
+
}
|
|
21476
|
+
}
|
|
21477
|
+
stop() {
|
|
21478
|
+
if (this.tickTimer === null) return;
|
|
21479
|
+
clearInterval(this.tickTimer);
|
|
21480
|
+
this.tickTimer = null;
|
|
21481
|
+
}
|
|
21482
|
+
async dispose() {
|
|
21483
|
+
this.stop();
|
|
21484
|
+
const ids = [...this.detectors.keys()];
|
|
21485
|
+
for (const id of ids) {
|
|
21486
|
+
try {
|
|
21487
|
+
await this.unregisterDetector(id);
|
|
21488
|
+
} catch {
|
|
21489
|
+
}
|
|
21490
|
+
}
|
|
21491
|
+
this.listeners.clear();
|
|
21492
|
+
}
|
|
21493
|
+
async routeFinding(detectorId, raw) {
|
|
21494
|
+
const stamped = {
|
|
21495
|
+
...raw,
|
|
21496
|
+
finding_id: raw.finding_id || crypto.randomUUID(),
|
|
21497
|
+
fortress_id: this.fortressId,
|
|
21498
|
+
observed_at: raw.observed_at || this.now().toISOString()
|
|
21499
|
+
};
|
|
21500
|
+
await this.findingStore.saveFinding(stamped);
|
|
21501
|
+
this.auditLog.append(
|
|
21502
|
+
"l2",
|
|
21503
|
+
ANOMALY_AUDIT_OPS.FINDING_EMITTED,
|
|
21504
|
+
this.identityId,
|
|
21505
|
+
{
|
|
21506
|
+
detector_id: detectorId,
|
|
21507
|
+
finding_id: stamped.finding_id,
|
|
21508
|
+
severity: stamped.severity,
|
|
21509
|
+
anomaly_score: stamped.details["anomaly_score"] ?? null,
|
|
21510
|
+
...stamped.agent_id !== void 0 ? { agent_id: stamped.agent_id } : {},
|
|
21511
|
+
fortress_id: this.fortressId
|
|
21512
|
+
}
|
|
21513
|
+
);
|
|
21514
|
+
this.emit({ type: "finding", finding: stamped });
|
|
21515
|
+
return stamped;
|
|
21516
|
+
}
|
|
21517
|
+
emit(event) {
|
|
21518
|
+
for (const listener of this.listeners) {
|
|
21519
|
+
try {
|
|
21520
|
+
listener(event);
|
|
21521
|
+
} catch {
|
|
21522
|
+
}
|
|
21523
|
+
}
|
|
21524
|
+
}
|
|
21525
|
+
};
|
|
20950
21526
|
|
|
20951
21527
|
// src/sentinel/sentinel.ts
|
|
20952
21528
|
var Sentinel = class {
|
|
@@ -21127,7 +21703,7 @@ var ALERT_SIGMA2 = 6;
|
|
|
21127
21703
|
var BASELINE_WINDOWS2 = 7;
|
|
21128
21704
|
var QUERY_LIMIT2 = 1e4;
|
|
21129
21705
|
var MULTI_NEW_PARTNER_ALERT_THRESHOLD = 3;
|
|
21130
|
-
var
|
|
21706
|
+
var OPERATOR_PSEUDO_AGENT2 = "operator";
|
|
21131
21707
|
var HANDOFF_OP = "v1.1_local_handoff";
|
|
21132
21708
|
var CROSS_HARNESS_OPS = /* @__PURE__ */ new Set([
|
|
21133
21709
|
"cross_harness_approval_aggregated",
|
|
@@ -21360,7 +21936,7 @@ function extractInterAgentEvents(entries) {
|
|
|
21360
21936
|
if (!sender) continue;
|
|
21361
21937
|
out.push({
|
|
21362
21938
|
sender,
|
|
21363
|
-
recipient:
|
|
21939
|
+
recipient: OPERATOR_PSEUDO_AGENT2,
|
|
21364
21940
|
timestampMs: Date.parse(entry.timestamp),
|
|
21365
21941
|
auditId: `${entry.timestamp}:${entry.operation}`
|
|
21366
21942
|
});
|
|
@@ -22087,6 +22663,223 @@ function truncateSummary2(s) {
|
|
|
22087
22663
|
return s.length > 240 ? s.slice(0, 237) + "..." : s;
|
|
22088
22664
|
}
|
|
22089
22665
|
|
|
22666
|
+
// src/sentinel/sentinels/anomaly-trigger.ts
|
|
22667
|
+
var ANOMALY_TRIGGER_SENTINEL_ID = "anomaly-trigger";
|
|
22668
|
+
var WARN_SIGMA5 = 3;
|
|
22669
|
+
var ALERT_SIGMA5 = 6;
|
|
22670
|
+
var BASELINE_WINDOWS5 = 7;
|
|
22671
|
+
var QUERY_LIMIT5 = 5e3;
|
|
22672
|
+
var WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
22673
|
+
var COMPOUND_TRIGGER_MIN_SENTINELS = 2;
|
|
22674
|
+
var AnomalyTriggerWatcher = class extends Sentinel {
|
|
22675
|
+
sentinelId = ANOMALY_TRIGGER_SENTINEL_ID;
|
|
22676
|
+
description = "Meta-sentinel. Watches for patterns ACROSS other sentinels' findings: compound suspicious behavior on one agent, fortress-wide finding-count spikes, and novel cross-sentinel combinations. Closes WP-V1.3-1 Sentinel Baseline Pack.";
|
|
22677
|
+
async subscribe(context) {
|
|
22678
|
+
if (!context.findingStore) {
|
|
22679
|
+
throw new Error(
|
|
22680
|
+
`${ANOMALY_TRIGGER_SENTINEL_ID}: findingStore missing from SentinelContext; this meta-sentinel requires the Phi-1 finding store`
|
|
22681
|
+
);
|
|
22682
|
+
}
|
|
22683
|
+
await super.subscribe(context);
|
|
22684
|
+
}
|
|
22685
|
+
async evaluate() {
|
|
22686
|
+
const ctx = this.requireContext();
|
|
22687
|
+
const findingStore = ctx.findingStore;
|
|
22688
|
+
if (!findingStore) {
|
|
22689
|
+
return [];
|
|
22690
|
+
}
|
|
22691
|
+
const now = ctx.now();
|
|
22692
|
+
const nowMs = now.getTime();
|
|
22693
|
+
const windowSpanMs = (BASELINE_WINDOWS5 + 1) * WINDOW_MS;
|
|
22694
|
+
const sinceIso = new Date(nowMs - windowSpanMs).toISOString();
|
|
22695
|
+
let findings;
|
|
22696
|
+
try {
|
|
22697
|
+
findings = await findingStore.listFindings({
|
|
22698
|
+
since: sinceIso,
|
|
22699
|
+
limit: QUERY_LIMIT5
|
|
22700
|
+
});
|
|
22701
|
+
} catch {
|
|
22702
|
+
return [];
|
|
22703
|
+
}
|
|
22704
|
+
const firstOrderFindings = findings.filter(
|
|
22705
|
+
(f) => f.sentinel_id !== ANOMALY_TRIGGER_SENTINEL_ID
|
|
22706
|
+
);
|
|
22707
|
+
const windowed = bucketByWindow(firstOrderFindings, nowMs);
|
|
22708
|
+
const out = [];
|
|
22709
|
+
const compoundFindings = computeCompoundFindings(
|
|
22710
|
+
windowed[0] ?? [],
|
|
22711
|
+
now
|
|
22712
|
+
);
|
|
22713
|
+
out.push(...compoundFindings);
|
|
22714
|
+
const countSpikeFinding = computeCountSpikeFinding(windowed, now);
|
|
22715
|
+
if (countSpikeFinding) out.push(countSpikeFinding);
|
|
22716
|
+
const novelComboFinding = computeNovelComboFinding(windowed, now);
|
|
22717
|
+
if (novelComboFinding) out.push(novelComboFinding);
|
|
22718
|
+
return out;
|
|
22719
|
+
}
|
|
22720
|
+
};
|
|
22721
|
+
function computeCompoundFindings(windowZero, now) {
|
|
22722
|
+
const byAgent = /* @__PURE__ */ new Map();
|
|
22723
|
+
for (const f of windowZero) {
|
|
22724
|
+
if (!f.agent_id) continue;
|
|
22725
|
+
if (!isWarnOrAlert(f.severity)) continue;
|
|
22726
|
+
let bucket = byAgent.get(f.agent_id);
|
|
22727
|
+
if (!bucket) {
|
|
22728
|
+
bucket = [];
|
|
22729
|
+
byAgent.set(f.agent_id, bucket);
|
|
22730
|
+
}
|
|
22731
|
+
bucket.push(f);
|
|
22732
|
+
}
|
|
22733
|
+
const out = [];
|
|
22734
|
+
for (const [agentId, group] of byAgent.entries()) {
|
|
22735
|
+
const distinctSentinels = new Set(group.map((f) => f.sentinel_id));
|
|
22736
|
+
if (distinctSentinels.size < COMPOUND_TRIGGER_MIN_SENTINELS) continue;
|
|
22737
|
+
const contributingSentinels = [...distinctSentinels].sort();
|
|
22738
|
+
const evidence = group.map((f) => f.finding_id).filter((id) => id.length > 0).slice(0, 50);
|
|
22739
|
+
const summary = `${agentId} agent triggered ${distinctSentinels.size} distinct sentinels in the last 24h: ${contributingSentinels.join(", ")}. Compound suspicious behavior; review the contributing findings.`;
|
|
22740
|
+
out.push({
|
|
22741
|
+
finding_id: "",
|
|
22742
|
+
sentinel_id: ANOMALY_TRIGGER_SENTINEL_ID,
|
|
22743
|
+
severity: "alert",
|
|
22744
|
+
agent_id: agentId,
|
|
22745
|
+
summary,
|
|
22746
|
+
details: {
|
|
22747
|
+
trigger: "compound",
|
|
22748
|
+
agent_id: agentId,
|
|
22749
|
+
contributing_sentinels: contributingSentinels,
|
|
22750
|
+
contributing_finding_count: group.length
|
|
22751
|
+
},
|
|
22752
|
+
observed_at: now.toISOString(),
|
|
22753
|
+
evidence_audit_ids: evidence,
|
|
22754
|
+
fortress_id: ""
|
|
22755
|
+
});
|
|
22756
|
+
}
|
|
22757
|
+
return out;
|
|
22758
|
+
}
|
|
22759
|
+
function computeCountSpikeFinding(windowed, now) {
|
|
22760
|
+
const currentCount = (windowed[0] ?? []).length;
|
|
22761
|
+
const baselineCounts = [];
|
|
22762
|
+
for (let i = 1; i <= BASELINE_WINDOWS5; i += 1) {
|
|
22763
|
+
baselineCounts.push((windowed[i] ?? []).length);
|
|
22764
|
+
}
|
|
22765
|
+
const populated = baselineCounts.filter((c) => c > 0).length;
|
|
22766
|
+
if (populated < BASELINE_WINDOWS5) {
|
|
22767
|
+
return null;
|
|
22768
|
+
}
|
|
22769
|
+
const mean = baselineCounts.reduce((sum, c) => sum + c, 0) / baselineCounts.length;
|
|
22770
|
+
const variance = baselineCounts.reduce((sum, c) => sum + (c - mean) ** 2, 0) / baselineCounts.length;
|
|
22771
|
+
const stddev = Math.sqrt(variance);
|
|
22772
|
+
const warnThreshold = mean + WARN_SIGMA5 * stddev;
|
|
22773
|
+
const alertThreshold = mean + ALERT_SIGMA5 * stddev;
|
|
22774
|
+
if (currentCount > alertThreshold) {
|
|
22775
|
+
return buildCountFinding(
|
|
22776
|
+
currentCount,
|
|
22777
|
+
mean,
|
|
22778
|
+
stddev,
|
|
22779
|
+
ALERT_SIGMA5,
|
|
22780
|
+
"alert",
|
|
22781
|
+
windowed[0] ?? [],
|
|
22782
|
+
now
|
|
22783
|
+
);
|
|
22784
|
+
}
|
|
22785
|
+
if (currentCount > warnThreshold) {
|
|
22786
|
+
return buildCountFinding(
|
|
22787
|
+
currentCount,
|
|
22788
|
+
mean,
|
|
22789
|
+
stddev,
|
|
22790
|
+
WARN_SIGMA5,
|
|
22791
|
+
"warn",
|
|
22792
|
+
windowed[0] ?? [],
|
|
22793
|
+
now
|
|
22794
|
+
);
|
|
22795
|
+
}
|
|
22796
|
+
return null;
|
|
22797
|
+
}
|
|
22798
|
+
function buildCountFinding(currentCount, mean, stddev, sigma, severity, windowZero, now) {
|
|
22799
|
+
const ratio = mean === 0 ? Number.POSITIVE_INFINITY : currentCount / mean;
|
|
22800
|
+
const ratioStr = Number.isFinite(ratio) ? `${ratio.toFixed(1)}x normal` : "no prior baseline";
|
|
22801
|
+
const summary = `Fortress finding count is ${ratioStr}: ${currentCount} findings in last 24h, baseline ${mean.toFixed(1)} +/- ${stddev.toFixed(1)}. Crossed +${sigma} sigma threshold across all sentinels.`;
|
|
22802
|
+
const evidence = windowZero.map((f) => f.finding_id).filter((id) => id.length > 0).slice(0, 50);
|
|
22803
|
+
return {
|
|
22804
|
+
finding_id: "",
|
|
22805
|
+
sentinel_id: ANOMALY_TRIGGER_SENTINEL_ID,
|
|
22806
|
+
severity,
|
|
22807
|
+
summary,
|
|
22808
|
+
details: {
|
|
22809
|
+
trigger: "count_spike",
|
|
22810
|
+
current_count: currentCount,
|
|
22811
|
+
baseline_mean: mean,
|
|
22812
|
+
baseline_stddev: stddev,
|
|
22813
|
+
sigma_threshold: sigma,
|
|
22814
|
+
ratio: Number.isFinite(ratio) ? ratio : null
|
|
22815
|
+
},
|
|
22816
|
+
observed_at: now.toISOString(),
|
|
22817
|
+
evidence_audit_ids: evidence,
|
|
22818
|
+
fortress_id: ""
|
|
22819
|
+
};
|
|
22820
|
+
}
|
|
22821
|
+
function computeNovelComboFinding(windowed, now) {
|
|
22822
|
+
const distinctByWindow = [];
|
|
22823
|
+
for (let i = 0; i <= BASELINE_WINDOWS5; i += 1) {
|
|
22824
|
+
const set = /* @__PURE__ */ new Set();
|
|
22825
|
+
for (const f of windowed[i] ?? []) {
|
|
22826
|
+
set.add(f.sentinel_id);
|
|
22827
|
+
}
|
|
22828
|
+
distinctByWindow.push(set);
|
|
22829
|
+
}
|
|
22830
|
+
const populatedBaselineWindows = distinctByWindow.slice(1).filter((s) => s.size > 0).length;
|
|
22831
|
+
if (populatedBaselineWindows < BASELINE_WINDOWS5) {
|
|
22832
|
+
return null;
|
|
22833
|
+
}
|
|
22834
|
+
const currentCombo = distinctByWindow[0];
|
|
22835
|
+
if (currentCombo.size < 2) return null;
|
|
22836
|
+
const currentKey = comboKey(currentCombo);
|
|
22837
|
+
for (let i = 1; i <= BASELINE_WINDOWS5; i += 1) {
|
|
22838
|
+
if (comboKey(distinctByWindow[i]) === currentKey) {
|
|
22839
|
+
return null;
|
|
22840
|
+
}
|
|
22841
|
+
}
|
|
22842
|
+
const sentinelIds = [...currentCombo].sort();
|
|
22843
|
+
const summary = `Novel sentinel-ID combination this 24h window: ${sentinelIds.join(" + ")}. This co-occurrence pattern has not appeared in the prior ${BASELINE_WINDOWS5} baseline windows.`;
|
|
22844
|
+
const evidence = (windowed[0] ?? []).map((f) => f.finding_id).filter((id) => id.length > 0).slice(0, 50);
|
|
22845
|
+
return {
|
|
22846
|
+
finding_id: "",
|
|
22847
|
+
sentinel_id: ANOMALY_TRIGGER_SENTINEL_ID,
|
|
22848
|
+
severity: "info",
|
|
22849
|
+
summary,
|
|
22850
|
+
details: {
|
|
22851
|
+
trigger: "novel_combo",
|
|
22852
|
+
sentinel_ids: sentinelIds,
|
|
22853
|
+
baseline_window_count: BASELINE_WINDOWS5
|
|
22854
|
+
},
|
|
22855
|
+
observed_at: now.toISOString(),
|
|
22856
|
+
evidence_audit_ids: evidence,
|
|
22857
|
+
fortress_id: ""
|
|
22858
|
+
};
|
|
22859
|
+
}
|
|
22860
|
+
function isWarnOrAlert(s) {
|
|
22861
|
+
return s === "warn" || s === "alert";
|
|
22862
|
+
}
|
|
22863
|
+
function bucketByWindow(findings, nowMs) {
|
|
22864
|
+
const buckets = Array.from(
|
|
22865
|
+
{ length: BASELINE_WINDOWS5 + 1 },
|
|
22866
|
+
() => []
|
|
22867
|
+
);
|
|
22868
|
+
for (const f of findings) {
|
|
22869
|
+
const ts = Date.parse(f.observed_at);
|
|
22870
|
+
if (!Number.isFinite(ts)) continue;
|
|
22871
|
+
const age = nowMs - ts;
|
|
22872
|
+
if (age < 0) continue;
|
|
22873
|
+
const idx = Math.floor(age / WINDOW_MS);
|
|
22874
|
+
if (idx > BASELINE_WINDOWS5) continue;
|
|
22875
|
+
buckets[idx].push(f);
|
|
22876
|
+
}
|
|
22877
|
+
return buckets;
|
|
22878
|
+
}
|
|
22879
|
+
function comboKey(set) {
|
|
22880
|
+
return [...set].sort().join("|");
|
|
22881
|
+
}
|
|
22882
|
+
|
|
22090
22883
|
// src/sentinel/sentinels/index.ts
|
|
22091
22884
|
var PHI1_BASELINE_CATALOG = [
|
|
22092
22885
|
{
|
|
@@ -22108,6 +22901,11 @@ var PHI1_BASELINE_CATALOG = [
|
|
|
22108
22901
|
sentinelId: SUSPICIOUS_TOOL_CALL_SENTINEL_ID,
|
|
22109
22902
|
description: "Surfaces tool calls whose argument shape, call frequency, or permission combination looks unusual for the fortress's recent history.",
|
|
22110
22903
|
factory: () => new SuspiciousToolCallDetector()
|
|
22904
|
+
},
|
|
22905
|
+
{
|
|
22906
|
+
sentinelId: ANOMALY_TRIGGER_SENTINEL_ID,
|
|
22907
|
+
description: "Meta-sentinel. Watches for patterns ACROSS other sentinels' findings: compound suspicious behavior on one agent, fortress-wide finding-count spikes, and novel cross-sentinel combinations. Closes WP-V1.3-1 Sentinel Baseline Pack.",
|
|
22908
|
+
factory: () => new AnomalyTriggerWatcher()
|
|
22111
22909
|
}
|
|
22112
22910
|
];
|
|
22113
22911
|
var FILE_VERSION = 1;
|
|
@@ -40640,6 +41438,28 @@ ${err.message}
|
|
|
40640
41438
|
if (dashboard) {
|
|
40641
41439
|
dashboard.setSentinelDispatcher(sentinelDispatcher);
|
|
40642
41440
|
}
|
|
41441
|
+
const anomalyDispatcher = new AnomalyPipelineDispatcher({
|
|
41442
|
+
findingStore: sentinelFindingStore,
|
|
41443
|
+
auditLog,
|
|
41444
|
+
storage,
|
|
41445
|
+
masterKey,
|
|
41446
|
+
fortressId: fortressIdForAggregator,
|
|
41447
|
+
identityId: aggregatorIdentityId
|
|
41448
|
+
});
|
|
41449
|
+
anomalyDispatcher.start();
|
|
41450
|
+
const handoffLog = new HandoffLog({
|
|
41451
|
+
auditLog,
|
|
41452
|
+
fortressId: fortressIdForAggregator
|
|
41453
|
+
});
|
|
41454
|
+
const handoffEventBridge = new HandoffEventBridge();
|
|
41455
|
+
if (dashboard) {
|
|
41456
|
+
dashboard.setHandoffLog({
|
|
41457
|
+
handoffLog,
|
|
41458
|
+
eventBridge: handoffEventBridge,
|
|
41459
|
+
auditLog,
|
|
41460
|
+
operatorId: aggregatorIdentityId
|
|
41461
|
+
});
|
|
41462
|
+
}
|
|
40643
41463
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
40644
41464
|
const { tools: sanctuaryMetaTools } = createSanctuaryTools({
|
|
40645
41465
|
config,
|