@sanctuary-framework/mcp-server 0.2.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/LICENSE +190 -0
- package/README.md +210 -0
- package/dist/cli.cjs +4451 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +4449 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +4524 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1207 -0
- package/dist/index.d.ts +1207 -0
- package/dist/index.js +4502 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4502 @@
|
|
|
1
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
2
|
+
import { hmac } from '@noble/hashes/hmac';
|
|
3
|
+
import { readFile, mkdir, writeFile, stat, unlink, readdir, chmod } from 'fs/promises';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { randomBytes as randomBytes$1 } from 'crypto';
|
|
7
|
+
import { gcm } from '@noble/ciphers/aes.js';
|
|
8
|
+
import { ed25519 } from '@noble/curves/ed25519';
|
|
9
|
+
import { argon2id } from 'hash-wasm';
|
|
10
|
+
import { hkdf } from '@noble/hashes/hkdf';
|
|
11
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
12
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
13
|
+
|
|
14
|
+
var __defProp = Object.defineProperty;
|
|
15
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
16
|
+
var __esm = (fn, res) => function __init() {
|
|
17
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
18
|
+
};
|
|
19
|
+
var __export = (target, all) => {
|
|
20
|
+
for (var name in all)
|
|
21
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// src/core/encoding.ts
|
|
25
|
+
var encoding_exports = {};
|
|
26
|
+
__export(encoding_exports, {
|
|
27
|
+
bytesToString: () => bytesToString,
|
|
28
|
+
concatBytes: () => concatBytes,
|
|
29
|
+
constantTimeEqual: () => constantTimeEqual,
|
|
30
|
+
fromBase64url: () => fromBase64url,
|
|
31
|
+
stringToBytes: () => stringToBytes,
|
|
32
|
+
toBase64url: () => toBase64url
|
|
33
|
+
});
|
|
34
|
+
function toBase64url(bytes) {
|
|
35
|
+
const base64 = Buffer.from(bytes).toString("base64");
|
|
36
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
37
|
+
}
|
|
38
|
+
function fromBase64url(str) {
|
|
39
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
40
|
+
while (base64.length % 4 !== 0) {
|
|
41
|
+
base64 += "=";
|
|
42
|
+
}
|
|
43
|
+
const buf = Buffer.from(base64, "base64");
|
|
44
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
45
|
+
}
|
|
46
|
+
function stringToBytes(str) {
|
|
47
|
+
return new TextEncoder().encode(str);
|
|
48
|
+
}
|
|
49
|
+
function bytesToString(bytes) {
|
|
50
|
+
return new TextDecoder().decode(bytes);
|
|
51
|
+
}
|
|
52
|
+
function concatBytes(...arrays) {
|
|
53
|
+
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
|
54
|
+
const result = new Uint8Array(totalLength);
|
|
55
|
+
let offset = 0;
|
|
56
|
+
for (const arr of arrays) {
|
|
57
|
+
result.set(arr, offset);
|
|
58
|
+
offset += arr.length;
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
function constantTimeEqual(a, b) {
|
|
63
|
+
if (a.length !== b.length) return false;
|
|
64
|
+
let diff = 0;
|
|
65
|
+
for (let i = 0; i < a.length; i++) {
|
|
66
|
+
diff |= a[i] ^ b[i];
|
|
67
|
+
}
|
|
68
|
+
return diff === 0;
|
|
69
|
+
}
|
|
70
|
+
var init_encoding = __esm({
|
|
71
|
+
"src/core/encoding.ts"() {
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// src/core/hashing.ts
|
|
76
|
+
var hashing_exports = {};
|
|
77
|
+
__export(hashing_exports, {
|
|
78
|
+
buildMerkleTree: () => buildMerkleTree,
|
|
79
|
+
computeMerkleRoot: () => computeMerkleRoot,
|
|
80
|
+
generateMerkleProof: () => generateMerkleProof,
|
|
81
|
+
hash: () => hash,
|
|
82
|
+
hashToString: () => hashToString,
|
|
83
|
+
hmacSha256: () => hmacSha256,
|
|
84
|
+
verifyMerkleProof: () => verifyMerkleProof
|
|
85
|
+
});
|
|
86
|
+
function hash(data) {
|
|
87
|
+
return sha256(data);
|
|
88
|
+
}
|
|
89
|
+
function hashToString(data) {
|
|
90
|
+
return toBase64url(hash(data));
|
|
91
|
+
}
|
|
92
|
+
function hmacSha256(key, data) {
|
|
93
|
+
return hmac(sha256, key, data);
|
|
94
|
+
}
|
|
95
|
+
function buildMerkleTree(entries) {
|
|
96
|
+
if (entries.size === 0) return null;
|
|
97
|
+
const sortedKeys = Array.from(entries.keys()).sort();
|
|
98
|
+
let nodes = sortedKeys.map((key) => {
|
|
99
|
+
const contentHash = entries.get(key);
|
|
100
|
+
const leafData = concatBytes(
|
|
101
|
+
stringToBytes(key),
|
|
102
|
+
stringToBytes(contentHash)
|
|
103
|
+
);
|
|
104
|
+
return {
|
|
105
|
+
hash: hashToString(leafData),
|
|
106
|
+
key
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
while (nodes.length > 1) {
|
|
110
|
+
const nextLevel = [];
|
|
111
|
+
for (let i = 0; i < nodes.length; i += 2) {
|
|
112
|
+
const left = nodes[i];
|
|
113
|
+
if (i + 1 < nodes.length) {
|
|
114
|
+
const right = nodes[i + 1];
|
|
115
|
+
const parentData = concatBytes(
|
|
116
|
+
stringToBytes(left.hash),
|
|
117
|
+
stringToBytes(right.hash)
|
|
118
|
+
);
|
|
119
|
+
nextLevel.push({
|
|
120
|
+
hash: hashToString(parentData),
|
|
121
|
+
left,
|
|
122
|
+
right
|
|
123
|
+
});
|
|
124
|
+
} else {
|
|
125
|
+
nextLevel.push(left);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
nodes = nextLevel;
|
|
129
|
+
}
|
|
130
|
+
return nodes[0] ?? null;
|
|
131
|
+
}
|
|
132
|
+
function generateMerkleProof(entries, targetKey) {
|
|
133
|
+
if (!entries.has(targetKey)) return null;
|
|
134
|
+
const sortedKeys = Array.from(entries.keys()).sort();
|
|
135
|
+
const targetIndex = sortedKeys.indexOf(targetKey);
|
|
136
|
+
if (targetIndex === -1) return null;
|
|
137
|
+
const leafHashes = sortedKeys.map((key) => {
|
|
138
|
+
const contentHash = entries.get(key);
|
|
139
|
+
const leafData = concatBytes(
|
|
140
|
+
stringToBytes(key),
|
|
141
|
+
stringToBytes(contentHash)
|
|
142
|
+
);
|
|
143
|
+
return hashToString(leafData);
|
|
144
|
+
});
|
|
145
|
+
const path = [];
|
|
146
|
+
let currentIndex = targetIndex;
|
|
147
|
+
let currentLevel = leafHashes;
|
|
148
|
+
while (currentLevel.length > 1) {
|
|
149
|
+
const nextLevel = [];
|
|
150
|
+
for (let i = 0; i < currentLevel.length; i += 2) {
|
|
151
|
+
const left = currentLevel[i];
|
|
152
|
+
if (i + 1 < currentLevel.length) {
|
|
153
|
+
const right = currentLevel[i + 1];
|
|
154
|
+
if (i === currentIndex || i + 1 === currentIndex) {
|
|
155
|
+
if (currentIndex === i) {
|
|
156
|
+
path.push({ hash: right, position: "right" });
|
|
157
|
+
} else {
|
|
158
|
+
path.push({ hash: left, position: "left" });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const parentData = concatBytes(
|
|
162
|
+
stringToBytes(left),
|
|
163
|
+
stringToBytes(right)
|
|
164
|
+
);
|
|
165
|
+
nextLevel.push(hashToString(parentData));
|
|
166
|
+
} else {
|
|
167
|
+
nextLevel.push(left);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
currentIndex = Math.floor(currentIndex / 2);
|
|
171
|
+
currentLevel = nextLevel;
|
|
172
|
+
}
|
|
173
|
+
const root = buildMerkleTree(entries);
|
|
174
|
+
return {
|
|
175
|
+
leaf: leafHashes[targetIndex],
|
|
176
|
+
path,
|
|
177
|
+
root: root?.hash ?? ""
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function verifyMerkleProof(proof) {
|
|
181
|
+
let currentHash = proof.leaf;
|
|
182
|
+
for (const step of proof.path) {
|
|
183
|
+
const left = step.position === "left" ? step.hash : currentHash;
|
|
184
|
+
const right = step.position === "right" ? step.hash : currentHash;
|
|
185
|
+
const parentData = concatBytes(
|
|
186
|
+
stringToBytes(left),
|
|
187
|
+
stringToBytes(right)
|
|
188
|
+
);
|
|
189
|
+
currentHash = hashToString(parentData);
|
|
190
|
+
}
|
|
191
|
+
return currentHash === proof.root;
|
|
192
|
+
}
|
|
193
|
+
function computeMerkleRoot(entries) {
|
|
194
|
+
const tree = buildMerkleTree(entries);
|
|
195
|
+
return tree?.hash ?? "";
|
|
196
|
+
}
|
|
197
|
+
var init_hashing = __esm({
|
|
198
|
+
"src/core/hashing.ts"() {
|
|
199
|
+
init_encoding();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
function defaultConfig() {
|
|
203
|
+
return {
|
|
204
|
+
version: "0.2.0",
|
|
205
|
+
storage_path: join(homedir(), ".sanctuary"),
|
|
206
|
+
state: {
|
|
207
|
+
encryption: "aes-256-gcm",
|
|
208
|
+
key_protection: "none",
|
|
209
|
+
key_derivation: "argon2id",
|
|
210
|
+
integrity: "merkle-sha256",
|
|
211
|
+
identity_provider: "ed25519"
|
|
212
|
+
},
|
|
213
|
+
execution: {
|
|
214
|
+
environment: "local-process",
|
|
215
|
+
attestation: true,
|
|
216
|
+
resource_limits: {
|
|
217
|
+
max_memory_mb: 512,
|
|
218
|
+
max_storage_mb: 1024,
|
|
219
|
+
max_cpu_percent: 50
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
disclosure: {
|
|
223
|
+
proof_system: "commitment-only",
|
|
224
|
+
default_policy: "minimum-necessary"
|
|
225
|
+
},
|
|
226
|
+
reputation: {
|
|
227
|
+
mode: "self-custodied",
|
|
228
|
+
attestation_format: "eas-compatible",
|
|
229
|
+
export_format: "SANCTUARY_REP_V1",
|
|
230
|
+
service_endpoints: []
|
|
231
|
+
},
|
|
232
|
+
transport: "stdio",
|
|
233
|
+
http_port: 3500
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
async function loadConfig(configPath) {
|
|
237
|
+
const config = defaultConfig();
|
|
238
|
+
if (process.env.SANCTUARY_STORAGE_PATH) {
|
|
239
|
+
config.storage_path = process.env.SANCTUARY_STORAGE_PATH;
|
|
240
|
+
}
|
|
241
|
+
if (process.env.SANCTUARY_TRANSPORT) {
|
|
242
|
+
config.transport = process.env.SANCTUARY_TRANSPORT;
|
|
243
|
+
}
|
|
244
|
+
if (process.env.SANCTUARY_HTTP_PORT) {
|
|
245
|
+
config.http_port = parseInt(process.env.SANCTUARY_HTTP_PORT, 10);
|
|
246
|
+
}
|
|
247
|
+
const path = configPath ?? join(config.storage_path, "sanctuary.json");
|
|
248
|
+
try {
|
|
249
|
+
const raw = await readFile(path, "utf-8");
|
|
250
|
+
const fileConfig = JSON.parse(raw);
|
|
251
|
+
return deepMerge(config, fileConfig);
|
|
252
|
+
} catch {
|
|
253
|
+
return config;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async function saveConfig(config, configPath) {
|
|
257
|
+
const path = join(config.storage_path, "sanctuary.json");
|
|
258
|
+
await writeFile(path, JSON.stringify(config, null, 2), { mode: 384 });
|
|
259
|
+
}
|
|
260
|
+
function deepMerge(base, override) {
|
|
261
|
+
const result = { ...base };
|
|
262
|
+
for (const [key, value] of Object.entries(override)) {
|
|
263
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value) && typeof result[key] === "object" && result[key] !== null) {
|
|
264
|
+
result[key] = deepMerge(
|
|
265
|
+
result[key],
|
|
266
|
+
value
|
|
267
|
+
);
|
|
268
|
+
} else {
|
|
269
|
+
result[key] = value;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
function randomBytes(length) {
|
|
275
|
+
if (length <= 0) {
|
|
276
|
+
throw new RangeError("Length must be positive");
|
|
277
|
+
}
|
|
278
|
+
const buf = randomBytes$1(length);
|
|
279
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
280
|
+
}
|
|
281
|
+
function generateIV() {
|
|
282
|
+
return randomBytes(12);
|
|
283
|
+
}
|
|
284
|
+
function generateSalt() {
|
|
285
|
+
return randomBytes(32);
|
|
286
|
+
}
|
|
287
|
+
function generateRandomKey() {
|
|
288
|
+
return randomBytes(32);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/storage/filesystem.ts
|
|
292
|
+
var FilesystemStorage = class {
|
|
293
|
+
basePath;
|
|
294
|
+
constructor(basePath) {
|
|
295
|
+
this.basePath = basePath;
|
|
296
|
+
}
|
|
297
|
+
entryPath(namespace, key) {
|
|
298
|
+
const safeNamespace = namespace.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
299
|
+
const safeKey = key.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
300
|
+
return join(this.basePath, safeNamespace, `${safeKey}.enc`);
|
|
301
|
+
}
|
|
302
|
+
namespacePath(namespace) {
|
|
303
|
+
const safeNamespace = namespace.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
304
|
+
return join(this.basePath, safeNamespace);
|
|
305
|
+
}
|
|
306
|
+
async write(namespace, key, data) {
|
|
307
|
+
const dirPath = this.namespacePath(namespace);
|
|
308
|
+
const filePath = this.entryPath(namespace, key);
|
|
309
|
+
await mkdir(dirPath, { recursive: true, mode: 448 });
|
|
310
|
+
await writeFile(filePath, data, { mode: 384 });
|
|
311
|
+
}
|
|
312
|
+
async read(namespace, key) {
|
|
313
|
+
const filePath = this.entryPath(namespace, key);
|
|
314
|
+
try {
|
|
315
|
+
const buf = await readFile(filePath);
|
|
316
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
317
|
+
} catch (err) {
|
|
318
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
throw err;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async delete(namespace, key, secureOverwrite = true) {
|
|
325
|
+
const filePath = this.entryPath(namespace, key);
|
|
326
|
+
try {
|
|
327
|
+
if (secureOverwrite) {
|
|
328
|
+
const fileStat = await stat(filePath);
|
|
329
|
+
const size = fileStat.size;
|
|
330
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
331
|
+
const randomData = randomBytes(size);
|
|
332
|
+
await writeFile(filePath, randomData, { mode: 384 });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
await unlink(filePath);
|
|
336
|
+
return true;
|
|
337
|
+
} catch (err) {
|
|
338
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
throw err;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
async list(namespace, prefix) {
|
|
345
|
+
const dirPath = this.namespacePath(namespace);
|
|
346
|
+
try {
|
|
347
|
+
const files = await readdir(dirPath);
|
|
348
|
+
const entries = [];
|
|
349
|
+
for (const file of files) {
|
|
350
|
+
if (!file.endsWith(".enc")) continue;
|
|
351
|
+
const key = file.slice(0, -4);
|
|
352
|
+
if (prefix && !key.startsWith(prefix)) continue;
|
|
353
|
+
const filePath = join(dirPath, file);
|
|
354
|
+
const fileStat = await stat(filePath);
|
|
355
|
+
entries.push({
|
|
356
|
+
key,
|
|
357
|
+
namespace,
|
|
358
|
+
size_bytes: fileStat.size,
|
|
359
|
+
modified_at: fileStat.mtime.toISOString()
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
return entries.sort((a, b) => a.key.localeCompare(b.key));
|
|
363
|
+
} catch (err) {
|
|
364
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
throw err;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async exists(namespace, key) {
|
|
371
|
+
const filePath = this.entryPath(namespace, key);
|
|
372
|
+
try {
|
|
373
|
+
await stat(filePath);
|
|
374
|
+
return true;
|
|
375
|
+
} catch {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async totalSize() {
|
|
380
|
+
let total = 0;
|
|
381
|
+
try {
|
|
382
|
+
const namespaces = await readdir(this.basePath);
|
|
383
|
+
for (const ns of namespaces) {
|
|
384
|
+
const nsPath = join(this.basePath, ns);
|
|
385
|
+
const nsStat = await stat(nsPath);
|
|
386
|
+
if (!nsStat.isDirectory()) continue;
|
|
387
|
+
const files = await readdir(nsPath);
|
|
388
|
+
for (const file of files) {
|
|
389
|
+
const filePath = join(nsPath, file);
|
|
390
|
+
const fileStat = await stat(filePath);
|
|
391
|
+
total += fileStat.size;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
} catch {
|
|
395
|
+
}
|
|
396
|
+
return total;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
init_encoding();
|
|
400
|
+
function encrypt(plaintext, key, aad) {
|
|
401
|
+
if (key.length !== 32) {
|
|
402
|
+
throw new Error("Key must be exactly 32 bytes (256 bits)");
|
|
403
|
+
}
|
|
404
|
+
const iv = generateIV();
|
|
405
|
+
const cipher = gcm(key, iv, aad);
|
|
406
|
+
const ciphertext = cipher.encrypt(plaintext);
|
|
407
|
+
return {
|
|
408
|
+
v: 1,
|
|
409
|
+
alg: "aes-256-gcm",
|
|
410
|
+
iv: toBase64url(iv),
|
|
411
|
+
ct: toBase64url(ciphertext),
|
|
412
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function decrypt(payload, key, aad) {
|
|
416
|
+
if (key.length !== 32) {
|
|
417
|
+
throw new Error("Key must be exactly 32 bytes (256 bits)");
|
|
418
|
+
}
|
|
419
|
+
if (payload.v !== 1) {
|
|
420
|
+
throw new Error(`Unsupported payload version: ${payload.v}`);
|
|
421
|
+
}
|
|
422
|
+
if (payload.alg !== "aes-256-gcm") {
|
|
423
|
+
throw new Error(`Unsupported algorithm: ${payload.alg}`);
|
|
424
|
+
}
|
|
425
|
+
const iv = fromBase64url(payload.iv);
|
|
426
|
+
const ciphertext = fromBase64url(payload.ct);
|
|
427
|
+
const cipher = gcm(key, iv, aad);
|
|
428
|
+
return cipher.decrypt(ciphertext);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/l1-cognitive/state-store.ts
|
|
432
|
+
init_hashing();
|
|
433
|
+
|
|
434
|
+
// src/core/identity.ts
|
|
435
|
+
init_encoding();
|
|
436
|
+
init_hashing();
|
|
437
|
+
function generateKeypair() {
|
|
438
|
+
const privateKey = randomBytes(32);
|
|
439
|
+
const publicKey = ed25519.getPublicKey(privateKey);
|
|
440
|
+
return { publicKey, privateKey };
|
|
441
|
+
}
|
|
442
|
+
function publicKeyToDid(publicKey) {
|
|
443
|
+
const multicodec = new Uint8Array([237, 1, ...publicKey]);
|
|
444
|
+
return `did:key:z${toBase64url(multicodec)}`;
|
|
445
|
+
}
|
|
446
|
+
function generateIdentityId(publicKey) {
|
|
447
|
+
const keyHash = hash(publicKey);
|
|
448
|
+
return Array.from(keyHash.slice(0, 16)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
449
|
+
}
|
|
450
|
+
function createIdentity(label, encryptionKey, keyProtection) {
|
|
451
|
+
const { publicKey, privateKey } = generateKeypair();
|
|
452
|
+
const identityId = generateIdentityId(publicKey);
|
|
453
|
+
const did = publicKeyToDid(publicKey);
|
|
454
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
455
|
+
const encryptedPrivateKey = encrypt(privateKey, encryptionKey);
|
|
456
|
+
privateKey.fill(0);
|
|
457
|
+
const publicIdentity = {
|
|
458
|
+
identity_id: identityId,
|
|
459
|
+
label,
|
|
460
|
+
public_key: toBase64url(publicKey),
|
|
461
|
+
did,
|
|
462
|
+
created_at: now,
|
|
463
|
+
key_type: "ed25519",
|
|
464
|
+
key_protection: keyProtection
|
|
465
|
+
};
|
|
466
|
+
const storedIdentity = {
|
|
467
|
+
...publicIdentity,
|
|
468
|
+
encrypted_private_key: encryptedPrivateKey,
|
|
469
|
+
rotation_history: []
|
|
470
|
+
};
|
|
471
|
+
return { publicIdentity, storedIdentity };
|
|
472
|
+
}
|
|
473
|
+
function sign(payload, encryptedPrivateKey, encryptionKey) {
|
|
474
|
+
const privateKey = decrypt(encryptedPrivateKey, encryptionKey);
|
|
475
|
+
try {
|
|
476
|
+
return ed25519.sign(payload, privateKey);
|
|
477
|
+
} finally {
|
|
478
|
+
privateKey.fill(0);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
function verify(payload, signature, publicKey) {
|
|
482
|
+
try {
|
|
483
|
+
return ed25519.verify(signature, payload, publicKey);
|
|
484
|
+
} catch {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function rotateKeys(storedIdentity, encryptionKey, reason) {
|
|
489
|
+
const { publicKey: newPublicKey, privateKey: newPrivateKey } = generateKeypair();
|
|
490
|
+
const newIdentityDid = publicKeyToDid(newPublicKey);
|
|
491
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
492
|
+
const eventData = JSON.stringify({
|
|
493
|
+
old_public_key: storedIdentity.public_key,
|
|
494
|
+
new_public_key: toBase64url(newPublicKey),
|
|
495
|
+
identity_id: storedIdentity.identity_id,
|
|
496
|
+
reason,
|
|
497
|
+
rotated_at: now
|
|
498
|
+
});
|
|
499
|
+
const eventBytes = new TextEncoder().encode(eventData);
|
|
500
|
+
const signature = sign(
|
|
501
|
+
eventBytes,
|
|
502
|
+
storedIdentity.encrypted_private_key,
|
|
503
|
+
encryptionKey
|
|
504
|
+
);
|
|
505
|
+
const rotationEvent = {
|
|
506
|
+
old_public_key: storedIdentity.public_key,
|
|
507
|
+
new_public_key: toBase64url(newPublicKey),
|
|
508
|
+
identity_id: storedIdentity.identity_id,
|
|
509
|
+
reason,
|
|
510
|
+
rotated_at: now,
|
|
511
|
+
signature: toBase64url(signature)
|
|
512
|
+
};
|
|
513
|
+
const encryptedNewPrivateKey = encrypt(newPrivateKey, encryptionKey);
|
|
514
|
+
newPrivateKey.fill(0);
|
|
515
|
+
const updatedIdentity = {
|
|
516
|
+
...storedIdentity,
|
|
517
|
+
public_key: toBase64url(newPublicKey),
|
|
518
|
+
did: newIdentityDid,
|
|
519
|
+
encrypted_private_key: encryptedNewPrivateKey,
|
|
520
|
+
rotation_history: [
|
|
521
|
+
...storedIdentity.rotation_history,
|
|
522
|
+
{
|
|
523
|
+
old_public_key: storedIdentity.public_key,
|
|
524
|
+
new_public_key: toBase64url(newPublicKey),
|
|
525
|
+
rotation_event: toBase64url(
|
|
526
|
+
new TextEncoder().encode(JSON.stringify(rotationEvent))
|
|
527
|
+
),
|
|
528
|
+
rotated_at: now
|
|
529
|
+
}
|
|
530
|
+
]
|
|
531
|
+
};
|
|
532
|
+
return { updatedIdentity, rotationEvent };
|
|
533
|
+
}
|
|
534
|
+
init_encoding();
|
|
535
|
+
var ARGON2_MEMORY_COST = 65536;
|
|
536
|
+
var ARGON2_TIME_COST = 3;
|
|
537
|
+
var ARGON2_PARALLELISM = 4;
|
|
538
|
+
var ARGON2_HASH_LENGTH = 32;
|
|
539
|
+
async function deriveMasterKey(passphrase, existingParams) {
|
|
540
|
+
const salt = existingParams ? fromBase64url(existingParams.salt) : generateSalt();
|
|
541
|
+
const params = existingParams ?? {
|
|
542
|
+
alg: "argon2id",
|
|
543
|
+
salt: toBase64url(salt),
|
|
544
|
+
m: ARGON2_MEMORY_COST,
|
|
545
|
+
t: ARGON2_TIME_COST,
|
|
546
|
+
p: ARGON2_PARALLELISM,
|
|
547
|
+
l: ARGON2_HASH_LENGTH
|
|
548
|
+
};
|
|
549
|
+
const hashHex = await argon2id({
|
|
550
|
+
password: passphrase,
|
|
551
|
+
salt,
|
|
552
|
+
parallelism: params.p,
|
|
553
|
+
iterations: params.t,
|
|
554
|
+
memorySize: params.m,
|
|
555
|
+
hashLength: params.l,
|
|
556
|
+
outputType: "hex"
|
|
557
|
+
});
|
|
558
|
+
const key = new Uint8Array(params.l);
|
|
559
|
+
for (let i = 0; i < params.l; i++) {
|
|
560
|
+
key[i] = parseInt(hashHex.substring(i * 2, i * 2 + 2), 16);
|
|
561
|
+
}
|
|
562
|
+
return { key, params };
|
|
563
|
+
}
|
|
564
|
+
function deriveNamespaceKey(masterKey, namespace) {
|
|
565
|
+
if (masterKey.length !== 32) {
|
|
566
|
+
throw new Error("Master key must be 32 bytes");
|
|
567
|
+
}
|
|
568
|
+
return hkdf(
|
|
569
|
+
sha256,
|
|
570
|
+
masterKey,
|
|
571
|
+
stringToBytes("sanctuary-namespace-v1"),
|
|
572
|
+
// salt (fixed, acts as domain separator)
|
|
573
|
+
stringToBytes(namespace),
|
|
574
|
+
// info (namespace name)
|
|
575
|
+
32
|
|
576
|
+
// output length: 256 bits
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
function derivePurposeKey(masterKey, purpose) {
|
|
580
|
+
if (masterKey.length !== 32) {
|
|
581
|
+
throw new Error("Master key must be 32 bytes");
|
|
582
|
+
}
|
|
583
|
+
return hkdf(
|
|
584
|
+
sha256,
|
|
585
|
+
masterKey,
|
|
586
|
+
stringToBytes("sanctuary-purpose-v1"),
|
|
587
|
+
stringToBytes(purpose),
|
|
588
|
+
32
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/l1-cognitive/state-store.ts
|
|
593
|
+
init_encoding();
|
|
594
|
+
var RESERVED_NAMESPACE_PREFIXES = [
|
|
595
|
+
"_identities",
|
|
596
|
+
"_policies",
|
|
597
|
+
"_audit",
|
|
598
|
+
"_meta",
|
|
599
|
+
"_principal",
|
|
600
|
+
"_commitments",
|
|
601
|
+
"_reputation",
|
|
602
|
+
"_escrow",
|
|
603
|
+
"_guarantees"
|
|
604
|
+
];
|
|
605
|
+
var StateStore = class {
|
|
606
|
+
storage;
|
|
607
|
+
masterKey;
|
|
608
|
+
// Cache of version numbers per namespace/key for anti-rollback
|
|
609
|
+
versionCache = /* @__PURE__ */ new Map();
|
|
610
|
+
// Cache of content hashes per namespace for Merkle tree computation
|
|
611
|
+
contentHashes = /* @__PURE__ */ new Map();
|
|
612
|
+
constructor(storage, masterKey) {
|
|
613
|
+
this.storage = storage;
|
|
614
|
+
this.masterKey = masterKey;
|
|
615
|
+
}
|
|
616
|
+
versionKey(namespace, key) {
|
|
617
|
+
return `${namespace}/${key}`;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Get or initialize the content hash map for a namespace.
|
|
621
|
+
*/
|
|
622
|
+
async getNamespaceHashes(namespace) {
|
|
623
|
+
if (this.contentHashes.has(namespace)) {
|
|
624
|
+
return this.contentHashes.get(namespace);
|
|
625
|
+
}
|
|
626
|
+
const entries = await this.storage.list(namespace);
|
|
627
|
+
const hashMap = /* @__PURE__ */ new Map();
|
|
628
|
+
for (const entry of entries) {
|
|
629
|
+
const raw = await this.storage.read(namespace, entry.key);
|
|
630
|
+
if (raw) {
|
|
631
|
+
try {
|
|
632
|
+
const stateEntry = JSON.parse(bytesToString(raw));
|
|
633
|
+
hashMap.set(entry.key, stateEntry.integrity_hash);
|
|
634
|
+
this.versionCache.set(
|
|
635
|
+
this.versionKey(namespace, entry.key),
|
|
636
|
+
stateEntry.ver
|
|
637
|
+
);
|
|
638
|
+
} catch {
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
this.contentHashes.set(namespace, hashMap);
|
|
643
|
+
return hashMap;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Write encrypted state.
|
|
647
|
+
*
|
|
648
|
+
* @param namespace - Logical grouping
|
|
649
|
+
* @param key - State key
|
|
650
|
+
* @param value - Plaintext value (will be encrypted)
|
|
651
|
+
* @param identityId - Identity performing the write
|
|
652
|
+
* @param encryptedPrivateKey - Identity's encrypted private key (for signing)
|
|
653
|
+
* @param identityEncryptionKey - Key to decrypt the identity's private key
|
|
654
|
+
* @param options - Optional metadata
|
|
655
|
+
*/
|
|
656
|
+
async write(namespace, key, value, identityId, encryptedPrivateKey, identityEncryptionKey, options = {}) {
|
|
657
|
+
const namespaceKey = deriveNamespaceKey(this.masterKey, namespace);
|
|
658
|
+
const plaintext = stringToBytes(value);
|
|
659
|
+
const integrityHash = hashToString(plaintext);
|
|
660
|
+
const payload = encrypt(plaintext, namespaceKey);
|
|
661
|
+
const vk = this.versionKey(namespace, key);
|
|
662
|
+
const currentVersion = this.versionCache.get(vk) ?? 0;
|
|
663
|
+
const newVersion = currentVersion + 1;
|
|
664
|
+
const ciphertextBytes = fromBase64url(payload.ct);
|
|
665
|
+
const signature = sign(
|
|
666
|
+
ciphertextBytes,
|
|
667
|
+
encryptedPrivateKey,
|
|
668
|
+
identityEncryptionKey
|
|
669
|
+
);
|
|
670
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
671
|
+
const stateEntry = {
|
|
672
|
+
v: 1,
|
|
673
|
+
payload,
|
|
674
|
+
ver: newVersion,
|
|
675
|
+
sig: toBase64url(signature),
|
|
676
|
+
kid: identityId,
|
|
677
|
+
integrity_hash: integrityHash,
|
|
678
|
+
metadata: {
|
|
679
|
+
content_type: options.content_type,
|
|
680
|
+
ttl_seconds: options.ttl_seconds,
|
|
681
|
+
tags: options.tags,
|
|
682
|
+
written_at: now
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
const serialized = stringToBytes(JSON.stringify(stateEntry));
|
|
686
|
+
await this.storage.write(namespace, key, serialized);
|
|
687
|
+
this.versionCache.set(vk, newVersion);
|
|
688
|
+
const nsHashes = await this.getNamespaceHashes(namespace);
|
|
689
|
+
nsHashes.set(key, integrityHash);
|
|
690
|
+
const merkleRoot = computeMerkleRoot(nsHashes);
|
|
691
|
+
return {
|
|
692
|
+
key,
|
|
693
|
+
namespace,
|
|
694
|
+
version: newVersion,
|
|
695
|
+
merkle_root: merkleRoot,
|
|
696
|
+
written_at: now,
|
|
697
|
+
size_bytes: serialized.length,
|
|
698
|
+
integrity_hash: integrityHash
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Read and decrypt state.
|
|
703
|
+
*
|
|
704
|
+
* @param namespace - Logical grouping
|
|
705
|
+
* @param key - State key
|
|
706
|
+
* @param signerPublicKey - Expected signer's public key (for signature verification)
|
|
707
|
+
* @param verifyIntegrity - Whether to verify Merkle proof (default: true)
|
|
708
|
+
*/
|
|
709
|
+
async read(namespace, key, signerPublicKey, verifyIntegrity = true) {
|
|
710
|
+
const raw = await this.storage.read(namespace, key);
|
|
711
|
+
if (!raw) return null;
|
|
712
|
+
let stateEntry;
|
|
713
|
+
try {
|
|
714
|
+
stateEntry = JSON.parse(bytesToString(raw));
|
|
715
|
+
} catch {
|
|
716
|
+
throw new Error(`Corrupted state entry: ${namespace}/${key}`);
|
|
717
|
+
}
|
|
718
|
+
if (stateEntry.v !== 1) {
|
|
719
|
+
throw new Error(`Unsupported state entry version: ${stateEntry.v}`);
|
|
720
|
+
}
|
|
721
|
+
const vk = this.versionKey(namespace, key);
|
|
722
|
+
const cachedVersion = this.versionCache.get(vk);
|
|
723
|
+
if (cachedVersion !== void 0 && stateEntry.ver < cachedVersion) {
|
|
724
|
+
throw new Error(
|
|
725
|
+
`Rollback detected for ${namespace}/${key}: found version ${stateEntry.ver}, expected >= ${cachedVersion}`
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
if (signerPublicKey) {
|
|
729
|
+
const ciphertextBytes = fromBase64url(stateEntry.payload.ct);
|
|
730
|
+
const signatureBytes = fromBase64url(stateEntry.sig);
|
|
731
|
+
const sigValid = verify(ciphertextBytes, signatureBytes, signerPublicKey);
|
|
732
|
+
if (!sigValid) {
|
|
733
|
+
throw new Error(
|
|
734
|
+
`Signature verification failed for ${namespace}/${key}`
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
const namespaceKey = deriveNamespaceKey(this.masterKey, namespace);
|
|
739
|
+
const plaintext = decrypt(stateEntry.payload, namespaceKey);
|
|
740
|
+
const value = bytesToString(plaintext);
|
|
741
|
+
const computedHash = hashToString(plaintext);
|
|
742
|
+
if (computedHash !== stateEntry.integrity_hash) {
|
|
743
|
+
throw new Error(
|
|
744
|
+
`Integrity hash mismatch for ${namespace}/${key}: computed ${computedHash}, stored ${stateEntry.integrity_hash}`
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
let merkleProofPath = [];
|
|
748
|
+
let integrityVerified = true;
|
|
749
|
+
if (verifyIntegrity) {
|
|
750
|
+
const nsHashes = await this.getNamespaceHashes(namespace);
|
|
751
|
+
const proof = generateMerkleProof(nsHashes, key);
|
|
752
|
+
if (proof) {
|
|
753
|
+
integrityVerified = verifyMerkleProof(proof);
|
|
754
|
+
merkleProofPath = proof.path.map(
|
|
755
|
+
(step) => `${step.position}:${step.hash}`
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
this.versionCache.set(vk, stateEntry.ver);
|
|
760
|
+
return {
|
|
761
|
+
key,
|
|
762
|
+
namespace,
|
|
763
|
+
value,
|
|
764
|
+
version: stateEntry.ver,
|
|
765
|
+
integrity_verified: integrityVerified,
|
|
766
|
+
merkle_proof: merkleProofPath,
|
|
767
|
+
written_at: stateEntry.metadata.written_at,
|
|
768
|
+
written_by: stateEntry.kid
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* List keys in a namespace (metadata only — no decryption).
|
|
773
|
+
*/
|
|
774
|
+
async list(namespace, prefix, tags, limit = 100, offset = 0) {
|
|
775
|
+
const storageEntries = await this.storage.list(namespace, prefix);
|
|
776
|
+
const result = [];
|
|
777
|
+
for (const entry of storageEntries) {
|
|
778
|
+
const raw = await this.storage.read(namespace, entry.key);
|
|
779
|
+
if (!raw) continue;
|
|
780
|
+
try {
|
|
781
|
+
const stateEntry = JSON.parse(bytesToString(raw));
|
|
782
|
+
if (tags && tags.length > 0) {
|
|
783
|
+
const entryTags = stateEntry.metadata.tags ?? [];
|
|
784
|
+
const hasMatchingTag = tags.some((t) => entryTags.includes(t));
|
|
785
|
+
if (!hasMatchingTag) continue;
|
|
786
|
+
}
|
|
787
|
+
result.push({
|
|
788
|
+
key: entry.key,
|
|
789
|
+
version: stateEntry.ver,
|
|
790
|
+
size_bytes: entry.size_bytes,
|
|
791
|
+
written_at: stateEntry.metadata.written_at,
|
|
792
|
+
tags: stateEntry.metadata.tags ?? []
|
|
793
|
+
});
|
|
794
|
+
} catch {
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const nsHashes = await this.getNamespaceHashes(namespace);
|
|
798
|
+
const merkleRoot = computeMerkleRoot(nsHashes);
|
|
799
|
+
return {
|
|
800
|
+
keys: result.slice(offset, offset + limit),
|
|
801
|
+
total: result.length,
|
|
802
|
+
merkle_root: merkleRoot
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Securely delete state (overwrite with random bytes before removal).
|
|
807
|
+
*/
|
|
808
|
+
async delete(namespace, key) {
|
|
809
|
+
const deleted = await this.storage.delete(namespace, key, true);
|
|
810
|
+
const vk = this.versionKey(namespace, key);
|
|
811
|
+
this.versionCache.delete(vk);
|
|
812
|
+
const nsHashes = await this.getNamespaceHashes(namespace);
|
|
813
|
+
nsHashes.delete(key);
|
|
814
|
+
const merkleRoot = computeMerkleRoot(nsHashes);
|
|
815
|
+
return {
|
|
816
|
+
deleted,
|
|
817
|
+
key,
|
|
818
|
+
namespace,
|
|
819
|
+
new_merkle_root: merkleRoot,
|
|
820
|
+
deleted_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Export all state for a namespace as an encrypted bundle.
|
|
825
|
+
*/
|
|
826
|
+
async export(namespace) {
|
|
827
|
+
const namespacesToExport = [];
|
|
828
|
+
if (namespace) {
|
|
829
|
+
namespacesToExport.push(namespace);
|
|
830
|
+
} else {
|
|
831
|
+
for (const ns of this.contentHashes.keys()) {
|
|
832
|
+
namespacesToExport.push(ns);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
const exportData = {};
|
|
836
|
+
let totalKeys = 0;
|
|
837
|
+
for (const ns of namespacesToExport) {
|
|
838
|
+
const entries = await this.storage.list(ns);
|
|
839
|
+
exportData[ns] = [];
|
|
840
|
+
for (const entry of entries) {
|
|
841
|
+
const raw = await this.storage.read(ns, entry.key);
|
|
842
|
+
if (!raw) continue;
|
|
843
|
+
try {
|
|
844
|
+
const stateEntry = JSON.parse(bytesToString(raw));
|
|
845
|
+
exportData[ns].push({ key: entry.key, entry: stateEntry });
|
|
846
|
+
totalKeys++;
|
|
847
|
+
} catch {
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
const bundleJson = JSON.stringify({
|
|
852
|
+
sanctuary_export_version: 1,
|
|
853
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
854
|
+
namespaces: namespacesToExport,
|
|
855
|
+
data: exportData
|
|
856
|
+
});
|
|
857
|
+
const bundleBytes = stringToBytes(bundleJson);
|
|
858
|
+
const bundleHash = hashToString(bundleBytes);
|
|
859
|
+
return {
|
|
860
|
+
bundle: toBase64url(bundleBytes),
|
|
861
|
+
namespaces: namespacesToExport,
|
|
862
|
+
total_keys: totalKeys,
|
|
863
|
+
bundle_hash: bundleHash,
|
|
864
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Import a previously exported state bundle.
|
|
869
|
+
*/
|
|
870
|
+
async import(bundleBase64, conflictResolution = "skip") {
|
|
871
|
+
const bundleBytes = fromBase64url(bundleBase64);
|
|
872
|
+
const bundleJson = bytesToString(bundleBytes);
|
|
873
|
+
const bundle = JSON.parse(bundleJson);
|
|
874
|
+
let importedKeys = 0;
|
|
875
|
+
let skippedKeys = 0;
|
|
876
|
+
let conflicts = 0;
|
|
877
|
+
const namespaces = [];
|
|
878
|
+
for (const [ns, entries] of Object.entries(
|
|
879
|
+
bundle.data
|
|
880
|
+
)) {
|
|
881
|
+
if (RESERVED_NAMESPACE_PREFIXES.some(
|
|
882
|
+
(prefix) => ns === prefix || ns.startsWith(prefix + "/")
|
|
883
|
+
)) {
|
|
884
|
+
skippedKeys += entries.length;
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
namespaces.push(ns);
|
|
888
|
+
for (const { key, entry } of entries) {
|
|
889
|
+
const exists = await this.storage.exists(ns, key);
|
|
890
|
+
if (exists) {
|
|
891
|
+
conflicts++;
|
|
892
|
+
if (conflictResolution === "skip") {
|
|
893
|
+
skippedKeys++;
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
if (conflictResolution === "version") {
|
|
897
|
+
const raw = await this.storage.read(ns, key);
|
|
898
|
+
if (raw) {
|
|
899
|
+
try {
|
|
900
|
+
const existingEntry = JSON.parse(
|
|
901
|
+
bytesToString(raw)
|
|
902
|
+
);
|
|
903
|
+
if (entry.ver <= existingEntry.ver) {
|
|
904
|
+
skippedKeys++;
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
} catch {
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
const serialized = stringToBytes(JSON.stringify(entry));
|
|
913
|
+
await this.storage.write(ns, key, serialized);
|
|
914
|
+
importedKeys++;
|
|
915
|
+
const vk = this.versionKey(ns, key);
|
|
916
|
+
this.versionCache.set(vk, entry.ver);
|
|
917
|
+
const nsHashes = await this.getNamespaceHashes(ns);
|
|
918
|
+
nsHashes.set(key, entry.integrity_hash);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
imported_keys: importedKeys,
|
|
923
|
+
skipped_keys: skippedKeys,
|
|
924
|
+
conflicts,
|
|
925
|
+
namespaces,
|
|
926
|
+
imported_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
var MAX_STRING_BYTES = 1048576;
|
|
931
|
+
var MAX_BUNDLE_BYTES = 5242880;
|
|
932
|
+
var BUNDLE_FIELDS = /* @__PURE__ */ new Set(["bundle"]);
|
|
933
|
+
function validateArgs(args, schema) {
|
|
934
|
+
const errors = [];
|
|
935
|
+
const properties = schema.properties ?? {};
|
|
936
|
+
const required = schema.required ?? [];
|
|
937
|
+
for (const field of required) {
|
|
938
|
+
if (args[field] === void 0 || args[field] === null) {
|
|
939
|
+
errors.push({ field, message: `Required field "${field}" is missing` });
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
const knownFields = new Set(Object.keys(properties));
|
|
943
|
+
for (const field of Object.keys(args)) {
|
|
944
|
+
if (!knownFields.has(field)) {
|
|
945
|
+
errors.push({ field, message: `Unknown field "${field}"` });
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
for (const [field, value] of Object.entries(args)) {
|
|
949
|
+
if (value === void 0 || value === null) continue;
|
|
950
|
+
const propSchema = properties[field];
|
|
951
|
+
if (!propSchema) continue;
|
|
952
|
+
const typeError = checkType(field, value, propSchema);
|
|
953
|
+
if (typeError) {
|
|
954
|
+
errors.push(typeError);
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
if (typeof value === "string") {
|
|
958
|
+
const maxBytes = BUNDLE_FIELDS.has(field) ? MAX_BUNDLE_BYTES : MAX_STRING_BYTES;
|
|
959
|
+
const byteLength = new TextEncoder().encode(value).length;
|
|
960
|
+
if (byteLength > maxBytes) {
|
|
961
|
+
errors.push({
|
|
962
|
+
field,
|
|
963
|
+
message: `Field "${field}" exceeds maximum size (${byteLength} bytes > ${maxBytes} bytes)`
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
if (propSchema.enum && !propSchema.enum.includes(value)) {
|
|
968
|
+
errors.push({
|
|
969
|
+
field,
|
|
970
|
+
message: `Field "${field}" must be one of: ${propSchema.enum.join(", ")}`
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return errors;
|
|
975
|
+
}
|
|
976
|
+
function checkType(field, value, schema) {
|
|
977
|
+
if (!schema.type) return null;
|
|
978
|
+
switch (schema.type) {
|
|
979
|
+
case "string":
|
|
980
|
+
if (typeof value !== "string") {
|
|
981
|
+
return { field, message: `Expected string for "${field}", got ${typeof value}` };
|
|
982
|
+
}
|
|
983
|
+
break;
|
|
984
|
+
case "number":
|
|
985
|
+
if (typeof value !== "number") {
|
|
986
|
+
return { field, message: `Expected number for "${field}", got ${typeof value}` };
|
|
987
|
+
}
|
|
988
|
+
break;
|
|
989
|
+
case "boolean":
|
|
990
|
+
if (typeof value !== "boolean") {
|
|
991
|
+
return { field, message: `Expected boolean for "${field}", got ${typeof value}` };
|
|
992
|
+
}
|
|
993
|
+
break;
|
|
994
|
+
case "object":
|
|
995
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
996
|
+
return { field, message: `Expected object for "${field}", got ${typeof value}` };
|
|
997
|
+
}
|
|
998
|
+
break;
|
|
999
|
+
case "array":
|
|
1000
|
+
if (!Array.isArray(value)) {
|
|
1001
|
+
return { field, message: `Expected array for "${field}", got ${typeof value}` };
|
|
1002
|
+
}
|
|
1003
|
+
break;
|
|
1004
|
+
}
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
function createServer(tools, options) {
|
|
1008
|
+
const gate = options?.gate;
|
|
1009
|
+
const server = new Server(
|
|
1010
|
+
{
|
|
1011
|
+
name: "sanctuary-mcp-server",
|
|
1012
|
+
version: "0.2.0"
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
capabilities: {
|
|
1016
|
+
tools: {}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
);
|
|
1020
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1021
|
+
return {
|
|
1022
|
+
tools: tools.map((t) => ({
|
|
1023
|
+
name: t.name,
|
|
1024
|
+
description: t.description,
|
|
1025
|
+
inputSchema: t.inputSchema
|
|
1026
|
+
}))
|
|
1027
|
+
};
|
|
1028
|
+
});
|
|
1029
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1030
|
+
const { name, arguments: args } = request.params;
|
|
1031
|
+
const typedArgs = args ?? {};
|
|
1032
|
+
const tool = tools.find((t) => t.name === name);
|
|
1033
|
+
if (!tool) {
|
|
1034
|
+
return {
|
|
1035
|
+
content: [
|
|
1036
|
+
{
|
|
1037
|
+
type: "text",
|
|
1038
|
+
text: JSON.stringify({ error: `Unknown tool: ${name}` })
|
|
1039
|
+
}
|
|
1040
|
+
],
|
|
1041
|
+
isError: true
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
const validationErrors = validateArgs(typedArgs, tool.inputSchema);
|
|
1045
|
+
if (validationErrors.length > 0) {
|
|
1046
|
+
return {
|
|
1047
|
+
content: [
|
|
1048
|
+
{
|
|
1049
|
+
type: "text",
|
|
1050
|
+
text: JSON.stringify({
|
|
1051
|
+
error: "validation_failed",
|
|
1052
|
+
message: "Tool arguments failed schema validation",
|
|
1053
|
+
violations: validationErrors
|
|
1054
|
+
})
|
|
1055
|
+
}
|
|
1056
|
+
],
|
|
1057
|
+
isError: true
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
if (gate) {
|
|
1061
|
+
const result = await gate.evaluate(name, typedArgs);
|
|
1062
|
+
if (!result.allowed) {
|
|
1063
|
+
return {
|
|
1064
|
+
content: [
|
|
1065
|
+
{
|
|
1066
|
+
type: "text",
|
|
1067
|
+
text: JSON.stringify({
|
|
1068
|
+
error: "Operation not permitted",
|
|
1069
|
+
approval_required: result.approval_required
|
|
1070
|
+
})
|
|
1071
|
+
}
|
|
1072
|
+
],
|
|
1073
|
+
isError: true
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
try {
|
|
1078
|
+
return await tool.handler(typedArgs);
|
|
1079
|
+
} catch (err) {
|
|
1080
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1081
|
+
return {
|
|
1082
|
+
content: [
|
|
1083
|
+
{
|
|
1084
|
+
type: "text",
|
|
1085
|
+
text: JSON.stringify({ error: message })
|
|
1086
|
+
}
|
|
1087
|
+
],
|
|
1088
|
+
isError: true
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
return server;
|
|
1093
|
+
}
|
|
1094
|
+
function toolResult(data) {
|
|
1095
|
+
return {
|
|
1096
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// src/l1-cognitive/tools.ts
|
|
1101
|
+
init_encoding();
|
|
1102
|
+
init_encoding();
|
|
1103
|
+
var RESERVED_NAMESPACE_PREFIXES2 = [
|
|
1104
|
+
"_identities",
|
|
1105
|
+
"_policies",
|
|
1106
|
+
"_audit",
|
|
1107
|
+
"_meta",
|
|
1108
|
+
"_principal",
|
|
1109
|
+
"_commitments",
|
|
1110
|
+
"_reputation",
|
|
1111
|
+
"_escrow",
|
|
1112
|
+
"_guarantees"
|
|
1113
|
+
];
|
|
1114
|
+
function getReservedNamespaceViolation(namespace) {
|
|
1115
|
+
for (const prefix of RESERVED_NAMESPACE_PREFIXES2) {
|
|
1116
|
+
if (namespace === prefix || namespace.startsWith(prefix + "/")) {
|
|
1117
|
+
return prefix;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
var IdentityManager = class {
|
|
1123
|
+
storage;
|
|
1124
|
+
masterKey;
|
|
1125
|
+
identities = /* @__PURE__ */ new Map();
|
|
1126
|
+
primaryIdentityId = null;
|
|
1127
|
+
constructor(storage, masterKey) {
|
|
1128
|
+
this.storage = storage;
|
|
1129
|
+
this.masterKey = masterKey;
|
|
1130
|
+
}
|
|
1131
|
+
get encryptionKey() {
|
|
1132
|
+
return derivePurposeKey(this.masterKey, "identity-encryption");
|
|
1133
|
+
}
|
|
1134
|
+
/** Load identities from storage on startup */
|
|
1135
|
+
async load() {
|
|
1136
|
+
const entries = await this.storage.list("_identities");
|
|
1137
|
+
for (const entry of entries) {
|
|
1138
|
+
const raw = await this.storage.read("_identities", entry.key);
|
|
1139
|
+
if (!raw) continue;
|
|
1140
|
+
try {
|
|
1141
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
1142
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
1143
|
+
const identity = JSON.parse(bytesToString(decrypted));
|
|
1144
|
+
this.identities.set(identity.identity_id, identity);
|
|
1145
|
+
if (!this.primaryIdentityId) {
|
|
1146
|
+
this.primaryIdentityId = identity.identity_id;
|
|
1147
|
+
}
|
|
1148
|
+
} catch {
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
/** Save an identity to storage */
|
|
1153
|
+
async save(identity) {
|
|
1154
|
+
const serialized = stringToBytes(JSON.stringify(identity));
|
|
1155
|
+
const encrypted = encrypt(serialized, this.encryptionKey);
|
|
1156
|
+
await this.storage.write(
|
|
1157
|
+
"_identities",
|
|
1158
|
+
identity.identity_id,
|
|
1159
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
1160
|
+
);
|
|
1161
|
+
this.identities.set(identity.identity_id, identity);
|
|
1162
|
+
if (!this.primaryIdentityId) {
|
|
1163
|
+
this.primaryIdentityId = identity.identity_id;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
get(id) {
|
|
1167
|
+
return this.identities.get(id);
|
|
1168
|
+
}
|
|
1169
|
+
getDefault() {
|
|
1170
|
+
if (!this.primaryIdentityId) return void 0;
|
|
1171
|
+
return this.identities.get(this.primaryIdentityId);
|
|
1172
|
+
}
|
|
1173
|
+
list() {
|
|
1174
|
+
return Array.from(this.identities.values()).map((si) => ({
|
|
1175
|
+
identity_id: si.identity_id,
|
|
1176
|
+
label: si.label,
|
|
1177
|
+
public_key: si.public_key,
|
|
1178
|
+
did: si.did,
|
|
1179
|
+
created_at: si.created_at,
|
|
1180
|
+
key_type: si.key_type,
|
|
1181
|
+
key_protection: si.key_protection
|
|
1182
|
+
}));
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog) {
|
|
1186
|
+
const identityMgr = new IdentityManager(storage, masterKey);
|
|
1187
|
+
const identityEncKey = derivePurposeKey(masterKey, "identity-encryption");
|
|
1188
|
+
function resolveIdentity(identityId) {
|
|
1189
|
+
const id = identityId ? identityMgr.get(identityId) : identityMgr.getDefault();
|
|
1190
|
+
if (!id) {
|
|
1191
|
+
throw new Error(
|
|
1192
|
+
identityId ? `Identity not found: ${identityId}` : "No default identity. Create one with sanctuary/identity_create."
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
return id;
|
|
1196
|
+
}
|
|
1197
|
+
const tools = [
|
|
1198
|
+
// ── Identity Tools ──────────────────────────────────────────────────
|
|
1199
|
+
{
|
|
1200
|
+
name: "sanctuary/identity_create",
|
|
1201
|
+
description: "Create a new sovereign identity (Ed25519 keypair). The private key is encrypted and never exposed.",
|
|
1202
|
+
inputSchema: {
|
|
1203
|
+
type: "object",
|
|
1204
|
+
properties: {
|
|
1205
|
+
label: {
|
|
1206
|
+
type: "string",
|
|
1207
|
+
description: 'Human-readable label (e.g., "my-agent")'
|
|
1208
|
+
}
|
|
1209
|
+
},
|
|
1210
|
+
required: ["label"]
|
|
1211
|
+
},
|
|
1212
|
+
handler: async (args) => {
|
|
1213
|
+
const label = args.label;
|
|
1214
|
+
const { publicIdentity, storedIdentity } = createIdentity(
|
|
1215
|
+
label,
|
|
1216
|
+
identityEncKey,
|
|
1217
|
+
keyProtection
|
|
1218
|
+
);
|
|
1219
|
+
await identityMgr.save(storedIdentity);
|
|
1220
|
+
auditLog?.append("l1", "identity_create", publicIdentity.identity_id, {
|
|
1221
|
+
label
|
|
1222
|
+
});
|
|
1223
|
+
return toolResult({
|
|
1224
|
+
identity_id: publicIdentity.identity_id,
|
|
1225
|
+
public_key: publicIdentity.public_key,
|
|
1226
|
+
did: publicIdentity.did,
|
|
1227
|
+
created_at: publicIdentity.created_at,
|
|
1228
|
+
key_type: publicIdentity.key_type,
|
|
1229
|
+
key_protection: publicIdentity.key_protection,
|
|
1230
|
+
backed_up: false
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
},
|
|
1234
|
+
{
|
|
1235
|
+
name: "sanctuary/identity_list",
|
|
1236
|
+
description: "List all managed sovereign identities.",
|
|
1237
|
+
inputSchema: {
|
|
1238
|
+
type: "object",
|
|
1239
|
+
properties: {
|
|
1240
|
+
filter: {
|
|
1241
|
+
type: "object",
|
|
1242
|
+
properties: {
|
|
1243
|
+
label: { type: "string" }
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
},
|
|
1248
|
+
handler: async (args) => {
|
|
1249
|
+
let identities = identityMgr.list();
|
|
1250
|
+
const filter = args.filter;
|
|
1251
|
+
if (filter?.label) {
|
|
1252
|
+
identities = identities.filter(
|
|
1253
|
+
(i) => i.label.includes(filter.label)
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
return toolResult({ identities });
|
|
1257
|
+
}
|
|
1258
|
+
},
|
|
1259
|
+
{
|
|
1260
|
+
name: "sanctuary/identity_sign",
|
|
1261
|
+
description: "Sign data with a managed identity. The private key is decrypted in memory only during signing.",
|
|
1262
|
+
inputSchema: {
|
|
1263
|
+
type: "object",
|
|
1264
|
+
properties: {
|
|
1265
|
+
identity_id: { type: "string" },
|
|
1266
|
+
payload: {
|
|
1267
|
+
type: "string",
|
|
1268
|
+
description: "Base64url-encoded data to sign"
|
|
1269
|
+
}
|
|
1270
|
+
},
|
|
1271
|
+
required: ["payload"]
|
|
1272
|
+
},
|
|
1273
|
+
handler: async (args) => {
|
|
1274
|
+
const identity = resolveIdentity(args.identity_id);
|
|
1275
|
+
const payloadStr = args.payload;
|
|
1276
|
+
let payload;
|
|
1277
|
+
try {
|
|
1278
|
+
payload = fromBase64url(payloadStr);
|
|
1279
|
+
} catch {
|
|
1280
|
+
payload = stringToBytes(payloadStr);
|
|
1281
|
+
}
|
|
1282
|
+
const signature = sign(
|
|
1283
|
+
payload,
|
|
1284
|
+
identity.encrypted_private_key,
|
|
1285
|
+
identityEncKey
|
|
1286
|
+
);
|
|
1287
|
+
auditLog?.append("l1", "identity_sign", identity.identity_id);
|
|
1288
|
+
return toolResult({
|
|
1289
|
+
signature: toBase64url(signature),
|
|
1290
|
+
algorithm: "Ed25519",
|
|
1291
|
+
signed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1292
|
+
public_key: identity.public_key,
|
|
1293
|
+
payload_encoding: "base64url"
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
},
|
|
1297
|
+
{
|
|
1298
|
+
name: "sanctuary/identity_verify",
|
|
1299
|
+
description: "Verify an Ed25519 signature. Provide either identity_id or public_key.",
|
|
1300
|
+
inputSchema: {
|
|
1301
|
+
type: "object",
|
|
1302
|
+
properties: {
|
|
1303
|
+
payload: {
|
|
1304
|
+
type: "string",
|
|
1305
|
+
description: "Original data (plain text or base64url-encoded)"
|
|
1306
|
+
},
|
|
1307
|
+
signature: { type: "string", description: "Base64url signature" },
|
|
1308
|
+
identity_id: {
|
|
1309
|
+
type: "string",
|
|
1310
|
+
description: "Identity ID to look up public key (alternative to public_key)"
|
|
1311
|
+
},
|
|
1312
|
+
public_key: {
|
|
1313
|
+
type: "string",
|
|
1314
|
+
description: "Base64url public key (alternative to identity_id)"
|
|
1315
|
+
}
|
|
1316
|
+
},
|
|
1317
|
+
required: ["payload", "signature"]
|
|
1318
|
+
},
|
|
1319
|
+
handler: async (args) => {
|
|
1320
|
+
const payloadStr = args.payload;
|
|
1321
|
+
let payload;
|
|
1322
|
+
try {
|
|
1323
|
+
payload = fromBase64url(payloadStr);
|
|
1324
|
+
} catch {
|
|
1325
|
+
payload = stringToBytes(payloadStr);
|
|
1326
|
+
}
|
|
1327
|
+
const signature = fromBase64url(args.signature);
|
|
1328
|
+
let publicKey;
|
|
1329
|
+
if (args.identity_id) {
|
|
1330
|
+
const identity = resolveIdentity(args.identity_id);
|
|
1331
|
+
publicKey = fromBase64url(identity.public_key);
|
|
1332
|
+
} else if (args.public_key) {
|
|
1333
|
+
publicKey = fromBase64url(args.public_key);
|
|
1334
|
+
} else {
|
|
1335
|
+
return toolResult({
|
|
1336
|
+
error: "Provide either identity_id or public_key for verification."
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
const valid = verify(payload, signature, publicKey);
|
|
1340
|
+
return toolResult({
|
|
1341
|
+
valid,
|
|
1342
|
+
verified_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
},
|
|
1346
|
+
{
|
|
1347
|
+
name: "sanctuary/identity_rotate",
|
|
1348
|
+
description: "Rotate keys for an identity. Generates a new keypair and signs a rotation event with the old key for verifiable chain.",
|
|
1349
|
+
inputSchema: {
|
|
1350
|
+
type: "object",
|
|
1351
|
+
properties: {
|
|
1352
|
+
identity_id: { type: "string" },
|
|
1353
|
+
reason: { type: "string" }
|
|
1354
|
+
},
|
|
1355
|
+
required: ["identity_id"]
|
|
1356
|
+
},
|
|
1357
|
+
handler: async (args) => {
|
|
1358
|
+
const identity = resolveIdentity(args.identity_id);
|
|
1359
|
+
const reason = args.reason ?? "Key rotation";
|
|
1360
|
+
const { updatedIdentity, rotationEvent } = rotateKeys(
|
|
1361
|
+
identity,
|
|
1362
|
+
identityEncKey,
|
|
1363
|
+
reason
|
|
1364
|
+
);
|
|
1365
|
+
await identityMgr.save(updatedIdentity);
|
|
1366
|
+
auditLog?.append("l1", "identity_rotate", identity.identity_id, {
|
|
1367
|
+
reason
|
|
1368
|
+
});
|
|
1369
|
+
return toolResult({
|
|
1370
|
+
identity_id: updatedIdentity.identity_id,
|
|
1371
|
+
old_public_key: rotationEvent.old_public_key,
|
|
1372
|
+
new_public_key: rotationEvent.new_public_key,
|
|
1373
|
+
new_did: updatedIdentity.did,
|
|
1374
|
+
rotated_at: rotationEvent.rotated_at
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
},
|
|
1378
|
+
// ── State Tools ─────────────────────────────────────────────────────
|
|
1379
|
+
{
|
|
1380
|
+
name: "sanctuary/state_write",
|
|
1381
|
+
description: "Write encrypted state to the sovereign store. Value is encrypted with a namespace-specific key. The write is signed by the active identity.",
|
|
1382
|
+
inputSchema: {
|
|
1383
|
+
type: "object",
|
|
1384
|
+
properties: {
|
|
1385
|
+
namespace: {
|
|
1386
|
+
type: "string",
|
|
1387
|
+
description: 'Logical grouping (e.g., "memory", "config")'
|
|
1388
|
+
},
|
|
1389
|
+
key: { type: "string", description: "State key within namespace" },
|
|
1390
|
+
value: {
|
|
1391
|
+
type: "string",
|
|
1392
|
+
description: "Plaintext value (encrypted before storage)"
|
|
1393
|
+
},
|
|
1394
|
+
metadata: {
|
|
1395
|
+
type: "object",
|
|
1396
|
+
properties: {
|
|
1397
|
+
content_type: { type: "string" },
|
|
1398
|
+
ttl_seconds: { type: "number" },
|
|
1399
|
+
tags: { type: "array", items: { type: "string" } }
|
|
1400
|
+
}
|
|
1401
|
+
},
|
|
1402
|
+
identity_id: { type: "string" }
|
|
1403
|
+
},
|
|
1404
|
+
required: ["namespace", "key", "value"]
|
|
1405
|
+
},
|
|
1406
|
+
handler: async (args) => {
|
|
1407
|
+
const reservedViolation = getReservedNamespaceViolation(args.namespace);
|
|
1408
|
+
if (reservedViolation) {
|
|
1409
|
+
return toolResult({
|
|
1410
|
+
error: "namespace_reserved",
|
|
1411
|
+
message: `Namespace "${args.namespace}" is reserved for internal use (prefix: ${reservedViolation}). Choose a different namespace.`
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
const identity = resolveIdentity(args.identity_id);
|
|
1415
|
+
const metadata = args.metadata;
|
|
1416
|
+
const result = await stateStore.write(
|
|
1417
|
+
args.namespace,
|
|
1418
|
+
args.key,
|
|
1419
|
+
args.value,
|
|
1420
|
+
identity.identity_id,
|
|
1421
|
+
identity.encrypted_private_key,
|
|
1422
|
+
identityEncKey,
|
|
1423
|
+
{
|
|
1424
|
+
content_type: metadata?.content_type,
|
|
1425
|
+
ttl_seconds: metadata?.ttl_seconds,
|
|
1426
|
+
tags: metadata?.tags
|
|
1427
|
+
}
|
|
1428
|
+
);
|
|
1429
|
+
auditLog?.append("l1", "state_write", identity.identity_id, {
|
|
1430
|
+
namespace: args.namespace,
|
|
1431
|
+
key: args.key
|
|
1432
|
+
});
|
|
1433
|
+
return toolResult(result);
|
|
1434
|
+
}
|
|
1435
|
+
},
|
|
1436
|
+
{
|
|
1437
|
+
name: "sanctuary/state_read",
|
|
1438
|
+
description: "Read and decrypt state from the sovereign store. Verifies integrity via Merkle proof and signature.",
|
|
1439
|
+
inputSchema: {
|
|
1440
|
+
type: "object",
|
|
1441
|
+
properties: {
|
|
1442
|
+
namespace: { type: "string" },
|
|
1443
|
+
key: { type: "string" },
|
|
1444
|
+
verify_integrity: { type: "boolean", default: true }
|
|
1445
|
+
},
|
|
1446
|
+
required: ["namespace", "key"]
|
|
1447
|
+
},
|
|
1448
|
+
handler: async (args) => {
|
|
1449
|
+
const result = await stateStore.read(
|
|
1450
|
+
args.namespace,
|
|
1451
|
+
args.key,
|
|
1452
|
+
void 0,
|
|
1453
|
+
// Skip signature verification for now (would need writer's pubkey)
|
|
1454
|
+
args.verify_integrity ?? true
|
|
1455
|
+
);
|
|
1456
|
+
if (!result) {
|
|
1457
|
+
return toolResult({
|
|
1458
|
+
error: "not_found",
|
|
1459
|
+
namespace: args.namespace,
|
|
1460
|
+
key: args.key
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
auditLog?.append("l1", "state_read", result.written_by, {
|
|
1464
|
+
namespace: args.namespace,
|
|
1465
|
+
key: args.key
|
|
1466
|
+
});
|
|
1467
|
+
return toolResult(result);
|
|
1468
|
+
}
|
|
1469
|
+
},
|
|
1470
|
+
{
|
|
1471
|
+
name: "sanctuary/state_list",
|
|
1472
|
+
description: "List keys in a namespace (metadata only \u2014 no decryption).",
|
|
1473
|
+
inputSchema: {
|
|
1474
|
+
type: "object",
|
|
1475
|
+
properties: {
|
|
1476
|
+
namespace: { type: "string" },
|
|
1477
|
+
prefix: { type: "string" },
|
|
1478
|
+
tags: { type: "array", items: { type: "string" } },
|
|
1479
|
+
limit: { type: "number", default: 100 },
|
|
1480
|
+
offset: { type: "number", default: 0 }
|
|
1481
|
+
},
|
|
1482
|
+
required: ["namespace"]
|
|
1483
|
+
},
|
|
1484
|
+
handler: async (args) => {
|
|
1485
|
+
const result = await stateStore.list(
|
|
1486
|
+
args.namespace,
|
|
1487
|
+
args.prefix,
|
|
1488
|
+
args.tags,
|
|
1489
|
+
args.limit ?? 100,
|
|
1490
|
+
args.offset ?? 0
|
|
1491
|
+
);
|
|
1492
|
+
return toolResult(result);
|
|
1493
|
+
}
|
|
1494
|
+
},
|
|
1495
|
+
{
|
|
1496
|
+
name: "sanctuary/state_delete",
|
|
1497
|
+
description: "Securely delete state. Overwrites file with random bytes before removal (right to deletion, S1.6).",
|
|
1498
|
+
inputSchema: {
|
|
1499
|
+
type: "object",
|
|
1500
|
+
properties: {
|
|
1501
|
+
namespace: { type: "string" },
|
|
1502
|
+
key: { type: "string" },
|
|
1503
|
+
reason: { type: "string" }
|
|
1504
|
+
},
|
|
1505
|
+
required: ["namespace", "key"]
|
|
1506
|
+
},
|
|
1507
|
+
handler: async (args) => {
|
|
1508
|
+
const reservedViolation = getReservedNamespaceViolation(args.namespace);
|
|
1509
|
+
if (reservedViolation) {
|
|
1510
|
+
return toolResult({
|
|
1511
|
+
error: "namespace_reserved",
|
|
1512
|
+
message: `Namespace "${args.namespace}" is reserved for internal use (prefix: ${reservedViolation}). Cannot delete from reserved namespaces.`
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
const result = await stateStore.delete(
|
|
1516
|
+
args.namespace,
|
|
1517
|
+
args.key
|
|
1518
|
+
);
|
|
1519
|
+
auditLog?.append("l1", "state_delete", "principal", {
|
|
1520
|
+
namespace: args.namespace,
|
|
1521
|
+
key: args.key,
|
|
1522
|
+
reason: args.reason
|
|
1523
|
+
});
|
|
1524
|
+
return toolResult(result);
|
|
1525
|
+
}
|
|
1526
|
+
},
|
|
1527
|
+
{
|
|
1528
|
+
name: "sanctuary/state_export",
|
|
1529
|
+
description: "Export state as an encrypted, portable bundle for migration.",
|
|
1530
|
+
inputSchema: {
|
|
1531
|
+
type: "object",
|
|
1532
|
+
properties: {
|
|
1533
|
+
namespace: { type: "string" },
|
|
1534
|
+
format: { type: "string", default: "sanctuary-v1" }
|
|
1535
|
+
}
|
|
1536
|
+
},
|
|
1537
|
+
handler: async (args) => {
|
|
1538
|
+
const result = await stateStore.export(
|
|
1539
|
+
args.namespace
|
|
1540
|
+
);
|
|
1541
|
+
auditLog?.append("l1", "state_export", "principal", {
|
|
1542
|
+
namespaces: result.namespaces
|
|
1543
|
+
});
|
|
1544
|
+
return toolResult(result);
|
|
1545
|
+
}
|
|
1546
|
+
},
|
|
1547
|
+
{
|
|
1548
|
+
name: "sanctuary/state_import",
|
|
1549
|
+
description: "Import a previously exported state bundle.",
|
|
1550
|
+
inputSchema: {
|
|
1551
|
+
type: "object",
|
|
1552
|
+
properties: {
|
|
1553
|
+
bundle: { type: "string", description: "Base64url-encoded bundle" },
|
|
1554
|
+
conflict_resolution: {
|
|
1555
|
+
type: "string",
|
|
1556
|
+
enum: ["skip", "overwrite", "version"],
|
|
1557
|
+
default: "skip"
|
|
1558
|
+
}
|
|
1559
|
+
},
|
|
1560
|
+
required: ["bundle"]
|
|
1561
|
+
},
|
|
1562
|
+
handler: async (args) => {
|
|
1563
|
+
const result = await stateStore.import(
|
|
1564
|
+
args.bundle,
|
|
1565
|
+
args.conflict_resolution ?? "skip"
|
|
1566
|
+
);
|
|
1567
|
+
auditLog?.append("l1", "state_import", "principal", {
|
|
1568
|
+
imported_keys: result.imported_keys
|
|
1569
|
+
});
|
|
1570
|
+
return toolResult(result);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
];
|
|
1574
|
+
return { tools, identityManager: identityMgr };
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// src/l2-operational/audit-log.ts
|
|
1578
|
+
init_encoding();
|
|
1579
|
+
var AuditLog = class {
|
|
1580
|
+
storage;
|
|
1581
|
+
encryptionKey;
|
|
1582
|
+
entries = [];
|
|
1583
|
+
counter = 0;
|
|
1584
|
+
constructor(storage, masterKey) {
|
|
1585
|
+
this.storage = storage;
|
|
1586
|
+
this.encryptionKey = derivePurposeKey(masterKey, "audit-log");
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* Append an audit entry.
|
|
1590
|
+
*/
|
|
1591
|
+
append(layer, operation, identityId, details, result = "success") {
|
|
1592
|
+
const entry = {
|
|
1593
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1594
|
+
layer,
|
|
1595
|
+
operation,
|
|
1596
|
+
identity_id: identityId,
|
|
1597
|
+
result,
|
|
1598
|
+
details
|
|
1599
|
+
};
|
|
1600
|
+
this.entries.push(entry);
|
|
1601
|
+
this.persistEntry(entry).catch(() => {
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
async persistEntry(entry) {
|
|
1605
|
+
const key = `${Date.now()}-${this.counter++}`;
|
|
1606
|
+
const serialized = stringToBytes(JSON.stringify(entry));
|
|
1607
|
+
const encrypted = encrypt(serialized, this.encryptionKey);
|
|
1608
|
+
await this.storage.write(
|
|
1609
|
+
"_audit",
|
|
1610
|
+
key,
|
|
1611
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Query the audit log with filtering.
|
|
1616
|
+
*/
|
|
1617
|
+
async query(options) {
|
|
1618
|
+
await this.loadPersistedEntries();
|
|
1619
|
+
let filtered = this.entries;
|
|
1620
|
+
if (options.since) {
|
|
1621
|
+
const sinceDate = new Date(options.since);
|
|
1622
|
+
filtered = filtered.filter(
|
|
1623
|
+
(e) => new Date(e.timestamp) >= sinceDate
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
if (options.layer) {
|
|
1627
|
+
filtered = filtered.filter((e) => e.layer === options.layer);
|
|
1628
|
+
}
|
|
1629
|
+
if (options.operation_type) {
|
|
1630
|
+
filtered = filtered.filter(
|
|
1631
|
+
(e) => e.operation === options.operation_type
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
const total = filtered.length;
|
|
1635
|
+
const limit = options.limit ?? 50;
|
|
1636
|
+
const entries = filtered.slice(-limit);
|
|
1637
|
+
return { entries, total };
|
|
1638
|
+
}
|
|
1639
|
+
async loadPersistedEntries() {
|
|
1640
|
+
try {
|
|
1641
|
+
const storedEntries = await this.storage.list("_audit");
|
|
1642
|
+
for (const meta of storedEntries) {
|
|
1643
|
+
const raw = await this.storage.read("_audit", meta.key);
|
|
1644
|
+
if (!raw) continue;
|
|
1645
|
+
try {
|
|
1646
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
1647
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
1648
|
+
const entry = JSON.parse(bytesToString(decrypted));
|
|
1649
|
+
const isDuplicate = this.entries.some(
|
|
1650
|
+
(e) => e.timestamp === entry.timestamp && e.operation === entry.operation && e.identity_id === entry.identity_id
|
|
1651
|
+
);
|
|
1652
|
+
if (!isDuplicate) {
|
|
1653
|
+
this.entries.push(entry);
|
|
1654
|
+
}
|
|
1655
|
+
} catch {
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
this.entries.sort(
|
|
1659
|
+
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
1660
|
+
);
|
|
1661
|
+
} catch {
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Get total number of entries.
|
|
1666
|
+
*/
|
|
1667
|
+
get size() {
|
|
1668
|
+
return this.entries.length;
|
|
1669
|
+
}
|
|
1670
|
+
};
|
|
1671
|
+
|
|
1672
|
+
// src/l3-disclosure/commitments.ts
|
|
1673
|
+
init_hashing();
|
|
1674
|
+
init_encoding();
|
|
1675
|
+
init_encoding();
|
|
1676
|
+
function createCommitment(value, blindingFactor) {
|
|
1677
|
+
const blindingBytes = blindingFactor ? fromBase64url(blindingFactor) : randomBytes(32);
|
|
1678
|
+
const valueBytes = stringToBytes(value);
|
|
1679
|
+
const combined = concatBytes(valueBytes, blindingBytes);
|
|
1680
|
+
const commitmentHash = hash(combined);
|
|
1681
|
+
return {
|
|
1682
|
+
commitment: toBase64url(commitmentHash),
|
|
1683
|
+
blinding_factor: toBase64url(blindingBytes),
|
|
1684
|
+
committed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
function verifyCommitment(commitment, value, blindingFactor) {
|
|
1688
|
+
const blindingBytes = fromBase64url(blindingFactor);
|
|
1689
|
+
const valueBytes = stringToBytes(value);
|
|
1690
|
+
const combined = concatBytes(valueBytes, blindingBytes);
|
|
1691
|
+
const expectedHash = toBase64url(hash(combined));
|
|
1692
|
+
return commitment === expectedHash;
|
|
1693
|
+
}
|
|
1694
|
+
var CommitmentStore = class {
|
|
1695
|
+
storage;
|
|
1696
|
+
encryptionKey;
|
|
1697
|
+
constructor(storage, masterKey) {
|
|
1698
|
+
this.storage = storage;
|
|
1699
|
+
this.encryptionKey = derivePurposeKey(masterKey, "l3-commitments");
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Store a commitment (encrypted) for later reference.
|
|
1703
|
+
*/
|
|
1704
|
+
async store(commitment, value) {
|
|
1705
|
+
const id = `cmt-${Date.now()}-${toBase64url(randomBytes(8))}`;
|
|
1706
|
+
const stored = {
|
|
1707
|
+
commitment: commitment.commitment,
|
|
1708
|
+
blinding_factor: commitment.blinding_factor,
|
|
1709
|
+
value,
|
|
1710
|
+
committed_at: commitment.committed_at,
|
|
1711
|
+
revealed: false
|
|
1712
|
+
};
|
|
1713
|
+
const serialized = stringToBytes(JSON.stringify(stored));
|
|
1714
|
+
const encrypted = encrypt(serialized, this.encryptionKey);
|
|
1715
|
+
await this.storage.write(
|
|
1716
|
+
"_commitments",
|
|
1717
|
+
id,
|
|
1718
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
1719
|
+
);
|
|
1720
|
+
return id;
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* Retrieve a stored commitment by ID.
|
|
1724
|
+
*/
|
|
1725
|
+
async get(id) {
|
|
1726
|
+
const raw = await this.storage.read("_commitments", id);
|
|
1727
|
+
if (!raw) return null;
|
|
1728
|
+
try {
|
|
1729
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
1730
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
1731
|
+
return JSON.parse(bytesToString(decrypted));
|
|
1732
|
+
} catch {
|
|
1733
|
+
return null;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Mark a commitment as revealed.
|
|
1738
|
+
*/
|
|
1739
|
+
async markRevealed(id) {
|
|
1740
|
+
const stored = await this.get(id);
|
|
1741
|
+
if (!stored) return;
|
|
1742
|
+
stored.revealed = true;
|
|
1743
|
+
stored.revealed_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1744
|
+
const serialized = stringToBytes(JSON.stringify(stored));
|
|
1745
|
+
const encrypted = encrypt(serialized, this.encryptionKey);
|
|
1746
|
+
await this.storage.write(
|
|
1747
|
+
"_commitments",
|
|
1748
|
+
id,
|
|
1749
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
|
|
1754
|
+
// src/l3-disclosure/policies.ts
|
|
1755
|
+
init_encoding();
|
|
1756
|
+
function evaluateDisclosure(policy, context, requestedFields) {
|
|
1757
|
+
return requestedFields.map((field) => {
|
|
1758
|
+
const exactRule = policy.rules.find((r) => r.context === context);
|
|
1759
|
+
const wildcardRule = policy.rules.find((r) => r.context === "*");
|
|
1760
|
+
const matchedRule = exactRule ?? wildcardRule;
|
|
1761
|
+
if (!matchedRule) {
|
|
1762
|
+
return {
|
|
1763
|
+
field,
|
|
1764
|
+
action: policy.default_action,
|
|
1765
|
+
reason: `No rule matches context "${context}"`,
|
|
1766
|
+
applicable_rule: "default"
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
const ruleName = `${matchedRule.context}`;
|
|
1770
|
+
if (matchedRule.withhold.includes(field)) {
|
|
1771
|
+
return {
|
|
1772
|
+
field,
|
|
1773
|
+
action: "withhold",
|
|
1774
|
+
reason: `Field "${field}" is explicitly withheld in ${ruleName} context`,
|
|
1775
|
+
applicable_rule: ruleName
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
if (matchedRule.proof_required.includes(field)) {
|
|
1779
|
+
return {
|
|
1780
|
+
field,
|
|
1781
|
+
action: "proof",
|
|
1782
|
+
reason: `Field "${field}" requires cryptographic proof in ${ruleName} context`,
|
|
1783
|
+
applicable_rule: ruleName
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
if (matchedRule.disclose.includes(field)) {
|
|
1787
|
+
return {
|
|
1788
|
+
field,
|
|
1789
|
+
action: "disclose",
|
|
1790
|
+
reason: `Field "${field}" is permitted for disclosure in ${ruleName} context`,
|
|
1791
|
+
applicable_rule: ruleName
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
return {
|
|
1795
|
+
field,
|
|
1796
|
+
action: policy.default_action,
|
|
1797
|
+
reason: `Field "${field}" not addressed in ${ruleName} rule; applying default`,
|
|
1798
|
+
applicable_rule: ruleName
|
|
1799
|
+
};
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
var PolicyStore = class {
|
|
1803
|
+
storage;
|
|
1804
|
+
encryptionKey;
|
|
1805
|
+
policies = /* @__PURE__ */ new Map();
|
|
1806
|
+
constructor(storage, masterKey) {
|
|
1807
|
+
this.storage = storage;
|
|
1808
|
+
this.encryptionKey = derivePurposeKey(masterKey, "l3-policies");
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Create and store a new disclosure policy.
|
|
1812
|
+
*/
|
|
1813
|
+
async create(policyName, rules, defaultAction, identityId) {
|
|
1814
|
+
const policyId = `pol-${Date.now()}-${toBase64url(randomBytes(8))}`;
|
|
1815
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1816
|
+
const policy = {
|
|
1817
|
+
policy_id: policyId,
|
|
1818
|
+
policy_name: policyName,
|
|
1819
|
+
rules,
|
|
1820
|
+
default_action: defaultAction,
|
|
1821
|
+
identity_id: identityId,
|
|
1822
|
+
created_at: now,
|
|
1823
|
+
updated_at: now
|
|
1824
|
+
};
|
|
1825
|
+
await this.persist(policy);
|
|
1826
|
+
this.policies.set(policyId, policy);
|
|
1827
|
+
return policy;
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Get a policy by ID.
|
|
1831
|
+
*/
|
|
1832
|
+
async get(policyId) {
|
|
1833
|
+
if (this.policies.has(policyId)) {
|
|
1834
|
+
return this.policies.get(policyId);
|
|
1835
|
+
}
|
|
1836
|
+
const raw = await this.storage.read("_policies", policyId);
|
|
1837
|
+
if (!raw) return null;
|
|
1838
|
+
try {
|
|
1839
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
1840
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
1841
|
+
const policy = JSON.parse(bytesToString(decrypted));
|
|
1842
|
+
this.policies.set(policyId, policy);
|
|
1843
|
+
return policy;
|
|
1844
|
+
} catch {
|
|
1845
|
+
return null;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
/**
|
|
1849
|
+
* List all policies.
|
|
1850
|
+
*/
|
|
1851
|
+
async list() {
|
|
1852
|
+
await this.loadAll();
|
|
1853
|
+
return Array.from(this.policies.values());
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* Load all persisted policies into memory.
|
|
1857
|
+
*/
|
|
1858
|
+
async loadAll() {
|
|
1859
|
+
try {
|
|
1860
|
+
const entries = await this.storage.list("_policies");
|
|
1861
|
+
for (const meta of entries) {
|
|
1862
|
+
if (this.policies.has(meta.key)) continue;
|
|
1863
|
+
const raw = await this.storage.read("_policies", meta.key);
|
|
1864
|
+
if (!raw) continue;
|
|
1865
|
+
try {
|
|
1866
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
1867
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
1868
|
+
const policy = JSON.parse(bytesToString(decrypted));
|
|
1869
|
+
this.policies.set(policy.policy_id, policy);
|
|
1870
|
+
} catch {
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
} catch {
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
async persist(policy) {
|
|
1877
|
+
const serialized = stringToBytes(JSON.stringify(policy));
|
|
1878
|
+
const encrypted = encrypt(serialized, this.encryptionKey);
|
|
1879
|
+
await this.storage.write(
|
|
1880
|
+
"_policies",
|
|
1881
|
+
policy.policy_id,
|
|
1882
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
};
|
|
1886
|
+
|
|
1887
|
+
// src/l3-disclosure/tools.ts
|
|
1888
|
+
function createL3Tools(storage, masterKey, auditLog) {
|
|
1889
|
+
const commitmentStore = new CommitmentStore(storage, masterKey);
|
|
1890
|
+
const policyStore = new PolicyStore(storage, masterKey);
|
|
1891
|
+
const tools = [
|
|
1892
|
+
// ─── Commitment Schemes ───────────────────────────────────────────────
|
|
1893
|
+
{
|
|
1894
|
+
name: "sanctuary/proof_commitment",
|
|
1895
|
+
description: "Create a cryptographic commitment to a value. The commitment hides the value until you choose to reveal it. Returns the commitment hash and a blinding factor (store securely).",
|
|
1896
|
+
inputSchema: {
|
|
1897
|
+
type: "object",
|
|
1898
|
+
properties: {
|
|
1899
|
+
value: {
|
|
1900
|
+
type: "string",
|
|
1901
|
+
description: "The value to commit to"
|
|
1902
|
+
},
|
|
1903
|
+
blinding_factor: {
|
|
1904
|
+
type: "string",
|
|
1905
|
+
description: "Optional base64url blinding factor (auto-generated if omitted)"
|
|
1906
|
+
}
|
|
1907
|
+
},
|
|
1908
|
+
required: ["value"]
|
|
1909
|
+
},
|
|
1910
|
+
handler: async (args) => {
|
|
1911
|
+
const value = args.value;
|
|
1912
|
+
const blindingFactor = args.blinding_factor;
|
|
1913
|
+
const commitment = createCommitment(value, blindingFactor);
|
|
1914
|
+
const commitmentId = await commitmentStore.store(commitment, value);
|
|
1915
|
+
auditLog.append("l3", "proof_commitment", "system", {
|
|
1916
|
+
commitment_id: commitmentId,
|
|
1917
|
+
commitment_hash: commitment.commitment
|
|
1918
|
+
});
|
|
1919
|
+
return toolResult({
|
|
1920
|
+
commitment_id: commitmentId,
|
|
1921
|
+
commitment: commitment.commitment,
|
|
1922
|
+
blinding_factor: commitment.blinding_factor,
|
|
1923
|
+
committed_at: commitment.committed_at,
|
|
1924
|
+
note: "Store the blinding_factor securely. You will need it to reveal the committed value."
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
},
|
|
1928
|
+
{
|
|
1929
|
+
name: "sanctuary/proof_reveal",
|
|
1930
|
+
description: "Verify a previously committed value by revealing it with the blinding factor. Returns whether the revealed value matches the commitment.",
|
|
1931
|
+
inputSchema: {
|
|
1932
|
+
type: "object",
|
|
1933
|
+
properties: {
|
|
1934
|
+
commitment: {
|
|
1935
|
+
type: "string",
|
|
1936
|
+
description: "The original commitment hash"
|
|
1937
|
+
},
|
|
1938
|
+
value: {
|
|
1939
|
+
type: "string",
|
|
1940
|
+
description: "The value being revealed"
|
|
1941
|
+
},
|
|
1942
|
+
blinding_factor: {
|
|
1943
|
+
type: "string",
|
|
1944
|
+
description: "The blinding factor from the original commitment"
|
|
1945
|
+
}
|
|
1946
|
+
},
|
|
1947
|
+
required: ["commitment", "value", "blinding_factor"]
|
|
1948
|
+
},
|
|
1949
|
+
handler: async (args) => {
|
|
1950
|
+
const commitment = args.commitment;
|
|
1951
|
+
const value = args.value;
|
|
1952
|
+
const blindingFactor = args.blinding_factor;
|
|
1953
|
+
const valid = verifyCommitment(commitment, value, blindingFactor);
|
|
1954
|
+
auditLog.append("l3", "proof_reveal", "system", {
|
|
1955
|
+
commitment_hash: commitment,
|
|
1956
|
+
valid
|
|
1957
|
+
});
|
|
1958
|
+
return toolResult({
|
|
1959
|
+
valid,
|
|
1960
|
+
commitment,
|
|
1961
|
+
revealed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
},
|
|
1965
|
+
// ─── Disclosure Policies ──────────────────────────────────────────────
|
|
1966
|
+
{
|
|
1967
|
+
name: "sanctuary/disclosure_set_policy",
|
|
1968
|
+
description: "Define a disclosure policy that controls what an agent will and will not disclose in different interaction contexts. Rules specify which fields may be disclosed, which must be withheld, and which require cryptographic proof.",
|
|
1969
|
+
inputSchema: {
|
|
1970
|
+
type: "object",
|
|
1971
|
+
properties: {
|
|
1972
|
+
policy_name: {
|
|
1973
|
+
type: "string",
|
|
1974
|
+
description: "Human-readable policy name"
|
|
1975
|
+
},
|
|
1976
|
+
rules: {
|
|
1977
|
+
type: "array",
|
|
1978
|
+
description: "Disclosure rules for different contexts",
|
|
1979
|
+
items: {
|
|
1980
|
+
type: "object",
|
|
1981
|
+
properties: {
|
|
1982
|
+
context: {
|
|
1983
|
+
type: "string",
|
|
1984
|
+
description: 'Interaction context: "negotiation", "commerce", "identity", "*" (wildcard)'
|
|
1985
|
+
},
|
|
1986
|
+
disclose: {
|
|
1987
|
+
type: "array",
|
|
1988
|
+
items: { type: "string" },
|
|
1989
|
+
description: "Fields the agent MAY disclose"
|
|
1990
|
+
},
|
|
1991
|
+
withhold: {
|
|
1992
|
+
type: "array",
|
|
1993
|
+
items: { type: "string" },
|
|
1994
|
+
description: "Fields the agent MUST NOT disclose"
|
|
1995
|
+
},
|
|
1996
|
+
proof_required: {
|
|
1997
|
+
type: "array",
|
|
1998
|
+
items: { type: "string" },
|
|
1999
|
+
description: "Fields that require proof rather than plain disclosure"
|
|
2000
|
+
}
|
|
2001
|
+
},
|
|
2002
|
+
required: ["context", "disclose", "withhold", "proof_required"]
|
|
2003
|
+
}
|
|
2004
|
+
},
|
|
2005
|
+
default_action: {
|
|
2006
|
+
type: "string",
|
|
2007
|
+
enum: ["withhold", "ask-principal"],
|
|
2008
|
+
description: "What to do when no rule matches a field"
|
|
2009
|
+
},
|
|
2010
|
+
identity_id: {
|
|
2011
|
+
type: "string",
|
|
2012
|
+
description: "Optional identity this policy is bound to"
|
|
2013
|
+
}
|
|
2014
|
+
},
|
|
2015
|
+
required: ["policy_name", "rules", "default_action"]
|
|
2016
|
+
},
|
|
2017
|
+
handler: async (args) => {
|
|
2018
|
+
const policyName = args.policy_name;
|
|
2019
|
+
const rules = args.rules;
|
|
2020
|
+
const defaultAction = args.default_action;
|
|
2021
|
+
const identityId = args.identity_id;
|
|
2022
|
+
const policy = await policyStore.create(
|
|
2023
|
+
policyName,
|
|
2024
|
+
rules,
|
|
2025
|
+
defaultAction,
|
|
2026
|
+
identityId
|
|
2027
|
+
);
|
|
2028
|
+
auditLog.append("l3", "disclosure_set_policy", identityId ?? "system", {
|
|
2029
|
+
policy_id: policy.policy_id,
|
|
2030
|
+
policy_name: policyName,
|
|
2031
|
+
rules_count: rules.length
|
|
2032
|
+
});
|
|
2033
|
+
return toolResult({
|
|
2034
|
+
policy_id: policy.policy_id,
|
|
2035
|
+
policy_name: policy.policy_name,
|
|
2036
|
+
rules_count: policy.rules.length,
|
|
2037
|
+
created_at: policy.created_at
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
},
|
|
2041
|
+
{
|
|
2042
|
+
name: "sanctuary/disclosure_evaluate",
|
|
2043
|
+
description: "Evaluate a disclosure request against an active policy. Returns per-field decisions: disclose, withhold, proof, or ask-principal.",
|
|
2044
|
+
inputSchema: {
|
|
2045
|
+
type: "object",
|
|
2046
|
+
properties: {
|
|
2047
|
+
context: {
|
|
2048
|
+
type: "string",
|
|
2049
|
+
description: "The interaction context"
|
|
2050
|
+
},
|
|
2051
|
+
requested_fields: {
|
|
2052
|
+
type: "array",
|
|
2053
|
+
items: { type: "string" },
|
|
2054
|
+
description: "Fields the counterparty is requesting"
|
|
2055
|
+
},
|
|
2056
|
+
policy_id: {
|
|
2057
|
+
type: "string",
|
|
2058
|
+
description: "Specific policy to evaluate (uses first available if omitted)"
|
|
2059
|
+
}
|
|
2060
|
+
},
|
|
2061
|
+
required: ["context", "requested_fields"]
|
|
2062
|
+
},
|
|
2063
|
+
handler: async (args) => {
|
|
2064
|
+
const context = args.context;
|
|
2065
|
+
const requestedFields = args.requested_fields;
|
|
2066
|
+
const policyId = args.policy_id;
|
|
2067
|
+
let policy;
|
|
2068
|
+
if (policyId) {
|
|
2069
|
+
policy = await policyStore.get(policyId);
|
|
2070
|
+
} else {
|
|
2071
|
+
const allPolicies = await policyStore.list();
|
|
2072
|
+
policy = allPolicies[0] ?? null;
|
|
2073
|
+
}
|
|
2074
|
+
if (!policy) {
|
|
2075
|
+
return toolResult({
|
|
2076
|
+
error: "No disclosure policy found. Create one with disclosure_set_policy first."
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
const decisions = evaluateDisclosure(policy, context, requestedFields);
|
|
2080
|
+
const withholding = decisions.filter(
|
|
2081
|
+
(d) => d.action === "withhold"
|
|
2082
|
+
).length;
|
|
2083
|
+
const disclosing = decisions.filter(
|
|
2084
|
+
(d) => d.action === "disclose"
|
|
2085
|
+
).length;
|
|
2086
|
+
const proofRequired = decisions.filter(
|
|
2087
|
+
(d) => d.action === "proof"
|
|
2088
|
+
).length;
|
|
2089
|
+
const askPrincipal = decisions.filter(
|
|
2090
|
+
(d) => d.action === "ask-principal"
|
|
2091
|
+
).length;
|
|
2092
|
+
auditLog.append("l3", "disclosure_evaluate", "system", {
|
|
2093
|
+
policy_id: policy.policy_id,
|
|
2094
|
+
context,
|
|
2095
|
+
fields_requested: requestedFields.length,
|
|
2096
|
+
withholding,
|
|
2097
|
+
disclosing,
|
|
2098
|
+
proof_required: proofRequired
|
|
2099
|
+
});
|
|
2100
|
+
return toolResult({
|
|
2101
|
+
policy_id: policy.policy_id,
|
|
2102
|
+
policy_name: policy.policy_name,
|
|
2103
|
+
context,
|
|
2104
|
+
decisions,
|
|
2105
|
+
summary: {
|
|
2106
|
+
total_fields: requestedFields.length,
|
|
2107
|
+
disclose: disclosing,
|
|
2108
|
+
withhold: withholding,
|
|
2109
|
+
proof: proofRequired,
|
|
2110
|
+
ask_principal: askPrincipal
|
|
2111
|
+
},
|
|
2112
|
+
overall_recommendation: withholding > 0 ? `Withholding ${withholding} of ${requestedFields.length} requested fields per policy "${policy.policy_name}"` : `All ${requestedFields.length} fields may be disclosed per policy "${policy.policy_name}"`
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
];
|
|
2117
|
+
return { tools, commitmentStore, policyStore };
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
// src/l4-reputation/reputation-store.ts
|
|
2121
|
+
init_encoding();
|
|
2122
|
+
function computeMedian(values) {
|
|
2123
|
+
if (values.length === 0) return 0;
|
|
2124
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
2125
|
+
const mid = Math.floor(sorted.length / 2);
|
|
2126
|
+
return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
2127
|
+
}
|
|
2128
|
+
function aggregateMetrics(attestations, metricNames) {
|
|
2129
|
+
const result = {};
|
|
2130
|
+
const names = metricNames ?? Array.from(
|
|
2131
|
+
new Set(
|
|
2132
|
+
attestations.flatMap(
|
|
2133
|
+
(a) => Object.keys(a.attestation.data.metrics)
|
|
2134
|
+
)
|
|
2135
|
+
)
|
|
2136
|
+
);
|
|
2137
|
+
for (const name of names) {
|
|
2138
|
+
const values = attestations.map((a) => a.attestation.data.metrics[name]).filter((v) => v !== void 0);
|
|
2139
|
+
if (values.length === 0) {
|
|
2140
|
+
result[name] = { mean: 0, median: 0, min: 0, max: 0, count: 0 };
|
|
2141
|
+
continue;
|
|
2142
|
+
}
|
|
2143
|
+
result[name] = {
|
|
2144
|
+
mean: values.reduce((s, v) => s + v, 0) / values.length,
|
|
2145
|
+
median: computeMedian(values),
|
|
2146
|
+
min: Math.min(...values),
|
|
2147
|
+
max: Math.max(...values),
|
|
2148
|
+
count: values.length
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
return result;
|
|
2152
|
+
}
|
|
2153
|
+
var ReputationStore = class {
|
|
2154
|
+
storage;
|
|
2155
|
+
encryptionKey;
|
|
2156
|
+
constructor(storage, masterKey) {
|
|
2157
|
+
this.storage = storage;
|
|
2158
|
+
this.encryptionKey = derivePurposeKey(masterKey, "l4-reputation");
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Record an interaction outcome as a signed attestation.
|
|
2162
|
+
*/
|
|
2163
|
+
async record(interactionId, counterpartyDid, outcome, context, identity, identityEncryptionKey, counterpartyAttestation) {
|
|
2164
|
+
const attestationId = `att-${Date.now()}-${toBase64url(randomBytes(8))}`;
|
|
2165
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2166
|
+
const attestationData = {
|
|
2167
|
+
interaction_id: interactionId,
|
|
2168
|
+
participant_did: identity.did,
|
|
2169
|
+
counterparty_did: counterpartyDid,
|
|
2170
|
+
outcome_type: outcome.type,
|
|
2171
|
+
outcome_result: outcome.result,
|
|
2172
|
+
metrics: outcome.metrics ?? {},
|
|
2173
|
+
context,
|
|
2174
|
+
timestamp: now
|
|
2175
|
+
};
|
|
2176
|
+
const dataBytes = stringToBytes(JSON.stringify(attestationData));
|
|
2177
|
+
const signature = sign(
|
|
2178
|
+
dataBytes,
|
|
2179
|
+
identity.encrypted_private_key,
|
|
2180
|
+
identityEncryptionKey
|
|
2181
|
+
);
|
|
2182
|
+
const attestation = {
|
|
2183
|
+
attestation_id: attestationId,
|
|
2184
|
+
schema: "sanctuary-interaction-v1",
|
|
2185
|
+
data: attestationData,
|
|
2186
|
+
signature: toBase64url(signature),
|
|
2187
|
+
signer: identity.did
|
|
2188
|
+
};
|
|
2189
|
+
const stored = {
|
|
2190
|
+
attestation,
|
|
2191
|
+
counterparty_attestation: counterpartyAttestation,
|
|
2192
|
+
counterparty_confirmed: !!counterpartyAttestation,
|
|
2193
|
+
recorded_at: now
|
|
2194
|
+
};
|
|
2195
|
+
const serialized = stringToBytes(JSON.stringify(stored));
|
|
2196
|
+
const encrypted = encrypt(serialized, this.encryptionKey);
|
|
2197
|
+
await this.storage.write(
|
|
2198
|
+
"_reputation",
|
|
2199
|
+
attestationId,
|
|
2200
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
2201
|
+
);
|
|
2202
|
+
return stored;
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
2205
|
+
* Query reputation data with filtering.
|
|
2206
|
+
* Returns aggregates only — not raw interaction data.
|
|
2207
|
+
*/
|
|
2208
|
+
async query(options) {
|
|
2209
|
+
const all = await this.loadAll();
|
|
2210
|
+
let filtered = all;
|
|
2211
|
+
if (options.context) {
|
|
2212
|
+
filtered = filtered.filter(
|
|
2213
|
+
(a) => a.attestation.data.context === options.context
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
if (options.time_range) {
|
|
2217
|
+
const start2 = new Date(options.time_range.start).getTime();
|
|
2218
|
+
const end2 = new Date(options.time_range.end).getTime();
|
|
2219
|
+
filtered = filtered.filter((a) => {
|
|
2220
|
+
const t = new Date(a.attestation.data.timestamp).getTime();
|
|
2221
|
+
return t >= start2 && t <= end2;
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
if (options.counterparty_did) {
|
|
2225
|
+
filtered = filtered.filter(
|
|
2226
|
+
(a) => a.attestation.data.counterparty_did === options.counterparty_did
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
2229
|
+
const contexts = Array.from(
|
|
2230
|
+
new Set(filtered.map((a) => a.attestation.data.context))
|
|
2231
|
+
);
|
|
2232
|
+
const timestamps = filtered.map(
|
|
2233
|
+
(a) => new Date(a.attestation.data.timestamp).getTime()
|
|
2234
|
+
);
|
|
2235
|
+
const start = timestamps.length > 0 ? new Date(Math.min(...timestamps)).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
2236
|
+
const end = timestamps.length > 0 ? new Date(Math.max(...timestamps)).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
2237
|
+
return {
|
|
2238
|
+
total_interactions: filtered.length,
|
|
2239
|
+
completed: filtered.filter(
|
|
2240
|
+
(a) => a.attestation.data.outcome_result === "completed"
|
|
2241
|
+
).length,
|
|
2242
|
+
partial: filtered.filter(
|
|
2243
|
+
(a) => a.attestation.data.outcome_result === "partial"
|
|
2244
|
+
).length,
|
|
2245
|
+
failed: filtered.filter(
|
|
2246
|
+
(a) => a.attestation.data.outcome_result === "failed"
|
|
2247
|
+
).length,
|
|
2248
|
+
disputed: filtered.filter(
|
|
2249
|
+
(a) => a.attestation.data.outcome_result === "disputed"
|
|
2250
|
+
).length,
|
|
2251
|
+
contexts,
|
|
2252
|
+
time_range: { start, end },
|
|
2253
|
+
aggregate_metrics: aggregateMetrics(filtered, options.metrics)
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
/**
|
|
2257
|
+
* Export attestations as a portable reputation bundle.
|
|
2258
|
+
*/
|
|
2259
|
+
async exportBundle(identity, identityEncryptionKey, context) {
|
|
2260
|
+
let all = await this.loadAll();
|
|
2261
|
+
if (context) {
|
|
2262
|
+
all = all.filter((a) => a.attestation.data.context === context);
|
|
2263
|
+
}
|
|
2264
|
+
const attestations = all.map((a) => a.attestation);
|
|
2265
|
+
const bundleData = {
|
|
2266
|
+
version: "SANCTUARY_REP_V1",
|
|
2267
|
+
attestations,
|
|
2268
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2269
|
+
exporter_did: identity.did
|
|
2270
|
+
};
|
|
2271
|
+
const bundleBytes = stringToBytes(JSON.stringify(bundleData));
|
|
2272
|
+
const bundleSignature = sign(
|
|
2273
|
+
bundleBytes,
|
|
2274
|
+
identity.encrypted_private_key,
|
|
2275
|
+
identityEncryptionKey
|
|
2276
|
+
);
|
|
2277
|
+
return {
|
|
2278
|
+
...bundleData,
|
|
2279
|
+
bundle_signature: toBase64url(bundleSignature)
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* Import attestations from a reputation bundle.
|
|
2284
|
+
* Verifies signatures if requested (default: true).
|
|
2285
|
+
*
|
|
2286
|
+
* @param publicKeys - Map of DID → public key bytes for signature verification
|
|
2287
|
+
*/
|
|
2288
|
+
async importBundle(bundle, verifySignatures, publicKeys) {
|
|
2289
|
+
let imported = 0;
|
|
2290
|
+
let invalid = 0;
|
|
2291
|
+
const contexts = /* @__PURE__ */ new Set();
|
|
2292
|
+
for (const attestation of bundle.attestations) {
|
|
2293
|
+
if (verifySignatures) {
|
|
2294
|
+
const signerKey = publicKeys.get(attestation.signer);
|
|
2295
|
+
if (!signerKey) {
|
|
2296
|
+
invalid++;
|
|
2297
|
+
continue;
|
|
2298
|
+
}
|
|
2299
|
+
const dataBytes = stringToBytes(
|
|
2300
|
+
JSON.stringify(attestation.data)
|
|
2301
|
+
);
|
|
2302
|
+
const sigBytes = fromBase64url(attestation.signature);
|
|
2303
|
+
if (!verify(dataBytes, sigBytes, signerKey)) {
|
|
2304
|
+
invalid++;
|
|
2305
|
+
continue;
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
const stored = {
|
|
2309
|
+
attestation,
|
|
2310
|
+
counterparty_confirmed: false,
|
|
2311
|
+
recorded_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2312
|
+
};
|
|
2313
|
+
const serialized = stringToBytes(JSON.stringify(stored));
|
|
2314
|
+
const encrypted = encrypt(serialized, this.encryptionKey);
|
|
2315
|
+
await this.storage.write(
|
|
2316
|
+
"_reputation",
|
|
2317
|
+
attestation.attestation_id,
|
|
2318
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
2319
|
+
);
|
|
2320
|
+
imported++;
|
|
2321
|
+
contexts.add(attestation.data.context);
|
|
2322
|
+
}
|
|
2323
|
+
return {
|
|
2324
|
+
imported,
|
|
2325
|
+
invalid,
|
|
2326
|
+
contexts: Array.from(contexts)
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
// ─── Escrow ───────────────────────────────────────────────────────────
|
|
2330
|
+
/**
|
|
2331
|
+
* Create an escrow for trust bootstrapping.
|
|
2332
|
+
*/
|
|
2333
|
+
async createEscrow(transactionTerms, counterpartyDid, timeoutSeconds, creatorDid, collateralAmount) {
|
|
2334
|
+
const escrowId = `esc-${Date.now()}-${toBase64url(randomBytes(8))}`;
|
|
2335
|
+
const now = /* @__PURE__ */ new Date();
|
|
2336
|
+
const expiresAt = new Date(now.getTime() + timeoutSeconds * 1e3);
|
|
2337
|
+
const { hashToString: hashToString2 } = await Promise.resolve().then(() => (init_hashing(), hashing_exports));
|
|
2338
|
+
const termsHash = hashToString2(stringToBytes(transactionTerms));
|
|
2339
|
+
const escrow = {
|
|
2340
|
+
escrow_id: escrowId,
|
|
2341
|
+
transaction_terms: transactionTerms,
|
|
2342
|
+
terms_hash: termsHash,
|
|
2343
|
+
collateral_amount: collateralAmount,
|
|
2344
|
+
counterparty_did: counterpartyDid,
|
|
2345
|
+
creator_did: creatorDid,
|
|
2346
|
+
created_at: now.toISOString(),
|
|
2347
|
+
expires_at: expiresAt.toISOString(),
|
|
2348
|
+
status: "pending"
|
|
2349
|
+
};
|
|
2350
|
+
const serialized = stringToBytes(JSON.stringify(escrow));
|
|
2351
|
+
const encrypted = encrypt(serialized, this.encryptionKey);
|
|
2352
|
+
await this.storage.write(
|
|
2353
|
+
"_escrows",
|
|
2354
|
+
escrowId,
|
|
2355
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
2356
|
+
);
|
|
2357
|
+
return escrow;
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Get an escrow by ID.
|
|
2361
|
+
*/
|
|
2362
|
+
async getEscrow(escrowId) {
|
|
2363
|
+
const raw = await this.storage.read("_escrows", escrowId);
|
|
2364
|
+
if (!raw) return null;
|
|
2365
|
+
try {
|
|
2366
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
2367
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
2368
|
+
return JSON.parse(bytesToString(decrypted));
|
|
2369
|
+
} catch {
|
|
2370
|
+
return null;
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
// ─── Guarantees ─────────────────────────────────────────────────────
|
|
2374
|
+
/**
|
|
2375
|
+
* Create a principal's guarantee for a new agent.
|
|
2376
|
+
*/
|
|
2377
|
+
async createGuarantee(principalIdentity, agentDid, scope, durationSeconds, identityEncryptionKey, maxLiability) {
|
|
2378
|
+
const guaranteeId = `guar-${Date.now()}-${toBase64url(randomBytes(8))}`;
|
|
2379
|
+
const now = /* @__PURE__ */ new Date();
|
|
2380
|
+
const validUntil = new Date(now.getTime() + durationSeconds * 1e3);
|
|
2381
|
+
const certificateData = {
|
|
2382
|
+
guarantee_id: guaranteeId,
|
|
2383
|
+
principal_did: principalIdentity.did,
|
|
2384
|
+
agent_did: agentDid,
|
|
2385
|
+
scope,
|
|
2386
|
+
max_liability: maxLiability,
|
|
2387
|
+
valid_until: validUntil.toISOString(),
|
|
2388
|
+
issued_at: now.toISOString()
|
|
2389
|
+
};
|
|
2390
|
+
const certBytes = stringToBytes(JSON.stringify(certificateData));
|
|
2391
|
+
const signature = sign(
|
|
2392
|
+
certBytes,
|
|
2393
|
+
principalIdentity.encrypted_private_key,
|
|
2394
|
+
identityEncryptionKey
|
|
2395
|
+
);
|
|
2396
|
+
const certificate = toBase64url(
|
|
2397
|
+
stringToBytes(
|
|
2398
|
+
JSON.stringify({
|
|
2399
|
+
...certificateData,
|
|
2400
|
+
signature: toBase64url(signature)
|
|
2401
|
+
})
|
|
2402
|
+
)
|
|
2403
|
+
);
|
|
2404
|
+
const guarantee = {
|
|
2405
|
+
guarantee_id: guaranteeId,
|
|
2406
|
+
principal_did: principalIdentity.did,
|
|
2407
|
+
agent_did: agentDid,
|
|
2408
|
+
scope,
|
|
2409
|
+
max_liability: maxLiability,
|
|
2410
|
+
valid_until: validUntil.toISOString(),
|
|
2411
|
+
certificate,
|
|
2412
|
+
created_at: now.toISOString()
|
|
2413
|
+
};
|
|
2414
|
+
const serialized = stringToBytes(JSON.stringify(guarantee));
|
|
2415
|
+
const encrypted = encrypt(serialized, this.encryptionKey);
|
|
2416
|
+
await this.storage.write(
|
|
2417
|
+
"_guarantees",
|
|
2418
|
+
guaranteeId,
|
|
2419
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
2420
|
+
);
|
|
2421
|
+
return guarantee;
|
|
2422
|
+
}
|
|
2423
|
+
// ─── Internal ─────────────────────────────────────────────────────────
|
|
2424
|
+
async loadAll() {
|
|
2425
|
+
const results = [];
|
|
2426
|
+
try {
|
|
2427
|
+
const entries = await this.storage.list("_reputation");
|
|
2428
|
+
for (const meta of entries) {
|
|
2429
|
+
const raw = await this.storage.read("_reputation", meta.key);
|
|
2430
|
+
if (!raw) continue;
|
|
2431
|
+
try {
|
|
2432
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
2433
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
2434
|
+
results.push(JSON.parse(bytesToString(decrypted)));
|
|
2435
|
+
} catch {
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
} catch {
|
|
2439
|
+
}
|
|
2440
|
+
return results;
|
|
2441
|
+
}
|
|
2442
|
+
};
|
|
2443
|
+
|
|
2444
|
+
// src/l4-reputation/tools.ts
|
|
2445
|
+
init_encoding();
|
|
2446
|
+
function createL4Tools(storage, masterKey, identityManager, auditLog) {
|
|
2447
|
+
const reputationStore = new ReputationStore(storage, masterKey);
|
|
2448
|
+
const identityEncryptionKey = derivePurposeKey(masterKey, "identity-encryption");
|
|
2449
|
+
const tools = [
|
|
2450
|
+
// ─── Reputation Recording ─────────────────────────────────────────
|
|
2451
|
+
{
|
|
2452
|
+
name: "sanctuary/reputation_record",
|
|
2453
|
+
description: "Record an interaction outcome as a signed attestation. Creates an EAS-compatible attestation signed by the specified identity.",
|
|
2454
|
+
inputSchema: {
|
|
2455
|
+
type: "object",
|
|
2456
|
+
properties: {
|
|
2457
|
+
interaction_id: {
|
|
2458
|
+
type: "string",
|
|
2459
|
+
description: "Unique interaction identifier"
|
|
2460
|
+
},
|
|
2461
|
+
counterparty_did: {
|
|
2462
|
+
type: "string",
|
|
2463
|
+
description: "Counterparty's DID"
|
|
2464
|
+
},
|
|
2465
|
+
outcome: {
|
|
2466
|
+
type: "object",
|
|
2467
|
+
description: "Interaction outcome",
|
|
2468
|
+
properties: {
|
|
2469
|
+
type: {
|
|
2470
|
+
type: "string",
|
|
2471
|
+
enum: ["transaction", "negotiation", "service", "dispute", "custom"]
|
|
2472
|
+
},
|
|
2473
|
+
result: {
|
|
2474
|
+
type: "string",
|
|
2475
|
+
enum: ["completed", "partial", "failed", "disputed"]
|
|
2476
|
+
},
|
|
2477
|
+
metrics: {
|
|
2478
|
+
type: "object",
|
|
2479
|
+
description: "Domain-specific metrics (e.g., fulfillment_rate, response_time_ms)"
|
|
2480
|
+
}
|
|
2481
|
+
},
|
|
2482
|
+
required: ["type", "result"]
|
|
2483
|
+
},
|
|
2484
|
+
context: {
|
|
2485
|
+
type: "string",
|
|
2486
|
+
description: "Category/domain for context-specific reputation",
|
|
2487
|
+
default: "general"
|
|
2488
|
+
},
|
|
2489
|
+
counterparty_attestation: {
|
|
2490
|
+
type: "string",
|
|
2491
|
+
description: "Counterparty's signed attestation of the same interaction"
|
|
2492
|
+
},
|
|
2493
|
+
identity_id: {
|
|
2494
|
+
type: "string",
|
|
2495
|
+
description: "Identity to sign with (uses default if omitted)"
|
|
2496
|
+
}
|
|
2497
|
+
},
|
|
2498
|
+
required: ["interaction_id", "counterparty_did", "outcome"]
|
|
2499
|
+
},
|
|
2500
|
+
handler: async (args) => {
|
|
2501
|
+
const identityId = args.identity_id;
|
|
2502
|
+
const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
|
|
2503
|
+
if (!identity) {
|
|
2504
|
+
return toolResult({
|
|
2505
|
+
error: "No identity found. Create one with identity_create first."
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
const outcome = args.outcome;
|
|
2509
|
+
const context = args.context ?? "general";
|
|
2510
|
+
const stored = await reputationStore.record(
|
|
2511
|
+
args.interaction_id,
|
|
2512
|
+
args.counterparty_did,
|
|
2513
|
+
outcome,
|
|
2514
|
+
context,
|
|
2515
|
+
identity,
|
|
2516
|
+
identityEncryptionKey,
|
|
2517
|
+
args.counterparty_attestation
|
|
2518
|
+
);
|
|
2519
|
+
auditLog.append("l4", "reputation_record", identity.identity_id, {
|
|
2520
|
+
interaction_id: args.interaction_id,
|
|
2521
|
+
outcome_type: outcome.type,
|
|
2522
|
+
outcome_result: outcome.result,
|
|
2523
|
+
context
|
|
2524
|
+
});
|
|
2525
|
+
return toolResult({
|
|
2526
|
+
attestation_id: stored.attestation.attestation_id,
|
|
2527
|
+
interaction_id: stored.attestation.data.interaction_id,
|
|
2528
|
+
self_attestation: stored.attestation.signature,
|
|
2529
|
+
counterparty_confirmed: stored.counterparty_confirmed,
|
|
2530
|
+
context,
|
|
2531
|
+
recorded_at: stored.recorded_at
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
},
|
|
2535
|
+
// ─── Reputation Query ─────────────────────────────────────────────
|
|
2536
|
+
{
|
|
2537
|
+
name: "sanctuary/reputation_query",
|
|
2538
|
+
description: "Query aggregated reputation data with filtering. Returns summary statistics, never raw interaction details.",
|
|
2539
|
+
inputSchema: {
|
|
2540
|
+
type: "object",
|
|
2541
|
+
properties: {
|
|
2542
|
+
context: {
|
|
2543
|
+
type: "string",
|
|
2544
|
+
description: "Filter by context/domain"
|
|
2545
|
+
},
|
|
2546
|
+
time_range: {
|
|
2547
|
+
type: "object",
|
|
2548
|
+
description: "Filter by time range",
|
|
2549
|
+
properties: {
|
|
2550
|
+
start: { type: "string", description: "ISO 8601 start" },
|
|
2551
|
+
end: { type: "string", description: "ISO 8601 end" }
|
|
2552
|
+
}
|
|
2553
|
+
},
|
|
2554
|
+
metrics: {
|
|
2555
|
+
type: "array",
|
|
2556
|
+
items: { type: "string" },
|
|
2557
|
+
description: "Which metrics to aggregate"
|
|
2558
|
+
},
|
|
2559
|
+
counterparty_did: {
|
|
2560
|
+
type: "string",
|
|
2561
|
+
description: "Filter by counterparty"
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
},
|
|
2565
|
+
handler: async (args) => {
|
|
2566
|
+
const summary = await reputationStore.query({
|
|
2567
|
+
context: args.context,
|
|
2568
|
+
time_range: args.time_range,
|
|
2569
|
+
metrics: args.metrics,
|
|
2570
|
+
counterparty_did: args.counterparty_did
|
|
2571
|
+
});
|
|
2572
|
+
auditLog.append("l4", "reputation_query", "system", {
|
|
2573
|
+
total_interactions: summary.total_interactions,
|
|
2574
|
+
contexts: summary.contexts
|
|
2575
|
+
});
|
|
2576
|
+
return toolResult({
|
|
2577
|
+
summary
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
},
|
|
2581
|
+
// ─── Reputation Export ─────────────────────────────────────────────
|
|
2582
|
+
{
|
|
2583
|
+
name: "sanctuary/reputation_export",
|
|
2584
|
+
description: "Export a portable reputation bundle (SANCTUARY_REP_V1). Includes all signed attestations for independent verification.",
|
|
2585
|
+
inputSchema: {
|
|
2586
|
+
type: "object",
|
|
2587
|
+
properties: {
|
|
2588
|
+
format: {
|
|
2589
|
+
type: "string",
|
|
2590
|
+
enum: ["SANCTUARY_REP_V1"],
|
|
2591
|
+
default: "SANCTUARY_REP_V1"
|
|
2592
|
+
},
|
|
2593
|
+
context: {
|
|
2594
|
+
type: "string",
|
|
2595
|
+
description: "Export specific context only"
|
|
2596
|
+
},
|
|
2597
|
+
identity_id: {
|
|
2598
|
+
type: "string",
|
|
2599
|
+
description: "Identity to sign the bundle with"
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
},
|
|
2603
|
+
handler: async (args) => {
|
|
2604
|
+
const identityId = args.identity_id;
|
|
2605
|
+
const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
|
|
2606
|
+
if (!identity) {
|
|
2607
|
+
return toolResult({
|
|
2608
|
+
error: "No identity found. Create one with identity_create first."
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
const context = args.context;
|
|
2612
|
+
const bundle = await reputationStore.exportBundle(
|
|
2613
|
+
identity,
|
|
2614
|
+
identityEncryptionKey,
|
|
2615
|
+
context
|
|
2616
|
+
);
|
|
2617
|
+
const bundleJson = JSON.stringify(bundle);
|
|
2618
|
+
const bundleBase64 = toBase64url(
|
|
2619
|
+
new TextEncoder().encode(bundleJson)
|
|
2620
|
+
);
|
|
2621
|
+
auditLog.append("l4", "reputation_export", identity.identity_id, {
|
|
2622
|
+
attestation_count: bundle.attestations.length,
|
|
2623
|
+
contexts: Array.from(
|
|
2624
|
+
new Set(bundle.attestations.map((a) => a.data.context))
|
|
2625
|
+
)
|
|
2626
|
+
});
|
|
2627
|
+
const { hashToString: hashToString2 } = await Promise.resolve().then(() => (init_hashing(), hashing_exports));
|
|
2628
|
+
const { stringToBytes: stringToBytes2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
|
|
2629
|
+
return toolResult({
|
|
2630
|
+
bundle: bundleBase64,
|
|
2631
|
+
attestation_count: bundle.attestations.length,
|
|
2632
|
+
contexts: Array.from(
|
|
2633
|
+
new Set(bundle.attestations.map((a) => a.data.context))
|
|
2634
|
+
),
|
|
2635
|
+
bundle_hash: hashToString2(stringToBytes2(bundleJson)),
|
|
2636
|
+
exported_at: bundle.exported_at
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
},
|
|
2640
|
+
// ─── Reputation Import ────────────────────────────────────────────
|
|
2641
|
+
{
|
|
2642
|
+
name: "sanctuary/reputation_import",
|
|
2643
|
+
description: "Import a reputation bundle from another Sanctuary instance. Verifies all attestation signatures by default.",
|
|
2644
|
+
inputSchema: {
|
|
2645
|
+
type: "object",
|
|
2646
|
+
properties: {
|
|
2647
|
+
bundle: {
|
|
2648
|
+
type: "string",
|
|
2649
|
+
description: "Base64url-encoded reputation bundle"
|
|
2650
|
+
}
|
|
2651
|
+
},
|
|
2652
|
+
required: ["bundle"]
|
|
2653
|
+
},
|
|
2654
|
+
handler: async (args) => {
|
|
2655
|
+
const bundleBase64 = args.bundle;
|
|
2656
|
+
const verifySignatures = true;
|
|
2657
|
+
let bundle;
|
|
2658
|
+
try {
|
|
2659
|
+
const bundleBytes = fromBase64url(bundleBase64);
|
|
2660
|
+
const bundleJson = new TextDecoder().decode(bundleBytes);
|
|
2661
|
+
bundle = JSON.parse(bundleJson);
|
|
2662
|
+
} catch {
|
|
2663
|
+
return toolResult({
|
|
2664
|
+
error: "Invalid bundle format. Expected base64url-encoded JSON."
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2667
|
+
const publicKeys = /* @__PURE__ */ new Map();
|
|
2668
|
+
for (const pub of identityManager.list()) {
|
|
2669
|
+
const identity = identityManager.get(pub.identity_id);
|
|
2670
|
+
if (identity) {
|
|
2671
|
+
publicKeys.set(identity.did, fromBase64url(identity.public_key));
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
const result = await reputationStore.importBundle(
|
|
2675
|
+
bundle,
|
|
2676
|
+
verifySignatures,
|
|
2677
|
+
publicKeys
|
|
2678
|
+
);
|
|
2679
|
+
auditLog.append("l4", "reputation_import", "system", {
|
|
2680
|
+
imported: result.imported,
|
|
2681
|
+
invalid: result.invalid,
|
|
2682
|
+
contexts: result.contexts
|
|
2683
|
+
});
|
|
2684
|
+
return toolResult({
|
|
2685
|
+
imported_attestations: result.imported,
|
|
2686
|
+
invalid_attestations: result.invalid,
|
|
2687
|
+
contexts: result.contexts,
|
|
2688
|
+
imported_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2689
|
+
});
|
|
2690
|
+
}
|
|
2691
|
+
},
|
|
2692
|
+
// ─── Trust Bootstrap: Escrow ──────────────────────────────────────
|
|
2693
|
+
{
|
|
2694
|
+
name: "sanctuary/bootstrap_create_escrow",
|
|
2695
|
+
description: "Create an escrow record for trust bootstrapping. Allows new participants with no reputation to transact safely.",
|
|
2696
|
+
inputSchema: {
|
|
2697
|
+
type: "object",
|
|
2698
|
+
properties: {
|
|
2699
|
+
transaction_terms: {
|
|
2700
|
+
type: "string",
|
|
2701
|
+
description: "Description of the transaction"
|
|
2702
|
+
},
|
|
2703
|
+
collateral_amount: {
|
|
2704
|
+
type: "number",
|
|
2705
|
+
description: "Optional stake/collateral amount"
|
|
2706
|
+
},
|
|
2707
|
+
counterparty_did: {
|
|
2708
|
+
type: "string",
|
|
2709
|
+
description: "Counterparty's DID"
|
|
2710
|
+
},
|
|
2711
|
+
timeout_seconds: {
|
|
2712
|
+
type: "number",
|
|
2713
|
+
description: "Escrow timeout in seconds"
|
|
2714
|
+
},
|
|
2715
|
+
identity_id: {
|
|
2716
|
+
type: "string",
|
|
2717
|
+
description: "Identity creating the escrow"
|
|
2718
|
+
}
|
|
2719
|
+
},
|
|
2720
|
+
required: ["transaction_terms", "counterparty_did", "timeout_seconds"]
|
|
2721
|
+
},
|
|
2722
|
+
handler: async (args) => {
|
|
2723
|
+
const identityId = args.identity_id;
|
|
2724
|
+
const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
|
|
2725
|
+
if (!identity) {
|
|
2726
|
+
return toolResult({
|
|
2727
|
+
error: "No identity found. Create one with identity_create first."
|
|
2728
|
+
});
|
|
2729
|
+
}
|
|
2730
|
+
const escrow = await reputationStore.createEscrow(
|
|
2731
|
+
args.transaction_terms,
|
|
2732
|
+
args.counterparty_did,
|
|
2733
|
+
args.timeout_seconds,
|
|
2734
|
+
identity.did,
|
|
2735
|
+
args.collateral_amount
|
|
2736
|
+
);
|
|
2737
|
+
auditLog.append("l4", "bootstrap_create_escrow", identity.identity_id, {
|
|
2738
|
+
escrow_id: escrow.escrow_id,
|
|
2739
|
+
counterparty_did: args.counterparty_did,
|
|
2740
|
+
timeout_seconds: args.timeout_seconds
|
|
2741
|
+
});
|
|
2742
|
+
return toolResult({
|
|
2743
|
+
escrow_id: escrow.escrow_id,
|
|
2744
|
+
terms_hash: escrow.terms_hash,
|
|
2745
|
+
created_at: escrow.created_at,
|
|
2746
|
+
expires_at: escrow.expires_at,
|
|
2747
|
+
status: escrow.status
|
|
2748
|
+
});
|
|
2749
|
+
}
|
|
2750
|
+
},
|
|
2751
|
+
// ─── Trust Bootstrap: Guarantee ───────────────────────────────────
|
|
2752
|
+
{
|
|
2753
|
+
name: "sanctuary/bootstrap_provide_guarantee",
|
|
2754
|
+
description: "A principal provides a signed reputation guarantee for a new agent. The guarantee certificate can be presented to counterparties.",
|
|
2755
|
+
inputSchema: {
|
|
2756
|
+
type: "object",
|
|
2757
|
+
properties: {
|
|
2758
|
+
principal_identity_id: {
|
|
2759
|
+
type: "string",
|
|
2760
|
+
description: "Identity of the guarantor (principal)"
|
|
2761
|
+
},
|
|
2762
|
+
agent_identity_id: {
|
|
2763
|
+
type: "string",
|
|
2764
|
+
description: "Identity of the agent being guaranteed"
|
|
2765
|
+
},
|
|
2766
|
+
scope: {
|
|
2767
|
+
type: "string",
|
|
2768
|
+
description: "What the guarantee covers"
|
|
2769
|
+
},
|
|
2770
|
+
duration_seconds: {
|
|
2771
|
+
type: "number",
|
|
2772
|
+
description: "How long the guarantee is valid"
|
|
2773
|
+
},
|
|
2774
|
+
max_liability: {
|
|
2775
|
+
type: "number",
|
|
2776
|
+
description: "Maximum liability amount"
|
|
2777
|
+
}
|
|
2778
|
+
},
|
|
2779
|
+
required: [
|
|
2780
|
+
"principal_identity_id",
|
|
2781
|
+
"agent_identity_id",
|
|
2782
|
+
"scope",
|
|
2783
|
+
"duration_seconds"
|
|
2784
|
+
]
|
|
2785
|
+
},
|
|
2786
|
+
handler: async (args) => {
|
|
2787
|
+
const principalIdentity = identityManager.get(
|
|
2788
|
+
args.principal_identity_id
|
|
2789
|
+
);
|
|
2790
|
+
const agentIdentity = identityManager.get(
|
|
2791
|
+
args.agent_identity_id
|
|
2792
|
+
);
|
|
2793
|
+
if (!principalIdentity) {
|
|
2794
|
+
return toolResult({
|
|
2795
|
+
error: `Principal identity "${args.principal_identity_id}" not found.`
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
2798
|
+
if (!agentIdentity) {
|
|
2799
|
+
return toolResult({
|
|
2800
|
+
error: `Agent identity "${args.agent_identity_id}" not found.`
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
const guarantee = await reputationStore.createGuarantee(
|
|
2804
|
+
principalIdentity,
|
|
2805
|
+
agentIdentity.did,
|
|
2806
|
+
args.scope,
|
|
2807
|
+
args.duration_seconds,
|
|
2808
|
+
identityEncryptionKey,
|
|
2809
|
+
args.max_liability
|
|
2810
|
+
);
|
|
2811
|
+
auditLog.append(
|
|
2812
|
+
"l4",
|
|
2813
|
+
"bootstrap_provide_guarantee",
|
|
2814
|
+
principalIdentity.identity_id,
|
|
2815
|
+
{
|
|
2816
|
+
guarantee_id: guarantee.guarantee_id,
|
|
2817
|
+
agent_did: agentIdentity.did,
|
|
2818
|
+
scope: args.scope
|
|
2819
|
+
}
|
|
2820
|
+
);
|
|
2821
|
+
return toolResult({
|
|
2822
|
+
guarantee_id: guarantee.guarantee_id,
|
|
2823
|
+
guarantee_certificate: guarantee.certificate,
|
|
2824
|
+
scope: guarantee.scope,
|
|
2825
|
+
valid_until: guarantee.valid_until
|
|
2826
|
+
});
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
];
|
|
2830
|
+
return { tools, reputationStore };
|
|
2831
|
+
}
|
|
2832
|
+
var DEFAULT_TIER2 = {
|
|
2833
|
+
new_namespace_access: "approve",
|
|
2834
|
+
new_counterparty: "approve",
|
|
2835
|
+
frequency_spike_multiplier: 5,
|
|
2836
|
+
max_signs_per_minute: 10,
|
|
2837
|
+
bulk_read_threshold: 20,
|
|
2838
|
+
first_session_policy: "approve"
|
|
2839
|
+
};
|
|
2840
|
+
var DEFAULT_CHANNEL = {
|
|
2841
|
+
type: "stderr",
|
|
2842
|
+
timeout_seconds: 300,
|
|
2843
|
+
auto_deny: true
|
|
2844
|
+
};
|
|
2845
|
+
var DEFAULT_POLICY = {
|
|
2846
|
+
version: 1,
|
|
2847
|
+
tier1_always_approve: [
|
|
2848
|
+
"state_export",
|
|
2849
|
+
"state_import",
|
|
2850
|
+
"identity_rotate",
|
|
2851
|
+
"reputation_import",
|
|
2852
|
+
"bootstrap_provide_guarantee"
|
|
2853
|
+
],
|
|
2854
|
+
tier2_anomaly: DEFAULT_TIER2,
|
|
2855
|
+
tier3_always_allow: [
|
|
2856
|
+
"state_read",
|
|
2857
|
+
"state_write",
|
|
2858
|
+
"state_list",
|
|
2859
|
+
"state_delete",
|
|
2860
|
+
"identity_create",
|
|
2861
|
+
"identity_list",
|
|
2862
|
+
"identity_sign",
|
|
2863
|
+
"identity_verify",
|
|
2864
|
+
"proof_commitment",
|
|
2865
|
+
"proof_reveal",
|
|
2866
|
+
"disclosure_set_policy",
|
|
2867
|
+
"disclosure_evaluate",
|
|
2868
|
+
"reputation_record",
|
|
2869
|
+
"reputation_query",
|
|
2870
|
+
"reputation_export",
|
|
2871
|
+
"bootstrap_create_escrow",
|
|
2872
|
+
"exec_attest",
|
|
2873
|
+
"monitor_health",
|
|
2874
|
+
"monitor_audit_log",
|
|
2875
|
+
"manifest",
|
|
2876
|
+
"principal_policy_view",
|
|
2877
|
+
"principal_baseline_view"
|
|
2878
|
+
],
|
|
2879
|
+
approval_channel: DEFAULT_CHANNEL
|
|
2880
|
+
};
|
|
2881
|
+
function extractOperationName(toolName) {
|
|
2882
|
+
return toolName.startsWith("sanctuary/") ? toolName.slice("sanctuary/".length) : toolName;
|
|
2883
|
+
}
|
|
2884
|
+
function parsePolicy(content) {
|
|
2885
|
+
const trimmed = content.trim();
|
|
2886
|
+
if (trimmed.startsWith("{")) {
|
|
2887
|
+
const parsed = JSON.parse(trimmed);
|
|
2888
|
+
return validatePolicy(parsed);
|
|
2889
|
+
}
|
|
2890
|
+
const policy = {};
|
|
2891
|
+
let currentKey = null;
|
|
2892
|
+
let currentList = null;
|
|
2893
|
+
let currentObject = null;
|
|
2894
|
+
for (const rawLine of trimmed.split("\n")) {
|
|
2895
|
+
const line = rawLine.split("#")[0];
|
|
2896
|
+
if (line.trim() === "") continue;
|
|
2897
|
+
const indent = line.length - line.trimStart().length;
|
|
2898
|
+
const stripped = line.trim();
|
|
2899
|
+
if (indent === 0 && stripped.includes(":")) {
|
|
2900
|
+
if (currentKey && currentList) {
|
|
2901
|
+
policy[currentKey] = currentList;
|
|
2902
|
+
} else if (currentKey && currentObject) {
|
|
2903
|
+
policy[currentKey] = currentObject;
|
|
2904
|
+
}
|
|
2905
|
+
const colonIdx = stripped.indexOf(":");
|
|
2906
|
+
const key = stripped.slice(0, colonIdx).trim();
|
|
2907
|
+
const value = stripped.slice(colonIdx + 1).trim();
|
|
2908
|
+
if (value === "" || value === "|") {
|
|
2909
|
+
currentKey = key;
|
|
2910
|
+
currentList = null;
|
|
2911
|
+
currentObject = null;
|
|
2912
|
+
} else {
|
|
2913
|
+
policy[key] = parseScalar(value);
|
|
2914
|
+
currentKey = null;
|
|
2915
|
+
currentList = null;
|
|
2916
|
+
currentObject = null;
|
|
2917
|
+
}
|
|
2918
|
+
} else if (indent > 0 && stripped.startsWith("- ")) {
|
|
2919
|
+
if (!currentList) currentList = [];
|
|
2920
|
+
currentList.push(stripped.slice(2).trim().split(/\s+/)[0]);
|
|
2921
|
+
} else if (indent > 0 && stripped.includes(":")) {
|
|
2922
|
+
if (!currentObject) currentObject = {};
|
|
2923
|
+
const colonIdx = stripped.indexOf(":");
|
|
2924
|
+
const key = stripped.slice(0, colonIdx).trim();
|
|
2925
|
+
const value = stripped.slice(colonIdx + 1).trim();
|
|
2926
|
+
currentObject[key] = parseScalar(value.split(/\s+/)[0]);
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
if (currentKey && currentList) {
|
|
2930
|
+
policy[currentKey] = currentList;
|
|
2931
|
+
} else if (currentKey && currentObject) {
|
|
2932
|
+
policy[currentKey] = currentObject;
|
|
2933
|
+
}
|
|
2934
|
+
return validatePolicy(policy);
|
|
2935
|
+
}
|
|
2936
|
+
function parseScalar(value) {
|
|
2937
|
+
if (value === "true") return true;
|
|
2938
|
+
if (value === "false") return false;
|
|
2939
|
+
const num = Number(value);
|
|
2940
|
+
if (!isNaN(num) && value !== "") return num;
|
|
2941
|
+
return value.replace(/^["']|["']$/g, "");
|
|
2942
|
+
}
|
|
2943
|
+
function validatePolicy(raw) {
|
|
2944
|
+
return {
|
|
2945
|
+
version: raw.version ?? 1,
|
|
2946
|
+
tier1_always_approve: raw.tier1_always_approve ?? DEFAULT_POLICY.tier1_always_approve,
|
|
2947
|
+
tier2_anomaly: {
|
|
2948
|
+
...DEFAULT_TIER2,
|
|
2949
|
+
...raw.tier2_anomaly ?? {}
|
|
2950
|
+
},
|
|
2951
|
+
tier3_always_allow: raw.tier3_always_allow ?? DEFAULT_POLICY.tier3_always_allow,
|
|
2952
|
+
approval_channel: {
|
|
2953
|
+
...DEFAULT_CHANNEL,
|
|
2954
|
+
...raw.approval_channel ?? {}
|
|
2955
|
+
}
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
function generateDefaultPolicyYaml() {
|
|
2959
|
+
return `# Sanctuary Principal Policy v1
|
|
2960
|
+
# This file controls what your agent can do without asking.
|
|
2961
|
+
# Edit this file directly. Your agent cannot modify it.
|
|
2962
|
+
# Changes take effect on server restart.
|
|
2963
|
+
|
|
2964
|
+
version: 1
|
|
2965
|
+
|
|
2966
|
+
# \u2500\u2500\u2500 Tier 1: Always Requires Approval \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
|
|
2967
|
+
# These operations ALWAYS require your explicit approval.
|
|
2968
|
+
# They are inherently high-risk regardless of context.
|
|
2969
|
+
tier1_always_approve:
|
|
2970
|
+
- state_export
|
|
2971
|
+
- state_import
|
|
2972
|
+
- identity_rotate
|
|
2973
|
+
- reputation_import
|
|
2974
|
+
- bootstrap_provide_guarantee
|
|
2975
|
+
|
|
2976
|
+
# \u2500\u2500\u2500 Tier 2: Behavioral Anomaly Detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2977
|
+
# Triggers approval when agent behavior deviates from its baseline.
|
|
2978
|
+
# Options for each setting: approve | log | allow
|
|
2979
|
+
tier2_anomaly:
|
|
2980
|
+
new_namespace_access: approve
|
|
2981
|
+
new_counterparty: approve
|
|
2982
|
+
frequency_spike_multiplier: 5
|
|
2983
|
+
max_signs_per_minute: 10
|
|
2984
|
+
bulk_read_threshold: 20
|
|
2985
|
+
first_session_policy: approve
|
|
2986
|
+
|
|
2987
|
+
# \u2500\u2500\u2500 Tier 3: Always Allowed (Audit Only) \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
|
|
2988
|
+
# These operations never require approval but are always logged.
|
|
2989
|
+
tier3_always_allow:
|
|
2990
|
+
- state_read
|
|
2991
|
+
- state_write
|
|
2992
|
+
- state_list
|
|
2993
|
+
- state_delete
|
|
2994
|
+
- identity_create
|
|
2995
|
+
- identity_list
|
|
2996
|
+
- identity_sign
|
|
2997
|
+
- identity_verify
|
|
2998
|
+
- proof_commitment
|
|
2999
|
+
- proof_reveal
|
|
3000
|
+
- disclosure_set_policy
|
|
3001
|
+
- disclosure_evaluate
|
|
3002
|
+
- reputation_record
|
|
3003
|
+
- reputation_query
|
|
3004
|
+
- reputation_export
|
|
3005
|
+
- bootstrap_create_escrow
|
|
3006
|
+
- exec_attest
|
|
3007
|
+
- monitor_health
|
|
3008
|
+
- monitor_audit_log
|
|
3009
|
+
- manifest
|
|
3010
|
+
- principal_policy_view
|
|
3011
|
+
- principal_baseline_view
|
|
3012
|
+
|
|
3013
|
+
# \u2500\u2500\u2500 Approval Channel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3014
|
+
# How Sanctuary reaches you when approval is needed.
|
|
3015
|
+
approval_channel:
|
|
3016
|
+
type: stderr
|
|
3017
|
+
timeout_seconds: 300
|
|
3018
|
+
auto_deny: true
|
|
3019
|
+
`;
|
|
3020
|
+
}
|
|
3021
|
+
async function loadPrincipalPolicy(storagePath) {
|
|
3022
|
+
const policyPath = join(storagePath, "principal-policy.yaml");
|
|
3023
|
+
try {
|
|
3024
|
+
const content = await readFile(policyPath, "utf-8");
|
|
3025
|
+
const policy = parsePolicy(content);
|
|
3026
|
+
return Object.freeze(policy);
|
|
3027
|
+
} catch {
|
|
3028
|
+
const defaultYaml = generateDefaultPolicyYaml();
|
|
3029
|
+
try {
|
|
3030
|
+
await writeFile(policyPath, defaultYaml, "utf-8");
|
|
3031
|
+
await chmod(policyPath, 384);
|
|
3032
|
+
} catch {
|
|
3033
|
+
}
|
|
3034
|
+
return Object.freeze({ ...DEFAULT_POLICY });
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
// src/principal-policy/baseline.ts
|
|
3039
|
+
init_encoding();
|
|
3040
|
+
var BASELINE_NAMESPACE = "_principal";
|
|
3041
|
+
var BASELINE_KEY = "session-baseline";
|
|
3042
|
+
var BaselineTracker = class {
|
|
3043
|
+
storage;
|
|
3044
|
+
encryptionKey;
|
|
3045
|
+
profile;
|
|
3046
|
+
/** Sliding window: timestamps of tool calls per tool name (last 60s) */
|
|
3047
|
+
callWindows = /* @__PURE__ */ new Map();
|
|
3048
|
+
/** Sliding window: read counts per namespace (last 60s) */
|
|
3049
|
+
readWindows = /* @__PURE__ */ new Map();
|
|
3050
|
+
/** Sliding window: sign call timestamps (last 60s) */
|
|
3051
|
+
signWindow = [];
|
|
3052
|
+
constructor(storage, masterKey) {
|
|
3053
|
+
this.storage = storage;
|
|
3054
|
+
this.encryptionKey = derivePurposeKey(masterKey, "principal-baseline");
|
|
3055
|
+
this.profile = {
|
|
3056
|
+
known_namespaces: [],
|
|
3057
|
+
known_counterparties: [],
|
|
3058
|
+
tool_call_counts: {},
|
|
3059
|
+
is_first_session: true,
|
|
3060
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3061
|
+
};
|
|
3062
|
+
}
|
|
3063
|
+
/**
|
|
3064
|
+
* Load the previous session's baseline from storage.
|
|
3065
|
+
* If none exists, this is a first session.
|
|
3066
|
+
*/
|
|
3067
|
+
async load() {
|
|
3068
|
+
try {
|
|
3069
|
+
const raw = await this.storage.read(BASELINE_NAMESPACE, BASELINE_KEY);
|
|
3070
|
+
if (!raw) return;
|
|
3071
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
3072
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
3073
|
+
const saved = JSON.parse(bytesToString(decrypted));
|
|
3074
|
+
this.profile.known_namespaces = saved.known_namespaces ?? [];
|
|
3075
|
+
this.profile.known_counterparties = saved.known_counterparties ?? [];
|
|
3076
|
+
this.profile.is_first_session = false;
|
|
3077
|
+
} catch {
|
|
3078
|
+
this.profile.is_first_session = true;
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
/**
|
|
3082
|
+
* Save the current baseline to storage (encrypted).
|
|
3083
|
+
* Called at session end or periodically.
|
|
3084
|
+
*/
|
|
3085
|
+
async save() {
|
|
3086
|
+
this.profile.saved_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
3087
|
+
const serialized = stringToBytes(JSON.stringify(this.profile));
|
|
3088
|
+
const encrypted = encrypt(serialized, this.encryptionKey);
|
|
3089
|
+
await this.storage.write(
|
|
3090
|
+
BASELINE_NAMESPACE,
|
|
3091
|
+
BASELINE_KEY,
|
|
3092
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
3093
|
+
);
|
|
3094
|
+
}
|
|
3095
|
+
/**
|
|
3096
|
+
* Record a tool call for baseline tracking.
|
|
3097
|
+
* Returns anomaly information if applicable.
|
|
3098
|
+
*/
|
|
3099
|
+
recordToolCall(toolName) {
|
|
3100
|
+
const now = Date.now();
|
|
3101
|
+
this.profile.tool_call_counts[toolName] = (this.profile.tool_call_counts[toolName] ?? 0) + 1;
|
|
3102
|
+
if (!this.callWindows.has(toolName)) {
|
|
3103
|
+
this.callWindows.set(toolName, []);
|
|
3104
|
+
}
|
|
3105
|
+
const window = this.callWindows.get(toolName);
|
|
3106
|
+
window.push(now);
|
|
3107
|
+
const cutoff = now - 6e4;
|
|
3108
|
+
while (window.length > 0 && window[0] < cutoff) {
|
|
3109
|
+
window.shift();
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
/**
|
|
3113
|
+
* Record a namespace access.
|
|
3114
|
+
* @returns true if this is a new namespace (not in baseline)
|
|
3115
|
+
*/
|
|
3116
|
+
recordNamespaceAccess(namespace) {
|
|
3117
|
+
if (namespace.startsWith("_")) return false;
|
|
3118
|
+
const isNew = !this.profile.known_namespaces.includes(namespace);
|
|
3119
|
+
if (isNew) {
|
|
3120
|
+
this.profile.known_namespaces.push(namespace);
|
|
3121
|
+
}
|
|
3122
|
+
return isNew;
|
|
3123
|
+
}
|
|
3124
|
+
/**
|
|
3125
|
+
* Record a namespace read for bulk-read detection.
|
|
3126
|
+
* @returns the number of reads in the current 60-second window
|
|
3127
|
+
*/
|
|
3128
|
+
recordNamespaceRead(namespace) {
|
|
3129
|
+
const now = Date.now();
|
|
3130
|
+
if (!this.readWindows.has(namespace)) {
|
|
3131
|
+
this.readWindows.set(namespace, []);
|
|
3132
|
+
}
|
|
3133
|
+
const window = this.readWindows.get(namespace);
|
|
3134
|
+
window.push(now);
|
|
3135
|
+
const cutoff = now - 6e4;
|
|
3136
|
+
while (window.length > 0 && window[0] < cutoff) {
|
|
3137
|
+
window.shift();
|
|
3138
|
+
}
|
|
3139
|
+
return window.length;
|
|
3140
|
+
}
|
|
3141
|
+
/**
|
|
3142
|
+
* Record a counterparty DID interaction.
|
|
3143
|
+
* @returns true if this is a new counterparty (not in baseline)
|
|
3144
|
+
*/
|
|
3145
|
+
recordCounterparty(did) {
|
|
3146
|
+
const isNew = !this.profile.known_counterparties.includes(did);
|
|
3147
|
+
if (isNew) {
|
|
3148
|
+
this.profile.known_counterparties.push(did);
|
|
3149
|
+
}
|
|
3150
|
+
return isNew;
|
|
3151
|
+
}
|
|
3152
|
+
/**
|
|
3153
|
+
* Record a signing operation.
|
|
3154
|
+
* @returns the number of signs in the current 60-second window
|
|
3155
|
+
*/
|
|
3156
|
+
recordSign() {
|
|
3157
|
+
const now = Date.now();
|
|
3158
|
+
this.signWindow.push(now);
|
|
3159
|
+
const cutoff = now - 6e4;
|
|
3160
|
+
while (this.signWindow.length > 0 && this.signWindow[0] < cutoff) {
|
|
3161
|
+
this.signWindow.shift();
|
|
3162
|
+
}
|
|
3163
|
+
return this.signWindow.length;
|
|
3164
|
+
}
|
|
3165
|
+
/**
|
|
3166
|
+
* Get the current call rate for a tool (calls per minute).
|
|
3167
|
+
*/
|
|
3168
|
+
getCallRate(toolName) {
|
|
3169
|
+
return this.callWindows.get(toolName)?.length ?? 0;
|
|
3170
|
+
}
|
|
3171
|
+
/**
|
|
3172
|
+
* Get the average call rate across all tools in the baseline.
|
|
3173
|
+
*/
|
|
3174
|
+
getAverageCallRate() {
|
|
3175
|
+
let total = 0;
|
|
3176
|
+
let count = 0;
|
|
3177
|
+
for (const window of this.callWindows.values()) {
|
|
3178
|
+
total += window.length;
|
|
3179
|
+
count++;
|
|
3180
|
+
}
|
|
3181
|
+
return count > 0 ? total / count : 0;
|
|
3182
|
+
}
|
|
3183
|
+
/** Whether this is the first session */
|
|
3184
|
+
get isFirstSession() {
|
|
3185
|
+
return this.profile.is_first_session;
|
|
3186
|
+
}
|
|
3187
|
+
/** Get a read-only view of the current profile */
|
|
3188
|
+
getProfile() {
|
|
3189
|
+
return { ...this.profile };
|
|
3190
|
+
}
|
|
3191
|
+
};
|
|
3192
|
+
|
|
3193
|
+
// src/principal-policy/approval-channel.ts
|
|
3194
|
+
var StderrApprovalChannel = class {
|
|
3195
|
+
config;
|
|
3196
|
+
constructor(config) {
|
|
3197
|
+
this.config = config;
|
|
3198
|
+
}
|
|
3199
|
+
async requestApproval(request) {
|
|
3200
|
+
const prompt = this.formatPrompt(request);
|
|
3201
|
+
process.stderr.write(prompt + "\n");
|
|
3202
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3203
|
+
if (this.config.auto_deny) {
|
|
3204
|
+
return {
|
|
3205
|
+
decision: "deny",
|
|
3206
|
+
decided_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3207
|
+
decided_by: "timeout"
|
|
3208
|
+
};
|
|
3209
|
+
} else {
|
|
3210
|
+
return {
|
|
3211
|
+
decision: "approve",
|
|
3212
|
+
decided_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3213
|
+
decided_by: "auto"
|
|
3214
|
+
};
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
formatPrompt(request) {
|
|
3218
|
+
const tierLabel = request.tier === 1 ? "Tier 1 \u2014 always requires approval" : "Tier 2 \u2014 behavioral anomaly detected";
|
|
3219
|
+
const contextLines = Object.entries(request.context).map(([k, v]) => ` ${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`).join("\n");
|
|
3220
|
+
return [
|
|
3221
|
+
"",
|
|
3222
|
+
"\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
|
|
3223
|
+
"\u2551 SANCTUARY: Approval Required \u2551",
|
|
3224
|
+
"\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563",
|
|
3225
|
+
`\u2551 Operation: ${request.operation.padEnd(50)}\u2551`,
|
|
3226
|
+
`\u2551 ${tierLabel.padEnd(62)}\u2551`,
|
|
3227
|
+
`\u2551 Reason: ${request.reason.slice(0, 50).padEnd(50)}\u2551`,
|
|
3228
|
+
"\u2551 \u2551",
|
|
3229
|
+
`\u2551 Details: \u2551`,
|
|
3230
|
+
...contextLines.split("\n").map(
|
|
3231
|
+
(line) => `\u2551 ${line.padEnd(60)}\u2551`
|
|
3232
|
+
),
|
|
3233
|
+
"\u2551 \u2551",
|
|
3234
|
+
this.config.auto_deny ? "\u2551 Auto-denying (configure approval_channel.auto_deny to change) \u2551" : "\u2551 Auto-approving (informational mode) \u2551",
|
|
3235
|
+
"\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
|
|
3236
|
+
""
|
|
3237
|
+
].join("\n");
|
|
3238
|
+
}
|
|
3239
|
+
};
|
|
3240
|
+
var CallbackApprovalChannel = class {
|
|
3241
|
+
callback;
|
|
3242
|
+
constructor(callback) {
|
|
3243
|
+
this.callback = callback;
|
|
3244
|
+
}
|
|
3245
|
+
async requestApproval(request) {
|
|
3246
|
+
return this.callback(request);
|
|
3247
|
+
}
|
|
3248
|
+
};
|
|
3249
|
+
var AutoApproveChannel = class {
|
|
3250
|
+
async requestApproval(_request) {
|
|
3251
|
+
return {
|
|
3252
|
+
decision: "approve",
|
|
3253
|
+
decided_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3254
|
+
decided_by: "auto"
|
|
3255
|
+
};
|
|
3256
|
+
}
|
|
3257
|
+
};
|
|
3258
|
+
|
|
3259
|
+
// src/principal-policy/gate.ts
|
|
3260
|
+
var ApprovalGate = class {
|
|
3261
|
+
policy;
|
|
3262
|
+
baseline;
|
|
3263
|
+
channel;
|
|
3264
|
+
auditLog;
|
|
3265
|
+
constructor(policy, baseline, channel, auditLog) {
|
|
3266
|
+
this.policy = policy;
|
|
3267
|
+
this.baseline = baseline;
|
|
3268
|
+
this.channel = channel;
|
|
3269
|
+
this.auditLog = auditLog;
|
|
3270
|
+
}
|
|
3271
|
+
/**
|
|
3272
|
+
* Evaluate a tool call against the Principal Policy.
|
|
3273
|
+
*
|
|
3274
|
+
* @param toolName - Full MCP tool name (e.g., "sanctuary/state_export")
|
|
3275
|
+
* @param args - Tool call arguments (for context extraction)
|
|
3276
|
+
* @returns GateResult indicating whether the call is allowed
|
|
3277
|
+
*/
|
|
3278
|
+
async evaluate(toolName, args) {
|
|
3279
|
+
const operation = extractOperationName(toolName);
|
|
3280
|
+
this.baseline.recordToolCall(operation);
|
|
3281
|
+
if (this.policy.tier1_always_approve.includes(operation)) {
|
|
3282
|
+
return this.requestApproval(operation, 1, `"${operation}" is a Tier 1 operation (always requires approval)`, {
|
|
3283
|
+
operation,
|
|
3284
|
+
args_summary: this.summarizeArgs(args)
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
3287
|
+
const anomaly = this.detectAnomaly(operation, args);
|
|
3288
|
+
if (anomaly) {
|
|
3289
|
+
return this.requestApproval(operation, 2, anomaly.reason, anomaly.context);
|
|
3290
|
+
}
|
|
3291
|
+
this.auditLog.append("l2", `gate_allow:${operation}`, "system", {
|
|
3292
|
+
tier: 3,
|
|
3293
|
+
operation
|
|
3294
|
+
});
|
|
3295
|
+
return {
|
|
3296
|
+
allowed: true,
|
|
3297
|
+
tier: 3,
|
|
3298
|
+
reason: "Operation allowed (Tier 3)",
|
|
3299
|
+
approval_required: false
|
|
3300
|
+
};
|
|
3301
|
+
}
|
|
3302
|
+
/**
|
|
3303
|
+
* Detect Tier 2 behavioral anomalies.
|
|
3304
|
+
*/
|
|
3305
|
+
detectAnomaly(operation, args) {
|
|
3306
|
+
const config = this.policy.tier2_anomaly;
|
|
3307
|
+
if (this.baseline.isFirstSession && config.first_session_policy === "approve") {
|
|
3308
|
+
if (!this.policy.tier3_always_allow.includes(operation)) {
|
|
3309
|
+
return {
|
|
3310
|
+
reason: `First session: "${operation}" has no established baseline`,
|
|
3311
|
+
context: { operation, is_first_session: true }
|
|
3312
|
+
};
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
if (config.new_namespace_access === "approve") {
|
|
3316
|
+
const namespace = args.namespace;
|
|
3317
|
+
if (namespace) {
|
|
3318
|
+
const isNew = this.baseline.recordNamespaceAccess(namespace);
|
|
3319
|
+
if (isNew) {
|
|
3320
|
+
return {
|
|
3321
|
+
reason: `First access to namespace "${namespace}" (not in session baseline)`,
|
|
3322
|
+
context: {
|
|
3323
|
+
operation,
|
|
3324
|
+
namespace,
|
|
3325
|
+
known_namespaces: this.baseline.getProfile().known_namespaces
|
|
3326
|
+
}
|
|
3327
|
+
};
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
} else if (config.new_namespace_access === "log") {
|
|
3331
|
+
const namespace = args.namespace;
|
|
3332
|
+
if (namespace) {
|
|
3333
|
+
this.baseline.recordNamespaceAccess(namespace);
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
if (config.new_counterparty === "approve") {
|
|
3337
|
+
const counterpartyDid = args.counterparty_did ?? args.agent_identity_id;
|
|
3338
|
+
if (counterpartyDid) {
|
|
3339
|
+
const isNew = this.baseline.recordCounterparty(counterpartyDid);
|
|
3340
|
+
if (isNew) {
|
|
3341
|
+
return {
|
|
3342
|
+
reason: `First interaction with counterparty "${counterpartyDid}"`,
|
|
3343
|
+
context: {
|
|
3344
|
+
operation,
|
|
3345
|
+
counterparty_did: counterpartyDid,
|
|
3346
|
+
known_counterparties: this.baseline.getProfile().known_counterparties
|
|
3347
|
+
}
|
|
3348
|
+
};
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
} else if (config.new_counterparty === "log") {
|
|
3352
|
+
const counterpartyDid = args.counterparty_did;
|
|
3353
|
+
if (counterpartyDid) {
|
|
3354
|
+
this.baseline.recordCounterparty(counterpartyDid);
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
if (operation === "identity_sign") {
|
|
3358
|
+
const signCount = this.baseline.recordSign();
|
|
3359
|
+
if (signCount > config.max_signs_per_minute) {
|
|
3360
|
+
return {
|
|
3361
|
+
reason: `Signing frequency (${signCount}/min) exceeds limit (${config.max_signs_per_minute}/min)`,
|
|
3362
|
+
context: {
|
|
3363
|
+
operation,
|
|
3364
|
+
signs_per_minute: signCount,
|
|
3365
|
+
limit: config.max_signs_per_minute
|
|
3366
|
+
}
|
|
3367
|
+
};
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
if (operation === "state_read") {
|
|
3371
|
+
const namespace = args.namespace;
|
|
3372
|
+
if (namespace) {
|
|
3373
|
+
const readCount = this.baseline.recordNamespaceRead(namespace);
|
|
3374
|
+
if (readCount > config.bulk_read_threshold) {
|
|
3375
|
+
return {
|
|
3376
|
+
reason: `Bulk read detected: ${readCount} reads from "${namespace}" in 60 seconds (threshold: ${config.bulk_read_threshold})`,
|
|
3377
|
+
context: {
|
|
3378
|
+
operation,
|
|
3379
|
+
namespace,
|
|
3380
|
+
reads_in_window: readCount,
|
|
3381
|
+
threshold: config.bulk_read_threshold
|
|
3382
|
+
}
|
|
3383
|
+
};
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
const callRate = this.baseline.getCallRate(operation);
|
|
3388
|
+
const avgRate = this.baseline.getAverageCallRate();
|
|
3389
|
+
if (avgRate > 0 && callRate > avgRate * config.frequency_spike_multiplier) {
|
|
3390
|
+
return {
|
|
3391
|
+
reason: `Frequency spike: "${operation}" at ${callRate}/min (${config.frequency_spike_multiplier}\xD7 above average ${avgRate.toFixed(1)}/min)`,
|
|
3392
|
+
context: {
|
|
3393
|
+
operation,
|
|
3394
|
+
current_rate: callRate,
|
|
3395
|
+
average_rate: avgRate,
|
|
3396
|
+
multiplier: config.frequency_spike_multiplier
|
|
3397
|
+
}
|
|
3398
|
+
};
|
|
3399
|
+
}
|
|
3400
|
+
return null;
|
|
3401
|
+
}
|
|
3402
|
+
/**
|
|
3403
|
+
* Request approval from the human principal.
|
|
3404
|
+
*/
|
|
3405
|
+
async requestApproval(operation, tier, reason, context) {
|
|
3406
|
+
const request = {
|
|
3407
|
+
operation,
|
|
3408
|
+
tier,
|
|
3409
|
+
reason,
|
|
3410
|
+
context,
|
|
3411
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3412
|
+
};
|
|
3413
|
+
const response = await this.channel.requestApproval(request);
|
|
3414
|
+
this.auditLog.append("l2", `gate_${response.decision}:${operation}`, "system", {
|
|
3415
|
+
tier,
|
|
3416
|
+
reason,
|
|
3417
|
+
decided_by: response.decided_by
|
|
3418
|
+
});
|
|
3419
|
+
return {
|
|
3420
|
+
allowed: response.decision === "approve",
|
|
3421
|
+
tier,
|
|
3422
|
+
reason: response.decision === "approve" ? `Approved by ${response.decided_by}` : reason,
|
|
3423
|
+
approval_required: true,
|
|
3424
|
+
approval_response: response
|
|
3425
|
+
};
|
|
3426
|
+
}
|
|
3427
|
+
/**
|
|
3428
|
+
* Summarize tool arguments for the approval prompt.
|
|
3429
|
+
* Strips potentially large values to keep the prompt readable.
|
|
3430
|
+
*/
|
|
3431
|
+
summarizeArgs(args) {
|
|
3432
|
+
const summary = {};
|
|
3433
|
+
for (const [key, value] of Object.entries(args)) {
|
|
3434
|
+
if (typeof value === "string" && value.length > 100) {
|
|
3435
|
+
summary[key] = value.slice(0, 100) + "...";
|
|
3436
|
+
} else {
|
|
3437
|
+
summary[key] = value;
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
return summary;
|
|
3441
|
+
}
|
|
3442
|
+
/** Get the baseline tracker for saving at session end */
|
|
3443
|
+
getBaseline() {
|
|
3444
|
+
return this.baseline;
|
|
3445
|
+
}
|
|
3446
|
+
};
|
|
3447
|
+
|
|
3448
|
+
// src/principal-policy/tools.ts
|
|
3449
|
+
function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
3450
|
+
return [
|
|
3451
|
+
{
|
|
3452
|
+
name: "sanctuary/principal_policy_view",
|
|
3453
|
+
description: "View the current Principal Policy \u2014 the human-controlled rules governing what operations require approval. Read-only.",
|
|
3454
|
+
inputSchema: {
|
|
3455
|
+
type: "object",
|
|
3456
|
+
properties: {
|
|
3457
|
+
include_defaults: {
|
|
3458
|
+
type: "boolean",
|
|
3459
|
+
description: "Include tier3_always_allow list (can be long)",
|
|
3460
|
+
default: false
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
},
|
|
3464
|
+
handler: async (args) => {
|
|
3465
|
+
const includeDefaults = args.include_defaults ?? false;
|
|
3466
|
+
const view = {
|
|
3467
|
+
version: policy.version,
|
|
3468
|
+
tier1_always_approve: policy.tier1_always_approve,
|
|
3469
|
+
tier2_anomaly: policy.tier2_anomaly,
|
|
3470
|
+
approval_channel: {
|
|
3471
|
+
type: policy.approval_channel.type,
|
|
3472
|
+
timeout_seconds: policy.approval_channel.timeout_seconds,
|
|
3473
|
+
auto_deny: policy.approval_channel.auto_deny
|
|
3474
|
+
}
|
|
3475
|
+
};
|
|
3476
|
+
if (includeDefaults) {
|
|
3477
|
+
view.tier3_always_allow = policy.tier3_always_allow;
|
|
3478
|
+
} else {
|
|
3479
|
+
view.tier3_always_allow_count = policy.tier3_always_allow.length;
|
|
3480
|
+
view.note = "Pass include_defaults: true to see the full tier3_always_allow list";
|
|
3481
|
+
}
|
|
3482
|
+
auditLog.append("l2", "principal_policy_view", "system", {
|
|
3483
|
+
include_defaults: includeDefaults
|
|
3484
|
+
});
|
|
3485
|
+
return toolResult(view);
|
|
3486
|
+
}
|
|
3487
|
+
},
|
|
3488
|
+
{
|
|
3489
|
+
name: "sanctuary/principal_baseline_view",
|
|
3490
|
+
description: "View the current behavioral baseline \u2014 the session profile used for anomaly detection. Shows known namespaces, counterparties, and tool call counts. Read-only.",
|
|
3491
|
+
inputSchema: {
|
|
3492
|
+
type: "object",
|
|
3493
|
+
properties: {}
|
|
3494
|
+
},
|
|
3495
|
+
handler: async () => {
|
|
3496
|
+
const profile = baseline.getProfile();
|
|
3497
|
+
auditLog.append("l2", "principal_baseline_view", "system");
|
|
3498
|
+
return toolResult({
|
|
3499
|
+
is_first_session: profile.is_first_session,
|
|
3500
|
+
session_started_at: profile.started_at,
|
|
3501
|
+
known_namespaces: profile.known_namespaces,
|
|
3502
|
+
known_counterparties: profile.known_counterparties,
|
|
3503
|
+
tool_call_counts: profile.tool_call_counts,
|
|
3504
|
+
last_saved: profile.saved_at ?? "not yet saved"
|
|
3505
|
+
});
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
];
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
// src/shr/types.ts
|
|
3512
|
+
function deepSortKeys(obj) {
|
|
3513
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
3514
|
+
if (Array.isArray(obj)) return obj.map(deepSortKeys);
|
|
3515
|
+
const sorted = {};
|
|
3516
|
+
for (const key of Object.keys(obj).sort()) {
|
|
3517
|
+
sorted[key] = deepSortKeys(obj[key]);
|
|
3518
|
+
}
|
|
3519
|
+
return sorted;
|
|
3520
|
+
}
|
|
3521
|
+
function canonicalizeForSigning(body) {
|
|
3522
|
+
return JSON.stringify(deepSortKeys(body));
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
// src/shr/generator.ts
|
|
3526
|
+
init_encoding();
|
|
3527
|
+
var DEFAULT_VALIDITY_MS = 60 * 60 * 1e3;
|
|
3528
|
+
function generateSHR(identityId, opts) {
|
|
3529
|
+
const { config, identityManager, masterKey, validityMs } = opts;
|
|
3530
|
+
const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
|
|
3531
|
+
if (!identity) {
|
|
3532
|
+
return "No identity available for signing. Create an identity first.";
|
|
3533
|
+
}
|
|
3534
|
+
const now = /* @__PURE__ */ new Date();
|
|
3535
|
+
const expiresAt = new Date(now.getTime() + (validityMs ?? DEFAULT_VALIDITY_MS));
|
|
3536
|
+
const degradations = [];
|
|
3537
|
+
if (config.execution.environment === "local-process") {
|
|
3538
|
+
degradations.push({
|
|
3539
|
+
layer: "l2",
|
|
3540
|
+
code: "PROCESS_ISOLATION_ONLY",
|
|
3541
|
+
severity: "warning",
|
|
3542
|
+
description: "Process-level isolation only (no TEE)",
|
|
3543
|
+
mitigation: "TEE support planned for v0.3.0"
|
|
3544
|
+
});
|
|
3545
|
+
degradations.push({
|
|
3546
|
+
layer: "l2",
|
|
3547
|
+
code: "SELF_REPORTED_ATTESTATION",
|
|
3548
|
+
severity: "warning",
|
|
3549
|
+
description: "Attestation is self-reported (no hardware root of trust)",
|
|
3550
|
+
mitigation: "TEE attestation planned for v0.3.0"
|
|
3551
|
+
});
|
|
3552
|
+
}
|
|
3553
|
+
if (config.disclosure.proof_system === "commitment-only") {
|
|
3554
|
+
degradations.push({
|
|
3555
|
+
layer: "l3",
|
|
3556
|
+
code: "COMMITMENT_ONLY",
|
|
3557
|
+
severity: "info",
|
|
3558
|
+
description: "Commitment schemes only (no ZK proofs)",
|
|
3559
|
+
mitigation: "ZK proof support planned for future release"
|
|
3560
|
+
});
|
|
3561
|
+
}
|
|
3562
|
+
const body = {
|
|
3563
|
+
shr_version: "1.0",
|
|
3564
|
+
instance_id: identity.identity_id,
|
|
3565
|
+
generated_at: now.toISOString(),
|
|
3566
|
+
expires_at: expiresAt.toISOString(),
|
|
3567
|
+
layers: {
|
|
3568
|
+
l1: {
|
|
3569
|
+
status: "active",
|
|
3570
|
+
encryption: config.state.encryption,
|
|
3571
|
+
key_custody: "self",
|
|
3572
|
+
integrity: config.state.integrity,
|
|
3573
|
+
identity_type: config.state.identity_provider,
|
|
3574
|
+
state_portable: true
|
|
3575
|
+
},
|
|
3576
|
+
l2: {
|
|
3577
|
+
status: config.execution.environment === "local-process" ? "degraded" : "active",
|
|
3578
|
+
isolation_type: config.execution.environment,
|
|
3579
|
+
attestation_available: config.execution.attestation
|
|
3580
|
+
},
|
|
3581
|
+
l3: {
|
|
3582
|
+
status: config.disclosure.proof_system === "commitment-only" ? "degraded" : "active",
|
|
3583
|
+
proof_system: config.disclosure.proof_system,
|
|
3584
|
+
selective_disclosure: config.disclosure.proof_system !== "commitment-only"
|
|
3585
|
+
},
|
|
3586
|
+
l4: {
|
|
3587
|
+
status: "active",
|
|
3588
|
+
reputation_mode: config.reputation.mode,
|
|
3589
|
+
attestation_format: config.reputation.attestation_format,
|
|
3590
|
+
reputation_portable: true
|
|
3591
|
+
}
|
|
3592
|
+
},
|
|
3593
|
+
capabilities: {
|
|
3594
|
+
handshake: true,
|
|
3595
|
+
shr_exchange: true,
|
|
3596
|
+
reputation_verify: true,
|
|
3597
|
+
encrypted_channel: false
|
|
3598
|
+
// Not yet implemented
|
|
3599
|
+
},
|
|
3600
|
+
degradations
|
|
3601
|
+
};
|
|
3602
|
+
const canonical = canonicalizeForSigning(body);
|
|
3603
|
+
const payload = stringToBytes(canonical);
|
|
3604
|
+
const encryptionKey = derivePurposeKey(masterKey, "identity-encryption");
|
|
3605
|
+
const signatureBytes = sign(
|
|
3606
|
+
payload,
|
|
3607
|
+
identity.encrypted_private_key,
|
|
3608
|
+
encryptionKey
|
|
3609
|
+
);
|
|
3610
|
+
return {
|
|
3611
|
+
body,
|
|
3612
|
+
signed_by: identity.public_key,
|
|
3613
|
+
signature: toBase64url(signatureBytes)
|
|
3614
|
+
};
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
// src/shr/verifier.ts
|
|
3618
|
+
init_encoding();
|
|
3619
|
+
function verifySHR(shr, now) {
|
|
3620
|
+
const errors = [];
|
|
3621
|
+
const warnings = [];
|
|
3622
|
+
const currentTime = now ?? /* @__PURE__ */ new Date();
|
|
3623
|
+
if (!shr.body || !shr.signed_by || !shr.signature) {
|
|
3624
|
+
errors.push("Missing required SHR fields (body, signed_by, or signature)");
|
|
3625
|
+
return {
|
|
3626
|
+
valid: false,
|
|
3627
|
+
errors,
|
|
3628
|
+
warnings,
|
|
3629
|
+
sovereignty_level: "minimal",
|
|
3630
|
+
counterparty_id: shr.body?.instance_id ?? "unknown",
|
|
3631
|
+
expires_at: shr.body?.expires_at ?? "unknown"
|
|
3632
|
+
};
|
|
3633
|
+
}
|
|
3634
|
+
if (shr.body.shr_version !== "1.0") {
|
|
3635
|
+
errors.push(`Unsupported SHR version: ${shr.body.shr_version}`);
|
|
3636
|
+
}
|
|
3637
|
+
const expiresAt = new Date(shr.body.expires_at);
|
|
3638
|
+
if (isNaN(expiresAt.getTime())) {
|
|
3639
|
+
errors.push("Invalid expires_at timestamp");
|
|
3640
|
+
} else if (currentTime > expiresAt) {
|
|
3641
|
+
errors.push(`SHR expired at ${shr.body.expires_at}`);
|
|
3642
|
+
}
|
|
3643
|
+
const generatedAt = new Date(shr.body.generated_at);
|
|
3644
|
+
if (isNaN(generatedAt.getTime())) {
|
|
3645
|
+
errors.push("Invalid generated_at timestamp");
|
|
3646
|
+
} else if (generatedAt > currentTime) {
|
|
3647
|
+
warnings.push("SHR generated_at is in the future \u2014 clock skew detected");
|
|
3648
|
+
}
|
|
3649
|
+
try {
|
|
3650
|
+
const publicKey = fromBase64url(shr.signed_by);
|
|
3651
|
+
const signatureBytes = fromBase64url(shr.signature);
|
|
3652
|
+
const canonical = canonicalizeForSigning(shr.body);
|
|
3653
|
+
const payload = stringToBytes(canonical);
|
|
3654
|
+
const signatureValid = verify(payload, signatureBytes, publicKey);
|
|
3655
|
+
if (!signatureValid) {
|
|
3656
|
+
errors.push("Invalid signature \u2014 SHR may have been tampered with");
|
|
3657
|
+
}
|
|
3658
|
+
} catch (e) {
|
|
3659
|
+
errors.push(`Signature verification failed: ${e.message}`);
|
|
3660
|
+
}
|
|
3661
|
+
const { layers } = shr.body;
|
|
3662
|
+
if (!layers.l1 || !layers.l2 || !layers.l3 || !layers.l4) {
|
|
3663
|
+
errors.push("Missing one or more layer definitions");
|
|
3664
|
+
}
|
|
3665
|
+
const sovereigntyLevel = assessSovereigntyLevel(shr.body);
|
|
3666
|
+
for (const d of shr.body.degradations ?? []) {
|
|
3667
|
+
if (d.severity === "critical") {
|
|
3668
|
+
warnings.push(`Critical degradation in ${d.layer}: ${d.description}`);
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
return {
|
|
3672
|
+
valid: errors.length === 0,
|
|
3673
|
+
errors,
|
|
3674
|
+
warnings,
|
|
3675
|
+
sovereignty_level: sovereigntyLevel,
|
|
3676
|
+
counterparty_id: shr.body.instance_id,
|
|
3677
|
+
expires_at: shr.body.expires_at
|
|
3678
|
+
};
|
|
3679
|
+
}
|
|
3680
|
+
function assessSovereigntyLevel(body) {
|
|
3681
|
+
const { l1, l2, l3, l4 } = body.layers;
|
|
3682
|
+
if (l1.status === "active" && l2.status === "active" && l3.status === "active" && l4.status === "active") {
|
|
3683
|
+
return "full";
|
|
3684
|
+
}
|
|
3685
|
+
if (l1.status !== "active") {
|
|
3686
|
+
return "minimal";
|
|
3687
|
+
}
|
|
3688
|
+
if (l4.status === "active" || l4.status === "degraded") {
|
|
3689
|
+
return "degraded";
|
|
3690
|
+
}
|
|
3691
|
+
return "minimal";
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
// src/shr/tools.ts
|
|
3695
|
+
function createSHRTools(config, identityManager, masterKey, auditLog) {
|
|
3696
|
+
const generatorOpts = {
|
|
3697
|
+
config,
|
|
3698
|
+
identityManager,
|
|
3699
|
+
masterKey
|
|
3700
|
+
};
|
|
3701
|
+
const tools = [
|
|
3702
|
+
{
|
|
3703
|
+
name: "sanctuary/shr_generate",
|
|
3704
|
+
description: "Generate a signed Sovereignty Health Report (SHR) \u2014 a machine-readable, cryptographically signed advertisement of this instance's sovereignty posture. Present this to counterparties to prove your sovereignty capabilities.",
|
|
3705
|
+
inputSchema: {
|
|
3706
|
+
type: "object",
|
|
3707
|
+
properties: {
|
|
3708
|
+
identity_id: {
|
|
3709
|
+
type: "string",
|
|
3710
|
+
description: "Identity to sign the SHR with. Defaults to primary identity."
|
|
3711
|
+
},
|
|
3712
|
+
validity_minutes: {
|
|
3713
|
+
type: "number",
|
|
3714
|
+
description: "How long the SHR is valid (minutes). Default: 60."
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
},
|
|
3718
|
+
handler: async (args) => {
|
|
3719
|
+
const validityMs = args.validity_minutes ? args.validity_minutes * 60 * 1e3 : void 0;
|
|
3720
|
+
const result = generateSHR(args.identity_id, {
|
|
3721
|
+
...generatorOpts,
|
|
3722
|
+
validityMs
|
|
3723
|
+
});
|
|
3724
|
+
if (typeof result === "string") {
|
|
3725
|
+
return toolResult({ error: result });
|
|
3726
|
+
}
|
|
3727
|
+
auditLog.append("l2", "shr_generate", result.body.instance_id);
|
|
3728
|
+
return toolResult(result);
|
|
3729
|
+
}
|
|
3730
|
+
},
|
|
3731
|
+
{
|
|
3732
|
+
name: "sanctuary/shr_verify",
|
|
3733
|
+
description: "Verify a counterparty's Sovereignty Health Report (SHR). Checks signature validity, temporal validity, and assesses sovereignty level.",
|
|
3734
|
+
inputSchema: {
|
|
3735
|
+
type: "object",
|
|
3736
|
+
properties: {
|
|
3737
|
+
shr: {
|
|
3738
|
+
type: "object",
|
|
3739
|
+
description: "The signed SHR to verify (full SignedSHR object)."
|
|
3740
|
+
}
|
|
3741
|
+
},
|
|
3742
|
+
required: ["shr"]
|
|
3743
|
+
},
|
|
3744
|
+
handler: async (args) => {
|
|
3745
|
+
const shr = args.shr;
|
|
3746
|
+
const result = verifySHR(shr);
|
|
3747
|
+
auditLog.append(
|
|
3748
|
+
"l2",
|
|
3749
|
+
"shr_verify",
|
|
3750
|
+
result.counterparty_id,
|
|
3751
|
+
void 0,
|
|
3752
|
+
result.valid ? "success" : "failure"
|
|
3753
|
+
);
|
|
3754
|
+
return toolResult(result);
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
];
|
|
3758
|
+
return { tools };
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
// src/handshake/protocol.ts
|
|
3762
|
+
init_encoding();
|
|
3763
|
+
function generateNonce() {
|
|
3764
|
+
return toBase64url(randomBytes(32));
|
|
3765
|
+
}
|
|
3766
|
+
function initiateHandshake(ourSHR) {
|
|
3767
|
+
const nonce = generateNonce();
|
|
3768
|
+
const sessionId = toBase64url(randomBytes(16));
|
|
3769
|
+
const challenge = {
|
|
3770
|
+
protocol_version: "1.0",
|
|
3771
|
+
shr: ourSHR,
|
|
3772
|
+
nonce,
|
|
3773
|
+
initiated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3774
|
+
};
|
|
3775
|
+
const session = {
|
|
3776
|
+
session_id: sessionId,
|
|
3777
|
+
role: "initiator",
|
|
3778
|
+
state: "initiated",
|
|
3779
|
+
our_nonce: nonce,
|
|
3780
|
+
our_shr: ourSHR,
|
|
3781
|
+
initiated_at: challenge.initiated_at
|
|
3782
|
+
};
|
|
3783
|
+
return { challenge, session };
|
|
3784
|
+
}
|
|
3785
|
+
function respondToHandshake(challenge, ourSHR, identityManager, masterKey, identityId) {
|
|
3786
|
+
if (challenge.protocol_version !== "1.0") {
|
|
3787
|
+
return { error: `Unsupported protocol version: ${challenge.protocol_version}` };
|
|
3788
|
+
}
|
|
3789
|
+
const shrResult = verifySHR(challenge.shr);
|
|
3790
|
+
if (!shrResult.valid) {
|
|
3791
|
+
return { error: `Initiator SHR verification failed: ${shrResult.errors.join(", ")}` };
|
|
3792
|
+
}
|
|
3793
|
+
const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
|
|
3794
|
+
if (!identity) {
|
|
3795
|
+
return { error: "No identity available for signing" };
|
|
3796
|
+
}
|
|
3797
|
+
const encryptionKey = derivePurposeKey(masterKey, "identity-encryption");
|
|
3798
|
+
const nonceBytes = stringToBytes(challenge.nonce);
|
|
3799
|
+
const nonceSignature = sign(
|
|
3800
|
+
nonceBytes,
|
|
3801
|
+
identity.encrypted_private_key,
|
|
3802
|
+
encryptionKey
|
|
3803
|
+
);
|
|
3804
|
+
const responderNonce = generateNonce();
|
|
3805
|
+
const response = {
|
|
3806
|
+
protocol_version: "1.0",
|
|
3807
|
+
shr: ourSHR,
|
|
3808
|
+
responder_nonce: responderNonce,
|
|
3809
|
+
initiator_nonce_signature: toBase64url(nonceSignature),
|
|
3810
|
+
responded_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3811
|
+
};
|
|
3812
|
+
const session = {
|
|
3813
|
+
session_id: toBase64url(randomBytes(16)),
|
|
3814
|
+
role: "responder",
|
|
3815
|
+
state: "responded",
|
|
3816
|
+
our_nonce: responderNonce,
|
|
3817
|
+
their_nonce: challenge.nonce,
|
|
3818
|
+
our_shr: ourSHR,
|
|
3819
|
+
their_shr: challenge.shr,
|
|
3820
|
+
initiated_at: challenge.initiated_at
|
|
3821
|
+
};
|
|
3822
|
+
return { response, session };
|
|
3823
|
+
}
|
|
3824
|
+
function completeHandshake(response, session, identityManager, masterKey, identityId) {
|
|
3825
|
+
if (response.protocol_version !== "1.0") {
|
|
3826
|
+
return { error: `Unsupported protocol version: ${response.protocol_version}` };
|
|
3827
|
+
}
|
|
3828
|
+
const shrResult = verifySHR(response.shr);
|
|
3829
|
+
if (!shrResult.valid) {
|
|
3830
|
+
return { error: `Responder SHR verification failed: ${shrResult.errors.join(", ")}` };
|
|
3831
|
+
}
|
|
3832
|
+
const responderPublicKey = fromBase64url(response.shr.signed_by);
|
|
3833
|
+
const ourNonceBytes = stringToBytes(session.our_nonce);
|
|
3834
|
+
const nonceSignatureBytes = fromBase64url(response.initiator_nonce_signature);
|
|
3835
|
+
const nonceSignatureValid = verify(
|
|
3836
|
+
ourNonceBytes,
|
|
3837
|
+
nonceSignatureBytes,
|
|
3838
|
+
responderPublicKey
|
|
3839
|
+
);
|
|
3840
|
+
if (!nonceSignatureValid) {
|
|
3841
|
+
return { error: "Responder's nonce signature is invalid \u2014 possible replay or MITM" };
|
|
3842
|
+
}
|
|
3843
|
+
const identity = identityId ? identityManager.get(identityId) : identityManager.getDefault();
|
|
3844
|
+
if (!identity) {
|
|
3845
|
+
return { error: "No identity available for signing" };
|
|
3846
|
+
}
|
|
3847
|
+
const encryptionKey = derivePurposeKey(masterKey, "identity-encryption");
|
|
3848
|
+
const responderNonceBytes = stringToBytes(response.responder_nonce);
|
|
3849
|
+
const responderNonceSignature = sign(
|
|
3850
|
+
responderNonceBytes,
|
|
3851
|
+
identity.encrypted_private_key,
|
|
3852
|
+
encryptionKey
|
|
3853
|
+
);
|
|
3854
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3855
|
+
const completion = {
|
|
3856
|
+
protocol_version: "1.0",
|
|
3857
|
+
responder_nonce_signature: toBase64url(responderNonceSignature),
|
|
3858
|
+
completed_at: now
|
|
3859
|
+
};
|
|
3860
|
+
const sovereigntyLevel = shrResult.sovereignty_level;
|
|
3861
|
+
const trustTier = deriveTrustTier(sovereigntyLevel);
|
|
3862
|
+
const result = {
|
|
3863
|
+
counterparty_id: shrResult.counterparty_id,
|
|
3864
|
+
counterparty_shr: response.shr,
|
|
3865
|
+
verified: true,
|
|
3866
|
+
sovereignty_level: sovereigntyLevel,
|
|
3867
|
+
trust_tier: trustTier,
|
|
3868
|
+
completed_at: now,
|
|
3869
|
+
expires_at: shrResult.expires_at,
|
|
3870
|
+
errors: []
|
|
3871
|
+
};
|
|
3872
|
+
return { completion, result };
|
|
3873
|
+
}
|
|
3874
|
+
function verifyCompletion(completion, session) {
|
|
3875
|
+
const errors = [];
|
|
3876
|
+
if (!session.their_shr) {
|
|
3877
|
+
return {
|
|
3878
|
+
counterparty_id: "unknown",
|
|
3879
|
+
counterparty_shr: session.our_shr,
|
|
3880
|
+
// placeholder
|
|
3881
|
+
verified: false,
|
|
3882
|
+
sovereignty_level: "unverified",
|
|
3883
|
+
trust_tier: "unverified",
|
|
3884
|
+
completed_at: completion.completed_at,
|
|
3885
|
+
expires_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3886
|
+
errors: ["No initiator SHR in session state"]
|
|
3887
|
+
};
|
|
3888
|
+
}
|
|
3889
|
+
const initiatorPublicKey = fromBase64url(session.their_shr.signed_by);
|
|
3890
|
+
const ourNonceBytes = stringToBytes(session.our_nonce);
|
|
3891
|
+
const nonceSignatureBytes = fromBase64url(completion.responder_nonce_signature);
|
|
3892
|
+
const nonceSignatureValid = verify(
|
|
3893
|
+
ourNonceBytes,
|
|
3894
|
+
nonceSignatureBytes,
|
|
3895
|
+
initiatorPublicKey
|
|
3896
|
+
);
|
|
3897
|
+
if (!nonceSignatureValid) {
|
|
3898
|
+
errors.push("Initiator's nonce signature is invalid \u2014 possible replay or MITM");
|
|
3899
|
+
}
|
|
3900
|
+
const shrResult = verifySHR(session.their_shr);
|
|
3901
|
+
if (!shrResult.valid) {
|
|
3902
|
+
errors.push(...shrResult.errors);
|
|
3903
|
+
}
|
|
3904
|
+
const verified = errors.length === 0;
|
|
3905
|
+
const sovereigntyLevel = verified ? shrResult.sovereignty_level : "unverified";
|
|
3906
|
+
return {
|
|
3907
|
+
counterparty_id: session.their_shr.body.instance_id,
|
|
3908
|
+
counterparty_shr: session.their_shr,
|
|
3909
|
+
verified,
|
|
3910
|
+
sovereignty_level: sovereigntyLevel,
|
|
3911
|
+
trust_tier: deriveTrustTier(sovereigntyLevel),
|
|
3912
|
+
completed_at: completion.completed_at,
|
|
3913
|
+
expires_at: session.their_shr.body.expires_at,
|
|
3914
|
+
errors
|
|
3915
|
+
};
|
|
3916
|
+
}
|
|
3917
|
+
function deriveTrustTier(level) {
|
|
3918
|
+
switch (level) {
|
|
3919
|
+
case "full":
|
|
3920
|
+
return "verified-sovereign";
|
|
3921
|
+
case "degraded":
|
|
3922
|
+
return "verified-degraded";
|
|
3923
|
+
default:
|
|
3924
|
+
return "unverified";
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3928
|
+
// src/handshake/tools.ts
|
|
3929
|
+
function createHandshakeTools(config, identityManager, masterKey, auditLog) {
|
|
3930
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
3931
|
+
const shrOpts = {
|
|
3932
|
+
config,
|
|
3933
|
+
identityManager,
|
|
3934
|
+
masterKey
|
|
3935
|
+
};
|
|
3936
|
+
const tools = [
|
|
3937
|
+
{
|
|
3938
|
+
name: "sanctuary/handshake_initiate",
|
|
3939
|
+
description: "Initiate a sovereignty handshake with a counterparty. Generates a challenge containing this instance's signed SHR and a cryptographic nonce. Send the returned challenge to the counterparty.",
|
|
3940
|
+
inputSchema: {
|
|
3941
|
+
type: "object",
|
|
3942
|
+
properties: {
|
|
3943
|
+
identity_id: {
|
|
3944
|
+
type: "string",
|
|
3945
|
+
description: "Identity to use for the handshake. Defaults to primary identity."
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
},
|
|
3949
|
+
handler: async (args) => {
|
|
3950
|
+
const shr = generateSHR(args.identity_id, shrOpts);
|
|
3951
|
+
if (typeof shr === "string") {
|
|
3952
|
+
return toolResult({ error: shr });
|
|
3953
|
+
}
|
|
3954
|
+
const { challenge, session } = initiateHandshake(shr);
|
|
3955
|
+
sessions.set(session.session_id, session);
|
|
3956
|
+
auditLog.append("l4", "handshake_initiate", shr.body.instance_id);
|
|
3957
|
+
return toolResult({
|
|
3958
|
+
session_id: session.session_id,
|
|
3959
|
+
challenge,
|
|
3960
|
+
instructions: "Send the 'challenge' object to the counterparty's sanctuary/handshake_respond tool. When you receive their response, pass it to sanctuary/handshake_complete with this session_id."
|
|
3961
|
+
});
|
|
3962
|
+
}
|
|
3963
|
+
},
|
|
3964
|
+
{
|
|
3965
|
+
name: "sanctuary/handshake_respond",
|
|
3966
|
+
description: "Respond to an incoming sovereignty handshake challenge. Verifies the initiator's SHR, signs their nonce, and returns our SHR with a counter-nonce.",
|
|
3967
|
+
inputSchema: {
|
|
3968
|
+
type: "object",
|
|
3969
|
+
properties: {
|
|
3970
|
+
challenge: {
|
|
3971
|
+
type: "object",
|
|
3972
|
+
description: "The HandshakeChallenge received from the initiator."
|
|
3973
|
+
},
|
|
3974
|
+
identity_id: {
|
|
3975
|
+
type: "string",
|
|
3976
|
+
description: "Identity to use for the response. Defaults to primary identity."
|
|
3977
|
+
}
|
|
3978
|
+
},
|
|
3979
|
+
required: ["challenge"]
|
|
3980
|
+
},
|
|
3981
|
+
handler: async (args) => {
|
|
3982
|
+
const challenge = args.challenge;
|
|
3983
|
+
const shr = generateSHR(args.identity_id, shrOpts);
|
|
3984
|
+
if (typeof shr === "string") {
|
|
3985
|
+
return toolResult({ error: shr });
|
|
3986
|
+
}
|
|
3987
|
+
const result = respondToHandshake(
|
|
3988
|
+
challenge,
|
|
3989
|
+
shr,
|
|
3990
|
+
identityManager,
|
|
3991
|
+
masterKey,
|
|
3992
|
+
args.identity_id
|
|
3993
|
+
);
|
|
3994
|
+
if ("error" in result) {
|
|
3995
|
+
auditLog.append("l4", "handshake_respond", shr.body.instance_id, void 0, "failure");
|
|
3996
|
+
return toolResult({ error: result.error });
|
|
3997
|
+
}
|
|
3998
|
+
sessions.set(result.session.session_id, result.session);
|
|
3999
|
+
auditLog.append("l4", "handshake_respond", shr.body.instance_id);
|
|
4000
|
+
return toolResult({
|
|
4001
|
+
session_id: result.session.session_id,
|
|
4002
|
+
response: result.response,
|
|
4003
|
+
instructions: "Send the 'response' object back to the initiator. When you receive their completion, pass it to sanctuary/handshake_status with this session_id."
|
|
4004
|
+
});
|
|
4005
|
+
}
|
|
4006
|
+
},
|
|
4007
|
+
{
|
|
4008
|
+
name: "sanctuary/handshake_complete",
|
|
4009
|
+
description: "Complete a sovereignty handshake (initiator side). Verifies the responder's SHR and nonce signature, signs their nonce, and produces the final result.",
|
|
4010
|
+
inputSchema: {
|
|
4011
|
+
type: "object",
|
|
4012
|
+
properties: {
|
|
4013
|
+
session_id: {
|
|
4014
|
+
type: "string",
|
|
4015
|
+
description: "Session ID from handshake_initiate."
|
|
4016
|
+
},
|
|
4017
|
+
response: {
|
|
4018
|
+
type: "object",
|
|
4019
|
+
description: "The HandshakeResponse received from the responder."
|
|
4020
|
+
}
|
|
4021
|
+
},
|
|
4022
|
+
required: ["session_id", "response"]
|
|
4023
|
+
},
|
|
4024
|
+
handler: async (args) => {
|
|
4025
|
+
const sessionId = args.session_id;
|
|
4026
|
+
const response = args.response;
|
|
4027
|
+
const session = sessions.get(sessionId);
|
|
4028
|
+
if (!session) {
|
|
4029
|
+
return toolResult({ error: `No handshake session found: ${sessionId}` });
|
|
4030
|
+
}
|
|
4031
|
+
if (session.state !== "initiated") {
|
|
4032
|
+
return toolResult({
|
|
4033
|
+
error: `Session is in state '${session.state}', expected 'initiated'`
|
|
4034
|
+
});
|
|
4035
|
+
}
|
|
4036
|
+
const result = completeHandshake(
|
|
4037
|
+
response,
|
|
4038
|
+
session,
|
|
4039
|
+
identityManager,
|
|
4040
|
+
masterKey
|
|
4041
|
+
);
|
|
4042
|
+
if ("error" in result) {
|
|
4043
|
+
session.state = "failed";
|
|
4044
|
+
auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id, void 0, "failure");
|
|
4045
|
+
return toolResult({ error: result.error });
|
|
4046
|
+
}
|
|
4047
|
+
session.state = "completed";
|
|
4048
|
+
session.their_shr = response.shr;
|
|
4049
|
+
session.their_nonce = response.responder_nonce;
|
|
4050
|
+
session.result = result.result;
|
|
4051
|
+
auditLog.append("l4", "handshake_complete", session.our_shr.body.instance_id);
|
|
4052
|
+
return toolResult({
|
|
4053
|
+
completion: result.completion,
|
|
4054
|
+
result: result.result,
|
|
4055
|
+
instructions: "Send the 'completion' object to the responder so they can verify the handshake. The 'result' object contains the verified counterparty status and trust tier."
|
|
4056
|
+
});
|
|
4057
|
+
}
|
|
4058
|
+
},
|
|
4059
|
+
{
|
|
4060
|
+
name: "sanctuary/handshake_status",
|
|
4061
|
+
description: "Check the status of a handshake session, or verify a completion message (responder side).",
|
|
4062
|
+
inputSchema: {
|
|
4063
|
+
type: "object",
|
|
4064
|
+
properties: {
|
|
4065
|
+
session_id: {
|
|
4066
|
+
type: "string",
|
|
4067
|
+
description: "Session ID to check."
|
|
4068
|
+
},
|
|
4069
|
+
completion: {
|
|
4070
|
+
type: "object",
|
|
4071
|
+
description: "Optional: HandshakeCompletion from the initiator (responder-side verification)."
|
|
4072
|
+
}
|
|
4073
|
+
},
|
|
4074
|
+
required: ["session_id"]
|
|
4075
|
+
},
|
|
4076
|
+
handler: async (args) => {
|
|
4077
|
+
const sessionId = args.session_id;
|
|
4078
|
+
const completion = args.completion;
|
|
4079
|
+
const session = sessions.get(sessionId);
|
|
4080
|
+
if (!session) {
|
|
4081
|
+
return toolResult({ error: `No handshake session found: ${sessionId}` });
|
|
4082
|
+
}
|
|
4083
|
+
if (completion && session.role === "responder" && session.state === "responded") {
|
|
4084
|
+
const result = verifyCompletion(completion, session);
|
|
4085
|
+
session.state = result.verified ? "completed" : "failed";
|
|
4086
|
+
session.result = result;
|
|
4087
|
+
auditLog.append(
|
|
4088
|
+
"l4",
|
|
4089
|
+
"handshake_verify_completion",
|
|
4090
|
+
session.our_shr.body.instance_id,
|
|
4091
|
+
void 0,
|
|
4092
|
+
result.verified ? "success" : "failure"
|
|
4093
|
+
);
|
|
4094
|
+
return toolResult({ result });
|
|
4095
|
+
}
|
|
4096
|
+
return toolResult({
|
|
4097
|
+
session_id: session.session_id,
|
|
4098
|
+
role: session.role,
|
|
4099
|
+
state: session.state,
|
|
4100
|
+
initiated_at: session.initiated_at,
|
|
4101
|
+
result: session.result ?? null
|
|
4102
|
+
});
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
];
|
|
4106
|
+
return { tools };
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4109
|
+
// src/index.ts
|
|
4110
|
+
init_encoding();
|
|
4111
|
+
|
|
4112
|
+
// src/storage/memory.ts
|
|
4113
|
+
var MemoryStorage = class {
|
|
4114
|
+
store = /* @__PURE__ */ new Map();
|
|
4115
|
+
storageKey(namespace, key) {
|
|
4116
|
+
return `${namespace}/${key}`;
|
|
4117
|
+
}
|
|
4118
|
+
async write(namespace, key, data) {
|
|
4119
|
+
this.store.set(this.storageKey(namespace, key), {
|
|
4120
|
+
data: new Uint8Array(data),
|
|
4121
|
+
// Copy to prevent external mutation
|
|
4122
|
+
modified_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4123
|
+
});
|
|
4124
|
+
}
|
|
4125
|
+
async read(namespace, key) {
|
|
4126
|
+
const entry = this.store.get(this.storageKey(namespace, key));
|
|
4127
|
+
if (!entry) return null;
|
|
4128
|
+
return new Uint8Array(entry.data);
|
|
4129
|
+
}
|
|
4130
|
+
async delete(namespace, key, _secureOverwrite) {
|
|
4131
|
+
return this.store.delete(this.storageKey(namespace, key));
|
|
4132
|
+
}
|
|
4133
|
+
async list(namespace, prefix) {
|
|
4134
|
+
const entries = [];
|
|
4135
|
+
const nsPrefix = `${namespace}/`;
|
|
4136
|
+
for (const [storeKey, entry] of this.store) {
|
|
4137
|
+
if (!storeKey.startsWith(nsPrefix)) continue;
|
|
4138
|
+
const key = storeKey.slice(nsPrefix.length);
|
|
4139
|
+
if (prefix && !key.startsWith(prefix)) continue;
|
|
4140
|
+
entries.push({
|
|
4141
|
+
key,
|
|
4142
|
+
namespace,
|
|
4143
|
+
size_bytes: entry.data.length,
|
|
4144
|
+
modified_at: entry.modified_at
|
|
4145
|
+
});
|
|
4146
|
+
}
|
|
4147
|
+
return entries.sort((a, b) => a.key.localeCompare(b.key));
|
|
4148
|
+
}
|
|
4149
|
+
async exists(namespace, key) {
|
|
4150
|
+
return this.store.has(this.storageKey(namespace, key));
|
|
4151
|
+
}
|
|
4152
|
+
async totalSize() {
|
|
4153
|
+
let total = 0;
|
|
4154
|
+
for (const entry of this.store.values()) {
|
|
4155
|
+
total += entry.data.length;
|
|
4156
|
+
}
|
|
4157
|
+
return total;
|
|
4158
|
+
}
|
|
4159
|
+
/** Clear all stored data (useful in tests) */
|
|
4160
|
+
clear() {
|
|
4161
|
+
this.store.clear();
|
|
4162
|
+
}
|
|
4163
|
+
};
|
|
4164
|
+
|
|
4165
|
+
// src/index.ts
|
|
4166
|
+
async function createSanctuaryServer(options) {
|
|
4167
|
+
const config = await loadConfig(options?.configPath);
|
|
4168
|
+
await mkdir(config.storage_path, { recursive: true, mode: 448 });
|
|
4169
|
+
const storage = options?.storage ?? new FilesystemStorage(
|
|
4170
|
+
`${config.storage_path}/state`
|
|
4171
|
+
);
|
|
4172
|
+
let masterKey;
|
|
4173
|
+
let keyProtection;
|
|
4174
|
+
let recoveryKey;
|
|
4175
|
+
const passphrase = options?.passphrase ?? process.env.SANCTUARY_PASSPHRASE;
|
|
4176
|
+
if (passphrase) {
|
|
4177
|
+
keyProtection = "passphrase";
|
|
4178
|
+
let existingParams;
|
|
4179
|
+
try {
|
|
4180
|
+
const raw = await storage.read("_meta", "key-params");
|
|
4181
|
+
if (raw) {
|
|
4182
|
+
const { bytesToString: bytesToString2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
|
|
4183
|
+
existingParams = JSON.parse(bytesToString2(raw));
|
|
4184
|
+
}
|
|
4185
|
+
} catch {
|
|
4186
|
+
}
|
|
4187
|
+
const result = await deriveMasterKey(passphrase, existingParams);
|
|
4188
|
+
masterKey = result.key;
|
|
4189
|
+
if (!existingParams) {
|
|
4190
|
+
const { stringToBytes: stringToBytes2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
|
|
4191
|
+
await storage.write(
|
|
4192
|
+
"_meta",
|
|
4193
|
+
"key-params",
|
|
4194
|
+
stringToBytes2(JSON.stringify(result.params))
|
|
4195
|
+
);
|
|
4196
|
+
}
|
|
4197
|
+
} else {
|
|
4198
|
+
keyProtection = "recovery-key";
|
|
4199
|
+
const existing = await storage.read("_meta", "recovery-key-hash");
|
|
4200
|
+
if (existing) {
|
|
4201
|
+
masterKey = generateRandomKey();
|
|
4202
|
+
recoveryKey = toBase64url(masterKey);
|
|
4203
|
+
} else {
|
|
4204
|
+
masterKey = generateRandomKey();
|
|
4205
|
+
recoveryKey = toBase64url(masterKey);
|
|
4206
|
+
const { hashToString: hashToString2 } = await Promise.resolve().then(() => (init_hashing(), hashing_exports));
|
|
4207
|
+
const { stringToBytes: stringToBytes2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
|
|
4208
|
+
const keyHash = hashToString2(masterKey);
|
|
4209
|
+
await storage.write(
|
|
4210
|
+
"_meta",
|
|
4211
|
+
"recovery-key-hash",
|
|
4212
|
+
stringToBytes2(keyHash)
|
|
4213
|
+
);
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
const auditLog = new AuditLog(storage, masterKey);
|
|
4217
|
+
const stateStore = new StateStore(storage, masterKey);
|
|
4218
|
+
const { tools: l1Tools, identityManager } = createL1Tools(
|
|
4219
|
+
stateStore,
|
|
4220
|
+
storage,
|
|
4221
|
+
masterKey,
|
|
4222
|
+
keyProtection,
|
|
4223
|
+
auditLog
|
|
4224
|
+
);
|
|
4225
|
+
await identityManager.load();
|
|
4226
|
+
const l2Tools = [
|
|
4227
|
+
{
|
|
4228
|
+
name: "sanctuary/exec_attest",
|
|
4229
|
+
description: "Generate an attestation of the current execution environment, including sovereignty assessment and degradation report.",
|
|
4230
|
+
inputSchema: {
|
|
4231
|
+
type: "object",
|
|
4232
|
+
properties: {
|
|
4233
|
+
include_hardware: { type: "boolean", default: true },
|
|
4234
|
+
include_software: { type: "boolean", default: true },
|
|
4235
|
+
include_network: { type: "boolean", default: true }
|
|
4236
|
+
}
|
|
4237
|
+
},
|
|
4238
|
+
handler: async () => {
|
|
4239
|
+
const degradations = [];
|
|
4240
|
+
degradations.push(
|
|
4241
|
+
"L2 isolation is process-level only; no TEE available"
|
|
4242
|
+
);
|
|
4243
|
+
if (config.disclosure.proof_system === "commitment-only") {
|
|
4244
|
+
degradations.push(
|
|
4245
|
+
"L3 proofs are commitment-based only; ZK proofs not yet available"
|
|
4246
|
+
);
|
|
4247
|
+
}
|
|
4248
|
+
return toolResult({
|
|
4249
|
+
attestation: {
|
|
4250
|
+
environment_type: config.execution.environment,
|
|
4251
|
+
hardware: {
|
|
4252
|
+
cpu_vendor: process.arch,
|
|
4253
|
+
tee_available: false,
|
|
4254
|
+
tee_type: void 0
|
|
4255
|
+
},
|
|
4256
|
+
software: {
|
|
4257
|
+
os: `${process.platform}-${process.arch}`,
|
|
4258
|
+
runtime: `node-${process.version}`,
|
|
4259
|
+
sanctuary_version: config.version,
|
|
4260
|
+
mcp_sdk_version: "1.26.0"
|
|
4261
|
+
},
|
|
4262
|
+
network: {
|
|
4263
|
+
internet_accessible: true,
|
|
4264
|
+
// Conservative assumption
|
|
4265
|
+
listening_ports: [],
|
|
4266
|
+
egress_restricted: false
|
|
4267
|
+
},
|
|
4268
|
+
isolation_level: "process",
|
|
4269
|
+
sovereignty_assessment: {
|
|
4270
|
+
l1_state_encrypted: true,
|
|
4271
|
+
l2_execution_isolated: false,
|
|
4272
|
+
l2_isolation_type: "process-level",
|
|
4273
|
+
l3_proofs_available: config.disclosure.proof_system !== "commitment-only",
|
|
4274
|
+
l4_reputation_active: true,
|
|
4275
|
+
overall_level: "mvs",
|
|
4276
|
+
degradations
|
|
4277
|
+
}
|
|
4278
|
+
},
|
|
4279
|
+
attested_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4280
|
+
});
|
|
4281
|
+
}
|
|
4282
|
+
},
|
|
4283
|
+
{
|
|
4284
|
+
name: "sanctuary/monitor_health",
|
|
4285
|
+
description: "Sanctuary Health Report (SHR) \u2014 standardized sovereignty status.",
|
|
4286
|
+
inputSchema: { type: "object", properties: {} },
|
|
4287
|
+
handler: async () => {
|
|
4288
|
+
const storageSizeBytes = await storage.totalSize();
|
|
4289
|
+
const degradations = [];
|
|
4290
|
+
degradations.push({
|
|
4291
|
+
layer: "l2",
|
|
4292
|
+
description: "Process-level isolation only (no TEE)",
|
|
4293
|
+
severity: "warning",
|
|
4294
|
+
mitigation: "TEE support planned for v0.3.0"
|
|
4295
|
+
});
|
|
4296
|
+
if (config.disclosure.proof_system === "commitment-only") {
|
|
4297
|
+
degradations.push({
|
|
4298
|
+
layer: "l3",
|
|
4299
|
+
description: "Commitment schemes only (no ZK proofs)",
|
|
4300
|
+
severity: "info",
|
|
4301
|
+
mitigation: "ZK proof support planned for v0.2.0"
|
|
4302
|
+
});
|
|
4303
|
+
}
|
|
4304
|
+
return toolResult({
|
|
4305
|
+
status: degradations.some((d) => d.severity === "critical") ? "compromised" : degradations.some((d) => d.severity === "warning") ? "degraded" : "healthy",
|
|
4306
|
+
storage_bytes: storageSizeBytes,
|
|
4307
|
+
layers: {
|
|
4308
|
+
l1: {
|
|
4309
|
+
status: "active",
|
|
4310
|
+
encryption_algorithm: "aes-256-gcm",
|
|
4311
|
+
key_count: identityManager.list().length,
|
|
4312
|
+
state_integrity: "verified",
|
|
4313
|
+
last_integrity_check: (/* @__PURE__ */ new Date()).toISOString()
|
|
4314
|
+
},
|
|
4315
|
+
l2: {
|
|
4316
|
+
status: "degraded",
|
|
4317
|
+
isolation_type: "process-level",
|
|
4318
|
+
attestation_available: true,
|
|
4319
|
+
last_attestation: (/* @__PURE__ */ new Date()).toISOString()
|
|
4320
|
+
},
|
|
4321
|
+
l3: {
|
|
4322
|
+
status: config.disclosure.proof_system === "commitment-only" ? "degraded" : "active",
|
|
4323
|
+
proof_system: config.disclosure.proof_system,
|
|
4324
|
+
circuits_loaded: 0,
|
|
4325
|
+
proofs_generated_total: 0
|
|
4326
|
+
},
|
|
4327
|
+
l4: {
|
|
4328
|
+
status: "active",
|
|
4329
|
+
mode: config.reputation.mode,
|
|
4330
|
+
interaction_count: 0,
|
|
4331
|
+
// TODO: track from reputation store
|
|
4332
|
+
reputation_exportable: true
|
|
4333
|
+
}
|
|
4334
|
+
},
|
|
4335
|
+
degradations,
|
|
4336
|
+
checked_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4337
|
+
});
|
|
4338
|
+
}
|
|
4339
|
+
},
|
|
4340
|
+
{
|
|
4341
|
+
name: "sanctuary/monitor_audit_log",
|
|
4342
|
+
description: "Query the sovereignty audit log.",
|
|
4343
|
+
inputSchema: {
|
|
4344
|
+
type: "object",
|
|
4345
|
+
properties: {
|
|
4346
|
+
since: { type: "string", description: "ISO 8601 timestamp" },
|
|
4347
|
+
layer: {
|
|
4348
|
+
type: "string",
|
|
4349
|
+
enum: ["l1", "l2", "l3", "l4"]
|
|
4350
|
+
},
|
|
4351
|
+
operation_type: { type: "string" },
|
|
4352
|
+
limit: { type: "number", default: 50 }
|
|
4353
|
+
}
|
|
4354
|
+
},
|
|
4355
|
+
handler: async (args) => {
|
|
4356
|
+
const result = await auditLog.query({
|
|
4357
|
+
since: args.since,
|
|
4358
|
+
layer: args.layer,
|
|
4359
|
+
operation_type: args.operation_type,
|
|
4360
|
+
limit: args.limit ?? 50
|
|
4361
|
+
});
|
|
4362
|
+
return toolResult(result);
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
];
|
|
4366
|
+
const manifestTool = {
|
|
4367
|
+
name: "sanctuary/manifest",
|
|
4368
|
+
description: "Generate the Sanctuary Interface Manifest (SIM) \u2014 a machine-readable declaration of this server's capabilities.",
|
|
4369
|
+
inputSchema: { type: "object", properties: {} },
|
|
4370
|
+
handler: async () => {
|
|
4371
|
+
return toolResult({
|
|
4372
|
+
sanctuary_version: "0.2",
|
|
4373
|
+
implementation: {
|
|
4374
|
+
name: "@sanctuary-framework/mcp-server",
|
|
4375
|
+
version: config.version,
|
|
4376
|
+
language: "typescript",
|
|
4377
|
+
license: "Apache-2.0"
|
|
4378
|
+
},
|
|
4379
|
+
layers: {
|
|
4380
|
+
l1: {
|
|
4381
|
+
implemented: true,
|
|
4382
|
+
interfaces: ["StateStore", "IdentityRoot"],
|
|
4383
|
+
encryption: ["aes-256-gcm"],
|
|
4384
|
+
identity: ["ed25519"],
|
|
4385
|
+
properties: {
|
|
4386
|
+
"S1.1_participant_held_keys": "full",
|
|
4387
|
+
"S1.2_encryption_at_rest": "full",
|
|
4388
|
+
"S1.3_integrity_verification": "full",
|
|
4389
|
+
"S1.4_selective_state_sharing": "full",
|
|
4390
|
+
"S1.5_state_portability": "full",
|
|
4391
|
+
"S1.6_deletion_rights": "full",
|
|
4392
|
+
"S1.7_identity_anchoring": "partial"
|
|
4393
|
+
}
|
|
4394
|
+
},
|
|
4395
|
+
l2: {
|
|
4396
|
+
implemented: true,
|
|
4397
|
+
interfaces: ["ExecutionEnvironment", "RuntimeMonitor"],
|
|
4398
|
+
isolation_types: [config.execution.environment],
|
|
4399
|
+
properties: {
|
|
4400
|
+
"S2.1_execution_confidentiality": "documented",
|
|
4401
|
+
"S2.2_verifiable_execution": "self-reported",
|
|
4402
|
+
"S2.5_attestation": "self-reported"
|
|
4403
|
+
}
|
|
4404
|
+
},
|
|
4405
|
+
l3: {
|
|
4406
|
+
implemented: true,
|
|
4407
|
+
interfaces: ["ProofEngine", "DisclosurePolicy"],
|
|
4408
|
+
proof_systems: [config.disclosure.proof_system],
|
|
4409
|
+
properties: {
|
|
4410
|
+
"S3.1_minimum_disclosure": "policy-based",
|
|
4411
|
+
"S3.3_proof_without_revelation": "commitment"
|
|
4412
|
+
}
|
|
4413
|
+
},
|
|
4414
|
+
l4: {
|
|
4415
|
+
implemented: true,
|
|
4416
|
+
interfaces: ["ReputationStore", "TrustBootstrap"],
|
|
4417
|
+
modes: [config.reputation.mode],
|
|
4418
|
+
properties: {
|
|
4419
|
+
"S4.1_earned_reputation": "full",
|
|
4420
|
+
"S4.2_participant_owned": "full",
|
|
4421
|
+
"S4.5_sybil_resistance": "basic",
|
|
4422
|
+
"S4.7_trust_bootstrapping": "full"
|
|
4423
|
+
}
|
|
4424
|
+
}
|
|
4425
|
+
},
|
|
4426
|
+
composition: {
|
|
4427
|
+
sim_version: "1.0",
|
|
4428
|
+
spf_supported: false,
|
|
4429
|
+
shr_supported: true,
|
|
4430
|
+
delegation_depth: 1
|
|
4431
|
+
},
|
|
4432
|
+
limitations: [
|
|
4433
|
+
"L1 identity uses ed25519 only; KERI support planned for v0.2.0",
|
|
4434
|
+
"L2 isolation is process-level only; TEE support planned for v0.3.0",
|
|
4435
|
+
"L3 uses commitment schemes only; ZK proofs planned for v0.2.0",
|
|
4436
|
+
"L4 Sybil resistance is escrow-based only",
|
|
4437
|
+
"Spec license: CC-BY-4.0 | Code license: Apache-2.0"
|
|
4438
|
+
]
|
|
4439
|
+
});
|
|
4440
|
+
}
|
|
4441
|
+
};
|
|
4442
|
+
const { tools: l3Tools } = createL3Tools(storage, masterKey, auditLog);
|
|
4443
|
+
const { tools: l4Tools } = createL4Tools(
|
|
4444
|
+
storage,
|
|
4445
|
+
masterKey,
|
|
4446
|
+
identityManager,
|
|
4447
|
+
auditLog
|
|
4448
|
+
);
|
|
4449
|
+
const policy = await loadPrincipalPolicy(config.storage_path);
|
|
4450
|
+
const baseline = new BaselineTracker(storage, masterKey);
|
|
4451
|
+
await baseline.load();
|
|
4452
|
+
const approvalChannel = new StderrApprovalChannel(policy.approval_channel);
|
|
4453
|
+
const gate = new ApprovalGate(policy, baseline, approvalChannel, auditLog);
|
|
4454
|
+
const policyTools = createPrincipalPolicyTools(policy, baseline, auditLog);
|
|
4455
|
+
const { tools: shrTools } = createSHRTools(
|
|
4456
|
+
config,
|
|
4457
|
+
identityManager,
|
|
4458
|
+
masterKey,
|
|
4459
|
+
auditLog
|
|
4460
|
+
);
|
|
4461
|
+
const { tools: handshakeTools } = createHandshakeTools(
|
|
4462
|
+
config,
|
|
4463
|
+
identityManager,
|
|
4464
|
+
masterKey,
|
|
4465
|
+
auditLog
|
|
4466
|
+
);
|
|
4467
|
+
const allTools = [
|
|
4468
|
+
...l1Tools,
|
|
4469
|
+
...l2Tools,
|
|
4470
|
+
...l3Tools,
|
|
4471
|
+
...l4Tools,
|
|
4472
|
+
...policyTools,
|
|
4473
|
+
...shrTools,
|
|
4474
|
+
...handshakeTools,
|
|
4475
|
+
manifestTool
|
|
4476
|
+
];
|
|
4477
|
+
const server = createServer(allTools, { gate });
|
|
4478
|
+
await saveConfig(config);
|
|
4479
|
+
const saveBaseline = () => {
|
|
4480
|
+
baseline.save().catch(() => {
|
|
4481
|
+
});
|
|
4482
|
+
};
|
|
4483
|
+
process.on("SIGINT", saveBaseline);
|
|
4484
|
+
process.on("SIGTERM", saveBaseline);
|
|
4485
|
+
if (recoveryKey) {
|
|
4486
|
+
console.error(
|
|
4487
|
+
`\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
4488
|
+
\u2551 SANCTUARY: First Run \u2014 Recovery Key Generated \u2551
|
|
4489
|
+
\u2551 \u2551
|
|
4490
|
+
\u2551 Recovery Key: ${recoveryKey.slice(0, 20)}... \u2551
|
|
4491
|
+
\u2551 \u2551
|
|
4492
|
+
\u2551 SAVE THIS KEY. It will not be shown again. \u2551
|
|
4493
|
+
\u2551 Without it, your encrypted state is unrecoverable. \u2551
|
|
4494
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D`
|
|
4495
|
+
);
|
|
4496
|
+
}
|
|
4497
|
+
return { server, config };
|
|
4498
|
+
}
|
|
4499
|
+
|
|
4500
|
+
export { ApprovalGate, AuditLog, AutoApproveChannel, BaselineTracker, CallbackApprovalChannel, CommitmentStore, FilesystemStorage, MemoryStorage, PolicyStore, ReputationStore, StateStore, StderrApprovalChannel, completeHandshake, createSanctuaryServer, generateSHR, initiateHandshake, loadConfig, loadPrincipalPolicy, respondToHandshake, verifyCompletion, verifySHR };
|
|
4501
|
+
//# sourceMappingURL=index.js.map
|
|
4502
|
+
//# sourceMappingURL=index.js.map
|