Skip to content

Commit bd7fa0a

Browse files
committed
fix(onboard): gate post-install API-key prompt on explicit selection for env-only skills (#74382)
Address Codex/clawsweeper P2 on PR #74891: the bins-only guard let `openai-whisper-api` (env-only missing on hosts with curl present) prompt for OPENAI_API_KEY even when the user opted into only the local `openai-whisper` skill. Bring env-only-missing skills into the dependency multiselect so the user gets an explicit Set up missing skill dependencies choice for them, then tighten the post-install prompt guard to require selection regardless of whether binaries are missing. The install loop already short-circuits env-only entries via `installable.find()`. Tests: add two regression cases — env-only skill not selected (no prompt) and env-only skill explicitly selected (prompt + no install attempted). All 6 setupSkills tests pass.
1 parent 890a159 commit bd7fa0a

3 files changed

Lines changed: 81 additions & 9 deletions

File tree

CHANGELOG.md

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

4343
- Media: treat legacy Word/OLE attachments with `application/msword` or `application/x-cfb` MIME as binary so printable-looking `.doc` files are not embedded into prompts as text. Fixes #54176; carries forward #54380. Thanks @andyliu.
4444
- Config: accept documented `browser.tabCleanup` keys in strict root config validation, so configured tab cleanup no longer fails before runtime reads it. Fixes #74577. Thanks @lonexreb and @ezdlp.
45+
- Onboard/skills: gate the post-install API-key prompt on the user's explicit dependency-multiselect choice for both binary-bearing and env-only skills, so a user who selects only local `openai-whisper` is no longer asked for `OPENAI_API_KEY` for the unselected `openai-whisper-api` skill on systems where `curl` is already present. Fixes #74382. Thanks @lonexreb.
4546
- Cron: validate disabled job schedule edits before persisting updates, so invalid cron changes no longer partially mutate stored jobs. Fixes #74459. Thanks @yfge.
4647
- Channels/status: keep Telegram, Slack, and Google Chat read-only allowlist/default-target accessors on config-only paths, so status and channel summaries do not resolve SecretRef-backed runtime credentials. Thanks @eusine.
4748
- Active Memory: clarify the deprecated `modelFallbackPolicy` warning and config help so `modelFallback` is described as a chain-resolution last resort, not runtime failover. (#74602) Thanks @jeffrey701.

src/commands/onboard-skills.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,4 +249,65 @@ describe("setupSkills", () => {
249249
const askedForApiKey = confirmCalls.some(([arg]) => arg.message.includes("Set OPENAI_API_KEY"));
250250
expect(askedForApiKey).toBe(true);
251251
});
252+
253+
it("does not prompt for an API key for an env-only-missing skill the user did not select", async () => {
254+
if (process.platform === "win32") {
255+
return;
256+
}
257+
258+
mockMissingBrewStatus([
259+
createBundledSkill({
260+
name: "openai-whisper",
261+
description: "Local whisper CLI (no API key)",
262+
bins: ["whisper"],
263+
installLabel: "Install OpenAI Whisper (brew)",
264+
}),
265+
createBundledSkill({
266+
name: "openai-whisper-api",
267+
description: "OpenAI Whisper API (curl already present)",
268+
bins: [],
269+
installLabel: "Install curl (brew)",
270+
primaryEnv: "OPENAI_API_KEY",
271+
envMissing: ["OPENAI_API_KEY"],
272+
}),
273+
]);
274+
275+
const { prompter } = createPrompter({ multiselect: ["openai-whisper"] });
276+
await setupSkills({} as OpenClawConfig, "/tmp/ws", runtime, prompter);
277+
278+
const confirmCalls = (
279+
prompter.confirm as unknown as { mock: { calls: Array<[{ message: string }]> } }
280+
).mock.calls;
281+
const askedForApiKey = confirmCalls.some(([arg]) => arg.message.includes("Set OPENAI_API_KEY"));
282+
expect(askedForApiKey).toBe(false);
283+
});
284+
285+
it("prompts for an API key for an env-only-missing skill the user explicitly selected", async () => {
286+
if (process.platform === "win32") {
287+
return;
288+
}
289+
290+
mockMissingBrewStatus([
291+
createBundledSkill({
292+
name: "openai-whisper-api",
293+
description: "OpenAI Whisper API (curl already present)",
294+
bins: [],
295+
installLabel: "Install curl (brew)",
296+
primaryEnv: "OPENAI_API_KEY",
297+
envMissing: ["OPENAI_API_KEY"],
298+
}),
299+
]);
300+
301+
const { prompter } = createPrompter({ multiselect: ["openai-whisper-api"] });
302+
mocks.installSkill.mockClear();
303+
await setupSkills({} as OpenClawConfig, "/tmp/ws", runtime, prompter);
304+
305+
const confirmCalls = (
306+
prompter.confirm as unknown as { mock: { calls: Array<[{ message: string }]> } }
307+
).mock.calls;
308+
const askedForApiKey = confirmCalls.some(([arg]) => arg.message.includes("Set OPENAI_API_KEY"));
309+
expect(askedForApiKey).toBe(true);
310+
// Install loop must not run installSkill for env-only entries.
311+
expect(mocks.installSkill).not.toHaveBeenCalled();
312+
});
252313
});

src/commands/onboard-skills.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,22 +84,32 @@ export async function setupSkills(
8484
const installable = missing.filter(
8585
(skill) => skill.install.length > 0 && skill.missing.bins.length > 0,
8686
);
87+
const envOnlyConfigurable = missing.filter(
88+
(skill) =>
89+
Boolean(skill.primaryEnv) && skill.missing.env.length > 0 && skill.missing.bins.length === 0,
90+
);
91+
const configurable = [...installable, ...envOnlyConfigurable];
8792
let next: OpenClawConfig = cfg;
8893
const installSelected = new Set<string>();
89-
if (installable.length > 0) {
94+
if (configurable.length > 0) {
9095
const toInstall = await prompter.multiselect({
91-
message: "Install missing skill dependencies",
96+
message: "Set up missing skill dependencies",
9297
options: [
9398
{
9499
value: "__skip__",
95100
label: "Skip for now",
96-
hint: "Continue without installing dependencies",
101+
hint: "Continue without configuring dependencies",
97102
},
98-
...installable.map((skill) => ({
99-
value: skill.name,
100-
label: `${skill.emoji ?? "🧩"} ${skill.name}`,
101-
hint: formatSkillHint(skill),
102-
})),
103+
...configurable.map((skill) => {
104+
const isEnvOnly = skill.missing.bins.length === 0;
105+
return {
106+
value: skill.name,
107+
label: `${skill.emoji ?? "🧩"} ${skill.name}`,
108+
hint: isEnvOnly
109+
? `Configure ${skill.primaryEnv ?? "credentials"}`
110+
: formatSkillHint(skill),
111+
};
112+
}),
103113
],
104114
});
105115

@@ -206,7 +216,7 @@ export async function setupSkills(
206216
if (!skill.primaryEnv || skill.missing.env.length === 0) {
207217
continue;
208218
}
209-
if (skill.missing.bins.length > 0 && !installSelected.has(skill.name)) {
219+
if (!installSelected.has(skill.name)) {
210220
continue;
211221
}
212222
const wantsKey = await prompter.confirm({

0 commit comments

Comments
 (0)