Skip to content
Merged
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
11 changes: 11 additions & 0 deletions src/AcpExtensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export type ExtMethodRequest = AuthenticationStatusRequest | AuthenticationLogoutRequest

export function isExtMethodRequest(request: { method: string, params: Record<string, unknown> }): request is ExtMethodRequest {
return request.method === "authentication/status" || request.method === "authentication/logout";
}

export type AuthenticationStatusRequest = { method: "authentication/status", params: {} }
export type AuthenticationStatusResponse = { type: "api-key" } | { type: "chat-gpt", email: string } | { type: "gateway", name: string } | { type: "unauthenticated" }

export type AuthenticationLogoutRequest = { method: "authentication/logout", params: {} }
export type AuthenticationLogoutResponse = {}
41 changes: 40 additions & 1 deletion src/CodexAcpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type {
UserInput,
} from "./app-server/v2";
import packageJson from "../package.json";
import type {AuthenticationLogoutResponse, AuthenticationStatusResponse} from "./AcpExtensions";

/**
* API for accessing the Codex App Server using ACP requests.
Expand Down Expand Up @@ -113,9 +114,47 @@ export class CodexAcpClient {
return result.success;
}

async logout(): Promise<void> {

async getAuthenticationStatus(): Promise<AuthenticationStatusResponse> {
const modelProvider = await this.getCurrentModelProvider();
if (modelProvider) {
return {
type: "gateway",
name: modelProvider,
};
}
const account = (await this.getAccount()).account;
if (account === null) {
return {
type: "unauthenticated",
};
}
switch (account.type) {
case "apiKey":
return {
type: "api-key",
};
case "chatgpt":
return {
type: "chat-gpt",
email: account.email,
};
}
}

async getCurrentModelProvider(): Promise<string | null> {
const sessionModelProvider = this.getModelProvider();
if (sessionModelProvider !== null) {
return sessionModelProvider;
}
const settingsModelProvider = await this.codexClient.configRead({includeLayers: false});
return settingsModelProvider.config.model_provider;
}

async logout(): Promise<AuthenticationLogoutResponse> {
await this.codexClient.accountLogout();
await this.codexClient.awaitAccountUpdated();
return {};
}

async authRequired(): Promise<Boolean> {
Expand Down
14 changes: 14 additions & 0 deletions src/CodexAcpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {TokenCount} from "./TokenCount";
import {CodexCommands} from "./CodexCommands";
import type {QuotaMeta} from "./QuotaMeta";
import {logger} from "./Logger";
import {isExtMethodRequest} from "./AcpExtensions";

export interface SessionState {
sessionId: string,
Expand Down Expand Up @@ -86,6 +87,19 @@ export class CodexAcpServer implements acp.Agent {
};
}

async extMethod(method: string, params: Record<string, unknown>): Promise<Record<string, unknown>> {
const methodRequest = { method: method, params: params };
if (!isExtMethodRequest(methodRequest)) {
return {};
}
switch (methodRequest.method) {
case "authentication/status":
return await this.runWithProcessCheck(() => this.codexAcpClient.getAuthenticationStatus());
case "authentication/logout":
return await this.runWithProcessCheck(() => this.codexAcpClient.logout());
}
}

async checkAuthorization(){
const authNeeded = await this.runWithProcessCheck(() => this.codexAcpClient.authRequired());
logger.log("Auth requirement checked", {authRequired: authNeeded});
Expand Down
6 changes: 5 additions & 1 deletion src/CodexAppServerClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import type {
SkillsListParams,
SkillsListResponse,
ListMcpServerStatusParams,
ListMcpServerStatusResponse,
ListMcpServerStatusResponse, ConfigReadParams, ConfigReadResponse,
} from "./app-server/v2";

export interface ApprovalHandler {
Expand Down Expand Up @@ -124,6 +124,10 @@ export class CodexAppServerClient {
return await this.sendRequest({ method: "account/logout", params: undefined });
}

async configRead(params: ConfigReadParams): Promise<ConfigReadResponse> {
return await this.sendRequest({ method: "config/read", params: params });
}

async awaitLoginCompleted(): Promise<AccountLoginCompletedNotification> {
return await new Promise((resolve) => {
this.connection.onNotification("account/login/completed", (event: AccountLoginCompletedNotification) => {
Expand Down
17 changes: 16 additions & 1 deletion src/__tests__/CodexACPAgent/CodexAcpClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ describe('ACP server test', { timeout: 40_000 }, () => {

await codexAcpAgent.initialize({protocolVersion: 1});
await fixture.getCodexAcpClient().logout();


const unauthenticatedResponse = await fixture.getCodexAcpAgent().extMethod("authentication/status", {});
expect(unauthenticatedResponse).toEqual({type: "unauthenticated"});

fixture.clearCodexConnectionDump();

const authRequest: CodexAuthRequest = { methodId: "api-key", _meta: { "api-key": { apiKey: "TOKEN" }}}
Expand All @@ -63,6 +68,13 @@ describe('ACP server test', { timeout: 40_000 }, () => {

const transportDump = fixture.getCodexConnectionDump([...ignoredFields, "upgrade"]);
await expect(transportDump).toMatchFileSnapshot("data/auth-with-key.json");

const authenticatedResponse = await fixture.getCodexAcpAgent().extMethod("authentication/status", {});
expect(authenticatedResponse).toEqual({type: "api-key"});

await fixture.getCodexAcpAgent().extMethod("authentication/logout", {});
const logoutResponse = await fixture.getCodexAcpAgent().extMethod("authentication/status", {});
expect(logoutResponse).toEqual({type: "unauthenticated"});
});

it('should authenticate with a gateway', async () => {
Expand All @@ -86,6 +98,9 @@ describe('ACP server test', { timeout: 40_000 }, () => {
await codexAcpAgent.authenticate(authRequest);
expect(await fixture.getCodexAcpClient().authRequired()).toBe(false);

const authenticatedResponse = await fixture.getCodexAcpAgent().extMethod("authentication/status", {});
expect(authenticatedResponse).toEqual({type: "gateway", name: "custom-gateway"});

const newSessionResponse = await codexAcpAgent.newSession({cwd: "", mcpServers: []});
expect(newSessionResponse.sessionId).toBeDefined()
})
Expand Down Expand Up @@ -358,7 +373,7 @@ describe('ACP server test', { timeout: 40_000 }, () => {

const sessionState: SessionState = createTestSessionState();

const logoutSpy = vi.spyOn(mockFixture.getCodexAcpClient(), "logout").mockResolvedValue(undefined);
const logoutSpy = vi.spyOn(mockFixture.getCodexAcpClient(), "logout").mockResolvedValue({});

// @ts-expect-error - exercising private helper
const handled = await codexAcpAgent.availableCommands.handleCommand({ name: "logout", input: null }, sessionState);
Expand Down