@sanctuary-framework/mcp-server 1.2.3 → 1.2.4

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
@@ -263,6 +263,13 @@ interface PrincipalPolicy {
263
263
  tier3_always_allow: string[];
264
264
  /** How approval requests reach the human */
265
265
  approval_channel: ApprovalChannelConfig;
266
+ /**
267
+ * WP-V1.3-9 Tau-1: operator-tunable retention window (days) for the
268
+ * concierge memory store. Per-turn `retention_until` is stamped at
269
+ * append time; cocoon-unlock pruning drops expired turns. Default 30
270
+ * days when absent; values <= 0 fall back to the default.
271
+ */
272
+ concierge_memory_retention_days?: number;
266
273
  }
267
274
  /** Approval request sent to the human */
268
275
  interface ApprovalRequest {
@@ -276,7 +283,7 @@ interface ApprovalRequest {
276
283
  interface ApprovalResponse {
277
284
  decision: "approve" | "deny";
278
285
  decided_at: string;
279
- decided_by: "human" | "timeout" | "auto" | "stderr:non-interactive";
286
+ decided_by: "human" | "timeout" | "auto" | "stderr:non-interactive" | "channel_failure";
280
287
  }
281
288
  /** Result of the approval gate evaluation */
282
289
  interface GateResult {
@@ -658,6 +665,31 @@ type InjectionAlertCallback = (alert: {
658
665
  }) => void;
659
666
  /** Resolver for proxy tool tiers — provided by the ProxyRouter */
660
667
  type ProxyTierResolver = (toolName: string) => (1 | 2 | 3) | null;
668
+ /**
669
+ * Approval-lifecycle callback. Wired in v1.3 Upsilon-1 by the Cross-
670
+ * Harness Approval Inbox aggregator. The gate fires this callback before
671
+ * `channel.requestApproval()` (phase = "requested") and after the channel
672
+ * resolves or fails (phase = "resolved"). The callback is fire-and-forget;
673
+ * exceptions are swallowed so a broken aggregator never blocks the gate.
674
+ *
675
+ * The callback shape is import-free here so the principal-policy module
676
+ * keeps no dependency on the aggregator (the aggregator imports the gate,
677
+ * not vice versa).
678
+ */
679
+ type ApprovalEventCallback = (event: {
680
+ phase: "requested" | "resolved";
681
+ operation: string;
682
+ tier: 1 | 2;
683
+ reason: string;
684
+ context: Record<string, unknown>;
685
+ request_timestamp: string;
686
+ resolution?: {
687
+ decision: "approve" | "deny";
688
+ decided_at: string;
689
+ decided_by: string;
690
+ };
691
+ correlation_id: string;
692
+ }) => void;
661
693
  declare class ApprovalGate {
662
694
  private policy;
663
695
  private baseline;
@@ -665,8 +697,16 @@ declare class ApprovalGate {
665
697
  private auditLog;
666
698
  private injectionDetector;
667
699
  private onInjectionAlert?;
700
+ private onApprovalEvent?;
668
701
  private proxyTierResolver?;
669
- constructor(policy: PrincipalPolicy, baseline: BaselineTracker, channel: ApprovalChannel, auditLog: AuditLog, injectionDetector?: InjectionDetector, onInjectionAlert?: InjectionAlertCallback);
702
+ constructor(policy: PrincipalPolicy, baseline: BaselineTracker, channel: ApprovalChannel, auditLog: AuditLog, injectionDetector?: InjectionDetector, onInjectionAlert?: InjectionAlertCallback, onApprovalEvent?: ApprovalEventCallback);
703
+ /**
704
+ * Set the approval-event callback after construction. Used by the
705
+ * Upsilon-1 wire-up when the aggregator is constructed alongside the
706
+ * gate. The aggregator subscribes through this setter rather than the
707
+ * constructor so existing call sites continue to work unchanged.
708
+ */
709
+ setApprovalEventCallback(cb: ApprovalEventCallback | undefined): void;
670
710
  /**
671
711
  * Set the proxy tier resolver. Called after the proxy router is initialized.
672
712
  */
@@ -685,6 +725,13 @@ declare class ApprovalGate {
685
725
  private detectAnomaly;
686
726
  /**
687
727
  * Request approval from the human principal.
728
+ *
729
+ * Fail-closed contract (full-sweep #49): if the channel throws (network
730
+ * down, callback unreachable, dashboard SSE peer dropped, webhook DNS
731
+ * failure, etc.), the gate denies the operation and audit-logs the cause.
732
+ * Channel-internal timeouts already resolve with decision: "deny" per
733
+ * SEC-002; this catch covers the remaining "channel raised" path so an
734
+ * unhandled rejection cannot turn into an indeterminate state at the gate.
688
735
  */
689
736
  private requestApproval;
690
737
  /**
@@ -3537,7 +3584,7 @@ interface ExitBundleVerifierResult {
3537
3584
  * Coarse failure-class enum, when `passed` is false. Distinct from a free-text
3538
3585
  * error message so import commands can branch.
3539
3586
  */
3540
- failure_class?: "manifest_signature_invalid" | "manifest_unknown_version" | "manifest_signature_scheme_invalid" | "artifact_hash_mismatch" | "artifact_missing" | "artifact_size_mismatch" | "aggregate_hash_mismatch" | "artifact_path_unsafe" | "artifact_path_duplicate" | "artifact_path_escapes_root" | "archive_contains_symlink" | "private_material_present" | "other";
3587
+ failure_class?: "manifest_signature_invalid" | "manifest_unknown_version" | "manifest_signature_scheme_invalid" | "artifact_hash_mismatch" | "artifact_missing" | "artifact_size_mismatch" | "aggregate_hash_mismatch" | "artifact_path_unsafe" | "artifact_path_duplicate" | "artifact_path_escapes_root" | "archive_contains_symlink" | "private_material_present" | "identity_signature_invalid" | "reputation_bundle_signature_invalid" | "reputation_attestation_signature_invalid" | "reputation_unverifiable_attestations" | "other";
3541
3588
  }
3542
3589
 
3543
3590
  /**
@@ -4104,21 +4151,35 @@ interface ExitCommandArgs {
4104
4151
  declare function runExitCommand(args: ExitCommandArgs): Promise<number>;
4105
4152
 
4106
4153
  /**
4107
- * Sanctuary MCP Server Principal Policy Loader
4154
+ * Sanctuary MCP Server -- Principal Policy Loader
4108
4155
  *
4109
4156
  * Loads the Principal Policy from a YAML file at server startup.
4110
- * The policy is immutable at runtime no MCP tool can modify it.
4157
+ * The policy is immutable at runtime -- no MCP tool can modify it.
4111
4158
  *
4112
4159
  * Security invariant:
4113
4160
  * - The policy is loaded ONCE at startup and frozen.
4114
4161
  * - No code path exists to modify the policy during a session.
4115
4162
  * - If no policy file exists, a sensible default is generated and saved.
4163
+ * - If the policy file exists but is malformed, the server refuses to start
4164
+ * rather than silently substituting a default (operator intent preservation).
4116
4165
  */
4117
4166
 
4167
+ /**
4168
+ * Thrown when a principal-policy.yaml file exists on disk but cannot be
4169
+ * parsed or validated. Sanctuary refuses to substitute a default policy
4170
+ * when an existing file is present, to avoid silently overriding operator
4171
+ * intent.
4172
+ */
4173
+ declare class MalformedPrincipalPolicyError extends Error {
4174
+ readonly policyPath: string;
4175
+ readonly reason: string;
4176
+ constructor(policyPath: string, reason: string);
4177
+ }
4118
4178
  /**
4119
4179
  * Load the Principal Policy from disk.
4120
4180
  * If no policy file exists, generate the default and save it.
4121
- * The returned policy is frozen immutable at runtime.
4181
+ * If the file exists but is malformed, throw MalformedPrincipalPolicyError.
4182
+ * The returned policy is frozen -- immutable at runtime.
4122
4183
  */
4123
4184
  declare function loadPrincipalPolicy(storagePath: string): Promise<PrincipalPolicy>;
4124
4185
 
@@ -5045,6 +5106,138 @@ declare class OperatorChatStore {
5045
5106
  deleteThread(surface: OperatorChatSurface, threadKey: string): Promise<void>;
5046
5107
  }
5047
5108
 
5109
+ /**
5110
+ * Sanctuary MCP Server — Concierge Memory Store (WP-V1.3-9 Tau-1)
5111
+ *
5112
+ * Per-fortress, encrypted-at-rest, cocoon-bound persistence for
5113
+ * concierge conversation turns. Distinct from the v1.2 OperatorChatStore
5114
+ * (single fortress-scoped thread, capped FIFO, no retention) — this
5115
+ * store is the foundation for v1.3 conversational sovereignty depth:
5116
+ * multi-thread enumeration, per-turn retention, scrollable history.
5117
+ *
5118
+ * Storage layout:
5119
+ * namespace: `_chat` (existing reserved L1 namespace)
5120
+ * key: `concierge_memory.{thread_id}` (one record per thread)
5121
+ * payload: AES-256-GCM ciphertext of the JSON-serialised turns bundle.
5122
+ * key: `concierge-memory-store-v1` HKDF subkey of fortress master.
5123
+ * AAD: UTF-8 bytes of `thread_id` — swapping records across
5124
+ * threads breaks the auth tag. Castle-walking discipline:
5125
+ * the encryption boundary holds even against on-disk shuffle.
5126
+ *
5127
+ * Concurrency:
5128
+ * Per-thread async lock serializes appendTurn calls within the same
5129
+ * store instance. Cross-process locking is out of scope (single-process
5130
+ * Sanctuary server).
5131
+ *
5132
+ * Retention:
5133
+ * Each turn carries an ISO-8601 `retention_until`. `pruneExpired()`
5134
+ * iterates threads and drops turns whose retention_until is in the
5135
+ * past. Empty bundles are deleted entirely. Caller wires this into
5136
+ * the cocoon-unlock initialization path (the wiring layer fires it
5137
+ * once the store is constructed).
5138
+ *
5139
+ * Multi-fortress isolation:
5140
+ * The HKDF subkey is derived from the fortress master key. Two
5141
+ * fortresses never produce identical encryption keys for identical
5142
+ * thread_ids. The `fortress_id` field on each turn is metadata for
5143
+ * audit clarity; isolation is enforced cryptographically.
5144
+ */
5145
+
5146
+ /** One turn in a concierge thread. */
5147
+ interface ConciergeTurn {
5148
+ /** UUID of the thread this turn belongs to. */
5149
+ thread_id: string;
5150
+ /** Stable fortress id (audit metadata; isolation is keyed). */
5151
+ fortress_id: string;
5152
+ /** Monotonic turn number within thread, starting at 1. */
5153
+ turn_id: number;
5154
+ /** Sender role. */
5155
+ role: "user" | "assistant";
5156
+ /** Cleartext at runtime; AES-256-GCM at rest. */
5157
+ content: string;
5158
+ /** ISO-8601 timestamp of turn creation. */
5159
+ created_at: string;
5160
+ /** ISO-8601 timestamp after which prune drops this turn. */
5161
+ retention_until: string;
5162
+ }
5163
+ /** Operator-facing summary surfaced by `listThreads`. */
5164
+ interface ConciergeThreadSummary {
5165
+ thread_id: string;
5166
+ created_at: string;
5167
+ last_turn_at: string;
5168
+ turn_count: number;
5169
+ }
5170
+ interface ConciergeMemoryStoreOptions {
5171
+ /** Storage backend that persists the encrypted bundles. */
5172
+ storage: StorageBackend;
5173
+ /** 32-byte fortress master key. */
5174
+ masterKey: Uint8Array;
5175
+ /** Stable fortress id stamped on every turn for audit clarity. */
5176
+ fortressId: string;
5177
+ /** Operator-tunable retention window. Default 30 days. */
5178
+ retentionDays?: number;
5179
+ }
5180
+ interface ReadThreadOptions {
5181
+ /** Return only turns with turn_id strictly greater than this. */
5182
+ sinceTurnId?: number;
5183
+ /** Cap the number of turns returned (oldest-first within the bundle). */
5184
+ limit?: number;
5185
+ }
5186
+ interface ListThreadsOptions {
5187
+ /** Cap the number of summaries returned. */
5188
+ limit?: number;
5189
+ }
5190
+ /**
5191
+ * Encrypted, AAD-bound, retention-aware concierge memory persistence.
5192
+ */
5193
+ declare class ConciergeMemoryStore {
5194
+ private storage;
5195
+ private encryptionKey;
5196
+ private fortressId;
5197
+ private retentionDays;
5198
+ private locks;
5199
+ constructor(opts: ConciergeMemoryStoreOptions);
5200
+ /**
5201
+ * Append a turn to the named thread, creating the bundle if no record
5202
+ * exists. Returns the persisted turn (with assigned turn_id +
5203
+ * retention_until). Per-thread serialisation guarantees turn_id
5204
+ * monotonicity even under concurrent callers.
5205
+ */
5206
+ appendTurn(threadId: string, role: "user" | "assistant", content: string): Promise<ConciergeTurn>;
5207
+ /**
5208
+ * Read turns from a thread, oldest-first. Returns an empty array if
5209
+ * the thread does not exist or its bundle is corrupt. Does not emit
5210
+ * audit events; the caller (HTTP route handler) owns audit semantics.
5211
+ */
5212
+ readThread(threadId: string, opts?: ReadThreadOptions): Promise<ConciergeTurn[]>;
5213
+ /**
5214
+ * Enumerate concierge threads in this fortress with summary metadata.
5215
+ * Sorted newest-first by last_turn_at.
5216
+ */
5217
+ listThreads(opts?: ListThreadsOptions): Promise<ConciergeThreadSummary[]>;
5218
+ /**
5219
+ * Delete a thread's bundle. Returns true if the bundle existed and
5220
+ * was removed; false if no bundle was present. Audit emission is the
5221
+ * caller's responsibility.
5222
+ */
5223
+ deleteThread(threadId: string): Promise<boolean>;
5224
+ /**
5225
+ * Drop expired turns across all threads. Threads emptied by pruning
5226
+ * are removed entirely. Returns the count of turns pruned.
5227
+ */
5228
+ pruneExpired(now?: Date): Promise<{
5229
+ pruned: number;
5230
+ }>;
5231
+ private loadBundle;
5232
+ private saveBundle;
5233
+ /**
5234
+ * Run `task` while holding the per-thread async lock. Lock is released
5235
+ * once the task settles (success or failure). Generic helper so
5236
+ * appendTurn / deleteThread / pruneExpired share serialisation.
5237
+ */
5238
+ private withLock;
5239
+ }
5240
+
5048
5241
  /**
5049
5242
  * Sanctuary MCP Server — Operator Chat Service
5050
5243
  *
@@ -5146,6 +5339,16 @@ interface OperatorChatServiceDeps {
5146
5339
  * summary). Operator-tunable via dashboard config.
5147
5340
  */
5148
5341
  conciergeMaxTokens?: number;
5342
+ /**
5343
+ * WP-V1.3-9 Tau-1: optional foundation memory store. When wired,
5344
+ * `sendConcierge` dual-writes each operator+concierge turn pair into
5345
+ * the memory store under a session-scoped thread_id. Read-side wiring
5346
+ * (substrate-selector context-fold) lands in Tau-2.
5347
+ *
5348
+ * Omit at construction time to disable memory-side persistence; the
5349
+ * existing `OperatorChatStore` write keeps working unchanged.
5350
+ */
5351
+ conciergeMemory?: ConciergeMemoryStore;
5149
5352
  }
5150
5353
  declare class OperatorChatService {
5151
5354
  private store;
@@ -5155,6 +5358,14 @@ declare class OperatorChatService {
5155
5358
  private contextProviders?;
5156
5359
  private piiFilter?;
5157
5360
  private conciergeMaxTokens;
5361
+ private memory?;
5362
+ /**
5363
+ * In-memory thread_id assigned to the active concierge session.
5364
+ * The first sendConcierge call after construction allocates a fresh
5365
+ * UUID; subsequent calls reuse it so multi-turn coherence (Tau-2)
5366
+ * folds the prior turns into context. Cleared by `resetConciergeMemoryThread`.
5367
+ */
5368
+ private activeMemoryThreadId?;
5158
5369
  constructor(deps: OperatorChatServiceDeps);
5159
5370
  /**
5160
5371
  * Operator submit on the concierge surface. Persists the operator's
@@ -5172,6 +5383,35 @@ declare class OperatorChatService {
5172
5383
  * an empty array when no thread exists yet.
5173
5384
  */
5174
5385
  getConciergeHistory(): Promise<OperatorChatMessage[]>;
5386
+ /**
5387
+ * Whether the foundation memory store is wired. Routes use this to
5388
+ * 503 cleanly when called against an unwired service.
5389
+ */
5390
+ hasConciergeMemory(): boolean;
5391
+ /**
5392
+ * List concierge memory threads, newest-first. Emits the
5393
+ * `operator_concierge_history_read` audit event with `thread_id="*"`.
5394
+ */
5395
+ listConciergeMemoryThreads(opts?: ListThreadsOptions): Promise<ConciergeThreadSummary[]>;
5396
+ /**
5397
+ * Read a concierge memory thread, oldest turn first. Emits the
5398
+ * `operator_concierge_history_read` audit event with the named
5399
+ * thread_id and the count of turns surfaced.
5400
+ */
5401
+ readConciergeMemoryThread(threadId: string, opts?: ReadThreadOptions): Promise<ConciergeTurn[]>;
5402
+ /**
5403
+ * Delete a concierge memory thread. Emits
5404
+ * `operator_concierge_thread_deleted` only when a bundle was actually
5405
+ * removed; absent threads return false without an audit event.
5406
+ */
5407
+ deleteConciergeMemoryThread(threadId: string): Promise<boolean>;
5408
+ /**
5409
+ * Reset the active session memory thread. Subsequent sendConcierge
5410
+ * calls allocate a fresh thread_id. Surfaced for tests + future "new
5411
+ * conversation" affordance; not currently called by the dashboard.
5412
+ */
5413
+ resetConciergeMemoryThread(): void;
5414
+ private ensureActiveMemoryThread;
5175
5415
  /**
5176
5416
  * Stitch fortress state into a single context blob the substrate
5177
5417
  * folds into its summarization prompt.
@@ -5603,6 +5843,15 @@ declare class HubService {
5603
5843
  * Read the persisted concierge thread.
5604
5844
  */
5605
5845
  getConciergeHistory(): Promise<OperatorChatMessage[]>;
5846
+ /**
5847
+ * Whether the operator-chat service has the WP-V1.3-9 memory store
5848
+ * wired. Routes use this to 503 cleanly when the foundation memory
5849
+ * surface is unavailable on a given fortress.
5850
+ */
5851
+ hasConciergeMemory(): boolean;
5852
+ listConciergeMemoryThreads(opts?: ListThreadsOptions): Promise<ConciergeThreadSummary[]>;
5853
+ readConciergeMemoryThread(threadId: string, opts?: ReadThreadOptions): Promise<ConciergeTurn[]>;
5854
+ deleteConciergeMemoryThread(threadId: string): Promise<boolean>;
5606
5855
  /**
5607
5856
  * Open the click-to-inspect/approve panel for a wrapped agent. The
5608
5857
  * panel surfaces recent activity routed through this agent, pending
@@ -5669,6 +5918,268 @@ interface V11Bindings {
5669
5918
  operatorChatService?: OperatorChatService;
5670
5919
  }
5671
5920
 
5921
+ /**
5922
+ * Sanctuary v1.3 WP-V1.3-10 Cross-Harness Approval Inbox Upsilon-1
5923
+ *
5924
+ * The aggregator is a passive subscriber to the existing ApprovalGate
5925
+ * lifecycle. On each `gate-request` event it normalizes the payload into a
5926
+ * stable `AggregatedApproval` record, dedupes against prior emissions of
5927
+ * the same `(source_harness, source_agent_id, audit_log_entry_id)` tuple,
5928
+ * persists the record under the reserved `_approval_aggregator` namespace,
5929
+ * and exposes a query + resolve API.
5930
+ *
5931
+ * Scope of Upsilon-1:
5932
+ * - Aggregator module + storage + resolve API.
5933
+ * - Wired into the gate via an additive callback. The gate's blocking
5934
+ * deny/accept logic is unchanged; the aggregator never fails the gate
5935
+ * (callback exceptions are swallowed and audit-logged).
5936
+ * - HTTP route surface and three audit event names are owned by the
5937
+ * sibling `approval-aggregator-routes.ts` module + the gate wire-up.
5938
+ *
5939
+ * Out of scope (carries to Upsilon-2..4):
5940
+ * - Per-harness wrapped-agent approval-redirect mode.
5941
+ * - Operator decisions through the aggregator's HTTP `approve`/`deny`
5942
+ * routes flowing back to a blocked `channel.requestApproval()` call;
5943
+ * Upsilon-1 records the decision on the aggregator entry only.
5944
+ * - Provenance + replay UX.
5945
+ * - Mobile companion preview hook.
5946
+ */
5947
+
5948
+ /**
5949
+ * Lifecycle status of an aggregated approval entry.
5950
+ * - `pending`: ingest fired, gate is awaiting a channel decision.
5951
+ * - `approved` / `denied`: gate's channel returned a decision OR the
5952
+ * operator resolved through the HTTP surface.
5953
+ * - `timeout`: the channel reported a timeout (decided_by ===
5954
+ * 'channel_failure' OR explicit timeout reason).
5955
+ * - `expired`: pending past TTL on a `list()` poll without a resolution.
5956
+ */
5957
+ type AggregatedApprovalStatus = "pending" | "approved" | "denied" | "timeout" | "expired";
5958
+ /**
5959
+ * Normalized record the aggregator stores per approval. Field set is
5960
+ * additive-stable; new fields go behind `?` so existing dashboards keep
5961
+ * rendering unchanged. Raw request payloads are not stored on the record;
5962
+ * only a SHA-256 hash and the original payload pulled separately via
5963
+ * `getFullPayload()`.
5964
+ */
5965
+ interface AggregatedApproval {
5966
+ /** Stable per-fortress aggregator id (UUID v4). */
5967
+ aggregator_id: string;
5968
+ /** Harness the source approval came from (claude-code, cline, cursor, etc). */
5969
+ source_harness: string;
5970
+ /** Wrapped-agent identifier. Ed25519 pubkey hex or label. */
5971
+ source_agent_id: string;
5972
+ /**
5973
+ * Audit-log entry id this approval correlates to. Used as the
5974
+ * deduplication key alongside source_harness and source_agent_id.
5975
+ * Format: `<iso-timestamp>:<operation>` so two different requests on the
5976
+ * same operation in the same millisecond are still distinguishable.
5977
+ */
5978
+ audit_log_entry_id: string;
5979
+ /** Policy rule that fired (`tier1_<operation>`, `tier2_<anomaly>`, etc.). */
5980
+ policy_rule_id: string;
5981
+ /** Operator-friendly one-line summary, derived from gate context. */
5982
+ action_summary: string;
5983
+ /** SHA-256 hex of the canonicalized request payload. */
5984
+ request_payload_hash: string;
5985
+ /** Lifecycle status. */
5986
+ status: AggregatedApprovalStatus;
5987
+ /** ISO 8601 timestamp the aggregator first ingested the request. */
5988
+ created_at: string;
5989
+ /** ISO 8601 timestamp the status left `pending`. */
5990
+ resolved_at?: string;
5991
+ /** Operator identity that resolved the entry, if applicable. */
5992
+ resolved_by?: string;
5993
+ /** ISO 8601 deadline. Defaults to created_at + DEFAULT_PENDING_TTL_MS. */
5994
+ expires_at: string;
5995
+ /**
5996
+ * Optional cross-link to a v1.1 hub inbox item id. When set, the
5997
+ * dashboard renderer SHOULD suppress the duplicate hub card so the
5998
+ * operator sees one card per approval. Hub inbox surface is unchanged
5999
+ * (the cross-link signal lives on the aggregator side only).
6000
+ */
6001
+ hub_inbox_item_id?: string;
6002
+ }
6003
+ /**
6004
+ * Source-context resolver. Called once per ingest to map gate context to
6005
+ * the aggregator's harness + agent_id fields. Default impl reads the
6006
+ * fortress identity (single-agent fortresses) and returns `unknown` when
6007
+ * the gate context does not name an agent.
6008
+ */
6009
+ interface ApprovalSourceContext {
6010
+ source_harness: string;
6011
+ source_agent_id: string;
6012
+ }
6013
+ /**
6014
+ * Event the gate emits per approval lifecycle transition.
6015
+ */
6016
+ interface ApprovalGateEvent {
6017
+ /** `requested` on initial gate entry; `resolved` when channel returns. */
6018
+ phase: "requested" | "resolved";
6019
+ /** Operation name from the policy gate. */
6020
+ operation: string;
6021
+ /** Tier the gate decided on. */
6022
+ tier: 1 | 2;
6023
+ /** Human-readable reason from the gate (audit log only; not user-facing). */
6024
+ reason: string;
6025
+ /** Sanitized context map the gate captured. */
6026
+ context: Record<string, unknown>;
6027
+ /** ISO 8601 timestamp the gate entered `requestApproval()`. */
6028
+ request_timestamp: string;
6029
+ /**
6030
+ * On `resolved` only. Channel decision plus decided_by metadata. When
6031
+ * `decision === 'deny'` and `decided_by === 'channel_failure'`, the
6032
+ * aggregator records `status: 'timeout'` (Castle-walking discipline:
6033
+ * fail-closed at the gate flows through to the inbox).
6034
+ */
6035
+ resolution?: {
6036
+ decision: "approve" | "deny";
6037
+ decided_at: string;
6038
+ decided_by: string;
6039
+ };
6040
+ /**
6041
+ * Stable correlation id the gate emits BOTH on `requested` and on
6042
+ * `resolved`. The aggregator uses this to update the same entry on the
6043
+ * resolution event. Format: `<iso-ms-timestamp>:<operation>:<random4>`.
6044
+ */
6045
+ correlation_id: string;
6046
+ }
6047
+ /**
6048
+ * Listener subscription returned by `onEvent`. Call to unsubscribe.
6049
+ */
6050
+ type ApprovalAggregatorUnsubscribe = () => void;
6051
+ /**
6052
+ * Event emitted to subscribers. Mirrors the lifecycle transitions the
6053
+ * aggregator records. SSE listeners surface these to the dashboard.
6054
+ */
6055
+ interface ApprovalAggregatorEmit {
6056
+ /**
6057
+ * `aggregated` on first ingest, `resolved` on a status leaving pending,
6058
+ * `deduped` when an ingest dropped because the same dedup key was seen.
6059
+ */
6060
+ type: "aggregated" | "resolved" | "deduped";
6061
+ entry: AggregatedApproval;
6062
+ }
6063
+ /**
6064
+ * Constructor dependencies. `pendingTtlMs` and `maxListLimit` default to
6065
+ * coordinator-CTO defaults; tests pass overrides for deterministic timing.
6066
+ */
6067
+ interface ApprovalAggregatorDeps {
6068
+ storage: StorageBackend;
6069
+ masterKey: Uint8Array;
6070
+ auditLog: AuditLog;
6071
+ /** Operator identity id. Recorded on the audit entries. */
6072
+ identityId: string;
6073
+ /** Stable fortress id. Drives the source-context default values. */
6074
+ fortressId: string;
6075
+ /** Optional override for the pending TTL. */
6076
+ pendingTtlMs?: number;
6077
+ /** Optional override for the maximum list limit. */
6078
+ maxListLimit?: number;
6079
+ /** Optional clock override for deterministic tests. Defaults to `() => new Date()`. */
6080
+ now?: () => Date;
6081
+ /** Optional source-context resolver. Defaults to fortress identifiers. */
6082
+ resolveSourceContext?: (event: ApprovalGateEvent) => ApprovalSourceContext;
6083
+ /**
6084
+ * Optional cross-link resolver. Returns the v1.1 hub inbox item id that
6085
+ * shadows the same approval, when known. Default returns undefined and
6086
+ * the dashboard renders both surfaces. Upsilon-2 wires this through the
6087
+ * hub inbox store.
6088
+ */
6089
+ resolveHubInboxItemId?: (event: ApprovalGateEvent) => string | undefined;
6090
+ }
6091
+ /**
6092
+ * Aggregator state. The map is hydrated lazily from the encrypted
6093
+ * namespace on first read; subsequent reads are served from memory.
6094
+ */
6095
+ declare class ApprovalAggregator {
6096
+ private readonly storage;
6097
+ private readonly encryptionKey;
6098
+ private readonly auditLog;
6099
+ private readonly identityId;
6100
+ private readonly fortressId;
6101
+ private readonly pendingTtlMs;
6102
+ private readonly maxListLimit;
6103
+ private readonly now;
6104
+ private readonly resolveSourceContext;
6105
+ private readonly resolveHubInboxItemId;
6106
+ /** Cached entries by `aggregator_id`. */
6107
+ private readonly entries;
6108
+ /** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
6109
+ private readonly dedupIndex;
6110
+ /** Correlation index: gate `correlation_id` -> aggregator_id. */
6111
+ private readonly correlationIndex;
6112
+ /** Original request payloads kept in-memory for `getFullPayload()`. */
6113
+ private readonly fullPayloads;
6114
+ /** Has the aggregator hydrated persisted entries on this process? */
6115
+ private hydrated;
6116
+ /** Active SSE listeners. */
6117
+ private readonly listeners;
6118
+ constructor(deps: ApprovalAggregatorDeps);
6119
+ /**
6120
+ * Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
6121
+ * use this to forward aggregator emissions to the dashboard.
6122
+ */
6123
+ onEvent(listener: (event: ApprovalAggregatorEmit) => void): ApprovalAggregatorUnsubscribe;
6124
+ /**
6125
+ * Ingest a gate event. Returns the aggregator entry on first sight,
6126
+ * `null` when deduped. Resolution events update the existing record;
6127
+ * unmatched resolutions are dropped silently (caller's gate emitted a
6128
+ * resolved-without-requested pair, which the aggregator does not invent
6129
+ * a record for).
6130
+ */
6131
+ ingest(event: ApprovalGateEvent): Promise<AggregatedApproval | null>;
6132
+ /**
6133
+ * List pending or recently resolved entries. Pending entries past TTL
6134
+ * are lazily transitioned to `expired` and persisted before the list
6135
+ * snapshot is returned.
6136
+ */
6137
+ list(opts?: {
6138
+ status?: AggregatedApprovalStatus;
6139
+ sinceTs?: string;
6140
+ limit?: number;
6141
+ }): Promise<AggregatedApproval[]>;
6142
+ /**
6143
+ * Return the original (unhashed) request payload for the entry. Returns
6144
+ * `null` when the entry is unknown or the payload was evicted (e.g. the
6145
+ * process restarted; payloads are in-memory only at v1.3 Upsilon-1).
6146
+ */
6147
+ getFullPayload(aggregatorId: string): Promise<unknown>;
6148
+ /**
6149
+ * Resolve an entry. Used by both:
6150
+ * 1. The gate wire-up on channel-decision return.
6151
+ * 2. The HTTP `approve`/`deny` routes when an operator clicks.
6152
+ *
6153
+ * Idempotent: resolving an already-resolved entry is a no-op (the record
6154
+ * keeps its first decision and the audit log is not double-fired).
6155
+ * Unknown ids throw `Error("approval-aggregator: not_found")` so HTTP
6156
+ * routes return 404.
6157
+ */
6158
+ resolve(aggregatorId: string, decision: "approved" | "denied", operatorId: string): Promise<AggregatedApproval>;
6159
+ private ingestRequested;
6160
+ private ingestResolved;
6161
+ /**
6162
+ * Audit-log entry id for the dedup tuple. The audit log itself does not
6163
+ * surface a stable per-entry id (counter-prefixed keys are internal); the
6164
+ * aggregator uses the request timestamp + operation, which together pin
6165
+ * the audit entry the gate appended on the same call.
6166
+ */
6167
+ private auditEntryIdForEvent;
6168
+ private derivePolicyRuleId;
6169
+ private deriveActionSummary;
6170
+ /**
6171
+ * Canonical SHA-256 of the request context. Sorted-keys serialization so
6172
+ * identical payloads always hash the same, even when key insertion order
6173
+ * varies. Defends against payload-replay smuggling (the aggregator can
6174
+ * tell the same payload was seen twice without storing it cleartext).
6175
+ */
6176
+ private hashPayload;
6177
+ private emit;
6178
+ private expireStale;
6179
+ private persist;
6180
+ private hydrate;
6181
+ }
6182
+
5672
6183
  /**
5673
6184
  * Sanctuary MCP Server — Principal Dashboard
5674
6185
  *
@@ -5758,6 +6269,14 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
5758
6269
  * regardless. Default route flip is deferred to v1.2.
5759
6270
  */
5760
6271
  private v11Bindings;
6272
+ /**
6273
+ * v1.3 WP-V1.3-10 Cross-Harness Approval Inbox aggregator. Mounted
6274
+ * additively at `/api/approval-inbox/*` when set. Legacy approval
6275
+ * routes at `/api/approvals/:id/(allow|deny)` continue to serve. The
6276
+ * aggregator is a passive subscriber to the gate; the routes here are
6277
+ * the operator-facing query / decision surface.
6278
+ */
6279
+ private approvalAggregator;
5761
6280
  constructor(config: DashboardConfig);
5762
6281
  /**
5763
6282
  * Inject dependencies after construction.
@@ -5788,6 +6307,18 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
5788
6307
  * Pass `null` to detach the bindings (used by tests and during shutdown).
5789
6308
  */
5790
6309
  setV11Bindings(bindings: V11Bindings | null): void;
6310
+ /**
6311
+ * v1.3 WP-V1.3-10 Upsilon-1: bind the cross-harness approval inbox
6312
+ * aggregator. Once set, requests to `/api/approval-inbox/*` route
6313
+ * through `handleApprovalInboxRoute`. Pass `null` to detach (used by
6314
+ * tests + during shutdown).
6315
+ */
6316
+ setApprovalAggregator(aggregator: ApprovalAggregator | null): void;
6317
+ /**
6318
+ * v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
6319
+ * before the legacy approval route table. Returns true when served.
6320
+ */
6321
+ private dispatchApprovalInbox;
5791
6322
  /**
5792
6323
  * v1.1 dispatch entry point. Called from `handleRequest` before the
5793
6324
  * legacy route table. Returns true when the request was served by v1.1
@@ -6772,4 +7303,4 @@ declare function createSanctuaryServer(options?: {
6772
7303
  storage?: StorageBackend;
6773
7304
  }): Promise<SanctuaryServer>;
6774
7305
 
6775
- export { ATTESTATION_VERSION, type ActivityEntry, type AggregatorSources, ApprovalGate, type ApprovalHandlers, type AttestationBody, type AttestationVerificationResult, AuditLog, AutoApproveChannel, BaselineTracker, type BridgeAttestationRequest, type BridgeAttestationResult, type BridgeCommitment, type BridgeVerificationResult, TEMPLATES as CONTEXT_GATE_TEMPLATES, CallbackApprovalChannel, ClientManager, CommitmentStore, type ConcordiaOutcome, type ConnectionState, type ContextAction, type ContextFilterResult, ContextGateEnforcer, type ContextGatePolicy, ContextGatePolicyStore, type ContextGateRule, type ContextGateTemplate, DashboardApprovalChannel, type DashboardConfig, type DashboardHandle, type DashboardServerOptions, type DetectionResult, type EnforcerConfig, type ExitAuditReceiptsArtifact, type ExitBundleDetailedVerifierResult, ExitBundleImportError, type ExitCommandArgs, type ExitCommitmentsArtifact, type ExitEncryptedStateBundle, type ExitPlaceholderVaultMetadataArtifact, type ExitPolicySetArtifact, type ExitPublicIdentityArtifact, type ExportExitBundleOptions, type ExportExitBundleResult, type FederationCapabilities, type FederationPeer, FederationRegistry, type FieldClassification, type FieldFilterResult, FilesystemStorage, type GateResult, HERO_COPY, type HandshakeChallenge, type HandshakeCompletion, type HandshakeResponse, type HandshakeResult, type ImportExitBundleOptions, type ImportExitBundleResult, InMemoryModelProvenanceStore, InjectionDetector, type InjectionDetectorConfig, type InjectionSignal, type L1Status, type L2Status, type L3Status, type L4Status, type LoadedExitArtifact, MODEL_PRESETS, MemoryStorage, type ModelProvenance, type ModelProvenanceStore, type PedersenCommitment, type PeerTrustEvaluation, type PendingApproval, type PolicyRecommendation, PolicyStore, type PrincipalPolicy, type ProtectionSnapshot, type ProviderCategory, ProxyRouter, type ProxyRouterOptions, type ReputationLookup, ReputationStore, type SHRBody, type SHRGeneratorOptions, type SHRVerificationResult, type SanctuaryConfig, type SanctuaryServer, type SignedAttestation, type SignedSHR, type SovereigntyProfile, SovereigntyProfileStore, type SovereigntyProfileUpdate, type SovereigntyTier, type StartDashboardOptions, StateStore, StderrApprovalChannel, type StreamEvent, TIER_WEIGHTS, type TierMetadata, type TieredAttestation, type UpstreamConnection, type UpstreamServer, type UpstreamTool, type VerifyExitBundleOptions, WebhookApprovalChannel, type WebhookCallbackPayload, type WebhookConfig, type WebhookPayload, type ZKProofOfKnowledge, type ZKRangeProof, canonicalize, classifyField, completeHandshake, computeWeightedScore, createBridgeCommitment, createDefaultProfile, createPedersenCommitment, createProofOfKnowledge, createRangeProof, createSanctuaryServer, evaluateField, exitBundleManifestShape, exportExitBundle, filterContext, generateAttestation, generateSHR, generateSystemPrompt, getProtectionSnapshot, getTemplate, importExitBundle, initiateHandshake, listTemplateIds, loadConfig, loadExitArtifact, loadPrincipalPolicy, readManifest, recommendPolicy, renderDashboardHTML, resolveTier, respondToHandshake, runExitCommand, signPayload, startDashboard, startDashboardServer, tierDistribution, verifyAttestation, verifyBridgeCommitment, verifyCompletion, verifyExitBundle, verifyPedersenCommitment, verifyProofOfKnowledge, verifyRangeProof, verifySHR, verifySignature };
7306
+ export { ATTESTATION_VERSION, type ActivityEntry, type AggregatorSources, ApprovalGate, type ApprovalHandlers, type AttestationBody, type AttestationVerificationResult, AuditLog, AutoApproveChannel, BaselineTracker, type BridgeAttestationRequest, type BridgeAttestationResult, type BridgeCommitment, type BridgeVerificationResult, TEMPLATES as CONTEXT_GATE_TEMPLATES, CallbackApprovalChannel, ClientManager, CommitmentStore, type ConcordiaOutcome, type ConnectionState, type ContextAction, type ContextFilterResult, ContextGateEnforcer, type ContextGatePolicy, ContextGatePolicyStore, type ContextGateRule, type ContextGateTemplate, DashboardApprovalChannel, type DashboardConfig, type DashboardHandle, type DashboardServerOptions, type DetectionResult, type EnforcerConfig, type ExitAuditReceiptsArtifact, type ExitBundleDetailedVerifierResult, ExitBundleImportError, type ExitCommandArgs, type ExitCommitmentsArtifact, type ExitEncryptedStateBundle, type ExitPlaceholderVaultMetadataArtifact, type ExitPolicySetArtifact, type ExitPublicIdentityArtifact, type ExportExitBundleOptions, type ExportExitBundleResult, type FederationCapabilities, type FederationPeer, FederationRegistry, type FieldClassification, type FieldFilterResult, FilesystemStorage, type GateResult, HERO_COPY, type HandshakeChallenge, type HandshakeCompletion, type HandshakeResponse, type HandshakeResult, type ImportExitBundleOptions, type ImportExitBundleResult, InMemoryModelProvenanceStore, InjectionDetector, type InjectionDetectorConfig, type InjectionSignal, type L1Status, type L2Status, type L3Status, type L4Status, type LoadedExitArtifact, MODEL_PRESETS, MalformedPrincipalPolicyError, MemoryStorage, type ModelProvenance, type ModelProvenanceStore, type PedersenCommitment, type PeerTrustEvaluation, type PendingApproval, type PolicyRecommendation, PolicyStore, type PrincipalPolicy, type ProtectionSnapshot, type ProviderCategory, ProxyRouter, type ProxyRouterOptions, type ReputationLookup, ReputationStore, type SHRBody, type SHRGeneratorOptions, type SHRVerificationResult, type SanctuaryConfig, type SanctuaryServer, type SignedAttestation, type SignedSHR, type SovereigntyProfile, SovereigntyProfileStore, type SovereigntyProfileUpdate, type SovereigntyTier, type StartDashboardOptions, StateStore, StderrApprovalChannel, type StreamEvent, TIER_WEIGHTS, type TierMetadata, type TieredAttestation, type UpstreamConnection, type UpstreamServer, type UpstreamTool, type VerifyExitBundleOptions, WebhookApprovalChannel, type WebhookCallbackPayload, type WebhookConfig, type WebhookPayload, type ZKProofOfKnowledge, type ZKRangeProof, canonicalize, classifyField, completeHandshake, computeWeightedScore, createBridgeCommitment, createDefaultProfile, createPedersenCommitment, createProofOfKnowledge, createRangeProof, createSanctuaryServer, evaluateField, exitBundleManifestShape, exportExitBundle, filterContext, generateAttestation, generateSHR, generateSystemPrompt, getProtectionSnapshot, getTemplate, importExitBundle, initiateHandshake, listTemplateIds, loadConfig, loadExitArtifact, loadPrincipalPolicy, readManifest, recommendPolicy, renderDashboardHTML, resolveTier, respondToHandshake, runExitCommand, signPayload, startDashboard, startDashboardServer, tierDistribution, verifyAttestation, verifyBridgeCommitment, verifyCompletion, verifyExitBundle, verifyPedersenCommitment, verifyProofOfKnowledge, verifyRangeProof, verifySHR, verifySignature };