@verax-ai/body 0.2.0 → 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 +11 -0
- package/dist/demo.d.ts +18 -0
- package/dist/demo.js +492 -0
- package/package.json +3 -3
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,8 @@ 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
|
|
23
|
+
[--with-conarium] also fetch Conarium with npx, attach it as a child, and put a masked read through the gate
|
|
21
24
|
approve <args> approve a waiting request from this machine
|
|
22
25
|
operator <args> enrol an operator and manage their passkeys
|
|
23
26
|
reconcile <args> compare the ledger against a statement
|
|
@@ -62,6 +65,14 @@ if (argv[0] === "--version" || argv[0] === "-v") {
|
|
|
62
65
|
process.stdout.write(`${version()}\n`);
|
|
63
66
|
process.exit(0);
|
|
64
67
|
}
|
|
68
|
+
if (argv[0] === "demo") {
|
|
69
|
+
process.exit(await runDemo(argv, process.env, {
|
|
70
|
+
stdout: process.stdout,
|
|
71
|
+
stderr: process.stderr,
|
|
72
|
+
stdin: process.stdin,
|
|
73
|
+
isTTY: Boolean(process.stdin.isTTY),
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
65
76
|
if (argv[0] === "approve") {
|
|
66
77
|
process.exit(await runApprove(argv));
|
|
67
78
|
}
|
package/dist/demo.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
/** 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
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
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 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]";
|
|
24
|
+
const AUDIENCE = "http://127.0.0.1/verax-demo";
|
|
25
|
+
const KID = "demo";
|
|
26
|
+
const TOKEN_TTL_SEC = 5 * 60;
|
|
27
|
+
const DENY_HOST = "blocked.test";
|
|
28
|
+
const EGRESS_HOST = "example.com";
|
|
29
|
+
const SPEND = {
|
|
30
|
+
amountMinor: 100,
|
|
31
|
+
currency: "USD",
|
|
32
|
+
payee: "sample-merchant",
|
|
33
|
+
reference: "demo-1",
|
|
34
|
+
};
|
|
35
|
+
function redact(text, token) {
|
|
36
|
+
if (token === "" || !text.includes(token))
|
|
37
|
+
return text;
|
|
38
|
+
return text.split(token).join("[token]");
|
|
39
|
+
}
|
|
40
|
+
function shippedPolicyPath() {
|
|
41
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
42
|
+
let resolved = null;
|
|
43
|
+
try {
|
|
44
|
+
resolved = join(dirname(fileURLToPath(import.meta.resolve("@verax-ai/proxy/package.json"))), "policy", "default.json");
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
resolved = null;
|
|
48
|
+
}
|
|
49
|
+
const candidates = [
|
|
50
|
+
resolved,
|
|
51
|
+
join(here, "..", "..", "proxy", "policy", "default.json"),
|
|
52
|
+
join(here, "..", "..", "@verax-ai", "proxy", "policy", "default.json"),
|
|
53
|
+
];
|
|
54
|
+
for (const path of candidates) {
|
|
55
|
+
if (path && existsSync(path))
|
|
56
|
+
return path;
|
|
57
|
+
}
|
|
58
|
+
throw new Error("demo: shipped policy file not found");
|
|
59
|
+
}
|
|
60
|
+
/** Shipped default plus message.send egress and a spend rule, written under the temp dir. */
|
|
61
|
+
function writeDemoPolicy(stateDir, withConarium) {
|
|
62
|
+
const shipped = JSON.parse(readFileSync(shippedPolicyPath(), "utf8"));
|
|
63
|
+
const document = {
|
|
64
|
+
...shipped,
|
|
65
|
+
egress: [EGRESS_HOST],
|
|
66
|
+
rules: [
|
|
67
|
+
...shipped.rules,
|
|
68
|
+
{
|
|
69
|
+
id: "message-send",
|
|
70
|
+
tool: "message.send",
|
|
71
|
+
requires: ["verax:memory"],
|
|
72
|
+
egress: true,
|
|
73
|
+
text: "Queueing a message needs the memory scope and a recipient host on the egress list.",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "spend",
|
|
77
|
+
tool: "spend",
|
|
78
|
+
requires: ["verax:pay"],
|
|
79
|
+
mode: "approve",
|
|
80
|
+
text: "A payment is held for an operator.",
|
|
81
|
+
spend: {
|
|
82
|
+
maxAmountMinor: 5000,
|
|
83
|
+
currency: "USD",
|
|
84
|
+
payees: [SPEND.payee],
|
|
85
|
+
dailyMaxMinor: 10_000,
|
|
86
|
+
},
|
|
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
|
+
: []),
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
const path = join(stateDir, "policy.json");
|
|
101
|
+
writeFileSync(path, `${JSON.stringify(document, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
102
|
+
return path;
|
|
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
|
+
}
|
|
116
|
+
function closeHttp(server) {
|
|
117
|
+
if (!server)
|
|
118
|
+
return Promise.resolve();
|
|
119
|
+
return new Promise((resolve) => {
|
|
120
|
+
if (typeof server.closeAllConnections === "function")
|
|
121
|
+
server.closeAllConnections();
|
|
122
|
+
server.close(() => resolve());
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async function serveJwks(jwk) {
|
|
126
|
+
const server = createServer((req, res) => {
|
|
127
|
+
if (req.method === "GET" && (req.url === "/.well-known/jwks.json" || req.url === "/.well-known/jwks.json/")) {
|
|
128
|
+
const body = JSON.stringify({ keys: [jwk] });
|
|
129
|
+
res.writeHead(200, {
|
|
130
|
+
"content-type": "application/json",
|
|
131
|
+
"content-length": Buffer.byteLength(body),
|
|
132
|
+
});
|
|
133
|
+
res.end(body);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
res.writeHead(404);
|
|
137
|
+
res.end();
|
|
138
|
+
});
|
|
139
|
+
await new Promise((resolve, reject) => {
|
|
140
|
+
server.once("error", reject);
|
|
141
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
142
|
+
});
|
|
143
|
+
const addr = server.address();
|
|
144
|
+
if (!addr || typeof addr === "string") {
|
|
145
|
+
server.close();
|
|
146
|
+
throw new Error("demo: jwks bind failed");
|
|
147
|
+
}
|
|
148
|
+
return { server, issuer: `http://127.0.0.1:${addr.port}` };
|
|
149
|
+
}
|
|
150
|
+
async function mintToken(privateKey, issuer, audience) {
|
|
151
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
152
|
+
return new SignJWT({ scope: "verax:read verax:memory verax:pay" })
|
|
153
|
+
.setProtectedHeader({ alg: "ES256", kid: KID })
|
|
154
|
+
.setSubject("demo-brain")
|
|
155
|
+
.setIssuer(issuer)
|
|
156
|
+
.setAudience(audience)
|
|
157
|
+
.setIssuedAt(nowSec)
|
|
158
|
+
.setExpirationTime(nowSec + TOKEN_TTL_SEC)
|
|
159
|
+
.setJti(randomUUID())
|
|
160
|
+
.sign(privateKey);
|
|
161
|
+
}
|
|
162
|
+
function toolText(result) {
|
|
163
|
+
const content = result && typeof result === "object" ? result.content : undefined;
|
|
164
|
+
if (!Array.isArray(content))
|
|
165
|
+
return "";
|
|
166
|
+
return content
|
|
167
|
+
.map((part) => part && typeof part === "object" && "text" in part && typeof part.text === "string"
|
|
168
|
+
? part.text
|
|
169
|
+
: "")
|
|
170
|
+
.filter((s) => s !== "")
|
|
171
|
+
.join("\n");
|
|
172
|
+
}
|
|
173
|
+
function parseTagged(text, kind) {
|
|
174
|
+
const m = new RegExp(`^${kind}:([^:]+):(.+)$`).exec(text.trim());
|
|
175
|
+
return m ? { code: m[1], ref: m[2] } : null;
|
|
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
|
+
}
|
|
220
|
+
async function readAnswer(stdin) {
|
|
221
|
+
const ended = stdin.readableEnded;
|
|
222
|
+
if (ended)
|
|
223
|
+
return "";
|
|
224
|
+
return await new Promise((resolve) => {
|
|
225
|
+
const done = (value) => {
|
|
226
|
+
stdin.off("data", onData);
|
|
227
|
+
stdin.off("end", onEnd);
|
|
228
|
+
stdin.off("error", onErr);
|
|
229
|
+
resolve(value);
|
|
230
|
+
};
|
|
231
|
+
const onData = (chunk) => {
|
|
232
|
+
done(String(chunk).split(/\r?\n/)[0] ?? "");
|
|
233
|
+
};
|
|
234
|
+
const onEnd = () => done("");
|
|
235
|
+
const onErr = () => done("");
|
|
236
|
+
stdin.once("data", onData);
|
|
237
|
+
stdin.once("end", onEnd);
|
|
238
|
+
stdin.once("error", onErr);
|
|
239
|
+
if (typeof stdin.resume === "function")
|
|
240
|
+
stdin.resume();
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
export async function runDemo(argv, env, io, opts) {
|
|
244
|
+
if (env.NODE_ENV === "production") {
|
|
245
|
+
io.stderr.write("demo refuses NODE_ENV=production\n");
|
|
246
|
+
return 1;
|
|
247
|
+
}
|
|
248
|
+
const keep = argv.includes("--keep");
|
|
249
|
+
const withConarium = argv.includes("--with-conarium");
|
|
250
|
+
let stateDir = null;
|
|
251
|
+
let jwks = null;
|
|
252
|
+
let body = null;
|
|
253
|
+
let client = null;
|
|
254
|
+
let token = "";
|
|
255
|
+
let cleaned = false;
|
|
256
|
+
let discardState = false;
|
|
257
|
+
let failed = false;
|
|
258
|
+
const writeOut = (s) => {
|
|
259
|
+
io.stdout.write(redact(s, token));
|
|
260
|
+
};
|
|
261
|
+
const writeErr = (s) => {
|
|
262
|
+
io.stderr.write(redact(s, token));
|
|
263
|
+
};
|
|
264
|
+
const cleanup = async () => {
|
|
265
|
+
if (cleaned)
|
|
266
|
+
return;
|
|
267
|
+
cleaned = true;
|
|
268
|
+
process.off("SIGINT", onStop);
|
|
269
|
+
process.off("SIGTERM", onStop);
|
|
270
|
+
if (client)
|
|
271
|
+
await client.close().catch(() => undefined);
|
|
272
|
+
client = null;
|
|
273
|
+
await closeHttp(body);
|
|
274
|
+
body = null;
|
|
275
|
+
await closeHttp(jwks);
|
|
276
|
+
jwks = null;
|
|
277
|
+
if (stateDir && (!keep || discardState)) {
|
|
278
|
+
try {
|
|
279
|
+
rmSync(stateDir, { recursive: true, force: true });
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
// OS temp cleanup
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
const onStop = () => {
|
|
287
|
+
void cleanup();
|
|
288
|
+
};
|
|
289
|
+
process.once("SIGINT", onStop);
|
|
290
|
+
process.once("SIGTERM", onStop);
|
|
291
|
+
try {
|
|
292
|
+
stateDir = mkdtempSync(join(tmpdir(), "verax-demo-"));
|
|
293
|
+
const policyFile = writeDemoPolicy(stateDir, withConarium);
|
|
294
|
+
const { privateKey, publicKey } = await generateKeyPair("ES256");
|
|
295
|
+
const jwk = { ...(await exportJWK(publicKey)), alg: "ES256", use: "sig", kid: KID };
|
|
296
|
+
const jwksBind = await serveJwks(jwk);
|
|
297
|
+
jwks = jwksBind.server;
|
|
298
|
+
const issuer = jwksBind.issuer;
|
|
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
|
+
}
|
|
308
|
+
body = await listen({
|
|
309
|
+
issuer,
|
|
310
|
+
jwksUrl,
|
|
311
|
+
audience: AUDIENCE,
|
|
312
|
+
stateDir,
|
|
313
|
+
bindHost: "127.0.0.1",
|
|
314
|
+
bindPort: 0,
|
|
315
|
+
policyFile,
|
|
316
|
+
tlsTerminated: false,
|
|
317
|
+
...(downstreamFile !== undefined ? { downstreamFile } : {}),
|
|
318
|
+
});
|
|
319
|
+
const addr = body.address();
|
|
320
|
+
if (!addr || typeof addr === "string")
|
|
321
|
+
throw new Error("demo: body bind failed");
|
|
322
|
+
const mcpUrl = `http://127.0.0.1:${addr.port}/mcp`;
|
|
323
|
+
token = await mintToken(privateKey, issuer, AUDIENCE);
|
|
324
|
+
client = new Client({ name: "verax-demo", version: "0.0.0" });
|
|
325
|
+
await client.connect(new StreamableHTTPClientTransport(new URL(mcpUrl), {
|
|
326
|
+
requestInit: { headers: { authorization: `Bearer ${token}` } },
|
|
327
|
+
}));
|
|
328
|
+
writeOut("verax demo\n");
|
|
329
|
+
writeOut("\n");
|
|
330
|
+
const put = await client.callTool({
|
|
331
|
+
name: "memory.put",
|
|
332
|
+
arguments: {
|
|
333
|
+
id: "demo-note",
|
|
334
|
+
body: { text: "loopback demo" },
|
|
335
|
+
source: { kind: "demo" },
|
|
336
|
+
validUntilMs: Date.now() + 60 * 60 * 1000,
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
if (put.isError)
|
|
340
|
+
throw new Error(`demo: memory.put ${toolText(put)}`);
|
|
341
|
+
const got = await client.callTool({ name: "memory.get", arguments: { id: "demo-note" } });
|
|
342
|
+
if (got.isError)
|
|
343
|
+
throw new Error(`demo: memory.get ${toolText(got)}`);
|
|
344
|
+
writeOut("memory.put / memory.get\n");
|
|
345
|
+
writeOut(" allowed; two signed records\n");
|
|
346
|
+
writeOut("\n");
|
|
347
|
+
const sent = await client.callTool({
|
|
348
|
+
name: "message.send",
|
|
349
|
+
arguments: { to: `ops@${DENY_HOST}`, text: "demo" },
|
|
350
|
+
});
|
|
351
|
+
const denied = parseTagged(toolText(sent), "denied");
|
|
352
|
+
if (!denied)
|
|
353
|
+
throw new Error(`demo: message.send ${toolText(sent)}`);
|
|
354
|
+
writeOut(`message.send -> ops@${DENY_HOST}\n`);
|
|
355
|
+
writeOut(` refused ${denied.code} (signed)\n`);
|
|
356
|
+
writeOut(` ref ${denied.ref}\n`);
|
|
357
|
+
writeOut("\n");
|
|
358
|
+
const spent = await client.callTool({
|
|
359
|
+
name: "spend",
|
|
360
|
+
arguments: { ...SPEND },
|
|
361
|
+
});
|
|
362
|
+
const held = parseTagged(toolText(spent), "deferred");
|
|
363
|
+
if (!held)
|
|
364
|
+
throw new Error(`demo: spend ${toolText(spent)}`);
|
|
365
|
+
writeOut(`spend ${SPEND.amountMinor} minor ${SPEND.currency} ${SPEND.payee}\n`);
|
|
366
|
+
writeOut(" held\n");
|
|
367
|
+
let approved = false;
|
|
368
|
+
if (io.isTTY) {
|
|
369
|
+
writeOut("Approve this payment as the operator on this machine? [y/N]\n");
|
|
370
|
+
const answer = await readAnswer(io.stdin);
|
|
371
|
+
if (answer.trim().toLowerCase() === "y") {
|
|
372
|
+
const code = await runApprove(["approve", stateDir, held.ref], (s) => writeErr(s), () => undefined);
|
|
373
|
+
if (code !== 0)
|
|
374
|
+
throw new Error("demo: approve failed");
|
|
375
|
+
approved = true;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (approved) {
|
|
379
|
+
writeOut(" approved; operator id bound on the record\n");
|
|
380
|
+
}
|
|
381
|
+
else if (io.isTTY) {
|
|
382
|
+
writeOut(" left held\n");
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
writeOut(" no terminal to ask, so it stays held (run this in a terminal to be asked)\n");
|
|
386
|
+
}
|
|
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
|
+
}
|
|
431
|
+
const explained = await client.callTool({
|
|
432
|
+
name: "audit.explain",
|
|
433
|
+
arguments: { ref: denied.ref },
|
|
434
|
+
});
|
|
435
|
+
if (explained.isError)
|
|
436
|
+
throw new Error(`demo: audit.explain ${toolText(explained)}`);
|
|
437
|
+
let findingCode = "none";
|
|
438
|
+
let trust = "unread";
|
|
439
|
+
try {
|
|
440
|
+
const parsed = JSON.parse(toolText(explained));
|
|
441
|
+
if (typeof parsed.finding?.code === "string")
|
|
442
|
+
findingCode = parsed.finding.code;
|
|
443
|
+
if (typeof parsed.trustRoot?.source === "string")
|
|
444
|
+
trust = parsed.trustRoot.source;
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
findingCode = "unparsed";
|
|
448
|
+
}
|
|
449
|
+
writeOut("audit.explain of the refuse\n");
|
|
450
|
+
writeOut(` finding ${findingCode}\n`);
|
|
451
|
+
writeOut(` trust-root ${trust}\n`);
|
|
452
|
+
if (findingCode === "none" && trust !== "unread") {
|
|
453
|
+
writeOut(" chain and signatures read back\n");
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
writeOut(" the read-back did not come out clean; see the finding above\n");
|
|
457
|
+
}
|
|
458
|
+
writeOut("\n");
|
|
459
|
+
await client.close().catch(() => undefined);
|
|
460
|
+
client = null;
|
|
461
|
+
await closeHttp(body);
|
|
462
|
+
body = null;
|
|
463
|
+
await closeHttp(jwks);
|
|
464
|
+
jwks = null;
|
|
465
|
+
const counted = await verifyLedger(stateDir);
|
|
466
|
+
writeOut(`records ${counted.decisions}\n`);
|
|
467
|
+
writeOut(`effects ${counted.effects}\n`);
|
|
468
|
+
if (keep) {
|
|
469
|
+
writeOut(`ledger ${stateDir}\n`);
|
|
470
|
+
writeOut(`verax verify ${stateDir}\n`);
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
writeOut("ledger removed on exit (run with --keep to keep it and check it with verax verify)\n");
|
|
474
|
+
}
|
|
475
|
+
writeOut("\n");
|
|
476
|
+
writeOut(`${withConarium ? NOT_SHOWN_WITH_CONARIUM : NOT_SHOWN}\n`);
|
|
477
|
+
await cleanup();
|
|
478
|
+
return failed ? 1 : 0;
|
|
479
|
+
}
|
|
480
|
+
catch (err) {
|
|
481
|
+
const detail = err instanceof Error ? err.message : "fault";
|
|
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
|
+
}
|
|
489
|
+
await cleanup();
|
|
490
|
+
return 1;
|
|
491
|
+
}
|
|
492
|
+
}
|
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.",
|