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