mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
feat(plugins): persist clawhub storepack metadata
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ export type InstalledPluginInstallRecordInfo = Pick<
|
||||
| "clawhubPackage"
|
||||
| "clawhubFamily"
|
||||
| "clawhubChannel"
|
||||
| "storepackSha256"
|
||||
| "storepackSpecVersion"
|
||||
| "storepackManifestSha256"
|
||||
| "storepackSize"
|
||||
| "gitUrl"
|
||||
| "gitRef"
|
||||
| "gitCommit"
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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<
|
||||
|
||||
Reference in New Issue
Block a user