@verax-ai/body 0.2.1 → 0.3.0
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 +2 -1
- package/dist/auth.d.ts +5 -1
- package/dist/auth.js +27 -3
- package/dist/cli.js +30 -3
- package/dist/config.d.ts +5 -0
- package/dist/config.js +34 -1
- package/dist/demo.d.ts +8 -1
- package/dist/demo.js +143 -7
- package/dist/desktop.d.ts +3 -0
- package/dist/desktop.js +16 -1
- package/dist/doctor.js +63 -5
- package/dist/init-local.d.ts +15 -0
- package/dist/init-local.js +219 -0
- package/dist/main.js +5 -0
- package/dist/server.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -29,7 +29,8 @@ Bearer token the configured issuer did not sign.
|
|
|
29
29
|
| Variable | Required | Meaning |
|
|
30
30
|
| --- | --- | --- |
|
|
31
31
|
| `VERAX_ISSUER` | yes | Issuer the agent's token must carry. |
|
|
32
|
-
| `VERAX_JWKS_URL` |
|
|
32
|
+
| `VERAX_JWKS_URL` | one of | Where the body fetches the keys that verify that token. Set this or `VERAX_JWKS_FILE`, not both. |
|
|
33
|
+
| `VERAX_JWKS_FILE` | one of | Path to a JWKS JSON file read once at start, instead of fetching `VERAX_JWKS_URL`. Accepted only with a loopback bind. |
|
|
33
34
|
| `VERAX_AUDIENCE` | yes | Audience the token must name, so a token minted for something else is refused. |
|
|
34
35
|
| `VERAX_STATE_DIR` | yes | Directory the ledger, keys and approvals live in. It stays on this machine. |
|
|
35
36
|
| `VERAX_POLICY_FILE` | yes | Policy the gate applies. `@verax-ai/proxy` ships `policy/default.json`, which denies what it does not name. |
|
package/dist/auth.d.ts
CHANGED
|
@@ -6,5 +6,9 @@ export type VerifiedBearer = {
|
|
|
6
6
|
};
|
|
7
7
|
export declare function resourceMetadataUrl(audience: string): string;
|
|
8
8
|
export declare function wwwAuthenticate(audience: string): string;
|
|
9
|
-
export declare
|
|
9
|
+
export declare class JwksFileError extends Error {
|
|
10
|
+
readonly code = 78;
|
|
11
|
+
constructor(message?: string);
|
|
12
|
+
}
|
|
13
|
+
export declare function createVerifier(jwksUrl: string, issuer: string, audience: string, jwksFile?: string | null): (token: string) => Promise<VerifiedBearer>;
|
|
10
14
|
export declare function readBearer(header: string | undefined): string | null;
|
package/dist/auth.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { createLocalJWKSet, createRemoteJWKSet, jwtVerify } from "jose";
|
|
2
3
|
const ALGS = ["ES256", "EdDSA"];
|
|
3
4
|
export function resourceMetadataUrl(audience) {
|
|
4
5
|
const u = new URL(audience);
|
|
@@ -11,8 +12,31 @@ export function resourceMetadataUrl(audience) {
|
|
|
11
12
|
export function wwwAuthenticate(audience) {
|
|
12
13
|
return `Bearer resource_metadata="${resourceMetadataUrl(audience)}"`;
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
function localJwks(path) {
|
|
16
|
+
let parsed;
|
|
17
|
+
try {
|
|
18
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new JwksFileError();
|
|
22
|
+
}
|
|
23
|
+
if (parsed === null ||
|
|
24
|
+
typeof parsed !== "object" ||
|
|
25
|
+
!Array.isArray(parsed.keys) ||
|
|
26
|
+
parsed.keys.length === 0) {
|
|
27
|
+
throw new JwksFileError();
|
|
28
|
+
}
|
|
29
|
+
return createLocalJWKSet(parsed);
|
|
30
|
+
}
|
|
31
|
+
export class JwksFileError extends Error {
|
|
32
|
+
code = 78;
|
|
33
|
+
constructor(message = "VERAX_JWKS_FILE is missing or unparsable") {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "JwksFileError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function createVerifier(jwksUrl, issuer, audience, jwksFile) {
|
|
39
|
+
const jwks = jwksFile ? localJwks(jwksFile) : createRemoteJWKSet(new URL(jwksUrl));
|
|
16
40
|
return async (token) => {
|
|
17
41
|
const { payload } = await jwtVerify(token, jwks, {
|
|
18
42
|
issuer,
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { runApprove } from "./approve-cli.js";
|
|
|
6
6
|
import { runDemo } from "./demo.js";
|
|
7
7
|
import { desktopMain } from "./desktop.js";
|
|
8
8
|
import { doctorExit, runDoctor } from "./doctor.js";
|
|
9
|
+
import { loadEnvFile, runInitLocal } from "./init-local.js";
|
|
9
10
|
import { runHalt } from "./halt.js";
|
|
10
11
|
import { main } from "./main.js";
|
|
11
12
|
import { runOperator } from "./operator-cli.js";
|
|
@@ -18,8 +19,12 @@ const HELP = `verax - the body an agent asks before it acts, and the ledger it a
|
|
|
18
19
|
Usage: verax <command> [options]
|
|
19
20
|
|
|
20
21
|
(no command) serve MCP over Streamable HTTP at /mcp on VERAX_BIND (default 127.0.0.1:8787)
|
|
22
|
+
serve [--env-file <file>] same as serving, after loading KEY=VALUE lines from that file
|
|
23
|
+
init --local <stateDir> [--force] [--days N] [--port N]
|
|
24
|
+
write a loopback key, one agent token, and verax.env
|
|
21
25
|
doctor [--json] check the configuration this process would run with
|
|
22
26
|
demo [--keep] run a loopback body against a temporary ledger and print what it recorded
|
|
27
|
+
[--with-conarium] also fetch Conarium with npx, attach it as a child, and put a masked read through the gate
|
|
23
28
|
approve <args> approve a waiting request from this machine
|
|
24
29
|
operator <args> enrol an operator and manage their passkeys
|
|
25
30
|
reconcile <args> compare the ledger against a statement
|
|
@@ -33,9 +38,9 @@ Usage: verax <command> [options]
|
|
|
33
38
|
--help, -h print this
|
|
34
39
|
--version, -v print the version
|
|
35
40
|
|
|
36
|
-
Serving needs VERAX_ISSUER, VERAX_JWKS_URL, VERAX_AUDIENCE,
|
|
37
|
-
VERAX_POLICY_FILE; \`verax doctor\` names what is missing.
|
|
38
|
-
machine, under VERAX_STATE_DIR.
|
|
41
|
+
Serving needs VERAX_ISSUER, one of VERAX_JWKS_URL or VERAX_JWKS_FILE, VERAX_AUDIENCE,
|
|
42
|
+
VERAX_STATE_DIR and VERAX_POLICY_FILE; \`verax doctor\` names what is missing.
|
|
43
|
+
Records stay on this machine, under VERAX_STATE_DIR.
|
|
39
44
|
`;
|
|
40
45
|
function version() {
|
|
41
46
|
// The published package answers with its own version, not a copy of it.
|
|
@@ -95,6 +100,28 @@ if (argv[0] === "unlock") {
|
|
|
95
100
|
}
|
|
96
101
|
process.exit(runUnlock(stateDir, (s) => process.stderr.write(s), { force }));
|
|
97
102
|
}
|
|
103
|
+
if (argv[0] === "init") {
|
|
104
|
+
process.exit(await runInitLocal(argv.slice(1)));
|
|
105
|
+
}
|
|
106
|
+
if (argv[0] === "serve") {
|
|
107
|
+
const fileFlag = argv.indexOf("--env-file");
|
|
108
|
+
if (fileFlag !== -1) {
|
|
109
|
+
const file = argv[fileFlag + 1];
|
|
110
|
+
if (!file || file.startsWith("-")) {
|
|
111
|
+
process.stderr.write("verax serve --env-file <file>\n");
|
|
112
|
+
process.exit(78);
|
|
113
|
+
}
|
|
114
|
+
const loaded = loadEnvFile(file);
|
|
115
|
+
if (!loaded.ok) {
|
|
116
|
+
process.stderr.write(`${loaded.reason}\n`);
|
|
117
|
+
process.exit(78);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
await main();
|
|
121
|
+
// main() returns once the server is listening. The open socket keeps the
|
|
122
|
+
// process up; this promise stops the rest of the dispatcher from running.
|
|
123
|
+
await new Promise(() => { });
|
|
124
|
+
}
|
|
98
125
|
if (argv[0] === "doctor") {
|
|
99
126
|
const json = argv.includes("--json");
|
|
100
127
|
const checks = runDoctor(process.env, process.argv);
|
package/dist/config.d.ts
CHANGED
|
@@ -8,6 +8,11 @@ export type BodyConfig = {
|
|
|
8
8
|
bindPort: number;
|
|
9
9
|
policyFile: string;
|
|
10
10
|
tlsTerminated: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Path to a JWKS file read once at start. Set instead of `jwksUrl`.
|
|
13
|
+
* Loopback binds only.
|
|
14
|
+
*/
|
|
15
|
+
jwksFile?: string | null;
|
|
11
16
|
/** Path to an inventory JSON file. Missing file is absence, not a fault. */
|
|
12
17
|
inventoryFile?: string | null;
|
|
13
18
|
/**
|
package/dist/config.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
export const EX_CONFIG = 78;
|
|
2
3
|
const LOOPBACK = new Set(["127.0.0.1", "::1", "localhost"]);
|
|
3
4
|
function parseBind(raw) {
|
|
@@ -14,11 +15,28 @@ export function isLoopbackHost(host) {
|
|
|
14
15
|
const stripped = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
15
16
|
return LOOPBACK.has(stripped);
|
|
16
17
|
}
|
|
18
|
+
function jwksFileParses(path) {
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
21
|
+
return Array.isArray(parsed.keys) && parsed.keys.length > 0;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
17
27
|
export function loadConfig(env) {
|
|
18
28
|
const issuer = env.VERAX_ISSUER?.trim() ?? "";
|
|
19
29
|
const jwksUrl = env.VERAX_JWKS_URL?.trim() ?? "";
|
|
30
|
+
const jwksFile = env.VERAX_JWKS_FILE?.trim() ?? "";
|
|
20
31
|
const audience = env.VERAX_AUDIENCE?.trim() ?? "";
|
|
21
|
-
if (
|
|
32
|
+
if (jwksUrl !== "" && jwksFile !== "") {
|
|
33
|
+
return {
|
|
34
|
+
ok: false,
|
|
35
|
+
code: EX_CONFIG,
|
|
36
|
+
reason: "both VERAX_JWKS_URL and VERAX_JWKS_FILE",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (issuer === "" || audience === "" || (jwksUrl === "" && jwksFile === "")) {
|
|
22
40
|
return {
|
|
23
41
|
ok: false,
|
|
24
42
|
code: EX_CONFIG,
|
|
@@ -27,6 +45,13 @@ export function loadConfig(env) {
|
|
|
27
45
|
}
|
|
28
46
|
const bind = parseBind(env.VERAX_BIND);
|
|
29
47
|
const tlsTerminated = env.VERAX_TLS_TERMINATED === "1";
|
|
48
|
+
if (jwksFile !== "" && !isLoopbackHost(bind.host)) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
code: EX_CONFIG,
|
|
52
|
+
reason: "VERAX_JWKS_FILE needs a loopback bind",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
30
55
|
if (!isLoopbackHost(bind.host) && !tlsTerminated) {
|
|
31
56
|
return {
|
|
32
57
|
ok: false,
|
|
@@ -34,6 +59,13 @@ export function loadConfig(env) {
|
|
|
34
59
|
reason: "non-loopback bind without VERAX_TLS_TERMINATED=1",
|
|
35
60
|
};
|
|
36
61
|
}
|
|
62
|
+
if (jwksFile !== "" && !jwksFileParses(jwksFile)) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
code: EX_CONFIG,
|
|
66
|
+
reason: "VERAX_JWKS_FILE is missing or unparsable",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
37
69
|
const stateDir = env.VERAX_STATE_DIR?.trim() ?? "";
|
|
38
70
|
if (stateDir === "") {
|
|
39
71
|
return { ok: false, code: EX_CONFIG, reason: "missing VERAX_STATE_DIR" };
|
|
@@ -49,6 +81,7 @@ export function loadConfig(env) {
|
|
|
49
81
|
value: {
|
|
50
82
|
issuer,
|
|
51
83
|
jwksUrl,
|
|
84
|
+
jwksFile: jwksFile === "" ? null : jwksFile,
|
|
52
85
|
audience,
|
|
53
86
|
stateDir,
|
|
54
87
|
bindHost: bind.host,
|
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/dist/desktop.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export declare function resolveRepoRoot(): string;
|
|
2
|
+
export declare const DESKTOP_CLONE_ONLY = "verax desktop runs from a clone of github.com/verax-ai/verax; it is not in the npm package\n";
|
|
3
|
+
export declare function desktopCloneError(root: string): string | null;
|
|
1
4
|
export type DesktopOpts = {
|
|
2
5
|
stateDir: string;
|
|
3
6
|
panelPort: number;
|
package/dist/desktop.js
CHANGED
|
@@ -6,7 +6,16 @@ import { dirname, join } from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { pidAlive, readLockFile } from "./unlock.js";
|
|
8
8
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
|
|
9
|
+
export function resolveRepoRoot() {
|
|
10
|
+
return join(here, "..", "..", "..");
|
|
11
|
+
}
|
|
12
|
+
export const DESKTOP_CLONE_ONLY = "verax desktop runs from a clone of github.com/verax-ai/verax; it is not in the npm package\n";
|
|
13
|
+
export function desktopCloneError(root) {
|
|
14
|
+
if (!existsSync(join(root, "scripts", "dev-issuer.mjs")) || !existsSync(join(root, "apps", "panel"))) {
|
|
15
|
+
return DESKTOP_CLONE_ONLY;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
10
19
|
/** Newest modification time under a file or directory, ignoring build output. */
|
|
11
20
|
function newestUnder(path, since) {
|
|
12
21
|
let entry;
|
|
@@ -228,6 +237,12 @@ function collectOutput(child, sink) {
|
|
|
228
237
|
child.stderr?.on("data", feed);
|
|
229
238
|
}
|
|
230
239
|
export async function runDesktop(opts, writeErr = (s) => process.stderr.write(s)) {
|
|
240
|
+
const repoRoot = resolveRepoRoot();
|
|
241
|
+
const cloneOnly = desktopCloneError(repoRoot);
|
|
242
|
+
if (cloneOnly) {
|
|
243
|
+
writeErr(cloneOnly);
|
|
244
|
+
return 78;
|
|
245
|
+
}
|
|
231
246
|
mkdirSync(opts.stateDir, { recursive: true });
|
|
232
247
|
const tokenPath = join(opts.stateDir, "dev-token");
|
|
233
248
|
const issuerScript = join(repoRoot, "scripts", "dev-issuer.mjs");
|
package/dist/doctor.js
CHANGED
|
@@ -9,13 +9,18 @@ const SECRET_NAME = /^(VERAX_DEV_TOKEN|.*_(TOKEN|SECRET|KEY))$/;
|
|
|
9
9
|
export function runDoctor(env, argv) {
|
|
10
10
|
const checks = [];
|
|
11
11
|
const issuer = Boolean(env.VERAX_ISSUER?.trim());
|
|
12
|
-
const
|
|
12
|
+
const jwksUrl = Boolean(env.VERAX_JWKS_URL?.trim());
|
|
13
|
+
const jwksFileSet = Boolean(env.VERAX_JWKS_FILE?.trim());
|
|
14
|
+
const jwks = jwksUrl || jwksFileSet;
|
|
13
15
|
const audience = Boolean(env.VERAX_AUDIENCE?.trim());
|
|
16
|
+
const tripleReady = issuer && jwks && audience;
|
|
14
17
|
checks.push({
|
|
15
18
|
id: "issuer-jwks-audience",
|
|
16
|
-
level:
|
|
17
|
-
detail:
|
|
18
|
-
?
|
|
19
|
+
level: tripleReady ? "ok" : "fail",
|
|
20
|
+
detail: tripleReady
|
|
21
|
+
? jwksFileSet && !jwksUrl
|
|
22
|
+
? "VERAX_ISSUER, VERAX_JWKS_FILE, and VERAX_AUDIENCE are set"
|
|
23
|
+
: "VERAX_ISSUER, VERAX_JWKS_URL, and VERAX_AUDIENCE are set"
|
|
19
24
|
: "one of VERAX_ISSUER, VERAX_JWKS_URL, VERAX_AUDIENCE is missing",
|
|
20
25
|
});
|
|
21
26
|
const loaded = loadConfig({
|
|
@@ -43,7 +48,7 @@ export function runDoctor(env, argv) {
|
|
|
43
48
|
});
|
|
44
49
|
}
|
|
45
50
|
}
|
|
46
|
-
else if (loaded.reason.includes("non-loopback")) {
|
|
51
|
+
else if (loaded.reason.includes("non-loopback") || loaded.reason.includes("loopback bind")) {
|
|
47
52
|
checks.push({ id: "bind", level: "fail", detail: loaded.reason });
|
|
48
53
|
}
|
|
49
54
|
else {
|
|
@@ -212,8 +217,61 @@ export function runDoctor(env, argv) {
|
|
|
212
217
|
});
|
|
213
218
|
}
|
|
214
219
|
}
|
|
220
|
+
checks.push(...jwksFileChecks(env, stateDir));
|
|
215
221
|
return checks;
|
|
216
222
|
}
|
|
223
|
+
function jwksFileChecks(env, stateDir) {
|
|
224
|
+
const file = env.VERAX_JWKS_FILE?.trim() ?? "";
|
|
225
|
+
if (file === "")
|
|
226
|
+
return [];
|
|
227
|
+
const out = [];
|
|
228
|
+
let kid = "";
|
|
229
|
+
let parses = false;
|
|
230
|
+
try {
|
|
231
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
232
|
+
const first = parsed.keys?.[0];
|
|
233
|
+
if (first && typeof first.kid === "string" && first.kid !== "") {
|
|
234
|
+
parses = true;
|
|
235
|
+
kid = first.kid;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
parses = false;
|
|
240
|
+
}
|
|
241
|
+
out.push({
|
|
242
|
+
id: "jwks-file",
|
|
243
|
+
level: parses ? "ok" : "fail",
|
|
244
|
+
detail: parses ? `VERAX_JWKS_FILE ${file} parses kid ${kid}` : `VERAX_JWKS_FILE ${file} does not parse`,
|
|
245
|
+
});
|
|
246
|
+
if (stateDir === "")
|
|
247
|
+
return out;
|
|
248
|
+
const tokenPath = join(stateDir, "local-issuer", "agent.token");
|
|
249
|
+
if (!existsSync(tokenPath))
|
|
250
|
+
return out;
|
|
251
|
+
const exp = expFromToken(readFileSync(tokenPath, "utf8"));
|
|
252
|
+
const weekSec = 7 * 24 * 60 * 60;
|
|
253
|
+
if (exp !== null && exp <= Math.floor(Date.now() / 1000) + weekSec) {
|
|
254
|
+
out.push({
|
|
255
|
+
id: "local-agent-token",
|
|
256
|
+
level: "warn",
|
|
257
|
+
detail: `${tokenPath} expires within 7 days`,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
function expFromToken(raw) {
|
|
263
|
+
const parts = raw.trim().split(".");
|
|
264
|
+
if (parts.length < 2)
|
|
265
|
+
return null;
|
|
266
|
+
try {
|
|
267
|
+
const json = Buffer.from(parts[1], "base64url").toString("utf8");
|
|
268
|
+
const payload = JSON.parse(json);
|
|
269
|
+
return typeof payload.exp === "number" ? payload.exp : null;
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
217
275
|
const DEFAULT_REDIRECTS = ["http://127.0.0.1:5173/", "http://127.0.0.1:4173/"];
|
|
218
276
|
function stripSlash(s) {
|
|
219
277
|
return s.replace(/\/$/, "");
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type InitIo = {
|
|
2
|
+
stdout: {
|
|
3
|
+
write(s: string): unknown;
|
|
4
|
+
};
|
|
5
|
+
stderr: {
|
|
6
|
+
write(s: string): unknown;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
export declare function loadEnvFile(path: string, env?: NodeJS.ProcessEnv): {
|
|
10
|
+
ok: true;
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
reason: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function runInitLocal(argv: readonly string[], io?: InitIo): Promise<number>;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { calculateJwkThumbprint, exportJWK, exportPKCS8, generateKeyPair, importPKCS8, SignJWT, } from "jose";
|
|
6
|
+
import { EX_CONFIG } from "./config.js";
|
|
7
|
+
const ISSUER = "verax-local";
|
|
8
|
+
const SUBJECT = "local-agent";
|
|
9
|
+
const SCOPE = "verax:read verax:memory";
|
|
10
|
+
const DEFAULT_DAYS = 30;
|
|
11
|
+
const MAX_DAYS = 90;
|
|
12
|
+
const DEFAULT_PORT = 8787;
|
|
13
|
+
const MIN_PORT = 1024;
|
|
14
|
+
const MAX_PORT = 65535;
|
|
15
|
+
export function loadEnvFile(path, env = process.env) {
|
|
16
|
+
let text;
|
|
17
|
+
try {
|
|
18
|
+
text = readFileSync(path, "utf8");
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return { ok: false, reason: "env file is missing or unreadable" };
|
|
22
|
+
}
|
|
23
|
+
for (const line of text.split(/\r?\n/)) {
|
|
24
|
+
const trimmed = line.trim();
|
|
25
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
26
|
+
continue;
|
|
27
|
+
const eq = trimmed.indexOf("=");
|
|
28
|
+
if (eq <= 0)
|
|
29
|
+
continue;
|
|
30
|
+
const key = trimmed.slice(0, eq).trim();
|
|
31
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
32
|
+
continue;
|
|
33
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
34
|
+
if ((value.startsWith('"') && value.endsWith('"') && value.length >= 2) ||
|
|
35
|
+
(value.startsWith("'") && value.endsWith("'") && value.length >= 2)) {
|
|
36
|
+
value = value.slice(1, -1);
|
|
37
|
+
}
|
|
38
|
+
env[key] = value;
|
|
39
|
+
}
|
|
40
|
+
return { ok: true };
|
|
41
|
+
}
|
|
42
|
+
function shippedPolicyPath() {
|
|
43
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
44
|
+
let resolved = null;
|
|
45
|
+
try {
|
|
46
|
+
resolved = join(dirname(fileURLToPath(import.meta.resolve("@verax-ai/proxy/package.json"))), "policy", "default.json");
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
resolved = null;
|
|
50
|
+
}
|
|
51
|
+
const candidates = [
|
|
52
|
+
resolved,
|
|
53
|
+
join(here, "..", "..", "proxy", "policy", "default.json"),
|
|
54
|
+
join(here, "..", "..", "@verax-ai", "proxy", "policy", "default.json"),
|
|
55
|
+
];
|
|
56
|
+
for (const path of candidates) {
|
|
57
|
+
if (path && existsSync(path))
|
|
58
|
+
return path;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function ownerOnly(path) {
|
|
63
|
+
try {
|
|
64
|
+
chmodSync(path, 0o600);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// The platform does not honour the mode bit.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function slashPath(path) {
|
|
71
|
+
return path.replaceAll("\\", "/");
|
|
72
|
+
}
|
|
73
|
+
function shQuote(path) {
|
|
74
|
+
return `'${slashPath(path).replaceAll("'", "'\\''")}'`;
|
|
75
|
+
}
|
|
76
|
+
function psQuote(path) {
|
|
77
|
+
return `'${slashPath(path).replaceAll("'", "''")}'`;
|
|
78
|
+
}
|
|
79
|
+
function parseInitArgs(argv) {
|
|
80
|
+
let stateDir = "";
|
|
81
|
+
let force = false;
|
|
82
|
+
let days = DEFAULT_DAYS;
|
|
83
|
+
let port = DEFAULT_PORT;
|
|
84
|
+
let sawDays = false;
|
|
85
|
+
let sawPort = false;
|
|
86
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
87
|
+
const a = argv[i];
|
|
88
|
+
if (a === "--local") {
|
|
89
|
+
stateDir = argv[i + 1] ?? "";
|
|
90
|
+
i += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (a === "--force") {
|
|
94
|
+
force = true;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (a === "--days") {
|
|
98
|
+
sawDays = true;
|
|
99
|
+
const raw = argv[i + 1] ?? "";
|
|
100
|
+
i += 1;
|
|
101
|
+
if (!/^[0-9]+$/.test(raw))
|
|
102
|
+
return { error: "--days wants an integer from 1 to 90" };
|
|
103
|
+
days = Number(raw);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (a === "--port") {
|
|
107
|
+
sawPort = true;
|
|
108
|
+
const raw = argv[i + 1] ?? "";
|
|
109
|
+
i += 1;
|
|
110
|
+
if (!/^[0-9]+$/.test(raw))
|
|
111
|
+
return { error: "--port wants an integer from 1024 to 65535" };
|
|
112
|
+
port = Number(raw);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
return { error: `flag-unknown:${a}` };
|
|
116
|
+
}
|
|
117
|
+
if (stateDir === "" || stateDir.startsWith("-")) {
|
|
118
|
+
return { error: "verax init --local <stateDir> [--force] [--days N] [--port N]" };
|
|
119
|
+
}
|
|
120
|
+
if (sawDays && (!Number.isInteger(days) || days < 1 || days > MAX_DAYS)) {
|
|
121
|
+
return { error: "--days wants an integer from 1 to 90" };
|
|
122
|
+
}
|
|
123
|
+
if (sawPort && (!Number.isInteger(port) || port < MIN_PORT || port > MAX_PORT)) {
|
|
124
|
+
return { error: "--port wants an integer from 1024 to 65535" };
|
|
125
|
+
}
|
|
126
|
+
return { stateDir: resolve(stateDir), force, days, port };
|
|
127
|
+
}
|
|
128
|
+
export async function runInitLocal(argv, io = { stdout: process.stdout, stderr: process.stderr }) {
|
|
129
|
+
const parsed = parseInitArgs(argv);
|
|
130
|
+
if ("error" in parsed) {
|
|
131
|
+
io.stderr.write(`${parsed.error}\n`);
|
|
132
|
+
return EX_CONFIG;
|
|
133
|
+
}
|
|
134
|
+
const stateDir = parsed.stateDir;
|
|
135
|
+
const issuerDir = join(stateDir, "local-issuer");
|
|
136
|
+
if (existsSync(issuerDir) && !parsed.force) {
|
|
137
|
+
io.stderr.write(`refusing: ${issuerDir} already exists\n`);
|
|
138
|
+
return EX_CONFIG;
|
|
139
|
+
}
|
|
140
|
+
const policyDest = join(stateDir, "policy.json");
|
|
141
|
+
const policySrc = existsSync(policyDest) ? null : shippedPolicyPath();
|
|
142
|
+
if (!existsSync(policyDest) && !policySrc) {
|
|
143
|
+
io.stderr.write("shipped policy file not found\n");
|
|
144
|
+
return EX_CONFIG;
|
|
145
|
+
}
|
|
146
|
+
const { privateKey, publicKey } = await generateKeyPair("ES256", { extractable: true });
|
|
147
|
+
const pem = await exportPKCS8(privateKey);
|
|
148
|
+
const jwk = await exportJWK(publicKey);
|
|
149
|
+
const thumb = await calculateJwkThumbprint(jwk, "sha256");
|
|
150
|
+
const hex = Buffer.from(thumb, "base64url").toString("hex").slice(0, 8);
|
|
151
|
+
const kid = `verax-local-${hex}`;
|
|
152
|
+
const publicJwk = { ...jwk, alg: "ES256", use: "sig", kid };
|
|
153
|
+
const key = await importPKCS8(pem, "ES256");
|
|
154
|
+
const now = Math.floor(Date.now() / 1000);
|
|
155
|
+
const origin = `http://127.0.0.1:${parsed.port}`;
|
|
156
|
+
const token = await new SignJWT({ scope: SCOPE })
|
|
157
|
+
.setProtectedHeader({ alg: "ES256", kid })
|
|
158
|
+
.setIssuer(ISSUER)
|
|
159
|
+
.setAudience(origin)
|
|
160
|
+
.setSubject(SUBJECT)
|
|
161
|
+
.setJti(randomUUID())
|
|
162
|
+
.setIssuedAt(now)
|
|
163
|
+
.setExpirationTime(now + parsed.days * 24 * 60 * 60)
|
|
164
|
+
.sign(key);
|
|
165
|
+
mkdirSync(issuerDir, { recursive: true, mode: 0o700 });
|
|
166
|
+
const keyPath = join(issuerDir, "key.pem");
|
|
167
|
+
const jwksPath = join(issuerDir, "jwks.json");
|
|
168
|
+
const tokenPath = join(issuerDir, "agent.token");
|
|
169
|
+
const envPath = join(stateDir, "verax.env");
|
|
170
|
+
writeFileSync(keyPath, pem, { encoding: "utf8", mode: 0o600 });
|
|
171
|
+
ownerOnly(keyPath);
|
|
172
|
+
writeFileSync(jwksPath, `${JSON.stringify({ keys: [publicJwk] })}\n`, { encoding: "utf8", mode: 0o600 });
|
|
173
|
+
writeFileSync(tokenPath, token, { encoding: "utf8", mode: 0o600 });
|
|
174
|
+
ownerOnly(tokenPath);
|
|
175
|
+
const wrote = [keyPath, jwksPath, tokenPath];
|
|
176
|
+
if (!existsSync(policyDest) && policySrc) {
|
|
177
|
+
writeFileSync(policyDest, readFileSync(policySrc));
|
|
178
|
+
wrote.push(policyDest);
|
|
179
|
+
}
|
|
180
|
+
const envBody = [
|
|
181
|
+
`VERAX_ISSUER=${ISSUER}`,
|
|
182
|
+
`VERAX_JWKS_FILE=${jwksPath}`,
|
|
183
|
+
`VERAX_AUDIENCE=${origin}`,
|
|
184
|
+
`VERAX_STATE_DIR=${stateDir}`,
|
|
185
|
+
`VERAX_POLICY_FILE=${policyDest}`,
|
|
186
|
+
`VERAX_BIND=127.0.0.1:${parsed.port}`,
|
|
187
|
+
"",
|
|
188
|
+
].join("\n");
|
|
189
|
+
writeFileSync(envPath, envBody, { encoding: "utf8" });
|
|
190
|
+
wrote.push(envPath);
|
|
191
|
+
const lines = [
|
|
192
|
+
...wrote.map((p) => `wrote ${p}`),
|
|
193
|
+
"",
|
|
194
|
+
"Start the body:",
|
|
195
|
+
`verax serve --env-file ${shQuote(envPath)}`,
|
|
196
|
+
"",
|
|
197
|
+
"Claude Code (sh):",
|
|
198
|
+
`claude mcp add --transport http verax ${origin}/mcp --header "Authorization: Bearer $(cat ${shQuote(tokenPath)})"`,
|
|
199
|
+
"",
|
|
200
|
+
"Claude Code (PowerShell):",
|
|
201
|
+
`claude mcp add --transport http verax ${origin}/mcp --header "Authorization: Bearer $(Get-Content -Raw ${psQuote(tokenPath)})"`,
|
|
202
|
+
"",
|
|
203
|
+
"Cursor / generic mcp.json:",
|
|
204
|
+
`{
|
|
205
|
+
"mcpServers": {
|
|
206
|
+
"verax": {
|
|
207
|
+
"url": "${origin}/mcp",
|
|
208
|
+
"headers": { "Authorization": "Bearer <token from your issuer>" }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}`,
|
|
212
|
+
`The token is the content of ${tokenPath}.`,
|
|
213
|
+
"",
|
|
214
|
+
"A held call is approved on this machine with: verax approve",
|
|
215
|
+
"",
|
|
216
|
+
];
|
|
217
|
+
io.stdout.write(lines.join("\n"));
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { JwksFileError } from "./auth.js";
|
|
1
2
|
import { EX_CONFIG, loadConfig, overlayInventoryArg } from "./config.js";
|
|
2
3
|
import { KeysPartialError } from "./keys.js";
|
|
3
4
|
import { listen } from "./server.js";
|
|
@@ -25,6 +26,10 @@ export async function main(env = process.env, argv = process.argv) {
|
|
|
25
26
|
process.stderr.write("keys-partial\n");
|
|
26
27
|
process.exit(err.code);
|
|
27
28
|
}
|
|
29
|
+
if (err instanceof JwksFileError) {
|
|
30
|
+
process.stderr.write(`${err.message}\n`);
|
|
31
|
+
process.exit(err.code);
|
|
32
|
+
}
|
|
28
33
|
throw err;
|
|
29
34
|
}
|
|
30
35
|
}
|
package/dist/server.js
CHANGED
|
@@ -308,7 +308,7 @@ export async function listen(config) {
|
|
|
308
308
|
throw err;
|
|
309
309
|
}
|
|
310
310
|
const toolMeta = [...TOOL_META, ...extraTools.map(downstreamMeta)];
|
|
311
|
-
const verify = createVerifier(config.jwksUrl, config.issuer, config.audience);
|
|
311
|
+
const verify = createVerifier(config.jwksUrl, config.issuer, config.audience, config.jwksFile);
|
|
312
312
|
const attachHandlers = (mcp) => {
|
|
313
313
|
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolMeta }));
|
|
314
314
|
mcp.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@verax-ai/body",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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.
|
|
26
|
-
"@verax-ai/proxy": "^0.
|
|
25
|
+
"@verax-ai/inventory": "^0.3.0",
|
|
26
|
+
"@verax-ai/proxy": "^0.3.0",
|
|
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.",
|