






















@@ -29,6 +29,11 @@ type ManagedNpmRootOpenClawMetadata = {
2929[key: string]: unknown;
3030};
313132+export type ManagedNpmRootPeerDependencySnapshot = {
33+dependencies: Record<string, string>;
34+managedPeerDependencies: string[];
35+};
36+3237export type ManagedNpmRootInstalledDependency = {
3338version?: string;
3439integrity?: string;
@@ -47,6 +52,16 @@ type ManagedNpmRootLogger = {
47524853type ManagedNpmRootRunCommand = typeof runCommandWithTimeout;
495455+type ManagedNpmPeerTraversalLimits = {
56+maxDepth: number;
57+maxDirectories: number;
58+};
59+60+const DEFAULT_MANAGED_NPM_PEER_TRAVERSAL_LIMITS: ManagedNpmPeerTraversalLimits = {
61+maxDepth: 64,
62+maxDirectories: 10_000,
63+};
64+5065function isRecord(value: unknown): value is Record<string, unknown> {
5166return typeof value === "object" && value !== null && !Array.isArray(value);
5267}
@@ -68,6 +83,47 @@ function readDependencyRecord(value: unknown): Record<string, string> {
6883return dependencies;
6984}
708586+function readPositiveIntegerEnv(name: string, fallback: number): number {
87+const rawValue = process.env[name];
88+if (!rawValue) {
89+return fallback;
90+}
91+const parsedValue = Number.parseInt(rawValue, 10);
92+return Number.isFinite(parsedValue) && parsedValue >= 1 ? parsedValue : fallback;
93+}
94+95+function resolveManagedNpmPeerTraversalLimits(): ManagedNpmPeerTraversalLimits {
96+return {
97+maxDepth: readPositiveIntegerEnv(
98+"OPENCLAW_INSTALL_SCAN_MAX_DEPTH",
99+DEFAULT_MANAGED_NPM_PEER_TRAVERSAL_LIMITS.maxDepth,
100+),
101+maxDirectories: readPositiveIntegerEnv(
102+"OPENCLAW_INSTALL_SCAN_MAX_DIRECTORIES",
103+DEFAULT_MANAGED_NPM_PEER_TRAVERSAL_LIMITS.maxDirectories,
104+),
105+};
106+}
107+108+function isSamePathOrInside(parentPath: string, candidatePath: string): boolean {
109+const relative = path.relative(parentPath, candidatePath);
110+return (
111+relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative))
112+);
113+}
114+115+function isSafePackageName(name: string): boolean {
116+if (name.startsWith("@")) {
117+const parts = name.split("/");
118+return (
119+parts.length === 2 && parts.every((part) => part.length > 0 && part !== "." && part !== "..")
120+);
121+}
122+return (
123+name.length > 0 && !name.includes("/") && !name.includes("\\") && name !== "." && name !== ".."
124+);
125+}
126+71127function readOverrideRecord(value: unknown): Record<string, unknown> {
72128if (!isRecord(value)) {
73129return {};
@@ -301,7 +357,7 @@ async function listNodeModulesPackageDirs(nodeModulesDir: string): Promise<strin
301357}
302358const packageDirs: string[] = [];
303359for (const entry of entries.toSorted((left, right) => left.name.localeCompare(right.name))) {
304-if (entry.name === ".bin" || entry.name.startsWith(".")) {
360+if (entry.name === ".bin" || entry.name === "openclaw" || entry.name.startsWith(".")) {
305361continue;
306362}
307363const entryPath = path.join(nodeModulesDir, entry.name);
@@ -338,20 +394,50 @@ async function collectManagedNpmRootPeerDependencyPins(params: {
338394npmRoot: string;
339395}): Promise<Record<string, string>> {
340396const pins = new Map<string, string>();
341-const queue = await listNodeModulesPackageDirs(path.join(params.npmRoot, "node_modules"));
397+const limits = resolveManagedNpmPeerTraversalLimits();
398+const boundaryRealPath = await fs
399+.realpath(params.npmRoot)
400+.catch(() => path.resolve(params.npmRoot));
401+const queue = (await listNodeModulesPackageDirs(path.join(params.npmRoot, "node_modules"))).map(
402+(packageDir) => ({ depth: 0, packageDir }),
403+);
404+const visitedRealPaths = new Set<string>();
342405for (let index = 0; index < queue.length; index += 1) {
343-const packageDir = queue[index];
344-if (!packageDir) {
406+const current = queue[index];
407+if (!current) {
408+continue;
409+}
410+if (current.depth > limits.maxDepth) {
411+throw new Error(
412+`managed npm peer dependency scan exceeded max depth (${limits.maxDepth}) at ${current.packageDir}`,
413+);
414+}
415+const packageDirRealPath = await fs
416+.realpath(current.packageDir)
417+.catch(() => path.resolve(current.packageDir));
418+if (!isSamePathOrInside(boundaryRealPath, packageDirRealPath)) {
419+throw new Error(
420+`managed npm peer dependency scan found package outside managed npm root at ${current.packageDir}`,
421+);
422+}
423+if (visitedRealPaths.has(packageDirRealPath)) {
345424continue;
346425}
426+visitedRealPaths.add(packageDirRealPath);
427+if (visitedRealPaths.size > limits.maxDirectories) {
428+throw new Error(
429+`managed npm peer dependency scan exceeded max packages (${limits.maxDirectories}) under ${params.npmRoot}`,
430+);
431+}
432+const packageDir = current.packageDir;
347433const manifest = await readPackageJsonIfExists(packageDir);
348434if (manifest) {
349435if (readOptionalString(manifest.name) === "openclaw") {
350436continue;
351437}
352438const peerDependencies = readDependencyRecord(manifest.peerDependencies);
353439for (const [peerName, peerRange] of Object.entries(peerDependencies)) {
354-if (peerName === "openclaw" || pins.has(peerName)) {
440+if (peerName === "openclaw" || pins.has(peerName) || !isSafePackageName(peerName)) {
355441continue;
356442}
357443const installedVersion = await readPackageVersion(
@@ -363,13 +449,69 @@ async function collectManagedNpmRootPeerDependencyPins(params: {
363449pins.set(peerName, installedVersion ?? peerRange);
364450}
365451}
366-queue.push(...(await listNodeModulesPackageDirs(path.join(packageDir, "node_modules"))));
452+queue.push(
453+ ...(await listNodeModulesPackageDirs(path.join(packageDir, "node_modules"))).map(
454+(nestedPackageDir) => ({
455+depth: current.depth + 1,
456+packageDir: nestedPackageDir,
457+}),
458+),
459+);
367460}
368461return Object.fromEntries(
369462[...pins.entries()].toSorted(([left], [right]) => left.localeCompare(right)),
370463);
371464}
372465466+export async function readManagedNpmRootPeerDependencySnapshot(params: {
467+npmRoot: string;
468+}): Promise<ManagedNpmRootPeerDependencySnapshot> {
469+const manifest = await readManagedNpmRootManifest(path.join(params.npmRoot, "package.json"));
470+const dependencies = readDependencyRecord(manifest.dependencies);
471+const managedPeerDependencies = readManagedPeerDependencyKeys(manifest.openclaw).toSorted();
472+const dependencySnapshot: Record<string, string> = {};
473+for (const packageName of managedPeerDependencies) {
474+const dependencySpec = dependencies[packageName];
475+if (dependencySpec) {
476+dependencySnapshot[packageName] = dependencySpec;
477+}
478+}
479+return {
480+dependencies: dependencySnapshot,
481+ managedPeerDependencies,
482+};
483+}
484+485+export async function restoreManagedNpmRootPeerDependencySnapshot(params: {
486+npmRoot: string;
487+snapshot: ManagedNpmRootPeerDependencySnapshot;
488+}): Promise<void> {
489+const manifestPath = path.join(params.npmRoot, "package.json");
490+const manifest = await readManagedNpmRootManifest(manifestPath);
491+const dependencies = readDependencyRecord(manifest.dependencies);
492+for (const packageName of readManagedPeerDependencyKeys(manifest.openclaw)) {
493+delete dependencies[packageName];
494+}
495+Object.assign(dependencies, params.snapshot.dependencies);
496+const managedOverrideKeys = readManagedOverrideKeys(manifest.openclaw).toSorted();
497+const openclawMetadata = buildManagedOpenClawMetadata({
498+current: manifest.openclaw,
499+ managedOverrideKeys,
500+managedPeerDependencyKeys: params.snapshot.managedPeerDependencies.toSorted(),
501+});
502+const next: ManagedNpmRootManifest = {
503+ ...manifest,
504+private: true,
505+ dependencies,
506+};
507+if (openclawMetadata) {
508+next.openclaw = openclawMetadata;
509+} else {
510+delete next.openclaw;
511+}
512+await writeJson(manifestPath, next, { trailingNewline: true });
513+}
514+373515export async function syncManagedNpmRootPeerDependencies(params: {
374516npmRoot: string;
375517managedOverrides?: Record<string, unknown>;
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。