UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

340 lines (339 loc) • 12.5 kB
import { r as resolveStateDir } from "./state-dir-CfAW6S2K.mjs"; import { i as runCommandWithTimeout } from "./exec-DkOImFhW.mjs"; import "./process-runtime-DYJFKdDk.mjs"; import "./state-paths-BVLkme98.mjs"; import path from "node:path"; import fs from "node:fs/promises"; import { randomUUID } from "node:crypto"; //#region extensions/crabbox/src/crabbox-managed-binary.ts const CRABBOX_MIN_VERSION = "0.67.0"; const RELEASE_URL = `https://github.com/openclaw/crabbox/releases/download/v${CRABBOX_MIN_VERSION}`; const MAX_ARCHIVE_BYTES = 134217728; const VERSION_TIMEOUT_MS = 3e4; const DOWNLOAD_IDLE_TIMEOUT_MS = 3e4; const DOWNLOAD_TOTAL_TIMEOUT_MS = 6e5; async function probeCrabboxVersion(binary, runCommand = runCommandWithTimeout, signal) { signal?.throwIfAborted(); let result; try { result = await runCommand([binary, "--version"], { killProcessTree: true, maxOutputBytes: 65536, timeoutMs: VERSION_TIMEOUT_MS, ...signal ? { signal } : {} }); } catch { signal?.throwIfAborted(); return { status: "indeterminate", reason: "version command could not start" }; } signal?.throwIfAborted(); if (result.termination !== "exit" || result.code !== 0 || result.outputLimitExceeded) return { status: "indeterminate", reason: result.outputLimitExceeded ? "version output exceeded 64 KiB" : result.termination === "timeout" ? `version command timed out after ${VERSION_TIMEOUT_MS} ms` : `version command failed (${result.termination}, code ${result.code ?? "unknown"})` }; const match = /(?:^|\s)v?((\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)(?:\s|$)/u.exec(`${result.stdout}\n${result.stderr}`); if (!match) return { status: "indeterminate", reason: "version output was not recognized" }; const current = [ Number(match[2]), Number(match[3]), Number(match[4]) ]; if (current.some((part) => !Number.isSafeInteger(part))) return { status: "indeterminate", reason: "version output was not recognized" }; const minimum = CRABBOX_MIN_VERSION.split(".").map(Number); const difference = current.findIndex((part, index) => part !== minimum[index]); return { status: (difference === -1 ? !match[5] : current[difference] > minimum[difference]) ? "supported" : "outdated", version: match[1] }; } function releaseTarget() { const platform = process.platform === "win32" ? "windows" : process.platform; const arch = process.arch === "x64" ? "amd64" : process.arch; if (![ "darwin", "linux", "windows" ].includes(platform) || !["amd64", "arm64"].includes(arch)) throw new Error(`Crabbox has no managed release for ${process.platform}/${process.arch}`); const extension = platform === "windows" ? "zip" : "tar.gz"; return { directory: `${platform}-${arch}`, executable: platform === "windows" ? "crabbox.exe" : "crabbox", asset: `crabbox_${CRABBOX_MIN_VERSION}_${platform}_${arch}.${extension}`, tar: extension === "tar.gz" }; } function resolveManagedCrabboxBinaryPath(env = process.env) { const target = releaseTarget(); return path.join(resolveStateDir(env), "tools", "crabbox", CRABBOX_MIN_VERSION, target.directory, target.executable); } async function downloadReleaseFile(destination, name, maxBytes, signal) { const [{ buildTimeoutAbortSignal }, { fetchWithSsrFGuard }] = await Promise.all([import("./plugin-sdk/extension-shared.js"), import("./plugin-sdk/ssrf-runtime.js")]); signal.throwIfAborted(); const deadline = buildTimeoutAbortSignal({ timeoutMs: DOWNLOAD_TOTAL_TIMEOUT_MS, signal, operation: "crabbox-release-download" }); const { response, release, refreshTimeout } = await fetchWithSsrFGuard({ url: `${RELEASE_URL}/${name}`, requireHttps: true, timeoutMs: DOWNLOAD_IDLE_TIMEOUT_MS, signal: deadline.signal, policy: { hostnameAllowlist: ["github.com", "release-assets.githubusercontent.com"] }, auditContext: "crabbox-managed-release" }).catch((error) => { deadline.cleanup(); throw error; }); try { if (!response.ok) throw new Error(`Crabbox release download failed for ${name} (HTTP ${response.status})`); if (Number(response.headers.get("content-length")) > maxBytes) throw new Error(`Crabbox release file ${name} exceeds ${maxBytes} bytes`); if (!response.body) throw new Error(`Crabbox release file ${name} has no response body`); const body = response.body; const mode = 384 & ~process.umask(); await destination.create(name, (async function* () { for await (const chunk of body.values({ preventCancel: true })) { deadline.signal?.throwIfAborted(); if (chunk.byteLength > 0) { refreshTimeout?.(); yield chunk; } } })(), { maxBytes, signal: deadline.signal, mode, durable: false, mkdir: false }); } finally { deadline.cleanup(); await response.body?.cancel().catch(() => void 0); await release(); } } async function probeInstallation(binary, runCommand, signal) { if (!(await fs.lstat(binary).catch(() => void 0))?.isFile()) return; const result = await probeCrabboxVersion(binary, runCommand, signal); return result.status === "supported" ? { binary, version: result.version } : void 0; } async function findManagedCrabboxBinary(params = {}) { const runCommand = params.runCommand ?? ((argv, options) => runCommandWithTimeout(argv, { ...options, baseEnv: params.env })); const binary = resolveManagedCrabboxBinaryPath(params.env); if (await inspectInstallationDirectory(path.dirname(binary))) return probeInstallation(binary, runCommand, params.signal); } async function inspectInstallationDirectory(destination) { let stat; try { stat = await fs.lstat(destination); } catch (error) { const { extractErrorCode } = await import("./plugin-sdk/error-runtime.js"); if (extractErrorCode(error) === "ENOENT") return; throw error; } if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`Managed Crabbox destination must be a regular directory: ${destination}`); return stat; } async function publishInstallation(params) { const { binary, payload, runCommand, signal } = params; const destination = path.dirname(binary); const { withFileLock } = await import("./plugin-sdk/file-lock.js"); signal.throwIfAborted(); return withFileLock(`${destination}.publication`, { retries: { retries: 100, factor: 1, minTimeout: 100, maxTimeout: 100 }, stale: 12e4, staleRecovery: "remove-if-definitely-stale" }, async () => { signal.throwIfAborted(); const existing = await inspectInstallationDirectory(destination); const installed = existing ? await probeInstallation(binary, runCommand, signal) : void 0; if (installed) return installed; signal.throwIfAborted(); let recovery; if (existing) { recovery = `${destination}.recovery-${randomUUID()}`; await fs.rename(destination, recovery); } try { signal.throwIfAborted(); await fs.rename(payload, destination); return { binary, version: params.version }; } catch (error) { if (recovery) try { if (!await inspectInstallationDirectory(destination)) { await fs.rename(recovery, destination); recovery = void 0; } } catch (restoreError) { throw new AggregateError([error, restoreError], `Crabbox publication failed; previous installation is preserved at ${recovery}`, { cause: restoreError }); } if (!signal.aborted && await inspectInstallationDirectory(destination)) { const winner = await probeInstallation(binary, runCommand, signal); if (winner) return winner; } if (recovery) throw new Error(`Crabbox publication failed; previous installation is preserved at ${recovery}`, { cause: error }); throw error; } }); } async function installManagedBinary(binary, runCommand, signal) { const destination = path.dirname(binary); await inspectInstallationDirectory(destination); const installed = await probeInstallation(binary, runCommand, signal); if (installed) return installed; const parent = path.dirname(destination); await fs.mkdir(parent, { recursive: true, mode: 448 }); const staging = await fs.mkdtemp(path.join(parent, ".install-")); try { const { root, sha256File } = await import("./plugin-sdk/file-access-runtime.js"); const stagingRoot = await root(staging); const target = releaseTarget(); await downloadReleaseFile(stagingRoot, "checksums.txt", 65536, signal); const hashes = (await stagingRoot.readText("checksums.txt", { maxBytes: 65536 })).split(/\r?\n/u).flatMap((line) => { const match = /^([a-fA-F0-9]{64})\s+\*?(\S+)\s*$/u.exec(line); return match?.[2] === target.asset ? [match[1].toLowerCase()] : []; }); if (hashes.length !== 1) throw new Error(`Crabbox release checksums must contain exactly one entry for ${target.asset}`); await downloadReleaseFile(stagingRoot, target.asset, MAX_ARCHIVE_BYTES, signal); const archivePath = path.join(staging, target.asset); if ((await sha256File(archivePath, { maxBytes: MAX_ARCHIVE_BYTES, signal })).digest !== hashes[0]) throw new Error(`Crabbox release checksum mismatch for ${target.asset}`); const { extractArchive } = await import("./plugin-sdk/archive.js"); signal.throwIfAborted(); const payload = path.join(staging, "distribution"); await fs.mkdir(payload, { mode: 448 }); await extractArchive({ archivePath, destDir: payload, kind: target.tar ? "tar" : "zip", tarGzip: target.tar, timeoutMs: 0, limits: { maxArchiveBytes: MAX_ARCHIVE_BYTES, maxEntries: 128, maxExtractedBytes: 536870912, maxEntryBytes: 268435456 }, entryFilter: (entry) => entry.kind === "file" || entry.kind === "directory" ? "extract" : "skip", onFiltered: "reject-archive" }); const staged = await probeInstallation(path.join(payload, target.executable), runCommand, signal); if (!staged) throw new Error(`Downloaded Crabbox executable does not satisfy ${CRABBOX_MIN_VERSION}`); return await publishInstallation({ binary, version: staged.version, payload, runCommand, signal }); } finally { await fs.rm(staging, { recursive: true, force: true }); } } const acquisitions = /* @__PURE__ */ new Map(); async function ensureManagedCrabboxBinary(params = {}) { const { signal } = params; const runCommand = params.runCommand ?? ((argv, options) => runCommandWithTimeout(argv, { ...options, baseEnv: params.env, cwd: params.cwd })); const candidate = params.binary ?? "crabbox"; const binary = resolveManagedCrabboxBinaryPath(params.env); if (path.resolve(params.cwd ?? ".", candidate) === binary) await inspectInstallationDirectory(path.dirname(binary)); const preferred = await probeCrabboxVersion(candidate, runCommand, signal); if (preferred.status === "supported") return { binary: candidate, version: preferred.version }; const cached = await findManagedCrabboxBinary({ env: params.env, runCommand, signal }); if (cached) return cached; const { toErrorObject } = await import("./plugin-sdk/error-runtime.js"); signal?.throwIfAborted(); let acquisition = acquisitions.get(binary); if (acquisition?.controller.signal.aborted) { await acquisition.promise.catch(() => void 0); signal?.throwIfAborted(); acquisition = acquisitions.get(binary); } if (!acquisition) { const controller = new AbortController(); acquisition = { controller, waiters: 0, promise: installManagedBinary(binary, runCommand, controller.signal) }; acquisitions.set(binary, acquisition); const owned = acquisition; acquisition.promise.finally(() => { if (acquisitions.get(binary) === owned) acquisitions.delete(binary); }).catch(() => void 0); } const shared = acquisition; shared.waiters += 1; let waiting = true; const leave = () => { if (waiting) { waiting = false; shared.waiters -= 1; if (shared.waiters === 0) shared.controller.abort(); } }; let abandon = () => {}; const abandoned = new Promise((_resolve, reject) => { abandon = () => { leave(); if (shared.waiters > 0) reject(toErrorObject(signal?.reason, "Crabbox acquisition aborted")); }; }); signal?.addEventListener("abort", abandon, { once: true }); if (signal?.aborted) abandon(); try { const resolved = await Promise.race([shared.promise, abandoned]); signal?.throwIfAborted(); return resolved; } catch (error) { signal?.throwIfAborted(); throw error; } finally { signal?.removeEventListener("abort", abandon); leave(); } } //#endregion export { resolveManagedCrabboxBinaryPath as a, probeCrabboxVersion as i, ensureManagedCrabboxBinary as n, findManagedCrabboxBinary as r, CRABBOX_MIN_VERSION as t };