@sanctuary-framework/mcp-server 0.5.0 → 0.5.2

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
@@ -256,7 +256,18 @@ function defaultConfig() {
256
256
  };
257
257
  }
258
258
  async function loadConfig(configPath) {
259
- const config = defaultConfig();
259
+ let config = defaultConfig();
260
+ const storagePath = process.env.SANCTUARY_STORAGE_PATH ?? config.storage_path;
261
+ const path = configPath ?? join(storagePath, "sanctuary.json");
262
+ try {
263
+ const raw = await readFile(path, "utf-8");
264
+ const fileConfig = JSON.parse(raw);
265
+ config = deepMerge(config, fileConfig);
266
+ } catch (err) {
267
+ if (err instanceof Error && err.message.includes("unimplemented features")) {
268
+ throw err;
269
+ }
270
+ }
260
271
  if (process.env.SANCTUARY_STORAGE_PATH) {
261
272
  config.storage_path = process.env.SANCTUARY_STORAGE_PATH;
262
273
  }
@@ -269,6 +280,9 @@ async function loadConfig(configPath) {
269
280
  if (process.env.SANCTUARY_DASHBOARD_ENABLED === "true") {
270
281
  config.dashboard.enabled = true;
271
282
  }
283
+ if (process.env.SANCTUARY_DASHBOARD_ENABLED === "false") {
284
+ config.dashboard.enabled = false;
285
+ }
272
286
  if (process.env.SANCTUARY_DASHBOARD_PORT) {
273
287
  config.dashboard.port = parseInt(process.env.SANCTUARY_DASHBOARD_PORT, 10);
274
288
  }
@@ -287,6 +301,9 @@ async function loadConfig(configPath) {
287
301
  if (process.env.SANCTUARY_WEBHOOK_ENABLED === "true") {
288
302
  config.webhook.enabled = true;
289
303
  }
304
+ if (process.env.SANCTUARY_WEBHOOK_ENABLED === "false") {
305
+ config.webhook.enabled = false;
306
+ }
290
307
  if (process.env.SANCTUARY_WEBHOOK_URL) {
291
308
  config.webhook.url = process.env.SANCTUARY_WEBHOOK_URL;
292
309
  }
@@ -299,19 +316,9 @@ async function loadConfig(configPath) {
299
316
  if (process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST) {
300
317
  config.webhook.callback_host = process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST;
301
318
  }
302
- const path = configPath ?? join(config.storage_path, "sanctuary.json");
303
- try {
304
- const raw = await readFile(path, "utf-8");
305
- const fileConfig = JSON.parse(raw);
306
- const merged = deepMerge(config, fileConfig);
307
- validateConfig(merged);
308
- return merged;
309
- } catch (err) {
310
- if (err instanceof Error && err.message.includes("unimplemented features")) {
311
- throw err;
312
- }
313
- return config;
314
- }
319
+ config.version = PKG_VERSION;
320
+ validateConfig(config);
321
+ return config;
315
322
  }
316
323
  async function saveConfig(config, configPath) {
317
324
  const path = join(config.storage_path, "sanctuary.json");
@@ -4032,6 +4039,181 @@ var StderrApprovalChannel = class {
4032
4039
  };
4033
4040
 
4034
4041
  // src/principal-policy/dashboard-html.ts
4042
+ function generateLoginHTML(options) {
4043
+ return `<!DOCTYPE html>
4044
+ <html lang="en">
4045
+ <head>
4046
+ <meta charset="utf-8">
4047
+ <meta name="viewport" content="width=device-width, initial-scale=1">
4048
+ <title>Sanctuary \u2014 Login</title>
4049
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
4050
+ <style>
4051
+ :root {
4052
+ --bg: #0d1117;
4053
+ --surface: #161b22;
4054
+ --border: #30363d;
4055
+ --text-primary: #e6edf3;
4056
+ --text-secondary: #8b949e;
4057
+ --green: #3fb950;
4058
+ --red: #f85149;
4059
+ --blue: #58a6ff;
4060
+ --mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
4061
+ --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
4062
+ --radius: 6px;
4063
+ }
4064
+ * { box-sizing: border-box; margin: 0; padding: 0; }
4065
+ html, body { width: 100%; height: 100%; }
4066
+ body {
4067
+ font-family: var(--sans);
4068
+ background: var(--bg);
4069
+ color: var(--text-primary);
4070
+ display: flex;
4071
+ align-items: center;
4072
+ justify-content: center;
4073
+ }
4074
+ .login-container {
4075
+ width: 100%;
4076
+ max-width: 400px;
4077
+ padding: 40px 32px;
4078
+ background: var(--surface);
4079
+ border: 1px solid var(--border);
4080
+ border-radius: 12px;
4081
+ }
4082
+ .login-logo {
4083
+ text-align: center;
4084
+ font-size: 20px;
4085
+ font-weight: 700;
4086
+ letter-spacing: -0.5px;
4087
+ margin-bottom: 8px;
4088
+ }
4089
+ .login-logo span { color: var(--blue); }
4090
+ .login-version {
4091
+ text-align: center;
4092
+ font-size: 11px;
4093
+ color: var(--text-secondary);
4094
+ font-family: var(--mono);
4095
+ margin-bottom: 32px;
4096
+ }
4097
+ .login-label {
4098
+ display: block;
4099
+ font-size: 13px;
4100
+ font-weight: 600;
4101
+ color: var(--text-secondary);
4102
+ margin-bottom: 8px;
4103
+ }
4104
+ .login-input {
4105
+ width: 100%;
4106
+ padding: 10px 14px;
4107
+ background: var(--bg);
4108
+ border: 1px solid var(--border);
4109
+ border-radius: var(--radius);
4110
+ color: var(--text-primary);
4111
+ font-family: var(--mono);
4112
+ font-size: 14px;
4113
+ outline: none;
4114
+ transition: border-color 0.15s;
4115
+ }
4116
+ .login-input:focus { border-color: var(--blue); }
4117
+ .login-input::placeholder { color: var(--text-secondary); opacity: 0.5; }
4118
+ .login-btn {
4119
+ width: 100%;
4120
+ margin-top: 20px;
4121
+ padding: 10px;
4122
+ background: var(--blue);
4123
+ color: var(--bg);
4124
+ border: none;
4125
+ border-radius: var(--radius);
4126
+ font-size: 14px;
4127
+ font-weight: 600;
4128
+ cursor: pointer;
4129
+ transition: opacity 0.15s;
4130
+ font-family: var(--sans);
4131
+ }
4132
+ .login-btn:hover { opacity: 0.9; }
4133
+ .login-btn:disabled { opacity: 0.5; cursor: not-allowed; }
4134
+ .login-error {
4135
+ margin-top: 16px;
4136
+ padding: 10px 14px;
4137
+ background: rgba(248, 81, 73, 0.1);
4138
+ border: 1px solid var(--red);
4139
+ border-radius: var(--radius);
4140
+ font-size: 12px;
4141
+ color: var(--red);
4142
+ display: none;
4143
+ }
4144
+ .login-hint {
4145
+ margin-top: 24px;
4146
+ padding-top: 16px;
4147
+ border-top: 1px solid var(--border);
4148
+ font-size: 11px;
4149
+ color: var(--text-secondary);
4150
+ line-height: 1.5;
4151
+ }
4152
+ .login-hint code {
4153
+ font-family: var(--mono);
4154
+ background: var(--bg);
4155
+ padding: 1px 4px;
4156
+ border-radius: 3px;
4157
+ font-size: 10px;
4158
+ }
4159
+ </style>
4160
+ </head>
4161
+ <body>
4162
+ <div class="login-container">
4163
+ <div class="login-logo"><span>&#9670;</span> SANCTUARY</div>
4164
+ <div class="login-version">Principal Dashboard v${options.serverVersion}</div>
4165
+ <form id="loginForm" onsubmit="return handleLogin(event)">
4166
+ <label class="login-label" for="tokenInput">Dashboard Auth Token</label>
4167
+ <input class="login-input" type="password" id="tokenInput"
4168
+ placeholder="Enter your auth token" autocomplete="off" autofocus required>
4169
+ <button class="login-btn" type="submit" id="loginBtn">Open Dashboard</button>
4170
+ </form>
4171
+ <div class="login-error" id="loginError"></div>
4172
+ <div class="login-hint">
4173
+ Your token is set via <code>SANCTUARY_DASHBOARD_AUTH_TOKEN</code> environment variable,
4174
+ or check your server's startup output.
4175
+ </div>
4176
+ </div>
4177
+ <script>
4178
+ async function handleLogin(e) {
4179
+ e.preventDefault();
4180
+ var btn = document.getElementById('loginBtn');
4181
+ var errEl = document.getElementById('loginError');
4182
+ var token = document.getElementById('tokenInput').value.trim();
4183
+ if (!token) return false;
4184
+ btn.disabled = true;
4185
+ btn.textContent = 'Authenticating...';
4186
+ errEl.style.display = 'none';
4187
+ try {
4188
+ var resp = await fetch('/auth/session', {
4189
+ method: 'POST',
4190
+ headers: { 'Authorization': 'Bearer ' + token }
4191
+ });
4192
+ if (!resp.ok) {
4193
+ var data = await resp.json().catch(function() { return {}; });
4194
+ throw new Error(data.error || 'Authentication failed');
4195
+ }
4196
+ var result = await resp.json();
4197
+ // Store token in sessionStorage for auto-renewal inside the dashboard
4198
+ try { sessionStorage.setItem('sanctuary_token', token); } catch(_) {}
4199
+ // Set session cookie
4200
+ var maxAge = result.expires_in_seconds || 300;
4201
+ document.cookie = 'sanctuary_session=' + result.session_id +
4202
+ '; path=/; SameSite=Strict; max-age=' + maxAge;
4203
+ // Reload to enter the dashboard
4204
+ window.location.reload();
4205
+ } catch (err) {
4206
+ errEl.textContent = err.message || 'Authentication failed. Check your token.';
4207
+ errEl.style.display = 'block';
4208
+ btn.disabled = false;
4209
+ btn.textContent = 'Open Dashboard';
4210
+ }
4211
+ return false;
4212
+ }
4213
+ </script>
4214
+ </body>
4215
+ </html>`;
4216
+ }
4035
4217
  function generateDashboardHTML(options) {
4036
4218
  return `<!DOCTYPE html>
4037
4219
  <html lang="en">
@@ -4901,7 +5083,9 @@ function generateDashboardHTML(options) {
4901
5083
  // \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
4902
5084
 
4903
5085
  const TIMEOUT_SECONDS = ${options.timeoutSeconds};
4904
- const AUTH_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
5086
+ // AUTH_TOKEN: embedded token (for direct session access) or from sessionStorage (login page flow)
5087
+ const EMBEDDED_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
5088
+ const AUTH_TOKEN = EMBEDDED_TOKEN || (function() { try { return sessionStorage.getItem('sanctuary_token'); } catch(_) { return null; } })();
4905
5089
  const MAX_ACTIVITY_ITEMS = 100;
4906
5090
  const MAX_THREAT_ITEMS = 20;
4907
5091
 
@@ -4916,6 +5100,7 @@ function generateDashboardHTML(options) {
4916
5100
  const activityItems = [];
4917
5101
  const threatItems = [];
4918
5102
  let sovereigntyScore = 85;
5103
+ let sessionRenewalTimer = null;
4919
5104
 
4920
5105
  // \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
4921
5106
 
@@ -4931,6 +5116,11 @@ function generateDashboardHTML(options) {
4931
5116
  return url + sep + 'session=' + SESSION_ID;
4932
5117
  }
4933
5118
 
5119
+ function setCookie(sessionId, maxAge) {
5120
+ document.cookie = 'sanctuary_session=' + sessionId +
5121
+ '; path=/; SameSite=Strict; max-age=' + maxAge;
5122
+ }
5123
+
4934
5124
  async function exchangeSession() {
4935
5125
  if (!AUTH_TOKEN) return;
4936
5126
  try {
@@ -4938,14 +5128,35 @@ function generateDashboardHTML(options) {
4938
5128
  if (resp.ok) {
4939
5129
  const data = await resp.json();
4940
5130
  SESSION_ID = data.session_id;
4941
- const refreshMs = (data.expires_in_seconds || 300) * 800;
4942
- setTimeout(() => { exchangeSession(); reconnectSSE(); }, refreshMs);
5131
+ var ttl = data.expires_in_seconds || 300;
5132
+ // Update cookie with new session
5133
+ setCookie(SESSION_ID, ttl);
5134
+ // Schedule renewal at 80% of TTL
5135
+ if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
5136
+ sessionRenewalTimer = setTimeout(function() {
5137
+ exchangeSession().then(function() { reconnectSSE(); });
5138
+ }, ttl * 800);
5139
+ } else if (resp.status === 401) {
5140
+ // Token invalid or expired \u2014 show non-destructive re-login overlay
5141
+ showSessionExpired();
4943
5142
  }
4944
5143
  } catch (e) {
4945
- // Retry on next connect
5144
+ // Network error \u2014 retry in 30s
5145
+ if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
5146
+ sessionRenewalTimer = setTimeout(function() {
5147
+ exchangeSession().then(function() { reconnectSSE(); });
5148
+ }, 30000);
4946
5149
  }
4947
5150
  }
4948
5151
 
5152
+ function showSessionExpired() {
5153
+ // Clear stored token
5154
+ try { sessionStorage.removeItem('sanctuary_token'); } catch(_) {}
5155
+ // Redirect to login page
5156
+ document.cookie = 'sanctuary_session=; path=/; max-age=0';
5157
+ window.location.reload();
5158
+ }
5159
+
4949
5160
  // \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
4950
5161
 
4951
5162
  function esc(s) {
@@ -5418,7 +5629,8 @@ function generateDashboardHTML(options) {
5418
5629
  }
5419
5630
 
5420
5631
  // src/principal-policy/dashboard.ts
5421
- var SESSION_TTL_MS = 5 * 60 * 1e3;
5632
+ var SESSION_TTL_REMOTE_MS = 5 * 60 * 1e3;
5633
+ var SESSION_TTL_LOCAL_MS = 24 * 60 * 60 * 1e3;
5422
5634
  var MAX_SESSIONS = 1e3;
5423
5635
  var RATE_LIMIT_WINDOW_MS = 6e4;
5424
5636
  var RATE_LIMIT_GENERAL = 120;
@@ -5433,8 +5645,11 @@ var DashboardApprovalChannel = class {
5433
5645
  baseline = null;
5434
5646
  auditLog = null;
5435
5647
  dashboardHTML;
5648
+ loginHTML;
5436
5649
  authToken;
5437
5650
  useTLS;
5651
+ /** Session TTL: longer for localhost, shorter for remote */
5652
+ sessionTTLMs;
5438
5653
  /** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
5439
5654
  sessions = /* @__PURE__ */ new Map();
5440
5655
  sessionCleanupTimer = null;
@@ -5444,11 +5659,14 @@ var DashboardApprovalChannel = class {
5444
5659
  this.config = config;
5445
5660
  this.authToken = config.auth_token;
5446
5661
  this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
5662
+ const isLocalhost = config.host === "127.0.0.1" || config.host === "localhost" || config.host === "::1";
5663
+ this.sessionTTLMs = isLocalhost ? SESSION_TTL_LOCAL_MS : SESSION_TTL_REMOTE_MS;
5447
5664
  this.dashboardHTML = generateDashboardHTML({
5448
5665
  timeoutSeconds: config.timeout_seconds,
5449
5666
  serverVersion: SANCTUARY_VERSION,
5450
5667
  authToken: this.authToken
5451
5668
  });
5669
+ this.loginHTML = generateLoginHTML({ serverVersion: SANCTUARY_VERSION });
5452
5670
  this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
5453
5671
  }
5454
5672
  /**
@@ -5478,25 +5696,26 @@ var DashboardApprovalChannel = class {
5478
5696
  const protocol = this.useTLS ? "https" : "http";
5479
5697
  const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
5480
5698
  this.httpServer.listen(this.config.port, this.config.host, () => {
5481
- if (this.authToken) {
5482
- const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
5483
- process.stderr.write(
5484
- `
5699
+ process.stderr.write(
5700
+ `
5485
5701
  Sanctuary Principal Dashboard: ${baseUrl}
5486
5702
  `
5487
- );
5703
+ );
5704
+ if (this.authToken) {
5705
+ const sessionUrl = this.createSessionUrl();
5488
5706
  process.stderr.write(
5489
- ` Auth required (token: ${hint}). Use Authorization: Bearer <TOKEN> header.
5490
-
5707
+ ` Quick open: ${sessionUrl}
5491
5708
  `
5492
5709
  );
5493
- } else {
5710
+ const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
5494
5711
  process.stderr.write(
5495
- `
5496
- Sanctuary Principal Dashboard: ${baseUrl}
5712
+ ` Auth token: ${hint}
5497
5713
 
5498
5714
  `
5499
5715
  );
5716
+ } else {
5717
+ process.stderr.write(`
5718
+ `);
5500
5719
  }
5501
5720
  resolve();
5502
5721
  });
@@ -5600,10 +5819,47 @@ var DashboardApprovalChannel = class {
5600
5819
  if (sessionId && this.validateSession(sessionId)) {
5601
5820
  return true;
5602
5821
  }
5822
+ const cookieSession = this.parseCookie(req, "sanctuary_session");
5823
+ if (cookieSession && this.validateSession(cookieSession)) {
5824
+ return true;
5825
+ }
5603
5826
  res.writeHead(401, { "Content-Type": "application/json" });
5604
5827
  res.end(JSON.stringify({ error: "Unauthorized \u2014 use Authorization: Bearer header or a valid session" }));
5605
5828
  return false;
5606
5829
  }
5830
+ /**
5831
+ * Check if a request is authenticated WITHOUT sending a response.
5832
+ * Used to decide between login page vs dashboard for GET /.
5833
+ */
5834
+ isAuthenticated(req, url) {
5835
+ if (!this.authToken) return true;
5836
+ const authHeader = req.headers.authorization;
5837
+ if (authHeader) {
5838
+ const parts = authHeader.split(" ");
5839
+ if (parts.length === 2 && parts[0] === "Bearer" && parts[1] === this.authToken) {
5840
+ return true;
5841
+ }
5842
+ }
5843
+ const sessionId = url.searchParams.get("session");
5844
+ if (sessionId && this.validateSession(sessionId)) return true;
5845
+ const cookieSession = this.parseCookie(req, "sanctuary_session");
5846
+ if (cookieSession && this.validateSession(cookieSession)) return true;
5847
+ return false;
5848
+ }
5849
+ /**
5850
+ * Parse a specific cookie value from the request.
5851
+ */
5852
+ parseCookie(req, name) {
5853
+ const header = req.headers.cookie;
5854
+ if (!header) return null;
5855
+ for (const part of header.split(";")) {
5856
+ const [key, ...rest] = part.split("=");
5857
+ if (key?.trim() === name) {
5858
+ return rest.join("=").trim();
5859
+ }
5860
+ }
5861
+ return null;
5862
+ }
5607
5863
  // ── Session Management (SEC-012) ──────────────────────────────────
5608
5864
  /**
5609
5865
  * Create a short-lived session by exchanging the long-lived auth token
@@ -5624,7 +5880,7 @@ var DashboardApprovalChannel = class {
5624
5880
  this.sessions.set(id, {
5625
5881
  id,
5626
5882
  created_at: now,
5627
- expires_at: now + SESSION_TTL_MS
5883
+ expires_at: now + this.sessionTTLMs
5628
5884
  });
5629
5885
  return id;
5630
5886
  }
@@ -5723,13 +5979,26 @@ var DashboardApprovalChannel = class {
5723
5979
  res.end();
5724
5980
  return;
5725
5981
  }
5726
- if (!this.checkAuth(req, url, res)) return;
5727
- if (!this.checkRateLimit(req, res, "general")) return;
5728
- try {
5729
- if (method === "POST" && url.pathname === "/auth/session") {
5982
+ if (method === "POST" && url.pathname === "/auth/session") {
5983
+ if (!this.checkRateLimit(req, res, "general")) return;
5984
+ try {
5730
5985
  this.handleSessionExchange(req, res);
5986
+ } catch {
5987
+ res.writeHead(500, { "Content-Type": "application/json" });
5988
+ res.end(JSON.stringify({ error: "Internal server error" }));
5989
+ }
5990
+ return;
5991
+ }
5992
+ if (method === "GET" && url.pathname === "/" && this.authToken) {
5993
+ if (!this.isAuthenticated(req, url)) {
5994
+ if (!this.checkRateLimit(req, res, "general")) return;
5995
+ this.serveLoginPage(res);
5731
5996
  return;
5732
5997
  }
5998
+ }
5999
+ if (!this.checkAuth(req, url, res)) return;
6000
+ if (!this.checkRateLimit(req, res, "general")) return;
6001
+ try {
5733
6002
  if (method === "GET" && url.pathname === "/") {
5734
6003
  this.serveDashboard(res);
5735
6004
  } else if (method === "GET" && url.pathname === "/events") {
@@ -5786,12 +6055,23 @@ var DashboardApprovalChannel = class {
5786
6055
  return;
5787
6056
  }
5788
6057
  const sessionId = this.createSession();
5789
- res.writeHead(200, { "Content-Type": "application/json" });
6058
+ const ttlSeconds = Math.floor(this.sessionTTLMs / 1e3);
6059
+ res.writeHead(200, {
6060
+ "Content-Type": "application/json",
6061
+ "Set-Cookie": `sanctuary_session=${sessionId}; Path=/; SameSite=Strict; Max-Age=${ttlSeconds}`
6062
+ });
5790
6063
  res.end(JSON.stringify({
5791
6064
  session_id: sessionId,
5792
- expires_in_seconds: SESSION_TTL_MS / 1e3
6065
+ expires_in_seconds: ttlSeconds
5793
6066
  }));
5794
6067
  }
6068
+ serveLoginPage(res) {
6069
+ res.writeHead(200, {
6070
+ "Content-Type": "text/html; charset=utf-8",
6071
+ "Cache-Control": "no-cache, no-store"
6072
+ });
6073
+ res.end(this.loginHTML);
6074
+ }
5795
6075
  serveDashboard(res) {
5796
6076
  res.writeHead(200, {
5797
6077
  "Content-Type": "text/html; charset=utf-8",
@@ -5967,6 +6247,22 @@ data: ${JSON.stringify(data)}
5967
6247
  broadcastProtectionStatus(data) {
5968
6248
  this.broadcastSSE("protection-status", data);
5969
6249
  }
6250
+ /**
6251
+ * Create a pre-authenticated URL for the dashboard.
6252
+ * Used by the sanctuary_dashboard_open tool and at startup.
6253
+ */
6254
+ createSessionUrl() {
6255
+ const sessionId = this.createSession();
6256
+ const protocol = this.useTLS ? "https" : "http";
6257
+ return `${protocol}://${this.config.host}:${this.config.port}/?session=${sessionId}`;
6258
+ }
6259
+ /**
6260
+ * Get the base URL for the dashboard.
6261
+ */
6262
+ getBaseUrl() {
6263
+ const protocol = this.useTLS ? "https" : "http";
6264
+ return `${protocol}://${this.config.host}:${this.config.port}`;
6265
+ }
5970
6266
  /** Get the number of pending requests */
5971
6267
  get pendingCount() {
5972
6268
  return this.pending.size;
@@ -11781,6 +12077,32 @@ async function createSanctuaryServer(options) {
11781
12077
  } : void 0;
11782
12078
  const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog, injectionDetector, onInjectionAlert);
11783
12079
  const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
12080
+ const dashboardTools = [];
12081
+ if (dashboard) {
12082
+ dashboardTools.push({
12083
+ name: "sanctuary/dashboard_open",
12084
+ 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.",
12085
+ inputSchema: {
12086
+ type: "object",
12087
+ properties: {}
12088
+ },
12089
+ handler: async () => {
12090
+ const url = dashboard.createSessionUrl();
12091
+ return {
12092
+ content: [
12093
+ {
12094
+ type: "text",
12095
+ text: JSON.stringify({
12096
+ dashboard_url: url,
12097
+ base_url: dashboard.getBaseUrl(),
12098
+ note: "Click the dashboard_url to open the Principal Dashboard. The session is pre-authenticated."
12099
+ }, null, 2)
12100
+ }
12101
+ ]
12102
+ };
12103
+ }
12104
+ });
12105
+ }
11784
12106
  let allTools = [
11785
12107
  ...l1Tools,
11786
12108
  ...l2Tools,
@@ -11794,6 +12116,7 @@ async function createSanctuaryServer(options) {
11794
12116
  ...auditTools,
11795
12117
  ...contextGateTools,
11796
12118
  ...hardeningTools,
12119
+ ...dashboardTools,
11797
12120
  manifestTool
11798
12121
  ];
11799
12122
  allTools = allTools.map((tool) => ({