@sanctuary-framework/mcp-server 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { sha256 } from '@noble/hashes/sha256';
2
2
  import { hmac } from '@noble/hashes/hmac';
3
- import { readFile, mkdir, writeFile, stat, unlink, readdir, chmod } from 'fs/promises';
3
+ import { readFile, mkdir, writeFile, stat, unlink, readdir, chmod, access } from 'fs/promises';
4
4
  import { join } from 'path';
5
5
  import { homedir } from 'os';
6
+ import { createRequire } from 'module';
6
7
  import { randomBytes as randomBytes$1, createHmac } from 'crypto';
7
8
  import { gcm } from '@noble/ciphers/aes.js';
8
9
  import { RistrettoPoint, ed25519 } from '@noble/curves/ed25519';
@@ -12,7 +13,8 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
12
13
  import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
13
14
  import { createServer as createServer$2 } from 'http';
14
15
  import { createServer as createServer$1 } from 'https';
15
- import { readFileSync } from 'fs';
16
+ import { readFileSync, statSync } from 'fs';
17
+ import { execSync } from 'child_process';
16
18
 
17
19
  var __defProp = Object.defineProperty;
18
20
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -202,9 +204,11 @@ var init_hashing = __esm({
202
204
  init_encoding();
203
205
  }
204
206
  });
207
+ var require2 = createRequire(import.meta.url);
208
+ var { version: PKG_VERSION } = require2("../package.json");
205
209
  function defaultConfig() {
206
210
  return {
207
- version: "0.3.0",
211
+ version: PKG_VERSION,
208
212
  storage_path: join(homedir(), ".sanctuary"),
209
213
  state: {
210
214
  encryption: "aes-256-gcm",
@@ -296,8 +300,13 @@ async function loadConfig(configPath) {
296
300
  try {
297
301
  const raw = await readFile(path, "utf-8");
298
302
  const fileConfig = JSON.parse(raw);
299
- return deepMerge(config, fileConfig);
300
- } catch {
303
+ const merged = deepMerge(config, fileConfig);
304
+ validateConfig(merged);
305
+ return merged;
306
+ } catch (err) {
307
+ if (err instanceof Error && err.message.includes("unimplemented features")) {
308
+ throw err;
309
+ }
301
310
  return config;
302
311
  }
303
312
  }
@@ -305,6 +314,45 @@ async function saveConfig(config, configPath) {
305
314
  const path = join(config.storage_path, "sanctuary.json");
306
315
  await writeFile(path, JSON.stringify(config, null, 2), { mode: 384 });
307
316
  }
317
+ function validateConfig(config) {
318
+ const errors = [];
319
+ const implementedKeyProtection = /* @__PURE__ */ new Set(["passphrase", "none"]);
320
+ if (!implementedKeyProtection.has(config.state.key_protection)) {
321
+ errors.push(
322
+ `Unimplemented config value: state.key_protection = "${config.state.key_protection}". Only ${[...implementedKeyProtection].map((v) => `"${v}"`).join(", ")} are currently implemented. Using an unimplemented key protection mode would silently degrade security.`
323
+ );
324
+ }
325
+ const implementedEnvironment = /* @__PURE__ */ new Set(["local-process", "docker"]);
326
+ if (!implementedEnvironment.has(config.execution.environment)) {
327
+ errors.push(
328
+ `Unimplemented config value: execution.environment = "${config.execution.environment}". Only ${[...implementedEnvironment].map((v) => `"${v}"`).join(", ")} are currently implemented. Using an unimplemented environment would silently degrade security.`
329
+ );
330
+ }
331
+ const implementedProofSystem = /* @__PURE__ */ new Set(["commitment-only"]);
332
+ if (!implementedProofSystem.has(config.disclosure.proof_system)) {
333
+ errors.push(
334
+ `Unimplemented config value: disclosure.proof_system = "${config.disclosure.proof_system}". Only ${[...implementedProofSystem].map((v) => `"${v}"`).join(", ")} is currently implemented. Using an unimplemented proof system would silently degrade security.`
335
+ );
336
+ }
337
+ const implementedDisclosurePolicy = /* @__PURE__ */ new Set(["minimum-necessary"]);
338
+ if (!implementedDisclosurePolicy.has(config.disclosure.default_policy)) {
339
+ errors.push(
340
+ `Unimplemented config value: disclosure.default_policy = "${config.disclosure.default_policy}". Only ${[...implementedDisclosurePolicy].map((v) => `"${v}"`).join(", ")} is currently implemented. Using an unimplemented disclosure policy would silently skip disclosure controls.`
341
+ );
342
+ }
343
+ const implementedReputationMode = /* @__PURE__ */ new Set(["self-custodied"]);
344
+ if (!implementedReputationMode.has(config.reputation.mode)) {
345
+ errors.push(
346
+ `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.`
347
+ );
348
+ }
349
+ if (errors.length > 0) {
350
+ throw new Error(
351
+ `Sanctuary configuration references unimplemented features:
352
+ ${errors.join("\n")}`
353
+ );
354
+ }
355
+ }
308
356
  function deepMerge(base, override) {
309
357
  const result = { ...base };
310
358
  for (const [key, value] of Object.entries(override)) {
@@ -648,7 +696,11 @@ var RESERVED_NAMESPACE_PREFIXES = [
648
696
  "_commitments",
649
697
  "_reputation",
650
698
  "_escrow",
651
- "_guarantees"
699
+ "_guarantees",
700
+ "_bridge",
701
+ "_federation",
702
+ "_handshake",
703
+ "_shr"
652
704
  ];
653
705
  var StateStore = class {
654
706
  storage;
@@ -915,12 +967,14 @@ var StateStore = class {
915
967
  /**
916
968
  * Import a previously exported state bundle.
917
969
  */
918
- async import(bundleBase64, conflictResolution = "skip") {
970
+ async import(bundleBase64, conflictResolution = "skip", publicKeyResolver) {
919
971
  const bundleBytes = fromBase64url(bundleBase64);
920
972
  const bundleJson = bytesToString(bundleBytes);
921
973
  const bundle = JSON.parse(bundleJson);
922
974
  let importedKeys = 0;
923
975
  let skippedKeys = 0;
976
+ let skippedInvalidSig = 0;
977
+ let skippedUnknownKid = 0;
924
978
  let conflicts = 0;
925
979
  const namespaces = [];
926
980
  for (const [ns, entries] of Object.entries(
@@ -934,6 +988,26 @@ var StateStore = class {
934
988
  }
935
989
  namespaces.push(ns);
936
990
  for (const { key, entry } of entries) {
991
+ const signerPublicKey = publicKeyResolver(entry.kid);
992
+ if (!signerPublicKey) {
993
+ skippedUnknownKid++;
994
+ skippedKeys++;
995
+ continue;
996
+ }
997
+ try {
998
+ const ciphertextBytes = fromBase64url(entry.payload.ct);
999
+ const signatureBytes = fromBase64url(entry.sig);
1000
+ const sigValid = verify(ciphertextBytes, signatureBytes, signerPublicKey);
1001
+ if (!sigValid) {
1002
+ skippedInvalidSig++;
1003
+ skippedKeys++;
1004
+ continue;
1005
+ }
1006
+ } catch {
1007
+ skippedInvalidSig++;
1008
+ skippedKeys++;
1009
+ continue;
1010
+ }
937
1011
  const exists = await this.storage.exists(ns, key);
938
1012
  if (exists) {
939
1013
  conflicts++;
@@ -969,12 +1043,16 @@ var StateStore = class {
969
1043
  return {
970
1044
  imported_keys: importedKeys,
971
1045
  skipped_keys: skippedKeys,
1046
+ skipped_invalid_sig: skippedInvalidSig,
1047
+ skipped_unknown_kid: skippedUnknownKid,
972
1048
  conflicts,
973
1049
  namespaces,
974
1050
  imported_at: (/* @__PURE__ */ new Date()).toISOString()
975
1051
  };
976
1052
  }
977
1053
  };
1054
+ var require3 = createRequire(import.meta.url);
1055
+ var { version: PKG_VERSION2 } = require3("../package.json");
978
1056
  var MAX_STRING_BYTES = 1048576;
979
1057
  var MAX_BUNDLE_BYTES = 5242880;
980
1058
  var BUNDLE_FIELDS = /* @__PURE__ */ new Set(["bundle"]);
@@ -1057,7 +1135,7 @@ function createServer(tools, options) {
1057
1135
  const server = new Server(
1058
1136
  {
1059
1137
  name: "sanctuary-mcp-server",
1060
- version: "0.3.0"
1138
+ version: PKG_VERSION2
1061
1139
  },
1062
1140
  {
1063
1141
  capabilities: {
@@ -1157,7 +1235,11 @@ var RESERVED_NAMESPACE_PREFIXES2 = [
1157
1235
  "_commitments",
1158
1236
  "_reputation",
1159
1237
  "_escrow",
1160
- "_guarantees"
1238
+ "_guarantees",
1239
+ "_bridge",
1240
+ "_federation",
1241
+ "_handshake",
1242
+ "_shr"
1161
1243
  ];
1162
1244
  function getReservedNamespaceViolation(namespace) {
1163
1245
  for (const prefix of RESERVED_NAMESPACE_PREFIXES2) {
@@ -1494,6 +1576,13 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1494
1576
  required: ["namespace", "key"]
1495
1577
  },
1496
1578
  handler: async (args) => {
1579
+ const reservedViolation = getReservedNamespaceViolation(args.namespace);
1580
+ if (reservedViolation) {
1581
+ return toolResult({
1582
+ error: "namespace_reserved",
1583
+ message: `Namespace "${args.namespace}" is reserved for internal use (prefix: ${reservedViolation}). Cannot read from reserved namespaces.`
1584
+ });
1585
+ }
1497
1586
  const result = await stateStore.read(
1498
1587
  args.namespace,
1499
1588
  args.key,
@@ -1530,6 +1619,13 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1530
1619
  required: ["namespace"]
1531
1620
  },
1532
1621
  handler: async (args) => {
1622
+ const reservedViolation = getReservedNamespaceViolation(args.namespace);
1623
+ if (reservedViolation) {
1624
+ return toolResult({
1625
+ error: "namespace_reserved",
1626
+ message: `Namespace "${args.namespace}" is reserved for internal use (prefix: ${reservedViolation}). Cannot list reserved namespaces.`
1627
+ });
1628
+ }
1533
1629
  const result = await stateStore.list(
1534
1630
  args.namespace,
1535
1631
  args.prefix,
@@ -1608,9 +1704,15 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1608
1704
  required: ["bundle"]
1609
1705
  },
1610
1706
  handler: async (args) => {
1707
+ const publicKeyResolver = (kid) => {
1708
+ const identity = identityMgr.get(kid);
1709
+ if (!identity) return null;
1710
+ return fromBase64url(identity.public_key);
1711
+ };
1611
1712
  const result = await stateStore.import(
1612
1713
  args.bundle,
1613
- args.conflict_resolution ?? "skip"
1714
+ args.conflict_resolution ?? "skip",
1715
+ publicKeyResolver
1614
1716
  );
1615
1717
  auditLog?.append("l1", "state_import", "principal", {
1616
1718
  imported_keys: result.imported_keys
@@ -2055,7 +2157,7 @@ function createRangeProof(value, blindingFactor, commitment, min, max) {
2055
2157
  bitProofs.push(bitProof);
2056
2158
  }
2057
2159
  const sumBlinding = bitBlindings.reduce(
2058
- (acc, bi, i) => mod(acc + mod(BigInt(1 << i)) * bi),
2160
+ (acc, bi, i) => mod(acc + mod(BigInt(1) << BigInt(i)) * bi),
2059
2161
  0n
2060
2162
  );
2061
2163
  const blindingDiff = mod(b - sumBlinding);
@@ -2097,7 +2199,7 @@ function verifyRangeProof(proof) {
2097
2199
  let reconstructed = RistrettoPoint.ZERO;
2098
2200
  for (let i = 0; i < numBits; i++) {
2099
2201
  const C_i = RistrettoPoint.fromHex(fromBase64url(proof.bit_commitments[i]));
2100
- const weight = mod(BigInt(1 << i));
2202
+ const weight = mod(BigInt(1) << BigInt(i));
2101
2203
  reconstructed = reconstructed.add(safeMultiply(C_i, weight));
2102
2204
  }
2103
2205
  const diff = C.subtract(safeMultiply(G, mod(BigInt(proof.min)))).subtract(reconstructed);
@@ -3152,7 +3254,9 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
3152
3254
  contexts: summary.contexts
3153
3255
  });
3154
3256
  return toolResult({
3155
- summary
3257
+ summary,
3258
+ // SEC-ADD-03: Tag response as containing counterparty-generated attestation data
3259
+ _content_trust: "external"
3156
3260
  });
3157
3261
  }
3158
3262
  },
@@ -3473,24 +3577,27 @@ var DEFAULT_TIER2 = {
3473
3577
  };
3474
3578
  var DEFAULT_CHANNEL = {
3475
3579
  type: "stderr",
3476
- timeout_seconds: 300,
3477
- auto_deny: true
3580
+ timeout_seconds: 300
3581
+ // SEC-002: auto_deny is not configurable. Timeout always denies.
3582
+ // Field omitted intentionally — all channels hardcode deny on timeout.
3478
3583
  };
3479
3584
  var DEFAULT_POLICY = {
3480
3585
  version: 1,
3481
3586
  tier1_always_approve: [
3482
3587
  "state_export",
3483
3588
  "state_import",
3589
+ "state_delete",
3484
3590
  "identity_rotate",
3485
3591
  "reputation_import",
3486
- "bootstrap_provide_guarantee"
3592
+ "reputation_export",
3593
+ "bootstrap_provide_guarantee",
3594
+ "decommission_certificate"
3487
3595
  ],
3488
3596
  tier2_anomaly: DEFAULT_TIER2,
3489
3597
  tier3_always_allow: [
3490
3598
  "state_read",
3491
3599
  "state_write",
3492
3600
  "state_list",
3493
- "state_delete",
3494
3601
  "identity_create",
3495
3602
  "identity_list",
3496
3603
  "identity_sign",
@@ -3501,7 +3608,6 @@ var DEFAULT_POLICY = {
3501
3608
  "disclosure_evaluate",
3502
3609
  "reputation_record",
3503
3610
  "reputation_query",
3504
- "reputation_export",
3505
3611
  "bootstrap_create_escrow",
3506
3612
  "exec_attest",
3507
3613
  "monitor_health",
@@ -3523,7 +3629,14 @@ var DEFAULT_POLICY = {
3523
3629
  "zk_prove",
3524
3630
  "zk_verify",
3525
3631
  "zk_range_prove",
3526
- "zk_range_verify"
3632
+ "zk_range_verify",
3633
+ "context_gate_set_policy",
3634
+ "context_gate_apply_template",
3635
+ "context_gate_recommend",
3636
+ "context_gate_filter",
3637
+ "context_gate_list_policies",
3638
+ "l2_hardening_status",
3639
+ "l2_verify_isolation"
3527
3640
  ],
3528
3641
  approval_channel: DEFAULT_CHANNEL
3529
3642
  };
@@ -3598,10 +3711,14 @@ function validatePolicy(raw) {
3598
3711
  ...raw.tier2_anomaly ?? {}
3599
3712
  },
3600
3713
  tier3_always_allow: raw.tier3_always_allow ?? DEFAULT_POLICY.tier3_always_allow,
3601
- approval_channel: {
3602
- ...DEFAULT_CHANNEL,
3603
- ...raw.approval_channel ?? {}
3604
- }
3714
+ approval_channel: (() => {
3715
+ const merged = {
3716
+ ...DEFAULT_CHANNEL,
3717
+ ...raw.approval_channel ?? {}
3718
+ };
3719
+ delete merged.auto_deny;
3720
+ return merged;
3721
+ })()
3605
3722
  };
3606
3723
  }
3607
3724
  function generateDefaultPolicyYaml() {
@@ -3618,8 +3735,10 @@ version: 1
3618
3735
  tier1_always_approve:
3619
3736
  - state_export
3620
3737
  - state_import
3738
+ - state_delete
3621
3739
  - identity_rotate
3622
3740
  - reputation_import
3741
+ - reputation_export
3623
3742
  - bootstrap_provide_guarantee
3624
3743
 
3625
3744
  # \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
@@ -3639,7 +3758,6 @@ tier3_always_allow:
3639
3758
  - state_read
3640
3759
  - state_write
3641
3760
  - state_list
3642
- - state_delete
3643
3761
  - identity_create
3644
3762
  - identity_list
3645
3763
  - identity_sign
@@ -3650,7 +3768,6 @@ tier3_always_allow:
3650
3768
  - disclosure_evaluate
3651
3769
  - reputation_record
3652
3770
  - reputation_query
3653
- - reputation_export
3654
3771
  - bootstrap_create_escrow
3655
3772
  - exec_attest
3656
3773
  - monitor_health
@@ -3673,13 +3790,18 @@ tier3_always_allow:
3673
3790
  - zk_verify
3674
3791
  - zk_range_prove
3675
3792
  - zk_range_verify
3793
+ - context_gate_set_policy
3794
+ - context_gate_apply_template
3795
+ - context_gate_recommend
3796
+ - context_gate_filter
3797
+ - context_gate_list_policies
3676
3798
 
3677
3799
  # \u2500\u2500\u2500 Approval Channel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3678
3800
  # How Sanctuary reaches you when approval is needed.
3801
+ # NOTE: Timeout always results in denial. This is not configurable (SEC-002).
3679
3802
  approval_channel:
3680
3803
  type: stderr
3681
3804
  timeout_seconds: 300
3682
- auto_deny: true
3683
3805
  `;
3684
3806
  }
3685
3807
  async function loadPrincipalPolicy(storagePath) {
@@ -3856,27 +3978,16 @@ var BaselineTracker = class {
3856
3978
 
3857
3979
  // src/principal-policy/approval-channel.ts
3858
3980
  var StderrApprovalChannel = class {
3859
- config;
3860
- constructor(config) {
3861
- this.config = config;
3981
+ constructor(_config) {
3862
3982
  }
3863
3983
  async requestApproval(request) {
3864
3984
  const prompt = this.formatPrompt(request);
3865
3985
  process.stderr.write(prompt + "\n");
3866
- await new Promise((resolve) => setTimeout(resolve, 100));
3867
- if (this.config.auto_deny) {
3868
- return {
3869
- decision: "deny",
3870
- decided_at: (/* @__PURE__ */ new Date()).toISOString(),
3871
- decided_by: "timeout"
3872
- };
3873
- } else {
3874
- return {
3875
- decision: "approve",
3876
- decided_at: (/* @__PURE__ */ new Date()).toISOString(),
3877
- decided_by: "auto"
3878
- };
3879
- }
3986
+ return {
3987
+ decision: "deny",
3988
+ decided_at: (/* @__PURE__ */ new Date()).toISOString(),
3989
+ decided_by: "stderr:non-interactive"
3990
+ };
3880
3991
  }
3881
3992
  formatPrompt(request) {
3882
3993
  const tierLabel = request.tier === 1 ? "Tier 1 \u2014 always requires approval" : "Tier 2 \u2014 behavioral anomaly detected";
@@ -3884,7 +3995,7 @@ var StderrApprovalChannel = class {
3884
3995
  return [
3885
3996
  "",
3886
3997
  "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
3887
- "\u2551 SANCTUARY: Approval Required \u2551",
3998
+ "\u2551 SANCTUARY: Operation Denied (non-interactive channel) \u2551",
3888
3999
  "\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563",
3889
4000
  `\u2551 Operation: ${request.operation.padEnd(50)}\u2551`,
3890
4001
  `\u2551 ${tierLabel.padEnd(62)}\u2551`,
@@ -3895,7 +4006,8 @@ var StderrApprovalChannel = class {
3895
4006
  (line) => `\u2551 ${line.padEnd(60)}\u2551`
3896
4007
  ),
3897
4008
  "\u2551 \u2551",
3898
- this.config.auto_deny ? "\u2551 Auto-denying (configure approval_channel.auto_deny to change) \u2551" : "\u2551 Auto-approving (informational mode) \u2551",
4009
+ "\u2551 Denied: stderr channel cannot accept input (SEC-016) \u2551",
4010
+ "\u2551 Use dashboard or webhook channel for interactive approval. \u2551",
3899
4011
  "\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
3900
4012
  ""
3901
4013
  ].join("\n");
@@ -4217,20 +4329,38 @@ function generateDashboardHTML(options) {
4217
4329
  <script>
4218
4330
  (function() {
4219
4331
  const TIMEOUT = ${options.timeoutSeconds};
4220
- const AUTH_TOKEN = ${options.authToken ? `'${options.authToken}'` : "null"};
4332
+ // SEC-012: Auth token is passed via Authorization header only \u2014 never in URLs.
4333
+ // The token is provided by the server at generation time (embedded for initial auth).
4334
+ const AUTH_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
4335
+ let SESSION_ID = null; // Short-lived session for SSE and URL-based requests
4221
4336
  const pending = new Map();
4222
4337
  let auditCount = 0;
4223
4338
 
4224
- // Auth helpers
4339
+ // Auth helpers \u2014 SEC-012: token goes in header, session goes in URL
4225
4340
  function authHeaders() {
4226
4341
  const h = { 'Content-Type': 'application/json' };
4227
4342
  if (AUTH_TOKEN) h['Authorization'] = 'Bearer ' + AUTH_TOKEN;
4228
4343
  return h;
4229
4344
  }
4230
- function authQuery(url) {
4231
- if (!AUTH_TOKEN) return url;
4345
+ function sessionQuery(url) {
4346
+ if (!SESSION_ID) return url;
4232
4347
  const sep = url.includes('?') ? '&' : '?';
4233
- return url + sep + 'token=' + AUTH_TOKEN;
4348
+ return url + sep + 'session=' + SESSION_ID;
4349
+ }
4350
+
4351
+ // SEC-012: Exchange the long-lived token for a short-lived session
4352
+ async function exchangeSession() {
4353
+ if (!AUTH_TOKEN) return;
4354
+ try {
4355
+ const resp = await fetch('/auth/session', { method: 'POST', headers: authHeaders() });
4356
+ if (resp.ok) {
4357
+ const data = await resp.json();
4358
+ SESSION_ID = data.session_id;
4359
+ // Refresh session before expiry (at 80% of TTL)
4360
+ const refreshMs = (data.expires_in_seconds || 300) * 800;
4361
+ setTimeout(async () => { await exchangeSession(); reconnectSSE(); }, refreshMs);
4362
+ }
4363
+ } catch(e) { /* will retry on next connect */ }
4234
4364
  }
4235
4365
 
4236
4366
  // Tab switching
@@ -4243,10 +4373,14 @@ function generateDashboardHTML(options) {
4243
4373
  });
4244
4374
  });
4245
4375
 
4246
- // SSE Connection
4376
+ // SSE Connection \u2014 SEC-012: uses short-lived session token in URL, not auth token
4247
4377
  let evtSource;
4378
+ function reconnectSSE() {
4379
+ if (evtSource) { evtSource.close(); }
4380
+ connect();
4381
+ }
4248
4382
  function connect() {
4249
- evtSource = new EventSource(authQuery('/events'));
4383
+ evtSource = new EventSource(sessionQuery('/events'));
4250
4384
  evtSource.onopen = () => {
4251
4385
  document.getElementById('statusDot').classList.remove('disconnected');
4252
4386
  document.getElementById('statusText').textContent = 'Connected';
@@ -4434,12 +4568,20 @@ function generateDashboardHTML(options) {
4434
4568
  return d.innerHTML;
4435
4569
  }
4436
4570
 
4437
- // Init
4438
- connect();
4439
- fetch('/api/status', { headers: authHeaders() }).then(r => r.json()).then(data => {
4440
- if (data.baseline) updateBaseline(data.baseline);
4441
- if (data.policy) updatePolicy(data.policy);
4442
- }).catch(() => {});
4571
+ // Init \u2014 SEC-012: exchange token for session before connecting SSE
4572
+ (async function init() {
4573
+ await exchangeSession();
4574
+ // Clean token from URL if present (legacy bookmarks)
4575
+ if (window.location.search.includes('token=')) {
4576
+ const clean = window.location.pathname;
4577
+ window.history.replaceState({}, '', clean);
4578
+ }
4579
+ connect();
4580
+ fetch('/api/status', { headers: authHeaders() }).then(r => r.json()).then(data => {
4581
+ if (data.baseline) updateBaseline(data.baseline);
4582
+ if (data.policy) updatePolicy(data.policy);
4583
+ }).catch(() => {});
4584
+ })();
4443
4585
  })();
4444
4586
  </script>
4445
4587
  </body>
@@ -4447,6 +4589,14 @@ function generateDashboardHTML(options) {
4447
4589
  }
4448
4590
 
4449
4591
  // src/principal-policy/dashboard.ts
4592
+ var require4 = createRequire(import.meta.url);
4593
+ var { version: PKG_VERSION3 } = require4("../../package.json");
4594
+ var SESSION_TTL_MS = 5 * 60 * 1e3;
4595
+ var MAX_SESSIONS = 1e3;
4596
+ var RATE_LIMIT_WINDOW_MS = 6e4;
4597
+ var RATE_LIMIT_GENERAL = 120;
4598
+ var RATE_LIMIT_DECISIONS = 20;
4599
+ var MAX_RATE_LIMIT_ENTRIES = 1e4;
4450
4600
  var DashboardApprovalChannel = class {
4451
4601
  config;
4452
4602
  pending = /* @__PURE__ */ new Map();
@@ -4458,15 +4608,21 @@ var DashboardApprovalChannel = class {
4458
4608
  dashboardHTML;
4459
4609
  authToken;
4460
4610
  useTLS;
4611
+ /** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
4612
+ sessions = /* @__PURE__ */ new Map();
4613
+ sessionCleanupTimer = null;
4614
+ /** Rate limiting: per-IP request tracking */
4615
+ rateLimits = /* @__PURE__ */ new Map();
4461
4616
  constructor(config) {
4462
4617
  this.config = config;
4463
4618
  this.authToken = config.auth_token;
4464
4619
  this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
4465
4620
  this.dashboardHTML = generateDashboardHTML({
4466
4621
  timeoutSeconds: config.timeout_seconds,
4467
- serverVersion: "0.3.0",
4622
+ serverVersion: PKG_VERSION3,
4468
4623
  authToken: this.authToken
4469
4624
  });
4625
+ this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
4470
4626
  }
4471
4627
  /**
4472
4628
  * Inject dependencies after construction.
@@ -4496,13 +4652,14 @@ var DashboardApprovalChannel = class {
4496
4652
  const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
4497
4653
  this.httpServer.listen(this.config.port, this.config.host, () => {
4498
4654
  if (this.authToken) {
4655
+ const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
4499
4656
  process.stderr.write(
4500
4657
  `
4501
- Sanctuary Principal Dashboard: ${baseUrl}/?token=${this.authToken}
4658
+ Sanctuary Principal Dashboard: ${baseUrl}
4502
4659
  `
4503
4660
  );
4504
4661
  process.stderr.write(
4505
- ` Auth token: ${this.authToken}
4662
+ ` Auth required (token: ${hint}). Use Authorization: Bearer <TOKEN> header.
4506
4663
 
4507
4664
  `
4508
4665
  );
@@ -4536,6 +4693,12 @@ var DashboardApprovalChannel = class {
4536
4693
  client.end();
4537
4694
  }
4538
4695
  this.sseClients.clear();
4696
+ this.sessions.clear();
4697
+ if (this.sessionCleanupTimer) {
4698
+ clearInterval(this.sessionCleanupTimer);
4699
+ this.sessionCleanupTimer = null;
4700
+ }
4701
+ this.rateLimits.clear();
4539
4702
  if (this.httpServer) {
4540
4703
  return new Promise((resolve) => {
4541
4704
  this.httpServer.close(() => resolve());
@@ -4556,7 +4719,8 @@ var DashboardApprovalChannel = class {
4556
4719
  const timer = setTimeout(() => {
4557
4720
  this.pending.delete(id);
4558
4721
  const response = {
4559
- decision: this.config.auto_deny ? "deny" : "approve",
4722
+ // SEC-002: Timeout ALWAYS denies. No configuration can change this.
4723
+ decision: "deny",
4560
4724
  decided_at: (/* @__PURE__ */ new Date()).toISOString(),
4561
4725
  decided_by: "timeout"
4562
4726
  };
@@ -4588,7 +4752,12 @@ var DashboardApprovalChannel = class {
4588
4752
  // ── Authentication ──────────────────────────────────────────────────
4589
4753
  /**
4590
4754
  * Verify bearer token authentication.
4591
- * Checks Authorization header first, falls back to ?token= query param.
4755
+ *
4756
+ * SEC-012: The long-lived auth token is ONLY accepted via the Authorization
4757
+ * header — never in URL query strings. For SSE and page loads that cannot
4758
+ * set headers, a short-lived session token (obtained via POST /auth/session)
4759
+ * is accepted via ?session= query parameter.
4760
+ *
4592
4761
  * Returns true if auth passes, false if blocked (response already sent).
4593
4762
  */
4594
4763
  checkAuth(req, url, res) {
@@ -4600,19 +4769,126 @@ var DashboardApprovalChannel = class {
4600
4769
  return true;
4601
4770
  }
4602
4771
  }
4603
- const queryToken = url.searchParams.get("token");
4604
- if (queryToken === this.authToken) {
4772
+ const sessionId = url.searchParams.get("session");
4773
+ if (sessionId && this.validateSession(sessionId)) {
4605
4774
  return true;
4606
4775
  }
4607
4776
  res.writeHead(401, { "Content-Type": "application/json" });
4608
- res.end(JSON.stringify({ error: "Unauthorized \u2014 valid bearer token required" }));
4777
+ res.end(JSON.stringify({ error: "Unauthorized \u2014 use Authorization: Bearer header or a valid session" }));
4609
4778
  return false;
4610
4779
  }
4780
+ // ── Session Management (SEC-012) ──────────────────────────────────
4781
+ /**
4782
+ * Create a short-lived session by exchanging the long-lived auth token
4783
+ * (provided in the Authorization header) for a session ID.
4784
+ */
4785
+ createSession() {
4786
+ if (this.sessions.size >= MAX_SESSIONS) {
4787
+ this.cleanupSessions();
4788
+ if (this.sessions.size >= MAX_SESSIONS) {
4789
+ const oldest = [...this.sessions.entries()].sort(
4790
+ (a, b) => a[1].created_at - b[1].created_at
4791
+ )[0];
4792
+ if (oldest) this.sessions.delete(oldest[0]);
4793
+ }
4794
+ }
4795
+ const id = randomBytes$1(32).toString("hex");
4796
+ const now = Date.now();
4797
+ this.sessions.set(id, {
4798
+ id,
4799
+ created_at: now,
4800
+ expires_at: now + SESSION_TTL_MS
4801
+ });
4802
+ return id;
4803
+ }
4804
+ /**
4805
+ * Validate a session ID — must exist and not be expired.
4806
+ */
4807
+ validateSession(sessionId) {
4808
+ const session = this.sessions.get(sessionId);
4809
+ if (!session) return false;
4810
+ if (Date.now() > session.expires_at) {
4811
+ this.sessions.delete(sessionId);
4812
+ return false;
4813
+ }
4814
+ return true;
4815
+ }
4816
+ /**
4817
+ * Remove all expired sessions.
4818
+ */
4819
+ cleanupSessions() {
4820
+ const now = Date.now();
4821
+ for (const [id, session] of this.sessions) {
4822
+ if (now > session.expires_at) {
4823
+ this.sessions.delete(id);
4824
+ }
4825
+ }
4826
+ }
4827
+ // ── Rate Limiting ─────────────────────────────────────────────────
4828
+ /**
4829
+ * Get the remote address from a request, normalizing IPv6-mapped IPv4.
4830
+ */
4831
+ getRemoteAddr(req) {
4832
+ const addr = req.socket.remoteAddress ?? "unknown";
4833
+ return addr.startsWith("::ffff:") ? addr.slice(7) : addr;
4834
+ }
4835
+ /**
4836
+ * Check rate limit for a request. Returns true if allowed, false if rate-limited.
4837
+ * When rate-limited, sends a 429 response.
4838
+ */
4839
+ checkRateLimit(req, res, type) {
4840
+ const addr = this.getRemoteAddr(req);
4841
+ const now = Date.now();
4842
+ const windowStart = now - RATE_LIMIT_WINDOW_MS;
4843
+ let entry = this.rateLimits.get(addr);
4844
+ if (!entry) {
4845
+ if (this.rateLimits.size >= MAX_RATE_LIMIT_ENTRIES) {
4846
+ this.pruneRateLimits(now);
4847
+ }
4848
+ entry = { general: [], decisions: [] };
4849
+ this.rateLimits.set(addr, entry);
4850
+ }
4851
+ entry.general = entry.general.filter((t) => t > windowStart);
4852
+ entry.decisions = entry.decisions.filter((t) => t > windowStart);
4853
+ const limit = type === "decisions" ? RATE_LIMIT_DECISIONS : RATE_LIMIT_GENERAL;
4854
+ const timestamps = entry[type];
4855
+ if (timestamps.length >= limit) {
4856
+ const retryAfter = Math.ceil((timestamps[0] + RATE_LIMIT_WINDOW_MS - now) / 1e3);
4857
+ res.writeHead(429, {
4858
+ "Content-Type": "application/json",
4859
+ "Retry-After": String(Math.max(1, retryAfter))
4860
+ });
4861
+ res.end(JSON.stringify({
4862
+ error: "Rate limit exceeded",
4863
+ retry_after_seconds: Math.max(1, retryAfter)
4864
+ }));
4865
+ return false;
4866
+ }
4867
+ timestamps.push(now);
4868
+ return true;
4869
+ }
4870
+ /**
4871
+ * Remove stale entries from the rate limit map.
4872
+ */
4873
+ pruneRateLimits(now) {
4874
+ const windowStart = now - RATE_LIMIT_WINDOW_MS;
4875
+ for (const [addr, entry] of this.rateLimits) {
4876
+ const hasRecent = entry.general.some((t) => t > windowStart) || entry.decisions.some((t) => t > windowStart);
4877
+ if (!hasRecent) {
4878
+ this.rateLimits.delete(addr);
4879
+ }
4880
+ }
4881
+ }
4611
4882
  // ── HTTP Request Handler ────────────────────────────────────────────
4612
4883
  handleRequest(req, res) {
4613
4884
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
4614
4885
  const method = req.method ?? "GET";
4615
- res.setHeader("Access-Control-Allow-Origin", "*");
4886
+ const origin = req.headers.origin;
4887
+ const protocol = this.useTLS ? "https" : "http";
4888
+ const selfOrigin = `${protocol}://${this.config.host}:${this.config.port}`;
4889
+ if (origin === selfOrigin) {
4890
+ res.setHeader("Access-Control-Allow-Origin", origin);
4891
+ }
4616
4892
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
4617
4893
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
4618
4894
  if (method === "OPTIONS") {
@@ -4621,7 +4897,12 @@ var DashboardApprovalChannel = class {
4621
4897
  return;
4622
4898
  }
4623
4899
  if (!this.checkAuth(req, url, res)) return;
4900
+ if (!this.checkRateLimit(req, res, "general")) return;
4624
4901
  try {
4902
+ if (method === "POST" && url.pathname === "/auth/session") {
4903
+ this.handleSessionExchange(req, res);
4904
+ return;
4905
+ }
4625
4906
  if (method === "GET" && url.pathname === "/") {
4626
4907
  this.serveDashboard(res);
4627
4908
  } else if (method === "GET" && url.pathname === "/events") {
@@ -4633,9 +4914,11 @@ var DashboardApprovalChannel = class {
4633
4914
  } else if (method === "GET" && url.pathname === "/api/audit-log") {
4634
4915
  this.handleAuditLog(url, res);
4635
4916
  } else if (method === "POST" && url.pathname.startsWith("/api/approve/")) {
4917
+ if (!this.checkRateLimit(req, res, "decisions")) return;
4636
4918
  const id = url.pathname.slice("/api/approve/".length);
4637
4919
  this.handleDecision(id, "approve", res);
4638
4920
  } else if (method === "POST" && url.pathname.startsWith("/api/deny/")) {
4921
+ if (!this.checkRateLimit(req, res, "decisions")) return;
4639
4922
  const id = url.pathname.slice("/api/deny/".length);
4640
4923
  this.handleDecision(id, "deny", res);
4641
4924
  } else {
@@ -4648,6 +4931,40 @@ var DashboardApprovalChannel = class {
4648
4931
  }
4649
4932
  }
4650
4933
  // ── Route Handlers ──────────────────────────────────────────────────
4934
+ /**
4935
+ * SEC-012: Exchange a long-lived auth token (in Authorization header)
4936
+ * for a short-lived session ID. The session ID can be used in URL
4937
+ * query parameters without exposing the long-lived credential.
4938
+ *
4939
+ * This endpoint performs its OWN auth check (header-only) because it
4940
+ * must reject query-parameter tokens and is called before the
4941
+ * normal checkAuth flow.
4942
+ */
4943
+ handleSessionExchange(req, res) {
4944
+ if (!this.authToken) {
4945
+ res.writeHead(200, { "Content-Type": "application/json" });
4946
+ res.end(JSON.stringify({ session_id: "no-auth" }));
4947
+ return;
4948
+ }
4949
+ const authHeader = req.headers.authorization;
4950
+ if (!authHeader) {
4951
+ res.writeHead(401, { "Content-Type": "application/json" });
4952
+ res.end(JSON.stringify({ error: "Authorization header required" }));
4953
+ return;
4954
+ }
4955
+ const parts = authHeader.split(" ");
4956
+ if (parts.length !== 2 || parts[0] !== "Bearer" || parts[1] !== this.authToken) {
4957
+ res.writeHead(401, { "Content-Type": "application/json" });
4958
+ res.end(JSON.stringify({ error: "Invalid bearer token" }));
4959
+ return;
4960
+ }
4961
+ const sessionId = this.createSession();
4962
+ res.writeHead(200, { "Content-Type": "application/json" });
4963
+ res.end(JSON.stringify({
4964
+ session_id: sessionId,
4965
+ expires_in_seconds: SESSION_TTL_MS / 1e3
4966
+ }));
4967
+ }
4651
4968
  serveDashboard(res) {
4652
4969
  res.writeHead(200, {
4653
4970
  "Content-Type": "text/html; charset=utf-8",
@@ -4673,7 +4990,8 @@ var DashboardApprovalChannel = class {
4673
4990
  approval_channel: {
4674
4991
  type: this.policy.approval_channel.type,
4675
4992
  timeout_seconds: this.policy.approval_channel.timeout_seconds,
4676
- auto_deny: this.policy.approval_channel.auto_deny
4993
+ auto_deny: true
4994
+ // SEC-002: hardcoded, not configurable
4677
4995
  }
4678
4996
  };
4679
4997
  }
@@ -4714,7 +5032,8 @@ data: ${JSON.stringify(initData)}
4714
5032
  approval_channel: {
4715
5033
  type: this.policy.approval_channel.type,
4716
5034
  timeout_seconds: this.policy.approval_channel.timeout_seconds,
4717
- auto_deny: this.policy.approval_channel.auto_deny
5035
+ auto_deny: true
5036
+ // SEC-002: hardcoded, not configurable
4718
5037
  }
4719
5038
  };
4720
5039
  }
@@ -4887,7 +5206,8 @@ var WebhookApprovalChannel = class {
4887
5206
  const timer = setTimeout(() => {
4888
5207
  this.pending.delete(id);
4889
5208
  const response = {
4890
- decision: this.config.auto_deny ? "deny" : "approve",
5209
+ // SEC-002: Timeout ALWAYS denies. No configuration can change this.
5210
+ decision: "deny",
4891
5211
  decided_at: (/* @__PURE__ */ new Date()).toISOString(),
4892
5212
  decided_by: "timeout"
4893
5213
  };
@@ -5075,16 +5395,29 @@ var ApprovalGate = class {
5075
5395
  if (anomaly) {
5076
5396
  return this.requestApproval(operation, 2, anomaly.reason, anomaly.context);
5077
5397
  }
5078
- this.auditLog.append("l2", `gate_allow:${operation}`, "system", {
5079
- tier: 3,
5080
- operation
5398
+ if (this.policy.tier3_always_allow.includes(operation)) {
5399
+ this.auditLog.append("l2", `gate_allow:${operation}`, "system", {
5400
+ tier: 3,
5401
+ operation
5402
+ });
5403
+ return {
5404
+ allowed: true,
5405
+ tier: 3,
5406
+ reason: "Operation allowed (Tier 3)",
5407
+ approval_required: false
5408
+ };
5409
+ }
5410
+ this.auditLog.append("l2", `gate_unclassified:${operation}`, "system", {
5411
+ tier: 1,
5412
+ operation,
5413
+ warning: "Operation is not classified in any policy tier \u2014 defaulting to Tier 1 (require approval)"
5081
5414
  });
5082
- return {
5083
- allowed: true,
5084
- tier: 3,
5085
- reason: "Operation allowed (Tier 3)",
5086
- approval_required: false
5087
- };
5415
+ return this.requestApproval(
5416
+ operation,
5417
+ 1,
5418
+ `"${operation}" is not classified in any policy tier \u2014 requires approval (SEC-011 safe default)`,
5419
+ { operation, unclassified: true }
5420
+ );
5088
5421
  }
5089
5422
  /**
5090
5423
  * Detect Tier 2 behavioral anomalies.
@@ -5257,7 +5590,8 @@ function createPrincipalPolicyTools(policy, baseline, auditLog) {
5257
5590
  approval_channel: {
5258
5591
  type: policy.approval_channel.type,
5259
5592
  timeout_seconds: policy.approval_channel.timeout_seconds,
5260
- auto_deny: policy.approval_channel.auto_deny
5593
+ auto_deny: true
5594
+ // SEC-002: hardcoded, not configurable
5261
5595
  }
5262
5596
  };
5263
5597
  if (includeDefaults) {
@@ -5327,14 +5661,14 @@ function generateSHR(identityId, opts) {
5327
5661
  code: "PROCESS_ISOLATION_ONLY",
5328
5662
  severity: "warning",
5329
5663
  description: "Process-level isolation only (no TEE)",
5330
- mitigation: "TEE support planned for v0.3.0"
5664
+ mitigation: "TEE support planned for a future release"
5331
5665
  });
5332
5666
  degradations.push({
5333
5667
  layer: "l2",
5334
5668
  code: "SELF_REPORTED_ATTESTATION",
5335
5669
  severity: "warning",
5336
5670
  description: "Attestation is self-reported (no hardware root of trust)",
5337
- mitigation: "TEE attestation planned for v0.3.0"
5671
+ mitigation: "TEE attestation planned for a future release"
5338
5672
  });
5339
5673
  }
5340
5674
  if (config.disclosure.proof_system === "commitment-only") {
@@ -5478,6 +5812,245 @@ function assessSovereigntyLevel(body) {
5478
5812
  return "minimal";
5479
5813
  }
5480
5814
 
5815
+ // src/shr/gateway-adapter.ts
5816
+ var LAYER_WEIGHTS = {
5817
+ l1: 100,
5818
+ l2: 100,
5819
+ l3: 100,
5820
+ l4: 100
5821
+ };
5822
+ var DEGRADATION_IMPACT = {
5823
+ critical: 40,
5824
+ warning: 25,
5825
+ info: 10
5826
+ };
5827
+ function transformSHRForGateway(shr) {
5828
+ const { body, signed_by, signature } = shr;
5829
+ const layerScores = calculateLayerScores(body);
5830
+ const overallScore = calculateOverallScore(layerScores);
5831
+ const trustLevel = determineTrustLevel(overallScore);
5832
+ const signals = extractAuthorizationSignals(body);
5833
+ const degradations = transformDegradations(body.degradations);
5834
+ const constraints = generateAuthorizationConstraints(body);
5835
+ return {
5836
+ shr_version: body.shr_version,
5837
+ agent_identity: signed_by,
5838
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
5839
+ context_expires_at: body.expires_at,
5840
+ overall_score: overallScore,
5841
+ recommended_trust_level: trustLevel,
5842
+ layer_scores: {
5843
+ l1_cognitive: layerScores.l1,
5844
+ l2_operational: layerScores.l2,
5845
+ l3_disclosure: layerScores.l3,
5846
+ l4_reputation: layerScores.l4
5847
+ },
5848
+ layer_status: {
5849
+ l1_cognitive: body.layers.l1.status,
5850
+ l2_operational: body.layers.l2.status,
5851
+ l3_disclosure: body.layers.l3.status,
5852
+ l4_reputation: body.layers.l4.status
5853
+ },
5854
+ authorization_signals: signals,
5855
+ degradations,
5856
+ recommended_constraints: constraints,
5857
+ shr_signature: signature,
5858
+ shr_signed_by: signed_by
5859
+ };
5860
+ }
5861
+ function calculateLayerScores(body) {
5862
+ const layers = body.layers;
5863
+ const degradations = body.degradations;
5864
+ let l1Score = LAYER_WEIGHTS.l1;
5865
+ let l2Score = LAYER_WEIGHTS.l2;
5866
+ let l3Score = LAYER_WEIGHTS.l3;
5867
+ let l4Score = LAYER_WEIGHTS.l4;
5868
+ for (const deg of degradations) {
5869
+ const impact = DEGRADATION_IMPACT[deg.severity] || 10;
5870
+ if (deg.layer === "l1") {
5871
+ l1Score = Math.max(0, l1Score - impact);
5872
+ } else if (deg.layer === "l2") {
5873
+ l2Score = Math.max(0, l2Score - impact);
5874
+ } else if (deg.layer === "l3") {
5875
+ l3Score = Math.max(0, l3Score - impact);
5876
+ } else if (deg.layer === "l4") {
5877
+ l4Score = Math.max(0, l4Score - impact);
5878
+ }
5879
+ }
5880
+ if (layers.l1.status === "active" && l1Score > 50) l1Score = Math.min(100, l1Score + 5);
5881
+ if (layers.l2.status === "active" && l2Score > 50) l2Score = Math.min(100, l2Score + 5);
5882
+ if (layers.l3.status === "active" && l3Score > 50) l3Score = Math.min(100, l3Score + 5);
5883
+ if (layers.l4.status === "active" && l4Score > 50) l4Score = Math.min(100, l4Score + 5);
5884
+ if (layers.l1.status === "inactive") l1Score = 0;
5885
+ if (layers.l2.status === "inactive") l2Score = 0;
5886
+ if (layers.l3.status === "inactive") l3Score = 0;
5887
+ if (layers.l4.status === "inactive") l4Score = 0;
5888
+ return {
5889
+ l1: Math.round(l1Score),
5890
+ l2: Math.round(l2Score),
5891
+ l3: Math.round(l3Score),
5892
+ l4: Math.round(l4Score)
5893
+ };
5894
+ }
5895
+ function calculateOverallScore(layerScores) {
5896
+ const average = (layerScores.l1 + layerScores.l2 + layerScores.l3 + layerScores.l4) / 4;
5897
+ return Math.round(average);
5898
+ }
5899
+ function determineTrustLevel(score) {
5900
+ if (score >= 80) return "full";
5901
+ if (score >= 60) return "elevated";
5902
+ if (score >= 40) return "standard";
5903
+ return "restricted";
5904
+ }
5905
+ function extractAuthorizationSignals(body) {
5906
+ const l1 = body.layers.l1;
5907
+ const l3 = body.layers.l3;
5908
+ const l4 = body.layers.l4;
5909
+ return {
5910
+ approval_gate_active: body.capabilities.handshake,
5911
+ // Handshake implies human loop capability
5912
+ context_gating_active: body.capabilities.encrypted_channel,
5913
+ // Proxy for gating capability
5914
+ encryption_at_rest: l1.encryption !== "none" && l1.encryption !== "unencrypted",
5915
+ behavioral_baseline_active: false,
5916
+ // Would need explicit field in SHR v1.1
5917
+ identity_verified: l1.identity_type === "ed25519" || l1.identity_type !== "none",
5918
+ zero_knowledge_capable: l3.status === "active" && l3.proof_system !== "commitment-only",
5919
+ selective_disclosure_active: l3.selective_disclosure,
5920
+ reputation_portable: l4.reputation_portable,
5921
+ handshake_capable: body.capabilities.handshake
5922
+ };
5923
+ }
5924
+ function transformDegradations(degradations) {
5925
+ return degradations.map((deg) => {
5926
+ let authzImpact = "";
5927
+ if (deg.code === "NO_TEE") {
5928
+ authzImpact = "Restricted to read-only operations until TEE available";
5929
+ } else if (deg.code === "PROCESS_ISOLATION_ONLY") {
5930
+ authzImpact = "Requires additional identity verification";
5931
+ } else if (deg.code === "COMMITMENT_ONLY") {
5932
+ authzImpact = "Limited data sharing scope \u2014 no zero-knowledge proofs";
5933
+ } else if (deg.code === "NO_ZK_PROOFS") {
5934
+ authzImpact = "Cannot perform confidential disclosures";
5935
+ } else if (deg.code === "SELF_REPORTED_ATTESTATION") {
5936
+ authzImpact = "Attestation trust degraded \u2014 human verification recommended";
5937
+ } else if (deg.code === "NO_SELECTIVE_DISCLOSURE") {
5938
+ authzImpact = "Must share entire data context, cannot redact";
5939
+ } else if (deg.code === "BASIC_SYBIL_ONLY") {
5940
+ authzImpact = "Restrict to interactions with known agents only";
5941
+ } else {
5942
+ authzImpact = "Unknown authorization impact";
5943
+ }
5944
+ return {
5945
+ layer: deg.layer,
5946
+ code: deg.code,
5947
+ severity: deg.severity,
5948
+ description: deg.description,
5949
+ authorization_impact: authzImpact
5950
+ };
5951
+ });
5952
+ }
5953
+ function generateAuthorizationConstraints(body, _degradations) {
5954
+ const constraints = [];
5955
+ const layers = body.layers;
5956
+ if (layers.l1.status === "degraded" || layers.l1.key_custody !== "self") {
5957
+ constraints.push({
5958
+ type: "identity_verification_required",
5959
+ description: "Additional identity verification required for sensitive operations",
5960
+ rationale: "L1 is degraded or key custody is not self-managed",
5961
+ priority: "high"
5962
+ });
5963
+ }
5964
+ if (!layers.l1.state_portable) {
5965
+ constraints.push({
5966
+ type: "location_bound",
5967
+ description: "Agent state is not portable \u2014 restrict to home environment",
5968
+ rationale: "State cannot be safely migrated across boundaries",
5969
+ priority: "medium"
5970
+ });
5971
+ }
5972
+ if (layers.l2.status === "degraded" || layers.l2.isolation_type === "local-process") {
5973
+ constraints.push({
5974
+ type: "read_only",
5975
+ description: "Restrict to read-only operations until operational isolation improves",
5976
+ rationale: "L2 isolation is process-level only (no TEE)",
5977
+ priority: "high"
5978
+ });
5979
+ }
5980
+ if (!layers.l2.attestation_available) {
5981
+ constraints.push({
5982
+ type: "requires_approval",
5983
+ description: "Human approval required for writes and sensitive reads",
5984
+ rationale: "No attestation available \u2014 self-reported integrity only",
5985
+ priority: "high"
5986
+ });
5987
+ }
5988
+ if (layers.l3.status === "degraded" || !layers.l3.selective_disclosure) {
5989
+ constraints.push({
5990
+ type: "restricted_scope",
5991
+ description: "Limit data sharing to minimal required scope \u2014 no selective disclosure",
5992
+ rationale: "Agent cannot redact data or prove predicates without revealing all context",
5993
+ priority: "high"
5994
+ });
5995
+ }
5996
+ if (layers.l3.proof_system === "commitment-only") {
5997
+ constraints.push({
5998
+ type: "restricted_scope",
5999
+ description: "No zero-knowledge proofs available \u2014 entire state context may be visible",
6000
+ rationale: "Proof system is commitment-only (no ZK)",
6001
+ priority: "medium"
6002
+ });
6003
+ }
6004
+ if (layers.l4.status === "degraded") {
6005
+ constraints.push({
6006
+ type: "known_agents_only",
6007
+ description: "Restrict interactions to known, pre-approved agents",
6008
+ rationale: "Reputation layer is degraded",
6009
+ priority: "medium"
6010
+ });
6011
+ }
6012
+ if (!layers.l4.reputation_portable) {
6013
+ constraints.push({
6014
+ type: "location_bound",
6015
+ description: "Reputation is not portable \u2014 restrict to home environment",
6016
+ rationale: "Cannot present reputation to external parties",
6017
+ priority: "low"
6018
+ });
6019
+ }
6020
+ const layerScores = calculateLayerScores(body);
6021
+ const overallScore = calculateOverallScore(layerScores);
6022
+ if (overallScore < 40) {
6023
+ constraints.push({
6024
+ type: "restricted_scope",
6025
+ description: "Overall sovereignty score below threshold \u2014 restrict to non-sensitive operations",
6026
+ rationale: `Overall sovereignty score is ${overallScore}/100`,
6027
+ priority: "high"
6028
+ });
6029
+ }
6030
+ return constraints;
6031
+ }
6032
+ function transformSHRGeneric(shr) {
6033
+ const context = transformSHRForGateway(shr);
6034
+ return {
6035
+ agent_id: context.agent_identity,
6036
+ sovereignty_score: context.overall_score,
6037
+ trust_level: context.recommended_trust_level,
6038
+ layer_scores: {
6039
+ l1: context.layer_scores.l1_cognitive,
6040
+ l2: context.layer_scores.l2_operational,
6041
+ l3: context.layer_scores.l3_disclosure,
6042
+ l4: context.layer_scores.l4_reputation
6043
+ },
6044
+ capabilities: context.authorization_signals,
6045
+ constraints: context.recommended_constraints.map((c) => ({
6046
+ type: c.type,
6047
+ description: c.description
6048
+ })),
6049
+ expires_at: context.context_expires_at,
6050
+ signature: context.shr_signature
6051
+ };
6052
+ }
6053
+
5481
6054
  // src/shr/tools.ts
5482
6055
  function createSHRTools(config, identityManager, masterKey, auditLog) {
5483
6056
  const generatorOpts = {
@@ -5540,6 +6113,53 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
5540
6113
  );
5541
6114
  return toolResult(result);
5542
6115
  }
6116
+ },
6117
+ {
6118
+ name: "sanctuary/shr_gateway_export",
6119
+ description: "Export this instance's Sovereignty Health Report formatted for Ping Identity's Agent Gateway or other identity providers. Transforms the SHR into an authorization context with sovereignty scores, capability flags, and recommended access constraints.",
6120
+ inputSchema: {
6121
+ type: "object",
6122
+ properties: {
6123
+ format: {
6124
+ type: "string",
6125
+ enum: ["ping", "generic"],
6126
+ description: "Output format: 'ping' (Ping Identity Gateway format) or 'generic' (format-agnostic). Default: 'ping'."
6127
+ },
6128
+ identity_id: {
6129
+ type: "string",
6130
+ description: "Identity to sign the SHR with. Defaults to primary identity."
6131
+ },
6132
+ validity_minutes: {
6133
+ type: "number",
6134
+ description: "How long the SHR is valid (minutes). Default: 60."
6135
+ }
6136
+ }
6137
+ },
6138
+ handler: async (args) => {
6139
+ const format = args.format || "ping";
6140
+ const validityMs = args.validity_minutes ? args.validity_minutes * 60 * 1e3 : void 0;
6141
+ const shrResult = generateSHR(args.identity_id, {
6142
+ ...generatorOpts,
6143
+ validityMs
6144
+ });
6145
+ if (typeof shrResult === "string") {
6146
+ return toolResult({ error: shrResult });
6147
+ }
6148
+ let context;
6149
+ if (format === "generic") {
6150
+ context = transformSHRGeneric(shrResult);
6151
+ } else {
6152
+ context = transformSHRForGateway(shrResult);
6153
+ }
6154
+ auditLog.append(
6155
+ "l2",
6156
+ "shr_gateway_export",
6157
+ shrResult.body.instance_id,
6158
+ void 0,
6159
+ "success"
6160
+ );
6161
+ return toolResult(context);
6162
+ }
5543
6163
  }
5544
6164
  ];
5545
6165
  return { tools };
@@ -5788,7 +6408,9 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
5788
6408
  return toolResult({
5789
6409
  session_id: result.session.session_id,
5790
6410
  response: result.response,
5791
- instructions: "Send the 'response' object back to the initiator. When you receive their completion, pass it to sanctuary/handshake_status with this session_id."
6411
+ instructions: "Send the 'response' object back to the initiator. When you receive their completion, pass it to sanctuary/handshake_status with this session_id.",
6412
+ // SEC-ADD-03: Tag response — contains SHR data that will be sent to counterparty
6413
+ _content_trust: "external"
5792
6414
  });
5793
6415
  }
5794
6416
  },
@@ -5841,7 +6463,9 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
5841
6463
  return toolResult({
5842
6464
  completion: result.completion,
5843
6465
  result: result.result,
5844
- instructions: "Send the 'completion' object to the responder so they can verify the handshake. The 'result' object contains the verified counterparty status and trust tier."
6466
+ instructions: "Send the 'completion' object to the responder so they can verify the handshake. The 'result' object contains the verified counterparty status and trust tier.",
6467
+ // SEC-ADD-03: Tag response as containing counterparty-controlled SHR data
6468
+ _content_trust: "external"
5845
6469
  });
5846
6470
  }
5847
6471
  },
@@ -6266,7 +6890,21 @@ function canonicalize(outcome) {
6266
6890
  return stringToBytes(stableStringify(outcome));
6267
6891
  }
6268
6892
  function stableStringify(value) {
6269
- if (value === null || value === void 0) return JSON.stringify(value);
6893
+ if (value === null) return "null";
6894
+ if (value === void 0) return "null";
6895
+ if (typeof value === "number") {
6896
+ if (!Number.isFinite(value)) {
6897
+ throw new Error(
6898
+ `Cannot canonicalize non-finite number: ${value}. NaN, Infinity, and -Infinity are not representable in JSON.`
6899
+ );
6900
+ }
6901
+ if (Object.is(value, -0)) {
6902
+ throw new Error(
6903
+ "Cannot canonicalize negative zero (-0). Use 0 instead for deterministic cross-language serialization."
6904
+ );
6905
+ }
6906
+ return JSON.stringify(value);
6907
+ }
6270
6908
  if (typeof value !== "object") return JSON.stringify(value);
6271
6909
  if (Array.isArray(value)) {
6272
6910
  return "[" + value.map((v) => stableStringify(v)).join(",") + "]";
@@ -6294,11 +6932,12 @@ function createBridgeCommitment(outcome, identity, identityEncryptionKey, includ
6294
6932
  bridge_commitment_id: commitmentId,
6295
6933
  session_id: outcome.session_id,
6296
6934
  sha256_commitment: sha2564.commitment,
6935
+ terms_hash: outcome.terms_hash,
6297
6936
  committer_did: identity.did,
6298
6937
  committed_at: now,
6299
6938
  bridge_version: "sanctuary-concordia-bridge-v1"
6300
6939
  };
6301
- const payloadBytes = stringToBytes(JSON.stringify(commitmentPayload));
6940
+ const payloadBytes = stringToBytes(stableStringify(commitmentPayload));
6302
6941
  const signature = sign(payloadBytes, identity.encrypted_private_key, identityEncryptionKey);
6303
6942
  return {
6304
6943
  bridge_commitment_id: commitmentId,
@@ -6324,11 +6963,12 @@ function verifyBridgeCommitment(commitment, outcome, committerPublicKey) {
6324
6963
  bridge_commitment_id: commitment.bridge_commitment_id,
6325
6964
  session_id: commitment.session_id,
6326
6965
  sha256_commitment: commitment.sha256_commitment,
6966
+ terms_hash: outcome.terms_hash,
6327
6967
  committer_did: commitment.committer_did,
6328
6968
  committed_at: commitment.committed_at,
6329
6969
  bridge_version: commitment.bridge_version
6330
6970
  };
6331
- const payloadBytes = stringToBytes(JSON.stringify(commitmentPayload));
6971
+ const payloadBytes = stringToBytes(stableStringify(commitmentPayload));
6332
6972
  const sigBytes = fromBase64url(commitment.signature);
6333
6973
  const signatureValid = verify(payloadBytes, sigBytes, committerPublicKey);
6334
6974
  const sessionIdMatch = commitment.session_id === outcome.session_id;
@@ -6555,7 +7195,9 @@ function createBridgeTools(storage, masterKey, identityManager, auditLog, handsh
6555
7195
  return toolResult({
6556
7196
  ...result,
6557
7197
  session_id: storedCommitment.session_id,
6558
- committer_did: storedCommitment.committer_did
7198
+ committer_did: storedCommitment.committer_did,
7199
+ // SEC-ADD-03: Tag response as containing counterparty-controlled data
7200
+ _content_trust: "external"
6559
7201
  });
6560
7202
  }
6561
7203
  },
@@ -6649,27 +7291,2245 @@ function createBridgeTools(storage, masterKey, identityManager, auditLog, handsh
6649
7291
  ];
6650
7292
  return { tools };
6651
7293
  }
6652
-
6653
- // src/index.ts
6654
- init_encoding();
6655
-
6656
- // src/storage/memory.ts
6657
- var MemoryStorage = class {
6658
- store = /* @__PURE__ */ new Map();
6659
- storageKey(namespace, key) {
6660
- return `${namespace}/${key}`;
6661
- }
6662
- async write(namespace, key, data) {
6663
- this.store.set(this.storageKey(namespace, key), {
6664
- data: new Uint8Array(data),
6665
- // Copy to prevent external mutation
6666
- modified_at: (/* @__PURE__ */ new Date()).toISOString()
6667
- });
7294
+ function lenientJsonParse(raw) {
7295
+ let cleaned = raw.replace(/\/\/[^\n]*/g, "");
7296
+ cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, "");
7297
+ cleaned = cleaned.replace(/,\s*([\]}])/g, "$1");
7298
+ return JSON.parse(cleaned);
7299
+ }
7300
+ async function fileExists(path) {
7301
+ try {
7302
+ await access(path);
7303
+ return true;
7304
+ } catch {
7305
+ return false;
6668
7306
  }
6669
- async read(namespace, key) {
6670
- const entry = this.store.get(this.storageKey(namespace, key));
6671
- if (!entry) return null;
6672
- return new Uint8Array(entry.data);
7307
+ }
7308
+ async function safeReadFile(path) {
7309
+ try {
7310
+ return await readFile(path, "utf-8");
7311
+ } catch {
7312
+ return null;
7313
+ }
7314
+ }
7315
+ async function detectEnvironment(config, deepScan) {
7316
+ const fingerprint = {
7317
+ sanctuary_installed: true,
7318
+ // We're running inside Sanctuary
7319
+ sanctuary_version: config.version,
7320
+ openclaw_detected: false,
7321
+ openclaw_version: null,
7322
+ openclaw_config: null,
7323
+ node_version: process.version,
7324
+ platform: `${process.platform}-${process.arch}`
7325
+ };
7326
+ if (!deepScan) {
7327
+ return fingerprint;
7328
+ }
7329
+ const home = homedir();
7330
+ const openclawConfigPath = join(home, ".openclaw", "openclaw.json");
7331
+ const openclawEnvPath = join(home, ".openclaw", ".env");
7332
+ const openclawMemoryPath = join(home, ".openclaw", "workspace", "MEMORY.md");
7333
+ const openclawMemoryDir = join(home, ".openclaw", "workspace", "memory");
7334
+ const configExists = await fileExists(openclawConfigPath);
7335
+ const envExists = await fileExists(openclawEnvPath);
7336
+ const memoryExists = await fileExists(openclawMemoryPath);
7337
+ const memoryDirExists = await fileExists(openclawMemoryDir);
7338
+ if (configExists || memoryExists || memoryDirExists) {
7339
+ fingerprint.openclaw_detected = true;
7340
+ fingerprint.openclaw_config = await auditOpenClawConfig(
7341
+ openclawConfigPath,
7342
+ openclawEnvPath,
7343
+ openclawMemoryPath,
7344
+ configExists,
7345
+ envExists,
7346
+ memoryExists
7347
+ );
7348
+ }
7349
+ return fingerprint;
7350
+ }
7351
+ async function auditOpenClawConfig(configPath, envPath, _memoryPath, configExists, envExists, memoryExists) {
7352
+ const audit = {
7353
+ config_path: configExists ? configPath : null,
7354
+ require_approval_enabled: false,
7355
+ sandbox_policy_active: false,
7356
+ sandbox_allow_list: [],
7357
+ sandbox_deny_list: [],
7358
+ memory_encrypted: false,
7359
+ // Stock OpenClaw never encrypts memory
7360
+ env_file_exposed: false,
7361
+ gateway_token_set: false,
7362
+ dm_pairing_enabled: false,
7363
+ mcp_bridge_active: false
7364
+ };
7365
+ if (configExists) {
7366
+ const raw = await safeReadFile(configPath);
7367
+ if (raw) {
7368
+ try {
7369
+ const parsed = lenientJsonParse(raw);
7370
+ const hooks = parsed.hooks;
7371
+ if (hooks) {
7372
+ const beforeToolCall = hooks.before_tool_call;
7373
+ if (beforeToolCall) {
7374
+ const hookStr = JSON.stringify(beforeToolCall);
7375
+ audit.require_approval_enabled = hookStr.includes("requireApproval");
7376
+ }
7377
+ }
7378
+ const tools = parsed.tools;
7379
+ if (tools) {
7380
+ const sandbox = tools.sandbox;
7381
+ if (sandbox) {
7382
+ const sandboxTools = sandbox.tools;
7383
+ if (sandboxTools) {
7384
+ audit.sandbox_policy_active = true;
7385
+ if (Array.isArray(sandboxTools.allow)) {
7386
+ audit.sandbox_allow_list = sandboxTools.allow.filter(
7387
+ (item) => typeof item === "string"
7388
+ );
7389
+ }
7390
+ if (Array.isArray(sandboxTools.alsoAllow)) {
7391
+ audit.sandbox_allow_list = [
7392
+ ...audit.sandbox_allow_list,
7393
+ ...sandboxTools.alsoAllow.filter(
7394
+ (item) => typeof item === "string"
7395
+ )
7396
+ ];
7397
+ }
7398
+ if (Array.isArray(sandboxTools.deny)) {
7399
+ audit.sandbox_deny_list = sandboxTools.deny.filter(
7400
+ (item) => typeof item === "string"
7401
+ );
7402
+ }
7403
+ }
7404
+ }
7405
+ }
7406
+ const mcpServers = parsed.mcpServers;
7407
+ if (mcpServers && Object.keys(mcpServers).length > 0) {
7408
+ audit.mcp_bridge_active = true;
7409
+ }
7410
+ } catch {
7411
+ }
7412
+ }
7413
+ }
7414
+ if (envExists) {
7415
+ const envContent = await safeReadFile(envPath);
7416
+ if (envContent) {
7417
+ const secretPatterns = [
7418
+ /[A-Z_]*API_KEY\s*=/,
7419
+ /[A-Z_]*TOKEN\s*=/,
7420
+ /[A-Z_]*SECRET\s*=/,
7421
+ /[A-Z_]*PASSWORD\s*=/,
7422
+ /[A-Z_]*PRIVATE_KEY\s*=/
7423
+ ];
7424
+ audit.env_file_exposed = secretPatterns.some((p) => p.test(envContent));
7425
+ audit.gateway_token_set = /OPENCLAW_GATEWAY_TOKEN\s*=/.test(envContent);
7426
+ }
7427
+ }
7428
+ if (memoryExists) {
7429
+ audit.memory_encrypted = false;
7430
+ }
7431
+ return audit;
7432
+ }
7433
+
7434
+ // src/audit/analyzer.ts
7435
+ var L1_ENCRYPTION_AT_REST = 10;
7436
+ var L1_IDENTITY_CRYPTOGRAPHIC = 10;
7437
+ var L1_INTEGRITY_VERIFICATION = 8;
7438
+ var L1_STATE_PORTABLE = 7;
7439
+ var L2_THREE_TIER_GATE = 10;
7440
+ var L2_BINARY_GATE = 3;
7441
+ var L2_ANOMALY_DETECTION = 5;
7442
+ var L2_ENCRYPTED_AUDIT = 4;
7443
+ var L2_TOOL_SANDBOXING = 2;
7444
+ var L2_CONTEXT_GATING = 4;
7445
+ var L2_PROCESS_HARDENING = 5;
7446
+ var L3_COMMITMENT_SCHEME = 8;
7447
+ var L3_ZK_PROOFS = 7;
7448
+ var L3_DISCLOSURE_POLICIES = 5;
7449
+ var L4_PORTABLE_REPUTATION = 6;
7450
+ var L4_SIGNED_ATTESTATIONS = 6;
7451
+ var L4_SYBIL_DETECTION = 4;
7452
+ var L4_SOVEREIGNTY_GATED = 4;
7453
+ var SEVERITY_ORDER = {
7454
+ critical: 0,
7455
+ high: 1,
7456
+ medium: 2,
7457
+ low: 3
7458
+ };
7459
+ var INCIDENT_META_SEV1 = {
7460
+ id: "META-SEV1-2026",
7461
+ name: "Meta Sev 1: Unauthorized autonomous data exposure",
7462
+ date: "2026-03-18",
7463
+ description: "AI agent autonomously posted proprietary code, business strategies, and user datasets to an internal forum without human approval. Two-hour exposure window."
7464
+ };
7465
+ var INCIDENT_OPENCLAW_SANDBOX = {
7466
+ id: "OPENCLAW-CVE-2026",
7467
+ name: "OpenClaw sandbox escape via privilege inheritance",
7468
+ date: "2026-03-18",
7469
+ description: "Nine CVEs in four days. Child processes inherited sandbox.mode=off from parent, bypassing runtime confinement. 42,900+ internet-exposed instances, 15,200 vulnerable to RCE.",
7470
+ cves: [
7471
+ "CVE-2026-32048",
7472
+ "CVE-2026-32915",
7473
+ "CVE-2026-32918"
7474
+ ]
7475
+ };
7476
+ var INCIDENT_CONTEXT_LEAKAGE = {
7477
+ id: "CONTEXT-LEAK-CLASS",
7478
+ name: "Context leakage: Full state exposure to inference providers",
7479
+ date: "2026-03",
7480
+ description: "Agents send full context \u2014 conversation history, memory, secrets, internal reasoning \u2014 to remote LLM providers on every inference call with no filtering mechanism."
7481
+ };
7482
+ var INCIDENT_CLAUDE_CODE_LEAK = {
7483
+ id: "CLAUDE-CODE-LEAK-2026",
7484
+ name: "Claude Code source leak: 512K lines exposed via npm source map",
7485
+ date: "2026-03-31",
7486
+ description: "Anthropic accidentally shipped a 59.8 MB source map in npm package v2.1.88, exposing the full Claude Code TypeScript source \u2014 1,900 files, internal model codenames, unreleased features, OAuth flows, and multi-agent coordination logic."
7487
+ };
7488
+ function analyzeSovereignty(env, config) {
7489
+ const l1 = assessL1(env, config);
7490
+ const l2 = assessL2(env);
7491
+ const l3 = assessL3(env);
7492
+ const l4 = assessL4(env);
7493
+ const l1Score = scoreL1(l1);
7494
+ const l2Score = scoreL2(l2);
7495
+ const l3Score = scoreL3(l3);
7496
+ const l4Score = scoreL4(l4);
7497
+ const overallScore = l1Score + l2Score + l3Score + l4Score;
7498
+ const sovereigntyLevel = overallScore >= 80 ? "full" : overallScore >= 50 ? "partial" : overallScore >= 20 ? "minimal" : "none";
7499
+ const gaps = generateGaps(env, l1, l2, l3, l4);
7500
+ gaps.sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
7501
+ const recommendations = generateRecommendations(env, l1, l2, l3, l4);
7502
+ return {
7503
+ version: "1.0",
7504
+ audited_at: (/* @__PURE__ */ new Date()).toISOString(),
7505
+ environment: env,
7506
+ layers: {
7507
+ l1_cognitive: l1,
7508
+ l2_operational: l2,
7509
+ l3_selective_disclosure: l3,
7510
+ l4_reputation: l4
7511
+ },
7512
+ overall_score: overallScore,
7513
+ sovereignty_level: sovereigntyLevel,
7514
+ gaps,
7515
+ recommendations
7516
+ };
7517
+ }
7518
+ function assessL1(env, config) {
7519
+ const findings = [];
7520
+ const sanctuaryActive = env.sanctuary_installed;
7521
+ const encryptionAtRest = sanctuaryActive;
7522
+ const keyCustody = sanctuaryActive ? "self" : "none";
7523
+ const integrityVerification = sanctuaryActive;
7524
+ const identityCryptographic = sanctuaryActive;
7525
+ const statePortable = sanctuaryActive;
7526
+ if (sanctuaryActive) {
7527
+ findings.push("AES-256-GCM encryption active for all state");
7528
+ findings.push(`Key derivation: ${config.state.key_derivation}`);
7529
+ findings.push(`Identity provider: ${config.state.identity_provider}`);
7530
+ findings.push("Merkle integrity verification enabled");
7531
+ findings.push("State export/import available");
7532
+ }
7533
+ if (env.openclaw_detected && env.openclaw_config) {
7534
+ if (!env.openclaw_config.memory_encrypted) {
7535
+ findings.push("OpenClaw agent memory (MEMORY.md, daily notes) stored in plaintext");
7536
+ }
7537
+ if (env.openclaw_config.env_file_exposed) {
7538
+ findings.push("OpenClaw .env file contains plaintext API keys/tokens");
7539
+ }
7540
+ }
7541
+ const status = encryptionAtRest && identityCryptographic ? "active" : encryptionAtRest || identityCryptographic ? "partial" : "inactive";
7542
+ return {
7543
+ status,
7544
+ encryption_at_rest: encryptionAtRest,
7545
+ key_custody: keyCustody,
7546
+ integrity_verification: integrityVerification,
7547
+ identity_cryptographic: identityCryptographic,
7548
+ state_portable: statePortable,
7549
+ findings
7550
+ };
7551
+ }
7552
+ function assessL2(env, _config) {
7553
+ const findings = [];
7554
+ const sanctuaryActive = env.sanctuary_installed;
7555
+ let approvalGate = "none";
7556
+ let behavioralAnomalyDetection = false;
7557
+ let auditTrailEncrypted = false;
7558
+ let auditTrailExists = false;
7559
+ let toolSandboxing = "none";
7560
+ let contextGating = false;
7561
+ let processIsolationHardening = "none";
7562
+ if (sanctuaryActive) {
7563
+ approvalGate = "three-tier";
7564
+ behavioralAnomalyDetection = true;
7565
+ auditTrailEncrypted = true;
7566
+ auditTrailExists = true;
7567
+ contextGating = true;
7568
+ findings.push("Three-tier Principal Policy gate active");
7569
+ findings.push("Behavioral anomaly detection (BaselineTracker) enabled");
7570
+ findings.push("Encrypted audit trail active");
7571
+ findings.push("Context gating available (sanctuary/context_gate_set_policy)");
7572
+ }
7573
+ if (env.openclaw_detected && env.openclaw_config) {
7574
+ if (env.openclaw_config.require_approval_enabled) {
7575
+ if (!sanctuaryActive) {
7576
+ approvalGate = "binary";
7577
+ }
7578
+ findings.push("OpenClaw requireApproval hook enabled (binary approve/deny)");
7579
+ }
7580
+ if (env.openclaw_config.sandbox_policy_active) {
7581
+ if (!sanctuaryActive) {
7582
+ toolSandboxing = "basic";
7583
+ }
7584
+ findings.push(
7585
+ `OpenClaw sandbox policy active (${env.openclaw_config.sandbox_allow_list.length} allowed, ${env.openclaw_config.sandbox_deny_list.length} denied)`
7586
+ );
7587
+ }
7588
+ }
7589
+ processIsolationHardening = "none";
7590
+ const status = approvalGate === "three-tier" && auditTrailEncrypted ? "active" : approvalGate !== "none" || auditTrailExists ? "partial" : "inactive";
7591
+ return {
7592
+ status,
7593
+ approval_gate: approvalGate,
7594
+ behavioral_anomaly_detection: behavioralAnomalyDetection,
7595
+ audit_trail_encrypted: auditTrailEncrypted,
7596
+ audit_trail_exists: auditTrailExists,
7597
+ tool_sandboxing: sanctuaryActive ? "policy-enforced" : toolSandboxing,
7598
+ context_gating: contextGating,
7599
+ process_isolation_hardening: processIsolationHardening,
7600
+ findings
7601
+ };
7602
+ }
7603
+ function assessL3(env, _config) {
7604
+ const findings = [];
7605
+ const sanctuaryActive = env.sanctuary_installed;
7606
+ let commitmentScheme = "none";
7607
+ let zkProofs = false;
7608
+ let selectiveDisclosurePolicy = false;
7609
+ if (sanctuaryActive) {
7610
+ commitmentScheme = "pedersen+sha256";
7611
+ zkProofs = true;
7612
+ selectiveDisclosurePolicy = true;
7613
+ findings.push("SHA-256 + Pedersen commitment schemes active");
7614
+ findings.push("Schnorr zero-knowledge proofs (Fiat-Shamir) enabled \u2014 genuine ZK proofs");
7615
+ findings.push("Range proofs (bit-decomposition + OR-proofs) enabled \u2014 genuine ZK proofs");
7616
+ findings.push("Selective disclosure policies configurable");
7617
+ findings.push("Non-interactive proofs with replay-resistant domain separation");
7618
+ }
7619
+ const status = commitmentScheme === "pedersen+sha256" && zkProofs ? "active" : commitmentScheme !== "none" ? "partial" : "inactive";
7620
+ return {
7621
+ status,
7622
+ commitment_scheme: commitmentScheme,
7623
+ zero_knowledge_proofs: zkProofs,
7624
+ selective_disclosure_policy: selectiveDisclosurePolicy,
7625
+ findings
7626
+ };
7627
+ }
7628
+ function assessL4(env, _config) {
7629
+ const findings = [];
7630
+ const sanctuaryActive = env.sanctuary_installed;
7631
+ const reputationPortable = sanctuaryActive;
7632
+ const reputationSigned = sanctuaryActive;
7633
+ const sybilDetection = sanctuaryActive;
7634
+ const sovereigntyGated = sanctuaryActive;
7635
+ if (sanctuaryActive) {
7636
+ findings.push("Signed EAS-compatible attestations active");
7637
+ findings.push("Reputation export/import available");
7638
+ findings.push("Sybil detection heuristics enabled");
7639
+ findings.push("Sovereignty-gated reputation tiers active");
7640
+ } else {
7641
+ findings.push("No portable reputation system detected");
7642
+ }
7643
+ const status = reputationPortable && reputationSigned && sovereigntyGated ? "active" : reputationPortable || reputationSigned ? "partial" : "inactive";
7644
+ return {
7645
+ status,
7646
+ reputation_portable: reputationPortable,
7647
+ reputation_signed: reputationSigned,
7648
+ reputation_sybil_detection: sybilDetection,
7649
+ sovereignty_gated_tiers: sovereigntyGated,
7650
+ findings
7651
+ };
7652
+ }
7653
+ function scoreL1(l1) {
7654
+ let score = 0;
7655
+ if (l1.encryption_at_rest) score += L1_ENCRYPTION_AT_REST;
7656
+ if (l1.identity_cryptographic) score += L1_IDENTITY_CRYPTOGRAPHIC;
7657
+ if (l1.integrity_verification) score += L1_INTEGRITY_VERIFICATION;
7658
+ if (l1.state_portable) score += L1_STATE_PORTABLE;
7659
+ return score;
7660
+ }
7661
+ function scoreL2(l2) {
7662
+ let score = 0;
7663
+ if (l2.approval_gate === "three-tier") score += L2_THREE_TIER_GATE;
7664
+ else if (l2.approval_gate === "binary") score += L2_BINARY_GATE;
7665
+ if (l2.behavioral_anomaly_detection) score += L2_ANOMALY_DETECTION;
7666
+ if (l2.audit_trail_encrypted) score += L2_ENCRYPTED_AUDIT;
7667
+ if (l2.tool_sandboxing === "policy-enforced") score += L2_TOOL_SANDBOXING;
7668
+ else if (l2.tool_sandboxing === "basic") score += 1;
7669
+ if (l2.context_gating) score += L2_CONTEXT_GATING;
7670
+ if (l2.process_isolation_hardening === "hardened") score += L2_PROCESS_HARDENING;
7671
+ else if (l2.process_isolation_hardening === "basic") score += 2;
7672
+ return score;
7673
+ }
7674
+ function scoreL3(l3) {
7675
+ let score = 0;
7676
+ if (l3.commitment_scheme === "pedersen+sha256") score += L3_COMMITMENT_SCHEME;
7677
+ else if (l3.commitment_scheme === "sha256-only") score += 4;
7678
+ if (l3.zero_knowledge_proofs) score += L3_ZK_PROOFS;
7679
+ if (l3.selective_disclosure_policy) score += L3_DISCLOSURE_POLICIES;
7680
+ return score;
7681
+ }
7682
+ function scoreL4(l4) {
7683
+ let score = 0;
7684
+ if (l4.reputation_portable) score += L4_PORTABLE_REPUTATION;
7685
+ if (l4.reputation_signed) score += L4_SIGNED_ATTESTATIONS;
7686
+ if (l4.reputation_sybil_detection) score += L4_SYBIL_DETECTION;
7687
+ if (l4.sovereignty_gated_tiers) score += L4_SOVEREIGNTY_GATED;
7688
+ return score;
7689
+ }
7690
+ function generateGaps(env, l1, l2, l3, l4) {
7691
+ const gaps = [];
7692
+ const oc = env.openclaw_config;
7693
+ if (oc && !oc.memory_encrypted) {
7694
+ gaps.push({
7695
+ id: "GAP-L1-001",
7696
+ layer: "L1",
7697
+ severity: "critical",
7698
+ title: "Agent memory stored in plaintext",
7699
+ description: "Your agent's memory (MEMORY.md, daily notes, SQLite index) is stored in plaintext at ~/.openclaw/workspace/. Any process with file access can read your agent's full context \u2014 preferences, decisions, conversation history.",
7700
+ openclaw_relevance: "Stock OpenClaw stores all agent memory in plaintext files. There is no built-in encryption for agent state.",
7701
+ sanctuary_solution: "Sanctuary encrypts all state at rest with AES-256-GCM using a key derived from Argon2id, making state opaque to any process that doesn't hold the master key. Use sanctuary/state_write to migrate sensitive state to the encrypted store.",
7702
+ incident_class: INCIDENT_META_SEV1
7703
+ });
7704
+ }
7705
+ if (oc && oc.env_file_exposed) {
7706
+ gaps.push({
7707
+ id: "GAP-L1-002",
7708
+ layer: "L1",
7709
+ severity: "critical",
7710
+ title: "Plaintext API keys in .env file",
7711
+ description: "Your .env file contains plaintext API keys and tokens. These secrets are readable by any process with filesystem access.",
7712
+ openclaw_relevance: "OpenClaw stores API keys (LLM providers, gateway tokens) in a plaintext .env file.",
7713
+ sanctuary_solution: "Sanctuary's encrypted state store can hold secrets under the same AES-256-GCM envelope as all other state, tied to your self-custodied identity. Use sanctuary/state_write with namespace 'secrets'."
7714
+ });
7715
+ }
7716
+ if (!l1.identity_cryptographic) {
7717
+ gaps.push({
7718
+ id: "GAP-L1-003",
7719
+ layer: "L1",
7720
+ severity: "critical",
7721
+ title: "No cryptographic agent identity",
7722
+ description: "Your agent has no cryptographic identity. It cannot prove it is who it claims to be to any counterparty, sign messages, or participate in sovereignty handshakes.",
7723
+ openclaw_relevance: env.openclaw_detected ? "OpenClaw has no cryptographic agent identity. Agent identity is implicit (tied to the process/session), not cryptographically verifiable." : null,
7724
+ sanctuary_solution: "Sanctuary provides Ed25519 self-custodied identity with key rotation and delegation. Use sanctuary/identity_create to establish your cryptographic identity."
7725
+ });
7726
+ }
7727
+ if (l2.approval_gate === "binary" && !l2.behavioral_anomaly_detection) {
7728
+ gaps.push({
7729
+ id: "GAP-L2-001",
7730
+ layer: "L2",
7731
+ severity: "high",
7732
+ title: "Binary approval gate (no anomaly detection)",
7733
+ description: "Your approval gate provides binary approve/deny gating without behavioral anomaly detection. Routine operations require the same manual approval as sensitive ones.",
7734
+ openclaw_relevance: env.openclaw_detected ? "OpenClaw's requireApproval hook provides binary approve/deny gating. Sanctuary's three-tier Principal Policy adds behavioral anomaly detection (auto-escalation when agent behavior deviates from baseline), encrypted audit trails, and graduated approval tiers \u2014 so routine operations auto-proceed while sensitive operations require explicit consent." : null,
7735
+ sanctuary_solution: "Sanctuary's three-tier Principal Policy gate auto-allows routine operations (Tier 3), escalates anomalous behavior (Tier 2), and always requires human approval for irreversible operations (Tier 1). Use sanctuary/principal_policy_view to inspect.",
7736
+ incident_class: INCIDENT_META_SEV1
7737
+ });
7738
+ } else if (l2.approval_gate === "none") {
7739
+ gaps.push({
7740
+ id: "GAP-L2-001",
7741
+ layer: "L2",
7742
+ severity: "critical",
7743
+ title: "No approval gate",
7744
+ description: "No approval gate is configured. All tool calls execute without oversight.",
7745
+ openclaw_relevance: null,
7746
+ sanctuary_solution: "Sanctuary's Principal Policy evaluates every tool call before execution. Enable it to get three-tier approval gating with behavioral anomaly detection.",
7747
+ incident_class: INCIDENT_META_SEV1
7748
+ });
7749
+ }
7750
+ if (l2.tool_sandboxing === "basic") {
7751
+ gaps.push({
7752
+ id: "GAP-L2-002",
7753
+ layer: "L2",
7754
+ severity: "medium",
7755
+ title: "Basic tool sandboxing (no cryptographic attestation)",
7756
+ description: "Your tool sandbox enforces allow/deny lists but provides no cryptographic attestation of execution context.",
7757
+ openclaw_relevance: env.openclaw_detected ? "OpenClaw's sandbox tool policy (tools.sandbox.tools) enforces allow/deny lists. Sanctuary adds cryptographic attestation of execution context \u2014 a verifiable proof that an operation ran within policy, not just that a policy was configured." : null,
7758
+ sanctuary_solution: "Sanctuary provides cryptographic execution attestation via sanctuary/exec_attest and policy-enforced sandboxing with encrypted audit trails.",
7759
+ incident_class: INCIDENT_OPENCLAW_SANDBOX
7760
+ });
7761
+ }
7762
+ if (!l2.context_gating) {
7763
+ gaps.push({
7764
+ id: "GAP-L2-003",
7765
+ layer: "L2",
7766
+ severity: "high",
7767
+ title: "No context gating for outbound inference calls",
7768
+ description: "Your agent sends its full context \u2014 conversation history, memory, preferences, internal reasoning \u2014 to remote LLM providers on every inference call. There is no mechanism to filter what leaves the sovereignty boundary. The provider sees everything the agent knows.",
7769
+ openclaw_relevance: env.openclaw_detected ? "OpenClaw sends full agent context (including MEMORY.md, tool results, and conversation history) to the configured LLM provider with every API call. There is no built-in context filtering." : null,
7770
+ sanctuary_solution: "Sanctuary's context gating (sanctuary/context_gate_set_policy + sanctuary/context_gate_filter) lets you define per-provider policies that control exactly what context flows outbound. Redact secrets, hash identifiers, and send only minimum-necessary context for each call.",
7771
+ incident_class: INCIDENT_CONTEXT_LEAKAGE
7772
+ });
7773
+ }
7774
+ if (!l2.audit_trail_exists) {
7775
+ gaps.push({
7776
+ id: "GAP-L2-004",
7777
+ layer: "L2",
7778
+ severity: "high",
7779
+ title: "No audit trail",
7780
+ description: "No audit trail exists for tool call history. There is no record of what operations were executed, when, or by whom.",
7781
+ openclaw_relevance: null,
7782
+ sanctuary_solution: "Sanctuary maintains an encrypted audit log of all operations, queryable via sanctuary/monitor_audit_log.",
7783
+ incident_class: INCIDENT_CLAUDE_CODE_LEAK
7784
+ });
7785
+ }
7786
+ if (l3.commitment_scheme === "none") {
7787
+ gaps.push({
7788
+ id: "GAP-L3-001",
7789
+ layer: "L3",
7790
+ severity: "high",
7791
+ title: "No selective disclosure capability",
7792
+ description: "Your agent has no cryptographic mechanism to prove facts about its state without revealing the state itself. Every disclosure is all-or-nothing: no commitments, no zero-knowledge proofs, no selective disclosure policies.",
7793
+ openclaw_relevance: env.openclaw_detected ? "OpenClaw has no selective disclosure mechanism. When your agent shares information, it shares everything or nothing \u2014 there is no way to prove a claim without revealing the underlying data." : null,
7794
+ sanctuary_solution: "Sanctuary's L3 provides SHA-256 + Pedersen commitments with genuine zero-knowledge proofs (Schnorr + range proofs via Fiat-Shamir transform). Your agent can prove it has a valid credential, sufficient reputation, or a completed transaction without exposing the underlying data. Use sanctuary/zk_commit and sanctuary/zk_prove.",
7795
+ incident_class: INCIDENT_META_SEV1
7796
+ });
7797
+ }
7798
+ if (!l4.reputation_portable) {
7799
+ gaps.push({
7800
+ id: "GAP-L4-001",
7801
+ layer: "L4",
7802
+ severity: "high",
7803
+ title: "No portable reputation",
7804
+ description: "Your agent's reputation is platform-locked. If you move to a different harness or platform, your track record doesn't follow.",
7805
+ openclaw_relevance: env.openclaw_detected ? "OpenClaw has no reputation system. Your agent's track record exists only in conversation history, which is not structured, signed, or portable." : null,
7806
+ sanctuary_solution: "Sanctuary's L4 provides signed EAS-compatible attestations that are self-custodied, portable, and cryptographically verifiable. Your reputation is yours, not your platform's. Use sanctuary/reputation_record to start building portable reputation."
7807
+ });
7808
+ }
7809
+ return gaps;
7810
+ }
7811
+ function generateRecommendations(env, l1, l2, l3, l4) {
7812
+ const recs = [];
7813
+ if (!l1.identity_cryptographic) {
7814
+ recs.push({
7815
+ priority: 1,
7816
+ action: "Create a cryptographic identity \u2014 your agent's foundation for all sovereignty operations",
7817
+ tool: "sanctuary/identity_create",
7818
+ effort: "immediate",
7819
+ impact: "critical"
7820
+ });
7821
+ }
7822
+ if (!l1.encryption_at_rest || env.openclaw_config && !env.openclaw_config.memory_encrypted) {
7823
+ recs.push({
7824
+ priority: 2,
7825
+ action: "Migrate plaintext agent state to Sanctuary's encrypted store",
7826
+ tool: "sanctuary/state_write",
7827
+ effort: "minutes",
7828
+ impact: "critical"
7829
+ });
7830
+ }
7831
+ recs.push({
7832
+ priority: 3,
7833
+ action: "Generate a Sovereignty Health Report to present to counterparties",
7834
+ tool: "sanctuary/shr_generate",
7835
+ effort: "immediate",
7836
+ impact: "high"
7837
+ });
7838
+ if (l2.approval_gate !== "three-tier") {
7839
+ recs.push({
7840
+ priority: 4,
7841
+ action: "Enable the three-tier Principal Policy gate for graduated approval",
7842
+ tool: "sanctuary/principal_policy_view",
7843
+ effort: "minutes",
7844
+ impact: "high"
7845
+ });
7846
+ }
7847
+ if (!l2.context_gating) {
7848
+ recs.push({
7849
+ priority: 5,
7850
+ action: "Configure context gating to control what flows to LLM providers",
7851
+ tool: "sanctuary/context_gate_set_policy",
7852
+ effort: "minutes",
7853
+ impact: "high"
7854
+ });
7855
+ }
7856
+ if (!l4.reputation_signed) {
7857
+ recs.push({
7858
+ priority: 6,
7859
+ action: "Start recording reputation attestations from completed interactions",
7860
+ tool: "sanctuary/reputation_record",
7861
+ effort: "minutes",
7862
+ impact: "medium"
7863
+ });
7864
+ }
7865
+ if (!l3.selective_disclosure_policy) {
7866
+ recs.push({
7867
+ priority: 7,
7868
+ action: "Configure selective disclosure policies for data sharing",
7869
+ tool: "sanctuary/disclosure_set_policy",
7870
+ effort: "hours",
7871
+ impact: "medium"
7872
+ });
7873
+ }
7874
+ return recs;
7875
+ }
7876
+ function formatAuditReport(result) {
7877
+ const { environment: env, layers, overall_score, sovereignty_level, gaps, recommendations } = result;
7878
+ const scoreBar = formatScoreBar(overall_score);
7879
+ const levelLabel = sovereignty_level.toUpperCase();
7880
+ let report = "";
7881
+ report += "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n";
7882
+ report += " SOVEREIGNTY AUDIT REPORT\n";
7883
+ report += ` Generated: ${result.audited_at}
7884
+ `;
7885
+ report += "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n";
7886
+ report += "\n";
7887
+ report += ` Overall Score: ${overall_score} / 100 ${scoreBar} ${levelLabel}
7888
+ `;
7889
+ report += "\n";
7890
+ report += " Environment:\n";
7891
+ report += ` \u2022 Sanctuary v${env.sanctuary_version ?? "?"} ${padDots("Sanctuary v" + (env.sanctuary_version ?? "?"))} ${env.sanctuary_installed ? "\u2713 installed" : "\u2717 not found"}
7892
+ `;
7893
+ if (env.openclaw_detected) {
7894
+ report += ` \u2022 OpenClaw ${padDots("OpenClaw")} \u2713 detected
7895
+ `;
7896
+ if (env.openclaw_config) {
7897
+ report += ` \u2022 OpenClaw requireApproval ${padDots("OpenClaw requireApproval")} ${env.openclaw_config.require_approval_enabled ? "\u2713 enabled" : "\u2717 disabled"}
7898
+ `;
7899
+ report += ` \u2022 OpenClaw sandbox policy ${padDots("OpenClaw sandbox policy")} ${env.openclaw_config.sandbox_policy_active ? "\u2713 active" : "\u2717 inactive"}
7900
+ `;
7901
+ }
7902
+ }
7903
+ report += "\n";
7904
+ const l1Score = scoreL1(layers.l1_cognitive);
7905
+ const l2Score = scoreL2(layers.l2_operational);
7906
+ const l3Score = scoreL3(layers.l3_selective_disclosure);
7907
+ const l4Score = scoreL4(layers.l4_reputation);
7908
+ report += " Layer Assessment:\n";
7909
+ report += " \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n";
7910
+ report += " \u2502 Layer \u2502 Status \u2502 Score \u2502\n";
7911
+ report += " \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n";
7912
+ report += ` \u2502 L1 Cognitive Sovereignty \u2502 ${padStatus(layers.l1_cognitive.status)} \u2502 ${padScore(l1Score, 35)} \u2502
7913
+ `;
7914
+ report += ` \u2502 L2 Operational Isolation \u2502 ${padStatus(layers.l2_operational.status)} \u2502 ${padScore(l2Score, 25)} \u2502
7915
+ `;
7916
+ if (layers.l2_operational.context_gating) {
7917
+ report += ` \u2502 \u2514 Context Gating \u2502 ACTIVE \u2502 \u2502
7918
+ `;
7919
+ }
7920
+ report += ` \u2502 L3 Selective Disclosure \u2502 ${padStatus(layers.l3_selective_disclosure.status)} \u2502 ${padScore(l3Score, 20)} \u2502
7921
+ `;
7922
+ report += ` \u2502 L4 Verifiable Reputation \u2502 ${padStatus(layers.l4_reputation.status)} \u2502 ${padScore(l4Score, 20)} \u2502
7923
+ `;
7924
+ report += " \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n";
7925
+ report += "\n";
7926
+ if (gaps.length > 0) {
7927
+ report += ` \u26A0 ${gaps.length} SOVEREIGNTY GAP${gaps.length !== 1 ? "S" : ""} FOUND
7928
+ `;
7929
+ report += "\n";
7930
+ for (const gap of gaps) {
7931
+ const severityLabel = `[${gap.severity.toUpperCase()}]`;
7932
+ report += ` ${severityLabel} ${gap.id}: ${gap.title}
7933
+ `;
7934
+ const descLines = wordWrap(gap.description, 66);
7935
+ for (const line of descLines) {
7936
+ report += ` ${line}
7937
+ `;
7938
+ }
7939
+ if (gap.incident_class) {
7940
+ const ic = gap.incident_class;
7941
+ const cveStr = ic.cves?.length ? ` (${ic.cves.join(", ")})` : "";
7942
+ report += ` \u2192 Incident precedent: ${ic.name}${cveStr} [${ic.date}]
7943
+ `;
7944
+ }
7945
+ report += ` \u2192 Fix: ${gap.sanctuary_solution.split(".")[0]}.
7946
+ `;
7947
+ if (gap.openclaw_relevance) {
7948
+ report += ` \u2192 OpenClaw context: ${gap.openclaw_relevance.split(".")[0]}.
7949
+ `;
7950
+ }
7951
+ report += "\n";
7952
+ }
7953
+ } else {
7954
+ report += " \u2713 NO SOVEREIGNTY GAPS FOUND\n";
7955
+ report += "\n";
7956
+ }
7957
+ if (recommendations.length > 0) {
7958
+ report += " RECOMMENDED NEXT STEPS (in order):\n";
7959
+ for (const rec of recommendations) {
7960
+ const effortLabel = rec.effort === "immediate" ? "immediate" : rec.effort === "minutes" ? "5 min" : "30 min";
7961
+ report += ` ${rec.priority}. [${effortLabel}] ${rec.action}`;
7962
+ if (rec.tool) {
7963
+ report += `: ${rec.tool}`;
7964
+ }
7965
+ report += "\n";
7966
+ }
7967
+ report += "\n";
7968
+ }
7969
+ report += "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n";
7970
+ return report;
7971
+ }
7972
+ function formatScoreBar(score) {
7973
+ const filled = Math.round(score / 10);
7974
+ return "[" + "\u25A0".repeat(filled) + "\u2591".repeat(10 - filled) + "]";
7975
+ }
7976
+ function padDots(label) {
7977
+ const totalWidth = 30;
7978
+ const dotsNeeded = Math.max(2, totalWidth - label.length - 4);
7979
+ return ".".repeat(dotsNeeded);
7980
+ }
7981
+ function padStatus(status) {
7982
+ const label = status.toUpperCase();
7983
+ return label + " ".repeat(Math.max(0, 8 - label.length));
7984
+ }
7985
+ function padScore(score, max) {
7986
+ const text = `${score}/${max}`;
7987
+ return " ".repeat(Math.max(0, 5 - text.length)) + text;
7988
+ }
7989
+ function wordWrap(text, maxWidth) {
7990
+ const words = text.split(" ");
7991
+ const lines = [];
7992
+ let current = "";
7993
+ for (const word of words) {
7994
+ if (current.length + word.length + 1 > maxWidth && current.length > 0) {
7995
+ lines.push(current);
7996
+ current = word;
7997
+ } else {
7998
+ current = current.length > 0 ? current + " " + word : word;
7999
+ }
8000
+ }
8001
+ if (current.length > 0) lines.push(current);
8002
+ return lines;
8003
+ }
8004
+
8005
+ // src/audit/tools.ts
8006
+ function createAuditTools(config) {
8007
+ const tools = [
8008
+ {
8009
+ name: "sanctuary/sovereignty_audit",
8010
+ description: "Audit your agent's sovereignty posture. Inspects the local environment for encryption, identity, approval gates, selective disclosure, and reputation \u2014 including OpenClaw-specific configurations. Returns a scored gap analysis with prioritized recommendations.",
8011
+ inputSchema: {
8012
+ type: "object",
8013
+ properties: {
8014
+ deep_scan: {
8015
+ type: "boolean",
8016
+ description: "If true (default), also scans for OpenClaw config, .env files, and memory files. Set to false for a Sanctuary-only assessment."
8017
+ }
8018
+ }
8019
+ },
8020
+ handler: async (args) => {
8021
+ const deepScan = args.deep_scan !== false;
8022
+ const env = await detectEnvironment(config, deepScan);
8023
+ const result = analyzeSovereignty(env, config);
8024
+ const report = formatAuditReport(result);
8025
+ return {
8026
+ content: [
8027
+ { type: "text", text: report },
8028
+ { type: "text", text: JSON.stringify(result, null, 2) }
8029
+ ]
8030
+ };
8031
+ }
8032
+ }
8033
+ ];
8034
+ return { tools };
8035
+ }
8036
+
8037
+ // src/l2-operational/context-gate.ts
8038
+ init_encoding();
8039
+ init_hashing();
8040
+ var MAX_CONTEXT_FIELDS = 1e3;
8041
+ var MAX_POLICY_RULES = 50;
8042
+ var MAX_PATTERNS_PER_ARRAY = 500;
8043
+ function evaluateField(policy, provider, field) {
8044
+ const exactRule = policy.rules.find((r) => r.provider === provider);
8045
+ const wildcardRule = policy.rules.find((r) => r.provider === "*");
8046
+ const matchedRule = exactRule ?? wildcardRule;
8047
+ if (!matchedRule) {
8048
+ return {
8049
+ field,
8050
+ action: policy.default_action === "deny" ? "deny" : "redact",
8051
+ reason: `No rule matches provider "${provider}"; applying default (${policy.default_action})`
8052
+ };
8053
+ }
8054
+ if (matchesPattern(field, matchedRule.redact)) {
8055
+ return {
8056
+ field,
8057
+ action: "redact",
8058
+ reason: `Field "${field}" is explicitly redacted for ${matchedRule.provider} provider`
8059
+ };
8060
+ }
8061
+ if (matchesPattern(field, matchedRule.hash)) {
8062
+ return {
8063
+ field,
8064
+ action: "hash",
8065
+ reason: `Field "${field}" is hashed for ${matchedRule.provider} provider`
8066
+ };
8067
+ }
8068
+ if (matchesPattern(field, matchedRule.summarize)) {
8069
+ return {
8070
+ field,
8071
+ action: "summarize",
8072
+ reason: `Field "${field}" should be summarized for ${matchedRule.provider} provider`
8073
+ };
8074
+ }
8075
+ if (matchesPattern(field, matchedRule.allow)) {
8076
+ return {
8077
+ field,
8078
+ action: "allow",
8079
+ reason: `Field "${field}" is allowed for ${matchedRule.provider} provider`
8080
+ };
8081
+ }
8082
+ return {
8083
+ field,
8084
+ action: policy.default_action === "deny" ? "deny" : "redact",
8085
+ reason: `Field "${field}" not addressed in ${matchedRule.provider} rule; applying default (${policy.default_action})`
8086
+ };
8087
+ }
8088
+ function filterContext(policy, provider, context) {
8089
+ const fields = Object.keys(context);
8090
+ if (fields.length > MAX_CONTEXT_FIELDS) {
8091
+ throw new Error(
8092
+ `Context object has ${fields.length} fields, exceeding limit of ${MAX_CONTEXT_FIELDS}`
8093
+ );
8094
+ }
8095
+ const decisions = [];
8096
+ let allowed = 0;
8097
+ let redacted = 0;
8098
+ let hashed = 0;
8099
+ let summarized = 0;
8100
+ let denied = 0;
8101
+ for (const field of fields) {
8102
+ const result = evaluateField(policy, provider, field);
8103
+ if (result.action === "hash") {
8104
+ const value = typeof context[field] === "string" ? context[field] : JSON.stringify(context[field]);
8105
+ result.hash_value = hashToString(stringToBytes(value));
8106
+ }
8107
+ decisions.push(result);
8108
+ switch (result.action) {
8109
+ case "allow":
8110
+ allowed++;
8111
+ break;
8112
+ case "redact":
8113
+ redacted++;
8114
+ break;
8115
+ case "hash":
8116
+ hashed++;
8117
+ break;
8118
+ case "summarize":
8119
+ summarized++;
8120
+ break;
8121
+ case "deny":
8122
+ denied++;
8123
+ break;
8124
+ }
8125
+ }
8126
+ const originalHash = hashToString(
8127
+ stringToBytes(JSON.stringify(context))
8128
+ );
8129
+ const filteredOutput = {};
8130
+ for (const decision of decisions) {
8131
+ switch (decision.action) {
8132
+ case "allow":
8133
+ filteredOutput[decision.field] = context[decision.field];
8134
+ break;
8135
+ case "redact":
8136
+ filteredOutput[decision.field] = "[REDACTED]";
8137
+ break;
8138
+ case "hash":
8139
+ filteredOutput[decision.field] = `[HASH:${decision.hash_value}]`;
8140
+ break;
8141
+ case "summarize":
8142
+ filteredOutput[decision.field] = "[SUMMARIZE]";
8143
+ break;
8144
+ }
8145
+ }
8146
+ const filteredHash = hashToString(
8147
+ stringToBytes(JSON.stringify(filteredOutput))
8148
+ );
8149
+ return {
8150
+ policy_id: policy.policy_id,
8151
+ provider,
8152
+ fields_allowed: allowed,
8153
+ fields_redacted: redacted,
8154
+ fields_hashed: hashed,
8155
+ fields_summarized: summarized,
8156
+ fields_denied: denied,
8157
+ decisions,
8158
+ original_context_hash: originalHash,
8159
+ filtered_context_hash: filteredHash,
8160
+ filtered_at: (/* @__PURE__ */ new Date()).toISOString()
8161
+ };
8162
+ }
8163
+ function matchesPattern(field, patterns) {
8164
+ const normalizedField = field.toLowerCase();
8165
+ for (const pattern of patterns) {
8166
+ if (pattern === "*") return true;
8167
+ const normalizedPattern = pattern.toLowerCase();
8168
+ if (normalizedPattern === normalizedField) return true;
8169
+ if (normalizedPattern.endsWith("*") && normalizedField.startsWith(normalizedPattern.slice(0, -1))) return true;
8170
+ if (normalizedPattern.startsWith("*") && normalizedField.endsWith(normalizedPattern.slice(1))) return true;
8171
+ }
8172
+ return false;
8173
+ }
8174
+ var ContextGatePolicyStore = class {
8175
+ storage;
8176
+ encryptionKey;
8177
+ policies = /* @__PURE__ */ new Map();
8178
+ constructor(storage, masterKey) {
8179
+ this.storage = storage;
8180
+ this.encryptionKey = derivePurposeKey(masterKey, "l2-context-gate");
8181
+ }
8182
+ /**
8183
+ * Create and store a new context-gating policy.
8184
+ */
8185
+ async create(policyName, rules, defaultAction, identityId) {
8186
+ const policyId = `cg-${Date.now()}-${toBase64url(randomBytes(8))}`;
8187
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8188
+ const policy = {
8189
+ policy_id: policyId,
8190
+ policy_name: policyName,
8191
+ rules,
8192
+ default_action: defaultAction,
8193
+ identity_id: identityId,
8194
+ created_at: now,
8195
+ updated_at: now
8196
+ };
8197
+ await this.persist(policy);
8198
+ this.policies.set(policyId, policy);
8199
+ return policy;
8200
+ }
8201
+ /**
8202
+ * Get a policy by ID.
8203
+ */
8204
+ async get(policyId) {
8205
+ if (this.policies.has(policyId)) {
8206
+ return this.policies.get(policyId);
8207
+ }
8208
+ const raw = await this.storage.read("_context_gate_policies", policyId);
8209
+ if (!raw) return null;
8210
+ try {
8211
+ const encrypted = JSON.parse(bytesToString(raw));
8212
+ const decrypted = decrypt(encrypted, this.encryptionKey);
8213
+ const policy = JSON.parse(bytesToString(decrypted));
8214
+ this.policies.set(policyId, policy);
8215
+ return policy;
8216
+ } catch {
8217
+ return null;
8218
+ }
8219
+ }
8220
+ /**
8221
+ * List all context-gating policies.
8222
+ */
8223
+ async list() {
8224
+ await this.loadAll();
8225
+ return Array.from(this.policies.values());
8226
+ }
8227
+ /**
8228
+ * Load all persisted policies into memory.
8229
+ */
8230
+ async loadAll() {
8231
+ try {
8232
+ const entries = await this.storage.list("_context_gate_policies");
8233
+ for (const meta of entries) {
8234
+ if (this.policies.has(meta.key)) continue;
8235
+ const raw = await this.storage.read("_context_gate_policies", meta.key);
8236
+ if (!raw) continue;
8237
+ try {
8238
+ const encrypted = JSON.parse(bytesToString(raw));
8239
+ const decrypted = decrypt(encrypted, this.encryptionKey);
8240
+ const policy = JSON.parse(bytesToString(decrypted));
8241
+ this.policies.set(policy.policy_id, policy);
8242
+ } catch {
8243
+ }
8244
+ }
8245
+ } catch {
8246
+ }
8247
+ }
8248
+ async persist(policy) {
8249
+ const serialized = stringToBytes(JSON.stringify(policy));
8250
+ const encrypted = encrypt(serialized, this.encryptionKey);
8251
+ await this.storage.write(
8252
+ "_context_gate_policies",
8253
+ policy.policy_id,
8254
+ stringToBytes(JSON.stringify(encrypted))
8255
+ );
8256
+ }
8257
+ };
8258
+
8259
+ // src/l2-operational/context-gate-templates.ts
8260
+ var ALWAYS_REDACT_SECRETS = [
8261
+ "api_key",
8262
+ "secret_*",
8263
+ "*_secret",
8264
+ "*_token",
8265
+ "*_key",
8266
+ "password",
8267
+ "*_password",
8268
+ "credential",
8269
+ "*_credential",
8270
+ "private_key",
8271
+ "recovery_key",
8272
+ "passphrase",
8273
+ "auth_*"
8274
+ ];
8275
+ var PII_PATTERNS = [
8276
+ "*_pii",
8277
+ "name",
8278
+ "full_name",
8279
+ "email",
8280
+ "email_address",
8281
+ "phone",
8282
+ "phone_number",
8283
+ "address",
8284
+ "ssn",
8285
+ "date_of_birth",
8286
+ "ip_address",
8287
+ "credit_card",
8288
+ "card_number",
8289
+ "cvv",
8290
+ "bank_account",
8291
+ "account_number",
8292
+ "routing_number"
8293
+ ];
8294
+ var INTERNAL_STATE_PATTERNS = [
8295
+ "memory",
8296
+ "agent_memory",
8297
+ "internal_reasoning",
8298
+ "internal_state",
8299
+ "reasoning_trace",
8300
+ "chain_of_thought",
8301
+ "private_notes",
8302
+ "soul",
8303
+ "personality",
8304
+ "system_prompt"
8305
+ ];
8306
+ var ID_PATTERNS = [
8307
+ "user_id",
8308
+ "session_id",
8309
+ "agent_id",
8310
+ "identity_id",
8311
+ "conversation_id",
8312
+ "thread_id"
8313
+ ];
8314
+ var HISTORY_PATTERNS = [
8315
+ "conversation_history",
8316
+ "message_history",
8317
+ "chat_history",
8318
+ "context_window",
8319
+ "previous_messages"
8320
+ ];
8321
+ var INFERENCE_MINIMAL = {
8322
+ id: "inference-minimal",
8323
+ name: "Inference Minimal",
8324
+ description: "Maximum privacy. Only the current task and query reach the LLM provider.",
8325
+ use_when: "You want the strictest possible context control for inference calls. The LLM sees only what it needs for the immediate task.",
8326
+ rules: [
8327
+ {
8328
+ provider: "inference",
8329
+ allow: [
8330
+ "task",
8331
+ "task_description",
8332
+ "current_query",
8333
+ "query",
8334
+ "prompt",
8335
+ "question",
8336
+ "instruction"
8337
+ ],
8338
+ redact: [
8339
+ ...ALWAYS_REDACT_SECRETS,
8340
+ ...PII_PATTERNS,
8341
+ ...INTERNAL_STATE_PATTERNS,
8342
+ ...HISTORY_PATTERNS,
8343
+ "tool_results",
8344
+ "previous_results"
8345
+ ],
8346
+ hash: [...ID_PATTERNS],
8347
+ summarize: []
8348
+ }
8349
+ ],
8350
+ default_action: "redact"
8351
+ };
8352
+ var INFERENCE_STANDARD = {
8353
+ id: "inference-standard",
8354
+ name: "Inference Standard",
8355
+ description: "Balanced privacy. Task, query, and tool results pass through. History flagged for summarization. Secrets and PII redacted.",
8356
+ use_when: "You need the LLM to have enough context for multi-step tasks while keeping secrets, PII, and internal reasoning private.",
8357
+ rules: [
8358
+ {
8359
+ provider: "inference",
8360
+ allow: [
8361
+ "task",
8362
+ "task_description",
8363
+ "current_query",
8364
+ "query",
8365
+ "prompt",
8366
+ "question",
8367
+ "instruction",
8368
+ "tool_results",
8369
+ "tool_output",
8370
+ "previous_results",
8371
+ "current_step",
8372
+ "remaining_steps",
8373
+ "objective",
8374
+ "constraints",
8375
+ "format",
8376
+ "output_format"
8377
+ ],
8378
+ redact: [
8379
+ ...ALWAYS_REDACT_SECRETS,
8380
+ ...PII_PATTERNS,
8381
+ ...INTERNAL_STATE_PATTERNS
8382
+ ],
8383
+ hash: [...ID_PATTERNS],
8384
+ summarize: [...HISTORY_PATTERNS]
8385
+ }
8386
+ ],
8387
+ default_action: "redact"
8388
+ };
8389
+ var LOGGING_STRICT = {
8390
+ id: "logging-strict",
8391
+ name: "Logging Strict",
8392
+ description: "Redacts all content for logging and analytics providers. Only operation metadata passes through.",
8393
+ use_when: "You send telemetry to logging or analytics services and want usage metrics without any content exposure.",
8394
+ rules: [
8395
+ {
8396
+ provider: "logging",
8397
+ allow: [
8398
+ "operation",
8399
+ "operation_name",
8400
+ "tool_name",
8401
+ "timestamp",
8402
+ "duration_ms",
8403
+ "status",
8404
+ "error_code",
8405
+ "event_type"
8406
+ ],
8407
+ redact: [
8408
+ ...ALWAYS_REDACT_SECRETS,
8409
+ ...PII_PATTERNS,
8410
+ ...INTERNAL_STATE_PATTERNS,
8411
+ ...HISTORY_PATTERNS
8412
+ ],
8413
+ hash: [...ID_PATTERNS],
8414
+ summarize: []
8415
+ },
8416
+ {
8417
+ provider: "analytics",
8418
+ allow: [
8419
+ "event_type",
8420
+ "timestamp",
8421
+ "duration_ms",
8422
+ "status",
8423
+ "tool_name"
8424
+ ],
8425
+ redact: [
8426
+ ...ALWAYS_REDACT_SECRETS,
8427
+ ...PII_PATTERNS,
8428
+ ...INTERNAL_STATE_PATTERNS,
8429
+ ...HISTORY_PATTERNS
8430
+ ],
8431
+ hash: [...ID_PATTERNS],
8432
+ summarize: []
8433
+ }
8434
+ ],
8435
+ default_action: "redact"
8436
+ };
8437
+ var TOOL_API_SCOPED = {
8438
+ id: "tool-api-scoped",
8439
+ name: "Tool API Scoped",
8440
+ description: "Allows tool-specific parameters for external API calls. Redacts memory, history, secrets, and PII.",
8441
+ use_when: "Your agent calls external APIs (search, database, web) and you want to send query parameters without full agent context. Note: 'headers' and 'body' are redacted by default because they frequently carry authorization tokens. Add them to 'allow' only if you verify they contain no credentials for your use case.",
8442
+ rules: [
8443
+ {
8444
+ provider: "tool-api",
8445
+ allow: [
8446
+ "task",
8447
+ "task_description",
8448
+ "query",
8449
+ "search_query",
8450
+ "tool_input",
8451
+ "tool_parameters",
8452
+ "url",
8453
+ "endpoint",
8454
+ "method",
8455
+ "filter",
8456
+ "sort",
8457
+ "limit",
8458
+ "offset"
8459
+ ],
8460
+ redact: [
8461
+ ...ALWAYS_REDACT_SECRETS,
8462
+ ...PII_PATTERNS,
8463
+ ...INTERNAL_STATE_PATTERNS,
8464
+ ...HISTORY_PATTERNS
8465
+ ],
8466
+ hash: [...ID_PATTERNS],
8467
+ summarize: []
8468
+ }
8469
+ ],
8470
+ default_action: "redact"
8471
+ };
8472
+ var TEMPLATES = {
8473
+ "inference-minimal": INFERENCE_MINIMAL,
8474
+ "inference-standard": INFERENCE_STANDARD,
8475
+ "logging-strict": LOGGING_STRICT,
8476
+ "tool-api-scoped": TOOL_API_SCOPED
8477
+ };
8478
+ function listTemplateIds() {
8479
+ return Object.keys(TEMPLATES);
8480
+ }
8481
+ function getTemplate(id) {
8482
+ return TEMPLATES[id];
8483
+ }
8484
+
8485
+ // src/l2-operational/context-gate-recommend.ts
8486
+ var CLASSIFICATION_RULES = [
8487
+ // ── Secrets (always redact, high confidence) ─────────────────────
8488
+ {
8489
+ patterns: [
8490
+ "api_key",
8491
+ "apikey",
8492
+ "api_secret",
8493
+ "secret",
8494
+ "secret_key",
8495
+ "secret_token",
8496
+ "password",
8497
+ "passwd",
8498
+ "pass",
8499
+ "credential",
8500
+ "credentials",
8501
+ "private_key",
8502
+ "privkey",
8503
+ "recovery_key",
8504
+ "passphrase",
8505
+ "token",
8506
+ "access_token",
8507
+ "refresh_token",
8508
+ "bearer_token",
8509
+ "auth_token",
8510
+ "auth_header",
8511
+ "authorization",
8512
+ "encryption_key",
8513
+ "master_key",
8514
+ "signing_key",
8515
+ "webhook_secret",
8516
+ "client_secret",
8517
+ "connection_string"
8518
+ ],
8519
+ action: "redact",
8520
+ confidence: "high",
8521
+ reason: "Matches known secret/credential pattern"
8522
+ },
8523
+ // ── PII (always redact, high confidence) ─────────────────────────
8524
+ {
8525
+ patterns: [
8526
+ "name",
8527
+ "full_name",
8528
+ "first_name",
8529
+ "last_name",
8530
+ "display_name",
8531
+ "email",
8532
+ "email_address",
8533
+ "phone",
8534
+ "phone_number",
8535
+ "mobile",
8536
+ "address",
8537
+ "street_address",
8538
+ "mailing_address",
8539
+ "ssn",
8540
+ "social_security",
8541
+ "date_of_birth",
8542
+ "dob",
8543
+ "birthday",
8544
+ "ip_address",
8545
+ "ip",
8546
+ "location",
8547
+ "geolocation",
8548
+ "coordinates",
8549
+ "credit_card",
8550
+ "card_number",
8551
+ "cvv",
8552
+ "bank_account",
8553
+ "routing_number",
8554
+ "passport",
8555
+ "drivers_license",
8556
+ "license_number"
8557
+ ],
8558
+ action: "redact",
8559
+ confidence: "high",
8560
+ reason: "Matches known PII pattern"
8561
+ },
8562
+ // ── Internal agent state (redact, high confidence) ───────────────
8563
+ {
8564
+ patterns: [
8565
+ "memory",
8566
+ "agent_memory",
8567
+ "long_term_memory",
8568
+ "internal_reasoning",
8569
+ "reasoning_trace",
8570
+ "chain_of_thought",
8571
+ "internal_state",
8572
+ "agent_state",
8573
+ "private_notes",
8574
+ "scratchpad",
8575
+ "soul",
8576
+ "personality",
8577
+ "persona",
8578
+ "system_prompt",
8579
+ "system_message",
8580
+ "system_instruction",
8581
+ "preferences",
8582
+ "user_preferences",
8583
+ "agent_preferences",
8584
+ "beliefs",
8585
+ "goals",
8586
+ "motivations"
8587
+ ],
8588
+ action: "redact",
8589
+ confidence: "high",
8590
+ reason: "Matches known internal agent state pattern"
8591
+ },
8592
+ // ── IDs (hash, medium confidence) ────────────────────────────────
8593
+ {
8594
+ patterns: [
8595
+ "user_id",
8596
+ "userid",
8597
+ "session_id",
8598
+ "sessionid",
8599
+ "agent_id",
8600
+ "agentid",
8601
+ "identity_id",
8602
+ "conversation_id",
8603
+ "thread_id",
8604
+ "threadid",
8605
+ "request_id",
8606
+ "requestid",
8607
+ "correlation_id",
8608
+ "trace_id",
8609
+ "traceid",
8610
+ "account_id",
8611
+ "accountid"
8612
+ ],
8613
+ action: "hash",
8614
+ confidence: "medium",
8615
+ reason: "Matches known identifier pattern \u2014 hash preserves correlation without exposing value"
8616
+ },
8617
+ // ── History (summarize, medium confidence) ───────────────────────
8618
+ {
8619
+ patterns: [
8620
+ "conversation_history",
8621
+ "chat_history",
8622
+ "message_history",
8623
+ "messages",
8624
+ "previous_messages",
8625
+ "prior_messages",
8626
+ "context_window",
8627
+ "interaction_history",
8628
+ "audit_log",
8629
+ "event_log"
8630
+ ],
8631
+ action: "summarize",
8632
+ confidence: "medium",
8633
+ reason: "Matches known history/log pattern \u2014 summarize to reduce exposure"
8634
+ },
8635
+ // ── Task/query (allow, medium confidence) ────────────────────────
8636
+ {
8637
+ patterns: [
8638
+ "task",
8639
+ "task_description",
8640
+ "query",
8641
+ "current_query",
8642
+ "search_query",
8643
+ "prompt",
8644
+ "user_prompt",
8645
+ "question",
8646
+ "current_question",
8647
+ "instruction",
8648
+ "instructions",
8649
+ "objective",
8650
+ "goal",
8651
+ "current_step",
8652
+ "next_step",
8653
+ "remaining_steps",
8654
+ "constraints",
8655
+ "requirements",
8656
+ "output_format",
8657
+ "format",
8658
+ "tool_results",
8659
+ "tool_output",
8660
+ "tool_input",
8661
+ "tool_parameters"
8662
+ ],
8663
+ action: "allow",
8664
+ confidence: "medium",
8665
+ reason: "Matches known task/query pattern \u2014 likely needed for inference"
8666
+ }
8667
+ ];
8668
+ function classifyField(fieldName) {
8669
+ const normalized = fieldName.toLowerCase().trim();
8670
+ for (const rule of CLASSIFICATION_RULES) {
8671
+ for (const pattern of rule.patterns) {
8672
+ if (matchesFieldPattern(normalized, pattern)) {
8673
+ return {
8674
+ field: fieldName,
8675
+ recommended_action: rule.action,
8676
+ reason: rule.reason,
8677
+ confidence: rule.confidence,
8678
+ matched_pattern: pattern
8679
+ };
8680
+ }
8681
+ }
8682
+ }
8683
+ return {
8684
+ field: fieldName,
8685
+ recommended_action: "redact",
8686
+ reason: "No known pattern matched \u2014 defaulting to redact (conservative)",
8687
+ confidence: "low",
8688
+ matched_pattern: null
8689
+ };
8690
+ }
8691
+ function recommendPolicy(context, provider = "inference") {
8692
+ const fields = Object.keys(context);
8693
+ const classifications = fields.map(classifyField);
8694
+ const warnings = [];
8695
+ const allow = [];
8696
+ const redact = [];
8697
+ const hash2 = [];
8698
+ const summarize = [];
8699
+ for (const c of classifications) {
8700
+ switch (c.recommended_action) {
8701
+ case "allow":
8702
+ allow.push(c.field);
8703
+ break;
8704
+ case "redact":
8705
+ redact.push(c.field);
8706
+ break;
8707
+ case "hash":
8708
+ hash2.push(c.field);
8709
+ break;
8710
+ case "summarize":
8711
+ summarize.push(c.field);
8712
+ break;
8713
+ }
8714
+ }
8715
+ const lowConfidence = classifications.filter((c) => c.confidence === "low");
8716
+ if (lowConfidence.length > 0) {
8717
+ warnings.push(
8718
+ `${lowConfidence.length} field(s) could not be classified by pattern and will default to redact: ${lowConfidence.map((c) => c.field).join(", ")}. Review these manually.`
8719
+ );
8720
+ }
8721
+ for (const [key, value] of Object.entries(context)) {
8722
+ if (typeof value === "string" && value.length > 5e3) {
8723
+ const existing = classifications.find((c) => c.field === key);
8724
+ if (existing && existing.recommended_action === "allow") {
8725
+ warnings.push(
8726
+ `Field "${key}" is allowed but contains ${value.length} characters. Consider summarizing it to reduce context size and exposure.`
8727
+ );
8728
+ }
8729
+ }
8730
+ }
8731
+ return {
8732
+ provider,
8733
+ classifications,
8734
+ recommended_rules: { allow, redact, hash: hash2, summarize },
8735
+ default_action: "redact",
8736
+ summary: {
8737
+ total_fields: fields.length,
8738
+ allow: allow.length,
8739
+ redact: redact.length,
8740
+ hash: hash2.length,
8741
+ summarize: summarize.length
8742
+ },
8743
+ warnings
8744
+ };
8745
+ }
8746
+ function matchesFieldPattern(normalizedField, pattern) {
8747
+ if (normalizedField === pattern) return true;
8748
+ if (pattern.length >= 3 && normalizedField.includes(pattern)) {
8749
+ const idx = normalizedField.indexOf(pattern);
8750
+ const before = idx === 0 || normalizedField[idx - 1] === "_" || normalizedField[idx - 1] === "-";
8751
+ const after = idx + pattern.length === normalizedField.length || normalizedField[idx + pattern.length] === "_" || normalizedField[idx + pattern.length] === "-";
8752
+ return before && after;
8753
+ }
8754
+ return false;
8755
+ }
8756
+
8757
+ // src/l2-operational/context-gate-tools.ts
8758
+ function createContextGateTools(storage, masterKey, auditLog) {
8759
+ const policyStore = new ContextGatePolicyStore(storage, masterKey);
8760
+ const tools = [
8761
+ // ── Set Policy ──────────────────────────────────────────────────
8762
+ {
8763
+ name: "sanctuary/context_gate_set_policy",
8764
+ description: "Create a context-gating policy that controls what information flows to remote providers (LLM APIs, tool APIs, logging services). Each rule specifies a provider category and which context fields to allow, redact, hash, or flag for summarization. Redact rules take absolute priority \u2014 if a field is in both 'allow' and 'redact', it is redacted. Default action applies to any field not mentioned in any rule. Use this to prevent your full agent context from being sent to remote LLM providers during inference calls.",
8765
+ inputSchema: {
8766
+ type: "object",
8767
+ properties: {
8768
+ policy_name: {
8769
+ type: "string",
8770
+ description: "Human-readable name for this policy (e.g., 'inference-minimal', 'tool-api-strict')"
8771
+ },
8772
+ rules: {
8773
+ type: "array",
8774
+ description: "Array of rules. Each rule has: provider (inference|tool-api|logging|analytics|peer-agent|custom|*), allow (fields to pass through), redact (fields to remove \u2014 highest priority), hash (fields to replace with SHA-256 hash), summarize (fields to flag for compression).",
8775
+ items: {
8776
+ type: "object",
8777
+ properties: {
8778
+ provider: {
8779
+ type: "string",
8780
+ description: "Provider category: inference, tool-api, logging, analytics, peer-agent, custom, or * for all"
8781
+ },
8782
+ allow: {
8783
+ type: "array",
8784
+ items: { type: "string" },
8785
+ description: "Fields/patterns to allow through (e.g., 'task_description', 'current_query', 'tool_*')"
8786
+ },
8787
+ redact: {
8788
+ type: "array",
8789
+ items: { type: "string" },
8790
+ description: "Fields/patterns to redact (e.g., 'conversation_history', 'secret_*', '*_pii'). Takes absolute priority."
8791
+ },
8792
+ hash: {
8793
+ type: "array",
8794
+ items: { type: "string" },
8795
+ description: "Fields/patterns to replace with SHA-256 hash (e.g., 'user_id', 'session_id')"
8796
+ },
8797
+ summarize: {
8798
+ type: "array",
8799
+ items: { type: "string" },
8800
+ description: "Fields/patterns to flag for summarization (advisory \u2014 agent should compress these before sending)"
8801
+ }
8802
+ },
8803
+ required: ["provider", "allow", "redact"]
8804
+ }
8805
+ },
8806
+ default_action: {
8807
+ type: "string",
8808
+ enum: ["redact", "deny"],
8809
+ description: "Action for fields not matched by any rule. 'redact' removes the field value; 'deny' blocks the entire request. Default: 'redact'."
8810
+ },
8811
+ identity_id: {
8812
+ type: "string",
8813
+ description: "Bind this policy to a specific identity (optional)"
8814
+ }
8815
+ },
8816
+ required: ["policy_name", "rules"]
8817
+ },
8818
+ handler: async (args) => {
8819
+ const policyName = args.policy_name;
8820
+ const rawRules = args.rules;
8821
+ const defaultAction = args.default_action ?? "redact";
8822
+ const identityId = args.identity_id;
8823
+ if (!Array.isArray(rawRules)) {
8824
+ return toolResult({ error: "invalid_rules", message: "rules must be an array" });
8825
+ }
8826
+ if (rawRules.length > MAX_POLICY_RULES) {
8827
+ return toolResult({
8828
+ error: "too_many_rules",
8829
+ message: `Policy has ${rawRules.length} rules, exceeding limit of ${MAX_POLICY_RULES}`
8830
+ });
8831
+ }
8832
+ const rules = [];
8833
+ for (const r of rawRules) {
8834
+ const allow = Array.isArray(r.allow) ? r.allow : [];
8835
+ const redact = Array.isArray(r.redact) ? r.redact : [];
8836
+ const hash2 = Array.isArray(r.hash) ? r.hash : [];
8837
+ const summarize = Array.isArray(r.summarize) ? r.summarize : [];
8838
+ for (const [name, arr] of [["allow", allow], ["redact", redact], ["hash", hash2], ["summarize", summarize]]) {
8839
+ if (arr.length > MAX_PATTERNS_PER_ARRAY) {
8840
+ return toolResult({
8841
+ error: "too_many_patterns",
8842
+ message: `Rule ${name} array has ${arr.length} patterns, exceeding limit of ${MAX_PATTERNS_PER_ARRAY}`
8843
+ });
8844
+ }
8845
+ }
8846
+ rules.push({
8847
+ provider: r.provider ?? "*",
8848
+ allow,
8849
+ redact,
8850
+ hash: hash2,
8851
+ summarize
8852
+ });
8853
+ }
8854
+ const policy = await policyStore.create(
8855
+ policyName,
8856
+ rules,
8857
+ defaultAction,
8858
+ identityId
8859
+ );
8860
+ auditLog.append("l2", "context_gate_set_policy", identityId ?? "system", {
8861
+ policy_id: policy.policy_id,
8862
+ policy_name: policyName,
8863
+ rule_count: rules.length,
8864
+ default_action: defaultAction
8865
+ });
8866
+ return toolResult({
8867
+ policy_id: policy.policy_id,
8868
+ policy_name: policy.policy_name,
8869
+ rules: policy.rules,
8870
+ default_action: policy.default_action,
8871
+ created_at: policy.created_at,
8872
+ message: "Context-gating policy created. Use sanctuary/context_gate_filter to apply this policy before making outbound calls."
8873
+ });
8874
+ }
8875
+ },
8876
+ // ── Apply Template ───────────────────────────────────────────────
8877
+ {
8878
+ name: "sanctuary/context_gate_apply_template",
8879
+ description: "Apply a starter context-gating template. Available templates: inference-minimal (strictest \u2014 only task and query pass through), inference-standard (balanced \u2014 adds tool results, summarizes history), logging-strict (redacts all content for telemetry services), tool-api-scoped (allows tool parameters, redacts agent state). Templates are starting points \u2014 customize after applying.",
8880
+ inputSchema: {
8881
+ type: "object",
8882
+ properties: {
8883
+ template_id: {
8884
+ type: "string",
8885
+ description: "Template to apply: inference-minimal, inference-standard, logging-strict, or tool-api-scoped"
8886
+ },
8887
+ identity_id: {
8888
+ type: "string",
8889
+ description: "Bind this policy to a specific identity (optional)"
8890
+ }
8891
+ },
8892
+ required: ["template_id"]
8893
+ },
8894
+ handler: async (args) => {
8895
+ const templateId = args.template_id;
8896
+ const identityId = args.identity_id;
8897
+ const template = getTemplate(templateId);
8898
+ if (!template) {
8899
+ return toolResult({
8900
+ error: "template_not_found",
8901
+ message: `Unknown template "${templateId}"`,
8902
+ available_templates: listTemplateIds().map((id) => {
8903
+ const t = TEMPLATES[id];
8904
+ return { id, name: t.name, description: t.description };
8905
+ })
8906
+ });
8907
+ }
8908
+ const policy = await policyStore.create(
8909
+ template.name,
8910
+ template.rules,
8911
+ template.default_action,
8912
+ identityId
8913
+ );
8914
+ auditLog.append("l2", "context_gate_apply_template", identityId ?? "system", {
8915
+ policy_id: policy.policy_id,
8916
+ template_id: templateId
8917
+ });
8918
+ return toolResult({
8919
+ policy_id: policy.policy_id,
8920
+ template_applied: templateId,
8921
+ policy_name: template.name,
8922
+ description: template.description,
8923
+ use_when: template.use_when,
8924
+ rules: policy.rules,
8925
+ default_action: policy.default_action,
8926
+ created_at: policy.created_at,
8927
+ message: "Template applied. Use sanctuary/context_gate_filter with this policy_id to filter context before outbound calls. Customize rules with sanctuary/context_gate_set_policy if needed."
8928
+ });
8929
+ }
8930
+ },
8931
+ // ── Recommend Policy ────────────────────────────────────────────
8932
+ {
8933
+ name: "sanctuary/context_gate_recommend",
8934
+ description: "Analyze a sample context object and recommend a context-gating policy based on field name heuristics. Classifies each field as allow, redact, hash, or summarize with confidence levels. Returns a ready-to-apply rule set. When in doubt, recommends redact (conservative). Review the recommendations before applying.",
8935
+ inputSchema: {
8936
+ type: "object",
8937
+ properties: {
8938
+ context: {
8939
+ type: "object",
8940
+ description: "A sample context object to analyze. Each top-level key will be classified. Values are inspected for size warnings but not stored."
8941
+ },
8942
+ provider: {
8943
+ type: "string",
8944
+ description: "Provider category to generate rules for. Default: 'inference'."
8945
+ }
8946
+ },
8947
+ required: ["context"]
8948
+ },
8949
+ handler: async (args) => {
8950
+ const context = args.context;
8951
+ const provider = args.provider ?? "inference";
8952
+ const contextKeys = Object.keys(context);
8953
+ if (contextKeys.length > MAX_CONTEXT_FIELDS) {
8954
+ return toolResult({
8955
+ error: "context_too_large",
8956
+ message: `Context has ${contextKeys.length} fields, exceeding limit of ${MAX_CONTEXT_FIELDS}`
8957
+ });
8958
+ }
8959
+ const recommendation = recommendPolicy(context, provider);
8960
+ auditLog.append("l2", "context_gate_recommend", "system", {
8961
+ provider,
8962
+ fields_analyzed: recommendation.summary.total_fields,
8963
+ fields_allow: recommendation.summary.allow,
8964
+ fields_redact: recommendation.summary.redact,
8965
+ fields_hash: recommendation.summary.hash,
8966
+ fields_summarize: recommendation.summary.summarize
8967
+ });
8968
+ return toolResult({
8969
+ ...recommendation,
8970
+ next_steps: "Review the classifications above. If they look correct, you can apply them directly with sanctuary/context_gate_set_policy using the recommended_rules. Or start with a template via sanctuary/context_gate_apply_template and customize from there.",
8971
+ available_templates: listTemplateIds().map((id) => {
8972
+ const t = TEMPLATES[id];
8973
+ return { id, name: t.name, description: t.description };
8974
+ })
8975
+ });
8976
+ }
8977
+ },
8978
+ // ── Filter Context ──────────────────────────────────────────────
8979
+ {
8980
+ name: "sanctuary/context_gate_filter",
8981
+ description: "Filter agent context through a gating policy before sending to a remote provider. Returns per-field decisions (allow, redact, hash, summarize) and content hashes for the audit trail. Call this BEFORE making any outbound API call to ensure you are only sending the minimum necessary context. The filtered output tells you exactly what can be sent safely.",
8982
+ inputSchema: {
8983
+ type: "object",
8984
+ properties: {
8985
+ policy_id: {
8986
+ type: "string",
8987
+ description: "ID of the context-gating policy to apply"
8988
+ },
8989
+ provider: {
8990
+ type: "string",
8991
+ description: "Provider category for this call: inference, tool-api, logging, analytics, peer-agent, or custom"
8992
+ },
8993
+ context: {
8994
+ type: "object",
8995
+ description: "The context object to filter. Each top-level key is evaluated against the policy. Example keys: task_description, conversation_history, user_preferences, api_keys, memory, internal_reasoning"
8996
+ }
8997
+ },
8998
+ required: ["policy_id", "provider", "context"]
8999
+ },
9000
+ handler: async (args) => {
9001
+ const policyId = args.policy_id;
9002
+ const provider = args.provider;
9003
+ const context = args.context;
9004
+ const contextKeys = Object.keys(context);
9005
+ if (contextKeys.length > MAX_CONTEXT_FIELDS) {
9006
+ return toolResult({
9007
+ error: "context_too_large",
9008
+ message: `Context has ${contextKeys.length} fields, exceeding limit of ${MAX_CONTEXT_FIELDS}`
9009
+ });
9010
+ }
9011
+ const policy = await policyStore.get(policyId);
9012
+ if (!policy) {
9013
+ return toolResult({
9014
+ error: "policy_not_found",
9015
+ message: `No context-gating policy found with ID "${policyId}"`
9016
+ });
9017
+ }
9018
+ const result = filterContext(policy, provider, context);
9019
+ const deniedFields = result.decisions.filter((d) => d.action === "deny");
9020
+ if (deniedFields.length > 0) {
9021
+ auditLog.append("l2", "context_gate_deny", policy.identity_id ?? "system", {
9022
+ policy_id: policyId,
9023
+ provider,
9024
+ denied_fields: deniedFields.map((d) => d.field),
9025
+ original_context_hash: result.original_context_hash
9026
+ });
9027
+ return toolResult({
9028
+ blocked: true,
9029
+ reason: "Context contains fields that trigger deny action",
9030
+ denied_fields: deniedFields.map((d) => ({
9031
+ field: d.field,
9032
+ reason: d.reason
9033
+ })),
9034
+ recommendation: "Remove the denied fields from context before retrying, or update the policy to handle these fields differently."
9035
+ });
9036
+ }
9037
+ const safeContext = {};
9038
+ for (const decision of result.decisions) {
9039
+ switch (decision.action) {
9040
+ case "allow":
9041
+ safeContext[decision.field] = context[decision.field];
9042
+ break;
9043
+ case "redact":
9044
+ break;
9045
+ case "hash":
9046
+ safeContext[decision.field] = decision.hash_value;
9047
+ break;
9048
+ case "summarize":
9049
+ safeContext[decision.field] = context[decision.field];
9050
+ break;
9051
+ }
9052
+ }
9053
+ auditLog.append("l2", "context_gate_filter", policy.identity_id ?? "system", {
9054
+ policy_id: policyId,
9055
+ provider,
9056
+ fields_total: Object.keys(context).length,
9057
+ fields_allowed: result.fields_allowed,
9058
+ fields_redacted: result.fields_redacted,
9059
+ fields_hashed: result.fields_hashed,
9060
+ fields_summarized: result.fields_summarized,
9061
+ original_context_hash: result.original_context_hash,
9062
+ filtered_context_hash: result.filtered_context_hash
9063
+ });
9064
+ return toolResult({
9065
+ blocked: false,
9066
+ safe_context: safeContext,
9067
+ summary: {
9068
+ total_fields: Object.keys(context).length,
9069
+ allowed: result.fields_allowed,
9070
+ redacted: result.fields_redacted,
9071
+ hashed: result.fields_hashed,
9072
+ summarized: result.fields_summarized
9073
+ },
9074
+ decisions: result.decisions,
9075
+ audit: {
9076
+ original_context_hash: result.original_context_hash,
9077
+ filtered_context_hash: result.filtered_context_hash,
9078
+ filtered_at: result.filtered_at
9079
+ },
9080
+ guidance: result.fields_summarized > 0 ? "Some fields are marked for summarization. Consider compressing them before sending to reduce context size and information exposure." : void 0
9081
+ });
9082
+ }
9083
+ },
9084
+ // ── List Policies ───────────────────────────────────────────────
9085
+ {
9086
+ name: "sanctuary/context_gate_list_policies",
9087
+ description: "List all configured context-gating policies. Returns policy IDs, names, rule summaries, and default actions.",
9088
+ inputSchema: {
9089
+ type: "object",
9090
+ properties: {}
9091
+ },
9092
+ handler: async () => {
9093
+ const policies = await policyStore.list();
9094
+ auditLog.append("l2", "context_gate_list_policies", "system", {
9095
+ policy_count: policies.length
9096
+ });
9097
+ return toolResult({
9098
+ policies: policies.map((p) => ({
9099
+ policy_id: p.policy_id,
9100
+ policy_name: p.policy_name,
9101
+ rule_count: p.rules.length,
9102
+ providers: p.rules.map((r) => r.provider),
9103
+ default_action: p.default_action,
9104
+ identity_id: p.identity_id ?? null,
9105
+ created_at: p.created_at,
9106
+ updated_at: p.updated_at
9107
+ })),
9108
+ count: policies.length,
9109
+ message: policies.length === 0 ? "No context-gating policies configured. Use sanctuary/context_gate_set_policy to create one." : `${policies.length} context-gating ${policies.length === 1 ? "policy" : "policies"} configured.`
9110
+ });
9111
+ }
9112
+ }
9113
+ ];
9114
+ return { tools, policyStore };
9115
+ }
9116
+ function checkMemoryProtection() {
9117
+ const checks = {
9118
+ aslr_enabled: checkASLR(),
9119
+ stack_canaries: true,
9120
+ // Enabled by default in Node.js runtime
9121
+ secure_buffer_zeros: true,
9122
+ // We use crypto.randomBytes and explicit zeroing
9123
+ argon2id_kdf: true
9124
+ // Master key derivation uses Argon2id
9125
+ };
9126
+ const activeCount = Object.values(checks).filter((v) => v).length;
9127
+ const overall = activeCount >= 4 ? "full" : activeCount >= 3 ? "partial" : "minimal";
9128
+ return {
9129
+ ...checks,
9130
+ overall
9131
+ };
9132
+ }
9133
+ function checkASLR() {
9134
+ if (process.platform === "linux") {
9135
+ try {
9136
+ const result = execSync("cat /proc/sys/kernel/randomize_va_space", {
9137
+ encoding: "utf-8",
9138
+ stdio: ["pipe", "pipe", "ignore"]
9139
+ }).trim();
9140
+ return result === "2";
9141
+ } catch {
9142
+ return false;
9143
+ }
9144
+ }
9145
+ if (process.platform === "darwin") {
9146
+ return true;
9147
+ }
9148
+ return false;
9149
+ }
9150
+ function checkProcessIsolation() {
9151
+ const isContainer = detectContainer();
9152
+ const isVM = detectVM();
9153
+ const isSandboxed = detectSandbox();
9154
+ let isolationLevel = "none";
9155
+ if (isContainer) isolationLevel = "hardened";
9156
+ else if (isVM) isolationLevel = "hardened";
9157
+ else if (isSandboxed) isolationLevel = "basic";
9158
+ const details = {};
9159
+ if (isContainer && isContainer !== true) details.container_type = isContainer;
9160
+ if (isVM && isVM !== true) details.vm_type = isVM;
9161
+ if (isSandboxed && isSandboxed !== true) details.sandbox_type = isSandboxed;
9162
+ return {
9163
+ isolation_level: isolationLevel,
9164
+ is_container: isContainer !== false,
9165
+ is_vm: isVM !== false,
9166
+ is_sandboxed: isSandboxed !== false,
9167
+ is_tee: false,
9168
+ details
9169
+ };
9170
+ }
9171
+ function detectContainer() {
9172
+ try {
9173
+ if (process.env.DOCKER_HOST) return "docker";
9174
+ try {
9175
+ statSync("/.dockerenv");
9176
+ return "docker";
9177
+ } catch {
9178
+ }
9179
+ if (process.platform === "linux") {
9180
+ const cgroup = execSync("cat /proc/1/cgroup 2>/dev/null || echo ''", {
9181
+ encoding: "utf-8"
9182
+ });
9183
+ if (cgroup.includes("docker")) return "docker";
9184
+ if (cgroup.includes("lxc")) return "lxc";
9185
+ if (cgroup.includes("kubepods") || cgroup.includes("kubernetes")) return "kubernetes";
9186
+ }
9187
+ if (process.env.container === "podman") return "podman";
9188
+ if (process.env.CONTAINER_ID) return "oci";
9189
+ return false;
9190
+ } catch {
9191
+ return false;
9192
+ }
9193
+ }
9194
+ function detectVM() {
9195
+ if (process.platform === "linux") {
9196
+ try {
9197
+ const dmidecode = execSync("dmidecode -s system-product-name 2>/dev/null || echo ''", {
9198
+ encoding: "utf-8"
9199
+ }).toLowerCase();
9200
+ if (dmidecode.includes("vmware")) return "vmware";
9201
+ if (dmidecode.includes("virtualbox")) return "virtualbox";
9202
+ if (dmidecode.includes("kvm")) return "kvm";
9203
+ if (dmidecode.includes("xen")) return "xen";
9204
+ if (dmidecode.includes("hyper-v")) return "hyper-v";
9205
+ const cpuinfo = execSync("grep -i hypervisor /proc/cpuinfo || echo ''", {
9206
+ encoding: "utf-8"
9207
+ });
9208
+ if (cpuinfo.length > 0) return "detected";
9209
+ } catch {
9210
+ }
9211
+ }
9212
+ if (process.platform === "darwin") {
9213
+ try {
9214
+ const bootargs = execSync(
9215
+ "nvram boot-args 2>/dev/null | grep -i 'parallels\\|vmware\\|virtualbox' || echo ''",
9216
+ {
9217
+ encoding: "utf-8"
9218
+ }
9219
+ );
9220
+ if (bootargs.length > 0) return "detected";
9221
+ } catch {
9222
+ }
9223
+ }
9224
+ return false;
9225
+ }
9226
+ function detectSandbox() {
9227
+ if (process.platform === "darwin") {
9228
+ if (process.env.APP_SANDBOX_READ_ONLY_HOME === "1") return "app-sandbox";
9229
+ if (process.env.TMPDIR && process.env.TMPDIR.includes("AppSandbox")) return "app-sandbox";
9230
+ }
9231
+ if (process.platform === "openbsd") {
9232
+ try {
9233
+ const pledge = execSync("pledge -v 2>/dev/null || echo ''", {
9234
+ encoding: "utf-8"
9235
+ });
9236
+ if (pledge.length > 0) return "pledge";
9237
+ } catch {
9238
+ }
9239
+ }
9240
+ if (process.platform === "linux") {
9241
+ if (process.env.container === "lxc") return "lxc";
9242
+ try {
9243
+ const context = execSync("getenforce 2>/dev/null || echo ''", {
9244
+ encoding: "utf-8"
9245
+ }).trim();
9246
+ if (context === "Enforcing") return "selinux";
9247
+ } catch {
9248
+ }
9249
+ }
9250
+ return false;
9251
+ }
9252
+ function checkFilesystemPermissions(storagePath) {
9253
+ try {
9254
+ const stats = statSync(storagePath);
9255
+ const mode = stats.mode & parseInt("777", 8);
9256
+ const modeString = mode.toString(8).padStart(3, "0");
9257
+ const isSecure = mode === parseInt("700", 8);
9258
+ const groupReadable = (mode & parseInt("040", 8)) !== 0;
9259
+ const othersReadable = (mode & parseInt("007", 8)) !== 0;
9260
+ const currentUid = process.getuid?.() || -1;
9261
+ const ownerIsCurrentUser = stats.uid === currentUid;
9262
+ let overall = "secure";
9263
+ if (groupReadable || othersReadable) overall = "insecure";
9264
+ else if (!ownerIsCurrentUser) overall = "warning";
9265
+ return {
9266
+ sanctuary_storage_protected: isSecure,
9267
+ sanctuary_storage_mode: modeString,
9268
+ owner_is_current_user: ownerIsCurrentUser,
9269
+ group_readable: groupReadable,
9270
+ others_readable: othersReadable,
9271
+ overall
9272
+ };
9273
+ } catch {
9274
+ return {
9275
+ sanctuary_storage_protected: false,
9276
+ sanctuary_storage_mode: "unknown",
9277
+ owner_is_current_user: false,
9278
+ group_readable: false,
9279
+ others_readable: false,
9280
+ overall: "warning"
9281
+ };
9282
+ }
9283
+ }
9284
+ function checkRuntimeIntegrity() {
9285
+ return {
9286
+ config_hash_stable: true,
9287
+ environment_state: "clean",
9288
+ discrepancies: []
9289
+ };
9290
+ }
9291
+ function assessL2Hardening(storagePath) {
9292
+ const memory = checkMemoryProtection();
9293
+ const isolation = checkProcessIsolation();
9294
+ const filesystem = checkFilesystemPermissions(storagePath);
9295
+ const integrity = checkRuntimeIntegrity();
9296
+ let checksPassed = 0;
9297
+ let checksTotal = 0;
9298
+ if (memory.aslr_enabled) checksPassed++;
9299
+ checksTotal++;
9300
+ if (memory.stack_canaries) checksPassed++;
9301
+ checksTotal++;
9302
+ if (memory.secure_buffer_zeros) checksPassed++;
9303
+ checksTotal++;
9304
+ if (memory.argon2id_kdf) checksPassed++;
9305
+ checksTotal++;
9306
+ if (isolation.is_container) checksPassed++;
9307
+ checksTotal++;
9308
+ if (isolation.is_vm) checksPassed++;
9309
+ checksTotal++;
9310
+ if (isolation.is_sandboxed) checksPassed++;
9311
+ checksTotal++;
9312
+ if (filesystem.sanctuary_storage_protected) checksPassed++;
9313
+ checksTotal++;
9314
+ {
9315
+ checksPassed++;
9316
+ }
9317
+ checksTotal++;
9318
+ let hardeningLevel = isolation.isolation_level;
9319
+ if (filesystem.overall === "insecure" || memory.overall === "none" || memory.overall === "minimal") {
9320
+ if (hardeningLevel === "hardened") {
9321
+ hardeningLevel = "basic";
9322
+ } else if (hardeningLevel === "basic") {
9323
+ hardeningLevel = "none";
9324
+ }
9325
+ }
9326
+ const summaryParts = [];
9327
+ if (isolation.is_container || isolation.is_vm) {
9328
+ summaryParts.push(`Running in ${isolation.details.container_type || isolation.details.vm_type || "isolated environment"}`);
9329
+ }
9330
+ if (memory.aslr_enabled) {
9331
+ summaryParts.push("ASLR enabled");
9332
+ }
9333
+ if (filesystem.sanctuary_storage_protected) {
9334
+ summaryParts.push("Storage permissions secured (0700)");
9335
+ }
9336
+ const summary = summaryParts.length > 0 ? summaryParts.join("; ") : "No process-level hardening detected";
9337
+ return {
9338
+ hardening_level: hardeningLevel,
9339
+ memory_protection: memory,
9340
+ process_isolation: isolation,
9341
+ filesystem_permissions: filesystem,
9342
+ runtime_integrity: integrity,
9343
+ checks_passed: checksPassed,
9344
+ checks_total: checksTotal,
9345
+ summary
9346
+ };
9347
+ }
9348
+
9349
+ // src/l2-operational/hardening-tools.ts
9350
+ function createL2HardeningTools(storagePath, auditLog) {
9351
+ return [
9352
+ {
9353
+ name: "sanctuary/l2_hardening_status",
9354
+ description: "L2 Process Hardening Status \u2014 Verify software-based operational isolation. Reports memory protection, process isolation level, filesystem permissions, and overall hardening assessment. Read-only. Tier 3 \u2014 always allowed.",
9355
+ inputSchema: {
9356
+ type: "object",
9357
+ properties: {
9358
+ include_details: {
9359
+ type: "boolean",
9360
+ description: "If true, include detailed check results for memory, process, and filesystem. If false, show summary only.",
9361
+ default: false
9362
+ }
9363
+ }
9364
+ },
9365
+ handler: async (args) => {
9366
+ const includeDetails = args.include_details ?? false;
9367
+ const status = assessL2Hardening(storagePath);
9368
+ auditLog.append(
9369
+ "l2",
9370
+ "l2_hardening_status",
9371
+ "system",
9372
+ { include_details: includeDetails }
9373
+ );
9374
+ if (includeDetails) {
9375
+ return toolResult({
9376
+ hardening_level: status.hardening_level,
9377
+ summary: status.summary,
9378
+ checks_passed: status.checks_passed,
9379
+ checks_total: status.checks_total,
9380
+ memory_protection: {
9381
+ aslr_enabled: status.memory_protection.aslr_enabled,
9382
+ stack_canaries: status.memory_protection.stack_canaries,
9383
+ secure_buffer_zeros: status.memory_protection.secure_buffer_zeros,
9384
+ argon2id_kdf: status.memory_protection.argon2id_kdf,
9385
+ overall: status.memory_protection.overall
9386
+ },
9387
+ process_isolation: {
9388
+ isolation_level: status.process_isolation.isolation_level,
9389
+ is_container: status.process_isolation.is_container,
9390
+ is_vm: status.process_isolation.is_vm,
9391
+ is_sandboxed: status.process_isolation.is_sandboxed,
9392
+ is_tee: status.process_isolation.is_tee,
9393
+ details: status.process_isolation.details
9394
+ },
9395
+ filesystem_permissions: {
9396
+ sanctuary_storage_protected: status.filesystem_permissions.sanctuary_storage_protected,
9397
+ sanctuary_storage_mode: status.filesystem_permissions.sanctuary_storage_mode,
9398
+ owner_is_current_user: status.filesystem_permissions.owner_is_current_user,
9399
+ group_readable: status.filesystem_permissions.group_readable,
9400
+ others_readable: status.filesystem_permissions.others_readable,
9401
+ overall: status.filesystem_permissions.overall
9402
+ },
9403
+ runtime_integrity: {
9404
+ config_hash_stable: status.runtime_integrity.config_hash_stable,
9405
+ environment_state: status.runtime_integrity.environment_state,
9406
+ discrepancies: status.runtime_integrity.discrepancies
9407
+ }
9408
+ });
9409
+ } else {
9410
+ return toolResult({
9411
+ hardening_level: status.hardening_level,
9412
+ summary: status.summary,
9413
+ checks_passed: status.checks_passed,
9414
+ checks_total: status.checks_total,
9415
+ note: "Pass include_details: true to see full breakdown of memory, process isolation, and filesystem checks."
9416
+ });
9417
+ }
9418
+ }
9419
+ },
9420
+ {
9421
+ name: "sanctuary/l2_verify_isolation",
9422
+ description: "Verify L2 process isolation at runtime. Checks whether the Sanctuary server is running in an isolated environment (container, VM, sandbox) and validates filesystem and memory protections. Reports isolation level and any issues. Read-only. Tier 3 \u2014 always allowed.",
9423
+ inputSchema: {
9424
+ type: "object",
9425
+ properties: {
9426
+ check_filesystem: {
9427
+ type: "boolean",
9428
+ description: "If true, verify Sanctuary storage directory permissions.",
9429
+ default: true
9430
+ },
9431
+ check_memory: {
9432
+ type: "boolean",
9433
+ description: "If true, verify memory protection mechanisms (ASLR, etc.).",
9434
+ default: true
9435
+ },
9436
+ check_process: {
9437
+ type: "boolean",
9438
+ description: "If true, detect container, VM, or sandbox environment.",
9439
+ default: true
9440
+ }
9441
+ }
9442
+ },
9443
+ handler: async (args) => {
9444
+ const checkFilesystem = args.check_filesystem ?? true;
9445
+ const checkMemory = args.check_memory ?? true;
9446
+ const checkProcess = args.check_process ?? true;
9447
+ const status = assessL2Hardening(storagePath);
9448
+ auditLog.append(
9449
+ "l2",
9450
+ "l2_verify_isolation",
9451
+ "system",
9452
+ {
9453
+ check_filesystem: checkFilesystem,
9454
+ check_memory: checkMemory,
9455
+ check_process: checkProcess
9456
+ }
9457
+ );
9458
+ const results = {
9459
+ isolation_level: status.hardening_level,
9460
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
9461
+ };
9462
+ if (checkFilesystem) {
9463
+ const fs = status.filesystem_permissions;
9464
+ results.filesystem = {
9465
+ sanctuary_storage_protected: fs.sanctuary_storage_protected,
9466
+ storage_mode: fs.sanctuary_storage_mode,
9467
+ is_secure: fs.overall === "secure",
9468
+ issues: fs.overall === "insecure" ? [
9469
+ "Storage directory is readable by group or others. Recommend: chmod 700 on Sanctuary storage path."
9470
+ ] : fs.overall === "warning" ? [
9471
+ "Storage directory not owned by current user. Verify correct user is running Sanctuary."
9472
+ ] : []
9473
+ };
9474
+ }
9475
+ if (checkMemory) {
9476
+ const mem = status.memory_protection;
9477
+ const issues = [];
9478
+ if (!mem.aslr_enabled) {
9479
+ issues.push(
9480
+ "ASLR not detected. On Linux, enable with: echo 2 | sudo tee /proc/sys/kernel/randomize_va_space"
9481
+ );
9482
+ }
9483
+ results.memory = {
9484
+ aslr_enabled: mem.aslr_enabled,
9485
+ stack_canaries: mem.stack_canaries,
9486
+ secure_buffer_handling: mem.secure_buffer_zeros,
9487
+ argon2id_key_derivation: mem.argon2id_kdf,
9488
+ protection_level: mem.overall,
9489
+ issues
9490
+ };
9491
+ }
9492
+ if (checkProcess) {
9493
+ const iso = status.process_isolation;
9494
+ results.process = {
9495
+ isolation_level: iso.isolation_level,
9496
+ in_container: iso.is_container,
9497
+ in_vm: iso.is_vm,
9498
+ sandboxed: iso.is_sandboxed,
9499
+ has_tee: iso.is_tee,
9500
+ environment: iso.details,
9501
+ recommendation: iso.isolation_level === "none" ? "Consider running Sanctuary in a container or VM for improved isolation." : iso.isolation_level === "basic" ? "Basic isolation detected. Container or VM would provide stronger guarantees." : "Running in isolated environment \u2014 process-level isolation is strong."
9502
+ };
9503
+ }
9504
+ return toolResult({
9505
+ status: "verified",
9506
+ results
9507
+ });
9508
+ }
9509
+ }
9510
+ ];
9511
+ }
9512
+
9513
+ // src/index.ts
9514
+ init_encoding();
9515
+
9516
+ // src/storage/memory.ts
9517
+ var MemoryStorage = class {
9518
+ store = /* @__PURE__ */ new Map();
9519
+ storageKey(namespace, key) {
9520
+ return `${namespace}/${key}`;
9521
+ }
9522
+ async write(namespace, key, data) {
9523
+ this.store.set(this.storageKey(namespace, key), {
9524
+ data: new Uint8Array(data),
9525
+ // Copy to prevent external mutation
9526
+ modified_at: (/* @__PURE__ */ new Date()).toISOString()
9527
+ });
9528
+ }
9529
+ async read(namespace, key) {
9530
+ const entry = this.store.get(this.storageKey(namespace, key));
9531
+ if (!entry) return null;
9532
+ return new Uint8Array(entry.data);
6673
9533
  }
6674
9534
  async delete(namespace, key, _secureOverwrite) {
6675
9535
  return this.store.delete(this.storageKey(namespace, key));
@@ -6740,15 +9600,51 @@ async function createSanctuaryServer(options) {
6740
9600
  }
6741
9601
  } else {
6742
9602
  keyProtection = "recovery-key";
6743
- const existing = await storage.read("_meta", "recovery-key-hash");
6744
- if (existing) {
6745
- masterKey = generateRandomKey();
6746
- recoveryKey = toBase64url(masterKey);
9603
+ const { hashToString: hashToString2 } = await Promise.resolve().then(() => (init_hashing(), hashing_exports));
9604
+ const { stringToBytes: stringToBytes2, bytesToString: bytesToString2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
9605
+ const { fromBase64url: fromBase64url2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
9606
+ const { constantTimeEqual: constantTimeEqual2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
9607
+ const existingHash = await storage.read("_meta", "recovery-key-hash");
9608
+ if (existingHash) {
9609
+ const envRecoveryKey = process.env.SANCTUARY_RECOVERY_KEY;
9610
+ if (!envRecoveryKey) {
9611
+ throw new Error(
9612
+ "Sanctuary: Existing encrypted data found but no credentials provided.\nThis installation was previously set up with a recovery key.\n\nTo start the server, provide one of:\n - SANCTUARY_PASSPHRASE (if you later configured a passphrase)\n - SANCTUARY_RECOVERY_KEY (the recovery key shown at first run)\n\nWithout the correct credentials, encrypted state cannot be accessed.\nRefusing to start to prevent silent data loss."
9613
+ );
9614
+ }
9615
+ let recoveryKeyBytes;
9616
+ try {
9617
+ recoveryKeyBytes = fromBase64url2(envRecoveryKey);
9618
+ } catch {
9619
+ throw new Error(
9620
+ "Sanctuary: SANCTUARY_RECOVERY_KEY is not valid base64url. The recovery key should be the exact string shown at first run."
9621
+ );
9622
+ }
9623
+ if (recoveryKeyBytes.length !== 32) {
9624
+ throw new Error(
9625
+ "Sanctuary: SANCTUARY_RECOVERY_KEY has incorrect length. The recovery key should be the exact string shown at first run."
9626
+ );
9627
+ }
9628
+ const providedHash = hashToString2(recoveryKeyBytes);
9629
+ const storedHash = bytesToString2(existingHash);
9630
+ const providedHashBytes = stringToBytes2(providedHash);
9631
+ const storedHashBytes = stringToBytes2(storedHash);
9632
+ if (!constantTimeEqual2(providedHashBytes, storedHashBytes)) {
9633
+ throw new Error(
9634
+ "Sanctuary: Recovery key does not match the stored key hash.\nThe recovery key provided via SANCTUARY_RECOVERY_KEY is incorrect.\nUse the exact recovery key that was displayed at first run."
9635
+ );
9636
+ }
9637
+ masterKey = recoveryKeyBytes;
6747
9638
  } else {
9639
+ const existingNamespaces = await storage.list("_meta");
9640
+ const hasKeyParams = existingNamespaces.some((e) => e.key === "key-params");
9641
+ if (hasKeyParams) {
9642
+ throw new Error(
9643
+ "Sanctuary: Found existing key derivation parameters but no recovery key hash.\nThis indicates a corrupted or incomplete installation.\nIf you previously used a passphrase, set SANCTUARY_PASSPHRASE to start."
9644
+ );
9645
+ }
6748
9646
  masterKey = generateRandomKey();
6749
9647
  recoveryKey = toBase64url(masterKey);
6750
- const { hashToString: hashToString2 } = await Promise.resolve().then(() => (init_hashing(), hashing_exports));
6751
- const { stringToBytes: stringToBytes2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
6752
9648
  const keyHash = hashToString2(masterKey);
6753
9649
  await storage.write(
6754
9650
  "_meta",
@@ -6835,7 +9731,7 @@ async function createSanctuaryServer(options) {
6835
9731
  layer: "l2",
6836
9732
  description: "Process-level isolation only (no TEE)",
6837
9733
  severity: "warning",
6838
- mitigation: "TEE support planned for v0.3.0"
9734
+ mitigation: "TEE support planned for a future release"
6839
9735
  });
6840
9736
  if (config.disclosure.proof_system === "commitment-only") {
6841
9737
  degradations.push({
@@ -6975,7 +9871,7 @@ async function createSanctuaryServer(options) {
6975
9871
  },
6976
9872
  limitations: [
6977
9873
  "L1 identity uses ed25519 only; KERI support planned for v0.2.0",
6978
- "L2 isolation is process-level only; TEE support planned for v0.3.0",
9874
+ "L2 isolation is process-level only; TEE support planned for a future release",
6979
9875
  "L3 uses commitment schemes only; ZK proofs planned for v0.2.0",
6980
9876
  "L4 Sybil resistance is escrow-based only",
6981
9877
  "Spec license: CC-BY-4.0 | Code license: Apache-2.0"
@@ -6996,7 +9892,7 @@ async function createSanctuaryServer(options) {
6996
9892
  masterKey,
6997
9893
  auditLog
6998
9894
  );
6999
- const { tools: l4Tools } = createL4Tools(
9895
+ const { tools: l4Tools} = createL4Tools(
7000
9896
  storage,
7001
9897
  masterKey,
7002
9898
  identityManager,
@@ -7014,6 +9910,13 @@ async function createSanctuaryServer(options) {
7014
9910
  auditLog,
7015
9911
  handshakeResults
7016
9912
  );
9913
+ const { tools: auditTools } = createAuditTools(config);
9914
+ const { tools: contextGateTools } = createContextGateTools(
9915
+ storage,
9916
+ masterKey,
9917
+ auditLog
9918
+ );
9919
+ const hardeningTools = createL2HardeningTools(config.storage_path, auditLog);
7017
9920
  const policy = await loadPrincipalPolicy(config.storage_path);
7018
9921
  const baseline = new BaselineTracker(storage, masterKey);
7019
9922
  await baseline.load();
@@ -7029,7 +9932,7 @@ async function createSanctuaryServer(options) {
7029
9932
  port: config.dashboard.port,
7030
9933
  host: config.dashboard.host,
7031
9934
  timeout_seconds: policy.approval_channel.timeout_seconds,
7032
- auto_deny: policy.approval_channel.auto_deny,
9935
+ // SEC-002: auto_deny removed — timeout always denies
7033
9936
  auth_token: authToken,
7034
9937
  tls: config.dashboard.tls
7035
9938
  });
@@ -7042,8 +9945,8 @@ async function createSanctuaryServer(options) {
7042
9945
  webhook_secret: config.webhook.secret,
7043
9946
  callback_port: config.webhook.callback_port,
7044
9947
  callback_host: config.webhook.callback_host,
7045
- timeout_seconds: policy.approval_channel.timeout_seconds,
7046
- auto_deny: policy.approval_channel.auto_deny
9948
+ timeout_seconds: policy.approval_channel.timeout_seconds
9949
+ // SEC-002: auto_deny removed — timeout always denies
7047
9950
  });
7048
9951
  await webhook.start();
7049
9952
  approvalChannel = webhook;
@@ -7062,6 +9965,9 @@ async function createSanctuaryServer(options) {
7062
9965
  ...handshakeTools,
7063
9966
  ...federationTools,
7064
9967
  ...bridgeTools,
9968
+ ...auditTools,
9969
+ ...contextGateTools,
9970
+ ...hardeningTools,
7065
9971
  manifestTool
7066
9972
  ];
7067
9973
  const server = createServer(allTools, { gate });
@@ -7087,6 +9993,6 @@ async function createSanctuaryServer(options) {
7087
9993
  return { server, config };
7088
9994
  }
7089
9995
 
7090
- export { ApprovalGate, AuditLog, AutoApproveChannel, BaselineTracker, CallbackApprovalChannel, CommitmentStore, DashboardApprovalChannel, FederationRegistry, FilesystemStorage, MemoryStorage, PolicyStore, ReputationStore, StateStore, StderrApprovalChannel, TIER_WEIGHTS, WebhookApprovalChannel, canonicalize, completeHandshake, computeWeightedScore, createBridgeCommitment, createPedersenCommitment, createProofOfKnowledge, createRangeProof, createSanctuaryServer, generateSHR, initiateHandshake, loadConfig, loadPrincipalPolicy, resolveTier, respondToHandshake, signPayload, tierDistribution, verifyBridgeCommitment, verifyCompletion, verifyPedersenCommitment, verifyProofOfKnowledge, verifyRangeProof, verifySHR, verifySignature };
9996
+ export { ApprovalGate, AuditLog, AutoApproveChannel, BaselineTracker, TEMPLATES as CONTEXT_GATE_TEMPLATES, CallbackApprovalChannel, CommitmentStore, ContextGatePolicyStore, DashboardApprovalChannel, FederationRegistry, FilesystemStorage, MemoryStorage, PolicyStore, ReputationStore, StateStore, StderrApprovalChannel, TIER_WEIGHTS, WebhookApprovalChannel, canonicalize, classifyField, completeHandshake, computeWeightedScore, createBridgeCommitment, createPedersenCommitment, createProofOfKnowledge, createRangeProof, createSanctuaryServer, evaluateField, filterContext, generateSHR, getTemplate, initiateHandshake, listTemplateIds, loadConfig, loadPrincipalPolicy, recommendPolicy, resolveTier, respondToHandshake, signPayload, tierDistribution, verifyBridgeCommitment, verifyCompletion, verifyPedersenCommitment, verifyProofOfKnowledge, verifyRangeProof, verifySHR, verifySignature };
7091
9997
  //# sourceMappingURL=index.js.map
7092
9998
  //# sourceMappingURL=index.js.map