Skip to content

Commit 2ada1b7

Browse files
steipeteMumuTW
andcommitted
fix(models-auth): land openclaw#38951 from @MumuTW
Co-authored-by: MumuTW <[email protected]>
1 parent 02f99c0 commit 2ada1b7

File tree

3 files changed

+93
-22
lines changed

3 files changed

+93
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ Docs: https://docs.openclaw.ai
246246
- CLI/bootstrap Node version hint maintenance: replace hardcoded nvm `22` instructions in `openclaw.mjs` with `MIN_NODE_MAJOR` interpolation so future minimum-Node bumps keep startup guidance in sync automatically. (#39056) Thanks @onstash.
247247
- Discord/native slash command auth: honor `commands.allowFrom.discord` (and `commands.allowFrom["*"]`) in guild slash-command pre-dispatch authorization so allowlisted senders are no longer incorrectly rejected as unauthorized. (#38794) Thanks @jskoiz and @thewilloftheshadow.
248248
- Outbound/message target normalization: ignore empty legacy `to`/`channelId` fields when explicit `target` is provided so valid target-based sends no longer fail legacy-param validation; includes regression coverage. (#38944) Thanks @Narcooo.
249+
- Models/auth token prompts: guard cancelled manual token prompts so `Symbol(clack:cancel)` values cannot be persisted into auth profiles; adds regression coverage for cancelled `models auth paste-token`. (#38951) Thanks @MumuTW.
249250

250251
## 2026.3.2
251252

src/commands/models/auth.test.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@ import type { OpenClawConfig } from "../../config/config.js";
33
import type { RuntimeEnv } from "../../runtime.js";
44

55
const mocks = vi.hoisted(() => ({
6+
clackCancel: vi.fn(),
7+
clackConfirm: vi.fn(),
8+
clackIsCancel: vi.fn((value: unknown) => value === Symbol.for("clack:cancel")),
9+
clackSelect: vi.fn(),
10+
clackText: vi.fn(),
611
resolveDefaultAgentId: vi.fn(),
712
resolveAgentDir: vi.fn(),
813
resolveAgentWorkspaceDir: vi.fn(),
914
resolveDefaultAgentWorkspaceDir: vi.fn(),
15+
upsertAuthProfile: vi.fn(),
1016
resolvePluginProviders: vi.fn(),
1117
createClackPrompter: vi.fn(),
1218
loginOpenAICodexOAuth: vi.fn(),
@@ -17,6 +23,14 @@ const mocks = vi.hoisted(() => ({
1723
openUrl: vi.fn(),
1824
}));
1925

26+
vi.mock("@clack/prompts", () => ({
27+
cancel: mocks.clackCancel,
28+
confirm: mocks.clackConfirm,
29+
isCancel: mocks.clackIsCancel,
30+
select: mocks.clackSelect,
31+
text: mocks.clackText,
32+
}));
33+
2034
vi.mock("../../agents/agent-scope.js", () => ({
2135
resolveDefaultAgentId: mocks.resolveDefaultAgentId,
2236
resolveAgentDir: mocks.resolveAgentDir,
@@ -27,6 +41,10 @@ vi.mock("../../agents/workspace.js", () => ({
2741
resolveDefaultAgentWorkspaceDir: mocks.resolveDefaultAgentWorkspaceDir,
2842
}));
2943

44+
vi.mock("../../agents/auth-profiles.js", () => ({
45+
upsertAuthProfile: mocks.upsertAuthProfile,
46+
}));
47+
3048
vi.mock("../../plugins/providers.js", () => ({
3149
resolvePluginProviders: mocks.resolvePluginProviders,
3250
}));
@@ -64,7 +82,7 @@ vi.mock("../onboard-helpers.js", () => ({
6482
openUrl: mocks.openUrl,
6583
}));
6684

67-
const { modelsAuthLoginCommand } = await import("./auth.js");
85+
const { modelsAuthLoginCommand, modelsAuthPasteTokenCommand } = await import("./auth.js");
6886

6987
function createRuntime(): RuntimeEnv {
7088
return {
@@ -102,6 +120,14 @@ describe("modelsAuthLoginCommand", () => {
102120
restoreStdin = withInteractiveStdin();
103121
currentConfig = {};
104122
lastUpdatedConfig = null;
123+
mocks.clackCancel.mockReset();
124+
mocks.clackConfirm.mockReset();
125+
mocks.clackIsCancel.mockImplementation(
126+
(value: unknown) => value === Symbol.for("clack:cancel"),
127+
);
128+
mocks.clackSelect.mockReset();
129+
mocks.clackText.mockReset();
130+
mocks.upsertAuthProfile.mockReset();
105131

106132
mocks.resolveDefaultAgentId.mockReturnValue("main");
107133
mocks.resolveAgentDir.mockReturnValue("/tmp/openclaw/agents/main");
@@ -179,4 +205,28 @@ describe("modelsAuthLoginCommand", () => {
179205
"No provider plugins found.",
180206
);
181207
});
208+
209+
it("does not persist a cancelled manual token entry", async () => {
210+
const runtime = createRuntime();
211+
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((
212+
code?: string | number | null,
213+
) => {
214+
throw new Error(`exit:${String(code ?? "")}`);
215+
}) as typeof process.exit);
216+
try {
217+
const cancelSymbol = Symbol.for("clack:cancel");
218+
mocks.clackText.mockResolvedValue(cancelSymbol);
219+
mocks.clackIsCancel.mockImplementation((value: unknown) => value === cancelSymbol);
220+
221+
await expect(modelsAuthPasteTokenCommand({ provider: "openai" }, runtime)).rejects.toThrow(
222+
"exit:0",
223+
);
224+
225+
expect(mocks.upsertAuthProfile).not.toHaveBeenCalled();
226+
expect(mocks.updateConfig).not.toHaveBeenCalled();
227+
expect(mocks.logConfigUpdated).not.toHaveBeenCalled();
228+
} finally {
229+
exitSpy.mockRestore();
230+
}
231+
});
182232
});

src/commands/models/auth.ts

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { confirm as clackConfirm, select as clackSelect, text as clackText } from "@clack/prompts";
1+
import {
2+
cancel,
3+
confirm as clackConfirm,
4+
isCancel,
5+
select as clackSelect,
6+
text as clackText,
7+
} from "@clack/prompts";
28
import {
39
resolveAgentDir,
410
resolveAgentWorkspaceDir,
@@ -34,24 +40,38 @@ import {
3440
} from "../provider-auth-helpers.js";
3541
import { loadValidConfigOrThrow, updateConfig } from "./shared.js";
3642

37-
const confirm = (params: Parameters<typeof clackConfirm>[0]) =>
38-
clackConfirm({
39-
...params,
40-
message: stylePromptMessage(params.message),
41-
});
42-
const text = (params: Parameters<typeof clackText>[0]) =>
43-
clackText({
44-
...params,
45-
message: stylePromptMessage(params.message),
46-
});
47-
const select = <T>(params: Parameters<typeof clackSelect<T>>[0]) =>
48-
clackSelect({
49-
...params,
50-
message: stylePromptMessage(params.message),
51-
options: params.options.map((opt) =>
52-
opt.hint === undefined ? opt : { ...opt, hint: stylePromptHint(opt.hint) },
53-
),
54-
});
43+
function guardCancel<T>(value: T | symbol): T {
44+
if (typeof value === "symbol" || isCancel(value)) {
45+
cancel("Cancelled.");
46+
process.exit(0);
47+
}
48+
return value;
49+
}
50+
51+
const confirm = async (params: Parameters<typeof clackConfirm>[0]) =>
52+
guardCancel(
53+
await clackConfirm({
54+
...params,
55+
message: stylePromptMessage(params.message),
56+
}),
57+
);
58+
const text = async (params: Parameters<typeof clackText>[0]) =>
59+
guardCancel(
60+
await clackText({
61+
...params,
62+
message: stylePromptMessage(params.message),
63+
}),
64+
);
65+
const select = async <T>(params: Parameters<typeof clackSelect<T>>[0]) =>
66+
guardCancel(
67+
await clackSelect({
68+
...params,
69+
message: stylePromptMessage(params.message),
70+
options: params.options.map((opt) =>
71+
opt.hint === undefined ? opt : { ...opt, hint: stylePromptHint(opt.hint) },
72+
),
73+
}),
74+
);
5575

5676
type TokenProvider = "anthropic";
5777

@@ -165,13 +185,13 @@ export async function modelsAuthPasteTokenCommand(
165185
}
166186

167187
export async function modelsAuthAddCommand(_opts: Record<string, never>, runtime: RuntimeEnv) {
168-
const provider = (await select({
188+
const provider = await select({
169189
message: "Token provider",
170190
options: [
171191
{ value: "anthropic", label: "anthropic" },
172192
{ value: "custom", label: "custom (type provider id)" },
173193
],
174-
})) as TokenProvider | "custom";
194+
});
175195

176196
const providerId =
177197
provider === "custom"

0 commit comments

Comments
 (0)