UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

337 lines (336 loc) 10.7 kB
import { n as bindPluginCacheRoot, o as getPluginCacheRoot } from "./plugin-cache-DGWspMEc.js"; import { l as readFileDescriptorBoundedSync, s as openRootFileSync } from "./boundary-file-read-uaJcf6X6.js"; import { a as resolveRootPathSync } from "./boundary-path-DMNeww4q.js"; import { t as FsSafeError } from "./fs-safe-B6pvPGnf.js"; import { t as parseJsonWithJson5Fallback } from "./parse-json-compat-Cc8PAOTi.js"; import { i as readRegularFileSync } from "./regular-file-Equ4rrfE.js"; import fs from "node:fs"; import path from "node:path"; import crypto from "node:crypto"; //#region src/plugins/plugin-cache-files.ts const DEFAULT_PLUGIN_METADATA_MAX_BYTES = 16777216; function entryKey(relativePath, rejectHardlinks) { return JSON.stringify([path.normalize(relativePath), rejectHardlinks]); } function enforceFileSize(entry, maxBytes) { if (entry.ok && maxBytes !== void 0 && Math.max(entry.signature.size, entry.contents.length) > maxBytes) return { ok: false, failure: { ok: false, reason: "validation", error: new FsSafeError("too-large", `File exceeds ${maxBytes} bytes: ${entry.path}`) } }; return entry; } function pathFacts(targetPath) { const absolute = path.resolve(targetPath); const root = getPluginCacheRoot(path.dirname(absolute)); const key = path.basename(absolute); let facts = root.paths.get(key); if (!facts) { facts = {}; root.paths.set(key, facts); } return facts; } function pluginCacheExistsSync(targetPath) { const facts = pathFacts(targetPath); return facts.exists ??= fs.existsSync(targetPath); } function pluginCacheRealpathSync(targetPath, native = false) { const facts = pathFacts(targetPath); const key = native ? "nativeRealpath" : "realpath"; if (facts[key] === void 0) try { facts[key] = native ? fs.realpathSync.native(targetPath) : fs.realpathSync(targetPath); pathFacts(facts[key])[key] = facts[key]; } catch { facts[key] = null; } return facts[key]; } /** A trusted owner that changes permissions replaces its own observed stat. */ function refreshPluginCacheStat(targetPath) { pathFacts(targetPath).stat = void 0; return pluginCacheStatSync(targetPath); } function pluginCacheStatSync(targetPath) { const facts = pathFacts(targetPath); if (facts.stat === void 0) try { facts.stat = fs.statSync(targetPath); facts.exists = true; } catch { facts.stat = null; } return facts.stat; } /** Final symlink checks must retain lstat facts separately from followed target stats. */ function pluginCacheLstatSync(targetPath) { const facts = pathFacts(targetPath); if (facts.lstat === void 0) try { facts.lstat = fs.lstatSync(targetPath); } catch { facts.lstat = null; } return facts.lstat; } function readPluginCacheDirectory(targetPath) { const root = getPluginCacheRoot(targetPath); if (!root.directory) try { root.directory = { ok: true, entries: fs.readdirSync(targetPath, { withFileTypes: true }) }; } catch (error) { root.directory = { ok: false, error }; } if (!root.directory.ok) throw root.directory.error; return root.directory.entries; } /** Discovery validation is immutable metadata; actual imports validate their own file identity. */ function checkPluginCacheEntry(params) { let root = getPluginCacheRoot(params.rootDir); const key = entryKey(params.relativePath, params.rejectHardlinks); const cached = root.checkedEntries.get(key); if (cached) return cached; const absolutePath = path.resolve(params.rootDir, params.relativePath); let checked; if (!pluginCacheExistsSync(absolutePath)) try { const resolved = resolveRootPathSync({ absolutePath, rootPath: params.rootDir, rootCanonicalPath: params.rootRealPath, boundaryLabel: "plugin package directory" }); root = bindPluginCacheRoot(params.rootDir, resolved.rootCanonicalPath); checked = { ok: true, path: resolved.canonicalPath, rootRealPath: resolved.rootCanonicalPath, exists: false }; } catch (error) { checked = { ok: false, reason: "validation", error }; } else { const opened = openRootFileSync({ absolutePath, rootPath: params.rootDir, rootRealPath: params.rootRealPath, boundaryLabel: "plugin package directory", rejectHardlinks: params.rejectHardlinks }); if (!opened.ok) checked = opened; else { fs.closeSync(opened.fd); root = bindPluginCacheRoot(params.rootDir, opened.rootRealPath); Object.assign(pathFacts(opened.path), { exists: true, stat: opened.stat }); checked = { ok: true, path: opened.path, rootRealPath: opened.rootRealPath, exists: true }; } } root.checkedEntries.set(key, checked); return checked; } /** Reads metadata once under its original boundary policy, including absence and invalid files. */ function readPluginCacheFile(params) { const lexicalRoot = path.resolve(params.rootDir); let root = getPluginCacheRoot(lexicalRoot); const maxBytes = params.maxBytes === null ? void 0 : params.maxBytes ?? DEFAULT_PLUGIN_METADATA_MAX_BYTES; const key = entryKey(params.relativePath, params.rejectHardlinks); const limitKey = JSON.stringify([key, maxBytes]); const strictKey = entryKey(params.relativePath, true); const strict = params.rejectHardlinks ? void 0 : root.files.get(strictKey); const cached = root.files.get(key) ?? root.files.get(limitKey) ?? (strict?.ok ? strict : void 0); if (cached) return enforceFileSize(cached, maxBytes); const requestedPath = path.resolve(lexicalRoot, params.relativePath); const checked = root.checkedEntries.get(entryKey(params.relativePath, params.rejectHardlinks)); if (pathFacts(requestedPath).exists === false || checked && (!checked.ok || !checked.exists)) { const entry = { ok: false, failure: checked && !checked.ok ? checked : { ok: false, reason: "path" } }; root.files.set(key, entry); return entry; } const canonicalRoot = params.rootRealPath ?? pluginCacheRealpathSync(lexicalRoot) ?? lexicalRoot; const canonicalCached = getPluginCacheRoot(canonicalRoot).files.get(key); if (canonicalCached?.ok) { bindPluginCacheRoot(lexicalRoot, canonicalRoot); return enforceFileSize(canonicalCached, maxBytes); } const absolutePath = path.resolve(canonicalRoot, params.relativePath); const opened = openRootFileSync({ absolutePath, rootPath: canonicalRoot, rootRealPath: canonicalRoot, boundaryLabel: "plugin root", rejectHardlinks: params.rejectHardlinks, maxBytes }); let entry; if (!opened.ok) { entry = { ok: false, failure: opened }; if (opened.reason === "path" && opened.error && typeof opened.error === "object" && "code" in opened.error && opened.error.code === "ENOENT") { pathFacts(requestedPath).exists = false; pathFacts(absolutePath).exists = false; } } else { root = bindPluginCacheRoot(lexicalRoot, opened.rootRealPath); try { const contents = maxBytes === void 0 ? fs.readFileSync(opened.fd) : readFileDescriptorBoundedSync(opened.fd, maxBytes); entry = { ok: true, path: opened.path, rootRealPath: opened.rootRealPath, contents, hash: crypto.createHash("sha256").update(contents).digest("hex"), signature: { size: opened.stat.size, mtimeMs: opened.stat.mtimeMs, ctimeMs: opened.stat.ctimeMs } }; Object.assign(pathFacts(absolutePath), { exists: true, stat: opened.stat }); root.checkedEntries.set(entryKey(params.relativePath, params.rejectHardlinks), { ok: true, path: opened.path, rootRealPath: opened.rootRealPath, exists: true }); } catch (error) { entry = { ok: false, failure: { ok: false, reason: "io", error }, failurePhase: "read" }; } finally { fs.closeSync(opened.fd); } } root.files.set(entry.ok ? key : limitKey, entry); return entry; } /** Catalog files retain the regular-file policy, which rejects final symlinks but allows hardlinks. */ function readPluginCacheRegularFile(params) { const absolutePath = path.resolve(params.filePath); const lexicalRoot = path.dirname(absolutePath); const canonicalRoot = pluginCacheRealpathSync(lexicalRoot) ?? lexicalRoot; const root = bindPluginCacheRoot(lexicalRoot, canonicalRoot); const filename = path.basename(absolutePath); const key = JSON.stringify(["regular", filename]); const limitKey = JSON.stringify([ "regular", filename, params.maxBytes ]); let entry = root.files.get(key) ?? root.files.get(limitKey); if (!entry && pathFacts(absolutePath).exists === false) { entry = { ok: false, failure: { ok: false, reason: "path" } }; root.files.set(key, entry); } if (!entry) try { const { buffer: contents, stat } = readRegularFileSync({ filePath: absolutePath, maxBytes: params.maxBytes }); entry = { ok: true, path: path.join(canonicalRoot, filename), rootRealPath: canonicalRoot, contents, hash: crypto.createHash("sha256").update(contents).digest("hex"), signature: { size: stat.size, mtimeMs: stat.mtimeMs, ctimeMs: stat.ctimeMs } }; Object.assign(pathFacts(absolutePath), { exists: true, stat }); root.files.set(key, entry); } catch (error) { entry = { ok: false, failure: { ok: false, reason: "io", error } }; root.files.set(error instanceof FsSafeError && error.code === "too-large" ? limitKey : key, entry); if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") pathFacts(absolutePath).exists = false; } if (entry.ok && params.maxBytes !== void 0 && Math.max(entry.signature.size, entry.contents.length) > params.maxBytes) return { ok: false, failure: { ok: false, reason: "io", error: new FsSafeError("too-large", `File exceeds ${params.maxBytes} bytes: ${absolutePath}`) } }; return entry; } function readPluginCacheJsonFile(filePath, options = {}) { const file = readPluginCacheRegularFile({ filePath, ...options }); return file.ok ? parsePluginCacheJson(file) : { ok: false, error: file.failure.error }; } function parsePluginCacheJson(file, options = {}) { const key = options.json5 ? "json5" : "json"; if (!file[key]) try { const source = file.contents.toString("utf8"); file[key] = { ok: true, value: options.json5 ? parseJsonWithJson5Fallback(source) : JSON.parse(source) }; } catch (error) { file[key] = { ok: false, error }; } return file[key]; } //#endregion export { pluginCacheRealpathSync as a, readPluginCacheFile as c, pluginCacheLstatSync as i, readPluginCacheJsonFile as l, parsePluginCacheJson as n, pluginCacheStatSync as o, pluginCacheExistsSync as r, readPluginCacheDirectory as s, checkPluginCacheEntry as t, refreshPluginCacheStat as u };