Skip to content

Commit e29c6d8

Browse files
steipeteLeoGe
andcommitted
fix(acpx): surface Codex ACP diagnostics
Co-authored-by: leoge007 <leoge@users.noreply.github.com>
1 parent 926bf66 commit e29c6d8

10 files changed

Lines changed: 925 additions & 29 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
2121

2222
### Fixes
2323

24+
- ACP/Codex: surface redacted Codex wrapper stderr for generic ACP internal failures and preserve safe Codex model/provider routing in isolated `CODEX_HOME`, making `sessions_spawn(runtime="acp", agentId="codex")` failures actionable. Fixes #80079. (#80718) Thanks @leoge007.
2425
- ACP: treat rejected timeout config options as best-effort hints so ACP turns continue with adapters that do not support `session/set_config_option` timeout keys. Fixes #81250. (#81603) Thanks @qkal.
2526
- Cron/Codex: default exact-command scheduled agent turns to lightweight bootstrap context so automation runs the command before loading workspace identity or memory context.
2627
- Codex plugin/Gateway: strip unpaired UTF-16 surrogates from Codex app-server JSON-RPC payloads and let stale reply-work recovery abort stalled reply runs, preventing malformed media turns from wedging gateway lanes.

docs/tools/acp-agents.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ an unavailable backend.
6060
<Accordion title="First-run gotchas">
6161
- If `plugins.allow` is set, it is a restrictive plugin inventory and **must** include `acpx`; otherwise the installed ACP backend is intentionally blocked and `/acp doctor` reports the missing allowlist entry.
6262
- The Codex ACP adapter is staged with the `acpx` plugin and launched locally when possible.
63-
- Codex ACP runs with an isolated `CODEX_HOME`; OpenClaw copies only trusted project entries from the host Codex config and trusts the active workspace, leaving auth, notifications, and hooks on the host config.
63+
- Codex ACP runs with an isolated `CODEX_HOME`; OpenClaw copies trusted project entries plus safe model/provider routing config from the host Codex config, while auth, notifications, and hooks stay on the host config.
6464
- Other target harness adapters may still be fetched on demand with `npx` the first time you use them.
6565
- Vendor auth still has to exist on the host for that harness.
6666
- If the host has no npm or network access, first-run adapter fetches fail until caches are pre-warmed or the adapter is installed another way.

extensions/acpx/src/codex-auth-bridge.test.ts

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { promisify } from "node:util";
66
import { afterEach, describe, expect, it, vi } from "vitest";
77
import { prepareAcpxCodexAuthConfig } from "./codex-auth-bridge.js";
88
import { resolveAcpxPluginConfig } from "./config.js";
9+
import { OPENCLAW_ACPX_LEASE_ID_ARG, OPENCLAW_GATEWAY_INSTANCE_ID_ARG } from "./process-lease.js";
910

1011
const execFileAsync = promisify(execFile);
1112
const tempDirs: string[] = [];
@@ -241,11 +242,14 @@ describe("prepareAcpxCodexAuthConfig", () => {
241242
expect(wrapper).toContain('killChildTree("SIGTERM")');
242243
expect(wrapper).toContain('killChildTree("SIGKILL", { force: true })');
243244
expect(wrapper).toMatch(
244-
/forceKillTimer = setTimeout\(\(\) => \{\s*killChildTree\("SIGKILL", \{ force: true \}\);\s*process\.exit\(1\);/s,
245+
/forceKillTimer = setTimeout\(\(\) => \{\s*killChildTree\("SIGKILL", \{ force: true \}\);\s*childExitCode = 1;/s,
245246
);
246247
expect(wrapper).toMatch(
247248
/child\.on\("exit", \(code, signal\) => \{\s*if \(parentWatcher\) \{\s*clearInterval\(parentWatcher\);\s*\}\s*if \(orphanCleanupStarted\) \{\s*return;\s*\}/s,
248249
);
250+
expect(wrapper).toMatch(
251+
/child\.on\("close", \(\) => \{\s*finishStderrLog\(\);\s*process\.exit\(childExitCode\);/s,
252+
);
249253
expect(wrapper).not.toMatch(
250254
/forceKillTimer = setTimeout\(\(\) => killChildTree\("SIGKILL"\), 1_500\);\s*forceKillTimer\.unref\?\.\(\);\s*process\.exit\(1\);/s,
251255
);
@@ -357,7 +361,33 @@ describe("prepareAcpxCodexAuthConfig", () => {
357361
);
358362
await fs.writeFile(
359363
path.join(sourceCodexHome, "config.toml"),
360-
'notify = ["SkyComputerUseClient", "turn-ended"]\n',
364+
[
365+
'model = "gpt-5.5-1"',
366+
'model_provider = "azure_foundry"',
367+
'model_reasoning_effort = "high"',
368+
'sandbox_mode = "workspace-write"',
369+
'notify = ["SkyComputerUseClient", "turn-ended"]',
370+
"",
371+
"[model_providers.azure_foundry]",
372+
'name = "Azure Foundry"',
373+
'base_url = "https://example.azure.com/openai/v1"',
374+
'wire_api = "responses"',
375+
'env_key = "AZURE_OPENAI_API_KEY"',
376+
'http_headers = { "api-key" = "inline-secret-key" }',
377+
'query_params = { "api-version" = "2026-01-01", "secret" = "inline-secret-param" }',
378+
'experimental_bearer_token = "inline-secret-bearer"',
379+
"",
380+
"[model_providers.azure_foundry.auth]",
381+
'command = "bash"',
382+
'args = ["-lc", "printf %s test-key"]',
383+
"",
384+
"[model_providers.secret_only]",
385+
'experimental_bearer_token = "secret-only-token"',
386+
"",
387+
`[projects.${JSON.stringify(path.join(root, "project-with-model-key"))}]`,
388+
'model = "nested-project-model"',
389+
"",
390+
].join("\n"),
361391
);
362392
process.env.CODEX_HOME = sourceCodexHome;
363393
process.env.OPENCLAW_AGENT_DIR = agentDir;
@@ -375,6 +405,21 @@ describe("prepareAcpxCodexAuthConfig", () => {
375405

376406
expectCodexWrapperCommand(resolved.agents.codex, generated.wrapperPath);
377407
const isolatedConfig = await fs.readFile(generated.configPath, "utf8");
408+
expect(isolatedConfig).toContain('model = "gpt-5.5-1"');
409+
expect(isolatedConfig).toContain('model_provider = "azure_foundry"');
410+
expect(isolatedConfig).toContain('model_reasoning_effort = "high"');
411+
expect(isolatedConfig).toContain('sandbox_mode = "workspace-write"');
412+
expect(isolatedConfig).toContain("[model_providers.azure_foundry]");
413+
expect(isolatedConfig).toContain('base_url = "https://example.azure.com/openai/v1"');
414+
expect(isolatedConfig).toContain('env_key = "AZURE_OPENAI_API_KEY"');
415+
expect(isolatedConfig).not.toContain("http_headers");
416+
expect(isolatedConfig).not.toContain("query_params");
417+
expect(isolatedConfig).not.toContain("experimental_bearer_token");
418+
expect(isolatedConfig).not.toContain("[model_providers.azure_foundry.auth]");
419+
expect(isolatedConfig).not.toContain("[model_providers.secret_only]");
420+
expect(isolatedConfig).not.toContain("nested-project-model");
421+
expect(isolatedConfig).not.toContain("inline-secret");
422+
expect(isolatedConfig).not.toContain('args = ["-lc", "printf %s test-key"]');
378423
expect(isolatedConfig).not.toContain("notify");
379424
expect(isolatedConfig).not.toContain("SkyComputerUseClient");
380425
expect(isolatedConfig).toContain(`[projects.${JSON.stringify(path.resolve(root))}]`);
@@ -498,6 +543,101 @@ describe("prepareAcpxCodexAuthConfig", () => {
498543
expect(resolved.agents.claude).toContain("bypass");
499544
});
500545

546+
it("captures Codex wrapper stderr in a stream-aware redacted per-lease log", async () => {
547+
const root = await makeTempDir();
548+
const stateDir = path.join(root, "state");
549+
const generated = generatedCodexPaths(stateDir);
550+
const stderrScript = path.join(root, "emit-stderr.mjs");
551+
await fs.writeFile(
552+
stderrScript,
553+
`const chunks = [
554+
"token=sk-test",
555+
"secret1234567890\\n",
556+
"Authorization: Bearer bearer-secret",
557+
"-token-1234567890\\n",
558+
'{"client_secret":"json-secret-1234567890","api_key":"json-api-key-1234567890"}\\n',
559+
"client-secret: kebab-secret-1234567890\\n",
560+
"standalone sk-live-secret",
561+
"1234567890\\n",
562+
"url=https://example.test/callback?token=query-secret",
563+
"-1234567890\\n",
564+
"github_pat_1234567890",
565+
"abcdefghijklmnopqrstuvwxyz\\n",
566+
"-----BEGIN PRIVATE KEY-----\\nprivate-secret-body\\n",
567+
"-----END PRIVATE KEY-----\\n",
568+
"tail-token=tail-secret-1234567890",
569+
"\\n-----BEGIN PRIVATE KEY-----\\ntruncated-private-secret",
570+
];
571+
let index = 0;
572+
function writeNext() {
573+
if (index >= chunks.length) {
574+
process.exit(1);
575+
return;
576+
}
577+
process.stderr.write(chunks[index]);
578+
index += 1;
579+
setTimeout(writeNext, 5);
580+
}
581+
writeNext();`,
582+
"utf8",
583+
);
584+
const pluginConfig = resolveAcpxPluginConfig({
585+
rawConfig: {
586+
agents: {
587+
codex: {
588+
command: `${process.execPath} ${stderrScript}`,
589+
},
590+
},
591+
},
592+
workspaceDir: root,
593+
});
594+
595+
await prepareAcpxCodexAuthConfig({
596+
pluginConfig,
597+
stateDir,
598+
resolveInstalledCodexAcpBinPath: async () => path.join(root, "codex-acp.js"),
599+
});
600+
601+
await expect(
602+
execFileAsync(process.execPath, [
603+
generated.wrapperPath,
604+
"--openclaw-run-configured",
605+
process.execPath,
606+
stderrScript,
607+
OPENCLAW_ACPX_LEASE_ID_ARG,
608+
"lease-secret",
609+
OPENCLAW_GATEWAY_INSTANCE_ID_ARG,
610+
"gateway-test",
611+
]),
612+
).rejects.toMatchObject({ code: 1 });
613+
614+
const log = await fs.readFile(
615+
path.join(stateDir, "acpx", "codex-acp-wrapper.stderr.lease-secret.log"),
616+
"utf8",
617+
);
618+
expect(log).toContain("token=[REDACTED]");
619+
expect(log).toContain("Authorization: Bearer [REDACTED]");
620+
expect(log).toContain('"client_secret":"[REDACTED]"');
621+
expect(log).toContain('"api_key":"[REDACTED]"');
622+
expect(log).toContain("client-secret: [REDACTED]");
623+
expect(log).toContain("standalone [REDACTED_OPENAI_KEY]");
624+
expect(log).toContain("?token=[REDACTED]");
625+
expect(log).toContain("[REDACTED_GITHUB_TOKEN]");
626+
expect(log).toContain("[REDACTED_PRIVATE_KEY]");
627+
expect(log).toContain("tail-token=[REDACTED]");
628+
expect(log).not.toContain("sk-testsecret1234567890");
629+
expect(log).not.toContain("bearer-secret-token-1234567890");
630+
expect(log).not.toContain("json-secret-1234567890");
631+
expect(log).not.toContain("json-api-key-1234567890");
632+
expect(log).not.toContain("kebab-secret-1234567890");
633+
expect(log).not.toContain("query-secret-1234567890");
634+
expect(log).not.toContain("github_pat_1234567890abcdefghijklmnopqrstuvwxyz");
635+
expect(log).not.toContain("private-secret-body");
636+
expect(log).not.toContain("truncated-private-secret");
637+
expect(log).not.toContain("tail-secret-1234567890");
638+
await expectPathMissing(path.join(stateDir, "acpx", "codex-acp-wrapper.stderr.log"));
639+
});
640+
501641
it("leaves a custom Claude agent command alone", async () => {
502642
const root = await makeTempDir();
503643
const stateDir = path.join(root, "state");

0 commit comments

Comments
 (0)