Expose verified ClawHub source in skill verify output (#93532)

* fix(skills): expose verified ClawHub source in verify output

* fix(ci): repair verify check regressions

* fix(ci): refresh prompt snapshots

* fix(skills): require pinned ClawHub verify commits
This commit is contained in:
Momo
2026-06-17 16:35:36 +08:00
committed by GitHub
parent 745b011632
commit db4bcd7d09
6 changed files with 201 additions and 8 deletions

View File

@@ -85,6 +85,7 @@ const mocks = vi.hoisted(() => {
installSkillFromSourceMock: vi.fn(),
updateSkillsFromClawHubMock: vi.fn(),
readTrackedClawHubSkillSlugsMock: vi.fn(),
readVerifiedClawHubSkillSourceUrlMock: vi.fn(),
resolveClawHubSkillVerificationTargetMock: vi.fn(),
readClawHubSkillsLockfileStatusSyncMock: vi.fn((..._args: unknown[]) => ({ kind: "missing" })),
resolveClawHubSkillStatusLinkSyncMock: vi.fn(),
@@ -110,6 +111,7 @@ const {
installSkillFromSourceMock,
updateSkillsFromClawHubMock,
readTrackedClawHubSkillSlugsMock,
readVerifiedClawHubSkillSourceUrlMock,
resolveClawHubSkillVerificationTargetMock,
readClawHubSkillsLockfileStatusSyncMock,
resolveClawHubSkillStatusLinkSyncMock,
@@ -191,6 +193,8 @@ vi.mock("../skills/lifecycle/clawhub.js", () => ({
updateSkillsFromClawHub: (...args: unknown[]) => mocks.updateSkillsFromClawHubMock(...args),
readTrackedClawHubSkillSlugs: (...args: unknown[]) =>
mocks.readTrackedClawHubSkillSlugsMock(...args),
readVerifiedClawHubSkillSourceUrl: (...args: unknown[]) =>
mocks.readVerifiedClawHubSkillSourceUrlMock(...args),
resolveClawHubSkillVerificationTarget: (...args: unknown[]) =>
mocks.resolveClawHubSkillVerificationTargetMock(...args),
readClawHubSkillsLockfileStatusSync: (...args: unknown[]) =>
@@ -255,6 +259,7 @@ describe("skills cli commands", () => {
installSkillFromSourceMock.mockReset();
updateSkillsFromClawHubMock.mockReset();
readTrackedClawHubSkillSlugsMock.mockReset();
readVerifiedClawHubSkillSourceUrlMock.mockReset();
resolveClawHubSkillVerificationTargetMock.mockReset();
readClawHubSkillsLockfileStatusSyncMock.mockReset();
resolveClawHubSkillStatusLinkSyncMock.mockReset();
@@ -278,6 +283,7 @@ describe("skills cli commands", () => {
});
updateSkillsFromClawHubMock.mockResolvedValue([]);
readTrackedClawHubSkillSlugsMock.mockResolvedValue([]);
readVerifiedClawHubSkillSourceUrlMock.mockReturnValue(undefined);
readClawHubSkillsLockfileStatusSyncMock.mockReturnValue({ kind: "missing" });
resolveClawHubSkillStatusLinkSyncMock.mockReturnValue(undefined);
resolveLocalSkillCardStatusSyncMock.mockReturnValue(undefined);
@@ -844,6 +850,47 @@ describe("skills cli commands", () => {
});
});
it("includes verified ClawHub source URLs in verify JSON output", async () => {
const provenance = {
source: "server-resolved-github-import",
repo: "openclaw/skills",
commit: "0123456789abcdef0123456789abcdef01234567",
path: "agentreceipt",
};
const verifiedSourceUrl =
"https://github.com/openclaw/skills/tree/0123456789abcdef0123456789abcdef01234567/agentreceipt";
readVerifiedClawHubSkillSourceUrlMock.mockReturnValueOnce(verifiedSourceUrl);
fetchClawHubSkillVerificationMock.mockResolvedValueOnce({
schema: "clawhub.skill.verify.v1",
ok: true,
decision: "pass",
reasons: [],
skill: { slug: "agentreceipt", displayName: "Agent Receipt" },
publisher: { handle: "openclaw" },
version: { version: "1.2.3" },
card: {
available: true,
url: "https://private.example.com/clawhub/api/v1/skills/agentreceipt/card?version=1.2.3",
},
artifact: {
sourceFingerprint: "source-fingerprint",
bundleFingerprints: ["generated-bundle-fingerprint"],
},
provenance,
security: { status: "clean" },
signature: { status: "unsigned" },
});
await runCommand(["skills", "verify", "agentreceipt"]);
expect(readVerifiedClawHubSkillSourceUrlMock).toHaveBeenCalledWith(provenance);
const payload = JSON.parse(runtimeStdout.at(-1) ?? "{}") as {
openclaw?: { verifiedSourceUrl?: string };
};
expect(payload.openclaw?.verifiedSourceUrl).toBe(verifiedSourceUrl);
expect(defaultRuntime.exit).not.toHaveBeenCalled();
});
it("fetches generated Skill Card markdown for --card", async () => {
fetchClawHubSkillVerificationMock.mockResolvedValueOnce({
schema: "clawhub.skill.verify.v1",

View File

@@ -17,6 +17,7 @@ import {
import { defaultRuntime } from "../runtime.js";
import {
installSkillFromClawHub,
readVerifiedClawHubSkillSourceUrl,
readTrackedClawHubSkillSlugs,
resolveClawHubSkillVerificationTarget,
searchSkillsFromClawHub,
@@ -151,6 +152,7 @@ function buildSkillVerificationOutput(
result: ClawHubSkillVerificationResponse,
target: ResolvedClawHubSkillVerificationTarget,
): Record<string, unknown> {
const verifiedSourceUrl = readVerifiedClawHubSkillSourceUrl(result.provenance);
return {
...result,
openclaw: {
@@ -160,6 +162,7 @@ function buildSkillVerificationOutput(
registry: target.resolution.registry,
installedVersion: target.resolution.installedVersion,
},
...(verifiedSourceUrl ? { verifiedSourceUrl } : {}),
},
};
}

View File

@@ -193,4 +193,70 @@ describe("skills verify CLI", () => {
expect(mocks.defaultRuntime.exit).not.toHaveBeenCalled();
expect(mocks.runtimeErrors).toStrictEqual([]);
});
it("surfaces only server-verified source provenance in verify JSON", async () => {
const sourceUrl = "https://github.com/openclaw/skills/tree/main/agentreceipt";
const verifiedSourceUrl =
"https://github.com/openclaw/skills/tree/0123456789abcdef0123456789abcdef01234567/agentreceipt";
mocks.fetchClawHubSkillVerificationMock.mockResolvedValueOnce({
schema: "clawhub.skill.verify.v1",
ok: true,
decision: "pass",
reasons: [],
skill: { slug: "agentreceipt" },
publisher: { handle: "openclaw" },
version: { version: "1.0.0" },
card: { available: true },
artifact: { sourceFingerprint: "source-fp" },
provenance: {
source: "server-resolved-github-import",
kind: "github",
url: sourceUrl,
repo: "openclaw/skills",
ref: "main",
commit: "0123456789abcdef0123456789abcdef01234567",
path: "agentreceipt",
},
security: { status: "clean" },
signature: { status: "unsigned" },
});
await runCommand(["skills", "verify", "agentreceipt"]);
const payload = JSON.parse(mocks.runtimeStdout.at(-1) ?? "{}") as {
openclaw?: { verifiedSourceUrl?: string };
};
expect(payload.openclaw?.verifiedSourceUrl).toBe(verifiedSourceUrl);
expect(mocks.defaultRuntime.exit).not.toHaveBeenCalled();
expect(mocks.runtimeErrors).toStrictEqual([]);
});
it("does not promote unavailable provenance URLs in verify JSON", async () => {
mocks.fetchClawHubSkillVerificationMock.mockResolvedValueOnce({
schema: "clawhub.skill.verify.v1",
ok: true,
decision: "pass",
reasons: [],
skill: { slug: "agentreceipt" },
publisher: { handle: "openclaw" },
version: { version: "1.0.0" },
card: { available: true },
artifact: { sourceFingerprint: "source-fp" },
provenance: {
source: "unavailable",
url: "https://github.com/openclaw/skills/tree/unverified/agentreceipt",
},
security: { status: "clean" },
signature: { status: "unsigned" },
});
await runCommand(["skills", "verify", "agentreceipt"]);
const payload = JSON.parse(mocks.runtimeStdout.at(-1) ?? "{}") as {
openclaw?: { verifiedSourceUrl?: string };
};
expect(payload.openclaw?.verifiedSourceUrl).toBeUndefined();
expect(mocks.defaultRuntime.exit).not.toHaveBeenCalled();
expect(mocks.runtimeErrors).toStrictEqual([]);
});
});