@sanctuary-framework/mcp-server 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { hmac } from '@noble/hashes/hmac';
3
3
  import { readFile, mkdir, writeFile, stat, unlink, readdir, chmod, access } from 'fs/promises';
4
4
  import { join } from 'path';
5
5
  import { homedir } from 'os';
6
+ import { createRequire } from 'module';
6
7
  import { randomBytes as randomBytes$1, createHmac } from 'crypto';
7
8
  import { gcm } from '@noble/ciphers/aes.js';
8
9
  import { RistrettoPoint, ed25519 } from '@noble/curves/ed25519';
@@ -12,7 +13,8 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
12
13
  import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
13
14
  import { createServer as createServer$2 } from 'http';
14
15
  import { createServer as createServer$1 } from 'https';
15
- import { readFileSync } from 'fs';
16
+ import { readFileSync, statSync } from 'fs';
17
+ import { execSync } from 'child_process';
16
18
 
17
19
  var __defProp = Object.defineProperty;
18
20
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -202,9 +204,12 @@ var init_hashing = __esm({
202
204
  init_encoding();
203
205
  }
204
206
  });
207
+ var require2 = createRequire(import.meta.url);
208
+ var { version: PKG_VERSION } = require2("../package.json");
209
+ var SANCTUARY_VERSION = PKG_VERSION;
205
210
  function defaultConfig() {
206
211
  return {
207
- version: "0.3.0",
212
+ version: PKG_VERSION,
208
213
  storage_path: join(homedir(), ".sanctuary"),
209
214
  state: {
210
215
  encryption: "aes-256-gcm",
@@ -330,6 +335,18 @@ function validateConfig(config) {
330
335
  `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.`
331
336
  );
332
337
  }
338
+ const implementedDisclosurePolicy = /* @__PURE__ */ new Set(["minimum-necessary"]);
339
+ if (!implementedDisclosurePolicy.has(config.disclosure.default_policy)) {
340
+ errors.push(
341
+ `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.`
342
+ );
343
+ }
344
+ const implementedReputationMode = /* @__PURE__ */ new Set(["self-custodied"]);
345
+ if (!implementedReputationMode.has(config.reputation.mode)) {
346
+ errors.push(
347
+ `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.`
348
+ );
349
+ }
333
350
  if (errors.length > 0) {
334
351
  throw new Error(
335
352
  `Sanctuary configuration references unimplemented features:
@@ -1035,6 +1052,8 @@ var StateStore = class {
1035
1052
  };
1036
1053
  }
1037
1054
  };
1055
+ var require3 = createRequire(import.meta.url);
1056
+ var { version: PKG_VERSION2 } = require3("../package.json");
1038
1057
  var MAX_STRING_BYTES = 1048576;
1039
1058
  var MAX_BUNDLE_BYTES = 5242880;
1040
1059
  var BUNDLE_FIELDS = /* @__PURE__ */ new Set(["bundle"]);
@@ -1117,7 +1136,7 @@ function createServer(tools, options) {
1117
1136
  const server = new Server(
1118
1137
  {
1119
1138
  name: "sanctuary-mcp-server",
1120
- version: "0.3.0"
1139
+ version: PKG_VERSION2
1121
1140
  },
1122
1141
  {
1123
1142
  capabilities: {
@@ -3571,7 +3590,9 @@ var DEFAULT_POLICY = {
3571
3590
  "state_delete",
3572
3591
  "identity_rotate",
3573
3592
  "reputation_import",
3574
- "bootstrap_provide_guarantee"
3593
+ "reputation_export",
3594
+ "bootstrap_provide_guarantee",
3595
+ "decommission_certificate"
3575
3596
  ],
3576
3597
  tier2_anomaly: DEFAULT_TIER2,
3577
3598
  tier3_always_allow: [
@@ -3588,7 +3609,6 @@ var DEFAULT_POLICY = {
3588
3609
  "disclosure_evaluate",
3589
3610
  "reputation_record",
3590
3611
  "reputation_query",
3591
- "reputation_export",
3592
3612
  "bootstrap_create_escrow",
3593
3613
  "exec_attest",
3594
3614
  "monitor_health",
@@ -3610,7 +3630,19 @@ var DEFAULT_POLICY = {
3610
3630
  "zk_prove",
3611
3631
  "zk_verify",
3612
3632
  "zk_range_prove",
3613
- "zk_range_verify"
3633
+ "zk_range_verify",
3634
+ "context_gate_set_policy",
3635
+ "context_gate_apply_template",
3636
+ "context_gate_recommend",
3637
+ "context_gate_filter",
3638
+ "context_gate_list_policies",
3639
+ "l2_hardening_status",
3640
+ "l2_verify_isolation",
3641
+ "sovereignty_audit",
3642
+ "shr_gateway_export",
3643
+ "bridge_commit",
3644
+ "bridge_verify",
3645
+ "bridge_attest"
3614
3646
  ],
3615
3647
  approval_channel: DEFAULT_CHANNEL
3616
3648
  };
@@ -3712,6 +3744,7 @@ tier1_always_approve:
3712
3744
  - state_delete
3713
3745
  - identity_rotate
3714
3746
  - reputation_import
3747
+ - reputation_export
3715
3748
  - bootstrap_provide_guarantee
3716
3749
 
3717
3750
  # \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
@@ -3741,7 +3774,6 @@ tier3_always_allow:
3741
3774
  - disclosure_evaluate
3742
3775
  - reputation_record
3743
3776
  - reputation_query
3744
- - reputation_export
3745
3777
  - bootstrap_create_escrow
3746
3778
  - exec_attest
3747
3779
  - monitor_health
@@ -3764,6 +3796,16 @@ tier3_always_allow:
3764
3796
  - zk_verify
3765
3797
  - zk_range_prove
3766
3798
  - zk_range_verify
3799
+ - context_gate_set_policy
3800
+ - context_gate_apply_template
3801
+ - context_gate_recommend
3802
+ - context_gate_filter
3803
+ - context_gate_list_policies
3804
+ - sovereignty_audit
3805
+ - shr_gateway_export
3806
+ - bridge_commit
3807
+ - bridge_verify
3808
+ - bridge_attest
3767
3809
 
3768
3810
  # \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
3769
3811
  # How Sanctuary reaches you when approval is needed.
@@ -4560,6 +4602,10 @@ function generateDashboardHTML(options) {
4560
4602
  // src/principal-policy/dashboard.ts
4561
4603
  var SESSION_TTL_MS = 5 * 60 * 1e3;
4562
4604
  var MAX_SESSIONS = 1e3;
4605
+ var RATE_LIMIT_WINDOW_MS = 6e4;
4606
+ var RATE_LIMIT_GENERAL = 120;
4607
+ var RATE_LIMIT_DECISIONS = 20;
4608
+ var MAX_RATE_LIMIT_ENTRIES = 1e4;
4563
4609
  var DashboardApprovalChannel = class {
4564
4610
  config;
4565
4611
  pending = /* @__PURE__ */ new Map();
@@ -4574,13 +4620,15 @@ var DashboardApprovalChannel = class {
4574
4620
  /** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
4575
4621
  sessions = /* @__PURE__ */ new Map();
4576
4622
  sessionCleanupTimer = null;
4623
+ /** Rate limiting: per-IP request tracking */
4624
+ rateLimits = /* @__PURE__ */ new Map();
4577
4625
  constructor(config) {
4578
4626
  this.config = config;
4579
4627
  this.authToken = config.auth_token;
4580
4628
  this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
4581
4629
  this.dashboardHTML = generateDashboardHTML({
4582
4630
  timeoutSeconds: config.timeout_seconds,
4583
- serverVersion: "0.3.0",
4631
+ serverVersion: SANCTUARY_VERSION,
4584
4632
  authToken: this.authToken
4585
4633
  });
4586
4634
  this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
@@ -4659,6 +4707,7 @@ var DashboardApprovalChannel = class {
4659
4707
  clearInterval(this.sessionCleanupTimer);
4660
4708
  this.sessionCleanupTimer = null;
4661
4709
  }
4710
+ this.rateLimits.clear();
4662
4711
  if (this.httpServer) {
4663
4712
  return new Promise((resolve) => {
4664
4713
  this.httpServer.close(() => resolve());
@@ -4784,6 +4833,61 @@ var DashboardApprovalChannel = class {
4784
4833
  }
4785
4834
  }
4786
4835
  }
4836
+ // ── Rate Limiting ─────────────────────────────────────────────────
4837
+ /**
4838
+ * Get the remote address from a request, normalizing IPv6-mapped IPv4.
4839
+ */
4840
+ getRemoteAddr(req) {
4841
+ const addr = req.socket.remoteAddress ?? "unknown";
4842
+ return addr.startsWith("::ffff:") ? addr.slice(7) : addr;
4843
+ }
4844
+ /**
4845
+ * Check rate limit for a request. Returns true if allowed, false if rate-limited.
4846
+ * When rate-limited, sends a 429 response.
4847
+ */
4848
+ checkRateLimit(req, res, type) {
4849
+ const addr = this.getRemoteAddr(req);
4850
+ const now = Date.now();
4851
+ const windowStart = now - RATE_LIMIT_WINDOW_MS;
4852
+ let entry = this.rateLimits.get(addr);
4853
+ if (!entry) {
4854
+ if (this.rateLimits.size >= MAX_RATE_LIMIT_ENTRIES) {
4855
+ this.pruneRateLimits(now);
4856
+ }
4857
+ entry = { general: [], decisions: [] };
4858
+ this.rateLimits.set(addr, entry);
4859
+ }
4860
+ entry.general = entry.general.filter((t) => t > windowStart);
4861
+ entry.decisions = entry.decisions.filter((t) => t > windowStart);
4862
+ const limit = type === "decisions" ? RATE_LIMIT_DECISIONS : RATE_LIMIT_GENERAL;
4863
+ const timestamps = entry[type];
4864
+ if (timestamps.length >= limit) {
4865
+ const retryAfter = Math.ceil((timestamps[0] + RATE_LIMIT_WINDOW_MS - now) / 1e3);
4866
+ res.writeHead(429, {
4867
+ "Content-Type": "application/json",
4868
+ "Retry-After": String(Math.max(1, retryAfter))
4869
+ });
4870
+ res.end(JSON.stringify({
4871
+ error: "Rate limit exceeded",
4872
+ retry_after_seconds: Math.max(1, retryAfter)
4873
+ }));
4874
+ return false;
4875
+ }
4876
+ timestamps.push(now);
4877
+ return true;
4878
+ }
4879
+ /**
4880
+ * Remove stale entries from the rate limit map.
4881
+ */
4882
+ pruneRateLimits(now) {
4883
+ const windowStart = now - RATE_LIMIT_WINDOW_MS;
4884
+ for (const [addr, entry] of this.rateLimits) {
4885
+ const hasRecent = entry.general.some((t) => t > windowStart) || entry.decisions.some((t) => t > windowStart);
4886
+ if (!hasRecent) {
4887
+ this.rateLimits.delete(addr);
4888
+ }
4889
+ }
4890
+ }
4787
4891
  // ── HTTP Request Handler ────────────────────────────────────────────
4788
4892
  handleRequest(req, res) {
4789
4893
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
@@ -4802,6 +4906,7 @@ var DashboardApprovalChannel = class {
4802
4906
  return;
4803
4907
  }
4804
4908
  if (!this.checkAuth(req, url, res)) return;
4909
+ if (!this.checkRateLimit(req, res, "general")) return;
4805
4910
  try {
4806
4911
  if (method === "POST" && url.pathname === "/auth/session") {
4807
4912
  this.handleSessionExchange(req, res);
@@ -4818,9 +4923,11 @@ var DashboardApprovalChannel = class {
4818
4923
  } else if (method === "GET" && url.pathname === "/api/audit-log") {
4819
4924
  this.handleAuditLog(url, res);
4820
4925
  } else if (method === "POST" && url.pathname.startsWith("/api/approve/")) {
4926
+ if (!this.checkRateLimit(req, res, "decisions")) return;
4821
4927
  const id = url.pathname.slice("/api/approve/".length);
4822
4928
  this.handleDecision(id, "approve", res);
4823
4929
  } else if (method === "POST" && url.pathname.startsWith("/api/deny/")) {
4930
+ if (!this.checkRateLimit(req, res, "decisions")) return;
4824
4931
  const id = url.pathname.slice("/api/deny/".length);
4825
4932
  this.handleDecision(id, "deny", res);
4826
4933
  } else {
@@ -5563,14 +5670,14 @@ function generateSHR(identityId, opts) {
5563
5670
  code: "PROCESS_ISOLATION_ONLY",
5564
5671
  severity: "warning",
5565
5672
  description: "Process-level isolation only (no TEE)",
5566
- mitigation: "TEE support planned for v0.3.0"
5673
+ mitigation: "TEE support planned for a future release"
5567
5674
  });
5568
5675
  degradations.push({
5569
5676
  layer: "l2",
5570
5677
  code: "SELF_REPORTED_ATTESTATION",
5571
5678
  severity: "warning",
5572
5679
  description: "Attestation is self-reported (no hardware root of trust)",
5573
- mitigation: "TEE attestation planned for v0.3.0"
5680
+ mitigation: "TEE attestation planned for a future release"
5574
5681
  });
5575
5682
  }
5576
5683
  if (config.disclosure.proof_system === "commitment-only") {
@@ -5584,6 +5691,11 @@ function generateSHR(identityId, opts) {
5584
5691
  }
5585
5692
  const body = {
5586
5693
  shr_version: "1.0",
5694
+ implementation: {
5695
+ sanctuary_version: config.version,
5696
+ node_version: process.versions.node,
5697
+ generated_by: "sanctuary-mcp-server"
5698
+ },
5587
5699
  instance_id: identity.identity_id,
5588
5700
  generated_at: now.toISOString(),
5589
5701
  expires_at: expiresAt.toISOString(),
@@ -5714,6 +5826,245 @@ function assessSovereigntyLevel(body) {
5714
5826
  return "minimal";
5715
5827
  }
5716
5828
 
5829
+ // src/shr/gateway-adapter.ts
5830
+ var LAYER_WEIGHTS = {
5831
+ l1: 100,
5832
+ l2: 100,
5833
+ l3: 100,
5834
+ l4: 100
5835
+ };
5836
+ var DEGRADATION_IMPACT = {
5837
+ critical: 40,
5838
+ warning: 25,
5839
+ info: 10
5840
+ };
5841
+ function transformSHRForGateway(shr) {
5842
+ const { body, signed_by, signature } = shr;
5843
+ const layerScores = calculateLayerScores(body);
5844
+ const overallScore = calculateOverallScore(layerScores);
5845
+ const trustLevel = determineTrustLevel(overallScore);
5846
+ const signals = extractAuthorizationSignals(body);
5847
+ const degradations = transformDegradations(body.degradations);
5848
+ const constraints = generateAuthorizationConstraints(body);
5849
+ return {
5850
+ shr_version: body.shr_version,
5851
+ agent_identity: signed_by,
5852
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
5853
+ context_expires_at: body.expires_at,
5854
+ overall_score: overallScore,
5855
+ recommended_trust_level: trustLevel,
5856
+ layer_scores: {
5857
+ l1_cognitive: layerScores.l1,
5858
+ l2_operational: layerScores.l2,
5859
+ l3_disclosure: layerScores.l3,
5860
+ l4_reputation: layerScores.l4
5861
+ },
5862
+ layer_status: {
5863
+ l1_cognitive: body.layers.l1.status,
5864
+ l2_operational: body.layers.l2.status,
5865
+ l3_disclosure: body.layers.l3.status,
5866
+ l4_reputation: body.layers.l4.status
5867
+ },
5868
+ authorization_signals: signals,
5869
+ degradations,
5870
+ recommended_constraints: constraints,
5871
+ shr_signature: signature,
5872
+ shr_signed_by: signed_by
5873
+ };
5874
+ }
5875
+ function calculateLayerScores(body) {
5876
+ const layers = body.layers;
5877
+ const degradations = body.degradations;
5878
+ let l1Score = LAYER_WEIGHTS.l1;
5879
+ let l2Score = LAYER_WEIGHTS.l2;
5880
+ let l3Score = LAYER_WEIGHTS.l3;
5881
+ let l4Score = LAYER_WEIGHTS.l4;
5882
+ for (const deg of degradations) {
5883
+ const impact = DEGRADATION_IMPACT[deg.severity] || 10;
5884
+ if (deg.layer === "l1") {
5885
+ l1Score = Math.max(0, l1Score - impact);
5886
+ } else if (deg.layer === "l2") {
5887
+ l2Score = Math.max(0, l2Score - impact);
5888
+ } else if (deg.layer === "l3") {
5889
+ l3Score = Math.max(0, l3Score - impact);
5890
+ } else if (deg.layer === "l4") {
5891
+ l4Score = Math.max(0, l4Score - impact);
5892
+ }
5893
+ }
5894
+ if (layers.l1.status === "active" && l1Score > 50) l1Score = Math.min(100, l1Score + 5);
5895
+ if (layers.l2.status === "active" && l2Score > 50) l2Score = Math.min(100, l2Score + 5);
5896
+ if (layers.l3.status === "active" && l3Score > 50) l3Score = Math.min(100, l3Score + 5);
5897
+ if (layers.l4.status === "active" && l4Score > 50) l4Score = Math.min(100, l4Score + 5);
5898
+ if (layers.l1.status === "inactive") l1Score = 0;
5899
+ if (layers.l2.status === "inactive") l2Score = 0;
5900
+ if (layers.l3.status === "inactive") l3Score = 0;
5901
+ if (layers.l4.status === "inactive") l4Score = 0;
5902
+ return {
5903
+ l1: Math.round(l1Score),
5904
+ l2: Math.round(l2Score),
5905
+ l3: Math.round(l3Score),
5906
+ l4: Math.round(l4Score)
5907
+ };
5908
+ }
5909
+ function calculateOverallScore(layerScores) {
5910
+ const average = (layerScores.l1 + layerScores.l2 + layerScores.l3 + layerScores.l4) / 4;
5911
+ return Math.round(average);
5912
+ }
5913
+ function determineTrustLevel(score) {
5914
+ if (score >= 80) return "full";
5915
+ if (score >= 60) return "elevated";
5916
+ if (score >= 40) return "standard";
5917
+ return "restricted";
5918
+ }
5919
+ function extractAuthorizationSignals(body) {
5920
+ const l1 = body.layers.l1;
5921
+ const l3 = body.layers.l3;
5922
+ const l4 = body.layers.l4;
5923
+ return {
5924
+ approval_gate_active: body.capabilities.handshake,
5925
+ // Handshake implies human loop capability
5926
+ context_gating_active: body.capabilities.encrypted_channel,
5927
+ // Proxy for gating capability
5928
+ encryption_at_rest: l1.encryption !== "none" && l1.encryption !== "unencrypted",
5929
+ behavioral_baseline_active: false,
5930
+ // Would need explicit field in SHR v1.1
5931
+ identity_verified: l1.identity_type === "ed25519" || l1.identity_type !== "none",
5932
+ zero_knowledge_capable: l3.status === "active" && l3.proof_system !== "commitment-only",
5933
+ selective_disclosure_active: l3.selective_disclosure,
5934
+ reputation_portable: l4.reputation_portable,
5935
+ handshake_capable: body.capabilities.handshake
5936
+ };
5937
+ }
5938
+ function transformDegradations(degradations) {
5939
+ return degradations.map((deg) => {
5940
+ let authzImpact = "";
5941
+ if (deg.code === "NO_TEE") {
5942
+ authzImpact = "Restricted to read-only operations until TEE available";
5943
+ } else if (deg.code === "PROCESS_ISOLATION_ONLY") {
5944
+ authzImpact = "Requires additional identity verification";
5945
+ } else if (deg.code === "COMMITMENT_ONLY") {
5946
+ authzImpact = "Limited data sharing scope \u2014 no zero-knowledge proofs";
5947
+ } else if (deg.code === "NO_ZK_PROOFS") {
5948
+ authzImpact = "Cannot perform confidential disclosures";
5949
+ } else if (deg.code === "SELF_REPORTED_ATTESTATION") {
5950
+ authzImpact = "Attestation trust degraded \u2014 human verification recommended";
5951
+ } else if (deg.code === "NO_SELECTIVE_DISCLOSURE") {
5952
+ authzImpact = "Must share entire data context, cannot redact";
5953
+ } else if (deg.code === "BASIC_SYBIL_ONLY") {
5954
+ authzImpact = "Restrict to interactions with known agents only";
5955
+ } else {
5956
+ authzImpact = "Unknown authorization impact";
5957
+ }
5958
+ return {
5959
+ layer: deg.layer,
5960
+ code: deg.code,
5961
+ severity: deg.severity,
5962
+ description: deg.description,
5963
+ authorization_impact: authzImpact
5964
+ };
5965
+ });
5966
+ }
5967
+ function generateAuthorizationConstraints(body, _degradations) {
5968
+ const constraints = [];
5969
+ const layers = body.layers;
5970
+ if (layers.l1.status === "degraded" || layers.l1.key_custody !== "self") {
5971
+ constraints.push({
5972
+ type: "identity_verification_required",
5973
+ description: "Additional identity verification required for sensitive operations",
5974
+ rationale: "L1 is degraded or key custody is not self-managed",
5975
+ priority: "high"
5976
+ });
5977
+ }
5978
+ if (!layers.l1.state_portable) {
5979
+ constraints.push({
5980
+ type: "location_bound",
5981
+ description: "Agent state is not portable \u2014 restrict to home environment",
5982
+ rationale: "State cannot be safely migrated across boundaries",
5983
+ priority: "medium"
5984
+ });
5985
+ }
5986
+ if (layers.l2.status === "degraded" || layers.l2.isolation_type === "local-process") {
5987
+ constraints.push({
5988
+ type: "read_only",
5989
+ description: "Restrict to read-only operations until operational isolation improves",
5990
+ rationale: "L2 isolation is process-level only (no TEE)",
5991
+ priority: "high"
5992
+ });
5993
+ }
5994
+ if (!layers.l2.attestation_available) {
5995
+ constraints.push({
5996
+ type: "requires_approval",
5997
+ description: "Human approval required for writes and sensitive reads",
5998
+ rationale: "No attestation available \u2014 self-reported integrity only",
5999
+ priority: "high"
6000
+ });
6001
+ }
6002
+ if (layers.l3.status === "degraded" || !layers.l3.selective_disclosure) {
6003
+ constraints.push({
6004
+ type: "restricted_scope",
6005
+ description: "Limit data sharing to minimal required scope \u2014 no selective disclosure",
6006
+ rationale: "Agent cannot redact data or prove predicates without revealing all context",
6007
+ priority: "high"
6008
+ });
6009
+ }
6010
+ if (layers.l3.proof_system === "commitment-only") {
6011
+ constraints.push({
6012
+ type: "restricted_scope",
6013
+ description: "No zero-knowledge proofs available \u2014 entire state context may be visible",
6014
+ rationale: "Proof system is commitment-only (no ZK)",
6015
+ priority: "medium"
6016
+ });
6017
+ }
6018
+ if (layers.l4.status === "degraded") {
6019
+ constraints.push({
6020
+ type: "known_agents_only",
6021
+ description: "Restrict interactions to known, pre-approved agents",
6022
+ rationale: "Reputation layer is degraded",
6023
+ priority: "medium"
6024
+ });
6025
+ }
6026
+ if (!layers.l4.reputation_portable) {
6027
+ constraints.push({
6028
+ type: "location_bound",
6029
+ description: "Reputation is not portable \u2014 restrict to home environment",
6030
+ rationale: "Cannot present reputation to external parties",
6031
+ priority: "low"
6032
+ });
6033
+ }
6034
+ const layerScores = calculateLayerScores(body);
6035
+ const overallScore = calculateOverallScore(layerScores);
6036
+ if (overallScore < 40) {
6037
+ constraints.push({
6038
+ type: "restricted_scope",
6039
+ description: "Overall sovereignty score below threshold \u2014 restrict to non-sensitive operations",
6040
+ rationale: `Overall sovereignty score is ${overallScore}/100`,
6041
+ priority: "high"
6042
+ });
6043
+ }
6044
+ return constraints;
6045
+ }
6046
+ function transformSHRGeneric(shr) {
6047
+ const context = transformSHRForGateway(shr);
6048
+ return {
6049
+ agent_id: context.agent_identity,
6050
+ sovereignty_score: context.overall_score,
6051
+ trust_level: context.recommended_trust_level,
6052
+ layer_scores: {
6053
+ l1: context.layer_scores.l1_cognitive,
6054
+ l2: context.layer_scores.l2_operational,
6055
+ l3: context.layer_scores.l3_disclosure,
6056
+ l4: context.layer_scores.l4_reputation
6057
+ },
6058
+ capabilities: context.authorization_signals,
6059
+ constraints: context.recommended_constraints.map((c) => ({
6060
+ type: c.type,
6061
+ description: c.description
6062
+ })),
6063
+ expires_at: context.context_expires_at,
6064
+ signature: context.shr_signature
6065
+ };
6066
+ }
6067
+
5717
6068
  // src/shr/tools.ts
5718
6069
  function createSHRTools(config, identityManager, masterKey, auditLog) {
5719
6070
  const generatorOpts = {
@@ -5776,6 +6127,53 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
5776
6127
  );
5777
6128
  return toolResult(result);
5778
6129
  }
6130
+ },
6131
+ {
6132
+ name: "sanctuary/shr_gateway_export",
6133
+ 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.",
6134
+ inputSchema: {
6135
+ type: "object",
6136
+ properties: {
6137
+ format: {
6138
+ type: "string",
6139
+ enum: ["ping", "generic"],
6140
+ description: "Output format: 'ping' (Ping Identity Gateway format) or 'generic' (format-agnostic). Default: 'ping'."
6141
+ },
6142
+ identity_id: {
6143
+ type: "string",
6144
+ description: "Identity to sign the SHR with. Defaults to primary identity."
6145
+ },
6146
+ validity_minutes: {
6147
+ type: "number",
6148
+ description: "How long the SHR is valid (minutes). Default: 60."
6149
+ }
6150
+ }
6151
+ },
6152
+ handler: async (args) => {
6153
+ const format = args.format || "ping";
6154
+ const validityMs = args.validity_minutes ? args.validity_minutes * 60 * 1e3 : void 0;
6155
+ const shrResult = generateSHR(args.identity_id, {
6156
+ ...generatorOpts,
6157
+ validityMs
6158
+ });
6159
+ if (typeof shrResult === "string") {
6160
+ return toolResult({ error: shrResult });
6161
+ }
6162
+ let context;
6163
+ if (format === "generic") {
6164
+ context = transformSHRGeneric(shrResult);
6165
+ } else {
6166
+ context = transformSHRForGateway(shrResult);
6167
+ }
6168
+ auditLog.append(
6169
+ "l2",
6170
+ "shr_gateway_export",
6171
+ shrResult.body.instance_id,
6172
+ void 0,
6173
+ "success"
6174
+ );
6175
+ return toolResult(context);
6176
+ }
5779
6177
  }
5780
6178
  ];
5781
6179
  return { tools };
@@ -7054,9 +7452,11 @@ var L1_INTEGRITY_VERIFICATION = 8;
7054
7452
  var L1_STATE_PORTABLE = 7;
7055
7453
  var L2_THREE_TIER_GATE = 10;
7056
7454
  var L2_BINARY_GATE = 3;
7057
- var L2_ANOMALY_DETECTION = 7;
7058
- var L2_ENCRYPTED_AUDIT = 5;
7059
- var L2_TOOL_SANDBOXING = 3;
7455
+ var L2_ANOMALY_DETECTION = 5;
7456
+ var L2_ENCRYPTED_AUDIT = 4;
7457
+ var L2_TOOL_SANDBOXING = 2;
7458
+ var L2_CONTEXT_GATING = 4;
7459
+ var L2_PROCESS_HARDENING = 5;
7060
7460
  var L3_COMMITMENT_SCHEME = 8;
7061
7461
  var L3_ZK_PROOFS = 7;
7062
7462
  var L3_DISCLOSURE_POLICIES = 5;
@@ -7070,6 +7470,35 @@ var SEVERITY_ORDER = {
7070
7470
  medium: 2,
7071
7471
  low: 3
7072
7472
  };
7473
+ var INCIDENT_META_SEV1 = {
7474
+ id: "META-SEV1-2026",
7475
+ name: "Meta Sev 1: Unauthorized autonomous data exposure",
7476
+ date: "2026-03-18",
7477
+ description: "AI agent autonomously posted proprietary code, business strategies, and user datasets to an internal forum without human approval. Two-hour exposure window."
7478
+ };
7479
+ var INCIDENT_OPENCLAW_SANDBOX = {
7480
+ id: "OPENCLAW-CVE-2026",
7481
+ name: "OpenClaw sandbox escape via privilege inheritance",
7482
+ date: "2026-03-18",
7483
+ 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.",
7484
+ cves: [
7485
+ "CVE-2026-32048",
7486
+ "CVE-2026-32915",
7487
+ "CVE-2026-32918"
7488
+ ]
7489
+ };
7490
+ var INCIDENT_CONTEXT_LEAKAGE = {
7491
+ id: "CONTEXT-LEAK-CLASS",
7492
+ name: "Context leakage: Full state exposure to inference providers",
7493
+ date: "2026-03",
7494
+ 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."
7495
+ };
7496
+ var INCIDENT_CLAUDE_CODE_LEAK = {
7497
+ id: "CLAUDE-CODE-LEAK-2026",
7498
+ name: "Claude Code source leak: 512K lines exposed via npm source map",
7499
+ date: "2026-03-31",
7500
+ 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."
7501
+ };
7073
7502
  function analyzeSovereignty(env, config) {
7074
7503
  const l1 = assessL1(env, config);
7075
7504
  const l2 = assessL2(env);
@@ -7142,14 +7571,18 @@ function assessL2(env, _config) {
7142
7571
  let auditTrailEncrypted = false;
7143
7572
  let auditTrailExists = false;
7144
7573
  let toolSandboxing = "none";
7574
+ let contextGating = false;
7575
+ let processIsolationHardening = "none";
7145
7576
  if (sanctuaryActive) {
7146
7577
  approvalGate = "three-tier";
7147
7578
  behavioralAnomalyDetection = true;
7148
7579
  auditTrailEncrypted = true;
7149
7580
  auditTrailExists = true;
7581
+ contextGating = true;
7150
7582
  findings.push("Three-tier Principal Policy gate active");
7151
7583
  findings.push("Behavioral anomaly detection (BaselineTracker) enabled");
7152
7584
  findings.push("Encrypted audit trail active");
7585
+ findings.push("Context gating available (sanctuary/context_gate_set_policy)");
7153
7586
  }
7154
7587
  if (env.openclaw_detected && env.openclaw_config) {
7155
7588
  if (env.openclaw_config.require_approval_enabled) {
@@ -7167,6 +7600,7 @@ function assessL2(env, _config) {
7167
7600
  );
7168
7601
  }
7169
7602
  }
7603
+ processIsolationHardening = "none";
7170
7604
  const status = approvalGate === "three-tier" && auditTrailEncrypted ? "active" : approvalGate !== "none" || auditTrailExists ? "partial" : "inactive";
7171
7605
  return {
7172
7606
  status,
@@ -7175,6 +7609,8 @@ function assessL2(env, _config) {
7175
7609
  audit_trail_encrypted: auditTrailEncrypted,
7176
7610
  audit_trail_exists: auditTrailExists,
7177
7611
  tool_sandboxing: sanctuaryActive ? "policy-enforced" : toolSandboxing,
7612
+ context_gating: contextGating,
7613
+ process_isolation_hardening: processIsolationHardening,
7178
7614
  findings
7179
7615
  };
7180
7616
  }
@@ -7189,8 +7625,10 @@ function assessL3(env, _config) {
7189
7625
  zkProofs = true;
7190
7626
  selectiveDisclosurePolicy = true;
7191
7627
  findings.push("SHA-256 + Pedersen commitment schemes active");
7192
- findings.push("Schnorr ZK proofs and range proofs available");
7628
+ findings.push("Schnorr zero-knowledge proofs (Fiat-Shamir) enabled \u2014 genuine ZK proofs");
7629
+ findings.push("Range proofs (bit-decomposition + OR-proofs) enabled \u2014 genuine ZK proofs");
7193
7630
  findings.push("Selective disclosure policies configurable");
7631
+ findings.push("Non-interactive proofs with replay-resistant domain separation");
7194
7632
  }
7195
7633
  const status = commitmentScheme === "pedersen+sha256" && zkProofs ? "active" : commitmentScheme !== "none" ? "partial" : "inactive";
7196
7634
  return {
@@ -7242,6 +7680,9 @@ function scoreL2(l2) {
7242
7680
  if (l2.audit_trail_encrypted) score += L2_ENCRYPTED_AUDIT;
7243
7681
  if (l2.tool_sandboxing === "policy-enforced") score += L2_TOOL_SANDBOXING;
7244
7682
  else if (l2.tool_sandboxing === "basic") score += 1;
7683
+ if (l2.context_gating) score += L2_CONTEXT_GATING;
7684
+ if (l2.process_isolation_hardening === "hardened") score += L2_PROCESS_HARDENING;
7685
+ else if (l2.process_isolation_hardening === "basic") score += 2;
7245
7686
  return score;
7246
7687
  }
7247
7688
  function scoreL3(l3) {
@@ -7271,7 +7712,8 @@ function generateGaps(env, l1, l2, l3, l4) {
7271
7712
  title: "Agent memory stored in plaintext",
7272
7713
  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.",
7273
7714
  openclaw_relevance: "Stock OpenClaw stores all agent memory in plaintext files. There is no built-in encryption for agent state.",
7274
- 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."
7715
+ 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.",
7716
+ incident_class: INCIDENT_META_SEV1
7275
7717
  });
7276
7718
  }
7277
7719
  if (oc && oc.env_file_exposed) {
@@ -7304,7 +7746,8 @@ function generateGaps(env, l1, l2, l3, l4) {
7304
7746
  title: "Binary approval gate (no anomaly detection)",
7305
7747
  description: "Your approval gate provides binary approve/deny gating without behavioral anomaly detection. Routine operations require the same manual approval as sensitive ones.",
7306
7748
  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,
7307
- 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."
7749
+ 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.",
7750
+ incident_class: INCIDENT_META_SEV1
7308
7751
  });
7309
7752
  } else if (l2.approval_gate === "none") {
7310
7753
  gaps.push({
@@ -7314,7 +7757,8 @@ function generateGaps(env, l1, l2, l3, l4) {
7314
7757
  title: "No approval gate",
7315
7758
  description: "No approval gate is configured. All tool calls execute without oversight.",
7316
7759
  openclaw_relevance: null,
7317
- sanctuary_solution: "Sanctuary's Principal Policy evaluates every tool call before execution. Enable it to get three-tier approval gating with behavioral anomaly detection."
7760
+ sanctuary_solution: "Sanctuary's Principal Policy evaluates every tool call before execution. Enable it to get three-tier approval gating with behavioral anomaly detection.",
7761
+ incident_class: INCIDENT_META_SEV1
7318
7762
  });
7319
7763
  }
7320
7764
  if (l2.tool_sandboxing === "basic") {
@@ -7325,18 +7769,32 @@ function generateGaps(env, l1, l2, l3, l4) {
7325
7769
  title: "Basic tool sandboxing (no cryptographic attestation)",
7326
7770
  description: "Your tool sandbox enforces allow/deny lists but provides no cryptographic attestation of execution context.",
7327
7771
  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,
7328
- sanctuary_solution: "Sanctuary provides cryptographic execution attestation via sanctuary/exec_attest and policy-enforced sandboxing with encrypted audit trails."
7772
+ sanctuary_solution: "Sanctuary provides cryptographic execution attestation via sanctuary/exec_attest and policy-enforced sandboxing with encrypted audit trails.",
7773
+ incident_class: INCIDENT_OPENCLAW_SANDBOX
7329
7774
  });
7330
7775
  }
7331
- if (!l2.audit_trail_exists) {
7776
+ if (!l2.context_gating) {
7332
7777
  gaps.push({
7333
7778
  id: "GAP-L2-003",
7334
7779
  layer: "L2",
7335
7780
  severity: "high",
7781
+ title: "No context gating for outbound inference calls",
7782
+ 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.",
7783
+ 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,
7784
+ 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.",
7785
+ incident_class: INCIDENT_CONTEXT_LEAKAGE
7786
+ });
7787
+ }
7788
+ if (!l2.audit_trail_exists) {
7789
+ gaps.push({
7790
+ id: "GAP-L2-004",
7791
+ layer: "L2",
7792
+ severity: "high",
7336
7793
  title: "No audit trail",
7337
7794
  description: "No audit trail exists for tool call history. There is no record of what operations were executed, when, or by whom.",
7338
7795
  openclaw_relevance: null,
7339
- sanctuary_solution: "Sanctuary maintains an encrypted audit log of all operations, queryable via sanctuary/monitor_audit_log."
7796
+ sanctuary_solution: "Sanctuary maintains an encrypted audit log of all operations, queryable via sanctuary/monitor_audit_log.",
7797
+ incident_class: INCIDENT_CLAUDE_CODE_LEAK
7340
7798
  });
7341
7799
  }
7342
7800
  if (l3.commitment_scheme === "none") {
@@ -7345,9 +7803,10 @@ function generateGaps(env, l1, l2, l3, l4) {
7345
7803
  layer: "L3",
7346
7804
  severity: "high",
7347
7805
  title: "No selective disclosure capability",
7348
- description: "Your agent has no way to prove facts about its state without revealing the state itself. Every disclosure is all-or-nothing.",
7806
+ 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.",
7349
7807
  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,
7350
- 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."
7808
+ 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.",
7809
+ incident_class: INCIDENT_META_SEV1
7351
7810
  });
7352
7811
  }
7353
7812
  if (!l4.reputation_portable) {
@@ -7399,9 +7858,18 @@ function generateRecommendations(env, l1, l2, l3, l4) {
7399
7858
  impact: "high"
7400
7859
  });
7401
7860
  }
7402
- if (!l4.reputation_signed) {
7861
+ if (!l2.context_gating) {
7403
7862
  recs.push({
7404
7863
  priority: 5,
7864
+ action: "Configure context gating to control what flows to LLM providers",
7865
+ tool: "sanctuary/context_gate_set_policy",
7866
+ effort: "minutes",
7867
+ impact: "high"
7868
+ });
7869
+ }
7870
+ if (!l4.reputation_signed) {
7871
+ recs.push({
7872
+ priority: 6,
7405
7873
  action: "Start recording reputation attestations from completed interactions",
7406
7874
  tool: "sanctuary/reputation_record",
7407
7875
  effort: "minutes",
@@ -7410,7 +7878,7 @@ function generateRecommendations(env, l1, l2, l3, l4) {
7410
7878
  }
7411
7879
  if (!l3.selective_disclosure_policy) {
7412
7880
  recs.push({
7413
- priority: 6,
7881
+ priority: 7,
7414
7882
  action: "Configure selective disclosure policies for data sharing",
7415
7883
  tool: "sanctuary/disclosure_set_policy",
7416
7884
  effort: "hours",
@@ -7459,6 +7927,10 @@ function formatAuditReport(result) {
7459
7927
  `;
7460
7928
  report += ` \u2502 L2 Operational Isolation \u2502 ${padStatus(layers.l2_operational.status)} \u2502 ${padScore(l2Score, 25)} \u2502
7461
7929
  `;
7930
+ if (layers.l2_operational.context_gating) {
7931
+ report += ` \u2502 \u2514 Context Gating \u2502 ACTIVE \u2502 \u2502
7932
+ `;
7933
+ }
7462
7934
  report += ` \u2502 L3 Selective Disclosure \u2502 ${padStatus(layers.l3_selective_disclosure.status)} \u2502 ${padScore(l3Score, 20)} \u2502
7463
7935
  `;
7464
7936
  report += ` \u2502 L4 Verifiable Reputation \u2502 ${padStatus(layers.l4_reputation.status)} \u2502 ${padScore(l4Score, 20)} \u2502
@@ -7476,6 +7948,12 @@ function formatAuditReport(result) {
7476
7948
  const descLines = wordWrap(gap.description, 66);
7477
7949
  for (const line of descLines) {
7478
7950
  report += ` ${line}
7951
+ `;
7952
+ }
7953
+ if (gap.incident_class) {
7954
+ const ic = gap.incident_class;
7955
+ const cveStr = ic.cves?.length ? ` (${ic.cves.join(", ")})` : "";
7956
+ report += ` \u2192 Incident precedent: ${ic.name}${cveStr} [${ic.date}]
7479
7957
  `;
7480
7958
  }
7481
7959
  report += ` \u2192 Fix: ${gap.sanctuary_solution.split(".")[0]}.
@@ -7570,76 +8048,1552 @@ function createAuditTools(config) {
7570
8048
  return { tools };
7571
8049
  }
7572
8050
 
7573
- // src/index.ts
8051
+ // src/l2-operational/context-gate.ts
7574
8052
  init_encoding();
7575
-
7576
- // src/storage/memory.ts
7577
- var MemoryStorage = class {
7578
- store = /* @__PURE__ */ new Map();
7579
- storageKey(namespace, key) {
7580
- return `${namespace}/${key}`;
8053
+ init_hashing();
8054
+ var MAX_CONTEXT_FIELDS = 1e3;
8055
+ var MAX_POLICY_RULES = 50;
8056
+ var MAX_PATTERNS_PER_ARRAY = 500;
8057
+ function evaluateField(policy, provider, field) {
8058
+ const exactRule = policy.rules.find((r) => r.provider === provider);
8059
+ const wildcardRule = policy.rules.find((r) => r.provider === "*");
8060
+ const matchedRule = exactRule ?? wildcardRule;
8061
+ if (!matchedRule) {
8062
+ return {
8063
+ field,
8064
+ action: policy.default_action === "deny" ? "deny" : "redact",
8065
+ reason: `No rule matches provider "${provider}"; applying default (${policy.default_action})`
8066
+ };
7581
8067
  }
7582
- async write(namespace, key, data) {
7583
- this.store.set(this.storageKey(namespace, key), {
7584
- data: new Uint8Array(data),
7585
- // Copy to prevent external mutation
7586
- modified_at: (/* @__PURE__ */ new Date()).toISOString()
7587
- });
8068
+ if (matchesPattern(field, matchedRule.redact)) {
8069
+ return {
8070
+ field,
8071
+ action: "redact",
8072
+ reason: `Field "${field}" is explicitly redacted for ${matchedRule.provider} provider`
8073
+ };
7588
8074
  }
7589
- async read(namespace, key) {
7590
- const entry = this.store.get(this.storageKey(namespace, key));
7591
- if (!entry) return null;
7592
- return new Uint8Array(entry.data);
8075
+ if (matchesPattern(field, matchedRule.hash)) {
8076
+ return {
8077
+ field,
8078
+ action: "hash",
8079
+ reason: `Field "${field}" is hashed for ${matchedRule.provider} provider`
8080
+ };
7593
8081
  }
7594
- async delete(namespace, key, _secureOverwrite) {
7595
- return this.store.delete(this.storageKey(namespace, key));
8082
+ if (matchesPattern(field, matchedRule.summarize)) {
8083
+ return {
8084
+ field,
8085
+ action: "summarize",
8086
+ reason: `Field "${field}" should be summarized for ${matchedRule.provider} provider`
8087
+ };
7596
8088
  }
7597
- async list(namespace, prefix) {
7598
- const entries = [];
7599
- const nsPrefix = `${namespace}/`;
7600
- for (const [storeKey, entry] of this.store) {
7601
- if (!storeKey.startsWith(nsPrefix)) continue;
7602
- const key = storeKey.slice(nsPrefix.length);
7603
- if (prefix && !key.startsWith(prefix)) continue;
7604
- entries.push({
7605
- key,
7606
- namespace,
7607
- size_bytes: entry.data.length,
7608
- modified_at: entry.modified_at
7609
- });
7610
- }
7611
- return entries.sort((a, b) => a.key.localeCompare(b.key));
8089
+ if (matchesPattern(field, matchedRule.allow)) {
8090
+ return {
8091
+ field,
8092
+ action: "allow",
8093
+ reason: `Field "${field}" is allowed for ${matchedRule.provider} provider`
8094
+ };
7612
8095
  }
7613
- async exists(namespace, key) {
7614
- return this.store.has(this.storageKey(namespace, key));
8096
+ return {
8097
+ field,
8098
+ action: policy.default_action === "deny" ? "deny" : "redact",
8099
+ reason: `Field "${field}" not addressed in ${matchedRule.provider} rule; applying default (${policy.default_action})`
8100
+ };
8101
+ }
8102
+ function filterContext(policy, provider, context) {
8103
+ const fields = Object.keys(context);
8104
+ if (fields.length > MAX_CONTEXT_FIELDS) {
8105
+ throw new Error(
8106
+ `Context object has ${fields.length} fields, exceeding limit of ${MAX_CONTEXT_FIELDS}`
8107
+ );
7615
8108
  }
7616
- async totalSize() {
7617
- let total = 0;
7618
- for (const entry of this.store.values()) {
7619
- total += entry.data.length;
8109
+ const decisions = [];
8110
+ let allowed = 0;
8111
+ let redacted = 0;
8112
+ let hashed = 0;
8113
+ let summarized = 0;
8114
+ let denied = 0;
8115
+ for (const field of fields) {
8116
+ const result = evaluateField(policy, provider, field);
8117
+ if (result.action === "hash") {
8118
+ const value = typeof context[field] === "string" ? context[field] : JSON.stringify(context[field]);
8119
+ result.hash_value = hashToString(stringToBytes(value));
8120
+ }
8121
+ decisions.push(result);
8122
+ switch (result.action) {
8123
+ case "allow":
8124
+ allowed++;
8125
+ break;
8126
+ case "redact":
8127
+ redacted++;
8128
+ break;
8129
+ case "hash":
8130
+ hashed++;
8131
+ break;
8132
+ case "summarize":
8133
+ summarized++;
8134
+ break;
8135
+ case "deny":
8136
+ denied++;
8137
+ break;
7620
8138
  }
7621
- return total;
7622
8139
  }
7623
- /** Clear all stored data (useful in tests) */
7624
- clear() {
7625
- this.store.clear();
8140
+ const originalHash = hashToString(
8141
+ stringToBytes(JSON.stringify(context))
8142
+ );
8143
+ const filteredOutput = {};
8144
+ for (const decision of decisions) {
8145
+ switch (decision.action) {
8146
+ case "allow":
8147
+ filteredOutput[decision.field] = context[decision.field];
8148
+ break;
8149
+ case "redact":
8150
+ filteredOutput[decision.field] = "[REDACTED]";
8151
+ break;
8152
+ case "hash":
8153
+ filteredOutput[decision.field] = `[HASH:${decision.hash_value}]`;
8154
+ break;
8155
+ case "summarize":
8156
+ filteredOutput[decision.field] = "[SUMMARIZE]";
8157
+ break;
8158
+ }
7626
8159
  }
7627
- };
7628
-
7629
- // src/index.ts
7630
- async function createSanctuaryServer(options) {
7631
- const config = await loadConfig(options?.configPath);
7632
- await mkdir(config.storage_path, { recursive: true, mode: 448 });
7633
- const storage = options?.storage ?? new FilesystemStorage(
7634
- `${config.storage_path}/state`
8160
+ const filteredHash = hashToString(
8161
+ stringToBytes(JSON.stringify(filteredOutput))
7635
8162
  );
7636
- let masterKey;
7637
- let keyProtection;
7638
- let recoveryKey;
7639
- const passphrase = options?.passphrase ?? process.env.SANCTUARY_PASSPHRASE;
7640
- if (passphrase) {
7641
- keyProtection = "passphrase";
7642
- let existingParams;
8163
+ return {
8164
+ policy_id: policy.policy_id,
8165
+ provider,
8166
+ fields_allowed: allowed,
8167
+ fields_redacted: redacted,
8168
+ fields_hashed: hashed,
8169
+ fields_summarized: summarized,
8170
+ fields_denied: denied,
8171
+ decisions,
8172
+ original_context_hash: originalHash,
8173
+ filtered_context_hash: filteredHash,
8174
+ filtered_at: (/* @__PURE__ */ new Date()).toISOString()
8175
+ };
8176
+ }
8177
+ function matchesPattern(field, patterns) {
8178
+ const normalizedField = field.toLowerCase();
8179
+ for (const pattern of patterns) {
8180
+ if (pattern === "*") return true;
8181
+ const normalizedPattern = pattern.toLowerCase();
8182
+ if (normalizedPattern === normalizedField) return true;
8183
+ if (normalizedPattern.endsWith("*") && normalizedField.startsWith(normalizedPattern.slice(0, -1))) return true;
8184
+ if (normalizedPattern.startsWith("*") && normalizedField.endsWith(normalizedPattern.slice(1))) return true;
8185
+ }
8186
+ return false;
8187
+ }
8188
+ var ContextGatePolicyStore = class {
8189
+ storage;
8190
+ encryptionKey;
8191
+ policies = /* @__PURE__ */ new Map();
8192
+ constructor(storage, masterKey) {
8193
+ this.storage = storage;
8194
+ this.encryptionKey = derivePurposeKey(masterKey, "l2-context-gate");
8195
+ }
8196
+ /**
8197
+ * Create and store a new context-gating policy.
8198
+ */
8199
+ async create(policyName, rules, defaultAction, identityId) {
8200
+ const policyId = `cg-${Date.now()}-${toBase64url(randomBytes(8))}`;
8201
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8202
+ const policy = {
8203
+ policy_id: policyId,
8204
+ policy_name: policyName,
8205
+ rules,
8206
+ default_action: defaultAction,
8207
+ identity_id: identityId,
8208
+ created_at: now,
8209
+ updated_at: now
8210
+ };
8211
+ await this.persist(policy);
8212
+ this.policies.set(policyId, policy);
8213
+ return policy;
8214
+ }
8215
+ /**
8216
+ * Get a policy by ID.
8217
+ */
8218
+ async get(policyId) {
8219
+ if (this.policies.has(policyId)) {
8220
+ return this.policies.get(policyId);
8221
+ }
8222
+ const raw = await this.storage.read("_context_gate_policies", policyId);
8223
+ if (!raw) return null;
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(policyId, policy);
8229
+ return policy;
8230
+ } catch {
8231
+ return null;
8232
+ }
8233
+ }
8234
+ /**
8235
+ * List all context-gating policies.
8236
+ */
8237
+ async list() {
8238
+ await this.loadAll();
8239
+ return Array.from(this.policies.values());
8240
+ }
8241
+ /**
8242
+ * Load all persisted policies into memory.
8243
+ */
8244
+ async loadAll() {
8245
+ try {
8246
+ const entries = await this.storage.list("_context_gate_policies");
8247
+ for (const meta of entries) {
8248
+ if (this.policies.has(meta.key)) continue;
8249
+ const raw = await this.storage.read("_context_gate_policies", meta.key);
8250
+ if (!raw) continue;
8251
+ try {
8252
+ const encrypted = JSON.parse(bytesToString(raw));
8253
+ const decrypted = decrypt(encrypted, this.encryptionKey);
8254
+ const policy = JSON.parse(bytesToString(decrypted));
8255
+ this.policies.set(policy.policy_id, policy);
8256
+ } catch {
8257
+ }
8258
+ }
8259
+ } catch {
8260
+ }
8261
+ }
8262
+ async persist(policy) {
8263
+ const serialized = stringToBytes(JSON.stringify(policy));
8264
+ const encrypted = encrypt(serialized, this.encryptionKey);
8265
+ await this.storage.write(
8266
+ "_context_gate_policies",
8267
+ policy.policy_id,
8268
+ stringToBytes(JSON.stringify(encrypted))
8269
+ );
8270
+ }
8271
+ };
8272
+
8273
+ // src/l2-operational/context-gate-templates.ts
8274
+ var ALWAYS_REDACT_SECRETS = [
8275
+ "api_key",
8276
+ "secret_*",
8277
+ "*_secret",
8278
+ "*_token",
8279
+ "*_key",
8280
+ "password",
8281
+ "*_password",
8282
+ "credential",
8283
+ "*_credential",
8284
+ "private_key",
8285
+ "recovery_key",
8286
+ "passphrase",
8287
+ "auth_*"
8288
+ ];
8289
+ var PII_PATTERNS = [
8290
+ "*_pii",
8291
+ "name",
8292
+ "full_name",
8293
+ "email",
8294
+ "email_address",
8295
+ "phone",
8296
+ "phone_number",
8297
+ "address",
8298
+ "ssn",
8299
+ "date_of_birth",
8300
+ "ip_address",
8301
+ "credit_card",
8302
+ "card_number",
8303
+ "cvv",
8304
+ "bank_account",
8305
+ "account_number",
8306
+ "routing_number"
8307
+ ];
8308
+ var INTERNAL_STATE_PATTERNS = [
8309
+ "memory",
8310
+ "agent_memory",
8311
+ "internal_reasoning",
8312
+ "internal_state",
8313
+ "reasoning_trace",
8314
+ "chain_of_thought",
8315
+ "private_notes",
8316
+ "soul",
8317
+ "personality",
8318
+ "system_prompt"
8319
+ ];
8320
+ var ID_PATTERNS = [
8321
+ "user_id",
8322
+ "session_id",
8323
+ "agent_id",
8324
+ "identity_id",
8325
+ "conversation_id",
8326
+ "thread_id"
8327
+ ];
8328
+ var HISTORY_PATTERNS = [
8329
+ "conversation_history",
8330
+ "message_history",
8331
+ "chat_history",
8332
+ "context_window",
8333
+ "previous_messages"
8334
+ ];
8335
+ var INFERENCE_MINIMAL = {
8336
+ id: "inference-minimal",
8337
+ name: "Inference Minimal",
8338
+ description: "Maximum privacy. Only the current task and query reach the LLM provider.",
8339
+ use_when: "You want the strictest possible context control for inference calls. The LLM sees only what it needs for the immediate task.",
8340
+ rules: [
8341
+ {
8342
+ provider: "inference",
8343
+ allow: [
8344
+ "task",
8345
+ "task_description",
8346
+ "current_query",
8347
+ "query",
8348
+ "prompt",
8349
+ "question",
8350
+ "instruction"
8351
+ ],
8352
+ redact: [
8353
+ ...ALWAYS_REDACT_SECRETS,
8354
+ ...PII_PATTERNS,
8355
+ ...INTERNAL_STATE_PATTERNS,
8356
+ ...HISTORY_PATTERNS,
8357
+ "tool_results",
8358
+ "previous_results"
8359
+ ],
8360
+ hash: [...ID_PATTERNS],
8361
+ summarize: []
8362
+ }
8363
+ ],
8364
+ default_action: "redact"
8365
+ };
8366
+ var INFERENCE_STANDARD = {
8367
+ id: "inference-standard",
8368
+ name: "Inference Standard",
8369
+ description: "Balanced privacy. Task, query, and tool results pass through. History flagged for summarization. Secrets and PII redacted.",
8370
+ use_when: "You need the LLM to have enough context for multi-step tasks while keeping secrets, PII, and internal reasoning private.",
8371
+ rules: [
8372
+ {
8373
+ provider: "inference",
8374
+ allow: [
8375
+ "task",
8376
+ "task_description",
8377
+ "current_query",
8378
+ "query",
8379
+ "prompt",
8380
+ "question",
8381
+ "instruction",
8382
+ "tool_results",
8383
+ "tool_output",
8384
+ "previous_results",
8385
+ "current_step",
8386
+ "remaining_steps",
8387
+ "objective",
8388
+ "constraints",
8389
+ "format",
8390
+ "output_format"
8391
+ ],
8392
+ redact: [
8393
+ ...ALWAYS_REDACT_SECRETS,
8394
+ ...PII_PATTERNS,
8395
+ ...INTERNAL_STATE_PATTERNS
8396
+ ],
8397
+ hash: [...ID_PATTERNS],
8398
+ summarize: [...HISTORY_PATTERNS]
8399
+ }
8400
+ ],
8401
+ default_action: "redact"
8402
+ };
8403
+ var LOGGING_STRICT = {
8404
+ id: "logging-strict",
8405
+ name: "Logging Strict",
8406
+ description: "Redacts all content for logging and analytics providers. Only operation metadata passes through.",
8407
+ use_when: "You send telemetry to logging or analytics services and want usage metrics without any content exposure.",
8408
+ rules: [
8409
+ {
8410
+ provider: "logging",
8411
+ allow: [
8412
+ "operation",
8413
+ "operation_name",
8414
+ "tool_name",
8415
+ "timestamp",
8416
+ "duration_ms",
8417
+ "status",
8418
+ "error_code",
8419
+ "event_type"
8420
+ ],
8421
+ redact: [
8422
+ ...ALWAYS_REDACT_SECRETS,
8423
+ ...PII_PATTERNS,
8424
+ ...INTERNAL_STATE_PATTERNS,
8425
+ ...HISTORY_PATTERNS
8426
+ ],
8427
+ hash: [...ID_PATTERNS],
8428
+ summarize: []
8429
+ },
8430
+ {
8431
+ provider: "analytics",
8432
+ allow: [
8433
+ "event_type",
8434
+ "timestamp",
8435
+ "duration_ms",
8436
+ "status",
8437
+ "tool_name"
8438
+ ],
8439
+ redact: [
8440
+ ...ALWAYS_REDACT_SECRETS,
8441
+ ...PII_PATTERNS,
8442
+ ...INTERNAL_STATE_PATTERNS,
8443
+ ...HISTORY_PATTERNS
8444
+ ],
8445
+ hash: [...ID_PATTERNS],
8446
+ summarize: []
8447
+ }
8448
+ ],
8449
+ default_action: "redact"
8450
+ };
8451
+ var TOOL_API_SCOPED = {
8452
+ id: "tool-api-scoped",
8453
+ name: "Tool API Scoped",
8454
+ description: "Allows tool-specific parameters for external API calls. Redacts memory, history, secrets, and PII.",
8455
+ 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.",
8456
+ rules: [
8457
+ {
8458
+ provider: "tool-api",
8459
+ allow: [
8460
+ "task",
8461
+ "task_description",
8462
+ "query",
8463
+ "search_query",
8464
+ "tool_input",
8465
+ "tool_parameters",
8466
+ "url",
8467
+ "endpoint",
8468
+ "method",
8469
+ "filter",
8470
+ "sort",
8471
+ "limit",
8472
+ "offset"
8473
+ ],
8474
+ redact: [
8475
+ ...ALWAYS_REDACT_SECRETS,
8476
+ ...PII_PATTERNS,
8477
+ ...INTERNAL_STATE_PATTERNS,
8478
+ ...HISTORY_PATTERNS
8479
+ ],
8480
+ hash: [...ID_PATTERNS],
8481
+ summarize: []
8482
+ }
8483
+ ],
8484
+ default_action: "redact"
8485
+ };
8486
+ var TEMPLATES = {
8487
+ "inference-minimal": INFERENCE_MINIMAL,
8488
+ "inference-standard": INFERENCE_STANDARD,
8489
+ "logging-strict": LOGGING_STRICT,
8490
+ "tool-api-scoped": TOOL_API_SCOPED
8491
+ };
8492
+ function listTemplateIds() {
8493
+ return Object.keys(TEMPLATES);
8494
+ }
8495
+ function getTemplate(id) {
8496
+ return TEMPLATES[id];
8497
+ }
8498
+
8499
+ // src/l2-operational/context-gate-recommend.ts
8500
+ var CLASSIFICATION_RULES = [
8501
+ // ── Secrets (always redact, high confidence) ─────────────────────
8502
+ {
8503
+ patterns: [
8504
+ "api_key",
8505
+ "apikey",
8506
+ "api_secret",
8507
+ "secret",
8508
+ "secret_key",
8509
+ "secret_token",
8510
+ "password",
8511
+ "passwd",
8512
+ "pass",
8513
+ "credential",
8514
+ "credentials",
8515
+ "private_key",
8516
+ "privkey",
8517
+ "recovery_key",
8518
+ "passphrase",
8519
+ "token",
8520
+ "access_token",
8521
+ "refresh_token",
8522
+ "bearer_token",
8523
+ "auth_token",
8524
+ "auth_header",
8525
+ "authorization",
8526
+ "encryption_key",
8527
+ "master_key",
8528
+ "signing_key",
8529
+ "webhook_secret",
8530
+ "client_secret",
8531
+ "connection_string"
8532
+ ],
8533
+ action: "redact",
8534
+ confidence: "high",
8535
+ reason: "Matches known secret/credential pattern"
8536
+ },
8537
+ // ── PII (always redact, high confidence) ─────────────────────────
8538
+ {
8539
+ patterns: [
8540
+ "name",
8541
+ "full_name",
8542
+ "first_name",
8543
+ "last_name",
8544
+ "display_name",
8545
+ "email",
8546
+ "email_address",
8547
+ "phone",
8548
+ "phone_number",
8549
+ "mobile",
8550
+ "address",
8551
+ "street_address",
8552
+ "mailing_address",
8553
+ "ssn",
8554
+ "social_security",
8555
+ "date_of_birth",
8556
+ "dob",
8557
+ "birthday",
8558
+ "ip_address",
8559
+ "ip",
8560
+ "location",
8561
+ "geolocation",
8562
+ "coordinates",
8563
+ "credit_card",
8564
+ "card_number",
8565
+ "cvv",
8566
+ "bank_account",
8567
+ "routing_number",
8568
+ "passport",
8569
+ "drivers_license",
8570
+ "license_number"
8571
+ ],
8572
+ action: "redact",
8573
+ confidence: "high",
8574
+ reason: "Matches known PII pattern"
8575
+ },
8576
+ // ── Internal agent state (redact, high confidence) ───────────────
8577
+ {
8578
+ patterns: [
8579
+ "memory",
8580
+ "agent_memory",
8581
+ "long_term_memory",
8582
+ "internal_reasoning",
8583
+ "reasoning_trace",
8584
+ "chain_of_thought",
8585
+ "internal_state",
8586
+ "agent_state",
8587
+ "private_notes",
8588
+ "scratchpad",
8589
+ "soul",
8590
+ "personality",
8591
+ "persona",
8592
+ "system_prompt",
8593
+ "system_message",
8594
+ "system_instruction",
8595
+ "preferences",
8596
+ "user_preferences",
8597
+ "agent_preferences",
8598
+ "beliefs",
8599
+ "goals",
8600
+ "motivations"
8601
+ ],
8602
+ action: "redact",
8603
+ confidence: "high",
8604
+ reason: "Matches known internal agent state pattern"
8605
+ },
8606
+ // ── IDs (hash, medium confidence) ────────────────────────────────
8607
+ {
8608
+ patterns: [
8609
+ "user_id",
8610
+ "userid",
8611
+ "session_id",
8612
+ "sessionid",
8613
+ "agent_id",
8614
+ "agentid",
8615
+ "identity_id",
8616
+ "conversation_id",
8617
+ "thread_id",
8618
+ "threadid",
8619
+ "request_id",
8620
+ "requestid",
8621
+ "correlation_id",
8622
+ "trace_id",
8623
+ "traceid",
8624
+ "account_id",
8625
+ "accountid"
8626
+ ],
8627
+ action: "hash",
8628
+ confidence: "medium",
8629
+ reason: "Matches known identifier pattern \u2014 hash preserves correlation without exposing value"
8630
+ },
8631
+ // ── History (summarize, medium confidence) ───────────────────────
8632
+ {
8633
+ patterns: [
8634
+ "conversation_history",
8635
+ "chat_history",
8636
+ "message_history",
8637
+ "messages",
8638
+ "previous_messages",
8639
+ "prior_messages",
8640
+ "context_window",
8641
+ "interaction_history",
8642
+ "audit_log",
8643
+ "event_log"
8644
+ ],
8645
+ action: "summarize",
8646
+ confidence: "medium",
8647
+ reason: "Matches known history/log pattern \u2014 summarize to reduce exposure"
8648
+ },
8649
+ // ── Task/query (allow, medium confidence) ────────────────────────
8650
+ {
8651
+ patterns: [
8652
+ "task",
8653
+ "task_description",
8654
+ "query",
8655
+ "current_query",
8656
+ "search_query",
8657
+ "prompt",
8658
+ "user_prompt",
8659
+ "question",
8660
+ "current_question",
8661
+ "instruction",
8662
+ "instructions",
8663
+ "objective",
8664
+ "goal",
8665
+ "current_step",
8666
+ "next_step",
8667
+ "remaining_steps",
8668
+ "constraints",
8669
+ "requirements",
8670
+ "output_format",
8671
+ "format",
8672
+ "tool_results",
8673
+ "tool_output",
8674
+ "tool_input",
8675
+ "tool_parameters"
8676
+ ],
8677
+ action: "allow",
8678
+ confidence: "medium",
8679
+ reason: "Matches known task/query pattern \u2014 likely needed for inference"
8680
+ }
8681
+ ];
8682
+ function classifyField(fieldName) {
8683
+ const normalized = fieldName.toLowerCase().trim();
8684
+ for (const rule of CLASSIFICATION_RULES) {
8685
+ for (const pattern of rule.patterns) {
8686
+ if (matchesFieldPattern(normalized, pattern)) {
8687
+ return {
8688
+ field: fieldName,
8689
+ recommended_action: rule.action,
8690
+ reason: rule.reason,
8691
+ confidence: rule.confidence,
8692
+ matched_pattern: pattern
8693
+ };
8694
+ }
8695
+ }
8696
+ }
8697
+ return {
8698
+ field: fieldName,
8699
+ recommended_action: "redact",
8700
+ reason: "No known pattern matched \u2014 defaulting to redact (conservative)",
8701
+ confidence: "low",
8702
+ matched_pattern: null
8703
+ };
8704
+ }
8705
+ function recommendPolicy(context, provider = "inference") {
8706
+ const fields = Object.keys(context);
8707
+ const classifications = fields.map(classifyField);
8708
+ const warnings = [];
8709
+ const allow = [];
8710
+ const redact = [];
8711
+ const hash2 = [];
8712
+ const summarize = [];
8713
+ for (const c of classifications) {
8714
+ switch (c.recommended_action) {
8715
+ case "allow":
8716
+ allow.push(c.field);
8717
+ break;
8718
+ case "redact":
8719
+ redact.push(c.field);
8720
+ break;
8721
+ case "hash":
8722
+ hash2.push(c.field);
8723
+ break;
8724
+ case "summarize":
8725
+ summarize.push(c.field);
8726
+ break;
8727
+ }
8728
+ }
8729
+ const lowConfidence = classifications.filter((c) => c.confidence === "low");
8730
+ if (lowConfidence.length > 0) {
8731
+ warnings.push(
8732
+ `${lowConfidence.length} field(s) could not be classified by pattern and will default to redact: ${lowConfidence.map((c) => c.field).join(", ")}. Review these manually.`
8733
+ );
8734
+ }
8735
+ for (const [key, value] of Object.entries(context)) {
8736
+ if (typeof value === "string" && value.length > 5e3) {
8737
+ const existing = classifications.find((c) => c.field === key);
8738
+ if (existing && existing.recommended_action === "allow") {
8739
+ warnings.push(
8740
+ `Field "${key}" is allowed but contains ${value.length} characters. Consider summarizing it to reduce context size and exposure.`
8741
+ );
8742
+ }
8743
+ }
8744
+ }
8745
+ return {
8746
+ provider,
8747
+ classifications,
8748
+ recommended_rules: { allow, redact, hash: hash2, summarize },
8749
+ default_action: "redact",
8750
+ summary: {
8751
+ total_fields: fields.length,
8752
+ allow: allow.length,
8753
+ redact: redact.length,
8754
+ hash: hash2.length,
8755
+ summarize: summarize.length
8756
+ },
8757
+ warnings
8758
+ };
8759
+ }
8760
+ function matchesFieldPattern(normalizedField, pattern) {
8761
+ if (normalizedField === pattern) return true;
8762
+ if (pattern.length >= 3 && normalizedField.includes(pattern)) {
8763
+ const idx = normalizedField.indexOf(pattern);
8764
+ const before = idx === 0 || normalizedField[idx - 1] === "_" || normalizedField[idx - 1] === "-";
8765
+ const after = idx + pattern.length === normalizedField.length || normalizedField[idx + pattern.length] === "_" || normalizedField[idx + pattern.length] === "-";
8766
+ return before && after;
8767
+ }
8768
+ return false;
8769
+ }
8770
+
8771
+ // src/l2-operational/context-gate-tools.ts
8772
+ function createContextGateTools(storage, masterKey, auditLog) {
8773
+ const policyStore = new ContextGatePolicyStore(storage, masterKey);
8774
+ const tools = [
8775
+ // ── Set Policy ──────────────────────────────────────────────────
8776
+ {
8777
+ name: "sanctuary/context_gate_set_policy",
8778
+ 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.",
8779
+ inputSchema: {
8780
+ type: "object",
8781
+ properties: {
8782
+ policy_name: {
8783
+ type: "string",
8784
+ description: "Human-readable name for this policy (e.g., 'inference-minimal', 'tool-api-strict')"
8785
+ },
8786
+ rules: {
8787
+ type: "array",
8788
+ 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).",
8789
+ items: {
8790
+ type: "object",
8791
+ properties: {
8792
+ provider: {
8793
+ type: "string",
8794
+ description: "Provider category: inference, tool-api, logging, analytics, peer-agent, custom, or * for all"
8795
+ },
8796
+ allow: {
8797
+ type: "array",
8798
+ items: { type: "string" },
8799
+ description: "Fields/patterns to allow through (e.g., 'task_description', 'current_query', 'tool_*')"
8800
+ },
8801
+ redact: {
8802
+ type: "array",
8803
+ items: { type: "string" },
8804
+ description: "Fields/patterns to redact (e.g., 'conversation_history', 'secret_*', '*_pii'). Takes absolute priority."
8805
+ },
8806
+ hash: {
8807
+ type: "array",
8808
+ items: { type: "string" },
8809
+ description: "Fields/patterns to replace with SHA-256 hash (e.g., 'user_id', 'session_id')"
8810
+ },
8811
+ summarize: {
8812
+ type: "array",
8813
+ items: { type: "string" },
8814
+ description: "Fields/patterns to flag for summarization (advisory \u2014 agent should compress these before sending)"
8815
+ }
8816
+ },
8817
+ required: ["provider", "allow", "redact"]
8818
+ }
8819
+ },
8820
+ default_action: {
8821
+ type: "string",
8822
+ enum: ["redact", "deny"],
8823
+ description: "Action for fields not matched by any rule. 'redact' removes the field value; 'deny' blocks the entire request. Default: 'redact'."
8824
+ },
8825
+ identity_id: {
8826
+ type: "string",
8827
+ description: "Bind this policy to a specific identity (optional)"
8828
+ }
8829
+ },
8830
+ required: ["policy_name", "rules"]
8831
+ },
8832
+ handler: async (args) => {
8833
+ const policyName = args.policy_name;
8834
+ const rawRules = args.rules;
8835
+ const defaultAction = args.default_action ?? "redact";
8836
+ const identityId = args.identity_id;
8837
+ if (!Array.isArray(rawRules)) {
8838
+ return toolResult({ error: "invalid_rules", message: "rules must be an array" });
8839
+ }
8840
+ if (rawRules.length > MAX_POLICY_RULES) {
8841
+ return toolResult({
8842
+ error: "too_many_rules",
8843
+ message: `Policy has ${rawRules.length} rules, exceeding limit of ${MAX_POLICY_RULES}`
8844
+ });
8845
+ }
8846
+ const rules = [];
8847
+ for (const r of rawRules) {
8848
+ const allow = Array.isArray(r.allow) ? r.allow : [];
8849
+ const redact = Array.isArray(r.redact) ? r.redact : [];
8850
+ const hash2 = Array.isArray(r.hash) ? r.hash : [];
8851
+ const summarize = Array.isArray(r.summarize) ? r.summarize : [];
8852
+ for (const [name, arr] of [["allow", allow], ["redact", redact], ["hash", hash2], ["summarize", summarize]]) {
8853
+ if (arr.length > MAX_PATTERNS_PER_ARRAY) {
8854
+ return toolResult({
8855
+ error: "too_many_patterns",
8856
+ message: `Rule ${name} array has ${arr.length} patterns, exceeding limit of ${MAX_PATTERNS_PER_ARRAY}`
8857
+ });
8858
+ }
8859
+ }
8860
+ rules.push({
8861
+ provider: r.provider ?? "*",
8862
+ allow,
8863
+ redact,
8864
+ hash: hash2,
8865
+ summarize
8866
+ });
8867
+ }
8868
+ const policy = await policyStore.create(
8869
+ policyName,
8870
+ rules,
8871
+ defaultAction,
8872
+ identityId
8873
+ );
8874
+ auditLog.append("l2", "context_gate_set_policy", identityId ?? "system", {
8875
+ policy_id: policy.policy_id,
8876
+ policy_name: policyName,
8877
+ rule_count: rules.length,
8878
+ default_action: defaultAction
8879
+ });
8880
+ return toolResult({
8881
+ policy_id: policy.policy_id,
8882
+ policy_name: policy.policy_name,
8883
+ rules: policy.rules,
8884
+ default_action: policy.default_action,
8885
+ created_at: policy.created_at,
8886
+ message: "Context-gating policy created. Use sanctuary/context_gate_filter to apply this policy before making outbound calls."
8887
+ });
8888
+ }
8889
+ },
8890
+ // ── Apply Template ───────────────────────────────────────────────
8891
+ {
8892
+ name: "sanctuary/context_gate_apply_template",
8893
+ 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.",
8894
+ inputSchema: {
8895
+ type: "object",
8896
+ properties: {
8897
+ template_id: {
8898
+ type: "string",
8899
+ description: "Template to apply: inference-minimal, inference-standard, logging-strict, or tool-api-scoped"
8900
+ },
8901
+ identity_id: {
8902
+ type: "string",
8903
+ description: "Bind this policy to a specific identity (optional)"
8904
+ }
8905
+ },
8906
+ required: ["template_id"]
8907
+ },
8908
+ handler: async (args) => {
8909
+ const templateId = args.template_id;
8910
+ const identityId = args.identity_id;
8911
+ const template = getTemplate(templateId);
8912
+ if (!template) {
8913
+ return toolResult({
8914
+ error: "template_not_found",
8915
+ message: `Unknown template "${templateId}"`,
8916
+ available_templates: listTemplateIds().map((id) => {
8917
+ const t = TEMPLATES[id];
8918
+ return { id, name: t.name, description: t.description };
8919
+ })
8920
+ });
8921
+ }
8922
+ const policy = await policyStore.create(
8923
+ template.name,
8924
+ template.rules,
8925
+ template.default_action,
8926
+ identityId
8927
+ );
8928
+ auditLog.append("l2", "context_gate_apply_template", identityId ?? "system", {
8929
+ policy_id: policy.policy_id,
8930
+ template_id: templateId
8931
+ });
8932
+ return toolResult({
8933
+ policy_id: policy.policy_id,
8934
+ template_applied: templateId,
8935
+ policy_name: template.name,
8936
+ description: template.description,
8937
+ use_when: template.use_when,
8938
+ rules: policy.rules,
8939
+ default_action: policy.default_action,
8940
+ created_at: policy.created_at,
8941
+ 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."
8942
+ });
8943
+ }
8944
+ },
8945
+ // ── Recommend Policy ────────────────────────────────────────────
8946
+ {
8947
+ name: "sanctuary/context_gate_recommend",
8948
+ 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.",
8949
+ inputSchema: {
8950
+ type: "object",
8951
+ properties: {
8952
+ context: {
8953
+ type: "object",
8954
+ description: "A sample context object to analyze. Each top-level key will be classified. Values are inspected for size warnings but not stored."
8955
+ },
8956
+ provider: {
8957
+ type: "string",
8958
+ description: "Provider category to generate rules for. Default: 'inference'."
8959
+ }
8960
+ },
8961
+ required: ["context"]
8962
+ },
8963
+ handler: async (args) => {
8964
+ const context = args.context;
8965
+ const provider = args.provider ?? "inference";
8966
+ const contextKeys = Object.keys(context);
8967
+ if (contextKeys.length > MAX_CONTEXT_FIELDS) {
8968
+ return toolResult({
8969
+ error: "context_too_large",
8970
+ message: `Context has ${contextKeys.length} fields, exceeding limit of ${MAX_CONTEXT_FIELDS}`
8971
+ });
8972
+ }
8973
+ const recommendation = recommendPolicy(context, provider);
8974
+ auditLog.append("l2", "context_gate_recommend", "system", {
8975
+ provider,
8976
+ fields_analyzed: recommendation.summary.total_fields,
8977
+ fields_allow: recommendation.summary.allow,
8978
+ fields_redact: recommendation.summary.redact,
8979
+ fields_hash: recommendation.summary.hash,
8980
+ fields_summarize: recommendation.summary.summarize
8981
+ });
8982
+ return toolResult({
8983
+ ...recommendation,
8984
+ 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.",
8985
+ available_templates: listTemplateIds().map((id) => {
8986
+ const t = TEMPLATES[id];
8987
+ return { id, name: t.name, description: t.description };
8988
+ })
8989
+ });
8990
+ }
8991
+ },
8992
+ // ── Filter Context ──────────────────────────────────────────────
8993
+ {
8994
+ name: "sanctuary/context_gate_filter",
8995
+ 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.",
8996
+ inputSchema: {
8997
+ type: "object",
8998
+ properties: {
8999
+ policy_id: {
9000
+ type: "string",
9001
+ description: "ID of the context-gating policy to apply"
9002
+ },
9003
+ provider: {
9004
+ type: "string",
9005
+ description: "Provider category for this call: inference, tool-api, logging, analytics, peer-agent, or custom"
9006
+ },
9007
+ context: {
9008
+ type: "object",
9009
+ 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"
9010
+ }
9011
+ },
9012
+ required: ["policy_id", "provider", "context"]
9013
+ },
9014
+ handler: async (args) => {
9015
+ const policyId = args.policy_id;
9016
+ const provider = args.provider;
9017
+ const context = args.context;
9018
+ const contextKeys = Object.keys(context);
9019
+ if (contextKeys.length > MAX_CONTEXT_FIELDS) {
9020
+ return toolResult({
9021
+ error: "context_too_large",
9022
+ message: `Context has ${contextKeys.length} fields, exceeding limit of ${MAX_CONTEXT_FIELDS}`
9023
+ });
9024
+ }
9025
+ const policy = await policyStore.get(policyId);
9026
+ if (!policy) {
9027
+ return toolResult({
9028
+ error: "policy_not_found",
9029
+ message: `No context-gating policy found with ID "${policyId}"`
9030
+ });
9031
+ }
9032
+ const result = filterContext(policy, provider, context);
9033
+ const deniedFields = result.decisions.filter((d) => d.action === "deny");
9034
+ if (deniedFields.length > 0) {
9035
+ auditLog.append("l2", "context_gate_deny", policy.identity_id ?? "system", {
9036
+ policy_id: policyId,
9037
+ provider,
9038
+ denied_fields: deniedFields.map((d) => d.field),
9039
+ original_context_hash: result.original_context_hash
9040
+ });
9041
+ return toolResult({
9042
+ blocked: true,
9043
+ reason: "Context contains fields that trigger deny action",
9044
+ denied_fields: deniedFields.map((d) => ({
9045
+ field: d.field,
9046
+ reason: d.reason
9047
+ })),
9048
+ recommendation: "Remove the denied fields from context before retrying, or update the policy to handle these fields differently."
9049
+ });
9050
+ }
9051
+ const safeContext = {};
9052
+ for (const decision of result.decisions) {
9053
+ switch (decision.action) {
9054
+ case "allow":
9055
+ safeContext[decision.field] = context[decision.field];
9056
+ break;
9057
+ case "redact":
9058
+ break;
9059
+ case "hash":
9060
+ safeContext[decision.field] = decision.hash_value;
9061
+ break;
9062
+ case "summarize":
9063
+ safeContext[decision.field] = context[decision.field];
9064
+ break;
9065
+ }
9066
+ }
9067
+ auditLog.append("l2", "context_gate_filter", policy.identity_id ?? "system", {
9068
+ policy_id: policyId,
9069
+ provider,
9070
+ fields_total: Object.keys(context).length,
9071
+ fields_allowed: result.fields_allowed,
9072
+ fields_redacted: result.fields_redacted,
9073
+ fields_hashed: result.fields_hashed,
9074
+ fields_summarized: result.fields_summarized,
9075
+ original_context_hash: result.original_context_hash,
9076
+ filtered_context_hash: result.filtered_context_hash
9077
+ });
9078
+ return toolResult({
9079
+ blocked: false,
9080
+ safe_context: safeContext,
9081
+ summary: {
9082
+ total_fields: Object.keys(context).length,
9083
+ allowed: result.fields_allowed,
9084
+ redacted: result.fields_redacted,
9085
+ hashed: result.fields_hashed,
9086
+ summarized: result.fields_summarized
9087
+ },
9088
+ decisions: result.decisions,
9089
+ audit: {
9090
+ original_context_hash: result.original_context_hash,
9091
+ filtered_context_hash: result.filtered_context_hash,
9092
+ filtered_at: result.filtered_at
9093
+ },
9094
+ 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
9095
+ });
9096
+ }
9097
+ },
9098
+ // ── List Policies ───────────────────────────────────────────────
9099
+ {
9100
+ name: "sanctuary/context_gate_list_policies",
9101
+ description: "List all configured context-gating policies. Returns policy IDs, names, rule summaries, and default actions.",
9102
+ inputSchema: {
9103
+ type: "object",
9104
+ properties: {}
9105
+ },
9106
+ handler: async () => {
9107
+ const policies = await policyStore.list();
9108
+ auditLog.append("l2", "context_gate_list_policies", "system", {
9109
+ policy_count: policies.length
9110
+ });
9111
+ return toolResult({
9112
+ policies: policies.map((p) => ({
9113
+ policy_id: p.policy_id,
9114
+ policy_name: p.policy_name,
9115
+ rule_count: p.rules.length,
9116
+ providers: p.rules.map((r) => r.provider),
9117
+ default_action: p.default_action,
9118
+ identity_id: p.identity_id ?? null,
9119
+ created_at: p.created_at,
9120
+ updated_at: p.updated_at
9121
+ })),
9122
+ count: policies.length,
9123
+ 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.`
9124
+ });
9125
+ }
9126
+ }
9127
+ ];
9128
+ return { tools, policyStore };
9129
+ }
9130
+ function checkMemoryProtection() {
9131
+ const checks = {
9132
+ aslr_enabled: checkASLR(),
9133
+ stack_canaries: true,
9134
+ // Enabled by default in Node.js runtime
9135
+ secure_buffer_zeros: true,
9136
+ // We use crypto.randomBytes and explicit zeroing
9137
+ argon2id_kdf: true
9138
+ // Master key derivation uses Argon2id
9139
+ };
9140
+ const activeCount = Object.values(checks).filter((v) => v).length;
9141
+ const overall = activeCount >= 4 ? "full" : activeCount >= 3 ? "partial" : "minimal";
9142
+ return {
9143
+ ...checks,
9144
+ overall
9145
+ };
9146
+ }
9147
+ function checkASLR() {
9148
+ if (process.platform === "linux") {
9149
+ try {
9150
+ const result = execSync("cat /proc/sys/kernel/randomize_va_space", {
9151
+ encoding: "utf-8",
9152
+ stdio: ["pipe", "pipe", "ignore"]
9153
+ }).trim();
9154
+ return result === "2";
9155
+ } catch {
9156
+ return false;
9157
+ }
9158
+ }
9159
+ if (process.platform === "darwin") {
9160
+ return true;
9161
+ }
9162
+ return false;
9163
+ }
9164
+ function checkProcessIsolation() {
9165
+ const isContainer = detectContainer();
9166
+ const isVM = detectVM();
9167
+ const isSandboxed = detectSandbox();
9168
+ let isolationLevel = "none";
9169
+ if (isContainer) isolationLevel = "hardened";
9170
+ else if (isVM) isolationLevel = "hardened";
9171
+ else if (isSandboxed) isolationLevel = "basic";
9172
+ const details = {};
9173
+ if (isContainer && isContainer !== true) details.container_type = isContainer;
9174
+ if (isVM && isVM !== true) details.vm_type = isVM;
9175
+ if (isSandboxed && isSandboxed !== true) details.sandbox_type = isSandboxed;
9176
+ return {
9177
+ isolation_level: isolationLevel,
9178
+ is_container: isContainer !== false,
9179
+ is_vm: isVM !== false,
9180
+ is_sandboxed: isSandboxed !== false,
9181
+ is_tee: false,
9182
+ details
9183
+ };
9184
+ }
9185
+ function detectContainer() {
9186
+ try {
9187
+ if (process.env.DOCKER_HOST) return "docker";
9188
+ try {
9189
+ statSync("/.dockerenv");
9190
+ return "docker";
9191
+ } catch {
9192
+ }
9193
+ if (process.platform === "linux") {
9194
+ const cgroup = execSync("cat /proc/1/cgroup 2>/dev/null || echo ''", {
9195
+ encoding: "utf-8"
9196
+ });
9197
+ if (cgroup.includes("docker")) return "docker";
9198
+ if (cgroup.includes("lxc")) return "lxc";
9199
+ if (cgroup.includes("kubepods") || cgroup.includes("kubernetes")) return "kubernetes";
9200
+ }
9201
+ if (process.env.container === "podman") return "podman";
9202
+ if (process.env.CONTAINER_ID) return "oci";
9203
+ return false;
9204
+ } catch {
9205
+ return false;
9206
+ }
9207
+ }
9208
+ function detectVM() {
9209
+ if (process.platform === "linux") {
9210
+ try {
9211
+ const dmidecode = execSync("dmidecode -s system-product-name 2>/dev/null || echo ''", {
9212
+ encoding: "utf-8"
9213
+ }).toLowerCase();
9214
+ if (dmidecode.includes("vmware")) return "vmware";
9215
+ if (dmidecode.includes("virtualbox")) return "virtualbox";
9216
+ if (dmidecode.includes("kvm")) return "kvm";
9217
+ if (dmidecode.includes("xen")) return "xen";
9218
+ if (dmidecode.includes("hyper-v")) return "hyper-v";
9219
+ const cpuinfo = execSync("grep -i hypervisor /proc/cpuinfo || echo ''", {
9220
+ encoding: "utf-8"
9221
+ });
9222
+ if (cpuinfo.length > 0) return "detected";
9223
+ } catch {
9224
+ }
9225
+ }
9226
+ if (process.platform === "darwin") {
9227
+ try {
9228
+ const bootargs = execSync(
9229
+ "nvram boot-args 2>/dev/null | grep -i 'parallels\\|vmware\\|virtualbox' || echo ''",
9230
+ {
9231
+ encoding: "utf-8"
9232
+ }
9233
+ );
9234
+ if (bootargs.length > 0) return "detected";
9235
+ } catch {
9236
+ }
9237
+ }
9238
+ return false;
9239
+ }
9240
+ function detectSandbox() {
9241
+ if (process.platform === "darwin") {
9242
+ if (process.env.APP_SANDBOX_READ_ONLY_HOME === "1") return "app-sandbox";
9243
+ if (process.env.TMPDIR && process.env.TMPDIR.includes("AppSandbox")) return "app-sandbox";
9244
+ }
9245
+ if (process.platform === "openbsd") {
9246
+ try {
9247
+ const pledge = execSync("pledge -v 2>/dev/null || echo ''", {
9248
+ encoding: "utf-8"
9249
+ });
9250
+ if (pledge.length > 0) return "pledge";
9251
+ } catch {
9252
+ }
9253
+ }
9254
+ if (process.platform === "linux") {
9255
+ if (process.env.container === "lxc") return "lxc";
9256
+ try {
9257
+ const context = execSync("getenforce 2>/dev/null || echo ''", {
9258
+ encoding: "utf-8"
9259
+ }).trim();
9260
+ if (context === "Enforcing") return "selinux";
9261
+ } catch {
9262
+ }
9263
+ }
9264
+ return false;
9265
+ }
9266
+ function checkFilesystemPermissions(storagePath) {
9267
+ try {
9268
+ const stats = statSync(storagePath);
9269
+ const mode = stats.mode & parseInt("777", 8);
9270
+ const modeString = mode.toString(8).padStart(3, "0");
9271
+ const isSecure = mode === parseInt("700", 8);
9272
+ const groupReadable = (mode & parseInt("040", 8)) !== 0;
9273
+ const othersReadable = (mode & parseInt("007", 8)) !== 0;
9274
+ const currentUid = process.getuid?.() || -1;
9275
+ const ownerIsCurrentUser = stats.uid === currentUid;
9276
+ let overall = "secure";
9277
+ if (groupReadable || othersReadable) overall = "insecure";
9278
+ else if (!ownerIsCurrentUser) overall = "warning";
9279
+ return {
9280
+ sanctuary_storage_protected: isSecure,
9281
+ sanctuary_storage_mode: modeString,
9282
+ owner_is_current_user: ownerIsCurrentUser,
9283
+ group_readable: groupReadable,
9284
+ others_readable: othersReadable,
9285
+ overall
9286
+ };
9287
+ } catch {
9288
+ return {
9289
+ sanctuary_storage_protected: false,
9290
+ sanctuary_storage_mode: "unknown",
9291
+ owner_is_current_user: false,
9292
+ group_readable: false,
9293
+ others_readable: false,
9294
+ overall: "warning"
9295
+ };
9296
+ }
9297
+ }
9298
+ function checkRuntimeIntegrity() {
9299
+ return {
9300
+ config_hash_stable: true,
9301
+ environment_state: "clean",
9302
+ discrepancies: []
9303
+ };
9304
+ }
9305
+ function assessL2Hardening(storagePath) {
9306
+ const memory = checkMemoryProtection();
9307
+ const isolation = checkProcessIsolation();
9308
+ const filesystem = checkFilesystemPermissions(storagePath);
9309
+ const integrity = checkRuntimeIntegrity();
9310
+ let checksPassed = 0;
9311
+ let checksTotal = 0;
9312
+ if (memory.aslr_enabled) checksPassed++;
9313
+ checksTotal++;
9314
+ if (memory.stack_canaries) checksPassed++;
9315
+ checksTotal++;
9316
+ if (memory.secure_buffer_zeros) checksPassed++;
9317
+ checksTotal++;
9318
+ if (memory.argon2id_kdf) checksPassed++;
9319
+ checksTotal++;
9320
+ if (isolation.is_container) checksPassed++;
9321
+ checksTotal++;
9322
+ if (isolation.is_vm) checksPassed++;
9323
+ checksTotal++;
9324
+ if (isolation.is_sandboxed) checksPassed++;
9325
+ checksTotal++;
9326
+ if (filesystem.sanctuary_storage_protected) checksPassed++;
9327
+ checksTotal++;
9328
+ {
9329
+ checksPassed++;
9330
+ }
9331
+ checksTotal++;
9332
+ let hardeningLevel = isolation.isolation_level;
9333
+ if (filesystem.overall === "insecure" || memory.overall === "none" || memory.overall === "minimal") {
9334
+ if (hardeningLevel === "hardened") {
9335
+ hardeningLevel = "basic";
9336
+ } else if (hardeningLevel === "basic") {
9337
+ hardeningLevel = "none";
9338
+ }
9339
+ }
9340
+ const summaryParts = [];
9341
+ if (isolation.is_container || isolation.is_vm) {
9342
+ summaryParts.push(`Running in ${isolation.details.container_type || isolation.details.vm_type || "isolated environment"}`);
9343
+ }
9344
+ if (memory.aslr_enabled) {
9345
+ summaryParts.push("ASLR enabled");
9346
+ }
9347
+ if (filesystem.sanctuary_storage_protected) {
9348
+ summaryParts.push("Storage permissions secured (0700)");
9349
+ }
9350
+ const summary = summaryParts.length > 0 ? summaryParts.join("; ") : "No process-level hardening detected";
9351
+ return {
9352
+ hardening_level: hardeningLevel,
9353
+ memory_protection: memory,
9354
+ process_isolation: isolation,
9355
+ filesystem_permissions: filesystem,
9356
+ runtime_integrity: integrity,
9357
+ checks_passed: checksPassed,
9358
+ checks_total: checksTotal,
9359
+ summary
9360
+ };
9361
+ }
9362
+
9363
+ // src/l2-operational/hardening-tools.ts
9364
+ function createL2HardeningTools(storagePath, auditLog) {
9365
+ return [
9366
+ {
9367
+ name: "sanctuary/l2_hardening_status",
9368
+ 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.",
9369
+ inputSchema: {
9370
+ type: "object",
9371
+ properties: {
9372
+ include_details: {
9373
+ type: "boolean",
9374
+ description: "If true, include detailed check results for memory, process, and filesystem. If false, show summary only.",
9375
+ default: false
9376
+ }
9377
+ }
9378
+ },
9379
+ handler: async (args) => {
9380
+ const includeDetails = args.include_details ?? false;
9381
+ const status = assessL2Hardening(storagePath);
9382
+ auditLog.append(
9383
+ "l2",
9384
+ "l2_hardening_status",
9385
+ "system",
9386
+ { include_details: includeDetails }
9387
+ );
9388
+ if (includeDetails) {
9389
+ return toolResult({
9390
+ hardening_level: status.hardening_level,
9391
+ summary: status.summary,
9392
+ checks_passed: status.checks_passed,
9393
+ checks_total: status.checks_total,
9394
+ memory_protection: {
9395
+ aslr_enabled: status.memory_protection.aslr_enabled,
9396
+ stack_canaries: status.memory_protection.stack_canaries,
9397
+ secure_buffer_zeros: status.memory_protection.secure_buffer_zeros,
9398
+ argon2id_kdf: status.memory_protection.argon2id_kdf,
9399
+ overall: status.memory_protection.overall
9400
+ },
9401
+ process_isolation: {
9402
+ isolation_level: status.process_isolation.isolation_level,
9403
+ is_container: status.process_isolation.is_container,
9404
+ is_vm: status.process_isolation.is_vm,
9405
+ is_sandboxed: status.process_isolation.is_sandboxed,
9406
+ is_tee: status.process_isolation.is_tee,
9407
+ details: status.process_isolation.details
9408
+ },
9409
+ filesystem_permissions: {
9410
+ sanctuary_storage_protected: status.filesystem_permissions.sanctuary_storage_protected,
9411
+ sanctuary_storage_mode: status.filesystem_permissions.sanctuary_storage_mode,
9412
+ owner_is_current_user: status.filesystem_permissions.owner_is_current_user,
9413
+ group_readable: status.filesystem_permissions.group_readable,
9414
+ others_readable: status.filesystem_permissions.others_readable,
9415
+ overall: status.filesystem_permissions.overall
9416
+ },
9417
+ runtime_integrity: {
9418
+ config_hash_stable: status.runtime_integrity.config_hash_stable,
9419
+ environment_state: status.runtime_integrity.environment_state,
9420
+ discrepancies: status.runtime_integrity.discrepancies
9421
+ }
9422
+ });
9423
+ } else {
9424
+ return toolResult({
9425
+ hardening_level: status.hardening_level,
9426
+ summary: status.summary,
9427
+ checks_passed: status.checks_passed,
9428
+ checks_total: status.checks_total,
9429
+ note: "Pass include_details: true to see full breakdown of memory, process isolation, and filesystem checks."
9430
+ });
9431
+ }
9432
+ }
9433
+ },
9434
+ {
9435
+ name: "sanctuary/l2_verify_isolation",
9436
+ 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.",
9437
+ inputSchema: {
9438
+ type: "object",
9439
+ properties: {
9440
+ check_filesystem: {
9441
+ type: "boolean",
9442
+ description: "If true, verify Sanctuary storage directory permissions.",
9443
+ default: true
9444
+ },
9445
+ check_memory: {
9446
+ type: "boolean",
9447
+ description: "If true, verify memory protection mechanisms (ASLR, etc.).",
9448
+ default: true
9449
+ },
9450
+ check_process: {
9451
+ type: "boolean",
9452
+ description: "If true, detect container, VM, or sandbox environment.",
9453
+ default: true
9454
+ }
9455
+ }
9456
+ },
9457
+ handler: async (args) => {
9458
+ const checkFilesystem = args.check_filesystem ?? true;
9459
+ const checkMemory = args.check_memory ?? true;
9460
+ const checkProcess = args.check_process ?? true;
9461
+ const status = assessL2Hardening(storagePath);
9462
+ auditLog.append(
9463
+ "l2",
9464
+ "l2_verify_isolation",
9465
+ "system",
9466
+ {
9467
+ check_filesystem: checkFilesystem,
9468
+ check_memory: checkMemory,
9469
+ check_process: checkProcess
9470
+ }
9471
+ );
9472
+ const results = {
9473
+ isolation_level: status.hardening_level,
9474
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
9475
+ };
9476
+ if (checkFilesystem) {
9477
+ const fs = status.filesystem_permissions;
9478
+ results.filesystem = {
9479
+ sanctuary_storage_protected: fs.sanctuary_storage_protected,
9480
+ storage_mode: fs.sanctuary_storage_mode,
9481
+ is_secure: fs.overall === "secure",
9482
+ issues: fs.overall === "insecure" ? [
9483
+ "Storage directory is readable by group or others. Recommend: chmod 700 on Sanctuary storage path."
9484
+ ] : fs.overall === "warning" ? [
9485
+ "Storage directory not owned by current user. Verify correct user is running Sanctuary."
9486
+ ] : []
9487
+ };
9488
+ }
9489
+ if (checkMemory) {
9490
+ const mem = status.memory_protection;
9491
+ const issues = [];
9492
+ if (!mem.aslr_enabled) {
9493
+ issues.push(
9494
+ "ASLR not detected. On Linux, enable with: echo 2 | sudo tee /proc/sys/kernel/randomize_va_space"
9495
+ );
9496
+ }
9497
+ results.memory = {
9498
+ aslr_enabled: mem.aslr_enabled,
9499
+ stack_canaries: mem.stack_canaries,
9500
+ secure_buffer_handling: mem.secure_buffer_zeros,
9501
+ argon2id_key_derivation: mem.argon2id_kdf,
9502
+ protection_level: mem.overall,
9503
+ issues
9504
+ };
9505
+ }
9506
+ if (checkProcess) {
9507
+ const iso = status.process_isolation;
9508
+ results.process = {
9509
+ isolation_level: iso.isolation_level,
9510
+ in_container: iso.is_container,
9511
+ in_vm: iso.is_vm,
9512
+ sandboxed: iso.is_sandboxed,
9513
+ has_tee: iso.is_tee,
9514
+ environment: iso.details,
9515
+ 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."
9516
+ };
9517
+ }
9518
+ return toolResult({
9519
+ status: "verified",
9520
+ results
9521
+ });
9522
+ }
9523
+ }
9524
+ ];
9525
+ }
9526
+
9527
+ // src/index.ts
9528
+ init_encoding();
9529
+
9530
+ // src/storage/memory.ts
9531
+ var MemoryStorage = class {
9532
+ store = /* @__PURE__ */ new Map();
9533
+ storageKey(namespace, key) {
9534
+ return `${namespace}/${key}`;
9535
+ }
9536
+ async write(namespace, key, data) {
9537
+ this.store.set(this.storageKey(namespace, key), {
9538
+ data: new Uint8Array(data),
9539
+ // Copy to prevent external mutation
9540
+ modified_at: (/* @__PURE__ */ new Date()).toISOString()
9541
+ });
9542
+ }
9543
+ async read(namespace, key) {
9544
+ const entry = this.store.get(this.storageKey(namespace, key));
9545
+ if (!entry) return null;
9546
+ return new Uint8Array(entry.data);
9547
+ }
9548
+ async delete(namespace, key, _secureOverwrite) {
9549
+ return this.store.delete(this.storageKey(namespace, key));
9550
+ }
9551
+ async list(namespace, prefix) {
9552
+ const entries = [];
9553
+ const nsPrefix = `${namespace}/`;
9554
+ for (const [storeKey, entry] of this.store) {
9555
+ if (!storeKey.startsWith(nsPrefix)) continue;
9556
+ const key = storeKey.slice(nsPrefix.length);
9557
+ if (prefix && !key.startsWith(prefix)) continue;
9558
+ entries.push({
9559
+ key,
9560
+ namespace,
9561
+ size_bytes: entry.data.length,
9562
+ modified_at: entry.modified_at
9563
+ });
9564
+ }
9565
+ return entries.sort((a, b) => a.key.localeCompare(b.key));
9566
+ }
9567
+ async exists(namespace, key) {
9568
+ return this.store.has(this.storageKey(namespace, key));
9569
+ }
9570
+ async totalSize() {
9571
+ let total = 0;
9572
+ for (const entry of this.store.values()) {
9573
+ total += entry.data.length;
9574
+ }
9575
+ return total;
9576
+ }
9577
+ /** Clear all stored data (useful in tests) */
9578
+ clear() {
9579
+ this.store.clear();
9580
+ }
9581
+ };
9582
+
9583
+ // src/index.ts
9584
+ async function createSanctuaryServer(options) {
9585
+ const config = await loadConfig(options?.configPath);
9586
+ await mkdir(config.storage_path, { recursive: true, mode: 448 });
9587
+ const storage = options?.storage ?? new FilesystemStorage(
9588
+ `${config.storage_path}/state`
9589
+ );
9590
+ let masterKey;
9591
+ let keyProtection;
9592
+ let recoveryKey;
9593
+ const passphrase = options?.passphrase ?? process.env.SANCTUARY_PASSPHRASE;
9594
+ if (passphrase) {
9595
+ keyProtection = "passphrase";
9596
+ let existingParams;
7643
9597
  try {
7644
9598
  const raw = await storage.read("_meta", "key-params");
7645
9599
  if (raw) {
@@ -7791,7 +9745,7 @@ async function createSanctuaryServer(options) {
7791
9745
  layer: "l2",
7792
9746
  description: "Process-level isolation only (no TEE)",
7793
9747
  severity: "warning",
7794
- mitigation: "TEE support planned for v0.3.0"
9748
+ mitigation: "TEE support planned for a future release"
7795
9749
  });
7796
9750
  if (config.disclosure.proof_system === "commitment-only") {
7797
9751
  degradations.push({
@@ -7931,7 +9885,7 @@ async function createSanctuaryServer(options) {
7931
9885
  },
7932
9886
  limitations: [
7933
9887
  "L1 identity uses ed25519 only; KERI support planned for v0.2.0",
7934
- "L2 isolation is process-level only; TEE support planned for v0.3.0",
9888
+ "L2 isolation is process-level only; TEE support planned for a future release",
7935
9889
  "L3 uses commitment schemes only; ZK proofs planned for v0.2.0",
7936
9890
  "L4 Sybil resistance is escrow-based only",
7937
9891
  "Spec license: CC-BY-4.0 | Code license: Apache-2.0"
@@ -7952,7 +9906,7 @@ async function createSanctuaryServer(options) {
7952
9906
  masterKey,
7953
9907
  auditLog
7954
9908
  );
7955
- const { tools: l4Tools } = createL4Tools(
9909
+ const { tools: l4Tools} = createL4Tools(
7956
9910
  storage,
7957
9911
  masterKey,
7958
9912
  identityManager,
@@ -7971,6 +9925,12 @@ async function createSanctuaryServer(options) {
7971
9925
  handshakeResults
7972
9926
  );
7973
9927
  const { tools: auditTools } = createAuditTools(config);
9928
+ const { tools: contextGateTools } = createContextGateTools(
9929
+ storage,
9930
+ masterKey,
9931
+ auditLog
9932
+ );
9933
+ const hardeningTools = createL2HardeningTools(config.storage_path, auditLog);
7974
9934
  const policy = await loadPrincipalPolicy(config.storage_path);
7975
9935
  const baseline = new BaselineTracker(storage, masterKey);
7976
9936
  await baseline.load();
@@ -8020,6 +9980,8 @@ async function createSanctuaryServer(options) {
8020
9980
  ...federationTools,
8021
9981
  ...bridgeTools,
8022
9982
  ...auditTools,
9983
+ ...contextGateTools,
9984
+ ...hardeningTools,
8023
9985
  manifestTool
8024
9986
  ];
8025
9987
  const server = createServer(allTools, { gate });
@@ -8045,6 +10007,6 @@ async function createSanctuaryServer(options) {
8045
10007
  return { server, config };
8046
10008
  }
8047
10009
 
8048
- export { ApprovalGate, AuditLog, AutoApproveChannel, BaselineTracker, CallbackApprovalChannel, CommitmentStore, DashboardApprovalChannel, FederationRegistry, FilesystemStorage, MemoryStorage, PolicyStore, ReputationStore, StateStore, StderrApprovalChannel, TIER_WEIGHTS, WebhookApprovalChannel, canonicalize, completeHandshake, computeWeightedScore, createBridgeCommitment, createPedersenCommitment, createProofOfKnowledge, createRangeProof, createSanctuaryServer, generateSHR, initiateHandshake, loadConfig, loadPrincipalPolicy, resolveTier, respondToHandshake, signPayload, tierDistribution, verifyBridgeCommitment, verifyCompletion, verifyPedersenCommitment, verifyProofOfKnowledge, verifyRangeProof, verifySHR, verifySignature };
10010
+ export { ApprovalGate, AuditLog, AutoApproveChannel, BaselineTracker, TEMPLATES as CONTEXT_GATE_TEMPLATES, CallbackApprovalChannel, CommitmentStore, ContextGatePolicyStore, DashboardApprovalChannel, FederationRegistry, FilesystemStorage, MemoryStorage, PolicyStore, ReputationStore, StateStore, StderrApprovalChannel, TIER_WEIGHTS, WebhookApprovalChannel, canonicalize, classifyField, completeHandshake, computeWeightedScore, createBridgeCommitment, createPedersenCommitment, createProofOfKnowledge, createRangeProof, createSanctuaryServer, evaluateField, filterContext, generateSHR, getTemplate, initiateHandshake, listTemplateIds, loadConfig, loadPrincipalPolicy, recommendPolicy, resolveTier, respondToHandshake, signPayload, tierDistribution, verifyBridgeCommitment, verifyCompletion, verifyPedersenCommitment, verifyProofOfKnowledge, verifyRangeProof, verifySHR, verifySignature };
8049
10011
  //# sourceMappingURL=index.js.map
8050
10012
  //# sourceMappingURL=index.js.map