Skip to content

Commit ea97470

Browse files
add acp extension methods to logout and read authentication status (#40)
1 parent 1c1ad2e commit ea97470

5 files changed

Lines changed: 86 additions & 3 deletions

File tree

src/AcpExtensions.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export type ExtMethodRequest = AuthenticationStatusRequest | AuthenticationLogoutRequest
2+
3+
export function isExtMethodRequest(request: { method: string, params: Record<string, unknown> }): request is ExtMethodRequest {
4+
return request.method === "authentication/status" || request.method === "authentication/logout";
5+
}
6+
7+
export type AuthenticationStatusRequest = { method: "authentication/status", params: {} }
8+
export type AuthenticationStatusResponse = { type: "api-key" } | { type: "chat-gpt", email: string } | { type: "gateway", name: string } | { type: "unauthenticated" }
9+
10+
export type AuthenticationLogoutRequest = { method: "authentication/logout", params: {} }
11+
export type AuthenticationLogoutResponse = {}

src/CodexAcpClient.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type {
2525
UserInput,
2626
} from "./app-server/v2";
2727
import packageJson from "../package.json";
28+
import type {AuthenticationLogoutResponse, AuthenticationStatusResponse} from "./AcpExtensions";
2829

2930
/**
3031
* API for accessing the Codex App Server using ACP requests.
@@ -113,9 +114,47 @@ export class CodexAcpClient {
113114
return result.success;
114115
}
115116

116-
async logout(): Promise<void> {
117+
118+
async getAuthenticationStatus(): Promise<AuthenticationStatusResponse> {
119+
const modelProvider = await this.getCurrentModelProvider();
120+
if (modelProvider) {
121+
return {
122+
type: "gateway",
123+
name: modelProvider,
124+
};
125+
}
126+
const account = (await this.getAccount()).account;
127+
if (account === null) {
128+
return {
129+
type: "unauthenticated",
130+
};
131+
}
132+
switch (account.type) {
133+
case "apiKey":
134+
return {
135+
type: "api-key",
136+
};
137+
case "chatgpt":
138+
return {
139+
type: "chat-gpt",
140+
email: account.email,
141+
};
142+
}
143+
}
144+
145+
async getCurrentModelProvider(): Promise<string | null> {
146+
const sessionModelProvider = this.getModelProvider();
147+
if (sessionModelProvider !== null) {
148+
return sessionModelProvider;
149+
}
150+
const settingsModelProvider = await this.codexClient.configRead({includeLayers: false});
151+
return settingsModelProvider.config.model_provider;
152+
}
153+
154+
async logout(): Promise<AuthenticationLogoutResponse> {
117155
await this.codexClient.accountLogout();
118156
await this.codexClient.awaitAccountUpdated();
157+
return {};
119158
}
120159

121160
async authRequired(): Promise<Boolean> {

src/CodexAcpServer.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {TokenCount} from "./TokenCount";
1818
import {CodexCommands} from "./CodexCommands";
1919
import type {QuotaMeta} from "./QuotaMeta";
2020
import {logger} from "./Logger";
21+
import {isExtMethodRequest} from "./AcpExtensions";
2122

2223
export interface SessionState {
2324
sessionId: string,
@@ -86,6 +87,19 @@ export class CodexAcpServer implements acp.Agent {
8687
};
8788
}
8889

90+
async extMethod(method: string, params: Record<string, unknown>): Promise<Record<string, unknown>> {
91+
const methodRequest = { method: method, params: params };
92+
if (!isExtMethodRequest(methodRequest)) {
93+
return {};
94+
}
95+
switch (methodRequest.method) {
96+
case "authentication/status":
97+
return await this.runWithProcessCheck(() => this.codexAcpClient.getAuthenticationStatus());
98+
case "authentication/logout":
99+
return await this.runWithProcessCheck(() => this.codexAcpClient.logout());
100+
}
101+
}
102+
89103
async checkAuthorization(){
90104
const authNeeded = await this.runWithProcessCheck(() => this.codexAcpClient.authRequired());
91105
logger.log("Auth requirement checked", {authRequired: authNeeded});

src/CodexAppServerClient.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import type {
2626
SkillsListParams,
2727
SkillsListResponse,
2828
ListMcpServerStatusParams,
29-
ListMcpServerStatusResponse,
29+
ListMcpServerStatusResponse, ConfigReadParams, ConfigReadResponse,
3030
} from "./app-server/v2";
3131

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

127+
async configRead(params: ConfigReadParams): Promise<ConfigReadResponse> {
128+
return await this.sendRequest({ method: "config/read", params: params });
129+
}
130+
127131
async awaitLoginCompleted(): Promise<AccountLoginCompletedNotification> {
128132
return await new Promise((resolve) => {
129133
this.connection.onNotification("account/login/completed", (event: AccountLoginCompletedNotification) => {

src/__tests__/CodexACPAgent/CodexAcpClient.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ describe('ACP server test', { timeout: 40_000 }, () => {
5454

5555
await codexAcpAgent.initialize({protocolVersion: 1});
5656
await fixture.getCodexAcpClient().logout();
57+
58+
59+
const unauthenticatedResponse = await fixture.getCodexAcpAgent().extMethod("authentication/status", {});
60+
expect(unauthenticatedResponse).toEqual({type: "unauthenticated"});
61+
5762
fixture.clearCodexConnectionDump();
5863

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

6469
const transportDump = fixture.getCodexConnectionDump([...ignoredFields, "upgrade"]);
6570
await expect(transportDump).toMatchFileSnapshot("data/auth-with-key.json");
71+
72+
const authenticatedResponse = await fixture.getCodexAcpAgent().extMethod("authentication/status", {});
73+
expect(authenticatedResponse).toEqual({type: "api-key"});
74+
75+
await fixture.getCodexAcpAgent().extMethod("authentication/logout", {});
76+
const logoutResponse = await fixture.getCodexAcpAgent().extMethod("authentication/status", {});
77+
expect(logoutResponse).toEqual({type: "unauthenticated"});
6678
});
6779

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

101+
const authenticatedResponse = await fixture.getCodexAcpAgent().extMethod("authentication/status", {});
102+
expect(authenticatedResponse).toEqual({type: "gateway", name: "custom-gateway"});
103+
89104
const newSessionResponse = await codexAcpAgent.newSession({cwd: "", mcpServers: []});
90105
expect(newSessionResponse.sessionId).toBeDefined()
91106
})
@@ -358,7 +373,7 @@ describe('ACP server test', { timeout: 40_000 }, () => {
358373

359374
const sessionState: SessionState = createTestSessionState();
360375

361-
const logoutSpy = vi.spyOn(mockFixture.getCodexAcpClient(), "logout").mockResolvedValue(undefined);
376+
const logoutSpy = vi.spyOn(mockFixture.getCodexAcpClient(), "logout").mockResolvedValue({});
362377

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

0 commit comments

Comments
 (0)