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