@sanctuary-framework/mcp-server 0.3.0 → 0.3.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/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { sha256 } from '@noble/hashes/sha256';
3
3
  import { hmac } from '@noble/hashes/hmac';
4
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
- import { mkdir, readFile, writeFile, stat, unlink, readdir, chmod } from 'fs/promises';
5
+ import { mkdir, readFile, writeFile, stat, unlink, readdir, chmod, access } from 'fs/promises';
6
6
  import { join } from 'path';
7
7
  import { homedir } from 'os';
8
8
  import { randomBytes as randomBytes$1, createHmac } from 'crypto';
@@ -298,8 +298,13 @@ async function loadConfig(configPath) {
298
298
  try {
299
299
  const raw = await readFile(path, "utf-8");
300
300
  const fileConfig = JSON.parse(raw);
301
- return deepMerge(config, fileConfig);
302
- } catch {
301
+ const merged = deepMerge(config, fileConfig);
302
+ validateConfig(merged);
303
+ return merged;
304
+ } catch (err) {
305
+ if (err instanceof Error && err.message.includes("unimplemented features")) {
306
+ throw err;
307
+ }
303
308
  return config;
304
309
  }
305
310
  }
@@ -307,6 +312,33 @@ async function saveConfig(config, configPath) {
307
312
  const path = join(config.storage_path, "sanctuary.json");
308
313
  await writeFile(path, JSON.stringify(config, null, 2), { mode: 384 });
309
314
  }
315
+ function validateConfig(config) {
316
+ const errors = [];
317
+ const implementedKeyProtection = /* @__PURE__ */ new Set(["passphrase", "none"]);
318
+ if (!implementedKeyProtection.has(config.state.key_protection)) {
319
+ errors.push(
320
+ `Unimplemented config value: state.key_protection = "${config.state.key_protection}". Only ${[...implementedKeyProtection].map((v) => `"${v}"`).join(", ")} are currently implemented. Using an unimplemented key protection mode would silently degrade security.`
321
+ );
322
+ }
323
+ const implementedEnvironment = /* @__PURE__ */ new Set(["local-process", "docker"]);
324
+ if (!implementedEnvironment.has(config.execution.environment)) {
325
+ errors.push(
326
+ `Unimplemented config value: execution.environment = "${config.execution.environment}". Only ${[...implementedEnvironment].map((v) => `"${v}"`).join(", ")} are currently implemented. Using an unimplemented environment would silently degrade security.`
327
+ );
328
+ }
329
+ const implementedProofSystem = /* @__PURE__ */ new Set(["commitment-only"]);
330
+ if (!implementedProofSystem.has(config.disclosure.proof_system)) {
331
+ errors.push(
332
+ `Unimplemented config value: disclosure.proof_system = "${config.disclosure.proof_system}". Only ${[...implementedProofSystem].map((v) => `"${v}"`).join(", ")} is currently implemented. Using an unimplemented proof system would silently degrade security.`
333
+ );
334
+ }
335
+ if (errors.length > 0) {
336
+ throw new Error(
337
+ `Sanctuary configuration references unimplemented features:
338
+ ${errors.join("\n")}`
339
+ );
340
+ }
341
+ }
310
342
  function deepMerge(base, override) {
311
343
  const result = { ...base };
312
344
  for (const [key, value] of Object.entries(override)) {
@@ -650,7 +682,11 @@ var RESERVED_NAMESPACE_PREFIXES = [
650
682
  "_commitments",
651
683
  "_reputation",
652
684
  "_escrow",
653
- "_guarantees"
685
+ "_guarantees",
686
+ "_bridge",
687
+ "_federation",
688
+ "_handshake",
689
+ "_shr"
654
690
  ];
655
691
  var StateStore = class {
656
692
  storage;
@@ -917,12 +953,14 @@ var StateStore = class {
917
953
  /**
918
954
  * Import a previously exported state bundle.
919
955
  */
920
- async import(bundleBase64, conflictResolution = "skip") {
956
+ async import(bundleBase64, conflictResolution = "skip", publicKeyResolver) {
921
957
  const bundleBytes = fromBase64url(bundleBase64);
922
958
  const bundleJson = bytesToString(bundleBytes);
923
959
  const bundle = JSON.parse(bundleJson);
924
960
  let importedKeys = 0;
925
961
  let skippedKeys = 0;
962
+ let skippedInvalidSig = 0;
963
+ let skippedUnknownKid = 0;
926
964
  let conflicts = 0;
927
965
  const namespaces = [];
928
966
  for (const [ns, entries] of Object.entries(
@@ -936,6 +974,26 @@ var StateStore = class {
936
974
  }
937
975
  namespaces.push(ns);
938
976
  for (const { key, entry } of entries) {
977
+ const signerPublicKey = publicKeyResolver(entry.kid);
978
+ if (!signerPublicKey) {
979
+ skippedUnknownKid++;
980
+ skippedKeys++;
981
+ continue;
982
+ }
983
+ try {
984
+ const ciphertextBytes = fromBase64url(entry.payload.ct);
985
+ const signatureBytes = fromBase64url(entry.sig);
986
+ const sigValid = verify(ciphertextBytes, signatureBytes, signerPublicKey);
987
+ if (!sigValid) {
988
+ skippedInvalidSig++;
989
+ skippedKeys++;
990
+ continue;
991
+ }
992
+ } catch {
993
+ skippedInvalidSig++;
994
+ skippedKeys++;
995
+ continue;
996
+ }
939
997
  const exists = await this.storage.exists(ns, key);
940
998
  if (exists) {
941
999
  conflicts++;
@@ -971,6 +1029,8 @@ var StateStore = class {
971
1029
  return {
972
1030
  imported_keys: importedKeys,
973
1031
  skipped_keys: skippedKeys,
1032
+ skipped_invalid_sig: skippedInvalidSig,
1033
+ skipped_unknown_kid: skippedUnknownKid,
974
1034
  conflicts,
975
1035
  namespaces,
976
1036
  imported_at: (/* @__PURE__ */ new Date()).toISOString()
@@ -1159,7 +1219,11 @@ var RESERVED_NAMESPACE_PREFIXES2 = [
1159
1219
  "_commitments",
1160
1220
  "_reputation",
1161
1221
  "_escrow",
1162
- "_guarantees"
1222
+ "_guarantees",
1223
+ "_bridge",
1224
+ "_federation",
1225
+ "_handshake",
1226
+ "_shr"
1163
1227
  ];
1164
1228
  function getReservedNamespaceViolation(namespace) {
1165
1229
  for (const prefix of RESERVED_NAMESPACE_PREFIXES2) {
@@ -1496,6 +1560,13 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1496
1560
  required: ["namespace", "key"]
1497
1561
  },
1498
1562
  handler: async (args) => {
1563
+ const reservedViolation = getReservedNamespaceViolation(args.namespace);
1564
+ if (reservedViolation) {
1565
+ return toolResult({
1566
+ error: "namespace_reserved",
1567
+ message: `Namespace "${args.namespace}" is reserved for internal use (prefix: ${reservedViolation}). Cannot read from reserved namespaces.`
1568
+ });
1569
+ }
1499
1570
  const result = await stateStore.read(
1500
1571
  args.namespace,
1501
1572
  args.key,
@@ -1532,6 +1603,13 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1532
1603
  required: ["namespace"]
1533
1604
  },
1534
1605
  handler: async (args) => {
1606
+ const reservedViolation = getReservedNamespaceViolation(args.namespace);
1607
+ if (reservedViolation) {
1608
+ return toolResult({
1609
+ error: "namespace_reserved",
1610
+ message: `Namespace "${args.namespace}" is reserved for internal use (prefix: ${reservedViolation}). Cannot list reserved namespaces.`
1611
+ });
1612
+ }
1535
1613
  const result = await stateStore.list(
1536
1614
  args.namespace,
1537
1615
  args.prefix,
@@ -1610,9 +1688,15 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1610
1688
  required: ["bundle"]
1611
1689
  },
1612
1690
  handler: async (args) => {
1691
+ const publicKeyResolver = (kid) => {
1692
+ const identity = identityMgr.get(kid);
1693
+ if (!identity) return null;
1694
+ return fromBase64url(identity.public_key);
1695
+ };
1613
1696
  const result = await stateStore.import(
1614
1697
  args.bundle,
1615
- args.conflict_resolution ?? "skip"
1698
+ args.conflict_resolution ?? "skip",
1699
+ publicKeyResolver
1616
1700
  );
1617
1701
  auditLog?.append("l1", "state_import", "principal", {
1618
1702
  imported_keys: result.imported_keys
@@ -2057,7 +2141,7 @@ function createRangeProof(value, blindingFactor, commitment, min, max) {
2057
2141
  bitProofs.push(bitProof);
2058
2142
  }
2059
2143
  const sumBlinding = bitBlindings.reduce(
2060
- (acc, bi, i) => mod(acc + mod(BigInt(1 << i)) * bi),
2144
+ (acc, bi, i) => mod(acc + mod(BigInt(1) << BigInt(i)) * bi),
2061
2145
  0n
2062
2146
  );
2063
2147
  const blindingDiff = mod(b - sumBlinding);
@@ -2099,7 +2183,7 @@ function verifyRangeProof(proof) {
2099
2183
  let reconstructed = RistrettoPoint.ZERO;
2100
2184
  for (let i = 0; i < numBits; i++) {
2101
2185
  const C_i = RistrettoPoint.fromHex(fromBase64url(proof.bit_commitments[i]));
2102
- const weight = mod(BigInt(1 << i));
2186
+ const weight = mod(BigInt(1) << BigInt(i));
2103
2187
  reconstructed = reconstructed.add(safeMultiply(C_i, weight));
2104
2188
  }
2105
2189
  const diff = C.subtract(safeMultiply(G, mod(BigInt(proof.min)))).subtract(reconstructed);
@@ -3154,7 +3238,9 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
3154
3238
  contexts: summary.contexts
3155
3239
  });
3156
3240
  return toolResult({
3157
- summary
3241
+ summary,
3242
+ // SEC-ADD-03: Tag response as containing counterparty-generated attestation data
3243
+ _content_trust: "external"
3158
3244
  });
3159
3245
  }
3160
3246
  },
@@ -3475,14 +3561,16 @@ var DEFAULT_TIER2 = {
3475
3561
  };
3476
3562
  var DEFAULT_CHANNEL = {
3477
3563
  type: "stderr",
3478
- timeout_seconds: 300,
3479
- auto_deny: true
3564
+ timeout_seconds: 300
3565
+ // SEC-002: auto_deny is not configurable. Timeout always denies.
3566
+ // Field omitted intentionally — all channels hardcode deny on timeout.
3480
3567
  };
3481
3568
  var DEFAULT_POLICY = {
3482
3569
  version: 1,
3483
3570
  tier1_always_approve: [
3484
3571
  "state_export",
3485
3572
  "state_import",
3573
+ "state_delete",
3486
3574
  "identity_rotate",
3487
3575
  "reputation_import",
3488
3576
  "bootstrap_provide_guarantee"
@@ -3492,7 +3580,6 @@ var DEFAULT_POLICY = {
3492
3580
  "state_read",
3493
3581
  "state_write",
3494
3582
  "state_list",
3495
- "state_delete",
3496
3583
  "identity_create",
3497
3584
  "identity_list",
3498
3585
  "identity_sign",
@@ -3600,10 +3687,14 @@ function validatePolicy(raw) {
3600
3687
  ...raw.tier2_anomaly ?? {}
3601
3688
  },
3602
3689
  tier3_always_allow: raw.tier3_always_allow ?? DEFAULT_POLICY.tier3_always_allow,
3603
- approval_channel: {
3604
- ...DEFAULT_CHANNEL,
3605
- ...raw.approval_channel ?? {}
3606
- }
3690
+ approval_channel: (() => {
3691
+ const merged = {
3692
+ ...DEFAULT_CHANNEL,
3693
+ ...raw.approval_channel ?? {}
3694
+ };
3695
+ delete merged.auto_deny;
3696
+ return merged;
3697
+ })()
3607
3698
  };
3608
3699
  }
3609
3700
  function generateDefaultPolicyYaml() {
@@ -3620,6 +3711,7 @@ version: 1
3620
3711
  tier1_always_approve:
3621
3712
  - state_export
3622
3713
  - state_import
3714
+ - state_delete
3623
3715
  - identity_rotate
3624
3716
  - reputation_import
3625
3717
  - bootstrap_provide_guarantee
@@ -3641,7 +3733,6 @@ tier3_always_allow:
3641
3733
  - state_read
3642
3734
  - state_write
3643
3735
  - state_list
3644
- - state_delete
3645
3736
  - identity_create
3646
3737
  - identity_list
3647
3738
  - identity_sign
@@ -3678,10 +3769,10 @@ tier3_always_allow:
3678
3769
 
3679
3770
  # \u2500\u2500\u2500 Approval Channel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3680
3771
  # How Sanctuary reaches you when approval is needed.
3772
+ # NOTE: Timeout always results in denial. This is not configurable (SEC-002).
3681
3773
  approval_channel:
3682
3774
  type: stderr
3683
3775
  timeout_seconds: 300
3684
- auto_deny: true
3685
3776
  `;
3686
3777
  }
3687
3778
  async function loadPrincipalPolicy(storagePath) {
@@ -3858,27 +3949,16 @@ var BaselineTracker = class {
3858
3949
 
3859
3950
  // src/principal-policy/approval-channel.ts
3860
3951
  var StderrApprovalChannel = class {
3861
- config;
3862
- constructor(config) {
3863
- this.config = config;
3952
+ constructor(_config) {
3864
3953
  }
3865
3954
  async requestApproval(request) {
3866
3955
  const prompt = this.formatPrompt(request);
3867
3956
  process.stderr.write(prompt + "\n");
3868
- await new Promise((resolve) => setTimeout(resolve, 100));
3869
- if (this.config.auto_deny) {
3870
- return {
3871
- decision: "deny",
3872
- decided_at: (/* @__PURE__ */ new Date()).toISOString(),
3873
- decided_by: "timeout"
3874
- };
3875
- } else {
3876
- return {
3877
- decision: "approve",
3878
- decided_at: (/* @__PURE__ */ new Date()).toISOString(),
3879
- decided_by: "auto"
3880
- };
3881
- }
3957
+ return {
3958
+ decision: "deny",
3959
+ decided_at: (/* @__PURE__ */ new Date()).toISOString(),
3960
+ decided_by: "stderr:non-interactive"
3961
+ };
3882
3962
  }
3883
3963
  formatPrompt(request) {
3884
3964
  const tierLabel = request.tier === 1 ? "Tier 1 \u2014 always requires approval" : "Tier 2 \u2014 behavioral anomaly detected";
@@ -3886,7 +3966,7 @@ var StderrApprovalChannel = class {
3886
3966
  return [
3887
3967
  "",
3888
3968
  "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
3889
- "\u2551 SANCTUARY: Approval Required \u2551",
3969
+ "\u2551 SANCTUARY: Operation Denied (non-interactive channel) \u2551",
3890
3970
  "\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563",
3891
3971
  `\u2551 Operation: ${request.operation.padEnd(50)}\u2551`,
3892
3972
  `\u2551 ${tierLabel.padEnd(62)}\u2551`,
@@ -3897,7 +3977,8 @@ var StderrApprovalChannel = class {
3897
3977
  (line) => `\u2551 ${line.padEnd(60)}\u2551`
3898
3978
  ),
3899
3979
  "\u2551 \u2551",
3900
- this.config.auto_deny ? "\u2551 Auto-denying (configure approval_channel.auto_deny to change) \u2551" : "\u2551 Auto-approving (informational mode) \u2551",
3980
+ "\u2551 Denied: stderr channel cannot accept input (SEC-016) \u2551",
3981
+ "\u2551 Use dashboard or webhook channel for interactive approval. \u2551",
3901
3982
  "\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
3902
3983
  ""
3903
3984
  ].join("\n");
@@ -4201,20 +4282,38 @@ function generateDashboardHTML(options) {
4201
4282
  <script>
4202
4283
  (function() {
4203
4284
  const TIMEOUT = ${options.timeoutSeconds};
4204
- const AUTH_TOKEN = ${options.authToken ? `'${options.authToken}'` : "null"};
4285
+ // SEC-012: Auth token is passed via Authorization header only \u2014 never in URLs.
4286
+ // The token is provided by the server at generation time (embedded for initial auth).
4287
+ const AUTH_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
4288
+ let SESSION_ID = null; // Short-lived session for SSE and URL-based requests
4205
4289
  const pending = new Map();
4206
4290
  let auditCount = 0;
4207
4291
 
4208
- // Auth helpers
4292
+ // Auth helpers \u2014 SEC-012: token goes in header, session goes in URL
4209
4293
  function authHeaders() {
4210
4294
  const h = { 'Content-Type': 'application/json' };
4211
4295
  if (AUTH_TOKEN) h['Authorization'] = 'Bearer ' + AUTH_TOKEN;
4212
4296
  return h;
4213
4297
  }
4214
- function authQuery(url) {
4215
- if (!AUTH_TOKEN) return url;
4298
+ function sessionQuery(url) {
4299
+ if (!SESSION_ID) return url;
4216
4300
  const sep = url.includes('?') ? '&' : '?';
4217
- return url + sep + 'token=' + AUTH_TOKEN;
4301
+ return url + sep + 'session=' + SESSION_ID;
4302
+ }
4303
+
4304
+ // SEC-012: Exchange the long-lived token for a short-lived session
4305
+ async function exchangeSession() {
4306
+ if (!AUTH_TOKEN) return;
4307
+ try {
4308
+ const resp = await fetch('/auth/session', { method: 'POST', headers: authHeaders() });
4309
+ if (resp.ok) {
4310
+ const data = await resp.json();
4311
+ SESSION_ID = data.session_id;
4312
+ // Refresh session before expiry (at 80% of TTL)
4313
+ const refreshMs = (data.expires_in_seconds || 300) * 800;
4314
+ setTimeout(async () => { await exchangeSession(); reconnectSSE(); }, refreshMs);
4315
+ }
4316
+ } catch(e) { /* will retry on next connect */ }
4218
4317
  }
4219
4318
 
4220
4319
  // Tab switching
@@ -4227,10 +4326,14 @@ function generateDashboardHTML(options) {
4227
4326
  });
4228
4327
  });
4229
4328
 
4230
- // SSE Connection
4329
+ // SSE Connection \u2014 SEC-012: uses short-lived session token in URL, not auth token
4231
4330
  let evtSource;
4331
+ function reconnectSSE() {
4332
+ if (evtSource) { evtSource.close(); }
4333
+ connect();
4334
+ }
4232
4335
  function connect() {
4233
- evtSource = new EventSource(authQuery('/events'));
4336
+ evtSource = new EventSource(sessionQuery('/events'));
4234
4337
  evtSource.onopen = () => {
4235
4338
  document.getElementById('statusDot').classList.remove('disconnected');
4236
4339
  document.getElementById('statusText').textContent = 'Connected';
@@ -4418,12 +4521,20 @@ function generateDashboardHTML(options) {
4418
4521
  return d.innerHTML;
4419
4522
  }
4420
4523
 
4421
- // Init
4422
- connect();
4423
- fetch('/api/status', { headers: authHeaders() }).then(r => r.json()).then(data => {
4424
- if (data.baseline) updateBaseline(data.baseline);
4425
- if (data.policy) updatePolicy(data.policy);
4426
- }).catch(() => {});
4524
+ // Init \u2014 SEC-012: exchange token for session before connecting SSE
4525
+ (async function init() {
4526
+ await exchangeSession();
4527
+ // Clean token from URL if present (legacy bookmarks)
4528
+ if (window.location.search.includes('token=')) {
4529
+ const clean = window.location.pathname;
4530
+ window.history.replaceState({}, '', clean);
4531
+ }
4532
+ connect();
4533
+ fetch('/api/status', { headers: authHeaders() }).then(r => r.json()).then(data => {
4534
+ if (data.baseline) updateBaseline(data.baseline);
4535
+ if (data.policy) updatePolicy(data.policy);
4536
+ }).catch(() => {});
4537
+ })();
4427
4538
  })();
4428
4539
  </script>
4429
4540
  </body>
@@ -4431,6 +4542,8 @@ function generateDashboardHTML(options) {
4431
4542
  }
4432
4543
 
4433
4544
  // src/principal-policy/dashboard.ts
4545
+ var SESSION_TTL_MS = 5 * 60 * 1e3;
4546
+ var MAX_SESSIONS = 1e3;
4434
4547
  var DashboardApprovalChannel = class {
4435
4548
  config;
4436
4549
  pending = /* @__PURE__ */ new Map();
@@ -4442,6 +4555,9 @@ var DashboardApprovalChannel = class {
4442
4555
  dashboardHTML;
4443
4556
  authToken;
4444
4557
  useTLS;
4558
+ /** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
4559
+ sessions = /* @__PURE__ */ new Map();
4560
+ sessionCleanupTimer = null;
4445
4561
  constructor(config) {
4446
4562
  this.config = config;
4447
4563
  this.authToken = config.auth_token;
@@ -4451,6 +4567,7 @@ var DashboardApprovalChannel = class {
4451
4567
  serverVersion: "0.3.0",
4452
4568
  authToken: this.authToken
4453
4569
  });
4570
+ this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
4454
4571
  }
4455
4572
  /**
4456
4573
  * Inject dependencies after construction.
@@ -4480,13 +4597,14 @@ var DashboardApprovalChannel = class {
4480
4597
  const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
4481
4598
  this.httpServer.listen(this.config.port, this.config.host, () => {
4482
4599
  if (this.authToken) {
4600
+ const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
4483
4601
  process.stderr.write(
4484
4602
  `
4485
- Sanctuary Principal Dashboard: ${baseUrl}/?token=${this.authToken}
4603
+ Sanctuary Principal Dashboard: ${baseUrl}
4486
4604
  `
4487
4605
  );
4488
4606
  process.stderr.write(
4489
- ` Auth token: ${this.authToken}
4607
+ ` Auth required (token: ${hint}). Use Authorization: Bearer <TOKEN> header.
4490
4608
 
4491
4609
  `
4492
4610
  );
@@ -4520,6 +4638,11 @@ var DashboardApprovalChannel = class {
4520
4638
  client.end();
4521
4639
  }
4522
4640
  this.sseClients.clear();
4641
+ this.sessions.clear();
4642
+ if (this.sessionCleanupTimer) {
4643
+ clearInterval(this.sessionCleanupTimer);
4644
+ this.sessionCleanupTimer = null;
4645
+ }
4523
4646
  if (this.httpServer) {
4524
4647
  return new Promise((resolve) => {
4525
4648
  this.httpServer.close(() => resolve());
@@ -4540,7 +4663,8 @@ var DashboardApprovalChannel = class {
4540
4663
  const timer = setTimeout(() => {
4541
4664
  this.pending.delete(id);
4542
4665
  const response = {
4543
- decision: this.config.auto_deny ? "deny" : "approve",
4666
+ // SEC-002: Timeout ALWAYS denies. No configuration can change this.
4667
+ decision: "deny",
4544
4668
  decided_at: (/* @__PURE__ */ new Date()).toISOString(),
4545
4669
  decided_by: "timeout"
4546
4670
  };
@@ -4572,7 +4696,12 @@ var DashboardApprovalChannel = class {
4572
4696
  // ── Authentication ──────────────────────────────────────────────────
4573
4697
  /**
4574
4698
  * Verify bearer token authentication.
4575
- * Checks Authorization header first, falls back to ?token= query param.
4699
+ *
4700
+ * SEC-012: The long-lived auth token is ONLY accepted via the Authorization
4701
+ * header — never in URL query strings. For SSE and page loads that cannot
4702
+ * set headers, a short-lived session token (obtained via POST /auth/session)
4703
+ * is accepted via ?session= query parameter.
4704
+ *
4576
4705
  * Returns true if auth passes, false if blocked (response already sent).
4577
4706
  */
4578
4707
  checkAuth(req, url, res) {
@@ -4584,19 +4713,71 @@ var DashboardApprovalChannel = class {
4584
4713
  return true;
4585
4714
  }
4586
4715
  }
4587
- const queryToken = url.searchParams.get("token");
4588
- if (queryToken === this.authToken) {
4716
+ const sessionId = url.searchParams.get("session");
4717
+ if (sessionId && this.validateSession(sessionId)) {
4589
4718
  return true;
4590
4719
  }
4591
4720
  res.writeHead(401, { "Content-Type": "application/json" });
4592
- res.end(JSON.stringify({ error: "Unauthorized \u2014 valid bearer token required" }));
4721
+ res.end(JSON.stringify({ error: "Unauthorized \u2014 use Authorization: Bearer header or a valid session" }));
4593
4722
  return false;
4594
4723
  }
4724
+ // ── Session Management (SEC-012) ──────────────────────────────────
4725
+ /**
4726
+ * Create a short-lived session by exchanging the long-lived auth token
4727
+ * (provided in the Authorization header) for a session ID.
4728
+ */
4729
+ createSession() {
4730
+ if (this.sessions.size >= MAX_SESSIONS) {
4731
+ this.cleanupSessions();
4732
+ if (this.sessions.size >= MAX_SESSIONS) {
4733
+ const oldest = [...this.sessions.entries()].sort(
4734
+ (a, b) => a[1].created_at - b[1].created_at
4735
+ )[0];
4736
+ if (oldest) this.sessions.delete(oldest[0]);
4737
+ }
4738
+ }
4739
+ const id = randomBytes$1(32).toString("hex");
4740
+ const now = Date.now();
4741
+ this.sessions.set(id, {
4742
+ id,
4743
+ created_at: now,
4744
+ expires_at: now + SESSION_TTL_MS
4745
+ });
4746
+ return id;
4747
+ }
4748
+ /**
4749
+ * Validate a session ID — must exist and not be expired.
4750
+ */
4751
+ validateSession(sessionId) {
4752
+ const session = this.sessions.get(sessionId);
4753
+ if (!session) return false;
4754
+ if (Date.now() > session.expires_at) {
4755
+ this.sessions.delete(sessionId);
4756
+ return false;
4757
+ }
4758
+ return true;
4759
+ }
4760
+ /**
4761
+ * Remove all expired sessions.
4762
+ */
4763
+ cleanupSessions() {
4764
+ const now = Date.now();
4765
+ for (const [id, session] of this.sessions) {
4766
+ if (now > session.expires_at) {
4767
+ this.sessions.delete(id);
4768
+ }
4769
+ }
4770
+ }
4595
4771
  // ── HTTP Request Handler ────────────────────────────────────────────
4596
4772
  handleRequest(req, res) {
4597
4773
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
4598
4774
  const method = req.method ?? "GET";
4599
- res.setHeader("Access-Control-Allow-Origin", "*");
4775
+ const origin = req.headers.origin;
4776
+ const protocol = this.useTLS ? "https" : "http";
4777
+ const selfOrigin = `${protocol}://${this.config.host}:${this.config.port}`;
4778
+ if (origin === selfOrigin) {
4779
+ res.setHeader("Access-Control-Allow-Origin", origin);
4780
+ }
4600
4781
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
4601
4782
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
4602
4783
  if (method === "OPTIONS") {
@@ -4606,6 +4787,10 @@ var DashboardApprovalChannel = class {
4606
4787
  }
4607
4788
  if (!this.checkAuth(req, url, res)) return;
4608
4789
  try {
4790
+ if (method === "POST" && url.pathname === "/auth/session") {
4791
+ this.handleSessionExchange(req, res);
4792
+ return;
4793
+ }
4609
4794
  if (method === "GET" && url.pathname === "/") {
4610
4795
  this.serveDashboard(res);
4611
4796
  } else if (method === "GET" && url.pathname === "/events") {
@@ -4632,6 +4817,40 @@ var DashboardApprovalChannel = class {
4632
4817
  }
4633
4818
  }
4634
4819
  // ── Route Handlers ──────────────────────────────────────────────────
4820
+ /**
4821
+ * SEC-012: Exchange a long-lived auth token (in Authorization header)
4822
+ * for a short-lived session ID. The session ID can be used in URL
4823
+ * query parameters without exposing the long-lived credential.
4824
+ *
4825
+ * This endpoint performs its OWN auth check (header-only) because it
4826
+ * must reject query-parameter tokens and is called before the
4827
+ * normal checkAuth flow.
4828
+ */
4829
+ handleSessionExchange(req, res) {
4830
+ if (!this.authToken) {
4831
+ res.writeHead(200, { "Content-Type": "application/json" });
4832
+ res.end(JSON.stringify({ session_id: "no-auth" }));
4833
+ return;
4834
+ }
4835
+ const authHeader = req.headers.authorization;
4836
+ if (!authHeader) {
4837
+ res.writeHead(401, { "Content-Type": "application/json" });
4838
+ res.end(JSON.stringify({ error: "Authorization header required" }));
4839
+ return;
4840
+ }
4841
+ const parts = authHeader.split(" ");
4842
+ if (parts.length !== 2 || parts[0] !== "Bearer" || parts[1] !== this.authToken) {
4843
+ res.writeHead(401, { "Content-Type": "application/json" });
4844
+ res.end(JSON.stringify({ error: "Invalid bearer token" }));
4845
+ return;
4846
+ }
4847
+ const sessionId = this.createSession();
4848
+ res.writeHead(200, { "Content-Type": "application/json" });
4849
+ res.end(JSON.stringify({
4850
+ session_id: sessionId,
4851
+ expires_in_seconds: SESSION_TTL_MS / 1e3
4852
+ }));
4853
+ }
4635
4854
  serveDashboard(res) {
4636
4855
  res.writeHead(200, {
4637
4856
  "Content-Type": "text/html; charset=utf-8",
@@ -4657,7 +4876,8 @@ var DashboardApprovalChannel = class {
4657
4876
  approval_channel: {
4658
4877
  type: this.policy.approval_channel.type,
4659
4878
  timeout_seconds: this.policy.approval_channel.timeout_seconds,
4660
- auto_deny: this.policy.approval_channel.auto_deny
4879
+ auto_deny: true
4880
+ // SEC-002: hardcoded, not configurable
4661
4881
  }
4662
4882
  };
4663
4883
  }
@@ -4698,7 +4918,8 @@ data: ${JSON.stringify(initData)}
4698
4918
  approval_channel: {
4699
4919
  type: this.policy.approval_channel.type,
4700
4920
  timeout_seconds: this.policy.approval_channel.timeout_seconds,
4701
- auto_deny: this.policy.approval_channel.auto_deny
4921
+ auto_deny: true
4922
+ // SEC-002: hardcoded, not configurable
4702
4923
  }
4703
4924
  };
4704
4925
  }
@@ -4871,7 +5092,8 @@ var WebhookApprovalChannel = class {
4871
5092
  const timer = setTimeout(() => {
4872
5093
  this.pending.delete(id);
4873
5094
  const response = {
4874
- decision: this.config.auto_deny ? "deny" : "approve",
5095
+ // SEC-002: Timeout ALWAYS denies. No configuration can change this.
5096
+ decision: "deny",
4875
5097
  decided_at: (/* @__PURE__ */ new Date()).toISOString(),
4876
5098
  decided_by: "timeout"
4877
5099
  };
@@ -5059,16 +5281,29 @@ var ApprovalGate = class {
5059
5281
  if (anomaly) {
5060
5282
  return this.requestApproval(operation, 2, anomaly.reason, anomaly.context);
5061
5283
  }
5062
- this.auditLog.append("l2", `gate_allow:${operation}`, "system", {
5063
- tier: 3,
5064
- operation
5284
+ if (this.policy.tier3_always_allow.includes(operation)) {
5285
+ this.auditLog.append("l2", `gate_allow:${operation}`, "system", {
5286
+ tier: 3,
5287
+ operation
5288
+ });
5289
+ return {
5290
+ allowed: true,
5291
+ tier: 3,
5292
+ reason: "Operation allowed (Tier 3)",
5293
+ approval_required: false
5294
+ };
5295
+ }
5296
+ this.auditLog.append("l2", `gate_unclassified:${operation}`, "system", {
5297
+ tier: 1,
5298
+ operation,
5299
+ warning: "Operation is not classified in any policy tier \u2014 defaulting to Tier 1 (require approval)"
5065
5300
  });
5066
- return {
5067
- allowed: true,
5068
- tier: 3,
5069
- reason: "Operation allowed (Tier 3)",
5070
- approval_required: false
5071
- };
5301
+ return this.requestApproval(
5302
+ operation,
5303
+ 1,
5304
+ `"${operation}" is not classified in any policy tier \u2014 requires approval (SEC-011 safe default)`,
5305
+ { operation, unclassified: true }
5306
+ );
5072
5307
  }
5073
5308
  /**
5074
5309
  * Detect Tier 2 behavioral anomalies.
@@ -5241,7 +5476,8 @@ function createPrincipalPolicyTools(policy, baseline, auditLog) {
5241
5476
  approval_channel: {
5242
5477
  type: policy.approval_channel.type,
5243
5478
  timeout_seconds: policy.approval_channel.timeout_seconds,
5244
- auto_deny: policy.approval_channel.auto_deny
5479
+ auto_deny: true
5480
+ // SEC-002: hardcoded, not configurable
5245
5481
  }
5246
5482
  };
5247
5483
  if (includeDefaults) {
@@ -5772,7 +6008,9 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
5772
6008
  return toolResult({
5773
6009
  session_id: result.session.session_id,
5774
6010
  response: result.response,
5775
- instructions: "Send the 'response' object back to the initiator. When you receive their completion, pass it to sanctuary/handshake_status with this session_id."
6011
+ instructions: "Send the 'response' object back to the initiator. When you receive their completion, pass it to sanctuary/handshake_status with this session_id.",
6012
+ // SEC-ADD-03: Tag response — contains SHR data that will be sent to counterparty
6013
+ _content_trust: "external"
5776
6014
  });
5777
6015
  }
5778
6016
  },
@@ -5825,7 +6063,9 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
5825
6063
  return toolResult({
5826
6064
  completion: result.completion,
5827
6065
  result: result.result,
5828
- instructions: "Send the 'completion' object to the responder so they can verify the handshake. The 'result' object contains the verified counterparty status and trust tier."
6066
+ instructions: "Send the 'completion' object to the responder so they can verify the handshake. The 'result' object contains the verified counterparty status and trust tier.",
6067
+ // SEC-ADD-03: Tag response as containing counterparty-controlled SHR data
6068
+ _content_trust: "external"
5829
6069
  });
5830
6070
  }
5831
6071
  },
@@ -6250,7 +6490,21 @@ function canonicalize(outcome) {
6250
6490
  return stringToBytes(stableStringify(outcome));
6251
6491
  }
6252
6492
  function stableStringify(value) {
6253
- if (value === null || value === void 0) return JSON.stringify(value);
6493
+ if (value === null) return "null";
6494
+ if (value === void 0) return "null";
6495
+ if (typeof value === "number") {
6496
+ if (!Number.isFinite(value)) {
6497
+ throw new Error(
6498
+ `Cannot canonicalize non-finite number: ${value}. NaN, Infinity, and -Infinity are not representable in JSON.`
6499
+ );
6500
+ }
6501
+ if (Object.is(value, -0)) {
6502
+ throw new Error(
6503
+ "Cannot canonicalize negative zero (-0). Use 0 instead for deterministic cross-language serialization."
6504
+ );
6505
+ }
6506
+ return JSON.stringify(value);
6507
+ }
6254
6508
  if (typeof value !== "object") return JSON.stringify(value);
6255
6509
  if (Array.isArray(value)) {
6256
6510
  return "[" + value.map((v) => stableStringify(v)).join(",") + "]";
@@ -6278,11 +6532,12 @@ function createBridgeCommitment(outcome, identity, identityEncryptionKey, includ
6278
6532
  bridge_commitment_id: commitmentId,
6279
6533
  session_id: outcome.session_id,
6280
6534
  sha256_commitment: sha2564.commitment,
6535
+ terms_hash: outcome.terms_hash,
6281
6536
  committer_did: identity.did,
6282
6537
  committed_at: now,
6283
6538
  bridge_version: "sanctuary-concordia-bridge-v1"
6284
6539
  };
6285
- const payloadBytes = stringToBytes(JSON.stringify(commitmentPayload));
6540
+ const payloadBytes = stringToBytes(stableStringify(commitmentPayload));
6286
6541
  const signature = sign(payloadBytes, identity.encrypted_private_key, identityEncryptionKey);
6287
6542
  return {
6288
6543
  bridge_commitment_id: commitmentId,
@@ -6308,11 +6563,12 @@ function verifyBridgeCommitment(commitment, outcome, committerPublicKey) {
6308
6563
  bridge_commitment_id: commitment.bridge_commitment_id,
6309
6564
  session_id: commitment.session_id,
6310
6565
  sha256_commitment: commitment.sha256_commitment,
6566
+ terms_hash: outcome.terms_hash,
6311
6567
  committer_did: commitment.committer_did,
6312
6568
  committed_at: commitment.committed_at,
6313
6569
  bridge_version: commitment.bridge_version
6314
6570
  };
6315
- const payloadBytes = stringToBytes(JSON.stringify(commitmentPayload));
6571
+ const payloadBytes = stringToBytes(stableStringify(commitmentPayload));
6316
6572
  const sigBytes = fromBase64url(commitment.signature);
6317
6573
  const signatureValid = verify(payloadBytes, sigBytes, committerPublicKey);
6318
6574
  const sessionIdMatch = commitment.session_id === outcome.session_id;
@@ -6539,7 +6795,9 @@ function createBridgeTools(storage, masterKey, identityManager, auditLog, handsh
6539
6795
  return toolResult({
6540
6796
  ...result,
6541
6797
  session_id: storedCommitment.session_id,
6542
- committer_did: storedCommitment.committer_did
6798
+ committer_did: storedCommitment.committer_did,
6799
+ // SEC-ADD-03: Tag response as containing counterparty-controlled data
6800
+ _content_trust: "external"
6543
6801
  });
6544
6802
  }
6545
6803
  },
@@ -6633,6 +6891,668 @@ function createBridgeTools(storage, masterKey, identityManager, auditLog, handsh
6633
6891
  ];
6634
6892
  return { tools };
6635
6893
  }
6894
+ function lenientJsonParse(raw) {
6895
+ let cleaned = raw.replace(/\/\/[^\n]*/g, "");
6896
+ cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, "");
6897
+ cleaned = cleaned.replace(/,\s*([\]}])/g, "$1");
6898
+ return JSON.parse(cleaned);
6899
+ }
6900
+ async function fileExists(path) {
6901
+ try {
6902
+ await access(path);
6903
+ return true;
6904
+ } catch {
6905
+ return false;
6906
+ }
6907
+ }
6908
+ async function safeReadFile(path) {
6909
+ try {
6910
+ return await readFile(path, "utf-8");
6911
+ } catch {
6912
+ return null;
6913
+ }
6914
+ }
6915
+ async function detectEnvironment(config, deepScan) {
6916
+ const fingerprint = {
6917
+ sanctuary_installed: true,
6918
+ // We're running inside Sanctuary
6919
+ sanctuary_version: config.version,
6920
+ openclaw_detected: false,
6921
+ openclaw_version: null,
6922
+ openclaw_config: null,
6923
+ node_version: process.version,
6924
+ platform: `${process.platform}-${process.arch}`
6925
+ };
6926
+ if (!deepScan) {
6927
+ return fingerprint;
6928
+ }
6929
+ const home = homedir();
6930
+ const openclawConfigPath = join(home, ".openclaw", "openclaw.json");
6931
+ const openclawEnvPath = join(home, ".openclaw", ".env");
6932
+ const openclawMemoryPath = join(home, ".openclaw", "workspace", "MEMORY.md");
6933
+ const openclawMemoryDir = join(home, ".openclaw", "workspace", "memory");
6934
+ const configExists = await fileExists(openclawConfigPath);
6935
+ const envExists = await fileExists(openclawEnvPath);
6936
+ const memoryExists = await fileExists(openclawMemoryPath);
6937
+ const memoryDirExists = await fileExists(openclawMemoryDir);
6938
+ if (configExists || memoryExists || memoryDirExists) {
6939
+ fingerprint.openclaw_detected = true;
6940
+ fingerprint.openclaw_config = await auditOpenClawConfig(
6941
+ openclawConfigPath,
6942
+ openclawEnvPath,
6943
+ openclawMemoryPath,
6944
+ configExists,
6945
+ envExists,
6946
+ memoryExists
6947
+ );
6948
+ }
6949
+ return fingerprint;
6950
+ }
6951
+ async function auditOpenClawConfig(configPath, envPath, _memoryPath, configExists, envExists, memoryExists) {
6952
+ const audit = {
6953
+ config_path: configExists ? configPath : null,
6954
+ require_approval_enabled: false,
6955
+ sandbox_policy_active: false,
6956
+ sandbox_allow_list: [],
6957
+ sandbox_deny_list: [],
6958
+ memory_encrypted: false,
6959
+ // Stock OpenClaw never encrypts memory
6960
+ env_file_exposed: false,
6961
+ gateway_token_set: false,
6962
+ dm_pairing_enabled: false,
6963
+ mcp_bridge_active: false
6964
+ };
6965
+ if (configExists) {
6966
+ const raw = await safeReadFile(configPath);
6967
+ if (raw) {
6968
+ try {
6969
+ const parsed = lenientJsonParse(raw);
6970
+ const hooks = parsed.hooks;
6971
+ if (hooks) {
6972
+ const beforeToolCall = hooks.before_tool_call;
6973
+ if (beforeToolCall) {
6974
+ const hookStr = JSON.stringify(beforeToolCall);
6975
+ audit.require_approval_enabled = hookStr.includes("requireApproval");
6976
+ }
6977
+ }
6978
+ const tools = parsed.tools;
6979
+ if (tools) {
6980
+ const sandbox = tools.sandbox;
6981
+ if (sandbox) {
6982
+ const sandboxTools = sandbox.tools;
6983
+ if (sandboxTools) {
6984
+ audit.sandbox_policy_active = true;
6985
+ if (Array.isArray(sandboxTools.allow)) {
6986
+ audit.sandbox_allow_list = sandboxTools.allow.filter(
6987
+ (item) => typeof item === "string"
6988
+ );
6989
+ }
6990
+ if (Array.isArray(sandboxTools.alsoAllow)) {
6991
+ audit.sandbox_allow_list = [
6992
+ ...audit.sandbox_allow_list,
6993
+ ...sandboxTools.alsoAllow.filter(
6994
+ (item) => typeof item === "string"
6995
+ )
6996
+ ];
6997
+ }
6998
+ if (Array.isArray(sandboxTools.deny)) {
6999
+ audit.sandbox_deny_list = sandboxTools.deny.filter(
7000
+ (item) => typeof item === "string"
7001
+ );
7002
+ }
7003
+ }
7004
+ }
7005
+ }
7006
+ const mcpServers = parsed.mcpServers;
7007
+ if (mcpServers && Object.keys(mcpServers).length > 0) {
7008
+ audit.mcp_bridge_active = true;
7009
+ }
7010
+ } catch {
7011
+ }
7012
+ }
7013
+ }
7014
+ if (envExists) {
7015
+ const envContent = await safeReadFile(envPath);
7016
+ if (envContent) {
7017
+ const secretPatterns = [
7018
+ /[A-Z_]*API_KEY\s*=/,
7019
+ /[A-Z_]*TOKEN\s*=/,
7020
+ /[A-Z_]*SECRET\s*=/,
7021
+ /[A-Z_]*PASSWORD\s*=/,
7022
+ /[A-Z_]*PRIVATE_KEY\s*=/
7023
+ ];
7024
+ audit.env_file_exposed = secretPatterns.some((p) => p.test(envContent));
7025
+ audit.gateway_token_set = /OPENCLAW_GATEWAY_TOKEN\s*=/.test(envContent);
7026
+ }
7027
+ }
7028
+ if (memoryExists) {
7029
+ audit.memory_encrypted = false;
7030
+ }
7031
+ return audit;
7032
+ }
7033
+
7034
+ // src/audit/analyzer.ts
7035
+ var L1_ENCRYPTION_AT_REST = 10;
7036
+ var L1_IDENTITY_CRYPTOGRAPHIC = 10;
7037
+ var L1_INTEGRITY_VERIFICATION = 8;
7038
+ var L1_STATE_PORTABLE = 7;
7039
+ var L2_THREE_TIER_GATE = 10;
7040
+ var L2_BINARY_GATE = 3;
7041
+ var L2_ANOMALY_DETECTION = 7;
7042
+ var L2_ENCRYPTED_AUDIT = 5;
7043
+ var L2_TOOL_SANDBOXING = 3;
7044
+ var L3_COMMITMENT_SCHEME = 8;
7045
+ var L3_ZK_PROOFS = 7;
7046
+ var L3_DISCLOSURE_POLICIES = 5;
7047
+ var L4_PORTABLE_REPUTATION = 6;
7048
+ var L4_SIGNED_ATTESTATIONS = 6;
7049
+ var L4_SYBIL_DETECTION = 4;
7050
+ var L4_SOVEREIGNTY_GATED = 4;
7051
+ var SEVERITY_ORDER = {
7052
+ critical: 0,
7053
+ high: 1,
7054
+ medium: 2,
7055
+ low: 3
7056
+ };
7057
+ function analyzeSovereignty(env, config) {
7058
+ const l1 = assessL1(env, config);
7059
+ const l2 = assessL2(env);
7060
+ const l3 = assessL3(env);
7061
+ const l4 = assessL4(env);
7062
+ const l1Score = scoreL1(l1);
7063
+ const l2Score = scoreL2(l2);
7064
+ const l3Score = scoreL3(l3);
7065
+ const l4Score = scoreL4(l4);
7066
+ const overallScore = l1Score + l2Score + l3Score + l4Score;
7067
+ const sovereigntyLevel = overallScore >= 80 ? "full" : overallScore >= 50 ? "partial" : overallScore >= 20 ? "minimal" : "none";
7068
+ const gaps = generateGaps(env, l1, l2, l3, l4);
7069
+ gaps.sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
7070
+ const recommendations = generateRecommendations(env, l1, l2, l3, l4);
7071
+ return {
7072
+ version: "1.0",
7073
+ audited_at: (/* @__PURE__ */ new Date()).toISOString(),
7074
+ environment: env,
7075
+ layers: {
7076
+ l1_cognitive: l1,
7077
+ l2_operational: l2,
7078
+ l3_selective_disclosure: l3,
7079
+ l4_reputation: l4
7080
+ },
7081
+ overall_score: overallScore,
7082
+ sovereignty_level: sovereigntyLevel,
7083
+ gaps,
7084
+ recommendations
7085
+ };
7086
+ }
7087
+ function assessL1(env, config) {
7088
+ const findings = [];
7089
+ const sanctuaryActive = env.sanctuary_installed;
7090
+ const encryptionAtRest = sanctuaryActive;
7091
+ const keyCustody = sanctuaryActive ? "self" : "none";
7092
+ const integrityVerification = sanctuaryActive;
7093
+ const identityCryptographic = sanctuaryActive;
7094
+ const statePortable = sanctuaryActive;
7095
+ if (sanctuaryActive) {
7096
+ findings.push("AES-256-GCM encryption active for all state");
7097
+ findings.push(`Key derivation: ${config.state.key_derivation}`);
7098
+ findings.push(`Identity provider: ${config.state.identity_provider}`);
7099
+ findings.push("Merkle integrity verification enabled");
7100
+ findings.push("State export/import available");
7101
+ }
7102
+ if (env.openclaw_detected && env.openclaw_config) {
7103
+ if (!env.openclaw_config.memory_encrypted) {
7104
+ findings.push("OpenClaw agent memory (MEMORY.md, daily notes) stored in plaintext");
7105
+ }
7106
+ if (env.openclaw_config.env_file_exposed) {
7107
+ findings.push("OpenClaw .env file contains plaintext API keys/tokens");
7108
+ }
7109
+ }
7110
+ const status = encryptionAtRest && identityCryptographic ? "active" : encryptionAtRest || identityCryptographic ? "partial" : "inactive";
7111
+ return {
7112
+ status,
7113
+ encryption_at_rest: encryptionAtRest,
7114
+ key_custody: keyCustody,
7115
+ integrity_verification: integrityVerification,
7116
+ identity_cryptographic: identityCryptographic,
7117
+ state_portable: statePortable,
7118
+ findings
7119
+ };
7120
+ }
7121
+ function assessL2(env, _config) {
7122
+ const findings = [];
7123
+ const sanctuaryActive = env.sanctuary_installed;
7124
+ let approvalGate = "none";
7125
+ let behavioralAnomalyDetection = false;
7126
+ let auditTrailEncrypted = false;
7127
+ let auditTrailExists = false;
7128
+ let toolSandboxing = "none";
7129
+ if (sanctuaryActive) {
7130
+ approvalGate = "three-tier";
7131
+ behavioralAnomalyDetection = true;
7132
+ auditTrailEncrypted = true;
7133
+ auditTrailExists = true;
7134
+ findings.push("Three-tier Principal Policy gate active");
7135
+ findings.push("Behavioral anomaly detection (BaselineTracker) enabled");
7136
+ findings.push("Encrypted audit trail active");
7137
+ }
7138
+ if (env.openclaw_detected && env.openclaw_config) {
7139
+ if (env.openclaw_config.require_approval_enabled) {
7140
+ if (!sanctuaryActive) {
7141
+ approvalGate = "binary";
7142
+ }
7143
+ findings.push("OpenClaw requireApproval hook enabled (binary approve/deny)");
7144
+ }
7145
+ if (env.openclaw_config.sandbox_policy_active) {
7146
+ if (!sanctuaryActive) {
7147
+ toolSandboxing = "basic";
7148
+ }
7149
+ findings.push(
7150
+ `OpenClaw sandbox policy active (${env.openclaw_config.sandbox_allow_list.length} allowed, ${env.openclaw_config.sandbox_deny_list.length} denied)`
7151
+ );
7152
+ }
7153
+ }
7154
+ const status = approvalGate === "three-tier" && auditTrailEncrypted ? "active" : approvalGate !== "none" || auditTrailExists ? "partial" : "inactive";
7155
+ return {
7156
+ status,
7157
+ approval_gate: approvalGate,
7158
+ behavioral_anomaly_detection: behavioralAnomalyDetection,
7159
+ audit_trail_encrypted: auditTrailEncrypted,
7160
+ audit_trail_exists: auditTrailExists,
7161
+ tool_sandboxing: sanctuaryActive ? "policy-enforced" : toolSandboxing,
7162
+ findings
7163
+ };
7164
+ }
7165
+ function assessL3(env, _config) {
7166
+ const findings = [];
7167
+ const sanctuaryActive = env.sanctuary_installed;
7168
+ let commitmentScheme = "none";
7169
+ let zkProofs = false;
7170
+ let selectiveDisclosurePolicy = false;
7171
+ if (sanctuaryActive) {
7172
+ commitmentScheme = "pedersen+sha256";
7173
+ zkProofs = true;
7174
+ selectiveDisclosurePolicy = true;
7175
+ findings.push("SHA-256 + Pedersen commitment schemes active");
7176
+ findings.push("Schnorr ZK proofs and range proofs available");
7177
+ findings.push("Selective disclosure policies configurable");
7178
+ }
7179
+ const status = commitmentScheme === "pedersen+sha256" && zkProofs ? "active" : commitmentScheme !== "none" ? "partial" : "inactive";
7180
+ return {
7181
+ status,
7182
+ commitment_scheme: commitmentScheme,
7183
+ zero_knowledge_proofs: zkProofs,
7184
+ selective_disclosure_policy: selectiveDisclosurePolicy,
7185
+ findings
7186
+ };
7187
+ }
7188
+ function assessL4(env, _config) {
7189
+ const findings = [];
7190
+ const sanctuaryActive = env.sanctuary_installed;
7191
+ const reputationPortable = sanctuaryActive;
7192
+ const reputationSigned = sanctuaryActive;
7193
+ const sybilDetection = sanctuaryActive;
7194
+ const sovereigntyGated = sanctuaryActive;
7195
+ if (sanctuaryActive) {
7196
+ findings.push("Signed EAS-compatible attestations active");
7197
+ findings.push("Reputation export/import available");
7198
+ findings.push("Sybil detection heuristics enabled");
7199
+ findings.push("Sovereignty-gated reputation tiers active");
7200
+ } else {
7201
+ findings.push("No portable reputation system detected");
7202
+ }
7203
+ const status = reputationPortable && reputationSigned && sovereigntyGated ? "active" : reputationPortable || reputationSigned ? "partial" : "inactive";
7204
+ return {
7205
+ status,
7206
+ reputation_portable: reputationPortable,
7207
+ reputation_signed: reputationSigned,
7208
+ reputation_sybil_detection: sybilDetection,
7209
+ sovereignty_gated_tiers: sovereigntyGated,
7210
+ findings
7211
+ };
7212
+ }
7213
+ function scoreL1(l1) {
7214
+ let score = 0;
7215
+ if (l1.encryption_at_rest) score += L1_ENCRYPTION_AT_REST;
7216
+ if (l1.identity_cryptographic) score += L1_IDENTITY_CRYPTOGRAPHIC;
7217
+ if (l1.integrity_verification) score += L1_INTEGRITY_VERIFICATION;
7218
+ if (l1.state_portable) score += L1_STATE_PORTABLE;
7219
+ return score;
7220
+ }
7221
+ function scoreL2(l2) {
7222
+ let score = 0;
7223
+ if (l2.approval_gate === "three-tier") score += L2_THREE_TIER_GATE;
7224
+ else if (l2.approval_gate === "binary") score += L2_BINARY_GATE;
7225
+ if (l2.behavioral_anomaly_detection) score += L2_ANOMALY_DETECTION;
7226
+ if (l2.audit_trail_encrypted) score += L2_ENCRYPTED_AUDIT;
7227
+ if (l2.tool_sandboxing === "policy-enforced") score += L2_TOOL_SANDBOXING;
7228
+ else if (l2.tool_sandboxing === "basic") score += 1;
7229
+ return score;
7230
+ }
7231
+ function scoreL3(l3) {
7232
+ let score = 0;
7233
+ if (l3.commitment_scheme === "pedersen+sha256") score += L3_COMMITMENT_SCHEME;
7234
+ else if (l3.commitment_scheme === "sha256-only") score += 4;
7235
+ if (l3.zero_knowledge_proofs) score += L3_ZK_PROOFS;
7236
+ if (l3.selective_disclosure_policy) score += L3_DISCLOSURE_POLICIES;
7237
+ return score;
7238
+ }
7239
+ function scoreL4(l4) {
7240
+ let score = 0;
7241
+ if (l4.reputation_portable) score += L4_PORTABLE_REPUTATION;
7242
+ if (l4.reputation_signed) score += L4_SIGNED_ATTESTATIONS;
7243
+ if (l4.reputation_sybil_detection) score += L4_SYBIL_DETECTION;
7244
+ if (l4.sovereignty_gated_tiers) score += L4_SOVEREIGNTY_GATED;
7245
+ return score;
7246
+ }
7247
+ function generateGaps(env, l1, l2, l3, l4) {
7248
+ const gaps = [];
7249
+ const oc = env.openclaw_config;
7250
+ if (oc && !oc.memory_encrypted) {
7251
+ gaps.push({
7252
+ id: "GAP-L1-001",
7253
+ layer: "L1",
7254
+ severity: "critical",
7255
+ title: "Agent memory stored in plaintext",
7256
+ description: "Your agent's memory (MEMORY.md, daily notes, SQLite index) is stored in plaintext at ~/.openclaw/workspace/. Any process with file access can read your agent's full context \u2014 preferences, decisions, conversation history.",
7257
+ openclaw_relevance: "Stock OpenClaw stores all agent memory in plaintext files. There is no built-in encryption for agent state.",
7258
+ sanctuary_solution: "Sanctuary encrypts all state at rest with AES-256-GCM using a key derived from Argon2id, making state opaque to any process that doesn't hold the master key. Use sanctuary/state_write to migrate sensitive state to the encrypted store."
7259
+ });
7260
+ }
7261
+ if (oc && oc.env_file_exposed) {
7262
+ gaps.push({
7263
+ id: "GAP-L1-002",
7264
+ layer: "L1",
7265
+ severity: "critical",
7266
+ title: "Plaintext API keys in .env file",
7267
+ description: "Your .env file contains plaintext API keys and tokens. These secrets are readable by any process with filesystem access.",
7268
+ openclaw_relevance: "OpenClaw stores API keys (LLM providers, gateway tokens) in a plaintext .env file.",
7269
+ sanctuary_solution: "Sanctuary's encrypted state store can hold secrets under the same AES-256-GCM envelope as all other state, tied to your self-custodied identity. Use sanctuary/state_write with namespace 'secrets'."
7270
+ });
7271
+ }
7272
+ if (!l1.identity_cryptographic) {
7273
+ gaps.push({
7274
+ id: "GAP-L1-003",
7275
+ layer: "L1",
7276
+ severity: "critical",
7277
+ title: "No cryptographic agent identity",
7278
+ description: "Your agent has no cryptographic identity. It cannot prove it is who it claims to be to any counterparty, sign messages, or participate in sovereignty handshakes.",
7279
+ openclaw_relevance: env.openclaw_detected ? "OpenClaw has no cryptographic agent identity. Agent identity is implicit (tied to the process/session), not cryptographically verifiable." : null,
7280
+ sanctuary_solution: "Sanctuary provides Ed25519 self-custodied identity with key rotation and delegation. Use sanctuary/identity_create to establish your cryptographic identity."
7281
+ });
7282
+ }
7283
+ if (l2.approval_gate === "binary" && !l2.behavioral_anomaly_detection) {
7284
+ gaps.push({
7285
+ id: "GAP-L2-001",
7286
+ layer: "L2",
7287
+ severity: "high",
7288
+ title: "Binary approval gate (no anomaly detection)",
7289
+ description: "Your approval gate provides binary approve/deny gating without behavioral anomaly detection. Routine operations require the same manual approval as sensitive ones.",
7290
+ openclaw_relevance: env.openclaw_detected ? "OpenClaw's requireApproval hook provides binary approve/deny gating. Sanctuary's three-tier Principal Policy adds behavioral anomaly detection (auto-escalation when agent behavior deviates from baseline), encrypted audit trails, and graduated approval tiers \u2014 so routine operations auto-proceed while sensitive operations require explicit consent." : null,
7291
+ sanctuary_solution: "Sanctuary's three-tier Principal Policy gate auto-allows routine operations (Tier 3), escalates anomalous behavior (Tier 2), and always requires human approval for irreversible operations (Tier 1). Use sanctuary/principal_policy_view to inspect."
7292
+ });
7293
+ } else if (l2.approval_gate === "none") {
7294
+ gaps.push({
7295
+ id: "GAP-L2-001",
7296
+ layer: "L2",
7297
+ severity: "critical",
7298
+ title: "No approval gate",
7299
+ description: "No approval gate is configured. All tool calls execute without oversight.",
7300
+ openclaw_relevance: null,
7301
+ sanctuary_solution: "Sanctuary's Principal Policy evaluates every tool call before execution. Enable it to get three-tier approval gating with behavioral anomaly detection."
7302
+ });
7303
+ }
7304
+ if (l2.tool_sandboxing === "basic") {
7305
+ gaps.push({
7306
+ id: "GAP-L2-002",
7307
+ layer: "L2",
7308
+ severity: "medium",
7309
+ title: "Basic tool sandboxing (no cryptographic attestation)",
7310
+ description: "Your tool sandbox enforces allow/deny lists but provides no cryptographic attestation of execution context.",
7311
+ openclaw_relevance: env.openclaw_detected ? "OpenClaw's sandbox tool policy (tools.sandbox.tools) enforces allow/deny lists. Sanctuary adds cryptographic attestation of execution context \u2014 a verifiable proof that an operation ran within policy, not just that a policy was configured." : null,
7312
+ sanctuary_solution: "Sanctuary provides cryptographic execution attestation via sanctuary/exec_attest and policy-enforced sandboxing with encrypted audit trails."
7313
+ });
7314
+ }
7315
+ if (!l2.audit_trail_exists) {
7316
+ gaps.push({
7317
+ id: "GAP-L2-003",
7318
+ layer: "L2",
7319
+ severity: "high",
7320
+ title: "No audit trail",
7321
+ description: "No audit trail exists for tool call history. There is no record of what operations were executed, when, or by whom.",
7322
+ openclaw_relevance: null,
7323
+ sanctuary_solution: "Sanctuary maintains an encrypted audit log of all operations, queryable via sanctuary/monitor_audit_log."
7324
+ });
7325
+ }
7326
+ if (l3.commitment_scheme === "none") {
7327
+ gaps.push({
7328
+ id: "GAP-L3-001",
7329
+ layer: "L3",
7330
+ severity: "high",
7331
+ title: "No selective disclosure capability",
7332
+ description: "Your agent has no way to prove facts about its state without revealing the state itself. Every disclosure is all-or-nothing.",
7333
+ openclaw_relevance: env.openclaw_detected ? "OpenClaw has no selective disclosure mechanism. When your agent shares information, it shares everything or nothing \u2014 there is no way to prove a claim without revealing the underlying data." : null,
7334
+ sanctuary_solution: "Sanctuary's L3 provides SHA-256 + Pedersen commitments and Schnorr zero-knowledge proofs. Your agent can prove it has a valid credential, sufficient reputation, or a completed transaction without exposing the underlying data. Use sanctuary/zk_commit and sanctuary/zk_prove."
7335
+ });
7336
+ }
7337
+ if (!l4.reputation_portable) {
7338
+ gaps.push({
7339
+ id: "GAP-L4-001",
7340
+ layer: "L4",
7341
+ severity: "high",
7342
+ title: "No portable reputation",
7343
+ description: "Your agent's reputation is platform-locked. If you move to a different harness or platform, your track record doesn't follow.",
7344
+ openclaw_relevance: env.openclaw_detected ? "OpenClaw has no reputation system. Your agent's track record exists only in conversation history, which is not structured, signed, or portable." : null,
7345
+ sanctuary_solution: "Sanctuary's L4 provides signed EAS-compatible attestations that are self-custodied, portable, and cryptographically verifiable. Your reputation is yours, not your platform's. Use sanctuary/reputation_record to start building portable reputation."
7346
+ });
7347
+ }
7348
+ return gaps;
7349
+ }
7350
+ function generateRecommendations(env, l1, l2, l3, l4) {
7351
+ const recs = [];
7352
+ if (!l1.identity_cryptographic) {
7353
+ recs.push({
7354
+ priority: 1,
7355
+ action: "Create a cryptographic identity \u2014 your agent's foundation for all sovereignty operations",
7356
+ tool: "sanctuary/identity_create",
7357
+ effort: "immediate",
7358
+ impact: "critical"
7359
+ });
7360
+ }
7361
+ if (!l1.encryption_at_rest || env.openclaw_config && !env.openclaw_config.memory_encrypted) {
7362
+ recs.push({
7363
+ priority: 2,
7364
+ action: "Migrate plaintext agent state to Sanctuary's encrypted store",
7365
+ tool: "sanctuary/state_write",
7366
+ effort: "minutes",
7367
+ impact: "critical"
7368
+ });
7369
+ }
7370
+ recs.push({
7371
+ priority: 3,
7372
+ action: "Generate a Sovereignty Health Report to present to counterparties",
7373
+ tool: "sanctuary/shr_generate",
7374
+ effort: "immediate",
7375
+ impact: "high"
7376
+ });
7377
+ if (l2.approval_gate !== "three-tier") {
7378
+ recs.push({
7379
+ priority: 4,
7380
+ action: "Enable the three-tier Principal Policy gate for graduated approval",
7381
+ tool: "sanctuary/principal_policy_view",
7382
+ effort: "minutes",
7383
+ impact: "high"
7384
+ });
7385
+ }
7386
+ if (!l4.reputation_signed) {
7387
+ recs.push({
7388
+ priority: 5,
7389
+ action: "Start recording reputation attestations from completed interactions",
7390
+ tool: "sanctuary/reputation_record",
7391
+ effort: "minutes",
7392
+ impact: "medium"
7393
+ });
7394
+ }
7395
+ if (!l3.selective_disclosure_policy) {
7396
+ recs.push({
7397
+ priority: 6,
7398
+ action: "Configure selective disclosure policies for data sharing",
7399
+ tool: "sanctuary/disclosure_set_policy",
7400
+ effort: "hours",
7401
+ impact: "medium"
7402
+ });
7403
+ }
7404
+ return recs;
7405
+ }
7406
+ function formatAuditReport(result) {
7407
+ const { environment: env, layers, overall_score, sovereignty_level, gaps, recommendations } = result;
7408
+ const scoreBar = formatScoreBar(overall_score);
7409
+ const levelLabel = sovereignty_level.toUpperCase();
7410
+ let report = "";
7411
+ report += "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n";
7412
+ report += " SOVEREIGNTY AUDIT REPORT\n";
7413
+ report += ` Generated: ${result.audited_at}
7414
+ `;
7415
+ report += "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n";
7416
+ report += "\n";
7417
+ report += ` Overall Score: ${overall_score} / 100 ${scoreBar} ${levelLabel}
7418
+ `;
7419
+ report += "\n";
7420
+ report += " Environment:\n";
7421
+ report += ` \u2022 Sanctuary v${env.sanctuary_version ?? "?"} ${padDots("Sanctuary v" + (env.sanctuary_version ?? "?"))} ${env.sanctuary_installed ? "\u2713 installed" : "\u2717 not found"}
7422
+ `;
7423
+ if (env.openclaw_detected) {
7424
+ report += ` \u2022 OpenClaw ${padDots("OpenClaw")} \u2713 detected
7425
+ `;
7426
+ if (env.openclaw_config) {
7427
+ report += ` \u2022 OpenClaw requireApproval ${padDots("OpenClaw requireApproval")} ${env.openclaw_config.require_approval_enabled ? "\u2713 enabled" : "\u2717 disabled"}
7428
+ `;
7429
+ report += ` \u2022 OpenClaw sandbox policy ${padDots("OpenClaw sandbox policy")} ${env.openclaw_config.sandbox_policy_active ? "\u2713 active" : "\u2717 inactive"}
7430
+ `;
7431
+ }
7432
+ }
7433
+ report += "\n";
7434
+ const l1Score = scoreL1(layers.l1_cognitive);
7435
+ const l2Score = scoreL2(layers.l2_operational);
7436
+ const l3Score = scoreL3(layers.l3_selective_disclosure);
7437
+ const l4Score = scoreL4(layers.l4_reputation);
7438
+ report += " Layer Assessment:\n";
7439
+ report += " \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n";
7440
+ report += " \u2502 Layer \u2502 Status \u2502 Score \u2502\n";
7441
+ report += " \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n";
7442
+ report += ` \u2502 L1 Cognitive Sovereignty \u2502 ${padStatus(layers.l1_cognitive.status)} \u2502 ${padScore(l1Score, 35)} \u2502
7443
+ `;
7444
+ report += ` \u2502 L2 Operational Isolation \u2502 ${padStatus(layers.l2_operational.status)} \u2502 ${padScore(l2Score, 25)} \u2502
7445
+ `;
7446
+ report += ` \u2502 L3 Selective Disclosure \u2502 ${padStatus(layers.l3_selective_disclosure.status)} \u2502 ${padScore(l3Score, 20)} \u2502
7447
+ `;
7448
+ report += ` \u2502 L4 Verifiable Reputation \u2502 ${padStatus(layers.l4_reputation.status)} \u2502 ${padScore(l4Score, 20)} \u2502
7449
+ `;
7450
+ report += " \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n";
7451
+ report += "\n";
7452
+ if (gaps.length > 0) {
7453
+ report += ` \u26A0 ${gaps.length} SOVEREIGNTY GAP${gaps.length !== 1 ? "S" : ""} FOUND
7454
+ `;
7455
+ report += "\n";
7456
+ for (const gap of gaps) {
7457
+ const severityLabel = `[${gap.severity.toUpperCase()}]`;
7458
+ report += ` ${severityLabel} ${gap.id}: ${gap.title}
7459
+ `;
7460
+ const descLines = wordWrap(gap.description, 66);
7461
+ for (const line of descLines) {
7462
+ report += ` ${line}
7463
+ `;
7464
+ }
7465
+ report += ` \u2192 Fix: ${gap.sanctuary_solution.split(".")[0]}.
7466
+ `;
7467
+ if (gap.openclaw_relevance) {
7468
+ report += ` \u2192 OpenClaw context: ${gap.openclaw_relevance.split(".")[0]}.
7469
+ `;
7470
+ }
7471
+ report += "\n";
7472
+ }
7473
+ } else {
7474
+ report += " \u2713 NO SOVEREIGNTY GAPS FOUND\n";
7475
+ report += "\n";
7476
+ }
7477
+ if (recommendations.length > 0) {
7478
+ report += " RECOMMENDED NEXT STEPS (in order):\n";
7479
+ for (const rec of recommendations) {
7480
+ const effortLabel = rec.effort === "immediate" ? "immediate" : rec.effort === "minutes" ? "5 min" : "30 min";
7481
+ report += ` ${rec.priority}. [${effortLabel}] ${rec.action}`;
7482
+ if (rec.tool) {
7483
+ report += `: ${rec.tool}`;
7484
+ }
7485
+ report += "\n";
7486
+ }
7487
+ report += "\n";
7488
+ }
7489
+ report += "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n";
7490
+ return report;
7491
+ }
7492
+ function formatScoreBar(score) {
7493
+ const filled = Math.round(score / 10);
7494
+ return "[" + "\u25A0".repeat(filled) + "\u2591".repeat(10 - filled) + "]";
7495
+ }
7496
+ function padDots(label) {
7497
+ const totalWidth = 30;
7498
+ const dotsNeeded = Math.max(2, totalWidth - label.length - 4);
7499
+ return ".".repeat(dotsNeeded);
7500
+ }
7501
+ function padStatus(status) {
7502
+ const label = status.toUpperCase();
7503
+ return label + " ".repeat(Math.max(0, 8 - label.length));
7504
+ }
7505
+ function padScore(score, max) {
7506
+ const text = `${score}/${max}`;
7507
+ return " ".repeat(Math.max(0, 5 - text.length)) + text;
7508
+ }
7509
+ function wordWrap(text, maxWidth) {
7510
+ const words = text.split(" ");
7511
+ const lines = [];
7512
+ let current = "";
7513
+ for (const word of words) {
7514
+ if (current.length + word.length + 1 > maxWidth && current.length > 0) {
7515
+ lines.push(current);
7516
+ current = word;
7517
+ } else {
7518
+ current = current.length > 0 ? current + " " + word : word;
7519
+ }
7520
+ }
7521
+ if (current.length > 0) lines.push(current);
7522
+ return lines;
7523
+ }
7524
+
7525
+ // src/audit/tools.ts
7526
+ function createAuditTools(config) {
7527
+ const tools = [
7528
+ {
7529
+ name: "sanctuary/sovereignty_audit",
7530
+ description: "Audit your agent's sovereignty posture. Inspects the local environment for encryption, identity, approval gates, selective disclosure, and reputation \u2014 including OpenClaw-specific configurations. Returns a scored gap analysis with prioritized recommendations.",
7531
+ inputSchema: {
7532
+ type: "object",
7533
+ properties: {
7534
+ deep_scan: {
7535
+ type: "boolean",
7536
+ description: "If true (default), also scans for OpenClaw config, .env files, and memory files. Set to false for a Sanctuary-only assessment."
7537
+ }
7538
+ }
7539
+ },
7540
+ handler: async (args) => {
7541
+ const deepScan = args.deep_scan !== false;
7542
+ const env = await detectEnvironment(config, deepScan);
7543
+ const result = analyzeSovereignty(env, config);
7544
+ const report = formatAuditReport(result);
7545
+ return {
7546
+ content: [
7547
+ { type: "text", text: report },
7548
+ { type: "text", text: JSON.stringify(result, null, 2) }
7549
+ ]
7550
+ };
7551
+ }
7552
+ }
7553
+ ];
7554
+ return { tools };
7555
+ }
6636
7556
 
6637
7557
  // src/index.ts
6638
7558
  init_encoding();
@@ -6669,15 +7589,51 @@ async function createSanctuaryServer(options) {
6669
7589
  }
6670
7590
  } else {
6671
7591
  keyProtection = "recovery-key";
6672
- const existing = await storage.read("_meta", "recovery-key-hash");
6673
- if (existing) {
6674
- masterKey = generateRandomKey();
6675
- recoveryKey = toBase64url(masterKey);
7592
+ const { hashToString: hashToString2 } = await Promise.resolve().then(() => (init_hashing(), hashing_exports));
7593
+ const { stringToBytes: stringToBytes2, bytesToString: bytesToString2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
7594
+ const { fromBase64url: fromBase64url2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
7595
+ const { constantTimeEqual: constantTimeEqual2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
7596
+ const existingHash = await storage.read("_meta", "recovery-key-hash");
7597
+ if (existingHash) {
7598
+ const envRecoveryKey = process.env.SANCTUARY_RECOVERY_KEY;
7599
+ if (!envRecoveryKey) {
7600
+ throw new Error(
7601
+ "Sanctuary: Existing encrypted data found but no credentials provided.\nThis installation was previously set up with a recovery key.\n\nTo start the server, provide one of:\n - SANCTUARY_PASSPHRASE (if you later configured a passphrase)\n - SANCTUARY_RECOVERY_KEY (the recovery key shown at first run)\n\nWithout the correct credentials, encrypted state cannot be accessed.\nRefusing to start to prevent silent data loss."
7602
+ );
7603
+ }
7604
+ let recoveryKeyBytes;
7605
+ try {
7606
+ recoveryKeyBytes = fromBase64url2(envRecoveryKey);
7607
+ } catch {
7608
+ throw new Error(
7609
+ "Sanctuary: SANCTUARY_RECOVERY_KEY is not valid base64url. The recovery key should be the exact string shown at first run."
7610
+ );
7611
+ }
7612
+ if (recoveryKeyBytes.length !== 32) {
7613
+ throw new Error(
7614
+ "Sanctuary: SANCTUARY_RECOVERY_KEY has incorrect length. The recovery key should be the exact string shown at first run."
7615
+ );
7616
+ }
7617
+ const providedHash = hashToString2(recoveryKeyBytes);
7618
+ const storedHash = bytesToString2(existingHash);
7619
+ const providedHashBytes = stringToBytes2(providedHash);
7620
+ const storedHashBytes = stringToBytes2(storedHash);
7621
+ if (!constantTimeEqual2(providedHashBytes, storedHashBytes)) {
7622
+ throw new Error(
7623
+ "Sanctuary: Recovery key does not match the stored key hash.\nThe recovery key provided via SANCTUARY_RECOVERY_KEY is incorrect.\nUse the exact recovery key that was displayed at first run."
7624
+ );
7625
+ }
7626
+ masterKey = recoveryKeyBytes;
6676
7627
  } else {
7628
+ const existingNamespaces = await storage.list("_meta");
7629
+ const hasKeyParams = existingNamespaces.some((e) => e.key === "key-params");
7630
+ if (hasKeyParams) {
7631
+ throw new Error(
7632
+ "Sanctuary: Found existing key derivation parameters but no recovery key hash.\nThis indicates a corrupted or incomplete installation.\nIf you previously used a passphrase, set SANCTUARY_PASSPHRASE to start."
7633
+ );
7634
+ }
6677
7635
  masterKey = generateRandomKey();
6678
7636
  recoveryKey = toBase64url(masterKey);
6679
- const { hashToString: hashToString2 } = await Promise.resolve().then(() => (init_hashing(), hashing_exports));
6680
- const { stringToBytes: stringToBytes2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
6681
7637
  const keyHash = hashToString2(masterKey);
6682
7638
  await storage.write(
6683
7639
  "_meta",
@@ -6943,6 +7899,7 @@ async function createSanctuaryServer(options) {
6943
7899
  auditLog,
6944
7900
  handshakeResults
6945
7901
  );
7902
+ const { tools: auditTools } = createAuditTools(config);
6946
7903
  const policy = await loadPrincipalPolicy(config.storage_path);
6947
7904
  const baseline = new BaselineTracker(storage, masterKey);
6948
7905
  await baseline.load();
@@ -6958,7 +7915,7 @@ async function createSanctuaryServer(options) {
6958
7915
  port: config.dashboard.port,
6959
7916
  host: config.dashboard.host,
6960
7917
  timeout_seconds: policy.approval_channel.timeout_seconds,
6961
- auto_deny: policy.approval_channel.auto_deny,
7918
+ // SEC-002: auto_deny removed — timeout always denies
6962
7919
  auth_token: authToken,
6963
7920
  tls: config.dashboard.tls
6964
7921
  });
@@ -6971,8 +7928,8 @@ async function createSanctuaryServer(options) {
6971
7928
  webhook_secret: config.webhook.secret,
6972
7929
  callback_port: config.webhook.callback_port,
6973
7930
  callback_host: config.webhook.callback_host,
6974
- timeout_seconds: policy.approval_channel.timeout_seconds,
6975
- auto_deny: policy.approval_channel.auto_deny
7931
+ timeout_seconds: policy.approval_channel.timeout_seconds
7932
+ // SEC-002: auto_deny removed — timeout always denies
6976
7933
  });
6977
7934
  await webhook.start();
6978
7935
  approvalChannel = webhook;
@@ -6991,6 +7948,7 @@ async function createSanctuaryServer(options) {
6991
7948
  ...handshakeTools,
6992
7949
  ...federationTools,
6993
7950
  ...bridgeTools,
7951
+ ...auditTools,
6994
7952
  manifestTool
6995
7953
  ];
6996
7954
  const server = createServer(allTools, { gate });