diff --git a/src/adapters/qoder/adapter.ts b/src/adapters/qoder/adapter.ts index 20c0b5581b..d42d9054fd 100644 --- a/src/adapters/qoder/adapter.ts +++ b/src/adapters/qoder/adapter.ts @@ -1,4 +1,7 @@ import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../../types"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import type { AdapterRequest, ProviderAdapter } from "../base"; import { mapReasoningEffort } from "../../reasoning-effort"; import { buildSystemPrompt } from "../coding-agent/protocol"; @@ -12,7 +15,7 @@ export function buildQoderChildEnv(profile: QoderProfile, apiKey: string): Recor } /** Single-shot, tools-disabled Qoder CLI invocation; Codex remains the tool owner. */ -export function buildQoderArgs(parsed: OcxParsedRequest, provider: OcxProviderConfig): string[] { +export function buildQoderArgs(parsed: OcxParsedRequest, provider: OcxProviderConfig, systemPromptFile?: string): string[] { const args = [ "-p", "--output-format", "stream-json", @@ -26,8 +29,7 @@ export function buildQoderArgs(parsed: OcxParsedRequest, provider: OcxProviderCo ]; const effort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning); if (effort) args.push("--reasoning-effort", effort); - const system = buildSystemPrompt(parsed); - if (system) args.push("--append-system-prompt", system); + if (systemPromptFile) args.push("--append-system-prompt-file", systemPromptFile); return args; } @@ -55,16 +57,39 @@ export function createQoderAdapter(provider: OcxProviderConfig, deps: QoderAdapt }); return; } - await runCodingAgentTurn({ - profiles: QODER_PROFILES, - provider, - parsed, - incoming, - emit, - buildArgs: (_profile, req, prov) => buildQoderArgs(req, prov), - buildEnv: (profile, apiKey) => buildQoderChildEnv(profile as QoderProfile, apiKey), - deps, - }); + const system = buildSystemPrompt(parsed); + let promptDir: string | undefined; + let promptFile: string | undefined; + try { + promptDir = system ? await mkdtemp(join(tmpdir(), "ocx-qoder-prompt-")) : undefined; + promptFile = promptDir ? join(promptDir, "system-prompt.txt") : undefined; + if (promptFile) await writeFile(promptFile, system!, { encoding: "utf8", mode: 0o600 }); + } catch { + if (promptDir) await rm(promptDir, { recursive: true, force: true }).catch(() => {}); + emit({ + type: "error", + message: "Qoder system prompt could not be prepared securely.", + status: 500, + errorType: "upstream_error", + code: "prompt_file_failed", + retryable: false, + }); + return; + } + try { + await runCodingAgentTurn({ + profiles: QODER_PROFILES, + provider, + parsed, + incoming, + emit, + buildArgs: (_profile, req, prov) => buildQoderArgs(req, prov, promptFile), + buildEnv: (profile, apiKey) => buildQoderChildEnv(profile as QoderProfile, apiKey), + deps, + }); + } finally { + if (promptDir) await rm(promptDir, { recursive: true, force: true }).catch(() => {}); + } }, }; } diff --git a/tests/providers/qoder-adapter.test.ts b/tests/providers/qoder-adapter.test.ts index 5411504354..4066fc5e99 100644 --- a/tests/providers/qoder-adapter.test.ts +++ b/tests/providers/qoder-adapter.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, test } from "bun:test"; import { EventEmitter } from "node:events"; import { Readable, Writable } from "node:stream"; +import { readFile } from "node:fs/promises"; import type { ChildProcess } from "node:child_process"; import { buildQoderArgs, buildQoderChildEnv, createQoderAdapter } from "../../src/adapters/qoder/adapter"; import { clearQoderBinaryCache, QODER_CN_PROFILE, QODER_GLOBAL_PROFILE, resolveQoderProfile } from "../../src/adapters/qoder/profiles"; @@ -44,6 +45,37 @@ describe("qoder adapter", () => { expect(args).not.toContain("--dangerously-skip-permissions"); }); + test("keeps system and developer prompts out of child-process arguments", async () => { + const secretSystem = "private system instructions"; + const secretDeveloper = "private developer context"; + let args: readonly string[] = []; + let promptFromFile: Promise | undefined; + const adapter = createQoderAdapter(provider(), { + which: () => "/bin/qoder", + spawn: (_command, childArgs) => { + args = childArgs; + const flag = childArgs.indexOf("--append-system-prompt-file"); + promptFromFile = readFile(childArgs[flag + 1]!, "utf8"); + return fakeChild(['{"type":"result","subtype":"success","is_error":false}\n']); + }, + }); + await adapter.runTurn!(parsed({ + context: { + systemPrompt: [secretSystem], + messages: [ + { role: "developer", content: secretDeveloper, timestamp: 0 }, + { role: "user", content: "hello", timestamp: 0 }, + ], + }, + }), { headers: new Headers(), translatorBudget: createTestTranslatorBudget() }, () => {}); + + const promptPath = args[args.indexOf("--append-system-prompt-file") + 1]!; + expect(args.join(" ")).not.toContain(secretSystem); + expect(args.join(" ")).not.toContain(secretDeveloper); + expect(await readFile(promptPath, "utf8").catch(() => "removed")).toBe("removed"); + expect(await promptFromFile).toBe(`${secretSystem}\n\n${secretDeveloper}`); + }); + test("keeps Global and CN profiles, executables, destinations, and PAT variables isolated", async () => { expect(resolveQoderProfile("https://qoder.com/")).toBe(QODER_GLOBAL_PROFILE); expect(resolveQoderProfile("https://qoder.cn/")).toBe(QODER_CN_PROFILE);