@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/index.cjs CHANGED
@@ -429,6 +429,12 @@ function defaultConfig() {
429
429
  secret: "",
430
430
  callback_port: 3502,
431
431
  callback_host: "127.0.0.1"
432
+ },
433
+ verascore: {
434
+ url: "https://verascore.ai",
435
+ auto_publish_to_verascore: true,
436
+ // DELTA-04: default OFF for privacy. Enable explicitly per deployment.
437
+ auto_publish_handshakes: false
432
438
  }
433
439
  };
434
440
  }
@@ -499,6 +505,21 @@ async function loadConfig(configPath) {
499
505
  if (process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST) {
500
506
  config.webhook.callback_host = process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST;
501
507
  }
508
+ if (process.env.SANCTUARY_VERASCORE_URL) {
509
+ config.verascore.url = process.env.SANCTUARY_VERASCORE_URL;
510
+ }
511
+ if (process.env.SANCTUARY_AUTO_PUBLISH_TO_VERASCORE === "true") {
512
+ config.verascore.auto_publish_to_verascore = true;
513
+ }
514
+ if (process.env.SANCTUARY_AUTO_PUBLISH_TO_VERASCORE === "false") {
515
+ config.verascore.auto_publish_to_verascore = false;
516
+ }
517
+ if (process.env.SANCTUARY_AUTO_PUBLISH_HANDSHAKES === "true") {
518
+ config.verascore.auto_publish_handshakes = true;
519
+ }
520
+ if (process.env.SANCTUARY_AUTO_PUBLISH_HANDSHAKES === "false") {
521
+ config.verascore.auto_publish_handshakes = false;
522
+ }
502
523
  config.version = PKG_VERSION;
503
524
  validateConfig(config);
504
525
  return config;
@@ -1390,7 +1411,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1390
1411
  const tools = [
1391
1412
  // ── Identity Tools ──────────────────────────────────────────────────
1392
1413
  {
1393
- name: "sanctuary/identity_create",
1414
+ name: "identity_create",
1394
1415
  description: "Create a new sovereign identity (Ed25519 keypair). The private key is encrypted and never exposed.",
1395
1416
  inputSchema: {
1396
1417
  type: "object",
@@ -1425,7 +1446,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1425
1446
  }
1426
1447
  },
1427
1448
  {
1428
- name: "sanctuary/identity_list",
1449
+ name: "identity_list",
1429
1450
  description: "List all managed sovereign identities.",
1430
1451
  inputSchema: {
1431
1452
  type: "object",
@@ -1450,7 +1471,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1450
1471
  }
1451
1472
  },
1452
1473
  {
1453
- name: "sanctuary/identity_sign",
1474
+ name: "identity_sign",
1454
1475
  description: "Sign data with a managed identity. The private key is decrypted in memory only during signing.",
1455
1476
  inputSchema: {
1456
1477
  type: "object",
@@ -1488,7 +1509,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1488
1509
  }
1489
1510
  },
1490
1511
  {
1491
- name: "sanctuary/identity_verify",
1512
+ name: "identity_verify",
1492
1513
  description: "Verify an Ed25519 signature. Provide either identity_id or public_key.",
1493
1514
  inputSchema: {
1494
1515
  type: "object",
@@ -1537,7 +1558,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1537
1558
  }
1538
1559
  },
1539
1560
  {
1540
- name: "sanctuary/identity_rotate",
1561
+ name: "identity_rotate",
1541
1562
  description: "Rotate keys for an identity. Generates a new keypair and signs a rotation event with the old key for verifiable chain.",
1542
1563
  inputSchema: {
1543
1564
  type: "object",
@@ -1570,7 +1591,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1570
1591
  },
1571
1592
  // ── State Tools ─────────────────────────────────────────────────────
1572
1593
  {
1573
- name: "sanctuary/state_write",
1594
+ name: "state_write",
1574
1595
  description: "Write encrypted state to the sovereign store. Value is encrypted with a namespace-specific key. The write is signed by the active identity.",
1575
1596
  inputSchema: {
1576
1597
  type: "object",
@@ -1627,7 +1648,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1627
1648
  }
1628
1649
  },
1629
1650
  {
1630
- name: "sanctuary/state_read",
1651
+ name: "state_read",
1631
1652
  description: "Read and decrypt state from the sovereign store. Verifies integrity via Merkle proof and signature.",
1632
1653
  inputSchema: {
1633
1654
  type: "object",
@@ -1668,7 +1689,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1668
1689
  }
1669
1690
  },
1670
1691
  {
1671
- name: "sanctuary/state_list",
1692
+ name: "state_list",
1672
1693
  description: "List keys in a namespace (metadata only \u2014 no decryption).",
1673
1694
  inputSchema: {
1674
1695
  type: "object",
@@ -1700,7 +1721,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1700
1721
  }
1701
1722
  },
1702
1723
  {
1703
- name: "sanctuary/state_delete",
1724
+ name: "state_delete",
1704
1725
  description: "Securely delete state. Overwrites file with random bytes before removal (right to deletion, S1.6).",
1705
1726
  inputSchema: {
1706
1727
  type: "object",
@@ -1732,7 +1753,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1732
1753
  }
1733
1754
  },
1734
1755
  {
1735
- name: "sanctuary/state_export",
1756
+ name: "state_export",
1736
1757
  description: "Export state as an encrypted, portable bundle for migration.",
1737
1758
  inputSchema: {
1738
1759
  type: "object",
@@ -1752,7 +1773,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1752
1773
  }
1753
1774
  },
1754
1775
  {
1755
- name: "sanctuary/state_import",
1776
+ name: "state_import",
1756
1777
  description: "Import a previously exported state bundle.",
1757
1778
  inputSchema: {
1758
1779
  type: "object",
@@ -2373,7 +2394,7 @@ function createL3Tools(storage, masterKey, auditLog) {
2373
2394
  const tools = [
2374
2395
  // ─── Commitment Schemes ───────────────────────────────────────────────
2375
2396
  {
2376
- name: "sanctuary/proof_commitment",
2397
+ name: "proof_commitment",
2377
2398
  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).",
2378
2399
  inputSchema: {
2379
2400
  type: "object",
@@ -2408,7 +2429,7 @@ function createL3Tools(storage, masterKey, auditLog) {
2408
2429
  }
2409
2430
  },
2410
2431
  {
2411
- name: "sanctuary/proof_reveal",
2432
+ name: "proof_reveal",
2412
2433
  description: "Verify a previously committed value by revealing it with the blinding factor. Returns whether the revealed value matches the commitment.",
2413
2434
  inputSchema: {
2414
2435
  type: "object",
@@ -2446,7 +2467,7 @@ function createL3Tools(storage, masterKey, auditLog) {
2446
2467
  },
2447
2468
  // ─── Disclosure Policies ──────────────────────────────────────────────
2448
2469
  {
2449
- name: "sanctuary/disclosure_set_policy",
2470
+ name: "disclosure_set_policy",
2450
2471
  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.",
2451
2472
  inputSchema: {
2452
2473
  type: "object",
@@ -2521,7 +2542,7 @@ function createL3Tools(storage, masterKey, auditLog) {
2521
2542
  }
2522
2543
  },
2523
2544
  {
2524
- name: "sanctuary/disclosure_evaluate",
2545
+ name: "disclosure_evaluate",
2525
2546
  description: "Evaluate a disclosure request against an active policy. Returns per-field decisions: disclose, withhold, proof, or ask-principal.",
2526
2547
  inputSchema: {
2527
2548
  type: "object",
@@ -2597,7 +2618,7 @@ function createL3Tools(storage, masterKey, auditLog) {
2597
2618
  },
2598
2619
  // ─── ZK Proof Tools ───────────────────────────────────────────────────
2599
2620
  {
2600
- name: "sanctuary/zk_commit",
2621
+ name: "zk_commit",
2601
2622
  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.",
2602
2623
  inputSchema: {
2603
2624
  type: "object",
@@ -2628,7 +2649,7 @@ function createL3Tools(storage, masterKey, auditLog) {
2628
2649
  }
2629
2650
  },
2630
2651
  {
2631
- name: "sanctuary/zk_prove",
2652
+ name: "zk_prove",
2632
2653
  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.",
2633
2654
  inputSchema: {
2634
2655
  type: "object",
@@ -2669,7 +2690,7 @@ function createL3Tools(storage, masterKey, auditLog) {
2669
2690
  }
2670
2691
  },
2671
2692
  {
2672
- name: "sanctuary/zk_verify",
2693
+ name: "zk_verify",
2673
2694
  description: "Verify a zero-knowledge proof of knowledge for a Pedersen commitment. Checks that the prover knows the commitment's opening without learning anything.",
2674
2695
  inputSchema: {
2675
2696
  type: "object",
@@ -2697,7 +2718,7 @@ function createL3Tools(storage, masterKey, auditLog) {
2697
2718
  }
2698
2719
  },
2699
2720
  {
2700
- name: "sanctuary/zk_range_prove",
2721
+ name: "zk_range_prove",
2701
2722
  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.",
2702
2723
  inputSchema: {
2703
2724
  type: "object",
@@ -2747,7 +2768,7 @@ function createL3Tools(storage, masterKey, auditLog) {
2747
2768
  }
2748
2769
  },
2749
2770
  {
2750
- name: "sanctuary/zk_range_verify",
2771
+ name: "zk_range_verify",
2751
2772
  description: "Verify a zero-knowledge range proof \u2014 confirms a committed value is within the claimed range without learning the value.",
2752
2773
  inputSchema: {
2753
2774
  type: "object",
@@ -3188,14 +3209,14 @@ function tierDistribution(tiers) {
3188
3209
  }
3189
3210
 
3190
3211
  // src/l4-reputation/tools.ts
3191
- function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeResults) {
3212
+ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeResults, verascoreUrl) {
3192
3213
  const reputationStore = new ReputationStore(storage, masterKey);
3193
3214
  const identityEncryptionKey = derivePurposeKey(masterKey, "identity-encryption");
3194
3215
  const hsResults = handshakeResults ?? /* @__PURE__ */ new Map();
3195
3216
  const tools = [
3196
3217
  // ─── Reputation Recording ─────────────────────────────────────────
3197
3218
  {
3198
- name: "sanctuary/reputation_record",
3219
+ name: "reputation_record",
3199
3220
  description: "Record an interaction outcome as a signed attestation. Creates an EAS-compatible attestation signed by the specified identity.",
3200
3221
  inputSchema: {
3201
3222
  type: "object",
@@ -3288,7 +3309,7 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
3288
3309
  },
3289
3310
  // ─── Reputation Query ─────────────────────────────────────────────
3290
3311
  {
3291
- name: "sanctuary/reputation_query",
3312
+ name: "reputation_query",
3292
3313
  description: "Query aggregated reputation data with filtering. Returns summary statistics, never raw interaction details.",
3293
3314
  inputSchema: {
3294
3315
  type: "object",
@@ -3336,7 +3357,7 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
3336
3357
  },
3337
3358
  // ─── Reputation Export ─────────────────────────────────────────────
3338
3359
  {
3339
- name: "sanctuary/reputation_export",
3360
+ name: "reputation_export",
3340
3361
  description: "Export a portable reputation bundle (SANCTUARY_REP_V1). Includes all signed attestations for independent verification.",
3341
3362
  inputSchema: {
3342
3363
  type: "object",
@@ -3395,7 +3416,7 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
3395
3416
  },
3396
3417
  // ─── Reputation Import ────────────────────────────────────────────
3397
3418
  {
3398
- name: "sanctuary/reputation_import",
3419
+ name: "reputation_import",
3399
3420
  description: "Import a reputation bundle from another Sanctuary instance. Verifies all attestation signatures by default.",
3400
3421
  inputSchema: {
3401
3422
  type: "object",
@@ -3447,7 +3468,7 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
3447
3468
  },
3448
3469
  // ─── Sovereignty-Weighted Query ──────────────────────────────────
3449
3470
  {
3450
- name: "sanctuary/reputation_query_weighted",
3471
+ name: "reputation_query_weighted",
3451
3472
  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.",
3452
3473
  inputSchema: {
3453
3474
  type: "object",
@@ -3503,7 +3524,7 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
3503
3524
  },
3504
3525
  // ─── Trust Bootstrap: Escrow ──────────────────────────────────────
3505
3526
  {
3506
- name: "sanctuary/bootstrap_create_escrow",
3527
+ name: "bootstrap_create_escrow",
3507
3528
  description: "Create an escrow record for trust bootstrapping. Allows new participants with no reputation to transact safely.",
3508
3529
  inputSchema: {
3509
3530
  type: "object",
@@ -3562,7 +3583,7 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
3562
3583
  },
3563
3584
  // ─── Trust Bootstrap: Guarantee ───────────────────────────────────
3564
3585
  {
3565
- name: "sanctuary/bootstrap_provide_guarantee",
3586
+ name: "bootstrap_provide_guarantee",
3566
3587
  description: "A principal provides a signed reputation guarantee for a new agent. The guarantee certificate can be presented to counterparties.",
3567
3588
  inputSchema: {
3568
3589
  type: "object",
@@ -3640,7 +3661,7 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
3640
3661
  },
3641
3662
  // ─── Verascore Reputation Publish ────────────────────────────────
3642
3663
  {
3643
- name: "sanctuary/reputation_publish",
3664
+ name: "reputation_publish",
3644
3665
  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.",
3645
3666
  inputSchema: {
3646
3667
  type: "object",
@@ -3678,8 +3699,18 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
3678
3699
  });
3679
3700
  }
3680
3701
  const publishType = args.type;
3681
- const veracoreUrl = args.verascore_url || "https://verascore.ai";
3682
- const ALLOWED_VERASCORE_HOSTS = ["verascore.ai", "www.verascore.ai", "api.verascore.ai"];
3702
+ const configuredVerascoreUrl = verascoreUrl || "https://verascore.ai";
3703
+ const veracoreUrl = args.verascore_url || configuredVerascoreUrl;
3704
+ const ALLOWED_VERASCORE_HOSTS = /* @__PURE__ */ new Set([
3705
+ "verascore.ai",
3706
+ "www.verascore.ai",
3707
+ "api.verascore.ai"
3708
+ ]);
3709
+ try {
3710
+ const configuredHost = new URL(configuredVerascoreUrl).hostname;
3711
+ ALLOWED_VERASCORE_HOSTS.add(configuredHost);
3712
+ } catch {
3713
+ }
3683
3714
  try {
3684
3715
  const parsed = new URL(veracoreUrl);
3685
3716
  if (parsed.protocol !== "https:") {
@@ -3687,9 +3718,9 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
3687
3718
  error: `verascore_url must use HTTPS. Got: ${parsed.protocol}`
3688
3719
  });
3689
3720
  }
3690
- if (!ALLOWED_VERASCORE_HOSTS.includes(parsed.hostname)) {
3721
+ if (!ALLOWED_VERASCORE_HOSTS.has(parsed.hostname)) {
3691
3722
  return toolResult({
3692
- error: `verascore_url must point to a known Verascore domain (${ALLOWED_VERASCORE_HOSTS.join(", ")}). Got: ${parsed.hostname}`
3723
+ error: `verascore_url must point to a known Verascore domain (${[...ALLOWED_VERASCORE_HOSTS].join(", ")}). Got: ${parsed.hostname}`
3693
3724
  });
3694
3725
  }
3695
3726
  } catch {
@@ -3820,12 +3851,14 @@ var DEFAULT_POLICY = {
3820
3851
  "reputation_export",
3821
3852
  "bootstrap_provide_guarantee",
3822
3853
  "decommission_certificate",
3823
- "reputation_publish",
3824
- // SEC-039: Explicit Tier 1 — sends data to external API
3825
3854
  "sovereignty_profile_update",
3826
3855
  // Changes enforcement behavior — always requires approval
3827
- "governor_reset"
3856
+ "governor_reset",
3828
3857
  // Clears all runtime governance state — always requires approval
3858
+ "sanctuary_bootstrap",
3859
+ // Creates new Ed25519 identity + publishes — always requires approval
3860
+ "sanctuary_export_identity_bundle"
3861
+ // Exports portable identity — always requires approval
3829
3862
  ],
3830
3863
  tier2_anomaly: DEFAULT_TIER2,
3831
3864
  tier3_always_allow: [
@@ -3883,7 +3916,11 @@ var DEFAULT_POLICY = {
3883
3916
  "sovereignty_profile_get",
3884
3917
  "sovereignty_profile_generate_prompt",
3885
3918
  // Agent needs its own config to generate system prompt
3886
- "governor_status"
3919
+ "governor_status",
3920
+ "reputation_publish",
3921
+ // Auto-allow: publishing sovereignty data to Verascore is routine
3922
+ "sanctuary_policy_status"
3923
+ // Read-only policy summary
3887
3924
  ],
3888
3925
  approval_channel: DEFAULT_CHANNEL
3889
3926
  };
@@ -3994,9 +4031,10 @@ tier1_always_approve:
3994
4031
  - reputation_import
3995
4032
  - reputation_export
3996
4033
  - bootstrap_provide_guarantee
3997
- - reputation_publish
3998
4034
  - sovereignty_profile_update
3999
4035
  - governor_reset
4036
+ - sanctuary_bootstrap
4037
+ - sanctuary_export_identity_bundle
4000
4038
 
4001
4039
  # \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
4002
4040
  # Triggers approval when agent behavior deviates from its baseline.
@@ -4062,6 +4100,8 @@ tier3_always_allow:
4062
4100
  - dashboard_open
4063
4101
  - sovereignty_profile_get
4064
4102
  - governor_status
4103
+ - reputation_publish
4104
+ - sanctuary_policy_status
4065
4105
 
4066
4106
  # \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
4067
4107
  # How Sanctuary reaches you when approval is needed.
@@ -7327,241 +7367,1039 @@ function generateDashboardHTML(options) {
7327
7367
  </html>`;
7328
7368
  }
7329
7369
 
7330
- // src/system-prompt-generator.ts
7331
- var FEATURE_INFO = {
7332
- audit_logging: {
7333
- name: "Audit Logging",
7334
- activeDescription: "All your tool calls are logged to an encrypted audit trail. No action needed \u2014 this is automatic. You can query the log with sanctuary/monitor_audit_log if you need to review past activity.",
7335
- toolNames: ["sanctuary/monitor_audit_log"],
7336
- disabledDescription: "audit logging (sanctuary/monitor_audit_log)",
7337
- usageExample: "Automatic \u2014 every tool call you make is recorded. No explicit action required."
7338
- },
7339
- injection_detection: {
7340
- name: "Injection Detection",
7341
- activeDescription: "Your tool call arguments are scanned for prompt injection attempts. This is automatic \u2014 no action needed. If injection is detected in your input, the call will be blocked and you will receive an error. Do not retry blocked calls with the same input.",
7342
- disabledDescription: "injection detection",
7343
- usageExample: "Automatic \u2014 if a tool call is blocked with an injection alert, do not retry with the same arguments."
7344
- },
7345
- context_gating: {
7346
- name: "Context Gating",
7347
- activeDescription: "Before sending context to any external API (LLM inference, tool APIs, logging services), call sanctuary/context_gate_filter to strip sensitive fields. Use sanctuary/context_gate_set_policy to define filtering rules, or sanctuary/context_gate_apply_template for presets.",
7348
- toolNames: [
7349
- "sanctuary/context_gate_filter",
7350
- "sanctuary/context_gate_set_policy",
7351
- "sanctuary/context_gate_apply_template",
7352
- "sanctuary/context_gate_recommend",
7353
- "sanctuary/context_gate_list_policies"
7354
- ],
7355
- disabledDescription: "context gating (sanctuary/context_gate_filter)",
7356
- usageExample: "Before calling an external API, run: sanctuary/context_gate_filter with your context object and policy_id to get a filtered version."
7357
- },
7358
- approval_gate: {
7359
- name: "Approval Gates",
7360
- activeDescription: "High-risk operations require human approval before execution. Tier 1 operations (export, import, key rotation, deletion) always require approval. Tier 2 operations trigger approval when anomalous behavior is detected. When an operation is held for approval, you will receive an async response \u2014 wait for the human decision before proceeding.",
7361
- disabledDescription: "approval gates",
7362
- usageExample: "When you call a Tier 1 operation (e.g., state_export), expect an async hold. The human operator will approve or deny via the dashboard."
7363
- },
7364
- zk_proofs: {
7365
- name: "Zero-Knowledge Proofs",
7366
- activeDescription: "You can prove claims about your data without revealing the underlying values. Use sanctuary/zk_commit to create a Pedersen commitment, sanctuary/zk_prove (Schnorr proof) to prove you know a committed value, and sanctuary/zk_range_prove to prove a value falls within a range \u2014 all without disclosing the actual data. For simpler SHA-256 commitments, use sanctuary/proof_commitment.",
7367
- toolNames: [
7368
- "sanctuary/zk_commit",
7369
- "sanctuary/zk_prove",
7370
- "sanctuary/zk_range_prove",
7371
- "sanctuary/proof_commitment"
7372
- ],
7373
- disabledDescription: "zero-knowledge proofs (sanctuary/zk_commit, sanctuary/zk_prove)",
7374
- usageExample: "To prove a claim without revealing data: first sanctuary/zk_commit to commit, then sanctuary/zk_prove or sanctuary/zk_range_prove to generate a verifiable proof."
7375
- }
7376
- };
7377
- function generateSystemPrompt(profile) {
7378
- const activeFeatures = [];
7379
- const inactiveFeatures = [];
7380
- const activeKeys = [];
7381
- const featureKeys = [
7382
- "audit_logging",
7383
- "injection_detection",
7384
- "context_gating",
7385
- "approval_gate",
7386
- "zk_proofs"
7387
- ];
7388
- for (const key of featureKeys) {
7389
- const featureConfig = profile.features[key];
7390
- const info = FEATURE_INFO[key];
7391
- if (featureConfig.enabled) {
7392
- activeKeys.push(key);
7393
- let desc = `- ${info.name}: ${info.activeDescription}`;
7394
- if (key === "injection_detection" && "sensitivity" in featureConfig && featureConfig.sensitivity) {
7395
- desc += ` Sensitivity: ${featureConfig.sensitivity}.`;
7396
- }
7397
- if (key === "context_gating" && "policy_id" in featureConfig && featureConfig.policy_id) {
7398
- desc += ` Active policy: ${featureConfig.policy_id}.`;
7399
- }
7400
- activeFeatures.push(desc);
7401
- } else {
7402
- inactiveFeatures.push(info.disabledDescription);
7370
+ // src/cocoon/fortress-view.ts
7371
+ function generateFortressViewHTML(options) {
7372
+ return `<!DOCTYPE html>
7373
+ <html lang="en">
7374
+ <head>
7375
+ <meta charset="UTF-8">
7376
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7377
+ <title>Sanctuary \u2014 Fortress View</title>
7378
+ <style>
7379
+ :root {
7380
+ --bg: #0d1117;
7381
+ --surface: #161b22;
7382
+ --surface-raised: #1c2128;
7383
+ --border: #30363d;
7384
+ --text-primary: #e6edf3;
7385
+ --text-secondary: #8b949e;
7386
+ --text-muted: #484f58;
7387
+ --green: #3fb950;
7388
+ --green-dim: #238636;
7389
+ --amber: #d29922;
7390
+ --amber-dim: #9e6a03;
7391
+ --red: #f85149;
7392
+ --red-dim: #da3633;
7393
+ --blue: #58a6ff;
7394
+ --blue-dim: #1f6feb;
7403
7395
  }
7404
- }
7405
- const lines = [];
7406
- if (activeKeys.length > 0) {
7407
- lines.push("QUICK START:");
7408
- const quickStartItems = buildQuickStart(activeKeys);
7409
- for (const item of quickStartItems) {
7410
- lines.push(` ${item}`);
7396
+
7397
+ * { margin: 0; padding: 0; box-sizing: border-box; }
7398
+
7399
+ body {
7400
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
7401
+ background-color: var(--bg);
7402
+ color: var(--text-primary);
7403
+ min-height: 100vh;
7411
7404
  }
7412
- lines.push("");
7413
- }
7414
- lines.push(
7415
- "You are protected by Sanctuary sovereignty infrastructure. The following protections are active:"
7416
- );
7417
- lines.push("");
7418
- if (activeFeatures.length > 0) {
7419
- lines.push(...activeFeatures);
7420
- } else {
7421
- lines.push(
7422
- "- No features are currently enabled. Contact your operator to configure protections."
7423
- );
7424
- }
7425
- if (inactiveFeatures.length > 0) {
7426
- lines.push("");
7427
- lines.push(
7428
- `Optional tools available but not currently enabled: ${inactiveFeatures.join(", ")}.`
7429
- );
7430
- }
7431
- return lines.join("\n");
7432
- }
7433
- function buildQuickStart(activeKeys) {
7434
- const items = [];
7435
- if (activeKeys.includes("context_gating")) {
7436
- items.push(
7437
- "1. ALWAYS call sanctuary/context_gate_filter before sending context to external APIs."
7438
- );
7439
- }
7440
- if (activeKeys.includes("zk_proofs")) {
7441
- items.push(
7442
- `${items.length + 1}. Use sanctuary/zk_commit to prove claims without revealing underlying data.`
7443
- );
7444
- }
7445
- if (activeKeys.includes("approval_gate")) {
7446
- items.push(
7447
- `${items.length + 1}. High-risk operations will be held for human approval \u2014 expect async responses.`
7448
- );
7449
- }
7450
- if (items.length === 0) {
7451
- if (activeKeys.includes("audit_logging")) {
7452
- items.push("1. All tool calls are automatically logged to an encrypted audit trail.");
7405
+
7406
+ /* \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 */
7407
+ .fortress-header {
7408
+ display: flex;
7409
+ align-items: center;
7410
+ justify-content: space-between;
7411
+ padding: 16px 24px;
7412
+ border-bottom: 1px solid var(--border);
7413
+ background: var(--surface);
7453
7414
  }
7454
- if (activeKeys.includes("injection_detection")) {
7455
- items.push(
7456
- `${items.length + 1}. Tool arguments are scanned for injection \u2014 blocked calls should not be retried.`
7457
- );
7415
+
7416
+ .fortress-brand {
7417
+ display: flex;
7418
+ align-items: center;
7419
+ gap: 12px;
7458
7420
  }
7459
- }
7460
- return items;
7461
- }
7462
7421
 
7463
- // src/principal-policy/dashboard.ts
7464
- var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
7465
- var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
7466
- var MAX_SESSIONS = 1e3;
7467
- var RATE_LIMIT_WINDOW_MS = 6e4;
7468
- var RATE_LIMIT_GENERAL = 120;
7469
- var RATE_LIMIT_DECISIONS = 20;
7470
- var MAX_RATE_LIMIT_ENTRIES = 1e4;
7471
- var DashboardApprovalChannel = class {
7472
- config;
7473
- pending = /* @__PURE__ */ new Map();
7474
- sseClients = /* @__PURE__ */ new Set();
7475
- httpServer = null;
7476
- policy = null;
7477
- baseline = null;
7478
- auditLog = null;
7479
- identityManager = null;
7480
- handshakeResults = null;
7481
- shrOpts = null;
7482
- _sanctuaryConfig = null;
7483
- profileStore = null;
7484
- clientManager = null;
7485
- dashboardHTML;
7486
- loginHTML;
7487
- authToken;
7488
- useTLS;
7489
- /** Session TTL: longer for localhost, shorter for remote */
7490
- sessionTTLMs;
7491
- /** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
7492
- sessions = /* @__PURE__ */ new Map();
7493
- sessionCleanupTimer = null;
7494
- /** Rate limiting: per-IP request tracking */
7495
- rateLimits = /* @__PURE__ */ new Map();
7496
- /** Whether the dashboard is running in standalone mode (no MCP server) */
7497
- _standaloneMode = false;
7498
- constructor(config) {
7499
- this.config = config;
7500
- this.authToken = config.auth_token;
7501
- this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
7502
- const isLocalhost = config.host === "127.0.0.1" || config.host === "localhost" || config.host === "::1";
7503
- this.sessionTTLMs = isLocalhost ? SESSION_TTL_LOCAL_MS : SESSION_TTL_REMOTE_MS;
7504
- this.dashboardHTML = generateDashboardHTML({
7505
- timeoutSeconds: config.timeout_seconds,
7506
- serverVersion: SANCTUARY_VERSION
7507
- });
7508
- this.loginHTML = generateLoginHTML({ serverVersion: SANCTUARY_VERSION });
7509
- this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
7510
- }
7511
- /**
7512
- * Inject dependencies after construction.
7513
- * Called from index.ts after all components are initialized.
7514
- */
7515
- setDependencies(deps) {
7516
- this.policy = deps.policy;
7517
- this.baseline = deps.baseline;
7518
- this.auditLog = deps.auditLog;
7519
- if (deps.identityManager) this.identityManager = deps.identityManager;
7520
- if (deps.handshakeResults) this.handshakeResults = deps.handshakeResults;
7521
- if (deps.shrOpts) this.shrOpts = deps.shrOpts;
7522
- if (deps.sanctuaryConfig) this._sanctuaryConfig = deps.sanctuaryConfig;
7523
- if (deps.profileStore) this.profileStore = deps.profileStore;
7524
- if (deps.clientManager) this.clientManager = deps.clientManager;
7525
- }
7526
- /**
7527
- * Mark this dashboard as running in standalone mode.
7528
- * Exposed via /api/status so the frontend can show an appropriate banner.
7529
- */
7530
- setStandaloneMode(standalone) {
7531
- this._standaloneMode = standalone;
7532
- }
7533
- /**
7534
- * Start the HTTP(S) server for the dashboard.
7535
- */
7536
- async start() {
7537
- return new Promise((resolve, reject) => {
7538
- const handler = (req, res) => this.handleRequest(req, res);
7539
- if (this.useTLS && this.config.tls) {
7540
- const tlsOpts = {
7541
- cert: fs.readFileSync(this.config.tls.cert_path),
7542
- key: fs.readFileSync(this.config.tls.key_path)
7543
- };
7544
- this.httpServer = https.createServer(tlsOpts, handler);
7545
- } else {
7546
- this.httpServer = http.createServer(handler);
7547
- }
7548
- const protocol = this.useTLS ? "https" : "http";
7549
- const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
7550
- this.httpServer.listen(this.config.port, this.config.host, () => {
7551
- const sessionUrl = this.authToken ? this.createSessionUrl() : baseUrl;
7552
- process.stderr.write(
7553
- `
7554
- Sanctuary Principal Dashboard: ${baseUrl}
7555
- `
7556
- );
7557
- if (this.authToken) {
7558
- const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
7559
- process.stderr.write(
7560
- ` Auth token: ${hint}
7561
- `
7562
- );
7563
- }
7564
- process.stderr.write(`
7422
+ .fortress-brand .shield {
7423
+ font-size: 28px;
7424
+ color: var(--blue);
7425
+ }
7426
+
7427
+ .fortress-brand h1 {
7428
+ font-size: 18px;
7429
+ font-weight: 600;
7430
+ letter-spacing: -0.5px;
7431
+ }
7432
+
7433
+ .fortress-brand .version {
7434
+ font-size: 12px;
7435
+ color: var(--text-secondary);
7436
+ }
7437
+
7438
+ .header-actions {
7439
+ display: flex;
7440
+ gap: 8px;
7441
+ }
7442
+
7443
+ .header-actions button {
7444
+ padding: 6px 16px;
7445
+ border-radius: 6px;
7446
+ border: 1px solid var(--border);
7447
+ background: var(--surface);
7448
+ color: var(--text-primary);
7449
+ font-size: 13px;
7450
+ cursor: pointer;
7451
+ transition: background 0.15s;
7452
+ }
7453
+
7454
+ .header-actions button:hover {
7455
+ background: var(--surface-raised);
7456
+ }
7457
+
7458
+ .header-actions .pause-btn {
7459
+ border-color: var(--red-dim);
7460
+ color: var(--red);
7461
+ }
7462
+
7463
+ .header-actions .pause-btn:hover {
7464
+ background: rgba(248, 81, 73, 0.1);
7465
+ }
7466
+
7467
+ .header-actions .pause-btn.paused {
7468
+ background: var(--red-dim);
7469
+ color: white;
7470
+ }
7471
+
7472
+ /* \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 */
7473
+ .tab-bar {
7474
+ display: flex;
7475
+ border-bottom: 1px solid var(--border);
7476
+ background: var(--surface);
7477
+ padding: 0 24px;
7478
+ }
7479
+
7480
+ .tab-bar button {
7481
+ padding: 10px 16px;
7482
+ border: none;
7483
+ background: none;
7484
+ color: var(--text-secondary);
7485
+ font-size: 14px;
7486
+ cursor: pointer;
7487
+ border-bottom: 2px solid transparent;
7488
+ transition: all 0.15s;
7489
+ }
7490
+
7491
+ .tab-bar button:hover {
7492
+ color: var(--text-primary);
7493
+ }
7494
+
7495
+ .tab-bar button.active {
7496
+ color: var(--text-primary);
7497
+ border-bottom-color: var(--blue);
7498
+ }
7499
+
7500
+ /* \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 */
7501
+ .fortress-content { padding: 24px; }
7502
+
7503
+ /* \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 */
7504
+ .status-banner {
7505
+ display: flex;
7506
+ align-items: center;
7507
+ gap: 16px;
7508
+ padding: 20px 24px;
7509
+ border-radius: 8px;
7510
+ border: 1px solid var(--border);
7511
+ background: var(--surface);
7512
+ margin-bottom: 24px;
7513
+ }
7514
+
7515
+ .status-indicator {
7516
+ width: 48px;
7517
+ height: 48px;
7518
+ border-radius: 50%;
7519
+ display: flex;
7520
+ align-items: center;
7521
+ justify-content: center;
7522
+ font-size: 24px;
7523
+ flex-shrink: 0;
7524
+ }
7525
+
7526
+ .status-indicator.green { background: rgba(63, 185, 80, 0.15); color: var(--green); }
7527
+ .status-indicator.amber { background: rgba(210, 153, 34, 0.15); color: var(--amber); }
7528
+ .status-indicator.red { background: rgba(248, 81, 73, 0.15); color: var(--red); }
7529
+
7530
+ .status-info h2 {
7531
+ font-size: 18px;
7532
+ font-weight: 600;
7533
+ margin-bottom: 4px;
7534
+ }
7535
+
7536
+ .status-info p {
7537
+ font-size: 14px;
7538
+ color: var(--text-secondary);
7539
+ }
7540
+
7541
+ .status-stats {
7542
+ display: flex;
7543
+ gap: 24px;
7544
+ margin-left: auto;
7545
+ }
7546
+
7547
+ .stat {
7548
+ text-align: center;
7549
+ }
7550
+
7551
+ .stat .value {
7552
+ font-size: 24px;
7553
+ font-weight: 600;
7554
+ font-variant-numeric: tabular-nums;
7555
+ }
7556
+
7557
+ .stat .label {
7558
+ font-size: 11px;
7559
+ color: var(--text-secondary);
7560
+ text-transform: uppercase;
7561
+ letter-spacing: 0.5px;
7562
+ }
7563
+
7564
+ /* \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 */
7565
+ .fortress-grid {
7566
+ display: grid;
7567
+ grid-template-columns: 1fr 360px;
7568
+ gap: 24px;
7569
+ }
7570
+
7571
+ @media (max-width: 900px) {
7572
+ .fortress-grid { grid-template-columns: 1fr; }
7573
+ }
7574
+
7575
+ /* \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 */
7576
+ .feed-panel {
7577
+ background: var(--surface);
7578
+ border: 1px solid var(--border);
7579
+ border-radius: 8px;
7580
+ overflow: hidden;
7581
+ }
7582
+
7583
+ .panel-header {
7584
+ display: flex;
7585
+ align-items: center;
7586
+ justify-content: space-between;
7587
+ padding: 12px 16px;
7588
+ border-bottom: 1px solid var(--border);
7589
+ }
7590
+
7591
+ .panel-header h3 {
7592
+ font-size: 14px;
7593
+ font-weight: 600;
7594
+ }
7595
+
7596
+ .feed-list {
7597
+ max-height: 600px;
7598
+ overflow-y: auto;
7599
+ scroll-behavior: smooth;
7600
+ }
7601
+
7602
+ .feed-item {
7603
+ display: flex;
7604
+ align-items: flex-start;
7605
+ gap: 10px;
7606
+ padding: 10px 16px;
7607
+ border-bottom: 1px solid var(--border);
7608
+ font-size: 13px;
7609
+ transition: background 0.1s;
7610
+ }
7611
+
7612
+ .feed-item:hover {
7613
+ background: var(--surface-raised);
7614
+ }
7615
+
7616
+ .feed-dot {
7617
+ width: 8px;
7618
+ height: 8px;
7619
+ border-radius: 50%;
7620
+ margin-top: 5px;
7621
+ flex-shrink: 0;
7622
+ }
7623
+
7624
+ .feed-dot.green { background: var(--green); }
7625
+ .feed-dot.amber { background: var(--amber); }
7626
+ .feed-dot.red { background: var(--red); }
7627
+
7628
+ .feed-detail {
7629
+ flex: 1;
7630
+ min-width: 0;
7631
+ }
7632
+
7633
+ .feed-tool {
7634
+ font-family: 'SF Mono', 'Fira Code', monospace;
7635
+ font-size: 12px;
7636
+ color: var(--blue);
7637
+ word-break: break-all;
7638
+ }
7639
+
7640
+ .feed-decision {
7641
+ font-size: 12px;
7642
+ color: var(--text-secondary);
7643
+ margin-top: 2px;
7644
+ }
7645
+
7646
+ .feed-time {
7647
+ font-size: 11px;
7648
+ color: var(--text-muted);
7649
+ flex-shrink: 0;
7650
+ white-space: nowrap;
7651
+ }
7652
+
7653
+ .feed-empty {
7654
+ padding: 40px 16px;
7655
+ text-align: center;
7656
+ color: var(--text-muted);
7657
+ font-size: 14px;
7658
+ }
7659
+
7660
+ /* \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 */
7661
+ .alerts-panel {
7662
+ background: var(--surface);
7663
+ border: 1px solid var(--border);
7664
+ border-radius: 8px;
7665
+ overflow: hidden;
7666
+ }
7667
+
7668
+ .alert-item {
7669
+ padding: 12px 16px;
7670
+ border-bottom: 1px solid var(--border);
7671
+ }
7672
+
7673
+ .alert-item .alert-title {
7674
+ font-size: 13px;
7675
+ font-weight: 500;
7676
+ margin-bottom: 4px;
7677
+ }
7678
+
7679
+ .alert-item .alert-desc {
7680
+ font-size: 12px;
7681
+ color: var(--text-secondary);
7682
+ margin-bottom: 8px;
7683
+ }
7684
+
7685
+ .alert-actions {
7686
+ display: flex;
7687
+ gap: 8px;
7688
+ }
7689
+
7690
+ .alert-actions button {
7691
+ padding: 4px 12px;
7692
+ border-radius: 4px;
7693
+ border: 1px solid var(--border);
7694
+ font-size: 12px;
7695
+ cursor: pointer;
7696
+ transition: all 0.15s;
7697
+ }
7698
+
7699
+ .approve-btn {
7700
+ background: var(--green-dim);
7701
+ color: white;
7702
+ border-color: var(--green-dim) !important;
7703
+ }
7704
+
7705
+ .approve-btn:hover { opacity: 0.9; }
7706
+
7707
+ .deny-btn {
7708
+ background: none;
7709
+ color: var(--red);
7710
+ border-color: var(--red-dim) !important;
7711
+ }
7712
+
7713
+ .deny-btn:hover {
7714
+ background: rgba(248, 81, 73, 0.1);
7715
+ }
7716
+
7717
+ .alerts-empty {
7718
+ padding: 40px 16px;
7719
+ text-align: center;
7720
+ color: var(--text-muted);
7721
+ font-size: 14px;
7722
+ }
7723
+
7724
+ /* \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 */
7725
+ .servers-panel {
7726
+ margin-top: 16px;
7727
+ }
7728
+
7729
+ .server-row {
7730
+ display: flex;
7731
+ align-items: center;
7732
+ gap: 8px;
7733
+ padding: 8px 16px;
7734
+ border-bottom: 1px solid var(--border);
7735
+ font-size: 13px;
7736
+ }
7737
+
7738
+ .server-status-dot {
7739
+ width: 8px;
7740
+ height: 8px;
7741
+ border-radius: 50%;
7742
+ }
7743
+
7744
+ .server-status-dot.connected { background: var(--green); }
7745
+ .server-status-dot.connecting { background: var(--amber); }
7746
+ .server-status-dot.disconnected, .server-status-dot.error { background: var(--red); }
7747
+
7748
+ .server-name {
7749
+ font-family: 'SF Mono', 'Fira Code', monospace;
7750
+ font-size: 12px;
7751
+ }
7752
+
7753
+ .server-tier {
7754
+ margin-left: auto;
7755
+ font-size: 11px;
7756
+ color: var(--text-secondary);
7757
+ }
7758
+ </style>
7759
+ </head>
7760
+ <body>
7761
+ <!-- Header -->
7762
+ <div class="fortress-header">
7763
+ <div class="fortress-brand">
7764
+ <div class="shield">&#x1F6E1;</div>
7765
+ <div>
7766
+ <h1>Sanctuary Cocoon</h1>
7767
+ <div class="version">v${esc(options.serverVersion)}</div>
7768
+ </div>
7769
+ </div>
7770
+ <div class="header-actions">
7771
+ <button class="pause-btn" id="pause-btn" title="Pause agent \u2014 requires approval for all operations">Pause Agent</button>
7772
+ <button id="advanced-btn">Advanced</button>
7773
+ </div>
7774
+ </div>
7775
+
7776
+ <!-- Tab bar -->
7777
+ <div class="tab-bar">
7778
+ <button class="active" data-tab="fortress">Fortress</button>
7779
+ <button data-tab="advanced">Advanced</button>
7780
+ </div>
7781
+
7782
+ <!-- Fortress View -->
7783
+ <div class="fortress-content" id="fortress-tab">
7784
+ <!-- Status Banner -->
7785
+ <div class="status-banner" id="status-banner">
7786
+ <div class="status-indicator green" id="status-indicator">&#x2713;</div>
7787
+ <div class="status-info">
7788
+ <h2 id="status-title">Agent Protected</h2>
7789
+ <p id="status-subtitle">${options.upstreamServerCount} server${options.upstreamServerCount !== 1 ? "s" : ""} monitored. All systems nominal.</p>
7790
+ </div>
7791
+ <div class="status-stats">
7792
+ <div class="stat">
7793
+ <div class="value" id="stat-total">0</div>
7794
+ <div class="label">Calls</div>
7795
+ </div>
7796
+ <div class="stat">
7797
+ <div class="value" id="stat-blocked">0</div>
7798
+ <div class="label">Blocked</div>
7799
+ </div>
7800
+ <div class="stat">
7801
+ <div class="value" id="stat-pending">0</div>
7802
+ <div class="label">Pending</div>
7803
+ </div>
7804
+ </div>
7805
+ </div>
7806
+
7807
+ <!-- Two-column layout -->
7808
+ <div class="fortress-grid">
7809
+ <!-- Live Feed -->
7810
+ <div class="feed-panel">
7811
+ <div class="panel-header">
7812
+ <h3>Live Activity</h3>
7813
+ <span style="font-size: 12px; color: var(--text-muted);" id="feed-count">0 events</span>
7814
+ </div>
7815
+ <div class="feed-list" id="feed-list">
7816
+ <div class="feed-empty">Waiting for tool calls...</div>
7817
+ </div>
7818
+ </div>
7819
+
7820
+ <!-- Right column: Alerts + Servers -->
7821
+ <div>
7822
+ <!-- Alerts -->
7823
+ <div class="alerts-panel">
7824
+ <div class="panel-header">
7825
+ <h3>Needs Attention</h3>
7826
+ <span style="font-size: 12px; color: var(--text-muted);" id="alert-count">0</span>
7827
+ </div>
7828
+ <div id="alerts-list">
7829
+ <div class="alerts-empty">No pending actions</div>
7830
+ </div>
7831
+ </div>
7832
+
7833
+ <!-- Servers -->
7834
+ <div class="alerts-panel servers-panel">
7835
+ <div class="panel-header">
7836
+ <h3>Upstream Servers</h3>
7837
+ </div>
7838
+ <div id="servers-list">
7839
+ <div class="alerts-empty">No servers configured</div>
7840
+ </div>
7841
+ </div>
7842
+ </div>
7843
+ </div>
7844
+ </div>
7845
+
7846
+ <script>
7847
+ // \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
7848
+ const API_BASE = window.location.origin;
7849
+ const SESSION_TOKEN = sessionStorage.getItem('sanctuary_session') || '';
7850
+ const MAX_FEED_ITEMS = 50;
7851
+
7852
+ let feedItems = [];
7853
+ let totalCalls = 0;
7854
+ let blockedCalls = 0;
7855
+ let pendingApprovals = [];
7856
+ let upstreamServers = [];
7857
+ let paused = false;
7858
+
7859
+ // \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
7860
+ function connectSSE() {
7861
+ const url = API_BASE + '/events' + (SESSION_TOKEN ? '?session=' + SESSION_TOKEN : '');
7862
+ const eventSource = new EventSource(url);
7863
+
7864
+ eventSource.addEventListener('proxy-call', (e) => {
7865
+ try {
7866
+ const data = JSON.parse(e.data);
7867
+ addFeedItem(data);
7868
+ } catch {}
7869
+ });
7870
+
7871
+ eventSource.addEventListener('proxy-server-status', (e) => {
7872
+ try {
7873
+ const data = JSON.parse(e.data);
7874
+ updateServerStatus(data.server, data.state, data.tool_count, data.error);
7875
+ } catch {}
7876
+ });
7877
+
7878
+ eventSource.addEventListener('injection-alert', (e) => {
7879
+ try {
7880
+ const data = JSON.parse(e.data);
7881
+ addFeedItem({
7882
+ tool: data.tool_name || 'unknown',
7883
+ server: 'detection',
7884
+ decision: 'blocked',
7885
+ reason: 'Injection detected: ' + (data.signals || []).join(', '),
7886
+ timestamp: new Date().toISOString(),
7887
+ });
7888
+ } catch {}
7889
+ });
7890
+
7891
+ eventSource.addEventListener('approval-request', (e) => {
7892
+ try {
7893
+ const data = JSON.parse(e.data);
7894
+ addPendingApproval(data);
7895
+ } catch {}
7896
+ });
7897
+
7898
+ eventSource.addEventListener('approval-resolved', (e) => {
7899
+ try {
7900
+ const data = JSON.parse(e.data);
7901
+ removePendingApproval(data.id);
7902
+ } catch {}
7903
+ });
7904
+
7905
+ eventSource.onerror = () => {
7906
+ eventSource.close();
7907
+ setTimeout(connectSSE, 3000);
7908
+ };
7909
+ }
7910
+
7911
+ // \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
7912
+ function addFeedItem(data) {
7913
+ totalCalls++;
7914
+ if (data.decision === 'blocked' || data.decision === 'denied') {
7915
+ blockedCalls++;
7916
+ }
7917
+
7918
+ feedItems.unshift({
7919
+ tool: data.tool || 'unknown',
7920
+ server: data.server || '',
7921
+ decision: data.decision || 'allowed',
7922
+ reason: data.reason || '',
7923
+ time: data.timestamp || new Date().toISOString(),
7924
+ });
7925
+
7926
+ if (feedItems.length > MAX_FEED_ITEMS) {
7927
+ feedItems = feedItems.slice(0, MAX_FEED_ITEMS);
7928
+ }
7929
+
7930
+ renderFeed();
7931
+ updateStats();
7932
+ updateStatus();
7933
+ }
7934
+
7935
+ function renderFeed() {
7936
+ const container = document.getElementById('feed-list');
7937
+ if (feedItems.length === 0) {
7938
+ container.innerHTML = '<div class="feed-empty">Waiting for tool calls...</div>';
7939
+ return;
7940
+ }
7941
+
7942
+ container.innerHTML = feedItems.map(item => {
7943
+ const dotColor = item.decision === 'allowed' ? 'green'
7944
+ : item.decision === 'pending' ? 'amber' : 'red';
7945
+ const decisionText = item.decision === 'allowed' ? 'Auto-allowed'
7946
+ : item.decision === 'pending' ? 'Awaiting approval'
7947
+ : item.decision === 'blocked' ? 'Blocked' : item.decision;
7948
+ const timeStr = new Date(item.time).toLocaleTimeString();
7949
+
7950
+ return '<div class="feed-item">' +
7951
+ '<div class="feed-dot ' + dotColor + '"></div>' +
7952
+ '<div class="feed-detail">' +
7953
+ '<div class="feed-tool">' + esc(item.tool) + '</div>' +
7954
+ '<div class="feed-decision">' + esc(decisionText) +
7955
+ (item.reason ? ' \u2014 ' + esc(item.reason) : '') + '</div>' +
7956
+ '</div>' +
7957
+ '<div class="feed-time">' + esc(timeStr) + '</div>' +
7958
+ '</div>';
7959
+ }).join('');
7960
+
7961
+ document.getElementById('feed-count').textContent = feedItems.length + ' events';
7962
+ }
7963
+
7964
+ // \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
7965
+ function addPendingApproval(data) {
7966
+ pendingApprovals.push(data);
7967
+ renderAlerts();
7968
+ updateStats();
7969
+ updateStatus();
7970
+ }
7971
+
7972
+ function removePendingApproval(id) {
7973
+ pendingApprovals = pendingApprovals.filter(a => a.id !== id);
7974
+ renderAlerts();
7975
+ updateStats();
7976
+ updateStatus();
7977
+ }
7978
+
7979
+ function renderAlerts() {
7980
+ const container = document.getElementById('alerts-list');
7981
+ if (pendingApprovals.length === 0) {
7982
+ container.innerHTML = '<div class="alerts-empty">No pending actions</div>';
7983
+ document.getElementById('alert-count').textContent = '0';
7984
+ return;
7985
+ }
7986
+
7987
+ document.getElementById('alert-count').textContent = pendingApprovals.length.toString();
7988
+
7989
+ container.innerHTML = pendingApprovals.map(approval => {
7990
+ return '<div class="alert-item">' +
7991
+ '<div class="alert-title">Approval required: ' + esc(approval.operation || approval.tool_name || 'unknown') + '</div>' +
7992
+ '<div class="alert-desc">' + esc(approval.reason || 'This operation requires your approval before it can proceed.') + '</div>' +
7993
+ '<div class="alert-actions">' +
7994
+ '<button class="approve-btn" onclick="handleApproval(\\'' + esc(approval.id) + '\\', true)">Approve</button>' +
7995
+ '<button class="deny-btn" onclick="handleApproval(\\'' + esc(approval.id) + '\\', false)">Deny</button>' +
7996
+ '</div>' +
7997
+ '</div>';
7998
+ }).join('');
7999
+ }
8000
+
8001
+ async function handleApproval(id, approved) {
8002
+ const endpoint = approved ? '/api/approve/' : '/api/deny/';
8003
+ try {
8004
+ await fetch(API_BASE + endpoint + id, {
8005
+ method: 'POST',
8006
+ headers: SESSION_TOKEN ? { 'Authorization': 'Bearer ' + SESSION_TOKEN } : {},
8007
+ });
8008
+ removePendingApproval(id);
8009
+ } catch (err) {
8010
+ console.error('Approval action failed:', err);
8011
+ }
8012
+ }
8013
+
8014
+ // \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
8015
+ function updateServerStatus(serverName, state, toolCount, error) {
8016
+ const existing = upstreamServers.find(s => s.name === serverName);
8017
+ if (existing) {
8018
+ existing.state = state;
8019
+ existing.tool_count = toolCount;
8020
+ existing.error = error;
8021
+ } else {
8022
+ upstreamServers.push({ name: serverName, state, tool_count: toolCount, error });
8023
+ }
8024
+ renderServers();
8025
+ updateStatus();
8026
+ }
8027
+
8028
+ function renderServers() {
8029
+ const container = document.getElementById('servers-list');
8030
+ if (upstreamServers.length === 0) {
8031
+ container.innerHTML = '<div class="alerts-empty">No servers configured</div>';
8032
+ return;
8033
+ }
8034
+
8035
+ container.innerHTML = upstreamServers.map(server => {
8036
+ const stateClass = server.state || 'disconnected';
8037
+ const stateLabel = server.state === 'connected' ? 'Connected'
8038
+ : server.state === 'connecting' ? 'Connecting...'
8039
+ : server.state === 'error' ? 'Error' : 'Disconnected';
8040
+
8041
+ return '<div class="server-row">' +
8042
+ '<div class="server-status-dot ' + stateClass + '"></div>' +
8043
+ '<span class="server-name">' + esc(server.name) + '</span>' +
8044
+ '<span class="server-tier">' + esc(stateLabel) +
8045
+ (server.tool_count ? ' (' + server.tool_count + ' tools)' : '') + '</span>' +
8046
+ '</div>';
8047
+ }).join('');
8048
+ }
8049
+
8050
+ // \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
8051
+ function updateStats() {
8052
+ document.getElementById('stat-total').textContent = totalCalls.toString();
8053
+ document.getElementById('stat-blocked').textContent = blockedCalls.toString();
8054
+ document.getElementById('stat-pending').textContent = pendingApprovals.length.toString();
8055
+ }
8056
+
8057
+ function updateStatus() {
8058
+ const indicator = document.getElementById('status-indicator');
8059
+ const title = document.getElementById('status-title');
8060
+ const subtitle = document.getElementById('status-subtitle');
8061
+
8062
+ const hasErrors = upstreamServers.some(s => s.state === 'error');
8063
+ const hasPending = pendingApprovals.length > 0;
8064
+ const hasBlocked = blockedCalls > 0;
8065
+
8066
+ if (paused) {
8067
+ indicator.className = 'status-indicator red';
8068
+ indicator.innerHTML = '&#x23F8;';
8069
+ title.textContent = 'Agent Paused';
8070
+ subtitle.textContent = 'All operations require approval. Click Resume to restore normal mode.';
8071
+ } else if (hasErrors) {
8072
+ indicator.className = 'status-indicator red';
8073
+ indicator.innerHTML = '&#x26A0;';
8074
+ title.textContent = 'Connection Issues';
8075
+ subtitle.textContent = 'One or more upstream servers have errors.';
8076
+ } else if (hasPending) {
8077
+ indicator.className = 'status-indicator amber';
8078
+ indicator.innerHTML = '&#x23F3;';
8079
+ title.textContent = 'Action Required';
8080
+ subtitle.textContent = pendingApprovals.length + ' operation' + (pendingApprovals.length > 1 ? 's' : '') + ' awaiting your approval.';
8081
+ } else {
8082
+ indicator.className = 'status-indicator green';
8083
+ indicator.innerHTML = '&#x2713;';
8084
+ title.textContent = 'Agent Protected';
8085
+ const serverCount = upstreamServers.filter(s => s.state === 'connected').length || ${options.upstreamServerCount};
8086
+ subtitle.textContent = serverCount + ' server' + (serverCount !== 1 ? 's' : '') + ' monitored. All systems nominal.';
8087
+ }
8088
+ }
8089
+
8090
+ // \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
8091
+ document.getElementById('pause-btn').addEventListener('click', () => {
8092
+ paused = !paused;
8093
+ const btn = document.getElementById('pause-btn');
8094
+ if (paused) {
8095
+ btn.textContent = 'Resume Agent';
8096
+ btn.classList.add('paused');
8097
+ } else {
8098
+ btn.textContent = 'Pause Agent';
8099
+ btn.classList.remove('paused');
8100
+ }
8101
+ updateStatus();
8102
+ // TODO: POST to /api/cocoon/pause to set all tiers to 1
8103
+ });
8104
+
8105
+ // \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
8106
+ document.getElementById('advanced-btn').addEventListener('click', () => {
8107
+ window.location.href = '/dashboard?session=' + SESSION_TOKEN;
8108
+ });
8109
+
8110
+ document.querySelectorAll('.tab-bar button').forEach(btn => {
8111
+ btn.addEventListener('click', () => {
8112
+ const tab = btn.dataset.tab;
8113
+ if (tab === 'advanced') {
8114
+ window.location.href = '/dashboard?session=' + SESSION_TOKEN;
8115
+ }
8116
+ });
8117
+ });
8118
+
8119
+ // \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
8120
+ function esc(str) {
8121
+ if (!str) return '';
8122
+ const d = document.createElement('div');
8123
+ d.textContent = String(str);
8124
+ return d.innerHTML;
8125
+ }
8126
+
8127
+ // \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
8128
+ async function init() {
8129
+ // Load initial server state
8130
+ try {
8131
+ const resp = await fetch(API_BASE + '/api/proxy/servers', {
8132
+ headers: SESSION_TOKEN ? { 'Authorization': 'Bearer ' + SESSION_TOKEN } : {},
8133
+ });
8134
+ if (resp.ok) {
8135
+ const data = await resp.json();
8136
+ upstreamServers = data.servers || [];
8137
+ renderServers();
8138
+ }
8139
+ } catch {}
8140
+
8141
+ // Load pending approvals
8142
+ try {
8143
+ const resp = await fetch(API_BASE + '/api/pending', {
8144
+ headers: SESSION_TOKEN ? { 'Authorization': 'Bearer ' + SESSION_TOKEN } : {},
8145
+ });
8146
+ if (resp.ok) {
8147
+ const data = await resp.json();
8148
+ pendingApprovals = data.pending || [];
8149
+ renderAlerts();
8150
+ updateStats();
8151
+ }
8152
+ } catch {}
8153
+
8154
+ updateStatus();
8155
+ connectSSE();
8156
+ }
8157
+
8158
+ init();
8159
+ </script>
8160
+ </body>
8161
+ </html>`;
8162
+ }
8163
+ function esc(str) {
8164
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
8165
+ }
8166
+
8167
+ // src/system-prompt-generator.ts
8168
+ var FEATURE_INFO = {
8169
+ audit_logging: {
8170
+ name: "Audit Logging",
8171
+ 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.",
8172
+ toolNames: ["monitor_audit_log"],
8173
+ disabledDescription: "audit logging (monitor_audit_log)",
8174
+ usageExample: "Automatic \u2014 every tool call you make is recorded. No explicit action required."
8175
+ },
8176
+ injection_detection: {
8177
+ name: "Injection Detection",
8178
+ 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.",
8179
+ disabledDescription: "injection detection",
8180
+ usageExample: "Automatic \u2014 if a tool call is blocked with an injection alert, do not retry with the same arguments."
8181
+ },
8182
+ context_gating: {
8183
+ name: "Context Gating",
8184
+ 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.",
8185
+ toolNames: [
8186
+ "context_gate_filter",
8187
+ "context_gate_set_policy",
8188
+ "context_gate_apply_template",
8189
+ "context_gate_recommend",
8190
+ "context_gate_list_policies"
8191
+ ],
8192
+ disabledDescription: "context gating (context_gate_filter)",
8193
+ usageExample: "Before calling an external API, run: context_gate_filter with your context object and policy_id to get a filtered version."
8194
+ },
8195
+ approval_gate: {
8196
+ name: "Approval Gates",
8197
+ 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.",
8198
+ disabledDescription: "approval gates",
8199
+ 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."
8200
+ },
8201
+ zk_proofs: {
8202
+ name: "Zero-Knowledge Proofs",
8203
+ 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.",
8204
+ toolNames: [
8205
+ "zk_commit",
8206
+ "zk_prove",
8207
+ "zk_range_prove",
8208
+ "proof_commitment"
8209
+ ],
8210
+ disabledDescription: "zero-knowledge proofs (zk_commit, zk_prove)",
8211
+ 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."
8212
+ }
8213
+ };
8214
+ function generateSystemPrompt(profile) {
8215
+ const activeFeatures = [];
8216
+ const inactiveFeatures = [];
8217
+ const activeKeys = [];
8218
+ const featureKeys = [
8219
+ "audit_logging",
8220
+ "injection_detection",
8221
+ "context_gating",
8222
+ "approval_gate",
8223
+ "zk_proofs"
8224
+ ];
8225
+ for (const key of featureKeys) {
8226
+ const featureConfig = profile.features[key];
8227
+ const info = FEATURE_INFO[key];
8228
+ if (featureConfig.enabled) {
8229
+ activeKeys.push(key);
8230
+ let desc = `- ${info.name}: ${info.activeDescription}`;
8231
+ if (key === "injection_detection" && "sensitivity" in featureConfig && featureConfig.sensitivity) {
8232
+ desc += ` Sensitivity: ${featureConfig.sensitivity}.`;
8233
+ }
8234
+ if (key === "context_gating" && "policy_id" in featureConfig && featureConfig.policy_id) {
8235
+ desc += ` Active policy: ${featureConfig.policy_id}.`;
8236
+ }
8237
+ activeFeatures.push(desc);
8238
+ } else {
8239
+ inactiveFeatures.push(info.disabledDescription);
8240
+ }
8241
+ }
8242
+ const lines = [];
8243
+ if (activeKeys.length > 0) {
8244
+ lines.push("QUICK START:");
8245
+ const quickStartItems = buildQuickStart(activeKeys);
8246
+ for (const item of quickStartItems) {
8247
+ lines.push(` ${item}`);
8248
+ }
8249
+ lines.push("");
8250
+ }
8251
+ lines.push(
8252
+ "You are protected by Sanctuary sovereignty infrastructure. The following protections are active:"
8253
+ );
8254
+ lines.push("");
8255
+ if (activeFeatures.length > 0) {
8256
+ lines.push(...activeFeatures);
8257
+ } else {
8258
+ lines.push(
8259
+ "- No features are currently enabled. Contact your operator to configure protections."
8260
+ );
8261
+ }
8262
+ if (inactiveFeatures.length > 0) {
8263
+ lines.push("");
8264
+ lines.push(
8265
+ `Optional tools available but not currently enabled: ${inactiveFeatures.join(", ")}.`
8266
+ );
8267
+ }
8268
+ return lines.join("\n");
8269
+ }
8270
+ function buildQuickStart(activeKeys) {
8271
+ const items = [];
8272
+ if (activeKeys.includes("context_gating")) {
8273
+ items.push(
8274
+ "1. ALWAYS call context_gate_filter before sending context to external APIs."
8275
+ );
8276
+ }
8277
+ if (activeKeys.includes("zk_proofs")) {
8278
+ items.push(
8279
+ `${items.length + 1}. Use zk_commit to prove claims without revealing underlying data.`
8280
+ );
8281
+ }
8282
+ if (activeKeys.includes("approval_gate")) {
8283
+ items.push(
8284
+ `${items.length + 1}. High-risk operations will be held for human approval \u2014 expect async responses.`
8285
+ );
8286
+ }
8287
+ if (items.length === 0) {
8288
+ if (activeKeys.includes("audit_logging")) {
8289
+ items.push("1. All tool calls are automatically logged to an encrypted audit trail.");
8290
+ }
8291
+ if (activeKeys.includes("injection_detection")) {
8292
+ items.push(
8293
+ `${items.length + 1}. Tool arguments are scanned for injection \u2014 blocked calls should not be retried.`
8294
+ );
8295
+ }
8296
+ }
8297
+ return items;
8298
+ }
8299
+
8300
+ // src/principal-policy/dashboard.ts
8301
+ var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
8302
+ var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
8303
+ var MAX_SESSIONS = 1e3;
8304
+ var RATE_LIMIT_WINDOW_MS = 6e4;
8305
+ var RATE_LIMIT_GENERAL = 120;
8306
+ var RATE_LIMIT_DECISIONS = 20;
8307
+ var MAX_RATE_LIMIT_ENTRIES = 1e4;
8308
+ var DashboardApprovalChannel = class {
8309
+ config;
8310
+ pending = /* @__PURE__ */ new Map();
8311
+ sseClients = /* @__PURE__ */ new Set();
8312
+ httpServer = null;
8313
+ policy = null;
8314
+ baseline = null;
8315
+ auditLog = null;
8316
+ identityManager = null;
8317
+ handshakeResults = null;
8318
+ shrOpts = null;
8319
+ _sanctuaryConfig = null;
8320
+ profileStore = null;
8321
+ clientManager = null;
8322
+ dashboardHTML;
8323
+ fortressHTML = null;
8324
+ loginHTML;
8325
+ authToken;
8326
+ useTLS;
8327
+ /** Session TTL: longer for localhost, shorter for remote */
8328
+ sessionTTLMs;
8329
+ /** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
8330
+ sessions = /* @__PURE__ */ new Map();
8331
+ sessionCleanupTimer = null;
8332
+ /** Rate limiting: per-IP request tracking */
8333
+ rateLimits = /* @__PURE__ */ new Map();
8334
+ /** Whether the dashboard is running in standalone mode (no MCP server) */
8335
+ _standaloneMode = false;
8336
+ constructor(config) {
8337
+ this.config = config;
8338
+ this.authToken = config.auth_token;
8339
+ this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
8340
+ const isLocalhost = config.host === "127.0.0.1" || config.host === "localhost" || config.host === "::1";
8341
+ this.sessionTTLMs = isLocalhost ? SESSION_TTL_LOCAL_MS : SESSION_TTL_REMOTE_MS;
8342
+ this.dashboardHTML = generateDashboardHTML({
8343
+ timeoutSeconds: config.timeout_seconds,
8344
+ serverVersion: SANCTUARY_VERSION
8345
+ });
8346
+ this.loginHTML = generateLoginHTML({ serverVersion: SANCTUARY_VERSION });
8347
+ this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
8348
+ }
8349
+ /**
8350
+ * Inject dependencies after construction.
8351
+ * Called from index.ts after all components are initialized.
8352
+ */
8353
+ setDependencies(deps) {
8354
+ this.policy = deps.policy;
8355
+ this.baseline = deps.baseline;
8356
+ this.auditLog = deps.auditLog;
8357
+ if (deps.identityManager) this.identityManager = deps.identityManager;
8358
+ if (deps.handshakeResults) this.handshakeResults = deps.handshakeResults;
8359
+ if (deps.shrOpts) this.shrOpts = deps.shrOpts;
8360
+ if (deps.sanctuaryConfig) this._sanctuaryConfig = deps.sanctuaryConfig;
8361
+ if (deps.profileStore) this.profileStore = deps.profileStore;
8362
+ if (deps.clientManager) this.clientManager = deps.clientManager;
8363
+ }
8364
+ /**
8365
+ * Mark this dashboard as running in standalone mode.
8366
+ * Exposed via /api/status so the frontend can show an appropriate banner.
8367
+ */
8368
+ setStandaloneMode(standalone) {
8369
+ this._standaloneMode = standalone;
8370
+ }
8371
+ /**
8372
+ * Start the HTTP(S) server for the dashboard.
8373
+ */
8374
+ async start() {
8375
+ return new Promise((resolve, reject) => {
8376
+ const handler = (req, res) => this.handleRequest(req, res);
8377
+ if (this.useTLS && this.config.tls) {
8378
+ const tlsOpts = {
8379
+ cert: fs.readFileSync(this.config.tls.cert_path),
8380
+ key: fs.readFileSync(this.config.tls.key_path)
8381
+ };
8382
+ this.httpServer = https.createServer(tlsOpts, handler);
8383
+ } else {
8384
+ this.httpServer = http.createServer(handler);
8385
+ }
8386
+ const protocol = this.useTLS ? "https" : "http";
8387
+ const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
8388
+ this.httpServer.listen(this.config.port, this.config.host, () => {
8389
+ const sessionUrl = this.authToken ? this.createSessionUrl() : baseUrl;
8390
+ process.stderr.write(
8391
+ `
8392
+ Sanctuary Principal Dashboard: ${baseUrl}
8393
+ `
8394
+ );
8395
+ if (this.authToken) {
8396
+ const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
8397
+ process.stderr.write(
8398
+ ` Auth token: ${hint}
8399
+ `
8400
+ );
8401
+ }
8402
+ process.stderr.write(`
7565
8403
  `);
7566
8404
  const isTest = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI);
7567
8405
  const isLocalhost = this.config.host === "127.0.0.1" || this.config.host === "localhost" || this.config.host === "::1";
@@ -7868,8 +8706,14 @@ var DashboardApprovalChannel = class {
7868
8706
  if (!this.checkAuth(req, url, res)) return;
7869
8707
  if (!this.checkRateLimit(req, res, "general")) return;
7870
8708
  try {
7871
- if (method === "GET" && (url.pathname === "/" || url.pathname === "/dashboard")) {
7872
- this.serveDashboard(res);
8709
+ if (method === "GET" && url.pathname === "/fortress") {
8710
+ this.serveFortressView(res);
8711
+ } else if (method === "GET" && (url.pathname === "/" || url.pathname === "/dashboard")) {
8712
+ if (this.fortressHTML) {
8713
+ this.serveFortressView(res);
8714
+ } else {
8715
+ this.serveDashboard(res);
8716
+ }
7873
8717
  } else if (method === "GET" && url.pathname === "/events") {
7874
8718
  this.handleSSE(req, res);
7875
8719
  } else if (method === "GET" && url.pathname === "/api/status") {
@@ -7964,6 +8808,35 @@ var DashboardApprovalChannel = class {
7964
8808
  });
7965
8809
  res.end(this.dashboardHTML);
7966
8810
  }
8811
+ serveFortressView(res) {
8812
+ if (!this.fortressHTML) {
8813
+ this.serveDashboard(res);
8814
+ return;
8815
+ }
8816
+ res.writeHead(200, {
8817
+ "Content-Type": "text/html; charset=utf-8",
8818
+ "Cache-Control": "no-cache"
8819
+ });
8820
+ res.end(this.fortressHTML);
8821
+ }
8822
+ /**
8823
+ * Enable Fortress View (Cocoon mode) with the given upstream server count.
8824
+ * Once enabled, the root path `/` serves the Fortress View instead of the
8825
+ * standard dashboard. The standard dashboard remains available at `/dashboard`.
8826
+ */
8827
+ enableFortressView(upstreamServerCount) {
8828
+ this.fortressHTML = generateFortressViewHTML({
8829
+ serverVersion: SANCTUARY_VERSION,
8830
+ authToken: this.authToken,
8831
+ upstreamServerCount
8832
+ });
8833
+ }
8834
+ /**
8835
+ * Broadcast a proxy call event to connected dashboards (Fortress View feed).
8836
+ */
8837
+ broadcastProxyCall(data) {
8838
+ this.broadcastSSE("proxy-call", data);
8839
+ }
7967
8840
  handleSSE(req, res) {
7968
8841
  res.writeHead(200, {
7969
8842
  "Content-Type": "text/event-stream",
@@ -8827,7 +9700,7 @@ var InjectionDetector = class {
8827
9700
  }
8828
9701
  /**
8829
9702
  * Scan tool arguments for injection signals.
8830
- * @param toolName Full tool name (e.g., "sanctuary/state_read")
9703
+ * @param toolName Full tool name (e.g., "state_read")
8831
9704
  * @param args Tool arguments
8832
9705
  * @returns DetectionResult with all detected signals
8833
9706
  */
@@ -9828,7 +10701,7 @@ var ApprovalGate = class {
9828
10701
  /**
9829
10702
  * Evaluate a tool call against the Principal Policy.
9830
10703
  *
9831
- * @param toolName - Full MCP tool name (e.g., "sanctuary/state_export")
10704
+ * @param toolName - Full MCP tool name (e.g., "state_export")
9832
10705
  * @param args - Tool call arguments (for context extraction)
9833
10706
  * @returns GateResult indicating whether the call is allowed
9834
10707
  */
@@ -10097,7 +10970,7 @@ var ApprovalGate = class {
10097
10970
  function createPrincipalPolicyTools(policy, baseline, auditLog) {
10098
10971
  return [
10099
10972
  {
10100
- name: "sanctuary/principal_policy_view",
10973
+ name: "principal_policy_view",
10101
10974
  description: "View the current Principal Policy \u2014 the human-controlled rules governing what operations require approval. Read-only.",
10102
10975
  inputSchema: {
10103
10976
  type: "object",
@@ -10135,7 +11008,7 @@ function createPrincipalPolicyTools(policy, baseline, auditLog) {
10135
11008
  }
10136
11009
  },
10137
11010
  {
10138
- name: "sanctuary/principal_baseline_view",
11011
+ name: "principal_baseline_view",
10139
11012
  description: "View the current behavioral baseline \u2014 the session profile used for anomaly detection. Shows known namespaces, counterparties, and tool call counts. Read-only.",
10140
11013
  inputSchema: {
10141
11014
  type: "object",
@@ -10475,7 +11348,7 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
10475
11348
  };
10476
11349
  const tools = [
10477
11350
  {
10478
- name: "sanctuary/shr_generate",
11351
+ name: "shr_generate",
10479
11352
  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.",
10480
11353
  inputSchema: {
10481
11354
  type: "object",
@@ -10504,7 +11377,7 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
10504
11377
  }
10505
11378
  },
10506
11379
  {
10507
- name: "sanctuary/shr_verify",
11380
+ name: "shr_verify",
10508
11381
  description: "Verify a counterparty's Sovereignty Health Report (SHR). Checks signature validity, temporal validity, and assesses sovereignty level.",
10509
11382
  inputSchema: {
10510
11383
  type: "object",
@@ -10530,7 +11403,7 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
10530
11403
  }
10531
11404
  },
10532
11405
  {
10533
- name: "sanctuary/shr_gateway_export",
11406
+ name: "shr_gateway_export",
10534
11407
  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.",
10535
11408
  inputSchema: {
10536
11409
  type: "object",
@@ -10580,6 +11453,10 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
10580
11453
  return { tools };
10581
11454
  }
10582
11455
 
11456
+ // src/handshake/tools.ts
11457
+ init_identity();
11458
+ init_encoding();
11459
+
10583
11460
  // src/handshake/protocol.ts
10584
11461
  init_identity();
10585
11462
  init_encoding();
@@ -10900,7 +11777,10 @@ function verifyAttestation(attestation, now) {
10900
11777
  }
10901
11778
 
10902
11779
  // src/handshake/tools.ts
10903
- function createHandshakeTools(config, identityManager, masterKey, auditLog) {
11780
+ function createHandshakeTools(config, identityManager, masterKey, auditLog, options) {
11781
+ const autoPublishHandshakes = options?.autoPublishHandshakes ?? false;
11782
+ const verascoreUrl = options?.verascoreUrl ?? "https://verascore.ai";
11783
+ const identityEncKey = derivePurposeKey(masterKey, "identity-encryption");
10904
11784
  const sessions = /* @__PURE__ */ new Map();
10905
11785
  const handshakeResults = /* @__PURE__ */ new Map();
10906
11786
  const shrOpts = {
@@ -10910,7 +11790,7 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
10910
11790
  };
10911
11791
  const tools = [
10912
11792
  {
10913
- name: "sanctuary/handshake_initiate",
11793
+ name: "handshake_initiate",
10914
11794
  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.",
10915
11795
  inputSchema: {
10916
11796
  type: "object",
@@ -10937,7 +11817,7 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
10937
11817
  }
10938
11818
  },
10939
11819
  {
10940
- name: "sanctuary/handshake_respond",
11820
+ name: "handshake_respond",
10941
11821
  description: "Respond to an incoming sovereignty handshake challenge. Verifies the initiator's SHR, signs their nonce, and returns our SHR with a counter-nonce.",
10942
11822
  inputSchema: {
10943
11823
  type: "object",
@@ -10972,17 +11852,93 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
10972
11852
  }
10973
11853
  sessions.set(result.session.session_id, result.session);
10974
11854
  auditLog.append("l4", "handshake_respond", shr.body.instance_id);
11855
+ let autoPublishResult;
11856
+ if (autoPublishHandshakes) {
11857
+ autoPublishResult = { attempted: true };
11858
+ try {
11859
+ const parsed = new URL(verascoreUrl);
11860
+ if (parsed.protocol !== "https:") {
11861
+ autoPublishResult.error = `verascore URL must use HTTPS (got ${parsed.protocol})`;
11862
+ } else {
11863
+ const attestationPayload = {
11864
+ type: "handshake",
11865
+ our_shr_signed_by: shr.signed_by,
11866
+ counterparty_signed_by: "redacted",
11867
+ session_id: result.session.session_id,
11868
+ responded_at: (/* @__PURE__ */ new Date()).toISOString()
11869
+ };
11870
+ const responderIdentity = identityManager.get(shr.body.instance_id);
11871
+ if (!responderIdentity) {
11872
+ autoPublishResult.error = `responder identity ${shr.body.instance_id} not found; skipping auto-publish`;
11873
+ auditLog.append(
11874
+ "l4",
11875
+ "handshake_auto_publish",
11876
+ shr.body.instance_id,
11877
+ { error: autoPublishResult.error },
11878
+ "failure"
11879
+ );
11880
+ } else {
11881
+ const payloadBytes = new TextEncoder().encode(
11882
+ JSON.stringify(attestationPayload)
11883
+ );
11884
+ const sigBytes = sign(
11885
+ payloadBytes,
11886
+ responderIdentity.encrypted_private_key,
11887
+ identityEncKey
11888
+ );
11889
+ const signatureB64 = toBase64url(sigBytes);
11890
+ const resp = await fetch(
11891
+ `${verascoreUrl.replace(/\/$/, "")}/api/publish`,
11892
+ {
11893
+ method: "POST",
11894
+ headers: { "Content-Type": "application/json" },
11895
+ body: JSON.stringify({
11896
+ agentId: shr.body.instance_id,
11897
+ publicKey: shr.signed_by,
11898
+ signature: signatureB64,
11899
+ type: "handshake",
11900
+ data: attestationPayload
11901
+ })
11902
+ }
11903
+ );
11904
+ autoPublishResult.ok = resp.ok;
11905
+ autoPublishResult.status = resp.status;
11906
+ auditLog.append(
11907
+ "l4",
11908
+ "handshake_auto_publish",
11909
+ shr.body.instance_id,
11910
+ {
11911
+ verascore_url: verascoreUrl,
11912
+ status: resp.status,
11913
+ ok: resp.ok
11914
+ },
11915
+ resp.ok ? "success" : "failure"
11916
+ );
11917
+ }
11918
+ }
11919
+ } catch (err) {
11920
+ autoPublishResult.error = err instanceof Error ? err.message : String(err);
11921
+ auditLog.append(
11922
+ "l4",
11923
+ "handshake_auto_publish",
11924
+ shr.body.instance_id,
11925
+ { verascore_url: verascoreUrl, error: autoPublishResult.error },
11926
+ "failure"
11927
+ );
11928
+ }
11929
+ }
10975
11930
  return toolResult({
10976
11931
  session_id: result.session.session_id,
10977
11932
  response: result.response,
10978
11933
  instructions: "Send the 'response' object back to the initiator. When you receive their completion, pass it to sanctuary/handshake_status with this session_id.",
11934
+ auto_publish: autoPublishResult,
10979
11935
  // SEC-ADD-03: Tag response — contains SHR data that will be sent to counterparty
10980
11936
  _content_trust: "external"
10981
11937
  });
10982
11938
  }
10983
11939
  },
10984
11940
  {
10985
- name: "sanctuary/handshake_complete",
11941
+ name: "handshake_complete",
10986
11942
  description: "Complete a sovereignty handshake (initiator side). Verifies the responder's SHR and nonce signature, signs their nonce, and produces the final result.",
10987
11943
  inputSchema: {
10988
11944
  type: "object",
@@ -11037,7 +11993,7 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
11037
11993
  }
11038
11994
  },
11039
11995
  {
11040
- name: "sanctuary/handshake_status",
11996
+ name: "handshake_status",
11041
11997
  description: "Check the status of a handshake session, or verify a completion message (responder side).",
11042
11998
  inputSchema: {
11043
11999
  type: "object",
@@ -11087,7 +12043,7 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
11087
12043
  },
11088
12044
  // ─── Streamlined Exchange ─────────────────────────────────────────
11089
12045
  {
11090
- name: "sanctuary/handshake_exchange",
12046
+ name: "handshake_exchange",
11091
12047
  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).",
11092
12048
  inputSchema: {
11093
12049
  type: "object",
@@ -11154,7 +12110,7 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
11154
12110
  }
11155
12111
  },
11156
12112
  {
11157
- name: "sanctuary/handshake_verify_attestation",
12113
+ name: "handshake_verify_attestation",
11158
12114
  description: "Verify a signed attestation artifact from another agent. Checks the Ed25519 signature, temporal validity, and structural integrity.",
11159
12115
  inputSchema: {
11160
12116
  type: "object",
@@ -11363,7 +12319,7 @@ function createFederationTools(auditLog, handshakeResults) {
11363
12319
  const tools = [
11364
12320
  // ─── Peer Management ──────────────────────────────────────────────
11365
12321
  {
11366
- name: "sanctuary/federation_peers",
12322
+ name: "federation_peers",
11367
12323
  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.",
11368
12324
  inputSchema: {
11369
12325
  type: "object",
@@ -11466,7 +12422,7 @@ function createFederationTools(auditLog, handshakeResults) {
11466
12422
  },
11467
12423
  // ─── Trust Evaluation ─────────────────────────────────────────────
11468
12424
  {
11469
- name: "sanctuary/federation_trust_evaluate",
12425
+ name: "federation_trust_evaluate",
11470
12426
  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.",
11471
12427
  inputSchema: {
11472
12428
  type: "object",
@@ -11501,7 +12457,7 @@ function createFederationTools(auditLog, handshakeResults) {
11501
12457
  },
11502
12458
  // ─── Federation Status ────────────────────────────────────────────
11503
12459
  {
11504
- name: "sanctuary/federation_status",
12460
+ name: "federation_status",
11505
12461
  description: "Overview of federation state: total peers, active connections, trust distribution, and readiness for cross-instance operations.",
11506
12462
  inputSchema: {
11507
12463
  type: "object",
@@ -11712,7 +12668,7 @@ function createBridgeTools(storage, masterKey, identityManager, auditLog, handsh
11712
12668
  const tools = [
11713
12669
  // ─── bridge_commit ─────────────────────────────────────────────────
11714
12670
  {
11715
- name: "sanctuary/bridge_commit",
12671
+ name: "bridge_commit",
11716
12672
  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.",
11717
12673
  inputSchema: {
11718
12674
  type: "object",
@@ -11814,7 +12770,7 @@ function createBridgeTools(storage, masterKey, identityManager, auditLog, handsh
11814
12770
  },
11815
12771
  // ─── bridge_verify ───────────────────────────────────────────────────
11816
12772
  {
11817
- name: "sanctuary/bridge_verify",
12773
+ name: "bridge_verify",
11818
12774
  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.",
11819
12775
  inputSchema: {
11820
12776
  type: "object",
@@ -11870,7 +12826,7 @@ function createBridgeTools(storage, masterKey, identityManager, auditLog, handsh
11870
12826
  },
11871
12827
  // ─── bridge_attest ───────────────────────────────────────────────────
11872
12828
  {
11873
- name: "sanctuary/bridge_attest",
12829
+ name: "bridge_attest",
11874
12830
  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.",
11875
12831
  inputSchema: {
11876
12832
  type: "object",
@@ -12434,7 +13390,7 @@ function generateGaps(env, l1, l2, l3, l4) {
12434
13390
  title: "No context gating for outbound inference calls",
12435
13391
  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.",
12436
13392
  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,
12437
- sanctuary_solution: "Sanctuary's context gating (sanctuary/context_gate_set_policy + sanctuary/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.",
13393
+ 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.",
12438
13394
  incident_class: INCIDENT_CONTEXT_LEAKAGE
12439
13395
  });
12440
13396
  }
@@ -12446,7 +13402,7 @@ function generateGaps(env, l1, l2, l3, l4) {
12446
13402
  title: "No audit trail",
12447
13403
  description: "No audit trail exists for tool call history. There is no record of what operations were executed, when, or by whom.",
12448
13404
  openclaw_relevance: null,
12449
- sanctuary_solution: "Sanctuary maintains an encrypted audit log of all operations, queryable via sanctuary/monitor_audit_log.",
13405
+ sanctuary_solution: "Sanctuary maintains an encrypted audit log of all operations, queryable via monitor_audit_log.",
12450
13406
  incident_class: INCIDENT_CLAUDE_CODE_LEAK
12451
13407
  });
12452
13408
  }
@@ -12481,7 +13437,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
12481
13437
  recs.push({
12482
13438
  priority: 1,
12483
13439
  action: "Create a cryptographic identity \u2014 your agent's foundation for all sovereignty operations",
12484
- tool: "sanctuary/identity_create",
13440
+ tool: "identity_create",
12485
13441
  effort: "immediate",
12486
13442
  impact: "critical"
12487
13443
  });
@@ -12490,7 +13446,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
12490
13446
  recs.push({
12491
13447
  priority: 2,
12492
13448
  action: "Migrate plaintext agent state to Sanctuary's encrypted store",
12493
- tool: "sanctuary/state_write",
13449
+ tool: "state_write",
12494
13450
  effort: "minutes",
12495
13451
  impact: "critical"
12496
13452
  });
@@ -12498,7 +13454,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
12498
13454
  recs.push({
12499
13455
  priority: 3,
12500
13456
  action: "Generate a Sovereignty Health Report to present to counterparties",
12501
- tool: "sanctuary/shr_generate",
13457
+ tool: "shr_generate",
12502
13458
  effort: "immediate",
12503
13459
  impact: "high"
12504
13460
  });
@@ -12506,7 +13462,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
12506
13462
  recs.push({
12507
13463
  priority: 4,
12508
13464
  action: "Enable the three-tier Principal Policy gate for graduated approval",
12509
- tool: "sanctuary/principal_policy_view",
13465
+ tool: "principal_policy_view",
12510
13466
  effort: "minutes",
12511
13467
  impact: "high"
12512
13468
  });
@@ -12515,7 +13471,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
12515
13471
  recs.push({
12516
13472
  priority: 5,
12517
13473
  action: "Configure context gating to control what flows to LLM providers",
12518
- tool: "sanctuary/context_gate_set_policy",
13474
+ tool: "context_gate_set_policy",
12519
13475
  effort: "minutes",
12520
13476
  impact: "high"
12521
13477
  });
@@ -12524,7 +13480,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
12524
13480
  recs.push({
12525
13481
  priority: 6,
12526
13482
  action: "Start recording reputation attestations from completed interactions",
12527
- tool: "sanctuary/reputation_record",
13483
+ tool: "reputation_record",
12528
13484
  effort: "minutes",
12529
13485
  impact: "medium"
12530
13486
  });
@@ -12533,7 +13489,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
12533
13489
  recs.push({
12534
13490
  priority: 7,
12535
13491
  action: "Configure selective disclosure policies for data sharing",
12536
- tool: "sanctuary/disclosure_set_policy",
13492
+ tool: "disclosure_set_policy",
12537
13493
  effort: "hours",
12538
13494
  impact: "medium"
12539
13495
  });
@@ -12673,7 +13629,7 @@ function wordWrap(text, maxWidth) {
12673
13629
  function createAuditTools(config) {
12674
13630
  const tools = [
12675
13631
  {
12676
- name: "sanctuary/sovereignty_audit",
13632
+ name: "sovereignty_audit",
12677
13633
  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.",
12678
13634
  inputSchema: {
12679
13635
  type: "object",
@@ -12683,16 +13639,312 @@ function createAuditTools(config) {
12683
13639
  description: "If true (default), also scans for OpenClaw config, .env files, and memory files. Set to false for a Sanctuary-only assessment."
12684
13640
  }
12685
13641
  }
12686
- },
12687
- handler: async (args) => {
12688
- const deepScan = args.deep_scan !== false;
12689
- const env = await detectEnvironment(config, deepScan);
12690
- const result = analyzeSovereignty(env, config);
12691
- const report = formatAuditReport(result);
13642
+ },
13643
+ handler: async (args) => {
13644
+ const deepScan = args.deep_scan !== false;
13645
+ const env = await detectEnvironment(config, deepScan);
13646
+ const result = analyzeSovereignty(env, config);
13647
+ const report = formatAuditReport(result);
13648
+ return {
13649
+ content: [
13650
+ { type: "text", text: report },
13651
+ { type: "text", text: JSON.stringify(result, null, 2) }
13652
+ ]
13653
+ };
13654
+ }
13655
+ }
13656
+ ];
13657
+ return { tools };
13658
+ }
13659
+
13660
+ // src/audit/siem-formatter.ts
13661
+ function parseGateDecision(details) {
13662
+ if (!details || typeof details.gate_decision !== "string") {
13663
+ return "auto-allow";
13664
+ }
13665
+ const decision = details.gate_decision.toLowerCase();
13666
+ if (decision === "approve" || decision === "deny") {
13667
+ return decision;
13668
+ }
13669
+ return "auto-allow";
13670
+ }
13671
+ function parseTier(details) {
13672
+ if (!details || typeof details.tier !== "number") {
13673
+ return 3;
13674
+ }
13675
+ return Math.max(1, Math.min(3, details.tier));
13676
+ }
13677
+ function parseSessionId(details) {
13678
+ if (!details || typeof details.session_id !== "string") {
13679
+ return "unknown";
13680
+ }
13681
+ return details.session_id;
13682
+ }
13683
+ function parseAgentDid(details) {
13684
+ if (!details || typeof details.agent_did !== "string") {
13685
+ return "unknown";
13686
+ }
13687
+ return details.agent_did;
13688
+ }
13689
+ function gateToCEFSeverity(decision, tier) {
13690
+ if (decision === "deny") {
13691
+ return 8;
13692
+ }
13693
+ if (decision === "approve") {
13694
+ if (tier === 1) return 5;
13695
+ if (tier === 2) return 3;
13696
+ }
13697
+ return 1;
13698
+ }
13699
+ function formatAsCEF(entry, options) {
13700
+ const version = "0";
13701
+ const vendor = "Sanctuary";
13702
+ const product = "MCP-Server";
13703
+ const productVersion = "0.7.0";
13704
+ const decision = parseGateDecision(entry.details);
13705
+ const tier = parseTier(entry.details);
13706
+ const sessionId = parseSessionId(entry.details);
13707
+ const agentDid = parseAgentDid(entry.details);
13708
+ const severity = gateToCEFSeverity(decision, tier);
13709
+ const signatureId = entry.operation.replace(/[^a-zA-Z0-9_-]/g, "_");
13710
+ const description = `Sanctuary ${entry.operation}`;
13711
+ const extensions = [
13712
+ `src=${agentDid}`,
13713
+ `act=${entry.operation}`,
13714
+ `outcome=${decision}`,
13715
+ `tier=${tier}`,
13716
+ `cs1=${sessionId}`,
13717
+ `cs1Label=SessionId`,
13718
+ `rt=${new Date(entry.timestamp).getTime()}`,
13719
+ `layer=${entry.layer}`,
13720
+ `result=${entry.result}`
13721
+ ];
13722
+ return `CEF:${version}|${vendor}|${product}|${productVersion}|${signatureId}|${description}|${severity}|${extensions.join(" ")}`;
13723
+ }
13724
+ function gateToOCSFStatus(decision, result) {
13725
+ return decision === "deny" || result === "failure" ? 2 : 1;
13726
+ }
13727
+ function gateToCOCSFSeverity(decision, tier) {
13728
+ if (decision === "deny") {
13729
+ return 4;
13730
+ }
13731
+ if (decision === "approve") {
13732
+ if (tier === 1) return 3;
13733
+ if (tier === 2) return 2;
13734
+ }
13735
+ return 1;
13736
+ }
13737
+ function gateToOCSFDisposition(decision) {
13738
+ return decision === "deny" ? 2 : 1;
13739
+ }
13740
+ function formatAsOCSF(entry) {
13741
+ const decision = parseGateDecision(entry.details);
13742
+ const tier = parseTier(entry.details);
13743
+ const agentDid = parseAgentDid(entry.details);
13744
+ const timestamp = new Date(entry.timestamp).getTime();
13745
+ const statusId = gateToOCSFStatus(decision, entry.result);
13746
+ const severityId = gateToCOCSFSeverity(decision, tier);
13747
+ const dispositionId = gateToOCSFDisposition(decision);
13748
+ return {
13749
+ class_uid: 3001,
13750
+ class_name: "API Activity",
13751
+ category_uid: 3,
13752
+ category_name: "Application Activity",
13753
+ severity_id: severityId,
13754
+ time: timestamp,
13755
+ activity_id: 1,
13756
+ activity_name: "API Call",
13757
+ actor: {
13758
+ user: {
13759
+ uid: agentDid
13760
+ }
13761
+ },
13762
+ api: {
13763
+ operation: entry.operation,
13764
+ service: {
13765
+ name: "sanctuary-mcp"
13766
+ }
13767
+ },
13768
+ status_id: statusId,
13769
+ disposition_id: dispositionId,
13770
+ metadata: {
13771
+ version: "1.3.0",
13772
+ product: {
13773
+ name: "Sanctuary Framework",
13774
+ vendor_name: "Erik Newton",
13775
+ version: "0.7.0"
13776
+ }
13777
+ }
13778
+ };
13779
+ }
13780
+
13781
+ // src/audit/siem-tools.ts
13782
+ function createSIEMTools(auditLog) {
13783
+ const tools = [
13784
+ {
13785
+ name: "audit_export_siem",
13786
+ 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.",
13787
+ inputSchema: {
13788
+ type: "object",
13789
+ properties: {
13790
+ format: {
13791
+ type: "string",
13792
+ enum: ["cef", "ocsf"],
13793
+ description: 'Output format: "cef" (Common Event Format, newline-delimited) or "ocsf" (Open Cybersecurity Schema Framework, JSON array)'
13794
+ },
13795
+ since: {
13796
+ type: "string",
13797
+ description: "Optional ISO 8601 timestamp. Export only events on or after this time. Defaults to 24 hours ago."
13798
+ },
13799
+ until: {
13800
+ type: "string",
13801
+ description: "Optional ISO 8601 timestamp. Export only events before this time. Defaults to now."
13802
+ },
13803
+ limit: {
13804
+ type: "number",
13805
+ description: "Maximum number of events to export (default 100, max 1000). Set to 1000 for bulk exports to SIEMs."
13806
+ },
13807
+ filter_tool: {
13808
+ type: "string",
13809
+ description: 'Optional. Export only events from this tool name (e.g., "sovereignty_audit", "state_set"). Case-insensitive substring matching.'
13810
+ },
13811
+ filter_decision: {
13812
+ type: "string",
13813
+ enum: ["approve", "deny", "auto-allow"],
13814
+ description: 'Optional. Export only events with this gate decision: "approve" (manual approval), "deny" (blocked), or "auto-allow" (Tier 3 auto-allowed).'
13815
+ },
13816
+ filter_layer: {
13817
+ type: "string",
13818
+ enum: ["l1", "l2", "l3", "l4"],
13819
+ description: "Optional. Export only events from this sovereignty layer (L1=Cognitive, L2=Operational, L3=Disclosure, L4=Reputation)."
13820
+ },
13821
+ filter_result: {
13822
+ type: "string",
13823
+ enum: ["success", "failure"],
13824
+ description: 'Optional. Export only events with this result: "success" or "failure".'
13825
+ }
13826
+ },
13827
+ required: ["format"]
13828
+ },
13829
+ handler: async (args) => {
13830
+ const format = String(args.format || "").toLowerCase();
13831
+ if (format !== "cef" && format !== "ocsf") {
13832
+ return {
13833
+ content: [
13834
+ {
13835
+ type: "text",
13836
+ text: JSON.stringify({
13837
+ error: "Invalid format. Must be 'cef' or 'ocsf'."
13838
+ })
13839
+ }
13840
+ ]
13841
+ };
13842
+ }
13843
+ let since;
13844
+ if (args.since) {
13845
+ since = String(args.since);
13846
+ const sinceDate = new Date(since);
13847
+ if (isNaN(sinceDate.getTime())) {
13848
+ return {
13849
+ content: [
13850
+ {
13851
+ type: "text",
13852
+ text: JSON.stringify({
13853
+ error: `Invalid 'since' timestamp: ${since}. Must be ISO 8601.`
13854
+ })
13855
+ }
13856
+ ]
13857
+ };
13858
+ }
13859
+ } else {
13860
+ const now = /* @__PURE__ */ new Date();
13861
+ const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1e3);
13862
+ since = oneDayAgo.toISOString();
13863
+ }
13864
+ let until;
13865
+ if (args.until) {
13866
+ until = String(args.until);
13867
+ const untilDate = new Date(until);
13868
+ if (isNaN(untilDate.getTime())) {
13869
+ return {
13870
+ content: [
13871
+ {
13872
+ type: "text",
13873
+ text: JSON.stringify({
13874
+ error: `Invalid 'until' timestamp: ${until}. Must be ISO 8601.`
13875
+ })
13876
+ }
13877
+ ]
13878
+ };
13879
+ }
13880
+ }
13881
+ let limit = 100;
13882
+ if (typeof args.limit === "number") {
13883
+ limit = Math.max(1, Math.min(1e3, args.limit));
13884
+ }
13885
+ const filterTool = args.filter_tool ? String(args.filter_tool).toLowerCase() : void 0;
13886
+ const filterDecision = args.filter_decision ? String(args.filter_decision).toLowerCase() : void 0;
13887
+ const filterLayer = args.filter_layer ? String(args.filter_layer).toLowerCase() : void 0;
13888
+ const filterResult = args.filter_result ? String(args.filter_result).toLowerCase() : void 0;
13889
+ const result = await auditLog.query({
13890
+ since,
13891
+ layer: filterLayer,
13892
+ operation_type: void 0,
13893
+ // Will filter after
13894
+ limit
13895
+ });
13896
+ let filtered = result.entries;
13897
+ if (filterTool) {
13898
+ filtered = filtered.filter(
13899
+ (e) => e.operation.toLowerCase().includes(filterTool)
13900
+ );
13901
+ }
13902
+ if (filterDecision) {
13903
+ filtered = filtered.filter((e) => {
13904
+ const decision = String(e.details?.gate_decision || "auto-allow").toLowerCase();
13905
+ return decision === filterDecision;
13906
+ });
13907
+ }
13908
+ if (filterResult) {
13909
+ filtered = filtered.filter((e) => e.result === filterResult);
13910
+ }
13911
+ if (until) {
13912
+ const untilDate = new Date(until);
13913
+ filtered = filtered.filter((e) => new Date(e.timestamp) < untilDate);
13914
+ }
13915
+ let output;
13916
+ if (format === "cef") {
13917
+ const cefLines = filtered.map((entry) => formatAsCEF(entry));
13918
+ output = cefLines.join("\n");
13919
+ } else {
13920
+ const ocsfObjects = filtered.map((entry) => formatAsOCSF(entry));
13921
+ output = JSON.stringify(ocsfObjects, null, 2);
13922
+ }
12692
13923
  return {
12693
13924
  content: [
12694
- { type: "text", text: report },
12695
- { type: "text", text: JSON.stringify(result, null, 2) }
13925
+ {
13926
+ type: "text",
13927
+ text: JSON.stringify({
13928
+ format,
13929
+ count: filtered.length,
13930
+ total_available: result.total,
13931
+ time_range: {
13932
+ since,
13933
+ until: until || (/* @__PURE__ */ new Date()).toISOString()
13934
+ },
13935
+ filters: {
13936
+ tool: filterTool,
13937
+ decision: filterDecision,
13938
+ layer: filterLayer,
13939
+ result: filterResult
13940
+ },
13941
+ note: format === "cef" ? `${filtered.length} CEF events (newline-delimited). Each line is a complete CEF event.` : `${filtered.length} OCSF objects in JSON array format.`
13942
+ })
13943
+ },
13944
+ {
13945
+ type: "text",
13946
+ text: output
13947
+ }
12696
13948
  ]
12697
13949
  };
12698
13950
  }
@@ -13697,13 +14949,17 @@ var ContextGateEnforcer = class {
13697
14949
  * Check if a tool should be filtered based on bypass prefixes.
13698
14950
  *
13699
14951
  * SEC-033: Uses exact namespace component matching, not bare startsWith().
13700
- * A prefix of "sanctuary/" matches "sanctuary/state_read" but NOT
13701
- * "sanctuary_evil/steal_data" (no slash boundary confusion). The prefix
13702
- * must match exactly up to its length, and the prefix must end with "/"
13703
- * to enforce namespace boundaries (if it doesn't, we add one for safety).
14952
+ * A prefix of "proxy/" matches "proxy/server/tool" but NOT "proxyevil/steal".
14953
+ * The prefix must match exactly up to its length, and the prefix must end
14954
+ * with "/" to enforce namespace boundaries (if it doesn't, we add one).
14955
+ *
14956
+ * Special sentinel: "*" bypasses ALL tools (used when all Sanctuary-internal
14957
+ * tools should skip context gating — the default). Only proxy/external tools
14958
+ * should be filtered in production.
13704
14959
  */
13705
14960
  shouldFilter(toolName) {
13706
14961
  for (const prefix of this.config.bypass_prefixes) {
14962
+ if (prefix === "*") return false;
13707
14963
  const safePrefix = prefix.endsWith("/") ? prefix : prefix + "/";
13708
14964
  if (toolName === safePrefix.slice(0, -1) || toolName.startsWith(safePrefix)) {
13709
14965
  return false;
@@ -13800,8 +15056,8 @@ function createContextGateTools(storage, masterKey, auditLog) {
13800
15056
  const enforcerConfig = {
13801
15057
  enabled: false,
13802
15058
  // Off by default; agents must explicitly enable it
13803
- bypass_prefixes: ["sanctuary/"],
13804
- // Skip internal tools by default
15059
+ bypass_prefixes: ["*"],
15060
+ // Skip all Sanctuary-internal tools; only proxy/ tools get filtered
13805
15061
  log_only: false,
13806
15062
  // Filter immediately
13807
15063
  on_deny: "block"
@@ -13811,7 +15067,7 @@ function createContextGateTools(storage, masterKey, auditLog) {
13811
15067
  const tools = [
13812
15068
  // ── Set Policy ──────────────────────────────────────────────────
13813
15069
  {
13814
- name: "sanctuary/context_gate_set_policy",
15070
+ name: "context_gate_set_policy",
13815
15071
  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.",
13816
15072
  inputSchema: {
13817
15073
  type: "object",
@@ -13920,13 +15176,13 @@ function createContextGateTools(storage, masterKey, auditLog) {
13920
15176
  rules: policy.rules,
13921
15177
  default_action: policy.default_action,
13922
15178
  created_at: policy.created_at,
13923
- message: "Context-gating policy created. Use sanctuary/context_gate_filter to apply this policy before making outbound calls."
15179
+ message: "Context-gating policy created. Use context_gate_filter to apply this policy before making outbound calls."
13924
15180
  });
13925
15181
  }
13926
15182
  },
13927
15183
  // ── Apply Template ───────────────────────────────────────────────
13928
15184
  {
13929
- name: "sanctuary/context_gate_apply_template",
15185
+ name: "context_gate_apply_template",
13930
15186
  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.",
13931
15187
  inputSchema: {
13932
15188
  type: "object",
@@ -13975,13 +15231,13 @@ function createContextGateTools(storage, masterKey, auditLog) {
13975
15231
  rules: policy.rules,
13976
15232
  default_action: policy.default_action,
13977
15233
  created_at: policy.created_at,
13978
- message: "Template applied. Use sanctuary/context_gate_filter with this policy_id to filter context before outbound calls. Customize rules with sanctuary/context_gate_set_policy if needed."
15234
+ 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."
13979
15235
  });
13980
15236
  }
13981
15237
  },
13982
15238
  // ── Recommend Policy ────────────────────────────────────────────
13983
15239
  {
13984
- name: "sanctuary/context_gate_recommend",
15240
+ name: "context_gate_recommend",
13985
15241
  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.",
13986
15242
  inputSchema: {
13987
15243
  type: "object",
@@ -14018,7 +15274,7 @@ function createContextGateTools(storage, masterKey, auditLog) {
14018
15274
  });
14019
15275
  return toolResult({
14020
15276
  ...recommendation,
14021
- next_steps: "Review the classifications above. If they look correct, you can apply them directly with sanctuary/context_gate_set_policy using the recommended_rules. Or start with a template via sanctuary/context_gate_apply_template and customize from there.",
15277
+ 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.",
14022
15278
  available_templates: listTemplateIds().map((id) => {
14023
15279
  const t = TEMPLATES[id];
14024
15280
  return { id, name: t.name, description: t.description };
@@ -14028,7 +15284,7 @@ function createContextGateTools(storage, masterKey, auditLog) {
14028
15284
  },
14029
15285
  // ── Filter Context ──────────────────────────────────────────────
14030
15286
  {
14031
- name: "sanctuary/context_gate_filter",
15287
+ name: "context_gate_filter",
14032
15288
  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.",
14033
15289
  inputSchema: {
14034
15290
  type: "object",
@@ -14134,7 +15390,7 @@ function createContextGateTools(storage, masterKey, auditLog) {
14134
15390
  },
14135
15391
  // ── List Policies ───────────────────────────────────────────────
14136
15392
  {
14137
- name: "sanctuary/context_gate_list_policies",
15393
+ name: "context_gate_list_policies",
14138
15394
  description: "List all configured context-gating policies. Returns policy IDs, names, rule summaries, and default actions.",
14139
15395
  inputSchema: {
14140
15396
  type: "object",
@@ -14157,13 +15413,13 @@ function createContextGateTools(storage, masterKey, auditLog) {
14157
15413
  updated_at: p.updated_at
14158
15414
  })),
14159
15415
  count: policies.length,
14160
- message: policies.length === 0 ? "No context-gating policies configured. Use sanctuary/context_gate_set_policy to create one." : `${policies.length} context-gating ${policies.length === 1 ? "policy" : "policies"} configured.`
15416
+ 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.`
14161
15417
  });
14162
15418
  }
14163
15419
  },
14164
15420
  // ── Enforcer Status ─────────────────────────────────────────────────
14165
15421
  {
14166
- name: "sanctuary/context_gate_enforcer_status",
15422
+ name: "context_gate_enforcer_status",
14167
15423
  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.",
14168
15424
  inputSchema: {
14169
15425
  type: "object",
@@ -14184,13 +15440,13 @@ function createContextGateTools(storage, masterKey, auditLog) {
14184
15440
  return toolResult({
14185
15441
  enforcer_status: status,
14186
15442
  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."),
14187
- guidance: status.stats.calls_inspected > 0 ? `Over ${status.stats.calls_inspected} tool calls, ${status.stats.fields_redacted} sensitive fields were redacted. Use sanctuary/context_gate_enforcer_configure to adjust settings.` : "No tool calls have been inspected yet."
15443
+ 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."
14188
15444
  });
14189
15445
  }
14190
15446
  },
14191
15447
  // ── Enforcer Configuration ──────────────────────────────────────────
14192
15448
  {
14193
- name: "sanctuary/context_gate_enforcer_configure",
15449
+ name: "context_gate_enforcer_configure",
14194
15450
  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.",
14195
15451
  inputSchema: {
14196
15452
  type: "object",
@@ -14513,7 +15769,7 @@ function assessL2Hardening(storagePath) {
14513
15769
  function createL2HardeningTools(storagePath, auditLog) {
14514
15770
  return [
14515
15771
  {
14516
- name: "sanctuary/l2_hardening_status",
15772
+ name: "l2_hardening_status",
14517
15773
  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.",
14518
15774
  inputSchema: {
14519
15775
  type: "object",
@@ -14581,7 +15837,7 @@ function createL2HardeningTools(storagePath, auditLog) {
14581
15837
  }
14582
15838
  },
14583
15839
  {
14584
- name: "sanctuary/l2_verify_isolation",
15840
+ name: "l2_verify_isolation",
14585
15841
  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.",
14586
15842
  inputSchema: {
14587
15843
  type: "object",
@@ -14860,7 +16116,7 @@ function createSovereigntyProfileTools(profileStore, auditLog) {
14860
16116
  const tools = [
14861
16117
  // ── Get Profile ──────────────────────────────────────────────────
14862
16118
  {
14863
- name: "sanctuary/sovereignty_profile_get",
16119
+ name: "sovereignty_profile_get",
14864
16120
  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.",
14865
16121
  inputSchema: {
14866
16122
  type: "object",
@@ -14879,7 +16135,7 @@ function createSovereigntyProfileTools(profileStore, auditLog) {
14879
16135
  },
14880
16136
  // ── Update Profile ───────────────────────────────────────────────
14881
16137
  {
14882
- name: "sanctuary/sovereignty_profile_update",
16138
+ name: "sovereignty_profile_update",
14883
16139
  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.",
14884
16140
  inputSchema: {
14885
16141
  type: "object",
@@ -14960,7 +16216,7 @@ function createSovereigntyProfileTools(profileStore, auditLog) {
14960
16216
  },
14961
16217
  // ── Generate System Prompt ───────────────────────────────────────
14962
16218
  {
14963
- name: "sanctuary/sovereignty_profile_generate_prompt",
16219
+ name: "sovereignty_profile_generate_prompt",
14964
16220
  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.",
14965
16221
  inputSchema: {
14966
16222
  type: "object",
@@ -15354,6 +16610,7 @@ var ProxyRouter = class {
15354
16610
  confidence: injectionResult.confidence,
15355
16611
  latency_ms: Date.now() - start
15356
16612
  }, "failure");
16613
+ this.notifyProxyCall(proxyName, serverName, "blocked", "injection_detected", tier);
15357
16614
  return toolResult({
15358
16615
  error: "Operation not permitted",
15359
16616
  proxy: true
@@ -15384,6 +16641,7 @@ var ProxyRouter = class {
15384
16641
  reason: govResult.reason,
15385
16642
  latency_ms: Date.now() - start
15386
16643
  }, "failure");
16644
+ this.notifyProxyCall(proxyName, serverName, "blocked", govResult.reason, tier);
15387
16645
  return toolResult({
15388
16646
  error: "Operation not permitted",
15389
16647
  proxy: true,
@@ -15418,6 +16676,7 @@ var ProxyRouter = class {
15418
16676
  decision: "allowed",
15419
16677
  latency_ms: latencyMs
15420
16678
  });
16679
+ this.notifyProxyCall(proxyName, serverName, "allowed", void 0, tier);
15421
16680
  return this.normalizeResponse(result);
15422
16681
  } catch (err) {
15423
16682
  const latencyMs = Date.now() - start;
@@ -15437,6 +16696,7 @@ var ProxyRouter = class {
15437
16696
  error: errorMessage,
15438
16697
  latency_ms: latencyMs
15439
16698
  }, "failure");
16699
+ this.notifyProxyCall(proxyName, serverName, "error", errorMessage, tier);
15440
16700
  return {
15441
16701
  content: [{
15442
16702
  type: "text",
@@ -15451,6 +16711,24 @@ var ProxyRouter = class {
15451
16711
  }
15452
16712
  };
15453
16713
  }
16714
+ /**
16715
+ * Notify the onProxyCall callback if configured.
16716
+ */
16717
+ notifyProxyCall(tool, server, decision, reason, tier) {
16718
+ if (this.options.onProxyCall) {
16719
+ try {
16720
+ this.options.onProxyCall({
16721
+ tool,
16722
+ server,
16723
+ decision,
16724
+ reason,
16725
+ tier,
16726
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
16727
+ });
16728
+ } catch {
16729
+ }
16730
+ }
16731
+ }
15454
16732
  /**
15455
16733
  * Call an upstream tool with a timeout.
15456
16734
  */
@@ -15723,7 +17001,7 @@ function createGovernorTools(governor, auditLog) {
15723
17001
  const tools = [
15724
17002
  // ── Governor Status ─────────────────────────────────────────────
15725
17003
  {
15726
- name: "sanctuary/governor_status",
17004
+ name: "governor_status",
15727
17005
  description: "View the current Call Governor status including volume counters, per-tool rate counts, duplicate cache size, and lifetime counter. Use this to monitor tool call consumption, detect potential loops, and check how close you are to governance limits. The governor protects against runaway tool calls by enforcing volume limits, rate limits, duplicate detection, and a session lifetime cap.",
15728
17006
  inputSchema: {
15729
17007
  type: "object",
@@ -15763,7 +17041,7 @@ function createGovernorTools(governor, auditLog) {
15763
17041
  },
15764
17042
  // ── Governor Reset ──────────────────────────────────────────────
15765
17043
  {
15766
- name: "sanctuary/governor_reset",
17044
+ name: "governor_reset",
15767
17045
  description: "Reset all Call Governor counters: volume window, per-tool rate windows, duplicate cache, and lifetime counter. This clears the hard stop if the lifetime limit was reached. This is a Tier 1 operation \u2014 requires human approval because it removes all runtime governance state and could allow previously blocked behavior to resume.",
15768
17046
  inputSchema: {
15769
17047
  type: "object",
@@ -15817,6 +17095,435 @@ function createGovernorTools(governor, auditLog) {
15817
17095
  return { tools };
15818
17096
  }
15819
17097
 
17098
+ // src/sanctuary-tools.ts
17099
+ init_identity();
17100
+ init_encoding();
17101
+ init_identity();
17102
+ function validateVerascoreUrl(urlStr, configuredUrl) {
17103
+ const allowed = /* @__PURE__ */ new Set([
17104
+ "verascore.ai",
17105
+ "www.verascore.ai",
17106
+ "api.verascore.ai"
17107
+ ]);
17108
+ try {
17109
+ allowed.add(new URL(configuredUrl).hostname);
17110
+ } catch {
17111
+ }
17112
+ try {
17113
+ const parsed = new URL(urlStr);
17114
+ if (parsed.protocol !== "https:") {
17115
+ return { ok: false, error: `Verascore URL must use HTTPS. Got: ${parsed.protocol}` };
17116
+ }
17117
+ if (!allowed.has(parsed.hostname)) {
17118
+ return {
17119
+ ok: false,
17120
+ error: `Verascore URL must point to a known Verascore host (${[...allowed].join(", ")}). Got: ${parsed.hostname}`
17121
+ };
17122
+ }
17123
+ return { ok: true };
17124
+ } catch {
17125
+ return { ok: false, error: `Invalid Verascore URL: ${urlStr}` };
17126
+ }
17127
+ }
17128
+ function createSanctuaryTools(opts) {
17129
+ const { config, identityManager, masterKey, auditLog, policy, keyProtection } = opts;
17130
+ const identityEncKey = derivePurposeKey(masterKey, "identity-encryption");
17131
+ const tools = [
17132
+ // ─── sanctuary_bootstrap ───────────────────────────────────────────
17133
+ {
17134
+ name: "sanctuary_bootstrap",
17135
+ 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.",
17136
+ inputSchema: {
17137
+ type: "object",
17138
+ properties: {
17139
+ label: {
17140
+ type: "string",
17141
+ description: "Human-readable label for the new identity (default: 'sovereign-agent')"
17142
+ },
17143
+ verascore_url: {
17144
+ type: "string",
17145
+ description: "Verascore base URL. Defaults to server config / SANCTUARY_VERASCORE_URL."
17146
+ },
17147
+ publish: {
17148
+ type: "boolean",
17149
+ description: "Whether to publish the SHR to Verascore. Defaults to true."
17150
+ }
17151
+ }
17152
+ },
17153
+ handler: async (args) => {
17154
+ const label = args.label || "sovereign-agent";
17155
+ const publish = args.publish === void 0 ? true : Boolean(args.publish);
17156
+ const verascoreUrl = args.verascore_url || config.verascore.url || "https://verascore.ai";
17157
+ const { publicIdentity, storedIdentity } = createIdentity(
17158
+ label,
17159
+ identityEncKey,
17160
+ keyProtection
17161
+ );
17162
+ await identityManager.save(storedIdentity);
17163
+ auditLog.append("l1", "sanctuary_bootstrap:identity_create", publicIdentity.identity_id, {
17164
+ label,
17165
+ did: publicIdentity.did
17166
+ });
17167
+ const shr = generateSHR(publicIdentity.identity_id, {
17168
+ config,
17169
+ identityManager,
17170
+ masterKey
17171
+ });
17172
+ if (typeof shr === "string") {
17173
+ return toolResult({
17174
+ error: `Identity created but SHR generation failed: ${shr}`,
17175
+ did: publicIdentity.did,
17176
+ identity_id: publicIdentity.identity_id
17177
+ });
17178
+ }
17179
+ const agentSlug = publicIdentity.did.replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase();
17180
+ const profileUrl = `${verascoreUrl.replace(/\/$/, "")}/agent/${publicIdentity.did}`;
17181
+ if (!publish || !config.verascore.auto_publish_to_verascore) {
17182
+ auditLog.append("l4", "sanctuary_bootstrap", publicIdentity.identity_id, {
17183
+ did: publicIdentity.did,
17184
+ published: false
17185
+ });
17186
+ return toolResult({
17187
+ did: publicIdentity.did,
17188
+ identity_id: publicIdentity.identity_id,
17189
+ profileUrl,
17190
+ tier: "self-attested",
17191
+ published: false
17192
+ });
17193
+ }
17194
+ const urlCheck = validateVerascoreUrl(verascoreUrl, config.verascore.url);
17195
+ if (!urlCheck.ok) {
17196
+ return toolResult({
17197
+ error: urlCheck.error,
17198
+ did: publicIdentity.did,
17199
+ identity_id: publicIdentity.identity_id
17200
+ });
17201
+ }
17202
+ const publishData = {
17203
+ sovereigntyLayers: shr.body.layers,
17204
+ capabilities: shr.body.capabilities,
17205
+ degradations: shr.body.degradations,
17206
+ did: publicIdentity.did,
17207
+ label
17208
+ };
17209
+ const payloadBytes = new TextEncoder().encode(JSON.stringify(publishData));
17210
+ let signatureB64;
17211
+ try {
17212
+ const sigBytes = sign(
17213
+ payloadBytes,
17214
+ storedIdentity.encrypted_private_key,
17215
+ identityEncKey
17216
+ );
17217
+ signatureB64 = toBase64url(sigBytes);
17218
+ } catch (err) {
17219
+ return toolResult({
17220
+ error: "Failed to sign bootstrap payload",
17221
+ details: err instanceof Error ? err.message : String(err),
17222
+ did: publicIdentity.did
17223
+ });
17224
+ }
17225
+ const body = {
17226
+ agentId: agentSlug,
17227
+ signature: signatureB64,
17228
+ publicKey: publicIdentity.public_key,
17229
+ type: "shr",
17230
+ data: publishData
17231
+ };
17232
+ try {
17233
+ const response = await fetch(`${verascoreUrl.replace(/\/$/, "")}/api/publish`, {
17234
+ method: "POST",
17235
+ headers: { "Content-Type": "application/json" },
17236
+ body: JSON.stringify(body)
17237
+ });
17238
+ const result = await response.json().catch(() => ({}));
17239
+ auditLog.append("l4", "sanctuary_bootstrap", publicIdentity.identity_id, {
17240
+ did: publicIdentity.did,
17241
+ verascore_url: verascoreUrl,
17242
+ status: response.status,
17243
+ published: response.ok
17244
+ });
17245
+ return toolResult({
17246
+ did: publicIdentity.did,
17247
+ identity_id: publicIdentity.identity_id,
17248
+ profileUrl,
17249
+ tier: "self-attested",
17250
+ published: response.ok,
17251
+ verascore_status: response.status,
17252
+ verascore_response: result
17253
+ });
17254
+ } catch (err) {
17255
+ auditLog.append("l4", "sanctuary_bootstrap", publicIdentity.identity_id, {
17256
+ did: publicIdentity.did,
17257
+ error: err instanceof Error ? err.message : String(err)
17258
+ });
17259
+ return toolResult({
17260
+ did: publicIdentity.did,
17261
+ identity_id: publicIdentity.identity_id,
17262
+ profileUrl,
17263
+ tier: "self-attested",
17264
+ published: false,
17265
+ warning: `Identity created but Verascore publish failed: ${err instanceof Error ? err.message : String(err)}`
17266
+ });
17267
+ }
17268
+ }
17269
+ },
17270
+ // ─── sanctuary_policy_status ───────────────────────────────────────
17271
+ {
17272
+ name: "sanctuary_policy_status",
17273
+ 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).",
17274
+ inputSchema: {
17275
+ type: "object",
17276
+ properties: {}
17277
+ },
17278
+ handler: async () => {
17279
+ const tier1 = [...policy.tier1_always_approve].sort();
17280
+ const tier3 = [...policy.tier3_always_allow].sort();
17281
+ const tier2Config = policy.tier2_anomaly;
17282
+ auditLog.append("l2", "sanctuary_policy_status", "system", {
17283
+ tier1_count: tier1.length,
17284
+ tier3_count: tier3.length
17285
+ });
17286
+ return toolResult({
17287
+ tier1,
17288
+ tier2: [],
17289
+ tier3,
17290
+ tier2_anomaly_config: tier2Config,
17291
+ counts: {
17292
+ tier1: tier1.length,
17293
+ tier2: 0,
17294
+ tier3: tier3.length
17295
+ },
17296
+ note: "Tier 2 is not a named list in Sanctuary \u2014 it is behavioral anomaly detection applied to all operations. See tier2_anomaly_config."
17297
+ });
17298
+ }
17299
+ },
17300
+ // ─── sanctuary_export_identity_bundle ──────────────────────────────
17301
+ {
17302
+ name: "sanctuary_export_identity_bundle",
17303
+ 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.",
17304
+ inputSchema: {
17305
+ type: "object",
17306
+ properties: {
17307
+ identity_id: {
17308
+ type: "string",
17309
+ description: "Identity to export (defaults to primary identity)."
17310
+ },
17311
+ attestations: {
17312
+ type: "array",
17313
+ items: { type: "object" },
17314
+ description: "Optional list of attestation objects to include in the bundle."
17315
+ }
17316
+ }
17317
+ },
17318
+ handler: async (args) => {
17319
+ const identityId = args.identity_id;
17320
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
17321
+ if (!identity) {
17322
+ return toolResult({
17323
+ error: "No identity found. Create one with identity_create first."
17324
+ });
17325
+ }
17326
+ const shr = generateSHR(identity.identity_id, {
17327
+ config,
17328
+ identityManager,
17329
+ masterKey
17330
+ });
17331
+ const attestations = args.attestations ?? [];
17332
+ const body = {
17333
+ format: "SANCTUARY_IDENTITY_BUNDLE_V1",
17334
+ publicKey: identity.public_key,
17335
+ did: identity.did,
17336
+ identity_id: identity.identity_id,
17337
+ label: identity.label,
17338
+ key_type: identity.key_type,
17339
+ shr: typeof shr === "string" ? null : shr,
17340
+ attestations,
17341
+ exported_at: (/* @__PURE__ */ new Date()).toISOString()
17342
+ };
17343
+ const bodyBytes = new TextEncoder().encode(JSON.stringify(body));
17344
+ let signatureB64;
17345
+ try {
17346
+ const sigBytes = sign(
17347
+ bodyBytes,
17348
+ identity.encrypted_private_key,
17349
+ identityEncKey
17350
+ );
17351
+ signatureB64 = toBase64url(sigBytes);
17352
+ } catch (err) {
17353
+ return toolResult({
17354
+ error: "Failed to sign identity bundle.",
17355
+ details: err instanceof Error ? err.message : String(err)
17356
+ });
17357
+ }
17358
+ auditLog.append("l1", "sanctuary_export_identity_bundle", identity.identity_id, {
17359
+ did: identity.did,
17360
+ attestation_count: attestations.length
17361
+ });
17362
+ return toolResult({
17363
+ bundle: body,
17364
+ signature: signatureB64,
17365
+ signed_by: identity.did
17366
+ });
17367
+ }
17368
+ },
17369
+ // ─── sanctuary_link_to_human ───────────────────────────────────────
17370
+ {
17371
+ name: "sanctuary_link_to_human",
17372
+ 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.",
17373
+ inputSchema: {
17374
+ type: "object",
17375
+ properties: {
17376
+ email: {
17377
+ type: "string",
17378
+ description: "Email address of the human to link this agent to."
17379
+ },
17380
+ verascore_url: {
17381
+ type: "string",
17382
+ description: "Verascore base URL. Defaults to server config."
17383
+ }
17384
+ },
17385
+ required: ["email"]
17386
+ },
17387
+ handler: async (args) => {
17388
+ const email = args.email;
17389
+ const verascoreUrl = args.verascore_url || config.verascore.url || "https://verascore.ai";
17390
+ const urlCheck = validateVerascoreUrl(verascoreUrl, config.verascore.url);
17391
+ if (!urlCheck.ok) {
17392
+ return toolResult({ ok: false, error: urlCheck.error });
17393
+ }
17394
+ if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
17395
+ return toolResult({ ok: false, error: "Invalid email format." });
17396
+ }
17397
+ try {
17398
+ const response = await fetch(`${verascoreUrl.replace(/\/$/, "")}/api/auth/request`, {
17399
+ method: "POST",
17400
+ headers: { "Content-Type": "application/json" },
17401
+ body: JSON.stringify({ email })
17402
+ });
17403
+ await response.json().catch(() => ({}));
17404
+ auditLog.append("l4", "sanctuary_link_to_human", "system", {
17405
+ verascore_url: verascoreUrl,
17406
+ status: response.status,
17407
+ // Do not log the email to the audit trail — keep it local.
17408
+ email_domain: email.split("@")[1] ?? null
17409
+ });
17410
+ return toolResult({
17411
+ ok: response.ok,
17412
+ message: "Check your email for a login link. After logging in, visit verascore.ai to claim this agent's DID.",
17413
+ email_redacted: `***@${email.split("@")[1] ?? "***"}`,
17414
+ verascore_status: response.status
17415
+ });
17416
+ } catch (err) {
17417
+ return toolResult({
17418
+ ok: false,
17419
+ error: `Failed to reach Verascore at ${verascoreUrl}: ${err instanceof Error ? err.message : String(err)}`
17420
+ });
17421
+ }
17422
+ }
17423
+ },
17424
+ // ─── sanctuary_sign_challenge ──────────────────────────────────────
17425
+ {
17426
+ name: "sanctuary_sign_challenge",
17427
+ 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.",
17428
+ inputSchema: {
17429
+ type: "object",
17430
+ properties: {
17431
+ nonce: {
17432
+ type: "string",
17433
+ description: "The nonce / challenge string to sign."
17434
+ },
17435
+ purpose: {
17436
+ type: "string",
17437
+ description: "Domain-separation tag identifying what the signature will be used for (e.g. 'verascore-claim'). Required. Max 128 chars, printable ASCII only."
17438
+ },
17439
+ identity_id: {
17440
+ type: "string",
17441
+ description: "Identity to sign with (defaults to primary)."
17442
+ }
17443
+ },
17444
+ required: ["nonce", "purpose"]
17445
+ },
17446
+ handler: async (args) => {
17447
+ const nonce = args.nonce;
17448
+ const purpose = args.purpose;
17449
+ if (!nonce || nonce.length === 0) {
17450
+ return toolResult({ error: "nonce must be a non-empty string." });
17451
+ }
17452
+ if (nonce.length > 4096) {
17453
+ return toolResult({ error: "nonce exceeds maximum length (4096)." });
17454
+ }
17455
+ if (typeof purpose !== "string" || purpose.length === 0) {
17456
+ return toolResult({
17457
+ error: "purpose is required (domain-separation tag, e.g. 'verascore-claim')."
17458
+ });
17459
+ }
17460
+ if (purpose.length > 128) {
17461
+ return toolResult({ error: "purpose exceeds maximum length (128)." });
17462
+ }
17463
+ if (!/^[\x20-\x7E]+$/.test(purpose)) {
17464
+ return toolResult({
17465
+ error: "purpose must be printable ASCII only (no NUL, no non-ASCII)."
17466
+ });
17467
+ }
17468
+ const identityId = args.identity_id;
17469
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
17470
+ if (!identity) {
17471
+ return toolResult({
17472
+ error: "No identity found. Create one with identity_create first."
17473
+ });
17474
+ }
17475
+ const domainTag = "sanctuary-sign-challenge-v1";
17476
+ const enc = new TextEncoder();
17477
+ const tagBytes = enc.encode(domainTag);
17478
+ const purposeBytes = enc.encode(purpose);
17479
+ const nonceBytes = enc.encode(nonce);
17480
+ const sep = new Uint8Array([0]);
17481
+ const message = new Uint8Array(
17482
+ tagBytes.length + 1 + purposeBytes.length + 1 + nonceBytes.length
17483
+ );
17484
+ let offset = 0;
17485
+ message.set(tagBytes, offset);
17486
+ offset += tagBytes.length;
17487
+ message.set(sep, offset);
17488
+ offset += 1;
17489
+ message.set(purposeBytes, offset);
17490
+ offset += purposeBytes.length;
17491
+ message.set(sep, offset);
17492
+ offset += 1;
17493
+ message.set(nonceBytes, offset);
17494
+ let sigB64;
17495
+ try {
17496
+ const sig = sign(
17497
+ message,
17498
+ identity.encrypted_private_key,
17499
+ identityEncKey
17500
+ );
17501
+ sigB64 = toBase64url(sig);
17502
+ } catch (err) {
17503
+ return toolResult({
17504
+ error: "Failed to sign nonce.",
17505
+ details: err instanceof Error ? err.message : String(err)
17506
+ });
17507
+ }
17508
+ auditLog.append("l1", "sanctuary_sign_challenge", identity.identity_id, {
17509
+ did: identity.did,
17510
+ nonce_len: nonce.length,
17511
+ purpose
17512
+ });
17513
+ return toolResult({
17514
+ signature: sigB64,
17515
+ did: identity.did,
17516
+ public_key: identity.public_key,
17517
+ signed_by: identity.did,
17518
+ domain_tag: domainTag,
17519
+ purpose
17520
+ });
17521
+ }
17522
+ }
17523
+ ];
17524
+ return { tools };
17525
+ }
17526
+
15820
17527
  // src/index.ts
15821
17528
  init_random();
15822
17529
  init_encoding();
@@ -16090,7 +17797,7 @@ async function createSanctuaryServer(options) {
16090
17797
  }
16091
17798
  const l2Tools = [
16092
17799
  {
16093
- name: "sanctuary/exec_attest",
17800
+ name: "exec_attest",
16094
17801
  description: "Generate an attestation of the current execution environment, including sovereignty assessment and degradation report.",
16095
17802
  inputSchema: {
16096
17803
  type: "object",
@@ -16141,7 +17848,7 @@ async function createSanctuaryServer(options) {
16141
17848
  }
16142
17849
  },
16143
17850
  {
16144
- name: "sanctuary/monitor_health",
17851
+ name: "monitor_health",
16145
17852
  description: "Sanctuary Health Report (SHR) \u2014 standardized sovereignty status.",
16146
17853
  inputSchema: { type: "object", properties: {} },
16147
17854
  handler: async () => {
@@ -16190,7 +17897,7 @@ async function createSanctuaryServer(options) {
16190
17897
  }
16191
17898
  },
16192
17899
  {
16193
- name: "sanctuary/monitor_audit_log",
17900
+ name: "monitor_audit_log",
16194
17901
  description: "Query the sovereignty audit log.",
16195
17902
  inputSchema: {
16196
17903
  type: "object",
@@ -16216,7 +17923,7 @@ async function createSanctuaryServer(options) {
16216
17923
  }
16217
17924
  ];
16218
17925
  const manifestTool = {
16219
- name: "sanctuary/manifest",
17926
+ name: "manifest",
16220
17927
  description: "Generate the Sanctuary Interface Manifest (SIM) \u2014 a machine-readable declaration of this server's capabilities.",
16221
17928
  inputSchema: { type: "object", properties: {} },
16222
17929
  handler: async () => {
@@ -16302,14 +18009,19 @@ async function createSanctuaryServer(options) {
16302
18009
  config,
16303
18010
  identityManager,
16304
18011
  masterKey,
16305
- auditLog
18012
+ auditLog,
18013
+ {
18014
+ autoPublishHandshakes: config.verascore.auto_publish_handshakes,
18015
+ verascoreUrl: config.verascore.url
18016
+ }
16306
18017
  );
16307
18018
  const { tools: l4Tools} = createL4Tools(
16308
18019
  storage,
16309
18020
  masterKey,
16310
18021
  identityManager,
16311
18022
  auditLog,
16312
- handshakeResults
18023
+ handshakeResults,
18024
+ config.verascore.url
16313
18025
  );
16314
18026
  const { tools: federationTools } = createFederationTools(
16315
18027
  auditLog,
@@ -16323,6 +18035,7 @@ async function createSanctuaryServer(options) {
16323
18035
  handshakeResults
16324
18036
  );
16325
18037
  const { tools: auditTools } = createAuditTools(config);
18038
+ const { tools: siemTools } = createSIEMTools(auditLog);
16326
18039
  const { tools: contextGateTools, enforcer: contextGateEnforcer } = createContextGateTools(storage, masterKey, auditLog);
16327
18040
  const hardeningTools = createL2HardeningTools(config.storage_path, auditLog);
16328
18041
  const profileStore = new SovereigntyProfileStore(storage, masterKey);
@@ -16394,10 +18107,18 @@ async function createSanctuaryServer(options) {
16394
18107
  } : void 0;
16395
18108
  const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
16396
18109
  const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
18110
+ const { tools: sanctuaryMetaTools } = createSanctuaryTools({
18111
+ config,
18112
+ identityManager,
18113
+ masterKey,
18114
+ auditLog,
18115
+ policy,
18116
+ keyProtection
18117
+ });
16397
18118
  const dashboardTools = [];
16398
18119
  if (dashboard) {
16399
18120
  dashboardTools.push({
16400
- name: "sanctuary/dashboard_open",
18121
+ name: "dashboard_open",
16401
18122
  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.",
16402
18123
  inputSchema: {
16403
18124
  type: "object",
@@ -16431,10 +18152,12 @@ async function createSanctuaryServer(options) {
16431
18152
  ...federationTools,
16432
18153
  ...bridgeTools,
16433
18154
  ...auditTools,
18155
+ ...siemTools,
16434
18156
  ...contextGateTools,
16435
18157
  ...hardeningTools,
16436
18158
  ...profileTools,
16437
18159
  ...dashboardTools,
18160
+ ...sanctuaryMetaTools,
16438
18161
  manifestTool
16439
18162
  ];
16440
18163
  let clientManager;
@@ -16476,7 +18199,12 @@ async function createSanctuaryServer(options) {
16476
18199
  }
16477
18200
  return args;
16478
18201
  },
16479
- governor
18202
+ governor,
18203
+ onProxyCall: (data) => {
18204
+ if (dashboard) {
18205
+ dashboard.broadcastProxyCall(data);
18206
+ }
18207
+ }
16480
18208
  }
16481
18209
  );
16482
18210
  clientManager.configure(enabledServers).catch((err) => {
@@ -16494,6 +18222,7 @@ async function createSanctuaryServer(options) {
16494
18222
  auditLog,
16495
18223
  clientManager
16496
18224
  });
18225
+ dashboard.enableFortressView(enabledServers.length);
16497
18226
  }
16498
18227
  }
16499
18228
  }