fix(agents): preserve claude cli backend defaults

This commit is contained in:
Peter Steinberger
2026-04-05 09:46:46 +01:00
parent 03be4c2489
commit 3d952aa35d
6 changed files with 155 additions and 4 deletions

View File

@@ -1,3 +1,4 @@
export { buildAnthropicCliBackend } from "./cli-backend.js";
export {
CLAUDE_CLI_BACKEND_ID,
isClaudeCliProvider,

View File

@@ -352,6 +352,12 @@ describe("resolveCliBackendConfig claude-cli defaults", () => {
"--permission-mode",
"bypassPermissions",
]);
expect(resolved?.config.systemPromptArg).toBe("--append-system-prompt");
expect(resolved?.config.systemPromptWhen).toBe("first");
expect(resolved?.config.sessionArg).toBe("--session-id");
expect(resolved?.config.sessionMode).toBe("always");
expect(resolved?.config.input).toBe("stdin");
expect(resolved?.config.output).toBe("jsonl");
});
});

View File

@@ -1,5 +1,6 @@
import {
CLAUDE_CLI_BACKEND_ID,
buildAnthropicCliBackend,
normalizeClaudeBackendConfig,
} from "../../extensions/anthropic/cli-backend-api.js";
import type { OpenClawConfig } from "../config/config.js";
@@ -18,6 +19,7 @@ export { normalizeClaudeBackendConfig };
type FallbackCliBackendPolicy = {
bundleMcp: boolean;
baseConfig?: CliBackendConfig;
normalizeConfig?: (config: CliBackendConfig) => CliBackendConfig;
};
@@ -27,6 +29,7 @@ const FALLBACK_CLI_BACKEND_POLICIES: Record<string, FallbackCliBackendPolicy> =
// plugin registry is not initialized yet (for example direct runner tests
// or narrow non-gateway entrypoints).
bundleMcp: true,
baseConfig: buildAnthropicCliBackend().config,
normalizeConfig: normalizeClaudeBackendConfig,
},
};
@@ -134,11 +137,28 @@ export function resolveCliBackendConfig(
}
if (!override) {
return null;
if (!fallbackPolicy?.baseConfig) {
return null;
}
const baseConfig = fallbackPolicy.normalizeConfig
? fallbackPolicy.normalizeConfig(fallbackPolicy.baseConfig)
: fallbackPolicy.baseConfig;
const command = baseConfig.command?.trim();
if (!command) {
return null;
}
return {
id: normalized,
config: { ...baseConfig, command },
bundleMcp: fallbackPolicy.bundleMcp,
};
}
const config = fallbackPolicy?.normalizeConfig
? fallbackPolicy.normalizeConfig(override)
const mergedFallback = fallbackPolicy?.baseConfig
? mergeBackendConfig(fallbackPolicy.baseConfig, override)
: override;
const config = fallbackPolicy?.normalizeConfig
? fallbackPolicy.normalizeConfig(mergedFallback)
: mergedFallback;
const command = config.command?.trim();
if (!command) {
return null;

View File

@@ -1,21 +1,29 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { beforeEach, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { onAgentEvent, resetAgentEventsForTest } from "../infra/agent-events.js";
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
import {
makeBootstrapWarn as realMakeBootstrapWarn,
resolveBootstrapContextForRun as realResolveBootstrapContextForRun,
} from "./bootstrap-files.js";
import {
createManagedRun,
mockSuccessfulCliRun,
restoreCliRunnerPrepareTestDeps,
runCliAgentWithBackendConfig,
setupCliRunnerTestModule,
SMALL_PNG_BASE64,
stubBootstrapContext,
supervisorSpawnMock,
} from "./cli-runner.test-support.js";
import { setCliRunnerPrepareTestDeps } from "./cli-runner/prepare.js";
beforeEach(() => {
resetAgentEventsForTest();
restoreCliRunnerPrepareTestDeps();
});
describe("runCliAgent spawn path", () => {
@@ -308,6 +316,28 @@ describe("runCliAgent spawn path", () => {
expect(input.env?.SAFE_CLEAR).toBeUndefined();
});
it("keeps explicit backend env overrides even when clearEnv drops inherited values", async () => {
const runCliAgent = await setupCliRunnerTestModule();
process.env.SAFE_OVERRIDE = "from-base";
mockSuccessfulCliRun();
await runCliAgentWithBackendConfig({
runCliAgent,
backend: {
command: "codex",
env: {
SAFE_OVERRIDE: "from-override",
},
clearEnv: ["SAFE_OVERRIDE"],
},
runId: "run-clear-env-override",
});
const input = supervisorSpawnMock.mock.calls[0]?.[0] as {
env?: Record<string, string | undefined>;
};
expect(input.env?.SAFE_OVERRIDE).toBe("from-override");
});
it("prepends bootstrap warnings to the CLI prompt body", async () => {
const runCliAgent = await setupCliRunnerTestModule();
supervisorSpawnMock.mockResolvedValueOnce(
@@ -365,6 +395,84 @@ describe("runCliAgent spawn path", () => {
expect(promptCarrier).toContain("hi");
});
it("loads workspace bootstrap files into the Claude CLI system prompt", async () => {
const runCliAgent = await setupCliRunnerTestModule();
const workspaceDir = await fs.mkdtemp(
path.join(os.tmpdir(), "openclaw-cli-bootstrap-context-"),
);
supervisorSpawnMock.mockResolvedValueOnce(
createManagedRun({
reason: "exit",
exitCode: 0,
exitSignal: null,
durationMs: 50,
stdout: "ok",
stderr: "",
timedOut: false,
noOutputTimedOut: false,
}),
);
await fs.writeFile(
path.join(workspaceDir, "AGENTS.md"),
[
"# AGENTS.md",
"",
"Read SOUL.md and IDENTITY.md before replying.",
"Use the injected workspace bootstrap files as standing instructions.",
].join("\n"),
"utf-8",
);
await fs.writeFile(path.join(workspaceDir, "SOUL.md"), "SOUL-SECRET\n", "utf-8");
await fs.writeFile(path.join(workspaceDir, "IDENTITY.md"), "IDENTITY-SECRET\n", "utf-8");
await fs.writeFile(path.join(workspaceDir, "USER.md"), "USER-SECRET\n", "utf-8");
setCliRunnerPrepareTestDeps({
makeBootstrapWarn: realMakeBootstrapWarn,
resolveBootstrapContextForRun: realResolveBootstrapContextForRun,
});
try {
await runCliAgent({
sessionId: "s1",
sessionFile: "/tmp/session.jsonl",
workspaceDir,
prompt: "BOOTSTRAP_CAPTURE_CHECK",
provider: "claude-cli",
model: "sonnet",
timeoutMs: 1_000,
runId: "run-bootstrap-context",
});
const input = supervisorSpawnMock.mock.calls[0]?.[0] as {
argv?: string[];
input?: string;
};
const allArgs = (input.argv ?? []).join("\n");
const agentsPath = path.join(workspaceDir, "AGENTS.md");
const soulPath = path.join(workspaceDir, "SOUL.md");
const identityPath = path.join(workspaceDir, "IDENTITY.md");
const userPath = path.join(workspaceDir, "USER.md");
expect(input.input).toContain("BOOTSTRAP_CAPTURE_CHECK");
expect(allArgs).toContain("--append-system-prompt");
expect(allArgs).toContain("# Project Context");
expect(allArgs).toContain(`## ${agentsPath}`);
expect(allArgs).toContain("Read SOUL.md and IDENTITY.md before replying.");
expect(allArgs).toContain(`## ${soulPath}`);
expect(allArgs).toContain("SOUL-SECRET");
expect(allArgs).toContain(
"If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.",
);
expect(allArgs).toContain(`## ${identityPath}`);
expect(allArgs).toContain("IDENTITY-SECRET");
expect(allArgs).toContain(`## ${userPath}`);
expect(allArgs).toContain("USER-SECRET");
} finally {
await fs.rm(workspaceDir, { recursive: true, force: true });
restoreCliRunnerPrepareTestDeps();
}
});
it("hydrates prompt media refs into CLI image args", async () => {
const runCliAgent = await setupCliRunnerTestModule();
supervisorSpawnMock.mockResolvedValueOnce(

View File

@@ -191,6 +191,13 @@ export function stubBootstrapContext(params: {
hoisted.resolveBootstrapContextForRunMock.mockResolvedValueOnce(params);
}
export function restoreCliRunnerPrepareTestDeps() {
setCliRunnerPrepareTestDeps({
makeBootstrapWarn: () => () => {},
resolveBootstrapContextForRun: hoisted.resolveBootstrapContextForRunMock,
});
}
export async function runCliAgentWithBackendConfig(params: {
runCliAgent: typeof import("./cli-runner.js").runCliAgent;
backend: TestCliBackendConfig;

View File

@@ -171,12 +171,21 @@ export async function executePreparedCliRun(
const env = (() => {
const next = sanitizeHostExecEnv({
baseEnv: process.env,
overrides: backend.env,
blockPathOverrides: true,
});
for (const key of backend.clearEnv ?? []) {
delete next[key];
}
if (backend.env && Object.keys(backend.env).length > 0) {
Object.assign(
next,
sanitizeHostExecEnv({
baseEnv: {},
overrides: backend.env,
blockPathOverrides: true,
}),
);
}
Object.assign(next, context.preparedBackend.env);
return next;
})();