From 169dba2042155fa254e2405cc69ad36700ad16c7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Apr 2026 09:23:53 +0100 Subject: [PATCH] fix(skills): require opt-in for coding-agent --- CHANGELOG.md | 1 + docs/tools/skills.md | 4 ++ skills/coding-agent/SKILL.md | 6 +- .../skills.buildworkspaceskillstatus.test.ts | 58 +++++++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31f8f28721f..2cdfc7f2c96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Agents/Anthropic: send implicit Anthropic beta headers only to direct public Anthropic endpoints, including OAuth, so custom Anthropic-compatible providers no longer mis-handle unsupported beta flags unless explicitly configured. Refs #73346. Thanks @byBrodowski. +- Skills: require explicit `skills.entries.coding-agent.enabled` before exposing the bundled coding-agent skill, so installs with Codex on PATH but no OpenAI auth do not silently offer Codex delegation. Fixes #73358. Thanks @LaFleurAdvertising and @Sanjays2402. - Plugins/startup: precompute bundled runtime mirror fingerprints before taking the mirror lock, including dist-runtime canonical roots, so Docker Desktop/WSL cold starts no longer hold `.openclaw-runtime-mirror.lock` while scanning slow persisted volumes. Fixes #73339. Thanks @1yihui. - Channels/LINE: persist inbound image, video, audio, and file downloads in `~/.openclaw/media/inbound/` instead of temporary files so agents can still read LINE media after `/tmp` cleanup. Fixes #73370. Thanks @hijirii and @wenxu007. - Control UI/WebChat: keep large attachment payloads out of Lit state and optimistic chat messages, using object URL previews plus send-time payload serialization so PDF/image uploads no longer trigger `RangeError: Maximum call stack size exceeded`. Fixes #73360; refs #54378 and #63432. Thanks @hejunhui-73, @Ansub, and @christianhernandez3-afk. diff --git a/docs/tools/skills.md b/docs/tools/skills.md index c8610ad74e1..244bb29d327 100644 --- a/docs/tools/skills.md +++ b/docs/tools/skills.md @@ -324,6 +324,10 @@ under `skills.entries` in `~/.openclaw/openclaw.json`: `false` disables the skill even if it is bundled or installed. + The bundled `coding-agent` skill is opt-in: set + `skills.entries.coding-agent.enabled: true` before exposing it to agents, + then make sure one of `claude`, `codex`, `opencode`, or `pi` is installed and + authenticated for its own CLI. Convenience for skills that declare `metadata.openclaw.primaryEnv`. Supports plaintext or SecretRef. diff --git a/skills/coding-agent/SKILL.md b/skills/coding-agent/SKILL.md index 6961bd1ba5e..8d8adb032ff 100644 --- a/skills/coding-agent/SKILL.md +++ b/skills/coding-agent/SKILL.md @@ -6,7 +6,11 @@ metadata: "openclaw": { "emoji": "🧩", - "requires": { "anyBins": ["claude", "codex", "opencode", "pi"] }, + "requires": + { + "anyBins": ["claude", "codex", "opencode", "pi"], + "config": ["skills.entries.coding-agent.enabled"], + }, "install": [ { diff --git a/src/agents/skills.buildworkspaceskillstatus.test.ts b/src/agents/skills.buildworkspaceskillstatus.test.ts index f0445542862..9d20d47b05e 100644 --- a/src/agents/skills.buildworkspaceskillstatus.test.ts +++ b/src/agents/skills.buildworkspaceskillstatus.test.ts @@ -7,6 +7,7 @@ import { buildWorkspaceSkillStatus } from "./skills-status.js"; import { writeSkill } from "./skills.e2e-test-helpers.js"; import { createCanonicalFixtureSkill } from "./skills.test-helpers.js"; import type { SkillEntry } from "./skills/types.js"; +import { loadWorkspaceSkillEntries } from "./skills/workspace.js"; const tempDirs: string[] = []; @@ -166,6 +167,63 @@ describe("buildWorkspaceSkillStatus", () => { expect(skill?.bundled).toBe(true); }); + it("requires explicit enablement before exposing bundled coding-agent", async () => { + const workspaceDir = await createTempWorkspaceDir(); + const bundledSkillsDir = path.resolve("skills"); + const entries = loadWorkspaceSkillEntries(workspaceDir, { + managedSkillsDir: path.join(workspaceDir, ".managed"), + bundledSkillsDir, + config: { + skills: { + allowBundled: ["coding-agent"], + }, + }, + }); + const codingAgent = entries.find((entry) => entry.skill.name === "coding-agent"); + expect(codingAgent).toBeDefined(); + + const eligibility = { + remote: { + platforms: [process.platform], + hasBin: () => false, + hasAnyBin: (bins: string[]) => bins.includes("codex"), + }, + }; + const defaultReport = withEnv({ PATH: "" }, () => + buildWorkspaceSkillStatus(workspaceDir, { + entries: [codingAgent as SkillEntry], + config: { + skills: { + allowBundled: ["coding-agent"], + }, + }, + eligibility, + }), + ); + const defaultStatus = defaultReport.skills[0]; + expect(defaultStatus?.eligible).toBe(false); + expect(defaultStatus?.requirements.config).toEqual(["skills.entries.coding-agent.enabled"]); + expect(defaultStatus?.missing.config).toEqual(["skills.entries.coding-agent.enabled"]); + + const enabledReport = withEnv({ PATH: "" }, () => + buildWorkspaceSkillStatus(workspaceDir, { + entries: [codingAgent as SkillEntry], + config: { + skills: { + allowBundled: ["coding-agent"], + entries: { + "coding-agent": { enabled: true }, + }, + }, + }, + eligibility, + }), + ); + const enabledStatus = enabledReport.skills[0]; + expect(enabledStatus?.eligible).toBe(true); + expect(enabledStatus?.missing.config).toEqual([]); + }); + it("does not mark an overridden workspace skill as bundled by bundled name alone", async () => { const bundledDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-bundled-")); tempDirs.push(bundledDir);