openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,146 lines (1,145 loc) • 44.4 kB
JavaScript
import { createRequire } from "node:module";
import { createHash } from "node:crypto";
import path from "node:path";
import { compileFunction } from "node:vm";
//#region src/gateway/worker-environments/workspace-mutation-lock-remote-script.ts
const REMOTE_WORKSPACE_MUTATION_LOCK_JS = String.raw`const lockRoot = path.join(
transactionRoot,
".openclaw-accepted-lock-" + workspaceKey,
);
const lockToken = crypto.randomBytes(16).toString("hex");
const lockOwner = {
action,
nonce,
pid: lockOwnerPid,
controllerPid: process.pid,
token: lockToken,
};
const lockWait = new Int32Array(new SharedArrayBuffer(4));
const lockDeadlineMs = Date.now() + 9 * 60 * 1000;
let acquiredLock;
function encodeLockIdentity(identity) {
return [
identity.action,
identity.nonce,
identity.pid,
identity.controllerPid,
identity.token,
].join(".");
}
// Lock files are transient current-runtime state; unknown identity shapes fail closed.
function parseLockIdentity(parts) {
if (parts.length !== 5) return null;
const [entryAction, entryNonce, rawPid, rawControllerPid, token] = parts;
const pid = Number(rawPid);
const controllerPid = Number(rawControllerPid);
if (
!mutationActions.includes(entryAction) ||
!/^[a-f0-9]{32}$/.test(entryNonce || "") ||
!/^[1-9][0-9]*$/.test(rawPid || "") ||
!Number.isSafeInteger(pid) ||
!/^[1-9][0-9]*$/.test(rawControllerPid || "") ||
!Number.isSafeInteger(controllerPid) ||
(entryAction !== "receiver" && controllerPid !== pid) ||
!/^[a-f0-9]{32}$/.test(token || "")
) {
return null;
}
return { action: entryAction, nonce: entryNonce, pid, controllerPid, token };
}
function sameLockIdentity(left, right) {
return (
left.action === right.action &&
left.nonce === right.nonce &&
left.pid === right.pid &&
left.controllerPid === right.controllerPid &&
left.token === right.token
);
}
function processIsAlive(pid) {
try {
process.kill(pid, 0);
return true;
} catch (error) {
if (error && error.code === "EPERM") return true;
if (error && error.code === "ESRCH") return false;
throw error;
}
}
function processGroupIsAlive(pid) {
try {
process.kill(-pid, 0);
return true;
} catch (error) {
if (error && error.code === "EPERM") return true;
if (error && error.code === "ESRCH") return false;
throw error;
}
}
function lockIdentityIsAlive(identity) {
if (identity.action === "receiver") {
// Receiver descendants own mutation liveness; the wrapper owns acquire/release.
// Reclaim is safe only after both the receiver group and wrapper are dead.
return processIsAlive(identity.pid) ||
processGroupIsAlive(identity.pid) ||
processIsAlive(identity.controllerPid);
}
return processIsAlive(identity.pid);
}
function ownerEntryName(owner) {
return "owner." + encodeLockIdentity(owner);
}
function reclaimEntryName(owner, reclaimer) {
return "reclaim." + encodeLockIdentity(owner) + "." + encodeLockIdentity(reclaimer);
}
function parseLockEntry(name) {
const parts = name.split(".");
if (parts[0] === "owner" && parts.length === 6) {
const owner = parseLockIdentity(parts.slice(1));
return owner ? { kind: "owner", owner } : null;
}
if (parts[0] === "reclaim" && parts.length === 11) {
const owner = parseLockIdentity(parts.slice(1, 6));
const reclaimer = parseLockIdentity(parts.slice(6));
return owner && reclaimer ? { kind: "reclaim", owner, reclaimer } : null;
}
return null;
}
function readLock() {
let directoryStats;
let names;
try {
directoryStats = fs.lstatSync(lockRoot);
names = fs.readdirSync(lockRoot);
} catch (error) {
if (error && error.code === "ENOENT") return null;
throw error;
}
if (directoryStats.isSymbolicLink() || !directoryStats.isDirectory()) {
throw new Error("unsafe workspace mutation lock");
}
if (names.length !== 1) throw new Error("invalid workspace mutation lock");
const entry = parseLockEntry(names[0]);
if (!entry) throw new Error("invalid workspace mutation lock owner");
const entryPath = path.join(lockRoot, names[0]);
try {
const entryStats = fs.lstatSync(entryPath);
if (entryStats.isSymbolicLink() || !entryStats.isFile()) {
throw new Error("unsafe workspace mutation lock owner");
}
return { ...entry, name: names[0], entryPath, directoryStats, entryStats };
} catch (error) {
if (error && error.code === "ENOENT") return null;
throw error;
}
}
function sameLock(left, right) {
return (
left.kind === right.kind &&
left.name === right.name &&
sameInode(left.directoryStats, right.directoryStats) &&
sameInode(left.entryStats, right.entryStats)
);
}
function observedLockIdentity(lock) {
return [
lock.directoryStats.dev,
lock.directoryStats.ino,
lock.entryStats.dev,
lock.entryStats.ino,
lock.name,
].join(":");
}
function restoreOwnerEntry(observed) {
const current = readLock();
if (!current || !sameLock(current, observed)) return false;
try {
fs.renameSync(current.entryPath, path.join(lockRoot, ownerEntryName(current.owner)));
} catch (error) {
if (error && error.code === "ENOENT") return false;
throw error;
}
return true;
}
function restoreAbandonedTransition(observed) {
if (observed.kind !== "reclaim" || lockIdentityIsAlive(observed.reclaimer)) return false;
const current = readLock();
if (!current || !sameLock(current, observed) || lockIdentityIsAlive(current.reclaimer)) {
return false;
}
return restoreOwnerEntry(current);
}
function reclaimDeadOwner(observed) {
const current = readLock();
if (
!current ||
current.kind !== "owner" ||
!sameLock(current, observed) ||
lockIdentityIsAlive(current.owner)
) {
return false;
}
const claimName = reclaimEntryName(current.owner, lockOwner);
try {
// This sole-entry rename is the reclaim CAS. Only one dead-owner contender
// can install its complete owner+reclaimer identity.
fs.renameSync(current.entryPath, path.join(lockRoot, claimName));
} catch (error) {
if (error && error.code === "ENOENT") return false;
throw error;
}
let claimed = readLock();
if (
!claimed ||
claimed.kind !== "reclaim" ||
claimed.name !== claimName ||
!sameInode(claimed.directoryStats, current.directoryStats) ||
!sameInode(claimed.entryStats, current.entryStats) ||
!sameLockIdentity(claimed.owner, current.owner) ||
!sameLockIdentity(claimed.reclaimer, lockOwner)
) {
throw new Error("workspace mutation reclaim ownership changed");
}
let quarantined = false;
const quarantine = lockRoot + ".stale." + process.pid + "." + lockToken;
try {
if (lockIdentityIsAlive(claimed.owner)) return false;
const validated = readLock();
if (!validated || !sameLock(validated, claimed) || lockIdentityIsAlive(validated.owner)) {
return false;
}
claimed = validated;
fs.renameSync(lockRoot, quarantine);
quarantined = true;
const quarantinedDirectory = fs.lstatSync(quarantine);
const quarantinedEntry = fs.lstatSync(path.join(quarantine, claimed.name));
if (
!sameInode(quarantinedDirectory, claimed.directoryStats) ||
!sameInode(quarantinedEntry, claimed.entryStats)
) {
throw new Error("workspace mutation lock changed during reclamation");
}
removeTree(quarantine);
return true;
} finally {
if (!quarantined) restoreOwnerEntry(claimed);
}
}
function acquireWorkspaceLock() {
const candidate = lockRoot + "." + process.pid + "." + lockToken;
const ownerName = ownerEntryName(lockOwner);
fs.mkdirSync(candidate, { mode: 0o700 });
fs.writeFileSync(path.join(candidate, ownerName), "", { flag: "wx", mode: 0o600 });
let acquired = false;
let previousIdentity = "";
let waitMs = 10;
try {
while (Date.now() < lockDeadlineMs) {
try {
// The owner entry is complete before this atomic namespace operation,
// so contenders never mistake an initializing live owner for stale.
fs.renameSync(candidate, lockRoot);
acquired = true;
const observed = readLock();
if (
!observed ||
observed.kind !== "owner" ||
!sameLockIdentity(observed.owner, lockOwner)
) {
throw new Error("workspace mutation lock acquisition changed");
}
acquiredLock = observed;
return;
} catch (error) {
if (!error || (error.code !== "EEXIST" && error.code !== "ENOTEMPTY")) throw error;
}
const observed = readLock();
if (!observed) {
previousIdentity = "";
waitMs = 10;
continue;
}
const identity = observedLockIdentity(observed);
if (identity !== previousIdentity) {
previousIdentity = identity;
waitMs = 10;
}
if (observed.kind !== "owner") {
if (restoreAbandonedTransition(observed)) continue;
} else if (!lockIdentityIsAlive(observed.owner) && reclaimDeadOwner(observed)) {
continue;
}
Atomics.wait(lockWait, 0, 0, waitMs);
waitMs = Math.min(waitMs * 2, 500);
}
throw new Error("timed out waiting for workspace mutation lock");
} finally {
if (!acquired) removeTree(candidate);
}
}
function releaseWorkspaceLock() {
const current = readLock();
if (
!current ||
!acquiredLock ||
current.kind !== "owner" ||
!sameLock(current, acquiredLock) ||
!sameLockIdentity(current.owner, lockOwner)
) {
throw new Error("workspace mutation lock ownership changed");
}
const validated = readLock();
if (!validated || !sameLock(validated, current)) {
throw new Error("workspace mutation lock changed during release");
}
const quarantine = lockRoot + ".released." + process.pid + "." + lockToken;
fs.renameSync(lockRoot, quarantine);
const quarantinedDirectory = fs.lstatSync(quarantine);
const quarantinedEntry = fs.lstatSync(path.join(quarantine, validated.name));
if (
!sameInode(quarantinedDirectory, validated.directoryStats) ||
!sameInode(quarantinedEntry, validated.entryStats)
) {
throw new Error("workspace mutation lock changed during release");
}
removeTree(quarantine);
}`;
//#endregion
//#region node_modules/.pnpm/@openclaw+fs-safe@0.8.1/node_modules/@openclaw/fs-safe/dist/native-config.js
let overrideConfig = {};
function configureFsSafeNative(config) {
overrideConfig = {
...overrideConfig,
...config
};
}
//#endregion
//#region src/infra/fs-safe-defaults.ts
if (!Object.keys(process.env).some((key) => /^(?:OPENCLAW_)?FS_SAFE_(?:NATIVE|PYTHON)_MODE$/u.test(process.platform === "win32" ? key.toUpperCase() : key))) configureFsSafeNative({ mode: "off" });
//#endregion
//#region src/media/staged-inputs.ts
const STAGED_INPUT_DIRECTORY_PREFIX = "media/inbound/openclaw-staged-";
const STAGED_INPUT_GITIGNORE = "# Raw task inputs remain private; copy outputs into the project to publish.\n*\n";
const STAGED_INPUT_GITIGNORE_SHA256 = createHash("sha256").update(STAGED_INPUT_GITIGNORE).digest("hex");
/** A producer-shaped name is only a candidate; the marker establishes ownership. */
function stagedInputPathDirectory(relativePath) {
if (!relativePath.startsWith(STAGED_INPUT_DIRECTORY_PREFIX)) return;
const identity = relativePath.slice(30).split("/")[0];
return /^(?:[a-f0-9]{64}|[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12})$/u.exec(identity)?.[0] === identity ? STAGED_INPUT_DIRECTORY_PREFIX + identity : void 0;
}
function isStagedInputPath(relativePath, directories) {
const directory = stagedInputPathDirectory(relativePath);
return directory !== void 0 && directories.has(directory);
}
/** Complete manifests bind the regular marker's exact bytes through their file digest. */
function stagedInputDirectoriesFromEntries(entries) {
const directories = /* @__PURE__ */ new Set();
for (const entry of entries) {
const directory = stagedInputPathDirectory(entry.path);
if (directory && entry.path === `${directory}/.gitignore` && entry.type === "file" && entry.size === 78 && entry.sha256 === STAGED_INPUT_GITIGNORE_SHA256) directories.add(directory);
}
return directories;
}
const STAGED_INPUT_PATHS_JS = `
const STAGED_INPUT_DIRECTORY_PREFIX = ${JSON.stringify(STAGED_INPUT_DIRECTORY_PREFIX)};
const STAGED_INPUT_GITIGNORE = ${JSON.stringify(STAGED_INPUT_GITIGNORE)};
const STAGED_INPUT_GITIGNORE_SHA256 = ${JSON.stringify(STAGED_INPUT_GITIGNORE_SHA256)};
const stagedInputPathDirectory = ${stagedInputPathDirectory.toString()};
const isStagedInputPath = ${isStagedInputPath.toString()};
const stagedInputDirectoriesFromEntries = ${stagedInputDirectoriesFromEntries.toString()};`;
//#endregion
//#region src/gateway/worker-environments/workspace-path-exclusions.ts
const DERIVED_WORKSPACE_DIRECTORY_NAMES = [
"__pycache__",
".pytest_cache",
".mypy_cache",
".ruff_cache",
"node_modules"
];
const DERIVED_WORKSPACE_FILE_NAMES = [".DS_Store"];
const DERIVED_WORKSPACE_FILE_SUFFIXES = [".pyc", ".pyo"];
const WORKER_ATTACHMENT_DIRECTORY_PREFIX = "openclaw-inbound-";
const UUID_HEX = "[0-9a-f]";
const WORKER_ATTACHMENT_DIRECTORY_PATTERN = WORKER_ATTACHMENT_DIRECTORY_PREFIX + [
UUID_HEX.repeat(8),
UUID_HEX.repeat(4),
`4${UUID_HEX.repeat(3)}`,
`[89ab]${UUID_HEX.repeat(3)}`,
UUID_HEX.repeat(12)
].join("-");
const WORKER_ATTACHMENT_DIRECTORY_RE = new RegExp(`^${WORKER_ATTACHMENT_DIRECTORY_PATTERN}$`);
function isDerivedWorkspacePath(relativePath, retainedInput = false) {
if (retainedInput) return false;
return relativePath.split("/").some((segment) => WORKER_ATTACHMENT_DIRECTORY_RE.exec(segment)?.[0] === segment || DERIVED_WORKSPACE_DIRECTORY_NAMES.includes(segment) || DERIVED_WORKSPACE_FILE_NAMES.includes(segment) || DERIVED_WORKSPACE_FILE_SUFFIXES.some((suffix) => segment.endsWith(suffix)));
}
[
...DERIVED_WORKSPACE_DIRECTORY_NAMES,
...DERIVED_WORKSPACE_FILE_NAMES,
...DERIVED_WORKSPACE_FILE_SUFFIXES.map((suffix) => `*${suffix}`)
];
const WORKSPACE_PATH_EXCLUSIONS_JS = `
${STAGED_INPUT_PATHS_JS}
const DERIVED_WORKSPACE_DIRECTORY_NAMES = ${JSON.stringify(DERIVED_WORKSPACE_DIRECTORY_NAMES)};
const DERIVED_WORKSPACE_FILE_NAMES = ${JSON.stringify(DERIVED_WORKSPACE_FILE_NAMES)};
const DERIVED_WORKSPACE_FILE_SUFFIXES = ${JSON.stringify(DERIVED_WORKSPACE_FILE_SUFFIXES)};
const WORKER_ATTACHMENT_DIRECTORY_RE = ${WORKER_ATTACHMENT_DIRECTORY_RE.toString()};
const isDerivedWorkspacePath = ${isDerivedWorkspacePath.toString()};`;
const WORKSPACE_STAGED_INPUT_OWNERSHIP_JS = String.raw`
const stagedInputOwnership = new Map();
function isStagedInput(relativePath) {
const directory = stagedInputPathDirectory(relativePath);
if (!directory) return false;
if (stagedInputOwnership.has(directory)) return stagedInputOwnership.get(directory);
let owned = false;
let descriptor;
try {
let parent = root;
for (const segment of directory.split("/")) {
parent = path.join(parent, segment);
const stats = fs.lstatSync(parent);
if (!stats.isDirectory() || stats.isSymbolicLink()) return false;
}
const marker = path.join(parent, ".gitignore");
const before = fs.lstatSync(marker);
if (!before.isFile() || before.nlink !== 1 || before.size !== STAGED_INPUT_GITIGNORE.length) return false;
descriptor = fs.openSync(marker, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
const opened = fs.fstatSync(descriptor);
if (!opened.isFile() || opened.nlink !== 1 || opened.dev !== before.dev || opened.ino !== before.ino) return false;
const bytes = Buffer.alloc(STAGED_INPUT_GITIGNORE.length + 1);
const length = fs.readSync(descriptor, bytes, 0, bytes.length, 0);
const after = fs.lstatSync(marker);
owned = after.isFile() && after.nlink === 1 && after.dev === opened.dev && after.ino === opened.ino &&
after.mtimeMs === opened.mtimeMs && after.ctimeMs === opened.ctimeMs &&
fs.realpathSync(marker) === marker && length === STAGED_INPUT_GITIGNORE.length &&
bytes.subarray(0, length).toString("utf8") === STAGED_INPUT_GITIGNORE;
} catch {
// An ignored project directory with an absent or unsafe marker is not an input.
} finally {
if (descriptor !== undefined) fs.closeSync(descriptor);
stagedInputOwnership.set(directory, owned);
}
return owned;
}`;
//#endregion
//#region src/gateway/worker-environments/workspace-mutation-remote-script.ts
const REMOTE_WORKSPACE_MUTATION_CONTEXT_JS = String.raw`const mutationActions = [
"begin", "apply", "rollback", "recover", "commit", "settle", "receiver", "reset",
];
const workspace = process.argv[1];
const canonicalHome = process.argv[2];
const remoteRelative = process.argv[3];
const nonce = process.argv[4];
const currentHome = process.env.HOME;
if (
!currentHome ||
typeof workspace !== "string" ||
typeof canonicalHome !== "string" ||
typeof remoteRelative !== "string" ||
!path.posix.isAbsolute(canonicalHome) ||
path.posix.normalize(canonicalHome) !== canonicalHome ||
path.posix.isAbsolute(remoteRelative) ||
path.posix.normalize(remoteRelative) !== remoteRelative ||
path.posix.join(canonicalHome, remoteRelative) !== workspace ||
!/^[a-f0-9]{32}$/.test(nonce || "") ||
fs.realpathSync(currentHome) !== canonicalHome
) {
throw new Error("worker workspace mutation no longer matches its attested owner");
}
const workspaceStats = fs.lstatSync(workspace);
if (
!workspaceStats.isDirectory() ||
workspaceStats.isSymbolicLink() ||
fs.realpathSync(workspace) !== workspace
) {
throw new Error("worker workspace mutation no longer matches its attested owner");
}
const root = workspace;
const transactionRoot = path.dirname(root);
const transactionRootStats = fs.lstatSync(transactionRoot);
if (transactionRootStats.isSymbolicLink() || !transactionRootStats.isDirectory()) {
throw new Error("unsafe workspace mutation directory");
}
const workspaceKey = crypto.createHash("sha256").update(root).digest("hex");
function removeTree(target) {
let stats;
try {
stats = fs.lstatSync(target);
} catch (error) {
if (error && error.code === "ENOENT") return;
throw error;
}
if (stats.isDirectory() && !stats.isSymbolicLink()) {
fs.chmodSync(target, 0o700);
for (const name of fs.readdirSync(target)) removeTree(path.join(target, name));
fs.rmdirSync(target);
} else {
fs.unlinkSync(target);
}
}
function sameInode(left, right) {
return left.dev === right.dev && left.ino === right.ino;
}`;
const REMOTE_WORKSPACE_RSYNC_RECEIVER_RUNTIME_JS = String.raw`const receiverArgs = process.argv.slice(receiverArgvIndex);
const receiverDestination = receiverArgs.at(-1);
if (
receiverArgs[0] !== "--server" ||
receiverArgs.includes("--sender") ||
typeof receiverDestination !== "string" ||
!path.posix.isAbsolute(receiverDestination) ||
path.posix.normalize(receiverDestination).replace(/\/+$/, "") !== receiverTarget
) {
throw new Error("invalid worker workspace rsync receiver command");
}
const receiver = childProcess.spawn(
"sh",
["-c", 'IFS= read -r gate <&3 && [ "$gate" = open ] && exec rsync "$@"', "openclaw-rsync", ...receiverArgs],
{ detached: true, stdio: ["inherit", "inherit", "inherit", "pipe"] },
);
if (!Number.isSafeInteger(receiver.pid) || receiver.pid < 1) {
throw new Error("worker workspace rsync receiver did not start");
}
const lockOwnerPid = receiver.pid;
${REMOTE_WORKSPACE_MUTATION_LOCK_JS}
const receiverExit = new Promise((resolve, reject) => {
receiver.once("error", reject);
receiver.once("close", (code, signal) => resolve({ code, signal }));
});
const gate = receiver.stdio[3];
let lockAcquired = false;
let gateOpened = false;
(async () => {
try {
acquireWorkspaceLock();
lockAcquired = true;
validateReceiverTarget();
gateOpened = true;
gate.end("open\n");
const result = await receiverExit;
const groupWait = new Int32Array(new SharedArrayBuffer(4));
while (processGroupIsAlive(lockOwnerPid)) Atomics.wait(groupWait, 0, 0, 10);
releaseWorkspaceLock();
lockAcquired = false;
if (result.signal) process.kill(process.pid, result.signal);
process.exitCode = result.code === null ? 1 : result.code;
} finally {
if (!gateOpened) gate.end();
await receiverExit.catch(() => undefined);
if (lockAcquired) releaseWorkspaceLock();
}
})().catch((error) => {
process.stderr.write(String(error && error.stack ? error.stack : error) + "\n");
process.exitCode = 1;
});`;
const REMOTE_WORKSPACE_RSYNC_RECEIVER_JS = String.raw`const childProcess = require("node:child_process");
const crypto = require("node:crypto");
const fs = require("node:fs");
const path = require("node:path");
const action = "receiver";
${REMOTE_WORKSPACE_MUTATION_CONTEXT_JS}
const receiverTarget = process.argv[5];
function validateReceiverTarget() {
if (
typeof receiverTarget !== "string" ||
!path.posix.isAbsolute(receiverTarget) ||
path.posix.normalize(receiverTarget) !== receiverTarget ||
(receiverTarget !== root && path.posix.dirname(receiverTarget) !== root)
) {
throw new Error("invalid worker workspace rsync receiver target");
}
if (receiverTarget === root) return;
try {
if (fs.lstatSync(receiverTarget).isSymbolicLink()) {
throw new Error("unsafe worker workspace rsync receiver target");
}
} catch (error) {
if (!error || error.code !== "ENOENT") throw error;
}
}
validateReceiverTarget();
const receiverArgvIndex = 6;
${REMOTE_WORKSPACE_RSYNC_RECEIVER_RUNTIME_JS}`;
String.raw`const crypto = require("node:crypto");
const fs = require("node:fs");
const path = require("node:path");
const action = "reset";
${REMOTE_WORKSPACE_MUTATION_CONTEXT_JS}
const lockOwnerPid = process.pid;
${REMOTE_WORKSPACE_MUTATION_LOCK_JS}
${WORKSPACE_PATH_EXCLUSIONS_JS}
${WORKSPACE_STAGED_INPUT_OWNERSHIP_JS}
function clean(directory, relativeDirectory) {
const originalMode = fs.lstatSync(directory).mode & 0o7777;
fs.chmodSync(directory, originalMode | 0o700);
for (const name of fs.readdirSync(directory)) {
const relative = relativeDirectory ? relativeDirectory + "/" + name : name;
// Match the initial rsync receiver protections exactly: retry cleanup owns
// transferable workspace bytes, never Git metadata or derived scratch state.
if (name === ".git" || isDerivedWorkspacePath(relative, isStagedInput(relative))) continue;
const target = path.join(directory, name);
const stats = fs.lstatSync(target);
if (stats.isDirectory() && !stats.isSymbolicLink()) {
clean(target, relative);
if (fs.readdirSync(target).length === 0) fs.rmdirSync(target);
} else {
fs.unlinkSync(target);
}
}
fs.chmodSync(directory, originalMode);
}
acquireWorkspaceLock();
try {
clean(root, "");
process.stdout.write("reset " + nonce + "\n");
} finally {
releaseWorkspaceLock();
}`;
//#endregion
//#region src/gateway/worker-environments/workspace-accepted-remote-script.ts
const REMOTE_WORKSPACE_ACCEPTED_RSYNC_RECEIVER_JS = String.raw`const childProcess = require("node:child_process");
const crypto = require("node:crypto");
const fs = require("node:fs");
const path = require("node:path");
const action = "receiver";
${REMOTE_WORKSPACE_MUTATION_CONTEXT_JS}
const transaction = path.join(
transactionRoot,
".openclaw-accepted-" + workspaceKey + "-" + nonce,
);
const receiverRoot = path.join(transaction, "next");
function validateReceiverTarget() {
const phase = JSON.parse(fs.readFileSync(path.join(transaction, "phase.json"), "utf8"));
if (phase.version !== 1 || phase.nonce !== nonce || phase.phase !== "begun") {
throw new Error("invalid accepted workspace staging receiver phase");
}
const receiverRootStats = fs.lstatSync(receiverRoot);
if (
receiverRootStats.isSymbolicLink() ||
!receiverRootStats.isDirectory() ||
fs.realpathSync(receiverRoot) !== receiverRoot
) {
throw new Error("unsafe accepted workspace staging receiver");
}
}
validateReceiverTarget();
const receiverTarget = receiverRoot;
const receiverArgvIndex = 5;
${REMOTE_WORKSPACE_RSYNC_RECEIVER_RUNTIME_JS}`;
String.raw`const crypto = require("node:crypto");
const fs = require("node:fs");
const path = require("node:path");
const action = process.argv[1];
const acceptedActions = ["begin", "apply", "rollback", "recover", "commit", "settle"];
const mutationActions = [...acceptedActions, "receiver", "reset"];
if (!acceptedActions.includes(action)) throw new Error("invalid accepted workspace transaction action");
const root = fs.realpathSync(process.argv[2]);
const nonce = process.argv[3];
if (!/^[a-f0-9]{32}$/.test(nonce || "")) throw new Error("invalid accepted workspace transaction");
// REMOTE_WORKSPACE_SETUP_SCRIPT creates and chmods every workspace parent for this worker.
// Keeping the transaction beside the workspace makes all live swaps same-filesystem renames.
const transactionRoot = path.dirname(root);
const transactionRootStats = fs.lstatSync(transactionRoot);
if (transactionRootStats.isSymbolicLink() || !transactionRootStats.isDirectory()) {
throw new Error("unsafe accepted workspace transaction directory");
}
const workspaceKey = crypto.createHash("sha256").update(root).digest("hex");
const lockOwnerPid = process.pid;
const transactionPrefix = ".openclaw-accepted-" + workspaceKey + "-";
const cleanupPrefix = ".openclaw-accepted-cleanup-" + workspaceKey + "-";
const transaction = path.join(transactionRoot, transactionPrefix + nonce);
const cleanup = path.join(transactionRoot, cleanupPrefix + nonce);
const nextRoot = path.join(transaction, "next");
const backupRoot = path.join(transaction, "backup");
const pathsFile = path.join(transaction, "paths.json");
const stateFile = path.join(transaction, "state.json");
const ancestorModesFile = path.join(transaction, "ancestor-modes.json");
function isSafeRelativePath(relative) {
return (
typeof relative === "string" &&
relative &&
!relative.includes("\\") &&
!path.posix.isAbsolute(relative) &&
path.posix.normalize(relative) === relative &&
relative !== "." &&
relative !== ".." &&
relative !== ".git" &&
!relative.startsWith(".git/") &&
!relative.startsWith("../")
);
}
function parsePaths(raw) {
const values = JSON.parse(raw);
if (!Array.isArray(values) || values.length > 25_000) {
throw new Error("invalid accepted workspace paths");
}
const paths = [...new Set(values)];
for (const relative of paths) {
if (!isSafeRelativePath(relative)) throw new Error("unsafe accepted workspace path");
}
const selected = new Set(paths);
// Directory modes are canonical, so a changed directory is added, removed, or
// replaced and all of its accepted descendants are changed and staged too.
return paths
.filter((relative) => {
const segments = relative.split("/");
for (let index = 1; index < segments.length; index += 1) {
if (selected.has(segments.slice(0, index).join("/"))) return false;
}
return true;
})
.sort();
}
function targetPath(base, relative) {
return path.join(base, relative);
}
function livePath(relative) {
const segments = relative.split("/");
let parent = root;
for (const segment of segments.slice(0, -1)) {
parent = path.join(parent, segment);
const stats = fs.lstatSync(parent);
if (stats.isSymbolicLink() || !stats.isDirectory()) {
throw new Error("unsafe accepted workspace parent");
}
}
return path.join(root, relative);
}
function exists(target) {
try {
fs.lstatSync(target);
return true;
} catch (error) {
if (error && error.code === "ENOENT") return false;
throw error;
}
}
function removeTree(target) {
let stats;
try {
stats = fs.lstatSync(target);
} catch (error) {
if (error && error.code === "ENOENT") return;
throw error;
}
if (stats.isDirectory() && !stats.isSymbolicLink()) {
fs.chmodSync(target, 0o700);
for (const name of fs.readdirSync(target)) removeTree(path.join(target, name));
fs.rmdirSync(target);
} else {
fs.unlinkSync(target);
}
}
function sameInode(left, right) {
return left.dev === right.dev && left.ino === right.ino;
}
${REMOTE_WORKSPACE_MUTATION_LOCK_JS}
function readPaths() {
return parsePaths(fs.readFileSync(pathsFile, "utf8"));
}
function readPhase(candidate, required = true) {
let value;
try {
value = JSON.parse(fs.readFileSync(path.join(candidate, "phase.json"), "utf8"));
} catch (error) {
if (!required && error && error.code === "ENOENT") return null;
throw error;
}
if (
!value ||
value.version !== 1 ||
!/^[a-f0-9]{32}$/.test(value.nonce || "") ||
!candidate.endsWith("-" + value.nonce) ||
!["begun", "applying", "applied", "committed"].includes(value.phase)
) {
throw new Error("invalid accepted workspace transaction phase");
}
return value.phase;
}
function transitionPhase(candidate, current, expected, next) {
const allowed =
(expected === null && next !== null) ||
(expected === "begun" && next === "applying") ||
(expected === "applying" && next === "applied") ||
(expected === "applied" && next === "committed");
if (current !== expected || !allowed) {
throw new Error("invalid accepted workspace transaction phase transition");
}
const candidateNonce = path.basename(candidate).slice(-32);
if (!/^[a-f0-9]{32}$/.test(candidateNonce)) {
throw new Error("invalid accepted workspace transaction phase path");
}
const candidatePhase = path.join(candidate, "phase.json");
const temporary = candidatePhase + "." + process.pid + "." + crypto.randomBytes(4).toString("hex");
fs.writeFileSync(temporary, JSON.stringify({ version: 1, nonce: candidateNonce, phase: next }), {
flag: "wx",
mode: 0o600,
});
fs.renameSync(temporary, candidatePhase);
}
function normalizeRecoveredPhase(candidate, cleanupNamespace = false) {
const phase = readPhase(candidate, false);
if (phase !== null) return phase;
const inferred = cleanupNamespace
? "committed"
: exists(path.join(candidate, "applied"))
? "applied"
: exists(path.join(candidate, "state.json")) ||
exists(path.join(candidate, "ancestor-modes.json"))
? "applying"
: "begun";
// Transactions from pre-phase beta workers are normalized only while the
// locked recovery owner is deciding their existing durable rollback state.
transitionPhase(candidate, null, null, inferred);
return inferred;
}
function readState(candidate) {
const value = JSON.parse(fs.readFileSync(path.join(candidate, "state.json"), "utf8"));
if (!Array.isArray(value) || value.length > 25_000) {
throw new Error("invalid accepted workspace transaction state");
}
const relatives = parsePaths(JSON.stringify(value.map((entry) => entry && entry.relative)));
if (
relatives.length !== value.length ||
value.some(
(entry, index) =>
!entry ||
entry.relative !== relatives[index] ||
typeof entry.hadLive !== "boolean" ||
(entry.directoryMode !== undefined &&
(!Number.isInteger(entry.directoryMode) ||
entry.directoryMode < 0 ||
entry.directoryMode > 0o7777)),
)
) {
throw new Error("invalid accepted workspace transaction state");
}
return value;
}
function readAncestorModes(candidate) {
const candidateModes = path.join(candidate, "ancestor-modes.json");
if (!exists(candidateModes)) return [];
const value = JSON.parse(fs.readFileSync(candidateModes, "utf8"));
if (!Array.isArray(value) || value.length > 250_000) {
throw new Error("invalid accepted workspace ancestor modes");
}
const seen = new Set();
for (const entry of value) {
if (
!entry ||
(entry.relative !== "" && !isSafeRelativePath(entry.relative)) ||
seen.has(entry.relative) ||
!Number.isInteger(entry.mode) ||
entry.mode < 0 ||
entry.mode > 0o7777
) {
throw new Error("invalid accepted workspace ancestor modes");
}
seen.add(entry.relative);
}
return value;
}
function writeAncestorModes(value) {
const temporary = ancestorModesFile + ".tmp";
fs.writeFileSync(temporary, JSON.stringify(value), { flag: "wx", mode: 0o600 });
fs.renameSync(temporary, ancestorModesFile);
}
function ancestorPaths(paths) {
const ancestors = new Set();
for (const relative of paths) {
const segments = relative.split("/");
for (let index = 1; index < segments.length; index += 1) {
ancestors.add(segments.slice(0, index).join("/"));
}
}
if (ancestors.size + 1 > 250_000) {
throw new Error("accepted workspace transaction has too many ancestors");
}
return [...ancestors].sort((left, right) => {
const depth = left.split("/").length - right.split("/").length;
return depth || (left < right ? -1 : left > right ? 1 : 0);
});
}
function prepareWritableAncestors(paths) {
// parsePaths removes descendants of changed directories, so these are all
// unchanged live ancestors. Read every mode before mutating any permission.
const modes = ["", ...ancestorPaths(paths)].map((relative) => {
const target = relative ? targetPath(root, relative) : root;
const stats = fs.lstatSync(target);
if (stats.isSymbolicLink() || !stats.isDirectory()) {
throw new Error("unsafe accepted workspace parent");
}
return { relative, mode: stats.mode & 0o7777 };
});
writeAncestorModes(modes);
makeAncestorsWritable(modes);
return modes;
}
function makeAncestorsWritable(modes) {
const widened = [];
try {
for (const entry of modes) {
const target = entry.relative ? targetPath(root, entry.relative) : root;
const stats = fs.lstatSync(target);
if (stats.isSymbolicLink() || !stats.isDirectory()) {
throw new Error("unsafe accepted workspace parent");
}
const currentMode = stats.mode & 0o7777;
const writableMode = entry.mode | 0o700;
if (currentMode !== writableMode) {
fs.chmodSync(target, writableMode);
widened.push(entry);
}
}
} catch (error) {
try {
restoreAncestorModes(widened);
} catch (restoreError) {
const failure = new Error("accepted workspace ancestor mode rollback failed", {
cause: error,
});
Object.defineProperty(failure, "restoreFailure", { value: restoreError });
throw failure;
}
throw error;
}
}
function restoreAncestorModes(modes) {
for (const entry of [...modes].reverse()) {
const target = entry.relative ? targetPath(root, entry.relative) : root;
const stats = fs.lstatSync(target);
if (stats.isSymbolicLink() || !stats.isDirectory()) {
throw new Error("unsafe accepted workspace parent");
}
if ((stats.mode & 0o7777) !== entry.mode) fs.chmodSync(target, entry.mode);
}
}
function removeTransaction(candidate = transaction) {
removeTree(candidate);
}
function restoreTransaction(candidate) {
if (!exists(candidate)) return;
const ancestorModes = readAncestorModes(candidate);
makeAncestorsWritable(ancestorModes);
const candidateState = path.join(candidate, "state.json");
try {
if (exists(candidateState)) {
const candidateBackup = path.join(candidate, "backup");
for (const entry of [...readState(candidate)].reverse()) {
const live = livePath(entry.relative);
const backup = targetPath(candidateBackup, entry.relative);
if (exists(backup)) {
removeTree(live);
fs.renameSync(backup, live);
if (entry.directoryMode !== undefined) fs.chmodSync(live, entry.directoryMode);
} else if (!entry.hadLive) {
removeTree(live);
} else if (entry.directoryMode !== undefined && exists(live)) {
fs.chmodSync(live, entry.directoryMode);
}
}
}
} finally {
restoreAncestorModes(ancestorModes);
}
removeTransaction(candidate);
}
function recoverTransaction(candidate) {
const phase = normalizeRecoveredPhase(candidate);
if (phase === "committed") {
throw new Error("committed accepted workspace transaction is outside cleanup");
}
restoreTransaction(candidate);
}
function recoverCleanup(candidate) {
const phase = normalizeRecoveredPhase(candidate, true);
if (phase === "applied") transitionPhase(candidate, phase, "applied", "committed");
else if (phase !== "committed") throw new Error("invalid accepted workspace cleanup phase");
removeTransaction(candidate);
}
function recoverTransactions() {
for (const name of fs.readdirSync(transactionRoot)) {
if (name.startsWith(cleanupPrefix) && /^[a-f0-9]{32}$/.test(name.slice(cleanupPrefix.length))) {
recoverCleanup(path.join(transactionRoot, name));
}
}
for (const name of fs.readdirSync(transactionRoot)) {
if (
name.startsWith(transactionPrefix) &&
/^[a-f0-9]{32}$/.test(name.slice(transactionPrefix.length))
) {
recoverTransaction(path.join(transactionRoot, name));
}
}
}
function writeSettlementOutcome(outcome) {
process.stdout.write(JSON.stringify({ version: 1, outcome }) + "\n");
}
function runAction() {
if (action === "begin") {
const paths = parsePaths(fs.readFileSync(0, "utf8"));
recoverTransactions();
fs.mkdirSync(transaction, { mode: 0o700 });
fs.mkdirSync(nextRoot, { mode: 0o700 });
fs.mkdirSync(backupRoot, { mode: 0o700 });
fs.writeFileSync(pathsFile, JSON.stringify(paths), { mode: 0o600 });
transitionPhase(transaction, null, null, "begun");
process.stdout.write(nextRoot + "\n");
return;
}
if (action === "apply") {
const phase = readPhase(transaction);
if (phase === "applied") return;
if (phase === "applying") {
restoreTransaction(transaction);
throw new Error("recovered interrupted accepted workspace apply");
}
if (phase !== "begun") throw new Error("accepted workspace transaction cannot be applied");
transitionPhase(transaction, phase, "begun", "applying");
const paths = readPaths();
try {
const ancestorModes = prepareWritableAncestors(paths);
const state = paths.map((relative) => {
const live = livePath(relative);
if (!exists(live)) return { relative, hadLive: false };
const stats = fs.lstatSync(live);
return {
relative,
hadLive: true,
...(stats.isDirectory() && !stats.isSymbolicLink()
? { directoryMode: stats.mode & 0o7777 }
: {}),
};
});
const temporaryStateFile = stateFile + ".tmp";
fs.writeFileSync(temporaryStateFile, JSON.stringify(state), { flag: "wx", mode: 0o600 });
fs.renameSync(temporaryStateFile, stateFile);
for (const entry of state) {
if (!entry.hadLive) continue;
const source = livePath(entry.relative);
const sourceStats = fs.lstatSync(source);
const destination = targetPath(backupRoot, entry.relative);
fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
try {
if (sourceStats.isDirectory() && !sourceStats.isSymbolicLink()) {
fs.chmodSync(source, 0o700);
}
fs.renameSync(source, destination);
} catch (error) {
if (entry.directoryMode !== undefined && exists(source)) {
fs.chmodSync(source, entry.directoryMode);
}
throw error;
}
}
for (const entry of state) {
const source = targetPath(nextRoot, entry.relative);
if (exists(source)) fs.renameSync(source, livePath(entry.relative));
}
restoreAncestorModes(ancestorModes);
transitionPhase(transaction, "applying", "applying", "applied");
} catch (error) {
restoreTransaction(transaction);
throw error;
}
return;
}
if (action === "rollback") {
if (exists(cleanup)) {
if (exists(transaction)) throw new Error("ambiguous accepted workspace transaction state");
const cleanupPhase = normalizeRecoveredPhase(cleanup, true);
if (cleanupPhase !== "applied" && cleanupPhase !== "committed") {
throw new Error("accepted workspace cleanup cannot be rolled back");
}
fs.renameSync(cleanup, transaction);
restoreTransaction(transaction);
} else if (exists(transaction)) {
recoverTransaction(transaction);
}
return;
}
if (action === "recover") {
recoverTransactions();
return;
}
if (action === "settle") {
if (exists(transaction) && exists(cleanup)) {
throw new Error("ambiguous accepted workspace transaction state");
}
if (exists(cleanup)) {
const phase = normalizeRecoveredPhase(cleanup, true);
if (phase === "applied") transitionPhase(cleanup, phase, "applied", "committed");
else if (phase !== "committed") throw new Error("invalid accepted workspace cleanup phase");
writeSettlementOutcome("committed");
return;
}
if (!exists(transaction)) {
writeSettlementOutcome("rolled-back");
return;
}
const phase = normalizeRecoveredPhase(transaction);
if (phase === "applied") {
writeSettlementOutcome("applied");
return;
}
if (phase === "applying") {
restoreTransaction(transaction);
writeSettlementOutcome("rolled-back");
return;
}
if (phase === "begun") {
writeSettlementOutcome("begun");
return;
}
throw new Error("invalid accepted workspace settlement phase");
}
if (action === "commit") {
if (exists(transaction) && exists(cleanup)) {
throw new Error("ambiguous accepted workspace transaction state");
}
if (exists(cleanup)) {
const phase = readPhase(cleanup);
if (phase === "applied") transitionPhase(cleanup, phase, "applied", "committed");
else if (phase !== "committed") throw new Error("accepted workspace cleanup is not committed");
} else if (exists(transaction)) {
const phase = readPhase(transaction);
if (phase !== "applied") throw new Error("accepted workspace transaction is not applied");
// The namespace rename is the commit point. Later recovery removes the backup
// only after the gateway has had a chance to observe this command's success.
fs.renameSync(transaction, cleanup);
transitionPhase(cleanup, phase, "applied", "committed");
}
return;
}
throw new Error("invalid accepted workspace transaction action");
}
// Every mutating action and SSH-loss settlement shares this remote owner lock;
// a disconnected gateway can never overlap rollback with the live apply process.
acquireWorkspaceLock();
try {
runAction();
} finally {
releaseWorkspaceLock();
}`;
//#endregion
//#region src/worker/workspace-rsync-receiver.ts
const FIXED_DESTINATION = "openclaw-rsync-destination";
const [mode, encodedContext, nonce, ...receiverArgs] = process.argv.slice(2);
if (!/^(?:workspace-root|git-pack|accepted-next)$/u.test(mode ?? "") || !/^[A-Za-z0-9_-]+$/u.test(encodedContext ?? "") || !/^[a-f0-9]{32}$/u.test(nonce ?? "") || receiverArgs.at(-1) !== FIXED_DESTINATION) throw new Error("invalid worker workspace rsync receiver invocation");
const contextBytes = Buffer.from(encodedContext, "base64url");
const context = JSON.parse(contextBytes.toString("utf8"));
if (contextBytes.toString("base64url") !== encodedContext || !Array.isArray(context) || context.length !== 3 || !context.every((value) => typeof value === "string")) throw new Error("invalid worker workspace rsync receiver context");
const [workspace, canonicalHome, remoteRelative] = context;
const receiverMode = mode;
const receiverNonce = nonce;
const receiverTarget = receiverMode === "git-pack" ? path.posix.join(workspace, ".openclaw-base.pack") : receiverMode === "accepted-next" ? path.posix.join(path.posix.dirname(workspace), `.openclaw-accepted-${createHash("sha256").update(workspace).digest("hex")}-${receiverNonce}`, "next") : workspace;
process.argv = [
process.argv[0],
workspace,
canonicalHome,
remoteRelative,
receiverNonce,
...receiverMode === "accepted-next" ? [] : [receiverTarget],
...receiverArgs.with(receiverArgs.length - 1, receiverTarget)
];
compileFunction(receiverMode === "accepted-next" ? REMOTE_WORKSPACE_ACCEPTED_RSYNC_RECEIVER_JS : REMOTE_WORKSPACE_RSYNC_RECEIVER_JS, ["require"])(createRequire(import.meta.url));
//#endregion
export {};