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