@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/index.js CHANGED
@@ -20802,6 +20802,11 @@ var SentinelDispatcher = class {
20802
20802
  fortressId: this.fortressId,
20803
20803
  auditLog: this.auditLog,
20804
20804
  now: this.now,
20805
+ // Phi-5 meta-sentinel reads the per-fortress finding store to
20806
+ // detect patterns across other sentinels' findings. First-order
20807
+ // sentinels ignore the field; the dispatcher always attaches it
20808
+ // because the store is already in scope here.
20809
+ findingStore: this.findingStore,
20805
20810
  ...contextOverrides ?? {}
20806
20811
  };
20807
20812
  const sentinel = await this.registry.subscribe(sentinelId, context);
@@ -20940,6 +20945,218 @@ var SentinelDispatcher = class {
20940
20945
  }
20941
20946
  }
20942
20947
  };
20948
+ var ANOMALY_AUDIT_OPS = {
20949
+ DETECTOR_REGISTERED: "anomaly_detector_registered",
20950
+ DETECTOR_UNREGISTERED: "anomaly_detector_unregistered",
20951
+ FINDING_EMITTED: "anomaly_finding_emitted",
20952
+ EVALUATION_FAILED: "anomaly_evaluation_failed",
20953
+ TRAINING_COMPLETED: "anomaly_training_completed",
20954
+ TRAINING_FAILED: "anomaly_training_failed"
20955
+ };
20956
+ var DEFAULT_TICK_INTERVAL_MS2 = 6e4;
20957
+ var AnomalyPipelineDispatcher = class {
20958
+ findingStore;
20959
+ auditLog;
20960
+ storage;
20961
+ masterKey;
20962
+ fortressId;
20963
+ identityId;
20964
+ now;
20965
+ tickIntervalMs;
20966
+ detectors = /* @__PURE__ */ new Map();
20967
+ listeners = /* @__PURE__ */ new Set();
20968
+ tickTimer = null;
20969
+ tickInFlight = false;
20970
+ constructor(deps) {
20971
+ this.findingStore = deps.findingStore;
20972
+ this.auditLog = deps.auditLog;
20973
+ this.storage = deps.storage;
20974
+ this.masterKey = deps.masterKey;
20975
+ this.fortressId = deps.fortressId;
20976
+ this.identityId = deps.identityId;
20977
+ this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
20978
+ this.tickIntervalMs = deps.tickIntervalMs ?? DEFAULT_TICK_INTERVAL_MS2;
20979
+ }
20980
+ onEvent(listener) {
20981
+ this.listeners.add(listener);
20982
+ return () => this.listeners.delete(listener);
20983
+ }
20984
+ /**
20985
+ * Register + subscribe a detector to this fortress. Idempotent: a
20986
+ * second call with the same detectorId returns the already-
20987
+ * registered instance without re-subscribing.
20988
+ */
20989
+ async registerDetector(detector) {
20990
+ const existing = this.detectors.get(detector.detectorId);
20991
+ if (existing) return existing;
20992
+ const context = {
20993
+ fortressId: this.fortressId,
20994
+ auditLog: this.auditLog,
20995
+ storage: this.storage,
20996
+ masterKey: this.masterKey,
20997
+ now: this.now
20998
+ };
20999
+ await detector.subscribe(context);
21000
+ this.detectors.set(detector.detectorId, detector);
21001
+ this.auditLog.append(
21002
+ "l2",
21003
+ ANOMALY_AUDIT_OPS.DETECTOR_REGISTERED,
21004
+ this.identityId,
21005
+ { detector_id: detector.detectorId, fortress_id: this.fortressId }
21006
+ );
21007
+ return detector;
21008
+ }
21009
+ /**
21010
+ * Unregister + tear down a detector. Idempotent. Returns true when
21011
+ * an active registration was removed.
21012
+ */
21013
+ async unregisterDetector(detectorId) {
21014
+ const detector = this.detectors.get(detectorId);
21015
+ if (!detector) return false;
21016
+ try {
21017
+ await detector.unsubscribe();
21018
+ } finally {
21019
+ this.detectors.delete(detectorId);
21020
+ }
21021
+ this.auditLog.append(
21022
+ "l2",
21023
+ ANOMALY_AUDIT_OPS.DETECTOR_UNREGISTERED,
21024
+ this.identityId,
21025
+ { detector_id: detectorId, fortress_id: this.fortressId }
21026
+ );
21027
+ return true;
21028
+ }
21029
+ listDetectors() {
21030
+ return [...this.detectors.keys()];
21031
+ }
21032
+ /** Run one evaluation pass over every registered detector. */
21033
+ async tick() {
21034
+ if (this.tickInFlight) return [];
21035
+ this.tickInFlight = true;
21036
+ try {
21037
+ const findings = [];
21038
+ for (const [detectorId, detector] of this.detectors.entries()) {
21039
+ try {
21040
+ const detectorFindings = await detector.evaluate();
21041
+ for (const raw of detectorFindings) {
21042
+ const stamped = await this.routeFinding(detectorId, raw);
21043
+ findings.push(stamped);
21044
+ }
21045
+ try {
21046
+ const trainingResult = await detector.classifier.train();
21047
+ this.auditLog.append(
21048
+ "l2",
21049
+ ANOMALY_AUDIT_OPS.TRAINING_COMPLETED,
21050
+ this.identityId,
21051
+ {
21052
+ detector_id: detectorId,
21053
+ classifier_id: detector.classifier.classifierId,
21054
+ trained_at: trainingResult.trained_at,
21055
+ sample_count: trainingResult.sample_count,
21056
+ agent_count: trainingResult.agent_count,
21057
+ fortress_id: this.fortressId
21058
+ }
21059
+ );
21060
+ } catch (trainErr) {
21061
+ const message = trainErr instanceof Error ? trainErr.message : String(trainErr);
21062
+ this.auditLog.append(
21063
+ "l2",
21064
+ ANOMALY_AUDIT_OPS.TRAINING_FAILED,
21065
+ this.identityId,
21066
+ {
21067
+ detector_id: detectorId,
21068
+ error_message: message,
21069
+ fortress_id: this.fortressId
21070
+ },
21071
+ "failure"
21072
+ );
21073
+ }
21074
+ } catch (err) {
21075
+ const message = err instanceof Error ? err.message : String(err);
21076
+ const observedAt = this.now().toISOString();
21077
+ this.auditLog.append(
21078
+ "l2",
21079
+ ANOMALY_AUDIT_OPS.EVALUATION_FAILED,
21080
+ this.identityId,
21081
+ {
21082
+ detector_id: detectorId,
21083
+ error_message: message,
21084
+ fortress_id: this.fortressId
21085
+ },
21086
+ "failure"
21087
+ );
21088
+ this.emit({
21089
+ type: "evaluation_failed",
21090
+ detector_id: detectorId,
21091
+ error_message: message,
21092
+ observed_at: observedAt
21093
+ });
21094
+ }
21095
+ }
21096
+ return findings;
21097
+ } finally {
21098
+ this.tickInFlight = false;
21099
+ }
21100
+ }
21101
+ start() {
21102
+ if (this.tickTimer !== null) return;
21103
+ if (this.tickIntervalMs <= 0) return;
21104
+ this.tickTimer = setInterval(() => {
21105
+ void this.tick();
21106
+ }, this.tickIntervalMs);
21107
+ if (typeof this.tickTimer.unref === "function") {
21108
+ this.tickTimer.unref();
21109
+ }
21110
+ }
21111
+ stop() {
21112
+ if (this.tickTimer === null) return;
21113
+ clearInterval(this.tickTimer);
21114
+ this.tickTimer = null;
21115
+ }
21116
+ async dispose() {
21117
+ this.stop();
21118
+ const ids = [...this.detectors.keys()];
21119
+ for (const id of ids) {
21120
+ try {
21121
+ await this.unregisterDetector(id);
21122
+ } catch {
21123
+ }
21124
+ }
21125
+ this.listeners.clear();
21126
+ }
21127
+ async routeFinding(detectorId, raw) {
21128
+ const stamped = {
21129
+ ...raw,
21130
+ finding_id: raw.finding_id || randomUUID(),
21131
+ fortress_id: this.fortressId,
21132
+ observed_at: raw.observed_at || this.now().toISOString()
21133
+ };
21134
+ await this.findingStore.saveFinding(stamped);
21135
+ this.auditLog.append(
21136
+ "l2",
21137
+ ANOMALY_AUDIT_OPS.FINDING_EMITTED,
21138
+ this.identityId,
21139
+ {
21140
+ detector_id: detectorId,
21141
+ finding_id: stamped.finding_id,
21142
+ severity: stamped.severity,
21143
+ anomaly_score: stamped.details["anomaly_score"] ?? null,
21144
+ ...stamped.agent_id !== void 0 ? { agent_id: stamped.agent_id } : {},
21145
+ fortress_id: this.fortressId
21146
+ }
21147
+ );
21148
+ this.emit({ type: "finding", finding: stamped });
21149
+ return stamped;
21150
+ }
21151
+ emit(event) {
21152
+ for (const listener of this.listeners) {
21153
+ try {
21154
+ listener(event);
21155
+ } catch {
21156
+ }
21157
+ }
21158
+ }
21159
+ };
20943
21160
 
20944
21161
  // src/sentinel/sentinel.ts
20945
21162
  var Sentinel = class {
@@ -22080,6 +22297,223 @@ function truncateSummary2(s) {
22080
22297
  return s.length > 240 ? s.slice(0, 237) + "..." : s;
22081
22298
  }
22082
22299
 
22300
+ // src/sentinel/sentinels/anomaly-trigger.ts
22301
+ var ANOMALY_TRIGGER_SENTINEL_ID = "anomaly-trigger";
22302
+ var WARN_SIGMA5 = 3;
22303
+ var ALERT_SIGMA5 = 6;
22304
+ var BASELINE_WINDOWS5 = 7;
22305
+ var QUERY_LIMIT5 = 5e3;
22306
+ var WINDOW_MS = 24 * 60 * 60 * 1e3;
22307
+ var COMPOUND_TRIGGER_MIN_SENTINELS = 2;
22308
+ var AnomalyTriggerWatcher = class extends Sentinel {
22309
+ sentinelId = ANOMALY_TRIGGER_SENTINEL_ID;
22310
+ 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.";
22311
+ async subscribe(context) {
22312
+ if (!context.findingStore) {
22313
+ throw new Error(
22314
+ `${ANOMALY_TRIGGER_SENTINEL_ID}: findingStore missing from SentinelContext; this meta-sentinel requires the Phi-1 finding store`
22315
+ );
22316
+ }
22317
+ await super.subscribe(context);
22318
+ }
22319
+ async evaluate() {
22320
+ const ctx = this.requireContext();
22321
+ const findingStore = ctx.findingStore;
22322
+ if (!findingStore) {
22323
+ return [];
22324
+ }
22325
+ const now = ctx.now();
22326
+ const nowMs = now.getTime();
22327
+ const windowSpanMs = (BASELINE_WINDOWS5 + 1) * WINDOW_MS;
22328
+ const sinceIso = new Date(nowMs - windowSpanMs).toISOString();
22329
+ let findings;
22330
+ try {
22331
+ findings = await findingStore.listFindings({
22332
+ since: sinceIso,
22333
+ limit: QUERY_LIMIT5
22334
+ });
22335
+ } catch {
22336
+ return [];
22337
+ }
22338
+ const firstOrderFindings = findings.filter(
22339
+ (f) => f.sentinel_id !== ANOMALY_TRIGGER_SENTINEL_ID
22340
+ );
22341
+ const windowed = bucketByWindow(firstOrderFindings, nowMs);
22342
+ const out = [];
22343
+ const compoundFindings = computeCompoundFindings(
22344
+ windowed[0] ?? [],
22345
+ now
22346
+ );
22347
+ out.push(...compoundFindings);
22348
+ const countSpikeFinding = computeCountSpikeFinding(windowed, now);
22349
+ if (countSpikeFinding) out.push(countSpikeFinding);
22350
+ const novelComboFinding = computeNovelComboFinding(windowed, now);
22351
+ if (novelComboFinding) out.push(novelComboFinding);
22352
+ return out;
22353
+ }
22354
+ };
22355
+ function computeCompoundFindings(windowZero, now) {
22356
+ const byAgent = /* @__PURE__ */ new Map();
22357
+ for (const f of windowZero) {
22358
+ if (!f.agent_id) continue;
22359
+ if (!isWarnOrAlert(f.severity)) continue;
22360
+ let bucket = byAgent.get(f.agent_id);
22361
+ if (!bucket) {
22362
+ bucket = [];
22363
+ byAgent.set(f.agent_id, bucket);
22364
+ }
22365
+ bucket.push(f);
22366
+ }
22367
+ const out = [];
22368
+ for (const [agentId, group] of byAgent.entries()) {
22369
+ const distinctSentinels = new Set(group.map((f) => f.sentinel_id));
22370
+ if (distinctSentinels.size < COMPOUND_TRIGGER_MIN_SENTINELS) continue;
22371
+ const contributingSentinels = [...distinctSentinels].sort();
22372
+ const evidence = group.map((f) => f.finding_id).filter((id) => id.length > 0).slice(0, 50);
22373
+ const summary = `${agentId} agent triggered ${distinctSentinels.size} distinct sentinels in the last 24h: ${contributingSentinels.join(", ")}. Compound suspicious behavior; review the contributing findings.`;
22374
+ out.push({
22375
+ finding_id: "",
22376
+ sentinel_id: ANOMALY_TRIGGER_SENTINEL_ID,
22377
+ severity: "alert",
22378
+ agent_id: agentId,
22379
+ summary,
22380
+ details: {
22381
+ trigger: "compound",
22382
+ agent_id: agentId,
22383
+ contributing_sentinels: contributingSentinels,
22384
+ contributing_finding_count: group.length
22385
+ },
22386
+ observed_at: now.toISOString(),
22387
+ evidence_audit_ids: evidence,
22388
+ fortress_id: ""
22389
+ });
22390
+ }
22391
+ return out;
22392
+ }
22393
+ function computeCountSpikeFinding(windowed, now) {
22394
+ const currentCount = (windowed[0] ?? []).length;
22395
+ const baselineCounts = [];
22396
+ for (let i = 1; i <= BASELINE_WINDOWS5; i += 1) {
22397
+ baselineCounts.push((windowed[i] ?? []).length);
22398
+ }
22399
+ const populated = baselineCounts.filter((c) => c > 0).length;
22400
+ if (populated < BASELINE_WINDOWS5) {
22401
+ return null;
22402
+ }
22403
+ const mean = baselineCounts.reduce((sum, c) => sum + c, 0) / baselineCounts.length;
22404
+ const variance = baselineCounts.reduce((sum, c) => sum + (c - mean) ** 2, 0) / baselineCounts.length;
22405
+ const stddev = Math.sqrt(variance);
22406
+ const warnThreshold = mean + WARN_SIGMA5 * stddev;
22407
+ const alertThreshold = mean + ALERT_SIGMA5 * stddev;
22408
+ if (currentCount > alertThreshold) {
22409
+ return buildCountFinding(
22410
+ currentCount,
22411
+ mean,
22412
+ stddev,
22413
+ ALERT_SIGMA5,
22414
+ "alert",
22415
+ windowed[0] ?? [],
22416
+ now
22417
+ );
22418
+ }
22419
+ if (currentCount > warnThreshold) {
22420
+ return buildCountFinding(
22421
+ currentCount,
22422
+ mean,
22423
+ stddev,
22424
+ WARN_SIGMA5,
22425
+ "warn",
22426
+ windowed[0] ?? [],
22427
+ now
22428
+ );
22429
+ }
22430
+ return null;
22431
+ }
22432
+ function buildCountFinding(currentCount, mean, stddev, sigma, severity, windowZero, now) {
22433
+ const ratio = mean === 0 ? Number.POSITIVE_INFINITY : currentCount / mean;
22434
+ const ratioStr = Number.isFinite(ratio) ? `${ratio.toFixed(1)}x normal` : "no prior baseline";
22435
+ 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.`;
22436
+ const evidence = windowZero.map((f) => f.finding_id).filter((id) => id.length > 0).slice(0, 50);
22437
+ return {
22438
+ finding_id: "",
22439
+ sentinel_id: ANOMALY_TRIGGER_SENTINEL_ID,
22440
+ severity,
22441
+ summary,
22442
+ details: {
22443
+ trigger: "count_spike",
22444
+ current_count: currentCount,
22445
+ baseline_mean: mean,
22446
+ baseline_stddev: stddev,
22447
+ sigma_threshold: sigma,
22448
+ ratio: Number.isFinite(ratio) ? ratio : null
22449
+ },
22450
+ observed_at: now.toISOString(),
22451
+ evidence_audit_ids: evidence,
22452
+ fortress_id: ""
22453
+ };
22454
+ }
22455
+ function computeNovelComboFinding(windowed, now) {
22456
+ const distinctByWindow = [];
22457
+ for (let i = 0; i <= BASELINE_WINDOWS5; i += 1) {
22458
+ const set = /* @__PURE__ */ new Set();
22459
+ for (const f of windowed[i] ?? []) {
22460
+ set.add(f.sentinel_id);
22461
+ }
22462
+ distinctByWindow.push(set);
22463
+ }
22464
+ const populatedBaselineWindows = distinctByWindow.slice(1).filter((s) => s.size > 0).length;
22465
+ if (populatedBaselineWindows < BASELINE_WINDOWS5) {
22466
+ return null;
22467
+ }
22468
+ const currentCombo = distinctByWindow[0];
22469
+ if (currentCombo.size < 2) return null;
22470
+ const currentKey = comboKey(currentCombo);
22471
+ for (let i = 1; i <= BASELINE_WINDOWS5; i += 1) {
22472
+ if (comboKey(distinctByWindow[i]) === currentKey) {
22473
+ return null;
22474
+ }
22475
+ }
22476
+ const sentinelIds = [...currentCombo].sort();
22477
+ 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.`;
22478
+ const evidence = (windowed[0] ?? []).map((f) => f.finding_id).filter((id) => id.length > 0).slice(0, 50);
22479
+ return {
22480
+ finding_id: "",
22481
+ sentinel_id: ANOMALY_TRIGGER_SENTINEL_ID,
22482
+ severity: "info",
22483
+ summary,
22484
+ details: {
22485
+ trigger: "novel_combo",
22486
+ sentinel_ids: sentinelIds,
22487
+ baseline_window_count: BASELINE_WINDOWS5
22488
+ },
22489
+ observed_at: now.toISOString(),
22490
+ evidence_audit_ids: evidence,
22491
+ fortress_id: ""
22492
+ };
22493
+ }
22494
+ function isWarnOrAlert(s) {
22495
+ return s === "warn" || s === "alert";
22496
+ }
22497
+ function bucketByWindow(findings, nowMs) {
22498
+ const buckets = Array.from(
22499
+ { length: BASELINE_WINDOWS5 + 1 },
22500
+ () => []
22501
+ );
22502
+ for (const f of findings) {
22503
+ const ts = Date.parse(f.observed_at);
22504
+ if (!Number.isFinite(ts)) continue;
22505
+ const age = nowMs - ts;
22506
+ if (age < 0) continue;
22507
+ const idx = Math.floor(age / WINDOW_MS);
22508
+ if (idx > BASELINE_WINDOWS5) continue;
22509
+ buckets[idx].push(f);
22510
+ }
22511
+ return buckets;
22512
+ }
22513
+ function comboKey(set) {
22514
+ return [...set].sort().join("|");
22515
+ }
22516
+
22083
22517
  // src/sentinel/sentinels/index.ts
22084
22518
  var PHI1_BASELINE_CATALOG = [
22085
22519
  {
@@ -22101,6 +22535,11 @@ var PHI1_BASELINE_CATALOG = [
22101
22535
  sentinelId: SUSPICIOUS_TOOL_CALL_SENTINEL_ID,
22102
22536
  description: "Surfaces tool calls whose argument shape, call frequency, or permission combination looks unusual for the fortress's recent history.",
22103
22537
  factory: () => new SuspiciousToolCallDetector()
22538
+ },
22539
+ {
22540
+ sentinelId: ANOMALY_TRIGGER_SENTINEL_ID,
22541
+ 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.",
22542
+ factory: () => new AnomalyTriggerWatcher()
22104
22543
  }
22105
22544
  ];
22106
22545
  var FILE_VERSION = 1;
@@ -40633,6 +41072,15 @@ ${err.message}
40633
41072
  if (dashboard) {
40634
41073
  dashboard.setSentinelDispatcher(sentinelDispatcher);
40635
41074
  }
41075
+ const anomalyDispatcher = new AnomalyPipelineDispatcher({
41076
+ findingStore: sentinelFindingStore,
41077
+ auditLog,
41078
+ storage,
41079
+ masterKey,
41080
+ fortressId: fortressIdForAggregator,
41081
+ identityId: aggregatorIdentityId
41082
+ });
41083
+ anomalyDispatcher.start();
40636
41084
  const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
40637
41085
  const { tools: sanctuaryMetaTools } = createSanctuaryTools({
40638
41086
  config,