From 5a1d9c947228a3312f21c693902e5b0d5426d47e Mon Sep 17 00:00:00 2001 From: Patrick Erichsen Date: Thu, 30 Jul 2026 04:49:52 -0700 Subject: [PATCH] fix: preserve canonical skills.sh supplement hashes (#3308) --- .github/workflows/deploy-test.yml | 2 +- server/skillsShCatalogSource.test.ts | 8 +++++--- server/skillsShCatalogSource.ts | 12 +++++++++--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy-test.yml b/.github/workflows/deploy-test.yml index e4d41865..3c7df851 100644 --- a/.github/workflows/deploy-test.yml +++ b/.github/workflows/deploy-test.yml @@ -521,7 +521,7 @@ jobs: .digest.repo == "clawdis" and .digest.githubPath == ".agents/skills/discrawl" and .digest.githubCommit == "690ed564419291ca6e832dc69b53061300075b62" and - .digest.sourceContentHash == "889dc43180b210dbca12f8291e007feb231250ecfdba90c4d3938a18125efb6d" and + .digest.sourceContentHash == "4d3c90262b309ee9ee0ef2f83397fc2a68e8c7d098a8a960fdcbf8908f077f10" and .digest.active == true and .digest.publicVisible == false and .digest.installable == false diff --git a/server/skillsShCatalogSource.test.ts b/server/skillsShCatalogSource.test.ts index 742994d5..5ed15db2 100644 --- a/server/skillsShCatalogSource.test.ts +++ b/server/skillsShCatalogSource.test.ts @@ -157,8 +157,9 @@ describe("skills.sh Vercel source boundary", () => { }); }); - it("builds a hidden controlled observation only from exact immutable content", () => { + it("keeps the canonical folder hash while verifying exact controlled detail content", () => { const fullContent = "# Controlled\n\nThis content is longer than the retained prefix."; + const detailContentHash = createHash("sha256").update(fullContent).digest("hex"); const supplement = { externalId: "owner/repo/controlled", owner: "owner", @@ -169,7 +170,8 @@ describe("skills.sh Vercel source boundary", () => { githubPath: "skills/controlled", detailPath: "skills/controlled/SKILL.md", githubCommit: "0123456789abcdef0123456789abcdef01234567", - sourceContentHash: createHash("sha256").update(fullContent).digest("hex"), + sourceContentHash: "a".repeat(64), + detailContentHash, }; expect( @@ -203,7 +205,7 @@ describe("skills.sh Vercel source boundary", () => { }); expect(() => buildSkillsShMirrorControlledObservation( - { ...supplement, sourceContentHash: "0".repeat(64) }, + { ...supplement, detailContentHash: "0".repeat(64) }, fullContent, Buffer.byteLength(fullContent), 16, diff --git a/server/skillsShCatalogSource.ts b/server/skillsShCatalogSource.ts index bc913b01..2b383e49 100644 --- a/server/skillsShCatalogSource.ts +++ b/server/skillsShCatalogSource.ts @@ -40,6 +40,7 @@ const SKILLS_SH_MIRROR_CONTROLLED_SUPPLEMENTS = [ detailPath: "skills/html/SKILL.md", githubCommit: "050daba89f6b6636470add5cb300aac46a412cf8", sourceContentHash: "a47adb2c1ac33c088f664b5187971b63d2b958a7b9f01516d26005ca941a108f", + detailContentHash: "42d2e89358ea927441dfede45c3b0cf89a21603bc7c32246f098d24a9cbea1ff", }, { externalId: "steipete/clawdis/discrawl", @@ -51,7 +52,8 @@ const SKILLS_SH_MIRROR_CONTROLLED_SUPPLEMENTS = [ githubPath: ".agents/skills/discrawl", detailPath: ".agents/skills/discrawl/SKILL.md", githubCommit: "690ed564419291ca6e832dc69b53061300075b62", - sourceContentHash: "889dc43180b210dbca12f8291e007feb231250ecfdba90c4d3938a18125efb6d", + sourceContentHash: "4d3c90262b309ee9ee0ef2f83397fc2a68e8c7d098a8a960fdcbf8908f077f10", + detailContentHash: "889dc43180b210dbca12f8291e007feb231250ecfdba90c4d3938a18125efb6d", }, ] as const; @@ -66,6 +68,7 @@ type SkillsShMirrorControlledSupplement = { detailPath: string; githubCommit: string; sourceContentHash: string; + detailContentHash: string; }; export const SKILLS_SH_MIRROR_CONTROLLED_SUPPLEMENT_COUNT = @@ -2389,10 +2392,13 @@ export function buildSkillsShMirrorControlledObservation( ) { assertIntegerInRange("sourceBytes", sourceBytes, 0, MAX_CONTROLLED_DETAIL_BYTES); assertIntegerInRange("maxDetailBytes", maxDetailBytes, 1, 64 * 1024); - const sourceContentHash = sha256Hex(fullContent); - if (sourceContentHash !== supplement.sourceContentHash) { + const detailContentHash = sha256Hex(fullContent); + if (detailContentHash !== supplement.detailContentHash) { throw new Error(`controlled skills.sh mirror source hash changed: ${supplement.externalId}`); } + // Mirror and claim identity use the canonical whole-folder hash. The raw + // detail response is verified separately because it contains only SKILL.md. + const sourceContentHash = supplement.sourceContentHash; const content = truncateUtf8(fullContent, maxDetailBytes); const unavailable = { status: "unavailable" as const }; return {