fix(codex): harden gh wrapper resolution with explicit GH_PATH

This commit is contained in:
Prateek 2026-03-08 02:55:31 +05:30
parent ca46607b69
commit b865549ea9
3 changed files with 38 additions and 1 deletions

View File

@ -42,4 +42,9 @@ describe("agent-codex launch/env wiring (integration)", () => {
const env = agent.getEnvironment(makeLaunchConfig());
expect(env["CODEX_DISABLE_UPDATE_CHECK"]).toBe("1");
});
it("sets GH_PATH to preferred system gh wrapper location", () => {
const env = agent.getEnvironment(makeLaunchConfig());
expect(env["GH_PATH"]).toBe("/usr/local/bin/gh");
});
});

View File

@ -400,6 +400,11 @@ describe("getEnvironment", () => {
expect(env["CODEX_DISABLE_UPDATE_CHECK"]).toBe("1");
});
it("sets GH_PATH to preferred wrapper target", () => {
const env = agent.getEnvironment(makeLaunchConfig());
expect(env["GH_PATH"]).toBe("/usr/local/bin/gh");
});
it("deduplicates ao and /usr/local/bin entries", () => {
const originalPath = process.env["PATH"];
process.env["PATH"] = "/mock/home/.ao/bin:/usr/local/bin:/usr/bin:/usr/local/bin";
@ -1624,6 +1629,18 @@ describe("shell wrapper content", () => {
expect(content).toContain('exec "$real_gh"');
});
it("prefers GH_PATH when provided and executable", async () => {
const content = await getWrapperContent("gh");
expect(content).toContain("GH_PATH");
expect(content).toContain('-x "$GH_PATH"');
expect(content).toContain('real_gh="$GH_PATH"');
});
it("guards against recursive GH_PATH pointing to ao wrapper dir", async () => {
const content = await getWrapperContent("gh");
expect(content).toContain('if [[ "$gh_dir" != "$ao_bin_dir" ]]');
});
it("extracts PR URL from gh pr create output", async () => {
const content = await getWrapperContent("gh");
expect(content).toContain("https://github");

View File

@ -37,6 +37,7 @@ function normalizePermissionMode(mode: string | undefined): "permissionless" | "
const AO_BIN_DIR = join(homedir(), ".ao", "bin");
const DEFAULT_PATH = "/usr/bin:/bin";
const PREFERRED_GH_BIN_DIR = "/usr/local/bin";
const PREFERRED_GH_PATH = `${PREFERRED_GH_BIN_DIR}/gh`;
function buildAgentPath(basePath: string | undefined): string {
const inherited = (basePath ?? DEFAULT_PATH).split(":").filter(Boolean);
@ -141,7 +142,20 @@ const GH_WRAPPER = `#!/usr/bin/env bash
ao_bin_dir="\$(cd "\$(dirname "\$0")" && pwd)"
clean_path="\$(echo "\$PATH" | tr ':' '\\n' | grep -Fxv "\$ao_bin_dir" | grep . | tr '\\n' ':')"
clean_path="\${clean_path%:}"
real_gh="\$(PATH="\$clean_path" command -v gh 2>/dev/null)"
real_gh=""
# Prefer explicit gh path when provided by AO environment.
# Guard against recursive self-reference to the wrapper in ~/.ao/bin.
if [[ -n "\${GH_PATH:-}" && -x "\$GH_PATH" ]]; then
gh_dir="\$(cd "\$(dirname "\$GH_PATH")" 2>/dev/null && pwd)"
if [[ "\$gh_dir" != "\$ao_bin_dir" ]]; then
real_gh="\$GH_PATH"
fi
fi
if [[ -z "\$real_gh" ]]; then
real_gh="\$(PATH="\$clean_path" command -v gh 2>/dev/null)"
fi
if [[ -z "\$real_gh" ]]; then
echo "ao-wrapper: gh not found in PATH" >&2
@ -642,6 +656,7 @@ function createCodexAgent(): Agent {
// The wrappers strip this directory from PATH before calling the real
// binary, so there's no infinite recursion.
env["PATH"] = buildAgentPath(process.env["PATH"]);
env["GH_PATH"] = PREFERRED_GH_PATH;
// Disable Codex's version check/update prompt for non-interactive AO sessions.
env["CODEX_DISABLE_UPDATE_CHECK"] = "1";