@sanctuary-framework/mcp-server 1.2.9 → 1.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -17473,6 +17473,318 @@ var init_sentinel_routes = __esm({
17473
17473
  FINDINGS_MAX_LIMIT = 500;
17474
17474
  }
17475
17475
  });
17476
+ function makeEntryId(auditEventId) {
17477
+ return createHash("sha256").update(auditEventId).digest("hex").slice(0, 32);
17478
+ }
17479
+ function optString(details, key) {
17480
+ if (!details) return null;
17481
+ const value = details[key];
17482
+ if (typeof value !== "string" || value.length === 0) return null;
17483
+ return value;
17484
+ }
17485
+ function auditEventIdFallback(audit) {
17486
+ return `${audit.timestamp}:${audit.operation}`;
17487
+ }
17488
+ function localHandoffSummary(details, sender, recipient) {
17489
+ const taskScope = optString(details, "task_scope");
17490
+ const reasonClass = optString(details, "reason_class");
17491
+ if (taskScope) {
17492
+ return `${sender} -> ${recipient} handoff: ${taskScope}`;
17493
+ }
17494
+ if (reasonClass) {
17495
+ return `${sender} -> ${recipient} handoff (${reasonClass})`;
17496
+ }
17497
+ return `${sender} -> ${recipient} handoff`;
17498
+ }
17499
+ function crossHarnessSummary(details, sender) {
17500
+ const policyRule = optString(details, "policy_rule_id");
17501
+ if (policyRule) {
17502
+ return `${sender} -> operator approval (${policyRule})`;
17503
+ }
17504
+ return `${sender} -> operator approval`;
17505
+ }
17506
+ var HANDOFF_LOG_OBSERVED_OPS, OBSERVED_OP_LIST, OPERATOR_PSEUDO_AGENT, DEFAULT_LIMIT, MAX_LIMIT, AUDIT_QUERY_LIMIT, HandoffLog, COORDINATION_VIEW_AUDIT_OPS;
17507
+ var init_handoff_log = __esm({
17508
+ "src/coordination/handoff-log.ts"() {
17509
+ HANDOFF_LOG_OBSERVED_OPS = {
17510
+ /** Tau-3 in-process coordination handoff. */
17511
+ LOCAL_HANDOFF: "v1.1_local_handoff",
17512
+ /** Upsilon-1 cross-harness approval (wrapped-agent -> operator). */
17513
+ CROSS_HARNESS_APPROVAL: "cross_harness_approval_aggregated"
17514
+ };
17515
+ OBSERVED_OP_LIST = [
17516
+ HANDOFF_LOG_OBSERVED_OPS.LOCAL_HANDOFF,
17517
+ HANDOFF_LOG_OBSERVED_OPS.CROSS_HARNESS_APPROVAL
17518
+ ];
17519
+ OPERATOR_PSEUDO_AGENT = "operator";
17520
+ DEFAULT_LIMIT = 50;
17521
+ MAX_LIMIT = 500;
17522
+ AUDIT_QUERY_LIMIT = 1e4;
17523
+ HandoffLog = class {
17524
+ auditLog;
17525
+ fortressId;
17526
+ constructor(opts) {
17527
+ this.auditLog = opts.auditLog;
17528
+ this.fortressId = opts.fortressId;
17529
+ }
17530
+ /** Stable fortress id this HandoffLog reads. */
17531
+ getFortressId() {
17532
+ return this.fortressId;
17533
+ }
17534
+ /**
17535
+ * Query handoffs in chronological-newest-first order. Filters
17536
+ * applied after normalization so the per-event-class shape
17537
+ * differences (sender field name, recipient inference) are handled
17538
+ * once.
17539
+ */
17540
+ async query(opts) {
17541
+ const limit = Math.min(opts.limit ?? DEFAULT_LIMIT, MAX_LIMIT);
17542
+ const queryResult = await this.auditLog.query({
17543
+ ...opts.since !== void 0 ? { since: opts.since } : {},
17544
+ layer: "l2",
17545
+ limit: AUDIT_QUERY_LIMIT
17546
+ });
17547
+ const normalized = [];
17548
+ for (const entry of queryResult.entries) {
17549
+ if (!OBSERVED_OP_LIST.includes(entry.operation)) continue;
17550
+ const handoff = this.normalize(entry);
17551
+ if (!handoff) continue;
17552
+ if (opts.until && handoff.observed_at > opts.until) continue;
17553
+ if (opts.since && handoff.observed_at < opts.since) continue;
17554
+ if (opts.agent_id) {
17555
+ if (handoff.source_agent_id !== opts.agent_id && handoff.target_agent_id !== opts.agent_id) {
17556
+ continue;
17557
+ }
17558
+ }
17559
+ normalized.push(handoff);
17560
+ }
17561
+ normalized.sort((a, b) => a.observed_at < b.observed_at ? 1 : -1);
17562
+ return normalized.slice(0, limit);
17563
+ }
17564
+ /**
17565
+ * Look up a single entry by id. Returns the normalized entry +
17566
+ * the source audit payload for operator-facing detail rendering.
17567
+ * Returns null when no audit entry maps to the given id.
17568
+ */
17569
+ async getEntry(entryId) {
17570
+ const queryResult = await this.auditLog.query({
17571
+ layer: "l2",
17572
+ limit: AUDIT_QUERY_LIMIT
17573
+ });
17574
+ for (const audit of queryResult.entries) {
17575
+ if (!OBSERVED_OP_LIST.includes(audit.operation)) continue;
17576
+ const handoff = this.normalize(audit);
17577
+ if (!handoff) continue;
17578
+ if (handoff.entry_id === entryId) {
17579
+ return { entry: handoff, source_audit_entry: audit };
17580
+ }
17581
+ }
17582
+ return null;
17583
+ }
17584
+ normalize(audit) {
17585
+ const details = audit.details;
17586
+ if (audit.operation === HANDOFF_LOG_OBSERVED_OPS.LOCAL_HANDOFF) {
17587
+ const sender = optString(details, "sender_agent_id");
17588
+ const recipient = optString(details, "recipient_agent_id");
17589
+ if (!sender || !recipient || sender === recipient) return null;
17590
+ const auditEventId = optString(details, "event_id") ?? auditEventIdFallback(audit);
17591
+ return {
17592
+ entry_id: makeEntryId(auditEventId),
17593
+ audit_event_id: auditEventId,
17594
+ source_agent_id: sender,
17595
+ target_agent_id: recipient,
17596
+ observed_at: audit.timestamp,
17597
+ event_class: audit.operation,
17598
+ context_transfer_summary: localHandoffSummary(details, sender, recipient),
17599
+ workflow_link: null
17600
+ };
17601
+ }
17602
+ if (audit.operation === HANDOFF_LOG_OBSERVED_OPS.CROSS_HARNESS_APPROVAL) {
17603
+ const sender = optString(details, "source_harness") ?? optString(details, "source_agent_id");
17604
+ if (!sender) return null;
17605
+ const auditEventId = optString(details, "aggregator_id") ?? auditEventIdFallback(audit);
17606
+ return {
17607
+ entry_id: makeEntryId(auditEventId),
17608
+ audit_event_id: auditEventId,
17609
+ source_agent_id: sender,
17610
+ target_agent_id: OPERATOR_PSEUDO_AGENT,
17611
+ observed_at: audit.timestamp,
17612
+ event_class: audit.operation,
17613
+ context_transfer_summary: crossHarnessSummary(details, sender),
17614
+ workflow_link: null
17615
+ };
17616
+ }
17617
+ return null;
17618
+ }
17619
+ };
17620
+ COORDINATION_VIEW_AUDIT_OPS = {
17621
+ VIEW_OPENED: "operator_coordination_view_opened",
17622
+ ENTRY_DRILLED: "operator_handoff_entry_drilled"
17623
+ };
17624
+ }
17625
+ });
17626
+
17627
+ // src/coordination/handoff-routes.ts
17628
+ function writeJSON6(res, status, payload) {
17629
+ res.writeHead(status, {
17630
+ "Content-Type": "application/json",
17631
+ "Cache-Control": "no-store"
17632
+ });
17633
+ res.end(JSON.stringify(payload));
17634
+ }
17635
+ function parseLimit4(raw, defaultValue, max) {
17636
+ if (raw === null || raw === "") return defaultValue;
17637
+ const parsed = Number.parseInt(raw, 10);
17638
+ if (Number.isNaN(parsed) || parsed < 0) return defaultValue;
17639
+ return Math.min(parsed, max);
17640
+ }
17641
+ function matchEntryRoute2(path) {
17642
+ const prefix = `${COORDINATION_HANDOFFS_PREFIX}/`;
17643
+ if (!path.startsWith(prefix)) return null;
17644
+ const rest = path.slice(prefix.length);
17645
+ if (rest.length === 0 || rest === "stream") return null;
17646
+ if (rest.includes("/")) return null;
17647
+ return { entryId: decodeURIComponent(rest) };
17648
+ }
17649
+ async function handleStream3(deps, res) {
17650
+ res.writeHead(200, {
17651
+ "Content-Type": "text/event-stream",
17652
+ "Cache-Control": "no-cache, no-transform",
17653
+ Connection: "keep-alive",
17654
+ "X-Accel-Buffering": "no"
17655
+ });
17656
+ const snapshot = await deps.handoffLog.query({ limit: 50 });
17657
+ res.write(
17658
+ `event: handoff_snapshot
17659
+ data: ${JSON.stringify({ entries: snapshot })}
17660
+
17661
+ `
17662
+ );
17663
+ const unsubscribe = deps.events.subscribe((entry) => {
17664
+ try {
17665
+ res.write(
17666
+ `event: handoff_added
17667
+ data: ${JSON.stringify(entry)}
17668
+
17669
+ `
17670
+ );
17671
+ } catch {
17672
+ }
17673
+ });
17674
+ const keepAlive = setInterval(() => {
17675
+ try {
17676
+ res.write(": keepalive\n\n");
17677
+ } catch {
17678
+ }
17679
+ }, 25e3);
17680
+ const cleanup = () => {
17681
+ clearInterval(keepAlive);
17682
+ unsubscribe();
17683
+ };
17684
+ res.on("close", cleanup);
17685
+ res.on("error", cleanup);
17686
+ }
17687
+ async function handleCoordinationRoute(deps, req, res) {
17688
+ const host = req.headers.host || "localhost";
17689
+ const url = new URL(req.url ?? "/", `http://${host}`);
17690
+ const method = (req.method ?? "GET").toUpperCase();
17691
+ const path = url.pathname;
17692
+ if (path !== COORDINATION_API_PREFIX && !path.startsWith(`${COORDINATION_API_PREFIX}/`)) {
17693
+ return false;
17694
+ }
17695
+ const checkAuth = authMiddleware(deps.authConfig);
17696
+ if (!checkAuth(req, res, url)) return true;
17697
+ try {
17698
+ if (method === "GET" && path === `${COORDINATION_HANDOFFS_PREFIX}/stream`) {
17699
+ await handleStream3(deps, res);
17700
+ return true;
17701
+ }
17702
+ if (method === "GET" && path === COORDINATION_HANDOFFS_PREFIX) {
17703
+ const limit = parseLimit4(
17704
+ url.searchParams.get("limit"),
17705
+ COORDINATION_LIST_DEFAULT_LIMIT,
17706
+ COORDINATION_LIST_MAX_LIMIT
17707
+ );
17708
+ const since = url.searchParams.get("since") ?? void 0;
17709
+ const until = url.searchParams.get("until") ?? void 0;
17710
+ const agentId = url.searchParams.get("agent_id") ?? void 0;
17711
+ const entries = await deps.handoffLog.query({
17712
+ limit,
17713
+ ...since !== void 0 ? { since } : {},
17714
+ ...until !== void 0 ? { until } : {},
17715
+ ...agentId !== void 0 ? { agent_id: agentId } : {}
17716
+ });
17717
+ deps.auditLog.append(
17718
+ "l2",
17719
+ COORDINATION_VIEW_AUDIT_OPS.VIEW_OPENED,
17720
+ deps.operatorId,
17721
+ {
17722
+ fortress_id: deps.handoffLog.getFortressId(),
17723
+ result_count: entries.length,
17724
+ ...since !== void 0 ? { since } : {},
17725
+ ...until !== void 0 ? { until } : {},
17726
+ ...agentId !== void 0 ? { agent_id: agentId } : {}
17727
+ }
17728
+ );
17729
+ writeJSON6(res, 200, { ok: true, data: { entries } });
17730
+ return true;
17731
+ }
17732
+ const entryMatch = matchEntryRoute2(path);
17733
+ if (method === "GET" && entryMatch) {
17734
+ const detail = await deps.handoffLog.getEntry(entryMatch.entryId);
17735
+ if (!detail) {
17736
+ writeJSON6(res, 404, { ok: false, error: "not_found" });
17737
+ return true;
17738
+ }
17739
+ deps.auditLog.append(
17740
+ "l2",
17741
+ COORDINATION_VIEW_AUDIT_OPS.ENTRY_DRILLED,
17742
+ deps.operatorId,
17743
+ {
17744
+ fortress_id: deps.handoffLog.getFortressId(),
17745
+ entry_id: detail.entry.entry_id,
17746
+ event_class: detail.entry.event_class,
17747
+ source_agent_id: detail.entry.source_agent_id,
17748
+ target_agent_id: detail.entry.target_agent_id
17749
+ }
17750
+ );
17751
+ writeJSON6(res, 200, { ok: true, data: detail });
17752
+ return true;
17753
+ }
17754
+ writeJSON6(res, 404, { ok: false, error: "not_found", path });
17755
+ return true;
17756
+ } catch (err) {
17757
+ const msg = err instanceof Error ? err.message : String(err);
17758
+ writeJSON6(res, 500, { ok: false, error: "internal", detail: msg });
17759
+ return true;
17760
+ }
17761
+ }
17762
+ var COORDINATION_API_PREFIX, COORDINATION_HANDOFFS_PREFIX, COORDINATION_LIST_DEFAULT_LIMIT, COORDINATION_LIST_MAX_LIMIT, HandoffEventBridge;
17763
+ var init_handoff_routes = __esm({
17764
+ "src/coordination/handoff-routes.ts"() {
17765
+ init_auth_middleware();
17766
+ init_handoff_log();
17767
+ COORDINATION_API_PREFIX = "/api/coordination";
17768
+ COORDINATION_HANDOFFS_PREFIX = "/api/coordination/handoffs";
17769
+ COORDINATION_LIST_DEFAULT_LIMIT = 50;
17770
+ COORDINATION_LIST_MAX_LIMIT = 500;
17771
+ HandoffEventBridge = class {
17772
+ listeners = /* @__PURE__ */ new Set();
17773
+ subscribe(listener) {
17774
+ this.listeners.add(listener);
17775
+ return () => this.listeners.delete(listener);
17776
+ }
17777
+ emit(entry) {
17778
+ for (const listener of this.listeners) {
17779
+ try {
17780
+ listener(entry);
17781
+ } catch {
17782
+ }
17783
+ }
17784
+ }
17785
+ };
17786
+ }
17787
+ });
17476
17788
  function isDashboardViewRoute(method, path) {
17477
17789
  if (method !== "GET") return false;
17478
17790
  return path === "/" || path === "/dashboard" || path === "/v1.0" || path === "/fortress" || path === "/events";
@@ -17488,6 +17800,7 @@ var init_dashboard = __esm({
17488
17800
  init_dispatch();
17489
17801
  init_approval_aggregator_routes();
17490
17802
  init_sentinel_routes();
17803
+ init_handoff_routes();
17491
17804
  SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
17492
17805
  SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
17493
17806
  MAX_SESSIONS = 1e3;
@@ -17562,6 +17875,17 @@ var init_dashboard = __esm({
17562
17875
  * dispatcher's audited paths.
17563
17876
  */
17564
17877
  sentinelDispatcher = null;
17878
+ /**
17879
+ * v1.3 WP-V1.3-3 Omega-1 Coordination Handoff Visualization.
17880
+ * Mounted additively at `/api/coordination/*` when set. Read-only
17881
+ * against the audit log; the only writes are operator-action audit
17882
+ * events (operator_coordination_view_opened,
17883
+ * operator_handoff_entry_drilled).
17884
+ */
17885
+ handoffLog = null;
17886
+ handoffEventBridge = null;
17887
+ handoffAuditLog = null;
17888
+ handoffOperatorId = null;
17565
17889
  constructor(config) {
17566
17890
  this.config = config;
17567
17891
  this.authToken = config.auth_token;
@@ -17629,6 +17953,18 @@ var init_dashboard = __esm({
17629
17953
  setSentinelDispatcher(dispatcher) {
17630
17954
  this.sentinelDispatcher = dispatcher;
17631
17955
  }
17956
+ /**
17957
+ * v1.3 WP-V1.3-3 Omega-1: bind the Coordination handoff log +
17958
+ * event bridge + audit log + operator id. Once set, requests to
17959
+ * `/api/coordination/*` route through `handleCoordinationRoute`.
17960
+ * Pass `null` for any field to detach.
17961
+ */
17962
+ setHandoffLog(opts) {
17963
+ this.handoffLog = opts.handoffLog;
17964
+ this.handoffEventBridge = opts.eventBridge ?? null;
17965
+ this.handoffAuditLog = opts.auditLog ?? null;
17966
+ this.handoffOperatorId = opts.operatorId ?? null;
17967
+ }
17632
17968
  /**
17633
17969
  * v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
17634
17970
  * before the legacy approval route table. Returns true when served.
@@ -17667,6 +18003,30 @@ var init_dashboard = __esm({
17667
18003
  res
17668
18004
  );
17669
18005
  }
18006
+ /**
18007
+ * v1.3 WP-V1.3-3 Omega-1 dispatch entry point. Routes
18008
+ * `/api/coordination/*` requests through the coordination router
18009
+ * when a HandoffLog has been bound. Returns true when served.
18010
+ */
18011
+ async dispatchCoordination(req, res) {
18012
+ if (!this.handoffLog || !this.handoffEventBridge || !this.handoffAuditLog) {
18013
+ return false;
18014
+ }
18015
+ return handleCoordinationRoute(
18016
+ {
18017
+ authConfig: {
18018
+ loopbackAutoAuth: this._autoAuthLocalhost,
18019
+ ...this.authToken !== void 0 ? { authToken: this.authToken } : {}
18020
+ },
18021
+ handoffLog: this.handoffLog,
18022
+ auditLog: this.handoffAuditLog,
18023
+ operatorId: this.handoffOperatorId ?? this.identityManager?.getPrimaryIdentityId() ?? "operator_dashboard",
18024
+ events: this.handoffEventBridge
18025
+ },
18026
+ req,
18027
+ res
18028
+ );
18029
+ }
17670
18030
  /**
17671
18031
  * v1.1 dispatch entry point. Called from `handleRequest` before the
17672
18032
  * legacy route table. Returns true when the request was served by v1.1
@@ -18066,6 +18426,18 @@ var init_dashboard = __esm({
18066
18426
  });
18067
18427
  return;
18068
18428
  }
18429
+ if (this.handoffLog && url.pathname.startsWith(COORDINATION_API_PREFIX)) {
18430
+ this.dispatchCoordination(req, res).then((handled) => {
18431
+ if (handled) return;
18432
+ this.handleLegacyRequest(req, res, url, method);
18433
+ }).catch(() => {
18434
+ if (!res.headersSent) {
18435
+ res.writeHead(500, { "Content-Type": "application/json" });
18436
+ res.end(JSON.stringify({ error: "Internal server error" }));
18437
+ }
18438
+ });
18439
+ return;
18440
+ }
18069
18441
  if (this.v11Bindings) {
18070
18442
  this.dispatchV11(req, res, url, method).then((handled) => {
18071
18443
  if (handled) return;
@@ -21787,6 +22159,11 @@ var init_sentinel_dispatcher = __esm({
21787
22159
  fortressId: this.fortressId,
21788
22160
  auditLog: this.auditLog,
21789
22161
  now: this.now,
22162
+ // Phi-5 meta-sentinel reads the per-fortress finding store to
22163
+ // detect patterns across other sentinels' findings. First-order
22164
+ // sentinels ignore the field; the dispatcher always attaches it
22165
+ // because the store is already in scope here.
22166
+ findingStore: this.findingStore,
21790
22167
  ...contextOverrides ?? {}
21791
22168
  };
21792
22169
  const sentinel = await this.registry.subscribe(sentinelId, context);
@@ -21888,28 +22265,252 @@ var init_sentinel_dispatcher = __esm({
21888
22265
  */
21889
22266
  async dispose() {
21890
22267
  this.stop();
21891
- await this.registry.unsubscribeAll();
22268
+ await this.registry.unsubscribeAll();
22269
+ this.listeners.clear();
22270
+ }
22271
+ async routeFinding(sentinelId, raw) {
22272
+ const stamped = {
22273
+ ...raw,
22274
+ finding_id: raw.finding_id || randomUUID(),
22275
+ sentinel_id: sentinelId,
22276
+ fortress_id: this.fortressId,
22277
+ observed_at: raw.observed_at || this.now().toISOString()
22278
+ };
22279
+ await this.findingStore.saveFinding(stamped);
22280
+ this.auditLog.append(
22281
+ "l2",
22282
+ SENTINEL_AUDIT_OPS.FINDING_EMITTED,
22283
+ this.identityId,
22284
+ {
22285
+ sentinel_id: sentinelId,
22286
+ finding_id: stamped.finding_id,
22287
+ severity: stamped.severity,
22288
+ ...stamped.agent_id !== void 0 ? { agent_id: stamped.agent_id } : {},
22289
+ evidence_audit_ids: stamped.evidence_audit_ids,
22290
+ fortress_id: this.fortressId
22291
+ }
22292
+ );
22293
+ this.emit({ type: "finding", finding: stamped });
22294
+ return stamped;
22295
+ }
22296
+ emit(event) {
22297
+ for (const listener of this.listeners) {
22298
+ try {
22299
+ listener(event);
22300
+ } catch {
22301
+ }
22302
+ }
22303
+ }
22304
+ };
22305
+ }
22306
+ });
22307
+
22308
+ // src/anomaly-detection/types.ts
22309
+ var init_types4 = __esm({
22310
+ "src/anomaly-detection/types.ts"() {
22311
+ }
22312
+ });
22313
+ var ANOMALY_AUDIT_OPS, DEFAULT_TICK_INTERVAL_MS2, AnomalyPipelineDispatcher;
22314
+ var init_anomaly_pipeline = __esm({
22315
+ "src/anomaly-detection/anomaly-pipeline.ts"() {
22316
+ init_types4();
22317
+ ANOMALY_AUDIT_OPS = {
22318
+ DETECTOR_REGISTERED: "anomaly_detector_registered",
22319
+ DETECTOR_UNREGISTERED: "anomaly_detector_unregistered",
22320
+ FINDING_EMITTED: "anomaly_finding_emitted",
22321
+ EVALUATION_FAILED: "anomaly_evaluation_failed",
22322
+ TRAINING_COMPLETED: "anomaly_training_completed",
22323
+ TRAINING_FAILED: "anomaly_training_failed"
22324
+ };
22325
+ DEFAULT_TICK_INTERVAL_MS2 = 6e4;
22326
+ AnomalyPipelineDispatcher = class {
22327
+ findingStore;
22328
+ auditLog;
22329
+ storage;
22330
+ masterKey;
22331
+ fortressId;
22332
+ identityId;
22333
+ now;
22334
+ tickIntervalMs;
22335
+ detectors = /* @__PURE__ */ new Map();
22336
+ listeners = /* @__PURE__ */ new Set();
22337
+ tickTimer = null;
22338
+ tickInFlight = false;
22339
+ constructor(deps) {
22340
+ this.findingStore = deps.findingStore;
22341
+ this.auditLog = deps.auditLog;
22342
+ this.storage = deps.storage;
22343
+ this.masterKey = deps.masterKey;
22344
+ this.fortressId = deps.fortressId;
22345
+ this.identityId = deps.identityId;
22346
+ this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
22347
+ this.tickIntervalMs = deps.tickIntervalMs ?? DEFAULT_TICK_INTERVAL_MS2;
22348
+ }
22349
+ onEvent(listener) {
22350
+ this.listeners.add(listener);
22351
+ return () => this.listeners.delete(listener);
22352
+ }
22353
+ /**
22354
+ * Register + subscribe a detector to this fortress. Idempotent: a
22355
+ * second call with the same detectorId returns the already-
22356
+ * registered instance without re-subscribing.
22357
+ */
22358
+ async registerDetector(detector) {
22359
+ const existing = this.detectors.get(detector.detectorId);
22360
+ if (existing) return existing;
22361
+ const context = {
22362
+ fortressId: this.fortressId,
22363
+ auditLog: this.auditLog,
22364
+ storage: this.storage,
22365
+ masterKey: this.masterKey,
22366
+ now: this.now
22367
+ };
22368
+ await detector.subscribe(context);
22369
+ this.detectors.set(detector.detectorId, detector);
22370
+ this.auditLog.append(
22371
+ "l2",
22372
+ ANOMALY_AUDIT_OPS.DETECTOR_REGISTERED,
22373
+ this.identityId,
22374
+ { detector_id: detector.detectorId, fortress_id: this.fortressId }
22375
+ );
22376
+ return detector;
22377
+ }
22378
+ /**
22379
+ * Unregister + tear down a detector. Idempotent. Returns true when
22380
+ * an active registration was removed.
22381
+ */
22382
+ async unregisterDetector(detectorId) {
22383
+ const detector = this.detectors.get(detectorId);
22384
+ if (!detector) return false;
22385
+ try {
22386
+ await detector.unsubscribe();
22387
+ } finally {
22388
+ this.detectors.delete(detectorId);
22389
+ }
22390
+ this.auditLog.append(
22391
+ "l2",
22392
+ ANOMALY_AUDIT_OPS.DETECTOR_UNREGISTERED,
22393
+ this.identityId,
22394
+ { detector_id: detectorId, fortress_id: this.fortressId }
22395
+ );
22396
+ return true;
22397
+ }
22398
+ listDetectors() {
22399
+ return [...this.detectors.keys()];
22400
+ }
22401
+ /** Run one evaluation pass over every registered detector. */
22402
+ async tick() {
22403
+ if (this.tickInFlight) return [];
22404
+ this.tickInFlight = true;
22405
+ try {
22406
+ const findings = [];
22407
+ for (const [detectorId, detector] of this.detectors.entries()) {
22408
+ try {
22409
+ const detectorFindings = await detector.evaluate();
22410
+ for (const raw of detectorFindings) {
22411
+ const stamped = await this.routeFinding(detectorId, raw);
22412
+ findings.push(stamped);
22413
+ }
22414
+ try {
22415
+ const trainingResult = await detector.classifier.train();
22416
+ this.auditLog.append(
22417
+ "l2",
22418
+ ANOMALY_AUDIT_OPS.TRAINING_COMPLETED,
22419
+ this.identityId,
22420
+ {
22421
+ detector_id: detectorId,
22422
+ classifier_id: detector.classifier.classifierId,
22423
+ trained_at: trainingResult.trained_at,
22424
+ sample_count: trainingResult.sample_count,
22425
+ agent_count: trainingResult.agent_count,
22426
+ fortress_id: this.fortressId
22427
+ }
22428
+ );
22429
+ } catch (trainErr) {
22430
+ const message = trainErr instanceof Error ? trainErr.message : String(trainErr);
22431
+ this.auditLog.append(
22432
+ "l2",
22433
+ ANOMALY_AUDIT_OPS.TRAINING_FAILED,
22434
+ this.identityId,
22435
+ {
22436
+ detector_id: detectorId,
22437
+ error_message: message,
22438
+ fortress_id: this.fortressId
22439
+ },
22440
+ "failure"
22441
+ );
22442
+ }
22443
+ } catch (err) {
22444
+ const message = err instanceof Error ? err.message : String(err);
22445
+ const observedAt = this.now().toISOString();
22446
+ this.auditLog.append(
22447
+ "l2",
22448
+ ANOMALY_AUDIT_OPS.EVALUATION_FAILED,
22449
+ this.identityId,
22450
+ {
22451
+ detector_id: detectorId,
22452
+ error_message: message,
22453
+ fortress_id: this.fortressId
22454
+ },
22455
+ "failure"
22456
+ );
22457
+ this.emit({
22458
+ type: "evaluation_failed",
22459
+ detector_id: detectorId,
22460
+ error_message: message,
22461
+ observed_at: observedAt
22462
+ });
22463
+ }
22464
+ }
22465
+ return findings;
22466
+ } finally {
22467
+ this.tickInFlight = false;
22468
+ }
22469
+ }
22470
+ start() {
22471
+ if (this.tickTimer !== null) return;
22472
+ if (this.tickIntervalMs <= 0) return;
22473
+ this.tickTimer = setInterval(() => {
22474
+ void this.tick();
22475
+ }, this.tickIntervalMs);
22476
+ if (typeof this.tickTimer.unref === "function") {
22477
+ this.tickTimer.unref();
22478
+ }
22479
+ }
22480
+ stop() {
22481
+ if (this.tickTimer === null) return;
22482
+ clearInterval(this.tickTimer);
22483
+ this.tickTimer = null;
22484
+ }
22485
+ async dispose() {
22486
+ this.stop();
22487
+ const ids = [...this.detectors.keys()];
22488
+ for (const id of ids) {
22489
+ try {
22490
+ await this.unregisterDetector(id);
22491
+ } catch {
22492
+ }
22493
+ }
21892
22494
  this.listeners.clear();
21893
22495
  }
21894
- async routeFinding(sentinelId, raw) {
22496
+ async routeFinding(detectorId, raw) {
21895
22497
  const stamped = {
21896
22498
  ...raw,
21897
22499
  finding_id: raw.finding_id || randomUUID(),
21898
- sentinel_id: sentinelId,
21899
22500
  fortress_id: this.fortressId,
21900
22501
  observed_at: raw.observed_at || this.now().toISOString()
21901
22502
  };
21902
22503
  await this.findingStore.saveFinding(stamped);
21903
22504
  this.auditLog.append(
21904
22505
  "l2",
21905
- SENTINEL_AUDIT_OPS.FINDING_EMITTED,
22506
+ ANOMALY_AUDIT_OPS.FINDING_EMITTED,
21906
22507
  this.identityId,
21907
22508
  {
21908
- sentinel_id: sentinelId,
22509
+ detector_id: detectorId,
21909
22510
  finding_id: stamped.finding_id,
21910
22511
  severity: stamped.severity,
22512
+ anomaly_score: stamped.details["anomaly_score"] ?? null,
21911
22513
  ...stamped.agent_id !== void 0 ? { agent_id: stamped.agent_id } : {},
21912
- evidence_audit_ids: stamped.evidence_audit_ids,
21913
22514
  fortress_id: this.fortressId
21914
22515
  }
21915
22516
  );
@@ -22192,7 +22793,7 @@ function extractInterAgentEvents(entries) {
22192
22793
  if (!sender) continue;
22193
22794
  out.push({
22194
22795
  sender,
22195
- recipient: OPERATOR_PSEUDO_AGENT,
22796
+ recipient: OPERATOR_PSEUDO_AGENT2,
22196
22797
  timestampMs: Date.parse(entry.timestamp),
22197
22798
  auditId: `${entry.timestamp}:${entry.operation}`
22198
22799
  });
@@ -22206,7 +22807,7 @@ function optionalString(details, key) {
22206
22807
  if (typeof value !== "string" || value.length === 0) return null;
22207
22808
  return value;
22208
22809
  }
22209
- var CROSS_AGENT_CHATTER_SENTINEL_ID, WARN_SIGMA2, ALERT_SIGMA2, BASELINE_WINDOWS2, QUERY_LIMIT2, MULTI_NEW_PARTNER_ALERT_THRESHOLD, OPERATOR_PSEUDO_AGENT, HANDOFF_OP, CROSS_HARNESS_OPS, CrossAgentChatterWatcher;
22810
+ var CROSS_AGENT_CHATTER_SENTINEL_ID, WARN_SIGMA2, ALERT_SIGMA2, BASELINE_WINDOWS2, QUERY_LIMIT2, MULTI_NEW_PARTNER_ALERT_THRESHOLD, OPERATOR_PSEUDO_AGENT2, HANDOFF_OP, CROSS_HARNESS_OPS, CrossAgentChatterWatcher;
22210
22811
  var init_cross_agent_chatter_watcher = __esm({
22211
22812
  "src/sentinel/sentinels/cross-agent-chatter-watcher.ts"() {
22212
22813
  init_sentinel();
@@ -22216,7 +22817,7 @@ var init_cross_agent_chatter_watcher = __esm({
22216
22817
  BASELINE_WINDOWS2 = 7;
22217
22818
  QUERY_LIMIT2 = 1e4;
22218
22819
  MULTI_NEW_PARTNER_ALERT_THRESHOLD = 3;
22219
- OPERATOR_PSEUDO_AGENT = "operator";
22820
+ OPERATOR_PSEUDO_AGENT2 = "operator";
22220
22821
  HANDOFF_OP = "v1.1_local_handoff";
22221
22822
  CROSS_HARNESS_OPS = /* @__PURE__ */ new Set([
22222
22823
  "cross_harness_approval_aggregated",
@@ -23097,6 +23698,229 @@ var init_suspicious_tool_call_detector = __esm({
23097
23698
  }
23098
23699
  });
23099
23700
 
23701
+ // src/sentinel/sentinels/anomaly-trigger.ts
23702
+ function computeCompoundFindings(windowZero, now) {
23703
+ const byAgent = /* @__PURE__ */ new Map();
23704
+ for (const f of windowZero) {
23705
+ if (!f.agent_id) continue;
23706
+ if (!isWarnOrAlert(f.severity)) continue;
23707
+ let bucket = byAgent.get(f.agent_id);
23708
+ if (!bucket) {
23709
+ bucket = [];
23710
+ byAgent.set(f.agent_id, bucket);
23711
+ }
23712
+ bucket.push(f);
23713
+ }
23714
+ const out = [];
23715
+ for (const [agentId, group] of byAgent.entries()) {
23716
+ const distinctSentinels = new Set(group.map((f) => f.sentinel_id));
23717
+ if (distinctSentinels.size < COMPOUND_TRIGGER_MIN_SENTINELS) continue;
23718
+ const contributingSentinels = [...distinctSentinels].sort();
23719
+ const evidence = group.map((f) => f.finding_id).filter((id) => id.length > 0).slice(0, 50);
23720
+ const summary = `${agentId} agent triggered ${distinctSentinels.size} distinct sentinels in the last 24h: ${contributingSentinels.join(", ")}. Compound suspicious behavior; review the contributing findings.`;
23721
+ out.push({
23722
+ finding_id: "",
23723
+ sentinel_id: ANOMALY_TRIGGER_SENTINEL_ID,
23724
+ severity: "alert",
23725
+ agent_id: agentId,
23726
+ summary,
23727
+ details: {
23728
+ trigger: "compound",
23729
+ agent_id: agentId,
23730
+ contributing_sentinels: contributingSentinels,
23731
+ contributing_finding_count: group.length
23732
+ },
23733
+ observed_at: now.toISOString(),
23734
+ evidence_audit_ids: evidence,
23735
+ fortress_id: ""
23736
+ });
23737
+ }
23738
+ return out;
23739
+ }
23740
+ function computeCountSpikeFinding(windowed, now) {
23741
+ const currentCount = (windowed[0] ?? []).length;
23742
+ const baselineCounts = [];
23743
+ for (let i = 1; i <= BASELINE_WINDOWS5; i += 1) {
23744
+ baselineCounts.push((windowed[i] ?? []).length);
23745
+ }
23746
+ const populated = baselineCounts.filter((c) => c > 0).length;
23747
+ if (populated < BASELINE_WINDOWS5) {
23748
+ return null;
23749
+ }
23750
+ const mean = baselineCounts.reduce((sum, c) => sum + c, 0) / baselineCounts.length;
23751
+ const variance = baselineCounts.reduce((sum, c) => sum + (c - mean) ** 2, 0) / baselineCounts.length;
23752
+ const stddev = Math.sqrt(variance);
23753
+ const warnThreshold = mean + WARN_SIGMA5 * stddev;
23754
+ const alertThreshold = mean + ALERT_SIGMA5 * stddev;
23755
+ if (currentCount > alertThreshold) {
23756
+ return buildCountFinding(
23757
+ currentCount,
23758
+ mean,
23759
+ stddev,
23760
+ ALERT_SIGMA5,
23761
+ "alert",
23762
+ windowed[0] ?? [],
23763
+ now
23764
+ );
23765
+ }
23766
+ if (currentCount > warnThreshold) {
23767
+ return buildCountFinding(
23768
+ currentCount,
23769
+ mean,
23770
+ stddev,
23771
+ WARN_SIGMA5,
23772
+ "warn",
23773
+ windowed[0] ?? [],
23774
+ now
23775
+ );
23776
+ }
23777
+ return null;
23778
+ }
23779
+ function buildCountFinding(currentCount, mean, stddev, sigma, severity, windowZero, now) {
23780
+ const ratio = mean === 0 ? Number.POSITIVE_INFINITY : currentCount / mean;
23781
+ const ratioStr = Number.isFinite(ratio) ? `${ratio.toFixed(1)}x normal` : "no prior baseline";
23782
+ 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.`;
23783
+ const evidence = windowZero.map((f) => f.finding_id).filter((id) => id.length > 0).slice(0, 50);
23784
+ return {
23785
+ finding_id: "",
23786
+ sentinel_id: ANOMALY_TRIGGER_SENTINEL_ID,
23787
+ severity,
23788
+ summary,
23789
+ details: {
23790
+ trigger: "count_spike",
23791
+ current_count: currentCount,
23792
+ baseline_mean: mean,
23793
+ baseline_stddev: stddev,
23794
+ sigma_threshold: sigma,
23795
+ ratio: Number.isFinite(ratio) ? ratio : null
23796
+ },
23797
+ observed_at: now.toISOString(),
23798
+ evidence_audit_ids: evidence,
23799
+ fortress_id: ""
23800
+ };
23801
+ }
23802
+ function computeNovelComboFinding(windowed, now) {
23803
+ const distinctByWindow = [];
23804
+ for (let i = 0; i <= BASELINE_WINDOWS5; i += 1) {
23805
+ const set = /* @__PURE__ */ new Set();
23806
+ for (const f of windowed[i] ?? []) {
23807
+ set.add(f.sentinel_id);
23808
+ }
23809
+ distinctByWindow.push(set);
23810
+ }
23811
+ const populatedBaselineWindows = distinctByWindow.slice(1).filter((s) => s.size > 0).length;
23812
+ if (populatedBaselineWindows < BASELINE_WINDOWS5) {
23813
+ return null;
23814
+ }
23815
+ const currentCombo = distinctByWindow[0];
23816
+ if (currentCombo.size < 2) return null;
23817
+ const currentKey = comboKey(currentCombo);
23818
+ for (let i = 1; i <= BASELINE_WINDOWS5; i += 1) {
23819
+ if (comboKey(distinctByWindow[i]) === currentKey) {
23820
+ return null;
23821
+ }
23822
+ }
23823
+ const sentinelIds = [...currentCombo].sort();
23824
+ 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.`;
23825
+ const evidence = (windowed[0] ?? []).map((f) => f.finding_id).filter((id) => id.length > 0).slice(0, 50);
23826
+ return {
23827
+ finding_id: "",
23828
+ sentinel_id: ANOMALY_TRIGGER_SENTINEL_ID,
23829
+ severity: "info",
23830
+ summary,
23831
+ details: {
23832
+ trigger: "novel_combo",
23833
+ sentinel_ids: sentinelIds,
23834
+ baseline_window_count: BASELINE_WINDOWS5
23835
+ },
23836
+ observed_at: now.toISOString(),
23837
+ evidence_audit_ids: evidence,
23838
+ fortress_id: ""
23839
+ };
23840
+ }
23841
+ function isWarnOrAlert(s) {
23842
+ return s === "warn" || s === "alert";
23843
+ }
23844
+ function bucketByWindow(findings, nowMs) {
23845
+ const buckets = Array.from(
23846
+ { length: BASELINE_WINDOWS5 + 1 },
23847
+ () => []
23848
+ );
23849
+ for (const f of findings) {
23850
+ const ts = Date.parse(f.observed_at);
23851
+ if (!Number.isFinite(ts)) continue;
23852
+ const age = nowMs - ts;
23853
+ if (age < 0) continue;
23854
+ const idx = Math.floor(age / WINDOW_MS);
23855
+ if (idx > BASELINE_WINDOWS5) continue;
23856
+ buckets[idx].push(f);
23857
+ }
23858
+ return buckets;
23859
+ }
23860
+ function comboKey(set) {
23861
+ return [...set].sort().join("|");
23862
+ }
23863
+ var ANOMALY_TRIGGER_SENTINEL_ID, WARN_SIGMA5, ALERT_SIGMA5, BASELINE_WINDOWS5, QUERY_LIMIT5, WINDOW_MS, COMPOUND_TRIGGER_MIN_SENTINELS, AnomalyTriggerWatcher;
23864
+ var init_anomaly_trigger = __esm({
23865
+ "src/sentinel/sentinels/anomaly-trigger.ts"() {
23866
+ init_sentinel();
23867
+ ANOMALY_TRIGGER_SENTINEL_ID = "anomaly-trigger";
23868
+ WARN_SIGMA5 = 3;
23869
+ ALERT_SIGMA5 = 6;
23870
+ BASELINE_WINDOWS5 = 7;
23871
+ QUERY_LIMIT5 = 5e3;
23872
+ WINDOW_MS = 24 * 60 * 60 * 1e3;
23873
+ COMPOUND_TRIGGER_MIN_SENTINELS = 2;
23874
+ AnomalyTriggerWatcher = class extends Sentinel {
23875
+ sentinelId = ANOMALY_TRIGGER_SENTINEL_ID;
23876
+ 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.";
23877
+ async subscribe(context) {
23878
+ if (!context.findingStore) {
23879
+ throw new Error(
23880
+ `${ANOMALY_TRIGGER_SENTINEL_ID}: findingStore missing from SentinelContext; this meta-sentinel requires the Phi-1 finding store`
23881
+ );
23882
+ }
23883
+ await super.subscribe(context);
23884
+ }
23885
+ async evaluate() {
23886
+ const ctx = this.requireContext();
23887
+ const findingStore = ctx.findingStore;
23888
+ if (!findingStore) {
23889
+ return [];
23890
+ }
23891
+ const now = ctx.now();
23892
+ const nowMs = now.getTime();
23893
+ const windowSpanMs = (BASELINE_WINDOWS5 + 1) * WINDOW_MS;
23894
+ const sinceIso = new Date(nowMs - windowSpanMs).toISOString();
23895
+ let findings;
23896
+ try {
23897
+ findings = await findingStore.listFindings({
23898
+ since: sinceIso,
23899
+ limit: QUERY_LIMIT5
23900
+ });
23901
+ } catch {
23902
+ return [];
23903
+ }
23904
+ const firstOrderFindings = findings.filter(
23905
+ (f) => f.sentinel_id !== ANOMALY_TRIGGER_SENTINEL_ID
23906
+ );
23907
+ const windowed = bucketByWindow(firstOrderFindings, nowMs);
23908
+ const out = [];
23909
+ const compoundFindings = computeCompoundFindings(
23910
+ windowed[0] ?? [],
23911
+ now
23912
+ );
23913
+ out.push(...compoundFindings);
23914
+ const countSpikeFinding = computeCountSpikeFinding(windowed, now);
23915
+ if (countSpikeFinding) out.push(countSpikeFinding);
23916
+ const novelComboFinding = computeNovelComboFinding(windowed, now);
23917
+ if (novelComboFinding) out.push(novelComboFinding);
23918
+ return out;
23919
+ }
23920
+ };
23921
+ }
23922
+ });
23923
+
23100
23924
  // src/sentinel/sentinels/index.ts
23101
23925
  var PHI1_BASELINE_CATALOG;
23102
23926
  var init_sentinels = __esm({
@@ -23105,6 +23929,7 @@ var init_sentinels = __esm({
23105
23929
  init_cross_agent_chatter_watcher();
23106
23930
  init_credential_usage_watcher();
23107
23931
  init_suspicious_tool_call_detector();
23932
+ init_anomaly_trigger();
23108
23933
  PHI1_BASELINE_CATALOG = [
23109
23934
  {
23110
23935
  sentinelId: EGRESS_VOLUME_SENTINEL_ID,
@@ -23125,6 +23950,11 @@ var init_sentinels = __esm({
23125
23950
  sentinelId: SUSPICIOUS_TOOL_CALL_SENTINEL_ID,
23126
23951
  description: "Surfaces tool calls whose argument shape, call frequency, or permission combination looks unusual for the fortress's recent history.",
23127
23952
  factory: () => new SuspiciousToolCallDetector()
23953
+ },
23954
+ {
23955
+ sentinelId: ANOMALY_TRIGGER_SENTINEL_ID,
23956
+ 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.",
23957
+ factory: () => new AnomalyTriggerWatcher()
23128
23958
  }
23129
23959
  ];
23130
23960
  }
@@ -25049,7 +25879,7 @@ function createBridgeCommitment(outcome, identity, identityEncryptionKey, includ
25049
25879
  const now = (/* @__PURE__ */ new Date()).toISOString();
25050
25880
  const canonicalBytes = canonicalize2(outcome);
25051
25881
  const canonicalString = new TextDecoder().decode(canonicalBytes);
25052
- const sha25611 = createCommitment(canonicalString);
25882
+ const sha25612 = createCommitment(canonicalString);
25053
25883
  let pedersenData;
25054
25884
  if (includePedersen && Number.isInteger(outcome.rounds) && outcome.rounds >= 0) {
25055
25885
  const pedersen = createPedersenCommitment(outcome.rounds);
@@ -25061,7 +25891,7 @@ function createBridgeCommitment(outcome, identity, identityEncryptionKey, includ
25061
25891
  const commitmentPayload = {
25062
25892
  bridge_commitment_id: commitmentId,
25063
25893
  session_id: outcome.session_id,
25064
- sha256_commitment: sha25611.commitment,
25894
+ sha256_commitment: sha25612.commitment,
25065
25895
  terms_hash: outcome.terms_hash,
25066
25896
  committer_did: identity.did,
25067
25897
  committed_at: now,
@@ -25072,8 +25902,8 @@ function createBridgeCommitment(outcome, identity, identityEncryptionKey, includ
25072
25902
  return {
25073
25903
  bridge_commitment_id: commitmentId,
25074
25904
  session_id: outcome.session_id,
25075
- sha256_commitment: sha25611.commitment,
25076
- blinding_factor: sha25611.blinding_factor,
25905
+ sha256_commitment: sha25612.commitment,
25906
+ blinding_factor: sha25612.blinding_factor,
25077
25907
  committer_did: identity.did,
25078
25908
  signature: toBase64url(signature),
25079
25909
  pedersen_commitment: pedersenData,
@@ -34643,7 +35473,7 @@ var init_recovery_key_disclosure = __esm({
34643
35473
  });
34644
35474
 
34645
35475
  // src/hub/types.ts
34646
- var init_types4 = __esm({
35476
+ var init_types5 = __esm({
34647
35477
  "src/hub/types.ts"() {
34648
35478
  }
34649
35479
  });
@@ -35682,7 +36512,7 @@ var init_hub = __esm({
35682
36512
  "src/hub/index.ts"() {
35683
36513
  init_constants3();
35684
36514
  init_errors4();
35685
- init_types4();
36515
+ init_types5();
35686
36516
  init_agent_registry();
35687
36517
  init_inbox_store();
35688
36518
  init_inbox_aggregator();
@@ -42186,6 +43016,28 @@ ${err.message}
42186
43016
  if (dashboard) {
42187
43017
  dashboard.setSentinelDispatcher(sentinelDispatcher);
42188
43018
  }
43019
+ const anomalyDispatcher = new AnomalyPipelineDispatcher({
43020
+ findingStore: sentinelFindingStore,
43021
+ auditLog,
43022
+ storage,
43023
+ masterKey,
43024
+ fortressId: fortressIdForAggregator,
43025
+ identityId: aggregatorIdentityId
43026
+ });
43027
+ anomalyDispatcher.start();
43028
+ const handoffLog = new HandoffLog({
43029
+ auditLog,
43030
+ fortressId: fortressIdForAggregator
43031
+ });
43032
+ const handoffEventBridge = new HandoffEventBridge();
43033
+ if (dashboard) {
43034
+ dashboard.setHandoffLog({
43035
+ handoffLog,
43036
+ eventBridge: handoffEventBridge,
43037
+ auditLog,
43038
+ operatorId: aggregatorIdentityId
43039
+ });
43040
+ }
42189
43041
  const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
42190
43042
  const { tools: sanctuaryMetaTools } = createSanctuaryTools({
42191
43043
  config,
@@ -42382,6 +43234,9 @@ var init_src = __esm({
42382
43234
  init_sentinel_finding_store();
42383
43235
  init_sentinel_registry();
42384
43236
  init_sentinel_dispatcher();
43237
+ init_anomaly_pipeline();
43238
+ init_handoff_log();
43239
+ init_handoff_routes();
42385
43240
  init_sentinels();
42386
43241
  init_subscription_store();
42387
43242
  init_tools4();
@@ -44482,32 +45337,32 @@ endstream`;
44482
45337
  const offsets = new Array(totalObjects + 1).fill(0);
44483
45338
  const chunks = [];
44484
45339
  let bytePos = 0;
44485
- const write3 = (s) => {
45340
+ const write4 = (s) => {
44486
45341
  const buf = Buffer.from(s, "latin1");
44487
45342
  chunks.push(buf);
44488
45343
  bytePos += buf.length;
44489
45344
  };
44490
- write3("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n");
45345
+ write4("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n");
44491
45346
  for (let i = 1; i <= totalObjects; i++) {
44492
45347
  offsets[i] = bytePos;
44493
- write3(`${i} 0 obj
45348
+ write4(`${i} 0 obj
44494
45349
  ${objectBodies[i]}
44495
45350
  endobj
44496
45351
  `);
44497
45352
  }
44498
45353
  const xrefPos = bytePos;
44499
- write3(`xref
45354
+ write4(`xref
44500
45355
  0 ${totalObjects + 1}
44501
45356
  `);
44502
- write3("0000000000 65535 f \n");
45357
+ write4("0000000000 65535 f \n");
44503
45358
  for (let i = 1; i <= totalObjects; i++) {
44504
- write3(`${offsets[i].toString().padStart(10, "0")} 00000 n
45359
+ write4(`${offsets[i].toString().padStart(10, "0")} 00000 n
44505
45360
  `);
44506
45361
  }
44507
- write3(`trailer
45362
+ write4(`trailer
44508
45363
  << /Size ${totalObjects + 1} /Root 1 0 R >>
44509
45364
  `);
44510
- write3(`startxref
45365
+ write4(`startxref
44511
45366
  ${xrefPos}
44512
45367
  %%EOF
44513
45368
  `);
@@ -47778,6 +48633,365 @@ var init_sentinel2 = __esm({
47778
48633
  init_sentinels();
47779
48634
  }
47780
48635
  });
48636
+ async function issueDidWeb(opts) {
48637
+ if (!opts.authority_host || !HOST_RE.test(opts.authority_host)) {
48638
+ throw new Error(
48639
+ `did-web: authority_host '${opts.authority_host}' is not a valid DNS host`
48640
+ );
48641
+ }
48642
+ if (!FORTRESS_LABEL_RE.test(opts.fortress_id)) {
48643
+ throw new Error(
48644
+ `did-web: fortress_id '${opts.fortress_id}' is not a valid label`
48645
+ );
48646
+ }
48647
+ if (opts.agent_label !== void 0 && !AGENT_LABEL_RE.test(opts.agent_label)) {
48648
+ throw new Error(
48649
+ `did-web: agent_label '${opts.agent_label}' is not a valid label`
48650
+ );
48651
+ }
48652
+ if (opts.public_key.length !== 32) {
48653
+ throw new Error(
48654
+ `did-web: public_key must be exactly 32 bytes (Ed25519), got ${opts.public_key.length}`
48655
+ );
48656
+ }
48657
+ const did = buildDid(opts);
48658
+ const verificationMethodId = `${did}#key-1`;
48659
+ const verificationMethod = {
48660
+ id: verificationMethodId,
48661
+ type: "JsonWebKey2020",
48662
+ controller: did,
48663
+ publicKeyJwk: {
48664
+ kty: "OKP",
48665
+ crv: "Ed25519",
48666
+ x: toBase64url(opts.public_key)
48667
+ }
48668
+ };
48669
+ const didDocument = {
48670
+ "@context": [...DID_CONTEXT],
48671
+ id: did,
48672
+ verificationMethod: [verificationMethod],
48673
+ authentication: [verificationMethodId],
48674
+ assertionMethod: [verificationMethodId]
48675
+ };
48676
+ const now = (opts.now ?? (() => /* @__PURE__ */ new Date()))();
48677
+ return {
48678
+ did,
48679
+ did_document: didDocument,
48680
+ public_key: opts.public_key,
48681
+ created_at: now.toISOString(),
48682
+ authority_host: opts.authority_host,
48683
+ fortress_id: opts.fortress_id,
48684
+ ...opts.agent_label !== void 0 ? { agent_label: opts.agent_label } : {}
48685
+ };
48686
+ }
48687
+ function publishDidWebDocument(identifier, opts = {}) {
48688
+ const path = opts.publish_path ?? canonicalPublishPath(identifier);
48689
+ const artifact = canonicalSerializeDidDocument(identifier.did_document);
48690
+ const digest = sha256(stringToBytes(artifact));
48691
+ const url = `https://${identifier.authority_host}${path}`;
48692
+ return {
48693
+ url,
48694
+ publish_path: path,
48695
+ artifact,
48696
+ sha256: hashToString(digest)
48697
+ };
48698
+ }
48699
+ function buildDid(opts) {
48700
+ if (opts.agent_label === void 0) {
48701
+ return `did:web:${opts.authority_host}`;
48702
+ }
48703
+ return `did:web:${opts.authority_host}:fortress:${opts.fortress_id}:agent:${opts.agent_label}`;
48704
+ }
48705
+ function canonicalPublishPath(identifier) {
48706
+ if (identifier.agent_label === void 0) {
48707
+ return "/.well-known/did.json";
48708
+ }
48709
+ return `/fortress/${identifier.fortress_id}/agent/${identifier.agent_label}/did.json`;
48710
+ }
48711
+ function canonicalSerializeDidDocument(doc) {
48712
+ return JSON.stringify(
48713
+ {
48714
+ "@context": doc["@context"],
48715
+ id: doc.id,
48716
+ verificationMethod: doc.verificationMethod,
48717
+ authentication: doc.authentication,
48718
+ assertionMethod: doc.assertionMethod
48719
+ },
48720
+ null,
48721
+ 2
48722
+ );
48723
+ }
48724
+ var DID_CONTEXT, HOST_RE, FORTRESS_LABEL_RE, AGENT_LABEL_RE;
48725
+ var init_did_web = __esm({
48726
+ "src/recognition/did-web.ts"() {
48727
+ init_encoding();
48728
+ init_hashing();
48729
+ DID_CONTEXT = [
48730
+ "https://www.w3.org/ns/did/v1",
48731
+ "https://w3id.org/security/suites/jws-2020/v1"
48732
+ ];
48733
+ HOST_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/i;
48734
+ FORTRESS_LABEL_RE = /^[a-zA-Z0-9_-]{1,64}$/;
48735
+ AGENT_LABEL_RE = /^[a-zA-Z0-9_-]{1,64}$/;
48736
+ }
48737
+ });
48738
+
48739
+ // src/cli/did-web.ts
48740
+ var did_web_exports = {};
48741
+ __export(did_web_exports, {
48742
+ runDidWebCommand: () => runDidWebCommand
48743
+ });
48744
+ function write3(stream, text) {
48745
+ stream.write(text);
48746
+ }
48747
+ function flagValue3(argv, name) {
48748
+ const i = argv.indexOf(name);
48749
+ if (i === -1) return void 0;
48750
+ return argv[i + 1];
48751
+ }
48752
+ function hasFlag3(argv, name) {
48753
+ return argv.includes(name);
48754
+ }
48755
+ function printUsage7(out) {
48756
+ write3(
48757
+ out,
48758
+ `Usage: sanctuary did-web <command> [options]
48759
+
48760
+ Commands:
48761
+ issue --authority-host <host> [--agent-label <label>] [--json]
48762
+ Generate a did:web identifier bound to the operator's
48763
+ fortress Ed25519 public key. Writes the DID Document
48764
+ artifact to <storage>/recognition/did-web.json and
48765
+ prints publication instructions for the operator's
48766
+ HTTPS server.
48767
+
48768
+ show [--json] Display the previously issued did:web identifier.
48769
+ Exits non-zero if none issued.
48770
+
48771
+ Options:
48772
+ --authority-host <host> HTTPS host the operator controls and will
48773
+ serve /.well-known/did.json from.
48774
+ --agent-label <label> Optional agent-scoped identifier (label-safe;
48775
+ alphanumeric + dash + underscore, 1-64 chars).
48776
+ --fortress <path> Override the storage path.
48777
+ --passphrase <val> Passphrase for master-key derivation.
48778
+ --json Output as JSON.
48779
+ --help, -h Show this help.
48780
+
48781
+ Castle-walking note: did:web resolution is outbound HTTPS by design.
48782
+ This CLI never opens an outbound socket. The opt-in surface is your
48783
+ choice to run "did-web issue" with --authority-host; the resulting
48784
+ artifact is yours to publish on your own infrastructure. Sanctuary
48785
+ does not phone home.
48786
+ `
48787
+ );
48788
+ }
48789
+ async function runDidWebCommand(args) {
48790
+ const argv = args.argv;
48791
+ const out = args.out ?? process.stdout;
48792
+ const err = args.err ?? process.stderr;
48793
+ const env = args.env ?? process.env;
48794
+ if (argv.length === 0 || hasFlag3(argv, "--help") || hasFlag3(argv, "-h")) {
48795
+ printUsage7(out);
48796
+ return 0;
48797
+ }
48798
+ const command = argv[0];
48799
+ if (command === "issue") {
48800
+ return await cmdIssue(argv.slice(1), out, err, env);
48801
+ }
48802
+ if (command === "show") {
48803
+ return await cmdShow3(argv.slice(1), out, err);
48804
+ }
48805
+ write3(err, `Unknown did-web command: ${command}
48806
+ `);
48807
+ write3(err, `Run "sanctuary did-web --help" for usage.
48808
+ `);
48809
+ return 2;
48810
+ }
48811
+ async function loadFortressIdentity(argv, env, err) {
48812
+ const fortressFlag = flagValue3(argv, "--fortress");
48813
+ if (fortressFlag) {
48814
+ process.env.SANCTUARY_STORAGE_PATH = fortressFlag;
48815
+ }
48816
+ const passphrase = flagValue3(argv, "--passphrase") ?? env.SANCTUARY_PASSPHRASE;
48817
+ const recoveryKey = env.SANCTUARY_RECOVERY_KEY;
48818
+ if (!passphrase && !recoveryKey) {
48819
+ write3(
48820
+ err,
48821
+ "Error: sanctuary did-web requires SANCTUARY_PASSPHRASE, --passphrase, or SANCTUARY_RECOVERY_KEY.\n"
48822
+ );
48823
+ return null;
48824
+ }
48825
+ const config = await loadConfig();
48826
+ await mkdir(config.storage_path, { recursive: true, mode: 448 });
48827
+ const stateStoragePath = join(config.storage_path, "state");
48828
+ const storage = new FilesystemStorage(stateStoragePath);
48829
+ let masterKey;
48830
+ if (passphrase) {
48831
+ let existingParams;
48832
+ const raw = await storage.read("_meta", "key-params");
48833
+ if (raw) {
48834
+ existingParams = JSON.parse(bytesToString(raw));
48835
+ }
48836
+ const derivation = await deriveMasterKey(passphrase, existingParams);
48837
+ masterKey = derivation.key;
48838
+ } else if (recoveryKey) {
48839
+ masterKey = fromBase64url(recoveryKey);
48840
+ } else {
48841
+ return null;
48842
+ }
48843
+ const identityManager = new IdentityManager(storage, masterKey);
48844
+ const loadResult = await identityManager.load();
48845
+ if (loadResult.loaded === 0) {
48846
+ write3(
48847
+ err,
48848
+ loadResult.total > 0 ? "Error: identity files found but none could be decrypted. Wrong passphrase?\n" : "Error: no identities on this fortress yet. Run sanctuary wrap first.\n"
48849
+ );
48850
+ return null;
48851
+ }
48852
+ const primary = identityManager.getDefault();
48853
+ if (!primary) {
48854
+ write3(err, "Error: no primary identity on this fortress yet. Run sanctuary wrap first.\n");
48855
+ return null;
48856
+ }
48857
+ return {
48858
+ publicKey: fromBase64url(primary.public_key),
48859
+ identityId: primary.identity_id,
48860
+ storagePath: config.storage_path
48861
+ };
48862
+ }
48863
+ async function cmdIssue(argv, out, err, env) {
48864
+ const authorityHost = flagValue3(argv, "--authority-host");
48865
+ const agentLabel = flagValue3(argv, "--agent-label");
48866
+ const json = hasFlag3(argv, "--json");
48867
+ if (!authorityHost) {
48868
+ write3(err, "Error: --authority-host is required.\n");
48869
+ write3(err, "Example: sanctuary did-web issue --authority-host alice.example.com\n");
48870
+ return 1;
48871
+ }
48872
+ const snapshot = await loadFortressIdentity(argv, env, err);
48873
+ if (!snapshot) return 1;
48874
+ let identifier;
48875
+ try {
48876
+ identifier = await issueDidWeb({
48877
+ fortress_id: snapshot.identityId,
48878
+ authority_host: authorityHost,
48879
+ public_key: snapshot.publicKey,
48880
+ ...agentLabel !== void 0 ? { agent_label: agentLabel } : {}
48881
+ });
48882
+ } catch (e) {
48883
+ const message = e instanceof Error ? e.message : String(e);
48884
+ write3(err, `Error: ${message}
48885
+ `);
48886
+ return 1;
48887
+ }
48888
+ const artifact = publishDidWebDocument(identifier);
48889
+ const persistDir = join(snapshot.storagePath, "recognition");
48890
+ await mkdir(persistDir, { recursive: true, mode: 448 });
48891
+ const persistPath = join(persistDir, "did-web.json");
48892
+ const record = {
48893
+ version: 1,
48894
+ identifier: {
48895
+ did: identifier.did,
48896
+ created_at: identifier.created_at,
48897
+ authority_host: identifier.authority_host,
48898
+ fortress_id: identifier.fortress_id,
48899
+ ...identifier.agent_label !== void 0 ? { agent_label: identifier.agent_label } : {},
48900
+ did_document: identifier.did_document
48901
+ },
48902
+ artifact: {
48903
+ url: artifact.url,
48904
+ publish_path: artifact.publish_path,
48905
+ sha256: artifact.sha256
48906
+ }
48907
+ };
48908
+ await writeFile(persistPath, JSON.stringify(record, null, 2), {
48909
+ mode: 384
48910
+ });
48911
+ if (json) {
48912
+ write3(out, JSON.stringify(record, null, 2) + "\n");
48913
+ return 0;
48914
+ }
48915
+ write3(out, `did:web identifier issued.
48916
+ `);
48917
+ write3(out, ` DID: ${identifier.did}
48918
+ `);
48919
+ write3(out, ` Authority host: ${identifier.authority_host}
48920
+ `);
48921
+ write3(out, ` Created at: ${identifier.created_at}
48922
+ `);
48923
+ write3(out, ` Persisted: ${persistPath}
48924
+ `);
48925
+ write3(out, `
48926
+ Next step: publish the DID Document to your HTTPS host.
48927
+ `);
48928
+ write3(out, ` Target URL: ${artifact.url}
48929
+ `);
48930
+ write3(out, ` SHA-256: ${artifact.sha256}
48931
+ `);
48932
+ write3(out, ` Artifact: ${join(persistDir, "did.json")}
48933
+ `);
48934
+ const artifactPath = join(persistDir, "did.json");
48935
+ await writeFile(artifactPath, artifact.artifact, { mode: 420 });
48936
+ write3(out, `
48937
+ Castle-walking note: this CLI never opens an outbound socket.
48938
+ `);
48939
+ write3(out, `Publishing the DID Document is your operation; serve the artifact
48940
+ `);
48941
+ write3(out, `at the URL above from infrastructure you control.
48942
+ `);
48943
+ return 0;
48944
+ }
48945
+ async function cmdShow3(argv, out, err, _env) {
48946
+ const json = hasFlag3(argv, "--json");
48947
+ const fortressFlag = flagValue3(argv, "--fortress");
48948
+ if (fortressFlag) {
48949
+ process.env.SANCTUARY_STORAGE_PATH = fortressFlag;
48950
+ }
48951
+ const config = await loadConfig();
48952
+ const persistPath = join(config.storage_path, "recognition", "did-web.json");
48953
+ let bytes;
48954
+ try {
48955
+ bytes = await readFile(persistPath);
48956
+ } catch {
48957
+ write3(
48958
+ err,
48959
+ `No did:web identifier configured on this fortress.
48960
+ Run "sanctuary did-web issue --authority-host <host>" to issue one.
48961
+ `
48962
+ );
48963
+ return 1;
48964
+ }
48965
+ if (json) {
48966
+ write3(out, bytes.toString("utf-8"));
48967
+ if (!bytes.toString("utf-8").endsWith("\n")) write3(out, "\n");
48968
+ return 0;
48969
+ }
48970
+ const parsed = JSON.parse(bytes.toString("utf-8"));
48971
+ write3(out, `did:web identifier on this fortress:
48972
+ `);
48973
+ write3(out, ` DID: ${parsed.identifier.did}
48974
+ `);
48975
+ write3(out, ` Authority host: ${parsed.identifier.authority_host}
48976
+ `);
48977
+ write3(out, ` Created at: ${parsed.identifier.created_at}
48978
+ `);
48979
+ write3(out, ` Publish URL: ${parsed.artifact.url}
48980
+ `);
48981
+ write3(out, ` SHA-256: ${parsed.artifact.sha256}
48982
+ `);
48983
+ return 0;
48984
+ }
48985
+ var init_did_web2 = __esm({
48986
+ "src/cli/did-web.ts"() {
48987
+ init_filesystem();
48988
+ init_tools();
48989
+ init_key_derivation();
48990
+ init_encoding();
48991
+ init_config();
48992
+ init_did_web();
48993
+ }
48994
+ });
47781
48995
 
47782
48996
  // src/mcp/broker-server.ts
47783
48997
  var broker_server_exports = {};
@@ -48643,6 +49857,11 @@ async function main() {
48643
49857
  const code = await runSentinelCommand2({ argv: args.slice(1) });
48644
49858
  process.exit(code);
48645
49859
  }
49860
+ if (args[0] === "did-web") {
49861
+ const { runDidWebCommand: runDidWebCommand2 } = await Promise.resolve().then(() => (init_did_web2(), did_web_exports));
49862
+ const code = await runDidWebCommand2({ argv: args.slice(1) });
49863
+ process.exit(code);
49864
+ }
48646
49865
  if (args[0] === "broker-server") {
48647
49866
  const { openBroker: openBroker2 } = await Promise.resolve().then(() => (init_open(), open_exports));
48648
49867
  const { createBrokerMcpServer: createBrokerMcpServer2 } = await Promise.resolve().then(() => (init_broker_server(), broker_server_exports));