UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

673 lines (672 loc) • 25.8 kB
import { r as isPathInside } from "./path-guards-CH7noY0Z.mjs"; import { r as resolveStateDir } from "./state-dir-CfAW6S2K.mjs"; import { l as resolveSessionStorePathCore } from "./paths-CAzSovN1.mjs"; import { t as withOpenClawStateLease } from "./openclaw-state-lease-D_rzQ4lw.mjs"; import { s as loadSessionEntry } from "./session-accessor.sqlite-entry-BuCmtpuj.mjs"; import "./session-accessor-BM3Wa1Bx.mjs"; import { c as serializeWorkerWorkspaceManifest, l as serializeWorkerWorkspaceReconciliationPlan, o as parseWorkerWorkspaceManifest, s as parseWorkerWorkspaceReconciliationPlan } from "./workspace-manifest-f0Cr1Qtt.mjs"; import { r as manifestNodes } from "./workspace-manifest-comparison-BTPqF98t.mjs"; import { c as requireGit, l as requireGitBuffer } from "./git-DOeuOnFt.mjs"; import { u as captureWorkspaceSnapshot } from "./workspace-reconcile-fs-BL6nrAyR.mjs"; import { a as recoverWorkerWorkspaceReconciliation, n as AcceptedWorkspacePublicationIndeterminateError, t as applyStagedWorkerWorkspace } from "./workspace-reconcile-E9Cb_Pf6.mjs"; import { t as withWorktreeGitConfig } from "./checkout-git-config-XnzMDYqJ.mjs"; import { d as getRegistryWorktree, u as findLiveRegistryWorktreeByPath } from "./registry-CvH4JdSJ.mjs"; import { l as splitNullBuffer } from "./git-path-inventory-BRi-Wr7x.mjs"; import { a as hasWorkerWorkspaceResultRef, d as withStagedWorkerWorkspaceResult, f as workerWorkspaceResultRef, l as readStagedWorkerWorkspaceResult, p as workerWorkspaceResultStaging, r as deleteStagedWorkerWorkspaceResult } from "./workspace-result-staging-CEgPD0Zu.mjs"; import { n as localWorkspaceStore } from "./local-workspace-store-G0MGIaMh.mjs"; import { a as runWorkspaceInventoryCommandToFile } from "./workspace-sync-inventory-DKxfFAxe.mjs"; import { n as prepareWorkerWorkspaceGitPack } from "./workspace-git-base-DqTs5T65.mjs"; import { realpathSync } from "node:fs"; import path from "node:path"; import fs$1 from "node:fs/promises"; import os from "node:os"; import { createHash, randomUUID } from "node:crypto"; import { isUtf8 } from "node:buffer"; //#region src/gateway/worker-environments/local-workspace-archive.ts /** Git preserves ordinary files, not accepted ignored leaves or empty dirs. * Keep only that missing delta in the existing projection recovery receipt. */ async function stageLocalWorkspaceArchive(params) { if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(params.snapshot)) throw new Error("Invalid archive snapshot commit"); const listed = await requireGitBuffer(params.root, [ "ls-tree", "-r", "-z", "--name-only", params.snapshot ], { signal: params.signal, beforeRun: params.assertCurrent, maxOutputBytes: 67108864 }); params.assertCurrent(); const paths = new Set(splitNullBuffer(listed).map((entry) => entry.toString("hex"))); const entries = params.accepted.entries.filter((entry) => paths.has(Buffer.from(entry.path).toString("hex"))); const directories = /* @__PURE__ */ new Set(); for (const entry of entries) for (let parent = path.posix.dirname(entry.path); parent !== "."; parent = path.posix.dirname(parent)) directories.add(parent); const base = serializeWorkerWorkspaceManifest({ ...params.accepted, entries, directories: [...directories].toSorted() }); const baseRef = "sha256:" + createHash("sha256").update(base).digest("hex"); if (baseRef === params.acceptedRef) return; const resultRef = workerWorkspaceResultRef(randomUUID()); params.reserve(base, baseRef, resultRef); await workerWorkspaceResultStaging.stageWorkerWorkspaceResult({ root: params.root, stagingRoot: params.projection, stagedResultRef: resultRef, baseManifestRaw: base, baseManifestRef: baseRef, currentManifestRaw: serializeWorkerWorkspaceManifest(params.accepted), currentManifestRef: params.acceptedRef }); params.assertCurrent(); } function localWorkspaceArchiveOperations(params) { const { owner, signal, current, update, capture, settle, recover, cleanupAccepted, assertDirectory: assertOwnedDirectory, deleteBinding } = params; return { restoreSnapshot: async () => { await recover(); const selected = current(); if (!selected.pending_ref) return; if (!selected.pending_target) { const receipt = await readStagedWorkerWorkspaceResult(owner.worktree.repoRoot, selected.pending_ref); current(); if (receipt.currentManifestRef !== selected.baseline_ref) throw new Error("Archive restore receipt changed"); update({ baseline_json: serializeWorkerWorkspaceManifest(receipt.base), baseline_ref: receipt.baseManifestRef, pending_target: "canonical" }); } await settle(true); }, finishRestore: cleanupAccepted, prepareArchive: async (snapshotCommit) => { await settle(); const accepted = await capture("canonical"); if (accepted.manifestRef !== current().baseline_ref) throw new Error("Local workspace changed before archive capture"); await stageLocalWorkspaceArchive({ root: owner.worktree.repoRoot, projection: current().projection_path, snapshot: snapshotCommit, accepted: accepted.manifest, acceptedRef: accepted.manifestRef, signal, assertCurrent: () => { current(); }, reserve: (base, baseRef, resultRef) => { update({ baseline_json: base, baseline_ref: baseRef, pending_ref: resultRef, pending_target: "canonical" }); } }); }, expire: async (retireSnapshot) => { const selected = current(); if (selected.journal_json || selected.pending_target && selected.pending_target !== "canonical") throw new Error("Local sandbox has pending edits; restore its worktree before cleanup"); let accepted = selected.baseline_json && selected.baseline_ref ? { raw: selected.baseline_json, ref: selected.baseline_ref } : void 0; if (selected.pending_ref) { if (!retireSnapshot || owner.worktree.removedAt === void 0) throw new Error("Archive receipt still owns its restore data"); if (selected.pending_target) { const receipt = await readStagedWorkerWorkspaceResult(owner.worktree.repoRoot, selected.pending_ref); current(); if (receipt.baseManifestRef !== selected.baseline_ref) throw new Error("Archive retention receipt changed"); accepted = { raw: serializeWorkerWorkspaceManifest(receipt.current), ref: receipt.currentManifestRef }; } } if (accepted && (await capture("canonical")).manifestRef !== accepted.ref) throw new Error("Local sandbox has unaccepted edits; restore its worktree before cleanup"); const { readLocalWorkspaceRuntimes } = await import("./local-workspace-quiescence-DjHjlRUV.mjs"); if ((await readLocalWorkspaceRuntimes(selected.projection_path)).length) throw new Error("Local sandbox runtime still owns its workspace; cleanup deferred"); const parent = path.dirname(selected.projection_path); await assertOwnedDirectory(parent); current(); await retireSnapshot?.(() => { current(); }); current(); if (selected.pending_ref && accepted) { update({ baseline_json: accepted.raw, baseline_ref: accepted.ref, pending_target: null }); await cleanupAccepted(); } await fs$1.rm(parent, { recursive: true, force: true }); deleteBinding(); } }; } //#endregion //#region src/gateway/worker-environments/local-workspace-inventory.ts function decodeGitPaths(listed) { if (!isUtf8(listed)) throw new Error("Local sandbox source paths must be UTF-8; rename the invalid Git path before retrying"); return listed.toString("utf8").split("\0").filter(Boolean); } /** Admit untracked source once, before any guest can change canonical ignore rules. */ async function admitLocalWorkspaceSourcePaths(params) { const listed = await requireGitBuffer(params.root, [ "ls-files", "--cached", "--others", "--exclude-standard", "-z" ], { signal: params.signal, beforeRun: params.assertCurrent }); params.assertCurrent(); return JSON.stringify([...new Set(decodeGitPaths(listed))]); } /** Ignore changes are source edits, not authority to enroll canonical-only bytes. */ async function selectLocalWorkspaceCanonicalPaths(params) { const listed = await requireGitBuffer(params.root, [ "ls-files", "--cached", "-z" ], { signal: params.signal, beforeRun: params.assertCurrent }); params.assertCurrent(); const admitted = JSON.parse(params.admittedPaths); if (!Array.isArray(admitted) || !admitted.every((entry) => typeof entry === "string")) throw new Error("Invalid local workspace source admission"); const paths = /* @__PURE__ */ new Set([...admitted, ...decodeGitPaths(listed)]); if (params.baseline) for (const entryPath of manifestNodes(params.baseline).keys()) paths.add(entryPath); for (const entryPath of paths) for (let parent = path.posix.dirname(entryPath); parent !== "."; parent = path.posix.dirname(parent)) paths.add(parent); return paths; } //#endregion //#region src/gateway/worker-environments/local-workspace-projection.ts /** Publication and lifecycle callers retain their own authority while joining local settlement. */ async function withSettledLocalWorkspace(params, operation) { const row = localWorkspaceStore(params.env).get(params.worktree.id); if (!row) return await operation(); const worktree = params.worktree; const owner = { worktree, env: params.env, agentId: row.agent_id, sessionKey: row.session_key, sessionId: row.session_id, lifecycleRevision: row.lifecycle_revision, assertCurrent: () => { params.assertCurrent?.(); const current = getRegistryWorktree(params.env ?? process.env, worktree.id); if (!current || current.ownerKind !== "session" || current.ownerId !== row.session_key || current.path !== worktree.path || current.repoRoot !== worktree.repoRoot) throw new Error("Managed projection owner changed during settlement"); } }; return await withLocalWorkspaceProjection(owner, async (state, quiescence) => { if (params.finishRestore) await state.finishRestore(); else if (params.restoreSnapshot) await state.restoreSnapshot(); else if (state.current().baseline_ref) { await state.synchronize("canonical"); if (params.retireRuntime) await state.synchronize("projection"); } if (params.retireRuntime) await quiescence?.retire(); owner.assertCurrent(); return await operation(state.current().baseline_ref ? { prepareArchive: state.prepareArchive, canonicalPaths: state.canonicalPaths, assertCurrent: () => { state.current(); } } : void 0); }); } async function withSettledLocalWorkspacePath(params, operation) { const record = findLiveRegistryWorktreeByPath(process.env, params.cwd); return record ? await withSettledLocalWorkspace({ worktree: record, assertCurrent: params.assertCurrent }, operation) : await operation(); } function assertBinding(row, owner) { owner.assertCurrent(); if (row.worktree_id !== owner.worktree.id || row.agent_id !== owner.agentId || row.session_key !== owner.sessionKey || row.session_id !== owner.sessionId || row.lifecycle_revision !== owner.lifecycleRevision) throw new Error("Local sandbox workspace belongs to a different session incarnation; pending edits were preserved"); } function projectionPath(owner) { if (!/^[a-f0-9-]{36}$/u.test(owner.worktree.id)) throw new Error("Invalid managed worktree identity"); return path.join(realpathSync(resolveStateDir(owner.env)), "worktree-projections", owner.worktree.id, "workspace"); } async function assertOwnedDirectory(directory) { const stat = await fs$1.lstat(directory); if (!stat.isDirectory() || stat.isSymbolicLink() || await fs$1.realpath(directory) !== directory) throw new Error("Local sandbox workspace directory changed; preserved for recovery"); } /** Every operation owns the same renewable, cross-process reconciliation lease. */ async function withLocalWorkspaceProjection(owner, run, options = {}) { return await withOpenClawStateLease({ scope: "workspace.local-reconciliation", key: owner.worktree.id, database: { scope: "shared", options: { env: owner.env } }, leaseMs: 6e4, waitMs: 6e5, leaseLabel: "local sandbox workspace", operationLabel: "workspace.local-reconciliation" }, async (lease) => { const assertCurrent = () => { lease.assertOwned(); owner.assertCurrent(); }; assertCurrent(); const store = localWorkspaceStore(owner.env); let previous = store.get(owner.worktree.id); if (previous && previous.session_id === owner.sessionId && previous.session_key === owner.sessionKey && previous.agent_id === owner.agentId && previous.lifecycle_revision !== owner.lifecycleRevision) previous = store.update(previous, { lifecycle_revision: owner.lifecycleRevision }, assertCurrent); const operations = projectionOperations({ ...owner, assertCurrent }, lease.signal); const { quiesceLocalWorkspace, parseLocalWorkspacePausedRuntimes } = await import("./local-workspace-quiescence-DjHjlRUV.mjs"); const quiescence = previous && !options.provision ? await quiesceLocalWorkspace({ workspaceDir: previous.projection_path, retained: parseLocalWorkspacePausedRuntimes(previous.paused_runtimes_json), persist: (runtimes) => operations.rememberPaused(runtimes.length ? JSON.stringify(runtimes) : null), assertCurrent }) : void 0; try { return await run(operations, quiescence); } finally { const retained = localWorkspaceStore(owner.env).get(owner.worktree.id); if (retained && !retained.journal_json) await quiescence?.resume(); } }); } function projectionOperations(owner, signal) { const store = localWorkspaceStore(owner.env); let row = store.get(owner.worktree.id); const current = () => { if (!row) throw new Error("Local workspace binding is missing"); assertBinding(row, owner); if (row.projection_path !== projectionPath(owner) || store.revision(row.worktree_id) !== row.revision) throw new Error("Local workspace binding changed"); return row; }; const update = (patch) => { row = store.update(current(), patch, () => { current(); }); return row; }; const sourcePath = (target) => target === "canonical" ? current().projection_path : owner.worktree.path; const targetPath = (target) => target === "canonical" ? owner.worktree.path : current().projection_path; const canonicalPaths = async () => { const selected = current(); return await selectLocalWorkspaceCanonicalPaths({ root: owner.worktree.path, admittedPaths: selected.source_paths_json, signal, assertCurrent: () => { current(); }, baseline: selected.baseline_json && selected.baseline_ref ? parseWorkerWorkspaceManifest(selected.baseline_json, selected.baseline_ref) : void 0 }); }; const capture = async (target) => { const selected = current(); const root = sourcePath(target); await assertOwnedDirectory(root); current(); const includePaths = target === "projection" ? await canonicalPaths() : void 0; const snapshot = await captureWorkspaceSnapshot({ root, baseCommit: selected.base_commit, includePaths, signal }); current(); return snapshot; }; const recover = async () => { const selected = current(); if (!selected.journal_json) return; if (!selected.journal_pack || selected.pending_target !== "canonical" && selected.pending_target !== "projection") throw new Error("Local workspace recovery metadata is incomplete"); const journal = { ...parseWorkerWorkspaceReconciliationPlan(selected.journal_json), basePack: selected.journal_pack }; await recoverWorkerWorkspaceReconciliation({ root: targetPath(selected.pending_target), journal }); update({ journal_json: null, journal_pack: null }); }; const cleanupAccepted = async () => { const selected = current(); if (!selected.pending_ref || selected.pending_target) return; await deleteStagedWorkerWorkspaceResult({ root: owner.worktree.repoRoot, stagedResultRef: selected.pending_ref }); update({ pending_ref: null }); }; const settle = async (retainAccepted = false) => { await recover(); if (!retainAccepted) await cleanupAccepted(); const selected = current(); if (!selected.pending_ref) return; if (!selected.baseline_ref || !selected.baseline_json || selected.pending_target !== "canonical" && selected.pending_target !== "projection") throw new Error("Local workspace result has no accepted baseline"); const target = selected.pending_target; if (!await hasWorkerWorkspaceResultRef({ root: owner.worktree.path, stagedResultRef: selected.pending_ref })) { const snapshot = await capture(target); await workerWorkspaceResultStaging.stageWorkerWorkspaceResult({ root: owner.worktree.path, stagingRoot: sourcePath(target), stagedResultRef: selected.pending_ref, baseManifestRef: selected.baseline_ref, currentManifestRef: snapshot.manifestRef, baseManifestRaw: selected.baseline_json, currentManifestRaw: snapshot.rawManifest }); current(); } let accepted; await withStagedWorkerWorkspaceResult({ root: owner.worktree.path, stagedResultRef: selected.pending_ref }, async (snapshot) => { current(); await applyStagedWorkerWorkspace({ root: targetPath(target), stagingRoot: snapshot.stagingRoot, baseManifestRef: snapshot.baseManifestRef, currentManifestRef: snapshot.currentManifestRef, base: snapshot.base, current: snapshot.current, journal: { load: () => void 0, begin: (journal) => { update({ journal_json: serializeWorkerWorkspaceReconciliationPlan(journal), journal_pack: journal.basePack }); }, abort: () => { update({ journal_json: null, journal_pack: null }); }, commit: () => { if (!accepted) throw new Error("Local workspace acceptance is missing"); try { update(accepted.conflictPaths.length ? { journal_json: null, journal_pack: null } : { baseline_json: serializeWorkerWorkspaceManifest(snapshot.current), baseline_ref: snapshot.currentManifestRef, pending_target: null, journal_json: null, journal_pack: null }); } catch (error) { throw new AcceptedWorkspacePublicationIndeterminateError("commit", error, void 0); } } }, acceptance: { kind: "reconcile", publish: async (value) => { current(); accepted = value; } } }); }); if (current().pending_target) throw new Error("Local sandbox edits conflict with the managed workspace; pending changes were preserved. Resolve the workspace conflicts before retrying."); if (!retainAccepted) await cleanupAccepted(); }; const synchronize = async (target) => { await settle(); if ((await capture(target)).manifestRef === current().baseline_ref) return; update({ pending_ref: workerWorkspaceResultRef(randomUUID()), pending_target: target }); await settle(); }; const prepare = async () => { if (!row) { const baseCommit = await requireGit(owner.worktree.path, [ "rev-parse", "--verify", "HEAD^{commit}" ], { signal, beforeRun: owner.assertCurrent }); const sourcePaths = await admitLocalWorkspaceSourcePaths({ root: owner.worktree.path, signal, assertCurrent: owner.assertCurrent }); owner.assertCurrent(); row = store.create({ worktree_id: owner.worktree.id, agent_id: owner.agentId, session_key: owner.sessionKey, session_id: owner.sessionId, lifecycle_revision: owner.lifecycleRevision, projection_path: projectionPath(owner), base_commit: baseCommit, source_paths_json: sourcePaths, baseline_json: null, baseline_ref: null, pending_ref: null, pending_target: null, journal_json: null, journal_pack: null, paused_runtimes_json: null, created_at_ms: Date.now() }, owner.assertCurrent); } const selected = current(); if (!selected.baseline_ref) { const parent = path.dirname(selected.projection_path); await fs$1.mkdir(parent, { recursive: true, mode: 448 }); await assertOwnedDirectory(parent); current(); await fs$1.rm(selected.projection_path, { recursive: true, force: true }); const temporary = await fs$1.mkdtemp(path.join(parent, ".prepare-")); try { const pack = await withWorktreeGitConfig(owner.worktree.path, true, { signal, beforeRun: () => { current(); } }, (git) => git.withContentEnvironment((baseEnv) => prepareWorkerWorkspaceGitPack({ root: owner.worktree.path, baseCommit: selected.base_commit, temporaryRoot: temporary, signal, baseEnv }))); current(); const repo = path.join(temporary, "workspace"); await fs$1.mkdir(repo, { mode: 448 }); const cleanEnv = { PATH: process.env.PATH, HOME: temporary, GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: os.devNull, GIT_CONFIG_SYSTEM: os.devNull, GIT_NO_REPLACE_OBJECTS: "1", GIT_TERMINAL_PROMPT: "0" }; const git = (args, input) => requireGit(repo, args, { baseEnv: cleanEnv, env: cleanEnv, input, signal, beforeRun: () => { current(); } }); await git([ "init", "--quiet", "--template=", "--object-format=" + (selected.base_commit.length === 40 ? "sha1" : "sha256") ]); current(); await runWorkspaceInventoryCommandToFile({ argv: [ "git", "-c", "core.hooksPath=" + os.devNull, "-c", "core.fsmonitor=false", "-C", repo, "index-pack", "--stdin" ], inputPath: pack, outputPath: path.join(temporary, "index-pack-result"), baseEnv: cleanEnv, signal, timeoutMs: 3e5, maxOutputBytes: 4096 }); current(); await fs$1.writeFile(path.join(repo, ".git", "shallow"), selected.base_commit + "\n", { mode: 384 }); await git([ "checkout", "--quiet", "-b", owner.worktree.branch, selected.base_commit ]); const initial = await captureWorkspaceSnapshot({ root: repo, baseCommit: selected.base_commit, signal }); current(); await fs$1.rename(repo, selected.projection_path); update({ baseline_json: initial.rawManifest, baseline_ref: initial.manifestRef }); } finally { await fs$1.rm(temporary, { recursive: true, force: true }); } } await assertOwnedDirectory(current().projection_path); await synchronize("canonical"); await synchronize("projection"); return current().projection_path; }; return { current, canonicalPaths, prepare, synchronize, settle, recover, ...localWorkspaceArchiveOperations({ owner, signal, current, update, capture, settle, recover, cleanupAccepted, assertDirectory: assertOwnedDirectory, deleteBinding: () => store.delete(current(), owner.assertCurrent) }), rememberPaused: (value) => { update({ paused_runtimes_json: value }); } }; } /** Expiry belongs to the existing worktree retention owner, never sandbox pruning. */ async function expireLocalWorkspaceProjection(params) { const row = localWorkspaceStore(params.env).get(params.worktree.id); if (!row) { await params.retireSnapshot?.(params.assertCurrent); return; } if (params.worktree.removedAt === void 0) throw new Error("Cannot expire a live sandbox workspace"); await withLocalWorkspaceProjection({ worktree: params.worktree, env: params.env, agentId: row.agent_id, sessionKey: row.session_key, sessionId: row.session_id, lifecycleRevision: row.lifecycle_revision, assertCurrent: () => { params.assertCurrent(); const record = getRegistryWorktree(params.env, params.worktree.id); if (record?.removedAt !== params.worktree.removedAt || record?.ownerId !== row.session_key) throw new Error("Workspace retention owner changed"); } }, (state) => state.expire(params.retireSnapshot)); } /** Bind only a live session-owned managed checkout, never an arbitrary host path. */ function resolveLocalWorkspaceOwner(params) { const env = params.env ?? process.env; const scope = { agentId: params.agentId, sessionKey: params.sessionKey, storePath: resolveSessionStorePathCore(params.cfg.session?.store, { agentId: params.agentId, env }), env }; const entry = loadSessionEntry(scope); if (!entry?.worktree?.id) return; const worktree = getRegistryWorktree(env, entry.worktree.id); if (!worktree || worktree.removedAt !== void 0 || worktree.ownerKind !== "session" || worktree.ownerId !== params.sessionKey || worktree.repoRoot !== entry.worktree.repoRoot || worktree.branch !== entry.worktree.branch || params.workspaceDir && !isPathInside(worktree.path, params.workspaceDir)) throw new Error("Local sandbox managed workspace owner changed"); const assertCurrent = () => { params.assertCurrent?.(); const now = loadSessionEntry(scope); const current = getRegistryWorktree(env, worktree.id); if (now?.sessionId !== entry.sessionId || now?.lifecycleRevision !== entry.lifecycleRevision || now?.archivedAt !== void 0 || now?.worktree?.id !== worktree.id || current?.removedAt !== void 0 || current?.ownerId !== params.sessionKey || current?.path !== worktree.path || current?.repoRoot !== worktree.repoRoot) throw new Error("Local sandbox workspace authority changed"); }; assertCurrent(); return { agentId: params.agentId, sessionKey: params.sessionKey, sessionId: entry.sessionId, lifecycleRevision: entry.lifecycleRevision ?? null, worktree, assertCurrent, env }; } //#endregion export { withSettledLocalWorkspacePath as a, withSettledLocalWorkspace as i, resolveLocalWorkspaceOwner as n, withLocalWorkspaceProjection as r, expireLocalWorkspaceProjection as t };