Skip to content

Commit 0ba94f5

Browse files
authored
fix: disable Codex's Visualize skill, whose output ACP clients show as raw text (spacering-net#667)
Codex's bundled Visualize skill (`visualize@openai-bundled`, installed by the ChatGPT and Codex desktop apps) ends a reply with a content reference that only the OpenAI apps render. ACP clients showed it as raw `visualize{"path":...}` text. Upstream: openai/codex#40525. - Every new, resumed, loaded, and forked thread gets the config rule `skills.config = [{name: "visualize:visualize", enabled: false}]`. Codex applies session rules after the user's config, and a later rule for the same selector replaces an earlier one, so this rule wins. - The skill commands and `/skills` leave the skill out, because `skills/list` still reports it as enabled. - Matching by name needs no `skills/list` request at session start, and still applies if the plugin is installed mid-session. Limitation: a skill of another plugin with the same qualified name is disabled too. Documented in `readme-dev.md`. - Tests for v1 and v2. Verified with real Codex 0.164.0: without the rule, the model lists `visualize:visualize` among its skills; with it, the skill is gone and the other skills stay.
1 parent e227c0e commit 0ba94f5

12 files changed

Lines changed: 165 additions & 2 deletions

‎readme-dev.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@ Command replies, review results, and terminal/retrying errors retain their exist
115115
failure channels. Clients advertising session compaction support continue to receive the dedicated
116116
compaction lifecycle instead of the legacy completion advisory.
117117

118+
### Unsupported skills
119+
120+
The adapter disables Codex skills whose output ACP clients cannot show, and leaves them out of the skill commands and
121+
`/skills`. The list is in `src/UnsupportedSkills.ts`. Today it holds Codex's bundled Visualize skill: it ends a reply
122+
with a content reference that only the OpenAI apps render, so ACP clients show raw `visualize{...}` text.
123+
124+
The adapter matches skills by name, so it needs no `skills/list` request when a session starts. The cost: a skill from
125+
another plugin with the same qualified name, such as a `visualize` skill in some other plugin named `visualize`, is
126+
disabled too.
118127

119128
### App-server recovery
120129

‎src/CodexAcpClient.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ import {
8383
narrowToListedModels,
8484
} from "./GatewayModelDiscovery";
8585
import {findSupportedEffort} from "./ModelConfigOption";
86+
import {disableUnsupportedSkills, withoutUnsupportedSkills} from "./UnsupportedSkills";
8687
export type {SessionMetadata, SessionMetadataWithThread} from "./SessionMetadata";
8788

8889
/**
@@ -1098,7 +1099,7 @@ export class CodexAcpClient {
10981099
baseUrl: activeProvider.baseUrl,
10991100
});
11001101
const mergedConfig = {
1101-
...forceGitRootTurnDiffPaths(mergeGatewayConfig(this.config, this.gatewayConfig)),
1102+
...disableUnsupportedSkills(forceGitRootTurnDiffPaths(mergeGatewayConfig(this.config, this.gatewayConfig))),
11021103
projects: Object.fromEntries(sessionRoots.map(root => [root, {
11031104
trust_level: "trusted",
11041105
}])),
@@ -1335,7 +1336,7 @@ export class CodexAcpClient {
13351336
}
13361337

13371338
async listSkills(params?: SkillsListParams): Promise<SkillsListResponse> {
1338-
return this.codexClient.listSkills(params ?? {});
1339+
return withoutUnsupportedSkills(await this.codexClient.listSkills(params ?? {}));
13391340
}
13401341

13411342
private async awaitNextLoginCompleted(): Promise<AccountLoginCompletedNotification> {

‎src/UnsupportedSkills.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type {JsonValue} from "./app-server/serde_json/JsonValue";
2+
import type {SkillsListResponse} from "./app-server/v2";
3+
4+
/**
5+
* Skills that ACP clients cannot use.
6+
*
7+
* Codex's bundled Visualize skill ends a reply with a content reference that only the OpenAI apps render,
8+
* so ACP clients show it as raw `visualize{...}` text.
9+
*
10+
* Skills are matched by their plugin-qualified name, so that a session needs no `skills/list` lookup. A skill of
11+
* another plugin with the same name, such as a `visualize` skill in some other `visualize` plugin, is disabled too.
12+
*/
13+
const UNSUPPORTED_SKILL_NAMES: ReadonlySet<string> = new Set(["visualize:visualize"]);
14+
15+
type JsonObject = { [key in string]?: JsonValue };
16+
17+
/** Adds the rules that disable the unsupported skills to a thread config. */
18+
export function disableUnsupportedSkills(config: JsonObject): JsonObject {
19+
const skills = isJsonObject(config["skills"]) ? config["skills"] : {};
20+
const rules = Array.isArray(skills["config"]) ? skills["config"] : [];
21+
return {
22+
...config,
23+
skills: {
24+
...skills,
25+
// Last, because Codex lets a later rule for the same selector replace an earlier one.
26+
config: [...rules, ...Array.from(UNSUPPORTED_SKILL_NAMES, name => ({name, enabled: false}))],
27+
},
28+
};
29+
}
30+
31+
/** Removes the unsupported skills from a `skills/list` response. */
32+
export function withoutUnsupportedSkills(response: SkillsListResponse): SkillsListResponse {
33+
return {
34+
...response,
35+
data: response.data.map(entry => ({
36+
...entry,
37+
skills: entry.skills.filter(skill => !UNSUPPORTED_SKILL_NAMES.has(skill.name)),
38+
})),
39+
};
40+
}
41+
42+
function isJsonObject(value: JsonValue | undefined): value is JsonObject {
43+
return value !== null && typeof value === "object" && !Array.isArray(value);
44+
}

‎src/__tests__/CodexACPAgent/CodexAcpClient.test.ts‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1992,6 +1992,42 @@ describe('ACP server test', { timeout: 40_000 }, () => {
19921992
expect(airSkill?._meta).toEqual({jetbrains: {air: {version: 1, skillPath: "/workspace/.agents/skills/build/SKILL.md"}}});
19931993
});
19941994

1995+
it('does not offer the Visualize skill, whose output ACP clients cannot render', async () => {
1996+
const mockFixture = createCodexMockTestFixture();
1997+
const codexAcpAgent = mockFixture.getCodexAcpAgent();
1998+
const skill = (name: string, pluginId: string | null) => ({
1999+
name,
2000+
description: name,
2001+
path: `/workspace/.agents/skills/${name}/SKILL.md`,
2002+
scope: "user" as const,
2003+
enabled: true,
2004+
pluginId,
2005+
});
2006+
vi.spyOn(mockFixture.getCodexAppServerClient(), "listSkills").mockResolvedValue({
2007+
data: [{
2008+
cwd: "/workspace",
2009+
skills: [skill("visualize:visualize", "visualize@openai-bundled"), skill("build", null)],
2010+
errors: [],
2011+
}],
2012+
});
2013+
const sessionState = createTestSessionState({sessionId: "session-id", cwd: "/workspace"});
2014+
vi.spyOn(codexAcpAgent, "getSessionState").mockReturnValue(sessionState);
2015+
2016+
// @ts-expect-error - exercising private helper
2017+
await codexAcpAgent.availableCommands.publish(sessionState);
2018+
await codexAcpAgent.prompt({sessionId: "session-id", prompt: [{type: "text", text: "/skills"}]});
2019+
2020+
const events = mockFixture.getAcpConnectionEvents([])
2021+
.filter(event => event.method === "sessionUpdate")
2022+
.map(event => event.args[0].update);
2023+
const commands = events.find(update => update.sessionUpdate === "available_commands_update")
2024+
?.availableCommands.map((command: acp.AvailableCommand) => command.name);
2025+
expect(commands).toContain("$build");
2026+
expect(commands).not.toContain("$visualize:visualize");
2027+
const skillsList = events.find(update => update.sessionUpdate === "agent_message_chunk")?.content.text;
2028+
expect(skillsList).toBe("Available skills:\n- build: build");
2029+
});
2030+
19952031
it('publishes the commands again after a turn only when the skills changed', async () => {
19962032
const mockFixture = createCodexMockTestFixture();
19972033
const codexAcpAgent = mockFixture.getCodexAcpAgent();

‎src/__tests__/CodexACPAgent/data/auth-v2-login-then-new-session.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@
1919
"features": {
2020
"cwd_relative_turn_diffs": false
2121
},
22+
"skills": {
23+
"config": [
24+
{
25+
"name": "visualize:visualize",
26+
"enabled": false
27+
}
28+
]
29+
},
2230
"projects": {
2331
"/workspace": {
2432
"trust_level": "trusted"

‎src/__tests__/CodexACPAgent/data/session-lifecycle-v2-fork.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,14 @@
167167
"features": {
168168
"cwd_relative_turn_diffs": false
169169
},
170+
"skills": {
171+
"config": [
172+
{
173+
"name": "visualize:visualize",
174+
"enabled": false
175+
}
176+
]
177+
},
170178
"projects": {
171179
"/workspace": {
172180
"trust_level": "trusted"

‎src/__tests__/CodexACPAgent/data/session-lifecycle-v2-new-mcp-servers.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66
"features": {
77
"cwd_relative_turn_diffs": false
88
},
9+
"skills": {
10+
"config": [
11+
{
12+
"name": "visualize:visualize",
13+
"enabled": false
14+
}
15+
]
16+
},
917
"projects": {
1018
"/workspace": {
1119
"trust_level": "trusted"

‎src/__tests__/CodexACPAgent/data/session-lifecycle-v2-new.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,14 @@
166166
"features": {
167167
"cwd_relative_turn_diffs": false
168168
},
169+
"skills": {
170+
"config": [
171+
{
172+
"name": "visualize:visualize",
173+
"enabled": false
174+
}
175+
]
176+
},
169177
"projects": {
170178
"/workspace": {
171179
"trust_level": "trusted"

‎src/__tests__/CodexACPAgent/data/session-lifecycle-v2-resume-null-replay.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,14 @@
166166
"features": {
167167
"cwd_relative_turn_diffs": false
168168
},
169+
"skills": {
170+
"config": [
171+
{
172+
"name": "visualize:visualize",
173+
"enabled": false
174+
}
175+
]
176+
},
169177
"projects": {
170178
"/workspace": {
171179
"trust_level": "trusted"

‎src/__tests__/CodexACPAgent/data/session-lifecycle-v2-resume-start-replay.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,14 @@
166166
"features": {
167167
"cwd_relative_turn_diffs": false
168168
},
169+
"skills": {
170+
"config": [
171+
{
172+
"name": "visualize:visualize",
173+
"enabled": false
174+
}
175+
]
176+
},
169177
"projects": {
170178
"/workspace": {
171179
"trust_level": "trusted"

0 commit comments

Comments
 (0)