@verax-ai/body 0.3.0 → 0.4.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 +7 -2
- package/dist/approve-cli.d.ts +22 -1
- package/dist/approve-cli.js +209 -22
- package/dist/auth.d.ts +9 -1
- package/dist/auth.js +12 -2
- package/dist/body-log.d.ts +14 -0
- package/dist/body-log.js +61 -0
- package/dist/cli.d.ts +23 -1
- package/dist/cli.js +227 -94
- package/dist/config.d.ts +26 -0
- package/dist/config.js +47 -0
- package/dist/demo.d.ts +2 -0
- package/dist/demo.js +8 -2
- package/dist/desktop-parent.d.ts +19 -0
- package/dist/desktop-parent.js +44 -0
- package/dist/desktop.d.ts +140 -11
- package/dist/desktop.js +931 -103
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +94 -11
- package/dist/health-extras.d.ts +4 -0
- package/dist/health-extras.js +14 -0
- package/dist/init-local.d.ts +25 -1
- package/dist/init-local.js +151 -54
- package/dist/install.d.ts +745 -0
- package/dist/install.js +5738 -0
- package/dist/keys.d.ts +6 -0
- package/dist/keys.js +43 -20
- package/dist/main.js +11 -1
- package/dist/no-bypass-scan.js +3 -2
- package/dist/reconcile-cli.js +3 -1
- package/dist/server.d.ts +14 -1
- package/dist/server.js +125 -3
- package/dist/tools/audit.d.ts +5 -2
- package/dist/tools/audit.js +12 -2
- package/dist/tools/memory.js +41 -6
- package/dist/tools/message.js +38 -9
- package/dist/unlock.d.ts +5 -2
- package/dist/unlock.js +6 -1
- package/dist/verify-cli.js +135 -15
- package/dist/wiring.js +1 -1
- package/dist/witness.js +154 -24
- package/package.json +7 -3
package/dist/doctor.d.ts
CHANGED
|
@@ -5,4 +5,6 @@ export type DoctorCheck = {
|
|
|
5
5
|
detail: string;
|
|
6
6
|
};
|
|
7
7
|
export declare function runDoctor(env: NodeJS.ProcessEnv, argv: readonly string[]): DoctorCheck[];
|
|
8
|
+
/** Last 20 lines of `<stateDir>/body.log`, or a missing-file sentence. */
|
|
9
|
+
export declare function doctorBodyLog(stateDir: string): string;
|
|
8
10
|
export declare function doctorExit(checks: readonly DoctorCheck[]): number;
|
package/dist/doctor.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { join } from "node:path";
|
|
4
|
+
import { readWindowsMachineRoots, SystemToolError, windowsAgentTokenPath } from "./install.js";
|
|
5
|
+
import { readLogTail } from "./body-log.js";
|
|
3
6
|
import { indexCoverage, listPieceFiles } from "@verax-ai/proxy";
|
|
4
7
|
import { loadConfig, isLoopbackHost } from "./config.js";
|
|
5
8
|
import { parseDownstreamDocument } from "./downstream.js";
|
|
@@ -172,24 +175,32 @@ export function runDoctor(env, argv) {
|
|
|
172
175
|
}
|
|
173
176
|
const tokenScope = scopeFromDevToken(env.VERAX_DEV_TOKEN);
|
|
174
177
|
const mintScope = env.VERAX_DEV_SCOPE?.trim() ?? "";
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
+
// VERAX_DEV_SCOPE cannot put verax:audit on the agent file or on a session
|
|
179
|
+
// that has not passed a passkey. The panel reads the ledger with that session.
|
|
180
|
+
if (tokenScope !== null) {
|
|
181
|
+
const parts = tokenScope.split(/\s+/).filter((s) => s !== "");
|
|
178
182
|
if (parts.includes("verax:audit")) {
|
|
179
183
|
checks.push({
|
|
180
184
|
id: "dev-token-audit",
|
|
181
|
-
level: "
|
|
182
|
-
detail: "development token
|
|
185
|
+
level: "warn",
|
|
186
|
+
detail: "development token carries verax:audit; that scope belongs on the passkey session, not the agent token. The panel reads /api/ledger only after a passkey sign-in",
|
|
183
187
|
});
|
|
184
188
|
}
|
|
185
189
|
else {
|
|
186
190
|
checks.push({
|
|
187
191
|
id: "dev-token-audit",
|
|
188
|
-
level: "
|
|
189
|
-
detail: "development token
|
|
192
|
+
level: "ok",
|
|
193
|
+
detail: "development token does not carry verax:audit",
|
|
190
194
|
});
|
|
191
195
|
}
|
|
192
196
|
}
|
|
197
|
+
else if (mintScope !== "") {
|
|
198
|
+
checks.push({
|
|
199
|
+
id: "dev-token-audit",
|
|
200
|
+
level: "ok",
|
|
201
|
+
detail: "VERAX_DEV_SCOPE does not mint verax:audit onto the agent token or a session without a passkey; the panel reads /api/ledger with the passkey session",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
193
204
|
const downstreamPath = env.VERAX_DOWNSTREAM?.trim() ?? "";
|
|
194
205
|
if (downstreamPath !== "") {
|
|
195
206
|
try {
|
|
@@ -245,8 +256,9 @@ function jwksFileChecks(env, stateDir) {
|
|
|
245
256
|
});
|
|
246
257
|
if (stateDir === "")
|
|
247
258
|
return out;
|
|
248
|
-
const
|
|
249
|
-
|
|
259
|
+
const candidates = [agentTokenOnDisk(env), join(stateDir, "local-issuer", "agent.token")].filter((file) => file !== null);
|
|
260
|
+
const tokenPath = candidates.find((file) => existsSync(file));
|
|
261
|
+
if (!tokenPath)
|
|
250
262
|
return out;
|
|
251
263
|
const exp = expFromToken(readFileSync(tokenPath, "utf8"));
|
|
252
264
|
const weekSec = 7 * 24 * 60 * 60;
|
|
@@ -259,6 +271,26 @@ function jwksFileChecks(env, stateDir) {
|
|
|
259
271
|
}
|
|
260
272
|
return out;
|
|
261
273
|
}
|
|
274
|
+
/** Install token: ProgramData on Windows, `~/.verax/agent.token` on Linux and macOS. */
|
|
275
|
+
function agentTokenOnDisk(env) {
|
|
276
|
+
if (process.platform === "win32") {
|
|
277
|
+
const sid = env.VERAX_USER_SID?.trim().replace(/^\*/, "") ?? "";
|
|
278
|
+
if (!/^S-1-[0-9-]+$/i.test(sid))
|
|
279
|
+
return null;
|
|
280
|
+
try {
|
|
281
|
+
return windowsAgentTokenPath(env, sid, readWindowsMachineRoots());
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
if (err instanceof SystemToolError)
|
|
285
|
+
return null;
|
|
286
|
+
throw err;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const home = env.HOME?.trim() || env.USERPROFILE?.trim() || homedir();
|
|
290
|
+
if (home === "")
|
|
291
|
+
return null;
|
|
292
|
+
return join(home, ".verax", "agent.token");
|
|
293
|
+
}
|
|
262
294
|
function expFromToken(raw) {
|
|
263
295
|
const parts = raw.trim().split(".");
|
|
264
296
|
if (parts.length < 2)
|
|
@@ -318,6 +350,14 @@ function scopeFromDevToken(raw) {
|
|
|
318
350
|
return null;
|
|
319
351
|
}
|
|
320
352
|
}
|
|
353
|
+
/** Last 20 lines of `<stateDir>/body.log`, or a missing-file sentence. */
|
|
354
|
+
export function doctorBodyLog(stateDir) {
|
|
355
|
+
const file = join(stateDir, "body.log");
|
|
356
|
+
const tail = readLogTail(file, 20);
|
|
357
|
+
if (tail === null)
|
|
358
|
+
return `body.log missing: ${file}\n`;
|
|
359
|
+
return tail;
|
|
360
|
+
}
|
|
321
361
|
export function doctorExit(checks) {
|
|
322
362
|
return checks.some((c) => c.level === "fail") ? 1 : 0;
|
|
323
363
|
}
|
|
@@ -348,6 +388,8 @@ function heartbeatMaxMs(env) {
|
|
|
348
388
|
const n = Number(env.VERAX_HEARTBEAT_MAX_MS ?? "30000");
|
|
349
389
|
return Number.isFinite(n) && n > 0 ? n : 30_000;
|
|
350
390
|
}
|
|
391
|
+
/** A pulse this far ahead of the clock is still treated as now. */
|
|
392
|
+
const HEARTBEAT_FUTURE_SKEW_MS = 60_000;
|
|
351
393
|
function evidenceChecks(stateDir, env) {
|
|
352
394
|
const checks = [];
|
|
353
395
|
let pieces;
|
|
@@ -360,7 +402,24 @@ function evidenceChecks(stateDir, env) {
|
|
|
360
402
|
}
|
|
361
403
|
const srcLines = pieces.reduce((s, p) => s + countJsonl(p.decisions).lines, 0);
|
|
362
404
|
const decisionN = pieces.reduce((s, p) => s + (p.closed ? p.n : countJsonl(p.decisions).lines), 0);
|
|
363
|
-
|
|
405
|
+
let closedOff = null;
|
|
406
|
+
for (const piece of pieces) {
|
|
407
|
+
if (!piece.closed)
|
|
408
|
+
continue;
|
|
409
|
+
const lines = countJsonl(piece.decisions).lines;
|
|
410
|
+
if (lines !== piece.n) {
|
|
411
|
+
closedOff = { id: piece.id, lines, n: piece.n };
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (closedOff) {
|
|
416
|
+
checks.push({
|
|
417
|
+
id: "ledger-index",
|
|
418
|
+
level: "fail",
|
|
419
|
+
detail: `piece ${closedOff.id} has ${closedOff.lines} decision line(s), the manifest says ${closedOff.n}`,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
else if (decisionN > 0) {
|
|
364
423
|
const known = new Set(pieces.map((p) => p.id));
|
|
365
424
|
const cov = indexCoverage(stateDir);
|
|
366
425
|
if (cov === null) {
|
|
@@ -413,6 +472,13 @@ function evidenceChecks(stateDir, env) {
|
|
|
413
472
|
detail: "ledger has rows but no readable heartbeat; the evidence service looks silent",
|
|
414
473
|
});
|
|
415
474
|
}
|
|
475
|
+
else if (!Number.isFinite(hb.atMs) || hb.atMs > Date.now() + HEARTBEAT_FUTURE_SKEW_MS) {
|
|
476
|
+
checks.push({
|
|
477
|
+
id: "heartbeat",
|
|
478
|
+
level: "fail",
|
|
479
|
+
detail: "heartbeat time is in the future",
|
|
480
|
+
});
|
|
481
|
+
}
|
|
416
482
|
else if (Date.now() - hb.atMs > heartbeatMaxMs(env)) {
|
|
417
483
|
checks.push({
|
|
418
484
|
id: "heartbeat",
|
|
@@ -445,6 +511,8 @@ function evidenceChecks(stateDir, env) {
|
|
|
445
511
|
srcEffects.lines > 0 ||
|
|
446
512
|
copy.lines > 0 ||
|
|
447
513
|
copyEffects.lines > 0 ||
|
|
514
|
+
src.corrupt ||
|
|
515
|
+
srcEffects.corrupt ||
|
|
448
516
|
copy.corrupt ||
|
|
449
517
|
copyEffects.corrupt;
|
|
450
518
|
if (!pieceAnything)
|
|
@@ -452,7 +520,15 @@ function evidenceChecks(stateDir, env) {
|
|
|
452
520
|
anything = true;
|
|
453
521
|
if (fail)
|
|
454
522
|
continue;
|
|
455
|
-
if (
|
|
523
|
+
if (src.corrupt || srcEffects.corrupt) {
|
|
524
|
+
const file = src.corrupt ? "decisions.jsonl" : "effects.jsonl";
|
|
525
|
+
fail = {
|
|
526
|
+
id: "evidence-copy",
|
|
527
|
+
level: "fail",
|
|
528
|
+
detail: `source ${file} on piece ${piece.id} is corrupt`,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
else if (copy.corrupt || copyEffects.corrupt) {
|
|
456
532
|
fail = {
|
|
457
533
|
id: "evidence-copy",
|
|
458
534
|
level: "fail",
|
|
@@ -473,6 +549,13 @@ function evidenceChecks(stateDir, env) {
|
|
|
473
549
|
detail: `evidence copy is stale on piece ${piece.id} effects: ${copyEffects.lines} effect line(s) behind source ${srcEffects.lines}`,
|
|
474
550
|
};
|
|
475
551
|
}
|
|
552
|
+
else if (copy.lines > src.lines || copyEffects.lines > srcEffects.lines) {
|
|
553
|
+
fail = {
|
|
554
|
+
id: "evidence-copy",
|
|
555
|
+
level: "fail",
|
|
556
|
+
detail: `source is shorter than its evidence copy on piece ${piece.id}`,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
476
559
|
}
|
|
477
560
|
if (anything) {
|
|
478
561
|
checks.push(fail ?? {
|
package/dist/health-extras.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/** Written by `verax install` before the service starts. `/healthz` echoes it. */
|
|
2
|
+
export declare const INSTALL_HEALTH_NONCE = "install-health-nonce";
|
|
3
|
+
/** Hex nonce from this install, or null when the file is absent or not that shape. */
|
|
4
|
+
export declare function readInstallHealthNonce(stateDir: string): string | null;
|
|
1
5
|
export type HealthHeartbeat = {
|
|
2
6
|
atMs: number;
|
|
3
7
|
lastDecisionN: number;
|
package/dist/health-extras.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
/** Written by `verax install` before the service starts. `/healthz` echoes it. */
|
|
4
|
+
export const INSTALL_HEALTH_NONCE = "install-health-nonce";
|
|
5
|
+
/** Hex nonce from this install, or null when the file is absent or not that shape. */
|
|
6
|
+
export function readInstallHealthNonce(stateDir) {
|
|
7
|
+
try {
|
|
8
|
+
const text = readFileSync(join(stateDir, INSTALL_HEALTH_NONCE), "utf8").trim();
|
|
9
|
+
if (!/^[0-9a-f]{64}$/i.test(text))
|
|
10
|
+
return null;
|
|
11
|
+
return text.toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
3
17
|
export function readHeartbeat(stateDir) {
|
|
4
18
|
const path = join(stateDir, "heartbeat.json");
|
|
5
19
|
if (!existsSync(path))
|
package/dist/init-local.d.ts
CHANGED
|
@@ -12,4 +12,28 @@ export declare function loadEnvFile(path: string, env?: NodeJS.ProcessEnv): {
|
|
|
12
12
|
ok: false;
|
|
13
13
|
reason: string;
|
|
14
14
|
};
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Drop every `VERAX_*` name before an env file is applied. Comparison is
|
|
17
|
+
* case-insensitive because Windows environment names are.
|
|
18
|
+
*/
|
|
19
|
+
export declare function clearVeraxEnv(env: NodeJS.ProcessEnv): void;
|
|
20
|
+
/** `serve --env-file` refuses a file that names neither JWKS source. */
|
|
21
|
+
export declare function envFileJwksMissing(env: NodeJS.ProcessEnv): string | null;
|
|
22
|
+
export type InitLocalOpts = {
|
|
23
|
+
/** Write the agent token here and leave no copy under the state directory. */
|
|
24
|
+
tokenPath?: string;
|
|
25
|
+
/** Installed body prints its own summary. Skip this command's user-facing text. */
|
|
26
|
+
quiet?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Install mode. State files inherit the state directory ACL (svc, Administrators, SYSTEM).
|
|
29
|
+
* Do not grant the invoking user and do not call `restrictToOwnerWin32`.
|
|
30
|
+
* Elevated install does not pass a token path under the invoking user's home.
|
|
31
|
+
* `tokenPath` remains for a non-elevated `verax init --local`.
|
|
32
|
+
*/
|
|
33
|
+
noOwnerGrant?: boolean;
|
|
34
|
+
/** Install mode. Defaults to process.env. SUDO_USER names the POSIX owner. */
|
|
35
|
+
env?: NodeJS.ProcessEnv;
|
|
36
|
+
/** Test hook. A throw here runs the rollback after the issuer directory exists and before any file is written. */
|
|
37
|
+
beforeWrite?: () => void;
|
|
38
|
+
};
|
|
39
|
+
export declare function runInitLocal(argv: readonly string[], io?: InitIo, opts?: InitLocalOpts): Promise<number>;
|
package/dist/init-local.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { chmodSync, existsSync,
|
|
2
|
+
import { chmodSync, existsSync, lstatSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { beginWinOwnerRun, endWinOwnerRun, ensureTokenParent, mkdirLeaf, restrictToOwnerWin32, SystemToolError } from "./install.js";
|
|
5
6
|
import { calculateJwkThumbprint, exportJWK, exportPKCS8, generateKeyPair, importPKCS8, SignJWT, } from "jose";
|
|
6
7
|
import { EX_CONFIG } from "./config.js";
|
|
7
8
|
const ISSUER = "verax-local";
|
|
@@ -20,25 +21,50 @@ export function loadEnvFile(path, env = process.env) {
|
|
|
20
21
|
catch {
|
|
21
22
|
return { ok: false, reason: "env file is missing or unreadable" };
|
|
22
23
|
}
|
|
24
|
+
const parsed = [];
|
|
23
25
|
for (const line of text.split(/\r?\n/)) {
|
|
24
|
-
|
|
26
|
+
let trimmed = line.trim();
|
|
25
27
|
if (trimmed === "" || trimmed.startsWith("#"))
|
|
26
28
|
continue;
|
|
29
|
+
if (trimmed.startsWith("export "))
|
|
30
|
+
trimmed = trimmed.slice("export ".length).trim();
|
|
27
31
|
const eq = trimmed.indexOf("=");
|
|
28
32
|
if (eq <= 0)
|
|
29
33
|
continue;
|
|
30
34
|
const key = trimmed.slice(0, eq).trim();
|
|
31
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
32
|
-
|
|
35
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || !key.startsWith("VERAX_")) {
|
|
36
|
+
return { ok: false, reason: `env file refuses ${key}` };
|
|
37
|
+
}
|
|
33
38
|
let value = trimmed.slice(eq + 1).trim();
|
|
34
39
|
if ((value.startsWith('"') && value.endsWith('"') && value.length >= 2) ||
|
|
35
40
|
(value.startsWith("'") && value.endsWith("'") && value.length >= 2)) {
|
|
36
41
|
value = value.slice(1, -1);
|
|
37
42
|
}
|
|
38
|
-
|
|
43
|
+
parsed.push([key, value]);
|
|
39
44
|
}
|
|
45
|
+
clearVeraxEnv(env);
|
|
46
|
+
for (const [key, value] of parsed)
|
|
47
|
+
env[key] = value;
|
|
40
48
|
return { ok: true };
|
|
41
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Drop every `VERAX_*` name before an env file is applied. Comparison is
|
|
52
|
+
* case-insensitive because Windows environment names are.
|
|
53
|
+
*/
|
|
54
|
+
export function clearVeraxEnv(env) {
|
|
55
|
+
for (const key of Object.keys(env)) {
|
|
56
|
+
if (key.toUpperCase().startsWith("VERAX_"))
|
|
57
|
+
delete env[key];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** `serve --env-file` refuses a file that names neither JWKS source. */
|
|
61
|
+
export function envFileJwksMissing(env) {
|
|
62
|
+
const file = env.VERAX_JWKS_FILE?.trim() ?? "";
|
|
63
|
+
const url = env.VERAX_JWKS_URL?.trim() ?? "";
|
|
64
|
+
if (file === "" && url === "")
|
|
65
|
+
return "env file sets neither VERAX_JWKS_FILE nor VERAX_JWKS_URL";
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
42
68
|
function shippedPolicyPath() {
|
|
43
69
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
44
70
|
let resolved = null;
|
|
@@ -59,7 +85,13 @@ function shippedPolicyPath() {
|
|
|
59
85
|
}
|
|
60
86
|
return null;
|
|
61
87
|
}
|
|
62
|
-
|
|
88
|
+
/** POSIX keeps owner-only mode. Windows grants the current user only outside install mode. */
|
|
89
|
+
function ownerOnly(path, grantOwner) {
|
|
90
|
+
if (process.platform === "win32") {
|
|
91
|
+
if (grantOwner)
|
|
92
|
+
restrictToOwnerWin32(path);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
63
95
|
try {
|
|
64
96
|
chmodSync(path, 0o600);
|
|
65
97
|
}
|
|
@@ -125,7 +157,25 @@ function parseInitArgs(argv) {
|
|
|
125
157
|
}
|
|
126
158
|
return { stateDir: resolve(stateDir), force, days, port };
|
|
127
159
|
}
|
|
128
|
-
|
|
160
|
+
function pathExists(target) {
|
|
161
|
+
try {
|
|
162
|
+
lstatSync(target);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
export async function runInitLocal(argv, io = { stdout: process.stdout, stderr: process.stderr }, opts = {}) {
|
|
170
|
+
beginWinOwnerRun();
|
|
171
|
+
try {
|
|
172
|
+
return await runInitLocalBody(argv, io, opts);
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
endWinOwnerRun();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function runInitLocalBody(argv, io, opts) {
|
|
129
179
|
const parsed = parseInitArgs(argv);
|
|
130
180
|
if ("error" in parsed) {
|
|
131
181
|
io.stderr.write(`${parsed.error}\n`);
|
|
@@ -162,46 +212,82 @@ export async function runInitLocal(argv, io = { stdout: process.stdout, stderr:
|
|
|
162
212
|
.setIssuedAt(now)
|
|
163
213
|
.setExpirationTime(now + parsed.days * 24 * 60 * 60)
|
|
164
214
|
.sign(key);
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
""
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
215
|
+
const createdState = !pathExists(stateDir);
|
|
216
|
+
const issuerExisted = pathExists(issuerDir);
|
|
217
|
+
const wrote = [];
|
|
218
|
+
const createdFiles = new Set();
|
|
219
|
+
const rollback = () => {
|
|
220
|
+
if (createdState) {
|
|
221
|
+
rmSync(stateDir, { recursive: true, force: true });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
for (const p of [...createdFiles].reverse())
|
|
225
|
+
rmSync(p, { force: true });
|
|
226
|
+
if (!issuerExisted)
|
|
227
|
+
rmSync(issuerDir, { recursive: true, force: true });
|
|
228
|
+
};
|
|
229
|
+
const writeNew = (target, contents, mode) => {
|
|
230
|
+
if (!pathExists(target))
|
|
231
|
+
createdFiles.add(target);
|
|
232
|
+
writeFileSync(target, contents, mode === undefined ? { encoding: "utf8" } : { encoding: "utf8", mode });
|
|
233
|
+
wrote.push(target);
|
|
234
|
+
};
|
|
235
|
+
const grantOwner = opts.noOwnerGrant !== true;
|
|
236
|
+
try {
|
|
237
|
+
if (createdState)
|
|
238
|
+
mkdirLeaf(stateDir, 0o700);
|
|
239
|
+
if (process.platform === "win32" && grantOwner)
|
|
240
|
+
restrictToOwnerWin32(stateDir);
|
|
241
|
+
// --force keeps the existing issuer directory and rewrites what is in it.
|
|
242
|
+
mkdirLeaf(issuerDir, 0o700, { existingOk: parsed.force === true });
|
|
243
|
+
opts.beforeWrite?.();
|
|
244
|
+
const keyPath = join(issuerDir, "key.pem");
|
|
245
|
+
const jwksPath = join(issuerDir, "jwks.json");
|
|
246
|
+
const insideToken = join(issuerDir, "agent.token");
|
|
247
|
+
const external = opts.tokenPath !== undefined && resolve(opts.tokenPath) !== resolve(insideToken);
|
|
248
|
+
const tokenPath = external ? resolve(opts.tokenPath) : insideToken;
|
|
249
|
+
if (external)
|
|
250
|
+
ensureTokenParent(dirname(tokenPath), opts.env ?? process.env);
|
|
251
|
+
const envPath = join(stateDir, "verax.env");
|
|
252
|
+
writeNew(keyPath, pem, 0o600);
|
|
253
|
+
ownerOnly(keyPath, grantOwner);
|
|
254
|
+
writeNew(jwksPath, `${JSON.stringify({ keys: [publicJwk] })}\n`, 0o600);
|
|
255
|
+
ownerOnly(jwksPath, grantOwner);
|
|
256
|
+
writeNew(tokenPath, token, 0o600);
|
|
257
|
+
ownerOnly(tokenPath, grantOwner);
|
|
258
|
+
if (external && existsSync(insideToken))
|
|
259
|
+
unlinkSync(insideToken);
|
|
260
|
+
if (!existsSync(policyDest) && policySrc) {
|
|
261
|
+
writeNew(policyDest, readFileSync(policySrc, "utf8"));
|
|
262
|
+
ownerOnly(policyDest, grantOwner);
|
|
263
|
+
}
|
|
264
|
+
const envBody = [
|
|
265
|
+
`VERAX_ISSUER=${ISSUER}`,
|
|
266
|
+
`VERAX_JWKS_FILE=${jwksPath}`,
|
|
267
|
+
`VERAX_AUDIENCE=${origin}`,
|
|
268
|
+
`VERAX_STATE_DIR=${stateDir}`,
|
|
269
|
+
`VERAX_POLICY_FILE=${policyDest}`,
|
|
270
|
+
`VERAX_BIND=127.0.0.1:${parsed.port}`,
|
|
271
|
+
"",
|
|
272
|
+
].join("\n");
|
|
273
|
+
writeNew(envPath, envBody);
|
|
274
|
+
ownerOnly(envPath, grantOwner);
|
|
275
|
+
const lines = [
|
|
276
|
+
...wrote.map((p) => `wrote ${p}`),
|
|
277
|
+
"",
|
|
278
|
+
"A shell as the same user can read this directory and approve its own calls, so for anything that matters run the body as another OS user or in a container, or keep the agent in a sandbox that cannot read the state directory.",
|
|
279
|
+
"",
|
|
280
|
+
"Start the body:",
|
|
281
|
+
`verax serve --env-file ${shQuote(envPath)}`,
|
|
282
|
+
"",
|
|
283
|
+
"Claude Code (sh):",
|
|
284
|
+
`claude mcp add --transport http verax ${origin}/mcp --header "Authorization: Bearer $(cat ${shQuote(tokenPath)})"`,
|
|
285
|
+
"",
|
|
286
|
+
"Claude Code (PowerShell):",
|
|
287
|
+
`claude mcp add --transport http verax ${origin}/mcp --header "Authorization: Bearer $(Get-Content -Raw ${psQuote(tokenPath)})"`,
|
|
288
|
+
"",
|
|
289
|
+
"Cursor / generic mcp.json:",
|
|
290
|
+
`{
|
|
205
291
|
"mcpServers": {
|
|
206
292
|
"verax": {
|
|
207
293
|
"url": "${origin}/mcp",
|
|
@@ -209,11 +295,22 @@ export async function runInitLocal(argv, io = { stdout: process.stdout, stderr:
|
|
|
209
295
|
}
|
|
210
296
|
}
|
|
211
297
|
}`,
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
298
|
+
`The token is the content of ${tokenPath}.`,
|
|
299
|
+
"",
|
|
300
|
+
"A held call is approved on this machine with: verax approve",
|
|
301
|
+
"",
|
|
302
|
+
];
|
|
303
|
+
if (!opts.quiet)
|
|
304
|
+
io.stdout.write(lines.join("\n"));
|
|
305
|
+
return 0;
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
const externalToken = wrote.find((p) => !p.startsWith(stateDir));
|
|
309
|
+
rollback();
|
|
310
|
+
if (externalToken && createdFiles.has(externalToken))
|
|
311
|
+
rmSync(externalToken, { force: true });
|
|
312
|
+
const detail = err instanceof Error ? err.message : "owner-only failed";
|
|
313
|
+
io.stderr.write(`${detail}\n`);
|
|
314
|
+
return err instanceof SystemToolError ? EX_CONFIG : 1;
|
|
315
|
+
}
|
|
219
316
|
}
|