UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

217 lines (216 loc) 10.5 kB
import { c as isRecord } from "./record-coerce-DItp3I4t.js"; import "./workspace-actual-manifest-BGixPzCq.js"; import { n as selectWorkspaceSeedsToPrune, t as WORKSPACE_SEED_RETENTION } from "./workspace-seed-retention-DCC3NY0G.js"; import { n as prepareWorkerWorkspaceGitPack, r as workerProjectSeedKey } from "./workspace-git-base-qAMhycVO.js"; import fs from "node:fs"; import path from "node:path"; import fs$1 from "node:fs/promises"; import os from "node:os"; import { createHash } from "node:crypto"; //#region src/gateway/worker-environments/service-contract.ts function deriveEnvironmentIntent(idempotencyKey) { const digest = createHash("sha256").update(idempotencyKey).digest("hex"); return { environmentId: `worker:${digest.slice(0, 32)}`, provisionOperationId: `provision:v2:${digest}` }; } /** Canonical admission rejected the session owner, not a caller or process cancellation. */ var WorkerPlacementAdmissionTargetError = class extends Error { constructor(..._args) { super(..._args); this.code = "invalid_state"; } }; //#endregion //#region src/gateway/worker-environments/project-seed-script.ts /** Only immutable Git content and non-secret preparation metadata enter the machine image. */ function createProjectSeedScript(input) { return `set -eu node <<'PROJECT_SEED_SCRIPT' const fs = require("node:fs"); const fsp = fs.promises; const path = require("node:path"); const os = require("node:os"); const crypto = require("node:crypto"); const { spawnSync } = require("node:child_process"); const input = ${JSON.stringify(input)}; const retention = ${JSON.stringify(WORKSPACE_SEED_RETENTION)}; const selectSeedsToPrune = ${selectWorkspaceSeedsToPrune.toString()}; process.umask(0o077); const env = { ...process.env, GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: os.devNull, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "", SSH_ASKPASS: "" }; const git = (root, args, stdin) => { const result = spawnSync("git", ["-c", "core.hooksPath=" + os.devNull, "-c", "core.fsmonitor=false", "-c", "credential.helper=", "-c", "core.askPass=", "-c", "init.templateDir=", "-C", root, ...args], { env, encoding: "utf8", timeout: 600000, maxBuffer: 262144, stdio: [stdin ?? "ignore", "pipe", "pipe"] }); if (result.status !== 0) throw new Error("Project Git preparation failed: " + (result.stderr?.trim() || result.error?.message || "exit " + result.status)); return result.stdout.trim(); }; const ownedDirectory = (parent, target) => { const stat = fs.lstatSync(target); if (stat.isSymbolicLink() || !stat.isDirectory() || path.dirname(fs.realpathSync(target)) !== parent) throw new Error("Project seed directory escaped its owner"); return stat; }; (async () => { const home = fs.realpathSync(os.homedir()); const workerRoot = path.join(home, ".openclaw-worker"); fs.mkdirSync(workerRoot, { recursive: true, mode: 0o700 }); ownedDirectory(home, workerRoot); const root = path.join(workerRoot, "git-seeds"); fs.mkdirSync(root, { mode: 0o700, recursive: true }); ownedDirectory(workerRoot, root); const namespace = path.join(root, input.namespace); fs.mkdirSync(namespace, { recursive: true, mode: 0o700 }); ownedDirectory(root, namespace); const prune = () => { const entries = fs.readdirSync(namespace, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry) => ({ name: entry.name, mtimeMs: ownedDirectory(namespace, path.join(namespace, entry.name)).mtimeMs })); for (const entry of selectSeedsToPrune(entries, retention, Date.now(), input.seedKey)) { const target = path.join(namespace, entry.name); if (ownedDirectory(namespace, target).mtimeMs === entry.mtimeMs) fs.rmSync(target, { recursive: true }); } }; const seed = path.join(namespace, input.seedKey); const stagingPrefix = ".tmp-" + input.seedKey + "-"; if (!input.pack) { if (fs.existsSync(seed)) { ownedDirectory(namespace, seed); ownedDirectory(seed, path.join(seed, ".git")); if (git(seed, ["rev-parse", "--verify", "HEAD"]) !== input.baseCommit || git(seed, ["status", "--porcelain=v1", "--untracked-files=all"])) throw new Error("Prepared project seed is not pristine"); prune(); process.stdout.write(JSON.stringify({ ready: true })); return; } // Provisioning serializes this lease. Discard only this project's abandoned staging. for (const entry of fs.readdirSync(namespace)) { if (!entry.startsWith(stagingPrefix)) continue; const stale = path.join(namespace, entry); ownedDirectory(namespace, stale); fs.rmSync(stale, { recursive: true }); } const directory = fs.mkdtempSync(path.join(namespace, stagingPrefix)); process.stdout.write(JSON.stringify({ ready: false, directory })); return; } const directory = input.pack.directory; if (path.dirname(directory) !== namespace || !path.basename(directory).startsWith(stagingPrefix)) throw new Error("Project staging path escaped its owner"); ownedDirectory(namespace, directory); try { const pack = path.join(directory, "base.pack"); const stat = fs.lstatSync(pack); if (!stat.isFile() || stat.isSymbolicLink() || stat.size !== input.pack.bytes) throw new Error("Project pack size does not match"); const hash = crypto.createHash("sha256"); for await (const chunk of fs.createReadStream(pack)) hash.update(chunk); if (hash.digest("hex") !== input.pack.sha256) throw new Error("Project pack digest does not match"); const repository = path.join(directory, "repository"); fs.mkdirSync(repository, { mode: 0o700 }); git(repository, ["init", "--quiet", "--object-format=" + (input.baseCommit.length === 40 ? "sha1" : "sha256"), "."]); const fd = fs.openSync(pack, "r"); try { git(repository, ["index-pack", "--stdin"], fd); } finally { fs.closeSync(fd); } fs.writeFileSync(path.join(repository, ".git", "shallow"), input.baseCommit + "\\n", { mode: 0o600 }); if (git(repository, ["rev-parse", "--verify", input.baseCommit + "^{commit}"]) !== input.baseCommit) throw new Error("Project pack commit does not match"); git(repository, ["checkout", "--detach", "--force", input.baseCommit]); git(repository, ["fsck", "--connectivity-only", "--no-reflogs"]); if (git(repository, ["status", "--porcelain=v1", "--untracked-files=all"])) throw new Error("Prepared project checkout is not pristine"); fs.renameSync(repository, seed); prune(); process.stdout.write(JSON.stringify({ ready: true })); } finally { fs.rmSync(directory, { recursive: true, force: true }); } })().catch((error) => { console.error(error.message); process.exitCode = 1; }); PROJECT_SEED_SCRIPT`; } //#endregion //#region src/gateway/worker-environments/project-preparation.ts function readWorkerProjectSnapshot(value) { if (value === void 0) return; if (!isRecord(value) || typeof value.key !== "string" || !/^[a-f0-9]{64}$/u.test(value.key) || typeof value.baseCommit !== "string" || !/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(value.baseCommit) || typeof value.root !== "string" || value.root.length > 4096 || !path.isAbsolute(value.root)) throw new Error("Worker environment has an invalid project preparation snapshot"); return { key: value.key, root: value.root, baseCommit: value.baseCommit }; } function createWorkerProjectPreparation(params) { if (!/^[A-Za-z0-9_-]{1,128}$/u.test(params.namespace)) throw new Error("Worker project preparation namespace is invalid"); const abort = new AbortController(); const signal = params.signal ? AbortSignal.any([abort.signal, params.signal]) : abort.signal; const seedKey = workerProjectSeedKey(params.project); let active; const requireCurrent = () => { signal.throwIfAborted(); try { params.requireCurrent(); } catch (error) { abort.abort(error); throw error; } }; const prepare = async (transport) => { requireCurrent(); const scriptInput = { namespace: params.namespace, seedKey, baseCommit: params.project.baseCommit }; const inspection = JSON.parse(await transport.runScript(createProjectSeedScript(scriptInput), signal)); requireCurrent(); if (!isRecord(inspection) || typeof inspection.ready !== "boolean") throw new Error("Project preparation returned invalid seed status"); if (inspection.ready) return { seedKey, cacheHit: true }; const directory = inspection.directory; if (typeof directory !== "string" || directory.length > 4096 || !path.posix.isAbsolute(directory) || !directory.includes(`/.openclaw-worker/git-seeds/${params.namespace}/`) || path.posix.normalize(directory) !== directory || path.posix.basename(path.posix.dirname(directory)) !== params.namespace || !path.posix.basename(directory).startsWith(`.tmp-${seedKey}-`)) throw new Error("Project preparation returned an invalid staging directory"); const temporaryRoot = await fs$1.mkdtemp(path.join(os.tmpdir(), "openclaw-project-base-")); try { requireCurrent(); const pack = await prepareWorkerWorkspaceGitPack({ root: params.project.root, baseCommit: params.project.baseCommit, temporaryRoot, signal }); requireCurrent(); const bytes = (await fs$1.stat(pack)).size; if (bytes > 4294967296) throw new Error("Project Git pack exceeds the workspace byte limit"); const hash = createHash("sha256"); for await (const chunk of fs.createReadStream(pack, { signal })) hash.update(chunk); requireCurrent(); await transport.upload(pack, path.posix.join(directory, "base.pack"), signal); requireCurrent(); const installed = JSON.parse(await transport.runScript(createProjectSeedScript({ ...scriptInput, pack: { directory, bytes, sha256: hash.digest("hex") } }), signal)); requireCurrent(); if (!isRecord(installed) || installed.ready !== true) throw new Error("Project checkout was not verified before capture"); return { seedKey, cacheHit: false }; } finally { await fs$1.rm(temporaryRoot, { recursive: true, force: true }); } }; return { project: { key: params.project.key, baseCommit: params.project.baseCommit, signal, assertCurrent: requireCurrent, prepare: (transport) => { requireCurrent(); return active ??= prepare(transport); } }, close: () => abort.abort(new DOMException("Project preparation operation is closed", "AbortError")) }; } //#endregion export { deriveEnvironmentIntent as i, readWorkerProjectSnapshot as n, WorkerPlacementAdmissionTargetError as r, createWorkerProjectPreparation as t };