@verax-ai/body 0.1.0 → 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/LICENSE +192 -0
- package/README.md +60 -0
- package/dist/agents.d.ts +33 -0
- package/dist/agents.js +59 -0
- package/dist/approve-cli.js +27 -1
- package/dist/cli.js +5 -4
- 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 +12 -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>;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
+
import { getDefaultEnvironment, StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
4
|
+
const PREFIX_RE = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;
|
|
5
|
+
const CHILD_TOOL_RE = /^[A-Za-z][A-Za-z0-9._-]{0,63}$/;
|
|
6
|
+
/** Prefixed name a caller may put on extraTools: `prefix.childName`. */
|
|
7
|
+
const EXTRA_NAME_RE = /^[A-Za-z][A-Za-z0-9_-]{0,31}\.[A-Za-z][A-Za-z0-9._-]{0,63}$/;
|
|
8
|
+
export class DownstreamCallError extends Error {
|
|
9
|
+
prefix;
|
|
10
|
+
tool;
|
|
11
|
+
constructor(prefix, tool, cause) {
|
|
12
|
+
const detail = cause instanceof Error ? cause.message : "call-failed";
|
|
13
|
+
super(`downstream-call-failed:${prefix}.${tool}:${detail}`);
|
|
14
|
+
this.name = "DownstreamCallError";
|
|
15
|
+
this.prefix = prefix;
|
|
16
|
+
this.tool = tool;
|
|
17
|
+
if (cause !== undefined)
|
|
18
|
+
this.cause = cause;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function prefixedName(prefix, childName) {
|
|
22
|
+
return `${prefix}.${childName}`;
|
|
23
|
+
}
|
|
24
|
+
export function extraToolNameOk(name) {
|
|
25
|
+
return EXTRA_NAME_RE.test(name);
|
|
26
|
+
}
|
|
27
|
+
export function parseDownstreamJson(raw) {
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(raw);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new Error("downstream-json-invalid");
|
|
34
|
+
}
|
|
35
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
36
|
+
throw new Error("downstream-json-not-object");
|
|
37
|
+
}
|
|
38
|
+
const rec = parsed;
|
|
39
|
+
if (typeof rec.prefix !== "string" || !PREFIX_RE.test(rec.prefix)) {
|
|
40
|
+
throw new Error("downstream-prefix-invalid");
|
|
41
|
+
}
|
|
42
|
+
if (typeof rec.command !== "string" || rec.command.trim() === "") {
|
|
43
|
+
throw new Error("downstream-command-invalid");
|
|
44
|
+
}
|
|
45
|
+
const spec = { prefix: rec.prefix, command: rec.command };
|
|
46
|
+
if (rec.args !== undefined) {
|
|
47
|
+
if (!Array.isArray(rec.args) || rec.args.some((a) => typeof a !== "string")) {
|
|
48
|
+
throw new Error("downstream-args-invalid");
|
|
49
|
+
}
|
|
50
|
+
spec.args = rec.args;
|
|
51
|
+
}
|
|
52
|
+
if (rec.cwd !== undefined) {
|
|
53
|
+
if (typeof rec.cwd !== "string" || rec.cwd === "") {
|
|
54
|
+
throw new Error("downstream-cwd-invalid");
|
|
55
|
+
}
|
|
56
|
+
spec.cwd = rec.cwd;
|
|
57
|
+
}
|
|
58
|
+
if (rec.env !== undefined) {
|
|
59
|
+
if (rec.env === null || typeof rec.env !== "object" || Array.isArray(rec.env)) {
|
|
60
|
+
throw new Error("downstream-env-invalid");
|
|
61
|
+
}
|
|
62
|
+
const env = {};
|
|
63
|
+
for (const [k, v] of Object.entries(rec.env)) {
|
|
64
|
+
if (typeof v !== "string")
|
|
65
|
+
throw new Error("downstream-env-invalid");
|
|
66
|
+
env[k] = v;
|
|
67
|
+
}
|
|
68
|
+
spec.env = env;
|
|
69
|
+
}
|
|
70
|
+
if (rec.timeoutMs !== undefined) {
|
|
71
|
+
if (typeof rec.timeoutMs !== "number" || !Number.isInteger(rec.timeoutMs) || rec.timeoutMs <= 0) {
|
|
72
|
+
throw new Error("downstream-timeout-invalid");
|
|
73
|
+
}
|
|
74
|
+
spec.timeoutMs = rec.timeoutMs;
|
|
75
|
+
}
|
|
76
|
+
return spec;
|
|
77
|
+
}
|
|
78
|
+
// The SDK result is a union (current shape with an index signature, or the
|
|
79
|
+
// legacy { toolResult } shape), so it is narrowed here rather than trusted.
|
|
80
|
+
function asTextResult(raw) {
|
|
81
|
+
const rec = raw !== null && typeof raw === "object" ? raw : {};
|
|
82
|
+
const content = [];
|
|
83
|
+
const items = Array.isArray(rec.content) ? rec.content : [];
|
|
84
|
+
for (const item of items) {
|
|
85
|
+
if (item !== null &&
|
|
86
|
+
typeof item === "object" &&
|
|
87
|
+
item.type === "text" &&
|
|
88
|
+
typeof item.text === "string") {
|
|
89
|
+
content.push({ type: "text", text: item.text });
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
content.push({ type: "text", text: JSON.stringify(item) });
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
content: content.length > 0 ? content : [{ type: "text", text: "" }],
|
|
96
|
+
isError: rec.isError === true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async function shut(client, transport) {
|
|
100
|
+
await client.close().catch(() => undefined);
|
|
101
|
+
await transport.close().catch(() => undefined);
|
|
102
|
+
}
|
|
103
|
+
export async function openDownstream(spec) {
|
|
104
|
+
if (!PREFIX_RE.test(spec.prefix)) {
|
|
105
|
+
throw new Error("downstream-prefix-invalid");
|
|
106
|
+
}
|
|
107
|
+
if (spec.command.trim() === "") {
|
|
108
|
+
throw new Error("downstream-command-invalid");
|
|
109
|
+
}
|
|
110
|
+
const timeoutMs = spec.timeoutMs ?? 10_000;
|
|
111
|
+
// Safe inherit + operator overlay. Not process.env: that would copy
|
|
112
|
+
// VERAX_* tokens the body already holds.
|
|
113
|
+
const env = { ...getDefaultEnvironment(), ...(spec.env ?? {}) };
|
|
114
|
+
const transport = new StdioClientTransport({
|
|
115
|
+
command: spec.command,
|
|
116
|
+
args: spec.args,
|
|
117
|
+
cwd: spec.cwd,
|
|
118
|
+
env,
|
|
119
|
+
stderr: "pipe",
|
|
120
|
+
});
|
|
121
|
+
// The SDK types the pipe as Stream; only a Readable can be drained.
|
|
122
|
+
const stderr = transport.stderr;
|
|
123
|
+
if (stderr instanceof Readable)
|
|
124
|
+
stderr.resume();
|
|
125
|
+
const client = new Client({ name: "verax-downstream", version: "0.0.0" });
|
|
126
|
+
let listed;
|
|
127
|
+
try {
|
|
128
|
+
await client.connect(transport);
|
|
129
|
+
listed = await client.listTools();
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
await shut(client, transport);
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
const tools = [];
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
for (const tool of listed.tools) {
|
|
138
|
+
if (!CHILD_TOOL_RE.test(tool.name)) {
|
|
139
|
+
await shut(client, transport);
|
|
140
|
+
throw new Error(`downstream-child-name-invalid:${tool.name}`);
|
|
141
|
+
}
|
|
142
|
+
const name = prefixedName(spec.prefix, tool.name);
|
|
143
|
+
if (seen.has(name)) {
|
|
144
|
+
await shut(client, transport);
|
|
145
|
+
throw new Error(`downstream-name-collision:${name}`);
|
|
146
|
+
}
|
|
147
|
+
seen.add(name);
|
|
148
|
+
const childName = tool.name;
|
|
149
|
+
tools.push({
|
|
150
|
+
name,
|
|
151
|
+
fn: async (call) => {
|
|
152
|
+
let raw;
|
|
153
|
+
try {
|
|
154
|
+
// Mutation check: stub this call and the allow test expecting
|
|
155
|
+
// "pong":true from the child goes red.
|
|
156
|
+
raw = await client.callTool({ name: childName, arguments: call.arguments }, undefined, { timeout: timeoutMs });
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
throw new DownstreamCallError(spec.prefix, childName, err);
|
|
160
|
+
}
|
|
161
|
+
const result = asTextResult(raw);
|
|
162
|
+
if (result.isError) {
|
|
163
|
+
throw new DownstreamCallError(spec.prefix, childName, textOf(result) || "isError");
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
let closed = false;
|
|
170
|
+
return {
|
|
171
|
+
prefix: spec.prefix,
|
|
172
|
+
tools,
|
|
173
|
+
async close() {
|
|
174
|
+
if (closed)
|
|
175
|
+
return;
|
|
176
|
+
closed = true;
|
|
177
|
+
await shut(client, transport);
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function textOf(result) {
|
|
182
|
+
return result.content.map((c) => c.text).join("");
|
|
183
|
+
}
|
package/dist/inputs-read.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The inputs documents behind these decisions, keyed by ref, each checked
|
|
3
|
+
* against the hash its decision recorded. inputs.jsonl is written in the
|
|
4
|
+
* same order as decisions.jsonl, so the rows for a window of decisions sit
|
|
5
|
+
* near its end: the file is read from there, and the read stops once every
|
|
6
|
+
* ref has been seen. The newest row for a ref is the one that counts.
|
|
7
|
+
*/
|
|
1
8
|
export declare function matchingInputs(stateDir: string, decisions: {
|
|
2
9
|
claims: {
|
|
3
10
|
ref: string | null;
|
package/dist/inputs-read.js
CHANGED
|
@@ -1,57 +1,64 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
3
|
import { canonical } from "@cedulon/core";
|
|
4
|
+
import { listPieceFiles, readLedgerTail } from "@verax-ai/proxy";
|
|
5
5
|
function sha256Canonical(value) {
|
|
6
6
|
return createHash("sha256").update(canonical(value), "utf8").digest("hex");
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* The inputs documents behind these decisions, keyed by ref, each checked
|
|
10
|
+
* against the hash its decision recorded. inputs.jsonl is written in the
|
|
11
|
+
* same order as decisions.jsonl, so the rows for a window of decisions sit
|
|
12
|
+
* near its end: the file is read from there, and the read stops once every
|
|
13
|
+
* ref has been seen. The newest row for a ref is the one that counts.
|
|
14
|
+
*/
|
|
8
15
|
export async function matchingInputs(stateDir, decisions) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
catch (err) {
|
|
14
|
-
if (err.code === "ENOENT")
|
|
15
|
-
return {};
|
|
16
|
-
throw err;
|
|
17
|
-
}
|
|
18
|
-
const byRef = new Map();
|
|
19
|
-
for (const line of text.split("\n")) {
|
|
20
|
-
if (line === "")
|
|
21
|
-
continue;
|
|
22
|
-
const row = JSON.parse(line);
|
|
23
|
-
if (typeof row.ref === "string")
|
|
24
|
-
byRef.set(row.ref, row.inputs);
|
|
16
|
+
const wanted = new Map();
|
|
17
|
+
for (const d of decisions) {
|
|
18
|
+
if (d.claims.ref && typeof d.claims.inputsHash === "string")
|
|
19
|
+
wanted.set(d.claims.ref, d.claims.inputsHash);
|
|
25
20
|
}
|
|
26
21
|
const out = {};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
22
|
+
if (wanted.size === 0)
|
|
23
|
+
return out;
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
const pieces = [...listPieceFiles(stateDir)].reverse();
|
|
26
|
+
for (const piece of pieces) {
|
|
27
|
+
if (seen.size === wanted.size)
|
|
28
|
+
break;
|
|
29
|
+
await readLedgerTail(piece.inputs, (row) => {
|
|
30
|
+
if (typeof row.ref !== "string" || seen.has(row.ref))
|
|
31
|
+
return "skip";
|
|
32
|
+
const hash = wanted.get(row.ref);
|
|
33
|
+
if (hash === undefined)
|
|
34
|
+
return "skip";
|
|
35
|
+
seen.add(row.ref);
|
|
36
|
+
if (sha256Canonical(row.inputs) === hash)
|
|
37
|
+
out[row.ref] = row.inputs;
|
|
38
|
+
return seen.size === wanted.size ? "stop" : "skip";
|
|
39
|
+
});
|
|
35
40
|
}
|
|
36
41
|
return out;
|
|
37
42
|
}
|
|
38
43
|
export async function inputsPrincipal(stateDir, ref) {
|
|
39
|
-
let text;
|
|
40
|
-
try {
|
|
41
|
-
text = await readFile(join(stateDir, "inputs.jsonl"), "utf8");
|
|
42
|
-
}
|
|
43
|
-
catch (err) {
|
|
44
|
-
if (err.code === "ENOENT")
|
|
45
|
-
return null;
|
|
46
|
-
throw err;
|
|
47
|
-
}
|
|
48
44
|
let found = null;
|
|
49
|
-
for (const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
for (const piece of listPieceFiles(stateDir)) {
|
|
46
|
+
let text;
|
|
47
|
+
try {
|
|
48
|
+
text = await readFile(piece.inputs, "utf8");
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (err.code === "ENOENT")
|
|
52
|
+
continue;
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
55
|
+
for (const line of text.split("\n")) {
|
|
56
|
+
if (line === "")
|
|
57
|
+
continue;
|
|
58
|
+
const row = JSON.parse(line);
|
|
59
|
+
if (row.ref === ref)
|
|
60
|
+
found = row.inputs?.principal ?? null;
|
|
61
|
+
}
|
|
55
62
|
}
|
|
56
63
|
if (!found || typeof found.brain !== "string")
|
|
57
64
|
return null;
|
package/dist/no-bypass-scan.js
CHANGED
|
@@ -26,6 +26,9 @@ const CREATE_REQUIRE = /\bcreateRequire\b/;
|
|
|
26
26
|
const CONCAT_TOOLS = /["']\.\/to["']\s*\+\s*["']ols/;
|
|
27
27
|
const CHILD = /\bchild_process\b/;
|
|
28
28
|
const WORKER = /\bworker_threads\b/;
|
|
29
|
+
// The SDK stdio client transport spawns a process without naming
|
|
30
|
+
// child_process in this tree. Only src/downstream.ts may use it.
|
|
31
|
+
const SDK_STDIO = /client\/stdio\.js|\bStdioClientTransport\b/;
|
|
29
32
|
function lineAt(text, index) {
|
|
30
33
|
return text.slice(0, index).split(/\n/).length;
|
|
31
34
|
}
|
|
@@ -51,6 +54,8 @@ export function scanNoBypass(base = pkgRoot, extra = []) {
|
|
|
51
54
|
const wiring = rel === "src/wiring.ts";
|
|
52
55
|
// Desktop supervises issuer/body/panel. It is not a tool-call path.
|
|
53
56
|
const desktop = rel === "src/desktop.ts";
|
|
57
|
+
// The downstream spike attaches one stdio MCP server. Not a tool-call path.
|
|
58
|
+
const downstream = rel === "src/downstream.ts";
|
|
54
59
|
const originalLines = text.split(/\r?\n/);
|
|
55
60
|
for (const match of text.matchAll(DYNAMIC_IMPORT)) {
|
|
56
61
|
const index = match.index ?? 0;
|
|
@@ -89,6 +94,8 @@ export function scanNoBypass(base = pkgRoot, extra = []) {
|
|
|
89
94
|
push("child_process");
|
|
90
95
|
if (WORKER.test(line))
|
|
91
96
|
push("worker_threads");
|
|
97
|
+
if (SDK_STDIO.test(line) && !downstream)
|
|
98
|
+
push("sdk-stdio-spawn");
|
|
92
99
|
}
|
|
93
100
|
}
|
|
94
101
|
return hits;
|