From fe2537fff55dbc3ef8e276678cc9d682cd44b93e Mon Sep 17 00:00:00 2001 From: cedrecode Date: Wed, 26 Aug 2026 20:58:43 +0200 Subject: [PATCH] fix: isolate runner startup from project config --- CHANGES.md | 4 ++ .../poteto-mode/scripts/runner/entry.ts | 5 ++ .../poteto-mode/scripts/runner/pstack-runner | 14 ++-- .../poteto-mode/scripts/runner/run.test.ts | 67 ++++++++++++++++++- 4 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 plugins/pstack/skills/poteto-mode/scripts/runner/entry.ts diff --git a/CHANGES.md b/CHANGES.md index 8953a58..423ec12 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,10 @@ This port applies the Cursor → Claude Code substitutions in skill bodies. Earlier drafts left them flagged; this revision resolves them. A later pass added a Codex build that shares the same skills; see [Codex port](#codex-port) below. +## Unreleased isolates runner startup from project-controlled Bun configuration + +`pstack-runner` now starts Bun through a small POSIX shell boundary that disables automatic environment-file loading, ignores the repository's `bunfig.toml`, and removes inherited Bun and Node startup options before trusted TypeScript runs. It still inherits ordinary environment variables that the parent explicitly passes, but a repository can no longer inject credentials or preload code before the runner performs its subscription preflight. A boundary regression test executes the shipped wrapper from a fixture repository and covers `.env`, `bunfig.toml`, `BUN_OPTIONS`, and `NODE_OPTIONS`. + ## 1.2.0 adds verified multi-PR plans, earlier runtime diagnostics, and shared review-bot triage Plans with several stages now use one checklist instead of an overview and separate files for each stage. It has one ordered section for every pull request and keeps all ten ways of testing the real product, unit tests, live and performance proof, checks for how changes work together, merge rules, and supporting details in one place. A Node-based checker with no extra dependencies rejects missing or out-of-order sections, fake screenshots, empty definitions of success, incomplete performance proof, incorrectly written review checks, unsupported punctuation, and incorrect command use. Claude Code and Codex use the same installed skill and checker through their existing parent-controlled setup. If a provider fails, it is identified by name and treated as a dropout. No backup provider or hidden time limit was added. diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/entry.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/entry.ts new file mode 100644 index 0000000..2a357bb --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/entry.ts @@ -0,0 +1,5 @@ +export {}; + +const startedAt = Date.now(); +const { main } = await import("./cli.ts"); +process.exitCode = await main(process.argv.slice(2), startedAt); diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/pstack-runner b/plugins/pstack/skills/poteto-mode/scripts/runner/pstack-runner index 7cd5a6e..e7880a3 100755 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/pstack-runner +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/pstack-runner @@ -1,4 +1,10 @@ -#!/usr/bin/env bun -const startedAt = Date.now(); -const { main } = await import("./cli.ts"); -process.exitCode = await main(process.argv.slice(2), startedAt); +#!/bin/sh +set -eu + +runner_dir=$(CDPATH= cd -P "$(dirname "$0")" && pwd) + +# Bun reads these before application code runs. Do not let an untrusted project +# select an env file or preload code into the trusted runner process. +unset BUN_OPTIONS NODE_OPTIONS + +exec bun --no-env-file --config=/dev/null "$runner_dir/entry.ts" "$@" diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/run.test.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/run.test.ts index deac398..6d52046 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/run.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/run.test.ts @@ -158,7 +158,7 @@ function receipt(path: string): RunnerReceipt { function runnerArgs(input: RunnerOptions): string[] { const args = [ - join(import.meta.dir, "pstack-runner"), + join(import.meta.dir, "entry.ts"), "--parent", input.parent, "--provider", input.provider, "--model", input.model, @@ -668,6 +668,70 @@ describe("runLane", () => { expect(receipt(input.receiptPath).status).toBe("complete"); }); + it("isolates Bun startup from project configuration at the shipped executable boundary", async () => { + writeFileSync( + join(scratch, ".env"), + [ + "PSTACK_PROJECT_ENV_SENTINEL=loaded-from-project-dotenv", + "PSTACK_INHERITED_ENV_SENTINEL=dotenv-must-not-override-parent", + "", + ].join("\n") + ); + const preloadCapture = join(scratch, "preload-ran.txt"); + writeFileSync( + join(scratch, "bunfig.toml"), + 'preload = ["./preload.ts"]\n' + ); + writeFileSync( + join(scratch, "preload.ts"), + 'await Bun.write(process.env.PSTACK_PRELOAD_CAPTURE!, "project preload ran");\n' + ); + const capturedEnv = join(scratch, "captured-env.txt"); + const codex = join(bin, "codex"); + writeFileSync( + codex, + `#!/bin/sh +if [ "$1" = "login" ]; then + echo "Logged in using ChatGPT" + exit 0 +fi +printf '%s\n%s' "\${PSTACK_PROJECT_ENV_SENTINEL-unset}" "\${PSTACK_INHERITED_ENV_SENTINEL-unset}" > "$PSTACK_ENV_CAPTURE" +echo '{"type":"thread.started","thread_id":"o1"}' +echo '{"type":"item.completed","item":{"type":"agent_message","text":"CODEX_OK"}}' +echo '{"type":"turn.completed","usage":{"input_tokens":20,"cached_input_tokens":5,"output_tokens":3,"reasoning_output_tokens":1}}' +` + ); + chmodSync(codex, 0o755); + + const input = options("codex", "project-env-boundary"); + const runner = Bun.spawn([ + join(import.meta.dir, "pstack-runner"), + ...runnerArgs(input).slice(1), + ], { + cwd: scratch, + env: { + ...process.env, + BUN_OPTIONS: "--env-file=.env", + NODE_OPTIONS: "--require=./missing-project-preload.cjs", + PSTACK_ENV_CAPTURE: capturedEnv, + PSTACK_INHERITED_ENV_SENTINEL: "inherited-from-parent", + PSTACK_PRELOAD_CAPTURE: preloadCapture, + }, + stdout: "pipe", + stderr: "pipe", + }); + const stdout = new Response(runner.stdout).text(); + const stderr = new Response(runner.stderr).text(); + + expect(await runner.exited).toBe(0); + await Promise.all([stdout, stderr]); + expect(readFileSync(capturedEnv, "utf8")).toBe( + "unset\ninherited-from-parent" + ); + expect(existsSync(preloadCapture)).toBe(false); + expect(receipt(input.receiptPath).status).toBe("complete"); + }); + it("cancels a preflight with SIGINT and writes a terminal receipt", async () => { const input = options("claude", "preflight-cancelled"); const started = join(scratch, "preflight-child.started"); @@ -675,7 +739,6 @@ describe("runLane", () => { const isolatedRunner = join(scratch, "isolated-runner"); cpSync(import.meta.dir, isolatedRunner, { recursive: true }); const runner = Bun.spawn([ - process.execPath, join(isolatedRunner, "pstack-runner"), ...runnerArgs(input).slice(1), ], {