diff --git a/cli/src/__tests__/runtime.test.ts b/cli/src/__tests__/runtime.test.ts index babd9f8f10f..62afe9f49d4 100644 --- a/cli/src/__tests__/runtime.test.ts +++ b/cli/src/__tests__/runtime.test.ts @@ -50,3 +50,9 @@ it("passes explicit setup network settings without provider keys or ambient Node expect(environment).toEqual({ ...network, LANG: "C.UTF-8" }); expect(buildPiSetupEnvironment({})).toEqual({ PATH: "/usr/bin:/bin", LANG: "C.UTF-8" }); }); + +it("preserves lowercase proxy settings and leaves precedence to Node/npm", () => { + const lower = { http_proxy: "http://lower-proxy.example:8080", https_proxy: "http://lower-proxy.example:8080", no_proxy: "localhost" }; + expect(buildPiSetupEnvironment(lower)).toEqual({ PATH: "/usr/bin:/bin", LANG: "C.UTF-8", ...lower }); + expect(buildPiSetupEnvironment({ ...lower, HTTPS_PROXY: "http://upper-proxy.example:8080", OPENROUTER_API_KEY: "must-not-forward" })).toEqual({ PATH: "/usr/bin:/bin", LANG: "C.UTF-8", ...lower, HTTPS_PROXY: "http://upper-proxy.example:8080" }); +}); diff --git a/cli/src/commands/runtime.ts b/cli/src/commands/runtime.ts index 4526b23e4d8..c3e1b6365e0 100644 --- a/cli/src/commands/runtime.ts +++ b/cli/src/commands/runtime.ts @@ -30,7 +30,7 @@ export async function resolveRemoteCompanionImporter(serverUrl: string): Promise /** 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"]) { + for (const key of ["LC_ALL", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY", "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; diff --git a/doc/architecture/runner-pi-capabilities.md b/doc/architecture/runner-pi-capabilities.md index 527066125a7..79810979053 100644 --- a/doc/architecture/runner-pi-capabilities.md +++ b/doc/architecture/runner-pi-capabilities.md @@ -1130,7 +1130,8 @@ perform it automatically. It installs only this host's supported platform 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. Explicit setup preserves -`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, `SSL_CERT_FILE`, `SSL_CERT_DIR` and +`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, their lowercase equivalents, +`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 diff --git a/doc/plans/2026-10-02-pi-production-readiness.md b/doc/plans/2026-10-02-pi-production-readiness.md index 4d6df2cfc4a..67cb65a30ab 100644 --- a/doc/plans/2026-10-02-pi-production-readiness.md +++ b/doc/plans/2026-10-02-pi-production-readiness.md @@ -27,7 +27,7 @@ qualification. The existing draft stack must be reviewed in dependency order. | Runner protocol | Pi roster passes through the native packaged runner and authenticated mock control plane; lifecycle/denial/control cases remain distinct from Product tests | All seven cases are attempted at `0bd040093`: five pass; context-before-action retains its 120-second terminal timeout, and finish-task retains use of the advisory report without the required task mutation. One context/document workflow retry passes only after verified protection against the recorded host sleep; its original failure stays unchanged. The seven `b148b73ea` passes remain historical. | | Installed distribution | Public CLI/server tars on ARM Mac, Intel Mac and Linux; normal Pi setup; exact Linux companion and immutable Daytona image imported without binary override | Public CLI/server installation and normal Pi setup pass on all three platforms at original execution source `0bd040093`. The installer correction at `fcef1eae9` changes CLI and provisioner bytes; fresh normal public artifact/install proof must bind the updated source. Old artifact proofs keep their original labels. Public immutable image `ghcr.io/paperclipai/paperclip-daytona-runner@sha256:342f1fd5cb8cabfa2242f38f4f686608b28b6536aa879c54b0cb0da6fba9ef47` imports into native Linux Daytona and passes closed admission. Temporary sandbox cleanup passes. Image jobs 37102904889, 37106313185 and 37110683910 are terminal/cancelled. | | Governance and spend | Company isolation, human-only permission, duplicate/stale answers, Stop and budget hard stop; pricing estimates never become claimed bills | 58 focused governance/cost/session invariants pass at `f5f57e380`. Live pending-permission Stop passes at `03dd6ef93`. The qualification key cap is not proof of Paperclip budget enforcement. | -| Integration and rollout | Review each prerequisite; final-head typecheck, tests, build and CI pass; no unresolved review; exact artifacts; rollback recorded | Last verified readiness head `146f578c0` has 53 successful CI checks, two skips, current-head Greptile 5/5 and no unresolved root threads, including full build and typecheck. The new installer integration must receive its own completed current-head CI, review and updated public artifact/install proof. All 116 targeted native Linux fixture tests and 13 browser-case collections pass at `04f639eeb`; the new descriptor correction separately passes all 17 free native Linux fault tests. Full typecheck, complete test and build evidence remains recorded below; original failed commands remain failed. Frozen Pi execution inputs remain equivalent to `0bd040093`; the explicit installer has two changed executable inputs. Prerequisite #14921 passes every test/build/typecheck check but its valid admission review blocks merge. The retained SDK and setup network findings receive scoped corrections with focused verification; their new heads require fresh CI and review. Production stays held for eight failed gates and prerequisite disposition. No merge or release. | +| Integration and rollout | Review each prerequisite; final-head typecheck, tests, build and CI pass; no unresolved review; exact artifacts; rollback recorded | Last verified readiness head `146f578c0` has 53 successful CI checks, two skips, current-head Greptile 5/5 and no unresolved root threads, including full build and typecheck. The new installer integration must receive its own completed current-head CI, review and updated public artifact/install proof. All 116 targeted native Linux fixture tests and 13 browser-case collections pass at `04f639eeb`; the new descriptor correction separately passes all 17 free native Linux fault tests. Full typecheck, complete test and build evidence remains recorded below; original failed commands remain failed. Frozen Pi execution inputs remain equivalent to `0bd040093`; the explicit installer has three changed executable inputs. Prerequisite #14921 passes every test/build/typecheck check but its valid admission review blocks merge. The retained SDK and setup network findings receive scoped corrections with focused verification; their new heads require fresh CI and review. Production stays held for eight failed gates and prerequisite disposition. No merge or release. | ## Bounded execution @@ -82,8 +82,8 @@ success label is corrected by its strict verdict. All three owned sandboxes are deleted. The original paid rejection stage remains unproven. Prerequisite fixes preserve every previous commit. The latest heads are -#14921 `7f452cbd7`, #14922 `fcef1eae9`, #14923 `5aff21d77`, and #14924 -`5120ddc8f`. They carry the scoped UI/profile assertions and notice-display +#14921 `7f452cbd7`, #14922 `b7e692f48`, #14923 `cca38f29a`, and #14924 +`c2e2e39db`. They carry the scoped UI/profile assertions and notice-display corrections. The first prerequisite also receives the existing package-local Runner probe import repair and remaining admission assertions already present downstream. The complete repair includes both public probe export surfaces. @@ -132,7 +132,7 @@ canonical roots and all existing ambient-Node negative fixtures stay enforced. The explicit CLI setup child and its bundled provisioner now preserve the same closed allowlist: `PATH`, fixed `LANG`, optional `LC_ALL`, `HTTP_PROXY`, -`HTTPS_PROXY`, `NO_PROXY`, `SSL_CERT_FILE`, `SSL_CERT_DIR` and +`HTTPS_PROXY`, `NO_PROXY`, their lowercase equivalents, `SSL_CERT_FILE`, `SSL_CERT_DIR` and `NODE_EXTRA_CA_CERTS`. The CLI enables Node's environment proxy handling before the helper starts. Provider keys, `HOME`, npm configuration, `NODE_OPTIONS`, `NODE_PATH` and TLS-validation bypass remain excluded. These settings apply to @@ -150,6 +150,25 @@ failure; the exact correction delta and preserved negative tests then pass. Local missing-Commander and incomplete Product source-alias setup failures are retained separately; no repository test configuration changes occur. +Fresh review at `5120ddc8f` identifies omitted lowercase proxies. The scoped +follow-up at #14922 `b7e692f48` preserves `http_proxy`, `https_proxy` and +`no_proxy` through the CLI, provisioner and npm-download allowlists. All seven +CLI tests, eight SDK fixtures and two bundled checks pass there. Additive merges +preserve the later prerequisites' distinct runtime/build pins; their bundled +checks also pass. The overly broad whole-materializer equality guard remains a +local verification failure; exact six-file correction deltas pass afterward. + +A separate loopback-only calibration at #14924 `c2e2e39db` exercises the actual +bundled CLI and generated provisioner. Without its owned custom CA, the TLS +certificate is rejected before any archive request. With that CA, lowercase +proxies carry exactly one GET for the pinned Node archive; the owned endpoint +returns a deliberate 503 before any package is installed. Both children retire, +setup staging/locks disappear, the owned proxy/certificate fixture is removed +and no outside networking, real credentials or model calls occur. This is a +limited setup-boundary calibration with a private egress-denial prefix, not a +normal public artifact/install proof. The two earlier private calibration +startup failures remain retained after their shebang/guard repairs. + Normal updated CLI/server tar installation and setup on ARM Mac, Intel Mac and Linux remain required before releasing these installer bytes. Original `0bd040093` artifacts and all 33 paid grades retain their exact source labels. diff --git a/packages/paperclip-runner/scripts/materialize-pi-distribution.mjs b/packages/paperclip-runner/scripts/materialize-pi-distribution.mjs index df961b4b6c0..cbec504770f 100644 --- a/packages/paperclip-runner/scripts/materialize-pi-distribution.mjs +++ b/packages/paperclip-runner/scripts/materialize-pi-distribution.mjs @@ -174,7 +174,7 @@ export async function materializePiDistribution({ outputRoot, nodeExecutable, np const buildHome = join(staging, "build-home"); await mkdir(buildHome); // Do not inherit NPM_TOKEN, npm_config_*, NODE_OPTIONS, provider keys or user // .npmrc. Public registry downloads need no private application credential. - const environment = Object.fromEntries(["PATH", "LANG", "LC_ALL", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY", "SSL_CERT_FILE", "SSL_CERT_DIR", "NODE_EXTRA_CA_CERTS"].flatMap((key) => typeof process.env[key] === "string" ? [[key, process.env[key]]] : [])); + const environment = Object.fromEntries(["PATH", "LANG", "LC_ALL", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY", "https_proxy", "http_proxy", "no_proxy", "SSL_CERT_FILE", "SSL_CERT_DIR", "NODE_EXTRA_CA_CERTS"].flatMap((key) => typeof process.env[key] === "string" ? [[key, process.env[key]]] : [])); environment.HOME = buildHome; // npm 10 prunes non-host packages bundled by upstream Pi, unlike the npm // 11.19.0 used to qualify this complete closure. Public setup must use the diff --git a/packages/paperclip-runner/scripts/provision-pi-package.test.mjs b/packages/paperclip-runner/scripts/provision-pi-package.test.mjs index 2ca03443ce5..8e9a56e9383 100644 --- a/packages/paperclip-runner/scripts/provision-pi-package.test.mjs +++ b/packages/paperclip-runner/scripts/provision-pi-package.test.mjs @@ -34,14 +34,14 @@ 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" }; + const network = { PATH: "/usr/bin:/bin", HTTPS_PROXY: "http://proxy.example:8080", HTTP_PROXY: "http://proxy.example:8080", NO_PROXY: "localhost", 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: "/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, `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 }); + 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.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", 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}`]); @@ -60,7 +60,7 @@ test("explicit CLI setup passes only network settings to its real child and enab 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.NO_PROXY,'localhost');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'); @@ -69,7 +69,7 @@ test("explicit CLI setup passes only network settings to its real child and enab 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" }, + 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", 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/); diff --git a/packages/paperclip-runner/scripts/provision-pi.mjs b/packages/paperclip-runner/scripts/provision-pi.mjs index ae4a4862518..a544815aa56 100644 --- a/packages/paperclip-runner/scripts/provision-pi.mjs +++ b/packages/paperclip-runner/scripts/provision-pi.mjs @@ -11,7 +11,7 @@ import { PI_DISTRIBUTION_CLOSURE_SHA256 } from "../src/drivers/acpx/pi-closure-p 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"]) { + for (const key of ["LC_ALL", "HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY", "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;