@sanctuary-framework/mcp-server 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -15,8 +15,8 @@ var index_js$1 = require('@modelcontextprotocol/sdk/server/index.js');
15
15
  var types_js = require('@modelcontextprotocol/sdk/types.js');
16
16
  var http = require('http');
17
17
  var https = require('https');
18
- var fs = require('fs');
19
18
  var child_process = require('child_process');
19
+ var fs = require('fs');
20
20
  var index_js = require('@modelcontextprotocol/sdk/client/index.js');
21
21
  var stdio_js = require('@modelcontextprotocol/sdk/client/stdio.js');
22
22
  var sse_js = require('@modelcontextprotocol/sdk/client/sse.js');
@@ -383,6 +383,48 @@ var init_identity = __esm({
383
383
  init_random();
384
384
  }
385
385
  });
386
+ async function tightenStoragePermissions(root) {
387
+ try {
388
+ await tightenEntry(root);
389
+ } catch {
390
+ }
391
+ }
392
+ async function tightenEntry(path$1) {
393
+ let info;
394
+ try {
395
+ info = await promises.stat(path$1);
396
+ } catch {
397
+ return;
398
+ }
399
+ if (info.isDirectory()) {
400
+ const current = info.mode & 511;
401
+ if (current !== 448) {
402
+ try {
403
+ await promises.chmod(path$1, 448);
404
+ } catch (err) {
405
+ console.error(` Warning: could not chmod dir ${path$1}: ${err.message}`);
406
+ }
407
+ }
408
+ let entries;
409
+ try {
410
+ entries = await promises.readdir(path$1);
411
+ } catch {
412
+ return;
413
+ }
414
+ for (const entry of entries) {
415
+ await tightenEntry(path.join(path$1, entry));
416
+ }
417
+ } else if (info.isFile()) {
418
+ const current = info.mode & 511;
419
+ if (current !== 384) {
420
+ try {
421
+ await promises.chmod(path$1, 384);
422
+ } catch (err) {
423
+ console.error(` Warning: could not chmod file ${path$1}: ${err.message}`);
424
+ }
425
+ }
426
+ }
427
+ }
386
428
  var require2 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
387
429
  var { version: PKG_VERSION } = require2("../package.json");
388
430
  var SANCTUARY_VERSION = PKG_VERSION;
@@ -776,17 +818,48 @@ var RESERVED_NAMESPACE_PREFIXES = [
776
818
  "_sovereignty_profile",
777
819
  "_context_gate_policies"
778
820
  ];
779
- var StateStore = class {
821
+ var StateStore = class _StateStore {
780
822
  storage;
781
823
  masterKey;
782
824
  // Cache of version numbers per namespace/key for anti-rollback
783
825
  versionCache = /* @__PURE__ */ new Map();
784
826
  // Cache of content hashes per namespace for Merkle tree computation
785
827
  contentHashes = /* @__PURE__ */ new Map();
828
+ // LRU-with-TTL cache for derived namespace keys (avoids repeated HKDF)
829
+ namespaceKeyCache = /* @__PURE__ */ new Map();
830
+ static KEY_CACHE_TTL_MS = 15 * 60 * 1e3;
831
+ // 15 minutes
832
+ static KEY_CACHE_MAX_ENTRIES = 128;
786
833
  constructor(storage, masterKey) {
787
834
  this.storage = storage;
788
835
  this.masterKey = masterKey;
789
836
  }
837
+ /**
838
+ * Get or derive a namespace encryption key, with caching.
839
+ * Cache entries expire after 15 minutes and are evicted LRU when
840
+ * the cache exceeds 128 entries.
841
+ */
842
+ getNamespaceKey(namespace) {
843
+ const now = Date.now();
844
+ const cached = this.namespaceKeyCache.get(namespace);
845
+ if (cached && cached.expiresAt > now) {
846
+ return cached.key;
847
+ }
848
+ if (this.namespaceKeyCache.size >= _StateStore.KEY_CACHE_MAX_ENTRIES) {
849
+ const firstKey = this.namespaceKeyCache.keys().next().value;
850
+ if (firstKey !== void 0) this.namespaceKeyCache.delete(firstKey);
851
+ }
852
+ const derived = deriveNamespaceKey(this.masterKey, namespace);
853
+ this.namespaceKeyCache.set(namespace, {
854
+ key: derived,
855
+ expiresAt: now + _StateStore.KEY_CACHE_TTL_MS
856
+ });
857
+ return derived;
858
+ }
859
+ /** Invalidate all cached namespace keys (call on master key rotation). */
860
+ invalidateKeyCache() {
861
+ this.namespaceKeyCache.clear();
862
+ }
790
863
  versionKey(namespace, key) {
791
864
  return `${namespace}/${key}`;
792
865
  }
@@ -828,7 +901,7 @@ var StateStore = class {
828
901
  * @param options - Optional metadata
829
902
  */
830
903
  async write(namespace, key, value, identityId, encryptedPrivateKey, identityEncryptionKey, options = {}) {
831
- const namespaceKey = deriveNamespaceKey(this.masterKey, namespace);
904
+ const namespaceKey = this.getNamespaceKey(namespace);
832
905
  const plaintext = stringToBytes(value);
833
906
  const integrityHash = hashToString(plaintext);
834
907
  const payload = encrypt(plaintext, namespaceKey);
@@ -909,7 +982,7 @@ var StateStore = class {
909
982
  );
910
983
  }
911
984
  }
912
- const namespaceKey = deriveNamespaceKey(this.masterKey, namespace);
985
+ const namespaceKey = this.getNamespaceKey(namespace);
913
986
  const plaintext = decrypt(stateEntry.payload, namespaceKey);
914
987
  const value = bytesToString(plaintext);
915
988
  const computedHash = hashToString(plaintext);
@@ -1431,6 +1504,19 @@ var IdentityManager = class {
1431
1504
  key_protection: si.key_protection
1432
1505
  }));
1433
1506
  }
1507
+ /** List identities with rotation count (for dashboard display). */
1508
+ listWithRotationCount() {
1509
+ return Array.from(this.identities.values()).map((si) => ({
1510
+ identity_id: si.identity_id,
1511
+ label: si.label,
1512
+ public_key: si.public_key,
1513
+ did: si.did,
1514
+ created_at: si.created_at,
1515
+ key_type: si.key_type,
1516
+ key_protection: si.key_protection,
1517
+ rotation_count: si.rotation_history?.length ?? 0
1518
+ }));
1519
+ }
1434
1520
  };
1435
1521
  function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog) {
1436
1522
  const identityMgr = new IdentityManager(storage, masterKey);
@@ -1885,14 +1971,21 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1885
1971
  // src/l2-operational/audit-log.ts
1886
1972
  init_encryption();
1887
1973
  init_encoding();
1974
+ var DEFAULT_MAX_TOTAL_SIZE_BYTES = 100 * 1024 * 1024;
1975
+ var DEFAULT_MAX_ENTRIES = 1e5;
1888
1976
  var AuditLog = class {
1889
1977
  storage;
1890
1978
  encryptionKey;
1891
1979
  entries = [];
1892
1980
  counter = 0;
1893
- constructor(storage, masterKey) {
1981
+ maxTotalSizeBytes;
1982
+ maxEntries;
1983
+ rotationInFlight = false;
1984
+ constructor(storage, masterKey, config) {
1894
1985
  this.storage = storage;
1895
1986
  this.encryptionKey = derivePurposeKey(masterKey, "audit-log");
1987
+ this.maxTotalSizeBytes = config?.maxTotalSizeBytes ?? DEFAULT_MAX_TOTAL_SIZE_BYTES;
1988
+ this.maxEntries = config?.maxEntries ?? DEFAULT_MAX_ENTRIES;
1896
1989
  }
1897
1990
  /**
1898
1991
  * Append an audit entry.
@@ -1919,6 +2012,38 @@ var AuditLog = class {
1919
2012
  key,
1920
2013
  stringToBytes(JSON.stringify(encrypted))
1921
2014
  );
2015
+ this.maybeRotate().catch(() => {
2016
+ });
2017
+ }
2018
+ /**
2019
+ * Prune oldest audit entries when storage exceeds configured limits.
2020
+ * Entries are sorted by key (timestamp-based) so oldest are pruned first.
2021
+ */
2022
+ async maybeRotate() {
2023
+ if (this.rotationInFlight) return;
2024
+ this.rotationInFlight = true;
2025
+ try {
2026
+ const metas = await this.storage.list("_audit");
2027
+ if (metas.length === 0) return;
2028
+ metas.sort((a, b) => a.key.localeCompare(b.key));
2029
+ const totalSize = metas.reduce((sum, m) => sum + m.size_bytes, 0);
2030
+ let toDelete = 0;
2031
+ if (metas.length > this.maxEntries) {
2032
+ toDelete = metas.length - this.maxEntries;
2033
+ }
2034
+ if (totalSize > this.maxTotalSizeBytes) {
2035
+ let runningSize = totalSize;
2036
+ for (let i = toDelete; i < metas.length && runningSize > this.maxTotalSizeBytes; i++) {
2037
+ runningSize -= metas[i].size_bytes;
2038
+ toDelete = i + 1;
2039
+ }
2040
+ }
2041
+ for (let i = 0; i < toDelete; i++) {
2042
+ await this.storage.delete("_audit", metas[i].key);
2043
+ }
2044
+ } finally {
2045
+ this.rotationInFlight = false;
2046
+ }
1922
2047
  }
1923
2048
  /**
1924
2049
  * Query the audit log with filtering.
@@ -2000,7 +2125,9 @@ function verifyCommitment(commitment, value, blindingFactor) {
2000
2125
  const valueBytes = stringToBytes(value);
2001
2126
  const combined = concatBytes(valueBytes, blindingBytes);
2002
2127
  const expectedHash = toBase64url(hash(combined));
2003
- return commitment === expectedHash;
2128
+ const commitmentBytes = fromBase64url(commitment);
2129
+ const expectedBytes = fromBase64url(expectedHash);
2130
+ return constantTimeEqual(commitmentBytes, expectedBytes);
2004
2131
  }
2005
2132
  var CommitmentStore = class {
2006
2133
  storage;
@@ -3182,6 +3309,51 @@ var ReputationStore = class {
3182
3309
  );
3183
3310
  return guarantee;
3184
3311
  }
3312
+ // ─── L4 Evidence Summary ─────────────────────────────────────────────
3313
+ /**
3314
+ * Summarize attestations for the L4 degradation emitter and dashboard widget.
3315
+ *
3316
+ * Returns aggregate evidence about the identity's reputation state —
3317
+ * counts, tier distribution, recency, dispute counts, context coverage —
3318
+ * without exposing raw attestations. The caller combines this with an
3319
+ * audit-log check for Verascore link state to produce the final
3320
+ * `L4Evidence` struct consumed by the SHR generator.
3321
+ *
3322
+ * @param participantDid - If provided, only count attestations where the
3323
+ * `participant_did` matches. If omitted, covers all attestations in the
3324
+ * store.
3325
+ */
3326
+ async summarizeForSHR(participantDid) {
3327
+ const all = await this.loadAll();
3328
+ const filtered = participantDid ? all.filter((a) => a.attestation.data.participant_did === participantDid) : all;
3329
+ const tierDist = {
3330
+ "verified-sovereign": 0,
3331
+ "verified-degraded": 0,
3332
+ "self-attested": 0,
3333
+ "unverified": 0
3334
+ };
3335
+ const contextBreakdown = {};
3336
+ let mostRecentMs = null;
3337
+ let disputeCount = 0;
3338
+ for (const a of filtered) {
3339
+ const tier = a.attestation.data.sovereignty_tier;
3340
+ if (tier) tierDist[tier]++;
3341
+ const ctx = a.attestation.data.context;
3342
+ if (ctx) contextBreakdown[ctx] = (contextBreakdown[ctx] ?? 0) + 1;
3343
+ const ts = new Date(a.attestation.data.timestamp).getTime();
3344
+ if (!isNaN(ts) && (mostRecentMs === null || ts > mostRecentMs)) {
3345
+ mostRecentMs = ts;
3346
+ }
3347
+ if (a.attestation.data.outcome_result === "disputed") disputeCount++;
3348
+ }
3349
+ return {
3350
+ attestation_count: filtered.length,
3351
+ tier_distribution: tierDist,
3352
+ most_recent_attestation_at: mostRecentMs !== null ? new Date(mostRecentMs).toISOString() : null,
3353
+ dispute_count: disputeCount,
3354
+ context_breakdown: contextBreakdown
3355
+ };
3356
+ }
3185
3357
  // ─── Tier-Aware Access ───────────────────────────────────────────────
3186
3358
  /**
3187
3359
  * Load attestations for tier-weighted scoring.
@@ -3203,21 +3375,37 @@ var ReputationStore = class {
3203
3375
  // ─── Internal ─────────────────────────────────────────────────────────
3204
3376
  async loadAll() {
3205
3377
  const results = [];
3378
+ for await (const page of this.loadAllPaginated(100)) {
3379
+ results.push(...page);
3380
+ }
3381
+ return results;
3382
+ }
3383
+ /**
3384
+ * Cursor-based async iterator that loads attestations in pages.
3385
+ * Prevents OOM at 100K+ records by reading and decrypting in batches.
3386
+ */
3387
+ async *loadAllPaginated(pageSize = 100) {
3388
+ let entries;
3206
3389
  try {
3207
- const entries = await this.storage.list("_reputation");
3208
- for (const meta of entries) {
3390
+ entries = await this.storage.list("_reputation");
3391
+ } catch {
3392
+ return;
3393
+ }
3394
+ for (let i = 0; i < entries.length; i += pageSize) {
3395
+ const page = [];
3396
+ const slice = entries.slice(i, i + pageSize);
3397
+ for (const meta of slice) {
3209
3398
  const raw = await this.storage.read("_reputation", meta.key);
3210
3399
  if (!raw) continue;
3211
3400
  try {
3212
3401
  const encrypted = JSON.parse(bytesToString(raw));
3213
3402
  const decrypted = decrypt(encrypted, this.encryptionKey);
3214
- results.push(JSON.parse(bytesToString(decrypted)));
3403
+ page.push(JSON.parse(bytesToString(decrypted)));
3215
3404
  } catch {
3216
3405
  }
3217
3406
  }
3218
- } catch {
3407
+ if (page.length > 0) yield page;
3219
3408
  }
3220
- return results;
3221
3409
  }
3222
3410
  };
3223
3411
 
@@ -4444,13 +4632,78 @@ function canonicalizeForSigning(body) {
4444
4632
  init_identity();
4445
4633
  init_encoding();
4446
4634
  var DEFAULT_VALIDITY_MS = 60 * 60 * 1e3;
4635
+ var DEFAULT_FRESHNESS_WINDOW_DAYS = 30;
4636
+ var DEFAULT_LOW_TIER_DOMINANCE_THRESHOLD = 0.6;
4637
+ function deriveL4Degradations(evidence, now = /* @__PURE__ */ new Date()) {
4638
+ const out = [];
4639
+ const freshnessDays = evidence.thresholds?.freshness_window_days ?? DEFAULT_FRESHNESS_WINDOW_DAYS;
4640
+ const dominanceThreshold = evidence.thresholds?.low_tier_dominance_threshold ?? DEFAULT_LOW_TIER_DOMINANCE_THRESHOLD;
4641
+ if (evidence.attestation_count === 0) {
4642
+ out.push({
4643
+ layer: "l4",
4644
+ code: "NO_REPUTATION_HISTORY",
4645
+ severity: "warning",
4646
+ description: "Signing identity has no recorded reputation attestations",
4647
+ mitigation: "Complete interactions that produce reputation_record calls, or import a portable reputation bundle"
4648
+ });
4649
+ } else {
4650
+ const lowTierCount = (evidence.tier_distribution["self-attested"] ?? 0) + (evidence.tier_distribution["unverified"] ?? 0);
4651
+ const lowTierShare = lowTierCount / evidence.attestation_count;
4652
+ if (lowTierShare > dominanceThreshold) {
4653
+ const pct = Math.round(lowTierShare * 100);
4654
+ out.push({
4655
+ layer: "l4",
4656
+ code: "LOW_TIER_DOMINANCE",
4657
+ severity: "info",
4658
+ description: `${pct}% of attestations are self-attested or unverified`,
4659
+ mitigation: "Complete sovereignty handshakes with counterparties to upgrade future attestations to verified tiers"
4660
+ });
4661
+ }
4662
+ if (evidence.most_recent_attestation_at) {
4663
+ const mostRecentMs = new Date(evidence.most_recent_attestation_at).getTime();
4664
+ if (!isNaN(mostRecentMs)) {
4665
+ const ageMs = now.getTime() - mostRecentMs;
4666
+ const windowMs = freshnessDays * 24 * 60 * 60 * 1e3;
4667
+ if (ageMs > windowMs) {
4668
+ const ageDays = Math.round(ageMs / (24 * 60 * 60 * 1e3));
4669
+ out.push({
4670
+ layer: "l4",
4671
+ code: "STALE_REPUTATION",
4672
+ severity: "info",
4673
+ description: `Most recent attestation is ${ageDays} days old (freshness window: ${freshnessDays} days)`,
4674
+ mitigation: "Record a fresh interaction outcome or refresh reputation from active counterparties"
4675
+ });
4676
+ }
4677
+ }
4678
+ }
4679
+ if (evidence.dispute_count > 0) {
4680
+ out.push({
4681
+ layer: "l4",
4682
+ code: "DISPUTE_ON_RECORD",
4683
+ severity: "warning",
4684
+ description: `${evidence.dispute_count} attestation${evidence.dispute_count === 1 ? "" : "s"} marked as disputed`,
4685
+ mitigation: "Review disputed interactions; counterparties may weigh this signal when evaluating trust"
4686
+ });
4687
+ }
4688
+ }
4689
+ if (!evidence.verascore_linked) {
4690
+ out.push({
4691
+ layer: "l4",
4692
+ code: "NO_VERASCORE_LINK",
4693
+ severity: "info",
4694
+ description: "No successful reputation_publish call for this identity \u2014 reputation is not externally discoverable",
4695
+ mitigation: "Run reputation_publish to link this identity to a Verascore profile"
4696
+ });
4697
+ }
4698
+ return out;
4699
+ }
4447
4700
  function generateSHR(identityId, opts) {
4448
- const { config, identityManager, masterKey, validityMs } = opts;
4701
+ const { config, identityManager, masterKey, validityMs, l4Evidence, now: nowOverride } = opts;
4449
4702
  const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
4450
4703
  if (!identity) {
4451
4704
  return "No identity available for signing. Create an identity first.";
4452
4705
  }
4453
- const now = /* @__PURE__ */ new Date();
4706
+ const now = nowOverride ?? /* @__PURE__ */ new Date();
4454
4707
  const expiresAt = new Date(now.getTime() + (validityMs ?? DEFAULT_VALIDITY_MS));
4455
4708
  const degradations = [];
4456
4709
  if (config.execution.environment === "local-process") {
@@ -4469,6 +4722,9 @@ function generateSHR(identityId, opts) {
4469
4722
  mitigation: "TEE attestation planned for a future release"
4470
4723
  });
4471
4724
  }
4725
+ const l4Degradations = l4Evidence ? deriveL4Degradations(l4Evidence, now) : [];
4726
+ degradations.push(...l4Degradations);
4727
+ const l4Status = l4Degradations.length > 0 ? "degraded" : "active";
4472
4728
  const body = {
4473
4729
  shr_version: "1.0",
4474
4730
  implementation: {
@@ -4499,7 +4755,7 @@ function generateSHR(identityId, opts) {
4499
4755
  selective_disclosure: true
4500
4756
  },
4501
4757
  l4: {
4502
- status: "active",
4758
+ status: l4Status,
4503
4759
  reputation_mode: config.reputation.mode,
4504
4760
  attestation_format: config.reputation.attestation_format,
4505
4761
  reputation_portable: true
@@ -7460,7 +7716,7 @@ function generateFortressViewHTML(options) {
7460
7716
  <head>
7461
7717
  <meta charset="UTF-8">
7462
7718
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7463
- <title>Sanctuary \u2014 Fortress View</title>
7719
+ <title>Sanctuary</title>
7464
7720
  <style>
7465
7721
  :root {
7466
7722
  --bg: #0d1117;
@@ -7849,7 +8105,7 @@ function generateFortressViewHTML(options) {
7849
8105
  <div class="fortress-brand">
7850
8106
  <div class="shield">&#x1F6E1;</div>
7851
8107
  <div>
7852
- <h1>Sanctuary Cocoon</h1>
8108
+ <h1>Sanctuary</h1>
7853
8109
  <div class="version">v${esc(options.serverVersion)}</div>
7854
8110
  </div>
7855
8111
  </div>
@@ -8391,6 +8647,10 @@ var RATE_LIMIT_WINDOW_MS = 6e4;
8391
8647
  var RATE_LIMIT_GENERAL = 120;
8392
8648
  var RATE_LIMIT_DECISIONS = 20;
8393
8649
  var MAX_RATE_LIMIT_ENTRIES = 1e4;
8650
+ function isDashboardViewRoute(method, path) {
8651
+ if (method !== "GET") return false;
8652
+ return path === "/" || path === "/dashboard" || path === "/fortress" || path === "/events";
8653
+ }
8394
8654
  var DashboardApprovalChannel = class {
8395
8655
  config;
8396
8656
  pending = /* @__PURE__ */ new Map();
@@ -8458,20 +8718,22 @@ var DashboardApprovalChannel = class {
8458
8718
  * Start the HTTP(S) server for the dashboard.
8459
8719
  */
8460
8720
  async start() {
8721
+ const handler = (req, res) => this.handleRequest(req, res);
8722
+ let server;
8723
+ if (this.useTLS && this.config.tls) {
8724
+ const tlsOpts = {
8725
+ cert: await promises.readFile(this.config.tls.cert_path),
8726
+ key: await promises.readFile(this.config.tls.key_path)
8727
+ };
8728
+ server = https.createServer(tlsOpts, handler);
8729
+ } else {
8730
+ server = http.createServer(handler);
8731
+ }
8732
+ this.httpServer = server;
8461
8733
  return new Promise((resolve, reject) => {
8462
- const handler = (req, res) => this.handleRequest(req, res);
8463
- if (this.useTLS && this.config.tls) {
8464
- const tlsOpts = {
8465
- cert: fs.readFileSync(this.config.tls.cert_path),
8466
- key: fs.readFileSync(this.config.tls.key_path)
8467
- };
8468
- this.httpServer = https.createServer(tlsOpts, handler);
8469
- } else {
8470
- this.httpServer = http.createServer(handler);
8471
- }
8472
8734
  const protocol = this.useTLS ? "https" : "http";
8473
8735
  const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
8474
- this.httpServer.listen(this.config.port, this.config.host, () => {
8736
+ server.listen(this.config.port, this.config.host, () => {
8475
8737
  const sessionUrl = this.authToken ? this.createSessionUrl() : baseUrl;
8476
8738
  process.stderr.write(
8477
8739
  `
@@ -8495,7 +8757,7 @@ var DashboardApprovalChannel = class {
8495
8757
  }
8496
8758
  resolve();
8497
8759
  });
8498
- this.httpServer.on("error", (err) => {
8760
+ server.on("error", (err) => {
8499
8761
  if (err.code === "EADDRINUSE") {
8500
8762
  const port = this.config.port;
8501
8763
  process.stderr.write(
@@ -8784,13 +9046,14 @@ var DashboardApprovalChannel = class {
8784
9046
  }
8785
9047
  if (method === "GET" && url.pathname === "/" && this.authToken) {
8786
9048
  if (!this.isAuthenticated(req, url)) {
8787
- if (!this.checkRateLimit(req, res, "general")) return;
8788
9049
  this.serveLoginPage(res);
8789
9050
  return;
8790
9051
  }
8791
9052
  }
8792
9053
  if (!this.checkAuth(req, url, res)) return;
8793
- if (!this.checkRateLimit(req, res, "general")) return;
9054
+ if (!isDashboardViewRoute(method, url.pathname)) {
9055
+ if (!this.checkRateLimit(req, res, "general")) return;
9056
+ }
8794
9057
  try {
8795
9058
  if (method === "GET" && url.pathname === "/fortress") {
8796
9059
  this.serveFortressView(res);
@@ -9084,16 +9347,7 @@ data: ${JSON.stringify(initData)}
9084
9347
  res.end(JSON.stringify({ identities: [], count: 0 }));
9085
9348
  return;
9086
9349
  }
9087
- const identities = this.identityManager.list().map((id) => ({
9088
- identity_id: id.identity_id,
9089
- label: id.label,
9090
- public_key: id.public_key,
9091
- did: id.did,
9092
- created_at: id.created_at,
9093
- key_type: id.key_type,
9094
- key_protection: id.key_protection,
9095
- rotation_count: id.rotation_history?.length ?? 0
9096
- }));
9350
+ const identities = this.identityManager.listWithRotationCount();
9097
9351
  const primary = this.identityManager.getDefault();
9098
9352
  res.writeHead(200, { "Content-Type": "application/json" });
9099
9353
  res.end(JSON.stringify({
@@ -9698,16 +9952,6 @@ var INVISIBLE_CHARS = [
9698
9952
  ];
9699
9953
  var VARIATION_SELECTOR_RANGE_START = 65024;
9700
9954
  var VARIATION_SELECTOR_RANGE_END = 65039;
9701
- var ZERO_WIDTH_CHARS = [
9702
- "\u200B",
9703
- // Zero-width space
9704
- "\u200C",
9705
- // Zero-width non-joiner
9706
- "\u200D",
9707
- // Zero-width joiner
9708
- "\uFEFF"
9709
- // Zero-width no-break space
9710
- ];
9711
9955
  var BASE64_STANDARD_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
9712
9956
  var BASE64URL_PATTERN = /^[A-Za-z0-9_-]+={0,2}$/;
9713
9957
  var BASE64_BLOCK_PATTERN = /[A-Za-z0-9+/]{20,}={0,2}/g;
@@ -10324,10 +10568,8 @@ var InjectionDetector = class {
10324
10568
  });
10325
10569
  }
10326
10570
  }
10327
- let zeroWidthCount = 0;
10328
- for (const char of ZERO_WIDTH_CHARS) {
10329
- zeroWidthCount += (value.match(new RegExp(char, "g")) || []).length;
10330
- }
10571
+ const zeroWidthMatches = value.match(/[\u200B\u200C\u200D\uFEFF]/g);
10572
+ const zeroWidthCount = zeroWidthMatches ? zeroWidthMatches.length : 0;
10331
10573
  if (zeroWidthCount > 0) {
10332
10574
  signals.push({
10333
10575
  type: "encoding_evasion",
@@ -11320,6 +11562,16 @@ function transformDegradations(degradations) {
11320
11562
  authzImpact = "Must share entire data context, cannot redact";
11321
11563
  } else if (deg.code === "BASIC_SYBIL_ONLY") {
11322
11564
  authzImpact = "Restrict to interactions with known agents only";
11565
+ } else if (deg.code === "NO_REPUTATION_HISTORY") {
11566
+ authzImpact = "No prior reputation \u2014 treat as a new counterparty; require escrow or human approval for value transfer";
11567
+ } else if (deg.code === "LOW_TIER_DOMINANCE") {
11568
+ authzImpact = "Reputation dominated by self-attested / unverified signers \u2014 weight accordingly, require additional confirmation on high-value actions";
11569
+ } else if (deg.code === "STALE_REPUTATION") {
11570
+ authzImpact = "Reputation is stale \u2014 may not reflect current behavior; refresh before relying on it";
11571
+ } else if (deg.code === "DISPUTE_ON_RECORD") {
11572
+ authzImpact = "Disputes on record \u2014 review dispute context before extending trust beyond low-value interactions";
11573
+ } else if (deg.code === "NO_VERASCORE_LINK") {
11574
+ authzImpact = "No external reputation profile \u2014 counterparty cannot independently discover reputation bundle";
11323
11575
  } else {
11324
11576
  authzImpact = "Unknown authorization impact";
11325
11577
  }
@@ -11426,12 +11678,37 @@ function transformSHRGeneric(shr) {
11426
11678
  }
11427
11679
 
11428
11680
  // src/shr/tools.ts
11429
- function createSHRTools(config, identityManager, masterKey, auditLog) {
11681
+ async function gatherL4Evidence(reputationStore, auditLog, identity) {
11682
+ const summary = await reputationStore.summarizeForSHR(identity.did);
11683
+ const published = await auditLog.query({
11684
+ layer: "l4",
11685
+ operation_type: "reputation_publish",
11686
+ limit: 500
11687
+ });
11688
+ const verascoreLinked = published.entries.some(
11689
+ (e) => e.result === "success" && e.identity_id === identity.identity_id
11690
+ );
11691
+ return {
11692
+ attestation_count: summary.attestation_count,
11693
+ tier_distribution: summary.tier_distribution,
11694
+ most_recent_attestation_at: summary.most_recent_attestation_at,
11695
+ dispute_count: summary.dispute_count,
11696
+ context_breakdown: summary.context_breakdown,
11697
+ verascore_linked: verascoreLinked
11698
+ };
11699
+ }
11700
+ function createSHRTools(config, identityManager, masterKey, auditLog, reputationStore) {
11430
11701
  const generatorOpts = {
11431
11702
  config,
11432
11703
  identityManager,
11433
11704
  masterKey
11434
11705
  };
11706
+ async function resolveL4Evidence(identityId) {
11707
+ if (!reputationStore) return void 0;
11708
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
11709
+ if (!identity) return void 0;
11710
+ return gatherL4Evidence(reputationStore, auditLog, identity);
11711
+ }
11435
11712
  const tools = [
11436
11713
  {
11437
11714
  name: "shr_generate",
@@ -11451,9 +11728,12 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
11451
11728
  },
11452
11729
  handler: async (args) => {
11453
11730
  const validityMs = args.validity_minutes ? args.validity_minutes * 60 * 1e3 : void 0;
11454
- const result = generateSHR(args.identity_id, {
11731
+ const identityId = args.identity_id;
11732
+ const l4Evidence = await resolveL4Evidence(identityId);
11733
+ const result = generateSHR(identityId, {
11455
11734
  ...generatorOpts,
11456
- validityMs
11735
+ validityMs,
11736
+ l4Evidence
11457
11737
  });
11458
11738
  if (typeof result === "string") {
11459
11739
  return toolResult({ error: result });
@@ -11512,9 +11792,12 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
11512
11792
  handler: async (args) => {
11513
11793
  const format = args.format || "ping";
11514
11794
  const validityMs = args.validity_minutes ? args.validity_minutes * 60 * 1e3 : void 0;
11515
- const shrResult = generateSHR(args.identity_id, {
11795
+ const identityId = args.identity_id;
11796
+ const l4Evidence = await resolveL4Evidence(identityId);
11797
+ const shrResult = generateSHR(identityId, {
11516
11798
  ...generatorOpts,
11517
- validityMs
11799
+ validityMs,
11800
+ l4Evidence
11518
11801
  });
11519
11802
  if (typeof shrResult === "string") {
11520
11803
  return toolResult({ error: shrResult });
@@ -17213,8 +17496,27 @@ function validateVerascoreUrl(urlStr, configuredUrl) {
17213
17496
  }
17214
17497
  }
17215
17498
  function createSanctuaryTools(opts) {
17216
- const { config, identityManager, masterKey, auditLog, policy, keyProtection } = opts;
17499
+ const { config, identityManager, masterKey, auditLog, policy, keyProtection, reputationStore } = opts;
17217
17500
  const identityEncKey = derivePurposeKey(masterKey, "identity-encryption");
17501
+ async function l4EvidenceForIdentity(identity) {
17502
+ if (!reputationStore) return void 0;
17503
+ return gatherL4Evidence(reputationStore, auditLog, identity);
17504
+ }
17505
+ function emptyL4Evidence() {
17506
+ return {
17507
+ attestation_count: 0,
17508
+ tier_distribution: {
17509
+ "verified-sovereign": 0,
17510
+ "verified-degraded": 0,
17511
+ "self-attested": 0,
17512
+ "unverified": 0
17513
+ },
17514
+ most_recent_attestation_at: null,
17515
+ dispute_count: 0,
17516
+ context_breakdown: {},
17517
+ verascore_linked: false
17518
+ };
17519
+ }
17218
17520
  const tools = [
17219
17521
  // ─── sanctuary_bootstrap ───────────────────────────────────────────
17220
17522
  {
@@ -17254,7 +17556,8 @@ function createSanctuaryTools(opts) {
17254
17556
  const shr = generateSHR(publicIdentity.identity_id, {
17255
17557
  config,
17256
17558
  identityManager,
17257
- masterKey
17559
+ masterKey,
17560
+ l4Evidence: emptyL4Evidence()
17258
17561
  });
17259
17562
  if (typeof shr === "string") {
17260
17563
  return toolResult({
@@ -17413,10 +17716,12 @@ function createSanctuaryTools(opts) {
17413
17716
  error: "No identity found. Create one with identity_create first."
17414
17717
  });
17415
17718
  }
17719
+ const l4Evidence = await l4EvidenceForIdentity(identity);
17416
17720
  const shr = generateSHR(identity.identity_id, {
17417
17721
  config,
17418
17722
  identityManager,
17419
- masterKey
17723
+ masterKey,
17724
+ l4Evidence
17420
17725
  });
17421
17726
  const attestations = args.attestations ?? [];
17422
17727
  const body = {
@@ -19218,7 +19523,7 @@ the Sanctuary oversight gate recorded the following activity:
19218
19523
  | Outcome | Count |
19219
19524
  |---|---|
19220
19525
  | Gate allow (Tier 3 auto-allow or approved Tier 1/2) | {{ gate_allow_count }} |
19221
- | Gate allow_proxy (Cocoon MCP-proxy pass-through) | {{ gate_allow_proxy_count }} |
19526
+ | Gate allow_proxy (Sanctuary MCP-proxy pass-through) | {{ gate_allow_proxy_count }} |
19222
19527
  | Gate deny (approval denied or timeout) | {{ gate_deny_count }} |
19223
19528
  | Gate escalate (Tier 2 anomaly raised for human review) | {{ gate_escalate_count }} |
19224
19529
  | Gate unclassified (no matching rule, default behaviour applied) | {{ gate_unclassified_count }} |
@@ -20851,10 +21156,1206 @@ var MemoryStorage = class {
20851
21156
  }
20852
21157
  };
20853
21158
 
21159
+ // src/dashboard/aggregator.ts
21160
+ var L4_DEGRADATION_IMPACT = {
21161
+ critical: 40,
21162
+ warning: 25,
21163
+ info: 10
21164
+ };
21165
+ function computeL4LayerScore(degradations, status) {
21166
+ if (status === "compromised") return 0;
21167
+ let score = 100;
21168
+ for (const deg of degradations) {
21169
+ score -= L4_DEGRADATION_IMPACT[deg.severity] ?? 10;
21170
+ }
21171
+ score = Math.max(0, score);
21172
+ if (degradations.length === 0 && score > 50) {
21173
+ score = Math.min(100, score + 5);
21174
+ }
21175
+ return Math.round(score);
21176
+ }
21177
+ var MAX_ACTIVITY = 50;
21178
+ var MAX_AUDIT = 50;
21179
+ function fingerprintDID(did) {
21180
+ const raw = did.replace(/^did:[a-z0-9]+:/i, "");
21181
+ if (raw.length <= 12) return raw;
21182
+ return `${raw.slice(0, 6)}\u2026${raw.slice(-6)}`;
21183
+ }
21184
+ function countInjectionsToday(audit) {
21185
+ const startOfDay = /* @__PURE__ */ new Date();
21186
+ startOfDay.setHours(0, 0, 0, 0);
21187
+ const cutoff = startOfDay.getTime();
21188
+ return audit.filter((e) => {
21189
+ const ts = new Date(e.timestamp).getTime();
21190
+ if (isNaN(ts) || ts < cutoff) return false;
21191
+ const op = (e.operation ?? "").toLowerCase();
21192
+ return op.includes("injection") || op.includes("blocked");
21193
+ }).length;
21194
+ }
21195
+ var PROOF_CREATION_OPS = /* @__PURE__ */ new Set([
21196
+ "zk_prove",
21197
+ "zk_range_prove",
21198
+ "proof_commitment"
21199
+ ]);
21200
+ function countProofsToday(audit) {
21201
+ const startOfDay = /* @__PURE__ */ new Date();
21202
+ startOfDay.setHours(0, 0, 0, 0);
21203
+ const cutoff = startOfDay.getTime();
21204
+ return audit.filter((e) => {
21205
+ if (e.layer !== "l3") return false;
21206
+ if (!PROOF_CREATION_OPS.has(e.operation)) return false;
21207
+ const ts = new Date(e.timestamp).getTime();
21208
+ return !isNaN(ts) && ts >= cutoff;
21209
+ }).length;
21210
+ }
21211
+ function buildAgent(sources) {
21212
+ if (!sources.identityManager) {
21213
+ return {
21214
+ display_name: "Unclaimed agent",
21215
+ did: null,
21216
+ did_fingerprint: null,
21217
+ identity_count: 0,
21218
+ primary_identity_id: null
21219
+ };
21220
+ }
21221
+ const primary = sources.identityManager.getDefault();
21222
+ const identities = sources.identityManager.list();
21223
+ if (!primary) {
21224
+ return {
21225
+ display_name: "Unclaimed agent",
21226
+ did: null,
21227
+ did_fingerprint: null,
21228
+ identity_count: identities.length,
21229
+ primary_identity_id: null
21230
+ };
21231
+ }
21232
+ return {
21233
+ display_name: primary.label || "Sovereign agent",
21234
+ did: primary.did,
21235
+ did_fingerprint: fingerprintDID(primary.did),
21236
+ identity_count: identities.length,
21237
+ primary_identity_id: primary.identity_id
21238
+ };
21239
+ }
21240
+ function buildL1(sources, audit) {
21241
+ const hasIdentity = !!sources.identityManager?.getDefault();
21242
+ const state = hasIdentity ? "full" : "degraded";
21243
+ return {
21244
+ label: "L1 Cognitive",
21245
+ state,
21246
+ headline: hasIdentity ? "State encrypted at rest" : "No sovereign identity \u2014 run sanctuary_bootstrap",
21247
+ encryption: "AES-256-GCM + HKDF per namespace",
21248
+ injection_blocked_today: countInjectionsToday(audit),
21249
+ memory_attest_ready: hasIdentity
21250
+ };
21251
+ }
21252
+ function buildL2(sources) {
21253
+ const teeAvailable = sources.teeAvailable ?? false;
21254
+ const state = teeAvailable ? "full" : "degraded";
21255
+ return {
21256
+ label: "L2 Operational",
21257
+ state,
21258
+ headline: teeAvailable ? "Hardware isolation active" : "Process isolation \u2014 no TEE on this host",
21259
+ isolation_type: teeAvailable ? "hardware-tee" : "process-level",
21260
+ tee_available: teeAvailable,
21261
+ tee_status: teeAvailable ? "Attested" : "Not available \u2014 normal on local dev",
21262
+ sandbox_status: "Principal Policy gate active"
21263
+ };
21264
+ }
21265
+ function buildL3(sources, audit) {
21266
+ const VC_ISSUING_OPS = /* @__PURE__ */ new Set([
21267
+ "reputation_record",
21268
+ "bootstrap_provide_guarantee",
21269
+ "reputation_publish"
21270
+ ]);
21271
+ const didActive = !!sources.identityManager?.getDefault()?.did;
21272
+ const vcCount = audit.filter(
21273
+ (e) => e.layer === "l4" && VC_ISSUING_OPS.has(e.operation)
21274
+ ).length;
21275
+ return {
21276
+ label: "L3 Disclosure",
21277
+ state: didActive ? "full" : "degraded",
21278
+ headline: didActive ? "Selective disclosure ready" : "No DID \u2014 disclosure unavailable",
21279
+ did_active: didActive,
21280
+ vc_count: vcCount,
21281
+ proofs_today: countProofsToday(audit)
21282
+ };
21283
+ }
21284
+ function buildL4(sources) {
21285
+ const rep = sources.reputation;
21286
+ const hasDid = !!sources.identityManager?.getDefault()?.did;
21287
+ const evidenceBlock = buildL4EvidenceBlock(sources);
21288
+ const base = rep?.score != null ? {
21289
+ label: "L4 Reputation",
21290
+ state: "full",
21291
+ headline: "Verascore attached",
21292
+ score: rep.score,
21293
+ profile_url: rep.profile_url,
21294
+ claim_cta: null
21295
+ } : hasDid ? {
21296
+ label: "L4 Reputation",
21297
+ state: "degraded",
21298
+ headline: "Claim your profile",
21299
+ score: null,
21300
+ profile_url: null,
21301
+ claim_cta: "Claim your profile at verascore.ai"
21302
+ } : {
21303
+ label: "L4 Reputation",
21304
+ state: "degraded",
21305
+ headline: "No identity claimed",
21306
+ score: null,
21307
+ profile_url: null,
21308
+ claim_cta: "Claim your profile at verascore.ai"
21309
+ };
21310
+ if (!evidenceBlock) return base;
21311
+ const nextState = evidenceBlock.active_degradations.length > 0 && base.state === "full" ? "degraded" : base.state;
21312
+ const nextHeadline = nextState === base.state ? base.headline : "Attached, but evidence is degraded";
21313
+ return {
21314
+ ...base,
21315
+ state: nextState,
21316
+ headline: nextHeadline,
21317
+ evidence: evidenceBlock.evidence,
21318
+ layer_score: evidenceBlock.layer_score,
21319
+ active_degradations: evidenceBlock.active_degradations
21320
+ };
21321
+ }
21322
+ function buildL4EvidenceBlock(sources) {
21323
+ const ev = sources.l4Evidence;
21324
+ if (!ev) return null;
21325
+ const degradations = deriveL4Degradations(ev, sources.l4Now ?? /* @__PURE__ */ new Date());
21326
+ const status = degradations.length > 0 ? "degraded" : "full";
21327
+ const layer_score = computeL4LayerScore(degradations, status);
21328
+ return {
21329
+ evidence: {
21330
+ attestation_count: ev.attestation_count,
21331
+ tier_distribution: ev.tier_distribution,
21332
+ most_recent_attestation_at: ev.most_recent_attestation_at,
21333
+ dispute_count: ev.dispute_count,
21334
+ context_breakdown: ev.context_breakdown ?? {},
21335
+ verascore_linked: ev.verascore_linked
21336
+ },
21337
+ layer_score,
21338
+ active_degradations: degradations.map((d) => ({
21339
+ code: d.code,
21340
+ severity: d.severity,
21341
+ description: d.description,
21342
+ ...d.mitigation !== void 0 ? { mitigation: d.mitigation } : {}
21343
+ }))
21344
+ };
21345
+ }
21346
+ function computeOverall(l1, l2, l3, l4) {
21347
+ const critical = [l1.state, l3.state, l4.state];
21348
+ if (critical.includes("compromised") || l2.state === "compromised") {
21349
+ return {
21350
+ status: "compromised",
21351
+ light: "red",
21352
+ headline: "Sovereignty compromised"
21353
+ };
21354
+ }
21355
+ const allCriticalFull = critical.every((s) => s === "full");
21356
+ if (allCriticalFull && l2.state === "full") {
21357
+ return {
21358
+ status: "healthy",
21359
+ light: "green",
21360
+ headline: "All layers full"
21361
+ };
21362
+ }
21363
+ if (allCriticalFull && l2.state === "degraded") {
21364
+ return {
21365
+ status: "healthy",
21366
+ light: "green",
21367
+ headline: "L1\xB7L3\xB7L4 full \u2014 L2 degraded (no TEE on this host)"
21368
+ };
21369
+ }
21370
+ return {
21371
+ status: "degraded",
21372
+ light: "yellow",
21373
+ headline: "One or more layers degraded"
21374
+ };
21375
+ }
21376
+ function buildUpstreamServers(sources) {
21377
+ if (!sources.clientManager) return [];
21378
+ return sources.clientManager.getStatus().map((s) => {
21379
+ const entry = {
21380
+ name: s.name,
21381
+ state: s.state,
21382
+ tool_count: s.tool_count
21383
+ };
21384
+ if (s.error) entry.error = s.error;
21385
+ return entry;
21386
+ });
21387
+ }
21388
+ async function getProtectionSnapshot(sources) {
21389
+ let audit = [];
21390
+ if (sources.auditLog) {
21391
+ try {
21392
+ const result = await sources.auditLog.query({ limit: MAX_AUDIT });
21393
+ audit = result.entries;
21394
+ } catch {
21395
+ audit = [];
21396
+ }
21397
+ }
21398
+ const agent = buildAgent(sources);
21399
+ const l1 = buildL1(sources, audit);
21400
+ const l2 = buildL2(sources);
21401
+ const l3 = buildL3(sources, audit);
21402
+ const l4 = buildL4(sources);
21403
+ const activity = (sources.activity ?? []).slice(0, MAX_ACTIVITY);
21404
+ const pending_approvals = sources.pendingApprovals ?? [];
21405
+ const upstream_servers = buildUpstreamServers(sources);
21406
+ return {
21407
+ overall: computeOverall(l1, l2, l3, l4),
21408
+ agent,
21409
+ layers: { l1, l2, l3, l4 },
21410
+ activity,
21411
+ pending_approvals,
21412
+ audit: audit.slice(-MAX_AUDIT).reverse(),
21413
+ upstream_servers,
21414
+ mode: sources.mode,
21415
+ server_version: sources.server_version,
21416
+ generated_at: (/* @__PURE__ */ new Date()).toISOString()
21417
+ };
21418
+ }
21419
+
21420
+ // src/dashboard/html.ts
21421
+ var HERO_COPY = "Your agent is protected.";
21422
+ function escHtml(value) {
21423
+ if (value == null) return "";
21424
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
21425
+ }
21426
+ function layerCard(layer, extra) {
21427
+ return `<section class="layer-card layer-${escHtml(layer.state)}" data-layer-label="${escHtml(layer.label)}">
21428
+ <div class="layer-head">
21429
+ <div class="layer-dot"></div>
21430
+ <div>
21431
+ <h3>${escHtml(layer.label)}</h3>
21432
+ <p class="layer-headline">${escHtml(layer.headline)}</p>
21433
+ </div>
21434
+ </div>
21435
+ <dl class="layer-detail">${extra}</dl>
21436
+ </section>`;
21437
+ }
21438
+ function l1Card(l1) {
21439
+ return layerCard(
21440
+ l1,
21441
+ `<div><dt>Encryption</dt><dd>${escHtml(l1.encryption)}</dd></div>
21442
+ <div><dt>Injections blocked today</dt><dd>${escHtml(l1.injection_blocked_today)}</dd></div>
21443
+ <div><dt>memory_attest</dt><dd>${l1.memory_attest_ready ? "Ready" : "Not ready"}</dd></div>`
21444
+ );
21445
+ }
21446
+ function l2Card(l2) {
21447
+ return layerCard(
21448
+ l2,
21449
+ `<div><dt>Isolation</dt><dd>${escHtml(l2.isolation_type)}</dd></div>
21450
+ <div><dt>TEE</dt><dd>${escHtml(l2.tee_status)}</dd></div>
21451
+ <div><dt>Sandbox</dt><dd>${escHtml(l2.sandbox_status)}</dd></div>`
21452
+ );
21453
+ }
21454
+ function l3Card(l3) {
21455
+ return layerCard(
21456
+ l3,
21457
+ `<div><dt>DID</dt><dd>${l3.did_active ? "Active" : "None"}</dd></div>
21458
+ <div><dt>Credentials</dt><dd>${escHtml(l3.vc_count)}</dd></div>
21459
+ <div><dt>Proofs today</dt><dd>${escHtml(l3.proofs_today)}</dd></div>`
21460
+ );
21461
+ }
21462
+ function l4Card(l4) {
21463
+ const score = l4.score != null ? `<div class="score-block"><span class="score-value">${escHtml(l4.score)}</span><span class="score-label">Verascore</span></div>` : `<div class="claim-block">${escHtml(l4.claim_cta ?? "Claim your profile at verascore.ai")}</div>`;
21464
+ return layerCard(
21465
+ l4,
21466
+ `<div class="layer-cta">${score}</div>${l4EvidenceBlock(l4)}`
21467
+ );
21468
+ }
21469
+ function formatRelativeDays(iso) {
21470
+ if (!iso) return "none on record";
21471
+ const ts = new Date(iso).getTime();
21472
+ if (isNaN(ts)) return "unknown";
21473
+ const days = Math.round((Date.now() - ts) / (24 * 60 * 60 * 1e3));
21474
+ if (days <= 0) return "today";
21475
+ if (days === 1) return "1 day ago";
21476
+ return `${days} days ago`;
21477
+ }
21478
+ function l4EvidenceBlock(l4) {
21479
+ if (!l4.evidence) return "";
21480
+ const ev = l4.evidence;
21481
+ const tierSovereign = ev.tier_distribution["verified-sovereign"];
21482
+ const tierDegraded = ev.tier_distribution["verified-degraded"];
21483
+ const tierSelf = ev.tier_distribution["self-attested"];
21484
+ const tierUnverified = ev.tier_distribution["unverified"];
21485
+ const contextCount = Object.keys(ev.context_breakdown).length;
21486
+ const mostRecent = formatRelativeDays(ev.most_recent_attestation_at);
21487
+ const score = l4.layer_score ?? 100;
21488
+ const summaryLine = `
21489
+ <div><dt>L4 score</dt><dd>${escHtml(score)} / 100</dd></div>
21490
+ <div><dt>Attestations</dt><dd>${escHtml(ev.attestation_count)}</dd></div>
21491
+ <div><dt>Verified tiers</dt><dd>${escHtml(tierSovereign)} sovereign \xB7 ${escHtml(tierDegraded)} degraded</dd></div>
21492
+ <div><dt>Lower tiers</dt><dd>${escHtml(tierSelf)} self \xB7 ${escHtml(tierUnverified)} unverified</dd></div>
21493
+ <div><dt>Contexts</dt><dd>${escHtml(contextCount)}</dd></div>
21494
+ <div><dt>Disputes</dt><dd>${escHtml(ev.dispute_count)}</dd></div>
21495
+ <div><dt>Last activity</dt><dd>${escHtml(mostRecent)}</dd></div>
21496
+ <div><dt>Verascore link</dt><dd>${ev.verascore_linked ? "Yes" : "Not linked"}</dd></div>
21497
+ `;
21498
+ const degs = l4.active_degradations ?? [];
21499
+ const degList = degs.length === 0 ? "" : `<ul class="l4-deg-list">${degs.map(
21500
+ (d) => `
21501
+ <li class="l4-deg l4-deg-${escHtml(d.severity)}">
21502
+ <div class="l4-deg-head">
21503
+ <span class="l4-deg-code">${escHtml(d.code)}</span>
21504
+ <span class="l4-deg-sev">${escHtml(d.severity)}</span>
21505
+ </div>
21506
+ <p class="l4-deg-desc">${escHtml(d.description)}</p>
21507
+ ${d.mitigation ? `<p class="l4-deg-mit">${escHtml(d.mitigation)}</p>` : ""}
21508
+ </li>`
21509
+ ).join("")}</ul>`;
21510
+ return `
21511
+ <div class="l4-evidence">
21512
+ <dl class="layer-detail l4-evidence-summary">${summaryLine}</dl>
21513
+ ${degList}
21514
+ </div>
21515
+ `;
21516
+ }
21517
+ function renderDashboardHTML(options) {
21518
+ const { snapshot } = options;
21519
+ const { overall, agent, layers, activity, pending_approvals, audit, upstream_servers } = snapshot;
21520
+ const activityRows = activity.length === 0 ? `<tr class="empty"><td colspan="5">Waiting for tool calls\u2026</td></tr>` : activity.map((entry) => {
21521
+ const time = new Date(entry.timestamp).toLocaleTimeString();
21522
+ return `<tr class="result-${escHtml(entry.result)}">
21523
+ <td class="mono time">${escHtml(time)}</td>
21524
+ <td class="mono">${escHtml(entry.tool)}</td>
21525
+ <td class="mono">${escHtml(entry.server)}</td>
21526
+ <td class="tier tier-${escHtml(entry.tier)}">T${escHtml(entry.tier)}</td>
21527
+ <td class="result">${escHtml(entry.result)}</td>
21528
+ </tr>`;
21529
+ }).join("");
21530
+ const approvalItems = pending_approvals.length === 0 ? `<div class="empty-block">No pending approvals</div>` : pending_approvals.map((p) => `<article class="approval" data-id="${escHtml(p.id)}">
21531
+ <div class="approval-head">
21532
+ <span class="tier-chip tier-${escHtml(p.tier)}">Tier ${escHtml(p.tier)}</span>
21533
+ <span class="mono">${escHtml(p.operation)}</span>
21534
+ </div>
21535
+ <p class="approval-reason">${escHtml(p.reason)}</p>
21536
+ <div class="approval-actions">
21537
+ <button class="btn btn-allow" data-action="allow" data-id="${escHtml(p.id)}">Allow</button>
21538
+ <button class="btn btn-deny" data-action="deny" data-id="${escHtml(p.id)}">Deny</button>
21539
+ </div>
21540
+ </article>`).join("");
21541
+ const auditRows = audit.length === 0 ? `<tr class="empty"><td colspan="4">Audit log empty</td></tr>` : audit.map((entry) => `<tr data-kind="${escHtml(entry.layer)}" data-op="${escHtml(entry.operation)}">
21542
+ <td class="mono time">${escHtml(new Date(entry.timestamp).toLocaleTimeString())}</td>
21543
+ <td class="layer-pill">${escHtml(entry.layer.toUpperCase())}</td>
21544
+ <td class="mono">${escHtml(entry.operation)}</td>
21545
+ <td class="result-${escHtml(entry.result)}">${escHtml(entry.result)}</td>
21546
+ </tr>`).join("");
21547
+ const serverRows = upstream_servers.length === 0 ? `<li class="empty-block">No upstream servers connected</li>` : upstream_servers.map((s) => `<li class="server-row state-${escHtml(s.state)}">
21548
+ <span class="server-dot"></span>
21549
+ <span class="mono">${escHtml(s.name)}</span>
21550
+ <span class="server-meta">${escHtml(s.state)} \xB7 ${escHtml(s.tool_count)} tool${s.tool_count === 1 ? "" : "s"}</span>
21551
+ </li>`).join("");
21552
+ const initialSnapshot = JSON.stringify(snapshot).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
21553
+ return `<!DOCTYPE html>
21554
+ <html lang="en">
21555
+ <head>
21556
+ <meta charset="UTF-8">
21557
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
21558
+ <title>Sanctuary \u2014 Sovereignty Dashboard</title>
21559
+ <style>
21560
+ :root {
21561
+ --bg: #07080c;
21562
+ --bg-2: #0f1220;
21563
+ --surface: #131729;
21564
+ --surface-2: #1a1f36;
21565
+ --border: #26304d;
21566
+ --border-strong: #39436a;
21567
+ --ink: #eef1fb;
21568
+ --ink-dim: #b9c0dc;
21569
+ --ink-mute: #7e86a8;
21570
+ --indigo: #6e7bff;
21571
+ --indigo-deep: #3b4ad8;
21572
+ --green: #3ee08f;
21573
+ --green-deep: #168a4d;
21574
+ --amber: #f1c05a;
21575
+ --red: #ff6b7a;
21576
+ --violet: #a77bff;
21577
+ --radius: 14px;
21578
+ --radius-sm: 8px;
21579
+ --shadow: 0 14px 42px rgba(3, 6, 19, 0.45);
21580
+ --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
21581
+ }
21582
+ * { margin: 0; padding: 0; box-sizing: border-box; }
21583
+ html, body { background: var(--bg); color: var(--ink); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, sans-serif; min-height: 100vh; -webkit-font-smoothing: antialiased; }
21584
+ body { background: radial-gradient(circle at 50% -200px, rgba(110, 123, 255, 0.18), transparent 60%), var(--bg); }
21585
+ a { color: var(--indigo); text-decoration: none; }
21586
+ button { font: inherit; cursor: pointer; }
21587
+
21588
+ .wrap { max-width: 1180px; margin: 0 auto; padding: 40px 32px 120px; }
21589
+
21590
+ /* \u2500\u2500 Top meta row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
21591
+ .meta-row {
21592
+ display: flex;
21593
+ justify-content: space-between;
21594
+ align-items: center;
21595
+ color: var(--ink-mute);
21596
+ font-size: 12px;
21597
+ letter-spacing: 0.08em;
21598
+ text-transform: uppercase;
21599
+ margin-bottom: 8px;
21600
+ }
21601
+ .meta-row .mode-pill {
21602
+ padding: 4px 10px;
21603
+ border-radius: 999px;
21604
+ border: 1px solid var(--border);
21605
+ background: var(--surface);
21606
+ font-family: var(--mono);
21607
+ text-transform: none;
21608
+ letter-spacing: 0;
21609
+ font-size: 11px;
21610
+ color: var(--ink-dim);
21611
+ }
21612
+
21613
+ /* \u2500\u2500 Hero \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
21614
+ .hero {
21615
+ display: flex;
21616
+ flex-direction: column;
21617
+ align-items: center;
21618
+ text-align: center;
21619
+ padding: 48px 24px 56px;
21620
+ border-radius: 22px;
21621
+ background:
21622
+ radial-gradient(circle at 50% 0%, rgba(62, 224, 143, 0.08), transparent 70%),
21623
+ linear-gradient(180deg, var(--bg-2) 0%, var(--surface) 100%);
21624
+ border: 1px solid var(--border);
21625
+ box-shadow: var(--shadow);
21626
+ margin-bottom: 32px;
21627
+ position: relative;
21628
+ overflow: hidden;
21629
+ }
21630
+ .hero::after {
21631
+ content: "";
21632
+ position: absolute;
21633
+ inset: 0;
21634
+ background: radial-gradient(circle at 50% 100%, rgba(110, 123, 255, 0.10), transparent 60%);
21635
+ pointer-events: none;
21636
+ }
21637
+ .shield {
21638
+ width: 200px;
21639
+ height: 200px;
21640
+ position: relative;
21641
+ filter: drop-shadow(0 16px 32px rgba(62, 224, 143, 0.18));
21642
+ animation: hero-in 600ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
21643
+ }
21644
+ @keyframes hero-in {
21645
+ from { opacity: 0; transform: translateY(12px) scale(0.96); }
21646
+ to { opacity: 1; transform: translateY(0) scale(1); }
21647
+ }
21648
+ .shield svg { width: 100%; height: 100%; display: block; }
21649
+ .shield.green .shield-ring { stroke: var(--green); }
21650
+ .shield.green .shield-core { fill: rgba(62, 224, 143, 0.14); }
21651
+ .shield.green .shield-mark { stroke: var(--green); }
21652
+ .shield.yellow .shield-ring { stroke: var(--amber); }
21653
+ .shield.yellow .shield-core { fill: rgba(241, 192, 90, 0.14); }
21654
+ .shield.yellow .shield-mark { stroke: var(--amber); }
21655
+ .shield.red .shield-ring { stroke: var(--red); }
21656
+ .shield.red .shield-core { fill: rgba(255, 107, 122, 0.16); }
21657
+ .shield.red .shield-mark { stroke: var(--red); }
21658
+ .shield .shield-ring {
21659
+ fill: none;
21660
+ stroke-width: 3;
21661
+ stroke-dasharray: 600;
21662
+ stroke-dashoffset: 0;
21663
+ transform-origin: center;
21664
+ transition: stroke 320ms ease;
21665
+ }
21666
+ .shield .shield-ring-bg { fill: none; stroke: rgba(255, 255, 255, 0.06); stroke-width: 3; }
21667
+ .shield .shield-mark { fill: none; stroke-width: 4; stroke-linecap: round; stroke-linejoin: round; transition: stroke 320ms ease; }
21668
+
21669
+ .hero h1 {
21670
+ font-size: 40px;
21671
+ font-weight: 650;
21672
+ letter-spacing: -0.02em;
21673
+ margin-top: 28px;
21674
+ position: relative;
21675
+ z-index: 1;
21676
+ }
21677
+ .hero .hero-sub {
21678
+ margin-top: 10px;
21679
+ color: var(--ink-dim);
21680
+ font-size: 15px;
21681
+ position: relative;
21682
+ z-index: 1;
21683
+ }
21684
+ .identity-line {
21685
+ margin-top: 22px;
21686
+ display: inline-flex;
21687
+ align-items: center;
21688
+ gap: 10px;
21689
+ padding: 10px 18px;
21690
+ border-radius: 999px;
21691
+ border: 1px solid var(--border);
21692
+ background: rgba(19, 23, 41, 0.7);
21693
+ font-family: var(--mono);
21694
+ font-size: 13px;
21695
+ color: var(--ink-dim);
21696
+ position: relative;
21697
+ z-index: 1;
21698
+ }
21699
+ .identity-line .name { color: var(--ink); font-weight: 500; font-family: -apple-system, BlinkMacSystemFont, Inter, sans-serif; letter-spacing: 0; }
21700
+ .identity-line .sep { color: var(--ink-mute); }
21701
+ .identity-line .did { color: var(--violet); }
21702
+ .identity-line .primary-flag {
21703
+ padding: 2px 8px;
21704
+ border-radius: 999px;
21705
+ background: rgba(110, 123, 255, 0.16);
21706
+ color: var(--indigo);
21707
+ font-size: 11px;
21708
+ text-transform: uppercase;
21709
+ letter-spacing: 0.1em;
21710
+ font-family: -apple-system, BlinkMacSystemFont, Inter, sans-serif;
21711
+ }
21712
+
21713
+ /* \u2500\u2500 Layer grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
21714
+ .layer-grid {
21715
+ display: grid;
21716
+ grid-template-columns: repeat(4, 1fr);
21717
+ gap: 16px;
21718
+ margin-bottom: 32px;
21719
+ }
21720
+ @media (max-width: 960px) { .layer-grid { grid-template-columns: repeat(2, 1fr); } }
21721
+ @media (max-width: 520px) { .layer-grid { grid-template-columns: 1fr; } }
21722
+
21723
+ .layer-card {
21724
+ padding: 20px;
21725
+ border-radius: var(--radius);
21726
+ background: var(--surface);
21727
+ border: 1px solid var(--border);
21728
+ transition: border 220ms ease, transform 220ms ease;
21729
+ }
21730
+ .layer-card:hover { border-color: var(--border-strong); transform: translateY(-1px); }
21731
+
21732
+ .layer-head { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 14px; }
21733
+ .layer-dot { width: 12px; height: 12px; border-radius: 50%; margin-top: 6px; background: var(--ink-mute); box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.04); }
21734
+ .layer-full .layer-dot { background: var(--green); box-shadow: 0 0 0 3px rgba(62, 224, 143, 0.18); }
21735
+ .layer-degraded .layer-dot { background: var(--amber); box-shadow: 0 0 0 3px rgba(241, 192, 90, 0.18); }
21736
+ .layer-compromised .layer-dot { background: var(--red); box-shadow: 0 0 0 3px rgba(255, 107, 122, 0.18); }
21737
+ .layer-head h3 { font-size: 13px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-dim); margin-bottom: 4px; }
21738
+ .layer-headline { font-size: 15px; color: var(--ink); line-height: 1.35; }
21739
+
21740
+ .layer-detail { display: flex; flex-direction: column; gap: 8px; }
21741
+ .layer-detail > div { display: flex; justify-content: space-between; gap: 12px; font-size: 12px; }
21742
+ .layer-detail dt { color: var(--ink-mute); text-transform: uppercase; letter-spacing: 0.06em; font-size: 11px; }
21743
+ .layer-detail dd { color: var(--ink-dim); text-align: right; font-family: var(--mono); font-size: 12px; }
21744
+
21745
+ .layer-cta { margin-top: 6px; text-align: center; padding: 16px; border-radius: var(--radius-sm); background: var(--bg-2); border: 1px solid var(--border); }
21746
+ .score-block { display: flex; flex-direction: column; gap: 2px; }
21747
+ .score-value { font-size: 28px; font-weight: 650; color: var(--green); letter-spacing: -0.02em; }
21748
+ .score-label { font-size: 11px; text-transform: uppercase; color: var(--ink-mute); letter-spacing: 0.08em; }
21749
+ .claim-block { font-size: 13px; color: var(--violet); }
21750
+
21751
+ /* \u2500\u2500 L4 evidence widget (v0.9.1) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
21752
+ .l4-evidence { margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--border); }
21753
+ .l4-evidence-summary { gap: 6px; margin-bottom: 10px; }
21754
+ .l4-evidence-summary dt { font-size: 10px; }
21755
+ .l4-evidence-summary dd { font-size: 11px; }
21756
+ .l4-deg-list { list-style: none; display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
21757
+ .l4-deg { padding: 8px 10px; border-radius: var(--radius-sm); background: var(--bg-2); border: 1px solid var(--border); font-size: 12px; }
21758
+ .l4-deg-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; margin-bottom: 3px; }
21759
+ .l4-deg-code { font-family: var(--mono); font-size: 11px; color: var(--ink); letter-spacing: 0.02em; }
21760
+ .l4-deg-sev { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-mute); }
21761
+ .l4-deg-warning { border-color: rgba(241, 192, 90, 0.4); }
21762
+ .l4-deg-warning .l4-deg-sev { color: var(--amber); }
21763
+ .l4-deg-critical { border-color: rgba(255, 107, 122, 0.5); }
21764
+ .l4-deg-critical .l4-deg-sev { color: var(--red); }
21765
+ .l4-deg-info .l4-deg-sev { color: var(--indigo); }
21766
+ .l4-deg-desc { color: var(--ink-dim); line-height: 1.35; font-size: 11px; }
21767
+ .l4-deg-mit { color: var(--ink-mute); line-height: 1.35; font-size: 10px; margin-top: 3px; font-style: italic; }
21768
+
21769
+ /* \u2500\u2500 Section headers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
21770
+ .section { margin-bottom: 28px; }
21771
+ .section h2 {
21772
+ font-size: 13px;
21773
+ text-transform: uppercase;
21774
+ letter-spacing: 0.1em;
21775
+ color: var(--ink-dim);
21776
+ margin-bottom: 12px;
21777
+ display: flex;
21778
+ align-items: center;
21779
+ gap: 12px;
21780
+ }
21781
+ .section h2 .count {
21782
+ font-family: var(--mono);
21783
+ font-size: 11px;
21784
+ padding: 2px 8px;
21785
+ border-radius: 999px;
21786
+ background: var(--surface);
21787
+ border: 1px solid var(--border);
21788
+ color: var(--ink-mute);
21789
+ text-transform: none;
21790
+ letter-spacing: 0;
21791
+ }
21792
+
21793
+ /* \u2500\u2500 Approval queue \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
21794
+ .approval-list { display: flex; flex-direction: column; gap: 10px; }
21795
+ .approval {
21796
+ padding: 16px;
21797
+ background: var(--surface);
21798
+ border: 1px solid var(--amber);
21799
+ border-radius: var(--radius);
21800
+ box-shadow: 0 0 0 1px rgba(241, 192, 90, 0.18);
21801
+ animation: fade-up 260ms ease both;
21802
+ }
21803
+ @keyframes fade-up { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
21804
+ .approval-head { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; font-size: 14px; }
21805
+ .tier-chip { padding: 2px 8px; border-radius: 999px; font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; font-family: -apple-system, Inter, sans-serif; }
21806
+ .tier-chip.tier-1 { background: rgba(255, 107, 122, 0.16); color: var(--red); }
21807
+ .tier-chip.tier-2 { background: rgba(241, 192, 90, 0.16); color: var(--amber); }
21808
+ .approval-reason { font-size: 13px; color: var(--ink-dim); margin-bottom: 12px; }
21809
+ .approval-actions { display: flex; gap: 8px; }
21810
+ .btn {
21811
+ padding: 7px 14px;
21812
+ border-radius: var(--radius-sm);
21813
+ border: 1px solid var(--border);
21814
+ background: var(--surface-2);
21815
+ color: var(--ink);
21816
+ font-size: 13px;
21817
+ transition: all 160ms ease;
21818
+ }
21819
+ .btn:hover { border-color: var(--border-strong); background: #202641; }
21820
+ .btn-allow { background: var(--green-deep); border-color: var(--green-deep); color: white; }
21821
+ .btn-allow:hover { background: #1ba25b; border-color: #1ba25b; }
21822
+ .btn-deny { background: transparent; border-color: var(--red); color: var(--red); }
21823
+ .btn-deny:hover { background: rgba(255, 107, 122, 0.1); border-color: var(--red); }
21824
+
21825
+ /* \u2500\u2500 Tables \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
21826
+ .panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
21827
+ .panel-head { display: flex; justify-content: space-between; align-items: center; padding: 12px 18px; border-bottom: 1px solid var(--border); }
21828
+ .panel-head h3 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-dim); }
21829
+ .filter-row { display: flex; gap: 6px; }
21830
+ .filter-row button {
21831
+ padding: 4px 10px;
21832
+ font-size: 11px;
21833
+ border-radius: 999px;
21834
+ background: transparent;
21835
+ border: 1px solid var(--border);
21836
+ color: var(--ink-mute);
21837
+ text-transform: uppercase;
21838
+ letter-spacing: 0.06em;
21839
+ transition: all 140ms ease;
21840
+ }
21841
+ .filter-row button.active, .filter-row button:hover { color: var(--ink); border-color: var(--border-strong); background: var(--surface-2); }
21842
+
21843
+ table { width: 100%; border-collapse: collapse; }
21844
+ th, td { padding: 10px 18px; text-align: left; font-size: 13px; border-bottom: 1px solid var(--border); }
21845
+ th { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-mute); font-weight: 500; background: var(--bg-2); }
21846
+ tr:last-child td { border-bottom: none; }
21847
+ tr.empty td { text-align: center; color: var(--ink-mute); padding: 32px 18px; }
21848
+ .mono { font-family: var(--mono); font-size: 12px; }
21849
+ .time { color: var(--ink-mute); white-space: nowrap; }
21850
+ .tier { text-align: center; font-family: var(--mono); font-size: 11px; font-weight: 600; }
21851
+ .tier-1 { color: var(--red); }
21852
+ .tier-2 { color: var(--amber); }
21853
+ .tier-3 { color: var(--green); }
21854
+ .result { font-family: var(--mono); font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; }
21855
+ .result-allowed, .result-success { color: var(--green); }
21856
+ .result-denied, .result-failure { color: var(--red); }
21857
+ .result-approved { color: var(--indigo); }
21858
+ .result-pending { color: var(--amber); }
21859
+ .layer-pill { font-family: var(--mono); font-size: 11px; padding: 2px 8px; border-radius: 999px; background: var(--surface-2); color: var(--ink-dim); display: inline-block; }
21860
+
21861
+ /* \u2500\u2500 Upstream servers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
21862
+ .server-list { list-style: none; display: flex; flex-direction: column; gap: 8px; }
21863
+ .server-row { display: flex; align-items: center; gap: 10px; padding: 10px 14px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: 13px; }
21864
+ .server-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ink-mute); }
21865
+ .server-row.state-connected .server-dot { background: var(--green); }
21866
+ .server-row.state-connecting .server-dot { background: var(--amber); }
21867
+ .server-row.state-disconnected .server-dot, .server-row.state-error .server-dot { background: var(--red); }
21868
+ .server-meta { margin-left: auto; font-size: 12px; color: var(--ink-mute); font-family: var(--mono); }
21869
+
21870
+ .empty-block { padding: 18px; color: var(--ink-mute); text-align: center; font-size: 13px; background: var(--surface); border: 1px dashed var(--border); border-radius: var(--radius-sm); }
21871
+
21872
+ /* \u2500\u2500 Audit collapsible \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
21873
+ details.audit-details { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
21874
+ details.audit-details summary { cursor: pointer; list-style: none; padding: 14px 18px; display: flex; align-items: center; justify-content: space-between; }
21875
+ details.audit-details summary::-webkit-details-marker { display: none; }
21876
+ details.audit-details summary h3 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-dim); }
21877
+ details.audit-details summary .caret { color: var(--ink-mute); font-family: var(--mono); }
21878
+ details.audit-details[open] .caret { transform: rotate(90deg); display: inline-block; }
21879
+ details.audit-details .audit-filters { display: flex; gap: 6px; padding: 0 18px 12px; border-bottom: 1px solid var(--border); }
21880
+ </style>
21881
+ </head>
21882
+ <body>
21883
+ <div class="wrap">
21884
+ <div class="meta-row">
21885
+ <span>Sanctuary Framework</span>
21886
+ <span class="mode-pill" id="mode-pill">${escHtml(snapshot.mode)} \xB7 v${escHtml(snapshot.server_version)}</span>
21887
+ </div>
21888
+
21889
+ <section class="hero">
21890
+ <div class="shield ${escHtml(overall.light)}" id="shield">
21891
+ <svg viewBox="0 0 200 200" aria-hidden="true">
21892
+ <circle class="shield-ring-bg" cx="100" cy="100" r="92"></circle>
21893
+ <circle class="shield-ring" cx="100" cy="100" r="92"></circle>
21894
+ <circle class="shield-core" cx="100" cy="100" r="80" fill="rgba(255,255,255,0.02)"></circle>
21895
+ <path class="shield-mark" d="M100 52 L132 68 L132 108 C132 130 118 144 100 150 C82 144 68 130 68 108 L68 68 Z"></path>
21896
+ <path class="shield-mark" d="M85 102 L96 113 L118 90"></path>
21897
+ </svg>
21898
+ </div>
21899
+ <h1 id="hero-copy">${escHtml(HERO_COPY)}</h1>
21900
+ <p class="hero-sub" id="hero-sub">${escHtml(overall.headline)}</p>
21901
+ <div class="identity-line" id="identity-line">
21902
+ <span class="name" id="agent-name">${escHtml(agent.display_name)}</span>
21903
+ <span class="sep">\xB7</span>
21904
+ <span class="did" id="agent-did">${escHtml(agent.did_fingerprint ?? "unclaimed")}</span>
21905
+ ${agent.primary_identity_id ? `<span class="primary-flag">Primary</span>` : ""}
21906
+ </div>
21907
+ </section>
21908
+
21909
+ <div class="layer-grid" id="layer-grid">
21910
+ ${l1Card(layers.l1)}
21911
+ ${l2Card(layers.l2)}
21912
+ ${l3Card(layers.l3)}
21913
+ ${l4Card(layers.l4)}
21914
+ </div>
21915
+
21916
+ <section class="section" id="approval-section">
21917
+ <h2>Needs approval <span class="count" id="approval-count">${pending_approvals.length}</span></h2>
21918
+ <div class="approval-list" id="approval-list">${approvalItems}</div>
21919
+ </section>
21920
+
21921
+ <section class="section">
21922
+ <h2>Upstream servers <span class="count">${upstream_servers.length}</span></h2>
21923
+ <ul class="server-list" id="server-list">${serverRows}</ul>
21924
+ </section>
21925
+
21926
+ <section class="section">
21927
+ <h2>Live activity <span class="count" id="activity-count">${activity.length}</span></h2>
21928
+ <div class="panel">
21929
+ <div class="panel-head"><h3>Recent tool calls</h3></div>
21930
+ <table>
21931
+ <thead><tr><th>Time</th><th>Tool</th><th>Server</th><th>Tier</th><th>Result</th></tr></thead>
21932
+ <tbody id="activity-body">${activityRows}</tbody>
21933
+ </table>
21934
+ </div>
21935
+ </section>
21936
+
21937
+ <section class="section">
21938
+ <details class="audit-details">
21939
+ <summary>
21940
+ <h3>Audit trail <span class="count" id="audit-count">${audit.length}</span></h3>
21941
+ <span class="caret">\u25B8</span>
21942
+ </summary>
21943
+ <div class="audit-filters">
21944
+ <div class="filter-row" id="audit-filter">
21945
+ <button class="active" data-filter="all">All</button>
21946
+ <button data-filter="l1">Cognitive</button>
21947
+ <button data-filter="l2">Operational</button>
21948
+ <button data-filter="l3">Disclosure</button>
21949
+ <button data-filter="l4">Reputation</button>
21950
+ </div>
21951
+ </div>
21952
+ <table>
21953
+ <thead><tr><th>Time</th><th>Layer</th><th>Operation</th><th>Result</th></tr></thead>
21954
+ <tbody id="audit-body">${auditRows}</tbody>
21955
+ </table>
21956
+ </details>
21957
+ </section>
21958
+ </div>
21959
+
21960
+ <script>
21961
+ (() => {
21962
+ const INITIAL_SNAPSHOT = ${initialSnapshot};
21963
+ const AUTH_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
21964
+ const AUTH_HEADER = AUTH_TOKEN ? { "Authorization": "Bearer " + AUTH_TOKEN } : {};
21965
+ const AUTH_QS = AUTH_TOKEN ? "?token=" + encodeURIComponent(AUTH_TOKEN) : "";
21966
+
21967
+ let snapshot = INITIAL_SNAPSHOT;
21968
+
21969
+ function esc(value) {
21970
+ if (value == null) return "";
21971
+ return String(value)
21972
+ .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
21973
+ .replace(/"/g, "&quot;").replace(/'/g, "&#039;");
21974
+ }
21975
+ function fmtTime(iso) {
21976
+ try { return new Date(iso).toLocaleTimeString(); } catch { return iso; }
21977
+ }
21978
+
21979
+ function renderShield(light, headline) {
21980
+ const shield = document.getElementById("shield");
21981
+ if (!shield) return;
21982
+ shield.classList.remove("green", "yellow", "red");
21983
+ shield.classList.add(light);
21984
+ document.getElementById("hero-sub").textContent = headline;
21985
+ }
21986
+
21987
+ function renderActivity(entries) {
21988
+ const body = document.getElementById("activity-body");
21989
+ const count = document.getElementById("activity-count");
21990
+ if (!body) return;
21991
+ count.textContent = String(entries.length);
21992
+ if (!entries.length) {
21993
+ body.innerHTML = '<tr class="empty"><td colspan="5">Waiting for tool calls\u2026</td></tr>';
21994
+ return;
21995
+ }
21996
+ body.innerHTML = entries.map(e => (
21997
+ '<tr class="result-' + esc(e.result) + '">' +
21998
+ '<td class="mono time">' + esc(fmtTime(e.timestamp)) + '</td>' +
21999
+ '<td class="mono">' + esc(e.tool) + '</td>' +
22000
+ '<td class="mono">' + esc(e.server) + '</td>' +
22001
+ '<td class="tier tier-' + esc(e.tier) + '">T' + esc(e.tier) + '</td>' +
22002
+ '<td class="result">' + esc(e.result) + '</td>' +
22003
+ '</tr>'
22004
+ )).join("");
22005
+ }
22006
+
22007
+ function renderApprovals(list) {
22008
+ const container = document.getElementById("approval-list");
22009
+ const count = document.getElementById("approval-count");
22010
+ if (!container) return;
22011
+ count.textContent = String(list.length);
22012
+ if (!list.length) {
22013
+ container.innerHTML = '<div class="empty-block">No pending approvals</div>';
22014
+ return;
22015
+ }
22016
+ container.innerHTML = list.map(p => (
22017
+ '<article class="approval" data-id="' + esc(p.id) + '">' +
22018
+ '<div class="approval-head">' +
22019
+ '<span class="tier-chip tier-' + esc(p.tier) + '">Tier ' + esc(p.tier) + '</span>' +
22020
+ '<span class="mono">' + esc(p.operation) + '</span>' +
22021
+ '</div>' +
22022
+ '<p class="approval-reason">' + esc(p.reason) + '</p>' +
22023
+ '<div class="approval-actions">' +
22024
+ '<button class="btn btn-allow" data-action="allow" data-id="' + esc(p.id) + '">Allow</button>' +
22025
+ '<button class="btn btn-deny" data-action="deny" data-id="' + esc(p.id) + '">Deny</button>' +
22026
+ '</div>' +
22027
+ '</article>'
22028
+ )).join("");
22029
+ wireApprovalButtons();
22030
+ }
22031
+
22032
+ function renderAudit(entries, filter) {
22033
+ filter = filter || "all";
22034
+ const body = document.getElementById("audit-body");
22035
+ const count = document.getElementById("audit-count");
22036
+ if (!body) return;
22037
+ const visible = filter === "all" ? entries : entries.filter(e => e.layer === filter);
22038
+ count.textContent = String(visible.length);
22039
+ if (!visible.length) {
22040
+ body.innerHTML = '<tr class="empty"><td colspan="4">Audit log empty</td></tr>';
22041
+ return;
22042
+ }
22043
+ body.innerHTML = visible.map(e => (
22044
+ '<tr data-kind="' + esc(e.layer) + '" data-op="' + esc(e.operation) + '">' +
22045
+ '<td class="mono time">' + esc(fmtTime(e.timestamp)) + '</td>' +
22046
+ '<td><span class="layer-pill">' + esc(String(e.layer).toUpperCase()) + '</span></td>' +
22047
+ '<td class="mono">' + esc(e.operation) + '</td>' +
22048
+ '<td class="result-' + esc(e.result) + '">' + esc(e.result) + '</td>' +
22049
+ '</tr>'
22050
+ )).join("");
22051
+ }
22052
+
22053
+ function renderAll(snap) {
22054
+ snapshot = snap;
22055
+ renderShield(snap.overall.light, snap.overall.headline);
22056
+ const mp = document.getElementById("mode-pill");
22057
+ if (mp) mp.textContent = snap.mode + " \xB7 v" + snap.server_version;
22058
+ document.getElementById("agent-name").textContent = snap.agent.display_name;
22059
+ document.getElementById("agent-did").textContent = snap.agent.did_fingerprint || "unclaimed";
22060
+ renderActivity(snap.activity);
22061
+ renderApprovals(snap.pending_approvals);
22062
+ renderAudit(snap.audit, currentAuditFilter());
22063
+ }
22064
+
22065
+ function currentAuditFilter() {
22066
+ const active = document.querySelector("#audit-filter button.active");
22067
+ return active ? active.dataset.filter : "all";
22068
+ }
22069
+
22070
+ function wireApprovalButtons() {
22071
+ document.querySelectorAll("#approval-list button[data-action]").forEach(btn => {
22072
+ btn.addEventListener("click", async () => {
22073
+ const id = btn.dataset.id;
22074
+ const action = btn.dataset.action;
22075
+ btn.disabled = true;
22076
+ try {
22077
+ await fetch("/api/approvals/" + encodeURIComponent(id) + "/" + action + AUTH_QS, {
22078
+ method: "POST", headers: AUTH_HEADER
22079
+ });
22080
+ } catch {}
22081
+ await refreshSnapshot();
22082
+ });
22083
+ });
22084
+ }
22085
+
22086
+ function wireAuditFilter() {
22087
+ document.querySelectorAll("#audit-filter button").forEach(btn => {
22088
+ btn.addEventListener("click", () => {
22089
+ document.querySelectorAll("#audit-filter button").forEach(b => b.classList.remove("active"));
22090
+ btn.classList.add("active");
22091
+ renderAudit(snapshot.audit, btn.dataset.filter);
22092
+ });
22093
+ });
22094
+ }
22095
+
22096
+ async function refreshSnapshot() {
22097
+ try {
22098
+ const res = await fetch("/api/snapshot" + AUTH_QS, { headers: AUTH_HEADER });
22099
+ if (!res.ok) return;
22100
+ const snap = await res.json();
22101
+ renderAll(snap);
22102
+ } catch {}
22103
+ }
22104
+
22105
+ function connectSSE() {
22106
+ try {
22107
+ const es = new EventSource("/api/stream" + AUTH_QS);
22108
+ es.addEventListener("snapshot", (ev) => {
22109
+ try { renderAll(JSON.parse(ev.data)); } catch {}
22110
+ });
22111
+ es.addEventListener("activity", () => refreshSnapshot());
22112
+ es.addEventListener("approval", () => refreshSnapshot());
22113
+ es.onerror = () => { es.close(); setTimeout(connectSSE, 3000); };
22114
+ } catch {}
22115
+ }
22116
+
22117
+ wireApprovalButtons();
22118
+ wireAuditFilter();
22119
+ connectSSE();
22120
+ })();
22121
+ </script>
22122
+ </body>
22123
+ </html>`;
22124
+ }
22125
+
22126
+ // src/dashboard/api.ts
22127
+ function constantTimeEquals(a, b) {
22128
+ if (a.length !== b.length) return false;
22129
+ let diff = 0;
22130
+ for (let i = 0; i < a.length; i++) {
22131
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
22132
+ }
22133
+ return diff === 0;
22134
+ }
22135
+ function extractToken(req, url) {
22136
+ const header = req.headers.authorization;
22137
+ if (header && header.startsWith("Bearer ")) {
22138
+ return header.slice(7).trim();
22139
+ }
22140
+ const q = url.searchParams.get("token");
22141
+ return q ?? null;
22142
+ }
22143
+ function isAuthorized(deps, req, url) {
22144
+ if (!deps.authToken) return true;
22145
+ const token = extractToken(req, url);
22146
+ if (!token) return false;
22147
+ return constantTimeEquals(token, deps.authToken);
22148
+ }
22149
+ function writeJSON(res, status, payload) {
22150
+ res.writeHead(status, {
22151
+ "Content-Type": "application/json",
22152
+ "Cache-Control": "no-store"
22153
+ });
22154
+ res.end(JSON.stringify(payload));
22155
+ }
22156
+ function writeText(res, status, body, contentType = "text/plain") {
22157
+ res.writeHead(status, {
22158
+ "Content-Type": contentType,
22159
+ "Cache-Control": "no-store"
22160
+ });
22161
+ res.end(body);
22162
+ }
22163
+ async function handleRequest(deps, req, res) {
22164
+ const host = req.headers.host || "localhost";
22165
+ const url = new URL(req.url ?? "/", `http://${host}`);
22166
+ const method = (req.method ?? "GET").toUpperCase();
22167
+ const path = url.pathname;
22168
+ if (!isAuthorized(deps, req, url)) {
22169
+ writeJSON(res, 401, { error: "unauthorized" });
22170
+ return true;
22171
+ }
22172
+ if (method === "GET" && path === "/api/health") {
22173
+ writeJSON(res, 200, { ok: true, mode: deps.sources.mode });
22174
+ return true;
22175
+ }
22176
+ if (method === "GET" && (path === "/" || path === "/index.html")) {
22177
+ const snapshot = await getProtectionSnapshot(deps.sources);
22178
+ const html = renderDashboardHTML({ snapshot, authToken: deps.authToken });
22179
+ writeText(res, 200, html, "text/html; charset=utf-8");
22180
+ return true;
22181
+ }
22182
+ if (method === "GET" && path === "/api/snapshot") {
22183
+ const snapshot = await getProtectionSnapshot(deps.sources);
22184
+ writeJSON(res, 200, snapshot);
22185
+ return true;
22186
+ }
22187
+ const approvalMatch = /^\/api\/approvals\/([^/]+)\/(allow|deny)$/.exec(path);
22188
+ if (method === "POST" && approvalMatch) {
22189
+ const id = decodeURIComponent(approvalMatch[1]);
22190
+ const action = approvalMatch[2];
22191
+ if (!deps.approvals) {
22192
+ writeJSON(res, 503, { error: "approvals_unavailable" });
22193
+ return true;
22194
+ }
22195
+ const handler = action === "allow" ? deps.approvals.allow : deps.approvals.deny;
22196
+ try {
22197
+ const ok = await handler(id);
22198
+ writeJSON(res, ok ? 200 : 404, { id, action, ok });
22199
+ } catch (err) {
22200
+ writeJSON(res, 500, { error: "approval_failed", message: err.message });
22201
+ }
22202
+ return true;
22203
+ }
22204
+ if (method === "GET" && path === "/api/stream") {
22205
+ await handleStream(deps, res);
22206
+ return true;
22207
+ }
22208
+ return false;
22209
+ }
22210
+ async function handleStream(deps, res) {
22211
+ res.writeHead(200, {
22212
+ "Content-Type": "text/event-stream",
22213
+ "Cache-Control": "no-cache, no-transform",
22214
+ Connection: "keep-alive",
22215
+ "X-Accel-Buffering": "no"
22216
+ });
22217
+ const snapshot = await getProtectionSnapshot(deps.sources);
22218
+ res.write(`event: snapshot
22219
+ data: ${JSON.stringify(snapshot)}
22220
+
22221
+ `);
22222
+ const unsubscribe = deps.onEvent ? deps.onEvent((event) => {
22223
+ try {
22224
+ res.write(`event: ${event.type}
22225
+ data: ${JSON.stringify(event.data)}
22226
+
22227
+ `);
22228
+ } catch {
22229
+ }
22230
+ }) : () => {
22231
+ };
22232
+ const keepAlive = setInterval(() => {
22233
+ try {
22234
+ res.write(": keepalive\n\n");
22235
+ } catch {
22236
+ }
22237
+ }, 25e3);
22238
+ const cleanup = () => {
22239
+ clearInterval(keepAlive);
22240
+ unsubscribe();
22241
+ };
22242
+ res.on("close", cleanup);
22243
+ res.on("error", cleanup);
22244
+ }
22245
+
22246
+ // src/dashboard/server.ts
22247
+ var DEFAULT_PORT = 3501;
22248
+ var DEFAULT_HOST = "127.0.0.1";
22249
+ async function startDashboardServer(options) {
22250
+ const port = options.port ?? DEFAULT_PORT;
22251
+ const host = options.host ?? DEFAULT_HOST;
22252
+ const listeners = /* @__PURE__ */ new Set();
22253
+ const onEvent = (listener) => {
22254
+ listeners.add(listener);
22255
+ return () => listeners.delete(listener);
22256
+ };
22257
+ const publish = (event) => {
22258
+ for (const listener of listeners) {
22259
+ try {
22260
+ listener(event);
22261
+ } catch {
22262
+ }
22263
+ }
22264
+ };
22265
+ const deps = {
22266
+ sources: options.sources,
22267
+ authToken: options.authToken,
22268
+ approvals: options.approvals,
22269
+ onEvent
22270
+ };
22271
+ const server = http.createServer(async (req, res) => {
22272
+ try {
22273
+ const served = await handleRequest(deps, req, res);
22274
+ if (!served) {
22275
+ res.writeHead(404, { "Content-Type": "application/json" });
22276
+ res.end(JSON.stringify({ error: "not_found", path: req.url }));
22277
+ }
22278
+ } catch (err) {
22279
+ try {
22280
+ res.writeHead(500, { "Content-Type": "application/json" });
22281
+ res.end(JSON.stringify({ error: "internal", message: err.message }));
22282
+ } catch {
22283
+ }
22284
+ }
22285
+ });
22286
+ await new Promise((resolve, reject) => {
22287
+ server.once("error", reject);
22288
+ server.listen(port, host, () => {
22289
+ server.off("error", reject);
22290
+ resolve();
22291
+ });
22292
+ });
22293
+ const actualPort = (() => {
22294
+ const addr = server.address();
22295
+ if (addr && typeof addr === "object") return addr.port;
22296
+ return port;
22297
+ })();
22298
+ const url = `http://${host}:${actualPort}`;
22299
+ return {
22300
+ url,
22301
+ port: actualPort,
22302
+ host,
22303
+ stop: () => new Promise((resolve, reject) => {
22304
+ server.close((err) => err ? reject(err) : resolve());
22305
+ }),
22306
+ publish,
22307
+ publishActivity: (entry) => publish({ type: "activity", data: entry }),
22308
+ publishApproval: (approval) => publish({ type: "approval", data: approval })
22309
+ };
22310
+ }
22311
+
22312
+ // src/dashboard/index.ts
22313
+ async function startDashboard(options) {
22314
+ const activity = options.initialActivity ? [...options.initialActivity] : [];
22315
+ const pending = options.initialPendingApprovals ? [...options.initialPendingApprovals] : [];
22316
+ const sources = {
22317
+ mode: options.mode,
22318
+ server_version: options.serverVersion,
22319
+ ...options.auditLog ? { auditLog: options.auditLog } : {},
22320
+ ...options.identityManager ? { identityManager: options.identityManager } : {},
22321
+ ...options.clientManager ? { clientManager: options.clientManager } : {},
22322
+ ...options.baseline ? { baseline: options.baseline } : {},
22323
+ ...options.policy ? { policy: options.policy } : {},
22324
+ ...options.reputation ? { reputation: options.reputation } : {},
22325
+ ...options.teeAvailable != null ? { teeAvailable: options.teeAvailable } : {},
22326
+ ...options.l4Evidence ? { l4Evidence: options.l4Evidence } : {},
22327
+ activity,
22328
+ pendingApprovals: pending
22329
+ };
22330
+ const serverOpts = {
22331
+ mode: options.mode,
22332
+ sources,
22333
+ ...options.port != null ? { port: options.port } : {},
22334
+ ...options.host ? { host: options.host } : {},
22335
+ ...options.authToken ? { authToken: options.authToken } : {},
22336
+ ...options.approvals ? { approvals: options.approvals } : {}
22337
+ };
22338
+ const handle = await startDashboardServer(serverOpts);
22339
+ const wrapped = {
22340
+ ...handle,
22341
+ publishActivity: (entry) => {
22342
+ activity.unshift(entry);
22343
+ if (activity.length > 50) activity.length = 50;
22344
+ handle.publishActivity(entry);
22345
+ },
22346
+ publishApproval: (approval) => {
22347
+ pending.push(approval);
22348
+ handle.publishApproval(approval);
22349
+ }
22350
+ };
22351
+ return wrapped;
22352
+ }
22353
+
20854
22354
  // src/index.ts
20855
22355
  async function createSanctuaryServer(options) {
20856
22356
  const config = await loadConfig(options?.configPath);
20857
22357
  await promises.mkdir(config.storage_path, { recursive: true, mode: 448 });
22358
+ await tightenStoragePermissions(config.storage_path);
20858
22359
  const storage = options?.storage ?? new FilesystemStorage(
20859
22360
  `${config.storage_path}/state`
20860
22361
  );
@@ -21174,12 +22675,6 @@ async function createSanctuaryServer(options) {
21174
22675
  }
21175
22676
  };
21176
22677
  const { tools: l3Tools } = createL3Tools(storage, masterKey, auditLog);
21177
- const { tools: shrTools } = createSHRTools(
21178
- config,
21179
- identityManager,
21180
- masterKey,
21181
- auditLog
21182
- );
21183
22678
  const { tools: handshakeTools, handshakeResults } = createHandshakeTools(
21184
22679
  config,
21185
22680
  identityManager,
@@ -21190,7 +22685,7 @@ async function createSanctuaryServer(options) {
21190
22685
  verascoreUrl: config.verascore.url
21191
22686
  }
21192
22687
  );
21193
- const { tools: l4Tools} = createL4Tools(
22688
+ const { tools: l4Tools, reputationStore } = createL4Tools(
21194
22689
  storage,
21195
22690
  masterKey,
21196
22691
  identityManager,
@@ -21198,6 +22693,13 @@ async function createSanctuaryServer(options) {
21198
22693
  handshakeResults,
21199
22694
  config.verascore.url
21200
22695
  );
22696
+ const { tools: shrTools } = createSHRTools(
22697
+ config,
22698
+ identityManager,
22699
+ masterKey,
22700
+ auditLog,
22701
+ reputationStore
22702
+ );
21201
22703
  const { tools: federationTools } = createFederationTools(
21202
22704
  auditLog,
21203
22705
  handshakeResults
@@ -21288,7 +22790,8 @@ async function createSanctuaryServer(options) {
21288
22790
  masterKey,
21289
22791
  auditLog,
21290
22792
  policy,
21291
- keyProtection
22793
+ keyProtection,
22794
+ reputationStore
21292
22795
  });
21293
22796
  const { tools: memoryAttestTools } = createMemoryAttestTools(
21294
22797
  identityManager,
@@ -21474,6 +22977,7 @@ exports.ContextGatePolicyStore = ContextGatePolicyStore;
21474
22977
  exports.DashboardApprovalChannel = DashboardApprovalChannel;
21475
22978
  exports.FederationRegistry = FederationRegistry;
21476
22979
  exports.FilesystemStorage = FilesystemStorage;
22980
+ exports.HERO_COPY = HERO_COPY;
21477
22981
  exports.InMemoryModelProvenanceStore = InMemoryModelProvenanceStore;
21478
22982
  exports.InjectionDetector = InjectionDetector;
21479
22983
  exports.MODEL_PRESETS = MODEL_PRESETS;
@@ -21501,15 +23005,19 @@ exports.filterContext = filterContext;
21501
23005
  exports.generateAttestation = generateAttestation;
21502
23006
  exports.generateSHR = generateSHR;
21503
23007
  exports.generateSystemPrompt = generateSystemPrompt;
23008
+ exports.getProtectionSnapshot = getProtectionSnapshot;
21504
23009
  exports.getTemplate = getTemplate;
21505
23010
  exports.initiateHandshake = initiateHandshake;
21506
23011
  exports.listTemplateIds = listTemplateIds;
21507
23012
  exports.loadConfig = loadConfig;
21508
23013
  exports.loadPrincipalPolicy = loadPrincipalPolicy;
21509
23014
  exports.recommendPolicy = recommendPolicy;
23015
+ exports.renderDashboardHTML = renderDashboardHTML;
21510
23016
  exports.resolveTier = resolveTier;
21511
23017
  exports.respondToHandshake = respondToHandshake;
21512
23018
  exports.signPayload = signPayload;
23019
+ exports.startDashboard = startDashboard;
23020
+ exports.startDashboardServer = startDashboardServer;
21513
23021
  exports.tierDistribution = tierDistribution;
21514
23022
  exports.verifyAttestation = verifyAttestation;
21515
23023
  exports.verifyBridgeCommitment = verifyBridgeCommitment;