fix(plugins): use clawhub artifact resolver for installs

This commit is contained in:
Vincent Koc
2026-05-02 11:41:50 -07:00
parent c3c2c31168
commit 62aa4df3da
4 changed files with 202 additions and 23 deletions

View File

@@ -57,6 +57,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Codex/app-server: resolve managed binaries from bundled `dist` chunks and from the `@openai/codex` package bin when installs do not provide a nearby `.bin/codex` shim, avoiding false missing-binary startup failures.
- Plugins/ClawHub: use the ClawHub artifact resolver response as the install decision before downloading, keeping legacy ZIP fallback and future ClawPack npm-pack installs on the same explicit resolver path. Thanks @vincentkoc.
- Plugins/ClawHub: gate bare plugin specs on ClawHub readiness before preferring ClawHub, so packages without deployed ClawPack readiness keep the npm fallback path instead of failing through a half-ready registry route. Thanks @vincentkoc.
- Plugins/source checkout: discover source-only plugins such as Codex from the `extensions/*` workspace while using npm package excludes as the packaged-core boundary, removing the stale core-bundle metadata path.
- Plugins/ClawHub: install ClawPack artifacts from the explicit npm-pack `.tgz` resolver path and persist artifact kind, npm integrity, shasum, and tarball metadata for update and diagnostics flows. Thanks @vincentkoc.

View File

@@ -94,8 +94,30 @@ export type ClawHubResolvedArtifact =
moderationState?: ClawHubArtifactModerationState | null;
};
export type ClawHubPackageArtifactResolverResponse = {
package?: { name?: string | null } | null;
version?: { version?: string | null } | string | null;
package?: {
name?: string | null;
displayName?: string | null;
family?: ClawHubPackageFamily | (string & {}) | null;
} | null;
version?:
| ({
version?: string | null;
createdAt?: number | null;
changelog?: string | null;
distTags?: string[];
files?: Array<{
path: string;
size?: number;
sha256: string;
contentType?: string;
}>;
sha256hash?: string | null;
compatibility?: ClawHubPackageCompatibility | null;
artifact?: ClawHubPackageArtifactSummary | null;
clawpack?: ClawHubPackageClawPackSummary | null;
} & Record<string, unknown>)
| string
| null;
artifact?: ClawHubResolvedArtifact | null;
};
export type ClawHubPackageSecurityResponse = {

View File

@@ -8,6 +8,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const parseClawHubPluginSpecMock = vi.fn();
const fetchClawHubPackageDetailMock = vi.fn();
const fetchClawHubPackageArtifactMock = vi.fn();
const fetchClawHubPackageVersionMock = vi.fn();
const downloadClawHubPackageArchiveMock = vi.fn();
const archiveCleanupMock = vi.fn();
@@ -21,6 +22,7 @@ vi.mock("../infra/clawhub.js", async () => {
...actual,
parseClawHubPluginSpec: (...args: unknown[]) => parseClawHubPluginSpecMock(...args),
fetchClawHubPackageDetail: (...args: unknown[]) => fetchClawHubPackageDetailMock(...args),
fetchClawHubPackageArtifact: (...args: unknown[]) => fetchClawHubPackageArtifactMock(...args),
fetchClawHubPackageVersion: (...args: unknown[]) => fetchClawHubPackageVersionMock(...args),
downloadClawHubPackageArchive: (...args: unknown[]) =>
downloadClawHubPackageArchiveMock(...args),
@@ -143,6 +145,12 @@ function expectClawHubInstallFlow(params: {
version: params.version,
}),
);
expect(fetchClawHubPackageArtifactMock).toHaveBeenCalledWith(
expect.objectContaining({
name: "demo",
version: params.version,
}),
);
expect(installPluginFromArchiveMock).toHaveBeenCalledWith(
expect.objectContaining({
archivePath: params.archivePath,
@@ -175,6 +183,7 @@ describe("installPluginFromClawHub", () => {
beforeEach(() => {
parseClawHubPluginSpecMock.mockReset();
fetchClawHubPackageDetailMock.mockReset();
fetchClawHubPackageArtifactMock.mockReset();
fetchClawHubPackageVersionMock.mockReset();
downloadClawHubPackageArchiveMock.mockReset();
archiveCleanupMock.mockReset();
@@ -211,6 +220,9 @@ describe("installPluginFromClawHub", () => {
},
},
});
fetchClawHubPackageArtifactMock.mockImplementation((params) =>
fetchClawHubPackageVersionMock(params),
);
downloadClawHubPackageArchiveMock.mockResolvedValue({
archivePath: "/tmp/clawhub-demo/archive.zip",
integrity: DEMO_ARCHIVE_INTEGRITY,
@@ -384,6 +396,73 @@ describe("installPluginFromClawHub", () => {
);
});
it("uses the artifact resolver response as the install decision", async () => {
fetchClawHubPackageVersionMock.mockClear();
fetchClawHubPackageArtifactMock.mockResolvedValueOnce({
package: {
name: "demo",
displayName: "Demo",
family: "code-plugin",
},
version: {
version: "2026.3.22",
compatibility: {
pluginApiRange: ">=2026.3.22",
minGatewayVersion: "2026.3.0",
},
},
artifact: {
source: "clawhub",
artifactKind: "npm-pack",
packageName: "demo",
version: "2026.3.22",
artifactSha256: DEMO_CLAWPACK_SHA256,
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",
artifactFormat: "tgz",
npmIntegrity: "sha512-clawpack",
npmShasum: "1".repeat(40),
clawpackSha256: DEMO_CLAWPACK_SHA256,
},
});
expect(fetchClawHubPackageArtifactMock).toHaveBeenCalledWith(
expect.objectContaining({
name: "demo",
version: "2026.3.22",
}),
);
expect(fetchClawHubPackageVersionMock).not.toHaveBeenCalled();
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

@@ -13,8 +13,8 @@ import {
import {
ClawHubRequestError,
downloadClawHubPackageArchive,
fetchClawHubPackageArtifact,
fetchClawHubPackageDetail,
fetchClawHubPackageVersion,
normalizeClawHubSha256Integrity,
normalizeClawHubSha256Hex,
parseClawHubPluginSpec,
@@ -22,9 +22,11 @@ import {
satisfiesGatewayMinimum,
satisfiesPluginApiRange,
type ClawHubPackageArtifactSummary,
type ClawHubPackageArtifactResolverResponse,
type ClawHubPackageCompatibility,
type ClawHubPackageDetail,
type ClawHubPackageClawPackSummary,
type ClawHubResolvedArtifact,
type ClawHubPackageVersion,
} from "../infra/clawhub.js";
import { formatErrorMessage } from "../infra/errors.js";
@@ -89,6 +91,17 @@ type ClawHubArchiveVerificationResolution =
}
| ClawHubInstallFailure;
type ClawHubArtifactResolverVersion = NonNullable<
Exclude<ClawHubPackageArtifactResolverResponse["version"], string | null | undefined>
>;
type ClawHubInstallArtifactDecision = {
version: string;
compatibility?: ClawHubPackageCompatibility | null;
verification: ClawHubArchiveVerification | null;
clawpack?: ClawHubPackageArtifactSummary | ClawHubPackageClawPackSummary | null;
};
type ClawHubArchiveFileVerificationResult =
| {
ok: true;
@@ -219,6 +232,49 @@ function resolveClawHubNpmPackArtifact(
return null;
}
function readArtifactResolverVersion(
response: ClawHubPackageArtifactResolverResponse,
requestedVersion: string,
): ClawHubArtifactResolverVersion {
if (
response.version &&
typeof response.version === "object" &&
!Array.isArray(response.version)
) {
return response.version;
}
if (typeof response.version === "string" && response.version.trim().length > 0) {
return { version: response.version.trim() };
}
return { version: requestedVersion };
}
function resolveTopLevelNpmPackArtifact(
artifact: ClawHubResolvedArtifact | null | undefined,
): ClawHubPackageArtifactSummary | null {
if (artifact?.artifactKind !== "npm-pack") {
return null;
}
return {
kind: "npm-pack",
format: "tgz",
sha256: artifact.artifactSha256 ?? null,
npmIntegrity: artifact.npmIntegrity,
npmShasum: artifact.npmShasum ?? null,
downloadUrl: artifact.downloadUrl ?? null,
};
}
function resolveTopLevelLegacyArchiveVerification(
artifact: ClawHubResolvedArtifact | null | undefined,
): ClawHubArchiveVerification | null {
if (artifact?.artifactKind !== "legacy-zip" || typeof artifact.artifactSha256 !== "string") {
return null;
}
const integrity = normalizeClawHubSha256Integrity(artifact.artifactSha256);
return integrity ? { kind: "archive-integrity", integrity } : null;
}
export function formatClawHubSpecifier(params: { name: string; version?: string }): string {
return `clawhub:${params.name}${params.version ? `@${params.version}` : ""}`;
}
@@ -692,16 +748,7 @@ async function resolveCompatiblePackageVersion(params: {
baseUrl?: string;
token?: string;
timeoutMs?: number;
}): Promise<
| {
ok: true;
version: string;
compatibility?: ClawHubPackageCompatibility | null;
verification: ClawHubArchiveVerification | null;
clawpack?: ClawHubPackageArtifactSummary | ClawHubPackageClawPackSummary | null;
}
| ClawHubInstallFailure
> {
}): Promise<({ ok: true } & ClawHubInstallArtifactDecision) | ClawHubInstallFailure> {
const requestedVersion = resolveRequestedVersion(params);
if (!requestedVersion) {
return buildClawHubInstallFailure(
@@ -709,9 +756,9 @@ async function resolveCompatiblePackageVersion(params: {
CLAWHUB_INSTALL_ERROR_CODE.NO_INSTALLABLE_VERSION,
);
}
let versionDetail;
let artifactResponse;
try {
versionDetail = await fetchClawHubPackageVersion({
artifactResponse = await fetchClawHubPackageArtifact({
name: params.detail.package?.name ?? "",
version: requestedVersion,
baseUrl: params.baseUrl,
@@ -725,20 +772,47 @@ async function resolveCompatiblePackageVersion(params: {
version: requestedVersion,
});
}
const resolvedVersion = versionDetail.version?.version ?? requestedVersion;
const artifactVersion = readArtifactResolverVersion(artifactResponse, requestedVersion);
const resolvedVersion = normalizeOptionalString(artifactVersion.version) ?? requestedVersion;
if (params.detail.package?.family === "skill") {
return {
ok: true,
version: resolvedVersion,
compatibility:
versionDetail.version?.compatibility ?? params.detail.package?.compatibility ?? null,
compatibility: artifactVersion.compatibility ?? params.detail.package?.compatibility ?? null,
verification: null,
clawpack: versionDetail.version?.clawpack ?? null,
clawpack:
artifactVersion.clawpack ?? resolveTopLevelNpmPackArtifact(artifactResponse.artifact),
};
}
const clawpack = versionDetail.version
? resolveClawHubNpmPackArtifact(versionDetail.version)
: null;
const versionDetail: ClawHubPackageVersion = {
package: artifactResponse.package
? {
name: artifactResponse.package.name ?? params.detail.package?.name ?? "",
displayName:
artifactResponse.package.displayName ?? params.detail.package?.displayName ?? "",
family:
artifactResponse.package.family === "code-plugin" ||
artifactResponse.package.family === "bundle-plugin" ||
artifactResponse.package.family === "skill"
? artifactResponse.package.family
: (params.detail.package?.family ?? "code-plugin"),
}
: null,
version: {
version: resolvedVersion,
createdAt: typeof artifactVersion.createdAt === "number" ? artifactVersion.createdAt : 0,
changelog: typeof artifactVersion.changelog === "string" ? artifactVersion.changelog : "",
distTags: artifactVersion.distTags,
files: artifactVersion.files,
sha256hash: artifactVersion.sha256hash,
compatibility: artifactVersion.compatibility,
artifact: artifactVersion.artifact,
clawpack: artifactVersion.clawpack ?? undefined,
},
};
const clawpack =
resolveClawHubNpmPackArtifact(versionDetail.version) ??
resolveTopLevelNpmPackArtifact(artifactResponse.artifact);
const verificationState = resolveClawHubArchiveVerification(
versionDetail,
params.detail.package?.name ?? "unknown",
@@ -757,12 +831,15 @@ async function resolveCompatiblePackageVersion(params: {
clawpack,
};
}
const topLevelLegacyVerification = resolveTopLevelLegacyArchiveVerification(
artifactResponse.artifact,
);
return {
ok: true,
version: resolvedVersion,
compatibility:
versionDetail.version?.compatibility ?? params.detail.package?.compatibility ?? null,
verification: verificationState.verification,
verification: verificationState.verification ?? topLevelLegacyVerification,
clawpack,
};
}