@sanctuary-framework/mcp-server 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2,16 +2,19 @@
2
2
  import { sha256 } from '@noble/hashes/sha256';
3
3
  import { hmac } from '@noble/hashes/hmac';
4
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
- import { mkdir, readFile, writeFile, stat, unlink, readdir, chmod } from 'fs/promises';
5
+ import { mkdir, readFile, writeFile, stat, unlink, readdir, chmod, access } from 'fs/promises';
6
6
  import { join } from 'path';
7
7
  import { homedir } from 'os';
8
- import { randomBytes as randomBytes$1 } from 'crypto';
8
+ import { randomBytes as randomBytes$1, createHmac } from 'crypto';
9
9
  import { gcm } from '@noble/ciphers/aes.js';
10
- import { ed25519 } from '@noble/curves/ed25519';
10
+ import { RistrettoPoint, ed25519 } from '@noble/curves/ed25519';
11
11
  import { argon2id } from 'hash-wasm';
12
12
  import { hkdf } from '@noble/hashes/hkdf';
13
13
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
14
14
  import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
15
+ import { createServer as createServer$2 } from 'http';
16
+ import { createServer as createServer$1 } from 'https';
17
+ import { readFileSync } from 'fs';
15
18
 
16
19
  var __defProp = Object.defineProperty;
17
20
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -203,7 +206,7 @@ var init_hashing = __esm({
203
206
  });
204
207
  function defaultConfig() {
205
208
  return {
206
- version: "0.2.0",
209
+ version: "0.3.0",
207
210
  storage_path: join(homedir(), ".sanctuary"),
208
211
  state: {
209
212
  encryption: "aes-256-gcm",
@@ -232,7 +235,19 @@ function defaultConfig() {
232
235
  service_endpoints: []
233
236
  },
234
237
  transport: "stdio",
235
- http_port: 3500
238
+ http_port: 3500,
239
+ dashboard: {
240
+ enabled: false,
241
+ port: 3501,
242
+ host: "127.0.0.1"
243
+ },
244
+ webhook: {
245
+ enabled: false,
246
+ url: "",
247
+ secret: "",
248
+ callback_port: 3502,
249
+ callback_host: "127.0.0.1"
250
+ }
236
251
  };
237
252
  }
238
253
  async function loadConfig(configPath) {
@@ -246,12 +261,50 @@ async function loadConfig(configPath) {
246
261
  if (process.env.SANCTUARY_HTTP_PORT) {
247
262
  config.http_port = parseInt(process.env.SANCTUARY_HTTP_PORT, 10);
248
263
  }
264
+ if (process.env.SANCTUARY_DASHBOARD_ENABLED === "true") {
265
+ config.dashboard.enabled = true;
266
+ }
267
+ if (process.env.SANCTUARY_DASHBOARD_PORT) {
268
+ config.dashboard.port = parseInt(process.env.SANCTUARY_DASHBOARD_PORT, 10);
269
+ }
270
+ if (process.env.SANCTUARY_DASHBOARD_HOST) {
271
+ config.dashboard.host = process.env.SANCTUARY_DASHBOARD_HOST;
272
+ }
273
+ if (process.env.SANCTUARY_DASHBOARD_AUTH_TOKEN) {
274
+ config.dashboard.auth_token = process.env.SANCTUARY_DASHBOARD_AUTH_TOKEN;
275
+ }
276
+ if (process.env.SANCTUARY_DASHBOARD_TLS_CERT && process.env.SANCTUARY_DASHBOARD_TLS_KEY) {
277
+ config.dashboard.tls = {
278
+ cert_path: process.env.SANCTUARY_DASHBOARD_TLS_CERT,
279
+ key_path: process.env.SANCTUARY_DASHBOARD_TLS_KEY
280
+ };
281
+ }
282
+ if (process.env.SANCTUARY_WEBHOOK_ENABLED === "true") {
283
+ config.webhook.enabled = true;
284
+ }
285
+ if (process.env.SANCTUARY_WEBHOOK_URL) {
286
+ config.webhook.url = process.env.SANCTUARY_WEBHOOK_URL;
287
+ }
288
+ if (process.env.SANCTUARY_WEBHOOK_SECRET) {
289
+ config.webhook.secret = process.env.SANCTUARY_WEBHOOK_SECRET;
290
+ }
291
+ if (process.env.SANCTUARY_WEBHOOK_CALLBACK_PORT) {
292
+ config.webhook.callback_port = parseInt(process.env.SANCTUARY_WEBHOOK_CALLBACK_PORT, 10);
293
+ }
294
+ if (process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST) {
295
+ config.webhook.callback_host = process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST;
296
+ }
249
297
  const path = configPath ?? join(config.storage_path, "sanctuary.json");
250
298
  try {
251
299
  const raw = await readFile(path, "utf-8");
252
300
  const fileConfig = JSON.parse(raw);
253
- return deepMerge(config, fileConfig);
254
- } catch {
301
+ const merged = deepMerge(config, fileConfig);
302
+ validateConfig(merged);
303
+ return merged;
304
+ } catch (err) {
305
+ if (err instanceof Error && err.message.includes("unimplemented features")) {
306
+ throw err;
307
+ }
255
308
  return config;
256
309
  }
257
310
  }
@@ -259,6 +312,33 @@ async function saveConfig(config, configPath) {
259
312
  const path = join(config.storage_path, "sanctuary.json");
260
313
  await writeFile(path, JSON.stringify(config, null, 2), { mode: 384 });
261
314
  }
315
+ function validateConfig(config) {
316
+ const errors = [];
317
+ const implementedKeyProtection = /* @__PURE__ */ new Set(["passphrase", "none"]);
318
+ if (!implementedKeyProtection.has(config.state.key_protection)) {
319
+ errors.push(
320
+ `Unimplemented config value: state.key_protection = "${config.state.key_protection}". Only ${[...implementedKeyProtection].map((v) => `"${v}"`).join(", ")} are currently implemented. Using an unimplemented key protection mode would silently degrade security.`
321
+ );
322
+ }
323
+ const implementedEnvironment = /* @__PURE__ */ new Set(["local-process", "docker"]);
324
+ if (!implementedEnvironment.has(config.execution.environment)) {
325
+ errors.push(
326
+ `Unimplemented config value: execution.environment = "${config.execution.environment}". Only ${[...implementedEnvironment].map((v) => `"${v}"`).join(", ")} are currently implemented. Using an unimplemented environment would silently degrade security.`
327
+ );
328
+ }
329
+ const implementedProofSystem = /* @__PURE__ */ new Set(["commitment-only"]);
330
+ if (!implementedProofSystem.has(config.disclosure.proof_system)) {
331
+ errors.push(
332
+ `Unimplemented config value: disclosure.proof_system = "${config.disclosure.proof_system}". Only ${[...implementedProofSystem].map((v) => `"${v}"`).join(", ")} is currently implemented. Using an unimplemented proof system would silently degrade security.`
333
+ );
334
+ }
335
+ if (errors.length > 0) {
336
+ throw new Error(
337
+ `Sanctuary configuration references unimplemented features:
338
+ ${errors.join("\n")}`
339
+ );
340
+ }
341
+ }
262
342
  function deepMerge(base, override) {
263
343
  const result = { ...base };
264
344
  for (const [key, value] of Object.entries(override)) {
@@ -602,7 +682,11 @@ var RESERVED_NAMESPACE_PREFIXES = [
602
682
  "_commitments",
603
683
  "_reputation",
604
684
  "_escrow",
605
- "_guarantees"
685
+ "_guarantees",
686
+ "_bridge",
687
+ "_federation",
688
+ "_handshake",
689
+ "_shr"
606
690
  ];
607
691
  var StateStore = class {
608
692
  storage;
@@ -869,12 +953,14 @@ var StateStore = class {
869
953
  /**
870
954
  * Import a previously exported state bundle.
871
955
  */
872
- async import(bundleBase64, conflictResolution = "skip") {
956
+ async import(bundleBase64, conflictResolution = "skip", publicKeyResolver) {
873
957
  const bundleBytes = fromBase64url(bundleBase64);
874
958
  const bundleJson = bytesToString(bundleBytes);
875
959
  const bundle = JSON.parse(bundleJson);
876
960
  let importedKeys = 0;
877
961
  let skippedKeys = 0;
962
+ let skippedInvalidSig = 0;
963
+ let skippedUnknownKid = 0;
878
964
  let conflicts = 0;
879
965
  const namespaces = [];
880
966
  for (const [ns, entries] of Object.entries(
@@ -888,6 +974,26 @@ var StateStore = class {
888
974
  }
889
975
  namespaces.push(ns);
890
976
  for (const { key, entry } of entries) {
977
+ const signerPublicKey = publicKeyResolver(entry.kid);
978
+ if (!signerPublicKey) {
979
+ skippedUnknownKid++;
980
+ skippedKeys++;
981
+ continue;
982
+ }
983
+ try {
984
+ const ciphertextBytes = fromBase64url(entry.payload.ct);
985
+ const signatureBytes = fromBase64url(entry.sig);
986
+ const sigValid = verify(ciphertextBytes, signatureBytes, signerPublicKey);
987
+ if (!sigValid) {
988
+ skippedInvalidSig++;
989
+ skippedKeys++;
990
+ continue;
991
+ }
992
+ } catch {
993
+ skippedInvalidSig++;
994
+ skippedKeys++;
995
+ continue;
996
+ }
891
997
  const exists = await this.storage.exists(ns, key);
892
998
  if (exists) {
893
999
  conflicts++;
@@ -923,6 +1029,8 @@ var StateStore = class {
923
1029
  return {
924
1030
  imported_keys: importedKeys,
925
1031
  skipped_keys: skippedKeys,
1032
+ skipped_invalid_sig: skippedInvalidSig,
1033
+ skipped_unknown_kid: skippedUnknownKid,
926
1034
  conflicts,
927
1035
  namespaces,
928
1036
  imported_at: (/* @__PURE__ */ new Date()).toISOString()
@@ -1011,7 +1119,7 @@ function createServer(tools, options) {
1011
1119
  const server = new Server(
1012
1120
  {
1013
1121
  name: "sanctuary-mcp-server",
1014
- version: "0.2.0"
1122
+ version: "0.3.0"
1015
1123
  },
1016
1124
  {
1017
1125
  capabilities: {
@@ -1111,7 +1219,11 @@ var RESERVED_NAMESPACE_PREFIXES2 = [
1111
1219
  "_commitments",
1112
1220
  "_reputation",
1113
1221
  "_escrow",
1114
- "_guarantees"
1222
+ "_guarantees",
1223
+ "_bridge",
1224
+ "_federation",
1225
+ "_handshake",
1226
+ "_shr"
1115
1227
  ];
1116
1228
  function getReservedNamespaceViolation(namespace) {
1117
1229
  for (const prefix of RESERVED_NAMESPACE_PREFIXES2) {
@@ -1448,6 +1560,13 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1448
1560
  required: ["namespace", "key"]
1449
1561
  },
1450
1562
  handler: async (args) => {
1563
+ const reservedViolation = getReservedNamespaceViolation(args.namespace);
1564
+ if (reservedViolation) {
1565
+ return toolResult({
1566
+ error: "namespace_reserved",
1567
+ message: `Namespace "${args.namespace}" is reserved for internal use (prefix: ${reservedViolation}). Cannot read from reserved namespaces.`
1568
+ });
1569
+ }
1451
1570
  const result = await stateStore.read(
1452
1571
  args.namespace,
1453
1572
  args.key,
@@ -1484,6 +1603,13 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1484
1603
  required: ["namespace"]
1485
1604
  },
1486
1605
  handler: async (args) => {
1606
+ const reservedViolation = getReservedNamespaceViolation(args.namespace);
1607
+ if (reservedViolation) {
1608
+ return toolResult({
1609
+ error: "namespace_reserved",
1610
+ message: `Namespace "${args.namespace}" is reserved for internal use (prefix: ${reservedViolation}). Cannot list reserved namespaces.`
1611
+ });
1612
+ }
1487
1613
  const result = await stateStore.list(
1488
1614
  args.namespace,
1489
1615
  args.prefix,
@@ -1562,9 +1688,15 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
1562
1688
  required: ["bundle"]
1563
1689
  },
1564
1690
  handler: async (args) => {
1691
+ const publicKeyResolver = (kid) => {
1692
+ const identity = identityMgr.get(kid);
1693
+ if (!identity) return null;
1694
+ return fromBase64url(identity.public_key);
1695
+ };
1565
1696
  const result = await stateStore.import(
1566
1697
  args.bundle,
1567
- args.conflict_resolution ?? "skip"
1698
+ args.conflict_resolution ?? "skip",
1699
+ publicKeyResolver
1568
1700
  );
1569
1701
  auditLog?.append("l1", "state_import", "principal", {
1570
1702
  imported_keys: result.imported_keys
@@ -1885,6 +2017,267 @@ var PolicyStore = class {
1885
2017
  );
1886
2018
  }
1887
2019
  };
2020
+ init_encoding();
2021
+ var G = RistrettoPoint.BASE;
2022
+ var H_INPUT = concatBytes(
2023
+ sha256(stringToBytes("sanctuary-pedersen-generator-H-v1-a")),
2024
+ sha256(stringToBytes("sanctuary-pedersen-generator-H-v1-b"))
2025
+ );
2026
+ var H = RistrettoPoint.hashToCurve(H_INPUT);
2027
+ function bigintToBytes(n) {
2028
+ const hex = n.toString(16).padStart(64, "0");
2029
+ const bytes = new Uint8Array(32);
2030
+ for (let i = 0; i < 32; i++) {
2031
+ bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
2032
+ }
2033
+ return bytes;
2034
+ }
2035
+ function bytesToBigint(bytes) {
2036
+ let hex = "";
2037
+ for (const b of bytes) {
2038
+ hex += b.toString(16).padStart(2, "0");
2039
+ }
2040
+ return BigInt("0x" + hex);
2041
+ }
2042
+ var ORDER = BigInt("7237005577332262213973186563042994240857116359379907606001950938285454250989");
2043
+ function mod(n) {
2044
+ return (n % ORDER + ORDER) % ORDER;
2045
+ }
2046
+ function safeMultiply(point, scalar) {
2047
+ const s = mod(scalar);
2048
+ if (s === 0n) return RistrettoPoint.ZERO;
2049
+ return point.multiply(s);
2050
+ }
2051
+ function randomScalar() {
2052
+ const bytes = randomBytes(64);
2053
+ return mod(bytesToBigint(bytes));
2054
+ }
2055
+ function fiatShamirChallenge(domain, ...points) {
2056
+ const domainBytes = stringToBytes(domain);
2057
+ const combined = concatBytes(domainBytes, ...points);
2058
+ const hash2 = sha256(combined);
2059
+ return mod(bytesToBigint(hash2));
2060
+ }
2061
+ function createPedersenCommitment(value) {
2062
+ const v = mod(BigInt(value));
2063
+ const b = randomScalar();
2064
+ const C = safeMultiply(G, v).add(safeMultiply(H, b));
2065
+ return {
2066
+ commitment: toBase64url(C.toRawBytes()),
2067
+ blinding_factor: toBase64url(bigintToBytes(b)),
2068
+ committed_at: (/* @__PURE__ */ new Date()).toISOString()
2069
+ };
2070
+ }
2071
+ function verifyPedersenCommitment(commitment, value, blindingFactor) {
2072
+ try {
2073
+ const C = RistrettoPoint.fromHex(fromBase64url(commitment));
2074
+ const v = mod(BigInt(value));
2075
+ const b = bytesToBigint(fromBase64url(blindingFactor));
2076
+ const expected = safeMultiply(G, v).add(safeMultiply(H, b));
2077
+ return C.equals(expected);
2078
+ } catch {
2079
+ return false;
2080
+ }
2081
+ }
2082
+ function createProofOfKnowledge(value, blindingFactor, commitment) {
2083
+ const v = mod(BigInt(value));
2084
+ const b = bytesToBigint(fromBase64url(blindingFactor));
2085
+ const r_v = randomScalar();
2086
+ const r_b = randomScalar();
2087
+ const R = safeMultiply(G, r_v).add(safeMultiply(H, r_b));
2088
+ const C_bytes = fromBase64url(commitment);
2089
+ const R_bytes = R.toRawBytes();
2090
+ const e = fiatShamirChallenge("sanctuary-zk-pok-v1", C_bytes, R_bytes);
2091
+ const s_v = mod(r_v + e * v);
2092
+ const s_b = mod(r_b + e * b);
2093
+ return {
2094
+ type: "schnorr-pedersen-ristretto255",
2095
+ commitment,
2096
+ announcement: toBase64url(R_bytes),
2097
+ response_v: toBase64url(bigintToBytes(s_v)),
2098
+ response_b: toBase64url(bigintToBytes(s_b)),
2099
+ generated_at: (/* @__PURE__ */ new Date()).toISOString()
2100
+ };
2101
+ }
2102
+ function verifyProofOfKnowledge(proof) {
2103
+ try {
2104
+ const C = RistrettoPoint.fromHex(fromBase64url(proof.commitment));
2105
+ const R = RistrettoPoint.fromHex(fromBase64url(proof.announcement));
2106
+ const s_v = bytesToBigint(fromBase64url(proof.response_v));
2107
+ const s_b = bytesToBigint(fromBase64url(proof.response_b));
2108
+ const e = fiatShamirChallenge(
2109
+ "sanctuary-zk-pok-v1",
2110
+ fromBase64url(proof.commitment),
2111
+ fromBase64url(proof.announcement)
2112
+ );
2113
+ const lhs = safeMultiply(G, s_v).add(safeMultiply(H, s_b));
2114
+ const rhs = R.add(safeMultiply(C, e));
2115
+ return lhs.equals(rhs);
2116
+ } catch {
2117
+ return false;
2118
+ }
2119
+ }
2120
+ function createRangeProof(value, blindingFactor, commitment, min, max) {
2121
+ if (value < min || value > max) {
2122
+ return { error: `Value ${value} is not in range [${min}, ${max}]` };
2123
+ }
2124
+ const range = max - min;
2125
+ const numBits = Math.ceil(Math.log2(range + 1));
2126
+ const shifted = value - min;
2127
+ const b = bytesToBigint(fromBase64url(blindingFactor));
2128
+ const bits = [];
2129
+ for (let i = 0; i < numBits; i++) {
2130
+ bits.push(shifted >> i & 1);
2131
+ }
2132
+ const bitBlindings = [];
2133
+ const bitCommitments = [];
2134
+ const bitProofs = [];
2135
+ for (let i = 0; i < numBits; i++) {
2136
+ const bit_b = randomScalar();
2137
+ bitBlindings.push(bit_b);
2138
+ const C_i = safeMultiply(G, mod(BigInt(bits[i]))).add(safeMultiply(H, bit_b));
2139
+ bitCommitments.push(toBase64url(C_i.toRawBytes()));
2140
+ const bitProof = createBitProof(bits[i], bit_b, C_i);
2141
+ bitProofs.push(bitProof);
2142
+ }
2143
+ const sumBlinding = bitBlindings.reduce(
2144
+ (acc, bi, i) => mod(acc + mod(BigInt(1) << BigInt(i)) * bi),
2145
+ 0n
2146
+ );
2147
+ const blindingDiff = mod(b - sumBlinding);
2148
+ const r_sum = randomScalar();
2149
+ const R_sum = safeMultiply(H, r_sum);
2150
+ const e_sum = fiatShamirChallenge(
2151
+ "sanctuary-zk-range-sum-v1",
2152
+ fromBase64url(commitment),
2153
+ R_sum.toRawBytes()
2154
+ );
2155
+ const s_sum = mod(r_sum + e_sum * blindingDiff);
2156
+ return {
2157
+ type: "range-pedersen-ristretto255",
2158
+ commitment,
2159
+ min,
2160
+ max,
2161
+ bit_commitments: bitCommitments,
2162
+ bit_proofs: bitProofs,
2163
+ sum_proof: {
2164
+ announcement: toBase64url(R_sum.toRawBytes()),
2165
+ response: toBase64url(bigintToBytes(s_sum))
2166
+ },
2167
+ generated_at: (/* @__PURE__ */ new Date()).toISOString()
2168
+ };
2169
+ }
2170
+ function verifyRangeProof(proof) {
2171
+ try {
2172
+ const C = RistrettoPoint.fromHex(fromBase64url(proof.commitment));
2173
+ const range = proof.max - proof.min;
2174
+ const numBits = Math.ceil(Math.log2(range + 1));
2175
+ if (proof.bit_commitments.length !== numBits) return false;
2176
+ if (proof.bit_proofs.length !== numBits) return false;
2177
+ for (let i = 0; i < numBits; i++) {
2178
+ const C_i = RistrettoPoint.fromHex(fromBase64url(proof.bit_commitments[i]));
2179
+ if (!verifyBitProof(proof.bit_proofs[i], C_i)) {
2180
+ return false;
2181
+ }
2182
+ }
2183
+ let reconstructed = RistrettoPoint.ZERO;
2184
+ for (let i = 0; i < numBits; i++) {
2185
+ const C_i = RistrettoPoint.fromHex(fromBase64url(proof.bit_commitments[i]));
2186
+ const weight = mod(BigInt(1) << BigInt(i));
2187
+ reconstructed = reconstructed.add(safeMultiply(C_i, weight));
2188
+ }
2189
+ const diff = C.subtract(safeMultiply(G, mod(BigInt(proof.min)))).subtract(reconstructed);
2190
+ const R_sum = RistrettoPoint.fromHex(fromBase64url(proof.sum_proof.announcement));
2191
+ const s_sum = bytesToBigint(fromBase64url(proof.sum_proof.response));
2192
+ const e_sum = fiatShamirChallenge(
2193
+ "sanctuary-zk-range-sum-v1",
2194
+ fromBase64url(proof.commitment),
2195
+ fromBase64url(proof.sum_proof.announcement)
2196
+ );
2197
+ const lhs = safeMultiply(H, s_sum);
2198
+ const rhs = R_sum.add(safeMultiply(diff, e_sum));
2199
+ return lhs.equals(rhs);
2200
+ } catch {
2201
+ return false;
2202
+ }
2203
+ }
2204
+ function createBitProof(bit, blinding, commitment) {
2205
+ const C_bytes = commitment.toRawBytes();
2206
+ if (bit === 0) {
2207
+ const C_minus_G = commitment.subtract(G);
2208
+ const e_1 = randomScalar();
2209
+ const s_1 = randomScalar();
2210
+ const R_1 = safeMultiply(H, s_1).subtract(safeMultiply(C_minus_G, e_1));
2211
+ const r_0 = randomScalar();
2212
+ const R_0 = safeMultiply(H, r_0);
2213
+ const e = fiatShamirChallenge(
2214
+ "sanctuary-zk-bit-v1",
2215
+ C_bytes,
2216
+ R_0.toRawBytes(),
2217
+ R_1.toRawBytes()
2218
+ );
2219
+ const e_0 = mod(e - e_1);
2220
+ const s_0 = mod(r_0 + e_0 * blinding);
2221
+ return {
2222
+ announcement_0: toBase64url(R_0.toRawBytes()),
2223
+ announcement_1: toBase64url(R_1.toRawBytes()),
2224
+ challenge_0: toBase64url(bigintToBytes(e_0)),
2225
+ challenge_1: toBase64url(bigintToBytes(e_1)),
2226
+ response_0: toBase64url(bigintToBytes(s_0)),
2227
+ response_1: toBase64url(bigintToBytes(s_1))
2228
+ };
2229
+ } else {
2230
+ const e_0 = randomScalar();
2231
+ const s_0 = randomScalar();
2232
+ const R_0 = safeMultiply(H, s_0).subtract(safeMultiply(commitment, e_0));
2233
+ const r_1 = randomScalar();
2234
+ const R_1 = safeMultiply(H, r_1);
2235
+ const e = fiatShamirChallenge(
2236
+ "sanctuary-zk-bit-v1",
2237
+ C_bytes,
2238
+ R_0.toRawBytes(),
2239
+ R_1.toRawBytes()
2240
+ );
2241
+ const e_1 = mod(e - e_0);
2242
+ const s_1 = mod(r_1 + e_1 * blinding);
2243
+ return {
2244
+ announcement_0: toBase64url(R_0.toRawBytes()),
2245
+ announcement_1: toBase64url(R_1.toRawBytes()),
2246
+ challenge_0: toBase64url(bigintToBytes(e_0)),
2247
+ challenge_1: toBase64url(bigintToBytes(e_1)),
2248
+ response_0: toBase64url(bigintToBytes(s_0)),
2249
+ response_1: toBase64url(bigintToBytes(s_1))
2250
+ };
2251
+ }
2252
+ }
2253
+ function verifyBitProof(proof, commitment) {
2254
+ try {
2255
+ const C_bytes = commitment.toRawBytes();
2256
+ const R_0 = RistrettoPoint.fromHex(fromBase64url(proof.announcement_0));
2257
+ const R_1 = RistrettoPoint.fromHex(fromBase64url(proof.announcement_1));
2258
+ const e_0 = bytesToBigint(fromBase64url(proof.challenge_0));
2259
+ const e_1 = bytesToBigint(fromBase64url(proof.challenge_1));
2260
+ const s_0 = bytesToBigint(fromBase64url(proof.response_0));
2261
+ const s_1 = bytesToBigint(fromBase64url(proof.response_1));
2262
+ const e = fiatShamirChallenge(
2263
+ "sanctuary-zk-bit-v1",
2264
+ C_bytes,
2265
+ R_0.toRawBytes(),
2266
+ R_1.toRawBytes()
2267
+ );
2268
+ if (mod(e_0 + e_1) !== e) return false;
2269
+ const lhs_0 = safeMultiply(H, s_0);
2270
+ const rhs_0 = R_0.add(safeMultiply(commitment, e_0));
2271
+ if (!lhs_0.equals(rhs_0)) return false;
2272
+ const C_minus_G = commitment.subtract(G);
2273
+ const lhs_1 = safeMultiply(H, s_1);
2274
+ const rhs_1 = R_1.add(safeMultiply(C_minus_G, e_1));
2275
+ if (!lhs_1.equals(rhs_1)) return false;
2276
+ return true;
2277
+ } catch {
2278
+ return false;
2279
+ }
2280
+ }
1888
2281
 
1889
2282
  // src/l3-disclosure/tools.ts
1890
2283
  function createL3Tools(storage, masterKey, auditLog) {
@@ -2114,6 +2507,187 @@ function createL3Tools(storage, masterKey, auditLog) {
2114
2507
  overall_recommendation: withholding > 0 ? `Withholding ${withholding} of ${requestedFields.length} requested fields per policy "${policy.policy_name}"` : `All ${requestedFields.length} fields may be disclosed per policy "${policy.policy_name}"`
2115
2508
  });
2116
2509
  }
2510
+ },
2511
+ // ─── ZK Proof Tools ───────────────────────────────────────────────────
2512
+ {
2513
+ name: "sanctuary/zk_commit",
2514
+ description: "Create a Pedersen commitment to a numeric value on Ristretto255. Unlike SHA-256 commitments, Pedersen commitments support zero-knowledge proofs: you can prove properties about the committed value without revealing it.",
2515
+ inputSchema: {
2516
+ type: "object",
2517
+ properties: {
2518
+ value: {
2519
+ type: "number",
2520
+ description: "The integer value to commit to"
2521
+ }
2522
+ },
2523
+ required: ["value"]
2524
+ },
2525
+ handler: async (args) => {
2526
+ const value = args.value;
2527
+ if (!Number.isInteger(value)) {
2528
+ return toolResult({ error: "Value must be an integer." });
2529
+ }
2530
+ const commitment = createPedersenCommitment(value);
2531
+ auditLog.append("l3", "zk_commit", "system", {
2532
+ commitment_hash: commitment.commitment.slice(0, 16) + "..."
2533
+ });
2534
+ return toolResult({
2535
+ commitment: commitment.commitment,
2536
+ blinding_factor: commitment.blinding_factor,
2537
+ committed_at: commitment.committed_at,
2538
+ proof_system: "pedersen-ristretto255",
2539
+ note: "Store the blinding_factor securely. Use zk_prove to create proofs about this commitment."
2540
+ });
2541
+ }
2542
+ },
2543
+ {
2544
+ name: "sanctuary/zk_prove",
2545
+ description: "Create a zero-knowledge proof of knowledge for a Pedersen commitment. Proves you know the value and blinding factor without revealing either. Uses a Schnorr sigma protocol with Fiat-Shamir transform.",
2546
+ inputSchema: {
2547
+ type: "object",
2548
+ properties: {
2549
+ value: {
2550
+ type: "number",
2551
+ description: "The committed value (integer)"
2552
+ },
2553
+ blinding_factor: {
2554
+ type: "string",
2555
+ description: "The blinding factor from zk_commit (base64url)"
2556
+ },
2557
+ commitment: {
2558
+ type: "string",
2559
+ description: "The Pedersen commitment (base64url)"
2560
+ }
2561
+ },
2562
+ required: ["value", "blinding_factor", "commitment"]
2563
+ },
2564
+ handler: async (args) => {
2565
+ const value = args.value;
2566
+ const blindingFactor = args.blinding_factor;
2567
+ const commitment = args.commitment;
2568
+ if (!verifyPedersenCommitment(commitment, value, blindingFactor)) {
2569
+ return toolResult({
2570
+ error: "The provided value and blinding factor do not match the commitment."
2571
+ });
2572
+ }
2573
+ const proof = createProofOfKnowledge(value, blindingFactor, commitment);
2574
+ auditLog.append("l3", "zk_prove", "system", {
2575
+ proof_type: proof.type,
2576
+ commitment: commitment.slice(0, 16) + "..."
2577
+ });
2578
+ return toolResult({
2579
+ proof,
2580
+ note: "This proof demonstrates knowledge of the commitment opening without revealing the value."
2581
+ });
2582
+ }
2583
+ },
2584
+ {
2585
+ name: "sanctuary/zk_verify",
2586
+ description: "Verify a zero-knowledge proof of knowledge for a Pedersen commitment. Checks that the prover knows the commitment's opening without learning anything.",
2587
+ inputSchema: {
2588
+ type: "object",
2589
+ properties: {
2590
+ proof: {
2591
+ type: "object",
2592
+ description: "The ZK proof object from zk_prove"
2593
+ }
2594
+ },
2595
+ required: ["proof"]
2596
+ },
2597
+ handler: async (args) => {
2598
+ const proof = args.proof;
2599
+ const valid = verifyProofOfKnowledge(proof);
2600
+ auditLog.append("l3", "zk_verify", "system", {
2601
+ proof_type: proof.type,
2602
+ valid
2603
+ });
2604
+ return toolResult({
2605
+ valid,
2606
+ proof_type: proof.type,
2607
+ commitment: proof.commitment,
2608
+ verified_at: (/* @__PURE__ */ new Date()).toISOString()
2609
+ });
2610
+ }
2611
+ },
2612
+ {
2613
+ name: "sanctuary/zk_range_prove",
2614
+ description: "Create a zero-knowledge range proof: prove that a committed value is within [min, max] without revealing the exact value. Uses bit-decomposition with OR-proofs on Ristretto255.",
2615
+ inputSchema: {
2616
+ type: "object",
2617
+ properties: {
2618
+ value: {
2619
+ type: "number",
2620
+ description: "The committed value (integer)"
2621
+ },
2622
+ blinding_factor: {
2623
+ type: "string",
2624
+ description: "The blinding factor from zk_commit (base64url)"
2625
+ },
2626
+ commitment: {
2627
+ type: "string",
2628
+ description: "The Pedersen commitment (base64url)"
2629
+ },
2630
+ min: {
2631
+ type: "number",
2632
+ description: "Minimum of the range (inclusive)"
2633
+ },
2634
+ max: {
2635
+ type: "number",
2636
+ description: "Maximum of the range (inclusive)"
2637
+ }
2638
+ },
2639
+ required: ["value", "blinding_factor", "commitment", "min", "max"]
2640
+ },
2641
+ handler: async (args) => {
2642
+ const value = args.value;
2643
+ const blindingFactor = args.blinding_factor;
2644
+ const commitment = args.commitment;
2645
+ const min = args.min;
2646
+ const max = args.max;
2647
+ const proof = createRangeProof(value, blindingFactor, commitment, min, max);
2648
+ if ("error" in proof) {
2649
+ return toolResult({ error: proof.error });
2650
+ }
2651
+ auditLog.append("l3", "zk_range_prove", "system", {
2652
+ proof_type: proof.type,
2653
+ range: `[${min}, ${max}]`,
2654
+ bits: proof.bit_commitments.length
2655
+ });
2656
+ return toolResult({
2657
+ proof,
2658
+ note: `This proof demonstrates the committed value is in [${min}, ${max}] without revealing it.`
2659
+ });
2660
+ }
2661
+ },
2662
+ {
2663
+ name: "sanctuary/zk_range_verify",
2664
+ description: "Verify a zero-knowledge range proof \u2014 confirms a committed value is within the claimed range without learning the value.",
2665
+ inputSchema: {
2666
+ type: "object",
2667
+ properties: {
2668
+ proof: {
2669
+ type: "object",
2670
+ description: "The range proof object from zk_range_prove"
2671
+ }
2672
+ },
2673
+ required: ["proof"]
2674
+ },
2675
+ handler: async (args) => {
2676
+ const proof = args.proof;
2677
+ const valid = verifyRangeProof(proof);
2678
+ auditLog.append("l3", "zk_range_verify", "system", {
2679
+ proof_type: proof.type,
2680
+ valid,
2681
+ range: `[${proof.min}, ${proof.max}]`
2682
+ });
2683
+ return toolResult({
2684
+ valid,
2685
+ proof_type: proof.type,
2686
+ range: { min: proof.min, max: proof.max },
2687
+ commitment: proof.commitment,
2688
+ verified_at: (/* @__PURE__ */ new Date()).toISOString()
2689
+ });
2690
+ }
2117
2691
  }
2118
2692
  ];
2119
2693
  return { tools, commitmentStore, policyStore };
@@ -2162,7 +2736,7 @@ var ReputationStore = class {
2162
2736
  /**
2163
2737
  * Record an interaction outcome as a signed attestation.
2164
2738
  */
2165
- async record(interactionId, counterpartyDid, outcome, context, identity, identityEncryptionKey, counterpartyAttestation) {
2739
+ async record(interactionId, counterpartyDid, outcome, context, identity, identityEncryptionKey, counterpartyAttestation, sovereigntyTier) {
2166
2740
  const attestationId = `att-${Date.now()}-${toBase64url(randomBytes(8))}`;
2167
2741
  const now = (/* @__PURE__ */ new Date()).toISOString();
2168
2742
  const attestationData = {
@@ -2173,7 +2747,8 @@ var ReputationStore = class {
2173
2747
  outcome_result: outcome.result,
2174
2748
  metrics: outcome.metrics ?? {},
2175
2749
  context,
2176
- timestamp: now
2750
+ timestamp: now,
2751
+ sovereignty_tier: sovereigntyTier
2177
2752
  };
2178
2753
  const dataBytes = stringToBytes(JSON.stringify(attestationData));
2179
2754
  const signature = sign(
@@ -2422,6 +2997,24 @@ var ReputationStore = class {
2422
2997
  );
2423
2998
  return guarantee;
2424
2999
  }
3000
+ // ─── Tier-Aware Access ───────────────────────────────────────────────
3001
+ /**
3002
+ * Load attestations for tier-weighted scoring.
3003
+ * Applies basic context/counterparty filtering, returns full StoredAttestations
3004
+ * so callers can access sovereignty_tier from attestation data.
3005
+ */
3006
+ async loadAllForTierScoring(options) {
3007
+ let all = await this.loadAll();
3008
+ if (options?.context) {
3009
+ all = all.filter((a) => a.attestation.data.context === options.context);
3010
+ }
3011
+ if (options?.counterparty_did) {
3012
+ all = all.filter(
3013
+ (a) => a.attestation.data.counterparty_did === options.counterparty_did
3014
+ );
3015
+ }
3016
+ return all;
3017
+ }
2425
3018
  // ─── Internal ─────────────────────────────────────────────────────────
2426
3019
  async loadAll() {
2427
3020
  const results = [];
@@ -2445,9 +3038,70 @@ var ReputationStore = class {
2445
3038
 
2446
3039
  // src/l4-reputation/tools.ts
2447
3040
  init_encoding();
2448
- function createL4Tools(storage, masterKey, identityManager, auditLog) {
3041
+
3042
+ // src/l4-reputation/tiers.ts
3043
+ var TIER_WEIGHTS = {
3044
+ "verified-sovereign": 1,
3045
+ "verified-degraded": 0.8,
3046
+ "self-attested": 0.5,
3047
+ "unverified": 0.2
3048
+ };
3049
+ function resolveTier(counterpartyId, handshakeResults, hasSanctuaryIdentity) {
3050
+ const handshake = handshakeResults.get(counterpartyId);
3051
+ if (handshake && handshake.verified) {
3052
+ const expiresAt = new Date(handshake.expires_at);
3053
+ if (expiresAt > /* @__PURE__ */ new Date()) {
3054
+ return {
3055
+ sovereignty_tier: handshake.trust_tier,
3056
+ handshake_completed_at: handshake.completed_at,
3057
+ verified_by: handshake.counterparty_id
3058
+ };
3059
+ }
3060
+ }
3061
+ if (hasSanctuaryIdentity) {
3062
+ return { sovereignty_tier: "self-attested" };
3063
+ }
3064
+ return { sovereignty_tier: "unverified" };
3065
+ }
3066
+ function trustTierToSovereigntyTier(trustTier) {
3067
+ switch (trustTier) {
3068
+ case "verified-sovereign":
3069
+ return "verified-sovereign";
3070
+ case "verified-degraded":
3071
+ return "verified-degraded";
3072
+ default:
3073
+ return "unverified";
3074
+ }
3075
+ }
3076
+ function computeWeightedScore(attestations) {
3077
+ if (attestations.length === 0) return null;
3078
+ let weightedSum = 0;
3079
+ let totalWeight = 0;
3080
+ for (const a of attestations) {
3081
+ const weight = TIER_WEIGHTS[a.tier];
3082
+ weightedSum += a.value * weight;
3083
+ totalWeight += weight;
3084
+ }
3085
+ return totalWeight > 0 ? weightedSum / totalWeight : null;
3086
+ }
3087
+ function tierDistribution(tiers) {
3088
+ const dist = {
3089
+ "verified-sovereign": 0,
3090
+ "verified-degraded": 0,
3091
+ "self-attested": 0,
3092
+ "unverified": 0
3093
+ };
3094
+ for (const tier of tiers) {
3095
+ dist[tier]++;
3096
+ }
3097
+ return dist;
3098
+ }
3099
+
3100
+ // src/l4-reputation/tools.ts
3101
+ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeResults) {
2449
3102
  const reputationStore = new ReputationStore(storage, masterKey);
2450
3103
  const identityEncryptionKey = derivePurposeKey(masterKey, "identity-encryption");
3104
+ const hsResults = handshakeResults ?? /* @__PURE__ */ new Map();
2451
3105
  const tools = [
2452
3106
  // ─── Reputation Recording ─────────────────────────────────────────
2453
3107
  {
@@ -2509,26 +3163,34 @@ function createL4Tools(storage, masterKey, identityManager, auditLog) {
2509
3163
  }
2510
3164
  const outcome = args.outcome;
2511
3165
  const context = args.context ?? "general";
3166
+ const counterpartyDid = args.counterparty_did;
3167
+ const hasSanctuaryIdentity = identityManager.list().some(
3168
+ (id) => identityManager.get(id.identity_id)?.did === counterpartyDid
3169
+ );
3170
+ const tierMeta = resolveTier(counterpartyDid, hsResults, hasSanctuaryIdentity);
2512
3171
  const stored = await reputationStore.record(
2513
3172
  args.interaction_id,
2514
- args.counterparty_did,
3173
+ counterpartyDid,
2515
3174
  outcome,
2516
3175
  context,
2517
3176
  identity,
2518
3177
  identityEncryptionKey,
2519
- args.counterparty_attestation
3178
+ args.counterparty_attestation,
3179
+ tierMeta.sovereignty_tier
2520
3180
  );
2521
3181
  auditLog.append("l4", "reputation_record", identity.identity_id, {
2522
3182
  interaction_id: args.interaction_id,
2523
3183
  outcome_type: outcome.type,
2524
3184
  outcome_result: outcome.result,
2525
- context
3185
+ context,
3186
+ sovereignty_tier: tierMeta.sovereignty_tier
2526
3187
  });
2527
3188
  return toolResult({
2528
3189
  attestation_id: stored.attestation.attestation_id,
2529
3190
  interaction_id: stored.attestation.data.interaction_id,
2530
3191
  self_attestation: stored.attestation.signature,
2531
3192
  counterparty_confirmed: stored.counterparty_confirmed,
3193
+ sovereignty_tier: tierMeta.sovereignty_tier,
2532
3194
  context,
2533
3195
  recorded_at: stored.recorded_at
2534
3196
  });
@@ -2576,7 +3238,9 @@ function createL4Tools(storage, masterKey, identityManager, auditLog) {
2576
3238
  contexts: summary.contexts
2577
3239
  });
2578
3240
  return toolResult({
2579
- summary
3241
+ summary,
3242
+ // SEC-ADD-03: Tag response as containing counterparty-generated attestation data
3243
+ _content_trust: "external"
2580
3244
  });
2581
3245
  }
2582
3246
  },
@@ -2691,20 +3355,76 @@ function createL4Tools(storage, masterKey, identityManager, auditLog) {
2691
3355
  });
2692
3356
  }
2693
3357
  },
2694
- // ─── Trust Bootstrap: Escrow ──────────────────────────────────────
3358
+ // ─── Sovereignty-Weighted Query ──────────────────────────────────
2695
3359
  {
2696
- name: "sanctuary/bootstrap_create_escrow",
2697
- description: "Create an escrow record for trust bootstrapping. Allows new participants with no reputation to transact safely.",
3360
+ name: "sanctuary/reputation_query_weighted",
3361
+ description: "Query reputation with sovereignty-weighted scoring. Attestations from verified-sovereign agents carry full weight (1.0); unverified attestations carry reduced weight (0.2). Returns both the weighted score and tier distribution.",
2698
3362
  inputSchema: {
2699
3363
  type: "object",
2700
3364
  properties: {
2701
- transaction_terms: {
3365
+ metric: {
2702
3366
  type: "string",
2703
- description: "Description of the transaction"
3367
+ description: "Which metric to compute the weighted score for"
2704
3368
  },
2705
- collateral_amount: {
2706
- type: "number",
2707
- description: "Optional stake/collateral amount"
3369
+ context: {
3370
+ type: "string",
3371
+ description: "Filter by context/domain"
3372
+ },
3373
+ counterparty_did: {
3374
+ type: "string",
3375
+ description: "Filter by counterparty"
3376
+ }
3377
+ },
3378
+ required: ["metric"]
3379
+ },
3380
+ handler: async (args) => {
3381
+ const summary = await reputationStore.query({
3382
+ context: args.context,
3383
+ counterparty_did: args.counterparty_did
3384
+ });
3385
+ const allAttestations = await reputationStore.loadAllForTierScoring({
3386
+ context: args.context,
3387
+ counterparty_did: args.counterparty_did
3388
+ });
3389
+ const metric = args.metric;
3390
+ const tieredAttestations = allAttestations.filter((a) => a.attestation.data.metrics[metric] !== void 0).map((a) => ({
3391
+ value: a.attestation.data.metrics[metric],
3392
+ tier: a.attestation.data.sovereignty_tier ?? "unverified"
3393
+ }));
3394
+ const weightedScore = computeWeightedScore(tieredAttestations);
3395
+ const tiers = allAttestations.map(
3396
+ (a) => a.attestation.data.sovereignty_tier ?? "unverified"
3397
+ );
3398
+ const dist = tierDistribution(tiers);
3399
+ auditLog.append("l4", "reputation_query_weighted", "system", {
3400
+ metric,
3401
+ attestation_count: tieredAttestations.length,
3402
+ weighted_score: weightedScore
3403
+ });
3404
+ return toolResult({
3405
+ metric,
3406
+ weighted_score: weightedScore,
3407
+ attestation_count: tieredAttestations.length,
3408
+ tier_distribution: dist,
3409
+ tier_weights: TIER_WEIGHTS,
3410
+ unweighted_summary: summary
3411
+ });
3412
+ }
3413
+ },
3414
+ // ─── Trust Bootstrap: Escrow ──────────────────────────────────────
3415
+ {
3416
+ name: "sanctuary/bootstrap_create_escrow",
3417
+ description: "Create an escrow record for trust bootstrapping. Allows new participants with no reputation to transact safely.",
3418
+ inputSchema: {
3419
+ type: "object",
3420
+ properties: {
3421
+ transaction_terms: {
3422
+ type: "string",
3423
+ description: "Description of the transaction"
3424
+ },
3425
+ collateral_amount: {
3426
+ type: "number",
3427
+ description: "Optional stake/collateral amount"
2708
3428
  },
2709
3429
  counterparty_did: {
2710
3430
  type: "string",
@@ -2841,14 +3561,16 @@ var DEFAULT_TIER2 = {
2841
3561
  };
2842
3562
  var DEFAULT_CHANNEL = {
2843
3563
  type: "stderr",
2844
- timeout_seconds: 300,
2845
- auto_deny: true
3564
+ timeout_seconds: 300
3565
+ // SEC-002: auto_deny is not configurable. Timeout always denies.
3566
+ // Field omitted intentionally — all channels hardcode deny on timeout.
2846
3567
  };
2847
3568
  var DEFAULT_POLICY = {
2848
3569
  version: 1,
2849
3570
  tier1_always_approve: [
2850
3571
  "state_export",
2851
3572
  "state_import",
3573
+ "state_delete",
2852
3574
  "identity_rotate",
2853
3575
  "reputation_import",
2854
3576
  "bootstrap_provide_guarantee"
@@ -2858,7 +3580,6 @@ var DEFAULT_POLICY = {
2858
3580
  "state_read",
2859
3581
  "state_write",
2860
3582
  "state_list",
2861
- "state_delete",
2862
3583
  "identity_create",
2863
3584
  "identity_list",
2864
3585
  "identity_sign",
@@ -2876,7 +3597,22 @@ var DEFAULT_POLICY = {
2876
3597
  "monitor_audit_log",
2877
3598
  "manifest",
2878
3599
  "principal_policy_view",
2879
- "principal_baseline_view"
3600
+ "principal_baseline_view",
3601
+ "shr_generate",
3602
+ "shr_verify",
3603
+ "handshake_initiate",
3604
+ "handshake_respond",
3605
+ "handshake_complete",
3606
+ "handshake_status",
3607
+ "reputation_query_weighted",
3608
+ "federation_peers",
3609
+ "federation_trust_evaluate",
3610
+ "federation_status",
3611
+ "zk_commit",
3612
+ "zk_prove",
3613
+ "zk_verify",
3614
+ "zk_range_prove",
3615
+ "zk_range_verify"
2880
3616
  ],
2881
3617
  approval_channel: DEFAULT_CHANNEL
2882
3618
  };
@@ -2951,10 +3687,14 @@ function validatePolicy(raw) {
2951
3687
  ...raw.tier2_anomaly ?? {}
2952
3688
  },
2953
3689
  tier3_always_allow: raw.tier3_always_allow ?? DEFAULT_POLICY.tier3_always_allow,
2954
- approval_channel: {
2955
- ...DEFAULT_CHANNEL,
2956
- ...raw.approval_channel ?? {}
2957
- }
3690
+ approval_channel: (() => {
3691
+ const merged = {
3692
+ ...DEFAULT_CHANNEL,
3693
+ ...raw.approval_channel ?? {}
3694
+ };
3695
+ delete merged.auto_deny;
3696
+ return merged;
3697
+ })()
2958
3698
  };
2959
3699
  }
2960
3700
  function generateDefaultPolicyYaml() {
@@ -2971,6 +3711,7 @@ version: 1
2971
3711
  tier1_always_approve:
2972
3712
  - state_export
2973
3713
  - state_import
3714
+ - state_delete
2974
3715
  - identity_rotate
2975
3716
  - reputation_import
2976
3717
  - bootstrap_provide_guarantee
@@ -2992,7 +3733,6 @@ tier3_always_allow:
2992
3733
  - state_read
2993
3734
  - state_write
2994
3735
  - state_list
2995
- - state_delete
2996
3736
  - identity_create
2997
3737
  - identity_list
2998
3738
  - identity_sign
@@ -3011,13 +3751,28 @@ tier3_always_allow:
3011
3751
  - manifest
3012
3752
  - principal_policy_view
3013
3753
  - principal_baseline_view
3754
+ - shr_generate
3755
+ - shr_verify
3756
+ - handshake_initiate
3757
+ - handshake_respond
3758
+ - handshake_complete
3759
+ - handshake_status
3760
+ - reputation_query_weighted
3761
+ - federation_peers
3762
+ - federation_trust_evaluate
3763
+ - federation_status
3764
+ - zk_commit
3765
+ - zk_prove
3766
+ - zk_verify
3767
+ - zk_range_prove
3768
+ - zk_range_verify
3014
3769
 
3015
3770
  # \u2500\u2500\u2500 Approval Channel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3016
3771
  # How Sanctuary reaches you when approval is needed.
3772
+ # NOTE: Timeout always results in denial. This is not configurable (SEC-002).
3017
3773
  approval_channel:
3018
3774
  type: stderr
3019
3775
  timeout_seconds: 300
3020
- auto_deny: true
3021
3776
  `;
3022
3777
  }
3023
3778
  async function loadPrincipalPolicy(storagePath) {
@@ -3194,27 +3949,16 @@ var BaselineTracker = class {
3194
3949
 
3195
3950
  // src/principal-policy/approval-channel.ts
3196
3951
  var StderrApprovalChannel = class {
3197
- config;
3198
- constructor(config) {
3199
- this.config = config;
3952
+ constructor(_config) {
3200
3953
  }
3201
3954
  async requestApproval(request) {
3202
3955
  const prompt = this.formatPrompt(request);
3203
3956
  process.stderr.write(prompt + "\n");
3204
- await new Promise((resolve) => setTimeout(resolve, 100));
3205
- if (this.config.auto_deny) {
3206
- return {
3207
- decision: "deny",
3208
- decided_at: (/* @__PURE__ */ new Date()).toISOString(),
3209
- decided_by: "timeout"
3210
- };
3211
- } else {
3212
- return {
3213
- decision: "approve",
3214
- decided_at: (/* @__PURE__ */ new Date()).toISOString(),
3215
- decided_by: "auto"
3216
- };
3217
- }
3957
+ return {
3958
+ decision: "deny",
3959
+ decided_at: (/* @__PURE__ */ new Date()).toISOString(),
3960
+ decided_by: "stderr:non-interactive"
3961
+ };
3218
3962
  }
3219
3963
  formatPrompt(request) {
3220
3964
  const tierLabel = request.tier === 1 ? "Tier 1 \u2014 always requires approval" : "Tier 2 \u2014 behavioral anomaly detected";
@@ -3222,7 +3966,7 @@ var StderrApprovalChannel = class {
3222
3966
  return [
3223
3967
  "",
3224
3968
  "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
3225
- "\u2551 SANCTUARY: Approval Required \u2551",
3969
+ "\u2551 SANCTUARY: Operation Denied (non-interactive channel) \u2551",
3226
3970
  "\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563",
3227
3971
  `\u2551 Operation: ${request.operation.padEnd(50)}\u2551`,
3228
3972
  `\u2551 ${tierLabel.padEnd(62)}\u2551`,
@@ -3233,113 +3977,1391 @@ var StderrApprovalChannel = class {
3233
3977
  (line) => `\u2551 ${line.padEnd(60)}\u2551`
3234
3978
  ),
3235
3979
  "\u2551 \u2551",
3236
- this.config.auto_deny ? "\u2551 Auto-denying (configure approval_channel.auto_deny to change) \u2551" : "\u2551 Auto-approving (informational mode) \u2551",
3980
+ "\u2551 Denied: stderr channel cannot accept input (SEC-016) \u2551",
3981
+ "\u2551 Use dashboard or webhook channel for interactive approval. \u2551",
3237
3982
  "\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
3238
3983
  ""
3239
3984
  ].join("\n");
3240
3985
  }
3241
3986
  };
3242
3987
 
3243
- // src/principal-policy/gate.ts
3244
- var ApprovalGate = class {
3245
- policy;
3246
- baseline;
3247
- channel;
3248
- auditLog;
3249
- constructor(policy, baseline, channel, auditLog) {
3250
- this.policy = policy;
3251
- this.baseline = baseline;
3252
- this.channel = channel;
3253
- this.auditLog = auditLog;
3988
+ // src/principal-policy/dashboard-html.ts
3989
+ function generateDashboardHTML(options) {
3990
+ return `<!DOCTYPE html>
3991
+ <html lang="en">
3992
+ <head>
3993
+ <meta charset="utf-8">
3994
+ <meta name="viewport" content="width=device-width, initial-scale=1">
3995
+ <title>Sanctuary \u2014 Principal Dashboard</title>
3996
+ <style>
3997
+ :root {
3998
+ --bg: #0f1117;
3999
+ --bg-surface: #1a1d27;
4000
+ --bg-elevated: #242736;
4001
+ --border: #2e3244;
4002
+ --text: #e4e6f0;
4003
+ --text-muted: #8b8fa3;
4004
+ --accent: #6c8aff;
4005
+ --accent-hover: #839dff;
4006
+ --approve: #3ecf8e;
4007
+ --approve-hover: #5dd9a3;
4008
+ --deny: #f87171;
4009
+ --deny-hover: #fca5a5;
4010
+ --warning: #fbbf24;
4011
+ --tier1: #f87171;
4012
+ --tier2: #fbbf24;
4013
+ --tier3: #3ecf8e;
4014
+ --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
4015
+ --mono: "SF Mono", "Fira Code", "Cascadia Code", monospace;
4016
+ --radius: 8px;
4017
+ }
4018
+
4019
+ * { box-sizing: border-box; margin: 0; padding: 0; }
4020
+ body {
4021
+ font-family: var(--font);
4022
+ background: var(--bg);
4023
+ color: var(--text);
4024
+ line-height: 1.5;
4025
+ min-height: 100vh;
4026
+ }
4027
+
4028
+ /* Layout */
4029
+ .container { max-width: 960px; margin: 0 auto; padding: 24px 16px; }
4030
+
4031
+ header {
4032
+ display: flex; align-items: center; justify-content: space-between;
4033
+ padding-bottom: 20px; border-bottom: 1px solid var(--border);
4034
+ margin-bottom: 24px;
4035
+ }
4036
+ header h1 { font-size: 20px; font-weight: 600; letter-spacing: -0.3px; }
4037
+ header h1 span { color: var(--accent); }
4038
+ .status-badge {
4039
+ display: inline-flex; align-items: center; gap: 6px;
4040
+ font-size: 12px; color: var(--text-muted);
4041
+ padding: 4px 10px; border-radius: 12px;
4042
+ background: var(--bg-surface); border: 1px solid var(--border);
4043
+ }
4044
+ .status-dot {
4045
+ width: 8px; height: 8px; border-radius: 50%;
4046
+ background: var(--approve); animation: pulse 2s infinite;
4047
+ }
4048
+ .status-dot.disconnected { background: var(--deny); animation: none; }
4049
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
4050
+
4051
+ /* Tabs */
4052
+ .tabs {
4053
+ display: flex; gap: 2px; margin-bottom: 20px;
4054
+ background: var(--bg-surface); border-radius: var(--radius);
4055
+ padding: 3px; border: 1px solid var(--border);
4056
+ }
4057
+ .tab {
4058
+ flex: 1; padding: 8px 12px; text-align: center;
4059
+ font-size: 13px; font-weight: 500; cursor: pointer;
4060
+ border-radius: 6px; border: none; color: var(--text-muted);
4061
+ background: transparent; transition: all 0.15s;
4062
+ }
4063
+ .tab:hover { color: var(--text); }
4064
+ .tab.active { background: var(--bg-elevated); color: var(--text); }
4065
+ .tab .count {
4066
+ display: inline-flex; align-items: center; justify-content: center;
4067
+ min-width: 18px; height: 18px; padding: 0 5px;
4068
+ font-size: 11px; font-weight: 600; border-radius: 9px;
4069
+ margin-left: 6px;
4070
+ }
4071
+ .tab .count.alert { background: var(--deny); color: white; }
4072
+ .tab .count.muted { background: var(--border); color: var(--text-muted); }
4073
+
4074
+ /* Tab Content */
4075
+ .tab-content { display: none; }
4076
+ .tab-content.active { display: block; }
4077
+
4078
+ /* Pending Requests */
4079
+ .pending-empty {
4080
+ text-align: center; padding: 60px 20px; color: var(--text-muted);
4081
+ }
4082
+ .pending-empty .icon { font-size: 32px; margin-bottom: 12px; }
4083
+ .pending-empty p { font-size: 14px; }
4084
+
4085
+ .request-card {
4086
+ background: var(--bg-surface); border: 1px solid var(--border);
4087
+ border-radius: var(--radius); padding: 16px; margin-bottom: 12px;
4088
+ animation: slideIn 0.2s ease-out;
4089
+ }
4090
+ @keyframes slideIn { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } }
4091
+ .request-card.tier1 { border-left: 3px solid var(--tier1); }
4092
+ .request-card.tier2 { border-left: 3px solid var(--tier2); }
4093
+ .request-header {
4094
+ display: flex; align-items: center; justify-content: space-between;
4095
+ margin-bottom: 10px;
4096
+ }
4097
+ .request-op {
4098
+ font-family: var(--mono); font-size: 14px; font-weight: 600;
4099
+ }
4100
+ .tier-badge {
4101
+ font-size: 11px; font-weight: 600; padding: 2px 8px;
4102
+ border-radius: 4px; text-transform: uppercase;
4103
+ }
4104
+ .tier-badge.tier1 { background: rgba(248,113,113,0.15); color: var(--tier1); }
4105
+ .tier-badge.tier2 { background: rgba(251,191,36,0.15); color: var(--tier2); }
4106
+ .request-reason {
4107
+ font-size: 13px; color: var(--text-muted); margin-bottom: 12px;
4108
+ }
4109
+ .request-context {
4110
+ font-family: var(--mono); font-size: 12px; color: var(--text-muted);
4111
+ background: var(--bg); border-radius: 4px; padding: 8px 10px;
4112
+ margin-bottom: 14px; white-space: pre-wrap; word-break: break-all;
4113
+ max-height: 120px; overflow-y: auto;
4114
+ }
4115
+ .request-actions {
4116
+ display: flex; align-items: center; gap: 10px;
4117
+ }
4118
+ .btn {
4119
+ padding: 7px 16px; border-radius: 6px; font-size: 13px;
4120
+ font-weight: 600; border: none; cursor: pointer;
4121
+ transition: all 0.15s;
4122
+ }
4123
+ .btn-approve { background: var(--approve); color: #0f1117; }
4124
+ .btn-approve:hover { background: var(--approve-hover); }
4125
+ .btn-deny { background: var(--deny); color: white; }
4126
+ .btn-deny:hover { background: var(--deny-hover); }
4127
+ .countdown {
4128
+ margin-left: auto; font-size: 12px; color: var(--text-muted);
4129
+ font-family: var(--mono);
4130
+ }
4131
+ .countdown.urgent { color: var(--deny); font-weight: 600; }
4132
+
4133
+ /* Audit Log */
4134
+ .audit-table { width: 100%; border-collapse: collapse; }
4135
+ .audit-table th {
4136
+ text-align: left; font-size: 11px; font-weight: 600;
4137
+ text-transform: uppercase; letter-spacing: 0.5px;
4138
+ color: var(--text-muted); padding: 8px 10px;
4139
+ border-bottom: 1px solid var(--border);
4140
+ }
4141
+ .audit-table td {
4142
+ font-size: 13px; padding: 8px 10px;
4143
+ border-bottom: 1px solid var(--border);
4144
+ }
4145
+ .audit-table tr { transition: background 0.1s; }
4146
+ .audit-table tr:hover { background: var(--bg-elevated); }
4147
+ .audit-table tr.new { animation: highlight 1s ease-out; }
4148
+ @keyframes highlight { from { background: rgba(108,138,255,0.15); } to { background: transparent; } }
4149
+ .audit-time { font-family: var(--mono); font-size: 12px; color: var(--text-muted); }
4150
+ .audit-op { font-family: var(--mono); font-size: 12px; }
4151
+ .audit-layer {
4152
+ font-size: 11px; font-weight: 600; padding: 1px 6px;
4153
+ border-radius: 3px; text-transform: uppercase;
4154
+ }
4155
+ .audit-layer.l1 { background: rgba(108,138,255,0.15); color: var(--accent); }
4156
+ .audit-layer.l2 { background: rgba(251,191,36,0.15); color: var(--tier2); }
4157
+ .audit-layer.l3 { background: rgba(62,207,142,0.15); color: var(--tier3); }
4158
+ .audit-layer.l4 { background: rgba(168,85,247,0.15); color: #a855f7; }
4159
+
4160
+ /* Baseline & Policy */
4161
+ .info-section {
4162
+ background: var(--bg-surface); border: 1px solid var(--border);
4163
+ border-radius: var(--radius); padding: 16px; margin-bottom: 16px;
4164
+ }
4165
+ .info-section h3 {
4166
+ font-size: 13px; font-weight: 600; text-transform: uppercase;
4167
+ letter-spacing: 0.5px; color: var(--text-muted); margin-bottom: 12px;
4168
+ }
4169
+ .info-row {
4170
+ display: flex; justify-content: space-between; align-items: center;
4171
+ padding: 6px 0; font-size: 13px;
4172
+ }
4173
+ .info-label { color: var(--text-muted); }
4174
+ .info-value { font-family: var(--mono); font-size: 12px; }
4175
+ .tag-list { display: flex; flex-wrap: wrap; gap: 4px; }
4176
+ .tag {
4177
+ font-family: var(--mono); font-size: 11px; padding: 2px 8px;
4178
+ background: var(--bg-elevated); border-radius: 4px;
4179
+ color: var(--text-muted); border: 1px solid var(--border);
4180
+ }
4181
+ .policy-op {
4182
+ font-family: var(--mono); font-size: 12px; padding: 3px 0;
4183
+ }
4184
+
4185
+ /* Footer */
4186
+ footer {
4187
+ margin-top: 32px; padding-top: 16px;
4188
+ border-top: 1px solid var(--border);
4189
+ font-size: 12px; color: var(--text-muted);
4190
+ text-align: center;
4191
+ }
4192
+ </style>
4193
+ </head>
4194
+ <body>
4195
+ <div class="container">
4196
+ <header>
4197
+ <h1><span>Sanctuary</span> Principal Dashboard</h1>
4198
+ <div class="status-badge">
4199
+ <div class="status-dot" id="statusDot"></div>
4200
+ <span id="statusText">Connected</span>
4201
+ </div>
4202
+ </header>
4203
+
4204
+ <div class="tabs">
4205
+ <button class="tab active" data-tab="pending">
4206
+ Pending<span class="count muted" id="pendingCount">0</span>
4207
+ </button>
4208
+ <button class="tab" data-tab="audit">
4209
+ Audit Log<span class="count muted" id="auditCount">0</span>
4210
+ </button>
4211
+ <button class="tab" data-tab="baseline">Baseline</button>
4212
+ <button class="tab" data-tab="policy">Policy</button>
4213
+ </div>
4214
+
4215
+ <!-- Pending Approvals -->
4216
+ <div class="tab-content active" id="tab-pending">
4217
+ <div class="pending-empty" id="pendingEmpty">
4218
+ <div class="icon">&#x2714;</div>
4219
+ <p>No pending approval requests.</p>
4220
+ <p style="font-size:12px; margin-top:4px;">Requests will appear here in real time.</p>
4221
+ </div>
4222
+ <div id="pendingList"></div>
4223
+ </div>
4224
+
4225
+ <!-- Audit Log -->
4226
+ <div class="tab-content" id="tab-audit">
4227
+ <table class="audit-table">
4228
+ <thead>
4229
+ <tr><th>Time</th><th>Layer</th><th>Operation</th><th>Identity</th></tr>
4230
+ </thead>
4231
+ <tbody id="auditBody"></tbody>
4232
+ </table>
4233
+ </div>
4234
+
4235
+ <!-- Baseline -->
4236
+ <div class="tab-content" id="tab-baseline">
4237
+ <div class="info-section">
4238
+ <h3>Session Info</h3>
4239
+ <div class="info-row"><span class="info-label">First session</span><span class="info-value" id="bFirstSession">\u2014</span></div>
4240
+ <div class="info-row"><span class="info-label">Started</span><span class="info-value" id="bStarted">\u2014</span></div>
4241
+ </div>
4242
+ <div class="info-section">
4243
+ <h3>Known Namespaces</h3>
4244
+ <div class="tag-list" id="bNamespaces"><span class="tag">\u2014</span></div>
4245
+ </div>
4246
+ <div class="info-section">
4247
+ <h3>Known Counterparties</h3>
4248
+ <div class="tag-list" id="bCounterparties"><span class="tag">\u2014</span></div>
4249
+ </div>
4250
+ <div class="info-section">
4251
+ <h3>Tool Call Counts</h3>
4252
+ <div id="bToolCalls"><span class="info-value">\u2014</span></div>
4253
+ </div>
4254
+ </div>
4255
+
4256
+ <!-- Policy -->
4257
+ <div class="tab-content" id="tab-policy">
4258
+ <div class="info-section">
4259
+ <h3>Tier 1 \u2014 Always Requires Approval</h3>
4260
+ <div id="pTier1"></div>
4261
+ </div>
4262
+ <div class="info-section">
4263
+ <h3>Tier 2 \u2014 Anomaly Detection</h3>
4264
+ <div id="pTier2"></div>
4265
+ </div>
4266
+ <div class="info-section">
4267
+ <h3>Tier 3 \u2014 Always Allowed</h3>
4268
+ <div class="info-row">
4269
+ <span class="info-label">Operations</span>
4270
+ <span class="info-value" id="pTier3Count">\u2014</span>
4271
+ </div>
4272
+ </div>
4273
+ <div class="info-section">
4274
+ <h3>Approval Channel</h3>
4275
+ <div id="pChannel"></div>
4276
+ </div>
4277
+ </div>
4278
+
4279
+ <footer>Sanctuary Framework v${options.serverVersion} \u2014 Principal Dashboard</footer>
4280
+ </div>
4281
+
4282
+ <script>
4283
+ (function() {
4284
+ const TIMEOUT = ${options.timeoutSeconds};
4285
+ // SEC-012: Auth token is passed via Authorization header only \u2014 never in URLs.
4286
+ // The token is provided by the server at generation time (embedded for initial auth).
4287
+ const AUTH_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
4288
+ let SESSION_ID = null; // Short-lived session for SSE and URL-based requests
4289
+ const pending = new Map();
4290
+ let auditCount = 0;
4291
+
4292
+ // Auth helpers \u2014 SEC-012: token goes in header, session goes in URL
4293
+ function authHeaders() {
4294
+ const h = { 'Content-Type': 'application/json' };
4295
+ if (AUTH_TOKEN) h['Authorization'] = 'Bearer ' + AUTH_TOKEN;
4296
+ return h;
4297
+ }
4298
+ function sessionQuery(url) {
4299
+ if (!SESSION_ID) return url;
4300
+ const sep = url.includes('?') ? '&' : '?';
4301
+ return url + sep + 'session=' + SESSION_ID;
4302
+ }
4303
+
4304
+ // SEC-012: Exchange the long-lived token for a short-lived session
4305
+ async function exchangeSession() {
4306
+ if (!AUTH_TOKEN) return;
4307
+ try {
4308
+ const resp = await fetch('/auth/session', { method: 'POST', headers: authHeaders() });
4309
+ if (resp.ok) {
4310
+ const data = await resp.json();
4311
+ SESSION_ID = data.session_id;
4312
+ // Refresh session before expiry (at 80% of TTL)
4313
+ const refreshMs = (data.expires_in_seconds || 300) * 800;
4314
+ setTimeout(async () => { await exchangeSession(); reconnectSSE(); }, refreshMs);
4315
+ }
4316
+ } catch(e) { /* will retry on next connect */ }
4317
+ }
4318
+
4319
+ // Tab switching
4320
+ document.querySelectorAll('.tab').forEach(tab => {
4321
+ tab.addEventListener('click', () => {
4322
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
4323
+ document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
4324
+ tab.classList.add('active');
4325
+ document.getElementById('tab-' + tab.dataset.tab).classList.add('active');
4326
+ });
4327
+ });
4328
+
4329
+ // SSE Connection \u2014 SEC-012: uses short-lived session token in URL, not auth token
4330
+ let evtSource;
4331
+ function reconnectSSE() {
4332
+ if (evtSource) { evtSource.close(); }
4333
+ connect();
4334
+ }
4335
+ function connect() {
4336
+ evtSource = new EventSource(sessionQuery('/events'));
4337
+ evtSource.onopen = () => {
4338
+ document.getElementById('statusDot').classList.remove('disconnected');
4339
+ document.getElementById('statusText').textContent = 'Connected';
4340
+ };
4341
+ evtSource.onerror = () => {
4342
+ document.getElementById('statusDot').classList.add('disconnected');
4343
+ document.getElementById('statusText').textContent = 'Reconnecting...';
4344
+ };
4345
+ evtSource.addEventListener('pending-request', (e) => {
4346
+ const data = JSON.parse(e.data);
4347
+ addPendingRequest(data);
4348
+ });
4349
+ evtSource.addEventListener('request-resolved', (e) => {
4350
+ const data = JSON.parse(e.data);
4351
+ removePendingRequest(data.request_id);
4352
+ });
4353
+ evtSource.addEventListener('audit-entry', (e) => {
4354
+ const data = JSON.parse(e.data);
4355
+ addAuditEntry(data);
4356
+ });
4357
+ evtSource.addEventListener('baseline-update', (e) => {
4358
+ const data = JSON.parse(e.data);
4359
+ updateBaseline(data);
4360
+ });
4361
+ evtSource.addEventListener('policy-update', (e) => {
4362
+ const data = JSON.parse(e.data);
4363
+ updatePolicy(data);
4364
+ });
4365
+ evtSource.addEventListener('init', (e) => {
4366
+ const data = JSON.parse(e.data);
4367
+ if (data.baseline) updateBaseline(data.baseline);
4368
+ if (data.policy) updatePolicy(data.policy);
4369
+ if (data.pending) data.pending.forEach(addPendingRequest);
4370
+ if (data.audit) data.audit.forEach(addAuditEntry);
4371
+ });
4372
+ }
4373
+
4374
+ // Pending requests
4375
+ function addPendingRequest(req) {
4376
+ pending.set(req.request_id, { ...req, remaining: TIMEOUT });
4377
+ renderPending();
4378
+ updatePendingCount();
4379
+ flashTab('pending');
4380
+ }
4381
+
4382
+ function removePendingRequest(id) {
4383
+ pending.delete(id);
4384
+ renderPending();
4385
+ updatePendingCount();
4386
+ }
4387
+
4388
+ function renderPending() {
4389
+ const list = document.getElementById('pendingList');
4390
+ const empty = document.getElementById('pendingEmpty');
4391
+ if (pending.size === 0) {
4392
+ list.innerHTML = '';
4393
+ empty.style.display = 'block';
4394
+ return;
4395
+ }
4396
+ empty.style.display = 'none';
4397
+ list.innerHTML = '';
4398
+ for (const [id, req] of pending) {
4399
+ const card = document.createElement('div');
4400
+ card.className = 'request-card tier' + req.tier;
4401
+ card.id = 'req-' + id;
4402
+ const ctx = typeof req.context === 'string' ? req.context : JSON.stringify(req.context, null, 2);
4403
+ card.innerHTML =
4404
+ '<div class="request-header">' +
4405
+ '<span class="request-op">' + esc(req.operation) + '</span>' +
4406
+ '<span class="tier-badge tier' + req.tier + '">Tier ' + req.tier + '</span>' +
4407
+ '</div>' +
4408
+ '<div class="request-reason">' + esc(req.reason) + '</div>' +
4409
+ '<div class="request-context">' + esc(ctx) + '</div>' +
4410
+ '<div class="request-actions">' +
4411
+ '<button class="btn btn-approve" onclick="handleApprove(\\'' + id + '\\')">Approve</button>' +
4412
+ '<button class="btn btn-deny" onclick="handleDeny(\\'' + id + '\\')">Deny</button>' +
4413
+ '<span class="countdown" id="cd-' + id + '">' + req.remaining + 's</span>' +
4414
+ '</div>';
4415
+ list.appendChild(card);
4416
+ }
4417
+ }
4418
+
4419
+ function updatePendingCount() {
4420
+ const el = document.getElementById('pendingCount');
4421
+ el.textContent = pending.size;
4422
+ el.className = pending.size > 0 ? 'count alert' : 'count muted';
4423
+ }
4424
+
4425
+ function flashTab(name) {
4426
+ const tab = document.querySelector('[data-tab="' + name + '"]');
4427
+ if (!tab.classList.contains('active')) {
4428
+ tab.style.background = 'rgba(248,113,113,0.15)';
4429
+ setTimeout(() => { tab.style.background = ''; }, 1500);
4430
+ }
4431
+ }
4432
+
4433
+ // Countdown timer
4434
+ setInterval(() => {
4435
+ for (const [id, req] of pending) {
4436
+ req.remaining = Math.max(0, req.remaining - 1);
4437
+ const el = document.getElementById('cd-' + id);
4438
+ if (el) {
4439
+ el.textContent = req.remaining + 's';
4440
+ el.className = req.remaining <= 30 ? 'countdown urgent' : 'countdown';
4441
+ }
4442
+ }
4443
+ }, 1000);
4444
+
4445
+ // Approve / Deny handlers (global scope)
4446
+ window.handleApprove = function(id) {
4447
+ fetch('/api/approve/' + id, { method: 'POST', headers: authHeaders() }).then(() => {
4448
+ removePendingRequest(id);
4449
+ });
4450
+ };
4451
+ window.handleDeny = function(id) {
4452
+ fetch('/api/deny/' + id, { method: 'POST', headers: authHeaders() }).then(() => {
4453
+ removePendingRequest(id);
4454
+ });
4455
+ };
4456
+
4457
+ // Audit log
4458
+ function addAuditEntry(entry) {
4459
+ auditCount++;
4460
+ document.getElementById('auditCount').textContent = auditCount;
4461
+ const tbody = document.getElementById('auditBody');
4462
+ const tr = document.createElement('tr');
4463
+ tr.className = 'new';
4464
+ const time = entry.timestamp ? new Date(entry.timestamp).toLocaleTimeString() : '\u2014';
4465
+ const layer = entry.layer || '\u2014';
4466
+ tr.innerHTML =
4467
+ '<td class="audit-time">' + esc(time) + '</td>' +
4468
+ '<td><span class="audit-layer ' + layer + '">' + esc(layer) + '</span></td>' +
4469
+ '<td class="audit-op">' + esc(entry.operation || '\u2014') + '</td>' +
4470
+ '<td style="font-size:12px;color:var(--text-muted)">' + esc(entry.identity_id || '\u2014') + '</td>';
4471
+ tbody.insertBefore(tr, tbody.firstChild);
4472
+ // Keep last 100 entries
4473
+ while (tbody.children.length > 100) tbody.removeChild(tbody.lastChild);
4474
+ }
4475
+
4476
+ // Baseline
4477
+ function updateBaseline(b) {
4478
+ if (!b) return;
4479
+ document.getElementById('bFirstSession').textContent = b.is_first_session ? 'Yes' : 'No';
4480
+ document.getElementById('bStarted').textContent = b.started_at ? new Date(b.started_at).toLocaleString() : '\u2014';
4481
+ const ns = document.getElementById('bNamespaces');
4482
+ ns.innerHTML = (b.known_namespaces || []).length > 0
4483
+ ? (b.known_namespaces || []).map(n => '<span class="tag">' + esc(n) + '</span>').join('')
4484
+ : '<span class="tag">none</span>';
4485
+ const cp = document.getElementById('bCounterparties');
4486
+ cp.innerHTML = (b.known_counterparties || []).length > 0
4487
+ ? (b.known_counterparties || []).map(c => '<span class="tag">' + esc(c.slice(0,16)) + '...</span>').join('')
4488
+ : '<span class="tag">none</span>';
4489
+ const tc = document.getElementById('bToolCalls');
4490
+ const counts = b.tool_call_counts || {};
4491
+ const entries = Object.entries(counts).sort((a,b) => b[1] - a[1]);
4492
+ tc.innerHTML = entries.length > 0
4493
+ ? entries.map(([k,v]) => '<div class="info-row"><span class="info-label">' + esc(k) + '</span><span class="info-value">' + v + '</span></div>').join('')
4494
+ : '<span class="info-value">no calls yet</span>';
4495
+ }
4496
+
4497
+ // Policy
4498
+ function updatePolicy(p) {
4499
+ if (!p) return;
4500
+ const t1 = document.getElementById('pTier1');
4501
+ t1.innerHTML = (p.tier1_always_approve || []).map(op =>
4502
+ '<div class="policy-op">' + esc(op) + '</div>'
4503
+ ).join('');
4504
+ const t2 = document.getElementById('pTier2');
4505
+ const cfg = p.tier2_anomaly || {};
4506
+ t2.innerHTML = Object.entries(cfg).map(([k,v]) =>
4507
+ '<div class="info-row"><span class="info-label">' + esc(k) + '</span><span class="info-value">' + esc(String(v)) + '</span></div>'
4508
+ ).join('');
4509
+ document.getElementById('pTier3Count').textContent = (p.tier3_always_allow || []).length + ' operations';
4510
+ const ch = document.getElementById('pChannel');
4511
+ const chan = p.approval_channel || {};
4512
+ ch.innerHTML = Object.entries(chan).filter(([k]) => k !== 'webhook_secret').map(([k,v]) =>
4513
+ '<div class="info-row"><span class="info-label">' + esc(k) + '</span><span class="info-value">' + esc(String(v)) + '</span></div>'
4514
+ ).join('');
4515
+ }
4516
+
4517
+ function esc(s) {
4518
+ if (!s) return '';
4519
+ const d = document.createElement('div');
4520
+ d.textContent = String(s);
4521
+ return d.innerHTML;
4522
+ }
4523
+
4524
+ // Init \u2014 SEC-012: exchange token for session before connecting SSE
4525
+ (async function init() {
4526
+ await exchangeSession();
4527
+ // Clean token from URL if present (legacy bookmarks)
4528
+ if (window.location.search.includes('token=')) {
4529
+ const clean = window.location.pathname;
4530
+ window.history.replaceState({}, '', clean);
4531
+ }
4532
+ connect();
4533
+ fetch('/api/status', { headers: authHeaders() }).then(r => r.json()).then(data => {
4534
+ if (data.baseline) updateBaseline(data.baseline);
4535
+ if (data.policy) updatePolicy(data.policy);
4536
+ }).catch(() => {});
4537
+ })();
4538
+ })();
4539
+ </script>
4540
+ </body>
4541
+ </html>`;
4542
+ }
4543
+
4544
+ // src/principal-policy/dashboard.ts
4545
+ var SESSION_TTL_MS = 5 * 60 * 1e3;
4546
+ var MAX_SESSIONS = 1e3;
4547
+ var DashboardApprovalChannel = class {
4548
+ config;
4549
+ pending = /* @__PURE__ */ new Map();
4550
+ sseClients = /* @__PURE__ */ new Set();
4551
+ httpServer = null;
4552
+ policy = null;
4553
+ baseline = null;
4554
+ auditLog = null;
4555
+ dashboardHTML;
4556
+ authToken;
4557
+ useTLS;
4558
+ /** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
4559
+ sessions = /* @__PURE__ */ new Map();
4560
+ sessionCleanupTimer = null;
4561
+ constructor(config) {
4562
+ this.config = config;
4563
+ this.authToken = config.auth_token;
4564
+ this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
4565
+ this.dashboardHTML = generateDashboardHTML({
4566
+ timeoutSeconds: config.timeout_seconds,
4567
+ serverVersion: "0.3.0",
4568
+ authToken: this.authToken
4569
+ });
4570
+ this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
3254
4571
  }
3255
4572
  /**
3256
- * Evaluate a tool call against the Principal Policy.
3257
- *
3258
- * @param toolName - Full MCP tool name (e.g., "sanctuary/state_export")
3259
- * @param args - Tool call arguments (for context extraction)
3260
- * @returns GateResult indicating whether the call is allowed
4573
+ * Inject dependencies after construction.
4574
+ * Called from index.ts after all components are initialized.
3261
4575
  */
3262
- async evaluate(toolName, args) {
3263
- const operation = extractOperationName(toolName);
3264
- this.baseline.recordToolCall(operation);
3265
- if (this.policy.tier1_always_approve.includes(operation)) {
3266
- return this.requestApproval(operation, 1, `"${operation}" is a Tier 1 operation (always requires approval)`, {
3267
- operation,
3268
- args_summary: this.summarizeArgs(args)
4576
+ setDependencies(deps) {
4577
+ this.policy = deps.policy;
4578
+ this.baseline = deps.baseline;
4579
+ this.auditLog = deps.auditLog;
4580
+ }
4581
+ /**
4582
+ * Start the HTTP(S) server for the dashboard.
4583
+ */
4584
+ async start() {
4585
+ return new Promise((resolve, reject) => {
4586
+ const handler = (req, res) => this.handleRequest(req, res);
4587
+ if (this.useTLS && this.config.tls) {
4588
+ const tlsOpts = {
4589
+ cert: readFileSync(this.config.tls.cert_path),
4590
+ key: readFileSync(this.config.tls.key_path)
4591
+ };
4592
+ this.httpServer = createServer$1(tlsOpts, handler);
4593
+ } else {
4594
+ this.httpServer = createServer$2(handler);
4595
+ }
4596
+ const protocol = this.useTLS ? "https" : "http";
4597
+ const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
4598
+ this.httpServer.listen(this.config.port, this.config.host, () => {
4599
+ if (this.authToken) {
4600
+ const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
4601
+ process.stderr.write(
4602
+ `
4603
+ Sanctuary Principal Dashboard: ${baseUrl}
4604
+ `
4605
+ );
4606
+ process.stderr.write(
4607
+ ` Auth required (token: ${hint}). Use Authorization: Bearer <TOKEN> header.
4608
+
4609
+ `
4610
+ );
4611
+ } else {
4612
+ process.stderr.write(
4613
+ `
4614
+ Sanctuary Principal Dashboard: ${baseUrl}
4615
+
4616
+ `
4617
+ );
4618
+ }
4619
+ resolve();
4620
+ });
4621
+ this.httpServer.on("error", reject);
4622
+ });
4623
+ }
4624
+ /**
4625
+ * Stop the HTTP server and clean up.
4626
+ */
4627
+ async stop() {
4628
+ for (const [, pending] of this.pending) {
4629
+ clearTimeout(pending.timer);
4630
+ pending.resolve({
4631
+ decision: "deny",
4632
+ decided_at: (/* @__PURE__ */ new Date()).toISOString(),
4633
+ decided_by: "auto"
3269
4634
  });
3270
4635
  }
3271
- const anomaly = this.detectAnomaly(operation, args);
3272
- if (anomaly) {
3273
- return this.requestApproval(operation, 2, anomaly.reason, anomaly.context);
4636
+ this.pending.clear();
4637
+ for (const client of this.sseClients) {
4638
+ client.end();
4639
+ }
4640
+ this.sseClients.clear();
4641
+ this.sessions.clear();
4642
+ if (this.sessionCleanupTimer) {
4643
+ clearInterval(this.sessionCleanupTimer);
4644
+ this.sessionCleanupTimer = null;
4645
+ }
4646
+ if (this.httpServer) {
4647
+ return new Promise((resolve) => {
4648
+ this.httpServer.close(() => resolve());
4649
+ });
3274
4650
  }
3275
- this.auditLog.append("l2", `gate_allow:${operation}`, "system", {
3276
- tier: 3,
3277
- operation
3278
- });
3279
- return {
3280
- allowed: true,
3281
- tier: 3,
3282
- reason: "Operation allowed (Tier 3)",
3283
- approval_required: false
3284
- };
3285
4651
  }
3286
4652
  /**
3287
- * Detect Tier 2 behavioral anomalies.
4653
+ * Request approval from the human via the dashboard.
4654
+ * Blocks until the human approves/denies or timeout occurs.
3288
4655
  */
3289
- detectAnomaly(operation, args) {
3290
- const config = this.policy.tier2_anomaly;
3291
- if (this.baseline.isFirstSession && config.first_session_policy === "approve") {
3292
- if (!this.policy.tier3_always_allow.includes(operation)) {
3293
- return {
3294
- reason: `First session: "${operation}" has no established baseline`,
3295
- context: { operation, is_first_session: true }
4656
+ async requestApproval(request) {
4657
+ const id = randomBytes$1(8).toString("hex");
4658
+ process.stderr.write(
4659
+ `[Sanctuary] Approval required: ${request.operation} (Tier ${request.tier}) \u2014 open dashboard to respond
4660
+ `
4661
+ );
4662
+ return new Promise((resolve) => {
4663
+ const timer = setTimeout(() => {
4664
+ this.pending.delete(id);
4665
+ const response = {
4666
+ // SEC-002: Timeout ALWAYS denies. No configuration can change this.
4667
+ decision: "deny",
4668
+ decided_at: (/* @__PURE__ */ new Date()).toISOString(),
4669
+ decided_by: "timeout"
3296
4670
  };
4671
+ this.broadcastSSE("request-resolved", {
4672
+ request_id: id,
4673
+ decision: response.decision,
4674
+ decided_by: "timeout"
4675
+ });
4676
+ resolve(response);
4677
+ }, this.config.timeout_seconds * 1e3);
4678
+ const pending = {
4679
+ id,
4680
+ request,
4681
+ resolve,
4682
+ timer,
4683
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
4684
+ };
4685
+ this.pending.set(id, pending);
4686
+ this.broadcastSSE("pending-request", {
4687
+ request_id: id,
4688
+ operation: request.operation,
4689
+ tier: request.tier,
4690
+ reason: request.reason,
4691
+ context: request.context,
4692
+ timestamp: request.timestamp
4693
+ });
4694
+ });
4695
+ }
4696
+ // ── Authentication ──────────────────────────────────────────────────
4697
+ /**
4698
+ * Verify bearer token authentication.
4699
+ *
4700
+ * SEC-012: The long-lived auth token is ONLY accepted via the Authorization
4701
+ * header — never in URL query strings. For SSE and page loads that cannot
4702
+ * set headers, a short-lived session token (obtained via POST /auth/session)
4703
+ * is accepted via ?session= query parameter.
4704
+ *
4705
+ * Returns true if auth passes, false if blocked (response already sent).
4706
+ */
4707
+ checkAuth(req, url, res) {
4708
+ if (!this.authToken) return true;
4709
+ const authHeader = req.headers.authorization;
4710
+ if (authHeader) {
4711
+ const parts = authHeader.split(" ");
4712
+ if (parts.length === 2 && parts[0] === "Bearer" && parts[1] === this.authToken) {
4713
+ return true;
3297
4714
  }
3298
4715
  }
3299
- if (config.new_namespace_access === "approve") {
3300
- const namespace = args.namespace;
3301
- if (namespace) {
3302
- const isNew = this.baseline.recordNamespaceAccess(namespace);
3303
- if (isNew) {
3304
- return {
3305
- reason: `First access to namespace "${namespace}" (not in session baseline)`,
3306
- context: {
3307
- operation,
3308
- namespace,
3309
- known_namespaces: this.baseline.getProfile().known_namespaces
3310
- }
3311
- };
3312
- }
3313
- }
3314
- } else if (config.new_namespace_access === "log") {
3315
- const namespace = args.namespace;
3316
- if (namespace) {
3317
- this.baseline.recordNamespaceAccess(namespace);
3318
- }
4716
+ const sessionId = url.searchParams.get("session");
4717
+ if (sessionId && this.validateSession(sessionId)) {
4718
+ return true;
3319
4719
  }
3320
- if (config.new_counterparty === "approve") {
3321
- const counterpartyDid = args.counterparty_did ?? args.agent_identity_id;
3322
- if (counterpartyDid) {
3323
- const isNew = this.baseline.recordCounterparty(counterpartyDid);
3324
- if (isNew) {
3325
- return {
3326
- reason: `First interaction with counterparty "${counterpartyDid}"`,
3327
- context: {
3328
- operation,
3329
- counterparty_did: counterpartyDid,
3330
- known_counterparties: this.baseline.getProfile().known_counterparties
3331
- }
3332
- };
3333
- }
3334
- }
3335
- } else if (config.new_counterparty === "log") {
3336
- const counterpartyDid = args.counterparty_did;
3337
- if (counterpartyDid) {
3338
- this.baseline.recordCounterparty(counterpartyDid);
4720
+ res.writeHead(401, { "Content-Type": "application/json" });
4721
+ res.end(JSON.stringify({ error: "Unauthorized \u2014 use Authorization: Bearer header or a valid session" }));
4722
+ return false;
4723
+ }
4724
+ // ── Session Management (SEC-012) ──────────────────────────────────
4725
+ /**
4726
+ * Create a short-lived session by exchanging the long-lived auth token
4727
+ * (provided in the Authorization header) for a session ID.
4728
+ */
4729
+ createSession() {
4730
+ if (this.sessions.size >= MAX_SESSIONS) {
4731
+ this.cleanupSessions();
4732
+ if (this.sessions.size >= MAX_SESSIONS) {
4733
+ const oldest = [...this.sessions.entries()].sort(
4734
+ (a, b) => a[1].created_at - b[1].created_at
4735
+ )[0];
4736
+ if (oldest) this.sessions.delete(oldest[0]);
3339
4737
  }
3340
4738
  }
3341
- if (operation === "identity_sign") {
3342
- const signCount = this.baseline.recordSign();
4739
+ const id = randomBytes$1(32).toString("hex");
4740
+ const now = Date.now();
4741
+ this.sessions.set(id, {
4742
+ id,
4743
+ created_at: now,
4744
+ expires_at: now + SESSION_TTL_MS
4745
+ });
4746
+ return id;
4747
+ }
4748
+ /**
4749
+ * Validate a session ID — must exist and not be expired.
4750
+ */
4751
+ validateSession(sessionId) {
4752
+ const session = this.sessions.get(sessionId);
4753
+ if (!session) return false;
4754
+ if (Date.now() > session.expires_at) {
4755
+ this.sessions.delete(sessionId);
4756
+ return false;
4757
+ }
4758
+ return true;
4759
+ }
4760
+ /**
4761
+ * Remove all expired sessions.
4762
+ */
4763
+ cleanupSessions() {
4764
+ const now = Date.now();
4765
+ for (const [id, session] of this.sessions) {
4766
+ if (now > session.expires_at) {
4767
+ this.sessions.delete(id);
4768
+ }
4769
+ }
4770
+ }
4771
+ // ── HTTP Request Handler ────────────────────────────────────────────
4772
+ handleRequest(req, res) {
4773
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
4774
+ const method = req.method ?? "GET";
4775
+ const origin = req.headers.origin;
4776
+ const protocol = this.useTLS ? "https" : "http";
4777
+ const selfOrigin = `${protocol}://${this.config.host}:${this.config.port}`;
4778
+ if (origin === selfOrigin) {
4779
+ res.setHeader("Access-Control-Allow-Origin", origin);
4780
+ }
4781
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
4782
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
4783
+ if (method === "OPTIONS") {
4784
+ res.writeHead(204);
4785
+ res.end();
4786
+ return;
4787
+ }
4788
+ if (!this.checkAuth(req, url, res)) return;
4789
+ try {
4790
+ if (method === "POST" && url.pathname === "/auth/session") {
4791
+ this.handleSessionExchange(req, res);
4792
+ return;
4793
+ }
4794
+ if (method === "GET" && url.pathname === "/") {
4795
+ this.serveDashboard(res);
4796
+ } else if (method === "GET" && url.pathname === "/events") {
4797
+ this.handleSSE(req, res);
4798
+ } else if (method === "GET" && url.pathname === "/api/status") {
4799
+ this.handleStatus(res);
4800
+ } else if (method === "GET" && url.pathname === "/api/pending") {
4801
+ this.handlePendingList(res);
4802
+ } else if (method === "GET" && url.pathname === "/api/audit-log") {
4803
+ this.handleAuditLog(url, res);
4804
+ } else if (method === "POST" && url.pathname.startsWith("/api/approve/")) {
4805
+ const id = url.pathname.slice("/api/approve/".length);
4806
+ this.handleDecision(id, "approve", res);
4807
+ } else if (method === "POST" && url.pathname.startsWith("/api/deny/")) {
4808
+ const id = url.pathname.slice("/api/deny/".length);
4809
+ this.handleDecision(id, "deny", res);
4810
+ } else {
4811
+ res.writeHead(404, { "Content-Type": "application/json" });
4812
+ res.end(JSON.stringify({ error: "Not found" }));
4813
+ }
4814
+ } catch (err) {
4815
+ res.writeHead(500, { "Content-Type": "application/json" });
4816
+ res.end(JSON.stringify({ error: "Internal server error" }));
4817
+ }
4818
+ }
4819
+ // ── Route Handlers ──────────────────────────────────────────────────
4820
+ /**
4821
+ * SEC-012: Exchange a long-lived auth token (in Authorization header)
4822
+ * for a short-lived session ID. The session ID can be used in URL
4823
+ * query parameters without exposing the long-lived credential.
4824
+ *
4825
+ * This endpoint performs its OWN auth check (header-only) because it
4826
+ * must reject query-parameter tokens and is called before the
4827
+ * normal checkAuth flow.
4828
+ */
4829
+ handleSessionExchange(req, res) {
4830
+ if (!this.authToken) {
4831
+ res.writeHead(200, { "Content-Type": "application/json" });
4832
+ res.end(JSON.stringify({ session_id: "no-auth" }));
4833
+ return;
4834
+ }
4835
+ const authHeader = req.headers.authorization;
4836
+ if (!authHeader) {
4837
+ res.writeHead(401, { "Content-Type": "application/json" });
4838
+ res.end(JSON.stringify({ error: "Authorization header required" }));
4839
+ return;
4840
+ }
4841
+ const parts = authHeader.split(" ");
4842
+ if (parts.length !== 2 || parts[0] !== "Bearer" || parts[1] !== this.authToken) {
4843
+ res.writeHead(401, { "Content-Type": "application/json" });
4844
+ res.end(JSON.stringify({ error: "Invalid bearer token" }));
4845
+ return;
4846
+ }
4847
+ const sessionId = this.createSession();
4848
+ res.writeHead(200, { "Content-Type": "application/json" });
4849
+ res.end(JSON.stringify({
4850
+ session_id: sessionId,
4851
+ expires_in_seconds: SESSION_TTL_MS / 1e3
4852
+ }));
4853
+ }
4854
+ serveDashboard(res) {
4855
+ res.writeHead(200, {
4856
+ "Content-Type": "text/html; charset=utf-8",
4857
+ "Cache-Control": "no-cache"
4858
+ });
4859
+ res.end(this.dashboardHTML);
4860
+ }
4861
+ handleSSE(req, res) {
4862
+ res.writeHead(200, {
4863
+ "Content-Type": "text/event-stream",
4864
+ "Cache-Control": "no-cache",
4865
+ "Connection": "keep-alive"
4866
+ });
4867
+ const initData = {};
4868
+ if (this.baseline) {
4869
+ initData.baseline = this.baseline.getProfile();
4870
+ }
4871
+ if (this.policy) {
4872
+ initData.policy = {
4873
+ tier1_always_approve: this.policy.tier1_always_approve,
4874
+ tier2_anomaly: this.policy.tier2_anomaly,
4875
+ tier3_always_allow: this.policy.tier3_always_allow,
4876
+ approval_channel: {
4877
+ type: this.policy.approval_channel.type,
4878
+ timeout_seconds: this.policy.approval_channel.timeout_seconds,
4879
+ auto_deny: true
4880
+ // SEC-002: hardcoded, not configurable
4881
+ }
4882
+ };
4883
+ }
4884
+ const pendingList = Array.from(this.pending.values()).map((p) => ({
4885
+ request_id: p.id,
4886
+ operation: p.request.operation,
4887
+ tier: p.request.tier,
4888
+ reason: p.request.reason,
4889
+ context: p.request.context,
4890
+ timestamp: p.request.timestamp
4891
+ }));
4892
+ if (pendingList.length > 0) {
4893
+ initData.pending = pendingList;
4894
+ }
4895
+ res.write(`event: init
4896
+ data: ${JSON.stringify(initData)}
4897
+
4898
+ `);
4899
+ this.sseClients.add(res);
4900
+ req.on("close", () => {
4901
+ this.sseClients.delete(res);
4902
+ });
4903
+ }
4904
+ handleStatus(res) {
4905
+ const status = {
4906
+ pending_count: this.pending.size,
4907
+ connected_clients: this.sseClients.size
4908
+ };
4909
+ if (this.baseline) {
4910
+ status.baseline = this.baseline.getProfile();
4911
+ }
4912
+ if (this.policy) {
4913
+ status.policy = {
4914
+ version: this.policy.version,
4915
+ tier1_always_approve: this.policy.tier1_always_approve,
4916
+ tier2_anomaly: this.policy.tier2_anomaly,
4917
+ tier3_always_allow: this.policy.tier3_always_allow,
4918
+ approval_channel: {
4919
+ type: this.policy.approval_channel.type,
4920
+ timeout_seconds: this.policy.approval_channel.timeout_seconds,
4921
+ auto_deny: true
4922
+ // SEC-002: hardcoded, not configurable
4923
+ }
4924
+ };
4925
+ }
4926
+ res.writeHead(200, { "Content-Type": "application/json" });
4927
+ res.end(JSON.stringify(status));
4928
+ }
4929
+ handlePendingList(res) {
4930
+ const list = Array.from(this.pending.values()).map((p) => ({
4931
+ id: p.id,
4932
+ operation: p.request.operation,
4933
+ tier: p.request.tier,
4934
+ reason: p.request.reason,
4935
+ context: p.request.context,
4936
+ timestamp: p.request.timestamp,
4937
+ created_at: p.created_at
4938
+ }));
4939
+ res.writeHead(200, { "Content-Type": "application/json" });
4940
+ res.end(JSON.stringify(list));
4941
+ }
4942
+ handleAuditLog(url, res) {
4943
+ const limit = parseInt(url.searchParams.get("limit") ?? "50", 10);
4944
+ if (this.auditLog) {
4945
+ this.auditLog.query({ limit }).then((entries) => {
4946
+ res.writeHead(200, { "Content-Type": "application/json" });
4947
+ res.end(JSON.stringify(entries));
4948
+ }).catch(() => {
4949
+ res.writeHead(200, { "Content-Type": "application/json" });
4950
+ res.end(JSON.stringify([]));
4951
+ });
4952
+ } else {
4953
+ res.writeHead(200, { "Content-Type": "application/json" });
4954
+ res.end(JSON.stringify([]));
4955
+ }
4956
+ }
4957
+ handleDecision(id, decision, res) {
4958
+ const pending = this.pending.get(id);
4959
+ if (!pending) {
4960
+ res.writeHead(404, { "Content-Type": "application/json" });
4961
+ res.end(JSON.stringify({ error: "Request not found or already resolved" }));
4962
+ return;
4963
+ }
4964
+ clearTimeout(pending.timer);
4965
+ this.pending.delete(id);
4966
+ const response = {
4967
+ decision,
4968
+ decided_at: (/* @__PURE__ */ new Date()).toISOString(),
4969
+ decided_by: "human"
4970
+ };
4971
+ this.broadcastSSE("request-resolved", {
4972
+ request_id: id,
4973
+ decision,
4974
+ decided_by: "human"
4975
+ });
4976
+ pending.resolve(response);
4977
+ res.writeHead(200, { "Content-Type": "application/json" });
4978
+ res.end(JSON.stringify({ success: true, decision }));
4979
+ }
4980
+ // ── SSE Broadcasting ────────────────────────────────────────────────
4981
+ broadcastSSE(event, data) {
4982
+ const message = `event: ${event}
4983
+ data: ${JSON.stringify(data)}
4984
+
4985
+ `;
4986
+ for (const client of this.sseClients) {
4987
+ try {
4988
+ client.write(message);
4989
+ } catch {
4990
+ this.sseClients.delete(client);
4991
+ }
4992
+ }
4993
+ }
4994
+ /**
4995
+ * Broadcast an audit entry to connected dashboards.
4996
+ * Called externally when audit events happen.
4997
+ */
4998
+ broadcastAuditEntry(entry) {
4999
+ this.broadcastSSE("audit-entry", entry);
5000
+ }
5001
+ /**
5002
+ * Broadcast a baseline update to connected dashboards.
5003
+ * Called externally after baseline changes.
5004
+ */
5005
+ broadcastBaselineUpdate() {
5006
+ if (this.baseline) {
5007
+ this.broadcastSSE("baseline-update", this.baseline.getProfile());
5008
+ }
5009
+ }
5010
+ /** Get the number of pending requests */
5011
+ get pendingCount() {
5012
+ return this.pending.size;
5013
+ }
5014
+ /** Get the number of connected SSE clients */
5015
+ get clientCount() {
5016
+ return this.sseClients.size;
5017
+ }
5018
+ };
5019
+ function signPayload(body, secret) {
5020
+ return createHmac("sha256", secret).update(body).digest("hex");
5021
+ }
5022
+ function verifySignature(body, signature, secret) {
5023
+ const expected = signPayload(body, secret);
5024
+ if (expected.length !== signature.length) return false;
5025
+ let mismatch = 0;
5026
+ for (let i = 0; i < expected.length; i++) {
5027
+ mismatch |= expected.charCodeAt(i) ^ signature.charCodeAt(i);
5028
+ }
5029
+ return mismatch === 0;
5030
+ }
5031
+ var WebhookApprovalChannel = class {
5032
+ config;
5033
+ pending = /* @__PURE__ */ new Map();
5034
+ callbackServer = null;
5035
+ constructor(config) {
5036
+ this.config = config;
5037
+ }
5038
+ /**
5039
+ * Start the callback listener server.
5040
+ */
5041
+ async start() {
5042
+ return new Promise((resolve, reject) => {
5043
+ this.callbackServer = createServer$2(
5044
+ (req, res) => this.handleCallback(req, res)
5045
+ );
5046
+ this.callbackServer.listen(
5047
+ this.config.callback_port,
5048
+ this.config.callback_host,
5049
+ () => {
5050
+ process.stderr.write(
5051
+ `
5052
+ Sanctuary Webhook Callback: http://${this.config.callback_host}:${this.config.callback_port}
5053
+ Webhook target: ${this.config.webhook_url}
5054
+
5055
+ `
5056
+ );
5057
+ resolve();
5058
+ }
5059
+ );
5060
+ this.callbackServer.on("error", reject);
5061
+ });
5062
+ }
5063
+ /**
5064
+ * Stop the callback server and clean up pending requests.
5065
+ */
5066
+ async stop() {
5067
+ for (const [, pending] of this.pending) {
5068
+ clearTimeout(pending.timer);
5069
+ pending.resolve({
5070
+ decision: "deny",
5071
+ decided_at: (/* @__PURE__ */ new Date()).toISOString(),
5072
+ decided_by: "auto"
5073
+ });
5074
+ }
5075
+ this.pending.clear();
5076
+ if (this.callbackServer) {
5077
+ return new Promise((resolve) => {
5078
+ this.callbackServer.close(() => resolve());
5079
+ });
5080
+ }
5081
+ }
5082
+ /**
5083
+ * Request approval by POSTing to the webhook and waiting for a callback.
5084
+ */
5085
+ async requestApproval(request) {
5086
+ const id = randomBytes$1(8).toString("hex");
5087
+ process.stderr.write(
5088
+ `[Sanctuary] Webhook approval sent: ${request.operation} (Tier ${request.tier}) \u2014 awaiting callback
5089
+ `
5090
+ );
5091
+ return new Promise((resolve) => {
5092
+ const timer = setTimeout(() => {
5093
+ this.pending.delete(id);
5094
+ const response = {
5095
+ // SEC-002: Timeout ALWAYS denies. No configuration can change this.
5096
+ decision: "deny",
5097
+ decided_at: (/* @__PURE__ */ new Date()).toISOString(),
5098
+ decided_by: "timeout"
5099
+ };
5100
+ resolve(response);
5101
+ }, this.config.timeout_seconds * 1e3);
5102
+ const pending = {
5103
+ id,
5104
+ request,
5105
+ resolve,
5106
+ timer,
5107
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
5108
+ };
5109
+ this.pending.set(id, pending);
5110
+ const callbackUrl = `http://${this.config.callback_host}:${this.config.callback_port}/webhook/respond/${id}`;
5111
+ const payload = {
5112
+ request_id: id,
5113
+ operation: request.operation,
5114
+ tier: request.tier,
5115
+ reason: request.reason,
5116
+ context: request.context,
5117
+ timestamp: request.timestamp,
5118
+ callback_url: callbackUrl,
5119
+ timeout_seconds: this.config.timeout_seconds
5120
+ };
5121
+ this.sendWebhook(payload).catch((err) => {
5122
+ process.stderr.write(
5123
+ `[Sanctuary] Webhook delivery failed: ${err instanceof Error ? err.message : String(err)}
5124
+ `
5125
+ );
5126
+ });
5127
+ });
5128
+ }
5129
+ // ── Outbound Webhook ──────────────────────────────────────────────────
5130
+ async sendWebhook(payload) {
5131
+ const body = JSON.stringify(payload);
5132
+ const signature = signPayload(body, this.config.webhook_secret);
5133
+ const response = await fetch(this.config.webhook_url, {
5134
+ method: "POST",
5135
+ headers: {
5136
+ "Content-Type": "application/json",
5137
+ "X-Sanctuary-Signature": signature,
5138
+ "X-Sanctuary-Request-Id": payload.request_id
5139
+ },
5140
+ body
5141
+ });
5142
+ if (!response.ok) {
5143
+ throw new Error(
5144
+ `Webhook returned ${response.status}: ${await response.text().catch(() => "")}`
5145
+ );
5146
+ }
5147
+ }
5148
+ // ── Inbound Callback Handler ──────────────────────────────────────────
5149
+ handleCallback(req, res) {
5150
+ const url = new URL(
5151
+ req.url ?? "/",
5152
+ `http://${req.headers.host ?? "localhost"}`
5153
+ );
5154
+ const method = req.method ?? "GET";
5155
+ res.setHeader("Access-Control-Allow-Origin", "*");
5156
+ res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
5157
+ res.setHeader(
5158
+ "Access-Control-Allow-Headers",
5159
+ "Content-Type, X-Sanctuary-Signature"
5160
+ );
5161
+ if (method === "OPTIONS") {
5162
+ res.writeHead(204);
5163
+ res.end();
5164
+ return;
5165
+ }
5166
+ if (method === "GET" && url.pathname === "/health") {
5167
+ res.writeHead(200, { "Content-Type": "application/json" });
5168
+ res.end(
5169
+ JSON.stringify({
5170
+ status: "ok",
5171
+ pending_count: this.pending.size
5172
+ })
5173
+ );
5174
+ return;
5175
+ }
5176
+ const match = url.pathname.match(/^\/webhook\/respond\/([a-f0-9]+)$/);
5177
+ if (method !== "POST" || !match) {
5178
+ res.writeHead(404, { "Content-Type": "application/json" });
5179
+ res.end(JSON.stringify({ error: "Not found" }));
5180
+ return;
5181
+ }
5182
+ const requestId = match[1];
5183
+ let bodyChunks = [];
5184
+ req.on("data", (chunk) => bodyChunks.push(chunk));
5185
+ req.on("end", () => {
5186
+ const body = Buffer.concat(bodyChunks).toString("utf-8");
5187
+ const signature = req.headers["x-sanctuary-signature"];
5188
+ if (typeof signature !== "string" || !verifySignature(body, signature, this.config.webhook_secret)) {
5189
+ res.writeHead(401, { "Content-Type": "application/json" });
5190
+ res.end(
5191
+ JSON.stringify({ error: "Invalid signature" })
5192
+ );
5193
+ return;
5194
+ }
5195
+ let callbackPayload;
5196
+ try {
5197
+ callbackPayload = JSON.parse(body);
5198
+ } catch {
5199
+ res.writeHead(400, { "Content-Type": "application/json" });
5200
+ res.end(JSON.stringify({ error: "Invalid JSON" }));
5201
+ return;
5202
+ }
5203
+ if (callbackPayload.decision !== "approve" && callbackPayload.decision !== "deny") {
5204
+ res.writeHead(400, { "Content-Type": "application/json" });
5205
+ res.end(
5206
+ JSON.stringify({
5207
+ error: 'Decision must be "approve" or "deny"'
5208
+ })
5209
+ );
5210
+ return;
5211
+ }
5212
+ if (callbackPayload.request_id !== requestId) {
5213
+ res.writeHead(400, { "Content-Type": "application/json" });
5214
+ res.end(
5215
+ JSON.stringify({ error: "Request ID mismatch" })
5216
+ );
5217
+ return;
5218
+ }
5219
+ const pending = this.pending.get(requestId);
5220
+ if (!pending) {
5221
+ res.writeHead(404, { "Content-Type": "application/json" });
5222
+ res.end(
5223
+ JSON.stringify({
5224
+ error: "Request not found or already resolved"
5225
+ })
5226
+ );
5227
+ return;
5228
+ }
5229
+ clearTimeout(pending.timer);
5230
+ this.pending.delete(requestId);
5231
+ const response = {
5232
+ decision: callbackPayload.decision,
5233
+ decided_at: (/* @__PURE__ */ new Date()).toISOString(),
5234
+ decided_by: "human"
5235
+ };
5236
+ pending.resolve(response);
5237
+ res.writeHead(200, { "Content-Type": "application/json" });
5238
+ res.end(
5239
+ JSON.stringify({
5240
+ success: true,
5241
+ decision: callbackPayload.decision
5242
+ })
5243
+ );
5244
+ });
5245
+ }
5246
+ /** Get the number of pending requests */
5247
+ get pendingCount() {
5248
+ return this.pending.size;
5249
+ }
5250
+ };
5251
+
5252
+ // src/principal-policy/gate.ts
5253
+ var ApprovalGate = class {
5254
+ policy;
5255
+ baseline;
5256
+ channel;
5257
+ auditLog;
5258
+ constructor(policy, baseline, channel, auditLog) {
5259
+ this.policy = policy;
5260
+ this.baseline = baseline;
5261
+ this.channel = channel;
5262
+ this.auditLog = auditLog;
5263
+ }
5264
+ /**
5265
+ * Evaluate a tool call against the Principal Policy.
5266
+ *
5267
+ * @param toolName - Full MCP tool name (e.g., "sanctuary/state_export")
5268
+ * @param args - Tool call arguments (for context extraction)
5269
+ * @returns GateResult indicating whether the call is allowed
5270
+ */
5271
+ async evaluate(toolName, args) {
5272
+ const operation = extractOperationName(toolName);
5273
+ this.baseline.recordToolCall(operation);
5274
+ if (this.policy.tier1_always_approve.includes(operation)) {
5275
+ return this.requestApproval(operation, 1, `"${operation}" is a Tier 1 operation (always requires approval)`, {
5276
+ operation,
5277
+ args_summary: this.summarizeArgs(args)
5278
+ });
5279
+ }
5280
+ const anomaly = this.detectAnomaly(operation, args);
5281
+ if (anomaly) {
5282
+ return this.requestApproval(operation, 2, anomaly.reason, anomaly.context);
5283
+ }
5284
+ if (this.policy.tier3_always_allow.includes(operation)) {
5285
+ this.auditLog.append("l2", `gate_allow:${operation}`, "system", {
5286
+ tier: 3,
5287
+ operation
5288
+ });
5289
+ return {
5290
+ allowed: true,
5291
+ tier: 3,
5292
+ reason: "Operation allowed (Tier 3)",
5293
+ approval_required: false
5294
+ };
5295
+ }
5296
+ this.auditLog.append("l2", `gate_unclassified:${operation}`, "system", {
5297
+ tier: 1,
5298
+ operation,
5299
+ warning: "Operation is not classified in any policy tier \u2014 defaulting to Tier 1 (require approval)"
5300
+ });
5301
+ return this.requestApproval(
5302
+ operation,
5303
+ 1,
5304
+ `"${operation}" is not classified in any policy tier \u2014 requires approval (SEC-011 safe default)`,
5305
+ { operation, unclassified: true }
5306
+ );
5307
+ }
5308
+ /**
5309
+ * Detect Tier 2 behavioral anomalies.
5310
+ */
5311
+ detectAnomaly(operation, args) {
5312
+ const config = this.policy.tier2_anomaly;
5313
+ if (this.baseline.isFirstSession && config.first_session_policy === "approve") {
5314
+ if (!this.policy.tier3_always_allow.includes(operation)) {
5315
+ return {
5316
+ reason: `First session: "${operation}" has no established baseline`,
5317
+ context: { operation, is_first_session: true }
5318
+ };
5319
+ }
5320
+ }
5321
+ if (config.new_namespace_access === "approve") {
5322
+ const namespace = args.namespace;
5323
+ if (namespace) {
5324
+ const isNew = this.baseline.recordNamespaceAccess(namespace);
5325
+ if (isNew) {
5326
+ return {
5327
+ reason: `First access to namespace "${namespace}" (not in session baseline)`,
5328
+ context: {
5329
+ operation,
5330
+ namespace,
5331
+ known_namespaces: this.baseline.getProfile().known_namespaces
5332
+ }
5333
+ };
5334
+ }
5335
+ }
5336
+ } else if (config.new_namespace_access === "log") {
5337
+ const namespace = args.namespace;
5338
+ if (namespace) {
5339
+ this.baseline.recordNamespaceAccess(namespace);
5340
+ }
5341
+ }
5342
+ if (config.new_counterparty === "approve") {
5343
+ const counterpartyDid = args.counterparty_did ?? args.agent_identity_id;
5344
+ if (counterpartyDid) {
5345
+ const isNew = this.baseline.recordCounterparty(counterpartyDid);
5346
+ if (isNew) {
5347
+ return {
5348
+ reason: `First interaction with counterparty "${counterpartyDid}"`,
5349
+ context: {
5350
+ operation,
5351
+ counterparty_did: counterpartyDid,
5352
+ known_counterparties: this.baseline.getProfile().known_counterparties
5353
+ }
5354
+ };
5355
+ }
5356
+ }
5357
+ } else if (config.new_counterparty === "log") {
5358
+ const counterpartyDid = args.counterparty_did;
5359
+ if (counterpartyDid) {
5360
+ this.baseline.recordCounterparty(counterpartyDid);
5361
+ }
5362
+ }
5363
+ if (operation === "identity_sign") {
5364
+ const signCount = this.baseline.recordSign();
3343
5365
  if (signCount > config.max_signs_per_minute) {
3344
5366
  return {
3345
5367
  reason: `Signing frequency (${signCount}/min) exceeds limit (${config.max_signs_per_minute}/min)`,
@@ -3454,7 +5476,8 @@ function createPrincipalPolicyTools(policy, baseline, auditLog) {
3454
5476
  approval_channel: {
3455
5477
  type: policy.approval_channel.type,
3456
5478
  timeout_seconds: policy.approval_channel.timeout_seconds,
3457
- auto_deny: policy.approval_channel.auto_deny
5479
+ auto_deny: true
5480
+ // SEC-002: hardcoded, not configurable
3458
5481
  }
3459
5482
  };
3460
5483
  if (includeDefaults) {
@@ -3912,6 +5935,7 @@ function deriveTrustTier(level) {
3912
5935
  // src/handshake/tools.ts
3913
5936
  function createHandshakeTools(config, identityManager, masterKey, auditLog) {
3914
5937
  const sessions = /* @__PURE__ */ new Map();
5938
+ const handshakeResults = /* @__PURE__ */ new Map();
3915
5939
  const shrOpts = {
3916
5940
  config,
3917
5941
  identityManager,
@@ -3984,7 +6008,9 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
3984
6008
  return toolResult({
3985
6009
  session_id: result.session.session_id,
3986
6010
  response: result.response,
3987
- instructions: "Send the 'response' object back to the initiator. When you receive their completion, pass it to sanctuary/handshake_status with this session_id."
6011
+ instructions: "Send the 'response' object back to the initiator. When you receive their completion, pass it to sanctuary/handshake_status with this session_id.",
6012
+ // SEC-ADD-03: Tag response — contains SHR data that will be sent to counterparty
6013
+ _content_trust: "external"
3988
6014
  });
3989
6015
  }
3990
6016
  },
@@ -4032,11 +6058,14 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
4032
6058
  session.their_shr = response.shr;
4033
6059
  session.their_nonce = response.responder_nonce;
4034
6060
  session.result = result.result;
6061
+ handshakeResults.set(result.result.counterparty_id, result.result);
4035
6062
  auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id);
4036
6063
  return toolResult({
4037
6064
  completion: result.completion,
4038
6065
  result: result.result,
4039
- instructions: "Send the 'completion' object to the responder so they can verify the handshake. The 'result' object contains the verified counterparty status and trust tier."
6066
+ instructions: "Send the 'completion' object to the responder so they can verify the handshake. The 'result' object contains the verified counterparty status and trust tier.",
6067
+ // SEC-ADD-03: Tag response as containing counterparty-controlled SHR data
6068
+ _content_trust: "external"
4040
6069
  });
4041
6070
  }
4042
6071
  },
@@ -4068,6 +6097,9 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
4068
6097
  const result = verifyCompletion(completion, session);
4069
6098
  session.state = result.verified ? "completed" : "failed";
4070
6099
  session.result = result;
6100
+ if (result.verified) {
6101
+ handshakeResults.set(result.counterparty_id, result);
6102
+ }
4071
6103
  auditLog.append(
4072
6104
  "l4",
4073
6105
  "handshake_verify_completion",
@@ -4087,18 +6119,1450 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
4087
6119
  }
4088
6120
  }
4089
6121
  ];
4090
- return { tools };
6122
+ return { tools, handshakeResults };
4091
6123
  }
4092
6124
 
4093
- // src/index.ts
4094
- init_encoding();
4095
- async function createSanctuaryServer(options) {
4096
- const config = await loadConfig(options?.configPath);
4097
- await mkdir(config.storage_path, { recursive: true, mode: 448 });
4098
- const storage = options?.storage ?? new FilesystemStorage(
4099
- `${config.storage_path}/state`
4100
- );
4101
- let masterKey;
6125
+ // src/federation/registry.ts
6126
+ var DEFAULT_CAPABILITIES = {
6127
+ reputation_exchange: true,
6128
+ mutual_attestation: true,
6129
+ encrypted_channel: false,
6130
+ attestation_formats: ["sanctuary-interaction-v1"]
6131
+ };
6132
+ var FederationRegistry = class {
6133
+ peers = /* @__PURE__ */ new Map();
6134
+ /**
6135
+ * Register or update a peer from a completed handshake.
6136
+ * This is the ONLY way peers enter the registry.
6137
+ */
6138
+ registerFromHandshake(result, peerDid, capabilities) {
6139
+ const existing = this.peers.get(result.counterparty_id);
6140
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6141
+ const peer = {
6142
+ peer_id: result.counterparty_id,
6143
+ peer_did: peerDid,
6144
+ first_seen: existing?.first_seen ?? now,
6145
+ last_handshake: result.completed_at,
6146
+ trust_tier: trustTierToSovereigntyTier(result.trust_tier),
6147
+ handshake_result: result,
6148
+ capabilities: {
6149
+ ...DEFAULT_CAPABILITIES,
6150
+ ...existing?.capabilities ?? {},
6151
+ ...capabilities ?? {}
6152
+ },
6153
+ active: result.verified && new Date(result.expires_at) > /* @__PURE__ */ new Date()
6154
+ };
6155
+ if (!peer.active) {
6156
+ peer.trust_tier = "self-attested";
6157
+ }
6158
+ this.peers.set(result.counterparty_id, peer);
6159
+ return peer;
6160
+ }
6161
+ /**
6162
+ * Get a peer by instance ID.
6163
+ * Automatically updates active status based on handshake expiry.
6164
+ */
6165
+ getPeer(peerId) {
6166
+ const peer = this.peers.get(peerId);
6167
+ if (!peer) return null;
6168
+ if (peer.active && new Date(peer.handshake_result.expires_at) <= /* @__PURE__ */ new Date()) {
6169
+ peer.active = false;
6170
+ peer.trust_tier = "self-attested";
6171
+ }
6172
+ return peer;
6173
+ }
6174
+ /**
6175
+ * List all known peers, optionally filtered by status.
6176
+ */
6177
+ listPeers(filter) {
6178
+ const peers = Array.from(this.peers.values());
6179
+ for (const peer of peers) {
6180
+ if (peer.active && new Date(peer.handshake_result.expires_at) <= /* @__PURE__ */ new Date()) {
6181
+ peer.active = false;
6182
+ peer.trust_tier = "self-attested";
6183
+ }
6184
+ }
6185
+ if (filter?.active_only) {
6186
+ return peers.filter((p) => p.active);
6187
+ }
6188
+ return peers;
6189
+ }
6190
+ /**
6191
+ * Evaluate trust for a federation peer.
6192
+ *
6193
+ * Trust assessment considers:
6194
+ * - Handshake status (current vs expired)
6195
+ * - Sovereignty tier (verified-sovereign vs degraded vs unverified)
6196
+ * - Reputation data (if available)
6197
+ * - Mutual attestation history
6198
+ */
6199
+ evaluateTrust(peerId, mutualAttestationCount = 0, reputationScore) {
6200
+ const peer = this.getPeer(peerId);
6201
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6202
+ if (!peer) {
6203
+ return {
6204
+ peer_id: peerId,
6205
+ sovereignty_tier: "unverified",
6206
+ handshake_current: false,
6207
+ mutual_attestation_count: 0,
6208
+ trust_level: "none",
6209
+ factors: ["Peer not found in federation registry"],
6210
+ evaluated_at: now
6211
+ };
6212
+ }
6213
+ const factors = [];
6214
+ let score = 0;
6215
+ if (peer.active) {
6216
+ factors.push("Active handshake (trust current)");
6217
+ score += 3;
6218
+ } else {
6219
+ factors.push("Handshake expired (trust degraded)");
6220
+ score += 1;
6221
+ }
6222
+ switch (peer.trust_tier) {
6223
+ case "verified-sovereign":
6224
+ factors.push("Verified sovereign \u2014 full sovereignty posture");
6225
+ score += 4;
6226
+ break;
6227
+ case "verified-degraded":
6228
+ factors.push("Verified degraded \u2014 sovereignty with known limitations");
6229
+ score += 3;
6230
+ break;
6231
+ case "self-attested":
6232
+ factors.push("Self-attested \u2014 claims not independently verified");
6233
+ score += 1;
6234
+ break;
6235
+ case "unverified":
6236
+ factors.push("Unverified \u2014 no sovereignty proof");
6237
+ score += 0;
6238
+ break;
6239
+ }
6240
+ if (mutualAttestationCount > 10) {
6241
+ factors.push(`Strong attestation history (${mutualAttestationCount} mutual attestations)`);
6242
+ score += 3;
6243
+ } else if (mutualAttestationCount > 0) {
6244
+ factors.push(`Some attestation history (${mutualAttestationCount} mutual attestations)`);
6245
+ score += 1;
6246
+ } else {
6247
+ factors.push("No mutual attestation history");
6248
+ }
6249
+ if (reputationScore !== void 0) {
6250
+ if (reputationScore >= 80) {
6251
+ factors.push(`High reputation score (${reputationScore})`);
6252
+ score += 2;
6253
+ } else if (reputationScore >= 50) {
6254
+ factors.push(`Moderate reputation score (${reputationScore})`);
6255
+ score += 1;
6256
+ } else {
6257
+ factors.push(`Low reputation score (${reputationScore})`);
6258
+ }
6259
+ }
6260
+ let trust_level;
6261
+ if (score >= 9) trust_level = "high";
6262
+ else if (score >= 5) trust_level = "medium";
6263
+ else if (score >= 2) trust_level = "low";
6264
+ else trust_level = "none";
6265
+ return {
6266
+ peer_id: peerId,
6267
+ sovereignty_tier: peer.trust_tier,
6268
+ handshake_current: peer.active,
6269
+ reputation_score: reputationScore,
6270
+ mutual_attestation_count: mutualAttestationCount,
6271
+ trust_level,
6272
+ factors,
6273
+ evaluated_at: now
6274
+ };
6275
+ }
6276
+ /**
6277
+ * Remove a peer from the registry.
6278
+ */
6279
+ removePeer(peerId) {
6280
+ return this.peers.delete(peerId);
6281
+ }
6282
+ /**
6283
+ * Get the handshake results map (for tier resolution integration).
6284
+ */
6285
+ getHandshakeResults() {
6286
+ const results = /* @__PURE__ */ new Map();
6287
+ for (const [id, peer] of this.peers) {
6288
+ if (peer.active) {
6289
+ results.set(id, peer.handshake_result);
6290
+ }
6291
+ }
6292
+ return results;
6293
+ }
6294
+ };
6295
+
6296
+ // src/federation/tools.ts
6297
+ function createFederationTools(auditLog, handshakeResults) {
6298
+ const registry = new FederationRegistry();
6299
+ const tools = [
6300
+ // ─── Peer Management ──────────────────────────────────────────────
6301
+ {
6302
+ name: "sanctuary/federation_peers",
6303
+ description: "List known federation peers, register a peer from a completed handshake, or remove a peer. Every peer MUST enter through a verified handshake \u2014 no self-registration allowed.",
6304
+ inputSchema: {
6305
+ type: "object",
6306
+ properties: {
6307
+ action: {
6308
+ type: "string",
6309
+ enum: ["list", "register", "remove"],
6310
+ description: "Operation to perform on the peer registry"
6311
+ },
6312
+ peer_id: {
6313
+ type: "string",
6314
+ description: "Peer instance ID (required for register/remove)"
6315
+ },
6316
+ peer_did: {
6317
+ type: "string",
6318
+ description: "Peer DID (required for register)"
6319
+ },
6320
+ active_only: {
6321
+ type: "boolean",
6322
+ description: "When listing, only show peers with active handshakes"
6323
+ }
6324
+ },
6325
+ required: ["action"]
6326
+ },
6327
+ handler: async (args) => {
6328
+ const action = args.action;
6329
+ switch (action) {
6330
+ case "list": {
6331
+ const peers = registry.listPeers({
6332
+ active_only: args.active_only
6333
+ });
6334
+ auditLog.append("l4", "federation_peers_list", "system", {
6335
+ peer_count: peers.length
6336
+ });
6337
+ return toolResult({
6338
+ peers: peers.map((p) => ({
6339
+ peer_id: p.peer_id,
6340
+ peer_did: p.peer_did,
6341
+ trust_tier: p.trust_tier,
6342
+ active: p.active,
6343
+ first_seen: p.first_seen,
6344
+ last_handshake: p.last_handshake,
6345
+ capabilities: p.capabilities
6346
+ })),
6347
+ total: peers.length
6348
+ });
6349
+ }
6350
+ case "register": {
6351
+ const peerId = args.peer_id;
6352
+ const peerDid = args.peer_did;
6353
+ if (!peerId || !peerDid) {
6354
+ return toolResult({
6355
+ error: "Both peer_id and peer_did are required for registration."
6356
+ });
6357
+ }
6358
+ const hsResult = handshakeResults.get(peerId);
6359
+ if (!hsResult) {
6360
+ return toolResult({
6361
+ error: `No completed handshake found for peer "${peerId}". Complete a sovereignty handshake first using handshake_initiate.`
6362
+ });
6363
+ }
6364
+ if (!hsResult.verified) {
6365
+ return toolResult({
6366
+ error: `Handshake with "${peerId}" was not verified. Only verified handshakes can establish federation.`
6367
+ });
6368
+ }
6369
+ const peer = registry.registerFromHandshake(hsResult, peerDid);
6370
+ auditLog.append("l4", "federation_peer_register", "system", {
6371
+ peer_id: peerId,
6372
+ peer_did: peerDid,
6373
+ trust_tier: peer.trust_tier
6374
+ });
6375
+ return toolResult({
6376
+ registered: true,
6377
+ peer_id: peer.peer_id,
6378
+ trust_tier: peer.trust_tier,
6379
+ active: peer.active,
6380
+ capabilities: peer.capabilities
6381
+ });
6382
+ }
6383
+ case "remove": {
6384
+ const peerId = args.peer_id;
6385
+ if (!peerId) {
6386
+ return toolResult({ error: "peer_id is required for removal." });
6387
+ }
6388
+ const removed = registry.removePeer(peerId);
6389
+ auditLog.append("l4", "federation_peer_remove", "system", {
6390
+ peer_id: peerId,
6391
+ removed
6392
+ });
6393
+ return toolResult({
6394
+ removed,
6395
+ peer_id: peerId
6396
+ });
6397
+ }
6398
+ default:
6399
+ return toolResult({ error: `Unknown action: ${action}` });
6400
+ }
6401
+ }
6402
+ },
6403
+ // ─── Trust Evaluation ─────────────────────────────────────────────
6404
+ {
6405
+ name: "sanctuary/federation_trust_evaluate",
6406
+ description: "Evaluate the trust level of a federation peer. Considers handshake status, sovereignty tier, reputation score, and mutual attestation history. Returns a composite trust assessment.",
6407
+ inputSchema: {
6408
+ type: "object",
6409
+ properties: {
6410
+ peer_id: {
6411
+ type: "string",
6412
+ description: "Peer instance ID to evaluate"
6413
+ },
6414
+ mutual_attestation_count: {
6415
+ type: "number",
6416
+ description: "Number of mutual attestations with this peer (0 if unknown)"
6417
+ },
6418
+ reputation_score: {
6419
+ type: "number",
6420
+ description: "Peer's weighted reputation score (from reputation_query_weighted)"
6421
+ }
6422
+ },
6423
+ required: ["peer_id"]
6424
+ },
6425
+ handler: async (args) => {
6426
+ const peerId = args.peer_id;
6427
+ const mutualCount = args.mutual_attestation_count ?? 0;
6428
+ const repScore = args.reputation_score;
6429
+ const evaluation = registry.evaluateTrust(peerId, mutualCount, repScore);
6430
+ auditLog.append("l4", "federation_trust_evaluate", "system", {
6431
+ peer_id: peerId,
6432
+ trust_level: evaluation.trust_level,
6433
+ sovereignty_tier: evaluation.sovereignty_tier
6434
+ });
6435
+ return toolResult(evaluation);
6436
+ }
6437
+ },
6438
+ // ─── Federation Status ────────────────────────────────────────────
6439
+ {
6440
+ name: "sanctuary/federation_status",
6441
+ description: "Overview of federation state: total peers, active connections, trust distribution, and readiness for cross-instance operations.",
6442
+ inputSchema: {
6443
+ type: "object",
6444
+ properties: {}
6445
+ },
6446
+ handler: async () => {
6447
+ const allPeers = registry.listPeers();
6448
+ const activePeers = registry.listPeers({ active_only: true });
6449
+ const tierCounts = {
6450
+ "verified-sovereign": 0,
6451
+ "verified-degraded": 0,
6452
+ "self-attested": 0,
6453
+ "unverified": 0
6454
+ };
6455
+ for (const peer of allPeers) {
6456
+ tierCounts[peer.trust_tier] = (tierCounts[peer.trust_tier] ?? 0) + 1;
6457
+ }
6458
+ const capCounts = {
6459
+ reputation_exchange: activePeers.filter((p) => p.capabilities.reputation_exchange).length,
6460
+ mutual_attestation: activePeers.filter((p) => p.capabilities.mutual_attestation).length,
6461
+ encrypted_channel: activePeers.filter((p) => p.capabilities.encrypted_channel).length
6462
+ };
6463
+ auditLog.append("l4", "federation_status", "system", {
6464
+ total_peers: allPeers.length,
6465
+ active_peers: activePeers.length
6466
+ });
6467
+ return toolResult({
6468
+ total_peers: allPeers.length,
6469
+ active_peers: activePeers.length,
6470
+ expired_peers: allPeers.length - activePeers.length,
6471
+ trust_distribution: tierCounts,
6472
+ capability_coverage: capCounts,
6473
+ federation_ready: activePeers.length > 0,
6474
+ checked_at: (/* @__PURE__ */ new Date()).toISOString()
6475
+ });
6476
+ }
6477
+ }
6478
+ ];
6479
+ return { tools, registry };
6480
+ }
6481
+
6482
+ // src/bridge/tools.ts
6483
+ init_encoding();
6484
+ init_encoding();
6485
+
6486
+ // src/bridge/bridge.ts
6487
+ init_encoding();
6488
+ init_hashing();
6489
+ function canonicalize(outcome) {
6490
+ return stringToBytes(stableStringify(outcome));
6491
+ }
6492
+ function stableStringify(value) {
6493
+ if (value === null) return "null";
6494
+ if (value === void 0) return "null";
6495
+ if (typeof value === "number") {
6496
+ if (!Number.isFinite(value)) {
6497
+ throw new Error(
6498
+ `Cannot canonicalize non-finite number: ${value}. NaN, Infinity, and -Infinity are not representable in JSON.`
6499
+ );
6500
+ }
6501
+ if (Object.is(value, -0)) {
6502
+ throw new Error(
6503
+ "Cannot canonicalize negative zero (-0). Use 0 instead for deterministic cross-language serialization."
6504
+ );
6505
+ }
6506
+ return JSON.stringify(value);
6507
+ }
6508
+ if (typeof value !== "object") return JSON.stringify(value);
6509
+ if (Array.isArray(value)) {
6510
+ return "[" + value.map((v) => stableStringify(v)).join(",") + "]";
6511
+ }
6512
+ const obj = value;
6513
+ const keys = Object.keys(obj).sort();
6514
+ const pairs = keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k]));
6515
+ return "{" + pairs.join(",") + "}";
6516
+ }
6517
+ function createBridgeCommitment(outcome, identity, identityEncryptionKey, includePedersen = false) {
6518
+ const commitmentId = `bridge-${Date.now()}-${toBase64url(randomBytes(8))}`;
6519
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6520
+ const canonicalBytes = canonicalize(outcome);
6521
+ const canonicalString = new TextDecoder().decode(canonicalBytes);
6522
+ const sha2564 = createCommitment(canonicalString);
6523
+ let pedersenData;
6524
+ if (includePedersen && Number.isInteger(outcome.rounds) && outcome.rounds >= 0) {
6525
+ const pedersen = createPedersenCommitment(outcome.rounds);
6526
+ pedersenData = {
6527
+ commitment: pedersen.commitment,
6528
+ blinding_factor: pedersen.blinding_factor
6529
+ };
6530
+ }
6531
+ const commitmentPayload = {
6532
+ bridge_commitment_id: commitmentId,
6533
+ session_id: outcome.session_id,
6534
+ sha256_commitment: sha2564.commitment,
6535
+ terms_hash: outcome.terms_hash,
6536
+ committer_did: identity.did,
6537
+ committed_at: now,
6538
+ bridge_version: "sanctuary-concordia-bridge-v1"
6539
+ };
6540
+ const payloadBytes = stringToBytes(stableStringify(commitmentPayload));
6541
+ const signature = sign(payloadBytes, identity.encrypted_private_key, identityEncryptionKey);
6542
+ return {
6543
+ bridge_commitment_id: commitmentId,
6544
+ session_id: outcome.session_id,
6545
+ sha256_commitment: sha2564.commitment,
6546
+ blinding_factor: sha2564.blinding_factor,
6547
+ committer_did: identity.did,
6548
+ signature: toBase64url(signature),
6549
+ pedersen_commitment: pedersenData,
6550
+ committed_at: now,
6551
+ bridge_version: "sanctuary-concordia-bridge-v1"
6552
+ };
6553
+ }
6554
+ function verifyBridgeCommitment(commitment, outcome, committerPublicKey) {
6555
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6556
+ const canonicalString = new TextDecoder().decode(canonicalize(outcome));
6557
+ const sha256Match = verifyCommitment(
6558
+ commitment.sha256_commitment,
6559
+ canonicalString,
6560
+ commitment.blinding_factor
6561
+ );
6562
+ const commitmentPayload = {
6563
+ bridge_commitment_id: commitment.bridge_commitment_id,
6564
+ session_id: commitment.session_id,
6565
+ sha256_commitment: commitment.sha256_commitment,
6566
+ terms_hash: outcome.terms_hash,
6567
+ committer_did: commitment.committer_did,
6568
+ committed_at: commitment.committed_at,
6569
+ bridge_version: commitment.bridge_version
6570
+ };
6571
+ const payloadBytes = stringToBytes(stableStringify(commitmentPayload));
6572
+ const sigBytes = fromBase64url(commitment.signature);
6573
+ const signatureValid = verify(payloadBytes, sigBytes, committerPublicKey);
6574
+ const sessionIdMatch = commitment.session_id === outcome.session_id;
6575
+ const termsBytes = stringToBytes(stableStringify(outcome.terms));
6576
+ const computedTermsHash = toBase64url(hash(termsBytes));
6577
+ const termsHashMatch = computedTermsHash === outcome.terms_hash;
6578
+ let pedersenMatch;
6579
+ if (commitment.pedersen_commitment) {
6580
+ pedersenMatch = verifyPedersenCommitment(
6581
+ commitment.pedersen_commitment.commitment,
6582
+ outcome.rounds,
6583
+ commitment.pedersen_commitment.blinding_factor
6584
+ );
6585
+ }
6586
+ const valid = sha256Match && signatureValid && sessionIdMatch && termsHashMatch && (pedersenMatch === void 0 || pedersenMatch);
6587
+ return {
6588
+ valid,
6589
+ checks: {
6590
+ sha256_match: sha256Match,
6591
+ signature_valid: signatureValid,
6592
+ session_id_match: sessionIdMatch,
6593
+ terms_hash_match: termsHashMatch,
6594
+ pedersen_match: pedersenMatch
6595
+ },
6596
+ bridge_commitment_id: commitment.bridge_commitment_id,
6597
+ verified_at: now
6598
+ };
6599
+ }
6600
+
6601
+ // src/bridge/tools.ts
6602
+ var BridgeStore = class {
6603
+ storage;
6604
+ encryptionKey;
6605
+ constructor(storage, masterKey) {
6606
+ this.storage = storage;
6607
+ this.encryptionKey = derivePurposeKey(masterKey, "bridge-commitments");
6608
+ }
6609
+ async save(commitment, outcome) {
6610
+ const record = { commitment, outcome };
6611
+ const serialized = stringToBytes(JSON.stringify(record));
6612
+ const encrypted = encrypt(serialized, this.encryptionKey);
6613
+ await this.storage.write(
6614
+ "_bridge",
6615
+ commitment.bridge_commitment_id,
6616
+ stringToBytes(JSON.stringify(encrypted))
6617
+ );
6618
+ }
6619
+ async get(commitmentId) {
6620
+ const raw = await this.storage.read("_bridge", commitmentId);
6621
+ if (!raw) return null;
6622
+ try {
6623
+ const encrypted = JSON.parse(bytesToString(raw));
6624
+ const decrypted = decrypt(encrypted, this.encryptionKey);
6625
+ return JSON.parse(bytesToString(decrypted));
6626
+ } catch {
6627
+ return null;
6628
+ }
6629
+ }
6630
+ };
6631
+ function createBridgeTools(storage, masterKey, identityManager, auditLog, handshakeResults) {
6632
+ const bridgeStore = new BridgeStore(storage, masterKey);
6633
+ const reputationStore = new ReputationStore(storage, masterKey);
6634
+ const identityEncryptionKey = derivePurposeKey(masterKey, "identity-encryption");
6635
+ const hsResults = handshakeResults ?? /* @__PURE__ */ new Map();
6636
+ function resolveIdentity(identityId) {
6637
+ const id = identityId ? identityManager.get(identityId) : identityManager.getDefault();
6638
+ if (!id) {
6639
+ throw new Error(
6640
+ identityId ? `Identity "${identityId}" not found` : "No identity available. Create one with identity_create first."
6641
+ );
6642
+ }
6643
+ return id;
6644
+ }
6645
+ const tools = [
6646
+ // ─── bridge_commit ─────────────────────────────────────────────────
6647
+ {
6648
+ name: "sanctuary/bridge_commit",
6649
+ description: "Create a cryptographic commitment binding a Concordia negotiation outcome to Sanctuary's L3 proof layer. The commitment includes a SHA-256 hash of the canonical outcome (hiding + binding), an Ed25519 signature by the committer's identity, and an optional Pedersen commitment on the round count for zero-knowledge range proofs. This is the Sanctuary side of the Concordia bridge \u2014 call this when a Concordia `accept` fires.",
6650
+ inputSchema: {
6651
+ type: "object",
6652
+ properties: {
6653
+ session_id: {
6654
+ type: "string",
6655
+ description: "Concordia session identifier"
6656
+ },
6657
+ protocol_version: {
6658
+ type: "string",
6659
+ description: 'Concordia protocol version (e.g., "concordia-v1")'
6660
+ },
6661
+ proposer_did: {
6662
+ type: "string",
6663
+ description: "DID of the party who proposed the accepted terms"
6664
+ },
6665
+ acceptor_did: {
6666
+ type: "string",
6667
+ description: "DID of the party who accepted"
6668
+ },
6669
+ terms: {
6670
+ type: "object",
6671
+ description: "The accepted terms (opaque to Sanctuary, meaningful to Concordia)"
6672
+ },
6673
+ terms_hash: {
6674
+ type: "string",
6675
+ description: "SHA-256 hash of the canonical terms serialization (computed by Concordia)"
6676
+ },
6677
+ rounds: {
6678
+ type: "number",
6679
+ description: "Number of negotiation rounds (propose/counter cycles)"
6680
+ },
6681
+ accepted_at: {
6682
+ type: "string",
6683
+ description: "ISO 8601 timestamp when accept was issued"
6684
+ },
6685
+ session_receipt: {
6686
+ type: "string",
6687
+ description: "Optional: signed Concordia session receipt"
6688
+ },
6689
+ identity_id: {
6690
+ type: "string",
6691
+ description: "Sanctuary identity to sign the commitment (uses default if omitted)"
6692
+ },
6693
+ include_pedersen: {
6694
+ type: "boolean",
6695
+ description: "Include a Pedersen commitment on round count for ZK range proofs"
6696
+ }
6697
+ },
6698
+ required: [
6699
+ "session_id",
6700
+ "protocol_version",
6701
+ "proposer_did",
6702
+ "acceptor_did",
6703
+ "terms",
6704
+ "terms_hash",
6705
+ "rounds",
6706
+ "accepted_at"
6707
+ ]
6708
+ },
6709
+ handler: async (args) => {
6710
+ const outcome = {
6711
+ session_id: args.session_id,
6712
+ protocol_version: args.protocol_version,
6713
+ proposer_did: args.proposer_did,
6714
+ acceptor_did: args.acceptor_did,
6715
+ terms: args.terms,
6716
+ terms_hash: args.terms_hash,
6717
+ rounds: args.rounds,
6718
+ accepted_at: args.accepted_at,
6719
+ session_receipt: args.session_receipt
6720
+ };
6721
+ const identity = resolveIdentity(args.identity_id);
6722
+ const includePedersen = args.include_pedersen ?? false;
6723
+ const bridgeCommitment = createBridgeCommitment(
6724
+ outcome,
6725
+ identity,
6726
+ identityEncryptionKey,
6727
+ includePedersen
6728
+ );
6729
+ await bridgeStore.save(bridgeCommitment, outcome);
6730
+ auditLog.append("l3", "bridge_commit", identity.identity_id, {
6731
+ bridge_commitment_id: bridgeCommitment.bridge_commitment_id,
6732
+ session_id: outcome.session_id,
6733
+ counterparty: outcome.proposer_did === identity.did ? outcome.acceptor_did : outcome.proposer_did
6734
+ });
6735
+ return toolResult({
6736
+ bridge_commitment_id: bridgeCommitment.bridge_commitment_id,
6737
+ session_id: bridgeCommitment.session_id,
6738
+ sha256_commitment: bridgeCommitment.sha256_commitment,
6739
+ committer_did: bridgeCommitment.committer_did,
6740
+ signature: bridgeCommitment.signature,
6741
+ pedersen_commitment: bridgeCommitment.pedersen_commitment ? { commitment: bridgeCommitment.pedersen_commitment.commitment } : void 0,
6742
+ committed_at: bridgeCommitment.committed_at,
6743
+ bridge_version: bridgeCommitment.bridge_version,
6744
+ note: "Bridge commitment created. The blinding factor is stored encrypted. Use bridge_verify to verify the commitment against the revealed outcome. Use bridge_attest to link this negotiation to your reputation."
6745
+ });
6746
+ }
6747
+ },
6748
+ // ─── bridge_verify ───────────────────────────────────────────────────
6749
+ {
6750
+ name: "sanctuary/bridge_verify",
6751
+ description: "Verify a bridge commitment against a revealed Concordia negotiation outcome. Checks SHA-256 commitment validity, Ed25519 signature, session ID match, terms hash integrity, and Pedersen commitment (if present). Use this to confirm that a counterparty's claimed negotiation outcome matches what was cryptographically committed.",
6752
+ inputSchema: {
6753
+ type: "object",
6754
+ properties: {
6755
+ bridge_commitment_id: {
6756
+ type: "string",
6757
+ description: "The bridge commitment ID to verify"
6758
+ },
6759
+ committer_public_key: {
6760
+ type: "string",
6761
+ description: "The committer's Ed25519 public key (base64url). Required if verifying a counterparty's commitment. Omit to auto-resolve from local identities."
6762
+ }
6763
+ },
6764
+ required: ["bridge_commitment_id"]
6765
+ },
6766
+ handler: async (args) => {
6767
+ const commitmentId = args.bridge_commitment_id;
6768
+ const externalPublicKey = args.committer_public_key;
6769
+ const record = await bridgeStore.get(commitmentId);
6770
+ if (!record) {
6771
+ return toolResult({
6772
+ error: `Bridge commitment "${commitmentId}" not found`
6773
+ });
6774
+ }
6775
+ const { commitment: storedCommitment, outcome } = record;
6776
+ let publicKey;
6777
+ if (externalPublicKey) {
6778
+ publicKey = fromBase64url(externalPublicKey);
6779
+ } else {
6780
+ const localIdentities = identityManager.list();
6781
+ const match = localIdentities.find((i) => i.did === storedCommitment.committer_did);
6782
+ if (!match) {
6783
+ return toolResult({
6784
+ error: `Cannot resolve public key for committer "${storedCommitment.committer_did}". Provide committer_public_key for external verification.`
6785
+ });
6786
+ }
6787
+ publicKey = fromBase64url(match.public_key);
6788
+ }
6789
+ const result = verifyBridgeCommitment(storedCommitment, outcome, publicKey);
6790
+ auditLog.append("l3", "bridge_verify", "system", {
6791
+ bridge_commitment_id: commitmentId,
6792
+ session_id: storedCommitment.session_id,
6793
+ valid: result.valid
6794
+ });
6795
+ return toolResult({
6796
+ ...result,
6797
+ session_id: storedCommitment.session_id,
6798
+ committer_did: storedCommitment.committer_did,
6799
+ // SEC-ADD-03: Tag response as containing counterparty-controlled data
6800
+ _content_trust: "external"
6801
+ });
6802
+ }
6803
+ },
6804
+ // ─── bridge_attest ───────────────────────────────────────────────────
6805
+ {
6806
+ name: "sanctuary/bridge_attest",
6807
+ description: "Record a Concordia negotiation as a Sanctuary L4 reputation attestation, linked to a bridge commitment. This completes the bridge: the commitment (L3) proves the terms were agreed, and the attestation (L4) feeds the sovereignty-weighted reputation score. The attestation is automatically tagged with the counterparty's sovereignty tier from any completed handshake.",
6808
+ inputSchema: {
6809
+ type: "object",
6810
+ properties: {
6811
+ bridge_commitment_id: {
6812
+ type: "string",
6813
+ description: "The bridge commitment ID to link"
6814
+ },
6815
+ outcome_result: {
6816
+ type: "string",
6817
+ enum: ["completed", "partial", "failed", "disputed"],
6818
+ description: "Negotiation outcome for reputation scoring"
6819
+ },
6820
+ metrics: {
6821
+ type: "object",
6822
+ description: "Optional metrics (e.g., rounds, response_time_ms, terms_complexity)"
6823
+ },
6824
+ identity_id: {
6825
+ type: "string",
6826
+ description: "Identity to sign the attestation (uses default if omitted)"
6827
+ }
6828
+ },
6829
+ required: ["bridge_commitment_id", "outcome_result"]
6830
+ },
6831
+ handler: async (args) => {
6832
+ const commitmentId = args.bridge_commitment_id;
6833
+ const outcomeResult = args.outcome_result;
6834
+ const metrics = args.metrics ?? {};
6835
+ const identityId = args.identity_id;
6836
+ const record = await bridgeStore.get(commitmentId);
6837
+ if (!record) {
6838
+ return toolResult({
6839
+ error: `Bridge commitment "${commitmentId}" not found`
6840
+ });
6841
+ }
6842
+ const { outcome } = record;
6843
+ const identity = resolveIdentity(identityId);
6844
+ const counterpartyDid = outcome.proposer_did === identity.did ? outcome.acceptor_did : outcome.proposer_did;
6845
+ const hasSanctuaryIdentity = identityManager.list().some(
6846
+ (id) => identityManager.get(id.identity_id)?.did === counterpartyDid
6847
+ );
6848
+ const tierMeta = resolveTier(counterpartyDid, hsResults, hasSanctuaryIdentity);
6849
+ const tier = tierMeta.sovereignty_tier;
6850
+ const fullMetrics = {
6851
+ ...metrics,
6852
+ negotiation_rounds: outcome.rounds
6853
+ };
6854
+ const attestation = await reputationStore.record(
6855
+ outcome.session_id,
6856
+ // interaction_id = concordia session
6857
+ counterpartyDid,
6858
+ {
6859
+ type: "negotiation",
6860
+ result: outcomeResult,
6861
+ metrics: fullMetrics
6862
+ },
6863
+ "concordia-bridge",
6864
+ // context
6865
+ identity,
6866
+ identityEncryptionKey,
6867
+ void 0,
6868
+ // counterparty_attestation
6869
+ tier
6870
+ );
6871
+ auditLog.append("l4", "bridge_attest", identity.identity_id, {
6872
+ bridge_commitment_id: commitmentId,
6873
+ session_id: outcome.session_id,
6874
+ attestation_id: attestation.attestation.attestation_id,
6875
+ counterparty_did: counterpartyDid,
6876
+ sovereignty_tier: tier
6877
+ });
6878
+ const weight = TIER_WEIGHTS[tier];
6879
+ return toolResult({
6880
+ attestation_id: attestation.attestation.attestation_id,
6881
+ bridge_commitment_id: commitmentId,
6882
+ session_id: outcome.session_id,
6883
+ counterparty_did: counterpartyDid,
6884
+ outcome_result: outcomeResult,
6885
+ sovereignty_tier: tier,
6886
+ attested_at: attestation.recorded_at,
6887
+ note: `Negotiation recorded as reputation attestation. Counterparty sovereignty tier: ${tier} (weight: ${weight}).`
6888
+ });
6889
+ }
6890
+ }
6891
+ ];
6892
+ return { tools };
6893
+ }
6894
+ function lenientJsonParse(raw) {
6895
+ let cleaned = raw.replace(/\/\/[^\n]*/g, "");
6896
+ cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, "");
6897
+ cleaned = cleaned.replace(/,\s*([\]}])/g, "$1");
6898
+ return JSON.parse(cleaned);
6899
+ }
6900
+ async function fileExists(path) {
6901
+ try {
6902
+ await access(path);
6903
+ return true;
6904
+ } catch {
6905
+ return false;
6906
+ }
6907
+ }
6908
+ async function safeReadFile(path) {
6909
+ try {
6910
+ return await readFile(path, "utf-8");
6911
+ } catch {
6912
+ return null;
6913
+ }
6914
+ }
6915
+ async function detectEnvironment(config, deepScan) {
6916
+ const fingerprint = {
6917
+ sanctuary_installed: true,
6918
+ // We're running inside Sanctuary
6919
+ sanctuary_version: config.version,
6920
+ openclaw_detected: false,
6921
+ openclaw_version: null,
6922
+ openclaw_config: null,
6923
+ node_version: process.version,
6924
+ platform: `${process.platform}-${process.arch}`
6925
+ };
6926
+ if (!deepScan) {
6927
+ return fingerprint;
6928
+ }
6929
+ const home = homedir();
6930
+ const openclawConfigPath = join(home, ".openclaw", "openclaw.json");
6931
+ const openclawEnvPath = join(home, ".openclaw", ".env");
6932
+ const openclawMemoryPath = join(home, ".openclaw", "workspace", "MEMORY.md");
6933
+ const openclawMemoryDir = join(home, ".openclaw", "workspace", "memory");
6934
+ const configExists = await fileExists(openclawConfigPath);
6935
+ const envExists = await fileExists(openclawEnvPath);
6936
+ const memoryExists = await fileExists(openclawMemoryPath);
6937
+ const memoryDirExists = await fileExists(openclawMemoryDir);
6938
+ if (configExists || memoryExists || memoryDirExists) {
6939
+ fingerprint.openclaw_detected = true;
6940
+ fingerprint.openclaw_config = await auditOpenClawConfig(
6941
+ openclawConfigPath,
6942
+ openclawEnvPath,
6943
+ openclawMemoryPath,
6944
+ configExists,
6945
+ envExists,
6946
+ memoryExists
6947
+ );
6948
+ }
6949
+ return fingerprint;
6950
+ }
6951
+ async function auditOpenClawConfig(configPath, envPath, _memoryPath, configExists, envExists, memoryExists) {
6952
+ const audit = {
6953
+ config_path: configExists ? configPath : null,
6954
+ require_approval_enabled: false,
6955
+ sandbox_policy_active: false,
6956
+ sandbox_allow_list: [],
6957
+ sandbox_deny_list: [],
6958
+ memory_encrypted: false,
6959
+ // Stock OpenClaw never encrypts memory
6960
+ env_file_exposed: false,
6961
+ gateway_token_set: false,
6962
+ dm_pairing_enabled: false,
6963
+ mcp_bridge_active: false
6964
+ };
6965
+ if (configExists) {
6966
+ const raw = await safeReadFile(configPath);
6967
+ if (raw) {
6968
+ try {
6969
+ const parsed = lenientJsonParse(raw);
6970
+ const hooks = parsed.hooks;
6971
+ if (hooks) {
6972
+ const beforeToolCall = hooks.before_tool_call;
6973
+ if (beforeToolCall) {
6974
+ const hookStr = JSON.stringify(beforeToolCall);
6975
+ audit.require_approval_enabled = hookStr.includes("requireApproval");
6976
+ }
6977
+ }
6978
+ const tools = parsed.tools;
6979
+ if (tools) {
6980
+ const sandbox = tools.sandbox;
6981
+ if (sandbox) {
6982
+ const sandboxTools = sandbox.tools;
6983
+ if (sandboxTools) {
6984
+ audit.sandbox_policy_active = true;
6985
+ if (Array.isArray(sandboxTools.allow)) {
6986
+ audit.sandbox_allow_list = sandboxTools.allow.filter(
6987
+ (item) => typeof item === "string"
6988
+ );
6989
+ }
6990
+ if (Array.isArray(sandboxTools.alsoAllow)) {
6991
+ audit.sandbox_allow_list = [
6992
+ ...audit.sandbox_allow_list,
6993
+ ...sandboxTools.alsoAllow.filter(
6994
+ (item) => typeof item === "string"
6995
+ )
6996
+ ];
6997
+ }
6998
+ if (Array.isArray(sandboxTools.deny)) {
6999
+ audit.sandbox_deny_list = sandboxTools.deny.filter(
7000
+ (item) => typeof item === "string"
7001
+ );
7002
+ }
7003
+ }
7004
+ }
7005
+ }
7006
+ const mcpServers = parsed.mcpServers;
7007
+ if (mcpServers && Object.keys(mcpServers).length > 0) {
7008
+ audit.mcp_bridge_active = true;
7009
+ }
7010
+ } catch {
7011
+ }
7012
+ }
7013
+ }
7014
+ if (envExists) {
7015
+ const envContent = await safeReadFile(envPath);
7016
+ if (envContent) {
7017
+ const secretPatterns = [
7018
+ /[A-Z_]*API_KEY\s*=/,
7019
+ /[A-Z_]*TOKEN\s*=/,
7020
+ /[A-Z_]*SECRET\s*=/,
7021
+ /[A-Z_]*PASSWORD\s*=/,
7022
+ /[A-Z_]*PRIVATE_KEY\s*=/
7023
+ ];
7024
+ audit.env_file_exposed = secretPatterns.some((p) => p.test(envContent));
7025
+ audit.gateway_token_set = /OPENCLAW_GATEWAY_TOKEN\s*=/.test(envContent);
7026
+ }
7027
+ }
7028
+ if (memoryExists) {
7029
+ audit.memory_encrypted = false;
7030
+ }
7031
+ return audit;
7032
+ }
7033
+
7034
+ // src/audit/analyzer.ts
7035
+ var L1_ENCRYPTION_AT_REST = 10;
7036
+ var L1_IDENTITY_CRYPTOGRAPHIC = 10;
7037
+ var L1_INTEGRITY_VERIFICATION = 8;
7038
+ var L1_STATE_PORTABLE = 7;
7039
+ var L2_THREE_TIER_GATE = 10;
7040
+ var L2_BINARY_GATE = 3;
7041
+ var L2_ANOMALY_DETECTION = 7;
7042
+ var L2_ENCRYPTED_AUDIT = 5;
7043
+ var L2_TOOL_SANDBOXING = 3;
7044
+ var L3_COMMITMENT_SCHEME = 8;
7045
+ var L3_ZK_PROOFS = 7;
7046
+ var L3_DISCLOSURE_POLICIES = 5;
7047
+ var L4_PORTABLE_REPUTATION = 6;
7048
+ var L4_SIGNED_ATTESTATIONS = 6;
7049
+ var L4_SYBIL_DETECTION = 4;
7050
+ var L4_SOVEREIGNTY_GATED = 4;
7051
+ var SEVERITY_ORDER = {
7052
+ critical: 0,
7053
+ high: 1,
7054
+ medium: 2,
7055
+ low: 3
7056
+ };
7057
+ function analyzeSovereignty(env, config) {
7058
+ const l1 = assessL1(env, config);
7059
+ const l2 = assessL2(env);
7060
+ const l3 = assessL3(env);
7061
+ const l4 = assessL4(env);
7062
+ const l1Score = scoreL1(l1);
7063
+ const l2Score = scoreL2(l2);
7064
+ const l3Score = scoreL3(l3);
7065
+ const l4Score = scoreL4(l4);
7066
+ const overallScore = l1Score + l2Score + l3Score + l4Score;
7067
+ const sovereigntyLevel = overallScore >= 80 ? "full" : overallScore >= 50 ? "partial" : overallScore >= 20 ? "minimal" : "none";
7068
+ const gaps = generateGaps(env, l1, l2, l3, l4);
7069
+ gaps.sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
7070
+ const recommendations = generateRecommendations(env, l1, l2, l3, l4);
7071
+ return {
7072
+ version: "1.0",
7073
+ audited_at: (/* @__PURE__ */ new Date()).toISOString(),
7074
+ environment: env,
7075
+ layers: {
7076
+ l1_cognitive: l1,
7077
+ l2_operational: l2,
7078
+ l3_selective_disclosure: l3,
7079
+ l4_reputation: l4
7080
+ },
7081
+ overall_score: overallScore,
7082
+ sovereignty_level: sovereigntyLevel,
7083
+ gaps,
7084
+ recommendations
7085
+ };
7086
+ }
7087
+ function assessL1(env, config) {
7088
+ const findings = [];
7089
+ const sanctuaryActive = env.sanctuary_installed;
7090
+ const encryptionAtRest = sanctuaryActive;
7091
+ const keyCustody = sanctuaryActive ? "self" : "none";
7092
+ const integrityVerification = sanctuaryActive;
7093
+ const identityCryptographic = sanctuaryActive;
7094
+ const statePortable = sanctuaryActive;
7095
+ if (sanctuaryActive) {
7096
+ findings.push("AES-256-GCM encryption active for all state");
7097
+ findings.push(`Key derivation: ${config.state.key_derivation}`);
7098
+ findings.push(`Identity provider: ${config.state.identity_provider}`);
7099
+ findings.push("Merkle integrity verification enabled");
7100
+ findings.push("State export/import available");
7101
+ }
7102
+ if (env.openclaw_detected && env.openclaw_config) {
7103
+ if (!env.openclaw_config.memory_encrypted) {
7104
+ findings.push("OpenClaw agent memory (MEMORY.md, daily notes) stored in plaintext");
7105
+ }
7106
+ if (env.openclaw_config.env_file_exposed) {
7107
+ findings.push("OpenClaw .env file contains plaintext API keys/tokens");
7108
+ }
7109
+ }
7110
+ const status = encryptionAtRest && identityCryptographic ? "active" : encryptionAtRest || identityCryptographic ? "partial" : "inactive";
7111
+ return {
7112
+ status,
7113
+ encryption_at_rest: encryptionAtRest,
7114
+ key_custody: keyCustody,
7115
+ integrity_verification: integrityVerification,
7116
+ identity_cryptographic: identityCryptographic,
7117
+ state_portable: statePortable,
7118
+ findings
7119
+ };
7120
+ }
7121
+ function assessL2(env, _config) {
7122
+ const findings = [];
7123
+ const sanctuaryActive = env.sanctuary_installed;
7124
+ let approvalGate = "none";
7125
+ let behavioralAnomalyDetection = false;
7126
+ let auditTrailEncrypted = false;
7127
+ let auditTrailExists = false;
7128
+ let toolSandboxing = "none";
7129
+ if (sanctuaryActive) {
7130
+ approvalGate = "three-tier";
7131
+ behavioralAnomalyDetection = true;
7132
+ auditTrailEncrypted = true;
7133
+ auditTrailExists = true;
7134
+ findings.push("Three-tier Principal Policy gate active");
7135
+ findings.push("Behavioral anomaly detection (BaselineTracker) enabled");
7136
+ findings.push("Encrypted audit trail active");
7137
+ }
7138
+ if (env.openclaw_detected && env.openclaw_config) {
7139
+ if (env.openclaw_config.require_approval_enabled) {
7140
+ if (!sanctuaryActive) {
7141
+ approvalGate = "binary";
7142
+ }
7143
+ findings.push("OpenClaw requireApproval hook enabled (binary approve/deny)");
7144
+ }
7145
+ if (env.openclaw_config.sandbox_policy_active) {
7146
+ if (!sanctuaryActive) {
7147
+ toolSandboxing = "basic";
7148
+ }
7149
+ findings.push(
7150
+ `OpenClaw sandbox policy active (${env.openclaw_config.sandbox_allow_list.length} allowed, ${env.openclaw_config.sandbox_deny_list.length} denied)`
7151
+ );
7152
+ }
7153
+ }
7154
+ const status = approvalGate === "three-tier" && auditTrailEncrypted ? "active" : approvalGate !== "none" || auditTrailExists ? "partial" : "inactive";
7155
+ return {
7156
+ status,
7157
+ approval_gate: approvalGate,
7158
+ behavioral_anomaly_detection: behavioralAnomalyDetection,
7159
+ audit_trail_encrypted: auditTrailEncrypted,
7160
+ audit_trail_exists: auditTrailExists,
7161
+ tool_sandboxing: sanctuaryActive ? "policy-enforced" : toolSandboxing,
7162
+ findings
7163
+ };
7164
+ }
7165
+ function assessL3(env, _config) {
7166
+ const findings = [];
7167
+ const sanctuaryActive = env.sanctuary_installed;
7168
+ let commitmentScheme = "none";
7169
+ let zkProofs = false;
7170
+ let selectiveDisclosurePolicy = false;
7171
+ if (sanctuaryActive) {
7172
+ commitmentScheme = "pedersen+sha256";
7173
+ zkProofs = true;
7174
+ selectiveDisclosurePolicy = true;
7175
+ findings.push("SHA-256 + Pedersen commitment schemes active");
7176
+ findings.push("Schnorr ZK proofs and range proofs available");
7177
+ findings.push("Selective disclosure policies configurable");
7178
+ }
7179
+ const status = commitmentScheme === "pedersen+sha256" && zkProofs ? "active" : commitmentScheme !== "none" ? "partial" : "inactive";
7180
+ return {
7181
+ status,
7182
+ commitment_scheme: commitmentScheme,
7183
+ zero_knowledge_proofs: zkProofs,
7184
+ selective_disclosure_policy: selectiveDisclosurePolicy,
7185
+ findings
7186
+ };
7187
+ }
7188
+ function assessL4(env, _config) {
7189
+ const findings = [];
7190
+ const sanctuaryActive = env.sanctuary_installed;
7191
+ const reputationPortable = sanctuaryActive;
7192
+ const reputationSigned = sanctuaryActive;
7193
+ const sybilDetection = sanctuaryActive;
7194
+ const sovereigntyGated = sanctuaryActive;
7195
+ if (sanctuaryActive) {
7196
+ findings.push("Signed EAS-compatible attestations active");
7197
+ findings.push("Reputation export/import available");
7198
+ findings.push("Sybil detection heuristics enabled");
7199
+ findings.push("Sovereignty-gated reputation tiers active");
7200
+ } else {
7201
+ findings.push("No portable reputation system detected");
7202
+ }
7203
+ const status = reputationPortable && reputationSigned && sovereigntyGated ? "active" : reputationPortable || reputationSigned ? "partial" : "inactive";
7204
+ return {
7205
+ status,
7206
+ reputation_portable: reputationPortable,
7207
+ reputation_signed: reputationSigned,
7208
+ reputation_sybil_detection: sybilDetection,
7209
+ sovereignty_gated_tiers: sovereigntyGated,
7210
+ findings
7211
+ };
7212
+ }
7213
+ function scoreL1(l1) {
7214
+ let score = 0;
7215
+ if (l1.encryption_at_rest) score += L1_ENCRYPTION_AT_REST;
7216
+ if (l1.identity_cryptographic) score += L1_IDENTITY_CRYPTOGRAPHIC;
7217
+ if (l1.integrity_verification) score += L1_INTEGRITY_VERIFICATION;
7218
+ if (l1.state_portable) score += L1_STATE_PORTABLE;
7219
+ return score;
7220
+ }
7221
+ function scoreL2(l2) {
7222
+ let score = 0;
7223
+ if (l2.approval_gate === "three-tier") score += L2_THREE_TIER_GATE;
7224
+ else if (l2.approval_gate === "binary") score += L2_BINARY_GATE;
7225
+ if (l2.behavioral_anomaly_detection) score += L2_ANOMALY_DETECTION;
7226
+ if (l2.audit_trail_encrypted) score += L2_ENCRYPTED_AUDIT;
7227
+ if (l2.tool_sandboxing === "policy-enforced") score += L2_TOOL_SANDBOXING;
7228
+ else if (l2.tool_sandboxing === "basic") score += 1;
7229
+ return score;
7230
+ }
7231
+ function scoreL3(l3) {
7232
+ let score = 0;
7233
+ if (l3.commitment_scheme === "pedersen+sha256") score += L3_COMMITMENT_SCHEME;
7234
+ else if (l3.commitment_scheme === "sha256-only") score += 4;
7235
+ if (l3.zero_knowledge_proofs) score += L3_ZK_PROOFS;
7236
+ if (l3.selective_disclosure_policy) score += L3_DISCLOSURE_POLICIES;
7237
+ return score;
7238
+ }
7239
+ function scoreL4(l4) {
7240
+ let score = 0;
7241
+ if (l4.reputation_portable) score += L4_PORTABLE_REPUTATION;
7242
+ if (l4.reputation_signed) score += L4_SIGNED_ATTESTATIONS;
7243
+ if (l4.reputation_sybil_detection) score += L4_SYBIL_DETECTION;
7244
+ if (l4.sovereignty_gated_tiers) score += L4_SOVEREIGNTY_GATED;
7245
+ return score;
7246
+ }
7247
+ function generateGaps(env, l1, l2, l3, l4) {
7248
+ const gaps = [];
7249
+ const oc = env.openclaw_config;
7250
+ if (oc && !oc.memory_encrypted) {
7251
+ gaps.push({
7252
+ id: "GAP-L1-001",
7253
+ layer: "L1",
7254
+ severity: "critical",
7255
+ title: "Agent memory stored in plaintext",
7256
+ description: "Your agent's memory (MEMORY.md, daily notes, SQLite index) is stored in plaintext at ~/.openclaw/workspace/. Any process with file access can read your agent's full context \u2014 preferences, decisions, conversation history.",
7257
+ openclaw_relevance: "Stock OpenClaw stores all agent memory in plaintext files. There is no built-in encryption for agent state.",
7258
+ sanctuary_solution: "Sanctuary encrypts all state at rest with AES-256-GCM using a key derived from Argon2id, making state opaque to any process that doesn't hold the master key. Use sanctuary/state_write to migrate sensitive state to the encrypted store."
7259
+ });
7260
+ }
7261
+ if (oc && oc.env_file_exposed) {
7262
+ gaps.push({
7263
+ id: "GAP-L1-002",
7264
+ layer: "L1",
7265
+ severity: "critical",
7266
+ title: "Plaintext API keys in .env file",
7267
+ description: "Your .env file contains plaintext API keys and tokens. These secrets are readable by any process with filesystem access.",
7268
+ openclaw_relevance: "OpenClaw stores API keys (LLM providers, gateway tokens) in a plaintext .env file.",
7269
+ sanctuary_solution: "Sanctuary's encrypted state store can hold secrets under the same AES-256-GCM envelope as all other state, tied to your self-custodied identity. Use sanctuary/state_write with namespace 'secrets'."
7270
+ });
7271
+ }
7272
+ if (!l1.identity_cryptographic) {
7273
+ gaps.push({
7274
+ id: "GAP-L1-003",
7275
+ layer: "L1",
7276
+ severity: "critical",
7277
+ title: "No cryptographic agent identity",
7278
+ description: "Your agent has no cryptographic identity. It cannot prove it is who it claims to be to any counterparty, sign messages, or participate in sovereignty handshakes.",
7279
+ openclaw_relevance: env.openclaw_detected ? "OpenClaw has no cryptographic agent identity. Agent identity is implicit (tied to the process/session), not cryptographically verifiable." : null,
7280
+ sanctuary_solution: "Sanctuary provides Ed25519 self-custodied identity with key rotation and delegation. Use sanctuary/identity_create to establish your cryptographic identity."
7281
+ });
7282
+ }
7283
+ if (l2.approval_gate === "binary" && !l2.behavioral_anomaly_detection) {
7284
+ gaps.push({
7285
+ id: "GAP-L2-001",
7286
+ layer: "L2",
7287
+ severity: "high",
7288
+ title: "Binary approval gate (no anomaly detection)",
7289
+ description: "Your approval gate provides binary approve/deny gating without behavioral anomaly detection. Routine operations require the same manual approval as sensitive ones.",
7290
+ openclaw_relevance: env.openclaw_detected ? "OpenClaw's requireApproval hook provides binary approve/deny gating. Sanctuary's three-tier Principal Policy adds behavioral anomaly detection (auto-escalation when agent behavior deviates from baseline), encrypted audit trails, and graduated approval tiers \u2014 so routine operations auto-proceed while sensitive operations require explicit consent." : null,
7291
+ sanctuary_solution: "Sanctuary's three-tier Principal Policy gate auto-allows routine operations (Tier 3), escalates anomalous behavior (Tier 2), and always requires human approval for irreversible operations (Tier 1). Use sanctuary/principal_policy_view to inspect."
7292
+ });
7293
+ } else if (l2.approval_gate === "none") {
7294
+ gaps.push({
7295
+ id: "GAP-L2-001",
7296
+ layer: "L2",
7297
+ severity: "critical",
7298
+ title: "No approval gate",
7299
+ description: "No approval gate is configured. All tool calls execute without oversight.",
7300
+ openclaw_relevance: null,
7301
+ sanctuary_solution: "Sanctuary's Principal Policy evaluates every tool call before execution. Enable it to get three-tier approval gating with behavioral anomaly detection."
7302
+ });
7303
+ }
7304
+ if (l2.tool_sandboxing === "basic") {
7305
+ gaps.push({
7306
+ id: "GAP-L2-002",
7307
+ layer: "L2",
7308
+ severity: "medium",
7309
+ title: "Basic tool sandboxing (no cryptographic attestation)",
7310
+ description: "Your tool sandbox enforces allow/deny lists but provides no cryptographic attestation of execution context.",
7311
+ openclaw_relevance: env.openclaw_detected ? "OpenClaw's sandbox tool policy (tools.sandbox.tools) enforces allow/deny lists. Sanctuary adds cryptographic attestation of execution context \u2014 a verifiable proof that an operation ran within policy, not just that a policy was configured." : null,
7312
+ sanctuary_solution: "Sanctuary provides cryptographic execution attestation via sanctuary/exec_attest and policy-enforced sandboxing with encrypted audit trails."
7313
+ });
7314
+ }
7315
+ if (!l2.audit_trail_exists) {
7316
+ gaps.push({
7317
+ id: "GAP-L2-003",
7318
+ layer: "L2",
7319
+ severity: "high",
7320
+ title: "No audit trail",
7321
+ description: "No audit trail exists for tool call history. There is no record of what operations were executed, when, or by whom.",
7322
+ openclaw_relevance: null,
7323
+ sanctuary_solution: "Sanctuary maintains an encrypted audit log of all operations, queryable via sanctuary/monitor_audit_log."
7324
+ });
7325
+ }
7326
+ if (l3.commitment_scheme === "none") {
7327
+ gaps.push({
7328
+ id: "GAP-L3-001",
7329
+ layer: "L3",
7330
+ severity: "high",
7331
+ title: "No selective disclosure capability",
7332
+ description: "Your agent has no way to prove facts about its state without revealing the state itself. Every disclosure is all-or-nothing.",
7333
+ openclaw_relevance: env.openclaw_detected ? "OpenClaw has no selective disclosure mechanism. When your agent shares information, it shares everything or nothing \u2014 there is no way to prove a claim without revealing the underlying data." : null,
7334
+ sanctuary_solution: "Sanctuary's L3 provides SHA-256 + Pedersen commitments and Schnorr zero-knowledge proofs. Your agent can prove it has a valid credential, sufficient reputation, or a completed transaction without exposing the underlying data. Use sanctuary/zk_commit and sanctuary/zk_prove."
7335
+ });
7336
+ }
7337
+ if (!l4.reputation_portable) {
7338
+ gaps.push({
7339
+ id: "GAP-L4-001",
7340
+ layer: "L4",
7341
+ severity: "high",
7342
+ title: "No portable reputation",
7343
+ description: "Your agent's reputation is platform-locked. If you move to a different harness or platform, your track record doesn't follow.",
7344
+ openclaw_relevance: env.openclaw_detected ? "OpenClaw has no reputation system. Your agent's track record exists only in conversation history, which is not structured, signed, or portable." : null,
7345
+ sanctuary_solution: "Sanctuary's L4 provides signed EAS-compatible attestations that are self-custodied, portable, and cryptographically verifiable. Your reputation is yours, not your platform's. Use sanctuary/reputation_record to start building portable reputation."
7346
+ });
7347
+ }
7348
+ return gaps;
7349
+ }
7350
+ function generateRecommendations(env, l1, l2, l3, l4) {
7351
+ const recs = [];
7352
+ if (!l1.identity_cryptographic) {
7353
+ recs.push({
7354
+ priority: 1,
7355
+ action: "Create a cryptographic identity \u2014 your agent's foundation for all sovereignty operations",
7356
+ tool: "sanctuary/identity_create",
7357
+ effort: "immediate",
7358
+ impact: "critical"
7359
+ });
7360
+ }
7361
+ if (!l1.encryption_at_rest || env.openclaw_config && !env.openclaw_config.memory_encrypted) {
7362
+ recs.push({
7363
+ priority: 2,
7364
+ action: "Migrate plaintext agent state to Sanctuary's encrypted store",
7365
+ tool: "sanctuary/state_write",
7366
+ effort: "minutes",
7367
+ impact: "critical"
7368
+ });
7369
+ }
7370
+ recs.push({
7371
+ priority: 3,
7372
+ action: "Generate a Sovereignty Health Report to present to counterparties",
7373
+ tool: "sanctuary/shr_generate",
7374
+ effort: "immediate",
7375
+ impact: "high"
7376
+ });
7377
+ if (l2.approval_gate !== "three-tier") {
7378
+ recs.push({
7379
+ priority: 4,
7380
+ action: "Enable the three-tier Principal Policy gate for graduated approval",
7381
+ tool: "sanctuary/principal_policy_view",
7382
+ effort: "minutes",
7383
+ impact: "high"
7384
+ });
7385
+ }
7386
+ if (!l4.reputation_signed) {
7387
+ recs.push({
7388
+ priority: 5,
7389
+ action: "Start recording reputation attestations from completed interactions",
7390
+ tool: "sanctuary/reputation_record",
7391
+ effort: "minutes",
7392
+ impact: "medium"
7393
+ });
7394
+ }
7395
+ if (!l3.selective_disclosure_policy) {
7396
+ recs.push({
7397
+ priority: 6,
7398
+ action: "Configure selective disclosure policies for data sharing",
7399
+ tool: "sanctuary/disclosure_set_policy",
7400
+ effort: "hours",
7401
+ impact: "medium"
7402
+ });
7403
+ }
7404
+ return recs;
7405
+ }
7406
+ function formatAuditReport(result) {
7407
+ const { environment: env, layers, overall_score, sovereignty_level, gaps, recommendations } = result;
7408
+ const scoreBar = formatScoreBar(overall_score);
7409
+ const levelLabel = sovereignty_level.toUpperCase();
7410
+ let report = "";
7411
+ report += "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n";
7412
+ report += " SOVEREIGNTY AUDIT REPORT\n";
7413
+ report += ` Generated: ${result.audited_at}
7414
+ `;
7415
+ report += "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n";
7416
+ report += "\n";
7417
+ report += ` Overall Score: ${overall_score} / 100 ${scoreBar} ${levelLabel}
7418
+ `;
7419
+ report += "\n";
7420
+ report += " Environment:\n";
7421
+ report += ` \u2022 Sanctuary v${env.sanctuary_version ?? "?"} ${padDots("Sanctuary v" + (env.sanctuary_version ?? "?"))} ${env.sanctuary_installed ? "\u2713 installed" : "\u2717 not found"}
7422
+ `;
7423
+ if (env.openclaw_detected) {
7424
+ report += ` \u2022 OpenClaw ${padDots("OpenClaw")} \u2713 detected
7425
+ `;
7426
+ if (env.openclaw_config) {
7427
+ report += ` \u2022 OpenClaw requireApproval ${padDots("OpenClaw requireApproval")} ${env.openclaw_config.require_approval_enabled ? "\u2713 enabled" : "\u2717 disabled"}
7428
+ `;
7429
+ report += ` \u2022 OpenClaw sandbox policy ${padDots("OpenClaw sandbox policy")} ${env.openclaw_config.sandbox_policy_active ? "\u2713 active" : "\u2717 inactive"}
7430
+ `;
7431
+ }
7432
+ }
7433
+ report += "\n";
7434
+ const l1Score = scoreL1(layers.l1_cognitive);
7435
+ const l2Score = scoreL2(layers.l2_operational);
7436
+ const l3Score = scoreL3(layers.l3_selective_disclosure);
7437
+ const l4Score = scoreL4(layers.l4_reputation);
7438
+ report += " Layer Assessment:\n";
7439
+ report += " \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n";
7440
+ report += " \u2502 Layer \u2502 Status \u2502 Score \u2502\n";
7441
+ report += " \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n";
7442
+ report += ` \u2502 L1 Cognitive Sovereignty \u2502 ${padStatus(layers.l1_cognitive.status)} \u2502 ${padScore(l1Score, 35)} \u2502
7443
+ `;
7444
+ report += ` \u2502 L2 Operational Isolation \u2502 ${padStatus(layers.l2_operational.status)} \u2502 ${padScore(l2Score, 25)} \u2502
7445
+ `;
7446
+ report += ` \u2502 L3 Selective Disclosure \u2502 ${padStatus(layers.l3_selective_disclosure.status)} \u2502 ${padScore(l3Score, 20)} \u2502
7447
+ `;
7448
+ report += ` \u2502 L4 Verifiable Reputation \u2502 ${padStatus(layers.l4_reputation.status)} \u2502 ${padScore(l4Score, 20)} \u2502
7449
+ `;
7450
+ report += " \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n";
7451
+ report += "\n";
7452
+ if (gaps.length > 0) {
7453
+ report += ` \u26A0 ${gaps.length} SOVEREIGNTY GAP${gaps.length !== 1 ? "S" : ""} FOUND
7454
+ `;
7455
+ report += "\n";
7456
+ for (const gap of gaps) {
7457
+ const severityLabel = `[${gap.severity.toUpperCase()}]`;
7458
+ report += ` ${severityLabel} ${gap.id}: ${gap.title}
7459
+ `;
7460
+ const descLines = wordWrap(gap.description, 66);
7461
+ for (const line of descLines) {
7462
+ report += ` ${line}
7463
+ `;
7464
+ }
7465
+ report += ` \u2192 Fix: ${gap.sanctuary_solution.split(".")[0]}.
7466
+ `;
7467
+ if (gap.openclaw_relevance) {
7468
+ report += ` \u2192 OpenClaw context: ${gap.openclaw_relevance.split(".")[0]}.
7469
+ `;
7470
+ }
7471
+ report += "\n";
7472
+ }
7473
+ } else {
7474
+ report += " \u2713 NO SOVEREIGNTY GAPS FOUND\n";
7475
+ report += "\n";
7476
+ }
7477
+ if (recommendations.length > 0) {
7478
+ report += " RECOMMENDED NEXT STEPS (in order):\n";
7479
+ for (const rec of recommendations) {
7480
+ const effortLabel = rec.effort === "immediate" ? "immediate" : rec.effort === "minutes" ? "5 min" : "30 min";
7481
+ report += ` ${rec.priority}. [${effortLabel}] ${rec.action}`;
7482
+ if (rec.tool) {
7483
+ report += `: ${rec.tool}`;
7484
+ }
7485
+ report += "\n";
7486
+ }
7487
+ report += "\n";
7488
+ }
7489
+ report += "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n";
7490
+ return report;
7491
+ }
7492
+ function formatScoreBar(score) {
7493
+ const filled = Math.round(score / 10);
7494
+ return "[" + "\u25A0".repeat(filled) + "\u2591".repeat(10 - filled) + "]";
7495
+ }
7496
+ function padDots(label) {
7497
+ const totalWidth = 30;
7498
+ const dotsNeeded = Math.max(2, totalWidth - label.length - 4);
7499
+ return ".".repeat(dotsNeeded);
7500
+ }
7501
+ function padStatus(status) {
7502
+ const label = status.toUpperCase();
7503
+ return label + " ".repeat(Math.max(0, 8 - label.length));
7504
+ }
7505
+ function padScore(score, max) {
7506
+ const text = `${score}/${max}`;
7507
+ return " ".repeat(Math.max(0, 5 - text.length)) + text;
7508
+ }
7509
+ function wordWrap(text, maxWidth) {
7510
+ const words = text.split(" ");
7511
+ const lines = [];
7512
+ let current = "";
7513
+ for (const word of words) {
7514
+ if (current.length + word.length + 1 > maxWidth && current.length > 0) {
7515
+ lines.push(current);
7516
+ current = word;
7517
+ } else {
7518
+ current = current.length > 0 ? current + " " + word : word;
7519
+ }
7520
+ }
7521
+ if (current.length > 0) lines.push(current);
7522
+ return lines;
7523
+ }
7524
+
7525
+ // src/audit/tools.ts
7526
+ function createAuditTools(config) {
7527
+ const tools = [
7528
+ {
7529
+ name: "sanctuary/sovereignty_audit",
7530
+ description: "Audit your agent's sovereignty posture. Inspects the local environment for encryption, identity, approval gates, selective disclosure, and reputation \u2014 including OpenClaw-specific configurations. Returns a scored gap analysis with prioritized recommendations.",
7531
+ inputSchema: {
7532
+ type: "object",
7533
+ properties: {
7534
+ deep_scan: {
7535
+ type: "boolean",
7536
+ description: "If true (default), also scans for OpenClaw config, .env files, and memory files. Set to false for a Sanctuary-only assessment."
7537
+ }
7538
+ }
7539
+ },
7540
+ handler: async (args) => {
7541
+ const deepScan = args.deep_scan !== false;
7542
+ const env = await detectEnvironment(config, deepScan);
7543
+ const result = analyzeSovereignty(env, config);
7544
+ const report = formatAuditReport(result);
7545
+ return {
7546
+ content: [
7547
+ { type: "text", text: report },
7548
+ { type: "text", text: JSON.stringify(result, null, 2) }
7549
+ ]
7550
+ };
7551
+ }
7552
+ }
7553
+ ];
7554
+ return { tools };
7555
+ }
7556
+
7557
+ // src/index.ts
7558
+ init_encoding();
7559
+ async function createSanctuaryServer(options) {
7560
+ const config = await loadConfig(options?.configPath);
7561
+ await mkdir(config.storage_path, { recursive: true, mode: 448 });
7562
+ const storage = options?.storage ?? new FilesystemStorage(
7563
+ `${config.storage_path}/state`
7564
+ );
7565
+ let masterKey;
4102
7566
  let keyProtection;
4103
7567
  let recoveryKey;
4104
7568
  const passphrase = options?.passphrase ?? process.env.SANCTUARY_PASSPHRASE;
@@ -4125,15 +7589,51 @@ async function createSanctuaryServer(options) {
4125
7589
  }
4126
7590
  } else {
4127
7591
  keyProtection = "recovery-key";
4128
- const existing = await storage.read("_meta", "recovery-key-hash");
4129
- if (existing) {
4130
- masterKey = generateRandomKey();
4131
- recoveryKey = toBase64url(masterKey);
7592
+ const { hashToString: hashToString2 } = await Promise.resolve().then(() => (init_hashing(), hashing_exports));
7593
+ const { stringToBytes: stringToBytes2, bytesToString: bytesToString2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
7594
+ const { fromBase64url: fromBase64url2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
7595
+ const { constantTimeEqual: constantTimeEqual2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
7596
+ const existingHash = await storage.read("_meta", "recovery-key-hash");
7597
+ if (existingHash) {
7598
+ const envRecoveryKey = process.env.SANCTUARY_RECOVERY_KEY;
7599
+ if (!envRecoveryKey) {
7600
+ throw new Error(
7601
+ "Sanctuary: Existing encrypted data found but no credentials provided.\nThis installation was previously set up with a recovery key.\n\nTo start the server, provide one of:\n - SANCTUARY_PASSPHRASE (if you later configured a passphrase)\n - SANCTUARY_RECOVERY_KEY (the recovery key shown at first run)\n\nWithout the correct credentials, encrypted state cannot be accessed.\nRefusing to start to prevent silent data loss."
7602
+ );
7603
+ }
7604
+ let recoveryKeyBytes;
7605
+ try {
7606
+ recoveryKeyBytes = fromBase64url2(envRecoveryKey);
7607
+ } catch {
7608
+ throw new Error(
7609
+ "Sanctuary: SANCTUARY_RECOVERY_KEY is not valid base64url. The recovery key should be the exact string shown at first run."
7610
+ );
7611
+ }
7612
+ if (recoveryKeyBytes.length !== 32) {
7613
+ throw new Error(
7614
+ "Sanctuary: SANCTUARY_RECOVERY_KEY has incorrect length. The recovery key should be the exact string shown at first run."
7615
+ );
7616
+ }
7617
+ const providedHash = hashToString2(recoveryKeyBytes);
7618
+ const storedHash = bytesToString2(existingHash);
7619
+ const providedHashBytes = stringToBytes2(providedHash);
7620
+ const storedHashBytes = stringToBytes2(storedHash);
7621
+ if (!constantTimeEqual2(providedHashBytes, storedHashBytes)) {
7622
+ throw new Error(
7623
+ "Sanctuary: Recovery key does not match the stored key hash.\nThe recovery key provided via SANCTUARY_RECOVERY_KEY is incorrect.\nUse the exact recovery key that was displayed at first run."
7624
+ );
7625
+ }
7626
+ masterKey = recoveryKeyBytes;
4132
7627
  } else {
7628
+ const existingNamespaces = await storage.list("_meta");
7629
+ const hasKeyParams = existingNamespaces.some((e) => e.key === "key-params");
7630
+ if (hasKeyParams) {
7631
+ throw new Error(
7632
+ "Sanctuary: Found existing key derivation parameters but no recovery key hash.\nThis indicates a corrupted or incomplete installation.\nIf you previously used a passphrase, set SANCTUARY_PASSPHRASE to start."
7633
+ );
7634
+ }
4133
7635
  masterKey = generateRandomKey();
4134
7636
  recoveryKey = toBase64url(masterKey);
4135
- const { hashToString: hashToString2 } = await Promise.resolve().then(() => (init_hashing(), hashing_exports));
4136
- const { stringToBytes: stringToBytes2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
4137
7637
  const keyHash = hashToString2(masterKey);
4138
7638
  await storage.write(
4139
7639
  "_meta",
@@ -4369,30 +7869,75 @@ async function createSanctuaryServer(options) {
4369
7869
  }
4370
7870
  };
4371
7871
  const { tools: l3Tools } = createL3Tools(storage, masterKey, auditLog);
4372
- const { tools: l4Tools } = createL4Tools(
4373
- storage,
4374
- masterKey,
4375
- identityManager,
4376
- auditLog
4377
- );
4378
- const policy = await loadPrincipalPolicy(config.storage_path);
4379
- const baseline = new BaselineTracker(storage, masterKey);
4380
- await baseline.load();
4381
- const approvalChannel = new StderrApprovalChannel(policy.approval_channel);
4382
- const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog);
4383
- const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
4384
7872
  const { tools: shrTools } = createSHRTools(
4385
7873
  config,
4386
7874
  identityManager,
4387
7875
  masterKey,
4388
7876
  auditLog
4389
7877
  );
4390
- const { tools: handshakeTools } = createHandshakeTools(
7878
+ const { tools: handshakeTools, handshakeResults } = createHandshakeTools(
4391
7879
  config,
4392
7880
  identityManager,
4393
7881
  masterKey,
4394
7882
  auditLog
4395
7883
  );
7884
+ const { tools: l4Tools } = createL4Tools(
7885
+ storage,
7886
+ masterKey,
7887
+ identityManager,
7888
+ auditLog,
7889
+ handshakeResults
7890
+ );
7891
+ const { tools: federationTools } = createFederationTools(
7892
+ auditLog,
7893
+ handshakeResults
7894
+ );
7895
+ const { tools: bridgeTools } = createBridgeTools(
7896
+ storage,
7897
+ masterKey,
7898
+ identityManager,
7899
+ auditLog,
7900
+ handshakeResults
7901
+ );
7902
+ const { tools: auditTools } = createAuditTools(config);
7903
+ const policy = await loadPrincipalPolicy(config.storage_path);
7904
+ const baseline = new BaselineTracker(storage, masterKey);
7905
+ await baseline.load();
7906
+ let approvalChannel;
7907
+ let dashboard;
7908
+ if (config.dashboard.enabled) {
7909
+ let authToken = config.dashboard.auth_token;
7910
+ if (authToken === "auto") {
7911
+ const { randomBytes: rb } = await import('crypto');
7912
+ authToken = rb(32).toString("hex");
7913
+ }
7914
+ dashboard = new DashboardApprovalChannel({
7915
+ port: config.dashboard.port,
7916
+ host: config.dashboard.host,
7917
+ timeout_seconds: policy.approval_channel.timeout_seconds,
7918
+ // SEC-002: auto_deny removed — timeout always denies
7919
+ auth_token: authToken,
7920
+ tls: config.dashboard.tls
7921
+ });
7922
+ dashboard.setDependencies({ policy, baseline, auditLog });
7923
+ await dashboard.start();
7924
+ approvalChannel = dashboard;
7925
+ } else if (config.webhook.enabled && config.webhook.url && config.webhook.secret) {
7926
+ const webhook = new WebhookApprovalChannel({
7927
+ webhook_url: config.webhook.url,
7928
+ webhook_secret: config.webhook.secret,
7929
+ callback_port: config.webhook.callback_port,
7930
+ callback_host: config.webhook.callback_host,
7931
+ timeout_seconds: policy.approval_channel.timeout_seconds
7932
+ // SEC-002: auto_deny removed — timeout always denies
7933
+ });
7934
+ await webhook.start();
7935
+ approvalChannel = webhook;
7936
+ } else {
7937
+ approvalChannel = new StderrApprovalChannel(policy.approval_channel);
7938
+ }
7939
+ const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog);
7940
+ const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
4396
7941
  const allTools = [
4397
7942
  ...l1Tools,
4398
7943
  ...l2Tools,
@@ -4401,6 +7946,9 @@ async function createSanctuaryServer(options) {
4401
7946
  ...policyTools,
4402
7947
  ...shrTools,
4403
7948
  ...handshakeTools,
7949
+ ...federationTools,
7950
+ ...bridgeTools,
7951
+ ...auditTools,
4404
7952
  manifestTool
4405
7953
  ];
4406
7954
  const server = createServer(allTools, { gate });
@@ -4428,7 +7976,21 @@ async function createSanctuaryServer(options) {
4428
7976
 
4429
7977
  // src/cli.ts
4430
7978
  async function main() {
4431
- const passphrase = process.env.SANCTUARY_PASSPHRASE;
7979
+ const args = process.argv.slice(2);
7980
+ let passphrase = process.env.SANCTUARY_PASSPHRASE;
7981
+ for (let i = 0; i < args.length; i++) {
7982
+ if (args[i] === "--dashboard") {
7983
+ process.env.SANCTUARY_DASHBOARD_ENABLED = "true";
7984
+ } else if (args[i] === "--passphrase" && args[i + 1]) {
7985
+ passphrase = args[++i];
7986
+ } else if (args[i] === "--help" || args[i] === "-h") {
7987
+ printHelp();
7988
+ process.exit(0);
7989
+ } else if (args[i] === "--version" || args[i] === "-v") {
7990
+ console.log("@sanctuary-framework/mcp-server 0.3.0");
7991
+ process.exit(0);
7992
+ }
7993
+ }
4432
7994
  const { server, config } = await createSanctuaryServer({ passphrase });
4433
7995
  if (config.transport === "stdio") {
4434
7996
  const transport = new StdioServerTransport();
@@ -4441,6 +8003,34 @@ async function main() {
4441
8003
  process.exit(1);
4442
8004
  }
4443
8005
  }
8006
+ function printHelp() {
8007
+ console.log(`
8008
+ @sanctuary-framework/mcp-server v0.3.0
8009
+
8010
+ Sovereignty infrastructure for agents in the agentic economy.
8011
+
8012
+ Usage:
8013
+ sanctuary-mcp-server [options]
8014
+
8015
+ Options:
8016
+ --dashboard Enable the Principal Dashboard (web UI)
8017
+ --passphrase <pass> Derive encryption key from passphrase
8018
+ --help, -h Show this help
8019
+ --version, -v Show version
8020
+
8021
+ Environment variables:
8022
+ SANCTUARY_STORAGE_PATH State directory (default: ~/.sanctuary)
8023
+ SANCTUARY_PASSPHRASE Key derivation passphrase
8024
+ SANCTUARY_DASHBOARD_ENABLED "true" to enable dashboard
8025
+ SANCTUARY_DASHBOARD_PORT Dashboard port (default: 3501)
8026
+ SANCTUARY_DASHBOARD_AUTH_TOKEN Bearer token or "auto"
8027
+ SANCTUARY_WEBHOOK_ENABLED "true" to enable webhook approvals
8028
+ SANCTUARY_WEBHOOK_URL Webhook target URL
8029
+ SANCTUARY_WEBHOOK_SECRET HMAC-SHA256 shared secret
8030
+
8031
+ For more info: https://github.com/eriknewton/sanctuary-framework
8032
+ `);
8033
+ }
4444
8034
  main().catch((err) => {
4445
8035
  console.error("Sanctuary MCP Server failed to start:", err);
4446
8036
  process.exit(1);