UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

452 lines (451 loc) 16.7 kB
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 };