feat(plugins): expose install source facts

* feat(plugins): expose install source facts

* fix(plugins): normalize install integrity facts

* fix(plugins): guard install source string fields

* fix(plugins): keep install source facts additive
This commit is contained in:
Vincent Koc
2026-04-23 23:21:43 -07:00
committed by GitHub
parent b588b5a230
commit 37c37eecfb
8 changed files with 317 additions and 4 deletions

View File

@@ -884,6 +884,20 @@ Or point `OPENCLAW_PLUGIN_CATALOG_PATHS` (or `OPENCLAW_MPM_CATALOG_PATHS`) at
one or more JSON files (comma/semicolon/`PATH`-delimited). Each file should
contain `{ "entries": [ { "name": "@scope/pkg", "openclaw": { "channel": {...}, "install": {...} } } ] }`. The parser also accepts `"packages"` or `"plugins"` as legacy aliases for the `"entries"` key.
Generated channel catalog entries and provider install catalog entries expose
normalized install-source facts next to the raw `openclaw.install` block. The
normalized facts identify whether the npm spec is an exact version or floating
selector, whether expected integrity metadata is present, and whether a local
source path is also available. Consumers should treat `installSource` as an
additive optional field so older hand-built entries and compatibility shims do
not have to synthesize it. This lets onboarding and diagnostics explain
source-plane state without importing plugin runtime.
Official external npm entries should prefer an exact `npmSpec` plus
`expectedIntegrity`. Bare package names and dist-tags still work for
compatibility, but they surface source-plane warnings so the catalog can move
toward pinned, integrity-checked installs without breaking existing plugins.
## Context engine plugins
Context engine plugins own session context orchestration for ingest, assembly,

View File

@@ -591,10 +591,12 @@ registry loading. Invalid values are rejected; newer-but-valid values skip the
plugin on older hosts.
Exact npm version pinning already lives in `npmSpec`, for example
`"npmSpec": "@wecom/wecom-openclaw-plugin@1.2.3"`. Pair that with
`expectedIntegrity` when you want update flows to fail closed if the fetched
npm artifact no longer matches the pinned release. Interactive onboarding
offers trusted registry npm specs, including bare package names and dist-tags.
`"npmSpec": "@wecom/wecom-openclaw-plugin@1.2.3"`. Official external catalog
entries should pair exact specs with `expectedIntegrity` so update flows fail
closed if the fetched npm artifact no longer matches the pinned release.
Interactive onboarding still offers trusted registry npm specs, including bare
package names and dist-tags, for compatibility. Catalog diagnostics can
distinguish exact, floating, integrity-pinned, and missing-integrity sources.
When `expectedIntegrity` is present, install/update flows enforce it; when it
is omitted, the registry resolution is recorded without an integrity pin.

View File

@@ -4,6 +4,10 @@ import officialExternalChannelCatalog from "../../../scripts/lib/official-extern
import { MANIFEST_KEY } from "../../compat/legacy-names.js";
import { resolveOpenClawPackageRootSync } from "../../infra/openclaw-root.js";
import { listChannelCatalogEntries } from "../../plugins/channel-catalog-registry.js";
import {
describePluginInstallSource,
type PluginInstallSourceInfo,
} from "../../plugins/install-source-info.js";
import type { OpenClawPackageManifest } from "../../plugins/manifest.js";
import type { PluginPackageChannel, PluginPackageInstall } from "../../plugins/manifest.js";
import type { PluginOrigin } from "../../plugins/plugin-origin.types.js";
@@ -36,6 +40,7 @@ export type ChannelPluginCatalogEntry = {
install: PluginPackageInstall & {
npmSpec: string;
};
installSource?: PluginInstallSourceInfo;
};
type CatalogOptions = {
@@ -264,6 +269,7 @@ function buildCatalogEntryFromManifest(params: {
...(params.origin ? { origin: params.origin } : {}),
meta,
install,
installSource: describePluginInstallSource(install),
};
}

View File

@@ -0,0 +1,134 @@
import { describe, expect, it } from "vitest";
import { describePluginInstallSource } from "./install-source-info.js";
describe("describePluginInstallSource", () => {
it("marks exact npm specs with integrity as fully pinned", () => {
expect(
describePluginInstallSource({
npmSpec: "@vendor/demo@1.2.3",
expectedIntegrity: " sha512-demo ",
defaultChoice: "npm",
}),
).toEqual({
defaultChoice: "npm",
npm: {
spec: "@vendor/demo@1.2.3",
packageName: "@vendor/demo",
selector: "1.2.3",
selectorKind: "exact-version",
exactVersion: true,
expectedIntegrity: "sha512-demo",
pinState: "exact-with-integrity",
},
warnings: [],
});
});
it("marks exact npm specs without integrity as version-pinned only", () => {
expect(
describePluginInstallSource({
npmSpec: "@vendor/demo@1.2.3",
}),
).toEqual({
npm: {
spec: "@vendor/demo@1.2.3",
packageName: "@vendor/demo",
selector: "1.2.3",
selectorKind: "exact-version",
exactVersion: true,
pinState: "exact-without-integrity",
},
warnings: ["npm-spec-missing-integrity"],
});
});
it("omits whitespace-only integrity from npm source facts", () => {
expect(
describePluginInstallSource({
npmSpec: "@vendor/demo@1.2.3",
expectedIntegrity: " ",
}),
).toEqual({
npm: {
spec: "@vendor/demo@1.2.3",
packageName: "@vendor/demo",
selector: "1.2.3",
selectorKind: "exact-version",
exactVersion: true,
pinState: "exact-without-integrity",
},
warnings: ["npm-spec-missing-integrity"],
});
});
it("treats non-string integrity metadata as missing", () => {
expect(
describePluginInstallSource({
npmSpec: "@vendor/demo@1.2.3",
expectedIntegrity: 123,
} as never),
).toEqual({
npm: {
spec: "@vendor/demo@1.2.3",
packageName: "@vendor/demo",
selector: "1.2.3",
selectorKind: "exact-version",
exactVersion: true,
pinState: "exact-without-integrity",
},
warnings: ["npm-spec-missing-integrity"],
});
});
it("surfaces floating specs with integrity without rejecting them", () => {
expect(
describePluginInstallSource({
npmSpec: "@vendor/demo@beta",
expectedIntegrity: "sha512-demo",
}),
).toEqual({
npm: {
spec: "@vendor/demo@beta",
packageName: "@vendor/demo",
selector: "beta",
selectorKind: "tag",
exactVersion: false,
expectedIntegrity: "sha512-demo",
pinState: "floating-with-integrity",
},
warnings: ["npm-spec-floating"],
});
});
it("surfaces floating specs without integrity without rejecting them", () => {
expect(
describePluginInstallSource({
npmSpec: "@vendor/demo@beta",
}),
).toEqual({
npm: {
spec: "@vendor/demo@beta",
packageName: "@vendor/demo",
selector: "beta",
selectorKind: "tag",
exactVersion: false,
pinState: "floating-without-integrity",
},
warnings: ["npm-spec-floating", "npm-spec-missing-integrity"],
});
});
it("reports invalid npm specs while preserving local source metadata", () => {
expect(
describePluginInstallSource({
npmSpec: "github:vendor/demo",
localPath: "extensions/demo",
}),
).toEqual({
local: {
path: "extensions/demo",
},
warnings: ["invalid-npm-spec"],
});
});
});

View File

@@ -0,0 +1,91 @@
import { parseRegistryNpmSpec, type ParsedRegistryNpmSpec } from "../infra/npm-registry-spec.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import type { PluginPackageInstall } from "./manifest.js";
export type PluginInstallSourceWarning =
| "invalid-npm-spec"
| "npm-spec-floating"
| "npm-spec-missing-integrity";
export type PluginInstallNpmPinState =
| "exact-with-integrity"
| "exact-without-integrity"
| "floating-with-integrity"
| "floating-without-integrity";
export type PluginInstallNpmSourceInfo = {
spec: string;
packageName: string;
selector?: string;
selectorKind: ParsedRegistryNpmSpec["selectorKind"];
exactVersion: boolean;
expectedIntegrity?: string;
pinState: PluginInstallNpmPinState;
};
export type PluginInstallLocalSourceInfo = {
path: string;
};
export type PluginInstallSourceInfo = {
defaultChoice?: PluginPackageInstall["defaultChoice"];
npm?: PluginInstallNpmSourceInfo;
local?: PluginInstallLocalSourceInfo;
warnings: readonly PluginInstallSourceWarning[];
};
function resolveNpmPinState(params: {
exactVersion: boolean;
hasIntegrity: boolean;
}): PluginInstallNpmPinState {
if (params.exactVersion) {
return params.hasIntegrity ? "exact-with-integrity" : "exact-without-integrity";
}
return params.hasIntegrity ? "floating-with-integrity" : "floating-without-integrity";
}
export function describePluginInstallSource(
install: PluginPackageInstall,
): PluginInstallSourceInfo {
const npmSpec = normalizeOptionalString(install.npmSpec);
const localPath = normalizeOptionalString(install.localPath);
const defaultChoice =
install.defaultChoice === "npm" || install.defaultChoice === "local"
? install.defaultChoice
: undefined;
const warnings: PluginInstallSourceWarning[] = [];
let npm: PluginInstallNpmSourceInfo | undefined;
if (npmSpec) {
const parsed = parseRegistryNpmSpec(npmSpec);
if (parsed) {
const exactVersion = parsed.selectorKind === "exact-version";
const expectedIntegrity = normalizeOptionalString(install.expectedIntegrity);
const hasIntegrity = Boolean(expectedIntegrity);
if (!exactVersion) {
warnings.push("npm-spec-floating");
}
if (!hasIntegrity) {
warnings.push("npm-spec-missing-integrity");
}
npm = {
spec: parsed.raw,
packageName: parsed.name,
selectorKind: parsed.selectorKind,
exactVersion,
pinState: resolveNpmPinState({ exactVersion, hasIntegrity }),
...(parsed.selector ? { selector: parsed.selector } : {}),
...(expectedIntegrity ? { expectedIntegrity } : {}),
};
} else {
warnings.push("invalid-npm-spec");
}
}
return {
...(defaultChoice ? { defaultChoice } : {}),
...(npm ? { npm } : {}),
...(localPath ? { local: { path: localPath } } : {}),
warnings,
};
}

View File

@@ -104,6 +104,22 @@ describe("provider install catalog", () => {
defaultChoice: "npm",
expectedIntegrity: "sha512-openai",
},
installSource: {
defaultChoice: "npm",
npm: {
spec: "@openclaw/openai@1.2.3",
packageName: "@openclaw/openai",
selector: "1.2.3",
selectorKind: "exact-version",
exactVersion: true,
expectedIntegrity: "sha512-openai",
pinState: "exact-with-integrity",
},
local: {
path: "extensions/openai",
},
warnings: [],
},
},
]);
});
@@ -157,6 +173,13 @@ describe("provider install catalog", () => {
localPath: "extensions/demo-provider",
defaultChoice: "local",
},
installSource: {
defaultChoice: "local",
local: {
path: "extensions/demo-provider",
},
warnings: [],
},
},
]);
});
@@ -216,6 +239,19 @@ describe("provider install catalog", () => {
expectedIntegrity: "sha512-vllm",
defaultChoice: "npm",
},
installSource: {
defaultChoice: "npm",
npm: {
spec: "@openclaw/vllm@2.0.0",
packageName: "@openclaw/vllm",
selector: "2.0.0",
selectorKind: "exact-version",
exactVersion: true,
expectedIntegrity: "sha512-vllm",
pinState: "exact-with-integrity",
},
warnings: [],
},
});
});
@@ -270,6 +306,17 @@ describe("provider install catalog", () => {
npmSpec: "@openclaw/vllm",
defaultChoice: "npm",
},
installSource: {
defaultChoice: "npm",
npm: {
spec: "@openclaw/vllm",
packageName: "@openclaw/vllm",
selectorKind: "none",
exactVersion: false,
pinState: "floating-without-integrity",
},
warnings: ["npm-spec-floating", "npm-spec-missing-integrity"],
},
});
});

View File

@@ -2,6 +2,10 @@ import path from "node:path";
import { parseRegistryNpmSpec } from "../infra/npm-registry-spec.js";
import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js";
import { discoverOpenClawPlugins } from "./discovery.js";
import {
describePluginInstallSource,
type PluginInstallSourceInfo,
} from "./install-source-info.js";
import {
loadPluginManifest,
type PluginPackageInstall,
@@ -17,6 +21,7 @@ export type ProviderInstallCatalogEntry = ProviderAuthChoiceMetadata & {
label: string;
origin: PluginOrigin;
install: PluginPackageInstall;
installSource?: PluginInstallSourceInfo;
};
type ProviderInstallCatalogParams = {
@@ -179,6 +184,7 @@ export function resolveProviderInstallCatalogEntries(
label: choice.groupLabel ?? choice.choiceLabel,
origin: install.origin,
install: install.install,
installSource: describePluginInstallSource(install.install),
} satisfies ProviderInstallCatalogEntry,
];
})

View File

@@ -264,6 +264,19 @@ export function describeChannelPluginCatalogEntriesContract() {
minHostVersion: ">=2026.4.10",
expectedIntegrity: "sha512-wecom",
},
installSource: {
defaultChoice: "npm",
npm: {
spec: "@wecom/wecom-openclaw-plugin@1.2.3",
packageName: "@wecom/wecom-openclaw-plugin",
selector: "1.2.3",
selectorKind: "exact-version",
exactVersion: true,
expectedIntegrity: "sha512-wecom",
pinState: "exact-with-integrity",
},
warnings: [],
},
},
};
},