UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

285 lines (284 loc) • 12.5 kB
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs"; import { t as WorkerProviderError } from "./capability-provider.types-BsBg4rOj.mjs"; import "./string-coerce-runtime-50qKeWeL.mjs"; import "./plugin-entry-2l_2QeB2.mjs"; import { i as CRABBOX_HEARTBEAT_TIMEOUT_MS } from "./crabbox-worker-timeouts-DXsC1sef.mjs"; import path from "node:path"; import { createHash } from "node:crypto"; //#region extensions/crabbox/src/crabbox-worker-profile.ts const PROFILE_KEYS = /* @__PURE__ */ new Set([ "binary", "class", "desktop", "idleTimeout", "provider", "setup", "setupEnv", "ttl", "target", "warmImage" ]); const GO_DURATION_PATTERN = /^\+?(?:(?:\d+(?:\.\d*)?|\.\d+)(?:ns|us|µs|μs|ms|s|m|h))+$/u; const GO_DURATION_TOKEN_PATTERN = /(\d+(?:\.\d*)?|\.\d+)(ns|us|µs|μs|ms|s|m|h)/gu; const MAX_GO_DURATION_NANOSECONDS = 9223372036854775807n; const CRABBOX_LEASE_ID_DOMAIN = "openclaw:crabbox-worker-lease-id:v1\0"; const LEGACY_PROVISION_OPERATION_ID_PATTERN = /^provision:[a-f0-9]{64}$/u; const DURATION_UNIT_NANOSECONDS = { h: 3600000000000n, m: 60000000000n, s: 1000000000n, ms: 1000000n, us: 1000n, µs: 1000n, μs: 1000n, ns: 1n }; const MAX_CRABBOX_MACHINE_CLASS_LENGTH = 128; const MAX_CRABBOX_MACHINE_OPTIONS = 64; const CRABBOX_ENROLLABLE_TARGETS = [ "linux", "windows/wsl2", "windows/normal", "macos" ]; const CRABBOX_OS_LABELS = { linux: "Linux", "windows/wsl2": "Windows (WSL2)", "windows/normal": "Windows", macos: "macOS" }; function parseCrabboxOperatingSystem(value) { if (value === void 0) return "linux"; const target = normalizeOptionalString(value); const supported = CRABBOX_ENROLLABLE_TARGETS.find((os) => os === target); if (supported) return supported; throw new WorkerProviderError(`Crabbox target must be ${CRABBOX_ENROLLABLE_TARGETS.join(" or ")}`); } const CRABBOX_DESKTOP_PROVIDERS = /* @__PURE__ */ new Set([ "aws", "azure", "hetzner" ]); const CRABBOX_WORKER_PROVIDER_ID = "crabbox"; function requirePositiveDuration(value, key) { const duration = normalizeOptionalString(value); const nanoseconds = duration ? parsePositiveGoDurationNanoseconds(duration) : void 0; if (!duration || nanoseconds === void 0) throw new WorkerProviderError(`Crabbox profile ${key} must be a positive Go duration such as 60m`); return { duration, milliseconds: Number(nanoseconds) / 1e6 }; } function parsePositiveGoDurationNanoseconds(duration) { if (!GO_DURATION_PATTERN.test(duration)) return; let total = 0n; for (const match of duration.matchAll(GO_DURATION_TOKEN_PATTERN)) { const numberText = match[1]; const unit = match[2] ? DURATION_UNIT_NANOSECONDS[match[2]] : void 0; if (!numberText || unit === void 0) return; const [wholeText = "", fractionText = ""] = numberText.split(".", 2); const whole = wholeText.replace(/^0+/u, "") || "0"; if (whole.length > 19) return; total += BigInt(whole) * unit; const fraction = fractionText.slice(0, 18); if (fraction) total += BigInt(fraction) * unit / 10n ** BigInt(fraction.length); if (total > MAX_GO_DURATION_NANOSECONDS) return; } return total > 0n ? total : void 0; } function heartbeatIntervalMs(idleTimeoutMs) { const referenceIntervalMs = Math.max(5e3, Math.min(6e4, idleTimeoutMs / 3)); return Math.min(referenceIntervalMs, Math.max(1, Math.floor(idleTimeoutMs / 2))); } function parseCrabboxProfile(profile) { for (const key of Object.keys(profile)) if (!PROFILE_KEYS.has(key)) throw new WorkerProviderError(`unknown Crabbox profile setting: ${key}`); const provider = normalizeOptionalString(profile.provider)?.toLowerCase(); const machineClass = normalizeOptionalString(profile.class); const target = parseCrabboxOperatingSystem(profile.target); if (!provider) throw new WorkerProviderError("Crabbox profile provider must be a non-empty string"); if (profile.class !== void 0 && !machineClass) throw new WorkerProviderError("Crabbox profile class must be a non-empty string"); const { duration: ttl } = requirePositiveDuration(profile.ttl, "ttl"); const { duration: idleTimeout, milliseconds: idleTimeoutMs } = requirePositiveDuration(profile.idleTimeout, "idleTimeout"); const binary = normalizeOptionalString(profile.binary); if (profile.binary !== void 0 && !binary) throw new WorkerProviderError("Crabbox profile binary must be a non-empty string"); if (binary && !path.isAbsolute(binary)) throw new WorkerProviderError("Crabbox profile binary must be an absolute path"); const setup = normalizeOptionalString(profile.setup); if (profile.setup !== void 0 && !setup) throw new WorkerProviderError("Crabbox profile setup must be a non-empty command string"); let setupEnv; if (profile.setupEnv !== void 0) { if (!Array.isArray(profile.setupEnv)) throw new WorkerProviderError("Crabbox profile setupEnv must be an array"); if (profile.setupEnv.length > 16) throw new WorkerProviderError("Crabbox profile setupEnv must contain at most 16 names"); setupEnv = profile.setupEnv.map((name) => { if (typeof name !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name)) throw new WorkerProviderError("Crabbox profile setupEnv must contain only valid POSIX environment variable names"); if (name === "CRABBOX_ENV_ALLOW") throw new WorkerProviderError(`Crabbox profile setupEnv name ${name} is reserved`); return name; }); if (new Set(setupEnv).size !== setupEnv.length) throw new WorkerProviderError("Crabbox profile setupEnv must not contain duplicate names"); if (setupEnv.length > 0 && !setup) throw new WorkerProviderError("Crabbox profile setupEnv requires setup"); } const desktop = profile.desktop; if (desktop !== void 0 && typeof desktop !== "boolean") throw new WorkerProviderError("Crabbox profile desktop must be a boolean"); if (desktop && !CRABBOX_DESKTOP_PROVIDERS.has(provider)) throw new WorkerProviderError("Crabbox desktop profiles support only AWS, Azure, and coordinator-backed Hetzner"); const warmImage = profile.warmImage; if (warmImage !== void 0 && typeof warmImage !== "boolean") throw new WorkerProviderError("Crabbox profile warmImage must be a boolean"); return { binary, class: machineClass, desktop, heartbeatIntervalMs: heartbeatIntervalMs(idleTimeoutMs), heartbeatTimeoutMs: Math.min(CRABBOX_HEARTBEAT_TIMEOUT_MS, Math.max(1, Math.floor(idleTimeoutMs / 2))), idleTimeout, idleTimeoutMs, provider, setup, setupEnv, ttl, target, warmImage }; } function resolveCrabboxProfileSetupEnv(setupEnv) { if (!setupEnv?.length) return; return Object.fromEntries(setupEnv.map((name) => { const value = process.env[name]; if (!Object.hasOwn(process.env, name) || value === void 0) throw new Error(`Crabbox profile setupEnv variable is missing: ${name}`); return [name, value]; })); } function resolveCrabboxWarmImageProfile(profile, machineClass = profile.class, target = profile.target) { if (target === "windows/wsl2" && profile.desktop) throw new WorkerProviderError("Crabbox WSL2 does not support desktops; select native Windows for a desktop viewer"); return { ...profile, class: machineClass, target, warmImage: target === "linux" && (profile.warmImage ?? (machineClass !== void 0 && !profile.setupEnv?.length)) }; } function resolveCrabboxWarmImageProfileKey(profile, projectKey) { return createHash("sha256").update(JSON.stringify({ backendProvider: profile.provider, ...profile.target !== "linux" ? { target: profile.target } : {}, setup: profile.setup ?? "", setupEnvKeys: [...profile.setupEnv ?? []].toSorted(), desktop: profile.desktop ?? false, machineClass: profile.class, ...projectKey ? { projectKey } : {} })).digest("hex"); } function resolveCrabboxProvisionProfile(profile, requestedClassValue, requestedOsValue) { const configured = parseCrabboxProfile(profile); const requestedClass = normalizeOptionalString(requestedClassValue); if (requestedClassValue !== void 0 && (!requestedClass || requestedClass.length > MAX_CRABBOX_MACHINE_CLASS_LENGTH)) throw new WorkerProviderError("Crabbox machine class must be a non-empty string of at most 128 characters"); const resolved = resolveCrabboxWarmImageProfile(configured, requestedClass ?? configured.class, requestedOsValue === void 0 ? configured.target : parseCrabboxOperatingSystem(requestedOsValue)); let provisionProfile; if (!resolved.warmImage) provisionProfile = { ...resolved, warmImage: false }; else { if (!resolved.class) throw new WorkerProviderError("Crabbox warmImage requires a configured class or a placement machine class"); provisionProfile = { ...resolved, class: resolved.class, warmImage: true }; } return { profile: provisionProfile, forwardedEnv: resolveCrabboxProfileSetupEnv(resolved.setupEnv) }; } function listCrabboxMachineOptions(configuredClass, shapes = []) { const seen = /* @__PURE__ */ new Set(); const options = []; for (const os of CRABBOX_ENROLLABLE_TARGETS) { const candidates = shapes.filter((shape) => { const key = `${shape.os}:${shape.class}`; if (shape.os !== os || shape.class.length > MAX_CRABBOX_MACHINE_CLASS_LENGTH || seen.has(key)) return false; seen.add(key); return true; }); if (candidates.length === 0) continue; const remaining = MAX_CRABBOX_MACHINE_OPTIONS - options.length; const reserveDefault = configuredClass !== void 0 && !candidates.slice(0, remaining).some((shape) => shape.class === configuredClass); for (const shape of candidates.slice(0, Math.max(0, remaining - Number(reserveDefault)))) options.push({ id: shape.class, os, label: shape.class.replace(/^./u, (initial) => initial.toUpperCase()), ...shape.cpu !== void 0 ? { cpu: shape.cpu } : {}, ...shape.memoryGb !== void 0 ? { memoryGb: shape.memoryGb } : {}, ...shape.class === configuredClass ? { default: true } : {} }); if (reserveDefault && remaining > 0 && configuredClass !== void 0) options.push({ id: configuredClass, os, label: configuredClass, default: true }); } return options; } function buildCrabboxAllocationArgs(profile, leaseId, slug) { const args = [ "--provider", profile.provider, "--network", "public", "--tailscale=false", ...profile.class ? ["--class", profile.class] : [], ...profile.target === "linux" ? ["--target", "linux"] : [], ...profile.target === "windows/wsl2" ? [ "--target", "windows", "--windows-mode", "wsl2" ] : [], ...profile.target === "windows/normal" ? [ "--target", "windows", "--windows-mode", "normal" ] : [], ...profile.target === "macos" ? [ "--target", "macos", "--market", "on-demand" ] : [], "--ttl", profile.ttl, "--idle-timeout", profile.idleTimeout, "--lease-id", leaseId, "--slug", slug, "--keep=true" ]; if (profile.desktop) { args.push("--desktop"); if (profile.target === "linux") args.push("--browser", "--desktop-env", "xfce"); } return args; } function resolveOpenClawRoot(pluginRoot) { if (!pluginRoot) return process.cwd(); const extensionsDir = path.dirname(pluginRoot); if (path.basename(extensionsDir) !== "extensions") return process.cwd(); const extensionParent = path.dirname(extensionsDir); return path.basename(extensionParent) === "dist" || path.basename(extensionParent) === "dist-runtime" ? path.dirname(extensionParent) : extensionParent; } function operationSlug(operationId) { return `openclaw-${createHash("sha256").update(operationId).digest("hex").slice(0, 32)}`; } const LEASE_ID_PATTERN = /^(?:cbx_|tbx_)[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u; function assertCrabboxLeaseId(leaseId) { if (!LEASE_ID_PATTERN.test(leaseId)) throw new Error("Crabbox lease id is invalid"); } function operationLeaseId(operationId) { if (!operationId.trim()) throw new Error("Crabbox provision requires an operation id"); if (LEGACY_PROVISION_OPERATION_ID_PATTERN.test(operationId)) throw new Error("Legacy Crabbox provision state cannot be replayed safely; clean up any prior lease and dispatch again"); return `cbx_${createHash("sha256").update(CRABBOX_LEASE_ID_DOMAIN).update(operationId).digest("hex").slice(0, 12)}`; } //#endregion export { buildCrabboxAllocationArgs as a, operationSlug as c, resolveCrabboxProvisionProfile as d, resolveCrabboxWarmImageProfile as f, assertCrabboxLeaseId as i, parseCrabboxOperatingSystem as l, resolveOpenClawRoot as m, CRABBOX_OS_LABELS as n, listCrabboxMachineOptions as o, resolveCrabboxWarmImageProfileKey as p, CRABBOX_WORKER_PROVIDER_ID as r, operationLeaseId as s, CRABBOX_ENROLLABLE_TARGETS as t, parseCrabboxProfile as u };