@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 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
- export declare function runDemo(argv: string[], env: NodeJS.ProcessEnv, io: DemoIo): Promise<number>;
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
- writeErr(`demo: ${detail}\n`);
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.1",
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.1",
26
- "@verax-ai/proxy": "^0.2.1",
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.",