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