mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 21:49:18 +01:00
fix: allow managed Git helpers to select a private interpreter (#18337)
## Thinking Path > - Paperclip manages AI agents and their execution environments. > - Managed Git helpers run next to those agents. > - Some environments keep their Node interpreter outside the agent PATH. > - The current generator always emits an env-based Node shebang. > - This pull request lets the environment select an absolute interpreter path. > - Existing callers keep the same default behavior. ## Linked Issues or Issue Description Refs #18212. **What happened?** A managed helper can fail when Node exists only as a private runtime binary. The caller must currently alter generated source or expose that runtime through PATH. **Expected behavior** The source generator accepts an optional interpreter path. The environment can keep its own runtime private. **Steps to reproduce** Generate a helper in an environment whose PATH has no usable Node. Run a local Git command through that helper. The env-based shebang cannot select the private interpreter. ## What Changed - Add optional `interpreterPath` to `githubLauncherSource`. - Preserve `/usr/bin/env node` when the option is absent. - Reject relative paths, whitespace and NUL bytes. - Document the option and test actual execution with an unusable Node on PATH. ## Verification - The affected adapter-utils package typecheck passed. - All 26 tests in `packages/adapter-utils/src/github-launcher.test.ts` passed. - The new test runs a generated helper as a child process. It proves that the selected interpreter bypasses a failing PATH-selected Node and that the helper body stays unchanged. - Invalid-path tests passed. Existing broker and local-Git tests passed. - Full checks run in GitHub CI per the user's request. No full local suite was run. ## Risks The option changes only the shebang. Callers own interpreter installation and immutability. Default behavior, PATH, credentials and permission policy stay unchanged. No schema or workflow changes. ## Model Used OpenAI GPT-6.1 Sol (`gpt-6.1-sol`), high reasoning, with source inspection and targeted execution. The host does not expose an exact context-window size. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
abc270ee4c
commit
6d61990692
3 files changed
+42
-3
No files matched your search
@@ -79,3 +79,11 @@ The [environment task API](plugins/PLUGIN_ENVIRONMENT_TASKS.md#launch-preparatio
|
||||
defines the plugin preparation exchange. The [native plugin integration](plugins/PLUGIN_RUNNER_EXECUTION.md)
|
||||
connects the callback to task execution. A Launcher implementation remains the
|
||||
execution owner's responsibility.
|
||||
|
||||
## Managed Git helper interpreter
|
||||
|
||||
`githubLauncherSource({ interpreterPath })` can select an absolute Node interpreter
|
||||
owned by the execution environment. Without this option, generated helpers retain
|
||||
`#!/usr/bin/env node`. The option changes only the shebang; it does not change PATH,
|
||||
command discovery, credential handling, or helper behavior. Interpreter paths must
|
||||
be absolute and contain no whitespace or NUL bytes.
|
||||
@@ -11,6 +11,31 @@ const cleanups: Array<() => Promise<unknown>> = [];
|
||||
afterEach(async () => { for (const cleanup of cleanups.splice(0).reverse()) await cleanup(); });
|
||||
|
||||
describe("managed GitHub launchers", () => {
|
||||
|
||||
it("uses a private interpreter without selecting Node from PATH", async () => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "paperclip-github-interpreter-"));
|
||||
cleanups.push(() => rm(root, { recursive: true, force: true }));
|
||||
const managed = path.join(root, "managed"), real = path.join(root, "real");
|
||||
await mkdir(managed); await mkdir(real);
|
||||
const source = githubLauncherSource({ interpreterPath: process.execPath });
|
||||
const defaultSource = githubLauncherSource();
|
||||
expect(defaultSource.split("\n")[0]).toBe("#!/usr/bin/env node");
|
||||
expect(source.split("\n")[0]).toBe(`#!${process.execPath}`);
|
||||
expect(source.slice(source.indexOf("\n"))).toBe(defaultSource.slice(defaultSource.indexOf("\n")));
|
||||
const launcher = path.join(managed, "git");
|
||||
await writeFile(launcher, source, { mode: 0o700 });
|
||||
await writeFile(path.join(real, "node"), "#!/bin/sh\nexit 126\n", { mode: 0o700 });
|
||||
await writeFile(path.join(real, "git"), "#!/bin/sh\nprintf 'private interpreter works'\n", { mode: 0o700 });
|
||||
const env = { ...githubBrokerEnvironment({}, { url: "", token: "" }), HOME: root,
|
||||
GH_CONFIG_DIR: path.join(root, "config"), PATH: `${managed}${path.delimiter}${real}` };
|
||||
expect((await exec(launcher, ["--version"], { env })).stdout).toBe("private interpreter works");
|
||||
});
|
||||
|
||||
it.each(["", "relative/node", "/runtime/node --flag", "/runtime/node\nexit 1", "/runtime/node\0"])(
|
||||
"rejects an invalid interpreter path %j", interpreterPath => {
|
||||
expect(() => githubLauncherSource({ interpreterPath })).toThrow("absolute path without whitespace");
|
||||
},
|
||||
);
|
||||
it.each([false, true])("rejects a PATH shim that re-enters the same helper before it can recurse (broker file: %s)", async brokerFile => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "paperclip-github-recursion-"));
|
||||
cleanups.push(() => rm(root, { recursive: true, force: true }));
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
/** Standalone source is staged unchanged on local, SSH, and sandbox runtimes. No secrets in files. */
|
||||
export function githubLauncherSource(options: { brokerFile?: boolean } = {}): string {
|
||||
return String.raw`#!/usr/bin/env node
|
||||
import { isAbsolute } from "node:path";
|
||||
|
||||
/** Standalone source for local, SSH, and sandbox runtimes. No secrets in files. */
|
||||
export function githubLauncherSource(options: { brokerFile?: boolean; interpreterPath?: string } = {}): string {
|
||||
if (options.interpreterPath !== undefined && (typeof options.interpreterPath !== "string"
|
||||
|| !isAbsolute(options.interpreterPath) || /[\s\0]/u.test(options.interpreterPath))) {
|
||||
throw new Error("GitHub launcher interpreter must be an absolute path without whitespace");
|
||||
}
|
||||
return String.raw`#!${options.interpreterPath ?? "/usr/bin/env node"}
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
|
||||
Reference in new issue
Block a user