diff --git a/CHANGELOG.md b/CHANGELOG.md index 066a926591b..d1ddeec8180 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai - CLI/model probes: reject empty or whitespace-only `infer model run --prompt` values before calling local providers or the Gateway, so smoke checks do not spend provider calls on invalid turns. Fixes #73185. Thanks @iot2edge. - Gateway/media: route text-only `chat.send` image offloads through media-understanding fields so `agents.defaults.imageModel` can describe WebChat attachments instead of leaving only an opaque `media://inbound` marker. Fixes #72968. Thanks @vorajeeah. - Channels/Telegram: fail fast when Telegram rejects the startup `getMe` token probe with 401, so invalid or stale BotFather tokens are reported as token auth failures instead of misleading `deleteWebhook` cleanup failures. Fixes #47674. Thanks @samaedan-arch. +- ACPX: keep generated Codex and Claude ACP wrapper startup paths working when remote or special state filesystems reject chmod, since OpenClaw invokes the wrappers through Node instead of executing them directly. Fixes #73333. Thanks @david-garcia-garcia. - CLI/onboarding: infer image input for common custom-provider vision model IDs, ask only for unknown models, and keep `--custom-image-input`/`--custom-text-input` overrides so vision-capable proxies do not get saved as text-only configs. Fixes #51869. Thanks @Antsoldier1974. - Models/OpenAI Codex: stop listing or resolving unsupported `openai-codex/gpt-5.4-mini` rows through Codex OAuth, keep stale discovery rows suppressed with a clear API-key-route hint, and leave direct `openai/gpt-5.4-mini` available. Fixes #73242. Thanks @0xCyda. - Plugin SDK: restore the root-alias bridge for `registerContextEngine` and expose missing legacy compat helpers `normalizeAccountId` and `resolvePreferredOpenClawTmpDir` so older external plugins such as `openclaw-weixin` can keep loading while migrating to focused SDK subpaths. Fixes #53497. Thanks @alanxchen85. diff --git a/extensions/acpx/src/codex-auth-bridge.test.ts b/extensions/acpx/src/codex-auth-bridge.test.ts index 1683d1bc3d0..617b6c2b16b 100644 --- a/extensions/acpx/src/codex-auth-bridge.test.ts +++ b/extensions/acpx/src/codex-auth-bridge.test.ts @@ -3,7 +3,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { promisify } from "node:util"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { prepareAcpxCodexAuthConfig } from "./codex-auth-bridge.js"; import { resolveAcpxPluginConfig } from "./config.js"; @@ -66,6 +66,7 @@ function expectClaudeWrapperCommand(command: string | undefined, wrapperPath: st } afterEach(async () => { + vi.restoreAllMocks(); restoreEnv("CODEX_HOME"); restoreEnv("OPENCLAW_AGENT_DIR"); restoreEnv("PI_CODING_AGENT_DIR"); @@ -114,6 +115,31 @@ describe("prepareAcpxCodexAuthConfig", () => { ).rejects.toMatchObject({ code: "ENOENT" }); }); + it("keeps generated wrappers usable when chmod is rejected by the state filesystem", async () => { + const root = await makeTempDir(); + const stateDir = path.join(root, "state"); + const generatedCodex = generatedCodexPaths(stateDir); + const generatedClaude = generatedClaudePaths(stateDir); + const chmodError = Object.assign(new Error("operation not permitted"), { code: "EPERM" }); + const chmodSpy = vi.spyOn(fs, "chmod").mockRejectedValue(chmodError); + const pluginConfig = resolveAcpxPluginConfig({ + rawConfig: {}, + workspaceDir: root, + }); + + const resolved = await prepareAcpxCodexAuthConfig({ + pluginConfig, + stateDir, + }); + + expect(chmodSpy).toHaveBeenCalledWith(generatedCodex.wrapperPath, 0o755); + expect(chmodSpy).toHaveBeenCalledWith(generatedClaude.wrapperPath, 0o755); + expectCodexWrapperCommand(resolved.agents.codex, generatedCodex.wrapperPath); + expectClaudeWrapperCommand(resolved.agents.claude, generatedClaude.wrapperPath); + await expect(fs.access(generatedCodex.wrapperPath)).resolves.toBeUndefined(); + await expect(fs.access(generatedClaude.wrapperPath)).resolves.toBeUndefined(); + }); + it("falls back to the current Codex ACP package range when the local adapter is unavailable", async () => { const root = await makeTempDir(); const stateDir = path.join(root, "state"); diff --git a/extensions/acpx/src/codex-auth-bridge.ts b/extensions/acpx/src/codex-auth-bridge.ts index 762ed5b45f6..3e710bfc0f9 100644 --- a/extensions/acpx/src/codex-auth-bridge.ts +++ b/extensions/acpx/src/codex-auth-bridge.ts @@ -240,13 +240,21 @@ async function prepareIsolatedCodexHome(baseDir: string): Promise { return codexHome; } +async function makeGeneratedWrapperExecutableIfPossible(wrapperPath: string): Promise { + try { + await fs.chmod(wrapperPath, 0o755); + } catch { + // The wrapper is invoked via `node wrapper.mjs`; executable mode is only a convenience. + } +} + async function writeCodexAcpWrapper(baseDir: string, installedBinPath?: string): Promise { await fs.mkdir(baseDir, { recursive: true }); const wrapperPath = path.join(baseDir, "codex-acp-wrapper.mjs"); await fs.writeFile(wrapperPath, buildCodexAcpWrapperScript(installedBinPath), { encoding: "utf8", }); - await fs.chmod(wrapperPath, 0o755); + await makeGeneratedWrapperExecutableIfPossible(wrapperPath); return wrapperPath; } @@ -256,7 +264,7 @@ async function writeClaudeAcpWrapper(baseDir: string, installedBinPath?: string) await fs.writeFile(wrapperPath, buildClaudeAcpWrapperScript(installedBinPath), { encoding: "utf8", }); - await fs.chmod(wrapperPath, 0o755); + await makeGeneratedWrapperExecutableIfPossible(wrapperPath); return wrapperPath; }