@sanctuary-framework/mcp-server 0.8.0 → 0.10.0

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