@sanctuary-framework/mcp-server 1.2.8 → 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 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 init_types4 = __esm({
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
- init_types4();
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();