@dorigjo/besa 0.1.0-alpha.2 → 0.1.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +310 -337
- package/dist/admit.d.ts +13 -2
- package/dist/admit.js +186 -26
- package/dist/crypto.d.ts +4 -1
- package/dist/crypto.js +127 -19
- package/dist/grant.js +50 -5
- package/dist/index.js +418 -57
- package/dist/io.d.ts +5 -0
- package/dist/io.js +97 -0
- package/dist/keystore.d.ts +16 -0
- package/dist/keystore.js +117 -0
- package/dist/manifest.js +83 -17
- package/dist/sdk.d.ts +2 -0
- package/dist/sdk.js +2 -0
- package/dist/signing.d.ts +16 -2
- package/dist/signing.js +317 -31
- package/dist/trust.d.ts +17 -0
- package/dist/trust.js +466 -0
- package/dist/types.d.ts +25 -0
- package/examples/request.json +3 -0
- package/package.json +66 -57
- package/scripts/postinstall.mjs +30 -0
package/dist/index.js
CHANGED
|
@@ -1,34 +1,149 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync,
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import {
|
|
2
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { generateKeyPair, publicKeyId, validateKeyPair, } from "./crypto.js";
|
|
5
6
|
import { loadManifest } from "./manifest.js";
|
|
6
|
-
import { createReceipt, signManifest, verifySignedManifest } from "./signing.js";
|
|
7
|
-
import { admit,
|
|
7
|
+
import { createReceipt, hashRequest, signManifest, validateReceipt, validateSignedManifest, verifyReceiptDetailed, verifySignedManifest, } from "./signing.js";
|
|
8
|
+
import { admit, admitAndConsume, getCount, loadMeter, meterKey, } from "./admit.js";
|
|
8
9
|
import { checkGrant, loadGrants } from "./grant.js";
|
|
10
|
+
import { addTrustAnchor, applyKeyRotation, checkTrustedKey, createKeyRotation, emptyTrustStore, revokeTrustAnchor, validateTrustStore, verifyKeyRotation, verifyTrustedSignedManifest, } from "./trust.js";
|
|
11
|
+
import { readJsonFile, writeJsonAtomic, writeJsonExclusive, } from "./io.js";
|
|
12
|
+
import { isStoredKeyPair, openKeyPair, sealKeyPair, } from "./keystore.js";
|
|
9
13
|
const BESA_DIR = ".besa";
|
|
10
14
|
const KEY_PATH = join(BESA_DIR, "key.json");
|
|
15
|
+
const KEYS_DIR = join(BESA_DIR, "keys");
|
|
11
16
|
const METER_PATH = join(BESA_DIR, "meter.json");
|
|
12
17
|
const ACTIVE_MANIFEST_PATH = join(BESA_DIR, "active-manifest.json");
|
|
13
18
|
const RECEIPTS_DIR = join(BESA_DIR, "receipts");
|
|
19
|
+
const ROTATIONS_DIR = join(BESA_DIR, "rotations");
|
|
20
|
+
const TRUST_PATH = join(BESA_DIR, "trust.json");
|
|
21
|
+
const FLAGS_WITH_VALUES = new Set([
|
|
22
|
+
"--agent",
|
|
23
|
+
"--grants",
|
|
24
|
+
"--request",
|
|
25
|
+
"--trust",
|
|
26
|
+
]);
|
|
27
|
+
const COMMAND_FLAGS = {
|
|
28
|
+
keys: new Set(["--trust"]),
|
|
29
|
+
trust: new Set(["--trust"]),
|
|
30
|
+
load: new Set(),
|
|
31
|
+
sign: new Set(["--trust"]),
|
|
32
|
+
verify: new Set(["--trust"]),
|
|
33
|
+
admit: new Set(["--trust", "--agent", "--grants"]),
|
|
34
|
+
receipt: new Set([
|
|
35
|
+
"--trust",
|
|
36
|
+
"--request",
|
|
37
|
+
"--agent",
|
|
38
|
+
"--grants",
|
|
39
|
+
]),
|
|
40
|
+
"verify-receipt": new Set(["--trust"]),
|
|
41
|
+
};
|
|
14
42
|
function readJson(path) {
|
|
15
|
-
return
|
|
43
|
+
return readJsonFile(path);
|
|
16
44
|
}
|
|
17
|
-
function writeJson(path, value) {
|
|
18
|
-
|
|
45
|
+
function writeJson(path, value, mode) {
|
|
46
|
+
writeJsonAtomic(path, value, mode ?? 0o600);
|
|
19
47
|
}
|
|
20
48
|
function ensureBesaDir() {
|
|
21
|
-
mkdirSync(BESA_DIR, { recursive: true });
|
|
49
|
+
mkdirSync(BESA_DIR, { recursive: true, mode: 0o700 });
|
|
50
|
+
const stats = lstatSync(BESA_DIR);
|
|
51
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
52
|
+
throw new Error(`${BESA_DIR} must be a real private directory, not a link`);
|
|
53
|
+
}
|
|
54
|
+
if (process.platform !== "win32")
|
|
55
|
+
chmodSync(BESA_DIR, 0o700);
|
|
56
|
+
}
|
|
57
|
+
function protectKeyFile(path = KEY_PATH) {
|
|
58
|
+
if (lstatSync(path).isSymbolicLink()) {
|
|
59
|
+
throw new Error(`refusing to use symbolic-link key file at ${path}`);
|
|
60
|
+
}
|
|
61
|
+
if (process.platform !== "win32")
|
|
62
|
+
chmodSync(path, 0o600);
|
|
63
|
+
}
|
|
64
|
+
function keyPassphrase() {
|
|
65
|
+
const passphrase = process.env.BESA_KEY_PASSPHRASE;
|
|
66
|
+
if (!passphrase) {
|
|
67
|
+
throw new Error("BESA_KEY_PASSPHRASE is required and must contain at least 16 UTF-8 bytes");
|
|
68
|
+
}
|
|
69
|
+
return passphrase;
|
|
70
|
+
}
|
|
71
|
+
function loadExistingKeyPair() {
|
|
72
|
+
if (!existsSync(KEY_PATH)) {
|
|
73
|
+
throw new Error(`no signing key found at ${KEY_PATH}; run besa keys first`);
|
|
74
|
+
}
|
|
75
|
+
ensureBesaDir();
|
|
76
|
+
protectKeyFile();
|
|
77
|
+
const stored = readJson(KEY_PATH);
|
|
78
|
+
const passphrase = keyPassphrase();
|
|
79
|
+
if (isStoredKeyPair(stored)) {
|
|
80
|
+
return openKeyPair(stored, passphrase);
|
|
81
|
+
}
|
|
82
|
+
if (!validateKeyPair(stored)) {
|
|
83
|
+
throw new Error(`invalid or mismatched Ed25519 key pair at ${KEY_PATH}`);
|
|
84
|
+
}
|
|
85
|
+
writeJson(KEY_PATH, sealKeyPair(stored, passphrase), 0o600);
|
|
86
|
+
return stored;
|
|
22
87
|
}
|
|
23
88
|
function loadOrCreateKeyPair() {
|
|
24
89
|
ensureBesaDir();
|
|
25
90
|
if (existsSync(KEY_PATH)) {
|
|
26
|
-
return
|
|
91
|
+
return loadExistingKeyPair();
|
|
27
92
|
}
|
|
28
93
|
const keypair = generateKeyPair();
|
|
29
|
-
|
|
94
|
+
try {
|
|
95
|
+
writeJsonExclusive(KEY_PATH, sealKeyPair(keypair, keyPassphrase()), 0o600);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error.code === "EEXIST") {
|
|
99
|
+
return loadExistingKeyPair();
|
|
100
|
+
}
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
protectKeyFile();
|
|
30
104
|
return keypair;
|
|
31
105
|
}
|
|
106
|
+
function terminalText(value) {
|
|
107
|
+
return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, "?");
|
|
108
|
+
}
|
|
109
|
+
function selectedTrustPath() {
|
|
110
|
+
const path = flagValue("--trust") ?? TRUST_PATH;
|
|
111
|
+
if (!path.endsWith(".json")) {
|
|
112
|
+
throw new Error(`trust store path must end in .json: ${terminalText(path)}`);
|
|
113
|
+
}
|
|
114
|
+
return path;
|
|
115
|
+
}
|
|
116
|
+
function loadTrustStore(path = selectedTrustPath()) {
|
|
117
|
+
if (!existsSync(path)) {
|
|
118
|
+
throw new Error(`no trust store found at ${path}; run besa trust add <signed-manifest> first`);
|
|
119
|
+
}
|
|
120
|
+
if (lstatSync(path).isSymbolicLink()) {
|
|
121
|
+
throw new Error(`refusing to use symbolic-link trust store at ${path}`);
|
|
122
|
+
}
|
|
123
|
+
const validation = validateTrustStore(readJson(path));
|
|
124
|
+
if (!validation.ok || !validation.trustStore) {
|
|
125
|
+
throw new Error(`invalid trust store at ${path}:\n - ${validation.errors.join("\n - ")}`);
|
|
126
|
+
}
|
|
127
|
+
return validation.trustStore;
|
|
128
|
+
}
|
|
129
|
+
function loadOrCreateTrustStore(path = selectedTrustPath()) {
|
|
130
|
+
return existsSync(path) ? loadTrustStore(path) : emptyTrustStore();
|
|
131
|
+
}
|
|
132
|
+
function saveTrustStore(store, path = selectedTrustPath()) {
|
|
133
|
+
if (existsSync(path) && lstatSync(path).isSymbolicLink()) {
|
|
134
|
+
throw new Error(`refusing to write to symbolic-link trust store at ${path}`);
|
|
135
|
+
}
|
|
136
|
+
const validation = validateTrustStore(store);
|
|
137
|
+
if (!validation.ok) {
|
|
138
|
+
throw new Error(`refusing to save invalid trust store: ${validation.errors.join("; ")}`);
|
|
139
|
+
}
|
|
140
|
+
writeJson(path, store, 0o600);
|
|
141
|
+
}
|
|
142
|
+
function trustSignedManifestKey(signed) {
|
|
143
|
+
const path = selectedTrustPath();
|
|
144
|
+
const store = addTrustAnchor(loadOrCreateTrustStore(path), signed.publicKey);
|
|
145
|
+
saveTrustStore(store, path);
|
|
146
|
+
}
|
|
32
147
|
function printJson(label, value) {
|
|
33
148
|
console.log("");
|
|
34
149
|
console.log(label + ":");
|
|
@@ -48,14 +163,35 @@ function signedOutPath(manifestPath) {
|
|
|
48
163
|
}
|
|
49
164
|
function flagValue(name) {
|
|
50
165
|
const index = process.argv.indexOf(name);
|
|
51
|
-
|
|
166
|
+
if (index < 0) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
const value = process.argv[index + 1];
|
|
170
|
+
if (!value || value.startsWith("--")) {
|
|
171
|
+
throw new Error(`${name} requires a value`);
|
|
172
|
+
}
|
|
173
|
+
return value;
|
|
52
174
|
}
|
|
53
|
-
function positionals(args) {
|
|
175
|
+
function positionals(args, allowedFlags) {
|
|
54
176
|
const values = [];
|
|
55
|
-
const
|
|
177
|
+
const seenFlags = new Set();
|
|
56
178
|
for (let index = 0; index < args.length; index += 1) {
|
|
57
179
|
const value = args[index];
|
|
58
|
-
if (value
|
|
180
|
+
if (value?.startsWith("-")) {
|
|
181
|
+
if (!FLAGS_WITH_VALUES.has(value)) {
|
|
182
|
+
throw new Error(`unknown flag '${value}'`);
|
|
183
|
+
}
|
|
184
|
+
if (!allowedFlags.has(value)) {
|
|
185
|
+
throw new Error(`flag '${value}' is not supported by this command`);
|
|
186
|
+
}
|
|
187
|
+
if (seenFlags.has(value)) {
|
|
188
|
+
throw new Error(`duplicate flag '${value}'`);
|
|
189
|
+
}
|
|
190
|
+
const flagArgument = args[index + 1];
|
|
191
|
+
if (!flagArgument || flagArgument.startsWith("--")) {
|
|
192
|
+
throw new Error(`${value} requires a value`);
|
|
193
|
+
}
|
|
194
|
+
seenFlags.add(value);
|
|
59
195
|
index += 1;
|
|
60
196
|
continue;
|
|
61
197
|
}
|
|
@@ -65,7 +201,52 @@ function positionals(args) {
|
|
|
65
201
|
}
|
|
66
202
|
return values;
|
|
67
203
|
}
|
|
68
|
-
function
|
|
204
|
+
function requireSignedManifest(value) {
|
|
205
|
+
const validation = validateSignedManifest(value);
|
|
206
|
+
if (!validation.ok || !validation.signedManifest) {
|
|
207
|
+
throw new Error(`invalid signed manifest:\n - ${validation.errors.join("\n - ")}`);
|
|
208
|
+
}
|
|
209
|
+
return validation.signedManifest;
|
|
210
|
+
}
|
|
211
|
+
function cmdRotateKeys() {
|
|
212
|
+
const previous = loadExistingKeyPair();
|
|
213
|
+
const next = generateKeyPair();
|
|
214
|
+
const rotation = createKeyRotation(previous, next);
|
|
215
|
+
const previousId = rotation.previousPublicKeyId;
|
|
216
|
+
const archivePath = join(KEYS_DIR, `${previousId}.json`);
|
|
217
|
+
const rotationPath = join(ROTATIONS_DIR, `${previousId}-to-${rotation.newPublicKeyId}.json`);
|
|
218
|
+
const path = selectedTrustPath();
|
|
219
|
+
const anchored = addTrustAnchor(loadOrCreateTrustStore(path), previous.publicKeyDer);
|
|
220
|
+
const rotatedStore = applyKeyRotation(anchored, rotation);
|
|
221
|
+
const passphrase = keyPassphrase();
|
|
222
|
+
// Pre-compute all crypto before touching the filesystem.
|
|
223
|
+
// If scrypt or key derivation throws, no files are written.
|
|
224
|
+
const sealedPrevious = sealKeyPair(previous, passphrase);
|
|
225
|
+
const sealedNext = sealKeyPair(next, passphrase);
|
|
226
|
+
writeJson(archivePath, sealedPrevious, 0o600);
|
|
227
|
+
protectKeyFile(archivePath);
|
|
228
|
+
writeJson(rotationPath, rotation);
|
|
229
|
+
writeJson(KEY_PATH, sealedNext, 0o600);
|
|
230
|
+
protectKeyFile();
|
|
231
|
+
saveTrustStore(rotatedStore, path);
|
|
232
|
+
printJson("keyRotation", rotation);
|
|
233
|
+
console.log("");
|
|
234
|
+
console.log(`OK: active key rotated to ${rotation.newPublicKeyId}`);
|
|
235
|
+
console.log(`OK: previous private key archived at ${terminalText(archivePath)}`);
|
|
236
|
+
console.log(`OK: rotation proof written to ${terminalText(rotationPath)}`);
|
|
237
|
+
console.log("NEXT: re-sign active manifests with the new key");
|
|
238
|
+
}
|
|
239
|
+
function cmdKeys(action) {
|
|
240
|
+
if (action === "rotate") {
|
|
241
|
+
cmdRotateKeys();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (action) {
|
|
245
|
+
throw new Error(`unknown keys action '${action}'`);
|
|
246
|
+
}
|
|
247
|
+
if (flagValue("--trust")) {
|
|
248
|
+
throw new Error("--trust is only supported by keys rotate");
|
|
249
|
+
}
|
|
69
250
|
const keypair = loadOrCreateKeyPair();
|
|
70
251
|
printJson("keypair", {
|
|
71
252
|
publicKeyDer: keypair.publicKeyDer,
|
|
@@ -74,11 +255,77 @@ function cmdKeys() {
|
|
|
74
255
|
console.log("");
|
|
75
256
|
console.log("OK: keypair ready at " + KEY_PATH);
|
|
76
257
|
}
|
|
258
|
+
function cmdTrustAdd(file) {
|
|
259
|
+
const raw = readJson(file);
|
|
260
|
+
const verification = verifySignedManifest(raw);
|
|
261
|
+
if (!verification.valid) {
|
|
262
|
+
throw new Error(`${verification.reasonCode}: ${verification.detail}`);
|
|
263
|
+
}
|
|
264
|
+
const signed = requireSignedManifest(raw);
|
|
265
|
+
const path = selectedTrustPath();
|
|
266
|
+
const store = addTrustAnchor(loadOrCreateTrustStore(path), signed.publicKey);
|
|
267
|
+
saveTrustStore(store, path);
|
|
268
|
+
console.log(`OK: trusted public key ${signed.publicKeyId} in ${terminalText(path)}`);
|
|
269
|
+
}
|
|
270
|
+
function cmdTrustApply(file) {
|
|
271
|
+
const rotation = readJson(file);
|
|
272
|
+
const verification = verifyKeyRotation(rotation);
|
|
273
|
+
if (!verification.valid) {
|
|
274
|
+
throw new Error(`${verification.reasonCode}: ${verification.detail}`);
|
|
275
|
+
}
|
|
276
|
+
const path = selectedTrustPath();
|
|
277
|
+
const store = applyKeyRotation(loadTrustStore(path), rotation);
|
|
278
|
+
saveTrustStore(store, path);
|
|
279
|
+
console.log(`OK: retired ${rotation.previousPublicKeyId} and trusted ${rotation.newPublicKeyId}`);
|
|
280
|
+
}
|
|
281
|
+
function cmdTrustRevoke(keyId) {
|
|
282
|
+
const path = selectedTrustPath();
|
|
283
|
+
const store = revokeTrustAnchor(loadTrustStore(path), keyId);
|
|
284
|
+
saveTrustStore(store, path);
|
|
285
|
+
console.log(`OK: revoked public key ${keyId} in ${terminalText(path)}`);
|
|
286
|
+
}
|
|
287
|
+
function cmdTrustList() {
|
|
288
|
+
const path = selectedTrustPath();
|
|
289
|
+
const store = loadTrustStore(path);
|
|
290
|
+
printJson("trustStore", store);
|
|
291
|
+
console.log("");
|
|
292
|
+
console.log(`OK: loaded ${String(store.keys.length)} trust anchor(s) from ${terminalText(path)}`);
|
|
293
|
+
}
|
|
294
|
+
function cmdTrust(action, value) {
|
|
295
|
+
switch (action) {
|
|
296
|
+
case "add":
|
|
297
|
+
if (!value) {
|
|
298
|
+
throw new Error("trust add requires a signed manifest path");
|
|
299
|
+
}
|
|
300
|
+
cmdTrustAdd(value);
|
|
301
|
+
break;
|
|
302
|
+
case "apply":
|
|
303
|
+
if (!value) {
|
|
304
|
+
throw new Error("trust apply requires a key rotation path");
|
|
305
|
+
}
|
|
306
|
+
cmdTrustApply(value);
|
|
307
|
+
break;
|
|
308
|
+
case "revoke":
|
|
309
|
+
if (!value) {
|
|
310
|
+
throw new Error("trust revoke requires a public key id");
|
|
311
|
+
}
|
|
312
|
+
cmdTrustRevoke(value);
|
|
313
|
+
break;
|
|
314
|
+
case "list":
|
|
315
|
+
if (value) {
|
|
316
|
+
throw new Error("trust list does not accept a positional value");
|
|
317
|
+
}
|
|
318
|
+
cmdTrustList();
|
|
319
|
+
break;
|
|
320
|
+
default:
|
|
321
|
+
throw new Error(`unknown trust action '${action}'`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
77
324
|
function cmdLoad(file) {
|
|
78
325
|
const manifest = loadManifest(file);
|
|
79
326
|
printJson("manifest", manifest);
|
|
80
327
|
console.log("");
|
|
81
|
-
console.log("OK: loaded " + String(manifest.tools.length) + " tool(s) from " + file);
|
|
328
|
+
console.log("OK: loaded " + String(manifest.tools.length) + " tool(s) from " + terminalText(file));
|
|
82
329
|
}
|
|
83
330
|
function cmdSign(file) {
|
|
84
331
|
const manifest = loadManifest(file);
|
|
@@ -88,13 +335,15 @@ function cmdSign(file) {
|
|
|
88
335
|
writeJson(out, signed);
|
|
89
336
|
ensureBesaDir();
|
|
90
337
|
writeJson(ACTIVE_MANIFEST_PATH, signed);
|
|
338
|
+
trustSignedManifestKey(signed);
|
|
91
339
|
printJson("signedManifest", signed);
|
|
92
340
|
console.log("");
|
|
93
|
-
console.log("OK: signed -> " + out + " with publicKeyId " + signed.publicKeyId);
|
|
341
|
+
console.log("OK: signed -> " + terminalText(out) + " with publicKeyId " + signed.publicKeyId);
|
|
342
|
+
console.log("OK: public key anchored in " + terminalText(selectedTrustPath()));
|
|
94
343
|
}
|
|
95
344
|
function cmdVerify(file) {
|
|
96
345
|
const signed = readJson(file);
|
|
97
|
-
const result =
|
|
346
|
+
const result = verifyTrustedSignedManifest(signed, loadTrustStore());
|
|
98
347
|
printJson("verify", result);
|
|
99
348
|
if (!result.valid) {
|
|
100
349
|
process.exitCode = 1;
|
|
@@ -115,11 +364,14 @@ function denyFromVerification(toolName, reasonCode, detail) {
|
|
|
115
364
|
}
|
|
116
365
|
export function grantGate(toolName) {
|
|
117
366
|
const grantsPath = flagValue("--grants");
|
|
367
|
+
const agentId = flagValue("--agent");
|
|
368
|
+
if (Boolean(grantsPath) !== Boolean(agentId)) {
|
|
369
|
+
throw new Error("--agent and --grants must be provided together");
|
|
370
|
+
}
|
|
118
371
|
if (!grantsPath) {
|
|
119
372
|
return undefined;
|
|
120
373
|
}
|
|
121
|
-
const
|
|
122
|
-
const grant = checkGrant(loadGrants(grantsPath), agentId, toolName);
|
|
374
|
+
const grant = checkGrant(loadGrants(grantsPath), agentId ?? "", toolName);
|
|
123
375
|
return {
|
|
124
376
|
decision: grant.granted ? "allow" : "deny",
|
|
125
377
|
reasonCode: grant.reasonCode,
|
|
@@ -129,14 +381,15 @@ export function grantGate(toolName) {
|
|
|
129
381
|
};
|
|
130
382
|
}
|
|
131
383
|
function cmdAdmit(file, toolName) {
|
|
132
|
-
const
|
|
133
|
-
const verified =
|
|
384
|
+
const raw = readJson(file);
|
|
385
|
+
const verified = verifyTrustedSignedManifest(raw, loadTrustStore(), "admit");
|
|
134
386
|
if (!verified.valid) {
|
|
135
387
|
const denied = denyFromVerification(toolName, verified.reasonCode, verified.detail);
|
|
136
388
|
printJson("admission", denied);
|
|
137
389
|
process.exitCode = 1;
|
|
138
390
|
return;
|
|
139
391
|
}
|
|
392
|
+
const signed = requireSignedManifest(raw);
|
|
140
393
|
const grantDecision = grantGate(toolName);
|
|
141
394
|
if (grantDecision && grantDecision.decision === "deny") {
|
|
142
395
|
printJson("admission", grantDecision);
|
|
@@ -144,24 +397,37 @@ function cmdAdmit(file, toolName) {
|
|
|
144
397
|
return;
|
|
145
398
|
}
|
|
146
399
|
const meter = loadMeter(METER_PATH);
|
|
147
|
-
const
|
|
148
|
-
const decision = admit(signed.manifest, toolName,
|
|
400
|
+
const key = meterKey(signed.manifestHash, toolName);
|
|
401
|
+
const decision = admit(signed.manifest, toolName, getCount(meter, key));
|
|
149
402
|
if (grantDecision?.agentId) {
|
|
150
403
|
decision.agentId = grantDecision.agentId;
|
|
151
404
|
}
|
|
152
405
|
printJson("admission", decision);
|
|
406
|
+
console.log("");
|
|
407
|
+
console.log("[dry-run: budget not consumed — use 'besa receipt' to enforce and record]");
|
|
153
408
|
if (decision.decision === "deny") {
|
|
154
409
|
process.exitCode = 1;
|
|
155
410
|
}
|
|
156
411
|
}
|
|
412
|
+
function readRequest(toolName) {
|
|
413
|
+
const requestPath = flagValue("--request");
|
|
414
|
+
return requestPath ? readJson(requestPath) : { toolName };
|
|
415
|
+
}
|
|
157
416
|
function cmdReceipt(toolName, file) {
|
|
158
417
|
const signedPath = file ?? ACTIVE_MANIFEST_PATH;
|
|
159
418
|
if (!existsSync(signedPath)) {
|
|
160
|
-
throw new Error("no signed manifest found at " +
|
|
419
|
+
throw new Error("no signed manifest found at " +
|
|
420
|
+
signedPath +
|
|
421
|
+
"; run besa sign <manifest> first");
|
|
161
422
|
}
|
|
162
|
-
const signed = readJson(signedPath);
|
|
163
|
-
const keypair =
|
|
164
|
-
const
|
|
423
|
+
const signed = requireSignedManifest(readJson(signedPath));
|
|
424
|
+
const keypair = loadExistingKeyPair();
|
|
425
|
+
const request = readRequest(toolName);
|
|
426
|
+
void hashRequest(request);
|
|
427
|
+
if (publicKeyId(keypair.publicKeyDer) !== signed.publicKeyId) {
|
|
428
|
+
throw new Error("local receipt key does not match the signed manifest publicKeyId");
|
|
429
|
+
}
|
|
430
|
+
const verified = verifyTrustedSignedManifest(signed, loadTrustStore(), "admit");
|
|
165
431
|
let decision;
|
|
166
432
|
let grantReasonCode;
|
|
167
433
|
if (!verified.valid) {
|
|
@@ -174,14 +440,10 @@ function cmdReceipt(toolName, file) {
|
|
|
174
440
|
decision = grantDecision;
|
|
175
441
|
}
|
|
176
442
|
else {
|
|
177
|
-
|
|
178
|
-
decision = admit(signed.manifest, toolName, getCount(meter, toolName));
|
|
443
|
+
decision = admitAndConsume(METER_PATH, signed.manifestHash, signed.manifest, toolName);
|
|
179
444
|
if (grantDecision?.agentId) {
|
|
180
445
|
decision.agentId = grantDecision.agentId;
|
|
181
446
|
}
|
|
182
|
-
if (decision.decision === "allow") {
|
|
183
|
-
saveMeter(METER_PATH, increment(meter, toolName));
|
|
184
|
-
}
|
|
185
447
|
}
|
|
186
448
|
}
|
|
187
449
|
const receipt = createReceipt({
|
|
@@ -189,10 +451,7 @@ function cmdReceipt(toolName, file) {
|
|
|
189
451
|
toolName,
|
|
190
452
|
decision: decision.decision,
|
|
191
453
|
reasonCode: decision.reasonCode,
|
|
192
|
-
request
|
|
193
|
-
toolName,
|
|
194
|
-
signedManifest: signedPath,
|
|
195
|
-
},
|
|
454
|
+
request,
|
|
196
455
|
agentId: decision.agentId,
|
|
197
456
|
grantReasonCode,
|
|
198
457
|
}, keypair);
|
|
@@ -201,47 +460,140 @@ function cmdReceipt(toolName, file) {
|
|
|
201
460
|
writeJson(receiptPath, receipt);
|
|
202
461
|
printJson("receipt", receipt);
|
|
203
462
|
console.log("");
|
|
204
|
-
console.log(decision.decision.toUpperCase() +
|
|
463
|
+
console.log(decision.decision.toUpperCase() +
|
|
464
|
+
": " +
|
|
465
|
+
decision.reasonCode +
|
|
466
|
+
" -> " +
|
|
467
|
+
receiptPath);
|
|
205
468
|
if (decision.decision === "deny") {
|
|
206
469
|
process.exitCode = 1;
|
|
207
470
|
}
|
|
208
471
|
}
|
|
472
|
+
function cmdVerifyReceipt(receiptFile, manifestFile) {
|
|
473
|
+
const signedPath = manifestFile ?? ACTIVE_MANIFEST_PATH;
|
|
474
|
+
if (!existsSync(signedPath)) {
|
|
475
|
+
throw new Error("no signed manifest found at " +
|
|
476
|
+
signedPath +
|
|
477
|
+
"; provide one or run besa sign <manifest> first");
|
|
478
|
+
}
|
|
479
|
+
const signedRaw = readJson(signedPath);
|
|
480
|
+
const trustStore = loadTrustStore();
|
|
481
|
+
const manifestVerification = verifyTrustedSignedManifest(signedRaw, trustStore);
|
|
482
|
+
if (!manifestVerification.valid) {
|
|
483
|
+
printJson("verifyReceipt", manifestVerification);
|
|
484
|
+
process.exitCode = 1;
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const signed = requireSignedManifest(signedRaw);
|
|
488
|
+
const receiptRaw = readJson(receiptFile);
|
|
489
|
+
const receiptValidation = validateReceipt(receiptRaw);
|
|
490
|
+
if (!receiptValidation.ok || !receiptValidation.receipt) {
|
|
491
|
+
const result = {
|
|
492
|
+
valid: false,
|
|
493
|
+
reasonCode: "E_RECEIPT_INVALID",
|
|
494
|
+
detail: receiptValidation.errors.join("; "),
|
|
495
|
+
};
|
|
496
|
+
printJson("verifyReceipt", result);
|
|
497
|
+
process.exitCode = 1;
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const receipt = receiptValidation.receipt;
|
|
501
|
+
if (receipt.manifestHash !== signed.manifestHash) {
|
|
502
|
+
const result = {
|
|
503
|
+
valid: false,
|
|
504
|
+
reasonCode: "E_RECEIPT_MANIFEST_MISMATCH",
|
|
505
|
+
detail: "receipt manifestHash does not match the signed manifest",
|
|
506
|
+
};
|
|
507
|
+
printJson("verifyReceipt", result);
|
|
508
|
+
process.exitCode = 1;
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const signatureResult = verifyReceiptDetailed(receipt, signed.publicKey);
|
|
512
|
+
const result = signatureResult.valid
|
|
513
|
+
? checkTrustedKey(trustStore, signed.publicKey, receipt.timestamp)
|
|
514
|
+
: signatureResult;
|
|
515
|
+
printJson("verifyReceipt", result);
|
|
516
|
+
if (!result.valid) {
|
|
517
|
+
process.exitCode = 1;
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
console.log("");
|
|
521
|
+
console.log("OK: receipt and signed manifest form a valid trust chain");
|
|
522
|
+
}
|
|
523
|
+
function readVersion() {
|
|
524
|
+
try {
|
|
525
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
526
|
+
const pkg = readJsonFile(join(packageRoot, "package.json"));
|
|
527
|
+
return typeof pkg.version === "string" ? pkg.version : "unknown";
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
return "unknown";
|
|
531
|
+
}
|
|
532
|
+
}
|
|
209
533
|
function usage() {
|
|
210
534
|
console.log([
|
|
211
535
|
"Besa - signed trust infrastructure for AI-agent tools",
|
|
212
536
|
"",
|
|
213
537
|
"Usage:",
|
|
214
|
-
" besa
|
|
215
|
-
" besa
|
|
216
|
-
"
|
|
217
|
-
"
|
|
218
|
-
"
|
|
219
|
-
"
|
|
538
|
+
" besa <command> [arguments] [options]",
|
|
539
|
+
" besa --help | --version",
|
|
540
|
+
"",
|
|
541
|
+
"Commands:",
|
|
542
|
+
" keys Show the local signing key, generating one if absent",
|
|
543
|
+
" keys rotate Rotate the signing key and emit a signed rotation proof",
|
|
544
|
+
" trust add Anchor a signed manifest's public key in a trust store",
|
|
545
|
+
" trust apply Apply a signed rotation proof to a trust store",
|
|
546
|
+
" trust revoke Revoke a public key in a trust store",
|
|
547
|
+
" trust list List trusted, retired, and revoked keys",
|
|
548
|
+
" load Load and validate a manifest (YAML or JSON)",
|
|
549
|
+
" sign Sign a manifest and anchor the publisher key",
|
|
550
|
+
" verify Verify a signed manifest against a trust store",
|
|
551
|
+
" admit Check whether a tool call is allowed (dry-run)",
|
|
552
|
+
" receipt Enforce budget and issue a signed execution receipt",
|
|
553
|
+
" verify-receipt Verify a receipt and its manifest trust chain",
|
|
554
|
+
"",
|
|
555
|
+
"Options:",
|
|
556
|
+
" --trust <file> Trust store path (default: .besa/trust.json)",
|
|
557
|
+
" --agent <id> Scope admission to a named agent (admit, receipt)",
|
|
558
|
+
" --grants <file> Grant set for agent-scoped admission (admit, receipt)",
|
|
559
|
+
" --request <file> Request payload hashed into the receipt (receipt)",
|
|
220
560
|
"",
|
|
221
561
|
"Examples:",
|
|
222
562
|
" besa keys",
|
|
223
|
-
" besa load examples/manifest.yaml",
|
|
224
563
|
" besa sign examples/manifest.yaml",
|
|
564
|
+
" besa trust add examples/manifest.signed.json --trust consumer-trust.json",
|
|
225
565
|
" besa verify examples/manifest.signed.json",
|
|
226
566
|
" besa admit examples/manifest.signed.json crm.lookup",
|
|
227
567
|
" besa admit examples/manifest.signed.json crm.lookup --agent agent-alpha --grants examples/grants.yaml",
|
|
228
|
-
" besa
|
|
229
|
-
" besa receipt
|
|
230
|
-
"
|
|
568
|
+
" besa receipt crm.lookup examples/manifest.signed.json --request examples/request.json",
|
|
569
|
+
" besa verify-receipt .besa/receipts/<receipt-id>.json examples/manifest.signed.json",
|
|
570
|
+
"",
|
|
571
|
+
"Security:",
|
|
572
|
+
" Local developer beta. Private keys are encrypted at rest (AES-256-GCM + scrypt).",
|
|
573
|
+
" Never commit the .besa/ directory. Not hardened for production use yet.",
|
|
231
574
|
].join("\n"));
|
|
232
575
|
}
|
|
233
|
-
function requireArgs(args,
|
|
234
|
-
if (args.length <
|
|
235
|
-
|
|
576
|
+
function requireArgs(args, minimum, command, maximum = minimum) {
|
|
577
|
+
if (args.length < minimum || args.length > maximum) {
|
|
578
|
+
const expected = minimum === maximum
|
|
579
|
+
? String(minimum)
|
|
580
|
+
: `${String(minimum)}-${String(maximum)}`;
|
|
581
|
+
throw new Error(`${command} requires ${expected} argument(s), received ${String(args.length)}`);
|
|
236
582
|
}
|
|
237
583
|
}
|
|
238
584
|
function main(argv) {
|
|
239
585
|
const command = argv[0] ?? "";
|
|
240
|
-
const args = positionals(argv.slice(1));
|
|
241
586
|
try {
|
|
587
|
+
const allowedFlags = COMMAND_FLAGS[command] ?? new Set();
|
|
588
|
+
const args = positionals(argv.slice(1), allowedFlags);
|
|
242
589
|
switch (command) {
|
|
243
590
|
case "keys":
|
|
244
|
-
|
|
591
|
+
requireArgs(args, 0, command, 1);
|
|
592
|
+
cmdKeys(args[0]);
|
|
593
|
+
break;
|
|
594
|
+
case "trust":
|
|
595
|
+
requireArgs(args, 1, command, 2);
|
|
596
|
+
cmdTrust(args[0] ?? "", args[1]);
|
|
245
597
|
break;
|
|
246
598
|
case "load":
|
|
247
599
|
requireArgs(args, 1, command);
|
|
@@ -260,9 +612,18 @@ function main(argv) {
|
|
|
260
612
|
cmdAdmit(args[0] ?? "", args[1] ?? "");
|
|
261
613
|
break;
|
|
262
614
|
case "receipt":
|
|
263
|
-
requireArgs(args, 1, command);
|
|
615
|
+
requireArgs(args, 1, command, 2);
|
|
264
616
|
cmdReceipt(args[0] ?? "", args[1]);
|
|
265
617
|
break;
|
|
618
|
+
case "verify-receipt":
|
|
619
|
+
requireArgs(args, 1, command, 2);
|
|
620
|
+
cmdVerifyReceipt(args[0] ?? "", args[1]);
|
|
621
|
+
break;
|
|
622
|
+
case "version":
|
|
623
|
+
case "--version":
|
|
624
|
+
case "-v":
|
|
625
|
+
console.log("besa " + readVersion());
|
|
626
|
+
break;
|
|
266
627
|
case "":
|
|
267
628
|
case "help":
|
|
268
629
|
case "--help":
|
|
@@ -270,7 +631,7 @@ function main(argv) {
|
|
|
270
631
|
usage();
|
|
271
632
|
break;
|
|
272
633
|
default:
|
|
273
|
-
console.error("Unknown command: " + command);
|
|
634
|
+
console.error("Unknown command: " + terminalText(command));
|
|
274
635
|
usage();
|
|
275
636
|
process.exitCode = 1;
|
|
276
637
|
break;
|
|
@@ -278,7 +639,7 @@ function main(argv) {
|
|
|
278
639
|
}
|
|
279
640
|
catch (error) {
|
|
280
641
|
const message = error instanceof Error ? error.message : String(error);
|
|
281
|
-
console.error("Error: " + message);
|
|
642
|
+
console.error("Error: " + terminalText(message));
|
|
282
643
|
process.exitCode = 1;
|
|
283
644
|
}
|
|
284
645
|
}
|
package/dist/io.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const MAX_ARTIFACT_BYTES = 1048576;
|
|
2
|
+
export declare function readUtf8File(path: string, maximumBytes?: number): string;
|
|
3
|
+
export declare function readJsonFile(path: string): unknown;
|
|
4
|
+
export declare function writeJsonAtomic(path: string, value: unknown, mode?: number): void;
|
|
5
|
+
export declare function writeJsonExclusive(path: string, value: unknown, mode?: number): void;
|