diff --git a/src/adapters/codebuddy/adapter.ts b/src/adapters/codebuddy/adapter.ts index 234e06907e..5c42b5e5db 100644 --- a/src/adapters/codebuddy/adapter.ts +++ b/src/adapters/codebuddy/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"; @@ -34,7 +37,12 @@ export function buildChildEnv(profile: CodeBuddyProfile, apiKey: string): Record * would require authorization is blocked. The turn is a single text/reasoning pass over stream-json; * Codex's tool catalog is not advertised in v1 (the control-protocol tool bridge is a fast-follow). */ -export function buildArgs(profile: CodeBuddyProfile, parsed: OcxParsedRequest, provider: OcxProviderConfig): string[] { +export function buildArgs( + profile: CodeBuddyProfile, + parsed: OcxParsedRequest, + provider: OcxProviderConfig, + systemPromptFile?: string, +): string[] { const args: string[] = [ "-p", "--output-format", "stream-json", @@ -49,8 +57,7 @@ export function buildArgs(profile: CodeBuddyProfile, parsed: OcxParsedRequest, p ]; const effort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning); if (effort) args.push("--effort", effort); - const system = buildSystemPrompt(parsed); - if (system) args.push("--append-system-prompt", system); + if (systemPromptFile) args.push("--system-prompt-file", systemPromptFile); // profile is retained for symmetry with the region-isolated design and future per-region flags. void profile; return args; @@ -70,16 +77,42 @@ export function createCodeBuddyAdapter(provider: OcxProviderConfig, deps: CodeBu }, async runTurn(parsed, incoming, emit): Promise { - await runCodingAgentTurn({ - profiles: CODEBUDDY_PROFILES, - provider, - parsed, - incoming, - emit, - buildArgs: (resolved, req, prov) => buildArgs(resolved as CodeBuddyProfile, req, prov), - buildEnv: (resolved, apiKey) => buildChildEnv(resolved as CodeBuddyProfile, apiKey), - deps, - }); + const system = buildSystemPrompt(parsed); + let promptDir: string | undefined; + let promptFile: string | undefined; + if (system) { + try { + promptDir = await mkdtemp(join(tmpdir(), "ocx-codebuddy-prompt-")); + promptFile = join(promptDir, "system-prompt.txt"); + await writeFile(promptFile, system, { encoding: "utf8", mode: 0o600, flag: "wx" }); + } catch { + if (promptDir) await rm(promptDir, { recursive: true, force: true }).catch(() => {}); + emit({ + type: "error", + message: "CodeBuddy system prompt could not be staged securely.", + status: 500, + errorType: "upstream_error", + code: "system_prompt_staging_failed", + retryable: false, + }); + return; + } + } + + try { + await runCodingAgentTurn({ + profiles: CODEBUDDY_PROFILES, + provider, + parsed, + incoming, + emit, + buildArgs: (resolved, req, prov) => buildArgs(resolved as CodeBuddyProfile, req, prov, promptFile), + buildEnv: (resolved, apiKey) => buildChildEnv(resolved as CodeBuddyProfile, apiKey), + deps, + }); + } finally { + if (promptDir) await rm(promptDir, { recursive: true, force: true }).catch(() => {}); + } }, }; } diff --git a/tests/providers/codebuddy-adapter.test.ts b/tests/providers/codebuddy-adapter.test.ts index 0da898ea82..cae5d98484 100644 --- a/tests/providers/codebuddy-adapter.test.ts +++ b/tests/providers/codebuddy-adapter.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, test } from "bun:test"; +import { existsSync, readFileSync, statSync } from "node:fs"; import { EventEmitter } from "node:events"; import { Readable, Writable } from "node:stream"; import type { ChildProcess } from "node:child_process"; @@ -113,14 +114,16 @@ describe("codebuddy headless arguments keep tool ownership with Codex", () => { expect(args[args.indexOf("--model") + 1]).toBe("glm-5.3"); }); - test("maps Codex reasoning effort onto --effort and folds the system prompt", () => { + test("maps Codex reasoning effort and references a private system-prompt file", () => { const args = buildArgs( CODEBUDDY_GLOBAL_PROFILE, parsed({ options: { reasoning: "high" }, context: { systemPrompt: ["Be terse."], messages: [] } }), provider(), + "/private/system-prompt.txt", ); expect(args[args.indexOf("--effort") + 1]).toBe("high"); - expect(args[args.indexOf("--append-system-prompt") + 1]).toBe("Be terse."); + expect(args[args.indexOf("--system-prompt-file") + 1]).toBe("/private/system-prompt.txt"); + expect(args).not.toContain("Be terse."); }); }); @@ -184,6 +187,7 @@ describe("codebuddy runTurn fails closed before any spawn", () => { let command = ""; let args: readonly string[] = []; let options: import("node:child_process").SpawnOptions | undefined; + let promptFile = ""; const adapter = createCodeBuddyAdapter(provider(), { platform: "win32", which: () => "C:\\npm\\codebuddy.cmd", @@ -191,6 +195,10 @@ describe("codebuddy runTurn fails closed before any spawn", () => { command = seenCommand; args = seenArgs; options = seenOptions; + const commandLine = seenArgs[3] ?? ""; + const match = commandLine.match(/--system-prompt-file\s+"([^"]+)"/); + promptFile = match?.[1] ?? ""; + expect(readFileSync(promptFile, "utf8")).toBe('Say "hello" & stop'); return fakeChild([enc.encode('{"type":"result","subtype":"success"}\n')]) as unknown as ChildProcess; }, killGraceMs: 20, @@ -200,8 +208,30 @@ describe("codebuddy runTurn fails closed before any spawn", () => { expect(command.toLowerCase()).toContain("cmd.exe"); expect(args.slice(0, 3)).toEqual(["/d", "/s", "/c"]); expect(args[3]).toContain("codebuddy.cmd"); - expect(args[3]).toContain("Say"); + expect(args[3]).not.toContain("Say"); expect(options?.windowsVerbatimArguments).toBe(true); + expect(existsSync(promptFile)).toBe(false); + }); + + test("keeps request-derived prompts out of argv and removes the private staging file", async () => { + let promptFile = ""; + const secret = "private-system-instruction"; + const adapter = createCodeBuddyAdapter(provider(), { + which: () => "/usr/bin/codebuddy", + spawn: (_command, args) => { + expect(args).not.toContain(secret); + const index = args.indexOf("--system-prompt-file"); + expect(index).toBeGreaterThanOrEqual(0); + promptFile = args[index + 1] ?? ""; + expect(readFileSync(promptFile, "utf8")).toBe(secret); + if (process.platform !== "win32") expect(statSync(promptFile).mode & 0o777).toBe(0o600); + return fakeChild([enc.encode('{"type":"result","subtype":"success"}\n')]) as unknown as ChildProcess; + }, + killGraceMs: 20, + }); + + await run(adapter, parsed({ context: { systemPrompt: [secret], messages: [] } })); + expect(existsSync(promptFile)).toBe(false); }); });