From 4db62dfd4958abc0e34bdfd6065eacb6a4eacbf6 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:15:08 -0700 Subject: [PATCH] Resolve ClawHub downloads by publisher Co-authored-by: kite Agent-Model: codex/gpt-6-sol/medium --- .../unit/routes/skills.source-unavailable.test.js | 5 +++-- backend/routes/skills.ts | 3 ++- backend/services/skillsCatalogService.ts | 10 +++++----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/backend/__tests__/unit/routes/skills.source-unavailable.test.js b/backend/__tests__/unit/routes/skills.source-unavailable.test.js index 65dc0125b5..7d405906ab 100644 --- a/backend/__tests__/unit/routes/skills.source-unavailable.test.js +++ b/backend/__tests__/unit/routes/skills.source-unavailable.test.js @@ -48,8 +48,9 @@ const importBody = (overrides) => ({ podId: POD_ID, name: 'notes', ...overrides const LEGACY_SOURCE = 'https://github.com/openclaw/skills/tree/main/skills/acme/notes/SKILL.md'; const CLAWHUB_META = 'https://clawhub.ai/api/skill?owner=acme&slug=notes'; -const CLAWHUB_ZIP = 'https://clawhub.ai/api/v1/download?slug=notes&version=1.2.0'; -const SOURCE_MISSING = 'This skill is no longer published at its source.'; +const CLAWHUB_ZIP = 'https://clawhub.ai/api/v1/download?slug=notes&ownerHandle=acme&version=1.2.0'; +const SOURCE_MISSING = 'This skill is unavailable from its listed publisher. ' + + 'Choose another skill or ask the publisher for an updated source.'; const SOURCE_UNREACHABLE = "Couldn't reach this skill's source. Try again in a few minutes."; const zipOf = (files) => { diff --git a/backend/routes/skills.ts b/backend/routes/skills.ts index d5b3b66fba..ae5c72565c 100644 --- a/backend/routes/skills.ts +++ b/backend/routes/skills.ts @@ -38,7 +38,8 @@ const { isOutboundUrlRefused } = require('../services/outboundUrlGuard'); const SOURCE_URL_REFUSED = 'Skill source URL must be an https link on github.com or raw.githubusercontent.com.'; // A skill's source not having it is not our server failing (#2189): a 404 when // the source answered without the skill, a 502 when it could not be read. -const SOURCE_MISSING = 'This skill is no longer published at its source.'; +const SOURCE_MISSING = 'This skill is unavailable from its listed publisher. ' + + 'Choose another skill or ask the publisher for an updated source.'; const SOURCE_UNREACHABLE = "Couldn't reach this skill's source. Try again in a few minutes."; const sourceUnavailableReply = (res: Res, error: { missing: boolean; message: string }, what: string) => { console.warn(`[skills] ${what} source unavailable:`, error.message); diff --git a/backend/services/skillsCatalogService.ts b/backend/services/skillsCatalogService.ts index 2fa1b838a7..c66951d06c 100644 --- a/backend/services/skillsCatalogService.ts +++ b/backend/services/skillsCatalogService.ts @@ -332,7 +332,8 @@ export const fetchSkillDirectoryFiles = async (sourceUrl: string, options: Fetch // repo went 404 in early May 2026, but the same skills are served by ClawHub // at canonical URLs: // GET /api/skill?owner=&slug= → metadata JSON (incl. latestVersion.version) -// GET /api/v1/download?slug=&version= → ZIP bundle (SKILL.md + sub-files) +// GET /api/v1/download?slug=&ownerHandle=&version= +// → ZIP bundle (SKILL.md + sub-files) // // We use this both to recover dead catalog entries (presetSkillsAutoImport // rewrites openclaw/skills URLs to ClawHub fetches) and for any future @@ -372,9 +373,8 @@ export const parseLegacyOpenclawSkillsUrl = (sourceUrl: string): { owner: string * Resolve and download a skill from ClawHub. Returns the SKILL.md content + * any sub-files (sub-skills, references/, scripts/) up to size budgets. * - * Note: the metadata endpoint requires `owner` to be passed; the download - * endpoint only takes `slug` (slugs appear to be globally unique). If two - * authors publish the same slug ClawHub presumably disambiguates server-side. + * Both calls must retain the catalog publisher. ClawHub returns 409 for a + * download by slug alone when multiple publishers use that slug. */ export const fetchSkillBundleFromClawHub = async ( owner: string, @@ -412,7 +412,7 @@ export const fetchSkillBundleFromClawHub = async ( let buf: Buffer; try { const dlResp = await fetchBytesFromAllowedHost( - `${CLAWHUB_BASE}/api/v1/download?slug=${safeSlug}&version=${encodeURIComponent(version)}`, + `${CLAWHUB_BASE}/api/v1/download?slug=${safeSlug}&ownerHandle=${safeOwner}&version=${encodeURIComponent(version)}`, { allowedOrigins: CLAWHUB_ORIGINS, maxBytes: CLAWHUB_MAX_ZIP_BYTES }, ); if (!dlResp.ok) throw unavailableForStatus('ClawHub download', dlResp.status);