@sanctuary-framework/mcp-server 0.8.0 → 0.10.1

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,17 +1971,34 @@ 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
+ pendingWrites = /* @__PURE__ */ new Set();
1985
+ constructor(storage, masterKey, config) {
1894
1986
  this.storage = storage;
1895
1987
  this.encryptionKey = derivePurposeKey(masterKey, "audit-log");
1988
+ this.maxTotalSizeBytes = config?.maxTotalSizeBytes ?? DEFAULT_MAX_TOTAL_SIZE_BYTES;
1989
+ this.maxEntries = config?.maxEntries ?? DEFAULT_MAX_ENTRIES;
1896
1990
  }
1897
1991
  /**
1898
1992
  * Append an audit entry.
1993
+ *
1994
+ * The on-disk persist is async and tracked via `pendingWrites`. Long-lived
1995
+ * callers (the main MCP server) can ignore that tracking and let writes
1996
+ * drain naturally. Short-lived callers — the `sanctuary secrets` CLI which
1997
+ * `process.exit()`s immediately after returning from a broker mutation —
1998
+ * MUST await `flush()` before exiting, or in-flight writes get killed
1999
+ * with the event loop and the entry is silently lost. That was the
2000
+ * v0.10.0-rc.2 soak failure mode where `secrets audit` returned empty
2001
+ * after a clean 7-verb lifecycle.
1899
2002
  */
1900
2003
  append(layer, operation, identityId, details, result = "success") {
1901
2004
  const entry = {
@@ -1907,8 +2010,22 @@ var AuditLog = class {
1907
2010
  details
1908
2011
  };
1909
2012
  this.entries.push(entry);
1910
- this.persistEntry(entry).catch(() => {
2013
+ const writePromise = this.persistEntry(entry).catch(() => {
1911
2014
  });
2015
+ this.pendingWrites.add(writePromise);
2016
+ void writePromise.finally(() => this.pendingWrites.delete(writePromise));
2017
+ }
2018
+ /**
2019
+ * Wait for every in-flight `append()` persist (and its rotation pass) to
2020
+ * settle. Safe to call multiple times — newly-appended entries during a
2021
+ * flush are also awaited. Re-entrant only at the granularity of "drain
2022
+ * everything queued so far". Short-lived CLIs MUST call this before
2023
+ * `process.exit()` to keep audit writes durable.
2024
+ */
2025
+ async flush() {
2026
+ while (this.pendingWrites.size > 0) {
2027
+ await Promise.allSettled([...this.pendingWrites]);
2028
+ }
1912
2029
  }
1913
2030
  async persistEntry(entry) {
1914
2031
  const key = `${Date.now()}-${this.counter++}`;
@@ -1919,6 +2036,38 @@ var AuditLog = class {
1919
2036
  key,
1920
2037
  stringToBytes(JSON.stringify(encrypted))
1921
2038
  );
2039
+ await this.maybeRotate().catch(() => {
2040
+ });
2041
+ }
2042
+ /**
2043
+ * Prune oldest audit entries when storage exceeds configured limits.
2044
+ * Entries are sorted by key (timestamp-based) so oldest are pruned first.
2045
+ */
2046
+ async maybeRotate() {
2047
+ if (this.rotationInFlight) return;
2048
+ this.rotationInFlight = true;
2049
+ try {
2050
+ const metas = await this.storage.list("_audit");
2051
+ if (metas.length === 0) return;
2052
+ metas.sort((a, b) => a.key.localeCompare(b.key));
2053
+ const totalSize = metas.reduce((sum, m) => sum + m.size_bytes, 0);
2054
+ let toDelete = 0;
2055
+ if (metas.length > this.maxEntries) {
2056
+ toDelete = metas.length - this.maxEntries;
2057
+ }
2058
+ if (totalSize > this.maxTotalSizeBytes) {
2059
+ let runningSize = totalSize;
2060
+ for (let i = toDelete; i < metas.length && runningSize > this.maxTotalSizeBytes; i++) {
2061
+ runningSize -= metas[i].size_bytes;
2062
+ toDelete = i + 1;
2063
+ }
2064
+ }
2065
+ for (let i = 0; i < toDelete; i++) {
2066
+ await this.storage.delete("_audit", metas[i].key);
2067
+ }
2068
+ } finally {
2069
+ this.rotationInFlight = false;
2070
+ }
1922
2071
  }
1923
2072
  /**
1924
2073
  * Query the audit log with filtering.
@@ -2000,7 +2149,9 @@ function verifyCommitment(commitment, value, blindingFactor) {
2000
2149
  const valueBytes = stringToBytes(value);
2001
2150
  const combined = concatBytes(valueBytes, blindingBytes);
2002
2151
  const expectedHash = toBase64url(hash(combined));
2003
- return commitment === expectedHash;
2152
+ const commitmentBytes = fromBase64url(commitment);
2153
+ const expectedBytes = fromBase64url(expectedHash);
2154
+ return constantTimeEqual(commitmentBytes, expectedBytes);
2004
2155
  }
2005
2156
  var CommitmentStore = class {
2006
2157
  storage;
@@ -3182,6 +3333,51 @@ var ReputationStore = class {
3182
3333
  );
3183
3334
  return guarantee;
3184
3335
  }
3336
+ // ─── L4 Evidence Summary ─────────────────────────────────────────────
3337
+ /**
3338
+ * Summarize attestations for the L4 degradation emitter and dashboard widget.
3339
+ *
3340
+ * Returns aggregate evidence about the identity's reputation state —
3341
+ * counts, tier distribution, recency, dispute counts, context coverage —
3342
+ * without exposing raw attestations. The caller combines this with an
3343
+ * audit-log check for Verascore link state to produce the final
3344
+ * `L4Evidence` struct consumed by the SHR generator.
3345
+ *
3346
+ * @param participantDid - If provided, only count attestations where the
3347
+ * `participant_did` matches. If omitted, covers all attestations in the
3348
+ * store.
3349
+ */
3350
+ async summarizeForSHR(participantDid) {
3351
+ const all = await this.loadAll();
3352
+ const filtered = participantDid ? all.filter((a) => a.attestation.data.participant_did === participantDid) : all;
3353
+ const tierDist = {
3354
+ "verified-sovereign": 0,
3355
+ "verified-degraded": 0,
3356
+ "self-attested": 0,
3357
+ "unverified": 0
3358
+ };
3359
+ const contextBreakdown = {};
3360
+ let mostRecentMs = null;
3361
+ let disputeCount = 0;
3362
+ for (const a of filtered) {
3363
+ const tier = a.attestation.data.sovereignty_tier;
3364
+ if (tier) tierDist[tier]++;
3365
+ const ctx = a.attestation.data.context;
3366
+ if (ctx) contextBreakdown[ctx] = (contextBreakdown[ctx] ?? 0) + 1;
3367
+ const ts = new Date(a.attestation.data.timestamp).getTime();
3368
+ if (!isNaN(ts) && (mostRecentMs === null || ts > mostRecentMs)) {
3369
+ mostRecentMs = ts;
3370
+ }
3371
+ if (a.attestation.data.outcome_result === "disputed") disputeCount++;
3372
+ }
3373
+ return {
3374
+ attestation_count: filtered.length,
3375
+ tier_distribution: tierDist,
3376
+ most_recent_attestation_at: mostRecentMs !== null ? new Date(mostRecentMs).toISOString() : null,
3377
+ dispute_count: disputeCount,
3378
+ context_breakdown: contextBreakdown
3379
+ };
3380
+ }
3185
3381
  // ─── Tier-Aware Access ───────────────────────────────────────────────
3186
3382
  /**
3187
3383
  * Load attestations for tier-weighted scoring.
@@ -3203,21 +3399,37 @@ var ReputationStore = class {
3203
3399
  // ─── Internal ─────────────────────────────────────────────────────────
3204
3400
  async loadAll() {
3205
3401
  const results = [];
3402
+ for await (const page of this.loadAllPaginated(100)) {
3403
+ results.push(...page);
3404
+ }
3405
+ return results;
3406
+ }
3407
+ /**
3408
+ * Cursor-based async iterator that loads attestations in pages.
3409
+ * Prevents OOM at 100K+ records by reading and decrypting in batches.
3410
+ */
3411
+ async *loadAllPaginated(pageSize = 100) {
3412
+ let entries;
3206
3413
  try {
3207
- const entries = await this.storage.list("_reputation");
3208
- for (const meta of entries) {
3414
+ entries = await this.storage.list("_reputation");
3415
+ } catch {
3416
+ return;
3417
+ }
3418
+ for (let i = 0; i < entries.length; i += pageSize) {
3419
+ const page = [];
3420
+ const slice = entries.slice(i, i + pageSize);
3421
+ for (const meta of slice) {
3209
3422
  const raw = await this.storage.read("_reputation", meta.key);
3210
3423
  if (!raw) continue;
3211
3424
  try {
3212
3425
  const encrypted = JSON.parse(bytesToString(raw));
3213
3426
  const decrypted = decrypt(encrypted, this.encryptionKey);
3214
- results.push(JSON.parse(bytesToString(decrypted)));
3427
+ page.push(JSON.parse(bytesToString(decrypted)));
3215
3428
  } catch {
3216
3429
  }
3217
3430
  }
3218
- } catch {
3431
+ if (page.length > 0) yield page;
3219
3432
  }
3220
- return results;
3221
3433
  }
3222
3434
  };
3223
3435
 
@@ -4444,13 +4656,78 @@ function canonicalizeForSigning(body) {
4444
4656
  init_identity();
4445
4657
  init_encoding();
4446
4658
  var DEFAULT_VALIDITY_MS = 60 * 60 * 1e3;
4659
+ var DEFAULT_FRESHNESS_WINDOW_DAYS = 30;
4660
+ var DEFAULT_LOW_TIER_DOMINANCE_THRESHOLD = 0.6;
4661
+ function deriveL4Degradations(evidence, now = /* @__PURE__ */ new Date()) {
4662
+ const out = [];
4663
+ const freshnessDays = evidence.thresholds?.freshness_window_days ?? DEFAULT_FRESHNESS_WINDOW_DAYS;
4664
+ const dominanceThreshold = evidence.thresholds?.low_tier_dominance_threshold ?? DEFAULT_LOW_TIER_DOMINANCE_THRESHOLD;
4665
+ if (evidence.attestation_count === 0) {
4666
+ out.push({
4667
+ layer: "l4",
4668
+ code: "NO_REPUTATION_HISTORY",
4669
+ severity: "warning",
4670
+ description: "Signing identity has no recorded reputation attestations",
4671
+ mitigation: "Complete interactions that produce reputation_record calls, or import a portable reputation bundle"
4672
+ });
4673
+ } else {
4674
+ const lowTierCount = (evidence.tier_distribution["self-attested"] ?? 0) + (evidence.tier_distribution["unverified"] ?? 0);
4675
+ const lowTierShare = lowTierCount / evidence.attestation_count;
4676
+ if (lowTierShare > dominanceThreshold) {
4677
+ const pct = Math.round(lowTierShare * 100);
4678
+ out.push({
4679
+ layer: "l4",
4680
+ code: "LOW_TIER_DOMINANCE",
4681
+ severity: "info",
4682
+ description: `${pct}% of attestations are self-attested or unverified`,
4683
+ mitigation: "Complete sovereignty handshakes with counterparties to upgrade future attestations to verified tiers"
4684
+ });
4685
+ }
4686
+ if (evidence.most_recent_attestation_at) {
4687
+ const mostRecentMs = new Date(evidence.most_recent_attestation_at).getTime();
4688
+ if (!isNaN(mostRecentMs)) {
4689
+ const ageMs = now.getTime() - mostRecentMs;
4690
+ const windowMs = freshnessDays * 24 * 60 * 60 * 1e3;
4691
+ if (ageMs > windowMs) {
4692
+ const ageDays = Math.round(ageMs / (24 * 60 * 60 * 1e3));
4693
+ out.push({
4694
+ layer: "l4",
4695
+ code: "STALE_REPUTATION",
4696
+ severity: "info",
4697
+ description: `Most recent attestation is ${ageDays} days old (freshness window: ${freshnessDays} days)`,
4698
+ mitigation: "Record a fresh interaction outcome or refresh reputation from active counterparties"
4699
+ });
4700
+ }
4701
+ }
4702
+ }
4703
+ if (evidence.dispute_count > 0) {
4704
+ out.push({
4705
+ layer: "l4",
4706
+ code: "DISPUTE_ON_RECORD",
4707
+ severity: "warning",
4708
+ description: `${evidence.dispute_count} attestation${evidence.dispute_count === 1 ? "" : "s"} marked as disputed`,
4709
+ mitigation: "Review disputed interactions; counterparties may weigh this signal when evaluating trust"
4710
+ });
4711
+ }
4712
+ }
4713
+ if (!evidence.verascore_linked) {
4714
+ out.push({
4715
+ layer: "l4",
4716
+ code: "NO_VERASCORE_LINK",
4717
+ severity: "info",
4718
+ description: "No successful reputation_publish call for this identity \u2014 reputation is not externally discoverable",
4719
+ mitigation: "Run reputation_publish to link this identity to a Verascore profile"
4720
+ });
4721
+ }
4722
+ return out;
4723
+ }
4447
4724
  function generateSHR(identityId, opts) {
4448
- const { config, identityManager, masterKey, validityMs } = opts;
4725
+ const { config, identityManager, masterKey, validityMs, l4Evidence, now: nowOverride } = opts;
4449
4726
  const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
4450
4727
  if (!identity) {
4451
4728
  return "No identity available for signing. Create an identity first.";
4452
4729
  }
4453
- const now = /* @__PURE__ */ new Date();
4730
+ const now = nowOverride ?? /* @__PURE__ */ new Date();
4454
4731
  const expiresAt = new Date(now.getTime() + (validityMs ?? DEFAULT_VALIDITY_MS));
4455
4732
  const degradations = [];
4456
4733
  if (config.execution.environment === "local-process") {
@@ -4469,6 +4746,9 @@ function generateSHR(identityId, opts) {
4469
4746
  mitigation: "TEE attestation planned for a future release"
4470
4747
  });
4471
4748
  }
4749
+ const l4Degradations = l4Evidence ? deriveL4Degradations(l4Evidence, now) : [];
4750
+ degradations.push(...l4Degradations);
4751
+ const l4Status = l4Degradations.length > 0 ? "degraded" : "active";
4472
4752
  const body = {
4473
4753
  shr_version: "1.0",
4474
4754
  implementation: {
@@ -4499,7 +4779,7 @@ function generateSHR(identityId, opts) {
4499
4779
  selective_disclosure: true
4500
4780
  },
4501
4781
  l4: {
4502
- status: "active",
4782
+ status: l4Status,
4503
4783
  reputation_mode: config.reputation.mode,
4504
4784
  attestation_format: config.reputation.attestation_format,
4505
4785
  reputation_portable: true
@@ -7460,7 +7740,7 @@ function generateFortressViewHTML(options) {
7460
7740
  <head>
7461
7741
  <meta charset="UTF-8">
7462
7742
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7463
- <title>Sanctuary \u2014 Fortress View</title>
7743
+ <title>Sanctuary</title>
7464
7744
  <style>
7465
7745
  :root {
7466
7746
  --bg: #0d1117;
@@ -7849,7 +8129,7 @@ function generateFortressViewHTML(options) {
7849
8129
  <div class="fortress-brand">
7850
8130
  <div class="shield">&#x1F6E1;</div>
7851
8131
  <div>
7852
- <h1>Sanctuary Cocoon</h1>
8132
+ <h1>Sanctuary</h1>
7853
8133
  <div class="version">v${esc(options.serverVersion)}</div>
7854
8134
  </div>
7855
8135
  </div>
@@ -8391,6 +8671,10 @@ var RATE_LIMIT_WINDOW_MS = 6e4;
8391
8671
  var RATE_LIMIT_GENERAL = 120;
8392
8672
  var RATE_LIMIT_DECISIONS = 20;
8393
8673
  var MAX_RATE_LIMIT_ENTRIES = 1e4;
8674
+ function isDashboardViewRoute(method, path) {
8675
+ if (method !== "GET") return false;
8676
+ return path === "/" || path === "/dashboard" || path === "/fortress" || path === "/events";
8677
+ }
8394
8678
  var DashboardApprovalChannel = class {
8395
8679
  config;
8396
8680
  pending = /* @__PURE__ */ new Map();
@@ -8458,20 +8742,22 @@ var DashboardApprovalChannel = class {
8458
8742
  * Start the HTTP(S) server for the dashboard.
8459
8743
  */
8460
8744
  async start() {
8745
+ const handler = (req, res) => this.handleRequest(req, res);
8746
+ let server;
8747
+ if (this.useTLS && this.config.tls) {
8748
+ const tlsOpts = {
8749
+ cert: await promises.readFile(this.config.tls.cert_path),
8750
+ key: await promises.readFile(this.config.tls.key_path)
8751
+ };
8752
+ server = https.createServer(tlsOpts, handler);
8753
+ } else {
8754
+ server = http.createServer(handler);
8755
+ }
8756
+ this.httpServer = server;
8461
8757
  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
8758
  const protocol = this.useTLS ? "https" : "http";
8473
8759
  const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
8474
- this.httpServer.listen(this.config.port, this.config.host, () => {
8760
+ server.listen(this.config.port, this.config.host, () => {
8475
8761
  const sessionUrl = this.authToken ? this.createSessionUrl() : baseUrl;
8476
8762
  process.stderr.write(
8477
8763
  `
@@ -8495,7 +8781,7 @@ var DashboardApprovalChannel = class {
8495
8781
  }
8496
8782
  resolve();
8497
8783
  });
8498
- this.httpServer.on("error", (err) => {
8784
+ server.on("error", (err) => {
8499
8785
  if (err.code === "EADDRINUSE") {
8500
8786
  const port = this.config.port;
8501
8787
  process.stderr.write(
@@ -8784,13 +9070,14 @@ var DashboardApprovalChannel = class {
8784
9070
  }
8785
9071
  if (method === "GET" && url.pathname === "/" && this.authToken) {
8786
9072
  if (!this.isAuthenticated(req, url)) {
8787
- if (!this.checkRateLimit(req, res, "general")) return;
8788
9073
  this.serveLoginPage(res);
8789
9074
  return;
8790
9075
  }
8791
9076
  }
8792
9077
  if (!this.checkAuth(req, url, res)) return;
8793
- if (!this.checkRateLimit(req, res, "general")) return;
9078
+ if (!isDashboardViewRoute(method, url.pathname)) {
9079
+ if (!this.checkRateLimit(req, res, "general")) return;
9080
+ }
8794
9081
  try {
8795
9082
  if (method === "GET" && url.pathname === "/fortress") {
8796
9083
  this.serveFortressView(res);
@@ -9084,16 +9371,7 @@ data: ${JSON.stringify(initData)}
9084
9371
  res.end(JSON.stringify({ identities: [], count: 0 }));
9085
9372
  return;
9086
9373
  }
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
- }));
9374
+ const identities = this.identityManager.listWithRotationCount();
9097
9375
  const primary = this.identityManager.getDefault();
9098
9376
  res.writeHead(200, { "Content-Type": "application/json" });
9099
9377
  res.end(JSON.stringify({
@@ -9698,16 +9976,6 @@ var INVISIBLE_CHARS = [
9698
9976
  ];
9699
9977
  var VARIATION_SELECTOR_RANGE_START = 65024;
9700
9978
  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
9979
  var BASE64_STANDARD_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
9712
9980
  var BASE64URL_PATTERN = /^[A-Za-z0-9_-]+={0,2}$/;
9713
9981
  var BASE64_BLOCK_PATTERN = /[A-Za-z0-9+/]{20,}={0,2}/g;
@@ -10324,10 +10592,8 @@ var InjectionDetector = class {
10324
10592
  });
10325
10593
  }
10326
10594
  }
10327
- let zeroWidthCount = 0;
10328
- for (const char of ZERO_WIDTH_CHARS) {
10329
- zeroWidthCount += (value.match(new RegExp(char, "g")) || []).length;
10330
- }
10595
+ const zeroWidthMatches = value.match(/[\u200B\u200C\u200D\uFEFF]/g);
10596
+ const zeroWidthCount = zeroWidthMatches ? zeroWidthMatches.length : 0;
10331
10597
  if (zeroWidthCount > 0) {
10332
10598
  signals.push({
10333
10599
  type: "encoding_evasion",
@@ -11320,6 +11586,16 @@ function transformDegradations(degradations) {
11320
11586
  authzImpact = "Must share entire data context, cannot redact";
11321
11587
  } else if (deg.code === "BASIC_SYBIL_ONLY") {
11322
11588
  authzImpact = "Restrict to interactions with known agents only";
11589
+ } else if (deg.code === "NO_REPUTATION_HISTORY") {
11590
+ authzImpact = "No prior reputation \u2014 treat as a new counterparty; require escrow or human approval for value transfer";
11591
+ } else if (deg.code === "LOW_TIER_DOMINANCE") {
11592
+ authzImpact = "Reputation dominated by self-attested / unverified signers \u2014 weight accordingly, require additional confirmation on high-value actions";
11593
+ } else if (deg.code === "STALE_REPUTATION") {
11594
+ authzImpact = "Reputation is stale \u2014 may not reflect current behavior; refresh before relying on it";
11595
+ } else if (deg.code === "DISPUTE_ON_RECORD") {
11596
+ authzImpact = "Disputes on record \u2014 review dispute context before extending trust beyond low-value interactions";
11597
+ } else if (deg.code === "NO_VERASCORE_LINK") {
11598
+ authzImpact = "No external reputation profile \u2014 counterparty cannot independently discover reputation bundle";
11323
11599
  } else {
11324
11600
  authzImpact = "Unknown authorization impact";
11325
11601
  }
@@ -11426,12 +11702,37 @@ function transformSHRGeneric(shr) {
11426
11702
  }
11427
11703
 
11428
11704
  // src/shr/tools.ts
11429
- function createSHRTools(config, identityManager, masterKey, auditLog) {
11705
+ async function gatherL4Evidence(reputationStore, auditLog, identity) {
11706
+ const summary = await reputationStore.summarizeForSHR(identity.did);
11707
+ const published = await auditLog.query({
11708
+ layer: "l4",
11709
+ operation_type: "reputation_publish",
11710
+ limit: 500
11711
+ });
11712
+ const verascoreLinked = published.entries.some(
11713
+ (e) => e.result === "success" && e.identity_id === identity.identity_id
11714
+ );
11715
+ return {
11716
+ attestation_count: summary.attestation_count,
11717
+ tier_distribution: summary.tier_distribution,
11718
+ most_recent_attestation_at: summary.most_recent_attestation_at,
11719
+ dispute_count: summary.dispute_count,
11720
+ context_breakdown: summary.context_breakdown,
11721
+ verascore_linked: verascoreLinked
11722
+ };
11723
+ }
11724
+ function createSHRTools(config, identityManager, masterKey, auditLog, reputationStore) {
11430
11725
  const generatorOpts = {
11431
11726
  config,
11432
11727
  identityManager,
11433
11728
  masterKey
11434
11729
  };
11730
+ async function resolveL4Evidence(identityId) {
11731
+ if (!reputationStore) return void 0;
11732
+ const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
11733
+ if (!identity) return void 0;
11734
+ return gatherL4Evidence(reputationStore, auditLog, identity);
11735
+ }
11435
11736
  const tools = [
11436
11737
  {
11437
11738
  name: "shr_generate",
@@ -11451,9 +11752,12 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
11451
11752
  },
11452
11753
  handler: async (args) => {
11453
11754
  const validityMs = args.validity_minutes ? args.validity_minutes * 60 * 1e3 : void 0;
11454
- const result = generateSHR(args.identity_id, {
11755
+ const identityId = args.identity_id;
11756
+ const l4Evidence = await resolveL4Evidence(identityId);
11757
+ const result = generateSHR(identityId, {
11455
11758
  ...generatorOpts,
11456
- validityMs
11759
+ validityMs,
11760
+ l4Evidence
11457
11761
  });
11458
11762
  if (typeof result === "string") {
11459
11763
  return toolResult({ error: result });
@@ -11512,9 +11816,12 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
11512
11816
  handler: async (args) => {
11513
11817
  const format = args.format || "ping";
11514
11818
  const validityMs = args.validity_minutes ? args.validity_minutes * 60 * 1e3 : void 0;
11515
- const shrResult = generateSHR(args.identity_id, {
11819
+ const identityId = args.identity_id;
11820
+ const l4Evidence = await resolveL4Evidence(identityId);
11821
+ const shrResult = generateSHR(identityId, {
11516
11822
  ...generatorOpts,
11517
- validityMs
11823
+ validityMs,
11824
+ l4Evidence
11518
11825
  });
11519
11826
  if (typeof shrResult === "string") {
11520
11827
  return toolResult({ error: shrResult });
@@ -17213,8 +17520,27 @@ function validateVerascoreUrl(urlStr, configuredUrl) {
17213
17520
  }
17214
17521
  }
17215
17522
  function createSanctuaryTools(opts) {
17216
- const { config, identityManager, masterKey, auditLog, policy, keyProtection } = opts;
17523
+ const { config, identityManager, masterKey, auditLog, policy, keyProtection, reputationStore } = opts;
17217
17524
  const identityEncKey = derivePurposeKey(masterKey, "identity-encryption");
17525
+ async function l4EvidenceForIdentity(identity) {
17526
+ if (!reputationStore) return void 0;
17527
+ return gatherL4Evidence(reputationStore, auditLog, identity);
17528
+ }
17529
+ function emptyL4Evidence() {
17530
+ return {
17531
+ attestation_count: 0,
17532
+ tier_distribution: {
17533
+ "verified-sovereign": 0,
17534
+ "verified-degraded": 0,
17535
+ "self-attested": 0,
17536
+ "unverified": 0
17537
+ },
17538
+ most_recent_attestation_at: null,
17539
+ dispute_count: 0,
17540
+ context_breakdown: {},
17541
+ verascore_linked: false
17542
+ };
17543
+ }
17218
17544
  const tools = [
17219
17545
  // ─── sanctuary_bootstrap ───────────────────────────────────────────
17220
17546
  {
@@ -17254,7 +17580,8 @@ function createSanctuaryTools(opts) {
17254
17580
  const shr = generateSHR(publicIdentity.identity_id, {
17255
17581
  config,
17256
17582
  identityManager,
17257
- masterKey
17583
+ masterKey,
17584
+ l4Evidence: emptyL4Evidence()
17258
17585
  });
17259
17586
  if (typeof shr === "string") {
17260
17587
  return toolResult({
@@ -17413,10 +17740,12 @@ function createSanctuaryTools(opts) {
17413
17740
  error: "No identity found. Create one with identity_create first."
17414
17741
  });
17415
17742
  }
17743
+ const l4Evidence = await l4EvidenceForIdentity(identity);
17416
17744
  const shr = generateSHR(identity.identity_id, {
17417
17745
  config,
17418
17746
  identityManager,
17419
- masterKey
17747
+ masterKey,
17748
+ l4Evidence
17420
17749
  });
17421
17750
  const attestations = args.attestations ?? [];
17422
17751
  const body = {
@@ -19218,7 +19547,7 @@ the Sanctuary oversight gate recorded the following activity:
19218
19547
  | Outcome | Count |
19219
19548
  |---|---|
19220
19549
  | 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 }} |
19550
+ | Gate allow_proxy (Sanctuary MCP-proxy pass-through) | {{ gate_allow_proxy_count }} |
19222
19551
  | Gate deny (approval denied or timeout) | {{ gate_deny_count }} |
19223
19552
  | Gate escalate (Tier 2 anomaly raised for human review) | {{ gate_escalate_count }} |
19224
19553
  | Gate unclassified (no matching rule, default behaviour applied) | {{ gate_unclassified_count }} |
@@ -20851,10 +21180,1206 @@ var MemoryStorage = class {
20851
21180
  }
20852
21181
  };
20853
21182
 
21183
+ // src/dashboard/aggregator.ts
21184
+ var L4_DEGRADATION_IMPACT = {
21185
+ critical: 40,
21186
+ warning: 25,
21187
+ info: 10
21188
+ };
21189
+ function computeL4LayerScore(degradations, status) {
21190
+ if (status === "compromised") return 0;
21191
+ let score = 100;
21192
+ for (const deg of degradations) {
21193
+ score -= L4_DEGRADATION_IMPACT[deg.severity] ?? 10;
21194
+ }
21195
+ score = Math.max(0, score);
21196
+ if (degradations.length === 0 && score > 50) {
21197
+ score = Math.min(100, score + 5);
21198
+ }
21199
+ return Math.round(score);
21200
+ }
21201
+ var MAX_ACTIVITY = 50;
21202
+ var MAX_AUDIT = 50;
21203
+ function fingerprintDID(did) {
21204
+ const raw = did.replace(/^did:[a-z0-9]+:/i, "");
21205
+ if (raw.length <= 12) return raw;
21206
+ return `${raw.slice(0, 6)}\u2026${raw.slice(-6)}`;
21207
+ }
21208
+ function countInjectionsToday(audit) {
21209
+ const startOfDay = /* @__PURE__ */ new Date();
21210
+ startOfDay.setHours(0, 0, 0, 0);
21211
+ const cutoff = startOfDay.getTime();
21212
+ return audit.filter((e) => {
21213
+ const ts = new Date(e.timestamp).getTime();
21214
+ if (isNaN(ts) || ts < cutoff) return false;
21215
+ const op = (e.operation ?? "").toLowerCase();
21216
+ return op.includes("injection") || op.includes("blocked");
21217
+ }).length;
21218
+ }
21219
+ var PROOF_CREATION_OPS = /* @__PURE__ */ new Set([
21220
+ "zk_prove",
21221
+ "zk_range_prove",
21222
+ "proof_commitment"
21223
+ ]);
21224
+ function countProofsToday(audit) {
21225
+ const startOfDay = /* @__PURE__ */ new Date();
21226
+ startOfDay.setHours(0, 0, 0, 0);
21227
+ const cutoff = startOfDay.getTime();
21228
+ return audit.filter((e) => {
21229
+ if (e.layer !== "l3") return false;
21230
+ if (!PROOF_CREATION_OPS.has(e.operation)) return false;
21231
+ const ts = new Date(e.timestamp).getTime();
21232
+ return !isNaN(ts) && ts >= cutoff;
21233
+ }).length;
21234
+ }
21235
+ function buildAgent(sources) {
21236
+ if (!sources.identityManager) {
21237
+ return {
21238
+ display_name: "Unclaimed agent",
21239
+ did: null,
21240
+ did_fingerprint: null,
21241
+ identity_count: 0,
21242
+ primary_identity_id: null
21243
+ };
21244
+ }
21245
+ const primary = sources.identityManager.getDefault();
21246
+ const identities = sources.identityManager.list();
21247
+ if (!primary) {
21248
+ return {
21249
+ display_name: "Unclaimed agent",
21250
+ did: null,
21251
+ did_fingerprint: null,
21252
+ identity_count: identities.length,
21253
+ primary_identity_id: null
21254
+ };
21255
+ }
21256
+ return {
21257
+ display_name: primary.label || "Sovereign agent",
21258
+ did: primary.did,
21259
+ did_fingerprint: fingerprintDID(primary.did),
21260
+ identity_count: identities.length,
21261
+ primary_identity_id: primary.identity_id
21262
+ };
21263
+ }
21264
+ function buildL1(sources, audit) {
21265
+ const hasIdentity = !!sources.identityManager?.getDefault();
21266
+ const state = hasIdentity ? "full" : "degraded";
21267
+ return {
21268
+ label: "L1 Cognitive",
21269
+ state,
21270
+ headline: hasIdentity ? "State encrypted at rest" : "No sovereign identity \u2014 run sanctuary_bootstrap",
21271
+ encryption: "AES-256-GCM + HKDF per namespace",
21272
+ injection_blocked_today: countInjectionsToday(audit),
21273
+ memory_attest_ready: hasIdentity
21274
+ };
21275
+ }
21276
+ function buildL2(sources) {
21277
+ const teeAvailable = sources.teeAvailable ?? false;
21278
+ const state = teeAvailable ? "full" : "degraded";
21279
+ return {
21280
+ label: "L2 Operational",
21281
+ state,
21282
+ headline: teeAvailable ? "Hardware isolation active" : "Process isolation \u2014 no TEE on this host",
21283
+ isolation_type: teeAvailable ? "hardware-tee" : "process-level",
21284
+ tee_available: teeAvailable,
21285
+ tee_status: teeAvailable ? "Attested" : "Not available \u2014 normal on local dev",
21286
+ sandbox_status: "Principal Policy gate active"
21287
+ };
21288
+ }
21289
+ function buildL3(sources, audit) {
21290
+ const VC_ISSUING_OPS = /* @__PURE__ */ new Set([
21291
+ "reputation_record",
21292
+ "bootstrap_provide_guarantee",
21293
+ "reputation_publish"
21294
+ ]);
21295
+ const didActive = !!sources.identityManager?.getDefault()?.did;
21296
+ const vcCount = audit.filter(
21297
+ (e) => e.layer === "l4" && VC_ISSUING_OPS.has(e.operation)
21298
+ ).length;
21299
+ return {
21300
+ label: "L3 Disclosure",
21301
+ state: didActive ? "full" : "degraded",
21302
+ headline: didActive ? "Selective disclosure ready" : "No DID \u2014 disclosure unavailable",
21303
+ did_active: didActive,
21304
+ vc_count: vcCount,
21305
+ proofs_today: countProofsToday(audit)
21306
+ };
21307
+ }
21308
+ function buildL4(sources) {
21309
+ const rep = sources.reputation;
21310
+ const hasDid = !!sources.identityManager?.getDefault()?.did;
21311
+ const evidenceBlock = buildL4EvidenceBlock(sources);
21312
+ const base = rep?.score != null ? {
21313
+ label: "L4 Reputation",
21314
+ state: "full",
21315
+ headline: "Verascore attached",
21316
+ score: rep.score,
21317
+ profile_url: rep.profile_url,
21318
+ claim_cta: null
21319
+ } : hasDid ? {
21320
+ label: "L4 Reputation",
21321
+ state: "degraded",
21322
+ headline: "Claim your profile",
21323
+ score: null,
21324
+ profile_url: null,
21325
+ claim_cta: "Claim your profile at verascore.ai"
21326
+ } : {
21327
+ label: "L4 Reputation",
21328
+ state: "degraded",
21329
+ headline: "No identity claimed",
21330
+ score: null,
21331
+ profile_url: null,
21332
+ claim_cta: "Claim your profile at verascore.ai"
21333
+ };
21334
+ if (!evidenceBlock) return base;
21335
+ const nextState = evidenceBlock.active_degradations.length > 0 && base.state === "full" ? "degraded" : base.state;
21336
+ const nextHeadline = nextState === base.state ? base.headline : "Attached, but evidence is degraded";
21337
+ return {
21338
+ ...base,
21339
+ state: nextState,
21340
+ headline: nextHeadline,
21341
+ evidence: evidenceBlock.evidence,
21342
+ layer_score: evidenceBlock.layer_score,
21343
+ active_degradations: evidenceBlock.active_degradations
21344
+ };
21345
+ }
21346
+ function buildL4EvidenceBlock(sources) {
21347
+ const ev = sources.l4Evidence;
21348
+ if (!ev) return null;
21349
+ const degradations = deriveL4Degradations(ev, sources.l4Now ?? /* @__PURE__ */ new Date());
21350
+ const status = degradations.length > 0 ? "degraded" : "full";
21351
+ const layer_score = computeL4LayerScore(degradations, status);
21352
+ return {
21353
+ evidence: {
21354
+ attestation_count: ev.attestation_count,
21355
+ tier_distribution: ev.tier_distribution,
21356
+ most_recent_attestation_at: ev.most_recent_attestation_at,
21357
+ dispute_count: ev.dispute_count,
21358
+ context_breakdown: ev.context_breakdown ?? {},
21359
+ verascore_linked: ev.verascore_linked
21360
+ },
21361
+ layer_score,
21362
+ active_degradations: degradations.map((d) => ({
21363
+ code: d.code,
21364
+ severity: d.severity,
21365
+ description: d.description,
21366
+ ...d.mitigation !== void 0 ? { mitigation: d.mitigation } : {}
21367
+ }))
21368
+ };
21369
+ }
21370
+ function computeOverall(l1, l2, l3, l4) {
21371
+ const critical = [l1.state, l3.state, l4.state];
21372
+ if (critical.includes("compromised") || l2.state === "compromised") {
21373
+ return {
21374
+ status: "compromised",
21375
+ light: "red",
21376
+ headline: "Sovereignty compromised"
21377
+ };
21378
+ }
21379
+ const allCriticalFull = critical.every((s) => s === "full");
21380
+ if (allCriticalFull && l2.state === "full") {
21381
+ return {
21382
+ status: "healthy",
21383
+ light: "green",
21384
+ headline: "All layers full"
21385
+ };
21386
+ }
21387
+ if (allCriticalFull && l2.state === "degraded") {
21388
+ return {
21389
+ status: "healthy",
21390
+ light: "green",
21391
+ headline: "L1\xB7L3\xB7L4 full \u2014 L2 degraded (no TEE on this host)"
21392
+ };
21393
+ }
21394
+ return {
21395
+ status: "degraded",
21396
+ light: "yellow",
21397
+ headline: "One or more layers degraded"
21398
+ };
21399
+ }
21400
+ function buildUpstreamServers(sources) {
21401
+ if (!sources.clientManager) return [];
21402
+ return sources.clientManager.getStatus().map((s) => {
21403
+ const entry = {
21404
+ name: s.name,
21405
+ state: s.state,
21406
+ tool_count: s.tool_count
21407
+ };
21408
+ if (s.error) entry.error = s.error;
21409
+ return entry;
21410
+ });
21411
+ }
21412
+ async function getProtectionSnapshot(sources) {
21413
+ let audit = [];
21414
+ if (sources.auditLog) {
21415
+ try {
21416
+ const result = await sources.auditLog.query({ limit: MAX_AUDIT });
21417
+ audit = result.entries;
21418
+ } catch {
21419
+ audit = [];
21420
+ }
21421
+ }
21422
+ const agent = buildAgent(sources);
21423
+ const l1 = buildL1(sources, audit);
21424
+ const l2 = buildL2(sources);
21425
+ const l3 = buildL3(sources, audit);
21426
+ const l4 = buildL4(sources);
21427
+ const activity = (sources.activity ?? []).slice(0, MAX_ACTIVITY);
21428
+ const pending_approvals = sources.pendingApprovals ?? [];
21429
+ const upstream_servers = buildUpstreamServers(sources);
21430
+ return {
21431
+ overall: computeOverall(l1, l2, l3, l4),
21432
+ agent,
21433
+ layers: { l1, l2, l3, l4 },
21434
+ activity,
21435
+ pending_approvals,
21436
+ audit: audit.slice(-MAX_AUDIT).reverse(),
21437
+ upstream_servers,
21438
+ mode: sources.mode,
21439
+ server_version: sources.server_version,
21440
+ generated_at: (/* @__PURE__ */ new Date()).toISOString()
21441
+ };
21442
+ }
21443
+
21444
+ // src/dashboard/html.ts
21445
+ var HERO_COPY = "Your agent is protected.";
21446
+ function escHtml(value) {
21447
+ if (value == null) return "";
21448
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
21449
+ }
21450
+ function layerCard(layer, extra) {
21451
+ return `<section class="layer-card layer-${escHtml(layer.state)}" data-layer-label="${escHtml(layer.label)}">
21452
+ <div class="layer-head">
21453
+ <div class="layer-dot"></div>
21454
+ <div>
21455
+ <h3>${escHtml(layer.label)}</h3>
21456
+ <p class="layer-headline">${escHtml(layer.headline)}</p>
21457
+ </div>
21458
+ </div>
21459
+ <dl class="layer-detail">${extra}</dl>
21460
+ </section>`;
21461
+ }
21462
+ function l1Card(l1) {
21463
+ return layerCard(
21464
+ l1,
21465
+ `<div><dt>Encryption</dt><dd>${escHtml(l1.encryption)}</dd></div>
21466
+ <div><dt>Injections blocked today</dt><dd>${escHtml(l1.injection_blocked_today)}</dd></div>
21467
+ <div><dt>memory_attest</dt><dd>${l1.memory_attest_ready ? "Ready" : "Not ready"}</dd></div>`
21468
+ );
21469
+ }
21470
+ function l2Card(l2) {
21471
+ return layerCard(
21472
+ l2,
21473
+ `<div><dt>Isolation</dt><dd>${escHtml(l2.isolation_type)}</dd></div>
21474
+ <div><dt>TEE</dt><dd>${escHtml(l2.tee_status)}</dd></div>
21475
+ <div><dt>Sandbox</dt><dd>${escHtml(l2.sandbox_status)}</dd></div>`
21476
+ );
21477
+ }
21478
+ function l3Card(l3) {
21479
+ return layerCard(
21480
+ l3,
21481
+ `<div><dt>DID</dt><dd>${l3.did_active ? "Active" : "None"}</dd></div>
21482
+ <div><dt>Credentials</dt><dd>${escHtml(l3.vc_count)}</dd></div>
21483
+ <div><dt>Proofs today</dt><dd>${escHtml(l3.proofs_today)}</dd></div>`
21484
+ );
21485
+ }
21486
+ function l4Card(l4) {
21487
+ 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>`;
21488
+ return layerCard(
21489
+ l4,
21490
+ `<div class="layer-cta">${score}</div>${l4EvidenceBlock(l4)}`
21491
+ );
21492
+ }
21493
+ function formatRelativeDays(iso) {
21494
+ if (!iso) return "none on record";
21495
+ const ts = new Date(iso).getTime();
21496
+ if (isNaN(ts)) return "unknown";
21497
+ const days = Math.round((Date.now() - ts) / (24 * 60 * 60 * 1e3));
21498
+ if (days <= 0) return "today";
21499
+ if (days === 1) return "1 day ago";
21500
+ return `${days} days ago`;
21501
+ }
21502
+ function l4EvidenceBlock(l4) {
21503
+ if (!l4.evidence) return "";
21504
+ const ev = l4.evidence;
21505
+ const tierSovereign = ev.tier_distribution["verified-sovereign"];
21506
+ const tierDegraded = ev.tier_distribution["verified-degraded"];
21507
+ const tierSelf = ev.tier_distribution["self-attested"];
21508
+ const tierUnverified = ev.tier_distribution["unverified"];
21509
+ const contextCount = Object.keys(ev.context_breakdown).length;
21510
+ const mostRecent = formatRelativeDays(ev.most_recent_attestation_at);
21511
+ const score = l4.layer_score ?? 100;
21512
+ const summaryLine = `
21513
+ <div><dt>L4 score</dt><dd>${escHtml(score)} / 100</dd></div>
21514
+ <div><dt>Attestations</dt><dd>${escHtml(ev.attestation_count)}</dd></div>
21515
+ <div><dt>Verified tiers</dt><dd>${escHtml(tierSovereign)} sovereign \xB7 ${escHtml(tierDegraded)} degraded</dd></div>
21516
+ <div><dt>Lower tiers</dt><dd>${escHtml(tierSelf)} self \xB7 ${escHtml(tierUnverified)} unverified</dd></div>
21517
+ <div><dt>Contexts</dt><dd>${escHtml(contextCount)}</dd></div>
21518
+ <div><dt>Disputes</dt><dd>${escHtml(ev.dispute_count)}</dd></div>
21519
+ <div><dt>Last activity</dt><dd>${escHtml(mostRecent)}</dd></div>
21520
+ <div><dt>Verascore link</dt><dd>${ev.verascore_linked ? "Yes" : "Not linked"}</dd></div>
21521
+ `;
21522
+ const degs = l4.active_degradations ?? [];
21523
+ const degList = degs.length === 0 ? "" : `<ul class="l4-deg-list">${degs.map(
21524
+ (d) => `
21525
+ <li class="l4-deg l4-deg-${escHtml(d.severity)}">
21526
+ <div class="l4-deg-head">
21527
+ <span class="l4-deg-code">${escHtml(d.code)}</span>
21528
+ <span class="l4-deg-sev">${escHtml(d.severity)}</span>
21529
+ </div>
21530
+ <p class="l4-deg-desc">${escHtml(d.description)}</p>
21531
+ ${d.mitigation ? `<p class="l4-deg-mit">${escHtml(d.mitigation)}</p>` : ""}
21532
+ </li>`
21533
+ ).join("")}</ul>`;
21534
+ return `
21535
+ <div class="l4-evidence">
21536
+ <dl class="layer-detail l4-evidence-summary">${summaryLine}</dl>
21537
+ ${degList}
21538
+ </div>
21539
+ `;
21540
+ }
21541
+ function renderDashboardHTML(options) {
21542
+ const { snapshot } = options;
21543
+ const { overall, agent, layers, activity, pending_approvals, audit, upstream_servers } = snapshot;
21544
+ const activityRows = activity.length === 0 ? `<tr class="empty"><td colspan="5">Waiting for tool calls\u2026</td></tr>` : activity.map((entry) => {
21545
+ const time = new Date(entry.timestamp).toLocaleTimeString();
21546
+ return `<tr class="result-${escHtml(entry.result)}">
21547
+ <td class="mono time">${escHtml(time)}</td>
21548
+ <td class="mono">${escHtml(entry.tool)}</td>
21549
+ <td class="mono">${escHtml(entry.server)}</td>
21550
+ <td class="tier tier-${escHtml(entry.tier)}">T${escHtml(entry.tier)}</td>
21551
+ <td class="result">${escHtml(entry.result)}</td>
21552
+ </tr>`;
21553
+ }).join("");
21554
+ 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)}">
21555
+ <div class="approval-head">
21556
+ <span class="tier-chip tier-${escHtml(p.tier)}">Tier ${escHtml(p.tier)}</span>
21557
+ <span class="mono">${escHtml(p.operation)}</span>
21558
+ </div>
21559
+ <p class="approval-reason">${escHtml(p.reason)}</p>
21560
+ <div class="approval-actions">
21561
+ <button class="btn btn-allow" data-action="allow" data-id="${escHtml(p.id)}">Allow</button>
21562
+ <button class="btn btn-deny" data-action="deny" data-id="${escHtml(p.id)}">Deny</button>
21563
+ </div>
21564
+ </article>`).join("");
21565
+ 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)}">
21566
+ <td class="mono time">${escHtml(new Date(entry.timestamp).toLocaleTimeString())}</td>
21567
+ <td class="layer-pill">${escHtml(entry.layer.toUpperCase())}</td>
21568
+ <td class="mono">${escHtml(entry.operation)}</td>
21569
+ <td class="result-${escHtml(entry.result)}">${escHtml(entry.result)}</td>
21570
+ </tr>`).join("");
21571
+ 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)}">
21572
+ <span class="server-dot"></span>
21573
+ <span class="mono">${escHtml(s.name)}</span>
21574
+ <span class="server-meta">${escHtml(s.state)} \xB7 ${escHtml(s.tool_count)} tool${s.tool_count === 1 ? "" : "s"}</span>
21575
+ </li>`).join("");
21576
+ const initialSnapshot = JSON.stringify(snapshot).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
21577
+ return `<!DOCTYPE html>
21578
+ <html lang="en">
21579
+ <head>
21580
+ <meta charset="UTF-8">
21581
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
21582
+ <title>Sanctuary \u2014 Sovereignty Dashboard</title>
21583
+ <style>
21584
+ :root {
21585
+ --bg: #07080c;
21586
+ --bg-2: #0f1220;
21587
+ --surface: #131729;
21588
+ --surface-2: #1a1f36;
21589
+ --border: #26304d;
21590
+ --border-strong: #39436a;
21591
+ --ink: #eef1fb;
21592
+ --ink-dim: #b9c0dc;
21593
+ --ink-mute: #7e86a8;
21594
+ --indigo: #6e7bff;
21595
+ --indigo-deep: #3b4ad8;
21596
+ --green: #3ee08f;
21597
+ --green-deep: #168a4d;
21598
+ --amber: #f1c05a;
21599
+ --red: #ff6b7a;
21600
+ --violet: #a77bff;
21601
+ --radius: 14px;
21602
+ --radius-sm: 8px;
21603
+ --shadow: 0 14px 42px rgba(3, 6, 19, 0.45);
21604
+ --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
21605
+ }
21606
+ * { margin: 0; padding: 0; box-sizing: border-box; }
21607
+ 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; }
21608
+ body { background: radial-gradient(circle at 50% -200px, rgba(110, 123, 255, 0.18), transparent 60%), var(--bg); }
21609
+ a { color: var(--indigo); text-decoration: none; }
21610
+ button { font: inherit; cursor: pointer; }
21611
+
21612
+ .wrap { max-width: 1180px; margin: 0 auto; padding: 40px 32px 120px; }
21613
+
21614
+ /* \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 */
21615
+ .meta-row {
21616
+ display: flex;
21617
+ justify-content: space-between;
21618
+ align-items: center;
21619
+ color: var(--ink-mute);
21620
+ font-size: 12px;
21621
+ letter-spacing: 0.08em;
21622
+ text-transform: uppercase;
21623
+ margin-bottom: 8px;
21624
+ }
21625
+ .meta-row .mode-pill {
21626
+ padding: 4px 10px;
21627
+ border-radius: 999px;
21628
+ border: 1px solid var(--border);
21629
+ background: var(--surface);
21630
+ font-family: var(--mono);
21631
+ text-transform: none;
21632
+ letter-spacing: 0;
21633
+ font-size: 11px;
21634
+ color: var(--ink-dim);
21635
+ }
21636
+
21637
+ /* \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 */
21638
+ .hero {
21639
+ display: flex;
21640
+ flex-direction: column;
21641
+ align-items: center;
21642
+ text-align: center;
21643
+ padding: 48px 24px 56px;
21644
+ border-radius: 22px;
21645
+ background:
21646
+ radial-gradient(circle at 50% 0%, rgba(62, 224, 143, 0.08), transparent 70%),
21647
+ linear-gradient(180deg, var(--bg-2) 0%, var(--surface) 100%);
21648
+ border: 1px solid var(--border);
21649
+ box-shadow: var(--shadow);
21650
+ margin-bottom: 32px;
21651
+ position: relative;
21652
+ overflow: hidden;
21653
+ }
21654
+ .hero::after {
21655
+ content: "";
21656
+ position: absolute;
21657
+ inset: 0;
21658
+ background: radial-gradient(circle at 50% 100%, rgba(110, 123, 255, 0.10), transparent 60%);
21659
+ pointer-events: none;
21660
+ }
21661
+ .shield {
21662
+ width: 200px;
21663
+ height: 200px;
21664
+ position: relative;
21665
+ filter: drop-shadow(0 16px 32px rgba(62, 224, 143, 0.18));
21666
+ animation: hero-in 600ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
21667
+ }
21668
+ @keyframes hero-in {
21669
+ from { opacity: 0; transform: translateY(12px) scale(0.96); }
21670
+ to { opacity: 1; transform: translateY(0) scale(1); }
21671
+ }
21672
+ .shield svg { width: 100%; height: 100%; display: block; }
21673
+ .shield.green .shield-ring { stroke: var(--green); }
21674
+ .shield.green .shield-core { fill: rgba(62, 224, 143, 0.14); }
21675
+ .shield.green .shield-mark { stroke: var(--green); }
21676
+ .shield.yellow .shield-ring { stroke: var(--amber); }
21677
+ .shield.yellow .shield-core { fill: rgba(241, 192, 90, 0.14); }
21678
+ .shield.yellow .shield-mark { stroke: var(--amber); }
21679
+ .shield.red .shield-ring { stroke: var(--red); }
21680
+ .shield.red .shield-core { fill: rgba(255, 107, 122, 0.16); }
21681
+ .shield.red .shield-mark { stroke: var(--red); }
21682
+ .shield .shield-ring {
21683
+ fill: none;
21684
+ stroke-width: 3;
21685
+ stroke-dasharray: 600;
21686
+ stroke-dashoffset: 0;
21687
+ transform-origin: center;
21688
+ transition: stroke 320ms ease;
21689
+ }
21690
+ .shield .shield-ring-bg { fill: none; stroke: rgba(255, 255, 255, 0.06); stroke-width: 3; }
21691
+ .shield .shield-mark { fill: none; stroke-width: 4; stroke-linecap: round; stroke-linejoin: round; transition: stroke 320ms ease; }
21692
+
21693
+ .hero h1 {
21694
+ font-size: 40px;
21695
+ font-weight: 650;
21696
+ letter-spacing: -0.02em;
21697
+ margin-top: 28px;
21698
+ position: relative;
21699
+ z-index: 1;
21700
+ }
21701
+ .hero .hero-sub {
21702
+ margin-top: 10px;
21703
+ color: var(--ink-dim);
21704
+ font-size: 15px;
21705
+ position: relative;
21706
+ z-index: 1;
21707
+ }
21708
+ .identity-line {
21709
+ margin-top: 22px;
21710
+ display: inline-flex;
21711
+ align-items: center;
21712
+ gap: 10px;
21713
+ padding: 10px 18px;
21714
+ border-radius: 999px;
21715
+ border: 1px solid var(--border);
21716
+ background: rgba(19, 23, 41, 0.7);
21717
+ font-family: var(--mono);
21718
+ font-size: 13px;
21719
+ color: var(--ink-dim);
21720
+ position: relative;
21721
+ z-index: 1;
21722
+ }
21723
+ .identity-line .name { color: var(--ink); font-weight: 500; font-family: -apple-system, BlinkMacSystemFont, Inter, sans-serif; letter-spacing: 0; }
21724
+ .identity-line .sep { color: var(--ink-mute); }
21725
+ .identity-line .did { color: var(--violet); }
21726
+ .identity-line .primary-flag {
21727
+ padding: 2px 8px;
21728
+ border-radius: 999px;
21729
+ background: rgba(110, 123, 255, 0.16);
21730
+ color: var(--indigo);
21731
+ font-size: 11px;
21732
+ text-transform: uppercase;
21733
+ letter-spacing: 0.1em;
21734
+ font-family: -apple-system, BlinkMacSystemFont, Inter, sans-serif;
21735
+ }
21736
+
21737
+ /* \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 */
21738
+ .layer-grid {
21739
+ display: grid;
21740
+ grid-template-columns: repeat(4, 1fr);
21741
+ gap: 16px;
21742
+ margin-bottom: 32px;
21743
+ }
21744
+ @media (max-width: 960px) { .layer-grid { grid-template-columns: repeat(2, 1fr); } }
21745
+ @media (max-width: 520px) { .layer-grid { grid-template-columns: 1fr; } }
21746
+
21747
+ .layer-card {
21748
+ padding: 20px;
21749
+ border-radius: var(--radius);
21750
+ background: var(--surface);
21751
+ border: 1px solid var(--border);
21752
+ transition: border 220ms ease, transform 220ms ease;
21753
+ }
21754
+ .layer-card:hover { border-color: var(--border-strong); transform: translateY(-1px); }
21755
+
21756
+ .layer-head { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 14px; }
21757
+ .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); }
21758
+ .layer-full .layer-dot { background: var(--green); box-shadow: 0 0 0 3px rgba(62, 224, 143, 0.18); }
21759
+ .layer-degraded .layer-dot { background: var(--amber); box-shadow: 0 0 0 3px rgba(241, 192, 90, 0.18); }
21760
+ .layer-compromised .layer-dot { background: var(--red); box-shadow: 0 0 0 3px rgba(255, 107, 122, 0.18); }
21761
+ .layer-head h3 { font-size: 13px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-dim); margin-bottom: 4px; }
21762
+ .layer-headline { font-size: 15px; color: var(--ink); line-height: 1.35; }
21763
+
21764
+ .layer-detail { display: flex; flex-direction: column; gap: 8px; }
21765
+ .layer-detail > div { display: flex; justify-content: space-between; gap: 12px; font-size: 12px; }
21766
+ .layer-detail dt { color: var(--ink-mute); text-transform: uppercase; letter-spacing: 0.06em; font-size: 11px; }
21767
+ .layer-detail dd { color: var(--ink-dim); text-align: right; font-family: var(--mono); font-size: 12px; }
21768
+
21769
+ .layer-cta { margin-top: 6px; text-align: center; padding: 16px; border-radius: var(--radius-sm); background: var(--bg-2); border: 1px solid var(--border); }
21770
+ .score-block { display: flex; flex-direction: column; gap: 2px; }
21771
+ .score-value { font-size: 28px; font-weight: 650; color: var(--green); letter-spacing: -0.02em; }
21772
+ .score-label { font-size: 11px; text-transform: uppercase; color: var(--ink-mute); letter-spacing: 0.08em; }
21773
+ .claim-block { font-size: 13px; color: var(--violet); }
21774
+
21775
+ /* \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 */
21776
+ .l4-evidence { margin-top: 12px; padding-top: 12px; border-top: 1px dashed var(--border); }
21777
+ .l4-evidence-summary { gap: 6px; margin-bottom: 10px; }
21778
+ .l4-evidence-summary dt { font-size: 10px; }
21779
+ .l4-evidence-summary dd { font-size: 11px; }
21780
+ .l4-deg-list { list-style: none; display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
21781
+ .l4-deg { padding: 8px 10px; border-radius: var(--radius-sm); background: var(--bg-2); border: 1px solid var(--border); font-size: 12px; }
21782
+ .l4-deg-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; margin-bottom: 3px; }
21783
+ .l4-deg-code { font-family: var(--mono); font-size: 11px; color: var(--ink); letter-spacing: 0.02em; }
21784
+ .l4-deg-sev { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-mute); }
21785
+ .l4-deg-warning { border-color: rgba(241, 192, 90, 0.4); }
21786
+ .l4-deg-warning .l4-deg-sev { color: var(--amber); }
21787
+ .l4-deg-critical { border-color: rgba(255, 107, 122, 0.5); }
21788
+ .l4-deg-critical .l4-deg-sev { color: var(--red); }
21789
+ .l4-deg-info .l4-deg-sev { color: var(--indigo); }
21790
+ .l4-deg-desc { color: var(--ink-dim); line-height: 1.35; font-size: 11px; }
21791
+ .l4-deg-mit { color: var(--ink-mute); line-height: 1.35; font-size: 10px; margin-top: 3px; font-style: italic; }
21792
+
21793
+ /* \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 */
21794
+ .section { margin-bottom: 28px; }
21795
+ .section h2 {
21796
+ font-size: 13px;
21797
+ text-transform: uppercase;
21798
+ letter-spacing: 0.1em;
21799
+ color: var(--ink-dim);
21800
+ margin-bottom: 12px;
21801
+ display: flex;
21802
+ align-items: center;
21803
+ gap: 12px;
21804
+ }
21805
+ .section h2 .count {
21806
+ font-family: var(--mono);
21807
+ font-size: 11px;
21808
+ padding: 2px 8px;
21809
+ border-radius: 999px;
21810
+ background: var(--surface);
21811
+ border: 1px solid var(--border);
21812
+ color: var(--ink-mute);
21813
+ text-transform: none;
21814
+ letter-spacing: 0;
21815
+ }
21816
+
21817
+ /* \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 */
21818
+ .approval-list { display: flex; flex-direction: column; gap: 10px; }
21819
+ .approval {
21820
+ padding: 16px;
21821
+ background: var(--surface);
21822
+ border: 1px solid var(--amber);
21823
+ border-radius: var(--radius);
21824
+ box-shadow: 0 0 0 1px rgba(241, 192, 90, 0.18);
21825
+ animation: fade-up 260ms ease both;
21826
+ }
21827
+ @keyframes fade-up { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
21828
+ .approval-head { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; font-size: 14px; }
21829
+ .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; }
21830
+ .tier-chip.tier-1 { background: rgba(255, 107, 122, 0.16); color: var(--red); }
21831
+ .tier-chip.tier-2 { background: rgba(241, 192, 90, 0.16); color: var(--amber); }
21832
+ .approval-reason { font-size: 13px; color: var(--ink-dim); margin-bottom: 12px; }
21833
+ .approval-actions { display: flex; gap: 8px; }
21834
+ .btn {
21835
+ padding: 7px 14px;
21836
+ border-radius: var(--radius-sm);
21837
+ border: 1px solid var(--border);
21838
+ background: var(--surface-2);
21839
+ color: var(--ink);
21840
+ font-size: 13px;
21841
+ transition: all 160ms ease;
21842
+ }
21843
+ .btn:hover { border-color: var(--border-strong); background: #202641; }
21844
+ .btn-allow { background: var(--green-deep); border-color: var(--green-deep); color: white; }
21845
+ .btn-allow:hover { background: #1ba25b; border-color: #1ba25b; }
21846
+ .btn-deny { background: transparent; border-color: var(--red); color: var(--red); }
21847
+ .btn-deny:hover { background: rgba(255, 107, 122, 0.1); border-color: var(--red); }
21848
+
21849
+ /* \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 */
21850
+ .panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
21851
+ .panel-head { display: flex; justify-content: space-between; align-items: center; padding: 12px 18px; border-bottom: 1px solid var(--border); }
21852
+ .panel-head h3 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-dim); }
21853
+ .filter-row { display: flex; gap: 6px; }
21854
+ .filter-row button {
21855
+ padding: 4px 10px;
21856
+ font-size: 11px;
21857
+ border-radius: 999px;
21858
+ background: transparent;
21859
+ border: 1px solid var(--border);
21860
+ color: var(--ink-mute);
21861
+ text-transform: uppercase;
21862
+ letter-spacing: 0.06em;
21863
+ transition: all 140ms ease;
21864
+ }
21865
+ .filter-row button.active, .filter-row button:hover { color: var(--ink); border-color: var(--border-strong); background: var(--surface-2); }
21866
+
21867
+ table { width: 100%; border-collapse: collapse; }
21868
+ th, td { padding: 10px 18px; text-align: left; font-size: 13px; border-bottom: 1px solid var(--border); }
21869
+ th { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-mute); font-weight: 500; background: var(--bg-2); }
21870
+ tr:last-child td { border-bottom: none; }
21871
+ tr.empty td { text-align: center; color: var(--ink-mute); padding: 32px 18px; }
21872
+ .mono { font-family: var(--mono); font-size: 12px; }
21873
+ .time { color: var(--ink-mute); white-space: nowrap; }
21874
+ .tier { text-align: center; font-family: var(--mono); font-size: 11px; font-weight: 600; }
21875
+ .tier-1 { color: var(--red); }
21876
+ .tier-2 { color: var(--amber); }
21877
+ .tier-3 { color: var(--green); }
21878
+ .result { font-family: var(--mono); font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; }
21879
+ .result-allowed, .result-success { color: var(--green); }
21880
+ .result-denied, .result-failure { color: var(--red); }
21881
+ .result-approved { color: var(--indigo); }
21882
+ .result-pending { color: var(--amber); }
21883
+ .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; }
21884
+
21885
+ /* \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 */
21886
+ .server-list { list-style: none; display: flex; flex-direction: column; gap: 8px; }
21887
+ .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; }
21888
+ .server-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ink-mute); }
21889
+ .server-row.state-connected .server-dot { background: var(--green); }
21890
+ .server-row.state-connecting .server-dot { background: var(--amber); }
21891
+ .server-row.state-disconnected .server-dot, .server-row.state-error .server-dot { background: var(--red); }
21892
+ .server-meta { margin-left: auto; font-size: 12px; color: var(--ink-mute); font-family: var(--mono); }
21893
+
21894
+ .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); }
21895
+
21896
+ /* \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 */
21897
+ details.audit-details { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
21898
+ details.audit-details summary { cursor: pointer; list-style: none; padding: 14px 18px; display: flex; align-items: center; justify-content: space-between; }
21899
+ details.audit-details summary::-webkit-details-marker { display: none; }
21900
+ details.audit-details summary h3 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-dim); }
21901
+ details.audit-details summary .caret { color: var(--ink-mute); font-family: var(--mono); }
21902
+ details.audit-details[open] .caret { transform: rotate(90deg); display: inline-block; }
21903
+ details.audit-details .audit-filters { display: flex; gap: 6px; padding: 0 18px 12px; border-bottom: 1px solid var(--border); }
21904
+ </style>
21905
+ </head>
21906
+ <body>
21907
+ <div class="wrap">
21908
+ <div class="meta-row">
21909
+ <span>Sanctuary Framework</span>
21910
+ <span class="mode-pill" id="mode-pill">${escHtml(snapshot.mode)} \xB7 v${escHtml(snapshot.server_version)}</span>
21911
+ </div>
21912
+
21913
+ <section class="hero">
21914
+ <div class="shield ${escHtml(overall.light)}" id="shield">
21915
+ <svg viewBox="0 0 200 200" aria-hidden="true">
21916
+ <circle class="shield-ring-bg" cx="100" cy="100" r="92"></circle>
21917
+ <circle class="shield-ring" cx="100" cy="100" r="92"></circle>
21918
+ <circle class="shield-core" cx="100" cy="100" r="80" fill="rgba(255,255,255,0.02)"></circle>
21919
+ <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>
21920
+ <path class="shield-mark" d="M85 102 L96 113 L118 90"></path>
21921
+ </svg>
21922
+ </div>
21923
+ <h1 id="hero-copy">${escHtml(HERO_COPY)}</h1>
21924
+ <p class="hero-sub" id="hero-sub">${escHtml(overall.headline)}</p>
21925
+ <div class="identity-line" id="identity-line">
21926
+ <span class="name" id="agent-name">${escHtml(agent.display_name)}</span>
21927
+ <span class="sep">\xB7</span>
21928
+ <span class="did" id="agent-did">${escHtml(agent.did_fingerprint ?? "unclaimed")}</span>
21929
+ ${agent.primary_identity_id ? `<span class="primary-flag">Primary</span>` : ""}
21930
+ </div>
21931
+ </section>
21932
+
21933
+ <div class="layer-grid" id="layer-grid">
21934
+ ${l1Card(layers.l1)}
21935
+ ${l2Card(layers.l2)}
21936
+ ${l3Card(layers.l3)}
21937
+ ${l4Card(layers.l4)}
21938
+ </div>
21939
+
21940
+ <section class="section" id="approval-section">
21941
+ <h2>Needs approval <span class="count" id="approval-count">${pending_approvals.length}</span></h2>
21942
+ <div class="approval-list" id="approval-list">${approvalItems}</div>
21943
+ </section>
21944
+
21945
+ <section class="section">
21946
+ <h2>Upstream servers <span class="count">${upstream_servers.length}</span></h2>
21947
+ <ul class="server-list" id="server-list">${serverRows}</ul>
21948
+ </section>
21949
+
21950
+ <section class="section">
21951
+ <h2>Live activity <span class="count" id="activity-count">${activity.length}</span></h2>
21952
+ <div class="panel">
21953
+ <div class="panel-head"><h3>Recent tool calls</h3></div>
21954
+ <table>
21955
+ <thead><tr><th>Time</th><th>Tool</th><th>Server</th><th>Tier</th><th>Result</th></tr></thead>
21956
+ <tbody id="activity-body">${activityRows}</tbody>
21957
+ </table>
21958
+ </div>
21959
+ </section>
21960
+
21961
+ <section class="section">
21962
+ <details class="audit-details">
21963
+ <summary>
21964
+ <h3>Audit trail <span class="count" id="audit-count">${audit.length}</span></h3>
21965
+ <span class="caret">\u25B8</span>
21966
+ </summary>
21967
+ <div class="audit-filters">
21968
+ <div class="filter-row" id="audit-filter">
21969
+ <button class="active" data-filter="all">All</button>
21970
+ <button data-filter="l1">Cognitive</button>
21971
+ <button data-filter="l2">Operational</button>
21972
+ <button data-filter="l3">Disclosure</button>
21973
+ <button data-filter="l4">Reputation</button>
21974
+ </div>
21975
+ </div>
21976
+ <table>
21977
+ <thead><tr><th>Time</th><th>Layer</th><th>Operation</th><th>Result</th></tr></thead>
21978
+ <tbody id="audit-body">${auditRows}</tbody>
21979
+ </table>
21980
+ </details>
21981
+ </section>
21982
+ </div>
21983
+
21984
+ <script>
21985
+ (() => {
21986
+ const INITIAL_SNAPSHOT = ${initialSnapshot};
21987
+ const AUTH_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
21988
+ const AUTH_HEADER = AUTH_TOKEN ? { "Authorization": "Bearer " + AUTH_TOKEN } : {};
21989
+ const AUTH_QS = AUTH_TOKEN ? "?token=" + encodeURIComponent(AUTH_TOKEN) : "";
21990
+
21991
+ let snapshot = INITIAL_SNAPSHOT;
21992
+
21993
+ function esc(value) {
21994
+ if (value == null) return "";
21995
+ return String(value)
21996
+ .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
21997
+ .replace(/"/g, "&quot;").replace(/'/g, "&#039;");
21998
+ }
21999
+ function fmtTime(iso) {
22000
+ try { return new Date(iso).toLocaleTimeString(); } catch { return iso; }
22001
+ }
22002
+
22003
+ function renderShield(light, headline) {
22004
+ const shield = document.getElementById("shield");
22005
+ if (!shield) return;
22006
+ shield.classList.remove("green", "yellow", "red");
22007
+ shield.classList.add(light);
22008
+ document.getElementById("hero-sub").textContent = headline;
22009
+ }
22010
+
22011
+ function renderActivity(entries) {
22012
+ const body = document.getElementById("activity-body");
22013
+ const count = document.getElementById("activity-count");
22014
+ if (!body) return;
22015
+ count.textContent = String(entries.length);
22016
+ if (!entries.length) {
22017
+ body.innerHTML = '<tr class="empty"><td colspan="5">Waiting for tool calls\u2026</td></tr>';
22018
+ return;
22019
+ }
22020
+ body.innerHTML = entries.map(e => (
22021
+ '<tr class="result-' + esc(e.result) + '">' +
22022
+ '<td class="mono time">' + esc(fmtTime(e.timestamp)) + '</td>' +
22023
+ '<td class="mono">' + esc(e.tool) + '</td>' +
22024
+ '<td class="mono">' + esc(e.server) + '</td>' +
22025
+ '<td class="tier tier-' + esc(e.tier) + '">T' + esc(e.tier) + '</td>' +
22026
+ '<td class="result">' + esc(e.result) + '</td>' +
22027
+ '</tr>'
22028
+ )).join("");
22029
+ }
22030
+
22031
+ function renderApprovals(list) {
22032
+ const container = document.getElementById("approval-list");
22033
+ const count = document.getElementById("approval-count");
22034
+ if (!container) return;
22035
+ count.textContent = String(list.length);
22036
+ if (!list.length) {
22037
+ container.innerHTML = '<div class="empty-block">No pending approvals</div>';
22038
+ return;
22039
+ }
22040
+ container.innerHTML = list.map(p => (
22041
+ '<article class="approval" data-id="' + esc(p.id) + '">' +
22042
+ '<div class="approval-head">' +
22043
+ '<span class="tier-chip tier-' + esc(p.tier) + '">Tier ' + esc(p.tier) + '</span>' +
22044
+ '<span class="mono">' + esc(p.operation) + '</span>' +
22045
+ '</div>' +
22046
+ '<p class="approval-reason">' + esc(p.reason) + '</p>' +
22047
+ '<div class="approval-actions">' +
22048
+ '<button class="btn btn-allow" data-action="allow" data-id="' + esc(p.id) + '">Allow</button>' +
22049
+ '<button class="btn btn-deny" data-action="deny" data-id="' + esc(p.id) + '">Deny</button>' +
22050
+ '</div>' +
22051
+ '</article>'
22052
+ )).join("");
22053
+ wireApprovalButtons();
22054
+ }
22055
+
22056
+ function renderAudit(entries, filter) {
22057
+ filter = filter || "all";
22058
+ const body = document.getElementById("audit-body");
22059
+ const count = document.getElementById("audit-count");
22060
+ if (!body) return;
22061
+ const visible = filter === "all" ? entries : entries.filter(e => e.layer === filter);
22062
+ count.textContent = String(visible.length);
22063
+ if (!visible.length) {
22064
+ body.innerHTML = '<tr class="empty"><td colspan="4">Audit log empty</td></tr>';
22065
+ return;
22066
+ }
22067
+ body.innerHTML = visible.map(e => (
22068
+ '<tr data-kind="' + esc(e.layer) + '" data-op="' + esc(e.operation) + '">' +
22069
+ '<td class="mono time">' + esc(fmtTime(e.timestamp)) + '</td>' +
22070
+ '<td><span class="layer-pill">' + esc(String(e.layer).toUpperCase()) + '</span></td>' +
22071
+ '<td class="mono">' + esc(e.operation) + '</td>' +
22072
+ '<td class="result-' + esc(e.result) + '">' + esc(e.result) + '</td>' +
22073
+ '</tr>'
22074
+ )).join("");
22075
+ }
22076
+
22077
+ function renderAll(snap) {
22078
+ snapshot = snap;
22079
+ renderShield(snap.overall.light, snap.overall.headline);
22080
+ const mp = document.getElementById("mode-pill");
22081
+ if (mp) mp.textContent = snap.mode + " \xB7 v" + snap.server_version;
22082
+ document.getElementById("agent-name").textContent = snap.agent.display_name;
22083
+ document.getElementById("agent-did").textContent = snap.agent.did_fingerprint || "unclaimed";
22084
+ renderActivity(snap.activity);
22085
+ renderApprovals(snap.pending_approvals);
22086
+ renderAudit(snap.audit, currentAuditFilter());
22087
+ }
22088
+
22089
+ function currentAuditFilter() {
22090
+ const active = document.querySelector("#audit-filter button.active");
22091
+ return active ? active.dataset.filter : "all";
22092
+ }
22093
+
22094
+ function wireApprovalButtons() {
22095
+ document.querySelectorAll("#approval-list button[data-action]").forEach(btn => {
22096
+ btn.addEventListener("click", async () => {
22097
+ const id = btn.dataset.id;
22098
+ const action = btn.dataset.action;
22099
+ btn.disabled = true;
22100
+ try {
22101
+ await fetch("/api/approvals/" + encodeURIComponent(id) + "/" + action + AUTH_QS, {
22102
+ method: "POST", headers: AUTH_HEADER
22103
+ });
22104
+ } catch {}
22105
+ await refreshSnapshot();
22106
+ });
22107
+ });
22108
+ }
22109
+
22110
+ function wireAuditFilter() {
22111
+ document.querySelectorAll("#audit-filter button").forEach(btn => {
22112
+ btn.addEventListener("click", () => {
22113
+ document.querySelectorAll("#audit-filter button").forEach(b => b.classList.remove("active"));
22114
+ btn.classList.add("active");
22115
+ renderAudit(snapshot.audit, btn.dataset.filter);
22116
+ });
22117
+ });
22118
+ }
22119
+
22120
+ async function refreshSnapshot() {
22121
+ try {
22122
+ const res = await fetch("/api/snapshot" + AUTH_QS, { headers: AUTH_HEADER });
22123
+ if (!res.ok) return;
22124
+ const snap = await res.json();
22125
+ renderAll(snap);
22126
+ } catch {}
22127
+ }
22128
+
22129
+ function connectSSE() {
22130
+ try {
22131
+ const es = new EventSource("/api/stream" + AUTH_QS);
22132
+ es.addEventListener("snapshot", (ev) => {
22133
+ try { renderAll(JSON.parse(ev.data)); } catch {}
22134
+ });
22135
+ es.addEventListener("activity", () => refreshSnapshot());
22136
+ es.addEventListener("approval", () => refreshSnapshot());
22137
+ es.onerror = () => { es.close(); setTimeout(connectSSE, 3000); };
22138
+ } catch {}
22139
+ }
22140
+
22141
+ wireApprovalButtons();
22142
+ wireAuditFilter();
22143
+ connectSSE();
22144
+ })();
22145
+ </script>
22146
+ </body>
22147
+ </html>`;
22148
+ }
22149
+
22150
+ // src/dashboard/api.ts
22151
+ function constantTimeEquals(a, b) {
22152
+ if (a.length !== b.length) return false;
22153
+ let diff = 0;
22154
+ for (let i = 0; i < a.length; i++) {
22155
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
22156
+ }
22157
+ return diff === 0;
22158
+ }
22159
+ function extractToken(req, url) {
22160
+ const header = req.headers.authorization;
22161
+ if (header && header.startsWith("Bearer ")) {
22162
+ return header.slice(7).trim();
22163
+ }
22164
+ const q = url.searchParams.get("token");
22165
+ return q ?? null;
22166
+ }
22167
+ function isAuthorized(deps, req, url) {
22168
+ if (!deps.authToken) return true;
22169
+ const token = extractToken(req, url);
22170
+ if (!token) return false;
22171
+ return constantTimeEquals(token, deps.authToken);
22172
+ }
22173
+ function writeJSON(res, status, payload) {
22174
+ res.writeHead(status, {
22175
+ "Content-Type": "application/json",
22176
+ "Cache-Control": "no-store"
22177
+ });
22178
+ res.end(JSON.stringify(payload));
22179
+ }
22180
+ function writeText(res, status, body, contentType = "text/plain") {
22181
+ res.writeHead(status, {
22182
+ "Content-Type": contentType,
22183
+ "Cache-Control": "no-store"
22184
+ });
22185
+ res.end(body);
22186
+ }
22187
+ async function handleRequest(deps, req, res) {
22188
+ const host = req.headers.host || "localhost";
22189
+ const url = new URL(req.url ?? "/", `http://${host}`);
22190
+ const method = (req.method ?? "GET").toUpperCase();
22191
+ const path = url.pathname;
22192
+ if (!isAuthorized(deps, req, url)) {
22193
+ writeJSON(res, 401, { error: "unauthorized" });
22194
+ return true;
22195
+ }
22196
+ if (method === "GET" && path === "/api/health") {
22197
+ writeJSON(res, 200, { ok: true, mode: deps.sources.mode });
22198
+ return true;
22199
+ }
22200
+ if (method === "GET" && (path === "/" || path === "/index.html")) {
22201
+ const snapshot = await getProtectionSnapshot(deps.sources);
22202
+ const html = renderDashboardHTML({ snapshot, authToken: deps.authToken });
22203
+ writeText(res, 200, html, "text/html; charset=utf-8");
22204
+ return true;
22205
+ }
22206
+ if (method === "GET" && path === "/api/snapshot") {
22207
+ const snapshot = await getProtectionSnapshot(deps.sources);
22208
+ writeJSON(res, 200, snapshot);
22209
+ return true;
22210
+ }
22211
+ const approvalMatch = /^\/api\/approvals\/([^/]+)\/(allow|deny)$/.exec(path);
22212
+ if (method === "POST" && approvalMatch) {
22213
+ const id = decodeURIComponent(approvalMatch[1]);
22214
+ const action = approvalMatch[2];
22215
+ if (!deps.approvals) {
22216
+ writeJSON(res, 503, { error: "approvals_unavailable" });
22217
+ return true;
22218
+ }
22219
+ const handler = action === "allow" ? deps.approvals.allow : deps.approvals.deny;
22220
+ try {
22221
+ const ok = await handler(id);
22222
+ writeJSON(res, ok ? 200 : 404, { id, action, ok });
22223
+ } catch (err) {
22224
+ writeJSON(res, 500, { error: "approval_failed", message: err.message });
22225
+ }
22226
+ return true;
22227
+ }
22228
+ if (method === "GET" && path === "/api/stream") {
22229
+ await handleStream(deps, res);
22230
+ return true;
22231
+ }
22232
+ return false;
22233
+ }
22234
+ async function handleStream(deps, res) {
22235
+ res.writeHead(200, {
22236
+ "Content-Type": "text/event-stream",
22237
+ "Cache-Control": "no-cache, no-transform",
22238
+ Connection: "keep-alive",
22239
+ "X-Accel-Buffering": "no"
22240
+ });
22241
+ const snapshot = await getProtectionSnapshot(deps.sources);
22242
+ res.write(`event: snapshot
22243
+ data: ${JSON.stringify(snapshot)}
22244
+
22245
+ `);
22246
+ const unsubscribe = deps.onEvent ? deps.onEvent((event) => {
22247
+ try {
22248
+ res.write(`event: ${event.type}
22249
+ data: ${JSON.stringify(event.data)}
22250
+
22251
+ `);
22252
+ } catch {
22253
+ }
22254
+ }) : () => {
22255
+ };
22256
+ const keepAlive = setInterval(() => {
22257
+ try {
22258
+ res.write(": keepalive\n\n");
22259
+ } catch {
22260
+ }
22261
+ }, 25e3);
22262
+ const cleanup = () => {
22263
+ clearInterval(keepAlive);
22264
+ unsubscribe();
22265
+ };
22266
+ res.on("close", cleanup);
22267
+ res.on("error", cleanup);
22268
+ }
22269
+
22270
+ // src/dashboard/server.ts
22271
+ var DEFAULT_PORT = 3501;
22272
+ var DEFAULT_HOST = "127.0.0.1";
22273
+ async function startDashboardServer(options) {
22274
+ const port = options.port ?? DEFAULT_PORT;
22275
+ const host = options.host ?? DEFAULT_HOST;
22276
+ const listeners = /* @__PURE__ */ new Set();
22277
+ const onEvent = (listener) => {
22278
+ listeners.add(listener);
22279
+ return () => listeners.delete(listener);
22280
+ };
22281
+ const publish = (event) => {
22282
+ for (const listener of listeners) {
22283
+ try {
22284
+ listener(event);
22285
+ } catch {
22286
+ }
22287
+ }
22288
+ };
22289
+ const deps = {
22290
+ sources: options.sources,
22291
+ authToken: options.authToken,
22292
+ approvals: options.approvals,
22293
+ onEvent
22294
+ };
22295
+ const server = http.createServer(async (req, res) => {
22296
+ try {
22297
+ const served = await handleRequest(deps, req, res);
22298
+ if (!served) {
22299
+ res.writeHead(404, { "Content-Type": "application/json" });
22300
+ res.end(JSON.stringify({ error: "not_found", path: req.url }));
22301
+ }
22302
+ } catch (err) {
22303
+ try {
22304
+ res.writeHead(500, { "Content-Type": "application/json" });
22305
+ res.end(JSON.stringify({ error: "internal", message: err.message }));
22306
+ } catch {
22307
+ }
22308
+ }
22309
+ });
22310
+ await new Promise((resolve, reject) => {
22311
+ server.once("error", reject);
22312
+ server.listen(port, host, () => {
22313
+ server.off("error", reject);
22314
+ resolve();
22315
+ });
22316
+ });
22317
+ const actualPort = (() => {
22318
+ const addr = server.address();
22319
+ if (addr && typeof addr === "object") return addr.port;
22320
+ return port;
22321
+ })();
22322
+ const url = `http://${host}:${actualPort}`;
22323
+ return {
22324
+ url,
22325
+ port: actualPort,
22326
+ host,
22327
+ stop: () => new Promise((resolve, reject) => {
22328
+ server.close((err) => err ? reject(err) : resolve());
22329
+ }),
22330
+ publish,
22331
+ publishActivity: (entry) => publish({ type: "activity", data: entry }),
22332
+ publishApproval: (approval) => publish({ type: "approval", data: approval })
22333
+ };
22334
+ }
22335
+
22336
+ // src/dashboard/index.ts
22337
+ async function startDashboard(options) {
22338
+ const activity = options.initialActivity ? [...options.initialActivity] : [];
22339
+ const pending = options.initialPendingApprovals ? [...options.initialPendingApprovals] : [];
22340
+ const sources = {
22341
+ mode: options.mode,
22342
+ server_version: options.serverVersion,
22343
+ ...options.auditLog ? { auditLog: options.auditLog } : {},
22344
+ ...options.identityManager ? { identityManager: options.identityManager } : {},
22345
+ ...options.clientManager ? { clientManager: options.clientManager } : {},
22346
+ ...options.baseline ? { baseline: options.baseline } : {},
22347
+ ...options.policy ? { policy: options.policy } : {},
22348
+ ...options.reputation ? { reputation: options.reputation } : {},
22349
+ ...options.teeAvailable != null ? { teeAvailable: options.teeAvailable } : {},
22350
+ ...options.l4Evidence ? { l4Evidence: options.l4Evidence } : {},
22351
+ activity,
22352
+ pendingApprovals: pending
22353
+ };
22354
+ const serverOpts = {
22355
+ mode: options.mode,
22356
+ sources,
22357
+ ...options.port != null ? { port: options.port } : {},
22358
+ ...options.host ? { host: options.host } : {},
22359
+ ...options.authToken ? { authToken: options.authToken } : {},
22360
+ ...options.approvals ? { approvals: options.approvals } : {}
22361
+ };
22362
+ const handle = await startDashboardServer(serverOpts);
22363
+ const wrapped = {
22364
+ ...handle,
22365
+ publishActivity: (entry) => {
22366
+ activity.unshift(entry);
22367
+ if (activity.length > 50) activity.length = 50;
22368
+ handle.publishActivity(entry);
22369
+ },
22370
+ publishApproval: (approval) => {
22371
+ pending.push(approval);
22372
+ handle.publishApproval(approval);
22373
+ }
22374
+ };
22375
+ return wrapped;
22376
+ }
22377
+
20854
22378
  // src/index.ts
20855
22379
  async function createSanctuaryServer(options) {
20856
22380
  const config = await loadConfig(options?.configPath);
20857
22381
  await promises.mkdir(config.storage_path, { recursive: true, mode: 448 });
22382
+ await tightenStoragePermissions(config.storage_path);
20858
22383
  const storage = options?.storage ?? new FilesystemStorage(
20859
22384
  `${config.storage_path}/state`
20860
22385
  );
@@ -21174,12 +22699,6 @@ async function createSanctuaryServer(options) {
21174
22699
  }
21175
22700
  };
21176
22701
  const { tools: l3Tools } = createL3Tools(storage, masterKey, auditLog);
21177
- const { tools: shrTools } = createSHRTools(
21178
- config,
21179
- identityManager,
21180
- masterKey,
21181
- auditLog
21182
- );
21183
22702
  const { tools: handshakeTools, handshakeResults } = createHandshakeTools(
21184
22703
  config,
21185
22704
  identityManager,
@@ -21190,7 +22709,7 @@ async function createSanctuaryServer(options) {
21190
22709
  verascoreUrl: config.verascore.url
21191
22710
  }
21192
22711
  );
21193
- const { tools: l4Tools} = createL4Tools(
22712
+ const { tools: l4Tools, reputationStore } = createL4Tools(
21194
22713
  storage,
21195
22714
  masterKey,
21196
22715
  identityManager,
@@ -21198,6 +22717,13 @@ async function createSanctuaryServer(options) {
21198
22717
  handshakeResults,
21199
22718
  config.verascore.url
21200
22719
  );
22720
+ const { tools: shrTools } = createSHRTools(
22721
+ config,
22722
+ identityManager,
22723
+ masterKey,
22724
+ auditLog,
22725
+ reputationStore
22726
+ );
21201
22727
  const { tools: federationTools } = createFederationTools(
21202
22728
  auditLog,
21203
22729
  handshakeResults
@@ -21288,7 +22814,8 @@ async function createSanctuaryServer(options) {
21288
22814
  masterKey,
21289
22815
  auditLog,
21290
22816
  policy,
21291
- keyProtection
22817
+ keyProtection,
22818
+ reputationStore
21292
22819
  });
21293
22820
  const { tools: memoryAttestTools } = createMemoryAttestTools(
21294
22821
  identityManager,
@@ -21474,6 +23001,7 @@ exports.ContextGatePolicyStore = ContextGatePolicyStore;
21474
23001
  exports.DashboardApprovalChannel = DashboardApprovalChannel;
21475
23002
  exports.FederationRegistry = FederationRegistry;
21476
23003
  exports.FilesystemStorage = FilesystemStorage;
23004
+ exports.HERO_COPY = HERO_COPY;
21477
23005
  exports.InMemoryModelProvenanceStore = InMemoryModelProvenanceStore;
21478
23006
  exports.InjectionDetector = InjectionDetector;
21479
23007
  exports.MODEL_PRESETS = MODEL_PRESETS;
@@ -21501,15 +23029,19 @@ exports.filterContext = filterContext;
21501
23029
  exports.generateAttestation = generateAttestation;
21502
23030
  exports.generateSHR = generateSHR;
21503
23031
  exports.generateSystemPrompt = generateSystemPrompt;
23032
+ exports.getProtectionSnapshot = getProtectionSnapshot;
21504
23033
  exports.getTemplate = getTemplate;
21505
23034
  exports.initiateHandshake = initiateHandshake;
21506
23035
  exports.listTemplateIds = listTemplateIds;
21507
23036
  exports.loadConfig = loadConfig;
21508
23037
  exports.loadPrincipalPolicy = loadPrincipalPolicy;
21509
23038
  exports.recommendPolicy = recommendPolicy;
23039
+ exports.renderDashboardHTML = renderDashboardHTML;
21510
23040
  exports.resolveTier = resolveTier;
21511
23041
  exports.respondToHandshake = respondToHandshake;
21512
23042
  exports.signPayload = signPayload;
23043
+ exports.startDashboard = startDashboard;
23044
+ exports.startDashboardServer = startDashboardServer;
21513
23045
  exports.tierDistribution = tierDistribution;
21514
23046
  exports.verifyAttestation = verifyAttestation;
21515
23047
  exports.verifyBridgeCommitment = verifyBridgeCommitment;