@verax-ai/body 0.1.1 → 0.1.3

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.
@@ -0,0 +1,274 @@
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
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
+ const PREFIX_RE = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;
6
+ const CHILD_TOOL_RE = /^[A-Za-z][A-Za-z0-9._-]{0,63}$/;
7
+ /** Prefixed name a caller may put on extraTools: `prefix.childName`. */
8
+ const EXTRA_NAME_RE = /^[A-Za-z][A-Za-z0-9_-]{0,31}\.[A-Za-z][A-Za-z0-9._-]{0,63}$/;
9
+ export class DownstreamCallError extends Error {
10
+ prefix;
11
+ tool;
12
+ constructor(prefix, tool, cause) {
13
+ const detail = cause instanceof Error ? cause.message : "call-failed";
14
+ super(`downstream-call-failed:${prefix}.${tool}:${detail}`);
15
+ this.name = "DownstreamCallError";
16
+ this.prefix = prefix;
17
+ this.tool = tool;
18
+ if (cause !== undefined)
19
+ this.cause = cause;
20
+ }
21
+ }
22
+ export function prefixedName(prefix, childName) {
23
+ return `${prefix}.${childName}`;
24
+ }
25
+ export function extraToolNameOk(name) {
26
+ return EXTRA_NAME_RE.test(name);
27
+ }
28
+ export function parseDownstreamJson(raw) {
29
+ let parsed;
30
+ try {
31
+ parsed = JSON.parse(raw);
32
+ }
33
+ catch {
34
+ throw new Error("downstream-json-invalid");
35
+ }
36
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
37
+ throw new Error("downstream-json-not-object");
38
+ }
39
+ const rec = parsed;
40
+ if (typeof rec.prefix !== "string" || !PREFIX_RE.test(rec.prefix)) {
41
+ throw new Error("downstream-prefix-invalid");
42
+ }
43
+ const cmdVar = rec.command !== undefined;
44
+ const urlVar = rec.url !== undefined;
45
+ if (cmdVar && urlVar)
46
+ throw new Error("downstream-transport-ambiguous");
47
+ if (!cmdVar && !urlVar)
48
+ throw new Error("downstream-transport-missing");
49
+ const spec = { prefix: rec.prefix };
50
+ if (cmdVar) {
51
+ if (typeof rec.command !== "string" || rec.command.trim() === "") {
52
+ throw new Error("downstream-command-invalid");
53
+ }
54
+ spec.command = rec.command;
55
+ }
56
+ else {
57
+ if (typeof rec.url !== "string" || rec.url.trim() === "") {
58
+ throw new Error("downstream-url-invalid");
59
+ }
60
+ let parsed;
61
+ try {
62
+ parsed = new URL(rec.url);
63
+ }
64
+ catch {
65
+ throw new Error("downstream-url-invalid");
66
+ }
67
+ // Only the two schemes the SDK transport speaks. A `file:` or `ftp:` URL
68
+ // would be read as a fetch target by something later, so it is refused here.
69
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
70
+ throw new Error("downstream-url-invalid");
71
+ }
72
+ spec.url = rec.url;
73
+ if (rec.headers !== undefined) {
74
+ if (rec.headers === null || typeof rec.headers !== "object" || Array.isArray(rec.headers)) {
75
+ throw new Error("downstream-headers-invalid");
76
+ }
77
+ const headers = {};
78
+ for (const [k, v] of Object.entries(rec.headers)) {
79
+ if (typeof v !== "string")
80
+ throw new Error("downstream-headers-invalid");
81
+ headers[k] = v;
82
+ }
83
+ spec.headers = headers;
84
+ }
85
+ }
86
+ if (rec.args !== undefined) {
87
+ if (!Array.isArray(rec.args) || rec.args.some((a) => typeof a !== "string")) {
88
+ throw new Error("downstream-args-invalid");
89
+ }
90
+ spec.args = rec.args;
91
+ }
92
+ if (rec.cwd !== undefined) {
93
+ if (typeof rec.cwd !== "string" || rec.cwd === "") {
94
+ throw new Error("downstream-cwd-invalid");
95
+ }
96
+ spec.cwd = rec.cwd;
97
+ }
98
+ if (rec.env !== undefined) {
99
+ if (rec.env === null || typeof rec.env !== "object" || Array.isArray(rec.env)) {
100
+ throw new Error("downstream-env-invalid");
101
+ }
102
+ const env = {};
103
+ for (const [k, v] of Object.entries(rec.env)) {
104
+ if (typeof v !== "string")
105
+ throw new Error("downstream-env-invalid");
106
+ env[k] = v;
107
+ }
108
+ spec.env = env;
109
+ }
110
+ if (rec.timeoutMs !== undefined) {
111
+ if (typeof rec.timeoutMs !== "number" || !Number.isInteger(rec.timeoutMs) || rec.timeoutMs <= 0) {
112
+ throw new Error("downstream-timeout-invalid");
113
+ }
114
+ spec.timeoutMs = rec.timeoutMs;
115
+ }
116
+ return spec;
117
+ }
118
+ /**
119
+ * The `VERAX_DOWNSTREAM` document: one child, or an array of them. An empty
120
+ * array is a document that attaches nothing, which is not the same as no
121
+ * document at all — the operator wrote it, so it is honoured.
122
+ */
123
+ export function parseDownstreamDocument(raw) {
124
+ let parsed;
125
+ try {
126
+ parsed = JSON.parse(raw);
127
+ }
128
+ catch {
129
+ throw new Error("downstream-json-invalid");
130
+ }
131
+ const items = Array.isArray(parsed) ? parsed : [parsed];
132
+ const specs = [];
133
+ const prefixes = new Set();
134
+ for (const item of items) {
135
+ const spec = parseDownstreamJson(JSON.stringify(item));
136
+ if (prefixes.has(spec.prefix)) {
137
+ throw new Error(`downstream-prefix-duplicate:${spec.prefix}`);
138
+ }
139
+ prefixes.add(spec.prefix);
140
+ specs.push(spec);
141
+ }
142
+ return specs;
143
+ }
144
+ // The SDK result is a union (current shape with an index signature, or the
145
+ // legacy { toolResult } shape), so it is narrowed here rather than trusted.
146
+ function asTextResult(raw) {
147
+ const rec = raw !== null && typeof raw === "object" ? raw : {};
148
+ const content = [];
149
+ const items = Array.isArray(rec.content) ? rec.content : [];
150
+ for (const item of items) {
151
+ if (item !== null &&
152
+ typeof item === "object" &&
153
+ item.type === "text" &&
154
+ typeof item.text === "string") {
155
+ content.push({ type: "text", text: item.text });
156
+ continue;
157
+ }
158
+ content.push({ type: "text", text: JSON.stringify(item) });
159
+ }
160
+ return {
161
+ content: content.length > 0 ? content : [{ type: "text", text: "" }],
162
+ isError: rec.isError === true,
163
+ };
164
+ }
165
+ async function shut(client, transport) {
166
+ await client.close().catch(() => undefined);
167
+ await transport.close().catch(() => undefined);
168
+ }
169
+ /** Spawns the child. Its stderr is drained so a chatty child cannot block it. */
170
+ function stdioTransport(spec) {
171
+ if (spec.command === undefined || spec.command.trim() === "") {
172
+ throw new Error("downstream-command-invalid");
173
+ }
174
+ // Safe inherit + operator overlay. Not process.env: that would copy
175
+ // VERAX_* tokens the body already holds.
176
+ const env = { ...getDefaultEnvironment(), ...(spec.env ?? {}) };
177
+ const transport = new StdioClientTransport({
178
+ command: spec.command,
179
+ args: spec.args,
180
+ cwd: spec.cwd,
181
+ env,
182
+ stderr: "pipe",
183
+ });
184
+ // The SDK types the pipe as Stream; only a Readable can be drained.
185
+ const stderr = transport.stderr;
186
+ if (stderr instanceof Readable)
187
+ stderr.resume();
188
+ return transport;
189
+ }
190
+ /**
191
+ * Talks to a server already running. The headers are the operator's, from the
192
+ * document — the body's own bearer is never forwarded, for the same
193
+ * confused-deputy reason the stdio child does not inherit `VERAX_*`.
194
+ */
195
+ function httpTransport(spec) {
196
+ if (spec.url === undefined)
197
+ throw new Error("downstream-url-invalid");
198
+ return new StreamableHTTPClientTransport(new URL(spec.url), {
199
+ ...(spec.headers ? { requestInit: { headers: spec.headers } } : {}),
200
+ });
201
+ }
202
+ export async function openDownstream(spec) {
203
+ if (!PREFIX_RE.test(spec.prefix)) {
204
+ throw new Error("downstream-prefix-invalid");
205
+ }
206
+ if (spec.command !== undefined && spec.url !== undefined) {
207
+ throw new Error("downstream-transport-ambiguous");
208
+ }
209
+ if (spec.command === undefined && spec.url === undefined) {
210
+ throw new Error("downstream-transport-missing");
211
+ }
212
+ const timeoutMs = spec.timeoutMs ?? 10_000;
213
+ const transport = spec.url !== undefined ? httpTransport(spec) : stdioTransport(spec);
214
+ const client = new Client({ name: "verax-downstream", version: "0.0.0" });
215
+ let listed;
216
+ try {
217
+ await client.connect(transport);
218
+ listed = await client.listTools();
219
+ }
220
+ catch (err) {
221
+ await shut(client, transport);
222
+ throw err;
223
+ }
224
+ const tools = [];
225
+ const seen = new Set();
226
+ for (const tool of listed.tools) {
227
+ if (!CHILD_TOOL_RE.test(tool.name)) {
228
+ await shut(client, transport);
229
+ throw new Error(`downstream-child-name-invalid:${tool.name}`);
230
+ }
231
+ const name = prefixedName(spec.prefix, tool.name);
232
+ if (seen.has(name)) {
233
+ await shut(client, transport);
234
+ throw new Error(`downstream-name-collision:${name}`);
235
+ }
236
+ seen.add(name);
237
+ const childName = tool.name;
238
+ tools.push({
239
+ name,
240
+ ...(typeof tool.description === "string" ? { description: tool.description } : {}),
241
+ ...(tool.inputSchema !== undefined ? { inputSchema: tool.inputSchema } : {}),
242
+ fn: async (call) => {
243
+ let raw;
244
+ try {
245
+ // Mutation check: stub this call and the allow test expecting
246
+ // "pong":true from the child goes red.
247
+ raw = await client.callTool({ name: childName, arguments: call.arguments }, undefined, { timeout: timeoutMs });
248
+ }
249
+ catch (err) {
250
+ throw new DownstreamCallError(spec.prefix, childName, err);
251
+ }
252
+ const result = asTextResult(raw);
253
+ if (result.isError) {
254
+ throw new DownstreamCallError(spec.prefix, childName, textOf(result) || "isError");
255
+ }
256
+ return result;
257
+ },
258
+ });
259
+ }
260
+ let closed = false;
261
+ return {
262
+ prefix: spec.prefix,
263
+ tools,
264
+ async close() {
265
+ if (closed)
266
+ return;
267
+ closed = true;
268
+ await shut(client, transport);
269
+ },
270
+ };
271
+ }
272
+ function textOf(result) {
273
+ return result.content.map((c) => c.text).join("");
274
+ }
@@ -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;
@@ -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
- let text;
10
- try {
11
- text = await readFile(join(stateDir, "inputs.jsonl"), "utf8");
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
- for (const d of decisions) {
28
- const ref = d.claims.ref;
29
- if (!ref || typeof d.claims.inputsHash !== "string")
30
- continue;
31
- const doc = byRef.get(ref);
32
- if (doc !== undefined && sha256Canonical(doc) === d.claims.inputsHash) {
33
- out[ref] = doc;
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 line of text.split("\n")) {
50
- if (line === "")
51
- continue;
52
- const row = JSON.parse(line);
53
- if (row.ref === ref)
54
- found = row.inputs?.principal ?? null;
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;
@@ -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;
package/dist/server.d.ts CHANGED
@@ -1,5 +1,188 @@
1
1
  import { type Server } from "node:http";
2
2
  import type { BodyConfig } from "./config.ts";
3
3
  import { TOOL_NAMES } from "./wiring.ts";
4
+ export declare const TOOL_META: ({
5
+ name: string;
6
+ description: string;
7
+ inputSchema: {
8
+ type: string;
9
+ additionalProperties: boolean;
10
+ properties: {
11
+ id: {
12
+ type: string;
13
+ description: string;
14
+ };
15
+ body?: undefined;
16
+ source?: undefined;
17
+ validFromMs?: undefined;
18
+ validUntilMs?: undefined;
19
+ ref?: undefined;
20
+ to?: undefined;
21
+ text?: undefined;
22
+ _ref?: undefined;
23
+ amountMinor?: undefined;
24
+ currency?: undefined;
25
+ payee?: undefined;
26
+ reference?: undefined;
27
+ };
28
+ required: string[];
29
+ };
30
+ } | {
31
+ name: string;
32
+ description: string;
33
+ inputSchema: {
34
+ type: string;
35
+ additionalProperties: boolean;
36
+ properties: {
37
+ id: {
38
+ type: string;
39
+ description: string;
40
+ };
41
+ body: {
42
+ description: string;
43
+ };
44
+ source: {
45
+ type: string;
46
+ description: string;
47
+ };
48
+ validFromMs: {
49
+ type: string;
50
+ description: string;
51
+ };
52
+ validUntilMs: {
53
+ type: string;
54
+ description: string;
55
+ };
56
+ ref?: undefined;
57
+ to?: undefined;
58
+ text?: undefined;
59
+ _ref?: undefined;
60
+ amountMinor?: undefined;
61
+ currency?: undefined;
62
+ payee?: undefined;
63
+ reference?: undefined;
64
+ };
65
+ required: string[];
66
+ };
67
+ } | {
68
+ name: string;
69
+ description: string;
70
+ inputSchema: {
71
+ type: string;
72
+ additionalProperties: boolean;
73
+ properties: {
74
+ ref: {
75
+ type: string;
76
+ description: string;
77
+ };
78
+ id?: undefined;
79
+ body?: undefined;
80
+ source?: undefined;
81
+ validFromMs?: undefined;
82
+ validUntilMs?: undefined;
83
+ to?: undefined;
84
+ text?: undefined;
85
+ _ref?: undefined;
86
+ amountMinor?: undefined;
87
+ currency?: undefined;
88
+ payee?: undefined;
89
+ reference?: undefined;
90
+ };
91
+ required: string[];
92
+ };
93
+ } | {
94
+ name: string;
95
+ description: string;
96
+ inputSchema: {
97
+ type: string;
98
+ additionalProperties: boolean;
99
+ properties: {
100
+ id?: undefined;
101
+ body?: undefined;
102
+ source?: undefined;
103
+ validFromMs?: undefined;
104
+ validUntilMs?: undefined;
105
+ ref?: undefined;
106
+ to?: undefined;
107
+ text?: undefined;
108
+ _ref?: undefined;
109
+ amountMinor?: undefined;
110
+ currency?: undefined;
111
+ payee?: undefined;
112
+ reference?: undefined;
113
+ };
114
+ required?: undefined;
115
+ };
116
+ } | {
117
+ name: string;
118
+ description: string;
119
+ inputSchema: {
120
+ type: string;
121
+ additionalProperties: boolean;
122
+ properties: {
123
+ to: {
124
+ type: string;
125
+ description: string;
126
+ };
127
+ text: {
128
+ type: string;
129
+ description: string;
130
+ };
131
+ _ref: {
132
+ type: string;
133
+ description: string;
134
+ };
135
+ id?: undefined;
136
+ body?: undefined;
137
+ source?: undefined;
138
+ validFromMs?: undefined;
139
+ validUntilMs?: undefined;
140
+ ref?: undefined;
141
+ amountMinor?: undefined;
142
+ currency?: undefined;
143
+ payee?: undefined;
144
+ reference?: undefined;
145
+ };
146
+ required: string[];
147
+ };
148
+ } | {
149
+ name: string;
150
+ description: string;
151
+ inputSchema: {
152
+ type: string;
153
+ additionalProperties: boolean;
154
+ properties: {
155
+ amountMinor: {
156
+ type: string;
157
+ description: string;
158
+ };
159
+ currency: {
160
+ type: string;
161
+ description: string;
162
+ };
163
+ payee: {
164
+ type: string;
165
+ description: string;
166
+ };
167
+ reference: {
168
+ type: string;
169
+ description: string;
170
+ };
171
+ _ref: {
172
+ type: string;
173
+ description: string;
174
+ };
175
+ id?: undefined;
176
+ body?: undefined;
177
+ source?: undefined;
178
+ validFromMs?: undefined;
179
+ validUntilMs?: undefined;
180
+ ref?: undefined;
181
+ to?: undefined;
182
+ text?: undefined;
183
+ };
184
+ required: string[];
185
+ };
186
+ })[];
4
187
  export declare function listen(config: BodyConfig): Promise<Server>;
5
188
  export { TOOL_NAMES };