@sanctuary-framework/mcp-server 1.2.5 → 1.2.7

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
@@ -5315,6 +5315,212 @@ 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
+
5464
+ /**
5465
+ * Closed enum of categories the router may classify a query into. Adding
5466
+ * a category requires updating: this enum, the keyword table, the
5467
+ * fetcher interface, every wiring fetcher, and the test surface.
5468
+ */
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
+ }
5495
+ /**
5496
+ * Caller-supplied data sources. Each fetcher returns plain text the
5497
+ * router stitches into the rendered section. Returning the empty string
5498
+ * (or whitespace only) is treated as "nothing to fold for this category"
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.
5506
+ */
5507
+ interface ContextFetchers {
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>;
5516
+ }
5517
+ /**
5518
+ * Auxiliary classifier the caller may supply for the LLM-assist fallback
5519
+ * path. Invoked only when keyword classification returns zero matches
5520
+ * AND the query is non-trivial (>= 8 chars, not a clear greeting).
5521
+ */
5522
+ type LlmAssistClassifier = (query: string, categories: readonly ContextCategory[]) => Promise<ContextCategory | "none">;
5523
+
5318
5524
  /**
5319
5525
  * Sanctuary MCP Server — Operator Chat Service
5320
5526
  *
@@ -5461,6 +5667,50 @@ interface OperatorChatServiceDeps {
5461
5667
  * implementation run.
5462
5668
  */
5463
5669
  conciergeClock?: () => number;
5670
+ /**
5671
+ * WP-V1.3-9 Tau-3: caller-supplied fetchers for dynamic context
5672
+ * injection. When wired alongside the substrate selector, every
5673
+ * `sendConcierge` round-trip routes the operator's query through the
5674
+ * concierge-context-router and folds matching live data into the
5675
+ * substrate prompt between the static Sanctuary reference and the
5676
+ * prior-turns fold. Omit to disable dynamic context (the static
5677
+ * reference + prior-turns paths keep working unchanged).
5678
+ */
5679
+ conciergeContextFetchers?: ContextFetchers;
5680
+ /**
5681
+ * WP-V1.3-9 Tau-3: optional LLM-assist classifier for queries that
5682
+ * fail keyword classification. Coordinator-CTO bake: when wired, the
5683
+ * service routes the auxiliary classifier call through the substrate
5684
+ * selector at the same `concierge` surface, so the auxiliary call
5685
+ * shares the operator's substrate choice and never opens a new
5686
+ * outbound surface.
5687
+ */
5688
+ conciergeContextLlmAssist?: LlmAssistClassifier;
5689
+ /**
5690
+ * WP-V1.3-9 Tau-3: rough token budget for the dynamic-context fold.
5691
+ * Defaults to `DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET` (2000).
5692
+ */
5693
+ conciergeDynamicContextBudget?: number;
5694
+ /**
5695
+ * WP-V1.3-9 Tau-4: read-only registry view consumed by the operator-
5696
+ * query grammar to extract agent-name parameters. Wired from
5697
+ * `HubAgentRegistrySource.list()` at the hub-service layer; absent
5698
+ * services skip agent-name extraction (the grammar still resolves
5699
+ * time ranges and event types). The registry is read at every
5700
+ * `sendConcierge` call so newly-wrapped agents are visible without
5701
+ * service reconstruction.
5702
+ */
5703
+ conciergeAgentRegistry?: AgentRegistryView;
5704
+ /**
5705
+ * WP-V1.3-9 Tau-4: optional LLM-assist completion for low-confidence
5706
+ * grammar parses. When wired alongside the substrate selector, the
5707
+ * service routes the call through the substrate selector at the
5708
+ * `concierge` surface, so the grammar fallback shares the operator's
5709
+ * substrate choice and never opens a new outbound surface. Distinct
5710
+ * from `conciergeContextLlmAssist`, which classifies queries into
5711
+ * fetcher categories; this hook completes structured grammar fields.
5712
+ */
5713
+ conciergeGrammarLlmAssist?: LlmAssistGrammarCompletion;
5464
5714
  }
5465
5715
  declare class OperatorChatService {
5466
5716
  private store;
@@ -5476,6 +5726,11 @@ declare class OperatorChatService {
5476
5726
  private historyTokenBudget;
5477
5727
  private sessionTtlMs;
5478
5728
  private clock;
5729
+ private contextFetchers?;
5730
+ private contextLlmAssist?;
5731
+ private dynamicContextBudget;
5732
+ private agentRegistry?;
5733
+ private grammarLlmAssist?;
5479
5734
  /**
5480
5735
  * In-memory thread_id assigned to the active concierge session.
5481
5736
  * The first sendConcierge call after construction allocates a fresh
@@ -5556,10 +5811,13 @@ declare class OperatorChatService {
5556
5811
  * ## Sanctuary reference
5557
5812
  * <static domain reference block>
5558
5813
  *
5814
+ * ## Live fortress context ← WP-V1.3-9 Tau-3, when present
5815
+ * ### <Category>
5816
+ * <fetcher payload>
5817
+ *
5559
5818
  * ## Prior conversation ← WP-V1.3-9 Tau-2, when present
5560
5819
  * OPERATOR: ...
5561
5820
  * CONCIERGE: ...
5562
- * ---
5563
5821
  *
5564
5822
  * ## Recent activity
5565
5823
  * <recentActivity output>
@@ -5579,6 +5837,35 @@ declare class OperatorChatService {
5579
5837
  * serialization is the canonical path for v1.3.
5580
5838
  */
5581
5839
  private assembleConciergeContext;
5840
+ /**
5841
+ * Run the WP-V1.3-9 Tau-3 dynamic-context fold for a single round-
5842
+ * trip. Fail-soft on every axis: missing fetchers short-circuit to
5843
+ * an empty fold, fetcher failures emit a per-category audit event
5844
+ * and are omitted from the rendered section, an LLM-assist failure
5845
+ * proceeds with no fold. Returns the rendered section + the list of
5846
+ * categories whose data made it into the section (used for the
5847
+ * round-trip audit emission).
5848
+ *
5849
+ * Tau-4: receives the pre-parsed `ParsedQuery` and forwards it as the
5850
+ * `parsed` opt to `foldContext`, so fetchers see the structured
5851
+ * `FetcherHints` derived from it.
5852
+ */
5853
+ private runDynamicContextFold;
5854
+ /**
5855
+ * WP-V1.3-9 Tau-4: parse the (PII-filtered) operator query into a
5856
+ * `ParsedQuery`. Routes through the LLM-assist completion hook when
5857
+ * configured and the rule-based parse is below
5858
+ * `LLM_ASSIST_THRESHOLD`. Always returns a parse object (never
5859
+ * throws) so the audit emission can carry the result unconditionally.
5860
+ */
5861
+ private runGrammarParse;
5862
+ /**
5863
+ * Emit the WP-V1.3-9 Tau-3 fetcher-failure audit event. Pulled out
5864
+ * of the fold path so the dynamic-context handler stays readable.
5865
+ * Emits with `result: "failure"` since the named category dropped
5866
+ * from the rendered section for this round-trip.
5867
+ */
5868
+ private emitContextFetcherFailed;
5582
5869
  /**
5583
5870
  * Render the prior-conversation section with token-budget enforcement
5584
5871
  * (WP-V1.3-9 Tau-2). Drops oldest turns first when the rendered
@@ -6069,6 +6356,87 @@ interface V11Bindings {
6069
6356
  operatorChatService?: OperatorChatService;
6070
6357
  }
6071
6358
 
6359
+ /**
6360
+ * Sanctuary v1.3 WP-V1.3-10 Cross-Harness Approval Inbox Upsilon-3
6361
+ *
6362
+ * At-rest encrypted persistence for full request payloads tied to
6363
+ * AggregatedApproval entries. Sibling store to ApprovalAggregator: the
6364
+ * aggregator's own entry record (status, provenance, hashes) is persisted
6365
+ * by the aggregator class under `_approval_aggregator`; this store holds
6366
+ * the original request payload separately under
6367
+ * `_approval_aggregator_payloads` so the operator can replay the full
6368
+ * payload after a server restart.
6369
+ *
6370
+ * Storage layout:
6371
+ * namespace: `_approval_aggregator_payloads`
6372
+ * key: `payload.{aggregator_id}` (one record per approval)
6373
+ * payload: AES-256-GCM ciphertext of the JSON-serialised bundle.
6374
+ * key: `l2-approval-aggregator-payload-v1` HKDF subkey of fortress
6375
+ * master.
6376
+ * AAD: UTF-8 bytes of `aggregator_id`. Swapping records across
6377
+ * aggregator_ids breaks the auth tag. Castle-walking
6378
+ * discipline: the encryption boundary holds even against
6379
+ * on-disk shuffle.
6380
+ *
6381
+ * Retention:
6382
+ * Each bundle carries an ISO-8601 `retention_until`. `pruneExpired()`
6383
+ * iterates payloads and drops entries whose retention_until is in the
6384
+ * past. Default retention is 30 days, mirroring the audit-log retention
6385
+ * envelope. Operator override flows through the aggregator's deps.
6386
+ *
6387
+ * Multi-fortress isolation:
6388
+ * The HKDF subkey is derived from the fortress master key. Two
6389
+ * fortresses never produce identical encryption keys for identical
6390
+ * aggregator_ids. Isolation is enforced cryptographically; the AAD
6391
+ * binds the ciphertext to a specific aggregator_id within a fortress.
6392
+ */
6393
+
6394
+ interface AggregatorPayloadStoreOptions {
6395
+ /** Storage backend that persists the encrypted bundles. */
6396
+ storage: StorageBackend;
6397
+ /** 32-byte fortress master key. */
6398
+ masterKey: Uint8Array;
6399
+ /** Stable fortress id stamped on every bundle for audit clarity. */
6400
+ fortressId: string;
6401
+ /** Operator-tunable retention window. Default 30 days. */
6402
+ retentionDays?: number;
6403
+ }
6404
+ /**
6405
+ * Encrypted, AAD-bound, retention-aware payload persistence for the
6406
+ * cross-harness approval inbox.
6407
+ */
6408
+ declare class AggregatorPayloadStore {
6409
+ private readonly storage;
6410
+ private readonly encryptionKey;
6411
+ private readonly fortressId;
6412
+ private readonly retentionDays;
6413
+ constructor(opts: AggregatorPayloadStoreOptions);
6414
+ /**
6415
+ * Persist `payload` under the given aggregator_id. Idempotent; calling
6416
+ * twice with the same id rewrites the bundle (retention_until is
6417
+ * recomputed). Returns the bundle's retention_until ISO-8601 timestamp
6418
+ * so callers can log it.
6419
+ */
6420
+ savePayload(aggregatorId: string, payload: unknown): Promise<string>;
6421
+ /**
6422
+ * Read the persisted payload for the aggregator_id. Returns null if no
6423
+ * bundle exists, the bundle is corrupted, or AAD binding fails.
6424
+ */
6425
+ loadPayload(aggregatorId: string): Promise<unknown>;
6426
+ /**
6427
+ * Delete the persisted payload. Returns true when a bundle was removed,
6428
+ * false when none existed.
6429
+ */
6430
+ deletePayload(aggregatorId: string): Promise<boolean>;
6431
+ /**
6432
+ * Drop expired payload bundles. Returns the count of bundles pruned.
6433
+ * Caller wires this into the cocoon-unlock initialization path.
6434
+ */
6435
+ pruneExpired(now?: Date): Promise<{
6436
+ pruned: number;
6437
+ }>;
6438
+ }
6439
+
6072
6440
  /**
6073
6441
  * Sanctuary v1.3 WP-V1.3-10 Cross-Harness Approval Inbox Upsilon-1
6074
6442
  *
@@ -6106,6 +6474,19 @@ interface V11Bindings {
6106
6474
  * - `expired`: pending past TTL on a `list()` poll without a resolution.
6107
6475
  */
6108
6476
  type AggregatedApprovalStatus = "pending" | "approved" | "denied" | "timeout" | "expired";
6477
+ /**
6478
+ * One step in the Castle Architecture enforcement chain that led to this
6479
+ * approval. Layers are: l1 (Castle Wall, OS-level egress), l2 (Sentinel +
6480
+ * cooperative MCP gate), l3 (selective disclosure), l4 (reputation).
6481
+ * v1.3 Upsilon-3 ships the schema; default resolver populates a single
6482
+ * `l2` entry. Future Castle Wall wiring will extend the chain when a
6483
+ * payload's egress was first observed by the kernel filter.
6484
+ */
6485
+ interface EnforcementLayerEvent {
6486
+ layer: "l1" | "l2" | "l3" | "l4";
6487
+ event: string;
6488
+ timestamp: string;
6489
+ }
6109
6490
  /**
6110
6491
  * Normalized record the aggregator stores per approval. Field set is
6111
6492
  * additive-stable; new fields go behind `?` so existing dashboards keep
@@ -6150,6 +6531,31 @@ interface AggregatedApproval {
6150
6531
  * (the cross-link signal lives on the aggregator side only).
6151
6532
  */
6152
6533
  hub_inbox_item_id?: string;
6534
+ /**
6535
+ * Castle Architecture enforcement-layer chain that led to this
6536
+ * approval. Populated by the optional `resolveEnforcementChain` deps
6537
+ * hook; default returns a single `l2` step (cooperative MCP gate
6538
+ * fired). Persisted with the entry so the operator-replay surface can
6539
+ * render the enforcement context after a server restart. v1.3
6540
+ * Upsilon-3.
6541
+ */
6542
+ enforcement_chain?: EnforcementLayerEvent[];
6543
+ /**
6544
+ * Aggregator revision number stamped at entry creation. Stable across
6545
+ * status transitions. Used by the v1.4 mobile companion sync API to
6546
+ * separate "added" from "changed" deltas (created_at_revision >
6547
+ * sinceRevision means added; otherwise it is a status change).
6548
+ * v1.3 Upsilon-4.
6549
+ */
6550
+ created_at_revision?: number;
6551
+ /**
6552
+ * Aggregator revision number stamped at the last mutation (creation,
6553
+ * resolution, expiration). Sync-API consumers compare against the
6554
+ * revision they last saw to compute the delta. Monotonically
6555
+ * increasing per fortress; gaps are normal (one bump per mutation).
6556
+ * v1.3 Upsilon-4.
6557
+ */
6558
+ last_modified_revision?: number;
6153
6559
  }
6154
6560
  /**
6155
6561
  * Source-context resolver. Called once per ingest to map gate context to
@@ -6206,11 +6612,35 @@ type ApprovalAggregatorUnsubscribe = () => void;
6206
6612
  interface ApprovalAggregatorEmit {
6207
6613
  /**
6208
6614
  * `aggregated` on first ingest, `resolved` on a status leaving pending,
6209
- * `deduped` when an ingest dropped because the same dedup key was seen.
6615
+ * `deduped` when an ingest dropped because the same dedup key was seen,
6616
+ * `removed` when an entry was pruned (v1.3 Upsilon-4).
6210
6617
  */
6211
- type: "aggregated" | "resolved" | "deduped";
6618
+ type: "aggregated" | "resolved" | "deduped" | "removed";
6212
6619
  entry: AggregatedApproval;
6213
6620
  }
6621
+ /**
6622
+ * Sync-API delta returned by `getSync()`. v1.3 Upsilon-4. Mobile
6623
+ * companions poll this for cheap state-sync without re-fetching the
6624
+ * full inbox. The `revision` field on the response is the aggregator's
6625
+ * current revision; pass it back as `sinceRevision` on the next call.
6626
+ *
6627
+ * `added` carries entries created after `sinceRevision`. `changed`
6628
+ * carries entries that existed at `sinceRevision` but have transitioned
6629
+ * status (resolved, expired) since. `removed` carries the
6630
+ * aggregator_ids of entries pruned after `sinceRevision`.
6631
+ *
6632
+ * Tombstone caveat: removal tombstones live in-process memory. Server
6633
+ * restart clears them. Mobile clients reconnecting after the server
6634
+ * restarted MUST re-bootstrap from `list()` rather than rely on the
6635
+ * sync delta. The v1.4 mobile companion build wires the bootstrap-on-
6636
+ * reconnect flow; v1.3 documents the constraint.
6637
+ */
6638
+ interface ApprovalAggregatorSyncDelta {
6639
+ revision: number;
6640
+ added: AggregatedApproval[];
6641
+ changed: AggregatedApproval[];
6642
+ removed: string[];
6643
+ }
6214
6644
  /**
6215
6645
  * Constructor dependencies. `pendingTtlMs` and `maxListLimit` default to
6216
6646
  * coordinator-CTO defaults; tests pass overrides for deterministic timing.
@@ -6238,6 +6668,23 @@ interface ApprovalAggregatorDeps {
6238
6668
  * hub inbox store.
6239
6669
  */
6240
6670
  resolveHubInboxItemId?: (event: ApprovalGateEvent) => string | undefined;
6671
+ /**
6672
+ * Optional at-rest payload store. When provided, the aggregator
6673
+ * persists each request payload via `savePayload` on ingest, and
6674
+ * rehydrates payloads on `getFullPayload` if the in-memory map lost
6675
+ * them (e.g. after a server restart). Upsilon-3 surface; absent in
6676
+ * Upsilon-1 / Upsilon-2 deployments, where payloads remain in-memory
6677
+ * only.
6678
+ */
6679
+ payloadStore?: AggregatorPayloadStore;
6680
+ /**
6681
+ * Optional resolver for the Castle Architecture enforcement chain
6682
+ * leading to this approval. Default returns a single `l2` step
6683
+ * (cooperative MCP gate fired). When the Castle Wall (Layer 1) ships,
6684
+ * its kernel-filter observer can populate richer chains by passing a
6685
+ * resolver here.
6686
+ */
6687
+ resolveEnforcementChain?: (event: ApprovalGateEvent) => EnforcementLayerEvent[];
6241
6688
  }
6242
6689
  /**
6243
6690
  * Aggregator state. The map is hydrated lazily from the encrypted
@@ -6254,6 +6701,8 @@ declare class ApprovalAggregator {
6254
6701
  private readonly now;
6255
6702
  private readonly resolveSourceContext;
6256
6703
  private readonly resolveHubInboxItemId;
6704
+ private readonly payloadStore;
6705
+ private readonly resolveEnforcementChain;
6257
6706
  /** Cached entries by `aggregator_id`. */
6258
6707
  private readonly entries;
6259
6708
  /** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
@@ -6266,12 +6715,63 @@ declare class ApprovalAggregator {
6266
6715
  private hydrated;
6267
6716
  /** Active SSE listeners. */
6268
6717
  private readonly listeners;
6718
+ /**
6719
+ * Monotonic revision counter, bumped on every mutation (ingest of new
6720
+ * entry, resolve, expire, delete). Hydrated from max(last_modified_revision)
6721
+ * across persisted entries on first read; in-memory after that. v1.3
6722
+ * Upsilon-4.
6723
+ */
6724
+ private currentRevision;
6725
+ /**
6726
+ * Removal tombstones: aggregator_id -> revision at removal. Used by the
6727
+ * sync API to surface "removed" entries to mobile consumers between
6728
+ * polls. In-memory only; server restart clears tombstones (mobile
6729
+ * bootstraps via `list()` on reconnect). v1.3 Upsilon-4.
6730
+ */
6731
+ private readonly removedTombstones;
6269
6732
  constructor(deps: ApprovalAggregatorDeps);
6270
6733
  /**
6271
6734
  * Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
6272
6735
  * use this to forward aggregator emissions to the dashboard.
6273
6736
  */
6274
6737
  onEvent(listener: (event: ApprovalAggregatorEmit) => void): ApprovalAggregatorUnsubscribe;
6738
+ /**
6739
+ * Current aggregator revision. v1.3 Upsilon-4. Mobile companions
6740
+ * poll the lightweight `/revision` route to detect that something
6741
+ * changed before fetching a full sync delta.
6742
+ */
6743
+ getRevision(): Promise<number>;
6744
+ /**
6745
+ * Compute a delta since `sinceRevision`. v1.3 Upsilon-4. Mobile
6746
+ * clients poll this for cheap state-sync. Behavior:
6747
+ * - `added`: entries whose `created_at_revision > sinceRevision`.
6748
+ * - `changed`: entries that existed at `sinceRevision` but had a
6749
+ * status transition (resolve, expire) since.
6750
+ * - `removed`: aggregator_ids deleted after `sinceRevision`.
6751
+ * - `revision`: current aggregator revision; pass this back as
6752
+ * `sinceRevision` on the next call.
6753
+ *
6754
+ * `limit` caps the total count returned across all three lists,
6755
+ * prioritized as added -> changed -> removed (newer-state first).
6756
+ * When more changes exist than fit, the next call with the returned
6757
+ * revision will pick up the rest because each entry's
6758
+ * last_modified_revision is unchanged by truncation.
6759
+ */
6760
+ getSync(opts?: {
6761
+ sinceRevision?: number;
6762
+ limit?: number;
6763
+ }): Promise<ApprovalAggregatorSyncDelta>;
6764
+ /**
6765
+ * Delete an entry. Drops the in-memory record, the persisted bundle,
6766
+ * and the at-rest payload (if a payload store is wired). Records a
6767
+ * tombstone with the new revision so sync-API consumers see a
6768
+ * `removed` delta. Returns true when an entry was deleted, false on
6769
+ * unknown id. v1.3 Upsilon-4. Reserved for v1.4+ retention housekeeping;
6770
+ * Upsilon-4 ships the surface so mobile sync-API tests can exercise the
6771
+ * removal path.
6772
+ */
6773
+ deleteEntry(aggregatorId: string): Promise<boolean>;
6774
+ private nextRevision;
6275
6775
  /**
6276
6776
  * Ingest a gate event. Returns the aggregator entry on first sight,
6277
6777
  * `null` when deduped. Resolution events update the existing record;
@@ -6292,10 +6792,48 @@ declare class ApprovalAggregator {
6292
6792
  }): Promise<AggregatedApproval[]>;
6293
6793
  /**
6294
6794
  * Return the original (unhashed) request payload for the entry. Returns
6295
- * `null` when the entry is unknown or the payload was evicted (e.g. the
6296
- * process restarted; payloads are in-memory only at v1.3 Upsilon-1).
6795
+ * `null` when the entry is unknown. When the in-memory payload map has
6796
+ * been evicted (e.g. after a server restart) and a `payloadStore` was
6797
+ * provided, the at-rest bundle is decrypted and the in-memory map is
6798
+ * refilled. Audit emission lives on the `*WithAudit` variant; this base
6799
+ * accessor is silent so internal callers can read without polluting the
6800
+ * audit trail.
6297
6801
  */
6298
6802
  getFullPayload(aggregatorId: string): Promise<unknown>;
6803
+ /**
6804
+ * Return the entry record for the given id, or null when unknown.
6805
+ * Idempotent. v1.3 Upsilon-3.
6806
+ */
6807
+ getEntry(aggregatorId: string): Promise<AggregatedApproval | null>;
6808
+ /**
6809
+ * Audited variant of `getFullPayload`. Emits the
6810
+ * `cross_harness_approval_payload_decrypted` audit event before
6811
+ * returning. Used by the operator-facing /payload replay route.
6812
+ * v1.3 Upsilon-3.
6813
+ */
6814
+ getFullPayloadWithAudit(aggregatorId: string, operatorId: string): Promise<unknown>;
6815
+ /**
6816
+ * Return the audit-log entries that led to and surround this approval.
6817
+ * Best-effort matching: aggregator-side emissions (AGGREGATED, RESOLVED,
6818
+ * DEDUPED, replay events) all carry `details.aggregator_id` and link
6819
+ * directly. Gate-side emissions (`gate_*:operation`) do not carry the
6820
+ * aggregator id at v1.3, so they are matched via timestamp window
6821
+ * (entry.created_at to entry.resolved_at + 1s, or expires_at + 1s while
6822
+ * pending) and operation suffix. Emits AUDIT_TRAIL_VIEWED on call.
6823
+ * v1.3 Upsilon-3.
6824
+ */
6825
+ getAuditTrail(aggregatorId: string, operatorId: string): Promise<AuditEntry[]>;
6826
+ /**
6827
+ * List historical (resolved) approvals. Excludes pending entries by
6828
+ * design: `list()` is the pending-inbox surface and `getHistory()` is
6829
+ * the resolved-replay surface. Emits REPLAYED on each call. v1.3
6830
+ * Upsilon-3.
6831
+ */
6832
+ getHistory(opts: {
6833
+ status?: AggregatedApprovalStatus;
6834
+ sinceTs?: string;
6835
+ limit?: number;
6836
+ } | undefined, operatorId: string): Promise<AggregatedApproval[]>;
6299
6837
  /**
6300
6838
  * Resolve an entry. Used by both:
6301
6839
  * 1. The gate wire-up on channel-decision return.