openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
452 lines (451 loc) • 16.7 kB
JavaScript
import { c as isRecord } from "./record-coerce-DItp3I4t.js";
import { t as hasErrnoCode } from "./errno-CkbDOfLk.js";
import { I as sameFileIdentity, d as pathExists } from "./fs-safe-B6pvPGnf.js";
import { t as movePathWithCopyFallback } from "./replace-file-BAJ-TWzD.js";
import { h as writeJson, p as tryReadJson } from "./json-files-Bq1lIlQB.js";
import { t as assertCanonicalPathWithinBase } from "./install-safe-path-CVdg_-66.js";
import { r as runCommandWithTimeout } from "./exec-BIE-3oLG.js";
import { i as createNpmProjectInstallEnv } from "./npm-install-env-DKWpWmP2.js";
import { r as formatNpmCommandFailureOutput } from "./install-source-utils-DPJygB4h.js";
import fs from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
//#region src/infra/safe-package-install.ts
/**
* Creates a project-local npm install environment for untrusted package dirs.
* It disables lifecycle scripts, global/workspace leakage, prompts, and noisy
* npm features while preserving caller-supplied process env values.
*/
function createSafeNpmInstallEnv(env, options = {}) {
const nextEnv = {
...createNpmProjectInstallEnv(env, options),
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
NPM_CONFIG_IGNORE_SCRIPTS: "true",
npm_config_audit: "false",
npm_config_fund: "false",
npm_config_ignore_scripts: "true",
npm_config_legacy_peer_deps: options.legacyPeerDeps ? "true" : "false",
npm_config_package_lock: options.packageLock === true ? "true" : "false",
npm_config_strict_peer_deps: "false",
...options.packageLock === true ? { npm_config_save: "true" } : {},
...options.ignoreWorkspaces ? { npm_config_workspaces: "false" } : {}
};
if (options.quiet) Object.assign(nextEnv, {
npm_config_loglevel: "error",
npm_config_progress: "false",
npm_config_yes: "true"
});
return nextEnv;
}
/**
* Builds npm install argv that mirrors the safe environment defaults.
* Callers opt into dependency omission, legacy peer resolution, and quiet flags.
*/
function createSafeNpmInstallArgs(options = {}) {
return [
"install",
...options.omitDev ? ["--omit=dev"] : [],
...options.omitPeer ? ["--omit=peer"] : [],
...options.legacyPeerDeps ? ["--legacy-peer-deps"] : [],
...options.loglevel ? [`--loglevel=${options.loglevel}`] : [],
"--ignore-scripts",
...options.ignoreWorkspaces ? ["--workspaces=false"] : [],
...options.noAudit ? ["--no-audit"] : [],
...options.noFund ? ["--no-fund"] : []
];
}
//#endregion
//#region src/infra/install-package-dir.ts
const DEFAULT_INSTALL_SOURCE_HARDLINKS = "reject";
const INSTALL_BASE_CHANGED_ERROR_MESSAGE = "install base directory changed during install";
const INSTALL_BASE_CHANGED_ABORT_WARNING = "Install base directory changed during install; aborting staged publish.";
const INSTALL_BASE_CHANGED_BACKUP_WARNING = "Install base directory changed before backup cleanup; leaving backup in place.";
const STAGED_NPM_PROJECT_CONFIG_NAME = ".npmrc";
const STAGED_NPM_PROJECT_CONFIG_PREFIX = ".openclaw-install-hidden-npmrc-";
function hasPackageRuntimeDependencies(manifest) {
return Object.keys(manifest.dependencies ?? {}).length > 0 || Object.keys(manifest.optionalDependencies ?? {}).length > 0;
}
async function sanitizeManifestForNpmInstall(targetDir) {
const manifestPath = path.join(targetDir, "package.json");
const parsed = await tryReadJson(manifestPath);
if (!isRecord(parsed)) return;
const manifest = parsed;
const devDependencies = manifest.devDependencies;
if (!isRecord(devDependencies)) return;
const filteredEntries = Object.entries(devDependencies).filter(([, rawSpec]) => {
return !(typeof rawSpec === "string" ? rawSpec.trim() : "").startsWith("workspace:");
});
if (filteredEntries.length === Object.keys(devDependencies).length) return;
if (filteredEntries.length === 0) delete manifest.devDependencies;
else manifest.devDependencies = Object.fromEntries(filteredEntries);
await writeJson(manifestPath, manifest, { trailingNewline: true });
}
async function hideProjectNpmConfigForInstall(targetDir) {
const originalPath = path.join(targetDir, STAGED_NPM_PROJECT_CONFIG_NAME);
let hiddenDir = "";
try {
hiddenDir = await fs$1.mkdtemp(path.join(targetDir, STAGED_NPM_PROJECT_CONFIG_PREFIX));
const hiddenPath = path.join(hiddenDir, STAGED_NPM_PROJECT_CONFIG_NAME);
await fs$1.rename(originalPath, hiddenPath);
return {
hiddenDir,
originalPath,
hiddenPath
};
} catch (error) {
if (hiddenDir) await fs$1.rm(hiddenDir, {
recursive: true,
force: true
}).catch(() => void 0);
if (hasErrnoCode(error, "ENOENT")) return null;
throw error;
}
}
async function restoreProjectNpmConfigAfterInstall(hiddenConfig) {
if (!hiddenConfig) return;
await fs$1.rename(hiddenConfig.hiddenPath, hiddenConfig.originalPath);
await fs$1.rm(hiddenConfig.hiddenDir, {
recursive: true,
force: true
});
}
async function assertInstallBoundaryPaths(params) {
for (const candidatePath of params.candidatePaths) await assertCanonicalPathWithinBase({
baseDir: params.installBaseDir,
candidatePath,
boundaryLabel: "install directory"
});
}
function isRelativePathInsideBase(relativePath) {
return Boolean(relativePath) && relativePath !== ".." && !relativePath.startsWith(`..${path.sep}`);
}
function isInstallBaseChangedError(error) {
return error instanceof Error && error.message === INSTALL_BASE_CHANGED_ERROR_MESSAGE;
}
function resolveMoveSourceHardlinks(policy) {
return policy === "package-manager" ? "allow" : "reject";
}
async function assertInstallBaseStable(params) {
if (!(await fs$1.stat(params.installBaseDir)).isDirectory()) throw new Error(INSTALL_BASE_CHANGED_ERROR_MESSAGE);
if (await fs$1.realpath(params.installBaseDir) !== params.expectedRealPath) throw new Error(INSTALL_BASE_CHANGED_ERROR_MESSAGE);
}
async function cleanupInstallTempDir(dirPath) {
if (!dirPath) return;
await fs$1.rm(dirPath, {
recursive: true,
force: true
}).catch(() => void 0);
}
async function resolveInstallPublishTarget(params) {
const installBaseResolved = path.resolve(params.installBaseDir);
const targetResolved = path.resolve(params.targetDir);
const targetRelativePath = path.relative(installBaseResolved, targetResolved);
if (!isRelativePathInsideBase(targetRelativePath)) throw new Error("invalid install target path");
const installBaseRealPath = await fs$1.realpath(params.installBaseDir);
return {
installBaseRealPath,
canonicalTargetDir: path.join(installBaseRealPath, targetRelativePath)
};
}
const PACKAGE_DIR_INSTALL_TRANSACTION = Symbol.for("openclaw.packageDirInstallTransaction");
const PACKAGE_DIR_INSTALL_TRANSACTION_REQUEST = Symbol.for("openclaw.packageDirInstallTransactionRequest");
function requestDeferredPackageDirInstall(params, assertOwned) {
Object.defineProperty(params, PACKAGE_DIR_INSTALL_TRANSACTION_REQUEST, {
configurable: false,
enumerable: true,
value: { assertOwned }
});
return params;
}
function copyPackageDirInstallTransactionRequest(source, target) {
const request = resolvePackageDirInstallTransactionRequest(source);
return request ? requestDeferredPackageDirInstall(target, request.assertOwned) : target;
}
function resolvePackageDirInstallTransactionRequest(params) {
return params[PACKAGE_DIR_INSTALL_TRANSACTION_REQUEST];
}
function attachPackageDirInstallTransaction(result, transaction) {
Object.defineProperty(result, PACKAGE_DIR_INSTALL_TRANSACTION, {
configurable: false,
enumerable: true,
value: transaction
});
return result;
}
function resolvePackageDirInstallTransaction(result) {
return result[PACKAGE_DIR_INSTALL_TRANSACTION];
}
/**
* Publishes a package directory into an install target via a staged copy.
* Update mode backs up the existing target, runs optional validation hooks,
* and rolls back when copy, dependency install, or validation fails.
*/
async function installPackageDir(params) {
const transactionRequest = resolvePackageDirInstallTransactionRequest(params);
const deferCommit = transactionRequest !== void 0;
const assertOwned = transactionRequest?.assertOwned;
const assertPersistentApply = () => {
params.beforePersistentApply?.();
assertOwned?.();
};
params.logger?.info?.(`Installing to ${params.targetDir}…`);
const installBaseDir = path.dirname(params.targetDir);
let initialInstallBaseRealPath;
try {
await fs$1.mkdir(installBaseDir, { recursive: true });
initialInstallBaseRealPath = await fs$1.realpath(installBaseDir);
await assertInstallBoundaryPaths({
installBaseDir,
candidatePaths: [params.targetDir]
});
} catch (err) {
return {
ok: false,
error: `${params.copyErrorPrefix}: ${String(err)}`
};
}
let installBaseRealPath;
let canonicalTargetDir;
try {
({installBaseRealPath, canonicalTargetDir} = await resolveInstallPublishTarget({
installBaseDir,
targetDir: params.targetDir
}));
if (installBaseRealPath !== initialInstallBaseRealPath) throw new Error(INSTALL_BASE_CHANGED_ERROR_MESSAGE);
} catch (err) {
if (isInstallBaseChangedError(err)) params.logger?.warn?.(INSTALL_BASE_CHANGED_ABORT_WARNING);
return {
ok: false,
error: `${params.copyErrorPrefix}: ${String(err)}`
};
}
let stageDir = null;
let backupDir = null;
const sourceHardlinks = resolveMoveSourceHardlinks(params.sourceHardlinks ?? DEFAULT_INSTALL_SOURCE_HARDLINKS);
const fail = async (error, cause) => {
const installBaseChanged = isInstallBaseChangedError(cause);
let restoreError;
if (installBaseChanged) params.logger?.warn?.(INSTALL_BASE_CHANGED_ABORT_WARNING);
else {
restoreError = await restoreBackup();
if (stageDir) {
await cleanupInstallTempDir(stageDir);
stageDir = null;
}
}
return {
ok: false,
error: restoreError ? `${error}; could not restore existing install: ${restoreError}` : error
};
};
const restoreBackup = async () => {
if (!backupDir) return;
try {
await movePathWithCopyFallback({
assertBeforeRename: assertOwned,
from: backupDir,
sourceHardlinks,
to: canonicalTargetDir
});
backupDir = null;
return;
} catch (error) {
return String(error);
}
};
try {
await assertInstallBoundaryPaths({
installBaseDir: installBaseRealPath,
candidatePaths: [canonicalTargetDir]
});
stageDir = await fs$1.mkdtemp(path.join(installBaseRealPath, ".openclaw-install-stage-"));
await fs$1.cp(params.sourceDir, stageDir, {
recursive: true,
verbatimSymlinks: true
});
} catch (err) {
return await fail(`${params.copyErrorPrefix}: ${String(err)}`, err);
}
try {
await params.afterCopy?.(stageDir);
} catch (err) {
return await fail(`post-copy validation failed: ${String(err)}`, err);
}
if (params.hasDeps) try {
await sanitizeManifestForNpmInstall(stageDir);
const hiddenProjectNpmConfig = await hideProjectNpmConfigForInstall(stageDir);
params.logger?.info?.(params.depsLogMessage);
const npmRes = await (async () => {
try {
return await runCommandWithTimeout(["npm", ...createSafeNpmInstallArgs({
omitDev: true,
loglevel: "error"
})], {
timeoutMs: Math.max(params.timeoutMs, 3e5),
cwd: stageDir,
env: createSafeNpmInstallEnv(process.env, { npmConfigCwd: stageDir })
});
} finally {
await restoreProjectNpmConfigAfterInstall(hiddenProjectNpmConfig);
}
})();
if (npmRes.code !== 0) return await fail(`npm install failed: ${formatNpmCommandFailureOutput(npmRes)}`);
} catch (error) {
return await fail(`npm install failed: ${String(error)}`, error);
}
if (params.afterInstall) try {
const postInstallResult = await params.afterInstall(stageDir);
if (!postInstallResult.ok) {
const failed = await fail(postInstallResult.error);
return {
...postInstallResult,
error: failed.error
};
}
} catch (err) {
return await fail(`post-install validation failed: ${String(err)}`, err);
}
if (params.mode === "update" && await pathExists(canonicalTargetDir)) {
const backupRoot = path.join(installBaseRealPath, ".openclaw-install-backups");
const backupPath = path.join(backupRoot, `${path.basename(canonicalTargetDir)}-${Date.now()}`);
try {
await fs$1.mkdir(backupRoot, { recursive: true });
await assertInstallBoundaryPaths({
installBaseDir: installBaseRealPath,
candidatePaths: [backupPath]
});
await assertInstallBaseStable({
installBaseDir,
expectedRealPath: installBaseRealPath
});
backupDir = backupPath;
await movePathWithCopyFallback({
assertBeforeRename: assertPersistentApply,
from: canonicalTargetDir,
sourceHardlinks,
to: backupDir
});
} catch (err) {
await fs$1.lstat(backupPath).catch((error) => {
if (hasErrnoCode(error, "ENOENT")) backupDir = null;
});
return await fail(`${params.copyErrorPrefix}: ${String(err)}`, err);
}
}
if (backupDir && params.afterBackup) try {
const backupResult = await params.afterBackup(backupDir);
if (!backupResult.ok) {
const failed = await fail(backupResult.error);
return {
...backupResult,
error: failed.error
};
}
} catch (err) {
return await fail(`backup validation failed: ${String(err)}`, err);
}
try {
await assertInstallBaseStable({
installBaseDir,
expectedRealPath: installBaseRealPath
});
await movePathWithCopyFallback({
assertBeforeRename: assertPersistentApply,
from: stageDir,
sourceHardlinks,
to: canonicalTargetDir
});
stageDir = null;
} catch (err) {
return await fail(`${params.copyErrorPrefix}: ${String(err)}`, err);
}
if (backupDir) try {
await assertInstallBaseStable({
installBaseDir,
expectedRealPath: installBaseRealPath
});
} catch (err) {
if (isInstallBaseChangedError(err)) params.logger?.warn?.(INSTALL_BASE_CHANGED_BACKUP_WARNING);
backupDir = null;
}
if (backupDir && !deferCommit) await fs$1.rm(backupDir, {
recursive: true,
force: true
}).catch(() => void 0);
if (stageDir) await cleanupInstallTempDir(stageDir);
if (!deferCommit) return { ok: true };
const installedIdentity = fs.lstatSync(canonicalTargetDir, { bigint: true });
const baseIdentity = fs.lstatSync(installBaseRealPath, { bigint: true });
const assertDirectoryIdentity = (directory, identity) => {
const current = fs.lstatSync(directory, { bigint: true });
if (!current.isDirectory() || !sameFileIdentity(current, identity)) throw new Error(`install directory changed during rollback: ${directory}`);
};
const assertRollbackOwned = () => {
assertDirectoryIdentity(installBaseRealPath, baseIdentity);
assertOwned?.();
};
let quarantine;
let settlement;
const settle = (apply) => {
settlement ??= Promise.resolve().then(apply).catch((error) => {
settlement = void 0;
throw error;
});
return settlement;
};
return attachPackageDirInstallTransaction({ ok: true }, {
commit: () => settle(async () => {
if (quarantine) throw new Error("cannot commit an install after rollback has started");
assertOwned?.();
if (backupDir) await fs$1.rm(backupDir, {
recursive: true,
force: true
}).catch(() => void 0);
}),
rollback: () => settle(async () => {
assertRollbackOwned();
if (!quarantine) {
const directory = await fs$1.mkdtemp(path.join(installBaseRealPath, ".openclaw-install-rollback-"));
const identity = fs.lstatSync(directory, { bigint: true });
try {
assertDirectoryIdentity(directory, identity);
assertDirectoryIdentity(canonicalTargetDir, installedIdentity);
assertRollbackOwned();
fs.renameSync(canonicalTargetDir, path.join(directory, "package"));
quarantine = {
directory,
identity
};
} catch (error) {
await fs$1.rmdir(directory).catch(() => void 0);
throw error;
}
}
assertDirectoryIdentity(quarantine.directory, quarantine.identity);
const discardedPackage = path.join(quarantine.directory, "package");
if (fs.lstatSync(discardedPackage, {
bigint: true,
throwIfNoEntry: false
})) assertDirectoryIdentity(discardedPackage, installedIdentity);
await fs$1.rm(discardedPackage, {
recursive: true,
force: true
});
if (backupDir) {
await movePathWithCopyFallback({
assertBeforeRename: () => {
if (fs.lstatSync(canonicalTargetDir, { throwIfNoEntry: false })) throw new Error(`install target changed during rollback: ${canonicalTargetDir}`);
assertRollbackOwned();
},
from: backupDir,
sourceHardlinks,
to: canonicalTargetDir
});
backupDir = null;
}
await fs$1.rmdir(quarantine.directory);
})
});
}
//#endregion
export { resolvePackageDirInstallTransaction as a, requestDeferredPackageDirInstall as i, hasPackageRuntimeDependencies as n, createSafeNpmInstallArgs as o, installPackageDir as r, createSafeNpmInstallEnv as s, copyPackageDirInstallTransactionRequest as t };