mirror of
https://github.com/openai/codex.git
synced 2025-12-03 18:35:00 +00:00
feat(ts-sdk): allow overriding CLI environment (#6648)
## Summary - add an `env` option for the TypeScript Codex client and plumb it into `CodexExec` so the CLI can run without inheriting `process.env` - extend the test spy to capture spawn environments, add coverage for the new option, and document how to use it ## Testing - `pnpm test` *(fails: corepack cannot download pnpm because outbound network access is blocked in the sandbox)* ------ [Codex Task](https://chatgpt.com/codex/tasks/task_i_6916b2d7c7548322a72d61d91a2dac85)
This commit is contained in:
@@ -115,3 +115,19 @@ const thread = codex.startThread({
|
||||
skipGitRepoCheck: true,
|
||||
});
|
||||
```
|
||||
|
||||
### Controlling the Codex CLI environment
|
||||
|
||||
By default, the Codex CLI inherits the Node.js process environment. Provide the optional `env` parameter when instantiating the
|
||||
`Codex` client to fully control which variables the CLI receives—useful for sandboxed hosts like Electron apps.
|
||||
|
||||
```typescript
|
||||
const codex = new Codex({
|
||||
env: {
|
||||
PATH: "/usr/local/bin",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The SDK still injects its required variables (such as `OPENAI_BASE_URL` and `CODEX_API_KEY`) on top of the environment you
|
||||
provide.
|
||||
|
||||
@@ -13,7 +13,7 @@ export class Codex {
|
||||
private options: CodexOptions;
|
||||
|
||||
constructor(options: CodexOptions = {}) {
|
||||
this.exec = new CodexExec(options.codexPathOverride);
|
||||
this.exec = new CodexExec(options.codexPathOverride, options.env);
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,4 +2,9 @@ export type CodexOptions = {
|
||||
codexPathOverride?: string;
|
||||
baseUrl?: string;
|
||||
apiKey?: string;
|
||||
/**
|
||||
* Environment variables passed to the Codex CLI process. When provided, the SDK
|
||||
* will not inherit variables from `process.env`.
|
||||
*/
|
||||
env?: Record<string, string>;
|
||||
};
|
||||
|
||||
@@ -41,8 +41,11 @@ const TYPESCRIPT_SDK_ORIGINATOR = "codex_sdk_ts";
|
||||
|
||||
export class CodexExec {
|
||||
private executablePath: string;
|
||||
constructor(executablePath: string | null = null) {
|
||||
private envOverride?: Record<string, string>;
|
||||
|
||||
constructor(executablePath: string | null = null, env?: Record<string, string>) {
|
||||
this.executablePath = executablePath || findCodexPath();
|
||||
this.envOverride = env;
|
||||
}
|
||||
|
||||
async *run(args: CodexExecArgs): AsyncGenerator<string> {
|
||||
@@ -103,9 +106,16 @@ export class CodexExec {
|
||||
commandArgs.push("resume", args.threadId);
|
||||
}
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
};
|
||||
const env: Record<string, string> = {};
|
||||
if (this.envOverride) {
|
||||
Object.assign(env, this.envOverride);
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (value !== undefined) {
|
||||
env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!env[INTERNAL_ORIGINATOR_ENV]) {
|
||||
env[INTERNAL_ORIGINATOR_ENV] = TYPESCRIPT_SDK_ORIGINATOR;
|
||||
}
|
||||
|
||||
@@ -9,18 +9,26 @@ const actualChildProcess =
|
||||
jest.requireActual<typeof import("node:child_process")>("node:child_process");
|
||||
const spawnMock = child_process.spawn as jest.MockedFunction<typeof actualChildProcess.spawn>;
|
||||
|
||||
export function codexExecSpy(): { args: string[][]; restore: () => void } {
|
||||
export function codexExecSpy(): {
|
||||
args: string[][];
|
||||
envs: (Record<string, string> | undefined)[];
|
||||
restore: () => void;
|
||||
} {
|
||||
const previousImplementation = spawnMock.getMockImplementation() ?? actualChildProcess.spawn;
|
||||
const args: string[][] = [];
|
||||
const envs: (Record<string, string> | undefined)[] = [];
|
||||
|
||||
spawnMock.mockImplementation(((...spawnArgs: Parameters<typeof child_process.spawn>) => {
|
||||
const commandArgs = spawnArgs[1];
|
||||
args.push(Array.isArray(commandArgs) ? [...commandArgs] : []);
|
||||
const options = spawnArgs[2] as child_process.SpawnOptions | undefined;
|
||||
envs.push(options?.env as Record<string, string> | undefined);
|
||||
return previousImplementation(...spawnArgs);
|
||||
}) as typeof actualChildProcess.spawn);
|
||||
|
||||
return {
|
||||
args,
|
||||
envs,
|
||||
restore: () => {
|
||||
spawnMock.mockClear();
|
||||
spawnMock.mockImplementation(previousImplementation);
|
||||
|
||||
@@ -348,6 +348,49 @@ describe("Codex", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("allows overriding the env passed to the Codex CLI", async () => {
|
||||
const { url, close } = await startResponsesTestProxy({
|
||||
statusCode: 200,
|
||||
responseBodies: [
|
||||
sse(
|
||||
responseStarted("response_1"),
|
||||
assistantMessage("Custom env", "item_1"),
|
||||
responseCompleted("response_1"),
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
const { envs: spawnEnvs, restore } = codexExecSpy();
|
||||
process.env.CODEX_ENV_SHOULD_NOT_LEAK = "leak";
|
||||
|
||||
try {
|
||||
const client = new Codex({
|
||||
codexPathOverride: codexExecPath,
|
||||
baseUrl: url,
|
||||
apiKey: "test",
|
||||
env: { CUSTOM_ENV: "custom" },
|
||||
});
|
||||
|
||||
const thread = client.startThread();
|
||||
await thread.run("custom env");
|
||||
|
||||
const spawnEnv = spawnEnvs[0];
|
||||
expect(spawnEnv).toBeDefined();
|
||||
if (!spawnEnv) {
|
||||
throw new Error("Spawn env missing");
|
||||
}
|
||||
expect(spawnEnv.CUSTOM_ENV).toBe("custom");
|
||||
expect(spawnEnv.CODEX_ENV_SHOULD_NOT_LEAK).toBeUndefined();
|
||||
expect(spawnEnv.OPENAI_BASE_URL).toBe(url);
|
||||
expect(spawnEnv.CODEX_API_KEY).toBe("test");
|
||||
expect(spawnEnv.CODEX_INTERNAL_ORIGINATOR_OVERRIDE).toBeDefined();
|
||||
} finally {
|
||||
delete process.env.CODEX_ENV_SHOULD_NOT_LEAK;
|
||||
restore();
|
||||
await close();
|
||||
}
|
||||
});
|
||||
|
||||
it("passes additionalDirectories as repeated flags", async () => {
|
||||
const { url, close } = await startResponsesTestProxy({
|
||||
statusCode: 200,
|
||||
|
||||
Reference in New Issue
Block a user