@sanctuary-framework/mcp-server 0.5.16 → 0.7.0
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 +2526 -340
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2527 -341
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +2070 -341
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -7
- package/dist/index.d.ts +51 -7
- package/dist/index.js +2070 -341
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -426,6 +426,12 @@ function defaultConfig() {
|
|
|
426
426
|
secret: "",
|
|
427
427
|
callback_port: 3502,
|
|
428
428
|
callback_host: "127.0.0.1"
|
|
429
|
+
},
|
|
430
|
+
verascore: {
|
|
431
|
+
url: "https://verascore.ai",
|
|
432
|
+
auto_publish_to_verascore: true,
|
|
433
|
+
// DELTA-04: default OFF for privacy. Enable explicitly per deployment.
|
|
434
|
+
auto_publish_handshakes: false
|
|
429
435
|
}
|
|
430
436
|
};
|
|
431
437
|
}
|
|
@@ -496,6 +502,21 @@ async function loadConfig(configPath) {
|
|
|
496
502
|
if (process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST) {
|
|
497
503
|
config.webhook.callback_host = process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST;
|
|
498
504
|
}
|
|
505
|
+
if (process.env.SANCTUARY_VERASCORE_URL) {
|
|
506
|
+
config.verascore.url = process.env.SANCTUARY_VERASCORE_URL;
|
|
507
|
+
}
|
|
508
|
+
if (process.env.SANCTUARY_AUTO_PUBLISH_TO_VERASCORE === "true") {
|
|
509
|
+
config.verascore.auto_publish_to_verascore = true;
|
|
510
|
+
}
|
|
511
|
+
if (process.env.SANCTUARY_AUTO_PUBLISH_TO_VERASCORE === "false") {
|
|
512
|
+
config.verascore.auto_publish_to_verascore = false;
|
|
513
|
+
}
|
|
514
|
+
if (process.env.SANCTUARY_AUTO_PUBLISH_HANDSHAKES === "true") {
|
|
515
|
+
config.verascore.auto_publish_handshakes = true;
|
|
516
|
+
}
|
|
517
|
+
if (process.env.SANCTUARY_AUTO_PUBLISH_HANDSHAKES === "false") {
|
|
518
|
+
config.verascore.auto_publish_handshakes = false;
|
|
519
|
+
}
|
|
499
520
|
config.version = PKG_VERSION;
|
|
500
521
|
validateConfig(config);
|
|
501
522
|
return config;
|
|
@@ -1387,7 +1408,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
|
|
|
1387
1408
|
const tools = [
|
|
1388
1409
|
// ── Identity Tools ──────────────────────────────────────────────────
|
|
1389
1410
|
{
|
|
1390
|
-
name: "
|
|
1411
|
+
name: "identity_create",
|
|
1391
1412
|
description: "Create a new sovereign identity (Ed25519 keypair). The private key is encrypted and never exposed.",
|
|
1392
1413
|
inputSchema: {
|
|
1393
1414
|
type: "object",
|
|
@@ -1422,7 +1443,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
|
|
|
1422
1443
|
}
|
|
1423
1444
|
},
|
|
1424
1445
|
{
|
|
1425
|
-
name: "
|
|
1446
|
+
name: "identity_list",
|
|
1426
1447
|
description: "List all managed sovereign identities.",
|
|
1427
1448
|
inputSchema: {
|
|
1428
1449
|
type: "object",
|
|
@@ -1447,7 +1468,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
|
|
|
1447
1468
|
}
|
|
1448
1469
|
},
|
|
1449
1470
|
{
|
|
1450
|
-
name: "
|
|
1471
|
+
name: "identity_sign",
|
|
1451
1472
|
description: "Sign data with a managed identity. The private key is decrypted in memory only during signing.",
|
|
1452
1473
|
inputSchema: {
|
|
1453
1474
|
type: "object",
|
|
@@ -1485,7 +1506,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
|
|
|
1485
1506
|
}
|
|
1486
1507
|
},
|
|
1487
1508
|
{
|
|
1488
|
-
name: "
|
|
1509
|
+
name: "identity_verify",
|
|
1489
1510
|
description: "Verify an Ed25519 signature. Provide either identity_id or public_key.",
|
|
1490
1511
|
inputSchema: {
|
|
1491
1512
|
type: "object",
|
|
@@ -1534,7 +1555,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
|
|
|
1534
1555
|
}
|
|
1535
1556
|
},
|
|
1536
1557
|
{
|
|
1537
|
-
name: "
|
|
1558
|
+
name: "identity_rotate",
|
|
1538
1559
|
description: "Rotate keys for an identity. Generates a new keypair and signs a rotation event with the old key for verifiable chain.",
|
|
1539
1560
|
inputSchema: {
|
|
1540
1561
|
type: "object",
|
|
@@ -1567,7 +1588,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
|
|
|
1567
1588
|
},
|
|
1568
1589
|
// ── State Tools ─────────────────────────────────────────────────────
|
|
1569
1590
|
{
|
|
1570
|
-
name: "
|
|
1591
|
+
name: "state_write",
|
|
1571
1592
|
description: "Write encrypted state to the sovereign store. Value is encrypted with a namespace-specific key. The write is signed by the active identity.",
|
|
1572
1593
|
inputSchema: {
|
|
1573
1594
|
type: "object",
|
|
@@ -1624,7 +1645,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
|
|
|
1624
1645
|
}
|
|
1625
1646
|
},
|
|
1626
1647
|
{
|
|
1627
|
-
name: "
|
|
1648
|
+
name: "state_read",
|
|
1628
1649
|
description: "Read and decrypt state from the sovereign store. Verifies integrity via Merkle proof and signature.",
|
|
1629
1650
|
inputSchema: {
|
|
1630
1651
|
type: "object",
|
|
@@ -1665,7 +1686,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
|
|
|
1665
1686
|
}
|
|
1666
1687
|
},
|
|
1667
1688
|
{
|
|
1668
|
-
name: "
|
|
1689
|
+
name: "state_list",
|
|
1669
1690
|
description: "List keys in a namespace (metadata only \u2014 no decryption).",
|
|
1670
1691
|
inputSchema: {
|
|
1671
1692
|
type: "object",
|
|
@@ -1697,7 +1718,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
|
|
|
1697
1718
|
}
|
|
1698
1719
|
},
|
|
1699
1720
|
{
|
|
1700
|
-
name: "
|
|
1721
|
+
name: "state_delete",
|
|
1701
1722
|
description: "Securely delete state. Overwrites file with random bytes before removal (right to deletion, S1.6).",
|
|
1702
1723
|
inputSchema: {
|
|
1703
1724
|
type: "object",
|
|
@@ -1729,7 +1750,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
|
|
|
1729
1750
|
}
|
|
1730
1751
|
},
|
|
1731
1752
|
{
|
|
1732
|
-
name: "
|
|
1753
|
+
name: "state_export",
|
|
1733
1754
|
description: "Export state as an encrypted, portable bundle for migration.",
|
|
1734
1755
|
inputSchema: {
|
|
1735
1756
|
type: "object",
|
|
@@ -1749,7 +1770,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
|
|
|
1749
1770
|
}
|
|
1750
1771
|
},
|
|
1751
1772
|
{
|
|
1752
|
-
name: "
|
|
1773
|
+
name: "state_import",
|
|
1753
1774
|
description: "Import a previously exported state bundle.",
|
|
1754
1775
|
inputSchema: {
|
|
1755
1776
|
type: "object",
|
|
@@ -2370,7 +2391,7 @@ function createL3Tools(storage, masterKey, auditLog) {
|
|
|
2370
2391
|
const tools = [
|
|
2371
2392
|
// ─── Commitment Schemes ───────────────────────────────────────────────
|
|
2372
2393
|
{
|
|
2373
|
-
name: "
|
|
2394
|
+
name: "proof_commitment",
|
|
2374
2395
|
description: "Create a cryptographic commitment to a value. The commitment hides the value until you choose to reveal it. Returns the commitment hash and a blinding factor (store securely).",
|
|
2375
2396
|
inputSchema: {
|
|
2376
2397
|
type: "object",
|
|
@@ -2405,7 +2426,7 @@ function createL3Tools(storage, masterKey, auditLog) {
|
|
|
2405
2426
|
}
|
|
2406
2427
|
},
|
|
2407
2428
|
{
|
|
2408
|
-
name: "
|
|
2429
|
+
name: "proof_reveal",
|
|
2409
2430
|
description: "Verify a previously committed value by revealing it with the blinding factor. Returns whether the revealed value matches the commitment.",
|
|
2410
2431
|
inputSchema: {
|
|
2411
2432
|
type: "object",
|
|
@@ -2443,7 +2464,7 @@ function createL3Tools(storage, masterKey, auditLog) {
|
|
|
2443
2464
|
},
|
|
2444
2465
|
// ─── Disclosure Policies ──────────────────────────────────────────────
|
|
2445
2466
|
{
|
|
2446
|
-
name: "
|
|
2467
|
+
name: "disclosure_set_policy",
|
|
2447
2468
|
description: "Define a disclosure policy that controls what an agent will and will not disclose in different interaction contexts. Rules specify which fields may be disclosed, which must be withheld, and which require cryptographic proof.",
|
|
2448
2469
|
inputSchema: {
|
|
2449
2470
|
type: "object",
|
|
@@ -2518,7 +2539,7 @@ function createL3Tools(storage, masterKey, auditLog) {
|
|
|
2518
2539
|
}
|
|
2519
2540
|
},
|
|
2520
2541
|
{
|
|
2521
|
-
name: "
|
|
2542
|
+
name: "disclosure_evaluate",
|
|
2522
2543
|
description: "Evaluate a disclosure request against an active policy. Returns per-field decisions: disclose, withhold, proof, or ask-principal.",
|
|
2523
2544
|
inputSchema: {
|
|
2524
2545
|
type: "object",
|
|
@@ -2594,7 +2615,7 @@ function createL3Tools(storage, masterKey, auditLog) {
|
|
|
2594
2615
|
},
|
|
2595
2616
|
// ─── ZK Proof Tools ───────────────────────────────────────────────────
|
|
2596
2617
|
{
|
|
2597
|
-
name: "
|
|
2618
|
+
name: "zk_commit",
|
|
2598
2619
|
description: "Create a Pedersen commitment to a numeric value on Ristretto255. Unlike SHA-256 commitments, Pedersen commitments support zero-knowledge proofs: you can prove properties about the committed value without revealing it.",
|
|
2599
2620
|
inputSchema: {
|
|
2600
2621
|
type: "object",
|
|
@@ -2625,7 +2646,7 @@ function createL3Tools(storage, masterKey, auditLog) {
|
|
|
2625
2646
|
}
|
|
2626
2647
|
},
|
|
2627
2648
|
{
|
|
2628
|
-
name: "
|
|
2649
|
+
name: "zk_prove",
|
|
2629
2650
|
description: "Create a zero-knowledge proof of knowledge for a Pedersen commitment. Proves you know the value and blinding factor without revealing either. Uses a Schnorr sigma protocol with Fiat-Shamir transform.",
|
|
2630
2651
|
inputSchema: {
|
|
2631
2652
|
type: "object",
|
|
@@ -2666,7 +2687,7 @@ function createL3Tools(storage, masterKey, auditLog) {
|
|
|
2666
2687
|
}
|
|
2667
2688
|
},
|
|
2668
2689
|
{
|
|
2669
|
-
name: "
|
|
2690
|
+
name: "zk_verify",
|
|
2670
2691
|
description: "Verify a zero-knowledge proof of knowledge for a Pedersen commitment. Checks that the prover knows the commitment's opening without learning anything.",
|
|
2671
2692
|
inputSchema: {
|
|
2672
2693
|
type: "object",
|
|
@@ -2694,7 +2715,7 @@ function createL3Tools(storage, masterKey, auditLog) {
|
|
|
2694
2715
|
}
|
|
2695
2716
|
},
|
|
2696
2717
|
{
|
|
2697
|
-
name: "
|
|
2718
|
+
name: "zk_range_prove",
|
|
2698
2719
|
description: "Create a zero-knowledge range proof: prove that a committed value is within [min, max] without revealing the exact value. Uses bit-decomposition with OR-proofs on Ristretto255.",
|
|
2699
2720
|
inputSchema: {
|
|
2700
2721
|
type: "object",
|
|
@@ -2744,7 +2765,7 @@ function createL3Tools(storage, masterKey, auditLog) {
|
|
|
2744
2765
|
}
|
|
2745
2766
|
},
|
|
2746
2767
|
{
|
|
2747
|
-
name: "
|
|
2768
|
+
name: "zk_range_verify",
|
|
2748
2769
|
description: "Verify a zero-knowledge range proof \u2014 confirms a committed value is within the claimed range without learning the value.",
|
|
2749
2770
|
inputSchema: {
|
|
2750
2771
|
type: "object",
|
|
@@ -3185,14 +3206,14 @@ function tierDistribution(tiers) {
|
|
|
3185
3206
|
}
|
|
3186
3207
|
|
|
3187
3208
|
// src/l4-reputation/tools.ts
|
|
3188
|
-
function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeResults) {
|
|
3209
|
+
function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeResults, verascoreUrl) {
|
|
3189
3210
|
const reputationStore = new ReputationStore(storage, masterKey);
|
|
3190
3211
|
const identityEncryptionKey = derivePurposeKey(masterKey, "identity-encryption");
|
|
3191
3212
|
const hsResults = handshakeResults ?? /* @__PURE__ */ new Map();
|
|
3192
3213
|
const tools = [
|
|
3193
3214
|
// ─── Reputation Recording ─────────────────────────────────────────
|
|
3194
3215
|
{
|
|
3195
|
-
name: "
|
|
3216
|
+
name: "reputation_record",
|
|
3196
3217
|
description: "Record an interaction outcome as a signed attestation. Creates an EAS-compatible attestation signed by the specified identity.",
|
|
3197
3218
|
inputSchema: {
|
|
3198
3219
|
type: "object",
|
|
@@ -3285,7 +3306,7 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
|
|
|
3285
3306
|
},
|
|
3286
3307
|
// ─── Reputation Query ─────────────────────────────────────────────
|
|
3287
3308
|
{
|
|
3288
|
-
name: "
|
|
3309
|
+
name: "reputation_query",
|
|
3289
3310
|
description: "Query aggregated reputation data with filtering. Returns summary statistics, never raw interaction details.",
|
|
3290
3311
|
inputSchema: {
|
|
3291
3312
|
type: "object",
|
|
@@ -3333,7 +3354,7 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
|
|
|
3333
3354
|
},
|
|
3334
3355
|
// ─── Reputation Export ─────────────────────────────────────────────
|
|
3335
3356
|
{
|
|
3336
|
-
name: "
|
|
3357
|
+
name: "reputation_export",
|
|
3337
3358
|
description: "Export a portable reputation bundle (SANCTUARY_REP_V1). Includes all signed attestations for independent verification.",
|
|
3338
3359
|
inputSchema: {
|
|
3339
3360
|
type: "object",
|
|
@@ -3392,7 +3413,7 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
|
|
|
3392
3413
|
},
|
|
3393
3414
|
// ─── Reputation Import ────────────────────────────────────────────
|
|
3394
3415
|
{
|
|
3395
|
-
name: "
|
|
3416
|
+
name: "reputation_import",
|
|
3396
3417
|
description: "Import a reputation bundle from another Sanctuary instance. Verifies all attestation signatures by default.",
|
|
3397
3418
|
inputSchema: {
|
|
3398
3419
|
type: "object",
|
|
@@ -3444,7 +3465,7 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
|
|
|
3444
3465
|
},
|
|
3445
3466
|
// ─── Sovereignty-Weighted Query ──────────────────────────────────
|
|
3446
3467
|
{
|
|
3447
|
-
name: "
|
|
3468
|
+
name: "reputation_query_weighted",
|
|
3448
3469
|
description: "Query reputation with sovereignty-weighted scoring. Attestations from verified-sovereign agents carry full weight (1.0); unverified attestations carry reduced weight (0.2). Returns both the weighted score and tier distribution.",
|
|
3449
3470
|
inputSchema: {
|
|
3450
3471
|
type: "object",
|
|
@@ -3500,7 +3521,7 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
|
|
|
3500
3521
|
},
|
|
3501
3522
|
// ─── Trust Bootstrap: Escrow ──────────────────────────────────────
|
|
3502
3523
|
{
|
|
3503
|
-
name: "
|
|
3524
|
+
name: "bootstrap_create_escrow",
|
|
3504
3525
|
description: "Create an escrow record for trust bootstrapping. Allows new participants with no reputation to transact safely.",
|
|
3505
3526
|
inputSchema: {
|
|
3506
3527
|
type: "object",
|
|
@@ -3559,7 +3580,7 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
|
|
|
3559
3580
|
},
|
|
3560
3581
|
// ─── Trust Bootstrap: Guarantee ───────────────────────────────────
|
|
3561
3582
|
{
|
|
3562
|
-
name: "
|
|
3583
|
+
name: "bootstrap_provide_guarantee",
|
|
3563
3584
|
description: "A principal provides a signed reputation guarantee for a new agent. The guarantee certificate can be presented to counterparties.",
|
|
3564
3585
|
inputSchema: {
|
|
3565
3586
|
type: "object",
|
|
@@ -3637,7 +3658,7 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
|
|
|
3637
3658
|
},
|
|
3638
3659
|
// ─── Verascore Reputation Publish ────────────────────────────────
|
|
3639
3660
|
{
|
|
3640
|
-
name: "
|
|
3661
|
+
name: "reputation_publish",
|
|
3641
3662
|
description: "Publish sovereignty data to Verascore (verascore.ai) \u2014 the agent reputation platform. Sends SHR data, handshake attestations, or sovereignty updates. The data is signed with the agent's Ed25519 key for verification. Requires a Verascore agent profile (claimed or stub) to exist.",
|
|
3642
3663
|
inputSchema: {
|
|
3643
3664
|
type: "object",
|
|
@@ -3675,8 +3696,18 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
|
|
|
3675
3696
|
});
|
|
3676
3697
|
}
|
|
3677
3698
|
const publishType = args.type;
|
|
3678
|
-
const
|
|
3679
|
-
const
|
|
3699
|
+
const configuredVerascoreUrl = verascoreUrl || "https://verascore.ai";
|
|
3700
|
+
const veracoreUrl = args.verascore_url || configuredVerascoreUrl;
|
|
3701
|
+
const ALLOWED_VERASCORE_HOSTS = /* @__PURE__ */ new Set([
|
|
3702
|
+
"verascore.ai",
|
|
3703
|
+
"www.verascore.ai",
|
|
3704
|
+
"api.verascore.ai"
|
|
3705
|
+
]);
|
|
3706
|
+
try {
|
|
3707
|
+
const configuredHost = new URL(configuredVerascoreUrl).hostname;
|
|
3708
|
+
ALLOWED_VERASCORE_HOSTS.add(configuredHost);
|
|
3709
|
+
} catch {
|
|
3710
|
+
}
|
|
3680
3711
|
try {
|
|
3681
3712
|
const parsed = new URL(veracoreUrl);
|
|
3682
3713
|
if (parsed.protocol !== "https:") {
|
|
@@ -3684,9 +3715,9 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
|
|
|
3684
3715
|
error: `verascore_url must use HTTPS. Got: ${parsed.protocol}`
|
|
3685
3716
|
});
|
|
3686
3717
|
}
|
|
3687
|
-
if (!ALLOWED_VERASCORE_HOSTS.
|
|
3718
|
+
if (!ALLOWED_VERASCORE_HOSTS.has(parsed.hostname)) {
|
|
3688
3719
|
return toolResult({
|
|
3689
|
-
error: `verascore_url must point to a known Verascore domain (${ALLOWED_VERASCORE_HOSTS.join(", ")}). Got: ${parsed.hostname}`
|
|
3720
|
+
error: `verascore_url must point to a known Verascore domain (${[...ALLOWED_VERASCORE_HOSTS].join(", ")}). Got: ${parsed.hostname}`
|
|
3690
3721
|
});
|
|
3691
3722
|
}
|
|
3692
3723
|
} catch {
|
|
@@ -3817,12 +3848,14 @@ var DEFAULT_POLICY = {
|
|
|
3817
3848
|
"reputation_export",
|
|
3818
3849
|
"bootstrap_provide_guarantee",
|
|
3819
3850
|
"decommission_certificate",
|
|
3820
|
-
"reputation_publish",
|
|
3821
|
-
// SEC-039: Explicit Tier 1 — sends data to external API
|
|
3822
3851
|
"sovereignty_profile_update",
|
|
3823
3852
|
// Changes enforcement behavior — always requires approval
|
|
3824
|
-
"governor_reset"
|
|
3853
|
+
"governor_reset",
|
|
3825
3854
|
// Clears all runtime governance state — always requires approval
|
|
3855
|
+
"sanctuary_bootstrap",
|
|
3856
|
+
// Creates new Ed25519 identity + publishes — always requires approval
|
|
3857
|
+
"sanctuary_export_identity_bundle"
|
|
3858
|
+
// Exports portable identity — always requires approval
|
|
3826
3859
|
],
|
|
3827
3860
|
tier2_anomaly: DEFAULT_TIER2,
|
|
3828
3861
|
tier3_always_allow: [
|
|
@@ -3880,7 +3913,11 @@ var DEFAULT_POLICY = {
|
|
|
3880
3913
|
"sovereignty_profile_get",
|
|
3881
3914
|
"sovereignty_profile_generate_prompt",
|
|
3882
3915
|
// Agent needs its own config to generate system prompt
|
|
3883
|
-
"governor_status"
|
|
3916
|
+
"governor_status",
|
|
3917
|
+
"reputation_publish",
|
|
3918
|
+
// Auto-allow: publishing sovereignty data to Verascore is routine
|
|
3919
|
+
"sanctuary_policy_status"
|
|
3920
|
+
// Read-only policy summary
|
|
3884
3921
|
],
|
|
3885
3922
|
approval_channel: DEFAULT_CHANNEL
|
|
3886
3923
|
};
|
|
@@ -3991,9 +4028,10 @@ tier1_always_approve:
|
|
|
3991
4028
|
- reputation_import
|
|
3992
4029
|
- reputation_export
|
|
3993
4030
|
- bootstrap_provide_guarantee
|
|
3994
|
-
- reputation_publish
|
|
3995
4031
|
- sovereignty_profile_update
|
|
3996
4032
|
- governor_reset
|
|
4033
|
+
- sanctuary_bootstrap
|
|
4034
|
+
- sanctuary_export_identity_bundle
|
|
3997
4035
|
|
|
3998
4036
|
# \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
|
|
3999
4037
|
# Triggers approval when agent behavior deviates from its baseline.
|
|
@@ -4059,6 +4097,8 @@ tier3_always_allow:
|
|
|
4059
4097
|
- dashboard_open
|
|
4060
4098
|
- sovereignty_profile_get
|
|
4061
4099
|
- governor_status
|
|
4100
|
+
- reputation_publish
|
|
4101
|
+
- sanctuary_policy_status
|
|
4062
4102
|
|
|
4063
4103
|
# \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
|
|
4064
4104
|
# How Sanctuary reaches you when approval is needed.
|
|
@@ -7324,241 +7364,1039 @@ function generateDashboardHTML(options) {
|
|
|
7324
7364
|
</html>`;
|
|
7325
7365
|
}
|
|
7326
7366
|
|
|
7327
|
-
// src/
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
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."
|
|
7354
|
-
},
|
|
7355
|
-
approval_gate: {
|
|
7356
|
-
name: "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."
|
|
7360
|
-
},
|
|
7361
|
-
zk_proofs: {
|
|
7362
|
-
name: "Zero-Knowledge Proofs",
|
|
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."
|
|
7372
|
-
}
|
|
7373
|
-
};
|
|
7374
|
-
function generateSystemPrompt(profile) {
|
|
7375
|
-
const activeFeatures = [];
|
|
7376
|
-
const inactiveFeatures = [];
|
|
7377
|
-
const activeKeys = [];
|
|
7378
|
-
const featureKeys = [
|
|
7379
|
-
"audit_logging",
|
|
7380
|
-
"injection_detection",
|
|
7381
|
-
"context_gating",
|
|
7382
|
-
"approval_gate",
|
|
7383
|
-
"zk_proofs"
|
|
7384
|
-
];
|
|
7385
|
-
for (const key of featureKeys) {
|
|
7386
|
-
const featureConfig = profile.features[key];
|
|
7387
|
-
const info = FEATURE_INFO[key];
|
|
7388
|
-
if (featureConfig.enabled) {
|
|
7389
|
-
activeKeys.push(key);
|
|
7390
|
-
let desc = `- ${info.name}: ${info.activeDescription}`;
|
|
7391
|
-
if (key === "injection_detection" && "sensitivity" in featureConfig && featureConfig.sensitivity) {
|
|
7392
|
-
desc += ` Sensitivity: ${featureConfig.sensitivity}.`;
|
|
7393
|
-
}
|
|
7394
|
-
if (key === "context_gating" && "policy_id" in featureConfig && featureConfig.policy_id) {
|
|
7395
|
-
desc += ` Active policy: ${featureConfig.policy_id}.`;
|
|
7396
|
-
}
|
|
7397
|
-
activeFeatures.push(desc);
|
|
7398
|
-
} else {
|
|
7399
|
-
inactiveFeatures.push(info.disabledDescription);
|
|
7367
|
+
// src/cocoon/fortress-view.ts
|
|
7368
|
+
function generateFortressViewHTML(options) {
|
|
7369
|
+
return `<!DOCTYPE html>
|
|
7370
|
+
<html lang="en">
|
|
7371
|
+
<head>
|
|
7372
|
+
<meta charset="UTF-8">
|
|
7373
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7374
|
+
<title>Sanctuary \u2014 Fortress View</title>
|
|
7375
|
+
<style>
|
|
7376
|
+
:root {
|
|
7377
|
+
--bg: #0d1117;
|
|
7378
|
+
--surface: #161b22;
|
|
7379
|
+
--surface-raised: #1c2128;
|
|
7380
|
+
--border: #30363d;
|
|
7381
|
+
--text-primary: #e6edf3;
|
|
7382
|
+
--text-secondary: #8b949e;
|
|
7383
|
+
--text-muted: #484f58;
|
|
7384
|
+
--green: #3fb950;
|
|
7385
|
+
--green-dim: #238636;
|
|
7386
|
+
--amber: #d29922;
|
|
7387
|
+
--amber-dim: #9e6a03;
|
|
7388
|
+
--red: #f85149;
|
|
7389
|
+
--red-dim: #da3633;
|
|
7390
|
+
--blue: #58a6ff;
|
|
7391
|
+
--blue-dim: #1f6feb;
|
|
7400
7392
|
}
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7393
|
+
|
|
7394
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
7395
|
+
|
|
7396
|
+
body {
|
|
7397
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
7398
|
+
background-color: var(--bg);
|
|
7399
|
+
color: var(--text-primary);
|
|
7400
|
+
min-height: 100vh;
|
|
7408
7401
|
}
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
lines.push(
|
|
7419
|
-
"- No features are currently enabled. Contact your operator to configure protections."
|
|
7420
|
-
);
|
|
7421
|
-
}
|
|
7422
|
-
if (inactiveFeatures.length > 0) {
|
|
7423
|
-
lines.push("");
|
|
7424
|
-
lines.push(
|
|
7425
|
-
`Optional tools available but not currently enabled: ${inactiveFeatures.join(", ")}.`
|
|
7426
|
-
);
|
|
7427
|
-
}
|
|
7428
|
-
return lines.join("\n");
|
|
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.");
|
|
7402
|
+
|
|
7403
|
+
/* \u2500\u2500 Header \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 */
|
|
7404
|
+
.fortress-header {
|
|
7405
|
+
display: flex;
|
|
7406
|
+
align-items: center;
|
|
7407
|
+
justify-content: space-between;
|
|
7408
|
+
padding: 16px 24px;
|
|
7409
|
+
border-bottom: 1px solid var(--border);
|
|
7410
|
+
background: var(--surface);
|
|
7450
7411
|
}
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7412
|
+
|
|
7413
|
+
.fortress-brand {
|
|
7414
|
+
display: flex;
|
|
7415
|
+
align-items: center;
|
|
7416
|
+
gap: 12px;
|
|
7455
7417
|
}
|
|
7456
|
-
}
|
|
7457
|
-
return items;
|
|
7458
|
-
}
|
|
7459
7418
|
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
var
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7419
|
+
.fortress-brand .shield {
|
|
7420
|
+
font-size: 28px;
|
|
7421
|
+
color: var(--blue);
|
|
7422
|
+
}
|
|
7423
|
+
|
|
7424
|
+
.fortress-brand h1 {
|
|
7425
|
+
font-size: 18px;
|
|
7426
|
+
font-weight: 600;
|
|
7427
|
+
letter-spacing: -0.5px;
|
|
7428
|
+
}
|
|
7429
|
+
|
|
7430
|
+
.fortress-brand .version {
|
|
7431
|
+
font-size: 12px;
|
|
7432
|
+
color: var(--text-secondary);
|
|
7433
|
+
}
|
|
7434
|
+
|
|
7435
|
+
.header-actions {
|
|
7436
|
+
display: flex;
|
|
7437
|
+
gap: 8px;
|
|
7438
|
+
}
|
|
7439
|
+
|
|
7440
|
+
.header-actions button {
|
|
7441
|
+
padding: 6px 16px;
|
|
7442
|
+
border-radius: 6px;
|
|
7443
|
+
border: 1px solid var(--border);
|
|
7444
|
+
background: var(--surface);
|
|
7445
|
+
color: var(--text-primary);
|
|
7446
|
+
font-size: 13px;
|
|
7447
|
+
cursor: pointer;
|
|
7448
|
+
transition: background 0.15s;
|
|
7449
|
+
}
|
|
7450
|
+
|
|
7451
|
+
.header-actions button:hover {
|
|
7452
|
+
background: var(--surface-raised);
|
|
7453
|
+
}
|
|
7454
|
+
|
|
7455
|
+
.header-actions .pause-btn {
|
|
7456
|
+
border-color: var(--red-dim);
|
|
7457
|
+
color: var(--red);
|
|
7458
|
+
}
|
|
7459
|
+
|
|
7460
|
+
.header-actions .pause-btn:hover {
|
|
7461
|
+
background: rgba(248, 81, 73, 0.1);
|
|
7462
|
+
}
|
|
7463
|
+
|
|
7464
|
+
.header-actions .pause-btn.paused {
|
|
7465
|
+
background: var(--red-dim);
|
|
7466
|
+
color: white;
|
|
7467
|
+
}
|
|
7468
|
+
|
|
7469
|
+
/* \u2500\u2500 Tab bar \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 */
|
|
7470
|
+
.tab-bar {
|
|
7471
|
+
display: flex;
|
|
7472
|
+
border-bottom: 1px solid var(--border);
|
|
7473
|
+
background: var(--surface);
|
|
7474
|
+
padding: 0 24px;
|
|
7475
|
+
}
|
|
7476
|
+
|
|
7477
|
+
.tab-bar button {
|
|
7478
|
+
padding: 10px 16px;
|
|
7479
|
+
border: none;
|
|
7480
|
+
background: none;
|
|
7481
|
+
color: var(--text-secondary);
|
|
7482
|
+
font-size: 14px;
|
|
7483
|
+
cursor: pointer;
|
|
7484
|
+
border-bottom: 2px solid transparent;
|
|
7485
|
+
transition: all 0.15s;
|
|
7486
|
+
}
|
|
7487
|
+
|
|
7488
|
+
.tab-bar button:hover {
|
|
7489
|
+
color: var(--text-primary);
|
|
7490
|
+
}
|
|
7491
|
+
|
|
7492
|
+
.tab-bar button.active {
|
|
7493
|
+
color: var(--text-primary);
|
|
7494
|
+
border-bottom-color: var(--blue);
|
|
7495
|
+
}
|
|
7496
|
+
|
|
7497
|
+
/* \u2500\u2500 Content \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 */
|
|
7498
|
+
.fortress-content { padding: 24px; }
|
|
7499
|
+
|
|
7500
|
+
/* \u2500\u2500 Status Banner \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 */
|
|
7501
|
+
.status-banner {
|
|
7502
|
+
display: flex;
|
|
7503
|
+
align-items: center;
|
|
7504
|
+
gap: 16px;
|
|
7505
|
+
padding: 20px 24px;
|
|
7506
|
+
border-radius: 8px;
|
|
7507
|
+
border: 1px solid var(--border);
|
|
7508
|
+
background: var(--surface);
|
|
7509
|
+
margin-bottom: 24px;
|
|
7510
|
+
}
|
|
7511
|
+
|
|
7512
|
+
.status-indicator {
|
|
7513
|
+
width: 48px;
|
|
7514
|
+
height: 48px;
|
|
7515
|
+
border-radius: 50%;
|
|
7516
|
+
display: flex;
|
|
7517
|
+
align-items: center;
|
|
7518
|
+
justify-content: center;
|
|
7519
|
+
font-size: 24px;
|
|
7520
|
+
flex-shrink: 0;
|
|
7521
|
+
}
|
|
7522
|
+
|
|
7523
|
+
.status-indicator.green { background: rgba(63, 185, 80, 0.15); color: var(--green); }
|
|
7524
|
+
.status-indicator.amber { background: rgba(210, 153, 34, 0.15); color: var(--amber); }
|
|
7525
|
+
.status-indicator.red { background: rgba(248, 81, 73, 0.15); color: var(--red); }
|
|
7526
|
+
|
|
7527
|
+
.status-info h2 {
|
|
7528
|
+
font-size: 18px;
|
|
7529
|
+
font-weight: 600;
|
|
7530
|
+
margin-bottom: 4px;
|
|
7531
|
+
}
|
|
7532
|
+
|
|
7533
|
+
.status-info p {
|
|
7534
|
+
font-size: 14px;
|
|
7535
|
+
color: var(--text-secondary);
|
|
7536
|
+
}
|
|
7537
|
+
|
|
7538
|
+
.status-stats {
|
|
7539
|
+
display: flex;
|
|
7540
|
+
gap: 24px;
|
|
7541
|
+
margin-left: auto;
|
|
7542
|
+
}
|
|
7543
|
+
|
|
7544
|
+
.stat {
|
|
7545
|
+
text-align: center;
|
|
7546
|
+
}
|
|
7547
|
+
|
|
7548
|
+
.stat .value {
|
|
7549
|
+
font-size: 24px;
|
|
7550
|
+
font-weight: 600;
|
|
7551
|
+
font-variant-numeric: tabular-nums;
|
|
7552
|
+
}
|
|
7553
|
+
|
|
7554
|
+
.stat .label {
|
|
7555
|
+
font-size: 11px;
|
|
7556
|
+
color: var(--text-secondary);
|
|
7557
|
+
text-transform: uppercase;
|
|
7558
|
+
letter-spacing: 0.5px;
|
|
7559
|
+
}
|
|
7560
|
+
|
|
7561
|
+
/* \u2500\u2500 Two-column layout \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 */
|
|
7562
|
+
.fortress-grid {
|
|
7563
|
+
display: grid;
|
|
7564
|
+
grid-template-columns: 1fr 360px;
|
|
7565
|
+
gap: 24px;
|
|
7566
|
+
}
|
|
7567
|
+
|
|
7568
|
+
@media (max-width: 900px) {
|
|
7569
|
+
.fortress-grid { grid-template-columns: 1fr; }
|
|
7570
|
+
}
|
|
7571
|
+
|
|
7572
|
+
/* \u2500\u2500 Feed \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 */
|
|
7573
|
+
.feed-panel {
|
|
7574
|
+
background: var(--surface);
|
|
7575
|
+
border: 1px solid var(--border);
|
|
7576
|
+
border-radius: 8px;
|
|
7577
|
+
overflow: hidden;
|
|
7578
|
+
}
|
|
7579
|
+
|
|
7580
|
+
.panel-header {
|
|
7581
|
+
display: flex;
|
|
7582
|
+
align-items: center;
|
|
7583
|
+
justify-content: space-between;
|
|
7584
|
+
padding: 12px 16px;
|
|
7585
|
+
border-bottom: 1px solid var(--border);
|
|
7586
|
+
}
|
|
7587
|
+
|
|
7588
|
+
.panel-header h3 {
|
|
7589
|
+
font-size: 14px;
|
|
7590
|
+
font-weight: 600;
|
|
7591
|
+
}
|
|
7592
|
+
|
|
7593
|
+
.feed-list {
|
|
7594
|
+
max-height: 600px;
|
|
7595
|
+
overflow-y: auto;
|
|
7596
|
+
scroll-behavior: smooth;
|
|
7597
|
+
}
|
|
7598
|
+
|
|
7599
|
+
.feed-item {
|
|
7600
|
+
display: flex;
|
|
7601
|
+
align-items: flex-start;
|
|
7602
|
+
gap: 10px;
|
|
7603
|
+
padding: 10px 16px;
|
|
7604
|
+
border-bottom: 1px solid var(--border);
|
|
7605
|
+
font-size: 13px;
|
|
7606
|
+
transition: background 0.1s;
|
|
7607
|
+
}
|
|
7608
|
+
|
|
7609
|
+
.feed-item:hover {
|
|
7610
|
+
background: var(--surface-raised);
|
|
7611
|
+
}
|
|
7612
|
+
|
|
7613
|
+
.feed-dot {
|
|
7614
|
+
width: 8px;
|
|
7615
|
+
height: 8px;
|
|
7616
|
+
border-radius: 50%;
|
|
7617
|
+
margin-top: 5px;
|
|
7618
|
+
flex-shrink: 0;
|
|
7619
|
+
}
|
|
7620
|
+
|
|
7621
|
+
.feed-dot.green { background: var(--green); }
|
|
7622
|
+
.feed-dot.amber { background: var(--amber); }
|
|
7623
|
+
.feed-dot.red { background: var(--red); }
|
|
7624
|
+
|
|
7625
|
+
.feed-detail {
|
|
7626
|
+
flex: 1;
|
|
7627
|
+
min-width: 0;
|
|
7628
|
+
}
|
|
7629
|
+
|
|
7630
|
+
.feed-tool {
|
|
7631
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
7632
|
+
font-size: 12px;
|
|
7633
|
+
color: var(--blue);
|
|
7634
|
+
word-break: break-all;
|
|
7635
|
+
}
|
|
7636
|
+
|
|
7637
|
+
.feed-decision {
|
|
7638
|
+
font-size: 12px;
|
|
7639
|
+
color: var(--text-secondary);
|
|
7640
|
+
margin-top: 2px;
|
|
7641
|
+
}
|
|
7642
|
+
|
|
7643
|
+
.feed-time {
|
|
7644
|
+
font-size: 11px;
|
|
7645
|
+
color: var(--text-muted);
|
|
7646
|
+
flex-shrink: 0;
|
|
7647
|
+
white-space: nowrap;
|
|
7648
|
+
}
|
|
7649
|
+
|
|
7650
|
+
.feed-empty {
|
|
7651
|
+
padding: 40px 16px;
|
|
7652
|
+
text-align: center;
|
|
7653
|
+
color: var(--text-muted);
|
|
7654
|
+
font-size: 14px;
|
|
7655
|
+
}
|
|
7656
|
+
|
|
7657
|
+
/* \u2500\u2500 Alerts Panel \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 */
|
|
7658
|
+
.alerts-panel {
|
|
7659
|
+
background: var(--surface);
|
|
7660
|
+
border: 1px solid var(--border);
|
|
7661
|
+
border-radius: 8px;
|
|
7662
|
+
overflow: hidden;
|
|
7663
|
+
}
|
|
7664
|
+
|
|
7665
|
+
.alert-item {
|
|
7666
|
+
padding: 12px 16px;
|
|
7667
|
+
border-bottom: 1px solid var(--border);
|
|
7668
|
+
}
|
|
7669
|
+
|
|
7670
|
+
.alert-item .alert-title {
|
|
7671
|
+
font-size: 13px;
|
|
7672
|
+
font-weight: 500;
|
|
7673
|
+
margin-bottom: 4px;
|
|
7674
|
+
}
|
|
7675
|
+
|
|
7676
|
+
.alert-item .alert-desc {
|
|
7677
|
+
font-size: 12px;
|
|
7678
|
+
color: var(--text-secondary);
|
|
7679
|
+
margin-bottom: 8px;
|
|
7680
|
+
}
|
|
7681
|
+
|
|
7682
|
+
.alert-actions {
|
|
7683
|
+
display: flex;
|
|
7684
|
+
gap: 8px;
|
|
7685
|
+
}
|
|
7686
|
+
|
|
7687
|
+
.alert-actions button {
|
|
7688
|
+
padding: 4px 12px;
|
|
7689
|
+
border-radius: 4px;
|
|
7690
|
+
border: 1px solid var(--border);
|
|
7691
|
+
font-size: 12px;
|
|
7692
|
+
cursor: pointer;
|
|
7693
|
+
transition: all 0.15s;
|
|
7694
|
+
}
|
|
7695
|
+
|
|
7696
|
+
.approve-btn {
|
|
7697
|
+
background: var(--green-dim);
|
|
7698
|
+
color: white;
|
|
7699
|
+
border-color: var(--green-dim) !important;
|
|
7700
|
+
}
|
|
7701
|
+
|
|
7702
|
+
.approve-btn:hover { opacity: 0.9; }
|
|
7703
|
+
|
|
7704
|
+
.deny-btn {
|
|
7705
|
+
background: none;
|
|
7706
|
+
color: var(--red);
|
|
7707
|
+
border-color: var(--red-dim) !important;
|
|
7708
|
+
}
|
|
7709
|
+
|
|
7710
|
+
.deny-btn:hover {
|
|
7711
|
+
background: rgba(248, 81, 73, 0.1);
|
|
7712
|
+
}
|
|
7713
|
+
|
|
7714
|
+
.alerts-empty {
|
|
7715
|
+
padding: 40px 16px;
|
|
7716
|
+
text-align: center;
|
|
7717
|
+
color: var(--text-muted);
|
|
7718
|
+
font-size: 14px;
|
|
7719
|
+
}
|
|
7720
|
+
|
|
7721
|
+
/* \u2500\u2500 Servers panel \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 */
|
|
7722
|
+
.servers-panel {
|
|
7723
|
+
margin-top: 16px;
|
|
7724
|
+
}
|
|
7725
|
+
|
|
7726
|
+
.server-row {
|
|
7727
|
+
display: flex;
|
|
7728
|
+
align-items: center;
|
|
7729
|
+
gap: 8px;
|
|
7730
|
+
padding: 8px 16px;
|
|
7731
|
+
border-bottom: 1px solid var(--border);
|
|
7732
|
+
font-size: 13px;
|
|
7733
|
+
}
|
|
7734
|
+
|
|
7735
|
+
.server-status-dot {
|
|
7736
|
+
width: 8px;
|
|
7737
|
+
height: 8px;
|
|
7738
|
+
border-radius: 50%;
|
|
7739
|
+
}
|
|
7740
|
+
|
|
7741
|
+
.server-status-dot.connected { background: var(--green); }
|
|
7742
|
+
.server-status-dot.connecting { background: var(--amber); }
|
|
7743
|
+
.server-status-dot.disconnected, .server-status-dot.error { background: var(--red); }
|
|
7744
|
+
|
|
7745
|
+
.server-name {
|
|
7746
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
7747
|
+
font-size: 12px;
|
|
7748
|
+
}
|
|
7749
|
+
|
|
7750
|
+
.server-tier {
|
|
7751
|
+
margin-left: auto;
|
|
7752
|
+
font-size: 11px;
|
|
7753
|
+
color: var(--text-secondary);
|
|
7754
|
+
}
|
|
7755
|
+
</style>
|
|
7756
|
+
</head>
|
|
7757
|
+
<body>
|
|
7758
|
+
<!-- Header -->
|
|
7759
|
+
<div class="fortress-header">
|
|
7760
|
+
<div class="fortress-brand">
|
|
7761
|
+
<div class="shield">🛡</div>
|
|
7762
|
+
<div>
|
|
7763
|
+
<h1>Sanctuary Cocoon</h1>
|
|
7764
|
+
<div class="version">v${esc(options.serverVersion)}</div>
|
|
7765
|
+
</div>
|
|
7766
|
+
</div>
|
|
7767
|
+
<div class="header-actions">
|
|
7768
|
+
<button class="pause-btn" id="pause-btn" title="Pause agent \u2014 requires approval for all operations">Pause Agent</button>
|
|
7769
|
+
<button id="advanced-btn">Advanced</button>
|
|
7770
|
+
</div>
|
|
7771
|
+
</div>
|
|
7772
|
+
|
|
7773
|
+
<!-- Tab bar -->
|
|
7774
|
+
<div class="tab-bar">
|
|
7775
|
+
<button class="active" data-tab="fortress">Fortress</button>
|
|
7776
|
+
<button data-tab="advanced">Advanced</button>
|
|
7777
|
+
</div>
|
|
7778
|
+
|
|
7779
|
+
<!-- Fortress View -->
|
|
7780
|
+
<div class="fortress-content" id="fortress-tab">
|
|
7781
|
+
<!-- Status Banner -->
|
|
7782
|
+
<div class="status-banner" id="status-banner">
|
|
7783
|
+
<div class="status-indicator green" id="status-indicator">✓</div>
|
|
7784
|
+
<div class="status-info">
|
|
7785
|
+
<h2 id="status-title">Agent Protected</h2>
|
|
7786
|
+
<p id="status-subtitle">${options.upstreamServerCount} server${options.upstreamServerCount !== 1 ? "s" : ""} monitored. All systems nominal.</p>
|
|
7787
|
+
</div>
|
|
7788
|
+
<div class="status-stats">
|
|
7789
|
+
<div class="stat">
|
|
7790
|
+
<div class="value" id="stat-total">0</div>
|
|
7791
|
+
<div class="label">Calls</div>
|
|
7792
|
+
</div>
|
|
7793
|
+
<div class="stat">
|
|
7794
|
+
<div class="value" id="stat-blocked">0</div>
|
|
7795
|
+
<div class="label">Blocked</div>
|
|
7796
|
+
</div>
|
|
7797
|
+
<div class="stat">
|
|
7798
|
+
<div class="value" id="stat-pending">0</div>
|
|
7799
|
+
<div class="label">Pending</div>
|
|
7800
|
+
</div>
|
|
7801
|
+
</div>
|
|
7802
|
+
</div>
|
|
7803
|
+
|
|
7804
|
+
<!-- Two-column layout -->
|
|
7805
|
+
<div class="fortress-grid">
|
|
7806
|
+
<!-- Live Feed -->
|
|
7807
|
+
<div class="feed-panel">
|
|
7808
|
+
<div class="panel-header">
|
|
7809
|
+
<h3>Live Activity</h3>
|
|
7810
|
+
<span style="font-size: 12px; color: var(--text-muted);" id="feed-count">0 events</span>
|
|
7811
|
+
</div>
|
|
7812
|
+
<div class="feed-list" id="feed-list">
|
|
7813
|
+
<div class="feed-empty">Waiting for tool calls...</div>
|
|
7814
|
+
</div>
|
|
7815
|
+
</div>
|
|
7816
|
+
|
|
7817
|
+
<!-- Right column: Alerts + Servers -->
|
|
7818
|
+
<div>
|
|
7819
|
+
<!-- Alerts -->
|
|
7820
|
+
<div class="alerts-panel">
|
|
7821
|
+
<div class="panel-header">
|
|
7822
|
+
<h3>Needs Attention</h3>
|
|
7823
|
+
<span style="font-size: 12px; color: var(--text-muted);" id="alert-count">0</span>
|
|
7824
|
+
</div>
|
|
7825
|
+
<div id="alerts-list">
|
|
7826
|
+
<div class="alerts-empty">No pending actions</div>
|
|
7827
|
+
</div>
|
|
7828
|
+
</div>
|
|
7829
|
+
|
|
7830
|
+
<!-- Servers -->
|
|
7831
|
+
<div class="alerts-panel servers-panel">
|
|
7832
|
+
<div class="panel-header">
|
|
7833
|
+
<h3>Upstream Servers</h3>
|
|
7834
|
+
</div>
|
|
7835
|
+
<div id="servers-list">
|
|
7836
|
+
<div class="alerts-empty">No servers configured</div>
|
|
7837
|
+
</div>
|
|
7838
|
+
</div>
|
|
7839
|
+
</div>
|
|
7840
|
+
</div>
|
|
7841
|
+
</div>
|
|
7842
|
+
|
|
7843
|
+
<script>
|
|
7844
|
+
// \u2500\u2500 State \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\u2500\u2500\u2500
|
|
7845
|
+
const API_BASE = window.location.origin;
|
|
7846
|
+
const SESSION_TOKEN = sessionStorage.getItem('sanctuary_session') || '';
|
|
7847
|
+
const MAX_FEED_ITEMS = 50;
|
|
7848
|
+
|
|
7849
|
+
let feedItems = [];
|
|
7850
|
+
let totalCalls = 0;
|
|
7851
|
+
let blockedCalls = 0;
|
|
7852
|
+
let pendingApprovals = [];
|
|
7853
|
+
let upstreamServers = [];
|
|
7854
|
+
let paused = false;
|
|
7855
|
+
|
|
7856
|
+
// \u2500\u2500 SSE Connection \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
|
|
7857
|
+
function connectSSE() {
|
|
7858
|
+
const url = API_BASE + '/events' + (SESSION_TOKEN ? '?session=' + SESSION_TOKEN : '');
|
|
7859
|
+
const eventSource = new EventSource(url);
|
|
7860
|
+
|
|
7861
|
+
eventSource.addEventListener('proxy-call', (e) => {
|
|
7862
|
+
try {
|
|
7863
|
+
const data = JSON.parse(e.data);
|
|
7864
|
+
addFeedItem(data);
|
|
7865
|
+
} catch {}
|
|
7866
|
+
});
|
|
7867
|
+
|
|
7868
|
+
eventSource.addEventListener('proxy-server-status', (e) => {
|
|
7869
|
+
try {
|
|
7870
|
+
const data = JSON.parse(e.data);
|
|
7871
|
+
updateServerStatus(data.server, data.state, data.tool_count, data.error);
|
|
7872
|
+
} catch {}
|
|
7873
|
+
});
|
|
7874
|
+
|
|
7875
|
+
eventSource.addEventListener('injection-alert', (e) => {
|
|
7876
|
+
try {
|
|
7877
|
+
const data = JSON.parse(e.data);
|
|
7878
|
+
addFeedItem({
|
|
7879
|
+
tool: data.tool_name || 'unknown',
|
|
7880
|
+
server: 'detection',
|
|
7881
|
+
decision: 'blocked',
|
|
7882
|
+
reason: 'Injection detected: ' + (data.signals || []).join(', '),
|
|
7883
|
+
timestamp: new Date().toISOString(),
|
|
7884
|
+
});
|
|
7885
|
+
} catch {}
|
|
7886
|
+
});
|
|
7887
|
+
|
|
7888
|
+
eventSource.addEventListener('approval-request', (e) => {
|
|
7889
|
+
try {
|
|
7890
|
+
const data = JSON.parse(e.data);
|
|
7891
|
+
addPendingApproval(data);
|
|
7892
|
+
} catch {}
|
|
7893
|
+
});
|
|
7894
|
+
|
|
7895
|
+
eventSource.addEventListener('approval-resolved', (e) => {
|
|
7896
|
+
try {
|
|
7897
|
+
const data = JSON.parse(e.data);
|
|
7898
|
+
removePendingApproval(data.id);
|
|
7899
|
+
} catch {}
|
|
7900
|
+
});
|
|
7901
|
+
|
|
7902
|
+
eventSource.onerror = () => {
|
|
7903
|
+
eventSource.close();
|
|
7904
|
+
setTimeout(connectSSE, 3000);
|
|
7905
|
+
};
|
|
7906
|
+
}
|
|
7907
|
+
|
|
7908
|
+
// \u2500\u2500 Feed \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\u2500\u2500\u2500\u2500
|
|
7909
|
+
function addFeedItem(data) {
|
|
7910
|
+
totalCalls++;
|
|
7911
|
+
if (data.decision === 'blocked' || data.decision === 'denied') {
|
|
7912
|
+
blockedCalls++;
|
|
7913
|
+
}
|
|
7914
|
+
|
|
7915
|
+
feedItems.unshift({
|
|
7916
|
+
tool: data.tool || 'unknown',
|
|
7917
|
+
server: data.server || '',
|
|
7918
|
+
decision: data.decision || 'allowed',
|
|
7919
|
+
reason: data.reason || '',
|
|
7920
|
+
time: data.timestamp || new Date().toISOString(),
|
|
7921
|
+
});
|
|
7922
|
+
|
|
7923
|
+
if (feedItems.length > MAX_FEED_ITEMS) {
|
|
7924
|
+
feedItems = feedItems.slice(0, MAX_FEED_ITEMS);
|
|
7925
|
+
}
|
|
7926
|
+
|
|
7927
|
+
renderFeed();
|
|
7928
|
+
updateStats();
|
|
7929
|
+
updateStatus();
|
|
7930
|
+
}
|
|
7931
|
+
|
|
7932
|
+
function renderFeed() {
|
|
7933
|
+
const container = document.getElementById('feed-list');
|
|
7934
|
+
if (feedItems.length === 0) {
|
|
7935
|
+
container.innerHTML = '<div class="feed-empty">Waiting for tool calls...</div>';
|
|
7936
|
+
return;
|
|
7937
|
+
}
|
|
7938
|
+
|
|
7939
|
+
container.innerHTML = feedItems.map(item => {
|
|
7940
|
+
const dotColor = item.decision === 'allowed' ? 'green'
|
|
7941
|
+
: item.decision === 'pending' ? 'amber' : 'red';
|
|
7942
|
+
const decisionText = item.decision === 'allowed' ? 'Auto-allowed'
|
|
7943
|
+
: item.decision === 'pending' ? 'Awaiting approval'
|
|
7944
|
+
: item.decision === 'blocked' ? 'Blocked' : item.decision;
|
|
7945
|
+
const timeStr = new Date(item.time).toLocaleTimeString();
|
|
7946
|
+
|
|
7947
|
+
return '<div class="feed-item">' +
|
|
7948
|
+
'<div class="feed-dot ' + dotColor + '"></div>' +
|
|
7949
|
+
'<div class="feed-detail">' +
|
|
7950
|
+
'<div class="feed-tool">' + esc(item.tool) + '</div>' +
|
|
7951
|
+
'<div class="feed-decision">' + esc(decisionText) +
|
|
7952
|
+
(item.reason ? ' \u2014 ' + esc(item.reason) : '') + '</div>' +
|
|
7953
|
+
'</div>' +
|
|
7954
|
+
'<div class="feed-time">' + esc(timeStr) + '</div>' +
|
|
7955
|
+
'</div>';
|
|
7956
|
+
}).join('');
|
|
7957
|
+
|
|
7958
|
+
document.getElementById('feed-count').textContent = feedItems.length + ' events';
|
|
7959
|
+
}
|
|
7960
|
+
|
|
7961
|
+
// \u2500\u2500 Alerts (Pending Approvals) \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
|
|
7962
|
+
function addPendingApproval(data) {
|
|
7963
|
+
pendingApprovals.push(data);
|
|
7964
|
+
renderAlerts();
|
|
7965
|
+
updateStats();
|
|
7966
|
+
updateStatus();
|
|
7967
|
+
}
|
|
7968
|
+
|
|
7969
|
+
function removePendingApproval(id) {
|
|
7970
|
+
pendingApprovals = pendingApprovals.filter(a => a.id !== id);
|
|
7971
|
+
renderAlerts();
|
|
7972
|
+
updateStats();
|
|
7973
|
+
updateStatus();
|
|
7974
|
+
}
|
|
7975
|
+
|
|
7976
|
+
function renderAlerts() {
|
|
7977
|
+
const container = document.getElementById('alerts-list');
|
|
7978
|
+
if (pendingApprovals.length === 0) {
|
|
7979
|
+
container.innerHTML = '<div class="alerts-empty">No pending actions</div>';
|
|
7980
|
+
document.getElementById('alert-count').textContent = '0';
|
|
7981
|
+
return;
|
|
7982
|
+
}
|
|
7983
|
+
|
|
7984
|
+
document.getElementById('alert-count').textContent = pendingApprovals.length.toString();
|
|
7985
|
+
|
|
7986
|
+
container.innerHTML = pendingApprovals.map(approval => {
|
|
7987
|
+
return '<div class="alert-item">' +
|
|
7988
|
+
'<div class="alert-title">Approval required: ' + esc(approval.operation || approval.tool_name || 'unknown') + '</div>' +
|
|
7989
|
+
'<div class="alert-desc">' + esc(approval.reason || 'This operation requires your approval before it can proceed.') + '</div>' +
|
|
7990
|
+
'<div class="alert-actions">' +
|
|
7991
|
+
'<button class="approve-btn" onclick="handleApproval(\\'' + esc(approval.id) + '\\', true)">Approve</button>' +
|
|
7992
|
+
'<button class="deny-btn" onclick="handleApproval(\\'' + esc(approval.id) + '\\', false)">Deny</button>' +
|
|
7993
|
+
'</div>' +
|
|
7994
|
+
'</div>';
|
|
7995
|
+
}).join('');
|
|
7996
|
+
}
|
|
7997
|
+
|
|
7998
|
+
async function handleApproval(id, approved) {
|
|
7999
|
+
const endpoint = approved ? '/api/approve/' : '/api/deny/';
|
|
8000
|
+
try {
|
|
8001
|
+
await fetch(API_BASE + endpoint + id, {
|
|
8002
|
+
method: 'POST',
|
|
8003
|
+
headers: SESSION_TOKEN ? { 'Authorization': 'Bearer ' + SESSION_TOKEN } : {},
|
|
8004
|
+
});
|
|
8005
|
+
removePendingApproval(id);
|
|
8006
|
+
} catch (err) {
|
|
8007
|
+
console.error('Approval action failed:', err);
|
|
8008
|
+
}
|
|
8009
|
+
}
|
|
8010
|
+
|
|
8011
|
+
// \u2500\u2500 Servers \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\u2500
|
|
8012
|
+
function updateServerStatus(serverName, state, toolCount, error) {
|
|
8013
|
+
const existing = upstreamServers.find(s => s.name === serverName);
|
|
8014
|
+
if (existing) {
|
|
8015
|
+
existing.state = state;
|
|
8016
|
+
existing.tool_count = toolCount;
|
|
8017
|
+
existing.error = error;
|
|
8018
|
+
} else {
|
|
8019
|
+
upstreamServers.push({ name: serverName, state, tool_count: toolCount, error });
|
|
8020
|
+
}
|
|
8021
|
+
renderServers();
|
|
8022
|
+
updateStatus();
|
|
8023
|
+
}
|
|
8024
|
+
|
|
8025
|
+
function renderServers() {
|
|
8026
|
+
const container = document.getElementById('servers-list');
|
|
8027
|
+
if (upstreamServers.length === 0) {
|
|
8028
|
+
container.innerHTML = '<div class="alerts-empty">No servers configured</div>';
|
|
8029
|
+
return;
|
|
8030
|
+
}
|
|
8031
|
+
|
|
8032
|
+
container.innerHTML = upstreamServers.map(server => {
|
|
8033
|
+
const stateClass = server.state || 'disconnected';
|
|
8034
|
+
const stateLabel = server.state === 'connected' ? 'Connected'
|
|
8035
|
+
: server.state === 'connecting' ? 'Connecting...'
|
|
8036
|
+
: server.state === 'error' ? 'Error' : 'Disconnected';
|
|
8037
|
+
|
|
8038
|
+
return '<div class="server-row">' +
|
|
8039
|
+
'<div class="server-status-dot ' + stateClass + '"></div>' +
|
|
8040
|
+
'<span class="server-name">' + esc(server.name) + '</span>' +
|
|
8041
|
+
'<span class="server-tier">' + esc(stateLabel) +
|
|
8042
|
+
(server.tool_count ? ' (' + server.tool_count + ' tools)' : '') + '</span>' +
|
|
8043
|
+
'</div>';
|
|
8044
|
+
}).join('');
|
|
8045
|
+
}
|
|
8046
|
+
|
|
8047
|
+
// \u2500\u2500 Status Banner \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
|
|
8048
|
+
function updateStats() {
|
|
8049
|
+
document.getElementById('stat-total').textContent = totalCalls.toString();
|
|
8050
|
+
document.getElementById('stat-blocked').textContent = blockedCalls.toString();
|
|
8051
|
+
document.getElementById('stat-pending').textContent = pendingApprovals.length.toString();
|
|
8052
|
+
}
|
|
8053
|
+
|
|
8054
|
+
function updateStatus() {
|
|
8055
|
+
const indicator = document.getElementById('status-indicator');
|
|
8056
|
+
const title = document.getElementById('status-title');
|
|
8057
|
+
const subtitle = document.getElementById('status-subtitle');
|
|
8058
|
+
|
|
8059
|
+
const hasErrors = upstreamServers.some(s => s.state === 'error');
|
|
8060
|
+
const hasPending = pendingApprovals.length > 0;
|
|
8061
|
+
const hasBlocked = blockedCalls > 0;
|
|
8062
|
+
|
|
8063
|
+
if (paused) {
|
|
8064
|
+
indicator.className = 'status-indicator red';
|
|
8065
|
+
indicator.innerHTML = '⏸';
|
|
8066
|
+
title.textContent = 'Agent Paused';
|
|
8067
|
+
subtitle.textContent = 'All operations require approval. Click Resume to restore normal mode.';
|
|
8068
|
+
} else if (hasErrors) {
|
|
8069
|
+
indicator.className = 'status-indicator red';
|
|
8070
|
+
indicator.innerHTML = '⚠';
|
|
8071
|
+
title.textContent = 'Connection Issues';
|
|
8072
|
+
subtitle.textContent = 'One or more upstream servers have errors.';
|
|
8073
|
+
} else if (hasPending) {
|
|
8074
|
+
indicator.className = 'status-indicator amber';
|
|
8075
|
+
indicator.innerHTML = '⏳';
|
|
8076
|
+
title.textContent = 'Action Required';
|
|
8077
|
+
subtitle.textContent = pendingApprovals.length + ' operation' + (pendingApprovals.length > 1 ? 's' : '') + ' awaiting your approval.';
|
|
8078
|
+
} else {
|
|
8079
|
+
indicator.className = 'status-indicator green';
|
|
8080
|
+
indicator.innerHTML = '✓';
|
|
8081
|
+
title.textContent = 'Agent Protected';
|
|
8082
|
+
const serverCount = upstreamServers.filter(s => s.state === 'connected').length || ${options.upstreamServerCount};
|
|
8083
|
+
subtitle.textContent = serverCount + ' server' + (serverCount !== 1 ? 's' : '') + ' monitored. All systems nominal.';
|
|
8084
|
+
}
|
|
8085
|
+
}
|
|
8086
|
+
|
|
8087
|
+
// \u2500\u2500 Pause/Resume \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
|
|
8088
|
+
document.getElementById('pause-btn').addEventListener('click', () => {
|
|
8089
|
+
paused = !paused;
|
|
8090
|
+
const btn = document.getElementById('pause-btn');
|
|
8091
|
+
if (paused) {
|
|
8092
|
+
btn.textContent = 'Resume Agent';
|
|
8093
|
+
btn.classList.add('paused');
|
|
8094
|
+
} else {
|
|
8095
|
+
btn.textContent = 'Pause Agent';
|
|
8096
|
+
btn.classList.remove('paused');
|
|
8097
|
+
}
|
|
8098
|
+
updateStatus();
|
|
8099
|
+
// TODO: POST to /api/cocoon/pause to set all tiers to 1
|
|
8100
|
+
});
|
|
8101
|
+
|
|
8102
|
+
// \u2500\u2500 Tab switching \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
|
|
8103
|
+
document.getElementById('advanced-btn').addEventListener('click', () => {
|
|
8104
|
+
window.location.href = '/dashboard?session=' + SESSION_TOKEN;
|
|
8105
|
+
});
|
|
8106
|
+
|
|
8107
|
+
document.querySelectorAll('.tab-bar button').forEach(btn => {
|
|
8108
|
+
btn.addEventListener('click', () => {
|
|
8109
|
+
const tab = btn.dataset.tab;
|
|
8110
|
+
if (tab === 'advanced') {
|
|
8111
|
+
window.location.href = '/dashboard?session=' + SESSION_TOKEN;
|
|
8112
|
+
}
|
|
8113
|
+
});
|
|
8114
|
+
});
|
|
8115
|
+
|
|
8116
|
+
// \u2500\u2500 Escape helper \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
|
|
8117
|
+
function esc(str) {
|
|
8118
|
+
if (!str) return '';
|
|
8119
|
+
const d = document.createElement('div');
|
|
8120
|
+
d.textContent = String(str);
|
|
8121
|
+
return d.innerHTML;
|
|
8122
|
+
}
|
|
8123
|
+
|
|
8124
|
+
// \u2500\u2500 Init \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\u2500\u2500
|
|
8125
|
+
async function init() {
|
|
8126
|
+
// Load initial server state
|
|
8127
|
+
try {
|
|
8128
|
+
const resp = await fetch(API_BASE + '/api/proxy/servers', {
|
|
8129
|
+
headers: SESSION_TOKEN ? { 'Authorization': 'Bearer ' + SESSION_TOKEN } : {},
|
|
8130
|
+
});
|
|
8131
|
+
if (resp.ok) {
|
|
8132
|
+
const data = await resp.json();
|
|
8133
|
+
upstreamServers = data.servers || [];
|
|
8134
|
+
renderServers();
|
|
8135
|
+
}
|
|
8136
|
+
} catch {}
|
|
8137
|
+
|
|
8138
|
+
// Load pending approvals
|
|
8139
|
+
try {
|
|
8140
|
+
const resp = await fetch(API_BASE + '/api/pending', {
|
|
8141
|
+
headers: SESSION_TOKEN ? { 'Authorization': 'Bearer ' + SESSION_TOKEN } : {},
|
|
8142
|
+
});
|
|
8143
|
+
if (resp.ok) {
|
|
8144
|
+
const data = await resp.json();
|
|
8145
|
+
pendingApprovals = data.pending || [];
|
|
8146
|
+
renderAlerts();
|
|
8147
|
+
updateStats();
|
|
8148
|
+
}
|
|
8149
|
+
} catch {}
|
|
8150
|
+
|
|
8151
|
+
updateStatus();
|
|
8152
|
+
connectSSE();
|
|
8153
|
+
}
|
|
8154
|
+
|
|
8155
|
+
init();
|
|
8156
|
+
</script>
|
|
8157
|
+
</body>
|
|
8158
|
+
</html>`;
|
|
8159
|
+
}
|
|
8160
|
+
function esc(str) {
|
|
8161
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
8162
|
+
}
|
|
8163
|
+
|
|
8164
|
+
// src/system-prompt-generator.ts
|
|
8165
|
+
var FEATURE_INFO = {
|
|
8166
|
+
audit_logging: {
|
|
8167
|
+
name: "Audit Logging",
|
|
8168
|
+
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 monitor_audit_log if you need to review past activity.",
|
|
8169
|
+
toolNames: ["monitor_audit_log"],
|
|
8170
|
+
disabledDescription: "audit logging (monitor_audit_log)",
|
|
8171
|
+
usageExample: "Automatic \u2014 every tool call you make is recorded. No explicit action required."
|
|
8172
|
+
},
|
|
8173
|
+
injection_detection: {
|
|
8174
|
+
name: "Injection Detection",
|
|
8175
|
+
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.",
|
|
8176
|
+
disabledDescription: "injection detection",
|
|
8177
|
+
usageExample: "Automatic \u2014 if a tool call is blocked with an injection alert, do not retry with the same arguments."
|
|
8178
|
+
},
|
|
8179
|
+
context_gating: {
|
|
8180
|
+
name: "Context Gating",
|
|
8181
|
+
activeDescription: "Before sending context to any external API (LLM inference, tool APIs, logging services), call context_gate_filter to strip sensitive fields. Use context_gate_set_policy to define filtering rules, or context_gate_apply_template for presets.",
|
|
8182
|
+
toolNames: [
|
|
8183
|
+
"context_gate_filter",
|
|
8184
|
+
"context_gate_set_policy",
|
|
8185
|
+
"context_gate_apply_template",
|
|
8186
|
+
"context_gate_recommend",
|
|
8187
|
+
"context_gate_list_policies"
|
|
8188
|
+
],
|
|
8189
|
+
disabledDescription: "context gating (context_gate_filter)",
|
|
8190
|
+
usageExample: "Before calling an external API, run: context_gate_filter with your context object and policy_id to get a filtered version."
|
|
8191
|
+
},
|
|
8192
|
+
approval_gate: {
|
|
8193
|
+
name: "Approval Gates",
|
|
8194
|
+
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.",
|
|
8195
|
+
disabledDescription: "approval gates",
|
|
8196
|
+
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."
|
|
8197
|
+
},
|
|
8198
|
+
zk_proofs: {
|
|
8199
|
+
name: "Zero-Knowledge Proofs",
|
|
8200
|
+
activeDescription: "You can prove claims about your data without revealing the underlying values. Use zk_commit to create a Pedersen commitment, zk_prove (Schnorr proof) to prove you know a committed value, and zk_range_prove to prove a value falls within a range \u2014 all without disclosing the actual data. For simpler SHA-256 commitments, use proof_commitment.",
|
|
8201
|
+
toolNames: [
|
|
8202
|
+
"zk_commit",
|
|
8203
|
+
"zk_prove",
|
|
8204
|
+
"zk_range_prove",
|
|
8205
|
+
"proof_commitment"
|
|
8206
|
+
],
|
|
8207
|
+
disabledDescription: "zero-knowledge proofs (zk_commit, zk_prove)",
|
|
8208
|
+
usageExample: "To prove a claim without revealing data: first zk_commit to commit, then zk_prove or zk_range_prove to generate a verifiable proof."
|
|
8209
|
+
}
|
|
8210
|
+
};
|
|
8211
|
+
function generateSystemPrompt(profile) {
|
|
8212
|
+
const activeFeatures = [];
|
|
8213
|
+
const inactiveFeatures = [];
|
|
8214
|
+
const activeKeys = [];
|
|
8215
|
+
const featureKeys = [
|
|
8216
|
+
"audit_logging",
|
|
8217
|
+
"injection_detection",
|
|
8218
|
+
"context_gating",
|
|
8219
|
+
"approval_gate",
|
|
8220
|
+
"zk_proofs"
|
|
8221
|
+
];
|
|
8222
|
+
for (const key of featureKeys) {
|
|
8223
|
+
const featureConfig = profile.features[key];
|
|
8224
|
+
const info = FEATURE_INFO[key];
|
|
8225
|
+
if (featureConfig.enabled) {
|
|
8226
|
+
activeKeys.push(key);
|
|
8227
|
+
let desc = `- ${info.name}: ${info.activeDescription}`;
|
|
8228
|
+
if (key === "injection_detection" && "sensitivity" in featureConfig && featureConfig.sensitivity) {
|
|
8229
|
+
desc += ` Sensitivity: ${featureConfig.sensitivity}.`;
|
|
8230
|
+
}
|
|
8231
|
+
if (key === "context_gating" && "policy_id" in featureConfig && featureConfig.policy_id) {
|
|
8232
|
+
desc += ` Active policy: ${featureConfig.policy_id}.`;
|
|
8233
|
+
}
|
|
8234
|
+
activeFeatures.push(desc);
|
|
8235
|
+
} else {
|
|
8236
|
+
inactiveFeatures.push(info.disabledDescription);
|
|
8237
|
+
}
|
|
8238
|
+
}
|
|
8239
|
+
const lines = [];
|
|
8240
|
+
if (activeKeys.length > 0) {
|
|
8241
|
+
lines.push("QUICK START:");
|
|
8242
|
+
const quickStartItems = buildQuickStart(activeKeys);
|
|
8243
|
+
for (const item of quickStartItems) {
|
|
8244
|
+
lines.push(` ${item}`);
|
|
8245
|
+
}
|
|
8246
|
+
lines.push("");
|
|
8247
|
+
}
|
|
8248
|
+
lines.push(
|
|
8249
|
+
"You are protected by Sanctuary sovereignty infrastructure. The following protections are active:"
|
|
8250
|
+
);
|
|
8251
|
+
lines.push("");
|
|
8252
|
+
if (activeFeatures.length > 0) {
|
|
8253
|
+
lines.push(...activeFeatures);
|
|
8254
|
+
} else {
|
|
8255
|
+
lines.push(
|
|
8256
|
+
"- No features are currently enabled. Contact your operator to configure protections."
|
|
8257
|
+
);
|
|
8258
|
+
}
|
|
8259
|
+
if (inactiveFeatures.length > 0) {
|
|
8260
|
+
lines.push("");
|
|
8261
|
+
lines.push(
|
|
8262
|
+
`Optional tools available but not currently enabled: ${inactiveFeatures.join(", ")}.`
|
|
8263
|
+
);
|
|
8264
|
+
}
|
|
8265
|
+
return lines.join("\n");
|
|
8266
|
+
}
|
|
8267
|
+
function buildQuickStart(activeKeys) {
|
|
8268
|
+
const items = [];
|
|
8269
|
+
if (activeKeys.includes("context_gating")) {
|
|
8270
|
+
items.push(
|
|
8271
|
+
"1. ALWAYS call context_gate_filter before sending context to external APIs."
|
|
8272
|
+
);
|
|
8273
|
+
}
|
|
8274
|
+
if (activeKeys.includes("zk_proofs")) {
|
|
8275
|
+
items.push(
|
|
8276
|
+
`${items.length + 1}. Use zk_commit to prove claims without revealing underlying data.`
|
|
8277
|
+
);
|
|
8278
|
+
}
|
|
8279
|
+
if (activeKeys.includes("approval_gate")) {
|
|
8280
|
+
items.push(
|
|
8281
|
+
`${items.length + 1}. High-risk operations will be held for human approval \u2014 expect async responses.`
|
|
8282
|
+
);
|
|
8283
|
+
}
|
|
8284
|
+
if (items.length === 0) {
|
|
8285
|
+
if (activeKeys.includes("audit_logging")) {
|
|
8286
|
+
items.push("1. All tool calls are automatically logged to an encrypted audit trail.");
|
|
8287
|
+
}
|
|
8288
|
+
if (activeKeys.includes("injection_detection")) {
|
|
8289
|
+
items.push(
|
|
8290
|
+
`${items.length + 1}. Tool arguments are scanned for injection \u2014 blocked calls should not be retried.`
|
|
8291
|
+
);
|
|
8292
|
+
}
|
|
8293
|
+
}
|
|
8294
|
+
return items;
|
|
8295
|
+
}
|
|
8296
|
+
|
|
8297
|
+
// src/principal-policy/dashboard.ts
|
|
8298
|
+
var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
|
|
8299
|
+
var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
|
|
8300
|
+
var MAX_SESSIONS = 1e3;
|
|
8301
|
+
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
8302
|
+
var RATE_LIMIT_GENERAL = 120;
|
|
8303
|
+
var RATE_LIMIT_DECISIONS = 20;
|
|
8304
|
+
var MAX_RATE_LIMIT_ENTRIES = 1e4;
|
|
8305
|
+
var DashboardApprovalChannel = class {
|
|
8306
|
+
config;
|
|
8307
|
+
pending = /* @__PURE__ */ new Map();
|
|
8308
|
+
sseClients = /* @__PURE__ */ new Set();
|
|
8309
|
+
httpServer = null;
|
|
8310
|
+
policy = null;
|
|
8311
|
+
baseline = null;
|
|
8312
|
+
auditLog = null;
|
|
8313
|
+
identityManager = null;
|
|
8314
|
+
handshakeResults = null;
|
|
8315
|
+
shrOpts = null;
|
|
8316
|
+
_sanctuaryConfig = null;
|
|
8317
|
+
profileStore = null;
|
|
8318
|
+
clientManager = null;
|
|
8319
|
+
dashboardHTML;
|
|
8320
|
+
fortressHTML = null;
|
|
8321
|
+
loginHTML;
|
|
8322
|
+
authToken;
|
|
8323
|
+
useTLS;
|
|
8324
|
+
/** Session TTL: longer for localhost, shorter for remote */
|
|
8325
|
+
sessionTTLMs;
|
|
8326
|
+
/** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
|
|
8327
|
+
sessions = /* @__PURE__ */ new Map();
|
|
8328
|
+
sessionCleanupTimer = null;
|
|
8329
|
+
/** Rate limiting: per-IP request tracking */
|
|
8330
|
+
rateLimits = /* @__PURE__ */ new Map();
|
|
8331
|
+
/** Whether the dashboard is running in standalone mode (no MCP server) */
|
|
8332
|
+
_standaloneMode = false;
|
|
8333
|
+
constructor(config) {
|
|
8334
|
+
this.config = config;
|
|
8335
|
+
this.authToken = config.auth_token;
|
|
8336
|
+
this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
|
|
8337
|
+
const isLocalhost = config.host === "127.0.0.1" || config.host === "localhost" || config.host === "::1";
|
|
8338
|
+
this.sessionTTLMs = isLocalhost ? SESSION_TTL_LOCAL_MS : SESSION_TTL_REMOTE_MS;
|
|
8339
|
+
this.dashboardHTML = generateDashboardHTML({
|
|
8340
|
+
timeoutSeconds: config.timeout_seconds,
|
|
8341
|
+
serverVersion: SANCTUARY_VERSION
|
|
8342
|
+
});
|
|
8343
|
+
this.loginHTML = generateLoginHTML({ serverVersion: SANCTUARY_VERSION });
|
|
8344
|
+
this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
|
|
8345
|
+
}
|
|
8346
|
+
/**
|
|
8347
|
+
* Inject dependencies after construction.
|
|
8348
|
+
* Called from index.ts after all components are initialized.
|
|
8349
|
+
*/
|
|
8350
|
+
setDependencies(deps) {
|
|
8351
|
+
this.policy = deps.policy;
|
|
8352
|
+
this.baseline = deps.baseline;
|
|
8353
|
+
this.auditLog = deps.auditLog;
|
|
8354
|
+
if (deps.identityManager) this.identityManager = deps.identityManager;
|
|
8355
|
+
if (deps.handshakeResults) this.handshakeResults = deps.handshakeResults;
|
|
8356
|
+
if (deps.shrOpts) this.shrOpts = deps.shrOpts;
|
|
8357
|
+
if (deps.sanctuaryConfig) this._sanctuaryConfig = deps.sanctuaryConfig;
|
|
8358
|
+
if (deps.profileStore) this.profileStore = deps.profileStore;
|
|
8359
|
+
if (deps.clientManager) this.clientManager = deps.clientManager;
|
|
8360
|
+
}
|
|
8361
|
+
/**
|
|
8362
|
+
* Mark this dashboard as running in standalone mode.
|
|
8363
|
+
* Exposed via /api/status so the frontend can show an appropriate banner.
|
|
8364
|
+
*/
|
|
8365
|
+
setStandaloneMode(standalone) {
|
|
8366
|
+
this._standaloneMode = standalone;
|
|
8367
|
+
}
|
|
8368
|
+
/**
|
|
8369
|
+
* Start the HTTP(S) server for the dashboard.
|
|
8370
|
+
*/
|
|
8371
|
+
async start() {
|
|
8372
|
+
return new Promise((resolve, reject) => {
|
|
8373
|
+
const handler = (req, res) => this.handleRequest(req, res);
|
|
8374
|
+
if (this.useTLS && this.config.tls) {
|
|
8375
|
+
const tlsOpts = {
|
|
8376
|
+
cert: readFileSync(this.config.tls.cert_path),
|
|
8377
|
+
key: readFileSync(this.config.tls.key_path)
|
|
8378
|
+
};
|
|
8379
|
+
this.httpServer = createServer$1(tlsOpts, handler);
|
|
8380
|
+
} else {
|
|
8381
|
+
this.httpServer = createServer$2(handler);
|
|
8382
|
+
}
|
|
8383
|
+
const protocol = this.useTLS ? "https" : "http";
|
|
8384
|
+
const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
|
|
8385
|
+
this.httpServer.listen(this.config.port, this.config.host, () => {
|
|
8386
|
+
const sessionUrl = this.authToken ? this.createSessionUrl() : baseUrl;
|
|
8387
|
+
process.stderr.write(
|
|
8388
|
+
`
|
|
8389
|
+
Sanctuary Principal Dashboard: ${baseUrl}
|
|
8390
|
+
`
|
|
8391
|
+
);
|
|
8392
|
+
if (this.authToken) {
|
|
8393
|
+
const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
|
|
8394
|
+
process.stderr.write(
|
|
8395
|
+
` Auth token: ${hint}
|
|
8396
|
+
`
|
|
8397
|
+
);
|
|
8398
|
+
}
|
|
8399
|
+
process.stderr.write(`
|
|
7562
8400
|
`);
|
|
7563
8401
|
const isTest = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI);
|
|
7564
8402
|
const isLocalhost = this.config.host === "127.0.0.1" || this.config.host === "localhost" || this.config.host === "::1";
|
|
@@ -7865,8 +8703,14 @@ var DashboardApprovalChannel = class {
|
|
|
7865
8703
|
if (!this.checkAuth(req, url, res)) return;
|
|
7866
8704
|
if (!this.checkRateLimit(req, res, "general")) return;
|
|
7867
8705
|
try {
|
|
7868
|
-
if (method === "GET" &&
|
|
7869
|
-
this.
|
|
8706
|
+
if (method === "GET" && url.pathname === "/fortress") {
|
|
8707
|
+
this.serveFortressView(res);
|
|
8708
|
+
} else if (method === "GET" && (url.pathname === "/" || url.pathname === "/dashboard")) {
|
|
8709
|
+
if (this.fortressHTML) {
|
|
8710
|
+
this.serveFortressView(res);
|
|
8711
|
+
} else {
|
|
8712
|
+
this.serveDashboard(res);
|
|
8713
|
+
}
|
|
7870
8714
|
} else if (method === "GET" && url.pathname === "/events") {
|
|
7871
8715
|
this.handleSSE(req, res);
|
|
7872
8716
|
} else if (method === "GET" && url.pathname === "/api/status") {
|
|
@@ -7961,6 +8805,35 @@ var DashboardApprovalChannel = class {
|
|
|
7961
8805
|
});
|
|
7962
8806
|
res.end(this.dashboardHTML);
|
|
7963
8807
|
}
|
|
8808
|
+
serveFortressView(res) {
|
|
8809
|
+
if (!this.fortressHTML) {
|
|
8810
|
+
this.serveDashboard(res);
|
|
8811
|
+
return;
|
|
8812
|
+
}
|
|
8813
|
+
res.writeHead(200, {
|
|
8814
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
8815
|
+
"Cache-Control": "no-cache"
|
|
8816
|
+
});
|
|
8817
|
+
res.end(this.fortressHTML);
|
|
8818
|
+
}
|
|
8819
|
+
/**
|
|
8820
|
+
* Enable Fortress View (Cocoon mode) with the given upstream server count.
|
|
8821
|
+
* Once enabled, the root path `/` serves the Fortress View instead of the
|
|
8822
|
+
* standard dashboard. The standard dashboard remains available at `/dashboard`.
|
|
8823
|
+
*/
|
|
8824
|
+
enableFortressView(upstreamServerCount) {
|
|
8825
|
+
this.fortressHTML = generateFortressViewHTML({
|
|
8826
|
+
serverVersion: SANCTUARY_VERSION,
|
|
8827
|
+
authToken: this.authToken,
|
|
8828
|
+
upstreamServerCount
|
|
8829
|
+
});
|
|
8830
|
+
}
|
|
8831
|
+
/**
|
|
8832
|
+
* Broadcast a proxy call event to connected dashboards (Fortress View feed).
|
|
8833
|
+
*/
|
|
8834
|
+
broadcastProxyCall(data) {
|
|
8835
|
+
this.broadcastSSE("proxy-call", data);
|
|
8836
|
+
}
|
|
7964
8837
|
handleSSE(req, res) {
|
|
7965
8838
|
res.writeHead(200, {
|
|
7966
8839
|
"Content-Type": "text/event-stream",
|
|
@@ -8824,7 +9697,7 @@ var InjectionDetector = class {
|
|
|
8824
9697
|
}
|
|
8825
9698
|
/**
|
|
8826
9699
|
* Scan tool arguments for injection signals.
|
|
8827
|
-
* @param toolName Full tool name (e.g., "
|
|
9700
|
+
* @param toolName Full tool name (e.g., "state_read")
|
|
8828
9701
|
* @param args Tool arguments
|
|
8829
9702
|
* @returns DetectionResult with all detected signals
|
|
8830
9703
|
*/
|
|
@@ -9825,7 +10698,7 @@ var ApprovalGate = class {
|
|
|
9825
10698
|
/**
|
|
9826
10699
|
* Evaluate a tool call against the Principal Policy.
|
|
9827
10700
|
*
|
|
9828
|
-
* @param toolName - Full MCP tool name (e.g., "
|
|
10701
|
+
* @param toolName - Full MCP tool name (e.g., "state_export")
|
|
9829
10702
|
* @param args - Tool call arguments (for context extraction)
|
|
9830
10703
|
* @returns GateResult indicating whether the call is allowed
|
|
9831
10704
|
*/
|
|
@@ -10094,7 +10967,7 @@ var ApprovalGate = class {
|
|
|
10094
10967
|
function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
10095
10968
|
return [
|
|
10096
10969
|
{
|
|
10097
|
-
name: "
|
|
10970
|
+
name: "principal_policy_view",
|
|
10098
10971
|
description: "View the current Principal Policy \u2014 the human-controlled rules governing what operations require approval. Read-only.",
|
|
10099
10972
|
inputSchema: {
|
|
10100
10973
|
type: "object",
|
|
@@ -10132,7 +11005,7 @@ function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
|
10132
11005
|
}
|
|
10133
11006
|
},
|
|
10134
11007
|
{
|
|
10135
|
-
name: "
|
|
11008
|
+
name: "principal_baseline_view",
|
|
10136
11009
|
description: "View the current behavioral baseline \u2014 the session profile used for anomaly detection. Shows known namespaces, counterparties, and tool call counts. Read-only.",
|
|
10137
11010
|
inputSchema: {
|
|
10138
11011
|
type: "object",
|
|
@@ -10472,7 +11345,7 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
|
|
|
10472
11345
|
};
|
|
10473
11346
|
const tools = [
|
|
10474
11347
|
{
|
|
10475
|
-
name: "
|
|
11348
|
+
name: "shr_generate",
|
|
10476
11349
|
description: "Generate a signed Sovereignty Health Report (SHR) \u2014 a machine-readable, cryptographically signed advertisement of this instance's sovereignty posture. Present this to counterparties to prove your sovereignty capabilities.",
|
|
10477
11350
|
inputSchema: {
|
|
10478
11351
|
type: "object",
|
|
@@ -10501,7 +11374,7 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
|
|
|
10501
11374
|
}
|
|
10502
11375
|
},
|
|
10503
11376
|
{
|
|
10504
|
-
name: "
|
|
11377
|
+
name: "shr_verify",
|
|
10505
11378
|
description: "Verify a counterparty's Sovereignty Health Report (SHR). Checks signature validity, temporal validity, and assesses sovereignty level.",
|
|
10506
11379
|
inputSchema: {
|
|
10507
11380
|
type: "object",
|
|
@@ -10527,7 +11400,7 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
|
|
|
10527
11400
|
}
|
|
10528
11401
|
},
|
|
10529
11402
|
{
|
|
10530
|
-
name: "
|
|
11403
|
+
name: "shr_gateway_export",
|
|
10531
11404
|
description: "Export this instance's Sovereignty Health Report formatted for Ping Identity's Agent Gateway or other identity providers. Transforms the SHR into an authorization context with sovereignty scores, capability flags, and recommended access constraints.",
|
|
10532
11405
|
inputSchema: {
|
|
10533
11406
|
type: "object",
|
|
@@ -10577,6 +11450,10 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
|
|
|
10577
11450
|
return { tools };
|
|
10578
11451
|
}
|
|
10579
11452
|
|
|
11453
|
+
// src/handshake/tools.ts
|
|
11454
|
+
init_identity();
|
|
11455
|
+
init_encoding();
|
|
11456
|
+
|
|
10580
11457
|
// src/handshake/protocol.ts
|
|
10581
11458
|
init_identity();
|
|
10582
11459
|
init_encoding();
|
|
@@ -10897,7 +11774,10 @@ function verifyAttestation(attestation, now) {
|
|
|
10897
11774
|
}
|
|
10898
11775
|
|
|
10899
11776
|
// src/handshake/tools.ts
|
|
10900
|
-
function createHandshakeTools(config, identityManager, masterKey, auditLog) {
|
|
11777
|
+
function createHandshakeTools(config, identityManager, masterKey, auditLog, options) {
|
|
11778
|
+
const autoPublishHandshakes = options?.autoPublishHandshakes ?? false;
|
|
11779
|
+
const verascoreUrl = options?.verascoreUrl ?? "https://verascore.ai";
|
|
11780
|
+
const identityEncKey = derivePurposeKey(masterKey, "identity-encryption");
|
|
10901
11781
|
const sessions = /* @__PURE__ */ new Map();
|
|
10902
11782
|
const handshakeResults = /* @__PURE__ */ new Map();
|
|
10903
11783
|
const shrOpts = {
|
|
@@ -10907,7 +11787,7 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
|
|
|
10907
11787
|
};
|
|
10908
11788
|
const tools = [
|
|
10909
11789
|
{
|
|
10910
|
-
name: "
|
|
11790
|
+
name: "handshake_initiate",
|
|
10911
11791
|
description: "Initiate a sovereignty handshake with a counterparty. Generates a challenge containing this instance's signed SHR and a cryptographic nonce. Send the returned challenge to the counterparty.",
|
|
10912
11792
|
inputSchema: {
|
|
10913
11793
|
type: "object",
|
|
@@ -10934,7 +11814,7 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
|
|
|
10934
11814
|
}
|
|
10935
11815
|
},
|
|
10936
11816
|
{
|
|
10937
|
-
name: "
|
|
11817
|
+
name: "handshake_respond",
|
|
10938
11818
|
description: "Respond to an incoming sovereignty handshake challenge. Verifies the initiator's SHR, signs their nonce, and returns our SHR with a counter-nonce.",
|
|
10939
11819
|
inputSchema: {
|
|
10940
11820
|
type: "object",
|
|
@@ -10969,17 +11849,93 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
|
|
|
10969
11849
|
}
|
|
10970
11850
|
sessions.set(result.session.session_id, result.session);
|
|
10971
11851
|
auditLog.append("l4", "handshake_respond", shr.body.instance_id);
|
|
11852
|
+
let autoPublishResult;
|
|
11853
|
+
if (autoPublishHandshakes) {
|
|
11854
|
+
autoPublishResult = { attempted: true };
|
|
11855
|
+
try {
|
|
11856
|
+
const parsed = new URL(verascoreUrl);
|
|
11857
|
+
if (parsed.protocol !== "https:") {
|
|
11858
|
+
autoPublishResult.error = `verascore URL must use HTTPS (got ${parsed.protocol})`;
|
|
11859
|
+
} else {
|
|
11860
|
+
const attestationPayload = {
|
|
11861
|
+
type: "handshake",
|
|
11862
|
+
our_shr_signed_by: shr.signed_by,
|
|
11863
|
+
counterparty_signed_by: "redacted",
|
|
11864
|
+
session_id: result.session.session_id,
|
|
11865
|
+
responded_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
11866
|
+
};
|
|
11867
|
+
const responderIdentity = identityManager.get(shr.body.instance_id);
|
|
11868
|
+
if (!responderIdentity) {
|
|
11869
|
+
autoPublishResult.error = `responder identity ${shr.body.instance_id} not found; skipping auto-publish`;
|
|
11870
|
+
auditLog.append(
|
|
11871
|
+
"l4",
|
|
11872
|
+
"handshake_auto_publish",
|
|
11873
|
+
shr.body.instance_id,
|
|
11874
|
+
{ error: autoPublishResult.error },
|
|
11875
|
+
"failure"
|
|
11876
|
+
);
|
|
11877
|
+
} else {
|
|
11878
|
+
const payloadBytes = new TextEncoder().encode(
|
|
11879
|
+
JSON.stringify(attestationPayload)
|
|
11880
|
+
);
|
|
11881
|
+
const sigBytes = sign(
|
|
11882
|
+
payloadBytes,
|
|
11883
|
+
responderIdentity.encrypted_private_key,
|
|
11884
|
+
identityEncKey
|
|
11885
|
+
);
|
|
11886
|
+
const signatureB64 = toBase64url(sigBytes);
|
|
11887
|
+
const resp = await fetch(
|
|
11888
|
+
`${verascoreUrl.replace(/\/$/, "")}/api/publish`,
|
|
11889
|
+
{
|
|
11890
|
+
method: "POST",
|
|
11891
|
+
headers: { "Content-Type": "application/json" },
|
|
11892
|
+
body: JSON.stringify({
|
|
11893
|
+
agentId: shr.body.instance_id,
|
|
11894
|
+
publicKey: shr.signed_by,
|
|
11895
|
+
signature: signatureB64,
|
|
11896
|
+
type: "handshake",
|
|
11897
|
+
data: attestationPayload
|
|
11898
|
+
})
|
|
11899
|
+
}
|
|
11900
|
+
);
|
|
11901
|
+
autoPublishResult.ok = resp.ok;
|
|
11902
|
+
autoPublishResult.status = resp.status;
|
|
11903
|
+
auditLog.append(
|
|
11904
|
+
"l4",
|
|
11905
|
+
"handshake_auto_publish",
|
|
11906
|
+
shr.body.instance_id,
|
|
11907
|
+
{
|
|
11908
|
+
verascore_url: verascoreUrl,
|
|
11909
|
+
status: resp.status,
|
|
11910
|
+
ok: resp.ok
|
|
11911
|
+
},
|
|
11912
|
+
resp.ok ? "success" : "failure"
|
|
11913
|
+
);
|
|
11914
|
+
}
|
|
11915
|
+
}
|
|
11916
|
+
} catch (err) {
|
|
11917
|
+
autoPublishResult.error = err instanceof Error ? err.message : String(err);
|
|
11918
|
+
auditLog.append(
|
|
11919
|
+
"l4",
|
|
11920
|
+
"handshake_auto_publish",
|
|
11921
|
+
shr.body.instance_id,
|
|
11922
|
+
{ verascore_url: verascoreUrl, error: autoPublishResult.error },
|
|
11923
|
+
"failure"
|
|
11924
|
+
);
|
|
11925
|
+
}
|
|
11926
|
+
}
|
|
10972
11927
|
return toolResult({
|
|
10973
11928
|
session_id: result.session.session_id,
|
|
10974
11929
|
response: result.response,
|
|
10975
11930
|
instructions: "Send the 'response' object back to the initiator. When you receive their completion, pass it to sanctuary/handshake_status with this session_id.",
|
|
11931
|
+
auto_publish: autoPublishResult,
|
|
10976
11932
|
// SEC-ADD-03: Tag response — contains SHR data that will be sent to counterparty
|
|
10977
11933
|
_content_trust: "external"
|
|
10978
11934
|
});
|
|
10979
11935
|
}
|
|
10980
11936
|
},
|
|
10981
11937
|
{
|
|
10982
|
-
name: "
|
|
11938
|
+
name: "handshake_complete",
|
|
10983
11939
|
description: "Complete a sovereignty handshake (initiator side). Verifies the responder's SHR and nonce signature, signs their nonce, and produces the final result.",
|
|
10984
11940
|
inputSchema: {
|
|
10985
11941
|
type: "object",
|
|
@@ -11034,7 +11990,7 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
|
|
|
11034
11990
|
}
|
|
11035
11991
|
},
|
|
11036
11992
|
{
|
|
11037
|
-
name: "
|
|
11993
|
+
name: "handshake_status",
|
|
11038
11994
|
description: "Check the status of a handshake session, or verify a completion message (responder side).",
|
|
11039
11995
|
inputSchema: {
|
|
11040
11996
|
type: "object",
|
|
@@ -11084,7 +12040,7 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
|
|
|
11084
12040
|
},
|
|
11085
12041
|
// ─── Streamlined Exchange ─────────────────────────────────────────
|
|
11086
12042
|
{
|
|
11087
|
-
name: "
|
|
12043
|
+
name: "handshake_exchange",
|
|
11088
12044
|
description: "One-shot sovereignty exchange. Accepts a counterparty's signed SHR, verifies it, generates our SHR, and produces a signed attestation artifact \u2014 all in a single call. Returns a shareable attestation with human-readable summary. Use this instead of the 4-step handshake protocol when you want a quick, portable sovereignty verification (e.g., for social posting or async exchanges).",
|
|
11089
12045
|
inputSchema: {
|
|
11090
12046
|
type: "object",
|
|
@@ -11151,7 +12107,7 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
|
|
|
11151
12107
|
}
|
|
11152
12108
|
},
|
|
11153
12109
|
{
|
|
11154
|
-
name: "
|
|
12110
|
+
name: "handshake_verify_attestation",
|
|
11155
12111
|
description: "Verify a signed attestation artifact from another agent. Checks the Ed25519 signature, temporal validity, and structural integrity.",
|
|
11156
12112
|
inputSchema: {
|
|
11157
12113
|
type: "object",
|
|
@@ -11360,7 +12316,7 @@ function createFederationTools(auditLog, handshakeResults) {
|
|
|
11360
12316
|
const tools = [
|
|
11361
12317
|
// ─── Peer Management ──────────────────────────────────────────────
|
|
11362
12318
|
{
|
|
11363
|
-
name: "
|
|
12319
|
+
name: "federation_peers",
|
|
11364
12320
|
description: "List known federation peers, register a peer from a completed handshake, or remove a peer. Every peer MUST enter through a verified handshake \u2014 no self-registration allowed.",
|
|
11365
12321
|
inputSchema: {
|
|
11366
12322
|
type: "object",
|
|
@@ -11463,7 +12419,7 @@ function createFederationTools(auditLog, handshakeResults) {
|
|
|
11463
12419
|
},
|
|
11464
12420
|
// ─── Trust Evaluation ─────────────────────────────────────────────
|
|
11465
12421
|
{
|
|
11466
|
-
name: "
|
|
12422
|
+
name: "federation_trust_evaluate",
|
|
11467
12423
|
description: "Evaluate the trust level of a federation peer. Considers handshake status, sovereignty tier, reputation score, and mutual attestation history. Returns a composite trust assessment.",
|
|
11468
12424
|
inputSchema: {
|
|
11469
12425
|
type: "object",
|
|
@@ -11498,7 +12454,7 @@ function createFederationTools(auditLog, handshakeResults) {
|
|
|
11498
12454
|
},
|
|
11499
12455
|
// ─── Federation Status ────────────────────────────────────────────
|
|
11500
12456
|
{
|
|
11501
|
-
name: "
|
|
12457
|
+
name: "federation_status",
|
|
11502
12458
|
description: "Overview of federation state: total peers, active connections, trust distribution, and readiness for cross-instance operations.",
|
|
11503
12459
|
inputSchema: {
|
|
11504
12460
|
type: "object",
|
|
@@ -11709,7 +12665,7 @@ function createBridgeTools(storage, masterKey, identityManager, auditLog, handsh
|
|
|
11709
12665
|
const tools = [
|
|
11710
12666
|
// ─── bridge_commit ─────────────────────────────────────────────────
|
|
11711
12667
|
{
|
|
11712
|
-
name: "
|
|
12668
|
+
name: "bridge_commit",
|
|
11713
12669
|
description: "Create a cryptographic commitment binding a Concordia negotiation outcome to Sanctuary's L3 proof layer. The commitment includes a SHA-256 hash of the canonical outcome (hiding + binding), an Ed25519 signature by the committer's identity, and an optional Pedersen commitment on the round count for zero-knowledge range proofs. This is the Sanctuary side of the Concordia bridge \u2014 call this when a Concordia `accept` fires.",
|
|
11714
12670
|
inputSchema: {
|
|
11715
12671
|
type: "object",
|
|
@@ -11811,7 +12767,7 @@ function createBridgeTools(storage, masterKey, identityManager, auditLog, handsh
|
|
|
11811
12767
|
},
|
|
11812
12768
|
// ─── bridge_verify ───────────────────────────────────────────────────
|
|
11813
12769
|
{
|
|
11814
|
-
name: "
|
|
12770
|
+
name: "bridge_verify",
|
|
11815
12771
|
description: "Verify a bridge commitment against a revealed Concordia negotiation outcome. Checks SHA-256 commitment validity, Ed25519 signature, session ID match, terms hash integrity, and Pedersen commitment (if present). Use this to confirm that a counterparty's claimed negotiation outcome matches what was cryptographically committed.",
|
|
11816
12772
|
inputSchema: {
|
|
11817
12773
|
type: "object",
|
|
@@ -11867,7 +12823,7 @@ function createBridgeTools(storage, masterKey, identityManager, auditLog, handsh
|
|
|
11867
12823
|
},
|
|
11868
12824
|
// ─── bridge_attest ───────────────────────────────────────────────────
|
|
11869
12825
|
{
|
|
11870
|
-
name: "
|
|
12826
|
+
name: "bridge_attest",
|
|
11871
12827
|
description: "Record a Concordia negotiation as a Sanctuary L4 reputation attestation, linked to a bridge commitment. This completes the bridge: the commitment (L3) proves the terms were agreed, and the attestation (L4) feeds the sovereignty-weighted reputation score. The attestation is automatically tagged with the counterparty's sovereignty tier from any completed handshake.",
|
|
11872
12828
|
inputSchema: {
|
|
11873
12829
|
type: "object",
|
|
@@ -12431,7 +13387,7 @@ function generateGaps(env, l1, l2, l3, l4) {
|
|
|
12431
13387
|
title: "No context gating for outbound inference calls",
|
|
12432
13388
|
description: "Your agent sends its full context \u2014 conversation history, memory, preferences, internal reasoning \u2014 to remote LLM providers on every inference call. There is no mechanism to filter what leaves the sovereignty boundary. The provider sees everything the agent knows.",
|
|
12433
13389
|
openclaw_relevance: env.openclaw_detected ? "OpenClaw sends full agent context (including MEMORY.md, tool results, and conversation history) to the configured LLM provider with every API call. There is no built-in context filtering." : null,
|
|
12434
|
-
sanctuary_solution: "Sanctuary's context gating (sanctuary/context_gate_set_policy +
|
|
13390
|
+
sanctuary_solution: "Sanctuary's context gating (sanctuary/context_gate_set_policy + context_gate_filter) lets you define per-provider policies that control exactly what context flows outbound. Redact secrets, hash identifiers, and send only minimum-necessary context for each call.",
|
|
12435
13391
|
incident_class: INCIDENT_CONTEXT_LEAKAGE
|
|
12436
13392
|
});
|
|
12437
13393
|
}
|
|
@@ -12443,7 +13399,7 @@ function generateGaps(env, l1, l2, l3, l4) {
|
|
|
12443
13399
|
title: "No audit trail",
|
|
12444
13400
|
description: "No audit trail exists for tool call history. There is no record of what operations were executed, when, or by whom.",
|
|
12445
13401
|
openclaw_relevance: null,
|
|
12446
|
-
sanctuary_solution: "Sanctuary maintains an encrypted audit log of all operations, queryable via
|
|
13402
|
+
sanctuary_solution: "Sanctuary maintains an encrypted audit log of all operations, queryable via monitor_audit_log.",
|
|
12447
13403
|
incident_class: INCIDENT_CLAUDE_CODE_LEAK
|
|
12448
13404
|
});
|
|
12449
13405
|
}
|
|
@@ -12478,7 +13434,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
|
|
|
12478
13434
|
recs.push({
|
|
12479
13435
|
priority: 1,
|
|
12480
13436
|
action: "Create a cryptographic identity \u2014 your agent's foundation for all sovereignty operations",
|
|
12481
|
-
tool: "
|
|
13437
|
+
tool: "identity_create",
|
|
12482
13438
|
effort: "immediate",
|
|
12483
13439
|
impact: "critical"
|
|
12484
13440
|
});
|
|
@@ -12487,7 +13443,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
|
|
|
12487
13443
|
recs.push({
|
|
12488
13444
|
priority: 2,
|
|
12489
13445
|
action: "Migrate plaintext agent state to Sanctuary's encrypted store",
|
|
12490
|
-
tool: "
|
|
13446
|
+
tool: "state_write",
|
|
12491
13447
|
effort: "minutes",
|
|
12492
13448
|
impact: "critical"
|
|
12493
13449
|
});
|
|
@@ -12495,7 +13451,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
|
|
|
12495
13451
|
recs.push({
|
|
12496
13452
|
priority: 3,
|
|
12497
13453
|
action: "Generate a Sovereignty Health Report to present to counterparties",
|
|
12498
|
-
tool: "
|
|
13454
|
+
tool: "shr_generate",
|
|
12499
13455
|
effort: "immediate",
|
|
12500
13456
|
impact: "high"
|
|
12501
13457
|
});
|
|
@@ -12503,7 +13459,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
|
|
|
12503
13459
|
recs.push({
|
|
12504
13460
|
priority: 4,
|
|
12505
13461
|
action: "Enable the three-tier Principal Policy gate for graduated approval",
|
|
12506
|
-
tool: "
|
|
13462
|
+
tool: "principal_policy_view",
|
|
12507
13463
|
effort: "minutes",
|
|
12508
13464
|
impact: "high"
|
|
12509
13465
|
});
|
|
@@ -12512,7 +13468,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
|
|
|
12512
13468
|
recs.push({
|
|
12513
13469
|
priority: 5,
|
|
12514
13470
|
action: "Configure context gating to control what flows to LLM providers",
|
|
12515
|
-
tool: "
|
|
13471
|
+
tool: "context_gate_set_policy",
|
|
12516
13472
|
effort: "minutes",
|
|
12517
13473
|
impact: "high"
|
|
12518
13474
|
});
|
|
@@ -12521,7 +13477,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
|
|
|
12521
13477
|
recs.push({
|
|
12522
13478
|
priority: 6,
|
|
12523
13479
|
action: "Start recording reputation attestations from completed interactions",
|
|
12524
|
-
tool: "
|
|
13480
|
+
tool: "reputation_record",
|
|
12525
13481
|
effort: "minutes",
|
|
12526
13482
|
impact: "medium"
|
|
12527
13483
|
});
|
|
@@ -12530,7 +13486,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
|
|
|
12530
13486
|
recs.push({
|
|
12531
13487
|
priority: 7,
|
|
12532
13488
|
action: "Configure selective disclosure policies for data sharing",
|
|
12533
|
-
tool: "
|
|
13489
|
+
tool: "disclosure_set_policy",
|
|
12534
13490
|
effort: "hours",
|
|
12535
13491
|
impact: "medium"
|
|
12536
13492
|
});
|
|
@@ -12670,7 +13626,7 @@ function wordWrap(text, maxWidth) {
|
|
|
12670
13626
|
function createAuditTools(config) {
|
|
12671
13627
|
const tools = [
|
|
12672
13628
|
{
|
|
12673
|
-
name: "
|
|
13629
|
+
name: "sovereignty_audit",
|
|
12674
13630
|
description: "Audit your agent's sovereignty posture. Inspects the local environment for encryption, identity, approval gates, selective disclosure, and reputation \u2014 including OpenClaw-specific configurations. Returns a scored gap analysis with prioritized recommendations.",
|
|
12675
13631
|
inputSchema: {
|
|
12676
13632
|
type: "object",
|
|
@@ -12680,16 +13636,312 @@ function createAuditTools(config) {
|
|
|
12680
13636
|
description: "If true (default), also scans for OpenClaw config, .env files, and memory files. Set to false for a Sanctuary-only assessment."
|
|
12681
13637
|
}
|
|
12682
13638
|
}
|
|
12683
|
-
},
|
|
12684
|
-
handler: async (args) => {
|
|
12685
|
-
const deepScan = args.deep_scan !== false;
|
|
12686
|
-
const env = await detectEnvironment(config, deepScan);
|
|
12687
|
-
const result = analyzeSovereignty(env, config);
|
|
12688
|
-
const report = formatAuditReport(result);
|
|
13639
|
+
},
|
|
13640
|
+
handler: async (args) => {
|
|
13641
|
+
const deepScan = args.deep_scan !== false;
|
|
13642
|
+
const env = await detectEnvironment(config, deepScan);
|
|
13643
|
+
const result = analyzeSovereignty(env, config);
|
|
13644
|
+
const report = formatAuditReport(result);
|
|
13645
|
+
return {
|
|
13646
|
+
content: [
|
|
13647
|
+
{ type: "text", text: report },
|
|
13648
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
13649
|
+
]
|
|
13650
|
+
};
|
|
13651
|
+
}
|
|
13652
|
+
}
|
|
13653
|
+
];
|
|
13654
|
+
return { tools };
|
|
13655
|
+
}
|
|
13656
|
+
|
|
13657
|
+
// src/audit/siem-formatter.ts
|
|
13658
|
+
function parseGateDecision(details) {
|
|
13659
|
+
if (!details || typeof details.gate_decision !== "string") {
|
|
13660
|
+
return "auto-allow";
|
|
13661
|
+
}
|
|
13662
|
+
const decision = details.gate_decision.toLowerCase();
|
|
13663
|
+
if (decision === "approve" || decision === "deny") {
|
|
13664
|
+
return decision;
|
|
13665
|
+
}
|
|
13666
|
+
return "auto-allow";
|
|
13667
|
+
}
|
|
13668
|
+
function parseTier(details) {
|
|
13669
|
+
if (!details || typeof details.tier !== "number") {
|
|
13670
|
+
return 3;
|
|
13671
|
+
}
|
|
13672
|
+
return Math.max(1, Math.min(3, details.tier));
|
|
13673
|
+
}
|
|
13674
|
+
function parseSessionId(details) {
|
|
13675
|
+
if (!details || typeof details.session_id !== "string") {
|
|
13676
|
+
return "unknown";
|
|
13677
|
+
}
|
|
13678
|
+
return details.session_id;
|
|
13679
|
+
}
|
|
13680
|
+
function parseAgentDid(details) {
|
|
13681
|
+
if (!details || typeof details.agent_did !== "string") {
|
|
13682
|
+
return "unknown";
|
|
13683
|
+
}
|
|
13684
|
+
return details.agent_did;
|
|
13685
|
+
}
|
|
13686
|
+
function gateToCEFSeverity(decision, tier) {
|
|
13687
|
+
if (decision === "deny") {
|
|
13688
|
+
return 8;
|
|
13689
|
+
}
|
|
13690
|
+
if (decision === "approve") {
|
|
13691
|
+
if (tier === 1) return 5;
|
|
13692
|
+
if (tier === 2) return 3;
|
|
13693
|
+
}
|
|
13694
|
+
return 1;
|
|
13695
|
+
}
|
|
13696
|
+
function formatAsCEF(entry, options) {
|
|
13697
|
+
const version = "0";
|
|
13698
|
+
const vendor = "Sanctuary";
|
|
13699
|
+
const product = "MCP-Server";
|
|
13700
|
+
const productVersion = "0.7.0";
|
|
13701
|
+
const decision = parseGateDecision(entry.details);
|
|
13702
|
+
const tier = parseTier(entry.details);
|
|
13703
|
+
const sessionId = parseSessionId(entry.details);
|
|
13704
|
+
const agentDid = parseAgentDid(entry.details);
|
|
13705
|
+
const severity = gateToCEFSeverity(decision, tier);
|
|
13706
|
+
const signatureId = entry.operation.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
13707
|
+
const description = `Sanctuary ${entry.operation}`;
|
|
13708
|
+
const extensions = [
|
|
13709
|
+
`src=${agentDid}`,
|
|
13710
|
+
`act=${entry.operation}`,
|
|
13711
|
+
`outcome=${decision}`,
|
|
13712
|
+
`tier=${tier}`,
|
|
13713
|
+
`cs1=${sessionId}`,
|
|
13714
|
+
`cs1Label=SessionId`,
|
|
13715
|
+
`rt=${new Date(entry.timestamp).getTime()}`,
|
|
13716
|
+
`layer=${entry.layer}`,
|
|
13717
|
+
`result=${entry.result}`
|
|
13718
|
+
];
|
|
13719
|
+
return `CEF:${version}|${vendor}|${product}|${productVersion}|${signatureId}|${description}|${severity}|${extensions.join(" ")}`;
|
|
13720
|
+
}
|
|
13721
|
+
function gateToOCSFStatus(decision, result) {
|
|
13722
|
+
return decision === "deny" || result === "failure" ? 2 : 1;
|
|
13723
|
+
}
|
|
13724
|
+
function gateToCOCSFSeverity(decision, tier) {
|
|
13725
|
+
if (decision === "deny") {
|
|
13726
|
+
return 4;
|
|
13727
|
+
}
|
|
13728
|
+
if (decision === "approve") {
|
|
13729
|
+
if (tier === 1) return 3;
|
|
13730
|
+
if (tier === 2) return 2;
|
|
13731
|
+
}
|
|
13732
|
+
return 1;
|
|
13733
|
+
}
|
|
13734
|
+
function gateToOCSFDisposition(decision) {
|
|
13735
|
+
return decision === "deny" ? 2 : 1;
|
|
13736
|
+
}
|
|
13737
|
+
function formatAsOCSF(entry) {
|
|
13738
|
+
const decision = parseGateDecision(entry.details);
|
|
13739
|
+
const tier = parseTier(entry.details);
|
|
13740
|
+
const agentDid = parseAgentDid(entry.details);
|
|
13741
|
+
const timestamp = new Date(entry.timestamp).getTime();
|
|
13742
|
+
const statusId = gateToOCSFStatus(decision, entry.result);
|
|
13743
|
+
const severityId = gateToCOCSFSeverity(decision, tier);
|
|
13744
|
+
const dispositionId = gateToOCSFDisposition(decision);
|
|
13745
|
+
return {
|
|
13746
|
+
class_uid: 3001,
|
|
13747
|
+
class_name: "API Activity",
|
|
13748
|
+
category_uid: 3,
|
|
13749
|
+
category_name: "Application Activity",
|
|
13750
|
+
severity_id: severityId,
|
|
13751
|
+
time: timestamp,
|
|
13752
|
+
activity_id: 1,
|
|
13753
|
+
activity_name: "API Call",
|
|
13754
|
+
actor: {
|
|
13755
|
+
user: {
|
|
13756
|
+
uid: agentDid
|
|
13757
|
+
}
|
|
13758
|
+
},
|
|
13759
|
+
api: {
|
|
13760
|
+
operation: entry.operation,
|
|
13761
|
+
service: {
|
|
13762
|
+
name: "sanctuary-mcp"
|
|
13763
|
+
}
|
|
13764
|
+
},
|
|
13765
|
+
status_id: statusId,
|
|
13766
|
+
disposition_id: dispositionId,
|
|
13767
|
+
metadata: {
|
|
13768
|
+
version: "1.3.0",
|
|
13769
|
+
product: {
|
|
13770
|
+
name: "Sanctuary Framework",
|
|
13771
|
+
vendor_name: "Erik Newton",
|
|
13772
|
+
version: "0.7.0"
|
|
13773
|
+
}
|
|
13774
|
+
}
|
|
13775
|
+
};
|
|
13776
|
+
}
|
|
13777
|
+
|
|
13778
|
+
// src/audit/siem-tools.ts
|
|
13779
|
+
function createSIEMTools(auditLog) {
|
|
13780
|
+
const tools = [
|
|
13781
|
+
{
|
|
13782
|
+
name: "audit_export_siem",
|
|
13783
|
+
description: "Export audit log events in SIEM-standard formats (CEF or OCSF) for ingestion into Splunk, Datadog, QRadar, and other security information and event management (SIEM) platforms. Encrypted audit entries are decrypted and formatted according to your chosen standard. Tier 2 \u2014 may contain sensitive operation metadata.",
|
|
13784
|
+
inputSchema: {
|
|
13785
|
+
type: "object",
|
|
13786
|
+
properties: {
|
|
13787
|
+
format: {
|
|
13788
|
+
type: "string",
|
|
13789
|
+
enum: ["cef", "ocsf"],
|
|
13790
|
+
description: 'Output format: "cef" (Common Event Format, newline-delimited) or "ocsf" (Open Cybersecurity Schema Framework, JSON array)'
|
|
13791
|
+
},
|
|
13792
|
+
since: {
|
|
13793
|
+
type: "string",
|
|
13794
|
+
description: "Optional ISO 8601 timestamp. Export only events on or after this time. Defaults to 24 hours ago."
|
|
13795
|
+
},
|
|
13796
|
+
until: {
|
|
13797
|
+
type: "string",
|
|
13798
|
+
description: "Optional ISO 8601 timestamp. Export only events before this time. Defaults to now."
|
|
13799
|
+
},
|
|
13800
|
+
limit: {
|
|
13801
|
+
type: "number",
|
|
13802
|
+
description: "Maximum number of events to export (default 100, max 1000). Set to 1000 for bulk exports to SIEMs."
|
|
13803
|
+
},
|
|
13804
|
+
filter_tool: {
|
|
13805
|
+
type: "string",
|
|
13806
|
+
description: 'Optional. Export only events from this tool name (e.g., "sovereignty_audit", "state_set"). Case-insensitive substring matching.'
|
|
13807
|
+
},
|
|
13808
|
+
filter_decision: {
|
|
13809
|
+
type: "string",
|
|
13810
|
+
enum: ["approve", "deny", "auto-allow"],
|
|
13811
|
+
description: 'Optional. Export only events with this gate decision: "approve" (manual approval), "deny" (blocked), or "auto-allow" (Tier 3 auto-allowed).'
|
|
13812
|
+
},
|
|
13813
|
+
filter_layer: {
|
|
13814
|
+
type: "string",
|
|
13815
|
+
enum: ["l1", "l2", "l3", "l4"],
|
|
13816
|
+
description: "Optional. Export only events from this sovereignty layer (L1=Cognitive, L2=Operational, L3=Disclosure, L4=Reputation)."
|
|
13817
|
+
},
|
|
13818
|
+
filter_result: {
|
|
13819
|
+
type: "string",
|
|
13820
|
+
enum: ["success", "failure"],
|
|
13821
|
+
description: 'Optional. Export only events with this result: "success" or "failure".'
|
|
13822
|
+
}
|
|
13823
|
+
},
|
|
13824
|
+
required: ["format"]
|
|
13825
|
+
},
|
|
13826
|
+
handler: async (args) => {
|
|
13827
|
+
const format = String(args.format || "").toLowerCase();
|
|
13828
|
+
if (format !== "cef" && format !== "ocsf") {
|
|
13829
|
+
return {
|
|
13830
|
+
content: [
|
|
13831
|
+
{
|
|
13832
|
+
type: "text",
|
|
13833
|
+
text: JSON.stringify({
|
|
13834
|
+
error: "Invalid format. Must be 'cef' or 'ocsf'."
|
|
13835
|
+
})
|
|
13836
|
+
}
|
|
13837
|
+
]
|
|
13838
|
+
};
|
|
13839
|
+
}
|
|
13840
|
+
let since;
|
|
13841
|
+
if (args.since) {
|
|
13842
|
+
since = String(args.since);
|
|
13843
|
+
const sinceDate = new Date(since);
|
|
13844
|
+
if (isNaN(sinceDate.getTime())) {
|
|
13845
|
+
return {
|
|
13846
|
+
content: [
|
|
13847
|
+
{
|
|
13848
|
+
type: "text",
|
|
13849
|
+
text: JSON.stringify({
|
|
13850
|
+
error: `Invalid 'since' timestamp: ${since}. Must be ISO 8601.`
|
|
13851
|
+
})
|
|
13852
|
+
}
|
|
13853
|
+
]
|
|
13854
|
+
};
|
|
13855
|
+
}
|
|
13856
|
+
} else {
|
|
13857
|
+
const now = /* @__PURE__ */ new Date();
|
|
13858
|
+
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1e3);
|
|
13859
|
+
since = oneDayAgo.toISOString();
|
|
13860
|
+
}
|
|
13861
|
+
let until;
|
|
13862
|
+
if (args.until) {
|
|
13863
|
+
until = String(args.until);
|
|
13864
|
+
const untilDate = new Date(until);
|
|
13865
|
+
if (isNaN(untilDate.getTime())) {
|
|
13866
|
+
return {
|
|
13867
|
+
content: [
|
|
13868
|
+
{
|
|
13869
|
+
type: "text",
|
|
13870
|
+
text: JSON.stringify({
|
|
13871
|
+
error: `Invalid 'until' timestamp: ${until}. Must be ISO 8601.`
|
|
13872
|
+
})
|
|
13873
|
+
}
|
|
13874
|
+
]
|
|
13875
|
+
};
|
|
13876
|
+
}
|
|
13877
|
+
}
|
|
13878
|
+
let limit = 100;
|
|
13879
|
+
if (typeof args.limit === "number") {
|
|
13880
|
+
limit = Math.max(1, Math.min(1e3, args.limit));
|
|
13881
|
+
}
|
|
13882
|
+
const filterTool = args.filter_tool ? String(args.filter_tool).toLowerCase() : void 0;
|
|
13883
|
+
const filterDecision = args.filter_decision ? String(args.filter_decision).toLowerCase() : void 0;
|
|
13884
|
+
const filterLayer = args.filter_layer ? String(args.filter_layer).toLowerCase() : void 0;
|
|
13885
|
+
const filterResult = args.filter_result ? String(args.filter_result).toLowerCase() : void 0;
|
|
13886
|
+
const result = await auditLog.query({
|
|
13887
|
+
since,
|
|
13888
|
+
layer: filterLayer,
|
|
13889
|
+
operation_type: void 0,
|
|
13890
|
+
// Will filter after
|
|
13891
|
+
limit
|
|
13892
|
+
});
|
|
13893
|
+
let filtered = result.entries;
|
|
13894
|
+
if (filterTool) {
|
|
13895
|
+
filtered = filtered.filter(
|
|
13896
|
+
(e) => e.operation.toLowerCase().includes(filterTool)
|
|
13897
|
+
);
|
|
13898
|
+
}
|
|
13899
|
+
if (filterDecision) {
|
|
13900
|
+
filtered = filtered.filter((e) => {
|
|
13901
|
+
const decision = String(e.details?.gate_decision || "auto-allow").toLowerCase();
|
|
13902
|
+
return decision === filterDecision;
|
|
13903
|
+
});
|
|
13904
|
+
}
|
|
13905
|
+
if (filterResult) {
|
|
13906
|
+
filtered = filtered.filter((e) => e.result === filterResult);
|
|
13907
|
+
}
|
|
13908
|
+
if (until) {
|
|
13909
|
+
const untilDate = new Date(until);
|
|
13910
|
+
filtered = filtered.filter((e) => new Date(e.timestamp) < untilDate);
|
|
13911
|
+
}
|
|
13912
|
+
let output;
|
|
13913
|
+
if (format === "cef") {
|
|
13914
|
+
const cefLines = filtered.map((entry) => formatAsCEF(entry));
|
|
13915
|
+
output = cefLines.join("\n");
|
|
13916
|
+
} else {
|
|
13917
|
+
const ocsfObjects = filtered.map((entry) => formatAsOCSF(entry));
|
|
13918
|
+
output = JSON.stringify(ocsfObjects, null, 2);
|
|
13919
|
+
}
|
|
12689
13920
|
return {
|
|
12690
13921
|
content: [
|
|
12691
|
-
{
|
|
12692
|
-
|
|
13922
|
+
{
|
|
13923
|
+
type: "text",
|
|
13924
|
+
text: JSON.stringify({
|
|
13925
|
+
format,
|
|
13926
|
+
count: filtered.length,
|
|
13927
|
+
total_available: result.total,
|
|
13928
|
+
time_range: {
|
|
13929
|
+
since,
|
|
13930
|
+
until: until || (/* @__PURE__ */ new Date()).toISOString()
|
|
13931
|
+
},
|
|
13932
|
+
filters: {
|
|
13933
|
+
tool: filterTool,
|
|
13934
|
+
decision: filterDecision,
|
|
13935
|
+
layer: filterLayer,
|
|
13936
|
+
result: filterResult
|
|
13937
|
+
},
|
|
13938
|
+
note: format === "cef" ? `${filtered.length} CEF events (newline-delimited). Each line is a complete CEF event.` : `${filtered.length} OCSF objects in JSON array format.`
|
|
13939
|
+
})
|
|
13940
|
+
},
|
|
13941
|
+
{
|
|
13942
|
+
type: "text",
|
|
13943
|
+
text: output
|
|
13944
|
+
}
|
|
12693
13945
|
]
|
|
12694
13946
|
};
|
|
12695
13947
|
}
|
|
@@ -13694,13 +14946,17 @@ var ContextGateEnforcer = class {
|
|
|
13694
14946
|
* Check if a tool should be filtered based on bypass prefixes.
|
|
13695
14947
|
*
|
|
13696
14948
|
* SEC-033: Uses exact namespace component matching, not bare startsWith().
|
|
13697
|
-
* A prefix of "
|
|
13698
|
-
*
|
|
13699
|
-
*
|
|
13700
|
-
*
|
|
14949
|
+
* A prefix of "proxy/" matches "proxy/server/tool" but NOT "proxyevil/steal".
|
|
14950
|
+
* The prefix must match exactly up to its length, and the prefix must end
|
|
14951
|
+
* with "/" to enforce namespace boundaries (if it doesn't, we add one).
|
|
14952
|
+
*
|
|
14953
|
+
* Special sentinel: "*" bypasses ALL tools (used when all Sanctuary-internal
|
|
14954
|
+
* tools should skip context gating — the default). Only proxy/external tools
|
|
14955
|
+
* should be filtered in production.
|
|
13701
14956
|
*/
|
|
13702
14957
|
shouldFilter(toolName) {
|
|
13703
14958
|
for (const prefix of this.config.bypass_prefixes) {
|
|
14959
|
+
if (prefix === "*") return false;
|
|
13704
14960
|
const safePrefix = prefix.endsWith("/") ? prefix : prefix + "/";
|
|
13705
14961
|
if (toolName === safePrefix.slice(0, -1) || toolName.startsWith(safePrefix)) {
|
|
13706
14962
|
return false;
|
|
@@ -13797,8 +15053,8 @@ function createContextGateTools(storage, masterKey, auditLog) {
|
|
|
13797
15053
|
const enforcerConfig = {
|
|
13798
15054
|
enabled: false,
|
|
13799
15055
|
// Off by default; agents must explicitly enable it
|
|
13800
|
-
bypass_prefixes: ["
|
|
13801
|
-
// Skip internal tools
|
|
15056
|
+
bypass_prefixes: ["*"],
|
|
15057
|
+
// Skip all Sanctuary-internal tools; only proxy/ tools get filtered
|
|
13802
15058
|
log_only: false,
|
|
13803
15059
|
// Filter immediately
|
|
13804
15060
|
on_deny: "block"
|
|
@@ -13808,7 +15064,7 @@ function createContextGateTools(storage, masterKey, auditLog) {
|
|
|
13808
15064
|
const tools = [
|
|
13809
15065
|
// ── Set Policy ──────────────────────────────────────────────────
|
|
13810
15066
|
{
|
|
13811
|
-
name: "
|
|
15067
|
+
name: "context_gate_set_policy",
|
|
13812
15068
|
description: "Create a context-gating policy that controls what information flows to remote providers (LLM APIs, tool APIs, logging services). Each rule specifies a provider category and which context fields to allow, redact, hash, or flag for summarization. Redact rules take absolute priority \u2014 if a field is in both 'allow' and 'redact', it is redacted. Default action applies to any field not mentioned in any rule. Use this to prevent your full agent context from being sent to remote LLM providers during inference calls.",
|
|
13813
15069
|
inputSchema: {
|
|
13814
15070
|
type: "object",
|
|
@@ -13917,13 +15173,13 @@ function createContextGateTools(storage, masterKey, auditLog) {
|
|
|
13917
15173
|
rules: policy.rules,
|
|
13918
15174
|
default_action: policy.default_action,
|
|
13919
15175
|
created_at: policy.created_at,
|
|
13920
|
-
message: "Context-gating policy created. Use
|
|
15176
|
+
message: "Context-gating policy created. Use context_gate_filter to apply this policy before making outbound calls."
|
|
13921
15177
|
});
|
|
13922
15178
|
}
|
|
13923
15179
|
},
|
|
13924
15180
|
// ── Apply Template ───────────────────────────────────────────────
|
|
13925
15181
|
{
|
|
13926
|
-
name: "
|
|
15182
|
+
name: "context_gate_apply_template",
|
|
13927
15183
|
description: "Apply a starter context-gating template. Available templates: inference-minimal (strictest \u2014 only task and query pass through), inference-standard (balanced \u2014 adds tool results, summarizes history), logging-strict (redacts all content for telemetry services), tool-api-scoped (allows tool parameters, redacts agent state). Templates are starting points \u2014 customize after applying.",
|
|
13928
15184
|
inputSchema: {
|
|
13929
15185
|
type: "object",
|
|
@@ -13972,13 +15228,13 @@ function createContextGateTools(storage, masterKey, auditLog) {
|
|
|
13972
15228
|
rules: policy.rules,
|
|
13973
15229
|
default_action: policy.default_action,
|
|
13974
15230
|
created_at: policy.created_at,
|
|
13975
|
-
message: "Template applied. Use
|
|
15231
|
+
message: "Template applied. Use context_gate_filter with this policy_id to filter context before outbound calls. Customize rules with context_gate_set_policy if needed."
|
|
13976
15232
|
});
|
|
13977
15233
|
}
|
|
13978
15234
|
},
|
|
13979
15235
|
// ── Recommend Policy ────────────────────────────────────────────
|
|
13980
15236
|
{
|
|
13981
|
-
name: "
|
|
15237
|
+
name: "context_gate_recommend",
|
|
13982
15238
|
description: "Analyze a sample context object and recommend a context-gating policy based on field name heuristics. Classifies each field as allow, redact, hash, or summarize with confidence levels. Returns a ready-to-apply rule set. When in doubt, recommends redact (conservative). Review the recommendations before applying.",
|
|
13983
15239
|
inputSchema: {
|
|
13984
15240
|
type: "object",
|
|
@@ -14015,7 +15271,7 @@ function createContextGateTools(storage, masterKey, auditLog) {
|
|
|
14015
15271
|
});
|
|
14016
15272
|
return toolResult({
|
|
14017
15273
|
...recommendation,
|
|
14018
|
-
next_steps: "Review the classifications above. If they look correct, you can apply them directly with
|
|
15274
|
+
next_steps: "Review the classifications above. If they look correct, you can apply them directly with context_gate_set_policy using the recommended_rules. Or start with a template via context_gate_apply_template and customize from there.",
|
|
14019
15275
|
available_templates: listTemplateIds().map((id) => {
|
|
14020
15276
|
const t = TEMPLATES[id];
|
|
14021
15277
|
return { id, name: t.name, description: t.description };
|
|
@@ -14025,7 +15281,7 @@ function createContextGateTools(storage, masterKey, auditLog) {
|
|
|
14025
15281
|
},
|
|
14026
15282
|
// ── Filter Context ──────────────────────────────────────────────
|
|
14027
15283
|
{
|
|
14028
|
-
name: "
|
|
15284
|
+
name: "context_gate_filter",
|
|
14029
15285
|
description: "Filter agent context through a gating policy before sending to a remote provider. Returns per-field decisions (allow, redact, hash, summarize) and content hashes for the audit trail. Call this BEFORE making any outbound API call to ensure you are only sending the minimum necessary context. The filtered output tells you exactly what can be sent safely.",
|
|
14030
15286
|
inputSchema: {
|
|
14031
15287
|
type: "object",
|
|
@@ -14131,7 +15387,7 @@ function createContextGateTools(storage, masterKey, auditLog) {
|
|
|
14131
15387
|
},
|
|
14132
15388
|
// ── List Policies ───────────────────────────────────────────────
|
|
14133
15389
|
{
|
|
14134
|
-
name: "
|
|
15390
|
+
name: "context_gate_list_policies",
|
|
14135
15391
|
description: "List all configured context-gating policies. Returns policy IDs, names, rule summaries, and default actions.",
|
|
14136
15392
|
inputSchema: {
|
|
14137
15393
|
type: "object",
|
|
@@ -14154,13 +15410,13 @@ function createContextGateTools(storage, masterKey, auditLog) {
|
|
|
14154
15410
|
updated_at: p.updated_at
|
|
14155
15411
|
})),
|
|
14156
15412
|
count: policies.length,
|
|
14157
|
-
message: policies.length === 0 ? "No context-gating policies configured. Use
|
|
15413
|
+
message: policies.length === 0 ? "No context-gating policies configured. Use context_gate_set_policy to create one." : `${policies.length} context-gating ${policies.length === 1 ? "policy" : "policies"} configured.`
|
|
14158
15414
|
});
|
|
14159
15415
|
}
|
|
14160
15416
|
},
|
|
14161
15417
|
// ── Enforcer Status ─────────────────────────────────────────────────
|
|
14162
15418
|
{
|
|
14163
|
-
name: "
|
|
15419
|
+
name: "context_gate_enforcer_status",
|
|
14164
15420
|
description: "Get the status of the automatic context gate enforcer, including enabled/disabled state, log_only mode, active policy, and statistics. The enforcer automatically filters tool arguments when enabled. Use this to monitor what the enforcer has been filtering.",
|
|
14165
15421
|
inputSchema: {
|
|
14166
15422
|
type: "object",
|
|
@@ -14181,13 +15437,13 @@ function createContextGateTools(storage, masterKey, auditLog) {
|
|
|
14181
15437
|
return toolResult({
|
|
14182
15438
|
enforcer_status: status,
|
|
14183
15439
|
description: "The enforcer is " + (status.enabled ? "enabled" : "disabled") + ". " + (status.log_only ? "Currently in log_only mode \u2014 filtering is logged but not applied." : "Filtering is actively applied to tool arguments."),
|
|
14184
|
-
guidance: status.stats.calls_inspected > 0 ? `Over ${status.stats.calls_inspected} tool calls, ${status.stats.fields_redacted} sensitive fields were redacted. Use
|
|
15440
|
+
guidance: status.stats.calls_inspected > 0 ? `Over ${status.stats.calls_inspected} tool calls, ${status.stats.fields_redacted} sensitive fields were redacted. Use context_gate_enforcer_configure to adjust settings.` : "No tool calls have been inspected yet."
|
|
14185
15441
|
});
|
|
14186
15442
|
}
|
|
14187
15443
|
},
|
|
14188
15444
|
// ── Enforcer Configuration ──────────────────────────────────────────
|
|
14189
15445
|
{
|
|
14190
|
-
name: "
|
|
15446
|
+
name: "context_gate_enforcer_configure",
|
|
14191
15447
|
description: "Configure the automatic context gate enforcer. Control whether it filters tool arguments, toggle log_only mode for gradual rollout, set the active policy, and choose what to do when denied fields are encountered (block the request or redact the field). Use this to enable automatic context protection.",
|
|
14192
15448
|
inputSchema: {
|
|
14193
15449
|
type: "object",
|
|
@@ -14510,7 +15766,7 @@ function assessL2Hardening(storagePath) {
|
|
|
14510
15766
|
function createL2HardeningTools(storagePath, auditLog) {
|
|
14511
15767
|
return [
|
|
14512
15768
|
{
|
|
14513
|
-
name: "
|
|
15769
|
+
name: "l2_hardening_status",
|
|
14514
15770
|
description: "L2 Process Hardening Status \u2014 Verify software-based operational isolation. Reports memory protection, process isolation level, filesystem permissions, and overall hardening assessment. Read-only. Tier 3 \u2014 always allowed.",
|
|
14515
15771
|
inputSchema: {
|
|
14516
15772
|
type: "object",
|
|
@@ -14578,7 +15834,7 @@ function createL2HardeningTools(storagePath, auditLog) {
|
|
|
14578
15834
|
}
|
|
14579
15835
|
},
|
|
14580
15836
|
{
|
|
14581
|
-
name: "
|
|
15837
|
+
name: "l2_verify_isolation",
|
|
14582
15838
|
description: "Verify L2 process isolation at runtime. Checks whether the Sanctuary server is running in an isolated environment (container, VM, sandbox) and validates filesystem and memory protections. Reports isolation level and any issues. Read-only. Tier 3 \u2014 always allowed.",
|
|
14583
15839
|
inputSchema: {
|
|
14584
15840
|
type: "object",
|
|
@@ -14857,7 +16113,7 @@ function createSovereigntyProfileTools(profileStore, auditLog) {
|
|
|
14857
16113
|
const tools = [
|
|
14858
16114
|
// ── Get Profile ──────────────────────────────────────────────────
|
|
14859
16115
|
{
|
|
14860
|
-
name: "
|
|
16116
|
+
name: "sovereignty_profile_get",
|
|
14861
16117
|
description: "Get the current Sovereignty Profile \u2014 shows which Sanctuary features are active (audit logging, injection detection, context gating, approval gates, ZK proofs) and their configuration.",
|
|
14862
16118
|
inputSchema: {
|
|
14863
16119
|
type: "object",
|
|
@@ -14876,7 +16132,7 @@ function createSovereigntyProfileTools(profileStore, auditLog) {
|
|
|
14876
16132
|
},
|
|
14877
16133
|
// ── Update Profile ───────────────────────────────────────────────
|
|
14878
16134
|
{
|
|
14879
|
-
name: "
|
|
16135
|
+
name: "sovereignty_profile_update",
|
|
14880
16136
|
description: "Update the Sovereignty Profile feature toggles. This changes which Sanctuary protections are active. Requires human approval (Tier 1) because it modifies enforcement behavior. Pass only the features you want to change \u2014 unspecified features remain unchanged.",
|
|
14881
16137
|
inputSchema: {
|
|
14882
16138
|
type: "object",
|
|
@@ -14957,7 +16213,7 @@ function createSovereigntyProfileTools(profileStore, auditLog) {
|
|
|
14957
16213
|
},
|
|
14958
16214
|
// ── Generate System Prompt ───────────────────────────────────────
|
|
14959
16215
|
{
|
|
14960
|
-
name: "
|
|
16216
|
+
name: "sovereignty_profile_generate_prompt",
|
|
14961
16217
|
description: "Generate a system prompt snippet based on the active Sovereignty Profile. The snippet instructs an agent on which Sanctuary features are active and how to use them. Copy and paste this into your agent's system configuration.",
|
|
14962
16218
|
inputSchema: {
|
|
14963
16219
|
type: "object",
|
|
@@ -15351,6 +16607,7 @@ var ProxyRouter = class {
|
|
|
15351
16607
|
confidence: injectionResult.confidence,
|
|
15352
16608
|
latency_ms: Date.now() - start
|
|
15353
16609
|
}, "failure");
|
|
16610
|
+
this.notifyProxyCall(proxyName, serverName, "blocked", "injection_detected", tier);
|
|
15354
16611
|
return toolResult({
|
|
15355
16612
|
error: "Operation not permitted",
|
|
15356
16613
|
proxy: true
|
|
@@ -15381,6 +16638,7 @@ var ProxyRouter = class {
|
|
|
15381
16638
|
reason: govResult.reason,
|
|
15382
16639
|
latency_ms: Date.now() - start
|
|
15383
16640
|
}, "failure");
|
|
16641
|
+
this.notifyProxyCall(proxyName, serverName, "blocked", govResult.reason, tier);
|
|
15384
16642
|
return toolResult({
|
|
15385
16643
|
error: "Operation not permitted",
|
|
15386
16644
|
proxy: true,
|
|
@@ -15415,6 +16673,7 @@ var ProxyRouter = class {
|
|
|
15415
16673
|
decision: "allowed",
|
|
15416
16674
|
latency_ms: latencyMs
|
|
15417
16675
|
});
|
|
16676
|
+
this.notifyProxyCall(proxyName, serverName, "allowed", void 0, tier);
|
|
15418
16677
|
return this.normalizeResponse(result);
|
|
15419
16678
|
} catch (err) {
|
|
15420
16679
|
const latencyMs = Date.now() - start;
|
|
@@ -15434,6 +16693,7 @@ var ProxyRouter = class {
|
|
|
15434
16693
|
error: errorMessage,
|
|
15435
16694
|
latency_ms: latencyMs
|
|
15436
16695
|
}, "failure");
|
|
16696
|
+
this.notifyProxyCall(proxyName, serverName, "error", errorMessage, tier);
|
|
15437
16697
|
return {
|
|
15438
16698
|
content: [{
|
|
15439
16699
|
type: "text",
|
|
@@ -15448,6 +16708,24 @@ var ProxyRouter = class {
|
|
|
15448
16708
|
}
|
|
15449
16709
|
};
|
|
15450
16710
|
}
|
|
16711
|
+
/**
|
|
16712
|
+
* Notify the onProxyCall callback if configured.
|
|
16713
|
+
*/
|
|
16714
|
+
notifyProxyCall(tool, server, decision, reason, tier) {
|
|
16715
|
+
if (this.options.onProxyCall) {
|
|
16716
|
+
try {
|
|
16717
|
+
this.options.onProxyCall({
|
|
16718
|
+
tool,
|
|
16719
|
+
server,
|
|
16720
|
+
decision,
|
|
16721
|
+
reason,
|
|
16722
|
+
tier,
|
|
16723
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
16724
|
+
});
|
|
16725
|
+
} catch {
|
|
16726
|
+
}
|
|
16727
|
+
}
|
|
16728
|
+
}
|
|
15451
16729
|
/**
|
|
15452
16730
|
* Call an upstream tool with a timeout.
|
|
15453
16731
|
*/
|
|
@@ -15720,7 +16998,7 @@ function createGovernorTools(governor, auditLog) {
|
|
|
15720
16998
|
const tools = [
|
|
15721
16999
|
// ── Governor Status ─────────────────────────────────────────────
|
|
15722
17000
|
{
|
|
15723
|
-
name: "
|
|
17001
|
+
name: "governor_status",
|
|
15724
17002
|
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
17003
|
inputSchema: {
|
|
15726
17004
|
type: "object",
|
|
@@ -15760,7 +17038,7 @@ function createGovernorTools(governor, auditLog) {
|
|
|
15760
17038
|
},
|
|
15761
17039
|
// ── Governor Reset ──────────────────────────────────────────────
|
|
15762
17040
|
{
|
|
15763
|
-
name: "
|
|
17041
|
+
name: "governor_reset",
|
|
15764
17042
|
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
17043
|
inputSchema: {
|
|
15766
17044
|
type: "object",
|
|
@@ -15814,6 +17092,435 @@ function createGovernorTools(governor, auditLog) {
|
|
|
15814
17092
|
return { tools };
|
|
15815
17093
|
}
|
|
15816
17094
|
|
|
17095
|
+
// src/sanctuary-tools.ts
|
|
17096
|
+
init_identity();
|
|
17097
|
+
init_encoding();
|
|
17098
|
+
init_identity();
|
|
17099
|
+
function validateVerascoreUrl(urlStr, configuredUrl) {
|
|
17100
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
17101
|
+
"verascore.ai",
|
|
17102
|
+
"www.verascore.ai",
|
|
17103
|
+
"api.verascore.ai"
|
|
17104
|
+
]);
|
|
17105
|
+
try {
|
|
17106
|
+
allowed.add(new URL(configuredUrl).hostname);
|
|
17107
|
+
} catch {
|
|
17108
|
+
}
|
|
17109
|
+
try {
|
|
17110
|
+
const parsed = new URL(urlStr);
|
|
17111
|
+
if (parsed.protocol !== "https:") {
|
|
17112
|
+
return { ok: false, error: `Verascore URL must use HTTPS. Got: ${parsed.protocol}` };
|
|
17113
|
+
}
|
|
17114
|
+
if (!allowed.has(parsed.hostname)) {
|
|
17115
|
+
return {
|
|
17116
|
+
ok: false,
|
|
17117
|
+
error: `Verascore URL must point to a known Verascore host (${[...allowed].join(", ")}). Got: ${parsed.hostname}`
|
|
17118
|
+
};
|
|
17119
|
+
}
|
|
17120
|
+
return { ok: true };
|
|
17121
|
+
} catch {
|
|
17122
|
+
return { ok: false, error: `Invalid Verascore URL: ${urlStr}` };
|
|
17123
|
+
}
|
|
17124
|
+
}
|
|
17125
|
+
function createSanctuaryTools(opts) {
|
|
17126
|
+
const { config, identityManager, masterKey, auditLog, policy, keyProtection } = opts;
|
|
17127
|
+
const identityEncKey = derivePurposeKey(masterKey, "identity-encryption");
|
|
17128
|
+
const tools = [
|
|
17129
|
+
// ─── sanctuary_bootstrap ───────────────────────────────────────────
|
|
17130
|
+
{
|
|
17131
|
+
name: "sanctuary_bootstrap",
|
|
17132
|
+
description: "One-shot bootstrap for a new sovereign agent identity. Generates an Ed25519 keypair, stores the encrypted identity, constructs a Sovereignty Health Report (SHR), and publishes it to Verascore. Returns { did, profileUrl, tier } for the newly-minted agent.",
|
|
17133
|
+
inputSchema: {
|
|
17134
|
+
type: "object",
|
|
17135
|
+
properties: {
|
|
17136
|
+
label: {
|
|
17137
|
+
type: "string",
|
|
17138
|
+
description: "Human-readable label for the new identity (default: 'sovereign-agent')"
|
|
17139
|
+
},
|
|
17140
|
+
verascore_url: {
|
|
17141
|
+
type: "string",
|
|
17142
|
+
description: "Verascore base URL. Defaults to server config / SANCTUARY_VERASCORE_URL."
|
|
17143
|
+
},
|
|
17144
|
+
publish: {
|
|
17145
|
+
type: "boolean",
|
|
17146
|
+
description: "Whether to publish the SHR to Verascore. Defaults to true."
|
|
17147
|
+
}
|
|
17148
|
+
}
|
|
17149
|
+
},
|
|
17150
|
+
handler: async (args) => {
|
|
17151
|
+
const label = args.label || "sovereign-agent";
|
|
17152
|
+
const publish = args.publish === void 0 ? true : Boolean(args.publish);
|
|
17153
|
+
const verascoreUrl = args.verascore_url || config.verascore.url || "https://verascore.ai";
|
|
17154
|
+
const { publicIdentity, storedIdentity } = createIdentity(
|
|
17155
|
+
label,
|
|
17156
|
+
identityEncKey,
|
|
17157
|
+
keyProtection
|
|
17158
|
+
);
|
|
17159
|
+
await identityManager.save(storedIdentity);
|
|
17160
|
+
auditLog.append("l1", "sanctuary_bootstrap:identity_create", publicIdentity.identity_id, {
|
|
17161
|
+
label,
|
|
17162
|
+
did: publicIdentity.did
|
|
17163
|
+
});
|
|
17164
|
+
const shr = generateSHR(publicIdentity.identity_id, {
|
|
17165
|
+
config,
|
|
17166
|
+
identityManager,
|
|
17167
|
+
masterKey
|
|
17168
|
+
});
|
|
17169
|
+
if (typeof shr === "string") {
|
|
17170
|
+
return toolResult({
|
|
17171
|
+
error: `Identity created but SHR generation failed: ${shr}`,
|
|
17172
|
+
did: publicIdentity.did,
|
|
17173
|
+
identity_id: publicIdentity.identity_id
|
|
17174
|
+
});
|
|
17175
|
+
}
|
|
17176
|
+
const agentSlug = publicIdentity.did.replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase();
|
|
17177
|
+
const profileUrl = `${verascoreUrl.replace(/\/$/, "")}/agent/${publicIdentity.did}`;
|
|
17178
|
+
if (!publish || !config.verascore.auto_publish_to_verascore) {
|
|
17179
|
+
auditLog.append("l4", "sanctuary_bootstrap", publicIdentity.identity_id, {
|
|
17180
|
+
did: publicIdentity.did,
|
|
17181
|
+
published: false
|
|
17182
|
+
});
|
|
17183
|
+
return toolResult({
|
|
17184
|
+
did: publicIdentity.did,
|
|
17185
|
+
identity_id: publicIdentity.identity_id,
|
|
17186
|
+
profileUrl,
|
|
17187
|
+
tier: "self-attested",
|
|
17188
|
+
published: false
|
|
17189
|
+
});
|
|
17190
|
+
}
|
|
17191
|
+
const urlCheck = validateVerascoreUrl(verascoreUrl, config.verascore.url);
|
|
17192
|
+
if (!urlCheck.ok) {
|
|
17193
|
+
return toolResult({
|
|
17194
|
+
error: urlCheck.error,
|
|
17195
|
+
did: publicIdentity.did,
|
|
17196
|
+
identity_id: publicIdentity.identity_id
|
|
17197
|
+
});
|
|
17198
|
+
}
|
|
17199
|
+
const publishData = {
|
|
17200
|
+
sovereigntyLayers: shr.body.layers,
|
|
17201
|
+
capabilities: shr.body.capabilities,
|
|
17202
|
+
degradations: shr.body.degradations,
|
|
17203
|
+
did: publicIdentity.did,
|
|
17204
|
+
label
|
|
17205
|
+
};
|
|
17206
|
+
const payloadBytes = new TextEncoder().encode(JSON.stringify(publishData));
|
|
17207
|
+
let signatureB64;
|
|
17208
|
+
try {
|
|
17209
|
+
const sigBytes = sign(
|
|
17210
|
+
payloadBytes,
|
|
17211
|
+
storedIdentity.encrypted_private_key,
|
|
17212
|
+
identityEncKey
|
|
17213
|
+
);
|
|
17214
|
+
signatureB64 = toBase64url(sigBytes);
|
|
17215
|
+
} catch (err) {
|
|
17216
|
+
return toolResult({
|
|
17217
|
+
error: "Failed to sign bootstrap payload",
|
|
17218
|
+
details: err instanceof Error ? err.message : String(err),
|
|
17219
|
+
did: publicIdentity.did
|
|
17220
|
+
});
|
|
17221
|
+
}
|
|
17222
|
+
const body = {
|
|
17223
|
+
agentId: agentSlug,
|
|
17224
|
+
signature: signatureB64,
|
|
17225
|
+
publicKey: publicIdentity.public_key,
|
|
17226
|
+
type: "shr",
|
|
17227
|
+
data: publishData
|
|
17228
|
+
};
|
|
17229
|
+
try {
|
|
17230
|
+
const response = await fetch(`${verascoreUrl.replace(/\/$/, "")}/api/publish`, {
|
|
17231
|
+
method: "POST",
|
|
17232
|
+
headers: { "Content-Type": "application/json" },
|
|
17233
|
+
body: JSON.stringify(body)
|
|
17234
|
+
});
|
|
17235
|
+
const result = await response.json().catch(() => ({}));
|
|
17236
|
+
auditLog.append("l4", "sanctuary_bootstrap", publicIdentity.identity_id, {
|
|
17237
|
+
did: publicIdentity.did,
|
|
17238
|
+
verascore_url: verascoreUrl,
|
|
17239
|
+
status: response.status,
|
|
17240
|
+
published: response.ok
|
|
17241
|
+
});
|
|
17242
|
+
return toolResult({
|
|
17243
|
+
did: publicIdentity.did,
|
|
17244
|
+
identity_id: publicIdentity.identity_id,
|
|
17245
|
+
profileUrl,
|
|
17246
|
+
tier: "self-attested",
|
|
17247
|
+
published: response.ok,
|
|
17248
|
+
verascore_status: response.status,
|
|
17249
|
+
verascore_response: result
|
|
17250
|
+
});
|
|
17251
|
+
} catch (err) {
|
|
17252
|
+
auditLog.append("l4", "sanctuary_bootstrap", publicIdentity.identity_id, {
|
|
17253
|
+
did: publicIdentity.did,
|
|
17254
|
+
error: err instanceof Error ? err.message : String(err)
|
|
17255
|
+
});
|
|
17256
|
+
return toolResult({
|
|
17257
|
+
did: publicIdentity.did,
|
|
17258
|
+
identity_id: publicIdentity.identity_id,
|
|
17259
|
+
profileUrl,
|
|
17260
|
+
tier: "self-attested",
|
|
17261
|
+
published: false,
|
|
17262
|
+
warning: `Identity created but Verascore publish failed: ${err instanceof Error ? err.message : String(err)}`
|
|
17263
|
+
});
|
|
17264
|
+
}
|
|
17265
|
+
}
|
|
17266
|
+
},
|
|
17267
|
+
// ─── sanctuary_policy_status ───────────────────────────────────────
|
|
17268
|
+
{
|
|
17269
|
+
name: "sanctuary_policy_status",
|
|
17270
|
+
description: "Return a summary of the active Principal Policy: which operations require approval (Tier 1), which are subject to anomaly detection (Tier 2), and which auto-allow with audit (Tier 3).",
|
|
17271
|
+
inputSchema: {
|
|
17272
|
+
type: "object",
|
|
17273
|
+
properties: {}
|
|
17274
|
+
},
|
|
17275
|
+
handler: async () => {
|
|
17276
|
+
const tier1 = [...policy.tier1_always_approve].sort();
|
|
17277
|
+
const tier3 = [...policy.tier3_always_allow].sort();
|
|
17278
|
+
const tier2Config = policy.tier2_anomaly;
|
|
17279
|
+
auditLog.append("l2", "sanctuary_policy_status", "system", {
|
|
17280
|
+
tier1_count: tier1.length,
|
|
17281
|
+
tier3_count: tier3.length
|
|
17282
|
+
});
|
|
17283
|
+
return toolResult({
|
|
17284
|
+
tier1,
|
|
17285
|
+
tier2: [],
|
|
17286
|
+
tier3,
|
|
17287
|
+
tier2_anomaly_config: tier2Config,
|
|
17288
|
+
counts: {
|
|
17289
|
+
tier1: tier1.length,
|
|
17290
|
+
tier2: 0,
|
|
17291
|
+
tier3: tier3.length
|
|
17292
|
+
},
|
|
17293
|
+
note: "Tier 2 is not a named list in Sanctuary \u2014 it is behavioral anomaly detection applied to all operations. See tier2_anomaly_config."
|
|
17294
|
+
});
|
|
17295
|
+
}
|
|
17296
|
+
},
|
|
17297
|
+
// ─── sanctuary_export_identity_bundle ──────────────────────────────
|
|
17298
|
+
{
|
|
17299
|
+
name: "sanctuary_export_identity_bundle",
|
|
17300
|
+
description: "Export a signed, portable identity bundle: { publicKey, did, shr, attestations }. The bundle is signed with the identity's Ed25519 key so a recipient can verify authenticity against the public key. Private keys are never included.",
|
|
17301
|
+
inputSchema: {
|
|
17302
|
+
type: "object",
|
|
17303
|
+
properties: {
|
|
17304
|
+
identity_id: {
|
|
17305
|
+
type: "string",
|
|
17306
|
+
description: "Identity to export (defaults to primary identity)."
|
|
17307
|
+
},
|
|
17308
|
+
attestations: {
|
|
17309
|
+
type: "array",
|
|
17310
|
+
items: { type: "object" },
|
|
17311
|
+
description: "Optional list of attestation objects to include in the bundle."
|
|
17312
|
+
}
|
|
17313
|
+
}
|
|
17314
|
+
},
|
|
17315
|
+
handler: async (args) => {
|
|
17316
|
+
const identityId = args.identity_id;
|
|
17317
|
+
const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
|
|
17318
|
+
if (!identity) {
|
|
17319
|
+
return toolResult({
|
|
17320
|
+
error: "No identity found. Create one with identity_create first."
|
|
17321
|
+
});
|
|
17322
|
+
}
|
|
17323
|
+
const shr = generateSHR(identity.identity_id, {
|
|
17324
|
+
config,
|
|
17325
|
+
identityManager,
|
|
17326
|
+
masterKey
|
|
17327
|
+
});
|
|
17328
|
+
const attestations = args.attestations ?? [];
|
|
17329
|
+
const body = {
|
|
17330
|
+
format: "SANCTUARY_IDENTITY_BUNDLE_V1",
|
|
17331
|
+
publicKey: identity.public_key,
|
|
17332
|
+
did: identity.did,
|
|
17333
|
+
identity_id: identity.identity_id,
|
|
17334
|
+
label: identity.label,
|
|
17335
|
+
key_type: identity.key_type,
|
|
17336
|
+
shr: typeof shr === "string" ? null : shr,
|
|
17337
|
+
attestations,
|
|
17338
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
17339
|
+
};
|
|
17340
|
+
const bodyBytes = new TextEncoder().encode(JSON.stringify(body));
|
|
17341
|
+
let signatureB64;
|
|
17342
|
+
try {
|
|
17343
|
+
const sigBytes = sign(
|
|
17344
|
+
bodyBytes,
|
|
17345
|
+
identity.encrypted_private_key,
|
|
17346
|
+
identityEncKey
|
|
17347
|
+
);
|
|
17348
|
+
signatureB64 = toBase64url(sigBytes);
|
|
17349
|
+
} catch (err) {
|
|
17350
|
+
return toolResult({
|
|
17351
|
+
error: "Failed to sign identity bundle.",
|
|
17352
|
+
details: err instanceof Error ? err.message : String(err)
|
|
17353
|
+
});
|
|
17354
|
+
}
|
|
17355
|
+
auditLog.append("l1", "sanctuary_export_identity_bundle", identity.identity_id, {
|
|
17356
|
+
did: identity.did,
|
|
17357
|
+
attestation_count: attestations.length
|
|
17358
|
+
});
|
|
17359
|
+
return toolResult({
|
|
17360
|
+
bundle: body,
|
|
17361
|
+
signature: signatureB64,
|
|
17362
|
+
signed_by: identity.did
|
|
17363
|
+
});
|
|
17364
|
+
}
|
|
17365
|
+
},
|
|
17366
|
+
// ─── sanctuary_link_to_human ───────────────────────────────────────
|
|
17367
|
+
{
|
|
17368
|
+
name: "sanctuary_link_to_human",
|
|
17369
|
+
description: "Trigger a Verascore magic-link login flow so a human principal can authenticate and subsequently claim this agent's DID. The email is sent by Verascore to the supplied address. This tool only initiates the flow \u2014 it does not directly bind the DID.",
|
|
17370
|
+
inputSchema: {
|
|
17371
|
+
type: "object",
|
|
17372
|
+
properties: {
|
|
17373
|
+
email: {
|
|
17374
|
+
type: "string",
|
|
17375
|
+
description: "Email address of the human to link this agent to."
|
|
17376
|
+
},
|
|
17377
|
+
verascore_url: {
|
|
17378
|
+
type: "string",
|
|
17379
|
+
description: "Verascore base URL. Defaults to server config."
|
|
17380
|
+
}
|
|
17381
|
+
},
|
|
17382
|
+
required: ["email"]
|
|
17383
|
+
},
|
|
17384
|
+
handler: async (args) => {
|
|
17385
|
+
const email = args.email;
|
|
17386
|
+
const verascoreUrl = args.verascore_url || config.verascore.url || "https://verascore.ai";
|
|
17387
|
+
const urlCheck = validateVerascoreUrl(verascoreUrl, config.verascore.url);
|
|
17388
|
+
if (!urlCheck.ok) {
|
|
17389
|
+
return toolResult({ ok: false, error: urlCheck.error });
|
|
17390
|
+
}
|
|
17391
|
+
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
|
|
17392
|
+
return toolResult({ ok: false, error: "Invalid email format." });
|
|
17393
|
+
}
|
|
17394
|
+
try {
|
|
17395
|
+
const response = await fetch(`${verascoreUrl.replace(/\/$/, "")}/api/auth/request`, {
|
|
17396
|
+
method: "POST",
|
|
17397
|
+
headers: { "Content-Type": "application/json" },
|
|
17398
|
+
body: JSON.stringify({ email })
|
|
17399
|
+
});
|
|
17400
|
+
await response.json().catch(() => ({}));
|
|
17401
|
+
auditLog.append("l4", "sanctuary_link_to_human", "system", {
|
|
17402
|
+
verascore_url: verascoreUrl,
|
|
17403
|
+
status: response.status,
|
|
17404
|
+
// Do not log the email to the audit trail — keep it local.
|
|
17405
|
+
email_domain: email.split("@")[1] ?? null
|
|
17406
|
+
});
|
|
17407
|
+
return toolResult({
|
|
17408
|
+
ok: response.ok,
|
|
17409
|
+
message: "Check your email for a login link. After logging in, visit verascore.ai to claim this agent's DID.",
|
|
17410
|
+
email_redacted: `***@${email.split("@")[1] ?? "***"}`,
|
|
17411
|
+
verascore_status: response.status
|
|
17412
|
+
});
|
|
17413
|
+
} catch (err) {
|
|
17414
|
+
return toolResult({
|
|
17415
|
+
ok: false,
|
|
17416
|
+
error: `Failed to reach Verascore at ${verascoreUrl}: ${err instanceof Error ? err.message : String(err)}`
|
|
17417
|
+
});
|
|
17418
|
+
}
|
|
17419
|
+
}
|
|
17420
|
+
},
|
|
17421
|
+
// ─── sanctuary_sign_challenge ──────────────────────────────────────
|
|
17422
|
+
{
|
|
17423
|
+
name: "sanctuary_sign_challenge",
|
|
17424
|
+
description: "Sign a domain-separated nonce with the agent's Ed25519 key. Used in DID-ownership proof flows. The signed message is constructed as: 'sanctuary-sign-challenge-v1\\x00' + purpose + '\\x00' + nonce. The verifier MUST reconstruct the same domain-prefixed message before calling Ed25519 verify \u2014 a raw-nonce signature is NOT valid for this tool. The `purpose` field binds the signature to a specific use case (e.g. 'verascore-claim') so a signature produced for one purpose cannot be replayed against a different verifier.",
|
|
17425
|
+
inputSchema: {
|
|
17426
|
+
type: "object",
|
|
17427
|
+
properties: {
|
|
17428
|
+
nonce: {
|
|
17429
|
+
type: "string",
|
|
17430
|
+
description: "The nonce / challenge string to sign."
|
|
17431
|
+
},
|
|
17432
|
+
purpose: {
|
|
17433
|
+
type: "string",
|
|
17434
|
+
description: "Domain-separation tag identifying what the signature will be used for (e.g. 'verascore-claim'). Required. Max 128 chars, printable ASCII only."
|
|
17435
|
+
},
|
|
17436
|
+
identity_id: {
|
|
17437
|
+
type: "string",
|
|
17438
|
+
description: "Identity to sign with (defaults to primary)."
|
|
17439
|
+
}
|
|
17440
|
+
},
|
|
17441
|
+
required: ["nonce", "purpose"]
|
|
17442
|
+
},
|
|
17443
|
+
handler: async (args) => {
|
|
17444
|
+
const nonce = args.nonce;
|
|
17445
|
+
const purpose = args.purpose;
|
|
17446
|
+
if (!nonce || nonce.length === 0) {
|
|
17447
|
+
return toolResult({ error: "nonce must be a non-empty string." });
|
|
17448
|
+
}
|
|
17449
|
+
if (nonce.length > 4096) {
|
|
17450
|
+
return toolResult({ error: "nonce exceeds maximum length (4096)." });
|
|
17451
|
+
}
|
|
17452
|
+
if (typeof purpose !== "string" || purpose.length === 0) {
|
|
17453
|
+
return toolResult({
|
|
17454
|
+
error: "purpose is required (domain-separation tag, e.g. 'verascore-claim')."
|
|
17455
|
+
});
|
|
17456
|
+
}
|
|
17457
|
+
if (purpose.length > 128) {
|
|
17458
|
+
return toolResult({ error: "purpose exceeds maximum length (128)." });
|
|
17459
|
+
}
|
|
17460
|
+
if (!/^[\x20-\x7E]+$/.test(purpose)) {
|
|
17461
|
+
return toolResult({
|
|
17462
|
+
error: "purpose must be printable ASCII only (no NUL, no non-ASCII)."
|
|
17463
|
+
});
|
|
17464
|
+
}
|
|
17465
|
+
const identityId = args.identity_id;
|
|
17466
|
+
const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
|
|
17467
|
+
if (!identity) {
|
|
17468
|
+
return toolResult({
|
|
17469
|
+
error: "No identity found. Create one with identity_create first."
|
|
17470
|
+
});
|
|
17471
|
+
}
|
|
17472
|
+
const domainTag = "sanctuary-sign-challenge-v1";
|
|
17473
|
+
const enc = new TextEncoder();
|
|
17474
|
+
const tagBytes = enc.encode(domainTag);
|
|
17475
|
+
const purposeBytes = enc.encode(purpose);
|
|
17476
|
+
const nonceBytes = enc.encode(nonce);
|
|
17477
|
+
const sep = new Uint8Array([0]);
|
|
17478
|
+
const message = new Uint8Array(
|
|
17479
|
+
tagBytes.length + 1 + purposeBytes.length + 1 + nonceBytes.length
|
|
17480
|
+
);
|
|
17481
|
+
let offset = 0;
|
|
17482
|
+
message.set(tagBytes, offset);
|
|
17483
|
+
offset += tagBytes.length;
|
|
17484
|
+
message.set(sep, offset);
|
|
17485
|
+
offset += 1;
|
|
17486
|
+
message.set(purposeBytes, offset);
|
|
17487
|
+
offset += purposeBytes.length;
|
|
17488
|
+
message.set(sep, offset);
|
|
17489
|
+
offset += 1;
|
|
17490
|
+
message.set(nonceBytes, offset);
|
|
17491
|
+
let sigB64;
|
|
17492
|
+
try {
|
|
17493
|
+
const sig = sign(
|
|
17494
|
+
message,
|
|
17495
|
+
identity.encrypted_private_key,
|
|
17496
|
+
identityEncKey
|
|
17497
|
+
);
|
|
17498
|
+
sigB64 = toBase64url(sig);
|
|
17499
|
+
} catch (err) {
|
|
17500
|
+
return toolResult({
|
|
17501
|
+
error: "Failed to sign nonce.",
|
|
17502
|
+
details: err instanceof Error ? err.message : String(err)
|
|
17503
|
+
});
|
|
17504
|
+
}
|
|
17505
|
+
auditLog.append("l1", "sanctuary_sign_challenge", identity.identity_id, {
|
|
17506
|
+
did: identity.did,
|
|
17507
|
+
nonce_len: nonce.length,
|
|
17508
|
+
purpose
|
|
17509
|
+
});
|
|
17510
|
+
return toolResult({
|
|
17511
|
+
signature: sigB64,
|
|
17512
|
+
did: identity.did,
|
|
17513
|
+
public_key: identity.public_key,
|
|
17514
|
+
signed_by: identity.did,
|
|
17515
|
+
domain_tag: domainTag,
|
|
17516
|
+
purpose
|
|
17517
|
+
});
|
|
17518
|
+
}
|
|
17519
|
+
}
|
|
17520
|
+
];
|
|
17521
|
+
return { tools };
|
|
17522
|
+
}
|
|
17523
|
+
|
|
15817
17524
|
// src/index.ts
|
|
15818
17525
|
init_random();
|
|
15819
17526
|
init_encoding();
|
|
@@ -16087,7 +17794,7 @@ async function createSanctuaryServer(options) {
|
|
|
16087
17794
|
}
|
|
16088
17795
|
const l2Tools = [
|
|
16089
17796
|
{
|
|
16090
|
-
name: "
|
|
17797
|
+
name: "exec_attest",
|
|
16091
17798
|
description: "Generate an attestation of the current execution environment, including sovereignty assessment and degradation report.",
|
|
16092
17799
|
inputSchema: {
|
|
16093
17800
|
type: "object",
|
|
@@ -16138,7 +17845,7 @@ async function createSanctuaryServer(options) {
|
|
|
16138
17845
|
}
|
|
16139
17846
|
},
|
|
16140
17847
|
{
|
|
16141
|
-
name: "
|
|
17848
|
+
name: "monitor_health",
|
|
16142
17849
|
description: "Sanctuary Health Report (SHR) \u2014 standardized sovereignty status.",
|
|
16143
17850
|
inputSchema: { type: "object", properties: {} },
|
|
16144
17851
|
handler: async () => {
|
|
@@ -16187,7 +17894,7 @@ async function createSanctuaryServer(options) {
|
|
|
16187
17894
|
}
|
|
16188
17895
|
},
|
|
16189
17896
|
{
|
|
16190
|
-
name: "
|
|
17897
|
+
name: "monitor_audit_log",
|
|
16191
17898
|
description: "Query the sovereignty audit log.",
|
|
16192
17899
|
inputSchema: {
|
|
16193
17900
|
type: "object",
|
|
@@ -16213,7 +17920,7 @@ async function createSanctuaryServer(options) {
|
|
|
16213
17920
|
}
|
|
16214
17921
|
];
|
|
16215
17922
|
const manifestTool = {
|
|
16216
|
-
name: "
|
|
17923
|
+
name: "manifest",
|
|
16217
17924
|
description: "Generate the Sanctuary Interface Manifest (SIM) \u2014 a machine-readable declaration of this server's capabilities.",
|
|
16218
17925
|
inputSchema: { type: "object", properties: {} },
|
|
16219
17926
|
handler: async () => {
|
|
@@ -16299,14 +18006,19 @@ async function createSanctuaryServer(options) {
|
|
|
16299
18006
|
config,
|
|
16300
18007
|
identityManager,
|
|
16301
18008
|
masterKey,
|
|
16302
|
-
auditLog
|
|
18009
|
+
auditLog,
|
|
18010
|
+
{
|
|
18011
|
+
autoPublishHandshakes: config.verascore.auto_publish_handshakes,
|
|
18012
|
+
verascoreUrl: config.verascore.url
|
|
18013
|
+
}
|
|
16303
18014
|
);
|
|
16304
18015
|
const { tools: l4Tools} = createL4Tools(
|
|
16305
18016
|
storage,
|
|
16306
18017
|
masterKey,
|
|
16307
18018
|
identityManager,
|
|
16308
18019
|
auditLog,
|
|
16309
|
-
handshakeResults
|
|
18020
|
+
handshakeResults,
|
|
18021
|
+
config.verascore.url
|
|
16310
18022
|
);
|
|
16311
18023
|
const { tools: federationTools } = createFederationTools(
|
|
16312
18024
|
auditLog,
|
|
@@ -16320,6 +18032,7 @@ async function createSanctuaryServer(options) {
|
|
|
16320
18032
|
handshakeResults
|
|
16321
18033
|
);
|
|
16322
18034
|
const { tools: auditTools } = createAuditTools(config);
|
|
18035
|
+
const { tools: siemTools } = createSIEMTools(auditLog);
|
|
16323
18036
|
const { tools: contextGateTools, enforcer: contextGateEnforcer } = createContextGateTools(storage, masterKey, auditLog);
|
|
16324
18037
|
const hardeningTools = createL2HardeningTools(config.storage_path, auditLog);
|
|
16325
18038
|
const profileStore = new SovereigntyProfileStore(storage, masterKey);
|
|
@@ -16391,10 +18104,18 @@ async function createSanctuaryServer(options) {
|
|
|
16391
18104
|
} : void 0;
|
|
16392
18105
|
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
|
|
16393
18106
|
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
18107
|
+
const { tools: sanctuaryMetaTools } = createSanctuaryTools({
|
|
18108
|
+
config,
|
|
18109
|
+
identityManager,
|
|
18110
|
+
masterKey,
|
|
18111
|
+
auditLog,
|
|
18112
|
+
policy,
|
|
18113
|
+
keyProtection
|
|
18114
|
+
});
|
|
16394
18115
|
const dashboardTools = [];
|
|
16395
18116
|
if (dashboard) {
|
|
16396
18117
|
dashboardTools.push({
|
|
16397
|
-
name: "
|
|
18118
|
+
name: "dashboard_open",
|
|
16398
18119
|
description: "Generate a one-click URL to open the Principal Dashboard in a browser. Returns a pre-authenticated link \u2014 no manual token entry needed.",
|
|
16399
18120
|
inputSchema: {
|
|
16400
18121
|
type: "object",
|
|
@@ -16428,10 +18149,12 @@ async function createSanctuaryServer(options) {
|
|
|
16428
18149
|
...federationTools,
|
|
16429
18150
|
...bridgeTools,
|
|
16430
18151
|
...auditTools,
|
|
18152
|
+
...siemTools,
|
|
16431
18153
|
...contextGateTools,
|
|
16432
18154
|
...hardeningTools,
|
|
16433
18155
|
...profileTools,
|
|
16434
18156
|
...dashboardTools,
|
|
18157
|
+
...sanctuaryMetaTools,
|
|
16435
18158
|
manifestTool
|
|
16436
18159
|
];
|
|
16437
18160
|
let clientManager;
|
|
@@ -16473,7 +18196,12 @@ async function createSanctuaryServer(options) {
|
|
|
16473
18196
|
}
|
|
16474
18197
|
return args;
|
|
16475
18198
|
},
|
|
16476
|
-
governor
|
|
18199
|
+
governor,
|
|
18200
|
+
onProxyCall: (data) => {
|
|
18201
|
+
if (dashboard) {
|
|
18202
|
+
dashboard.broadcastProxyCall(data);
|
|
18203
|
+
}
|
|
18204
|
+
}
|
|
16477
18205
|
}
|
|
16478
18206
|
);
|
|
16479
18207
|
clientManager.configure(enabledServers).catch((err) => {
|
|
@@ -16491,6 +18219,7 @@ async function createSanctuaryServer(options) {
|
|
|
16491
18219
|
auditLog,
|
|
16492
18220
|
clientManager
|
|
16493
18221
|
});
|
|
18222
|
+
dashboard.enableFortressView(enabledServers.length);
|
|
16494
18223
|
}
|
|
16495
18224
|
}
|
|
16496
18225
|
}
|