fix: trust official source-linked ClawHub plugins

This commit is contained in:
Peter Steinberger
2026-05-02 05:15:50 +01:00
parent 374529d612
commit 87f43ca88c
6 changed files with 103 additions and 1 deletions

View File

@@ -255,6 +255,35 @@ describe("installPluginFromClawHub", () => {
expect(archiveCleanupMock).toHaveBeenCalledTimes(1);
});
it("marks official source-linked OpenClaw packages as trusted for install scanning", async () => {
fetchClawHubPackageDetailMock.mockResolvedValueOnce({
package: {
name: "demo",
displayName: "Demo",
family: "code-plugin",
channel: "official",
isOfficial: true,
createdAt: 0,
updatedAt: 0,
verification: {
tier: "source-linked",
sourceRepo: "openclaw/openclaw",
},
},
});
await installPluginFromClawHub({
spec: "clawhub:demo",
baseUrl: "https://clawhub.ai",
});
expect(installPluginFromArchiveMock).toHaveBeenCalledWith(
expect.objectContaining({
trustedSourceLinkedOfficialInstall: true,
}),
);
});
it("resolves explicit ClawHub dist tags before fetching version metadata", async () => {
parseClawHubPluginSpecMock.mockReturnValueOnce({ name: "demo", version: "latest" });
fetchClawHubPackageDetailMock.mockResolvedValueOnce({

View File

@@ -136,6 +136,7 @@ function normalizeClawHubClawPackInstallFields(
if (clawpack?.available !== true) {
return {};
}
const clawpackSha256 =
typeof clawpack.sha256 === "string" ? normalizeClawHubSha256Hex(clawpack.sha256) : null;
const clawpackManifestSha256 =
@@ -160,6 +161,18 @@ function normalizeClawHubClawPackInstallFields(
};
}
function isTrustedSourceLinkedOfficialPackage(pkg: NonNullable<ClawHubPackageDetail["package"]>) {
const sourceRepo = normalizeOptionalString(pkg.verification?.sourceRepo);
return (
pkg.channel === "official" &&
pkg.isOfficial === true &&
pkg.verification?.tier === "source-linked" &&
(sourceRepo === "openclaw/openclaw" ||
sourceRepo === "github.com/openclaw/openclaw" ||
sourceRepo === "https://github.com/openclaw/openclaw")
);
}
function resolveClawHubClawPackArtifactSha256(
clawpack: ClawHubPackageClawPackSummary | null | undefined,
): string | null {
@@ -943,6 +956,7 @@ export async function installPluginFromClawHub(
const installResult = await installPluginFromArchive({
archivePath: archive.archivePath,
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
trustedSourceLinkedOfficialInstall: isTrustedSourceLinkedOfficialPackage(detail.package!),
logger: params.logger,
mode: params.mode,
extensionsDir: params.extensionsDir,

View File

@@ -554,6 +554,7 @@ async function scanDirectoryTarget(params: {
function buildBlockedScanResult(params: {
builtinScan: BuiltinInstallScan;
dangerouslyForceUnsafeInstall?: boolean;
trustedSourceLinkedOfficialInstall?: boolean;
targetLabel: string;
}): InstallSecurityScanResult | undefined {
if (params.builtinScan.status === "error") {
@@ -568,7 +569,7 @@ function buildBlockedScanResult(params: {
};
}
if (params.builtinScan.critical > 0) {
if (params.dangerouslyForceUnsafeInstall) {
if (params.dangerouslyForceUnsafeInstall || params.trustedSourceLinkedOfficialInstall) {
return undefined;
}
return {
@@ -594,6 +595,16 @@ function logDangerousForceUnsafeInstall(params: {
);
}
function logTrustedSourceLinkedOfficialInstall(params: {
findings: Array<{ file: string; line: number; message: string; severity: string }>;
logger: InstallScanLogger;
targetLabel: string;
}) {
params.logger.warn?.(
`WARNING: ${params.targetLabel} allowed because it is an official source-linked ClawHub package: ${buildCriticalDetails({ findings: params.findings })}`,
);
}
function resolveBuiltinScanDecision(
params: InstallSafetyOverrides & {
builtinScan: BuiltinInstallScan;
@@ -604,6 +615,7 @@ function resolveBuiltinScanDecision(
const builtinBlocked = buildBlockedScanResult({
builtinScan: params.builtinScan,
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
trustedSourceLinkedOfficialInstall: params.trustedSourceLinkedOfficialInstall,
targetLabel: params.targetLabel,
});
if (params.dangerouslyForceUnsafeInstall && params.builtinScan.critical > 0) {
@@ -612,6 +624,12 @@ function resolveBuiltinScanDecision(
logger: params.logger,
targetLabel: params.targetLabel,
});
} else if (params.trustedSourceLinkedOfficialInstall && params.builtinScan.critical > 0) {
logTrustedSourceLinkedOfficialInstall({
findings: params.builtinScan.findings,
logger: params.logger,
targetLabel: params.targetLabel,
});
}
return builtinBlocked;
}
@@ -810,6 +828,7 @@ export async function scanPackageInstallSourceRuntime(
builtinScan,
logger: params.logger,
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
trustedSourceLinkedOfficialInstall: params.trustedSourceLinkedOfficialInstall,
targetLabel: `Plugin "${params.pluginId}" installation`,
});
@@ -913,6 +932,7 @@ export async function scanSkillInstallSourceRuntime(params: {
const builtinBlocked = buildBlockedScanResult({
builtinScan,
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
trustedSourceLinkedOfficialInstall: false,
targetLabel: `Skill "${params.skillName}" installation`,
});
if (params.dangerouslyForceUnsafeInstall && builtinScan.critical > 0) {

View File

@@ -1,3 +1,4 @@
export type InstallSafetyOverrides = {
dangerouslyForceUnsafeInstall?: boolean;
trustedSourceLinkedOfficialInstall?: boolean;
};

View File

@@ -213,11 +213,13 @@ async function installFromDirWithWarnings(params: {
pluginDir: string;
extensionsDir: string;
dangerouslyForceUnsafeInstall?: boolean;
trustedSourceLinkedOfficialInstall?: boolean;
mode?: "install" | "update";
}) {
const warnings: string[] = [];
const result = await installPluginFromDir({
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
trustedSourceLinkedOfficialInstall: params.trustedSourceLinkedOfficialInstall,
dirPath: params.pluginDir,
extensionsDir: params.extensionsDir,
mode: params.mode,
@@ -1871,6 +1873,36 @@ describe("installPluginFromArchive", () => {
).toBe(true);
});
it("allows package installs with dangerous code patterns for trusted source-linked official installs", async () => {
const { pluginDir, extensionsDir } = setupPluginInstallDirs();
fs.writeFileSync(
path.join(pluginDir, "package.json"),
JSON.stringify({
name: "official-dangerous-plugin",
version: "1.0.0",
openclaw: { extensions: ["index.js"] },
}),
);
fs.writeFileSync(
path.join(pluginDir, "index.js"),
`const { spawn } = require("child_process");\nspawn("google-chrome", []);`,
);
const { result, warnings } = await installFromDirWithWarnings({
pluginDir,
extensionsDir,
trustedSourceLinkedOfficialInstall: true,
});
expect(result.ok).toBe(true);
expect(
warnings.some((warning) =>
warning.includes("allowed because it is an official source-linked ClawHub package"),
),
).toBe(true);
});
it("does not flag the real qa-matrix plugin as dangerous install code", async () => {
const sourcePluginDir = path.resolve(process.cwd(), "extensions", "qa-matrix");
const pluginDir = path.join(suiteTempRootTracker.makeTempDir(), "qa-matrix");

View File

@@ -207,6 +207,7 @@ type PackageInstallCommonParams = InstallSafetyOverrides & {
type FileInstallCommonParams = Pick<
PackageInstallCommonParams,
| "dangerouslyForceUnsafeInstall"
| "trustedSourceLinkedOfficialInstall"
| "extensionsDir"
| "logger"
| "mode"
@@ -219,6 +220,7 @@ function pickPackageInstallCommonParams(
): PackageInstallCommonParams {
return {
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
trustedSourceLinkedOfficialInstall: params.trustedSourceLinkedOfficialInstall,
extensionsDir: params.extensionsDir,
npmDir: params.npmDir,
timeoutMs: params.timeoutMs,
@@ -567,6 +569,7 @@ async function validatePackagePluginInstallSource(params: {
expectedPluginId?: string;
requirePluginManifest?: boolean;
dangerouslyForceUnsafeInstall?: boolean;
trustedSourceLinkedOfficialInstall?: boolean;
installPolicyRequest?: PluginInstallPolicyRequest;
logger: PluginInstallLogger;
mode: "install" | "update";
@@ -691,6 +694,7 @@ async function validatePackagePluginInstallSource(params: {
scan: async () =>
await params.runtime.scanPackageInstallSource({
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
trustedSourceLinkedOfficialInstall: params.trustedSourceLinkedOfficialInstall,
packageDir: params.packageDir,
pluginId,
logger: params.logger,
@@ -762,6 +766,7 @@ export async function installPluginFromInstalledPackageDir(
expectedPluginId: params.expectedPluginId,
requirePluginManifest: params.requirePluginManifest,
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
trustedSourceLinkedOfficialInstall: params.trustedSourceLinkedOfficialInstall,
installPolicyRequest: params.installPolicyRequest,
logger,
mode: params.mode ?? "install",
@@ -823,6 +828,7 @@ async function installPluginFromPackageDir(
expectedPluginId: params.expectedPluginId,
requirePluginManifest: params.requirePluginManifest,
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
trustedSourceLinkedOfficialInstall: params.trustedSourceLinkedOfficialInstall,
installPolicyRequest: params.installPolicyRequest,
logger,
mode,