Skip to content
Closed
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
04c2097
fix(qoder): 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
51 changes: 38 additions & 13 deletions src/adapters/qoder/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 All @@ -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",
Expand All @@ -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;
}

Expand Down Expand Up @@ -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 });
Comment on lines +64 to +66

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 Harden the prompt file ACL on Windows

On Windows, mode: 0o600 does not remove inherited ACEs, as documented by the repository's existing src/lib/windows-secret-acl.ts:4-8. When %TEMP% is shared or inherits access for Users/Authenticated Users, another local account can enumerate or read this file, whose path is also exposed through --append-system-prompt-file, defeating the security purpose of this change. Harden the temporary directory before writing and/or the file before spawning with the existing required async Windows ACL helpers, and fail closed if hardening fails.

AGENTS.md reference: AGENTS.md:L357-L363

Useful? React with 👍 / 👎.

} 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(() => {});
}
},
};
}
32 changes: 32 additions & 0 deletions tests/providers/qoder-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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<string> | 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);
Expand Down
Loading