@verax-ai/body 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -0
- package/dist/demo.d.ts +8 -1
- package/dist/demo.js +143 -7
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ Usage: verax <command> [options]
|
|
|
20
20
|
(no command) serve MCP over Streamable HTTP at /mcp on VERAX_BIND (default 127.0.0.1:8787)
|
|
21
21
|
doctor [--json] check the configuration this process would run with
|
|
22
22
|
demo [--keep] run a loopback body against a temporary ledger and print what it recorded
|
|
23
|
+
[--with-conarium] also fetch Conarium with npx, attach it as a child, and put a masked read through the gate
|
|
23
24
|
approve <args> approve a waiting request from this machine
|
|
24
25
|
operator <args> enrol an operator and manage their passkeys
|
|
25
26
|
reconcile <args> compare the ledger against a statement
|
package/dist/demo.d.ts
CHANGED
|
@@ -8,4 +8,11 @@ export type DemoIo = {
|
|
|
8
8
|
stdin: NodeJS.ReadableStream;
|
|
9
9
|
isTTY: boolean;
|
|
10
10
|
};
|
|
11
|
-
|
|
11
|
+
/** Test injection. The CLI never passes this; it is not read from argv or env. */
|
|
12
|
+
export type DemoOpts = {
|
|
13
|
+
conariumChild?: {
|
|
14
|
+
command: string;
|
|
15
|
+
args: string[];
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export declare function runDemo(argv: string[], env: NodeJS.ProcessEnv, io: DemoIo, opts?: DemoOpts): Promise<number>;
|
package/dist/demo.js
CHANGED
|
@@ -11,6 +11,16 @@ import { verifyLedger } from "@verax-ai/proxy";
|
|
|
11
11
|
import { runApprove } from "./approve-cli.js";
|
|
12
12
|
import { listen } from "./server.js";
|
|
13
13
|
const NOT_SHOWN = "Not shown here: data masking arrives with a downstream server such as Conarium; statement reconciliation needs a real statement (verax reconcile).";
|
|
14
|
+
const NOT_SHOWN_WITH_CONARIUM = "Not shown here: a real database (these are Conarium's sample rows); statement reconciliation needs a real statement (verax reconcile).";
|
|
15
|
+
const NPX_DOWNLOAD_LINE = "npx will download @conarium-ai/core from npm and run it as a child process; a cold download takes longer.\n";
|
|
16
|
+
const CONARIUM_NPX = {
|
|
17
|
+
command: "npx",
|
|
18
|
+
args: ["-y", "--package=@conarium-ai/core@^0.2.51", "conarium", "--demo"],
|
|
19
|
+
};
|
|
20
|
+
const CONARIUM_TIMEOUT_MS = 120_000;
|
|
21
|
+
const CUSTOMERS_SQL = "SELECT name, email, card FROM public.customers";
|
|
22
|
+
const SECRETS_SQL = "SELECT * FROM public.secrets";
|
|
23
|
+
const MASKED_PII = "[MASKED_PII]";
|
|
14
24
|
const AUDIENCE = "http://127.0.0.1/verax-demo";
|
|
15
25
|
const KID = "demo";
|
|
16
26
|
const TOKEN_TTL_SEC = 5 * 60;
|
|
@@ -48,7 +58,7 @@ function shippedPolicyPath() {
|
|
|
48
58
|
throw new Error("demo: shipped policy file not found");
|
|
49
59
|
}
|
|
50
60
|
/** Shipped default plus message.send egress and a spend rule, written under the temp dir. */
|
|
51
|
-
function writeDemoPolicy(stateDir) {
|
|
61
|
+
function writeDemoPolicy(stateDir, withConarium) {
|
|
52
62
|
const shipped = JSON.parse(readFileSync(shippedPolicyPath(), "utf8"));
|
|
53
63
|
const document = {
|
|
54
64
|
...shipped,
|
|
@@ -75,12 +85,34 @@ function writeDemoPolicy(stateDir) {
|
|
|
75
85
|
dailyMaxMinor: 10_000,
|
|
76
86
|
},
|
|
77
87
|
},
|
|
88
|
+
...(withConarium
|
|
89
|
+
? [
|
|
90
|
+
{
|
|
91
|
+
id: "conarium-query",
|
|
92
|
+
tool: "conarium.query",
|
|
93
|
+
requires: ["verax:read"],
|
|
94
|
+
text: "A data read through Conarium needs the read scope.",
|
|
95
|
+
},
|
|
96
|
+
]
|
|
97
|
+
: []),
|
|
78
98
|
],
|
|
79
99
|
};
|
|
80
100
|
const path = join(stateDir, "policy.json");
|
|
81
101
|
writeFileSync(path, `${JSON.stringify(document, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
82
102
|
return path;
|
|
83
103
|
}
|
|
104
|
+
function writeConariumDownstream(stateDir, child) {
|
|
105
|
+
const path = join(stateDir, "downstream.json");
|
|
106
|
+
const document = {
|
|
107
|
+
prefix: "conarium",
|
|
108
|
+
command: child.command,
|
|
109
|
+
args: [...child.args],
|
|
110
|
+
trust: "same-user",
|
|
111
|
+
timeoutMs: CONARIUM_TIMEOUT_MS,
|
|
112
|
+
};
|
|
113
|
+
writeFileSync(path, `${JSON.stringify(document, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
114
|
+
return path;
|
|
115
|
+
}
|
|
84
116
|
function closeHttp(server) {
|
|
85
117
|
if (!server)
|
|
86
118
|
return Promise.resolve();
|
|
@@ -142,6 +174,49 @@ function parseTagged(text, kind) {
|
|
|
142
174
|
const m = new RegExp(`^${kind}:([^:]+):(.+)$`).exec(text.trim());
|
|
143
175
|
return m ? { code: m[1], ref: m[2] } : null;
|
|
144
176
|
}
|
|
177
|
+
async function callToolCaught(client, name, args) {
|
|
178
|
+
try {
|
|
179
|
+
const result = await client.callTool({ name, arguments: args });
|
|
180
|
+
return { isError: result.isError === true, text: toolText(result) };
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
return { isError: true, text: err instanceof Error ? err.message : "fault" };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function customerRowsMasked(text) {
|
|
187
|
+
let parsed;
|
|
188
|
+
try {
|
|
189
|
+
parsed = JSON.parse(text);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
if (parsed === null || typeof parsed !== "object")
|
|
195
|
+
return false;
|
|
196
|
+
const rows = parsed.rows;
|
|
197
|
+
if (!Array.isArray(rows) || rows.length === 0)
|
|
198
|
+
return false;
|
|
199
|
+
for (const row of rows) {
|
|
200
|
+
if (row === null || typeof row !== "object")
|
|
201
|
+
return false;
|
|
202
|
+
const rec = row;
|
|
203
|
+
if (rec.email !== MASKED_PII || rec.card !== MASKED_PII)
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The body passes on that a downstream tool answered with an error, not what it
|
|
210
|
+
* said: the detail is the fixed word `call-failed`. A child that died or timed
|
|
211
|
+
* out carries the transport's own message there instead, and that is not an
|
|
212
|
+
* answer from Conarium.
|
|
213
|
+
*/
|
|
214
|
+
function childAnsweredWithError(text, tool) {
|
|
215
|
+
return text.trim().endsWith(`downstream-call-failed:${tool}:call-failed`);
|
|
216
|
+
}
|
|
217
|
+
function attachFailedDetail(detail) {
|
|
218
|
+
return /downstream-attach-failed|downstream-attach-timeout/.test(detail);
|
|
219
|
+
}
|
|
145
220
|
async function readAnswer(stdin) {
|
|
146
221
|
const ended = stdin.readableEnded;
|
|
147
222
|
if (ended)
|
|
@@ -165,18 +240,21 @@ async function readAnswer(stdin) {
|
|
|
165
240
|
stdin.resume();
|
|
166
241
|
});
|
|
167
242
|
}
|
|
168
|
-
export async function runDemo(argv, env, io) {
|
|
243
|
+
export async function runDemo(argv, env, io, opts) {
|
|
169
244
|
if (env.NODE_ENV === "production") {
|
|
170
245
|
io.stderr.write("demo refuses NODE_ENV=production\n");
|
|
171
246
|
return 1;
|
|
172
247
|
}
|
|
173
248
|
const keep = argv.includes("--keep");
|
|
249
|
+
const withConarium = argv.includes("--with-conarium");
|
|
174
250
|
let stateDir = null;
|
|
175
251
|
let jwks = null;
|
|
176
252
|
let body = null;
|
|
177
253
|
let client = null;
|
|
178
254
|
let token = "";
|
|
179
255
|
let cleaned = false;
|
|
256
|
+
let discardState = false;
|
|
257
|
+
let failed = false;
|
|
180
258
|
const writeOut = (s) => {
|
|
181
259
|
io.stdout.write(redact(s, token));
|
|
182
260
|
};
|
|
@@ -196,7 +274,7 @@ export async function runDemo(argv, env, io) {
|
|
|
196
274
|
body = null;
|
|
197
275
|
await closeHttp(jwks);
|
|
198
276
|
jwks = null;
|
|
199
|
-
if (stateDir && !keep) {
|
|
277
|
+
if (stateDir && (!keep || discardState)) {
|
|
200
278
|
try {
|
|
201
279
|
rmSync(stateDir, { recursive: true, force: true });
|
|
202
280
|
}
|
|
@@ -212,13 +290,21 @@ export async function runDemo(argv, env, io) {
|
|
|
212
290
|
process.once("SIGTERM", onStop);
|
|
213
291
|
try {
|
|
214
292
|
stateDir = mkdtempSync(join(tmpdir(), "verax-demo-"));
|
|
215
|
-
const policyFile = writeDemoPolicy(stateDir);
|
|
293
|
+
const policyFile = writeDemoPolicy(stateDir, withConarium);
|
|
216
294
|
const { privateKey, publicKey } = await generateKeyPair("ES256");
|
|
217
295
|
const jwk = { ...(await exportJWK(publicKey)), alg: "ES256", use: "sig", kid: KID };
|
|
218
296
|
const jwksBind = await serveJwks(jwk);
|
|
219
297
|
jwks = jwksBind.server;
|
|
220
298
|
const issuer = jwksBind.issuer;
|
|
221
299
|
const jwksUrl = `${issuer}/.well-known/jwks.json`;
|
|
300
|
+
let downstreamFile;
|
|
301
|
+
if (withConarium) {
|
|
302
|
+
const child = opts?.conariumChild ?? CONARIUM_NPX;
|
|
303
|
+
downstreamFile = writeConariumDownstream(stateDir, child);
|
|
304
|
+
if (!opts?.conariumChild) {
|
|
305
|
+
writeErr(NPX_DOWNLOAD_LINE);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
222
308
|
body = await listen({
|
|
223
309
|
issuer,
|
|
224
310
|
jwksUrl,
|
|
@@ -228,6 +314,7 @@ export async function runDemo(argv, env, io) {
|
|
|
228
314
|
bindPort: 0,
|
|
229
315
|
policyFile,
|
|
230
316
|
tlsTerminated: false,
|
|
317
|
+
...(downstreamFile !== undefined ? { downstreamFile } : {}),
|
|
231
318
|
});
|
|
232
319
|
const addr = body.address();
|
|
233
320
|
if (!addr || typeof addr === "string")
|
|
@@ -298,6 +385,49 @@ export async function runDemo(argv, env, io) {
|
|
|
298
385
|
writeOut(" no terminal to ask, so it stays held (run this in a terminal to be asked)\n");
|
|
299
386
|
}
|
|
300
387
|
writeOut("\n");
|
|
388
|
+
if (withConarium) {
|
|
389
|
+
const customers = await callToolCaught(client, "conarium.query", { sql: CUSTOMERS_SQL });
|
|
390
|
+
if (customers.isError)
|
|
391
|
+
throw new Error(`demo: conarium.query customers ${customers.text}`);
|
|
392
|
+
writeOut("conarium.query customers\n");
|
|
393
|
+
if (customerRowsMasked(customers.text)) {
|
|
394
|
+
writeOut(" allowed; masked by Conarium before the rows left it\n");
|
|
395
|
+
writeOut(" measured [MASKED_PII] on every email and card\n");
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
writeOut(" allowed; did not come out masked\n");
|
|
399
|
+
failed = true;
|
|
400
|
+
}
|
|
401
|
+
writeOut("\n");
|
|
402
|
+
const secrets = await callToolCaught(client, "conarium.query", { sql: SECRETS_SQL });
|
|
403
|
+
const secretsDenied = parseTagged(secrets.text, "denied");
|
|
404
|
+
writeOut("conarium.query public.secrets\n");
|
|
405
|
+
if (secretsDenied) {
|
|
406
|
+
writeOut(` refused ${secretsDenied.code} (signed)\n`);
|
|
407
|
+
failed = true;
|
|
408
|
+
}
|
|
409
|
+
else if (childAnsweredWithError(secrets.text, "conarium.query")) {
|
|
410
|
+
writeOut(" allowed by this gate; Conarium answered with an error and no rows\n");
|
|
411
|
+
writeOut(" recorded as a failed call\n");
|
|
412
|
+
}
|
|
413
|
+
else if (secrets.isError) {
|
|
414
|
+
writeOut(" allowed by this gate; the call to Conarium failed before it answered\n");
|
|
415
|
+
failed = true;
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
writeOut(" allowed; Conarium did not refuse public.secrets\n");
|
|
419
|
+
failed = true;
|
|
420
|
+
}
|
|
421
|
+
writeOut("\n");
|
|
422
|
+
const listed = await callToolCaught(client, "conarium.list_tables", {});
|
|
423
|
+
const listedDenied = parseTagged(listed.text, "denied");
|
|
424
|
+
writeOut("conarium.list_tables\n");
|
|
425
|
+
if (!listedDenied)
|
|
426
|
+
throw new Error(`demo: conarium.list_tables ${listed.text}`);
|
|
427
|
+
writeOut(` refused ${listedDenied.code} (signed)\n`);
|
|
428
|
+
writeOut(" refused by this gate; the downstream server never saw the call\n");
|
|
429
|
+
writeOut("\n");
|
|
430
|
+
}
|
|
301
431
|
const explained = await client.callTool({
|
|
302
432
|
name: "audit.explain",
|
|
303
433
|
arguments: { ref: denied.ref },
|
|
@@ -343,13 +473,19 @@ export async function runDemo(argv, env, io) {
|
|
|
343
473
|
writeOut("ledger removed on exit (run with --keep to keep it and check it with verax verify)\n");
|
|
344
474
|
}
|
|
345
475
|
writeOut("\n");
|
|
346
|
-
writeOut(`${NOT_SHOWN}\n`);
|
|
476
|
+
writeOut(`${withConarium ? NOT_SHOWN_WITH_CONARIUM : NOT_SHOWN}\n`);
|
|
347
477
|
await cleanup();
|
|
348
|
-
return 0;
|
|
478
|
+
return failed ? 1 : 0;
|
|
349
479
|
}
|
|
350
480
|
catch (err) {
|
|
351
481
|
const detail = err instanceof Error ? err.message : "fault";
|
|
352
|
-
|
|
482
|
+
if (withConarium && attachFailedDetail(detail)) {
|
|
483
|
+
discardState = true;
|
|
484
|
+
writeErr(`could not start the Conarium child (${detail}). run without --with-conarium\n`);
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
writeErr(`demo: ${detail}\n`);
|
|
488
|
+
}
|
|
353
489
|
await cleanup();
|
|
354
490
|
return 1;
|
|
355
491
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@verax-ai/body",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
25
|
-
"@verax-ai/inventory": "^0.2.
|
|
26
|
-
"@verax-ai/proxy": "^0.2.
|
|
25
|
+
"@verax-ai/inventory": "^0.2.2",
|
|
26
|
+
"@verax-ai/proxy": "^0.2.2",
|
|
27
27
|
"jose": "^6.2.11"
|
|
28
28
|
},
|
|
29
29
|
"description": "VERAX body: an MCP server that decides, records and proves what an agent did.",
|