@sanctuary-framework/mcp-server 0.3.1 → 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/cli.cjs CHANGED
@@ -7,6 +7,7 @@ var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
7
7
  var promises = require('fs/promises');
8
8
  var path = require('path');
9
9
  var os = require('os');
10
+ var module$1 = require('module');
10
11
  var crypto = require('crypto');
11
12
  var aes_js = require('@noble/ciphers/aes.js');
12
13
  var ed25519 = require('@noble/curves/ed25519');
@@ -17,7 +18,9 @@ var types_js = require('@modelcontextprotocol/sdk/types.js');
17
18
  var http = require('http');
18
19
  var https = require('https');
19
20
  var fs = require('fs');
21
+ var child_process = require('child_process');
20
22
 
23
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
21
24
  var __defProp = Object.defineProperty;
22
25
  var __getOwnPropNames = Object.getOwnPropertyNames;
23
26
  var __esm = (fn, res) => function __init() {
@@ -206,9 +209,11 @@ var init_hashing = __esm({
206
209
  init_encoding();
207
210
  }
208
211
  });
212
+ var require2 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)));
213
+ var { version: PKG_VERSION } = require2("../package.json");
209
214
  function defaultConfig() {
210
215
  return {
211
- version: "0.3.0",
216
+ version: PKG_VERSION,
212
217
  storage_path: path.join(os.homedir(), ".sanctuary"),
213
218
  state: {
214
219
  encryption: "aes-256-gcm",
@@ -334,6 +339,18 @@ function validateConfig(config) {
334
339
  `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
340
  );
336
341
  }
342
+ const implementedDisclosurePolicy = /* @__PURE__ */ new Set(["minimum-necessary"]);
343
+ if (!implementedDisclosurePolicy.has(config.disclosure.default_policy)) {
344
+ errors.push(
345
+ `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.`
346
+ );
347
+ }
348
+ const implementedReputationMode = /* @__PURE__ */ new Set(["self-custodied"]);
349
+ if (!implementedReputationMode.has(config.reputation.mode)) {
350
+ errors.push(
351
+ `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.`
352
+ );
353
+ }
337
354
  if (errors.length > 0) {
338
355
  throw new Error(
339
356
  `Sanctuary configuration references unimplemented features:
@@ -1039,6 +1056,8 @@ var StateStore = class {
1039
1056
  };
1040
1057
  }
1041
1058
  };
1059
+ var require3 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)));
1060
+ var { version: PKG_VERSION2 } = require3("../package.json");
1042
1061
  var MAX_STRING_BYTES = 1048576;
1043
1062
  var MAX_BUNDLE_BYTES = 5242880;
1044
1063
  var BUNDLE_FIELDS = /* @__PURE__ */ new Set(["bundle"]);
@@ -1121,7 +1140,7 @@ function createServer(tools, options) {
1121
1140
  const server = new index_js.Server(
1122
1141
  {
1123
1142
  name: "sanctuary-mcp-server",
1124
- version: "0.3.0"
1143
+ version: PKG_VERSION2
1125
1144
  },
1126
1145
  {
1127
1146
  capabilities: {
@@ -3575,7 +3594,9 @@ var DEFAULT_POLICY = {
3575
3594
  "state_delete",
3576
3595
  "identity_rotate",
3577
3596
  "reputation_import",
3578
- "bootstrap_provide_guarantee"
3597
+ "reputation_export",
3598
+ "bootstrap_provide_guarantee",
3599
+ "decommission_certificate"
3579
3600
  ],
3580
3601
  tier2_anomaly: DEFAULT_TIER2,
3581
3602
  tier3_always_allow: [
@@ -3592,7 +3613,6 @@ var DEFAULT_POLICY = {
3592
3613
  "disclosure_evaluate",
3593
3614
  "reputation_record",
3594
3615
  "reputation_query",
3595
- "reputation_export",
3596
3616
  "bootstrap_create_escrow",
3597
3617
  "exec_attest",
3598
3618
  "monitor_health",
@@ -3614,7 +3634,14 @@ var DEFAULT_POLICY = {
3614
3634
  "zk_prove",
3615
3635
  "zk_verify",
3616
3636
  "zk_range_prove",
3617
- "zk_range_verify"
3637
+ "zk_range_verify",
3638
+ "context_gate_set_policy",
3639
+ "context_gate_apply_template",
3640
+ "context_gate_recommend",
3641
+ "context_gate_filter",
3642
+ "context_gate_list_policies",
3643
+ "l2_hardening_status",
3644
+ "l2_verify_isolation"
3618
3645
  ],
3619
3646
  approval_channel: DEFAULT_CHANNEL
3620
3647
  };
@@ -3716,6 +3743,7 @@ tier1_always_approve:
3716
3743
  - state_delete
3717
3744
  - identity_rotate
3718
3745
  - reputation_import
3746
+ - reputation_export
3719
3747
  - bootstrap_provide_guarantee
3720
3748
 
3721
3749
  # \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
@@ -3745,7 +3773,6 @@ tier3_always_allow:
3745
3773
  - disclosure_evaluate
3746
3774
  - reputation_record
3747
3775
  - reputation_query
3748
- - reputation_export
3749
3776
  - bootstrap_create_escrow
3750
3777
  - exec_attest
3751
3778
  - monitor_health
@@ -3768,6 +3795,11 @@ tier3_always_allow:
3768
3795
  - zk_verify
3769
3796
  - zk_range_prove
3770
3797
  - zk_range_verify
3798
+ - context_gate_set_policy
3799
+ - context_gate_apply_template
3800
+ - context_gate_recommend
3801
+ - context_gate_filter
3802
+ - context_gate_list_policies
3771
3803
 
3772
3804
  # \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
3773
3805
  # How Sanctuary reaches you when approval is needed.
@@ -4544,8 +4576,14 @@ function generateDashboardHTML(options) {
4544
4576
  }
4545
4577
 
4546
4578
  // src/principal-policy/dashboard.ts
4579
+ var require4 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)));
4580
+ var { version: PKG_VERSION3 } = require4("../../package.json");
4547
4581
  var SESSION_TTL_MS = 5 * 60 * 1e3;
4548
4582
  var MAX_SESSIONS = 1e3;
4583
+ var RATE_LIMIT_WINDOW_MS = 6e4;
4584
+ var RATE_LIMIT_GENERAL = 120;
4585
+ var RATE_LIMIT_DECISIONS = 20;
4586
+ var MAX_RATE_LIMIT_ENTRIES = 1e4;
4549
4587
  var DashboardApprovalChannel = class {
4550
4588
  config;
4551
4589
  pending = /* @__PURE__ */ new Map();
@@ -4560,13 +4598,15 @@ var DashboardApprovalChannel = class {
4560
4598
  /** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
4561
4599
  sessions = /* @__PURE__ */ new Map();
4562
4600
  sessionCleanupTimer = null;
4601
+ /** Rate limiting: per-IP request tracking */
4602
+ rateLimits = /* @__PURE__ */ new Map();
4563
4603
  constructor(config) {
4564
4604
  this.config = config;
4565
4605
  this.authToken = config.auth_token;
4566
4606
  this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
4567
4607
  this.dashboardHTML = generateDashboardHTML({
4568
4608
  timeoutSeconds: config.timeout_seconds,
4569
- serverVersion: "0.3.0",
4609
+ serverVersion: PKG_VERSION3,
4570
4610
  authToken: this.authToken
4571
4611
  });
4572
4612
  this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
@@ -4645,6 +4685,7 @@ var DashboardApprovalChannel = class {
4645
4685
  clearInterval(this.sessionCleanupTimer);
4646
4686
  this.sessionCleanupTimer = null;
4647
4687
  }
4688
+ this.rateLimits.clear();
4648
4689
  if (this.httpServer) {
4649
4690
  return new Promise((resolve) => {
4650
4691
  this.httpServer.close(() => resolve());
@@ -4770,6 +4811,61 @@ var DashboardApprovalChannel = class {
4770
4811
  }
4771
4812
  }
4772
4813
  }
4814
+ // ── Rate Limiting ─────────────────────────────────────────────────
4815
+ /**
4816
+ * Get the remote address from a request, normalizing IPv6-mapped IPv4.
4817
+ */
4818
+ getRemoteAddr(req) {
4819
+ const addr = req.socket.remoteAddress ?? "unknown";
4820
+ return addr.startsWith("::ffff:") ? addr.slice(7) : addr;
4821
+ }
4822
+ /**
4823
+ * Check rate limit for a request. Returns true if allowed, false if rate-limited.
4824
+ * When rate-limited, sends a 429 response.
4825
+ */
4826
+ checkRateLimit(req, res, type) {
4827
+ const addr = this.getRemoteAddr(req);
4828
+ const now = Date.now();
4829
+ const windowStart = now - RATE_LIMIT_WINDOW_MS;
4830
+ let entry = this.rateLimits.get(addr);
4831
+ if (!entry) {
4832
+ if (this.rateLimits.size >= MAX_RATE_LIMIT_ENTRIES) {
4833
+ this.pruneRateLimits(now);
4834
+ }
4835
+ entry = { general: [], decisions: [] };
4836
+ this.rateLimits.set(addr, entry);
4837
+ }
4838
+ entry.general = entry.general.filter((t) => t > windowStart);
4839
+ entry.decisions = entry.decisions.filter((t) => t > windowStart);
4840
+ const limit = type === "decisions" ? RATE_LIMIT_DECISIONS : RATE_LIMIT_GENERAL;
4841
+ const timestamps = entry[type];
4842
+ if (timestamps.length >= limit) {
4843
+ const retryAfter = Math.ceil((timestamps[0] + RATE_LIMIT_WINDOW_MS - now) / 1e3);
4844
+ res.writeHead(429, {
4845
+ "Content-Type": "application/json",
4846
+ "Retry-After": String(Math.max(1, retryAfter))
4847
+ });
4848
+ res.end(JSON.stringify({
4849
+ error: "Rate limit exceeded",
4850
+ retry_after_seconds: Math.max(1, retryAfter)
4851
+ }));
4852
+ return false;
4853
+ }
4854
+ timestamps.push(now);
4855
+ return true;
4856
+ }
4857
+ /**
4858
+ * Remove stale entries from the rate limit map.
4859
+ */
4860
+ pruneRateLimits(now) {
4861
+ const windowStart = now - RATE_LIMIT_WINDOW_MS;
4862
+ for (const [addr, entry] of this.rateLimits) {
4863
+ const hasRecent = entry.general.some((t) => t > windowStart) || entry.decisions.some((t) => t > windowStart);
4864
+ if (!hasRecent) {
4865
+ this.rateLimits.delete(addr);
4866
+ }
4867
+ }
4868
+ }
4773
4869
  // ── HTTP Request Handler ────────────────────────────────────────────
4774
4870
  handleRequest(req, res) {
4775
4871
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
@@ -4788,6 +4884,7 @@ var DashboardApprovalChannel = class {
4788
4884
  return;
4789
4885
  }
4790
4886
  if (!this.checkAuth(req, url, res)) return;
4887
+ if (!this.checkRateLimit(req, res, "general")) return;
4791
4888
  try {
4792
4889
  if (method === "POST" && url.pathname === "/auth/session") {
4793
4890
  this.handleSessionExchange(req, res);
@@ -4804,9 +4901,11 @@ var DashboardApprovalChannel = class {
4804
4901
  } else if (method === "GET" && url.pathname === "/api/audit-log") {
4805
4902
  this.handleAuditLog(url, res);
4806
4903
  } else if (method === "POST" && url.pathname.startsWith("/api/approve/")) {
4904
+ if (!this.checkRateLimit(req, res, "decisions")) return;
4807
4905
  const id = url.pathname.slice("/api/approve/".length);
4808
4906
  this.handleDecision(id, "approve", res);
4809
4907
  } else if (method === "POST" && url.pathname.startsWith("/api/deny/")) {
4908
+ if (!this.checkRateLimit(req, res, "decisions")) return;
4810
4909
  const id = url.pathname.slice("/api/deny/".length);
4811
4910
  this.handleDecision(id, "deny", res);
4812
4911
  } else {
@@ -5549,14 +5648,14 @@ function generateSHR(identityId, opts) {
5549
5648
  code: "PROCESS_ISOLATION_ONLY",
5550
5649
  severity: "warning",
5551
5650
  description: "Process-level isolation only (no TEE)",
5552
- mitigation: "TEE support planned for v0.3.0"
5651
+ mitigation: "TEE support planned for a future release"
5553
5652
  });
5554
5653
  degradations.push({
5555
5654
  layer: "l2",
5556
5655
  code: "SELF_REPORTED_ATTESTATION",
5557
5656
  severity: "warning",
5558
5657
  description: "Attestation is self-reported (no hardware root of trust)",
5559
- mitigation: "TEE attestation planned for v0.3.0"
5658
+ mitigation: "TEE attestation planned for a future release"
5560
5659
  });
5561
5660
  }
5562
5661
  if (config.disclosure.proof_system === "commitment-only") {
@@ -5700,6 +5799,245 @@ function assessSovereigntyLevel(body) {
5700
5799
  return "minimal";
5701
5800
  }
5702
5801
 
5802
+ // src/shr/gateway-adapter.ts
5803
+ var LAYER_WEIGHTS = {
5804
+ l1: 100,
5805
+ l2: 100,
5806
+ l3: 100,
5807
+ l4: 100
5808
+ };
5809
+ var DEGRADATION_IMPACT = {
5810
+ critical: 40,
5811
+ warning: 25,
5812
+ info: 10
5813
+ };
5814
+ function transformSHRForGateway(shr) {
5815
+ const { body, signed_by, signature } = shr;
5816
+ const layerScores = calculateLayerScores(body);
5817
+ const overallScore = calculateOverallScore(layerScores);
5818
+ const trustLevel = determineTrustLevel(overallScore);
5819
+ const signals = extractAuthorizationSignals(body);
5820
+ const degradations = transformDegradations(body.degradations);
5821
+ const constraints = generateAuthorizationConstraints(body);
5822
+ return {
5823
+ shr_version: body.shr_version,
5824
+ agent_identity: signed_by,
5825
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
5826
+ context_expires_at: body.expires_at,
5827
+ overall_score: overallScore,
5828
+ recommended_trust_level: trustLevel,
5829
+ layer_scores: {
5830
+ l1_cognitive: layerScores.l1,
5831
+ l2_operational: layerScores.l2,
5832
+ l3_disclosure: layerScores.l3,
5833
+ l4_reputation: layerScores.l4
5834
+ },
5835
+ layer_status: {
5836
+ l1_cognitive: body.layers.l1.status,
5837
+ l2_operational: body.layers.l2.status,
5838
+ l3_disclosure: body.layers.l3.status,
5839
+ l4_reputation: body.layers.l4.status
5840
+ },
5841
+ authorization_signals: signals,
5842
+ degradations,
5843
+ recommended_constraints: constraints,
5844
+ shr_signature: signature,
5845
+ shr_signed_by: signed_by
5846
+ };
5847
+ }
5848
+ function calculateLayerScores(body) {
5849
+ const layers = body.layers;
5850
+ const degradations = body.degradations;
5851
+ let l1Score = LAYER_WEIGHTS.l1;
5852
+ let l2Score = LAYER_WEIGHTS.l2;
5853
+ let l3Score = LAYER_WEIGHTS.l3;
5854
+ let l4Score = LAYER_WEIGHTS.l4;
5855
+ for (const deg of degradations) {
5856
+ const impact = DEGRADATION_IMPACT[deg.severity] || 10;
5857
+ if (deg.layer === "l1") {
5858
+ l1Score = Math.max(0, l1Score - impact);
5859
+ } else if (deg.layer === "l2") {
5860
+ l2Score = Math.max(0, l2Score - impact);
5861
+ } else if (deg.layer === "l3") {
5862
+ l3Score = Math.max(0, l3Score - impact);
5863
+ } else if (deg.layer === "l4") {
5864
+ l4Score = Math.max(0, l4Score - impact);
5865
+ }
5866
+ }
5867
+ if (layers.l1.status === "active" && l1Score > 50) l1Score = Math.min(100, l1Score + 5);
5868
+ if (layers.l2.status === "active" && l2Score > 50) l2Score = Math.min(100, l2Score + 5);
5869
+ if (layers.l3.status === "active" && l3Score > 50) l3Score = Math.min(100, l3Score + 5);
5870
+ if (layers.l4.status === "active" && l4Score > 50) l4Score = Math.min(100, l4Score + 5);
5871
+ if (layers.l1.status === "inactive") l1Score = 0;
5872
+ if (layers.l2.status === "inactive") l2Score = 0;
5873
+ if (layers.l3.status === "inactive") l3Score = 0;
5874
+ if (layers.l4.status === "inactive") l4Score = 0;
5875
+ return {
5876
+ l1: Math.round(l1Score),
5877
+ l2: Math.round(l2Score),
5878
+ l3: Math.round(l3Score),
5879
+ l4: Math.round(l4Score)
5880
+ };
5881
+ }
5882
+ function calculateOverallScore(layerScores) {
5883
+ const average = (layerScores.l1 + layerScores.l2 + layerScores.l3 + layerScores.l4) / 4;
5884
+ return Math.round(average);
5885
+ }
5886
+ function determineTrustLevel(score) {
5887
+ if (score >= 80) return "full";
5888
+ if (score >= 60) return "elevated";
5889
+ if (score >= 40) return "standard";
5890
+ return "restricted";
5891
+ }
5892
+ function extractAuthorizationSignals(body) {
5893
+ const l1 = body.layers.l1;
5894
+ const l3 = body.layers.l3;
5895
+ const l4 = body.layers.l4;
5896
+ return {
5897
+ approval_gate_active: body.capabilities.handshake,
5898
+ // Handshake implies human loop capability
5899
+ context_gating_active: body.capabilities.encrypted_channel,
5900
+ // Proxy for gating capability
5901
+ encryption_at_rest: l1.encryption !== "none" && l1.encryption !== "unencrypted",
5902
+ behavioral_baseline_active: false,
5903
+ // Would need explicit field in SHR v1.1
5904
+ identity_verified: l1.identity_type === "ed25519" || l1.identity_type !== "none",
5905
+ zero_knowledge_capable: l3.status === "active" && l3.proof_system !== "commitment-only",
5906
+ selective_disclosure_active: l3.selective_disclosure,
5907
+ reputation_portable: l4.reputation_portable,
5908
+ handshake_capable: body.capabilities.handshake
5909
+ };
5910
+ }
5911
+ function transformDegradations(degradations) {
5912
+ return degradations.map((deg) => {
5913
+ let authzImpact = "";
5914
+ if (deg.code === "NO_TEE") {
5915
+ authzImpact = "Restricted to read-only operations until TEE available";
5916
+ } else if (deg.code === "PROCESS_ISOLATION_ONLY") {
5917
+ authzImpact = "Requires additional identity verification";
5918
+ } else if (deg.code === "COMMITMENT_ONLY") {
5919
+ authzImpact = "Limited data sharing scope \u2014 no zero-knowledge proofs";
5920
+ } else if (deg.code === "NO_ZK_PROOFS") {
5921
+ authzImpact = "Cannot perform confidential disclosures";
5922
+ } else if (deg.code === "SELF_REPORTED_ATTESTATION") {
5923
+ authzImpact = "Attestation trust degraded \u2014 human verification recommended";
5924
+ } else if (deg.code === "NO_SELECTIVE_DISCLOSURE") {
5925
+ authzImpact = "Must share entire data context, cannot redact";
5926
+ } else if (deg.code === "BASIC_SYBIL_ONLY") {
5927
+ authzImpact = "Restrict to interactions with known agents only";
5928
+ } else {
5929
+ authzImpact = "Unknown authorization impact";
5930
+ }
5931
+ return {
5932
+ layer: deg.layer,
5933
+ code: deg.code,
5934
+ severity: deg.severity,
5935
+ description: deg.description,
5936
+ authorization_impact: authzImpact
5937
+ };
5938
+ });
5939
+ }
5940
+ function generateAuthorizationConstraints(body, _degradations) {
5941
+ const constraints = [];
5942
+ const layers = body.layers;
5943
+ if (layers.l1.status === "degraded" || layers.l1.key_custody !== "self") {
5944
+ constraints.push({
5945
+ type: "identity_verification_required",
5946
+ description: "Additional identity verification required for sensitive operations",
5947
+ rationale: "L1 is degraded or key custody is not self-managed",
5948
+ priority: "high"
5949
+ });
5950
+ }
5951
+ if (!layers.l1.state_portable) {
5952
+ constraints.push({
5953
+ type: "location_bound",
5954
+ description: "Agent state is not portable \u2014 restrict to home environment",
5955
+ rationale: "State cannot be safely migrated across boundaries",
5956
+ priority: "medium"
5957
+ });
5958
+ }
5959
+ if (layers.l2.status === "degraded" || layers.l2.isolation_type === "local-process") {
5960
+ constraints.push({
5961
+ type: "read_only",
5962
+ description: "Restrict to read-only operations until operational isolation improves",
5963
+ rationale: "L2 isolation is process-level only (no TEE)",
5964
+ priority: "high"
5965
+ });
5966
+ }
5967
+ if (!layers.l2.attestation_available) {
5968
+ constraints.push({
5969
+ type: "requires_approval",
5970
+ description: "Human approval required for writes and sensitive reads",
5971
+ rationale: "No attestation available \u2014 self-reported integrity only",
5972
+ priority: "high"
5973
+ });
5974
+ }
5975
+ if (layers.l3.status === "degraded" || !layers.l3.selective_disclosure) {
5976
+ constraints.push({
5977
+ type: "restricted_scope",
5978
+ description: "Limit data sharing to minimal required scope \u2014 no selective disclosure",
5979
+ rationale: "Agent cannot redact data or prove predicates without revealing all context",
5980
+ priority: "high"
5981
+ });
5982
+ }
5983
+ if (layers.l3.proof_system === "commitment-only") {
5984
+ constraints.push({
5985
+ type: "restricted_scope",
5986
+ description: "No zero-knowledge proofs available \u2014 entire state context may be visible",
5987
+ rationale: "Proof system is commitment-only (no ZK)",
5988
+ priority: "medium"
5989
+ });
5990
+ }
5991
+ if (layers.l4.status === "degraded") {
5992
+ constraints.push({
5993
+ type: "known_agents_only",
5994
+ description: "Restrict interactions to known, pre-approved agents",
5995
+ rationale: "Reputation layer is degraded",
5996
+ priority: "medium"
5997
+ });
5998
+ }
5999
+ if (!layers.l4.reputation_portable) {
6000
+ constraints.push({
6001
+ type: "location_bound",
6002
+ description: "Reputation is not portable \u2014 restrict to home environment",
6003
+ rationale: "Cannot present reputation to external parties",
6004
+ priority: "low"
6005
+ });
6006
+ }
6007
+ const layerScores = calculateLayerScores(body);
6008
+ const overallScore = calculateOverallScore(layerScores);
6009
+ if (overallScore < 40) {
6010
+ constraints.push({
6011
+ type: "restricted_scope",
6012
+ description: "Overall sovereignty score below threshold \u2014 restrict to non-sensitive operations",
6013
+ rationale: `Overall sovereignty score is ${overallScore}/100`,
6014
+ priority: "high"
6015
+ });
6016
+ }
6017
+ return constraints;
6018
+ }
6019
+ function transformSHRGeneric(shr) {
6020
+ const context = transformSHRForGateway(shr);
6021
+ return {
6022
+ agent_id: context.agent_identity,
6023
+ sovereignty_score: context.overall_score,
6024
+ trust_level: context.recommended_trust_level,
6025
+ layer_scores: {
6026
+ l1: context.layer_scores.l1_cognitive,
6027
+ l2: context.layer_scores.l2_operational,
6028
+ l3: context.layer_scores.l3_disclosure,
6029
+ l4: context.layer_scores.l4_reputation
6030
+ },
6031
+ capabilities: context.authorization_signals,
6032
+ constraints: context.recommended_constraints.map((c) => ({
6033
+ type: c.type,
6034
+ description: c.description
6035
+ })),
6036
+ expires_at: context.context_expires_at,
6037
+ signature: context.shr_signature
6038
+ };
6039
+ }
6040
+
5703
6041
  // src/shr/tools.ts
5704
6042
  function createSHRTools(config, identityManager, masterKey, auditLog) {
5705
6043
  const generatorOpts = {
@@ -5762,6 +6100,53 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
5762
6100
  );
5763
6101
  return toolResult(result);
5764
6102
  }
6103
+ },
6104
+ {
6105
+ name: "sanctuary/shr_gateway_export",
6106
+ 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.",
6107
+ inputSchema: {
6108
+ type: "object",
6109
+ properties: {
6110
+ format: {
6111
+ type: "string",
6112
+ enum: ["ping", "generic"],
6113
+ description: "Output format: 'ping' (Ping Identity Gateway format) or 'generic' (format-agnostic). Default: 'ping'."
6114
+ },
6115
+ identity_id: {
6116
+ type: "string",
6117
+ description: "Identity to sign the SHR with. Defaults to primary identity."
6118
+ },
6119
+ validity_minutes: {
6120
+ type: "number",
6121
+ description: "How long the SHR is valid (minutes). Default: 60."
6122
+ }
6123
+ }
6124
+ },
6125
+ handler: async (args) => {
6126
+ const format = args.format || "ping";
6127
+ const validityMs = args.validity_minutes ? args.validity_minutes * 60 * 1e3 : void 0;
6128
+ const shrResult = generateSHR(args.identity_id, {
6129
+ ...generatorOpts,
6130
+ validityMs
6131
+ });
6132
+ if (typeof shrResult === "string") {
6133
+ return toolResult({ error: shrResult });
6134
+ }
6135
+ let context;
6136
+ if (format === "generic") {
6137
+ context = transformSHRGeneric(shrResult);
6138
+ } else {
6139
+ context = transformSHRForGateway(shrResult);
6140
+ }
6141
+ auditLog.append(
6142
+ "l2",
6143
+ "shr_gateway_export",
6144
+ shrResult.body.instance_id,
6145
+ void 0,
6146
+ "success"
6147
+ );
6148
+ return toolResult(context);
6149
+ }
5765
6150
  }
5766
6151
  ];
5767
6152
  return { tools };
@@ -7040,9 +7425,11 @@ var L1_INTEGRITY_VERIFICATION = 8;
7040
7425
  var L1_STATE_PORTABLE = 7;
7041
7426
  var L2_THREE_TIER_GATE = 10;
7042
7427
  var L2_BINARY_GATE = 3;
7043
- var L2_ANOMALY_DETECTION = 7;
7044
- var L2_ENCRYPTED_AUDIT = 5;
7045
- var L2_TOOL_SANDBOXING = 3;
7428
+ var L2_ANOMALY_DETECTION = 5;
7429
+ var L2_ENCRYPTED_AUDIT = 4;
7430
+ var L2_TOOL_SANDBOXING = 2;
7431
+ var L2_CONTEXT_GATING = 4;
7432
+ var L2_PROCESS_HARDENING = 5;
7046
7433
  var L3_COMMITMENT_SCHEME = 8;
7047
7434
  var L3_ZK_PROOFS = 7;
7048
7435
  var L3_DISCLOSURE_POLICIES = 5;
@@ -7056,6 +7443,35 @@ var SEVERITY_ORDER = {
7056
7443
  medium: 2,
7057
7444
  low: 3
7058
7445
  };
7446
+ var INCIDENT_META_SEV1 = {
7447
+ id: "META-SEV1-2026",
7448
+ name: "Meta Sev 1: Unauthorized autonomous data exposure",
7449
+ date: "2026-03-18",
7450
+ description: "AI agent autonomously posted proprietary code, business strategies, and user datasets to an internal forum without human approval. Two-hour exposure window."
7451
+ };
7452
+ var INCIDENT_OPENCLAW_SANDBOX = {
7453
+ id: "OPENCLAW-CVE-2026",
7454
+ name: "OpenClaw sandbox escape via privilege inheritance",
7455
+ date: "2026-03-18",
7456
+ 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.",
7457
+ cves: [
7458
+ "CVE-2026-32048",
7459
+ "CVE-2026-32915",
7460
+ "CVE-2026-32918"
7461
+ ]
7462
+ };
7463
+ var INCIDENT_CONTEXT_LEAKAGE = {
7464
+ id: "CONTEXT-LEAK-CLASS",
7465
+ name: "Context leakage: Full state exposure to inference providers",
7466
+ date: "2026-03",
7467
+ 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."
7468
+ };
7469
+ var INCIDENT_CLAUDE_CODE_LEAK = {
7470
+ id: "CLAUDE-CODE-LEAK-2026",
7471
+ name: "Claude Code source leak: 512K lines exposed via npm source map",
7472
+ date: "2026-03-31",
7473
+ 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."
7474
+ };
7059
7475
  function analyzeSovereignty(env, config) {
7060
7476
  const l1 = assessL1(env, config);
7061
7477
  const l2 = assessL2(env);
@@ -7128,14 +7544,18 @@ function assessL2(env, _config) {
7128
7544
  let auditTrailEncrypted = false;
7129
7545
  let auditTrailExists = false;
7130
7546
  let toolSandboxing = "none";
7547
+ let contextGating = false;
7548
+ let processIsolationHardening = "none";
7131
7549
  if (sanctuaryActive) {
7132
7550
  approvalGate = "three-tier";
7133
7551
  behavioralAnomalyDetection = true;
7134
7552
  auditTrailEncrypted = true;
7135
7553
  auditTrailExists = true;
7554
+ contextGating = true;
7136
7555
  findings.push("Three-tier Principal Policy gate active");
7137
7556
  findings.push("Behavioral anomaly detection (BaselineTracker) enabled");
7138
7557
  findings.push("Encrypted audit trail active");
7558
+ findings.push("Context gating available (sanctuary/context_gate_set_policy)");
7139
7559
  }
7140
7560
  if (env.openclaw_detected && env.openclaw_config) {
7141
7561
  if (env.openclaw_config.require_approval_enabled) {
@@ -7153,6 +7573,7 @@ function assessL2(env, _config) {
7153
7573
  );
7154
7574
  }
7155
7575
  }
7576
+ processIsolationHardening = "none";
7156
7577
  const status = approvalGate === "three-tier" && auditTrailEncrypted ? "active" : approvalGate !== "none" || auditTrailExists ? "partial" : "inactive";
7157
7578
  return {
7158
7579
  status,
@@ -7161,6 +7582,8 @@ function assessL2(env, _config) {
7161
7582
  audit_trail_encrypted: auditTrailEncrypted,
7162
7583
  audit_trail_exists: auditTrailExists,
7163
7584
  tool_sandboxing: sanctuaryActive ? "policy-enforced" : toolSandboxing,
7585
+ context_gating: contextGating,
7586
+ process_isolation_hardening: processIsolationHardening,
7164
7587
  findings
7165
7588
  };
7166
7589
  }
@@ -7175,8 +7598,10 @@ function assessL3(env, _config) {
7175
7598
  zkProofs = true;
7176
7599
  selectiveDisclosurePolicy = true;
7177
7600
  findings.push("SHA-256 + Pedersen commitment schemes active");
7178
- findings.push("Schnorr ZK proofs and range proofs available");
7601
+ findings.push("Schnorr zero-knowledge proofs (Fiat-Shamir) enabled \u2014 genuine ZK proofs");
7602
+ findings.push("Range proofs (bit-decomposition + OR-proofs) enabled \u2014 genuine ZK proofs");
7179
7603
  findings.push("Selective disclosure policies configurable");
7604
+ findings.push("Non-interactive proofs with replay-resistant domain separation");
7180
7605
  }
7181
7606
  const status = commitmentScheme === "pedersen+sha256" && zkProofs ? "active" : commitmentScheme !== "none" ? "partial" : "inactive";
7182
7607
  return {
@@ -7228,6 +7653,9 @@ function scoreL2(l2) {
7228
7653
  if (l2.audit_trail_encrypted) score += L2_ENCRYPTED_AUDIT;
7229
7654
  if (l2.tool_sandboxing === "policy-enforced") score += L2_TOOL_SANDBOXING;
7230
7655
  else if (l2.tool_sandboxing === "basic") score += 1;
7656
+ if (l2.context_gating) score += L2_CONTEXT_GATING;
7657
+ if (l2.process_isolation_hardening === "hardened") score += L2_PROCESS_HARDENING;
7658
+ else if (l2.process_isolation_hardening === "basic") score += 2;
7231
7659
  return score;
7232
7660
  }
7233
7661
  function scoreL3(l3) {
@@ -7257,7 +7685,8 @@ function generateGaps(env, l1, l2, l3, l4) {
7257
7685
  title: "Agent memory stored in plaintext",
7258
7686
  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.",
7259
7687
  openclaw_relevance: "Stock OpenClaw stores all agent memory in plaintext files. There is no built-in encryption for agent state.",
7260
- 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."
7688
+ 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.",
7689
+ incident_class: INCIDENT_META_SEV1
7261
7690
  });
7262
7691
  }
7263
7692
  if (oc && oc.env_file_exposed) {
@@ -7290,7 +7719,8 @@ function generateGaps(env, l1, l2, l3, l4) {
7290
7719
  title: "Binary approval gate (no anomaly detection)",
7291
7720
  description: "Your approval gate provides binary approve/deny gating without behavioral anomaly detection. Routine operations require the same manual approval as sensitive ones.",
7292
7721
  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,
7293
- 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."
7722
+ 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.",
7723
+ incident_class: INCIDENT_META_SEV1
7294
7724
  });
7295
7725
  } else if (l2.approval_gate === "none") {
7296
7726
  gaps.push({
@@ -7300,7 +7730,8 @@ function generateGaps(env, l1, l2, l3, l4) {
7300
7730
  title: "No approval gate",
7301
7731
  description: "No approval gate is configured. All tool calls execute without oversight.",
7302
7732
  openclaw_relevance: null,
7303
- sanctuary_solution: "Sanctuary's Principal Policy evaluates every tool call before execution. Enable it to get three-tier approval gating with behavioral anomaly detection."
7733
+ sanctuary_solution: "Sanctuary's Principal Policy evaluates every tool call before execution. Enable it to get three-tier approval gating with behavioral anomaly detection.",
7734
+ incident_class: INCIDENT_META_SEV1
7304
7735
  });
7305
7736
  }
7306
7737
  if (l2.tool_sandboxing === "basic") {
@@ -7311,18 +7742,32 @@ function generateGaps(env, l1, l2, l3, l4) {
7311
7742
  title: "Basic tool sandboxing (no cryptographic attestation)",
7312
7743
  description: "Your tool sandbox enforces allow/deny lists but provides no cryptographic attestation of execution context.",
7313
7744
  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,
7314
- sanctuary_solution: "Sanctuary provides cryptographic execution attestation via sanctuary/exec_attest and policy-enforced sandboxing with encrypted audit trails."
7745
+ sanctuary_solution: "Sanctuary provides cryptographic execution attestation via sanctuary/exec_attest and policy-enforced sandboxing with encrypted audit trails.",
7746
+ incident_class: INCIDENT_OPENCLAW_SANDBOX
7315
7747
  });
7316
7748
  }
7317
- if (!l2.audit_trail_exists) {
7749
+ if (!l2.context_gating) {
7318
7750
  gaps.push({
7319
7751
  id: "GAP-L2-003",
7320
7752
  layer: "L2",
7321
7753
  severity: "high",
7754
+ title: "No context gating for outbound inference calls",
7755
+ 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.",
7756
+ 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,
7757
+ 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.",
7758
+ incident_class: INCIDENT_CONTEXT_LEAKAGE
7759
+ });
7760
+ }
7761
+ if (!l2.audit_trail_exists) {
7762
+ gaps.push({
7763
+ id: "GAP-L2-004",
7764
+ layer: "L2",
7765
+ severity: "high",
7322
7766
  title: "No audit trail",
7323
7767
  description: "No audit trail exists for tool call history. There is no record of what operations were executed, when, or by whom.",
7324
7768
  openclaw_relevance: null,
7325
- sanctuary_solution: "Sanctuary maintains an encrypted audit log of all operations, queryable via sanctuary/monitor_audit_log."
7769
+ sanctuary_solution: "Sanctuary maintains an encrypted audit log of all operations, queryable via sanctuary/monitor_audit_log.",
7770
+ incident_class: INCIDENT_CLAUDE_CODE_LEAK
7326
7771
  });
7327
7772
  }
7328
7773
  if (l3.commitment_scheme === "none") {
@@ -7331,9 +7776,10 @@ function generateGaps(env, l1, l2, l3, l4) {
7331
7776
  layer: "L3",
7332
7777
  severity: "high",
7333
7778
  title: "No selective disclosure capability",
7334
- description: "Your agent has no way to prove facts about its state without revealing the state itself. Every disclosure is all-or-nothing.",
7779
+ 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.",
7335
7780
  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,
7336
- sanctuary_solution: "Sanctuary's L3 provides SHA-256 + Pedersen commitments and Schnorr zero-knowledge proofs. Your agent can prove it has a valid credential, sufficient reputation, or a completed transaction without exposing the underlying data. Use sanctuary/zk_commit and sanctuary/zk_prove."
7781
+ 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.",
7782
+ incident_class: INCIDENT_META_SEV1
7337
7783
  });
7338
7784
  }
7339
7785
  if (!l4.reputation_portable) {
@@ -7385,9 +7831,18 @@ function generateRecommendations(env, l1, l2, l3, l4) {
7385
7831
  impact: "high"
7386
7832
  });
7387
7833
  }
7388
- if (!l4.reputation_signed) {
7834
+ if (!l2.context_gating) {
7389
7835
  recs.push({
7390
7836
  priority: 5,
7837
+ action: "Configure context gating to control what flows to LLM providers",
7838
+ tool: "sanctuary/context_gate_set_policy",
7839
+ effort: "minutes",
7840
+ impact: "high"
7841
+ });
7842
+ }
7843
+ if (!l4.reputation_signed) {
7844
+ recs.push({
7845
+ priority: 6,
7391
7846
  action: "Start recording reputation attestations from completed interactions",
7392
7847
  tool: "sanctuary/reputation_record",
7393
7848
  effort: "minutes",
@@ -7396,7 +7851,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
7396
7851
  }
7397
7852
  if (!l3.selective_disclosure_policy) {
7398
7853
  recs.push({
7399
- priority: 6,
7854
+ priority: 7,
7400
7855
  action: "Configure selective disclosure policies for data sharing",
7401
7856
  tool: "sanctuary/disclosure_set_policy",
7402
7857
  effort: "hours",
@@ -7445,6 +7900,10 @@ function formatAuditReport(result) {
7445
7900
  `;
7446
7901
  report += ` \u2502 L2 Operational Isolation \u2502 ${padStatus(layers.l2_operational.status)} \u2502 ${padScore(l2Score, 25)} \u2502
7447
7902
  `;
7903
+ if (layers.l2_operational.context_gating) {
7904
+ report += ` \u2502 \u2514 Context Gating \u2502 ACTIVE \u2502 \u2502
7905
+ `;
7906
+ }
7448
7907
  report += ` \u2502 L3 Selective Disclosure \u2502 ${padStatus(layers.l3_selective_disclosure.status)} \u2502 ${padScore(l3Score, 20)} \u2502
7449
7908
  `;
7450
7909
  report += ` \u2502 L4 Verifiable Reputation \u2502 ${padStatus(layers.l4_reputation.status)} \u2502 ${padScore(l4Score, 20)} \u2502
@@ -7462,6 +7921,12 @@ function formatAuditReport(result) {
7462
7921
  const descLines = wordWrap(gap.description, 66);
7463
7922
  for (const line of descLines) {
7464
7923
  report += ` ${line}
7924
+ `;
7925
+ }
7926
+ if (gap.incident_class) {
7927
+ const ic = gap.incident_class;
7928
+ const cveStr = ic.cves?.length ? ` (${ic.cves.join(", ")})` : "";
7929
+ report += ` \u2192 Incident precedent: ${ic.name}${cveStr} [${ic.date}]
7465
7930
  `;
7466
7931
  }
7467
7932
  report += ` \u2192 Fix: ${gap.sanctuary_solution.split(".")[0]}.
@@ -7556,33 +8021,1509 @@ function createAuditTools(config) {
7556
8021
  return { tools };
7557
8022
  }
7558
8023
 
7559
- // src/index.ts
8024
+ // src/l2-operational/context-gate.ts
7560
8025
  init_encoding();
7561
- async function createSanctuaryServer(options) {
7562
- const config = await loadConfig(options?.configPath);
7563
- await promises.mkdir(config.storage_path, { recursive: true, mode: 448 });
7564
- const storage = options?.storage ?? new FilesystemStorage(
7565
- `${config.storage_path}/state`
8026
+ init_hashing();
8027
+ var MAX_CONTEXT_FIELDS = 1e3;
8028
+ var MAX_POLICY_RULES = 50;
8029
+ var MAX_PATTERNS_PER_ARRAY = 500;
8030
+ function evaluateField(policy, provider, field) {
8031
+ const exactRule = policy.rules.find((r) => r.provider === provider);
8032
+ const wildcardRule = policy.rules.find((r) => r.provider === "*");
8033
+ const matchedRule = exactRule ?? wildcardRule;
8034
+ if (!matchedRule) {
8035
+ return {
8036
+ field,
8037
+ action: policy.default_action === "deny" ? "deny" : "redact",
8038
+ reason: `No rule matches provider "${provider}"; applying default (${policy.default_action})`
8039
+ };
8040
+ }
8041
+ if (matchesPattern(field, matchedRule.redact)) {
8042
+ return {
8043
+ field,
8044
+ action: "redact",
8045
+ reason: `Field "${field}" is explicitly redacted for ${matchedRule.provider} provider`
8046
+ };
8047
+ }
8048
+ if (matchesPattern(field, matchedRule.hash)) {
8049
+ return {
8050
+ field,
8051
+ action: "hash",
8052
+ reason: `Field "${field}" is hashed for ${matchedRule.provider} provider`
8053
+ };
8054
+ }
8055
+ if (matchesPattern(field, matchedRule.summarize)) {
8056
+ return {
8057
+ field,
8058
+ action: "summarize",
8059
+ reason: `Field "${field}" should be summarized for ${matchedRule.provider} provider`
8060
+ };
8061
+ }
8062
+ if (matchesPattern(field, matchedRule.allow)) {
8063
+ return {
8064
+ field,
8065
+ action: "allow",
8066
+ reason: `Field "${field}" is allowed for ${matchedRule.provider} provider`
8067
+ };
8068
+ }
8069
+ return {
8070
+ field,
8071
+ action: policy.default_action === "deny" ? "deny" : "redact",
8072
+ reason: `Field "${field}" not addressed in ${matchedRule.provider} rule; applying default (${policy.default_action})`
8073
+ };
8074
+ }
8075
+ function filterContext(policy, provider, context) {
8076
+ const fields = Object.keys(context);
8077
+ if (fields.length > MAX_CONTEXT_FIELDS) {
8078
+ throw new Error(
8079
+ `Context object has ${fields.length} fields, exceeding limit of ${MAX_CONTEXT_FIELDS}`
8080
+ );
8081
+ }
8082
+ const decisions = [];
8083
+ let allowed = 0;
8084
+ let redacted = 0;
8085
+ let hashed = 0;
8086
+ let summarized = 0;
8087
+ let denied = 0;
8088
+ for (const field of fields) {
8089
+ const result = evaluateField(policy, provider, field);
8090
+ if (result.action === "hash") {
8091
+ const value = typeof context[field] === "string" ? context[field] : JSON.stringify(context[field]);
8092
+ result.hash_value = hashToString(stringToBytes(value));
8093
+ }
8094
+ decisions.push(result);
8095
+ switch (result.action) {
8096
+ case "allow":
8097
+ allowed++;
8098
+ break;
8099
+ case "redact":
8100
+ redacted++;
8101
+ break;
8102
+ case "hash":
8103
+ hashed++;
8104
+ break;
8105
+ case "summarize":
8106
+ summarized++;
8107
+ break;
8108
+ case "deny":
8109
+ denied++;
8110
+ break;
8111
+ }
8112
+ }
8113
+ const originalHash = hashToString(
8114
+ stringToBytes(JSON.stringify(context))
7566
8115
  );
7567
- let masterKey;
7568
- let keyProtection;
7569
- let recoveryKey;
7570
- const passphrase = options?.passphrase ?? process.env.SANCTUARY_PASSPHRASE;
7571
- if (passphrase) {
7572
- keyProtection = "passphrase";
7573
- let existingParams;
8116
+ const filteredOutput = {};
8117
+ for (const decision of decisions) {
8118
+ switch (decision.action) {
8119
+ case "allow":
8120
+ filteredOutput[decision.field] = context[decision.field];
8121
+ break;
8122
+ case "redact":
8123
+ filteredOutput[decision.field] = "[REDACTED]";
8124
+ break;
8125
+ case "hash":
8126
+ filteredOutput[decision.field] = `[HASH:${decision.hash_value}]`;
8127
+ break;
8128
+ case "summarize":
8129
+ filteredOutput[decision.field] = "[SUMMARIZE]";
8130
+ break;
8131
+ }
8132
+ }
8133
+ const filteredHash = hashToString(
8134
+ stringToBytes(JSON.stringify(filteredOutput))
8135
+ );
8136
+ return {
8137
+ policy_id: policy.policy_id,
8138
+ provider,
8139
+ fields_allowed: allowed,
8140
+ fields_redacted: redacted,
8141
+ fields_hashed: hashed,
8142
+ fields_summarized: summarized,
8143
+ fields_denied: denied,
8144
+ decisions,
8145
+ original_context_hash: originalHash,
8146
+ filtered_context_hash: filteredHash,
8147
+ filtered_at: (/* @__PURE__ */ new Date()).toISOString()
8148
+ };
8149
+ }
8150
+ function matchesPattern(field, patterns) {
8151
+ const normalizedField = field.toLowerCase();
8152
+ for (const pattern of patterns) {
8153
+ if (pattern === "*") return true;
8154
+ const normalizedPattern = pattern.toLowerCase();
8155
+ if (normalizedPattern === normalizedField) return true;
8156
+ if (normalizedPattern.endsWith("*") && normalizedField.startsWith(normalizedPattern.slice(0, -1))) return true;
8157
+ if (normalizedPattern.startsWith("*") && normalizedField.endsWith(normalizedPattern.slice(1))) return true;
8158
+ }
8159
+ return false;
8160
+ }
8161
+ var ContextGatePolicyStore = class {
8162
+ storage;
8163
+ encryptionKey;
8164
+ policies = /* @__PURE__ */ new Map();
8165
+ constructor(storage, masterKey) {
8166
+ this.storage = storage;
8167
+ this.encryptionKey = derivePurposeKey(masterKey, "l2-context-gate");
8168
+ }
8169
+ /**
8170
+ * Create and store a new context-gating policy.
8171
+ */
8172
+ async create(policyName, rules, defaultAction, identityId) {
8173
+ const policyId = `cg-${Date.now()}-${toBase64url(randomBytes(8))}`;
8174
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8175
+ const policy = {
8176
+ policy_id: policyId,
8177
+ policy_name: policyName,
8178
+ rules,
8179
+ default_action: defaultAction,
8180
+ identity_id: identityId,
8181
+ created_at: now,
8182
+ updated_at: now
8183
+ };
8184
+ await this.persist(policy);
8185
+ this.policies.set(policyId, policy);
8186
+ return policy;
8187
+ }
8188
+ /**
8189
+ * Get a policy by ID.
8190
+ */
8191
+ async get(policyId) {
8192
+ if (this.policies.has(policyId)) {
8193
+ return this.policies.get(policyId);
8194
+ }
8195
+ const raw = await this.storage.read("_context_gate_policies", policyId);
8196
+ if (!raw) return null;
7574
8197
  try {
7575
- const raw = await storage.read("_meta", "key-params");
7576
- if (raw) {
7577
- const { bytesToString: bytesToString2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
7578
- existingParams = JSON.parse(bytesToString2(raw));
8198
+ const encrypted = JSON.parse(bytesToString(raw));
8199
+ const decrypted = decrypt(encrypted, this.encryptionKey);
8200
+ const policy = JSON.parse(bytesToString(decrypted));
8201
+ this.policies.set(policyId, policy);
8202
+ return policy;
8203
+ } catch {
8204
+ return null;
8205
+ }
8206
+ }
8207
+ /**
8208
+ * List all context-gating policies.
8209
+ */
8210
+ async list() {
8211
+ await this.loadAll();
8212
+ return Array.from(this.policies.values());
8213
+ }
8214
+ /**
8215
+ * Load all persisted policies into memory.
8216
+ */
8217
+ async loadAll() {
8218
+ try {
8219
+ const entries = await this.storage.list("_context_gate_policies");
8220
+ for (const meta of entries) {
8221
+ if (this.policies.has(meta.key)) continue;
8222
+ const raw = await this.storage.read("_context_gate_policies", meta.key);
8223
+ if (!raw) continue;
8224
+ try {
8225
+ const encrypted = JSON.parse(bytesToString(raw));
8226
+ const decrypted = decrypt(encrypted, this.encryptionKey);
8227
+ const policy = JSON.parse(bytesToString(decrypted));
8228
+ this.policies.set(policy.policy_id, policy);
8229
+ } catch {
8230
+ }
7579
8231
  }
7580
8232
  } catch {
7581
8233
  }
7582
- const result = await deriveMasterKey(passphrase, existingParams);
7583
- masterKey = result.key;
7584
- if (!existingParams) {
7585
- const { stringToBytes: stringToBytes2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
8234
+ }
8235
+ async persist(policy) {
8236
+ const serialized = stringToBytes(JSON.stringify(policy));
8237
+ const encrypted = encrypt(serialized, this.encryptionKey);
8238
+ await this.storage.write(
8239
+ "_context_gate_policies",
8240
+ policy.policy_id,
8241
+ stringToBytes(JSON.stringify(encrypted))
8242
+ );
8243
+ }
8244
+ };
8245
+
8246
+ // src/l2-operational/context-gate-templates.ts
8247
+ var ALWAYS_REDACT_SECRETS = [
8248
+ "api_key",
8249
+ "secret_*",
8250
+ "*_secret",
8251
+ "*_token",
8252
+ "*_key",
8253
+ "password",
8254
+ "*_password",
8255
+ "credential",
8256
+ "*_credential",
8257
+ "private_key",
8258
+ "recovery_key",
8259
+ "passphrase",
8260
+ "auth_*"
8261
+ ];
8262
+ var PII_PATTERNS = [
8263
+ "*_pii",
8264
+ "name",
8265
+ "full_name",
8266
+ "email",
8267
+ "email_address",
8268
+ "phone",
8269
+ "phone_number",
8270
+ "address",
8271
+ "ssn",
8272
+ "date_of_birth",
8273
+ "ip_address",
8274
+ "credit_card",
8275
+ "card_number",
8276
+ "cvv",
8277
+ "bank_account",
8278
+ "account_number",
8279
+ "routing_number"
8280
+ ];
8281
+ var INTERNAL_STATE_PATTERNS = [
8282
+ "memory",
8283
+ "agent_memory",
8284
+ "internal_reasoning",
8285
+ "internal_state",
8286
+ "reasoning_trace",
8287
+ "chain_of_thought",
8288
+ "private_notes",
8289
+ "soul",
8290
+ "personality",
8291
+ "system_prompt"
8292
+ ];
8293
+ var ID_PATTERNS = [
8294
+ "user_id",
8295
+ "session_id",
8296
+ "agent_id",
8297
+ "identity_id",
8298
+ "conversation_id",
8299
+ "thread_id"
8300
+ ];
8301
+ var HISTORY_PATTERNS = [
8302
+ "conversation_history",
8303
+ "message_history",
8304
+ "chat_history",
8305
+ "context_window",
8306
+ "previous_messages"
8307
+ ];
8308
+ var INFERENCE_MINIMAL = {
8309
+ id: "inference-minimal",
8310
+ name: "Inference Minimal",
8311
+ description: "Maximum privacy. Only the current task and query reach the LLM provider.",
8312
+ use_when: "You want the strictest possible context control for inference calls. The LLM sees only what it needs for the immediate task.",
8313
+ rules: [
8314
+ {
8315
+ provider: "inference",
8316
+ allow: [
8317
+ "task",
8318
+ "task_description",
8319
+ "current_query",
8320
+ "query",
8321
+ "prompt",
8322
+ "question",
8323
+ "instruction"
8324
+ ],
8325
+ redact: [
8326
+ ...ALWAYS_REDACT_SECRETS,
8327
+ ...PII_PATTERNS,
8328
+ ...INTERNAL_STATE_PATTERNS,
8329
+ ...HISTORY_PATTERNS,
8330
+ "tool_results",
8331
+ "previous_results"
8332
+ ],
8333
+ hash: [...ID_PATTERNS],
8334
+ summarize: []
8335
+ }
8336
+ ],
8337
+ default_action: "redact"
8338
+ };
8339
+ var INFERENCE_STANDARD = {
8340
+ id: "inference-standard",
8341
+ name: "Inference Standard",
8342
+ description: "Balanced privacy. Task, query, and tool results pass through. History flagged for summarization. Secrets and PII redacted.",
8343
+ use_when: "You need the LLM to have enough context for multi-step tasks while keeping secrets, PII, and internal reasoning private.",
8344
+ rules: [
8345
+ {
8346
+ provider: "inference",
8347
+ allow: [
8348
+ "task",
8349
+ "task_description",
8350
+ "current_query",
8351
+ "query",
8352
+ "prompt",
8353
+ "question",
8354
+ "instruction",
8355
+ "tool_results",
8356
+ "tool_output",
8357
+ "previous_results",
8358
+ "current_step",
8359
+ "remaining_steps",
8360
+ "objective",
8361
+ "constraints",
8362
+ "format",
8363
+ "output_format"
8364
+ ],
8365
+ redact: [
8366
+ ...ALWAYS_REDACT_SECRETS,
8367
+ ...PII_PATTERNS,
8368
+ ...INTERNAL_STATE_PATTERNS
8369
+ ],
8370
+ hash: [...ID_PATTERNS],
8371
+ summarize: [...HISTORY_PATTERNS]
8372
+ }
8373
+ ],
8374
+ default_action: "redact"
8375
+ };
8376
+ var LOGGING_STRICT = {
8377
+ id: "logging-strict",
8378
+ name: "Logging Strict",
8379
+ description: "Redacts all content for logging and analytics providers. Only operation metadata passes through.",
8380
+ use_when: "You send telemetry to logging or analytics services and want usage metrics without any content exposure.",
8381
+ rules: [
8382
+ {
8383
+ provider: "logging",
8384
+ allow: [
8385
+ "operation",
8386
+ "operation_name",
8387
+ "tool_name",
8388
+ "timestamp",
8389
+ "duration_ms",
8390
+ "status",
8391
+ "error_code",
8392
+ "event_type"
8393
+ ],
8394
+ redact: [
8395
+ ...ALWAYS_REDACT_SECRETS,
8396
+ ...PII_PATTERNS,
8397
+ ...INTERNAL_STATE_PATTERNS,
8398
+ ...HISTORY_PATTERNS
8399
+ ],
8400
+ hash: [...ID_PATTERNS],
8401
+ summarize: []
8402
+ },
8403
+ {
8404
+ provider: "analytics",
8405
+ allow: [
8406
+ "event_type",
8407
+ "timestamp",
8408
+ "duration_ms",
8409
+ "status",
8410
+ "tool_name"
8411
+ ],
8412
+ redact: [
8413
+ ...ALWAYS_REDACT_SECRETS,
8414
+ ...PII_PATTERNS,
8415
+ ...INTERNAL_STATE_PATTERNS,
8416
+ ...HISTORY_PATTERNS
8417
+ ],
8418
+ hash: [...ID_PATTERNS],
8419
+ summarize: []
8420
+ }
8421
+ ],
8422
+ default_action: "redact"
8423
+ };
8424
+ var TOOL_API_SCOPED = {
8425
+ id: "tool-api-scoped",
8426
+ name: "Tool API Scoped",
8427
+ description: "Allows tool-specific parameters for external API calls. Redacts memory, history, secrets, and PII.",
8428
+ 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.",
8429
+ rules: [
8430
+ {
8431
+ provider: "tool-api",
8432
+ allow: [
8433
+ "task",
8434
+ "task_description",
8435
+ "query",
8436
+ "search_query",
8437
+ "tool_input",
8438
+ "tool_parameters",
8439
+ "url",
8440
+ "endpoint",
8441
+ "method",
8442
+ "filter",
8443
+ "sort",
8444
+ "limit",
8445
+ "offset"
8446
+ ],
8447
+ redact: [
8448
+ ...ALWAYS_REDACT_SECRETS,
8449
+ ...PII_PATTERNS,
8450
+ ...INTERNAL_STATE_PATTERNS,
8451
+ ...HISTORY_PATTERNS
8452
+ ],
8453
+ hash: [...ID_PATTERNS],
8454
+ summarize: []
8455
+ }
8456
+ ],
8457
+ default_action: "redact"
8458
+ };
8459
+ var TEMPLATES = {
8460
+ "inference-minimal": INFERENCE_MINIMAL,
8461
+ "inference-standard": INFERENCE_STANDARD,
8462
+ "logging-strict": LOGGING_STRICT,
8463
+ "tool-api-scoped": TOOL_API_SCOPED
8464
+ };
8465
+ function listTemplateIds() {
8466
+ return Object.keys(TEMPLATES);
8467
+ }
8468
+ function getTemplate(id) {
8469
+ return TEMPLATES[id];
8470
+ }
8471
+
8472
+ // src/l2-operational/context-gate-recommend.ts
8473
+ var CLASSIFICATION_RULES = [
8474
+ // ── Secrets (always redact, high confidence) ─────────────────────
8475
+ {
8476
+ patterns: [
8477
+ "api_key",
8478
+ "apikey",
8479
+ "api_secret",
8480
+ "secret",
8481
+ "secret_key",
8482
+ "secret_token",
8483
+ "password",
8484
+ "passwd",
8485
+ "pass",
8486
+ "credential",
8487
+ "credentials",
8488
+ "private_key",
8489
+ "privkey",
8490
+ "recovery_key",
8491
+ "passphrase",
8492
+ "token",
8493
+ "access_token",
8494
+ "refresh_token",
8495
+ "bearer_token",
8496
+ "auth_token",
8497
+ "auth_header",
8498
+ "authorization",
8499
+ "encryption_key",
8500
+ "master_key",
8501
+ "signing_key",
8502
+ "webhook_secret",
8503
+ "client_secret",
8504
+ "connection_string"
8505
+ ],
8506
+ action: "redact",
8507
+ confidence: "high",
8508
+ reason: "Matches known secret/credential pattern"
8509
+ },
8510
+ // ── PII (always redact, high confidence) ─────────────────────────
8511
+ {
8512
+ patterns: [
8513
+ "name",
8514
+ "full_name",
8515
+ "first_name",
8516
+ "last_name",
8517
+ "display_name",
8518
+ "email",
8519
+ "email_address",
8520
+ "phone",
8521
+ "phone_number",
8522
+ "mobile",
8523
+ "address",
8524
+ "street_address",
8525
+ "mailing_address",
8526
+ "ssn",
8527
+ "social_security",
8528
+ "date_of_birth",
8529
+ "dob",
8530
+ "birthday",
8531
+ "ip_address",
8532
+ "ip",
8533
+ "location",
8534
+ "geolocation",
8535
+ "coordinates",
8536
+ "credit_card",
8537
+ "card_number",
8538
+ "cvv",
8539
+ "bank_account",
8540
+ "routing_number",
8541
+ "passport",
8542
+ "drivers_license",
8543
+ "license_number"
8544
+ ],
8545
+ action: "redact",
8546
+ confidence: "high",
8547
+ reason: "Matches known PII pattern"
8548
+ },
8549
+ // ── Internal agent state (redact, high confidence) ───────────────
8550
+ {
8551
+ patterns: [
8552
+ "memory",
8553
+ "agent_memory",
8554
+ "long_term_memory",
8555
+ "internal_reasoning",
8556
+ "reasoning_trace",
8557
+ "chain_of_thought",
8558
+ "internal_state",
8559
+ "agent_state",
8560
+ "private_notes",
8561
+ "scratchpad",
8562
+ "soul",
8563
+ "personality",
8564
+ "persona",
8565
+ "system_prompt",
8566
+ "system_message",
8567
+ "system_instruction",
8568
+ "preferences",
8569
+ "user_preferences",
8570
+ "agent_preferences",
8571
+ "beliefs",
8572
+ "goals",
8573
+ "motivations"
8574
+ ],
8575
+ action: "redact",
8576
+ confidence: "high",
8577
+ reason: "Matches known internal agent state pattern"
8578
+ },
8579
+ // ── IDs (hash, medium confidence) ────────────────────────────────
8580
+ {
8581
+ patterns: [
8582
+ "user_id",
8583
+ "userid",
8584
+ "session_id",
8585
+ "sessionid",
8586
+ "agent_id",
8587
+ "agentid",
8588
+ "identity_id",
8589
+ "conversation_id",
8590
+ "thread_id",
8591
+ "threadid",
8592
+ "request_id",
8593
+ "requestid",
8594
+ "correlation_id",
8595
+ "trace_id",
8596
+ "traceid",
8597
+ "account_id",
8598
+ "accountid"
8599
+ ],
8600
+ action: "hash",
8601
+ confidence: "medium",
8602
+ reason: "Matches known identifier pattern \u2014 hash preserves correlation without exposing value"
8603
+ },
8604
+ // ── History (summarize, medium confidence) ───────────────────────
8605
+ {
8606
+ patterns: [
8607
+ "conversation_history",
8608
+ "chat_history",
8609
+ "message_history",
8610
+ "messages",
8611
+ "previous_messages",
8612
+ "prior_messages",
8613
+ "context_window",
8614
+ "interaction_history",
8615
+ "audit_log",
8616
+ "event_log"
8617
+ ],
8618
+ action: "summarize",
8619
+ confidence: "medium",
8620
+ reason: "Matches known history/log pattern \u2014 summarize to reduce exposure"
8621
+ },
8622
+ // ── Task/query (allow, medium confidence) ────────────────────────
8623
+ {
8624
+ patterns: [
8625
+ "task",
8626
+ "task_description",
8627
+ "query",
8628
+ "current_query",
8629
+ "search_query",
8630
+ "prompt",
8631
+ "user_prompt",
8632
+ "question",
8633
+ "current_question",
8634
+ "instruction",
8635
+ "instructions",
8636
+ "objective",
8637
+ "goal",
8638
+ "current_step",
8639
+ "next_step",
8640
+ "remaining_steps",
8641
+ "constraints",
8642
+ "requirements",
8643
+ "output_format",
8644
+ "format",
8645
+ "tool_results",
8646
+ "tool_output",
8647
+ "tool_input",
8648
+ "tool_parameters"
8649
+ ],
8650
+ action: "allow",
8651
+ confidence: "medium",
8652
+ reason: "Matches known task/query pattern \u2014 likely needed for inference"
8653
+ }
8654
+ ];
8655
+ function classifyField(fieldName) {
8656
+ const normalized = fieldName.toLowerCase().trim();
8657
+ for (const rule of CLASSIFICATION_RULES) {
8658
+ for (const pattern of rule.patterns) {
8659
+ if (matchesFieldPattern(normalized, pattern)) {
8660
+ return {
8661
+ field: fieldName,
8662
+ recommended_action: rule.action,
8663
+ reason: rule.reason,
8664
+ confidence: rule.confidence,
8665
+ matched_pattern: pattern
8666
+ };
8667
+ }
8668
+ }
8669
+ }
8670
+ return {
8671
+ field: fieldName,
8672
+ recommended_action: "redact",
8673
+ reason: "No known pattern matched \u2014 defaulting to redact (conservative)",
8674
+ confidence: "low",
8675
+ matched_pattern: null
8676
+ };
8677
+ }
8678
+ function recommendPolicy(context, provider = "inference") {
8679
+ const fields = Object.keys(context);
8680
+ const classifications = fields.map(classifyField);
8681
+ const warnings = [];
8682
+ const allow = [];
8683
+ const redact = [];
8684
+ const hash2 = [];
8685
+ const summarize = [];
8686
+ for (const c of classifications) {
8687
+ switch (c.recommended_action) {
8688
+ case "allow":
8689
+ allow.push(c.field);
8690
+ break;
8691
+ case "redact":
8692
+ redact.push(c.field);
8693
+ break;
8694
+ case "hash":
8695
+ hash2.push(c.field);
8696
+ break;
8697
+ case "summarize":
8698
+ summarize.push(c.field);
8699
+ break;
8700
+ }
8701
+ }
8702
+ const lowConfidence = classifications.filter((c) => c.confidence === "low");
8703
+ if (lowConfidence.length > 0) {
8704
+ warnings.push(
8705
+ `${lowConfidence.length} field(s) could not be classified by pattern and will default to redact: ${lowConfidence.map((c) => c.field).join(", ")}. Review these manually.`
8706
+ );
8707
+ }
8708
+ for (const [key, value] of Object.entries(context)) {
8709
+ if (typeof value === "string" && value.length > 5e3) {
8710
+ const existing = classifications.find((c) => c.field === key);
8711
+ if (existing && existing.recommended_action === "allow") {
8712
+ warnings.push(
8713
+ `Field "${key}" is allowed but contains ${value.length} characters. Consider summarizing it to reduce context size and exposure.`
8714
+ );
8715
+ }
8716
+ }
8717
+ }
8718
+ return {
8719
+ provider,
8720
+ classifications,
8721
+ recommended_rules: { allow, redact, hash: hash2, summarize },
8722
+ default_action: "redact",
8723
+ summary: {
8724
+ total_fields: fields.length,
8725
+ allow: allow.length,
8726
+ redact: redact.length,
8727
+ hash: hash2.length,
8728
+ summarize: summarize.length
8729
+ },
8730
+ warnings
8731
+ };
8732
+ }
8733
+ function matchesFieldPattern(normalizedField, pattern) {
8734
+ if (normalizedField === pattern) return true;
8735
+ if (pattern.length >= 3 && normalizedField.includes(pattern)) {
8736
+ const idx = normalizedField.indexOf(pattern);
8737
+ const before = idx === 0 || normalizedField[idx - 1] === "_" || normalizedField[idx - 1] === "-";
8738
+ const after = idx + pattern.length === normalizedField.length || normalizedField[idx + pattern.length] === "_" || normalizedField[idx + pattern.length] === "-";
8739
+ return before && after;
8740
+ }
8741
+ return false;
8742
+ }
8743
+
8744
+ // src/l2-operational/context-gate-tools.ts
8745
+ function createContextGateTools(storage, masterKey, auditLog) {
8746
+ const policyStore = new ContextGatePolicyStore(storage, masterKey);
8747
+ const tools = [
8748
+ // ── Set Policy ──────────────────────────────────────────────────
8749
+ {
8750
+ name: "sanctuary/context_gate_set_policy",
8751
+ 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.",
8752
+ inputSchema: {
8753
+ type: "object",
8754
+ properties: {
8755
+ policy_name: {
8756
+ type: "string",
8757
+ description: "Human-readable name for this policy (e.g., 'inference-minimal', 'tool-api-strict')"
8758
+ },
8759
+ rules: {
8760
+ type: "array",
8761
+ 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).",
8762
+ items: {
8763
+ type: "object",
8764
+ properties: {
8765
+ provider: {
8766
+ type: "string",
8767
+ description: "Provider category: inference, tool-api, logging, analytics, peer-agent, custom, or * for all"
8768
+ },
8769
+ allow: {
8770
+ type: "array",
8771
+ items: { type: "string" },
8772
+ description: "Fields/patterns to allow through (e.g., 'task_description', 'current_query', 'tool_*')"
8773
+ },
8774
+ redact: {
8775
+ type: "array",
8776
+ items: { type: "string" },
8777
+ description: "Fields/patterns to redact (e.g., 'conversation_history', 'secret_*', '*_pii'). Takes absolute priority."
8778
+ },
8779
+ hash: {
8780
+ type: "array",
8781
+ items: { type: "string" },
8782
+ description: "Fields/patterns to replace with SHA-256 hash (e.g., 'user_id', 'session_id')"
8783
+ },
8784
+ summarize: {
8785
+ type: "array",
8786
+ items: { type: "string" },
8787
+ description: "Fields/patterns to flag for summarization (advisory \u2014 agent should compress these before sending)"
8788
+ }
8789
+ },
8790
+ required: ["provider", "allow", "redact"]
8791
+ }
8792
+ },
8793
+ default_action: {
8794
+ type: "string",
8795
+ enum: ["redact", "deny"],
8796
+ description: "Action for fields not matched by any rule. 'redact' removes the field value; 'deny' blocks the entire request. Default: 'redact'."
8797
+ },
8798
+ identity_id: {
8799
+ type: "string",
8800
+ description: "Bind this policy to a specific identity (optional)"
8801
+ }
8802
+ },
8803
+ required: ["policy_name", "rules"]
8804
+ },
8805
+ handler: async (args) => {
8806
+ const policyName = args.policy_name;
8807
+ const rawRules = args.rules;
8808
+ const defaultAction = args.default_action ?? "redact";
8809
+ const identityId = args.identity_id;
8810
+ if (!Array.isArray(rawRules)) {
8811
+ return toolResult({ error: "invalid_rules", message: "rules must be an array" });
8812
+ }
8813
+ if (rawRules.length > MAX_POLICY_RULES) {
8814
+ return toolResult({
8815
+ error: "too_many_rules",
8816
+ message: `Policy has ${rawRules.length} rules, exceeding limit of ${MAX_POLICY_RULES}`
8817
+ });
8818
+ }
8819
+ const rules = [];
8820
+ for (const r of rawRules) {
8821
+ const allow = Array.isArray(r.allow) ? r.allow : [];
8822
+ const redact = Array.isArray(r.redact) ? r.redact : [];
8823
+ const hash2 = Array.isArray(r.hash) ? r.hash : [];
8824
+ const summarize = Array.isArray(r.summarize) ? r.summarize : [];
8825
+ for (const [name, arr] of [["allow", allow], ["redact", redact], ["hash", hash2], ["summarize", summarize]]) {
8826
+ if (arr.length > MAX_PATTERNS_PER_ARRAY) {
8827
+ return toolResult({
8828
+ error: "too_many_patterns",
8829
+ message: `Rule ${name} array has ${arr.length} patterns, exceeding limit of ${MAX_PATTERNS_PER_ARRAY}`
8830
+ });
8831
+ }
8832
+ }
8833
+ rules.push({
8834
+ provider: r.provider ?? "*",
8835
+ allow,
8836
+ redact,
8837
+ hash: hash2,
8838
+ summarize
8839
+ });
8840
+ }
8841
+ const policy = await policyStore.create(
8842
+ policyName,
8843
+ rules,
8844
+ defaultAction,
8845
+ identityId
8846
+ );
8847
+ auditLog.append("l2", "context_gate_set_policy", identityId ?? "system", {
8848
+ policy_id: policy.policy_id,
8849
+ policy_name: policyName,
8850
+ rule_count: rules.length,
8851
+ default_action: defaultAction
8852
+ });
8853
+ return toolResult({
8854
+ policy_id: policy.policy_id,
8855
+ policy_name: policy.policy_name,
8856
+ rules: policy.rules,
8857
+ default_action: policy.default_action,
8858
+ created_at: policy.created_at,
8859
+ message: "Context-gating policy created. Use sanctuary/context_gate_filter to apply this policy before making outbound calls."
8860
+ });
8861
+ }
8862
+ },
8863
+ // ── Apply Template ───────────────────────────────────────────────
8864
+ {
8865
+ name: "sanctuary/context_gate_apply_template",
8866
+ 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.",
8867
+ inputSchema: {
8868
+ type: "object",
8869
+ properties: {
8870
+ template_id: {
8871
+ type: "string",
8872
+ description: "Template to apply: inference-minimal, inference-standard, logging-strict, or tool-api-scoped"
8873
+ },
8874
+ identity_id: {
8875
+ type: "string",
8876
+ description: "Bind this policy to a specific identity (optional)"
8877
+ }
8878
+ },
8879
+ required: ["template_id"]
8880
+ },
8881
+ handler: async (args) => {
8882
+ const templateId = args.template_id;
8883
+ const identityId = args.identity_id;
8884
+ const template = getTemplate(templateId);
8885
+ if (!template) {
8886
+ return toolResult({
8887
+ error: "template_not_found",
8888
+ message: `Unknown template "${templateId}"`,
8889
+ available_templates: listTemplateIds().map((id) => {
8890
+ const t = TEMPLATES[id];
8891
+ return { id, name: t.name, description: t.description };
8892
+ })
8893
+ });
8894
+ }
8895
+ const policy = await policyStore.create(
8896
+ template.name,
8897
+ template.rules,
8898
+ template.default_action,
8899
+ identityId
8900
+ );
8901
+ auditLog.append("l2", "context_gate_apply_template", identityId ?? "system", {
8902
+ policy_id: policy.policy_id,
8903
+ template_id: templateId
8904
+ });
8905
+ return toolResult({
8906
+ policy_id: policy.policy_id,
8907
+ template_applied: templateId,
8908
+ policy_name: template.name,
8909
+ description: template.description,
8910
+ use_when: template.use_when,
8911
+ rules: policy.rules,
8912
+ default_action: policy.default_action,
8913
+ created_at: policy.created_at,
8914
+ 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."
8915
+ });
8916
+ }
8917
+ },
8918
+ // ── Recommend Policy ────────────────────────────────────────────
8919
+ {
8920
+ name: "sanctuary/context_gate_recommend",
8921
+ 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.",
8922
+ inputSchema: {
8923
+ type: "object",
8924
+ properties: {
8925
+ context: {
8926
+ type: "object",
8927
+ description: "A sample context object to analyze. Each top-level key will be classified. Values are inspected for size warnings but not stored."
8928
+ },
8929
+ provider: {
8930
+ type: "string",
8931
+ description: "Provider category to generate rules for. Default: 'inference'."
8932
+ }
8933
+ },
8934
+ required: ["context"]
8935
+ },
8936
+ handler: async (args) => {
8937
+ const context = args.context;
8938
+ const provider = args.provider ?? "inference";
8939
+ const contextKeys = Object.keys(context);
8940
+ if (contextKeys.length > MAX_CONTEXT_FIELDS) {
8941
+ return toolResult({
8942
+ error: "context_too_large",
8943
+ message: `Context has ${contextKeys.length} fields, exceeding limit of ${MAX_CONTEXT_FIELDS}`
8944
+ });
8945
+ }
8946
+ const recommendation = recommendPolicy(context, provider);
8947
+ auditLog.append("l2", "context_gate_recommend", "system", {
8948
+ provider,
8949
+ fields_analyzed: recommendation.summary.total_fields,
8950
+ fields_allow: recommendation.summary.allow,
8951
+ fields_redact: recommendation.summary.redact,
8952
+ fields_hash: recommendation.summary.hash,
8953
+ fields_summarize: recommendation.summary.summarize
8954
+ });
8955
+ return toolResult({
8956
+ ...recommendation,
8957
+ 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.",
8958
+ available_templates: listTemplateIds().map((id) => {
8959
+ const t = TEMPLATES[id];
8960
+ return { id, name: t.name, description: t.description };
8961
+ })
8962
+ });
8963
+ }
8964
+ },
8965
+ // ── Filter Context ──────────────────────────────────────────────
8966
+ {
8967
+ name: "sanctuary/context_gate_filter",
8968
+ 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.",
8969
+ inputSchema: {
8970
+ type: "object",
8971
+ properties: {
8972
+ policy_id: {
8973
+ type: "string",
8974
+ description: "ID of the context-gating policy to apply"
8975
+ },
8976
+ provider: {
8977
+ type: "string",
8978
+ description: "Provider category for this call: inference, tool-api, logging, analytics, peer-agent, or custom"
8979
+ },
8980
+ context: {
8981
+ type: "object",
8982
+ 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"
8983
+ }
8984
+ },
8985
+ required: ["policy_id", "provider", "context"]
8986
+ },
8987
+ handler: async (args) => {
8988
+ const policyId = args.policy_id;
8989
+ const provider = args.provider;
8990
+ const context = args.context;
8991
+ const contextKeys = Object.keys(context);
8992
+ if (contextKeys.length > MAX_CONTEXT_FIELDS) {
8993
+ return toolResult({
8994
+ error: "context_too_large",
8995
+ message: `Context has ${contextKeys.length} fields, exceeding limit of ${MAX_CONTEXT_FIELDS}`
8996
+ });
8997
+ }
8998
+ const policy = await policyStore.get(policyId);
8999
+ if (!policy) {
9000
+ return toolResult({
9001
+ error: "policy_not_found",
9002
+ message: `No context-gating policy found with ID "${policyId}"`
9003
+ });
9004
+ }
9005
+ const result = filterContext(policy, provider, context);
9006
+ const deniedFields = result.decisions.filter((d) => d.action === "deny");
9007
+ if (deniedFields.length > 0) {
9008
+ auditLog.append("l2", "context_gate_deny", policy.identity_id ?? "system", {
9009
+ policy_id: policyId,
9010
+ provider,
9011
+ denied_fields: deniedFields.map((d) => d.field),
9012
+ original_context_hash: result.original_context_hash
9013
+ });
9014
+ return toolResult({
9015
+ blocked: true,
9016
+ reason: "Context contains fields that trigger deny action",
9017
+ denied_fields: deniedFields.map((d) => ({
9018
+ field: d.field,
9019
+ reason: d.reason
9020
+ })),
9021
+ recommendation: "Remove the denied fields from context before retrying, or update the policy to handle these fields differently."
9022
+ });
9023
+ }
9024
+ const safeContext = {};
9025
+ for (const decision of result.decisions) {
9026
+ switch (decision.action) {
9027
+ case "allow":
9028
+ safeContext[decision.field] = context[decision.field];
9029
+ break;
9030
+ case "redact":
9031
+ break;
9032
+ case "hash":
9033
+ safeContext[decision.field] = decision.hash_value;
9034
+ break;
9035
+ case "summarize":
9036
+ safeContext[decision.field] = context[decision.field];
9037
+ break;
9038
+ }
9039
+ }
9040
+ auditLog.append("l2", "context_gate_filter", policy.identity_id ?? "system", {
9041
+ policy_id: policyId,
9042
+ provider,
9043
+ fields_total: Object.keys(context).length,
9044
+ fields_allowed: result.fields_allowed,
9045
+ fields_redacted: result.fields_redacted,
9046
+ fields_hashed: result.fields_hashed,
9047
+ fields_summarized: result.fields_summarized,
9048
+ original_context_hash: result.original_context_hash,
9049
+ filtered_context_hash: result.filtered_context_hash
9050
+ });
9051
+ return toolResult({
9052
+ blocked: false,
9053
+ safe_context: safeContext,
9054
+ summary: {
9055
+ total_fields: Object.keys(context).length,
9056
+ allowed: result.fields_allowed,
9057
+ redacted: result.fields_redacted,
9058
+ hashed: result.fields_hashed,
9059
+ summarized: result.fields_summarized
9060
+ },
9061
+ decisions: result.decisions,
9062
+ audit: {
9063
+ original_context_hash: result.original_context_hash,
9064
+ filtered_context_hash: result.filtered_context_hash,
9065
+ filtered_at: result.filtered_at
9066
+ },
9067
+ 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
9068
+ });
9069
+ }
9070
+ },
9071
+ // ── List Policies ───────────────────────────────────────────────
9072
+ {
9073
+ name: "sanctuary/context_gate_list_policies",
9074
+ description: "List all configured context-gating policies. Returns policy IDs, names, rule summaries, and default actions.",
9075
+ inputSchema: {
9076
+ type: "object",
9077
+ properties: {}
9078
+ },
9079
+ handler: async () => {
9080
+ const policies = await policyStore.list();
9081
+ auditLog.append("l2", "context_gate_list_policies", "system", {
9082
+ policy_count: policies.length
9083
+ });
9084
+ return toolResult({
9085
+ policies: policies.map((p) => ({
9086
+ policy_id: p.policy_id,
9087
+ policy_name: p.policy_name,
9088
+ rule_count: p.rules.length,
9089
+ providers: p.rules.map((r) => r.provider),
9090
+ default_action: p.default_action,
9091
+ identity_id: p.identity_id ?? null,
9092
+ created_at: p.created_at,
9093
+ updated_at: p.updated_at
9094
+ })),
9095
+ count: policies.length,
9096
+ 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.`
9097
+ });
9098
+ }
9099
+ }
9100
+ ];
9101
+ return { tools, policyStore };
9102
+ }
9103
+ function checkMemoryProtection() {
9104
+ const checks = {
9105
+ aslr_enabled: checkASLR(),
9106
+ stack_canaries: true,
9107
+ // Enabled by default in Node.js runtime
9108
+ secure_buffer_zeros: true,
9109
+ // We use crypto.randomBytes and explicit zeroing
9110
+ argon2id_kdf: true
9111
+ // Master key derivation uses Argon2id
9112
+ };
9113
+ const activeCount = Object.values(checks).filter((v) => v).length;
9114
+ const overall = activeCount >= 4 ? "full" : activeCount >= 3 ? "partial" : "minimal";
9115
+ return {
9116
+ ...checks,
9117
+ overall
9118
+ };
9119
+ }
9120
+ function checkASLR() {
9121
+ if (process.platform === "linux") {
9122
+ try {
9123
+ const result = child_process.execSync("cat /proc/sys/kernel/randomize_va_space", {
9124
+ encoding: "utf-8",
9125
+ stdio: ["pipe", "pipe", "ignore"]
9126
+ }).trim();
9127
+ return result === "2";
9128
+ } catch {
9129
+ return false;
9130
+ }
9131
+ }
9132
+ if (process.platform === "darwin") {
9133
+ return true;
9134
+ }
9135
+ return false;
9136
+ }
9137
+ function checkProcessIsolation() {
9138
+ const isContainer = detectContainer();
9139
+ const isVM = detectVM();
9140
+ const isSandboxed = detectSandbox();
9141
+ let isolationLevel = "none";
9142
+ if (isContainer) isolationLevel = "hardened";
9143
+ else if (isVM) isolationLevel = "hardened";
9144
+ else if (isSandboxed) isolationLevel = "basic";
9145
+ const details = {};
9146
+ if (isContainer && isContainer !== true) details.container_type = isContainer;
9147
+ if (isVM && isVM !== true) details.vm_type = isVM;
9148
+ if (isSandboxed && isSandboxed !== true) details.sandbox_type = isSandboxed;
9149
+ return {
9150
+ isolation_level: isolationLevel,
9151
+ is_container: isContainer !== false,
9152
+ is_vm: isVM !== false,
9153
+ is_sandboxed: isSandboxed !== false,
9154
+ is_tee: false,
9155
+ details
9156
+ };
9157
+ }
9158
+ function detectContainer() {
9159
+ try {
9160
+ if (process.env.DOCKER_HOST) return "docker";
9161
+ try {
9162
+ fs.statSync("/.dockerenv");
9163
+ return "docker";
9164
+ } catch {
9165
+ }
9166
+ if (process.platform === "linux") {
9167
+ const cgroup = child_process.execSync("cat /proc/1/cgroup 2>/dev/null || echo ''", {
9168
+ encoding: "utf-8"
9169
+ });
9170
+ if (cgroup.includes("docker")) return "docker";
9171
+ if (cgroup.includes("lxc")) return "lxc";
9172
+ if (cgroup.includes("kubepods") || cgroup.includes("kubernetes")) return "kubernetes";
9173
+ }
9174
+ if (process.env.container === "podman") return "podman";
9175
+ if (process.env.CONTAINER_ID) return "oci";
9176
+ return false;
9177
+ } catch {
9178
+ return false;
9179
+ }
9180
+ }
9181
+ function detectVM() {
9182
+ if (process.platform === "linux") {
9183
+ try {
9184
+ const dmidecode = child_process.execSync("dmidecode -s system-product-name 2>/dev/null || echo ''", {
9185
+ encoding: "utf-8"
9186
+ }).toLowerCase();
9187
+ if (dmidecode.includes("vmware")) return "vmware";
9188
+ if (dmidecode.includes("virtualbox")) return "virtualbox";
9189
+ if (dmidecode.includes("kvm")) return "kvm";
9190
+ if (dmidecode.includes("xen")) return "xen";
9191
+ if (dmidecode.includes("hyper-v")) return "hyper-v";
9192
+ const cpuinfo = child_process.execSync("grep -i hypervisor /proc/cpuinfo || echo ''", {
9193
+ encoding: "utf-8"
9194
+ });
9195
+ if (cpuinfo.length > 0) return "detected";
9196
+ } catch {
9197
+ }
9198
+ }
9199
+ if (process.platform === "darwin") {
9200
+ try {
9201
+ const bootargs = child_process.execSync(
9202
+ "nvram boot-args 2>/dev/null | grep -i 'parallels\\|vmware\\|virtualbox' || echo ''",
9203
+ {
9204
+ encoding: "utf-8"
9205
+ }
9206
+ );
9207
+ if (bootargs.length > 0) return "detected";
9208
+ } catch {
9209
+ }
9210
+ }
9211
+ return false;
9212
+ }
9213
+ function detectSandbox() {
9214
+ if (process.platform === "darwin") {
9215
+ if (process.env.APP_SANDBOX_READ_ONLY_HOME === "1") return "app-sandbox";
9216
+ if (process.env.TMPDIR && process.env.TMPDIR.includes("AppSandbox")) return "app-sandbox";
9217
+ }
9218
+ if (process.platform === "openbsd") {
9219
+ try {
9220
+ const pledge = child_process.execSync("pledge -v 2>/dev/null || echo ''", {
9221
+ encoding: "utf-8"
9222
+ });
9223
+ if (pledge.length > 0) return "pledge";
9224
+ } catch {
9225
+ }
9226
+ }
9227
+ if (process.platform === "linux") {
9228
+ if (process.env.container === "lxc") return "lxc";
9229
+ try {
9230
+ const context = child_process.execSync("getenforce 2>/dev/null || echo ''", {
9231
+ encoding: "utf-8"
9232
+ }).trim();
9233
+ if (context === "Enforcing") return "selinux";
9234
+ } catch {
9235
+ }
9236
+ }
9237
+ return false;
9238
+ }
9239
+ function checkFilesystemPermissions(storagePath) {
9240
+ try {
9241
+ const stats = fs.statSync(storagePath);
9242
+ const mode = stats.mode & parseInt("777", 8);
9243
+ const modeString = mode.toString(8).padStart(3, "0");
9244
+ const isSecure = mode === parseInt("700", 8);
9245
+ const groupReadable = (mode & parseInt("040", 8)) !== 0;
9246
+ const othersReadable = (mode & parseInt("007", 8)) !== 0;
9247
+ const currentUid = process.getuid?.() || -1;
9248
+ const ownerIsCurrentUser = stats.uid === currentUid;
9249
+ let overall = "secure";
9250
+ if (groupReadable || othersReadable) overall = "insecure";
9251
+ else if (!ownerIsCurrentUser) overall = "warning";
9252
+ return {
9253
+ sanctuary_storage_protected: isSecure,
9254
+ sanctuary_storage_mode: modeString,
9255
+ owner_is_current_user: ownerIsCurrentUser,
9256
+ group_readable: groupReadable,
9257
+ others_readable: othersReadable,
9258
+ overall
9259
+ };
9260
+ } catch {
9261
+ return {
9262
+ sanctuary_storage_protected: false,
9263
+ sanctuary_storage_mode: "unknown",
9264
+ owner_is_current_user: false,
9265
+ group_readable: false,
9266
+ others_readable: false,
9267
+ overall: "warning"
9268
+ };
9269
+ }
9270
+ }
9271
+ function checkRuntimeIntegrity() {
9272
+ return {
9273
+ config_hash_stable: true,
9274
+ environment_state: "clean",
9275
+ discrepancies: []
9276
+ };
9277
+ }
9278
+ function assessL2Hardening(storagePath) {
9279
+ const memory = checkMemoryProtection();
9280
+ const isolation = checkProcessIsolation();
9281
+ const filesystem = checkFilesystemPermissions(storagePath);
9282
+ const integrity = checkRuntimeIntegrity();
9283
+ let checksPassed = 0;
9284
+ let checksTotal = 0;
9285
+ if (memory.aslr_enabled) checksPassed++;
9286
+ checksTotal++;
9287
+ if (memory.stack_canaries) checksPassed++;
9288
+ checksTotal++;
9289
+ if (memory.secure_buffer_zeros) checksPassed++;
9290
+ checksTotal++;
9291
+ if (memory.argon2id_kdf) checksPassed++;
9292
+ checksTotal++;
9293
+ if (isolation.is_container) checksPassed++;
9294
+ checksTotal++;
9295
+ if (isolation.is_vm) checksPassed++;
9296
+ checksTotal++;
9297
+ if (isolation.is_sandboxed) checksPassed++;
9298
+ checksTotal++;
9299
+ if (filesystem.sanctuary_storage_protected) checksPassed++;
9300
+ checksTotal++;
9301
+ {
9302
+ checksPassed++;
9303
+ }
9304
+ checksTotal++;
9305
+ let hardeningLevel = isolation.isolation_level;
9306
+ if (filesystem.overall === "insecure" || memory.overall === "none" || memory.overall === "minimal") {
9307
+ if (hardeningLevel === "hardened") {
9308
+ hardeningLevel = "basic";
9309
+ } else if (hardeningLevel === "basic") {
9310
+ hardeningLevel = "none";
9311
+ }
9312
+ }
9313
+ const summaryParts = [];
9314
+ if (isolation.is_container || isolation.is_vm) {
9315
+ summaryParts.push(`Running in ${isolation.details.container_type || isolation.details.vm_type || "isolated environment"}`);
9316
+ }
9317
+ if (memory.aslr_enabled) {
9318
+ summaryParts.push("ASLR enabled");
9319
+ }
9320
+ if (filesystem.sanctuary_storage_protected) {
9321
+ summaryParts.push("Storage permissions secured (0700)");
9322
+ }
9323
+ const summary = summaryParts.length > 0 ? summaryParts.join("; ") : "No process-level hardening detected";
9324
+ return {
9325
+ hardening_level: hardeningLevel,
9326
+ memory_protection: memory,
9327
+ process_isolation: isolation,
9328
+ filesystem_permissions: filesystem,
9329
+ runtime_integrity: integrity,
9330
+ checks_passed: checksPassed,
9331
+ checks_total: checksTotal,
9332
+ summary
9333
+ };
9334
+ }
9335
+
9336
+ // src/l2-operational/hardening-tools.ts
9337
+ function createL2HardeningTools(storagePath, auditLog) {
9338
+ return [
9339
+ {
9340
+ name: "sanctuary/l2_hardening_status",
9341
+ 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.",
9342
+ inputSchema: {
9343
+ type: "object",
9344
+ properties: {
9345
+ include_details: {
9346
+ type: "boolean",
9347
+ description: "If true, include detailed check results for memory, process, and filesystem. If false, show summary only.",
9348
+ default: false
9349
+ }
9350
+ }
9351
+ },
9352
+ handler: async (args) => {
9353
+ const includeDetails = args.include_details ?? false;
9354
+ const status = assessL2Hardening(storagePath);
9355
+ auditLog.append(
9356
+ "l2",
9357
+ "l2_hardening_status",
9358
+ "system",
9359
+ { include_details: includeDetails }
9360
+ );
9361
+ if (includeDetails) {
9362
+ return toolResult({
9363
+ hardening_level: status.hardening_level,
9364
+ summary: status.summary,
9365
+ checks_passed: status.checks_passed,
9366
+ checks_total: status.checks_total,
9367
+ memory_protection: {
9368
+ aslr_enabled: status.memory_protection.aslr_enabled,
9369
+ stack_canaries: status.memory_protection.stack_canaries,
9370
+ secure_buffer_zeros: status.memory_protection.secure_buffer_zeros,
9371
+ argon2id_kdf: status.memory_protection.argon2id_kdf,
9372
+ overall: status.memory_protection.overall
9373
+ },
9374
+ process_isolation: {
9375
+ isolation_level: status.process_isolation.isolation_level,
9376
+ is_container: status.process_isolation.is_container,
9377
+ is_vm: status.process_isolation.is_vm,
9378
+ is_sandboxed: status.process_isolation.is_sandboxed,
9379
+ is_tee: status.process_isolation.is_tee,
9380
+ details: status.process_isolation.details
9381
+ },
9382
+ filesystem_permissions: {
9383
+ sanctuary_storage_protected: status.filesystem_permissions.sanctuary_storage_protected,
9384
+ sanctuary_storage_mode: status.filesystem_permissions.sanctuary_storage_mode,
9385
+ owner_is_current_user: status.filesystem_permissions.owner_is_current_user,
9386
+ group_readable: status.filesystem_permissions.group_readable,
9387
+ others_readable: status.filesystem_permissions.others_readable,
9388
+ overall: status.filesystem_permissions.overall
9389
+ },
9390
+ runtime_integrity: {
9391
+ config_hash_stable: status.runtime_integrity.config_hash_stable,
9392
+ environment_state: status.runtime_integrity.environment_state,
9393
+ discrepancies: status.runtime_integrity.discrepancies
9394
+ }
9395
+ });
9396
+ } else {
9397
+ return toolResult({
9398
+ hardening_level: status.hardening_level,
9399
+ summary: status.summary,
9400
+ checks_passed: status.checks_passed,
9401
+ checks_total: status.checks_total,
9402
+ note: "Pass include_details: true to see full breakdown of memory, process isolation, and filesystem checks."
9403
+ });
9404
+ }
9405
+ }
9406
+ },
9407
+ {
9408
+ name: "sanctuary/l2_verify_isolation",
9409
+ 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.",
9410
+ inputSchema: {
9411
+ type: "object",
9412
+ properties: {
9413
+ check_filesystem: {
9414
+ type: "boolean",
9415
+ description: "If true, verify Sanctuary storage directory permissions.",
9416
+ default: true
9417
+ },
9418
+ check_memory: {
9419
+ type: "boolean",
9420
+ description: "If true, verify memory protection mechanisms (ASLR, etc.).",
9421
+ default: true
9422
+ },
9423
+ check_process: {
9424
+ type: "boolean",
9425
+ description: "If true, detect container, VM, or sandbox environment.",
9426
+ default: true
9427
+ }
9428
+ }
9429
+ },
9430
+ handler: async (args) => {
9431
+ const checkFilesystem = args.check_filesystem ?? true;
9432
+ const checkMemory = args.check_memory ?? true;
9433
+ const checkProcess = args.check_process ?? true;
9434
+ const status = assessL2Hardening(storagePath);
9435
+ auditLog.append(
9436
+ "l2",
9437
+ "l2_verify_isolation",
9438
+ "system",
9439
+ {
9440
+ check_filesystem: checkFilesystem,
9441
+ check_memory: checkMemory,
9442
+ check_process: checkProcess
9443
+ }
9444
+ );
9445
+ const results = {
9446
+ isolation_level: status.hardening_level,
9447
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
9448
+ };
9449
+ if (checkFilesystem) {
9450
+ const fs = status.filesystem_permissions;
9451
+ results.filesystem = {
9452
+ sanctuary_storage_protected: fs.sanctuary_storage_protected,
9453
+ storage_mode: fs.sanctuary_storage_mode,
9454
+ is_secure: fs.overall === "secure",
9455
+ issues: fs.overall === "insecure" ? [
9456
+ "Storage directory is readable by group or others. Recommend: chmod 700 on Sanctuary storage path."
9457
+ ] : fs.overall === "warning" ? [
9458
+ "Storage directory not owned by current user. Verify correct user is running Sanctuary."
9459
+ ] : []
9460
+ };
9461
+ }
9462
+ if (checkMemory) {
9463
+ const mem = status.memory_protection;
9464
+ const issues = [];
9465
+ if (!mem.aslr_enabled) {
9466
+ issues.push(
9467
+ "ASLR not detected. On Linux, enable with: echo 2 | sudo tee /proc/sys/kernel/randomize_va_space"
9468
+ );
9469
+ }
9470
+ results.memory = {
9471
+ aslr_enabled: mem.aslr_enabled,
9472
+ stack_canaries: mem.stack_canaries,
9473
+ secure_buffer_handling: mem.secure_buffer_zeros,
9474
+ argon2id_key_derivation: mem.argon2id_kdf,
9475
+ protection_level: mem.overall,
9476
+ issues
9477
+ };
9478
+ }
9479
+ if (checkProcess) {
9480
+ const iso = status.process_isolation;
9481
+ results.process = {
9482
+ isolation_level: iso.isolation_level,
9483
+ in_container: iso.is_container,
9484
+ in_vm: iso.is_vm,
9485
+ sandboxed: iso.is_sandboxed,
9486
+ has_tee: iso.is_tee,
9487
+ environment: iso.details,
9488
+ 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."
9489
+ };
9490
+ }
9491
+ return toolResult({
9492
+ status: "verified",
9493
+ results
9494
+ });
9495
+ }
9496
+ }
9497
+ ];
9498
+ }
9499
+
9500
+ // src/index.ts
9501
+ init_encoding();
9502
+ async function createSanctuaryServer(options) {
9503
+ const config = await loadConfig(options?.configPath);
9504
+ await promises.mkdir(config.storage_path, { recursive: true, mode: 448 });
9505
+ const storage = options?.storage ?? new FilesystemStorage(
9506
+ `${config.storage_path}/state`
9507
+ );
9508
+ let masterKey;
9509
+ let keyProtection;
9510
+ let recoveryKey;
9511
+ const passphrase = options?.passphrase ?? process.env.SANCTUARY_PASSPHRASE;
9512
+ if (passphrase) {
9513
+ keyProtection = "passphrase";
9514
+ let existingParams;
9515
+ try {
9516
+ const raw = await storage.read("_meta", "key-params");
9517
+ if (raw) {
9518
+ const { bytesToString: bytesToString2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
9519
+ existingParams = JSON.parse(bytesToString2(raw));
9520
+ }
9521
+ } catch {
9522
+ }
9523
+ const result = await deriveMasterKey(passphrase, existingParams);
9524
+ masterKey = result.key;
9525
+ if (!existingParams) {
9526
+ const { stringToBytes: stringToBytes2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
7586
9527
  await storage.write(
7587
9528
  "_meta",
7588
9529
  "key-params",
@@ -7722,7 +9663,7 @@ async function createSanctuaryServer(options) {
7722
9663
  layer: "l2",
7723
9664
  description: "Process-level isolation only (no TEE)",
7724
9665
  severity: "warning",
7725
- mitigation: "TEE support planned for v0.3.0"
9666
+ mitigation: "TEE support planned for a future release"
7726
9667
  });
7727
9668
  if (config.disclosure.proof_system === "commitment-only") {
7728
9669
  degradations.push({
@@ -7862,7 +9803,7 @@ async function createSanctuaryServer(options) {
7862
9803
  },
7863
9804
  limitations: [
7864
9805
  "L1 identity uses ed25519 only; KERI support planned for v0.2.0",
7865
- "L2 isolation is process-level only; TEE support planned for v0.3.0",
9806
+ "L2 isolation is process-level only; TEE support planned for a future release",
7866
9807
  "L3 uses commitment schemes only; ZK proofs planned for v0.2.0",
7867
9808
  "L4 Sybil resistance is escrow-based only",
7868
9809
  "Spec license: CC-BY-4.0 | Code license: Apache-2.0"
@@ -7883,7 +9824,7 @@ async function createSanctuaryServer(options) {
7883
9824
  masterKey,
7884
9825
  auditLog
7885
9826
  );
7886
- const { tools: l4Tools } = createL4Tools(
9827
+ const { tools: l4Tools} = createL4Tools(
7887
9828
  storage,
7888
9829
  masterKey,
7889
9830
  identityManager,
@@ -7902,6 +9843,12 @@ async function createSanctuaryServer(options) {
7902
9843
  handshakeResults
7903
9844
  );
7904
9845
  const { tools: auditTools } = createAuditTools(config);
9846
+ const { tools: contextGateTools } = createContextGateTools(
9847
+ storage,
9848
+ masterKey,
9849
+ auditLog
9850
+ );
9851
+ const hardeningTools = createL2HardeningTools(config.storage_path, auditLog);
7905
9852
  const policy = await loadPrincipalPolicy(config.storage_path);
7906
9853
  const baseline = new BaselineTracker(storage, masterKey);
7907
9854
  await baseline.load();
@@ -7951,6 +9898,8 @@ async function createSanctuaryServer(options) {
7951
9898
  ...federationTools,
7952
9899
  ...bridgeTools,
7953
9900
  ...auditTools,
9901
+ ...contextGateTools,
9902
+ ...hardeningTools,
7954
9903
  manifestTool
7955
9904
  ];
7956
9905
  const server = createServer(allTools, { gate });
@@ -7975,8 +9924,78 @@ async function createSanctuaryServer(options) {
7975
9924
  }
7976
9925
  return { server, config };
7977
9926
  }
7978
-
7979
- // src/cli.ts
9927
+ var REGISTRY_URL = "https://registry.npmjs.org/@sanctuary-framework/mcp-server/latest";
9928
+ var TIMEOUT_MS = 3e3;
9929
+ function isNewerVersion(current, latest) {
9930
+ const parse = (v) => v.replace(/^v/, "").split(".").map(Number);
9931
+ const [curMajor = 0, curMinor = 0, curPatch = 0] = parse(current);
9932
+ const [latMajor = 0, latMinor = 0, latPatch = 0] = parse(latest);
9933
+ if (latMajor !== curMajor) return latMajor > curMajor;
9934
+ if (latMinor !== curMinor) return latMinor > curMinor;
9935
+ return latPatch > curPatch;
9936
+ }
9937
+ function formatUpdateMessage(current, latest) {
9938
+ return `[Sanctuary] Update available: ${current} \u2192 ${latest} \u2014 run: npx @sanctuary-framework/mcp-server@latest`;
9939
+ }
9940
+ function fetchLatestVersion(currentVersion) {
9941
+ return new Promise((resolve) => {
9942
+ const req = https.get(
9943
+ REGISTRY_URL,
9944
+ {
9945
+ headers: { Accept: "application/json" },
9946
+ timeout: TIMEOUT_MS
9947
+ },
9948
+ (res) => {
9949
+ if (res.statusCode !== 200) {
9950
+ res.resume();
9951
+ resolve(null);
9952
+ return;
9953
+ }
9954
+ let data = "";
9955
+ res.setEncoding("utf-8");
9956
+ res.on("data", (chunk) => {
9957
+ data += chunk;
9958
+ if (data.length > 32768) {
9959
+ res.destroy();
9960
+ resolve(null);
9961
+ }
9962
+ });
9963
+ res.on("end", () => {
9964
+ try {
9965
+ const json = JSON.parse(data);
9966
+ const latest = json.version;
9967
+ if (typeof latest === "string" && isNewerVersion(currentVersion, latest)) {
9968
+ resolve(latest);
9969
+ } else {
9970
+ resolve(null);
9971
+ }
9972
+ } catch {
9973
+ resolve(null);
9974
+ }
9975
+ });
9976
+ }
9977
+ );
9978
+ req.on("error", () => resolve(null));
9979
+ req.on("timeout", () => {
9980
+ req.destroy();
9981
+ resolve(null);
9982
+ });
9983
+ });
9984
+ }
9985
+ async function checkForUpdate(currentVersion) {
9986
+ if (process.env.SANCTUARY_NO_UPDATE_CHECK === "1") {
9987
+ return;
9988
+ }
9989
+ try {
9990
+ const latest = await fetchLatestVersion(currentVersion);
9991
+ if (latest) {
9992
+ console.error(formatUpdateMessage(currentVersion, latest));
9993
+ }
9994
+ } catch {
9995
+ }
9996
+ }
9997
+ var require5 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)));
9998
+ var { version: PKG_VERSION4 } = require5("../package.json");
7980
9999
  async function main() {
7981
10000
  const args = process.argv.slice(2);
7982
10001
  let passphrase = process.env.SANCTUARY_PASSPHRASE;
@@ -7989,7 +10008,7 @@ async function main() {
7989
10008
  printHelp();
7990
10009
  process.exit(0);
7991
10010
  } else if (args[i] === "--version" || args[i] === "-v") {
7992
- console.log("@sanctuary-framework/mcp-server 0.3.0");
10011
+ console.log(`@sanctuary-framework/mcp-server ${PKG_VERSION4}`);
7993
10012
  process.exit(0);
7994
10013
  }
7995
10014
  }
@@ -8000,6 +10019,7 @@ async function main() {
8000
10019
  console.error(`Sanctuary MCP Server v${config.version} running (stdio)`);
8001
10020
  console.error(`Storage: ${config.storage_path}`);
8002
10021
  console.error("Tools: all registered");
10022
+ checkForUpdate(PKG_VERSION4);
8003
10023
  } else {
8004
10024
  console.error("HTTP transport not yet implemented. Use stdio.");
8005
10025
  process.exit(1);
@@ -8007,7 +10027,7 @@ async function main() {
8007
10027
  }
8008
10028
  function printHelp() {
8009
10029
  console.log(`
8010
- @sanctuary-framework/mcp-server v0.3.0
10030
+ @sanctuary-framework/mcp-server v${PKG_VERSION4}
8011
10031
 
8012
10032
  Sovereignty infrastructure for agents in the agentic economy.
8013
10033
 
@@ -8029,6 +10049,7 @@ Environment variables:
8029
10049
  SANCTUARY_WEBHOOK_ENABLED "true" to enable webhook approvals
8030
10050
  SANCTUARY_WEBHOOK_URL Webhook target URL
8031
10051
  SANCTUARY_WEBHOOK_SECRET HMAC-SHA256 shared secret
10052
+ SANCTUARY_NO_UPDATE_CHECK "1" to disable startup update check
8032
10053
 
8033
10054
  For more info: https://github.com/eriknewton/sanctuary-framework
8034
10055
  `);