mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:40:44 +00:00
fix(plugins): use clawhub artifact resolver for installs
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user