feat(skills): diagnose skill_workshop hidden by tool policy (#100654)

* feat(skills): diagnose skill_workshop hidden by tool policy (#87570)

Workshop can be enabled and auto-capturing while tools.profile hides the
skill_workshop tool; every inspection surface looked healthy. plugins
inspect and openclaw doctor now name the excluding policy layer (global/
agent/provider profile, allowlist, denylist) and the exact alsoAllow
grant to add, via a shared resolveSkillWorkshopToolPolicyAvailability
helper that /learn's guard now reuses instead of composing policy
itself. Diagnosis only; no policy behavior change.

* ci: retrigger
This commit is contained in:
Peter Steinberger
2026-07-06 07:21:08 +01:00
committed by GitHub
parent eda99cb8df
commit 133ca4b469
12 changed files with 570 additions and 38 deletions

View File

@@ -1,5 +1,6 @@
// Plugins CLI list tests cover plugin listing output and installed-state formatting.
import { beforeEach, describe, expect, it } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { createPluginRecord } from "../plugins/status.test-helpers.js";
import {
buildPluginDiagnosticsReport,
@@ -17,9 +18,18 @@ import {
setInstalledPluginIndexInstallRecords,
} from "./plugins-cli-test-helpers.js";
const workshopMocks = vi.hoisted(() => ({
detectToolPolicyDiagnostic: vi.fn(),
}));
vi.mock("../skills/workshop/tool-policy-diagnostic.js", () => ({
detectSkillWorkshopToolPolicyDiagnostic: workshopMocks.detectToolPolicyDiagnostic,
}));
describe("plugins cli list", () => {
beforeEach(() => {
resetPluginsCliTestState();
workshopMocks.detectToolPolicyDiagnostic.mockReset();
});
it("includes imported state in JSON output", async () => {
@@ -620,4 +630,39 @@ describe("plugins cli list", () => {
expect(buildPluginDiagnosticsReport).not.toHaveBeenCalled();
expect(runtimeErrors.at(-1)).toContain("Plugin not found: missing-plugin");
});
it("explains a policy-hidden built-in Skill Workshop at the legacy inspect surface", async () => {
const config: OpenClawConfig = {
tools: { profile: "messaging" },
};
loadConfig.mockReturnValue(config);
workshopMocks.detectToolPolicyDiagnostic.mockReturnValue({
agentId: "main",
source: "tools.profile",
detail: 'tools.profile: "messaging" does not include "skill_workshop".',
fix: 'Add tools.alsoAllow: ["skill_workshop"].',
message:
'Skill Workshop is active, but "skill_workshop" is hidden for agent "main": tools.profile: "messaging" does not include "skill_workshop". Add tools.alsoAllow: ["skill_workshop"].',
});
buildPluginSnapshotReport.mockReturnValue({
plugins: [],
diagnostics: [],
});
await expect(runPluginsCommand(["plugins", "inspect", "skill-workshop"])).rejects.toThrow(
"__exit__:1",
);
expect(runtimeErrors.at(-1)).toContain(
"Skill Workshop is built into OpenClaw, not a plugin; configure it under skills.workshop.",
);
expect(workshopMocks.detectToolPolicyDiagnostic).toHaveBeenCalledWith({
config,
workshopEnabled: true,
});
expect(runtimeErrors.at(-1)).toContain(
'tools.profile: "messaging" does not include "skill_workshop".',
);
expect(runtimeErrors.at(-1)).toContain('Add tools.alsoAllow: ["skill_workshop"].');
});
});