mirror of
https://github.com/openai/codex.git
synced 2026-04-24 22:54:54 +00:00
feat: make it possible to specify --config flags in the SDK (#10003)
Updates the `CodexOptions` passed to the `Codex()` constructor in the
SDK to support a `config` property that is a map of configuration data
that will be transformed into `--config` flags passed to the invocation
of `codex`.
Therefore, something like this:
```typescript
const codex = new Codex({
config: {
show_raw_agent_reasoning: true,
sandbox_workspace_write: { network_access: true },
},
});
```
would result in the following args being added to the invocation of
`codex`:
```shell
--config show_raw_agent_reasoning=true --config sandbox_workspace_write.network_access=true
```
This commit is contained in:
@@ -131,3 +131,19 @@ const codex = new Codex({
|
|||||||
|
|
||||||
The SDK still injects its required variables (such as `OPENAI_BASE_URL` and `CODEX_API_KEY`) on top of the environment you
|
The SDK still injects its required variables (such as `OPENAI_BASE_URL` and `CODEX_API_KEY`) on top of the environment you
|
||||||
provide.
|
provide.
|
||||||
|
|
||||||
|
### Passing `--config` overrides
|
||||||
|
|
||||||
|
Use the `config` option to provide additional Codex CLI configuration overrides. The SDK accepts a JSON object, flattens it
|
||||||
|
into dotted paths, and serializes values as TOML literals before passing them as repeated `--config key=value` flags.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const codex = new Codex({
|
||||||
|
config: {
|
||||||
|
show_raw_agent_reasoning: true,
|
||||||
|
sandbox_workspace_write: { network_access: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Thread options still take precedence for overlapping settings because they are emitted after these global overrides.
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ export class Codex {
|
|||||||
private options: CodexOptions;
|
private options: CodexOptions;
|
||||||
|
|
||||||
constructor(options: CodexOptions = {}) {
|
constructor(options: CodexOptions = {}) {
|
||||||
this.exec = new CodexExec(options.codexPathOverride, options.env);
|
const { codexPathOverride, env, config } = options;
|
||||||
|
this.exec = new CodexExec(codexPathOverride, env, config);
|
||||||
this.options = options;
|
this.options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
|
export type CodexConfigValue = string | number | boolean | CodexConfigValue[] | CodexConfigObject;
|
||||||
|
|
||||||
|
export type CodexConfigObject = { [key: string]: CodexConfigValue };
|
||||||
|
|
||||||
export type CodexOptions = {
|
export type CodexOptions = {
|
||||||
codexPathOverride?: string;
|
codexPathOverride?: string;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
|
/**
|
||||||
|
* Additional `--config key=value` overrides to pass to the Codex CLI.
|
||||||
|
*
|
||||||
|
* Provide a JSON object and the SDK will flatten it into dotted paths and
|
||||||
|
* serialize values as TOML literals so they are compatible with the CLI's
|
||||||
|
* `--config` parsing.
|
||||||
|
*/
|
||||||
|
config?: CodexConfigObject;
|
||||||
/**
|
/**
|
||||||
* Environment variables passed to the Codex CLI process. When provided, the SDK
|
* Environment variables passed to the Codex CLI process. When provided, the SDK
|
||||||
* will not inherit variables from `process.env`.
|
* will not inherit variables from `process.env`.
|
||||||
|
|||||||
@@ -3,12 +3,8 @@ import path from "node:path";
|
|||||||
import readline from "node:readline";
|
import readline from "node:readline";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
import {
|
import type { CodexConfigObject, CodexConfigValue } from "./codexOptions";
|
||||||
SandboxMode,
|
import { SandboxMode, ModelReasoningEffort, ApprovalMode, WebSearchMode } from "./threadOptions";
|
||||||
ModelReasoningEffort,
|
|
||||||
ApprovalMode,
|
|
||||||
WebSearchMode,
|
|
||||||
} from "./threadOptions";
|
|
||||||
|
|
||||||
export type CodexExecArgs = {
|
export type CodexExecArgs = {
|
||||||
input: string;
|
input: string;
|
||||||
@@ -49,15 +45,27 @@ const TYPESCRIPT_SDK_ORIGINATOR = "codex_sdk_ts";
|
|||||||
export class CodexExec {
|
export class CodexExec {
|
||||||
private executablePath: string;
|
private executablePath: string;
|
||||||
private envOverride?: Record<string, string>;
|
private envOverride?: Record<string, string>;
|
||||||
|
private configOverrides?: CodexConfigObject;
|
||||||
|
|
||||||
constructor(executablePath: string | null = null, env?: Record<string, string>) {
|
constructor(
|
||||||
|
executablePath: string | null = null,
|
||||||
|
env?: Record<string, string>,
|
||||||
|
configOverrides?: CodexConfigObject,
|
||||||
|
) {
|
||||||
this.executablePath = executablePath || findCodexPath();
|
this.executablePath = executablePath || findCodexPath();
|
||||||
this.envOverride = env;
|
this.envOverride = env;
|
||||||
|
this.configOverrides = configOverrides;
|
||||||
}
|
}
|
||||||
|
|
||||||
async *run(args: CodexExecArgs): AsyncGenerator<string> {
|
async *run(args: CodexExecArgs): AsyncGenerator<string> {
|
||||||
const commandArgs: string[] = ["exec", "--experimental-json"];
|
const commandArgs: string[] = ["exec", "--experimental-json"];
|
||||||
|
|
||||||
|
if (this.configOverrides) {
|
||||||
|
for (const override of serializeConfigOverrides(this.configOverrides)) {
|
||||||
|
commandArgs.push("--config", override);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (args.model) {
|
if (args.model) {
|
||||||
commandArgs.push("--model", args.model);
|
commandArgs.push("--model", args.model);
|
||||||
}
|
}
|
||||||
@@ -202,6 +210,94 @@ export class CodexExec {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function serializeConfigOverrides(configOverrides: CodexConfigObject): string[] {
|
||||||
|
const overrides: string[] = [];
|
||||||
|
flattenConfigOverrides(configOverrides, "", overrides);
|
||||||
|
return overrides;
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenConfigOverrides(
|
||||||
|
value: CodexConfigValue,
|
||||||
|
prefix: string,
|
||||||
|
overrides: string[],
|
||||||
|
): void {
|
||||||
|
if (!isPlainObject(value)) {
|
||||||
|
if (prefix) {
|
||||||
|
overrides.push(`${prefix}=${toTomlValue(value, prefix)}`);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
throw new Error("Codex config overrides must be a plain object");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = Object.entries(value);
|
||||||
|
if (!prefix && entries.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefix && entries.length === 0) {
|
||||||
|
overrides.push(`${prefix}={}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, child] of entries) {
|
||||||
|
if (!key) {
|
||||||
|
throw new Error("Codex config override keys must be non-empty strings");
|
||||||
|
}
|
||||||
|
if (child === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const path = prefix ? `${prefix}.${key}` : key;
|
||||||
|
if (isPlainObject(child)) {
|
||||||
|
flattenConfigOverrides(child, path, overrides);
|
||||||
|
} else {
|
||||||
|
overrides.push(`${path}=${toTomlValue(child, path)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toTomlValue(value: CodexConfigValue, path: string): string {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return JSON.stringify(value);
|
||||||
|
} else if (typeof value === "number") {
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
throw new Error(`Codex config override at ${path} must be a finite number`);
|
||||||
|
}
|
||||||
|
return `${value}`;
|
||||||
|
} else if (typeof value === "boolean") {
|
||||||
|
return value ? "true" : "false";
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
const rendered = value.map((item, index) => toTomlValue(item, `${path}[${index}]`));
|
||||||
|
return `[${rendered.join(", ")}]`;
|
||||||
|
} else if (isPlainObject(value)) {
|
||||||
|
const parts: string[] = [];
|
||||||
|
for (const [key, child] of Object.entries(value)) {
|
||||||
|
if (!key) {
|
||||||
|
throw new Error("Codex config override keys must be non-empty strings");
|
||||||
|
}
|
||||||
|
if (child === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
parts.push(`${formatTomlKey(key)} = ${toTomlValue(child, `${path}.${key}`)}`);
|
||||||
|
}
|
||||||
|
return `{${parts.join(", ")}}`;
|
||||||
|
} else if (value === null) {
|
||||||
|
throw new Error(`Codex config override at ${path} cannot be null`);
|
||||||
|
} else {
|
||||||
|
const typeName = typeof value;
|
||||||
|
throw new Error(`Unsupported Codex config override value at ${path}: ${typeName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOML_BARE_KEY = /^[A-Za-z0-9_-]+$/;
|
||||||
|
function formatTomlKey(key: string): string {
|
||||||
|
return TOML_BARE_KEY.test(key) ? key : JSON.stringify(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainObject(value: unknown): value is CodexConfigObject {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
const scriptFileName = fileURLToPath(import.meta.url);
|
const scriptFileName = fileURLToPath(import.meta.url);
|
||||||
const scriptDirName = path.dirname(scriptFileName);
|
const scriptDirName = path.dirname(scriptFileName);
|
||||||
|
|
||||||
|
|||||||
@@ -410,6 +410,86 @@ describe("Codex", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("passes CodexOptions config overrides as TOML --config flags", async () => {
|
||||||
|
const { url, close } = await startResponsesTestProxy({
|
||||||
|
statusCode: 200,
|
||||||
|
responseBodies: [
|
||||||
|
sse(
|
||||||
|
responseStarted("response_1"),
|
||||||
|
assistantMessage("Config overrides applied", "item_1"),
|
||||||
|
responseCompleted("response_1"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { args: spawnArgs, restore } = codexExecSpy();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = new Codex({
|
||||||
|
codexPathOverride: codexExecPath,
|
||||||
|
baseUrl: url,
|
||||||
|
apiKey: "test",
|
||||||
|
config: {
|
||||||
|
approval_policy: "never",
|
||||||
|
sandbox_workspace_write: { network_access: true },
|
||||||
|
retry_budget: 3,
|
||||||
|
tool_rules: { allow: ["git status", "git diff"] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const thread = client.startThread();
|
||||||
|
await thread.run("apply config overrides");
|
||||||
|
|
||||||
|
const commandArgs = spawnArgs[0];
|
||||||
|
expect(commandArgs).toBeDefined();
|
||||||
|
expectPair(commandArgs, ["--config", 'approval_policy="never"']);
|
||||||
|
expectPair(commandArgs, ["--config", "sandbox_workspace_write.network_access=true"]);
|
||||||
|
expectPair(commandArgs, ["--config", "retry_budget=3"]);
|
||||||
|
expectPair(commandArgs, ["--config", 'tool_rules.allow=["git status", "git diff"]']);
|
||||||
|
} finally {
|
||||||
|
restore();
|
||||||
|
await close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lets thread options override CodexOptions config overrides", async () => {
|
||||||
|
const { url, close } = await startResponsesTestProxy({
|
||||||
|
statusCode: 200,
|
||||||
|
responseBodies: [
|
||||||
|
sse(
|
||||||
|
responseStarted("response_1"),
|
||||||
|
assistantMessage("Thread overrides applied", "item_1"),
|
||||||
|
responseCompleted("response_1"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { args: spawnArgs, restore } = codexExecSpy();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = new Codex({
|
||||||
|
codexPathOverride: codexExecPath,
|
||||||
|
baseUrl: url,
|
||||||
|
apiKey: "test",
|
||||||
|
config: { approval_policy: "never" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const thread = client.startThread({ approvalPolicy: "on-request" });
|
||||||
|
await thread.run("override approval policy");
|
||||||
|
|
||||||
|
const commandArgs = spawnArgs[0];
|
||||||
|
const approvalPolicyOverrides = collectConfigValues(commandArgs, "approval_policy");
|
||||||
|
expect(approvalPolicyOverrides).toEqual([
|
||||||
|
'approval_policy="never"',
|
||||||
|
'approval_policy="on-request"',
|
||||||
|
]);
|
||||||
|
expect(approvalPolicyOverrides.at(-1)).toBe('approval_policy="on-request"');
|
||||||
|
} finally {
|
||||||
|
restore();
|
||||||
|
await close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("allows overriding the env passed to the Codex CLI", async () => {
|
it("allows overriding the env passed to the Codex CLI", async () => {
|
||||||
const { url, close } = await startResponsesTestProxy({
|
const { url, close } = await startResponsesTestProxy({
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
@@ -737,13 +817,37 @@ describe("Codex", () => {
|
|||||||
}
|
}
|
||||||
}, 10000); // TODO(pakrym): remove timeout
|
}, 10000); // TODO(pakrym): remove timeout
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a list of args to `codex` and a `key`, collects all `--config`
|
||||||
|
* overrides for that key.
|
||||||
|
*/
|
||||||
|
function collectConfigValues(args: string[] | undefined, key: string): string[] {
|
||||||
|
if (!args) {
|
||||||
|
throw new Error("args is undefined");
|
||||||
|
}
|
||||||
|
|
||||||
|
const values: string[] = [];
|
||||||
|
for (let i = 0; i < args.length; i += 1) {
|
||||||
|
if (args[i] !== "--config") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const override = args[i + 1];
|
||||||
|
if (override?.startsWith(`${key}=`)) {
|
||||||
|
values.push(override);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
function expectPair(args: string[] | undefined, pair: [string, string]) {
|
function expectPair(args: string[] | undefined, pair: [string, string]) {
|
||||||
if (!args) {
|
if (!args) {
|
||||||
throw new Error("Args is undefined");
|
throw new Error("args is undefined");
|
||||||
}
|
}
|
||||||
const index = args.indexOf(pair[0]);
|
const index = args.findIndex((arg, i) => arg === pair[0] && args[i + 1] === pair[1]);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
throw new Error(`Pair ${pair[0]} not found in args`);
|
throw new Error(`Pair ${pair[0]} ${pair[1]} not found in args`);
|
||||||
}
|
}
|
||||||
expect(args[index + 1]).toBe(pair[1]);
|
expect(args[index + 1]).toBe(pair[1]);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user