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:
Michael Bolin
2026-01-27 10:47:07 -08:00
committed by GitHub
parent fc0fd85349
commit 894923ed5d
5 changed files with 240 additions and 11 deletions
+2 -1
View File
@@ -13,7 +13,8 @@ export class Codex {
private 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;
}
+12
View File
@@ -1,7 +1,19 @@
export type CodexConfigValue = string | number | boolean | CodexConfigValue[] | CodexConfigObject;
export type CodexConfigObject = { [key: string]: CodexConfigValue };
export type CodexOptions = {
codexPathOverride?: string;
baseUrl?: 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
* will not inherit variables from `process.env`.
+103 -7
View File
@@ -3,12 +3,8 @@ import path from "node:path";
import readline from "node:readline";
import { fileURLToPath } from "node:url";
import {
SandboxMode,
ModelReasoningEffort,
ApprovalMode,
WebSearchMode,
} from "./threadOptions";
import type { CodexConfigObject, CodexConfigValue } from "./codexOptions";
import { SandboxMode, ModelReasoningEffort, ApprovalMode, WebSearchMode } from "./threadOptions";
export type CodexExecArgs = {
input: string;
@@ -49,15 +45,27 @@ const TYPESCRIPT_SDK_ORIGINATOR = "codex_sdk_ts";
export class CodexExec {
private executablePath: 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.envOverride = env;
this.configOverrides = configOverrides;
}
async *run(args: CodexExecArgs): AsyncGenerator<string> {
const commandArgs: string[] = ["exec", "--experimental-json"];
if (this.configOverrides) {
for (const override of serializeConfigOverrides(this.configOverrides)) {
commandArgs.push("--config", override);
}
}
if (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 scriptDirName = path.dirname(scriptFileName);