@sanctuary-framework/mcp-server 1.0.0-rc.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -478,6 +478,15 @@ function defaultConfig() {
478
478
  auto_publish_to_verascore: true,
479
479
  // DELTA-04: default OFF for privacy. Enable explicitly per deployment.
480
480
  auto_publish_handshakes: false
481
+ },
482
+ privacy_filter: {
483
+ mode: "local",
484
+ // Fail-closed by default per Sanctuary Invariant #5: never silently
485
+ // degrade to a less-secure behavior on error. Operators who need a
486
+ // legacy fallback path opt in explicitly via config or env var.
487
+ fail_mode: "closed",
488
+ command: "opf",
489
+ timeout_ms: 5e3
481
490
  }
482
491
  };
483
492
  }
@@ -489,10 +498,14 @@ async function loadConfig(configPath) {
489
498
  const raw = await promises.readFile(path$1, "utf-8");
490
499
  const fileConfig = JSON.parse(raw);
491
500
  config = deepMerge(config, fileConfig);
501
+ assertSanctuaryConfigShape(config);
492
502
  } catch (err) {
493
503
  if (err instanceof Error && err.message.includes("unimplemented features")) {
494
504
  throw err;
495
505
  }
506
+ if (err instanceof Error && err.message.startsWith("Sanctuary config field")) {
507
+ throw err;
508
+ }
496
509
  }
497
510
  if (process.env.SANCTUARY_STORAGE_PATH) {
498
511
  config.storage_path = process.env.SANCTUARY_STORAGE_PATH;
@@ -563,6 +576,21 @@ async function loadConfig(configPath) {
563
576
  if (process.env.SANCTUARY_AUTO_PUBLISH_HANDSHAKES === "false") {
564
577
  config.verascore.auto_publish_handshakes = false;
565
578
  }
579
+ if (process.env.SANCTUARY_PRIVACY_FILTER) {
580
+ config.privacy_filter.mode = process.env.SANCTUARY_PRIVACY_FILTER;
581
+ }
582
+ if (process.env.SANCTUARY_PRIVACY_FILTER_FAIL_MODE) {
583
+ config.privacy_filter.fail_mode = process.env.SANCTUARY_PRIVACY_FILTER_FAIL_MODE;
584
+ }
585
+ if (process.env.SANCTUARY_PRIVACY_FILTER_COMMAND) {
586
+ config.privacy_filter.command = process.env.SANCTUARY_PRIVACY_FILTER_COMMAND;
587
+ }
588
+ if (process.env.SANCTUARY_PRIVACY_FILTER_TIMEOUT_MS) {
589
+ config.privacy_filter.timeout_ms = parseInt(
590
+ process.env.SANCTUARY_PRIVACY_FILTER_TIMEOUT_MS,
591
+ 10
592
+ );
593
+ }
566
594
  config.version = PKG_VERSION;
567
595
  validateConfig(config);
568
596
  return config;
@@ -603,6 +631,23 @@ function validateConfig(config) {
603
631
  `Unimplemented config value: reputation.mode = "${config.reputation.mode}". Only ${[...implementedReputationMode].map((v) => `"${v}"`).join(", ")} is currently implemented. Using an unimplemented reputation mode would silently skip reputation verification.`
604
632
  );
605
633
  }
634
+ const implementedPrivacyModes = /* @__PURE__ */ new Set(["local", "opf", "off"]);
635
+ if (!implementedPrivacyModes.has(config.privacy_filter.mode)) {
636
+ errors.push(
637
+ `Invalid config value: privacy_filter.mode = "${config.privacy_filter.mode}". Use ${[...implementedPrivacyModes].map((v) => `"${v}"`).join(", ")}.`
638
+ );
639
+ }
640
+ const implementedPrivacyFailModes = /* @__PURE__ */ new Set(["closed", "fallback"]);
641
+ if (!implementedPrivacyFailModes.has(config.privacy_filter.fail_mode)) {
642
+ errors.push(
643
+ `Invalid config value: privacy_filter.fail_mode = "${config.privacy_filter.fail_mode}". Use ${[...implementedPrivacyFailModes].map((v) => `"${v}"`).join(", ")}.`
644
+ );
645
+ }
646
+ if (!Number.isFinite(config.privacy_filter.timeout_ms) || config.privacy_filter.timeout_ms < 100) {
647
+ errors.push(
648
+ `Invalid config value: privacy_filter.timeout_ms = "${config.privacy_filter.timeout_ms}". Use an integer timeout of at least 100 ms.`
649
+ );
650
+ }
606
651
  if (errors.length > 0) {
607
652
  throw new Error(
608
653
  `Sanctuary configuration references unimplemented features:
@@ -624,6 +669,39 @@ function deepMerge(base, override) {
624
669
  }
625
670
  return result;
626
671
  }
672
+ function assertSanctuaryConfigShape(c) {
673
+ const requiredObjectKeys = [
674
+ "state",
675
+ "execution",
676
+ "disclosure",
677
+ "reputation",
678
+ "dashboard",
679
+ "webhook",
680
+ "verascore",
681
+ "privacy_filter"
682
+ ];
683
+ for (const k of requiredObjectKeys) {
684
+ if (typeof c[k] !== "object" || c[k] === null || Array.isArray(c[k])) {
685
+ throw new Error(
686
+ `Sanctuary config field "${k}" must be an object; got ${c[k] === null ? "null" : Array.isArray(c[k]) ? "array" : typeof c[k]}`
687
+ );
688
+ }
689
+ }
690
+ if (typeof c.version !== "string") {
691
+ throw new Error(`Sanctuary config field "version" must be a string`);
692
+ }
693
+ if (typeof c.storage_path !== "string") {
694
+ throw new Error(`Sanctuary config field "storage_path" must be a string`);
695
+ }
696
+ if (c.transport !== "stdio" && c.transport !== "http") {
697
+ throw new Error(
698
+ `Sanctuary config field "transport" must be "stdio" | "http"; got ${JSON.stringify(c.transport)}`
699
+ );
700
+ }
701
+ if (typeof c.http_port !== "number" || !Number.isFinite(c.http_port)) {
702
+ throw new Error(`Sanctuary config field "http_port" must be a finite number`);
703
+ }
704
+ }
627
705
 
628
706
  // src/storage/filesystem.ts
629
707
  init_random();
@@ -820,6 +898,11 @@ var RESERVED_NAMESPACE_PREFIXES = [
820
898
  "_context_gate_policies",
821
899
  "_fortress_mode"
822
900
  ];
901
+ function isReservedNamespace(namespace) {
902
+ return RESERVED_NAMESPACE_PREFIXES.some(
903
+ (prefix) => namespace === prefix || namespace.startsWith(prefix + "/")
904
+ );
905
+ }
823
906
  var StateStore = class _StateStore {
824
907
  storage;
825
908
  masterKey;
@@ -4148,6 +4231,25 @@ var DEFAULT_POLICY = {
4148
4231
  // Creates new Ed25519 identity + publishes — always requires approval
4149
4232
  "sanctuary_export_identity_bundle",
4150
4233
  // Exports portable identity — always requires approval
4234
+ "exit_bundle_export",
4235
+ // Complete portability bundle export. Always requires approval.
4236
+ "exit_bundle_import",
4237
+ // External durable-record import. Always requires approval.
4238
+ "exit_bundle_import_activate",
4239
+ // Activates imported material. Always requires approval.
4240
+ "exit_bundle_rekey",
4241
+ // Re-encrypts imported state under destination keys.
4242
+ // v1.1 hub-control surfaces. Every operation_category in
4243
+ // server/src/contracts/v1.1/hub-events.ts that is not already canonical
4244
+ // here MUST be enrolled under Tier 1 in the same PR that lands the hub
4245
+ // endpoint that surfaces it. Drift between the contract enum and this
4246
+ // list is a release blocker per the contract comment.
4247
+ "policy_change",
4248
+ // Operator-driven policy bind on a wrapped agent.
4249
+ "lockdown",
4250
+ // Operator-driven hard-stop of a wrapped agent.
4251
+ "unwrap",
4252
+ // Operator-driven removal of the Sanctuary wrap from an agent.
4151
4253
  // WP-MVP-2 Operator Console: federation-node-join requires explicit
4152
4254
  // operator confirmation per Key 8. No auto-approve path. The console's
4153
4255
  // JoinApprover drives this gate via `MeshConsoleClient.makeJoinApprover`.
@@ -4337,6 +4439,13 @@ tier1_always_approve:
4337
4439
  - governor_reset
4338
4440
  - sanctuary_bootstrap
4339
4441
  - sanctuary_export_identity_bundle
4442
+ - exit_bundle_export
4443
+ - exit_bundle_import
4444
+ - exit_bundle_import_activate
4445
+ - exit_bundle_rekey
4446
+ - policy_change
4447
+ - lockdown
4448
+ - unwrap
4340
4449
 
4341
4450
  # \u2500\u2500\u2500 Tier 2: Behavioral Anomaly Detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4342
4451
  # Triggers approval when agent behavior deviates from its baseline.
@@ -8934,7 +9043,7 @@ var DashboardApprovalChannel = class {
8934
9043
  server = http.createServer(handler);
8935
9044
  }
8936
9045
  this.httpServer = server;
8937
- return new Promise((resolve2, reject) => {
9046
+ return new Promise((resolve4, reject) => {
8938
9047
  const protocol = this.useTLS ? "https" : "http";
8939
9048
  const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
8940
9049
  server.listen(this.config.port, this.config.host, () => {
@@ -8959,7 +9068,7 @@ var DashboardApprovalChannel = class {
8959
9068
  if (shouldAutoOpen) {
8960
9069
  this.openInBrowser(sessionUrl);
8961
9070
  }
8962
- resolve2();
9071
+ resolve4();
8963
9072
  });
8964
9073
  server.on("error", (err) => {
8965
9074
  if (err.code === "EADDRINUSE") {
@@ -9005,8 +9114,8 @@ var DashboardApprovalChannel = class {
9005
9114
  }
9006
9115
  this.rateLimits.clear();
9007
9116
  if (this.httpServer) {
9008
- return new Promise((resolve2) => {
9009
- this.httpServer.close(() => resolve2());
9117
+ return new Promise((resolve4) => {
9118
+ this.httpServer.close(() => resolve4());
9010
9119
  });
9011
9120
  }
9012
9121
  }
@@ -9020,7 +9129,7 @@ var DashboardApprovalChannel = class {
9020
9129
  `[Sanctuary] Approval required: ${request.operation} (Tier ${request.tier}) \u2014 open dashboard to respond
9021
9130
  `
9022
9131
  );
9023
- return new Promise((resolve2) => {
9132
+ return new Promise((resolve4) => {
9024
9133
  const timer = setTimeout(() => {
9025
9134
  this.pending.delete(id);
9026
9135
  const response = {
@@ -9034,12 +9143,12 @@ var DashboardApprovalChannel = class {
9034
9143
  decision: response.decision,
9035
9144
  decided_by: "timeout"
9036
9145
  });
9037
- resolve2(response);
9146
+ resolve4(response);
9038
9147
  }, this.config.timeout_seconds * 1e3);
9039
9148
  const pending = {
9040
9149
  id,
9041
9150
  request,
9042
- resolve: resolve2,
9151
+ resolve: resolve4,
9043
9152
  timer,
9044
9153
  created_at: (/* @__PURE__ */ new Date()).toISOString()
9045
9154
  };
@@ -9885,7 +9994,7 @@ var WebhookApprovalChannel = class {
9885
9994
  * Start the callback listener server.
9886
9995
  */
9887
9996
  async start() {
9888
- return new Promise((resolve2, reject) => {
9997
+ return new Promise((resolve4, reject) => {
9889
9998
  this.callbackServer = http.createServer(
9890
9999
  (req, res) => this.handleCallback(req, res)
9891
10000
  );
@@ -9900,7 +10009,7 @@ var WebhookApprovalChannel = class {
9900
10009
 
9901
10010
  `
9902
10011
  );
9903
- resolve2();
10012
+ resolve4();
9904
10013
  }
9905
10014
  );
9906
10015
  this.callbackServer.on("error", reject);
@@ -9920,8 +10029,8 @@ var WebhookApprovalChannel = class {
9920
10029
  }
9921
10030
  this.pending.clear();
9922
10031
  if (this.callbackServer) {
9923
- return new Promise((resolve2) => {
9924
- this.callbackServer.close(() => resolve2());
10032
+ return new Promise((resolve4) => {
10033
+ this.callbackServer.close(() => resolve4());
9925
10034
  });
9926
10035
  }
9927
10036
  }
@@ -9934,7 +10043,7 @@ var WebhookApprovalChannel = class {
9934
10043
  `[Sanctuary] Webhook approval sent: ${request.operation} (Tier ${request.tier}) \u2014 awaiting callback
9935
10044
  `
9936
10045
  );
9937
- return new Promise((resolve2) => {
10046
+ return new Promise((resolve4) => {
9938
10047
  const timer = setTimeout(() => {
9939
10048
  this.pending.delete(id);
9940
10049
  const response = {
@@ -9943,12 +10052,12 @@ var WebhookApprovalChannel = class {
9943
10052
  decided_at: (/* @__PURE__ */ new Date()).toISOString(),
9944
10053
  decided_by: "timeout"
9945
10054
  };
9946
- resolve2(response);
10055
+ resolve4(response);
9947
10056
  }, this.config.timeout_seconds * 1e3);
9948
10057
  const pending = {
9949
10058
  id,
9950
10059
  request,
9951
- resolve: resolve2,
10060
+ resolve: resolve4,
9952
10061
  timer,
9953
10062
  created_at: (/* @__PURE__ */ new Date()).toISOString()
9954
10063
  };
@@ -11495,7 +11604,7 @@ var ApprovalGate = class {
11495
11604
  return {
11496
11605
  allowed: response.decision === "approve",
11497
11606
  tier,
11498
- reason: response.decision === "approve" ? `Approved by ${response.decided_by}` : reason,
11607
+ reason: response.decision === "approve" ? `Approved by ${response.decided_by}` : `Tier ${tier} operation requires approval`,
11499
11608
  approval_required: true,
11500
11609
  approval_response: response
11501
11610
  };
@@ -14257,6 +14366,140 @@ function createAuditTools(config) {
14257
14366
  return { tools };
14258
14367
  }
14259
14368
 
14369
+ // src/audit/reset-history.ts
14370
+ init_hashing();
14371
+ init_encoding();
14372
+ var RESET_HISTORY_FILENAME = ".reset-history.log";
14373
+ var RECOVERED_FROM_RESET_OPERATION = "fortress_recovered_from_reset";
14374
+ var ResetHistoryMalformedError = class extends Error {
14375
+ constructor(markerPath, lineNumber, cause) {
14376
+ const reason = cause instanceof Error ? cause.message : String(cause);
14377
+ super(
14378
+ `Reset-history marker at ${markerPath} is malformed at line ${lineNumber}: ${reason}.
14379
+ Inspect the file and either correct the JSON or delete it manually before re-running.`
14380
+ );
14381
+ this.markerPath = markerPath;
14382
+ this.lineNumber = lineNumber;
14383
+ this.cause = cause;
14384
+ this.name = "ResetHistoryMalformedError";
14385
+ }
14386
+ };
14387
+ function parseResetHistory(content, markerPath) {
14388
+ const markerHash = hashToString(stringToBytes(content));
14389
+ const markers = [];
14390
+ const lines = content.split("\n");
14391
+ for (let i = 0; i < lines.length; i++) {
14392
+ const raw = lines[i] ?? "";
14393
+ if (raw.trim().length === 0) continue;
14394
+ let parsed;
14395
+ try {
14396
+ parsed = JSON.parse(raw);
14397
+ } catch (err) {
14398
+ throw new ResetHistoryMalformedError(markerPath, i + 1, err);
14399
+ }
14400
+ markers.push(coerceMarker(parsed, markerPath, i + 1));
14401
+ }
14402
+ return { markers, markerHash };
14403
+ }
14404
+ function coerceMarker(raw, markerPath, lineNumber) {
14405
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
14406
+ throw new ResetHistoryMalformedError(
14407
+ markerPath,
14408
+ lineNumber,
14409
+ new Error("expected a JSON object")
14410
+ );
14411
+ }
14412
+ const obj = raw;
14413
+ const required = [
14414
+ "started_at",
14415
+ "completed_at",
14416
+ "recovery_mode",
14417
+ "fortress_name",
14418
+ "storage_path",
14419
+ "keychain_cleared"
14420
+ ];
14421
+ for (const field of required) {
14422
+ if (!(field in obj)) {
14423
+ throw new ResetHistoryMalformedError(
14424
+ markerPath,
14425
+ lineNumber,
14426
+ new Error(`missing required field "${field}"`)
14427
+ );
14428
+ }
14429
+ }
14430
+ if (typeof obj.started_at !== "string")
14431
+ throw typed(markerPath, lineNumber, "started_at", "string");
14432
+ if (typeof obj.completed_at !== "string")
14433
+ throw typed(markerPath, lineNumber, "completed_at", "string");
14434
+ if (typeof obj.fortress_name !== "string")
14435
+ throw typed(markerPath, lineNumber, "fortress_name", "string");
14436
+ if (typeof obj.storage_path !== "string")
14437
+ throw typed(markerPath, lineNumber, "storage_path", "string");
14438
+ if (typeof obj.keychain_cleared !== "boolean")
14439
+ throw typed(markerPath, lineNumber, "keychain_cleared", "boolean");
14440
+ if (obj.recovery_mode !== "shares" && obj.recovery_mode !== "guardian" && obj.recovery_mode !== "nuke") {
14441
+ throw new ResetHistoryMalformedError(
14442
+ markerPath,
14443
+ lineNumber,
14444
+ new Error(
14445
+ `recovery_mode must be one of shares|guardian|nuke (got ${JSON.stringify(obj.recovery_mode)})`
14446
+ )
14447
+ );
14448
+ }
14449
+ return {
14450
+ started_at: obj.started_at,
14451
+ completed_at: obj.completed_at,
14452
+ recovery_mode: obj.recovery_mode,
14453
+ fortress_name: obj.fortress_name,
14454
+ storage_path: obj.storage_path,
14455
+ keychain_cleared: obj.keychain_cleared
14456
+ };
14457
+ }
14458
+ function typed(markerPath, lineNumber, field, expected) {
14459
+ return new ResetHistoryMalformedError(
14460
+ markerPath,
14461
+ lineNumber,
14462
+ new Error(`field "${field}" must be a ${expected}`)
14463
+ );
14464
+ }
14465
+ async function consumeResetHistoryMarker(options) {
14466
+ const markerPath = path.join(options.storagePath, RESET_HISTORY_FILENAME);
14467
+ if (!await fileExists2(markerPath)) {
14468
+ return { emitted: 0, markerPath };
14469
+ }
14470
+ const content = await promises.readFile(markerPath, "utf-8");
14471
+ const { markers, markerHash } = parseResetHistory(content, markerPath);
14472
+ if (markers.length === 0) {
14473
+ await promises.rm(markerPath, { force: true });
14474
+ return { emitted: 0, markerHash, markerPath };
14475
+ }
14476
+ for (let i = 0; i < markers.length; i++) {
14477
+ const marker = markers[i];
14478
+ options.auditLog.append("l2", RECOVERED_FROM_RESET_OPERATION, "system", {
14479
+ reset_at_started: marker.started_at,
14480
+ reset_at_completed: marker.completed_at,
14481
+ recovery_mode: marker.recovery_mode,
14482
+ fortress_name: marker.fortress_name,
14483
+ reset_storage_path: marker.storage_path,
14484
+ keychain_cleared: marker.keychain_cleared,
14485
+ reset_marker_hash: markerHash,
14486
+ reset_marker_index: i,
14487
+ reset_marker_total: markers.length
14488
+ });
14489
+ }
14490
+ await options.auditLog.flush();
14491
+ await promises.rm(markerPath, { force: true });
14492
+ return { emitted: markers.length, markerHash, markerPath };
14493
+ }
14494
+ async function fileExists2(path) {
14495
+ try {
14496
+ await promises.access(path);
14497
+ return true;
14498
+ } catch {
14499
+ return false;
14500
+ }
14501
+ }
14502
+
14260
14503
  // src/audit/siem-formatter.ts
14261
14504
  function parseGateDecision(details) {
14262
14505
  if (!details || typeof details.gate_decision !== "string") {
@@ -15324,6 +15567,607 @@ function matchesFieldPattern(normalizedField, pattern) {
15324
15567
  // src/l2-operational/context-gate-enforcer.ts
15325
15568
  init_encoding();
15326
15569
  init_hashing();
15570
+
15571
+ // src/l2-operational/privacy-filter.ts
15572
+ init_encryption();
15573
+ init_encoding();
15574
+ init_hashing();
15575
+ var SPAN_PATTERNS = [
15576
+ {
15577
+ class: "email",
15578
+ pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi,
15579
+ replacement: "[EMAIL_REDACTED]",
15580
+ placeholderPrefix: "EMAIL",
15581
+ detectorClass: "person"
15582
+ },
15583
+ {
15584
+ class: "ssn",
15585
+ pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
15586
+ replacement: "[SSN_REDACTED]",
15587
+ placeholderPrefix: "SSN",
15588
+ detectorClass: "person"
15589
+ },
15590
+ {
15591
+ class: "credit_card",
15592
+ pattern: /\b(?:\d[ -]*?){13,19}\b/g,
15593
+ replacement: "[CARD_REDACTED]",
15594
+ placeholderPrefix: "CARD",
15595
+ detectorClass: "account"
15596
+ },
15597
+ {
15598
+ class: "phone",
15599
+ pattern: /\b(?:\+?1[-.\s]?)?(?:\(?\d{3}\)?[-.\s]?)\d{3}[-.\s]?\d{4}\b/g,
15600
+ replacement: "[PHONE_REDACTED]",
15601
+ placeholderPrefix: "PHONE",
15602
+ detectorClass: "person"
15603
+ },
15604
+ {
15605
+ class: "secret_assignment",
15606
+ pattern: /\b(api[_-]?key|access[_-]?token|refresh[_-]?token|password|secret)\s*[:=]\s*["']?[^"',\s}]+/gi,
15607
+ replacement: "$1=[SECRET_REDACTED]",
15608
+ placeholderPrefix: "SECRET",
15609
+ detectorClass: "secret"
15610
+ },
15611
+ {
15612
+ class: "secret",
15613
+ pattern: /\b(?:Bearer\s+[A-Za-z0-9._~+/-]{16,}|sk-[A-Za-z0-9_-]{12,}|sk_(?:live|test)_[A-Za-z0-9]{12,}|ghp_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b/g,
15614
+ replacement: "[SECRET_REDACTED]",
15615
+ placeholderPrefix: "SECRET",
15616
+ detectorClass: "secret"
15617
+ },
15618
+ {
15619
+ class: "credential",
15620
+ pattern: /\b(?:credential|credentials|client[_-]?secret|private[_-]?key)\s*[:=]\s*["']?[^"',\s}]+/gi,
15621
+ replacement: "[CREDENTIAL_REDACTED]",
15622
+ placeholderPrefix: "CREDENTIAL",
15623
+ detectorClass: "credential"
15624
+ },
15625
+ {
15626
+ class: "account_number",
15627
+ pattern: /\b(?:(?:acct|account|customer|tenant|org)[_-][A-Za-z0-9]{4,}|(?:acct|account)[0-9]{4,})\b/gi,
15628
+ replacement: "[ACCOUNT_REDACTED]",
15629
+ placeholderPrefix: "ACCOUNT",
15630
+ detectorClass: "account"
15631
+ },
15632
+ {
15633
+ class: "file_path",
15634
+ pattern: /(?:~\/|\/(?:Users|home|var|tmp|etc|opt)\/|[A-Za-z]:\\|\.{1,2}\/)(?:[A-Za-z0-9._-]+[\\/])+[A-Za-z0-9._-]+/g,
15635
+ replacement: "[FILE_PATH_REDACTED]",
15636
+ placeholderPrefix: "FILE_PATH",
15637
+ detectorClass: "file_path"
15638
+ }
15639
+ ];
15640
+ var MAX_DEPTH = 20;
15641
+ var VAULT_NAMESPACE = "_privacy_placeholder_vault";
15642
+ var PRIVACY_VAULT_CACHE_MAX = 5e3;
15643
+ var LruCache = class {
15644
+ max;
15645
+ map = /* @__PURE__ */ new Map();
15646
+ constructor(max) {
15647
+ this.max = max;
15648
+ }
15649
+ get(key) {
15650
+ const value = this.map.get(key);
15651
+ if (value === void 0) return void 0;
15652
+ this.map.delete(key);
15653
+ this.map.set(key, value);
15654
+ return value;
15655
+ }
15656
+ set(key, value) {
15657
+ if (this.map.has(key)) {
15658
+ this.map.delete(key);
15659
+ } else if (this.map.size >= this.max) {
15660
+ const oldestKey = this.map.keys().next().value;
15661
+ if (oldestKey !== void 0) {
15662
+ this.map.delete(oldestKey);
15663
+ }
15664
+ }
15665
+ this.map.set(key, value);
15666
+ }
15667
+ has(key) {
15668
+ return this.map.has(key);
15669
+ }
15670
+ get size() {
15671
+ return this.map.size;
15672
+ }
15673
+ };
15674
+ var PrivacyPlaceholderVault = class {
15675
+ storage;
15676
+ encryptionKey;
15677
+ lookupKey;
15678
+ cache = new LruCache(PRIVACY_VAULT_CACHE_MAX);
15679
+ pathCache = new LruCache(PRIVACY_VAULT_CACHE_MAX);
15680
+ constructor(storage, masterKey) {
15681
+ this.storage = storage;
15682
+ this.encryptionKey = derivePurposeKey(masterKey, "l2-privacy-placeholders");
15683
+ this.lookupKey = derivePurposeKey(masterKey, "l2-privacy-placeholder-lookup");
15684
+ }
15685
+ async placeholderFor(spanClass, rawValue, scope = "default") {
15686
+ const key = this.recordKey(spanClass, rawValue, scope);
15687
+ const cached = this.cache.get(key);
15688
+ if (cached) return cached.placeholder;
15689
+ const existing = await this.readRecord(key);
15690
+ if (existing) {
15691
+ this.cache.set(key, existing);
15692
+ return existing.placeholder;
15693
+ }
15694
+ const index = await this.readIndex(scope);
15695
+ const next = (index.counters[spanClass] ?? 0) + 1;
15696
+ index.counters[spanClass] = next;
15697
+ const placeholder = `${placeholderPrefixFor(spanClass)}_${next}`;
15698
+ const record = {
15699
+ version: 1,
15700
+ kind: "placeholder",
15701
+ scope,
15702
+ class: spanClass,
15703
+ placeholder,
15704
+ raw_value: rawValue,
15705
+ raw_hash: this.hmacString(`raw:${rawValue}`),
15706
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
15707
+ };
15708
+ await this.writeRecord(key, record);
15709
+ await this.writeIndex(scope, index);
15710
+ this.cache.set(key, record);
15711
+ return placeholder;
15712
+ }
15713
+ async resolvePlaceholder(placeholder, scope = "default") {
15714
+ const entries = await this.storage.list(VAULT_NAMESPACE, `${scope}__record__`);
15715
+ for (const meta of entries) {
15716
+ const record = await this.readRecord(meta.key);
15717
+ if (record?.placeholder === placeholder) {
15718
+ return record.raw_value;
15719
+ }
15720
+ }
15721
+ return null;
15722
+ }
15723
+ async aliasForFieldPath(path, scope = "default") {
15724
+ const key = this.pathRecordKey(path, scope);
15725
+ const cached = this.pathCache.get(key);
15726
+ if (cached) return cached.alias;
15727
+ const existing = await this.readPathRecord(key);
15728
+ if (existing) {
15729
+ this.pathCache.set(key, existing);
15730
+ return existing.alias;
15731
+ }
15732
+ const index = await this.readPathIndex(scope);
15733
+ const alias = `$${index.next}`;
15734
+ const nextIndex = { version: 1, next: index.next + 1 };
15735
+ const record = {
15736
+ version: 1,
15737
+ kind: "field_path",
15738
+ scope,
15739
+ alias,
15740
+ raw_path: path,
15741
+ raw_hash: this.hmacString(`path:${path}`),
15742
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
15743
+ };
15744
+ await this.writePathRecord(key, record);
15745
+ await this.writePathIndex(scope, nextIndex);
15746
+ this.pathCache.set(key, record);
15747
+ return alias;
15748
+ }
15749
+ async resolveFieldPathAlias(alias, scope = "default") {
15750
+ const entries = await this.storage.list(VAULT_NAMESPACE, `${scope}__path__`);
15751
+ for (const meta of entries) {
15752
+ const record = await this.readPathRecord(meta.key);
15753
+ if (record?.alias === alias) {
15754
+ return record.raw_path;
15755
+ }
15756
+ }
15757
+ return null;
15758
+ }
15759
+ recordKey(spanClass, rawValue, scope) {
15760
+ const rawHash = this.hmacString(`${scope}:${spanClass}:${rawValue}`);
15761
+ return `${scope}__record__${spanClass}__${rawHash}`;
15762
+ }
15763
+ indexKey(scope) {
15764
+ return `${scope}__index`;
15765
+ }
15766
+ pathRecordKey(path, scope) {
15767
+ return `${scope}__path__${this.hmacString(`${scope}:field_path:${path}`)}`;
15768
+ }
15769
+ pathIndexKey(scope) {
15770
+ return `${scope}__path_index`;
15771
+ }
15772
+ async readIndex(scope) {
15773
+ const raw = await this.storage.read(VAULT_NAMESPACE, this.indexKey(scope));
15774
+ if (!raw) return { version: 1, counters: {} };
15775
+ try {
15776
+ const encrypted = JSON.parse(bytesToString(raw));
15777
+ const decrypted = decrypt(encrypted, this.encryptionKey);
15778
+ return JSON.parse(bytesToString(decrypted));
15779
+ } catch (err) {
15780
+ throw new PrivacyVaultError("privacy_vault_index_unreadable", err);
15781
+ }
15782
+ }
15783
+ async writeIndex(scope, index) {
15784
+ const encrypted = encrypt(stringToBytes(JSON.stringify(index)), this.encryptionKey);
15785
+ await this.storage.write(
15786
+ VAULT_NAMESPACE,
15787
+ this.indexKey(scope),
15788
+ stringToBytes(JSON.stringify(encrypted))
15789
+ );
15790
+ }
15791
+ async readRecord(key) {
15792
+ const raw = await this.storage.read(VAULT_NAMESPACE, key);
15793
+ if (!raw) return null;
15794
+ try {
15795
+ const encrypted = JSON.parse(bytesToString(raw));
15796
+ const decrypted = decrypt(encrypted, this.encryptionKey);
15797
+ const parsed = JSON.parse(bytesToString(decrypted));
15798
+ return parsed.kind === "placeholder" || parsed.kind === void 0 ? parsed : null;
15799
+ } catch (err) {
15800
+ throw new PrivacyVaultError("privacy_vault_record_unreadable", err);
15801
+ }
15802
+ }
15803
+ async writeRecord(key, record) {
15804
+ const encrypted = encrypt(stringToBytes(JSON.stringify(record)), this.encryptionKey);
15805
+ await this.storage.write(
15806
+ VAULT_NAMESPACE,
15807
+ key,
15808
+ stringToBytes(JSON.stringify(encrypted))
15809
+ );
15810
+ }
15811
+ async readPathIndex(scope) {
15812
+ const raw = await this.storage.read(VAULT_NAMESPACE, this.pathIndexKey(scope));
15813
+ if (!raw) return { version: 1, next: 0 };
15814
+ try {
15815
+ const encrypted = JSON.parse(bytesToString(raw));
15816
+ const decrypted = decrypt(encrypted, this.encryptionKey);
15817
+ return JSON.parse(bytesToString(decrypted));
15818
+ } catch (err) {
15819
+ throw new PrivacyVaultError("privacy_vault_path_index_unreadable", err);
15820
+ }
15821
+ }
15822
+ async writePathIndex(scope, index) {
15823
+ const encrypted = encrypt(stringToBytes(JSON.stringify(index)), this.encryptionKey);
15824
+ await this.storage.write(
15825
+ VAULT_NAMESPACE,
15826
+ this.pathIndexKey(scope),
15827
+ stringToBytes(JSON.stringify(encrypted))
15828
+ );
15829
+ }
15830
+ async readPathRecord(key) {
15831
+ const raw = await this.storage.read(VAULT_NAMESPACE, key);
15832
+ if (!raw) return null;
15833
+ try {
15834
+ const encrypted = JSON.parse(bytesToString(raw));
15835
+ const decrypted = decrypt(encrypted, this.encryptionKey);
15836
+ const parsed = JSON.parse(bytesToString(decrypted));
15837
+ return parsed.kind === "field_path" ? parsed : null;
15838
+ } catch (err) {
15839
+ throw new PrivacyVaultError("privacy_vault_path_record_unreadable", err);
15840
+ }
15841
+ }
15842
+ async writePathRecord(key, record) {
15843
+ const encrypted = encrypt(stringToBytes(JSON.stringify(record)), this.encryptionKey);
15844
+ await this.storage.write(
15845
+ VAULT_NAMESPACE,
15846
+ key,
15847
+ stringToBytes(JSON.stringify(encrypted))
15848
+ );
15849
+ }
15850
+ hmacString(value) {
15851
+ return bytesToHex(hmacSha256(this.lookupKey, stringToBytes(value)));
15852
+ }
15853
+ };
15854
+ var PrivacyVaultError = class extends Error {
15855
+ code;
15856
+ cause;
15857
+ constructor(code, cause) {
15858
+ super(code);
15859
+ this.name = "PrivacyVaultError";
15860
+ this.code = code;
15861
+ this.cause = cause;
15862
+ }
15863
+ };
15864
+ function applyLocalPrivacyFilter(value, path = "$") {
15865
+ const findings = [];
15866
+ const filtered = filterValue(value, path, findings, 0);
15867
+ return { value: filtered, findings };
15868
+ }
15869
+ async function applyPrivacyPlaceholders(value, vault, scope = "default", path = "$") {
15870
+ const findings = [];
15871
+ const filtered = await placeholderValue(value, path, findings, vault, scope, 0);
15872
+ return { value: filtered, findings };
15873
+ }
15874
+ async function applyOpenAIPrivacyFilterResult(result, vault, scope = "default", path = "$") {
15875
+ const text = result.text;
15876
+ const findings = [];
15877
+ const spans = result.detected_spans.map((span) => ({
15878
+ ...span,
15879
+ class: mapOpenAIPrivacyLabel(span.label)
15880
+ })).filter(
15881
+ (span) => span.class !== null && Number.isInteger(span.start) && Number.isInteger(span.end) && span.start >= 0 && span.end > span.start && span.end <= text.length
15882
+ ).sort((a, b) => a.start - b.start);
15883
+ let cursor = 0;
15884
+ const pieces = [];
15885
+ for (const span of spans) {
15886
+ if (span.start < cursor) continue;
15887
+ const raw = text.slice(span.start, span.end);
15888
+ const placeholder = await vault.placeholderFor(span.class, raw, scope);
15889
+ pieces.push(text.slice(cursor, span.start));
15890
+ pieces.push(placeholder);
15891
+ cursor = span.end;
15892
+ findings.push({
15893
+ path,
15894
+ class: span.class,
15895
+ action: "placeholder",
15896
+ placeholder
15897
+ });
15898
+ }
15899
+ pieces.push(text.slice(cursor));
15900
+ return {
15901
+ value: pieces.join(""),
15902
+ findings
15903
+ };
15904
+ }
15905
+ function detectSensitiveSpans(input, options = {}) {
15906
+ const spans = [];
15907
+ addConfiguredTermSpans(
15908
+ spans,
15909
+ input,
15910
+ options.clientNames ?? [],
15911
+ "client",
15912
+ "client",
15913
+ "CLIENT"
15914
+ );
15915
+ addConfiguredTermSpans(
15916
+ spans,
15917
+ input,
15918
+ options.projectNames ?? [],
15919
+ "project",
15920
+ "project",
15921
+ "PROJECT"
15922
+ );
15923
+ addConfiguredTermSpans(
15924
+ spans,
15925
+ input,
15926
+ options.domainTerms ?? [],
15927
+ "domain_term",
15928
+ "domain_term",
15929
+ "TERM"
15930
+ );
15931
+ addConfiguredTermSpans(
15932
+ spans,
15933
+ input,
15934
+ options.personNames ?? [],
15935
+ "person",
15936
+ "person",
15937
+ "PERSON"
15938
+ );
15939
+ const pathHint = options.pathHint?.toLowerCase() ?? "";
15940
+ if (input.length <= 120 && /(?:^|[^a-z0-9])(?:name|owner|recipient|contact|person)(?:$|[^a-z0-9])/.test(pathHint) && /^[A-Z][A-Za-z'-]+(?:\s+[A-Z][A-Za-z'-]+){1,3}$/.test(input.trim())) {
15941
+ const start = input.indexOf(input.trim());
15942
+ spans.push({
15943
+ class: "person",
15944
+ detectorClass: "person",
15945
+ start,
15946
+ end: start + input.trim().length,
15947
+ text: input.trim(),
15948
+ placeholderPrefix: "PERSON"
15949
+ });
15950
+ }
15951
+ for (const pattern of SPAN_PATTERNS) {
15952
+ pattern.pattern.lastIndex = 0;
15953
+ for (const match of input.matchAll(pattern.pattern)) {
15954
+ if (match.index === void 0 || match[0].length === 0) continue;
15955
+ spans.push({
15956
+ class: pattern.class,
15957
+ detectorClass: pattern.detectorClass ?? detectorClassForSpan(pattern.class),
15958
+ start: match.index,
15959
+ end: match.index + match[0].length,
15960
+ text: match[0],
15961
+ placeholderPrefix: pattern.placeholderPrefix
15962
+ });
15963
+ }
15964
+ }
15965
+ return removeOverlappingSpans(spans);
15966
+ }
15967
+ function detectorClassForSpan(spanClass) {
15968
+ switch (spanClass) {
15969
+ case "client":
15970
+ return "client";
15971
+ case "project":
15972
+ return "project";
15973
+ case "secret":
15974
+ case "secret_assignment":
15975
+ return "secret";
15976
+ case "credential":
15977
+ return "credential";
15978
+ case "account_number":
15979
+ case "credit_card":
15980
+ return "account";
15981
+ case "file_path":
15982
+ return "file_path";
15983
+ case "domain_term":
15984
+ return "domain_term";
15985
+ case "custom":
15986
+ return "custom";
15987
+ case "email":
15988
+ case "phone":
15989
+ case "ssn":
15990
+ case "address":
15991
+ case "person":
15992
+ case "url":
15993
+ case "date":
15994
+ default:
15995
+ return "person";
15996
+ }
15997
+ }
15998
+ function placeholderPrefixFor(spanClass) {
15999
+ return SPAN_PATTERNS.find((p) => p.class === spanClass)?.placeholderPrefix ?? OPENAI_LABEL_PREFIXES[spanClass] ?? CONTRACT_CLASS_PREFIXES[spanClass] ?? "PRIVATE";
16000
+ }
16001
+ function filterValue(value, path, findings, depth) {
16002
+ if (depth > MAX_DEPTH) return value;
16003
+ if (typeof value === "string") {
16004
+ return filterString(value, path, findings);
16005
+ }
16006
+ if (Array.isArray(value)) {
16007
+ return value.map(
16008
+ (item, index) => filterValue(item, `${path}[${index}]`, findings, depth + 1)
16009
+ );
16010
+ }
16011
+ if (value && typeof value === "object") {
16012
+ const filtered = {};
16013
+ for (const [key, child] of Object.entries(value)) {
16014
+ filtered[key] = filterValue(child, `${path}.${key}`, findings, depth + 1);
16015
+ }
16016
+ return filtered;
16017
+ }
16018
+ return value;
16019
+ }
16020
+ function filterString(input, path, findings) {
16021
+ let output = input;
16022
+ for (const span of SPAN_PATTERNS) {
16023
+ const before = output;
16024
+ output = output.replace(span.pattern, span.replacement);
16025
+ if (output !== before) {
16026
+ findings.push({ path, class: span.class, action: "redact" });
16027
+ }
16028
+ }
16029
+ return output;
16030
+ }
16031
+ async function placeholderValue(value, path, findings, vault, scope, depth) {
16032
+ if (depth > MAX_DEPTH) return value;
16033
+ if (typeof value === "string") {
16034
+ return placeholderString(value, path, findings, vault, scope);
16035
+ }
16036
+ if (Array.isArray(value)) {
16037
+ const out = [];
16038
+ for (let index = 0; index < value.length; index++) {
16039
+ out.push(await placeholderValue(
16040
+ value[index],
16041
+ `${path}[${index}]`,
16042
+ findings,
16043
+ vault,
16044
+ scope,
16045
+ depth + 1
16046
+ ));
16047
+ }
16048
+ return out;
16049
+ }
16050
+ if (value && typeof value === "object") {
16051
+ const filtered = {};
16052
+ for (const [key, child] of Object.entries(value)) {
16053
+ filtered[key] = await placeholderValue(
16054
+ child,
16055
+ `${path}.${key}`,
16056
+ findings,
16057
+ vault,
16058
+ scope,
16059
+ depth + 1
16060
+ );
16061
+ }
16062
+ return filtered;
16063
+ }
16064
+ return value;
16065
+ }
16066
+ async function placeholderString(input, path, findings, vault, scope) {
16067
+ const spans = detectSensitiveSpans(input, { pathHint: path });
16068
+ if (spans.length === 0) return input;
16069
+ let cursor = 0;
16070
+ const pieces = [];
16071
+ for (const span of spans) {
16072
+ const placeholder = await vault.placeholderFor(span.class, span.text, scope);
16073
+ pieces.push(input.slice(cursor, span.start));
16074
+ pieces.push(placeholder);
16075
+ cursor = span.end;
16076
+ findings.push({
16077
+ path,
16078
+ class: span.class,
16079
+ action: "placeholder",
16080
+ placeholder
16081
+ });
16082
+ }
16083
+ pieces.push(input.slice(cursor));
16084
+ return pieces.join("");
16085
+ }
16086
+ var OPENAI_LABEL_PREFIXES = {
16087
+ account_number: "ACCOUNT",
16088
+ address: "ADDRESS",
16089
+ client: "CLIENT",
16090
+ credential: "CREDENTIAL",
16091
+ domain_term: "TERM",
16092
+ person: "PERSON",
16093
+ project: "PROJECT",
16094
+ file_path: "FILE_PATH",
16095
+ secret: "SECRET",
16096
+ url: "URL",
16097
+ date: "DATE"
16098
+ };
16099
+ var CONTRACT_CLASS_PREFIXES = {
16100
+ account_number: "ACCOUNT",
16101
+ client: "CLIENT",
16102
+ credential: "CREDENTIAL",
16103
+ domain_term: "TERM",
16104
+ file_path: "FILE_PATH",
16105
+ project: "PROJECT",
16106
+ secret: "SECRET"
16107
+ };
16108
+ function addConfiguredTermSpans(spans, input, terms, spanClass, detectorClass, placeholderPrefix) {
16109
+ const sortedTerms = [...new Set(terms.map((term) => term.trim()).filter(Boolean))].sort((a, b) => b.length - a.length);
16110
+ for (const term of sortedTerms) {
16111
+ const pattern = new RegExp(escapeRegExp(term), "gi");
16112
+ for (const match of input.matchAll(pattern)) {
16113
+ if (match.index === void 0 || match[0].length === 0) continue;
16114
+ spans.push({
16115
+ class: spanClass,
16116
+ detectorClass,
16117
+ start: match.index,
16118
+ end: match.index + match[0].length,
16119
+ text: match[0],
16120
+ placeholderPrefix
16121
+ });
16122
+ }
16123
+ }
16124
+ }
16125
+ function removeOverlappingSpans(spans) {
16126
+ const sorted = spans.sort((a, b) => {
16127
+ if (a.start !== b.start) return a.start - b.start;
16128
+ return b.end - b.start - (a.end - a.start);
16129
+ });
16130
+ const accepted = [];
16131
+ let cursor = -1;
16132
+ for (const span of sorted) {
16133
+ if (span.start < cursor) continue;
16134
+ accepted.push(span);
16135
+ cursor = span.end;
16136
+ }
16137
+ return accepted;
16138
+ }
16139
+ function escapeRegExp(input) {
16140
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16141
+ }
16142
+ function bytesToHex(bytes) {
16143
+ return Buffer.from(bytes).toString("hex");
16144
+ }
16145
+ function mapOpenAIPrivacyLabel(label) {
16146
+ switch (label) {
16147
+ case "account_number":
16148
+ return "account_number";
16149
+ case "private_address":
16150
+ return "address";
16151
+ case "private_email":
16152
+ return "email";
16153
+ case "private_person":
16154
+ return "person";
16155
+ case "private_phone":
16156
+ return "phone";
16157
+ case "private_url":
16158
+ return "url";
16159
+ case "private_date":
16160
+ return "date";
16161
+ case "secret":
16162
+ return "secret_assignment";
16163
+ case "redacted":
16164
+ return "secret_assignment";
16165
+ default:
16166
+ return null;
16167
+ }
16168
+ }
16169
+
16170
+ // src/l2-operational/context-gate-enforcer.ts
15327
16171
  var BUILTIN_SENSITIVE_PATTERNS = [
15328
16172
  "*_key",
15329
16173
  "*_token",
@@ -15349,6 +16193,7 @@ var BUILTIN_SENSITIVE_PATTERNS = [
15349
16193
  var ContextGateEnforcer = class {
15350
16194
  policyStore;
15351
16195
  auditLog;
16196
+ privacyVault;
15352
16197
  config;
15353
16198
  stats = {
15354
16199
  calls_inspected: 0,
@@ -15358,9 +16203,10 @@ var ContextGateEnforcer = class {
15358
16203
  fields_blocked: 0,
15359
16204
  calls_blocked: 0
15360
16205
  };
15361
- constructor(policyStore, auditLog, config) {
16206
+ constructor(policyStore, auditLog, config, privacyVault) {
15362
16207
  this.policyStore = policyStore;
15363
16208
  this.auditLog = auditLog;
16209
+ this.privacyVault = privacyVault;
15364
16210
  this.config = config;
15365
16211
  }
15366
16212
  /**
@@ -15437,6 +16283,7 @@ var ContextGateEnforcer = class {
15437
16283
  }
15438
16284
  }
15439
16285
  const filteredArgs = this.buildFilteredArgs(args, result.decisions);
16286
+ const privacyFiltered = this.privacyVault ? await applyPrivacyPlaceholders(filteredArgs, this.privacyVault, policy.policy_id) : applyLocalPrivacyFilter(filteredArgs);
15440
16287
  if (this.config.log_only) {
15441
16288
  this.auditLog.append(
15442
16289
  "l2",
@@ -15450,6 +16297,8 @@ var ContextGateEnforcer = class {
15450
16297
  fields_redacted: result.fields_redacted,
15451
16298
  fields_hashed: result.fields_hashed,
15452
16299
  fields_blocked: deniedFields.length,
16300
+ privacy_findings: privacyFiltered.findings.length,
16301
+ privacy_classes: [...new Set(privacyFiltered.findings.map((f) => f.class))],
15453
16302
  original_context_hash: result.original_context_hash
15454
16303
  }
15455
16304
  );
@@ -15470,13 +16319,15 @@ var ContextGateEnforcer = class {
15470
16319
  fields_redacted: result.fields_redacted,
15471
16320
  fields_hashed: result.fields_hashed,
15472
16321
  fields_blocked: deniedFields.length,
16322
+ privacy_findings: privacyFiltered.findings.length,
16323
+ privacy_classes: [...new Set(privacyFiltered.findings.map((f) => f.class))],
15473
16324
  original_context_hash: result.original_context_hash
15474
16325
  }
15475
16326
  );
15476
16327
  this.stats.fields_redacted += result.fields_redacted;
15477
16328
  this.stats.fields_hashed += result.fields_hashed;
15478
16329
  this.stats.fields_blocked += deniedFields.length;
15479
- return originalHandler(filteredArgs);
16330
+ return originalHandler(privacyFiltered.value);
15480
16331
  }
15481
16332
  /**
15482
16333
  * Filter tool arguments using built-in sensitive patterns.
@@ -15493,6 +16344,39 @@ var ContextGateEnforcer = class {
15493
16344
  }
15494
16345
  }
15495
16346
  if (fieldsToRedact.length === 0) {
16347
+ const privacyFiltered2 = this.privacyVault ? await applyPrivacyPlaceholders(args, this.privacyVault, `builtin:${toolName}`) : applyLocalPrivacyFilter(args);
16348
+ if (privacyFiltered2.findings.length > 0) {
16349
+ const filteredHash2 = hashToString(
16350
+ stringToBytes(JSON.stringify(privacyFiltered2.value))
16351
+ );
16352
+ if (this.config.log_only) {
16353
+ this.auditLog.append(
16354
+ "l2",
16355
+ "context_gate_enforcer_builtin_privacy_log_only",
16356
+ "system",
16357
+ {
16358
+ tool_name: toolName,
16359
+ privacy_findings: privacyFiltered2.findings.length,
16360
+ privacy_classes: [...new Set(privacyFiltered2.findings.map((f) => f.class))],
16361
+ original_context_hash: originalHash
16362
+ }
16363
+ );
16364
+ return originalHandler(args);
16365
+ }
16366
+ this.auditLog.append(
16367
+ "l2",
16368
+ "context_gate_enforcer_builtin_privacy_filter",
16369
+ "system",
16370
+ {
16371
+ tool_name: toolName,
16372
+ privacy_findings: privacyFiltered2.findings.length,
16373
+ privacy_classes: [...new Set(privacyFiltered2.findings.map((f) => f.class))],
16374
+ original_context_hash: originalHash,
16375
+ filtered_context_hash: filteredHash2
16376
+ }
16377
+ );
16378
+ return originalHandler(privacyFiltered2.value);
16379
+ }
15496
16380
  this.auditLog.append(
15497
16381
  "l2",
15498
16382
  "context_gate_enforcer_builtin_pass",
@@ -15512,8 +16396,9 @@ var ContextGateEnforcer = class {
15512
16396
  filteredArgs[key] = value;
15513
16397
  }
15514
16398
  }
16399
+ const privacyFiltered = this.privacyVault ? await applyPrivacyPlaceholders(filteredArgs, this.privacyVault, `builtin:${toolName}`) : applyLocalPrivacyFilter(filteredArgs);
15515
16400
  const filteredHash = hashToString(
15516
- stringToBytes(JSON.stringify(filteredArgs))
16401
+ stringToBytes(JSON.stringify(privacyFiltered.value))
15517
16402
  );
15518
16403
  if (this.config.log_only) {
15519
16404
  this.auditLog.append(
@@ -15524,6 +16409,8 @@ var ContextGateEnforcer = class {
15524
16409
  tool_name: toolName,
15525
16410
  fields_redacted: fieldsToRedact.length,
15526
16411
  redacted_fields: fieldsToRedact,
16412
+ privacy_findings: privacyFiltered.findings.length,
16413
+ privacy_classes: [...new Set(privacyFiltered.findings.map((f) => f.class))],
15527
16414
  original_context_hash: originalHash
15528
16415
  }
15529
16416
  );
@@ -15538,12 +16425,14 @@ var ContextGateEnforcer = class {
15538
16425
  tool_name: toolName,
15539
16426
  fields_redacted: fieldsToRedact.length,
15540
16427
  redacted_fields: fieldsToRedact,
16428
+ privacy_findings: privacyFiltered.findings.length,
16429
+ privacy_classes: [...new Set(privacyFiltered.findings.map((f) => f.class))],
15541
16430
  original_context_hash: originalHash,
15542
16431
  filtered_context_hash: filteredHash
15543
16432
  }
15544
16433
  );
15545
16434
  this.stats.fields_redacted += fieldsToRedact.length;
15546
- return originalHandler(filteredArgs);
16435
+ return originalHandler(privacyFiltered.value);
15547
16436
  }
15548
16437
  /**
15549
16438
  * Check if a tool should be filtered based on bypass prefixes.
@@ -15649,21 +16538,166 @@ var ContextGateEnforcer = class {
15649
16538
  };
15650
16539
  }
15651
16540
  };
15652
-
15653
- // src/l2-operational/context-gate-tools.ts
15654
- function createContextGateTools(storage, masterKey, auditLog) {
15655
- const policyStore = new ContextGatePolicyStore(storage, masterKey);
15656
- const enforcerConfig = {
15657
- enabled: false,
15658
- // Off by default; agents must explicitly enable it
15659
- bypass_prefixes: ["*"],
15660
- // Skip all Sanctuary-internal tools; only proxy/ tools get filtered
15661
- log_only: false,
16541
+ var PrivacyFilterRuntimeError = class extends Error {
16542
+ constructor(message) {
16543
+ super(message);
16544
+ this.name = "PrivacyFilterRuntimeError";
16545
+ }
16546
+ };
16547
+ async function applyConfiguredPrivacyFilter(value, vault, scope, config) {
16548
+ if (config.mode === "off") {
16549
+ return { value, findings: [], mode: "off" };
16550
+ }
16551
+ if (config.mode === "local") {
16552
+ const result = await applyPrivacyPlaceholders(value, vault, scope);
16553
+ return { ...result, mode: "local" };
16554
+ }
16555
+ try {
16556
+ const result = await applyOpenAIPrivacyFilter(value, vault, scope, config);
16557
+ return { ...result, mode: "opf" };
16558
+ } catch (err) {
16559
+ if (config.fail_mode === "fallback") {
16560
+ const result = await applyPrivacyPlaceholders(value, vault, scope);
16561
+ return { ...result, mode: "local", fallback_from: "opf" };
16562
+ }
16563
+ throw err;
16564
+ }
16565
+ }
16566
+ async function applyOpenAIPrivacyFilter(value, vault, scope, config) {
16567
+ if (typeof value === "string") {
16568
+ const opf = await runOpenAIPrivacyFilter(value, config);
16569
+ const filtered = await applyOpenAIPrivacyFilterResult(opf, vault, scope);
16570
+ return filtered;
16571
+ }
16572
+ if (Array.isArray(value)) {
16573
+ const findings = [];
16574
+ const out = [];
16575
+ for (let index = 0; index < value.length; index++) {
16576
+ const filtered = await applyOpenAIPrivacyFilter(
16577
+ value[index],
16578
+ vault,
16579
+ scope,
16580
+ config
16581
+ );
16582
+ out.push(filtered.value);
16583
+ findings.push(...filtered.findings.map((f) => ({
16584
+ ...f,
16585
+ path: `$[${index}]${f.path === "$" ? "" : f.path.slice(1)}`
16586
+ })));
16587
+ }
16588
+ return { value: out, findings };
16589
+ }
16590
+ if (value && typeof value === "object") {
16591
+ const findings = [];
16592
+ const out = {};
16593
+ for (const [key, child] of Object.entries(value)) {
16594
+ const filtered = await applyOpenAIPrivacyFilter(child, vault, scope, config);
16595
+ out[key] = filtered.value;
16596
+ findings.push(...filtered.findings.map((f) => ({
16597
+ ...f,
16598
+ path: `$.${key}${f.path === "$" ? "" : f.path.slice(1)}`
16599
+ })));
16600
+ }
16601
+ return { value: out, findings };
16602
+ }
16603
+ return { value, findings: [] };
16604
+ }
16605
+ var OPF_STDOUT_MAX_BYTES = 1e7;
16606
+ async function runOpenAIPrivacyFilter(text, config) {
16607
+ const stdout = await runCommand(config.command, text, config.timeout_ms);
16608
+ if (Buffer.byteLength(stdout, "utf8") > OPF_STDOUT_MAX_BYTES) {
16609
+ throw new PrivacyFilterRuntimeError(
16610
+ `OpenAI privacy-filter stdout exceeded ${OPF_STDOUT_MAX_BYTES}-byte cap`
16611
+ );
16612
+ }
16613
+ let parsed;
16614
+ try {
16615
+ parsed = JSON.parse(stdout);
16616
+ } catch {
16617
+ throw new PrivacyFilterRuntimeError("OpenAI privacy-filter returned invalid JSON");
16618
+ }
16619
+ if (!isOpenAIPrivacyFilterResult(parsed)) {
16620
+ throw new PrivacyFilterRuntimeError(
16621
+ "OpenAI privacy-filter JSON did not match the expected span schema"
16622
+ );
16623
+ }
16624
+ return parsed;
16625
+ }
16626
+ function runCommand(command, input, timeoutMs) {
16627
+ return new Promise((resolve4, reject) => {
16628
+ const child = child_process.spawn(command, [], {
16629
+ stdio: ["pipe", "pipe", "pipe"],
16630
+ shell: false
16631
+ });
16632
+ let stdout = "";
16633
+ let stderr = "";
16634
+ const timer = setTimeout(() => {
16635
+ child.kill("SIGTERM");
16636
+ reject(new PrivacyFilterRuntimeError("OpenAI privacy-filter timed out"));
16637
+ }, timeoutMs);
16638
+ child.stdout.setEncoding("utf8");
16639
+ child.stderr.setEncoding("utf8");
16640
+ child.stdout.on("data", (chunk) => {
16641
+ stdout += chunk;
16642
+ });
16643
+ child.stderr.on("data", (chunk) => {
16644
+ stderr += chunk;
16645
+ });
16646
+ child.on("error", (err) => {
16647
+ clearTimeout(timer);
16648
+ reject(new PrivacyFilterRuntimeError(`OpenAI privacy-filter failed to start: ${err.message}`));
16649
+ });
16650
+ child.on("close", (code) => {
16651
+ clearTimeout(timer);
16652
+ if (code !== 0) {
16653
+ reject(new PrivacyFilterRuntimeError(
16654
+ `OpenAI privacy-filter exited with code ${code}: ${stderr.trim()}`
16655
+ ));
16656
+ return;
16657
+ }
16658
+ resolve4(stdout);
16659
+ });
16660
+ child.stdin.end(input);
16661
+ });
16662
+ }
16663
+ function isOpenAIPrivacyFilterResult(value) {
16664
+ if (!value || typeof value !== "object") return false;
16665
+ const v = value;
16666
+ if (typeof v.text !== "string") return false;
16667
+ if (!Array.isArray(v.detected_spans)) return false;
16668
+ return v.detected_spans.every((span) => {
16669
+ if (!span || typeof span !== "object") return false;
16670
+ const s = span;
16671
+ return typeof s.label === "string" && typeof s.start === "number" && typeof s.end === "number" && typeof s.text === "string";
16672
+ });
16673
+ }
16674
+
16675
+ // src/l2-operational/context-gate-tools.ts
16676
+ function createContextGateTools(storage, masterKey, auditLog, options = {}) {
16677
+ const policyStore = new ContextGatePolicyStore(storage, masterKey);
16678
+ const privacyVault = new PrivacyPlaceholderVault(storage, masterKey);
16679
+ const privacyFilterConfig = options.privacyFilter ?? {
16680
+ mode: "local",
16681
+ fail_mode: "closed",
16682
+ command: "opf",
16683
+ timeout_ms: 5e3
16684
+ };
16685
+ const enforcerConfig = {
16686
+ enabled: false,
16687
+ // Off by default; agents must explicitly enable it
16688
+ bypass_prefixes: ["*"],
16689
+ // Skip all Sanctuary-internal tools; only proxy/ tools get filtered
16690
+ log_only: false,
15662
16691
  // Filter immediately
15663
16692
  on_deny: "block"
15664
16693
  // Block requests with denied fields
15665
16694
  };
15666
- const enforcer = new ContextGateEnforcer(policyStore, auditLog, enforcerConfig);
16695
+ const enforcer = new ContextGateEnforcer(
16696
+ policyStore,
16697
+ auditLog,
16698
+ enforcerConfig,
16699
+ privacyVault
16700
+ );
15667
16701
  const tools = [
15668
16702
  // ── Set Policy ──────────────────────────────────────────────────
15669
16703
  {
@@ -15957,6 +16991,34 @@ function createContextGateTools(storage, masterKey, auditLog) {
15957
16991
  break;
15958
16992
  }
15959
16993
  }
16994
+ let privacyFiltered;
16995
+ try {
16996
+ privacyFiltered = await applyConfiguredPrivacyFilter(
16997
+ safeContext,
16998
+ privacyVault,
16999
+ policyId,
17000
+ privacyFilterConfig
17001
+ );
17002
+ } catch (err) {
17003
+ if (err instanceof PrivacyFilterRuntimeError) {
17004
+ auditLog.append("l2", "context_gate_privacy_filter_failure", policy.identity_id ?? "system", {
17005
+ policy_id: policyId,
17006
+ provider,
17007
+ mode: privacyFilterConfig.mode,
17008
+ fail_mode: privacyFilterConfig.fail_mode,
17009
+ original_context_hash: result.original_context_hash,
17010
+ error: err.message
17011
+ }, "failure");
17012
+ return toolResult({
17013
+ blocked: true,
17014
+ error: "privacy_filter_failed",
17015
+ message: err.message,
17016
+ mode: privacyFilterConfig.mode,
17017
+ recommendation: privacyFilterConfig.fail_mode === "closed" ? "Install/configure the local privacy filter or switch SANCTUARY_PRIVACY_FILTER_FAIL_MODE to fallback." : "Check the local privacy filter configuration."
17018
+ });
17019
+ }
17020
+ throw err;
17021
+ }
15960
17022
  auditLog.append("l2", "context_gate_filter", policy.identity_id ?? "system", {
15961
17023
  policy_id: policyId,
15962
17024
  provider,
@@ -15965,20 +17027,32 @@ function createContextGateTools(storage, masterKey, auditLog) {
15965
17027
  fields_redacted: result.fields_redacted,
15966
17028
  fields_hashed: result.fields_hashed,
15967
17029
  fields_summarized: result.fields_summarized,
17030
+ privacy_findings: privacyFiltered.findings.length,
17031
+ privacy_classes: [...new Set(privacyFiltered.findings.map((f) => f.class))],
17032
+ privacy_filter_mode: privacyFiltered.mode,
17033
+ privacy_filter_configured_mode: privacyFilterConfig.mode,
17034
+ privacy_filter_fallback_from: privacyFiltered.fallback_from,
15968
17035
  original_context_hash: result.original_context_hash,
15969
17036
  filtered_context_hash: result.filtered_context_hash
15970
17037
  });
15971
17038
  return toolResult({
15972
17039
  blocked: false,
15973
- safe_context: safeContext,
17040
+ safe_context: privacyFiltered.value,
15974
17041
  summary: {
15975
17042
  total_fields: Object.keys(context).length,
15976
17043
  allowed: result.fields_allowed,
15977
17044
  redacted: result.fields_redacted,
15978
17045
  hashed: result.fields_hashed,
15979
- summarized: result.fields_summarized
17046
+ summarized: result.fields_summarized,
17047
+ privacy_filtered_spans: privacyFiltered.findings.length
15980
17048
  },
15981
17049
  decisions: result.decisions,
17050
+ privacy_filter: {
17051
+ mode: privacyFiltered.mode,
17052
+ configured_mode: privacyFilterConfig.mode,
17053
+ fallback_from: privacyFiltered.fallback_from,
17054
+ findings: privacyFiltered.findings
17055
+ },
15982
17056
  audit: {
15983
17057
  original_context_hash: result.original_context_hash,
15984
17058
  filtered_context_hash: result.filtered_context_hash,
@@ -17259,6 +18333,55 @@ var ProxyRouter = class {
17259
18333
  return toolResult(govResult.cached_result ?? {});
17260
18334
  }
17261
18335
  }
18336
+ let privacyPolicy = null;
18337
+ let privacyDestination = "tool-api";
18338
+ let outboundFiltered = false;
18339
+ if (this.options.privacyEnforcement) {
18340
+ const serverConfig = this.clientManager.getServerConfig(serverName);
18341
+ privacyDestination = serverConfig?.destination_category ?? "tool-api";
18342
+ const identityId = serverConfig?.privacy_identity_id;
18343
+ try {
18344
+ privacyPolicy = await this.options.privacyEnforcement.policyResolver(
18345
+ serverName,
18346
+ identityId
18347
+ );
18348
+ } catch {
18349
+ privacyPolicy = null;
18350
+ }
18351
+ const decision = await this.options.privacyEnforcement.engine.filterOutbound({
18352
+ payload: filteredArgs,
18353
+ policy: privacyPolicy,
18354
+ identity_id: identityId,
18355
+ agent_id: `proxy:${serverName}`,
18356
+ destination_category: privacyDestination,
18357
+ audit_log: this.auditLog
18358
+ });
18359
+ if (decision.status === "denied") {
18360
+ this.auditLog.append("l2", `proxy_privacy_denied:${proxyName}`, "system", {
18361
+ server: serverName,
18362
+ tool: toolName,
18363
+ tier,
18364
+ denial_reason_class: decision.audit_payload.denial_reason_class,
18365
+ latency_ms: Date.now() - start
18366
+ }, "failure");
18367
+ this.notifyProxyCall(
18368
+ proxyName,
18369
+ serverName,
18370
+ "blocked",
18371
+ "privacy_denied",
18372
+ tier
18373
+ );
18374
+ return toolResult({
18375
+ error: "Operation not permitted",
18376
+ proxy: true,
18377
+ privacy_denied: true
18378
+ });
18379
+ }
18380
+ if (decision.status === "filtered") {
18381
+ outboundFiltered = true;
18382
+ filteredArgs = decision.payload;
18383
+ }
18384
+ }
17262
18385
  const result = await this.callWithTimeout(
17263
18386
  serverName,
17264
18387
  toolName,
@@ -17277,6 +18400,26 @@ var ProxyRouter = class {
17277
18400
  latency_ms: latencyMs
17278
18401
  });
17279
18402
  this.notifyProxyCall(proxyName, serverName, "allowed", void 0, tier);
18403
+ if (outboundFiltered && this.options.privacyEnforcement && privacyPolicy) {
18404
+ const serverConfig = this.clientManager.getServerConfig(serverName);
18405
+ const identityId = serverConfig?.privacy_identity_id;
18406
+ const rehydrated = await this.options.privacyEnforcement.engine.rehydrateResponse({
18407
+ response: result,
18408
+ policy: privacyPolicy,
18409
+ identity_id: identityId,
18410
+ agent_id: `proxy:${serverName}`,
18411
+ destination_category: privacyDestination,
18412
+ audit_log: this.auditLog
18413
+ });
18414
+ if (rehydrated.status === "rehydrated") {
18415
+ return this.normalizeResponse(
18416
+ rehydrated.response
18417
+ );
18418
+ }
18419
+ return this.normalizeResponse(
18420
+ rehydrated.response
18421
+ );
18422
+ }
17280
18423
  return this.normalizeResponse(result);
17281
18424
  } catch (err) {
17282
18425
  const latencyMs = Date.now() - start;
@@ -17333,13 +18476,13 @@ var ProxyRouter = class {
17333
18476
  * Call an upstream tool with a timeout.
17334
18477
  */
17335
18478
  async callWithTimeout(serverName, toolName, args, timeoutMs) {
17336
- return new Promise((resolve2, reject) => {
18479
+ return new Promise((resolve4, reject) => {
17337
18480
  const timer = setTimeout(() => {
17338
18481
  reject(new Error(`Upstream tool call timed out after ${timeoutMs}ms`));
17339
18482
  }, timeoutMs);
17340
18483
  this.clientManager.callTool(serverName, toolName, args).then((result) => {
17341
18484
  clearTimeout(timer);
17342
- resolve2(result);
18485
+ resolve4(result);
17343
18486
  }).catch((err) => {
17344
18487
  clearTimeout(timer);
17345
18488
  reject(err);
@@ -17381,7 +18524,7 @@ var ProxyRouter = class {
17381
18524
  function strToBytes(s) {
17382
18525
  return new TextEncoder().encode(s);
17383
18526
  }
17384
- function bytesToHex(bytes) {
18527
+ function bytesToHex2(bytes) {
17385
18528
  let hex = "";
17386
18529
  for (let i = 0; i < bytes.length; i++) {
17387
18530
  hex += bytes[i].toString(16).padStart(2, "0");
@@ -17389,7 +18532,7 @@ function bytesToHex(bytes) {
17389
18532
  return hex;
17390
18533
  }
17391
18534
  function sha256Hex(input) {
17392
- return bytesToHex(sha256.sha256(strToBytes(input)));
18535
+ return bytesToHex2(sha256.sha256(strToBytes(input)));
17393
18536
  }
17394
18537
  var DEFAULT_CONFIG = {
17395
18538
  volume_limit: 200,
@@ -18103,18 +19246,18 @@ function createSanctuaryTools(opts) {
18103
19246
  const tagBytes = enc.encode(domainTag);
18104
19247
  const purposeBytes = enc.encode(purpose);
18105
19248
  const nonceBytes = enc.encode(nonce);
18106
- const sep = new Uint8Array([0]);
19249
+ const sep2 = new Uint8Array([0]);
18107
19250
  const message = new Uint8Array(
18108
19251
  tagBytes.length + 1 + purposeBytes.length + 1 + nonceBytes.length
18109
19252
  );
18110
19253
  let offset = 0;
18111
19254
  message.set(tagBytes, offset);
18112
19255
  offset += tagBytes.length;
18113
- message.set(sep, offset);
19256
+ message.set(sep2, offset);
18114
19257
  offset += 1;
18115
19258
  message.set(purposeBytes, offset);
18116
19259
  offset += purposeBytes.length;
18117
- message.set(sep, offset);
19260
+ message.set(sep2, offset);
18118
19261
  offset += 1;
18119
19262
  message.set(nonceBytes, offset);
18120
19263
  let sigB64;
@@ -20414,7 +21557,7 @@ function classifyAgentDescription(description) {
20414
21557
  }
20415
21558
 
20416
21559
  // src/compliance/eu_ai_act/generator.ts
20417
- function bytesToHex2(bytes) {
21560
+ function bytesToHex3(bytes) {
20418
21561
  let hex = "";
20419
21562
  for (let i = 0; i < bytes.length; i++) {
20420
21563
  hex += bytes[i].toString(16).padStart(2, "0");
@@ -20578,18 +21721,18 @@ function makeSigner(signer, masterKey) {
20578
21721
  const sig = sign(digest, signer.encrypted_private_key, encryptionKey);
20579
21722
  return toBase64url(sig);
20580
21723
  },
20581
- sha256Hex: (content) => bytesToHex2(hash(stringToBytes(content)))
21724
+ sha256Hex: (content) => bytesToHex3(hash(stringToBytes(content)))
20582
21725
  };
20583
21726
  }
20584
21727
  function finaliseDocument(template, context, filename, ds) {
20585
21728
  const content = render(template, context);
20586
- const sha256Hex2 = ds.sha256Hex(content);
21729
+ const sha256Hex4 = ds.sha256Hex(content);
20587
21730
  const signature = ds.signContent(content);
20588
21731
  return {
20589
21732
  filename,
20590
21733
  content,
20591
21734
  content_type: "text/markdown",
20592
- sha256: sha256Hex2,
21735
+ sha256: sha256Hex4,
20593
21736
  signature
20594
21737
  };
20595
21738
  }
@@ -21387,102 +22530,1566 @@ var MemoryStorage = class {
21387
22530
  }
21388
22531
  };
21389
22532
 
21390
- // src/dashboard/aggregator.ts
21391
- var L4_DEGRADATION_IMPACT = {
21392
- critical: 40,
21393
- warning: 25,
21394
- info: 10
22533
+ // src/contracts/v1.1/constants.ts
22534
+ var SIGNATURE_SCHEME_V1 = "ed25519-v1";
22535
+ var EXIT_BUNDLE_MANIFEST_VERSION = "SANCTUARY_EXIT_BUNDLE_V1";
22536
+ var EXIT_BUNDLE_ARTIFACT_KINDS = [
22537
+ "public_identity",
22538
+ "encrypted_state",
22539
+ "policy_set",
22540
+ "audit_receipts",
22541
+ "reputation_bundle",
22542
+ "commitments",
22543
+ "placeholder_vault_metadata"
22544
+ ];
22545
+
22546
+ // src/mesh/errors.ts
22547
+ var MeshError = class extends Error {
22548
+ constructor(message) {
22549
+ super(message);
22550
+ this.name = "MeshError";
22551
+ }
21395
22552
  };
21396
- function computeL4LayerScore(degradations, status) {
21397
- if (status === "compromised") return 0;
21398
- let score = 100;
21399
- for (const deg of degradations) {
21400
- score -= L4_DEGRADATION_IMPACT[deg.severity] ?? 10;
22553
+ var MeshEnvelopeError = class extends MeshError {
22554
+ constructor(message) {
22555
+ super(message);
22556
+ this.name = "MeshEnvelopeError";
21401
22557
  }
21402
- score = Math.max(0, score);
21403
- if (degradations.length === 0 && score > 50) {
21404
- score = Math.min(100, score + 5);
22558
+ };
22559
+ var MeshReservedExtensionKeyError = class extends MeshEnvelopeError {
22560
+ constructor(key) {
22561
+ super(
22562
+ `v0.1 emitters MUST NOT populate reserved extension_envelope key: ${key}`
22563
+ );
22564
+ this.name = "MeshReservedExtensionKeyError";
21405
22565
  }
21406
- return Math.round(score);
22566
+ };
22567
+ var MeshReservedEventTypeError = class extends MeshEnvelopeError {
22568
+ constructor(eventType) {
22569
+ super(
22570
+ `v0.1 emitters MUST NOT emit reserved-namespace event_type: ${eventType}`
22571
+ );
22572
+ this.name = "MeshReservedEventTypeError";
22573
+ }
22574
+ };
22575
+
22576
+ // src/mesh/canonical-json.ts
22577
+ var MeshCanonicalJsonError = class extends MeshError {
22578
+ constructor(message) {
22579
+ super(message);
22580
+ this.name = "MeshCanonicalJsonError";
22581
+ }
22582
+ };
22583
+ function canonicalize2(value) {
22584
+ if (value === void 0) {
22585
+ throw new MeshCanonicalJsonError(
22586
+ "canonicalize(): top-level undefined is not serializable"
22587
+ );
22588
+ }
22589
+ return encode(value);
21407
22590
  }
21408
- var MAX_ACTIVITY = 50;
21409
- var MAX_AUDIT = 50;
21410
- function fingerprintDID(did) {
21411
- const raw = did.replace(/^did:[a-z0-9]+:/i, "");
21412
- if (raw.length <= 12) return raw;
21413
- return `${raw.slice(0, 6)}\u2026${raw.slice(-6)}`;
22591
+ function encode(value) {
22592
+ if (value === null) return "null";
22593
+ if (typeof value === "boolean") return value ? "true" : "false";
22594
+ if (typeof value === "number") {
22595
+ if (!Number.isFinite(value)) {
22596
+ throw new MeshCanonicalJsonError(
22597
+ `canonicalize(): non-finite number (${String(value)}) is not serializable`
22598
+ );
22599
+ }
22600
+ return JSON.stringify(value);
22601
+ }
22602
+ if (typeof value === "string") return JSON.stringify(value);
22603
+ if (Array.isArray(value)) return encodeArray(value);
22604
+ if (typeof value === "object") return encodeObject(value);
22605
+ throw new MeshCanonicalJsonError(
22606
+ `canonicalize(): unsupported type ${typeof value}`
22607
+ );
21414
22608
  }
21415
- function countInjectionsToday(audit) {
21416
- const startOfDay = /* @__PURE__ */ new Date();
21417
- startOfDay.setHours(0, 0, 0, 0);
21418
- const cutoff = startOfDay.getTime();
21419
- return audit.filter((e) => {
21420
- const ts = new Date(e.timestamp).getTime();
21421
- if (isNaN(ts) || ts < cutoff) return false;
21422
- const op = (e.operation ?? "").toLowerCase();
21423
- return op.includes("injection") || op.includes("blocked");
21424
- }).length;
22609
+ function encodeArray(arr) {
22610
+ const parts = [];
22611
+ for (const item of arr) {
22612
+ parts.push(item === void 0 ? "null" : encode(item));
22613
+ }
22614
+ return "[" + parts.join(",") + "]";
21425
22615
  }
21426
- var PROOF_CREATION_OPS = /* @__PURE__ */ new Set([
21427
- "zk_prove",
21428
- "zk_range_prove",
21429
- "proof_commitment"
22616
+ function encodeObject(obj) {
22617
+ const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
22618
+ const parts = [];
22619
+ for (const k of keys) {
22620
+ parts.push(JSON.stringify(k) + ":" + encode(obj[k]));
22621
+ }
22622
+ return "{" + parts.join(",") + "}";
22623
+ }
22624
+ function canonicalizeToBytes(value) {
22625
+ return new TextEncoder().encode(canonicalize2(value));
22626
+ }
22627
+
22628
+ // src/exit/bundle.ts
22629
+ init_hashing();
22630
+ init_encoding();
22631
+ init_encryption();
22632
+ init_identity();
22633
+
22634
+ // src/contracts/v1.1/exit-bundle-manifest.ts
22635
+ var EXIT_BUNDLE_PATH_PATTERN = /^[a-z0-9._-]+(?:\/[a-z0-9._-]+)*$/;
22636
+ var EXIT_BUNDLE_PATH_MAX_BYTES = 256;
22637
+
22638
+ // src/exit/verifier.ts
22639
+ init_encoding();
22640
+ init_hashing();
22641
+ var PRIVATE_MATERIAL_KEYS = /* @__PURE__ */ new Set([
22642
+ "private_key",
22643
+ "privatekey",
22644
+ "encrypted_private_key",
22645
+ "encryptedprivatekey",
22646
+ "passphrase",
22647
+ "recovery_key",
22648
+ "recoverykey",
22649
+ "seed",
22650
+ "mnemonic"
21430
22651
  ]);
21431
- function countProofsToday(audit) {
21432
- const startOfDay = /* @__PURE__ */ new Date();
21433
- startOfDay.setHours(0, 0, 0, 0);
21434
- const cutoff = startOfDay.getTime();
21435
- return audit.filter((e) => {
21436
- if (e.layer !== "l3") return false;
21437
- if (!PROOF_CREATION_OPS.has(e.operation)) return false;
21438
- const ts = new Date(e.timestamp).getTime();
21439
- return !isNaN(ts) && ts >= cutoff;
21440
- }).length;
22652
+ function sha256Hex2(bytes) {
22653
+ return Array.from(hash(bytes)).map((b) => b.toString(16).padStart(2, "0")).join("");
21441
22654
  }
21442
- function buildAgent(sources) {
21443
- if (!sources.identityManager) {
21444
- return {
21445
- display_name: "Unclaimed agent",
21446
- did: null,
21447
- did_fingerprint: null,
21448
- identity_count: 0,
21449
- primary_identity_id: null
21450
- };
21451
- }
21452
- const primary = sources.identityManager.getDefault();
21453
- const identities = sources.identityManager.list();
21454
- if (!primary) {
21455
- return {
21456
- display_name: "Unclaimed agent",
21457
- did: null,
21458
- did_fingerprint: null,
21459
- identity_count: identities.length,
21460
- primary_identity_id: null
21461
- };
21462
- }
22655
+ function resultBase(bundleDir, manifest, warnings = [], unsupported = []) {
22656
+ const body = manifest?.body;
21463
22657
  return {
21464
- display_name: primary.label || "Sovereign agent",
21465
- did: primary.did,
21466
- did_fingerprint: fingerprintDID(primary.did),
21467
- identity_count: identities.length,
21468
- primary_identity_id: primary.identity_id
22658
+ version: "1.1",
22659
+ passed: false,
22660
+ verified_at: (/* @__PURE__ */ new Date()).toISOString(),
22661
+ manifest_path: path.join(bundleDir, "manifest.json"),
22662
+ manifest_hash: null,
22663
+ manifest_summary: {
22664
+ manifest_version: body?.manifest_version ?? EXIT_BUNDLE_MANIFEST_VERSION,
22665
+ fortress_id: body?.identity_binding?.fortress_id ?? "",
22666
+ identity_id: body?.identity_binding?.identity_id ?? "",
22667
+ exported_at: body?.exported_at ?? "",
22668
+ artifact_count: body?.artifacts?.length ?? 0
22669
+ },
22670
+ artifact_results: body?.artifacts?.map((artifact) => ({
22671
+ path: artifact.path,
22672
+ kind: artifact.kind,
22673
+ hash_passed: false,
22674
+ size_passed: false
22675
+ })) ?? [],
22676
+ warnings,
22677
+ unsupported_artifacts: unsupported
21469
22678
  };
21470
22679
  }
21471
- function buildL1(sources, audit) {
21472
- const hasIdentity = !!sources.identityManager?.getDefault();
21473
- const state = hasIdentity ? "full" : "degraded";
22680
+ function fail(bundleDir, manifest, failureClass, warnings = [], unsupported = []) {
21474
22681
  return {
21475
- label: "L1 Cognitive",
21476
- state,
21477
- headline: hasIdentity ? "State encrypted at rest" : "No sovereign identity \u2014 run sanctuary_bootstrap",
21478
- encryption: "AES-256-GCM + HKDF per namespace",
21479
- injection_blocked_today: countInjectionsToday(audit),
21480
- memory_attest_ready: hasIdentity
22682
+ ...resultBase(bundleDir, manifest, warnings, unsupported),
22683
+ failure_class: failureClass
21481
22684
  };
21482
22685
  }
21483
- function buildL2(sources) {
21484
- const teeAvailable = sources.teeAvailable ?? false;
21485
- const state = teeAvailable ? "full" : "degraded";
22686
+ function isKnownKind(kind) {
22687
+ return EXIT_BUNDLE_ARTIFACT_KINDS.includes(kind);
22688
+ }
22689
+ function validateArtifactPath(path) {
22690
+ if (Buffer.byteLength(path, "utf8") > EXIT_BUNDLE_PATH_MAX_BYTES) {
22691
+ return "unsafe";
22692
+ }
22693
+ if (!EXIT_BUNDLE_PATH_PATTERN.test(path)) return "unsafe";
22694
+ if (path.startsWith("/") || path.includes("\\") || path.includes("\0")) {
22695
+ return "unsafe";
22696
+ }
22697
+ const normalized = decodeURIComponentSafe(path).normalize("NFKC");
22698
+ if (normalized.split("/").some((segment) => segment === "." || segment === "..")) {
22699
+ return "unsafe";
22700
+ }
22701
+ return "ok";
22702
+ }
22703
+ function decodeURIComponentSafe(value) {
22704
+ let current = value;
22705
+ for (let i = 0; i < 2; i++) {
22706
+ try {
22707
+ const decoded = decodeURIComponent(current);
22708
+ if (decoded === current) return decoded;
22709
+ current = decoded;
22710
+ } catch {
22711
+ return current;
22712
+ }
22713
+ }
22714
+ return current;
22715
+ }
22716
+ async function assertDescendant(root, candidate) {
22717
+ const rootReal = await promises.realpath(root);
22718
+ const candidateDir = await promises.realpath(path.dirname(candidate));
22719
+ const rootWithSep = rootReal.endsWith(path.sep) ? rootReal : rootReal + path.sep;
22720
+ return candidateDir === rootReal || candidateDir.startsWith(rootWithSep);
22721
+ }
22722
+ function findPrivateMaterial(value, path = "$") {
22723
+ if (value === null || typeof value !== "object") return [];
22724
+ if (Array.isArray(value)) {
22725
+ return value.flatMap(
22726
+ (item, index) => findPrivateMaterial(item, `${path}[${index}]`)
22727
+ );
22728
+ }
22729
+ const findings = [];
22730
+ for (const [key, child] of Object.entries(value)) {
22731
+ const normalized = key.toLowerCase().replace(/[-\s]/g, "_");
22732
+ if (PRIVATE_MATERIAL_KEYS.has(normalized)) {
22733
+ findings.push(`${path}.${key}`);
22734
+ continue;
22735
+ }
22736
+ findings.push(...findPrivateMaterial(child, `${path}.${key}`));
22737
+ }
22738
+ return findings;
22739
+ }
22740
+ async function readManifest(bundleDir) {
22741
+ const bytes = await promises.readFile(path.join(bundleDir, "manifest.json"));
22742
+ return JSON.parse(Buffer.from(bytes).toString("utf8"));
22743
+ }
22744
+ async function loadExitArtifact(bundleDir, manifest, kind) {
22745
+ const entry = manifest.body.artifacts.find((artifact) => artifact.kind === kind);
22746
+ if (!entry) return null;
22747
+ const artifactPath = path.join(bundleDir, entry.path);
22748
+ const bytes = await promises.readFile(artifactPath);
22749
+ return {
22750
+ entry,
22751
+ path: artifactPath,
22752
+ bytes: new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength),
22753
+ json: JSON.parse(Buffer.from(bytes).toString("utf8"))
22754
+ };
22755
+ }
22756
+ function verifyIdentityArtifact(identityArtifact) {
22757
+ const wrapper = identityArtifact;
22758
+ const bundle = wrapper.bundle;
22759
+ if (!bundle || typeof wrapper.signature !== "string") {
22760
+ return { signature_valid: false };
22761
+ }
22762
+ const publicKey = bundle.publicKey;
22763
+ if (typeof publicKey !== "string") {
22764
+ return { signature_valid: false };
22765
+ }
22766
+ const signatureValid = ed25519.ed25519.verify(
22767
+ fromBase64url(wrapper.signature),
22768
+ canonicalizeToBytes(bundle),
22769
+ fromBase64url(publicKey)
22770
+ );
22771
+ return {
22772
+ signature_valid: signatureValid,
22773
+ identity_id: typeof bundle.identity_id === "string" ? bundle.identity_id : void 0,
22774
+ did: typeof bundle.did === "string" ? bundle.did : void 0,
22775
+ public_key: publicKey
22776
+ };
22777
+ }
22778
+ function verifyReputationArtifact(reputationArtifact, publicKeysByDid) {
22779
+ const bundle = reputationArtifact;
22780
+ const attestations = Array.isArray(bundle.attestations) ? bundle.attestations : [];
22781
+ let bundleSignatureValid = "unverifiable";
22782
+ if (bundle.version === "SANCTUARY_REP_V1" && typeof bundle.exporter_did === "string" && typeof bundle.bundle_signature === "string") {
22783
+ const exporterKey = publicKeysByDid.get(bundle.exporter_did);
22784
+ if (exporterKey) {
22785
+ const signedBody = {
22786
+ version: "SANCTUARY_REP_V1",
22787
+ attestations,
22788
+ exported_at: bundle.exported_at,
22789
+ exporter_did: bundle.exporter_did
22790
+ };
22791
+ bundleSignatureValid = ed25519.ed25519.verify(
22792
+ fromBase64url(bundle.bundle_signature),
22793
+ stringToBytes(JSON.stringify(signedBody)),
22794
+ exporterKey
22795
+ );
22796
+ }
22797
+ }
22798
+ let verified = 0;
22799
+ let invalid = 0;
22800
+ let unverifiable = 0;
22801
+ for (const attestation of attestations) {
22802
+ const signerKey = publicKeysByDid.get(attestation.signer);
22803
+ if (!signerKey) {
22804
+ unverifiable++;
22805
+ continue;
22806
+ }
22807
+ const ok = ed25519.ed25519.verify(
22808
+ fromBase64url(attestation.signature),
22809
+ stringToBytes(JSON.stringify(attestation.data)),
22810
+ signerKey
22811
+ );
22812
+ if (ok) verified++;
22813
+ else invalid++;
22814
+ }
22815
+ return {
22816
+ bundle_signature_valid: bundleSignatureValid,
22817
+ attestation_count: attestations.length,
22818
+ verified_attestations: verified,
22819
+ invalid_attestations: invalid,
22820
+ unverifiable_attestations: unverifiable
22821
+ };
22822
+ }
22823
+ async function verifyExitBundle(bundleDir) {
22824
+ const root = path.resolve(bundleDir);
22825
+ let manifest;
22826
+ let manifestBytes;
22827
+ try {
22828
+ const raw = await promises.readFile(path.join(root, "manifest.json"));
22829
+ manifestBytes = new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
22830
+ manifest = JSON.parse(Buffer.from(raw).toString("utf8"));
22831
+ } catch {
22832
+ return fail(root, null, "other", ["manifest.json is missing or unreadable"]);
22833
+ }
22834
+ const warnings = [];
22835
+ const unsupportedArtifacts = [];
22836
+ const body = manifest.body;
22837
+ if (!body || body.manifest_version !== EXIT_BUNDLE_MANIFEST_VERSION) {
22838
+ return fail(root, manifest, "manifest_unknown_version", warnings, unsupportedArtifacts);
22839
+ }
22840
+ if (body.signature_scheme !== SIGNATURE_SCHEME_V1) {
22841
+ return fail(
22842
+ root,
22843
+ manifest,
22844
+ "manifest_signature_scheme_invalid",
22845
+ warnings,
22846
+ unsupportedArtifacts
22847
+ );
22848
+ }
22849
+ const seenPaths = /* @__PURE__ */ new Set();
22850
+ for (const artifact of body.artifacts) {
22851
+ if (!isKnownKind(artifact.kind)) {
22852
+ return fail(root, manifest, "other", [`unknown artifact kind: ${artifact.kind}`]);
22853
+ }
22854
+ if (seenPaths.has(artifact.path)) {
22855
+ return fail(root, manifest, "artifact_path_duplicate", warnings, unsupportedArtifacts);
22856
+ }
22857
+ seenPaths.add(artifact.path);
22858
+ if (validateArtifactPath(artifact.path) !== "ok") {
22859
+ return fail(root, manifest, "artifact_path_unsafe", warnings, unsupportedArtifacts);
22860
+ }
22861
+ }
22862
+ const signatureOk = ed25519.ed25519.verify(
22863
+ fromBase64url(manifest.signature),
22864
+ canonicalizeToBytes(body),
22865
+ fromBase64url(body.identity_binding.fortress_master_pubkey)
22866
+ );
22867
+ if (!signatureOk) {
22868
+ return fail(root, manifest, "manifest_signature_invalid", warnings, unsupportedArtifacts);
22869
+ }
22870
+ const expectedAggregate = sha256Hex2(
22871
+ stringToBytes(canonicalize2(body.artifacts))
22872
+ );
22873
+ if (expectedAggregate !== body.artifacts_aggregate_hash) {
22874
+ return fail(root, manifest, "aggregate_hash_mismatch", warnings, unsupportedArtifacts);
22875
+ }
22876
+ const artifactResults = [];
22877
+ let artifactFailure = null;
22878
+ for (const artifact of body.artifacts) {
22879
+ const artifactPath = path.join(root, artifact.path);
22880
+ let bytes;
22881
+ let fileSize = -1;
22882
+ try {
22883
+ const linkStat = await promises.lstat(artifactPath);
22884
+ if (linkStat.isSymbolicLink()) {
22885
+ artifactFailure = "archive_contains_symlink";
22886
+ artifactResults.push({
22887
+ path: artifact.path,
22888
+ kind: artifact.kind,
22889
+ hash_passed: false,
22890
+ size_passed: false
22891
+ });
22892
+ continue;
22893
+ }
22894
+ const descends = await assertDescendant(root, artifactPath);
22895
+ if (!descends) {
22896
+ artifactFailure = "artifact_path_escapes_root";
22897
+ artifactResults.push({
22898
+ path: artifact.path,
22899
+ kind: artifact.kind,
22900
+ hash_passed: false,
22901
+ size_passed: false
22902
+ });
22903
+ continue;
22904
+ }
22905
+ const fileStat = await promises.stat(artifactPath);
22906
+ fileSize = fileStat.size;
22907
+ const raw = await promises.readFile(artifactPath);
22908
+ bytes = new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
22909
+ } catch {
22910
+ artifactFailure = "artifact_missing";
22911
+ artifactResults.push({
22912
+ path: artifact.path,
22913
+ kind: artifact.kind,
22914
+ hash_passed: false,
22915
+ size_passed: false
22916
+ });
22917
+ continue;
22918
+ }
22919
+ const hashPassed = sha256Hex2(bytes) === artifact.hash;
22920
+ const sizePassed = fileSize === artifact.size_bytes;
22921
+ if (!hashPassed && !artifactFailure) artifactFailure = "artifact_hash_mismatch";
22922
+ if (!sizePassed && !artifactFailure) artifactFailure = "artifact_size_mismatch";
22923
+ artifactResults.push({
22924
+ path: artifact.path,
22925
+ kind: artifact.kind,
22926
+ hash_passed: hashPassed,
22927
+ size_passed: sizePassed
22928
+ });
22929
+ try {
22930
+ const parsed = JSON.parse(Buffer.from(bytes).toString("utf8"));
22931
+ const privateFindings = findPrivateMaterial(parsed);
22932
+ if (privateFindings.length > 0) {
22933
+ artifactFailure = "private_material_present";
22934
+ warnings.push(
22935
+ `${artifact.path} contains private-material field(s): ${privateFindings.join(", ")}`
22936
+ );
22937
+ }
22938
+ } catch {
22939
+ warnings.push(`${artifact.path} is not parseable JSON`);
22940
+ }
22941
+ }
22942
+ if (artifactFailure) {
22943
+ return {
22944
+ ...fail(root, manifest, artifactFailure, warnings, unsupportedArtifacts),
22945
+ manifest_hash: sha256Hex2(manifestBytes),
22946
+ artifact_results: artifactResults
22947
+ };
22948
+ }
22949
+ const publicKeysByDid = /* @__PURE__ */ new Map();
22950
+ const identityArtifact = await loadExitArtifact(root, manifest, "public_identity");
22951
+ let identity;
22952
+ if (identityArtifact) {
22953
+ const identityVerification = verifyIdentityArtifact(identityArtifact.json);
22954
+ identity = {
22955
+ signature_valid: identityVerification.signature_valid,
22956
+ identity_id: identityVerification.identity_id,
22957
+ did: identityVerification.did
22958
+ };
22959
+ if (identityVerification.did && identityVerification.public_key) {
22960
+ publicKeysByDid.set(
22961
+ identityVerification.did,
22962
+ fromBase64url(identityVerification.public_key)
22963
+ );
22964
+ }
22965
+ if (!identityVerification.signature_valid) {
22966
+ warnings.push("public identity artifact signature is invalid");
22967
+ }
22968
+ }
22969
+ const auditArtifact = await loadExitArtifact(root, manifest, "audit_receipts");
22970
+ const audit = auditArtifact ? {
22971
+ receipt_count: Array.isArray(auditArtifact.json.entries) ? auditArtifact.json.entries.length : 0,
22972
+ individual_signatures_verified: false
22973
+ } : void 0;
22974
+ if (auditArtifact) {
22975
+ unsupportedArtifacts.push(
22976
+ "audit_receipts: individual audit entries are not signed in the legacy L2 audit log; verifier pins them by signed manifest hash"
22977
+ );
22978
+ }
22979
+ const reputationArtifact = await loadExitArtifact(root, manifest, "reputation_bundle");
22980
+ const reputation = reputationArtifact ? verifyReputationArtifact(reputationArtifact.json, publicKeysByDid) : void 0;
22981
+ if (reputation) {
22982
+ if (reputation.bundle_signature_valid === "unverifiable") {
22983
+ warnings.push("reputation bundle signature is unverifiable from included public identities");
22984
+ } else if (!reputation.bundle_signature_valid) {
22985
+ warnings.push("reputation bundle signature is invalid");
22986
+ }
22987
+ if (reputation.unverifiable_attestations > 0) {
22988
+ warnings.push(
22989
+ `${reputation.unverifiable_attestations} reputation attestation(s) have unknown signer public keys`
22990
+ );
22991
+ }
22992
+ if (reputation.invalid_attestations > 0) {
22993
+ warnings.push(
22994
+ `${reputation.invalid_attestations} reputation attestation(s) failed signature verification`
22995
+ );
22996
+ }
22997
+ }
22998
+ const reputationFailed = reputation?.bundle_signature_valid === false || (reputation?.invalid_attestations ?? 0) > 0;
22999
+ const identityFailed = identity ? !identity.signature_valid : false;
23000
+ return {
23001
+ version: "1.1",
23002
+ passed: !reputationFailed && !identityFailed,
23003
+ verified_at: (/* @__PURE__ */ new Date()).toISOString(),
23004
+ manifest_path: path.join(root, "manifest.json"),
23005
+ manifest_hash: sha256Hex2(manifestBytes),
23006
+ manifest_summary: {
23007
+ manifest_version: body.manifest_version,
23008
+ fortress_id: body.identity_binding.fortress_id,
23009
+ identity_id: body.identity_binding.identity_id,
23010
+ exported_at: body.exported_at,
23011
+ artifact_count: body.artifacts.length
23012
+ },
23013
+ artifact_results: artifactResults,
23014
+ warnings,
23015
+ unsupported_artifacts: unsupportedArtifacts,
23016
+ identity,
23017
+ audit,
23018
+ reputation,
23019
+ failure_class: reputationFailed || identityFailed ? "other" : void 0
23020
+ };
23021
+ }
23022
+
23023
+ // src/exit/bundle.ts
23024
+ var ARTIFACT_DIR = "artifacts";
23025
+ var EXIT_IMPORT_NAMESPACE = "_exit_imports";
23026
+ var EXIT_PUBLIC_IDENTITIES_NAMESPACE = "_exit_public_identities";
23027
+ var EXIT_AUDIT_RECEIPTS_NAMESPACE = "_exit_audit_receipts";
23028
+ var EXIT_POLICY_SETS_NAMESPACE = "_exit_policy_sets";
23029
+ var EXIT_COMMITMENTS_NAMESPACE = "_exit_commitments";
23030
+ var EXIT_PLACEHOLDER_METADATA_NAMESPACE = "_exit_placeholder_metadata";
23031
+ var PRIVACY_PLACEHOLDER_NAMESPACE = "_privacy_placeholder_vault";
23032
+ function sha256Hex3(bytes) {
23033
+ return Array.from(hash(bytes)).map((b) => b.toString(16).padStart(2, "0")).join("");
23034
+ }
23035
+ function jsonBytes(value) {
23036
+ return stringToBytes(JSON.stringify(value, null, 2) + "\n");
23037
+ }
23038
+ async function writeJsonArtifact(bundleDir, path$1, value, kind) {
23039
+ const bytes = jsonBytes(value);
23040
+ const fullPath = path.join(bundleDir, path$1);
23041
+ await promises.mkdir(path.join(bundleDir, ARTIFACT_DIR), { recursive: true, mode: 448 });
23042
+ await promises.writeFile(fullPath, bytes, { mode: 384 });
23043
+ return {
23044
+ kind,
23045
+ path: path$1,
23046
+ hash_alg: "sha256",
23047
+ hash: sha256Hex3(bytes),
23048
+ size_bytes: bytes.length
23049
+ };
23050
+ }
23051
+ async function readSourceKeyParams(storage) {
23052
+ const raw = await storage.read("_meta", "key-params");
23053
+ if (!raw) return void 0;
23054
+ return JSON.parse(bytesToString(raw));
23055
+ }
23056
+ async function discoverFilesystemStateNamespaces(stateStoragePath) {
23057
+ if (!stateStoragePath) return [];
23058
+ try {
23059
+ const names = await promises.readdir(stateStoragePath);
23060
+ const namespaces = [];
23061
+ for (const name of names) {
23062
+ const full = path.join(stateStoragePath, name);
23063
+ const entryStat = await promises.stat(full);
23064
+ if (!entryStat.isDirectory()) continue;
23065
+ if (name.startsWith("_")) continue;
23066
+ namespaces.push(name);
23067
+ }
23068
+ return namespaces.sort();
23069
+ } catch {
23070
+ return [];
23071
+ }
23072
+ }
23073
+ async function exportEncryptedState(opts) {
23074
+ const namespaceSet = new Set(
23075
+ opts.stateNamespaces ?? await discoverFilesystemStateNamespaces(opts.stateStoragePath)
23076
+ );
23077
+ const entries = [];
23078
+ for (const namespace of [...namespaceSet].sort()) {
23079
+ if (isReservedNamespace(namespace)) continue;
23080
+ const metas = await opts.storage.list(namespace);
23081
+ for (const meta of metas) {
23082
+ const raw = await opts.storage.read(namespace, meta.key);
23083
+ if (!raw) continue;
23084
+ try {
23085
+ entries.push({
23086
+ namespace,
23087
+ key: meta.key,
23088
+ entry: JSON.parse(bytesToString(raw))
23089
+ });
23090
+ } catch {
23091
+ }
23092
+ }
23093
+ }
23094
+ return {
23095
+ format: "SANCTUARY_EXIT_ENCRYPTED_STATE_V1",
23096
+ exported_at: (/* @__PURE__ */ new Date()).toISOString(),
23097
+ key_source: opts.keySource ?? "unknown",
23098
+ source_key_derivation: await readSourceKeyParams(opts.storage),
23099
+ namespaces: [...new Set(entries.map((entry) => entry.namespace))].sort(),
23100
+ total_keys: entries.length,
23101
+ contains_reserved_namespaces: false,
23102
+ entries
23103
+ };
23104
+ }
23105
+ function exportPublicIdentity(identity, masterKey) {
23106
+ const body = {
23107
+ format: "SANCTUARY_IDENTITY_BUNDLE_V1",
23108
+ publicKey: identity.public_key,
23109
+ did: identity.did,
23110
+ identity_id: identity.identity_id,
23111
+ label: identity.label,
23112
+ key_type: identity.key_type,
23113
+ key_protection: identity.key_protection,
23114
+ rotation_history: identity.rotation_history ?? [],
23115
+ exported_at: (/* @__PURE__ */ new Date()).toISOString()
23116
+ };
23117
+ const signature = sign(
23118
+ canonicalizeToBytes(body),
23119
+ identity.encrypted_private_key,
23120
+ derivePurposeKey(masterKey, "identity-encryption")
23121
+ );
23122
+ return {
23123
+ bundle: body,
23124
+ signature: toBase64url(signature),
23125
+ signed_by: identity.did
23126
+ };
23127
+ }
23128
+ function redactedPolicy(policy) {
23129
+ return {
23130
+ ...policy,
23131
+ approval_channel: {
23132
+ type: policy.approval_channel.type,
23133
+ timeout_seconds: policy.approval_channel.timeout_seconds,
23134
+ webhook_url: policy.approval_channel.webhook_url
23135
+ }
23136
+ };
23137
+ }
23138
+ function exportPolicySet(policy, config) {
23139
+ const cfg = config ?? defaultConfig();
23140
+ return {
23141
+ format: "SANCTUARY_EXIT_POLICY_SET_V1",
23142
+ exported_at: (/* @__PURE__ */ new Date()).toISOString(),
23143
+ principal_policy: redactedPolicy(policy),
23144
+ config_summary: {
23145
+ version: cfg.version,
23146
+ state: cfg.state,
23147
+ execution: cfg.execution,
23148
+ disclosure: cfg.disclosure,
23149
+ reputation: cfg.reputation,
23150
+ privacy_filter: cfg.privacy_filter
23151
+ }
23152
+ };
23153
+ }
23154
+ async function exportAuditReceipts(auditLog) {
23155
+ await auditLog.flush();
23156
+ const result = await auditLog.query({ limit: 1e5 });
23157
+ return {
23158
+ format: "SANCTUARY_AUDIT_RECEIPTS_V1",
23159
+ exported_at: (/* @__PURE__ */ new Date()).toISOString(),
23160
+ total: result.total,
23161
+ individual_entry_signatures: false,
23162
+ entries: result.entries
23163
+ };
23164
+ }
23165
+ async function exportCommitments(storage, masterKey) {
23166
+ const encryptionKey = derivePurposeKey(masterKey, "l3-commitments");
23167
+ const publicCommitments = [];
23168
+ let unreadable = 0;
23169
+ for (const meta of await storage.list("_commitments")) {
23170
+ const raw = await storage.read("_commitments", meta.key);
23171
+ if (!raw) continue;
23172
+ try {
23173
+ const encrypted = JSON.parse(bytesToString(raw));
23174
+ const decrypted = decrypt(encrypted, encryptionKey);
23175
+ const parsed = JSON.parse(bytesToString(decrypted));
23176
+ publicCommitments.push({
23177
+ commitment_id: meta.key,
23178
+ commitment: parsed.commitment,
23179
+ committed_at: parsed.committed_at,
23180
+ revealed: parsed.revealed,
23181
+ revealed_at: parsed.revealed_at
23182
+ });
23183
+ } catch {
23184
+ unreadable++;
23185
+ }
23186
+ }
23187
+ return {
23188
+ format: "SANCTUARY_EXIT_COMMITMENTS_V1",
23189
+ exported_at: (/* @__PURE__ */ new Date()).toISOString(),
23190
+ public_commitments: publicCommitments,
23191
+ unreadable_count: unreadable,
23192
+ redacted_fields: ["value", "blinding_factor"]
23193
+ };
23194
+ }
23195
+ async function exportPlaceholderVaultMetadata(storage, masterKey) {
23196
+ const encryptionKey = derivePurposeKey(masterKey, "l2-privacy-placeholders");
23197
+ const entries = [];
23198
+ let unreadable = 0;
23199
+ for (const meta of await storage.list(PRIVACY_PLACEHOLDER_NAMESPACE)) {
23200
+ const raw = await storage.read(PRIVACY_PLACEHOLDER_NAMESPACE, meta.key);
23201
+ if (!raw) continue;
23202
+ try {
23203
+ const encrypted = JSON.parse(bytesToString(raw));
23204
+ const decrypted = decrypt(encrypted, encryptionKey);
23205
+ const parsed = JSON.parse(bytesToString(decrypted));
23206
+ const safe = {
23207
+ key: meta.key,
23208
+ version: parsed.version,
23209
+ kind: parsed.kind ?? (meta.key.endsWith("__index") ? "index" : "metadata"),
23210
+ scope: parsed.scope,
23211
+ class: parsed.class,
23212
+ placeholder: parsed.placeholder,
23213
+ alias: parsed.alias,
23214
+ raw_hash: parsed.raw_hash,
23215
+ counters: parsed.counters,
23216
+ next: parsed.next,
23217
+ created_at: parsed.created_at
23218
+ };
23219
+ entries.push(
23220
+ Object.fromEntries(
23221
+ Object.entries(safe).filter(([, value]) => value !== void 0)
23222
+ )
23223
+ );
23224
+ } catch {
23225
+ unreadable++;
23226
+ }
23227
+ }
23228
+ return {
23229
+ format: "SANCTUARY_PLACEHOLDER_VAULT_METADATA_V1",
23230
+ exported_at: (/* @__PURE__ */ new Date()).toISOString(),
23231
+ entries,
23232
+ unreadable_count: unreadable,
23233
+ redacted_fields: ["raw_value", "raw_path"]
23234
+ };
23235
+ }
23236
+ async function exportExitBundle(opts) {
23237
+ const bundleDir = path.resolve(opts.bundleDir);
23238
+ await promises.mkdir(bundleDir, { recursive: true, mode: 448 });
23239
+ await promises.mkdir(path.join(bundleDir, ARTIFACT_DIR), { recursive: true, mode: 448 });
23240
+ const identity = opts.identityManager.getDefault();
23241
+ if (!identity) {
23242
+ throw new Error("Cannot export exit bundle: no default identity exists.");
23243
+ }
23244
+ const exportApprovalAuditId = opts.exportApprovalAuditId ?? `exit-export-${Date.now()}`;
23245
+ opts.auditLog.append("l1", "exit_bundle_export", identity.identity_id, {
23246
+ approval_id: exportApprovalAuditId,
23247
+ manifest_version: EXIT_BUNDLE_MANIFEST_VERSION
23248
+ });
23249
+ const reputationStore = opts.reputationStore ?? new ReputationStore(opts.storage, opts.masterKey);
23250
+ const identityEncryptionKey = derivePurposeKey(opts.masterKey, "identity-encryption");
23251
+ const artifacts = [];
23252
+ artifacts.push(
23253
+ await writeJsonArtifact(
23254
+ bundleDir,
23255
+ `${ARTIFACT_DIR}/public_identity.json`,
23256
+ exportPublicIdentity(identity, opts.masterKey),
23257
+ "public_identity"
23258
+ )
23259
+ );
23260
+ artifacts.push(
23261
+ await writeJsonArtifact(
23262
+ bundleDir,
23263
+ `${ARTIFACT_DIR}/encrypted_state.json`,
23264
+ await exportEncryptedState(opts),
23265
+ "encrypted_state"
23266
+ )
23267
+ );
23268
+ artifacts.push(
23269
+ await writeJsonArtifact(
23270
+ bundleDir,
23271
+ `${ARTIFACT_DIR}/policy_set.json`,
23272
+ exportPolicySet(opts.policy, opts.config),
23273
+ "policy_set"
23274
+ )
23275
+ );
23276
+ artifacts.push(
23277
+ await writeJsonArtifact(
23278
+ bundleDir,
23279
+ `${ARTIFACT_DIR}/audit_receipts.json`,
23280
+ await exportAuditReceipts(opts.auditLog),
23281
+ "audit_receipts"
23282
+ )
23283
+ );
23284
+ artifacts.push(
23285
+ await writeJsonArtifact(
23286
+ bundleDir,
23287
+ `${ARTIFACT_DIR}/reputation_bundle.json`,
23288
+ await reputationStore.exportBundle(identity, identityEncryptionKey),
23289
+ "reputation_bundle"
23290
+ )
23291
+ );
23292
+ artifacts.push(
23293
+ await writeJsonArtifact(
23294
+ bundleDir,
23295
+ `${ARTIFACT_DIR}/commitments.json`,
23296
+ await exportCommitments(opts.storage, opts.masterKey),
23297
+ "commitments"
23298
+ )
23299
+ );
23300
+ artifacts.push(
23301
+ await writeJsonArtifact(
23302
+ bundleDir,
23303
+ `${ARTIFACT_DIR}/placeholder_vault_metadata.json`,
23304
+ await exportPlaceholderVaultMetadata(opts.storage, opts.masterKey),
23305
+ "placeholder_vault_metadata"
23306
+ )
23307
+ );
23308
+ const body = {
23309
+ manifest_version: EXIT_BUNDLE_MANIFEST_VERSION,
23310
+ exported_at: (/* @__PURE__ */ new Date()).toISOString(),
23311
+ identity_binding: {
23312
+ identity_id: identity.identity_id,
23313
+ fortress_id: identity.did,
23314
+ fortress_master_pubkey: identity.public_key,
23315
+ did: identity.did
23316
+ },
23317
+ source_sanctuary_version: opts.config?.version ?? SANCTUARY_VERSION,
23318
+ artifacts,
23319
+ artifacts_aggregate_hash: sha256Hex3(
23320
+ stringToBytes(canonicalize2(artifacts))
23321
+ ),
23322
+ artifacts_aggregate_hash_alg: "sha256",
23323
+ export_approval_audit_id: exportApprovalAuditId,
23324
+ signature_scheme: SIGNATURE_SCHEME_V1
23325
+ };
23326
+ const signature = sign(
23327
+ canonicalizeToBytes(body),
23328
+ identity.encrypted_private_key,
23329
+ identityEncryptionKey
23330
+ );
23331
+ const manifest = {
23332
+ body,
23333
+ signature: toBase64url(signature)
23334
+ };
23335
+ const manifestBytes = jsonBytes(manifest);
23336
+ await promises.writeFile(path.join(bundleDir, "manifest.json"), manifestBytes, { mode: 384 });
23337
+ await opts.auditLog.flush();
23338
+ return {
23339
+ bundle_dir: bundleDir,
23340
+ manifest,
23341
+ manifest_hash: sha256Hex3(manifestBytes),
23342
+ artifact_count: artifacts.length,
23343
+ unsupported_artifacts: [
23344
+ "audit_receipts: legacy L2 audit entries are manifest-pinned but not individually signed"
23345
+ ]
23346
+ };
23347
+ }
23348
+ function publicKeysFromIdentityArtifact(identityArtifact) {
23349
+ const pubkey = fromBase64url(identityArtifact.bundle.publicKey);
23350
+ return {
23351
+ byIdentityId: /* @__PURE__ */ new Map([[identityArtifact.bundle.identity_id, pubkey]]),
23352
+ byDid: /* @__PURE__ */ new Map([[identityArtifact.bundle.did, pubkey]])
23353
+ };
23354
+ }
23355
+ async function conflictReport(storage, identityArtifact, encryptedState, reputationBundle, manifest) {
23356
+ const stateConflicts = [];
23357
+ for (const item of encryptedState?.entries ?? []) {
23358
+ if (await storage.exists(item.namespace, item.key)) {
23359
+ stateConflicts.push({ namespace: item.namespace, key: item.key });
23360
+ }
23361
+ }
23362
+ const reputationConflicts = [];
23363
+ for (const attestation of reputationBundle?.attestations ?? []) {
23364
+ if (await storage.exists("_reputation", attestation.attestation_id)) {
23365
+ reputationConflicts.push(attestation.attestation_id);
23366
+ }
23367
+ }
23368
+ const importId = importIdForManifest(manifest);
23369
+ return {
23370
+ public_identity_exists: identityArtifact ? await storage.exists(
23371
+ EXIT_PUBLIC_IDENTITIES_NAMESPACE,
23372
+ identityArtifact.bundle.identity_id
23373
+ ) : false,
23374
+ state_conflicts: stateConflicts,
23375
+ reputation_conflicts: reputationConflicts,
23376
+ policy_set_exists: await storage.exists(EXIT_POLICY_SETS_NAMESPACE, importId),
23377
+ audit_receipts_exist: await storage.exists(EXIT_AUDIT_RECEIPTS_NAMESPACE, importId)
23378
+ };
23379
+ }
23380
+ function importIdForManifest(manifest) {
23381
+ return `${manifest.body.identity_binding.identity_id}-${manifest.body.exported_at.replace(/[^0-9a-zA-Z_.-]/g, "_")}`;
23382
+ }
23383
+ async function resolveSourceMasterKey(encryptedState, opts) {
23384
+ if (!encryptedState || encryptedState.entries.length === 0) return null;
23385
+ if (opts.sourceMasterKey) return opts.sourceMasterKey;
23386
+ if (opts.sourcePassphrase && encryptedState.source_key_derivation) {
23387
+ return (await deriveMasterKey(
23388
+ opts.sourcePassphrase,
23389
+ encryptedState.source_key_derivation
23390
+ )).key;
23391
+ }
23392
+ if (opts.sourceRecoveryKey) {
23393
+ const key = fromBase64url(opts.sourceRecoveryKey);
23394
+ if (key.length !== 32) {
23395
+ throw new Error("Source recovery key must decode to 32 bytes.");
23396
+ }
23397
+ return key;
23398
+ }
23399
+ return null;
23400
+ }
23401
+ async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId) {
23402
+ const destinationSigner = opts.destinationSignerIdentityId ? opts.identityManager.get(opts.destinationSignerIdentityId) : opts.identityManager.getDefault();
23403
+ if (!destinationSigner) {
23404
+ return {
23405
+ status: "skipped_no_destination_signer",
23406
+ imported_keys: 0,
23407
+ skipped_keys: encryptedState.entries.length,
23408
+ skipped_invalid_sig: 0,
23409
+ skipped_unknown_kid: 0,
23410
+ conflicts: 0
23411
+ };
23412
+ }
23413
+ const stateStore = new StateStore(opts.storage, opts.masterKey);
23414
+ const identityEncryptionKey = derivePurposeKey(opts.masterKey, "identity-encryption");
23415
+ let imported = 0;
23416
+ let skipped = 0;
23417
+ let skippedInvalidSig = 0;
23418
+ let skippedUnknownKid = 0;
23419
+ let conflicts = 0;
23420
+ for (const item of encryptedState.entries) {
23421
+ if (isReservedNamespace(item.namespace)) {
23422
+ skipped++;
23423
+ continue;
23424
+ }
23425
+ const signerPubkey = publicKeysByIdentityId.get(item.entry.kid);
23426
+ if (!signerPubkey) {
23427
+ skippedUnknownKid++;
23428
+ skipped++;
23429
+ continue;
23430
+ }
23431
+ const sourceSigValid = verify(
23432
+ fromBase64url(item.entry.payload.ct),
23433
+ fromBase64url(item.entry.sig),
23434
+ signerPubkey
23435
+ );
23436
+ if (!sourceSigValid) {
23437
+ skippedInvalidSig++;
23438
+ skipped++;
23439
+ continue;
23440
+ }
23441
+ const exists = await opts.storage.exists(item.namespace, item.key);
23442
+ if (exists) {
23443
+ conflicts++;
23444
+ const resolution = opts.conflictResolution ?? "skip";
23445
+ if (resolution === "skip") {
23446
+ skipped++;
23447
+ continue;
23448
+ }
23449
+ if (resolution === "version") {
23450
+ const raw = await opts.storage.read(item.namespace, item.key);
23451
+ if (raw) {
23452
+ try {
23453
+ const existing = JSON.parse(bytesToString(raw));
23454
+ if (item.entry.ver <= existing.ver) {
23455
+ skipped++;
23456
+ continue;
23457
+ }
23458
+ } catch {
23459
+ }
23460
+ }
23461
+ }
23462
+ }
23463
+ try {
23464
+ const plaintext = decrypt(
23465
+ item.entry.payload,
23466
+ deriveNamespaceKey(sourceMasterKey, item.namespace)
23467
+ );
23468
+ if (hashToString(plaintext) !== item.entry.integrity_hash) {
23469
+ skippedInvalidSig++;
23470
+ skipped++;
23471
+ continue;
23472
+ }
23473
+ await stateStore.write(
23474
+ item.namespace,
23475
+ item.key,
23476
+ bytesToString(plaintext),
23477
+ destinationSigner.identity_id,
23478
+ destinationSigner.encrypted_private_key,
23479
+ identityEncryptionKey,
23480
+ {
23481
+ content_type: item.entry.metadata.content_type,
23482
+ ttl_seconds: item.entry.metadata.ttl_seconds,
23483
+ tags: [
23484
+ ...item.entry.metadata.tags ?? [],
23485
+ "exit-import",
23486
+ `source:${item.entry.kid}`
23487
+ ]
23488
+ }
23489
+ );
23490
+ imported++;
23491
+ } catch {
23492
+ skippedInvalidSig++;
23493
+ skipped++;
23494
+ }
23495
+ }
23496
+ return {
23497
+ status: "rekeyed",
23498
+ imported_keys: imported,
23499
+ skipped_keys: skipped,
23500
+ skipped_invalid_sig: skippedInvalidSig,
23501
+ skipped_unknown_kid: skippedUnknownKid,
23502
+ conflicts
23503
+ };
23504
+ }
23505
+ async function stageArtifact(storage, namespace, key, value) {
23506
+ await storage.write(namespace, key, jsonBytes(value));
23507
+ }
23508
+ async function importExitBundle(opts) {
23509
+ const verification = await verifyExitBundle(opts.bundleDir);
23510
+ if (!verification.passed) {
23511
+ return {
23512
+ verified: false,
23513
+ activated: false,
23514
+ conflicts: {
23515
+ public_identity_exists: false,
23516
+ state_conflicts: [],
23517
+ reputation_conflicts: [],
23518
+ policy_set_exists: false,
23519
+ audit_receipts_exist: false
23520
+ },
23521
+ state: {
23522
+ status: "not_requested",
23523
+ imported_keys: 0,
23524
+ skipped_keys: 0,
23525
+ skipped_invalid_sig: 0,
23526
+ skipped_unknown_kid: 0,
23527
+ conflicts: 0
23528
+ },
23529
+ reputation: {
23530
+ imported_attestations: 0,
23531
+ invalid_attestations: 0,
23532
+ unverifiable_attestations: verification.reputation?.unverifiable_attestations ?? 0
23533
+ },
23534
+ staged_artifacts: [],
23535
+ warnings: verification.warnings,
23536
+ unsupported_artifacts: verification.unsupported_artifacts
23537
+ };
23538
+ }
23539
+ const manifest = await readManifest(opts.bundleDir);
23540
+ const identityArtifact = await loadExitArtifact(
23541
+ opts.bundleDir,
23542
+ manifest,
23543
+ "public_identity"
23544
+ );
23545
+ const encryptedState = await loadExitArtifact(
23546
+ opts.bundleDir,
23547
+ manifest,
23548
+ "encrypted_state"
23549
+ );
23550
+ const policySet = await loadExitArtifact(
23551
+ opts.bundleDir,
23552
+ manifest,
23553
+ "policy_set"
23554
+ );
23555
+ const auditReceipts = await loadExitArtifact(
23556
+ opts.bundleDir,
23557
+ manifest,
23558
+ "audit_receipts"
23559
+ );
23560
+ const reputationArtifact = await loadExitArtifact(
23561
+ opts.bundleDir,
23562
+ manifest,
23563
+ "reputation_bundle"
23564
+ );
23565
+ const commitments = await loadExitArtifact(
23566
+ opts.bundleDir,
23567
+ manifest,
23568
+ "commitments"
23569
+ );
23570
+ const placeholderMetadata = await loadExitArtifact(
23571
+ opts.bundleDir,
23572
+ manifest,
23573
+ "placeholder_vault_metadata"
23574
+ );
23575
+ const conflicts = await conflictReport(
23576
+ opts.storage,
23577
+ identityArtifact?.json ?? null,
23578
+ encryptedState?.json ?? null,
23579
+ reputationArtifact?.json ?? null,
23580
+ manifest
23581
+ );
23582
+ if (!opts.activate) {
23583
+ return {
23584
+ verified: true,
23585
+ activated: false,
23586
+ conflicts,
23587
+ state: {
23588
+ status: "not_requested",
23589
+ imported_keys: 0,
23590
+ skipped_keys: 0,
23591
+ skipped_invalid_sig: 0,
23592
+ skipped_unknown_kid: 0,
23593
+ conflicts: conflicts.state_conflicts.length
23594
+ },
23595
+ reputation: {
23596
+ imported_attestations: 0,
23597
+ invalid_attestations: 0,
23598
+ unverifiable_attestations: verification.reputation?.unverifiable_attestations ?? 0
23599
+ },
23600
+ staged_artifacts: [],
23601
+ warnings: verification.warnings,
23602
+ unsupported_artifacts: verification.unsupported_artifacts
23603
+ };
23604
+ }
23605
+ const importId = importIdForManifest(manifest);
23606
+ const stagedArtifacts = [];
23607
+ if (identityArtifact) {
23608
+ await stageArtifact(
23609
+ opts.storage,
23610
+ EXIT_PUBLIC_IDENTITIES_NAMESPACE,
23611
+ identityArtifact.json.bundle.identity_id,
23612
+ identityArtifact.json
23613
+ );
23614
+ stagedArtifacts.push("public_identity");
23615
+ }
23616
+ if (policySet) {
23617
+ await stageArtifact(opts.storage, EXIT_POLICY_SETS_NAMESPACE, importId, policySet.json);
23618
+ stagedArtifacts.push("policy_set");
23619
+ }
23620
+ if (auditReceipts) {
23621
+ await stageArtifact(
23622
+ opts.storage,
23623
+ EXIT_AUDIT_RECEIPTS_NAMESPACE,
23624
+ importId,
23625
+ auditReceipts.json
23626
+ );
23627
+ stagedArtifacts.push("audit_receipts");
23628
+ }
23629
+ if (commitments) {
23630
+ await stageArtifact(opts.storage, EXIT_COMMITMENTS_NAMESPACE, importId, commitments.json);
23631
+ stagedArtifacts.push("commitments");
23632
+ }
23633
+ if (placeholderMetadata) {
23634
+ await stageArtifact(
23635
+ opts.storage,
23636
+ EXIT_PLACEHOLDER_METADATA_NAMESPACE,
23637
+ importId,
23638
+ placeholderMetadata.json
23639
+ );
23640
+ stagedArtifacts.push("placeholder_vault_metadata");
23641
+ }
23642
+ await stageArtifact(opts.storage, EXIT_IMPORT_NAMESPACE, importId, {
23643
+ manifest: manifest.body,
23644
+ verified_at: verification.verified_at,
23645
+ activated_at: (/* @__PURE__ */ new Date()).toISOString()
23646
+ });
23647
+ const publicKeys = identityArtifact ? publicKeysFromIdentityArtifact(identityArtifact.json) : { byIdentityId: /* @__PURE__ */ new Map(), byDid: /* @__PURE__ */ new Map() };
23648
+ let reputationResult = {
23649
+ imported_attestations: 0,
23650
+ invalid_attestations: 0,
23651
+ unverifiable_attestations: verification.reputation?.unverifiable_attestations ?? 0
23652
+ };
23653
+ if (reputationArtifact) {
23654
+ const reputationStore = opts.reputationStore ?? new ReputationStore(opts.storage, opts.masterKey);
23655
+ const imported = await reputationStore.importBundle(
23656
+ reputationArtifact.json,
23657
+ true,
23658
+ publicKeys.byDid
23659
+ );
23660
+ reputationResult = {
23661
+ imported_attestations: imported.imported,
23662
+ invalid_attestations: imported.invalid,
23663
+ unverifiable_attestations: verification.reputation?.unverifiable_attestations ?? 0
23664
+ };
23665
+ stagedArtifacts.push("reputation_bundle");
23666
+ }
23667
+ const sourceMasterKey = await resolveSourceMasterKey(
23668
+ encryptedState?.json ?? null,
23669
+ opts
23670
+ );
23671
+ const stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
23672
+ encryptedState.json,
23673
+ opts,
23674
+ sourceMasterKey,
23675
+ publicKeys.byIdentityId
23676
+ ) : {
23677
+ status: "staged_requires_source_key",
23678
+ imported_keys: 0,
23679
+ skipped_keys: encryptedState.json.entries.length,
23680
+ skipped_invalid_sig: 0,
23681
+ skipped_unknown_kid: 0,
23682
+ conflicts: conflicts.state_conflicts.length
23683
+ } : {
23684
+ status: "not_requested",
23685
+ imported_keys: 0,
23686
+ skipped_keys: 0,
23687
+ skipped_invalid_sig: 0,
23688
+ skipped_unknown_kid: 0,
23689
+ conflicts: 0
23690
+ };
23691
+ opts.auditLog.append("l1", "exit_bundle_import_activate", manifest.body.identity_binding.identity_id, {
23692
+ import_id: importId,
23693
+ manifest_version: manifest.body.manifest_version,
23694
+ state_status: stateResult.status,
23695
+ state_imported_keys: stateResult.imported_keys,
23696
+ reputation_imported_attestations: reputationResult.imported_attestations
23697
+ });
23698
+ await opts.auditLog.flush();
23699
+ return {
23700
+ verified: true,
23701
+ activated: true,
23702
+ conflicts,
23703
+ state: stateResult,
23704
+ reputation: reputationResult,
23705
+ staged_artifacts: stagedArtifacts,
23706
+ warnings: verification.warnings,
23707
+ unsupported_artifacts: verification.unsupported_artifacts
23708
+ };
23709
+ }
23710
+ function exitBundleManifestShape() {
23711
+ return {
23712
+ manifest_version: EXIT_BUNDLE_MANIFEST_VERSION,
23713
+ artifacts: [...EXIT_BUNDLE_ARTIFACT_KINDS],
23714
+ hash_alg: "sha256",
23715
+ signature_scheme: SIGNATURE_SCHEME_V1,
23716
+ required_top_level_file: "manifest.json",
23717
+ artifact_paths: [
23718
+ "artifacts/public_identity.json",
23719
+ "artifacts/encrypted_state.json",
23720
+ "artifacts/policy_set.json",
23721
+ "artifacts/audit_receipts.json",
23722
+ "artifacts/reputation_bundle.json",
23723
+ "artifacts/commitments.json",
23724
+ "artifacts/placeholder_vault_metadata.json"
23725
+ ]
23726
+ };
23727
+ }
23728
+ init_encoding();
23729
+ function write(stream, text) {
23730
+ stream.write(text);
23731
+ }
23732
+ function flagValue(argv, name) {
23733
+ const index = argv.indexOf(name);
23734
+ if (index === -1) return void 0;
23735
+ return argv[index + 1];
23736
+ }
23737
+ function hasFlag(argv, name) {
23738
+ return argv.includes(name);
23739
+ }
23740
+ function repeatedFlagValues(argv, name) {
23741
+ const values = [];
23742
+ for (let i = 0; i < argv.length; i++) {
23743
+ if (argv[i] === name && argv[i + 1]) values.push(argv[++i]);
23744
+ }
23745
+ return values;
23746
+ }
23747
+ async function confirmTier1(prompt, assumeYes, stdin, err) {
23748
+ if (assumeYes) return true;
23749
+ const readline = await import('readline/promises');
23750
+ const rl = readline.createInterface({
23751
+ input: stdin,
23752
+ output: err
23753
+ });
23754
+ const answer = await rl.question(`${prompt} [y/N] `);
23755
+ rl.close();
23756
+ return /^y(es)?$/i.test(answer.trim());
23757
+ }
23758
+ async function openExitContext(argv, env) {
23759
+ const passphrase = flagValue(argv, "--passphrase") ?? env.SANCTUARY_PASSPHRASE;
23760
+ const recoveryKey = env.SANCTUARY_RECOVERY_KEY;
23761
+ const config = await loadConfig();
23762
+ await promises.mkdir(config.storage_path, { recursive: true, mode: 448 });
23763
+ const stateStoragePath = path.join(config.storage_path, "state");
23764
+ const storage = new FilesystemStorage(stateStoragePath);
23765
+ let masterKey;
23766
+ let keySource = "unknown";
23767
+ if (passphrase) {
23768
+ let existingParams;
23769
+ const raw = await storage.read("_meta", "key-params");
23770
+ if (raw) existingParams = JSON.parse(bytesToString(raw));
23771
+ const derived = await deriveMasterKey(passphrase, existingParams);
23772
+ masterKey = derived.key;
23773
+ if (!existingParams) {
23774
+ await storage.write(
23775
+ "_meta",
23776
+ "key-params",
23777
+ stringToBytes(JSON.stringify(derived.params))
23778
+ );
23779
+ }
23780
+ keySource = "passphrase";
23781
+ } else if (recoveryKey) {
23782
+ masterKey = fromBase64url(recoveryKey);
23783
+ if (masterKey.length !== 32) {
23784
+ throw new Error("SANCTUARY_RECOVERY_KEY must decode to 32 bytes.");
23785
+ }
23786
+ keySource = "recovery-key";
23787
+ } else {
23788
+ throw new Error(
23789
+ "sanctuary exit requires SANCTUARY_PASSPHRASE, --passphrase, or SANCTUARY_RECOVERY_KEY."
23790
+ );
23791
+ }
23792
+ const auditLog = new AuditLog(storage, masterKey);
23793
+ const identityManager = new IdentityManager(storage, masterKey);
23794
+ await identityManager.load();
23795
+ const reputationStore = new ReputationStore(storage, masterKey);
23796
+ return {
23797
+ storagePath: config.storage_path,
23798
+ stateStoragePath,
23799
+ storage,
23800
+ masterKey,
23801
+ auditLog,
23802
+ identityManager,
23803
+ reputationStore,
23804
+ keySource
23805
+ };
23806
+ }
23807
+ function printUsage(out) {
23808
+ write(out, `
23809
+ Usage: sanctuary exit <command> [options]
23810
+
23811
+ Commands:
23812
+ export --out <dir> Create a SANCTUARY_EXIT_BUNDLE_V1 directory
23813
+ verify <dir> Verify manifest, audit receipts, and reputation bundle
23814
+ import <dir> [--activate] Verify, report conflicts, and optionally activate
23815
+ manifest-shape Print the v1.1 manifest shape
23816
+
23817
+ Options:
23818
+ --passphrase <value> Current destination/source passphrase
23819
+ --source-passphrase <value> Source passphrase for state re-key on import
23820
+ --source-recovery-key <value> Source recovery key for state re-key on import
23821
+ --destination-identity-id <id> Destination signer for re-keyed state
23822
+ --state-namespace <name> Export a namespace; repeatable
23823
+ --conflict <skip|overwrite|version>
23824
+ --json
23825
+ --yes, -y Explicit non-interactive Tier 1 approval
23826
+ --help, -h
23827
+ `);
23828
+ }
23829
+ async function runExitCommand(args) {
23830
+ const argv = args.argv;
23831
+ const out = args.out ?? process.stdout;
23832
+ const err = args.err ?? process.stderr;
23833
+ const stdin = args.stdin ?? process.stdin;
23834
+ const env = args.env ?? process.env;
23835
+ if (argv.length === 0 || hasFlag(argv, "--help") || hasFlag(argv, "-h")) {
23836
+ printUsage(out);
23837
+ return 0;
23838
+ }
23839
+ const command = argv[0];
23840
+ const json = hasFlag(argv, "--json");
23841
+ try {
23842
+ if (command === "manifest-shape") {
23843
+ write(out, JSON.stringify(exitBundleManifestShape(), null, 2) + "\n");
23844
+ return 0;
23845
+ }
23846
+ if (command === "verify") {
23847
+ const dir = argv[1];
23848
+ if (!dir) {
23849
+ write(err, "Usage: sanctuary exit verify <dir>\n");
23850
+ return 2;
23851
+ }
23852
+ const result = await verifyExitBundle(dir);
23853
+ if (json) {
23854
+ write(out, JSON.stringify(result, null, 2) + "\n");
23855
+ } else {
23856
+ write(out, `manifest: ${result.passed ? "verified" : "failed"}
23857
+ `);
23858
+ write(out, `identity: ${result.manifest_summary.identity_id}
23859
+ `);
23860
+ write(out, `artifacts: ${result.manifest_summary.artifact_count}
23861
+ `);
23862
+ if (result.reputation) {
23863
+ write(
23864
+ out,
23865
+ `reputation: ${result.reputation.verified_attestations}/${result.reputation.attestation_count} attestations verified
23866
+ `
23867
+ );
23868
+ }
23869
+ for (const warning of result.warnings) write(out, `warning: ${warning}
23870
+ `);
23871
+ for (const item of result.unsupported_artifacts) {
23872
+ write(out, `unsupported: ${item}
23873
+ `);
23874
+ }
23875
+ }
23876
+ return result.passed ? 0 : 1;
23877
+ }
23878
+ if (command === "export") {
23879
+ const outDir = flagValue(argv, "--out");
23880
+ if (!outDir) {
23881
+ write(err, "Usage: sanctuary exit export --out <dir>\n");
23882
+ return 2;
23883
+ }
23884
+ const approved = await confirmTier1(
23885
+ "Tier 1 approval required: export complete Sanctuary exit bundle?",
23886
+ hasFlag(argv, "--yes") || hasFlag(argv, "-y"),
23887
+ stdin,
23888
+ err
23889
+ );
23890
+ if (!approved) {
23891
+ write(err, "Aborted.\n");
23892
+ return 1;
23893
+ }
23894
+ const config = await loadConfig();
23895
+ const ctx = await openExitContext(argv, env);
23896
+ const policy = await loadPrincipalPolicy(ctx.storagePath);
23897
+ const result = await exportExitBundle({
23898
+ bundleDir: outDir,
23899
+ storage: ctx.storage,
23900
+ masterKey: ctx.masterKey,
23901
+ identityManager: ctx.identityManager,
23902
+ auditLog: ctx.auditLog,
23903
+ reputationStore: ctx.reputationStore,
23904
+ policy,
23905
+ config,
23906
+ stateStoragePath: ctx.stateStoragePath,
23907
+ stateNamespaces: repeatedFlagValues(argv, "--state-namespace"),
23908
+ keySource: ctx.keySource
23909
+ });
23910
+ if (json) write(out, JSON.stringify(result, null, 2) + "\n");
23911
+ else {
23912
+ write(out, `exported: ${result.bundle_dir}
23913
+ `);
23914
+ write(out, `manifest_hash: ${result.manifest_hash}
23915
+ `);
23916
+ for (const item of result.unsupported_artifacts) {
23917
+ write(out, `unsupported: ${item}
23918
+ `);
23919
+ }
23920
+ }
23921
+ return 0;
23922
+ }
23923
+ if (command === "import") {
23924
+ const dir = argv[1];
23925
+ if (!dir) {
23926
+ write(err, "Usage: sanctuary exit import <dir> [--activate]\n");
23927
+ return 2;
23928
+ }
23929
+ const activate = hasFlag(argv, "--activate");
23930
+ if (activate) {
23931
+ const approved = await confirmTier1(
23932
+ "Tier 1 approval required: activate verified imported exit bundle?",
23933
+ hasFlag(argv, "--yes") || hasFlag(argv, "-y"),
23934
+ stdin,
23935
+ err
23936
+ );
23937
+ if (!approved) {
23938
+ write(err, "Aborted.\n");
23939
+ return 1;
23940
+ }
23941
+ }
23942
+ const ctx = await openExitContext(argv, env);
23943
+ const conflict = flagValue(argv, "--conflict") ?? "skip";
23944
+ if (!["skip", "overwrite", "version"].includes(conflict)) {
23945
+ write(err, "--conflict must be skip, overwrite, or version\n");
23946
+ return 2;
23947
+ }
23948
+ const result = await importExitBundle({
23949
+ bundleDir: dir,
23950
+ storage: ctx.storage,
23951
+ masterKey: ctx.masterKey,
23952
+ identityManager: ctx.identityManager,
23953
+ auditLog: ctx.auditLog,
23954
+ reputationStore: ctx.reputationStore,
23955
+ activate,
23956
+ conflictResolution: conflict,
23957
+ sourcePassphrase: flagValue(argv, "--source-passphrase"),
23958
+ sourceRecoveryKey: flagValue(argv, "--source-recovery-key"),
23959
+ destinationSignerIdentityId: flagValue(argv, "--destination-identity-id")
23960
+ });
23961
+ if (json) write(out, JSON.stringify(result, null, 2) + "\n");
23962
+ else {
23963
+ write(out, `verified: ${result.verified}
23964
+ `);
23965
+ write(out, `activated: ${result.activated}
23966
+ `);
23967
+ write(out, `state_conflicts: ${result.conflicts.state_conflicts.length}
23968
+ `);
23969
+ write(out, `reputation_conflicts: ${result.conflicts.reputation_conflicts.length}
23970
+ `);
23971
+ write(out, `state_status: ${result.state.status}
23972
+ `);
23973
+ write(out, `state_imported_keys: ${result.state.imported_keys}
23974
+ `);
23975
+ write(out, `reputation_imported_attestations: ${result.reputation.imported_attestations}
23976
+ `);
23977
+ for (const warning of result.warnings) write(out, `warning: ${warning}
23978
+ `);
23979
+ for (const item of result.unsupported_artifacts) {
23980
+ write(out, `unsupported: ${item}
23981
+ `);
23982
+ }
23983
+ }
23984
+ return result.verified ? 0 : 1;
23985
+ }
23986
+ write(err, `Unknown exit command: ${command}
23987
+ `);
23988
+ return 2;
23989
+ } catch (error) {
23990
+ write(err, error instanceof Error ? `${error.message}
23991
+ ` : `${String(error)}
23992
+ `);
23993
+ return 1;
23994
+ }
23995
+ }
23996
+
23997
+ // src/dashboard/aggregator.ts
23998
+ var L4_DEGRADATION_IMPACT = {
23999
+ critical: 40,
24000
+ warning: 25,
24001
+ info: 10
24002
+ };
24003
+ function computeL4LayerScore(degradations, status) {
24004
+ if (status === "compromised") return 0;
24005
+ let score = 100;
24006
+ for (const deg of degradations) {
24007
+ score -= L4_DEGRADATION_IMPACT[deg.severity] ?? 10;
24008
+ }
24009
+ score = Math.max(0, score);
24010
+ if (degradations.length === 0 && score > 50) {
24011
+ score = Math.min(100, score + 5);
24012
+ }
24013
+ return Math.round(score);
24014
+ }
24015
+ var MAX_ACTIVITY = 50;
24016
+ var MAX_AUDIT = 50;
24017
+ function fingerprintDID(did) {
24018
+ const raw = did.replace(/^did:[a-z0-9]+:/i, "");
24019
+ if (raw.length <= 12) return raw;
24020
+ return `${raw.slice(0, 6)}\u2026${raw.slice(-6)}`;
24021
+ }
24022
+ function countInjectionsToday(audit) {
24023
+ const startOfDay = /* @__PURE__ */ new Date();
24024
+ startOfDay.setHours(0, 0, 0, 0);
24025
+ const cutoff = startOfDay.getTime();
24026
+ return audit.filter((e) => {
24027
+ const ts = new Date(e.timestamp).getTime();
24028
+ if (isNaN(ts) || ts < cutoff) return false;
24029
+ const op = (e.operation ?? "").toLowerCase();
24030
+ return op.includes("injection") || op.includes("blocked");
24031
+ }).length;
24032
+ }
24033
+ var PROOF_CREATION_OPS = /* @__PURE__ */ new Set([
24034
+ "zk_prove",
24035
+ "zk_range_prove",
24036
+ "proof_commitment"
24037
+ ]);
24038
+ function countProofsToday(audit) {
24039
+ const startOfDay = /* @__PURE__ */ new Date();
24040
+ startOfDay.setHours(0, 0, 0, 0);
24041
+ const cutoff = startOfDay.getTime();
24042
+ return audit.filter((e) => {
24043
+ if (e.layer !== "l3") return false;
24044
+ if (!PROOF_CREATION_OPS.has(e.operation)) return false;
24045
+ const ts = new Date(e.timestamp).getTime();
24046
+ return !isNaN(ts) && ts >= cutoff;
24047
+ }).length;
24048
+ }
24049
+ function buildAgent(sources) {
24050
+ if (!sources.identityManager) {
24051
+ return {
24052
+ display_name: "Unclaimed agent",
24053
+ did: null,
24054
+ did_fingerprint: null,
24055
+ identity_count: 0,
24056
+ primary_identity_id: null
24057
+ };
24058
+ }
24059
+ const primary = sources.identityManager.getDefault();
24060
+ const identities = sources.identityManager.list();
24061
+ if (!primary) {
24062
+ return {
24063
+ display_name: "Unclaimed agent",
24064
+ did: null,
24065
+ did_fingerprint: null,
24066
+ identity_count: identities.length,
24067
+ primary_identity_id: null
24068
+ };
24069
+ }
24070
+ return {
24071
+ display_name: primary.label || "Sovereign agent",
24072
+ did: primary.did,
24073
+ did_fingerprint: fingerprintDID(primary.did),
24074
+ identity_count: identities.length,
24075
+ primary_identity_id: primary.identity_id
24076
+ };
24077
+ }
24078
+ function buildL1(sources, audit) {
24079
+ const hasIdentity = !!sources.identityManager?.getDefault();
24080
+ const state = hasIdentity ? "full" : "degraded";
24081
+ return {
24082
+ label: "L1 Cognitive",
24083
+ state,
24084
+ headline: hasIdentity ? "State encrypted at rest" : "No sovereign identity \u2014 run sanctuary_bootstrap",
24085
+ encryption: "AES-256-GCM + HKDF per namespace",
24086
+ injection_blocked_today: countInjectionsToday(audit),
24087
+ memory_attest_ready: hasIdentity
24088
+ };
24089
+ }
24090
+ function buildL2(sources) {
24091
+ const teeAvailable = sources.teeAvailable ?? false;
24092
+ const state = teeAvailable ? "full" : "degraded";
21486
24093
  return {
21487
24094
  label: "L2 Operational",
21488
24095
  state,
@@ -21616,6 +24223,36 @@ function buildUpstreamServers(sources) {
21616
24223
  return entry;
21617
24224
  });
21618
24225
  }
24226
+ function buildPrivacySummary(audit) {
24227
+ const classes = {};
24228
+ let filteredEvents = 0;
24229
+ let filteredSpans = 0;
24230
+ let lastFilteredAt = null;
24231
+ for (const entry of audit) {
24232
+ const details = entry.details ?? {};
24233
+ const rawFindings = details.privacy_findings;
24234
+ const findings = typeof rawFindings === "number" && Number.isFinite(rawFindings) ? Math.max(0, rawFindings) : 0;
24235
+ if (findings <= 0) continue;
24236
+ filteredEvents++;
24237
+ filteredSpans += findings;
24238
+ if (!lastFilteredAt || new Date(entry.timestamp).getTime() > new Date(lastFilteredAt).getTime()) {
24239
+ lastFilteredAt = entry.timestamp;
24240
+ }
24241
+ const rawClasses = details.privacy_classes;
24242
+ if (Array.isArray(rawClasses)) {
24243
+ for (const rawClass of rawClasses) {
24244
+ const key = String(rawClass);
24245
+ classes[key] = (classes[key] ?? 0) + 1;
24246
+ }
24247
+ }
24248
+ }
24249
+ return {
24250
+ filtered_events: filteredEvents,
24251
+ filtered_spans: filteredSpans,
24252
+ classes,
24253
+ last_filtered_at: lastFilteredAt
24254
+ };
24255
+ }
21619
24256
  async function getProtectionSnapshot(sources) {
21620
24257
  let audit = [];
21621
24258
  if (sources.auditLog) {
@@ -21633,6 +24270,7 @@ async function getProtectionSnapshot(sources) {
21633
24270
  const l4 = buildL4(sources);
21634
24271
  const activity = (sources.activity ?? []).slice(0, MAX_ACTIVITY);
21635
24272
  const pending_approvals = sources.pendingApprovals ?? [];
24273
+ const privacy = buildPrivacySummary(audit);
21636
24274
  const upstream_servers = buildUpstreamServers(sources);
21637
24275
  return {
21638
24276
  overall: computeOverall(l1, l2, l3, l4),
@@ -21641,6 +24279,7 @@ async function getProtectionSnapshot(sources) {
21641
24279
  activity,
21642
24280
  pending_approvals,
21643
24281
  audit: audit.slice(-MAX_AUDIT).reverse(),
24282
+ privacy,
21644
24283
  upstream_servers,
21645
24284
  mode: sources.mode,
21646
24285
  server_version: sources.server_version,
@@ -21754,7 +24393,7 @@ function l4EvidenceBlock(l4) {
21754
24393
  }
21755
24394
  function renderDashboardHTML(options) {
21756
24395
  const { snapshot } = options;
21757
- const { overall, agent, layers, activity, pending_approvals, audit, upstream_servers } = snapshot;
24396
+ const { overall, agent, layers, activity, pending_approvals, audit, privacy, upstream_servers } = snapshot;
21758
24397
  const activityRows = activity.length === 0 ? `<tr class="empty"><td colspan="5">Waiting for tool calls\u2026</td></tr>` : activity.map((entry) => {
21759
24398
  const time = new Date(entry.timestamp).toLocaleTimeString();
21760
24399
  return `<tr class="result-${escHtml(entry.result)}">
@@ -21787,6 +24426,8 @@ function renderDashboardHTML(options) {
21787
24426
  <span class="mono">${escHtml(s.name)}</span>
21788
24427
  <span class="server-meta">${escHtml(s.state)} \xB7 ${escHtml(s.tool_count)} tool${s.tool_count === 1 ? "" : "s"}</span>
21789
24428
  </li>`).join("");
24429
+ const privacyClasses = Object.entries(privacy.classes).sort((a, b) => b[1] - a[1]).map(([name, count]) => `<span class="privacy-chip">${escHtml(name)} ${escHtml(count)}</span>`).join("");
24430
+ const privacyLast = privacy.last_filtered_at ? new Date(privacy.last_filtered_at).toLocaleString() : "No filtering events yet";
21790
24431
  const initialSnapshot = JSON.stringify(snapshot).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
21791
24432
  return `<!DOCTYPE html>
21792
24433
  <html lang="en">
@@ -22064,6 +24705,13 @@ button { font: inherit; cursor: pointer; }
22064
24705
  .panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
22065
24706
  .panel-head { display: flex; justify-content: space-between; align-items: center; padding: 12px 18px; border-bottom: 1px solid var(--border); }
22066
24707
  .panel-head h3 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-dim); }
24708
+ .privacy-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
24709
+ .privacy-metric { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 14px 16px; }
24710
+ .privacy-metric dt { color: var(--ink-mute); font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 6px; }
24711
+ .privacy-metric dd { color: var(--ink); font-size: 22px; font-weight: 700; }
24712
+ .privacy-metric dd.small { font-size: 13px; font-weight: 500; color: var(--ink-dim); line-height: 1.35; overflow-wrap: anywhere; }
24713
+ .privacy-classes { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
24714
+ .privacy-chip { border: 1px solid var(--border); color: var(--ink-dim); border-radius: 999px; padding: 4px 8px; font-size: 12px; background: rgba(255,255,255,0.03); }
22067
24715
  .filter-row { display: flex; gap: 6px; }
22068
24716
  .filter-row button {
22069
24717
  padding: 4px 10px;
@@ -22161,6 +24809,27 @@ details.audit-details .audit-filters { display: flex; gap: 6px; padding: 0 18px
22161
24809
  <ul class="server-list" id="server-list">${serverRows}</ul>
22162
24810
  </section>
22163
24811
 
24812
+ <section class="section">
24813
+ <h2>Privacy boundary <span class="count" id="privacy-count">${privacy.filtered_spans}</span></h2>
24814
+ <dl class="privacy-grid">
24815
+ <div class="privacy-metric">
24816
+ <dt>Filtered spans</dt>
24817
+ <dd id="privacy-spans">${escHtml(privacy.filtered_spans)}</dd>
24818
+ </div>
24819
+ <div class="privacy-metric">
24820
+ <dt>Filtered events</dt>
24821
+ <dd id="privacy-events">${escHtml(privacy.filtered_events)}</dd>
24822
+ </div>
24823
+ <div class="privacy-metric">
24824
+ <dt>Last filter</dt>
24825
+ <dd class="small" id="privacy-last">${escHtml(privacyLast)}</dd>
24826
+ </div>
24827
+ </dl>
24828
+ <div class="privacy-classes" id="privacy-classes">
24829
+ ${privacyClasses || `<span class="privacy-chip">No classes recorded</span>`}
24830
+ </div>
24831
+ </section>
24832
+
22164
24833
  <section class="section">
22165
24834
  <h2>Live activity <span class="count" id="activity-count">${activity.length}</span></h2>
22166
24835
  <div class="panel">
@@ -22288,6 +24957,23 @@ details.audit-details .audit-filters { display: flex; gap: 6px; padding: 0 18px
22288
24957
  )).join("");
22289
24958
  }
22290
24959
 
24960
+ function renderPrivacy(summary) {
24961
+ const count = document.getElementById("privacy-count");
24962
+ const spans = document.getElementById("privacy-spans");
24963
+ const events = document.getElementById("privacy-events");
24964
+ const last = document.getElementById("privacy-last");
24965
+ const classes = document.getElementById("privacy-classes");
24966
+ if (!summary || !spans || !events || !last || !classes) return;
24967
+ if (count) count.textContent = String(summary.filtered_spans || 0);
24968
+ spans.textContent = String(summary.filtered_spans || 0);
24969
+ events.textContent = String(summary.filtered_events || 0);
24970
+ last.textContent = summary.last_filtered_at ? new Date(summary.last_filtered_at).toLocaleString() : "No filtering events yet";
24971
+ const rows = Object.entries(summary.classes || {}).sort((a, b) => b[1] - a[1]);
24972
+ classes.innerHTML = rows.length
24973
+ ? rows.map(([name, n]) => '<span class="privacy-chip">' + esc(name) + ' ' + esc(n) + '</span>').join("")
24974
+ : '<span class="privacy-chip">No classes recorded</span>';
24975
+ }
24976
+
22291
24977
  function renderAll(snap) {
22292
24978
  snapshot = snap;
22293
24979
  renderShield(snap.overall.light, snap.overall.headline);
@@ -22297,6 +24983,7 @@ details.audit-details .audit-filters { display: flex; gap: 6px; padding: 0 18px
22297
24983
  document.getElementById("agent-did").textContent = snap.agent.did_fingerprint || "unclaimed";
22298
24984
  renderActivity(snap.activity);
22299
24985
  renderApprovals(snap.pending_approvals);
24986
+ renderPrivacy(snap.privacy);
22300
24987
  renderAudit(snap.audit, currentAuditFilter());
22301
24988
  }
22302
24989
 
@@ -22764,88 +25451,6 @@ function applyChannelTemplate(id, params) {
22764
25451
  return entry.factory(params);
22765
25452
  }
22766
25453
 
22767
- // src/mesh/errors.ts
22768
- var MeshError = class extends Error {
22769
- constructor(message) {
22770
- super(message);
22771
- this.name = "MeshError";
22772
- }
22773
- };
22774
- var MeshEnvelopeError = class extends MeshError {
22775
- constructor(message) {
22776
- super(message);
22777
- this.name = "MeshEnvelopeError";
22778
- }
22779
- };
22780
- var MeshReservedExtensionKeyError = class extends MeshEnvelopeError {
22781
- constructor(key) {
22782
- super(
22783
- `v0.1 emitters MUST NOT populate reserved extension_envelope key: ${key}`
22784
- );
22785
- this.name = "MeshReservedExtensionKeyError";
22786
- }
22787
- };
22788
- var MeshReservedEventTypeError = class extends MeshEnvelopeError {
22789
- constructor(eventType) {
22790
- super(
22791
- `v0.1 emitters MUST NOT emit reserved-namespace event_type: ${eventType}`
22792
- );
22793
- this.name = "MeshReservedEventTypeError";
22794
- }
22795
- };
22796
-
22797
- // src/mesh/canonical-json.ts
22798
- var MeshCanonicalJsonError = class extends MeshError {
22799
- constructor(message) {
22800
- super(message);
22801
- this.name = "MeshCanonicalJsonError";
22802
- }
22803
- };
22804
- function canonicalize2(value) {
22805
- if (value === void 0) {
22806
- throw new MeshCanonicalJsonError(
22807
- "canonicalize(): top-level undefined is not serializable"
22808
- );
22809
- }
22810
- return encode(value);
22811
- }
22812
- function encode(value) {
22813
- if (value === null) return "null";
22814
- if (typeof value === "boolean") return value ? "true" : "false";
22815
- if (typeof value === "number") {
22816
- if (!Number.isFinite(value)) {
22817
- throw new MeshCanonicalJsonError(
22818
- `canonicalize(): non-finite number (${String(value)}) is not serializable`
22819
- );
22820
- }
22821
- return JSON.stringify(value);
22822
- }
22823
- if (typeof value === "string") return JSON.stringify(value);
22824
- if (Array.isArray(value)) return encodeArray(value);
22825
- if (typeof value === "object") return encodeObject(value);
22826
- throw new MeshCanonicalJsonError(
22827
- `canonicalize(): unsupported type ${typeof value}`
22828
- );
22829
- }
22830
- function encodeArray(arr) {
22831
- const parts = [];
22832
- for (const item of arr) {
22833
- parts.push(item === void 0 ? "null" : encode(item));
22834
- }
22835
- return "[" + parts.join(",") + "]";
22836
- }
22837
- function encodeObject(obj) {
22838
- const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
22839
- const parts = [];
22840
- for (const k of keys) {
22841
- parts.push(JSON.stringify(k) + ":" + encode(obj[k]));
22842
- }
22843
- return "{" + parts.join(",") + "}";
22844
- }
22845
- function canonicalizeToBytes(value) {
22846
- return new TextEncoder().encode(canonicalize2(value));
22847
- }
22848
-
22849
25454
  // src/policy-engine/canonical-policy.ts
22850
25455
  init_encoding();
22851
25456
 
@@ -23358,8 +25963,8 @@ var EXTRAS_FILE_NAME = "agents-extra.json";
23358
25963
  async function isTenantDir(path$1) {
23359
25964
  const [hasState, hasProfile, hasFallback] = await Promise.all([
23360
25965
  dirExists(path.join(path$1, "state")),
23361
- fileExists2(path.join(path$1, "cocoon-profile.json")),
23362
- fileExists2(path.join(path$1, "passphrase.enc"))
25966
+ fileExists3(path.join(path$1, "cocoon-profile.json")),
25967
+ fileExists3(path.join(path$1, "passphrase.enc"))
23363
25968
  ]);
23364
25969
  const initialized = hasState;
23365
25970
  let passphraseStatus;
@@ -23376,7 +25981,7 @@ async function dirExists(path) {
23376
25981
  return false;
23377
25982
  }
23378
25983
  }
23379
- async function fileExists2(path) {
25984
+ async function fileExists3(path) {
23380
25985
  try {
23381
25986
  const s = await promises.stat(path);
23382
25987
  return s.isFile();
@@ -23756,11 +26361,11 @@ async function startDashboardServer(options) {
23756
26361
  }
23757
26362
  }
23758
26363
  });
23759
- await new Promise((resolve2, reject) => {
26364
+ await new Promise((resolve4, reject) => {
23760
26365
  server.once("error", reject);
23761
26366
  server.listen(port, host, () => {
23762
26367
  server.off("error", reject);
23763
- resolve2();
26368
+ resolve4();
23764
26369
  });
23765
26370
  });
23766
26371
  const actualPort = (() => {
@@ -23773,12 +26378,14 @@ async function startDashboardServer(options) {
23773
26378
  url,
23774
26379
  port: actualPort,
23775
26380
  host,
23776
- stop: () => new Promise((resolve2, reject) => {
23777
- server.close((err) => err ? reject(err) : resolve2());
26381
+ stop: () => new Promise((resolve4, reject) => {
26382
+ server.close((err) => err ? reject(err) : resolve4());
23778
26383
  }),
23779
26384
  publish,
23780
26385
  publishActivity: (entry) => publish({ type: "activity", data: entry }),
23781
- publishApproval: (approval) => publish({ type: "approval", data: approval })
26386
+ publishApproval: (approval) => publish({ type: "approval", data: approval }),
26387
+ publishInbox: (item) => publish({ type: "inbox", data: item }),
26388
+ publishAgentStatus: (snapshot) => publish({ type: "agent_status", data: snapshot })
23782
26389
  };
23783
26390
  }
23784
26391
 
@@ -23913,6 +26520,20 @@ async function createSanctuaryServer(options) {
23913
26520
  }
23914
26521
  }
23915
26522
  const auditLog = new AuditLog(storage, masterKey);
26523
+ try {
26524
+ await consumeResetHistoryMarker({
26525
+ storagePath: config.storage_path,
26526
+ auditLog
26527
+ });
26528
+ } catch (err) {
26529
+ if (err instanceof ResetHistoryMalformedError) {
26530
+ throw new Error(
26531
+ `Sanctuary: ${err.message}
26532
+ Refusing to start the cocoon while the reset-history marker is unreadable.`
26533
+ );
26534
+ }
26535
+ throw err;
26536
+ }
23916
26537
  const stateStore = new StateStore(storage, masterKey);
23917
26538
  const { tools: l1Tools, identityManager } = createL1Tools(
23918
26539
  stateStore,
@@ -24186,7 +26807,9 @@ async function createSanctuaryServer(options) {
24186
26807
  );
24187
26808
  const { tools: auditTools } = createAuditTools(config);
24188
26809
  const { tools: siemTools } = createSIEMTools(auditLog);
24189
- const { tools: contextGateTools, enforcer: contextGateEnforcer } = createContextGateTools(storage, masterKey, auditLog);
26810
+ const { tools: contextGateTools, enforcer: contextGateEnforcer } = createContextGateTools(storage, masterKey, auditLog, {
26811
+ privacyFilter: config.privacy_filter
26812
+ });
24190
26813
  const hardeningTools = createL2HardeningTools(config.storage_path, auditLog);
24191
26814
  const profileStore = new SovereigntyProfileStore(storage, masterKey);
24192
26815
  await profileStore.load();
@@ -24375,7 +26998,7 @@ async function createSanctuaryServer(options) {
24375
26998
  clientManager.configure(enabledServers).catch((err) => {
24376
26999
  console.error(`[Sanctuary] Failed to configure upstream servers: ${err instanceof Error ? err.message : "unknown error"}`);
24377
27000
  });
24378
- await new Promise((resolve2) => setTimeout(resolve2, 2e3));
27001
+ await new Promise((resolve4) => setTimeout(resolve4, 2e3));
24379
27002
  const proxiedTools = proxyRouter.getProxiedTools();
24380
27003
  if (proxiedTools.length > 0) {
24381
27004
  allTools.push(...proxiedTools);
@@ -24417,7 +27040,7 @@ async function createSanctuaryServer(options) {
24417
27040
  if (recoveryKey) {
24418
27041
  console.error(
24419
27042
  `\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\u2557
24420
- \u2551 SANCTUARY: First Run \u2014 Recovery Key Generated \u2551
27043
+ \u2551 SANCTUARY: First Run, Recovery Key Generated \u2551
24421
27044
  \u2551 \u2551
24422
27045
  \u2551 Recovery Key: ${recoveryKey.slice(0, 20)}... \u2551
24423
27046
  \u2551 \u2551
@@ -24474,20 +27097,26 @@ exports.createProofOfKnowledge = createProofOfKnowledge;
24474
27097
  exports.createRangeProof = createRangeProof;
24475
27098
  exports.createSanctuaryServer = createSanctuaryServer;
24476
27099
  exports.evaluateField = evaluateField;
27100
+ exports.exitBundleManifestShape = exitBundleManifestShape;
27101
+ exports.exportExitBundle = exportExitBundle;
24477
27102
  exports.filterContext = filterContext;
24478
27103
  exports.generateAttestation = generateAttestation;
24479
27104
  exports.generateSHR = generateSHR;
24480
27105
  exports.generateSystemPrompt = generateSystemPrompt;
24481
27106
  exports.getProtectionSnapshot = getProtectionSnapshot;
24482
27107
  exports.getTemplate = getTemplate;
27108
+ exports.importExitBundle = importExitBundle;
24483
27109
  exports.initiateHandshake = initiateHandshake;
24484
27110
  exports.listTemplateIds = listTemplateIds;
24485
27111
  exports.loadConfig = loadConfig;
27112
+ exports.loadExitArtifact = loadExitArtifact;
24486
27113
  exports.loadPrincipalPolicy = loadPrincipalPolicy;
27114
+ exports.readManifest = readManifest;
24487
27115
  exports.recommendPolicy = recommendPolicy;
24488
27116
  exports.renderDashboardHTML = renderDashboardHTML;
24489
27117
  exports.resolveTier = resolveTier;
24490
27118
  exports.respondToHandshake = respondToHandshake;
27119
+ exports.runExitCommand = runExitCommand;
24491
27120
  exports.signPayload = signPayload;
24492
27121
  exports.startDashboard = startDashboard;
24493
27122
  exports.startDashboardServer = startDashboardServer;
@@ -24495,6 +27124,7 @@ exports.tierDistribution = tierDistribution;
24495
27124
  exports.verifyAttestation = verifyAttestation;
24496
27125
  exports.verifyBridgeCommitment = verifyBridgeCommitment;
24497
27126
  exports.verifyCompletion = verifyCompletion;
27127
+ exports.verifyExitBundle = verifyExitBundle;
24498
27128
  exports.verifyPedersenCommitment = verifyPedersenCommitment;
24499
27129
  exports.verifyProofOfKnowledge = verifyProofOfKnowledge;
24500
27130
  exports.verifyRangeProof = verifyRangeProof;