@sanctuary-framework/mcp-server 1.2.5 → 1.2.6

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,35 @@ declare class ConciergeMemoryStore {
5315
5315
  private withLock;
5316
5316
  }
5317
5317
 
5318
+ /**
5319
+ * Closed enum of categories the router may classify a query into. Adding
5320
+ * a category requires updating: this enum, the keyword table, the
5321
+ * fetcher interface, every wiring fetcher, and the test surface.
5322
+ */
5323
+ type ContextCategory = "templates" | "agent_state" | "agent_activity" | "audit_log" | "sentinel_findings" | "anomaly_alerts" | "recent_receipts" | "verascore_deltas";
5324
+ /**
5325
+ * Caller-supplied data sources. Each fetcher returns plain text the
5326
+ * router stitches into the rendered section. Returning the empty string
5327
+ * (or whitespace only) is treated as "nothing to fold for this category"
5328
+ * and the category drops out of the final categoriesIncluded list.
5329
+ */
5330
+ 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>;
5339
+ }
5340
+ /**
5341
+ * Auxiliary classifier the caller may supply for the LLM-assist fallback
5342
+ * path. Invoked only when keyword classification returns zero matches
5343
+ * AND the query is non-trivial (>= 8 chars, not a clear greeting).
5344
+ */
5345
+ type LlmAssistClassifier = (query: string, categories: readonly ContextCategory[]) => Promise<ContextCategory | "none">;
5346
+
5318
5347
  /**
5319
5348
  * Sanctuary MCP Server — Operator Chat Service
5320
5349
  *
@@ -5461,6 +5490,30 @@ interface OperatorChatServiceDeps {
5461
5490
  * implementation run.
5462
5491
  */
5463
5492
  conciergeClock?: () => number;
5493
+ /**
5494
+ * WP-V1.3-9 Tau-3: caller-supplied fetchers for dynamic context
5495
+ * injection. When wired alongside the substrate selector, every
5496
+ * `sendConcierge` round-trip routes the operator's query through the
5497
+ * concierge-context-router and folds matching live data into the
5498
+ * substrate prompt between the static Sanctuary reference and the
5499
+ * prior-turns fold. Omit to disable dynamic context (the static
5500
+ * reference + prior-turns paths keep working unchanged).
5501
+ */
5502
+ conciergeContextFetchers?: ContextFetchers;
5503
+ /**
5504
+ * WP-V1.3-9 Tau-3: optional LLM-assist classifier for queries that
5505
+ * fail keyword classification. Coordinator-CTO bake: when wired, the
5506
+ * service routes the auxiliary classifier call through the substrate
5507
+ * selector at the same `concierge` surface, so the auxiliary call
5508
+ * shares the operator's substrate choice and never opens a new
5509
+ * outbound surface.
5510
+ */
5511
+ conciergeContextLlmAssist?: LlmAssistClassifier;
5512
+ /**
5513
+ * WP-V1.3-9 Tau-3: rough token budget for the dynamic-context fold.
5514
+ * Defaults to `DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET` (2000).
5515
+ */
5516
+ conciergeDynamicContextBudget?: number;
5464
5517
  }
5465
5518
  declare class OperatorChatService {
5466
5519
  private store;
@@ -5476,6 +5529,9 @@ declare class OperatorChatService {
5476
5529
  private historyTokenBudget;
5477
5530
  private sessionTtlMs;
5478
5531
  private clock;
5532
+ private contextFetchers?;
5533
+ private contextLlmAssist?;
5534
+ private dynamicContextBudget;
5479
5535
  /**
5480
5536
  * In-memory thread_id assigned to the active concierge session.
5481
5537
  * The first sendConcierge call after construction allocates a fresh
@@ -5556,10 +5612,13 @@ declare class OperatorChatService {
5556
5612
  * ## Sanctuary reference
5557
5613
  * <static domain reference block>
5558
5614
  *
5615
+ * ## Live fortress context ← WP-V1.3-9 Tau-3, when present
5616
+ * ### <Category>
5617
+ * <fetcher payload>
5618
+ *
5559
5619
  * ## Prior conversation ← WP-V1.3-9 Tau-2, when present
5560
5620
  * OPERATOR: ...
5561
5621
  * CONCIERGE: ...
5562
- * ---
5563
5622
  *
5564
5623
  * ## Recent activity
5565
5624
  * <recentActivity output>
@@ -5579,6 +5638,23 @@ declare class OperatorChatService {
5579
5638
  * serialization is the canonical path for v1.3.
5580
5639
  */
5581
5640
  private assembleConciergeContext;
5641
+ /**
5642
+ * Run the WP-V1.3-9 Tau-3 dynamic-context fold for a single round-
5643
+ * trip. Fail-soft on every axis: missing fetchers short-circuit to
5644
+ * an empty fold, fetcher failures emit a per-category audit event
5645
+ * and are omitted from the rendered section, an LLM-assist failure
5646
+ * proceeds with no fold. Returns the rendered section + the list of
5647
+ * categories whose data made it into the section (used for the
5648
+ * round-trip audit emission).
5649
+ */
5650
+ private runDynamicContextFold;
5651
+ /**
5652
+ * Emit the WP-V1.3-9 Tau-3 fetcher-failure audit event. Pulled out
5653
+ * of the fold path so the dynamic-context handler stays readable.
5654
+ * Emits with `result: "failure"` since the named category dropped
5655
+ * from the rendered section for this round-trip.
5656
+ */
5657
+ private emitContextFetcherFailed;
5582
5658
  /**
5583
5659
  * Render the prior-conversation section with token-budget enforcement
5584
5660
  * (WP-V1.3-9 Tau-2). Drops oldest turns first when the rendered
@@ -6069,6 +6145,87 @@ interface V11Bindings {
6069
6145
  operatorChatService?: OperatorChatService;
6070
6146
  }
6071
6147
 
6148
+ /**
6149
+ * Sanctuary v1.3 WP-V1.3-10 Cross-Harness Approval Inbox Upsilon-3
6150
+ *
6151
+ * At-rest encrypted persistence for full request payloads tied to
6152
+ * AggregatedApproval entries. Sibling store to ApprovalAggregator: the
6153
+ * aggregator's own entry record (status, provenance, hashes) is persisted
6154
+ * by the aggregator class under `_approval_aggregator`; this store holds
6155
+ * the original request payload separately under
6156
+ * `_approval_aggregator_payloads` so the operator can replay the full
6157
+ * payload after a server restart.
6158
+ *
6159
+ * Storage layout:
6160
+ * namespace: `_approval_aggregator_payloads`
6161
+ * key: `payload.{aggregator_id}` (one record per approval)
6162
+ * payload: AES-256-GCM ciphertext of the JSON-serialised bundle.
6163
+ * key: `l2-approval-aggregator-payload-v1` HKDF subkey of fortress
6164
+ * master.
6165
+ * AAD: UTF-8 bytes of `aggregator_id`. Swapping records across
6166
+ * aggregator_ids breaks the auth tag. Castle-walking
6167
+ * discipline: the encryption boundary holds even against
6168
+ * on-disk shuffle.
6169
+ *
6170
+ * Retention:
6171
+ * Each bundle carries an ISO-8601 `retention_until`. `pruneExpired()`
6172
+ * iterates payloads and drops entries whose retention_until is in the
6173
+ * past. Default retention is 30 days, mirroring the audit-log retention
6174
+ * envelope. Operator override flows through the aggregator's deps.
6175
+ *
6176
+ * Multi-fortress isolation:
6177
+ * The HKDF subkey is derived from the fortress master key. Two
6178
+ * fortresses never produce identical encryption keys for identical
6179
+ * aggregator_ids. Isolation is enforced cryptographically; the AAD
6180
+ * binds the ciphertext to a specific aggregator_id within a fortress.
6181
+ */
6182
+
6183
+ interface AggregatorPayloadStoreOptions {
6184
+ /** Storage backend that persists the encrypted bundles. */
6185
+ storage: StorageBackend;
6186
+ /** 32-byte fortress master key. */
6187
+ masterKey: Uint8Array;
6188
+ /** Stable fortress id stamped on every bundle for audit clarity. */
6189
+ fortressId: string;
6190
+ /** Operator-tunable retention window. Default 30 days. */
6191
+ retentionDays?: number;
6192
+ }
6193
+ /**
6194
+ * Encrypted, AAD-bound, retention-aware payload persistence for the
6195
+ * cross-harness approval inbox.
6196
+ */
6197
+ declare class AggregatorPayloadStore {
6198
+ private readonly storage;
6199
+ private readonly encryptionKey;
6200
+ private readonly fortressId;
6201
+ private readonly retentionDays;
6202
+ constructor(opts: AggregatorPayloadStoreOptions);
6203
+ /**
6204
+ * Persist `payload` under the given aggregator_id. Idempotent; calling
6205
+ * twice with the same id rewrites the bundle (retention_until is
6206
+ * recomputed). Returns the bundle's retention_until ISO-8601 timestamp
6207
+ * so callers can log it.
6208
+ */
6209
+ savePayload(aggregatorId: string, payload: unknown): Promise<string>;
6210
+ /**
6211
+ * Read the persisted payload for the aggregator_id. Returns null if no
6212
+ * bundle exists, the bundle is corrupted, or AAD binding fails.
6213
+ */
6214
+ loadPayload(aggregatorId: string): Promise<unknown>;
6215
+ /**
6216
+ * Delete the persisted payload. Returns true when a bundle was removed,
6217
+ * false when none existed.
6218
+ */
6219
+ deletePayload(aggregatorId: string): Promise<boolean>;
6220
+ /**
6221
+ * Drop expired payload bundles. Returns the count of bundles pruned.
6222
+ * Caller wires this into the cocoon-unlock initialization path.
6223
+ */
6224
+ pruneExpired(now?: Date): Promise<{
6225
+ pruned: number;
6226
+ }>;
6227
+ }
6228
+
6072
6229
  /**
6073
6230
  * Sanctuary v1.3 WP-V1.3-10 Cross-Harness Approval Inbox Upsilon-1
6074
6231
  *
@@ -6106,6 +6263,19 @@ interface V11Bindings {
6106
6263
  * - `expired`: pending past TTL on a `list()` poll without a resolution.
6107
6264
  */
6108
6265
  type AggregatedApprovalStatus = "pending" | "approved" | "denied" | "timeout" | "expired";
6266
+ /**
6267
+ * One step in the Castle Architecture enforcement chain that led to this
6268
+ * approval. Layers are: l1 (Castle Wall, OS-level egress), l2 (Sentinel +
6269
+ * cooperative MCP gate), l3 (selective disclosure), l4 (reputation).
6270
+ * v1.3 Upsilon-3 ships the schema; default resolver populates a single
6271
+ * `l2` entry. Future Castle Wall wiring will extend the chain when a
6272
+ * payload's egress was first observed by the kernel filter.
6273
+ */
6274
+ interface EnforcementLayerEvent {
6275
+ layer: "l1" | "l2" | "l3" | "l4";
6276
+ event: string;
6277
+ timestamp: string;
6278
+ }
6109
6279
  /**
6110
6280
  * Normalized record the aggregator stores per approval. Field set is
6111
6281
  * additive-stable; new fields go behind `?` so existing dashboards keep
@@ -6150,6 +6320,15 @@ interface AggregatedApproval {
6150
6320
  * (the cross-link signal lives on the aggregator side only).
6151
6321
  */
6152
6322
  hub_inbox_item_id?: string;
6323
+ /**
6324
+ * Castle Architecture enforcement-layer chain that led to this
6325
+ * approval. Populated by the optional `resolveEnforcementChain` deps
6326
+ * hook; default returns a single `l2` step (cooperative MCP gate
6327
+ * fired). Persisted with the entry so the operator-replay surface can
6328
+ * render the enforcement context after a server restart. v1.3
6329
+ * Upsilon-3.
6330
+ */
6331
+ enforcement_chain?: EnforcementLayerEvent[];
6153
6332
  }
6154
6333
  /**
6155
6334
  * Source-context resolver. Called once per ingest to map gate context to
@@ -6238,6 +6417,23 @@ interface ApprovalAggregatorDeps {
6238
6417
  * hub inbox store.
6239
6418
  */
6240
6419
  resolveHubInboxItemId?: (event: ApprovalGateEvent) => string | undefined;
6420
+ /**
6421
+ * Optional at-rest payload store. When provided, the aggregator
6422
+ * persists each request payload via `savePayload` on ingest, and
6423
+ * rehydrates payloads on `getFullPayload` if the in-memory map lost
6424
+ * them (e.g. after a server restart). Upsilon-3 surface; absent in
6425
+ * Upsilon-1 / Upsilon-2 deployments, where payloads remain in-memory
6426
+ * only.
6427
+ */
6428
+ payloadStore?: AggregatorPayloadStore;
6429
+ /**
6430
+ * Optional resolver for the Castle Architecture enforcement chain
6431
+ * leading to this approval. Default returns a single `l2` step
6432
+ * (cooperative MCP gate fired). When the Castle Wall (Layer 1) ships,
6433
+ * its kernel-filter observer can populate richer chains by passing a
6434
+ * resolver here.
6435
+ */
6436
+ resolveEnforcementChain?: (event: ApprovalGateEvent) => EnforcementLayerEvent[];
6241
6437
  }
6242
6438
  /**
6243
6439
  * Aggregator state. The map is hydrated lazily from the encrypted
@@ -6254,6 +6450,8 @@ declare class ApprovalAggregator {
6254
6450
  private readonly now;
6255
6451
  private readonly resolveSourceContext;
6256
6452
  private readonly resolveHubInboxItemId;
6453
+ private readonly payloadStore;
6454
+ private readonly resolveEnforcementChain;
6257
6455
  /** Cached entries by `aggregator_id`. */
6258
6456
  private readonly entries;
6259
6457
  /** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
@@ -6292,10 +6490,48 @@ declare class ApprovalAggregator {
6292
6490
  }): Promise<AggregatedApproval[]>;
6293
6491
  /**
6294
6492
  * 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).
6493
+ * `null` when the entry is unknown. When the in-memory payload map has
6494
+ * been evicted (e.g. after a server restart) and a `payloadStore` was
6495
+ * provided, the at-rest bundle is decrypted and the in-memory map is
6496
+ * refilled. Audit emission lives on the `*WithAudit` variant; this base
6497
+ * accessor is silent so internal callers can read without polluting the
6498
+ * audit trail.
6297
6499
  */
6298
6500
  getFullPayload(aggregatorId: string): Promise<unknown>;
6501
+ /**
6502
+ * Return the entry record for the given id, or null when unknown.
6503
+ * Idempotent. v1.3 Upsilon-3.
6504
+ */
6505
+ getEntry(aggregatorId: string): Promise<AggregatedApproval | null>;
6506
+ /**
6507
+ * Audited variant of `getFullPayload`. Emits the
6508
+ * `cross_harness_approval_payload_decrypted` audit event before
6509
+ * returning. Used by the operator-facing /payload replay route.
6510
+ * v1.3 Upsilon-3.
6511
+ */
6512
+ getFullPayloadWithAudit(aggregatorId: string, operatorId: string): Promise<unknown>;
6513
+ /**
6514
+ * Return the audit-log entries that led to and surround this approval.
6515
+ * Best-effort matching: aggregator-side emissions (AGGREGATED, RESOLVED,
6516
+ * DEDUPED, replay events) all carry `details.aggregator_id` and link
6517
+ * directly. Gate-side emissions (`gate_*:operation`) do not carry the
6518
+ * aggregator id at v1.3, so they are matched via timestamp window
6519
+ * (entry.created_at to entry.resolved_at + 1s, or expires_at + 1s while
6520
+ * pending) and operation suffix. Emits AUDIT_TRAIL_VIEWED on call.
6521
+ * v1.3 Upsilon-3.
6522
+ */
6523
+ getAuditTrail(aggregatorId: string, operatorId: string): Promise<AuditEntry[]>;
6524
+ /**
6525
+ * List historical (resolved) approvals. Excludes pending entries by
6526
+ * design: `list()` is the pending-inbox surface and `getHistory()` is
6527
+ * the resolved-replay surface. Emits REPLAYED on each call. v1.3
6528
+ * Upsilon-3.
6529
+ */
6530
+ getHistory(opts: {
6531
+ status?: AggregatedApprovalStatus;
6532
+ sinceTs?: string;
6533
+ limit?: number;
6534
+ } | undefined, operatorId: string): Promise<AggregatedApproval[]>;
6299
6535
  /**
6300
6536
  * Resolve an entry. Used by both:
6301
6537
  * 1. The gate wire-up on channel-decision return.
package/dist/index.d.ts CHANGED
@@ -5315,6 +5315,35 @@ declare class ConciergeMemoryStore {
5315
5315
  private withLock;
5316
5316
  }
5317
5317
 
5318
+ /**
5319
+ * Closed enum of categories the router may classify a query into. Adding
5320
+ * a category requires updating: this enum, the keyword table, the
5321
+ * fetcher interface, every wiring fetcher, and the test surface.
5322
+ */
5323
+ type ContextCategory = "templates" | "agent_state" | "agent_activity" | "audit_log" | "sentinel_findings" | "anomaly_alerts" | "recent_receipts" | "verascore_deltas";
5324
+ /**
5325
+ * Caller-supplied data sources. Each fetcher returns plain text the
5326
+ * router stitches into the rendered section. Returning the empty string
5327
+ * (or whitespace only) is treated as "nothing to fold for this category"
5328
+ * and the category drops out of the final categoriesIncluded list.
5329
+ */
5330
+ 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>;
5339
+ }
5340
+ /**
5341
+ * Auxiliary classifier the caller may supply for the LLM-assist fallback
5342
+ * path. Invoked only when keyword classification returns zero matches
5343
+ * AND the query is non-trivial (>= 8 chars, not a clear greeting).
5344
+ */
5345
+ type LlmAssistClassifier = (query: string, categories: readonly ContextCategory[]) => Promise<ContextCategory | "none">;
5346
+
5318
5347
  /**
5319
5348
  * Sanctuary MCP Server — Operator Chat Service
5320
5349
  *
@@ -5461,6 +5490,30 @@ interface OperatorChatServiceDeps {
5461
5490
  * implementation run.
5462
5491
  */
5463
5492
  conciergeClock?: () => number;
5493
+ /**
5494
+ * WP-V1.3-9 Tau-3: caller-supplied fetchers for dynamic context
5495
+ * injection. When wired alongside the substrate selector, every
5496
+ * `sendConcierge` round-trip routes the operator's query through the
5497
+ * concierge-context-router and folds matching live data into the
5498
+ * substrate prompt between the static Sanctuary reference and the
5499
+ * prior-turns fold. Omit to disable dynamic context (the static
5500
+ * reference + prior-turns paths keep working unchanged).
5501
+ */
5502
+ conciergeContextFetchers?: ContextFetchers;
5503
+ /**
5504
+ * WP-V1.3-9 Tau-3: optional LLM-assist classifier for queries that
5505
+ * fail keyword classification. Coordinator-CTO bake: when wired, the
5506
+ * service routes the auxiliary classifier call through the substrate
5507
+ * selector at the same `concierge` surface, so the auxiliary call
5508
+ * shares the operator's substrate choice and never opens a new
5509
+ * outbound surface.
5510
+ */
5511
+ conciergeContextLlmAssist?: LlmAssistClassifier;
5512
+ /**
5513
+ * WP-V1.3-9 Tau-3: rough token budget for the dynamic-context fold.
5514
+ * Defaults to `DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET` (2000).
5515
+ */
5516
+ conciergeDynamicContextBudget?: number;
5464
5517
  }
5465
5518
  declare class OperatorChatService {
5466
5519
  private store;
@@ -5476,6 +5529,9 @@ declare class OperatorChatService {
5476
5529
  private historyTokenBudget;
5477
5530
  private sessionTtlMs;
5478
5531
  private clock;
5532
+ private contextFetchers?;
5533
+ private contextLlmAssist?;
5534
+ private dynamicContextBudget;
5479
5535
  /**
5480
5536
  * In-memory thread_id assigned to the active concierge session.
5481
5537
  * The first sendConcierge call after construction allocates a fresh
@@ -5556,10 +5612,13 @@ declare class OperatorChatService {
5556
5612
  * ## Sanctuary reference
5557
5613
  * <static domain reference block>
5558
5614
  *
5615
+ * ## Live fortress context ← WP-V1.3-9 Tau-3, when present
5616
+ * ### <Category>
5617
+ * <fetcher payload>
5618
+ *
5559
5619
  * ## Prior conversation ← WP-V1.3-9 Tau-2, when present
5560
5620
  * OPERATOR: ...
5561
5621
  * CONCIERGE: ...
5562
- * ---
5563
5622
  *
5564
5623
  * ## Recent activity
5565
5624
  * <recentActivity output>
@@ -5579,6 +5638,23 @@ declare class OperatorChatService {
5579
5638
  * serialization is the canonical path for v1.3.
5580
5639
  */
5581
5640
  private assembleConciergeContext;
5641
+ /**
5642
+ * Run the WP-V1.3-9 Tau-3 dynamic-context fold for a single round-
5643
+ * trip. Fail-soft on every axis: missing fetchers short-circuit to
5644
+ * an empty fold, fetcher failures emit a per-category audit event
5645
+ * and are omitted from the rendered section, an LLM-assist failure
5646
+ * proceeds with no fold. Returns the rendered section + the list of
5647
+ * categories whose data made it into the section (used for the
5648
+ * round-trip audit emission).
5649
+ */
5650
+ private runDynamicContextFold;
5651
+ /**
5652
+ * Emit the WP-V1.3-9 Tau-3 fetcher-failure audit event. Pulled out
5653
+ * of the fold path so the dynamic-context handler stays readable.
5654
+ * Emits with `result: "failure"` since the named category dropped
5655
+ * from the rendered section for this round-trip.
5656
+ */
5657
+ private emitContextFetcherFailed;
5582
5658
  /**
5583
5659
  * Render the prior-conversation section with token-budget enforcement
5584
5660
  * (WP-V1.3-9 Tau-2). Drops oldest turns first when the rendered
@@ -6069,6 +6145,87 @@ interface V11Bindings {
6069
6145
  operatorChatService?: OperatorChatService;
6070
6146
  }
6071
6147
 
6148
+ /**
6149
+ * Sanctuary v1.3 WP-V1.3-10 Cross-Harness Approval Inbox Upsilon-3
6150
+ *
6151
+ * At-rest encrypted persistence for full request payloads tied to
6152
+ * AggregatedApproval entries. Sibling store to ApprovalAggregator: the
6153
+ * aggregator's own entry record (status, provenance, hashes) is persisted
6154
+ * by the aggregator class under `_approval_aggregator`; this store holds
6155
+ * the original request payload separately under
6156
+ * `_approval_aggregator_payloads` so the operator can replay the full
6157
+ * payload after a server restart.
6158
+ *
6159
+ * Storage layout:
6160
+ * namespace: `_approval_aggregator_payloads`
6161
+ * key: `payload.{aggregator_id}` (one record per approval)
6162
+ * payload: AES-256-GCM ciphertext of the JSON-serialised bundle.
6163
+ * key: `l2-approval-aggregator-payload-v1` HKDF subkey of fortress
6164
+ * master.
6165
+ * AAD: UTF-8 bytes of `aggregator_id`. Swapping records across
6166
+ * aggregator_ids breaks the auth tag. Castle-walking
6167
+ * discipline: the encryption boundary holds even against
6168
+ * on-disk shuffle.
6169
+ *
6170
+ * Retention:
6171
+ * Each bundle carries an ISO-8601 `retention_until`. `pruneExpired()`
6172
+ * iterates payloads and drops entries whose retention_until is in the
6173
+ * past. Default retention is 30 days, mirroring the audit-log retention
6174
+ * envelope. Operator override flows through the aggregator's deps.
6175
+ *
6176
+ * Multi-fortress isolation:
6177
+ * The HKDF subkey is derived from the fortress master key. Two
6178
+ * fortresses never produce identical encryption keys for identical
6179
+ * aggregator_ids. Isolation is enforced cryptographically; the AAD
6180
+ * binds the ciphertext to a specific aggregator_id within a fortress.
6181
+ */
6182
+
6183
+ interface AggregatorPayloadStoreOptions {
6184
+ /** Storage backend that persists the encrypted bundles. */
6185
+ storage: StorageBackend;
6186
+ /** 32-byte fortress master key. */
6187
+ masterKey: Uint8Array;
6188
+ /** Stable fortress id stamped on every bundle for audit clarity. */
6189
+ fortressId: string;
6190
+ /** Operator-tunable retention window. Default 30 days. */
6191
+ retentionDays?: number;
6192
+ }
6193
+ /**
6194
+ * Encrypted, AAD-bound, retention-aware payload persistence for the
6195
+ * cross-harness approval inbox.
6196
+ */
6197
+ declare class AggregatorPayloadStore {
6198
+ private readonly storage;
6199
+ private readonly encryptionKey;
6200
+ private readonly fortressId;
6201
+ private readonly retentionDays;
6202
+ constructor(opts: AggregatorPayloadStoreOptions);
6203
+ /**
6204
+ * Persist `payload` under the given aggregator_id. Idempotent; calling
6205
+ * twice with the same id rewrites the bundle (retention_until is
6206
+ * recomputed). Returns the bundle's retention_until ISO-8601 timestamp
6207
+ * so callers can log it.
6208
+ */
6209
+ savePayload(aggregatorId: string, payload: unknown): Promise<string>;
6210
+ /**
6211
+ * Read the persisted payload for the aggregator_id. Returns null if no
6212
+ * bundle exists, the bundle is corrupted, or AAD binding fails.
6213
+ */
6214
+ loadPayload(aggregatorId: string): Promise<unknown>;
6215
+ /**
6216
+ * Delete the persisted payload. Returns true when a bundle was removed,
6217
+ * false when none existed.
6218
+ */
6219
+ deletePayload(aggregatorId: string): Promise<boolean>;
6220
+ /**
6221
+ * Drop expired payload bundles. Returns the count of bundles pruned.
6222
+ * Caller wires this into the cocoon-unlock initialization path.
6223
+ */
6224
+ pruneExpired(now?: Date): Promise<{
6225
+ pruned: number;
6226
+ }>;
6227
+ }
6228
+
6072
6229
  /**
6073
6230
  * Sanctuary v1.3 WP-V1.3-10 Cross-Harness Approval Inbox Upsilon-1
6074
6231
  *
@@ -6106,6 +6263,19 @@ interface V11Bindings {
6106
6263
  * - `expired`: pending past TTL on a `list()` poll without a resolution.
6107
6264
  */
6108
6265
  type AggregatedApprovalStatus = "pending" | "approved" | "denied" | "timeout" | "expired";
6266
+ /**
6267
+ * One step in the Castle Architecture enforcement chain that led to this
6268
+ * approval. Layers are: l1 (Castle Wall, OS-level egress), l2 (Sentinel +
6269
+ * cooperative MCP gate), l3 (selective disclosure), l4 (reputation).
6270
+ * v1.3 Upsilon-3 ships the schema; default resolver populates a single
6271
+ * `l2` entry. Future Castle Wall wiring will extend the chain when a
6272
+ * payload's egress was first observed by the kernel filter.
6273
+ */
6274
+ interface EnforcementLayerEvent {
6275
+ layer: "l1" | "l2" | "l3" | "l4";
6276
+ event: string;
6277
+ timestamp: string;
6278
+ }
6109
6279
  /**
6110
6280
  * Normalized record the aggregator stores per approval. Field set is
6111
6281
  * additive-stable; new fields go behind `?` so existing dashboards keep
@@ -6150,6 +6320,15 @@ interface AggregatedApproval {
6150
6320
  * (the cross-link signal lives on the aggregator side only).
6151
6321
  */
6152
6322
  hub_inbox_item_id?: string;
6323
+ /**
6324
+ * Castle Architecture enforcement-layer chain that led to this
6325
+ * approval. Populated by the optional `resolveEnforcementChain` deps
6326
+ * hook; default returns a single `l2` step (cooperative MCP gate
6327
+ * fired). Persisted with the entry so the operator-replay surface can
6328
+ * render the enforcement context after a server restart. v1.3
6329
+ * Upsilon-3.
6330
+ */
6331
+ enforcement_chain?: EnforcementLayerEvent[];
6153
6332
  }
6154
6333
  /**
6155
6334
  * Source-context resolver. Called once per ingest to map gate context to
@@ -6238,6 +6417,23 @@ interface ApprovalAggregatorDeps {
6238
6417
  * hub inbox store.
6239
6418
  */
6240
6419
  resolveHubInboxItemId?: (event: ApprovalGateEvent) => string | undefined;
6420
+ /**
6421
+ * Optional at-rest payload store. When provided, the aggregator
6422
+ * persists each request payload via `savePayload` on ingest, and
6423
+ * rehydrates payloads on `getFullPayload` if the in-memory map lost
6424
+ * them (e.g. after a server restart). Upsilon-3 surface; absent in
6425
+ * Upsilon-1 / Upsilon-2 deployments, where payloads remain in-memory
6426
+ * only.
6427
+ */
6428
+ payloadStore?: AggregatorPayloadStore;
6429
+ /**
6430
+ * Optional resolver for the Castle Architecture enforcement chain
6431
+ * leading to this approval. Default returns a single `l2` step
6432
+ * (cooperative MCP gate fired). When the Castle Wall (Layer 1) ships,
6433
+ * its kernel-filter observer can populate richer chains by passing a
6434
+ * resolver here.
6435
+ */
6436
+ resolveEnforcementChain?: (event: ApprovalGateEvent) => EnforcementLayerEvent[];
6241
6437
  }
6242
6438
  /**
6243
6439
  * Aggregator state. The map is hydrated lazily from the encrypted
@@ -6254,6 +6450,8 @@ declare class ApprovalAggregator {
6254
6450
  private readonly now;
6255
6451
  private readonly resolveSourceContext;
6256
6452
  private readonly resolveHubInboxItemId;
6453
+ private readonly payloadStore;
6454
+ private readonly resolveEnforcementChain;
6257
6455
  /** Cached entries by `aggregator_id`. */
6258
6456
  private readonly entries;
6259
6457
  /** Dedup index: `${harness}|${agent}|${audit_id}` -> aggregator_id. */
@@ -6292,10 +6490,48 @@ declare class ApprovalAggregator {
6292
6490
  }): Promise<AggregatedApproval[]>;
6293
6491
  /**
6294
6492
  * 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).
6493
+ * `null` when the entry is unknown. When the in-memory payload map has
6494
+ * been evicted (e.g. after a server restart) and a `payloadStore` was
6495
+ * provided, the at-rest bundle is decrypted and the in-memory map is
6496
+ * refilled. Audit emission lives on the `*WithAudit` variant; this base
6497
+ * accessor is silent so internal callers can read without polluting the
6498
+ * audit trail.
6297
6499
  */
6298
6500
  getFullPayload(aggregatorId: string): Promise<unknown>;
6501
+ /**
6502
+ * Return the entry record for the given id, or null when unknown.
6503
+ * Idempotent. v1.3 Upsilon-3.
6504
+ */
6505
+ getEntry(aggregatorId: string): Promise<AggregatedApproval | null>;
6506
+ /**
6507
+ * Audited variant of `getFullPayload`. Emits the
6508
+ * `cross_harness_approval_payload_decrypted` audit event before
6509
+ * returning. Used by the operator-facing /payload replay route.
6510
+ * v1.3 Upsilon-3.
6511
+ */
6512
+ getFullPayloadWithAudit(aggregatorId: string, operatorId: string): Promise<unknown>;
6513
+ /**
6514
+ * Return the audit-log entries that led to and surround this approval.
6515
+ * Best-effort matching: aggregator-side emissions (AGGREGATED, RESOLVED,
6516
+ * DEDUPED, replay events) all carry `details.aggregator_id` and link
6517
+ * directly. Gate-side emissions (`gate_*:operation`) do not carry the
6518
+ * aggregator id at v1.3, so they are matched via timestamp window
6519
+ * (entry.created_at to entry.resolved_at + 1s, or expires_at + 1s while
6520
+ * pending) and operation suffix. Emits AUDIT_TRAIL_VIEWED on call.
6521
+ * v1.3 Upsilon-3.
6522
+ */
6523
+ getAuditTrail(aggregatorId: string, operatorId: string): Promise<AuditEntry[]>;
6524
+ /**
6525
+ * List historical (resolved) approvals. Excludes pending entries by
6526
+ * design: `list()` is the pending-inbox surface and `getHistory()` is
6527
+ * the resolved-replay surface. Emits REPLAYED on each call. v1.3
6528
+ * Upsilon-3.
6529
+ */
6530
+ getHistory(opts: {
6531
+ status?: AggregatedApprovalStatus;
6532
+ sinceTs?: string;
6533
+ limit?: number;
6534
+ } | undefined, operatorId: string): Promise<AggregatedApproval[]>;
6299
6535
  /**
6300
6536
  * Resolve an entry. Used by both:
6301
6537
  * 1. The gate wire-up on channel-decision return.