fix: reject agent-scoped model default writes

This commit is contained in:
Peter Steinberger
2026-05-02 04:19:01 +01:00
parent 9a814bcec2
commit ebe8f615e5
3 changed files with 40 additions and 4 deletions

View File

@@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Control UI/chat: keep live replies visible when a raw session alias such as `main` sends the chat turn but Gateway emits events under the canonical session key for the same run. Fixes #73716. Thanks @teebes.
- CLI/models: reject `--agent` on `openclaw models set` and `set-image` instead of silently writing agent-scoped requests to global model defaults. Fixes #68391. Thanks @derrickabellard.
- CLI: stop treating the legacy singular `openclaw tool ...` token as a plugin id under restrictive `plugins.allow`, so it falls through as a normal unknown/reserved command instead of suggesting a stale allowlist entry. Fixes #64732. Thanks @efe-arv, @SweetSophia, and @hashtag1974.
- Media: write inbound media buffers through same-directory temp files before rename, so failed disk writes do not leave zero-byte artifacts for later voice transcription. Fixes #55966. Thanks @OpenCodeEngineer.
- TTS/Telegram: keep trusted local audio generated by the TTS tool queued for voice-note delivery even when the run-level built-in tool list omits the raw `tts` name. Fixes #74752. Thanks @Loveworld3033 and @andyliu.

View File

@@ -5,6 +5,8 @@ import { registerModelsCli } from "./models-cli.js";
const mocks = vi.hoisted(() => ({
modelsStatusCommand: vi.fn().mockResolvedValue(undefined),
modelsSetCommand: vi.fn().mockResolvedValue(undefined),
modelsSetImageCommand: vi.fn().mockResolvedValue(undefined),
noopAsync: vi.fn(async () => undefined),
modelsAuthAddCommand: vi.fn().mockResolvedValue(undefined),
modelsAuthLoginCommand: vi.fn().mockResolvedValue(undefined),
@@ -17,6 +19,8 @@ const {
modelsAuthLoginCommand,
modelsAuthPasteTokenCommand,
modelsAuthSetupTokenCommand,
modelsSetCommand,
modelsSetImageCommand,
modelsStatusCommand,
} = mocks;
@@ -58,10 +62,10 @@ vi.mock("../commands/models/scan.js", () => ({
modelsScanCommand: mocks.noopAsync,
}));
vi.mock("../commands/models/set.js", () => ({
modelsSetCommand: mocks.noopAsync,
modelsSetCommand: mocks.modelsSetCommand,
}));
vi.mock("../commands/models/set-image.js", () => ({
modelsSetImageCommand: mocks.noopAsync,
modelsSetImageCommand: mocks.modelsSetImageCommand,
}));
describe("models cli", () => {
@@ -70,6 +74,8 @@ describe("models cli", () => {
modelsAuthLoginCommand.mockClear();
modelsAuthPasteTokenCommand.mockClear();
modelsAuthSetupTokenCommand.mockClear();
modelsSetCommand.mockClear();
modelsSetImageCommand.mockClear();
modelsStatusCommand.mockClear();
});
@@ -162,6 +168,23 @@ describe("models cli", () => {
expect(command).toHaveBeenCalledWith(expect.objectContaining(expected), expect.any(Object));
});
it.each([
{
label: "set",
args: ["models", "--agent", "poe", "set", "anthropic/claude-sonnet-4-6"],
command: modelsSetCommand,
},
{
label: "set-image",
args: ["models", "--agent", "poe", "set-image", "openai/gpt-image-1"],
command: modelsSetImageCommand,
},
])("rejects parent --agent for models $label", async ({ args, command }) => {
await expect(runModelsCommand(args)).rejects.toThrow("does not support `--agent`");
expect(command).not.toHaveBeenCalled();
});
it("shows help for models auth without error exit", async () => {
const program = new Command();
program.exitOverride();

View File

@@ -8,6 +8,16 @@ function runModelsCommand(action: () => Promise<void>) {
return runCommandWithRuntime(defaultRuntime, action);
}
function rejectAgentScopedModelWrite(command: Command, commandName: "set" | "set-image"): void {
const agent = resolveOptionFromCommand<string>(command, "agent");
if (!agent) {
return;
}
throw new Error(
`\`openclaw models ${commandName}\` does not support \`--agent\`; it only updates global model defaults. Remove \`--agent\` or use agent config to set a per-agent model override.`,
);
}
export function registerModelsCli(program: Command) {
const models = program
.command("models")
@@ -94,7 +104,8 @@ export function registerModelsCli(program: Command) {
.command("set")
.description("Set the default model")
.argument("<model>", "Model id or alias")
.action(async (model: string) => {
.action(async (model: string, _opts: unknown, command: Command) => {
rejectAgentScopedModelWrite(command, "set");
await runModelsCommand(async () => {
const { modelsSetCommand } = await import("../commands/models/set.js");
await modelsSetCommand(model, defaultRuntime);
@@ -105,7 +116,8 @@ export function registerModelsCli(program: Command) {
.command("set-image")
.description("Set the image model")
.argument("<model>", "Model id or alias")
.action(async (model: string) => {
.action(async (model: string, _opts: unknown, command: Command) => {
rejectAgentScopedModelWrite(command, "set-image");
await runModelsCommand(async () => {
const { modelsSetImageCommand } = await import("../commands/models/set-image.js");
await modelsSetImageCommand(model, defaultRuntime);