diff --git a/CHANGELOG.md b/CHANGELOG.md index 0133680b2f8..d3d13c96b75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### Changes +- Plugins/ClawHub: persist StorePack digest metadata on ClawHub plugin install and update records so registry refreshes and download verification can reuse stored artifact facts. Thanks @vincentkoc. - Providers/OpenAI: add `extraBody`/`extra_body` passthrough for OpenAI-compatible TTS endpoints, so custom speech servers can receive fields such as `lang` in `/audio/speech` requests. Fixes #39900. Thanks @R3NK0R. - Dependencies: refresh workspace dependency pins, including TypeBox 1.1.37, AWS SDK 3.1041.0, Microsoft Teams 2.0.9, and Marked 18.0.3. Thanks @mariozechner, @aws, and @microsoft. - Discord/channels: add reusable message-channel access groups plus Discord channel-audience DM authorization, so allowlists can reference `accessGroup:` across channel auth paths. (#75813) diff --git a/src/cli/plugins-cli.install.test.ts b/src/cli/plugins-cli.install.test.ts index 99d87b2aff0..816da45aebe 100644 --- a/src/cli/plugins-cli.install.test.ts +++ b/src/cli/plugins-cli.install.test.ts @@ -84,6 +84,10 @@ function createClawHubInstallResult(params: { version: params.version, integrity: "sha256-abc", resolvedAt: "2026-03-22T00:00:00.000Z", + storepackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + storepackSpecVersion: 1, + storepackManifestSha256: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + storepackSize: 4096, }, }; } @@ -467,6 +471,10 @@ describe("plugins cli install", () => { clawhubPackage: "demo", clawhubFamily: "code-plugin", clawhubChannel: "official", + storepackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + storepackSpecVersion: 1, + storepackManifestSha256: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + storepackSize: 4096, }), }); expect(writeConfigFile).toHaveBeenCalledWith(enabledCfg); @@ -544,7 +552,7 @@ describe("plugins cli install", () => { "memory-lancedb": { config: { embedding: { - provider: "openai", + apiKey: "sk-test", model: "text-embedding-3-small", }, }, @@ -636,6 +644,10 @@ describe("plugins cli install", () => { installPath: cliInstallPath("demo"), version: "1.2.3", clawhubPackage: "demo", + storepackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + storepackSpecVersion: 1, + storepackManifestSha256: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + storepackSize: 4096, }), }); }); diff --git a/src/cli/plugins-install-command.ts b/src/cli/plugins-install-command.ts index e4dff4d958d..7d71922114c 100644 --- a/src/cli/plugins-install-command.ts +++ b/src/cli/plugins-install-command.ts @@ -757,6 +757,10 @@ export async function runPluginInstallCommand(params: { clawhubPackage: result.clawhub.clawhubPackage, clawhubFamily: result.clawhub.clawhubFamily, clawhubChannel: result.clawhub.clawhubChannel, + storepackSha256: result.clawhub.storepackSha256, + storepackSpecVersion: result.clawhub.storepackSpecVersion, + storepackManifestSha256: result.clawhub.storepackManifestSha256, + storepackSize: result.clawhub.storepackSize, }, }); return; @@ -786,6 +790,10 @@ export async function runPluginInstallCommand(params: { clawhubPackage: clawhubResult.clawhub.clawhubPackage, clawhubFamily: clawhubResult.clawhub.clawhubFamily, clawhubChannel: clawhubResult.clawhub.clawhubChannel, + storepackSha256: clawhubResult.clawhub.storepackSha256, + storepackSpecVersion: clawhubResult.clawhub.storepackSpecVersion, + storepackManifestSha256: clawhubResult.clawhub.storepackManifestSha256, + storepackSize: clawhubResult.clawhub.storepackSize, }, }); return; diff --git a/src/config/types.installs.ts b/src/config/types.installs.ts index ed75679466f..0e9614c2d97 100644 --- a/src/config/types.installs.ts +++ b/src/config/types.installs.ts @@ -15,6 +15,10 @@ export type InstallRecordBase = { clawhubPackage?: string; clawhubFamily?: "code-plugin" | "bundle-plugin"; clawhubChannel?: "official" | "community" | "private"; + storepackSha256?: string; + storepackSpecVersion?: number; + storepackManifestSha256?: string; + storepackSize?: number; gitUrl?: string; gitRef?: string; gitCommit?: string; diff --git a/src/config/zod-schema.installs.ts b/src/config/zod-schema.installs.ts index 20e5d444eac..f64da1623db 100644 --- a/src/config/zod-schema.installs.ts +++ b/src/config/zod-schema.installs.ts @@ -29,6 +29,10 @@ export const InstallRecordShape = { clawhubChannel: z .union([z.literal("official"), z.literal("community"), z.literal("private")]) .optional(), + storepackSha256: z.string().optional(), + storepackSpecVersion: z.number().int().nonnegative().optional(), + storepackManifestSha256: z.string().optional(), + storepackSize: z.number().int().nonnegative().optional(), gitUrl: z.string().optional(), gitRef: z.string().optional(), gitCommit: z.string().optional(), diff --git a/src/infra/clawhub.ts b/src/infra/clawhub.ts index dea97767e3d..9a1601ccc1d 100644 --- a/src/infra/clawhub.ts +++ b/src/infra/clawhub.ts @@ -23,6 +23,36 @@ export type ClawHubPackageCompatibility = { pluginSdkVersion?: string; minGatewayVersion?: string; }; +export type ClawHubPackageHostTarget = { + os?: string | null; + arch?: string | null; + libc?: string | null; + key?: string | null; +}; +export type ClawHubPackageEnvironmentSummary = { + requiresLocalDesktop?: boolean; + requiresBrowser?: boolean; + requiresAudioDevice?: boolean; + requiresNetwork?: boolean; + requiresExternalServices?: string[]; + requiresOsPermissions?: string[]; + supportsRemoteHost?: boolean; + knownUnsupported?: string[]; +}; +export type ClawHubPackageStorePackSummary = { + available: boolean; + specVersion?: number | null; + format?: string | null; + sha256?: string | null; + size?: number | null; + fileCount?: number | null; + manifestSha256?: string | null; + builtAt?: number | null; + buildVersion?: string | null; + hostTargets?: ClawHubPackageHostTarget[]; + environment?: ClawHubPackageEnvironmentSummary | null; + runtimeBundles?: unknown[]; +}; export type ClawHubPackageListItem = { name: string; displayName: string; @@ -38,6 +68,10 @@ export type ClawHubPackageListItem = { capabilityTags?: string[]; executesCode?: boolean; verificationTier?: string | null; + storepackAvailable?: boolean; + hostTargetKeys?: string[]; + environmentFlags?: string[]; + storepack?: ClawHubPackageStorePackSummary; }; export type ClawHubPackageDetail = { package: @@ -65,6 +99,7 @@ export type ClawHubPackageDetail = { hasProvenance?: boolean; scanStatus?: string; } | null; + storepack?: ClawHubPackageStorePackSummary; }) | null; owner?: { @@ -103,6 +138,7 @@ export type ClawHubPackageVersion = { ? C : never : never; + storepack?: ClawHubPackageStorePackSummary; } | null; }; diff --git a/src/plugins/clawhub.test.ts b/src/plugins/clawhub.test.ts index e1e6098121c..7651eaed72b 100644 --- a/src/plugins/clawhub.test.ts +++ b/src/plugins/clawhub.test.ts @@ -53,6 +53,9 @@ const { CLAWHUB_INSTALL_ERROR_CODE, formatClawHubSpecifier, installPluginFromCla await import("./clawhub.js"); const DEMO_ARCHIVE_INTEGRITY = "sha256-qerEjGEpvES2+Tyan0j2xwDRkbcnmh4ZFfKN9vWbsa8="; +const DEMO_STOREPACK_SHA256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const DEMO_STOREPACK_MANIFEST_SHA256 = + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; const tempDirs: string[] = []; function sha256Hex(value: string): string { @@ -290,6 +293,50 @@ describe("installPluginFromClawHub", () => { ); }); + it("returns StorePack metadata from compatible ClawHub package versions", async () => { + fetchClawHubPackageVersionMock.mockResolvedValueOnce({ + version: { + version: "2026.3.22", + createdAt: 0, + changelog: "", + sha256hash: "a9eac48c6129bc44b6f93c9a9f48f6c700d191b7279a1e1915f28df6f59bb1af", + compatibility: { + pluginApiRange: ">=2026.3.22", + minGatewayVersion: "2026.3.0", + }, + storepack: { + available: true, + specVersion: 1, + format: "storepack.zip", + sha256: DEMO_STOREPACK_SHA256, + size: 4096, + fileCount: 7, + manifestSha256: DEMO_STOREPACK_MANIFEST_SHA256, + builtAt: 1774200000000, + buildVersion: "2026.3.22", + hostTargets: [], + environment: null, + runtimeBundles: [], + }, + }, + }); + + const result = await installPluginFromClawHub({ + spec: "clawhub:demo", + baseUrl: "https://clawhub.ai", + }); + + expect(result).toMatchObject({ + ok: true, + clawhub: { + storepackSha256: DEMO_STOREPACK_SHA256, + storepackSpecVersion: 1, + storepackManifestSha256: DEMO_STOREPACK_MANIFEST_SHA256, + storepackSize: 4096, + }, + }); + }); + it("installs when ClawHub advertises a wildcard plugin API range", async () => { fetchClawHubPackageVersionMock.mockResolvedValueOnce({ version: { diff --git a/src/plugins/clawhub.ts b/src/plugins/clawhub.ts index e54edee9c31..ef13a9cfa10 100644 --- a/src/plugins/clawhub.ts +++ b/src/plugins/clawhub.ts @@ -25,6 +25,7 @@ import { type ClawHubPackageCompatibility, type ClawHubPackageDetail, type ClawHubPackageFamily, + type ClawHubPackageStorePackSummary, type ClawHubPackageVersion, } from "../infra/clawhub.js"; import { formatErrorMessage } from "../infra/errors.js"; @@ -65,6 +66,10 @@ export type ClawHubPluginInstallRecordFields = { integrity?: string; resolvedAt?: string; installedAt?: string; + storepackSha256?: string; + storepackSpecVersion?: number; + storepackManifestSha256?: string; + storepackSize?: number; }; type ClawHubInstallFailure = { @@ -122,6 +127,41 @@ type ClawHubArchiveEntryLimits = { addArchiveBytes: (bytes: number) => boolean; }; +function normalizeClawHubStorePackInstallFields( + storepack: ClawHubPackageStorePackSummary | null | undefined, +): Pick< + ClawHubPluginInstallRecordFields, + "storepackSha256" | "storepackSpecVersion" | "storepackManifestSha256" | "storepackSize" +> { + if (storepack?.available !== true) { + return {}; + } + const storepackSha256 = + typeof storepack.sha256 === "string" ? normalizeClawHubSha256Hex(storepack.sha256) : null; + const storepackManifestSha256 = + typeof storepack.manifestSha256 === "string" + ? normalizeClawHubSha256Hex(storepack.manifestSha256) + : null; + const storepackSpecVersion = + typeof storepack.specVersion === "number" && + Number.isSafeInteger(storepack.specVersion) && + storepack.specVersion >= 0 + ? storepack.specVersion + : undefined; + const storepackSize = + typeof storepack.size === "number" && + Number.isSafeInteger(storepack.size) && + storepack.size >= 0 + ? storepack.size + : undefined; + return { + ...(storepackSha256 ? { storepackSha256 } : {}), + ...(storepackSpecVersion !== undefined ? { storepackSpecVersion } : {}), + ...(storepackManifestSha256 ? { storepackManifestSha256 } : {}), + ...(storepackSize !== undefined ? { storepackSize } : {}), + }; +} + export function formatClawHubSpecifier(params: { name: string; version?: string }): string { return `clawhub:${params.name}${params.version ? `@${params.version}` : ""}`; } @@ -601,6 +641,7 @@ async function resolveCompatiblePackageVersion(params: { version: string; compatibility?: ClawHubPackageCompatibility | null; verification: ClawHubArchiveVerification | null; + storepack?: ClawHubPackageStorePackSummary | null; } | ClawHubInstallFailure > { @@ -635,6 +676,7 @@ async function resolveCompatiblePackageVersion(params: { compatibility: versionDetail.version?.compatibility ?? params.detail.package?.compatibility ?? null, verification: null, + storepack: versionDetail.version?.storepack ?? params.detail.package?.storepack ?? null, }; } const verificationState = resolveClawHubArchiveVerification( @@ -651,6 +693,7 @@ async function resolveCompatiblePackageVersion(params: { compatibility: versionDetail.version?.compatibility ?? params.detail.package?.compatibility ?? null, verification: verificationState.verification, + storepack: versionDetail.version?.storepack ?? params.detail.package?.storepack ?? null, }; } @@ -879,6 +922,7 @@ export async function installPluginFromClawHub( } const pkg = detail.package!; + const storepackFields = normalizeClawHubStorePackInstallFields(versionState.storepack); const clawhubFamily = pkg.family === "code-plugin" || pkg.family === "bundle-plugin" ? pkg.family : null; if (!clawhubFamily) { @@ -904,6 +948,7 @@ export async function installPluginFromClawHub( // server-attested sha256hash from ClawHub version metadata. integrity: archive.integrity, resolvedAt: new Date().toISOString(), + ...storepackFields, }, }; } finally { diff --git a/src/plugins/installed-plugin-index-install-records.ts b/src/plugins/installed-plugin-index-install-records.ts index aa17d4f9275..40f893e1d67 100644 --- a/src/plugins/installed-plugin-index-install-records.ts +++ b/src/plugins/installed-plugin-index-install-records.ts @@ -18,6 +18,16 @@ function setInstallStringField>( + target: InstalledPluginInstallRecordInfo, + key: Key, + value: PluginInstallRecord[Key], +): void { + if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) { + target[key] = value as InstalledPluginInstallRecordInfo[Key]; + } +} + function normalizeInstallRecord( record: PluginInstallRecord | undefined, ): InstalledPluginInstallRecordInfo | undefined { @@ -42,6 +52,10 @@ function normalizeInstallRecord( setInstallStringField(normalized, "clawhubPackage", record.clawhubPackage); setInstallStringField(normalized, "clawhubFamily", record.clawhubFamily); setInstallStringField(normalized, "clawhubChannel", record.clawhubChannel); + setInstallStringField(normalized, "storepackSha256", record.storepackSha256); + setInstallNumberField(normalized, "storepackSpecVersion", record.storepackSpecVersion); + setInstallStringField(normalized, "storepackManifestSha256", record.storepackManifestSha256); + setInstallNumberField(normalized, "storepackSize", record.storepackSize); setInstallStringField(normalized, "gitUrl", record.gitUrl); setInstallStringField(normalized, "gitRef", record.gitRef); setInstallStringField(normalized, "gitCommit", record.gitCommit); diff --git a/src/plugins/installed-plugin-index-records.test.ts b/src/plugins/installed-plugin-index-records.test.ts index 817699d3661..cb6776451ad 100644 --- a/src/plugins/installed-plugin-index-records.test.ts +++ b/src/plugins/installed-plugin-index-records.test.ts @@ -192,6 +192,41 @@ describe("plugin index install records store", () => { }); }); + it("preserves ClawHub StorePack install metadata in persisted records", async () => { + const stateDir = makeStateDir(); + const candidate = createPluginCandidate(stateDir, "storepack-demo"); + await writePersistedInstalledPluginIndexInstallRecords( + { + "storepack-demo": { + source: "clawhub", + spec: "clawhub:storepack-demo", + installPath: path.join(stateDir, "plugins", "storepack-demo"), + clawhubUrl: "https://clawhub.ai", + clawhubPackage: "storepack-demo", + clawhubFamily: "code-plugin", + clawhubChannel: "official", + storepackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + storepackSpecVersion: 1, + storepackManifestSha256: + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + storepackSize: 4096, + }, + }, + { stateDir, candidates: [candidate] }, + ); + + await expect(loadInstalledPluginIndexInstallRecords({ stateDir })).resolves.toMatchObject({ + "storepack-demo": { + source: "clawhub", + spec: "clawhub:storepack-demo", + storepackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + storepackSpecVersion: 1, + storepackManifestSha256: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + storepackSize: 4096, + }, + }); + }); + it("returns an empty record map when no plugin index exists", () => { const stateDir = makeStateDir(); diff --git a/src/plugins/installed-plugin-index-types.ts b/src/plugins/installed-plugin-index-types.ts index 4d1e9bcf079..d9dd458c404 100644 --- a/src/plugins/installed-plugin-index-types.ts +++ b/src/plugins/installed-plugin-index-types.ts @@ -48,6 +48,10 @@ export type InstalledPluginInstallRecordInfo = Pick< | "clawhubPackage" | "clawhubFamily" | "clawhubChannel" + | "storepackSha256" + | "storepackSpecVersion" + | "storepackManifestSha256" + | "storepackSize" | "gitUrl" | "gitRef" | "gitCommit" diff --git a/src/plugins/update.test.ts b/src/plugins/update.test.ts index 0befad09b61..16c1839a3ad 100644 --- a/src/plugins/update.test.ts +++ b/src/plugins/update.test.ts @@ -1003,6 +1003,10 @@ describe("updateNpmInstalledPlugins", () => { clawhubChannel: "official", integrity: "sha256-next", resolvedAt: "2026-03-22T00:00:00.000Z", + storepackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + storepackSpecVersion: 1, + storepackManifestSha256: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + storepackSize: 4096, }, }); @@ -1037,6 +1041,10 @@ describe("updateNpmInstalledPlugins", () => { clawhubFamily: "code-plugin", clawhubChannel: "official", integrity: "sha256-next", + storepackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + storepackSpecVersion: 1, + storepackManifestSha256: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + storepackSize: 4096, }); }); diff --git a/src/plugins/update.ts b/src/plugins/update.ts index d551429344f..97ed99e09ae 100644 --- a/src/plugins/update.ts +++ b/src/plugins/update.ts @@ -938,6 +938,10 @@ export async function updateNpmInstalledPlugins(params: { clawhubPackage: clawhubResult.clawhub.clawhubPackage, clawhubFamily: clawhubResult.clawhub.clawhubFamily, clawhubChannel: clawhubResult.clawhub.clawhubChannel, + storepackSha256: clawhubResult.clawhub.storepackSha256, + storepackSpecVersion: clawhubResult.clawhub.storepackSpecVersion, + storepackManifestSha256: clawhubResult.clawhub.storepackManifestSha256, + storepackSize: clawhubResult.clawhub.storepackSize, }); } else if (record.source === "git") { const gitResult = result as Extract<