Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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