fix(skills): trust verified ClawHub source provenance (#93506)

Merged via squash.

Prepared head SHA: a9ec22fa47
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
This commit is contained in:
Vincent Koc
2026-06-16 14:36:42 +08:00
committed by GitHub
parent 4c9e7f6c61
commit 15166e81ca
2 changed files with 208 additions and 8 deletions
+184
View File
@@ -382,6 +382,190 @@ 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";
fetchClawHubSkillDetailMock.mockResolvedValueOnce({
skill: {
slug: "agentreceipt",
displayName: "AgentReceipt",
createdAt: 1,
updatedAt: 2,
},
latestVersion: {
version: "1.0.0",
createdAt: 3,
},
});
fetchClawHubSkillVerificationMock.mockResolvedValueOnce({
schema: "clawhub.skill.verify.v1",
ok: true,
decision: "pass",
reasons: [],
card: { available: true },
artifact: { sourceFingerprint: "source-fp" },
provenance: {
source: "server-resolved-github-import",
kind: "github",
url: sourceUrl,
repo: "openclaw/skills",
ref: "main",
commit: "def456",
path: "agentreceipt",
importedAt: 4,
},
security: { status: "clean" },
signature: { status: "unsigned" },
});
installPackageDirMock.mockImplementationOnce(async (params: { targetDir: string }) => {
await fs.mkdir(params.targetDir, { recursive: true });
await fs.writeFile(path.join(params.targetDir, "SKILL.md"), "# AgentReceipt\n", "utf8");
return { ok: true, targetDir: params.targetDir };
});
try {
const result = await installSkillFromClawHub({
workspaceDir,
slug: "agentreceipt",
version: "1.0.0",
});
expectInstalledSkill(result, {
slug: "agentreceipt",
version: "1.0.0",
targetDir: path.join(workspaceDir, "skills", "agentreceipt"),
});
const lock = JSON.parse(
await fs.readFile(path.join(workspaceDir, ".clawhub", "lock.json"), "utf8"),
) as { skills: Record<string, Record<string, unknown>> };
expect(lock.skills.agentreceipt).toMatchObject({
sourceUrl,
verification: {
provenance: {
source: "server-resolved-github-import",
kind: "github",
url: sourceUrl,
repo: "openclaw/skills",
ref: "main",
commit: "def456",
path: "agentreceipt",
importedAt: 4,
},
},
});
const origin = JSON.parse(
await fs.readFile(
path.join(workspaceDir, "skills", "agentreceipt", ".clawhub", "origin.json"),
"utf8",
),
) as Record<string, unknown>;
expect(origin.sourceUrl).toBe(sourceUrl);
} finally {
await fs.rm(workspaceDir, { recursive: true, force: true });
}
});
it("does not treat detail metadata as verified source provenance", async () => {
const workspaceDir = await tempDirs.make("openclaw-skills-source-");
fetchClawHubSkillDetailMock.mockResolvedValueOnce({
skill: {
slug: "agentreceipt",
displayName: "AgentReceipt",
createdAt: 1,
updatedAt: 2,
sourceUrl: "https://github.com/openclaw/skills/tree/latest/agentreceipt",
},
latestVersion: {
version: "1.0.0",
createdAt: 3,
sourceUrl: "https://github.com/openclaw/skills/tree/latest/agentreceipt",
},
});
fetchClawHubSkillVerificationMock.mockRejectedValueOnce(new Error("verification down"));
installPackageDirMock.mockImplementationOnce(async (params: { targetDir: string }) => {
await fs.mkdir(params.targetDir, { recursive: true });
await fs.writeFile(path.join(params.targetDir, "SKILL.md"), "# AgentReceipt\n", "utf8");
return { ok: true, targetDir: params.targetDir };
});
try {
const result = await installSkillFromClawHub({
workspaceDir,
slug: "agentreceipt",
version: "1.0.0",
});
expectInstalledSkill(result, {
slug: "agentreceipt",
version: "1.0.0",
targetDir: path.join(workspaceDir, "skills", "agentreceipt"),
});
const lock = JSON.parse(
await fs.readFile(path.join(workspaceDir, ".clawhub", "lock.json"), "utf8"),
) as { skills: Record<string, Record<string, unknown>> };
expect(lock.skills.agentreceipt?.sourceUrl).toBeUndefined();
const origin = JSON.parse(
await fs.readFile(
path.join(workspaceDir, "skills", "agentreceipt", ".clawhub", "origin.json"),
"utf8",
),
) as Record<string, unknown>;
expect(origin.sourceUrl).toBeUndefined();
} finally {
await fs.rm(workspaceDir, { recursive: true, force: true });
}
});
it("does not trust URLs from unavailable verification provenance", async () => {
const workspaceDir = await tempDirs.make("openclaw-skills-source-");
fetchClawHubSkillVerificationMock.mockResolvedValueOnce({
schema: "clawhub.skill.verify.v1",
ok: true,
decision: "pass",
reasons: [],
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" },
});
installPackageDirMock.mockImplementationOnce(async (params: { targetDir: string }) => {
await fs.mkdir(params.targetDir, { recursive: true });
await fs.writeFile(path.join(params.targetDir, "SKILL.md"), "# AgentReceipt\n", "utf8");
return { ok: true, targetDir: params.targetDir };
});
try {
const result = await installSkillFromClawHub({
workspaceDir,
slug: "agentreceipt",
version: "1.0.0",
});
expectInstalledSkill(result, {
slug: "agentreceipt",
version: "1.0.0",
targetDir: path.join(workspaceDir, "skills", "agentreceipt"),
});
const lock = JSON.parse(
await fs.readFile(path.join(workspaceDir, ".clawhub", "lock.json"), "utf8"),
) as { skills: Record<string, Record<string, unknown>> };
expect(lock.skills.agentreceipt?.sourceUrl).toBeUndefined();
const origin = JSON.parse(
await fs.readFile(
path.join(workspaceDir, "skills", "agentreceipt", ".clawhub", "origin.json"),
"utf8",
),
) as Record<string, unknown>;
expect(origin.sourceUrl).toBeUndefined();
} finally {
await fs.rm(workspaceDir, { recursive: true, force: true });
}
});
it("keeps installing when the ClawHub verification snapshot is unavailable", async () => {
const workspaceDir = await tempDirs.make("openclaw-skills-lock-");
fetchClawHubSkillVerificationMock.mockRejectedValueOnce(new Error("verification down"));
+24 -8
View File
@@ -273,12 +273,29 @@ function normalizeOptionalStringValue(raw: unknown): string | undefined {
return typeof raw === "string" && raw.trim() ? raw.trim() : undefined;
}
function readSkillDetailSourceUrl(detail: ClawHubSkillDetail | undefined): string | undefined {
const skill = detail?.skill;
if (!skill || typeof skill !== "object") {
function asRecord(raw: unknown): Record<string, unknown> | undefined {
return raw && typeof raw === "object" && !Array.isArray(raw)
? (raw as Record<string, unknown>)
: undefined;
}
function readVerifiedProvenanceSourceUrl(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((skill as { sourceUrl?: unknown }).sourceUrl);
return normalizeOptionalStringValue(provenance.url);
}
function readInstallResolutionSourceUrl(
resolution: Extract<ClawHubSkillInstallResolutionResponse, { ok: true }> | undefined,
): string | undefined {
if (resolution?.installKind !== "github") {
return undefined;
}
return normalizeOptionalStringValue(resolution.github.sourceUrl);
}
function buildDownloadedArtifactLock(
@@ -1106,10 +1123,6 @@ async function performClawHubSkillInstall(
const installedAt = Date.now();
const artifact = buildDownloadedArtifactLock(archive);
const sourceUrl =
latestResolution?.installKind === "github"
? normalizeOptionalStringValue(latestResolution.github.sourceUrl)
: readSkillDetailSourceUrl(detail);
const verificationVersion =
latestResolution?.installKind === "github" && !params.version ? undefined : version;
const [skillFile, verification] = await Promise.all([
@@ -1120,6 +1133,9 @@ async function performClawHubSkillInstall(
baseUrl: params.baseUrl,
}),
]);
const sourceUrl =
readInstallResolutionSourceUrl(latestResolution) ??
readVerifiedProvenanceSourceUrl(verification?.provenance);
await writeClawHubSkillOrigin(install.targetDir, {
version: 1,
registry: resolveClawHubBaseUrl(params.baseUrl),