feat(plugins): persist clawhub storepack metadata

This commit is contained in:
Vincent Koc
2026-05-01 16:50:02 -07:00
parent df32527298
commit 20e8769d93
13 changed files with 223 additions and 1 deletions

View File

@@ -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:<name>` across channel auth paths. (#75813)

View File

@@ -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,
}),
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,16 @@ function setInstallStringField<Key extends keyof Omit<InstalledPluginInstallReco
}
}
function setInstallNumberField<Key extends keyof Omit<InstalledPluginInstallRecordInfo, "source">>(
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);

View File

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

View File

@@ -48,6 +48,10 @@ export type InstalledPluginInstallRecordInfo = Pick<
| "clawhubPackage"
| "clawhubFamily"
| "clawhubChannel"
| "storepackSha256"
| "storepackSpecVersion"
| "storepackManifestSha256"
| "storepackSize"
| "gitUrl"
| "gitRef"
| "gitCommit"

View File

@@ -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,
});
});

View File

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