UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

567 lines (566 loc) • 20.8 kB
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs"; import { d as pathExists$1, t as FsSafeError } from "./fs-safe-CS1DrPTK.mjs"; import { t as hasErrnoCode } from "./errno-FR4CRyya.mjs"; import { i as runCommandWithTimeout } from "./exec-DkOImFhW.mjs"; import { h as writeJson, p as tryReadJson } from "./json-files-Br8qKrAz.mjs"; import { a as resolveNpmCommand, i as createNpmProjectInstallEnv } from "./npm-install-env-BzCWZDMx.mjs"; import { t as assertCanonicalPathWithinBase } from "./install-safe-path-CvmSx2MJ.mjs"; import { n as resolveInstallWorkTimeoutMs } from "./install-mode-options-HTpLEgtT.mjs"; import { n as formatNpmCommandFailureOutput } from "./install-source-utils-C8ESZ4tU.mjs"; import { t as retainMutationAuthority } from "./mutation-authority-E8BC9dNY.mjs"; import { n as isRemovalIoError, r as removePathWithinRoot } from "./fs-safe-remove-DuP7yp5L.mjs"; import fs from "node:fs"; import path from "node:path"; import fs$1 from "node:fs/promises"; import { randomUUID } from "node:crypto"; import { assertDirectoryIdentitySync, readDirectoryIdentity } from "@openclaw/fs-safe/advanced"; import { movePathWithCopyFallback } from "@openclaw/fs-safe/atomic"; //#region src/infra/install-progress.ts /** Only this operation's settlement completes its activity; later work cannot imply success. */ async function withInstallActivity(observer, stage, run, succeeded = (result) => !(result && typeof result === "object" && "ok" in result && result.ok === false)) { if (!observer?.activity) return await run(); const activityId = randomUUID(); const emit = (status) => { try { observer.activity?.({ activityId, stage, status }); } catch {} }; emit("started"); try { const result = await run(); emit(succeeded(result) ? "completed" : "failed"); return result; } catch (error) { emit("failed"); throw error; } } //#endregion //#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, omitOpenClawHostDependency) { const manifestPath = path.join(targetDir, "package.json"); const parsed = await tryReadJson(manifestPath); if (!isRecord(parsed)) return () => Promise.resolve(); const manifest = parsed; const originalManifest = await fs$1.readFile(manifestPath); let changed = false; if (Object.hasOwn(manifest, "devDependencies")) { delete manifest.devDependencies; changed = true; } if (omitOpenClawHostDependency) for (const key of [ "dependencies", "optionalDependencies", "peerDependencies", "peerDependenciesMeta" ]) { const dependencies = manifest[key]; if (!isRecord(dependencies) || !Object.hasOwn(dependencies, "openclaw")) continue; delete dependencies.openclaw; if (Object.keys(dependencies).length === 0) delete manifest[key]; changed = true; } if (changed) { await writeJson(manifestPath, manifest, { trailingNewline: true }); return async () => { await fs$1.writeFile(manifestPath, originalManifest); }; } return () => Promise.resolve(); } 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 }); } 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; } 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 copied package or a privately prepared package directory into its install target. * 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 = retainMutationAuthority(transactionRequest?.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 assertCanonicalPathWithinBase({ baseDir: installBaseDir, candidatePath: params.targetDir, boundaryLabel: "install directory" }); } 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)}` }; } const baseIdentity = await readDirectoryIdentity(installBaseRealPath); const assertDirectoryIdentity = (directory, identity) => { try { assertDirectoryIdentitySync(directory, { dev: identity.dev, ino: identity.ino }); } catch (error) { if (error instanceof FsSafeError && error.category === "policy") throw new Error(`install directory changed: ${directory}`, { cause: error }); throw error; } }; const assertRollbackOwned = () => { assertOwned(); assertDirectoryIdentity(installBaseRealPath, baseIdentity); }; const assertPersistentApply = retainMutationAuthority(() => { assertRollbackOwned(); return params.beforePersistentApply?.(); }); const removeInstallTree = async (removal) => { const assertCurrent = retainMutationAuthority(() => { removal.assertOwner(); assertDirectoryIdentity(installBaseRealPath, baseIdentity); if (fs.lstatSync(removal.directory, { throwIfNoEntry: false })) assertDirectoryIdentity(removal.directory, removal.identity); }); try { await removePathWithinRoot({ rootDir: installBaseRealPath, relativePath: path.relative(installBaseRealPath, removal.directory), recursive: removal.recursive !== false, force: true, symlinks: "unlink", assertBeforeMutation: assertCurrent }); } catch (error) { assertCurrent(); if (!removal.bestEffort || !isRemovalIoError(error)) throw error; } }; let stageDir = null; const published = { backup: null, install: null, restore: null }; const sourceHardlinks = (params.sourceHardlinks ?? DEFAULT_INSTALL_SOURCE_HARDLINKS) === "package-manager" ? "allow" : "reject"; let quarantine; const rollback = async () => { const installedIdentity = published.install; if (installedIdentity) { assertRollbackOwned(); if (published.backup && !published.restore) assertDirectoryIdentity(published.backup.path, published.backup); if (!quarantine) { const directory = await fs$1.mkdtemp(path.join(installBaseRealPath, ".openclaw-install-rollback-")); const identity = await readDirectoryIdentity(directory); 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; } } const detached = quarantine; await removeInstallTree({ directory: path.join(detached.directory, "package"), identity: installedIdentity, assertOwner: () => assertDirectoryIdentity(detached.directory, detached.identity) }); } await restoreBackup(); if (quarantine) await removeInstallTree({ directory: quarantine.directory, identity: quarantine.identity, assertOwner: () => {}, recursive: false }); published.install = null; }; const fail = async (error, cause) => { const installBaseChanged = isInstallBaseChangedError(cause); let restoreError; if (installBaseChanged) params.logger?.warn?.(INSTALL_BASE_CHANGED_ABORT_WARNING); else { try { await rollback(); } catch (restoreFailure) { restoreError = String(restoreFailure); } if (stageDir) { await cleanupInstallTempDir(stageDir); stageDir = null; } } return { ok: false, error: [error, ...[ restoreError && `could not restore existing install: ${restoreError}`, published.install && `install was published at ${published.install.path}; recovery incomplete`, published.backup && `backup recovery path: ${published.backup.path}` ].filter(Boolean)].join("; ") }; }; const restoreBackup = async () => { if (!published.backup) return; const restoring = published.backup; try { if (published.restore) { const restored = published.restore; await removeInstallTree({ directory: restoring.path, identity: restoring, assertOwner: () => { assertDirectoryIdentity(canonicalTargetDir, restored); assertRollbackOwned(); } }); } else await movePathWithCopyFallback({ assertBeforeRename: () => { assertDirectoryIdentity(restoring.path, restoring); if (fs.lstatSync(canonicalTargetDir, { throwIfNoEntry: false })) throw new Error(`install target changed during rollback: ${canonicalTargetDir}`); }, assertBeforeMutation: () => { assertDirectoryIdentity(restoring.path, restoring); assertRollbackOwned(); }, onDestinationPublished: (receipt) => { published.restore = receipt; }, from: restoring.path, sourceHardlinks, to: canonicalTargetDir }); published.backup = null; } catch (error) { const recovery = published.restore ? `original install published at ${published.restore.path}; cleanup incomplete at ${restoring.path}` : `backup retained at ${restoring.path}`; throw new Error(`${String(error)}; ${recovery}`, { cause: error }); } }; try { await assertCanonicalPathWithinBase({ baseDir: installBaseRealPath, candidatePath: canonicalTargetDir, boundaryLabel: "install directory" }); stageDir = await fs$1.mkdtemp(path.join(installBaseRealPath, ".openclaw-install-stage-")); if (params.sourceDir !== void 0) await withInstallActivity(params.logger, "files", () => 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) { const dependencyDir = stageDir; try { const restoreManifest = await sanitizeManifestForNpmInstall(stageDir, params.omitOpenClawHostDependency === true); let npmFailure; try { const hiddenProjectNpmConfig = await hideProjectNpmConfigForInstall(stageDir); params.logger?.info?.(params.depsLogMessage); const npmRes = await withInstallActivity(params.logger, "dependencies", async () => { try { return await runCommandWithTimeout(resolveNpmCommand([...createSafeNpmInstallArgs({ omitDev: true, loglevel: "error", ignoreWorkspaces: true })]), { timeoutMs: resolveInstallWorkTimeoutMs(params.workTimeoutMs, Math.max(params.timeoutMs, 3e5)), cwd: dependencyDir, env: createSafeNpmInstallEnv(process.env, { npmConfigCwd: dependencyDir, ignoreWorkspaces: true }) }); } finally { await restoreProjectNpmConfigAfterInstall(hiddenProjectNpmConfig); } }, (result) => result.code === 0); if (npmRes.code !== 0) npmFailure = `npm install failed: ${formatNpmCommandFailureOutput(npmRes)}`; } finally { await restoreManifest(); } if (npmFailure) return await fail(npmFailure); } 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$1(canonicalTargetDir)) { const backupRoot = path.join(installBaseRealPath, ".openclaw-install-backups"); const backupPath = path.join(backupRoot, `${path.basename(canonicalTargetDir)}-${randomUUID()}`); try { await fs$1.mkdir(backupRoot, { recursive: true }); await assertCanonicalPathWithinBase({ baseDir: installBaseRealPath, candidatePath: backupPath, boundaryLabel: "install directory" }); await assertInstallBaseStable({ installBaseDir, expectedRealPath: installBaseRealPath }); await movePathWithCopyFallback({ assertBeforeMutation: assertPersistentApply, onDestinationPublished: (receipt) => { published.backup = receipt; }, from: canonicalTargetDir, sourceHardlinks, to: backupPath }); } catch (err) { return await fail(`${params.copyErrorPrefix}: ${String(err)}`, err); } } if (published.backup && params.afterBackup) try { const backupResult = await params.afterBackup(published.backup.path); 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({ assertBeforeMutation: assertPersistentApply, onDestinationPublished: (receipt) => { published.install = receipt; }, from: stageDir, sourceHardlinks, to: canonicalTargetDir }); stageDir = null; } catch (err) { return await fail(`${params.copyErrorPrefix}: ${String(err)}`, err); } if (published.backup) try { await assertInstallBaseStable({ installBaseDir, expectedRealPath: installBaseRealPath }); } catch (err) { if (isInstallBaseChangedError(err)) params.logger?.warn?.(INSTALL_BASE_CHANGED_BACKUP_WARNING); published.backup = null; } if (published.backup && !deferCommit) await removeInstallTree({ directory: published.backup.path, identity: published.backup, assertOwner: assertPersistentApply, bestEffort: true }); if (!deferCommit) return { ok: true }; let settlement; const settle = (apply) => { settlement ??= Promise.resolve().then(() => { assertOwned(); return 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 (published.backup) await removeInstallTree({ directory: published.backup.path, identity: published.backup, assertOwner: assertRollbackOwned, bestEffort: true }); }), rollback: () => settle(rollback) }); } //#endregion export { resolvePackageDirInstallTransaction as a, withInstallActivity as c, requestDeferredPackageDirInstall as i, hasPackageRuntimeDependencies as n, createSafeNpmInstallArgs as o, installPackageDir as r, createSafeNpmInstallEnv as s, copyPackageDirInstallTransactionRequest as t };