@sanctuary-framework/mcp-server 0.5.16 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { mkdir, readFile, writeFile, chmod, access, stat, unlink, readdir } from 'fs/promises';
2
+ import { mkdir, readFile, writeFile, chmod, access, stat, unlink, readdir, copyFile } from 'fs/promises';
3
3
  import { join } from 'path';
4
4
  import { homedir, platform } from 'os';
5
5
  import { createRequire } from 'module';
@@ -73,6 +73,12 @@ function defaultConfig() {
73
73
  secret: "",
74
74
  callback_port: 3502,
75
75
  callback_host: "127.0.0.1"
76
+ },
77
+ verascore: {
78
+ url: "https://verascore.ai",
79
+ auto_publish_to_verascore: true,
80
+ // DELTA-04: default OFF for privacy. Enable explicitly per deployment.
81
+ auto_publish_handshakes: false
76
82
  }
77
83
  };
78
84
  }
@@ -143,6 +149,21 @@ async function loadConfig(configPath) {
143
149
  if (process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST) {
144
150
  config.webhook.callback_host = process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST;
145
151
  }
152
+ if (process.env.SANCTUARY_VERASCORE_URL) {
153
+ config.verascore.url = process.env.SANCTUARY_VERASCORE_URL;
154
+ }
155
+ if (process.env.SANCTUARY_AUTO_PUBLISH_TO_VERASCORE === "true") {
156
+ config.verascore.auto_publish_to_verascore = true;
157
+ }
158
+ if (process.env.SANCTUARY_AUTO_PUBLISH_TO_VERASCORE === "false") {
159
+ config.verascore.auto_publish_to_verascore = false;
160
+ }
161
+ if (process.env.SANCTUARY_AUTO_PUBLISH_HANDSHAKES === "true") {
162
+ config.verascore.auto_publish_handshakes = true;
163
+ }
164
+ if (process.env.SANCTUARY_AUTO_PUBLISH_HANDSHAKES === "false") {
165
+ config.verascore.auto_publish_handshakes = false;
166
+ }
146
167
  config.version = PKG_VERSION;
147
168
  validateConfig(config);
148
169
  return config;
@@ -941,7 +962,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
941
962
  const tools = [
942
963
  // ── Identity Tools ──────────────────────────────────────────────────
943
964
  {
944
- name: "sanctuary/identity_create",
965
+ name: "identity_create",
945
966
  description: "Create a new sovereign identity (Ed25519 keypair). The private key is encrypted and never exposed.",
946
967
  inputSchema: {
947
968
  type: "object",
@@ -976,7 +997,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
976
997
  }
977
998
  },
978
999
  {
979
- name: "sanctuary/identity_list",
1000
+ name: "identity_list",
980
1001
  description: "List all managed sovereign identities.",
981
1002
  inputSchema: {
982
1003
  type: "object",
@@ -1001,7 +1022,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1001
1022
  }
1002
1023
  },
1003
1024
  {
1004
- name: "sanctuary/identity_sign",
1025
+ name: "identity_sign",
1005
1026
  description: "Sign data with a managed identity. The private key is decrypted in memory only during signing.",
1006
1027
  inputSchema: {
1007
1028
  type: "object",
@@ -1039,7 +1060,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1039
1060
  }
1040
1061
  },
1041
1062
  {
1042
- name: "sanctuary/identity_verify",
1063
+ name: "identity_verify",
1043
1064
  description: "Verify an Ed25519 signature. Provide either identity_id or public_key.",
1044
1065
  inputSchema: {
1045
1066
  type: "object",
@@ -1088,7 +1109,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1088
1109
  }
1089
1110
  },
1090
1111
  {
1091
- name: "sanctuary/identity_rotate",
1112
+ name: "identity_rotate",
1092
1113
  description: "Rotate keys for an identity. Generates a new keypair and signs a rotation event with the old key for verifiable chain.",
1093
1114
  inputSchema: {
1094
1115
  type: "object",
@@ -1121,7 +1142,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1121
1142
  },
1122
1143
  // ── State Tools ─────────────────────────────────────────────────────
1123
1144
  {
1124
- name: "sanctuary/state_write",
1145
+ name: "state_write",
1125
1146
  description: "Write encrypted state to the sovereign store. Value is encrypted with a namespace-specific key. The write is signed by the active identity.",
1126
1147
  inputSchema: {
1127
1148
  type: "object",
@@ -1178,7 +1199,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1178
1199
  }
1179
1200
  },
1180
1201
  {
1181
- name: "sanctuary/state_read",
1202
+ name: "state_read",
1182
1203
  description: "Read and decrypt state from the sovereign store. Verifies integrity via Merkle proof and signature.",
1183
1204
  inputSchema: {
1184
1205
  type: "object",
@@ -1219,7 +1240,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1219
1240
  }
1220
1241
  },
1221
1242
  {
1222
- name: "sanctuary/state_list",
1243
+ name: "state_list",
1223
1244
  description: "List keys in a namespace (metadata only \u2014 no decryption).",
1224
1245
  inputSchema: {
1225
1246
  type: "object",
@@ -1251,7 +1272,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1251
1272
  }
1252
1273
  },
1253
1274
  {
1254
- name: "sanctuary/state_delete",
1275
+ name: "state_delete",
1255
1276
  description: "Securely delete state. Overwrites file with random bytes before removal (right to deletion, S1.6).",
1256
1277
  inputSchema: {
1257
1278
  type: "object",
@@ -1283,7 +1304,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1283
1304
  }
1284
1305
  },
1285
1306
  {
1286
- name: "sanctuary/state_export",
1307
+ name: "state_export",
1287
1308
  description: "Export state as an encrypted, portable bundle for migration.",
1288
1309
  inputSchema: {
1289
1310
  type: "object",
@@ -1303,7 +1324,7 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1303
1324
  }
1304
1325
  },
1305
1326
  {
1306
- name: "sanctuary/state_import",
1327
+ name: "state_import",
1307
1328
  description: "Import a previously exported state bundle.",
1308
1329
  inputSchema: {
1309
1330
  type: "object",
@@ -1643,9 +1664,10 @@ tier1_always_approve:
1643
1664
  - reputation_import
1644
1665
  - reputation_export
1645
1666
  - bootstrap_provide_guarantee
1646
- - reputation_publish
1647
1667
  - sovereignty_profile_update
1648
1668
  - governor_reset
1669
+ - sanctuary_bootstrap
1670
+ - sanctuary_export_identity_bundle
1649
1671
 
1650
1672
  # \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
1651
1673
  # Triggers approval when agent behavior deviates from its baseline.
@@ -1711,6 +1733,8 @@ tier3_always_allow:
1711
1733
  - dashboard_open
1712
1734
  - sovereignty_profile_get
1713
1735
  - governor_status
1736
+ - reputation_publish
1737
+ - sanctuary_policy_status
1714
1738
 
1715
1739
  # \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
1716
1740
  # How Sanctuary reaches you when approval is needed.
@@ -1764,12 +1788,14 @@ var init_loader = __esm({
1764
1788
  "reputation_export",
1765
1789
  "bootstrap_provide_guarantee",
1766
1790
  "decommission_certificate",
1767
- "reputation_publish",
1768
- // SEC-039: Explicit Tier 1 — sends data to external API
1769
1791
  "sovereignty_profile_update",
1770
1792
  // Changes enforcement behavior — always requires approval
1771
- "governor_reset"
1793
+ "governor_reset",
1772
1794
  // Clears all runtime governance state — always requires approval
1795
+ "sanctuary_bootstrap",
1796
+ // Creates new Ed25519 identity + publishes — always requires approval
1797
+ "sanctuary_export_identity_bundle"
1798
+ // Exports portable identity — always requires approval
1773
1799
  ],
1774
1800
  tier2_anomaly: DEFAULT_TIER2,
1775
1801
  tier3_always_allow: [
@@ -1827,7 +1853,11 @@ var init_loader = __esm({
1827
1853
  "sovereignty_profile_get",
1828
1854
  "sovereignty_profile_generate_prompt",
1829
1855
  // Agent needs its own config to generate system prompt
1830
- "governor_status"
1856
+ "governor_status",
1857
+ "reputation_publish",
1858
+ // Auto-allow: publishing sovereignty data to Verascore is routine
1859
+ "sanctuary_policy_status"
1860
+ // Read-only policy summary
1831
1861
  ],
1832
1862
  approval_channel: DEFAULT_CHANNEL
1833
1863
  };
@@ -5038,244 +5068,1047 @@ var init_dashboard_html = __esm({
5038
5068
  }
5039
5069
  });
5040
5070
 
5041
- // src/system-prompt-generator.ts
5042
- function generateSystemPrompt(profile) {
5043
- const activeFeatures = [];
5044
- const inactiveFeatures = [];
5045
- const activeKeys = [];
5046
- const featureKeys = [
5047
- "audit_logging",
5048
- "injection_detection",
5049
- "context_gating",
5050
- "approval_gate",
5051
- "zk_proofs"
5052
- ];
5053
- for (const key of featureKeys) {
5054
- const featureConfig = profile.features[key];
5055
- const info = FEATURE_INFO[key];
5056
- if (featureConfig.enabled) {
5057
- activeKeys.push(key);
5058
- let desc = `- ${info.name}: ${info.activeDescription}`;
5059
- if (key === "injection_detection" && "sensitivity" in featureConfig && featureConfig.sensitivity) {
5060
- desc += ` Sensitivity: ${featureConfig.sensitivity}.`;
5061
- }
5062
- if (key === "context_gating" && "policy_id" in featureConfig && featureConfig.policy_id) {
5063
- desc += ` Active policy: ${featureConfig.policy_id}.`;
5064
- }
5065
- activeFeatures.push(desc);
5066
- } else {
5067
- inactiveFeatures.push(info.disabledDescription);
5071
+ // src/cocoon/fortress-view.ts
5072
+ function generateFortressViewHTML(options) {
5073
+ return `<!DOCTYPE html>
5074
+ <html lang="en">
5075
+ <head>
5076
+ <meta charset="UTF-8">
5077
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
5078
+ <title>Sanctuary \u2014 Fortress View</title>
5079
+ <style>
5080
+ :root {
5081
+ --bg: #0d1117;
5082
+ --surface: #161b22;
5083
+ --surface-raised: #1c2128;
5084
+ --border: #30363d;
5085
+ --text-primary: #e6edf3;
5086
+ --text-secondary: #8b949e;
5087
+ --text-muted: #484f58;
5088
+ --green: #3fb950;
5089
+ --green-dim: #238636;
5090
+ --amber: #d29922;
5091
+ --amber-dim: #9e6a03;
5092
+ --red: #f85149;
5093
+ --red-dim: #da3633;
5094
+ --blue: #58a6ff;
5095
+ --blue-dim: #1f6feb;
5068
5096
  }
5069
- }
5070
- const lines = [];
5071
- if (activeKeys.length > 0) {
5072
- lines.push("QUICK START:");
5073
- const quickStartItems = buildQuickStart(activeKeys);
5074
- for (const item of quickStartItems) {
5075
- lines.push(` ${item}`);
5097
+
5098
+ * { margin: 0; padding: 0; box-sizing: border-box; }
5099
+
5100
+ body {
5101
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
5102
+ background-color: var(--bg);
5103
+ color: var(--text-primary);
5104
+ min-height: 100vh;
5076
5105
  }
5077
- lines.push("");
5078
- }
5079
- lines.push(
5080
- "You are protected by Sanctuary sovereignty infrastructure. The following protections are active:"
5081
- );
5082
- lines.push("");
5083
- if (activeFeatures.length > 0) {
5084
- lines.push(...activeFeatures);
5085
- } else {
5086
- lines.push(
5087
- "- No features are currently enabled. Contact your operator to configure protections."
5088
- );
5089
- }
5090
- if (inactiveFeatures.length > 0) {
5091
- lines.push("");
5092
- lines.push(
5093
- `Optional tools available but not currently enabled: ${inactiveFeatures.join(", ")}.`
5094
- );
5095
- }
5096
- return lines.join("\n");
5097
- }
5098
- function buildQuickStart(activeKeys) {
5099
- const items = [];
5100
- if (activeKeys.includes("context_gating")) {
5101
- items.push(
5102
- "1. ALWAYS call sanctuary/context_gate_filter before sending context to external APIs."
5103
- );
5104
- }
5105
- if (activeKeys.includes("zk_proofs")) {
5106
- items.push(
5107
- `${items.length + 1}. Use sanctuary/zk_commit to prove claims without revealing underlying data.`
5108
- );
5109
- }
5110
- if (activeKeys.includes("approval_gate")) {
5111
- items.push(
5112
- `${items.length + 1}. High-risk operations will be held for human approval \u2014 expect async responses.`
5113
- );
5114
- }
5115
- if (items.length === 0) {
5116
- if (activeKeys.includes("audit_logging")) {
5117
- items.push("1. All tool calls are automatically logged to an encrypted audit trail.");
5106
+
5107
+ /* \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 */
5108
+ .fortress-header {
5109
+ display: flex;
5110
+ align-items: center;
5111
+ justify-content: space-between;
5112
+ padding: 16px 24px;
5113
+ border-bottom: 1px solid var(--border);
5114
+ background: var(--surface);
5118
5115
  }
5119
- if (activeKeys.includes("injection_detection")) {
5120
- items.push(
5121
- `${items.length + 1}. Tool arguments are scanned for injection \u2014 blocked calls should not be retried.`
5122
- );
5116
+
5117
+ .fortress-brand {
5118
+ display: flex;
5119
+ align-items: center;
5120
+ gap: 12px;
5123
5121
  }
5124
- }
5125
- return items;
5126
- }
5127
- var FEATURE_INFO;
5128
- var init_system_prompt_generator = __esm({
5129
- "src/system-prompt-generator.ts"() {
5130
- FEATURE_INFO = {
5131
- audit_logging: {
5132
- name: "Audit Logging",
5133
- 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.",
5134
- toolNames: ["sanctuary/monitor_audit_log"],
5135
- disabledDescription: "audit logging (sanctuary/monitor_audit_log)",
5136
- usageExample: "Automatic \u2014 every tool call you make is recorded. No explicit action required."
5137
- },
5138
- injection_detection: {
5139
- name: "Injection Detection",
5140
- 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.",
5141
- disabledDescription: "injection detection",
5142
- usageExample: "Automatic \u2014 if a tool call is blocked with an injection alert, do not retry with the same arguments."
5143
- },
5144
- context_gating: {
5145
- name: "Context Gating",
5146
- 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.",
5147
- toolNames: [
5148
- "sanctuary/context_gate_filter",
5149
- "sanctuary/context_gate_set_policy",
5150
- "sanctuary/context_gate_apply_template",
5151
- "sanctuary/context_gate_recommend",
5152
- "sanctuary/context_gate_list_policies"
5153
- ],
5154
- disabledDescription: "context gating (sanctuary/context_gate_filter)",
5155
- usageExample: "Before calling an external API, run: sanctuary/context_gate_filter with your context object and policy_id to get a filtered version."
5156
- },
5157
- approval_gate: {
5158
- name: "Approval Gates",
5159
- 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.",
5160
- disabledDescription: "approval gates",
5161
- 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."
5162
- },
5163
- zk_proofs: {
5164
- name: "Zero-Knowledge Proofs",
5165
- 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.",
5166
- toolNames: [
5167
- "sanctuary/zk_commit",
5168
- "sanctuary/zk_prove",
5169
- "sanctuary/zk_range_prove",
5170
- "sanctuary/proof_commitment"
5171
- ],
5172
- disabledDescription: "zero-knowledge proofs (sanctuary/zk_commit, sanctuary/zk_prove)",
5173
- 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."
5174
- }
5175
- };
5176
- }
5177
- });
5178
- var SESSION_TTL_REMOTE_MS, SESSION_TTL_LOCAL_MS, MAX_SESSIONS, RATE_LIMIT_WINDOW_MS, RATE_LIMIT_GENERAL, RATE_LIMIT_DECISIONS, MAX_RATE_LIMIT_ENTRIES, DashboardApprovalChannel;
5179
- var init_dashboard = __esm({
5180
- "src/principal-policy/dashboard.ts"() {
5181
- init_config();
5182
- init_generator();
5183
- init_dashboard_html();
5184
- init_system_prompt_generator();
5185
- SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
5186
- SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
5187
- MAX_SESSIONS = 1e3;
5188
- RATE_LIMIT_WINDOW_MS = 6e4;
5189
- RATE_LIMIT_GENERAL = 120;
5190
- RATE_LIMIT_DECISIONS = 20;
5191
- MAX_RATE_LIMIT_ENTRIES = 1e4;
5192
- DashboardApprovalChannel = class {
5193
- config;
5194
- pending = /* @__PURE__ */ new Map();
5195
- sseClients = /* @__PURE__ */ new Set();
5196
- httpServer = null;
5197
- policy = null;
5198
- baseline = null;
5199
- auditLog = null;
5200
- identityManager = null;
5201
- handshakeResults = null;
5202
- shrOpts = null;
5203
- _sanctuaryConfig = null;
5204
- profileStore = null;
5205
- clientManager = null;
5206
- dashboardHTML;
5207
- loginHTML;
5208
- authToken;
5209
- useTLS;
5210
- /** Session TTL: longer for localhost, shorter for remote */
5211
- sessionTTLMs;
5212
- /** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
5213
- sessions = /* @__PURE__ */ new Map();
5214
- sessionCleanupTimer = null;
5215
- /** Rate limiting: per-IP request tracking */
5216
- rateLimits = /* @__PURE__ */ new Map();
5217
- /** Whether the dashboard is running in standalone mode (no MCP server) */
5218
- _standaloneMode = false;
5219
- constructor(config) {
5220
- this.config = config;
5221
- this.authToken = config.auth_token;
5222
- this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
5223
- const isLocalhost = config.host === "127.0.0.1" || config.host === "localhost" || config.host === "::1";
5224
- this.sessionTTLMs = isLocalhost ? SESSION_TTL_LOCAL_MS : SESSION_TTL_REMOTE_MS;
5225
- this.dashboardHTML = generateDashboardHTML({
5226
- timeoutSeconds: config.timeout_seconds,
5227
- serverVersion: SANCTUARY_VERSION
5228
- });
5229
- this.loginHTML = generateLoginHTML({ serverVersion: SANCTUARY_VERSION });
5230
- this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
5231
- }
5232
- /**
5233
- * Inject dependencies after construction.
5234
- * Called from index.ts after all components are initialized.
5235
- */
5236
- setDependencies(deps) {
5237
- this.policy = deps.policy;
5238
- this.baseline = deps.baseline;
5239
- this.auditLog = deps.auditLog;
5240
- if (deps.identityManager) this.identityManager = deps.identityManager;
5241
- if (deps.handshakeResults) this.handshakeResults = deps.handshakeResults;
5242
- if (deps.shrOpts) this.shrOpts = deps.shrOpts;
5243
- if (deps.sanctuaryConfig) this._sanctuaryConfig = deps.sanctuaryConfig;
5244
- if (deps.profileStore) this.profileStore = deps.profileStore;
5245
- if (deps.clientManager) this.clientManager = deps.clientManager;
5246
- }
5247
- /**
5248
- * Mark this dashboard as running in standalone mode.
5249
- * Exposed via /api/status so the frontend can show an appropriate banner.
5250
- */
5251
- setStandaloneMode(standalone) {
5252
- this._standaloneMode = standalone;
5253
- }
5254
- /**
5255
- * Start the HTTP(S) server for the dashboard.
5256
- */
5257
- async start() {
5258
- return new Promise((resolve, reject) => {
5259
- const handler = (req, res) => this.handleRequest(req, res);
5260
- if (this.useTLS && this.config.tls) {
5261
- const tlsOpts = {
5262
- cert: readFileSync(this.config.tls.cert_path),
5263
- key: readFileSync(this.config.tls.key_path)
5264
- };
5265
- this.httpServer = createServer$2(tlsOpts, handler);
5266
- } else {
5267
- this.httpServer = createServer$1(handler);
5268
- }
5269
- const protocol = this.useTLS ? "https" : "http";
5270
- const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
5271
- this.httpServer.listen(this.config.port, this.config.host, () => {
5272
- const sessionUrl = this.authToken ? this.createSessionUrl() : baseUrl;
5273
- process.stderr.write(
5274
- `
5275
- Sanctuary Principal Dashboard: ${baseUrl}
5276
- `
5277
- );
5278
- if (this.authToken) {
5122
+
5123
+ .fortress-brand .shield {
5124
+ font-size: 28px;
5125
+ color: var(--blue);
5126
+ }
5127
+
5128
+ .fortress-brand h1 {
5129
+ font-size: 18px;
5130
+ font-weight: 600;
5131
+ letter-spacing: -0.5px;
5132
+ }
5133
+
5134
+ .fortress-brand .version {
5135
+ font-size: 12px;
5136
+ color: var(--text-secondary);
5137
+ }
5138
+
5139
+ .header-actions {
5140
+ display: flex;
5141
+ gap: 8px;
5142
+ }
5143
+
5144
+ .header-actions button {
5145
+ padding: 6px 16px;
5146
+ border-radius: 6px;
5147
+ border: 1px solid var(--border);
5148
+ background: var(--surface);
5149
+ color: var(--text-primary);
5150
+ font-size: 13px;
5151
+ cursor: pointer;
5152
+ transition: background 0.15s;
5153
+ }
5154
+
5155
+ .header-actions button:hover {
5156
+ background: var(--surface-raised);
5157
+ }
5158
+
5159
+ .header-actions .pause-btn {
5160
+ border-color: var(--red-dim);
5161
+ color: var(--red);
5162
+ }
5163
+
5164
+ .header-actions .pause-btn:hover {
5165
+ background: rgba(248, 81, 73, 0.1);
5166
+ }
5167
+
5168
+ .header-actions .pause-btn.paused {
5169
+ background: var(--red-dim);
5170
+ color: white;
5171
+ }
5172
+
5173
+ /* \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 */
5174
+ .tab-bar {
5175
+ display: flex;
5176
+ border-bottom: 1px solid var(--border);
5177
+ background: var(--surface);
5178
+ padding: 0 24px;
5179
+ }
5180
+
5181
+ .tab-bar button {
5182
+ padding: 10px 16px;
5183
+ border: none;
5184
+ background: none;
5185
+ color: var(--text-secondary);
5186
+ font-size: 14px;
5187
+ cursor: pointer;
5188
+ border-bottom: 2px solid transparent;
5189
+ transition: all 0.15s;
5190
+ }
5191
+
5192
+ .tab-bar button:hover {
5193
+ color: var(--text-primary);
5194
+ }
5195
+
5196
+ .tab-bar button.active {
5197
+ color: var(--text-primary);
5198
+ border-bottom-color: var(--blue);
5199
+ }
5200
+
5201
+ /* \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 */
5202
+ .fortress-content { padding: 24px; }
5203
+
5204
+ /* \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 */
5205
+ .status-banner {
5206
+ display: flex;
5207
+ align-items: center;
5208
+ gap: 16px;
5209
+ padding: 20px 24px;
5210
+ border-radius: 8px;
5211
+ border: 1px solid var(--border);
5212
+ background: var(--surface);
5213
+ margin-bottom: 24px;
5214
+ }
5215
+
5216
+ .status-indicator {
5217
+ width: 48px;
5218
+ height: 48px;
5219
+ border-radius: 50%;
5220
+ display: flex;
5221
+ align-items: center;
5222
+ justify-content: center;
5223
+ font-size: 24px;
5224
+ flex-shrink: 0;
5225
+ }
5226
+
5227
+ .status-indicator.green { background: rgba(63, 185, 80, 0.15); color: var(--green); }
5228
+ .status-indicator.amber { background: rgba(210, 153, 34, 0.15); color: var(--amber); }
5229
+ .status-indicator.red { background: rgba(248, 81, 73, 0.15); color: var(--red); }
5230
+
5231
+ .status-info h2 {
5232
+ font-size: 18px;
5233
+ font-weight: 600;
5234
+ margin-bottom: 4px;
5235
+ }
5236
+
5237
+ .status-info p {
5238
+ font-size: 14px;
5239
+ color: var(--text-secondary);
5240
+ }
5241
+
5242
+ .status-stats {
5243
+ display: flex;
5244
+ gap: 24px;
5245
+ margin-left: auto;
5246
+ }
5247
+
5248
+ .stat {
5249
+ text-align: center;
5250
+ }
5251
+
5252
+ .stat .value {
5253
+ font-size: 24px;
5254
+ font-weight: 600;
5255
+ font-variant-numeric: tabular-nums;
5256
+ }
5257
+
5258
+ .stat .label {
5259
+ font-size: 11px;
5260
+ color: var(--text-secondary);
5261
+ text-transform: uppercase;
5262
+ letter-spacing: 0.5px;
5263
+ }
5264
+
5265
+ /* \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 */
5266
+ .fortress-grid {
5267
+ display: grid;
5268
+ grid-template-columns: 1fr 360px;
5269
+ gap: 24px;
5270
+ }
5271
+
5272
+ @media (max-width: 900px) {
5273
+ .fortress-grid { grid-template-columns: 1fr; }
5274
+ }
5275
+
5276
+ /* \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 */
5277
+ .feed-panel {
5278
+ background: var(--surface);
5279
+ border: 1px solid var(--border);
5280
+ border-radius: 8px;
5281
+ overflow: hidden;
5282
+ }
5283
+
5284
+ .panel-header {
5285
+ display: flex;
5286
+ align-items: center;
5287
+ justify-content: space-between;
5288
+ padding: 12px 16px;
5289
+ border-bottom: 1px solid var(--border);
5290
+ }
5291
+
5292
+ .panel-header h3 {
5293
+ font-size: 14px;
5294
+ font-weight: 600;
5295
+ }
5296
+
5297
+ .feed-list {
5298
+ max-height: 600px;
5299
+ overflow-y: auto;
5300
+ scroll-behavior: smooth;
5301
+ }
5302
+
5303
+ .feed-item {
5304
+ display: flex;
5305
+ align-items: flex-start;
5306
+ gap: 10px;
5307
+ padding: 10px 16px;
5308
+ border-bottom: 1px solid var(--border);
5309
+ font-size: 13px;
5310
+ transition: background 0.1s;
5311
+ }
5312
+
5313
+ .feed-item:hover {
5314
+ background: var(--surface-raised);
5315
+ }
5316
+
5317
+ .feed-dot {
5318
+ width: 8px;
5319
+ height: 8px;
5320
+ border-radius: 50%;
5321
+ margin-top: 5px;
5322
+ flex-shrink: 0;
5323
+ }
5324
+
5325
+ .feed-dot.green { background: var(--green); }
5326
+ .feed-dot.amber { background: var(--amber); }
5327
+ .feed-dot.red { background: var(--red); }
5328
+
5329
+ .feed-detail {
5330
+ flex: 1;
5331
+ min-width: 0;
5332
+ }
5333
+
5334
+ .feed-tool {
5335
+ font-family: 'SF Mono', 'Fira Code', monospace;
5336
+ font-size: 12px;
5337
+ color: var(--blue);
5338
+ word-break: break-all;
5339
+ }
5340
+
5341
+ .feed-decision {
5342
+ font-size: 12px;
5343
+ color: var(--text-secondary);
5344
+ margin-top: 2px;
5345
+ }
5346
+
5347
+ .feed-time {
5348
+ font-size: 11px;
5349
+ color: var(--text-muted);
5350
+ flex-shrink: 0;
5351
+ white-space: nowrap;
5352
+ }
5353
+
5354
+ .feed-empty {
5355
+ padding: 40px 16px;
5356
+ text-align: center;
5357
+ color: var(--text-muted);
5358
+ font-size: 14px;
5359
+ }
5360
+
5361
+ /* \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 */
5362
+ .alerts-panel {
5363
+ background: var(--surface);
5364
+ border: 1px solid var(--border);
5365
+ border-radius: 8px;
5366
+ overflow: hidden;
5367
+ }
5368
+
5369
+ .alert-item {
5370
+ padding: 12px 16px;
5371
+ border-bottom: 1px solid var(--border);
5372
+ }
5373
+
5374
+ .alert-item .alert-title {
5375
+ font-size: 13px;
5376
+ font-weight: 500;
5377
+ margin-bottom: 4px;
5378
+ }
5379
+
5380
+ .alert-item .alert-desc {
5381
+ font-size: 12px;
5382
+ color: var(--text-secondary);
5383
+ margin-bottom: 8px;
5384
+ }
5385
+
5386
+ .alert-actions {
5387
+ display: flex;
5388
+ gap: 8px;
5389
+ }
5390
+
5391
+ .alert-actions button {
5392
+ padding: 4px 12px;
5393
+ border-radius: 4px;
5394
+ border: 1px solid var(--border);
5395
+ font-size: 12px;
5396
+ cursor: pointer;
5397
+ transition: all 0.15s;
5398
+ }
5399
+
5400
+ .approve-btn {
5401
+ background: var(--green-dim);
5402
+ color: white;
5403
+ border-color: var(--green-dim) !important;
5404
+ }
5405
+
5406
+ .approve-btn:hover { opacity: 0.9; }
5407
+
5408
+ .deny-btn {
5409
+ background: none;
5410
+ color: var(--red);
5411
+ border-color: var(--red-dim) !important;
5412
+ }
5413
+
5414
+ .deny-btn:hover {
5415
+ background: rgba(248, 81, 73, 0.1);
5416
+ }
5417
+
5418
+ .alerts-empty {
5419
+ padding: 40px 16px;
5420
+ text-align: center;
5421
+ color: var(--text-muted);
5422
+ font-size: 14px;
5423
+ }
5424
+
5425
+ /* \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 */
5426
+ .servers-panel {
5427
+ margin-top: 16px;
5428
+ }
5429
+
5430
+ .server-row {
5431
+ display: flex;
5432
+ align-items: center;
5433
+ gap: 8px;
5434
+ padding: 8px 16px;
5435
+ border-bottom: 1px solid var(--border);
5436
+ font-size: 13px;
5437
+ }
5438
+
5439
+ .server-status-dot {
5440
+ width: 8px;
5441
+ height: 8px;
5442
+ border-radius: 50%;
5443
+ }
5444
+
5445
+ .server-status-dot.connected { background: var(--green); }
5446
+ .server-status-dot.connecting { background: var(--amber); }
5447
+ .server-status-dot.disconnected, .server-status-dot.error { background: var(--red); }
5448
+
5449
+ .server-name {
5450
+ font-family: 'SF Mono', 'Fira Code', monospace;
5451
+ font-size: 12px;
5452
+ }
5453
+
5454
+ .server-tier {
5455
+ margin-left: auto;
5456
+ font-size: 11px;
5457
+ color: var(--text-secondary);
5458
+ }
5459
+ </style>
5460
+ </head>
5461
+ <body>
5462
+ <!-- Header -->
5463
+ <div class="fortress-header">
5464
+ <div class="fortress-brand">
5465
+ <div class="shield">&#x1F6E1;</div>
5466
+ <div>
5467
+ <h1>Sanctuary Cocoon</h1>
5468
+ <div class="version">v${esc(options.serverVersion)}</div>
5469
+ </div>
5470
+ </div>
5471
+ <div class="header-actions">
5472
+ <button class="pause-btn" id="pause-btn" title="Pause agent \u2014 requires approval for all operations">Pause Agent</button>
5473
+ <button id="advanced-btn">Advanced</button>
5474
+ </div>
5475
+ </div>
5476
+
5477
+ <!-- Tab bar -->
5478
+ <div class="tab-bar">
5479
+ <button class="active" data-tab="fortress">Fortress</button>
5480
+ <button data-tab="advanced">Advanced</button>
5481
+ </div>
5482
+
5483
+ <!-- Fortress View -->
5484
+ <div class="fortress-content" id="fortress-tab">
5485
+ <!-- Status Banner -->
5486
+ <div class="status-banner" id="status-banner">
5487
+ <div class="status-indicator green" id="status-indicator">&#x2713;</div>
5488
+ <div class="status-info">
5489
+ <h2 id="status-title">Agent Protected</h2>
5490
+ <p id="status-subtitle">${options.upstreamServerCount} server${options.upstreamServerCount !== 1 ? "s" : ""} monitored. All systems nominal.</p>
5491
+ </div>
5492
+ <div class="status-stats">
5493
+ <div class="stat">
5494
+ <div class="value" id="stat-total">0</div>
5495
+ <div class="label">Calls</div>
5496
+ </div>
5497
+ <div class="stat">
5498
+ <div class="value" id="stat-blocked">0</div>
5499
+ <div class="label">Blocked</div>
5500
+ </div>
5501
+ <div class="stat">
5502
+ <div class="value" id="stat-pending">0</div>
5503
+ <div class="label">Pending</div>
5504
+ </div>
5505
+ </div>
5506
+ </div>
5507
+
5508
+ <!-- Two-column layout -->
5509
+ <div class="fortress-grid">
5510
+ <!-- Live Feed -->
5511
+ <div class="feed-panel">
5512
+ <div class="panel-header">
5513
+ <h3>Live Activity</h3>
5514
+ <span style="font-size: 12px; color: var(--text-muted);" id="feed-count">0 events</span>
5515
+ </div>
5516
+ <div class="feed-list" id="feed-list">
5517
+ <div class="feed-empty">Waiting for tool calls...</div>
5518
+ </div>
5519
+ </div>
5520
+
5521
+ <!-- Right column: Alerts + Servers -->
5522
+ <div>
5523
+ <!-- Alerts -->
5524
+ <div class="alerts-panel">
5525
+ <div class="panel-header">
5526
+ <h3>Needs Attention</h3>
5527
+ <span style="font-size: 12px; color: var(--text-muted);" id="alert-count">0</span>
5528
+ </div>
5529
+ <div id="alerts-list">
5530
+ <div class="alerts-empty">No pending actions</div>
5531
+ </div>
5532
+ </div>
5533
+
5534
+ <!-- Servers -->
5535
+ <div class="alerts-panel servers-panel">
5536
+ <div class="panel-header">
5537
+ <h3>Upstream Servers</h3>
5538
+ </div>
5539
+ <div id="servers-list">
5540
+ <div class="alerts-empty">No servers configured</div>
5541
+ </div>
5542
+ </div>
5543
+ </div>
5544
+ </div>
5545
+ </div>
5546
+
5547
+ <script>
5548
+ // \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
5549
+ const API_BASE = window.location.origin;
5550
+ const SESSION_TOKEN = sessionStorage.getItem('sanctuary_session') || '';
5551
+ const MAX_FEED_ITEMS = 50;
5552
+
5553
+ let feedItems = [];
5554
+ let totalCalls = 0;
5555
+ let blockedCalls = 0;
5556
+ let pendingApprovals = [];
5557
+ let upstreamServers = [];
5558
+ let paused = false;
5559
+
5560
+ // \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
5561
+ function connectSSE() {
5562
+ const url = API_BASE + '/events' + (SESSION_TOKEN ? '?session=' + SESSION_TOKEN : '');
5563
+ const eventSource = new EventSource(url);
5564
+
5565
+ eventSource.addEventListener('proxy-call', (e) => {
5566
+ try {
5567
+ const data = JSON.parse(e.data);
5568
+ addFeedItem(data);
5569
+ } catch {}
5570
+ });
5571
+
5572
+ eventSource.addEventListener('proxy-server-status', (e) => {
5573
+ try {
5574
+ const data = JSON.parse(e.data);
5575
+ updateServerStatus(data.server, data.state, data.tool_count, data.error);
5576
+ } catch {}
5577
+ });
5578
+
5579
+ eventSource.addEventListener('injection-alert', (e) => {
5580
+ try {
5581
+ const data = JSON.parse(e.data);
5582
+ addFeedItem({
5583
+ tool: data.tool_name || 'unknown',
5584
+ server: 'detection',
5585
+ decision: 'blocked',
5586
+ reason: 'Injection detected: ' + (data.signals || []).join(', '),
5587
+ timestamp: new Date().toISOString(),
5588
+ });
5589
+ } catch {}
5590
+ });
5591
+
5592
+ eventSource.addEventListener('approval-request', (e) => {
5593
+ try {
5594
+ const data = JSON.parse(e.data);
5595
+ addPendingApproval(data);
5596
+ } catch {}
5597
+ });
5598
+
5599
+ eventSource.addEventListener('approval-resolved', (e) => {
5600
+ try {
5601
+ const data = JSON.parse(e.data);
5602
+ removePendingApproval(data.id);
5603
+ } catch {}
5604
+ });
5605
+
5606
+ eventSource.onerror = () => {
5607
+ eventSource.close();
5608
+ setTimeout(connectSSE, 3000);
5609
+ };
5610
+ }
5611
+
5612
+ // \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
5613
+ function addFeedItem(data) {
5614
+ totalCalls++;
5615
+ if (data.decision === 'blocked' || data.decision === 'denied') {
5616
+ blockedCalls++;
5617
+ }
5618
+
5619
+ feedItems.unshift({
5620
+ tool: data.tool || 'unknown',
5621
+ server: data.server || '',
5622
+ decision: data.decision || 'allowed',
5623
+ reason: data.reason || '',
5624
+ time: data.timestamp || new Date().toISOString(),
5625
+ });
5626
+
5627
+ if (feedItems.length > MAX_FEED_ITEMS) {
5628
+ feedItems = feedItems.slice(0, MAX_FEED_ITEMS);
5629
+ }
5630
+
5631
+ renderFeed();
5632
+ updateStats();
5633
+ updateStatus();
5634
+ }
5635
+
5636
+ function renderFeed() {
5637
+ const container = document.getElementById('feed-list');
5638
+ if (feedItems.length === 0) {
5639
+ container.innerHTML = '<div class="feed-empty">Waiting for tool calls...</div>';
5640
+ return;
5641
+ }
5642
+
5643
+ container.innerHTML = feedItems.map(item => {
5644
+ const dotColor = item.decision === 'allowed' ? 'green'
5645
+ : item.decision === 'pending' ? 'amber' : 'red';
5646
+ const decisionText = item.decision === 'allowed' ? 'Auto-allowed'
5647
+ : item.decision === 'pending' ? 'Awaiting approval'
5648
+ : item.decision === 'blocked' ? 'Blocked' : item.decision;
5649
+ const timeStr = new Date(item.time).toLocaleTimeString();
5650
+
5651
+ return '<div class="feed-item">' +
5652
+ '<div class="feed-dot ' + dotColor + '"></div>' +
5653
+ '<div class="feed-detail">' +
5654
+ '<div class="feed-tool">' + esc(item.tool) + '</div>' +
5655
+ '<div class="feed-decision">' + esc(decisionText) +
5656
+ (item.reason ? ' \u2014 ' + esc(item.reason) : '') + '</div>' +
5657
+ '</div>' +
5658
+ '<div class="feed-time">' + esc(timeStr) + '</div>' +
5659
+ '</div>';
5660
+ }).join('');
5661
+
5662
+ document.getElementById('feed-count').textContent = feedItems.length + ' events';
5663
+ }
5664
+
5665
+ // \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
5666
+ function addPendingApproval(data) {
5667
+ pendingApprovals.push(data);
5668
+ renderAlerts();
5669
+ updateStats();
5670
+ updateStatus();
5671
+ }
5672
+
5673
+ function removePendingApproval(id) {
5674
+ pendingApprovals = pendingApprovals.filter(a => a.id !== id);
5675
+ renderAlerts();
5676
+ updateStats();
5677
+ updateStatus();
5678
+ }
5679
+
5680
+ function renderAlerts() {
5681
+ const container = document.getElementById('alerts-list');
5682
+ if (pendingApprovals.length === 0) {
5683
+ container.innerHTML = '<div class="alerts-empty">No pending actions</div>';
5684
+ document.getElementById('alert-count').textContent = '0';
5685
+ return;
5686
+ }
5687
+
5688
+ document.getElementById('alert-count').textContent = pendingApprovals.length.toString();
5689
+
5690
+ container.innerHTML = pendingApprovals.map(approval => {
5691
+ return '<div class="alert-item">' +
5692
+ '<div class="alert-title">Approval required: ' + esc(approval.operation || approval.tool_name || 'unknown') + '</div>' +
5693
+ '<div class="alert-desc">' + esc(approval.reason || 'This operation requires your approval before it can proceed.') + '</div>' +
5694
+ '<div class="alert-actions">' +
5695
+ '<button class="approve-btn" onclick="handleApproval(\\'' + esc(approval.id) + '\\', true)">Approve</button>' +
5696
+ '<button class="deny-btn" onclick="handleApproval(\\'' + esc(approval.id) + '\\', false)">Deny</button>' +
5697
+ '</div>' +
5698
+ '</div>';
5699
+ }).join('');
5700
+ }
5701
+
5702
+ async function handleApproval(id, approved) {
5703
+ const endpoint = approved ? '/api/approve/' : '/api/deny/';
5704
+ try {
5705
+ await fetch(API_BASE + endpoint + id, {
5706
+ method: 'POST',
5707
+ headers: SESSION_TOKEN ? { 'Authorization': 'Bearer ' + SESSION_TOKEN } : {},
5708
+ });
5709
+ removePendingApproval(id);
5710
+ } catch (err) {
5711
+ console.error('Approval action failed:', err);
5712
+ }
5713
+ }
5714
+
5715
+ // \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
5716
+ function updateServerStatus(serverName, state, toolCount, error) {
5717
+ const existing = upstreamServers.find(s => s.name === serverName);
5718
+ if (existing) {
5719
+ existing.state = state;
5720
+ existing.tool_count = toolCount;
5721
+ existing.error = error;
5722
+ } else {
5723
+ upstreamServers.push({ name: serverName, state, tool_count: toolCount, error });
5724
+ }
5725
+ renderServers();
5726
+ updateStatus();
5727
+ }
5728
+
5729
+ function renderServers() {
5730
+ const container = document.getElementById('servers-list');
5731
+ if (upstreamServers.length === 0) {
5732
+ container.innerHTML = '<div class="alerts-empty">No servers configured</div>';
5733
+ return;
5734
+ }
5735
+
5736
+ container.innerHTML = upstreamServers.map(server => {
5737
+ const stateClass = server.state || 'disconnected';
5738
+ const stateLabel = server.state === 'connected' ? 'Connected'
5739
+ : server.state === 'connecting' ? 'Connecting...'
5740
+ : server.state === 'error' ? 'Error' : 'Disconnected';
5741
+
5742
+ return '<div class="server-row">' +
5743
+ '<div class="server-status-dot ' + stateClass + '"></div>' +
5744
+ '<span class="server-name">' + esc(server.name) + '</span>' +
5745
+ '<span class="server-tier">' + esc(stateLabel) +
5746
+ (server.tool_count ? ' (' + server.tool_count + ' tools)' : '') + '</span>' +
5747
+ '</div>';
5748
+ }).join('');
5749
+ }
5750
+
5751
+ // \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
5752
+ function updateStats() {
5753
+ document.getElementById('stat-total').textContent = totalCalls.toString();
5754
+ document.getElementById('stat-blocked').textContent = blockedCalls.toString();
5755
+ document.getElementById('stat-pending').textContent = pendingApprovals.length.toString();
5756
+ }
5757
+
5758
+ function updateStatus() {
5759
+ const indicator = document.getElementById('status-indicator');
5760
+ const title = document.getElementById('status-title');
5761
+ const subtitle = document.getElementById('status-subtitle');
5762
+
5763
+ const hasErrors = upstreamServers.some(s => s.state === 'error');
5764
+ const hasPending = pendingApprovals.length > 0;
5765
+ const hasBlocked = blockedCalls > 0;
5766
+
5767
+ if (paused) {
5768
+ indicator.className = 'status-indicator red';
5769
+ indicator.innerHTML = '&#x23F8;';
5770
+ title.textContent = 'Agent Paused';
5771
+ subtitle.textContent = 'All operations require approval. Click Resume to restore normal mode.';
5772
+ } else if (hasErrors) {
5773
+ indicator.className = 'status-indicator red';
5774
+ indicator.innerHTML = '&#x26A0;';
5775
+ title.textContent = 'Connection Issues';
5776
+ subtitle.textContent = 'One or more upstream servers have errors.';
5777
+ } else if (hasPending) {
5778
+ indicator.className = 'status-indicator amber';
5779
+ indicator.innerHTML = '&#x23F3;';
5780
+ title.textContent = 'Action Required';
5781
+ subtitle.textContent = pendingApprovals.length + ' operation' + (pendingApprovals.length > 1 ? 's' : '') + ' awaiting your approval.';
5782
+ } else {
5783
+ indicator.className = 'status-indicator green';
5784
+ indicator.innerHTML = '&#x2713;';
5785
+ title.textContent = 'Agent Protected';
5786
+ const serverCount = upstreamServers.filter(s => s.state === 'connected').length || ${options.upstreamServerCount};
5787
+ subtitle.textContent = serverCount + ' server' + (serverCount !== 1 ? 's' : '') + ' monitored. All systems nominal.';
5788
+ }
5789
+ }
5790
+
5791
+ // \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
5792
+ document.getElementById('pause-btn').addEventListener('click', () => {
5793
+ paused = !paused;
5794
+ const btn = document.getElementById('pause-btn');
5795
+ if (paused) {
5796
+ btn.textContent = 'Resume Agent';
5797
+ btn.classList.add('paused');
5798
+ } else {
5799
+ btn.textContent = 'Pause Agent';
5800
+ btn.classList.remove('paused');
5801
+ }
5802
+ updateStatus();
5803
+ // TODO: POST to /api/cocoon/pause to set all tiers to 1
5804
+ });
5805
+
5806
+ // \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
5807
+ document.getElementById('advanced-btn').addEventListener('click', () => {
5808
+ window.location.href = '/dashboard?session=' + SESSION_TOKEN;
5809
+ });
5810
+
5811
+ document.querySelectorAll('.tab-bar button').forEach(btn => {
5812
+ btn.addEventListener('click', () => {
5813
+ const tab = btn.dataset.tab;
5814
+ if (tab === 'advanced') {
5815
+ window.location.href = '/dashboard?session=' + SESSION_TOKEN;
5816
+ }
5817
+ });
5818
+ });
5819
+
5820
+ // \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
5821
+ function esc(str) {
5822
+ if (!str) return '';
5823
+ const d = document.createElement('div');
5824
+ d.textContent = String(str);
5825
+ return d.innerHTML;
5826
+ }
5827
+
5828
+ // \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
5829
+ async function init() {
5830
+ // Load initial server state
5831
+ try {
5832
+ const resp = await fetch(API_BASE + '/api/proxy/servers', {
5833
+ headers: SESSION_TOKEN ? { 'Authorization': 'Bearer ' + SESSION_TOKEN } : {},
5834
+ });
5835
+ if (resp.ok) {
5836
+ const data = await resp.json();
5837
+ upstreamServers = data.servers || [];
5838
+ renderServers();
5839
+ }
5840
+ } catch {}
5841
+
5842
+ // Load pending approvals
5843
+ try {
5844
+ const resp = await fetch(API_BASE + '/api/pending', {
5845
+ headers: SESSION_TOKEN ? { 'Authorization': 'Bearer ' + SESSION_TOKEN } : {},
5846
+ });
5847
+ if (resp.ok) {
5848
+ const data = await resp.json();
5849
+ pendingApprovals = data.pending || [];
5850
+ renderAlerts();
5851
+ updateStats();
5852
+ }
5853
+ } catch {}
5854
+
5855
+ updateStatus();
5856
+ connectSSE();
5857
+ }
5858
+
5859
+ init();
5860
+ </script>
5861
+ </body>
5862
+ </html>`;
5863
+ }
5864
+ function esc(str) {
5865
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
5866
+ }
5867
+ var init_fortress_view = __esm({
5868
+ "src/cocoon/fortress-view.ts"() {
5869
+ }
5870
+ });
5871
+
5872
+ // src/system-prompt-generator.ts
5873
+ function generateSystemPrompt(profile) {
5874
+ const activeFeatures = [];
5875
+ const inactiveFeatures = [];
5876
+ const activeKeys = [];
5877
+ const featureKeys = [
5878
+ "audit_logging",
5879
+ "injection_detection",
5880
+ "context_gating",
5881
+ "approval_gate",
5882
+ "zk_proofs"
5883
+ ];
5884
+ for (const key of featureKeys) {
5885
+ const featureConfig = profile.features[key];
5886
+ const info = FEATURE_INFO[key];
5887
+ if (featureConfig.enabled) {
5888
+ activeKeys.push(key);
5889
+ let desc = `- ${info.name}: ${info.activeDescription}`;
5890
+ if (key === "injection_detection" && "sensitivity" in featureConfig && featureConfig.sensitivity) {
5891
+ desc += ` Sensitivity: ${featureConfig.sensitivity}.`;
5892
+ }
5893
+ if (key === "context_gating" && "policy_id" in featureConfig && featureConfig.policy_id) {
5894
+ desc += ` Active policy: ${featureConfig.policy_id}.`;
5895
+ }
5896
+ activeFeatures.push(desc);
5897
+ } else {
5898
+ inactiveFeatures.push(info.disabledDescription);
5899
+ }
5900
+ }
5901
+ const lines = [];
5902
+ if (activeKeys.length > 0) {
5903
+ lines.push("QUICK START:");
5904
+ const quickStartItems = buildQuickStart(activeKeys);
5905
+ for (const item of quickStartItems) {
5906
+ lines.push(` ${item}`);
5907
+ }
5908
+ lines.push("");
5909
+ }
5910
+ lines.push(
5911
+ "You are protected by Sanctuary sovereignty infrastructure. The following protections are active:"
5912
+ );
5913
+ lines.push("");
5914
+ if (activeFeatures.length > 0) {
5915
+ lines.push(...activeFeatures);
5916
+ } else {
5917
+ lines.push(
5918
+ "- No features are currently enabled. Contact your operator to configure protections."
5919
+ );
5920
+ }
5921
+ if (inactiveFeatures.length > 0) {
5922
+ lines.push("");
5923
+ lines.push(
5924
+ `Optional tools available but not currently enabled: ${inactiveFeatures.join(", ")}.`
5925
+ );
5926
+ }
5927
+ return lines.join("\n");
5928
+ }
5929
+ function buildQuickStart(activeKeys) {
5930
+ const items = [];
5931
+ if (activeKeys.includes("context_gating")) {
5932
+ items.push(
5933
+ "1. ALWAYS call context_gate_filter before sending context to external APIs."
5934
+ );
5935
+ }
5936
+ if (activeKeys.includes("zk_proofs")) {
5937
+ items.push(
5938
+ `${items.length + 1}. Use zk_commit to prove claims without revealing underlying data.`
5939
+ );
5940
+ }
5941
+ if (activeKeys.includes("approval_gate")) {
5942
+ items.push(
5943
+ `${items.length + 1}. High-risk operations will be held for human approval \u2014 expect async responses.`
5944
+ );
5945
+ }
5946
+ if (items.length === 0) {
5947
+ if (activeKeys.includes("audit_logging")) {
5948
+ items.push("1. All tool calls are automatically logged to an encrypted audit trail.");
5949
+ }
5950
+ if (activeKeys.includes("injection_detection")) {
5951
+ items.push(
5952
+ `${items.length + 1}. Tool arguments are scanned for injection \u2014 blocked calls should not be retried.`
5953
+ );
5954
+ }
5955
+ }
5956
+ return items;
5957
+ }
5958
+ var FEATURE_INFO;
5959
+ var init_system_prompt_generator = __esm({
5960
+ "src/system-prompt-generator.ts"() {
5961
+ FEATURE_INFO = {
5962
+ audit_logging: {
5963
+ name: "Audit Logging",
5964
+ 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.",
5965
+ toolNames: ["monitor_audit_log"],
5966
+ disabledDescription: "audit logging (monitor_audit_log)",
5967
+ usageExample: "Automatic \u2014 every tool call you make is recorded. No explicit action required."
5968
+ },
5969
+ injection_detection: {
5970
+ name: "Injection Detection",
5971
+ 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.",
5972
+ disabledDescription: "injection detection",
5973
+ usageExample: "Automatic \u2014 if a tool call is blocked with an injection alert, do not retry with the same arguments."
5974
+ },
5975
+ context_gating: {
5976
+ name: "Context Gating",
5977
+ 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.",
5978
+ toolNames: [
5979
+ "context_gate_filter",
5980
+ "context_gate_set_policy",
5981
+ "context_gate_apply_template",
5982
+ "context_gate_recommend",
5983
+ "context_gate_list_policies"
5984
+ ],
5985
+ disabledDescription: "context gating (context_gate_filter)",
5986
+ usageExample: "Before calling an external API, run: context_gate_filter with your context object and policy_id to get a filtered version."
5987
+ },
5988
+ approval_gate: {
5989
+ name: "Approval Gates",
5990
+ 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.",
5991
+ disabledDescription: "approval gates",
5992
+ 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."
5993
+ },
5994
+ zk_proofs: {
5995
+ name: "Zero-Knowledge Proofs",
5996
+ 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.",
5997
+ toolNames: [
5998
+ "zk_commit",
5999
+ "zk_prove",
6000
+ "zk_range_prove",
6001
+ "proof_commitment"
6002
+ ],
6003
+ disabledDescription: "zero-knowledge proofs (zk_commit, zk_prove)",
6004
+ 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."
6005
+ }
6006
+ };
6007
+ }
6008
+ });
6009
+ var SESSION_TTL_REMOTE_MS, SESSION_TTL_LOCAL_MS, MAX_SESSIONS, RATE_LIMIT_WINDOW_MS, RATE_LIMIT_GENERAL, RATE_LIMIT_DECISIONS, MAX_RATE_LIMIT_ENTRIES, DashboardApprovalChannel;
6010
+ var init_dashboard = __esm({
6011
+ "src/principal-policy/dashboard.ts"() {
6012
+ init_config();
6013
+ init_generator();
6014
+ init_dashboard_html();
6015
+ init_fortress_view();
6016
+ init_system_prompt_generator();
6017
+ SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
6018
+ SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
6019
+ MAX_SESSIONS = 1e3;
6020
+ RATE_LIMIT_WINDOW_MS = 6e4;
6021
+ RATE_LIMIT_GENERAL = 120;
6022
+ RATE_LIMIT_DECISIONS = 20;
6023
+ MAX_RATE_LIMIT_ENTRIES = 1e4;
6024
+ DashboardApprovalChannel = class {
6025
+ config;
6026
+ pending = /* @__PURE__ */ new Map();
6027
+ sseClients = /* @__PURE__ */ new Set();
6028
+ httpServer = null;
6029
+ policy = null;
6030
+ baseline = null;
6031
+ auditLog = null;
6032
+ identityManager = null;
6033
+ handshakeResults = null;
6034
+ shrOpts = null;
6035
+ _sanctuaryConfig = null;
6036
+ profileStore = null;
6037
+ clientManager = null;
6038
+ dashboardHTML;
6039
+ fortressHTML = null;
6040
+ loginHTML;
6041
+ authToken;
6042
+ useTLS;
6043
+ /** Session TTL: longer for localhost, shorter for remote */
6044
+ sessionTTLMs;
6045
+ /** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
6046
+ sessions = /* @__PURE__ */ new Map();
6047
+ sessionCleanupTimer = null;
6048
+ /** Rate limiting: per-IP request tracking */
6049
+ rateLimits = /* @__PURE__ */ new Map();
6050
+ /** Whether the dashboard is running in standalone mode (no MCP server) */
6051
+ _standaloneMode = false;
6052
+ constructor(config) {
6053
+ this.config = config;
6054
+ this.authToken = config.auth_token;
6055
+ this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
6056
+ const isLocalhost = config.host === "127.0.0.1" || config.host === "localhost" || config.host === "::1";
6057
+ this.sessionTTLMs = isLocalhost ? SESSION_TTL_LOCAL_MS : SESSION_TTL_REMOTE_MS;
6058
+ this.dashboardHTML = generateDashboardHTML({
6059
+ timeoutSeconds: config.timeout_seconds,
6060
+ serverVersion: SANCTUARY_VERSION
6061
+ });
6062
+ this.loginHTML = generateLoginHTML({ serverVersion: SANCTUARY_VERSION });
6063
+ this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
6064
+ }
6065
+ /**
6066
+ * Inject dependencies after construction.
6067
+ * Called from index.ts after all components are initialized.
6068
+ */
6069
+ setDependencies(deps) {
6070
+ this.policy = deps.policy;
6071
+ this.baseline = deps.baseline;
6072
+ this.auditLog = deps.auditLog;
6073
+ if (deps.identityManager) this.identityManager = deps.identityManager;
6074
+ if (deps.handshakeResults) this.handshakeResults = deps.handshakeResults;
6075
+ if (deps.shrOpts) this.shrOpts = deps.shrOpts;
6076
+ if (deps.sanctuaryConfig) this._sanctuaryConfig = deps.sanctuaryConfig;
6077
+ if (deps.profileStore) this.profileStore = deps.profileStore;
6078
+ if (deps.clientManager) this.clientManager = deps.clientManager;
6079
+ }
6080
+ /**
6081
+ * Mark this dashboard as running in standalone mode.
6082
+ * Exposed via /api/status so the frontend can show an appropriate banner.
6083
+ */
6084
+ setStandaloneMode(standalone) {
6085
+ this._standaloneMode = standalone;
6086
+ }
6087
+ /**
6088
+ * Start the HTTP(S) server for the dashboard.
6089
+ */
6090
+ async start() {
6091
+ return new Promise((resolve, reject) => {
6092
+ const handler = (req, res) => this.handleRequest(req, res);
6093
+ if (this.useTLS && this.config.tls) {
6094
+ const tlsOpts = {
6095
+ cert: readFileSync(this.config.tls.cert_path),
6096
+ key: readFileSync(this.config.tls.key_path)
6097
+ };
6098
+ this.httpServer = createServer$2(tlsOpts, handler);
6099
+ } else {
6100
+ this.httpServer = createServer$1(handler);
6101
+ }
6102
+ const protocol = this.useTLS ? "https" : "http";
6103
+ const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
6104
+ this.httpServer.listen(this.config.port, this.config.host, () => {
6105
+ const sessionUrl = this.authToken ? this.createSessionUrl() : baseUrl;
6106
+ process.stderr.write(
6107
+ `
6108
+ Sanctuary Principal Dashboard: ${baseUrl}
6109
+ `
6110
+ );
6111
+ if (this.authToken) {
5279
6112
  const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
5280
6113
  process.stderr.write(
5281
6114
  ` Auth token: ${hint}
@@ -5589,8 +6422,14 @@ var init_dashboard = __esm({
5589
6422
  if (!this.checkAuth(req, url, res)) return;
5590
6423
  if (!this.checkRateLimit(req, res, "general")) return;
5591
6424
  try {
5592
- if (method === "GET" && (url.pathname === "/" || url.pathname === "/dashboard")) {
5593
- this.serveDashboard(res);
6425
+ if (method === "GET" && url.pathname === "/fortress") {
6426
+ this.serveFortressView(res);
6427
+ } else if (method === "GET" && (url.pathname === "/" || url.pathname === "/dashboard")) {
6428
+ if (this.fortressHTML) {
6429
+ this.serveFortressView(res);
6430
+ } else {
6431
+ this.serveDashboard(res);
6432
+ }
5594
6433
  } else if (method === "GET" && url.pathname === "/events") {
5595
6434
  this.handleSSE(req, res);
5596
6435
  } else if (method === "GET" && url.pathname === "/api/status") {
@@ -5683,7 +6522,36 @@ var init_dashboard = __esm({
5683
6522
  "Content-Type": "text/html; charset=utf-8",
5684
6523
  "Cache-Control": "no-cache"
5685
6524
  });
5686
- res.end(this.dashboardHTML);
6525
+ res.end(this.dashboardHTML);
6526
+ }
6527
+ serveFortressView(res) {
6528
+ if (!this.fortressHTML) {
6529
+ this.serveDashboard(res);
6530
+ return;
6531
+ }
6532
+ res.writeHead(200, {
6533
+ "Content-Type": "text/html; charset=utf-8",
6534
+ "Cache-Control": "no-cache"
6535
+ });
6536
+ res.end(this.fortressHTML);
6537
+ }
6538
+ /**
6539
+ * Enable Fortress View (Cocoon mode) with the given upstream server count.
6540
+ * Once enabled, the root path `/` serves the Fortress View instead of the
6541
+ * standard dashboard. The standard dashboard remains available at `/dashboard`.
6542
+ */
6543
+ enableFortressView(upstreamServerCount) {
6544
+ this.fortressHTML = generateFortressViewHTML({
6545
+ serverVersion: SANCTUARY_VERSION,
6546
+ authToken: this.authToken,
6547
+ upstreamServerCount
6548
+ });
6549
+ }
6550
+ /**
6551
+ * Broadcast a proxy call event to connected dashboards (Fortress View feed).
6552
+ */
6553
+ broadcastProxyCall(data) {
6554
+ this.broadcastSSE("proxy-call", data);
5687
6555
  }
5688
6556
  handleSSE(req, res) {
5689
6557
  res.writeHead(200, {
@@ -6329,6 +7197,445 @@ var init_sovereignty_profile = __esm({
6329
7197
  };
6330
7198
  }
6331
7199
  });
7200
+ async function backupConfig(configPath) {
7201
+ await mkdir(BACKUP_DIR, { recursive: true, mode: 448 });
7202
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7203
+ const backupPath = join(BACKUP_DIR, `config-backup-${timestamp}.json`);
7204
+ await copyFile(configPath, backupPath);
7205
+ return backupPath;
7206
+ }
7207
+ async function restoreConfig(backupPath, targetPath) {
7208
+ await copyFile(backupPath, targetPath);
7209
+ }
7210
+ async function findLatestBackup() {
7211
+ const metaPath = join(BACKUP_DIR, "cocoon-meta.json");
7212
+ try {
7213
+ const raw = await readFile(metaPath, "utf-8");
7214
+ const meta = JSON.parse(raw);
7215
+ return {
7216
+ backupPath: meta.backupPath,
7217
+ originalPath: meta.originalPath
7218
+ };
7219
+ } catch {
7220
+ return null;
7221
+ }
7222
+ }
7223
+ async function saveCocoonMeta(meta) {
7224
+ await mkdir(BACKUP_DIR, { recursive: true, mode: 448 });
7225
+ const metaPath = join(BACKUP_DIR, "cocoon-meta.json");
7226
+ await writeFile(metaPath, JSON.stringify(meta, null, 2), { mode: 384 });
7227
+ }
7228
+ async function detectAgentConfig(platform2, configPath) {
7229
+ if (configPath) {
7230
+ return readConfigFile(configPath, platform2 ?? "generic");
7231
+ }
7232
+ if (platform2) {
7233
+ const paths = PLATFORM_PATHS[platform2];
7234
+ for (const path of paths) {
7235
+ const config = await readConfigFile(path, platform2);
7236
+ if (config) return config;
7237
+ }
7238
+ return null;
7239
+ }
7240
+ for (const [plat, paths] of Object.entries(PLATFORM_PATHS)) {
7241
+ for (const path of paths) {
7242
+ const config = await readConfigFile(path, plat);
7243
+ if (config) return config;
7244
+ }
7245
+ }
7246
+ return null;
7247
+ }
7248
+ async function readConfigFile(path, platform2) {
7249
+ try {
7250
+ await access(path);
7251
+ } catch {
7252
+ return null;
7253
+ }
7254
+ try {
7255
+ const raw = await readFile(path, "utf-8");
7256
+ const config = JSON.parse(raw);
7257
+ const servers = extractServers(config, platform2);
7258
+ return { platform: platform2, configPath: path, servers, rawConfig: config };
7259
+ } catch {
7260
+ return null;
7261
+ }
7262
+ }
7263
+ function extractServers(config, platform2) {
7264
+ if (!config || typeof config !== "object") return [];
7265
+ const servers = [];
7266
+ const obj = config;
7267
+ if (platform2 === "openclaw" || platform2 === "generic") {
7268
+ const mcp = obj.mcp;
7269
+ const nestedServers = mcp?.servers;
7270
+ if (nestedServers && typeof nestedServers === "object") {
7271
+ for (const [name, serverConfig] of Object.entries(nestedServers)) {
7272
+ const entry = parseServerEntry(name, serverConfig);
7273
+ if (entry) servers.push(entry);
7274
+ }
7275
+ }
7276
+ if (servers.length === 0) {
7277
+ const mcpServers = obj.mcpServers;
7278
+ if (mcpServers && typeof mcpServers === "object") {
7279
+ for (const [name, serverConfig] of Object.entries(mcpServers)) {
7280
+ const entry = parseServerEntry(name, serverConfig);
7281
+ if (entry) servers.push(entry);
7282
+ }
7283
+ }
7284
+ }
7285
+ }
7286
+ if (platform2 === "claude-code") {
7287
+ const mcpServers = obj.mcpServers;
7288
+ if (mcpServers && typeof mcpServers === "object") {
7289
+ for (const [name, serverConfig] of Object.entries(mcpServers)) {
7290
+ if (name.toLowerCase().includes("sanctuary")) continue;
7291
+ const entry = parseServerEntry(name, serverConfig);
7292
+ if (entry) servers.push(entry);
7293
+ }
7294
+ }
7295
+ }
7296
+ if (platform2 === "cursor") {
7297
+ const mcpServers = obj.mcpServers;
7298
+ if (mcpServers && typeof mcpServers === "object") {
7299
+ for (const [name, serverConfig] of Object.entries(mcpServers)) {
7300
+ if (name.toLowerCase().includes("sanctuary")) continue;
7301
+ const entry = parseServerEntry(name, serverConfig);
7302
+ if (entry) servers.push(entry);
7303
+ }
7304
+ }
7305
+ }
7306
+ return servers;
7307
+ }
7308
+ function parseServerEntry(name, config) {
7309
+ if (!config || typeof config !== "object") return null;
7310
+ const c = config;
7311
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "-").substring(0, 128);
7312
+ if (!safeName) return null;
7313
+ if (c.url && typeof c.url === "string") {
7314
+ return {
7315
+ name: safeName,
7316
+ transport: "sse",
7317
+ url: c.url,
7318
+ env: extractEnv(c.env)
7319
+ };
7320
+ }
7321
+ if (c.command && typeof c.command === "string") {
7322
+ return {
7323
+ name: safeName,
7324
+ transport: "stdio",
7325
+ command: c.command,
7326
+ args: Array.isArray(c.args) ? c.args.filter((a) => typeof a === "string") : void 0,
7327
+ env: extractEnv(c.env)
7328
+ };
7329
+ }
7330
+ return null;
7331
+ }
7332
+ function extractEnv(env) {
7333
+ if (!env || typeof env !== "object") return void 0;
7334
+ const result = {};
7335
+ for (const [k, v] of Object.entries(env)) {
7336
+ if (typeof v === "string") result[k] = v;
7337
+ }
7338
+ return Object.keys(result).length > 0 ? result : void 0;
7339
+ }
7340
+ async function rewriteConfigForCocoon(agentConfig, sanctuaryCommand, sanctuaryArgs, sanctuaryEnv) {
7341
+ const raw = agentConfig.rawConfig;
7342
+ let existingServers = {};
7343
+ if (agentConfig.platform === "openclaw") {
7344
+ const existingMcp = raw.mcp ?? {};
7345
+ existingServers = existingMcp.servers ?? {};
7346
+ } else {
7347
+ existingServers = raw.mcpServers ?? {};
7348
+ }
7349
+ let resolvedEnv = sanctuaryEnv;
7350
+ if (!resolvedEnv) {
7351
+ const existingSanctuary = existingServers.sanctuary;
7352
+ if (existingSanctuary?.env && typeof existingSanctuary.env === "object") {
7353
+ const extracted = extractEnv(existingSanctuary.env);
7354
+ if (extracted) resolvedEnv = extracted;
7355
+ }
7356
+ }
7357
+ const sanctuaryEntry = {
7358
+ command: sanctuaryCommand,
7359
+ args: sanctuaryArgs
7360
+ };
7361
+ if (resolvedEnv && Object.keys(resolvedEnv).length > 0) {
7362
+ sanctuaryEntry.env = resolvedEnv;
7363
+ }
7364
+ let rewritten;
7365
+ if (agentConfig.platform === "openclaw") {
7366
+ const existingMcp = raw.mcp ?? {};
7367
+ rewritten = {
7368
+ ...raw,
7369
+ mcp: {
7370
+ ...existingMcp,
7371
+ servers: {
7372
+ ...existingServers,
7373
+ sanctuary: sanctuaryEntry
7374
+ }
7375
+ }
7376
+ };
7377
+ delete rewritten.mcpServers;
7378
+ } else {
7379
+ rewritten = {
7380
+ ...raw,
7381
+ mcpServers: {
7382
+ ...existingServers,
7383
+ sanctuary: sanctuaryEntry
7384
+ }
7385
+ };
7386
+ }
7387
+ await writeFile(agentConfig.configPath, JSON.stringify(rewritten, null, 2), { mode: 384 });
7388
+ return agentConfig.configPath;
7389
+ }
7390
+ var PLATFORM_PATHS, BACKUP_DIR;
7391
+ var init_config_reader = __esm({
7392
+ "src/cocoon/config-reader.ts"() {
7393
+ PLATFORM_PATHS = {
7394
+ "openclaw": [
7395
+ join(homedir(), ".openclaw", "openclaw.json"),
7396
+ join(homedir(), ".openclaw", "config.json"),
7397
+ join(homedir(), "Library", "Application Support", "OpenClaw", "openclaw.json"),
7398
+ join(homedir(), "Library", "Application Support", "OpenClaw", "config.json")
7399
+ ],
7400
+ "claude-code": [
7401
+ join(homedir(), ".claude", "settings.json"),
7402
+ join(homedir(), ".config", "claude-code", "settings.json")
7403
+ ],
7404
+ "cursor": [
7405
+ join(homedir(), ".cursor", "mcp.json")
7406
+ ],
7407
+ "generic": []
7408
+ };
7409
+ BACKUP_DIR = join(homedir(), ".sanctuary", "backup");
7410
+ }
7411
+ });
7412
+
7413
+ // src/cocoon/cli.ts
7414
+ var cli_exports = {};
7415
+ __export(cli_exports, {
7416
+ COCOON_GOVERNOR_DEFAULTS: () => COCOON_GOVERNOR_DEFAULTS,
7417
+ parseCocoonArgs: () => parseCocoonArgs,
7418
+ runCocoon: () => runCocoon
7419
+ });
7420
+ async function runCocoon(options) {
7421
+ if (options.unwrap) {
7422
+ await unwrap();
7423
+ return;
7424
+ }
7425
+ let platform2;
7426
+ if (options.openclaw) platform2 = "openclaw";
7427
+ else if (options.claudeCode) platform2 = "claude-code";
7428
+ else if (options.cursor) platform2 = "cursor";
7429
+ const agentConfig = await detectAgentConfig(platform2, options.wrap);
7430
+ if (!agentConfig) {
7431
+ if (platform2) {
7432
+ console.error(`Could not find ${platform2} configuration. Check that the agent is installed.`);
7433
+ } else if (options.wrap) {
7434
+ console.error(`Could not read config file: ${options.wrap}`);
7435
+ } else {
7436
+ console.error("Could not auto-detect any agent configuration.");
7437
+ console.error("Use --openclaw, --claude-code, --cursor, or --wrap /path/to/config.json");
7438
+ }
7439
+ process.exit(1);
7440
+ }
7441
+ if (agentConfig.servers.length === 0) {
7442
+ console.error(`Found ${agentConfig.platform} config at ${agentConfig.configPath}, but no MCP servers configured.`);
7443
+ process.exit(1);
7444
+ }
7445
+ console.error(`
7446
+ Sanctuary Cocoon
7447
+ `);
7448
+ console.error(` Platform: ${agentConfig.platform}`);
7449
+ console.error(` Config: ${agentConfig.configPath}`);
7450
+ console.error(` MCP servers found: ${agentConfig.servers.length}
7451
+ `);
7452
+ const upstreamServers = convertToUpstreamServers(agentConfig.servers);
7453
+ for (const server of upstreamServers) {
7454
+ const overrideCount = Object.keys(server.tool_overrides ?? {}).length;
7455
+ console.error(` \u2192 ${server.name} (${server.transport.type}) \u2014 default: Tier ${server.default_tier}`);
7456
+ if (overrideCount > 0) {
7457
+ console.error(` ${overrideCount} tool-specific tier overrides`);
7458
+ }
7459
+ }
7460
+ if (options.dryRun) {
7461
+ console.error(`
7462
+ Dry run \u2014 no changes made.
7463
+ `);
7464
+ return;
7465
+ }
7466
+ const storagePath = join(homedir(), ".sanctuary");
7467
+ await mkdir(storagePath, { recursive: true, mode: 448 });
7468
+ const profile = createCocoonProfile(upstreamServers);
7469
+ const cocoonConfigPath = join(storagePath, "cocoon-profile.json");
7470
+ await writeFile(cocoonConfigPath, JSON.stringify(profile, null, 2), { mode: 384 });
7471
+ const backupPath = await backupConfig(agentConfig.configPath);
7472
+ await saveCocoonMeta({
7473
+ backupPath,
7474
+ originalPath: agentConfig.configPath,
7475
+ platform: agentConfig.platform,
7476
+ wrappedAt: (/* @__PURE__ */ new Date()).toISOString()
7477
+ });
7478
+ console.error(`
7479
+ Original config backed up to: ${backupPath}`);
7480
+ await rewriteConfigForCocoon(
7481
+ agentConfig,
7482
+ "npx",
7483
+ [
7484
+ "@sanctuary-framework/mcp-server",
7485
+ ...options.passphrase ? ["--passphrase", options.passphrase] : []
7486
+ ]
7487
+ );
7488
+ console.error(` Agent config rewritten to route through Sanctuary`);
7489
+ const dashboardPort = options.port ?? 3501;
7490
+ console.error(`
7491
+ Your agent is now protected.`);
7492
+ console.error(` All tool calls are being logged and scanned.`);
7493
+ console.error(`
7494
+ To view the dashboard, run in a separate terminal:`);
7495
+ console.error(` npx @sanctuary-framework/mcp-server dashboard --port ${dashboardPort}`);
7496
+ console.error(`
7497
+ To restore: npx @sanctuary-framework/cocoon --unwrap
7498
+ `);
7499
+ }
7500
+ async function unwrap() {
7501
+ const meta = await findLatestBackup();
7502
+ if (!meta) {
7503
+ console.error("No Cocoon wrapping found to restore.");
7504
+ console.error("Run --wrap or --openclaw first.");
7505
+ process.exit(1);
7506
+ }
7507
+ try {
7508
+ await access(meta.backupPath);
7509
+ } catch {
7510
+ console.error(`Backup file not found: ${meta.backupPath}`);
7511
+ process.exit(1);
7512
+ }
7513
+ await restoreConfig(meta.backupPath, meta.originalPath);
7514
+ console.error(`
7515
+ Sanctuary Cocoon \u2014 Unwrapped`);
7516
+ console.error(` Original config restored to: ${meta.originalPath}`);
7517
+ console.error(` Backup preserved at: ${meta.backupPath}
7518
+ `);
7519
+ }
7520
+ function convertToUpstreamServers(servers) {
7521
+ return servers.map((server) => {
7522
+ const upstream = {
7523
+ name: server.name,
7524
+ transport: server.transport === "sse" ? { type: "sse", url: server.url } : {
7525
+ type: "stdio",
7526
+ command: server.command,
7527
+ ...server.args ? { args: server.args } : {},
7528
+ ...server.env ? { env: server.env } : {}
7529
+ },
7530
+ enabled: true,
7531
+ default_tier: 2
7532
+ };
7533
+ return upstream;
7534
+ });
7535
+ }
7536
+ function createCocoonProfile(upstreamServers) {
7537
+ return {
7538
+ version: 1,
7539
+ features: {
7540
+ audit_logging: { enabled: true },
7541
+ // Non-negotiable
7542
+ injection_detection: { enabled: true },
7543
+ // Non-negotiable
7544
+ context_gating: { enabled: false },
7545
+ // Can enable later
7546
+ approval_gate: { enabled: true },
7547
+ // Core enforcement — always ON
7548
+ zk_proofs: { enabled: false }
7549
+ // Not needed for Cocoon
7550
+ },
7551
+ upstream_servers: upstreamServers,
7552
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
7553
+ };
7554
+ }
7555
+ function parseCocoonArgs(argv) {
7556
+ const options = {};
7557
+ for (let i = 0; i < argv.length; i++) {
7558
+ switch (argv[i]) {
7559
+ case "--wrap":
7560
+ options.wrap = argv[++i];
7561
+ break;
7562
+ case "--openclaw":
7563
+ options.openclaw = true;
7564
+ break;
7565
+ case "--claude-code":
7566
+ options.claudeCode = true;
7567
+ break;
7568
+ case "--cursor":
7569
+ options.cursor = true;
7570
+ break;
7571
+ case "--unwrap":
7572
+ options.unwrap = true;
7573
+ break;
7574
+ case "--passphrase":
7575
+ options.passphrase = argv[++i];
7576
+ break;
7577
+ case "--port":
7578
+ options.port = parseInt(argv[++i], 10);
7579
+ break;
7580
+ case "--dry-run":
7581
+ options.dryRun = true;
7582
+ break;
7583
+ case "--help":
7584
+ case "-h":
7585
+ printCocoonHelp();
7586
+ process.exit(0);
7587
+ }
7588
+ }
7589
+ return options;
7590
+ }
7591
+ function printCocoonHelp() {
7592
+ console.log(`
7593
+ Sanctuary Cocoon \u2014 Wrap any agent in sovereignty protection
7594
+
7595
+ Usage:
7596
+ npx @sanctuary-framework/cocoon --openclaw # Wrap OpenClaw agent
7597
+ npx @sanctuary-framework/cocoon --claude-code # Wrap Claude Code
7598
+ npx @sanctuary-framework/cocoon --cursor # Wrap Cursor
7599
+ npx @sanctuary-framework/cocoon --wrap config.json # Wrap generic MCP config
7600
+ npx @sanctuary-framework/cocoon --unwrap # Restore original config
7601
+
7602
+ Options:
7603
+ --openclaw Auto-detect and wrap OpenClaw agent
7604
+ --claude-code Auto-detect and wrap Claude Code
7605
+ --cursor Auto-detect and wrap Cursor
7606
+ --wrap <path> Wrap a specific MCP config file
7607
+ --unwrap Restore original config from backup
7608
+ --passphrase <p> Encryption passphrase
7609
+ --port <port> Dashboard port (default: 3501)
7610
+ --dry-run Show what would happen without making changes
7611
+ --help, -h Show this help
7612
+
7613
+ What happens:
7614
+ 1. Reads your agent's MCP server configuration
7615
+ 2. Backs up the original config to ~/.sanctuary/backup/
7616
+ 3. Rewrites the config so your agent routes through Sanctuary
7617
+ 4. All tool calls are logged, scanned for injection, and rate-limited
7618
+ 5. Dangerous operations require your approval via the dashboard
7619
+
7620
+ Rollback:
7621
+ --unwrap restores the original config from backup.
7622
+ Backups are preserved and never deleted.
7623
+ `);
7624
+ }
7625
+ var COCOON_GOVERNOR_DEFAULTS;
7626
+ var init_cli = __esm({
7627
+ "src/cocoon/cli.ts"() {
7628
+ init_config_reader();
7629
+ COCOON_GOVERNOR_DEFAULTS = {
7630
+ volume_limit: 200,
7631
+ // 200 calls per 10-minute window
7632
+ rate_limit_per_tool: 20,
7633
+ // 20 calls/min per individual tool
7634
+ lifetime_limit: 1e3
7635
+ // 1000 total calls per session
7636
+ };
7637
+ }
7638
+ });
6332
7639
 
6333
7640
  // src/dashboard-standalone.ts
6334
7641
  var dashboard_standalone_exports = {};
@@ -7380,7 +8687,7 @@ function createL3Tools(storage, masterKey, auditLog) {
7380
8687
  const tools = [
7381
8688
  // ─── Commitment Schemes ───────────────────────────────────────────────
7382
8689
  {
7383
- name: "sanctuary/proof_commitment",
8690
+ name: "proof_commitment",
7384
8691
  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).",
7385
8692
  inputSchema: {
7386
8693
  type: "object",
@@ -7415,7 +8722,7 @@ function createL3Tools(storage, masterKey, auditLog) {
7415
8722
  }
7416
8723
  },
7417
8724
  {
7418
- name: "sanctuary/proof_reveal",
8725
+ name: "proof_reveal",
7419
8726
  description: "Verify a previously committed value by revealing it with the blinding factor. Returns whether the revealed value matches the commitment.",
7420
8727
  inputSchema: {
7421
8728
  type: "object",
@@ -7453,7 +8760,7 @@ function createL3Tools(storage, masterKey, auditLog) {
7453
8760
  },
7454
8761
  // ─── Disclosure Policies ──────────────────────────────────────────────
7455
8762
  {
7456
- name: "sanctuary/disclosure_set_policy",
8763
+ name: "disclosure_set_policy",
7457
8764
  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.",
7458
8765
  inputSchema: {
7459
8766
  type: "object",
@@ -7528,7 +8835,7 @@ function createL3Tools(storage, masterKey, auditLog) {
7528
8835
  }
7529
8836
  },
7530
8837
  {
7531
- name: "sanctuary/disclosure_evaluate",
8838
+ name: "disclosure_evaluate",
7532
8839
  description: "Evaluate a disclosure request against an active policy. Returns per-field decisions: disclose, withhold, proof, or ask-principal.",
7533
8840
  inputSchema: {
7534
8841
  type: "object",
@@ -7604,7 +8911,7 @@ function createL3Tools(storage, masterKey, auditLog) {
7604
8911
  },
7605
8912
  // ─── ZK Proof Tools ───────────────────────────────────────────────────
7606
8913
  {
7607
- name: "sanctuary/zk_commit",
8914
+ name: "zk_commit",
7608
8915
  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.",
7609
8916
  inputSchema: {
7610
8917
  type: "object",
@@ -7635,7 +8942,7 @@ function createL3Tools(storage, masterKey, auditLog) {
7635
8942
  }
7636
8943
  },
7637
8944
  {
7638
- name: "sanctuary/zk_prove",
8945
+ name: "zk_prove",
7639
8946
  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.",
7640
8947
  inputSchema: {
7641
8948
  type: "object",
@@ -7676,7 +8983,7 @@ function createL3Tools(storage, masterKey, auditLog) {
7676
8983
  }
7677
8984
  },
7678
8985
  {
7679
- name: "sanctuary/zk_verify",
8986
+ name: "zk_verify",
7680
8987
  description: "Verify a zero-knowledge proof of knowledge for a Pedersen commitment. Checks that the prover knows the commitment's opening without learning anything.",
7681
8988
  inputSchema: {
7682
8989
  type: "object",
@@ -7704,7 +9011,7 @@ function createL3Tools(storage, masterKey, auditLog) {
7704
9011
  }
7705
9012
  },
7706
9013
  {
7707
- name: "sanctuary/zk_range_prove",
9014
+ name: "zk_range_prove",
7708
9015
  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.",
7709
9016
  inputSchema: {
7710
9017
  type: "object",
@@ -7754,7 +9061,7 @@ function createL3Tools(storage, masterKey, auditLog) {
7754
9061
  }
7755
9062
  },
7756
9063
  {
7757
- name: "sanctuary/zk_range_verify",
9064
+ name: "zk_range_verify",
7758
9065
  description: "Verify a zero-knowledge range proof \u2014 confirms a committed value is within the claimed range without learning the value.",
7759
9066
  inputSchema: {
7760
9067
  type: "object",
@@ -8200,14 +9507,14 @@ function tierDistribution(tiers) {
8200
9507
  }
8201
9508
 
8202
9509
  // src/l4-reputation/tools.ts
8203
- function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeResults) {
9510
+ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeResults, verascoreUrl) {
8204
9511
  const reputationStore = new ReputationStore(storage, masterKey);
8205
9512
  const identityEncryptionKey = derivePurposeKey(masterKey, "identity-encryption");
8206
9513
  const hsResults = handshakeResults ?? /* @__PURE__ */ new Map();
8207
9514
  const tools = [
8208
9515
  // ─── Reputation Recording ─────────────────────────────────────────
8209
9516
  {
8210
- name: "sanctuary/reputation_record",
9517
+ name: "reputation_record",
8211
9518
  description: "Record an interaction outcome as a signed attestation. Creates an EAS-compatible attestation signed by the specified identity.",
8212
9519
  inputSchema: {
8213
9520
  type: "object",
@@ -8300,7 +9607,7 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
8300
9607
  },
8301
9608
  // ─── Reputation Query ─────────────────────────────────────────────
8302
9609
  {
8303
- name: "sanctuary/reputation_query",
9610
+ name: "reputation_query",
8304
9611
  description: "Query aggregated reputation data with filtering. Returns summary statistics, never raw interaction details.",
8305
9612
  inputSchema: {
8306
9613
  type: "object",
@@ -8348,7 +9655,7 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
8348
9655
  },
8349
9656
  // ─── Reputation Export ─────────────────────────────────────────────
8350
9657
  {
8351
- name: "sanctuary/reputation_export",
9658
+ name: "reputation_export",
8352
9659
  description: "Export a portable reputation bundle (SANCTUARY_REP_V1). Includes all signed attestations for independent verification.",
8353
9660
  inputSchema: {
8354
9661
  type: "object",
@@ -8407,7 +9714,7 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
8407
9714
  },
8408
9715
  // ─── Reputation Import ────────────────────────────────────────────
8409
9716
  {
8410
- name: "sanctuary/reputation_import",
9717
+ name: "reputation_import",
8411
9718
  description: "Import a reputation bundle from another Sanctuary instance. Verifies all attestation signatures by default.",
8412
9719
  inputSchema: {
8413
9720
  type: "object",
@@ -8459,7 +9766,7 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
8459
9766
  },
8460
9767
  // ─── Sovereignty-Weighted Query ──────────────────────────────────
8461
9768
  {
8462
- name: "sanctuary/reputation_query_weighted",
9769
+ name: "reputation_query_weighted",
8463
9770
  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.",
8464
9771
  inputSchema: {
8465
9772
  type: "object",
@@ -8515,7 +9822,7 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
8515
9822
  },
8516
9823
  // ─── Trust Bootstrap: Escrow ──────────────────────────────────────
8517
9824
  {
8518
- name: "sanctuary/bootstrap_create_escrow",
9825
+ name: "bootstrap_create_escrow",
8519
9826
  description: "Create an escrow record for trust bootstrapping. Allows new participants with no reputation to transact safely.",
8520
9827
  inputSchema: {
8521
9828
  type: "object",
@@ -8574,7 +9881,7 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
8574
9881
  },
8575
9882
  // ─── Trust Bootstrap: Guarantee ───────────────────────────────────
8576
9883
  {
8577
- name: "sanctuary/bootstrap_provide_guarantee",
9884
+ name: "bootstrap_provide_guarantee",
8578
9885
  description: "A principal provides a signed reputation guarantee for a new agent. The guarantee certificate can be presented to counterparties.",
8579
9886
  inputSchema: {
8580
9887
  type: "object",
@@ -8652,7 +9959,7 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
8652
9959
  },
8653
9960
  // ─── Verascore Reputation Publish ────────────────────────────────
8654
9961
  {
8655
- name: "sanctuary/reputation_publish",
9962
+ name: "reputation_publish",
8656
9963
  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.",
8657
9964
  inputSchema: {
8658
9965
  type: "object",
@@ -8690,8 +9997,18 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
8690
9997
  });
8691
9998
  }
8692
9999
  const publishType = args.type;
8693
- const veracoreUrl = args.verascore_url || "https://verascore.ai";
8694
- const ALLOWED_VERASCORE_HOSTS = ["verascore.ai", "www.verascore.ai", "api.verascore.ai"];
10000
+ const configuredVerascoreUrl = verascoreUrl || "https://verascore.ai";
10001
+ const veracoreUrl = args.verascore_url || configuredVerascoreUrl;
10002
+ const ALLOWED_VERASCORE_HOSTS = /* @__PURE__ */ new Set([
10003
+ "verascore.ai",
10004
+ "www.verascore.ai",
10005
+ "api.verascore.ai"
10006
+ ]);
10007
+ try {
10008
+ const configuredHost = new URL(configuredVerascoreUrl).hostname;
10009
+ ALLOWED_VERASCORE_HOSTS.add(configuredHost);
10010
+ } catch {
10011
+ }
8695
10012
  try {
8696
10013
  const parsed = new URL(veracoreUrl);
8697
10014
  if (parsed.protocol !== "https:") {
@@ -8699,9 +10016,9 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
8699
10016
  error: `verascore_url must use HTTPS. Got: ${parsed.protocol}`
8700
10017
  });
8701
10018
  }
8702
- if (!ALLOWED_VERASCORE_HOSTS.includes(parsed.hostname)) {
10019
+ if (!ALLOWED_VERASCORE_HOSTS.has(parsed.hostname)) {
8703
10020
  return toolResult({
8704
- error: `verascore_url must point to a known Verascore domain (${ALLOWED_VERASCORE_HOSTS.join(", ")}). Got: ${parsed.hostname}`
10021
+ error: `verascore_url must point to a known Verascore domain (${[...ALLOWED_VERASCORE_HOSTS].join(", ")}). Got: ${parsed.hostname}`
8705
10022
  });
8706
10023
  }
8707
10024
  } catch {
@@ -9264,7 +10581,7 @@ var InjectionDetector = class {
9264
10581
  }
9265
10582
  /**
9266
10583
  * Scan tool arguments for injection signals.
9267
- * @param toolName Full tool name (e.g., "sanctuary/state_read")
10584
+ * @param toolName Full tool name (e.g., "state_read")
9268
10585
  * @param args Tool arguments
9269
10586
  * @returns DetectionResult with all detected signals
9270
10587
  */
@@ -10265,7 +11582,7 @@ var ApprovalGate = class {
10265
11582
  /**
10266
11583
  * Evaluate a tool call against the Principal Policy.
10267
11584
  *
10268
- * @param toolName - Full MCP tool name (e.g., "sanctuary/state_export")
11585
+ * @param toolName - Full MCP tool name (e.g., "state_export")
10269
11586
  * @param args - Tool call arguments (for context extraction)
10270
11587
  * @returns GateResult indicating whether the call is allowed
10271
11588
  */
@@ -10535,7 +11852,7 @@ init_router();
10535
11852
  function createPrincipalPolicyTools(policy, baseline, auditLog) {
10536
11853
  return [
10537
11854
  {
10538
- name: "sanctuary/principal_policy_view",
11855
+ name: "principal_policy_view",
10539
11856
  description: "View the current Principal Policy \u2014 the human-controlled rules governing what operations require approval. Read-only.",
10540
11857
  inputSchema: {
10541
11858
  type: "object",
@@ -10573,7 +11890,7 @@ function createPrincipalPolicyTools(policy, baseline, auditLog) {
10573
11890
  }
10574
11891
  },
10575
11892
  {
10576
- name: "sanctuary/principal_baseline_view",
11893
+ name: "principal_baseline_view",
10577
11894
  description: "View the current behavioral baseline \u2014 the session profile used for anomaly detection. Shows known namespaces, counterparties, and tool call counts. Read-only.",
10578
11895
  inputSchema: {
10579
11896
  type: "object",
@@ -10922,7 +12239,7 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
10922
12239
  };
10923
12240
  const tools = [
10924
12241
  {
10925
- name: "sanctuary/shr_generate",
12242
+ name: "shr_generate",
10926
12243
  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.",
10927
12244
  inputSchema: {
10928
12245
  type: "object",
@@ -10951,7 +12268,7 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
10951
12268
  }
10952
12269
  },
10953
12270
  {
10954
- name: "sanctuary/shr_verify",
12271
+ name: "shr_verify",
10955
12272
  description: "Verify a counterparty's Sovereignty Health Report (SHR). Checks signature validity, temporal validity, and assesses sovereignty level.",
10956
12273
  inputSchema: {
10957
12274
  type: "object",
@@ -10977,7 +12294,7 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
10977
12294
  }
10978
12295
  },
10979
12296
  {
10980
- name: "sanctuary/shr_gateway_export",
12297
+ name: "shr_gateway_export",
10981
12298
  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.",
10982
12299
  inputSchema: {
10983
12300
  type: "object",
@@ -11030,6 +12347,9 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
11030
12347
  // src/handshake/tools.ts
11031
12348
  init_router();
11032
12349
  init_generator();
12350
+ init_identity();
12351
+ init_key_derivation();
12352
+ init_encoding();
11033
12353
 
11034
12354
  // src/handshake/protocol.ts
11035
12355
  init_identity();
@@ -11354,7 +12674,10 @@ function verifyAttestation(attestation, now) {
11354
12674
  }
11355
12675
 
11356
12676
  // src/handshake/tools.ts
11357
- function createHandshakeTools(config, identityManager, masterKey, auditLog) {
12677
+ function createHandshakeTools(config, identityManager, masterKey, auditLog, options) {
12678
+ const autoPublishHandshakes = options?.autoPublishHandshakes ?? false;
12679
+ const verascoreUrl = options?.verascoreUrl ?? "https://verascore.ai";
12680
+ const identityEncKey = derivePurposeKey(masterKey, "identity-encryption");
11358
12681
  const sessions = /* @__PURE__ */ new Map();
11359
12682
  const handshakeResults = /* @__PURE__ */ new Map();
11360
12683
  const shrOpts = {
@@ -11364,7 +12687,7 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
11364
12687
  };
11365
12688
  const tools = [
11366
12689
  {
11367
- name: "sanctuary/handshake_initiate",
12690
+ name: "handshake_initiate",
11368
12691
  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.",
11369
12692
  inputSchema: {
11370
12693
  type: "object",
@@ -11391,7 +12714,7 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
11391
12714
  }
11392
12715
  },
11393
12716
  {
11394
- name: "sanctuary/handshake_respond",
12717
+ name: "handshake_respond",
11395
12718
  description: "Respond to an incoming sovereignty handshake challenge. Verifies the initiator's SHR, signs their nonce, and returns our SHR with a counter-nonce.",
11396
12719
  inputSchema: {
11397
12720
  type: "object",
@@ -11426,17 +12749,93 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
11426
12749
  }
11427
12750
  sessions.set(result.session.session_id, result.session);
11428
12751
  auditLog.append("l4", "handshake_respond", shr.body.instance_id);
12752
+ let autoPublishResult;
12753
+ if (autoPublishHandshakes) {
12754
+ autoPublishResult = { attempted: true };
12755
+ try {
12756
+ const parsed = new URL(verascoreUrl);
12757
+ if (parsed.protocol !== "https:") {
12758
+ autoPublishResult.error = `verascore URL must use HTTPS (got ${parsed.protocol})`;
12759
+ } else {
12760
+ const attestationPayload = {
12761
+ type: "handshake",
12762
+ our_shr_signed_by: shr.signed_by,
12763
+ counterparty_signed_by: "redacted",
12764
+ session_id: result.session.session_id,
12765
+ responded_at: (/* @__PURE__ */ new Date()).toISOString()
12766
+ };
12767
+ const responderIdentity = identityManager.get(shr.body.instance_id);
12768
+ if (!responderIdentity) {
12769
+ autoPublishResult.error = `responder identity ${shr.body.instance_id} not found; skipping auto-publish`;
12770
+ auditLog.append(
12771
+ "l4",
12772
+ "handshake_auto_publish",
12773
+ shr.body.instance_id,
12774
+ { error: autoPublishResult.error },
12775
+ "failure"
12776
+ );
12777
+ } else {
12778
+ const payloadBytes = new TextEncoder().encode(
12779
+ JSON.stringify(attestationPayload)
12780
+ );
12781
+ const sigBytes = sign(
12782
+ payloadBytes,
12783
+ responderIdentity.encrypted_private_key,
12784
+ identityEncKey
12785
+ );
12786
+ const signatureB64 = toBase64url(sigBytes);
12787
+ const resp = await fetch(
12788
+ `${verascoreUrl.replace(/\/$/, "")}/api/publish`,
12789
+ {
12790
+ method: "POST",
12791
+ headers: { "Content-Type": "application/json" },
12792
+ body: JSON.stringify({
12793
+ agentId: shr.body.instance_id,
12794
+ publicKey: shr.signed_by,
12795
+ signature: signatureB64,
12796
+ type: "handshake",
12797
+ data: attestationPayload
12798
+ })
12799
+ }
12800
+ );
12801
+ autoPublishResult.ok = resp.ok;
12802
+ autoPublishResult.status = resp.status;
12803
+ auditLog.append(
12804
+ "l4",
12805
+ "handshake_auto_publish",
12806
+ shr.body.instance_id,
12807
+ {
12808
+ verascore_url: verascoreUrl,
12809
+ status: resp.status,
12810
+ ok: resp.ok
12811
+ },
12812
+ resp.ok ? "success" : "failure"
12813
+ );
12814
+ }
12815
+ }
12816
+ } catch (err) {
12817
+ autoPublishResult.error = err instanceof Error ? err.message : String(err);
12818
+ auditLog.append(
12819
+ "l4",
12820
+ "handshake_auto_publish",
12821
+ shr.body.instance_id,
12822
+ { verascore_url: verascoreUrl, error: autoPublishResult.error },
12823
+ "failure"
12824
+ );
12825
+ }
12826
+ }
11429
12827
  return toolResult({
11430
12828
  session_id: result.session.session_id,
11431
12829
  response: result.response,
11432
12830
  instructions: "Send the 'response' object back to the initiator. When you receive their completion, pass it to sanctuary/handshake_status with this session_id.",
12831
+ auto_publish: autoPublishResult,
11433
12832
  // SEC-ADD-03: Tag response — contains SHR data that will be sent to counterparty
11434
12833
  _content_trust: "external"
11435
12834
  });
11436
12835
  }
11437
12836
  },
11438
12837
  {
11439
- name: "sanctuary/handshake_complete",
12838
+ name: "handshake_complete",
11440
12839
  description: "Complete a sovereignty handshake (initiator side). Verifies the responder's SHR and nonce signature, signs their nonce, and produces the final result.",
11441
12840
  inputSchema: {
11442
12841
  type: "object",
@@ -11491,7 +12890,7 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
11491
12890
  }
11492
12891
  },
11493
12892
  {
11494
- name: "sanctuary/handshake_status",
12893
+ name: "handshake_status",
11495
12894
  description: "Check the status of a handshake session, or verify a completion message (responder side).",
11496
12895
  inputSchema: {
11497
12896
  type: "object",
@@ -11541,7 +12940,7 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
11541
12940
  },
11542
12941
  // ─── Streamlined Exchange ─────────────────────────────────────────
11543
12942
  {
11544
- name: "sanctuary/handshake_exchange",
12943
+ name: "handshake_exchange",
11545
12944
  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).",
11546
12945
  inputSchema: {
11547
12946
  type: "object",
@@ -11608,7 +13007,7 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
11608
13007
  }
11609
13008
  },
11610
13009
  {
11611
- name: "sanctuary/handshake_verify_attestation",
13010
+ name: "handshake_verify_attestation",
11612
13011
  description: "Verify a signed attestation artifact from another agent. Checks the Ed25519 signature, temporal validity, and structural integrity.",
11613
13012
  inputSchema: {
11614
13013
  type: "object",
@@ -11820,7 +13219,7 @@ function createFederationTools(auditLog, handshakeResults) {
11820
13219
  const tools = [
11821
13220
  // ─── Peer Management ──────────────────────────────────────────────
11822
13221
  {
11823
- name: "sanctuary/federation_peers",
13222
+ name: "federation_peers",
11824
13223
  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.",
11825
13224
  inputSchema: {
11826
13225
  type: "object",
@@ -11923,7 +13322,7 @@ function createFederationTools(auditLog, handshakeResults) {
11923
13322
  },
11924
13323
  // ─── Trust Evaluation ─────────────────────────────────────────────
11925
13324
  {
11926
- name: "sanctuary/federation_trust_evaluate",
13325
+ name: "federation_trust_evaluate",
11927
13326
  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.",
11928
13327
  inputSchema: {
11929
13328
  type: "object",
@@ -11958,7 +13357,7 @@ function createFederationTools(auditLog, handshakeResults) {
11958
13357
  },
11959
13358
  // ─── Federation Status ────────────────────────────────────────────
11960
13359
  {
11961
- name: "sanctuary/federation_status",
13360
+ name: "federation_status",
11962
13361
  description: "Overview of federation state: total peers, active connections, trust distribution, and readiness for cross-instance operations.",
11963
13362
  inputSchema: {
11964
13363
  type: "object",
@@ -12171,7 +13570,7 @@ function createBridgeTools(storage, masterKey, identityManager, auditLog, handsh
12171
13570
  const tools = [
12172
13571
  // ─── bridge_commit ─────────────────────────────────────────────────
12173
13572
  {
12174
- name: "sanctuary/bridge_commit",
13573
+ name: "bridge_commit",
12175
13574
  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.",
12176
13575
  inputSchema: {
12177
13576
  type: "object",
@@ -12273,7 +13672,7 @@ function createBridgeTools(storage, masterKey, identityManager, auditLog, handsh
12273
13672
  },
12274
13673
  // ─── bridge_verify ───────────────────────────────────────────────────
12275
13674
  {
12276
- name: "sanctuary/bridge_verify",
13675
+ name: "bridge_verify",
12277
13676
  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.",
12278
13677
  inputSchema: {
12279
13678
  type: "object",
@@ -12329,7 +13728,7 @@ function createBridgeTools(storage, masterKey, identityManager, auditLog, handsh
12329
13728
  },
12330
13729
  // ─── bridge_attest ───────────────────────────────────────────────────
12331
13730
  {
12332
- name: "sanctuary/bridge_attest",
13731
+ name: "bridge_attest",
12333
13732
  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.",
12334
13733
  inputSchema: {
12335
13734
  type: "object",
@@ -12893,7 +14292,7 @@ function generateGaps(env, l1, l2, l3, l4) {
12893
14292
  title: "No context gating for outbound inference calls",
12894
14293
  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.",
12895
14294
  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,
12896
- 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.",
14295
+ 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.",
12897
14296
  incident_class: INCIDENT_CONTEXT_LEAKAGE
12898
14297
  });
12899
14298
  }
@@ -12905,7 +14304,7 @@ function generateGaps(env, l1, l2, l3, l4) {
12905
14304
  title: "No audit trail",
12906
14305
  description: "No audit trail exists for tool call history. There is no record of what operations were executed, when, or by whom.",
12907
14306
  openclaw_relevance: null,
12908
- sanctuary_solution: "Sanctuary maintains an encrypted audit log of all operations, queryable via sanctuary/monitor_audit_log.",
14307
+ sanctuary_solution: "Sanctuary maintains an encrypted audit log of all operations, queryable via monitor_audit_log.",
12909
14308
  incident_class: INCIDENT_CLAUDE_CODE_LEAK
12910
14309
  });
12911
14310
  }
@@ -12940,7 +14339,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
12940
14339
  recs.push({
12941
14340
  priority: 1,
12942
14341
  action: "Create a cryptographic identity \u2014 your agent's foundation for all sovereignty operations",
12943
- tool: "sanctuary/identity_create",
14342
+ tool: "identity_create",
12944
14343
  effort: "immediate",
12945
14344
  impact: "critical"
12946
14345
  });
@@ -12949,7 +14348,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
12949
14348
  recs.push({
12950
14349
  priority: 2,
12951
14350
  action: "Migrate plaintext agent state to Sanctuary's encrypted store",
12952
- tool: "sanctuary/state_write",
14351
+ tool: "state_write",
12953
14352
  effort: "minutes",
12954
14353
  impact: "critical"
12955
14354
  });
@@ -12957,7 +14356,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
12957
14356
  recs.push({
12958
14357
  priority: 3,
12959
14358
  action: "Generate a Sovereignty Health Report to present to counterparties",
12960
- tool: "sanctuary/shr_generate",
14359
+ tool: "shr_generate",
12961
14360
  effort: "immediate",
12962
14361
  impact: "high"
12963
14362
  });
@@ -12965,7 +14364,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
12965
14364
  recs.push({
12966
14365
  priority: 4,
12967
14366
  action: "Enable the three-tier Principal Policy gate for graduated approval",
12968
- tool: "sanctuary/principal_policy_view",
14367
+ tool: "principal_policy_view",
12969
14368
  effort: "minutes",
12970
14369
  impact: "high"
12971
14370
  });
@@ -12974,7 +14373,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
12974
14373
  recs.push({
12975
14374
  priority: 5,
12976
14375
  action: "Configure context gating to control what flows to LLM providers",
12977
- tool: "sanctuary/context_gate_set_policy",
14376
+ tool: "context_gate_set_policy",
12978
14377
  effort: "minutes",
12979
14378
  impact: "high"
12980
14379
  });
@@ -12983,7 +14382,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
12983
14382
  recs.push({
12984
14383
  priority: 6,
12985
14384
  action: "Start recording reputation attestations from completed interactions",
12986
- tool: "sanctuary/reputation_record",
14385
+ tool: "reputation_record",
12987
14386
  effort: "minutes",
12988
14387
  impact: "medium"
12989
14388
  });
@@ -12992,7 +14391,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
12992
14391
  recs.push({
12993
14392
  priority: 7,
12994
14393
  action: "Configure selective disclosure policies for data sharing",
12995
- tool: "sanctuary/disclosure_set_policy",
14394
+ tool: "disclosure_set_policy",
12996
14395
  effort: "hours",
12997
14396
  impact: "medium"
12998
14397
  });
@@ -13132,7 +14531,7 @@ function wordWrap(text, maxWidth) {
13132
14531
  function createAuditTools(config) {
13133
14532
  const tools = [
13134
14533
  {
13135
- name: "sanctuary/sovereignty_audit",
14534
+ name: "sovereignty_audit",
13136
14535
  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.",
13137
14536
  inputSchema: {
13138
14537
  type: "object",
@@ -13150,8 +14549,304 @@ function createAuditTools(config) {
13150
14549
  const report = formatAuditReport(result);
13151
14550
  return {
13152
14551
  content: [
13153
- { type: "text", text: report },
13154
- { type: "text", text: JSON.stringify(result, null, 2) }
14552
+ { type: "text", text: report },
14553
+ { type: "text", text: JSON.stringify(result, null, 2) }
14554
+ ]
14555
+ };
14556
+ }
14557
+ }
14558
+ ];
14559
+ return { tools };
14560
+ }
14561
+
14562
+ // src/audit/siem-formatter.ts
14563
+ function parseGateDecision(details) {
14564
+ if (!details || typeof details.gate_decision !== "string") {
14565
+ return "auto-allow";
14566
+ }
14567
+ const decision = details.gate_decision.toLowerCase();
14568
+ if (decision === "approve" || decision === "deny") {
14569
+ return decision;
14570
+ }
14571
+ return "auto-allow";
14572
+ }
14573
+ function parseTier(details) {
14574
+ if (!details || typeof details.tier !== "number") {
14575
+ return 3;
14576
+ }
14577
+ return Math.max(1, Math.min(3, details.tier));
14578
+ }
14579
+ function parseSessionId(details) {
14580
+ if (!details || typeof details.session_id !== "string") {
14581
+ return "unknown";
14582
+ }
14583
+ return details.session_id;
14584
+ }
14585
+ function parseAgentDid(details) {
14586
+ if (!details || typeof details.agent_did !== "string") {
14587
+ return "unknown";
14588
+ }
14589
+ return details.agent_did;
14590
+ }
14591
+ function gateToCEFSeverity(decision, tier) {
14592
+ if (decision === "deny") {
14593
+ return 8;
14594
+ }
14595
+ if (decision === "approve") {
14596
+ if (tier === 1) return 5;
14597
+ if (tier === 2) return 3;
14598
+ }
14599
+ return 1;
14600
+ }
14601
+ function formatAsCEF(entry, options) {
14602
+ const version = "0";
14603
+ const vendor = "Sanctuary";
14604
+ const product = "MCP-Server";
14605
+ const productVersion = "0.7.0";
14606
+ const decision = parseGateDecision(entry.details);
14607
+ const tier = parseTier(entry.details);
14608
+ const sessionId = parseSessionId(entry.details);
14609
+ const agentDid = parseAgentDid(entry.details);
14610
+ const severity = gateToCEFSeverity(decision, tier);
14611
+ const signatureId = entry.operation.replace(/[^a-zA-Z0-9_-]/g, "_");
14612
+ const description = `Sanctuary ${entry.operation}`;
14613
+ const extensions = [
14614
+ `src=${agentDid}`,
14615
+ `act=${entry.operation}`,
14616
+ `outcome=${decision}`,
14617
+ `tier=${tier}`,
14618
+ `cs1=${sessionId}`,
14619
+ `cs1Label=SessionId`,
14620
+ `rt=${new Date(entry.timestamp).getTime()}`,
14621
+ `layer=${entry.layer}`,
14622
+ `result=${entry.result}`
14623
+ ];
14624
+ return `CEF:${version}|${vendor}|${product}|${productVersion}|${signatureId}|${description}|${severity}|${extensions.join(" ")}`;
14625
+ }
14626
+ function gateToOCSFStatus(decision, result) {
14627
+ return decision === "deny" || result === "failure" ? 2 : 1;
14628
+ }
14629
+ function gateToCOCSFSeverity(decision, tier) {
14630
+ if (decision === "deny") {
14631
+ return 4;
14632
+ }
14633
+ if (decision === "approve") {
14634
+ if (tier === 1) return 3;
14635
+ if (tier === 2) return 2;
14636
+ }
14637
+ return 1;
14638
+ }
14639
+ function gateToOCSFDisposition(decision) {
14640
+ return decision === "deny" ? 2 : 1;
14641
+ }
14642
+ function formatAsOCSF(entry) {
14643
+ const decision = parseGateDecision(entry.details);
14644
+ const tier = parseTier(entry.details);
14645
+ const agentDid = parseAgentDid(entry.details);
14646
+ const timestamp = new Date(entry.timestamp).getTime();
14647
+ const statusId = gateToOCSFStatus(decision, entry.result);
14648
+ const severityId = gateToCOCSFSeverity(decision, tier);
14649
+ const dispositionId = gateToOCSFDisposition(decision);
14650
+ return {
14651
+ class_uid: 3001,
14652
+ class_name: "API Activity",
14653
+ category_uid: 3,
14654
+ category_name: "Application Activity",
14655
+ severity_id: severityId,
14656
+ time: timestamp,
14657
+ activity_id: 1,
14658
+ activity_name: "API Call",
14659
+ actor: {
14660
+ user: {
14661
+ uid: agentDid
14662
+ }
14663
+ },
14664
+ api: {
14665
+ operation: entry.operation,
14666
+ service: {
14667
+ name: "sanctuary-mcp"
14668
+ }
14669
+ },
14670
+ status_id: statusId,
14671
+ disposition_id: dispositionId,
14672
+ metadata: {
14673
+ version: "1.3.0",
14674
+ product: {
14675
+ name: "Sanctuary Framework",
14676
+ vendor_name: "Erik Newton",
14677
+ version: "0.7.0"
14678
+ }
14679
+ }
14680
+ };
14681
+ }
14682
+
14683
+ // src/audit/siem-tools.ts
14684
+ function createSIEMTools(auditLog) {
14685
+ const tools = [
14686
+ {
14687
+ name: "audit_export_siem",
14688
+ 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.",
14689
+ inputSchema: {
14690
+ type: "object",
14691
+ properties: {
14692
+ format: {
14693
+ type: "string",
14694
+ enum: ["cef", "ocsf"],
14695
+ description: 'Output format: "cef" (Common Event Format, newline-delimited) or "ocsf" (Open Cybersecurity Schema Framework, JSON array)'
14696
+ },
14697
+ since: {
14698
+ type: "string",
14699
+ description: "Optional ISO 8601 timestamp. Export only events on or after this time. Defaults to 24 hours ago."
14700
+ },
14701
+ until: {
14702
+ type: "string",
14703
+ description: "Optional ISO 8601 timestamp. Export only events before this time. Defaults to now."
14704
+ },
14705
+ limit: {
14706
+ type: "number",
14707
+ description: "Maximum number of events to export (default 100, max 1000). Set to 1000 for bulk exports to SIEMs."
14708
+ },
14709
+ filter_tool: {
14710
+ type: "string",
14711
+ description: 'Optional. Export only events from this tool name (e.g., "sovereignty_audit", "state_set"). Case-insensitive substring matching.'
14712
+ },
14713
+ filter_decision: {
14714
+ type: "string",
14715
+ enum: ["approve", "deny", "auto-allow"],
14716
+ description: 'Optional. Export only events with this gate decision: "approve" (manual approval), "deny" (blocked), or "auto-allow" (Tier 3 auto-allowed).'
14717
+ },
14718
+ filter_layer: {
14719
+ type: "string",
14720
+ enum: ["l1", "l2", "l3", "l4"],
14721
+ description: "Optional. Export only events from this sovereignty layer (L1=Cognitive, L2=Operational, L3=Disclosure, L4=Reputation)."
14722
+ },
14723
+ filter_result: {
14724
+ type: "string",
14725
+ enum: ["success", "failure"],
14726
+ description: 'Optional. Export only events with this result: "success" or "failure".'
14727
+ }
14728
+ },
14729
+ required: ["format"]
14730
+ },
14731
+ handler: async (args) => {
14732
+ const format = String(args.format || "").toLowerCase();
14733
+ if (format !== "cef" && format !== "ocsf") {
14734
+ return {
14735
+ content: [
14736
+ {
14737
+ type: "text",
14738
+ text: JSON.stringify({
14739
+ error: "Invalid format. Must be 'cef' or 'ocsf'."
14740
+ })
14741
+ }
14742
+ ]
14743
+ };
14744
+ }
14745
+ let since;
14746
+ if (args.since) {
14747
+ since = String(args.since);
14748
+ const sinceDate = new Date(since);
14749
+ if (isNaN(sinceDate.getTime())) {
14750
+ return {
14751
+ content: [
14752
+ {
14753
+ type: "text",
14754
+ text: JSON.stringify({
14755
+ error: `Invalid 'since' timestamp: ${since}. Must be ISO 8601.`
14756
+ })
14757
+ }
14758
+ ]
14759
+ };
14760
+ }
14761
+ } else {
14762
+ const now = /* @__PURE__ */ new Date();
14763
+ const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1e3);
14764
+ since = oneDayAgo.toISOString();
14765
+ }
14766
+ let until;
14767
+ if (args.until) {
14768
+ until = String(args.until);
14769
+ const untilDate = new Date(until);
14770
+ if (isNaN(untilDate.getTime())) {
14771
+ return {
14772
+ content: [
14773
+ {
14774
+ type: "text",
14775
+ text: JSON.stringify({
14776
+ error: `Invalid 'until' timestamp: ${until}. Must be ISO 8601.`
14777
+ })
14778
+ }
14779
+ ]
14780
+ };
14781
+ }
14782
+ }
14783
+ let limit = 100;
14784
+ if (typeof args.limit === "number") {
14785
+ limit = Math.max(1, Math.min(1e3, args.limit));
14786
+ }
14787
+ const filterTool = args.filter_tool ? String(args.filter_tool).toLowerCase() : void 0;
14788
+ const filterDecision = args.filter_decision ? String(args.filter_decision).toLowerCase() : void 0;
14789
+ const filterLayer = args.filter_layer ? String(args.filter_layer).toLowerCase() : void 0;
14790
+ const filterResult = args.filter_result ? String(args.filter_result).toLowerCase() : void 0;
14791
+ const result = await auditLog.query({
14792
+ since,
14793
+ layer: filterLayer,
14794
+ operation_type: void 0,
14795
+ // Will filter after
14796
+ limit
14797
+ });
14798
+ let filtered = result.entries;
14799
+ if (filterTool) {
14800
+ filtered = filtered.filter(
14801
+ (e) => e.operation.toLowerCase().includes(filterTool)
14802
+ );
14803
+ }
14804
+ if (filterDecision) {
14805
+ filtered = filtered.filter((e) => {
14806
+ const decision = String(e.details?.gate_decision || "auto-allow").toLowerCase();
14807
+ return decision === filterDecision;
14808
+ });
14809
+ }
14810
+ if (filterResult) {
14811
+ filtered = filtered.filter((e) => e.result === filterResult);
14812
+ }
14813
+ if (until) {
14814
+ const untilDate = new Date(until);
14815
+ filtered = filtered.filter((e) => new Date(e.timestamp) < untilDate);
14816
+ }
14817
+ let output;
14818
+ if (format === "cef") {
14819
+ const cefLines = filtered.map((entry) => formatAsCEF(entry));
14820
+ output = cefLines.join("\n");
14821
+ } else {
14822
+ const ocsfObjects = filtered.map((entry) => formatAsOCSF(entry));
14823
+ output = JSON.stringify(ocsfObjects, null, 2);
14824
+ }
14825
+ return {
14826
+ content: [
14827
+ {
14828
+ type: "text",
14829
+ text: JSON.stringify({
14830
+ format,
14831
+ count: filtered.length,
14832
+ total_available: result.total,
14833
+ time_range: {
14834
+ since,
14835
+ until: until || (/* @__PURE__ */ new Date()).toISOString()
14836
+ },
14837
+ filters: {
14838
+ tool: filterTool,
14839
+ decision: filterDecision,
14840
+ layer: filterLayer,
14841
+ result: filterResult
14842
+ },
14843
+ note: format === "cef" ? `${filtered.length} CEF events (newline-delimited). Each line is a complete CEF event.` : `${filtered.length} OCSF objects in JSON array format.`
14844
+ })
14845
+ },
14846
+ {
14847
+ type: "text",
14848
+ text: output
14849
+ }
13155
14850
  ]
13156
14851
  };
13157
14852
  }
@@ -14161,13 +15856,17 @@ var ContextGateEnforcer = class {
14161
15856
  * Check if a tool should be filtered based on bypass prefixes.
14162
15857
  *
14163
15858
  * SEC-033: Uses exact namespace component matching, not bare startsWith().
14164
- * A prefix of "sanctuary/" matches "sanctuary/state_read" but NOT
14165
- * "sanctuary_evil/steal_data" (no slash boundary confusion). The prefix
14166
- * must match exactly up to its length, and the prefix must end with "/"
14167
- * to enforce namespace boundaries (if it doesn't, we add one for safety).
15859
+ * A prefix of "proxy/" matches "proxy/server/tool" but NOT "proxyevil/steal".
15860
+ * The prefix must match exactly up to its length, and the prefix must end
15861
+ * with "/" to enforce namespace boundaries (if it doesn't, we add one).
15862
+ *
15863
+ * Special sentinel: "*" bypasses ALL tools (used when all Sanctuary-internal
15864
+ * tools should skip context gating — the default). Only proxy/external tools
15865
+ * should be filtered in production.
14168
15866
  */
14169
15867
  shouldFilter(toolName) {
14170
15868
  for (const prefix of this.config.bypass_prefixes) {
15869
+ if (prefix === "*") return false;
14171
15870
  const safePrefix = prefix.endsWith("/") ? prefix : prefix + "/";
14172
15871
  if (toolName === safePrefix.slice(0, -1) || toolName.startsWith(safePrefix)) {
14173
15872
  return false;
@@ -14264,8 +15963,8 @@ function createContextGateTools(storage, masterKey, auditLog) {
14264
15963
  const enforcerConfig = {
14265
15964
  enabled: false,
14266
15965
  // Off by default; agents must explicitly enable it
14267
- bypass_prefixes: ["sanctuary/"],
14268
- // Skip internal tools by default
15966
+ bypass_prefixes: ["*"],
15967
+ // Skip all Sanctuary-internal tools; only proxy/ tools get filtered
14269
15968
  log_only: false,
14270
15969
  // Filter immediately
14271
15970
  on_deny: "block"
@@ -14275,7 +15974,7 @@ function createContextGateTools(storage, masterKey, auditLog) {
14275
15974
  const tools = [
14276
15975
  // ── Set Policy ──────────────────────────────────────────────────
14277
15976
  {
14278
- name: "sanctuary/context_gate_set_policy",
15977
+ name: "context_gate_set_policy",
14279
15978
  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.",
14280
15979
  inputSchema: {
14281
15980
  type: "object",
@@ -14384,13 +16083,13 @@ function createContextGateTools(storage, masterKey, auditLog) {
14384
16083
  rules: policy.rules,
14385
16084
  default_action: policy.default_action,
14386
16085
  created_at: policy.created_at,
14387
- message: "Context-gating policy created. Use sanctuary/context_gate_filter to apply this policy before making outbound calls."
16086
+ message: "Context-gating policy created. Use context_gate_filter to apply this policy before making outbound calls."
14388
16087
  });
14389
16088
  }
14390
16089
  },
14391
16090
  // ── Apply Template ───────────────────────────────────────────────
14392
16091
  {
14393
- name: "sanctuary/context_gate_apply_template",
16092
+ name: "context_gate_apply_template",
14394
16093
  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.",
14395
16094
  inputSchema: {
14396
16095
  type: "object",
@@ -14439,13 +16138,13 @@ function createContextGateTools(storage, masterKey, auditLog) {
14439
16138
  rules: policy.rules,
14440
16139
  default_action: policy.default_action,
14441
16140
  created_at: policy.created_at,
14442
- 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."
16141
+ 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."
14443
16142
  });
14444
16143
  }
14445
16144
  },
14446
16145
  // ── Recommend Policy ────────────────────────────────────────────
14447
16146
  {
14448
- name: "sanctuary/context_gate_recommend",
16147
+ name: "context_gate_recommend",
14449
16148
  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.",
14450
16149
  inputSchema: {
14451
16150
  type: "object",
@@ -14482,7 +16181,7 @@ function createContextGateTools(storage, masterKey, auditLog) {
14482
16181
  });
14483
16182
  return toolResult({
14484
16183
  ...recommendation,
14485
- 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.",
16184
+ 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.",
14486
16185
  available_templates: listTemplateIds().map((id) => {
14487
16186
  const t = TEMPLATES[id];
14488
16187
  return { id, name: t.name, description: t.description };
@@ -14492,7 +16191,7 @@ function createContextGateTools(storage, masterKey, auditLog) {
14492
16191
  },
14493
16192
  // ── Filter Context ──────────────────────────────────────────────
14494
16193
  {
14495
- name: "sanctuary/context_gate_filter",
16194
+ name: "context_gate_filter",
14496
16195
  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.",
14497
16196
  inputSchema: {
14498
16197
  type: "object",
@@ -14598,7 +16297,7 @@ function createContextGateTools(storage, masterKey, auditLog) {
14598
16297
  },
14599
16298
  // ── List Policies ───────────────────────────────────────────────
14600
16299
  {
14601
- name: "sanctuary/context_gate_list_policies",
16300
+ name: "context_gate_list_policies",
14602
16301
  description: "List all configured context-gating policies. Returns policy IDs, names, rule summaries, and default actions.",
14603
16302
  inputSchema: {
14604
16303
  type: "object",
@@ -14621,13 +16320,13 @@ function createContextGateTools(storage, masterKey, auditLog) {
14621
16320
  updated_at: p.updated_at
14622
16321
  })),
14623
16322
  count: policies.length,
14624
- 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.`
16323
+ 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.`
14625
16324
  });
14626
16325
  }
14627
16326
  },
14628
16327
  // ── Enforcer Status ─────────────────────────────────────────────────
14629
16328
  {
14630
- name: "sanctuary/context_gate_enforcer_status",
16329
+ name: "context_gate_enforcer_status",
14631
16330
  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.",
14632
16331
  inputSchema: {
14633
16332
  type: "object",
@@ -14648,13 +16347,13 @@ function createContextGateTools(storage, masterKey, auditLog) {
14648
16347
  return toolResult({
14649
16348
  enforcer_status: status,
14650
16349
  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."),
14651
- 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."
16350
+ 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."
14652
16351
  });
14653
16352
  }
14654
16353
  },
14655
16354
  // ── Enforcer Configuration ──────────────────────────────────────────
14656
16355
  {
14657
- name: "sanctuary/context_gate_enforcer_configure",
16356
+ name: "context_gate_enforcer_configure",
14658
16357
  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.",
14659
16358
  inputSchema: {
14660
16359
  type: "object",
@@ -14980,7 +16679,7 @@ function assessL2Hardening(storagePath) {
14980
16679
  function createL2HardeningTools(storagePath, auditLog) {
14981
16680
  return [
14982
16681
  {
14983
- name: "sanctuary/l2_hardening_status",
16682
+ name: "l2_hardening_status",
14984
16683
  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.",
14985
16684
  inputSchema: {
14986
16685
  type: "object",
@@ -15048,7 +16747,7 @@ function createL2HardeningTools(storagePath, auditLog) {
15048
16747
  }
15049
16748
  },
15050
16749
  {
15051
- name: "sanctuary/l2_verify_isolation",
16750
+ name: "l2_verify_isolation",
15052
16751
  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.",
15053
16752
  inputSchema: {
15054
16753
  type: "object",
@@ -15150,7 +16849,7 @@ function createSovereigntyProfileTools(profileStore, auditLog) {
15150
16849
  const tools = [
15151
16850
  // ── Get Profile ──────────────────────────────────────────────────
15152
16851
  {
15153
- name: "sanctuary/sovereignty_profile_get",
16852
+ name: "sovereignty_profile_get",
15154
16853
  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.",
15155
16854
  inputSchema: {
15156
16855
  type: "object",
@@ -15169,7 +16868,7 @@ function createSovereigntyProfileTools(profileStore, auditLog) {
15169
16868
  },
15170
16869
  // ── Update Profile ───────────────────────────────────────────────
15171
16870
  {
15172
- name: "sanctuary/sovereignty_profile_update",
16871
+ name: "sovereignty_profile_update",
15173
16872
  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.",
15174
16873
  inputSchema: {
15175
16874
  type: "object",
@@ -15250,7 +16949,7 @@ function createSovereigntyProfileTools(profileStore, auditLog) {
15250
16949
  },
15251
16950
  // ── Generate System Prompt ───────────────────────────────────────
15252
16951
  {
15253
- name: "sanctuary/sovereignty_profile_generate_prompt",
16952
+ name: "sovereignty_profile_generate_prompt",
15254
16953
  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.",
15255
16954
  inputSchema: {
15256
16955
  type: "object",
@@ -15645,6 +17344,7 @@ var ProxyRouter = class {
15645
17344
  confidence: injectionResult.confidence,
15646
17345
  latency_ms: Date.now() - start
15647
17346
  }, "failure");
17347
+ this.notifyProxyCall(proxyName, serverName, "blocked", "injection_detected", tier);
15648
17348
  return toolResult({
15649
17349
  error: "Operation not permitted",
15650
17350
  proxy: true
@@ -15675,6 +17375,7 @@ var ProxyRouter = class {
15675
17375
  reason: govResult.reason,
15676
17376
  latency_ms: Date.now() - start
15677
17377
  }, "failure");
17378
+ this.notifyProxyCall(proxyName, serverName, "blocked", govResult.reason, tier);
15678
17379
  return toolResult({
15679
17380
  error: "Operation not permitted",
15680
17381
  proxy: true,
@@ -15709,6 +17410,7 @@ var ProxyRouter = class {
15709
17410
  decision: "allowed",
15710
17411
  latency_ms: latencyMs
15711
17412
  });
17413
+ this.notifyProxyCall(proxyName, serverName, "allowed", void 0, tier);
15712
17414
  return this.normalizeResponse(result);
15713
17415
  } catch (err) {
15714
17416
  const latencyMs = Date.now() - start;
@@ -15728,6 +17430,7 @@ var ProxyRouter = class {
15728
17430
  error: errorMessage,
15729
17431
  latency_ms: latencyMs
15730
17432
  }, "failure");
17433
+ this.notifyProxyCall(proxyName, serverName, "error", errorMessage, tier);
15731
17434
  return {
15732
17435
  content: [{
15733
17436
  type: "text",
@@ -15742,6 +17445,24 @@ var ProxyRouter = class {
15742
17445
  }
15743
17446
  };
15744
17447
  }
17448
+ /**
17449
+ * Notify the onProxyCall callback if configured.
17450
+ */
17451
+ notifyProxyCall(tool, server, decision, reason, tier) {
17452
+ if (this.options.onProxyCall) {
17453
+ try {
17454
+ this.options.onProxyCall({
17455
+ tool,
17456
+ server,
17457
+ decision,
17458
+ reason,
17459
+ tier,
17460
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
17461
+ });
17462
+ } catch {
17463
+ }
17464
+ }
17465
+ }
15745
17466
  /**
15746
17467
  * Call an upstream tool with a timeout.
15747
17468
  */
@@ -16015,7 +17736,7 @@ function createGovernorTools(governor, auditLog) {
16015
17736
  const tools = [
16016
17737
  // ── Governor Status ─────────────────────────────────────────────
16017
17738
  {
16018
- name: "sanctuary/governor_status",
17739
+ name: "governor_status",
16019
17740
  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.",
16020
17741
  inputSchema: {
16021
17742
  type: "object",
@@ -16055,7 +17776,7 @@ function createGovernorTools(governor, auditLog) {
16055
17776
  },
16056
17777
  // ── Governor Reset ──────────────────────────────────────────────
16057
17778
  {
16058
- name: "sanctuary/governor_reset",
17779
+ name: "governor_reset",
16059
17780
  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.",
16060
17781
  inputSchema: {
16061
17782
  type: "object",
@@ -16109,6 +17830,438 @@ function createGovernorTools(governor, auditLog) {
16109
17830
  return { tools };
16110
17831
  }
16111
17832
 
17833
+ // src/sanctuary-tools.ts
17834
+ init_router();
17835
+ init_identity();
17836
+ init_key_derivation();
17837
+ init_encoding();
17838
+ init_identity();
17839
+ init_generator();
17840
+ function validateVerascoreUrl(urlStr, configuredUrl) {
17841
+ const allowed = /* @__PURE__ */ new Set([
17842
+ "verascore.ai",
17843
+ "www.verascore.ai",
17844
+ "api.verascore.ai"
17845
+ ]);
17846
+ try {
17847
+ allowed.add(new URL(configuredUrl).hostname);
17848
+ } catch {
17849
+ }
17850
+ try {
17851
+ const parsed = new URL(urlStr);
17852
+ if (parsed.protocol !== "https:") {
17853
+ return { ok: false, error: `Verascore URL must use HTTPS. Got: ${parsed.protocol}` };
17854
+ }
17855
+ if (!allowed.has(parsed.hostname)) {
17856
+ return {
17857
+ ok: false,
17858
+ error: `Verascore URL must point to a known Verascore host (${[...allowed].join(", ")}). Got: ${parsed.hostname}`
17859
+ };
17860
+ }
17861
+ return { ok: true };
17862
+ } catch {
17863
+ return { ok: false, error: `Invalid Verascore URL: ${urlStr}` };
17864
+ }
17865
+ }
17866
+ function createSanctuaryTools(opts) {
17867
+ const { config, identityManager, masterKey, auditLog, policy, keyProtection } = opts;
17868
+ const identityEncKey = derivePurposeKey(masterKey, "identity-encryption");
17869
+ const tools = [
17870
+ // ─── sanctuary_bootstrap ───────────────────────────────────────────
17871
+ {
17872
+ name: "sanctuary_bootstrap",
17873
+ 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.",
17874
+ inputSchema: {
17875
+ type: "object",
17876
+ properties: {
17877
+ label: {
17878
+ type: "string",
17879
+ description: "Human-readable label for the new identity (default: 'sovereign-agent')"
17880
+ },
17881
+ verascore_url: {
17882
+ type: "string",
17883
+ description: "Verascore base URL. Defaults to server config / SANCTUARY_VERASCORE_URL."
17884
+ },
17885
+ publish: {
17886
+ type: "boolean",
17887
+ description: "Whether to publish the SHR to Verascore. Defaults to true."
17888
+ }
17889
+ }
17890
+ },
17891
+ handler: async (args) => {
17892
+ const label = args.label || "sovereign-agent";
17893
+ const publish = args.publish === void 0 ? true : Boolean(args.publish);
17894
+ const verascoreUrl = args.verascore_url || config.verascore.url || "https://verascore.ai";
17895
+ const { publicIdentity, storedIdentity } = createIdentity(
17896
+ label,
17897
+ identityEncKey,
17898
+ keyProtection
17899
+ );
17900
+ await identityManager.save(storedIdentity);
17901
+ auditLog.append("l1", "sanctuary_bootstrap:identity_create", publicIdentity.identity_id, {
17902
+ label,
17903
+ did: publicIdentity.did
17904
+ });
17905
+ const shr = generateSHR(publicIdentity.identity_id, {
17906
+ config,
17907
+ identityManager,
17908
+ masterKey
17909
+ });
17910
+ if (typeof shr === "string") {
17911
+ return toolResult({
17912
+ error: `Identity created but SHR generation failed: ${shr}`,
17913
+ did: publicIdentity.did,
17914
+ identity_id: publicIdentity.identity_id
17915
+ });
17916
+ }
17917
+ const agentSlug = publicIdentity.did.replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase();
17918
+ const profileUrl = `${verascoreUrl.replace(/\/$/, "")}/agent/${publicIdentity.did}`;
17919
+ if (!publish || !config.verascore.auto_publish_to_verascore) {
17920
+ auditLog.append("l4", "sanctuary_bootstrap", publicIdentity.identity_id, {
17921
+ did: publicIdentity.did,
17922
+ published: false
17923
+ });
17924
+ return toolResult({
17925
+ did: publicIdentity.did,
17926
+ identity_id: publicIdentity.identity_id,
17927
+ profileUrl,
17928
+ tier: "self-attested",
17929
+ published: false
17930
+ });
17931
+ }
17932
+ const urlCheck = validateVerascoreUrl(verascoreUrl, config.verascore.url);
17933
+ if (!urlCheck.ok) {
17934
+ return toolResult({
17935
+ error: urlCheck.error,
17936
+ did: publicIdentity.did,
17937
+ identity_id: publicIdentity.identity_id
17938
+ });
17939
+ }
17940
+ const publishData = {
17941
+ sovereigntyLayers: shr.body.layers,
17942
+ capabilities: shr.body.capabilities,
17943
+ degradations: shr.body.degradations,
17944
+ did: publicIdentity.did,
17945
+ label
17946
+ };
17947
+ const payloadBytes = new TextEncoder().encode(JSON.stringify(publishData));
17948
+ let signatureB64;
17949
+ try {
17950
+ const sigBytes = sign(
17951
+ payloadBytes,
17952
+ storedIdentity.encrypted_private_key,
17953
+ identityEncKey
17954
+ );
17955
+ signatureB64 = toBase64url(sigBytes);
17956
+ } catch (err) {
17957
+ return toolResult({
17958
+ error: "Failed to sign bootstrap payload",
17959
+ details: err instanceof Error ? err.message : String(err),
17960
+ did: publicIdentity.did
17961
+ });
17962
+ }
17963
+ const body = {
17964
+ agentId: agentSlug,
17965
+ signature: signatureB64,
17966
+ publicKey: publicIdentity.public_key,
17967
+ type: "shr",
17968
+ data: publishData
17969
+ };
17970
+ try {
17971
+ const response = await fetch(`${verascoreUrl.replace(/\/$/, "")}/api/publish`, {
17972
+ method: "POST",
17973
+ headers: { "Content-Type": "application/json" },
17974
+ body: JSON.stringify(body)
17975
+ });
17976
+ const result = await response.json().catch(() => ({}));
17977
+ auditLog.append("l4", "sanctuary_bootstrap", publicIdentity.identity_id, {
17978
+ did: publicIdentity.did,
17979
+ verascore_url: verascoreUrl,
17980
+ status: response.status,
17981
+ published: response.ok
17982
+ });
17983
+ return toolResult({
17984
+ did: publicIdentity.did,
17985
+ identity_id: publicIdentity.identity_id,
17986
+ profileUrl,
17987
+ tier: "self-attested",
17988
+ published: response.ok,
17989
+ verascore_status: response.status,
17990
+ verascore_response: result
17991
+ });
17992
+ } catch (err) {
17993
+ auditLog.append("l4", "sanctuary_bootstrap", publicIdentity.identity_id, {
17994
+ did: publicIdentity.did,
17995
+ error: err instanceof Error ? err.message : String(err)
17996
+ });
17997
+ return toolResult({
17998
+ did: publicIdentity.did,
17999
+ identity_id: publicIdentity.identity_id,
18000
+ profileUrl,
18001
+ tier: "self-attested",
18002
+ published: false,
18003
+ warning: `Identity created but Verascore publish failed: ${err instanceof Error ? err.message : String(err)}`
18004
+ });
18005
+ }
18006
+ }
18007
+ },
18008
+ // ─── sanctuary_policy_status ───────────────────────────────────────
18009
+ {
18010
+ name: "sanctuary_policy_status",
18011
+ 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).",
18012
+ inputSchema: {
18013
+ type: "object",
18014
+ properties: {}
18015
+ },
18016
+ handler: async () => {
18017
+ const tier1 = [...policy.tier1_always_approve].sort();
18018
+ const tier3 = [...policy.tier3_always_allow].sort();
18019
+ const tier2Config = policy.tier2_anomaly;
18020
+ auditLog.append("l2", "sanctuary_policy_status", "system", {
18021
+ tier1_count: tier1.length,
18022
+ tier3_count: tier3.length
18023
+ });
18024
+ return toolResult({
18025
+ tier1,
18026
+ tier2: [],
18027
+ tier3,
18028
+ tier2_anomaly_config: tier2Config,
18029
+ counts: {
18030
+ tier1: tier1.length,
18031
+ tier2: 0,
18032
+ tier3: tier3.length
18033
+ },
18034
+ note: "Tier 2 is not a named list in Sanctuary \u2014 it is behavioral anomaly detection applied to all operations. See tier2_anomaly_config."
18035
+ });
18036
+ }
18037
+ },
18038
+ // ─── sanctuary_export_identity_bundle ──────────────────────────────
18039
+ {
18040
+ name: "sanctuary_export_identity_bundle",
18041
+ 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.",
18042
+ inputSchema: {
18043
+ type: "object",
18044
+ properties: {
18045
+ identity_id: {
18046
+ type: "string",
18047
+ description: "Identity to export (defaults to primary identity)."
18048
+ },
18049
+ attestations: {
18050
+ type: "array",
18051
+ items: { type: "object" },
18052
+ description: "Optional list of attestation objects to include in the bundle."
18053
+ }
18054
+ }
18055
+ },
18056
+ handler: async (args) => {
18057
+ const identityId = args.identity_id;
18058
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
18059
+ if (!identity) {
18060
+ return toolResult({
18061
+ error: "No identity found. Create one with identity_create first."
18062
+ });
18063
+ }
18064
+ const shr = generateSHR(identity.identity_id, {
18065
+ config,
18066
+ identityManager,
18067
+ masterKey
18068
+ });
18069
+ const attestations = args.attestations ?? [];
18070
+ const body = {
18071
+ format: "SANCTUARY_IDENTITY_BUNDLE_V1",
18072
+ publicKey: identity.public_key,
18073
+ did: identity.did,
18074
+ identity_id: identity.identity_id,
18075
+ label: identity.label,
18076
+ key_type: identity.key_type,
18077
+ shr: typeof shr === "string" ? null : shr,
18078
+ attestations,
18079
+ exported_at: (/* @__PURE__ */ new Date()).toISOString()
18080
+ };
18081
+ const bodyBytes = new TextEncoder().encode(JSON.stringify(body));
18082
+ let signatureB64;
18083
+ try {
18084
+ const sigBytes = sign(
18085
+ bodyBytes,
18086
+ identity.encrypted_private_key,
18087
+ identityEncKey
18088
+ );
18089
+ signatureB64 = toBase64url(sigBytes);
18090
+ } catch (err) {
18091
+ return toolResult({
18092
+ error: "Failed to sign identity bundle.",
18093
+ details: err instanceof Error ? err.message : String(err)
18094
+ });
18095
+ }
18096
+ auditLog.append("l1", "sanctuary_export_identity_bundle", identity.identity_id, {
18097
+ did: identity.did,
18098
+ attestation_count: attestations.length
18099
+ });
18100
+ return toolResult({
18101
+ bundle: body,
18102
+ signature: signatureB64,
18103
+ signed_by: identity.did
18104
+ });
18105
+ }
18106
+ },
18107
+ // ─── sanctuary_link_to_human ───────────────────────────────────────
18108
+ {
18109
+ name: "sanctuary_link_to_human",
18110
+ 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.",
18111
+ inputSchema: {
18112
+ type: "object",
18113
+ properties: {
18114
+ email: {
18115
+ type: "string",
18116
+ description: "Email address of the human to link this agent to."
18117
+ },
18118
+ verascore_url: {
18119
+ type: "string",
18120
+ description: "Verascore base URL. Defaults to server config."
18121
+ }
18122
+ },
18123
+ required: ["email"]
18124
+ },
18125
+ handler: async (args) => {
18126
+ const email = args.email;
18127
+ const verascoreUrl = args.verascore_url || config.verascore.url || "https://verascore.ai";
18128
+ const urlCheck = validateVerascoreUrl(verascoreUrl, config.verascore.url);
18129
+ if (!urlCheck.ok) {
18130
+ return toolResult({ ok: false, error: urlCheck.error });
18131
+ }
18132
+ if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
18133
+ return toolResult({ ok: false, error: "Invalid email format." });
18134
+ }
18135
+ try {
18136
+ const response = await fetch(`${verascoreUrl.replace(/\/$/, "")}/api/auth/request`, {
18137
+ method: "POST",
18138
+ headers: { "Content-Type": "application/json" },
18139
+ body: JSON.stringify({ email })
18140
+ });
18141
+ await response.json().catch(() => ({}));
18142
+ auditLog.append("l4", "sanctuary_link_to_human", "system", {
18143
+ verascore_url: verascoreUrl,
18144
+ status: response.status,
18145
+ // Do not log the email to the audit trail — keep it local.
18146
+ email_domain: email.split("@")[1] ?? null
18147
+ });
18148
+ return toolResult({
18149
+ ok: response.ok,
18150
+ message: "Check your email for a login link. After logging in, visit verascore.ai to claim this agent's DID.",
18151
+ email_redacted: `***@${email.split("@")[1] ?? "***"}`,
18152
+ verascore_status: response.status
18153
+ });
18154
+ } catch (err) {
18155
+ return toolResult({
18156
+ ok: false,
18157
+ error: `Failed to reach Verascore at ${verascoreUrl}: ${err instanceof Error ? err.message : String(err)}`
18158
+ });
18159
+ }
18160
+ }
18161
+ },
18162
+ // ─── sanctuary_sign_challenge ──────────────────────────────────────
18163
+ {
18164
+ name: "sanctuary_sign_challenge",
18165
+ 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.",
18166
+ inputSchema: {
18167
+ type: "object",
18168
+ properties: {
18169
+ nonce: {
18170
+ type: "string",
18171
+ description: "The nonce / challenge string to sign."
18172
+ },
18173
+ purpose: {
18174
+ type: "string",
18175
+ description: "Domain-separation tag identifying what the signature will be used for (e.g. 'verascore-claim'). Required. Max 128 chars, printable ASCII only."
18176
+ },
18177
+ identity_id: {
18178
+ type: "string",
18179
+ description: "Identity to sign with (defaults to primary)."
18180
+ }
18181
+ },
18182
+ required: ["nonce", "purpose"]
18183
+ },
18184
+ handler: async (args) => {
18185
+ const nonce = args.nonce;
18186
+ const purpose = args.purpose;
18187
+ if (!nonce || nonce.length === 0) {
18188
+ return toolResult({ error: "nonce must be a non-empty string." });
18189
+ }
18190
+ if (nonce.length > 4096) {
18191
+ return toolResult({ error: "nonce exceeds maximum length (4096)." });
18192
+ }
18193
+ if (typeof purpose !== "string" || purpose.length === 0) {
18194
+ return toolResult({
18195
+ error: "purpose is required (domain-separation tag, e.g. 'verascore-claim')."
18196
+ });
18197
+ }
18198
+ if (purpose.length > 128) {
18199
+ return toolResult({ error: "purpose exceeds maximum length (128)." });
18200
+ }
18201
+ if (!/^[\x20-\x7E]+$/.test(purpose)) {
18202
+ return toolResult({
18203
+ error: "purpose must be printable ASCII only (no NUL, no non-ASCII)."
18204
+ });
18205
+ }
18206
+ const identityId = args.identity_id;
18207
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
18208
+ if (!identity) {
18209
+ return toolResult({
18210
+ error: "No identity found. Create one with identity_create first."
18211
+ });
18212
+ }
18213
+ const domainTag = "sanctuary-sign-challenge-v1";
18214
+ const enc = new TextEncoder();
18215
+ const tagBytes = enc.encode(domainTag);
18216
+ const purposeBytes = enc.encode(purpose);
18217
+ const nonceBytes = enc.encode(nonce);
18218
+ const sep = new Uint8Array([0]);
18219
+ const message = new Uint8Array(
18220
+ tagBytes.length + 1 + purposeBytes.length + 1 + nonceBytes.length
18221
+ );
18222
+ let offset = 0;
18223
+ message.set(tagBytes, offset);
18224
+ offset += tagBytes.length;
18225
+ message.set(sep, offset);
18226
+ offset += 1;
18227
+ message.set(purposeBytes, offset);
18228
+ offset += purposeBytes.length;
18229
+ message.set(sep, offset);
18230
+ offset += 1;
18231
+ message.set(nonceBytes, offset);
18232
+ let sigB64;
18233
+ try {
18234
+ const sig = sign(
18235
+ message,
18236
+ identity.encrypted_private_key,
18237
+ identityEncKey
18238
+ );
18239
+ sigB64 = toBase64url(sig);
18240
+ } catch (err) {
18241
+ return toolResult({
18242
+ error: "Failed to sign nonce.",
18243
+ details: err instanceof Error ? err.message : String(err)
18244
+ });
18245
+ }
18246
+ auditLog.append("l1", "sanctuary_sign_challenge", identity.identity_id, {
18247
+ did: identity.did,
18248
+ nonce_len: nonce.length,
18249
+ purpose
18250
+ });
18251
+ return toolResult({
18252
+ signature: sigB64,
18253
+ did: identity.did,
18254
+ public_key: identity.public_key,
18255
+ signed_by: identity.did,
18256
+ domain_tag: domainTag,
18257
+ purpose
18258
+ });
18259
+ }
18260
+ }
18261
+ ];
18262
+ return { tools };
18263
+ }
18264
+
16112
18265
  // src/index.ts
16113
18266
  init_key_derivation();
16114
18267
  init_random();
@@ -16242,7 +18395,7 @@ async function createSanctuaryServer(options) {
16242
18395
  }
16243
18396
  const l2Tools = [
16244
18397
  {
16245
- name: "sanctuary/exec_attest",
18398
+ name: "exec_attest",
16246
18399
  description: "Generate an attestation of the current execution environment, including sovereignty assessment and degradation report.",
16247
18400
  inputSchema: {
16248
18401
  type: "object",
@@ -16293,7 +18446,7 @@ async function createSanctuaryServer(options) {
16293
18446
  }
16294
18447
  },
16295
18448
  {
16296
- name: "sanctuary/monitor_health",
18449
+ name: "monitor_health",
16297
18450
  description: "Sanctuary Health Report (SHR) \u2014 standardized sovereignty status.",
16298
18451
  inputSchema: { type: "object", properties: {} },
16299
18452
  handler: async () => {
@@ -16342,7 +18495,7 @@ async function createSanctuaryServer(options) {
16342
18495
  }
16343
18496
  },
16344
18497
  {
16345
- name: "sanctuary/monitor_audit_log",
18498
+ name: "monitor_audit_log",
16346
18499
  description: "Query the sovereignty audit log.",
16347
18500
  inputSchema: {
16348
18501
  type: "object",
@@ -16368,7 +18521,7 @@ async function createSanctuaryServer(options) {
16368
18521
  }
16369
18522
  ];
16370
18523
  const manifestTool = {
16371
- name: "sanctuary/manifest",
18524
+ name: "manifest",
16372
18525
  description: "Generate the Sanctuary Interface Manifest (SIM) \u2014 a machine-readable declaration of this server's capabilities.",
16373
18526
  inputSchema: { type: "object", properties: {} },
16374
18527
  handler: async () => {
@@ -16454,14 +18607,19 @@ async function createSanctuaryServer(options) {
16454
18607
  config,
16455
18608
  identityManager,
16456
18609
  masterKey,
16457
- auditLog
18610
+ auditLog,
18611
+ {
18612
+ autoPublishHandshakes: config.verascore.auto_publish_handshakes,
18613
+ verascoreUrl: config.verascore.url
18614
+ }
16458
18615
  );
16459
18616
  const { tools: l4Tools} = createL4Tools(
16460
18617
  storage,
16461
18618
  masterKey,
16462
18619
  identityManager,
16463
18620
  auditLog,
16464
- handshakeResults
18621
+ handshakeResults,
18622
+ config.verascore.url
16465
18623
  );
16466
18624
  const { tools: federationTools } = createFederationTools(
16467
18625
  auditLog,
@@ -16475,6 +18633,7 @@ async function createSanctuaryServer(options) {
16475
18633
  handshakeResults
16476
18634
  );
16477
18635
  const { tools: auditTools } = createAuditTools(config);
18636
+ const { tools: siemTools } = createSIEMTools(auditLog);
16478
18637
  const { tools: contextGateTools, enforcer: contextGateEnforcer } = createContextGateTools(storage, masterKey, auditLog);
16479
18638
  const hardeningTools = createL2HardeningTools(config.storage_path, auditLog);
16480
18639
  const profileStore = new SovereigntyProfileStore(storage, masterKey);
@@ -16546,10 +18705,18 @@ async function createSanctuaryServer(options) {
16546
18705
  } : void 0;
16547
18706
  const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
16548
18707
  const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
18708
+ const { tools: sanctuaryMetaTools } = createSanctuaryTools({
18709
+ config,
18710
+ identityManager,
18711
+ masterKey,
18712
+ auditLog,
18713
+ policy,
18714
+ keyProtection
18715
+ });
16549
18716
  const dashboardTools = [];
16550
18717
  if (dashboard) {
16551
18718
  dashboardTools.push({
16552
- name: "sanctuary/dashboard_open",
18719
+ name: "dashboard_open",
16553
18720
  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.",
16554
18721
  inputSchema: {
16555
18722
  type: "object",
@@ -16583,10 +18750,12 @@ async function createSanctuaryServer(options) {
16583
18750
  ...federationTools,
16584
18751
  ...bridgeTools,
16585
18752
  ...auditTools,
18753
+ ...siemTools,
16586
18754
  ...contextGateTools,
16587
18755
  ...hardeningTools,
16588
18756
  ...profileTools,
16589
18757
  ...dashboardTools,
18758
+ ...sanctuaryMetaTools,
16590
18759
  manifestTool
16591
18760
  ];
16592
18761
  let clientManager;
@@ -16628,7 +18797,12 @@ async function createSanctuaryServer(options) {
16628
18797
  }
16629
18798
  return args;
16630
18799
  },
16631
- governor
18800
+ governor,
18801
+ onProxyCall: (data) => {
18802
+ if (dashboard) {
18803
+ dashboard.broadcastProxyCall(data);
18804
+ }
18805
+ }
16632
18806
  }
16633
18807
  );
16634
18808
  clientManager.configure(enabledServers).catch((err) => {
@@ -16646,6 +18820,7 @@ async function createSanctuaryServer(options) {
16646
18820
  auditLog,
16647
18821
  clientManager
16648
18822
  });
18823
+ dashboard.enableFortressView(enabledServers.length);
16649
18824
  }
16650
18825
  }
16651
18826
  }
@@ -16765,6 +18940,12 @@ async function main() {
16765
18940
  await runStandaloneDashboard(args.slice(1));
16766
18941
  return;
16767
18942
  }
18943
+ if (args[0] === "cocoon") {
18944
+ const { parseCocoonArgs: parseCocoonArgs2, runCocoon: runCocoon2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
18945
+ const cocoonOpts = parseCocoonArgs2(args.slice(1));
18946
+ await runCocoon2(cocoonOpts);
18947
+ return;
18948
+ }
16768
18949
  for (let i = 0; i < args.length; i++) {
16769
18950
  if (args[i] === "--dashboard") {
16770
18951
  process.env.SANCTUARY_DASHBOARD_ENABLED = "true";
@@ -16832,6 +19013,7 @@ Sovereignty infrastructure for agents in the agentic economy.
16832
19013
  Usage:
16833
19014
  sanctuary-mcp-server [options] # MCP server (stdio)
16834
19015
  sanctuary-mcp-server dashboard [opts] # Standalone dashboard
19016
+ sanctuary-mcp-server cocoon [opts] # Wrap agent in Cocoon protection
16835
19017
 
16836
19018
  Options:
16837
19019
  --dashboard Enable the Principal Dashboard (web UI)
@@ -16844,6 +19026,10 @@ Subcommands:
16844
19026
  Reads from the same storage as the MCP server.
16845
19027
  Use "sanctuary-mcp-server dashboard --help" for options.
16846
19028
 
19029
+ cocoon Wrap an existing agent in Sanctuary's enforcement chain.
19030
+ One command to protect any MCP-compatible agent.
19031
+ Use "sanctuary-mcp-server cocoon --help" for options.
19032
+
16847
19033
  Environment variables:
16848
19034
  SANCTUARY_STORAGE_PATH State directory (default: ~/.sanctuary)
16849
19035
  SANCTUARY_PASSPHRASE Key derivation passphrase