@sanctuary-framework/mcp-server 0.5.16 → 0.7.0

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