@sanctuary-framework/mcp-server 0.5.4 → 0.5.6

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
@@ -231,7 +231,7 @@ function defaultConfig() {
231
231
  }
232
232
  },
233
233
  disclosure: {
234
- proof_system: "commitment-only",
234
+ proof_system: "schnorr-pedersen",
235
235
  default_policy: "minimum-necessary"
236
236
  },
237
237
  reputation: {
@@ -345,7 +345,7 @@ function validateConfig(config) {
345
345
  `Unimplemented config value: execution.environment = "${config.execution.environment}". Only ${[...implementedEnvironment].map((v) => `"${v}"`).join(", ")} are currently implemented. Using an unimplemented environment would silently degrade security.`
346
346
  );
347
347
  }
348
- const implementedProofSystem = /* @__PURE__ */ new Set(["commitment-only"]);
348
+ const implementedProofSystem = /* @__PURE__ */ new Set(["schnorr-pedersen", "commitment-only"]);
349
349
  if (!implementedProofSystem.has(config.disclosure.proof_system)) {
350
350
  errors.push(
351
351
  `Unimplemented config value: disclosure.proof_system = "${config.disclosure.proof_system}". Only ${[...implementedProofSystem].map((v) => `"${v}"`).join(", ")} is currently implemented. Using an unimplemented proof system would silently degrade security.`
@@ -3638,6 +3638,8 @@ var DEFAULT_POLICY = {
3638
3638
  "handshake_respond",
3639
3639
  "handshake_complete",
3640
3640
  "handshake_status",
3641
+ "handshake_exchange",
3642
+ "handshake_verify_attestation",
3641
3643
  "reputation_query_weighted",
3642
3644
  "federation_peers",
3643
3645
  "federation_trust_evaluate",
@@ -3807,6 +3809,8 @@ tier3_always_allow:
3807
3809
  - handshake_respond
3808
3810
  - handshake_complete
3809
3811
  - handshake_status
3812
+ - handshake_exchange
3813
+ - handshake_verify_attestation
3810
3814
  - reputation_query_weighted
3811
3815
  - federation_peers
3812
3816
  - federation_trust_evaluate
@@ -4068,174 +4072,255 @@ function generateLoginHTML(options) {
4068
4072
  return `<!DOCTYPE html>
4069
4073
  <html lang="en">
4070
4074
  <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>
4075
+ <meta charset="UTF-8">
4076
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
4077
+ <title>Sanctuary Dashboard</title>
4078
+ <style>
4079
+ :root {
4080
+ --bg: #0d1117;
4081
+ --surface: #161b22;
4082
+ --border: #30363d;
4083
+ --text-primary: #e6edf3;
4084
+ --text-secondary: #8b949e;
4085
+ --green: #3fb950;
4086
+ --amber: #d29922;
4087
+ --red: #f85149;
4088
+ --blue: #58a6ff;
4089
+ }
4090
+
4091
+ * {
4092
+ margin: 0;
4093
+ padding: 0;
4094
+ box-sizing: border-box;
4095
+ }
4096
+
4097
+ body {
4098
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
4099
+ background-color: var(--bg);
4100
+ color: var(--text-primary);
4101
+ min-height: 100vh;
4102
+ display: flex;
4103
+ align-items: center;
4104
+ justify-content: center;
4105
+ }
4106
+
4107
+ .login-container {
4108
+ width: 100%;
4109
+ max-width: 400px;
4110
+ padding: 20px;
4111
+ }
4112
+
4113
+ .login-card {
4114
+ background-color: var(--surface);
4115
+ border: 1px solid var(--border);
4116
+ border-radius: 8px;
4117
+ padding: 40px 32px;
4118
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
4119
+ }
4120
+
4121
+ .login-header {
4122
+ display: flex;
4123
+ align-items: center;
4124
+ gap: 12px;
4125
+ margin-bottom: 32px;
4126
+ }
4127
+
4128
+ .logo {
4129
+ font-size: 24px;
4130
+ font-weight: 700;
4131
+ color: var(--blue);
4132
+ }
4133
+
4134
+ .logo-text {
4135
+ display: flex;
4136
+ flex-direction: column;
4137
+ }
4138
+
4139
+ .logo-text .title {
4140
+ font-size: 18px;
4141
+ font-weight: 600;
4142
+ letter-spacing: -0.5px;
4143
+ }
4144
+
4145
+ .logo-text .version {
4146
+ font-size: 12px;
4147
+ color: var(--text-secondary);
4148
+ margin-top: 2px;
4149
+ }
4150
+
4151
+ .form-group {
4152
+ margin-bottom: 24px;
4153
+ }
4154
+
4155
+ label {
4156
+ display: block;
4157
+ font-size: 14px;
4158
+ font-weight: 500;
4159
+ margin-bottom: 8px;
4160
+ color: var(--text-primary);
4161
+ }
4162
+
4163
+ input[type="text"],
4164
+ input[type="password"] {
4165
+ width: 100%;
4166
+ padding: 10px 12px;
4167
+ background-color: var(--bg);
4168
+ border: 1px solid var(--border);
4169
+ border-radius: 6px;
4170
+ color: var(--text-primary);
4171
+ font-size: 14px;
4172
+ font-family: 'JetBrains Mono', monospace;
4173
+ transition: border-color 0.2s;
4174
+ }
4175
+
4176
+ input[type="text"]:focus,
4177
+ input[type="password"]:focus {
4178
+ outline: none;
4179
+ border-color: var(--blue);
4180
+ box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.1);
4181
+ }
4182
+
4183
+ .error-message {
4184
+ display: none;
4185
+ background-color: rgba(248, 81, 73, 0.1);
4186
+ border: 1px solid var(--red);
4187
+ color: #ff9999;
4188
+ padding: 12px;
4189
+ border-radius: 6px;
4190
+ font-size: 13px;
4191
+ margin-bottom: 20px;
4192
+ }
4193
+
4194
+ .error-message.show {
4195
+ display: block;
4196
+ }
4197
+
4198
+ button {
4199
+ width: 100%;
4200
+ padding: 10px 16px;
4201
+ background-color: var(--blue);
4202
+ color: var(--bg);
4203
+ border: none;
4204
+ border-radius: 6px;
4205
+ font-size: 14px;
4206
+ font-weight: 600;
4207
+ cursor: pointer;
4208
+ transition: background-color 0.2s;
4209
+ }
4210
+
4211
+ button:hover {
4212
+ background-color: #79c0ff;
4213
+ }
4214
+
4215
+ button:active {
4216
+ background-color: #4184e4;
4217
+ }
4218
+
4219
+ button:disabled {
4220
+ background-color: var(--text-secondary);
4221
+ cursor: not-allowed;
4222
+ opacity: 0.5;
4223
+ }
4224
+
4225
+ .info-text {
4226
+ font-size: 12px;
4227
+ color: var(--text-secondary);
4228
+ margin-top: 16px;
4229
+ text-align: center;
4230
+ }
4231
+ </style>
4185
4232
  </head>
4186
4233
  <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.
4234
+ <div class="login-container">
4235
+ <div class="login-card">
4236
+ <div class="login-header">
4237
+ <div class="logo">\u25C6</div>
4238
+ <div class="logo-text">
4239
+ <div class="title">SANCTUARY</div>
4240
+ <div class="version">v${options.serverVersion}</div>
4241
+ </div>
4242
+ </div>
4243
+
4244
+ <div id="error-message" class="error-message"></div>
4245
+
4246
+ <form id="login-form">
4247
+ <div class="form-group">
4248
+ <label for="auth-token">Bearer Token</label>
4249
+ <input
4250
+ type="text"
4251
+ id="auth-token"
4252
+ name="token"
4253
+ placeholder="Paste your session token..."
4254
+ autocomplete="off"
4255
+ spellcheck="false"
4256
+ required
4257
+ />
4258
+ </div>
4259
+
4260
+ <button type="submit" id="login-button">Open Dashboard</button>
4261
+ </form>
4262
+
4263
+ <div class="info-text">
4264
+ Session tokens expire after 1 hour of inactivity
4265
+ </div>
4266
+ </div>
4200
4267
  </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 }
4268
+
4269
+ <script>
4270
+ const loginForm = document.getElementById('login-form');
4271
+ const authTokenInput = document.getElementById('auth-token');
4272
+ const errorMessage = document.getElementById('error-message');
4273
+ const loginButton = document.getElementById('login-button');
4274
+
4275
+ loginForm.addEventListener('submit', async (e) => {
4276
+ e.preventDefault();
4277
+ const token = authTokenInput.value.trim();
4278
+
4279
+ if (!token) {
4280
+ showError('Token is required');
4281
+ return;
4282
+ }
4283
+
4284
+ loginButton.disabled = true;
4285
+ loginButton.textContent = 'Verifying...';
4286
+ errorMessage.classList.remove('show');
4287
+
4288
+ try {
4289
+ const response = await fetch('/auth/session', {
4290
+ method: 'POST',
4291
+ headers: {
4292
+ 'Content-Type': 'application/json',
4293
+ 'Authorization': 'Bearer ' + token,
4294
+ },
4295
+ body: JSON.stringify({ token }),
4296
+ });
4297
+
4298
+ if (response.ok) {
4299
+ const data = await response.json();
4300
+ sessionStorage.setItem('authToken', token);
4301
+ window.location.href = '/dashboard';
4302
+ } else if (response.status === 401) {
4303
+ showError('Invalid token. Please check and try again.');
4304
+ } else {
4305
+ showError('Authentication failed. Please try again.');
4306
+ }
4307
+ } catch (err) {
4308
+ showError('Connection error. Please check your network.');
4309
+ } finally {
4310
+ loginButton.disabled = false;
4311
+ loginButton.textContent = 'Open Dashboard';
4312
+ }
4216
4313
  });
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>
4314
+
4315
+ function showError(message) {
4316
+ errorMessage.textContent = message;
4317
+ errorMessage.classList.add('show');
4318
+ }
4319
+
4320
+ authTokenInput.addEventListener('input', () => {
4321
+ errorMessage.classList.remove('show');
4322
+ });
4323
+ </script>
4239
4324
  </body>
4240
4325
  </html>`;
4241
4326
  }
@@ -4243,1412 +4328,1648 @@ function generateDashboardHTML(options) {
4243
4328
  return `<!DOCTYPE html>
4244
4329
  <html lang="en">
4245
4330
  <head>
4246
- <meta charset="utf-8">
4247
- <meta name="viewport" content="width=device-width, initial-scale=1">
4248
- <title>Sanctuary \u2014 Principal Dashboard</title>
4249
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
4250
- <style>
4251
- :root {
4252
- --bg: #0d1117;
4253
- --surface: #161b22;
4254
- --border: #30363d;
4255
- --text-primary: #e6edf3;
4256
- --text-secondary: #8b949e;
4257
- --green: #3fb950;
4258
- --amber: #d29922;
4259
- --red: #f85149;
4260
- --blue: #58a6ff;
4261
- --mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
4262
- --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
4263
- --radius: 6px;
4264
- }
4265
-
4266
- * {
4267
- box-sizing: border-box;
4268
- margin: 0;
4269
- padding: 0;
4270
- }
4331
+ <meta charset="UTF-8">
4332
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
4333
+ <title>Sanctuary Dashboard</title>
4334
+ <style>
4335
+ :root {
4336
+ --bg: #0d1117;
4337
+ --surface: #161b22;
4338
+ --border: #30363d;
4339
+ --text-primary: #e6edf3;
4340
+ --text-secondary: #8b949e;
4341
+ --green: #3fb950;
4342
+ --amber: #d29922;
4343
+ --red: #f85149;
4344
+ --blue: #58a6ff;
4345
+ --success: #3fb950;
4346
+ --warning: #d29922;
4347
+ --error: #f85149;
4348
+ --muted: #21262d;
4349
+ }
4350
+
4351
+ * {
4352
+ margin: 0;
4353
+ padding: 0;
4354
+ box-sizing: border-box;
4355
+ }
4356
+
4357
+ html, body {
4358
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
4359
+ background-color: var(--bg);
4360
+ color: var(--text-primary);
4361
+ height: 100%;
4362
+ overflow: hidden;
4363
+ }
4364
+
4365
+ body {
4366
+ display: flex;
4367
+ flex-direction: column;
4368
+ }
4369
+
4370
+ /* Status Bar */
4371
+ .status-bar {
4372
+ position: fixed;
4373
+ top: 0;
4374
+ left: 0;
4375
+ right: 0;
4376
+ height: 56px;
4377
+ background-color: var(--surface);
4378
+ border-bottom: 1px solid var(--border);
4379
+ display: flex;
4380
+ align-items: center;
4381
+ padding: 0 24px;
4382
+ gap: 24px;
4383
+ z-index: 100;
4384
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
4385
+ }
4386
+
4387
+ .status-bar-left {
4388
+ display: flex;
4389
+ align-items: center;
4390
+ gap: 12px;
4391
+ flex: 0 0 auto;
4392
+ }
4271
4393
 
4272
- html, body {
4273
- width: 100%;
4274
- height: 100%;
4275
- overflow: hidden;
4276
- }
4277
-
4278
- body {
4279
- font-family: var(--sans);
4280
- background: var(--bg);
4281
- color: var(--text-primary);
4282
- display: flex;
4283
- flex-direction: column;
4284
- }
4285
-
4286
- /* \u2500\u2500 Top Status Bar (fixed) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4394
+ .logo-icon {
4395
+ font-size: 20px;
4396
+ color: var(--blue);
4397
+ font-weight: 700;
4398
+ }
4287
4399
 
4288
- .status-bar {
4289
- position: fixed;
4290
- top: 0;
4291
- left: 0;
4292
- right: 0;
4293
- height: 56px;
4294
- background: var(--surface);
4295
- border-bottom: 1px solid var(--border);
4296
- display: flex;
4297
- align-items: center;
4298
- padding: 0 20px;
4299
- gap: 24px;
4300
- z-index: 1000;
4301
- }
4400
+ .logo-info {
4401
+ display: flex;
4402
+ flex-direction: column;
4403
+ }
4302
4404
 
4303
- .status-bar-left {
4304
- display: flex;
4305
- align-items: center;
4306
- gap: 12px;
4307
- flex: 0 0 auto;
4308
- }
4405
+ .logo-title {
4406
+ font-size: 13px;
4407
+ font-weight: 600;
4408
+ line-height: 1;
4409
+ color: var(--text-primary);
4410
+ }
4309
4411
 
4310
- .sanctuary-logo {
4311
- font-weight: 700;
4312
- font-size: 16px;
4313
- letter-spacing: -0.5px;
4314
- color: var(--text-primary);
4315
- }
4412
+ .logo-version {
4413
+ font-size: 11px;
4414
+ color: var(--text-secondary);
4415
+ margin-top: 2px;
4416
+ }
4316
4417
 
4317
- .sanctuary-logo span {
4318
- color: var(--blue);
4319
- }
4418
+ .status-bar-center {
4419
+ flex: 1;
4420
+ display: flex;
4421
+ justify-content: center;
4422
+ }
4320
4423
 
4321
- .version {
4322
- font-size: 11px;
4323
- color: var(--text-secondary);
4324
- font-family: var(--mono);
4325
- }
4424
+ .sovereignty-badge {
4425
+ display: flex;
4426
+ align-items: center;
4427
+ gap: 8px;
4428
+ padding: 8px 16px;
4429
+ background-color: rgba(63, 185, 80, 0.1);
4430
+ border: 1px solid rgba(63, 185, 80, 0.3);
4431
+ border-radius: 6px;
4432
+ font-size: 13px;
4433
+ font-weight: 500;
4434
+ }
4326
4435
 
4327
- .status-bar-center {
4328
- flex: 1;
4329
- display: flex;
4330
- align-items: center;
4331
- justify-content: center;
4332
- }
4333
-
4334
- .sovereignty-badge {
4335
- display: flex;
4336
- align-items: center;
4337
- gap: 8px;
4338
- padding: 6px 12px;
4339
- background: rgba(88, 166, 255, 0.1);
4340
- border: 1px solid var(--blue);
4341
- border-radius: 20px;
4342
- font-size: 13px;
4343
- font-weight: 600;
4344
- }
4436
+ .sovereignty-badge.degraded {
4437
+ background-color: rgba(210, 153, 34, 0.1);
4438
+ border-color: rgba(210, 153, 34, 0.3);
4439
+ }
4345
4440
 
4346
- .sovereignty-score {
4347
- display: flex;
4348
- align-items: center;
4349
- justify-content: center;
4350
- width: 28px;
4351
- height: 28px;
4352
- border-radius: 50%;
4353
- font-family: var(--mono);
4354
- font-weight: 700;
4355
- font-size: 12px;
4356
- background: var(--blue);
4357
- color: var(--bg);
4358
- }
4441
+ .sovereignty-badge.inactive {
4442
+ background-color: rgba(248, 81, 73, 0.1);
4443
+ border-color: rgba(248, 81, 73, 0.3);
4444
+ }
4359
4445
 
4360
- .sovereignty-score.high {
4361
- background: var(--green);
4362
- }
4446
+ .sovereignty-score {
4447
+ font-weight: 700;
4448
+ color: var(--green);
4449
+ }
4363
4450
 
4364
- .sovereignty-score.medium {
4365
- background: var(--amber);
4366
- }
4451
+ .sovereignty-badge.degraded .sovereignty-score {
4452
+ color: var(--amber);
4453
+ }
4367
4454
 
4368
- .sovereignty-score.low {
4369
- background: var(--red);
4370
- }
4455
+ .sovereignty-badge.inactive .sovereignty-score {
4456
+ color: var(--red);
4457
+ }
4371
4458
 
4372
- .status-bar-right {
4373
- display: flex;
4374
- align-items: center;
4375
- gap: 16px;
4376
- flex: 0 0 auto;
4377
- }
4459
+ .status-bar-right {
4460
+ display: flex;
4461
+ align-items: center;
4462
+ gap: 16px;
4463
+ flex: 0 0 auto;
4464
+ }
4378
4465
 
4379
- .protections-indicator {
4380
- display: flex;
4381
- align-items: center;
4382
- gap: 6px;
4383
- font-size: 12px;
4384
- color: var(--text-secondary);
4385
- font-family: var(--mono);
4386
- }
4466
+ .status-item {
4467
+ display: flex;
4468
+ align-items: center;
4469
+ gap: 6px;
4470
+ font-size: 12px;
4471
+ color: var(--text-secondary);
4472
+ }
4387
4473
 
4388
- .protections-indicator .count {
4389
- color: var(--text-primary);
4390
- font-weight: 600;
4391
- }
4474
+ .status-item strong {
4475
+ color: var(--text-primary);
4476
+ font-weight: 500;
4477
+ }
4392
4478
 
4393
- .uptime {
4394
- display: flex;
4395
- align-items: center;
4396
- gap: 6px;
4397
- font-size: 12px;
4398
- color: var(--text-secondary);
4399
- font-family: var(--mono);
4400
- }
4479
+ .status-dot {
4480
+ width: 8px;
4481
+ height: 8px;
4482
+ border-radius: 50%;
4483
+ background-color: var(--green);
4484
+ }
4401
4485
 
4402
- .status-dot {
4403
- width: 8px;
4404
- height: 8px;
4405
- border-radius: 50%;
4406
- background: var(--green);
4407
- animation: pulse 2s ease-in-out infinite;
4408
- }
4486
+ .status-dot.disconnected {
4487
+ background-color: var(--red);
4488
+ }
4409
4489
 
4410
- .status-dot.disconnected {
4411
- background: var(--red);
4412
- animation: none;
4413
- }
4490
+ .pending-badge {
4491
+ display: flex;
4492
+ align-items: center;
4493
+ gap: 6px;
4494
+ padding: 4px 8px;
4495
+ background-color: var(--blue);
4496
+ color: var(--bg);
4497
+ border-radius: 4px;
4498
+ font-size: 11px;
4499
+ font-weight: 600;
4500
+ cursor: pointer;
4501
+ }
4414
4502
 
4415
- @keyframes pulse {
4416
- 0%, 100% { opacity: 1; }
4417
- 50% { opacity: 0.5; }
4418
- }
4503
+ /* Main Content */
4504
+ .main-content {
4505
+ flex: 1;
4506
+ margin-top: 56px;
4507
+ overflow-y: auto;
4508
+ padding: 24px;
4509
+ }
4419
4510
 
4420
- .pending-badge {
4421
- display: inline-flex;
4422
- align-items: center;
4423
- justify-content: center;
4424
- min-width: 24px;
4425
- height: 24px;
4426
- padding: 0 6px;
4427
- background: var(--red);
4428
- color: white;
4429
- border-radius: 12px;
4430
- font-size: 11px;
4431
- font-weight: 700;
4432
- animation: pulse 1s ease-in-out infinite;
4433
- }
4511
+ .grid {
4512
+ display: grid;
4513
+ gap: 20px;
4514
+ }
4434
4515
 
4435
- .pending-badge.hidden {
4436
- display: none;
4437
- }
4516
+ /* Row 1: Sovereignty Layers */
4517
+ .sovereignty-layers {
4518
+ display: grid;
4519
+ grid-template-columns: repeat(4, 1fr);
4520
+ gap: 16px;
4521
+ }
4438
4522
 
4439
- /* \u2500\u2500 Main Layout \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4523
+ .layer-card {
4524
+ background-color: var(--surface);
4525
+ border: 1px solid var(--border);
4526
+ border-radius: 8px;
4527
+ padding: 20px;
4528
+ display: flex;
4529
+ flex-direction: column;
4530
+ gap: 12px;
4531
+ }
4440
4532
 
4441
- .main-container {
4442
- flex: 1;
4443
- display: flex;
4444
- margin-top: 56px;
4445
- overflow: hidden;
4446
- }
4533
+ .layer-card.degraded {
4534
+ border-color: var(--amber);
4535
+ background-color: rgba(210, 153, 34, 0.05);
4536
+ }
4447
4537
 
4448
- .activity-feed {
4449
- flex: 3;
4450
- display: flex;
4451
- flex-direction: column;
4452
- border-right: 1px solid var(--border);
4453
- overflow: hidden;
4454
- }
4538
+ .layer-card.inactive {
4539
+ border-color: var(--red);
4540
+ background-color: rgba(248, 81, 73, 0.05);
4541
+ }
4455
4542
 
4456
- .feed-header {
4457
- padding: 16px 20px;
4458
- border-bottom: 1px solid var(--border);
4459
- display: flex;
4460
- align-items: center;
4461
- gap: 8px;
4462
- font-size: 12px;
4463
- font-weight: 600;
4464
- text-transform: uppercase;
4465
- letter-spacing: 0.5px;
4466
- color: var(--text-secondary);
4467
- }
4543
+ .layer-name {
4544
+ font-size: 12px;
4545
+ font-weight: 600;
4546
+ color: var(--text-secondary);
4547
+ text-transform: uppercase;
4548
+ letter-spacing: 0.5px;
4549
+ }
4468
4550
 
4469
- .feed-header-dot {
4470
- width: 6px;
4471
- height: 6px;
4472
- border-radius: 50%;
4473
- background: var(--green);
4474
- }
4551
+ .layer-title {
4552
+ font-size: 14px;
4553
+ font-weight: 600;
4554
+ color: var(--text-primary);
4555
+ }
4475
4556
 
4476
- .activity-list {
4477
- flex: 1;
4478
- overflow-y: auto;
4479
- overflow-x: hidden;
4480
- }
4557
+ .layer-status {
4558
+ display: inline-flex;
4559
+ align-items: center;
4560
+ gap: 6px;
4561
+ padding: 4px 8px;
4562
+ background-color: rgba(63, 185, 80, 0.15);
4563
+ color: var(--green);
4564
+ border-radius: 4px;
4565
+ font-size: 11px;
4566
+ font-weight: 600;
4567
+ width: fit-content;
4568
+ }
4481
4569
 
4482
- .activity-item {
4483
- padding: 12px 20px;
4484
- border-bottom: 1px solid rgba(48, 54, 61, 0.5);
4485
- font-size: 13px;
4486
- font-family: var(--mono);
4487
- cursor: pointer;
4488
- transition: background 0.15s;
4489
- display: flex;
4490
- align-items: flex-start;
4491
- gap: 10px;
4492
- }
4570
+ .layer-card.degraded .layer-status {
4571
+ background-color: rgba(210, 153, 34, 0.15);
4572
+ color: var(--amber);
4573
+ }
4493
4574
 
4494
- .activity-item:hover {
4495
- background: rgba(88, 166, 255, 0.05);
4496
- }
4575
+ .layer-card.inactive .layer-status {
4576
+ background-color: rgba(248, 81, 73, 0.15);
4577
+ color: var(--red);
4578
+ }
4497
4579
 
4498
- .activity-item-icon {
4499
- flex: 0 0 auto;
4500
- width: 16px;
4501
- text-align: center;
4502
- font-size: 12px;
4503
- color: var(--text-secondary);
4504
- margin-top: 1px;
4505
- }
4580
+ .layer-detail {
4581
+ font-size: 12px;
4582
+ color: var(--text-secondary);
4583
+ font-family: 'JetBrains Mono', monospace;
4584
+ padding: 8px;
4585
+ background-color: var(--bg);
4586
+ border-radius: 4px;
4587
+ border-left: 2px solid var(--blue);
4588
+ }
4506
4589
 
4507
- .activity-item-content {
4508
- flex: 1;
4509
- min-width: 0;
4510
- }
4590
+ /* Row 2: Info Cards */
4591
+ .info-cards {
4592
+ display: grid;
4593
+ grid-template-columns: repeat(3, 1fr);
4594
+ gap: 16px;
4595
+ }
4511
4596
 
4512
- .activity-time {
4513
- color: var(--text-secondary);
4514
- font-size: 11px;
4515
- margin-bottom: 2px;
4516
- }
4597
+ .info-card {
4598
+ background-color: var(--surface);
4599
+ border: 1px solid var(--border);
4600
+ border-radius: 8px;
4601
+ padding: 20px;
4602
+ }
4517
4603
 
4518
- .activity-main {
4519
- display: flex;
4520
- gap: 8px;
4521
- align-items: baseline;
4522
- margin-bottom: 4px;
4523
- }
4604
+ .card-header {
4605
+ font-size: 12px;
4606
+ font-weight: 600;
4607
+ color: var(--text-secondary);
4608
+ text-transform: uppercase;
4609
+ letter-spacing: 0.5px;
4610
+ margin-bottom: 16px;
4611
+ }
4524
4612
 
4525
- .activity-tier {
4526
- display: inline-flex;
4527
- align-items: center;
4528
- justify-content: center;
4529
- width: 24px;
4530
- height: 16px;
4531
- font-size: 10px;
4532
- font-weight: 700;
4533
- border-radius: 3px;
4534
- text-transform: uppercase;
4535
- flex: 0 0 auto;
4536
- }
4613
+ .card-row {
4614
+ display: flex;
4615
+ justify-content: space-between;
4616
+ align-items: center;
4617
+ margin-bottom: 12px;
4618
+ font-size: 13px;
4619
+ }
4537
4620
 
4538
- .activity-tier.t1 {
4539
- background: rgba(248, 81, 73, 0.2);
4540
- color: var(--red);
4541
- }
4621
+ .card-row:last-child {
4622
+ margin-bottom: 0;
4623
+ }
4542
4624
 
4543
- .activity-tier.t2 {
4544
- background: rgba(210, 153, 34, 0.2);
4545
- color: var(--amber);
4546
- }
4625
+ .card-label {
4626
+ color: var(--text-secondary);
4627
+ }
4547
4628
 
4548
- .activity-tier.t3 {
4549
- background: rgba(63, 185, 80, 0.2);
4550
- color: var(--green);
4551
- }
4629
+ .card-value {
4630
+ color: var(--text-primary);
4631
+ font-family: 'JetBrains Mono', monospace;
4632
+ font-weight: 500;
4633
+ }
4552
4634
 
4553
- .activity-tool {
4554
- color: var(--text-primary);
4555
- font-weight: 600;
4556
- }
4635
+ .identity-badge {
4636
+ display: inline-flex;
4637
+ align-items: center;
4638
+ gap: 4px;
4639
+ padding: 2px 6px;
4640
+ background-color: rgba(88, 166, 255, 0.15);
4641
+ color: var(--blue);
4642
+ border-radius: 3px;
4643
+ font-size: 10px;
4644
+ font-weight: 600;
4645
+ text-transform: uppercase;
4646
+ }
4557
4647
 
4558
- .activity-outcome {
4559
- color: var(--green);
4560
- }
4648
+ .trust-tier-badge {
4649
+ display: inline-flex;
4650
+ align-items: center;
4651
+ gap: 4px;
4652
+ padding: 2px 6px;
4653
+ background-color: rgba(63, 185, 80, 0.15);
4654
+ color: var(--green);
4655
+ border-radius: 3px;
4656
+ font-size: 10px;
4657
+ font-weight: 600;
4658
+ }
4561
4659
 
4562
- .activity-outcome.denied {
4563
- color: var(--red);
4564
- }
4660
+ .truncated {
4661
+ max-width: 200px;
4662
+ overflow: hidden;
4663
+ text-overflow: ellipsis;
4664
+ white-space: nowrap;
4665
+ }
4565
4666
 
4566
- .activity-detail {
4567
- font-size: 12px;
4568
- color: var(--text-secondary);
4569
- margin-left: 0;
4570
- }
4667
+ /* Row 3: SHR & Activity */
4668
+ .main-panels {
4669
+ display: grid;
4670
+ grid-template-columns: 1fr 1fr;
4671
+ gap: 16px;
4672
+ min-height: 400px;
4673
+ }
4571
4674
 
4572
- .activity-item.expanded .activity-detail {
4573
- display: block;
4574
- margin-top: 8px;
4575
- padding: 10px;
4576
- background: rgba(88, 166, 255, 0.08);
4577
- border-left: 2px solid var(--blue);
4578
- border-radius: 4px;
4579
- }
4675
+ .panel {
4676
+ background-color: var(--surface);
4677
+ border: 1px solid var(--border);
4678
+ border-radius: 8px;
4679
+ display: flex;
4680
+ flex-direction: column;
4681
+ overflow: hidden;
4682
+ }
4580
4683
 
4581
- .activity-empty {
4582
- display: flex;
4583
- flex-direction: column;
4584
- align-items: center;
4585
- justify-content: center;
4586
- height: 100%;
4587
- color: var(--text-secondary);
4588
- }
4684
+ .panel-header {
4685
+ padding: 16px 20px;
4686
+ border-bottom: 1px solid var(--border);
4687
+ display: flex;
4688
+ justify-content: space-between;
4689
+ align-items: center;
4690
+ }
4589
4691
 
4590
- .activity-empty-icon {
4591
- font-size: 32px;
4592
- margin-bottom: 12px;
4593
- }
4692
+ .panel-title {
4693
+ font-size: 14px;
4694
+ font-weight: 600;
4695
+ color: var(--text-primary);
4696
+ }
4594
4697
 
4595
- .activity-empty-text {
4596
- font-size: 14px;
4597
- }
4698
+ .panel-action {
4699
+ background: none;
4700
+ border: none;
4701
+ color: var(--blue);
4702
+ cursor: pointer;
4703
+ font-size: 12px;
4704
+ padding: 0;
4705
+ font-weight: 500;
4706
+ transition: color 0.2s;
4707
+ }
4598
4708
 
4599
- /* \u2500\u2500 Protection Status Sidebar (40%) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4709
+ .panel-action:hover {
4710
+ color: #79c0ff;
4711
+ }
4600
4712
 
4601
- .protection-sidebar {
4602
- flex: 2;
4603
- display: flex;
4604
- flex-direction: column;
4605
- background: rgba(22, 27, 34, 0.5);
4606
- overflow: hidden;
4607
- }
4713
+ .panel-content {
4714
+ flex: 1;
4715
+ overflow-y: auto;
4716
+ padding: 20px;
4717
+ }
4608
4718
 
4609
- .sidebar-header {
4610
- padding: 16px 20px;
4611
- border-bottom: 1px solid var(--border);
4612
- font-size: 12px;
4613
- font-weight: 600;
4614
- text-transform: uppercase;
4615
- letter-spacing: 0.5px;
4616
- color: var(--text-secondary);
4617
- display: flex;
4618
- align-items: center;
4619
- gap: 8px;
4620
- }
4719
+ /* SHR Viewer */
4720
+ .shr-json {
4721
+ font-family: 'JetBrains Mono', monospace;
4722
+ font-size: 12px;
4723
+ line-height: 1.6;
4724
+ color: var(--text-secondary);
4725
+ }
4621
4726
 
4622
- .sidebar-content {
4623
- flex: 1;
4624
- overflow-y: auto;
4625
- padding: 16px 16px;
4626
- display: grid;
4627
- grid-template-columns: 1fr 1fr;
4628
- gap: 12px;
4629
- }
4727
+ .shr-section {
4728
+ margin-bottom: 12px;
4729
+ }
4630
4730
 
4631
- .protection-card {
4632
- background: var(--surface);
4633
- border: 1px solid var(--border);
4634
- border-radius: var(--radius);
4635
- padding: 14px;
4636
- display: flex;
4637
- flex-direction: column;
4638
- gap: 8px;
4639
- }
4731
+ .shr-section-header {
4732
+ display: flex;
4733
+ align-items: center;
4734
+ gap: 8px;
4735
+ cursor: pointer;
4736
+ font-weight: 600;
4737
+ color: var(--text-primary);
4738
+ padding: 8px;
4739
+ background-color: var(--bg);
4740
+ border-radius: 4px;
4741
+ user-select: none;
4742
+ }
4640
4743
 
4641
- .protection-card-icon {
4642
- font-size: 14px;
4643
- }
4744
+ .shr-section-header:hover {
4745
+ background-color: var(--muted);
4746
+ }
4644
4747
 
4645
- .protection-card-label {
4646
- font-size: 11px;
4647
- font-weight: 600;
4648
- text-transform: uppercase;
4649
- letter-spacing: 0.5px;
4650
- color: var(--text-secondary);
4651
- }
4748
+ .shr-toggle {
4749
+ width: 16px;
4750
+ height: 16px;
4751
+ display: flex;
4752
+ align-items: center;
4753
+ justify-content: center;
4754
+ font-size: 10px;
4755
+ transition: transform 0.2s;
4756
+ }
4652
4757
 
4653
- .protection-card-status {
4654
- display: flex;
4655
- align-items: center;
4656
- gap: 6px;
4657
- font-size: 12px;
4658
- font-weight: 600;
4659
- }
4758
+ .shr-section.collapsed .shr-toggle {
4759
+ transform: rotate(-90deg);
4760
+ }
4660
4761
 
4661
- .protection-card-status.active {
4662
- color: var(--green);
4663
- }
4762
+ .shr-section-content {
4763
+ padding: 8px 16px;
4764
+ background-color: rgba(0, 0, 0, 0.2);
4765
+ border-radius: 4px;
4766
+ margin-top: 4px;
4767
+ }
4664
4768
 
4665
- .protection-card-status.inactive {
4666
- color: var(--text-secondary);
4667
- }
4769
+ .shr-section.collapsed .shr-section-content {
4770
+ display: none;
4771
+ }
4668
4772
 
4669
- .protection-card-stat {
4670
- font-size: 11px;
4671
- color: var(--text-secondary);
4672
- font-family: var(--mono);
4673
- margin-top: 4px;
4674
- }
4773
+ .shr-item {
4774
+ display: flex;
4775
+ margin-bottom: 4px;
4776
+ }
4675
4777
 
4676
- /* \u2500\u2500 Pending Approvals Overlay \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4778
+ .shr-key {
4779
+ color: var(--blue);
4780
+ margin-right: 8px;
4781
+ min-width: 120px;
4782
+ }
4677
4783
 
4678
- .pending-overlay {
4679
- position: fixed;
4680
- top: 56px;
4681
- right: 0;
4682
- bottom: 0;
4683
- width: 0;
4684
- background: var(--surface);
4685
- border-left: 1px solid var(--border);
4686
- z-index: 999;
4687
- overflow-y: auto;
4688
- transition: width 0.3s ease-out;
4689
- display: flex;
4690
- flex-direction: column;
4691
- }
4784
+ .shr-value {
4785
+ color: var(--green);
4786
+ word-break: break-all;
4787
+ }
4692
4788
 
4693
- .pending-overlay.active {
4694
- width: 380px;
4695
- }
4789
+ /* Activity Feed */
4790
+ .activity-feed {
4791
+ display: flex;
4792
+ flex-direction: column;
4793
+ gap: 12px;
4794
+ }
4696
4795
 
4697
- @media (max-width: 1400px) {
4698
- .pending-overlay.active {
4699
- width: 100%;
4700
- right: auto;
4701
- left: 0;
4796
+ .activity-item {
4797
+ padding: 12px;
4798
+ background-color: var(--bg);
4799
+ border-left: 2px solid var(--border);
4800
+ border-radius: 4px;
4801
+ font-size: 12px;
4702
4802
  }
4703
- }
4704
4803
 
4705
- .pending-overlay-header {
4706
- padding: 16px 20px;
4707
- border-bottom: 1px solid var(--border);
4708
- display: flex;
4709
- align-items: center;
4710
- justify-content: space-between;
4711
- flex: 0 0 auto;
4712
- }
4804
+ .activity-item.tool-call {
4805
+ border-left-color: var(--blue);
4806
+ }
4713
4807
 
4714
- .pending-overlay-title {
4715
- font-size: 13px;
4716
- font-weight: 600;
4717
- text-transform: uppercase;
4718
- letter-spacing: 0.5px;
4719
- color: var(--text-primary);
4720
- }
4808
+ .activity-item.context-gate {
4809
+ border-left-color: var(--amber);
4810
+ }
4721
4811
 
4722
- .pending-overlay-close {
4723
- background: none;
4724
- border: none;
4725
- color: var(--text-secondary);
4726
- cursor: pointer;
4727
- font-size: 18px;
4728
- padding: 0;
4729
- display: flex;
4730
- align-items: center;
4731
- justify-content: center;
4732
- }
4812
+ .activity-item.injection {
4813
+ border-left-color: var(--red);
4814
+ }
4733
4815
 
4734
- .pending-overlay-close:hover {
4735
- color: var(--text-primary);
4736
- }
4816
+ .activity-item.protection {
4817
+ border-left-color: var(--green);
4818
+ }
4737
4819
 
4738
- .pending-list {
4739
- flex: 1;
4740
- overflow-y: auto;
4741
- }
4820
+ .activity-type {
4821
+ font-weight: 600;
4822
+ color: var(--text-primary);
4823
+ margin-bottom: 4px;
4824
+ text-transform: uppercase;
4825
+ font-size: 11px;
4826
+ letter-spacing: 0.5px;
4827
+ }
4742
4828
 
4743
- .pending-item {
4744
- padding: 16px 20px;
4745
- border-bottom: 1px solid rgba(48, 54, 61, 0.5);
4746
- display: flex;
4747
- flex-direction: column;
4748
- gap: 10px;
4749
- }
4829
+ .activity-content {
4830
+ color: var(--text-secondary);
4831
+ font-family: 'JetBrains Mono', monospace;
4832
+ margin-bottom: 4px;
4833
+ word-break: break-all;
4834
+ }
4750
4835
 
4751
- .pending-item-header {
4752
- display: flex;
4753
- align-items: center;
4754
- gap: 8px;
4755
- }
4836
+ .activity-time {
4837
+ font-size: 11px;
4838
+ color: var(--text-secondary);
4839
+ }
4756
4840
 
4757
- .pending-item-op {
4758
- font-family: var(--mono);
4759
- font-size: 12px;
4760
- font-weight: 600;
4761
- color: var(--text-primary);
4762
- flex: 1;
4763
- }
4841
+ .empty-state {
4842
+ display: flex;
4843
+ align-items: center;
4844
+ justify-content: center;
4845
+ height: 100%;
4846
+ color: var(--text-secondary);
4847
+ font-size: 13px;
4848
+ }
4764
4849
 
4765
- .pending-item-tier {
4766
- display: inline-flex;
4767
- align-items: center;
4768
- justify-content: center;
4769
- width: 28px;
4770
- height: 20px;
4771
- font-size: 9px;
4772
- font-weight: 700;
4773
- border-radius: 3px;
4774
- text-transform: uppercase;
4775
- color: white;
4776
- }
4850
+ /* Row 4: Handshake History */
4851
+ .handshake-table {
4852
+ background-color: var(--surface);
4853
+ border: 1px solid var(--border);
4854
+ border-radius: 8px;
4855
+ overflow: hidden;
4856
+ }
4777
4857
 
4778
- .pending-item-tier.tier1 {
4779
- background: var(--red);
4780
- }
4858
+ .table-header {
4859
+ display: grid;
4860
+ grid-template-columns: 2fr 1fr 1fr 1fr 1.5fr 1.5fr;
4861
+ gap: 16px;
4862
+ padding: 16px 20px;
4863
+ border-bottom: 1px solid var(--border);
4864
+ background-color: var(--bg);
4865
+ font-size: 12px;
4866
+ font-weight: 600;
4867
+ color: var(--text-secondary);
4868
+ text-transform: uppercase;
4869
+ letter-spacing: 0.5px;
4870
+ }
4781
4871
 
4782
- .pending-item-tier.tier2 {
4783
- background: var(--amber);
4784
- }
4872
+ .table-rows {
4873
+ max-height: 300px;
4874
+ overflow-y: auto;
4875
+ }
4785
4876
 
4786
- .pending-item-reason {
4787
- font-size: 12px;
4788
- color: var(--text-secondary);
4789
- }
4877
+ .table-row {
4878
+ display: grid;
4879
+ grid-template-columns: 2fr 1fr 1fr 1fr 1.5fr 1.5fr;
4880
+ gap: 16px;
4881
+ padding: 14px 20px;
4882
+ border-bottom: 1px solid var(--border);
4883
+ align-items: center;
4884
+ font-size: 12px;
4885
+ cursor: pointer;
4886
+ transition: background-color 0.2s;
4887
+ }
4790
4888
 
4791
- .pending-item-timer {
4792
- display: flex;
4793
- align-items: center;
4794
- gap: 6px;
4795
- font-size: 11px;
4796
- font-family: var(--mono);
4797
- color: var(--text-secondary);
4798
- }
4889
+ .table-row:hover {
4890
+ background-color: var(--bg);
4891
+ }
4799
4892
 
4800
- .pending-item-timer-bar {
4801
- flex: 1;
4802
- height: 4px;
4803
- background: rgba(48, 54, 61, 0.8);
4804
- border-radius: 2px;
4805
- overflow: hidden;
4806
- }
4893
+ .table-row:last-child {
4894
+ border-bottom: none;
4895
+ }
4807
4896
 
4808
- .pending-item-timer-fill {
4809
- height: 100%;
4810
- background: var(--blue);
4811
- transition: width 0.1s linear;
4812
- }
4897
+ .table-cell {
4898
+ color: var(--text-secondary);
4899
+ font-family: 'JetBrains Mono', monospace;
4900
+ }
4813
4901
 
4814
- .pending-item-timer.urgent .pending-item-timer-fill {
4815
- background: var(--red);
4816
- }
4902
+ .table-cell.strong {
4903
+ color: var(--text-primary);
4904
+ font-weight: 500;
4905
+ }
4817
4906
 
4818
- .pending-item-actions {
4819
- display: flex;
4820
- gap: 8px;
4821
- }
4907
+ .table-empty {
4908
+ padding: 40px 20px;
4909
+ text-align: center;
4910
+ color: var(--text-secondary);
4911
+ font-size: 13px;
4912
+ }
4822
4913
 
4823
- .btn {
4824
- flex: 1;
4825
- padding: 8px 12px;
4826
- border: none;
4827
- border-radius: var(--radius);
4828
- font-size: 12px;
4829
- font-weight: 600;
4830
- cursor: pointer;
4831
- transition: all 0.15s;
4832
- font-family: var(--sans);
4833
- }
4914
+ /* Pending Overlay */
4915
+ .pending-overlay {
4916
+ position: fixed;
4917
+ top: 0;
4918
+ right: -400px;
4919
+ width: 400px;
4920
+ height: 100vh;
4921
+ background-color: var(--surface);
4922
+ border-left: 1px solid var(--border);
4923
+ box-shadow: -2px 0 8px rgba(0, 0, 0, 0.3);
4924
+ z-index: 200;
4925
+ transition: right 0.3s ease;
4926
+ display: flex;
4927
+ flex-direction: column;
4928
+ overflow-y: auto;
4929
+ }
4834
4930
 
4835
- .btn-approve {
4836
- background: var(--green);
4837
- color: var(--bg);
4838
- }
4931
+ .pending-overlay.show {
4932
+ right: 0;
4933
+ }
4839
4934
 
4840
- .btn-approve:hover {
4841
- background: #4ecf5e;
4842
- }
4935
+ .pending-header {
4936
+ padding: 16px 20px;
4937
+ border-bottom: 1px solid var(--border);
4938
+ font-weight: 600;
4939
+ color: var(--text-primary);
4940
+ }
4843
4941
 
4844
- .btn-deny {
4845
- background: var(--red);
4846
- color: white;
4847
- }
4942
+ .pending-items {
4943
+ flex: 1;
4944
+ overflow-y: auto;
4945
+ padding: 16px;
4946
+ }
4848
4947
 
4849
- .btn-deny:hover {
4850
- background: #f9605e;
4851
- }
4948
+ .pending-item {
4949
+ background-color: var(--bg);
4950
+ border: 1px solid var(--border);
4951
+ border-radius: 6px;
4952
+ padding: 16px;
4953
+ margin-bottom: 12px;
4954
+ }
4852
4955
 
4853
- /* \u2500\u2500 Threat Panel (collapsible footer) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
4956
+ .pending-title {
4957
+ font-weight: 600;
4958
+ color: var(--text-primary);
4959
+ margin-bottom: 8px;
4960
+ word-break: break-word;
4961
+ }
4854
4962
 
4855
- .threat-panel {
4856
- position: fixed;
4857
- bottom: 0;
4858
- left: 0;
4859
- right: 0;
4860
- background: var(--surface);
4861
- border-top: 1px solid var(--border);
4862
- max-height: 240px;
4863
- z-index: 500;
4864
- display: flex;
4865
- flex-direction: column;
4866
- transition: max-height 0.3s ease-out;
4867
- }
4963
+ .pending-countdown {
4964
+ font-size: 12px;
4965
+ color: var(--amber);
4966
+ margin-bottom: 12px;
4967
+ font-weight: 500;
4968
+ }
4868
4969
 
4869
- .threat-panel.collapsed {
4870
- max-height: 40px;
4871
- }
4970
+ .pending-actions {
4971
+ display: flex;
4972
+ gap: 8px;
4973
+ }
4872
4974
 
4873
- .threat-header {
4874
- padding: 12px 20px;
4875
- cursor: pointer;
4876
- display: flex;
4877
- align-items: center;
4878
- gap: 8px;
4879
- font-size: 12px;
4880
- font-weight: 600;
4881
- text-transform: uppercase;
4882
- letter-spacing: 0.5px;
4883
- color: var(--text-secondary);
4884
- flex: 0 0 auto;
4885
- }
4975
+ .pending-btn {
4976
+ flex: 1;
4977
+ padding: 8px 12px;
4978
+ border: none;
4979
+ border-radius: 4px;
4980
+ font-size: 12px;
4981
+ font-weight: 600;
4982
+ cursor: pointer;
4983
+ transition: background-color 0.2s;
4984
+ }
4886
4985
 
4887
- .threat-header:hover {
4888
- background: rgba(88, 166, 255, 0.05);
4889
- }
4986
+ .pending-approve {
4987
+ background-color: var(--green);
4988
+ color: var(--bg);
4989
+ }
4890
4990
 
4891
- .threat-icon {
4892
- font-size: 14px;
4893
- }
4991
+ .pending-approve:hover {
4992
+ background-color: #3fa040;
4993
+ }
4894
4994
 
4895
- .threat-content {
4896
- flex: 1;
4897
- overflow-y: auto;
4898
- padding: 0 20px 12px;
4899
- display: flex;
4900
- flex-direction: column;
4901
- gap: 10px;
4902
- }
4995
+ .pending-deny {
4996
+ background-color: var(--red);
4997
+ color: var(--bg);
4998
+ }
4903
4999
 
4904
- .threat-item {
4905
- padding: 8px 10px;
4906
- background: rgba(248, 81, 73, 0.1);
4907
- border-left: 2px solid var(--red);
4908
- border-radius: 4px;
4909
- font-size: 11px;
4910
- color: var(--text-secondary);
4911
- }
5000
+ .pending-deny:hover {
5001
+ background-color: #e03c3c;
5002
+ }
4912
5003
 
4913
- .threat-item-type {
4914
- font-weight: 600;
4915
- color: var(--red);
4916
- font-family: var(--mono);
4917
- }
5004
+ /* Threat Panel */
5005
+ .threat-panel {
5006
+ background-color: var(--surface);
5007
+ border: 1px solid var(--border);
5008
+ border-radius: 8px;
5009
+ margin-top: 20px;
5010
+ overflow: hidden;
5011
+ }
4918
5012
 
4919
- .threat-empty {
4920
- text-align: center;
4921
- padding: 20px 10px;
4922
- color: var(--text-secondary);
4923
- font-size: 12px;
4924
- }
5013
+ .threat-header {
5014
+ padding: 16px 20px;
5015
+ border-bottom: 1px solid var(--border);
5016
+ display: flex;
5017
+ justify-content: space-between;
5018
+ align-items: center;
5019
+ cursor: pointer;
5020
+ user-select: none;
5021
+ }
4925
5022
 
4926
- /* \u2500\u2500 Scrollbars \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
5023
+ .threat-title {
5024
+ font-weight: 600;
5025
+ color: var(--text-primary);
5026
+ }
4927
5027
 
4928
- ::-webkit-scrollbar {
4929
- width: 6px;
4930
- }
5028
+ .threat-toggle {
5029
+ font-size: 10px;
5030
+ color: var(--text-secondary);
5031
+ transition: transform 0.2s;
5032
+ }
4931
5033
 
4932
- ::-webkit-scrollbar-track {
4933
- background: transparent;
4934
- }
5034
+ .threat-panel.collapsed .threat-toggle {
5035
+ transform: rotate(-90deg);
5036
+ }
4935
5037
 
4936
- ::-webkit-scrollbar-thumb {
4937
- background: var(--border);
4938
- border-radius: 3px;
4939
- }
5038
+ .threat-content {
5039
+ padding: 16px 20px;
5040
+ max-height: 300px;
5041
+ overflow-y: auto;
5042
+ }
4940
5043
 
4941
- ::-webkit-scrollbar-thumb:hover {
4942
- background: rgba(88, 166, 255, 0.3);
4943
- }
5044
+ .threat-panel.collapsed .threat-content {
5045
+ display: none;
5046
+ }
4944
5047
 
4945
- /* \u2500\u2500 Responsive \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
5048
+ .threat-alert {
5049
+ background-color: rgba(248, 81, 73, 0.1);
5050
+ border: 1px solid var(--red);
5051
+ border-radius: 4px;
5052
+ padding: 12px;
5053
+ margin-bottom: 8px;
5054
+ font-size: 12px;
5055
+ }
4946
5056
 
4947
- @media (max-width: 1200px) {
4948
- .protection-sidebar {
4949
- display: none;
5057
+ .threat-alert:last-child {
5058
+ margin-bottom: 0;
4950
5059
  }
4951
5060
 
4952
- .activity-feed {
4953
- border-right: none;
5061
+ .threat-type {
5062
+ font-weight: 600;
5063
+ color: var(--red);
5064
+ margin-bottom: 4px;
5065
+ text-transform: uppercase;
5066
+ font-size: 10px;
5067
+ letter-spacing: 0.5px;
4954
5068
  }
4955
- }
4956
5069
 
4957
- @media (max-width: 768px) {
4958
- .status-bar {
4959
- padding: 0 12px;
4960
- gap: 12px;
4961
- height: 48px;
5070
+ .threat-message {
5071
+ color: var(--text-secondary);
4962
5072
  }
4963
5073
 
4964
- .sanctuary-logo {
4965
- font-size: 14px;
5074
+ /* Scrollbar */
5075
+ ::-webkit-scrollbar {
5076
+ width: 8px;
4966
5077
  }
4967
5078
 
4968
- .status-bar-center {
4969
- display: none;
5079
+ ::-webkit-scrollbar-track {
5080
+ background-color: transparent;
4970
5081
  }
4971
5082
 
4972
- .main-container {
4973
- margin-top: 48px;
5083
+ ::-webkit-scrollbar-thumb {
5084
+ background-color: var(--border);
5085
+ border-radius: 4px;
4974
5086
  }
4975
5087
 
4976
- .activity-item {
4977
- padding: 10px 12px;
5088
+ ::-webkit-scrollbar-thumb:hover {
5089
+ background-color: var(--text-secondary);
4978
5090
  }
4979
5091
 
4980
- .pending-overlay.active {
4981
- width: 100%;
5092
+ /* Responsive */
5093
+ @media (max-width: 1400px) {
5094
+ .sovereignty-layers {
5095
+ grid-template-columns: repeat(2, 1fr);
5096
+ }
5097
+
5098
+ .main-panels {
5099
+ grid-template-columns: 1fr;
5100
+ }
5101
+
5102
+ .pending-overlay {
5103
+ width: 100%;
5104
+ right: -100%;
5105
+ }
4982
5106
  }
4983
5107
 
4984
- .threat-panel {
4985
- max-height: 200px;
5108
+ @media (max-width: 768px) {
5109
+ .status-bar {
5110
+ flex-wrap: wrap;
5111
+ height: auto;
5112
+ padding: 12px;
5113
+ gap: 12px;
5114
+ }
5115
+
5116
+ .status-bar-center {
5117
+ order: 3;
5118
+ flex-basis: 100%;
5119
+ }
5120
+
5121
+ .main-content {
5122
+ margin-top: auto;
5123
+ }
5124
+
5125
+ .info-cards {
5126
+ grid-template-columns: 1fr;
5127
+ }
5128
+
5129
+ .table-header,
5130
+ .table-row {
5131
+ grid-template-columns: 1fr;
5132
+ }
4986
5133
  }
4987
- }
4988
- </style>
5134
+ </style>
4989
5135
  </head>
4990
5136
  <body>
4991
-
4992
- <!-- Status Bar (fixed, top) -->
4993
- <div class="status-bar">
4994
- <div class="status-bar-left">
4995
- <div class="sanctuary-logo"><span>\u25C6</span> SANCTUARY</div>
4996
- <div class="version">v${options.serverVersion}</div>
4997
- </div>
4998
- <div class="status-bar-center">
4999
- <div class="sovereignty-badge">
5000
- <div class="sovereignty-score" id="sovereigntyScore">85</div>
5001
- <span>Sovereignty Health</span>
5137
+ <!-- Status Bar -->
5138
+ <div class="status-bar">
5139
+ <div class="status-bar-left">
5140
+ <div class="logo-icon">\u25C6</div>
5141
+ <div class="logo-info">
5142
+ <div class="logo-title">SANCTUARY</div>
5143
+ <div class="logo-version">v${options.serverVersion}</div>
5144
+ </div>
5002
5145
  </div>
5003
- </div>
5004
- <div class="status-bar-right">
5005
- <div class="protections-indicator">
5006
- <span class="count" id="activeProtections">6</span>/6 protections
5146
+
5147
+ <div class="status-bar-center">
5148
+ <div id="sovereignty-badge" class="sovereignty-badge">
5149
+ <span>Sovereignty Health:</span>
5150
+ <span class="sovereignty-score" id="sovereignty-score">\u2014</span>
5151
+ <span>/ 100</span>
5152
+ </div>
5007
5153
  </div>
5008
- <div class="uptime">
5009
- <span id="uptimeText">\u2014</span>
5154
+
5155
+ <div class="status-bar-right">
5156
+ <div class="status-item">
5157
+ <strong id="protections-count">\u2014</strong>
5158
+ <span>Protections</span>
5159
+ </div>
5160
+ <div class="status-item">
5161
+ <strong id="uptime-value">\u2014</strong>
5162
+ <span>Uptime</span>
5163
+ </div>
5164
+ <div class="status-dot" id="connection-status"></div>
5165
+ <div id="pending-item-badge" class="pending-badge" style="display: none;">
5166
+ <span>\u23F3</span>
5167
+ <span id="pending-count">0</span>
5168
+ </div>
5010
5169
  </div>
5011
- <div class="status-dot" id="statusDot"></div>
5012
- <div class="pending-badge hidden" id="pendingBadge">0</div>
5013
5170
  </div>
5014
- </div>
5015
-
5016
- <!-- Main Layout -->
5017
- <div class="main-container">
5018
- <!-- Activity Feed -->
5019
- <div class="activity-feed">
5020
- <div class="feed-header">
5021
- <div class="feed-header-dot"></div>
5022
- Live Activity
5023
- </div>
5024
- <div class="activity-list" id="activityList">
5025
- <div class="activity-empty">
5026
- <div class="activity-empty-icon">\u2192</div>
5027
- <div class="activity-empty-text">Waiting for activity...</div>
5171
+
5172
+ <!-- Main Content -->
5173
+ <div class="main-content">
5174
+ <div class="grid">
5175
+ <!-- Row 1: Sovereignty Layers -->
5176
+ <div class="sovereignty-layers" id="sovereignty-layers">
5177
+ <div class="layer-card" data-layer="l1">
5178
+ <div class="layer-name">Layer 1</div>
5179
+ <div class="layer-title">Cognitive Sovereignty</div>
5180
+ <div class="layer-status"><span>\u25CF</span> <span id="l1-status">\u2014</span></div>
5181
+ <div class="layer-detail" id="l1-detail">Loading...</div>
5182
+ </div>
5183
+ <div class="layer-card" data-layer="l2">
5184
+ <div class="layer-name">Layer 2</div>
5185
+ <div class="layer-title">Operational Isolation</div>
5186
+ <div class="layer-status"><span>\u25CF</span> <span id="l2-status">\u2014</span></div>
5187
+ <div class="layer-detail" id="l2-detail">Loading...</div>
5188
+ </div>
5189
+ <div class="layer-card" data-layer="l3">
5190
+ <div class="layer-name">Layer 3</div>
5191
+ <div class="layer-title">Selective Disclosure</div>
5192
+ <div class="layer-status"><span>\u25CF</span> <span id="l3-status">\u2014</span></div>
5193
+ <div class="layer-detail" id="l3-detail">Loading...</div>
5194
+ </div>
5195
+ <div class="layer-card" data-layer="l4">
5196
+ <div class="layer-name">Layer 4</div>
5197
+ <div class="layer-title">Verifiable Reputation</div>
5198
+ <div class="layer-status"><span>\u25CF</span> <span id="l4-status">\u2014</span></div>
5199
+ <div class="layer-detail" id="l4-detail">Loading...</div>
5200
+ </div>
5201
+ </div>
5202
+
5203
+ <!-- Row 2: Info Cards -->
5204
+ <div class="info-cards">
5205
+ <div class="info-card">
5206
+ <div class="card-header">Identity</div>
5207
+ <div class="card-row">
5208
+ <span class="card-label">Primary</span>
5209
+ <span class="card-value" id="identity-label">\u2014</span>
5210
+ </div>
5211
+ <div class="card-row">
5212
+ <span class="card-label">DID</span>
5213
+ <span class="card-value truncated" id="identity-did" title="">\u2014</span>
5214
+ </div>
5215
+ <div class="card-row">
5216
+ <span class="card-label">Public Key</span>
5217
+ <span class="card-value truncated" id="identity-pubkey" title="">\u2014</span>
5218
+ </div>
5219
+ <div class="card-row">
5220
+ <span class="card-label">Type</span>
5221
+ <span class="identity-badge">Ed25519</span>
5222
+ </div>
5223
+ <div class="card-row">
5224
+ <span class="card-label">Created</span>
5225
+ <span class="card-value" id="identity-created">\u2014</span>
5226
+ </div>
5227
+ <div class="card-row">
5228
+ <span class="card-label">Identities</span>
5229
+ <span class="card-value" id="identity-count">\u2014</span>
5230
+ </div>
5231
+ </div>
5232
+
5233
+ <div class="info-card">
5234
+ <div class="card-header">Handshakes</div>
5235
+ <div class="card-row">
5236
+ <span class="card-label">Total</span>
5237
+ <span class="card-value" id="handshake-count">\u2014</span>
5238
+ </div>
5239
+ <div class="card-row">
5240
+ <span class="card-label">Latest Peer</span>
5241
+ <span class="card-value truncated" id="handshake-latest">\u2014</span>
5242
+ </div>
5243
+ <div class="card-row">
5244
+ <span class="card-label">Trust Tier</span>
5245
+ <span class="trust-tier-badge" id="handshake-tier">Unverified</span>
5246
+ </div>
5247
+ <div class="card-row">
5248
+ <span class="card-label">Timestamp</span>
5249
+ <span class="card-value" id="handshake-time">\u2014</span>
5250
+ </div>
5251
+ </div>
5252
+
5253
+ <div class="info-card">
5254
+ <div class="card-header">Reputation</div>
5255
+ <div class="card-row">
5256
+ <span class="card-label">Weighted Score</span>
5257
+ <span class="card-value" id="reputation-score">\u2014</span>
5258
+ </div>
5259
+ <div class="card-row">
5260
+ <span class="card-label">Attestations</span>
5261
+ <span class="card-value" id="reputation-attestations">\u2014</span>
5262
+ </div>
5263
+ <div class="card-row">
5264
+ <span class="card-label">Verified Sovereign</span>
5265
+ <span class="card-value" id="reputation-verified">\u2014</span>
5266
+ </div>
5267
+ <div class="card-row">
5268
+ <span class="card-label">Verified Degraded</span>
5269
+ <span class="card-value" id="reputation-degraded">\u2014</span>
5270
+ </div>
5271
+ <div class="card-row">
5272
+ <span class="card-label">Unverified</span>
5273
+ <span class="card-value" id="reputation-unverified">\u2014</span>
5274
+ </div>
5275
+ </div>
5276
+ </div>
5277
+
5278
+ <!-- Row 3: SHR & Activity -->
5279
+ <div class="main-panels">
5280
+ <div class="panel">
5281
+ <div class="panel-header">
5282
+ <div class="panel-title">Sovereignty Health Report</div>
5283
+ <button class="panel-action" id="copy-shr-btn">Copy JSON</button>
5284
+ </div>
5285
+ <div class="panel-content">
5286
+ <div class="shr-json" id="shr-viewer">
5287
+ <div class="empty-state">Loading SHR...</div>
5288
+ </div>
5289
+ </div>
5290
+ </div>
5291
+
5292
+ <div class="panel">
5293
+ <div class="panel-header">
5294
+ <div class="panel-title">Activity Feed</div>
5295
+ </div>
5296
+ <div class="panel-content">
5297
+ <div id="activity-feed" class="activity-feed">
5298
+ <div class="empty-state">Waiting for activity...</div>
5299
+ </div>
5300
+ </div>
5301
+ </div>
5302
+ </div>
5303
+
5304
+ <!-- Row 4: Handshake History -->
5305
+ <div class="handshake-table">
5306
+ <div class="table-header">
5307
+ <div>Counterparty</div>
5308
+ <div>Trust Tier</div>
5309
+ <div>Sovereignty</div>
5310
+ <div>Verified</div>
5311
+ <div>Completed</div>
5312
+ <div>Expires</div>
5313
+ </div>
5314
+ <div class="table-rows" id="handshake-table">
5315
+ <div class="table-empty">No handshakes completed yet</div>
5316
+ </div>
5317
+ </div>
5318
+
5319
+ <!-- Threat Panel -->
5320
+ <div class="threat-panel collapsed">
5321
+ <div class="threat-header">
5322
+ <div class="threat-title">Security Threats</div>
5323
+ <div class="threat-toggle">\u25B6</div>
5324
+ </div>
5325
+ <div class="threat-content" id="threat-alerts">
5326
+ <div class="empty-state">No threats detected</div>
5327
+ </div>
5028
5328
  </div>
5029
5329
  </div>
5030
5330
  </div>
5031
5331
 
5032
- <!-- Protection Status Sidebar -->
5033
- <div class="protection-sidebar" id="protectionSidebar">
5034
- <div class="sidebar-header">
5035
- <span>\u25C6</span> Protection Status
5036
- </div>
5037
- <div class="sidebar-content">
5038
- <div class="protection-card">
5039
- <div class="protection-card-icon">\u{1F510}</div>
5040
- <div class="protection-card-label">Encryption</div>
5041
- <div class="protection-card-status active" id="encryptionStatus">\u2713 Active</div>
5042
- <div class="protection-card-stat" id="encryptionStat">Ed25519</div>
5043
- </div>
5332
+ <!-- Pending Overlay -->
5333
+ <div class="pending-overlay" id="pending-overlay">
5334
+ <div class="pending-header">Pending Approvals</div>
5335
+ <div class="pending-items" id="pending-items"></div>
5336
+ </div>
5337
+
5338
+ <script>
5339
+ // Constants
5340
+ const AUTH_TOKEN = '${options.authToken || ""}' || sessionStorage.getItem('authToken') || '';
5341
+ const TIMEOUT_SECONDS = ${options.timeoutSeconds};
5342
+ const API_BASE = '';
5343
+
5344
+ // State
5345
+ let apiState = {
5346
+ sovereignty: null,
5347
+ identity: null,
5348
+ handshakes: [],
5349
+ shr: null,
5350
+ status: null,
5351
+ };
5352
+
5353
+ let pendingRequests = new Map();
5354
+ let activityLog = [];
5355
+ const maxActivityItems = 50;
5356
+
5357
+ // Helpers
5358
+ function esc(text) {
5359
+ if (!text) return '';
5360
+ const div = document.createElement('div');
5361
+ div.textContent = text;
5362
+ return div.innerHTML;
5363
+ }
5364
+
5365
+ function formatTime(isoString) {
5366
+ if (!isoString) return '\u2014';
5367
+ const date = new Date(isoString);
5368
+ return date.toLocaleString('en-US', {
5369
+ month: 'short',
5370
+ day: 'numeric',
5371
+ hour: '2-digit',
5372
+ minute: '2-digit',
5373
+ });
5374
+ }
5375
+
5376
+ function truncate(str, len = 16) {
5377
+ if (!str) return '\u2014';
5378
+ if (str.length <= len) return str;
5379
+ return str.slice(0, len) + '...';
5380
+ }
5381
+
5382
+ function calculateSovereigntyScore(shr) {
5383
+ if (!shr || !shr.layers) return 0;
5384
+ const layers = shr.layers;
5385
+ let score = 100;
5386
+
5387
+ if (layers.l1?.status === 'degraded') score -= 20;
5388
+ if (layers.l1?.status === 'inactive') score -= 35;
5389
+ if (layers.l2?.status === 'degraded') score -= 15;
5390
+ if (layers.l2?.status === 'inactive') score -= 25;
5391
+ if (layers.l3?.status === 'degraded') score -= 15;
5392
+ if (layers.l3?.status === 'inactive') score -= 25;
5393
+ if (layers.l4?.status === 'degraded') score -= 10;
5394
+ if (layers.l4?.status === 'inactive') score -= 20;
5395
+
5396
+ return Math.max(0, Math.min(100, score));
5397
+ }
5398
+
5399
+ async function fetchAPI(endpoint) {
5400
+ try {
5401
+ const response = await fetch(API_BASE + endpoint, {
5402
+ headers: {
5403
+ 'Authorization': 'Bearer ' + AUTH_TOKEN,
5404
+ },
5405
+ });
5406
+
5407
+ if (response.status === 401) {
5408
+ redirectToLogin();
5409
+ return null;
5410
+ }
5411
+
5412
+ if (!response.ok) {
5413
+ console.error('API Error:', response.status);
5414
+ return null;
5415
+ }
5416
+
5417
+ return await response.json();
5418
+ } catch (err) {
5419
+ console.error('Fetch error:', err);
5420
+ return null;
5421
+ }
5422
+ }
5423
+
5424
+ function redirectToLogin() {
5425
+ sessionStorage.removeItem('authToken');
5426
+ window.location.href = '/';
5427
+ }
5428
+
5429
+ // API Updates
5430
+ async function updateSovereignty() {
5431
+ const data = await fetchAPI('/api/sovereignty');
5432
+ if (!data) return;
5044
5433
 
5045
- <div class="protection-card">
5046
- <div class="protection-card-icon">\u2713</div>
5047
- <div class="protection-card-label">Approval Gate</div>
5048
- <div class="protection-card-status active" id="approvalStatus">\u2713 Active</div>
5049
- <div class="protection-card-stat" id="approvalStat">T1: 2 | T2: 3</div>
5050
- </div>
5434
+ apiState.sovereignty = data;
5051
5435
 
5052
- <div class="protection-card">
5053
- <div class="protection-card-icon">\u{1F3AF}</div>
5054
- <div class="protection-card-label">Context Gating</div>
5055
- <div class="protection-card-status active" id="contextStatus">\u2713 Active</div>
5056
- <div class="protection-card-stat" id="contextStat">12 filtered</div>
5057
- </div>
5436
+ const score = calculateSovereigntyScore(data.shr);
5437
+ const badge = document.getElementById('sovereignty-badge');
5438
+ const scoreEl = document.getElementById('sovereignty-score');
5058
5439
 
5059
- <div class="protection-card">
5060
- <div class="protection-card-icon">\u26A0</div>
5061
- <div class="protection-card-label">Injection Detection</div>
5062
- <div class="protection-card-status active" id="injectionStatus">\u2713 Active</div>
5063
- <div class="protection-card-stat" id="injectionStat">3 flags today</div>
5064
- </div>
5440
+ scoreEl.textContent = score;
5065
5441
 
5066
- <div class="protection-card">
5067
- <div class="protection-card-icon">\u{1F4CA}</div>
5068
- <div class="protection-card-label">Behavioral Baseline</div>
5069
- <div class="protection-card-status active" id="baselineStatus">\u2713 Active</div>
5070
- <div class="protection-card-stat" id="baselineStat">0 anomalies</div>
5071
- </div>
5442
+ badge.classList.remove('degraded', 'inactive');
5443
+ if (score < 70) badge.classList.add('degraded');
5444
+ if (score < 40) badge.classList.add('inactive');
5072
5445
 
5073
- <div class="protection-card">
5074
- <div class="protection-card-icon">\u{1F4CB}</div>
5075
- <div class="protection-card-label">Audit Trail</div>
5076
- <div class="protection-card-status active" id="auditStatus">\u2713 Active</div>
5077
- <div class="protection-card-stat" id="auditStat">284 entries</div>
5078
- </div>
5079
- </div>
5080
- </div>
5081
- </div>
5446
+ updateLayerCards(data.shr);
5447
+ }
5082
5448
 
5083
- <!-- Pending Approvals Overlay -->
5084
- <div class="pending-overlay" id="pendingOverlay">
5085
- <div class="pending-overlay-header">
5086
- <div class="pending-overlay-title">Pending Approvals</div>
5087
- <button class="pending-overlay-close" onclick="closePendingOverlay()">\xD7</button>
5088
- </div>
5089
- <div class="pending-list" id="pendingList"></div>
5090
- </div>
5091
-
5092
- <!-- Threat Panel (collapsible footer) -->
5093
- <div class="threat-panel collapsed" id="threatPanel">
5094
- <div class="threat-header" onclick="toggleThreatPanel()">
5095
- <span class="threat-icon">\u26A0</span>
5096
- Recent Threats
5097
- <span id="threatCount" style="margin-left: auto; color: var(--red); font-weight: 700;">0</span>
5098
- </div>
5099
- <div class="threat-content" id="threatContent">
5100
- <div class="threat-empty">No threats detected</div>
5101
- </div>
5102
- </div>
5449
+ function updateLayerCards(shr) {
5450
+ if (!shr || !shr.layers) return;
5103
5451
 
5104
- <script>
5105
- (function() {
5106
- 'use strict';
5452
+ const layers = shr.layers;
5107
5453
 
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
5454
+ updateLayerCard('l1', layers.l1, layers.l1?.encryption || 'AES-256-GCM');
5455
+ updateLayerCard('l2', layers.l2, layers.l2?.isolation_type || 'Process-level');
5456
+ updateLayerCard('l3', layers.l3, layers.l3?.proof_system || 'Schnorr-Pedersen');
5457
+ updateLayerCard('l4', layers.l4, layers.l4?.reputation_mode || 'Weighted');
5458
+ }
5109
5459
 
5110
- const TIMEOUT_SECONDS = ${options.timeoutSeconds};
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; } })();
5114
- const MAX_ACTIVITY_ITEMS = 100;
5115
- const MAX_THREAT_ITEMS = 20;
5460
+ function updateLayerCard(layer, layerData, detail) {
5461
+ if (!layerData) return;
5116
5462
 
5117
- // \u2500\u2500 State \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5463
+ const card = document.querySelector(\`[data-layer="\${layer}"]\`);
5464
+ if (!card) return;
5118
5465
 
5119
- let SESSION_ID = null;
5120
- let evtSource = null;
5121
- let startTime = Date.now();
5122
- let activityCount = 0;
5123
- let threatCount = 0;
5124
- const pendingRequests = new Map();
5125
- const activityItems = [];
5126
- const threatItems = [];
5127
- let sovereigntyScore = 85;
5128
- let sessionRenewalTimer = null;
5466
+ const status = layerData.status || 'inactive';
5467
+ card.classList.remove('degraded', 'inactive');
5129
5468
 
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
5469
+ if (status === 'degraded') {
5470
+ card.classList.add('degraded');
5471
+ } else if (status === 'inactive') {
5472
+ card.classList.add('inactive');
5473
+ }
5131
5474
 
5132
- function authHeaders() {
5133
- const h = { 'Content-Type': 'application/json' };
5134
- if (AUTH_TOKEN) h['Authorization'] = 'Bearer ' + AUTH_TOKEN;
5135
- return h;
5136
- }
5475
+ document.getElementById(\`\${layer}-status\`).textContent = status.toUpperCase();
5476
+ document.getElementById(\`\${layer}-detail\`).textContent = detail;
5477
+ }
5137
5478
 
5138
- function sessionQuery(url) {
5139
- if (!SESSION_ID) return url;
5140
- const sep = url.includes('?') ? '&' : '?';
5141
- return url + sep + 'session=' + SESSION_ID;
5142
- }
5479
+ async function updateIdentity() {
5480
+ const data = await fetchAPI('/api/identity');
5481
+ if (!data) return;
5143
5482
 
5144
- function setCookie(sessionId, maxAge) {
5145
- document.cookie = 'sanctuary_session=' + sessionId +
5146
- '; path=/; SameSite=Strict; max-age=' + maxAge;
5147
- }
5483
+ apiState.identity = data;
5148
5484
 
5149
- async function exchangeSession() {
5150
- if (!AUTH_TOKEN) return;
5151
- try {
5152
- const resp = await fetch('/auth/session', { method: 'POST', headers: authHeaders() });
5153
- if (resp.ok) {
5154
- const data = await resp.json();
5155
- SESSION_ID = data.session_id;
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();
5167
- }
5168
- } catch (e) {
5169
- // Network error \u2014 retry in 30s
5170
- if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
5171
- sessionRenewalTimer = setTimeout(function() {
5172
- exchangeSession().then(function() { reconnectSSE(); });
5173
- }, 30000);
5174
- }
5175
- }
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
-
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
5186
-
5187
- function esc(s) {
5188
- const d = document.createElement('div');
5189
- d.textContent = String(s || '');
5190
- return d.innerHTML;
5191
- }
5192
-
5193
- function closePendingOverlay() {
5194
- document.getElementById('pendingOverlay').classList.remove('active');
5195
- }
5196
-
5197
- function toggleThreatPanel() {
5198
- document.getElementById('threatPanel').classList.toggle('collapsed');
5199
- }
5200
-
5201
- function updateUptime() {
5202
- const elapsed = Math.floor((Date.now() - startTime) / 1000);
5203
- const hours = Math.floor(elapsed / 3600);
5204
- const mins = Math.floor((elapsed % 3600) / 60);
5205
- const secs = elapsed % 60;
5206
- let uptimeStr = '';
5207
- if (hours > 0) uptimeStr += hours + 'h ';
5208
- if (mins > 0) uptimeStr += mins + 'm ';
5209
- uptimeStr += secs + 's';
5210
- document.getElementById('uptimeText').textContent = uptimeStr;
5211
- }
5212
-
5213
- // \u2500\u2500 Sovereignty Score \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5214
-
5215
- function updateSovereigntyScore(score) {
5216
- sovereigntyScore = Math.min(100, Math.max(0, score || 85));
5217
- const badge = document.getElementById('sovereigntyScore');
5218
- badge.textContent = sovereigntyScore;
5219
- badge.className = 'sovereignty-score';
5220
- if (sovereigntyScore >= 80) {
5221
- badge.classList.add('high');
5222
- } else if (sovereigntyScore >= 50) {
5223
- badge.classList.add('medium');
5224
- } else {
5225
- badge.classList.add('low');
5485
+ const primary = data.primary || {};
5486
+ document.getElementById('identity-label').textContent = primary.label || '\u2014';
5487
+ document.getElementById('identity-did').textContent = truncate(primary.did, 24);
5488
+ document.getElementById('identity-did').title = primary.did || '';
5489
+ document.getElementById('identity-pubkey').textContent = truncate(primary.publicKey, 24);
5490
+ document.getElementById('identity-pubkey').title = primary.publicKey || '';
5491
+ document.getElementById('identity-created').textContent = formatTime(primary.createdAt);
5492
+ document.getElementById('identity-count').textContent = data.identities?.length || '\u2014';
5226
5493
  }
5227
- }
5228
5494
 
5229
- // \u2500\u2500 Activity Feed \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5495
+ async function updateHandshakes() {
5496
+ const data = await fetchAPI('/api/handshakes');
5497
+ if (!data) return;
5230
5498
 
5231
- function addActivityItem(data) {
5232
- const {
5233
- timestamp,
5234
- tier,
5235
- tool,
5236
- outcome,
5237
- detail,
5238
- hasInjection,
5239
- isContextGated
5240
- } = data;
5241
-
5242
- const item = {
5243
- id: 'activity-' + activityCount++,
5244
- timestamp: timestamp || new Date().toISOString(),
5245
- tier: tier || 1,
5246
- tool: tool || 'unknown_tool',
5247
- outcome: outcome || 'executed',
5248
- detail: detail || '',
5249
- hasInjection: !!hasInjection,
5250
- isContextGated: !!isContextGated
5251
- };
5499
+ apiState.handshakes = data.handshakes || [];
5252
5500
 
5253
- activityItems.unshift(item);
5254
- if (activityItems.length > MAX_ACTIVITY_ITEMS) {
5255
- activityItems.pop();
5501
+ document.getElementById('handshake-count').textContent = data.handshakes?.length || '0';
5502
+
5503
+ if (data.handshakes && data.handshakes.length > 0) {
5504
+ const latest = data.handshakes[0];
5505
+ document.getElementById('handshake-latest').textContent = truncate(latest.counterpartyId, 20);
5506
+ document.getElementById('handshake-latest').title = latest.counterpartyId || '';
5507
+ document.getElementById('handshake-tier').textContent = (latest.trustTier || 'Unverified').toUpperCase();
5508
+ document.getElementById('handshake-time').textContent = formatTime(latest.completedAt);
5509
+ } else {
5510
+ document.getElementById('handshake-latest').textContent = '\u2014';
5511
+ document.getElementById('handshake-tier').textContent = 'Unverified';
5512
+ document.getElementById('handshake-time').textContent = '\u2014';
5513
+ }
5514
+
5515
+ updateHandshakeTable(data.handshakes || []);
5256
5516
  }
5257
5517
 
5258
- renderActivityFeed();
5259
- }
5518
+ function updateHandshakeTable(handshakes) {
5519
+ const table = document.getElementById('handshake-table');
5260
5520
 
5261
- function renderActivityFeed() {
5262
- const list = document.getElementById('activityList');
5521
+ if (!handshakes || handshakes.length === 0) {
5522
+ table.innerHTML = '<div class="table-empty">No handshakes completed yet</div>';
5523
+ return;
5524
+ }
5263
5525
 
5264
- if (activityItems.length === 0) {
5265
- list.innerHTML = '<div class="activity-empty"><div class="activity-empty-icon">\u2192</div><div class="activity-empty-text">Waiting for activity...</div></div>';
5266
- return;
5526
+ table.innerHTML = handshakes
5527
+ .map(
5528
+ (hs) => \`
5529
+ <div class="table-row">
5530
+ <div class="table-cell strong">\${esc(truncate(hs.counterpartyId, 24))}</div>
5531
+ <div class="table-cell">\${esc(hs.trustTier || 'Unverified')}</div>
5532
+ <div class="table-cell">\${esc(hs.sovereigntyLevel || '\u2014')}</div>
5533
+ <div class="table-cell">\${hs.verified ? 'Yes' : 'No'}</div>
5534
+ <div class="table-cell">\${formatTime(hs.completedAt)}</div>
5535
+ <div class="table-cell">\${formatTime(hs.expiresAt)}</div>
5536
+ </div>
5537
+ \`
5538
+ )
5539
+ .join('');
5267
5540
  }
5268
5541
 
5269
- list.innerHTML = '';
5270
- for (const item of activityItems) {
5271
- const tr = document.createElement('div');
5272
- tr.className = 'activity-item';
5273
- tr.id = item.id;
5274
-
5275
- const time = new Date(item.timestamp);
5276
- const timeStr = time.toLocaleTimeString();
5277
-
5278
- const tierClass = 't' + item.tier;
5279
- const outcomeClass = item.outcome === 'denied' ? 'outcome denied' : 'outcome';
5280
-
5281
- let icon = '\u25CF';
5282
- if (item.isContextGated) icon = '\u{1F3AF}';
5283
- else if (item.hasInjection) icon = '\u26A0';
5284
- else if (item.outcome === 'denied') icon = '\u2717';
5285
- else icon = '\u2713';
5286
-
5287
- tr.innerHTML =
5288
- '<div class="activity-item-icon">' + esc(icon) + '</div>' +
5289
- '<div class="activity-item-content">' +
5290
- '<div class="activity-time">' + esc(timeStr) + '</div>' +
5291
- '<div class="activity-main">' +
5292
- '<span class="activity-tier ' + tierClass + '">T' + item.tier + '</span>' +
5293
- '<span class="activity-tool">' + esc(item.tool) + '</span>' +
5294
- '<span class="activity-outcome ' + (outcomeClass === 'outcome denied' ? 'denied' : '') + '">' + (item.outcome === 'denied' ? '\u2717 denied' : '\u2713 allowed') + '</span>' +
5295
- '</div>' +
5296
- '<div class="activity-detail">' + esc(item.detail) + '</div>' +
5297
- '</div>' +
5298
- '';
5299
-
5300
- tr.addEventListener('click', () => {
5301
- tr.classList.toggle('expanded');
5302
- });
5542
+ async function updateSHR() {
5543
+ const data = await fetchAPI('/api/shr');
5544
+ if (!data) return;
5303
5545
 
5304
- list.appendChild(tr);
5546
+ apiState.shr = data;
5547
+ renderSHRViewer(data);
5305
5548
  }
5306
- }
5307
5549
 
5308
- // \u2500\u2500 Pending Approvals \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5550
+ function renderSHRViewer(shr) {
5551
+ const viewer = document.getElementById('shr-viewer');
5309
5552
 
5310
- function addPendingRequest(data) {
5311
- const {
5312
- request_id,
5313
- operation,
5314
- tier,
5315
- reason,
5316
- context,
5317
- timestamp
5318
- } = data;
5319
-
5320
- const pending = {
5321
- id: request_id,
5322
- operation: operation || 'unknown',
5323
- tier: tier || 1,
5324
- reason: reason || '',
5325
- context: context || {},
5326
- timestamp: timestamp || new Date().toISOString(),
5327
- remaining: TIMEOUT_SECONDS
5328
- };
5553
+ if (!shr) {
5554
+ viewer.innerHTML = '<div class="empty-state">No SHR available</div>';
5555
+ return;
5556
+ }
5329
5557
 
5330
- pendingRequests.set(request_id, pending);
5331
- updatePendingUI();
5332
- }
5558
+ let html = '';
5559
+
5560
+ // Implementation
5561
+ html += \`
5562
+ <div class="shr-section">
5563
+ <div class="shr-section-header">
5564
+ <div class="shr-toggle">\u25BC</div>
5565
+ <div>Implementation</div>
5566
+ </div>
5567
+ <div class="shr-section-content">
5568
+ <div class="shr-item">
5569
+ <div class="shr-key">sanctuary_version:</div>
5570
+ <div class="shr-value">\${esc(shr.implementation?.sanctuary_version || '\u2014')}</div>
5571
+ </div>
5572
+ <div class="shr-item">
5573
+ <div class="shr-key">node_version:</div>
5574
+ <div class="shr-value">\${esc(shr.implementation?.node_version || '\u2014')}</div>
5575
+ </div>
5576
+ <div class="shr-item">
5577
+ <div class="shr-key">generated_by:</div>
5578
+ <div class="shr-value">\${esc(shr.implementation?.generated_by || '\u2014')}</div>
5579
+ </div>
5580
+ </div>
5581
+ </div>
5582
+ \`;
5583
+
5584
+ // Metadata
5585
+ html += \`
5586
+ <div class="shr-section">
5587
+ <div class="shr-section-header">
5588
+ <div class="shr-toggle">\u25BC</div>
5589
+ <div>Metadata</div>
5590
+ </div>
5591
+ <div class="shr-section-content">
5592
+ <div class="shr-item">
5593
+ <div class="shr-key">instance_id:</div>
5594
+ <div class="shr-value">\${esc(truncate(shr.instance_id, 20))}</div>
5595
+ </div>
5596
+ <div class="shr-item">
5597
+ <div class="shr-key">generated_at:</div>
5598
+ <div class="shr-value">\${formatTime(shr.generated_at)}</div>
5599
+ </div>
5600
+ <div class="shr-item">
5601
+ <div class="shr-key">expires_at:</div>
5602
+ <div class="shr-value">\${formatTime(shr.expires_at)}</div>
5603
+ </div>
5604
+ </div>
5605
+ </div>
5606
+ \`;
5607
+
5608
+ // Layers
5609
+ if (shr.layers) {
5610
+ html += \`<div class="shr-section">
5611
+ <div class="shr-section-header">
5612
+ <div class="shr-toggle">\u25BC</div>
5613
+ <div>Layers</div>
5614
+ </div>
5615
+ <div class="shr-section-content">
5616
+ \`;
5617
+
5618
+ for (const [key, layer] of Object.entries(shr.layers)) {
5619
+ html += \`
5620
+ <div style="margin-bottom: 12px;">
5621
+ <div style="color: var(--blue); font-weight: 600; margin-bottom: 4px;">\${esc(key)}</div>
5622
+ <div style="padding-left: 12px;">
5623
+ \`;
5624
+
5625
+ for (const [lkey, lvalue] of Object.entries(layer || {})) {
5626
+ const displayValue =
5627
+ typeof lvalue === 'boolean'
5628
+ ? lvalue
5629
+ ? 'true'
5630
+ : 'false'
5631
+ : esc(String(lvalue));
5632
+ html += \`
5633
+ <div class="shr-item">
5634
+ <div class="shr-key">\${esc(lkey)}:</div>
5635
+ <div class="shr-value">\${displayValue}</div>
5636
+ </div>
5637
+ \`;
5638
+ }
5333
5639
 
5334
- function removePendingRequest(id) {
5335
- pendingRequests.delete(id);
5336
- updatePendingUI();
5337
- }
5640
+ html += \`
5641
+ </div>
5642
+ </div>
5643
+ \`;
5644
+ }
5338
5645
 
5339
- function updatePendingUI() {
5340
- const count = pendingRequests.size;
5341
- const badge = document.getElementById('pendingBadge');
5646
+ html += \`
5647
+ </div>
5648
+ </div>
5649
+ \`;
5650
+ }
5651
+
5652
+ // Capabilities
5653
+ if (shr.capabilities) {
5654
+ html += \`
5655
+ <div class="shr-section">
5656
+ <div class="shr-section-header">
5657
+ <div class="shr-toggle">\u25BC</div>
5658
+ <div>Capabilities</div>
5659
+ </div>
5660
+ <div class="shr-section-content">
5661
+ \`;
5662
+
5663
+ for (const [key, value] of Object.entries(shr.capabilities)) {
5664
+ const displayValue = value ? 'true' : 'false';
5665
+ html += \`
5666
+ <div class="shr-item">
5667
+ <div class="shr-key">\${esc(key)}:</div>
5668
+ <div class="shr-value">\${displayValue}</div>
5669
+ </div>
5670
+ \`;
5671
+ }
5342
5672
 
5343
- if (count > 0) {
5344
- badge.classList.remove('hidden');
5345
- badge.textContent = count;
5346
- document.getElementById('pendingOverlay').classList.add('active');
5347
- } else {
5348
- badge.classList.add('hidden');
5349
- document.getElementById('pendingOverlay').classList.remove('active');
5673
+ html += \`
5674
+ </div>
5675
+ </div>
5676
+ \`;
5677
+ }
5678
+
5679
+ // Signature
5680
+ html += \`
5681
+ <div class="shr-section">
5682
+ <div class="shr-section-header">
5683
+ <div class="shr-toggle">\u25BC</div>
5684
+ <div>Signature</div>
5685
+ </div>
5686
+ <div class="shr-section-content">
5687
+ <div class="shr-item">
5688
+ <div class="shr-key">signed_by:</div>
5689
+ <div class="shr-value">\${esc(truncate(shr.signed_by, 20))}</div>
5690
+ </div>
5691
+ <div class="shr-item">
5692
+ <div class="shr-key">signature:</div>
5693
+ <div class="shr-value">\${esc(truncate(shr.signature, 32))}</div>
5694
+ </div>
5695
+ </div>
5696
+ </div>
5697
+ \`;
5698
+
5699
+ viewer.innerHTML = html;
5700
+
5701
+ // Add collapse functionality
5702
+ document.querySelectorAll('.shr-section-header').forEach((header) => {
5703
+ header.addEventListener('click', () => {
5704
+ header.closest('.shr-section').classList.toggle('collapsed');
5705
+ });
5706
+ });
5350
5707
  }
5351
5708
 
5352
- renderPendingList();
5353
- }
5354
-
5355
- function renderPendingList() {
5356
- const list = document.getElementById('pendingList');
5357
- list.innerHTML = '';
5709
+ async function updateStatus() {
5710
+ const data = await fetchAPI('/api/status');
5711
+ if (!data) return;
5358
5712
 
5359
- for (const [id, req] of pendingRequests) {
5360
- const item = document.createElement('div');
5361
- item.className = 'pending-item';
5713
+ apiState.status = data;
5362
5714
 
5363
- const tier = req.tier || 1;
5364
- const tierClass = 'tier' + tier;
5365
- const pct = Math.max(0, Math.min(100, (req.remaining / TIMEOUT_SECONDS) * 100));
5366
- const isUrgent = req.remaining <= 30;
5715
+ document.getElementById('protections-count').textContent = data.protectionsCount || '0';
5716
+ document.getElementById('uptime-value').textContent = formatUptime(data.uptime);
5367
5717
 
5368
- item.innerHTML =
5369
- '<div class="pending-item-header">' +
5370
- '<div class="pending-item-op">' + esc(req.operation) + '</div>' +
5371
- '<div class="pending-item-tier ' + tierClass + '">T' + tier + '</div>' +
5372
- '</div>' +
5373
- '<div class="pending-item-reason">' + esc(req.reason) + '</div>' +
5374
- '<div class="pending-item-timer ' + (isUrgent ? 'urgent' : '') + '">' +
5375
- '<div class="pending-item-timer-bar">' +
5376
- '<div class="pending-item-timer-fill" style="width: ' + pct + '%"></div>' +
5377
- '</div>' +
5378
- '<span id="timer-' + id + '">' + req.remaining + 's</span>' +
5379
- '</div>' +
5380
- '<div class="pending-item-actions">' +
5381
- '<button class="btn btn-approve" onclick="handleApprove('' + id + '')">Approve</button>' +
5382
- '<button class="btn btn-deny" onclick="handleDeny('' + id + '')">Deny</button>' +
5383
- '</div>' +
5384
- '';
5718
+ const connectionStatus = document.getElementById('connection-status');
5719
+ connectionStatus.classList.toggle('disconnected', !data.connected);
5720
+ }
5385
5721
 
5386
- list.appendChild(item);
5722
+ function formatUptime(seconds) {
5723
+ if (!seconds) return '\u2014';
5724
+ const hours = Math.floor(seconds / 3600);
5725
+ const minutes = Math.floor((seconds % 3600) / 60);
5726
+ if (hours > 0) return \`\${hours}h \${minutes}m\`;
5727
+ return \`\${minutes}m\`;
5387
5728
  }
5388
- }
5389
5729
 
5390
- window.handleApprove = function(id) {
5391
- fetch('/api/approve/' + id, { method: 'POST', headers: authHeaders() }).then(() => {
5392
- removePendingRequest(id);
5393
- }).catch(() => {});
5394
- };
5730
+ // SSE Setup
5731
+ function setupSSE() {
5732
+ const eventSource = new EventSource(API_BASE + '/api/events', {
5733
+ headers: {
5734
+ 'Authorization': 'Bearer ' + AUTH_TOKEN,
5735
+ },
5736
+ });
5395
5737
 
5396
- window.handleDeny = function(id) {
5397
- fetch('/api/deny/' + id, { method: 'POST', headers: authHeaders() }).then(() => {
5398
- removePendingRequest(id);
5399
- }).catch(() => {});
5400
- };
5738
+ eventSource.addEventListener('init', (e) => {
5739
+ console.log('Connected to SSE');
5740
+ });
5401
5741
 
5402
- // \u2500\u2500 Threats \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5742
+ eventSource.addEventListener('sovereignty-update', () => {
5743
+ updateSovereignty();
5744
+ });
5403
5745
 
5404
- function addThreat(data) {
5405
- const {
5406
- timestamp,
5407
- severity,
5408
- type,
5409
- details
5410
- } = data;
5411
-
5412
- const threat = {
5413
- id: 'threat-' + threatCount++,
5414
- timestamp: timestamp || new Date().toISOString(),
5415
- severity: severity || 'medium',
5416
- type: type || 'unknown',
5417
- details: details || ''
5418
- };
5746
+ eventSource.addEventListener('handshake-update', () => {
5747
+ updateHandshakes();
5748
+ });
5419
5749
 
5420
- threatItems.unshift(threat);
5421
- if (threatItems.length > MAX_THREAT_ITEMS) {
5422
- threatItems.pop();
5423
- }
5750
+ eventSource.addEventListener('tool-call', (e) => {
5751
+ const data = JSON.parse(e.data);
5752
+ addActivityItem({
5753
+ type: 'tool-call',
5754
+ title: 'Tool Call',
5755
+ content: data.toolName,
5756
+ timestamp: new Date().toISOString(),
5757
+ });
5758
+ });
5424
5759
 
5425
- if (threatCount > 0) {
5426
- document.getElementById('threatPanel').classList.remove('collapsed');
5427
- }
5760
+ eventSource.addEventListener('context-gate-decision', (e) => {
5761
+ const data = JSON.parse(e.data);
5762
+ addActivityItem({
5763
+ type: 'context-gate',
5764
+ title: 'Context Gate',
5765
+ content: data.decision,
5766
+ timestamp: new Date().toISOString(),
5767
+ });
5768
+ });
5428
5769
 
5429
- renderThreats();
5430
- }
5770
+ eventSource.addEventListener('injection-alert', (e) => {
5771
+ const data = JSON.parse(e.data);
5772
+ addActivityItem({
5773
+ type: 'injection',
5774
+ title: 'Injection Alert',
5775
+ content: data.pattern,
5776
+ timestamp: new Date().toISOString(),
5777
+ });
5778
+ addThreatAlert(data);
5779
+ });
5780
+
5781
+ eventSource.addEventListener('pending-request', (e) => {
5782
+ const data = JSON.parse(e.data);
5783
+ addPendingRequest(data);
5784
+ });
5431
5785
 
5432
- function renderThreats() {
5433
- const content = document.getElementById('threatContent');
5434
- const badge = document.getElementById('threatCount');
5786
+ eventSource.addEventListener('request-resolved', (e) => {
5787
+ const data = JSON.parse(e.data);
5788
+ removePendingRequest(data.requestId);
5789
+ });
5435
5790
 
5436
- if (threatItems.length === 0) {
5437
- content.innerHTML = '<div class="threat-empty">No threats detected</div>';
5438
- badge.textContent = '0';
5439
- return;
5791
+ eventSource.onerror = () => {
5792
+ console.error('SSE error');
5793
+ setTimeout(setupSSE, 5000);
5794
+ };
5440
5795
  }
5441
5796
 
5442
- badge.textContent = threatItems.length;
5443
- content.innerHTML = '';
5797
+ // Activity Feed
5798
+ function addActivityItem(item) {
5799
+ activityLog.unshift(item);
5800
+ if (activityLog.length > maxActivityItems) {
5801
+ activityLog.pop();
5802
+ }
5444
5803
 
5445
- for (const threat of threatItems) {
5446
- const div = document.createElement('div');
5447
- div.className = 'threat-item';
5448
- const time = new Date(threat.timestamp).toLocaleTimeString();
5449
- div.innerHTML =
5450
- '<div style="margin-bottom: 3px;">' +
5451
- '<span class="threat-item-type">' + esc(threat.type) + '</span>' +
5452
- '<span style="font-size: 10px; color: var(--text-secondary); margin-left: 6px;">' + esc(time) + '</span>' +
5453
- '</div>' +
5454
- '<div>' + esc(threat.details) + '</div>' +
5455
- '';
5456
- content.appendChild(div);
5457
- }
5458
- }
5804
+ const feed = document.getElementById('activity-feed');
5805
+ const html = \`
5806
+ <div class="activity-item \${item.type}">
5807
+ <div class="activity-type">\${esc(item.title)}</div>
5808
+ <div class="activity-content">\${esc(item.content)}</div>
5809
+ <div class="activity-time">\${formatTime(item.timestamp)}</div>
5810
+ </div>
5811
+ \`;
5459
5812
 
5460
- // \u2500\u2500 SSE Connection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5813
+ if (feed.querySelector('.empty-state')) {
5814
+ feed.innerHTML = '';
5815
+ }
5461
5816
 
5462
- function reconnectSSE() {
5463
- if (evtSource) evtSource.close();
5464
- connect();
5465
- }
5817
+ feed.insertAdjacentHTML('afterbegin', html);
5466
5818
 
5467
- function connect() {
5468
- evtSource = new EventSource(sessionQuery('/events'));
5819
+ if (feed.children.length > maxActivityItems) {
5820
+ feed.lastChild.remove();
5821
+ }
5822
+ }
5469
5823
 
5470
- evtSource.onopen = () => {
5471
- document.getElementById('statusDot').classList.remove('disconnected');
5472
- };
5824
+ // Pending Requests
5825
+ function addPendingRequest(request) {
5826
+ pendingRequests.set(request.requestId, {
5827
+ id: request.requestId,
5828
+ title: request.title,
5829
+ details: request.details,
5830
+ expiresAt: new Date(Date.now() + TIMEOUT_SECONDS * 1000),
5831
+ });
5473
5832
 
5474
- evtSource.onerror = () => {
5475
- document.getElementById('statusDot').classList.add('disconnected');
5476
- };
5833
+ updatePendingDisplay();
5834
+ }
5477
5835
 
5478
- evtSource.addEventListener('init', (e) => {
5479
- const data = JSON.parse(e.data);
5480
- if (data.baseline) {
5481
- updateBaseline(data.baseline);
5482
- }
5483
- if (data.policy) {
5484
- updatePolicy(data.policy);
5836
+ function removePendingRequest(requestId) {
5837
+ pendingRequests.delete(requestId);
5838
+ updatePendingDisplay();
5839
+ }
5840
+
5841
+ function updatePendingDisplay() {
5842
+ const badge = document.getElementById('pending-item-badge');
5843
+ const count = pendingRequests.size;
5844
+
5845
+ if (count > 0) {
5846
+ document.getElementById('pending-count').textContent = count;
5847
+ badge.style.display = 'flex';
5848
+ } else {
5849
+ badge.style.display = 'none';
5485
5850
  }
5486
- if (data.pending) {
5487
- data.pending.forEach(addPendingRequest);
5851
+
5852
+ const overlay = document.getElementById('pending-overlay');
5853
+ const items = document.getElementById('pending-items');
5854
+
5855
+ if (count === 0) {
5856
+ items.innerHTML = '';
5857
+ overlay.classList.remove('show');
5858
+ return;
5488
5859
  }
5489
- });
5490
5860
 
5491
- evtSource.addEventListener('pending-request', (e) => {
5492
- const data = JSON.parse(e.data);
5493
- addPendingRequest(data);
5494
- });
5861
+ let html = '';
5862
+ for (const req of pendingRequests.values()) {
5863
+ const remaining = Math.max(0, Math.floor((req.expiresAt - Date.now()) / 1000));
5864
+ html += \`
5865
+ <div class="pending-item">
5866
+ <div class="pending-title">\${esc(req.title)}</div>
5867
+ <div class="pending-countdown">Expires in \${remaining}s</div>
5868
+ <div class="pending-actions">
5869
+ <button class="pending-btn pending-approve" data-id="\${req.id}">Approve</button>
5870
+ <button class="pending-btn pending-deny" data-id="\${req.id}">Deny</button>
5871
+ </div>
5872
+ </div>
5873
+ \`;
5874
+ }
5495
5875
 
5496
- evtSource.addEventListener('request-resolved', (e) => {
5497
- const data = JSON.parse(e.data);
5498
- removePendingRequest(data.request_id);
5499
- });
5876
+ items.innerHTML = html;
5500
5877
 
5501
- evtSource.addEventListener('tool-call', (e) => {
5502
- const data = JSON.parse(e.data);
5503
- addActivityItem({
5504
- timestamp: data.timestamp,
5505
- tier: data.tier || 1,
5506
- tool: data.tool || 'unknown',
5507
- outcome: data.outcome || 'executed',
5508
- detail: data.detail || ''
5878
+ document.querySelectorAll('.pending-approve').forEach((btn) => {
5879
+ btn.addEventListener('click', async () => {
5880
+ const id = btn.getAttribute('data-id');
5881
+ await fetchAPI(\`/api/approve/\${id}\`);
5882
+ });
5509
5883
  });
5510
- });
5511
5884
 
5512
- evtSource.addEventListener('context-gate-decision', (e) => {
5513
- const data = JSON.parse(e.data);
5514
- addActivityItem({
5515
- timestamp: data.timestamp,
5516
- tier: data.tier || 1,
5517
- tool: data.tool || 'unknown',
5518
- outcome: data.outcome || 'gated',
5519
- detail: data.fields_filtered ? 'Filtered ' + data.fields_filtered + ' fields' : data.reason || '',
5520
- isContextGated: true
5885
+ document.querySelectorAll('.pending-deny').forEach((btn) => {
5886
+ btn.addEventListener('click', async () => {
5887
+ const id = btn.getAttribute('data-id');
5888
+ await fetchAPI(\`/api/deny/\${id}\`);
5889
+ });
5521
5890
  });
5522
- });
5891
+ }
5523
5892
 
5524
- evtSource.addEventListener('injection-alert', (e) => {
5525
- const data = JSON.parse(e.data);
5526
- addActivityItem({
5527
- timestamp: data.timestamp,
5528
- tier: data.tier || 2,
5529
- tool: data.tool || 'unknown',
5530
- outcome: data.allowed ? 'allowed' : 'denied',
5531
- detail: data.signal || 'Injection detected',
5532
- hasInjection: true
5533
- });
5534
- addThreat({
5535
- timestamp: data.timestamp,
5536
- severity: data.severity || 'medium',
5537
- type: 'Injection Alert',
5538
- details: data.signal || 'Suspicious pattern detected'
5539
- });
5540
- });
5893
+ // Threat Panel
5894
+ function addThreatAlert(alert) {
5895
+ const panel = document.querySelector('.threat-panel');
5896
+ const content = document.getElementById('threat-alerts');
5541
5897
 
5542
- evtSource.addEventListener('protection-status', (e) => {
5543
- const data = JSON.parse(e.data);
5544
- updateProtectionStatus(data);
5545
- });
5898
+ if (content.querySelector('.empty-state')) {
5899
+ content.innerHTML = '';
5900
+ }
5546
5901
 
5547
- evtSource.addEventListener('audit-entry', (e) => {
5548
- const data = JSON.parse(e.data);
5549
- // Audit entries don't show in activity by default, but we could add them
5550
- });
5902
+ panel.classList.remove('collapsed');
5551
5903
 
5552
- evtSource.addEventListener('baseline-update', (e) => {
5553
- const data = JSON.parse(e.data);
5554
- updateBaseline(data);
5555
- });
5556
- }
5904
+ const html = \`
5905
+ <div class="threat-alert">
5906
+ <div class="threat-type">\${esc(alert.type || 'Injection Alert')}</div>
5907
+ <div class="threat-message">\${esc(alert.message || alert.pattern || '\u2014')}</div>
5908
+ </div>
5909
+ \`;
5557
5910
 
5558
- function updateBaseline(baseline) {
5559
- if (!baseline) return;
5560
- // Update baseline-derived stats if needed
5561
- }
5911
+ content.insertAdjacentHTML('afterbegin', html);
5562
5912
 
5563
- function updatePolicy(policy) {
5564
- if (!policy) return;
5565
- // Update policy-derived stats
5566
- if (policy.approval_channel) {
5567
- // Policy info updated
5913
+ const alerts = content.querySelectorAll('.threat-alert');
5914
+ if (alerts.length > 10) {
5915
+ alerts[alerts.length - 1].remove();
5916
+ }
5568
5917
  }
5569
- }
5570
5918
 
5571
- function updateProtectionStatus(status) {
5572
- if (status.sovereignty_score !== undefined) {
5573
- updateSovereigntyScore(status.sovereignty_score);
5574
- }
5575
- if (status.active_protections !== undefined) {
5576
- document.getElementById('activeProtections').textContent = status.active_protections;
5577
- }
5578
- // Update individual protection cards
5579
- if (status.encryption !== undefined) {
5580
- const el = document.getElementById('encryptionStatus');
5581
- el.className = 'protection-card-status ' + (status.encryption ? 'active' : 'inactive');
5582
- el.textContent = status.encryption ? '\u2713 Active' : '\u2717 Inactive';
5583
- }
5584
- if (status.approval_gate !== undefined) {
5585
- const el = document.getElementById('approvalStatus');
5586
- el.className = 'protection-card-status ' + (status.approval_gate ? 'active' : 'inactive');
5587
- el.textContent = status.approval_gate ? '\u2713 Active' : '\u2717 Inactive';
5588
- }
5589
- if (status.context_gating !== undefined) {
5590
- const el = document.getElementById('contextStatus');
5591
- el.className = 'protection-card-status ' + (status.context_gating ? 'active' : 'inactive');
5592
- el.textContent = status.context_gating ? '\u2713 Active' : '\u2717 Inactive';
5593
- }
5594
- if (status.injection_detection !== undefined) {
5595
- const el = document.getElementById('injectionStatus');
5596
- el.className = 'protection-card-status ' + (status.injection_detection ? 'active' : 'inactive');
5597
- el.textContent = status.injection_detection ? '\u2713 Active' : '\u2717 Inactive';
5598
- }
5599
- if (status.baseline !== undefined) {
5600
- const el = document.getElementById('baselineStatus');
5601
- el.className = 'protection-card-status ' + (status.baseline ? 'active' : 'inactive');
5602
- el.textContent = status.baseline ? '\u2713 Active' : '\u2717 Inactive';
5603
- }
5604
- if (status.audit_trail !== undefined) {
5605
- const el = document.getElementById('auditStatus');
5606
- el.className = 'protection-card-status ' + (status.audit_trail ? 'active' : 'inactive');
5607
- el.textContent = status.audit_trail ? '\u2713 Active' : '\u2717 Inactive';
5608
- }
5609
- }
5919
+ // Threat Panel Toggle
5920
+ document.querySelector('.threat-header').addEventListener('click', () => {
5921
+ document.querySelector('.threat-panel').classList.toggle('collapsed');
5922
+ });
5610
5923
 
5611
- // \u2500\u2500 Initialization \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5924
+ // SHR Copy Button
5925
+ document.getElementById('copy-shr-btn').addEventListener('click', async () => {
5926
+ if (!apiState.shr) return;
5612
5927
 
5613
- (async function init() {
5614
- await exchangeSession();
5615
- // Clean legacy ?token= from URL
5616
- if (window.location.search.includes('token=')) {
5617
- window.history.replaceState({}, '', window.location.pathname);
5618
- }
5619
- connect();
5928
+ const json = JSON.stringify(apiState.shr, null, 2);
5929
+ try {
5930
+ await navigator.clipboard.writeText(json);
5931
+ const btn = document.getElementById('copy-shr-btn');
5932
+ const original = btn.textContent;
5933
+ btn.textContent = 'Copied!';
5934
+ setTimeout(() => {
5935
+ btn.textContent = original;
5936
+ }, 2000);
5937
+ } catch (err) {
5938
+ console.error('Copy failed:', err);
5939
+ }
5940
+ });
5620
5941
 
5621
- // Start uptime ticker
5622
- setInterval(updateUptime, 1000);
5623
- updateUptime();
5942
+ // Pending Overlay Toggle
5943
+ document.getElementById('pending-item-badge').addEventListener('click', () => {
5944
+ document.getElementById('pending-overlay').classList.toggle('show');
5945
+ });
5624
5946
 
5625
- // Pending request countdown timer
5626
- setInterval(() => {
5627
- for (const [id, req] of pendingRequests) {
5628
- req.remaining = Math.max(0, req.remaining - 1);
5629
- const el = document.getElementById('timer-' + id);
5630
- if (el) {
5631
- el.textContent = req.remaining + 's';
5632
- }
5947
+ // Initialize
5948
+ async function initialize() {
5949
+ if (!AUTH_TOKEN) {
5950
+ redirectToLogin();
5951
+ return;
5633
5952
  }
5634
- }, 1000);
5635
5953
 
5636
- // Load initial status
5637
- try {
5638
- const resp = await fetch('/api/status', { headers: authHeaders() });
5639
- if (resp.ok) {
5640
- const status = await resp.json();
5641
- if (status.baseline) updateBaseline(status.baseline);
5642
- if (status.policy) updatePolicy(status.policy);
5643
- }
5644
- } catch (e) {
5645
- // Ignore
5646
- }
5647
- })();
5954
+ // Initial data fetch
5955
+ await Promise.all([
5956
+ updateSovereignty(),
5957
+ updateIdentity(),
5958
+ updateHandshakes(),
5959
+ updateSHR(),
5960
+ updateStatus(),
5961
+ ]);
5648
5962
 
5649
- })();
5650
- </script>
5963
+ // Setup SSE for real-time updates
5964
+ setupSSE();
5651
5965
 
5966
+ // Refresh status periodically
5967
+ setInterval(updateStatus, 30000);
5968
+ }
5969
+
5970
+ // Start
5971
+ initialize();
5972
+ </script>
5652
5973
  </body>
5653
5974
  </html>`;
5654
5975
  }
@@ -7431,15 +7752,6 @@ function generateSHR(identityId, opts) {
7431
7752
  mitigation: "TEE attestation planned for a future release"
7432
7753
  });
7433
7754
  }
7434
- if (config.disclosure.proof_system === "commitment-only") {
7435
- degradations.push({
7436
- layer: "l3",
7437
- code: "COMMITMENT_ONLY",
7438
- severity: "info",
7439
- description: "Commitment schemes only (no ZK proofs)",
7440
- mitigation: "ZK proof support planned for future release"
7441
- });
7442
- }
7443
7755
  const body = {
7444
7756
  shr_version: "1.0",
7445
7757
  implementation: {
@@ -7465,9 +7777,9 @@ function generateSHR(identityId, opts) {
7465
7777
  attestation_available: config.execution.attestation
7466
7778
  },
7467
7779
  l3: {
7468
- status: config.disclosure.proof_system === "commitment-only" ? "degraded" : "active",
7780
+ status: "active",
7469
7781
  proof_system: config.disclosure.proof_system,
7470
- selective_disclosure: config.disclosure.proof_system !== "commitment-only"
7782
+ selective_disclosure: true
7471
7783
  },
7472
7784
  l4: {
7473
7785
  status: "active",
@@ -7680,7 +7992,7 @@ function extractAuthorizationSignals(body) {
7680
7992
  behavioral_baseline_active: false,
7681
7993
  // Would need explicit field in SHR v1.1
7682
7994
  identity_verified: l1.identity_type === "ed25519" || l1.identity_type !== "none",
7683
- zero_knowledge_capable: l3.status === "active" && l3.proof_system !== "commitment-only",
7995
+ zero_knowledge_capable: l3.status === "active",
7684
7996
  selective_disclosure_active: l3.selective_disclosure,
7685
7997
  reputation_portable: l4.reputation_portable,
7686
7998
  handshake_capable: body.capabilities.handshake
@@ -7758,14 +8070,6 @@ function generateAuthorizationConstraints(body, _degradations) {
7758
8070
  priority: "high"
7759
8071
  });
7760
8072
  }
7761
- if (layers.l3.proof_system === "commitment-only") {
7762
- constraints.push({
7763
- type: "restricted_scope",
7764
- description: "No zero-knowledge proofs available \u2014 entire state context may be visible",
7765
- rationale: "Proof system is commitment-only (no ZK)",
7766
- priority: "medium"
7767
- });
7768
- }
7769
8073
  if (layers.l4.status === "degraded") {
7770
8074
  constraints.push({
7771
8075
  type: "known_agents_only",
@@ -8097,6 +8401,154 @@ function deriveTrustTier(level) {
8097
8401
  }
8098
8402
  }
8099
8403
 
8404
+ // src/handshake/attestation.ts
8405
+ init_encoding();
8406
+ init_encoding();
8407
+ var ATTESTATION_VERSION = "1.0";
8408
+ function deriveTrustTier2(level) {
8409
+ switch (level) {
8410
+ case "full":
8411
+ return "verified-sovereign";
8412
+ case "degraded":
8413
+ return "verified-degraded";
8414
+ default:
8415
+ return "unverified";
8416
+ }
8417
+ }
8418
+ function generateAttestation(opts) {
8419
+ const {
8420
+ attesterSHR,
8421
+ subjectSHR,
8422
+ verificationResult,
8423
+ mutual = false,
8424
+ identityManager,
8425
+ masterKey,
8426
+ identityId
8427
+ } = opts;
8428
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
8429
+ if (!identity) {
8430
+ return { error: "No identity available for signing attestation" };
8431
+ }
8432
+ const now = /* @__PURE__ */ new Date();
8433
+ const attesterExpiry = new Date(attesterSHR.body.expires_at);
8434
+ const subjectExpiry = new Date(subjectSHR.body.expires_at);
8435
+ const earliestExpiry = attesterExpiry < subjectExpiry ? attesterExpiry : subjectExpiry;
8436
+ const sovereigntyLevel = verificationResult.valid ? verificationResult.sovereignty_level : "unverified";
8437
+ const body = {
8438
+ attestation_version: ATTESTATION_VERSION,
8439
+ attester_id: attesterSHR.body.instance_id,
8440
+ subject_id: subjectSHR.body.instance_id,
8441
+ attester_shr: attesterSHR,
8442
+ subject_shr: subjectSHR,
8443
+ verification: {
8444
+ subject_shr_valid: verificationResult.valid,
8445
+ subject_sovereignty_level: sovereigntyLevel,
8446
+ subject_trust_tier: deriveTrustTier2(sovereigntyLevel),
8447
+ mutual,
8448
+ errors: verificationResult.errors,
8449
+ warnings: verificationResult.warnings
8450
+ },
8451
+ attested_at: now.toISOString(),
8452
+ expires_at: earliestExpiry.toISOString()
8453
+ };
8454
+ const canonical = JSON.stringify(deepSortKeys(body));
8455
+ const payload = stringToBytes(canonical);
8456
+ const encryptionKey = derivePurposeKey(masterKey, "identity-encryption");
8457
+ const signatureBytes = sign(
8458
+ payload,
8459
+ identity.encrypted_private_key,
8460
+ encryptionKey
8461
+ );
8462
+ const summary = generateSummary(body);
8463
+ return {
8464
+ body,
8465
+ signed_by: identity.public_key,
8466
+ signature: toBase64url(signatureBytes),
8467
+ summary
8468
+ };
8469
+ }
8470
+ function layerLine(label, status) {
8471
+ const icon = status === "active" ? "\u2713" : status === "degraded" ? "~" : "x";
8472
+ return ` ${icon} ${label}: ${status}`;
8473
+ }
8474
+ function generateSummary(body) {
8475
+ const v = body.verification;
8476
+ const sLayers = body.subject_shr.body.layers;
8477
+ const aLayers = body.attester_shr.body.layers;
8478
+ const tierLabel = v.subject_trust_tier === "verified-sovereign" ? "Verified Sovereign" : v.subject_trust_tier === "verified-degraded" ? "Verified (Degraded)" : "Unverified";
8479
+ const lines = [
8480
+ `--- Sovereignty Attestation ---`,
8481
+ ``,
8482
+ `Attester: ${body.attester_id.slice(0, 16)}...`,
8483
+ `Subject: ${body.subject_id.slice(0, 16)}...`,
8484
+ `Result: ${tierLabel}`,
8485
+ ``,
8486
+ `Subject Sovereignty Posture:`,
8487
+ layerLine("L1 Cognitive Sovereignty", sLayers.l1.status),
8488
+ layerLine("L2 Operational Isolation", sLayers.l2.status),
8489
+ layerLine("L3 Selective Disclosure", sLayers.l3.status),
8490
+ layerLine("L4 Verifiable Reputation", sLayers.l4.status),
8491
+ ``,
8492
+ `Attester Sovereignty Posture:`,
8493
+ layerLine("L1 Cognitive Sovereignty", aLayers.l1.status),
8494
+ layerLine("L2 Operational Isolation", aLayers.l2.status),
8495
+ layerLine("L3 Selective Disclosure", aLayers.l3.status),
8496
+ layerLine("L4 Verifiable Reputation", aLayers.l4.status),
8497
+ ``,
8498
+ `Mutual: ${v.mutual ? "Yes" : "One-sided"}`,
8499
+ `Attested: ${body.attested_at}`,
8500
+ `Expires: ${body.expires_at}`,
8501
+ `Signature: ${body.attestation_version} / Ed25519`
8502
+ ];
8503
+ if (v.warnings.length > 0) {
8504
+ lines.push(``, `Warnings: ${v.warnings.join("; ")}`);
8505
+ }
8506
+ if (v.errors.length > 0) {
8507
+ lines.push(``, `Errors: ${v.errors.join("; ")}`);
8508
+ }
8509
+ lines.push(``, `--- Verify: compare signed_by against attester's known public key ---`);
8510
+ return lines.join("\n");
8511
+ }
8512
+ function verifyAttestation(attestation, now) {
8513
+ const errors = [];
8514
+ const currentTime = now ?? /* @__PURE__ */ new Date();
8515
+ if (attestation.body.attestation_version !== ATTESTATION_VERSION) {
8516
+ errors.push(
8517
+ `Unsupported attestation version: ${attestation.body.attestation_version}`
8518
+ );
8519
+ }
8520
+ if (!attestation.body.attester_id || !attestation.body.subject_id) {
8521
+ errors.push("Missing attester_id or subject_id");
8522
+ }
8523
+ if (!attestation.body.attester_shr || !attestation.body.subject_shr) {
8524
+ errors.push("Missing attester or subject SHR");
8525
+ }
8526
+ const expired = new Date(attestation.body.expires_at) <= currentTime;
8527
+ if (expired) {
8528
+ errors.push("Attestation has expired");
8529
+ }
8530
+ try {
8531
+ const publicKey = fromBase64url(attestation.signed_by);
8532
+ const canonical = JSON.stringify(deepSortKeys(attestation.body));
8533
+ const payload = stringToBytes(canonical);
8534
+ const signatureBytes = fromBase64url(attestation.signature);
8535
+ const signatureValid = verify(payload, signatureBytes, publicKey);
8536
+ if (!signatureValid) {
8537
+ errors.push("Attestation signature is invalid");
8538
+ }
8539
+ } catch (e) {
8540
+ errors.push(`Signature verification error: ${e.message}`);
8541
+ }
8542
+ return {
8543
+ valid: errors.length === 0,
8544
+ errors,
8545
+ attester_id: attestation.body.attester_id ?? "unknown",
8546
+ subject_id: attestation.body.subject_id ?? "unknown",
8547
+ trust_tier: errors.length === 0 ? attestation.body.verification.subject_trust_tier : "unverified",
8548
+ expired
8549
+ };
8550
+ }
8551
+
8100
8552
  // src/handshake/tools.ts
8101
8553
  function createHandshakeTools(config, identityManager, masterKey, auditLog) {
8102
8554
  const sessions = /* @__PURE__ */ new Map();
@@ -8282,6 +8734,103 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
8282
8734
  result: session.result ?? null
8283
8735
  });
8284
8736
  }
8737
+ },
8738
+ // ─── Streamlined Exchange ─────────────────────────────────────────
8739
+ {
8740
+ name: "sanctuary/handshake_exchange",
8741
+ description: "One-shot sovereignty exchange. Accepts a counterparty's signed SHR, verifies it, generates our SHR, and produces a signed attestation artifact \u2014 all in a single call. Returns a shareable attestation with human-readable summary. Use this instead of the 4-step handshake protocol when you want a quick, portable sovereignty verification (e.g., for social posting or async exchanges).",
8742
+ inputSchema: {
8743
+ type: "object",
8744
+ properties: {
8745
+ counterparty_shr: {
8746
+ type: "object",
8747
+ description: "The counterparty's signed SHR (SignedSHR object with body, signed_by, signature)."
8748
+ },
8749
+ identity_id: {
8750
+ type: "string",
8751
+ description: "Identity to use for the exchange. Defaults to primary identity."
8752
+ }
8753
+ },
8754
+ required: ["counterparty_shr"]
8755
+ },
8756
+ handler: async (args) => {
8757
+ const counterpartySHR = args.counterparty_shr;
8758
+ const ourSHR = generateSHR(args.identity_id, shrOpts);
8759
+ if (typeof ourSHR === "string") {
8760
+ return toolResult({ error: ourSHR });
8761
+ }
8762
+ const verificationResult = verifySHR(counterpartySHR);
8763
+ const attestation = generateAttestation({
8764
+ attesterSHR: ourSHR,
8765
+ subjectSHR: counterpartySHR,
8766
+ verificationResult,
8767
+ mutual: false,
8768
+ identityManager,
8769
+ masterKey,
8770
+ identityId: args.identity_id
8771
+ });
8772
+ if ("error" in attestation) {
8773
+ auditLog.append("l4", "handshake_exchange", ourSHR.body.instance_id, void 0, "failure");
8774
+ return toolResult({ error: attestation.error });
8775
+ }
8776
+ if (verificationResult.valid) {
8777
+ const sovereigntyLevel = verificationResult.sovereignty_level;
8778
+ const trustTier = sovereigntyLevel === "full" ? "verified-sovereign" : sovereigntyLevel === "degraded" ? "verified-degraded" : "unverified";
8779
+ handshakeResults.set(verificationResult.counterparty_id, {
8780
+ counterparty_id: verificationResult.counterparty_id,
8781
+ counterparty_shr: counterpartySHR,
8782
+ verified: true,
8783
+ sovereignty_level: sovereigntyLevel,
8784
+ trust_tier: trustTier,
8785
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
8786
+ expires_at: verificationResult.expires_at,
8787
+ errors: []
8788
+ });
8789
+ }
8790
+ auditLog.append("l4", "handshake_exchange", ourSHR.body.instance_id);
8791
+ return toolResult({
8792
+ attestation,
8793
+ our_shr: ourSHR,
8794
+ verification: {
8795
+ counterparty_valid: verificationResult.valid,
8796
+ counterparty_sovereignty: verificationResult.sovereignty_level,
8797
+ counterparty_id: verificationResult.counterparty_id,
8798
+ errors: verificationResult.errors,
8799
+ warnings: verificationResult.warnings
8800
+ },
8801
+ instructions: "The 'attestation' object is a signed, portable sovereignty verification artifact. Share it with the counterparty or post attestation.summary publicly. The counterparty can verify the attestation signature using your public key. Our SHR is included so the counterparty can perform their own verification of us.",
8802
+ _content_trust: "external"
8803
+ });
8804
+ }
8805
+ },
8806
+ {
8807
+ name: "sanctuary/handshake_verify_attestation",
8808
+ description: "Verify a signed attestation artifact from another agent. Checks the Ed25519 signature, temporal validity, and structural integrity.",
8809
+ inputSchema: {
8810
+ type: "object",
8811
+ properties: {
8812
+ attestation: {
8813
+ type: "object",
8814
+ description: "The SignedAttestation object to verify (body, signed_by, signature, summary)."
8815
+ }
8816
+ },
8817
+ required: ["attestation"]
8818
+ },
8819
+ handler: async (args) => {
8820
+ const attestation = args.attestation;
8821
+ const result = verifyAttestation(attestation);
8822
+ auditLog.append(
8823
+ "l4",
8824
+ "handshake_verify_attestation",
8825
+ result.attester_id,
8826
+ void 0,
8827
+ result.valid ? "success" : "failure"
8828
+ );
8829
+ return toolResult({
8830
+ ...result,
8831
+ _content_trust: "external"
8832
+ });
8833
+ }
8285
8834
  }
8286
8835
  ];
8287
8836
  return { tools, handshakeResults };
@@ -11893,11 +12442,6 @@ async function createSanctuaryServer(options) {
11893
12442
  degradations.push(
11894
12443
  "L2 isolation is process-level only; no TEE available"
11895
12444
  );
11896
- if (config.disclosure.proof_system === "commitment-only") {
11897
- degradations.push(
11898
- "L3 proofs are commitment-based only; ZK proofs not yet available"
11899
- );
11900
- }
11901
12445
  return toolResult({
11902
12446
  attestation: {
11903
12447
  environment_type: config.execution.environment,
@@ -11923,7 +12467,7 @@ async function createSanctuaryServer(options) {
11923
12467
  l1_state_encrypted: true,
11924
12468
  l2_execution_isolated: false,
11925
12469
  l2_isolation_type: "process-level",
11926
- l3_proofs_available: config.disclosure.proof_system !== "commitment-only",
12470
+ l3_proofs_available: true,
11927
12471
  l4_reputation_active: true,
11928
12472
  overall_level: "mvs",
11929
12473
  degradations
@@ -11946,14 +12490,6 @@ async function createSanctuaryServer(options) {
11946
12490
  severity: "warning",
11947
12491
  mitigation: "TEE support planned for a future release"
11948
12492
  });
11949
- if (config.disclosure.proof_system === "commitment-only") {
11950
- degradations.push({
11951
- layer: "l3",
11952
- description: "Commitment schemes only (no ZK proofs)",
11953
- severity: "info",
11954
- mitigation: "ZK proof support planned for v0.2.0"
11955
- });
11956
- }
11957
12493
  return toolResult({
11958
12494
  status: degradations.some((d) => d.severity === "critical") ? "compromised" : degradations.some((d) => d.severity === "warning") ? "degraded" : "healthy",
11959
12495
  storage_bytes: storageSizeBytes,
@@ -11972,7 +12508,7 @@ async function createSanctuaryServer(options) {
11972
12508
  last_attestation: (/* @__PURE__ */ new Date()).toISOString()
11973
12509
  },
11974
12510
  l3: {
11975
- status: config.disclosure.proof_system === "commitment-only" ? "degraded" : "active",
12511
+ status: "active",
11976
12512
  proof_system: config.disclosure.proof_system,
11977
12513
  circuits_loaded: 0,
11978
12514
  proofs_generated_total: 0
@@ -12252,6 +12788,7 @@ async function createSanctuaryServer(options) {
12252
12788
  return { server, config };
12253
12789
  }
12254
12790
 
12791
+ exports.ATTESTATION_VERSION = ATTESTATION_VERSION;
12255
12792
  exports.ApprovalGate = ApprovalGate;
12256
12793
  exports.AuditLog = AuditLog;
12257
12794
  exports.AutoApproveChannel = AutoApproveChannel;
@@ -12283,6 +12820,7 @@ exports.createRangeProof = createRangeProof;
12283
12820
  exports.createSanctuaryServer = createSanctuaryServer;
12284
12821
  exports.evaluateField = evaluateField;
12285
12822
  exports.filterContext = filterContext;
12823
+ exports.generateAttestation = generateAttestation;
12286
12824
  exports.generateSHR = generateSHR;
12287
12825
  exports.getTemplate = getTemplate;
12288
12826
  exports.initiateHandshake = initiateHandshake;
@@ -12294,6 +12832,7 @@ exports.resolveTier = resolveTier;
12294
12832
  exports.respondToHandshake = respondToHandshake;
12295
12833
  exports.signPayload = signPayload;
12296
12834
  exports.tierDistribution = tierDistribution;
12835
+ exports.verifyAttestation = verifyAttestation;
12297
12836
  exports.verifyBridgeCommitment = verifyBridgeCommitment;
12298
12837
  exports.verifyCompletion = verifyCompletion;
12299
12838
  exports.verifyPedersenCommitment = verifyPedersenCommitment;