mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-22 13:08:07 +00:00
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:
@@ -107,6 +107,10 @@ Notes:
|
||||
in the shared managed skills directory when combined with `--global`.
|
||||
- `verify <slug>` prints ClawHub's `clawhub.skill.verify.v1` JSON envelope by
|
||||
default. There is no `--json` flag because JSON is already the default.
|
||||
- When ClawHub returns server-resolved source provenance, verify JSON also
|
||||
includes a commit-pinned `openclaw.verifiedSourceUrl`. Unavailable or
|
||||
self-declared source URLs stay only in the raw provenance envelope and are not
|
||||
promoted.
|
||||
- `verify` uses `.clawhub/origin.json` for installed ClawHub skills, so it
|
||||
verifies the installed version against the registry it came from. `--version`
|
||||
and `--tag` override the version selector but keep that installed registry
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,6 +60,7 @@ vi.mock("../../infra/fs-safe.js", () => ({
|
||||
|
||||
const {
|
||||
installSkillFromClawHub,
|
||||
readVerifiedClawHubSkillSourceUrl,
|
||||
resolveClawHubSkillStatusLinkSync,
|
||||
resolveClawHubSkillVerificationTarget,
|
||||
searchSkillsFromClawHub,
|
||||
@@ -385,7 +386,9 @@ describe("skills-clawhub", () => {
|
||||
|
||||
it("persists the source URL from server-resolved verification provenance", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-skills-source-");
|
||||
const sourceUrl = "https://github.com/openclaw/skills/tree/def456/agentreceipt";
|
||||
const sourceUrl = "https://github.com/openclaw/skills/tree/main/agentreceipt";
|
||||
const verifiedSourceUrl =
|
||||
"https://github.com/openclaw/skills/tree/0123456789abcdef0123456789abcdef01234567/agentreceipt";
|
||||
fetchClawHubSkillDetailMock.mockResolvedValueOnce({
|
||||
skill: {
|
||||
slug: "agentreceipt",
|
||||
@@ -411,7 +414,7 @@ describe("skills-clawhub", () => {
|
||||
url: sourceUrl,
|
||||
repo: "openclaw/skills",
|
||||
ref: "main",
|
||||
commit: "def456",
|
||||
commit: "0123456789abcdef0123456789abcdef01234567",
|
||||
path: "agentreceipt",
|
||||
importedAt: 4,
|
||||
},
|
||||
@@ -440,7 +443,7 @@ describe("skills-clawhub", () => {
|
||||
await fs.readFile(path.join(workspaceDir, ".clawhub", "lock.json"), "utf8"),
|
||||
) as { skills: Record<string, Record<string, unknown>> };
|
||||
expect(lock.skills.agentreceipt).toMatchObject({
|
||||
sourceUrl,
|
||||
sourceUrl: verifiedSourceUrl,
|
||||
verification: {
|
||||
provenance: {
|
||||
source: "server-resolved-github-import",
|
||||
@@ -448,7 +451,7 @@ describe("skills-clawhub", () => {
|
||||
url: sourceUrl,
|
||||
repo: "openclaw/skills",
|
||||
ref: "main",
|
||||
commit: "def456",
|
||||
commit: "0123456789abcdef0123456789abcdef01234567",
|
||||
path: "agentreceipt",
|
||||
importedAt: 4,
|
||||
},
|
||||
@@ -460,12 +463,41 @@ describe("skills-clawhub", () => {
|
||||
"utf8",
|
||||
),
|
||||
) as Record<string, unknown>;
|
||||
expect(origin.sourceUrl).toBe(sourceUrl);
|
||||
expect(origin.sourceUrl).toBe(verifiedSourceUrl);
|
||||
} finally {
|
||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("requires a full commit SHA before promoting verified source provenance", () => {
|
||||
const baseProvenance = {
|
||||
source: "server-resolved-github-import",
|
||||
repo: "openclaw/skills",
|
||||
path: "agentreceipt",
|
||||
};
|
||||
|
||||
expect(
|
||||
readVerifiedClawHubSkillSourceUrl({
|
||||
...baseProvenance,
|
||||
commit: "0123456789abcdef0123456789abcdef01234567",
|
||||
}),
|
||||
).toBe(
|
||||
"https://github.com/openclaw/skills/tree/0123456789abcdef0123456789abcdef01234567/agentreceipt",
|
||||
);
|
||||
expect(
|
||||
readVerifiedClawHubSkillSourceUrl({
|
||||
...baseProvenance,
|
||||
commit: "main",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
readVerifiedClawHubSkillSourceUrl({
|
||||
...baseProvenance,
|
||||
commit: "0123456",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not treat detail metadata as verified source provenance", async () => {
|
||||
const workspaceDir = await tempDirs.make("openclaw-skills-source-");
|
||||
fetchClawHubSkillDetailMock.mockResolvedValueOnce({
|
||||
|
||||
@@ -282,14 +282,55 @@ function asRecord(raw: unknown): Record<string, unknown> | undefined {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function readVerifiedProvenanceSourceUrl(raw: unknown): string | undefined {
|
||||
function normalizeGitHubRepoName(raw: unknown): string | undefined {
|
||||
const repo = normalizeOptionalStringValue(raw);
|
||||
if (!repo) {
|
||||
return undefined;
|
||||
}
|
||||
const parts = repo.split("/");
|
||||
if (parts.length !== 2 || parts.some((part) => !/^[A-Za-z0-9._-]+$/.test(part))) {
|
||||
return undefined;
|
||||
}
|
||||
return repo;
|
||||
}
|
||||
|
||||
function normalizeGitHubCommitSegment(raw: unknown): string | undefined {
|
||||
const commit = normalizeOptionalStringValue(raw);
|
||||
if (!commit || !/^[0-9a-f]{40}$/i.test(commit)) {
|
||||
return undefined;
|
||||
}
|
||||
return commit;
|
||||
}
|
||||
|
||||
function buildGitHubTreeUrl(params: { repo: string; commit: string; sourcePath?: string }): string {
|
||||
const [owner, name] = params.repo.split("/") as [string, string];
|
||||
const pathParts = params.sourcePath ? params.sourcePath.split("/") : [];
|
||||
const segments = [owner, name, "tree", params.commit, ...pathParts];
|
||||
return `https://github.com/${segments.map(encodeURIComponent).join("/")}`;
|
||||
}
|
||||
|
||||
export function readVerifiedClawHubSkillSourceUrl(raw: unknown): string | undefined {
|
||||
const provenance = asRecord(raw);
|
||||
// Only this ClawHub variant is server-resolved; other provenance metadata
|
||||
// must not become a trusted source link.
|
||||
if (provenance?.source !== "server-resolved-github-import") {
|
||||
return undefined;
|
||||
}
|
||||
return normalizeOptionalStringValue(provenance.url);
|
||||
const repo = normalizeGitHubRepoName(provenance.repo);
|
||||
const commit = normalizeGitHubCommitSegment(provenance.commit);
|
||||
if (!repo || !commit) {
|
||||
return undefined;
|
||||
}
|
||||
const pathValue = normalizeOptionalStringValue(provenance.path);
|
||||
let sourcePath: string | undefined;
|
||||
if (pathValue) {
|
||||
try {
|
||||
sourcePath = normalizeGitHubSourcePath(pathValue);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return buildGitHubTreeUrl({ repo, commit, ...(sourcePath ? { sourcePath } : {}) });
|
||||
}
|
||||
|
||||
function readInstallResolutionSourceUrl(
|
||||
@@ -1196,7 +1237,7 @@ async function performClawHubSkillInstall(
|
||||
]);
|
||||
const sourceUrl =
|
||||
readInstallResolutionSourceUrl(latestResolution) ??
|
||||
readVerifiedProvenanceSourceUrl(verification?.provenance);
|
||||
readVerifiedClawHubSkillSourceUrl(verification?.provenance);
|
||||
await writeClawHubSkillOrigin(install.targetDir, {
|
||||
version: 1,
|
||||
registry: resolveClawHubBaseUrl(params.baseUrl),
|
||||
|
||||
Reference in New Issue
Block a user