@sanctuary-framework/mcp-server 1.2.9 → 1.2.10
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 +470 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +470 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +448 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +82 -66
- package/dist/index.d.ts +82 -66
- package/dist/index.js +448 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -21794,6 +21794,11 @@ var init_sentinel_dispatcher = __esm({
|
|
|
21794
21794
|
fortressId: this.fortressId,
|
|
21795
21795
|
auditLog: this.auditLog,
|
|
21796
21796
|
now: this.now,
|
|
21797
|
+
// Phi-5 meta-sentinel reads the per-fortress finding store to
|
|
21798
|
+
// detect patterns across other sentinels' findings. First-order
|
|
21799
|
+
// sentinels ignore the field; the dispatcher always attaches it
|
|
21800
|
+
// because the store is already in scope here.
|
|
21801
|
+
findingStore: this.findingStore,
|
|
21797
21802
|
...contextOverrides ?? {}
|
|
21798
21803
|
};
|
|
21799
21804
|
const sentinel = await this.registry.subscribe(sentinelId, context);
|
|
@@ -21935,6 +21940,230 @@ var init_sentinel_dispatcher = __esm({
|
|
|
21935
21940
|
}
|
|
21936
21941
|
});
|
|
21937
21942
|
|
|
21943
|
+
// src/anomaly-detection/types.ts
|
|
21944
|
+
var init_types4 = __esm({
|
|
21945
|
+
"src/anomaly-detection/types.ts"() {
|
|
21946
|
+
}
|
|
21947
|
+
});
|
|
21948
|
+
var ANOMALY_AUDIT_OPS, DEFAULT_TICK_INTERVAL_MS2, AnomalyPipelineDispatcher;
|
|
21949
|
+
var init_anomaly_pipeline = __esm({
|
|
21950
|
+
"src/anomaly-detection/anomaly-pipeline.ts"() {
|
|
21951
|
+
init_types4();
|
|
21952
|
+
ANOMALY_AUDIT_OPS = {
|
|
21953
|
+
DETECTOR_REGISTERED: "anomaly_detector_registered",
|
|
21954
|
+
DETECTOR_UNREGISTERED: "anomaly_detector_unregistered",
|
|
21955
|
+
FINDING_EMITTED: "anomaly_finding_emitted",
|
|
21956
|
+
EVALUATION_FAILED: "anomaly_evaluation_failed",
|
|
21957
|
+
TRAINING_COMPLETED: "anomaly_training_completed",
|
|
21958
|
+
TRAINING_FAILED: "anomaly_training_failed"
|
|
21959
|
+
};
|
|
21960
|
+
DEFAULT_TICK_INTERVAL_MS2 = 6e4;
|
|
21961
|
+
AnomalyPipelineDispatcher = class {
|
|
21962
|
+
findingStore;
|
|
21963
|
+
auditLog;
|
|
21964
|
+
storage;
|
|
21965
|
+
masterKey;
|
|
21966
|
+
fortressId;
|
|
21967
|
+
identityId;
|
|
21968
|
+
now;
|
|
21969
|
+
tickIntervalMs;
|
|
21970
|
+
detectors = /* @__PURE__ */ new Map();
|
|
21971
|
+
listeners = /* @__PURE__ */ new Set();
|
|
21972
|
+
tickTimer = null;
|
|
21973
|
+
tickInFlight = false;
|
|
21974
|
+
constructor(deps) {
|
|
21975
|
+
this.findingStore = deps.findingStore;
|
|
21976
|
+
this.auditLog = deps.auditLog;
|
|
21977
|
+
this.storage = deps.storage;
|
|
21978
|
+
this.masterKey = deps.masterKey;
|
|
21979
|
+
this.fortressId = deps.fortressId;
|
|
21980
|
+
this.identityId = deps.identityId;
|
|
21981
|
+
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
21982
|
+
this.tickIntervalMs = deps.tickIntervalMs ?? DEFAULT_TICK_INTERVAL_MS2;
|
|
21983
|
+
}
|
|
21984
|
+
onEvent(listener) {
|
|
21985
|
+
this.listeners.add(listener);
|
|
21986
|
+
return () => this.listeners.delete(listener);
|
|
21987
|
+
}
|
|
21988
|
+
/**
|
|
21989
|
+
* Register + subscribe a detector to this fortress. Idempotent: a
|
|
21990
|
+
* second call with the same detectorId returns the already-
|
|
21991
|
+
* registered instance without re-subscribing.
|
|
21992
|
+
*/
|
|
21993
|
+
async registerDetector(detector) {
|
|
21994
|
+
const existing = this.detectors.get(detector.detectorId);
|
|
21995
|
+
if (existing) return existing;
|
|
21996
|
+
const context = {
|
|
21997
|
+
fortressId: this.fortressId,
|
|
21998
|
+
auditLog: this.auditLog,
|
|
21999
|
+
storage: this.storage,
|
|
22000
|
+
masterKey: this.masterKey,
|
|
22001
|
+
now: this.now
|
|
22002
|
+
};
|
|
22003
|
+
await detector.subscribe(context);
|
|
22004
|
+
this.detectors.set(detector.detectorId, detector);
|
|
22005
|
+
this.auditLog.append(
|
|
22006
|
+
"l2",
|
|
22007
|
+
ANOMALY_AUDIT_OPS.DETECTOR_REGISTERED,
|
|
22008
|
+
this.identityId,
|
|
22009
|
+
{ detector_id: detector.detectorId, fortress_id: this.fortressId }
|
|
22010
|
+
);
|
|
22011
|
+
return detector;
|
|
22012
|
+
}
|
|
22013
|
+
/**
|
|
22014
|
+
* Unregister + tear down a detector. Idempotent. Returns true when
|
|
22015
|
+
* an active registration was removed.
|
|
22016
|
+
*/
|
|
22017
|
+
async unregisterDetector(detectorId) {
|
|
22018
|
+
const detector = this.detectors.get(detectorId);
|
|
22019
|
+
if (!detector) return false;
|
|
22020
|
+
try {
|
|
22021
|
+
await detector.unsubscribe();
|
|
22022
|
+
} finally {
|
|
22023
|
+
this.detectors.delete(detectorId);
|
|
22024
|
+
}
|
|
22025
|
+
this.auditLog.append(
|
|
22026
|
+
"l2",
|
|
22027
|
+
ANOMALY_AUDIT_OPS.DETECTOR_UNREGISTERED,
|
|
22028
|
+
this.identityId,
|
|
22029
|
+
{ detector_id: detectorId, fortress_id: this.fortressId }
|
|
22030
|
+
);
|
|
22031
|
+
return true;
|
|
22032
|
+
}
|
|
22033
|
+
listDetectors() {
|
|
22034
|
+
return [...this.detectors.keys()];
|
|
22035
|
+
}
|
|
22036
|
+
/** Run one evaluation pass over every registered detector. */
|
|
22037
|
+
async tick() {
|
|
22038
|
+
if (this.tickInFlight) return [];
|
|
22039
|
+
this.tickInFlight = true;
|
|
22040
|
+
try {
|
|
22041
|
+
const findings = [];
|
|
22042
|
+
for (const [detectorId, detector] of this.detectors.entries()) {
|
|
22043
|
+
try {
|
|
22044
|
+
const detectorFindings = await detector.evaluate();
|
|
22045
|
+
for (const raw of detectorFindings) {
|
|
22046
|
+
const stamped = await this.routeFinding(detectorId, raw);
|
|
22047
|
+
findings.push(stamped);
|
|
22048
|
+
}
|
|
22049
|
+
try {
|
|
22050
|
+
const trainingResult = await detector.classifier.train();
|
|
22051
|
+
this.auditLog.append(
|
|
22052
|
+
"l2",
|
|
22053
|
+
ANOMALY_AUDIT_OPS.TRAINING_COMPLETED,
|
|
22054
|
+
this.identityId,
|
|
22055
|
+
{
|
|
22056
|
+
detector_id: detectorId,
|
|
22057
|
+
classifier_id: detector.classifier.classifierId,
|
|
22058
|
+
trained_at: trainingResult.trained_at,
|
|
22059
|
+
sample_count: trainingResult.sample_count,
|
|
22060
|
+
agent_count: trainingResult.agent_count,
|
|
22061
|
+
fortress_id: this.fortressId
|
|
22062
|
+
}
|
|
22063
|
+
);
|
|
22064
|
+
} catch (trainErr) {
|
|
22065
|
+
const message = trainErr instanceof Error ? trainErr.message : String(trainErr);
|
|
22066
|
+
this.auditLog.append(
|
|
22067
|
+
"l2",
|
|
22068
|
+
ANOMALY_AUDIT_OPS.TRAINING_FAILED,
|
|
22069
|
+
this.identityId,
|
|
22070
|
+
{
|
|
22071
|
+
detector_id: detectorId,
|
|
22072
|
+
error_message: message,
|
|
22073
|
+
fortress_id: this.fortressId
|
|
22074
|
+
},
|
|
22075
|
+
"failure"
|
|
22076
|
+
);
|
|
22077
|
+
}
|
|
22078
|
+
} catch (err) {
|
|
22079
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
22080
|
+
const observedAt = this.now().toISOString();
|
|
22081
|
+
this.auditLog.append(
|
|
22082
|
+
"l2",
|
|
22083
|
+
ANOMALY_AUDIT_OPS.EVALUATION_FAILED,
|
|
22084
|
+
this.identityId,
|
|
22085
|
+
{
|
|
22086
|
+
detector_id: detectorId,
|
|
22087
|
+
error_message: message,
|
|
22088
|
+
fortress_id: this.fortressId
|
|
22089
|
+
},
|
|
22090
|
+
"failure"
|
|
22091
|
+
);
|
|
22092
|
+
this.emit({
|
|
22093
|
+
type: "evaluation_failed",
|
|
22094
|
+
detector_id: detectorId,
|
|
22095
|
+
error_message: message,
|
|
22096
|
+
observed_at: observedAt
|
|
22097
|
+
});
|
|
22098
|
+
}
|
|
22099
|
+
}
|
|
22100
|
+
return findings;
|
|
22101
|
+
} finally {
|
|
22102
|
+
this.tickInFlight = false;
|
|
22103
|
+
}
|
|
22104
|
+
}
|
|
22105
|
+
start() {
|
|
22106
|
+
if (this.tickTimer !== null) return;
|
|
22107
|
+
if (this.tickIntervalMs <= 0) return;
|
|
22108
|
+
this.tickTimer = setInterval(() => {
|
|
22109
|
+
void this.tick();
|
|
22110
|
+
}, this.tickIntervalMs);
|
|
22111
|
+
if (typeof this.tickTimer.unref === "function") {
|
|
22112
|
+
this.tickTimer.unref();
|
|
22113
|
+
}
|
|
22114
|
+
}
|
|
22115
|
+
stop() {
|
|
22116
|
+
if (this.tickTimer === null) return;
|
|
22117
|
+
clearInterval(this.tickTimer);
|
|
22118
|
+
this.tickTimer = null;
|
|
22119
|
+
}
|
|
22120
|
+
async dispose() {
|
|
22121
|
+
this.stop();
|
|
22122
|
+
const ids = [...this.detectors.keys()];
|
|
22123
|
+
for (const id of ids) {
|
|
22124
|
+
try {
|
|
22125
|
+
await this.unregisterDetector(id);
|
|
22126
|
+
} catch {
|
|
22127
|
+
}
|
|
22128
|
+
}
|
|
22129
|
+
this.listeners.clear();
|
|
22130
|
+
}
|
|
22131
|
+
async routeFinding(detectorId, raw) {
|
|
22132
|
+
const stamped = {
|
|
22133
|
+
...raw,
|
|
22134
|
+
finding_id: raw.finding_id || crypto.randomUUID(),
|
|
22135
|
+
fortress_id: this.fortressId,
|
|
22136
|
+
observed_at: raw.observed_at || this.now().toISOString()
|
|
22137
|
+
};
|
|
22138
|
+
await this.findingStore.saveFinding(stamped);
|
|
22139
|
+
this.auditLog.append(
|
|
22140
|
+
"l2",
|
|
22141
|
+
ANOMALY_AUDIT_OPS.FINDING_EMITTED,
|
|
22142
|
+
this.identityId,
|
|
22143
|
+
{
|
|
22144
|
+
detector_id: detectorId,
|
|
22145
|
+
finding_id: stamped.finding_id,
|
|
22146
|
+
severity: stamped.severity,
|
|
22147
|
+
anomaly_score: stamped.details["anomaly_score"] ?? null,
|
|
22148
|
+
...stamped.agent_id !== void 0 ? { agent_id: stamped.agent_id } : {},
|
|
22149
|
+
fortress_id: this.fortressId
|
|
22150
|
+
}
|
|
22151
|
+
);
|
|
22152
|
+
this.emit({ type: "finding", finding: stamped });
|
|
22153
|
+
return stamped;
|
|
22154
|
+
}
|
|
22155
|
+
emit(event) {
|
|
22156
|
+
for (const listener of this.listeners) {
|
|
22157
|
+
try {
|
|
22158
|
+
listener(event);
|
|
22159
|
+
} catch {
|
|
22160
|
+
}
|
|
22161
|
+
}
|
|
22162
|
+
}
|
|
22163
|
+
};
|
|
22164
|
+
}
|
|
22165
|
+
});
|
|
22166
|
+
|
|
21938
22167
|
// src/sentinel/sentinel.ts
|
|
21939
22168
|
var Sentinel;
|
|
21940
22169
|
var init_sentinel = __esm({
|
|
@@ -23104,6 +23333,229 @@ var init_suspicious_tool_call_detector = __esm({
|
|
|
23104
23333
|
}
|
|
23105
23334
|
});
|
|
23106
23335
|
|
|
23336
|
+
// src/sentinel/sentinels/anomaly-trigger.ts
|
|
23337
|
+
function computeCompoundFindings(windowZero, now) {
|
|
23338
|
+
const byAgent = /* @__PURE__ */ new Map();
|
|
23339
|
+
for (const f of windowZero) {
|
|
23340
|
+
if (!f.agent_id) continue;
|
|
23341
|
+
if (!isWarnOrAlert(f.severity)) continue;
|
|
23342
|
+
let bucket = byAgent.get(f.agent_id);
|
|
23343
|
+
if (!bucket) {
|
|
23344
|
+
bucket = [];
|
|
23345
|
+
byAgent.set(f.agent_id, bucket);
|
|
23346
|
+
}
|
|
23347
|
+
bucket.push(f);
|
|
23348
|
+
}
|
|
23349
|
+
const out = [];
|
|
23350
|
+
for (const [agentId, group] of byAgent.entries()) {
|
|
23351
|
+
const distinctSentinels = new Set(group.map((f) => f.sentinel_id));
|
|
23352
|
+
if (distinctSentinels.size < COMPOUND_TRIGGER_MIN_SENTINELS) continue;
|
|
23353
|
+
const contributingSentinels = [...distinctSentinels].sort();
|
|
23354
|
+
const evidence = group.map((f) => f.finding_id).filter((id) => id.length > 0).slice(0, 50);
|
|
23355
|
+
const summary = `${agentId} agent triggered ${distinctSentinels.size} distinct sentinels in the last 24h: ${contributingSentinels.join(", ")}. Compound suspicious behavior; review the contributing findings.`;
|
|
23356
|
+
out.push({
|
|
23357
|
+
finding_id: "",
|
|
23358
|
+
sentinel_id: ANOMALY_TRIGGER_SENTINEL_ID,
|
|
23359
|
+
severity: "alert",
|
|
23360
|
+
agent_id: agentId,
|
|
23361
|
+
summary,
|
|
23362
|
+
details: {
|
|
23363
|
+
trigger: "compound",
|
|
23364
|
+
agent_id: agentId,
|
|
23365
|
+
contributing_sentinels: contributingSentinels,
|
|
23366
|
+
contributing_finding_count: group.length
|
|
23367
|
+
},
|
|
23368
|
+
observed_at: now.toISOString(),
|
|
23369
|
+
evidence_audit_ids: evidence,
|
|
23370
|
+
fortress_id: ""
|
|
23371
|
+
});
|
|
23372
|
+
}
|
|
23373
|
+
return out;
|
|
23374
|
+
}
|
|
23375
|
+
function computeCountSpikeFinding(windowed, now) {
|
|
23376
|
+
const currentCount = (windowed[0] ?? []).length;
|
|
23377
|
+
const baselineCounts = [];
|
|
23378
|
+
for (let i = 1; i <= BASELINE_WINDOWS5; i += 1) {
|
|
23379
|
+
baselineCounts.push((windowed[i] ?? []).length);
|
|
23380
|
+
}
|
|
23381
|
+
const populated = baselineCounts.filter((c) => c > 0).length;
|
|
23382
|
+
if (populated < BASELINE_WINDOWS5) {
|
|
23383
|
+
return null;
|
|
23384
|
+
}
|
|
23385
|
+
const mean = baselineCounts.reduce((sum, c) => sum + c, 0) / baselineCounts.length;
|
|
23386
|
+
const variance = baselineCounts.reduce((sum, c) => sum + (c - mean) ** 2, 0) / baselineCounts.length;
|
|
23387
|
+
const stddev = Math.sqrt(variance);
|
|
23388
|
+
const warnThreshold = mean + WARN_SIGMA5 * stddev;
|
|
23389
|
+
const alertThreshold = mean + ALERT_SIGMA5 * stddev;
|
|
23390
|
+
if (currentCount > alertThreshold) {
|
|
23391
|
+
return buildCountFinding(
|
|
23392
|
+
currentCount,
|
|
23393
|
+
mean,
|
|
23394
|
+
stddev,
|
|
23395
|
+
ALERT_SIGMA5,
|
|
23396
|
+
"alert",
|
|
23397
|
+
windowed[0] ?? [],
|
|
23398
|
+
now
|
|
23399
|
+
);
|
|
23400
|
+
}
|
|
23401
|
+
if (currentCount > warnThreshold) {
|
|
23402
|
+
return buildCountFinding(
|
|
23403
|
+
currentCount,
|
|
23404
|
+
mean,
|
|
23405
|
+
stddev,
|
|
23406
|
+
WARN_SIGMA5,
|
|
23407
|
+
"warn",
|
|
23408
|
+
windowed[0] ?? [],
|
|
23409
|
+
now
|
|
23410
|
+
);
|
|
23411
|
+
}
|
|
23412
|
+
return null;
|
|
23413
|
+
}
|
|
23414
|
+
function buildCountFinding(currentCount, mean, stddev, sigma, severity, windowZero, now) {
|
|
23415
|
+
const ratio = mean === 0 ? Number.POSITIVE_INFINITY : currentCount / mean;
|
|
23416
|
+
const ratioStr = Number.isFinite(ratio) ? `${ratio.toFixed(1)}x normal` : "no prior baseline";
|
|
23417
|
+
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.`;
|
|
23418
|
+
const evidence = windowZero.map((f) => f.finding_id).filter((id) => id.length > 0).slice(0, 50);
|
|
23419
|
+
return {
|
|
23420
|
+
finding_id: "",
|
|
23421
|
+
sentinel_id: ANOMALY_TRIGGER_SENTINEL_ID,
|
|
23422
|
+
severity,
|
|
23423
|
+
summary,
|
|
23424
|
+
details: {
|
|
23425
|
+
trigger: "count_spike",
|
|
23426
|
+
current_count: currentCount,
|
|
23427
|
+
baseline_mean: mean,
|
|
23428
|
+
baseline_stddev: stddev,
|
|
23429
|
+
sigma_threshold: sigma,
|
|
23430
|
+
ratio: Number.isFinite(ratio) ? ratio : null
|
|
23431
|
+
},
|
|
23432
|
+
observed_at: now.toISOString(),
|
|
23433
|
+
evidence_audit_ids: evidence,
|
|
23434
|
+
fortress_id: ""
|
|
23435
|
+
};
|
|
23436
|
+
}
|
|
23437
|
+
function computeNovelComboFinding(windowed, now) {
|
|
23438
|
+
const distinctByWindow = [];
|
|
23439
|
+
for (let i = 0; i <= BASELINE_WINDOWS5; i += 1) {
|
|
23440
|
+
const set = /* @__PURE__ */ new Set();
|
|
23441
|
+
for (const f of windowed[i] ?? []) {
|
|
23442
|
+
set.add(f.sentinel_id);
|
|
23443
|
+
}
|
|
23444
|
+
distinctByWindow.push(set);
|
|
23445
|
+
}
|
|
23446
|
+
const populatedBaselineWindows = distinctByWindow.slice(1).filter((s) => s.size > 0).length;
|
|
23447
|
+
if (populatedBaselineWindows < BASELINE_WINDOWS5) {
|
|
23448
|
+
return null;
|
|
23449
|
+
}
|
|
23450
|
+
const currentCombo = distinctByWindow[0];
|
|
23451
|
+
if (currentCombo.size < 2) return null;
|
|
23452
|
+
const currentKey = comboKey(currentCombo);
|
|
23453
|
+
for (let i = 1; i <= BASELINE_WINDOWS5; i += 1) {
|
|
23454
|
+
if (comboKey(distinctByWindow[i]) === currentKey) {
|
|
23455
|
+
return null;
|
|
23456
|
+
}
|
|
23457
|
+
}
|
|
23458
|
+
const sentinelIds = [...currentCombo].sort();
|
|
23459
|
+
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.`;
|
|
23460
|
+
const evidence = (windowed[0] ?? []).map((f) => f.finding_id).filter((id) => id.length > 0).slice(0, 50);
|
|
23461
|
+
return {
|
|
23462
|
+
finding_id: "",
|
|
23463
|
+
sentinel_id: ANOMALY_TRIGGER_SENTINEL_ID,
|
|
23464
|
+
severity: "info",
|
|
23465
|
+
summary,
|
|
23466
|
+
details: {
|
|
23467
|
+
trigger: "novel_combo",
|
|
23468
|
+
sentinel_ids: sentinelIds,
|
|
23469
|
+
baseline_window_count: BASELINE_WINDOWS5
|
|
23470
|
+
},
|
|
23471
|
+
observed_at: now.toISOString(),
|
|
23472
|
+
evidence_audit_ids: evidence,
|
|
23473
|
+
fortress_id: ""
|
|
23474
|
+
};
|
|
23475
|
+
}
|
|
23476
|
+
function isWarnOrAlert(s) {
|
|
23477
|
+
return s === "warn" || s === "alert";
|
|
23478
|
+
}
|
|
23479
|
+
function bucketByWindow(findings, nowMs) {
|
|
23480
|
+
const buckets = Array.from(
|
|
23481
|
+
{ length: BASELINE_WINDOWS5 + 1 },
|
|
23482
|
+
() => []
|
|
23483
|
+
);
|
|
23484
|
+
for (const f of findings) {
|
|
23485
|
+
const ts = Date.parse(f.observed_at);
|
|
23486
|
+
if (!Number.isFinite(ts)) continue;
|
|
23487
|
+
const age = nowMs - ts;
|
|
23488
|
+
if (age < 0) continue;
|
|
23489
|
+
const idx = Math.floor(age / WINDOW_MS);
|
|
23490
|
+
if (idx > BASELINE_WINDOWS5) continue;
|
|
23491
|
+
buckets[idx].push(f);
|
|
23492
|
+
}
|
|
23493
|
+
return buckets;
|
|
23494
|
+
}
|
|
23495
|
+
function comboKey(set) {
|
|
23496
|
+
return [...set].sort().join("|");
|
|
23497
|
+
}
|
|
23498
|
+
var ANOMALY_TRIGGER_SENTINEL_ID, WARN_SIGMA5, ALERT_SIGMA5, BASELINE_WINDOWS5, QUERY_LIMIT5, WINDOW_MS, COMPOUND_TRIGGER_MIN_SENTINELS, AnomalyTriggerWatcher;
|
|
23499
|
+
var init_anomaly_trigger = __esm({
|
|
23500
|
+
"src/sentinel/sentinels/anomaly-trigger.ts"() {
|
|
23501
|
+
init_sentinel();
|
|
23502
|
+
ANOMALY_TRIGGER_SENTINEL_ID = "anomaly-trigger";
|
|
23503
|
+
WARN_SIGMA5 = 3;
|
|
23504
|
+
ALERT_SIGMA5 = 6;
|
|
23505
|
+
BASELINE_WINDOWS5 = 7;
|
|
23506
|
+
QUERY_LIMIT5 = 5e3;
|
|
23507
|
+
WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
23508
|
+
COMPOUND_TRIGGER_MIN_SENTINELS = 2;
|
|
23509
|
+
AnomalyTriggerWatcher = class extends Sentinel {
|
|
23510
|
+
sentinelId = ANOMALY_TRIGGER_SENTINEL_ID;
|
|
23511
|
+
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.";
|
|
23512
|
+
async subscribe(context) {
|
|
23513
|
+
if (!context.findingStore) {
|
|
23514
|
+
throw new Error(
|
|
23515
|
+
`${ANOMALY_TRIGGER_SENTINEL_ID}: findingStore missing from SentinelContext; this meta-sentinel requires the Phi-1 finding store`
|
|
23516
|
+
);
|
|
23517
|
+
}
|
|
23518
|
+
await super.subscribe(context);
|
|
23519
|
+
}
|
|
23520
|
+
async evaluate() {
|
|
23521
|
+
const ctx = this.requireContext();
|
|
23522
|
+
const findingStore = ctx.findingStore;
|
|
23523
|
+
if (!findingStore) {
|
|
23524
|
+
return [];
|
|
23525
|
+
}
|
|
23526
|
+
const now = ctx.now();
|
|
23527
|
+
const nowMs = now.getTime();
|
|
23528
|
+
const windowSpanMs = (BASELINE_WINDOWS5 + 1) * WINDOW_MS;
|
|
23529
|
+
const sinceIso = new Date(nowMs - windowSpanMs).toISOString();
|
|
23530
|
+
let findings;
|
|
23531
|
+
try {
|
|
23532
|
+
findings = await findingStore.listFindings({
|
|
23533
|
+
since: sinceIso,
|
|
23534
|
+
limit: QUERY_LIMIT5
|
|
23535
|
+
});
|
|
23536
|
+
} catch {
|
|
23537
|
+
return [];
|
|
23538
|
+
}
|
|
23539
|
+
const firstOrderFindings = findings.filter(
|
|
23540
|
+
(f) => f.sentinel_id !== ANOMALY_TRIGGER_SENTINEL_ID
|
|
23541
|
+
);
|
|
23542
|
+
const windowed = bucketByWindow(firstOrderFindings, nowMs);
|
|
23543
|
+
const out = [];
|
|
23544
|
+
const compoundFindings = computeCompoundFindings(
|
|
23545
|
+
windowed[0] ?? [],
|
|
23546
|
+
now
|
|
23547
|
+
);
|
|
23548
|
+
out.push(...compoundFindings);
|
|
23549
|
+
const countSpikeFinding = computeCountSpikeFinding(windowed, now);
|
|
23550
|
+
if (countSpikeFinding) out.push(countSpikeFinding);
|
|
23551
|
+
const novelComboFinding = computeNovelComboFinding(windowed, now);
|
|
23552
|
+
if (novelComboFinding) out.push(novelComboFinding);
|
|
23553
|
+
return out;
|
|
23554
|
+
}
|
|
23555
|
+
};
|
|
23556
|
+
}
|
|
23557
|
+
});
|
|
23558
|
+
|
|
23107
23559
|
// src/sentinel/sentinels/index.ts
|
|
23108
23560
|
var PHI1_BASELINE_CATALOG;
|
|
23109
23561
|
var init_sentinels = __esm({
|
|
@@ -23112,6 +23564,7 @@ var init_sentinels = __esm({
|
|
|
23112
23564
|
init_cross_agent_chatter_watcher();
|
|
23113
23565
|
init_credential_usage_watcher();
|
|
23114
23566
|
init_suspicious_tool_call_detector();
|
|
23567
|
+
init_anomaly_trigger();
|
|
23115
23568
|
PHI1_BASELINE_CATALOG = [
|
|
23116
23569
|
{
|
|
23117
23570
|
sentinelId: EGRESS_VOLUME_SENTINEL_ID,
|
|
@@ -23132,6 +23585,11 @@ var init_sentinels = __esm({
|
|
|
23132
23585
|
sentinelId: SUSPICIOUS_TOOL_CALL_SENTINEL_ID,
|
|
23133
23586
|
description: "Surfaces tool calls whose argument shape, call frequency, or permission combination looks unusual for the fortress's recent history.",
|
|
23134
23587
|
factory: () => new SuspiciousToolCallDetector()
|
|
23588
|
+
},
|
|
23589
|
+
{
|
|
23590
|
+
sentinelId: ANOMALY_TRIGGER_SENTINEL_ID,
|
|
23591
|
+
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.",
|
|
23592
|
+
factory: () => new AnomalyTriggerWatcher()
|
|
23135
23593
|
}
|
|
23136
23594
|
];
|
|
23137
23595
|
}
|
|
@@ -34650,7 +35108,7 @@ var init_recovery_key_disclosure = __esm({
|
|
|
34650
35108
|
});
|
|
34651
35109
|
|
|
34652
35110
|
// src/hub/types.ts
|
|
34653
|
-
var
|
|
35111
|
+
var init_types5 = __esm({
|
|
34654
35112
|
"src/hub/types.ts"() {
|
|
34655
35113
|
}
|
|
34656
35114
|
});
|
|
@@ -35689,7 +36147,7 @@ var init_hub = __esm({
|
|
|
35689
36147
|
"src/hub/index.ts"() {
|
|
35690
36148
|
init_constants3();
|
|
35691
36149
|
init_errors4();
|
|
35692
|
-
|
|
36150
|
+
init_types5();
|
|
35693
36151
|
init_agent_registry();
|
|
35694
36152
|
init_inbox_store();
|
|
35695
36153
|
init_inbox_aggregator();
|
|
@@ -42193,6 +42651,15 @@ ${err.message}
|
|
|
42193
42651
|
if (dashboard) {
|
|
42194
42652
|
dashboard.setSentinelDispatcher(sentinelDispatcher);
|
|
42195
42653
|
}
|
|
42654
|
+
const anomalyDispatcher = new AnomalyPipelineDispatcher({
|
|
42655
|
+
findingStore: sentinelFindingStore,
|
|
42656
|
+
auditLog,
|
|
42657
|
+
storage,
|
|
42658
|
+
masterKey,
|
|
42659
|
+
fortressId: fortressIdForAggregator,
|
|
42660
|
+
identityId: aggregatorIdentityId
|
|
42661
|
+
});
|
|
42662
|
+
anomalyDispatcher.start();
|
|
42196
42663
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
42197
42664
|
const { tools: sanctuaryMetaTools } = createSanctuaryTools({
|
|
42198
42665
|
config,
|
|
@@ -42389,6 +42856,7 @@ var init_src = __esm({
|
|
|
42389
42856
|
init_sentinel_finding_store();
|
|
42390
42857
|
init_sentinel_registry();
|
|
42391
42858
|
init_sentinel_dispatcher();
|
|
42859
|
+
init_anomaly_pipeline();
|
|
42392
42860
|
init_sentinels();
|
|
42393
42861
|
init_subscription_store();
|
|
42394
42862
|
init_tools4();
|