Preserve explicit Pi setup network settings and current SDK fixtures

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-05 14:27:02 -05:00
1 parent ec67ba1e05
commit fcef1eae9b
6 files changed
+77 -15

No files matched your search

+8 -1
View File
@@ -4,7 +4,7 @@ import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { Command } from "commander";
import { afterEach, expect, it } from "vitest";
import { registerRuntimeCommands, resolvePiProvisioner, resolveRemoteCompanionImporter } from "../commands/runtime.js";
import { buildPiSetupEnvironment, registerRuntimeCommands, resolvePiProvisioner, resolveRemoteCompanionImporter } from "../commands/runtime.js";
const roots: string[] = [];
afterEach(async () => { await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))); });
async function fixture() {
@@ -43,3 +43,10 @@ it("requires an explicit digest for the companion import command", async () => {
const program = new Command(); program.exitOverride().configureOutput({ writeErr() {} }); registerRuntimeCommands(program);
await expect(program.parseAsync(["node", "paperclipai", "runtime", "import-remote", "/unused"])).rejects.toThrow("sha256");
});
it("passes explicit setup network settings without provider keys or ambient Node/npm configuration", () => {
const network = { PATH: "/public/bin", LC_ALL: "C.UTF-8", HTTPS_PROXY: "http://proxy.example:8080", HTTP_PROXY: "http://proxy.example:8080", NO_PROXY: "localhost", SSL_CERT_FILE: "/public/ca.pem", SSL_CERT_DIR: "/public/certs", NODE_EXTRA_CA_CERTS: "/public/extra-ca.pem" };
const environment = buildPiSetupEnvironment({ ...network, LANG: "foreign", HOME: "/private/home", OPENROUTER_API_KEY: "must-not-forward", ANTHROPIC_API_KEY: "must-not-forward", NPM_TOKEN: "must-not-forward", npm_config_registry: "https://foreign.example", NODE_OPTIONS: "--require /foreign.js", NODE_PATH: "/foreign/modules", NODE_TLS_REJECT_UNAUTHORIZED: "0" });
expect(environment).toEqual({ ...network, LANG: "C.UTF-8" });
expect(buildPiSetupEnvironment({})).toEqual({ PATH: "/usr/bin:/bin", LANG: "C.UTF-8" });
});
+13 -4
View File
@@ -27,12 +27,21 @@ export async function resolveRemoteCompanionImporter(serverUrl: string): Promise
return modulePath;
}
/** Public downloads may use operator network settings, never application secrets. */
export function buildPiSetupEnvironment(source: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
const environment: NodeJS.ProcessEnv = { PATH: source.PATH ?? "/usr/bin:/bin", LANG: "C.UTF-8" };
for (const key of ["LC_ALL", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY", "SSL_CERT_FILE", "SSL_CERT_DIR", "NODE_EXTRA_CA_CERTS"]) {
if (typeof source[key] === "string") environment[key] = source[key];
}
return environment;
}
export async function setupPiRuntime(): Promise<void> {
const provisioner = await resolvePiProvisioner(import.meta.resolve("@paperclipai/server"));
// Only the explicit setup command can download pinned public dependencies.
// Do not forward provider credentials, proxy/npm config, HOME or NODE_OPTIONS.
const child = spawn(process.execPath, [provisioner], {
stdio: "inherit", env: { PATH: process.env.PATH ?? "/usr/bin:/bin", LANG: "C.UTF-8" },
// Only explicit setup downloads. Enable Node's proxy handling for the helper;
// provider keys, npm configuration, HOME and Node injection remain excluded.
const child = spawn(process.execPath, ["--use-env-proxy", provisioner], {
stdio: "inherit", env: buildPiSetupEnvironment(),
});
const cancel = () => { if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM"); };
process.on("SIGINT", cancel); process.on("SIGTERM", cancel);
+6 -2
View File
@@ -1043,8 +1043,12 @@ perform it automatically. It installs only this host's supported platform
(macOS ARM64, macOS x64, or Linux x64), using the source-pinned Node archive, npm
lock, wrapper patch and complete Pi closure. Node 24, npm, git, tar and the normal
platform dependency inspector (`otool` or `ldd`) must be available. The server
package must be writable by the installing account. Setup forwards no instance
configuration, provider credentials, npm configuration or proxy credentials.
package must be writable by the installing account. Explicit setup preserves
`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, `SSL_CERT_FILE`, `SSL_CERT_DIR` and
`NODE_EXTRA_CA_CERTS` for public downloads, and enables Node's environment proxy
handling. The provisioner keeps the same closed allowlist. Instance settings,
provider credentials, user npm configuration, `HOME`, `NODE_OPTIONS` and
`NODE_PATH` are excluded; TLS certificate validation stays enabled.
The public server carries a self-contained setup tool and small pinned inputs in
`dist/vendor/paperclip-runner/cli`; the installed host closure lives in that
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { createRequire } from "node:module";
import test from "node:test";
import { build } from "esbuild";
import { bundlePiProvisioner } from "./build-verified-provider-entrypoints.mjs";
test("public server tar layout carries a self-contained host provisioner and exact small inputs", async t => {
@@ -33,13 +34,44 @@ test("public server tar layout carries a self-contained host provisioner and exa
const installedPackage = join(installed, "package"); const entry = join(installedPackage, "dist/vendor/paperclip-runner/cli/provision-pi.cjs");
const api = createRequire(import.meta.url)(entry);
assert.equal((await api.provisionPackageRoot(entry)).root, installedPackage);
const network = { PATH: "/usr/bin:/bin", HTTPS_PROXY: "http://proxy.example:8080", HTTP_PROXY: "http://proxy.example:8080", NO_PROXY: "localhost", SSL_CERT_FILE: "/public/ca.pem", SSL_CERT_DIR: "/public/certs", NODE_EXTRA_CA_CERTS: "/public/extra-ca.pem" };
assert.deepEqual(api.provisionEnvironment({ ...network, HOME: "/private/home", OPENROUTER_API_KEY: "sensitive-canary", NPM_TOKEN: "sensitive-canary", NODE_OPTIONS: "--require /foreign.js", NODE_PATH: "/foreign", NODE_TLS_REJECT_UNAUTHORIZED: "0" }), { ...network, LANG: "C.UTF-8" });
// Real, unmocked installation verifier rejects a corrupt cache before any
// download/process. The positive full-closure proof uses real platform packs.
await mkdir(join(installedPackage, "provider-assets/pi", `${process.platform}-${process.arch}`, "runtime"), { recursive: true });
const deny = join(root, "deny.cjs");
await writeFile(deny, `const fail=()=>{throw Error('UNEXPECTED_NETWORK_OR_CHILD')}; globalThis.fetch=fail; for(const m of ['node:net','node:tls','node:http','node:https']){const x=require(m);for(const k of ['connect','createConnection','request','get'])if(k in x)x[k]=fail;}const c=require('node:child_process');for(const k of ['spawn','execFile','exec'])c[k]=fail;`);
const result = spawnSync(process.execPath, ["--require", deny, entry], { encoding: "utf8", env: { PATH: "/usr/bin:/bin", OPENROUTER_API_KEY: "sensitive-canary", NODE_OPTIONS: "" }, timeout: 10_000 });
await writeFile(deny, `process.nextTick(()=>{const assert=require('node:assert/strict');assert.equal(process.env.HTTP_PROXY,'http://127.0.0.1:9');assert.equal(process.env.HTTPS_PROXY,'http://127.0.0.1:9');assert.equal(process.env.NO_PROXY,'localhost');assert.equal(process.env.NODE_EXTRA_CA_CERTS,'/dev/null');for(const key of ['OPENROUTER_API_KEY','NPM_TOKEN','HOME','NODE_OPTIONS','NODE_PATH','NODE_TLS_REJECT_UNAUTHORIZED'])assert.equal(process.env[key],undefined,key);});const fail=()=>{throw Error('UNEXPECTED_NETWORK_OR_CHILD')}; globalThis.fetch=fail; for(const m of ['node:net','node:tls','node:http','node:https']){const x=require(m);for(const k of ['connect','createConnection','request','get'])if(k in x)x[k]=fail;}const c=require('node:child_process');for(const k of ['spawn','execFile','exec'])c[k]=fail;`);
const result = spawnSync(process.execPath, ["--require", deny, entry], { encoding: "utf8", env: { PATH: "/usr/bin:/bin", HTTP_PROXY: "http://127.0.0.1:9", HTTPS_PROXY: "http://127.0.0.1:9", NO_PROXY: "localhost", NODE_EXTRA_CA_CERTS: "/dev/null", OPENROUTER_API_KEY: "sensitive-canary", NPM_TOKEN: "sensitive-canary", HOME: "/private/home", NODE_OPTIONS: "", NODE_PATH: "/foreign", NODE_TLS_REJECT_UNAUTHORIZED: "0" }, timeout: 10_000 });
assert.ifError(result.error); assert.equal(result.status, 1); assert.match(result.stderr, /Pi setup failed/);
assert.doesNotMatch(result.stderr, /UNEXPECTED_NETWORK_OR_CHILD|sensitive-canary/);
assert.deepEqual(await readdir(join(installedPackage, "provider-assets/pi")), [`${process.platform}-${process.arch}`]);
});
test("explicit CLI setup passes only network settings to its real child and enables Node proxy handling", async t => {
const root = await mkdtemp(join(tmpdir(), "paperclip-pi-setup-environment-"));
t.after(() => rm(root, { recursive: true, force: true }));
const server = join(root, "node_modules/@paperclipai/server");
const cli = join(server, "dist/vendor/paperclip-runner/cli");
await mkdir(cli, { recursive: true });
await writeFile(join(server, "package.json"), JSON.stringify({ name: "@paperclipai/server", type: "module", exports: "./dist/index.js" }));
await writeFile(join(server, "dist/index.js"), "throw Error('server must not start')");
// A capture child models the public layout only. It cannot download or launch Pi.
await writeFile(join(cli, "provision-pi.cjs"), `const assert=require('node:assert/strict');
assert.deepEqual(process.execArgv,['--use-env-proxy']);
assert.equal(process.env.HTTP_PROXY,'http://127.0.0.1:9');
assert.equal(process.env.HTTPS_PROXY,'http://127.0.0.1:9');
assert.equal(process.env.NO_PROXY,'localhost');
assert.equal(process.env.SSL_CERT_FILE,'/public/ca.pem');
assert.equal(process.env.SSL_CERT_DIR,'/public/certs');
assert.equal(process.env.NODE_EXTRA_CA_CERTS,'/dev/null');
for(const key of ['OPENROUTER_API_KEY','NPM_TOKEN','HOME','NODE_OPTIONS','NODE_PATH','NODE_TLS_REJECT_UNAUTHORIZED']) assert.equal(process.env[key],undefined,key);
console.log('SETUP_NETWORK_BOUNDARY_PASS');`);
const modulePath = join(root, "runtime.mjs");
await build({ entryPoints: [resolve(dirname(new URL(import.meta.url).pathname), "../../../cli/src/commands/runtime.ts")], outfile: modulePath, bundle: true, platform: "node", format: "esm", target: "node24", logLevel: "silent" });
const result = spawnSync(process.execPath, ["--input-type=module", "-e", "const runtime=await import(process.argv[1]);await runtime.setupPiRuntime();", modulePath], {
encoding: "utf8", timeout: 10_000, env: { PATH: "/usr/bin:/bin", HTTP_PROXY: "http://127.0.0.1:9", HTTPS_PROXY: "http://127.0.0.1:9", NO_PROXY: "localhost", SSL_CERT_FILE: "/public/ca.pem", SSL_CERT_DIR: "/public/certs", NODE_EXTRA_CA_CERTS: "/dev/null", OPENROUTER_API_KEY: "sensitive-canary", NPM_TOKEN: "sensitive-canary", HOME: "/private/home", NODE_PATH: "/foreign", NODE_TLS_REJECT_UNAUTHORIZED: "0" },
});
assert.ifError(result.error); assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /SETUP_NETWORK_BOUNDARY_PASS/);
assert.doesNotMatch(result.stdout + result.stderr, /sensitive-canary/);
});
@@ -9,6 +9,14 @@ import { verifyPiInstallation } from "../src/drivers/acpx/pi-installation.ts";
import { QUALIFIED_ACPX_PROFILES } from "../src/drivers/acpx/qualified-profiles.ts";
import { PI_DISTRIBUTION_CLOSURE_SHA256 } from "../src/drivers/acpx/pi-closure-pins.ts";
export function provisionEnvironment(source = process.env) {
const environment = { PATH: source.PATH ?? "/usr/bin:/bin", LANG: "C.UTF-8" };
for (const key of ["LC_ALL", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY", "SSL_CERT_FILE", "SSL_CERT_DIR", "NODE_EXTRA_CA_CERTS"]) {
if (typeof source[key] === "string") environment[key] = source[key];
}
return environment;
}
export async function provisionPackageRoot(entrypoint) {
const canonical = await realpath(entrypoint);
if (canonical !== resolve(entrypoint)) throw new Error("Pi setup entrypoint must not be linked");
@@ -116,9 +124,9 @@ export async function provisionPi(entrypoint, checkCancelled = () => {}) {
if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
const args = process.argv.slice(2);
if (args.length) throw new Error("Usage: paperclipai runtime setup pi (explicit host-platform installation; no model calls)");
// Setup uses only public, source-pinned downloads. Never forward credentials,
// HOME config, proxy configuration, NODE_OPTIONS or npm configuration.
const environment = { PATH: process.env.PATH ?? "/usr/bin:/bin", LANG: "C.UTF-8" };
// Preserve the same closed network allowlist as the explicit CLI command.
// Never forward provider keys, HOME config, NODE_OPTIONS or npm configuration.
const environment = provisionEnvironment();
for (const key of Object.keys(process.env)) delete process.env[key]; Object.assign(process.env, environment);
let cancelled = false;
const cancel = () => { cancelled = true; };
@@ -1,5 +1,6 @@
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { mkdtemp, mkdir, realpath, rm, symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
@@ -10,6 +11,7 @@ import { buildPaperclipServerEnvironment } from "./harness-env.js";
import { setupLiveFixtures } from "./live-fixtures.js";
import type { RunnerApi } from "./api.js";
const cell = runnerMatrix.find(e => e.id === "extended-harnesses.runner-acpx-pi.daytona.hello-complete")!;
const sdkVersion: string = JSON.parse(readFileSync(new URL("../../packages/plugins/sdk/package.json", import.meta.url), "utf8")).version;
const roots: string[] = [];
const hash = (v: string) => createHash("sha256").update(v).digest("hex");
afterEach(async () => { vi.unstubAllEnvs(); await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))); });
@@ -21,9 +23,9 @@ async function fixture() {
await writeFile(join(dir, "package.json"), content); await writeFile(join(dir, entry), "// compiled");
return { root: dir, packageSha256: hash(content), entry, entrySha256: hash("// compiled") };
}
const plugin = await pkg("@paperclipai/plugin-daytona", "0.1.1", { "@paperclipai/plugin-sdk": "0.3.1", "@daytonaio/sdk": "0.203.0" }, { paperclipPlugin: { manifest: "./dist/manifest.js", worker: "./dist/worker.js" } });
const plugin = await pkg("@paperclipai/plugin-daytona", "0.1.1", { "@paperclipai/plugin-sdk": sdkVersion, "@daytonaio/sdk": "0.203.0" }, { paperclipPlugin: { manifest: "./dist/manifest.js", worker: "./dist/worker.js" } });
await writeFile(join(plugin.root, "dist/manifest.js"), "// manifest"); await writeFile(join(plugin.root, "dist/worker.js"), "// worker");
const authority: InstalledDaytonaPluginAuthority = { schema: "paperclip.e2e.installed-daytona-plugin/v1", graphRoot: root, plugin: { ...plugin, manifestSha256: hash("// manifest"), workerSha256: hash("// worker") }, sdk: await pkg("@paperclipai/plugin-sdk", "0.3.1", { "@paperclipai/shared": "0.3.1" }), shared: await pkg("@paperclipai/shared", "0.3.1"), daytona: await pkg("@daytonaio/sdk", "0.203.0") };
const authority: InstalledDaytonaPluginAuthority = { schema: "paperclip.e2e.installed-daytona-plugin/v1", graphRoot: root, plugin: { ...plugin, manifestSha256: hash("// manifest"), workerSha256: hash("// worker") }, sdk: await pkg("@paperclipai/plugin-sdk", sdkVersion, { "@paperclipai/shared": "0.3.1" }), shared: await pkg("@paperclipai/shared", "0.3.1"), daytona: await pkg("@daytonaio/sdk", "0.203.0") };
const authorityPath = join(root, "authority.json");
const env: NodeJS.ProcessEnv = { PAPERCLIP_RUNNER_E2E_INSTALLED_CLI: "/reviewed/cli", PAPERCLIP_RUNNER_E2E_INSTALLED_CLI_SHA256: "reviewed-separately", PAPERCLIP_RUNNER_E2E_INSTALLED_SERVER_ROOT: "/reviewed/server", PAPERCLIP_RUNNER_E2E_INSTALLED_SERVER_SHA256: "reviewed-separately", PAPERCLIP_RUNNER_E2E_INSTALLED_DAYTONA_PLUGIN: plugin.root, PAPERCLIP_RUNNER_E2E_INSTALLED_DAYTONA_PLUGIN_AUTHORITY: authorityPath };
async function seal() { const bytes = JSON.stringify(authority); await writeFile(authorityPath, bytes); env.PAPERCLIP_RUNNER_E2E_INSTALLED_DAYTONA_PLUGIN_AUTHORITY_SHA256 = hash(bytes); }
@@ -51,7 +53,7 @@ it("rejects stale authority, worker, manifest and dependency entry bytes", async
});
it("rejects source exports and workspace dependency versions even with refreshed pins", async () => {
const f = await fixture(); const p = join(f.authority.plugin.root,"package.json");
const original = { name:"@paperclipai/plugin-daytona",version:"0.1.1",exports:{".":{import:"./dist/index.js"}},paperclipPlugin:{manifest:"./dist/manifest.js",worker:"./dist/worker.js"},dependencies:{"@paperclipai/plugin-sdk":"0.3.1","@daytonaio/sdk":"0.203.0"} };
const original = { name:"@paperclipai/plugin-daytona",version:"0.1.1",exports:{".":{import:"./dist/index.js"}},paperclipPlugin:{manifest:"./dist/manifest.js",worker:"./dist/worker.js"},dependencies:{"@paperclipai/plugin-sdk":sdkVersion,"@daytonaio/sdk":"0.203.0"} };
for (const manifest of [{ ...original, exports:{".":{import:"./src/index.ts"}} }, { ...original, dependencies:{ ...original.dependencies,"@paperclipai/plugin-sdk":"workspace:*" } }]) {
const bytes=JSON.stringify(manifest);await writeFile(p,bytes);f.authority.plugin.packageSha256=hash(bytes);await f.seal();await expect(verifyInstalledDaytonaPlugin(f.env,[cell])).rejects.toThrow(/compiled package exports|dependency identity/);
}