@verax-ai/body 0.1.4 → 0.2.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 +1 -1
- package/dist/doctor.js +28 -0
- package/dist/downstream.d.ts +6 -0
- package/dist/downstream.js +24 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Bearer token the configured issuer did not sign.
|
|
|
35
35
|
| `VERAX_POLICY_FILE` | yes | Policy the gate applies. `@verax-ai/proxy` ships `policy/default.json`, which denies what it does not name. |
|
|
36
36
|
| `VERAX_BIND` | no | `host:port` to listen on. Default `127.0.0.1:8787`; anything but loopback needs `VERAX_TLS_TERMINATED=1`. |
|
|
37
37
|
| `VERAX_INVENTORY_FILE` | no | Roster document the body serves. The format is `@verax-ai/inventory`. |
|
|
38
|
-
| `VERAX_DOWNSTREAM` | no | Path to a JSON document naming MCP servers to put behind this gate: one `{ prefix, command?, args?, cwd?, env?, url?, headers?, timeoutMs? }` or an array of them. Each child names exactly one way in — `command` to spawn it over stdio, or `url` to reach one already running over Streamable HTTP, with `headers` for its own bearer. A path and not the JSON itself, because the document may name a key in `env` or `headers`. Their tools are served as `prefix.childName` and need an exact policy rule under that name; a named child that cannot be opened stops the body. |
|
|
38
|
+
| `VERAX_DOWNSTREAM` | no | Path to a JSON document naming MCP servers to put behind this gate: one `{ prefix, command?, args?, cwd?, env?, url?, headers?, timeoutMs?, trust? }` or an array of them. Each child names exactly one way in — `command` to spawn it over stdio, or `url` to reach one already running over Streamable HTTP, with `headers` for its own bearer. A stdio child requires `"trust": "same-user"` (it runs as the same user as the body and can read the signing keys); an HTTP child does not ask for that field and must not carry it. This is a breaking change: existing stdio documents do not open until the field is added. A path and not the JSON itself, because the document may name a key in `env` or `headers`. Their tools are served as `prefix.childName` and need an exact policy rule under that name; a named child that cannot be opened stops the body. |
|
|
39
39
|
|
|
40
40
|
`verax doctor` names what is missing. A misconfigured body exits with code 78
|
|
41
41
|
before it listens.
|
package/dist/doctor.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { indexCoverage, listPieceFiles } from "@verax-ai/proxy";
|
|
4
4
|
import { loadConfig, isLoopbackHost } from "./config.js";
|
|
5
|
+
import { parseDownstreamDocument } from "./downstream.js";
|
|
5
6
|
import { pidAlive, readLockFile } from "./unlock.js";
|
|
6
7
|
const SECRET_RE = /sk-|-----BEGIN|Bearer |eyJ[A-Za-z0-9_-]{10,}\./;
|
|
7
8
|
const SECRET_NAME = /^(VERAX_DEV_TOKEN|.*_(TOKEN|SECRET|KEY))$/;
|
|
@@ -184,6 +185,33 @@ export function runDoctor(env, argv) {
|
|
|
184
185
|
});
|
|
185
186
|
}
|
|
186
187
|
}
|
|
188
|
+
const downstreamPath = env.VERAX_DOWNSTREAM?.trim() ?? "";
|
|
189
|
+
if (downstreamPath !== "") {
|
|
190
|
+
try {
|
|
191
|
+
const specs = parseDownstreamDocument(readFileSync(downstreamPath, "utf8"));
|
|
192
|
+
const stdio = specs.filter((s) => s.command !== undefined);
|
|
193
|
+
const http = specs.filter((s) => s.url !== undefined);
|
|
194
|
+
checks.push({
|
|
195
|
+
id: "downstream-document",
|
|
196
|
+
level: "ok",
|
|
197
|
+
detail: `${specs.length} child(ren): ${stdio.length} stdio, ${http.length} http`,
|
|
198
|
+
});
|
|
199
|
+
for (const spec of stdio) {
|
|
200
|
+
checks.push({
|
|
201
|
+
id: `downstream-stdio:${spec.prefix}`,
|
|
202
|
+
level: "warn",
|
|
203
|
+
detail: `stdio child ${spec.prefix} runs as the same user as the body and can read the body's signing keys (keys/*.pem). An untrusted child should be reached over HTTP, ideally on a separate machine or under a separate operating-system user.`,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
checks.push({
|
|
209
|
+
id: "downstream-document",
|
|
210
|
+
level: "fail",
|
|
211
|
+
detail: err instanceof Error ? err.message : "downstream-document-unreadable",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
187
215
|
return checks;
|
|
188
216
|
}
|
|
189
217
|
const DEFAULT_REDIRECTS = ["http://127.0.0.1:5173/", "http://127.0.0.1:4173/"];
|
package/dist/downstream.d.ts
CHANGED
|
@@ -18,6 +18,12 @@ export type DownstreamSpec = {
|
|
|
18
18
|
/** HTTP: headers the operator sends to the child, such as its own bearer. */
|
|
19
19
|
headers?: Record<string, string>;
|
|
20
20
|
timeoutMs?: number;
|
|
21
|
+
/**
|
|
22
|
+
* stdio only. The operator wrote that this child runs as the same user
|
|
23
|
+
* as the body and can read the body's signing keys. Required on stdio;
|
|
24
|
+
* forbidden on HTTP.
|
|
25
|
+
*/
|
|
26
|
+
trust?: "same-user";
|
|
21
27
|
};
|
|
22
28
|
export type DownstreamTool = {
|
|
23
29
|
name: string;
|
package/dist/downstream.js
CHANGED
|
@@ -29,6 +29,18 @@ export function prefixedName(prefix, childName) {
|
|
|
29
29
|
export function extraToolNameOk(name) {
|
|
30
30
|
return EXTRA_NAME_RE.test(name);
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* The operator wrote `"trust": "same-user"` on a stdio child. Missing ack
|
|
34
|
+
* refuses start. One rule, two call sites — do not duplicate the predicate.
|
|
35
|
+
*/
|
|
36
|
+
function assertStdioTrusted(spec) {
|
|
37
|
+
if (spec.command !== undefined && spec.trust !== "same-user") {
|
|
38
|
+
// The prefix is the operator's own name for the child and names WHICH entry
|
|
39
|
+
// to fix in a document with several. Nothing else about the child goes in:
|
|
40
|
+
// not the command, the arguments or the environment.
|
|
41
|
+
throw new Error(`downstream-stdio-trust-required:${spec.prefix}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
32
44
|
export function parseDownstreamJson(raw) {
|
|
33
45
|
let parsed;
|
|
34
46
|
try {
|
|
@@ -117,6 +129,16 @@ export function parseDownstreamJson(raw) {
|
|
|
117
129
|
}
|
|
118
130
|
spec.timeoutMs = rec.timeoutMs;
|
|
119
131
|
}
|
|
132
|
+
if (rec.trust !== undefined) {
|
|
133
|
+
// HTTP + any trust, or a value other than "same-user", is the same
|
|
134
|
+
// refusal: the field is not a claim about a remote host, and it is
|
|
135
|
+
// not a free-form string.
|
|
136
|
+
if (spec.url !== undefined || rec.trust !== "same-user") {
|
|
137
|
+
throw new Error("downstream-trust-invalid");
|
|
138
|
+
}
|
|
139
|
+
spec.trust = "same-user";
|
|
140
|
+
}
|
|
141
|
+
assertStdioTrusted(spec);
|
|
120
142
|
return spec;
|
|
121
143
|
}
|
|
122
144
|
/**
|
|
@@ -175,6 +197,8 @@ function stdioTransport(spec) {
|
|
|
175
197
|
if (spec.command === undefined || spec.command.trim() === "") {
|
|
176
198
|
throw new Error("downstream-command-invalid");
|
|
177
199
|
}
|
|
200
|
+
// Last check before the spawn: openDownstream can be called without parse.
|
|
201
|
+
assertStdioTrusted(spec);
|
|
178
202
|
// Safe inherit + operator overlay. Not process.env: that would copy
|
|
179
203
|
// VERAX_* tokens the body already holds.
|
|
180
204
|
const env = { ...getDefaultEnvironment(), ...(spec.env ?? {}) };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@verax-ai/body",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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.2.0",
|
|
26
|
+
"@verax-ai/proxy": "^0.2.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.",
|