Skip to content

Commit 7058983

Browse files
committed
fix: require approval before skill workshop apply
1 parent 67bc26c commit 7058983

3 files changed

Lines changed: 49 additions & 9 deletions

File tree

docs/plugins/skill-workshop.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,9 @@ the `apply` action after approval.
420420

421421
Apply a pending proposal.
422422

423+
With `approvalPolicy: "pending"`, this action asks for operator approval before writing the
424+
workspace skill.
425+
423426
```json
424427
{
425428
"action": "apply",

extensions/skill-workshop/index.test.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
22
import os from "node:os";
33
import path from "node:path";
44
import type { AnyAgentTool } from "openclaw/plugin-sdk/agent-runtime";
5+
import type { PluginTrustedToolPolicyRegistration } from "openclaw/plugin-sdk/core";
56
import { createTestPluginApi } from "openclaw/plugin-sdk/plugin-test-api";
67
import { afterEach, describe, expect, it, vi } from "vitest";
78
import plugin, {
@@ -657,18 +658,30 @@ describe("skill-workshop", () => {
657658
const store = new SkillWorkshopStore({ stateDir, workspaceDir });
658659
expect(await store.list("pending")).toHaveLength(1);
659660
expect(await store.list("applied")).toHaveLength(0);
661+
});
660662

661-
const applied = await tool?.execute?.("call-2", {
662-
action: "apply",
663-
id: proposalId,
663+
it("requires operator approval before applying queued proposals in pending mode", async () => {
664+
let trustedPolicy: PluginTrustedToolPolicyRegistration | undefined;
665+
const api = createTestPluginApi({
666+
pluginConfig: { approvalPolicy: "pending" },
667+
registerTrustedToolPolicy(policy) {
668+
trustedPolicy = policy;
669+
},
664670
});
665671

666-
expect(applied?.details).toMatchObject({ status: "applied" });
667-
await expect(
668-
fs.access(path.join(workspaceDir, "skills", "screenshot-asset-workflow", "SKILL.md")),
669-
).resolves.toBeUndefined();
670-
expect(await store.list("pending")).toHaveLength(0);
671-
expect(await store.list("applied")).toHaveLength(1);
672+
plugin.register(api);
673+
674+
const result = await trustedPolicy?.evaluate(
675+
{ toolName: "skill_workshop", params: { action: "apply", id: "proposal-1" } },
676+
{ toolName: "skill_workshop" },
677+
);
678+
679+
expect(result).toMatchObject({
680+
requireApproval: {
681+
title: "Apply workspace skill proposal",
682+
allowedDecisions: ["allow-once", "deny"],
683+
},
684+
});
672685
});
673686

674687
it("uses the reviewer to propose existing skill repairs", async () => {

extensions/skill-workshop/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,30 @@ export default definePluginEntry({
3838
},
3939
);
4040

41+
api.registerTrustedToolPolicy({
42+
id: "skill-workshop-apply-approval",
43+
description: "Require operator approval before applying queued workspace skill proposals.",
44+
evaluate(event) {
45+
const config = resolveCurrentConfig();
46+
if (
47+
!config.enabled ||
48+
config.approvalPolicy === "auto" ||
49+
event.toolName !== "skill_workshop" ||
50+
event.params.action !== "apply"
51+
) {
52+
return undefined;
53+
}
54+
return {
55+
requireApproval: {
56+
title: "Apply workspace skill proposal",
57+
description: "Apply a queued workspace skill proposal.",
58+
severity: "warning",
59+
allowedDecisions: ["allow-once", "deny"],
60+
},
61+
};
62+
},
63+
});
64+
4165
api.on("before_prompt_build", async () => {
4266
const config = resolveCurrentConfig();
4367
if (!config.enabled) {

0 commit comments

Comments
 (0)