@sanctuary-framework/mcp-server 1.2.6 → 1.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/cli.cjs +3131 -54
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3131 -54
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +2787 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +970 -10
- package/dist/index.d.ts +970 -10
- package/dist/index.js +2787 -43
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -5315,27 +5315,204 @@ declare class ConciergeMemoryStore {
|
|
|
5315
5315
|
private withLock;
|
|
5316
5316
|
}
|
|
5317
5317
|
|
|
5318
|
+
/**
|
|
5319
|
+
* Sanctuary MCP Server, Operator Chat, Concierge Query Grammar
|
|
5320
|
+
*
|
|
5321
|
+
* WP-V1.3-9 Tau-4 (criterion (d) of the v1.3 scope-lock revision). Builds
|
|
5322
|
+
* on Tau-3 (PR #171, dynamic context router). The grammar layer parses
|
|
5323
|
+
* operator queries into structured parameters: time ranges, agent names,
|
|
5324
|
+
* event types, and a free-form intent residual. Tau-3 fetchers receive
|
|
5325
|
+
* the parsed hints alongside the query so the substrate sees the most
|
|
5326
|
+
* relevant data slice.
|
|
5327
|
+
*
|
|
5328
|
+
* The module is pure: rule-based extraction, no I/O, no clock side
|
|
5329
|
+
* effects beyond the optional `now` parameter for deterministic tests.
|
|
5330
|
+
* The optional LLM-assist fallback is invoked by the caller when the
|
|
5331
|
+
* rule-based parser produces low confidence.
|
|
5332
|
+
*
|
|
5333
|
+
* Castle-walking discipline:
|
|
5334
|
+
* - No new outbound surface; the LLM-assist fallback routes through the
|
|
5335
|
+
* substrate selector at the same `concierge` surface Tau-3 already
|
|
5336
|
+
* uses.
|
|
5337
|
+
* - Encryption boundaries hold: this module receives plain strings
|
|
5338
|
+
* already produced inside the fortress.
|
|
5339
|
+
* - Failure-mode handling is fail-soft: low-confidence parses surface a
|
|
5340
|
+
* structured fallback menu so the operator sees what the concierge
|
|
5341
|
+
* can answer, never a silent drop.
|
|
5342
|
+
*
|
|
5343
|
+
* Privacy invariant:
|
|
5344
|
+
* The runtime `ParsedQuery` carries `intent_phrase` (free-form residual
|
|
5345
|
+
* pulled from the operator's query) for the router's own consumption.
|
|
5346
|
+
* The audit emission MUST NOT include `intent_phrase`; consumers serialize
|
|
5347
|
+
* via `auditSafeSummary()` which strips it. See `OperatorConciergeChatPayload`
|
|
5348
|
+
* safe-metadata invariant in `contracts/v1.2/operator-chat-events.ts`.
|
|
5349
|
+
*
|
|
5350
|
+
* Out of scope here (Tau-5 + later):
|
|
5351
|
+
* - Agent-context awareness (proactive parsing without an explicit
|
|
5352
|
+
* query). Tau-4 parses off explicit query content only.
|
|
5353
|
+
* - Voice input or natural-language-as-API surface (defer to v1.5+).
|
|
5354
|
+
* - Cross-fortress concierge query (defer to v1.4 cross-tenant scope).
|
|
5355
|
+
* - Window markers like "around the time of <event>" that require an
|
|
5356
|
+
* audit-log lookup to resolve. Tau-4 ships static patterns only.
|
|
5357
|
+
*/
|
|
5358
|
+
|
|
5359
|
+
/**
|
|
5360
|
+
* A resolved time range. `relative_label` carries the original token
|
|
5361
|
+
* ("yesterday", "past hour", etc.) when the range was derived from a
|
|
5362
|
+
* relative pattern, so consumers can render operator-readable copy.
|
|
5363
|
+
*/
|
|
5364
|
+
interface ParsedTimeRange {
|
|
5365
|
+
start: Date;
|
|
5366
|
+
end: Date;
|
|
5367
|
+
relative_label?: string;
|
|
5368
|
+
}
|
|
5369
|
+
/**
|
|
5370
|
+
* Stable structured-failure flags surfaced when the parser cannot
|
|
5371
|
+
* resolve a token confidently. The strings are enum-like (no free text)
|
|
5372
|
+
* so the audit emission stays safe-metadata-only and the dashboard can
|
|
5373
|
+
* group by cause.
|
|
5374
|
+
*/
|
|
5375
|
+
type AmbiguityFlag = "unknown_time_token" | "unknown_agent_token" | "unknown_event_token" | "ambiguous_time_range" | "no_signal_extracted";
|
|
5376
|
+
/**
|
|
5377
|
+
* Runtime parsed-query shape. Used by the router to thread parameters
|
|
5378
|
+
* into context fetchers, and by the structured-fallback menu builder.
|
|
5379
|
+
*
|
|
5380
|
+
* `intent_phrase` is the residual after time/agent/event tokens are
|
|
5381
|
+
* stripped, useful for router-level intent routing. It carries operator
|
|
5382
|
+
* query content and MUST NOT be serialized into the audit log.
|
|
5383
|
+
*/
|
|
5384
|
+
interface ParsedQuery {
|
|
5385
|
+
time_range: ParsedTimeRange | null;
|
|
5386
|
+
agent_names: string[];
|
|
5387
|
+
event_types: string[];
|
|
5388
|
+
intent_phrase: string;
|
|
5389
|
+
ambiguity_flags: AmbiguityFlag[];
|
|
5390
|
+
/** 0..1; 1.0 = every component resolved cleanly. */
|
|
5391
|
+
parse_confidence: number;
|
|
5392
|
+
}
|
|
5393
|
+
/**
|
|
5394
|
+
* Read-only registry view consumed by `parseQuery` for agent-name
|
|
5395
|
+
* extraction. The grammar matches operator query tokens against the
|
|
5396
|
+
* `agent_id` field of each record (case-insensitive, prefix-or-exact).
|
|
5397
|
+
*
|
|
5398
|
+
* Wired from `HubAgentRegistrySource.list()` at the service layer; tests
|
|
5399
|
+
* can hand a literal array. Both shapes are accepted so callers do not
|
|
5400
|
+
* have to wrap a hub registry in an adapter just to feed the parser.
|
|
5401
|
+
*/
|
|
5402
|
+
type AgentRegistryView = readonly {
|
|
5403
|
+
agent_id: string;
|
|
5404
|
+
}[] | {
|
|
5405
|
+
list: () => readonly {
|
|
5406
|
+
agent_id: string;
|
|
5407
|
+
}[];
|
|
5408
|
+
};
|
|
5409
|
+
/**
|
|
5410
|
+
* Optional LLM-assist completion. Invoked by `parseQueryWithLlmAssist`
|
|
5411
|
+
* when the rule-based parse confidence is below `LLM_ASSIST_THRESHOLD`.
|
|
5412
|
+
* The classifier sees the query plus the partial parse and may return
|
|
5413
|
+
* `null` (no completion) or a partial-shape patch the caller merges.
|
|
5414
|
+
*
|
|
5415
|
+
* The runtime caller (operator-chat-service) MUST route this through
|
|
5416
|
+
* the substrate selector at the `concierge` surface so no new outbound
|
|
5417
|
+
* channel opens. See Castle-walking discipline at the top of the file.
|
|
5418
|
+
*/
|
|
5419
|
+
type LlmAssistGrammarCompletion = (query: string, partial: ParsedQuery) => Promise<Partial<{
|
|
5420
|
+
time_range: ParsedTimeRange;
|
|
5421
|
+
agent_names: string[];
|
|
5422
|
+
event_types: string[];
|
|
5423
|
+
}> | null>;
|
|
5424
|
+
|
|
5425
|
+
/**
|
|
5426
|
+
* Sanctuary MCP Server, Operator Chat, Concierge Dynamic Context Router
|
|
5427
|
+
*
|
|
5428
|
+
* WP-V1.3-9 Tau-3 (criterion (c) of the v1.3 scope-lock revision). Builds
|
|
5429
|
+
* on Tau-1 (PR #164, concierge memory store) and Tau-2 (PR #166, multi-turn
|
|
5430
|
+
* coherence read-fold path). At each operator turn the concierge fetches
|
|
5431
|
+
* live data relevant to the operator's query and folds it into context
|
|
5432
|
+
* between the static Sanctuary reference and the prior-turns fold.
|
|
5433
|
+
*
|
|
5434
|
+
* Tau-4 (PR #_, criterion (d)) layers an operator-query grammar on top:
|
|
5435
|
+
* the caller (`operator-chat-service`) parses the query into a
|
|
5436
|
+
* `ParsedQuery` via `concierge-query-grammar`, threads the result into
|
|
5437
|
+
* `classifyQuery` + `foldContext`, and the matches carry the parsed
|
|
5438
|
+
* grammar through to fetchers as `FetcherHints` (time range + agent
|
|
5439
|
+
* names + event types). Existing fetcher implementations that do not
|
|
5440
|
+
* read the second argument continue to work unchanged.
|
|
5441
|
+
*
|
|
5442
|
+
* The module is intentionally pure: keyword classification + a small
|
|
5443
|
+
* amount of hint extraction + a fold orchestrator that calls caller-
|
|
5444
|
+
* provided fetchers. The wiring layer assembles the concrete fetchers
|
|
5445
|
+
* (templates registry, agent registry, audit log, sentinel store, etc.)
|
|
5446
|
+
* so this module stays test-isolatable and composable.
|
|
5447
|
+
*
|
|
5448
|
+
* Castle-walking discipline:
|
|
5449
|
+
* - No new outbound surface; the optional auxiliary classifier is invoked
|
|
5450
|
+
* through the substrate selector by the caller (the service threads the
|
|
5451
|
+
* classifier callback through, this module never owns the channel).
|
|
5452
|
+
* - Encryption boundaries hold for the underlying data sources; this
|
|
5453
|
+
* module receives plain strings already produced inside the fortress.
|
|
5454
|
+
* - Failure-mode handling is fail-soft: a fetcher failure omits its
|
|
5455
|
+
* category from the fold and surfaces an audit event through the
|
|
5456
|
+
* caller's onFetcherFailure hook. The user-facing concierge query is
|
|
5457
|
+
* never broken by a context-assembly failure.
|
|
5458
|
+
*
|
|
5459
|
+
* Out of scope here (Tau-5):
|
|
5460
|
+
* - Agent-context awareness (proactive folding without an explicit
|
|
5461
|
+
* query). The router still routes off explicit query content only.
|
|
5462
|
+
*/
|
|
5463
|
+
|
|
5318
5464
|
/**
|
|
5319
5465
|
* Closed enum of categories the router may classify a query into. Adding
|
|
5320
5466
|
* a category requires updating: this enum, the keyword table, the
|
|
5321
5467
|
* fetcher interface, every wiring fetcher, and the test surface.
|
|
5322
5468
|
*/
|
|
5323
5469
|
type ContextCategory = "templates" | "agent_state" | "agent_activity" | "audit_log" | "sentinel_findings" | "anomaly_alerts" | "recent_receipts" | "verascore_deltas";
|
|
5470
|
+
/**
|
|
5471
|
+
* Structured hints passed to fetchers as the optional second argument
|
|
5472
|
+
* (Tau-4). Derived from `ParsedQuery` by the router; fetchers are free
|
|
5473
|
+
* to read or ignore individual fields. Tau-3 fetchers that take no
|
|
5474
|
+
* second argument continue to work because the slot is optional.
|
|
5475
|
+
*/
|
|
5476
|
+
interface FetcherHints {
|
|
5477
|
+
/** Time range derived from the operator's query, when one resolved. */
|
|
5478
|
+
time_range?: {
|
|
5479
|
+
start: Date;
|
|
5480
|
+
end: Date;
|
|
5481
|
+
relative_label?: string;
|
|
5482
|
+
};
|
|
5483
|
+
/**
|
|
5484
|
+
* All agent names the parser matched against the registry, in the
|
|
5485
|
+
* order they surfaced. Distinct from the `agentNameHint` first arg
|
|
5486
|
+
* (which carries only the first match for back-compat).
|
|
5487
|
+
*/
|
|
5488
|
+
agent_names?: readonly string[];
|
|
5489
|
+
/**
|
|
5490
|
+
* Event-type names the parser matched against the canonical audit-
|
|
5491
|
+
* event-class enumeration. Already filtered to the closed set.
|
|
5492
|
+
*/
|
|
5493
|
+
event_types?: readonly string[];
|
|
5494
|
+
}
|
|
5324
5495
|
/**
|
|
5325
5496
|
* Caller-supplied data sources. Each fetcher returns plain text the
|
|
5326
5497
|
* router stitches into the rendered section. Returning the empty string
|
|
5327
5498
|
* (or whitespace only) is treated as "nothing to fold for this category"
|
|
5328
5499
|
* and the category drops out of the final categoriesIncluded list.
|
|
5500
|
+
*
|
|
5501
|
+
* Tau-4 extends every fetcher with an optional `hints?: FetcherHints`
|
|
5502
|
+
* argument carrying the parsed-query attachments (time range, agent
|
|
5503
|
+
* names, event types). Fetchers that ignore the argument continue to
|
|
5504
|
+
* compile; fetchers that consume it can scope queries against the
|
|
5505
|
+
* audit log, activity feed, etc., to the operator's stated window.
|
|
5329
5506
|
*/
|
|
5330
5507
|
interface ContextFetchers {
|
|
5331
|
-
templates: () => Promise<string>;
|
|
5332
|
-
agent_state: (agentNameHint: string | null) => Promise<string>;
|
|
5333
|
-
agent_activity: (agentNameHint: string | null) => Promise<string>;
|
|
5334
|
-
audit_log: () => Promise<string>;
|
|
5335
|
-
sentinel_findings: () => Promise<string>;
|
|
5336
|
-
anomaly_alerts: () => Promise<string>;
|
|
5337
|
-
recent_receipts: () => Promise<string>;
|
|
5338
|
-
verascore_deltas: () => Promise<string>;
|
|
5508
|
+
templates: (hints?: FetcherHints) => Promise<string>;
|
|
5509
|
+
agent_state: (agentNameHint: string | null, hints?: FetcherHints) => Promise<string>;
|
|
5510
|
+
agent_activity: (agentNameHint: string | null, hints?: FetcherHints) => Promise<string>;
|
|
5511
|
+
audit_log: (hints?: FetcherHints) => Promise<string>;
|
|
5512
|
+
sentinel_findings: (hints?: FetcherHints) => Promise<string>;
|
|
5513
|
+
anomaly_alerts: (hints?: FetcherHints) => Promise<string>;
|
|
5514
|
+
recent_receipts: (hints?: FetcherHints) => Promise<string>;
|
|
5515
|
+
verascore_deltas: (hints?: FetcherHints) => Promise<string>;
|
|
5339
5516
|
}
|
|
5340
5517
|
/**
|
|
5341
5518
|
* Auxiliary classifier the caller may supply for the LLM-assist fallback
|
|
@@ -5344,6 +5521,213 @@ interface ContextFetchers {
|
|
|
5344
5521
|
*/
|
|
5345
5522
|
type LlmAssistClassifier = (query: string, categories: readonly ContextCategory[]) => Promise<ContextCategory | "none">;
|
|
5346
5523
|
|
|
5524
|
+
/**
|
|
5525
|
+
* Sanctuary MCP Server, Operator Chat, Agent-Context Cache
|
|
5526
|
+
*
|
|
5527
|
+
* WP-V1.3-9 Tau-5 (criterion (e) of the v1.3 scope-lock revision; closes
|
|
5528
|
+
* WP-V1.3-9 Conversational Sovereignty Depth). Builds on Tau-1 (memory
|
|
5529
|
+
* persistence), Tau-2 (multi-turn coherence), Tau-3 (8-category dynamic
|
|
5530
|
+
* context routing), and Tau-4 (operator-query grammar). Tau-5 ships
|
|
5531
|
+
* proactive context awareness: the concierge knows which wrapped agents
|
|
5532
|
+
* the operator runs, their templates, current work, recent audit-log
|
|
5533
|
+
* activity, recent commitment chains, and recent Verascore deltas, and
|
|
5534
|
+
* folds that snapshot into every chat round-trip plus a starter
|
|
5535
|
+
* suggestion when a fresh thread opens.
|
|
5536
|
+
*
|
|
5537
|
+
* Architecture:
|
|
5538
|
+
*
|
|
5539
|
+
* - The cache is per-fortress: one `AgentContextCache` instance per
|
|
5540
|
+
* `OperatorChatService` instance. The service-construction layer in
|
|
5541
|
+
* `dashboard/v1_1/wiring.ts` wires the cache to the same agent
|
|
5542
|
+
* registry + audit log it already passes to the Tau-3 fetchers, so
|
|
5543
|
+
* data sources align across both surfaces.
|
|
5544
|
+
*
|
|
5545
|
+
* - Refresh is lazy: `refresh()` returns the latest snapshot list and
|
|
5546
|
+
* updates the in-memory cache. `read()` is a synchronous accessor for
|
|
5547
|
+
* the latest cached snapshot (returns `[]` until the first refresh
|
|
5548
|
+
* resolves).
|
|
5549
|
+
*
|
|
5550
|
+
* - Cadence: callers schedule refreshes via `start()` (60-second timer)
|
|
5551
|
+
* and trigger an explicit refresh on cocoon-unlock by calling
|
|
5552
|
+
* `refresh()` once at construction. Tests can opt out of the timer
|
|
5553
|
+
* by leaving `start()` uncalled.
|
|
5554
|
+
*
|
|
5555
|
+
* Castle-walking discipline:
|
|
5556
|
+
*
|
|
5557
|
+
* - No new outbound surface. The cache reads server-local data only:
|
|
5558
|
+
* the agent registry (in-memory) and the audit log (encrypted under
|
|
5559
|
+
* L1 master key at rest). Optional Verascore receipt cache is read
|
|
5560
|
+
* through a caller-provided source; absent, the verascore field
|
|
5561
|
+
* degrades to null, never to an outbound HTTP call.
|
|
5562
|
+
*
|
|
5563
|
+
* - Encryption boundary held: the cache holds plain text already
|
|
5564
|
+
* produced inside the fortress. Audit-log decryption happens inside
|
|
5565
|
+
* the audit-log subsystem, not here.
|
|
5566
|
+
*
|
|
5567
|
+
* - Failure-mode handling is fail-soft: a refresh that throws (audit
|
|
5568
|
+
* log read error, registry contention, oversize bundle) leaves the
|
|
5569
|
+
* prior snapshot list intact and returns `[]` from the failed
|
|
5570
|
+
* refresh. The caller can emit an audit event; the concierge
|
|
5571
|
+
* degrades gracefully to no agent-state section. The user-facing
|
|
5572
|
+
* query is never broken by a cache-refresh failure.
|
|
5573
|
+
*
|
|
5574
|
+
* Privacy invariant:
|
|
5575
|
+
*
|
|
5576
|
+
* Snapshots carry agent_id, harness, template, and aggregate counts.
|
|
5577
|
+
* They MUST NOT carry raw audit-log slices, raw operator queries, or
|
|
5578
|
+
* raw secret material. Counts are integers; current_work_summary is a
|
|
5579
|
+
* stable enum-derived string ("performed policy_change", "wrote to
|
|
5580
|
+
* state"); state_flags is a closed enum. The "Current agent state"
|
|
5581
|
+
* section the concierge renders into the substrate prompt aggregates
|
|
5582
|
+
* those fields only.
|
|
5583
|
+
*
|
|
5584
|
+
* Out of scope here (v1.4+ and later):
|
|
5585
|
+
*
|
|
5586
|
+
* - Cross-fortress agent state aggregation (v1.4 cross-tenant scope).
|
|
5587
|
+
* - Concierge-driven action execution (operator: "fix Cursor's
|
|
5588
|
+
* session"). Defer to v1.5+ pending approval-flow trust model.
|
|
5589
|
+
* - Advanced ML-backed state inference (per-operator agent-state norms).
|
|
5590
|
+
* Defer to v1.5+; out of scope for v1.3.
|
|
5591
|
+
*/
|
|
5592
|
+
|
|
5593
|
+
/**
|
|
5594
|
+
* Stable enum of agent state flags surfaced into the substrate prompt
|
|
5595
|
+
* and the proactive-suggestion trigger. The order in `STATE_FLAG_ORDER`
|
|
5596
|
+
* encodes urgency: flags earlier in the list rank higher when the
|
|
5597
|
+
* prompt-section pruner has to drop low-urgency agents to fit budget.
|
|
5598
|
+
*/
|
|
5599
|
+
type AgentStateFlag = "stuck" | "has_pending_approvals" | "has_open_findings" | "active" | "idle";
|
|
5600
|
+
/**
|
|
5601
|
+
* One snapshot entry per wrapped agent. Stable shape so the prompt
|
|
5602
|
+
* formatter and the proactive-suggestion generator can read off the
|
|
5603
|
+
* same data without re-fetching from the registry.
|
|
5604
|
+
*
|
|
5605
|
+
* Counts (`recent_audit_count_24h`, `recent_egress_count_24h`,
|
|
5606
|
+
* `recent_concordia_receipts_count_24h`) are integers derived from the
|
|
5607
|
+
* last-24h slice of the audit log. `recent_verascore_delta_24h` is
|
|
5608
|
+
* `null` when the optional Verascore source is not wired or returned
|
|
5609
|
+
* no delta.
|
|
5610
|
+
*/
|
|
5611
|
+
interface AgentContextSnapshot {
|
|
5612
|
+
agent_id: string;
|
|
5613
|
+
agent_name: string;
|
|
5614
|
+
template: string;
|
|
5615
|
+
last_activity_at: string | null;
|
|
5616
|
+
current_work_summary: string | null;
|
|
5617
|
+
recent_audit_count_24h: number;
|
|
5618
|
+
recent_egress_count_24h: number;
|
|
5619
|
+
recent_concordia_receipts_count_24h: number;
|
|
5620
|
+
recent_verascore_delta_24h: number | null;
|
|
5621
|
+
state_flags: AgentStateFlag[];
|
|
5622
|
+
}
|
|
5623
|
+
/**
|
|
5624
|
+
* Optional source for the Verascore reputation delta surface. v1.0
|
|
5625
|
+
* Sanctuary does not currently expose a queryable Verascore receipt
|
|
5626
|
+
* cache server-side; the source is wired in on Verascore-bridge-aware
|
|
5627
|
+
* deployments and absent everywhere else. When absent, the snapshot's
|
|
5628
|
+
* `recent_verascore_delta_24h` field is null. The concierge functions
|
|
5629
|
+
* fully without it (non-dependency invariant).
|
|
5630
|
+
*/
|
|
5631
|
+
interface VerascoreDeltaSource {
|
|
5632
|
+
/**
|
|
5633
|
+
* Return the most-recent 24h reputation delta for this agent, or
|
|
5634
|
+
* null when no delta is available. Implementations MUST be fast
|
|
5635
|
+
* (server-local, no outbound) so the cache refresh stays bounded.
|
|
5636
|
+
*/
|
|
5637
|
+
read(agentId: string): number | null;
|
|
5638
|
+
}
|
|
5639
|
+
/**
|
|
5640
|
+
* One handler registration's removal callback.
|
|
5641
|
+
*/
|
|
5642
|
+
type AgentContextChangeUnsubscribe = () => void;
|
|
5643
|
+
interface AgentContextCacheDeps {
|
|
5644
|
+
/** Operator identity that owns the agents this cache aggregates over. */
|
|
5645
|
+
identityId: string;
|
|
5646
|
+
/** Source of agent records, scoped to this operator's identity. */
|
|
5647
|
+
agentRegistry: HubAgentRegistrySource;
|
|
5648
|
+
/** Audit log used to derive recent-activity counts and state flags. */
|
|
5649
|
+
auditLog: AuditLog;
|
|
5650
|
+
/** Optional Verascore delta source. When absent, deltas degrade to null. */
|
|
5651
|
+
verascoreSource?: VerascoreDeltaSource;
|
|
5652
|
+
/** Hook invoked when a refresh throws. Defaults to a no-op. */
|
|
5653
|
+
onRefreshError?: (error: unknown) => void;
|
|
5654
|
+
/** Deterministic clock for tests. Defaults to `Date.now`. */
|
|
5655
|
+
clock?: () => number;
|
|
5656
|
+
/** Refresh cadence in ms. Defaults to 60_000. */
|
|
5657
|
+
refreshCadenceMs?: number;
|
|
5658
|
+
/**
|
|
5659
|
+
* Window in ms used to derive 24h counts. Defaults to 24h. Tunable
|
|
5660
|
+
* for tests so they can simulate older entries falling out of the
|
|
5661
|
+
* window without manipulating the clock.
|
|
5662
|
+
*/
|
|
5663
|
+
windowMs?: number;
|
|
5664
|
+
}
|
|
5665
|
+
/**
|
|
5666
|
+
* Per-fortress in-memory cache of agent context snapshots. See
|
|
5667
|
+
* file-level docstring for the invariants this class upholds.
|
|
5668
|
+
*/
|
|
5669
|
+
declare class AgentContextCache {
|
|
5670
|
+
private readonly identityId;
|
|
5671
|
+
private readonly agentRegistry;
|
|
5672
|
+
private readonly auditLog;
|
|
5673
|
+
private readonly verascoreSource;
|
|
5674
|
+
private readonly onRefreshError;
|
|
5675
|
+
private readonly clock;
|
|
5676
|
+
private readonly cadenceMs;
|
|
5677
|
+
private readonly windowMs;
|
|
5678
|
+
private snapshots;
|
|
5679
|
+
private observers;
|
|
5680
|
+
private timer;
|
|
5681
|
+
private inFlight;
|
|
5682
|
+
constructor(deps: AgentContextCacheDeps);
|
|
5683
|
+
/**
|
|
5684
|
+
* Run the refresh cycle. Returns the new snapshot list (which is
|
|
5685
|
+
* also stored as the cache state). On error, the prior snapshots
|
|
5686
|
+
* are kept and the resolved promise carries `[]`. The error is
|
|
5687
|
+
* dispatched to the `onRefreshError` hook so the caller can audit-
|
|
5688
|
+
* emit a degradation event.
|
|
5689
|
+
*/
|
|
5690
|
+
refresh(): Promise<AgentContextSnapshot[]>;
|
|
5691
|
+
/**
|
|
5692
|
+
* Synchronous read of the latest cached snapshot list. Returns `[]`
|
|
5693
|
+
* until the first `refresh()` resolves, or after a failure that left
|
|
5694
|
+
* the cache empty.
|
|
5695
|
+
*/
|
|
5696
|
+
read(): AgentContextSnapshot[];
|
|
5697
|
+
/**
|
|
5698
|
+
* Subscribe to refresh notifications. The handler fires once per
|
|
5699
|
+
* successful refresh with the new snapshot list. Returns an
|
|
5700
|
+
* unsubscribe callback the caller invokes on teardown.
|
|
5701
|
+
*/
|
|
5702
|
+
observeChanges(handler: (snapshots: AgentContextSnapshot[]) => void): AgentContextChangeUnsubscribe;
|
|
5703
|
+
/**
|
|
5704
|
+
* Start the periodic refresh timer. Idempotent. Tests typically do
|
|
5705
|
+
* not call this and trigger refreshes manually.
|
|
5706
|
+
*/
|
|
5707
|
+
start(): void;
|
|
5708
|
+
/**
|
|
5709
|
+
* Stop the periodic refresh timer. Idempotent.
|
|
5710
|
+
*/
|
|
5711
|
+
stop(): void;
|
|
5712
|
+
private notifyObservers;
|
|
5713
|
+
}
|
|
5714
|
+
/**
|
|
5715
|
+
* Stable enum of the trigger classes the starter generator surfaces.
|
|
5716
|
+
* Carried into the audit emission so dashboards can group by cause.
|
|
5717
|
+
*/
|
|
5718
|
+
type ProactiveStarterTrigger = "stuck_agent" | "pending_approvals" | "open_findings" | "all_idle";
|
|
5719
|
+
interface ConciergeProactiveStarter {
|
|
5720
|
+
text: string;
|
|
5721
|
+
trigger: ProactiveStarterTrigger;
|
|
5722
|
+
/**
|
|
5723
|
+
* How many agents contributed to this trigger. For `stuck_agent` and
|
|
5724
|
+
* `open_findings` this is the count of agents in that state; for
|
|
5725
|
+
* `pending_approvals` it is the count of agents with pending; for
|
|
5726
|
+
* `all_idle` it is the total agent count.
|
|
5727
|
+
*/
|
|
5728
|
+
triggered_agents_count: number;
|
|
5729
|
+
}
|
|
5730
|
+
|
|
5347
5731
|
/**
|
|
5348
5732
|
* Sanctuary MCP Server — Operator Chat Service
|
|
5349
5733
|
*
|
|
@@ -5514,6 +5898,41 @@ interface OperatorChatServiceDeps {
|
|
|
5514
5898
|
* Defaults to `DEFAULT_CONCIERGE_DYNAMIC_CONTEXT_BUDGET` (2000).
|
|
5515
5899
|
*/
|
|
5516
5900
|
conciergeDynamicContextBudget?: number;
|
|
5901
|
+
/**
|
|
5902
|
+
* WP-V1.3-9 Tau-4: read-only registry view consumed by the operator-
|
|
5903
|
+
* query grammar to extract agent-name parameters. Wired from
|
|
5904
|
+
* `HubAgentRegistrySource.list()` at the hub-service layer; absent
|
|
5905
|
+
* services skip agent-name extraction (the grammar still resolves
|
|
5906
|
+
* time ranges and event types). The registry is read at every
|
|
5907
|
+
* `sendConcierge` call so newly-wrapped agents are visible without
|
|
5908
|
+
* service reconstruction.
|
|
5909
|
+
*/
|
|
5910
|
+
conciergeAgentRegistry?: AgentRegistryView;
|
|
5911
|
+
/**
|
|
5912
|
+
* WP-V1.3-9 Tau-4: optional LLM-assist completion for low-confidence
|
|
5913
|
+
* grammar parses. When wired alongside the substrate selector, the
|
|
5914
|
+
* service routes the call through the substrate selector at the
|
|
5915
|
+
* `concierge` surface, so the grammar fallback shares the operator's
|
|
5916
|
+
* substrate choice and never opens a new outbound surface. Distinct
|
|
5917
|
+
* from `conciergeContextLlmAssist`, which classifies queries into
|
|
5918
|
+
* fetcher categories; this hook completes structured grammar fields.
|
|
5919
|
+
*/
|
|
5920
|
+
conciergeGrammarLlmAssist?: LlmAssistGrammarCompletion;
|
|
5921
|
+
/**
|
|
5922
|
+
* WP-V1.3-9 Tau-5: caller-supplied agent-context cache. When wired
|
|
5923
|
+
* alongside the substrate selector, every `sendConcierge` round-trip
|
|
5924
|
+
* folds a "Current agent state" section into the substrate prompt
|
|
5925
|
+
* (between the dynamic-context fold and the prior-conversation
|
|
5926
|
+
* fold), and `getProactiveStarter()` surfaces a fresh-thread starter
|
|
5927
|
+
* based on the cache state. Omit to disable agent-context awareness;
|
|
5928
|
+
* the rest of the concierge surface keeps working unchanged.
|
|
5929
|
+
*/
|
|
5930
|
+
conciergeAgentContextCache?: AgentContextCache;
|
|
5931
|
+
/**
|
|
5932
|
+
* WP-V1.3-9 Tau-5: rough token budget for the "Current agent state"
|
|
5933
|
+
* section. Defaults to `DEFAULT_CONCIERGE_AGENT_STATE_BUDGET` (400).
|
|
5934
|
+
*/
|
|
5935
|
+
conciergeAgentStateBudget?: number;
|
|
5517
5936
|
}
|
|
5518
5937
|
declare class OperatorChatService {
|
|
5519
5938
|
private store;
|
|
@@ -5532,6 +5951,17 @@ declare class OperatorChatService {
|
|
|
5532
5951
|
private contextFetchers?;
|
|
5533
5952
|
private contextLlmAssist?;
|
|
5534
5953
|
private dynamicContextBudget;
|
|
5954
|
+
private agentRegistry?;
|
|
5955
|
+
private grammarLlmAssist?;
|
|
5956
|
+
private agentContextCache?;
|
|
5957
|
+
private agentStateBudget;
|
|
5958
|
+
/**
|
|
5959
|
+
* Per-thread guard so the proactive starter fires at most once per
|
|
5960
|
+
* fresh thread. Tracks the thread_id the starter was last offered
|
|
5961
|
+
* for; subsequent `getProactiveStarter()` calls within the same
|
|
5962
|
+
* thread return null instead of re-emitting.
|
|
5963
|
+
*/
|
|
5964
|
+
private starterOfferedForThreadId?;
|
|
5535
5965
|
/**
|
|
5536
5966
|
* In-memory thread_id assigned to the active concierge session.
|
|
5537
5967
|
* The first sendConcierge call after construction allocates a fresh
|
|
@@ -5597,8 +6027,37 @@ declare class OperatorChatService {
|
|
|
5597
6027
|
* Reset the active session memory thread. Subsequent sendConcierge
|
|
5598
6028
|
* calls allocate a fresh thread_id. Surfaced for tests + future "new
|
|
5599
6029
|
* conversation" affordance; not currently called by the dashboard.
|
|
6030
|
+
*
|
|
6031
|
+
* Tau-5: also clears the proactive-starter guard so the next
|
|
6032
|
+
* `getProactiveStarter()` call against the freshly-allocated thread
|
|
6033
|
+
* is eligible to fire.
|
|
5600
6034
|
*/
|
|
5601
6035
|
resetConciergeMemoryThread(): void;
|
|
6036
|
+
/**
|
|
6037
|
+
* WP-V1.3-9 Tau-5: surface a proactive starter for the current
|
|
6038
|
+
* concierge session. Intended to be called by the dashboard UI when
|
|
6039
|
+
* the operator opens the chat surface, before any operator typing.
|
|
6040
|
+
*
|
|
6041
|
+
* Returns null when:
|
|
6042
|
+
* - No agent-context cache is wired (Tau-5 disabled).
|
|
6043
|
+
* - No concierge memory store is wired (no thread_id namespace).
|
|
6044
|
+
* - The cache snapshot has no signal (empty fortress).
|
|
6045
|
+
* - A starter has already been offered for the active thread (the
|
|
6046
|
+
* guard ensures one starter per fresh thread).
|
|
6047
|
+
*
|
|
6048
|
+
* Side effects:
|
|
6049
|
+
* - Allocates a fresh thread_id if none is active.
|
|
6050
|
+
* - Emits the `operator_concierge_proactive_suggestion_offered`
|
|
6051
|
+
* audit event with the trigger class + triggered_agents_count.
|
|
6052
|
+
* - Records the offered thread_id so the next call within the same
|
|
6053
|
+
* thread is a no-op.
|
|
6054
|
+
*
|
|
6055
|
+
* The returned starter's `text` is operator-visible copy; the
|
|
6056
|
+
* dashboard renders it as a system-message-style starter the
|
|
6057
|
+
* operator can accept (clicks/types follow-up) or dismiss (types a
|
|
6058
|
+
* new query).
|
|
6059
|
+
*/
|
|
6060
|
+
getProactiveStarter(): ConciergeProactiveStarter | null;
|
|
5602
6061
|
private ensureActiveMemoryThread;
|
|
5603
6062
|
/**
|
|
5604
6063
|
* Stitch fortress state into a single context blob the substrate
|
|
@@ -5646,8 +6105,20 @@ declare class OperatorChatService {
|
|
|
5646
6105
|
* proceeds with no fold. Returns the rendered section + the list of
|
|
5647
6106
|
* categories whose data made it into the section (used for the
|
|
5648
6107
|
* round-trip audit emission).
|
|
6108
|
+
*
|
|
6109
|
+
* Tau-4: receives the pre-parsed `ParsedQuery` and forwards it as the
|
|
6110
|
+
* `parsed` opt to `foldContext`, so fetchers see the structured
|
|
6111
|
+
* `FetcherHints` derived from it.
|
|
5649
6112
|
*/
|
|
5650
6113
|
private runDynamicContextFold;
|
|
6114
|
+
/**
|
|
6115
|
+
* WP-V1.3-9 Tau-4: parse the (PII-filtered) operator query into a
|
|
6116
|
+
* `ParsedQuery`. Routes through the LLM-assist completion hook when
|
|
6117
|
+
* configured and the rule-based parse is below
|
|
6118
|
+
* `LLM_ASSIST_THRESHOLD`. Always returns a parse object (never
|
|
6119
|
+
* throws) so the audit emission can carry the result unconditionally.
|
|
6120
|
+
*/
|
|
6121
|
+
private runGrammarParse;
|
|
5651
6122
|
/**
|
|
5652
6123
|
* Emit the WP-V1.3-9 Tau-3 fetcher-failure audit event. Pulled out
|
|
5653
6124
|
* of the fold path so the dynamic-context handler stays readable.
|
|
@@ -6329,6 +6800,22 @@ interface AggregatedApproval {
|
|
|
6329
6800
|
* Upsilon-3.
|
|
6330
6801
|
*/
|
|
6331
6802
|
enforcement_chain?: EnforcementLayerEvent[];
|
|
6803
|
+
/**
|
|
6804
|
+
* Aggregator revision number stamped at entry creation. Stable across
|
|
6805
|
+
* status transitions. Used by the v1.4 mobile companion sync API to
|
|
6806
|
+
* separate "added" from "changed" deltas (created_at_revision >
|
|
6807
|
+
* sinceRevision means added; otherwise it is a status change).
|
|
6808
|
+
* v1.3 Upsilon-4.
|
|
6809
|
+
*/
|
|
6810
|
+
created_at_revision?: number;
|
|
6811
|
+
/**
|
|
6812
|
+
* Aggregator revision number stamped at the last mutation (creation,
|
|
6813
|
+
* resolution, expiration). Sync-API consumers compare against the
|
|
6814
|
+
* revision they last saw to compute the delta. Monotonically
|
|
6815
|
+
* increasing per fortress; gaps are normal (one bump per mutation).
|
|
6816
|
+
* v1.3 Upsilon-4.
|
|
6817
|
+
*/
|
|
6818
|
+
last_modified_revision?: number;
|
|
6332
6819
|
}
|
|
6333
6820
|
/**
|
|
6334
6821
|
* Source-context resolver. Called once per ingest to map gate context to
|
|
@@ -6385,11 +6872,35 @@ type ApprovalAggregatorUnsubscribe = () => void;
|
|
|
6385
6872
|
interface ApprovalAggregatorEmit {
|
|
6386
6873
|
/**
|
|
6387
6874
|
* `aggregated` on first ingest, `resolved` on a status leaving pending,
|
|
6388
|
-
* `deduped` when an ingest dropped because the same dedup key was seen
|
|
6875
|
+
* `deduped` when an ingest dropped because the same dedup key was seen,
|
|
6876
|
+
* `removed` when an entry was pruned (v1.3 Upsilon-4).
|
|
6389
6877
|
*/
|
|
6390
|
-
type: "aggregated" | "resolved" | "deduped";
|
|
6878
|
+
type: "aggregated" | "resolved" | "deduped" | "removed";
|
|
6391
6879
|
entry: AggregatedApproval;
|
|
6392
6880
|
}
|
|
6881
|
+
/**
|
|
6882
|
+
* Sync-API delta returned by `getSync()`. v1.3 Upsilon-4. Mobile
|
|
6883
|
+
* companions poll this for cheap state-sync without re-fetching the
|
|
6884
|
+
* full inbox. The `revision` field on the response is the aggregator's
|
|
6885
|
+
* current revision; pass it back as `sinceRevision` on the next call.
|
|
6886
|
+
*
|
|
6887
|
+
* `added` carries entries created after `sinceRevision`. `changed`
|
|
6888
|
+
* carries entries that existed at `sinceRevision` but have transitioned
|
|
6889
|
+
* status (resolved, expired) since. `removed` carries the
|
|
6890
|
+
* aggregator_ids of entries pruned after `sinceRevision`.
|
|
6891
|
+
*
|
|
6892
|
+
* Tombstone caveat: removal tombstones live in-process memory. Server
|
|
6893
|
+
* restart clears them. Mobile clients reconnecting after the server
|
|
6894
|
+
* restarted MUST re-bootstrap from `list()` rather than rely on the
|
|
6895
|
+
* sync delta. The v1.4 mobile companion build wires the bootstrap-on-
|
|
6896
|
+
* reconnect flow; v1.3 documents the constraint.
|
|
6897
|
+
*/
|
|
6898
|
+
interface ApprovalAggregatorSyncDelta {
|
|
6899
|
+
revision: number;
|
|
6900
|
+
added: AggregatedApproval[];
|
|
6901
|
+
changed: AggregatedApproval[];
|
|
6902
|
+
removed: string[];
|
|
6903
|
+
}
|
|
6393
6904
|
/**
|
|
6394
6905
|
* Constructor dependencies. `pendingTtlMs` and `maxListLimit` default to
|
|
6395
6906
|
* coordinator-CTO defaults; tests pass overrides for deterministic timing.
|
|
@@ -6464,12 +6975,63 @@ declare class ApprovalAggregator {
|
|
|
6464
6975
|
private hydrated;
|
|
6465
6976
|
/** Active SSE listeners. */
|
|
6466
6977
|
private readonly listeners;
|
|
6978
|
+
/**
|
|
6979
|
+
* Monotonic revision counter, bumped on every mutation (ingest of new
|
|
6980
|
+
* entry, resolve, expire, delete). Hydrated from max(last_modified_revision)
|
|
6981
|
+
* across persisted entries on first read; in-memory after that. v1.3
|
|
6982
|
+
* Upsilon-4.
|
|
6983
|
+
*/
|
|
6984
|
+
private currentRevision;
|
|
6985
|
+
/**
|
|
6986
|
+
* Removal tombstones: aggregator_id -> revision at removal. Used by the
|
|
6987
|
+
* sync API to surface "removed" entries to mobile consumers between
|
|
6988
|
+
* polls. In-memory only; server restart clears tombstones (mobile
|
|
6989
|
+
* bootstraps via `list()` on reconnect). v1.3 Upsilon-4.
|
|
6990
|
+
*/
|
|
6991
|
+
private readonly removedTombstones;
|
|
6467
6992
|
constructor(deps: ApprovalAggregatorDeps);
|
|
6468
6993
|
/**
|
|
6469
6994
|
* Subscribe an event listener. Returns an unsubscribe fn. SSE handlers
|
|
6470
6995
|
* use this to forward aggregator emissions to the dashboard.
|
|
6471
6996
|
*/
|
|
6472
6997
|
onEvent(listener: (event: ApprovalAggregatorEmit) => void): ApprovalAggregatorUnsubscribe;
|
|
6998
|
+
/**
|
|
6999
|
+
* Current aggregator revision. v1.3 Upsilon-4. Mobile companions
|
|
7000
|
+
* poll the lightweight `/revision` route to detect that something
|
|
7001
|
+
* changed before fetching a full sync delta.
|
|
7002
|
+
*/
|
|
7003
|
+
getRevision(): Promise<number>;
|
|
7004
|
+
/**
|
|
7005
|
+
* Compute a delta since `sinceRevision`. v1.3 Upsilon-4. Mobile
|
|
7006
|
+
* clients poll this for cheap state-sync. Behavior:
|
|
7007
|
+
* - `added`: entries whose `created_at_revision > sinceRevision`.
|
|
7008
|
+
* - `changed`: entries that existed at `sinceRevision` but had a
|
|
7009
|
+
* status transition (resolve, expire) since.
|
|
7010
|
+
* - `removed`: aggregator_ids deleted after `sinceRevision`.
|
|
7011
|
+
* - `revision`: current aggregator revision; pass this back as
|
|
7012
|
+
* `sinceRevision` on the next call.
|
|
7013
|
+
*
|
|
7014
|
+
* `limit` caps the total count returned across all three lists,
|
|
7015
|
+
* prioritized as added -> changed -> removed (newer-state first).
|
|
7016
|
+
* When more changes exist than fit, the next call with the returned
|
|
7017
|
+
* revision will pick up the rest because each entry's
|
|
7018
|
+
* last_modified_revision is unchanged by truncation.
|
|
7019
|
+
*/
|
|
7020
|
+
getSync(opts?: {
|
|
7021
|
+
sinceRevision?: number;
|
|
7022
|
+
limit?: number;
|
|
7023
|
+
}): Promise<ApprovalAggregatorSyncDelta>;
|
|
7024
|
+
/**
|
|
7025
|
+
* Delete an entry. Drops the in-memory record, the persisted bundle,
|
|
7026
|
+
* and the at-rest payload (if a payload store is wired). Records a
|
|
7027
|
+
* tombstone with the new revision so sync-API consumers see a
|
|
7028
|
+
* `removed` delta. Returns true when an entry was deleted, false on
|
|
7029
|
+
* unknown id. v1.3 Upsilon-4. Reserved for v1.4+ retention housekeeping;
|
|
7030
|
+
* Upsilon-4 ships the surface so mobile sync-API tests can exercise the
|
|
7031
|
+
* removal path.
|
|
7032
|
+
*/
|
|
7033
|
+
deleteEntry(aggregatorId: string): Promise<boolean>;
|
|
7034
|
+
private nextRevision;
|
|
6473
7035
|
/**
|
|
6474
7036
|
* Ingest a gate event. Returns the aggregator entry on first sight,
|
|
6475
7037
|
* `null` when deduped. Resolution events update the existing record;
|
|
@@ -6567,6 +7129,385 @@ declare class ApprovalAggregator {
|
|
|
6567
7129
|
private hydrate;
|
|
6568
7130
|
}
|
|
6569
7131
|
|
|
7132
|
+
/**
|
|
7133
|
+
* Sanctuary v1.3 WP-V1.3-1 Sentinel Baseline Pack (Castle Layer 2 anchor)
|
|
7134
|
+
*
|
|
7135
|
+
* Shared types for the Sentinel observation surface.
|
|
7136
|
+
*
|
|
7137
|
+
* Castle-walking discipline:
|
|
7138
|
+
* - Castle Layer 1 (Castle Wall) blocks unauthorized egress at the kernel
|
|
7139
|
+
* boundary. That is the enforcement layer.
|
|
7140
|
+
* - Castle Layer 2 (Sentinels) observes server-local data only and surfaces
|
|
7141
|
+
* anomalies. NO outbound network. NO blocking. Findings flow into the
|
|
7142
|
+
* operator surface they already check.
|
|
7143
|
+
* - Castle Layer 3 (Cooperative MCP) is the agent contract. Sentinels do
|
|
7144
|
+
* not wrap agent tools and do not negotiate with agents.
|
|
7145
|
+
*
|
|
7146
|
+
* Phi-1 ships the framework + the egress-volume watcher. Phi-2 through
|
|
7147
|
+
* Phi-5 add credential-usage, cross-agent-chatter, suspicious-tool-call,
|
|
7148
|
+
* and anomaly-trigger sentinels against this same shape.
|
|
7149
|
+
*/
|
|
7150
|
+
|
|
7151
|
+
/** Severity tier returned by `Sentinel.evaluate()`. */
|
|
7152
|
+
type SentinelSeverity = "info" | "warn" | "alert";
|
|
7153
|
+
/**
|
|
7154
|
+
* Read-only context handed to every sentinel on subscribe + evaluate.
|
|
7155
|
+
* Sentinels do NOT receive a writable storage handle and do NOT receive
|
|
7156
|
+
* an outbound HTTP client. The substrate selector is the single
|
|
7157
|
+
* outbound-capable surface; Phi-1 sentinels do not exercise it but
|
|
7158
|
+
* Phi-2/3/4/5 may want to invoke a local LLM for reasoning.
|
|
7159
|
+
*/
|
|
7160
|
+
interface SentinelContext {
|
|
7161
|
+
/** Stable per-fortress identifier; multi-fortress isolation pivot. */
|
|
7162
|
+
fortressId: string;
|
|
7163
|
+
/** Audit log read API. Sentinels MUST NOT call append() through this. */
|
|
7164
|
+
auditLog: AuditLog;
|
|
7165
|
+
/** Optional substrate selector for LLM-backed reasoning. v1.3 Phi-1 unused. */
|
|
7166
|
+
substrateSelector?: SubstrateSelector;
|
|
7167
|
+
/** Wall-clock provider. Tests inject a deterministic clock. */
|
|
7168
|
+
now: () => Date;
|
|
7169
|
+
}
|
|
7170
|
+
/**
|
|
7171
|
+
* One observation a sentinel produced. Persisted under the sentinel
|
|
7172
|
+
* findings store + emitted as an audit event +
|
|
7173
|
+
* surfaced to the operator dashboard.
|
|
7174
|
+
*/
|
|
7175
|
+
interface SentinelFinding {
|
|
7176
|
+
/** Stable per-fortress finding id (UUID v4). */
|
|
7177
|
+
finding_id: string;
|
|
7178
|
+
/** Identifier of the sentinel that produced this finding. */
|
|
7179
|
+
sentinel_id: string;
|
|
7180
|
+
severity: SentinelSeverity;
|
|
7181
|
+
/**
|
|
7182
|
+
* Optional agent-id this finding pertains to. Absent when the finding
|
|
7183
|
+
* is fortress-wide (e.g. cross-agent chatter spike).
|
|
7184
|
+
*/
|
|
7185
|
+
agent_id?: string;
|
|
7186
|
+
/** Operator-friendly one-liner. Truncated to 240 chars on persist. */
|
|
7187
|
+
summary: string;
|
|
7188
|
+
/**
|
|
7189
|
+
* Structured payload. Schema varies per sentinel but every payload is
|
|
7190
|
+
* a flat record of JSON-serializable values.
|
|
7191
|
+
*/
|
|
7192
|
+
details: Record<string, unknown>;
|
|
7193
|
+
/** ISO 8601 timestamp at evaluation time. */
|
|
7194
|
+
observed_at: string;
|
|
7195
|
+
/**
|
|
7196
|
+
* Audit-log entries that triggered this finding. Sentinels populate
|
|
7197
|
+
* with `${entry.timestamp}:${entry.operation}` tuples (mirrors the
|
|
7198
|
+
* aggregator's audit_log_entry_id shape from Upsilon-1).
|
|
7199
|
+
*/
|
|
7200
|
+
evidence_audit_ids: string[];
|
|
7201
|
+
/** Stable fortress id stamped at emit. Multi-fortress isolation pivot. */
|
|
7202
|
+
fortress_id: string;
|
|
7203
|
+
}
|
|
7204
|
+
|
|
7205
|
+
/**
|
|
7206
|
+
* Sanctuary v1.3 WP-V1.3-1 Sentinel base class.
|
|
7207
|
+
*
|
|
7208
|
+
* Subclass to add a new sentinel. The dispatcher schedules `evaluate()`
|
|
7209
|
+
* on a tick. `subscribe()` is called once when an operator opts in;
|
|
7210
|
+
* `unsubscribe()` is called once when they opt out (or the fortress is
|
|
7211
|
+
* disposed). Sentinels MUST treat the SentinelContext as read-only.
|
|
7212
|
+
*/
|
|
7213
|
+
|
|
7214
|
+
/**
|
|
7215
|
+
* Concrete sentinels implement these three methods and stamp a
|
|
7216
|
+
* sentinel_id. Implementations must:
|
|
7217
|
+
* - Be pure with respect to the SentinelContext (no writable
|
|
7218
|
+
* storage, no external network).
|
|
7219
|
+
* - Return findings synchronously deterministically given the same
|
|
7220
|
+
* context state.
|
|
7221
|
+
* - Tolerate empty audit logs (return []).
|
|
7222
|
+
*/
|
|
7223
|
+
declare abstract class Sentinel {
|
|
7224
|
+
/**
|
|
7225
|
+
* Stable identifier. Used as the dictionary key in the registry, the
|
|
7226
|
+
* URL path component, and the audit-log details payload's
|
|
7227
|
+
* `sentinel_id` field. Convention: lowercase, dash-separated, prefix
|
|
7228
|
+
* with the sentinel category (e.g. `egress-volume`,
|
|
7229
|
+
* `credential-usage`).
|
|
7230
|
+
*/
|
|
7231
|
+
abstract readonly sentinelId: string;
|
|
7232
|
+
/**
|
|
7233
|
+
* Human-readable description shown in the operator dashboard's
|
|
7234
|
+
* "available sentinels" list. Should answer "what does this watch?"
|
|
7235
|
+
* in one sentence.
|
|
7236
|
+
*/
|
|
7237
|
+
abstract readonly description: string;
|
|
7238
|
+
/**
|
|
7239
|
+
* Bind the sentinel to a fortress context. Called once on
|
|
7240
|
+
* subscribe. Default implementation stores the context on `this`;
|
|
7241
|
+
* sentinels that need additional setup (e.g. priming a baseline
|
|
7242
|
+
* cache) override.
|
|
7243
|
+
*/
|
|
7244
|
+
subscribe(context: SentinelContext): Promise<void>;
|
|
7245
|
+
/**
|
|
7246
|
+
* Tear down. Default implementation clears the context; subclasses
|
|
7247
|
+
* that hold timers or external handles override.
|
|
7248
|
+
*/
|
|
7249
|
+
unsubscribe(): Promise<void>;
|
|
7250
|
+
/**
|
|
7251
|
+
* Compute findings against the current audit-log + agent-state
|
|
7252
|
+
* snapshot. Returns an empty array when nothing notable is observed.
|
|
7253
|
+
* Throwing is allowed; the dispatcher catches and emits a
|
|
7254
|
+
* `sentinel_evaluation_failed` audit event.
|
|
7255
|
+
*/
|
|
7256
|
+
abstract evaluate(): Promise<SentinelFinding[]>;
|
|
7257
|
+
protected context: SentinelContext | undefined;
|
|
7258
|
+
/** Internal helper: assert subscribed before evaluation. */
|
|
7259
|
+
protected requireContext(): SentinelContext;
|
|
7260
|
+
}
|
|
7261
|
+
|
|
7262
|
+
/**
|
|
7263
|
+
* Sanctuary v1.3 WP-V1.3-1 Sentinel Registry.
|
|
7264
|
+
*
|
|
7265
|
+
* Tracks two things for one fortress:
|
|
7266
|
+
* 1. The catalog of sentinel CLASSES the fortress can subscribe to
|
|
7267
|
+
* (registered at process boot).
|
|
7268
|
+
* 2. Which sentinels the operator has actually opted IN.
|
|
7269
|
+
*
|
|
7270
|
+
* Default subscription set is empty: the operator must opt in. This
|
|
7271
|
+
* keeps the no-outbound-by-default rule and the "operator pulls
|
|
7272
|
+
* features" UX pattern from CLAUDE.md.
|
|
7273
|
+
*
|
|
7274
|
+
* Multi-fortress isolation: one registry instance per fortress. The
|
|
7275
|
+
* dispatcher consults the registry and never crosses fortress
|
|
7276
|
+
* boundaries.
|
|
7277
|
+
*/
|
|
7278
|
+
|
|
7279
|
+
interface SentinelCatalogEntry {
|
|
7280
|
+
sentinelId: string;
|
|
7281
|
+
description: string;
|
|
7282
|
+
factory: () => Sentinel;
|
|
7283
|
+
}
|
|
7284
|
+
/**
|
|
7285
|
+
* Coordinator-CTO defaults: the registry ships with the Phi-1 catalog
|
|
7286
|
+
* pre-loaded but with zero subscriptions. Tests inject custom catalogs
|
|
7287
|
+
* via `register()`.
|
|
7288
|
+
*/
|
|
7289
|
+
declare class SentinelRegistry {
|
|
7290
|
+
private readonly catalog;
|
|
7291
|
+
private readonly subscribed;
|
|
7292
|
+
register(entry: SentinelCatalogEntry): void;
|
|
7293
|
+
/**
|
|
7294
|
+
* Available sentinels (catalog view). Operator UI lists this so the
|
|
7295
|
+
* operator can pick what to subscribe to.
|
|
7296
|
+
*/
|
|
7297
|
+
listCatalog(): Array<{
|
|
7298
|
+
sentinelId: string;
|
|
7299
|
+
description: string;
|
|
7300
|
+
}>;
|
|
7301
|
+
/** Currently subscribed sentinel ids. */
|
|
7302
|
+
listSubscribed(): string[];
|
|
7303
|
+
/** Has the fortress opted into this sentinel? */
|
|
7304
|
+
isSubscribed(sentinelId: string): boolean;
|
|
7305
|
+
/**
|
|
7306
|
+
* Subscribe a sentinel to a fortress context. Idempotent: a second
|
|
7307
|
+
* subscribe call on an already-subscribed sentinel returns the
|
|
7308
|
+
* existing instance without re-running `subscribe()`.
|
|
7309
|
+
*/
|
|
7310
|
+
subscribe(sentinelId: string, context: SentinelContext): Promise<Sentinel>;
|
|
7311
|
+
/**
|
|
7312
|
+
* Unsubscribe. Idempotent: unsubscribing an unsubscribed sentinel
|
|
7313
|
+
* returns false without throwing. Returns true when an active
|
|
7314
|
+
* subscription was torn down.
|
|
7315
|
+
*/
|
|
7316
|
+
unsubscribe(sentinelId: string): Promise<boolean>;
|
|
7317
|
+
/**
|
|
7318
|
+
* Snapshot of subscribed sentinels for the dispatcher's tick path.
|
|
7319
|
+
* Returned as an array so the dispatcher can iterate without holding
|
|
7320
|
+
* the map under modification.
|
|
7321
|
+
*/
|
|
7322
|
+
snapshotSubscribed(): Array<{
|
|
7323
|
+
sentinelId: string;
|
|
7324
|
+
sentinel: Sentinel;
|
|
7325
|
+
}>;
|
|
7326
|
+
/**
|
|
7327
|
+
* Tear down every subscription. Called by the dispatcher on
|
|
7328
|
+
* fortress-shutdown. Best-effort: a failing unsubscribe does not
|
|
7329
|
+
* abort the rest.
|
|
7330
|
+
*/
|
|
7331
|
+
unsubscribeAll(): Promise<void>;
|
|
7332
|
+
}
|
|
7333
|
+
|
|
7334
|
+
/**
|
|
7335
|
+
* Sanctuary v1.3 WP-V1.3-1 Sentinel Finding Store.
|
|
7336
|
+
*
|
|
7337
|
+
* Encrypted at-rest persistence for sentinel findings. Sibling to the
|
|
7338
|
+
* Upsilon-3 aggregator-store: same fortress-master-key-derived HKDF
|
|
7339
|
+
* subkey shape, AAD-bound to the finding_id, retention-aware.
|
|
7340
|
+
*
|
|
7341
|
+
* Storage layout:
|
|
7342
|
+
* namespace: `_sentinel_findings`
|
|
7343
|
+
* key: `finding.{finding_id}` (one record per finding)
|
|
7344
|
+
* payload: AES-256-GCM ciphertext of the JSON-serialized record.
|
|
7345
|
+
* key: `l2-sentinel-finding-v1` HKDF subkey of fortress master.
|
|
7346
|
+
* AAD: UTF-8 bytes of `finding_id`.
|
|
7347
|
+
*
|
|
7348
|
+
* Multi-fortress isolation: HKDF subkey derives from the fortress
|
|
7349
|
+
* master key. Two fortresses never produce identical encryption keys
|
|
7350
|
+
* for identical finding_ids.
|
|
7351
|
+
*
|
|
7352
|
+
* Retention: 30 days default, mirroring the audit-log envelope and the
|
|
7353
|
+
* aggregator payload store.
|
|
7354
|
+
*/
|
|
7355
|
+
|
|
7356
|
+
interface SentinelFindingStoreOptions {
|
|
7357
|
+
storage: StorageBackend;
|
|
7358
|
+
masterKey: Uint8Array;
|
|
7359
|
+
fortressId: string;
|
|
7360
|
+
/** Operator-tunable retention window. Default 30 days. */
|
|
7361
|
+
retentionDays?: number;
|
|
7362
|
+
/** Wall-clock provider for deterministic tests. */
|
|
7363
|
+
now?: () => Date;
|
|
7364
|
+
}
|
|
7365
|
+
declare class SentinelFindingStore {
|
|
7366
|
+
private readonly storage;
|
|
7367
|
+
private readonly encryptionKey;
|
|
7368
|
+
private readonly fortressId;
|
|
7369
|
+
private readonly retentionDays;
|
|
7370
|
+
private readonly now;
|
|
7371
|
+
constructor(opts: SentinelFindingStoreOptions);
|
|
7372
|
+
/**
|
|
7373
|
+
* Persist a finding. Truncates the operator-visible summary to
|
|
7374
|
+
* SENTINEL_SUMMARY_MAX_CHARS so the dashboard render stays bounded.
|
|
7375
|
+
* Returns the retention deadline so callers can audit it.
|
|
7376
|
+
*/
|
|
7377
|
+
saveFinding(finding: SentinelFinding): Promise<string>;
|
|
7378
|
+
/** Load a single finding by id, or null when absent / corrupted. */
|
|
7379
|
+
loadFinding(findingId: string): Promise<SentinelFinding | null>;
|
|
7380
|
+
/**
|
|
7381
|
+
* List findings, newest first. Optional filters: since (ISO 8601),
|
|
7382
|
+
* severity, sentinel_id, agent_id, limit. Default limit 100.
|
|
7383
|
+
*/
|
|
7384
|
+
listFindings(opts?: {
|
|
7385
|
+
since?: string;
|
|
7386
|
+
severity?: SentinelSeverity;
|
|
7387
|
+
sentinelId?: string;
|
|
7388
|
+
agentId?: string;
|
|
7389
|
+
limit?: number;
|
|
7390
|
+
}): Promise<SentinelFinding[]>;
|
|
7391
|
+
/**
|
|
7392
|
+
* Drop expired findings. Returns the count removed.
|
|
7393
|
+
*/
|
|
7394
|
+
pruneExpired(now?: Date): Promise<{
|
|
7395
|
+
pruned: number;
|
|
7396
|
+
}>;
|
|
7397
|
+
private decode;
|
|
7398
|
+
}
|
|
7399
|
+
|
|
7400
|
+
/**
|
|
7401
|
+
* Sanctuary v1.3 WP-V1.3-1 Sentinel Dispatcher.
|
|
7402
|
+
*
|
|
7403
|
+
* Drives the sentinel evaluation tick. On each tick:
|
|
7404
|
+
* 1. For every subscribed sentinel, call `evaluate()`.
|
|
7405
|
+
* 2. Each returned finding routes to:
|
|
7406
|
+
* - the sentinel finding store (encrypted persistence)
|
|
7407
|
+
* - the audit log (`sentinel_finding_emitted`)
|
|
7408
|
+
* - in-process subscribers (dashboard SSE wires through this)
|
|
7409
|
+
* 3. Per-sentinel exceptions log `sentinel_evaluation_failed` and the
|
|
7410
|
+
* dispatcher continues with the next sentinel.
|
|
7411
|
+
*
|
|
7412
|
+
* The dispatcher is one-per-fortress. The fortress id is stamped on
|
|
7413
|
+
* every finding before persistence + emission so multi-fortress
|
|
7414
|
+
* isolation holds at the cryptographic + structural layers both.
|
|
7415
|
+
*
|
|
7416
|
+
* Castle-walking discipline: the dispatcher introduces no outbound
|
|
7417
|
+
* surface. It reads from the audit log, writes to the encrypted
|
|
7418
|
+
* findings store + audit log, and emits to in-process subscribers.
|
|
7419
|
+
*/
|
|
7420
|
+
|
|
7421
|
+
type SentinelDispatcherUnsubscribe = () => void;
|
|
7422
|
+
/**
|
|
7423
|
+
* Event emitted to in-process subscribers when a finding is produced.
|
|
7424
|
+
* The dashboard SSE pipeline subscribes here.
|
|
7425
|
+
*/
|
|
7426
|
+
interface SentinelDispatcherEmit {
|
|
7427
|
+
type: "finding";
|
|
7428
|
+
finding: SentinelFinding;
|
|
7429
|
+
}
|
|
7430
|
+
/**
|
|
7431
|
+
* Event emitted on per-sentinel evaluation failure. Distinct from
|
|
7432
|
+
* `finding` so subscribers can render diagnostics separately.
|
|
7433
|
+
*/
|
|
7434
|
+
interface SentinelDispatcherFailureEmit {
|
|
7435
|
+
type: "evaluation_failed";
|
|
7436
|
+
sentinel_id: string;
|
|
7437
|
+
error_message: string;
|
|
7438
|
+
observed_at: string;
|
|
7439
|
+
}
|
|
7440
|
+
type SentinelDispatcherAnyEmit = SentinelDispatcherEmit | SentinelDispatcherFailureEmit;
|
|
7441
|
+
interface SentinelDispatcherDeps {
|
|
7442
|
+
registry: SentinelRegistry;
|
|
7443
|
+
findingStore: SentinelFindingStore;
|
|
7444
|
+
auditLog: AuditLog;
|
|
7445
|
+
/** Stable fortress id stamped on every finding. */
|
|
7446
|
+
fortressId: string;
|
|
7447
|
+
/** Operator identity id for audit attribution. */
|
|
7448
|
+
identityId: string;
|
|
7449
|
+
/** Wall-clock provider for deterministic tests. */
|
|
7450
|
+
now?: () => Date;
|
|
7451
|
+
/** Tick interval, ms. Default 60s. Set to 0 to disable auto-tick. */
|
|
7452
|
+
tickIntervalMs?: number;
|
|
7453
|
+
}
|
|
7454
|
+
declare class SentinelDispatcher {
|
|
7455
|
+
private readonly registry;
|
|
7456
|
+
private readonly findingStore;
|
|
7457
|
+
private readonly auditLog;
|
|
7458
|
+
private readonly fortressId;
|
|
7459
|
+
private readonly identityId;
|
|
7460
|
+
private readonly now;
|
|
7461
|
+
private readonly tickIntervalMs;
|
|
7462
|
+
private readonly listeners;
|
|
7463
|
+
private tickTimer;
|
|
7464
|
+
private tickInFlight;
|
|
7465
|
+
constructor(deps: SentinelDispatcherDeps);
|
|
7466
|
+
/** Read-only view of the registry. Convenience for route handlers. */
|
|
7467
|
+
getRegistry(): SentinelRegistry;
|
|
7468
|
+
/** Read-only view of the finding store. Convenience for route handlers. */
|
|
7469
|
+
getFindingStore(): SentinelFindingStore;
|
|
7470
|
+
/**
|
|
7471
|
+
* Subscribe an in-process listener. Returns an unsubscribe fn.
|
|
7472
|
+
*/
|
|
7473
|
+
onEvent(listener: (event: SentinelDispatcherAnyEmit) => void): SentinelDispatcherUnsubscribe;
|
|
7474
|
+
/**
|
|
7475
|
+
* Subscribe a sentinel to this fortress + emit the
|
|
7476
|
+
* `sentinel_subscribed` audit event. Wraps `registry.subscribe()` so
|
|
7477
|
+
* the audit emission lives at the dispatcher boundary (the
|
|
7478
|
+
* fortress-aware site).
|
|
7479
|
+
*/
|
|
7480
|
+
subscribeSentinel(sentinelId: string, contextOverrides?: Partial<SentinelContext>): Promise<Sentinel>;
|
|
7481
|
+
/**
|
|
7482
|
+
* Unsubscribe + emit `sentinel_unsubscribed`. Returns true when an
|
|
7483
|
+
* active subscription was torn down. Audit fires only on successful
|
|
7484
|
+
* removal.
|
|
7485
|
+
*/
|
|
7486
|
+
unsubscribeSentinel(sentinelId: string): Promise<boolean>;
|
|
7487
|
+
/**
|
|
7488
|
+
* Run one evaluation pass over every subscribed sentinel. Used by
|
|
7489
|
+
* the auto-tick AND by tests that want a synchronous evaluation
|
|
7490
|
+
* gate. Returns the findings produced this tick (already persisted
|
|
7491
|
+
* + audit-logged + emitted).
|
|
7492
|
+
*/
|
|
7493
|
+
tick(): Promise<SentinelFinding[]>;
|
|
7494
|
+
/**
|
|
7495
|
+
* Start the auto-tick loop. No-op when tickIntervalMs is 0 or when
|
|
7496
|
+
* already started. Tests typically leave auto-tick off and call
|
|
7497
|
+
* `tick()` directly.
|
|
7498
|
+
*/
|
|
7499
|
+
start(): void;
|
|
7500
|
+
/** Stop the auto-tick loop. Idempotent. */
|
|
7501
|
+
stop(): void;
|
|
7502
|
+
/**
|
|
7503
|
+
* Tear down every subscription + stop the tick loop. Called on
|
|
7504
|
+
* fortress shutdown.
|
|
7505
|
+
*/
|
|
7506
|
+
dispose(): Promise<void>;
|
|
7507
|
+
private routeFinding;
|
|
7508
|
+
private emit;
|
|
7509
|
+
}
|
|
7510
|
+
|
|
6570
7511
|
/**
|
|
6571
7512
|
* Sanctuary MCP Server — Principal Dashboard
|
|
6572
7513
|
*
|
|
@@ -6664,6 +7605,13 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
|
|
|
6664
7605
|
* the operator-facing query / decision surface.
|
|
6665
7606
|
*/
|
|
6666
7607
|
private approvalAggregator;
|
|
7608
|
+
/**
|
|
7609
|
+
* v1.3 WP-V1.3-1 Phi-1 Sentinel dispatcher. Mounted additively at
|
|
7610
|
+
* `/api/sentinels/*` when set. Sentinel surface is read-only against
|
|
7611
|
+
* the audit log; subscribe/unsubscribe writes flow through the
|
|
7612
|
+
* dispatcher's audited paths.
|
|
7613
|
+
*/
|
|
7614
|
+
private sentinelDispatcher;
|
|
6667
7615
|
constructor(config: DashboardConfig);
|
|
6668
7616
|
/**
|
|
6669
7617
|
* Inject dependencies after construction.
|
|
@@ -6701,11 +7649,23 @@ declare class DashboardApprovalChannel implements ApprovalChannel {
|
|
|
6701
7649
|
* tests + during shutdown).
|
|
6702
7650
|
*/
|
|
6703
7651
|
setApprovalAggregator(aggregator: ApprovalAggregator | null): void;
|
|
7652
|
+
/**
|
|
7653
|
+
* v1.3 WP-V1.3-1 Phi-1: bind the Sentinel dispatcher. Once set,
|
|
7654
|
+
* requests to `/api/sentinels/*` route through `handleSentinelRoute`.
|
|
7655
|
+
* Pass `null` to detach (used by tests + during shutdown).
|
|
7656
|
+
*/
|
|
7657
|
+
setSentinelDispatcher(dispatcher: SentinelDispatcher | null): void;
|
|
6704
7658
|
/**
|
|
6705
7659
|
* v1.3 WP-V1.3-10 dispatch entry point. Called from `handleRequest`
|
|
6706
7660
|
* before the legacy approval route table. Returns true when served.
|
|
6707
7661
|
*/
|
|
6708
7662
|
private dispatchApprovalInbox;
|
|
7663
|
+
/**
|
|
7664
|
+
* v1.3 WP-V1.3-1 Phi-1 dispatch entry point. Routes `/api/sentinels/*`
|
|
7665
|
+
* requests through the sentinel router when a dispatcher has been
|
|
7666
|
+
* bound. Returns true when served.
|
|
7667
|
+
*/
|
|
7668
|
+
private dispatchSentinel;
|
|
6709
7669
|
/**
|
|
6710
7670
|
* v1.1 dispatch entry point. Called from `handleRequest` before the
|
|
6711
7671
|
* legacy route table. Returns true when the request was served by v1.1
|