Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,7 @@ COPY agents/openclaw/wechat-runtime/package.json /usr/local/lib/nemoclaw/wechat-
COPY agents/openclaw/wechat-runtime/package-lock.json /usr/local/lib/nemoclaw/wechat-runtime/package-lock.json
COPY ci/npm-audit-exceptions.json /scripts/npm-audit-exceptions.json
COPY scripts/lib/reviewed-npm-archive.mts /scripts/lib/reviewed-npm-archive.mts
COPY scripts/lib/bundled-npm-package.mts /scripts/lib/bundled-npm-package.mts
COPY scripts/lib/reviewed-npm-audit.mts /scripts/lib/reviewed-npm-audit.mts
COPY scripts/lib/openclaw-npm-remediation.mts /scripts/lib/openclaw-npm-remediation.mts
COPY scripts/patch-bundled-npm-brace-expansion.mts /scripts/patch-bundled-npm-brace-expansion.mts
Expand Down Expand Up @@ -2313,6 +2314,7 @@ RUN check_metadata() { \
exit 1; \
fi; \
} \
&& check_metadata /scripts/lib/bundled-npm-package.mts 'root:root:644' \
&& check_metadata /scripts/patch-bundled-npm-brace-expansion.mts 'root:root:755' \
&& check_metadata /scripts/lib/patch-bundled-npm-ip-address.mts 'root:root:755' \
&& check_metadata /scripts/patch-bundled-npm-tar.mts 'root:root:755' \
Expand Down
1 change: 1 addition & 0 deletions Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ COPY agents/openclaw/mcporter-runtime/package.json \
/usr/local/lib/nemoclaw/mcporter-runtime/
COPY ci/npm-audit-exceptions.json /scripts/npm-audit-exceptions.json
COPY scripts/lib/reviewed-npm-archive.mts \
scripts/lib/bundled-npm-package.mts \
scripts/lib/reviewed-npm-audit.mts \
scripts/lib/openclaw-npm-remediation.mts \
/scripts/lib/
Expand Down
5 changes: 4 additions & 1 deletion agents/hermes/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ RUN set -eu; \
FROM scratch AS hermes-npm-patch-payload

COPY scripts/lib/reviewed-npm-archive.mts /scripts/lib/reviewed-npm-archive.mts
COPY scripts/lib/bundled-npm-package.mts /scripts/lib/bundled-npm-package.mts
COPY scripts/patch-bundled-npm-brace-expansion.mts /scripts/patch-bundled-npm-brace-expansion.mts
COPY scripts/lib/patch-bundled-npm-ip-address.mts /scripts/lib/patch-bundled-npm-ip-address.mts
COPY scripts/patch-bundled-npm-tar.mts /scripts/patch-bundled-npm-tar.mts
Expand Down Expand Up @@ -370,7 +371,8 @@ RUN chmod -R a+rX /opt/nemoclaw-hermes-plugin/
RUN find /opt/nemoclaw-hermes-config -type d -exec chmod 755 {} + \
&& find /opt/nemoclaw-hermes-config -type f -exec chmod 444 {} + \
&& chmod 444 /src/lib/hermes-managed-route.ts /src/lib/tool-disclosure.ts \
&& chmod 444 /scripts/lib/reviewed-npm-archive.mts /scripts/lib/openclaw-npm-remediation.mts \
&& chmod 444 /scripts/lib/reviewed-npm-archive.mts /scripts/lib/bundled-npm-package.mts \
/scripts/lib/openclaw-npm-remediation.mts \
/scripts/patch-bundled-npm-brace-expansion.mts /scripts/lib/patch-bundled-npm-ip-address.mts \
/scripts/patch-bundled-npm-tar.mts \
&& chmod -R a+rX /src/lib/messaging
Expand Down Expand Up @@ -1386,6 +1388,7 @@ RUN check_metadata() { \
&& check_absent /sandbox/.nemoclaw/hermes-cron-restore-drain.json \
&& check_absent /sandbox/.nemoclaw/hermes-cron-restore-release-recovery.json \
&& check_metadata /sandbox/.nemoclaw 'root:root 1755' \
&& check_metadata /scripts/lib/bundled-npm-package.mts 'root:root 444' \
&& check_metadata /scripts/patch-bundled-npm-brace-expansion.mts 'root:root 444' \
&& check_metadata /scripts/lib/patch-bundled-npm-ip-address.mts 'root:root 444' \
&& check_metadata /scripts/patch-bundled-npm-tar.mts 'root:root 444' \
Expand Down
1 change: 1 addition & 0 deletions agents/hermes/Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ RUN arch="$(dpkg --print-architecture)" \
&& test "$(npm --version)" = "11.16.0"

COPY scripts/lib/reviewed-npm-archive.mts /scripts/lib/reviewed-npm-archive.mts
COPY scripts/lib/bundled-npm-package.mts /scripts/lib/bundled-npm-package.mts
COPY scripts/patch-bundled-npm-brace-expansion.mts /scripts/patch-bundled-npm-brace-expansion.mts
COPY scripts/lib/patch-bundled-npm-ip-address.mts /scripts/lib/patch-bundled-npm-ip-address.mts
COPY scripts/patch-bundled-npm-tar.mts /scripts/patch-bundled-npm-tar.mts
Expand Down
1 change: 1 addition & 0 deletions agents/langchain-deepagents-code/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ RUN managed_runtime_assertion_failed() { \
&& install -d -o root -g root -m 0755 /run/nemoclaw

COPY scripts/lib/reviewed-npm-archive.mts /scripts/lib/reviewed-npm-archive.mts
COPY scripts/lib/bundled-npm-package.mts /scripts/lib/bundled-npm-package.mts
COPY scripts/patch-bundled-npm-brace-expansion.mts /scripts/patch-bundled-npm-brace-expansion.mts
COPY scripts/lib/patch-bundled-npm-ip-address.mts /scripts/lib/patch-bundled-npm-ip-address.mts
COPY scripts/patch-bundled-npm-tar.mts /scripts/patch-bundled-npm-tar.mts
Expand Down
1 change: 1 addition & 0 deletions agents/langchain-deepagents-code/Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ ARG NEMOCLAW_CORPORATE_CA_B64
COPY --from=perl-builder /out /tmp/nemoclaw-native-security

COPY scripts/lib/reviewed-npm-archive.mts /scripts/lib/reviewed-npm-archive.mts
COPY scripts/lib/bundled-npm-package.mts /scripts/lib/bundled-npm-package.mts
COPY scripts/patch-bundled-npm-brace-expansion.mts /scripts/patch-bundled-npm-brace-expansion.mts
COPY scripts/lib/patch-bundled-npm-ip-address.mts /scripts/lib/patch-bundled-npm-ip-address.mts
COPY scripts/patch-bundled-npm-tar.mts /scripts/patch-bundled-npm-tar.mts
Expand Down
1 change: 1 addition & 0 deletions agents/pi/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ RUN managed_runtime_assertion_failed() { \
&& install -d -o root -g root -m 0755 /run/nemoclaw

COPY scripts/lib/reviewed-npm-archive.mts /scripts/lib/reviewed-npm-archive.mts
COPY scripts/lib/bundled-npm-package.mts /scripts/lib/bundled-npm-package.mts
COPY scripts/patch-bundled-npm-brace-expansion.mts /scripts/patch-bundled-npm-brace-expansion.mts
COPY scripts/lib/patch-bundled-npm-ip-address.mts /scripts/lib/patch-bundled-npm-ip-address.mts
COPY scripts/patch-bundled-npm-tar.mts /scripts/patch-bundled-npm-tar.mts
Expand Down
1 change: 1 addition & 0 deletions agents/pi/Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ ARG PI_NPM_INTEGRITY
COPY --from=perl-builder /out /tmp/nemoclaw-native-security

COPY scripts/lib/reviewed-npm-archive.mts /scripts/lib/reviewed-npm-archive.mts
COPY scripts/lib/bundled-npm-package.mts /scripts/lib/bundled-npm-package.mts
COPY scripts/patch-bundled-npm-brace-expansion.mts /scripts/patch-bundled-npm-brace-expansion.mts
COPY scripts/lib/patch-bundled-npm-ip-address.mts /scripts/lib/patch-bundled-npm-ip-address.mts
COPY scripts/patch-bundled-npm-tar.mts /scripts/patch-bundled-npm-tar.mts
Expand Down
1 change: 1 addition & 0 deletions ci/full-e2e-cold-path-calibration.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@
"scripts/patch-openclaw-device-self-approval.mts",
"scripts/verify-wechat-runtime-lock.mts",
"scripts/lib/reviewed-npm-archive.mts",
"scripts/lib/bundled-npm-package.mts",
"src/lib/sandbox/build-context.ts"
],
"adjustedMetrics": [
Expand Down
115 changes: 115 additions & 0 deletions scripts/lib/bundled-npm-package.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import {
closeSync,
constants,
fstatSync,
lstatSync,
openSync,
readdirSync,
readFileSync,
realpathSync,
} from "node:fs";
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";

export type JsonObject = Record<string, unknown>;

export function jsonObject(value: unknown, label: string): JsonObject {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw new Error(`${label} must be a JSON object`);
}
return value as JsonObject;
}

export function readJsonObject(file: string, label: string): JsonObject {
const descriptor = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
try {
if (!fstatSync(descriptor).isFile()) throw new Error(`${label} must be a real file: ${file}`);
return jsonObject(JSON.parse(readFileSync(descriptor, "utf8")), label);
} catch (error) {
throw new Error(`${label} is invalid: ${String(error)}`);
} finally {
closeSync(descriptor);
}
}

export function requireRealDirectory(directory: string, label: string): string {
const resolved = resolve(directory);
const metadata = lstatSync(resolved);
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
throw new Error(`${label} must be a real directory: ${resolved}`);
}
return realpathSync(resolved);
}

export function rejectUnsafePackageTree(root: string, label: string): void {
for (const entry of readdirSync(root, { withFileTypes: true })) {
if (entry.isSymbolicLink() || (!entry.isDirectory() && !entry.isFile())) {
throw new Error(`${label} contains an unsafe member: ${entry.name}`);
}
if (entry.isDirectory()) rejectUnsafePackageTree(join(root, entry.name), label);
}
}

function isContainedBinSymlink(
nodeModulesRoot: string,
directory: string,
entryName: string,
): boolean {
if (basename(directory) !== ".bin") return false;
try {
const target = realpathSync(join(directory, entryName));
const targetRelative = relative(nodeModulesRoot, target);
return (
targetRelative !== "" &&
targetRelative !== ".." &&
!targetRelative.startsWith(`..${sep}`) &&
!isAbsolute(targetRelative) &&
lstatSync(target).isFile()
);
} catch {
return false;
}
}

export function collectBundledPackageVersions(options: {
ignoredDirectoryPrefixes: readonly string[];
nodeModulesRoot: string;
packageName: string;
}): string[] {
const versions: string[] = [];
const visit = (directory: string): void => {
for (const entry of readdirSync(directory, { withFileTypes: true })) {
if (entry.isSymbolicLink()) {
if (!isContainedBinSymlink(options.nodeModulesRoot, directory, entry.name)) {
throw new Error(`npm package contains an unsafe symlink: ${join(directory, entry.name)}`);
}
continue;
}
if (
entry.isDirectory() &&
options.ignoredDirectoryPrefixes.some((prefix) => entry.name.startsWith(prefix))
) {
continue;
}
const child = join(directory, entry.name);
if (!entry.isDirectory() && !entry.isFile()) {
throw new Error(`npm package contains an unsafe member: ${child}`);
}
if (entry.isDirectory()) {
visit(child);
continue;
}
if (entry.name !== "package.json") continue;
const manifest = readJsonObject(child, "npm bundled package manifest");
if (manifest.name !== options.packageName) continue;
if (typeof manifest.version !== "string") {
throw new Error(`npm bundled ${options.packageName} version is invalid`);
}
versions.push(manifest.version);
}
};
visit(options.nodeModulesRoot);
return versions;
}
123 changes: 17 additions & 106 deletions scripts/lib/patch-bundled-npm-ip-address.mts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,26 @@ import {
constants,
cpSync,
fstatSync,
lstatSync,
mkdirSync,
mkdtempSync,
openSync,
readdirSync,
readFileSync,
realpathSync,
renameSync,
rmSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

import {
type JsonObject,
collectBundledPackageVersions,
jsonObject as record,
readJsonObject as readJson,
rejectUnsafePackageTree,
requireRealDirectory as realDirectory,
} from "./bundled-npm-package.mts";

export const AFFECTED_IP_ADDRESS_VERSION = "10.2.0";
export const FIXED_IP_ADDRESS_VERSION = "10.3.1";
export const FIXED_IP_ADDRESS_INTEGRITY =
Expand All @@ -37,45 +43,6 @@ const REVIEWED_IP_ADDRESS_VERSIONS = new Set([
FIXED_IP_ADDRESS_VERSION,
]);

type JsonRecord = Record<string, unknown>;

function record(value: unknown, label: string): JsonRecord {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw new Error(`${label} must be a JSON object`);
}
return value as JsonRecord;
}

function readJson(file: string, label: string): JsonRecord {
const descriptor = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
try {
if (!fstatSync(descriptor).isFile()) throw new Error(`${label} must be a real file: ${file}`);
return record(JSON.parse(readFileSync(descriptor, "utf8")), label);
} catch (error) {
throw new Error(`${label} is invalid: ${String(error)}`);
} finally {
closeSync(descriptor);
}
}

function realDirectory(directory: string, label: string): string {
const resolved = resolve(directory);
const metadata = lstatSync(resolved);
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
throw new Error(`${label} must be a real directory: ${resolved}`);
}
return realpathSync(resolved);
}

function rejectUnsafeTree(root: string): void {
for (const entry of readdirSync(root, { withFileTypes: true })) {
if (entry.isSymbolicLink() || (!entry.isDirectory() && !entry.isFile())) {
throw new Error(`replacement ip-address package contains an unsafe member: ${entry.name}`);
}
if (entry.isDirectory()) rejectUnsafeTree(join(root, entry.name));
}
}

function removeBackup(backupPath: string): void {
try {
rmSync(backupPath, { force: true, recursive: true });
Expand All @@ -91,66 +58,7 @@ function removeBackup(backupPath: string): void {
}
}

function isContainedBinSymlink(
nodeModulesRoot: string,
directory: string,
entryName: string,
): boolean {
if (basename(directory) !== ".bin") return false;
try {
const target = realpathSync(join(directory, entryName));
const targetRelative = relative(nodeModulesRoot, target);
return (
targetRelative !== "" &&
targetRelative !== ".." &&
!targetRelative.startsWith(`..${sep}`) &&
!isAbsolute(targetRelative) &&
lstatSync(target).isFile()
);
} catch {
return false;
}
}

function collectIpAddressVersions(
directory: string,
nodeModulesRoot: string,
versions: string[],
): void {
for (const entry of readdirSync(directory, { withFileTypes: true })) {
if (entry.isSymbolicLink()) {
if (!isContainedBinSymlink(nodeModulesRoot, directory, entry.name)) {
throw new Error(`npm package contains an unsafe symlink: ${join(directory, entry.name)}`);
}
continue;
}
if (
entry.isDirectory() &&
(entry.name.startsWith(".ip-address.nemoclaw-stage-") ||
entry.name.startsWith("ip-address.nemoclaw-backup-"))
) {
continue;
}
if (!entry.isDirectory() && !entry.isFile()) {
throw new Error(`npm package contains an unsafe member: ${join(directory, entry.name)}`);
}
const child = join(directory, entry.name);
if (entry.isDirectory()) {
collectIpAddressVersions(child, nodeModulesRoot, versions);
continue;
}
if (entry.name !== "package.json") continue;
const manifest = readJson(child, "npm bundled package manifest");
if (manifest.name === "ip-address") {
if (typeof manifest.version !== "string") {
throw new Error("npm bundled ip-address version is invalid");
}
versions.push(manifest.version);
}
}
}

function verifyIpAddressManifest(manifest: JsonRecord): string {
function verifyIpAddressManifest(manifest: JsonObject): string {
const version = manifest.version;
const engines = record(manifest.engines, "npm bundled ip-address engines");
if (
Expand Down Expand Up @@ -196,9 +104,12 @@ export function inspectBundledNpmIpAddress(npmRoot: string): BundledNpmIpAddress
"npm bundled ip-address manifest",
),
);
const versions: string[] = [];
const nodeModulesRoot = realDirectory(join(root, "node_modules"), "npm node_modules root");
collectIpAddressVersions(nodeModulesRoot, nodeModulesRoot, versions);
const versions = collectBundledPackageVersions({
ignoredDirectoryPrefixes: [".ip-address.nemoclaw-stage-", "ip-address.nemoclaw-backup-"],
nodeModulesRoot,
packageName: "ip-address",
});
if (versions.length !== 1 || versions[0] !== version) {
throw new Error(`npm bundled ip-address layout has drifted: ${JSON.stringify(versions)}`);
}
Expand Down Expand Up @@ -226,7 +137,7 @@ export function patchBundledNpmIpAddress(options: {
}): BundledNpmIpAddressState {
const npmRoot = realDirectory(options.npmRoot, "npm package root");
const replacementRoot = realDirectory(options.replacementRoot, "replacement ip-address root");
rejectUnsafeTree(replacementRoot);
rejectUnsafePackageTree(replacementRoot, "replacement ip-address package");
const replacementVersion = verifyIpAddressManifest(
readJson(join(replacementRoot, "package.json"), "replacement ip-address manifest"),
);
Expand Down
Loading
Loading