fix(cli): reject --agent for global model refresh (#130448)

* fix(cli): models refresh rejects parent --agent like set/aliases/scan

`models refresh` refreshes the global hosted catalog
(refreshRemoteModelCatalog) and takes no agent, but silently accepted a
parent --agent, implying a per-agent scope it does not deliver. #126864
landed rejectAgentScopedModelCommand for set/set-image/aliases/scan but
left refresh unaddressed (it scoped out only fallbacks/image-fallbacks).
Add "refresh" to GlobalOnlyModelCommandName and switch refresh's action
to the same loadModelsRuntime + reject + runModelsCommand pattern, so
--agent fails fast with the same error instead of a silent global refresh.

Co-Authored-By: Claude <noreply@anthropic.com>

* docs(cli): list models refresh among --agent-rejecting commands

`docs/cli/models.md` listed `set`/`set-image`/`scan`/`aliases` as
rejecting `--agent` but omitted `refresh`, so the new guard landed in
the previous commit was not reflected in the docs. Add `refresh` to the
global-only list and note it on the refresh section.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: ruel225 <ruel225@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
ruel225
2026-08-27 14:25:44 +08:00
committed by GitHub
parent 3708934c63
commit a3eb0692a2
4 changed files with 25 additions and 6 deletions
+2 -2
View File
@@ -28,7 +28,7 @@ openclaw models set-image <model-or-alias>
openclaw models scan
```
`status`, `list`, and `auth` subcommands accept `--agent <id>` to target a configured agent; `fallbacks`/`image-fallbacks` always use the configured default agent, and `set`, `set-image`, `scan`, and `aliases` reject `--agent` outright because they are global and never agent-scoped. When omitted, `--agent`-aware commands use `OPENCLAW_AGENT_DIR` if set, otherwise the configured default agent.
`status`, `list`, and `auth` subcommands accept `--agent <id>` to target a configured agent; `fallbacks`/`image-fallbacks` always use the configured default agent, and `set`, `set-image`, `scan`, `refresh`, and `aliases` reject `--agent` outright because they are global and never agent-scoped. When omitted, `--agent`-aware commands use `OPENCLAW_AGENT_DIR` if set, otherwise the configured default agent.
`models set` and `models set-image` require the provider to be declared by an installed plugin or configured under `models.providers`. An unknown provider exits nonzero without changing config. If the provider is known but the model is absent from the local catalog, the command saves the selection and prints a warning because newly released and self-hosted models may not be cataloged yet. `openclaw doctor --json` reports configured unknown providers; add `--severity-min info` to also see active models that the local catalog cannot confirm.
@@ -73,7 +73,7 @@ For OpenAI ChatGPT/Codex OAuth troubleshooting, `openclaw models status`, `openc
`openclaw models list` is read-only: it reads config, auth profiles, existing catalog state, and provider-owned catalog rows, but never rewrites `models.json`.
`openclaw models refresh [--json]` forces an immediate hosted catalog check.
`openclaw models refresh [--json]` forces an immediate hosted catalog check. Like `scan`, it rejects `--agent` because the hosted catalog is global, not agent-scoped.
Updated rows apply to a running Gateway after its next restart. The command
prints a clear disabled result when `models.catalogRefresh.enabled` is `false`.
The catalog's public change history lives in
+2 -1
View File
@@ -27,7 +27,8 @@ export type GlobalOnlyModelCommandName =
| "scan"
| "aliases list"
| "aliases add"
| "aliases remove";
| "aliases remove"
| "refresh";
export function rejectAgentScopedModelCommand(
command: Command,
+16
View File
@@ -11,6 +11,7 @@ const mocks = vi.hoisted(() => ({
modelsStatusCommand: vi.fn().mockResolvedValue(undefined),
modelsSetCommand: vi.fn().mockResolvedValue(undefined),
modelsSetImageCommand: vi.fn().mockResolvedValue(undefined),
modelsRefreshCommand: vi.fn().mockResolvedValue(undefined),
noopAsync: vi.fn(async () => undefined),
modelsAliasesAddCommand: vi.fn().mockResolvedValue(undefined),
modelsAliasesListCommand: vi.fn().mockResolvedValue(undefined),
@@ -42,6 +43,7 @@ const {
modelsAuthPasteApiKeyCommand,
modelsAuthPasteTokenCommand,
modelsAuthSetupTokenCommand,
modelsRefreshCommand,
modelsScanCommand,
modelsSetCommand,
modelsSetImageCommand,
@@ -92,6 +94,9 @@ vi.mock("../commands/models/set.js", () => ({
vi.mock("../commands/models/set-image.js", () => ({
modelsSetImageCommand: mocks.modelsSetImageCommand,
}));
vi.mock("../commands/models/refresh.js", () => ({
modelsRefreshCommand: mocks.modelsRefreshCommand,
}));
describe("models cli", () => {
beforeEach(() => {
@@ -99,6 +104,7 @@ describe("models cli", () => {
modelsAliasesAddCommand.mockClear();
modelsAliasesListCommand.mockClear();
modelsAliasesRemoveCommand.mockClear();
modelsRefreshCommand.mockClear();
modelsScanCommand.mockClear();
modelsAuthAddCommand.mockClear();
modelsAuthListCommand.mockClear();
@@ -576,6 +582,11 @@ describe("models cli", () => {
args: ["models", "--agent", "poe", "scan", "--no-probe", "--no-input"],
command: modelsScanCommand,
},
{
label: "refresh",
args: ["models", "--agent", "poe", "refresh"],
command: modelsRefreshCommand,
},
])("rejects parent --agent for models $label", async ({ args, command }) => {
await expect(runModelsCommand(args)).rejects.toThrow("does not support --agent");
@@ -593,6 +604,11 @@ describe("models cli", () => {
args: ["models", "scan", "--no-probe", "--no-input"],
command: modelsScanCommand,
},
{
label: "refresh",
args: ["models", "refresh"],
command: modelsRefreshCommand,
},
])("still runs models $label without --agent", async ({ args, command }) => {
await runModelsCommand(args);
+5 -3
View File
@@ -128,10 +128,12 @@ export function registerModelsCli(program: Command) {
.command("refresh")
.description("Refresh the hosted model catalog")
.option("--json", "Output JSON", false)
.action(async (opts) => {
await withModelsRuntime(async ({ defaultRuntime }) => {
.action(async (opts, command: Command) => {
const runtime = await loadModelsRuntime();
runtime.rejectAgentScopedModelCommand(command, "refresh");
await runtime.runModelsCommand(async () => {
const { modelsRefreshCommand } = await import("../commands/models/refresh.js");
await modelsRefreshCommand({ json: hasJsonOutput(opts) }, defaultRuntime);
await modelsRefreshCommand({ json: hasJsonOutput(opts) }, runtime.defaultRuntime);
});
});