@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/cli.cjs CHANGED
@@ -295,6 +295,12 @@ async function loadConfig(configPath) {
295
295
  if (process.env.SANCTUARY_DASHBOARD_AUTH_TOKEN) {
296
296
  config.dashboard.auth_token = process.env.SANCTUARY_DASHBOARD_AUTH_TOKEN;
297
297
  }
298
+ if (process.env.SANCTUARY_DASHBOARD_AUTO_OPEN === "true") {
299
+ config.dashboard.auto_open = true;
300
+ }
301
+ if (process.env.SANCTUARY_DASHBOARD_AUTO_OPEN === "false") {
302
+ config.dashboard.auto_open = false;
303
+ }
298
304
  if (process.env.SANCTUARY_DASHBOARD_TLS_CERT && process.env.SANCTUARY_DASHBOARD_TLS_KEY) {
299
305
  config.dashboard.tls = {
300
306
  cert_path: process.env.SANCTUARY_DASHBOARD_TLS_CERT,
@@ -4042,6 +4048,181 @@ var StderrApprovalChannel = class {
4042
4048
  };
4043
4049
 
4044
4050
  // src/principal-policy/dashboard-html.ts
4051
+ function generateLoginHTML(options) {
4052
+ return `<!DOCTYPE html>
4053
+ <html lang="en">
4054
+ <head>
4055
+ <meta charset="utf-8">
4056
+ <meta name="viewport" content="width=device-width, initial-scale=1">
4057
+ <title>Sanctuary \u2014 Login</title>
4058
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
4059
+ <style>
4060
+ :root {
4061
+ --bg: #0d1117;
4062
+ --surface: #161b22;
4063
+ --border: #30363d;
4064
+ --text-primary: #e6edf3;
4065
+ --text-secondary: #8b949e;
4066
+ --green: #3fb950;
4067
+ --red: #f85149;
4068
+ --blue: #58a6ff;
4069
+ --mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
4070
+ --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
4071
+ --radius: 6px;
4072
+ }
4073
+ * { box-sizing: border-box; margin: 0; padding: 0; }
4074
+ html, body { width: 100%; height: 100%; }
4075
+ body {
4076
+ font-family: var(--sans);
4077
+ background: var(--bg);
4078
+ color: var(--text-primary);
4079
+ display: flex;
4080
+ align-items: center;
4081
+ justify-content: center;
4082
+ }
4083
+ .login-container {
4084
+ width: 100%;
4085
+ max-width: 400px;
4086
+ padding: 40px 32px;
4087
+ background: var(--surface);
4088
+ border: 1px solid var(--border);
4089
+ border-radius: 12px;
4090
+ }
4091
+ .login-logo {
4092
+ text-align: center;
4093
+ font-size: 20px;
4094
+ font-weight: 700;
4095
+ letter-spacing: -0.5px;
4096
+ margin-bottom: 8px;
4097
+ }
4098
+ .login-logo span { color: var(--blue); }
4099
+ .login-version {
4100
+ text-align: center;
4101
+ font-size: 11px;
4102
+ color: var(--text-secondary);
4103
+ font-family: var(--mono);
4104
+ margin-bottom: 32px;
4105
+ }
4106
+ .login-label {
4107
+ display: block;
4108
+ font-size: 13px;
4109
+ font-weight: 600;
4110
+ color: var(--text-secondary);
4111
+ margin-bottom: 8px;
4112
+ }
4113
+ .login-input {
4114
+ width: 100%;
4115
+ padding: 10px 14px;
4116
+ background: var(--bg);
4117
+ border: 1px solid var(--border);
4118
+ border-radius: var(--radius);
4119
+ color: var(--text-primary);
4120
+ font-family: var(--mono);
4121
+ font-size: 14px;
4122
+ outline: none;
4123
+ transition: border-color 0.15s;
4124
+ }
4125
+ .login-input:focus { border-color: var(--blue); }
4126
+ .login-input::placeholder { color: var(--text-secondary); opacity: 0.5; }
4127
+ .login-btn {
4128
+ width: 100%;
4129
+ margin-top: 20px;
4130
+ padding: 10px;
4131
+ background: var(--blue);
4132
+ color: var(--bg);
4133
+ border: none;
4134
+ border-radius: var(--radius);
4135
+ font-size: 14px;
4136
+ font-weight: 600;
4137
+ cursor: pointer;
4138
+ transition: opacity 0.15s;
4139
+ font-family: var(--sans);
4140
+ }
4141
+ .login-btn:hover { opacity: 0.9; }
4142
+ .login-btn:disabled { opacity: 0.5; cursor: not-allowed; }
4143
+ .login-error {
4144
+ margin-top: 16px;
4145
+ padding: 10px 14px;
4146
+ background: rgba(248, 81, 73, 0.1);
4147
+ border: 1px solid var(--red);
4148
+ border-radius: var(--radius);
4149
+ font-size: 12px;
4150
+ color: var(--red);
4151
+ display: none;
4152
+ }
4153
+ .login-hint {
4154
+ margin-top: 24px;
4155
+ padding-top: 16px;
4156
+ border-top: 1px solid var(--border);
4157
+ font-size: 11px;
4158
+ color: var(--text-secondary);
4159
+ line-height: 1.5;
4160
+ }
4161
+ .login-hint code {
4162
+ font-family: var(--mono);
4163
+ background: var(--bg);
4164
+ padding: 1px 4px;
4165
+ border-radius: 3px;
4166
+ font-size: 10px;
4167
+ }
4168
+ </style>
4169
+ </head>
4170
+ <body>
4171
+ <div class="login-container">
4172
+ <div class="login-logo"><span>&#9670;</span> SANCTUARY</div>
4173
+ <div class="login-version">Principal Dashboard v${options.serverVersion}</div>
4174
+ <form id="loginForm" onsubmit="return handleLogin(event)">
4175
+ <label class="login-label" for="tokenInput">Dashboard Auth Token</label>
4176
+ <input class="login-input" type="password" id="tokenInput"
4177
+ placeholder="Enter your auth token" autocomplete="off" autofocus required>
4178
+ <button class="login-btn" type="submit" id="loginBtn">Open Dashboard</button>
4179
+ </form>
4180
+ <div class="login-error" id="loginError"></div>
4181
+ <div class="login-hint">
4182
+ Your token is set via <code>SANCTUARY_DASHBOARD_AUTH_TOKEN</code> environment variable,
4183
+ or check your server's startup output.
4184
+ </div>
4185
+ </div>
4186
+ <script>
4187
+ async function handleLogin(e) {
4188
+ e.preventDefault();
4189
+ var btn = document.getElementById('loginBtn');
4190
+ var errEl = document.getElementById('loginError');
4191
+ var token = document.getElementById('tokenInput').value.trim();
4192
+ if (!token) return false;
4193
+ btn.disabled = true;
4194
+ btn.textContent = 'Authenticating...';
4195
+ errEl.style.display = 'none';
4196
+ try {
4197
+ var resp = await fetch('/auth/session', {
4198
+ method: 'POST',
4199
+ headers: { 'Authorization': 'Bearer ' + token }
4200
+ });
4201
+ if (!resp.ok) {
4202
+ var data = await resp.json().catch(function() { return {}; });
4203
+ throw new Error(data.error || 'Authentication failed');
4204
+ }
4205
+ var result = await resp.json();
4206
+ // Store token in sessionStorage for auto-renewal inside the dashboard
4207
+ try { sessionStorage.setItem('sanctuary_token', token); } catch(_) {}
4208
+ // Set session cookie
4209
+ var maxAge = result.expires_in_seconds || 300;
4210
+ document.cookie = 'sanctuary_session=' + result.session_id +
4211
+ '; path=/; SameSite=Strict; max-age=' + maxAge;
4212
+ // Reload to enter the dashboard
4213
+ window.location.reload();
4214
+ } catch (err) {
4215
+ errEl.textContent = err.message || 'Authentication failed. Check your token.';
4216
+ errEl.style.display = 'block';
4217
+ btn.disabled = false;
4218
+ btn.textContent = 'Open Dashboard';
4219
+ }
4220
+ return false;
4221
+ }
4222
+ </script>
4223
+ </body>
4224
+ </html>`;
4225
+ }
4045
4226
  function generateDashboardHTML(options) {
4046
4227
  return `<!DOCTYPE html>
4047
4228
  <html lang="en">
@@ -4911,7 +5092,9 @@ function generateDashboardHTML(options) {
4911
5092
  // \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
4912
5093
 
4913
5094
  const TIMEOUT_SECONDS = ${options.timeoutSeconds};
4914
- const AUTH_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
5095
+ // AUTH_TOKEN: embedded token (for direct session access) or from sessionStorage (login page flow)
5096
+ const EMBEDDED_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
5097
+ const AUTH_TOKEN = EMBEDDED_TOKEN || (function() { try { return sessionStorage.getItem('sanctuary_token'); } catch(_) { return null; } })();
4915
5098
  const MAX_ACTIVITY_ITEMS = 100;
4916
5099
  const MAX_THREAT_ITEMS = 20;
4917
5100
 
@@ -4926,6 +5109,7 @@ function generateDashboardHTML(options) {
4926
5109
  const activityItems = [];
4927
5110
  const threatItems = [];
4928
5111
  let sovereigntyScore = 85;
5112
+ let sessionRenewalTimer = null;
4929
5113
 
4930
5114
  // \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
4931
5115
 
@@ -4941,6 +5125,11 @@ function generateDashboardHTML(options) {
4941
5125
  return url + sep + 'session=' + SESSION_ID;
4942
5126
  }
4943
5127
 
5128
+ function setCookie(sessionId, maxAge) {
5129
+ document.cookie = 'sanctuary_session=' + sessionId +
5130
+ '; path=/; SameSite=Strict; max-age=' + maxAge;
5131
+ }
5132
+
4944
5133
  async function exchangeSession() {
4945
5134
  if (!AUTH_TOKEN) return;
4946
5135
  try {
@@ -4948,14 +5137,35 @@ function generateDashboardHTML(options) {
4948
5137
  if (resp.ok) {
4949
5138
  const data = await resp.json();
4950
5139
  SESSION_ID = data.session_id;
4951
- const refreshMs = (data.expires_in_seconds || 300) * 800;
4952
- setTimeout(() => { exchangeSession(); reconnectSSE(); }, refreshMs);
5140
+ var ttl = data.expires_in_seconds || 300;
5141
+ // Update cookie with new session
5142
+ setCookie(SESSION_ID, ttl);
5143
+ // Schedule renewal at 80% of TTL
5144
+ if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
5145
+ sessionRenewalTimer = setTimeout(function() {
5146
+ exchangeSession().then(function() { reconnectSSE(); });
5147
+ }, ttl * 800);
5148
+ } else if (resp.status === 401) {
5149
+ // Token invalid or expired \u2014 show non-destructive re-login overlay
5150
+ showSessionExpired();
4953
5151
  }
4954
5152
  } catch (e) {
4955
- // Retry on next connect
5153
+ // Network error \u2014 retry in 30s
5154
+ if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
5155
+ sessionRenewalTimer = setTimeout(function() {
5156
+ exchangeSession().then(function() { reconnectSSE(); });
5157
+ }, 30000);
4956
5158
  }
4957
5159
  }
4958
5160
 
5161
+ function showSessionExpired() {
5162
+ // Clear stored token
5163
+ try { sessionStorage.removeItem('sanctuary_token'); } catch(_) {}
5164
+ // Redirect to login page
5165
+ document.cookie = 'sanctuary_session=; path=/; max-age=0';
5166
+ window.location.reload();
5167
+ }
5168
+
4959
5169
  // \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
4960
5170
 
4961
5171
  function esc(s) {
@@ -5428,7 +5638,8 @@ function generateDashboardHTML(options) {
5428
5638
  }
5429
5639
 
5430
5640
  // src/principal-policy/dashboard.ts
5431
- var SESSION_TTL_MS = 5 * 60 * 1e3;
5641
+ var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
5642
+ var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
5432
5643
  var MAX_SESSIONS = 1e3;
5433
5644
  var RATE_LIMIT_WINDOW_MS = 6e4;
5434
5645
  var RATE_LIMIT_GENERAL = 120;
@@ -5443,8 +5654,11 @@ var DashboardApprovalChannel = class {
5443
5654
  baseline = null;
5444
5655
  auditLog = null;
5445
5656
  dashboardHTML;
5657
+ loginHTML;
5446
5658
  authToken;
5447
5659
  useTLS;
5660
+ /** Session TTL: longer for localhost, shorter for remote */
5661
+ sessionTTLMs;
5448
5662
  /** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
5449
5663
  sessions = /* @__PURE__ */ new Map();
5450
5664
  sessionCleanupTimer = null;
@@ -5454,11 +5668,14 @@ var DashboardApprovalChannel = class {
5454
5668
  this.config = config;
5455
5669
  this.authToken = config.auth_token;
5456
5670
  this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
5671
+ const isLocalhost = config.host === "127.0.0.1" || config.host === "localhost" || config.host === "::1";
5672
+ this.sessionTTLMs = isLocalhost ? SESSION_TTL_LOCAL_MS : SESSION_TTL_REMOTE_MS;
5457
5673
  this.dashboardHTML = generateDashboardHTML({
5458
5674
  timeoutSeconds: config.timeout_seconds,
5459
5675
  serverVersion: SANCTUARY_VERSION,
5460
5676
  authToken: this.authToken
5461
5677
  });
5678
+ this.loginHTML = generateLoginHTML({ serverVersion: SANCTUARY_VERSION });
5462
5679
  this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
5463
5680
  }
5464
5681
  /**
@@ -5488,26 +5705,27 @@ var DashboardApprovalChannel = class {
5488
5705
  const protocol = this.useTLS ? "https" : "http";
5489
5706
  const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
5490
5707
  this.httpServer.listen(this.config.port, this.config.host, () => {
5491
- if (this.authToken) {
5492
- const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
5493
- process.stderr.write(
5494
- `
5708
+ const sessionUrl = this.authToken ? this.createSessionUrl() : baseUrl;
5709
+ process.stderr.write(
5710
+ `
5495
5711
  Sanctuary Principal Dashboard: ${baseUrl}
5496
5712
  `
5497
- );
5498
- process.stderr.write(
5499
- ` Auth required (token: ${hint}). Use Authorization: Bearer <TOKEN> header.
5500
-
5501
- `
5502
- );
5503
- } else {
5713
+ );
5714
+ if (this.authToken) {
5715
+ const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
5504
5716
  process.stderr.write(
5505
- `
5506
- Sanctuary Principal Dashboard: ${baseUrl}
5507
-
5717
+ ` Auth token: ${hint}
5508
5718
  `
5509
5719
  );
5510
5720
  }
5721
+ process.stderr.write(`
5722
+ `);
5723
+ const isTest = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI);
5724
+ const isLocalhost = this.config.host === "127.0.0.1" || this.config.host === "localhost" || this.config.host === "::1";
5725
+ const shouldAutoOpen = !isTest && (this.config.auto_open ?? isLocalhost);
5726
+ if (shouldAutoOpen) {
5727
+ this.openInBrowser(sessionUrl);
5728
+ }
5511
5729
  resolve();
5512
5730
  });
5513
5731
  this.httpServer.on("error", reject);
@@ -5610,10 +5828,47 @@ var DashboardApprovalChannel = class {
5610
5828
  if (sessionId && this.validateSession(sessionId)) {
5611
5829
  return true;
5612
5830
  }
5831
+ const cookieSession = this.parseCookie(req, "sanctuary_session");
5832
+ if (cookieSession && this.validateSession(cookieSession)) {
5833
+ return true;
5834
+ }
5613
5835
  res.writeHead(401, { "Content-Type": "application/json" });
5614
5836
  res.end(JSON.stringify({ error: "Unauthorized \u2014 use Authorization: Bearer header or a valid session" }));
5615
5837
  return false;
5616
5838
  }
5839
+ /**
5840
+ * Check if a request is authenticated WITHOUT sending a response.
5841
+ * Used to decide between login page vs dashboard for GET /.
5842
+ */
5843
+ isAuthenticated(req, url) {
5844
+ if (!this.authToken) return true;
5845
+ const authHeader = req.headers.authorization;
5846
+ if (authHeader) {
5847
+ const parts = authHeader.split(" ");
5848
+ if (parts.length === 2 && parts[0] === "Bearer" && parts[1] === this.authToken) {
5849
+ return true;
5850
+ }
5851
+ }
5852
+ const sessionId = url.searchParams.get("session");
5853
+ if (sessionId && this.validateSession(sessionId)) return true;
5854
+ const cookieSession = this.parseCookie(req, "sanctuary_session");
5855
+ if (cookieSession && this.validateSession(cookieSession)) return true;
5856
+ return false;
5857
+ }
5858
+ /**
5859
+ * Parse a specific cookie value from the request.
5860
+ */
5861
+ parseCookie(req, name) {
5862
+ const header = req.headers.cookie;
5863
+ if (!header) return null;
5864
+ for (const part of header.split(";")) {
5865
+ const [key, ...rest] = part.split("=");
5866
+ if (key?.trim() === name) {
5867
+ return rest.join("=").trim();
5868
+ }
5869
+ }
5870
+ return null;
5871
+ }
5617
5872
  // ── Session Management (SEC-012) ──────────────────────────────────
5618
5873
  /**
5619
5874
  * Create a short-lived session by exchanging the long-lived auth token
@@ -5634,7 +5889,7 @@ var DashboardApprovalChannel = class {
5634
5889
  this.sessions.set(id, {
5635
5890
  id,
5636
5891
  created_at: now,
5637
- expires_at: now + SESSION_TTL_MS
5892
+ expires_at: now + this.sessionTTLMs
5638
5893
  });
5639
5894
  return id;
5640
5895
  }
@@ -5733,13 +5988,26 @@ var DashboardApprovalChannel = class {
5733
5988
  res.end();
5734
5989
  return;
5735
5990
  }
5736
- if (!this.checkAuth(req, url, res)) return;
5737
- if (!this.checkRateLimit(req, res, "general")) return;
5738
- try {
5739
- if (method === "POST" && url.pathname === "/auth/session") {
5991
+ if (method === "POST" && url.pathname === "/auth/session") {
5992
+ if (!this.checkRateLimit(req, res, "general")) return;
5993
+ try {
5740
5994
  this.handleSessionExchange(req, res);
5995
+ } catch {
5996
+ res.writeHead(500, { "Content-Type": "application/json" });
5997
+ res.end(JSON.stringify({ error: "Internal server error" }));
5998
+ }
5999
+ return;
6000
+ }
6001
+ if (method === "GET" && url.pathname === "/" && this.authToken) {
6002
+ if (!this.isAuthenticated(req, url)) {
6003
+ if (!this.checkRateLimit(req, res, "general")) return;
6004
+ this.serveLoginPage(res);
5741
6005
  return;
5742
6006
  }
6007
+ }
6008
+ if (!this.checkAuth(req, url, res)) return;
6009
+ if (!this.checkRateLimit(req, res, "general")) return;
6010
+ try {
5743
6011
  if (method === "GET" && url.pathname === "/") {
5744
6012
  this.serveDashboard(res);
5745
6013
  } else if (method === "GET" && url.pathname === "/events") {
@@ -5796,12 +6064,23 @@ var DashboardApprovalChannel = class {
5796
6064
  return;
5797
6065
  }
5798
6066
  const sessionId = this.createSession();
5799
- res.writeHead(200, { "Content-Type": "application/json" });
6067
+ const ttlSeconds = Math.floor(this.sessionTTLMs / 1e3);
6068
+ res.writeHead(200, {
6069
+ "Content-Type": "application/json",
6070
+ "Set-Cookie": `sanctuary_session=${sessionId}; Path=/; SameSite=Strict; Max-Age=${ttlSeconds}`
6071
+ });
5800
6072
  res.end(JSON.stringify({
5801
6073
  session_id: sessionId,
5802
- expires_in_seconds: SESSION_TTL_MS / 1e3
6074
+ expires_in_seconds: ttlSeconds
5803
6075
  }));
5804
6076
  }
6077
+ serveLoginPage(res) {
6078
+ res.writeHead(200, {
6079
+ "Content-Type": "text/html; charset=utf-8",
6080
+ "Cache-Control": "no-cache, no-store"
6081
+ });
6082
+ res.end(this.loginHTML);
6083
+ }
5805
6084
  serveDashboard(res) {
5806
6085
  res.writeHead(200, {
5807
6086
  "Content-Type": "text/html; charset=utf-8",
@@ -5977,6 +6256,47 @@ data: ${JSON.stringify(data)}
5977
6256
  broadcastProtectionStatus(data) {
5978
6257
  this.broadcastSSE("protection-status", data);
5979
6258
  }
6259
+ /**
6260
+ * Open a URL in the system's default browser.
6261
+ * Cross-platform: macOS (open), Linux (xdg-open), Windows (start).
6262
+ * Fails silently — dashboard still works via terminal URL.
6263
+ */
6264
+ openInBrowser(url) {
6265
+ const os$1 = os.platform();
6266
+ let cmd;
6267
+ if (os$1 === "darwin") {
6268
+ cmd = `open "${url}"`;
6269
+ } else if (os$1 === "win32") {
6270
+ cmd = `start "" "${url}"`;
6271
+ } else {
6272
+ cmd = `xdg-open "${url}"`;
6273
+ }
6274
+ child_process.exec(cmd, (err) => {
6275
+ if (err) {
6276
+ process.stderr.write(
6277
+ ` (Could not auto-open browser. Open the URL above manually.)
6278
+
6279
+ `
6280
+ );
6281
+ }
6282
+ });
6283
+ }
6284
+ /**
6285
+ * Create a pre-authenticated URL for the dashboard.
6286
+ * Used by the sanctuary_dashboard_open tool and at startup.
6287
+ */
6288
+ createSessionUrl() {
6289
+ const sessionId = this.createSession();
6290
+ const protocol = this.useTLS ? "https" : "http";
6291
+ return `${protocol}://${this.config.host}:${this.config.port}/?session=${sessionId}`;
6292
+ }
6293
+ /**
6294
+ * Get the base URL for the dashboard.
6295
+ */
6296
+ getBaseUrl() {
6297
+ const protocol = this.useTLS ? "https" : "http";
6298
+ return `${protocol}://${this.config.host}:${this.config.port}`;
6299
+ }
5980
6300
  /** Get the number of pending requests */
5981
6301
  get pendingCount() {
5982
6302
  return this.pending.size;
@@ -11752,7 +12072,8 @@ async function createSanctuaryServer(options) {
11752
12072
  timeout_seconds: policy.approval_channel.timeout_seconds,
11753
12073
  // SEC-002: auto_deny removed — timeout always denies
11754
12074
  auth_token: authToken,
11755
- tls: config.dashboard.tls
12075
+ tls: config.dashboard.tls,
12076
+ auto_open: config.dashboard.auto_open
11756
12077
  });
11757
12078
  dashboard.setDependencies({ policy, baseline, auditLog });
11758
12079
  await dashboard.start();
@@ -11791,6 +12112,32 @@ async function createSanctuaryServer(options) {
11791
12112
  } : void 0;
11792
12113
  const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
11793
12114
  const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
12115
+ const dashboardTools = [];
12116
+ if (dashboard) {
12117
+ dashboardTools.push({
12118
+ name: "sanctuary/dashboard_open",
12119
+ 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.",
12120
+ inputSchema: {
12121
+ type: "object",
12122
+ properties: {}
12123
+ },
12124
+ handler: async () => {
12125
+ const url = dashboard.createSessionUrl();
12126
+ return {
12127
+ content: [
12128
+ {
12129
+ type: "text",
12130
+ text: JSON.stringify({
12131
+ dashboard_url: url,
12132
+ base_url: dashboard.getBaseUrl(),
12133
+ note: "Click the dashboard_url to open the Principal Dashboard. The session is pre-authenticated."
12134
+ }, null, 2)
12135
+ }
12136
+ ]
12137
+ };
12138
+ }
12139
+ });
12140
+ }
11794
12141
  let allTools = [
11795
12142
  ...l1Tools,
11796
12143
  ...l2Tools,
@@ -11804,6 +12151,7 @@ async function createSanctuaryServer(options) {
11804
12151
  ...auditTools,
11805
12152
  ...contextGateTools,
11806
12153
  ...hardeningTools,
12154
+ ...dashboardTools,
11807
12155
  manifestTool
11808
12156
  ];
11809
12157
  allTools = allTools.map((tool) => ({