mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 19:35:28 -06:00
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:
@@ -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"));
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user