fix(acpx-engine): check the final env for Codex host-key auth selection

The Codex api-key auth-request check ran on the run's explicit env,
before the launch merged the projected host environment in. A local
Codex launch could inherit OPENAI_API_KEY or CODEX_API_KEY from the
host after that check, so the launched process had the credential but
never got the required api-key authentication request.

Move the check into resolveRuntimeEnv, after the host projection and
explicit env merge produce the final environment. This covers every
launch path (local, runner-less fallback, remote sandbox), because
they all resolve their launch env through this one function.

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
nickyleach 2026-09-11 21:34:52 +00:00 committed by Nicky Leach
parent 73d49c3741
commit c2e517d948
No known key found for this signature in database
GPG Key ID: 4861541D36B2037E
2 changed files with 44 additions and 12 deletions

View File

@ -1572,6 +1572,35 @@ describe("shared ACPX engine runtime behavior", () => {
},
);
it.each(["OPENAI_API_KEY", "CODEX_API_KEY"] as const)(
"selects Codex ACP API-key authentication when only the host process provides %s",
async (apiKeyName) => {
const root = await makeTempRoot();
const codexHome = path.join(root, "codex-home");
await fs.mkdir(codexHome, { recursive: true });
// Simulate a local launch that inherits a provider key from the host
// process environment. No adapter config sets the key directly, so the
// launched env only receives it through host projection.
vi.stubEnv(apiKeyName, "sk-host-inherited-key");
try {
const { sessionInputs } = await runExecutor({
agent: "codex",
stateDir: path.join(root, "state"),
env: { CODEX_HOME: codexHome },
paperclipRuntimeSkills: [],
paperclipSkillSync: { desiredSkills: [] },
});
const env = (sessionInputs[0]!.sessionOptions as { env: Record<string, string> }).env;
expect(env[apiKeyName]).toBe("sk-host-inherited-key");
expect(env.DEFAULT_AUTH_REQUEST).toBe(JSON.stringify({ methodId: "api-key" }));
} finally {
vi.unstubAllEnvs();
}
},
);
it("busts the session fingerprint when resolved adapter env changes but not across wakes", async () => {
const root = await makeTempRoot();
const stateDir = path.join(root, "state");

View File

@ -1967,17 +1967,6 @@ async function buildRuntime(input: {
// are absent from tempKeysApplied and keep their compatibility protection.
if (!scratchKeys.has(key) || value !== scratch.dir) resolvedAdapterEnv[key] = value;
}
// codex-acp supports both key names, but ACP clients must select its
// api-key authentication method during session creation. Without this
// request, the server advertises authentication and rejects session/new even
// though the credential is present in the launched process environment.
if (
acpxAgent === "codex" &&
(env.OPENAI_API_KEY || env.CODEX_API_KEY) &&
!env.DEFAULT_AUTH_REQUEST
) {
env.DEFAULT_AUTH_REQUEST = JSON.stringify({ methodId: "api-key" });
}
if (authToken) env.PAPERCLIP_API_KEY = authToken;
// For the claude agent, set model via ANTHROPIC_MODEL at startup rather than
// via session/set_config_option — the ACP server's set_config_option handler
@ -2629,11 +2618,25 @@ function resolveRuntimeEnv(
env,
(options.platform ?? process.platform) === "win32",
);
return Object.fromEntries(
const finalEnv = Object.fromEntries(
Object.entries(mergedEnv).filter(
(entry): entry is [string, string] => typeof entry[1] === "string",
),
);
// codex-acp supports both key names, but ACP clients must select its
// api-key authentication method during session creation. Without this
// request, the server advertises authentication and rejects session/new even
// though the credential is present in the launched process environment. Check
// the final merged environment, not just the explicit run config, so a host
// key the local launch inherits still selects this default.
if (
acpxAgent === "codex" &&
(finalEnv.OPENAI_API_KEY || finalEnv.CODEX_API_KEY) &&
!finalEnv.DEFAULT_AUTH_REQUEST
) {
finalEnv.DEFAULT_AUTH_REQUEST = JSON.stringify({ methodId: "api-key" });
}
return finalEnv;
}
function mergeRuntimeEnvironment(