From 1998bd4d328cd4c6563d51d718097bd04506c24a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 9 Nov 2026 23:58:25 +0000 Subject: [PATCH] fix: put bun and bunx on the review harness PATH Non-interactive shells skip Bun's ~/.bashrc edit, and the isolated check resolves programs from /usr/bin. Copy bun there so setup is visible without an interactive shell, and name the program the sandbox cannot exec. Co-authored-by: Oleksiy Akimov --- .cursor/environment.json | 2 +- CHANGELOG.md | 2 ++ scripts/cloud-agent-install.sh | 13 +++++++--- scripts/isolate-run.sh | 9 ++++++- test/review-scripts.test.ts | 46 +++++++++++++++++++++++++++++----- 5 files changed, 60 insertions(+), 12 deletions(-) diff --git a/.cursor/environment.json b/.cursor/environment.json index 331597b..76fd86f 100644 --- a/.cursor/environment.json +++ b/.cursor/environment.json @@ -1,4 +1,4 @@ { "name": "opencode-plugin-compat", - "install": "#!/usr/bin/env bash\n# Idempotent Cloud Agent bootstrap. .cursor/environment.json install is this\n# file, so a build of main does not need this path to exist.\n# Installs Bun and the workspace tools needed for isolated install/build/test.\n# There is no OpenCode dual install and no live plugin path in this repository.\n\ninstall_main() {\n set -euo pipefail\n export PYTHONDONTWRITEBYTECODE=1\n export DEBIAN_FRONTEND=noninteractive\n\n if [[ -n \"${BASH_SOURCE[0]:-}\" && -f \"${BASH_SOURCE[0]}\" && \"$(basename \"$(dirname \"${BASH_SOURCE[0]}\")\")\" == \"scripts\" ]]; then\n ROOT=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/..\" && pwd)\"\n else\n ROOT=\"$(pwd)\"\n fi\n cd \"$ROOT\"\n\n if [[ ! -x \"$HOME/.bun/bin/bun\" ]]; then\n curl -fsSL https://bun.sh/install | bash\n fi\n sudo ln -sf \"$HOME/.bun/bin/bun\" /usr/local/bin/bun\n # Workspace build/typecheck scripts call bunx. Bun selects that entry point\n # from argv[0], and scripts/isolated-check.sh mounts it by this name.\n sudo ln -sf \"$HOME/.bun/bin/bun\" /usr/local/bin/bunx\n\n if ! command -v zsh >/dev/null 2>&1; then\n sudo apt-get update\n sudo apt-get install -y zsh\n fi\n if ! command -v python3 >/dev/null 2>&1; then\n sudo apt-get update\n sudo apt-get install -y python3\n fi\n if ! command -v openssl >/dev/null 2>&1; then\n sudo apt-get update\n sudo apt-get install -y openssl\n fi\n\n /usr/local/bin/bun install --frozen-lockfile\n /usr/local/bin/bun run build\n # Workspace packages emit under packages/*/dist.\n if ! ls \"$ROOT\"/packages/*/dist/*.js >/dev/null 2>&1; then\n echo \"Missing packages/*/dist after build.\" >&2\n exit 1\n fi\n\n echo \"Bun $(/usr/local/bin/bun --version)\"\n}\n\nif [[ -z \"${BASH_SOURCE[0]:-}\" || \"${BASH_SOURCE[0]}\" == \"$0\" ]]; then\n install_main \"$@\"\nfi\n" + "install": "#!/usr/bin/env bash\n# Idempotent Cloud Agent bootstrap. .cursor/environment.json install is this\n# file, so a build of main does not need this path to exist.\n# Installs Bun and the workspace tools needed for isolated install/build/test.\n# There is no OpenCode dual install and no live plugin path in this repository.\n\ninstall_main() {\n set -euo pipefail\n export PYTHONDONTWRITEBYTECODE=1\n export DEBIAN_FRONTEND=noninteractive\n\n if [[ -n \"${BASH_SOURCE[0]:-}\" && -f \"${BASH_SOURCE[0]}\" && \"$(basename \"$(dirname \"${BASH_SOURCE[0]}\")\")\" == \"scripts\" ]]; then\n ROOT=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/..\" && pwd)\"\n else\n ROOT=\"$(pwd)\"\n fi\n cd \"$ROOT\"\n\n if [[ ! -x \"$HOME/.bun/bin/bun\" ]]; then\n curl -fsSL https://bun.sh/install | bash\n fi\n # Non-interactive bash skips the installer's ~/.bashrc PATH edit, and the\n # isolate-run PATH is /opt/tools:/usr/bin:/bin:/usr/sbin. /usr/local/bin is\n # not on that PATH, and a symlink from there into $HOME is not mounted.\n # Copy the binary into /usr/bin, which both search, and point bunx at it.\n # Workspace build/typecheck scripts call bunx; Bun selects that entry from argv[0].\n sudo install -m 0755 \"$HOME/.bun/bin/bun\" /usr/bin/bun\n sudo ln -sfn bun /usr/bin/bunx\n sudo ln -sfn /usr/bin/bun /usr/local/bin/bun\n sudo ln -sfn /usr/bin/bun /usr/local/bin/bunx\n\n if ! command -v zsh >/dev/null 2>&1; then\n sudo apt-get update\n sudo apt-get install -y zsh\n fi\n if ! command -v python3 >/dev/null 2>&1; then\n sudo apt-get update\n sudo apt-get install -y python3\n fi\n if ! command -v openssl >/dev/null 2>&1; then\n sudo apt-get update\n sudo apt-get install -y openssl\n fi\n\n /usr/local/bin/bun install --frozen-lockfile\n /usr/local/bin/bun run build\n # Workspace packages emit under packages/*/dist.\n if ! ls \"$ROOT\"/packages/*/dist/*.js >/dev/null 2>&1; then\n echo \"Missing packages/*/dist after build.\" >&2\n exit 1\n fi\n\n echo \"Bun $(/usr/local/bin/bun --version)\"\n}\n\nif [[ -z \"${BASH_SOURCE[0]:-}\" || \"${BASH_SOURCE[0]}\" == \"$0\" ]]; then\n install_main \"$@\"\nfi\n" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e53fb6..8672bd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ The monorepo ships **one shared train version** across all eleven packages, so u ### Fixed +- Cloud Agent setup copies `bun` and `bunx` onto `/usr/bin`, so non-interactive shells and the isolated-check sandbox can run them without `~/.bashrc`. +- The isolated-check sandbox names the program it could not start. - `ocp-dev` DSH local wiring generates Host Typert declarations before host `tsc`, so package `./remote` exports resolve and leftover empty package dirs no longer block a full reshim. - `ocp-dev` DSH file-stage and profile sync include root `cordis-entry.js`, so Cordis can import `@opencode-compat/dsh-bridge` after a local reshim. diff --git a/scripts/cloud-agent-install.sh b/scripts/cloud-agent-install.sh index 583d672..5c2995a 100755 --- a/scripts/cloud-agent-install.sh +++ b/scripts/cloud-agent-install.sh @@ -19,10 +19,15 @@ install_main() { if [[ ! -x "$HOME/.bun/bin/bun" ]]; then curl -fsSL https://bun.sh/install | bash fi - sudo ln -sf "$HOME/.bun/bin/bun" /usr/local/bin/bun - # Workspace build/typecheck scripts call bunx. Bun selects that entry point - # from argv[0], and scripts/isolated-check.sh mounts it by this name. - sudo ln -sf "$HOME/.bun/bin/bun" /usr/local/bin/bunx + # Non-interactive bash skips the installer's ~/.bashrc PATH edit, and the + # isolate-run PATH is /opt/tools:/usr/bin:/bin:/usr/sbin. /usr/local/bin is + # not on that PATH, and a symlink from there into $HOME is not mounted. + # Copy the binary into /usr/bin, which both search, and point bunx at it. + # Workspace build/typecheck scripts call bunx; Bun selects that entry from argv[0]. + sudo install -m 0755 "$HOME/.bun/bin/bun" /usr/bin/bun + sudo ln -sfn bun /usr/bin/bunx + sudo ln -sfn /usr/bin/bun /usr/local/bin/bun + sudo ln -sfn /usr/bin/bun /usr/local/bin/bunx if ! command -v zsh >/dev/null 2>&1; then sudo apt-get update diff --git a/scripts/isolate-run.sh b/scripts/isolate-run.sh index f1e087c..fe7acda 100755 --- a/scripts/isolate-run.sh +++ b/scripts/isolate-run.sh @@ -418,7 +418,14 @@ if [[ -f /home/reviewer/env ]]; then fi set +a cd /src -exec python3 -c 'import json, os; args = json.load(open("/argv.json")); os.execvp(args[0], args)' +exec python3 -c 'import json, os, sys +args = json.load(open("/argv.json")) +try: + os.execvp(args[0], args) +except OSError as error: + sys.stderr.write("cannot run %s: %s\n" % (args[0], error.strerror)) + raise SystemExit(127) +' EOF chmod 0755 "$root/init" "$root/enter-command" cat >"$root/drop-caps.py" <<'EOF' diff --git a/test/review-scripts.test.ts b/test/review-scripts.test.ts index 88d9e75..c7a1dbb 100644 --- a/test/review-scripts.test.ts +++ b/test/review-scripts.test.ts @@ -141,6 +141,7 @@ describe("cloud agent install", () => { } expect(environment.install).toBe(script) expect(environment.install).toContain("https://bun.sh/install") + expect(environment.install).toContain("/usr/bin/bunx") expect(environment.install).toContain("/usr/local/bin/bunx") expect(environment.install).toContain("bun install --frozen-lockfile") expect(environment.install).toContain("bun run build") @@ -205,16 +206,19 @@ describe("cloud agent install", () => { const bin = path.join(directory, "bin") const bunBin = path.join(directory, ".bun", "bin") const localBin = path.join(directory, "local", "bin") + const usrBin = path.join(directory, "usr", "bin") const workspace = path.join(directory, "workspace") fs.mkdirSync(bin) fs.mkdirSync(bunBin, { recursive: true }) fs.mkdirSync(localBin, { recursive: true }) + fs.mkdirSync(usrBin, { recursive: true }) fs.mkdirSync(path.join(workspace, "scripts"), { recursive: true }) - // Redirect the absolute Cloud Agent bun path into the private home so the - // test never writes under the real /usr/local/bin. + // Redirect the absolute Cloud Agent bun paths into the private home so the + // test never writes under the real /usr/bin or /usr/local/bin. const script = fs .readFileSync(path.join(root, "scripts/cloud-agent-install.sh"), "utf8") .replaceAll("/usr/local/bin/bun", `${localBin}/bun`) + .replaceAll("/usr/bin/bun", `${usrBin}/bun`) fs.writeFileSync(path.join(workspace, "scripts/cloud-agent-install.sh"), script) fs.writeFileSync( path.join(bunBin, "bun"), @@ -245,6 +249,10 @@ if [ "$1" = "ln" ]; then shift exec ln "$@" fi +if [ "$1" = "install" ]; then + shift + exec install "$@" +fi exit 0 `, ) @@ -265,10 +273,19 @@ exit 0 expect(fs.readFileSync(path.join(directory, "bun.log"), "utf8")).toContain("install --frozen-lockfile") expect(fs.readFileSync(path.join(directory, "bun.log"), "utf8")).toContain("run build") expect(fs.existsSync(path.join(workspace, "packages/profile/dist/index.js"))).toBe(true) - expect(fs.readFileSync(path.join(directory, "sudo.log"), "utf8")).toContain("ln -sf") - expect(fs.existsSync(path.join(localBin, "bun"))).toBe(true) - // Workspace build/typecheck scripts call bunx; the isolated check mounts it by that name. - expect(fs.realpathSync(path.join(localBin, "bunx"))).toBe(fs.realpathSync(path.join(bunBin, "bun"))) + expect(fs.readFileSync(path.join(directory, "sudo.log"), "utf8")).toContain("install -m 0755") + expect(fs.readFileSync(path.join(directory, "sudo.log"), "utf8")).toContain("ln -sfn") + expect(fs.lstatSync(path.join(usrBin, "bun")).isSymbolicLink()).toBe(false) + expect(fs.readlinkSync(path.join(usrBin, "bunx"))).toBe("bun") + expect(fs.realpathSync(path.join(usrBin, "bunx"))).toBe(fs.realpathSync(path.join(usrBin, "bun"))) + expect(fs.realpathSync(path.join(localBin, "bun"))).toBe(fs.realpathSync(path.join(usrBin, "bun"))) + expect(fs.realpathSync(path.join(localBin, "bunx"))).toBe(fs.realpathSync(path.join(usrBin, "bun"))) + const bash = fs.existsSync("/bin/bash") ? "/bin/bash" : "/usr/bin/bash" + const resolved = run(bash, ["--noprofile", "--norc", "-c", "command -v bun; command -v bunx"], { + env: { PATH: usrBin }, + }) + expect(resolved.status, resolved.stderr).toBe(0) + expect(resolved.stdout).toBe(`${usrBin}/bun\n${usrBin}/bunx\n`) } finally { fs.rmSync(directory, { recursive: true, force: true }) } @@ -279,14 +296,17 @@ exit 0 const bin = path.join(directory, "bin") const bunBin = path.join(directory, ".bun", "bin") const localBin = path.join(directory, "local", "bin") + const usrBin = path.join(directory, "usr", "bin") const workspace = path.join(directory, "workspace") fs.mkdirSync(bin) fs.mkdirSync(bunBin, { recursive: true }) fs.mkdirSync(localBin, { recursive: true }) + fs.mkdirSync(usrBin, { recursive: true }) fs.mkdirSync(path.join(workspace, "scripts"), { recursive: true }) const script = fs .readFileSync(path.join(root, "scripts/cloud-agent-install.sh"), "utf8") .replaceAll("/usr/local/bin/bun", `${localBin}/bun`) + .replaceAll("/usr/bin/bun", `${usrBin}/bun`) fs.writeFileSync(path.join(workspace, "scripts/cloud-agent-install.sh"), script) fs.writeFileSync( path.join(bunBin, "bun"), @@ -300,6 +320,7 @@ exit 0 path.join(bin, "sudo"), `#!/bin/sh if [ "$1" = "ln" ]; then shift; exec ln "$@"; fi +if [ "$1" = "install" ]; then shift; exec install "$@"; fi exit 0 `, ) @@ -1097,6 +1118,19 @@ print("SAW=" + str(found)) fs.rmSync(base, { recursive: true, force: true }) }) + it("names the program when the sandbox cannot exec it", () => { + const src = fs.mkdtempSync(path.join(os.tmpdir(), "review-missing-program-")) + try { + const result = run("bash", ["scripts/isolate-run.sh", "--src", src, "--", "not-a-real-program-a977"]) + if (result.status === 2 && /ISOLATION=unavailable/.test(result.stderr)) return + expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(127) + expect(result.stderr).toContain("cannot run not-a-real-program-a977:") + expect(result.stderr).not.toContain("Traceback") + } finally { + fs.rmSync(src, { recursive: true, force: true }) + } + }) + it("runs a command when no tools or environment names are passed", () => { const src = fs.mkdtempSync(path.join(os.tmpdir(), "review-empty-arrays-")) try {