fix(plugins): tolerate missing clawhub artifact resolver

This commit is contained in:
Vincent Koc
2026-05-02 14:23:07 -07:00
parent 871cd475af
commit dc6d9973e9
5 changed files with 158 additions and 6 deletions

View File

@@ -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.

View File

@@ -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`

View File

@@ -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: {

View File

@@ -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;

View File

@@ -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", () => {