From b013e0d42886612a8fc879fe70030547c3a026fc Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:49:02 +0100 Subject: [PATCH] fix(codex): read custom prompts from the provider's own CODEX_HOME (#5450) A provider that extends codex and sets CODEX_HOME in its env runs Codex against that home, but the daemon listed, expanded, and fell back to skills from its own CODEX_HOME or ~/.codex. The / menu offered another home's prompts and never the provider's own, and invoking one of the provider's prompts sent the raw slash text to Codex. Resolve the Codex home from the same environment the app-server is launched with, and give it to the session. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> (cherry picked from commit dffde6ae8b8d05afaa2751dc1d80bc253f73bc19) --- .../providers/codex-app-server-agent.test.ts | 125 +++++++++++++++++- .../agent/providers/codex-app-server-agent.ts | 28 ++-- 2 files changed, 140 insertions(+), 13 deletions(-) diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index a9e0b77db1..1fac9e06ad 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -617,6 +617,111 @@ process.stdin.on("data", (chunk) => { } } +async function withCustomCodexProviderHome( + run: (input: { + session: AgentSession; + readCaptured: () => CapturedFakeCodexRecord[]; + }) => Promise, +): Promise { + const tempDir = await mkdtemp(path.join(tmpdir(), "codex-provider-home-")); + const daemonCodexHome = path.join(tempDir, "daemon-codex-home"); + const providerCodexHome = path.join(tempDir, "provider-codex-home"); + const fakeAppServerPath = path.join(tempDir, "fake-codex-app-server.cjs"); + const capturedRequestsPath = path.join(tempDir, "requests.jsonl"); + mkdirSync(path.join(daemonCodexHome, "prompts"), { recursive: true }); + mkdirSync(path.join(providerCodexHome, "prompts"), { recursive: true }); + writeFileSync( + path.join(daemonCodexHome, "prompts", "probe-default.md"), + "---\ndescription: Daemon home prompt\n---\nfrom the daemon home\n", + ); + writeFileSync( + path.join(providerCodexHome, "prompts", "probe-profile.md"), + "---\ndescription: Provider home prompt\n---\nfrom the provider home\n", + ); + writeFileSync( + fakeAppServerPath, + ` +const fs = require("node:fs"); + +const capturePath = process.env.PASEO_FAKE_CODEX_CAPTURE; +let buffer = ""; + +fs.appendFileSync(capturePath, JSON.stringify({ kind: "env", CODEX_HOME: process.env.CODEX_HOME }) + "\\n"); + +function resultFor(method) { + if (method === "collaborationMode/list") return { data: [] }; + if (method === "skills/list") return { data: [] }; + if (method === "model/list") return { data: [{ id: "profile-model", isDefault: true }] }; + if (method === "thread/start") return { thread: { id: "thread-1" } }; + return {}; +} + +process.stdin.on("data", (chunk) => { + buffer += chunk.toString(); + for (;;) { + const newlineIndex = buffer.indexOf("\\n"); + if (newlineIndex === -1) break; + const line = buffer.slice(0, newlineIndex).trim(); + buffer = buffer.slice(newlineIndex + 1); + if (!line) continue; + const message = JSON.parse(line); + fs.appendFileSync(capturePath, JSON.stringify({ kind: "request", method: message.method, params: message.params }) + "\\n"); + process.stdout.write(JSON.stringify({ id: message.id, result: resultFor(message.method) }) + "\\n"); + } +}); +`, + ); + + vi.stubEnv("CODEX_HOME", daemonCodexHome); + const registry = buildProviderRegistry(createTestLogger(), { + providerOverrides: { + "profile-codex": { + extends: "codex", + label: "Profile Codex", + command: [process.execPath, fakeAppServerPath], + env: { + CODEX_HOME: providerCodexHome, + PASEO_FAKE_CODEX_CAPTURE: capturedRequestsPath, + }, + }, + }, + }); + const session = await registry["profile-codex"].createClient(createTestLogger()).createSession({ + provider: "profile-codex", + cwd: tempDir, + modeId: "auto", + }); + + try { + return await run({ + session, + readCaptured: () => + readFileSync(capturedRequestsPath, "utf8") + .trim() + .split("\n") + .map((line) => JSON.parse(line) as CapturedFakeCodexRecord), + }); + } finally { + await session.close(); + vi.unstubAllEnvs(); + rmSync(tempDir, { recursive: true, force: true }); + } +} + +async function listPromptCommandsFromCustomCodexHome(): Promise { + return withCustomCodexProviderHome(async ({ session }) => { + const commands = await session.listCommands!(); + return commands.map((command) => command.name).filter((name) => name.startsWith("prompts:")); + }); +} + +async function runPromptFromCustomCodexHome(prompt: string): Promise { + return withCustomCodexProviderHome(async ({ session, readCaptured }) => { + await session.startTurn(prompt); + return readCaptured(); + }); +} + function capturedThreadStartConfig(records: CapturedFakeCodexRecord[]): unknown { const threadStart = records.find((record) => record.method === "thread/start"); const params = threadStart?.params as Record | undefined; @@ -2062,7 +2167,9 @@ describe("Codex app-server provider", () => { }; try { - await expect(listCodexSkills(cwd, workspaceGitService)).resolves.toContainEqual({ + await expect( + listCodexSkills(cwd, path.join(tempDir, "codex-home"), workspaceGitService), + ).resolves.toContainEqual({ name: "shipper", description: "Ship changes carefully.", argumentHint: "", @@ -2323,6 +2430,22 @@ describe("Codex app-server provider", () => { }); }); + test("lists custom prompts from the CODEX_HOME a custom provider runs Codex with", async () => { + await expect(listPromptCommandsFromCustomCodexHome()).resolves.toEqual([ + "prompts:probe-profile", + ]); + }); + + test("runs a custom prompt from the CODEX_HOME a custom provider runs Codex with", async () => { + const records = await runPromptFromCustomCodexHome("/prompts:probe-profile"); + + expect(records.find((record) => record.kind === "env")?.CODEX_HOME).toMatch( + /provider-codex-home$/, + ); + const turnStart = records.find((record) => record.method === "turn/start"); + expect(JSON.stringify(turnStart?.params)).toContain("from the provider home"); + }); + test("deduplicates Codex skill slash commands returned from multiple skill roots", async () => { const commands = await listCommandsFromFakeCodex([ { diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index c21f39d747..b713de0666 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -260,6 +260,8 @@ interface CodexAppServerAgentDeps { extends: string; }; customCodexConfig?: CodexCustomProviderConfig | null; + // The CODEX_HOME the session's app-server runs with; prompts and skills are read from it. + codexHome?: string; _createCodexClient?: ( child: ChildProcessWithoutNullStreams, logger: Logger, @@ -545,8 +547,8 @@ async function checkCodexLaunchAvailable(launch: ResolvedProviderLaunch) { }); } -function resolveCodexHomeDir(): string { - return process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex"); +function resolveCodexHomeDir(env: NodeJS.ProcessEnv): string { + return env.CODEX_HOME ?? path.join(os.homedir(), ".codex"); } function decodeEscapedChar(next: string): string { @@ -656,8 +658,7 @@ function parseFrontMatter(markdown: string): { return { frontMatter, body }; } -async function listCodexCustomPrompts(): Promise { - const codexHome = resolveCodexHomeDir(); +async function listCodexCustomPrompts(codexHome: string): Promise { const promptsDir = path.join(codexHome, "prompts"); let entries: Dirent[]; try { @@ -699,6 +700,7 @@ async function listCodexCustomPrompts(): Promise { export async function listCodexSkills( cwd: string, + codexHome: string, workspaceGitService?: Pick, ): Promise { const candidates: string[] = []; @@ -712,7 +714,7 @@ export async function listCodexSkills( candidates.push(path.join(repoRoot, ".codex", "skills")); } - candidates.push(path.join(resolveCodexHomeDir(), "skills")); + candidates.push(path.join(codexHome, "skills")); const candidateReads = await Promise.all( candidates.map(async (dir) => { @@ -3354,6 +3356,7 @@ export class CodexAppServerAgentSession implements AgentSession { private readonly logger: Logger; private readonly config: AgentSessionConfig; private readonly asyncQuestions: CodexAsyncQuestions; + private readonly codexHome: string; private currentMode: string; private hasWorkflowModeOverride: boolean; private readonly providerOptions: CodexProviderOptions; @@ -3467,6 +3470,7 @@ export class CodexAppServerAgentSession implements AgentSession { this.providerOptions = CodexProviderOptionsSchema.parse(config.providerOptions ?? {}); this.config = config; this.asyncQuestions = new CodexAsyncQuestions(resumeHandle?.metadata?.asyncQuestions); + this.codexHome = deps.codexHome ?? resolveCodexHomeDir(process.env); this.config.thinkingOptionId = normalizeCodexThinkingOptionId(this.config.thinkingOptionId); if (this.config.featureValues?.fast_mode && codexModelSupportsFastMode(this.config.model)) { this.serviceTier = "fast"; @@ -4037,8 +4041,7 @@ export class CodexAppServerAgentSession implements AgentSession { ): Promise { if (commandName.startsWith("prompts:")) { const promptName = commandName.slice("prompts:".length); - const codexHome = resolveCodexHomeDir(); - const promptPath = path.join(codexHome, "prompts", `${promptName}.md`); + const promptPath = path.join(this.codexHome, "prompts", `${promptName}.md`); const raw = await fs.readFile(promptPath, "utf8"); const parsed = parseFrontMatter(raw); return expandCodexCustomPrompt(parsed.body, args); @@ -4942,7 +4945,7 @@ export class CodexAppServerAgentSession implements AgentSession { } async listCommands(): Promise { - const prompts = await listCodexCustomPrompts(); + const prompts = await listCodexCustomPrompts(this.codexHome); if (this.connectionState === "disconnected") { await this.connect(); } else { @@ -4956,7 +4959,7 @@ export class CodexAppServerAgentSession implements AgentSession { })); const fallbackSkills = this.cachedSkills === null - ? await listCodexSkills(this.config.cwd, this.deps.workspaceGitService) + ? await listCodexSkills(this.config.cwd, this.codexHome, this.deps.workspaceGitService) : []; const builtin: AgentSlashCommand[] = [ { @@ -7025,9 +7028,10 @@ export class CodexAppServerAgentClient implements AgentClient { private readonly deps: CodexAppServerAgentDeps = {}, ) {} - private sessionDeps(): CodexAppServerAgentDeps { + private sessionDeps(launchEnv: Record | undefined): CodexAppServerAgentDeps { return { ...this.deps, + codexHome: resolveCodexHomeDir(buildCodexAppServerEnv(this.runtimeSettings, launchEnv)), customCodexConfig: this.customProviderConfig(), }; } @@ -7139,7 +7143,7 @@ export class CodexAppServerAgentClient implements AgentClient { this.logger, () => this.spawnAppServer(launchContext?.env, { goalsEnabled, agentId: launchContext?.agentId }), - this.sessionDeps(), + this.sessionDeps(launchContext?.env), options?.persistSession === false, goalsEnabled, autoReviewEnabled, @@ -7170,7 +7174,7 @@ export class CodexAppServerAgentClient implements AgentClient { this.logger, () => this.spawnAppServer(launchContext?.env, { goalsEnabled, agentId: launchContext?.agentId }), - this.sessionDeps(), + this.sessionDeps(launchContext?.env), false, goalsEnabled, autoReviewEnabled,