Skip to content

Commit 3a63598

Browse files
authored
Support API key auth from environment variables (#236)
Has the behavior from the old adapter of reading the env vars if one isn't supplied. Closes #234
1 parent 374e684 commit 3a63598

7 files changed

Lines changed: 164 additions & 27 deletions

File tree

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,20 @@ CODEX_PATH=/path/to/codex npx -y @agentclientprotocol/codex-acp
4040

4141
The adapter advertises ACP auth methods during initialization. Clients can authenticate with:
4242

43-
- ChatGPT login.
44-
- OpenAI API key.
43+
- ChatGPT login. Set `NO_BROWSER=1` to hide this method in remote or browserless environments.
44+
- API key via `CODEX_API_KEY` or `OPENAI_API_KEY`.
4545
- A custom OpenAI-compatible gateway, when the client opts in to the gateway auth capability.
4646

4747
## Runtime options
4848

49+
- `CODEX_API_KEY` - API key used when the API-key auth method is selected. Takes precedence over `OPENAI_API_KEY`.
50+
- `OPENAI_API_KEY` - fallback API key used when the API-key auth method is selected.
4951
- `CODEX_PATH` - run a specific Codex executable instead of the bundled package dependency.
5052
- `CODEX_CONFIG` - JSON object merged into the Codex session config.
5153
- `MODEL_PROVIDER` - model provider to pass to Codex for new sessions.
5254
- `DEFAULT_AUTH_REQUEST` - ACP auth request JSON used when Codex requires authentication.
5355
- `INITIAL_AGENT_MODE` - initial mode id: `read-only`, `agent`, or `agent-full-access`.
56+
- `NO_BROWSER` - hide browser-based ChatGPT auth when set.
5457
- `APP_SERVER_LOGS` - directory for adapter logs.
5558

5659
## Development

readme-dev.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
This package uses the bundled `@openai/codex` dependency by default.
22
Set `CODEX_PATH` to run a different Codex binary; versions other than the one specified in `package.json` may not be compatible.
33

4+
### Runtime environment
5+
6+
- `CODEX_API_KEY` - API key used when the API-key auth method is selected. Takes precedence over `OPENAI_API_KEY`.
7+
- `OPENAI_API_KEY` - fallback API key used when the API-key auth method is selected.
8+
- `CODEX_PATH` - run a specific Codex executable instead of the bundled package dependency.
9+
- `CODEX_CONFIG` - JSON object merged into the Codex session config.
10+
- `MODEL_PROVIDER` - model provider to pass to Codex for new sessions.
11+
- `DEFAULT_AUTH_REQUEST` - ACP auth request JSON used when Codex requires authentication.
12+
- `INITIAL_AGENT_MODE` - initial mode id: `read-only`, `agent`, or `agent-full-access`.
13+
- `NO_BROWSER` - hide browser-based ChatGPT auth when set.
14+
- `APP_SERVER_LOGS` - directory for adapter logs.
15+
416
### Quick start
517

618
#### Develop on Windows?

src/CodexAcpClient.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {isCodexAuthRequest} from "./CodexAuthMethod";
1+
import {CODEX_API_KEY_ENV_VAR, isCodexAuthRequest, OPENAI_API_KEY_ENV_VAR} from "./CodexAuthMethod";
22
import type {EmbeddedResourceResource} from "@agentclientprotocol/sdk";
33
import * as acp from "@agentclientprotocol/sdk";
44
import {type McpServer, RequestError} from "@agentclientprotocol/sdk";
@@ -84,15 +84,8 @@ export class CodexAcpClient {
8484

8585
switch (authRequest.methodId) {
8686
case "api-key": {
87-
if (!authRequest._meta || !authRequest._meta["api-key"]) throw RequestError.invalidRequest();
88-
const loginCompletedPromise = this.awaitNextLoginCompleted();
89-
await this.codexClient.accountLogin({
90-
type: "apiKey",
91-
apiKey: authRequest._meta["api-key"].apiKey
92-
});
93-
this.gatewayConfig = null;
94-
const result = await loginCompletedPromise;
95-
return result.success;
87+
const apiKey = authRequest._meta?.["api-key"]?.apiKey ?? this.readApiKeyFromEnv();
88+
return await this.authenticateWithApiKey(apiKey);
9689
}
9790
case "chat-gpt": {
9891
const loginCompletedPromise = this.awaitNextLoginCompleted();
@@ -139,6 +132,30 @@ export class CodexAcpClient {
139132
return false;
140133
}
141134

135+
private async authenticateWithApiKey(apiKey: string): Promise<Boolean> {
136+
const loginCompletedPromise = this.awaitNextLoginCompleted();
137+
await this.codexClient.accountLogin({
138+
type: "apiKey",
139+
apiKey,
140+
});
141+
this.gatewayConfig = null;
142+
const result = await loginCompletedPromise;
143+
return result.success;
144+
}
145+
146+
private readApiKeyFromEnv(): string {
147+
for (const envVar of [CODEX_API_KEY_ENV_VAR, OPENAI_API_KEY_ENV_VAR]) {
148+
const value = process.env[envVar]?.trim();
149+
if (value) {
150+
return value;
151+
}
152+
}
153+
throw RequestError.internalError(
154+
{envVars: [CODEX_API_KEY_ENV_VAR, OPENAI_API_KEY_ENV_VAR]},
155+
`${CODEX_API_KEY_ENV_VAR} or ${OPENAI_API_KEY_ENV_VAR} is not set`
156+
);
157+
}
158+
142159

143160
async getAuthenticationStatus(): Promise<AuthenticationStatusResponse> {
144161
const modelProvider = await this.getCurrentModelProvider();

src/CodexAuthMethod.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11

22
import type {AuthenticateRequest, AuthMethod, ClientCapabilities} from "@agentclientprotocol/sdk";
33

4+
export const CODEX_API_KEY_ENV_VAR = "CODEX_API_KEY";
5+
export const OPENAI_API_KEY_ENV_VAR = "OPENAI_API_KEY";
6+
7+
interface ApiKeyAuthRequest extends AuthenticateRequest {
8+
methodId: "api-key";
9+
_meta?: {
10+
"api-key"?: {
11+
apiKey: string;
12+
}
13+
} | null;
14+
}
15+
416
const ApiKeyAuthMethod: AuthMethod = {
517
id: "api-key",
618
name: "API Key",
@@ -12,15 +24,6 @@ const ApiKeyAuthMethod: AuthMethod = {
1224
}
1325
}
1426

15-
interface ApiKeyAuthRequest extends AuthenticateRequest {
16-
methodId: "api-key";
17-
_meta: {
18-
"api-key": {
19-
apiKey: string;
20-
}
21-
};
22-
}
23-
2427
const ChatGptAuthMethod: AuthMethod = {
2528
id: "chat-gpt",
2629
name: "ChatGPT",
@@ -54,8 +57,11 @@ export interface GatewayAuthRequest extends AuthenticateRequest {
5457
};
5558
}
5659

57-
export function getCodexAuthMethods(clientCapabilities?: ClientCapabilities | null): AuthMethod[] {
58-
const authMethods: AuthMethod[] = [ApiKeyAuthMethod, ChatGptAuthMethod];
60+
export function getCodexAuthMethods(clientCapabilities?: ClientCapabilities | null, env: NodeJS.ProcessEnv = process.env): AuthMethod[] {
61+
const authMethods: AuthMethod[] = [ApiKeyAuthMethod];
62+
if (!env["NO_BROWSER"]) {
63+
authMethods.push(ChatGptAuthMethod);
64+
}
5965
const supportsGatewayAuth = clientCapabilities?.auth?._meta?.["gateway"] === true;
6066
if (supportsGatewayAuth) {
6167
authMethods.push(GatewayAuthMethod);

src/__tests__/CodexACPAgent/CodexAcpClient.test.ts

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// noinspection ES6RedundantAwait
22

3-
import {beforeEach, describe, expect, it, vi} from 'vitest';
4-
import type {CodexAuthRequest} from "../../CodexAuthMethod";
3+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
4+
import {CODEX_API_KEY_ENV_VAR, OPENAI_API_KEY_ENV_VAR, type CodexAuthRequest} from "../../CodexAuthMethod";
55
import type * as acp from "@agentclientprotocol/sdk";
66
import {
77
createCodexMockTestFixture,
@@ -25,6 +25,10 @@ describe('ACP server test', { timeout: 40_000 }, () => {
2525
vi.clearAllMocks();
2626
});
2727

28+
afterEach(() => {
29+
vi.unstubAllEnvs();
30+
});
31+
2832
const ignoredFields = ["thread", "cwd", "id", "createdAt", "path", "threadId", "userAgent", "sandbox", "conversationId", "origins", "supportedReasoningEfforts", "reasoningEffort", "model", "readOnlyAccess", "approvalsReviewer"];
2933

3034
it('should throw error without authentication', async () => {
@@ -114,6 +118,74 @@ describe('ACP server test', { timeout: 40_000 }, () => {
114118
expect(logoutResponse).toEqual({type: "unauthenticated"});
115119
});
116120

121+
it('should authenticate with CODEX_API_KEY from the environment', async () => {
122+
const envFixture = createTestFixture();
123+
const codexAcpAgent = envFixture.getCodexAcpAgent();
124+
vi.stubEnv(CODEX_API_KEY_ENV_VAR, "CODEX_ENV_TOKEN");
125+
vi.stubEnv(OPENAI_API_KEY_ENV_VAR, "OPENAI_ENV_TOKEN");
126+
127+
await codexAcpAgent.initialize({protocolVersion: 1});
128+
await envFixture.getCodexAcpClient().logout();
129+
envFixture.clearCodexConnectionDump();
130+
131+
await codexAcpAgent.authenticate({methodId: "api-key"});
132+
133+
const transportEvents = envFixture.getCodexConnectionEvents([]);
134+
const loginRequest = transportEvents.find(event =>
135+
event.eventType === "request" &&
136+
"method" in event &&
137+
event.method === "account/login/start"
138+
);
139+
expect(loginRequest).toEqual({
140+
eventType: "request",
141+
method: "account/login/start",
142+
params: {
143+
type: "apiKey",
144+
apiKey: "CODEX_ENV_TOKEN",
145+
}
146+
});
147+
await expect(codexAcpAgent.extMethod("authentication/status", {})).resolves.toEqual({type: "api-key"});
148+
});
149+
150+
it('should fall back to OPENAI_API_KEY from the environment', async () => {
151+
const envFixture = createTestFixture();
152+
const codexAcpAgent = envFixture.getCodexAcpAgent();
153+
vi.stubEnv(CODEX_API_KEY_ENV_VAR, "");
154+
vi.stubEnv(OPENAI_API_KEY_ENV_VAR, "OPENAI_ENV_TOKEN");
155+
156+
await codexAcpAgent.initialize({protocolVersion: 1});
157+
await envFixture.getCodexAcpClient().logout();
158+
envFixture.clearCodexConnectionDump();
159+
160+
await codexAcpAgent.authenticate({methodId: "api-key"});
161+
162+
const transportEvents = envFixture.getCodexConnectionEvents([]);
163+
const loginRequest = transportEvents.find(event =>
164+
event.eventType === "request" &&
165+
"method" in event &&
166+
event.method === "account/login/start"
167+
);
168+
expect(loginRequest).toEqual({
169+
eventType: "request",
170+
method: "account/login/start",
171+
params: {
172+
type: "apiKey",
173+
apiKey: "OPENAI_ENV_TOKEN",
174+
}
175+
});
176+
await expect(codexAcpAgent.extMethod("authentication/status", {})).resolves.toEqual({type: "api-key"});
177+
});
178+
179+
it('should report a clear error when the selected API key env var is missing', async () => {
180+
const envFixture = createTestFixture();
181+
const codexAcpAgent = envFixture.getCodexAcpAgent();
182+
vi.stubEnv(CODEX_API_KEY_ENV_VAR, "");
183+
vi.stubEnv(OPENAI_API_KEY_ENV_VAR, "");
184+
185+
await expect(codexAcpAgent.authenticate({methodId: "api-key"}))
186+
.rejects.toThrow(`${CODEX_API_KEY_ENV_VAR} or ${OPENAI_API_KEY_ENV_VAR} is not set`);
187+
});
188+
117189
it('should authenticate with a gateway', async () => {
118190
const gatewayFixture = createTestFixture();
119191
const codexAcpAgent = gatewayFixture.getCodexAcpAgent();

src/__tests__/CodexACPAgent/e2e/acp-e2e-test-utils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as acp from "@agentclientprotocol/sdk";
22
import {describe, expect} from "vitest";
33
import {AgentMode} from "../../../AgentMode";
4+
import {CODEX_API_KEY_ENV_VAR, OPENAI_API_KEY_ENV_VAR} from "../../../CodexAuthMethod";
45
import {createSpawnedAgentFixture, type SpawnedAgentFixture,} from "./spawned-agent-fixture";
56

67
export {
@@ -140,9 +141,9 @@ async function createSpawnedFixture(
140141
}
141142

142143
export function requireLiveApiKey(): string {
143-
const apiKey = process.env["CODEX_API_KEY"] ?? process.env["OPENAI_API_KEY"];
144+
const apiKey = process.env[CODEX_API_KEY_ENV_VAR] ?? process.env[OPENAI_API_KEY_ENV_VAR];
144145
if (!apiKey) {
145-
throw new Error("Live integration test requires CODEX_API_KEY or OPENAI_API_KEY.");
146+
throw new Error(`Live integration test requires ${CODEX_API_KEY_ENV_VAR} or ${OPENAI_API_KEY_ENV_VAR}.`);
146147
}
147148
return apiKey;
148149
}

src/__tests__/CodexACPAgent/initialize.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,30 @@ describe('CodexACPAgent - initialize', () => {
8383
})
8484
]));
8585
});
86+
87+
it('should advertise API key auth with the legacy metadata method', () => {
88+
expect(getCodexAuthMethods()).toEqual(expect.arrayContaining([
89+
expect.objectContaining({
90+
id: "api-key",
91+
_meta: {
92+
"api-key": {
93+
provider: "openai",
94+
},
95+
},
96+
}),
97+
]));
98+
expect(getCodexAuthMethods()).not.toEqual(expect.arrayContaining([
99+
expect.objectContaining({type: "env_var"}),
100+
expect.objectContaining({id: "codex-api-key"}),
101+
expect.objectContaining({id: "openai-api-key"}),
102+
]));
103+
});
104+
105+
it('should not advertise ChatGPT auth when browser auth is disabled', () => {
106+
const methodIds = getCodexAuthMethods(undefined, {NO_BROWSER: "1"} as NodeJS.ProcessEnv)
107+
.map((method) => method.id);
108+
109+
expect(methodIds).not.toContain("chat-gpt");
110+
expect(methodIds).toEqual(expect.arrayContaining(["api-key"]));
111+
});
86112
});

0 commit comments

Comments
 (0)