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