fix(plugins): align clawhub clawpack downloads

This commit is contained in:
Vincent Koc
2026-05-02 09:30:49 -07:00
parent 5c15ce3476
commit 03be4bfac5
11 changed files with 229 additions and 91 deletions

View File

@@ -57,8 +57,6 @@ const DEMO_CLAWPACK_SHA256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
const DEMO_CLAWPACK_INTEGRITY = `sha256-${Buffer.from(DEMO_CLAWPACK_SHA256, "hex").toString(
"base64",
)}`;
const DEMO_CLAWPACK_MANIFEST_SHA256 =
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
const tempDirs: string[] = [];
function sha256Hex(value: string): string {
@@ -336,29 +334,24 @@ describe("installPluginFromClawHub", () => {
pluginApiRange: ">=2026.3.22",
minGatewayVersion: "2026.3.0",
},
clawpack: {
available: true,
specVersion: 1,
format: "clawpack.zip",
artifact: {
kind: "npm-pack",
format: "tgz",
sha256: DEMO_CLAWPACK_SHA256,
size: 4096,
fileCount: 7,
manifestSha256: DEMO_CLAWPACK_MANIFEST_SHA256,
builtAt: 1774200000000,
buildVersion: "2026.3.22",
hostTargets: [],
environment: null,
runtimeBundles: [],
npmIntegrity: "sha512-clawpack",
npmShasum: "1".repeat(40),
npmTarballName: "demo-2026.3.22.tgz",
},
},
});
downloadClawHubPackageArchiveMock.mockResolvedValueOnce({
archivePath: "/tmp/clawhub-demo/clawpack.zip",
archivePath: "/tmp/clawhub-demo/demo-2026.3.22.tgz",
integrity: DEMO_CLAWPACK_INTEGRITY,
sha256Hex: DEMO_CLAWPACK_SHA256,
artifact: "clawpack",
clawpackHeaderSha256: DEMO_CLAWPACK_SHA256,
clawpackHeaderSpecVersion: 1,
npmIntegrity: "sha512-clawpack",
cleanup: archiveCleanupMock,
});
@@ -372,8 +365,6 @@ describe("installPluginFromClawHub", () => {
clawhub: {
integrity: DEMO_CLAWPACK_INTEGRITY,
clawpackSha256: DEMO_CLAWPACK_SHA256,
clawpackSpecVersion: 1,
clawpackManifestSha256: DEMO_CLAWPACK_MANIFEST_SHA256,
clawpackSize: 4096,
},
});
@@ -396,23 +387,20 @@ describe("installPluginFromClawHub", () => {
pluginApiRange: ">=2026.3.22",
minGatewayVersion: "2026.3.0",
},
clawpack: {
available: true,
specVersion: 1,
format: "clawpack.zip",
artifact: {
kind: "npm-pack",
format: "tgz",
sha256: DEMO_CLAWPACK_SHA256,
size: 4096,
manifestSha256: DEMO_CLAWPACK_MANIFEST_SHA256,
},
},
});
downloadClawHubPackageArchiveMock.mockResolvedValueOnce({
archivePath: "/tmp/clawhub-demo/clawpack.zip",
archivePath: "/tmp/clawhub-demo/demo-2026.3.22.tgz",
integrity: DEMO_CLAWPACK_INTEGRITY,
sha256Hex: DEMO_CLAWPACK_SHA256,
artifact: "clawpack",
clawpackHeaderSha256: DEMO_CLAWPACK_SHA256,
clawpackHeaderSpecVersion: 1,
cleanup: archiveCleanupMock,
});
@@ -435,7 +423,7 @@ describe("installPluginFromClawHub", () => {
);
expect(installPluginFromArchiveMock).toHaveBeenCalledWith(
expect.objectContaining({
archivePath: "/tmp/clawhub-demo/clawpack.zip",
archivePath: "/tmp/clawhub-demo/demo-2026.3.22.tgz",
}),
);
});
@@ -451,16 +439,15 @@ describe("installPluginFromClawHub", () => {
pluginApiRange: ">=2026.3.22",
minGatewayVersion: "2026.3.0",
},
clawpack: {
available: true,
specVersion: 1,
format: "clawpack.zip",
artifact: {
kind: "npm-pack",
format: "tgz",
sha256: DEMO_CLAWPACK_SHA256,
},
},
});
downloadClawHubPackageArchiveMock.mockResolvedValueOnce({
archivePath: "/tmp/clawhub-demo/clawpack.zip",
archivePath: "/tmp/clawhub-demo/demo-2026.3.22.tgz",
integrity: `sha256-${Buffer.from(mismatchedSha256, "hex").toString("base64")}`,
sha256Hex: mismatchedSha256,
artifact: "clawpack",
@@ -497,19 +484,11 @@ describe("installPluginFromClawHub", () => {
pluginApiRange: ">=2026.3.22",
minGatewayVersion: "2026.3.0",
},
clawpack: {
available: true,
specVersion: 1,
format: "clawpack.zip",
artifact: {
kind: "npm-pack",
format: "tgz",
sha256: DEMO_CLAWPACK_SHA256,
size: 4096,
fileCount: 7,
manifestSha256: DEMO_CLAWPACK_MANIFEST_SHA256,
builtAt: 1774200000000,
buildVersion: "2026.3.22",
hostTargets: [],
environment: null,
runtimeBundles: [],
},
},
});

View File

@@ -22,6 +22,7 @@ import {
satisfiesGatewayMinimum,
satisfiesPluginApiRange,
type ClawHubPackageChannel,
type ClawHubPackageArtifactSummary,
type ClawHubPackageCompatibility,
type ClawHubPackageDetail,
type ClawHubPackageFamily,
@@ -128,22 +129,26 @@ type ClawHubArchiveEntryLimits = {
};
function normalizeClawHubClawPackInstallFields(
clawpack: ClawHubPackageClawPackSummary | null | undefined,
clawpack: ClawHubPackageArtifactSummary | ClawHubPackageClawPackSummary | null | undefined,
): Pick<
ClawHubPluginInstallRecordFields,
"clawpackSha256" | "clawpackSpecVersion" | "clawpackManifestSha256" | "clawpackSize"
> {
if (clawpack?.available !== true) {
const isNpmPackArtifact =
clawpack && "kind" in clawpack && normalizeOptionalString(clawpack.kind) === "npm-pack";
const isLegacyClawPack = clawpack && "available" in clawpack && clawpack.available;
if (!isNpmPackArtifact && !isLegacyClawPack) {
return {};
}
const clawpackSha256 =
typeof clawpack.sha256 === "string" ? normalizeClawHubSha256Hex(clawpack.sha256) : null;
const clawpackManifestSha256 =
typeof clawpack.manifestSha256 === "string"
"manifestSha256" in clawpack && typeof clawpack.manifestSha256 === "string"
? normalizeClawHubSha256Hex(clawpack.manifestSha256)
: null;
const clawpackSpecVersion =
"specVersion" in clawpack &&
typeof clawpack.specVersion === "number" &&
Number.isSafeInteger(clawpack.specVersion) &&
clawpack.specVersion >= 0
@@ -174,14 +179,35 @@ function isTrustedSourceLinkedOfficialPackage(pkg: NonNullable<ClawHubPackageDet
}
function resolveClawHubClawPackArtifactSha256(
clawpack: ClawHubPackageClawPackSummary | null | undefined,
clawpack: ClawHubPackageArtifactSummary | ClawHubPackageClawPackSummary | null | undefined,
): string | null {
if (clawpack?.available !== true || typeof clawpack.sha256 !== "string") {
const isNpmPackArtifact =
clawpack && "kind" in clawpack && normalizeOptionalString(clawpack.kind) === "npm-pack";
const isLegacyClawPack = clawpack && "available" in clawpack && clawpack.available;
if ((!isNpmPackArtifact && !isLegacyClawPack) || typeof clawpack.sha256 !== "string") {
return null;
}
return normalizeClawHubSha256Hex(clawpack.sha256);
}
function resolveClawHubNpmIntegrity(
clawpack: ClawHubPackageArtifactSummary | ClawHubPackageClawPackSummary | null | undefined,
): string | null {
return normalizeOptionalString(clawpack?.npmIntegrity) ?? null;
}
function resolveClawHubNpmPackArtifact(
version: NonNullable<ClawHubPackageVersion["version"]>,
): ClawHubPackageArtifactSummary | ClawHubPackageClawPackSummary | null {
if (version.artifact?.kind === "npm-pack") {
return version.artifact;
}
if (version.clawpack?.available === true) {
return version.clawpack;
}
return null;
}
export function formatClawHubSpecifier(params: { name: string; version?: string }): string {
return `clawhub:${params.name}${params.version ? `@${params.version}` : ""}`;
}
@@ -661,7 +687,7 @@ async function resolveCompatiblePackageVersion(params: {
version: string;
compatibility?: ClawHubPackageCompatibility | null;
verification: ClawHubArchiveVerification | null;
clawpack?: ClawHubPackageClawPackSummary | null;
clawpack?: ClawHubPackageArtifactSummary | ClawHubPackageClawPackSummary | null;
}
| ClawHubInstallFailure
> {
@@ -699,7 +725,9 @@ async function resolveCompatiblePackageVersion(params: {
clawpack: versionDetail.version?.clawpack ?? null,
};
}
const clawpack = versionDetail.version?.clawpack ?? null;
const clawpack = versionDetail.version
? resolveClawHubNpmPackArtifact(versionDetail.version)
: null;
const verificationState = resolveClawHubArchiveVerification(
versionDetail,
params.detail.package?.name ?? "unknown",
@@ -910,6 +938,7 @@ export async function installPluginFromClawHub(
try {
if (expectedClawPackSha256) {
const expectedIntegrity = normalizeClawHubSha256Integrity(expectedClawPackSha256);
const expectedNpmIntegrity = resolveClawHubNpmIntegrity(versionState.clawpack);
if (
archive.artifact !== "clawpack" ||
archive.clawpackHeaderSha256 !== expectedClawPackSha256 ||
@@ -921,6 +950,12 @@ export async function installPluginFromClawHub(
CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH,
);
}
if (expectedNpmIntegrity && archive.npmIntegrity !== expectedNpmIntegrity) {
return buildClawHubInstallFailure(
`ClawHub ClawPack npm integrity mismatch for "${parsed.name}@${versionState.version}": expected ${expectedNpmIntegrity}, got ${archive.npmIntegrity ?? "unknown"}.`,
CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH,
);
}
} else if (versionState.verification?.kind === "archive-integrity") {
if (archive.integrity !== versionState.verification.integrity) {
return buildClawHubInstallFailure(