@sanctuary-framework/mcp-server 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -7
- package/dist/cli.cjs +2670 -38
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2672 -40
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +2644 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +911 -178
- package/dist/index.d.ts +911 -178
- package/dist/index.js +2629 -39
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/cli.js
CHANGED
|
@@ -5,13 +5,16 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
5
5
|
import { mkdir, readFile, writeFile, stat, unlink, readdir, chmod } from 'fs/promises';
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
import { homedir } from 'os';
|
|
8
|
-
import { randomBytes as randomBytes$1 } from 'crypto';
|
|
8
|
+
import { randomBytes as randomBytes$1, createHmac } from 'crypto';
|
|
9
9
|
import { gcm } from '@noble/ciphers/aes.js';
|
|
10
|
-
import { ed25519 } from '@noble/curves/ed25519';
|
|
10
|
+
import { RistrettoPoint, ed25519 } from '@noble/curves/ed25519';
|
|
11
11
|
import { argon2id } from 'hash-wasm';
|
|
12
12
|
import { hkdf } from '@noble/hashes/hkdf';
|
|
13
13
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
14
14
|
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
15
|
+
import { createServer as createServer$2 } from 'http';
|
|
16
|
+
import { createServer as createServer$1 } from 'https';
|
|
17
|
+
import { readFileSync } from 'fs';
|
|
15
18
|
|
|
16
19
|
var __defProp = Object.defineProperty;
|
|
17
20
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -203,7 +206,7 @@ var init_hashing = __esm({
|
|
|
203
206
|
});
|
|
204
207
|
function defaultConfig() {
|
|
205
208
|
return {
|
|
206
|
-
version: "0.
|
|
209
|
+
version: "0.3.0",
|
|
207
210
|
storage_path: join(homedir(), ".sanctuary"),
|
|
208
211
|
state: {
|
|
209
212
|
encryption: "aes-256-gcm",
|
|
@@ -232,7 +235,19 @@ function defaultConfig() {
|
|
|
232
235
|
service_endpoints: []
|
|
233
236
|
},
|
|
234
237
|
transport: "stdio",
|
|
235
|
-
http_port: 3500
|
|
238
|
+
http_port: 3500,
|
|
239
|
+
dashboard: {
|
|
240
|
+
enabled: false,
|
|
241
|
+
port: 3501,
|
|
242
|
+
host: "127.0.0.1"
|
|
243
|
+
},
|
|
244
|
+
webhook: {
|
|
245
|
+
enabled: false,
|
|
246
|
+
url: "",
|
|
247
|
+
secret: "",
|
|
248
|
+
callback_port: 3502,
|
|
249
|
+
callback_host: "127.0.0.1"
|
|
250
|
+
}
|
|
236
251
|
};
|
|
237
252
|
}
|
|
238
253
|
async function loadConfig(configPath) {
|
|
@@ -246,6 +261,39 @@ async function loadConfig(configPath) {
|
|
|
246
261
|
if (process.env.SANCTUARY_HTTP_PORT) {
|
|
247
262
|
config.http_port = parseInt(process.env.SANCTUARY_HTTP_PORT, 10);
|
|
248
263
|
}
|
|
264
|
+
if (process.env.SANCTUARY_DASHBOARD_ENABLED === "true") {
|
|
265
|
+
config.dashboard.enabled = true;
|
|
266
|
+
}
|
|
267
|
+
if (process.env.SANCTUARY_DASHBOARD_PORT) {
|
|
268
|
+
config.dashboard.port = parseInt(process.env.SANCTUARY_DASHBOARD_PORT, 10);
|
|
269
|
+
}
|
|
270
|
+
if (process.env.SANCTUARY_DASHBOARD_HOST) {
|
|
271
|
+
config.dashboard.host = process.env.SANCTUARY_DASHBOARD_HOST;
|
|
272
|
+
}
|
|
273
|
+
if (process.env.SANCTUARY_DASHBOARD_AUTH_TOKEN) {
|
|
274
|
+
config.dashboard.auth_token = process.env.SANCTUARY_DASHBOARD_AUTH_TOKEN;
|
|
275
|
+
}
|
|
276
|
+
if (process.env.SANCTUARY_DASHBOARD_TLS_CERT && process.env.SANCTUARY_DASHBOARD_TLS_KEY) {
|
|
277
|
+
config.dashboard.tls = {
|
|
278
|
+
cert_path: process.env.SANCTUARY_DASHBOARD_TLS_CERT,
|
|
279
|
+
key_path: process.env.SANCTUARY_DASHBOARD_TLS_KEY
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (process.env.SANCTUARY_WEBHOOK_ENABLED === "true") {
|
|
283
|
+
config.webhook.enabled = true;
|
|
284
|
+
}
|
|
285
|
+
if (process.env.SANCTUARY_WEBHOOK_URL) {
|
|
286
|
+
config.webhook.url = process.env.SANCTUARY_WEBHOOK_URL;
|
|
287
|
+
}
|
|
288
|
+
if (process.env.SANCTUARY_WEBHOOK_SECRET) {
|
|
289
|
+
config.webhook.secret = process.env.SANCTUARY_WEBHOOK_SECRET;
|
|
290
|
+
}
|
|
291
|
+
if (process.env.SANCTUARY_WEBHOOK_CALLBACK_PORT) {
|
|
292
|
+
config.webhook.callback_port = parseInt(process.env.SANCTUARY_WEBHOOK_CALLBACK_PORT, 10);
|
|
293
|
+
}
|
|
294
|
+
if (process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST) {
|
|
295
|
+
config.webhook.callback_host = process.env.SANCTUARY_WEBHOOK_CALLBACK_HOST;
|
|
296
|
+
}
|
|
249
297
|
const path = configPath ?? join(config.storage_path, "sanctuary.json");
|
|
250
298
|
try {
|
|
251
299
|
const raw = await readFile(path, "utf-8");
|
|
@@ -1011,7 +1059,7 @@ function createServer(tools, options) {
|
|
|
1011
1059
|
const server = new Server(
|
|
1012
1060
|
{
|
|
1013
1061
|
name: "sanctuary-mcp-server",
|
|
1014
|
-
version: "0.
|
|
1062
|
+
version: "0.3.0"
|
|
1015
1063
|
},
|
|
1016
1064
|
{
|
|
1017
1065
|
capabilities: {
|
|
@@ -1885,6 +1933,267 @@ var PolicyStore = class {
|
|
|
1885
1933
|
);
|
|
1886
1934
|
}
|
|
1887
1935
|
};
|
|
1936
|
+
init_encoding();
|
|
1937
|
+
var G = RistrettoPoint.BASE;
|
|
1938
|
+
var H_INPUT = concatBytes(
|
|
1939
|
+
sha256(stringToBytes("sanctuary-pedersen-generator-H-v1-a")),
|
|
1940
|
+
sha256(stringToBytes("sanctuary-pedersen-generator-H-v1-b"))
|
|
1941
|
+
);
|
|
1942
|
+
var H = RistrettoPoint.hashToCurve(H_INPUT);
|
|
1943
|
+
function bigintToBytes(n) {
|
|
1944
|
+
const hex = n.toString(16).padStart(64, "0");
|
|
1945
|
+
const bytes = new Uint8Array(32);
|
|
1946
|
+
for (let i = 0; i < 32; i++) {
|
|
1947
|
+
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
1948
|
+
}
|
|
1949
|
+
return bytes;
|
|
1950
|
+
}
|
|
1951
|
+
function bytesToBigint(bytes) {
|
|
1952
|
+
let hex = "";
|
|
1953
|
+
for (const b of bytes) {
|
|
1954
|
+
hex += b.toString(16).padStart(2, "0");
|
|
1955
|
+
}
|
|
1956
|
+
return BigInt("0x" + hex);
|
|
1957
|
+
}
|
|
1958
|
+
var ORDER = BigInt("7237005577332262213973186563042994240857116359379907606001950938285454250989");
|
|
1959
|
+
function mod(n) {
|
|
1960
|
+
return (n % ORDER + ORDER) % ORDER;
|
|
1961
|
+
}
|
|
1962
|
+
function safeMultiply(point, scalar) {
|
|
1963
|
+
const s = mod(scalar);
|
|
1964
|
+
if (s === 0n) return RistrettoPoint.ZERO;
|
|
1965
|
+
return point.multiply(s);
|
|
1966
|
+
}
|
|
1967
|
+
function randomScalar() {
|
|
1968
|
+
const bytes = randomBytes(64);
|
|
1969
|
+
return mod(bytesToBigint(bytes));
|
|
1970
|
+
}
|
|
1971
|
+
function fiatShamirChallenge(domain, ...points) {
|
|
1972
|
+
const domainBytes = stringToBytes(domain);
|
|
1973
|
+
const combined = concatBytes(domainBytes, ...points);
|
|
1974
|
+
const hash2 = sha256(combined);
|
|
1975
|
+
return mod(bytesToBigint(hash2));
|
|
1976
|
+
}
|
|
1977
|
+
function createPedersenCommitment(value) {
|
|
1978
|
+
const v = mod(BigInt(value));
|
|
1979
|
+
const b = randomScalar();
|
|
1980
|
+
const C = safeMultiply(G, v).add(safeMultiply(H, b));
|
|
1981
|
+
return {
|
|
1982
|
+
commitment: toBase64url(C.toRawBytes()),
|
|
1983
|
+
blinding_factor: toBase64url(bigintToBytes(b)),
|
|
1984
|
+
committed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
function verifyPedersenCommitment(commitment, value, blindingFactor) {
|
|
1988
|
+
try {
|
|
1989
|
+
const C = RistrettoPoint.fromHex(fromBase64url(commitment));
|
|
1990
|
+
const v = mod(BigInt(value));
|
|
1991
|
+
const b = bytesToBigint(fromBase64url(blindingFactor));
|
|
1992
|
+
const expected = safeMultiply(G, v).add(safeMultiply(H, b));
|
|
1993
|
+
return C.equals(expected);
|
|
1994
|
+
} catch {
|
|
1995
|
+
return false;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
function createProofOfKnowledge(value, blindingFactor, commitment) {
|
|
1999
|
+
const v = mod(BigInt(value));
|
|
2000
|
+
const b = bytesToBigint(fromBase64url(blindingFactor));
|
|
2001
|
+
const r_v = randomScalar();
|
|
2002
|
+
const r_b = randomScalar();
|
|
2003
|
+
const R = safeMultiply(G, r_v).add(safeMultiply(H, r_b));
|
|
2004
|
+
const C_bytes = fromBase64url(commitment);
|
|
2005
|
+
const R_bytes = R.toRawBytes();
|
|
2006
|
+
const e = fiatShamirChallenge("sanctuary-zk-pok-v1", C_bytes, R_bytes);
|
|
2007
|
+
const s_v = mod(r_v + e * v);
|
|
2008
|
+
const s_b = mod(r_b + e * b);
|
|
2009
|
+
return {
|
|
2010
|
+
type: "schnorr-pedersen-ristretto255",
|
|
2011
|
+
commitment,
|
|
2012
|
+
announcement: toBase64url(R_bytes),
|
|
2013
|
+
response_v: toBase64url(bigintToBytes(s_v)),
|
|
2014
|
+
response_b: toBase64url(bigintToBytes(s_b)),
|
|
2015
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
function verifyProofOfKnowledge(proof) {
|
|
2019
|
+
try {
|
|
2020
|
+
const C = RistrettoPoint.fromHex(fromBase64url(proof.commitment));
|
|
2021
|
+
const R = RistrettoPoint.fromHex(fromBase64url(proof.announcement));
|
|
2022
|
+
const s_v = bytesToBigint(fromBase64url(proof.response_v));
|
|
2023
|
+
const s_b = bytesToBigint(fromBase64url(proof.response_b));
|
|
2024
|
+
const e = fiatShamirChallenge(
|
|
2025
|
+
"sanctuary-zk-pok-v1",
|
|
2026
|
+
fromBase64url(proof.commitment),
|
|
2027
|
+
fromBase64url(proof.announcement)
|
|
2028
|
+
);
|
|
2029
|
+
const lhs = safeMultiply(G, s_v).add(safeMultiply(H, s_b));
|
|
2030
|
+
const rhs = R.add(safeMultiply(C, e));
|
|
2031
|
+
return lhs.equals(rhs);
|
|
2032
|
+
} catch {
|
|
2033
|
+
return false;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
function createRangeProof(value, blindingFactor, commitment, min, max) {
|
|
2037
|
+
if (value < min || value > max) {
|
|
2038
|
+
return { error: `Value ${value} is not in range [${min}, ${max}]` };
|
|
2039
|
+
}
|
|
2040
|
+
const range = max - min;
|
|
2041
|
+
const numBits = Math.ceil(Math.log2(range + 1));
|
|
2042
|
+
const shifted = value - min;
|
|
2043
|
+
const b = bytesToBigint(fromBase64url(blindingFactor));
|
|
2044
|
+
const bits = [];
|
|
2045
|
+
for (let i = 0; i < numBits; i++) {
|
|
2046
|
+
bits.push(shifted >> i & 1);
|
|
2047
|
+
}
|
|
2048
|
+
const bitBlindings = [];
|
|
2049
|
+
const bitCommitments = [];
|
|
2050
|
+
const bitProofs = [];
|
|
2051
|
+
for (let i = 0; i < numBits; i++) {
|
|
2052
|
+
const bit_b = randomScalar();
|
|
2053
|
+
bitBlindings.push(bit_b);
|
|
2054
|
+
const C_i = safeMultiply(G, mod(BigInt(bits[i]))).add(safeMultiply(H, bit_b));
|
|
2055
|
+
bitCommitments.push(toBase64url(C_i.toRawBytes()));
|
|
2056
|
+
const bitProof = createBitProof(bits[i], bit_b, C_i);
|
|
2057
|
+
bitProofs.push(bitProof);
|
|
2058
|
+
}
|
|
2059
|
+
const sumBlinding = bitBlindings.reduce(
|
|
2060
|
+
(acc, bi, i) => mod(acc + mod(BigInt(1 << i)) * bi),
|
|
2061
|
+
0n
|
|
2062
|
+
);
|
|
2063
|
+
const blindingDiff = mod(b - sumBlinding);
|
|
2064
|
+
const r_sum = randomScalar();
|
|
2065
|
+
const R_sum = safeMultiply(H, r_sum);
|
|
2066
|
+
const e_sum = fiatShamirChallenge(
|
|
2067
|
+
"sanctuary-zk-range-sum-v1",
|
|
2068
|
+
fromBase64url(commitment),
|
|
2069
|
+
R_sum.toRawBytes()
|
|
2070
|
+
);
|
|
2071
|
+
const s_sum = mod(r_sum + e_sum * blindingDiff);
|
|
2072
|
+
return {
|
|
2073
|
+
type: "range-pedersen-ristretto255",
|
|
2074
|
+
commitment,
|
|
2075
|
+
min,
|
|
2076
|
+
max,
|
|
2077
|
+
bit_commitments: bitCommitments,
|
|
2078
|
+
bit_proofs: bitProofs,
|
|
2079
|
+
sum_proof: {
|
|
2080
|
+
announcement: toBase64url(R_sum.toRawBytes()),
|
|
2081
|
+
response: toBase64url(bigintToBytes(s_sum))
|
|
2082
|
+
},
|
|
2083
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
function verifyRangeProof(proof) {
|
|
2087
|
+
try {
|
|
2088
|
+
const C = RistrettoPoint.fromHex(fromBase64url(proof.commitment));
|
|
2089
|
+
const range = proof.max - proof.min;
|
|
2090
|
+
const numBits = Math.ceil(Math.log2(range + 1));
|
|
2091
|
+
if (proof.bit_commitments.length !== numBits) return false;
|
|
2092
|
+
if (proof.bit_proofs.length !== numBits) return false;
|
|
2093
|
+
for (let i = 0; i < numBits; i++) {
|
|
2094
|
+
const C_i = RistrettoPoint.fromHex(fromBase64url(proof.bit_commitments[i]));
|
|
2095
|
+
if (!verifyBitProof(proof.bit_proofs[i], C_i)) {
|
|
2096
|
+
return false;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
let reconstructed = RistrettoPoint.ZERO;
|
|
2100
|
+
for (let i = 0; i < numBits; i++) {
|
|
2101
|
+
const C_i = RistrettoPoint.fromHex(fromBase64url(proof.bit_commitments[i]));
|
|
2102
|
+
const weight = mod(BigInt(1 << i));
|
|
2103
|
+
reconstructed = reconstructed.add(safeMultiply(C_i, weight));
|
|
2104
|
+
}
|
|
2105
|
+
const diff = C.subtract(safeMultiply(G, mod(BigInt(proof.min)))).subtract(reconstructed);
|
|
2106
|
+
const R_sum = RistrettoPoint.fromHex(fromBase64url(proof.sum_proof.announcement));
|
|
2107
|
+
const s_sum = bytesToBigint(fromBase64url(proof.sum_proof.response));
|
|
2108
|
+
const e_sum = fiatShamirChallenge(
|
|
2109
|
+
"sanctuary-zk-range-sum-v1",
|
|
2110
|
+
fromBase64url(proof.commitment),
|
|
2111
|
+
fromBase64url(proof.sum_proof.announcement)
|
|
2112
|
+
);
|
|
2113
|
+
const lhs = safeMultiply(H, s_sum);
|
|
2114
|
+
const rhs = R_sum.add(safeMultiply(diff, e_sum));
|
|
2115
|
+
return lhs.equals(rhs);
|
|
2116
|
+
} catch {
|
|
2117
|
+
return false;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
function createBitProof(bit, blinding, commitment) {
|
|
2121
|
+
const C_bytes = commitment.toRawBytes();
|
|
2122
|
+
if (bit === 0) {
|
|
2123
|
+
const C_minus_G = commitment.subtract(G);
|
|
2124
|
+
const e_1 = randomScalar();
|
|
2125
|
+
const s_1 = randomScalar();
|
|
2126
|
+
const R_1 = safeMultiply(H, s_1).subtract(safeMultiply(C_minus_G, e_1));
|
|
2127
|
+
const r_0 = randomScalar();
|
|
2128
|
+
const R_0 = safeMultiply(H, r_0);
|
|
2129
|
+
const e = fiatShamirChallenge(
|
|
2130
|
+
"sanctuary-zk-bit-v1",
|
|
2131
|
+
C_bytes,
|
|
2132
|
+
R_0.toRawBytes(),
|
|
2133
|
+
R_1.toRawBytes()
|
|
2134
|
+
);
|
|
2135
|
+
const e_0 = mod(e - e_1);
|
|
2136
|
+
const s_0 = mod(r_0 + e_0 * blinding);
|
|
2137
|
+
return {
|
|
2138
|
+
announcement_0: toBase64url(R_0.toRawBytes()),
|
|
2139
|
+
announcement_1: toBase64url(R_1.toRawBytes()),
|
|
2140
|
+
challenge_0: toBase64url(bigintToBytes(e_0)),
|
|
2141
|
+
challenge_1: toBase64url(bigintToBytes(e_1)),
|
|
2142
|
+
response_0: toBase64url(bigintToBytes(s_0)),
|
|
2143
|
+
response_1: toBase64url(bigintToBytes(s_1))
|
|
2144
|
+
};
|
|
2145
|
+
} else {
|
|
2146
|
+
const e_0 = randomScalar();
|
|
2147
|
+
const s_0 = randomScalar();
|
|
2148
|
+
const R_0 = safeMultiply(H, s_0).subtract(safeMultiply(commitment, e_0));
|
|
2149
|
+
const r_1 = randomScalar();
|
|
2150
|
+
const R_1 = safeMultiply(H, r_1);
|
|
2151
|
+
const e = fiatShamirChallenge(
|
|
2152
|
+
"sanctuary-zk-bit-v1",
|
|
2153
|
+
C_bytes,
|
|
2154
|
+
R_0.toRawBytes(),
|
|
2155
|
+
R_1.toRawBytes()
|
|
2156
|
+
);
|
|
2157
|
+
const e_1 = mod(e - e_0);
|
|
2158
|
+
const s_1 = mod(r_1 + e_1 * blinding);
|
|
2159
|
+
return {
|
|
2160
|
+
announcement_0: toBase64url(R_0.toRawBytes()),
|
|
2161
|
+
announcement_1: toBase64url(R_1.toRawBytes()),
|
|
2162
|
+
challenge_0: toBase64url(bigintToBytes(e_0)),
|
|
2163
|
+
challenge_1: toBase64url(bigintToBytes(e_1)),
|
|
2164
|
+
response_0: toBase64url(bigintToBytes(s_0)),
|
|
2165
|
+
response_1: toBase64url(bigintToBytes(s_1))
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
function verifyBitProof(proof, commitment) {
|
|
2170
|
+
try {
|
|
2171
|
+
const C_bytes = commitment.toRawBytes();
|
|
2172
|
+
const R_0 = RistrettoPoint.fromHex(fromBase64url(proof.announcement_0));
|
|
2173
|
+
const R_1 = RistrettoPoint.fromHex(fromBase64url(proof.announcement_1));
|
|
2174
|
+
const e_0 = bytesToBigint(fromBase64url(proof.challenge_0));
|
|
2175
|
+
const e_1 = bytesToBigint(fromBase64url(proof.challenge_1));
|
|
2176
|
+
const s_0 = bytesToBigint(fromBase64url(proof.response_0));
|
|
2177
|
+
const s_1 = bytesToBigint(fromBase64url(proof.response_1));
|
|
2178
|
+
const e = fiatShamirChallenge(
|
|
2179
|
+
"sanctuary-zk-bit-v1",
|
|
2180
|
+
C_bytes,
|
|
2181
|
+
R_0.toRawBytes(),
|
|
2182
|
+
R_1.toRawBytes()
|
|
2183
|
+
);
|
|
2184
|
+
if (mod(e_0 + e_1) !== e) return false;
|
|
2185
|
+
const lhs_0 = safeMultiply(H, s_0);
|
|
2186
|
+
const rhs_0 = R_0.add(safeMultiply(commitment, e_0));
|
|
2187
|
+
if (!lhs_0.equals(rhs_0)) return false;
|
|
2188
|
+
const C_minus_G = commitment.subtract(G);
|
|
2189
|
+
const lhs_1 = safeMultiply(H, s_1);
|
|
2190
|
+
const rhs_1 = R_1.add(safeMultiply(C_minus_G, e_1));
|
|
2191
|
+
if (!lhs_1.equals(rhs_1)) return false;
|
|
2192
|
+
return true;
|
|
2193
|
+
} catch {
|
|
2194
|
+
return false;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
1888
2197
|
|
|
1889
2198
|
// src/l3-disclosure/tools.ts
|
|
1890
2199
|
function createL3Tools(storage, masterKey, auditLog) {
|
|
@@ -2114,6 +2423,187 @@ function createL3Tools(storage, masterKey, auditLog) {
|
|
|
2114
2423
|
overall_recommendation: withholding > 0 ? `Withholding ${withholding} of ${requestedFields.length} requested fields per policy "${policy.policy_name}"` : `All ${requestedFields.length} fields may be disclosed per policy "${policy.policy_name}"`
|
|
2115
2424
|
});
|
|
2116
2425
|
}
|
|
2426
|
+
},
|
|
2427
|
+
// ─── ZK Proof Tools ───────────────────────────────────────────────────
|
|
2428
|
+
{
|
|
2429
|
+
name: "sanctuary/zk_commit",
|
|
2430
|
+
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.",
|
|
2431
|
+
inputSchema: {
|
|
2432
|
+
type: "object",
|
|
2433
|
+
properties: {
|
|
2434
|
+
value: {
|
|
2435
|
+
type: "number",
|
|
2436
|
+
description: "The integer value to commit to"
|
|
2437
|
+
}
|
|
2438
|
+
},
|
|
2439
|
+
required: ["value"]
|
|
2440
|
+
},
|
|
2441
|
+
handler: async (args) => {
|
|
2442
|
+
const value = args.value;
|
|
2443
|
+
if (!Number.isInteger(value)) {
|
|
2444
|
+
return toolResult({ error: "Value must be an integer." });
|
|
2445
|
+
}
|
|
2446
|
+
const commitment = createPedersenCommitment(value);
|
|
2447
|
+
auditLog.append("l3", "zk_commit", "system", {
|
|
2448
|
+
commitment_hash: commitment.commitment.slice(0, 16) + "..."
|
|
2449
|
+
});
|
|
2450
|
+
return toolResult({
|
|
2451
|
+
commitment: commitment.commitment,
|
|
2452
|
+
blinding_factor: commitment.blinding_factor,
|
|
2453
|
+
committed_at: commitment.committed_at,
|
|
2454
|
+
proof_system: "pedersen-ristretto255",
|
|
2455
|
+
note: "Store the blinding_factor securely. Use zk_prove to create proofs about this commitment."
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
},
|
|
2459
|
+
{
|
|
2460
|
+
name: "sanctuary/zk_prove",
|
|
2461
|
+
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.",
|
|
2462
|
+
inputSchema: {
|
|
2463
|
+
type: "object",
|
|
2464
|
+
properties: {
|
|
2465
|
+
value: {
|
|
2466
|
+
type: "number",
|
|
2467
|
+
description: "The committed value (integer)"
|
|
2468
|
+
},
|
|
2469
|
+
blinding_factor: {
|
|
2470
|
+
type: "string",
|
|
2471
|
+
description: "The blinding factor from zk_commit (base64url)"
|
|
2472
|
+
},
|
|
2473
|
+
commitment: {
|
|
2474
|
+
type: "string",
|
|
2475
|
+
description: "The Pedersen commitment (base64url)"
|
|
2476
|
+
}
|
|
2477
|
+
},
|
|
2478
|
+
required: ["value", "blinding_factor", "commitment"]
|
|
2479
|
+
},
|
|
2480
|
+
handler: async (args) => {
|
|
2481
|
+
const value = args.value;
|
|
2482
|
+
const blindingFactor = args.blinding_factor;
|
|
2483
|
+
const commitment = args.commitment;
|
|
2484
|
+
if (!verifyPedersenCommitment(commitment, value, blindingFactor)) {
|
|
2485
|
+
return toolResult({
|
|
2486
|
+
error: "The provided value and blinding factor do not match the commitment."
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
const proof = createProofOfKnowledge(value, blindingFactor, commitment);
|
|
2490
|
+
auditLog.append("l3", "zk_prove", "system", {
|
|
2491
|
+
proof_type: proof.type,
|
|
2492
|
+
commitment: commitment.slice(0, 16) + "..."
|
|
2493
|
+
});
|
|
2494
|
+
return toolResult({
|
|
2495
|
+
proof,
|
|
2496
|
+
note: "This proof demonstrates knowledge of the commitment opening without revealing the value."
|
|
2497
|
+
});
|
|
2498
|
+
}
|
|
2499
|
+
},
|
|
2500
|
+
{
|
|
2501
|
+
name: "sanctuary/zk_verify",
|
|
2502
|
+
description: "Verify a zero-knowledge proof of knowledge for a Pedersen commitment. Checks that the prover knows the commitment's opening without learning anything.",
|
|
2503
|
+
inputSchema: {
|
|
2504
|
+
type: "object",
|
|
2505
|
+
properties: {
|
|
2506
|
+
proof: {
|
|
2507
|
+
type: "object",
|
|
2508
|
+
description: "The ZK proof object from zk_prove"
|
|
2509
|
+
}
|
|
2510
|
+
},
|
|
2511
|
+
required: ["proof"]
|
|
2512
|
+
},
|
|
2513
|
+
handler: async (args) => {
|
|
2514
|
+
const proof = args.proof;
|
|
2515
|
+
const valid = verifyProofOfKnowledge(proof);
|
|
2516
|
+
auditLog.append("l3", "zk_verify", "system", {
|
|
2517
|
+
proof_type: proof.type,
|
|
2518
|
+
valid
|
|
2519
|
+
});
|
|
2520
|
+
return toolResult({
|
|
2521
|
+
valid,
|
|
2522
|
+
proof_type: proof.type,
|
|
2523
|
+
commitment: proof.commitment,
|
|
2524
|
+
verified_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2527
|
+
},
|
|
2528
|
+
{
|
|
2529
|
+
name: "sanctuary/zk_range_prove",
|
|
2530
|
+
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.",
|
|
2531
|
+
inputSchema: {
|
|
2532
|
+
type: "object",
|
|
2533
|
+
properties: {
|
|
2534
|
+
value: {
|
|
2535
|
+
type: "number",
|
|
2536
|
+
description: "The committed value (integer)"
|
|
2537
|
+
},
|
|
2538
|
+
blinding_factor: {
|
|
2539
|
+
type: "string",
|
|
2540
|
+
description: "The blinding factor from zk_commit (base64url)"
|
|
2541
|
+
},
|
|
2542
|
+
commitment: {
|
|
2543
|
+
type: "string",
|
|
2544
|
+
description: "The Pedersen commitment (base64url)"
|
|
2545
|
+
},
|
|
2546
|
+
min: {
|
|
2547
|
+
type: "number",
|
|
2548
|
+
description: "Minimum of the range (inclusive)"
|
|
2549
|
+
},
|
|
2550
|
+
max: {
|
|
2551
|
+
type: "number",
|
|
2552
|
+
description: "Maximum of the range (inclusive)"
|
|
2553
|
+
}
|
|
2554
|
+
},
|
|
2555
|
+
required: ["value", "blinding_factor", "commitment", "min", "max"]
|
|
2556
|
+
},
|
|
2557
|
+
handler: async (args) => {
|
|
2558
|
+
const value = args.value;
|
|
2559
|
+
const blindingFactor = args.blinding_factor;
|
|
2560
|
+
const commitment = args.commitment;
|
|
2561
|
+
const min = args.min;
|
|
2562
|
+
const max = args.max;
|
|
2563
|
+
const proof = createRangeProof(value, blindingFactor, commitment, min, max);
|
|
2564
|
+
if ("error" in proof) {
|
|
2565
|
+
return toolResult({ error: proof.error });
|
|
2566
|
+
}
|
|
2567
|
+
auditLog.append("l3", "zk_range_prove", "system", {
|
|
2568
|
+
proof_type: proof.type,
|
|
2569
|
+
range: `[${min}, ${max}]`,
|
|
2570
|
+
bits: proof.bit_commitments.length
|
|
2571
|
+
});
|
|
2572
|
+
return toolResult({
|
|
2573
|
+
proof,
|
|
2574
|
+
note: `This proof demonstrates the committed value is in [${min}, ${max}] without revealing it.`
|
|
2575
|
+
});
|
|
2576
|
+
}
|
|
2577
|
+
},
|
|
2578
|
+
{
|
|
2579
|
+
name: "sanctuary/zk_range_verify",
|
|
2580
|
+
description: "Verify a zero-knowledge range proof \u2014 confirms a committed value is within the claimed range without learning the value.",
|
|
2581
|
+
inputSchema: {
|
|
2582
|
+
type: "object",
|
|
2583
|
+
properties: {
|
|
2584
|
+
proof: {
|
|
2585
|
+
type: "object",
|
|
2586
|
+
description: "The range proof object from zk_range_prove"
|
|
2587
|
+
}
|
|
2588
|
+
},
|
|
2589
|
+
required: ["proof"]
|
|
2590
|
+
},
|
|
2591
|
+
handler: async (args) => {
|
|
2592
|
+
const proof = args.proof;
|
|
2593
|
+
const valid = verifyRangeProof(proof);
|
|
2594
|
+
auditLog.append("l3", "zk_range_verify", "system", {
|
|
2595
|
+
proof_type: proof.type,
|
|
2596
|
+
valid,
|
|
2597
|
+
range: `[${proof.min}, ${proof.max}]`
|
|
2598
|
+
});
|
|
2599
|
+
return toolResult({
|
|
2600
|
+
valid,
|
|
2601
|
+
proof_type: proof.type,
|
|
2602
|
+
range: { min: proof.min, max: proof.max },
|
|
2603
|
+
commitment: proof.commitment,
|
|
2604
|
+
verified_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2605
|
+
});
|
|
2606
|
+
}
|
|
2117
2607
|
}
|
|
2118
2608
|
];
|
|
2119
2609
|
return { tools, commitmentStore, policyStore };
|
|
@@ -2162,7 +2652,7 @@ var ReputationStore = class {
|
|
|
2162
2652
|
/**
|
|
2163
2653
|
* Record an interaction outcome as a signed attestation.
|
|
2164
2654
|
*/
|
|
2165
|
-
async record(interactionId, counterpartyDid, outcome, context, identity, identityEncryptionKey, counterpartyAttestation) {
|
|
2655
|
+
async record(interactionId, counterpartyDid, outcome, context, identity, identityEncryptionKey, counterpartyAttestation, sovereigntyTier) {
|
|
2166
2656
|
const attestationId = `att-${Date.now()}-${toBase64url(randomBytes(8))}`;
|
|
2167
2657
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2168
2658
|
const attestationData = {
|
|
@@ -2173,7 +2663,8 @@ var ReputationStore = class {
|
|
|
2173
2663
|
outcome_result: outcome.result,
|
|
2174
2664
|
metrics: outcome.metrics ?? {},
|
|
2175
2665
|
context,
|
|
2176
|
-
timestamp: now
|
|
2666
|
+
timestamp: now,
|
|
2667
|
+
sovereignty_tier: sovereigntyTier
|
|
2177
2668
|
};
|
|
2178
2669
|
const dataBytes = stringToBytes(JSON.stringify(attestationData));
|
|
2179
2670
|
const signature = sign(
|
|
@@ -2422,6 +2913,24 @@ var ReputationStore = class {
|
|
|
2422
2913
|
);
|
|
2423
2914
|
return guarantee;
|
|
2424
2915
|
}
|
|
2916
|
+
// ─── Tier-Aware Access ───────────────────────────────────────────────
|
|
2917
|
+
/**
|
|
2918
|
+
* Load attestations for tier-weighted scoring.
|
|
2919
|
+
* Applies basic context/counterparty filtering, returns full StoredAttestations
|
|
2920
|
+
* so callers can access sovereignty_tier from attestation data.
|
|
2921
|
+
*/
|
|
2922
|
+
async loadAllForTierScoring(options) {
|
|
2923
|
+
let all = await this.loadAll();
|
|
2924
|
+
if (options?.context) {
|
|
2925
|
+
all = all.filter((a) => a.attestation.data.context === options.context);
|
|
2926
|
+
}
|
|
2927
|
+
if (options?.counterparty_did) {
|
|
2928
|
+
all = all.filter(
|
|
2929
|
+
(a) => a.attestation.data.counterparty_did === options.counterparty_did
|
|
2930
|
+
);
|
|
2931
|
+
}
|
|
2932
|
+
return all;
|
|
2933
|
+
}
|
|
2425
2934
|
// ─── Internal ─────────────────────────────────────────────────────────
|
|
2426
2935
|
async loadAll() {
|
|
2427
2936
|
const results = [];
|
|
@@ -2445,9 +2954,70 @@ var ReputationStore = class {
|
|
|
2445
2954
|
|
|
2446
2955
|
// src/l4-reputation/tools.ts
|
|
2447
2956
|
init_encoding();
|
|
2448
|
-
|
|
2957
|
+
|
|
2958
|
+
// src/l4-reputation/tiers.ts
|
|
2959
|
+
var TIER_WEIGHTS = {
|
|
2960
|
+
"verified-sovereign": 1,
|
|
2961
|
+
"verified-degraded": 0.8,
|
|
2962
|
+
"self-attested": 0.5,
|
|
2963
|
+
"unverified": 0.2
|
|
2964
|
+
};
|
|
2965
|
+
function resolveTier(counterpartyId, handshakeResults, hasSanctuaryIdentity) {
|
|
2966
|
+
const handshake = handshakeResults.get(counterpartyId);
|
|
2967
|
+
if (handshake && handshake.verified) {
|
|
2968
|
+
const expiresAt = new Date(handshake.expires_at);
|
|
2969
|
+
if (expiresAt > /* @__PURE__ */ new Date()) {
|
|
2970
|
+
return {
|
|
2971
|
+
sovereignty_tier: handshake.trust_tier,
|
|
2972
|
+
handshake_completed_at: handshake.completed_at,
|
|
2973
|
+
verified_by: handshake.counterparty_id
|
|
2974
|
+
};
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
if (hasSanctuaryIdentity) {
|
|
2978
|
+
return { sovereignty_tier: "self-attested" };
|
|
2979
|
+
}
|
|
2980
|
+
return { sovereignty_tier: "unverified" };
|
|
2981
|
+
}
|
|
2982
|
+
function trustTierToSovereigntyTier(trustTier) {
|
|
2983
|
+
switch (trustTier) {
|
|
2984
|
+
case "verified-sovereign":
|
|
2985
|
+
return "verified-sovereign";
|
|
2986
|
+
case "verified-degraded":
|
|
2987
|
+
return "verified-degraded";
|
|
2988
|
+
default:
|
|
2989
|
+
return "unverified";
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
function computeWeightedScore(attestations) {
|
|
2993
|
+
if (attestations.length === 0) return null;
|
|
2994
|
+
let weightedSum = 0;
|
|
2995
|
+
let totalWeight = 0;
|
|
2996
|
+
for (const a of attestations) {
|
|
2997
|
+
const weight = TIER_WEIGHTS[a.tier];
|
|
2998
|
+
weightedSum += a.value * weight;
|
|
2999
|
+
totalWeight += weight;
|
|
3000
|
+
}
|
|
3001
|
+
return totalWeight > 0 ? weightedSum / totalWeight : null;
|
|
3002
|
+
}
|
|
3003
|
+
function tierDistribution(tiers) {
|
|
3004
|
+
const dist = {
|
|
3005
|
+
"verified-sovereign": 0,
|
|
3006
|
+
"verified-degraded": 0,
|
|
3007
|
+
"self-attested": 0,
|
|
3008
|
+
"unverified": 0
|
|
3009
|
+
};
|
|
3010
|
+
for (const tier of tiers) {
|
|
3011
|
+
dist[tier]++;
|
|
3012
|
+
}
|
|
3013
|
+
return dist;
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
// src/l4-reputation/tools.ts
|
|
3017
|
+
function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeResults) {
|
|
2449
3018
|
const reputationStore = new ReputationStore(storage, masterKey);
|
|
2450
3019
|
const identityEncryptionKey = derivePurposeKey(masterKey, "identity-encryption");
|
|
3020
|
+
const hsResults = handshakeResults ?? /* @__PURE__ */ new Map();
|
|
2451
3021
|
const tools = [
|
|
2452
3022
|
// ─── Reputation Recording ─────────────────────────────────────────
|
|
2453
3023
|
{
|
|
@@ -2509,26 +3079,34 @@ function createL4Tools(storage, masterKey, identityManager, auditLog) {
|
|
|
2509
3079
|
}
|
|
2510
3080
|
const outcome = args.outcome;
|
|
2511
3081
|
const context = args.context ?? "general";
|
|
3082
|
+
const counterpartyDid = args.counterparty_did;
|
|
3083
|
+
const hasSanctuaryIdentity = identityManager.list().some(
|
|
3084
|
+
(id) => identityManager.get(id.identity_id)?.did === counterpartyDid
|
|
3085
|
+
);
|
|
3086
|
+
const tierMeta = resolveTier(counterpartyDid, hsResults, hasSanctuaryIdentity);
|
|
2512
3087
|
const stored = await reputationStore.record(
|
|
2513
3088
|
args.interaction_id,
|
|
2514
|
-
|
|
3089
|
+
counterpartyDid,
|
|
2515
3090
|
outcome,
|
|
2516
3091
|
context,
|
|
2517
3092
|
identity,
|
|
2518
3093
|
identityEncryptionKey,
|
|
2519
|
-
args.counterparty_attestation
|
|
3094
|
+
args.counterparty_attestation,
|
|
3095
|
+
tierMeta.sovereignty_tier
|
|
2520
3096
|
);
|
|
2521
3097
|
auditLog.append("l4", "reputation_record", identity.identity_id, {
|
|
2522
3098
|
interaction_id: args.interaction_id,
|
|
2523
3099
|
outcome_type: outcome.type,
|
|
2524
3100
|
outcome_result: outcome.result,
|
|
2525
|
-
context
|
|
3101
|
+
context,
|
|
3102
|
+
sovereignty_tier: tierMeta.sovereignty_tier
|
|
2526
3103
|
});
|
|
2527
3104
|
return toolResult({
|
|
2528
3105
|
attestation_id: stored.attestation.attestation_id,
|
|
2529
3106
|
interaction_id: stored.attestation.data.interaction_id,
|
|
2530
3107
|
self_attestation: stored.attestation.signature,
|
|
2531
3108
|
counterparty_confirmed: stored.counterparty_confirmed,
|
|
3109
|
+
sovereignty_tier: tierMeta.sovereignty_tier,
|
|
2532
3110
|
context,
|
|
2533
3111
|
recorded_at: stored.recorded_at
|
|
2534
3112
|
});
|
|
@@ -2691,6 +3269,62 @@ function createL4Tools(storage, masterKey, identityManager, auditLog) {
|
|
|
2691
3269
|
});
|
|
2692
3270
|
}
|
|
2693
3271
|
},
|
|
3272
|
+
// ─── Sovereignty-Weighted Query ──────────────────────────────────
|
|
3273
|
+
{
|
|
3274
|
+
name: "sanctuary/reputation_query_weighted",
|
|
3275
|
+
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.",
|
|
3276
|
+
inputSchema: {
|
|
3277
|
+
type: "object",
|
|
3278
|
+
properties: {
|
|
3279
|
+
metric: {
|
|
3280
|
+
type: "string",
|
|
3281
|
+
description: "Which metric to compute the weighted score for"
|
|
3282
|
+
},
|
|
3283
|
+
context: {
|
|
3284
|
+
type: "string",
|
|
3285
|
+
description: "Filter by context/domain"
|
|
3286
|
+
},
|
|
3287
|
+
counterparty_did: {
|
|
3288
|
+
type: "string",
|
|
3289
|
+
description: "Filter by counterparty"
|
|
3290
|
+
}
|
|
3291
|
+
},
|
|
3292
|
+
required: ["metric"]
|
|
3293
|
+
},
|
|
3294
|
+
handler: async (args) => {
|
|
3295
|
+
const summary = await reputationStore.query({
|
|
3296
|
+
context: args.context,
|
|
3297
|
+
counterparty_did: args.counterparty_did
|
|
3298
|
+
});
|
|
3299
|
+
const allAttestations = await reputationStore.loadAllForTierScoring({
|
|
3300
|
+
context: args.context,
|
|
3301
|
+
counterparty_did: args.counterparty_did
|
|
3302
|
+
});
|
|
3303
|
+
const metric = args.metric;
|
|
3304
|
+
const tieredAttestations = allAttestations.filter((a) => a.attestation.data.metrics[metric] !== void 0).map((a) => ({
|
|
3305
|
+
value: a.attestation.data.metrics[metric],
|
|
3306
|
+
tier: a.attestation.data.sovereignty_tier ?? "unverified"
|
|
3307
|
+
}));
|
|
3308
|
+
const weightedScore = computeWeightedScore(tieredAttestations);
|
|
3309
|
+
const tiers = allAttestations.map(
|
|
3310
|
+
(a) => a.attestation.data.sovereignty_tier ?? "unverified"
|
|
3311
|
+
);
|
|
3312
|
+
const dist = tierDistribution(tiers);
|
|
3313
|
+
auditLog.append("l4", "reputation_query_weighted", "system", {
|
|
3314
|
+
metric,
|
|
3315
|
+
attestation_count: tieredAttestations.length,
|
|
3316
|
+
weighted_score: weightedScore
|
|
3317
|
+
});
|
|
3318
|
+
return toolResult({
|
|
3319
|
+
metric,
|
|
3320
|
+
weighted_score: weightedScore,
|
|
3321
|
+
attestation_count: tieredAttestations.length,
|
|
3322
|
+
tier_distribution: dist,
|
|
3323
|
+
tier_weights: TIER_WEIGHTS,
|
|
3324
|
+
unweighted_summary: summary
|
|
3325
|
+
});
|
|
3326
|
+
}
|
|
3327
|
+
},
|
|
2694
3328
|
// ─── Trust Bootstrap: Escrow ──────────────────────────────────────
|
|
2695
3329
|
{
|
|
2696
3330
|
name: "sanctuary/bootstrap_create_escrow",
|
|
@@ -2876,7 +3510,22 @@ var DEFAULT_POLICY = {
|
|
|
2876
3510
|
"monitor_audit_log",
|
|
2877
3511
|
"manifest",
|
|
2878
3512
|
"principal_policy_view",
|
|
2879
|
-
"principal_baseline_view"
|
|
3513
|
+
"principal_baseline_view",
|
|
3514
|
+
"shr_generate",
|
|
3515
|
+
"shr_verify",
|
|
3516
|
+
"handshake_initiate",
|
|
3517
|
+
"handshake_respond",
|
|
3518
|
+
"handshake_complete",
|
|
3519
|
+
"handshake_status",
|
|
3520
|
+
"reputation_query_weighted",
|
|
3521
|
+
"federation_peers",
|
|
3522
|
+
"federation_trust_evaluate",
|
|
3523
|
+
"federation_status",
|
|
3524
|
+
"zk_commit",
|
|
3525
|
+
"zk_prove",
|
|
3526
|
+
"zk_verify",
|
|
3527
|
+
"zk_range_prove",
|
|
3528
|
+
"zk_range_verify"
|
|
2880
3529
|
],
|
|
2881
3530
|
approval_channel: DEFAULT_CHANNEL
|
|
2882
3531
|
};
|
|
@@ -3011,6 +3660,21 @@ tier3_always_allow:
|
|
|
3011
3660
|
- manifest
|
|
3012
3661
|
- principal_policy_view
|
|
3013
3662
|
- principal_baseline_view
|
|
3663
|
+
- shr_generate
|
|
3664
|
+
- shr_verify
|
|
3665
|
+
- handshake_initiate
|
|
3666
|
+
- handshake_respond
|
|
3667
|
+
- handshake_complete
|
|
3668
|
+
- handshake_status
|
|
3669
|
+
- reputation_query_weighted
|
|
3670
|
+
- federation_peers
|
|
3671
|
+
- federation_trust_evaluate
|
|
3672
|
+
- federation_status
|
|
3673
|
+
- zk_commit
|
|
3674
|
+
- zk_prove
|
|
3675
|
+
- zk_verify
|
|
3676
|
+
- zk_range_prove
|
|
3677
|
+
- zk_range_verify
|
|
3014
3678
|
|
|
3015
3679
|
# \u2500\u2500\u2500 Approval Channel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3016
3680
|
# How Sanctuary reaches you when approval is needed.
|
|
@@ -3240,20 +3904,1143 @@ var StderrApprovalChannel = class {
|
|
|
3240
3904
|
}
|
|
3241
3905
|
};
|
|
3242
3906
|
|
|
3243
|
-
// src/principal-policy/
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3907
|
+
// src/principal-policy/dashboard-html.ts
|
|
3908
|
+
function generateDashboardHTML(options) {
|
|
3909
|
+
return `<!DOCTYPE html>
|
|
3910
|
+
<html lang="en">
|
|
3911
|
+
<head>
|
|
3912
|
+
<meta charset="utf-8">
|
|
3913
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3914
|
+
<title>Sanctuary \u2014 Principal Dashboard</title>
|
|
3915
|
+
<style>
|
|
3916
|
+
:root {
|
|
3917
|
+
--bg: #0f1117;
|
|
3918
|
+
--bg-surface: #1a1d27;
|
|
3919
|
+
--bg-elevated: #242736;
|
|
3920
|
+
--border: #2e3244;
|
|
3921
|
+
--text: #e4e6f0;
|
|
3922
|
+
--text-muted: #8b8fa3;
|
|
3923
|
+
--accent: #6c8aff;
|
|
3924
|
+
--accent-hover: #839dff;
|
|
3925
|
+
--approve: #3ecf8e;
|
|
3926
|
+
--approve-hover: #5dd9a3;
|
|
3927
|
+
--deny: #f87171;
|
|
3928
|
+
--deny-hover: #fca5a5;
|
|
3929
|
+
--warning: #fbbf24;
|
|
3930
|
+
--tier1: #f87171;
|
|
3931
|
+
--tier2: #fbbf24;
|
|
3932
|
+
--tier3: #3ecf8e;
|
|
3933
|
+
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
3934
|
+
--mono: "SF Mono", "Fira Code", "Cascadia Code", monospace;
|
|
3935
|
+
--radius: 8px;
|
|
3254
3936
|
}
|
|
3255
|
-
|
|
3256
|
-
|
|
3937
|
+
|
|
3938
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
3939
|
+
body {
|
|
3940
|
+
font-family: var(--font);
|
|
3941
|
+
background: var(--bg);
|
|
3942
|
+
color: var(--text);
|
|
3943
|
+
line-height: 1.5;
|
|
3944
|
+
min-height: 100vh;
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
/* Layout */
|
|
3948
|
+
.container { max-width: 960px; margin: 0 auto; padding: 24px 16px; }
|
|
3949
|
+
|
|
3950
|
+
header {
|
|
3951
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
3952
|
+
padding-bottom: 20px; border-bottom: 1px solid var(--border);
|
|
3953
|
+
margin-bottom: 24px;
|
|
3954
|
+
}
|
|
3955
|
+
header h1 { font-size: 20px; font-weight: 600; letter-spacing: -0.3px; }
|
|
3956
|
+
header h1 span { color: var(--accent); }
|
|
3957
|
+
.status-badge {
|
|
3958
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
3959
|
+
font-size: 12px; color: var(--text-muted);
|
|
3960
|
+
padding: 4px 10px; border-radius: 12px;
|
|
3961
|
+
background: var(--bg-surface); border: 1px solid var(--border);
|
|
3962
|
+
}
|
|
3963
|
+
.status-dot {
|
|
3964
|
+
width: 8px; height: 8px; border-radius: 50%;
|
|
3965
|
+
background: var(--approve); animation: pulse 2s infinite;
|
|
3966
|
+
}
|
|
3967
|
+
.status-dot.disconnected { background: var(--deny); animation: none; }
|
|
3968
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
3969
|
+
|
|
3970
|
+
/* Tabs */
|
|
3971
|
+
.tabs {
|
|
3972
|
+
display: flex; gap: 2px; margin-bottom: 20px;
|
|
3973
|
+
background: var(--bg-surface); border-radius: var(--radius);
|
|
3974
|
+
padding: 3px; border: 1px solid var(--border);
|
|
3975
|
+
}
|
|
3976
|
+
.tab {
|
|
3977
|
+
flex: 1; padding: 8px 12px; text-align: center;
|
|
3978
|
+
font-size: 13px; font-weight: 500; cursor: pointer;
|
|
3979
|
+
border-radius: 6px; border: none; color: var(--text-muted);
|
|
3980
|
+
background: transparent; transition: all 0.15s;
|
|
3981
|
+
}
|
|
3982
|
+
.tab:hover { color: var(--text); }
|
|
3983
|
+
.tab.active { background: var(--bg-elevated); color: var(--text); }
|
|
3984
|
+
.tab .count {
|
|
3985
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
3986
|
+
min-width: 18px; height: 18px; padding: 0 5px;
|
|
3987
|
+
font-size: 11px; font-weight: 600; border-radius: 9px;
|
|
3988
|
+
margin-left: 6px;
|
|
3989
|
+
}
|
|
3990
|
+
.tab .count.alert { background: var(--deny); color: white; }
|
|
3991
|
+
.tab .count.muted { background: var(--border); color: var(--text-muted); }
|
|
3992
|
+
|
|
3993
|
+
/* Tab Content */
|
|
3994
|
+
.tab-content { display: none; }
|
|
3995
|
+
.tab-content.active { display: block; }
|
|
3996
|
+
|
|
3997
|
+
/* Pending Requests */
|
|
3998
|
+
.pending-empty {
|
|
3999
|
+
text-align: center; padding: 60px 20px; color: var(--text-muted);
|
|
4000
|
+
}
|
|
4001
|
+
.pending-empty .icon { font-size: 32px; margin-bottom: 12px; }
|
|
4002
|
+
.pending-empty p { font-size: 14px; }
|
|
4003
|
+
|
|
4004
|
+
.request-card {
|
|
4005
|
+
background: var(--bg-surface); border: 1px solid var(--border);
|
|
4006
|
+
border-radius: var(--radius); padding: 16px; margin-bottom: 12px;
|
|
4007
|
+
animation: slideIn 0.2s ease-out;
|
|
4008
|
+
}
|
|
4009
|
+
@keyframes slideIn { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } }
|
|
4010
|
+
.request-card.tier1 { border-left: 3px solid var(--tier1); }
|
|
4011
|
+
.request-card.tier2 { border-left: 3px solid var(--tier2); }
|
|
4012
|
+
.request-header {
|
|
4013
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
4014
|
+
margin-bottom: 10px;
|
|
4015
|
+
}
|
|
4016
|
+
.request-op {
|
|
4017
|
+
font-family: var(--mono); font-size: 14px; font-weight: 600;
|
|
4018
|
+
}
|
|
4019
|
+
.tier-badge {
|
|
4020
|
+
font-size: 11px; font-weight: 600; padding: 2px 8px;
|
|
4021
|
+
border-radius: 4px; text-transform: uppercase;
|
|
4022
|
+
}
|
|
4023
|
+
.tier-badge.tier1 { background: rgba(248,113,113,0.15); color: var(--tier1); }
|
|
4024
|
+
.tier-badge.tier2 { background: rgba(251,191,36,0.15); color: var(--tier2); }
|
|
4025
|
+
.request-reason {
|
|
4026
|
+
font-size: 13px; color: var(--text-muted); margin-bottom: 12px;
|
|
4027
|
+
}
|
|
4028
|
+
.request-context {
|
|
4029
|
+
font-family: var(--mono); font-size: 12px; color: var(--text-muted);
|
|
4030
|
+
background: var(--bg); border-radius: 4px; padding: 8px 10px;
|
|
4031
|
+
margin-bottom: 14px; white-space: pre-wrap; word-break: break-all;
|
|
4032
|
+
max-height: 120px; overflow-y: auto;
|
|
4033
|
+
}
|
|
4034
|
+
.request-actions {
|
|
4035
|
+
display: flex; align-items: center; gap: 10px;
|
|
4036
|
+
}
|
|
4037
|
+
.btn {
|
|
4038
|
+
padding: 7px 16px; border-radius: 6px; font-size: 13px;
|
|
4039
|
+
font-weight: 600; border: none; cursor: pointer;
|
|
4040
|
+
transition: all 0.15s;
|
|
4041
|
+
}
|
|
4042
|
+
.btn-approve { background: var(--approve); color: #0f1117; }
|
|
4043
|
+
.btn-approve:hover { background: var(--approve-hover); }
|
|
4044
|
+
.btn-deny { background: var(--deny); color: white; }
|
|
4045
|
+
.btn-deny:hover { background: var(--deny-hover); }
|
|
4046
|
+
.countdown {
|
|
4047
|
+
margin-left: auto; font-size: 12px; color: var(--text-muted);
|
|
4048
|
+
font-family: var(--mono);
|
|
4049
|
+
}
|
|
4050
|
+
.countdown.urgent { color: var(--deny); font-weight: 600; }
|
|
4051
|
+
|
|
4052
|
+
/* Audit Log */
|
|
4053
|
+
.audit-table { width: 100%; border-collapse: collapse; }
|
|
4054
|
+
.audit-table th {
|
|
4055
|
+
text-align: left; font-size: 11px; font-weight: 600;
|
|
4056
|
+
text-transform: uppercase; letter-spacing: 0.5px;
|
|
4057
|
+
color: var(--text-muted); padding: 8px 10px;
|
|
4058
|
+
border-bottom: 1px solid var(--border);
|
|
4059
|
+
}
|
|
4060
|
+
.audit-table td {
|
|
4061
|
+
font-size: 13px; padding: 8px 10px;
|
|
4062
|
+
border-bottom: 1px solid var(--border);
|
|
4063
|
+
}
|
|
4064
|
+
.audit-table tr { transition: background 0.1s; }
|
|
4065
|
+
.audit-table tr:hover { background: var(--bg-elevated); }
|
|
4066
|
+
.audit-table tr.new { animation: highlight 1s ease-out; }
|
|
4067
|
+
@keyframes highlight { from { background: rgba(108,138,255,0.15); } to { background: transparent; } }
|
|
4068
|
+
.audit-time { font-family: var(--mono); font-size: 12px; color: var(--text-muted); }
|
|
4069
|
+
.audit-op { font-family: var(--mono); font-size: 12px; }
|
|
4070
|
+
.audit-layer {
|
|
4071
|
+
font-size: 11px; font-weight: 600; padding: 1px 6px;
|
|
4072
|
+
border-radius: 3px; text-transform: uppercase;
|
|
4073
|
+
}
|
|
4074
|
+
.audit-layer.l1 { background: rgba(108,138,255,0.15); color: var(--accent); }
|
|
4075
|
+
.audit-layer.l2 { background: rgba(251,191,36,0.15); color: var(--tier2); }
|
|
4076
|
+
.audit-layer.l3 { background: rgba(62,207,142,0.15); color: var(--tier3); }
|
|
4077
|
+
.audit-layer.l4 { background: rgba(168,85,247,0.15); color: #a855f7; }
|
|
4078
|
+
|
|
4079
|
+
/* Baseline & Policy */
|
|
4080
|
+
.info-section {
|
|
4081
|
+
background: var(--bg-surface); border: 1px solid var(--border);
|
|
4082
|
+
border-radius: var(--radius); padding: 16px; margin-bottom: 16px;
|
|
4083
|
+
}
|
|
4084
|
+
.info-section h3 {
|
|
4085
|
+
font-size: 13px; font-weight: 600; text-transform: uppercase;
|
|
4086
|
+
letter-spacing: 0.5px; color: var(--text-muted); margin-bottom: 12px;
|
|
4087
|
+
}
|
|
4088
|
+
.info-row {
|
|
4089
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
4090
|
+
padding: 6px 0; font-size: 13px;
|
|
4091
|
+
}
|
|
4092
|
+
.info-label { color: var(--text-muted); }
|
|
4093
|
+
.info-value { font-family: var(--mono); font-size: 12px; }
|
|
4094
|
+
.tag-list { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
4095
|
+
.tag {
|
|
4096
|
+
font-family: var(--mono); font-size: 11px; padding: 2px 8px;
|
|
4097
|
+
background: var(--bg-elevated); border-radius: 4px;
|
|
4098
|
+
color: var(--text-muted); border: 1px solid var(--border);
|
|
4099
|
+
}
|
|
4100
|
+
.policy-op {
|
|
4101
|
+
font-family: var(--mono); font-size: 12px; padding: 3px 0;
|
|
4102
|
+
}
|
|
4103
|
+
|
|
4104
|
+
/* Footer */
|
|
4105
|
+
footer {
|
|
4106
|
+
margin-top: 32px; padding-top: 16px;
|
|
4107
|
+
border-top: 1px solid var(--border);
|
|
4108
|
+
font-size: 12px; color: var(--text-muted);
|
|
4109
|
+
text-align: center;
|
|
4110
|
+
}
|
|
4111
|
+
</style>
|
|
4112
|
+
</head>
|
|
4113
|
+
<body>
|
|
4114
|
+
<div class="container">
|
|
4115
|
+
<header>
|
|
4116
|
+
<h1><span>Sanctuary</span> Principal Dashboard</h1>
|
|
4117
|
+
<div class="status-badge">
|
|
4118
|
+
<div class="status-dot" id="statusDot"></div>
|
|
4119
|
+
<span id="statusText">Connected</span>
|
|
4120
|
+
</div>
|
|
4121
|
+
</header>
|
|
4122
|
+
|
|
4123
|
+
<div class="tabs">
|
|
4124
|
+
<button class="tab active" data-tab="pending">
|
|
4125
|
+
Pending<span class="count muted" id="pendingCount">0</span>
|
|
4126
|
+
</button>
|
|
4127
|
+
<button class="tab" data-tab="audit">
|
|
4128
|
+
Audit Log<span class="count muted" id="auditCount">0</span>
|
|
4129
|
+
</button>
|
|
4130
|
+
<button class="tab" data-tab="baseline">Baseline</button>
|
|
4131
|
+
<button class="tab" data-tab="policy">Policy</button>
|
|
4132
|
+
</div>
|
|
4133
|
+
|
|
4134
|
+
<!-- Pending Approvals -->
|
|
4135
|
+
<div class="tab-content active" id="tab-pending">
|
|
4136
|
+
<div class="pending-empty" id="pendingEmpty">
|
|
4137
|
+
<div class="icon">✔</div>
|
|
4138
|
+
<p>No pending approval requests.</p>
|
|
4139
|
+
<p style="font-size:12px; margin-top:4px;">Requests will appear here in real time.</p>
|
|
4140
|
+
</div>
|
|
4141
|
+
<div id="pendingList"></div>
|
|
4142
|
+
</div>
|
|
4143
|
+
|
|
4144
|
+
<!-- Audit Log -->
|
|
4145
|
+
<div class="tab-content" id="tab-audit">
|
|
4146
|
+
<table class="audit-table">
|
|
4147
|
+
<thead>
|
|
4148
|
+
<tr><th>Time</th><th>Layer</th><th>Operation</th><th>Identity</th></tr>
|
|
4149
|
+
</thead>
|
|
4150
|
+
<tbody id="auditBody"></tbody>
|
|
4151
|
+
</table>
|
|
4152
|
+
</div>
|
|
4153
|
+
|
|
4154
|
+
<!-- Baseline -->
|
|
4155
|
+
<div class="tab-content" id="tab-baseline">
|
|
4156
|
+
<div class="info-section">
|
|
4157
|
+
<h3>Session Info</h3>
|
|
4158
|
+
<div class="info-row"><span class="info-label">First session</span><span class="info-value" id="bFirstSession">\u2014</span></div>
|
|
4159
|
+
<div class="info-row"><span class="info-label">Started</span><span class="info-value" id="bStarted">\u2014</span></div>
|
|
4160
|
+
</div>
|
|
4161
|
+
<div class="info-section">
|
|
4162
|
+
<h3>Known Namespaces</h3>
|
|
4163
|
+
<div class="tag-list" id="bNamespaces"><span class="tag">\u2014</span></div>
|
|
4164
|
+
</div>
|
|
4165
|
+
<div class="info-section">
|
|
4166
|
+
<h3>Known Counterparties</h3>
|
|
4167
|
+
<div class="tag-list" id="bCounterparties"><span class="tag">\u2014</span></div>
|
|
4168
|
+
</div>
|
|
4169
|
+
<div class="info-section">
|
|
4170
|
+
<h3>Tool Call Counts</h3>
|
|
4171
|
+
<div id="bToolCalls"><span class="info-value">\u2014</span></div>
|
|
4172
|
+
</div>
|
|
4173
|
+
</div>
|
|
4174
|
+
|
|
4175
|
+
<!-- Policy -->
|
|
4176
|
+
<div class="tab-content" id="tab-policy">
|
|
4177
|
+
<div class="info-section">
|
|
4178
|
+
<h3>Tier 1 \u2014 Always Requires Approval</h3>
|
|
4179
|
+
<div id="pTier1"></div>
|
|
4180
|
+
</div>
|
|
4181
|
+
<div class="info-section">
|
|
4182
|
+
<h3>Tier 2 \u2014 Anomaly Detection</h3>
|
|
4183
|
+
<div id="pTier2"></div>
|
|
4184
|
+
</div>
|
|
4185
|
+
<div class="info-section">
|
|
4186
|
+
<h3>Tier 3 \u2014 Always Allowed</h3>
|
|
4187
|
+
<div class="info-row">
|
|
4188
|
+
<span class="info-label">Operations</span>
|
|
4189
|
+
<span class="info-value" id="pTier3Count">\u2014</span>
|
|
4190
|
+
</div>
|
|
4191
|
+
</div>
|
|
4192
|
+
<div class="info-section">
|
|
4193
|
+
<h3>Approval Channel</h3>
|
|
4194
|
+
<div id="pChannel"></div>
|
|
4195
|
+
</div>
|
|
4196
|
+
</div>
|
|
4197
|
+
|
|
4198
|
+
<footer>Sanctuary Framework v${options.serverVersion} \u2014 Principal Dashboard</footer>
|
|
4199
|
+
</div>
|
|
4200
|
+
|
|
4201
|
+
<script>
|
|
4202
|
+
(function() {
|
|
4203
|
+
const TIMEOUT = ${options.timeoutSeconds};
|
|
4204
|
+
const AUTH_TOKEN = ${options.authToken ? `'${options.authToken}'` : "null"};
|
|
4205
|
+
const pending = new Map();
|
|
4206
|
+
let auditCount = 0;
|
|
4207
|
+
|
|
4208
|
+
// Auth helpers
|
|
4209
|
+
function authHeaders() {
|
|
4210
|
+
const h = { 'Content-Type': 'application/json' };
|
|
4211
|
+
if (AUTH_TOKEN) h['Authorization'] = 'Bearer ' + AUTH_TOKEN;
|
|
4212
|
+
return h;
|
|
4213
|
+
}
|
|
4214
|
+
function authQuery(url) {
|
|
4215
|
+
if (!AUTH_TOKEN) return url;
|
|
4216
|
+
const sep = url.includes('?') ? '&' : '?';
|
|
4217
|
+
return url + sep + 'token=' + AUTH_TOKEN;
|
|
4218
|
+
}
|
|
4219
|
+
|
|
4220
|
+
// Tab switching
|
|
4221
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
4222
|
+
tab.addEventListener('click', () => {
|
|
4223
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
4224
|
+
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
4225
|
+
tab.classList.add('active');
|
|
4226
|
+
document.getElementById('tab-' + tab.dataset.tab).classList.add('active');
|
|
4227
|
+
});
|
|
4228
|
+
});
|
|
4229
|
+
|
|
4230
|
+
// SSE Connection
|
|
4231
|
+
let evtSource;
|
|
4232
|
+
function connect() {
|
|
4233
|
+
evtSource = new EventSource(authQuery('/events'));
|
|
4234
|
+
evtSource.onopen = () => {
|
|
4235
|
+
document.getElementById('statusDot').classList.remove('disconnected');
|
|
4236
|
+
document.getElementById('statusText').textContent = 'Connected';
|
|
4237
|
+
};
|
|
4238
|
+
evtSource.onerror = () => {
|
|
4239
|
+
document.getElementById('statusDot').classList.add('disconnected');
|
|
4240
|
+
document.getElementById('statusText').textContent = 'Reconnecting...';
|
|
4241
|
+
};
|
|
4242
|
+
evtSource.addEventListener('pending-request', (e) => {
|
|
4243
|
+
const data = JSON.parse(e.data);
|
|
4244
|
+
addPendingRequest(data);
|
|
4245
|
+
});
|
|
4246
|
+
evtSource.addEventListener('request-resolved', (e) => {
|
|
4247
|
+
const data = JSON.parse(e.data);
|
|
4248
|
+
removePendingRequest(data.request_id);
|
|
4249
|
+
});
|
|
4250
|
+
evtSource.addEventListener('audit-entry', (e) => {
|
|
4251
|
+
const data = JSON.parse(e.data);
|
|
4252
|
+
addAuditEntry(data);
|
|
4253
|
+
});
|
|
4254
|
+
evtSource.addEventListener('baseline-update', (e) => {
|
|
4255
|
+
const data = JSON.parse(e.data);
|
|
4256
|
+
updateBaseline(data);
|
|
4257
|
+
});
|
|
4258
|
+
evtSource.addEventListener('policy-update', (e) => {
|
|
4259
|
+
const data = JSON.parse(e.data);
|
|
4260
|
+
updatePolicy(data);
|
|
4261
|
+
});
|
|
4262
|
+
evtSource.addEventListener('init', (e) => {
|
|
4263
|
+
const data = JSON.parse(e.data);
|
|
4264
|
+
if (data.baseline) updateBaseline(data.baseline);
|
|
4265
|
+
if (data.policy) updatePolicy(data.policy);
|
|
4266
|
+
if (data.pending) data.pending.forEach(addPendingRequest);
|
|
4267
|
+
if (data.audit) data.audit.forEach(addAuditEntry);
|
|
4268
|
+
});
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
// Pending requests
|
|
4272
|
+
function addPendingRequest(req) {
|
|
4273
|
+
pending.set(req.request_id, { ...req, remaining: TIMEOUT });
|
|
4274
|
+
renderPending();
|
|
4275
|
+
updatePendingCount();
|
|
4276
|
+
flashTab('pending');
|
|
4277
|
+
}
|
|
4278
|
+
|
|
4279
|
+
function removePendingRequest(id) {
|
|
4280
|
+
pending.delete(id);
|
|
4281
|
+
renderPending();
|
|
4282
|
+
updatePendingCount();
|
|
4283
|
+
}
|
|
4284
|
+
|
|
4285
|
+
function renderPending() {
|
|
4286
|
+
const list = document.getElementById('pendingList');
|
|
4287
|
+
const empty = document.getElementById('pendingEmpty');
|
|
4288
|
+
if (pending.size === 0) {
|
|
4289
|
+
list.innerHTML = '';
|
|
4290
|
+
empty.style.display = 'block';
|
|
4291
|
+
return;
|
|
4292
|
+
}
|
|
4293
|
+
empty.style.display = 'none';
|
|
4294
|
+
list.innerHTML = '';
|
|
4295
|
+
for (const [id, req] of pending) {
|
|
4296
|
+
const card = document.createElement('div');
|
|
4297
|
+
card.className = 'request-card tier' + req.tier;
|
|
4298
|
+
card.id = 'req-' + id;
|
|
4299
|
+
const ctx = typeof req.context === 'string' ? req.context : JSON.stringify(req.context, null, 2);
|
|
4300
|
+
card.innerHTML =
|
|
4301
|
+
'<div class="request-header">' +
|
|
4302
|
+
'<span class="request-op">' + esc(req.operation) + '</span>' +
|
|
4303
|
+
'<span class="tier-badge tier' + req.tier + '">Tier ' + req.tier + '</span>' +
|
|
4304
|
+
'</div>' +
|
|
4305
|
+
'<div class="request-reason">' + esc(req.reason) + '</div>' +
|
|
4306
|
+
'<div class="request-context">' + esc(ctx) + '</div>' +
|
|
4307
|
+
'<div class="request-actions">' +
|
|
4308
|
+
'<button class="btn btn-approve" onclick="handleApprove(\\'' + id + '\\')">Approve</button>' +
|
|
4309
|
+
'<button class="btn btn-deny" onclick="handleDeny(\\'' + id + '\\')">Deny</button>' +
|
|
4310
|
+
'<span class="countdown" id="cd-' + id + '">' + req.remaining + 's</span>' +
|
|
4311
|
+
'</div>';
|
|
4312
|
+
list.appendChild(card);
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
|
|
4316
|
+
function updatePendingCount() {
|
|
4317
|
+
const el = document.getElementById('pendingCount');
|
|
4318
|
+
el.textContent = pending.size;
|
|
4319
|
+
el.className = pending.size > 0 ? 'count alert' : 'count muted';
|
|
4320
|
+
}
|
|
4321
|
+
|
|
4322
|
+
function flashTab(name) {
|
|
4323
|
+
const tab = document.querySelector('[data-tab="' + name + '"]');
|
|
4324
|
+
if (!tab.classList.contains('active')) {
|
|
4325
|
+
tab.style.background = 'rgba(248,113,113,0.15)';
|
|
4326
|
+
setTimeout(() => { tab.style.background = ''; }, 1500);
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
|
|
4330
|
+
// Countdown timer
|
|
4331
|
+
setInterval(() => {
|
|
4332
|
+
for (const [id, req] of pending) {
|
|
4333
|
+
req.remaining = Math.max(0, req.remaining - 1);
|
|
4334
|
+
const el = document.getElementById('cd-' + id);
|
|
4335
|
+
if (el) {
|
|
4336
|
+
el.textContent = req.remaining + 's';
|
|
4337
|
+
el.className = req.remaining <= 30 ? 'countdown urgent' : 'countdown';
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
}, 1000);
|
|
4341
|
+
|
|
4342
|
+
// Approve / Deny handlers (global scope)
|
|
4343
|
+
window.handleApprove = function(id) {
|
|
4344
|
+
fetch('/api/approve/' + id, { method: 'POST', headers: authHeaders() }).then(() => {
|
|
4345
|
+
removePendingRequest(id);
|
|
4346
|
+
});
|
|
4347
|
+
};
|
|
4348
|
+
window.handleDeny = function(id) {
|
|
4349
|
+
fetch('/api/deny/' + id, { method: 'POST', headers: authHeaders() }).then(() => {
|
|
4350
|
+
removePendingRequest(id);
|
|
4351
|
+
});
|
|
4352
|
+
};
|
|
4353
|
+
|
|
4354
|
+
// Audit log
|
|
4355
|
+
function addAuditEntry(entry) {
|
|
4356
|
+
auditCount++;
|
|
4357
|
+
document.getElementById('auditCount').textContent = auditCount;
|
|
4358
|
+
const tbody = document.getElementById('auditBody');
|
|
4359
|
+
const tr = document.createElement('tr');
|
|
4360
|
+
tr.className = 'new';
|
|
4361
|
+
const time = entry.timestamp ? new Date(entry.timestamp).toLocaleTimeString() : '\u2014';
|
|
4362
|
+
const layer = entry.layer || '\u2014';
|
|
4363
|
+
tr.innerHTML =
|
|
4364
|
+
'<td class="audit-time">' + esc(time) + '</td>' +
|
|
4365
|
+
'<td><span class="audit-layer ' + layer + '">' + esc(layer) + '</span></td>' +
|
|
4366
|
+
'<td class="audit-op">' + esc(entry.operation || '\u2014') + '</td>' +
|
|
4367
|
+
'<td style="font-size:12px;color:var(--text-muted)">' + esc(entry.identity_id || '\u2014') + '</td>';
|
|
4368
|
+
tbody.insertBefore(tr, tbody.firstChild);
|
|
4369
|
+
// Keep last 100 entries
|
|
4370
|
+
while (tbody.children.length > 100) tbody.removeChild(tbody.lastChild);
|
|
4371
|
+
}
|
|
4372
|
+
|
|
4373
|
+
// Baseline
|
|
4374
|
+
function updateBaseline(b) {
|
|
4375
|
+
if (!b) return;
|
|
4376
|
+
document.getElementById('bFirstSession').textContent = b.is_first_session ? 'Yes' : 'No';
|
|
4377
|
+
document.getElementById('bStarted').textContent = b.started_at ? new Date(b.started_at).toLocaleString() : '\u2014';
|
|
4378
|
+
const ns = document.getElementById('bNamespaces');
|
|
4379
|
+
ns.innerHTML = (b.known_namespaces || []).length > 0
|
|
4380
|
+
? (b.known_namespaces || []).map(n => '<span class="tag">' + esc(n) + '</span>').join('')
|
|
4381
|
+
: '<span class="tag">none</span>';
|
|
4382
|
+
const cp = document.getElementById('bCounterparties');
|
|
4383
|
+
cp.innerHTML = (b.known_counterparties || []).length > 0
|
|
4384
|
+
? (b.known_counterparties || []).map(c => '<span class="tag">' + esc(c.slice(0,16)) + '...</span>').join('')
|
|
4385
|
+
: '<span class="tag">none</span>';
|
|
4386
|
+
const tc = document.getElementById('bToolCalls');
|
|
4387
|
+
const counts = b.tool_call_counts || {};
|
|
4388
|
+
const entries = Object.entries(counts).sort((a,b) => b[1] - a[1]);
|
|
4389
|
+
tc.innerHTML = entries.length > 0
|
|
4390
|
+
? entries.map(([k,v]) => '<div class="info-row"><span class="info-label">' + esc(k) + '</span><span class="info-value">' + v + '</span></div>').join('')
|
|
4391
|
+
: '<span class="info-value">no calls yet</span>';
|
|
4392
|
+
}
|
|
4393
|
+
|
|
4394
|
+
// Policy
|
|
4395
|
+
function updatePolicy(p) {
|
|
4396
|
+
if (!p) return;
|
|
4397
|
+
const t1 = document.getElementById('pTier1');
|
|
4398
|
+
t1.innerHTML = (p.tier1_always_approve || []).map(op =>
|
|
4399
|
+
'<div class="policy-op">' + esc(op) + '</div>'
|
|
4400
|
+
).join('');
|
|
4401
|
+
const t2 = document.getElementById('pTier2');
|
|
4402
|
+
const cfg = p.tier2_anomaly || {};
|
|
4403
|
+
t2.innerHTML = Object.entries(cfg).map(([k,v]) =>
|
|
4404
|
+
'<div class="info-row"><span class="info-label">' + esc(k) + '</span><span class="info-value">' + esc(String(v)) + '</span></div>'
|
|
4405
|
+
).join('');
|
|
4406
|
+
document.getElementById('pTier3Count').textContent = (p.tier3_always_allow || []).length + ' operations';
|
|
4407
|
+
const ch = document.getElementById('pChannel');
|
|
4408
|
+
const chan = p.approval_channel || {};
|
|
4409
|
+
ch.innerHTML = Object.entries(chan).filter(([k]) => k !== 'webhook_secret').map(([k,v]) =>
|
|
4410
|
+
'<div class="info-row"><span class="info-label">' + esc(k) + '</span><span class="info-value">' + esc(String(v)) + '</span></div>'
|
|
4411
|
+
).join('');
|
|
4412
|
+
}
|
|
4413
|
+
|
|
4414
|
+
function esc(s) {
|
|
4415
|
+
if (!s) return '';
|
|
4416
|
+
const d = document.createElement('div');
|
|
4417
|
+
d.textContent = String(s);
|
|
4418
|
+
return d.innerHTML;
|
|
4419
|
+
}
|
|
4420
|
+
|
|
4421
|
+
// Init
|
|
4422
|
+
connect();
|
|
4423
|
+
fetch('/api/status', { headers: authHeaders() }).then(r => r.json()).then(data => {
|
|
4424
|
+
if (data.baseline) updateBaseline(data.baseline);
|
|
4425
|
+
if (data.policy) updatePolicy(data.policy);
|
|
4426
|
+
}).catch(() => {});
|
|
4427
|
+
})();
|
|
4428
|
+
</script>
|
|
4429
|
+
</body>
|
|
4430
|
+
</html>`;
|
|
4431
|
+
}
|
|
4432
|
+
|
|
4433
|
+
// src/principal-policy/dashboard.ts
|
|
4434
|
+
var DashboardApprovalChannel = class {
|
|
4435
|
+
config;
|
|
4436
|
+
pending = /* @__PURE__ */ new Map();
|
|
4437
|
+
sseClients = /* @__PURE__ */ new Set();
|
|
4438
|
+
httpServer = null;
|
|
4439
|
+
policy = null;
|
|
4440
|
+
baseline = null;
|
|
4441
|
+
auditLog = null;
|
|
4442
|
+
dashboardHTML;
|
|
4443
|
+
authToken;
|
|
4444
|
+
useTLS;
|
|
4445
|
+
constructor(config) {
|
|
4446
|
+
this.config = config;
|
|
4447
|
+
this.authToken = config.auth_token;
|
|
4448
|
+
this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
|
|
4449
|
+
this.dashboardHTML = generateDashboardHTML({
|
|
4450
|
+
timeoutSeconds: config.timeout_seconds,
|
|
4451
|
+
serverVersion: "0.3.0",
|
|
4452
|
+
authToken: this.authToken
|
|
4453
|
+
});
|
|
4454
|
+
}
|
|
4455
|
+
/**
|
|
4456
|
+
* Inject dependencies after construction.
|
|
4457
|
+
* Called from index.ts after all components are initialized.
|
|
4458
|
+
*/
|
|
4459
|
+
setDependencies(deps) {
|
|
4460
|
+
this.policy = deps.policy;
|
|
4461
|
+
this.baseline = deps.baseline;
|
|
4462
|
+
this.auditLog = deps.auditLog;
|
|
4463
|
+
}
|
|
4464
|
+
/**
|
|
4465
|
+
* Start the HTTP(S) server for the dashboard.
|
|
4466
|
+
*/
|
|
4467
|
+
async start() {
|
|
4468
|
+
return new Promise((resolve, reject) => {
|
|
4469
|
+
const handler = (req, res) => this.handleRequest(req, res);
|
|
4470
|
+
if (this.useTLS && this.config.tls) {
|
|
4471
|
+
const tlsOpts = {
|
|
4472
|
+
cert: readFileSync(this.config.tls.cert_path),
|
|
4473
|
+
key: readFileSync(this.config.tls.key_path)
|
|
4474
|
+
};
|
|
4475
|
+
this.httpServer = createServer$1(tlsOpts, handler);
|
|
4476
|
+
} else {
|
|
4477
|
+
this.httpServer = createServer$2(handler);
|
|
4478
|
+
}
|
|
4479
|
+
const protocol = this.useTLS ? "https" : "http";
|
|
4480
|
+
const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
|
|
4481
|
+
this.httpServer.listen(this.config.port, this.config.host, () => {
|
|
4482
|
+
if (this.authToken) {
|
|
4483
|
+
process.stderr.write(
|
|
4484
|
+
`
|
|
4485
|
+
Sanctuary Principal Dashboard: ${baseUrl}/?token=${this.authToken}
|
|
4486
|
+
`
|
|
4487
|
+
);
|
|
4488
|
+
process.stderr.write(
|
|
4489
|
+
` Auth token: ${this.authToken}
|
|
4490
|
+
|
|
4491
|
+
`
|
|
4492
|
+
);
|
|
4493
|
+
} else {
|
|
4494
|
+
process.stderr.write(
|
|
4495
|
+
`
|
|
4496
|
+
Sanctuary Principal Dashboard: ${baseUrl}
|
|
4497
|
+
|
|
4498
|
+
`
|
|
4499
|
+
);
|
|
4500
|
+
}
|
|
4501
|
+
resolve();
|
|
4502
|
+
});
|
|
4503
|
+
this.httpServer.on("error", reject);
|
|
4504
|
+
});
|
|
4505
|
+
}
|
|
4506
|
+
/**
|
|
4507
|
+
* Stop the HTTP server and clean up.
|
|
4508
|
+
*/
|
|
4509
|
+
async stop() {
|
|
4510
|
+
for (const [, pending] of this.pending) {
|
|
4511
|
+
clearTimeout(pending.timer);
|
|
4512
|
+
pending.resolve({
|
|
4513
|
+
decision: "deny",
|
|
4514
|
+
decided_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4515
|
+
decided_by: "auto"
|
|
4516
|
+
});
|
|
4517
|
+
}
|
|
4518
|
+
this.pending.clear();
|
|
4519
|
+
for (const client of this.sseClients) {
|
|
4520
|
+
client.end();
|
|
4521
|
+
}
|
|
4522
|
+
this.sseClients.clear();
|
|
4523
|
+
if (this.httpServer) {
|
|
4524
|
+
return new Promise((resolve) => {
|
|
4525
|
+
this.httpServer.close(() => resolve());
|
|
4526
|
+
});
|
|
4527
|
+
}
|
|
4528
|
+
}
|
|
4529
|
+
/**
|
|
4530
|
+
* Request approval from the human via the dashboard.
|
|
4531
|
+
* Blocks until the human approves/denies or timeout occurs.
|
|
4532
|
+
*/
|
|
4533
|
+
async requestApproval(request) {
|
|
4534
|
+
const id = randomBytes$1(8).toString("hex");
|
|
4535
|
+
process.stderr.write(
|
|
4536
|
+
`[Sanctuary] Approval required: ${request.operation} (Tier ${request.tier}) \u2014 open dashboard to respond
|
|
4537
|
+
`
|
|
4538
|
+
);
|
|
4539
|
+
return new Promise((resolve) => {
|
|
4540
|
+
const timer = setTimeout(() => {
|
|
4541
|
+
this.pending.delete(id);
|
|
4542
|
+
const response = {
|
|
4543
|
+
decision: this.config.auto_deny ? "deny" : "approve",
|
|
4544
|
+
decided_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4545
|
+
decided_by: "timeout"
|
|
4546
|
+
};
|
|
4547
|
+
this.broadcastSSE("request-resolved", {
|
|
4548
|
+
request_id: id,
|
|
4549
|
+
decision: response.decision,
|
|
4550
|
+
decided_by: "timeout"
|
|
4551
|
+
});
|
|
4552
|
+
resolve(response);
|
|
4553
|
+
}, this.config.timeout_seconds * 1e3);
|
|
4554
|
+
const pending = {
|
|
4555
|
+
id,
|
|
4556
|
+
request,
|
|
4557
|
+
resolve,
|
|
4558
|
+
timer,
|
|
4559
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4560
|
+
};
|
|
4561
|
+
this.pending.set(id, pending);
|
|
4562
|
+
this.broadcastSSE("pending-request", {
|
|
4563
|
+
request_id: id,
|
|
4564
|
+
operation: request.operation,
|
|
4565
|
+
tier: request.tier,
|
|
4566
|
+
reason: request.reason,
|
|
4567
|
+
context: request.context,
|
|
4568
|
+
timestamp: request.timestamp
|
|
4569
|
+
});
|
|
4570
|
+
});
|
|
4571
|
+
}
|
|
4572
|
+
// ── Authentication ──────────────────────────────────────────────────
|
|
4573
|
+
/**
|
|
4574
|
+
* Verify bearer token authentication.
|
|
4575
|
+
* Checks Authorization header first, falls back to ?token= query param.
|
|
4576
|
+
* Returns true if auth passes, false if blocked (response already sent).
|
|
4577
|
+
*/
|
|
4578
|
+
checkAuth(req, url, res) {
|
|
4579
|
+
if (!this.authToken) return true;
|
|
4580
|
+
const authHeader = req.headers.authorization;
|
|
4581
|
+
if (authHeader) {
|
|
4582
|
+
const parts = authHeader.split(" ");
|
|
4583
|
+
if (parts.length === 2 && parts[0] === "Bearer" && parts[1] === this.authToken) {
|
|
4584
|
+
return true;
|
|
4585
|
+
}
|
|
4586
|
+
}
|
|
4587
|
+
const queryToken = url.searchParams.get("token");
|
|
4588
|
+
if (queryToken === this.authToken) {
|
|
4589
|
+
return true;
|
|
4590
|
+
}
|
|
4591
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
4592
|
+
res.end(JSON.stringify({ error: "Unauthorized \u2014 valid bearer token required" }));
|
|
4593
|
+
return false;
|
|
4594
|
+
}
|
|
4595
|
+
// ── HTTP Request Handler ────────────────────────────────────────────
|
|
4596
|
+
handleRequest(req, res) {
|
|
4597
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
4598
|
+
const method = req.method ?? "GET";
|
|
4599
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
4600
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
4601
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
4602
|
+
if (method === "OPTIONS") {
|
|
4603
|
+
res.writeHead(204);
|
|
4604
|
+
res.end();
|
|
4605
|
+
return;
|
|
4606
|
+
}
|
|
4607
|
+
if (!this.checkAuth(req, url, res)) return;
|
|
4608
|
+
try {
|
|
4609
|
+
if (method === "GET" && url.pathname === "/") {
|
|
4610
|
+
this.serveDashboard(res);
|
|
4611
|
+
} else if (method === "GET" && url.pathname === "/events") {
|
|
4612
|
+
this.handleSSE(req, res);
|
|
4613
|
+
} else if (method === "GET" && url.pathname === "/api/status") {
|
|
4614
|
+
this.handleStatus(res);
|
|
4615
|
+
} else if (method === "GET" && url.pathname === "/api/pending") {
|
|
4616
|
+
this.handlePendingList(res);
|
|
4617
|
+
} else if (method === "GET" && url.pathname === "/api/audit-log") {
|
|
4618
|
+
this.handleAuditLog(url, res);
|
|
4619
|
+
} else if (method === "POST" && url.pathname.startsWith("/api/approve/")) {
|
|
4620
|
+
const id = url.pathname.slice("/api/approve/".length);
|
|
4621
|
+
this.handleDecision(id, "approve", res);
|
|
4622
|
+
} else if (method === "POST" && url.pathname.startsWith("/api/deny/")) {
|
|
4623
|
+
const id = url.pathname.slice("/api/deny/".length);
|
|
4624
|
+
this.handleDecision(id, "deny", res);
|
|
4625
|
+
} else {
|
|
4626
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
4627
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
4628
|
+
}
|
|
4629
|
+
} catch (err) {
|
|
4630
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
4631
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
// ── Route Handlers ──────────────────────────────────────────────────
|
|
4635
|
+
serveDashboard(res) {
|
|
4636
|
+
res.writeHead(200, {
|
|
4637
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
4638
|
+
"Cache-Control": "no-cache"
|
|
4639
|
+
});
|
|
4640
|
+
res.end(this.dashboardHTML);
|
|
4641
|
+
}
|
|
4642
|
+
handleSSE(req, res) {
|
|
4643
|
+
res.writeHead(200, {
|
|
4644
|
+
"Content-Type": "text/event-stream",
|
|
4645
|
+
"Cache-Control": "no-cache",
|
|
4646
|
+
"Connection": "keep-alive"
|
|
4647
|
+
});
|
|
4648
|
+
const initData = {};
|
|
4649
|
+
if (this.baseline) {
|
|
4650
|
+
initData.baseline = this.baseline.getProfile();
|
|
4651
|
+
}
|
|
4652
|
+
if (this.policy) {
|
|
4653
|
+
initData.policy = {
|
|
4654
|
+
tier1_always_approve: this.policy.tier1_always_approve,
|
|
4655
|
+
tier2_anomaly: this.policy.tier2_anomaly,
|
|
4656
|
+
tier3_always_allow: this.policy.tier3_always_allow,
|
|
4657
|
+
approval_channel: {
|
|
4658
|
+
type: this.policy.approval_channel.type,
|
|
4659
|
+
timeout_seconds: this.policy.approval_channel.timeout_seconds,
|
|
4660
|
+
auto_deny: this.policy.approval_channel.auto_deny
|
|
4661
|
+
}
|
|
4662
|
+
};
|
|
4663
|
+
}
|
|
4664
|
+
const pendingList = Array.from(this.pending.values()).map((p) => ({
|
|
4665
|
+
request_id: p.id,
|
|
4666
|
+
operation: p.request.operation,
|
|
4667
|
+
tier: p.request.tier,
|
|
4668
|
+
reason: p.request.reason,
|
|
4669
|
+
context: p.request.context,
|
|
4670
|
+
timestamp: p.request.timestamp
|
|
4671
|
+
}));
|
|
4672
|
+
if (pendingList.length > 0) {
|
|
4673
|
+
initData.pending = pendingList;
|
|
4674
|
+
}
|
|
4675
|
+
res.write(`event: init
|
|
4676
|
+
data: ${JSON.stringify(initData)}
|
|
4677
|
+
|
|
4678
|
+
`);
|
|
4679
|
+
this.sseClients.add(res);
|
|
4680
|
+
req.on("close", () => {
|
|
4681
|
+
this.sseClients.delete(res);
|
|
4682
|
+
});
|
|
4683
|
+
}
|
|
4684
|
+
handleStatus(res) {
|
|
4685
|
+
const status = {
|
|
4686
|
+
pending_count: this.pending.size,
|
|
4687
|
+
connected_clients: this.sseClients.size
|
|
4688
|
+
};
|
|
4689
|
+
if (this.baseline) {
|
|
4690
|
+
status.baseline = this.baseline.getProfile();
|
|
4691
|
+
}
|
|
4692
|
+
if (this.policy) {
|
|
4693
|
+
status.policy = {
|
|
4694
|
+
version: this.policy.version,
|
|
4695
|
+
tier1_always_approve: this.policy.tier1_always_approve,
|
|
4696
|
+
tier2_anomaly: this.policy.tier2_anomaly,
|
|
4697
|
+
tier3_always_allow: this.policy.tier3_always_allow,
|
|
4698
|
+
approval_channel: {
|
|
4699
|
+
type: this.policy.approval_channel.type,
|
|
4700
|
+
timeout_seconds: this.policy.approval_channel.timeout_seconds,
|
|
4701
|
+
auto_deny: this.policy.approval_channel.auto_deny
|
|
4702
|
+
}
|
|
4703
|
+
};
|
|
4704
|
+
}
|
|
4705
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4706
|
+
res.end(JSON.stringify(status));
|
|
4707
|
+
}
|
|
4708
|
+
handlePendingList(res) {
|
|
4709
|
+
const list = Array.from(this.pending.values()).map((p) => ({
|
|
4710
|
+
id: p.id,
|
|
4711
|
+
operation: p.request.operation,
|
|
4712
|
+
tier: p.request.tier,
|
|
4713
|
+
reason: p.request.reason,
|
|
4714
|
+
context: p.request.context,
|
|
4715
|
+
timestamp: p.request.timestamp,
|
|
4716
|
+
created_at: p.created_at
|
|
4717
|
+
}));
|
|
4718
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4719
|
+
res.end(JSON.stringify(list));
|
|
4720
|
+
}
|
|
4721
|
+
handleAuditLog(url, res) {
|
|
4722
|
+
const limit = parseInt(url.searchParams.get("limit") ?? "50", 10);
|
|
4723
|
+
if (this.auditLog) {
|
|
4724
|
+
this.auditLog.query({ limit }).then((entries) => {
|
|
4725
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4726
|
+
res.end(JSON.stringify(entries));
|
|
4727
|
+
}).catch(() => {
|
|
4728
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4729
|
+
res.end(JSON.stringify([]));
|
|
4730
|
+
});
|
|
4731
|
+
} else {
|
|
4732
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4733
|
+
res.end(JSON.stringify([]));
|
|
4734
|
+
}
|
|
4735
|
+
}
|
|
4736
|
+
handleDecision(id, decision, res) {
|
|
4737
|
+
const pending = this.pending.get(id);
|
|
4738
|
+
if (!pending) {
|
|
4739
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
4740
|
+
res.end(JSON.stringify({ error: "Request not found or already resolved" }));
|
|
4741
|
+
return;
|
|
4742
|
+
}
|
|
4743
|
+
clearTimeout(pending.timer);
|
|
4744
|
+
this.pending.delete(id);
|
|
4745
|
+
const response = {
|
|
4746
|
+
decision,
|
|
4747
|
+
decided_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4748
|
+
decided_by: "human"
|
|
4749
|
+
};
|
|
4750
|
+
this.broadcastSSE("request-resolved", {
|
|
4751
|
+
request_id: id,
|
|
4752
|
+
decision,
|
|
4753
|
+
decided_by: "human"
|
|
4754
|
+
});
|
|
4755
|
+
pending.resolve(response);
|
|
4756
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4757
|
+
res.end(JSON.stringify({ success: true, decision }));
|
|
4758
|
+
}
|
|
4759
|
+
// ── SSE Broadcasting ────────────────────────────────────────────────
|
|
4760
|
+
broadcastSSE(event, data) {
|
|
4761
|
+
const message = `event: ${event}
|
|
4762
|
+
data: ${JSON.stringify(data)}
|
|
4763
|
+
|
|
4764
|
+
`;
|
|
4765
|
+
for (const client of this.sseClients) {
|
|
4766
|
+
try {
|
|
4767
|
+
client.write(message);
|
|
4768
|
+
} catch {
|
|
4769
|
+
this.sseClients.delete(client);
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
}
|
|
4773
|
+
/**
|
|
4774
|
+
* Broadcast an audit entry to connected dashboards.
|
|
4775
|
+
* Called externally when audit events happen.
|
|
4776
|
+
*/
|
|
4777
|
+
broadcastAuditEntry(entry) {
|
|
4778
|
+
this.broadcastSSE("audit-entry", entry);
|
|
4779
|
+
}
|
|
4780
|
+
/**
|
|
4781
|
+
* Broadcast a baseline update to connected dashboards.
|
|
4782
|
+
* Called externally after baseline changes.
|
|
4783
|
+
*/
|
|
4784
|
+
broadcastBaselineUpdate() {
|
|
4785
|
+
if (this.baseline) {
|
|
4786
|
+
this.broadcastSSE("baseline-update", this.baseline.getProfile());
|
|
4787
|
+
}
|
|
4788
|
+
}
|
|
4789
|
+
/** Get the number of pending requests */
|
|
4790
|
+
get pendingCount() {
|
|
4791
|
+
return this.pending.size;
|
|
4792
|
+
}
|
|
4793
|
+
/** Get the number of connected SSE clients */
|
|
4794
|
+
get clientCount() {
|
|
4795
|
+
return this.sseClients.size;
|
|
4796
|
+
}
|
|
4797
|
+
};
|
|
4798
|
+
function signPayload(body, secret) {
|
|
4799
|
+
return createHmac("sha256", secret).update(body).digest("hex");
|
|
4800
|
+
}
|
|
4801
|
+
function verifySignature(body, signature, secret) {
|
|
4802
|
+
const expected = signPayload(body, secret);
|
|
4803
|
+
if (expected.length !== signature.length) return false;
|
|
4804
|
+
let mismatch = 0;
|
|
4805
|
+
for (let i = 0; i < expected.length; i++) {
|
|
4806
|
+
mismatch |= expected.charCodeAt(i) ^ signature.charCodeAt(i);
|
|
4807
|
+
}
|
|
4808
|
+
return mismatch === 0;
|
|
4809
|
+
}
|
|
4810
|
+
var WebhookApprovalChannel = class {
|
|
4811
|
+
config;
|
|
4812
|
+
pending = /* @__PURE__ */ new Map();
|
|
4813
|
+
callbackServer = null;
|
|
4814
|
+
constructor(config) {
|
|
4815
|
+
this.config = config;
|
|
4816
|
+
}
|
|
4817
|
+
/**
|
|
4818
|
+
* Start the callback listener server.
|
|
4819
|
+
*/
|
|
4820
|
+
async start() {
|
|
4821
|
+
return new Promise((resolve, reject) => {
|
|
4822
|
+
this.callbackServer = createServer$2(
|
|
4823
|
+
(req, res) => this.handleCallback(req, res)
|
|
4824
|
+
);
|
|
4825
|
+
this.callbackServer.listen(
|
|
4826
|
+
this.config.callback_port,
|
|
4827
|
+
this.config.callback_host,
|
|
4828
|
+
() => {
|
|
4829
|
+
process.stderr.write(
|
|
4830
|
+
`
|
|
4831
|
+
Sanctuary Webhook Callback: http://${this.config.callback_host}:${this.config.callback_port}
|
|
4832
|
+
Webhook target: ${this.config.webhook_url}
|
|
4833
|
+
|
|
4834
|
+
`
|
|
4835
|
+
);
|
|
4836
|
+
resolve();
|
|
4837
|
+
}
|
|
4838
|
+
);
|
|
4839
|
+
this.callbackServer.on("error", reject);
|
|
4840
|
+
});
|
|
4841
|
+
}
|
|
4842
|
+
/**
|
|
4843
|
+
* Stop the callback server and clean up pending requests.
|
|
4844
|
+
*/
|
|
4845
|
+
async stop() {
|
|
4846
|
+
for (const [, pending] of this.pending) {
|
|
4847
|
+
clearTimeout(pending.timer);
|
|
4848
|
+
pending.resolve({
|
|
4849
|
+
decision: "deny",
|
|
4850
|
+
decided_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4851
|
+
decided_by: "auto"
|
|
4852
|
+
});
|
|
4853
|
+
}
|
|
4854
|
+
this.pending.clear();
|
|
4855
|
+
if (this.callbackServer) {
|
|
4856
|
+
return new Promise((resolve) => {
|
|
4857
|
+
this.callbackServer.close(() => resolve());
|
|
4858
|
+
});
|
|
4859
|
+
}
|
|
4860
|
+
}
|
|
4861
|
+
/**
|
|
4862
|
+
* Request approval by POSTing to the webhook and waiting for a callback.
|
|
4863
|
+
*/
|
|
4864
|
+
async requestApproval(request) {
|
|
4865
|
+
const id = randomBytes$1(8).toString("hex");
|
|
4866
|
+
process.stderr.write(
|
|
4867
|
+
`[Sanctuary] Webhook approval sent: ${request.operation} (Tier ${request.tier}) \u2014 awaiting callback
|
|
4868
|
+
`
|
|
4869
|
+
);
|
|
4870
|
+
return new Promise((resolve) => {
|
|
4871
|
+
const timer = setTimeout(() => {
|
|
4872
|
+
this.pending.delete(id);
|
|
4873
|
+
const response = {
|
|
4874
|
+
decision: this.config.auto_deny ? "deny" : "approve",
|
|
4875
|
+
decided_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4876
|
+
decided_by: "timeout"
|
|
4877
|
+
};
|
|
4878
|
+
resolve(response);
|
|
4879
|
+
}, this.config.timeout_seconds * 1e3);
|
|
4880
|
+
const pending = {
|
|
4881
|
+
id,
|
|
4882
|
+
request,
|
|
4883
|
+
resolve,
|
|
4884
|
+
timer,
|
|
4885
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4886
|
+
};
|
|
4887
|
+
this.pending.set(id, pending);
|
|
4888
|
+
const callbackUrl = `http://${this.config.callback_host}:${this.config.callback_port}/webhook/respond/${id}`;
|
|
4889
|
+
const payload = {
|
|
4890
|
+
request_id: id,
|
|
4891
|
+
operation: request.operation,
|
|
4892
|
+
tier: request.tier,
|
|
4893
|
+
reason: request.reason,
|
|
4894
|
+
context: request.context,
|
|
4895
|
+
timestamp: request.timestamp,
|
|
4896
|
+
callback_url: callbackUrl,
|
|
4897
|
+
timeout_seconds: this.config.timeout_seconds
|
|
4898
|
+
};
|
|
4899
|
+
this.sendWebhook(payload).catch((err) => {
|
|
4900
|
+
process.stderr.write(
|
|
4901
|
+
`[Sanctuary] Webhook delivery failed: ${err instanceof Error ? err.message : String(err)}
|
|
4902
|
+
`
|
|
4903
|
+
);
|
|
4904
|
+
});
|
|
4905
|
+
});
|
|
4906
|
+
}
|
|
4907
|
+
// ── Outbound Webhook ──────────────────────────────────────────────────
|
|
4908
|
+
async sendWebhook(payload) {
|
|
4909
|
+
const body = JSON.stringify(payload);
|
|
4910
|
+
const signature = signPayload(body, this.config.webhook_secret);
|
|
4911
|
+
const response = await fetch(this.config.webhook_url, {
|
|
4912
|
+
method: "POST",
|
|
4913
|
+
headers: {
|
|
4914
|
+
"Content-Type": "application/json",
|
|
4915
|
+
"X-Sanctuary-Signature": signature,
|
|
4916
|
+
"X-Sanctuary-Request-Id": payload.request_id
|
|
4917
|
+
},
|
|
4918
|
+
body
|
|
4919
|
+
});
|
|
4920
|
+
if (!response.ok) {
|
|
4921
|
+
throw new Error(
|
|
4922
|
+
`Webhook returned ${response.status}: ${await response.text().catch(() => "")}`
|
|
4923
|
+
);
|
|
4924
|
+
}
|
|
4925
|
+
}
|
|
4926
|
+
// ── Inbound Callback Handler ──────────────────────────────────────────
|
|
4927
|
+
handleCallback(req, res) {
|
|
4928
|
+
const url = new URL(
|
|
4929
|
+
req.url ?? "/",
|
|
4930
|
+
`http://${req.headers.host ?? "localhost"}`
|
|
4931
|
+
);
|
|
4932
|
+
const method = req.method ?? "GET";
|
|
4933
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
4934
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
4935
|
+
res.setHeader(
|
|
4936
|
+
"Access-Control-Allow-Headers",
|
|
4937
|
+
"Content-Type, X-Sanctuary-Signature"
|
|
4938
|
+
);
|
|
4939
|
+
if (method === "OPTIONS") {
|
|
4940
|
+
res.writeHead(204);
|
|
4941
|
+
res.end();
|
|
4942
|
+
return;
|
|
4943
|
+
}
|
|
4944
|
+
if (method === "GET" && url.pathname === "/health") {
|
|
4945
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4946
|
+
res.end(
|
|
4947
|
+
JSON.stringify({
|
|
4948
|
+
status: "ok",
|
|
4949
|
+
pending_count: this.pending.size
|
|
4950
|
+
})
|
|
4951
|
+
);
|
|
4952
|
+
return;
|
|
4953
|
+
}
|
|
4954
|
+
const match = url.pathname.match(/^\/webhook\/respond\/([a-f0-9]+)$/);
|
|
4955
|
+
if (method !== "POST" || !match) {
|
|
4956
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
4957
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
4958
|
+
return;
|
|
4959
|
+
}
|
|
4960
|
+
const requestId = match[1];
|
|
4961
|
+
let bodyChunks = [];
|
|
4962
|
+
req.on("data", (chunk) => bodyChunks.push(chunk));
|
|
4963
|
+
req.on("end", () => {
|
|
4964
|
+
const body = Buffer.concat(bodyChunks).toString("utf-8");
|
|
4965
|
+
const signature = req.headers["x-sanctuary-signature"];
|
|
4966
|
+
if (typeof signature !== "string" || !verifySignature(body, signature, this.config.webhook_secret)) {
|
|
4967
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
4968
|
+
res.end(
|
|
4969
|
+
JSON.stringify({ error: "Invalid signature" })
|
|
4970
|
+
);
|
|
4971
|
+
return;
|
|
4972
|
+
}
|
|
4973
|
+
let callbackPayload;
|
|
4974
|
+
try {
|
|
4975
|
+
callbackPayload = JSON.parse(body);
|
|
4976
|
+
} catch {
|
|
4977
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4978
|
+
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
4979
|
+
return;
|
|
4980
|
+
}
|
|
4981
|
+
if (callbackPayload.decision !== "approve" && callbackPayload.decision !== "deny") {
|
|
4982
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4983
|
+
res.end(
|
|
4984
|
+
JSON.stringify({
|
|
4985
|
+
error: 'Decision must be "approve" or "deny"'
|
|
4986
|
+
})
|
|
4987
|
+
);
|
|
4988
|
+
return;
|
|
4989
|
+
}
|
|
4990
|
+
if (callbackPayload.request_id !== requestId) {
|
|
4991
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4992
|
+
res.end(
|
|
4993
|
+
JSON.stringify({ error: "Request ID mismatch" })
|
|
4994
|
+
);
|
|
4995
|
+
return;
|
|
4996
|
+
}
|
|
4997
|
+
const pending = this.pending.get(requestId);
|
|
4998
|
+
if (!pending) {
|
|
4999
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
5000
|
+
res.end(
|
|
5001
|
+
JSON.stringify({
|
|
5002
|
+
error: "Request not found or already resolved"
|
|
5003
|
+
})
|
|
5004
|
+
);
|
|
5005
|
+
return;
|
|
5006
|
+
}
|
|
5007
|
+
clearTimeout(pending.timer);
|
|
5008
|
+
this.pending.delete(requestId);
|
|
5009
|
+
const response = {
|
|
5010
|
+
decision: callbackPayload.decision,
|
|
5011
|
+
decided_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5012
|
+
decided_by: "human"
|
|
5013
|
+
};
|
|
5014
|
+
pending.resolve(response);
|
|
5015
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
5016
|
+
res.end(
|
|
5017
|
+
JSON.stringify({
|
|
5018
|
+
success: true,
|
|
5019
|
+
decision: callbackPayload.decision
|
|
5020
|
+
})
|
|
5021
|
+
);
|
|
5022
|
+
});
|
|
5023
|
+
}
|
|
5024
|
+
/** Get the number of pending requests */
|
|
5025
|
+
get pendingCount() {
|
|
5026
|
+
return this.pending.size;
|
|
5027
|
+
}
|
|
5028
|
+
};
|
|
5029
|
+
|
|
5030
|
+
// src/principal-policy/gate.ts
|
|
5031
|
+
var ApprovalGate = class {
|
|
5032
|
+
policy;
|
|
5033
|
+
baseline;
|
|
5034
|
+
channel;
|
|
5035
|
+
auditLog;
|
|
5036
|
+
constructor(policy, baseline, channel, auditLog) {
|
|
5037
|
+
this.policy = policy;
|
|
5038
|
+
this.baseline = baseline;
|
|
5039
|
+
this.channel = channel;
|
|
5040
|
+
this.auditLog = auditLog;
|
|
5041
|
+
}
|
|
5042
|
+
/**
|
|
5043
|
+
* Evaluate a tool call against the Principal Policy.
|
|
3257
5044
|
*
|
|
3258
5045
|
* @param toolName - Full MCP tool name (e.g., "sanctuary/state_export")
|
|
3259
5046
|
* @param args - Tool call arguments (for context extraction)
|
|
@@ -3912,6 +5699,7 @@ function deriveTrustTier(level) {
|
|
|
3912
5699
|
// src/handshake/tools.ts
|
|
3913
5700
|
function createHandshakeTools(config, identityManager, masterKey, auditLog) {
|
|
3914
5701
|
const sessions = /* @__PURE__ */ new Map();
|
|
5702
|
+
const handshakeResults = /* @__PURE__ */ new Map();
|
|
3915
5703
|
const shrOpts = {
|
|
3916
5704
|
config,
|
|
3917
5705
|
identityManager,
|
|
@@ -4032,6 +5820,7 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
|
|
|
4032
5820
|
session.their_shr = response.shr;
|
|
4033
5821
|
session.their_nonce = response.responder_nonce;
|
|
4034
5822
|
session.result = result.result;
|
|
5823
|
+
handshakeResults.set(result.result.counterparty_id, result.result);
|
|
4035
5824
|
auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id);
|
|
4036
5825
|
return toolResult({
|
|
4037
5826
|
completion: result.completion,
|
|
@@ -4068,6 +5857,9 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
|
|
|
4068
5857
|
const result = verifyCompletion(completion, session);
|
|
4069
5858
|
session.state = result.verified ? "completed" : "failed";
|
|
4070
5859
|
session.result = result;
|
|
5860
|
+
if (result.verified) {
|
|
5861
|
+
handshakeResults.set(result.counterparty_id, result);
|
|
5862
|
+
}
|
|
4071
5863
|
auditLog.append(
|
|
4072
5864
|
"l4",
|
|
4073
5865
|
"handshake_verify_completion",
|
|
@@ -4087,7 +5879,759 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
|
|
|
4087
5879
|
}
|
|
4088
5880
|
}
|
|
4089
5881
|
];
|
|
4090
|
-
return { tools };
|
|
5882
|
+
return { tools, handshakeResults };
|
|
5883
|
+
}
|
|
5884
|
+
|
|
5885
|
+
// src/federation/registry.ts
|
|
5886
|
+
var DEFAULT_CAPABILITIES = {
|
|
5887
|
+
reputation_exchange: true,
|
|
5888
|
+
mutual_attestation: true,
|
|
5889
|
+
encrypted_channel: false,
|
|
5890
|
+
attestation_formats: ["sanctuary-interaction-v1"]
|
|
5891
|
+
};
|
|
5892
|
+
var FederationRegistry = class {
|
|
5893
|
+
peers = /* @__PURE__ */ new Map();
|
|
5894
|
+
/**
|
|
5895
|
+
* Register or update a peer from a completed handshake.
|
|
5896
|
+
* This is the ONLY way peers enter the registry.
|
|
5897
|
+
*/
|
|
5898
|
+
registerFromHandshake(result, peerDid, capabilities) {
|
|
5899
|
+
const existing = this.peers.get(result.counterparty_id);
|
|
5900
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5901
|
+
const peer = {
|
|
5902
|
+
peer_id: result.counterparty_id,
|
|
5903
|
+
peer_did: peerDid,
|
|
5904
|
+
first_seen: existing?.first_seen ?? now,
|
|
5905
|
+
last_handshake: result.completed_at,
|
|
5906
|
+
trust_tier: trustTierToSovereigntyTier(result.trust_tier),
|
|
5907
|
+
handshake_result: result,
|
|
5908
|
+
capabilities: {
|
|
5909
|
+
...DEFAULT_CAPABILITIES,
|
|
5910
|
+
...existing?.capabilities ?? {},
|
|
5911
|
+
...capabilities ?? {}
|
|
5912
|
+
},
|
|
5913
|
+
active: result.verified && new Date(result.expires_at) > /* @__PURE__ */ new Date()
|
|
5914
|
+
};
|
|
5915
|
+
if (!peer.active) {
|
|
5916
|
+
peer.trust_tier = "self-attested";
|
|
5917
|
+
}
|
|
5918
|
+
this.peers.set(result.counterparty_id, peer);
|
|
5919
|
+
return peer;
|
|
5920
|
+
}
|
|
5921
|
+
/**
|
|
5922
|
+
* Get a peer by instance ID.
|
|
5923
|
+
* Automatically updates active status based on handshake expiry.
|
|
5924
|
+
*/
|
|
5925
|
+
getPeer(peerId) {
|
|
5926
|
+
const peer = this.peers.get(peerId);
|
|
5927
|
+
if (!peer) return null;
|
|
5928
|
+
if (peer.active && new Date(peer.handshake_result.expires_at) <= /* @__PURE__ */ new Date()) {
|
|
5929
|
+
peer.active = false;
|
|
5930
|
+
peer.trust_tier = "self-attested";
|
|
5931
|
+
}
|
|
5932
|
+
return peer;
|
|
5933
|
+
}
|
|
5934
|
+
/**
|
|
5935
|
+
* List all known peers, optionally filtered by status.
|
|
5936
|
+
*/
|
|
5937
|
+
listPeers(filter) {
|
|
5938
|
+
const peers = Array.from(this.peers.values());
|
|
5939
|
+
for (const peer of peers) {
|
|
5940
|
+
if (peer.active && new Date(peer.handshake_result.expires_at) <= /* @__PURE__ */ new Date()) {
|
|
5941
|
+
peer.active = false;
|
|
5942
|
+
peer.trust_tier = "self-attested";
|
|
5943
|
+
}
|
|
5944
|
+
}
|
|
5945
|
+
if (filter?.active_only) {
|
|
5946
|
+
return peers.filter((p) => p.active);
|
|
5947
|
+
}
|
|
5948
|
+
return peers;
|
|
5949
|
+
}
|
|
5950
|
+
/**
|
|
5951
|
+
* Evaluate trust for a federation peer.
|
|
5952
|
+
*
|
|
5953
|
+
* Trust assessment considers:
|
|
5954
|
+
* - Handshake status (current vs expired)
|
|
5955
|
+
* - Sovereignty tier (verified-sovereign vs degraded vs unverified)
|
|
5956
|
+
* - Reputation data (if available)
|
|
5957
|
+
* - Mutual attestation history
|
|
5958
|
+
*/
|
|
5959
|
+
evaluateTrust(peerId, mutualAttestationCount = 0, reputationScore) {
|
|
5960
|
+
const peer = this.getPeer(peerId);
|
|
5961
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5962
|
+
if (!peer) {
|
|
5963
|
+
return {
|
|
5964
|
+
peer_id: peerId,
|
|
5965
|
+
sovereignty_tier: "unverified",
|
|
5966
|
+
handshake_current: false,
|
|
5967
|
+
mutual_attestation_count: 0,
|
|
5968
|
+
trust_level: "none",
|
|
5969
|
+
factors: ["Peer not found in federation registry"],
|
|
5970
|
+
evaluated_at: now
|
|
5971
|
+
};
|
|
5972
|
+
}
|
|
5973
|
+
const factors = [];
|
|
5974
|
+
let score = 0;
|
|
5975
|
+
if (peer.active) {
|
|
5976
|
+
factors.push("Active handshake (trust current)");
|
|
5977
|
+
score += 3;
|
|
5978
|
+
} else {
|
|
5979
|
+
factors.push("Handshake expired (trust degraded)");
|
|
5980
|
+
score += 1;
|
|
5981
|
+
}
|
|
5982
|
+
switch (peer.trust_tier) {
|
|
5983
|
+
case "verified-sovereign":
|
|
5984
|
+
factors.push("Verified sovereign \u2014 full sovereignty posture");
|
|
5985
|
+
score += 4;
|
|
5986
|
+
break;
|
|
5987
|
+
case "verified-degraded":
|
|
5988
|
+
factors.push("Verified degraded \u2014 sovereignty with known limitations");
|
|
5989
|
+
score += 3;
|
|
5990
|
+
break;
|
|
5991
|
+
case "self-attested":
|
|
5992
|
+
factors.push("Self-attested \u2014 claims not independently verified");
|
|
5993
|
+
score += 1;
|
|
5994
|
+
break;
|
|
5995
|
+
case "unverified":
|
|
5996
|
+
factors.push("Unverified \u2014 no sovereignty proof");
|
|
5997
|
+
score += 0;
|
|
5998
|
+
break;
|
|
5999
|
+
}
|
|
6000
|
+
if (mutualAttestationCount > 10) {
|
|
6001
|
+
factors.push(`Strong attestation history (${mutualAttestationCount} mutual attestations)`);
|
|
6002
|
+
score += 3;
|
|
6003
|
+
} else if (mutualAttestationCount > 0) {
|
|
6004
|
+
factors.push(`Some attestation history (${mutualAttestationCount} mutual attestations)`);
|
|
6005
|
+
score += 1;
|
|
6006
|
+
} else {
|
|
6007
|
+
factors.push("No mutual attestation history");
|
|
6008
|
+
}
|
|
6009
|
+
if (reputationScore !== void 0) {
|
|
6010
|
+
if (reputationScore >= 80) {
|
|
6011
|
+
factors.push(`High reputation score (${reputationScore})`);
|
|
6012
|
+
score += 2;
|
|
6013
|
+
} else if (reputationScore >= 50) {
|
|
6014
|
+
factors.push(`Moderate reputation score (${reputationScore})`);
|
|
6015
|
+
score += 1;
|
|
6016
|
+
} else {
|
|
6017
|
+
factors.push(`Low reputation score (${reputationScore})`);
|
|
6018
|
+
}
|
|
6019
|
+
}
|
|
6020
|
+
let trust_level;
|
|
6021
|
+
if (score >= 9) trust_level = "high";
|
|
6022
|
+
else if (score >= 5) trust_level = "medium";
|
|
6023
|
+
else if (score >= 2) trust_level = "low";
|
|
6024
|
+
else trust_level = "none";
|
|
6025
|
+
return {
|
|
6026
|
+
peer_id: peerId,
|
|
6027
|
+
sovereignty_tier: peer.trust_tier,
|
|
6028
|
+
handshake_current: peer.active,
|
|
6029
|
+
reputation_score: reputationScore,
|
|
6030
|
+
mutual_attestation_count: mutualAttestationCount,
|
|
6031
|
+
trust_level,
|
|
6032
|
+
factors,
|
|
6033
|
+
evaluated_at: now
|
|
6034
|
+
};
|
|
6035
|
+
}
|
|
6036
|
+
/**
|
|
6037
|
+
* Remove a peer from the registry.
|
|
6038
|
+
*/
|
|
6039
|
+
removePeer(peerId) {
|
|
6040
|
+
return this.peers.delete(peerId);
|
|
6041
|
+
}
|
|
6042
|
+
/**
|
|
6043
|
+
* Get the handshake results map (for tier resolution integration).
|
|
6044
|
+
*/
|
|
6045
|
+
getHandshakeResults() {
|
|
6046
|
+
const results = /* @__PURE__ */ new Map();
|
|
6047
|
+
for (const [id, peer] of this.peers) {
|
|
6048
|
+
if (peer.active) {
|
|
6049
|
+
results.set(id, peer.handshake_result);
|
|
6050
|
+
}
|
|
6051
|
+
}
|
|
6052
|
+
return results;
|
|
6053
|
+
}
|
|
6054
|
+
};
|
|
6055
|
+
|
|
6056
|
+
// src/federation/tools.ts
|
|
6057
|
+
function createFederationTools(auditLog, handshakeResults) {
|
|
6058
|
+
const registry = new FederationRegistry();
|
|
6059
|
+
const tools = [
|
|
6060
|
+
// ─── Peer Management ──────────────────────────────────────────────
|
|
6061
|
+
{
|
|
6062
|
+
name: "sanctuary/federation_peers",
|
|
6063
|
+
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.",
|
|
6064
|
+
inputSchema: {
|
|
6065
|
+
type: "object",
|
|
6066
|
+
properties: {
|
|
6067
|
+
action: {
|
|
6068
|
+
type: "string",
|
|
6069
|
+
enum: ["list", "register", "remove"],
|
|
6070
|
+
description: "Operation to perform on the peer registry"
|
|
6071
|
+
},
|
|
6072
|
+
peer_id: {
|
|
6073
|
+
type: "string",
|
|
6074
|
+
description: "Peer instance ID (required for register/remove)"
|
|
6075
|
+
},
|
|
6076
|
+
peer_did: {
|
|
6077
|
+
type: "string",
|
|
6078
|
+
description: "Peer DID (required for register)"
|
|
6079
|
+
},
|
|
6080
|
+
active_only: {
|
|
6081
|
+
type: "boolean",
|
|
6082
|
+
description: "When listing, only show peers with active handshakes"
|
|
6083
|
+
}
|
|
6084
|
+
},
|
|
6085
|
+
required: ["action"]
|
|
6086
|
+
},
|
|
6087
|
+
handler: async (args) => {
|
|
6088
|
+
const action = args.action;
|
|
6089
|
+
switch (action) {
|
|
6090
|
+
case "list": {
|
|
6091
|
+
const peers = registry.listPeers({
|
|
6092
|
+
active_only: args.active_only
|
|
6093
|
+
});
|
|
6094
|
+
auditLog.append("l4", "federation_peers_list", "system", {
|
|
6095
|
+
peer_count: peers.length
|
|
6096
|
+
});
|
|
6097
|
+
return toolResult({
|
|
6098
|
+
peers: peers.map((p) => ({
|
|
6099
|
+
peer_id: p.peer_id,
|
|
6100
|
+
peer_did: p.peer_did,
|
|
6101
|
+
trust_tier: p.trust_tier,
|
|
6102
|
+
active: p.active,
|
|
6103
|
+
first_seen: p.first_seen,
|
|
6104
|
+
last_handshake: p.last_handshake,
|
|
6105
|
+
capabilities: p.capabilities
|
|
6106
|
+
})),
|
|
6107
|
+
total: peers.length
|
|
6108
|
+
});
|
|
6109
|
+
}
|
|
6110
|
+
case "register": {
|
|
6111
|
+
const peerId = args.peer_id;
|
|
6112
|
+
const peerDid = args.peer_did;
|
|
6113
|
+
if (!peerId || !peerDid) {
|
|
6114
|
+
return toolResult({
|
|
6115
|
+
error: "Both peer_id and peer_did are required for registration."
|
|
6116
|
+
});
|
|
6117
|
+
}
|
|
6118
|
+
const hsResult = handshakeResults.get(peerId);
|
|
6119
|
+
if (!hsResult) {
|
|
6120
|
+
return toolResult({
|
|
6121
|
+
error: `No completed handshake found for peer "${peerId}". Complete a sovereignty handshake first using handshake_initiate.`
|
|
6122
|
+
});
|
|
6123
|
+
}
|
|
6124
|
+
if (!hsResult.verified) {
|
|
6125
|
+
return toolResult({
|
|
6126
|
+
error: `Handshake with "${peerId}" was not verified. Only verified handshakes can establish federation.`
|
|
6127
|
+
});
|
|
6128
|
+
}
|
|
6129
|
+
const peer = registry.registerFromHandshake(hsResult, peerDid);
|
|
6130
|
+
auditLog.append("l4", "federation_peer_register", "system", {
|
|
6131
|
+
peer_id: peerId,
|
|
6132
|
+
peer_did: peerDid,
|
|
6133
|
+
trust_tier: peer.trust_tier
|
|
6134
|
+
});
|
|
6135
|
+
return toolResult({
|
|
6136
|
+
registered: true,
|
|
6137
|
+
peer_id: peer.peer_id,
|
|
6138
|
+
trust_tier: peer.trust_tier,
|
|
6139
|
+
active: peer.active,
|
|
6140
|
+
capabilities: peer.capabilities
|
|
6141
|
+
});
|
|
6142
|
+
}
|
|
6143
|
+
case "remove": {
|
|
6144
|
+
const peerId = args.peer_id;
|
|
6145
|
+
if (!peerId) {
|
|
6146
|
+
return toolResult({ error: "peer_id is required for removal." });
|
|
6147
|
+
}
|
|
6148
|
+
const removed = registry.removePeer(peerId);
|
|
6149
|
+
auditLog.append("l4", "federation_peer_remove", "system", {
|
|
6150
|
+
peer_id: peerId,
|
|
6151
|
+
removed
|
|
6152
|
+
});
|
|
6153
|
+
return toolResult({
|
|
6154
|
+
removed,
|
|
6155
|
+
peer_id: peerId
|
|
6156
|
+
});
|
|
6157
|
+
}
|
|
6158
|
+
default:
|
|
6159
|
+
return toolResult({ error: `Unknown action: ${action}` });
|
|
6160
|
+
}
|
|
6161
|
+
}
|
|
6162
|
+
},
|
|
6163
|
+
// ─── Trust Evaluation ─────────────────────────────────────────────
|
|
6164
|
+
{
|
|
6165
|
+
name: "sanctuary/federation_trust_evaluate",
|
|
6166
|
+
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.",
|
|
6167
|
+
inputSchema: {
|
|
6168
|
+
type: "object",
|
|
6169
|
+
properties: {
|
|
6170
|
+
peer_id: {
|
|
6171
|
+
type: "string",
|
|
6172
|
+
description: "Peer instance ID to evaluate"
|
|
6173
|
+
},
|
|
6174
|
+
mutual_attestation_count: {
|
|
6175
|
+
type: "number",
|
|
6176
|
+
description: "Number of mutual attestations with this peer (0 if unknown)"
|
|
6177
|
+
},
|
|
6178
|
+
reputation_score: {
|
|
6179
|
+
type: "number",
|
|
6180
|
+
description: "Peer's weighted reputation score (from reputation_query_weighted)"
|
|
6181
|
+
}
|
|
6182
|
+
},
|
|
6183
|
+
required: ["peer_id"]
|
|
6184
|
+
},
|
|
6185
|
+
handler: async (args) => {
|
|
6186
|
+
const peerId = args.peer_id;
|
|
6187
|
+
const mutualCount = args.mutual_attestation_count ?? 0;
|
|
6188
|
+
const repScore = args.reputation_score;
|
|
6189
|
+
const evaluation = registry.evaluateTrust(peerId, mutualCount, repScore);
|
|
6190
|
+
auditLog.append("l4", "federation_trust_evaluate", "system", {
|
|
6191
|
+
peer_id: peerId,
|
|
6192
|
+
trust_level: evaluation.trust_level,
|
|
6193
|
+
sovereignty_tier: evaluation.sovereignty_tier
|
|
6194
|
+
});
|
|
6195
|
+
return toolResult(evaluation);
|
|
6196
|
+
}
|
|
6197
|
+
},
|
|
6198
|
+
// ─── Federation Status ────────────────────────────────────────────
|
|
6199
|
+
{
|
|
6200
|
+
name: "sanctuary/federation_status",
|
|
6201
|
+
description: "Overview of federation state: total peers, active connections, trust distribution, and readiness for cross-instance operations.",
|
|
6202
|
+
inputSchema: {
|
|
6203
|
+
type: "object",
|
|
6204
|
+
properties: {}
|
|
6205
|
+
},
|
|
6206
|
+
handler: async () => {
|
|
6207
|
+
const allPeers = registry.listPeers();
|
|
6208
|
+
const activePeers = registry.listPeers({ active_only: true });
|
|
6209
|
+
const tierCounts = {
|
|
6210
|
+
"verified-sovereign": 0,
|
|
6211
|
+
"verified-degraded": 0,
|
|
6212
|
+
"self-attested": 0,
|
|
6213
|
+
"unverified": 0
|
|
6214
|
+
};
|
|
6215
|
+
for (const peer of allPeers) {
|
|
6216
|
+
tierCounts[peer.trust_tier] = (tierCounts[peer.trust_tier] ?? 0) + 1;
|
|
6217
|
+
}
|
|
6218
|
+
const capCounts = {
|
|
6219
|
+
reputation_exchange: activePeers.filter((p) => p.capabilities.reputation_exchange).length,
|
|
6220
|
+
mutual_attestation: activePeers.filter((p) => p.capabilities.mutual_attestation).length,
|
|
6221
|
+
encrypted_channel: activePeers.filter((p) => p.capabilities.encrypted_channel).length
|
|
6222
|
+
};
|
|
6223
|
+
auditLog.append("l4", "federation_status", "system", {
|
|
6224
|
+
total_peers: allPeers.length,
|
|
6225
|
+
active_peers: activePeers.length
|
|
6226
|
+
});
|
|
6227
|
+
return toolResult({
|
|
6228
|
+
total_peers: allPeers.length,
|
|
6229
|
+
active_peers: activePeers.length,
|
|
6230
|
+
expired_peers: allPeers.length - activePeers.length,
|
|
6231
|
+
trust_distribution: tierCounts,
|
|
6232
|
+
capability_coverage: capCounts,
|
|
6233
|
+
federation_ready: activePeers.length > 0,
|
|
6234
|
+
checked_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6235
|
+
});
|
|
6236
|
+
}
|
|
6237
|
+
}
|
|
6238
|
+
];
|
|
6239
|
+
return { tools, registry };
|
|
6240
|
+
}
|
|
6241
|
+
|
|
6242
|
+
// src/bridge/tools.ts
|
|
6243
|
+
init_encoding();
|
|
6244
|
+
init_encoding();
|
|
6245
|
+
|
|
6246
|
+
// src/bridge/bridge.ts
|
|
6247
|
+
init_encoding();
|
|
6248
|
+
init_hashing();
|
|
6249
|
+
function canonicalize(outcome) {
|
|
6250
|
+
return stringToBytes(stableStringify(outcome));
|
|
6251
|
+
}
|
|
6252
|
+
function stableStringify(value) {
|
|
6253
|
+
if (value === null || value === void 0) return JSON.stringify(value);
|
|
6254
|
+
if (typeof value !== "object") return JSON.stringify(value);
|
|
6255
|
+
if (Array.isArray(value)) {
|
|
6256
|
+
return "[" + value.map((v) => stableStringify(v)).join(",") + "]";
|
|
6257
|
+
}
|
|
6258
|
+
const obj = value;
|
|
6259
|
+
const keys = Object.keys(obj).sort();
|
|
6260
|
+
const pairs = keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k]));
|
|
6261
|
+
return "{" + pairs.join(",") + "}";
|
|
6262
|
+
}
|
|
6263
|
+
function createBridgeCommitment(outcome, identity, identityEncryptionKey, includePedersen = false) {
|
|
6264
|
+
const commitmentId = `bridge-${Date.now()}-${toBase64url(randomBytes(8))}`;
|
|
6265
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6266
|
+
const canonicalBytes = canonicalize(outcome);
|
|
6267
|
+
const canonicalString = new TextDecoder().decode(canonicalBytes);
|
|
6268
|
+
const sha2564 = createCommitment(canonicalString);
|
|
6269
|
+
let pedersenData;
|
|
6270
|
+
if (includePedersen && Number.isInteger(outcome.rounds) && outcome.rounds >= 0) {
|
|
6271
|
+
const pedersen = createPedersenCommitment(outcome.rounds);
|
|
6272
|
+
pedersenData = {
|
|
6273
|
+
commitment: pedersen.commitment,
|
|
6274
|
+
blinding_factor: pedersen.blinding_factor
|
|
6275
|
+
};
|
|
6276
|
+
}
|
|
6277
|
+
const commitmentPayload = {
|
|
6278
|
+
bridge_commitment_id: commitmentId,
|
|
6279
|
+
session_id: outcome.session_id,
|
|
6280
|
+
sha256_commitment: sha2564.commitment,
|
|
6281
|
+
committer_did: identity.did,
|
|
6282
|
+
committed_at: now,
|
|
6283
|
+
bridge_version: "sanctuary-concordia-bridge-v1"
|
|
6284
|
+
};
|
|
6285
|
+
const payloadBytes = stringToBytes(JSON.stringify(commitmentPayload));
|
|
6286
|
+
const signature = sign(payloadBytes, identity.encrypted_private_key, identityEncryptionKey);
|
|
6287
|
+
return {
|
|
6288
|
+
bridge_commitment_id: commitmentId,
|
|
6289
|
+
session_id: outcome.session_id,
|
|
6290
|
+
sha256_commitment: sha2564.commitment,
|
|
6291
|
+
blinding_factor: sha2564.blinding_factor,
|
|
6292
|
+
committer_did: identity.did,
|
|
6293
|
+
signature: toBase64url(signature),
|
|
6294
|
+
pedersen_commitment: pedersenData,
|
|
6295
|
+
committed_at: now,
|
|
6296
|
+
bridge_version: "sanctuary-concordia-bridge-v1"
|
|
6297
|
+
};
|
|
6298
|
+
}
|
|
6299
|
+
function verifyBridgeCommitment(commitment, outcome, committerPublicKey) {
|
|
6300
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6301
|
+
const canonicalString = new TextDecoder().decode(canonicalize(outcome));
|
|
6302
|
+
const sha256Match = verifyCommitment(
|
|
6303
|
+
commitment.sha256_commitment,
|
|
6304
|
+
canonicalString,
|
|
6305
|
+
commitment.blinding_factor
|
|
6306
|
+
);
|
|
6307
|
+
const commitmentPayload = {
|
|
6308
|
+
bridge_commitment_id: commitment.bridge_commitment_id,
|
|
6309
|
+
session_id: commitment.session_id,
|
|
6310
|
+
sha256_commitment: commitment.sha256_commitment,
|
|
6311
|
+
committer_did: commitment.committer_did,
|
|
6312
|
+
committed_at: commitment.committed_at,
|
|
6313
|
+
bridge_version: commitment.bridge_version
|
|
6314
|
+
};
|
|
6315
|
+
const payloadBytes = stringToBytes(JSON.stringify(commitmentPayload));
|
|
6316
|
+
const sigBytes = fromBase64url(commitment.signature);
|
|
6317
|
+
const signatureValid = verify(payloadBytes, sigBytes, committerPublicKey);
|
|
6318
|
+
const sessionIdMatch = commitment.session_id === outcome.session_id;
|
|
6319
|
+
const termsBytes = stringToBytes(stableStringify(outcome.terms));
|
|
6320
|
+
const computedTermsHash = toBase64url(hash(termsBytes));
|
|
6321
|
+
const termsHashMatch = computedTermsHash === outcome.terms_hash;
|
|
6322
|
+
let pedersenMatch;
|
|
6323
|
+
if (commitment.pedersen_commitment) {
|
|
6324
|
+
pedersenMatch = verifyPedersenCommitment(
|
|
6325
|
+
commitment.pedersen_commitment.commitment,
|
|
6326
|
+
outcome.rounds,
|
|
6327
|
+
commitment.pedersen_commitment.blinding_factor
|
|
6328
|
+
);
|
|
6329
|
+
}
|
|
6330
|
+
const valid = sha256Match && signatureValid && sessionIdMatch && termsHashMatch && (pedersenMatch === void 0 || pedersenMatch);
|
|
6331
|
+
return {
|
|
6332
|
+
valid,
|
|
6333
|
+
checks: {
|
|
6334
|
+
sha256_match: sha256Match,
|
|
6335
|
+
signature_valid: signatureValid,
|
|
6336
|
+
session_id_match: sessionIdMatch,
|
|
6337
|
+
terms_hash_match: termsHashMatch,
|
|
6338
|
+
pedersen_match: pedersenMatch
|
|
6339
|
+
},
|
|
6340
|
+
bridge_commitment_id: commitment.bridge_commitment_id,
|
|
6341
|
+
verified_at: now
|
|
6342
|
+
};
|
|
6343
|
+
}
|
|
6344
|
+
|
|
6345
|
+
// src/bridge/tools.ts
|
|
6346
|
+
var BridgeStore = class {
|
|
6347
|
+
storage;
|
|
6348
|
+
encryptionKey;
|
|
6349
|
+
constructor(storage, masterKey) {
|
|
6350
|
+
this.storage = storage;
|
|
6351
|
+
this.encryptionKey = derivePurposeKey(masterKey, "bridge-commitments");
|
|
6352
|
+
}
|
|
6353
|
+
async save(commitment, outcome) {
|
|
6354
|
+
const record = { commitment, outcome };
|
|
6355
|
+
const serialized = stringToBytes(JSON.stringify(record));
|
|
6356
|
+
const encrypted = encrypt(serialized, this.encryptionKey);
|
|
6357
|
+
await this.storage.write(
|
|
6358
|
+
"_bridge",
|
|
6359
|
+
commitment.bridge_commitment_id,
|
|
6360
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
6361
|
+
);
|
|
6362
|
+
}
|
|
6363
|
+
async get(commitmentId) {
|
|
6364
|
+
const raw = await this.storage.read("_bridge", commitmentId);
|
|
6365
|
+
if (!raw) return null;
|
|
6366
|
+
try {
|
|
6367
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
6368
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
6369
|
+
return JSON.parse(bytesToString(decrypted));
|
|
6370
|
+
} catch {
|
|
6371
|
+
return null;
|
|
6372
|
+
}
|
|
6373
|
+
}
|
|
6374
|
+
};
|
|
6375
|
+
function createBridgeTools(storage, masterKey, identityManager, auditLog, handshakeResults) {
|
|
6376
|
+
const bridgeStore = new BridgeStore(storage, masterKey);
|
|
6377
|
+
const reputationStore = new ReputationStore(storage, masterKey);
|
|
6378
|
+
const identityEncryptionKey = derivePurposeKey(masterKey, "identity-encryption");
|
|
6379
|
+
const hsResults = handshakeResults ?? /* @__PURE__ */ new Map();
|
|
6380
|
+
function resolveIdentity(identityId) {
|
|
6381
|
+
const id = identityId ? identityManager.get(identityId) : identityManager.getDefault();
|
|
6382
|
+
if (!id) {
|
|
6383
|
+
throw new Error(
|
|
6384
|
+
identityId ? `Identity "${identityId}" not found` : "No identity available. Create one with identity_create first."
|
|
6385
|
+
);
|
|
6386
|
+
}
|
|
6387
|
+
return id;
|
|
6388
|
+
}
|
|
6389
|
+
const tools = [
|
|
6390
|
+
// ─── bridge_commit ─────────────────────────────────────────────────
|
|
6391
|
+
{
|
|
6392
|
+
name: "sanctuary/bridge_commit",
|
|
6393
|
+
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.",
|
|
6394
|
+
inputSchema: {
|
|
6395
|
+
type: "object",
|
|
6396
|
+
properties: {
|
|
6397
|
+
session_id: {
|
|
6398
|
+
type: "string",
|
|
6399
|
+
description: "Concordia session identifier"
|
|
6400
|
+
},
|
|
6401
|
+
protocol_version: {
|
|
6402
|
+
type: "string",
|
|
6403
|
+
description: 'Concordia protocol version (e.g., "concordia-v1")'
|
|
6404
|
+
},
|
|
6405
|
+
proposer_did: {
|
|
6406
|
+
type: "string",
|
|
6407
|
+
description: "DID of the party who proposed the accepted terms"
|
|
6408
|
+
},
|
|
6409
|
+
acceptor_did: {
|
|
6410
|
+
type: "string",
|
|
6411
|
+
description: "DID of the party who accepted"
|
|
6412
|
+
},
|
|
6413
|
+
terms: {
|
|
6414
|
+
type: "object",
|
|
6415
|
+
description: "The accepted terms (opaque to Sanctuary, meaningful to Concordia)"
|
|
6416
|
+
},
|
|
6417
|
+
terms_hash: {
|
|
6418
|
+
type: "string",
|
|
6419
|
+
description: "SHA-256 hash of the canonical terms serialization (computed by Concordia)"
|
|
6420
|
+
},
|
|
6421
|
+
rounds: {
|
|
6422
|
+
type: "number",
|
|
6423
|
+
description: "Number of negotiation rounds (propose/counter cycles)"
|
|
6424
|
+
},
|
|
6425
|
+
accepted_at: {
|
|
6426
|
+
type: "string",
|
|
6427
|
+
description: "ISO 8601 timestamp when accept was issued"
|
|
6428
|
+
},
|
|
6429
|
+
session_receipt: {
|
|
6430
|
+
type: "string",
|
|
6431
|
+
description: "Optional: signed Concordia session receipt"
|
|
6432
|
+
},
|
|
6433
|
+
identity_id: {
|
|
6434
|
+
type: "string",
|
|
6435
|
+
description: "Sanctuary identity to sign the commitment (uses default if omitted)"
|
|
6436
|
+
},
|
|
6437
|
+
include_pedersen: {
|
|
6438
|
+
type: "boolean",
|
|
6439
|
+
description: "Include a Pedersen commitment on round count for ZK range proofs"
|
|
6440
|
+
}
|
|
6441
|
+
},
|
|
6442
|
+
required: [
|
|
6443
|
+
"session_id",
|
|
6444
|
+
"protocol_version",
|
|
6445
|
+
"proposer_did",
|
|
6446
|
+
"acceptor_did",
|
|
6447
|
+
"terms",
|
|
6448
|
+
"terms_hash",
|
|
6449
|
+
"rounds",
|
|
6450
|
+
"accepted_at"
|
|
6451
|
+
]
|
|
6452
|
+
},
|
|
6453
|
+
handler: async (args) => {
|
|
6454
|
+
const outcome = {
|
|
6455
|
+
session_id: args.session_id,
|
|
6456
|
+
protocol_version: args.protocol_version,
|
|
6457
|
+
proposer_did: args.proposer_did,
|
|
6458
|
+
acceptor_did: args.acceptor_did,
|
|
6459
|
+
terms: args.terms,
|
|
6460
|
+
terms_hash: args.terms_hash,
|
|
6461
|
+
rounds: args.rounds,
|
|
6462
|
+
accepted_at: args.accepted_at,
|
|
6463
|
+
session_receipt: args.session_receipt
|
|
6464
|
+
};
|
|
6465
|
+
const identity = resolveIdentity(args.identity_id);
|
|
6466
|
+
const includePedersen = args.include_pedersen ?? false;
|
|
6467
|
+
const bridgeCommitment = createBridgeCommitment(
|
|
6468
|
+
outcome,
|
|
6469
|
+
identity,
|
|
6470
|
+
identityEncryptionKey,
|
|
6471
|
+
includePedersen
|
|
6472
|
+
);
|
|
6473
|
+
await bridgeStore.save(bridgeCommitment, outcome);
|
|
6474
|
+
auditLog.append("l3", "bridge_commit", identity.identity_id, {
|
|
6475
|
+
bridge_commitment_id: bridgeCommitment.bridge_commitment_id,
|
|
6476
|
+
session_id: outcome.session_id,
|
|
6477
|
+
counterparty: outcome.proposer_did === identity.did ? outcome.acceptor_did : outcome.proposer_did
|
|
6478
|
+
});
|
|
6479
|
+
return toolResult({
|
|
6480
|
+
bridge_commitment_id: bridgeCommitment.bridge_commitment_id,
|
|
6481
|
+
session_id: bridgeCommitment.session_id,
|
|
6482
|
+
sha256_commitment: bridgeCommitment.sha256_commitment,
|
|
6483
|
+
committer_did: bridgeCommitment.committer_did,
|
|
6484
|
+
signature: bridgeCommitment.signature,
|
|
6485
|
+
pedersen_commitment: bridgeCommitment.pedersen_commitment ? { commitment: bridgeCommitment.pedersen_commitment.commitment } : void 0,
|
|
6486
|
+
committed_at: bridgeCommitment.committed_at,
|
|
6487
|
+
bridge_version: bridgeCommitment.bridge_version,
|
|
6488
|
+
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."
|
|
6489
|
+
});
|
|
6490
|
+
}
|
|
6491
|
+
},
|
|
6492
|
+
// ─── bridge_verify ───────────────────────────────────────────────────
|
|
6493
|
+
{
|
|
6494
|
+
name: "sanctuary/bridge_verify",
|
|
6495
|
+
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.",
|
|
6496
|
+
inputSchema: {
|
|
6497
|
+
type: "object",
|
|
6498
|
+
properties: {
|
|
6499
|
+
bridge_commitment_id: {
|
|
6500
|
+
type: "string",
|
|
6501
|
+
description: "The bridge commitment ID to verify"
|
|
6502
|
+
},
|
|
6503
|
+
committer_public_key: {
|
|
6504
|
+
type: "string",
|
|
6505
|
+
description: "The committer's Ed25519 public key (base64url). Required if verifying a counterparty's commitment. Omit to auto-resolve from local identities."
|
|
6506
|
+
}
|
|
6507
|
+
},
|
|
6508
|
+
required: ["bridge_commitment_id"]
|
|
6509
|
+
},
|
|
6510
|
+
handler: async (args) => {
|
|
6511
|
+
const commitmentId = args.bridge_commitment_id;
|
|
6512
|
+
const externalPublicKey = args.committer_public_key;
|
|
6513
|
+
const record = await bridgeStore.get(commitmentId);
|
|
6514
|
+
if (!record) {
|
|
6515
|
+
return toolResult({
|
|
6516
|
+
error: `Bridge commitment "${commitmentId}" not found`
|
|
6517
|
+
});
|
|
6518
|
+
}
|
|
6519
|
+
const { commitment: storedCommitment, outcome } = record;
|
|
6520
|
+
let publicKey;
|
|
6521
|
+
if (externalPublicKey) {
|
|
6522
|
+
publicKey = fromBase64url(externalPublicKey);
|
|
6523
|
+
} else {
|
|
6524
|
+
const localIdentities = identityManager.list();
|
|
6525
|
+
const match = localIdentities.find((i) => i.did === storedCommitment.committer_did);
|
|
6526
|
+
if (!match) {
|
|
6527
|
+
return toolResult({
|
|
6528
|
+
error: `Cannot resolve public key for committer "${storedCommitment.committer_did}". Provide committer_public_key for external verification.`
|
|
6529
|
+
});
|
|
6530
|
+
}
|
|
6531
|
+
publicKey = fromBase64url(match.public_key);
|
|
6532
|
+
}
|
|
6533
|
+
const result = verifyBridgeCommitment(storedCommitment, outcome, publicKey);
|
|
6534
|
+
auditLog.append("l3", "bridge_verify", "system", {
|
|
6535
|
+
bridge_commitment_id: commitmentId,
|
|
6536
|
+
session_id: storedCommitment.session_id,
|
|
6537
|
+
valid: result.valid
|
|
6538
|
+
});
|
|
6539
|
+
return toolResult({
|
|
6540
|
+
...result,
|
|
6541
|
+
session_id: storedCommitment.session_id,
|
|
6542
|
+
committer_did: storedCommitment.committer_did
|
|
6543
|
+
});
|
|
6544
|
+
}
|
|
6545
|
+
},
|
|
6546
|
+
// ─── bridge_attest ───────────────────────────────────────────────────
|
|
6547
|
+
{
|
|
6548
|
+
name: "sanctuary/bridge_attest",
|
|
6549
|
+
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.",
|
|
6550
|
+
inputSchema: {
|
|
6551
|
+
type: "object",
|
|
6552
|
+
properties: {
|
|
6553
|
+
bridge_commitment_id: {
|
|
6554
|
+
type: "string",
|
|
6555
|
+
description: "The bridge commitment ID to link"
|
|
6556
|
+
},
|
|
6557
|
+
outcome_result: {
|
|
6558
|
+
type: "string",
|
|
6559
|
+
enum: ["completed", "partial", "failed", "disputed"],
|
|
6560
|
+
description: "Negotiation outcome for reputation scoring"
|
|
6561
|
+
},
|
|
6562
|
+
metrics: {
|
|
6563
|
+
type: "object",
|
|
6564
|
+
description: "Optional metrics (e.g., rounds, response_time_ms, terms_complexity)"
|
|
6565
|
+
},
|
|
6566
|
+
identity_id: {
|
|
6567
|
+
type: "string",
|
|
6568
|
+
description: "Identity to sign the attestation (uses default if omitted)"
|
|
6569
|
+
}
|
|
6570
|
+
},
|
|
6571
|
+
required: ["bridge_commitment_id", "outcome_result"]
|
|
6572
|
+
},
|
|
6573
|
+
handler: async (args) => {
|
|
6574
|
+
const commitmentId = args.bridge_commitment_id;
|
|
6575
|
+
const outcomeResult = args.outcome_result;
|
|
6576
|
+
const metrics = args.metrics ?? {};
|
|
6577
|
+
const identityId = args.identity_id;
|
|
6578
|
+
const record = await bridgeStore.get(commitmentId);
|
|
6579
|
+
if (!record) {
|
|
6580
|
+
return toolResult({
|
|
6581
|
+
error: `Bridge commitment "${commitmentId}" not found`
|
|
6582
|
+
});
|
|
6583
|
+
}
|
|
6584
|
+
const { outcome } = record;
|
|
6585
|
+
const identity = resolveIdentity(identityId);
|
|
6586
|
+
const counterpartyDid = outcome.proposer_did === identity.did ? outcome.acceptor_did : outcome.proposer_did;
|
|
6587
|
+
const hasSanctuaryIdentity = identityManager.list().some(
|
|
6588
|
+
(id) => identityManager.get(id.identity_id)?.did === counterpartyDid
|
|
6589
|
+
);
|
|
6590
|
+
const tierMeta = resolveTier(counterpartyDid, hsResults, hasSanctuaryIdentity);
|
|
6591
|
+
const tier = tierMeta.sovereignty_tier;
|
|
6592
|
+
const fullMetrics = {
|
|
6593
|
+
...metrics,
|
|
6594
|
+
negotiation_rounds: outcome.rounds
|
|
6595
|
+
};
|
|
6596
|
+
const attestation = await reputationStore.record(
|
|
6597
|
+
outcome.session_id,
|
|
6598
|
+
// interaction_id = concordia session
|
|
6599
|
+
counterpartyDid,
|
|
6600
|
+
{
|
|
6601
|
+
type: "negotiation",
|
|
6602
|
+
result: outcomeResult,
|
|
6603
|
+
metrics: fullMetrics
|
|
6604
|
+
},
|
|
6605
|
+
"concordia-bridge",
|
|
6606
|
+
// context
|
|
6607
|
+
identity,
|
|
6608
|
+
identityEncryptionKey,
|
|
6609
|
+
void 0,
|
|
6610
|
+
// counterparty_attestation
|
|
6611
|
+
tier
|
|
6612
|
+
);
|
|
6613
|
+
auditLog.append("l4", "bridge_attest", identity.identity_id, {
|
|
6614
|
+
bridge_commitment_id: commitmentId,
|
|
6615
|
+
session_id: outcome.session_id,
|
|
6616
|
+
attestation_id: attestation.attestation.attestation_id,
|
|
6617
|
+
counterparty_did: counterpartyDid,
|
|
6618
|
+
sovereignty_tier: tier
|
|
6619
|
+
});
|
|
6620
|
+
const weight = TIER_WEIGHTS[tier];
|
|
6621
|
+
return toolResult({
|
|
6622
|
+
attestation_id: attestation.attestation.attestation_id,
|
|
6623
|
+
bridge_commitment_id: commitmentId,
|
|
6624
|
+
session_id: outcome.session_id,
|
|
6625
|
+
counterparty_did: counterpartyDid,
|
|
6626
|
+
outcome_result: outcomeResult,
|
|
6627
|
+
sovereignty_tier: tier,
|
|
6628
|
+
attested_at: attestation.recorded_at,
|
|
6629
|
+
note: `Negotiation recorded as reputation attestation. Counterparty sovereignty tier: ${tier} (weight: ${weight}).`
|
|
6630
|
+
});
|
|
6631
|
+
}
|
|
6632
|
+
}
|
|
6633
|
+
];
|
|
6634
|
+
return { tools };
|
|
4091
6635
|
}
|
|
4092
6636
|
|
|
4093
6637
|
// src/index.ts
|
|
@@ -4369,30 +6913,74 @@ async function createSanctuaryServer(options) {
|
|
|
4369
6913
|
}
|
|
4370
6914
|
};
|
|
4371
6915
|
const { tools: l3Tools } = createL3Tools(storage, masterKey, auditLog);
|
|
4372
|
-
const { tools: l4Tools } = createL4Tools(
|
|
4373
|
-
storage,
|
|
4374
|
-
masterKey,
|
|
4375
|
-
identityManager,
|
|
4376
|
-
auditLog
|
|
4377
|
-
);
|
|
4378
|
-
const policy = await loadPrincipalPolicy(config.storage_path);
|
|
4379
|
-
const baseline = new BaselineTracker(storage, masterKey);
|
|
4380
|
-
await baseline.load();
|
|
4381
|
-
const approvalChannel = new StderrApprovalChannel(policy.approval_channel);
|
|
4382
|
-
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog);
|
|
4383
|
-
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
4384
6916
|
const { tools: shrTools } = createSHRTools(
|
|
4385
6917
|
config,
|
|
4386
6918
|
identityManager,
|
|
4387
6919
|
masterKey,
|
|
4388
6920
|
auditLog
|
|
4389
6921
|
);
|
|
4390
|
-
const { tools: handshakeTools } = createHandshakeTools(
|
|
6922
|
+
const { tools: handshakeTools, handshakeResults } = createHandshakeTools(
|
|
4391
6923
|
config,
|
|
4392
6924
|
identityManager,
|
|
4393
6925
|
masterKey,
|
|
4394
6926
|
auditLog
|
|
4395
6927
|
);
|
|
6928
|
+
const { tools: l4Tools } = createL4Tools(
|
|
6929
|
+
storage,
|
|
6930
|
+
masterKey,
|
|
6931
|
+
identityManager,
|
|
6932
|
+
auditLog,
|
|
6933
|
+
handshakeResults
|
|
6934
|
+
);
|
|
6935
|
+
const { tools: federationTools } = createFederationTools(
|
|
6936
|
+
auditLog,
|
|
6937
|
+
handshakeResults
|
|
6938
|
+
);
|
|
6939
|
+
const { tools: bridgeTools } = createBridgeTools(
|
|
6940
|
+
storage,
|
|
6941
|
+
masterKey,
|
|
6942
|
+
identityManager,
|
|
6943
|
+
auditLog,
|
|
6944
|
+
handshakeResults
|
|
6945
|
+
);
|
|
6946
|
+
const policy = await loadPrincipalPolicy(config.storage_path);
|
|
6947
|
+
const baseline = new BaselineTracker(storage, masterKey);
|
|
6948
|
+
await baseline.load();
|
|
6949
|
+
let approvalChannel;
|
|
6950
|
+
let dashboard;
|
|
6951
|
+
if (config.dashboard.enabled) {
|
|
6952
|
+
let authToken = config.dashboard.auth_token;
|
|
6953
|
+
if (authToken === "auto") {
|
|
6954
|
+
const { randomBytes: rb } = await import('crypto');
|
|
6955
|
+
authToken = rb(32).toString("hex");
|
|
6956
|
+
}
|
|
6957
|
+
dashboard = new DashboardApprovalChannel({
|
|
6958
|
+
port: config.dashboard.port,
|
|
6959
|
+
host: config.dashboard.host,
|
|
6960
|
+
timeout_seconds: policy.approval_channel.timeout_seconds,
|
|
6961
|
+
auto_deny: policy.approval_channel.auto_deny,
|
|
6962
|
+
auth_token: authToken,
|
|
6963
|
+
tls: config.dashboard.tls
|
|
6964
|
+
});
|
|
6965
|
+
dashboard.setDependencies({ policy, baseline, auditLog });
|
|
6966
|
+
await dashboard.start();
|
|
6967
|
+
approvalChannel = dashboard;
|
|
6968
|
+
} else if (config.webhook.enabled && config.webhook.url && config.webhook.secret) {
|
|
6969
|
+
const webhook = new WebhookApprovalChannel({
|
|
6970
|
+
webhook_url: config.webhook.url,
|
|
6971
|
+
webhook_secret: config.webhook.secret,
|
|
6972
|
+
callback_port: config.webhook.callback_port,
|
|
6973
|
+
callback_host: config.webhook.callback_host,
|
|
6974
|
+
timeout_seconds: policy.approval_channel.timeout_seconds,
|
|
6975
|
+
auto_deny: policy.approval_channel.auto_deny
|
|
6976
|
+
});
|
|
6977
|
+
await webhook.start();
|
|
6978
|
+
approvalChannel = webhook;
|
|
6979
|
+
} else {
|
|
6980
|
+
approvalChannel = new StderrApprovalChannel(policy.approval_channel);
|
|
6981
|
+
}
|
|
6982
|
+
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog);
|
|
6983
|
+
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
4396
6984
|
const allTools = [
|
|
4397
6985
|
...l1Tools,
|
|
4398
6986
|
...l2Tools,
|
|
@@ -4401,6 +6989,8 @@ async function createSanctuaryServer(options) {
|
|
|
4401
6989
|
...policyTools,
|
|
4402
6990
|
...shrTools,
|
|
4403
6991
|
...handshakeTools,
|
|
6992
|
+
...federationTools,
|
|
6993
|
+
...bridgeTools,
|
|
4404
6994
|
manifestTool
|
|
4405
6995
|
];
|
|
4406
6996
|
const server = createServer(allTools, { gate });
|
|
@@ -4428,7 +7018,21 @@ async function createSanctuaryServer(options) {
|
|
|
4428
7018
|
|
|
4429
7019
|
// src/cli.ts
|
|
4430
7020
|
async function main() {
|
|
4431
|
-
const
|
|
7021
|
+
const args = process.argv.slice(2);
|
|
7022
|
+
let passphrase = process.env.SANCTUARY_PASSPHRASE;
|
|
7023
|
+
for (let i = 0; i < args.length; i++) {
|
|
7024
|
+
if (args[i] === "--dashboard") {
|
|
7025
|
+
process.env.SANCTUARY_DASHBOARD_ENABLED = "true";
|
|
7026
|
+
} else if (args[i] === "--passphrase" && args[i + 1]) {
|
|
7027
|
+
passphrase = args[++i];
|
|
7028
|
+
} else if (args[i] === "--help" || args[i] === "-h") {
|
|
7029
|
+
printHelp();
|
|
7030
|
+
process.exit(0);
|
|
7031
|
+
} else if (args[i] === "--version" || args[i] === "-v") {
|
|
7032
|
+
console.log("@sanctuary-framework/mcp-server 0.3.0");
|
|
7033
|
+
process.exit(0);
|
|
7034
|
+
}
|
|
7035
|
+
}
|
|
4432
7036
|
const { server, config } = await createSanctuaryServer({ passphrase });
|
|
4433
7037
|
if (config.transport === "stdio") {
|
|
4434
7038
|
const transport = new StdioServerTransport();
|
|
@@ -4441,6 +7045,34 @@ async function main() {
|
|
|
4441
7045
|
process.exit(1);
|
|
4442
7046
|
}
|
|
4443
7047
|
}
|
|
7048
|
+
function printHelp() {
|
|
7049
|
+
console.log(`
|
|
7050
|
+
@sanctuary-framework/mcp-server v0.3.0
|
|
7051
|
+
|
|
7052
|
+
Sovereignty infrastructure for agents in the agentic economy.
|
|
7053
|
+
|
|
7054
|
+
Usage:
|
|
7055
|
+
sanctuary-mcp-server [options]
|
|
7056
|
+
|
|
7057
|
+
Options:
|
|
7058
|
+
--dashboard Enable the Principal Dashboard (web UI)
|
|
7059
|
+
--passphrase <pass> Derive encryption key from passphrase
|
|
7060
|
+
--help, -h Show this help
|
|
7061
|
+
--version, -v Show version
|
|
7062
|
+
|
|
7063
|
+
Environment variables:
|
|
7064
|
+
SANCTUARY_STORAGE_PATH State directory (default: ~/.sanctuary)
|
|
7065
|
+
SANCTUARY_PASSPHRASE Key derivation passphrase
|
|
7066
|
+
SANCTUARY_DASHBOARD_ENABLED "true" to enable dashboard
|
|
7067
|
+
SANCTUARY_DASHBOARD_PORT Dashboard port (default: 3501)
|
|
7068
|
+
SANCTUARY_DASHBOARD_AUTH_TOKEN Bearer token or "auto"
|
|
7069
|
+
SANCTUARY_WEBHOOK_ENABLED "true" to enable webhook approvals
|
|
7070
|
+
SANCTUARY_WEBHOOK_URL Webhook target URL
|
|
7071
|
+
SANCTUARY_WEBHOOK_SECRET HMAC-SHA256 shared secret
|
|
7072
|
+
|
|
7073
|
+
For more info: https://github.com/eriknewton/sanctuary-framework
|
|
7074
|
+
`);
|
|
7075
|
+
}
|
|
4444
7076
|
main().catch((err) => {
|
|
4445
7077
|
console.error("Sanctuary MCP Server failed to start:", err);
|
|
4446
7078
|
process.exit(1);
|