@sanctuary-framework/mcp-server 1.2.7 → 1.2.9

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.d.cts CHANGED
@@ -5521,6 +5521,213 @@ interface ContextFetchers {
5521
5521
  */
5522
5522
  type LlmAssistClassifier = (query: string, categories: readonly ContextCategory[]) => Promise<ContextCategory | "none">;
5523
5523
 
5524
+ /**
5525
+ * Sanctuary MCP Server, Operator Chat, Agent-Context Cache
5526
+ *
5527
+ * WP-V1.3-9 Tau-5 (criterion (e) of the v1.3 scope-lock revision; closes
5528
+ * WP-V1.3-9 Conversational Sovereignty Depth). Builds on Tau-1 (memory
5529
+ * persistence), Tau-2 (multi-turn coherence), Tau-3 (8-category dynamic
5530
+ * context routing), and Tau-4 (operator-query grammar). Tau-5 ships
5531
+ * proactive context awareness: the concierge knows which wrapped agents
5532
+ * the operator runs, their templates, current work, recent audit-log
5533
+ * activity, recent commitment chains, and recent Verascore deltas, and
5534
+ * folds that snapshot into every chat round-trip plus a starter
5535
+ * suggestion when a fresh thread opens.
5536
+ *
5537
+ * Architecture:
5538
+ *
5539
+ * - The cache is per-fortress: one `AgentContextCache` instance per
5540
+ * `OperatorChatService` instance. The service-construction layer in
5541
+ * `dashboard/v1_1/wiring.ts` wires the cache to the same agent
5542
+ * registry + audit log it already passes to the Tau-3 fetchers, so
5543
+ * data sources align across both surfaces.
5544
+ *
5545
+ * - Refresh is lazy: `refresh()` returns the latest snapshot list and
5546
+ * updates the in-memory cache. `read()` is a synchronous accessor for
5547
+ * the latest cached snapshot (returns `[]` until the first refresh
5548
+ * resolves).
5549
+ *
5550
+ * - Cadence: callers schedule refreshes via `start()` (60-second timer)
5551
+ * and trigger an explicit refresh on cocoon-unlock by calling
5552
+ * `refresh()` once at construction. Tests can opt out of the timer
5553
+ * by leaving `start()` uncalled.
5554
+ *
5555
+ * Castle-walking discipline:
5556
+ *
5557
+ * - No new outbound surface. The cache reads server-local data only:
5558
+ * the agent registry (in-memory) and the audit log (encrypted under
5559
+ * L1 master key at rest). Optional Verascore receipt cache is read
5560
+ * through a caller-provided source; absent, the verascore field
5561
+ * degrades to null, never to an outbound HTTP call.
5562
+ *
5563
+ * - Encryption boundary held: the cache holds plain text already
5564
+ * produced inside the fortress. Audit-log decryption happens inside
5565
+ * the audit-log subsystem, not here.
5566
+ *
5567
+ * - Failure-mode handling is fail-soft: a refresh that throws (audit
5568
+ * log read error, registry contention, oversize bundle) leaves the
5569
+ * prior snapshot list intact and returns `[]` from the failed
5570
+ * refresh. The caller can emit an audit event; the concierge
5571
+ * degrades gracefully to no agent-state section. The user-facing
5572
+ * query is never broken by a cache-refresh failure.
5573
+ *
5574
+ * Privacy invariant:
5575
+ *
5576
+ * Snapshots carry agent_id, harness, template, and aggregate counts.
5577
+ * They MUST NOT carry raw audit-log slices, raw operator queries, or
5578
+ * raw secret material. Counts are integers; current_work_summary is a
5579
+ * stable enum-derived string ("performed policy_change", "wrote to
5580
+ * state"); state_flags is a closed enum. The "Current agent state"
5581
+ * section the concierge renders into the substrate prompt aggregates
5582
+ * those fields only.
5583
+ *
5584
+ * Out of scope here (v1.4+ and later):
5585
+ *
5586
+ * - Cross-fortress agent state aggregation (v1.4 cross-tenant scope).
5587
+ * - Concierge-driven action execution (operator: "fix Cursor's
5588
+ * session"). Defer to v1.5+ pending approval-flow trust model.
5589
+ * - Advanced ML-backed state inference (per-operator agent-state norms).
5590
+ * Defer to v1.5+; out of scope for v1.3.
5591
+ */
5592
+
5593
+ /**
5594
+ * Stable enum of agent state flags surfaced into the substrate prompt
5595
+ * and the proactive-suggestion trigger. The order in `STATE_FLAG_ORDER`
5596
+ * encodes urgency: flags earlier in the list rank higher when the
5597
+ * prompt-section pruner has to drop low-urgency agents to fit budget.
5598
+ */
5599
+ type AgentStateFlag = "stuck" | "has_pending_approvals" | "has_open_findings" | "active" | "idle";
5600
+ /**
5601
+ * One snapshot entry per wrapped agent. Stable shape so the prompt
5602
+ * formatter and the proactive-suggestion generator can read off the
5603
+ * same data without re-fetching from the registry.
5604
+ *
5605
+ * Counts (`recent_audit_count_24h`, `recent_egress_count_24h`,
5606
+ * `recent_concordia_receipts_count_24h`) are integers derived from the
5607
+ * last-24h slice of the audit log. `recent_verascore_delta_24h` is
5608
+ * `null` when the optional Verascore source is not wired or returned
5609
+ * no delta.
5610
+ */
5611
+ interface AgentContextSnapshot {
5612
+ agent_id: string;
5613
+ agent_name: string;
5614
+ template: string;
5615
+ last_activity_at: string | null;
5616
+ current_work_summary: string | null;
5617
+ recent_audit_count_24h: number;
5618
+ recent_egress_count_24h: number;
5619
+ recent_concordia_receipts_count_24h: number;
5620
+ recent_verascore_delta_24h: number | null;
5621
+ state_flags: AgentStateFlag[];
5622
+ }
5623
+ /**
5624
+ * Optional source for the Verascore reputation delta surface. v1.0
5625
+ * Sanctuary does not currently expose a queryable Verascore receipt
5626
+ * cache server-side; the source is wired in on Verascore-bridge-aware
5627
+ * deployments and absent everywhere else. When absent, the snapshot's
5628
+ * `recent_verascore_delta_24h` field is null. The concierge functions
5629
+ * fully without it (non-dependency invariant).
5630
+ */
5631
+ interface VerascoreDeltaSource {
5632
+ /**
5633
+ * Return the most-recent 24h reputation delta for this agent, or
5634
+ * null when no delta is available. Implementations MUST be fast
5635
+ * (server-local, no outbound) so the cache refresh stays bounded.
5636
+ */
5637
+ read(agentId: string): number | null;
5638
+ }
5639
+ /**
5640
+ * One handler registration's removal callback.
5641
+ */
5642
+ type AgentContextChangeUnsubscribe = () => void;
5643
+ interface AgentContextCacheDeps {
5644
+ /** Operator identity that owns the agents this cache aggregates over. */
5645
+ identityId: string;
5646
+ /** Source of agent records, scoped to this operator's identity. */
5647
+ agentRegistry: HubAgentRegistrySource;
5648
+ /** Audit log used to derive recent-activity counts and state flags. */
5649
+ auditLog: AuditLog;
5650
+ /** Optional Verascore delta source. When absent, deltas degrade to null. */
5651
+ verascoreSource?: VerascoreDeltaSource;
5652
+ /** Hook invoked when a refresh throws. Defaults to a no-op. */
5653
+ onRefreshError?: (error: unknown) => void;
5654
+ /** Deterministic clock for tests. Defaults to `Date.now`. */
5655
+ clock?: () => number;
5656
+ /** Refresh cadence in ms. Defaults to 60_000. */
5657
+ refreshCadenceMs?: number;
5658
+ /**
5659
+ * Window in ms used to derive 24h counts. Defaults to 24h. Tunable
5660
+ * for tests so they can simulate older entries falling out of the
5661
+ * window without manipulating the clock.
5662
+ */
5663
+ windowMs?: number;
5664
+ }
5665
+ /**
5666
+ * Per-fortress in-memory cache of agent context snapshots. See
5667
+ * file-level docstring for the invariants this class upholds.
5668
+ */
5669
+ declare class AgentContextCache {
5670
+ private readonly identityId;
5671
+ private readonly agentRegistry;
5672
+ private readonly auditLog;
5673
+ private readonly verascoreSource;
5674
+ private readonly onRefreshError;
5675
+ private readonly clock;
5676
+ private readonly cadenceMs;
5677
+ private readonly windowMs;
5678
+ private snapshots;
5679
+ private observers;
5680
+ private timer;
5681
+ private inFlight;
5682
+ constructor(deps: AgentContextCacheDeps);
5683
+ /**
5684
+ * Run the refresh cycle. Returns the new snapshot list (which is
5685
+ * also stored as the cache state). On error, the prior snapshots
5686
+ * are kept and the resolved promise carries `[]`. The error is
5687
+ * dispatched to the `onRefreshError` hook so the caller can audit-
5688
+ * emit a degradation event.
5689
+ */
5690
+ refresh(): Promise<AgentContextSnapshot[]>;
5691
+ /**
5692
+ * Synchronous read of the latest cached snapshot list. Returns `[]`
5693
+ * until the first `refresh()` resolves, or after a failure that left
5694
+ * the cache empty.
5695
+ */
5696
+ read(): AgentContextSnapshot[];
5697
+ /**
5698
+ * Subscribe to refresh notifications. The handler fires once per
5699
+ * successful refresh with the new snapshot list. Returns an
5700
+ * unsubscribe callback the caller invokes on teardown.
5701
+ */
5702
+ observeChanges(handler: (snapshots: AgentContextSnapshot[]) => void): AgentContextChangeUnsubscribe;
5703
+ /**
5704
+ * Start the periodic refresh timer. Idempotent. Tests typically do
5705
+ * not call this and trigger refreshes manually.
5706
+ */
5707
+ start(): void;
5708
+ /**
5709
+ * Stop the periodic refresh timer. Idempotent.
5710
+ */
5711
+ stop(): void;
5712
+ private notifyObservers;
5713
+ }
5714
+ /**
5715
+ * Stable enum of the trigger classes the starter generator surfaces.
5716
+ * Carried into the audit emission so dashboards can group by cause.
5717
+ */
5718
+ type ProactiveStarterTrigger = "stuck_agent" | "pending_approvals" | "open_findings" | "all_idle";
5719
+ interface ConciergeProactiveStarter {
5720
+ text: string;
5721
+ trigger: ProactiveStarterTrigger;
5722
+ /**
5723
+ * How many agents contributed to this trigger. For `stuck_agent` and
5724
+ * `open_findings` this is the count of agents in that state; for
5725
+ * `pending_approvals` it is the count of agents with pending; for
5726
+ * `all_idle` it is the total agent count.
5727
+ */
5728
+ triggered_agents_count: number;
5729
+ }
5730
+
5524
5731
  /**
5525
5732
  * Sanctuary MCP Server — Operator Chat Service
5526
5733
  *
@@ -5711,6 +5918,21 @@ interface OperatorChatServiceDeps {
5711
5918
  * fetcher categories; this hook completes structured grammar fields.
5712
5919
  */
5713
5920
  conciergeGrammarLlmAssist?: LlmAssistGrammarCompletion;
5921
+ /**
5922
+ * WP-V1.3-9 Tau-5: caller-supplied agent-context cache. When wired
5923
+ * alongside the substrate selector, every `sendConcierge` round-trip
5924
+ * folds a "Current agent state" section into the substrate prompt
5925
+ * (between the dynamic-context fold and the prior-conversation
5926
+ * fold), and `getProactiveStarter()` surfaces a fresh-thread starter
5927
+ * based on the cache state. Omit to disable agent-context awareness;
5928
+ * the rest of the concierge surface keeps working unchanged.
5929
+ */
5930
+ conciergeAgentContextCache?: AgentContextCache;
5931
+ /**
5932
+ * WP-V1.3-9 Tau-5: rough token budget for the "Current agent state"
5933
+ * section. Defaults to `DEFAULT_CONCIERGE_AGENT_STATE_BUDGET` (400).
5934
+ */
5935
+ conciergeAgentStateBudget?: number;
5714
5936
  }
5715
5937
  declare class OperatorChatService {
5716
5938
  private store;
@@ -5731,6 +5953,15 @@ declare class OperatorChatService {
5731
5953
  private dynamicContextBudget;
5732
5954
  private agentRegistry?;
5733
5955
  private grammarLlmAssist?;
5956
+ private agentContextCache?;
5957
+ private agentStateBudget;
5958
+ /**
5959
+ * Per-thread guard so the proactive starter fires at most once per
5960
+ * fresh thread. Tracks the thread_id the starter was last offered
5961
+ * for; subsequent `getProactiveStarter()` calls within the same
5962
+ * thread return null instead of re-emitting.
5963
+ */
5964
+ private starterOfferedForThreadId?;
5734
5965
  /**
5735
5966
  * In-memory thread_id assigned to the active concierge session.
5736
5967
  * The first sendConcierge call after construction allocates a fresh
@@ -5796,8 +6027,37 @@ declare class OperatorChatService {
5796
6027
  * Reset the active session memory thread. Subsequent sendConcierge
5797
6028
  * calls allocate a fresh thread_id. Surfaced for tests + future "new
5798
6029
  * conversation" affordance; not currently called by the dashboard.
6030
+ *
6031
+ * Tau-5: also clears the proactive-starter guard so the next
6032
+ * `getProactiveStarter()` call against the freshly-allocated thread
6033
+ * is eligible to fire.
5799
6034
  */
5800
6035
  resetConciergeMemoryThread(): void;
6036
+ /**
6037
+ * WP-V1.3-9 Tau-5: surface a proactive starter for the current
6038
+ * concierge session. Intended to be called by the dashboard UI when
6039
+ * the operator opens the chat surface, before any operator typing.
6040
+ *
6041
+ * Returns null when:
6042
+ * - No agent-context cache is wired (Tau-5 disabled).
6043
+ * - No concierge memory store is wired (no thread_id namespace).
6044
+ * - The cache snapshot has no signal (empty fortress).
6045
+ * - A starter has already been offered for the active thread (the
6046
+ * guard ensures one starter per fresh thread).
6047
+ *
6048
+ * Side effects:
6049
+ * - Allocates a fresh thread_id if none is active.
6050
+ * - Emits the `operator_concierge_proactive_suggestion_offered`
6051
+ * audit event with the trigger class + triggered_agents_count.
6052
+ * - Records the offered thread_id so the next call within the same
6053
+ * thread is a no-op.
6054
+ *
6055
+ * The returned starter's `text` is operator-visible copy; the
6056
+ * dashboard renders it as a system-message-style starter the
6057
+ * operator can accept (clicks/types follow-up) or dismiss (types a
6058
+ * new query).
6059
+ */
6060
+ getProactiveStarter(): ConciergeProactiveStarter | null;
5801
6061
  private ensureActiveMemoryThread;
5802
6062
  /**
5803
6063
  * Stitch fortress state into a single context blob the substrate
@@ -6869,6 +7129,385 @@ declare class ApprovalAggregator {
6869
7129
  private hydrate;
6870
7130
  }
6871
7131
 
7132
+ /**
7133
+ * Sanctuary v1.3 WP-V1.3-1 Sentinel Baseline Pack (Castle Layer 2 anchor)
7134
+ *
7135
+ * Shared types for the Sentinel observation surface.
7136
+ *
7137
+ * Castle-walking discipline:
7138
+ * - Castle Layer 1 (Castle Wall) blocks unauthorized egress at the kernel
7139
+ * boundary. That is the enforcement layer.
7140
+ * - Castle Layer 2 (Sentinels) observes server-local data only and surfaces
7141
+ * anomalies. NO outbound network. NO blocking. Findings flow into the
7142
+ * operator surface they already check.
7143
+ * - Castle Layer 3 (Cooperative MCP) is the agent contract. Sentinels do
7144
+ * not wrap agent tools and do not negotiate with agents.
7145
+ *
7146
+ * Phi-1 ships the framework + the egress-volume watcher. Phi-2 through
7147
+ * Phi-5 add credential-usage, cross-agent-chatter, suspicious-tool-call,
7148
+ * and anomaly-trigger sentinels against this same shape.
7149
+ */
7150
+
7151
+ /** Severity tier returned by `Sentinel.evaluate()`. */
7152
+ type SentinelSeverity = "info" | "warn" | "alert";
7153
+ /**
7154
+ * Read-only context handed to every sentinel on subscribe + evaluate.
7155
+ * Sentinels do NOT receive a writable storage handle and do NOT receive
7156
+ * an outbound HTTP client. The substrate selector is the single
7157
+ * outbound-capable surface; Phi-1 sentinels do not exercise it but
7158
+ * Phi-2/3/4/5 may want to invoke a local LLM for reasoning.
7159
+ */
7160
+ interface SentinelContext {
7161
+ /** Stable per-fortress identifier; multi-fortress isolation pivot. */
7162
+ fortressId: string;
7163
+ /** Audit log read API. Sentinels MUST NOT call append() through this. */
7164
+ auditLog: AuditLog;
7165
+ /** Optional substrate selector for LLM-backed reasoning. v1.3 Phi-1 unused. */
7166
+ substrateSelector?: SubstrateSelector;
7167
+ /** Wall-clock provider. Tests inject a deterministic clock. */
7168
+ now: () => Date;
7169
+ }
7170
+ /**
7171
+ * One observation a sentinel produced. Persisted under the sentinel
7172
+ * findings store + emitted as an audit event +
7173
+ * surfaced to the operator dashboard.
7174
+ */
7175
+ interface SentinelFinding {
7176
+ /** Stable per-fortress finding id (UUID v4). */
7177
+ finding_id: string;
7178
+ /** Identifier of the sentinel that produced this finding. */
7179
+ sentinel_id: string;
7180
+ severity: SentinelSeverity;
7181
+ /**
7182
+ * Optional agent-id this finding pertains to. Absent when the finding
7183
+ * is fortress-wide (e.g. cross-agent chatter spike).
7184
+ */
7185
+ agent_id?: string;
7186
+ /** Operator-friendly one-liner. Truncated to 240 chars on persist. */
7187
+ summary: string;
7188
+ /**
7189
+ * Structured payload. Schema varies per sentinel but every payload is
7190
+ * a flat record of JSON-serializable values.
7191
+ */
7192
+ details: Record<string, unknown>;
7193
+ /** ISO 8601 timestamp at evaluation time. */
7194
+ observed_at: string;
7195
+ /**
7196
+ * Audit-log entries that triggered this finding. Sentinels populate
7197
+ * with `${entry.timestamp}:${entry.operation}` tuples (mirrors the
7198
+ * aggregator's audit_log_entry_id shape from Upsilon-1).
7199
+ */
7200
+ evidence_audit_ids: string[];
7201
+ /** Stable fortress id stamped at emit. Multi-fortress isolation pivot. */
7202
+ fortress_id: string;
7203
+ }
7204
+
7205
+ /**
7206
+ * Sanctuary v1.3 WP-V1.3-1 Sentinel base class.
7207
+ *
7208
+ * Subclass to add a new sentinel. The dispatcher schedules `evaluate()`
7209
+ * on a tick. `subscribe()` is called once when an operator opts in;
7210
+ * `unsubscribe()` is called once when they opt out (or the fortress is
7211
+ * disposed). Sentinels MUST treat the SentinelContext as read-only.
7212
+ */
7213
+
7214
+ /**
7215
+ * Concrete sentinels implement these three methods and stamp a
7216
+ * sentinel_id. Implementations must:
7217
+ * - Be pure with respect to the SentinelContext (no writable
7218
+ * storage, no external network).
7219
+ * - Return findings synchronously deterministically given the same
7220
+ * context state.
7221
+ * - Tolerate empty audit logs (return []).
7222
+ */
7223
+ declare abstract class Sentinel {
7224
+ /**
7225
+ * Stable identifier. Used as the dictionary key in the registry, the
7226
+ * URL path component, and the audit-log details payload's
7227
+ * `sentinel_id` field. Convention: lowercase, dash-separated, prefix
7228
+ * with the sentinel category (e.g. `egress-volume`,
7229
+ * `credential-usage`).
7230
+ */
7231
+ abstract readonly sentinelId: string;
7232
+ /**
7233
+ * Human-readable description shown in the operator dashboard's
7234
+ * "available sentinels" list. Should answer "what does this watch?"
7235
+ * in one sentence.
7236
+ */
7237
+ abstract readonly description: string;
7238
+ /**
7239
+ * Bind the sentinel to a fortress context. Called once on
7240
+ * subscribe. Default implementation stores the context on `this`;
7241
+ * sentinels that need additional setup (e.g. priming a baseline
7242
+ * cache) override.
7243
+ */
7244
+ subscribe(context: SentinelContext): Promise<void>;
7245
+ /**
7246
+ * Tear down. Default implementation clears the context; subclasses
7247
+ * that hold timers or external handles override.
7248
+ */
7249
+ unsubscribe(): Promise<void>;
7250
+ /**
7251
+ * Compute findings against the current audit-log + agent-state
7252
+ * snapshot. Returns an empty array when nothing notable is observed.
7253
+ * Throwing is allowed; the dispatcher catches and emits a
7254
+ * `sentinel_evaluation_failed` audit event.
7255
+ */
7256
+ abstract evaluate(): Promise<SentinelFinding[]>;
7257
+ protected context: SentinelContext | undefined;
7258
+ /** Internal helper: assert subscribed before evaluation. */
7259
+ protected requireContext(): SentinelContext;
7260
+ }
7261
+
7262
+ /**
7263
+ * Sanctuary v1.3 WP-V1.3-1 Sentinel Registry.
7264
+ *
7265
+ * Tracks two things for one fortress:
7266
+ * 1. The catalog of sentinel CLASSES the fortress can subscribe to
7267
+ * (registered at process boot).
7268
+ * 2. Which sentinels the operator has actually opted IN.
7269
+ *
7270
+ * Default subscription set is empty: the operator must opt in. This
7271
+ * keeps the no-outbound-by-default rule and the "operator pulls
7272
+ * features" UX pattern from CLAUDE.md.
7273
+ *
7274
+ * Multi-fortress isolation: one registry instance per fortress. The
7275
+ * dispatcher consults the registry and never crosses fortress
7276
+ * boundaries.
7277
+ */
7278
+
7279
+ interface SentinelCatalogEntry {
7280
+ sentinelId: string;
7281
+ description: string;
7282
+ factory: () => Sentinel;
7283
+ }
7284
+ /**
7285
+ * Coordinator-CTO defaults: the registry ships with the Phi-1 catalog
7286
+ * pre-loaded but with zero subscriptions. Tests inject custom catalogs
7287
+ * via `register()`.
7288
+ */
7289
+ declare class SentinelRegistry {
7290
+ private readonly catalog;
7291
+ private readonly subscribed;
7292
+ register(entry: SentinelCatalogEntry): void;
7293
+ /**
7294
+ * Available sentinels (catalog view). Operator UI lists this so the
7295
+ * operator can pick what to subscribe to.
7296
+ */
7297
+ listCatalog(): Array<{
7298
+ sentinelId: string;
7299
+ description: string;
7300
+ }>;
7301
+ /** Currently subscribed sentinel ids. */
7302
+ listSubscribed(): string[];
7303
+ /** Has the fortress opted into this sentinel? */
7304
+ isSubscribed(sentinelId: string): boolean;
7305
+ /**
7306
+ * Subscribe a sentinel to a fortress context. Idempotent: a second
7307
+ * subscribe call on an already-subscribed sentinel returns the
7308
+ * existing instance without re-running `subscribe()`.
7309
+ */
7310
+ subscribe(sentinelId: string, context: SentinelContext): Promise<Sentinel>;
7311
+ /**
7312
+ * Unsubscribe. Idempotent: unsubscribing an unsubscribed sentinel
7313
+ * returns false without throwing. Returns true when an active
7314
+ * subscription was torn down.
7315
+ */
7316
+ unsubscribe(sentinelId: string): Promise<boolean>;
7317
+ /**
7318
+ * Snapshot of subscribed sentinels for the dispatcher's tick path.
7319
+ * Returned as an array so the dispatcher can iterate without holding
7320
+ * the map under modification.
7321
+ */
7322
+ snapshotSubscribed(): Array<{
7323
+ sentinelId: string;
7324
+ sentinel: Sentinel;
7325
+ }>;
7326
+ /**
7327
+ * Tear down every subscription. Called by the dispatcher on
7328
+ * fortress-shutdown. Best-effort: a failing unsubscribe does not
7329
+ * abort the rest.
7330
+ */
7331
+ unsubscribeAll(): Promise<void>;
7332
+ }
7333
+
7334
+ /**
7335
+ * Sanctuary v1.3 WP-V1.3-1 Sentinel Finding Store.
7336
+ *
7337
+ * Encrypted at-rest persistence for sentinel findings. Sibling to the
7338
+ * Upsilon-3 aggregator-store: same fortress-master-key-derived HKDF
7339
+ * subkey shape, AAD-bound to the finding_id, retention-aware.
7340
+ *
7341
+ * Storage layout:
7342
+ * namespace: `_sentinel_findings`
7343
+ * key: `finding.{finding_id}` (one record per finding)
7344
+ * payload: AES-256-GCM ciphertext of the JSON-serialized record.
7345
+ * key: `l2-sentinel-finding-v1` HKDF subkey of fortress master.
7346
+ * AAD: UTF-8 bytes of `finding_id`.
7347
+ *
7348
+ * Multi-fortress isolation: HKDF subkey derives from the fortress
7349
+ * master key. Two fortresses never produce identical encryption keys
7350
+ * for identical finding_ids.
7351
+ *
7352
+ * Retention: 30 days default, mirroring the audit-log envelope and the
7353
+ * aggregator payload store.
7354
+ */
7355
+
7356
+ interface SentinelFindingStoreOptions {
7357
+ storage: StorageBackend;
7358
+ masterKey: Uint8Array;
7359
+ fortressId: string;
7360
+ /** Operator-tunable retention window. Default 30 days. */
7361
+ retentionDays?: number;
7362
+ /** Wall-clock provider for deterministic tests. */
7363
+ now?: () => Date;
7364
+ }
7365
+ declare class SentinelFindingStore {
7366
+ private readonly storage;
7367
+ private readonly encryptionKey;
7368
+ private readonly fortressId;
7369
+ private readonly retentionDays;
7370
+ private readonly now;
7371
+ constructor(opts: SentinelFindingStoreOptions);
7372
+ /**
7373
+ * Persist a finding. Truncates the operator-visible summary to
7374
+ * SENTINEL_SUMMARY_MAX_CHARS so the dashboard render stays bounded.
7375
+ * Returns the retention deadline so callers can audit it.
7376
+ */
7377
+ saveFinding(finding: SentinelFinding): Promise<string>;
7378
+ /** Load a single finding by id, or null when absent / corrupted. */
7379
+ loadFinding(findingId: string): Promise<SentinelFinding | null>;
7380
+ /**
7381
+ * List findings, newest first. Optional filters: since (ISO 8601),
7382
+ * severity, sentinel_id, agent_id, limit. Default limit 100.
7383
+ */
7384
+ listFindings(opts?: {
7385
+ since?: string;
7386
+ severity?: SentinelSeverity;
7387
+ sentinelId?: string;
7388
+ agentId?: string;
7389
+ limit?: number;
7390
+ }): Promise<SentinelFinding[]>;
7391
+ /**
7392
+ * Drop expired findings. Returns the count removed.
7393
+ */
7394
+ pruneExpired(now?: Date): Promise<{
7395
+ pruned: number;
7396
+ }>;
7397
+ private decode;
7398
+ }
7399
+
7400
+ /**
7401
+ * Sanctuary v1.3 WP-V1.3-1 Sentinel Dispatcher.
7402
+ *
7403
+ * Drives the sentinel evaluation tick. On each tick:
7404
+ * 1. For every subscribed sentinel, call `evaluate()`.
7405
+ * 2. Each returned finding routes to:
7406
+ * - the sentinel finding store (encrypted persistence)
7407
+ * - the audit log (`sentinel_finding_emitted`)
7408
+ * - in-process subscribers (dashboard SSE wires through this)
7409
+ * 3. Per-sentinel exceptions log `sentinel_evaluation_failed` and the
7410
+ * dispatcher continues with the next sentinel.
7411
+ *
7412
+ * The dispatcher is one-per-fortress. The fortress id is stamped on
7413
+ * every finding before persistence + emission so multi-fortress
7414
+ * isolation holds at the cryptographic + structural layers both.
7415
+ *
7416
+ * Castle-walking discipline: the dispatcher introduces no outbound
7417
+ * surface. It reads from the audit log, writes to the encrypted
7418
+ * findings store + audit log, and emits to in-process subscribers.
7419
+ */
7420
+
7421
+ type SentinelDispatcherUnsubscribe = () => void;
7422
+ /**
7423
+ * Event emitted to in-process subscribers when a finding is produced.
7424
+ * The dashboard SSE pipeline subscribes here.
7425
+ */
7426
+ interface SentinelDispatcherEmit {
7427
+ type: "finding";
7428
+ finding: SentinelFinding;
7429
+ }
7430
+ /**
7431
+ * Event emitted on per-sentinel evaluation failure. Distinct from
7432
+ * `finding` so subscribers can render diagnostics separately.
7433
+ */
7434
+ interface SentinelDispatcherFailureEmit {
7435
+ type: "evaluation_failed";
7436
+ sentinel_id: string;
7437
+ error_message: string;
7438
+ observed_at: string;
7439
+ }
7440
+ type SentinelDispatcherAnyEmit = SentinelDispatcherEmit | SentinelDispatcherFailureEmit;
7441
+ interface SentinelDispatcherDeps {
7442
+ registry: SentinelRegistry;
7443
+ findingStore: SentinelFindingStore;
7444
+ auditLog: AuditLog;
7445
+ /** Stable fortress id stamped on every finding. */
7446
+ fortressId: string;
7447
+ /** Operator identity id for audit attribution. */
7448
+ identityId: string;
7449
+ /** Wall-clock provider for deterministic tests. */
7450
+ now?: () => Date;
7451
+ /** Tick interval, ms. Default 60s. Set to 0 to disable auto-tick. */
7452
+ tickIntervalMs?: number;
7453
+ }
7454
+ declare class SentinelDispatcher {
7455
+ private readonly registry;
7456
+ private readonly findingStore;
7457
+ private readonly auditLog;
7458
+ private readonly fortressId;
7459
+ private readonly identityId;
7460
+ private readonly now;
7461
+ private readonly tickIntervalMs;
7462
+ private readonly listeners;
7463
+ private tickTimer;
7464
+ private tickInFlight;
7465
+ constructor(deps: SentinelDispatcherDeps);
7466
+ /** Read-only view of the registry. Convenience for route handlers. */
7467
+ getRegistry(): SentinelRegistry;
7468
+ /** Read-only view of the finding store. Convenience for route handlers. */
7469
+ getFindingStore(): SentinelFindingStore;
7470
+ /**
7471
+ * Subscribe an in-process listener. Returns an unsubscribe fn.
7472
+ */
7473
+ onEvent(listener: (event: SentinelDispatcherAnyEmit) => void): SentinelDispatcherUnsubscribe;
7474
+ /**
7475
+ * Subscribe a sentinel to this fortress + emit the
7476
+ * `sentinel_subscribed` audit event. Wraps `registry.subscribe()` so
7477
+ * the audit emission lives at the dispatcher boundary (the
7478
+ * fortress-aware site).
7479
+ */
7480
+ subscribeSentinel(sentinelId: string, contextOverrides?: Partial<SentinelContext>): Promise<Sentinel>;
7481
+ /**
7482
+ * Unsubscribe + emit `sentinel_unsubscribed`. Returns true when an
7483
+ * active subscription was torn down. Audit fires only on successful
7484
+ * removal.
7485
+ */
7486
+ unsubscribeSentinel(sentinelId: string): Promise<boolean>;
7487
+ /**
7488
+ * Run one evaluation pass over every subscribed sentinel. Used by
7489
+ * the auto-tick AND by tests that want a synchronous evaluation
7490
+ * gate. Returns the findings produced this tick (already persisted
7491
+ * + audit-logged + emitted).
7492
+ */
7493
+ tick(): Promise<SentinelFinding[]>;
7494
+ /**
7495
+ * Start the auto-tick loop. No-op when tickIntervalMs is 0 or when
7496
+ * already started. Tests typically leave auto-tick off and call
7497
+ * `tick()` directly.
7498
+ */
7499
+ start(): void;
7500
+ /** Stop the auto-tick loop. Idempotent. */
7501
+ stop(): void;
7502
+ /**
7503
+ * Tear down every subscription + stop the tick loop. Called on
7504
+ * fortress shutdown.
7505
+ */
7506
+ dispose(): Promise<void>;
7507
+ private routeFinding;
7508
+ private emit;
7509
+ }
7510
+
6872
7511
  /**
6873
7512
  * Sanctuary MCP Server — Principal Dashboard
6874
7513
  *
@@ -6966,6 +7605,13 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
6966
7605
  * the operator-facing query / decision surface.
6967
7606
  */
6968
7607
  private approvalAggregator;
7608
+ /**
7609
+ * v1.3 WP-V1.3-1 Phi-1 Sentinel dispatcher. Mounted additively at
7610
+ * `/api/sentinels/*` when set. Sentinel surface is read-only against
7611
+ * the audit log; subscribe/unsubscribe writes flow through the
7612
+ * dispatcher's audited paths.
7613
+ */
7614
+ private sentinelDispatcher;
6969
7615
  constructor(config: DashboardConfig);
6970
7616
  /**
6971
7617
  * Inject dependencies after construction.
@@ -7003,11 +7649,23 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
7003
7649
  * tests + during shutdown).
7004
7650
  */
7005
7651
  setApprovalAggregator(aggregator: ApprovalAggregator | null): void;
7652
+ /**
7653
+ * v1.3 WP-V1.3-1 Phi-1: bind the Sentinel dispatcher. Once set,
7654
+ * requests to `/api/sentinels/*` route through `handleSentinelRoute`.
7655
+ * Pass `null` to detach (used by tests + during shutdown).
7656
+ */
7657
+ setSentinelDispatcher(dispatcher: SentinelDispatcher | null): void;
7006
7658
  /**
7007
7659
  * v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
7008
7660
  * before the legacy approval route table. Returns true when served.
7009
7661
  */
7010
7662
  private dispatchApprovalInbox;
7663
+ /**
7664
+ * v1.3 WP-V1.3-1 Phi-1 dispatch entry point. Routes `/api/sentinels/*`
7665
+ * requests through the sentinel router when a dispatcher has been
7666
+ * bound. Returns true when served.
7667
+ */
7668
+ private dispatchSentinel;
7011
7669
  /**
7012
7670
  * v1.1 dispatch entry point. Called from `handleRequest` before the
7013
7671
  * legacy route table. Returns true when the request was served by v1.1