@sanctuary-framework/mcp-server 1.2.6 → 1.2.8

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.ts CHANGED
@@ -5315,27 +5315,204 @@ declare class ConciergeMemoryStore {
5315
5315
  private withLock;
5316
5316
  }
5317
5317
 
5318
+ /**
5319
+ * Sanctuary MCP Server, Operator Chat, Concierge Query Grammar
5320
+ *
5321
+ * WP-V1.3-9 Tau-4 (criterion (d) of the v1.3 scope-lock revision). Builds
5322
+ * on Tau-3 (PR #171, dynamic context router). The grammar layer parses
5323
+ * operator queries into structured parameters: time ranges, agent names,
5324
+ * event types, and a free-form intent residual. Tau-3 fetchers receive
5325
+ * the parsed hints alongside the query so the substrate sees the most
5326
+ * relevant data slice.
5327
+ *
5328
+ * The module is pure: rule-based extraction, no I/O, no clock side
5329
+ * effects beyond the optional `now` parameter for deterministic tests.
5330
+ * The optional LLM-assist fallback is invoked by the caller when the
5331
+ * rule-based parser produces low confidence.
5332
+ *
5333
+ * Castle-walking discipline:
5334
+ * - No new outbound surface; the LLM-assist fallback routes through the
5335
+ * substrate selector at the same `concierge` surface Tau-3 already
5336
+ * uses.
5337
+ * - Encryption boundaries hold: this module receives plain strings
5338
+ * already produced inside the fortress.
5339
+ * - Failure-mode handling is fail-soft: low-confidence parses surface a
5340
+ * structured fallback menu so the operator sees what the concierge
5341
+ * can answer, never a silent drop.
5342
+ *
5343
+ * Privacy invariant:
5344
+ * The runtime `ParsedQuery` carries `intent_phrase` (free-form residual
5345
+ * pulled from the operator's query) for the router's own consumption.
5346
+ * The audit emission MUST NOT include `intent_phrase`; consumers serialize
5347
+ * via `auditSafeSummary()` which strips it. See `OperatorConciergeChatPayload`
5348
+ * safe-metadata invariant in `contracts/v1.2/operator-chat-events.ts`.
5349
+ *
5350
+ * Out of scope here (Tau-5 + later):
5351
+ * - Agent-context awareness (proactive parsing without an explicit
5352
+ * query). Tau-4 parses off explicit query content only.
5353
+ * - Voice input or natural-language-as-API surface (defer to v1.5+).
5354
+ * - Cross-fortress concierge query (defer to v1.4 cross-tenant scope).
5355
+ * - Window markers like "around the time of <event>" that require an
5356
+ * audit-log lookup to resolve. Tau-4 ships static patterns only.
5357
+ */
5358
+
5359
+ /**
5360
+ * A resolved time range. `relative_label` carries the original token
5361
+ * ("yesterday", "past hour", etc.) when the range was derived from a
5362
+ * relative pattern, so consumers can render operator-readable copy.
5363
+ */
5364
+ interface ParsedTimeRange {
5365
+ start: Date;
5366
+ end: Date;
5367
+ relative_label?: string;
5368
+ }
5369
+ /**
5370
+ * Stable structured-failure flags surfaced when the parser cannot
5371
+ * resolve a token confidently. The strings are enum-like (no free text)
5372
+ * so the audit emission stays safe-metadata-only and the dashboard can
5373
+ * group by cause.
5374
+ */
5375
+ type AmbiguityFlag = "unknown_time_token" | "unknown_agent_token" | "unknown_event_token" | "ambiguous_time_range" | "no_signal_extracted";
5376
+ /**
5377
+ * Runtime parsed-query shape. Used by the router to thread parameters
5378
+ * into context fetchers, and by the structured-fallback menu builder.
5379
+ *
5380
+ * `intent_phrase` is the residual after time/agent/event tokens are
5381
+ * stripped, useful for router-level intent routing. It carries operator
5382
+ * query content and MUST NOT be serialized into the audit log.
5383
+ */
5384
+ interface ParsedQuery {
5385
+ time_range: ParsedTimeRange | null;
5386
+ agent_names: string[];
5387
+ event_types: string[];
5388
+ intent_phrase: string;
5389
+ ambiguity_flags: AmbiguityFlag[];
5390
+ /** 0..1; 1.0 = every component resolved cleanly. */
5391
+ parse_confidence: number;
5392
+ }
5393
+ /**
5394
+ * Read-only registry view consumed by `parseQuery` for agent-name
5395
+ * extraction. The grammar matches operator query tokens against the
5396
+ * `agent_id` field of each record (case-insensitive, prefix-or-exact).
5397
+ *
5398
+ * Wired from `HubAgentRegistrySource.list()` at the service layer; tests
5399
+ * can hand a literal array. Both shapes are accepted so callers do not
5400
+ * have to wrap a hub registry in an adapter just to feed the parser.
5401
+ */
5402
+ type AgentRegistryView = readonly {
5403
+ agent_id: string;
5404
+ }[] | {
5405
+ list: () => readonly {
5406
+ agent_id: string;
5407
+ }[];
5408
+ };
5409
+ /**
5410
+ * Optional LLM-assist completion. Invoked by `parseQueryWithLlmAssist`
5411
+ * when the rule-based parse confidence is below `LLM_ASSIST_THRESHOLD`.
5412
+ * The classifier sees the query plus the partial parse and may return
5413
+ * `null` (no completion) or a partial-shape patch the caller merges.
5414
+ *
5415
+ * The runtime caller (operator-chat-service) MUST route this through
5416
+ * the substrate selector at the `concierge` surface so no new outbound
5417
+ * channel opens. See Castle-walking discipline at the top of the file.
5418
+ */
5419
+ type LlmAssistGrammarCompletion = (query: string, partial: ParsedQuery) => Promise<Partial<{
5420
+ time_range: ParsedTimeRange;
5421
+ agent_names: string[];
5422
+ event_types: string[];
5423
+ }> | null>;
5424
+
5425
+ /**
5426
+ * Sanctuary MCP Server, Operator Chat, Concierge Dynamic Context Router
5427
+ *
5428
+ * WP-V1.3-9 Tau-3 (criterion (c) of the v1.3 scope-lock revision). Builds
5429
+ * on Tau-1 (PR #164, concierge memory store) and Tau-2 (PR #166, multi-turn
5430
+ * coherence read-fold path). At each operator turn the concierge fetches
5431
+ * live data relevant to the operator's query and folds it into context
5432
+ * between the static Sanctuary reference and the prior-turns fold.
5433
+ *
5434
+ * Tau-4 (PR #_, criterion (d)) layers an operator-query grammar on top:
5435
+ * the caller (`operator-chat-service`) parses the query into a
5436
+ * `ParsedQuery` via `concierge-query-grammar`, threads the result into
5437
+ * `classifyQuery` + `foldContext`, and the matches carry the parsed
5438
+ * grammar through to fetchers as `FetcherHints` (time range + agent
5439
+ * names + event types). Existing fetcher implementations that do not
5440
+ * read the second argument continue to work unchanged.
5441
+ *
5442
+ * The module is intentionally pure: keyword classification + a small
5443
+ * amount of hint extraction + a fold orchestrator that calls caller-
5444
+ * provided fetchers. The wiring layer assembles the concrete fetchers
5445
+ * (templates registry, agent registry, audit log, sentinel store, etc.)
5446
+ * so this module stays test-isolatable and composable.
5447
+ *
5448
+ * Castle-walking discipline:
5449
+ * - No new outbound surface; the optional auxiliary classifier is invoked
5450
+ * through the substrate selector by the caller (the service threads the
5451
+ * classifier callback through, this module never owns the channel).
5452
+ * - Encryption boundaries hold for the underlying data sources; this
5453
+ * module receives plain strings already produced inside the fortress.
5454
+ * - Failure-mode handling is fail-soft: a fetcher failure omits its
5455
+ * category from the fold and surfaces an audit event through the
5456
+ * caller's onFetcherFailure hook. The user-facing concierge query is
5457
+ * never broken by a context-assembly failure.
5458
+ *
5459
+ * Out of scope here (Tau-5):
5460
+ * - Agent-context awareness (proactive folding without an explicit
5461
+ * query). The router still routes off explicit query content only.
5462
+ */
5463
+
5318
5464
  /**
5319
5465
  * Closed enum of categories the router may classify a query into. Adding
5320
5466
  * a category requires updating: this enum, the keyword table, the
5321
5467
  * fetcher interface, every wiring fetcher, and the test surface.
5322
5468
  */
5323
5469
  type ContextCategory = "templates" | "agent_state" | "agent_activity" | "audit_log" | "sentinel_findings" | "anomaly_alerts" | "recent_receipts" | "verascore_deltas";
5470
+ /**
5471
+ * Structured hints passed to fetchers as the optional second argument
5472
+ * (Tau-4). Derived from `ParsedQuery` by the router; fetchers are free
5473
+ * to read or ignore individual fields. Tau-3 fetchers that take no
5474
+ * second argument continue to work because the slot is optional.
5475
+ */
5476
+ interface FetcherHints {
5477
+ /** Time range derived from the operator's query, when one resolved. */
5478
+ time_range?: {
5479
+ start: Date;
5480
+ end: Date;
5481
+ relative_label?: string;
5482
+ };
5483
+ /**
5484
+ * All agent names the parser matched against the registry, in the
5485
+ * order they surfaced. Distinct from the `agentNameHint` first arg
5486
+ * (which carries only the first match for back-compat).
5487
+ */
5488
+ agent_names?: readonly string[];
5489
+ /**
5490
+ * Event-type names the parser matched against the canonical audit-
5491
+ * event-class enumeration. Already filtered to the closed set.
5492
+ */
5493
+ event_types?: readonly string[];
5494
+ }
5324
5495
  /**
5325
5496
  * Caller-supplied data sources. Each fetcher returns plain text the
5326
5497
  * router stitches into the rendered section. Returning the empty string
5327
5498
  * (or whitespace only) is treated as "nothing to fold for this category"
5328
5499
  * and the category drops out of the final categoriesIncluded list.
5500
+ *
5501
+ * Tau-4 extends every fetcher with an optional `hints?: FetcherHints`
5502
+ * argument carrying the parsed-query attachments (time range, agent
5503
+ * names, event types). Fetchers that ignore the argument continue to
5504
+ * compile; fetchers that consume it can scope queries against the
5505
+ * audit log, activity feed, etc., to the operator's stated window.
5329
5506
  */
5330
5507
  interface ContextFetchers {
5331
- templates: () => Promise<string>;
5332
- agent_state: (agentNameHint: string | null) => Promise<string>;
5333
- agent_activity: (agentNameHint: string | null) => Promise<string>;
5334
- audit_log: () => Promise<string>;
5335
- sentinel_findings: () => Promise<string>;
5336
- anomaly_alerts: () => Promise<string>;
5337
- recent_receipts: () => Promise<string>;
5338
- verascore_deltas: () => Promise<string>;
5508
+ templates: (hints?: FetcherHints) => Promise<string>;
5509
+ agent_state: (agentNameHint: string | null, hints?: FetcherHints) => Promise<string>;
5510
+ agent_activity: (agentNameHint: string | null, hints?: FetcherHints) => Promise<string>;
5511
+ audit_log: (hints?: FetcherHints) => Promise<string>;
5512
+ sentinel_findings: (hints?: FetcherHints) => Promise<string>;
5513
+ anomaly_alerts: (hints?: FetcherHints) => Promise<string>;
5514
+ recent_receipts: (hints?: FetcherHints) => Promise<string>;
5515
+ verascore_deltas: (hints?: FetcherHints) => Promise<string>;
5339
5516
  }
5340
5517
  /**
5341
5518
  * Auxiliary classifier the caller may supply for the LLM-assist fallback
@@ -5344,6 +5521,213 @@ interface ContextFetchers {
5344
5521
  */
5345
5522
  type LlmAssistClassifier = (query: string, categories: readonly ContextCategory[]) => Promise<ContextCategory | "none">;
5346
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
+
5347
5731
  /**
5348
5732
  * Sanctuary MCP Server — Operator Chat Service
5349
5733
  *
@@ -5514,6 +5898,41 @@ interface OperatorChatServiceDeps {
5514
5898
  * Defaults to `DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET` (2000).
5515
5899
  */
5516
5900
  conciergeDynamicContextBudget?: number;
5901
+ /**
5902
+ * WP-V1.3-9 Tau-4: read-only registry view consumed by the operator-
5903
+ * query grammar to extract agent-name parameters. Wired from
5904
+ * `HubAgentRegistrySource.list()` at the hub-service layer; absent
5905
+ * services skip agent-name extraction (the grammar still resolves
5906
+ * time ranges and event types). The registry is read at every
5907
+ * `sendConcierge` call so newly-wrapped agents are visible without
5908
+ * service reconstruction.
5909
+ */
5910
+ conciergeAgentRegistry?: AgentRegistryView;
5911
+ /**
5912
+ * WP-V1.3-9 Tau-4: optional LLM-assist completion for low-confidence
5913
+ * grammar parses. When wired alongside the substrate selector, the
5914
+ * service routes the call through the substrate selector at the
5915
+ * `concierge` surface, so the grammar fallback shares the operator's
5916
+ * substrate choice and never opens a new outbound surface. Distinct
5917
+ * from `conciergeContextLlmAssist`, which classifies queries into
5918
+ * fetcher categories; this hook completes structured grammar fields.
5919
+ */
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;
5517
5936
  }
5518
5937
  declare class OperatorChatService {
5519
5938
  private store;
@@ -5532,6 +5951,17 @@ declare class OperatorChatService {
5532
5951
  private contextFetchers?;
5533
5952
  private contextLlmAssist?;
5534
5953
  private dynamicContextBudget;
5954
+ private agentRegistry?;
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?;
5535
5965
  /**
5536
5966
  * In-memory thread_id assigned to the active concierge session.
5537
5967
  * The first sendConcierge call after construction allocates a fresh
@@ -5597,8 +6027,37 @@ declare class OperatorChatService {
5597
6027
  * Reset the active session memory thread. Subsequent sendConcierge
5598
6028
  * calls allocate a fresh thread_id. Surfaced for tests + future "new
5599
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.
5600
6034
  */
5601
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;
5602
6061
  private ensureActiveMemoryThread;
5603
6062
  /**
5604
6063
  * Stitch fortress state into a single context blob the substrate
@@ -5646,8 +6105,20 @@ declare class OperatorChatService {
5646
6105
  * proceeds with no fold. Returns the rendered section + the list of
5647
6106
  * categories whose data made it into the section (used for the
5648
6107
  * round-trip audit emission).
6108
+ *
6109
+ * Tau-4: receives the pre-parsed `ParsedQuery` and forwards it as the
6110
+ * `parsed` opt to `foldContext`, so fetchers see the structured
6111
+ * `FetcherHints` derived from it.
5649
6112
  */
5650
6113
  private runDynamicContextFold;
6114
+ /**
6115
+ * WP-V1.3-9 Tau-4: parse the (PII-filtered) operator query into a
6116
+ * `ParsedQuery`. Routes through the LLM-assist completion hook when
6117
+ * configured and the rule-based parse is below
6118
+ * `LLM_ASSIST_THRESHOLD`. Always returns a parse object (never
6119
+ * throws) so the audit emission can carry the result unconditionally.
6120
+ */
6121
+ private runGrammarParse;
5651
6122
  /**
5652
6123
  * Emit the WP-V1.3-9 Tau-3 fetcher-failure audit event. Pulled out
5653
6124
  * of the fold path so the dynamic-context handler stays readable.
@@ -6329,6 +6800,22 @@ interface AggregatedApproval {
6329
6800
  * Upsilon-3.
6330
6801
  */
6331
6802
  enforcement_chain?: EnforcementLayerEvent[];
6803
+ /**
6804
+ * Aggregator revision number stamped at entry creation. Stable across
6805
+ * status transitions. Used by the v1.4 mobile companion sync API to
6806
+ * separate "added" from "changed" deltas (created_at_revision >
6807
+ * sinceRevision means added; otherwise it is a status change).
6808
+ * v1.3 Upsilon-4.
6809
+ */
6810
+ created_at_revision?: number;
6811
+ /**
6812
+ * Aggregator revision number stamped at the last mutation (creation,
6813
+ * resolution, expiration). Sync-API consumers compare against the
6814
+ * revision they last saw to compute the delta. Monotonically
6815
+ * increasing per fortress; gaps are normal (one bump per mutation).
6816
+ * v1.3 Upsilon-4.
6817
+ */
6818
+ last_modified_revision?: number;
6332
6819
  }
6333
6820
  /**
6334
6821
  * Source-context resolver. Called once per ingest to map gate context to
@@ -6385,11 +6872,35 @@ type ApprovalAggregatorUnsubscribe = () => void;
6385
6872
  interface ApprovalAggregatorEmit {
6386
6873
  /**
6387
6874
  * `aggregated` on first ingest, `resolved` on a status leaving pending,
6388
- * `deduped` when an ingest dropped because the same dedup key was seen.
6875
+ * `deduped` when an ingest dropped because the same dedup key was seen,
6876
+ * `removed` when an entry was pruned (v1.3 Upsilon-4).
6389
6877
  */
6390
- type: "aggregated" | "resolved" | "deduped";
6878
+ type: "aggregated" | "resolved" | "deduped" | "removed";
6391
6879
  entry: AggregatedApproval;
6392
6880
  }
6881
+ /**
6882
+ * Sync-API delta returned by `getSync()`. v1.3 Upsilon-4. Mobile
6883
+ * companions poll this for cheap state-sync without re-fetching the
6884
+ * full inbox. The `revision` field on the response is the aggregator's
6885
+ * current revision; pass it back as `sinceRevision` on the next call.
6886
+ *
6887
+ * `added` carries entries created after `sinceRevision`. `changed`
6888
+ * carries entries that existed at `sinceRevision` but have transitioned
6889
+ * status (resolved, expired) since. `removed` carries the
6890
+ * aggregator_ids of entries pruned after `sinceRevision`.
6891
+ *
6892
+ * Tombstone caveat: removal tombstones live in-process memory. Server
6893
+ * restart clears them. Mobile clients reconnecting after the server
6894
+ * restarted MUST re-bootstrap from `list()` rather than rely on the
6895
+ * sync delta. The v1.4 mobile companion build wires the bootstrap-on-
6896
+ * reconnect flow; v1.3 documents the constraint.
6897
+ */
6898
+ interface ApprovalAggregatorSyncDelta {
6899
+ revision: number;
6900
+ added: AggregatedApproval[];
6901
+ changed: AggregatedApproval[];
6902
+ removed: string[];
6903
+ }
6393
6904
  /**
6394
6905
  * Constructor dependencies. `pendingTtlMs` and `maxListLimit` default to
6395
6906
  * coordinator-CTO defaults; tests pass overrides for deterministic timing.
@@ -6464,12 +6975,63 @@ declare class ApprovalAggregator {
6464
6975
  private hydrated;
6465
6976
  /** Active SSE listeners. */
6466
6977
  private readonly listeners;
6978
+ /**
6979
+ * Monotonic revision counter, bumped on every mutation (ingest of new
6980
+ * entry, resolve, expire, delete). Hydrated from max(last_modified_revision)
6981
+ * across persisted entries on first read; in-memory after that. v1.3
6982
+ * Upsilon-4.
6983
+ */
6984
+ private currentRevision;
6985
+ /**
6986
+ * Removal tombstones: aggregator_id -> revision at removal. Used by the
6987
+ * sync API to surface "removed" entries to mobile consumers between
6988
+ * polls. In-memory only; server restart clears tombstones (mobile
6989
+ * bootstraps via `list()` on reconnect). v1.3 Upsilon-4.
6990
+ */
6991
+ private readonly removedTombstones;
6467
6992
  constructor(deps: ApprovalAggregatorDeps);
6468
6993
  /**
6469
6994
  * Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
6470
6995
  * use this to forward aggregator emissions to the dashboard.
6471
6996
  */
6472
6997
  onEvent(listener: (event: ApprovalAggregatorEmit) => void): ApprovalAggregatorUnsubscribe;
6998
+ /**
6999
+ * Current aggregator revision. v1.3 Upsilon-4. Mobile companions
7000
+ * poll the lightweight `/revision` route to detect that something
7001
+ * changed before fetching a full sync delta.
7002
+ */
7003
+ getRevision(): Promise<number>;
7004
+ /**
7005
+ * Compute a delta since `sinceRevision`. v1.3 Upsilon-4. Mobile
7006
+ * clients poll this for cheap state-sync. Behavior:
7007
+ * - `added`: entries whose `created_at_revision > sinceRevision`.
7008
+ * - `changed`: entries that existed at `sinceRevision` but had a
7009
+ * status transition (resolve, expire) since.
7010
+ * - `removed`: aggregator_ids deleted after `sinceRevision`.
7011
+ * - `revision`: current aggregator revision; pass this back as
7012
+ * `sinceRevision` on the next call.
7013
+ *
7014
+ * `limit` caps the total count returned across all three lists,
7015
+ * prioritized as added -> changed -> removed (newer-state first).
7016
+ * When more changes exist than fit, the next call with the returned
7017
+ * revision will pick up the rest because each entry's
7018
+ * last_modified_revision is unchanged by truncation.
7019
+ */
7020
+ getSync(opts?: {
7021
+ sinceRevision?: number;
7022
+ limit?: number;
7023
+ }): Promise<ApprovalAggregatorSyncDelta>;
7024
+ /**
7025
+ * Delete an entry. Drops the in-memory record, the persisted bundle,
7026
+ * and the at-rest payload (if a payload store is wired). Records a
7027
+ * tombstone with the new revision so sync-API consumers see a
7028
+ * `removed` delta. Returns true when an entry was deleted, false on
7029
+ * unknown id. v1.3 Upsilon-4. Reserved for v1.4+ retention housekeeping;
7030
+ * Upsilon-4 ships the surface so mobile sync-API tests can exercise the
7031
+ * removal path.
7032
+ */
7033
+ deleteEntry(aggregatorId: string): Promise<boolean>;
7034
+ private nextRevision;
6473
7035
  /**
6474
7036
  * Ingest a gate event. Returns the aggregator entry on first sight,
6475
7037
  * `null` when deduped. Resolution events update the existing record;
@@ -6567,6 +7129,385 @@ declare class ApprovalAggregator {
6567
7129
  private hydrate;
6568
7130
  }
6569
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
+
6570
7511
  /**
6571
7512
  * Sanctuary MCP Server — Principal Dashboard
6572
7513
  *
@@ -6664,6 +7605,13 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
6664
7605
  * the operator-facing query / decision surface.
6665
7606
  */
6666
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;
6667
7615
  constructor(config: DashboardConfig);
6668
7616
  /**
6669
7617
  * Inject dependencies after construction.
@@ -6701,11 +7649,23 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
6701
7649
  * tests + during shutdown).
6702
7650
  */
6703
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;
6704
7658
  /**
6705
7659
  * v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
6706
7660
  * before the legacy approval route table. Returns true when served.
6707
7661
  */
6708
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;
6709
7669
  /**
6710
7670
  * v1.1 dispatch entry point. Called from `handleRequest` before the
6711
7671
  * legacy route table. Returns true when the request was served by v1.1