Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
06ec553
Merge pull request #3678 from lidge-jun/codex/promote-main-243-01a07240
lidge-jun Sep 5, 2026
116c2ac
Merge commit '44ea9576e27c6be8be7f13a86e32bb349368c54d' into codex/re…
invalid-email-address Sep 6, 2026
07b48da
Merge pull request #3785 from lidge-jun/codex/release-244-main-07c0
lidge-jun Sep 6, 2026
bcdf559
chore(release): promote validated 2.45.0 to main [skip ci]
invalid-email-address Sep 6, 2026
b0900e5
chore(release): promote 2.45.0 to main (#3813)
lidge-jun Sep 6, 2026
3970601
chore(release): prepare 2.46.0 stable promotion
invalid-email-address Sep 7, 2026
bba6322
Merge pull request #3851 from lidge-jun/codex/release-246-main
lidge-jun Sep 7, 2026
3d53e5f
release: prepare 2.47.0 from audited regression candidate
invalid-email-address Sep 7, 2026
eda8754
Merge commit '48ab3e1e66cfa6e0c873de2fafa4540ac61d6c7d' into codex/re…
invalid-email-address Sep 7, 2026
f9e3515
Merge commit '57252193b' into codex/release-247-main
invalid-email-address Sep 7, 2026
6f71931
release: promote 2.47.0 to main (#3929)
lidge-jun Sep 7, 2026
9a60256
Merge commit 'd0737cff3' into codex/release-247-main-final
invalid-email-address Sep 7, 2026
9e9b1d3
Merge commit 'f48c322c0' into codex/release-247-main-final
invalid-email-address Sep 7, 2026
947bae9
Merge commit '0d7652ad1' into codex/release-247-main-final
invalid-email-address Sep 7, 2026
f7f890f
release: apply final roster correction to main (#3933)
lidge-jun Sep 7, 2026
544ebee
release: promote 2.48.0 to main
invalid-email-address Sep 8, 2026
d24ff57
release: set main channel version 2.48.0
invalid-email-address Sep 8, 2026
9a27e86
Merge pull request #4011 from lidge-jun/codex/release-248-main
lidge-jun Sep 8, 2026
62849df
release: promote verified 2.49.0 product tree to main
lidge-jun Sep 9, 2026
2f3f736
Merge pull request #4117 from lidge-jun/codex/release-249-main-01a08498
lidge-jun Sep 9, 2026
5a9cd6b
fix(codebuddy): keep system prompts out of argv
luvs01 Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 46 additions & 13 deletions src/adapters/codebuddy/adapter.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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",
Expand All @@ -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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the append-file flag to preserve the vendor prompt

For every turn containing a system or developer prompt, this changes the previous append behavior into replacement behavior: the Claude-compatible CLI distinguishes --system-prompt-file, which replaces its default system prompt, from --append-system-prompt-file, which appends file contents (CLI flag reference). Replacing the vendor prompt can remove the CLI's baseline behavioral and protocol instructions, so pass the staged file through the append-file variant instead.

Useful? React with 👍 / 👎.

// profile is retained for symmetry with the region-isolated design and future per-region flags.
void profile;
return args;
Expand All @@ -70,16 +77,42 @@ export function createCodeBuddyAdapter(provider: OcxProviderConfig, deps: CodeBu
},

async runTurn(parsed, incoming, emit): Promise<void> {
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" });
Comment on lines +83 to +87

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stage the prompt only after turn preflight

When the temporary directory is unavailable or unwritable, a request with a system prompt now emits system_prompt_staging_failed before runCodingAgentTurn can perform its pre-abort, canonical-destination, credential, or CLI checks. Consequently, even an already-cancelled request or invalid provider configuration gets the wrong error and unnecessarily writes request-derived content to disk; move staging into a post-preflight callback or perform it after those checks.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

} 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(() => {});
Comment on lines +113 to +114

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retry or report failures that leave the prompt file behind

If recursive removal encounters a transient EPERM or EBUSY—notably on Windows—the empty catch silently leaves the plaintext system/developer prompt in the temporary directory indefinitely. Since this change's privacy guarantee depends on deleting that file after the child exits, use bounded removal retries and retain a cleanup mechanism or diagnostic when deletion still fails rather than discarding the error.

AGENTS.md reference: src/AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

}
},
};
}
36 changes: 33 additions & 3 deletions tests/providers/codebuddy-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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.");
});
});

Expand Down Expand Up @@ -184,13 +187,18 @@ 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",
spawn: (seenCommand, seenArgs, seenOptions) => {
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,
Expand All @@ -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);
});
});

Expand Down
Loading