fix: isolate GitHub launcher module format

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-09-08 13:40:19 -05:00
parent 1c48625115
commit 30b9cd8c00
3 changed files with 33 additions and 1 deletions

View File

@ -43,7 +43,9 @@ adapters and the native runner enter the same host-owned lifecycle before
dispatch. Local execution keeps its existing workspace and home behavior.
Legacy ACP proxies use a private host staging directory while agent sessions start
in the sandbox home. For API-key Codex ACP runs, the adapter writes the explicit
key to an owner-only login file in the staging copy; host credentials stay unchanged. Native runner launches carry the validated scoped paths
key to an owner-only login file in the staging copy; host credentials stay unchanged.
Per-run GitHub launchers declare their own CommonJS package scope so warm runs
inside ES-module repositories can still execute Git and GitHub CLI commands. Native runner launches carry the validated scoped paths
through runnerd to ACPX; CLI configuration remains in its private runtime directories.
Warm sandbox task bindings persist independently of the experimental isolated
workspace setting. Only the active host run can establish that binding; the

View File

@ -1539,6 +1539,9 @@ export async function prepareGitHubOperationLaunchers(input: {
// the managed launchers after startup without loading a host user's profile.
const profile = `export PATH=${shellQuote(managedPath)}\n`;
const files: Record<string, string> = Object.fromEntries([
// Warm task paths can live inside an ESM repository. These extensionless
// launchers use CommonJS regardless of the surrounding project's type.
["package.json", JSON.stringify({ type: "commonjs" })],
...["git", "gh"].map((name) => [name, githubLauncherSource()] as const),
...[".zshenv", ".zprofile", ".zshrc", ".bash_profile", ".bashrc", ".profile"].map((name) => [name, profile] as const),
]);

View File

@ -6,11 +6,38 @@ import path from "node:path";
import { promisify } from "node:util";
import { afterEach, describe, expect, it } from "vitest";
import { githubBrokerEnvironment, githubLauncherSource } from "./github-launcher.js";
import { prepareGitHubOperationLaunchers } from "./execution-target.js";
import { runChildProcess } from "./server-utils.js";
import type { CommandManagedRuntimeRunner } from "./command-managed-runtime.js";
const exec = promisify(execFile);
const cleanups: Array<() => Promise<unknown>> = [];
afterEach(async () => { for (const cleanup of cleanups.splice(0).reverse()) await cleanup(); });
describe("managed GitHub launchers", () => {
it("runs staged git and gh launchers inside an ES-module repository", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "paperclip-github-esm-"));
cleanups.push(() => rm(root, { recursive: true, force: true }));
const realBin = path.join(root, "real-bin");
await mkdir(realBin);
await writeFile(path.join(root, "package.json"), JSON.stringify({ type: "module" }));
await writeFile(path.join(realBin, "gh"), "#!/bin/sh\nprintf gh-fixture", { mode: 0o700 });
const target = { kind: "remote" as const, transport: "sandbox" as const,
providerKey: "fixture", remoteCwd: root,
runner: { execute: async (input: Parameters<CommandManagedRuntimeRunner["execute"]>[0]) => runChildProcess("github-esm-fixture", input.command, input.args ?? [], {
cwd: input.cwd ?? root, env: input.env ?? {}, stdin: input.stdin,
timeoutSec: 15, graceSec: 1, onLog: async () => {},
}) },
};
// The second run models the fresh per-run wrapper staged on warm startup.
for (const runId of ["cold", "warm"]) {
const env = await prepareGitHubOperationLaunchers({ runId, target, cwd: root,
env: { PATH: `${realBin}:${process.env.PATH}`, PAPERCLIP_GITHUB_BROKER_TOKEN: "" } });
const launcher = env.PAPERCLIP_GITHUB_LAUNCHER_DIR;
expect((await exec(path.join(launcher, "git"), ["--version"], { cwd: root, env: { ...process.env, ...env } })).stdout).toMatch(/^git version /);
expect((await exec(path.join(launcher, "gh"), [], { cwd: root, env: { ...process.env, ...env } })).stdout).toBe("gh-fixture");
}
});
it("captures each command's identity and clears host credentials when the next person has none", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "paperclip-github-launcher-test-"));
cleanups.push(() => rm(root, { recursive: true, force: true }));