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