From dc6d9973e925aef23b822346517878336c7d8eac Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 2 May 2026 14:23:07 -0700 Subject: [PATCH] fix(plugins): tolerate missing clawhub artifact resolver --- CHANGELOG.md | 1 + scripts/e2e/lib/clawhub-fixture-server.cjs | 24 +++++++ src/plugins/clawhub.test.ts | 71 +++++++++++++++++++ src/plugins/clawhub.ts | 67 +++++++++++++++-- .../plugin-prerelease-test-plan.test.ts | 1 + 5 files changed, 158 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93ec2e2d284..abae5ac0d67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Plugins/externalization: keep ACPX, Google Chat, and LINE publishable plugin dist trees out of the core npm package file list. +- Plugins/ClawHub: fall back to version metadata when the artifact resolver route is missing and keep the Docker ClawHub fixture aligned with npm-pack artifact resolution, avoiding false version-not-found failures during plugin install validation. Thanks @vincentkoc. - Status/channels: show configured channels in `openclaw status` and config-only `openclaw channels status` output even when the Gateway is unreachable, avoiding empty Channels tables on WSL and other no-Gateway paths. Thanks @vincentkoc. - Plugins/ClawHub: explain unavailable explicit ClawHub ClawPack artifact downloads with a temporary npm install hint while ClawHub artifact routing rolls out. Thanks @vincentkoc. - Media: accept home-relative `MEDIA:~/...` attachment paths while preserving existing file-read policy, traversal checks, and media type validation. Fixes #73796. Thanks @fabkury. diff --git a/scripts/e2e/lib/clawhub-fixture-server.cjs b/scripts/e2e/lib/clawhub-fixture-server.cjs index 9ac28598acc..7e8e5f8a654 100644 --- a/scripts/e2e/lib/clawhub-fixture-server.cjs +++ b/scripts/e2e/lib/clawhub-fixture-server.cjs @@ -338,6 +338,23 @@ async function main() { response.writeHead(status, { "content-type": "application/json" }); response.end(`${JSON.stringify(value)}\n`); }; + const artifactResolverDetail = { + package: versionDetail.package ?? { + name: packageName, + displayName: packageDetail.package?.displayName ?? "OpenClaw Kitchen Sink", + family: packageDetail.package?.family ?? "code-plugin", + }, + version: versionDetail.version, + artifact: { + source: "clawhub", + artifactKind: "npm-pack", + packageName, + version: fixture.version, + artifactSha256: clawpack.clawpackSha256, + npmIntegrity: clawpack.npmIntegrity, + npmShasum: clawpack.npmShasum, + }, + }; const server = http.createServer((request, response) => { const url = new URL(request.url, "http://127.0.0.1"); @@ -357,6 +374,13 @@ async function main() { json(response, versionDetail); return; } + if ( + url.pathname === + `/api/v1/packages/${encodeURIComponent(packageName)}/versions/${fixture.version}/artifact` + ) { + json(response, artifactResolverDetail); + return; + } if ( betaStatus !== undefined && url.pathname === `/api/v1/packages/${encodeURIComponent(packageName)}/versions/beta` diff --git a/src/plugins/clawhub.test.ts b/src/plugins/clawhub.test.ts index 1a265bca60a..6d75275cb09 100644 --- a/src/plugins/clawhub.test.ts +++ b/src/plugins/clawhub.test.ts @@ -463,6 +463,77 @@ describe("installPluginFromClawHub", () => { ); }); + it("falls back to version metadata when the ClawHub artifact resolver route is missing", async () => { + fetchClawHubPackageArtifactMock.mockRejectedValueOnce( + new ClawHubRequestError({ + path: "/api/v1/packages/demo/versions/2026.3.22/artifact", + status: 404, + body: "Not Found", + }), + ); + fetchClawHubPackageVersionMock.mockResolvedValueOnce({ + package: { + name: "demo", + displayName: "Demo", + family: "code-plugin", + }, + version: { + version: "2026.3.22", + createdAt: 0, + changelog: "", + compatibility: { + pluginApiRange: ">=2026.3.22", + minGatewayVersion: "2026.3.0", + }, + artifact: { + kind: "npm-pack", + format: "tgz", + sha256: DEMO_CLAWPACK_SHA256, + size: 4096, + npmIntegrity: "sha512-clawpack", + npmShasum: "1".repeat(40), + }, + }, + }); + downloadClawHubPackageArchiveMock.mockResolvedValueOnce({ + archivePath: "/tmp/clawhub-demo/demo-2026.3.22.tgz", + integrity: DEMO_CLAWPACK_INTEGRITY, + sha256Hex: DEMO_CLAWPACK_SHA256, + artifact: "clawpack", + clawpackHeaderSha256: DEMO_CLAWPACK_SHA256, + npmIntegrity: "sha512-clawpack", + npmShasum: "1".repeat(40), + cleanup: archiveCleanupMock, + }); + + const result = await installPluginFromClawHub({ + spec: "clawhub:demo", + baseUrl: "https://clawhub.ai", + }); + + expect(result).toMatchObject({ + ok: true, + clawhub: { + artifactKind: "npm-pack", + npmIntegrity: "sha512-clawpack", + clawpackSha256: DEMO_CLAWPACK_SHA256, + }, + }); + expect(fetchClawHubPackageVersionMock).toHaveBeenCalledWith( + expect.objectContaining({ + name: "demo", + version: "2026.3.22", + }), + ); + expect(downloadClawHubPackageArchiveMock).toHaveBeenCalledWith( + expect.objectContaining({ + artifact: "clawpack", + name: "demo", + version: "2026.3.22", + }), + ); + }); + it("installs ClawPack artifacts when version metadata has no legacy archive hash", async () => { fetchClawHubPackageVersionMock.mockResolvedValueOnce({ version: { diff --git a/src/plugins/clawhub.ts b/src/plugins/clawhub.ts index 0eb2eeb4881..d37ef6d36f5 100644 --- a/src/plugins/clawhub.ts +++ b/src/plugins/clawhub.ts @@ -15,6 +15,7 @@ import { downloadClawHubPackageArchive, fetchClawHubPackageArtifact, fetchClawHubPackageDetail, + fetchClawHubPackageVersion, normalizeClawHubSha256Integrity, normalizeClawHubSha256Hex, parseClawHubPluginSpec, @@ -330,6 +331,38 @@ function mapClawHubRequestError( return buildClawHubInstallFailure(formatErrorMessage(error)); } +function isMissingArtifactResolverRoute(error: unknown): boolean { + return ( + error instanceof ClawHubRequestError && + error.status === 404 && + error.requestPath.endsWith("/artifact") + ); +} + +function buildArtifactResolverResponseFromVersion(params: { + detail: ClawHubPackageDetail; + versionDetail: ClawHubPackageVersion; +}): ClawHubPackageArtifactResolverResponse { + const packageDetail = params.detail.package; + const versionPackage = params.versionDetail.package; + return { + package: versionPackage + ? { + name: versionPackage.name, + displayName: versionPackage.displayName, + family: versionPackage.family, + } + : packageDetail + ? { + name: packageDetail.name, + displayName: packageDetail.displayName, + family: packageDetail.family, + } + : null, + version: params.versionDetail.version, + }; +} + function formatClawHubClawPackDownloadError(params: { error: unknown; packageName: string; @@ -783,7 +816,7 @@ async function resolveCompatiblePackageVersion(params: { CLAWHUB_INSTALL_ERROR_CODE.NO_INSTALLABLE_VERSION, ); } - let artifactResponse; + let artifactResponse: ClawHubPackageArtifactResolverResponse; try { artifactResponse = await fetchClawHubPackageArtifact({ name: params.detail.package?.name ?? "", @@ -793,11 +826,33 @@ async function resolveCompatiblePackageVersion(params: { timeoutMs: params.timeoutMs, }); } catch (error) { - return mapClawHubRequestError(error, { - stage: "version", - name: params.detail.package?.name ?? "unknown", - version: requestedVersion, - }); + if (isMissingArtifactResolverRoute(error)) { + try { + const versionDetail = await fetchClawHubPackageVersion({ + name: params.detail.package?.name ?? "", + version: requestedVersion, + baseUrl: params.baseUrl, + token: params.token, + timeoutMs: params.timeoutMs, + }); + artifactResponse = buildArtifactResolverResponseFromVersion({ + detail: params.detail, + versionDetail, + }); + } catch (versionError) { + return mapClawHubRequestError(versionError, { + stage: "version", + name: params.detail.package?.name ?? "unknown", + version: requestedVersion, + }); + } + } else { + return mapClawHubRequestError(error, { + stage: "version", + name: params.detail.package?.name ?? "unknown", + version: requestedVersion, + }); + } } const artifactVersion = readArtifactResolverVersion(artifactResponse, requestedVersion); const resolvedVersion = normalizeOptionalString(artifactVersion.version) ?? requestedVersion; diff --git a/test/scripts/plugin-prerelease-test-plan.test.ts b/test/scripts/plugin-prerelease-test-plan.test.ts index 938c9f68e7a..349e8fd7259 100644 --- a/test/scripts/plugin-prerelease-test-plan.test.ts +++ b/test/scripts/plugin-prerelease-test-plan.test.ts @@ -173,6 +173,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => { expect(assertionsScript).toContain('node_modules", "openclaw'); expect(fixtureServer).toContain('"is-number": "7.0.0"'); expect(fixtureServer).toContain('openclaw: ">=2026.4.11"'); + expect(fixtureServer).toContain("/versions/${fixture.version}/artifact"); }); it("wires the full plugin prerelease plan into its release workflow", () => {