diff --git a/packages/integration-tests/src/agent-codex-launch-env.integration.test.ts b/packages/integration-tests/src/agent-codex-launch-env.integration.test.ts index 2d76089b0..66fd74fd1 100644 --- a/packages/integration-tests/src/agent-codex-launch-env.integration.test.ts +++ b/packages/integration-tests/src/agent-codex-launch-env.integration.test.ts @@ -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"); + }); }); diff --git a/packages/plugins/agent-codex/src/index.test.ts b/packages/plugins/agent-codex/src/index.test.ts index f365e717a..a611b5e9e 100644 --- a/packages/plugins/agent-codex/src/index.test.ts +++ b/packages/plugins/agent-codex/src/index.test.ts @@ -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"); diff --git a/packages/plugins/agent-codex/src/index.ts b/packages/plugins/agent-codex/src/index.ts index 06893806b..f9a31c72e 100644 --- a/packages/plugins/agent-codex/src/index.ts +++ b/packages/plugins/agent-codex/src/index.ts @@ -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";