@verax-ai/body 0.2.0 → 0.2.1

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
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { runApprove } from "./approve-cli.js";
6
+ import { runDemo } from "./demo.js";
6
7
  import { desktopMain } from "./desktop.js";
7
8
  import { doctorExit, runDoctor } from "./doctor.js";
8
9
  import { runHalt } from "./halt.js";
@@ -18,6 +19,7 @@ Usage: verax <command> [options]
18
19
 
19
20
  (no command) serve MCP over Streamable HTTP at /mcp on VERAX_BIND (default 127.0.0.1:8787)
20
21
  doctor [--json] check the configuration this process would run with
22
+ demo [--keep] run a loopback body against a temporary ledger and print what it recorded
21
23
  approve <args> approve a waiting request from this machine
22
24
  operator <args> enrol an operator and manage their passkeys
23
25
  reconcile <args> compare the ledger against a statement
@@ -62,6 +64,14 @@ if (argv[0] === "--version" || argv[0] === "-v") {
62
64
  process.stdout.write(`${version()}\n`);
63
65
  process.exit(0);
64
66
  }
67
+ if (argv[0] === "demo") {
68
+ process.exit(await runDemo(argv, process.env, {
69
+ stdout: process.stdout,
70
+ stderr: process.stderr,
71
+ stdin: process.stdin,
72
+ isTTY: Boolean(process.stdin.isTTY),
73
+ }));
74
+ }
65
75
  if (argv[0] === "approve") {
66
76
  process.exit(await runApprove(argv));
67
77
  }
package/dist/demo.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ export type DemoIo = {
2
+ stdout: {
3
+ write(s: string): unknown;
4
+ };
5
+ stderr: {
6
+ write(s: string): unknown;
7
+ };
8
+ stdin: NodeJS.ReadableStream;
9
+ isTTY: boolean;
10
+ };
11
+ export declare function runDemo(argv: string[], env: NodeJS.ProcessEnv, io: DemoIo): Promise<number>;
package/dist/demo.js ADDED
@@ -0,0 +1,356 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { createServer } from "node:http";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
8
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
9
+ import { exportJWK, generateKeyPair, SignJWT } from "jose";
10
+ import { verifyLedger } from "@verax-ai/proxy";
11
+ import { runApprove } from "./approve-cli.js";
12
+ import { listen } from "./server.js";
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 AUDIENCE = "http://127.0.0.1/verax-demo";
15
+ const KID = "demo";
16
+ const TOKEN_TTL_SEC = 5 * 60;
17
+ const DENY_HOST = "blocked.test";
18
+ const EGRESS_HOST = "example.com";
19
+ const SPEND = {
20
+ amountMinor: 100,
21
+ currency: "USD",
22
+ payee: "sample-merchant",
23
+ reference: "demo-1",
24
+ };
25
+ function redact(text, token) {
26
+ if (token === "" || !text.includes(token))
27
+ return text;
28
+ return text.split(token).join("[token]");
29
+ }
30
+ function shippedPolicyPath() {
31
+ const here = dirname(fileURLToPath(import.meta.url));
32
+ let resolved = null;
33
+ try {
34
+ resolved = join(dirname(fileURLToPath(import.meta.resolve("@verax-ai/proxy/package.json"))), "policy", "default.json");
35
+ }
36
+ catch {
37
+ resolved = null;
38
+ }
39
+ const candidates = [
40
+ resolved,
41
+ join(here, "..", "..", "proxy", "policy", "default.json"),
42
+ join(here, "..", "..", "@verax-ai", "proxy", "policy", "default.json"),
43
+ ];
44
+ for (const path of candidates) {
45
+ if (path && existsSync(path))
46
+ return path;
47
+ }
48
+ throw new Error("demo: shipped policy file not found");
49
+ }
50
+ /** Shipped default plus message.send egress and a spend rule, written under the temp dir. */
51
+ function writeDemoPolicy(stateDir) {
52
+ const shipped = JSON.parse(readFileSync(shippedPolicyPath(), "utf8"));
53
+ const document = {
54
+ ...shipped,
55
+ egress: [EGRESS_HOST],
56
+ rules: [
57
+ ...shipped.rules,
58
+ {
59
+ id: "message-send",
60
+ tool: "message.send",
61
+ requires: ["verax:memory"],
62
+ egress: true,
63
+ text: "Queueing a message needs the memory scope and a recipient host on the egress list.",
64
+ },
65
+ {
66
+ id: "spend",
67
+ tool: "spend",
68
+ requires: ["verax:pay"],
69
+ mode: "approve",
70
+ text: "A payment is held for an operator.",
71
+ spend: {
72
+ maxAmountMinor: 5000,
73
+ currency: "USD",
74
+ payees: [SPEND.payee],
75
+ dailyMaxMinor: 10_000,
76
+ },
77
+ },
78
+ ],
79
+ };
80
+ const path = join(stateDir, "policy.json");
81
+ writeFileSync(path, `${JSON.stringify(document, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
82
+ return path;
83
+ }
84
+ function closeHttp(server) {
85
+ if (!server)
86
+ return Promise.resolve();
87
+ return new Promise((resolve) => {
88
+ if (typeof server.closeAllConnections === "function")
89
+ server.closeAllConnections();
90
+ server.close(() => resolve());
91
+ });
92
+ }
93
+ async function serveJwks(jwk) {
94
+ const server = createServer((req, res) => {
95
+ if (req.method === "GET" && (req.url === "/.well-known/jwks.json" || req.url === "/.well-known/jwks.json/")) {
96
+ const body = JSON.stringify({ keys: [jwk] });
97
+ res.writeHead(200, {
98
+ "content-type": "application/json",
99
+ "content-length": Buffer.byteLength(body),
100
+ });
101
+ res.end(body);
102
+ return;
103
+ }
104
+ res.writeHead(404);
105
+ res.end();
106
+ });
107
+ await new Promise((resolve, reject) => {
108
+ server.once("error", reject);
109
+ server.listen(0, "127.0.0.1", () => resolve());
110
+ });
111
+ const addr = server.address();
112
+ if (!addr || typeof addr === "string") {
113
+ server.close();
114
+ throw new Error("demo: jwks bind failed");
115
+ }
116
+ return { server, issuer: `http://127.0.0.1:${addr.port}` };
117
+ }
118
+ async function mintToken(privateKey, issuer, audience) {
119
+ const nowSec = Math.floor(Date.now() / 1000);
120
+ return new SignJWT({ scope: "verax:read verax:memory verax:pay" })
121
+ .setProtectedHeader({ alg: "ES256", kid: KID })
122
+ .setSubject("demo-brain")
123
+ .setIssuer(issuer)
124
+ .setAudience(audience)
125
+ .setIssuedAt(nowSec)
126
+ .setExpirationTime(nowSec + TOKEN_TTL_SEC)
127
+ .setJti(randomUUID())
128
+ .sign(privateKey);
129
+ }
130
+ function toolText(result) {
131
+ const content = result && typeof result === "object" ? result.content : undefined;
132
+ if (!Array.isArray(content))
133
+ return "";
134
+ return content
135
+ .map((part) => part && typeof part === "object" && "text" in part && typeof part.text === "string"
136
+ ? part.text
137
+ : "")
138
+ .filter((s) => s !== "")
139
+ .join("\n");
140
+ }
141
+ function parseTagged(text, kind) {
142
+ const m = new RegExp(`^${kind}:([^:]+):(.+)$`).exec(text.trim());
143
+ return m ? { code: m[1], ref: m[2] } : null;
144
+ }
145
+ async function readAnswer(stdin) {
146
+ const ended = stdin.readableEnded;
147
+ if (ended)
148
+ return "";
149
+ return await new Promise((resolve) => {
150
+ const done = (value) => {
151
+ stdin.off("data", onData);
152
+ stdin.off("end", onEnd);
153
+ stdin.off("error", onErr);
154
+ resolve(value);
155
+ };
156
+ const onData = (chunk) => {
157
+ done(String(chunk).split(/\r?\n/)[0] ?? "");
158
+ };
159
+ const onEnd = () => done("");
160
+ const onErr = () => done("");
161
+ stdin.once("data", onData);
162
+ stdin.once("end", onEnd);
163
+ stdin.once("error", onErr);
164
+ if (typeof stdin.resume === "function")
165
+ stdin.resume();
166
+ });
167
+ }
168
+ export async function runDemo(argv, env, io) {
169
+ if (env.NODE_ENV === "production") {
170
+ io.stderr.write("demo refuses NODE_ENV=production\n");
171
+ return 1;
172
+ }
173
+ const keep = argv.includes("--keep");
174
+ let stateDir = null;
175
+ let jwks = null;
176
+ let body = null;
177
+ let client = null;
178
+ let token = "";
179
+ let cleaned = false;
180
+ const writeOut = (s) => {
181
+ io.stdout.write(redact(s, token));
182
+ };
183
+ const writeErr = (s) => {
184
+ io.stderr.write(redact(s, token));
185
+ };
186
+ const cleanup = async () => {
187
+ if (cleaned)
188
+ return;
189
+ cleaned = true;
190
+ process.off("SIGINT", onStop);
191
+ process.off("SIGTERM", onStop);
192
+ if (client)
193
+ await client.close().catch(() => undefined);
194
+ client = null;
195
+ await closeHttp(body);
196
+ body = null;
197
+ await closeHttp(jwks);
198
+ jwks = null;
199
+ if (stateDir && !keep) {
200
+ try {
201
+ rmSync(stateDir, { recursive: true, force: true });
202
+ }
203
+ catch {
204
+ // OS temp cleanup
205
+ }
206
+ }
207
+ };
208
+ const onStop = () => {
209
+ void cleanup();
210
+ };
211
+ process.once("SIGINT", onStop);
212
+ process.once("SIGTERM", onStop);
213
+ try {
214
+ stateDir = mkdtempSync(join(tmpdir(), "verax-demo-"));
215
+ const policyFile = writeDemoPolicy(stateDir);
216
+ const { privateKey, publicKey } = await generateKeyPair("ES256");
217
+ const jwk = { ...(await exportJWK(publicKey)), alg: "ES256", use: "sig", kid: KID };
218
+ const jwksBind = await serveJwks(jwk);
219
+ jwks = jwksBind.server;
220
+ const issuer = jwksBind.issuer;
221
+ const jwksUrl = `${issuer}/.well-known/jwks.json`;
222
+ body = await listen({
223
+ issuer,
224
+ jwksUrl,
225
+ audience: AUDIENCE,
226
+ stateDir,
227
+ bindHost: "127.0.0.1",
228
+ bindPort: 0,
229
+ policyFile,
230
+ tlsTerminated: false,
231
+ });
232
+ const addr = body.address();
233
+ if (!addr || typeof addr === "string")
234
+ throw new Error("demo: body bind failed");
235
+ const mcpUrl = `http://127.0.0.1:${addr.port}/mcp`;
236
+ token = await mintToken(privateKey, issuer, AUDIENCE);
237
+ client = new Client({ name: "verax-demo", version: "0.0.0" });
238
+ await client.connect(new StreamableHTTPClientTransport(new URL(mcpUrl), {
239
+ requestInit: { headers: { authorization: `Bearer ${token}` } },
240
+ }));
241
+ writeOut("verax demo\n");
242
+ writeOut("\n");
243
+ const put = await client.callTool({
244
+ name: "memory.put",
245
+ arguments: {
246
+ id: "demo-note",
247
+ body: { text: "loopback demo" },
248
+ source: { kind: "demo" },
249
+ validUntilMs: Date.now() + 60 * 60 * 1000,
250
+ },
251
+ });
252
+ if (put.isError)
253
+ throw new Error(`demo: memory.put ${toolText(put)}`);
254
+ const got = await client.callTool({ name: "memory.get", arguments: { id: "demo-note" } });
255
+ if (got.isError)
256
+ throw new Error(`demo: memory.get ${toolText(got)}`);
257
+ writeOut("memory.put / memory.get\n");
258
+ writeOut(" allowed; two signed records\n");
259
+ writeOut("\n");
260
+ const sent = await client.callTool({
261
+ name: "message.send",
262
+ arguments: { to: `ops@${DENY_HOST}`, text: "demo" },
263
+ });
264
+ const denied = parseTagged(toolText(sent), "denied");
265
+ if (!denied)
266
+ throw new Error(`demo: message.send ${toolText(sent)}`);
267
+ writeOut(`message.send -> ops@${DENY_HOST}\n`);
268
+ writeOut(` refused ${denied.code} (signed)\n`);
269
+ writeOut(` ref ${denied.ref}\n`);
270
+ writeOut("\n");
271
+ const spent = await client.callTool({
272
+ name: "spend",
273
+ arguments: { ...SPEND },
274
+ });
275
+ const held = parseTagged(toolText(spent), "deferred");
276
+ if (!held)
277
+ throw new Error(`demo: spend ${toolText(spent)}`);
278
+ writeOut(`spend ${SPEND.amountMinor} minor ${SPEND.currency} ${SPEND.payee}\n`);
279
+ writeOut(" held\n");
280
+ let approved = false;
281
+ if (io.isTTY) {
282
+ writeOut("Approve this payment as the operator on this machine? [y/N]\n");
283
+ const answer = await readAnswer(io.stdin);
284
+ if (answer.trim().toLowerCase() === "y") {
285
+ const code = await runApprove(["approve", stateDir, held.ref], (s) => writeErr(s), () => undefined);
286
+ if (code !== 0)
287
+ throw new Error("demo: approve failed");
288
+ approved = true;
289
+ }
290
+ }
291
+ if (approved) {
292
+ writeOut(" approved; operator id bound on the record\n");
293
+ }
294
+ else if (io.isTTY) {
295
+ writeOut(" left held\n");
296
+ }
297
+ else {
298
+ writeOut(" no terminal to ask, so it stays held (run this in a terminal to be asked)\n");
299
+ }
300
+ writeOut("\n");
301
+ const explained = await client.callTool({
302
+ name: "audit.explain",
303
+ arguments: { ref: denied.ref },
304
+ });
305
+ if (explained.isError)
306
+ throw new Error(`demo: audit.explain ${toolText(explained)}`);
307
+ let findingCode = "none";
308
+ let trust = "unread";
309
+ try {
310
+ const parsed = JSON.parse(toolText(explained));
311
+ if (typeof parsed.finding?.code === "string")
312
+ findingCode = parsed.finding.code;
313
+ if (typeof parsed.trustRoot?.source === "string")
314
+ trust = parsed.trustRoot.source;
315
+ }
316
+ catch {
317
+ findingCode = "unparsed";
318
+ }
319
+ writeOut("audit.explain of the refuse\n");
320
+ writeOut(` finding ${findingCode}\n`);
321
+ writeOut(` trust-root ${trust}\n`);
322
+ if (findingCode === "none" && trust !== "unread") {
323
+ writeOut(" chain and signatures read back\n");
324
+ }
325
+ else {
326
+ writeOut(" the read-back did not come out clean; see the finding above\n");
327
+ }
328
+ writeOut("\n");
329
+ await client.close().catch(() => undefined);
330
+ client = null;
331
+ await closeHttp(body);
332
+ body = null;
333
+ await closeHttp(jwks);
334
+ jwks = null;
335
+ const counted = await verifyLedger(stateDir);
336
+ writeOut(`records ${counted.decisions}\n`);
337
+ writeOut(`effects ${counted.effects}\n`);
338
+ if (keep) {
339
+ writeOut(`ledger ${stateDir}\n`);
340
+ writeOut(`verax verify ${stateDir}\n`);
341
+ }
342
+ else {
343
+ writeOut("ledger removed on exit (run with --keep to keep it and check it with verax verify)\n");
344
+ }
345
+ writeOut("\n");
346
+ writeOut(`${NOT_SHOWN}\n`);
347
+ await cleanup();
348
+ return 0;
349
+ }
350
+ catch (err) {
351
+ const detail = err instanceof Error ? err.message : "fault";
352
+ writeErr(`demo: ${detail}\n`);
353
+ await cleanup();
354
+ return 1;
355
+ }
356
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@verax-ai/body",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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.0",
26
- "@verax-ai/proxy": "^0.2.0",
25
+ "@verax-ai/inventory": "^0.2.1",
26
+ "@verax-ai/proxy": "^0.2.1",
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.",