@sanctuary-framework/mcp-server 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { sha256 } from '@noble/hashes/sha256';
2
2
  import { hmac } from '@noble/hashes/hmac';
3
3
  import { readFile, mkdir, writeFile, stat, unlink, readdir, chmod, access } from 'fs/promises';
4
4
  import { join } from 'path';
5
- import { homedir } from 'os';
5
+ import { platform, homedir } from 'os';
6
6
  import { createRequire } from 'module';
7
7
  import { randomBytes as randomBytes$1, createHmac } from 'crypto';
8
8
  import { gcm } from '@noble/ciphers/aes.js';
@@ -14,7 +14,7 @@ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprot
14
14
  import { createServer as createServer$2 } from 'http';
15
15
  import { createServer as createServer$1 } from 'https';
16
16
  import { readFileSync, statSync } from 'fs';
17
- import { execSync } from 'child_process';
17
+ import { exec, execSync } from 'child_process';
18
18
 
19
19
  var __defProp = Object.defineProperty;
20
20
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -290,6 +290,12 @@ async function loadConfig(configPath) {
290
290
  if (process.env.SANCTUARY_DASHBOARD_AUTH_TOKEN) {
291
291
  config.dashboard.auth_token = process.env.SANCTUARY_DASHBOARD_AUTH_TOKEN;
292
292
  }
293
+ if (process.env.SANCTUARY_DASHBOARD_AUTO_OPEN === "true") {
294
+ config.dashboard.auto_open = true;
295
+ }
296
+ if (process.env.SANCTUARY_DASHBOARD_AUTO_OPEN === "false") {
297
+ config.dashboard.auto_open = false;
298
+ }
293
299
  if (process.env.SANCTUARY_DASHBOARD_TLS_CERT && process.env.SANCTUARY_DASHBOARD_TLS_KEY) {
294
300
  config.dashboard.tls = {
295
301
  cert_path: process.env.SANCTUARY_DASHBOARD_TLS_CERT,
@@ -4055,6 +4061,181 @@ var AutoApproveChannel = class {
4055
4061
  };
4056
4062
 
4057
4063
  // src/principal-policy/dashboard-html.ts
4064
+ function generateLoginHTML(options) {
4065
+ return `<!DOCTYPE html>
4066
+ <html lang="en">
4067
+ <head>
4068
+ <meta charset="utf-8">
4069
+ <meta name="viewport" content="width=device-width, initial-scale=1">
4070
+ <title>Sanctuary \u2014 Login</title>
4071
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
4072
+ <style>
4073
+ :root {
4074
+ --bg: #0d1117;
4075
+ --surface: #161b22;
4076
+ --border: #30363d;
4077
+ --text-primary: #e6edf3;
4078
+ --text-secondary: #8b949e;
4079
+ --green: #3fb950;
4080
+ --red: #f85149;
4081
+ --blue: #58a6ff;
4082
+ --mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
4083
+ --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
4084
+ --radius: 6px;
4085
+ }
4086
+ * { box-sizing: border-box; margin: 0; padding: 0; }
4087
+ html, body { width: 100%; height: 100%; }
4088
+ body {
4089
+ font-family: var(--sans);
4090
+ background: var(--bg);
4091
+ color: var(--text-primary);
4092
+ display: flex;
4093
+ align-items: center;
4094
+ justify-content: center;
4095
+ }
4096
+ .login-container {
4097
+ width: 100%;
4098
+ max-width: 400px;
4099
+ padding: 40px 32px;
4100
+ background: var(--surface);
4101
+ border: 1px solid var(--border);
4102
+ border-radius: 12px;
4103
+ }
4104
+ .login-logo {
4105
+ text-align: center;
4106
+ font-size: 20px;
4107
+ font-weight: 700;
4108
+ letter-spacing: -0.5px;
4109
+ margin-bottom: 8px;
4110
+ }
4111
+ .login-logo span { color: var(--blue); }
4112
+ .login-version {
4113
+ text-align: center;
4114
+ font-size: 11px;
4115
+ color: var(--text-secondary);
4116
+ font-family: var(--mono);
4117
+ margin-bottom: 32px;
4118
+ }
4119
+ .login-label {
4120
+ display: block;
4121
+ font-size: 13px;
4122
+ font-weight: 600;
4123
+ color: var(--text-secondary);
4124
+ margin-bottom: 8px;
4125
+ }
4126
+ .login-input {
4127
+ width: 100%;
4128
+ padding: 10px 14px;
4129
+ background: var(--bg);
4130
+ border: 1px solid var(--border);
4131
+ border-radius: var(--radius);
4132
+ color: var(--text-primary);
4133
+ font-family: var(--mono);
4134
+ font-size: 14px;
4135
+ outline: none;
4136
+ transition: border-color 0.15s;
4137
+ }
4138
+ .login-input:focus { border-color: var(--blue); }
4139
+ .login-input::placeholder { color: var(--text-secondary); opacity: 0.5; }
4140
+ .login-btn {
4141
+ width: 100%;
4142
+ margin-top: 20px;
4143
+ padding: 10px;
4144
+ background: var(--blue);
4145
+ color: var(--bg);
4146
+ border: none;
4147
+ border-radius: var(--radius);
4148
+ font-size: 14px;
4149
+ font-weight: 600;
4150
+ cursor: pointer;
4151
+ transition: opacity 0.15s;
4152
+ font-family: var(--sans);
4153
+ }
4154
+ .login-btn:hover { opacity: 0.9; }
4155
+ .login-btn:disabled { opacity: 0.5; cursor: not-allowed; }
4156
+ .login-error {
4157
+ margin-top: 16px;
4158
+ padding: 10px 14px;
4159
+ background: rgba(248, 81, 73, 0.1);
4160
+ border: 1px solid var(--red);
4161
+ border-radius: var(--radius);
4162
+ font-size: 12px;
4163
+ color: var(--red);
4164
+ display: none;
4165
+ }
4166
+ .login-hint {
4167
+ margin-top: 24px;
4168
+ padding-top: 16px;
4169
+ border-top: 1px solid var(--border);
4170
+ font-size: 11px;
4171
+ color: var(--text-secondary);
4172
+ line-height: 1.5;
4173
+ }
4174
+ .login-hint code {
4175
+ font-family: var(--mono);
4176
+ background: var(--bg);
4177
+ padding: 1px 4px;
4178
+ border-radius: 3px;
4179
+ font-size: 10px;
4180
+ }
4181
+ </style>
4182
+ </head>
4183
+ <body>
4184
+ <div class="login-container">
4185
+ <div class="login-logo"><span>&#9670;</span> SANCTUARY</div>
4186
+ <div class="login-version">Principal Dashboard v${options.serverVersion}</div>
4187
+ <form id="loginForm" onsubmit="return handleLogin(event)">
4188
+ <label class="login-label" for="tokenInput">Dashboard Auth Token</label>
4189
+ <input class="login-input" type="password" id="tokenInput"
4190
+ placeholder="Enter your auth token" autocomplete="off" autofocus required>
4191
+ <button class="login-btn" type="submit" id="loginBtn">Open Dashboard</button>
4192
+ </form>
4193
+ <div class="login-error" id="loginError"></div>
4194
+ <div class="login-hint">
4195
+ Your token is set via <code>SANCTUARY_DASHBOARD_AUTH_TOKEN</code> environment variable,
4196
+ or check your server's startup output.
4197
+ </div>
4198
+ </div>
4199
+ <script>
4200
+ async function handleLogin(e) {
4201
+ e.preventDefault();
4202
+ var btn = document.getElementById('loginBtn');
4203
+ var errEl = document.getElementById('loginError');
4204
+ var token = document.getElementById('tokenInput').value.trim();
4205
+ if (!token) return false;
4206
+ btn.disabled = true;
4207
+ btn.textContent = 'Authenticating...';
4208
+ errEl.style.display = 'none';
4209
+ try {
4210
+ var resp = await fetch('/auth/session', {
4211
+ method: 'POST',
4212
+ headers: { 'Authorization': 'Bearer ' + token }
4213
+ });
4214
+ if (!resp.ok) {
4215
+ var data = await resp.json().catch(function() { return {}; });
4216
+ throw new Error(data.error || 'Authentication failed');
4217
+ }
4218
+ var result = await resp.json();
4219
+ // Store token in sessionStorage for auto-renewal inside the dashboard
4220
+ try { sessionStorage.setItem('sanctuary_token', token); } catch(_) {}
4221
+ // Set session cookie
4222
+ var maxAge = result.expires_in_seconds || 300;
4223
+ document.cookie = 'sanctuary_session=' + result.session_id +
4224
+ '; path=/; SameSite=Strict; max-age=' + maxAge;
4225
+ // Reload to enter the dashboard
4226
+ window.location.reload();
4227
+ } catch (err) {
4228
+ errEl.textContent = err.message || 'Authentication failed. Check your token.';
4229
+ errEl.style.display = 'block';
4230
+ btn.disabled = false;
4231
+ btn.textContent = 'Open Dashboard';
4232
+ }
4233
+ return false;
4234
+ }
4235
+ </script>
4236
+ </body>
4237
+ </html>`;
4238
+ }
4058
4239
  function generateDashboardHTML(options) {
4059
4240
  return `<!DOCTYPE html>
4060
4241
  <html lang="en">
@@ -4924,7 +5105,9 @@ function generateDashboardHTML(options) {
4924
5105
  // \u2500\u2500 Configuration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4925
5106
 
4926
5107
  const TIMEOUT_SECONDS = ${options.timeoutSeconds};
4927
- const AUTH_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
5108
+ // AUTH_TOKEN: embedded token (for direct session access) or from sessionStorage (login page flow)
5109
+ const EMBEDDED_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
5110
+ const AUTH_TOKEN = EMBEDDED_TOKEN || (function() { try { return sessionStorage.getItem('sanctuary_token'); } catch(_) { return null; } })();
4928
5111
  const MAX_ACTIVITY_ITEMS = 100;
4929
5112
  const MAX_THREAT_ITEMS = 20;
4930
5113
 
@@ -4939,6 +5122,7 @@ function generateDashboardHTML(options) {
4939
5122
  const activityItems = [];
4940
5123
  const threatItems = [];
4941
5124
  let sovereigntyScore = 85;
5125
+ let sessionRenewalTimer = null;
4942
5126
 
4943
5127
  // \u2500\u2500 Auth Helpers (SEC-012) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4944
5128
 
@@ -4954,6 +5138,11 @@ function generateDashboardHTML(options) {
4954
5138
  return url + sep + 'session=' + SESSION_ID;
4955
5139
  }
4956
5140
 
5141
+ function setCookie(sessionId, maxAge) {
5142
+ document.cookie = 'sanctuary_session=' + sessionId +
5143
+ '; path=/; SameSite=Strict; max-age=' + maxAge;
5144
+ }
5145
+
4957
5146
  async function exchangeSession() {
4958
5147
  if (!AUTH_TOKEN) return;
4959
5148
  try {
@@ -4961,14 +5150,35 @@ function generateDashboardHTML(options) {
4961
5150
  if (resp.ok) {
4962
5151
  const data = await resp.json();
4963
5152
  SESSION_ID = data.session_id;
4964
- const refreshMs = (data.expires_in_seconds || 300) * 800;
4965
- setTimeout(() => { exchangeSession(); reconnectSSE(); }, refreshMs);
5153
+ var ttl = data.expires_in_seconds || 300;
5154
+ // Update cookie with new session
5155
+ setCookie(SESSION_ID, ttl);
5156
+ // Schedule renewal at 80% of TTL
5157
+ if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
5158
+ sessionRenewalTimer = setTimeout(function() {
5159
+ exchangeSession().then(function() { reconnectSSE(); });
5160
+ }, ttl * 800);
5161
+ } else if (resp.status === 401) {
5162
+ // Token invalid or expired \u2014 show non-destructive re-login overlay
5163
+ showSessionExpired();
4966
5164
  }
4967
5165
  } catch (e) {
4968
- // Retry on next connect
5166
+ // Network error \u2014 retry in 30s
5167
+ if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
5168
+ sessionRenewalTimer = setTimeout(function() {
5169
+ exchangeSession().then(function() { reconnectSSE(); });
5170
+ }, 30000);
4969
5171
  }
4970
5172
  }
4971
5173
 
5174
+ function showSessionExpired() {
5175
+ // Clear stored token
5176
+ try { sessionStorage.removeItem('sanctuary_token'); } catch(_) {}
5177
+ // Redirect to login page
5178
+ document.cookie = 'sanctuary_session=; path=/; max-age=0';
5179
+ window.location.reload();
5180
+ }
5181
+
4972
5182
  // \u2500\u2500 UI Utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4973
5183
 
4974
5184
  function esc(s) {
@@ -5441,7 +5651,8 @@ function generateDashboardHTML(options) {
5441
5651
  }
5442
5652
 
5443
5653
  // src/principal-policy/dashboard.ts
5444
- var SESSION_TTL_MS = 5 * 60 * 1e3;
5654
+ var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
5655
+ var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
5445
5656
  var MAX_SESSIONS = 1e3;
5446
5657
  var RATE_LIMIT_WINDOW_MS = 6e4;
5447
5658
  var RATE_LIMIT_GENERAL = 120;
@@ -5456,8 +5667,11 @@ var DashboardApprovalChannel = class {
5456
5667
  baseline = null;
5457
5668
  auditLog = null;
5458
5669
  dashboardHTML;
5670
+ loginHTML;
5459
5671
  authToken;
5460
5672
  useTLS;
5673
+ /** Session TTL: longer for localhost, shorter for remote */
5674
+ sessionTTLMs;
5461
5675
  /** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
5462
5676
  sessions = /* @__PURE__ */ new Map();
5463
5677
  sessionCleanupTimer = null;
@@ -5467,11 +5681,14 @@ var DashboardApprovalChannel = class {
5467
5681
  this.config = config;
5468
5682
  this.authToken = config.auth_token;
5469
5683
  this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
5684
+ const isLocalhost = config.host === "127.0.0.1" || config.host === "localhost" || config.host === "::1";
5685
+ this.sessionTTLMs = isLocalhost ? SESSION_TTL_LOCAL_MS : SESSION_TTL_REMOTE_MS;
5470
5686
  this.dashboardHTML = generateDashboardHTML({
5471
5687
  timeoutSeconds: config.timeout_seconds,
5472
5688
  serverVersion: SANCTUARY_VERSION,
5473
5689
  authToken: this.authToken
5474
5690
  });
5691
+ this.loginHTML = generateLoginHTML({ serverVersion: SANCTUARY_VERSION });
5475
5692
  this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
5476
5693
  }
5477
5694
  /**
@@ -5501,26 +5718,27 @@ var DashboardApprovalChannel = class {
5501
5718
  const protocol = this.useTLS ? "https" : "http";
5502
5719
  const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
5503
5720
  this.httpServer.listen(this.config.port, this.config.host, () => {
5504
- if (this.authToken) {
5505
- const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
5506
- process.stderr.write(
5507
- `
5721
+ const sessionUrl = this.authToken ? this.createSessionUrl() : baseUrl;
5722
+ process.stderr.write(
5723
+ `
5508
5724
  Sanctuary Principal Dashboard: ${baseUrl}
5509
5725
  `
5510
- );
5511
- process.stderr.write(
5512
- ` Auth required (token: ${hint}). Use Authorization: Bearer <TOKEN> header.
5513
-
5514
- `
5515
- );
5516
- } else {
5726
+ );
5727
+ if (this.authToken) {
5728
+ const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
5517
5729
  process.stderr.write(
5518
- `
5519
- Sanctuary Principal Dashboard: ${baseUrl}
5520
-
5730
+ ` Auth token: ${hint}
5521
5731
  `
5522
5732
  );
5523
5733
  }
5734
+ process.stderr.write(`
5735
+ `);
5736
+ const isTest = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI);
5737
+ const isLocalhost = this.config.host === "127.0.0.1" || this.config.host === "localhost" || this.config.host === "::1";
5738
+ const shouldAutoOpen = !isTest && (this.config.auto_open ?? isLocalhost);
5739
+ if (shouldAutoOpen) {
5740
+ this.openInBrowser(sessionUrl);
5741
+ }
5524
5742
  resolve();
5525
5743
  });
5526
5744
  this.httpServer.on("error", reject);
@@ -5623,10 +5841,47 @@ var DashboardApprovalChannel = class {
5623
5841
  if (sessionId && this.validateSession(sessionId)) {
5624
5842
  return true;
5625
5843
  }
5844
+ const cookieSession = this.parseCookie(req, "sanctuary_session");
5845
+ if (cookieSession && this.validateSession(cookieSession)) {
5846
+ return true;
5847
+ }
5626
5848
  res.writeHead(401, { "Content-Type": "application/json" });
5627
5849
  res.end(JSON.stringify({ error: "Unauthorized \u2014 use Authorization: Bearer header or a valid session" }));
5628
5850
  return false;
5629
5851
  }
5852
+ /**
5853
+ * Check if a request is authenticated WITHOUT sending a response.
5854
+ * Used to decide between login page vs dashboard for GET /.
5855
+ */
5856
+ isAuthenticated(req, url) {
5857
+ if (!this.authToken) return true;
5858
+ const authHeader = req.headers.authorization;
5859
+ if (authHeader) {
5860
+ const parts = authHeader.split(" ");
5861
+ if (parts.length === 2 && parts[0] === "Bearer" && parts[1] === this.authToken) {
5862
+ return true;
5863
+ }
5864
+ }
5865
+ const sessionId = url.searchParams.get("session");
5866
+ if (sessionId && this.validateSession(sessionId)) return true;
5867
+ const cookieSession = this.parseCookie(req, "sanctuary_session");
5868
+ if (cookieSession && this.validateSession(cookieSession)) return true;
5869
+ return false;
5870
+ }
5871
+ /**
5872
+ * Parse a specific cookie value from the request.
5873
+ */
5874
+ parseCookie(req, name) {
5875
+ const header = req.headers.cookie;
5876
+ if (!header) return null;
5877
+ for (const part of header.split(";")) {
5878
+ const [key, ...rest] = part.split("=");
5879
+ if (key?.trim() === name) {
5880
+ return rest.join("=").trim();
5881
+ }
5882
+ }
5883
+ return null;
5884
+ }
5630
5885
  // ── Session Management (SEC-012) ──────────────────────────────────
5631
5886
  /**
5632
5887
  * Create a short-lived session by exchanging the long-lived auth token
@@ -5647,7 +5902,7 @@ var DashboardApprovalChannel = class {
5647
5902
  this.sessions.set(id, {
5648
5903
  id,
5649
5904
  created_at: now,
5650
- expires_at: now + SESSION_TTL_MS
5905
+ expires_at: now + this.sessionTTLMs
5651
5906
  });
5652
5907
  return id;
5653
5908
  }
@@ -5746,13 +6001,26 @@ var DashboardApprovalChannel = class {
5746
6001
  res.end();
5747
6002
  return;
5748
6003
  }
5749
- if (!this.checkAuth(req, url, res)) return;
5750
- if (!this.checkRateLimit(req, res, "general")) return;
5751
- try {
5752
- if (method === "POST" && url.pathname === "/auth/session") {
6004
+ if (method === "POST" && url.pathname === "/auth/session") {
6005
+ if (!this.checkRateLimit(req, res, "general")) return;
6006
+ try {
5753
6007
  this.handleSessionExchange(req, res);
6008
+ } catch {
6009
+ res.writeHead(500, { "Content-Type": "application/json" });
6010
+ res.end(JSON.stringify({ error: "Internal server error" }));
6011
+ }
6012
+ return;
6013
+ }
6014
+ if (method === "GET" && url.pathname === "/" && this.authToken) {
6015
+ if (!this.isAuthenticated(req, url)) {
6016
+ if (!this.checkRateLimit(req, res, "general")) return;
6017
+ this.serveLoginPage(res);
5754
6018
  return;
5755
6019
  }
6020
+ }
6021
+ if (!this.checkAuth(req, url, res)) return;
6022
+ if (!this.checkRateLimit(req, res, "general")) return;
6023
+ try {
5756
6024
  if (method === "GET" && url.pathname === "/") {
5757
6025
  this.serveDashboard(res);
5758
6026
  } else if (method === "GET" && url.pathname === "/events") {
@@ -5809,12 +6077,23 @@ var DashboardApprovalChannel = class {
5809
6077
  return;
5810
6078
  }
5811
6079
  const sessionId = this.createSession();
5812
- res.writeHead(200, { "Content-Type": "application/json" });
6080
+ const ttlSeconds = Math.floor(this.sessionTTLMs / 1e3);
6081
+ res.writeHead(200, {
6082
+ "Content-Type": "application/json",
6083
+ "Set-Cookie": `sanctuary_session=${sessionId}; Path=/; SameSite=Strict; Max-Age=${ttlSeconds}`
6084
+ });
5813
6085
  res.end(JSON.stringify({
5814
6086
  session_id: sessionId,
5815
- expires_in_seconds: SESSION_TTL_MS / 1e3
6087
+ expires_in_seconds: ttlSeconds
5816
6088
  }));
5817
6089
  }
6090
+ serveLoginPage(res) {
6091
+ res.writeHead(200, {
6092
+ "Content-Type": "text/html; charset=utf-8",
6093
+ "Cache-Control": "no-cache, no-store"
6094
+ });
6095
+ res.end(this.loginHTML);
6096
+ }
5818
6097
  serveDashboard(res) {
5819
6098
  res.writeHead(200, {
5820
6099
  "Content-Type": "text/html; charset=utf-8",
@@ -5990,6 +6269,47 @@ data: ${JSON.stringify(data)}
5990
6269
  broadcastProtectionStatus(data) {
5991
6270
  this.broadcastSSE("protection-status", data);
5992
6271
  }
6272
+ /**
6273
+ * Open a URL in the system's default browser.
6274
+ * Cross-platform: macOS (open), Linux (xdg-open), Windows (start).
6275
+ * Fails silently — dashboard still works via terminal URL.
6276
+ */
6277
+ openInBrowser(url) {
6278
+ const os = platform();
6279
+ let cmd;
6280
+ if (os === "darwin") {
6281
+ cmd = `open "${url}"`;
6282
+ } else if (os === "win32") {
6283
+ cmd = `start "" "${url}"`;
6284
+ } else {
6285
+ cmd = `xdg-open "${url}"`;
6286
+ }
6287
+ exec(cmd, (err) => {
6288
+ if (err) {
6289
+ process.stderr.write(
6290
+ ` (Could not auto-open browser. Open the URL above manually.)
6291
+
6292
+ `
6293
+ );
6294
+ }
6295
+ });
6296
+ }
6297
+ /**
6298
+ * Create a pre-authenticated URL for the dashboard.
6299
+ * Used by the sanctuary_dashboard_open tool and at startup.
6300
+ */
6301
+ createSessionUrl() {
6302
+ const sessionId = this.createSession();
6303
+ const protocol = this.useTLS ? "https" : "http";
6304
+ return `${protocol}://${this.config.host}:${this.config.port}/?session=${sessionId}`;
6305
+ }
6306
+ /**
6307
+ * Get the base URL for the dashboard.
6308
+ */
6309
+ getBaseUrl() {
6310
+ const protocol = this.useTLS ? "https" : "http";
6311
+ return `${protocol}://${this.config.host}:${this.config.port}`;
6312
+ }
5993
6313
  /** Get the number of pending requests */
5994
6314
  get pendingCount() {
5995
6315
  return this.pending.size;
@@ -11820,7 +12140,8 @@ async function createSanctuaryServer(options) {
11820
12140
  timeout_seconds: policy.approval_channel.timeout_seconds,
11821
12141
  // SEC-002: auto_deny removed — timeout always denies
11822
12142
  auth_token: authToken,
11823
- tls: config.dashboard.tls
12143
+ tls: config.dashboard.tls,
12144
+ auto_open: config.dashboard.auto_open
11824
12145
  });
11825
12146
  dashboard.setDependencies({ policy, baseline, auditLog });
11826
12147
  await dashboard.start();
@@ -11859,6 +12180,32 @@ async function createSanctuaryServer(options) {
11859
12180
  } : void 0;
11860
12181
  const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
11861
12182
  const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
12183
+ const dashboardTools = [];
12184
+ if (dashboard) {
12185
+ dashboardTools.push({
12186
+ name: "sanctuary/dashboard_open",
12187
+ 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.",
12188
+ inputSchema: {
12189
+ type: "object",
12190
+ properties: {}
12191
+ },
12192
+ handler: async () => {
12193
+ const url = dashboard.createSessionUrl();
12194
+ return {
12195
+ content: [
12196
+ {
12197
+ type: "text",
12198
+ text: JSON.stringify({
12199
+ dashboard_url: url,
12200
+ base_url: dashboard.getBaseUrl(),
12201
+ note: "Click the dashboard_url to open the Principal Dashboard. The session is pre-authenticated."
12202
+ }, null, 2)
12203
+ }
12204
+ ]
12205
+ };
12206
+ }
12207
+ });
12208
+ }
11862
12209
  let allTools = [
11863
12210
  ...l1Tools,
11864
12211
  ...l2Tools,
@@ -11872,6 +12219,7 @@ async function createSanctuaryServer(options) {
11872
12219
  ...auditTools,
11873
12220
  ...contextGateTools,
11874
12221
  ...hardeningTools,
12222
+ ...dashboardTools,
11875
12223
  manifestTool
11876
12224
  ];
11877
12225
  allTools = allTools.map((tool) => ({