UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

491 lines (490 loc) 17.9 kB
import { p as normalizeTrimmedStringList } from "./string-normalization-DsCfAx8q.js"; import { c as resolveUserPath } from "./home-dir-BPhrG-aM.js"; import { r as createLazyRuntimeModule } from "./lazy-runtime-CgCh8H_K.js"; import { t as CONFIG_DIR } from "./utils-P__uGsPB.js"; import { r as readRegularFile } from "./regular-file-Equ4rrfE.js"; import "./manifest-ByRdkf9X.js"; import { n as MANIFEST_KEY } from "./legacy-names-NIXaj2oi.js"; import { a as detectBundleManifestFormat } from "./bundle-manifest-C0Vv9-Hj.js"; import { o as unscopedPackageName, r as resolveSafeInstallDir } from "./install-safe-path-CVdg_-66.js"; import { n as hasPackageRuntimeDependencies, t as copyPackageDirInstallTransactionRequest } from "./install-package-dir-kJK8m3kQ.js"; import { o as scanInstalledPackageDependencyTree, s as scanPackageInstallSource } from "./install-security-scan-DYLR970X.js"; import { t as parseHookFrontmatter } from "./frontmatter-C1QsxwKH.js"; import path from "node:path"; //#region src/hooks/install.ts const HOOK_MD_MAX_BYTES = 1048576; const loadHookInstallRuntime = createLazyRuntimeModule(() => import("./install.runtime-BHAS8rQ4.js")); const HOOK_INSTALL_ERROR_CODE = { MISSING_OPENCLAW_HOOKS: "missing_openclaw_hooks", EMPTY_OPENCLAW_HOOKS: "empty_openclaw_hooks" }; const defaultLogger = {}; function buildHookInstallForwardParams(params) { return copyPackageDirInstallTransactionRequest(params, { config: params.config, dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall, onInstallPolicyWarning: params.onInstallPolicyWarning, trustedSourceLinkedOfficialInstall: params.trustedSourceLinkedOfficialInstall, hooksDir: params.hooksDir, timeoutMs: params.timeoutMs, logger: params.logger, mode: params.mode, dryRun: params.dryRun, expectedHookPackId: params.expectedHookPackId, expectedPackageKind: params.expectedPackageKind, inspection: params.inspection, beforePersistentApply: params.beforePersistentApply, installPolicyRequest: params.installPolicyRequest }); } function localHookInstallPolicySource(kind) { return kind === "plugin-archive" ? { kind: "archive", authority: "user", mutable: true, network: false } : { kind: "local-path", authority: "user", mutable: true, network: false }; } async function runHookInstallScan(params) { try { const result = await params.scan(); if (!result?.blocked) return null; return { ok: false, error: result.blocked.reason, ...result.blocked.code ? { code: result.blocked.code } : {} }; } catch (error) { return { ok: false, error: `Hook pack "${params.hookPackId}" installation blocked: install policy failed (${String(error)})`, code: "security_scan_failed" }; } } async function runHookInstallPolicy(params) { const request = params.forward.installPolicyRequest; if (!request) return null; return await runHookInstallScan({ hookPackId: params.hookPackId, scan: async () => await scanPackageInstallSource({ config: params.forward.config, dangerouslyForceUnsafeInstall: params.forward.dangerouslyForceUnsafeInstall, onInstallPolicyWarning: params.forward.onInstallPolicyWarning, trustedSourceLinkedOfficialInstall: params.forward.trustedSourceLinkedOfficialInstall, packageDir: params.packageDir, pluginId: params.hookPackId, extensions: params.hookEntries, ...params.packageName ? { packageName: params.packageName } : {}, ...params.version ? { version: params.version } : {}, logger: params.logger, requestKind: request.kind, requestedSpecifier: request.requestedSpecifier, source: request.source, mode: params.mode }) }); } async function runHookInstalledDependencyPolicy(params) { const request = params.forward.installPolicyRequest; if (!request) return null; return await runHookInstallScan({ hookPackId: params.hookPackId, scan: async () => await scanInstalledPackageDependencyTree({ config: params.forward.config, onInstallPolicyWarning: params.forward.onInstallPolicyWarning, trustedSourceLinkedOfficialInstall: params.forward.trustedSourceLinkedOfficialInstall, packageDir: params.installedDir, pluginId: params.hookPackId, logger: params.logger, requestKind: request.kind, requestedSpecifier: request.requestedSpecifier, source: request.source, mode: params.mode }) }); } function validateHookId(hookId) { if (!hookId) return "invalid hook name: missing"; if (hookId === "." || hookId === "..") return "invalid hook name: reserved path segment"; if (hookId.includes("/") || hookId.includes("\\")) return "invalid hook name: path separators not allowed"; return null; } /** Resolve the canonical local install directory for one hook pack id. */ function resolveHookInstallDir(hookId, hooksDir) { const hooksBase = hooksDir ? resolveUserPath(hooksDir) : path.join(CONFIG_DIR, "hooks"); const hookIdError = validateHookId(hookId); if (hookIdError) throw new Error(hookIdError); const targetDirResult = resolveSafeInstallDir({ baseDir: hooksBase, id: hookId, invalidNameMessage: "invalid hook name: path traversal detected" }); if (!targetDirResult.ok) throw new Error(targetDirResult.error); return targetDirResult.path; } function resolveOpenClawHooks(manifest) { const hooks = manifest[MANIFEST_KEY]?.hooks; if (!Array.isArray(hooks)) return { ok: false, error: "package.json missing openclaw.hooks", code: HOOK_INSTALL_ERROR_CODE.MISSING_OPENCLAW_HOOKS }; const list = normalizeTrimmedStringList(hooks); if (list.length === 0) return { ok: false, error: "package.json openclaw.hooks is empty", code: HOOK_INSTALL_ERROR_CODE.EMPTY_OPENCLAW_HOOKS }; return { ok: true, entries: list }; } function resolveHookPackageKind(manifest, packageKind) { if (packageKind) return packageKind; const extensions = manifest[MANIFEST_KEY]?.extensions; if (extensions === void 0) return "hook-only"; return Array.isArray(extensions) && normalizeTrimmedStringList(extensions).length === 0 ? "hook-only" : "plugin-capable"; } function resolveHookInstallTargetPath(id, hooksDir) { const baseHooksDir = hooksDir ? resolveUserPath(hooksDir) : path.join(CONFIG_DIR, "hooks"); const result = resolveSafeInstallDir({ baseDir: baseHooksDir, id, invalidNameMessage: "invalid hook name: path traversal detected" }); return result.ok ? { ok: true, targetDir: result.path } : result; } async function resolveInstallTargetDir(id, hooksDir) { const runtime = await loadHookInstallRuntime(); const baseHooksDir = hooksDir ? resolveUserPath(hooksDir) : path.join(CONFIG_DIR, "hooks"); return await runtime.resolveCanonicalInstallTarget({ baseDir: baseHooksDir, id, invalidNameMessage: "invalid hook name: path traversal detected", boundaryLabel: "hooks directory" }); } async function resolvePreparedHookInstallTarget(params) { const runtime = await loadHookInstallRuntime(); const targetDirResult = await resolveInstallTargetDir(params.id, params.hooksDir); if (!targetDirResult.ok) return targetDirResult; const targetDir = targetDirResult.targetDir; const effectiveMode = params.requestedMode === "update" && await runtime.fileExists(targetDir) ? "update" : "install"; const availability = await runtime.ensureInstallTargetAvailable({ mode: effectiveMode, targetDir, alreadyExistsError: params.alreadyExistsError(targetDir) }); if (!availability.ok) return availability; return { ok: true, target: { targetDir, effectiveMode } }; } async function installFromResolvedHookDir(resolvedDir, params) { const runtime = await loadHookInstallRuntime(); const manifestPath = path.join(resolvedDir, "package.json"); const packageKind = await runtime.fileExists(path.join(resolvedDir, "openclaw.plugin.json")) || detectBundleManifestFormat(resolvedDir) !== null ? "plugin-capable" : void 0; if (await runtime.fileExists(manifestPath)) return await installHookPackageFromDir({ packageDir: resolvedDir, ...packageKind ? { packageKind } : {}, ...buildHookInstallForwardParams(params) }); return await installHookFromDir({ hookDir: resolvedDir, ...packageKind ? { packageKind } : {}, ...buildHookInstallForwardParams(params) }); } async function resolveHookNameFromDir(hookDir) { const runtime = await loadHookInstallRuntime(); const hookMdPath = path.join(hookDir, "HOOK.md"); if (!await runtime.fileExists(hookMdPath)) throw new Error(`HOOK.md missing in ${hookDir}`); const { buffer } = await readRegularFile({ filePath: hookMdPath, maxBytes: HOOK_MD_MAX_BYTES }); return parseHookFrontmatter(buffer.toString("utf-8")).name || path.basename(hookDir); } async function validateHookDir(hookDir) { const runtime = await loadHookInstallRuntime(); const hookMdPath = path.join(hookDir, "HOOK.md"); if (!await runtime.fileExists(hookMdPath)) throw new Error(`HOOK.md missing in ${hookDir}`); const handlerCandidates = [ "handler.ts", "handler.js", "index.ts", "index.js" ]; const handlerEntry = handlerCandidates[(await Promise.all(handlerCandidates.map(async (candidate) => runtime.fileExists(path.join(hookDir, candidate))))).findIndex(Boolean)]; if (!handlerEntry) throw new Error(`handler.ts/handler.js/index.ts/index.js missing in ${hookDir}`); return { handlerEntry }; } async function installValidatedHookDirectory(params, source) { const runtime = await loadHookInstallRuntime(); const { logger, mode, dryRun, timeoutMs } = source.options; const { hookPackId, version } = source.metadata; if (params.inspection === "package-kind") { const target = resolveHookInstallTargetPath(hookPackId, params.hooksDir); return target.ok ? { ...target, ...source.metadata } : target; } const preparedTarget = await resolvePreparedHookInstallTarget({ id: hookPackId, hooksDir: params.hooksDir, requestedMode: mode, alreadyExistsError: (targetDir) => `${source.label} already exists: ${targetDir} (delete it first)` }); if (!preparedTarget.ok) return preparedTarget; const { targetDir, effectiveMode } = preparedTarget.target; const policyFailure = await runHookInstallPolicy({ hookPackId, hookEntries: source.hookEntries, packageName: source.packageName, version, packageDir: source.directory, forward: params, logger, mode: effectiveMode }); if (policyFailure) return policyFailure; if (dryRun) return { ok: true, ...source.metadata, targetDir }; const hasDeps = source.manifest ? hasPackageRuntimeDependencies(source.manifest) : false; const installRes = await runtime.installPackageDir(copyPackageDirInstallTransactionRequest(params, { sourceDir: source.directory, targetDir, mode: effectiveMode, timeoutMs, logger, copyErrorPrefix: `failed to copy ${source.label}`, depsLogMessage: `Installing ${source.label} dependencies…`, hasDeps, sourceHardlinks: hasDeps ? "package-manager" : "reject", beforePersistentApply: params.beforePersistentApply, afterInstall: async (installedDir) => { return await runHookInstalledDependencyPolicy({ hookPackId, installedDir, forward: params, logger, mode: effectiveMode }) ?? { ok: true }; } })); return installRes.ok ? { ...installRes, ...source.metadata, targetDir } : installRes; } async function installHookPackageFromDir(params) { const runtime = await loadHookInstallRuntime(); const options = runtime.resolveTimedInstallModeOptions(params, defaultLogger); const manifestPath = path.join(params.packageDir, "package.json"); if (!await runtime.fileExists(manifestPath)) return { ok: false, error: "package.json missing" }; let manifest; try { manifest = await runtime.readJsonFile(manifestPath); } catch (err) { return { ok: false, error: `invalid package.json: ${String(err)}` }; } const hookManifest = resolveOpenClawHooks(manifest); if (!hookManifest.ok) return hookManifest; const hookEntries = hookManifest.entries; const pkgName = typeof manifest.name === "string" ? manifest.name : ""; const hookPackId = pkgName ? unscopedPackageName(pkgName) : path.basename(params.packageDir); const packageKind = resolveHookPackageKind(manifest, params.packageKind); if (params.expectedPackageKind && packageKind !== params.expectedPackageKind) return { ok: false, error: `hook package kind mismatch: expected ${params.expectedPackageKind}, got ${packageKind}` }; const hookIdError = validateHookId(hookPackId); if (hookIdError) return { ok: false, error: hookIdError }; if (params.expectedHookPackId && params.expectedHookPackId !== hookPackId) return { ok: false, error: `hook pack id mismatch: expected ${params.expectedHookPackId}, got ${hookPackId}` }; const resolvedHooks = /* @__PURE__ */ new Set(); for (const entry of hookEntries) { const hookDir = path.resolve(params.packageDir, entry); if (!runtime.isPathInside(params.packageDir, hookDir)) return { ok: false, error: `openclaw.hooks entry escapes package directory: ${entry}` }; await validateHookDir(hookDir); if (!runtime.isPathInsideWithRealpath(params.packageDir, hookDir, { requireRealpath: true })) return { ok: false, error: `openclaw.hooks entry resolves outside package directory: ${entry}` }; const hookName = await resolveHookNameFromDir(hookDir); if (resolvedHooks.has(hookName)) return { ok: false, error: `duplicate hook name "${hookName}" in hook package` }; resolvedHooks.add(hookName); } const hookNames = [...resolvedHooks]; return await installValidatedHookDirectory(params, { directory: params.packageDir, label: "hook pack", hookEntries, packageName: pkgName, manifest, options, metadata: { hookPackId, hooks: hookNames, packageKind, version: typeof manifest.version === "string" ? manifest.version : void 0 } }); } async function installHookFromDir(params) { const options = { ...(await loadHookInstallRuntime()).resolveInstallModeOptions(params, defaultLogger), timeoutMs: 12e4 }; const { handlerEntry } = await validateHookDir(params.hookDir); const hookName = await resolveHookNameFromDir(params.hookDir); const packageKind = params.packageKind ?? "hook-only"; if (params.expectedPackageKind && packageKind !== params.expectedPackageKind) return { ok: false, error: `hook package kind mismatch: expected ${params.expectedPackageKind}, got ${packageKind}` }; const hookIdError = validateHookId(hookName); if (hookIdError) return { ok: false, error: hookIdError }; if (params.expectedHookPackId && params.expectedHookPackId !== hookName) return { ok: false, error: `hook id mismatch: expected ${params.expectedHookPackId}, got ${hookName}` }; return await installValidatedHookDirectory(params, { directory: params.hookDir, label: "hook", hookEntries: [handlerEntry], options, metadata: { hookPackId: hookName, hooks: [hookName], packageKind } }); } /** Install hooks from an archive after extracting and validating the archive root. */ async function installHooksFromArchive(params) { const runtime = await loadHookInstallRuntime(); const logger = params.logger ?? defaultLogger; const timeoutMs = params.timeoutMs ?? 12e4; const archivePathResult = await runtime.resolveArchiveSourcePath(params.archivePath); if (!archivePathResult.ok) return archivePathResult; const archivePath = archivePathResult.path; const installPolicyRequest = params.installPolicyRequest ?? { kind: "plugin-archive", requestedSpecifier: params.archivePath, source: localHookInstallPolicySource("plugin-archive") }; return await runtime.withExtractedArchiveRoot({ archivePath, tempDirPrefix: "openclaw-hook-", timeoutMs, logger, onExtracted: async (rootDir) => await installFromResolvedHookDir(rootDir, buildHookInstallForwardParams({ ...params, timeoutMs, logger, installPolicyRequest })) }); } /** Download, verify, and install an npm hook pack tarball. */ async function installHooksFromNpmSpec(params) { const runtime = await loadHookInstallRuntime(); const { logger, timeoutMs, mode, dryRun } = runtime.resolveTimedInstallModeOptions(params, defaultLogger); const spec = params.spec; logger.info?.(`Downloading ${spec.trim()}…`); return await runtime.installFromValidatedNpmSpecArchive({ tempDirPrefix: "openclaw-hook-pack-", spec, timeoutMs, expectedIntegrity: params.expectedIntegrity, onIntegrityDrift: params.onIntegrityDrift, warn: (message) => { logger.warn?.(message); }, installFromArchive: installHooksFromArchive, archiveInstallParams: buildHookInstallForwardParams({ ...params, timeoutMs, logger, mode, dryRun, installPolicyRequest: { kind: "plugin-npm", requestedSpecifier: spec, source: { kind: "npm", authority: "third-party", mutable: false, network: true } } }) }); } /** Install a hook pack or single hook from a local directory/archive path. */ async function installHooksFromPath(params) { const runtime = await loadHookInstallRuntime(); const pathResult = await runtime.resolveExistingInstallPath(params.path); if (!pathResult.ok) return pathResult; const { resolvedPath: resolved, stat } = pathResult; const installPolicyKind = stat.isDirectory() ? "plugin-dir" : "plugin-archive"; const forwardParams = buildHookInstallForwardParams({ ...params, installPolicyRequest: { kind: installPolicyKind, requestedSpecifier: params.path, source: localHookInstallPolicySource(installPolicyKind) } }); if (stat.isDirectory()) return await installFromResolvedHookDir(resolved, forwardParams); if (!runtime.resolveArchiveKind(resolved)) return { ok: false, error: `unsupported hook file: ${resolved}` }; return await installHooksFromArchive({ archivePath: resolved, ...forwardParams }); } //#endregion export { resolveHookInstallDir as i, installHooksFromNpmSpec as n, installHooksFromPath as r, HOOK_INSTALL_ERROR_CODE as t };