openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
219 lines (218 loc) • 8.72 kB
JavaScript
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs";
import { w as root } from "./fs-safe-BBzelG8F.mjs";
import { t as createSubsystemLogger } from "./subsystem-4NzR43uz.mjs";
import { s as hasWorktreeUnknownOutcome } from "./errors-CgO78Orl.mjs";
import { d as runGit, s as requireGit } from "./git-D3obqZyl.mjs";
import { l as WORKTREE_SETUP_HEADROOM_BYTES } from "./allocation-li13b6uz.mjs";
import { t as withWorktreeGitConfig } from "./checkout-git-config-D9Pjt5zi.mjs";
import { i as timeWorktreePreparationPhase } from "./preparation-timing-B2Li8Sr2.mjs";
import { a as detectWorktreeFilesystemBackend, r as prepareWorktreeTemplate } from "./template-cache-7Sqourod.mjs";
import { n as resolveSandboxDependencyTemplateIdentity, t as prepareSandboxDependencyTemplate } from "./dependency-template-BFu65pR2.mjs";
import { t as prepareLocalWorkspaceCheckout } from "./local-workspace-checkout-DMfMraQ0.mjs";
import path from "node:path";
import fs from "node:fs/promises";
import { createHash } from "node:crypto";
import { parse as parse$1 } from "yaml";
//#region src/gateway/worker-environments/local-workspace-template.ts
const log = createSubsystemLogger("agents/worktrees");
const digest = (value) => createHash("sha256").update(value).digest("hex");
async function hasContainedVirtualStore(directory, workdir) {
try {
const { buffer } = await (await root(directory)).read("node_modules/.modules.yaml", {
symlinks: "reject",
maxBytes: 1048576
});
const manifest = parse$1(buffer.toString("utf8"));
if (!isRecord(manifest) || typeof manifest.virtualStoreDir !== "string") return false;
const modules = path.posix.resolve(workdir, "node_modules");
const relative = path.posix.relative(modules, path.posix.resolve(modules, manifest.virtualStoreDir));
return relative !== ".." && !relative.startsWith("../") && !path.posix.isAbsolute(relative);
} catch {
return false;
}
}
/** Only dependencies may survive installation; repository setup outputs remain private. */
async function validateTemplate(directory, commit, guard, trim = false) {
return await withWorktreeGitConfig(directory, true, {
signal: guard.signal,
beforeRun: guard.commitGuard
}, async (git) => {
const result = await git.run(directory, [
"--no-optional-locks",
"status",
"--porcelain=v2",
"--branch",
"-z",
"--untracked-files=normal",
"--ignored=matching"
], {
signal: guard.signal,
beforeRun: guard.commitGuard,
killProcessTree: true
});
const fields = result.stdout.split("\0");
if (result.code !== 0 || result.termination !== "exit" || result.stdoutTruncatedBytes || fields.pop() !== "") return false;
const heads = fields.filter((field) => field.startsWith("# branch.oid "));
if (heads.length !== 1 || heads[0] !== `# branch.oid ${commit}`) return false;
for (const field of fields) {
if (field.startsWith("# ")) continue;
if (!field.startsWith("! ") && !field.startsWith("? ")) return false;
const relative = field.slice(2);
const parts = relative.replace(/\/$/u, "").split("/");
if (parts.some((part) => !part || part === "." || part === "..") || path.isAbsolute(relative)) return false;
const target = path.join(directory, ...parts);
const modulesIndex = parts.indexOf("node_modules");
if (modulesIndex !== -1) {
if (!(await fs.lstat(path.join(directory, ...parts.slice(0, modulesIndex + 1)))).isDirectory()) return false;
continue;
}
if (!trim) return false;
guard.commitGuard();
await fs.rm(target, {
recursive: true,
force: true
});
}
return true;
});
}
/** The returned template retains its generation until the caller settles its clone. */
async function prepareLocalWorkspaceTemplate(params) {
const { guard } = params;
const backend = await detectWorktreeFilesystemBackend(params.temporaryRoot, guard);
if (!backend) return;
const gitOptions = {
signal: guard.signal,
beforeRun: guard.commitGuard,
killProcessTree: true
};
const lockfile = await runGit(params.source, ["show", `${params.baseCommit}:pnpm-lock.yaml`], gitOptions);
if (lockfile.code !== 0 || lockfile.stdoutTruncatedBytes) {
log.debug("sandbox dependency template skipped: no complete pnpm lockfile at the selected commit");
return;
}
const identity = await resolveSandboxDependencyTemplateIdentity(params.sandbox, {
signal: guard.signal,
assertCurrent: guard.commitGuard
});
if (!identity) {
log.debug("sandbox dependency template skipped: local image identity unavailable");
return;
}
const commonDir = path.resolve(params.source, await requireGit(params.source, ["rev-parse", "--git-common-dir"], gitOptions));
const prepareSource = async (directory, templateGuard) => {
await guard.requireDiskSpace([{
path: params.templateRoot,
bytes: WORKTREE_SETUP_HEADROOM_BYTES
}], "sandbox dependency template");
await backend.createTemplate(directory, templateGuard);
await timeWorktreePreparationPhase("checkout", () => prepareLocalWorkspaceCheckout({
source: params.source,
destination: directory,
temporaryRoot: params.temporaryRoot,
baseCommit: params.baseCommit,
branch: "openclaw-template",
signal: templateGuard.signal,
assertCurrent: templateGuard.commitGuard
}));
};
const record = await prepareWorktreeTemplate({
env: params.env,
now: Date.now,
options: guard,
cacheKey: digest(`sandbox-v1\n${commonDir}\n${params.templateRoot}\n${identity.key}`),
contentKey: digest(`${params.baseCommit}\n${digest(lockfile.stdout)}\n${identity.key}`),
repoRoot: params.repoRoot,
commonDir,
worktreeRoot: params.templateRoot,
sourceCommit: params.baseCommit,
backend: `sandbox-${backend.id}`,
requireSpace: () => guard.requireDiskSpace([{
path: params.templateRoot,
bytes: WORKTREE_SETUP_HEADROOM_BYTES
}], "sandbox dependency template"),
validate: (existing, templateOptions) => validateTemplate(existing.path, params.baseCommit, templateOptions),
prepare: async (preparing, templateOptions) => {
const templateGuard = {
...guard,
...templateOptions,
signal: templateOptions.signal ?? guard.signal
};
await prepareSource(preparing.path, templateGuard);
const result = await prepareSandboxDependencyTemplate({
directory: preparing.path,
cfg: params.sandbox,
scopeKey: preparing.id,
identity,
signal: templateGuard.signal,
assertCurrent: templateGuard.commitGuard,
rollbackGuard: templateOptions.rollbackGuard
});
let reason = result.installed ? void 0 : result.reason;
if (result.installed) {
const contained = await hasContainedVirtualStore(preparing.path, identity.docker.workdir);
templateGuard.commitGuard();
if (contained && await validateTemplate(preparing.path, params.baseCommit, templateGuard, true)) {
log.info("sandbox dependency template prepared for the selected commit");
return;
}
reason = contained ? "installation changed tracked source" : "pnpm virtual store is outside node_modules or unavailable";
}
log.warn(`sandbox dependency template using source-only fallback: ${reason}`);
templateGuard.commitGuard();
await fs.rm(preparing.path, {
recursive: true,
force: true
});
await prepareSource(preparing.path, templateGuard);
}
});
return record ? {
record,
backend
} : void 0;
}
/** The caller holds allocation before projection custody, matching removal's lock order. */
async function cloneLocalWorkspaceTemplate(params) {
const prepared = await timeWorktreePreparationPhase("templatePrepare", () => prepareLocalWorkspaceTemplate(params));
if (!prepared) return false;
const { record, backend } = prepared;
const { guard } = params;
let outcome;
try {
guard.commitGuard();
await guard.requireDiskSpace([{
path: params.destination,
bytes: backend.id === "btrfs" ? backend.estimateCloneBytes(0, 0) : WORKTREE_SETUP_HEADROOM_BYTES
}], "sandbox workspace clone");
try {
await timeWorktreePreparationPhase("templateApply", () => backend.cloneTemplate(record.path, params.destination, guard));
} catch (error) {
if (hasWorktreeUnknownOutcome(error)) throw error;
guard.commitGuard();
await fs.rm(params.destination, {
recursive: true,
force: true
});
log.warn("sandbox dependency snapshot unavailable; using source-only checkout");
return false;
}
await requireGit(params.destination, [
"branch",
"-m",
params.branch
], {
signal: guard.signal,
beforeRun: guard.commitGuard,
killProcessTree: true
});
return true;
} catch (error) {
outcome = error;
throw error;
} finally {
await record.release(outcome);
}
}
//#endregion
export { prepareLocalWorkspaceTemplate as n, cloneLocalWorkspaceTemplate as t };