mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
fix: trust official source-linked ClawHub plugins
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export type InstallSafetyOverrides = {
|
||||
dangerouslyForceUnsafeInstall?: boolean;
|
||||
trustedSourceLinkedOfficialInstall?: boolean;
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user