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