@sanctuary-framework/mcp-server 0.5.14 → 0.5.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -11,12 +11,15 @@ var os = require('os');
11
11
  var module$1 = require('module');
12
12
  var hashWasm = require('hash-wasm');
13
13
  var hkdf = require('@noble/hashes/hkdf');
14
- var index_js = require('@modelcontextprotocol/sdk/server/index.js');
14
+ var index_js$1 = require('@modelcontextprotocol/sdk/server/index.js');
15
15
  var types_js = require('@modelcontextprotocol/sdk/types.js');
16
16
  var http = require('http');
17
17
  var https = require('https');
18
18
  var fs = require('fs');
19
19
  var child_process = require('child_process');
20
+ var index_js = require('@modelcontextprotocol/sdk/client/index.js');
21
+ var stdio_js = require('@modelcontextprotocol/sdk/client/stdio.js');
22
+ var sse_js = require('@modelcontextprotocol/sdk/client/sse.js');
20
23
 
21
24
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
22
25
  var __defProp = Object.defineProperty;
@@ -1182,7 +1185,7 @@ function checkType(field, value, schema) {
1182
1185
  }
1183
1186
  function createServer(tools, options) {
1184
1187
  const gate = options?.gate;
1185
- const server = new index_js.Server(
1188
+ const server = new index_js$1.Server(
1186
1189
  {
1187
1190
  name: "sanctuary-mcp-server",
1188
1191
  version: PKG_VERSION2
@@ -3819,8 +3822,10 @@ var DEFAULT_POLICY = {
3819
3822
  "decommission_certificate",
3820
3823
  "reputation_publish",
3821
3824
  // SEC-039: Explicit Tier 1 — sends data to external API
3822
- "sovereignty_profile_update"
3825
+ "sovereignty_profile_update",
3823
3826
  // Changes enforcement behavior — always requires approval
3827
+ "governor_reset"
3828
+ // Clears all runtime governance state — always requires approval
3824
3829
  ],
3825
3830
  tier2_anomaly: DEFAULT_TIER2,
3826
3831
  tier3_always_allow: [
@@ -3876,11 +3881,16 @@ var DEFAULT_POLICY = {
3876
3881
  "dashboard_open",
3877
3882
  // SEC-039: Explicit Tier 3 — only generates a URL
3878
3883
  "sovereignty_profile_get",
3879
- "sovereignty_profile_generate_prompt"
3884
+ "sovereignty_profile_generate_prompt",
3885
+ // Agent needs its own config to generate system prompt
3886
+ "governor_status"
3880
3887
  ],
3881
3888
  approval_channel: DEFAULT_CHANNEL
3882
3889
  };
3883
3890
  function extractOperationName(toolName) {
3891
+ if (toolName.startsWith("proxy/")) {
3892
+ return toolName;
3893
+ }
3884
3894
  return toolName.startsWith("sanctuary/") ? toolName.slice("sanctuary/".length) : toolName;
3885
3895
  }
3886
3896
  function parsePolicy(content) {
@@ -3986,6 +3996,7 @@ tier1_always_approve:
3986
3996
  - bootstrap_provide_guarantee
3987
3997
  - reputation_publish
3988
3998
  - sovereignty_profile_update
3999
+ - governor_reset
3989
4000
 
3990
4001
  # \u2500\u2500\u2500 Tier 2: Behavioral Anomaly Detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3991
4002
  # Triggers approval when agent behavior deviates from its baseline.
@@ -4050,7 +4061,7 @@ tier3_always_allow:
4050
4061
  - bridge_attest
4051
4062
  - dashboard_open
4052
4063
  - sovereignty_profile_get
4053
- - sovereignty_profile_generate_prompt
4064
+ - governor_status
4054
4065
 
4055
4066
  # \u2500\u2500\u2500 Approval Channel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4056
4067
  # How Sanctuary reaches you when approval is needed.
@@ -4623,7 +4634,7 @@ function generateLoginHTML(options) {
4623
4634
  if (response.ok) {
4624
4635
  const data = await response.json();
4625
4636
  sessionStorage.setItem('authToken', token);
4626
- window.location.href = '/dashboard';
4637
+ window.location.href = '/'; // Dashboard is served at root path
4627
4638
  } else if (response.status === 401) {
4628
4639
  showError('Invalid token. Please check and try again.');
4629
4640
  } else {
@@ -5362,6 +5373,17 @@ function generateDashboardHTML(options) {
5362
5373
  display: flex;
5363
5374
  flex-direction: column;
5364
5375
  gap: 8px;
5376
+ transition: border-color 0.2s;
5377
+ }
5378
+
5379
+ .profile-card.active {
5380
+ border-color: var(--green);
5381
+ }
5382
+
5383
+ .profile-card-header {
5384
+ display: flex;
5385
+ justify-content: space-between;
5386
+ align-items: center;
5365
5387
  }
5366
5388
 
5367
5389
  .profile-card-name {
@@ -5376,6 +5398,53 @@ function generateDashboardHTML(options) {
5376
5398
  line-height: 1.4;
5377
5399
  }
5378
5400
 
5401
+ /* Toggle switch */
5402
+ .toggle-switch {
5403
+ position: relative;
5404
+ width: 36px;
5405
+ height: 20px;
5406
+ flex-shrink: 0;
5407
+ }
5408
+
5409
+ .toggle-switch input {
5410
+ opacity: 0;
5411
+ width: 0;
5412
+ height: 0;
5413
+ }
5414
+
5415
+ .toggle-slider {
5416
+ position: absolute;
5417
+ cursor: pointer;
5418
+ top: 0;
5419
+ left: 0;
5420
+ right: 0;
5421
+ bottom: 0;
5422
+ background-color: var(--border);
5423
+ border-radius: 10px;
5424
+ transition: background-color 0.2s;
5425
+ }
5426
+
5427
+ .toggle-slider::before {
5428
+ content: "";
5429
+ position: absolute;
5430
+ height: 14px;
5431
+ width: 14px;
5432
+ left: 3px;
5433
+ bottom: 3px;
5434
+ background-color: var(--text-secondary);
5435
+ border-radius: 50%;
5436
+ transition: transform 0.2s, background-color 0.2s;
5437
+ }
5438
+
5439
+ .toggle-switch input:checked + .toggle-slider {
5440
+ background-color: rgba(63, 185, 80, 0.3);
5441
+ }
5442
+
5443
+ .toggle-switch input:checked + .toggle-slider::before {
5444
+ transform: translateX(16px);
5445
+ background-color: var(--green);
5446
+ }
5447
+
5379
5448
  .profile-badge {
5380
5449
  display: inline-flex;
5381
5450
  align-items: center;
@@ -5397,22 +5466,159 @@ function generateDashboardHTML(options) {
5397
5466
  color: var(--text-secondary);
5398
5467
  }
5399
5468
 
5469
+ .profile-card-actions {
5470
+ display: flex;
5471
+ gap: 6px;
5472
+ margin-top: 4px;
5473
+ }
5474
+
5475
+ .config-btn {
5476
+ padding: 3px 8px;
5477
+ border: 1px solid var(--border);
5478
+ border-radius: 4px;
5479
+ background-color: transparent;
5480
+ color: var(--text-secondary);
5481
+ font-size: 10px;
5482
+ cursor: pointer;
5483
+ transition: color 0.2s, border-color 0.2s;
5484
+ }
5485
+
5486
+ .config-btn:hover {
5487
+ color: var(--blue);
5488
+ border-color: var(--blue);
5489
+ }
5490
+
5491
+ /* Configuration panels */
5492
+ .config-panel {
5493
+ display: none;
5494
+ margin-top: 8px;
5495
+ padding: 10px;
5496
+ background-color: var(--surface);
5497
+ border: 1px solid var(--border);
5498
+ border-radius: 4px;
5499
+ font-size: 11px;
5500
+ }
5501
+
5502
+ .config-panel.open {
5503
+ display: block;
5504
+ }
5505
+
5506
+ .config-panel-title {
5507
+ font-size: 11px;
5508
+ font-weight: 600;
5509
+ color: var(--text-primary);
5510
+ margin-bottom: 8px;
5511
+ }
5512
+
5513
+ .config-row {
5514
+ display: flex;
5515
+ align-items: center;
5516
+ gap: 8px;
5517
+ margin-bottom: 6px;
5518
+ }
5519
+
5520
+ .config-label {
5521
+ font-size: 11px;
5522
+ color: var(--text-secondary);
5523
+ min-width: 80px;
5524
+ }
5525
+
5526
+ .config-select, .config-input {
5527
+ background-color: var(--bg);
5528
+ border: 1px solid var(--border);
5529
+ border-radius: 4px;
5530
+ color: var(--text-primary);
5531
+ font-size: 11px;
5532
+ padding: 4px 8px;
5533
+ }
5534
+
5535
+ .config-info {
5536
+ font-size: 11px;
5537
+ color: var(--text-secondary);
5538
+ line-height: 1.5;
5539
+ }
5540
+
5541
+ .sensitivity-slider {
5542
+ display: flex;
5543
+ gap: 4px;
5544
+ }
5545
+
5546
+ .sensitivity-option {
5547
+ padding: 3px 10px;
5548
+ border: 1px solid var(--border);
5549
+ border-radius: 4px;
5550
+ background-color: transparent;
5551
+ color: var(--text-secondary);
5552
+ font-size: 10px;
5553
+ cursor: pointer;
5554
+ transition: all 0.2s;
5555
+ }
5556
+
5557
+ .sensitivity-option.selected {
5558
+ background-color: rgba(88, 166, 255, 0.15);
5559
+ color: var(--blue);
5560
+ border-color: var(--blue);
5561
+ }
5562
+
5563
+ .sensitivity-option:hover:not(.selected) {
5564
+ border-color: var(--text-secondary);
5565
+ }
5566
+
5567
+ /* Prompt section */
5400
5568
  .prompt-section {
5401
- margin-top: 12px;
5569
+ margin-top: 16px;
5402
5570
  }
5403
5571
 
5404
- .prompt-textarea {
5405
- width: 100%;
5406
- min-height: 120px;
5572
+ .prompt-display {
5573
+ position: relative;
5407
5574
  background-color: var(--bg);
5408
5575
  border: 1px solid var(--border);
5409
5576
  border-radius: 6px;
5577
+ margin-top: 8px;
5578
+ display: none;
5579
+ }
5580
+
5581
+ .prompt-display.visible {
5582
+ display: block;
5583
+ }
5584
+
5585
+ .prompt-display-header {
5586
+ display: flex;
5587
+ justify-content: space-between;
5588
+ align-items: center;
5589
+ padding: 8px 12px;
5590
+ border-bottom: 1px solid var(--border);
5591
+ }
5592
+
5593
+ .prompt-token-count {
5594
+ font-size: 11px;
5595
+ color: var(--text-secondary);
5596
+ }
5597
+
5598
+ .prompt-copy-btn {
5599
+ padding: 4px 10px;
5600
+ border: 1px solid var(--border);
5601
+ border-radius: 4px;
5602
+ background-color: var(--surface);
5410
5603
  color: var(--text-primary);
5604
+ font-size: 11px;
5605
+ cursor: pointer;
5606
+ transition: border-color 0.2s;
5607
+ }
5608
+
5609
+ .prompt-copy-btn:hover {
5610
+ border-color: var(--blue);
5611
+ }
5612
+
5613
+ .prompt-content {
5614
+ max-height: 300px;
5615
+ overflow-y: auto;
5616
+ padding: 12px;
5411
5617
  font-family: 'JetBrains Mono', monospace;
5412
5618
  font-size: 12px;
5413
- padding: 12px;
5414
- resize: vertical;
5415
- margin-top: 8px;
5619
+ line-height: 1.6;
5620
+ white-space: pre-wrap;
5621
+ color: var(--text-primary);
5416
5622
  }
5417
5623
 
5418
5624
  .prompt-actions {
@@ -5799,37 +6005,173 @@ function generateDashboardHTML(options) {
5799
6005
  </div>
5800
6006
  <div class="profile-cards" id="profile-cards">
5801
6007
  <div class="profile-card" data-feature="audit_logging">
5802
- <div class="profile-card-name">Audit Logging</div>
6008
+ <div class="profile-card-header">
6009
+ <div class="profile-card-name">Audit Logging</div>
6010
+ <label class="toggle-switch">
6011
+ <input type="checkbox" id="toggle-audit_logging" data-feature="audit_logging">
6012
+ <span class="toggle-slider"></span>
6013
+ </label>
6014
+ </div>
5803
6015
  <div class="profile-badge disabled" id="badge-audit_logging">OFF</div>
5804
6016
  <div class="profile-card-desc">Encrypted audit trail of all tool calls</div>
6017
+ <div class="profile-card-actions">
6018
+ <button class="config-btn" data-config="audit_logging">Configure</button>
6019
+ </div>
6020
+ <div class="config-panel" id="config-audit_logging">
6021
+ <div class="config-info">Audit logging is always-on when enabled. All tool calls, gate decisions, and profile changes are recorded to an encrypted audit trail. No additional configuration needed.</div>
6022
+ </div>
5805
6023
  </div>
5806
6024
  <div class="profile-card" data-feature="injection_detection">
5807
- <div class="profile-card-name">Injection Detection</div>
6025
+ <div class="profile-card-header">
6026
+ <div class="profile-card-name">Injection Detection</div>
6027
+ <label class="toggle-switch">
6028
+ <input type="checkbox" id="toggle-injection_detection" data-feature="injection_detection">
6029
+ <span class="toggle-slider"></span>
6030
+ </label>
6031
+ </div>
5808
6032
  <div class="profile-badge disabled" id="badge-injection_detection">OFF</div>
5809
6033
  <div class="profile-card-desc">Scans tool arguments for prompt injection</div>
6034
+ <div class="profile-card-actions">
6035
+ <button class="config-btn" data-config="injection_detection">Configure</button>
6036
+ </div>
6037
+ <div class="config-panel" id="config-injection_detection">
6038
+ <div class="config-panel-title">Sensitivity</div>
6039
+ <div class="sensitivity-slider">
6040
+ <button class="sensitivity-option" data-sensitivity="low">Low</button>
6041
+ <button class="sensitivity-option selected" data-sensitivity="medium">Medium</button>
6042
+ <button class="sensitivity-option" data-sensitivity="high">High</button>
6043
+ </div>
6044
+ </div>
5810
6045
  </div>
5811
6046
  <div class="profile-card" data-feature="context_gating">
5812
- <div class="profile-card-name">Context Gating</div>
6047
+ <div class="profile-card-header">
6048
+ <div class="profile-card-name">Context Gating</div>
6049
+ <label class="toggle-switch">
6050
+ <input type="checkbox" id="toggle-context_gating" data-feature="context_gating">
6051
+ <span class="toggle-slider"></span>
6052
+ </label>
6053
+ </div>
5813
6054
  <div class="profile-badge disabled" id="badge-context_gating">OFF</div>
5814
6055
  <div class="profile-card-desc">Controls context flow to remote providers</div>
6056
+ <div class="profile-card-actions">
6057
+ <button class="config-btn" data-config="context_gating">Configure</button>
6058
+ </div>
6059
+ <div class="config-panel" id="config-context_gating">
6060
+ <div class="config-panel-title">Active Policy</div>
6061
+ <div class="config-row">
6062
+ <span class="config-label">Policy ID:</span>
6063
+ <select class="config-select" id="config-context-policy">
6064
+ <option value="">None selected</option>
6065
+ </select>
6066
+ </div>
6067
+ <div class="config-info" style="margin-top: 6px;">Use MCP tool <code style="color: var(--blue);">sanctuary/context_gate_set_policy</code> or <code style="color: var(--blue);">sanctuary/context_gate_apply_template</code> to create policies.</div>
6068
+ </div>
5815
6069
  </div>
5816
6070
  <div class="profile-card" data-feature="approval_gate">
5817
- <div class="profile-card-name">Approval Gates</div>
6071
+ <div class="profile-card-header">
6072
+ <div class="profile-card-name">Approval Gates</div>
6073
+ <label class="toggle-switch">
6074
+ <input type="checkbox" id="toggle-approval_gate" data-feature="approval_gate">
6075
+ <span class="toggle-slider"></span>
6076
+ </label>
6077
+ </div>
5818
6078
  <div class="profile-badge disabled" id="badge-approval_gate">OFF</div>
5819
6079
  <div class="profile-card-desc">Human approval for high-risk operations</div>
6080
+ <div class="profile-card-actions">
6081
+ <button class="config-btn" data-config="approval_gate">Configure</button>
6082
+ </div>
6083
+ <div class="config-panel" id="config-approval_gate">
6084
+ <div class="config-panel-title">Tier Assignments</div>
6085
+ <div class="config-info">
6086
+ <strong style="color: var(--red);">Tier 1 (always approve):</strong> export, import, key rotation, deletion<br>
6087
+ <strong style="color: var(--amber);">Tier 2 (approve on anomaly):</strong> new namespaces, unfamiliar counterparties, frequency spikes<br>
6088
+ <strong style="color: var(--green);">Tier 3 (auto-allow):</strong> standard operations, queries, reads
6089
+ </div>
6090
+ </div>
5820
6091
  </div>
5821
6092
  <div class="profile-card" data-feature="zk_proofs">
5822
- <div class="profile-card-name">ZK Proofs</div>
6093
+ <div class="profile-card-header">
6094
+ <div class="profile-card-name">ZK Proofs</div>
6095
+ <label class="toggle-switch">
6096
+ <input type="checkbox" id="toggle-zk_proofs" data-feature="zk_proofs">
6097
+ <span class="toggle-slider"></span>
6098
+ </label>
6099
+ </div>
5823
6100
  <div class="profile-badge disabled" id="badge-zk_proofs">OFF</div>
5824
6101
  <div class="profile-card-desc">Prove claims without revealing data</div>
6102
+ <div class="profile-card-actions">
6103
+ <button class="config-btn" data-config="zk_proofs">Configure</button>
6104
+ </div>
6105
+ <div class="config-panel" id="config-zk_proofs">
6106
+ <div class="config-info">Zero-knowledge proofs use Pedersen commitments on Ristretto255 and Schnorr proofs. No additional configuration needed. Available tools: <code style="color: var(--blue);">sanctuary/zk_commit</code>, <code style="color: var(--blue);">sanctuary/zk_prove</code>, <code style="color: var(--blue);">sanctuary/zk_range_prove</code>.</div>
6107
+ </div>
5825
6108
  </div>
5826
6109
  </div>
5827
6110
  <div class="prompt-section">
5828
6111
  <div class="prompt-actions">
5829
6112
  <button class="prompt-btn primary" id="generate-prompt-btn">Generate System Prompt</button>
5830
- <button class="prompt-btn" id="copy-prompt-btn" style="display:none;">Copy</button>
5831
6113
  </div>
5832
- <textarea class="prompt-textarea" id="system-prompt-output" readonly style="display:none;" placeholder="Click 'Generate System Prompt' to create an agent instruction snippet..."></textarea>
6114
+ <div class="prompt-display" id="prompt-display">
6115
+ <div class="prompt-display-header">
6116
+ <span class="prompt-token-count" id="prompt-token-count"></span>
6117
+ <button class="prompt-copy-btn" id="copy-prompt-btn">Copy to Clipboard</button>
6118
+ </div>
6119
+ <div class="prompt-content" id="system-prompt-output"></div>
6120
+ </div>
6121
+ </div>
6122
+ </div>
6123
+
6124
+ <!-- Upstream Servers Panel -->
6125
+ <div class="profile-panel" id="proxy-servers-panel">
6126
+ <div class="panel-header">
6127
+ <div class="panel-title">Upstream Servers</div>
6128
+ <button class="panel-action" id="add-proxy-server-btn">+ Add Server</button>
6129
+ </div>
6130
+ <div id="proxy-servers-list">
6131
+ <div class="empty-state">No upstream servers configured</div>
6132
+ </div>
6133
+
6134
+ <!-- Add Server Form (hidden by default) -->
6135
+ <div id="add-server-form" style="display: none; padding: 16px; border-top: 1px solid var(--border);">
6136
+ <div style="margin-bottom: 12px;">
6137
+ <label style="font-size: 12px; color: var(--text-secondary); display: block; margin-bottom: 4px;">Server Name</label>
6138
+ <input type="text" id="new-server-name" placeholder="e.g., filesystem" style="width: 100%; padding: 8px; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; color: var(--text-primary); font-size: 13px;">
6139
+ </div>
6140
+ <div style="margin-bottom: 12px;">
6141
+ <label style="font-size: 12px; color: var(--text-secondary); display: block; margin-bottom: 4px;">Transport Type</label>
6142
+ <select id="new-server-transport" style="width: 100%; padding: 8px; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; color: var(--text-primary); font-size: 13px;">
6143
+ <option value="stdio">stdio</option>
6144
+ <option value="sse">SSE</option>
6145
+ </select>
6146
+ </div>
6147
+ <div id="stdio-fields">
6148
+ <div style="margin-bottom: 12px;">
6149
+ <label style="font-size: 12px; color: var(--text-secondary); display: block; margin-bottom: 4px;">Command</label>
6150
+ <input type="text" id="new-server-command" placeholder="e.g., npx -y @modelcontextprotocol/server-filesystem" style="width: 100%; padding: 8px; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; color: var(--text-primary); font-size: 13px;">
6151
+ </div>
6152
+ <div style="margin-bottom: 12px;">
6153
+ <label style="font-size: 12px; color: var(--text-secondary); display: block; margin-bottom: 4px;">Arguments (comma-separated)</label>
6154
+ <input type="text" id="new-server-args" placeholder="e.g., /Users/me/allowed-dir" style="width: 100%; padding: 8px; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; color: var(--text-primary); font-size: 13px;">
6155
+ </div>
6156
+ </div>
6157
+ <div id="sse-fields" style="display: none;">
6158
+ <div style="margin-bottom: 12px;">
6159
+ <label style="font-size: 12px; color: var(--text-secondary); display: block; margin-bottom: 4px;">Server URL</label>
6160
+ <input type="text" id="new-server-url" placeholder="e.g., http://localhost:3001/sse" style="width: 100%; padding: 8px; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; color: var(--text-primary); font-size: 13px;">
6161
+ </div>
6162
+ </div>
6163
+ <div style="margin-bottom: 12px;">
6164
+ <label style="font-size: 12px; color: var(--text-secondary); display: block; margin-bottom: 4px;">Default Tier</label>
6165
+ <select id="new-server-tier" style="width: 100%; padding: 8px; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; color: var(--text-primary); font-size: 13px;">
6166
+ <option value="1">Tier 1 (Always approve)</option>
6167
+ <option value="2" selected>Tier 2 (Anomaly detection)</option>
6168
+ <option value="3">Tier 3 (Always allow)</option>
6169
+ </select>
6170
+ </div>
6171
+ <div style="display: flex; gap: 8px;">
6172
+ <button id="save-server-btn" style="flex: 1; padding: 8px; background: var(--green); color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 13px;">Save</button>
6173
+ <button id="cancel-server-btn" style="flex: 1; padding: 8px; background: var(--surface); color: var(--text-secondary); border: 1px solid var(--border); border-radius: 4px; cursor: pointer; font-size: 13px;">Cancel</button>
6174
+ </div>
5833
6175
  </div>
5834
6176
  </div>
5835
6177
 
@@ -6320,8 +6662,34 @@ function generateDashboardHTML(options) {
6320
6662
  removePendingRequest(data.requestId);
6321
6663
  });
6322
6664
 
6323
- eventSource.addEventListener('sovereignty-profile-update', () => {
6324
- updateSovereigntyProfile();
6665
+ eventSource.addEventListener('sovereignty-profile-update', (e) => {
6666
+ try {
6667
+ const data = JSON.parse(e.data);
6668
+ if (data.profile) {
6669
+ applyProfileToUI(data.profile);
6670
+ }
6671
+ if (data.system_prompt) {
6672
+ apiState.systemPrompt = data.system_prompt;
6673
+ updatePromptDisplay(data.system_prompt);
6674
+ }
6675
+ } catch (err) {
6676
+ // Fallback to full refresh
6677
+ updateSovereigntyProfile();
6678
+ }
6679
+ });
6680
+
6681
+ eventSource.addEventListener('proxy-server-status', (e) => {
6682
+ try {
6683
+ const data = JSON.parse(e.data);
6684
+ updateProxyServerStatus(data.server, data.state, data.tool_count, data.error);
6685
+ } catch (err) {
6686
+ // Fallback to full refresh
6687
+ loadProxyServers();
6688
+ }
6689
+ });
6690
+
6691
+ eventSource.addEventListener('proxy-servers-update', () => {
6692
+ loadProxyServers();
6325
6693
  });
6326
6694
 
6327
6695
  eventSource.onerror = () => {
@@ -6339,7 +6707,7 @@ function generateDashboardHTML(options) {
6339
6707
 
6340
6708
  const feed = document.getElementById('activity-feed');
6341
6709
  const html = \`
6342
- <div class="activity-item \${item.type}">
6710
+ <div class="activity-item \${esc(item.type)}">
6343
6711
  <div class="activity-type">\${esc(item.title)}</div>
6344
6712
  <div class="activity-content">\${esc(item.content)}</div>
6345
6713
  <div class="activity-time">\${formatTime(item.timestamp)}</div>
@@ -6481,26 +6849,16 @@ function generateDashboardHTML(options) {
6481
6849
  });
6482
6850
 
6483
6851
  // Sovereignty Profile
6852
+ let profileToggleLock = false;
6853
+
6484
6854
  async function updateSovereigntyProfile() {
6485
6855
  try {
6486
6856
  const data = await fetchAPI('/api/sovereignty-profile');
6487
6857
  if (data && data.profile) {
6488
- const features = data.profile.features;
6489
- for (const [key, value] of Object.entries(features)) {
6490
- const badge = document.getElementById('badge-' + key);
6491
- if (badge) {
6492
- const enabled = value && value.enabled;
6493
- badge.textContent = enabled ? 'ON' : 'OFF';
6494
- badge.className = 'profile-badge ' + (enabled ? 'enabled' : 'disabled');
6495
- }
6496
- }
6497
- const updatedEl = document.getElementById('profile-updated-at');
6498
- if (updatedEl && data.profile.updated_at) {
6499
- updatedEl.textContent = 'Updated: ' + new Date(data.profile.updated_at).toLocaleString();
6500
- }
6501
- // Cache the prompt
6858
+ applyProfileToUI(data.profile);
6502
6859
  if (data.system_prompt) {
6503
6860
  apiState.systemPrompt = data.system_prompt;
6861
+ updatePromptDisplay(data.system_prompt);
6504
6862
  }
6505
6863
  }
6506
6864
  } catch (e) {
@@ -6508,94 +6866,518 @@ function generateDashboardHTML(options) {
6508
6866
  }
6509
6867
  }
6510
6868
 
6511
- document.getElementById('generate-prompt-btn').addEventListener('click', async () => {
6512
- const data = await fetchAPI('/api/sovereignty-profile');
6513
- if (data && data.system_prompt) {
6514
- const textarea = document.getElementById('system-prompt-output');
6515
- const copyBtn = document.getElementById('copy-prompt-btn');
6516
- textarea.value = data.system_prompt;
6517
- textarea.style.display = 'block';
6518
- copyBtn.style.display = 'inline-flex';
6519
- }
6520
- });
6869
+ function applyProfileToUI(profile) {
6870
+ const features = profile.features;
6871
+ for (const [key, value] of Object.entries(features)) {
6872
+ const enabled = value && value.enabled;
6521
6873
 
6522
- document.getElementById('copy-prompt-btn').addEventListener('click', async () => {
6523
- const textarea = document.getElementById('system-prompt-output');
6524
- try {
6525
- await navigator.clipboard.writeText(textarea.value);
6526
- const btn = document.getElementById('copy-prompt-btn');
6527
- const original = btn.textContent;
6528
- btn.textContent = 'Copied!';
6529
- setTimeout(() => { btn.textContent = original; }, 2000);
6530
- } catch (err) {
6531
- console.error('Copy failed:', err);
6532
- }
6533
- });
6874
+ // Update badge
6875
+ const badge = document.getElementById('badge-' + key);
6876
+ if (badge) {
6877
+ badge.textContent = enabled ? 'ON' : 'OFF';
6878
+ badge.className = 'profile-badge ' + (enabled ? 'enabled' : 'disabled');
6879
+ }
6534
6880
 
6535
- // Initialize
6536
- async function initialize() {
6537
- if (!AUTH_TOKEN) {
6538
- redirectToLogin();
6539
- return;
6881
+ // Update toggle (without triggering change event)
6882
+ const toggle = document.getElementById('toggle-' + key);
6883
+ if (toggle && toggle.checked !== enabled) {
6884
+ profileToggleLock = true;
6885
+ toggle.checked = enabled;
6886
+ profileToggleLock = false;
6887
+ }
6888
+
6889
+ // Update card active state
6890
+ const card = document.querySelector('[data-feature="' + key + '"]');
6891
+ if (card) {
6892
+ card.classList.toggle('active', enabled);
6893
+ }
6894
+
6895
+ // Feature-specific config UI updates
6896
+ if (key === 'injection_detection' && value.sensitivity) {
6897
+ document.querySelectorAll('#config-injection_detection .sensitivity-option').forEach(function(btn) {
6898
+ btn.classList.toggle('selected', btn.getAttribute('data-sensitivity') === value.sensitivity);
6899
+ });
6900
+ }
6901
+
6902
+ if (key === 'context_gating' && value.policy_id) {
6903
+ const sel = document.getElementById('config-context-policy');
6904
+ if (sel) {
6905
+ // Add the policy as an option if not present
6906
+ let found = false;
6907
+ for (let i = 0; i < sel.options.length; i++) {
6908
+ if (sel.options[i].value === value.policy_id) { found = true; break; }
6909
+ }
6910
+ if (!found) {
6911
+ const opt = document.createElement('option');
6912
+ opt.value = value.policy_id;
6913
+ opt.textContent = value.policy_id;
6914
+ sel.appendChild(opt);
6915
+ }
6916
+ sel.value = value.policy_id;
6917
+ }
6918
+ }
6540
6919
  }
6541
6920
 
6542
- // Initial data fetch
6543
- await Promise.all([
6544
- updateSovereignty(),
6545
- updateIdentity(),
6546
- updateHandshakes(),
6547
- updateSHR(),
6548
- updateStatus(),
6549
- updateSovereigntyProfile(),
6550
- ]);
6921
+ const updatedEl = document.getElementById('profile-updated-at');
6922
+ if (updatedEl && profile.updated_at) {
6923
+ updatedEl.textContent = 'Updated: ' + new Date(profile.updated_at).toLocaleString();
6924
+ }
6925
+ }
6551
6926
 
6552
- // Setup SSE for real-time updates
6553
- setupSSE();
6927
+ function updatePromptDisplay(promptText) {
6928
+ const display = document.getElementById('prompt-display');
6929
+ const content = document.getElementById('system-prompt-output');
6930
+ const tokenCount = document.getElementById('prompt-token-count');
6931
+ if (!display || !content) return;
6554
6932
 
6555
- // Refresh status periodically
6556
- setInterval(updateStatus, 30000);
6933
+ if (promptText) {
6934
+ content.textContent = promptText;
6935
+ // Rough token estimate: word count * 1.3
6936
+ const words = promptText.split(/\\s+/).filter(function(w) { return w.length > 0; }).length;
6937
+ const tokens = Math.round(words * 1.3);
6938
+ tokenCount.textContent = '~' + tokens + ' tokens';
6939
+ display.classList.add('visible');
6940
+ }
6557
6941
  }
6558
6942
 
6559
- // Start
6560
- initialize();
6561
- </script>
6562
- </body>
6563
- </html>`;
6564
- }
6943
+ // Toggle handlers
6944
+ async function handleToggle(feature, enabled) {
6945
+ if (profileToggleLock) return;
6946
+ const payload = {};
6947
+ payload[feature] = { enabled: enabled };
6565
6948
 
6566
- // src/system-prompt-generator.ts
6567
- var FEATURE_INFO = {
6568
- audit_logging: {
6569
- name: "Audit Logging",
6570
- activeDescription: "All your tool calls are logged to an encrypted audit trail. No action needed \u2014 this is automatic.",
6571
- disabledDescription: "audit logging (sanctuary/monitor_audit_log)"
6949
+ try {
6950
+ const response = await fetch(API_BASE + '/api/sovereignty-profile', {
6951
+ method: 'POST',
6952
+ headers: {
6953
+ 'Authorization': 'Bearer ' + AUTH_TOKEN,
6954
+ 'Content-Type': 'application/json',
6955
+ },
6956
+ body: JSON.stringify(payload),
6957
+ });
6958
+
6959
+ if (response.status === 401) {
6960
+ redirectToLogin();
6961
+ return;
6962
+ }
6963
+
6964
+ if (response.ok) {
6965
+ const data = await response.json();
6966
+ if (data.profile) {
6967
+ applyProfileToUI(data.profile);
6968
+ }
6969
+ if (data.system_prompt) {
6970
+ apiState.systemPrompt = data.system_prompt;
6971
+ updatePromptDisplay(data.system_prompt);
6972
+ }
6973
+ } else {
6974
+ // Revert toggle on failure
6975
+ const toggle = document.getElementById('toggle-' + feature);
6976
+ if (toggle) {
6977
+ profileToggleLock = true;
6978
+ toggle.checked = !enabled;
6979
+ profileToggleLock = false;
6980
+ }
6981
+ }
6982
+ } catch (err) {
6983
+ console.error('Toggle update failed:', err);
6984
+ // Revert toggle
6985
+ const toggle = document.getElementById('toggle-' + feature);
6986
+ if (toggle) {
6987
+ profileToggleLock = true;
6988
+ toggle.checked = !enabled;
6989
+ profileToggleLock = false;
6990
+ }
6991
+ }
6992
+ }
6993
+
6994
+ // Wire up toggle switches
6995
+ document.querySelectorAll('.toggle-switch input').forEach(function(toggle) {
6996
+ toggle.addEventListener('change', function() {
6997
+ const feature = this.getAttribute('data-feature');
6998
+ handleToggle(feature, this.checked);
6999
+ });
7000
+ });
7001
+
7002
+ // Wire up configure buttons
7003
+ document.querySelectorAll('.config-btn').forEach(function(btn) {
7004
+ btn.addEventListener('click', function() {
7005
+ const feature = this.getAttribute('data-config');
7006
+ const panel = document.getElementById('config-' + feature);
7007
+ if (panel) {
7008
+ panel.classList.toggle('open');
7009
+ this.textContent = panel.classList.contains('open') ? 'Close' : 'Configure';
7010
+ }
7011
+ });
7012
+ });
7013
+
7014
+ // Injection sensitivity handler
7015
+ document.querySelectorAll('#config-injection_detection .sensitivity-option').forEach(function(btn) {
7016
+ btn.addEventListener('click', async function() {
7017
+ const sensitivity = this.getAttribute('data-sensitivity');
7018
+ const response = await fetch(API_BASE + '/api/sovereignty-profile', {
7019
+ method: 'POST',
7020
+ headers: {
7021
+ 'Authorization': 'Bearer ' + AUTH_TOKEN,
7022
+ 'Content-Type': 'application/json',
7023
+ },
7024
+ body: JSON.stringify({ injection_detection: { sensitivity: sensitivity } }),
7025
+ });
7026
+ if (response.ok) {
7027
+ document.querySelectorAll('#config-injection_detection .sensitivity-option').forEach(function(b) {
7028
+ b.classList.toggle('selected', b.getAttribute('data-sensitivity') === sensitivity);
7029
+ });
7030
+ const data = await response.json();
7031
+ if (data.profile) applyProfileToUI(data.profile);
7032
+ if (data.system_prompt) {
7033
+ apiState.systemPrompt = data.system_prompt;
7034
+ updatePromptDisplay(data.system_prompt);
7035
+ }
7036
+ }
7037
+ });
7038
+ });
7039
+
7040
+ // Context gating policy selector handler
7041
+ document.getElementById('config-context-policy').addEventListener('change', async function() {
7042
+ const policyId = this.value;
7043
+ const response = await fetch(API_BASE + '/api/sovereignty-profile', {
7044
+ method: 'POST',
7045
+ headers: {
7046
+ 'Authorization': 'Bearer ' + AUTH_TOKEN,
7047
+ 'Content-Type': 'application/json',
7048
+ },
7049
+ body: JSON.stringify({ context_gating: { policy_id: policyId } }),
7050
+ });
7051
+ if (response.ok) {
7052
+ const data = await response.json();
7053
+ if (data.profile) applyProfileToUI(data.profile);
7054
+ if (data.system_prompt) {
7055
+ apiState.systemPrompt = data.system_prompt;
7056
+ updatePromptDisplay(data.system_prompt);
7057
+ }
7058
+ }
7059
+ });
7060
+
7061
+ // Generate prompt button
7062
+ document.getElementById('generate-prompt-btn').addEventListener('click', async () => {
7063
+ const data = await fetchAPI('/api/sovereignty-profile');
7064
+ if (data && data.system_prompt) {
7065
+ apiState.systemPrompt = data.system_prompt;
7066
+ updatePromptDisplay(data.system_prompt);
7067
+ }
7068
+ });
7069
+
7070
+ // Copy prompt button
7071
+ document.getElementById('copy-prompt-btn').addEventListener('click', async () => {
7072
+ const content = document.getElementById('system-prompt-output');
7073
+ if (!content || !content.textContent) return;
7074
+ try {
7075
+ await navigator.clipboard.writeText(content.textContent);
7076
+ const btn = document.getElementById('copy-prompt-btn');
7077
+ const original = btn.textContent;
7078
+ btn.textContent = 'Copied!';
7079
+ setTimeout(() => { btn.textContent = original; }, 2000);
7080
+ } catch (err) {
7081
+ console.error('Copy failed:', err);
7082
+ }
7083
+ });
7084
+
7085
+ // \u2500\u2500 Proxy Server Management \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
7086
+
7087
+ let proxyServers = [];
7088
+
7089
+ async function loadProxyServers() {
7090
+ try {
7091
+ const resp = await fetch(API_BASE + '/api/proxy/servers', {
7092
+ headers: { 'Authorization': 'Bearer ' + AUTH_TOKEN },
7093
+ });
7094
+ if (!resp.ok) return;
7095
+ const data = await resp.json();
7096
+ proxyServers = data.servers || [];
7097
+ renderProxyServers();
7098
+ } catch (err) {
7099
+ // Proxy endpoint may not be available
7100
+ }
7101
+ }
7102
+
7103
+ function renderProxyServers() {
7104
+ const container = document.getElementById('proxy-servers-list');
7105
+ if (!container) return;
7106
+
7107
+ if (proxyServers.length === 0) {
7108
+ container.innerHTML = '<div class="empty-state">No upstream servers configured</div>';
7109
+ return;
7110
+ }
7111
+
7112
+ container.innerHTML = proxyServers.map(server => {
7113
+ const stateColor = server.state === 'connected' ? 'var(--green)' :
7114
+ server.state === 'connecting' ? 'var(--amber)' : 'var(--red)';
7115
+ const stateLabel = server.state || 'disconnected';
7116
+ const tierLabel = 'Tier ' + server.default_tier;
7117
+
7118
+ return \`
7119
+ <div class="proxy-server-card" data-server="\${esc(server.name)}" style="padding: 12px 16px; border-bottom: 1px solid var(--border);">
7120
+ <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px;">
7121
+ <div style="display: flex; align-items: center; gap: 8px;">
7122
+ <span style="color: \${stateColor}; font-size: 10px;">\\u25CF</span>
7123
+ <span style="font-weight: 600; font-size: 14px;">\${esc(server.name)}</span>
7124
+ <span style="font-size: 11px; color: var(--text-secondary); background: var(--bg); padding: 2px 6px; border-radius: 3px;">\${esc(server.transport_type)}</span>
7125
+ </div>
7126
+ <div style="display: flex; align-items: center; gap: 8px;">
7127
+ <span style="font-size: 11px; color: var(--text-secondary);">\${server.tool_count} tools</span>
7128
+ <span style="font-size: 11px; color: var(--blue); background: rgba(88,166,255,0.1); padding: 2px 6px; border-radius: 3px;">\${tierLabel}</span>
7129
+ <button class="proxy-remove-btn" data-server="\${esc(server.name)}" style="background: none; border: none; color: var(--red); cursor: pointer; font-size: 14px; padding: 2px 6px;" title="Remove server">\\u00D7</button>
7130
+ </div>
7131
+ </div>
7132
+ <div style="font-size: 11px; color: var(--text-secondary);">
7133
+ Status: <span style="color: \${stateColor}">\${esc(stateLabel)}</span>
7134
+ \${server.error ? '<span style="color: var(--red);"> \u2014 ' + esc(server.error) + '</span>' : ''}
7135
+ </div>
7136
+ <div class="proxy-tools-expand" style="margin-top: 8px; display: none;">
7137
+ <div style="font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;">Discovered Tools:</div>
7138
+ <div class="proxy-tools-list" style="font-size: 11px; font-family: monospace; color: var(--text-primary); max-height: 150px; overflow-y: auto;"></div>
7139
+ </div>
7140
+ </div>
7141
+ \`;
7142
+ }).join('');
7143
+
7144
+ // Attach remove handlers
7145
+ container.querySelectorAll('.proxy-remove-btn').forEach(btn => {
7146
+ btn.addEventListener('click', (e) => {
7147
+ const serverName = e.target.dataset.server;
7148
+ removeProxyServer(serverName);
7149
+ });
7150
+ });
7151
+
7152
+ // Attach expand/collapse on card click
7153
+ container.querySelectorAll('.proxy-server-card').forEach(card => {
7154
+ card.style.cursor = 'pointer';
7155
+ card.addEventListener('click', (e) => {
7156
+ if (e.target.classList.contains('proxy-remove-btn')) return;
7157
+ const expand = card.querySelector('.proxy-tools-expand');
7158
+ if (expand) {
7159
+ expand.style.display = expand.style.display === 'none' ? 'block' : 'none';
7160
+ }
7161
+ });
7162
+ });
7163
+ }
7164
+
7165
+ function updateProxyServerStatus(serverName, state, toolCount, error) {
7166
+ const server = proxyServers.find(s => s.name === serverName);
7167
+ if (server) {
7168
+ server.state = state;
7169
+ server.tool_count = toolCount;
7170
+ server.error = error;
7171
+ renderProxyServers();
7172
+ }
7173
+ }
7174
+
7175
+ async function addProxyServer(serverConfig) {
7176
+ const current = [...proxyServers];
7177
+ // Check for duplicate
7178
+ if (current.find(s => s.name === serverConfig.name)) {
7179
+ alert('A server with that name already exists');
7180
+ return;
7181
+ }
7182
+ current.push(serverConfig);
7183
+
7184
+ try {
7185
+ const resp = await fetch(API_BASE + '/api/proxy/servers', {
7186
+ method: 'POST',
7187
+ headers: {
7188
+ 'Authorization': 'Bearer ' + AUTH_TOKEN,
7189
+ 'Content-Type': 'application/json',
7190
+ },
7191
+ body: JSON.stringify({ upstream_servers: current }),
7192
+ });
7193
+ if (!resp.ok) {
7194
+ const err = await resp.json();
7195
+ alert('Failed to add server: ' + (err.error || 'Unknown error'));
7196
+ return;
7197
+ }
7198
+ await loadProxyServers();
7199
+ } catch (err) {
7200
+ alert('Failed to add server: ' + err.message);
7201
+ }
7202
+ }
7203
+
7204
+ async function removeProxyServer(serverName) {
7205
+ if (!confirm('Remove upstream server "' + serverName + '"?')) return;
7206
+
7207
+ const updated = proxyServers.filter(s => s.name !== serverName);
7208
+
7209
+ try {
7210
+ const resp = await fetch(API_BASE + '/api/proxy/servers', {
7211
+ method: 'POST',
7212
+ headers: {
7213
+ 'Authorization': 'Bearer ' + AUTH_TOKEN,
7214
+ 'Content-Type': 'application/json',
7215
+ },
7216
+ body: JSON.stringify({ upstream_servers: updated }),
7217
+ });
7218
+ if (!resp.ok) {
7219
+ const err = await resp.json();
7220
+ alert('Failed to remove server: ' + (err.error || 'Unknown error'));
7221
+ return;
7222
+ }
7223
+ await loadProxyServers();
7224
+ } catch (err) {
7225
+ alert('Failed to remove server: ' + err.message);
7226
+ }
7227
+ }
7228
+
7229
+ // Add Server form handlers
7230
+ (function setupProxyForm() {
7231
+ const addBtn = document.getElementById('add-proxy-server-btn');
7232
+ const form = document.getElementById('add-server-form');
7233
+ const saveBtn = document.getElementById('save-server-btn');
7234
+ const cancelBtn = document.getElementById('cancel-server-btn');
7235
+ const transportSelect = document.getElementById('new-server-transport');
7236
+ const stdioFields = document.getElementById('stdio-fields');
7237
+ const sseFields = document.getElementById('sse-fields');
7238
+
7239
+ if (!addBtn || !form) return;
7240
+
7241
+ addBtn.addEventListener('click', () => {
7242
+ form.style.display = form.style.display === 'none' ? 'block' : 'none';
7243
+ });
7244
+
7245
+ cancelBtn.addEventListener('click', () => {
7246
+ form.style.display = 'none';
7247
+ });
7248
+
7249
+ transportSelect.addEventListener('change', () => {
7250
+ if (transportSelect.value === 'stdio') {
7251
+ stdioFields.style.display = 'block';
7252
+ sseFields.style.display = 'none';
7253
+ } else {
7254
+ stdioFields.style.display = 'none';
7255
+ sseFields.style.display = 'block';
7256
+ }
7257
+ });
7258
+
7259
+ saveBtn.addEventListener('click', () => {
7260
+ const name = document.getElementById('new-server-name').value.trim();
7261
+ const type = transportSelect.value;
7262
+ const tier = parseInt(document.getElementById('new-server-tier').value, 10);
7263
+
7264
+ if (!name) { alert('Server name is required'); return; }
7265
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) { alert('Name must contain only letters, numbers, hyphens, and underscores'); return; }
7266
+
7267
+ const transport = { type };
7268
+ if (type === 'stdio') {
7269
+ const command = document.getElementById('new-server-command').value.trim();
7270
+ if (!command) { alert('Command is required for stdio transport'); return; }
7271
+ transport.command = command;
7272
+ const argsStr = document.getElementById('new-server-args').value.trim();
7273
+ if (argsStr) {
7274
+ transport.args = argsStr.split(',').map(s => s.trim()).filter(Boolean);
7275
+ }
7276
+ } else {
7277
+ const url = document.getElementById('new-server-url').value.trim();
7278
+ if (!url) { alert('URL is required for SSE transport'); return; }
7279
+ transport.url = url;
7280
+ }
7281
+
7282
+ addProxyServer({
7283
+ name,
7284
+ transport,
7285
+ enabled: true,
7286
+ default_tier: tier,
7287
+ });
7288
+
7289
+ // Reset form
7290
+ form.style.display = 'none';
7291
+ document.getElementById('new-server-name').value = '';
7292
+ document.getElementById('new-server-command').value = '';
7293
+ document.getElementById('new-server-args').value = '';
7294
+ document.getElementById('new-server-url').value = '';
7295
+ });
7296
+ })();
7297
+
7298
+ // Initialize
7299
+ async function initialize() {
7300
+ if (!AUTH_TOKEN) {
7301
+ redirectToLogin();
7302
+ return;
7303
+ }
7304
+
7305
+ // Initial data fetch
7306
+ await Promise.all([
7307
+ updateSovereignty(),
7308
+ updateIdentity(),
7309
+ updateHandshakes(),
7310
+ updateSHR(),
7311
+ updateStatus(),
7312
+ updateSovereigntyProfile(),
7313
+ loadProxyServers(),
7314
+ ]);
7315
+
7316
+ // Setup SSE for real-time updates
7317
+ setupSSE();
7318
+
7319
+ // Refresh status periodically
7320
+ setInterval(updateStatus, 30000);
7321
+ }
7322
+
7323
+ // Start
7324
+ initialize();
7325
+ </script>
7326
+ </body>
7327
+ </html>`;
7328
+ }
7329
+
7330
+ // src/system-prompt-generator.ts
7331
+ var FEATURE_INFO = {
7332
+ audit_logging: {
7333
+ name: "Audit Logging",
7334
+ activeDescription: "All your tool calls are logged to an encrypted audit trail. No action needed \u2014 this is automatic. You can query the log with sanctuary/monitor_audit_log if you need to review past activity.",
7335
+ toolNames: ["sanctuary/monitor_audit_log"],
7336
+ disabledDescription: "audit logging (sanctuary/monitor_audit_log)",
7337
+ usageExample: "Automatic \u2014 every tool call you make is recorded. No explicit action required."
6572
7338
  },
6573
7339
  injection_detection: {
6574
7340
  name: "Injection Detection",
6575
- activeDescription: "Your tool call arguments are scanned for prompt injection attempts. No action needed \u2014 this is automatic.",
6576
- disabledDescription: "injection detection"
7341
+ activeDescription: "Your tool call arguments are scanned for prompt injection attempts. This is automatic \u2014 no action needed. If injection is detected in your input, the call will be blocked and you will receive an error. Do not retry blocked calls with the same input.",
7342
+ disabledDescription: "injection detection",
7343
+ usageExample: "Automatic \u2014 if a tool call is blocked with an injection alert, do not retry with the same arguments."
6577
7344
  },
6578
7345
  context_gating: {
6579
7346
  name: "Context Gating",
6580
- activeDescription: "Before making outbound calls to remote providers, filter your context through sanctuary/context_gate_filter to ensure minimum-necessary disclosure.",
6581
- toolNames: ["sanctuary/context_gate_filter", "sanctuary/context_gate_set_policy"],
6582
- disabledDescription: "context gating (sanctuary/context_gate_filter)"
7347
+ activeDescription: "Before sending context to any external API (LLM inference, tool APIs, logging services), call sanctuary/context_gate_filter to strip sensitive fields. Use sanctuary/context_gate_set_policy to define filtering rules, or sanctuary/context_gate_apply_template for presets.",
7348
+ toolNames: [
7349
+ "sanctuary/context_gate_filter",
7350
+ "sanctuary/context_gate_set_policy",
7351
+ "sanctuary/context_gate_apply_template",
7352
+ "sanctuary/context_gate_recommend",
7353
+ "sanctuary/context_gate_list_policies"
7354
+ ],
7355
+ disabledDescription: "context gating (sanctuary/context_gate_filter)",
7356
+ usageExample: "Before calling an external API, run: sanctuary/context_gate_filter with your context object and policy_id to get a filtered version."
6583
7357
  },
6584
7358
  approval_gate: {
6585
7359
  name: "Approval Gates",
6586
- activeDescription: "High-risk operations require human approval before execution. Tier 1 operations always require approval; Tier 2 operations trigger approval on anomaly detection.",
6587
- disabledDescription: "approval gates"
7360
+ activeDescription: "High-risk operations require human approval before execution. Tier 1 operations (export, import, key rotation, deletion) always require approval. Tier 2 operations trigger approval when anomalous behavior is detected. When an operation is held for approval, you will receive an async response \u2014 wait for the human decision before proceeding.",
7361
+ disabledDescription: "approval gates",
7362
+ usageExample: "When you call a Tier 1 operation (e.g., state_export), expect an async hold. The human operator will approve or deny via the dashboard."
6588
7363
  },
6589
7364
  zk_proofs: {
6590
7365
  name: "Zero-Knowledge Proofs",
6591
- activeDescription: "You can prove claims about your data without revealing the underlying values. Use sanctuary/zk_commit to create commitments, sanctuary/zk_prove for proofs of knowledge, and sanctuary/zk_range_prove for range proofs.",
6592
- toolNames: ["sanctuary/zk_commit", "sanctuary/zk_prove", "sanctuary/zk_range_prove"],
6593
- disabledDescription: "zero-knowledge proofs (sanctuary/zk_commit, sanctuary/zk_prove)"
7366
+ activeDescription: "You can prove claims about your data without revealing the underlying values. Use sanctuary/zk_commit to create a Pedersen commitment, sanctuary/zk_prove (Schnorr proof) to prove you know a committed value, and sanctuary/zk_range_prove to prove a value falls within a range \u2014 all without disclosing the actual data. For simpler SHA-256 commitments, use sanctuary/proof_commitment.",
7367
+ toolNames: [
7368
+ "sanctuary/zk_commit",
7369
+ "sanctuary/zk_prove",
7370
+ "sanctuary/zk_range_prove",
7371
+ "sanctuary/proof_commitment"
7372
+ ],
7373
+ disabledDescription: "zero-knowledge proofs (sanctuary/zk_commit, sanctuary/zk_prove)",
7374
+ usageExample: "To prove a claim without revealing data: first sanctuary/zk_commit to commit, then sanctuary/zk_prove or sanctuary/zk_range_prove to generate a verifiable proof."
6594
7375
  }
6595
7376
  };
6596
7377
  function generateSystemPrompt(profile) {
6597
7378
  const activeFeatures = [];
6598
7379
  const inactiveFeatures = [];
7380
+ const activeKeys = [];
6599
7381
  const featureKeys = [
6600
7382
  "audit_logging",
6601
7383
  "injection_detection",
@@ -6607,6 +7389,7 @@ function generateSystemPrompt(profile) {
6607
7389
  const featureConfig = profile.features[key];
6608
7390
  const info = FEATURE_INFO[key];
6609
7391
  if (featureConfig.enabled) {
7392
+ activeKeys.push(key);
6610
7393
  let desc = `- ${info.name}: ${info.activeDescription}`;
6611
7394
  if (key === "injection_detection" && "sensitivity" in featureConfig && featureConfig.sensitivity) {
6612
7395
  desc += ` Sensitivity: ${featureConfig.sensitivity}.`;
@@ -6619,14 +7402,25 @@ function generateSystemPrompt(profile) {
6619
7402
  inactiveFeatures.push(info.disabledDescription);
6620
7403
  }
6621
7404
  }
6622
- const lines = [
6623
- "You are protected by Sanctuary sovereignty infrastructure. The following protections are active:",
6624
- ""
6625
- ];
7405
+ const lines = [];
7406
+ if (activeKeys.length > 0) {
7407
+ lines.push("QUICK START:");
7408
+ const quickStartItems = buildQuickStart(activeKeys);
7409
+ for (const item of quickStartItems) {
7410
+ lines.push(` ${item}`);
7411
+ }
7412
+ lines.push("");
7413
+ }
7414
+ lines.push(
7415
+ "You are protected by Sanctuary sovereignty infrastructure. The following protections are active:"
7416
+ );
7417
+ lines.push("");
6626
7418
  if (activeFeatures.length > 0) {
6627
7419
  lines.push(...activeFeatures);
6628
7420
  } else {
6629
- lines.push("- No features are currently enabled. Contact your operator to configure protections.");
7421
+ lines.push(
7422
+ "- No features are currently enabled. Contact your operator to configure protections."
7423
+ );
6630
7424
  }
6631
7425
  if (inactiveFeatures.length > 0) {
6632
7426
  lines.push("");
@@ -6636,6 +7430,35 @@ function generateSystemPrompt(profile) {
6636
7430
  }
6637
7431
  return lines.join("\n");
6638
7432
  }
7433
+ function buildQuickStart(activeKeys) {
7434
+ const items = [];
7435
+ if (activeKeys.includes("context_gating")) {
7436
+ items.push(
7437
+ "1. ALWAYS call sanctuary/context_gate_filter before sending context to external APIs."
7438
+ );
7439
+ }
7440
+ if (activeKeys.includes("zk_proofs")) {
7441
+ items.push(
7442
+ `${items.length + 1}. Use sanctuary/zk_commit to prove claims without revealing underlying data.`
7443
+ );
7444
+ }
7445
+ if (activeKeys.includes("approval_gate")) {
7446
+ items.push(
7447
+ `${items.length + 1}. High-risk operations will be held for human approval \u2014 expect async responses.`
7448
+ );
7449
+ }
7450
+ if (items.length === 0) {
7451
+ if (activeKeys.includes("audit_logging")) {
7452
+ items.push("1. All tool calls are automatically logged to an encrypted audit trail.");
7453
+ }
7454
+ if (activeKeys.includes("injection_detection")) {
7455
+ items.push(
7456
+ `${items.length + 1}. Tool arguments are scanned for injection \u2014 blocked calls should not be retried.`
7457
+ );
7458
+ }
7459
+ }
7460
+ return items;
7461
+ }
6639
7462
 
6640
7463
  // src/principal-policy/dashboard.ts
6641
7464
  var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
@@ -6658,6 +7481,7 @@ var DashboardApprovalChannel = class {
6658
7481
  shrOpts = null;
6659
7482
  _sanctuaryConfig = null;
6660
7483
  profileStore = null;
7484
+ clientManager = null;
6661
7485
  dashboardHTML;
6662
7486
  loginHTML;
6663
7487
  authToken;
@@ -6679,8 +7503,7 @@ var DashboardApprovalChannel = class {
6679
7503
  this.sessionTTLMs = isLocalhost ? SESSION_TTL_LOCAL_MS : SESSION_TTL_REMOTE_MS;
6680
7504
  this.dashboardHTML = generateDashboardHTML({
6681
7505
  timeoutSeconds: config.timeout_seconds,
6682
- serverVersion: SANCTUARY_VERSION,
6683
- authToken: this.authToken
7506
+ serverVersion: SANCTUARY_VERSION
6684
7507
  });
6685
7508
  this.loginHTML = generateLoginHTML({ serverVersion: SANCTUARY_VERSION });
6686
7509
  this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
@@ -6698,6 +7521,7 @@ var DashboardApprovalChannel = class {
6698
7521
  if (deps.shrOpts) this.shrOpts = deps.shrOpts;
6699
7522
  if (deps.sanctuaryConfig) this._sanctuaryConfig = deps.sanctuaryConfig;
6700
7523
  if (deps.profileStore) this.profileStore = deps.profileStore;
7524
+ if (deps.clientManager) this.clientManager = deps.clientManager;
6701
7525
  }
6702
7526
  /**
6703
7527
  * Mark this dashboard as running in standalone mode.
@@ -6747,7 +7571,24 @@ var DashboardApprovalChannel = class {
6747
7571
  }
6748
7572
  resolve();
6749
7573
  });
6750
- this.httpServer.on("error", reject);
7574
+ this.httpServer.on("error", (err) => {
7575
+ if (err.code === "EADDRINUSE") {
7576
+ const port = this.config.port;
7577
+ process.stderr.write(
7578
+ `
7579
+ \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
7580
+ \u2551 Port ${port} is already in use. \u2551
7581
+ \u2551 \u2551
7582
+ \u2551 Another Sanctuary Dashboard may still be running. \u2551
7583
+ \u2551 To fix: lsof -ti:${port} | xargs kill \u2551
7584
+ \u2551 Then restart the dashboard. \u2551
7585
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
7586
+
7587
+ `
7588
+ );
7589
+ }
7590
+ reject(err);
7591
+ });
6751
7592
  });
6752
7593
  }
6753
7594
  /**
@@ -7027,7 +7868,7 @@ var DashboardApprovalChannel = class {
7027
7868
  if (!this.checkAuth(req, url, res)) return;
7028
7869
  if (!this.checkRateLimit(req, res, "general")) return;
7029
7870
  try {
7030
- if (method === "GET" && url.pathname === "/") {
7871
+ if (method === "GET" && (url.pathname === "/" || url.pathname === "/dashboard")) {
7031
7872
  this.serveDashboard(res);
7032
7873
  } else if (method === "GET" && url.pathname === "/events") {
7033
7874
  this.handleSSE(req, res);
@@ -7049,6 +7890,10 @@ var DashboardApprovalChannel = class {
7049
7890
  this.handleSovereigntyProfileGet(res);
7050
7891
  } else if (method === "POST" && url.pathname === "/api/sovereignty-profile") {
7051
7892
  this.handleSovereigntyProfileUpdate(req, res);
7893
+ } else if (method === "GET" && url.pathname === "/api/proxy/servers") {
7894
+ this.handleProxyServers(res);
7895
+ } else if (method === "POST" && url.pathname === "/api/proxy/servers") {
7896
+ this.handleProxyServersUpdate(req, res);
7052
7897
  } else if (method === "POST" && url.pathname.startsWith("/api/approve/")) {
7053
7898
  if (!this.checkRateLimit(req, res, "decisions")) return;
7054
7899
  const id = url.pathname.slice("/api/approve/".length);
@@ -7395,6 +8240,85 @@ data: ${JSON.stringify(initData)}
7395
8240
  }
7396
8241
  });
7397
8242
  }
8243
+ // ── Proxy Server Handlers ───────────────────────────────────────────
8244
+ /**
8245
+ * GET /api/proxy/servers — list upstream proxy servers and their status.
8246
+ */
8247
+ handleProxyServers(res) {
8248
+ const profile = this.profileStore?.get();
8249
+ const upstreamServers = profile?.upstream_servers ?? [];
8250
+ const clientStatus = this.clientManager?.getStatus() ?? [];
8251
+ const servers = upstreamServers.map((server) => {
8252
+ const status = clientStatus.find((s) => s.name === server.name);
8253
+ return {
8254
+ name: server.name,
8255
+ transport_type: server.transport.type,
8256
+ enabled: server.enabled,
8257
+ default_tier: server.default_tier,
8258
+ state: status?.state ?? "disconnected",
8259
+ tool_count: status?.tool_count ?? 0,
8260
+ error: status?.error,
8261
+ tool_overrides: server.tool_overrides ?? {}
8262
+ };
8263
+ });
8264
+ res.writeHead(200, { "Content-Type": "application/json" });
8265
+ res.end(JSON.stringify({ servers }));
8266
+ }
8267
+ /**
8268
+ * POST /api/proxy/servers — update upstream server configuration.
8269
+ * This is a dashboard action (human-initiated), so it's allowed with audit logging
8270
+ * rather than requiring Tier 1 approval.
8271
+ */
8272
+ handleProxyServersUpdate(req, res) {
8273
+ if (!this.profileStore) {
8274
+ res.writeHead(500, { "Content-Type": "application/json" });
8275
+ res.end(JSON.stringify({ error: "Profile store not available" }));
8276
+ return;
8277
+ }
8278
+ let body = "";
8279
+ let destroyed = false;
8280
+ req.on("data", (chunk) => {
8281
+ body += chunk.toString();
8282
+ if (body.length > 16384) {
8283
+ destroyed = true;
8284
+ res.writeHead(413, { "Content-Type": "application/json" });
8285
+ res.end(JSON.stringify({ error: "Request body too large" }));
8286
+ req.destroy();
8287
+ }
8288
+ });
8289
+ req.on("end", async () => {
8290
+ if (destroyed) return;
8291
+ try {
8292
+ const { upstream_servers } = JSON.parse(body);
8293
+ const updated = await this.profileStore.update({ upstream_servers });
8294
+ if (this.auditLog) {
8295
+ this.auditLog.append("l2", "proxy_servers_update_dashboard", "dashboard", {
8296
+ server_count: upstream_servers.length,
8297
+ servers: upstream_servers.map((s) => ({
8298
+ name: s.name,
8299
+ type: s.transport.type,
8300
+ enabled: s.enabled,
8301
+ tier: s.default_tier
8302
+ }))
8303
+ });
8304
+ }
8305
+ if (this.clientManager && updated.upstream_servers) {
8306
+ this.clientManager.configure(updated.upstream_servers).catch(() => {
8307
+ });
8308
+ }
8309
+ this.broadcastSSE("proxy-servers-update", {
8310
+ servers: updated.upstream_servers ?? [],
8311
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
8312
+ });
8313
+ res.writeHead(200, { "Content-Type": "application/json" });
8314
+ res.end(JSON.stringify({ upstream_servers: updated.upstream_servers ?? [] }));
8315
+ } catch (err) {
8316
+ const message = err instanceof Error ? err.message : "Invalid request";
8317
+ res.writeHead(400, { "Content-Type": "application/json" });
8318
+ res.end(JSON.stringify({ error: message }));
8319
+ }
8320
+ });
8321
+ }
7398
8322
  // ── SSE Broadcasting ────────────────────────────────────────────────
7399
8323
  broadcastSSE(event, data) {
7400
8324
  const message = `event: ${event}
@@ -7751,6 +8675,70 @@ var TOOL_INVOCATION_PATTERNS = [
7751
8675
  ];
7752
8676
  var URL_PATTERN = /https?:\/\/[^\s"'<>]+/i;
7753
8677
  var EMAIL_PATTERN = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
8678
+ var INVISIBLE_CHARS = [
8679
+ "\u200B",
8680
+ // Zero-width space
8681
+ "\u200C",
8682
+ // Zero-width non-joiner
8683
+ "\u200D",
8684
+ // Zero-width joiner
8685
+ "\uFEFF",
8686
+ // Zero-width no-break space (BOM)
8687
+ "\xAD",
8688
+ // Soft hyphen
8689
+ "\u200E",
8690
+ // Left-to-right mark
8691
+ "\u200F",
8692
+ // Right-to-left mark
8693
+ "\u2060",
8694
+ // Word joiner
8695
+ "\u2061",
8696
+ // Function application
8697
+ "\u2062",
8698
+ // Invisible times
8699
+ "\u2063",
8700
+ // Invisible separator
8701
+ "\u2064",
8702
+ // Invisible plus
8703
+ "\u180E",
8704
+ // Mongolian vowel separator
8705
+ "\u034F",
8706
+ // Combining grapheme joiner
8707
+ "\u061C",
8708
+ // Arabic letter mark
8709
+ "\u115F",
8710
+ // Hangul choseong filler
8711
+ "\u1160",
8712
+ // Hangul jungseong filler
8713
+ "\u17B4",
8714
+ // Khmer vowel inherent AQ
8715
+ "\u17B5",
8716
+ // Khmer vowel inherent AA
8717
+ "\u3164",
8718
+ // Hangul filler
8719
+ "\uFFA0",
8720
+ // Halfwidth hangul filler
8721
+ "\u202A",
8722
+ // Left-to-Right Embedding (LRE)
8723
+ "\u202B",
8724
+ // Right-to-Left Embedding (RLE)
8725
+ "\u202C",
8726
+ // Pop Directional Formatting (PDF)
8727
+ "\u202D",
8728
+ // Left-to-Right Override (LRO)
8729
+ "\u202E",
8730
+ // Right-to-Left Override (RLO)
8731
+ "\u2066",
8732
+ // Left-to-Right Isolate (LRI)
8733
+ "\u2067",
8734
+ // Right-to-Left Isolate (RLI)
8735
+ "\u2068",
8736
+ // First Strong Isolate (FSI)
8737
+ "\u2069"
8738
+ // Pop Directional Isolate (PDI)
8739
+ ];
8740
+ var VARIATION_SELECTOR_RANGE_START = 65024;
8741
+ var VARIATION_SELECTOR_RANGE_END = 65039;
7754
8742
  var ZERO_WIDTH_CHARS = [
7755
8743
  "\u200B",
7756
8744
  // Zero-width space
@@ -7761,6 +8749,66 @@ var ZERO_WIDTH_CHARS = [
7761
8749
  "\uFEFF"
7762
8750
  // Zero-width no-break space
7763
8751
  ];
8752
+ var BASE64_STANDARD_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
8753
+ var BASE64URL_PATTERN = /^[A-Za-z0-9_-]+={0,2}$/;
8754
+ var BASE64_BLOCK_PATTERN = /[A-Za-z0-9+/]{20,}={0,2}/g;
8755
+ var HEX_ENCODED_PATTERN = /(?:0x)?[0-9a-fA-F]{20,}/g;
8756
+ var HTML_ENTITY_PATTERN = /&#(?:x[0-9a-fA-F]{2,4}|[0-9]{2,5});/g;
8757
+ var URL_ENCODED_PATTERN = /(?:%[0-9a-fA-F]{2}){4,}/g;
8758
+ var SECRET_PATTERNS = [
8759
+ { pattern: /sk-[a-zA-Z0-9]{20,}/, name: "openai_api_key" },
8760
+ { pattern: /sk-ant-[a-zA-Z0-9_-]{20,}/, name: "anthropic_api_key" },
8761
+ { pattern: /ghp_[a-zA-Z0-9]{36,}/, name: "github_pat" },
8762
+ { pattern: /gho_[a-zA-Z0-9]{36,}/, name: "github_oauth" },
8763
+ { pattern: /ghs_[a-zA-Z0-9]{36,}/, name: "github_app" },
8764
+ { pattern: /github_pat_[a-zA-Z0-9_]{22,}/, name: "github_fine_grained_pat" },
8765
+ { pattern: /AKIA[0-9A-Z]{16}/, name: "aws_access_key" },
8766
+ { pattern: /xoxb-[0-9]{10,}-[a-zA-Z0-9-]+/, name: "slack_bot_token" },
8767
+ { pattern: /xoxp-[0-9]{10,}-[a-zA-Z0-9-]+/, name: "slack_user_token" },
8768
+ { pattern: /xapp-[0-9]-[A-Z0-9]+-[0-9]+-[a-z0-9]+/, name: "slack_app_token" },
8769
+ { pattern: /(?:Bearer|bearer)\s+[a-zA-Z0-9._~+/=-]{20,}/, name: "bearer_token" },
8770
+ { pattern: /glpat-[a-zA-Z0-9_-]{20,}/, name: "gitlab_pat" },
8771
+ { pattern: /npm_[a-zA-Z0-9]{36,}/, name: "npm_token" },
8772
+ { pattern: /pypi-[a-zA-Z0-9_-]{20,}/, name: "pypi_token" },
8773
+ { pattern: /AIza[a-zA-Z0-9_-]{35}/, name: "google_api_key" },
8774
+ { pattern: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/, name: "sendgrid_api_key" },
8775
+ { pattern: /sq0[a-z]{3}-[a-zA-Z0-9_-]{22,}/, name: "square_api_key" },
8776
+ { pattern: /sk_live_[a-zA-Z0-9]{24,}/, name: "stripe_secret_key" },
8777
+ { pattern: /rk_live_[a-zA-Z0-9]{24,}/, name: "stripe_restricted_key" },
8778
+ { pattern: /(?:-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----)/, name: "private_key_pem" }
8779
+ ];
8780
+ var MARKDOWN_IMAGE_EXFIL_PATTERN = /!\[[^\]]*\]\(https?:\/\/[^)]*[?&](?:data|secret|key|token|password|auth|session|cookie|api_key|access_token)=/i;
8781
+ var INTERNAL_PATH_PATTERNS = [
8782
+ /\/home\/[a-zA-Z0-9_.-]+\//,
8783
+ /\/Users\/[a-zA-Z0-9_.-]+\//,
8784
+ /[A-Z]:\\(?:Users|Documents|Program Files)\\/,
8785
+ /~\/\.(?:ssh|aws|config|gnupg|sanctuary)\//,
8786
+ /\/etc\/(?:passwd|shadow|hosts|ssh)/,
8787
+ /\/var\/(?:log|run|lib)\//
8788
+ ];
8789
+ var PRIVATE_NETWORK_PATTERNS = [
8790
+ /(?:^|\s|\/\/)(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3})/,
8791
+ /(?:^|\s|\/\/)(?:172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3})/,
8792
+ /(?:^|\s|\/\/)(?:192\.168\.\d{1,3}\.\d{1,3})/,
8793
+ /(?:^|\s|\/\/)localhost(?::\d+)?/,
8794
+ /(?:^|\s|\/\/)127\.0\.0\.1/,
8795
+ /(?:^|\s|\/\/)0\.0\.0\.0/,
8796
+ /(?:^|\s|\/\/)\[?::1\]?/
8797
+ ];
8798
+ var OUTPUT_ROLE_MARKER_PATTERNS = [
8799
+ /\bsystem\s*:/i,
8800
+ /\[INST\]/,
8801
+ /\[\/INST\]/,
8802
+ /<\|im_start\|>/,
8803
+ /<\|im_end\|>/,
8804
+ /<\|system\|>/,
8805
+ /<\|user\|>/,
8806
+ /<\|assistant\|>/,
8807
+ /<<\s*SYS\s*>>/,
8808
+ /<<\s*\/SYS\s*>>/,
8809
+ /\[SYSTEM\]/,
8810
+ /### (?:System|Human|Assistant):/
8811
+ ];
7764
8812
  var InjectionDetector = class {
7765
8813
  config;
7766
8814
  stats = {
@@ -7817,6 +8865,50 @@ var InjectionDetector = class {
7817
8865
  recommendation
7818
8866
  };
7819
8867
  }
8868
+ /**
8869
+ * SEC-035: Scan outbound content for secret leaks, data exfiltration,
8870
+ * internal path exposure, and injection artifact survival.
8871
+ *
8872
+ * @param content The outbound content string to scan
8873
+ * @returns DetectionResult with outbound-specific signal types
8874
+ */
8875
+ scanOutbound(content) {
8876
+ this.stats.total_scans++;
8877
+ if (!this.config.enabled) {
8878
+ return {
8879
+ flagged: false,
8880
+ confidence: 0,
8881
+ signals: [],
8882
+ recommendation: "allow"
8883
+ };
8884
+ }
8885
+ const signals = [];
8886
+ this.detectSecretPatterns(content, signals);
8887
+ this.detectOutboundExfiltration(content, signals);
8888
+ this.detectInternalPathLeaks(content, signals);
8889
+ this.detectPrivateNetworkLeaks(content, signals);
8890
+ this.detectOutputRoleMarkers(content, signals);
8891
+ const flagged = signals.length > 0;
8892
+ if (flagged) {
8893
+ this.stats.total_flags++;
8894
+ }
8895
+ for (const sig of signals) {
8896
+ this.stats.signals_by_type[sig.type] = (this.stats.signals_by_type[sig.type] ?? 0) + 1;
8897
+ }
8898
+ const recommendation = this.computeRecommendation(
8899
+ signals,
8900
+ this.config.sensitivity
8901
+ );
8902
+ if (recommendation === "block") {
8903
+ this.stats.total_blocks++;
8904
+ }
8905
+ return {
8906
+ flagged,
8907
+ confidence: this.computeConfidence(signals),
8908
+ signals,
8909
+ recommendation
8910
+ };
8911
+ }
7820
8912
  /**
7821
8913
  * Recursively scan a value and all nested values.
7822
8914
  */
@@ -7845,14 +8937,40 @@ var InjectionDetector = class {
7845
8937
  return;
7846
8938
  }
7847
8939
  const location = path || "root";
7848
- const normalized = this.normalizeConfusables(value.normalize("NFKC"));
7849
- if (normalized !== value) {
8940
+ const invisibleCount = this.countInvisibleChars(value);
8941
+ if (invisibleCount > 3) {
8942
+ signals.push({
8943
+ type: "unicode_smuggling",
8944
+ pattern: `invisible_chars_count_${invisibleCount}`,
8945
+ location,
8946
+ severity: "high"
8947
+ });
8948
+ }
8949
+ this.detectTokenBudgetAttack(value, location, signals);
8950
+ const stripped = this.stripInvisibleChars(value);
8951
+ const nfkcOnly = stripped.normalize("NFKC");
8952
+ const normalized = this.normalizeConfusables(nfkcOnly);
8953
+ if (nfkcOnly !== stripped) {
8954
+ signals.push({
8955
+ type: "encoding_evasion",
8956
+ pattern: "unicode_normalization_delta",
8957
+ location,
8958
+ severity: "medium"
8959
+ });
8960
+ }
8961
+ if (normalized !== nfkcOnly) {
7850
8962
  signals.push({
7851
8963
  type: "encoding_evasion",
7852
8964
  pattern: "unicode_normalization_delta",
7853
8965
  location,
7854
8966
  severity: "medium"
7855
8967
  });
8968
+ signals.push({
8969
+ type: "homoglyph_attack",
8970
+ pattern: "confusable_substitution",
8971
+ location,
8972
+ severity: "high"
8973
+ });
7856
8974
  }
7857
8975
  for (const pattern of ROLE_OVERRIDE_PATTERNS) {
7858
8976
  if (pattern.test(normalized)) {
@@ -7890,21 +9008,363 @@ var InjectionDetector = class {
7890
9008
  }
7891
9009
  }
7892
9010
  this.detectEncodingEvasion(value, location, signals);
9011
+ this.detectEncodedPayloads(value, location, signals);
7893
9012
  this.detectDataExfiltration(value, location, signals);
7894
9013
  this.detectPromptStuffing(value, location, signals);
7895
9014
  }
9015
+ // ═══════════════════════════════════════════════════════════════════════════
9016
+ // SEC-034: Unicode sanitization helpers
9017
+ // ═══════════════════════════════════════════════════════════════════════════
7896
9018
  /**
7897
- * Detect base64 strings and zero-width character evasion.
9019
+ * Count invisible Unicode characters in a string.
9020
+ * Includes zero-width chars, soft hyphens, directional marks,
9021
+ * variation selectors, and other invisible categories.
7898
9022
  */
7899
- detectEncodingEvasion(value, path, signals) {
7900
- if (value.length > 50 && /^[A-Za-z0-9+/]+={0,2}$/.test(value.trim())) {
9023
+ countInvisibleChars(value) {
9024
+ let count = 0;
9025
+ for (const ch of value) {
9026
+ const cp = ch.codePointAt(0);
9027
+ if (cp === void 0) continue;
9028
+ if (INVISIBLE_CHARS.includes(ch)) {
9029
+ count++;
9030
+ continue;
9031
+ }
9032
+ if (cp >= VARIATION_SELECTOR_RANGE_START && cp <= VARIATION_SELECTOR_RANGE_END) {
9033
+ count++;
9034
+ continue;
9035
+ }
9036
+ if (cp >= 917760 && cp <= 917999) {
9037
+ count++;
9038
+ continue;
9039
+ }
9040
+ if (cp >= 917505 && cp <= 917631) {
9041
+ count++;
9042
+ continue;
9043
+ }
9044
+ if (cp >= 65529 && cp <= 65531) {
9045
+ count++;
9046
+ continue;
9047
+ }
9048
+ }
9049
+ return count;
9050
+ }
9051
+ /**
9052
+ * Strip invisible characters from a string for clean pattern matching.
9053
+ * Returns a new string with all invisible chars removed.
9054
+ */
9055
+ stripInvisibleChars(value) {
9056
+ const chars = [];
9057
+ for (const ch of value) {
9058
+ const cp = ch.codePointAt(0);
9059
+ if (cp === void 0) continue;
9060
+ if (INVISIBLE_CHARS.includes(ch)) continue;
9061
+ if (cp >= VARIATION_SELECTOR_RANGE_START && cp <= VARIATION_SELECTOR_RANGE_END) continue;
9062
+ if (cp >= 917760 && cp <= 917999) continue;
9063
+ if (cp >= 917505 && cp <= 917631) continue;
9064
+ if (cp >= 65529 && cp <= 65531) continue;
9065
+ chars.push(ch);
9066
+ }
9067
+ return chars.join("");
9068
+ }
9069
+ /**
9070
+ * SEC-034: Token budget attack detection.
9071
+ * Some Unicode sequences expand dramatically during tokenization (e.g., CJK
9072
+ * ideographs, combining characters, emoji sequences). If the estimated token
9073
+ * cost per character is anomalously high, this may be a wallet-drain payload.
9074
+ *
9075
+ * Heuristic: count chars that typically tokenize into multiple tokens.
9076
+ * If the ratio of estimated tokens to char count exceeds 3x, flag it.
9077
+ */
9078
+ detectTokenBudgetAttack(value, path, signals) {
9079
+ if (value.length < 20) return;
9080
+ const MAX_ANALYSIS_LENGTH = 1e6;
9081
+ if (value.length > MAX_ANALYSIS_LENGTH) {
9082
+ value = value.substring(0, MAX_ANALYSIS_LENGTH);
9083
+ }
9084
+ let estimatedTokens = 0;
9085
+ for (const ch of value) {
9086
+ const cp = ch.codePointAt(0);
9087
+ if (cp === void 0) continue;
9088
+ if (cp <= 127) {
9089
+ estimatedTokens += 0.25;
9090
+ } else if (cp <= 2047) {
9091
+ estimatedTokens += 0.5;
9092
+ } else if (cp <= 65535) {
9093
+ estimatedTokens += 1.5;
9094
+ } else {
9095
+ estimatedTokens += 2.5;
9096
+ }
9097
+ }
9098
+ let charCount = 0;
9099
+ for (const _ch of value) {
9100
+ charCount++;
9101
+ }
9102
+ const ratio = estimatedTokens / charCount;
9103
+ if (ratio > 3) {
7901
9104
  signals.push({
7902
- type: "encoding_evasion",
7903
- pattern: "base64_string",
7904
- location: path || "root",
9105
+ type: "token_budget_attack",
9106
+ pattern: `token_char_ratio_${ratio.toFixed(2)}`,
9107
+ location: path,
7905
9108
  severity: "medium"
7906
9109
  });
7907
9110
  }
9111
+ }
9112
+ // ═══════════════════════════════════════════════════════════════════════════
9113
+ // SEC-034: Encoded payload detection and re-scanning
9114
+ // ═══════════════════════════════════════════════════════════════════════════
9115
+ /**
9116
+ * Detect encoded content (base64, hex, HTML entities, URL encoding),
9117
+ * decode it, and re-scan the decoded content through injection patterns.
9118
+ * If the decoded content contains injection patterns, flag as encoding_evasion.
9119
+ */
9120
+ detectEncodedPayloads(value, path, signals) {
9121
+ const decodedParts = [];
9122
+ const base64Matches = value.match(BASE64_BLOCK_PATTERN);
9123
+ if (base64Matches) {
9124
+ for (const match of base64Matches) {
9125
+ const decoded = this.safeBase64Decode(match);
9126
+ if (decoded !== null) {
9127
+ decodedParts.push(decoded);
9128
+ }
9129
+ }
9130
+ }
9131
+ const hexMatches = value.match(HEX_ENCODED_PATTERN);
9132
+ if (hexMatches) {
9133
+ for (const match of hexMatches) {
9134
+ const decoded = this.safeHexDecode(match);
9135
+ if (decoded !== null) {
9136
+ decodedParts.push(decoded);
9137
+ }
9138
+ }
9139
+ }
9140
+ if (HTML_ENTITY_PATTERN.test(value)) {
9141
+ const decoded = this.decodeHtmlEntities(value);
9142
+ if (decoded !== value) {
9143
+ decodedParts.push(decoded);
9144
+ }
9145
+ }
9146
+ const urlMatches = value.match(URL_ENCODED_PATTERN);
9147
+ if (urlMatches) {
9148
+ for (const match of urlMatches) {
9149
+ const decoded = this.safeUrlDecode(match);
9150
+ if (decoded !== null && decoded !== match) {
9151
+ decodedParts.push(decoded);
9152
+ }
9153
+ }
9154
+ }
9155
+ for (const decoded of decodedParts) {
9156
+ if (this.containsInjectionPatterns(decoded)) {
9157
+ signals.push({
9158
+ type: "encoding_evasion",
9159
+ pattern: "encoded_injection_payload",
9160
+ location: path,
9161
+ severity: "high"
9162
+ });
9163
+ return;
9164
+ }
9165
+ }
9166
+ }
9167
+ /**
9168
+ * Check if a string contains any injection patterns (role override or security bypass).
9169
+ */
9170
+ containsInjectionPatterns(value) {
9171
+ const normalized = this.normalizeConfusables(value.normalize("NFKC"));
9172
+ for (const pattern of ROLE_OVERRIDE_PATTERNS) {
9173
+ if (pattern.test(normalized)) return true;
9174
+ }
9175
+ for (const pattern of SECURITY_BYPASS_PATTERNS) {
9176
+ if (pattern.test(normalized)) return true;
9177
+ }
9178
+ return false;
9179
+ }
9180
+ /**
9181
+ * Safely decode a base64 string. Returns null if it's not valid base64
9182
+ * or doesn't decode to a meaningful string.
9183
+ */
9184
+ safeBase64Decode(value) {
9185
+ try {
9186
+ const cleaned = value.replace(/-/g, "+").replace(/_/g, "/");
9187
+ const padded = cleaned + "=".repeat((4 - cleaned.length % 4) % 4);
9188
+ if (!BASE64_STANDARD_PATTERN.test(padded)) return null;
9189
+ const decoded = Buffer.from(padded, "base64").toString("utf-8");
9190
+ if (this.looksLikeText(decoded)) {
9191
+ return decoded;
9192
+ }
9193
+ return null;
9194
+ } catch {
9195
+ return null;
9196
+ }
9197
+ }
9198
+ /**
9199
+ * Safely decode a hex string. Returns null on failure.
9200
+ */
9201
+ safeHexDecode(value) {
9202
+ try {
9203
+ const hex = value.startsWith("0x") ? value.slice(2) : value;
9204
+ if (hex.length % 2 !== 0) return null;
9205
+ const decoded = Buffer.from(hex, "hex").toString("utf-8");
9206
+ if (this.looksLikeText(decoded)) {
9207
+ return decoded;
9208
+ }
9209
+ return null;
9210
+ } catch {
9211
+ return null;
9212
+ }
9213
+ }
9214
+ /**
9215
+ * Decode HTML numeric entities (&#xHH; and &#DDD;) in a string.
9216
+ */
9217
+ decodeHtmlEntities(value) {
9218
+ return value.replace(/&#x([0-9a-fA-F]{2,4});/g, (_match, hex) => {
9219
+ try {
9220
+ return String.fromCodePoint(parseInt(hex, 16));
9221
+ } catch {
9222
+ return _match;
9223
+ }
9224
+ }).replace(/&#([0-9]{2,5});/g, (_match, dec) => {
9225
+ try {
9226
+ const cp = parseInt(dec, 10);
9227
+ if (cp > 1114111) return _match;
9228
+ return String.fromCodePoint(cp);
9229
+ } catch {
9230
+ return _match;
9231
+ }
9232
+ });
9233
+ }
9234
+ /**
9235
+ * Safely decode a URL-encoded string. Returns null on failure.
9236
+ */
9237
+ safeUrlDecode(value) {
9238
+ try {
9239
+ return decodeURIComponent(value);
9240
+ } catch {
9241
+ return null;
9242
+ }
9243
+ }
9244
+ /**
9245
+ * Heuristic: does this look like readable text (vs. binary garbage)?
9246
+ * Checks that most characters are printable ASCII or common Unicode.
9247
+ */
9248
+ looksLikeText(value) {
9249
+ if (value.length === 0) return false;
9250
+ let printable = 0;
9251
+ for (let i = 0; i < value.length; i++) {
9252
+ const code = value.charCodeAt(i);
9253
+ if (code >= 32 && code <= 126 || code === 10 || code === 13 || code === 9) {
9254
+ printable++;
9255
+ } else if (code >= 128) {
9256
+ if (code < 160) continue;
9257
+ printable++;
9258
+ }
9259
+ }
9260
+ return printable / value.length > 0.7;
9261
+ }
9262
+ // ═══════════════════════════════════════════════════════════════════════════
9263
+ // SEC-035: Outbound content scanning helpers
9264
+ // ═══════════════════════════════════════════════════════════════════════════
9265
+ /**
9266
+ * Detect API keys and secrets in outbound content.
9267
+ */
9268
+ detectSecretPatterns(content, signals) {
9269
+ for (const { pattern, name } of SECRET_PATTERNS) {
9270
+ if (pattern.test(content)) {
9271
+ signals.push({
9272
+ type: "secret_leak",
9273
+ pattern: name,
9274
+ location: "outbound",
9275
+ severity: "high"
9276
+ });
9277
+ }
9278
+ }
9279
+ }
9280
+ /**
9281
+ * Detect data exfiltration via markdown images with data-carrying query params.
9282
+ */
9283
+ detectOutboundExfiltration(content, signals) {
9284
+ if (MARKDOWN_IMAGE_EXFIL_PATTERN.test(content)) {
9285
+ signals.push({
9286
+ type: "data_exfiltration",
9287
+ pattern: "markdown_image_exfil",
9288
+ location: "outbound",
9289
+ severity: "high"
9290
+ });
9291
+ }
9292
+ }
9293
+ /**
9294
+ * Detect internal filesystem path leaks in outbound content.
9295
+ */
9296
+ detectInternalPathLeaks(content, signals) {
9297
+ for (const pattern of INTERNAL_PATH_PATTERNS) {
9298
+ if (pattern.test(content)) {
9299
+ signals.push({
9300
+ type: "internal_path_leak",
9301
+ pattern: pattern.source,
9302
+ location: "outbound",
9303
+ severity: "medium"
9304
+ });
9305
+ return;
9306
+ }
9307
+ }
9308
+ }
9309
+ /**
9310
+ * Detect private IP addresses and localhost references in outbound content.
9311
+ */
9312
+ detectPrivateNetworkLeaks(content, signals) {
9313
+ for (const pattern of PRIVATE_NETWORK_PATTERNS) {
9314
+ if (pattern.test(content)) {
9315
+ signals.push({
9316
+ type: "private_network_leak",
9317
+ pattern: pattern.source,
9318
+ location: "outbound",
9319
+ severity: "medium"
9320
+ });
9321
+ return;
9322
+ }
9323
+ }
9324
+ }
9325
+ /**
9326
+ * Detect role markers / prompt template artifacts in outbound content.
9327
+ * These should never appear in agent output — their presence indicates
9328
+ * injection artifact survival.
9329
+ */
9330
+ detectOutputRoleMarkers(content, signals) {
9331
+ for (const pattern of OUTPUT_ROLE_MARKER_PATTERNS) {
9332
+ if (pattern.test(content)) {
9333
+ signals.push({
9334
+ type: "injection_artifact",
9335
+ pattern: pattern.source,
9336
+ location: "outbound",
9337
+ severity: "high"
9338
+ });
9339
+ return;
9340
+ }
9341
+ }
9342
+ }
9343
+ // ═══════════════════════════════════════════════════════════════════════════
9344
+ // Existing detection methods (enhanced)
9345
+ // ═══════════════════════════════════════════════════════════════════════════
9346
+ /**
9347
+ * Detect base64 strings, base64url, and zero-width character evasion.
9348
+ */
9349
+ detectEncodingEvasion(value, path, signals) {
9350
+ const trimmed = value.trim();
9351
+ if (value.length > 50) {
9352
+ if (BASE64_STANDARD_PATTERN.test(trimmed)) {
9353
+ signals.push({
9354
+ type: "encoding_evasion",
9355
+ pattern: "base64_string",
9356
+ location: path || "root",
9357
+ severity: "medium"
9358
+ });
9359
+ } else if (BASE64URL_PATTERN.test(trimmed) && /[_-]/.test(trimmed)) {
9360
+ signals.push({
9361
+ type: "encoding_evasion",
9362
+ pattern: "base64url_string",
9363
+ location: path || "root",
9364
+ severity: "medium"
9365
+ });
9366
+ }
9367
+ }
7908
9368
  let zeroWidthCount = 0;
7909
9369
  for (const char of ZERO_WIDTH_CHARS) {
7910
9370
  zeroWidthCount += (value.match(new RegExp(char, "g")) || []).length;
@@ -8083,62 +9543,89 @@ var InjectionDetector = class {
8083
9543
  return structuredFields.some((p) => p.test(path));
8084
9544
  }
8085
9545
  /**
8086
- * SEC-032: Map common cross-script confusable characters to their Latin equivalents.
8087
- * NFKC normalization handles fullwidth and compatibility forms, but does NOT map
8088
- * Cyrillic/Greek lookalikes to Latin (they're distinct codepoints by design).
8089
- * This covers the most common confusables used in injection evasion.
9546
+ * SEC-032/SEC-034: Map common cross-script confusable characters to their
9547
+ * Latin equivalents. NFKC normalization handles fullwidth and compatibility
9548
+ * forms, but does NOT map Cyrillic/Greek/Armenian/Georgian lookalikes to
9549
+ * Latin (they're distinct codepoints by design).
9550
+ *
9551
+ * Extended to 50+ confusable pairs covering Cyrillic, Greek, Armenian,
9552
+ * Georgian, Cherokee, and mathematical/symbol lookalikes.
8090
9553
  */
8091
9554
  normalizeConfusables(value) {
8092
9555
  const confusables = {
8093
- // Cyrillic → Latin
9556
+ // ── Cyrillic → Latin ──────────────────────────────────────────────
8094
9557
  "\u0410": "A",
8095
9558
  "\u0430": "a",
8096
9559
  // А а
8097
9560
  "\u0412": "B",
8098
9561
  "\u0432": "b",
8099
- // В (not exact) в (not exact)
9562
+ // В в (visual approximation)
8100
9563
  "\u0421": "C",
8101
9564
  "\u0441": "c",
8102
9565
  // С с
9566
+ "\u0414": "D",
9567
+ // Д (visual approximation)
8103
9568
  "\u0415": "E",
8104
9569
  "\u0435": "e",
8105
9570
  // Е е
8106
9571
  "\u041D": "H",
8107
9572
  "\u043D": "h",
8108
- // Н (not exact) н (not exact)
9573
+ // Н н (visual approximation)
9574
+ "\u0406": "I",
9575
+ "\u0456": "i",
9576
+ // І і (Ukrainian I)
9577
+ "\u0408": "J",
9578
+ // Ј (Serbian Je)
8109
9579
  "\u041A": "K",
8110
9580
  "\u043A": "k",
8111
- // К к (not exact)
9581
+ // К к (visual approximation)
8112
9582
  "\u041C": "M",
8113
9583
  "\u043C": "m",
8114
- // М (not exact) м (not exact)
9584
+ // М м (visual approximation)
8115
9585
  "\u041E": "O",
8116
9586
  "\u043E": "o",
8117
9587
  // О о
8118
9588
  "\u0420": "P",
8119
9589
  "\u0440": "p",
8120
9590
  // Р р
9591
+ "\u0405": "S",
9592
+ "\u0455": "s",
9593
+ // Ѕ ѕ (Macedonian S)
8121
9594
  "\u0422": "T",
8122
9595
  "\u0442": "t",
8123
- // Т (not exact) т (not exact)
9596
+ // Т т (visual approximation)
8124
9597
  "\u0425": "X",
8125
9598
  "\u0445": "x",
8126
9599
  // Х х
8127
9600
  "\u0423": "Y",
8128
9601
  "\u0443": "y",
8129
- // У (not exact) у
8130
- // Greek → Latin
9602
+ // У у (visual approximation)
9603
+ "\u0417": "3",
9604
+ // З (looks like 3)
9605
+ "\u04BB": "h",
9606
+ // һ (Shha)
9607
+ "\u04C0": "I",
9608
+ // Ӏ (Palochka)
9609
+ "\u04CF": "l",
9610
+ // ӏ (Palochka small)
9611
+ // ── Greek → Latin ─────────────────────────────────────────────────
8131
9612
  "\u0391": "A",
8132
9613
  "\u03B1": "a",
8133
- // Α α (not exact)
9614
+ // Α α (alpha not exact)
8134
9615
  "\u0392": "B",
8135
9616
  "\u03B2": "b",
8136
9617
  // Β β (not exact)
9618
+ "\u0393": "G",
9619
+ // Γ (visual approximation)
8137
9620
  "\u0395": "E",
8138
9621
  "\u03B5": "e",
8139
9622
  // Ε ε (not exact)
9623
+ "\u0396": "Z",
9624
+ "\u03B6": "z",
9625
+ // Ζ ζ (not exact)
8140
9626
  "\u0397": "H",
8141
- // Η
9627
+ "\u03B7": "n",
9628
+ // Η η (not exact)
8142
9629
  "\u0399": "I",
8143
9630
  "\u03B9": "i",
8144
9631
  // Ι ι
@@ -8162,8 +9649,78 @@ var InjectionDetector = class {
8162
9649
  "\u03C5": "y",
8163
9650
  // Υ υ (not exact)
8164
9651
  "\u03A7": "X",
8165
- "\u03C7": "x"
9652
+ "\u03C7": "x",
8166
9653
  // Χ χ (not exact)
9654
+ "\u03C9": "w",
9655
+ // ω (omega, visual approximation)
9656
+ // ── Armenian → Latin ──────────────────────────────────────────────
9657
+ "\u0555": "O",
9658
+ // Օ
9659
+ "\u0585": "o",
9660
+ // օ
9661
+ "\u054D": "S",
9662
+ // Ս
9663
+ "\u057D": "s",
9664
+ // ս
9665
+ "\u054C": "L",
9666
+ // Լ (visual approximation)
9667
+ "\u0570": "h",
9668
+ // հ
9669
+ // ── Cherokee → Latin ──────────────────────────────────────────────
9670
+ "\u13A0": "D",
9671
+ // Ꭰ
9672
+ "\u13B3": "W",
9673
+ // Ꮃ
9674
+ "\u13A1": "R",
9675
+ // Ꭱ
9676
+ "\u13AA": "G",
9677
+ // Ꭺ (looks like A but maps to G sound)
9678
+ "\u13D2": "V",
9679
+ // Ꮢ (visual approximation)
9680
+ // ── Georgian → Latin ──────────────────────────────────────────────
9681
+ "\u10D5": "v",
9682
+ // ვ (Georgian letter vin)
9683
+ "\u10D3": "d",
9684
+ // დ (Georgian letter don)
9685
+ "\u10DA": "l",
9686
+ // ლ (Georgian letter las)
9687
+ // ── Latin special → Latin ────────────────────────────────────────
9688
+ "\u0131": "i",
9689
+ // ı (Latin small letter dotless i)
9690
+ // ── Symbols / Mathematical → Latin ────────────────────────────────
9691
+ // Note: NFKC normalization handles mathematical alphanumerics (U+1D400–U+1D7FF)
9692
+ "\u2160": "I",
9693
+ // Ⅰ (Roman numeral one)
9694
+ "\u2164": "V",
9695
+ // Ⅴ (Roman numeral five)
9696
+ "\u2169": "X",
9697
+ // Ⅹ (Roman numeral ten)
9698
+ "\u216C": "L",
9699
+ // Ⅼ (Roman numeral fifty)
9700
+ "\u216D": "C",
9701
+ // Ⅽ (Roman numeral one hundred)
9702
+ "\u216E": "D",
9703
+ // Ⅾ (Roman numeral five hundred)
9704
+ "\u216F": "M",
9705
+ // Ⅿ (Roman numeral one thousand)
9706
+ "\u2170": "i",
9707
+ // ⅰ (small Roman numeral one)
9708
+ "\u2174": "v",
9709
+ // ⅴ (small Roman numeral five)
9710
+ "\u2179": "x",
9711
+ // ⅹ (small Roman numeral ten)
9712
+ "\u217C": "l",
9713
+ // ⅼ (small Roman numeral fifty)
9714
+ "\u217D": "c",
9715
+ // ⅽ (small Roman numeral one hundred)
9716
+ "\u217E": "d",
9717
+ // ⅾ (small Roman numeral five hundred)
9718
+ "\u217F": "m",
9719
+ // ⅿ (small Roman numeral one thousand)
9720
+ "\u0251": "a",
9721
+ // ɑ (Latin alpha — looks like 'a')
9722
+ "\u0261": "g"
9723
+ // ɡ (Latin small letter script G)
8167
9724
  };
8168
9725
  let result = value;
8169
9726
  if (/[^\x00-\x7F]/.test(value)) {
@@ -8253,6 +9810,7 @@ var ApprovalGate = class {
8253
9810
  auditLog;
8254
9811
  injectionDetector;
8255
9812
  onInjectionAlert;
9813
+ proxyTierResolver;
8256
9814
  constructor(policy, baseline, channel, auditLog, injectionDetector, onInjectionAlert) {
8257
9815
  this.policy = policy;
8258
9816
  this.baseline = baseline;
@@ -8261,6 +9819,12 @@ var ApprovalGate = class {
8261
9819
  this.injectionDetector = injectionDetector ?? new InjectionDetector();
8262
9820
  this.onInjectionAlert = onInjectionAlert;
8263
9821
  }
9822
+ /**
9823
+ * Set the proxy tier resolver. Called after the proxy router is initialized.
9824
+ */
9825
+ setProxyTierResolver(resolver) {
9826
+ this.proxyTierResolver = resolver;
9827
+ }
8264
9828
  /**
8265
9829
  * Evaluate a tool call against the Principal Policy.
8266
9830
  *
@@ -8313,6 +9877,38 @@ var ApprovalGate = class {
8313
9877
  );
8314
9878
  }
8315
9879
  }
9880
+ if (toolName.startsWith("proxy/") && this.proxyTierResolver) {
9881
+ const proxyTier = this.proxyTierResolver(toolName);
9882
+ if (proxyTier !== null) {
9883
+ if (proxyTier === 1) {
9884
+ return this.requestApproval(operation, 1, `Proxy tool "${toolName}" is configured as Tier 1 (always requires approval)`, {
9885
+ operation: toolName,
9886
+ proxy: true,
9887
+ args_summary: this.summarizeArgs(args)
9888
+ });
9889
+ }
9890
+ if (proxyTier === 2) {
9891
+ const anomaly2 = this.detectAnomaly(operation, args);
9892
+ if (anomaly2) {
9893
+ return this.requestApproval(operation, 2, `Proxy: ${anomaly2.reason}`, {
9894
+ ...anomaly2.context,
9895
+ proxy: true
9896
+ });
9897
+ }
9898
+ }
9899
+ this.auditLog.append("l2", `gate_allow_proxy:${toolName}`, "system", {
9900
+ tier: proxyTier,
9901
+ operation: toolName,
9902
+ proxy: true
9903
+ });
9904
+ return {
9905
+ allowed: true,
9906
+ tier: proxyTier,
9907
+ reason: `Proxy operation allowed (Tier ${proxyTier})`,
9908
+ approval_required: false
9909
+ };
9910
+ }
9911
+ }
8316
9912
  if (this.policy.tier1_always_approve.includes(operation)) {
8317
9913
  return this.requestApproval(operation, 1, `"${operation}" is a Tier 1 operation (always requires approval)`, {
8318
9914
  operation,
@@ -9990,7 +11586,7 @@ function createBridgeCommitment(outcome, identity, identityEncryptionKey, includ
9990
11586
  const now = (/* @__PURE__ */ new Date()).toISOString();
9991
11587
  const canonicalBytes = canonicalize(outcome);
9992
11588
  const canonicalString = new TextDecoder().decode(canonicalBytes);
9993
- const sha2564 = createCommitment(canonicalString);
11589
+ const sha2565 = createCommitment(canonicalString);
9994
11590
  let pedersenData;
9995
11591
  if (includePedersen && Number.isInteger(outcome.rounds) && outcome.rounds >= 0) {
9996
11592
  const pedersen = createPedersenCommitment(outcome.rounds);
@@ -10002,7 +11598,7 @@ function createBridgeCommitment(outcome, identity, identityEncryptionKey, includ
10002
11598
  const commitmentPayload = {
10003
11599
  bridge_commitment_id: commitmentId,
10004
11600
  session_id: outcome.session_id,
10005
- sha256_commitment: sha2564.commitment,
11601
+ sha256_commitment: sha2565.commitment,
10006
11602
  terms_hash: outcome.terms_hash,
10007
11603
  committer_did: identity.did,
10008
11604
  committed_at: now,
@@ -10013,8 +11609,8 @@ function createBridgeCommitment(outcome, identity, identityEncryptionKey, includ
10013
11609
  return {
10014
11610
  bridge_commitment_id: commitmentId,
10015
11611
  session_id: outcome.session_id,
10016
- sha256_commitment: sha2564.commitment,
10017
- blinding_factor: sha2564.blinding_factor,
11612
+ sha256_commitment: sha2565.commitment,
11613
+ blinding_factor: sha2565.blinding_factor,
10018
11614
  committer_did: identity.did,
10019
11615
  signature: toBase64url(signature),
10020
11616
  pedersen_commitment: pedersenData,
@@ -13090,7 +14686,8 @@ function createDefaultProfile() {
13090
14686
  audit_logging: { enabled: true },
13091
14687
  injection_detection: { enabled: true },
13092
14688
  context_gating: { enabled: false },
13093
- approval_gate: { enabled: false },
14689
+ approval_gate: { enabled: true },
14690
+ // SEC-057: always enabled — core enforcement
13094
14691
  zk_proofs: { enabled: false }
13095
14692
  },
13096
14693
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
@@ -13141,6 +14738,9 @@ var SovereigntyProfileStore = class {
13141
14738
  if (!this.profile) {
13142
14739
  await this.load();
13143
14740
  }
14741
+ if (updates.approval_gate && updates.approval_gate.enabled === false) {
14742
+ throw new Error("approval_gate cannot be disabled \u2014 it is a core enforcement feature");
14743
+ }
13144
14744
  const features = this.profile.features;
13145
14745
  if (updates.audit_logging !== void 0) {
13146
14746
  if (updates.audit_logging.enabled !== void 0) {
@@ -13195,6 +14795,48 @@ var SovereigntyProfileStore = class {
13195
14795
  features.zk_proofs.enabled = updates.zk_proofs.enabled;
13196
14796
  }
13197
14797
  }
14798
+ if (updates.upstream_servers !== void 0) {
14799
+ if (!Array.isArray(updates.upstream_servers)) {
14800
+ throw new Error("upstream_servers must be an array");
14801
+ }
14802
+ for (const server of updates.upstream_servers) {
14803
+ if (!server.name || typeof server.name !== "string") {
14804
+ throw new Error("Each upstream server must have a name");
14805
+ }
14806
+ if (server.name.length > 128) {
14807
+ throw new Error("Upstream server name must be 128 characters or fewer");
14808
+ }
14809
+ if (!/^[a-zA-Z0-9_-]+$/.test(server.name)) {
14810
+ throw new Error("Upstream server name must contain only alphanumeric characters, hyphens, and underscores");
14811
+ }
14812
+ if (!server.transport || typeof server.transport !== "object") {
14813
+ throw new Error("Each upstream server must have a transport configuration");
14814
+ }
14815
+ if (server.transport.type !== "stdio" && server.transport.type !== "sse") {
14816
+ throw new Error("Transport type must be 'stdio' or 'sse'");
14817
+ }
14818
+ if (server.transport.type === "stdio" && !server.transport.command) {
14819
+ throw new Error("stdio transport requires a command");
14820
+ }
14821
+ if (server.transport.type === "sse" && !server.transport.url) {
14822
+ throw new Error("sse transport requires a url");
14823
+ }
14824
+ if (typeof server.enabled !== "boolean") {
14825
+ throw new Error("Each upstream server must have enabled as a boolean");
14826
+ }
14827
+ if (![1, 2, 3].includes(server.default_tier)) {
14828
+ throw new Error("default_tier must be 1, 2, or 3");
14829
+ }
14830
+ if (server.tool_overrides) {
14831
+ for (const [, override] of Object.entries(server.tool_overrides)) {
14832
+ if (![1, 2, 3].includes(override.tier)) {
14833
+ throw new Error("tool_overrides tier must be 1, 2, or 3");
14834
+ }
14835
+ }
14836
+ }
14837
+ }
14838
+ this.profile.upstream_servers = updates.upstream_servers;
14839
+ }
13198
14840
  this.profile.updated_at = (/* @__PURE__ */ new Date()).toISOString();
13199
14841
  await this.persist();
13200
14842
  return this.profile;
@@ -13340,17 +14982,851 @@ function createSovereigntyProfileTools(profileStore, auditLog) {
13340
14982
  ];
13341
14983
  return { tools };
13342
14984
  }
13343
-
13344
- // src/index.ts
13345
- init_random();
13346
- init_encoding();
13347
-
13348
- // src/l2-operational/model-provenance.ts
13349
- var InMemoryModelProvenanceStore = class {
13350
- models = /* @__PURE__ */ new Map();
13351
- primaryModelId = null;
13352
- declare(provenance) {
13353
- if (!provenance.model_id) {
14985
+ var MAX_RETRIES = 5;
14986
+ var BASE_BACKOFF_MS = 1e3;
14987
+ var MAX_BACKOFF_MS = 3e4;
14988
+ var MAX_UPSTREAM_SERVERS = 20;
14989
+ var ClientManager = class {
14990
+ connections = /* @__PURE__ */ new Map();
14991
+ onStateChange;
14992
+ shutdownRequested = false;
14993
+ constructor(options) {
14994
+ this.onStateChange = options?.onStateChange;
14995
+ }
14996
+ /**
14997
+ * Configure upstream servers. Disconnects removed servers, connects new ones.
14998
+ * Non-blocking — connection failures are handled asynchronously.
14999
+ */
15000
+ async configure(servers) {
15001
+ if (servers.length > MAX_UPSTREAM_SERVERS) {
15002
+ throw new Error(`Maximum ${MAX_UPSTREAM_SERVERS} upstream servers allowed`);
15003
+ }
15004
+ const SAFE_SERVER_NAME = /^[a-zA-Z0-9_\-]+$/;
15005
+ const newNames = new Set(servers.filter((s) => {
15006
+ if (!SAFE_SERVER_NAME.test(s.name)) {
15007
+ return false;
15008
+ }
15009
+ return s.enabled;
15010
+ }).map((s) => s.name));
15011
+ for (const [name] of this.connections) {
15012
+ if (!newNames.has(name)) {
15013
+ await this.disconnectServer(name);
15014
+ }
15015
+ }
15016
+ for (const server of servers) {
15017
+ if (!SAFE_SERVER_NAME.test(server.name)) {
15018
+ continue;
15019
+ }
15020
+ if (!server.enabled) {
15021
+ if (this.connections.has(server.name)) {
15022
+ await this.disconnectServer(server.name);
15023
+ }
15024
+ continue;
15025
+ }
15026
+ const existing = this.connections.get(server.name);
15027
+ if (existing && existing.state === "connected") {
15028
+ existing.server = server;
15029
+ continue;
15030
+ }
15031
+ this.connectServer(server);
15032
+ }
15033
+ }
15034
+ /**
15035
+ * Get all discovered tools across all connected upstream servers.
15036
+ */
15037
+ getAllTools() {
15038
+ const result = /* @__PURE__ */ new Map();
15039
+ for (const [name, conn] of this.connections) {
15040
+ if (conn.state === "connected" && conn.tools.length > 0) {
15041
+ result.set(name, conn.tools);
15042
+ }
15043
+ }
15044
+ return result;
15045
+ }
15046
+ /**
15047
+ * Get connection status for all configured servers.
15048
+ */
15049
+ getStatus() {
15050
+ return Array.from(this.connections.values()).map((conn) => ({
15051
+ name: conn.server.name,
15052
+ state: conn.state,
15053
+ transport_type: conn.server.transport.type,
15054
+ tool_count: conn.tools.length,
15055
+ error: conn.error
15056
+ }));
15057
+ }
15058
+ /**
15059
+ * Get the upstream server config by name.
15060
+ */
15061
+ getServerConfig(name) {
15062
+ return this.connections.get(name)?.server;
15063
+ }
15064
+ /**
15065
+ * Call a tool on an upstream server.
15066
+ */
15067
+ async callTool(serverName, toolName, args) {
15068
+ const conn = this.connections.get(serverName);
15069
+ if (!conn) {
15070
+ throw new Error(`Upstream server "${serverName}" is not configured`);
15071
+ }
15072
+ if (conn.state !== "connected" || !conn.client) {
15073
+ throw new Error(`Upstream server "${serverName}" is not connected (state: ${conn.state})`);
15074
+ }
15075
+ const result = await conn.client.callTool({
15076
+ name: toolName,
15077
+ arguments: args
15078
+ });
15079
+ return result;
15080
+ }
15081
+ /**
15082
+ * Shut down all connections cleanly.
15083
+ */
15084
+ async shutdown() {
15085
+ this.shutdownRequested = true;
15086
+ for (const conn of this.connections.values()) {
15087
+ if (conn.retryTimer) {
15088
+ clearTimeout(conn.retryTimer);
15089
+ conn.retryTimer = void 0;
15090
+ }
15091
+ }
15092
+ const disconnects = Array.from(this.connections.keys()).map(
15093
+ (name) => this.disconnectServer(name)
15094
+ );
15095
+ await Promise.allSettled(disconnects);
15096
+ }
15097
+ // ── Private ───────────────────────────────────────────────────────────
15098
+ /**
15099
+ * Connect to an upstream server (non-blocking).
15100
+ * Spawns connection attempt in background — does not throw.
15101
+ */
15102
+ connectServer(server) {
15103
+ const conn = {
15104
+ server,
15105
+ client: null,
15106
+ transport: null,
15107
+ state: "connecting",
15108
+ tools: [],
15109
+ retryCount: 0
15110
+ };
15111
+ this.connections.set(server.name, conn);
15112
+ this.notifyStateChange(conn);
15113
+ this.doConnect(conn).catch(() => {
15114
+ });
15115
+ }
15116
+ /**
15117
+ * Perform the actual connection to an upstream server.
15118
+ */
15119
+ async doConnect(conn) {
15120
+ try {
15121
+ conn.state = "connecting";
15122
+ this.notifyStateChange(conn);
15123
+ let transport;
15124
+ if (conn.server.transport.type === "stdio") {
15125
+ if (!conn.server.transport.command) {
15126
+ throw new Error("stdio transport requires a command");
15127
+ }
15128
+ if (conn.server.transport.args) {
15129
+ const SAFE_ARG_PATTERN = /^[a-zA-Z0-9._\-\/=:@]+$/;
15130
+ for (const arg of conn.server.transport.args) {
15131
+ if (!SAFE_ARG_PATTERN.test(arg)) {
15132
+ throw new Error(`Unsafe argument rejected: contains disallowed characters`);
15133
+ }
15134
+ }
15135
+ }
15136
+ const ENV_BLOCKLIST = /* @__PURE__ */ new Set([
15137
+ "PATH",
15138
+ "HOME",
15139
+ "USER",
15140
+ "SHELL",
15141
+ "NODE_OPTIONS",
15142
+ "NODE_PATH",
15143
+ "LD_PRELOAD",
15144
+ "LD_LIBRARY_PATH",
15145
+ "DYLD_INSERT_LIBRARIES",
15146
+ "PYTHONPATH",
15147
+ "RUBYLIB",
15148
+ "PERL5LIB",
15149
+ "HTTP_PROXY",
15150
+ "HTTPS_PROXY",
15151
+ "NO_PROXY",
15152
+ "http_proxy",
15153
+ "https_proxy",
15154
+ "no_proxy"
15155
+ ]);
15156
+ let transportEnv;
15157
+ if (conn.server.transport.env) {
15158
+ const safeEnv = { ...process.env };
15159
+ for (const [key, value] of Object.entries(conn.server.transport.env)) {
15160
+ if (!ENV_BLOCKLIST.has(key)) {
15161
+ safeEnv[key] = value;
15162
+ }
15163
+ }
15164
+ transportEnv = safeEnv;
15165
+ }
15166
+ transport = new stdio_js.StdioClientTransport({
15167
+ command: conn.server.transport.command,
15168
+ args: conn.server.transport.args,
15169
+ env: transportEnv
15170
+ });
15171
+ } else {
15172
+ if (!conn.server.transport.url) {
15173
+ throw new Error("sse transport requires a url");
15174
+ }
15175
+ const ssrfUrl = new URL(conn.server.transport.url);
15176
+ if (ssrfUrl.protocol !== "http:" && ssrfUrl.protocol !== "https:") {
15177
+ throw new Error("SSE transport URL must use http or https scheme");
15178
+ }
15179
+ transport = new sse_js.SSEClientTransport(ssrfUrl);
15180
+ }
15181
+ const client = new index_js.Client(
15182
+ { name: `sanctuary-proxy/${conn.server.name}`, version: "1.0.0" },
15183
+ { capabilities: {} }
15184
+ );
15185
+ await client.connect(transport);
15186
+ conn.client = client;
15187
+ conn.transport = transport;
15188
+ conn.state = "connected";
15189
+ conn.error = void 0;
15190
+ conn.retryCount = 0;
15191
+ await this.discoverTools(conn);
15192
+ this.notifyStateChange(conn);
15193
+ } catch (err) {
15194
+ const message = err instanceof Error ? err.message : "Unknown connection error";
15195
+ conn.state = "error";
15196
+ conn.error = message;
15197
+ conn.client = null;
15198
+ conn.transport = null;
15199
+ this.notifyStateChange(conn);
15200
+ this.scheduleRetry(conn);
15201
+ }
15202
+ }
15203
+ /**
15204
+ * Discover tools from a connected upstream server.
15205
+ */
15206
+ async discoverTools(conn) {
15207
+ if (!conn.client || conn.state !== "connected") return;
15208
+ try {
15209
+ const result = await conn.client.listTools();
15210
+ conn.tools = (result.tools ?? []).map((t) => ({
15211
+ name: t.name,
15212
+ description: t.description ?? "",
15213
+ inputSchema: t.inputSchema ?? { type: "object", properties: {} }
15214
+ }));
15215
+ } catch {
15216
+ conn.tools = [];
15217
+ }
15218
+ }
15219
+ /**
15220
+ * Schedule a reconnection attempt with exponential backoff.
15221
+ */
15222
+ scheduleRetry(conn) {
15223
+ if (this.shutdownRequested) return;
15224
+ if (conn.retryCount >= MAX_RETRIES) {
15225
+ conn.error = `Max retries (${MAX_RETRIES}) exceeded. Last error: ${conn.error}`;
15226
+ this.notifyStateChange(conn);
15227
+ return;
15228
+ }
15229
+ const delay = Math.min(
15230
+ BASE_BACKOFF_MS * Math.pow(2, conn.retryCount),
15231
+ MAX_BACKOFF_MS
15232
+ );
15233
+ conn.retryCount++;
15234
+ conn.retryTimer = setTimeout(() => {
15235
+ if (this.shutdownRequested) return;
15236
+ conn.retryTimer = void 0;
15237
+ this.doConnect(conn).catch(() => {
15238
+ });
15239
+ }, delay);
15240
+ }
15241
+ /**
15242
+ * Disconnect a specific upstream server.
15243
+ */
15244
+ async disconnectServer(name) {
15245
+ const conn = this.connections.get(name);
15246
+ if (!conn) return;
15247
+ if (conn.retryTimer) {
15248
+ clearTimeout(conn.retryTimer);
15249
+ conn.retryTimer = void 0;
15250
+ }
15251
+ if (conn.client) {
15252
+ try {
15253
+ await conn.client.close();
15254
+ } catch {
15255
+ }
15256
+ }
15257
+ if (conn.transport) {
15258
+ try {
15259
+ await conn.transport.close();
15260
+ } catch {
15261
+ }
15262
+ }
15263
+ this.connections.delete(name);
15264
+ }
15265
+ /**
15266
+ * Notify listener of state change.
15267
+ */
15268
+ notifyStateChange(conn) {
15269
+ if (this.onStateChange) {
15270
+ try {
15271
+ this.onStateChange(conn.server.name, conn.state, conn.tools.length, conn.error);
15272
+ } catch {
15273
+ }
15274
+ }
15275
+ }
15276
+ };
15277
+
15278
+ // src/proxy/proxy-router.ts
15279
+ var UPSTREAM_CALL_TIMEOUT_MS = 3e4;
15280
+ var ProxyRouter = class {
15281
+ clientManager;
15282
+ injectionDetector;
15283
+ auditLog;
15284
+ options;
15285
+ constructor(clientManager, injectionDetector, auditLog, options) {
15286
+ this.clientManager = clientManager;
15287
+ this.injectionDetector = injectionDetector;
15288
+ this.auditLog = auditLog;
15289
+ this.options = options ?? {};
15290
+ }
15291
+ /**
15292
+ * Convert all discovered upstream tools to Sanctuary ToolDefinitions.
15293
+ * Each tool is registered as `proxy/{server_name}/{tool_name}`.
15294
+ */
15295
+ getProxiedTools() {
15296
+ const tools = [];
15297
+ const allUpstreamTools = this.clientManager.getAllTools();
15298
+ for (const [serverName, serverTools] of allUpstreamTools) {
15299
+ for (const upstreamTool of serverTools) {
15300
+ const proxyName = `proxy/${serverName}/${upstreamTool.name}`;
15301
+ tools.push({
15302
+ name: proxyName,
15303
+ description: `[via ${serverName}] ${upstreamTool.description}`,
15304
+ inputSchema: upstreamTool.inputSchema,
15305
+ handler: this.createHandler(serverName, upstreamTool.name)
15306
+ });
15307
+ }
15308
+ }
15309
+ return tools;
15310
+ }
15311
+ /**
15312
+ * Determine the tier for a proxied tool call.
15313
+ * Checks tool_overrides first, then falls back to default_tier.
15314
+ */
15315
+ getTierForTool(serverName, toolName) {
15316
+ const serverConfig = this.clientManager.getServerConfig(serverName);
15317
+ if (!serverConfig) return 2;
15318
+ if (serverConfig.tool_overrides?.[toolName]) {
15319
+ return serverConfig.tool_overrides[toolName].tier;
15320
+ }
15321
+ return serverConfig.default_tier;
15322
+ }
15323
+ /**
15324
+ * Parse a proxy tool name into server name and tool name.
15325
+ * Returns null if the name doesn't match the proxy namespace.
15326
+ */
15327
+ static parseProxyToolName(fullName) {
15328
+ if (!fullName.startsWith("proxy/")) return null;
15329
+ const rest = fullName.slice("proxy/".length);
15330
+ const slashIdx = rest.indexOf("/");
15331
+ if (slashIdx === -1) return null;
15332
+ return {
15333
+ serverName: rest.slice(0, slashIdx),
15334
+ toolName: rest.slice(slashIdx + 1)
15335
+ };
15336
+ }
15337
+ // ── Private ───────────────────────────────────────────────────────────
15338
+ /**
15339
+ * Create a handler for a specific proxied tool.
15340
+ * The handler runs the full enforcement chain before forwarding.
15341
+ */
15342
+ createHandler(serverName, toolName) {
15343
+ return async (args) => {
15344
+ const proxyName = `proxy/${serverName}/${toolName}`;
15345
+ const start = Date.now();
15346
+ const tier = this.getTierForTool(serverName, toolName);
15347
+ try {
15348
+ const injectionResult = this.injectionDetector.scan(proxyName, args);
15349
+ if (injectionResult.flagged && injectionResult.recommendation === "block") {
15350
+ this.auditLog.append("l2", `proxy_injection_blocked:${proxyName}`, "system", {
15351
+ server: serverName,
15352
+ tool: toolName,
15353
+ tier,
15354
+ confidence: injectionResult.confidence,
15355
+ latency_ms: Date.now() - start
15356
+ }, "failure");
15357
+ return toolResult({
15358
+ error: "Operation not permitted",
15359
+ proxy: true
15360
+ });
15361
+ }
15362
+ if (injectionResult.flagged && injectionResult.recommendation === "escalate") {
15363
+ this.auditLog.append("l2", `proxy_injection_escalated:${proxyName}`, "system", {
15364
+ server: serverName,
15365
+ tool: toolName,
15366
+ tier,
15367
+ confidence: injectionResult.confidence
15368
+ });
15369
+ }
15370
+ let filteredArgs = args;
15371
+ if (this.options.contextGateFilter) {
15372
+ try {
15373
+ filteredArgs = await this.options.contextGateFilter(proxyName, args);
15374
+ } catch {
15375
+ }
15376
+ }
15377
+ if (this.options.governor) {
15378
+ const govResult = this.options.governor.check(serverName, toolName, filteredArgs);
15379
+ if (!govResult.allowed) {
15380
+ this.auditLog.append("l2", `proxy_governor_blocked:${proxyName}`, "system", {
15381
+ server: serverName,
15382
+ tool: toolName,
15383
+ tier,
15384
+ reason: govResult.reason,
15385
+ latency_ms: Date.now() - start
15386
+ }, "failure");
15387
+ return toolResult({
15388
+ error: "Operation not permitted",
15389
+ proxy: true,
15390
+ governor_reason: govResult.reason
15391
+ });
15392
+ }
15393
+ if (govResult.reason === "duplicate_cached" && govResult.cached_result !== void 0) {
15394
+ this.auditLog.append("l2", `proxy_governor_cached:${proxyName}`, "system", {
15395
+ server: serverName,
15396
+ tool: toolName,
15397
+ tier,
15398
+ cached: true,
15399
+ latency_ms: Date.now() - start
15400
+ });
15401
+ return toolResult(govResult.cached_result ?? {});
15402
+ }
15403
+ }
15404
+ const result = await this.callWithTimeout(
15405
+ serverName,
15406
+ toolName,
15407
+ filteredArgs,
15408
+ UPSTREAM_CALL_TIMEOUT_MS
15409
+ );
15410
+ const latencyMs = Date.now() - start;
15411
+ if (this.options.governor) {
15412
+ this.options.governor.recordResult(serverName, toolName, filteredArgs, result);
15413
+ }
15414
+ this.auditLog.append("l2", `proxy_call:${proxyName}`, "system", {
15415
+ server: serverName,
15416
+ tool: toolName,
15417
+ tier,
15418
+ decision: "allowed",
15419
+ latency_ms: latencyMs
15420
+ });
15421
+ return this.normalizeResponse(result);
15422
+ } catch (err) {
15423
+ const latencyMs = Date.now() - start;
15424
+ const rawErrorMessage = err instanceof Error ? err.message : "Unknown upstream error";
15425
+ const sanitizeError = (msg) => {
15426
+ let safe = msg.substring(0, 200);
15427
+ safe = safe.replace(/\/[^\s]+/g, "[path-redacted]");
15428
+ safe = safe.replace(/(?:mongodb|postgres|mysql|redis):\/\/[^\s]+/g, "[connection-redacted]");
15429
+ return safe;
15430
+ };
15431
+ const errorMessage = sanitizeError(rawErrorMessage);
15432
+ this.auditLog.append("l2", `proxy_call:${proxyName}`, "system", {
15433
+ server: serverName,
15434
+ tool: toolName,
15435
+ tier,
15436
+ decision: "error",
15437
+ error: errorMessage,
15438
+ latency_ms: latencyMs
15439
+ }, "failure");
15440
+ return {
15441
+ content: [{
15442
+ type: "text",
15443
+ text: JSON.stringify({
15444
+ error: errorMessage,
15445
+ proxy: true,
15446
+ server: serverName,
15447
+ tool: toolName
15448
+ })
15449
+ }]
15450
+ };
15451
+ }
15452
+ };
15453
+ }
15454
+ /**
15455
+ * Call an upstream tool with a timeout.
15456
+ */
15457
+ async callWithTimeout(serverName, toolName, args, timeoutMs) {
15458
+ return new Promise((resolve, reject) => {
15459
+ const timer = setTimeout(() => {
15460
+ reject(new Error(`Upstream tool call timed out after ${timeoutMs}ms`));
15461
+ }, timeoutMs);
15462
+ this.clientManager.callTool(serverName, toolName, args).then((result) => {
15463
+ clearTimeout(timer);
15464
+ resolve(result);
15465
+ }).catch((err) => {
15466
+ clearTimeout(timer);
15467
+ reject(err);
15468
+ });
15469
+ });
15470
+ }
15471
+ /**
15472
+ * Normalize an upstream response to the standard Sanctuary response format.
15473
+ */
15474
+ normalizeResponse(result) {
15475
+ const MAX_RESPONSE_SIZE = 1e6;
15476
+ const MAX_TEXT_BLOCK_SIZE = 1e5;
15477
+ const responseStr = JSON.stringify(result);
15478
+ if (responseStr.length > MAX_RESPONSE_SIZE) {
15479
+ return toolResult({
15480
+ error: "upstream_response_too_large",
15481
+ max_bytes: MAX_RESPONSE_SIZE
15482
+ });
15483
+ }
15484
+ if (!result.content || !Array.isArray(result.content)) {
15485
+ return toolResult({ upstream_response: result });
15486
+ }
15487
+ const textContent = result.content.filter((c) => c.type === "text" && typeof c.text === "string").map((c) => {
15488
+ const text = c.text;
15489
+ if (text.length > MAX_TEXT_BLOCK_SIZE) {
15490
+ return {
15491
+ type: "text",
15492
+ text: text.substring(0, MAX_TEXT_BLOCK_SIZE) + "\n[response truncated]"
15493
+ };
15494
+ }
15495
+ return { type: "text", text };
15496
+ });
15497
+ if (textContent.length > 0) {
15498
+ return { content: textContent };
15499
+ }
15500
+ return toolResult({ upstream_response: result.content });
15501
+ }
15502
+ };
15503
+ function strToBytes(s) {
15504
+ return new TextEncoder().encode(s);
15505
+ }
15506
+ function bytesToHex(bytes) {
15507
+ let hex = "";
15508
+ for (let i = 0; i < bytes.length; i++) {
15509
+ hex += bytes[i].toString(16).padStart(2, "0");
15510
+ }
15511
+ return hex;
15512
+ }
15513
+ function sha256Hex(input) {
15514
+ return bytesToHex(sha256.sha256(strToBytes(input)));
15515
+ }
15516
+ var DEFAULT_CONFIG = {
15517
+ volume_limit: 200,
15518
+ volume_window_ms: 6e5,
15519
+ // 10 minutes
15520
+ rate_limit: 20,
15521
+ rate_window_ms: 6e4,
15522
+ // 1 minute
15523
+ duplicate_ttl_ms: 6e4,
15524
+ // 1 minute
15525
+ lifetime_limit: 1e3
15526
+ };
15527
+ var CallGovernor = class {
15528
+ config;
15529
+ /** Sliding window of all call timestamps for volume tracking */
15530
+ volumeWindow = [];
15531
+ /** Per-tool sliding window of call timestamps for rate tracking */
15532
+ rateWindows = /* @__PURE__ */ new Map();
15533
+ /** Duplicate cache: SHA-256(server+tool+args) -> cached result + expiry */
15534
+ duplicateCache = /* @__PURE__ */ new Map();
15535
+ /** Total calls this session */
15536
+ lifetimeCount = 0;
15537
+ /** Hard stop flag — set when lifetime limit is reached */
15538
+ hardStopped = false;
15539
+ constructor(config) {
15540
+ this.config = { ...DEFAULT_CONFIG, ...config };
15541
+ }
15542
+ /**
15543
+ * Check if a call is allowed and apply governance.
15544
+ *
15545
+ * Evaluation order:
15546
+ * 1. Lifetime limit (hard stop — not recoverable without reset)
15547
+ * 2. Volume limit (sliding window)
15548
+ * 3. Rate limit per tool (escalates to Tier 2)
15549
+ * 4. Duplicate detection (returns cached result)
15550
+ */
15551
+ check(serverName, toolName, args) {
15552
+ const now = Date.now();
15553
+ const effectiveConfig = this.getEffectiveConfig(serverName);
15554
+ if (this.hardStopped) {
15555
+ return {
15556
+ allowed: false,
15557
+ reason: "lifetime_exceeded"
15558
+ };
15559
+ }
15560
+ if (this.lifetimeCount >= effectiveConfig.lifetime_limit) {
15561
+ this.hardStopped = true;
15562
+ return {
15563
+ allowed: false,
15564
+ reason: "lifetime_exceeded"
15565
+ };
15566
+ }
15567
+ this.pruneVolumeWindow(now, effectiveConfig.volume_window_ms);
15568
+ if (this.volumeWindow.length >= effectiveConfig.volume_limit) {
15569
+ return {
15570
+ allowed: false,
15571
+ reason: "volume_exceeded"
15572
+ };
15573
+ }
15574
+ const toolKey = `${serverName}::${toolName}`;
15575
+ this.pruneRateWindow(toolKey, now, effectiveConfig.rate_window_ms);
15576
+ const rateWindow = this.rateWindows.get(toolKey);
15577
+ const currentRate = rateWindow ? rateWindow.length : 0;
15578
+ if (currentRate >= effectiveConfig.rate_limit) {
15579
+ return {
15580
+ allowed: false,
15581
+ reason: "rate_exceeded",
15582
+ escalate: true
15583
+ };
15584
+ }
15585
+ const callHash = this.computeCallHash(serverName, toolName, args);
15586
+ this.pruneDuplicateCache(now);
15587
+ const cached = this.duplicateCache.get(callHash);
15588
+ if (cached && cached.expires_at > now) {
15589
+ return {
15590
+ allowed: true,
15591
+ reason: "duplicate_cached",
15592
+ cached_result: cached.result
15593
+ };
15594
+ }
15595
+ this.volumeWindow.push(now);
15596
+ if (!this.rateWindows.has(toolKey)) {
15597
+ this.rateWindows.set(toolKey, []);
15598
+ }
15599
+ this.rateWindows.get(toolKey).push(now);
15600
+ this.lifetimeCount++;
15601
+ return { allowed: true };
15602
+ }
15603
+ /**
15604
+ * Record a successful call result for duplicate caching.
15605
+ */
15606
+ recordResult(serverName, toolName, args, result) {
15607
+ const callHash = this.computeCallHash(serverName, toolName, args);
15608
+ const effectiveConfig = this.getEffectiveConfig(serverName);
15609
+ this.duplicateCache.set(callHash, {
15610
+ result,
15611
+ expires_at: Date.now() + effectiveConfig.duplicate_ttl_ms
15612
+ });
15613
+ }
15614
+ /**
15615
+ * Get current governor status for dashboard display.
15616
+ */
15617
+ getStatus() {
15618
+ const now = Date.now();
15619
+ this.pruneVolumeWindow(now, this.config.volume_window_ms);
15620
+ this.pruneDuplicateCache(now);
15621
+ const rateByTool = {};
15622
+ for (const [toolKey, timestamps] of this.rateWindows.entries()) {
15623
+ const cutoff = now - this.config.rate_window_ms;
15624
+ const activeCount = timestamps.filter((t) => t >= cutoff).length;
15625
+ if (activeCount > 0) {
15626
+ rateByTool[toolKey] = activeCount;
15627
+ }
15628
+ }
15629
+ return {
15630
+ volume_current: this.volumeWindow.length,
15631
+ volume_limit: this.config.volume_limit,
15632
+ lifetime_current: this.lifetimeCount,
15633
+ lifetime_limit: this.config.lifetime_limit,
15634
+ rate_by_tool: rateByTool,
15635
+ duplicate_cache_size: this.duplicateCache.size,
15636
+ hard_stopped: this.hardStopped
15637
+ };
15638
+ }
15639
+ /**
15640
+ * Reset all counters (Tier 1 — requires approval).
15641
+ * Clears volume window, rate windows, duplicate cache,
15642
+ * lifetime counter, and hard stop flag.
15643
+ */
15644
+ reset() {
15645
+ this.volumeWindow = [];
15646
+ this.rateWindows.clear();
15647
+ this.duplicateCache.clear();
15648
+ this.lifetimeCount = 0;
15649
+ this.hardStopped = false;
15650
+ }
15651
+ /**
15652
+ * Get the effective config for a given server, merging per-server overrides.
15653
+ */
15654
+ getEffectiveConfig(serverName) {
15655
+ const override = this.config.per_server_overrides?.[serverName];
15656
+ if (!override) return this.config;
15657
+ return { ...this.config, ...override };
15658
+ }
15659
+ /**
15660
+ * Compute a SHA-256 hash of server + tool + canonical args.
15661
+ * Used for duplicate detection.
15662
+ */
15663
+ computeCallHash(serverName, toolName, args) {
15664
+ const stableArgs = this.stableStringify(args);
15665
+ const input = `${serverName}\0${toolName}\0${stableArgs}`;
15666
+ return sha256Hex(input);
15667
+ }
15668
+ /**
15669
+ * Deterministic JSON serialization with sorted keys.
15670
+ */
15671
+ stableStringify(obj) {
15672
+ if (obj === null || obj === void 0) return "null";
15673
+ if (typeof obj !== "object") return JSON.stringify(obj);
15674
+ if (Array.isArray(obj)) {
15675
+ return "[" + obj.map((item) => this.stableStringify(item)).join(",") + "]";
15676
+ }
15677
+ const sortedKeys = Object.keys(obj).sort();
15678
+ const pairs = sortedKeys.map(
15679
+ (key) => JSON.stringify(key) + ":" + this.stableStringify(obj[key])
15680
+ );
15681
+ return "{" + pairs.join(",") + "}";
15682
+ }
15683
+ /**
15684
+ * Prune volume window entries older than the window size.
15685
+ * Uses shift() from the front — timestamps are appended in order.
15686
+ */
15687
+ pruneVolumeWindow(now, windowMs) {
15688
+ const cutoff = now - windowMs;
15689
+ while (this.volumeWindow.length > 0 && this.volumeWindow[0] < cutoff) {
15690
+ this.volumeWindow.shift();
15691
+ }
15692
+ }
15693
+ /**
15694
+ * Prune a per-tool rate window.
15695
+ */
15696
+ pruneRateWindow(toolKey, now, windowMs) {
15697
+ const window = this.rateWindows.get(toolKey);
15698
+ if (!window) return;
15699
+ const cutoff = now - windowMs;
15700
+ while (window.length > 0 && window[0] < cutoff) {
15701
+ window.shift();
15702
+ }
15703
+ if (window.length === 0) {
15704
+ this.rateWindows.delete(toolKey);
15705
+ }
15706
+ }
15707
+ /**
15708
+ * Prune expired entries from the duplicate cache.
15709
+ * Amortized O(1) — only prunes when cache exceeds a size threshold.
15710
+ */
15711
+ pruneDuplicateCache(now) {
15712
+ if (this.duplicateCache.size < 100) return;
15713
+ for (const [key, entry] of this.duplicateCache) {
15714
+ if (entry.expires_at <= now) {
15715
+ this.duplicateCache.delete(key);
15716
+ }
15717
+ }
15718
+ }
15719
+ };
15720
+
15721
+ // src/l2-operational/governor-tools.ts
15722
+ function createGovernorTools(governor, auditLog) {
15723
+ const tools = [
15724
+ // ── Governor Status ─────────────────────────────────────────────
15725
+ {
15726
+ name: "sanctuary/governor_status",
15727
+ description: "View the current Call Governor status including volume counters, per-tool rate counts, duplicate cache size, and lifetime counter. Use this to monitor tool call consumption, detect potential loops, and check how close you are to governance limits. The governor protects against runaway tool calls by enforcing volume limits, rate limits, duplicate detection, and a session lifetime cap.",
15728
+ inputSchema: {
15729
+ type: "object",
15730
+ properties: {}
15731
+ },
15732
+ handler: async () => {
15733
+ const status = governor.getStatus();
15734
+ auditLog.append("l2", "governor_status", "system", {
15735
+ volume_current: status.volume_current,
15736
+ volume_limit: status.volume_limit,
15737
+ lifetime_current: status.lifetime_current,
15738
+ lifetime_limit: status.lifetime_limit,
15739
+ duplicate_cache_size: status.duplicate_cache_size,
15740
+ hard_stopped: status.hard_stopped
15741
+ });
15742
+ const volumePercent = status.volume_limit > 0 ? Math.round(status.volume_current / status.volume_limit * 100) : 0;
15743
+ const lifetimePercent = status.lifetime_limit > 0 ? Math.round(status.lifetime_current / status.lifetime_limit * 100) : 0;
15744
+ const toolRateEntries = Object.entries(status.rate_by_tool);
15745
+ const highRateTools = toolRateEntries.filter(([, count]) => count > 10).map(([tool, count]) => `${tool} (${count} calls/min)`);
15746
+ return toolResult({
15747
+ governor_status: status,
15748
+ summary: {
15749
+ volume_usage: `${status.volume_current}/${status.volume_limit} (${volumePercent}%)`,
15750
+ lifetime_usage: `${status.lifetime_current}/${status.lifetime_limit} (${lifetimePercent}%)`,
15751
+ active_tools: toolRateEntries.length,
15752
+ cached_duplicates: status.duplicate_cache_size
15753
+ },
15754
+ warnings: [
15755
+ ...status.hard_stopped ? ["HARD STOP: Lifetime limit reached. Use sanctuary/governor_reset to continue."] : [],
15756
+ ...volumePercent > 80 ? [`Volume usage at ${volumePercent}% \u2014 approaching limit.`] : [],
15757
+ ...lifetimePercent > 80 ? [`Lifetime usage at ${lifetimePercent}% \u2014 approaching hard stop.`] : [],
15758
+ ...highRateTools.length > 0 ? [`High-rate tools detected: ${highRateTools.join(", ")}`] : []
15759
+ ],
15760
+ guidance: status.hard_stopped ? "The governor has stopped all proxied tool calls because the session lifetime limit was reached. Use sanctuary/governor_reset (requires human approval) to reset counters and resume." : "The governor is monitoring all proxied tool calls. Counters reset automatically on server restart."
15761
+ });
15762
+ }
15763
+ },
15764
+ // ── Governor Reset ──────────────────────────────────────────────
15765
+ {
15766
+ name: "sanctuary/governor_reset",
15767
+ description: "Reset all Call Governor counters: volume window, per-tool rate windows, duplicate cache, and lifetime counter. This clears the hard stop if the lifetime limit was reached. This is a Tier 1 operation \u2014 requires human approval because it removes all runtime governance state and could allow previously blocked behavior to resume.",
15768
+ inputSchema: {
15769
+ type: "object",
15770
+ properties: {
15771
+ confirm: {
15772
+ type: "boolean",
15773
+ description: "Must be true to confirm the reset. This clears all governor state including the lifetime counter."
15774
+ }
15775
+ },
15776
+ required: ["confirm"]
15777
+ },
15778
+ handler: async (args) => {
15779
+ const confirm = args.confirm;
15780
+ if (!confirm) {
15781
+ return toolResult({
15782
+ error: "confirmation_required",
15783
+ message: "Set confirm: true to reset all governor counters. This will clear volume limits, rate tracking, duplicate cache, and the lifetime counter."
15784
+ });
15785
+ }
15786
+ const preResetStatus = governor.getStatus();
15787
+ governor.reset();
15788
+ const postResetStatus = governor.getStatus();
15789
+ auditLog.append("l2", "governor_reset", "system", {
15790
+ pre_reset: {
15791
+ volume_current: preResetStatus.volume_current,
15792
+ lifetime_current: preResetStatus.lifetime_current,
15793
+ duplicate_cache_size: preResetStatus.duplicate_cache_size,
15794
+ hard_stopped: preResetStatus.hard_stopped
15795
+ },
15796
+ post_reset: {
15797
+ volume_current: postResetStatus.volume_current,
15798
+ lifetime_current: postResetStatus.lifetime_current,
15799
+ duplicate_cache_size: postResetStatus.duplicate_cache_size,
15800
+ hard_stopped: postResetStatus.hard_stopped
15801
+ }
15802
+ });
15803
+ return toolResult({
15804
+ reset: true,
15805
+ previous_state: {
15806
+ lifetime_calls: preResetStatus.lifetime_current,
15807
+ was_hard_stopped: preResetStatus.hard_stopped,
15808
+ volume_at_reset: preResetStatus.volume_current,
15809
+ cached_duplicates_cleared: preResetStatus.duplicate_cache_size
15810
+ },
15811
+ current_state: postResetStatus,
15812
+ message: "All governor counters have been reset. " + (preResetStatus.hard_stopped ? "Hard stop has been cleared \u2014 proxied tool calls will resume." : "Volume, rate, duplicate, and lifetime counters are now at zero.")
15813
+ });
15814
+ }
15815
+ }
15816
+ ];
15817
+ return { tools };
15818
+ }
15819
+
15820
+ // src/index.ts
15821
+ init_random();
15822
+ init_encoding();
15823
+
15824
+ // src/l2-operational/model-provenance.ts
15825
+ var InMemoryModelProvenanceStore = class {
15826
+ models = /* @__PURE__ */ new Map();
15827
+ primaryModelId = null;
15828
+ declare(provenance) {
15829
+ if (!provenance.model_id) {
13354
15830
  throw new Error("ModelProvenance requires a model_id");
13355
15831
  }
13356
15832
  if (!provenance.model_name) {
@@ -13961,18 +16437,89 @@ async function createSanctuaryServer(options) {
13961
16437
  ...dashboardTools,
13962
16438
  manifestTool
13963
16439
  ];
16440
+ let clientManager;
16441
+ let proxyRouter;
16442
+ const governor = new CallGovernor();
16443
+ const { tools: governorTools } = createGovernorTools(governor, auditLog);
16444
+ allTools.push(...governorTools);
16445
+ const profile = profileStore.get();
16446
+ if (profile.upstream_servers && profile.upstream_servers.length > 0) {
16447
+ const enabledServers = profile.upstream_servers.filter((s) => s.enabled);
16448
+ if (enabledServers.length > 0) {
16449
+ clientManager = new ClientManager({
16450
+ onStateChange: (serverName, state, toolCount, error) => {
16451
+ if (dashboard) {
16452
+ dashboard.broadcastSSE("proxy-server-status", {
16453
+ server: serverName,
16454
+ state,
16455
+ tool_count: toolCount,
16456
+ error,
16457
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
16458
+ });
16459
+ }
16460
+ auditLog.append("l2", `proxy_server_${state}`, "system", {
16461
+ server: serverName,
16462
+ tool_count: toolCount,
16463
+ error
16464
+ });
16465
+ }
16466
+ });
16467
+ proxyRouter = new ProxyRouter(
16468
+ clientManager,
16469
+ injectionDetector,
16470
+ auditLog,
16471
+ {
16472
+ contextGateFilter: async (_toolName, args) => {
16473
+ const activeProfile = profileStore.get();
16474
+ if (activeProfile.features.context_gating.enabled) {
16475
+ return args;
16476
+ }
16477
+ return args;
16478
+ },
16479
+ governor
16480
+ }
16481
+ );
16482
+ clientManager.configure(enabledServers).catch((err) => {
16483
+ console.error(`[Sanctuary] Failed to configure upstream servers: ${err instanceof Error ? err.message : "unknown error"}`);
16484
+ });
16485
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
16486
+ const proxiedTools = proxyRouter.getProxiedTools();
16487
+ if (proxiedTools.length > 0) {
16488
+ allTools.push(...proxiedTools);
16489
+ }
16490
+ if (dashboard) {
16491
+ dashboard.setDependencies({
16492
+ policy,
16493
+ baseline,
16494
+ auditLog,
16495
+ clientManager
16496
+ });
16497
+ }
16498
+ }
16499
+ }
13964
16500
  allTools = allTools.map((tool) => ({
13965
16501
  ...tool,
13966
16502
  handler: contextGateEnforcer.wrapHandler(tool.name, tool.handler)
13967
16503
  }));
16504
+ if (proxyRouter) {
16505
+ gate.setProxyTierResolver((toolName) => {
16506
+ const parsed = ProxyRouter.parseProxyToolName(toolName);
16507
+ if (!parsed) return null;
16508
+ return proxyRouter.getTierForTool(parsed.serverName, parsed.toolName);
16509
+ });
16510
+ }
13968
16511
  const server = createServer(allTools, { gate });
13969
16512
  await saveConfig(config);
13970
- const saveBaseline = () => {
16513
+ const cleanup = () => {
13971
16514
  baseline.save().catch(() => {
13972
16515
  });
16516
+ if (clientManager) {
16517
+ clientManager.shutdown().catch(() => {
16518
+ });
16519
+ }
13973
16520
  };
13974
- process.on("SIGINT", saveBaseline);
13975
- process.on("SIGTERM", saveBaseline);
16521
+ process.on("SIGINT", cleanup);
16522
+ process.on("SIGTERM", cleanup);
13976
16523
  if (recoveryKey) {
13977
16524
  console.error(
13978
16525
  `\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
@@ -13995,6 +16542,7 @@ exports.AutoApproveChannel = AutoApproveChannel;
13995
16542
  exports.BaselineTracker = BaselineTracker;
13996
16543
  exports.CONTEXT_GATE_TEMPLATES = TEMPLATES;
13997
16544
  exports.CallbackApprovalChannel = CallbackApprovalChannel;
16545
+ exports.ClientManager = ClientManager;
13998
16546
  exports.CommitmentStore = CommitmentStore;
13999
16547
  exports.ContextGateEnforcer = ContextGateEnforcer;
14000
16548
  exports.ContextGatePolicyStore = ContextGatePolicyStore;
@@ -14006,6 +16554,7 @@ exports.InjectionDetector = InjectionDetector;
14006
16554
  exports.MODEL_PRESETS = MODEL_PRESETS;
14007
16555
  exports.MemoryStorage = MemoryStorage;
14008
16556
  exports.PolicyStore = PolicyStore;
16557
+ exports.ProxyRouter = ProxyRouter;
14009
16558
  exports.ReputationStore = ReputationStore;
14010
16559
  exports.SovereigntyProfileStore = SovereigntyProfileStore;
14011
16560
  exports.StateStore = StateStore;