@verax-ai/body 0.1.1 → 0.1.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/agents.d.ts +33 -0
- package/dist/agents.js +59 -0
- package/dist/approve-cli.js +27 -1
- package/dist/cli.js +1 -1
- package/dist/desktop.d.ts +23 -0
- package/dist/desktop.js +82 -30
- package/dist/doctor.js +87 -30
- package/dist/downstream.d.ts +28 -0
- package/dist/downstream.js +183 -0
- package/dist/inputs-read.d.ts +7 -0
- package/dist/inputs-read.js +47 -40
- package/dist/no-bypass-scan.js +7 -0
- package/dist/server.d.ts +183 -0
- package/dist/server.js +179 -37
- package/dist/tools/memory.d.ts +2 -0
- package/dist/tools/memory.js +7 -0
- package/dist/wiring.d.ts +10 -2
- package/dist/wiring.js +31 -4
- package/dist/witness.js +15 -13
- package/package.json +1 -1
package/dist/agents.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type FileLedger } from "@verax-ai/proxy";
|
|
2
|
+
export type AgentRow = {
|
|
3
|
+
brain: string;
|
|
4
|
+
decisions: number;
|
|
5
|
+
allowed: number;
|
|
6
|
+
denied: number;
|
|
7
|
+
deferred: number;
|
|
8
|
+
/** Approvals waiting on an operator now, whatever the window. */
|
|
9
|
+
pending: number;
|
|
10
|
+
/** The newest decision in the window, or null when there was none. */
|
|
11
|
+
lastMs: number | null;
|
|
12
|
+
/** What the roster declares; null for an agent the roster does not name. */
|
|
13
|
+
roster: {
|
|
14
|
+
state: string;
|
|
15
|
+
label: string;
|
|
16
|
+
group: string | null;
|
|
17
|
+
} | null;
|
|
18
|
+
};
|
|
19
|
+
export type AgentsAnswer = {
|
|
20
|
+
fromMs: number;
|
|
21
|
+
toMs: number;
|
|
22
|
+
/** Pending first, then the most recently active; ties by name. */
|
|
23
|
+
agents: AgentRow[];
|
|
24
|
+
/** Decisions in the window whose inputs document was not found or did not match its hash. */
|
|
25
|
+
unattributed: number;
|
|
26
|
+
};
|
|
27
|
+
export declare function agentsWindow(opts: {
|
|
28
|
+
ledger: FileLedger;
|
|
29
|
+
stateDir: string;
|
|
30
|
+
inventoryFile: string | null | undefined;
|
|
31
|
+
fromMs: number;
|
|
32
|
+
toMs: number;
|
|
33
|
+
}): Promise<AgentsAnswer>;
|
package/dist/agents.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// One row per agent for the panel's status tab: what each did in a window
|
|
2
|
+
// of the ledger, what is waiting on an operator for it, when it last acted,
|
|
3
|
+
// and what the roster says about it. A company runs hundreds of agents; the
|
|
4
|
+
// operator reads this list, not the records. The window is read from the
|
|
5
|
+
// end of the ledger, so the answer costs the window and not the ledger.
|
|
6
|
+
import { parseInventory } from "@verax-ai/inventory";
|
|
7
|
+
import { loadApprovalsFromDir } from "@verax-ai/proxy";
|
|
8
|
+
import { matchingInputs } from "./inputs-read.js";
|
|
9
|
+
import { readInventoryFile } from "./inventory-file.js";
|
|
10
|
+
export async function agentsWindow(opts) {
|
|
11
|
+
const { rows } = await opts.ledger.decisionsWindow(opts.fromMs, opts.toMs);
|
|
12
|
+
const inputs = (await matchingInputs(opts.stateDir, rows));
|
|
13
|
+
const byBrain = new Map();
|
|
14
|
+
const rowFor = (brain) => {
|
|
15
|
+
let row = byBrain.get(brain);
|
|
16
|
+
if (!row) {
|
|
17
|
+
row = { brain, decisions: 0, allowed: 0, denied: 0, deferred: 0, pending: 0, lastMs: null, roster: null };
|
|
18
|
+
byBrain.set(brain, row);
|
|
19
|
+
}
|
|
20
|
+
return row;
|
|
21
|
+
};
|
|
22
|
+
let unattributed = 0;
|
|
23
|
+
for (const d of rows) {
|
|
24
|
+
const ref = d.claims.ref;
|
|
25
|
+
const brain = typeof ref === "string" ? inputs[ref]?.principal?.brain : undefined;
|
|
26
|
+
if (typeof brain !== "string" || brain === "") {
|
|
27
|
+
unattributed += 1;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const row = rowFor(brain);
|
|
31
|
+
row.decisions += 1;
|
|
32
|
+
if (d.claims.decision === "allow")
|
|
33
|
+
row.allowed += 1;
|
|
34
|
+
else if (d.claims.decision === "deny")
|
|
35
|
+
row.denied += 1;
|
|
36
|
+
else if (d.claims.decision === "defer")
|
|
37
|
+
row.deferred += 1;
|
|
38
|
+
if (row.lastMs === null || d.claims.timestampMs > row.lastMs)
|
|
39
|
+
row.lastMs = d.claims.timestampMs;
|
|
40
|
+
}
|
|
41
|
+
for (const approval of loadApprovalsFromDir(opts.stateDir)) {
|
|
42
|
+
if (approval.status === "pending")
|
|
43
|
+
rowFor(approval.brain).pending += 1;
|
|
44
|
+
}
|
|
45
|
+
const door = readInventoryFile(opts.inventoryFile);
|
|
46
|
+
const parsed = door.inventory == null ? null : parseInventory(door.inventory);
|
|
47
|
+
if (parsed && parsed.ok) {
|
|
48
|
+
const groups = new Map(parsed.value.groups.map((g) => [g.id, g.label]));
|
|
49
|
+
for (const agent of parsed.value.agents) {
|
|
50
|
+
rowFor(agent.id).roster = {
|
|
51
|
+
state: agent.state,
|
|
52
|
+
label: agent.label,
|
|
53
|
+
group: agent.groupId === null ? null : (groups.get(agent.groupId) ?? agent.groupId),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const agents = [...byBrain.values()].sort((a, b) => b.pending - a.pending || (b.lastMs ?? -1) - (a.lastMs ?? -1) || a.brain.localeCompare(b.brain));
|
|
58
|
+
return { fromMs: opts.fromMs, toMs: opts.toMs, agents, unattributed };
|
|
59
|
+
}
|
package/dist/approve-cli.js
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
1
2
|
import { userInfo } from "node:os";
|
|
2
|
-
import {
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { approvePending, approvalsLogFor, createApprovalBudgetGuard, enqueueApprovalCommand, FileLedger, loadApprovalsFromDir, loadPolicy, } from "@verax-ai/proxy";
|
|
3
5
|
import { loadOrCreateSigners } from "./keys.js";
|
|
6
|
+
function policyForApprove(stateDir, policyHash) {
|
|
7
|
+
const fromEnv = process.env.VERAX_POLICY_FILE?.trim();
|
|
8
|
+
if (fromEnv) {
|
|
9
|
+
try {
|
|
10
|
+
return loadPolicy(readFileSync(fromEnv, "utf8"));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// Fall through to the snapshot the body wrote for this hash.
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const snap = join(stateDir, "policies", `${policyHash}.json`);
|
|
17
|
+
if (!existsSync(snap))
|
|
18
|
+
return null;
|
|
19
|
+
try {
|
|
20
|
+
return loadPolicy(JSON.parse(readFileSync(snap, "utf8")));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
4
26
|
function operatorName() {
|
|
5
27
|
try {
|
|
6
28
|
const name = userInfo().username;
|
|
@@ -65,6 +87,7 @@ export async function runApprove(argv, writeErr = (s) => process.stderr.write(s)
|
|
|
65
87
|
writeErr("approve-unknown-ref\n");
|
|
66
88
|
return 78;
|
|
67
89
|
}
|
|
90
|
+
const policy = policyForApprove(stateDir, defer.claims.policyHash);
|
|
68
91
|
const result = await approvePending({
|
|
69
92
|
ledger,
|
|
70
93
|
recordSigner: signers.recordSigner,
|
|
@@ -75,6 +98,9 @@ export async function runApprove(argv, writeErr = (s) => process.stderr.write(s)
|
|
|
75
98
|
via: "cli",
|
|
76
99
|
policyHash: defer.claims.policyHash,
|
|
77
100
|
approvals,
|
|
101
|
+
...(policy
|
|
102
|
+
? { budgetGuard: createApprovalBudgetGuard({ policy, approvals, now: () => Date.now() }) }
|
|
103
|
+
: {}),
|
|
78
104
|
});
|
|
79
105
|
if (!result.ok) {
|
|
80
106
|
writeErr(`approve-${result.reason}\n`);
|
package/dist/cli.js
CHANGED
|
@@ -23,7 +23,7 @@ Usage: verax <command> [options]
|
|
|
23
23
|
witness <stateDir> run the witness alongside a body
|
|
24
24
|
halt <stateDir> stop the body from allowing anything further
|
|
25
25
|
unlock [--force] <stateDir> clear a stale ledger lock
|
|
26
|
-
desktop <args> open the local panel
|
|
26
|
+
desktop <args> open the local panel, joining the body that holds the ledger if one is up
|
|
27
27
|
|
|
28
28
|
--help, -h print this
|
|
29
29
|
--version, -v print the version
|
package/dist/desktop.d.ts
CHANGED
|
@@ -17,6 +17,29 @@ export declare function parseDesktopArgs(argv: string[]): DesktopOpts | {
|
|
|
17
17
|
error: string;
|
|
18
18
|
};
|
|
19
19
|
export declare function portOpen(port: number, host?: string): Promise<boolean>;
|
|
20
|
+
/** GET /healthz on the loopback port; true only for a 200. */
|
|
21
|
+
export declare function healthzUp(port: number): Promise<boolean>;
|
|
22
|
+
export type DesktopMode = {
|
|
23
|
+
mode: "spawn";
|
|
24
|
+
} | {
|
|
25
|
+
mode: "attach";
|
|
26
|
+
pid: number;
|
|
27
|
+
} | {
|
|
28
|
+
error: "desktop-body-locked";
|
|
29
|
+
pid: number;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* One ledger, one body. The desktop used to build its own issuer and body on
|
|
33
|
+
* every open, which on a machine whose body starts at logon put the window on
|
|
34
|
+
* a second, empty ledger: the panel looked broken while the real decisions sat
|
|
35
|
+
* in the directory the lock names. So the lock is read first. Held by a live
|
|
36
|
+
* process that answers /healthz where this run was told to look, the panel
|
|
37
|
+
* joins that body. Held by a live process that does not answer there, the
|
|
38
|
+
* desktop stops: someone owns the ledger and is not where we were pointed, and
|
|
39
|
+
* a second body would be refused by the lock anyway. A dead or unreadable lock
|
|
40
|
+
* is the body's own to clear (`verax unlock`); the desktop builds as before.
|
|
41
|
+
*/
|
|
42
|
+
export declare function desktopMode(stateDir: string, bodyPort: number, probe?: (port: number) => Promise<boolean>): Promise<DesktopMode>;
|
|
20
43
|
export declare function killTree(pid: number | undefined): void;
|
|
21
44
|
/** The panel's redirect is its own origin, so the issuer has to be told which port
|
|
22
45
|
* this run put it on: the allow-list default only holds 5173 and 4173. */
|
package/dist/desktop.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { get } from "node:http";
|
|
3
4
|
import { createConnection } from "node:net";
|
|
4
5
|
import { dirname, join } from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { pidAlive, readLockFile } from "./unlock.js";
|
|
6
8
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
7
9
|
const repoRoot = join(here, "..", "..", "..");
|
|
8
10
|
/** Newest modification time under a file or directory, ignoring build output. */
|
|
@@ -110,6 +112,42 @@ async function waitPort(port, ms) {
|
|
|
110
112
|
}
|
|
111
113
|
return false;
|
|
112
114
|
}
|
|
115
|
+
/** GET /healthz on the loopback port; true only for a 200. */
|
|
116
|
+
export function healthzUp(port) {
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
const req = get({ host: "127.0.0.1", port, path: "/healthz", timeout: 3_000 }, (res) => {
|
|
119
|
+
res.resume();
|
|
120
|
+
resolve(res.statusCode === 200);
|
|
121
|
+
});
|
|
122
|
+
req.once("timeout", () => {
|
|
123
|
+
req.destroy();
|
|
124
|
+
resolve(false);
|
|
125
|
+
});
|
|
126
|
+
req.once("error", () => resolve(false));
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* One ledger, one body. The desktop used to build its own issuer and body on
|
|
131
|
+
* every open, which on a machine whose body starts at logon put the window on
|
|
132
|
+
* a second, empty ledger: the panel looked broken while the real decisions sat
|
|
133
|
+
* in the directory the lock names. So the lock is read first. Held by a live
|
|
134
|
+
* process that answers /healthz where this run was told to look, the panel
|
|
135
|
+
* joins that body. Held by a live process that does not answer there, the
|
|
136
|
+
* desktop stops: someone owns the ledger and is not where we were pointed, and
|
|
137
|
+
* a second body would be refused by the lock anyway. A dead or unreadable lock
|
|
138
|
+
* is the body's own to clear (`verax unlock`); the desktop builds as before.
|
|
139
|
+
*/
|
|
140
|
+
export async function desktopMode(stateDir, bodyPort, probe = healthzUp) {
|
|
141
|
+
const lockPath = join(stateDir, "ledger.lock");
|
|
142
|
+
if (!existsSync(lockPath))
|
|
143
|
+
return { mode: "spawn" };
|
|
144
|
+
const lock = readLockFile(lockPath);
|
|
145
|
+
if (!lock || !pidAlive(lock.pid))
|
|
146
|
+
return { mode: "spawn" };
|
|
147
|
+
if (await probe(bodyPort))
|
|
148
|
+
return { mode: "attach", pid: lock.pid };
|
|
149
|
+
return { error: "desktop-body-locked", pid: lock.pid };
|
|
150
|
+
}
|
|
113
151
|
export function killTree(pid) {
|
|
114
152
|
if (pid == null)
|
|
115
153
|
return;
|
|
@@ -206,36 +244,48 @@ export async function runDesktop(opts, writeErr = (s) => process.stderr.write(s)
|
|
|
206
244
|
killTree(c.pid);
|
|
207
245
|
};
|
|
208
246
|
try {
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (!(await waitPort(opts.issuerPort, 15_000))) {
|
|
213
|
-
writeErr("desktop-issuer-timeout\n");
|
|
214
|
-
stopAll();
|
|
215
|
-
return 1;
|
|
216
|
-
}
|
|
217
|
-
const token = readFileSync(tokenPath, "utf8").trim();
|
|
218
|
-
if (token === "") {
|
|
219
|
-
writeErr("desktop-token-missing\n");
|
|
220
|
-
stopAll();
|
|
247
|
+
const decided = await desktopMode(opts.stateDir, opts.bodyPort);
|
|
248
|
+
if ("error" in decided) {
|
|
249
|
+
writeErr(`${decided.error}:${decided.pid}\n`);
|
|
221
250
|
return 1;
|
|
222
251
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
252
|
+
// Joining a running body: its issuer is whatever the body's resource
|
|
253
|
+
// metadata names, and its token is not ours to read. The panel's own
|
|
254
|
+
// origin has to be on that issuer's allow-list (VERAX_DEV_REDIRECT_URIS on
|
|
255
|
+
// the running issuer), which this run cannot set after the fact.
|
|
256
|
+
let token = null;
|
|
257
|
+
if (decided.mode === "spawn") {
|
|
258
|
+
const issuer = spawnLogged(process.execPath, ["--experimental-strip-types", issuerScript, "--out", tokenPath], issuerEnv(cleanEnv(), opts, audience, issuerUrl), repoRoot);
|
|
259
|
+
kids.push(issuer);
|
|
260
|
+
collectOutput(issuer, log);
|
|
261
|
+
if (!(await waitPort(opts.issuerPort, 15_000))) {
|
|
262
|
+
writeErr("desktop-issuer-timeout\n");
|
|
263
|
+
stopAll();
|
|
264
|
+
return 1;
|
|
265
|
+
}
|
|
266
|
+
token = readFileSync(tokenPath, "utf8").trim();
|
|
267
|
+
if (token === "") {
|
|
268
|
+
writeErr("desktop-token-missing\n");
|
|
269
|
+
stopAll();
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
const body = spawnLogged(process.execPath, ["--experimental-strip-types", mainTs], {
|
|
273
|
+
...cleanEnv(),
|
|
274
|
+
VERAX_STATE_DIR: opts.stateDir,
|
|
275
|
+
VERAX_ISSUER: issuerUrl,
|
|
276
|
+
VERAX_JWKS_URL: `${issuerUrl}/.well-known/jwks.json`,
|
|
277
|
+
VERAX_AUDIENCE: audience,
|
|
278
|
+
VERAX_BIND: `127.0.0.1:${opts.bodyPort}`,
|
|
279
|
+
VERAX_POLICY_FILE: policy,
|
|
280
|
+
...(opts.inventoryFile ? { VERAX_INVENTORY_FILE: opts.inventoryFile } : {}),
|
|
281
|
+
}, repoRoot);
|
|
282
|
+
kids.push(body);
|
|
283
|
+
collectOutput(body, log);
|
|
284
|
+
if (!(await waitPort(opts.bodyPort, 15_000))) {
|
|
285
|
+
writeErr("desktop-body-timeout\n");
|
|
286
|
+
stopAll();
|
|
287
|
+
return 1;
|
|
288
|
+
}
|
|
239
289
|
}
|
|
240
290
|
const panelSources = [
|
|
241
291
|
join(panelDir, "src"),
|
|
@@ -290,12 +340,14 @@ export async function runDesktop(opts, writeErr = (s) => process.stderr.write(s)
|
|
|
290
340
|
const browser = spawnLogged(browserBin, browserArgv, cleanEnv(), repoRoot, false);
|
|
291
341
|
kids.push(browser);
|
|
292
342
|
collectOutput(browser, log);
|
|
293
|
-
if (log.text.includes(token)) {
|
|
343
|
+
if (token !== null && log.text.includes(token)) {
|
|
294
344
|
writeErr("desktop-token-leaked\n");
|
|
295
345
|
stopAll();
|
|
296
346
|
return 1;
|
|
297
347
|
}
|
|
298
|
-
process.stdout.write(
|
|
348
|
+
process.stdout.write(decided.mode === "attach"
|
|
349
|
+
? `desktop-ready body=${opts.bodyPort} panel=${opts.panelPort} attached=1 pid=${decided.pid}\n`
|
|
350
|
+
: `desktop-ready issuer=${opts.issuerPort} body=${opts.bodyPort} panel=${opts.panelPort}\n`);
|
|
299
351
|
const onSignal = () => {
|
|
300
352
|
stopAll();
|
|
301
353
|
process.exit(1);
|
package/dist/doctor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { indexCoverage, listPieceFiles } from "@verax-ai/proxy";
|
|
3
4
|
import { loadConfig, isLoopbackHost } from "./config.js";
|
|
4
5
|
import { pidAlive, readLockFile } from "./unlock.js";
|
|
5
6
|
const SECRET_RE = /sk-|-----BEGIN|Bearer |eyJ[A-Za-z0-9_-]{10,}\./;
|
|
@@ -263,7 +264,51 @@ function heartbeatMaxMs(env) {
|
|
|
263
264
|
}
|
|
264
265
|
function evidenceChecks(stateDir, env) {
|
|
265
266
|
const checks = [];
|
|
266
|
-
|
|
267
|
+
let pieces;
|
|
268
|
+
try {
|
|
269
|
+
pieces = listPieceFiles(stateDir);
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
checks.push({ id: "ledger-manifest", level: "fail", detail: err.message });
|
|
273
|
+
return checks;
|
|
274
|
+
}
|
|
275
|
+
const srcLines = pieces.reduce((s, p) => s + countJsonl(p.decisions).lines, 0);
|
|
276
|
+
const decisionN = pieces.reduce((s, p) => s + (p.closed ? p.n : countJsonl(p.decisions).lines), 0);
|
|
277
|
+
if (decisionN > 0) {
|
|
278
|
+
const known = new Set(pieces.map((p) => p.id));
|
|
279
|
+
const cov = indexCoverage(stateDir);
|
|
280
|
+
if (cov === null) {
|
|
281
|
+
checks.push({
|
|
282
|
+
id: "ledger-index",
|
|
283
|
+
level: "fail",
|
|
284
|
+
detail: `index names 0 of ${decisionN} decisions; the next open rebuilds it, a running body misses the rest until then`,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
const ghost = [...cov.pieces].find((id) => !known.has(id));
|
|
289
|
+
if (ghost !== undefined) {
|
|
290
|
+
checks.push({
|
|
291
|
+
id: "ledger-index",
|
|
292
|
+
level: "fail",
|
|
293
|
+
detail: `index names piece ${ghost} that the manifest does not`,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
else if (cov.refs === decisionN) {
|
|
297
|
+
checks.push({
|
|
298
|
+
id: "ledger-index",
|
|
299
|
+
level: "ok",
|
|
300
|
+
detail: `index names ${decisionN} of ${decisionN} decisions`,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
checks.push({
|
|
305
|
+
id: "ledger-index",
|
|
306
|
+
level: "fail",
|
|
307
|
+
detail: `index names ${cov.refs} of ${decisionN} decisions; the next open rebuilds it, a running body misses the rest until then`,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
267
312
|
const hbPath = join(stateDir, "heartbeat.json");
|
|
268
313
|
let hb = null;
|
|
269
314
|
if (existsSync(hbPath)) {
|
|
@@ -274,7 +319,7 @@ function evidenceChecks(stateDir, env) {
|
|
|
274
319
|
hb = null;
|
|
275
320
|
}
|
|
276
321
|
}
|
|
277
|
-
if (
|
|
322
|
+
if (srcLines > 0 || hb) {
|
|
278
323
|
if (!hb || typeof hb.atMs !== "number") {
|
|
279
324
|
checks.push({
|
|
280
325
|
id: "heartbeat",
|
|
@@ -297,46 +342,58 @@ function evidenceChecks(stateDir, env) {
|
|
|
297
342
|
});
|
|
298
343
|
}
|
|
299
344
|
}
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
copy.
|
|
310
|
-
copyEffects.
|
|
311
|
-
|
|
345
|
+
// Each piece is mirrored on its own, so a short copy on one piece is a fail
|
|
346
|
+
// even when another piece's copy still matches.
|
|
347
|
+
let anything = false;
|
|
348
|
+
let fail = null;
|
|
349
|
+
let decisionLines = 0;
|
|
350
|
+
let effectLines = 0;
|
|
351
|
+
for (const piece of pieces) {
|
|
352
|
+
const src = countJsonl(piece.decisions);
|
|
353
|
+
const srcEffects = countJsonl(piece.effects);
|
|
354
|
+
const copy = countJsonl(piece.copyDecisions);
|
|
355
|
+
const copyEffects = countJsonl(piece.copyEffects);
|
|
356
|
+
decisionLines += src.lines;
|
|
357
|
+
effectLines += srcEffects.lines;
|
|
358
|
+
const pieceAnything = src.lines > 0 ||
|
|
359
|
+
srcEffects.lines > 0 ||
|
|
360
|
+
copy.lines > 0 ||
|
|
361
|
+
copyEffects.lines > 0 ||
|
|
362
|
+
copy.corrupt ||
|
|
363
|
+
copyEffects.corrupt;
|
|
364
|
+
if (!pieceAnything)
|
|
365
|
+
continue;
|
|
366
|
+
anything = true;
|
|
367
|
+
if (fail)
|
|
368
|
+
continue;
|
|
312
369
|
if (copy.corrupt || copyEffects.corrupt) {
|
|
313
|
-
|
|
370
|
+
fail = {
|
|
314
371
|
id: "evidence-copy",
|
|
315
372
|
level: "fail",
|
|
316
|
-
detail: `evidence-copy
|
|
317
|
-
}
|
|
373
|
+
detail: `evidence-copy piece ${piece.id} ${copy.corrupt ? "decisions" : "effects"}.jsonl is corrupt`,
|
|
374
|
+
};
|
|
318
375
|
}
|
|
319
376
|
else if (copy.lines < src.lines) {
|
|
320
|
-
|
|
377
|
+
fail = {
|
|
321
378
|
id: "evidence-copy",
|
|
322
379
|
level: "fail",
|
|
323
|
-
detail: `evidence copy is stale: ${copy.lines} lines behind source ${src.lines}`,
|
|
324
|
-
}
|
|
380
|
+
detail: `evidence copy is stale on piece ${piece.id}: ${copy.lines} lines behind source ${src.lines}`,
|
|
381
|
+
};
|
|
325
382
|
}
|
|
326
383
|
else if (copyEffects.lines < srcEffects.lines) {
|
|
327
|
-
|
|
384
|
+
fail = {
|
|
328
385
|
id: "evidence-copy",
|
|
329
386
|
level: "fail",
|
|
330
|
-
detail: `evidence copy is stale on effects: ${copyEffects.lines} effect line(s) behind source ${srcEffects.lines}`,
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
else {
|
|
334
|
-
checks.push({
|
|
335
|
-
id: "evidence-copy",
|
|
336
|
-
level: "ok",
|
|
337
|
-
detail: `evidence copy has ${copy.lines} decision line(s) and ${copyEffects.lines} effect line(s)`,
|
|
338
|
-
});
|
|
387
|
+
detail: `evidence copy is stale on piece ${piece.id} effects: ${copyEffects.lines} effect line(s) behind source ${srcEffects.lines}`,
|
|
388
|
+
};
|
|
339
389
|
}
|
|
340
390
|
}
|
|
391
|
+
if (anything) {
|
|
392
|
+
checks.push(fail ?? {
|
|
393
|
+
id: "evidence-copy",
|
|
394
|
+
level: "ok",
|
|
395
|
+
detail: `evidence copy has ${decisionLines} decision line(s) and ${effectLines} effect line(s)`,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
341
398
|
return checks;
|
|
342
399
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Principal, ToolCall, ToolResult } from "@verax-ai/proxy";
|
|
2
|
+
export type DownstreamToolFn = (call: ToolCall, principal: Principal, ref?: string) => Promise<ToolResult>;
|
|
3
|
+
export type DownstreamSpec = {
|
|
4
|
+
prefix: string;
|
|
5
|
+
command: string;
|
|
6
|
+
args?: string[];
|
|
7
|
+
cwd?: string;
|
|
8
|
+
env?: Record<string, string>;
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
};
|
|
11
|
+
export type DownstreamTool = {
|
|
12
|
+
name: string;
|
|
13
|
+
fn: DownstreamToolFn;
|
|
14
|
+
};
|
|
15
|
+
export type DownstreamSession = {
|
|
16
|
+
prefix: string;
|
|
17
|
+
tools: readonly DownstreamTool[];
|
|
18
|
+
close(): Promise<void>;
|
|
19
|
+
};
|
|
20
|
+
export declare class DownstreamCallError extends Error {
|
|
21
|
+
readonly prefix: string;
|
|
22
|
+
readonly tool: string;
|
|
23
|
+
constructor(prefix: string, tool: string, cause?: unknown);
|
|
24
|
+
}
|
|
25
|
+
export declare function prefixedName(prefix: string, childName: string): string;
|
|
26
|
+
export declare function extraToolNameOk(name: string): boolean;
|
|
27
|
+
export declare function parseDownstreamJson(raw: string): DownstreamSpec;
|
|
28
|
+
export declare function openDownstream(spec: DownstreamSpec): Promise<DownstreamSession>;
|