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