Skip to content

Commit d1e2fbd

Browse files
wahaha1223steipete
andauthored
fix(skills): keep blank env requirements unsatisfied (#108848)
* fix(skills): keep blank env requirements unsatisfied * refactor(skills): reuse secret input presence check --------- Co-authored-by: Peter Steinberger <[email protected]>
1 parent b273fd4 commit d1e2fbd

4 files changed

Lines changed: 106 additions & 10 deletions

File tree

src/skills/discovery/status.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,30 @@ type SkillStatus = ReturnType<typeof buildWorkspaceSkillStatus>["skills"][number
1313
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
1414

1515
describe("buildWorkspaceSkillStatus", () => {
16+
it("reports blank env requirements as missing", () => {
17+
const envName = "OPENCLAW_TEST_BLANK_SKILL_STATUS";
18+
const original = process.env[envName];
19+
process.env[envName] = " ";
20+
try {
21+
const report = buildWorkspaceSkillStatus("/tmp/ws", {
22+
entries: [
23+
createEntry("blank-env", {
24+
metadata: { primaryEnv: envName, requires: { env: [envName] } },
25+
}),
26+
],
27+
});
28+
29+
expect(report.skills[0]?.eligible).toBe(false);
30+
expect(report.skills[0]?.missing.env).toEqual([envName]);
31+
} finally {
32+
if (original === undefined) {
33+
delete process.env[envName];
34+
} else {
35+
process.env[envName] = original;
36+
}
37+
}
38+
});
39+
1640
it("surfaces valid ClawHub linkage and local Skill Card metadata", async () => {
1741
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-skill-status-"));
1842
try {

src/skills/discovery/status.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { resolveBundledSkillsContext } from "../loading/bundled-context.js";
1616
import {
1717
hasBinary,
1818
isBundledSkillAllowed,
19+
isSkillEnvRequirementSatisfied,
1920
isSkillConfigPathTruthy,
2021
resolveBundledAllowlist,
2122
resolveSkillConfig,
@@ -269,11 +270,11 @@ function buildSkillStatus(
269270
const blockedByAgentFilter = agentSkillFilter !== undefined && !indexed.agentAllowed;
270271
const always = entry.metadata?.always === true;
271272
const isEnvSatisfied = (envName: string) =>
272-
Boolean(
273-
process.env[envName] ||
274-
skillConfig?.env?.[envName] ||
275-
(skillConfig?.apiKey && entry.metadata?.primaryEnv === envName),
276-
);
273+
isSkillEnvRequirementSatisfied({
274+
envName,
275+
skillConfig,
276+
primaryEnv: entry.metadata?.primaryEnv,
277+
});
277278
const isConfigSatisfied = (pathStr: string) => isSkillConfigPathTruthy(config, pathStr);
278279
const skillSource = indexed.source;
279280
const bundled = indexed.bundled;

src/skills/loading/config.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
} from "@openclaw/normalization-core/string-coerce";
66
import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization";
77
import type { OpenClawConfig } from "../../config/types.openclaw.js";
8+
import { hasConfiguredSecretInput } from "../../config/types.secrets.js";
89
import type { SkillConfig } from "../../config/types.skills.js";
910
import {
1011
evaluateRuntimeEligibility,
@@ -56,6 +57,19 @@ export function resolveSkillConfig(
5657
return entry;
5758
}
5859

60+
export function isSkillEnvRequirementSatisfied(params: {
61+
envName: string;
62+
skillConfig?: SkillConfig;
63+
primaryEnv?: string;
64+
}): boolean {
65+
const { envName, skillConfig, primaryEnv } = params;
66+
return (
67+
normalizeOptionalString(process.env[envName]) !== undefined ||
68+
normalizeOptionalString(skillConfig?.env?.[envName]) !== undefined ||
69+
(primaryEnv === envName && hasConfiguredSecretInput(skillConfig?.apiKey))
70+
);
71+
}
72+
5973
function normalizeAllowlist(input: unknown): ReadonlySet<string> | undefined {
6074
if (!input) {
6175
return undefined;
@@ -113,11 +127,11 @@ export function shouldIncludeSkill(params: {
113127
hasRemoteBin: eligibility?.remote?.hasBin,
114128
hasAnyRemoteBin: eligibility?.remote?.hasAnyBin,
115129
hasEnv: (envName) =>
116-
Boolean(
117-
process.env[envName] ||
118-
skillConfig?.env?.[envName] ||
119-
(skillConfig?.apiKey && entry.metadata?.primaryEnv === envName),
120-
),
130+
isSkillEnvRequirementSatisfied({
131+
envName,
132+
skillConfig,
133+
primaryEnv: entry.metadata?.primaryEnv,
134+
}),
121135
isConfigPathTruthy: (configPath) => isSkillConfigPathTruthy(config, configPath),
122136
});
123137
}

src/skills/loading/skills.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
type SkillsHomeEnvSnapshot,
2525
} from "../test-support/home-env.test-support.js";
2626
import type { SkillEntry, SkillSnapshot } from "../types.js";
27+
import { shouldIncludeSkill } from "./config.js";
2728
import { buildWorkspaceSkillsPrompt } from "./workspace.js";
2829

2930
vi.mock("./plugin-skills.js", () => ({
@@ -502,6 +503,62 @@ describe("buildWorkspaceSkillsPrompt", () => {
502503
});
503504
});
504505

506+
describe("shouldIncludeSkill", () => {
507+
const envName = "OPENCLAW_TEST_SKILL_REQUIREMENT";
508+
const entry = makeSkillEntry("env-skill", {
509+
primaryEnv: envName,
510+
requires: { env: [envName] },
511+
});
512+
513+
function shouldInclude(config?: OpenClawConfig): boolean {
514+
return shouldIncludeSkill({ entry, config, bundledAllowlist: undefined });
515+
}
516+
517+
it("requires non-blank host and configured env values", () => {
518+
withClearedEnv([envName], () => {
519+
expect(shouldInclude()).toBe(false);
520+
521+
process.env[envName] = " ";
522+
expect(shouldInclude()).toBe(false);
523+
524+
process.env[envName] = " example ";
525+
expect(shouldInclude()).toBe(true);
526+
527+
delete process.env[envName];
528+
expect(
529+
shouldInclude({ skills: { entries: { "env-skill": { env: { [envName]: " " } } } } }),
530+
).toBe(false);
531+
expect(
532+
shouldInclude({
533+
skills: { entries: { "env-skill": { env: { [envName]: " example " } } } },
534+
}),
535+
).toBe(true);
536+
});
537+
});
538+
539+
it("requires a non-blank primary apiKey", () => {
540+
withClearedEnv([envName], () => {
541+
expect(shouldInclude(resolvedSkillApiKeyConfig("env-skill", " "))).toBe(false);
542+
expect(shouldInclude(resolvedSkillApiKeyConfig("env-skill", " example "))).toBe(true);
543+
expect(shouldInclude(rawSkillApiKeyRefConfig("env-skill"))).toBe(true);
544+
});
545+
});
546+
547+
it("keeps always-on skills eligible without credentials", () => {
548+
withClearedEnv([envName], () => {
549+
expect(
550+
shouldIncludeSkill({
551+
entry: makeSkillEntry("always-skill", {
552+
always: true,
553+
requires: { env: [envName] },
554+
}),
555+
bundledAllowlist: undefined,
556+
}),
557+
).toBe(true);
558+
});
559+
});
560+
});
561+
505562
describe("applySkillEnvOverrides", () => {
506563
it("sets and restores env vars", () => {
507564
const entries = envSkillEntries("env-skill", {

0 commit comments

Comments
 (0)