Skip to content

Commit 8e41c9c

Browse files
authored
Merge branch 'agentclientprotocol:main' into chapeta/history-attachments
2 parents 081f254 + ca66e03 commit 8e41c9c

9 files changed

Lines changed: 109 additions & 45 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
contents: read
1616
steps:
1717
- uses: actions/checkout@v7
18-
- uses: actions/setup-node@v6
18+
- uses: actions/setup-node@v7
1919
with:
2020
node-version: "24"
2121
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6

.github/workflows/e2e.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
contents: read
1515
steps:
1616
- uses: actions/checkout@v7
17-
- uses: actions/setup-node@v6
17+
- uses: actions/setup-node@v7
1818
with:
1919
node-version: '24'
2020
- name: Configure sandboxing

.github/workflows/publish.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
contents: read
1616
steps:
1717
- uses: actions/checkout@v7
18-
- uses: actions/setup-node@v6
18+
- uses: actions/setup-node@v7
1919
with:
2020
node-version: '24'
2121
- name: Configure sandboxing
@@ -43,7 +43,7 @@ jobs:
4343
id-token: write
4444
steps:
4545
- uses: actions/checkout@v7
46-
- uses: actions/setup-node@v6
46+
- uses: actions/setup-node@v7
4747
with:
4848
node-version: '24'
4949
registry-url: 'https://registry.npmjs.org'
@@ -64,7 +64,7 @@ jobs:
6464
private-key: ${{ secrets.RELEASE_PLZ_APP_PRIVATE_KEY }}
6565

6666
- name: Create Release
67-
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
67+
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
6868
with:
6969
token: ${{ steps.generate-token.outputs.token }}
7070
generate_release_notes: true

package-lock.json

Lines changed: 30 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"publishConfig": {
44
"access": "public"
55
},
6-
"version": "1.1.4",
6+
"version": "1.1.5",
77
"description": "",
88
"main": "dist/index.js",
99
"bin": {
@@ -62,7 +62,7 @@
6262
},
6363
"dependencies": {
6464
"@agentclientprotocol/sdk": "^1.2.1",
65-
"@openai/codex": "^0.144.4",
65+
"@openai/codex": "^0.144.6",
6666
"diff": "^9.0.0",
6767
"open": "^11.0.0",
6868
"vscode-jsonrpc": "^9.0.1",

src/CodexAcpClient.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export class CodexAcpClient {
7272
private pendingAccountUpdated: Promise<AccountUpdatedNotification> | null = null;
7373
private readonly sessionNotificationQueues = new Map<string, Promise<void>>();
7474
private skillExtraRoots: string[] = [];
75+
private configPath: string | null = null;
7576

7677

7778
constructor(codexClient: CodexAppServerClient, codexConfig?: JsonObject, modelProvider?: string) {
@@ -86,7 +87,7 @@ export class CodexAcpClient {
8687
};
8788

8889
async initialize(request: acp.InitializeRequest): Promise<void> {
89-
await this.codexClient.initialize({
90+
const response = await this.codexClient.initialize({
9091
capabilities: {
9192
experimentalApi: true,
9293
requestAttestation: false,
@@ -97,6 +98,11 @@ export class CodexAcpClient {
9798
title: request.clientInfo?.title ?? this.defaultClientInfo.title,
9899
}
99100
});
101+
this.configPath = response?.codexHome ?? null;
102+
}
103+
104+
getHomePath(): string | null {
105+
return this.configPath;
100106
}
101107

102108
async authenticate(authRequest: acp.AuthenticateRequest): Promise<Boolean> {
@@ -518,11 +524,16 @@ export class CodexAcpClient {
518524

519525
private async getConfigMcpServerNames(projectPath: string): Promise<Set<string>> {
520526
const response = await this.codexClient.configRead({ includeLayers: true, cwd: projectPath });
521-
const mcpServers = response?.config?.["mcp_servers"];
522-
if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
527+
const effectiveMcpServers = response?.config?.["mcp_servers"];
528+
const configLayers = response?.layers ?? [];
529+
const layerMcpServers = configLayers.map(layer => {
530+
return isJsonObject(layer.config) ? layer.config["mcp_servers"] : undefined;
531+
});
532+
const configuredMcpServers = [effectiveMcpServers, ...layerMcpServers].filter(isJsonObject);
533+
if (configuredMcpServers.length === 0) {
523534
return new Set();
524535
}
525-
return new Set(Object.keys(mcpServers));
536+
return new Set(configuredMcpServers.flatMap(server => Object.keys(server)));
526537
}
527538

528539
getModelProvider(): string | null {

src/CodexAcpServer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,10 @@ export class CodexAcpServer {
313313
await this.refreshSessionsAuthState(null);
314314
throw RequestError.internalError(`${(e.message)}\n\nYou have been logged out. Please try again.`);
315315
}
316+
const configPath = this.codexAcpClient.getHomePath() ?? "global";
317+
if (e.message.includes("load config")) {
318+
throw RequestError.internalError(`${e.message}\n\nCheck ${configPath} and project .codex directories, especially their config.toml files, or any CODEX_CONFIG override.`);
319+
}
316320
}
317321

318322
private beginSessionOpen(sessionId: string): number {

src/__tests__/CodexACPAgent/mcp-config-merge.test.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,30 @@ import type {McpServerStdio} from "@agentclientprotocol/sdk";
88
import {startCodexConnection} from "../../CodexJsonRpcConnection";
99
import {createBaseTestFixture, removeDirectoryWithRetry, type TestFixture} from "../acp-test-utils";
1010

11-
describe('MCP config merge across global config and ACP request', { timeout: 40_000 }, () => {
11+
describe('MCP config merge across configured MCP servers and ACP request', { timeout: 40_000 }, () => {
1212

1313
let codexHome: string;
14+
let projectPath: string;
1415
let fixture: TestFixture;
1516

1617
beforeEach(() => {
1718
vi.clearAllMocks();
1819

19-
const configToml = `
20+
const globalConfig = `
2021
[mcp_servers.shared-mcp]
2122
url = "https://example.com/mcp"
2223
`;
24+
25+
const projectConfig = `
26+
[mcp_servers.project-mcp]
27+
url = "https://example.com/mcp"
28+
`;
29+
2330
codexHome = fs.mkdtempSync(path.join(os.tmpdir(), "codex-acp-mcp-merge-"));
24-
fs.writeFileSync(path.join(codexHome, "config.toml"), configToml, "utf8");
31+
fs.writeFileSync(path.join(codexHome, "config.toml"), globalConfig, "utf8");
32+
projectPath = fs.mkdtempSync(path.join(os.tmpdir(), "codex-acp-mcp-project-"));
33+
fs.mkdirSync(path.join(projectPath, ".codex"));
34+
fs.writeFileSync(path.join(projectPath, ".codex", "config.toml"), projectConfig, "utf8");
2535

2636
const codexConnection = startCodexConnection(undefined, {
2737
...process.env,
@@ -37,6 +47,7 @@ url = "https://example.com/mcp"
3747
afterEach(() => {
3848
vi.unstubAllEnvs();
3949
removeDirectoryWithRetry(codexHome);
50+
removeDirectoryWithRetry(projectPath);
4051
});
4152

4253
it('should preserve the global url-based MCP when ACP passes a command-type MCP with the same name', async () => {
@@ -68,6 +79,24 @@ url = "https://example.com/mcp"
6879
expect(transportDump).contain("- shared-mcp");
6980
});
7081

82+
it('should preserve a project url-based MCP when ACP passes a command-type MCP with the same name', async () => {
83+
const codexAcpAgent = fixture.getCodexAcpAgent();
84+
await codexAcpAgent.initialize({protocolVersion: 1});
85+
fixture.getCodexAcpClient().authRequired = vi.fn().mockResolvedValue(false);
86+
87+
const conflictingMcp = {
88+
name: "project-mcp",
89+
command: "./node_modules/.bin/mcp-hello-world",
90+
args: ["example"],
91+
env: [{name: "example", value: "example"}],
92+
};
93+
94+
await expect(codexAcpAgent.newSession({
95+
cwd: projectPath,
96+
mcpServers: [conflictingMcp],
97+
})).resolves.toBeDefined();
98+
});
99+
71100
it('should not filter the conflicting ACP MCP when config filtering is disabled', async () => {
72101
vi.stubEnv("DISABLE_MCP_CONFIG_FILTERING", "true");
73102
const codexAcpAgent = fixture.getCodexAcpAgent();
@@ -85,6 +114,8 @@ url = "https://example.com/mcp"
85114
await expect(codexAcpAgent.newSession({
86115
cwd: "",
87116
mcpServers: [conflictingMcp],
88-
})).rejects.toThrow("url is not supported for stdio");
117+
})).rejects.toMatchObject({
118+
data: expect.stringContaining("url is not supported for stdio"),
119+
});
89120
});
90121
});

src/__tests__/CodexACPAgent/new-session-logout.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,24 @@ describe("New session logout handling", () => {
4141
expect(logoutSpy).toHaveBeenCalledOnce();
4242
});
4343

44+
it("includes the global config path in reload configuration errors", async () => {
45+
const fixture = createCodexMockTestFixture();
46+
const codexAcpAgent = fixture.getCodexAcpAgent();
47+
const codexAcpClient = fixture.getCodexAcpClient();
48+
const codexAppServerClient = fixture.getCodexAppServerClient();
49+
vi.spyOn(codexAcpClient, "authRequired").mockResolvedValue(false);
50+
const logoutSpy = vi.spyOn(codexAcpClient, "logout").mockResolvedValue();
51+
52+
const errorMessage = 'Internal error: "failed to reload config: filesystem path `/tmp` must be absolute, use `~/...`, or start with `:`"';
53+
vi.spyOn(codexAppServerClient, "threadStart").mockRejectedValue(new Error(errorMessage));
54+
55+
expect(logoutSpy).toHaveBeenCalledTimes(0);
56+
await expect(codexAcpAgent.newSession({cwd: "", mcpServers: []}))
57+
.rejects.toMatchObject({
58+
data: expect.stringContaining(`Check global and project .codex directories`),
59+
});
60+
});
61+
4462
it("refreshes OpenAI sessions when newSession error forces logout", async () => {
4563
const fixture = createCodexMockTestFixture();
4664
const codexAcpAgent = fixture.getCodexAcpAgent();

0 commit comments

Comments
 (0)