@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/cli.js CHANGED
@@ -48,7 +48,7 @@ function defaultConfig() {
48
48
  }
49
49
  },
50
50
  disclosure: {
51
- proof_system: "commitment-only",
51
+ proof_system: "schnorr-pedersen",
52
52
  default_policy: "minimum-necessary"
53
53
  },
54
54
  reputation: {
@@ -162,7 +162,7 @@ function validateConfig(config) {
162
162
  `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.`
163
163
  );
164
164
  }
165
- const implementedProofSystem = /* @__PURE__ */ new Set(["commitment-only"]);
165
+ const implementedProofSystem = /* @__PURE__ */ new Set(["schnorr-pedersen", "commitment-only"]);
166
166
  if (!implementedProofSystem.has(config.disclosure.proof_system)) {
167
167
  errors.push(
168
168
  `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.`
@@ -867,6 +867,8 @@ tier3_always_allow:
867
867
  - handshake_respond
868
868
  - handshake_complete
869
869
  - handshake_status
870
+ - handshake_exchange
871
+ - handshake_verify_attestation
870
872
  - reputation_query_weighted
871
873
  - federation_peers
872
874
  - federation_trust_evaluate
@@ -968,6 +970,8 @@ var init_loader = __esm({
968
970
  "handshake_respond",
969
971
  "handshake_complete",
970
972
  "handshake_status",
973
+ "handshake_exchange",
974
+ "handshake_verify_attestation",
971
975
  "reputation_query_weighted",
972
976
  "federation_peers",
973
977
  "federation_trust_evaluate",
@@ -1162,174 +1166,255 @@ function generateLoginHTML(options) {
1162
1166
  return `<!DOCTYPE html>
1163
1167
  <html lang="en">
1164
1168
  <head>
1165
- <meta charset="utf-8">
1166
- <meta name="viewport" content="width=device-width, initial-scale=1">
1167
- <title>Sanctuary \u2014 Login</title>
1168
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
1169
- <style>
1170
- :root {
1171
- --bg: #0d1117;
1172
- --surface: #161b22;
1173
- --border: #30363d;
1174
- --text-primary: #e6edf3;
1175
- --text-secondary: #8b949e;
1176
- --green: #3fb950;
1177
- --red: #f85149;
1178
- --blue: #58a6ff;
1179
- --mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
1180
- --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
1181
- --radius: 6px;
1182
- }
1183
- * { box-sizing: border-box; margin: 0; padding: 0; }
1184
- html, body { width: 100%; height: 100%; }
1185
- body {
1186
- font-family: var(--sans);
1187
- background: var(--bg);
1188
- color: var(--text-primary);
1189
- display: flex;
1190
- align-items: center;
1191
- justify-content: center;
1192
- }
1193
- .login-container {
1194
- width: 100%;
1195
- max-width: 400px;
1196
- padding: 40px 32px;
1197
- background: var(--surface);
1198
- border: 1px solid var(--border);
1199
- border-radius: 12px;
1200
- }
1201
- .login-logo {
1202
- text-align: center;
1203
- font-size: 20px;
1204
- font-weight: 700;
1205
- letter-spacing: -0.5px;
1206
- margin-bottom: 8px;
1207
- }
1208
- .login-logo span { color: var(--blue); }
1209
- .login-version {
1210
- text-align: center;
1211
- font-size: 11px;
1212
- color: var(--text-secondary);
1213
- font-family: var(--mono);
1214
- margin-bottom: 32px;
1215
- }
1216
- .login-label {
1217
- display: block;
1218
- font-size: 13px;
1219
- font-weight: 600;
1220
- color: var(--text-secondary);
1221
- margin-bottom: 8px;
1222
- }
1223
- .login-input {
1224
- width: 100%;
1225
- padding: 10px 14px;
1226
- background: var(--bg);
1227
- border: 1px solid var(--border);
1228
- border-radius: var(--radius);
1229
- color: var(--text-primary);
1230
- font-family: var(--mono);
1231
- font-size: 14px;
1232
- outline: none;
1233
- transition: border-color 0.15s;
1234
- }
1235
- .login-input:focus { border-color: var(--blue); }
1236
- .login-input::placeholder { color: var(--text-secondary); opacity: 0.5; }
1237
- .login-btn {
1238
- width: 100%;
1239
- margin-top: 20px;
1240
- padding: 10px;
1241
- background: var(--blue);
1242
- color: var(--bg);
1243
- border: none;
1244
- border-radius: var(--radius);
1245
- font-size: 14px;
1246
- font-weight: 600;
1247
- cursor: pointer;
1248
- transition: opacity 0.15s;
1249
- font-family: var(--sans);
1250
- }
1251
- .login-btn:hover { opacity: 0.9; }
1252
- .login-btn:disabled { opacity: 0.5; cursor: not-allowed; }
1253
- .login-error {
1254
- margin-top: 16px;
1255
- padding: 10px 14px;
1256
- background: rgba(248, 81, 73, 0.1);
1257
- border: 1px solid var(--red);
1258
- border-radius: var(--radius);
1259
- font-size: 12px;
1260
- color: var(--red);
1261
- display: none;
1262
- }
1263
- .login-hint {
1264
- margin-top: 24px;
1265
- padding-top: 16px;
1266
- border-top: 1px solid var(--border);
1267
- font-size: 11px;
1268
- color: var(--text-secondary);
1269
- line-height: 1.5;
1270
- }
1271
- .login-hint code {
1272
- font-family: var(--mono);
1273
- background: var(--bg);
1274
- padding: 1px 4px;
1275
- border-radius: 3px;
1276
- font-size: 10px;
1277
- }
1278
- </style>
1169
+ <meta charset="UTF-8">
1170
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1171
+ <title>Sanctuary Dashboard</title>
1172
+ <style>
1173
+ :root {
1174
+ --bg: #0d1117;
1175
+ --surface: #161b22;
1176
+ --border: #30363d;
1177
+ --text-primary: #e6edf3;
1178
+ --text-secondary: #8b949e;
1179
+ --green: #3fb950;
1180
+ --amber: #d29922;
1181
+ --red: #f85149;
1182
+ --blue: #58a6ff;
1183
+ }
1184
+
1185
+ * {
1186
+ margin: 0;
1187
+ padding: 0;
1188
+ box-sizing: border-box;
1189
+ }
1190
+
1191
+ body {
1192
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
1193
+ background-color: var(--bg);
1194
+ color: var(--text-primary);
1195
+ min-height: 100vh;
1196
+ display: flex;
1197
+ align-items: center;
1198
+ justify-content: center;
1199
+ }
1200
+
1201
+ .login-container {
1202
+ width: 100%;
1203
+ max-width: 400px;
1204
+ padding: 20px;
1205
+ }
1206
+
1207
+ .login-card {
1208
+ background-color: var(--surface);
1209
+ border: 1px solid var(--border);
1210
+ border-radius: 8px;
1211
+ padding: 40px 32px;
1212
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
1213
+ }
1214
+
1215
+ .login-header {
1216
+ display: flex;
1217
+ align-items: center;
1218
+ gap: 12px;
1219
+ margin-bottom: 32px;
1220
+ }
1221
+
1222
+ .logo {
1223
+ font-size: 24px;
1224
+ font-weight: 700;
1225
+ color: var(--blue);
1226
+ }
1227
+
1228
+ .logo-text {
1229
+ display: flex;
1230
+ flex-direction: column;
1231
+ }
1232
+
1233
+ .logo-text .title {
1234
+ font-size: 18px;
1235
+ font-weight: 600;
1236
+ letter-spacing: -0.5px;
1237
+ }
1238
+
1239
+ .logo-text .version {
1240
+ font-size: 12px;
1241
+ color: var(--text-secondary);
1242
+ margin-top: 2px;
1243
+ }
1244
+
1245
+ .form-group {
1246
+ margin-bottom: 24px;
1247
+ }
1248
+
1249
+ label {
1250
+ display: block;
1251
+ font-size: 14px;
1252
+ font-weight: 500;
1253
+ margin-bottom: 8px;
1254
+ color: var(--text-primary);
1255
+ }
1256
+
1257
+ input[type="text"],
1258
+ input[type="password"] {
1259
+ width: 100%;
1260
+ padding: 10px 12px;
1261
+ background-color: var(--bg);
1262
+ border: 1px solid var(--border);
1263
+ border-radius: 6px;
1264
+ color: var(--text-primary);
1265
+ font-size: 14px;
1266
+ font-family: 'JetBrains Mono', monospace;
1267
+ transition: border-color 0.2s;
1268
+ }
1269
+
1270
+ input[type="text"]:focus,
1271
+ input[type="password"]:focus {
1272
+ outline: none;
1273
+ border-color: var(--blue);
1274
+ box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.1);
1275
+ }
1276
+
1277
+ .error-message {
1278
+ display: none;
1279
+ background-color: rgba(248, 81, 73, 0.1);
1280
+ border: 1px solid var(--red);
1281
+ color: #ff9999;
1282
+ padding: 12px;
1283
+ border-radius: 6px;
1284
+ font-size: 13px;
1285
+ margin-bottom: 20px;
1286
+ }
1287
+
1288
+ .error-message.show {
1289
+ display: block;
1290
+ }
1291
+
1292
+ button {
1293
+ width: 100%;
1294
+ padding: 10px 16px;
1295
+ background-color: var(--blue);
1296
+ color: var(--bg);
1297
+ border: none;
1298
+ border-radius: 6px;
1299
+ font-size: 14px;
1300
+ font-weight: 600;
1301
+ cursor: pointer;
1302
+ transition: background-color 0.2s;
1303
+ }
1304
+
1305
+ button:hover {
1306
+ background-color: #79c0ff;
1307
+ }
1308
+
1309
+ button:active {
1310
+ background-color: #4184e4;
1311
+ }
1312
+
1313
+ button:disabled {
1314
+ background-color: var(--text-secondary);
1315
+ cursor: not-allowed;
1316
+ opacity: 0.5;
1317
+ }
1318
+
1319
+ .info-text {
1320
+ font-size: 12px;
1321
+ color: var(--text-secondary);
1322
+ margin-top: 16px;
1323
+ text-align: center;
1324
+ }
1325
+ </style>
1279
1326
  </head>
1280
1327
  <body>
1281
- <div class="login-container">
1282
- <div class="login-logo"><span>&#9670;</span> SANCTUARY</div>
1283
- <div class="login-version">Principal Dashboard v${options.serverVersion}</div>
1284
- <form id="loginForm" onsubmit="return handleLogin(event)">
1285
- <label class="login-label" for="tokenInput">Dashboard Auth Token</label>
1286
- <input class="login-input" type="password" id="tokenInput"
1287
- placeholder="Enter your auth token" autocomplete="off" autofocus required>
1288
- <button class="login-btn" type="submit" id="loginBtn">Open Dashboard</button>
1289
- </form>
1290
- <div class="login-error" id="loginError"></div>
1291
- <div class="login-hint">
1292
- Your token is set via <code>SANCTUARY_DASHBOARD_AUTH_TOKEN</code> environment variable,
1293
- or check your server's startup output.
1328
+ <div class="login-container">
1329
+ <div class="login-card">
1330
+ <div class="login-header">
1331
+ <div class="logo">\u25C6</div>
1332
+ <div class="logo-text">
1333
+ <div class="title">SANCTUARY</div>
1334
+ <div class="version">v${options.serverVersion}</div>
1335
+ </div>
1336
+ </div>
1337
+
1338
+ <div id="error-message" class="error-message"></div>
1339
+
1340
+ <form id="login-form">
1341
+ <div class="form-group">
1342
+ <label for="auth-token">Bearer Token</label>
1343
+ <input
1344
+ type="text"
1345
+ id="auth-token"
1346
+ name="token"
1347
+ placeholder="Paste your session token..."
1348
+ autocomplete="off"
1349
+ spellcheck="false"
1350
+ required
1351
+ />
1352
+ </div>
1353
+
1354
+ <button type="submit" id="login-button">Open Dashboard</button>
1355
+ </form>
1356
+
1357
+ <div class="info-text">
1358
+ Session tokens expire after 1 hour of inactivity
1359
+ </div>
1360
+ </div>
1294
1361
  </div>
1295
- </div>
1296
- <script>
1297
- async function handleLogin(e) {
1298
- e.preventDefault();
1299
- var btn = document.getElementById('loginBtn');
1300
- var errEl = document.getElementById('loginError');
1301
- var token = document.getElementById('tokenInput').value.trim();
1302
- if (!token) return false;
1303
- btn.disabled = true;
1304
- btn.textContent = 'Authenticating...';
1305
- errEl.style.display = 'none';
1306
- try {
1307
- var resp = await fetch('/auth/session', {
1308
- method: 'POST',
1309
- headers: { 'Authorization': 'Bearer ' + token }
1362
+
1363
+ <script>
1364
+ const loginForm = document.getElementById('login-form');
1365
+ const authTokenInput = document.getElementById('auth-token');
1366
+ const errorMessage = document.getElementById('error-message');
1367
+ const loginButton = document.getElementById('login-button');
1368
+
1369
+ loginForm.addEventListener('submit', async (e) => {
1370
+ e.preventDefault();
1371
+ const token = authTokenInput.value.trim();
1372
+
1373
+ if (!token) {
1374
+ showError('Token is required');
1375
+ return;
1376
+ }
1377
+
1378
+ loginButton.disabled = true;
1379
+ loginButton.textContent = 'Verifying...';
1380
+ errorMessage.classList.remove('show');
1381
+
1382
+ try {
1383
+ const response = await fetch('/auth/session', {
1384
+ method: 'POST',
1385
+ headers: {
1386
+ 'Content-Type': 'application/json',
1387
+ 'Authorization': 'Bearer ' + token,
1388
+ },
1389
+ body: JSON.stringify({ token }),
1390
+ });
1391
+
1392
+ if (response.ok) {
1393
+ const data = await response.json();
1394
+ sessionStorage.setItem('authToken', token);
1395
+ window.location.href = '/dashboard';
1396
+ } else if (response.status === 401) {
1397
+ showError('Invalid token. Please check and try again.');
1398
+ } else {
1399
+ showError('Authentication failed. Please try again.');
1400
+ }
1401
+ } catch (err) {
1402
+ showError('Connection error. Please check your network.');
1403
+ } finally {
1404
+ loginButton.disabled = false;
1405
+ loginButton.textContent = 'Open Dashboard';
1406
+ }
1310
1407
  });
1311
- if (!resp.ok) {
1312
- var data = await resp.json().catch(function() { return {}; });
1313
- throw new Error(data.error || 'Authentication failed');
1314
- }
1315
- var result = await resp.json();
1316
- // Store token in sessionStorage for auto-renewal inside the dashboard
1317
- try { sessionStorage.setItem('sanctuary_token', token); } catch(_) {}
1318
- // Set session cookie
1319
- var maxAge = result.expires_in_seconds || 300;
1320
- document.cookie = 'sanctuary_session=' + result.session_id +
1321
- '; path=/; SameSite=Strict; max-age=' + maxAge;
1322
- // Reload to enter the dashboard
1323
- window.location.reload();
1324
- } catch (err) {
1325
- errEl.textContent = err.message || 'Authentication failed. Check your token.';
1326
- errEl.style.display = 'block';
1327
- btn.disabled = false;
1328
- btn.textContent = 'Open Dashboard';
1329
- }
1330
- return false;
1331
- }
1332
- </script>
1408
+
1409
+ function showError(message) {
1410
+ errorMessage.textContent = message;
1411
+ errorMessage.classList.add('show');
1412
+ }
1413
+
1414
+ authTokenInput.addEventListener('input', () => {
1415
+ errorMessage.classList.remove('show');
1416
+ });
1417
+ </script>
1333
1418
  </body>
1334
1419
  </html>`;
1335
1420
  }
@@ -1337,1412 +1422,1648 @@ function generateDashboardHTML(options) {
1337
1422
  return `<!DOCTYPE html>
1338
1423
  <html lang="en">
1339
1424
  <head>
1340
- <meta charset="utf-8">
1341
- <meta name="viewport" content="width=device-width, initial-scale=1">
1342
- <title>Sanctuary \u2014 Principal Dashboard</title>
1343
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
1344
- <style>
1345
- :root {
1346
- --bg: #0d1117;
1347
- --surface: #161b22;
1348
- --border: #30363d;
1349
- --text-primary: #e6edf3;
1350
- --text-secondary: #8b949e;
1351
- --green: #3fb950;
1352
- --amber: #d29922;
1353
- --red: #f85149;
1354
- --blue: #58a6ff;
1355
- --mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
1356
- --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
1357
- --radius: 6px;
1358
- }
1359
-
1360
- * {
1361
- box-sizing: border-box;
1362
- margin: 0;
1363
- padding: 0;
1364
- }
1425
+ <meta charset="UTF-8">
1426
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1427
+ <title>Sanctuary Dashboard</title>
1428
+ <style>
1429
+ :root {
1430
+ --bg: #0d1117;
1431
+ --surface: #161b22;
1432
+ --border: #30363d;
1433
+ --text-primary: #e6edf3;
1434
+ --text-secondary: #8b949e;
1435
+ --green: #3fb950;
1436
+ --amber: #d29922;
1437
+ --red: #f85149;
1438
+ --blue: #58a6ff;
1439
+ --success: #3fb950;
1440
+ --warning: #d29922;
1441
+ --error: #f85149;
1442
+ --muted: #21262d;
1443
+ }
1444
+
1445
+ * {
1446
+ margin: 0;
1447
+ padding: 0;
1448
+ box-sizing: border-box;
1449
+ }
1450
+
1451
+ html, body {
1452
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
1453
+ background-color: var(--bg);
1454
+ color: var(--text-primary);
1455
+ height: 100%;
1456
+ overflow: hidden;
1457
+ }
1458
+
1459
+ body {
1460
+ display: flex;
1461
+ flex-direction: column;
1462
+ }
1463
+
1464
+ /* Status Bar */
1465
+ .status-bar {
1466
+ position: fixed;
1467
+ top: 0;
1468
+ left: 0;
1469
+ right: 0;
1470
+ height: 56px;
1471
+ background-color: var(--surface);
1472
+ border-bottom: 1px solid var(--border);
1473
+ display: flex;
1474
+ align-items: center;
1475
+ padding: 0 24px;
1476
+ gap: 24px;
1477
+ z-index: 100;
1478
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
1479
+ }
1480
+
1481
+ .status-bar-left {
1482
+ display: flex;
1483
+ align-items: center;
1484
+ gap: 12px;
1485
+ flex: 0 0 auto;
1486
+ }
1365
1487
 
1366
- html, body {
1367
- width: 100%;
1368
- height: 100%;
1369
- overflow: hidden;
1370
- }
1371
-
1372
- body {
1373
- font-family: var(--sans);
1374
- background: var(--bg);
1375
- color: var(--text-primary);
1376
- display: flex;
1377
- flex-direction: column;
1378
- }
1379
-
1380
- /* \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 */
1488
+ .logo-icon {
1489
+ font-size: 20px;
1490
+ color: var(--blue);
1491
+ font-weight: 700;
1492
+ }
1381
1493
 
1382
- .status-bar {
1383
- position: fixed;
1384
- top: 0;
1385
- left: 0;
1386
- right: 0;
1387
- height: 56px;
1388
- background: var(--surface);
1389
- border-bottom: 1px solid var(--border);
1390
- display: flex;
1391
- align-items: center;
1392
- padding: 0 20px;
1393
- gap: 24px;
1394
- z-index: 1000;
1395
- }
1494
+ .logo-info {
1495
+ display: flex;
1496
+ flex-direction: column;
1497
+ }
1396
1498
 
1397
- .status-bar-left {
1398
- display: flex;
1399
- align-items: center;
1400
- gap: 12px;
1401
- flex: 0 0 auto;
1402
- }
1499
+ .logo-title {
1500
+ font-size: 13px;
1501
+ font-weight: 600;
1502
+ line-height: 1;
1503
+ color: var(--text-primary);
1504
+ }
1403
1505
 
1404
- .sanctuary-logo {
1405
- font-weight: 700;
1406
- font-size: 16px;
1407
- letter-spacing: -0.5px;
1408
- color: var(--text-primary);
1409
- }
1506
+ .logo-version {
1507
+ font-size: 11px;
1508
+ color: var(--text-secondary);
1509
+ margin-top: 2px;
1510
+ }
1410
1511
 
1411
- .sanctuary-logo span {
1412
- color: var(--blue);
1413
- }
1512
+ .status-bar-center {
1513
+ flex: 1;
1514
+ display: flex;
1515
+ justify-content: center;
1516
+ }
1414
1517
 
1415
- .version {
1416
- font-size: 11px;
1417
- color: var(--text-secondary);
1418
- font-family: var(--mono);
1419
- }
1518
+ .sovereignty-badge {
1519
+ display: flex;
1520
+ align-items: center;
1521
+ gap: 8px;
1522
+ padding: 8px 16px;
1523
+ background-color: rgba(63, 185, 80, 0.1);
1524
+ border: 1px solid rgba(63, 185, 80, 0.3);
1525
+ border-radius: 6px;
1526
+ font-size: 13px;
1527
+ font-weight: 500;
1528
+ }
1420
1529
 
1421
- .status-bar-center {
1422
- flex: 1;
1423
- display: flex;
1424
- align-items: center;
1425
- justify-content: center;
1426
- }
1427
-
1428
- .sovereignty-badge {
1429
- display: flex;
1430
- align-items: center;
1431
- gap: 8px;
1432
- padding: 6px 12px;
1433
- background: rgba(88, 166, 255, 0.1);
1434
- border: 1px solid var(--blue);
1435
- border-radius: 20px;
1436
- font-size: 13px;
1437
- font-weight: 600;
1438
- }
1530
+ .sovereignty-badge.degraded {
1531
+ background-color: rgba(210, 153, 34, 0.1);
1532
+ border-color: rgba(210, 153, 34, 0.3);
1533
+ }
1439
1534
 
1440
- .sovereignty-score {
1441
- display: flex;
1442
- align-items: center;
1443
- justify-content: center;
1444
- width: 28px;
1445
- height: 28px;
1446
- border-radius: 50%;
1447
- font-family: var(--mono);
1448
- font-weight: 700;
1449
- font-size: 12px;
1450
- background: var(--blue);
1451
- color: var(--bg);
1452
- }
1535
+ .sovereignty-badge.inactive {
1536
+ background-color: rgba(248, 81, 73, 0.1);
1537
+ border-color: rgba(248, 81, 73, 0.3);
1538
+ }
1453
1539
 
1454
- .sovereignty-score.high {
1455
- background: var(--green);
1456
- }
1540
+ .sovereignty-score {
1541
+ font-weight: 700;
1542
+ color: var(--green);
1543
+ }
1457
1544
 
1458
- .sovereignty-score.medium {
1459
- background: var(--amber);
1460
- }
1545
+ .sovereignty-badge.degraded .sovereignty-score {
1546
+ color: var(--amber);
1547
+ }
1461
1548
 
1462
- .sovereignty-score.low {
1463
- background: var(--red);
1464
- }
1549
+ .sovereignty-badge.inactive .sovereignty-score {
1550
+ color: var(--red);
1551
+ }
1465
1552
 
1466
- .status-bar-right {
1467
- display: flex;
1468
- align-items: center;
1469
- gap: 16px;
1470
- flex: 0 0 auto;
1471
- }
1553
+ .status-bar-right {
1554
+ display: flex;
1555
+ align-items: center;
1556
+ gap: 16px;
1557
+ flex: 0 0 auto;
1558
+ }
1472
1559
 
1473
- .protections-indicator {
1474
- display: flex;
1475
- align-items: center;
1476
- gap: 6px;
1477
- font-size: 12px;
1478
- color: var(--text-secondary);
1479
- font-family: var(--mono);
1480
- }
1560
+ .status-item {
1561
+ display: flex;
1562
+ align-items: center;
1563
+ gap: 6px;
1564
+ font-size: 12px;
1565
+ color: var(--text-secondary);
1566
+ }
1481
1567
 
1482
- .protections-indicator .count {
1483
- color: var(--text-primary);
1484
- font-weight: 600;
1485
- }
1568
+ .status-item strong {
1569
+ color: var(--text-primary);
1570
+ font-weight: 500;
1571
+ }
1486
1572
 
1487
- .uptime {
1488
- display: flex;
1489
- align-items: center;
1490
- gap: 6px;
1491
- font-size: 12px;
1492
- color: var(--text-secondary);
1493
- font-family: var(--mono);
1494
- }
1573
+ .status-dot {
1574
+ width: 8px;
1575
+ height: 8px;
1576
+ border-radius: 50%;
1577
+ background-color: var(--green);
1578
+ }
1495
1579
 
1496
- .status-dot {
1497
- width: 8px;
1498
- height: 8px;
1499
- border-radius: 50%;
1500
- background: var(--green);
1501
- animation: pulse 2s ease-in-out infinite;
1502
- }
1580
+ .status-dot.disconnected {
1581
+ background-color: var(--red);
1582
+ }
1503
1583
 
1504
- .status-dot.disconnected {
1505
- background: var(--red);
1506
- animation: none;
1507
- }
1584
+ .pending-badge {
1585
+ display: flex;
1586
+ align-items: center;
1587
+ gap: 6px;
1588
+ padding: 4px 8px;
1589
+ background-color: var(--blue);
1590
+ color: var(--bg);
1591
+ border-radius: 4px;
1592
+ font-size: 11px;
1593
+ font-weight: 600;
1594
+ cursor: pointer;
1595
+ }
1508
1596
 
1509
- @keyframes pulse {
1510
- 0%, 100% { opacity: 1; }
1511
- 50% { opacity: 0.5; }
1512
- }
1597
+ /* Main Content */
1598
+ .main-content {
1599
+ flex: 1;
1600
+ margin-top: 56px;
1601
+ overflow-y: auto;
1602
+ padding: 24px;
1603
+ }
1513
1604
 
1514
- .pending-badge {
1515
- display: inline-flex;
1516
- align-items: center;
1517
- justify-content: center;
1518
- min-width: 24px;
1519
- height: 24px;
1520
- padding: 0 6px;
1521
- background: var(--red);
1522
- color: white;
1523
- border-radius: 12px;
1524
- font-size: 11px;
1525
- font-weight: 700;
1526
- animation: pulse 1s ease-in-out infinite;
1527
- }
1605
+ .grid {
1606
+ display: grid;
1607
+ gap: 20px;
1608
+ }
1528
1609
 
1529
- .pending-badge.hidden {
1530
- display: none;
1531
- }
1610
+ /* Row 1: Sovereignty Layers */
1611
+ .sovereignty-layers {
1612
+ display: grid;
1613
+ grid-template-columns: repeat(4, 1fr);
1614
+ gap: 16px;
1615
+ }
1532
1616
 
1533
- /* \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 */
1617
+ .layer-card {
1618
+ background-color: var(--surface);
1619
+ border: 1px solid var(--border);
1620
+ border-radius: 8px;
1621
+ padding: 20px;
1622
+ display: flex;
1623
+ flex-direction: column;
1624
+ gap: 12px;
1625
+ }
1534
1626
 
1535
- .main-container {
1536
- flex: 1;
1537
- display: flex;
1538
- margin-top: 56px;
1539
- overflow: hidden;
1540
- }
1627
+ .layer-card.degraded {
1628
+ border-color: var(--amber);
1629
+ background-color: rgba(210, 153, 34, 0.05);
1630
+ }
1541
1631
 
1542
- .activity-feed {
1543
- flex: 3;
1544
- display: flex;
1545
- flex-direction: column;
1546
- border-right: 1px solid var(--border);
1547
- overflow: hidden;
1548
- }
1632
+ .layer-card.inactive {
1633
+ border-color: var(--red);
1634
+ background-color: rgba(248, 81, 73, 0.05);
1635
+ }
1549
1636
 
1550
- .feed-header {
1551
- padding: 16px 20px;
1552
- border-bottom: 1px solid var(--border);
1553
- display: flex;
1554
- align-items: center;
1555
- gap: 8px;
1556
- font-size: 12px;
1557
- font-weight: 600;
1558
- text-transform: uppercase;
1559
- letter-spacing: 0.5px;
1560
- color: var(--text-secondary);
1561
- }
1637
+ .layer-name {
1638
+ font-size: 12px;
1639
+ font-weight: 600;
1640
+ color: var(--text-secondary);
1641
+ text-transform: uppercase;
1642
+ letter-spacing: 0.5px;
1643
+ }
1562
1644
 
1563
- .feed-header-dot {
1564
- width: 6px;
1565
- height: 6px;
1566
- border-radius: 50%;
1567
- background: var(--green);
1568
- }
1645
+ .layer-title {
1646
+ font-size: 14px;
1647
+ font-weight: 600;
1648
+ color: var(--text-primary);
1649
+ }
1569
1650
 
1570
- .activity-list {
1571
- flex: 1;
1572
- overflow-y: auto;
1573
- overflow-x: hidden;
1574
- }
1651
+ .layer-status {
1652
+ display: inline-flex;
1653
+ align-items: center;
1654
+ gap: 6px;
1655
+ padding: 4px 8px;
1656
+ background-color: rgba(63, 185, 80, 0.15);
1657
+ color: var(--green);
1658
+ border-radius: 4px;
1659
+ font-size: 11px;
1660
+ font-weight: 600;
1661
+ width: fit-content;
1662
+ }
1575
1663
 
1576
- .activity-item {
1577
- padding: 12px 20px;
1578
- border-bottom: 1px solid rgba(48, 54, 61, 0.5);
1579
- font-size: 13px;
1580
- font-family: var(--mono);
1581
- cursor: pointer;
1582
- transition: background 0.15s;
1583
- display: flex;
1584
- align-items: flex-start;
1585
- gap: 10px;
1586
- }
1664
+ .layer-card.degraded .layer-status {
1665
+ background-color: rgba(210, 153, 34, 0.15);
1666
+ color: var(--amber);
1667
+ }
1587
1668
 
1588
- .activity-item:hover {
1589
- background: rgba(88, 166, 255, 0.05);
1590
- }
1669
+ .layer-card.inactive .layer-status {
1670
+ background-color: rgba(248, 81, 73, 0.15);
1671
+ color: var(--red);
1672
+ }
1591
1673
 
1592
- .activity-item-icon {
1593
- flex: 0 0 auto;
1594
- width: 16px;
1595
- text-align: center;
1596
- font-size: 12px;
1597
- color: var(--text-secondary);
1598
- margin-top: 1px;
1599
- }
1674
+ .layer-detail {
1675
+ font-size: 12px;
1676
+ color: var(--text-secondary);
1677
+ font-family: 'JetBrains Mono', monospace;
1678
+ padding: 8px;
1679
+ background-color: var(--bg);
1680
+ border-radius: 4px;
1681
+ border-left: 2px solid var(--blue);
1682
+ }
1600
1683
 
1601
- .activity-item-content {
1602
- flex: 1;
1603
- min-width: 0;
1604
- }
1684
+ /* Row 2: Info Cards */
1685
+ .info-cards {
1686
+ display: grid;
1687
+ grid-template-columns: repeat(3, 1fr);
1688
+ gap: 16px;
1689
+ }
1605
1690
 
1606
- .activity-time {
1607
- color: var(--text-secondary);
1608
- font-size: 11px;
1609
- margin-bottom: 2px;
1610
- }
1691
+ .info-card {
1692
+ background-color: var(--surface);
1693
+ border: 1px solid var(--border);
1694
+ border-radius: 8px;
1695
+ padding: 20px;
1696
+ }
1611
1697
 
1612
- .activity-main {
1613
- display: flex;
1614
- gap: 8px;
1615
- align-items: baseline;
1616
- margin-bottom: 4px;
1617
- }
1698
+ .card-header {
1699
+ font-size: 12px;
1700
+ font-weight: 600;
1701
+ color: var(--text-secondary);
1702
+ text-transform: uppercase;
1703
+ letter-spacing: 0.5px;
1704
+ margin-bottom: 16px;
1705
+ }
1618
1706
 
1619
- .activity-tier {
1620
- display: inline-flex;
1621
- align-items: center;
1622
- justify-content: center;
1623
- width: 24px;
1624
- height: 16px;
1625
- font-size: 10px;
1626
- font-weight: 700;
1627
- border-radius: 3px;
1628
- text-transform: uppercase;
1629
- flex: 0 0 auto;
1630
- }
1707
+ .card-row {
1708
+ display: flex;
1709
+ justify-content: space-between;
1710
+ align-items: center;
1711
+ margin-bottom: 12px;
1712
+ font-size: 13px;
1713
+ }
1631
1714
 
1632
- .activity-tier.t1 {
1633
- background: rgba(248, 81, 73, 0.2);
1634
- color: var(--red);
1635
- }
1715
+ .card-row:last-child {
1716
+ margin-bottom: 0;
1717
+ }
1636
1718
 
1637
- .activity-tier.t2 {
1638
- background: rgba(210, 153, 34, 0.2);
1639
- color: var(--amber);
1640
- }
1719
+ .card-label {
1720
+ color: var(--text-secondary);
1721
+ }
1641
1722
 
1642
- .activity-tier.t3 {
1643
- background: rgba(63, 185, 80, 0.2);
1644
- color: var(--green);
1645
- }
1723
+ .card-value {
1724
+ color: var(--text-primary);
1725
+ font-family: 'JetBrains Mono', monospace;
1726
+ font-weight: 500;
1727
+ }
1646
1728
 
1647
- .activity-tool {
1648
- color: var(--text-primary);
1649
- font-weight: 600;
1650
- }
1729
+ .identity-badge {
1730
+ display: inline-flex;
1731
+ align-items: center;
1732
+ gap: 4px;
1733
+ padding: 2px 6px;
1734
+ background-color: rgba(88, 166, 255, 0.15);
1735
+ color: var(--blue);
1736
+ border-radius: 3px;
1737
+ font-size: 10px;
1738
+ font-weight: 600;
1739
+ text-transform: uppercase;
1740
+ }
1651
1741
 
1652
- .activity-outcome {
1653
- color: var(--green);
1654
- }
1742
+ .trust-tier-badge {
1743
+ display: inline-flex;
1744
+ align-items: center;
1745
+ gap: 4px;
1746
+ padding: 2px 6px;
1747
+ background-color: rgba(63, 185, 80, 0.15);
1748
+ color: var(--green);
1749
+ border-radius: 3px;
1750
+ font-size: 10px;
1751
+ font-weight: 600;
1752
+ }
1655
1753
 
1656
- .activity-outcome.denied {
1657
- color: var(--red);
1658
- }
1754
+ .truncated {
1755
+ max-width: 200px;
1756
+ overflow: hidden;
1757
+ text-overflow: ellipsis;
1758
+ white-space: nowrap;
1759
+ }
1659
1760
 
1660
- .activity-detail {
1661
- font-size: 12px;
1662
- color: var(--text-secondary);
1663
- margin-left: 0;
1664
- }
1761
+ /* Row 3: SHR & Activity */
1762
+ .main-panels {
1763
+ display: grid;
1764
+ grid-template-columns: 1fr 1fr;
1765
+ gap: 16px;
1766
+ min-height: 400px;
1767
+ }
1665
1768
 
1666
- .activity-item.expanded .activity-detail {
1667
- display: block;
1668
- margin-top: 8px;
1669
- padding: 10px;
1670
- background: rgba(88, 166, 255, 0.08);
1671
- border-left: 2px solid var(--blue);
1672
- border-radius: 4px;
1673
- }
1769
+ .panel {
1770
+ background-color: var(--surface);
1771
+ border: 1px solid var(--border);
1772
+ border-radius: 8px;
1773
+ display: flex;
1774
+ flex-direction: column;
1775
+ overflow: hidden;
1776
+ }
1674
1777
 
1675
- .activity-empty {
1676
- display: flex;
1677
- flex-direction: column;
1678
- align-items: center;
1679
- justify-content: center;
1680
- height: 100%;
1681
- color: var(--text-secondary);
1682
- }
1778
+ .panel-header {
1779
+ padding: 16px 20px;
1780
+ border-bottom: 1px solid var(--border);
1781
+ display: flex;
1782
+ justify-content: space-between;
1783
+ align-items: center;
1784
+ }
1683
1785
 
1684
- .activity-empty-icon {
1685
- font-size: 32px;
1686
- margin-bottom: 12px;
1687
- }
1786
+ .panel-title {
1787
+ font-size: 14px;
1788
+ font-weight: 600;
1789
+ color: var(--text-primary);
1790
+ }
1688
1791
 
1689
- .activity-empty-text {
1690
- font-size: 14px;
1691
- }
1792
+ .panel-action {
1793
+ background: none;
1794
+ border: none;
1795
+ color: var(--blue);
1796
+ cursor: pointer;
1797
+ font-size: 12px;
1798
+ padding: 0;
1799
+ font-weight: 500;
1800
+ transition: color 0.2s;
1801
+ }
1692
1802
 
1693
- /* \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 */
1803
+ .panel-action:hover {
1804
+ color: #79c0ff;
1805
+ }
1694
1806
 
1695
- .protection-sidebar {
1696
- flex: 2;
1697
- display: flex;
1698
- flex-direction: column;
1699
- background: rgba(22, 27, 34, 0.5);
1700
- overflow: hidden;
1701
- }
1807
+ .panel-content {
1808
+ flex: 1;
1809
+ overflow-y: auto;
1810
+ padding: 20px;
1811
+ }
1702
1812
 
1703
- .sidebar-header {
1704
- padding: 16px 20px;
1705
- border-bottom: 1px solid var(--border);
1706
- font-size: 12px;
1707
- font-weight: 600;
1708
- text-transform: uppercase;
1709
- letter-spacing: 0.5px;
1710
- color: var(--text-secondary);
1711
- display: flex;
1712
- align-items: center;
1713
- gap: 8px;
1714
- }
1813
+ /* SHR Viewer */
1814
+ .shr-json {
1815
+ font-family: 'JetBrains Mono', monospace;
1816
+ font-size: 12px;
1817
+ line-height: 1.6;
1818
+ color: var(--text-secondary);
1819
+ }
1715
1820
 
1716
- .sidebar-content {
1717
- flex: 1;
1718
- overflow-y: auto;
1719
- padding: 16px 16px;
1720
- display: grid;
1721
- grid-template-columns: 1fr 1fr;
1722
- gap: 12px;
1723
- }
1821
+ .shr-section {
1822
+ margin-bottom: 12px;
1823
+ }
1724
1824
 
1725
- .protection-card {
1726
- background: var(--surface);
1727
- border: 1px solid var(--border);
1728
- border-radius: var(--radius);
1729
- padding: 14px;
1730
- display: flex;
1731
- flex-direction: column;
1732
- gap: 8px;
1733
- }
1825
+ .shr-section-header {
1826
+ display: flex;
1827
+ align-items: center;
1828
+ gap: 8px;
1829
+ cursor: pointer;
1830
+ font-weight: 600;
1831
+ color: var(--text-primary);
1832
+ padding: 8px;
1833
+ background-color: var(--bg);
1834
+ border-radius: 4px;
1835
+ user-select: none;
1836
+ }
1734
1837
 
1735
- .protection-card-icon {
1736
- font-size: 14px;
1737
- }
1838
+ .shr-section-header:hover {
1839
+ background-color: var(--muted);
1840
+ }
1738
1841
 
1739
- .protection-card-label {
1740
- font-size: 11px;
1741
- font-weight: 600;
1742
- text-transform: uppercase;
1743
- letter-spacing: 0.5px;
1744
- color: var(--text-secondary);
1745
- }
1842
+ .shr-toggle {
1843
+ width: 16px;
1844
+ height: 16px;
1845
+ display: flex;
1846
+ align-items: center;
1847
+ justify-content: center;
1848
+ font-size: 10px;
1849
+ transition: transform 0.2s;
1850
+ }
1746
1851
 
1747
- .protection-card-status {
1748
- display: flex;
1749
- align-items: center;
1750
- gap: 6px;
1751
- font-size: 12px;
1752
- font-weight: 600;
1753
- }
1852
+ .shr-section.collapsed .shr-toggle {
1853
+ transform: rotate(-90deg);
1854
+ }
1754
1855
 
1755
- .protection-card-status.active {
1756
- color: var(--green);
1757
- }
1856
+ .shr-section-content {
1857
+ padding: 8px 16px;
1858
+ background-color: rgba(0, 0, 0, 0.2);
1859
+ border-radius: 4px;
1860
+ margin-top: 4px;
1861
+ }
1758
1862
 
1759
- .protection-card-status.inactive {
1760
- color: var(--text-secondary);
1761
- }
1863
+ .shr-section.collapsed .shr-section-content {
1864
+ display: none;
1865
+ }
1762
1866
 
1763
- .protection-card-stat {
1764
- font-size: 11px;
1765
- color: var(--text-secondary);
1766
- font-family: var(--mono);
1767
- margin-top: 4px;
1768
- }
1867
+ .shr-item {
1868
+ display: flex;
1869
+ margin-bottom: 4px;
1870
+ }
1769
1871
 
1770
- /* \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 */
1872
+ .shr-key {
1873
+ color: var(--blue);
1874
+ margin-right: 8px;
1875
+ min-width: 120px;
1876
+ }
1771
1877
 
1772
- .pending-overlay {
1773
- position: fixed;
1774
- top: 56px;
1775
- right: 0;
1776
- bottom: 0;
1777
- width: 0;
1778
- background: var(--surface);
1779
- border-left: 1px solid var(--border);
1780
- z-index: 999;
1781
- overflow-y: auto;
1782
- transition: width 0.3s ease-out;
1783
- display: flex;
1784
- flex-direction: column;
1785
- }
1878
+ .shr-value {
1879
+ color: var(--green);
1880
+ word-break: break-all;
1881
+ }
1786
1882
 
1787
- .pending-overlay.active {
1788
- width: 380px;
1789
- }
1883
+ /* Activity Feed */
1884
+ .activity-feed {
1885
+ display: flex;
1886
+ flex-direction: column;
1887
+ gap: 12px;
1888
+ }
1790
1889
 
1791
- @media (max-width: 1400px) {
1792
- .pending-overlay.active {
1793
- width: 100%;
1794
- right: auto;
1795
- left: 0;
1890
+ .activity-item {
1891
+ padding: 12px;
1892
+ background-color: var(--bg);
1893
+ border-left: 2px solid var(--border);
1894
+ border-radius: 4px;
1895
+ font-size: 12px;
1796
1896
  }
1797
- }
1798
1897
 
1799
- .pending-overlay-header {
1800
- padding: 16px 20px;
1801
- border-bottom: 1px solid var(--border);
1802
- display: flex;
1803
- align-items: center;
1804
- justify-content: space-between;
1805
- flex: 0 0 auto;
1806
- }
1898
+ .activity-item.tool-call {
1899
+ border-left-color: var(--blue);
1900
+ }
1807
1901
 
1808
- .pending-overlay-title {
1809
- font-size: 13px;
1810
- font-weight: 600;
1811
- text-transform: uppercase;
1812
- letter-spacing: 0.5px;
1813
- color: var(--text-primary);
1814
- }
1902
+ .activity-item.context-gate {
1903
+ border-left-color: var(--amber);
1904
+ }
1815
1905
 
1816
- .pending-overlay-close {
1817
- background: none;
1818
- border: none;
1819
- color: var(--text-secondary);
1820
- cursor: pointer;
1821
- font-size: 18px;
1822
- padding: 0;
1823
- display: flex;
1824
- align-items: center;
1825
- justify-content: center;
1826
- }
1906
+ .activity-item.injection {
1907
+ border-left-color: var(--red);
1908
+ }
1827
1909
 
1828
- .pending-overlay-close:hover {
1829
- color: var(--text-primary);
1830
- }
1910
+ .activity-item.protection {
1911
+ border-left-color: var(--green);
1912
+ }
1831
1913
 
1832
- .pending-list {
1833
- flex: 1;
1834
- overflow-y: auto;
1835
- }
1914
+ .activity-type {
1915
+ font-weight: 600;
1916
+ color: var(--text-primary);
1917
+ margin-bottom: 4px;
1918
+ text-transform: uppercase;
1919
+ font-size: 11px;
1920
+ letter-spacing: 0.5px;
1921
+ }
1836
1922
 
1837
- .pending-item {
1838
- padding: 16px 20px;
1839
- border-bottom: 1px solid rgba(48, 54, 61, 0.5);
1840
- display: flex;
1841
- flex-direction: column;
1842
- gap: 10px;
1843
- }
1923
+ .activity-content {
1924
+ color: var(--text-secondary);
1925
+ font-family: 'JetBrains Mono', monospace;
1926
+ margin-bottom: 4px;
1927
+ word-break: break-all;
1928
+ }
1844
1929
 
1845
- .pending-item-header {
1846
- display: flex;
1847
- align-items: center;
1848
- gap: 8px;
1849
- }
1930
+ .activity-time {
1931
+ font-size: 11px;
1932
+ color: var(--text-secondary);
1933
+ }
1850
1934
 
1851
- .pending-item-op {
1852
- font-family: var(--mono);
1853
- font-size: 12px;
1854
- font-weight: 600;
1855
- color: var(--text-primary);
1856
- flex: 1;
1857
- }
1935
+ .empty-state {
1936
+ display: flex;
1937
+ align-items: center;
1938
+ justify-content: center;
1939
+ height: 100%;
1940
+ color: var(--text-secondary);
1941
+ font-size: 13px;
1942
+ }
1858
1943
 
1859
- .pending-item-tier {
1860
- display: inline-flex;
1861
- align-items: center;
1862
- justify-content: center;
1863
- width: 28px;
1864
- height: 20px;
1865
- font-size: 9px;
1866
- font-weight: 700;
1867
- border-radius: 3px;
1868
- text-transform: uppercase;
1869
- color: white;
1870
- }
1944
+ /* Row 4: Handshake History */
1945
+ .handshake-table {
1946
+ background-color: var(--surface);
1947
+ border: 1px solid var(--border);
1948
+ border-radius: 8px;
1949
+ overflow: hidden;
1950
+ }
1871
1951
 
1872
- .pending-item-tier.tier1 {
1873
- background: var(--red);
1874
- }
1952
+ .table-header {
1953
+ display: grid;
1954
+ grid-template-columns: 2fr 1fr 1fr 1fr 1.5fr 1.5fr;
1955
+ gap: 16px;
1956
+ padding: 16px 20px;
1957
+ border-bottom: 1px solid var(--border);
1958
+ background-color: var(--bg);
1959
+ font-size: 12px;
1960
+ font-weight: 600;
1961
+ color: var(--text-secondary);
1962
+ text-transform: uppercase;
1963
+ letter-spacing: 0.5px;
1964
+ }
1875
1965
 
1876
- .pending-item-tier.tier2 {
1877
- background: var(--amber);
1878
- }
1966
+ .table-rows {
1967
+ max-height: 300px;
1968
+ overflow-y: auto;
1969
+ }
1879
1970
 
1880
- .pending-item-reason {
1881
- font-size: 12px;
1882
- color: var(--text-secondary);
1883
- }
1971
+ .table-row {
1972
+ display: grid;
1973
+ grid-template-columns: 2fr 1fr 1fr 1fr 1.5fr 1.5fr;
1974
+ gap: 16px;
1975
+ padding: 14px 20px;
1976
+ border-bottom: 1px solid var(--border);
1977
+ align-items: center;
1978
+ font-size: 12px;
1979
+ cursor: pointer;
1980
+ transition: background-color 0.2s;
1981
+ }
1884
1982
 
1885
- .pending-item-timer {
1886
- display: flex;
1887
- align-items: center;
1888
- gap: 6px;
1889
- font-size: 11px;
1890
- font-family: var(--mono);
1891
- color: var(--text-secondary);
1892
- }
1983
+ .table-row:hover {
1984
+ background-color: var(--bg);
1985
+ }
1893
1986
 
1894
- .pending-item-timer-bar {
1895
- flex: 1;
1896
- height: 4px;
1897
- background: rgba(48, 54, 61, 0.8);
1898
- border-radius: 2px;
1899
- overflow: hidden;
1900
- }
1987
+ .table-row:last-child {
1988
+ border-bottom: none;
1989
+ }
1901
1990
 
1902
- .pending-item-timer-fill {
1903
- height: 100%;
1904
- background: var(--blue);
1905
- transition: width 0.1s linear;
1906
- }
1991
+ .table-cell {
1992
+ color: var(--text-secondary);
1993
+ font-family: 'JetBrains Mono', monospace;
1994
+ }
1907
1995
 
1908
- .pending-item-timer.urgent .pending-item-timer-fill {
1909
- background: var(--red);
1910
- }
1996
+ .table-cell.strong {
1997
+ color: var(--text-primary);
1998
+ font-weight: 500;
1999
+ }
1911
2000
 
1912
- .pending-item-actions {
1913
- display: flex;
1914
- gap: 8px;
1915
- }
2001
+ .table-empty {
2002
+ padding: 40px 20px;
2003
+ text-align: center;
2004
+ color: var(--text-secondary);
2005
+ font-size: 13px;
2006
+ }
1916
2007
 
1917
- .btn {
1918
- flex: 1;
1919
- padding: 8px 12px;
1920
- border: none;
1921
- border-radius: var(--radius);
1922
- font-size: 12px;
1923
- font-weight: 600;
1924
- cursor: pointer;
1925
- transition: all 0.15s;
1926
- font-family: var(--sans);
1927
- }
2008
+ /* Pending Overlay */
2009
+ .pending-overlay {
2010
+ position: fixed;
2011
+ top: 0;
2012
+ right: -400px;
2013
+ width: 400px;
2014
+ height: 100vh;
2015
+ background-color: var(--surface);
2016
+ border-left: 1px solid var(--border);
2017
+ box-shadow: -2px 0 8px rgba(0, 0, 0, 0.3);
2018
+ z-index: 200;
2019
+ transition: right 0.3s ease;
2020
+ display: flex;
2021
+ flex-direction: column;
2022
+ overflow-y: auto;
2023
+ }
1928
2024
 
1929
- .btn-approve {
1930
- background: var(--green);
1931
- color: var(--bg);
1932
- }
2025
+ .pending-overlay.show {
2026
+ right: 0;
2027
+ }
1933
2028
 
1934
- .btn-approve:hover {
1935
- background: #4ecf5e;
1936
- }
2029
+ .pending-header {
2030
+ padding: 16px 20px;
2031
+ border-bottom: 1px solid var(--border);
2032
+ font-weight: 600;
2033
+ color: var(--text-primary);
2034
+ }
1937
2035
 
1938
- .btn-deny {
1939
- background: var(--red);
1940
- color: white;
1941
- }
2036
+ .pending-items {
2037
+ flex: 1;
2038
+ overflow-y: auto;
2039
+ padding: 16px;
2040
+ }
1942
2041
 
1943
- .btn-deny:hover {
1944
- background: #f9605e;
1945
- }
2042
+ .pending-item {
2043
+ background-color: var(--bg);
2044
+ border: 1px solid var(--border);
2045
+ border-radius: 6px;
2046
+ padding: 16px;
2047
+ margin-bottom: 12px;
2048
+ }
1946
2049
 
1947
- /* \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 */
2050
+ .pending-title {
2051
+ font-weight: 600;
2052
+ color: var(--text-primary);
2053
+ margin-bottom: 8px;
2054
+ word-break: break-word;
2055
+ }
1948
2056
 
1949
- .threat-panel {
1950
- position: fixed;
1951
- bottom: 0;
1952
- left: 0;
1953
- right: 0;
1954
- background: var(--surface);
1955
- border-top: 1px solid var(--border);
1956
- max-height: 240px;
1957
- z-index: 500;
1958
- display: flex;
1959
- flex-direction: column;
1960
- transition: max-height 0.3s ease-out;
1961
- }
2057
+ .pending-countdown {
2058
+ font-size: 12px;
2059
+ color: var(--amber);
2060
+ margin-bottom: 12px;
2061
+ font-weight: 500;
2062
+ }
1962
2063
 
1963
- .threat-panel.collapsed {
1964
- max-height: 40px;
1965
- }
2064
+ .pending-actions {
2065
+ display: flex;
2066
+ gap: 8px;
2067
+ }
1966
2068
 
1967
- .threat-header {
1968
- padding: 12px 20px;
1969
- cursor: pointer;
1970
- display: flex;
1971
- align-items: center;
1972
- gap: 8px;
1973
- font-size: 12px;
1974
- font-weight: 600;
1975
- text-transform: uppercase;
1976
- letter-spacing: 0.5px;
1977
- color: var(--text-secondary);
1978
- flex: 0 0 auto;
1979
- }
2069
+ .pending-btn {
2070
+ flex: 1;
2071
+ padding: 8px 12px;
2072
+ border: none;
2073
+ border-radius: 4px;
2074
+ font-size: 12px;
2075
+ font-weight: 600;
2076
+ cursor: pointer;
2077
+ transition: background-color 0.2s;
2078
+ }
1980
2079
 
1981
- .threat-header:hover {
1982
- background: rgba(88, 166, 255, 0.05);
1983
- }
2080
+ .pending-approve {
2081
+ background-color: var(--green);
2082
+ color: var(--bg);
2083
+ }
1984
2084
 
1985
- .threat-icon {
1986
- font-size: 14px;
1987
- }
2085
+ .pending-approve:hover {
2086
+ background-color: #3fa040;
2087
+ }
1988
2088
 
1989
- .threat-content {
1990
- flex: 1;
1991
- overflow-y: auto;
1992
- padding: 0 20px 12px;
1993
- display: flex;
1994
- flex-direction: column;
1995
- gap: 10px;
1996
- }
2089
+ .pending-deny {
2090
+ background-color: var(--red);
2091
+ color: var(--bg);
2092
+ }
1997
2093
 
1998
- .threat-item {
1999
- padding: 8px 10px;
2000
- background: rgba(248, 81, 73, 0.1);
2001
- border-left: 2px solid var(--red);
2002
- border-radius: 4px;
2003
- font-size: 11px;
2004
- color: var(--text-secondary);
2005
- }
2094
+ .pending-deny:hover {
2095
+ background-color: #e03c3c;
2096
+ }
2006
2097
 
2007
- .threat-item-type {
2008
- font-weight: 600;
2009
- color: var(--red);
2010
- font-family: var(--mono);
2011
- }
2098
+ /* Threat Panel */
2099
+ .threat-panel {
2100
+ background-color: var(--surface);
2101
+ border: 1px solid var(--border);
2102
+ border-radius: 8px;
2103
+ margin-top: 20px;
2104
+ overflow: hidden;
2105
+ }
2012
2106
 
2013
- .threat-empty {
2014
- text-align: center;
2015
- padding: 20px 10px;
2016
- color: var(--text-secondary);
2017
- font-size: 12px;
2018
- }
2107
+ .threat-header {
2108
+ padding: 16px 20px;
2109
+ border-bottom: 1px solid var(--border);
2110
+ display: flex;
2111
+ justify-content: space-between;
2112
+ align-items: center;
2113
+ cursor: pointer;
2114
+ user-select: none;
2115
+ }
2019
2116
 
2020
- /* \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 */
2117
+ .threat-title {
2118
+ font-weight: 600;
2119
+ color: var(--text-primary);
2120
+ }
2021
2121
 
2022
- ::-webkit-scrollbar {
2023
- width: 6px;
2024
- }
2122
+ .threat-toggle {
2123
+ font-size: 10px;
2124
+ color: var(--text-secondary);
2125
+ transition: transform 0.2s;
2126
+ }
2025
2127
 
2026
- ::-webkit-scrollbar-track {
2027
- background: transparent;
2028
- }
2128
+ .threat-panel.collapsed .threat-toggle {
2129
+ transform: rotate(-90deg);
2130
+ }
2029
2131
 
2030
- ::-webkit-scrollbar-thumb {
2031
- background: var(--border);
2032
- border-radius: 3px;
2033
- }
2132
+ .threat-content {
2133
+ padding: 16px 20px;
2134
+ max-height: 300px;
2135
+ overflow-y: auto;
2136
+ }
2034
2137
 
2035
- ::-webkit-scrollbar-thumb:hover {
2036
- background: rgba(88, 166, 255, 0.3);
2037
- }
2138
+ .threat-panel.collapsed .threat-content {
2139
+ display: none;
2140
+ }
2038
2141
 
2039
- /* \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 */
2142
+ .threat-alert {
2143
+ background-color: rgba(248, 81, 73, 0.1);
2144
+ border: 1px solid var(--red);
2145
+ border-radius: 4px;
2146
+ padding: 12px;
2147
+ margin-bottom: 8px;
2148
+ font-size: 12px;
2149
+ }
2040
2150
 
2041
- @media (max-width: 1200px) {
2042
- .protection-sidebar {
2043
- display: none;
2151
+ .threat-alert:last-child {
2152
+ margin-bottom: 0;
2044
2153
  }
2045
2154
 
2046
- .activity-feed {
2047
- border-right: none;
2155
+ .threat-type {
2156
+ font-weight: 600;
2157
+ color: var(--red);
2158
+ margin-bottom: 4px;
2159
+ text-transform: uppercase;
2160
+ font-size: 10px;
2161
+ letter-spacing: 0.5px;
2048
2162
  }
2049
- }
2050
2163
 
2051
- @media (max-width: 768px) {
2052
- .status-bar {
2053
- padding: 0 12px;
2054
- gap: 12px;
2055
- height: 48px;
2164
+ .threat-message {
2165
+ color: var(--text-secondary);
2056
2166
  }
2057
2167
 
2058
- .sanctuary-logo {
2059
- font-size: 14px;
2168
+ /* Scrollbar */
2169
+ ::-webkit-scrollbar {
2170
+ width: 8px;
2060
2171
  }
2061
2172
 
2062
- .status-bar-center {
2063
- display: none;
2173
+ ::-webkit-scrollbar-track {
2174
+ background-color: transparent;
2064
2175
  }
2065
2176
 
2066
- .main-container {
2067
- margin-top: 48px;
2177
+ ::-webkit-scrollbar-thumb {
2178
+ background-color: var(--border);
2179
+ border-radius: 4px;
2068
2180
  }
2069
2181
 
2070
- .activity-item {
2071
- padding: 10px 12px;
2182
+ ::-webkit-scrollbar-thumb:hover {
2183
+ background-color: var(--text-secondary);
2072
2184
  }
2073
2185
 
2074
- .pending-overlay.active {
2075
- width: 100%;
2186
+ /* Responsive */
2187
+ @media (max-width: 1400px) {
2188
+ .sovereignty-layers {
2189
+ grid-template-columns: repeat(2, 1fr);
2190
+ }
2191
+
2192
+ .main-panels {
2193
+ grid-template-columns: 1fr;
2194
+ }
2195
+
2196
+ .pending-overlay {
2197
+ width: 100%;
2198
+ right: -100%;
2199
+ }
2076
2200
  }
2077
2201
 
2078
- .threat-panel {
2079
- max-height: 200px;
2202
+ @media (max-width: 768px) {
2203
+ .status-bar {
2204
+ flex-wrap: wrap;
2205
+ height: auto;
2206
+ padding: 12px;
2207
+ gap: 12px;
2208
+ }
2209
+
2210
+ .status-bar-center {
2211
+ order: 3;
2212
+ flex-basis: 100%;
2213
+ }
2214
+
2215
+ .main-content {
2216
+ margin-top: auto;
2217
+ }
2218
+
2219
+ .info-cards {
2220
+ grid-template-columns: 1fr;
2221
+ }
2222
+
2223
+ .table-header,
2224
+ .table-row {
2225
+ grid-template-columns: 1fr;
2226
+ }
2080
2227
  }
2081
- }
2082
- </style>
2228
+ </style>
2083
2229
  </head>
2084
2230
  <body>
2085
-
2086
- <!-- Status Bar (fixed, top) -->
2087
- <div class="status-bar">
2088
- <div class="status-bar-left">
2089
- <div class="sanctuary-logo"><span>\u25C6</span> SANCTUARY</div>
2090
- <div class="version">v${options.serverVersion}</div>
2091
- </div>
2092
- <div class="status-bar-center">
2093
- <div class="sovereignty-badge">
2094
- <div class="sovereignty-score" id="sovereigntyScore">85</div>
2095
- <span>Sovereignty Health</span>
2231
+ <!-- Status Bar -->
2232
+ <div class="status-bar">
2233
+ <div class="status-bar-left">
2234
+ <div class="logo-icon">\u25C6</div>
2235
+ <div class="logo-info">
2236
+ <div class="logo-title">SANCTUARY</div>
2237
+ <div class="logo-version">v${options.serverVersion}</div>
2238
+ </div>
2096
2239
  </div>
2097
- </div>
2098
- <div class="status-bar-right">
2099
- <div class="protections-indicator">
2100
- <span class="count" id="activeProtections">6</span>/6 protections
2240
+
2241
+ <div class="status-bar-center">
2242
+ <div id="sovereignty-badge" class="sovereignty-badge">
2243
+ <span>Sovereignty Health:</span>
2244
+ <span class="sovereignty-score" id="sovereignty-score">\u2014</span>
2245
+ <span>/ 100</span>
2246
+ </div>
2101
2247
  </div>
2102
- <div class="uptime">
2103
- <span id="uptimeText">\u2014</span>
2248
+
2249
+ <div class="status-bar-right">
2250
+ <div class="status-item">
2251
+ <strong id="protections-count">\u2014</strong>
2252
+ <span>Protections</span>
2253
+ </div>
2254
+ <div class="status-item">
2255
+ <strong id="uptime-value">\u2014</strong>
2256
+ <span>Uptime</span>
2257
+ </div>
2258
+ <div class="status-dot" id="connection-status"></div>
2259
+ <div id="pending-item-badge" class="pending-badge" style="display: none;">
2260
+ <span>\u23F3</span>
2261
+ <span id="pending-count">0</span>
2262
+ </div>
2104
2263
  </div>
2105
- <div class="status-dot" id="statusDot"></div>
2106
- <div class="pending-badge hidden" id="pendingBadge">0</div>
2107
2264
  </div>
2108
- </div>
2109
-
2110
- <!-- Main Layout -->
2111
- <div class="main-container">
2112
- <!-- Activity Feed -->
2113
- <div class="activity-feed">
2114
- <div class="feed-header">
2115
- <div class="feed-header-dot"></div>
2116
- Live Activity
2117
- </div>
2118
- <div class="activity-list" id="activityList">
2119
- <div class="activity-empty">
2120
- <div class="activity-empty-icon">\u2192</div>
2121
- <div class="activity-empty-text">Waiting for activity...</div>
2265
+
2266
+ <!-- Main Content -->
2267
+ <div class="main-content">
2268
+ <div class="grid">
2269
+ <!-- Row 1: Sovereignty Layers -->
2270
+ <div class="sovereignty-layers" id="sovereignty-layers">
2271
+ <div class="layer-card" data-layer="l1">
2272
+ <div class="layer-name">Layer 1</div>
2273
+ <div class="layer-title">Cognitive Sovereignty</div>
2274
+ <div class="layer-status"><span>\u25CF</span> <span id="l1-status">\u2014</span></div>
2275
+ <div class="layer-detail" id="l1-detail">Loading...</div>
2276
+ </div>
2277
+ <div class="layer-card" data-layer="l2">
2278
+ <div class="layer-name">Layer 2</div>
2279
+ <div class="layer-title">Operational Isolation</div>
2280
+ <div class="layer-status"><span>\u25CF</span> <span id="l2-status">\u2014</span></div>
2281
+ <div class="layer-detail" id="l2-detail">Loading...</div>
2282
+ </div>
2283
+ <div class="layer-card" data-layer="l3">
2284
+ <div class="layer-name">Layer 3</div>
2285
+ <div class="layer-title">Selective Disclosure</div>
2286
+ <div class="layer-status"><span>\u25CF</span> <span id="l3-status">\u2014</span></div>
2287
+ <div class="layer-detail" id="l3-detail">Loading...</div>
2288
+ </div>
2289
+ <div class="layer-card" data-layer="l4">
2290
+ <div class="layer-name">Layer 4</div>
2291
+ <div class="layer-title">Verifiable Reputation</div>
2292
+ <div class="layer-status"><span>\u25CF</span> <span id="l4-status">\u2014</span></div>
2293
+ <div class="layer-detail" id="l4-detail">Loading...</div>
2294
+ </div>
2295
+ </div>
2296
+
2297
+ <!-- Row 2: Info Cards -->
2298
+ <div class="info-cards">
2299
+ <div class="info-card">
2300
+ <div class="card-header">Identity</div>
2301
+ <div class="card-row">
2302
+ <span class="card-label">Primary</span>
2303
+ <span class="card-value" id="identity-label">\u2014</span>
2304
+ </div>
2305
+ <div class="card-row">
2306
+ <span class="card-label">DID</span>
2307
+ <span class="card-value truncated" id="identity-did" title="">\u2014</span>
2308
+ </div>
2309
+ <div class="card-row">
2310
+ <span class="card-label">Public Key</span>
2311
+ <span class="card-value truncated" id="identity-pubkey" title="">\u2014</span>
2312
+ </div>
2313
+ <div class="card-row">
2314
+ <span class="card-label">Type</span>
2315
+ <span class="identity-badge">Ed25519</span>
2316
+ </div>
2317
+ <div class="card-row">
2318
+ <span class="card-label">Created</span>
2319
+ <span class="card-value" id="identity-created">\u2014</span>
2320
+ </div>
2321
+ <div class="card-row">
2322
+ <span class="card-label">Identities</span>
2323
+ <span class="card-value" id="identity-count">\u2014</span>
2324
+ </div>
2325
+ </div>
2326
+
2327
+ <div class="info-card">
2328
+ <div class="card-header">Handshakes</div>
2329
+ <div class="card-row">
2330
+ <span class="card-label">Total</span>
2331
+ <span class="card-value" id="handshake-count">\u2014</span>
2332
+ </div>
2333
+ <div class="card-row">
2334
+ <span class="card-label">Latest Peer</span>
2335
+ <span class="card-value truncated" id="handshake-latest">\u2014</span>
2336
+ </div>
2337
+ <div class="card-row">
2338
+ <span class="card-label">Trust Tier</span>
2339
+ <span class="trust-tier-badge" id="handshake-tier">Unverified</span>
2340
+ </div>
2341
+ <div class="card-row">
2342
+ <span class="card-label">Timestamp</span>
2343
+ <span class="card-value" id="handshake-time">\u2014</span>
2344
+ </div>
2345
+ </div>
2346
+
2347
+ <div class="info-card">
2348
+ <div class="card-header">Reputation</div>
2349
+ <div class="card-row">
2350
+ <span class="card-label">Weighted Score</span>
2351
+ <span class="card-value" id="reputation-score">\u2014</span>
2352
+ </div>
2353
+ <div class="card-row">
2354
+ <span class="card-label">Attestations</span>
2355
+ <span class="card-value" id="reputation-attestations">\u2014</span>
2356
+ </div>
2357
+ <div class="card-row">
2358
+ <span class="card-label">Verified Sovereign</span>
2359
+ <span class="card-value" id="reputation-verified">\u2014</span>
2360
+ </div>
2361
+ <div class="card-row">
2362
+ <span class="card-label">Verified Degraded</span>
2363
+ <span class="card-value" id="reputation-degraded">\u2014</span>
2364
+ </div>
2365
+ <div class="card-row">
2366
+ <span class="card-label">Unverified</span>
2367
+ <span class="card-value" id="reputation-unverified">\u2014</span>
2368
+ </div>
2369
+ </div>
2370
+ </div>
2371
+
2372
+ <!-- Row 3: SHR & Activity -->
2373
+ <div class="main-panels">
2374
+ <div class="panel">
2375
+ <div class="panel-header">
2376
+ <div class="panel-title">Sovereignty Health Report</div>
2377
+ <button class="panel-action" id="copy-shr-btn">Copy JSON</button>
2378
+ </div>
2379
+ <div class="panel-content">
2380
+ <div class="shr-json" id="shr-viewer">
2381
+ <div class="empty-state">Loading SHR...</div>
2382
+ </div>
2383
+ </div>
2384
+ </div>
2385
+
2386
+ <div class="panel">
2387
+ <div class="panel-header">
2388
+ <div class="panel-title">Activity Feed</div>
2389
+ </div>
2390
+ <div class="panel-content">
2391
+ <div id="activity-feed" class="activity-feed">
2392
+ <div class="empty-state">Waiting for activity...</div>
2393
+ </div>
2394
+ </div>
2395
+ </div>
2396
+ </div>
2397
+
2398
+ <!-- Row 4: Handshake History -->
2399
+ <div class="handshake-table">
2400
+ <div class="table-header">
2401
+ <div>Counterparty</div>
2402
+ <div>Trust Tier</div>
2403
+ <div>Sovereignty</div>
2404
+ <div>Verified</div>
2405
+ <div>Completed</div>
2406
+ <div>Expires</div>
2407
+ </div>
2408
+ <div class="table-rows" id="handshake-table">
2409
+ <div class="table-empty">No handshakes completed yet</div>
2410
+ </div>
2411
+ </div>
2412
+
2413
+ <!-- Threat Panel -->
2414
+ <div class="threat-panel collapsed">
2415
+ <div class="threat-header">
2416
+ <div class="threat-title">Security Threats</div>
2417
+ <div class="threat-toggle">\u25B6</div>
2418
+ </div>
2419
+ <div class="threat-content" id="threat-alerts">
2420
+ <div class="empty-state">No threats detected</div>
2421
+ </div>
2122
2422
  </div>
2123
2423
  </div>
2124
2424
  </div>
2125
2425
 
2126
- <!-- Protection Status Sidebar -->
2127
- <div class="protection-sidebar" id="protectionSidebar">
2128
- <div class="sidebar-header">
2129
- <span>\u25C6</span> Protection Status
2130
- </div>
2131
- <div class="sidebar-content">
2132
- <div class="protection-card">
2133
- <div class="protection-card-icon">\u{1F510}</div>
2134
- <div class="protection-card-label">Encryption</div>
2135
- <div class="protection-card-status active" id="encryptionStatus">\u2713 Active</div>
2136
- <div class="protection-card-stat" id="encryptionStat">Ed25519</div>
2137
- </div>
2426
+ <!-- Pending Overlay -->
2427
+ <div class="pending-overlay" id="pending-overlay">
2428
+ <div class="pending-header">Pending Approvals</div>
2429
+ <div class="pending-items" id="pending-items"></div>
2430
+ </div>
2431
+
2432
+ <script>
2433
+ // Constants
2434
+ const AUTH_TOKEN = '${options.authToken || ""}' || sessionStorage.getItem('authToken') || '';
2435
+ const TIMEOUT_SECONDS = ${options.timeoutSeconds};
2436
+ const API_BASE = '';
2437
+
2438
+ // State
2439
+ let apiState = {
2440
+ sovereignty: null,
2441
+ identity: null,
2442
+ handshakes: [],
2443
+ shr: null,
2444
+ status: null,
2445
+ };
2446
+
2447
+ let pendingRequests = new Map();
2448
+ let activityLog = [];
2449
+ const maxActivityItems = 50;
2450
+
2451
+ // Helpers
2452
+ function esc(text) {
2453
+ if (!text) return '';
2454
+ const div = document.createElement('div');
2455
+ div.textContent = text;
2456
+ return div.innerHTML;
2457
+ }
2458
+
2459
+ function formatTime(isoString) {
2460
+ if (!isoString) return '\u2014';
2461
+ const date = new Date(isoString);
2462
+ return date.toLocaleString('en-US', {
2463
+ month: 'short',
2464
+ day: 'numeric',
2465
+ hour: '2-digit',
2466
+ minute: '2-digit',
2467
+ });
2468
+ }
2469
+
2470
+ function truncate(str, len = 16) {
2471
+ if (!str) return '\u2014';
2472
+ if (str.length <= len) return str;
2473
+ return str.slice(0, len) + '...';
2474
+ }
2475
+
2476
+ function calculateSovereigntyScore(shr) {
2477
+ if (!shr || !shr.layers) return 0;
2478
+ const layers = shr.layers;
2479
+ let score = 100;
2480
+
2481
+ if (layers.l1?.status === 'degraded') score -= 20;
2482
+ if (layers.l1?.status === 'inactive') score -= 35;
2483
+ if (layers.l2?.status === 'degraded') score -= 15;
2484
+ if (layers.l2?.status === 'inactive') score -= 25;
2485
+ if (layers.l3?.status === 'degraded') score -= 15;
2486
+ if (layers.l3?.status === 'inactive') score -= 25;
2487
+ if (layers.l4?.status === 'degraded') score -= 10;
2488
+ if (layers.l4?.status === 'inactive') score -= 20;
2489
+
2490
+ return Math.max(0, Math.min(100, score));
2491
+ }
2492
+
2493
+ async function fetchAPI(endpoint) {
2494
+ try {
2495
+ const response = await fetch(API_BASE + endpoint, {
2496
+ headers: {
2497
+ 'Authorization': 'Bearer ' + AUTH_TOKEN,
2498
+ },
2499
+ });
2500
+
2501
+ if (response.status === 401) {
2502
+ redirectToLogin();
2503
+ return null;
2504
+ }
2505
+
2506
+ if (!response.ok) {
2507
+ console.error('API Error:', response.status);
2508
+ return null;
2509
+ }
2510
+
2511
+ return await response.json();
2512
+ } catch (err) {
2513
+ console.error('Fetch error:', err);
2514
+ return null;
2515
+ }
2516
+ }
2517
+
2518
+ function redirectToLogin() {
2519
+ sessionStorage.removeItem('authToken');
2520
+ window.location.href = '/';
2521
+ }
2138
2522
 
2139
- <div class="protection-card">
2140
- <div class="protection-card-icon">\u2713</div>
2141
- <div class="protection-card-label">Approval Gate</div>
2142
- <div class="protection-card-status active" id="approvalStatus">\u2713 Active</div>
2143
- <div class="protection-card-stat" id="approvalStat">T1: 2 | T2: 3</div>
2144
- </div>
2523
+ // API Updates
2524
+ async function updateSovereignty() {
2525
+ const data = await fetchAPI('/api/sovereignty');
2526
+ if (!data) return;
2145
2527
 
2146
- <div class="protection-card">
2147
- <div class="protection-card-icon">\u{1F3AF}</div>
2148
- <div class="protection-card-label">Context Gating</div>
2149
- <div class="protection-card-status active" id="contextStatus">\u2713 Active</div>
2150
- <div class="protection-card-stat" id="contextStat">12 filtered</div>
2151
- </div>
2528
+ apiState.sovereignty = data;
2152
2529
 
2153
- <div class="protection-card">
2154
- <div class="protection-card-icon">\u26A0</div>
2155
- <div class="protection-card-label">Injection Detection</div>
2156
- <div class="protection-card-status active" id="injectionStatus">\u2713 Active</div>
2157
- <div class="protection-card-stat" id="injectionStat">3 flags today</div>
2158
- </div>
2530
+ const score = calculateSovereigntyScore(data.shr);
2531
+ const badge = document.getElementById('sovereignty-badge');
2532
+ const scoreEl = document.getElementById('sovereignty-score');
2159
2533
 
2160
- <div class="protection-card">
2161
- <div class="protection-card-icon">\u{1F4CA}</div>
2162
- <div class="protection-card-label">Behavioral Baseline</div>
2163
- <div class="protection-card-status active" id="baselineStatus">\u2713 Active</div>
2164
- <div class="protection-card-stat" id="baselineStat">0 anomalies</div>
2165
- </div>
2534
+ scoreEl.textContent = score;
2166
2535
 
2167
- <div class="protection-card">
2168
- <div class="protection-card-icon">\u{1F4CB}</div>
2169
- <div class="protection-card-label">Audit Trail</div>
2170
- <div class="protection-card-status active" id="auditStatus">\u2713 Active</div>
2171
- <div class="protection-card-stat" id="auditStat">284 entries</div>
2172
- </div>
2173
- </div>
2174
- </div>
2175
- </div>
2536
+ badge.classList.remove('degraded', 'inactive');
2537
+ if (score < 70) badge.classList.add('degraded');
2538
+ if (score < 40) badge.classList.add('inactive');
2176
2539
 
2177
- <!-- Pending Approvals Overlay -->
2178
- <div class="pending-overlay" id="pendingOverlay">
2179
- <div class="pending-overlay-header">
2180
- <div class="pending-overlay-title">Pending Approvals</div>
2181
- <button class="pending-overlay-close" onclick="closePendingOverlay()">\xD7</button>
2182
- </div>
2183
- <div class="pending-list" id="pendingList"></div>
2184
- </div>
2185
-
2186
- <!-- Threat Panel (collapsible footer) -->
2187
- <div class="threat-panel collapsed" id="threatPanel">
2188
- <div class="threat-header" onclick="toggleThreatPanel()">
2189
- <span class="threat-icon">\u26A0</span>
2190
- Recent Threats
2191
- <span id="threatCount" style="margin-left: auto; color: var(--red); font-weight: 700;">0</span>
2192
- </div>
2193
- <div class="threat-content" id="threatContent">
2194
- <div class="threat-empty">No threats detected</div>
2195
- </div>
2196
- </div>
2540
+ updateLayerCards(data.shr);
2541
+ }
2197
2542
 
2198
- <script>
2199
- (function() {
2200
- 'use strict';
2543
+ function updateLayerCards(shr) {
2544
+ if (!shr || !shr.layers) return;
2201
2545
 
2202
- // \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
2546
+ const layers = shr.layers;
2203
2547
 
2204
- const TIMEOUT_SECONDS = ${options.timeoutSeconds};
2205
- // AUTH_TOKEN: embedded token (for direct session access) or from sessionStorage (login page flow)
2206
- const EMBEDDED_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
2207
- const AUTH_TOKEN = EMBEDDED_TOKEN || (function() { try { return sessionStorage.getItem('sanctuary_token'); } catch(_) { return null; } })();
2208
- const MAX_ACTIVITY_ITEMS = 100;
2209
- const MAX_THREAT_ITEMS = 20;
2548
+ updateLayerCard('l1', layers.l1, layers.l1?.encryption || 'AES-256-GCM');
2549
+ updateLayerCard('l2', layers.l2, layers.l2?.isolation_type || 'Process-level');
2550
+ updateLayerCard('l3', layers.l3, layers.l3?.proof_system || 'Schnorr-Pedersen');
2551
+ updateLayerCard('l4', layers.l4, layers.l4?.reputation_mode || 'Weighted');
2552
+ }
2210
2553
 
2211
- // \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
2554
+ function updateLayerCard(layer, layerData, detail) {
2555
+ if (!layerData) return;
2212
2556
 
2213
- let SESSION_ID = null;
2214
- let evtSource = null;
2215
- let startTime = Date.now();
2216
- let activityCount = 0;
2217
- let threatCount = 0;
2218
- const pendingRequests = new Map();
2219
- const activityItems = [];
2220
- const threatItems = [];
2221
- let sovereigntyScore = 85;
2222
- let sessionRenewalTimer = null;
2557
+ const card = document.querySelector(\`[data-layer="\${layer}"]\`);
2558
+ if (!card) return;
2223
2559
 
2224
- // \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
2560
+ const status = layerData.status || 'inactive';
2561
+ card.classList.remove('degraded', 'inactive');
2225
2562
 
2226
- function authHeaders() {
2227
- const h = { 'Content-Type': 'application/json' };
2228
- if (AUTH_TOKEN) h['Authorization'] = 'Bearer ' + AUTH_TOKEN;
2229
- return h;
2230
- }
2563
+ if (status === 'degraded') {
2564
+ card.classList.add('degraded');
2565
+ } else if (status === 'inactive') {
2566
+ card.classList.add('inactive');
2567
+ }
2231
2568
 
2232
- function sessionQuery(url) {
2233
- if (!SESSION_ID) return url;
2234
- const sep = url.includes('?') ? '&' : '?';
2235
- return url + sep + 'session=' + SESSION_ID;
2236
- }
2569
+ document.getElementById(\`\${layer}-status\`).textContent = status.toUpperCase();
2570
+ document.getElementById(\`\${layer}-detail\`).textContent = detail;
2571
+ }
2237
2572
 
2238
- function setCookie(sessionId, maxAge) {
2239
- document.cookie = 'sanctuary_session=' + sessionId +
2240
- '; path=/; SameSite=Strict; max-age=' + maxAge;
2241
- }
2573
+ async function updateIdentity() {
2574
+ const data = await fetchAPI('/api/identity');
2575
+ if (!data) return;
2242
2576
 
2243
- async function exchangeSession() {
2244
- if (!AUTH_TOKEN) return;
2245
- try {
2246
- const resp = await fetch('/auth/session', { method: 'POST', headers: authHeaders() });
2247
- if (resp.ok) {
2248
- const data = await resp.json();
2249
- SESSION_ID = data.session_id;
2250
- var ttl = data.expires_in_seconds || 300;
2251
- // Update cookie with new session
2252
- setCookie(SESSION_ID, ttl);
2253
- // Schedule renewal at 80% of TTL
2254
- if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
2255
- sessionRenewalTimer = setTimeout(function() {
2256
- exchangeSession().then(function() { reconnectSSE(); });
2257
- }, ttl * 800);
2258
- } else if (resp.status === 401) {
2259
- // Token invalid or expired \u2014 show non-destructive re-login overlay
2260
- showSessionExpired();
2261
- }
2262
- } catch (e) {
2263
- // Network error \u2014 retry in 30s
2264
- if (sessionRenewalTimer) clearTimeout(sessionRenewalTimer);
2265
- sessionRenewalTimer = setTimeout(function() {
2266
- exchangeSession().then(function() { reconnectSSE(); });
2267
- }, 30000);
2268
- }
2269
- }
2270
-
2271
- function showSessionExpired() {
2272
- // Clear stored token
2273
- try { sessionStorage.removeItem('sanctuary_token'); } catch(_) {}
2274
- // Redirect to login page
2275
- document.cookie = 'sanctuary_session=; path=/; max-age=0';
2276
- window.location.reload();
2277
- }
2278
-
2279
- // \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
2280
-
2281
- function esc(s) {
2282
- const d = document.createElement('div');
2283
- d.textContent = String(s || '');
2284
- return d.innerHTML;
2285
- }
2286
-
2287
- function closePendingOverlay() {
2288
- document.getElementById('pendingOverlay').classList.remove('active');
2289
- }
2290
-
2291
- function toggleThreatPanel() {
2292
- document.getElementById('threatPanel').classList.toggle('collapsed');
2293
- }
2294
-
2295
- function updateUptime() {
2296
- const elapsed = Math.floor((Date.now() - startTime) / 1000);
2297
- const hours = Math.floor(elapsed / 3600);
2298
- const mins = Math.floor((elapsed % 3600) / 60);
2299
- const secs = elapsed % 60;
2300
- let uptimeStr = '';
2301
- if (hours > 0) uptimeStr += hours + 'h ';
2302
- if (mins > 0) uptimeStr += mins + 'm ';
2303
- uptimeStr += secs + 's';
2304
- document.getElementById('uptimeText').textContent = uptimeStr;
2305
- }
2306
-
2307
- // \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
2308
-
2309
- function updateSovereigntyScore(score) {
2310
- sovereigntyScore = Math.min(100, Math.max(0, score || 85));
2311
- const badge = document.getElementById('sovereigntyScore');
2312
- badge.textContent = sovereigntyScore;
2313
- badge.className = 'sovereignty-score';
2314
- if (sovereigntyScore >= 80) {
2315
- badge.classList.add('high');
2316
- } else if (sovereigntyScore >= 50) {
2317
- badge.classList.add('medium');
2318
- } else {
2319
- badge.classList.add('low');
2577
+ apiState.identity = data;
2578
+
2579
+ const primary = data.primary || {};
2580
+ document.getElementById('identity-label').textContent = primary.label || '\u2014';
2581
+ document.getElementById('identity-did').textContent = truncate(primary.did, 24);
2582
+ document.getElementById('identity-did').title = primary.did || '';
2583
+ document.getElementById('identity-pubkey').textContent = truncate(primary.publicKey, 24);
2584
+ document.getElementById('identity-pubkey').title = primary.publicKey || '';
2585
+ document.getElementById('identity-created').textContent = formatTime(primary.createdAt);
2586
+ document.getElementById('identity-count').textContent = data.identities?.length || '\u2014';
2320
2587
  }
2321
- }
2322
2588
 
2323
- // \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
2589
+ async function updateHandshakes() {
2590
+ const data = await fetchAPI('/api/handshakes');
2591
+ if (!data) return;
2324
2592
 
2325
- function addActivityItem(data) {
2326
- const {
2327
- timestamp,
2328
- tier,
2329
- tool,
2330
- outcome,
2331
- detail,
2332
- hasInjection,
2333
- isContextGated
2334
- } = data;
2335
-
2336
- const item = {
2337
- id: 'activity-' + activityCount++,
2338
- timestamp: timestamp || new Date().toISOString(),
2339
- tier: tier || 1,
2340
- tool: tool || 'unknown_tool',
2341
- outcome: outcome || 'executed',
2342
- detail: detail || '',
2343
- hasInjection: !!hasInjection,
2344
- isContextGated: !!isContextGated
2345
- };
2593
+ apiState.handshakes = data.handshakes || [];
2594
+
2595
+ document.getElementById('handshake-count').textContent = data.handshakes?.length || '0';
2596
+
2597
+ if (data.handshakes && data.handshakes.length > 0) {
2598
+ const latest = data.handshakes[0];
2599
+ document.getElementById('handshake-latest').textContent = truncate(latest.counterpartyId, 20);
2600
+ document.getElementById('handshake-latest').title = latest.counterpartyId || '';
2601
+ document.getElementById('handshake-tier').textContent = (latest.trustTier || 'Unverified').toUpperCase();
2602
+ document.getElementById('handshake-time').textContent = formatTime(latest.completedAt);
2603
+ } else {
2604
+ document.getElementById('handshake-latest').textContent = '\u2014';
2605
+ document.getElementById('handshake-tier').textContent = 'Unverified';
2606
+ document.getElementById('handshake-time').textContent = '\u2014';
2607
+ }
2346
2608
 
2347
- activityItems.unshift(item);
2348
- if (activityItems.length > MAX_ACTIVITY_ITEMS) {
2349
- activityItems.pop();
2609
+ updateHandshakeTable(data.handshakes || []);
2350
2610
  }
2351
2611
 
2352
- renderActivityFeed();
2353
- }
2612
+ function updateHandshakeTable(handshakes) {
2613
+ const table = document.getElementById('handshake-table');
2354
2614
 
2355
- function renderActivityFeed() {
2356
- const list = document.getElementById('activityList');
2615
+ if (!handshakes || handshakes.length === 0) {
2616
+ table.innerHTML = '<div class="table-empty">No handshakes completed yet</div>';
2617
+ return;
2618
+ }
2357
2619
 
2358
- if (activityItems.length === 0) {
2359
- list.innerHTML = '<div class="activity-empty"><div class="activity-empty-icon">\u2192</div><div class="activity-empty-text">Waiting for activity...</div></div>';
2360
- return;
2620
+ table.innerHTML = handshakes
2621
+ .map(
2622
+ (hs) => \`
2623
+ <div class="table-row">
2624
+ <div class="table-cell strong">\${esc(truncate(hs.counterpartyId, 24))}</div>
2625
+ <div class="table-cell">\${esc(hs.trustTier || 'Unverified')}</div>
2626
+ <div class="table-cell">\${esc(hs.sovereigntyLevel || '\u2014')}</div>
2627
+ <div class="table-cell">\${hs.verified ? 'Yes' : 'No'}</div>
2628
+ <div class="table-cell">\${formatTime(hs.completedAt)}</div>
2629
+ <div class="table-cell">\${formatTime(hs.expiresAt)}</div>
2630
+ </div>
2631
+ \`
2632
+ )
2633
+ .join('');
2361
2634
  }
2362
2635
 
2363
- list.innerHTML = '';
2364
- for (const item of activityItems) {
2365
- const tr = document.createElement('div');
2366
- tr.className = 'activity-item';
2367
- tr.id = item.id;
2368
-
2369
- const time = new Date(item.timestamp);
2370
- const timeStr = time.toLocaleTimeString();
2371
-
2372
- const tierClass = 't' + item.tier;
2373
- const outcomeClass = item.outcome === 'denied' ? 'outcome denied' : 'outcome';
2374
-
2375
- let icon = '\u25CF';
2376
- if (item.isContextGated) icon = '\u{1F3AF}';
2377
- else if (item.hasInjection) icon = '\u26A0';
2378
- else if (item.outcome === 'denied') icon = '\u2717';
2379
- else icon = '\u2713';
2380
-
2381
- tr.innerHTML =
2382
- '<div class="activity-item-icon">' + esc(icon) + '</div>' +
2383
- '<div class="activity-item-content">' +
2384
- '<div class="activity-time">' + esc(timeStr) + '</div>' +
2385
- '<div class="activity-main">' +
2386
- '<span class="activity-tier ' + tierClass + '">T' + item.tier + '</span>' +
2387
- '<span class="activity-tool">' + esc(item.tool) + '</span>' +
2388
- '<span class="activity-outcome ' + (outcomeClass === 'outcome denied' ? 'denied' : '') + '">' + (item.outcome === 'denied' ? '\u2717 denied' : '\u2713 allowed') + '</span>' +
2389
- '</div>' +
2390
- '<div class="activity-detail">' + esc(item.detail) + '</div>' +
2391
- '</div>' +
2392
- '';
2393
-
2394
- tr.addEventListener('click', () => {
2395
- tr.classList.toggle('expanded');
2396
- });
2636
+ async function updateSHR() {
2637
+ const data = await fetchAPI('/api/shr');
2638
+ if (!data) return;
2397
2639
 
2398
- list.appendChild(tr);
2640
+ apiState.shr = data;
2641
+ renderSHRViewer(data);
2399
2642
  }
2400
- }
2401
2643
 
2402
- // \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
2644
+ function renderSHRViewer(shr) {
2645
+ const viewer = document.getElementById('shr-viewer');
2403
2646
 
2404
- function addPendingRequest(data) {
2405
- const {
2406
- request_id,
2407
- operation,
2408
- tier,
2409
- reason,
2410
- context,
2411
- timestamp
2412
- } = data;
2413
-
2414
- const pending = {
2415
- id: request_id,
2416
- operation: operation || 'unknown',
2417
- tier: tier || 1,
2418
- reason: reason || '',
2419
- context: context || {},
2420
- timestamp: timestamp || new Date().toISOString(),
2421
- remaining: TIMEOUT_SECONDS
2422
- };
2647
+ if (!shr) {
2648
+ viewer.innerHTML = '<div class="empty-state">No SHR available</div>';
2649
+ return;
2650
+ }
2423
2651
 
2424
- pendingRequests.set(request_id, pending);
2425
- updatePendingUI();
2426
- }
2652
+ let html = '';
2653
+
2654
+ // Implementation
2655
+ html += \`
2656
+ <div class="shr-section">
2657
+ <div class="shr-section-header">
2658
+ <div class="shr-toggle">\u25BC</div>
2659
+ <div>Implementation</div>
2660
+ </div>
2661
+ <div class="shr-section-content">
2662
+ <div class="shr-item">
2663
+ <div class="shr-key">sanctuary_version:</div>
2664
+ <div class="shr-value">\${esc(shr.implementation?.sanctuary_version || '\u2014')}</div>
2665
+ </div>
2666
+ <div class="shr-item">
2667
+ <div class="shr-key">node_version:</div>
2668
+ <div class="shr-value">\${esc(shr.implementation?.node_version || '\u2014')}</div>
2669
+ </div>
2670
+ <div class="shr-item">
2671
+ <div class="shr-key">generated_by:</div>
2672
+ <div class="shr-value">\${esc(shr.implementation?.generated_by || '\u2014')}</div>
2673
+ </div>
2674
+ </div>
2675
+ </div>
2676
+ \`;
2677
+
2678
+ // Metadata
2679
+ html += \`
2680
+ <div class="shr-section">
2681
+ <div class="shr-section-header">
2682
+ <div class="shr-toggle">\u25BC</div>
2683
+ <div>Metadata</div>
2684
+ </div>
2685
+ <div class="shr-section-content">
2686
+ <div class="shr-item">
2687
+ <div class="shr-key">instance_id:</div>
2688
+ <div class="shr-value">\${esc(truncate(shr.instance_id, 20))}</div>
2689
+ </div>
2690
+ <div class="shr-item">
2691
+ <div class="shr-key">generated_at:</div>
2692
+ <div class="shr-value">\${formatTime(shr.generated_at)}</div>
2693
+ </div>
2694
+ <div class="shr-item">
2695
+ <div class="shr-key">expires_at:</div>
2696
+ <div class="shr-value">\${formatTime(shr.expires_at)}</div>
2697
+ </div>
2698
+ </div>
2699
+ </div>
2700
+ \`;
2701
+
2702
+ // Layers
2703
+ if (shr.layers) {
2704
+ html += \`<div class="shr-section">
2705
+ <div class="shr-section-header">
2706
+ <div class="shr-toggle">\u25BC</div>
2707
+ <div>Layers</div>
2708
+ </div>
2709
+ <div class="shr-section-content">
2710
+ \`;
2711
+
2712
+ for (const [key, layer] of Object.entries(shr.layers)) {
2713
+ html += \`
2714
+ <div style="margin-bottom: 12px;">
2715
+ <div style="color: var(--blue); font-weight: 600; margin-bottom: 4px;">\${esc(key)}</div>
2716
+ <div style="padding-left: 12px;">
2717
+ \`;
2718
+
2719
+ for (const [lkey, lvalue] of Object.entries(layer || {})) {
2720
+ const displayValue =
2721
+ typeof lvalue === 'boolean'
2722
+ ? lvalue
2723
+ ? 'true'
2724
+ : 'false'
2725
+ : esc(String(lvalue));
2726
+ html += \`
2727
+ <div class="shr-item">
2728
+ <div class="shr-key">\${esc(lkey)}:</div>
2729
+ <div class="shr-value">\${displayValue}</div>
2730
+ </div>
2731
+ \`;
2732
+ }
2427
2733
 
2428
- function removePendingRequest(id) {
2429
- pendingRequests.delete(id);
2430
- updatePendingUI();
2431
- }
2734
+ html += \`
2735
+ </div>
2736
+ </div>
2737
+ \`;
2738
+ }
2739
+
2740
+ html += \`
2741
+ </div>
2742
+ </div>
2743
+ \`;
2744
+ }
2745
+
2746
+ // Capabilities
2747
+ if (shr.capabilities) {
2748
+ html += \`
2749
+ <div class="shr-section">
2750
+ <div class="shr-section-header">
2751
+ <div class="shr-toggle">\u25BC</div>
2752
+ <div>Capabilities</div>
2753
+ </div>
2754
+ <div class="shr-section-content">
2755
+ \`;
2756
+
2757
+ for (const [key, value] of Object.entries(shr.capabilities)) {
2758
+ const displayValue = value ? 'true' : 'false';
2759
+ html += \`
2760
+ <div class="shr-item">
2761
+ <div class="shr-key">\${esc(key)}:</div>
2762
+ <div class="shr-value">\${displayValue}</div>
2763
+ </div>
2764
+ \`;
2765
+ }
2766
+
2767
+ html += \`
2768
+ </div>
2769
+ </div>
2770
+ \`;
2771
+ }
2772
+
2773
+ // Signature
2774
+ html += \`
2775
+ <div class="shr-section">
2776
+ <div class="shr-section-header">
2777
+ <div class="shr-toggle">\u25BC</div>
2778
+ <div>Signature</div>
2779
+ </div>
2780
+ <div class="shr-section-content">
2781
+ <div class="shr-item">
2782
+ <div class="shr-key">signed_by:</div>
2783
+ <div class="shr-value">\${esc(truncate(shr.signed_by, 20))}</div>
2784
+ </div>
2785
+ <div class="shr-item">
2786
+ <div class="shr-key">signature:</div>
2787
+ <div class="shr-value">\${esc(truncate(shr.signature, 32))}</div>
2788
+ </div>
2789
+ </div>
2790
+ </div>
2791
+ \`;
2792
+
2793
+ viewer.innerHTML = html;
2794
+
2795
+ // Add collapse functionality
2796
+ document.querySelectorAll('.shr-section-header').forEach((header) => {
2797
+ header.addEventListener('click', () => {
2798
+ header.closest('.shr-section').classList.toggle('collapsed');
2799
+ });
2800
+ });
2801
+ }
2432
2802
 
2433
- function updatePendingUI() {
2434
- const count = pendingRequests.size;
2435
- const badge = document.getElementById('pendingBadge');
2803
+ async function updateStatus() {
2804
+ const data = await fetchAPI('/api/status');
2805
+ if (!data) return;
2436
2806
 
2437
- if (count > 0) {
2438
- badge.classList.remove('hidden');
2439
- badge.textContent = count;
2440
- document.getElementById('pendingOverlay').classList.add('active');
2441
- } else {
2442
- badge.classList.add('hidden');
2443
- document.getElementById('pendingOverlay').classList.remove('active');
2807
+ apiState.status = data;
2808
+
2809
+ document.getElementById('protections-count').textContent = data.protectionsCount || '0';
2810
+ document.getElementById('uptime-value').textContent = formatUptime(data.uptime);
2811
+
2812
+ const connectionStatus = document.getElementById('connection-status');
2813
+ connectionStatus.classList.toggle('disconnected', !data.connected);
2444
2814
  }
2445
2815
 
2446
- renderPendingList();
2447
- }
2816
+ function formatUptime(seconds) {
2817
+ if (!seconds) return '\u2014';
2818
+ const hours = Math.floor(seconds / 3600);
2819
+ const minutes = Math.floor((seconds % 3600) / 60);
2820
+ if (hours > 0) return \`\${hours}h \${minutes}m\`;
2821
+ return \`\${minutes}m\`;
2822
+ }
2448
2823
 
2449
- function renderPendingList() {
2450
- const list = document.getElementById('pendingList');
2451
- list.innerHTML = '';
2824
+ // SSE Setup
2825
+ function setupSSE() {
2826
+ const eventSource = new EventSource(API_BASE + '/api/events', {
2827
+ headers: {
2828
+ 'Authorization': 'Bearer ' + AUTH_TOKEN,
2829
+ },
2830
+ });
2452
2831
 
2453
- for (const [id, req] of pendingRequests) {
2454
- const item = document.createElement('div');
2455
- item.className = 'pending-item';
2832
+ eventSource.addEventListener('init', (e) => {
2833
+ console.log('Connected to SSE');
2834
+ });
2456
2835
 
2457
- const tier = req.tier || 1;
2458
- const tierClass = 'tier' + tier;
2459
- const pct = Math.max(0, Math.min(100, (req.remaining / TIMEOUT_SECONDS) * 100));
2460
- const isUrgent = req.remaining <= 30;
2836
+ eventSource.addEventListener('sovereignty-update', () => {
2837
+ updateSovereignty();
2838
+ });
2461
2839
 
2462
- item.innerHTML =
2463
- '<div class="pending-item-header">' +
2464
- '<div class="pending-item-op">' + esc(req.operation) + '</div>' +
2465
- '<div class="pending-item-tier ' + tierClass + '">T' + tier + '</div>' +
2466
- '</div>' +
2467
- '<div class="pending-item-reason">' + esc(req.reason) + '</div>' +
2468
- '<div class="pending-item-timer ' + (isUrgent ? 'urgent' : '') + '">' +
2469
- '<div class="pending-item-timer-bar">' +
2470
- '<div class="pending-item-timer-fill" style="width: ' + pct + '%"></div>' +
2471
- '</div>' +
2472
- '<span id="timer-' + id + '">' + req.remaining + 's</span>' +
2473
- '</div>' +
2474
- '<div class="pending-item-actions">' +
2475
- '<button class="btn btn-approve" onclick="handleApprove('' + id + '')">Approve</button>' +
2476
- '<button class="btn btn-deny" onclick="handleDeny('' + id + '')">Deny</button>' +
2477
- '</div>' +
2478
- '';
2840
+ eventSource.addEventListener('handshake-update', () => {
2841
+ updateHandshakes();
2842
+ });
2479
2843
 
2480
- list.appendChild(item);
2481
- }
2482
- }
2844
+ eventSource.addEventListener('tool-call', (e) => {
2845
+ const data = JSON.parse(e.data);
2846
+ addActivityItem({
2847
+ type: 'tool-call',
2848
+ title: 'Tool Call',
2849
+ content: data.toolName,
2850
+ timestamp: new Date().toISOString(),
2851
+ });
2852
+ });
2483
2853
 
2484
- window.handleApprove = function(id) {
2485
- fetch('/api/approve/' + id, { method: 'POST', headers: authHeaders() }).then(() => {
2486
- removePendingRequest(id);
2487
- }).catch(() => {});
2488
- };
2854
+ eventSource.addEventListener('context-gate-decision', (e) => {
2855
+ const data = JSON.parse(e.data);
2856
+ addActivityItem({
2857
+ type: 'context-gate',
2858
+ title: 'Context Gate',
2859
+ content: data.decision,
2860
+ timestamp: new Date().toISOString(),
2861
+ });
2862
+ });
2489
2863
 
2490
- window.handleDeny = function(id) {
2491
- fetch('/api/deny/' + id, { method: 'POST', headers: authHeaders() }).then(() => {
2492
- removePendingRequest(id);
2493
- }).catch(() => {});
2494
- };
2864
+ eventSource.addEventListener('injection-alert', (e) => {
2865
+ const data = JSON.parse(e.data);
2866
+ addActivityItem({
2867
+ type: 'injection',
2868
+ title: 'Injection Alert',
2869
+ content: data.pattern,
2870
+ timestamp: new Date().toISOString(),
2871
+ });
2872
+ addThreatAlert(data);
2873
+ });
2495
2874
 
2496
- // \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
2497
-
2498
- function addThreat(data) {
2499
- const {
2500
- timestamp,
2501
- severity,
2502
- type,
2503
- details
2504
- } = data;
2505
-
2506
- const threat = {
2507
- id: 'threat-' + threatCount++,
2508
- timestamp: timestamp || new Date().toISOString(),
2509
- severity: severity || 'medium',
2510
- type: type || 'unknown',
2511
- details: details || ''
2512
- };
2875
+ eventSource.addEventListener('pending-request', (e) => {
2876
+ const data = JSON.parse(e.data);
2877
+ addPendingRequest(data);
2878
+ });
2513
2879
 
2514
- threatItems.unshift(threat);
2515
- if (threatItems.length > MAX_THREAT_ITEMS) {
2516
- threatItems.pop();
2517
- }
2880
+ eventSource.addEventListener('request-resolved', (e) => {
2881
+ const data = JSON.parse(e.data);
2882
+ removePendingRequest(data.requestId);
2883
+ });
2518
2884
 
2519
- if (threatCount > 0) {
2520
- document.getElementById('threatPanel').classList.remove('collapsed');
2885
+ eventSource.onerror = () => {
2886
+ console.error('SSE error');
2887
+ setTimeout(setupSSE, 5000);
2888
+ };
2521
2889
  }
2522
2890
 
2523
- renderThreats();
2524
- }
2891
+ // Activity Feed
2892
+ function addActivityItem(item) {
2893
+ activityLog.unshift(item);
2894
+ if (activityLog.length > maxActivityItems) {
2895
+ activityLog.pop();
2896
+ }
2525
2897
 
2526
- function renderThreats() {
2527
- const content = document.getElementById('threatContent');
2528
- const badge = document.getElementById('threatCount');
2898
+ const feed = document.getElementById('activity-feed');
2899
+ const html = \`
2900
+ <div class="activity-item \${item.type}">
2901
+ <div class="activity-type">\${esc(item.title)}</div>
2902
+ <div class="activity-content">\${esc(item.content)}</div>
2903
+ <div class="activity-time">\${formatTime(item.timestamp)}</div>
2904
+ </div>
2905
+ \`;
2529
2906
 
2530
- if (threatItems.length === 0) {
2531
- content.innerHTML = '<div class="threat-empty">No threats detected</div>';
2532
- badge.textContent = '0';
2533
- return;
2534
- }
2907
+ if (feed.querySelector('.empty-state')) {
2908
+ feed.innerHTML = '';
2909
+ }
2535
2910
 
2536
- badge.textContent = threatItems.length;
2537
- content.innerHTML = '';
2911
+ feed.insertAdjacentHTML('afterbegin', html);
2538
2912
 
2539
- for (const threat of threatItems) {
2540
- const div = document.createElement('div');
2541
- div.className = 'threat-item';
2542
- const time = new Date(threat.timestamp).toLocaleTimeString();
2543
- div.innerHTML =
2544
- '<div style="margin-bottom: 3px;">' +
2545
- '<span class="threat-item-type">' + esc(threat.type) + '</span>' +
2546
- '<span style="font-size: 10px; color: var(--text-secondary); margin-left: 6px;">' + esc(time) + '</span>' +
2547
- '</div>' +
2548
- '<div>' + esc(threat.details) + '</div>' +
2549
- '';
2550
- content.appendChild(div);
2913
+ if (feed.children.length > maxActivityItems) {
2914
+ feed.lastChild.remove();
2915
+ }
2551
2916
  }
2552
- }
2553
-
2554
- // \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
2555
2917
 
2556
- function reconnectSSE() {
2557
- if (evtSource) evtSource.close();
2558
- connect();
2559
- }
2918
+ // Pending Requests
2919
+ function addPendingRequest(request) {
2920
+ pendingRequests.set(request.requestId, {
2921
+ id: request.requestId,
2922
+ title: request.title,
2923
+ details: request.details,
2924
+ expiresAt: new Date(Date.now() + TIMEOUT_SECONDS * 1000),
2925
+ });
2560
2926
 
2561
- function connect() {
2562
- evtSource = new EventSource(sessionQuery('/events'));
2927
+ updatePendingDisplay();
2928
+ }
2563
2929
 
2564
- evtSource.onopen = () => {
2565
- document.getElementById('statusDot').classList.remove('disconnected');
2566
- };
2930
+ function removePendingRequest(requestId) {
2931
+ pendingRequests.delete(requestId);
2932
+ updatePendingDisplay();
2933
+ }
2567
2934
 
2568
- evtSource.onerror = () => {
2569
- document.getElementById('statusDot').classList.add('disconnected');
2570
- };
2935
+ function updatePendingDisplay() {
2936
+ const badge = document.getElementById('pending-item-badge');
2937
+ const count = pendingRequests.size;
2571
2938
 
2572
- evtSource.addEventListener('init', (e) => {
2573
- const data = JSON.parse(e.data);
2574
- if (data.baseline) {
2575
- updateBaseline(data.baseline);
2576
- }
2577
- if (data.policy) {
2578
- updatePolicy(data.policy);
2939
+ if (count > 0) {
2940
+ document.getElementById('pending-count').textContent = count;
2941
+ badge.style.display = 'flex';
2942
+ } else {
2943
+ badge.style.display = 'none';
2579
2944
  }
2580
- if (data.pending) {
2581
- data.pending.forEach(addPendingRequest);
2945
+
2946
+ const overlay = document.getElementById('pending-overlay');
2947
+ const items = document.getElementById('pending-items');
2948
+
2949
+ if (count === 0) {
2950
+ items.innerHTML = '';
2951
+ overlay.classList.remove('show');
2952
+ return;
2582
2953
  }
2583
- });
2584
2954
 
2585
- evtSource.addEventListener('pending-request', (e) => {
2586
- const data = JSON.parse(e.data);
2587
- addPendingRequest(data);
2588
- });
2955
+ let html = '';
2956
+ for (const req of pendingRequests.values()) {
2957
+ const remaining = Math.max(0, Math.floor((req.expiresAt - Date.now()) / 1000));
2958
+ html += \`
2959
+ <div class="pending-item">
2960
+ <div class="pending-title">\${esc(req.title)}</div>
2961
+ <div class="pending-countdown">Expires in \${remaining}s</div>
2962
+ <div class="pending-actions">
2963
+ <button class="pending-btn pending-approve" data-id="\${req.id}">Approve</button>
2964
+ <button class="pending-btn pending-deny" data-id="\${req.id}">Deny</button>
2965
+ </div>
2966
+ </div>
2967
+ \`;
2968
+ }
2589
2969
 
2590
- evtSource.addEventListener('request-resolved', (e) => {
2591
- const data = JSON.parse(e.data);
2592
- removePendingRequest(data.request_id);
2593
- });
2970
+ items.innerHTML = html;
2594
2971
 
2595
- evtSource.addEventListener('tool-call', (e) => {
2596
- const data = JSON.parse(e.data);
2597
- addActivityItem({
2598
- timestamp: data.timestamp,
2599
- tier: data.tier || 1,
2600
- tool: data.tool || 'unknown',
2601
- outcome: data.outcome || 'executed',
2602
- detail: data.detail || ''
2972
+ document.querySelectorAll('.pending-approve').forEach((btn) => {
2973
+ btn.addEventListener('click', async () => {
2974
+ const id = btn.getAttribute('data-id');
2975
+ await fetchAPI(\`/api/approve/\${id}\`);
2976
+ });
2603
2977
  });
2604
- });
2605
2978
 
2606
- evtSource.addEventListener('context-gate-decision', (e) => {
2607
- const data = JSON.parse(e.data);
2608
- addActivityItem({
2609
- timestamp: data.timestamp,
2610
- tier: data.tier || 1,
2611
- tool: data.tool || 'unknown',
2612
- outcome: data.outcome || 'gated',
2613
- detail: data.fields_filtered ? 'Filtered ' + data.fields_filtered + ' fields' : data.reason || '',
2614
- isContextGated: true
2979
+ document.querySelectorAll('.pending-deny').forEach((btn) => {
2980
+ btn.addEventListener('click', async () => {
2981
+ const id = btn.getAttribute('data-id');
2982
+ await fetchAPI(\`/api/deny/\${id}\`);
2983
+ });
2615
2984
  });
2616
- });
2985
+ }
2617
2986
 
2618
- evtSource.addEventListener('injection-alert', (e) => {
2619
- const data = JSON.parse(e.data);
2620
- addActivityItem({
2621
- timestamp: data.timestamp,
2622
- tier: data.tier || 2,
2623
- tool: data.tool || 'unknown',
2624
- outcome: data.allowed ? 'allowed' : 'denied',
2625
- detail: data.signal || 'Injection detected',
2626
- hasInjection: true
2627
- });
2628
- addThreat({
2629
- timestamp: data.timestamp,
2630
- severity: data.severity || 'medium',
2631
- type: 'Injection Alert',
2632
- details: data.signal || 'Suspicious pattern detected'
2633
- });
2634
- });
2987
+ // Threat Panel
2988
+ function addThreatAlert(alert) {
2989
+ const panel = document.querySelector('.threat-panel');
2990
+ const content = document.getElementById('threat-alerts');
2635
2991
 
2636
- evtSource.addEventListener('protection-status', (e) => {
2637
- const data = JSON.parse(e.data);
2638
- updateProtectionStatus(data);
2639
- });
2992
+ if (content.querySelector('.empty-state')) {
2993
+ content.innerHTML = '';
2994
+ }
2640
2995
 
2641
- evtSource.addEventListener('audit-entry', (e) => {
2642
- const data = JSON.parse(e.data);
2643
- // Audit entries don't show in activity by default, but we could add them
2644
- });
2996
+ panel.classList.remove('collapsed');
2645
2997
 
2646
- evtSource.addEventListener('baseline-update', (e) => {
2647
- const data = JSON.parse(e.data);
2648
- updateBaseline(data);
2649
- });
2650
- }
2998
+ const html = \`
2999
+ <div class="threat-alert">
3000
+ <div class="threat-type">\${esc(alert.type || 'Injection Alert')}</div>
3001
+ <div class="threat-message">\${esc(alert.message || alert.pattern || '\u2014')}</div>
3002
+ </div>
3003
+ \`;
2651
3004
 
2652
- function updateBaseline(baseline) {
2653
- if (!baseline) return;
2654
- // Update baseline-derived stats if needed
2655
- }
3005
+ content.insertAdjacentHTML('afterbegin', html);
2656
3006
 
2657
- function updatePolicy(policy) {
2658
- if (!policy) return;
2659
- // Update policy-derived stats
2660
- if (policy.approval_channel) {
2661
- // Policy info updated
3007
+ const alerts = content.querySelectorAll('.threat-alert');
3008
+ if (alerts.length > 10) {
3009
+ alerts[alerts.length - 1].remove();
3010
+ }
2662
3011
  }
2663
- }
2664
3012
 
2665
- function updateProtectionStatus(status) {
2666
- if (status.sovereignty_score !== undefined) {
2667
- updateSovereigntyScore(status.sovereignty_score);
2668
- }
2669
- if (status.active_protections !== undefined) {
2670
- document.getElementById('activeProtections').textContent = status.active_protections;
2671
- }
2672
- // Update individual protection cards
2673
- if (status.encryption !== undefined) {
2674
- const el = document.getElementById('encryptionStatus');
2675
- el.className = 'protection-card-status ' + (status.encryption ? 'active' : 'inactive');
2676
- el.textContent = status.encryption ? '\u2713 Active' : '\u2717 Inactive';
2677
- }
2678
- if (status.approval_gate !== undefined) {
2679
- const el = document.getElementById('approvalStatus');
2680
- el.className = 'protection-card-status ' + (status.approval_gate ? 'active' : 'inactive');
2681
- el.textContent = status.approval_gate ? '\u2713 Active' : '\u2717 Inactive';
2682
- }
2683
- if (status.context_gating !== undefined) {
2684
- const el = document.getElementById('contextStatus');
2685
- el.className = 'protection-card-status ' + (status.context_gating ? 'active' : 'inactive');
2686
- el.textContent = status.context_gating ? '\u2713 Active' : '\u2717 Inactive';
2687
- }
2688
- if (status.injection_detection !== undefined) {
2689
- const el = document.getElementById('injectionStatus');
2690
- el.className = 'protection-card-status ' + (status.injection_detection ? 'active' : 'inactive');
2691
- el.textContent = status.injection_detection ? '\u2713 Active' : '\u2717 Inactive';
2692
- }
2693
- if (status.baseline !== undefined) {
2694
- const el = document.getElementById('baselineStatus');
2695
- el.className = 'protection-card-status ' + (status.baseline ? 'active' : 'inactive');
2696
- el.textContent = status.baseline ? '\u2713 Active' : '\u2717 Inactive';
2697
- }
2698
- if (status.audit_trail !== undefined) {
2699
- const el = document.getElementById('auditStatus');
2700
- el.className = 'protection-card-status ' + (status.audit_trail ? 'active' : 'inactive');
2701
- el.textContent = status.audit_trail ? '\u2713 Active' : '\u2717 Inactive';
2702
- }
2703
- }
3013
+ // Threat Panel Toggle
3014
+ document.querySelector('.threat-header').addEventListener('click', () => {
3015
+ document.querySelector('.threat-panel').classList.toggle('collapsed');
3016
+ });
2704
3017
 
2705
- // \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
3018
+ // SHR Copy Button
3019
+ document.getElementById('copy-shr-btn').addEventListener('click', async () => {
3020
+ if (!apiState.shr) return;
2706
3021
 
2707
- (async function init() {
2708
- await exchangeSession();
2709
- // Clean legacy ?token= from URL
2710
- if (window.location.search.includes('token=')) {
2711
- window.history.replaceState({}, '', window.location.pathname);
2712
- }
2713
- connect();
3022
+ const json = JSON.stringify(apiState.shr, null, 2);
3023
+ try {
3024
+ await navigator.clipboard.writeText(json);
3025
+ const btn = document.getElementById('copy-shr-btn');
3026
+ const original = btn.textContent;
3027
+ btn.textContent = 'Copied!';
3028
+ setTimeout(() => {
3029
+ btn.textContent = original;
3030
+ }, 2000);
3031
+ } catch (err) {
3032
+ console.error('Copy failed:', err);
3033
+ }
3034
+ });
2714
3035
 
2715
- // Start uptime ticker
2716
- setInterval(updateUptime, 1000);
2717
- updateUptime();
3036
+ // Pending Overlay Toggle
3037
+ document.getElementById('pending-item-badge').addEventListener('click', () => {
3038
+ document.getElementById('pending-overlay').classList.toggle('show');
3039
+ });
2718
3040
 
2719
- // Pending request countdown timer
2720
- setInterval(() => {
2721
- for (const [id, req] of pendingRequests) {
2722
- req.remaining = Math.max(0, req.remaining - 1);
2723
- const el = document.getElementById('timer-' + id);
2724
- if (el) {
2725
- el.textContent = req.remaining + 's';
2726
- }
3041
+ // Initialize
3042
+ async function initialize() {
3043
+ if (!AUTH_TOKEN) {
3044
+ redirectToLogin();
3045
+ return;
2727
3046
  }
2728
- }, 1000);
2729
3047
 
2730
- // Load initial status
2731
- try {
2732
- const resp = await fetch('/api/status', { headers: authHeaders() });
2733
- if (resp.ok) {
2734
- const status = await resp.json();
2735
- if (status.baseline) updateBaseline(status.baseline);
2736
- if (status.policy) updatePolicy(status.policy);
2737
- }
2738
- } catch (e) {
2739
- // Ignore
2740
- }
2741
- })();
3048
+ // Initial data fetch
3049
+ await Promise.all([
3050
+ updateSovereignty(),
3051
+ updateIdentity(),
3052
+ updateHandshakes(),
3053
+ updateSHR(),
3054
+ updateStatus(),
3055
+ ]);
3056
+
3057
+ // Setup SSE for real-time updates
3058
+ setupSSE();
2742
3059
 
2743
- })();
2744
- </script>
3060
+ // Refresh status periodically
3061
+ setInterval(updateStatus, 30000);
3062
+ }
2745
3063
 
3064
+ // Start
3065
+ initialize();
3066
+ </script>
2746
3067
  </body>
2747
3068
  </html>`;
2748
3069
  }
@@ -7633,15 +7954,6 @@ function generateSHR(identityId, opts) {
7633
7954
  mitigation: "TEE attestation planned for a future release"
7634
7955
  });
7635
7956
  }
7636
- if (config.disclosure.proof_system === "commitment-only") {
7637
- degradations.push({
7638
- layer: "l3",
7639
- code: "COMMITMENT_ONLY",
7640
- severity: "info",
7641
- description: "Commitment schemes only (no ZK proofs)",
7642
- mitigation: "ZK proof support planned for future release"
7643
- });
7644
- }
7645
7957
  const body = {
7646
7958
  shr_version: "1.0",
7647
7959
  implementation: {
@@ -7667,9 +7979,9 @@ function generateSHR(identityId, opts) {
7667
7979
  attestation_available: config.execution.attestation
7668
7980
  },
7669
7981
  l3: {
7670
- status: config.disclosure.proof_system === "commitment-only" ? "degraded" : "active",
7982
+ status: "active",
7671
7983
  proof_system: config.disclosure.proof_system,
7672
- selective_disclosure: config.disclosure.proof_system !== "commitment-only"
7984
+ selective_disclosure: true
7673
7985
  },
7674
7986
  l4: {
7675
7987
  status: "active",
@@ -7882,7 +8194,7 @@ function extractAuthorizationSignals(body) {
7882
8194
  behavioral_baseline_active: false,
7883
8195
  // Would need explicit field in SHR v1.1
7884
8196
  identity_verified: l1.identity_type === "ed25519" || l1.identity_type !== "none",
7885
- zero_knowledge_capable: l3.status === "active" && l3.proof_system !== "commitment-only",
8197
+ zero_knowledge_capable: l3.status === "active",
7886
8198
  selective_disclosure_active: l3.selective_disclosure,
7887
8199
  reputation_portable: l4.reputation_portable,
7888
8200
  handshake_capable: body.capabilities.handshake
@@ -7960,14 +8272,6 @@ function generateAuthorizationConstraints(body, _degradations) {
7960
8272
  priority: "high"
7961
8273
  });
7962
8274
  }
7963
- if (layers.l3.proof_system === "commitment-only") {
7964
- constraints.push({
7965
- type: "restricted_scope",
7966
- description: "No zero-knowledge proofs available \u2014 entire state context may be visible",
7967
- rationale: "Proof system is commitment-only (no ZK)",
7968
- priority: "medium"
7969
- });
7970
- }
7971
8275
  if (layers.l4.status === "degraded") {
7972
8276
  constraints.push({
7973
8277
  type: "known_agents_only",
@@ -8301,6 +8605,155 @@ function deriveTrustTier(level) {
8301
8605
  }
8302
8606
  }
8303
8607
 
8608
+ // src/handshake/attestation.ts
8609
+ init_encoding();
8610
+ init_key_derivation();
8611
+ init_encoding();
8612
+ var ATTESTATION_VERSION = "1.0";
8613
+ function deriveTrustTier2(level) {
8614
+ switch (level) {
8615
+ case "full":
8616
+ return "verified-sovereign";
8617
+ case "degraded":
8618
+ return "verified-degraded";
8619
+ default:
8620
+ return "unverified";
8621
+ }
8622
+ }
8623
+ function generateAttestation(opts) {
8624
+ const {
8625
+ attesterSHR,
8626
+ subjectSHR,
8627
+ verificationResult,
8628
+ mutual = false,
8629
+ identityManager,
8630
+ masterKey,
8631
+ identityId
8632
+ } = opts;
8633
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
8634
+ if (!identity) {
8635
+ return { error: "No identity available for signing attestation" };
8636
+ }
8637
+ const now = /* @__PURE__ */ new Date();
8638
+ const attesterExpiry = new Date(attesterSHR.body.expires_at);
8639
+ const subjectExpiry = new Date(subjectSHR.body.expires_at);
8640
+ const earliestExpiry = attesterExpiry < subjectExpiry ? attesterExpiry : subjectExpiry;
8641
+ const sovereigntyLevel = verificationResult.valid ? verificationResult.sovereignty_level : "unverified";
8642
+ const body = {
8643
+ attestation_version: ATTESTATION_VERSION,
8644
+ attester_id: attesterSHR.body.instance_id,
8645
+ subject_id: subjectSHR.body.instance_id,
8646
+ attester_shr: attesterSHR,
8647
+ subject_shr: subjectSHR,
8648
+ verification: {
8649
+ subject_shr_valid: verificationResult.valid,
8650
+ subject_sovereignty_level: sovereigntyLevel,
8651
+ subject_trust_tier: deriveTrustTier2(sovereigntyLevel),
8652
+ mutual,
8653
+ errors: verificationResult.errors,
8654
+ warnings: verificationResult.warnings
8655
+ },
8656
+ attested_at: now.toISOString(),
8657
+ expires_at: earliestExpiry.toISOString()
8658
+ };
8659
+ const canonical = JSON.stringify(deepSortKeys(body));
8660
+ const payload = stringToBytes(canonical);
8661
+ const encryptionKey = derivePurposeKey(masterKey, "identity-encryption");
8662
+ const signatureBytes = sign(
8663
+ payload,
8664
+ identity.encrypted_private_key,
8665
+ encryptionKey
8666
+ );
8667
+ const summary = generateSummary(body);
8668
+ return {
8669
+ body,
8670
+ signed_by: identity.public_key,
8671
+ signature: toBase64url(signatureBytes),
8672
+ summary
8673
+ };
8674
+ }
8675
+ function layerLine(label, status) {
8676
+ const icon = status === "active" ? "\u2713" : status === "degraded" ? "~" : "x";
8677
+ return ` ${icon} ${label}: ${status}`;
8678
+ }
8679
+ function generateSummary(body) {
8680
+ const v = body.verification;
8681
+ const sLayers = body.subject_shr.body.layers;
8682
+ const aLayers = body.attester_shr.body.layers;
8683
+ const tierLabel = v.subject_trust_tier === "verified-sovereign" ? "Verified Sovereign" : v.subject_trust_tier === "verified-degraded" ? "Verified (Degraded)" : "Unverified";
8684
+ const lines = [
8685
+ `--- Sovereignty Attestation ---`,
8686
+ ``,
8687
+ `Attester: ${body.attester_id.slice(0, 16)}...`,
8688
+ `Subject: ${body.subject_id.slice(0, 16)}...`,
8689
+ `Result: ${tierLabel}`,
8690
+ ``,
8691
+ `Subject Sovereignty Posture:`,
8692
+ layerLine("L1 Cognitive Sovereignty", sLayers.l1.status),
8693
+ layerLine("L2 Operational Isolation", sLayers.l2.status),
8694
+ layerLine("L3 Selective Disclosure", sLayers.l3.status),
8695
+ layerLine("L4 Verifiable Reputation", sLayers.l4.status),
8696
+ ``,
8697
+ `Attester Sovereignty Posture:`,
8698
+ layerLine("L1 Cognitive Sovereignty", aLayers.l1.status),
8699
+ layerLine("L2 Operational Isolation", aLayers.l2.status),
8700
+ layerLine("L3 Selective Disclosure", aLayers.l3.status),
8701
+ layerLine("L4 Verifiable Reputation", aLayers.l4.status),
8702
+ ``,
8703
+ `Mutual: ${v.mutual ? "Yes" : "One-sided"}`,
8704
+ `Attested: ${body.attested_at}`,
8705
+ `Expires: ${body.expires_at}`,
8706
+ `Signature: ${body.attestation_version} / Ed25519`
8707
+ ];
8708
+ if (v.warnings.length > 0) {
8709
+ lines.push(``, `Warnings: ${v.warnings.join("; ")}`);
8710
+ }
8711
+ if (v.errors.length > 0) {
8712
+ lines.push(``, `Errors: ${v.errors.join("; ")}`);
8713
+ }
8714
+ lines.push(``, `--- Verify: compare signed_by against attester's known public key ---`);
8715
+ return lines.join("\n");
8716
+ }
8717
+ function verifyAttestation(attestation, now) {
8718
+ const errors = [];
8719
+ const currentTime = /* @__PURE__ */ new Date();
8720
+ if (attestation.body.attestation_version !== ATTESTATION_VERSION) {
8721
+ errors.push(
8722
+ `Unsupported attestation version: ${attestation.body.attestation_version}`
8723
+ );
8724
+ }
8725
+ if (!attestation.body.attester_id || !attestation.body.subject_id) {
8726
+ errors.push("Missing attester_id or subject_id");
8727
+ }
8728
+ if (!attestation.body.attester_shr || !attestation.body.subject_shr) {
8729
+ errors.push("Missing attester or subject SHR");
8730
+ }
8731
+ const expired = new Date(attestation.body.expires_at) <= currentTime;
8732
+ if (expired) {
8733
+ errors.push("Attestation has expired");
8734
+ }
8735
+ try {
8736
+ const publicKey = fromBase64url(attestation.signed_by);
8737
+ const canonical = JSON.stringify(deepSortKeys(attestation.body));
8738
+ const payload = stringToBytes(canonical);
8739
+ const signatureBytes = fromBase64url(attestation.signature);
8740
+ const signatureValid = verify(payload, signatureBytes, publicKey);
8741
+ if (!signatureValid) {
8742
+ errors.push("Attestation signature is invalid");
8743
+ }
8744
+ } catch (e) {
8745
+ errors.push(`Signature verification error: ${e.message}`);
8746
+ }
8747
+ return {
8748
+ valid: errors.length === 0,
8749
+ errors,
8750
+ attester_id: attestation.body.attester_id ?? "unknown",
8751
+ subject_id: attestation.body.subject_id ?? "unknown",
8752
+ trust_tier: errors.length === 0 ? attestation.body.verification.subject_trust_tier : "unverified",
8753
+ expired
8754
+ };
8755
+ }
8756
+
8304
8757
  // src/handshake/tools.ts
8305
8758
  function createHandshakeTools(config, identityManager, masterKey, auditLog) {
8306
8759
  const sessions = /* @__PURE__ */ new Map();
@@ -8486,6 +8939,103 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
8486
8939
  result: session.result ?? null
8487
8940
  });
8488
8941
  }
8942
+ },
8943
+ // ─── Streamlined Exchange ─────────────────────────────────────────
8944
+ {
8945
+ name: "sanctuary/handshake_exchange",
8946
+ 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).",
8947
+ inputSchema: {
8948
+ type: "object",
8949
+ properties: {
8950
+ counterparty_shr: {
8951
+ type: "object",
8952
+ description: "The counterparty's signed SHR (SignedSHR object with body, signed_by, signature)."
8953
+ },
8954
+ identity_id: {
8955
+ type: "string",
8956
+ description: "Identity to use for the exchange. Defaults to primary identity."
8957
+ }
8958
+ },
8959
+ required: ["counterparty_shr"]
8960
+ },
8961
+ handler: async (args) => {
8962
+ const counterpartySHR = args.counterparty_shr;
8963
+ const ourSHR = generateSHR(args.identity_id, shrOpts);
8964
+ if (typeof ourSHR === "string") {
8965
+ return toolResult({ error: ourSHR });
8966
+ }
8967
+ const verificationResult = verifySHR(counterpartySHR);
8968
+ const attestation = generateAttestation({
8969
+ attesterSHR: ourSHR,
8970
+ subjectSHR: counterpartySHR,
8971
+ verificationResult,
8972
+ mutual: false,
8973
+ identityManager,
8974
+ masterKey,
8975
+ identityId: args.identity_id
8976
+ });
8977
+ if ("error" in attestation) {
8978
+ auditLog.append("l4", "handshake_exchange", ourSHR.body.instance_id, void 0, "failure");
8979
+ return toolResult({ error: attestation.error });
8980
+ }
8981
+ if (verificationResult.valid) {
8982
+ const sovereigntyLevel = verificationResult.sovereignty_level;
8983
+ const trustTier = sovereigntyLevel === "full" ? "verified-sovereign" : sovereigntyLevel === "degraded" ? "verified-degraded" : "unverified";
8984
+ handshakeResults.set(verificationResult.counterparty_id, {
8985
+ counterparty_id: verificationResult.counterparty_id,
8986
+ counterparty_shr: counterpartySHR,
8987
+ verified: true,
8988
+ sovereignty_level: sovereigntyLevel,
8989
+ trust_tier: trustTier,
8990
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
8991
+ expires_at: verificationResult.expires_at,
8992
+ errors: []
8993
+ });
8994
+ }
8995
+ auditLog.append("l4", "handshake_exchange", ourSHR.body.instance_id);
8996
+ return toolResult({
8997
+ attestation,
8998
+ our_shr: ourSHR,
8999
+ verification: {
9000
+ counterparty_valid: verificationResult.valid,
9001
+ counterparty_sovereignty: verificationResult.sovereignty_level,
9002
+ counterparty_id: verificationResult.counterparty_id,
9003
+ errors: verificationResult.errors,
9004
+ warnings: verificationResult.warnings
9005
+ },
9006
+ 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.",
9007
+ _content_trust: "external"
9008
+ });
9009
+ }
9010
+ },
9011
+ {
9012
+ name: "sanctuary/handshake_verify_attestation",
9013
+ description: "Verify a signed attestation artifact from another agent. Checks the Ed25519 signature, temporal validity, and structural integrity.",
9014
+ inputSchema: {
9015
+ type: "object",
9016
+ properties: {
9017
+ attestation: {
9018
+ type: "object",
9019
+ description: "The SignedAttestation object to verify (body, signed_by, signature, summary)."
9020
+ }
9021
+ },
9022
+ required: ["attestation"]
9023
+ },
9024
+ handler: async (args) => {
9025
+ const attestation = args.attestation;
9026
+ const result = verifyAttestation(attestation);
9027
+ auditLog.append(
9028
+ "l4",
9029
+ "handshake_verify_attestation",
9030
+ result.attester_id,
9031
+ void 0,
9032
+ result.valid ? "success" : "failure"
9033
+ );
9034
+ return toolResult({
9035
+ ...result,
9036
+ _content_trust: "external"
9037
+ });
9038
+ }
8489
9039
  }
8490
9040
  ];
8491
9041
  return { tools, handshakeResults };
@@ -12056,11 +12606,6 @@ async function createSanctuaryServer(options) {
12056
12606
  degradations.push(
12057
12607
  "L2 isolation is process-level only; no TEE available"
12058
12608
  );
12059
- if (config.disclosure.proof_system === "commitment-only") {
12060
- degradations.push(
12061
- "L3 proofs are commitment-based only; ZK proofs not yet available"
12062
- );
12063
- }
12064
12609
  return toolResult({
12065
12610
  attestation: {
12066
12611
  environment_type: config.execution.environment,
@@ -12086,7 +12631,7 @@ async function createSanctuaryServer(options) {
12086
12631
  l1_state_encrypted: true,
12087
12632
  l2_execution_isolated: false,
12088
12633
  l2_isolation_type: "process-level",
12089
- l3_proofs_available: config.disclosure.proof_system !== "commitment-only",
12634
+ l3_proofs_available: true,
12090
12635
  l4_reputation_active: true,
12091
12636
  overall_level: "mvs",
12092
12637
  degradations
@@ -12109,14 +12654,6 @@ async function createSanctuaryServer(options) {
12109
12654
  severity: "warning",
12110
12655
  mitigation: "TEE support planned for a future release"
12111
12656
  });
12112
- if (config.disclosure.proof_system === "commitment-only") {
12113
- degradations.push({
12114
- layer: "l3",
12115
- description: "Commitment schemes only (no ZK proofs)",
12116
- severity: "info",
12117
- mitigation: "ZK proof support planned for v0.2.0"
12118
- });
12119
- }
12120
12657
  return toolResult({
12121
12658
  status: degradations.some((d) => d.severity === "critical") ? "compromised" : degradations.some((d) => d.severity === "warning") ? "degraded" : "healthy",
12122
12659
  storage_bytes: storageSizeBytes,
@@ -12135,7 +12672,7 @@ async function createSanctuaryServer(options) {
12135
12672
  last_attestation: (/* @__PURE__ */ new Date()).toISOString()
12136
12673
  },
12137
12674
  l3: {
12138
- status: config.disclosure.proof_system === "commitment-only" ? "degraded" : "active",
12675
+ status: "active",
12139
12676
  proof_system: config.disclosure.proof_system,
12140
12677
  circuits_loaded: 0,
12141
12678
  proofs_generated_total: 0