Skip to content

Commit 6343e14

Browse files
fix(skills): accept owner-qualified verify refs (#95992)
Merged via squash. Prepared head SHA: de9f1e5 Co-authored-by: Patrick-Erichsen <[email protected]> Co-authored-by: Patrick-Erichsen <[email protected]> Reviewed-by: @Patrick-Erichsen
1 parent 5971319 commit 6343e14

8 files changed

Lines changed: 293 additions & 16 deletions

File tree

docs/clawhub/cli.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ OpenClaw agent or Gateway.
2525
openclaw skills search "calendar"
2626
openclaw skills install @owner/<slug>
2727
openclaw skills update @owner/<slug>
28-
openclaw skills verify <slug>
28+
openclaw skills verify @owner/<slug>
2929

3030
openclaw plugins search "calendar"
3131
openclaw plugins install clawhub:<package>

docs/cli/skills.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ openclaw skills update @owner/<slug> --global
3838
openclaw skills update --all
3939
openclaw skills update --all --agent <id>
4040
openclaw skills update --all --global
41-
openclaw skills verify <slug>
42-
openclaw skills verify <slug> --version <version>
43-
openclaw skills verify <slug> --tag <tag>
44-
openclaw skills verify <slug> --card
45-
openclaw skills verify <slug> --global
41+
openclaw skills verify @owner/<slug>
42+
openclaw skills verify @owner/<slug> --version <version>
43+
openclaw skills verify @owner/<slug> --tag <tag>
44+
openclaw skills verify @owner/<slug> --card
45+
openclaw skills verify @owner/<slug> --global
4646
openclaw skills list
4747
openclaw skills list --eligible
4848
openclaw skills list --json
@@ -105,8 +105,11 @@ Notes:
105105
target the shared managed skills directory instead of the workspace.
106106
- `update --all` updates tracked ClawHub installs in the selected workspace, or
107107
in the shared managed skills directory when combined with `--global`.
108-
- `verify <slug>` prints ClawHub's `clawhub.skill.verify.v1` JSON envelope by
109-
default. There is no `--json` flag because JSON is already the default.
108+
- `verify @owner/<slug>` prints ClawHub's `clawhub.skill.verify.v1` JSON
109+
envelope by default. There is no `--json` flag because JSON is already the
110+
default. Bare slugs remain accepted for compatibility when the skill is
111+
already installed or unambiguous, but owner-qualified refs avoid publisher
112+
ambiguity.
110113
- When ClawHub returns server-resolved source provenance, verify JSON also
111114
includes a commit-pinned `openclaw.verifiedSourceUrl`. Unavailable or
112115
self-declared source URLs stay only in the raw provenance envelope and are not

docs/tools/skills.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,8 @@ publish and sync.
152152
| Update all workspace skills | `openclaw skills update --all` |
153153
| Update a shared managed skill | `openclaw skills update @owner/<slug> --global` |
154154
| Update all shared managed skills | `openclaw skills update --all --global` |
155-
| Verify a skill's trust envelope | `openclaw skills verify <slug>` |
156-
| Print the generated Skill Card | `openclaw skills verify <slug> --card` |
155+
| Verify a skill's trust envelope | `openclaw skills verify @owner/<slug>` |
156+
| Print the generated Skill Card | `openclaw skills verify @owner/<slug> --card` |
157157
| Publish / sync via ClawHub CLI | `clawhub sync --all` |
158158

159159
<AccordionGroup>
@@ -171,9 +171,11 @@ publish and sync.
171171

172172
</Accordion>
173173
<Accordion title="Verification and security scanning">
174-
`openclaw skills verify <slug>` asks ClawHub for the skill's
174+
`openclaw skills verify @owner/<slug>` asks ClawHub for the skill's
175175
`clawhub.skill.verify.v1` trust envelope. Installed ClawHub skills verify
176176
against the version and registry recorded in `.clawhub/origin.json`.
177+
Bare slugs remain accepted for existing installed or unambiguous skills, but
178+
owner-qualified refs avoid publisher ambiguity.
177179

178180
ClawHub skill pages expose the latest security scan state before install,
179181
with detail pages for VirusTotal, ClawScan, and static analysis. The

src/cli/skills-cli.commands.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,24 @@ describe("skills cli commands", () => {
449449
expect(help).not.toContain("openclaw skills install weather");
450450
});
451451

452+
it("documents owner-qualified ClawHub verify refs in command help", () => {
453+
const skillsCommand = createProgram().commands.find((command) => command.name() === "skills");
454+
const verifyCommand = skillsCommand?.commands.find((command) => command.name() === "verify");
455+
const output: string[] = [];
456+
457+
verifyCommand?.configureOutput({
458+
writeOut: (value) => output.push(value),
459+
writeErr: (value) => output.push(value),
460+
});
461+
verifyCommand?.outputHelp();
462+
const help = output.join("");
463+
464+
expect(help).toContain("<skill-ref>");
465+
expect(help).toContain("@owner/slug");
466+
expect(help).toContain("openclaw skills verify @owner/weather");
467+
expect(help).not.toContain("openclaw skills verify weather");
468+
});
469+
452470
it("installs a skill from a git source into the active workspace", async () => {
453471
installSkillFromSourceMock.mockResolvedValue({
454472
ok: true,
@@ -935,6 +953,44 @@ describe("skills cli commands", () => {
935953
});
936954
});
937955

956+
it("passes owner-qualified verify refs and selectors through the resolver", async () => {
957+
resolveClawHubSkillVerificationTargetMock.mockResolvedValueOnce({
958+
ok: true,
959+
slug: "weather",
960+
ownerHandle: "demo-owner",
961+
baseUrl: "https://private.example.com/clawhub",
962+
version: undefined,
963+
tag: "latest",
964+
resolution: {
965+
source: "registry",
966+
selector: "tag",
967+
registry: "https://private.example.com/clawhub",
968+
skillDir: undefined,
969+
installedVersion: undefined,
970+
},
971+
});
972+
973+
await runCommand(["skills", "verify", "@demo-owner/weather", "--tag", "latest", "--card"]);
974+
975+
expect(resolveClawHubSkillVerificationTargetMock).toHaveBeenCalledWith({
976+
workspaceDir: "/tmp/workspace",
977+
slug: "@demo-owner/weather",
978+
version: undefined,
979+
tag: "latest",
980+
});
981+
expect(fetchClawHubSkillVerificationMock).toHaveBeenCalledWith({
982+
slug: "weather",
983+
ownerHandle: "demo-owner",
984+
version: undefined,
985+
tag: "latest",
986+
baseUrl: "https://private.example.com/clawhub",
987+
});
988+
expect(fetchClawHubSkillCardMock).toHaveBeenCalledWith({
989+
url: "https://private.example.com/clawhub/api/v1/skills/agentreceipt/card?version=1.2.3",
990+
baseUrl: "https://private.example.com/clawhub",
991+
});
992+
});
993+
938994
it("passes explicit verify selectors and shared workspace options to the resolver", async () => {
939995
await runCommand(["skills", "verify", "agentreceipt", "--version", "2.0.0", "--global"]);
940996

src/cli/skills-cli.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ export function registerSkillsCli(program: Command) {
415415
skills
416416
.command("update")
417417
.description("Update ClawHub-installed skills in the active or shared managed directory")
418-
.argument("[slug]", "Single skill slug")
418+
.argument("[skill-ref]", "Single ClawHub skill ref (@owner/slug)")
419419
.option("--all", "Update all tracked ClawHub skills", false)
420420
.option(
421421
"--force-install",
@@ -487,7 +487,7 @@ export function registerSkillsCli(program: Command) {
487487
skills
488488
.command("verify")
489489
.description("Verify a ClawHub skill with ClawHub")
490-
.argument("<slug>", "ClawHub skill slug")
490+
.argument("<skill-ref>", "ClawHub skill ref (@owner/slug)")
491491
.option("--version <version>", "Verify a specific version")
492492
.option("--tag <tag>", "Verify a dist tag")
493493
.option("--card", "Print the generated Skill Card Markdown", false)
@@ -497,6 +497,7 @@ export function registerSkillsCli(program: Command) {
497497
false,
498498
)
499499
.option("--agent <id>", "Target agent workspace (defaults to cwd-inferred, then default agent)")
500+
.addHelpText("after", "\nExamples:\n openclaw skills verify @owner/weather\n")
500501
.action(
501502
async (
502503
slug: string,

src/cli/skills-cli.verify.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,80 @@ describe("skills verify CLI", () => {
194194
expect(mocks.runtimeErrors).toStrictEqual([]);
195195
});
196196

197+
it("verifies owner-qualified registry refs with explicit versions", async () => {
198+
mocks.fetchClawHubSkillVerificationMock.mockResolvedValueOnce({
199+
schema: "clawhub.skill.verify.v1",
200+
ok: true,
201+
decision: "pass",
202+
reasons: [],
203+
skill: { slug: "weather" },
204+
publisher: { handle: "demo-owner" },
205+
version: { version: "2.0.0" },
206+
card: { available: true },
207+
artifact: { sourceFingerprint: "source-fp" },
208+
provenance: null,
209+
security: { status: "clean" },
210+
signature: { status: "unsigned" },
211+
});
212+
213+
await runCommand(["skills", "verify", "@demo-owner/weather", "--version", "2.0.0"]);
214+
215+
expect(mocks.fetchClawHubSkillVerificationMock).toHaveBeenCalledWith({
216+
slug: "weather",
217+
ownerHandle: "demo-owner",
218+
version: "2.0.0",
219+
tag: undefined,
220+
baseUrl: "https://clawhub.ai",
221+
});
222+
const payload = JSON.parse(mocks.runtimeStdout.at(-1) ?? "{}") as {
223+
openclaw?: { resolution?: { source?: string; selector?: string } };
224+
};
225+
expect(payload.openclaw?.resolution).toMatchObject({
226+
source: "registry",
227+
selector: "version",
228+
});
229+
expect(mocks.defaultRuntime.exit).not.toHaveBeenCalled();
230+
expect(mocks.runtimeErrors).toStrictEqual([]);
231+
});
232+
233+
it("prints cards for owner-qualified tag verification", async () => {
234+
mocks.fetchClawHubSkillVerificationMock.mockResolvedValueOnce({
235+
schema: "clawhub.skill.verify.v1",
236+
ok: true,
237+
decision: "pass",
238+
reasons: [],
239+
skill: { slug: "weather" },
240+
publisher: { handle: "demo-owner" },
241+
version: { version: "2.0.0" },
242+
card: {
243+
available: true,
244+
url: "https://cards.example.test/generated/weather.md",
245+
},
246+
artifact: { sourceFingerprint: "source-fp" },
247+
provenance: null,
248+
security: { status: "clean" },
249+
signature: { status: "unsigned" },
250+
});
251+
mocks.fetchClawHubSkillCardMock.mockResolvedValueOnce("# Weather\n");
252+
253+
await runCommand(["skills", "verify", "@demo-owner/weather", "--tag", "latest", "--card"]);
254+
255+
expect(mocks.fetchClawHubSkillVerificationMock).toHaveBeenCalledWith({
256+
slug: "weather",
257+
ownerHandle: "demo-owner",
258+
version: undefined,
259+
tag: "latest",
260+
baseUrl: "https://clawhub.ai",
261+
});
262+
expect(mocks.fetchClawHubSkillCardMock).toHaveBeenCalledWith({
263+
url: "https://cards.example.test/generated/weather.md",
264+
baseUrl: "https://clawhub.ai",
265+
});
266+
expect(mocks.runtimeStdout.at(-1)).toBe("# Weather");
267+
expect(mocks.defaultRuntime.exit).not.toHaveBeenCalled();
268+
expect(mocks.runtimeErrors).toStrictEqual([]);
269+
});
270+
197271
it("surfaces only server-verified source provenance in verify JSON", async () => {
198272
const sourceUrl = "https://github.com/openclaw/skills/tree/main/agentreceipt";
199273
const verifiedSourceUrl =

src/skills/lifecycle/clawhub.test.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,66 @@ describe("skills-clawhub", () => {
13291329
}
13301330
});
13311331

1332+
it("accepts owner-qualified installed verification targets", async () => {
1333+
const workspaceDir = await tempDirs.make("openclaw-skill-verify-");
1334+
try {
1335+
await writeClawHubOriginFixture({
1336+
workspaceDir,
1337+
slug: "weather",
1338+
ownerHandle: "demo-owner",
1339+
registry: "https://private.example.com/clawhub",
1340+
installedVersion: "2.0.0",
1341+
});
1342+
1343+
await expect(
1344+
resolveClawHubSkillVerificationTarget({
1345+
workspaceDir,
1346+
slug: "@demo-owner/weather",
1347+
}),
1348+
).resolves.toMatchObject({
1349+
ok: true,
1350+
slug: "weather",
1351+
ownerHandle: "demo-owner",
1352+
baseUrl: "https://private.example.com/clawhub",
1353+
version: "2.0.0",
1354+
tag: undefined,
1355+
resolution: {
1356+
source: "installed",
1357+
selector: "installed-version",
1358+
registry: "https://private.example.com/clawhub",
1359+
installedVersion: "2.0.0",
1360+
},
1361+
});
1362+
} finally {
1363+
await fs.rm(workspaceDir, { recursive: true, force: true });
1364+
}
1365+
});
1366+
1367+
it("rejects owner-qualified installed verification when the owner differs", async () => {
1368+
const workspaceDir = await tempDirs.make("openclaw-skill-verify-");
1369+
try {
1370+
await writeClawHubOriginFixture({
1371+
workspaceDir,
1372+
slug: "weather",
1373+
ownerHandle: "demo-owner",
1374+
});
1375+
1376+
const result = await resolveClawHubSkillVerificationTarget({
1377+
workspaceDir,
1378+
slug: "@other-owner/weather",
1379+
});
1380+
1381+
expect(result.ok).toBe(false);
1382+
if (result.ok) {
1383+
throw new Error("expected owner mismatch failure");
1384+
}
1385+
expect(result.error).toContain("is tracked as @demo-owner/weather");
1386+
expect(result.error).toContain("not @other-owner/weather");
1387+
} finally {
1388+
await fs.rm(workspaceDir, { recursive: true, force: true });
1389+
}
1390+
});
1391+
13321392
it("keeps the installed registry when an explicit version overrides the installed version", async () => {
13331393
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-skill-verify-"));
13341394
try {
@@ -1634,6 +1694,79 @@ describe("skills-clawhub", () => {
16341694
}
16351695
});
16361696

1697+
it("uses owner-qualified registry verification targets", async () => {
1698+
const workspaceDir = await tempDirs.make("openclaw-skill-verify-");
1699+
resolveClawHubBaseUrlMock.mockReturnValueOnce("https://configured.example.com/clawhub");
1700+
try {
1701+
await expect(
1702+
resolveClawHubSkillVerificationTarget({
1703+
workspaceDir,
1704+
slug: "@demo-owner/weather",
1705+
baseUrl: "https://configured.example.com/clawhub/",
1706+
}),
1707+
).resolves.toEqual({
1708+
ok: true,
1709+
slug: "weather",
1710+
ownerHandle: "demo-owner",
1711+
baseUrl: "https://configured.example.com/clawhub",
1712+
version: undefined,
1713+
tag: undefined,
1714+
resolution: {
1715+
source: "registry",
1716+
selector: "latest",
1717+
registry: "https://configured.example.com/clawhub",
1718+
skillDir: undefined,
1719+
installedVersion: undefined,
1720+
},
1721+
});
1722+
} finally {
1723+
await fs.rm(workspaceDir, { recursive: true, force: true });
1724+
}
1725+
});
1726+
1727+
it("keeps owner-qualified registry selectors for explicit versions and tags", async () => {
1728+
const workspaceDir = await tempDirs.make("openclaw-skill-verify-");
1729+
try {
1730+
await expect(
1731+
resolveClawHubSkillVerificationTarget({
1732+
workspaceDir,
1733+
slug: "@demo-owner/weather",
1734+
version: "2.0.0",
1735+
}),
1736+
).resolves.toMatchObject({
1737+
ok: true,
1738+
slug: "weather",
1739+
ownerHandle: "demo-owner",
1740+
version: "2.0.0",
1741+
tag: undefined,
1742+
resolution: {
1743+
source: "registry",
1744+
selector: "version",
1745+
},
1746+
});
1747+
1748+
await expect(
1749+
resolveClawHubSkillVerificationTarget({
1750+
workspaceDir,
1751+
slug: "@demo-owner/weather",
1752+
tag: "beta",
1753+
}),
1754+
).resolves.toMatchObject({
1755+
ok: true,
1756+
slug: "weather",
1757+
ownerHandle: "demo-owner",
1758+
version: undefined,
1759+
tag: "beta",
1760+
resolution: {
1761+
source: "registry",
1762+
selector: "tag",
1763+
},
1764+
});
1765+
} finally {
1766+
await fs.rm(workspaceDir, { recursive: true, force: true });
1767+
}
1768+
});
1769+
16371770
it("fails clearly when installed origin metadata is malformed", async () => {
16381771
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-skill-verify-"));
16391772
try {

0 commit comments

Comments
 (0)