openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,104 lines (1,103 loc) • 45.2 kB
JavaScript
import { c as normalizeOptionalLowercaseString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js";
import { t as createLazyImportLoader } from "./lazy-promise-DGqyc4Y4.js";
import { o as openRootFileFollowingParents } from "./boundary-file-read-uaJcf6X6.js";
import { a as canonicalPathFromExistingAncestor, t as FsSafeError, w as root } from "./fs-safe-B6pvPGnf.js";
import { t as createAbortError } from "./abort-signal-D2k14JsD.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { o as PROCESS_TOOL_DISPLAY_SUMMARY } from "./tool-description-presets-1n9mie5B.js";
import { l as rewrapToolWithBeforeToolCallHook, u as wrapToolWithBeforeToolCallHook } from "./agent-tools.before-tool-call-Bb7DJuFB.js";
import { J as decodeUtf8File, K as withFileMutationQueueKeyResolution, M as createReadTool, W as resolveFileMutationQueueKey, q as withFileMutationQueueKeysResolution } from "./sessions-BdNAJTEP.js";
import { t as PATH_ALIAS_POLICIES } from "./path-alias-guards-BjQDysBY.js";
import { n as assertSandboxPath } from "./sandbox-paths-hdOwYXJD.js";
import { n as resolvePathFromInput, r as toRelativeSandboxPath, t as preserveAtPrefixedRelativePath } from "./path-policy-s_BvJZSk.js";
import { a as resolveToolResultBudget } from "./tool-result-limits-CFk9DkYv.js";
import { r as resolveApplyPatchInputPath } from "./apply-patch-paths-CAGSzFJs.js";
import { r as copyAgentToolMetadata, t as bindAgentToolActionDescriptor, x as isToolWrappedWithBeforeToolCallHook } from "./agent-tool-metadata-CZvIgxro.js";
import { s as resolveReadOnlyWorkspaceSkillMounts } from "./workspace-mounts-CowGDwFc.js";
import { r as resolveSandboxFileMutationQueueKey } from "./file-mutation-identity-dqR8BH1r.js";
import { t as buildSandboxFsMounts } from "./fs-paths-a-ImteWc.js";
import { i as processSchema } from "./bash-tools.schemas-vTVPzmxz.js";
import { n as wrapToolWithAbortSignal } from "./agent-tools.abort-BzukmNyv.js";
import { n as describeProcessTool } from "./bash-tools.descriptions-CVVAAtco.js";
import { t as applyToolAvailabilityDescriptions } from "./agent-tools.deferred-followup-B6AL5yje.js";
import { a as createSandboxedEditTool, f as wrapToolWorkspaceRootGuardWithOptions, h as writeHostFile, i as createOpenClawReadTool, l as resolveAdaptiveReadMaxBytes, m as withMemoryWriteProvenance, n as createHostWorkspaceEditTool, o as createSandboxedReadTool, r as createHostWorkspaceWriteTool, s as createSandboxedWriteTool, u as wrapReadToolWithSkillContent } from "./tool-loop-detection-config-Bg911KiY.js";
import { t as createLazyExecTool } from "./lazy-exec-tool-BWPZD5in.js";
import fs from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
import { Type } from "typebox";
import { normalizeToolParameterSchema } from "@openclaw/ai/internal/openai";
//#region src/agents/agent-tools.schema.ts
/**
* Tool schema normalization wrappers.
* Applies provider-compatible parameter schema cleanup while preserving
* identity-backed metadata on normalized tools.
*/
function isObjectSchemaWithNoRequiredParams(schema) {
if (!schema || typeof schema !== "object" || Array.isArray(schema)) return false;
const record = schema;
const type = record.type;
if (!(type === "object" || Array.isArray(type) && type.some((entry) => entry === "object"))) return false;
return !schemaHasRequiredParams(record);
}
function schemaHasRequiredParams(schema) {
if (Array.isArray(schema.required) && schema.required.length > 0) return true;
for (const key of [
"allOf",
"anyOf",
"oneOf"
]) {
const variants = schema[key];
if (!Array.isArray(variants)) continue;
if (variants.some((variant) => variant !== null && typeof variant === "object" && !Array.isArray(variant) && schemaHasRequiredParams(variant))) return true;
}
return false;
}
/** Normalize a tool's parameter schema for the selected provider/model. */
function normalizeToolParameters(tool, options) {
const schema = tool.parameters && typeof tool.parameters === "object" ? tool.parameters : void 0;
if (!schema) return tool;
const parameters = normalizeToolParameterSchema(schema, options);
const normalized = {
...tool,
parameters
};
if (isObjectSchemaWithNoRequiredParams(parameters)) normalized.prepareArguments = (args) => {
const prepared = tool.prepareArguments ? tool.prepareArguments(args) : args;
return prepared === null || prepared === void 0 ? {} : prepared;
};
return copyAgentToolMetadata(tool, normalized);
}
//#endregion
//#region src/agents/agent-tools.finalize.ts
/** Apply the shared schema, hook, abort, and description wrappers to an authorized tool set. */
function finalizeAgentTools(options) {
const normalized = options.tools.map((tool) => normalizeToolParameters(tool, {
modelProvider: options.modelProvider,
modelId: options.modelId,
modelCompat: options.modelCompat
}));
options.recordToolPrepStage?.("schema-normalization");
const hookOptions = {
emitDiagnostics: options.emitBeforeToolCallDiagnostics,
...options.approvalMode ? { approvalMode: options.approvalMode } : {}
};
const withHooks = options.wrapBeforeToolCallHook === false ? normalized : normalized.map((tool) => isToolWrappedWithBeforeToolCallHook(tool) ? rewrapToolWithBeforeToolCallHook(tool, options.hookContext, hookOptions) : wrapToolWithBeforeToolCallHook(tool, options.hookContext, hookOptions));
options.recordToolPrepStage?.("tool-hooks");
const abortSignal = options.abortSignal;
const withAbort = abortSignal ? withHooks.map((tool) => wrapToolWithAbortSignal(tool, abortSignal)) : withHooks;
options.recordToolPrepStage?.("abort-wrappers");
const finalized = applyToolAvailabilityDescriptions(withAbort, { agentId: options.agentId });
options.recordToolPrepStage?.("deferred-followup-descriptions");
return finalized;
}
//#endregion
//#region src/agents/apply-patch-model-policy.ts
function isApplyPatchAllowedForModel(params) {
const allowModels = Array.isArray(params.allowModels) ? params.allowModels : [];
if (allowModels.length === 0) return true;
const modelId = params.modelId?.trim();
if (!modelId) return false;
const normalizedModelId = normalizeLowercaseStringOrEmpty(modelId);
const provider = normalizeOptionalLowercaseString(params.modelProvider);
const normalizedFull = provider && !normalizedModelId.includes("/") ? `${provider}/${normalizedModelId}` : normalizedModelId;
return allowModels.some((entry) => {
const normalized = normalizeOptionalLowercaseString(entry);
return Boolean(normalized && (normalized === normalizedModelId || normalized === normalizedFull));
});
}
//#endregion
//#region src/agents/apply-patch-file-ops.ts
async function createPatchTarget(params) {
if (await params.ops.createFileExclusive(params.target.resolved, params.contents) === "exists") throw new Error(`Cannot create ${params.target.display}: the file already exists. ${params.hint}`);
}
async function resolvePatchFileOps(options) {
if (options.sandbox) {
const { root, bridge } = options.sandbox;
return withPatchMemoryWriteProvenance({
observer: options.memoryWriteProvenance,
operations: {
readFile: async (filePath) => {
const buf = await bridge.readFile({
filePath,
cwd: root
});
return decodeUtf8File(buf, filePath);
},
writeFile: (filePath, content) => {
options.signal?.throwIfAborted();
return bridge.writeFile({
filePath,
cwd: root,
data: content,
signal: options.signal
});
},
createFileExclusive: (filePath, content) => {
if (!bridge.createFileExclusive) throw new Error("Sandbox filesystem bridge does not support atomic file creation; refusing to overwrite an existing path.");
options.signal?.throwIfAborted();
return bridge.createFileExclusive({
filePath,
cwd: root,
data: content,
signal: options.signal
});
},
remove: (filePath) => {
options.signal?.throwIfAborted();
return bridge.remove({
filePath,
cwd: root,
force: false,
signal: options.signal
});
},
mkdirp: (dir) => {
options.signal?.throwIfAborted();
return bridge.mkdirp({
filePath: dir,
cwd: root,
signal: options.signal
});
}
}
});
}
if (options.workspaceOnly === false) return withPatchMemoryWriteProvenance({
observer: options.memoryWriteProvenance,
operations: {
readFile: async (filePath) => decodeUtf8File(await fs$1.readFile(filePath), filePath),
writeFile: async (filePath, content) => {
await writeHostFile(filePath, content, options.signal);
},
createFileExclusive: async (filePath, content) => {
try {
options.signal?.throwIfAborted();
await fs$1.writeFile(filePath, content, {
encoding: "utf8",
flag: "wx"
});
return "created";
} catch (error) {
if (error.code === "EEXIST") return "exists";
throw error;
}
},
remove: (filePath) => {
options.signal?.throwIfAborted();
return fs$1.rm(filePath);
},
mkdirp: async (dir) => {
options.signal?.throwIfAborted();
await fs$1.mkdir(dir, { recursive: true });
}
}
});
const containmentRoot = options.root ?? options.cwd;
const root$1 = await root(containmentRoot);
const toCanonicalMutationRelative = async (filePath, pathOptions) => {
const absolute = path.resolve(options.cwd, filePath);
let canonicalAbsolute = absolute;
try {
const canonicalParent = await canonicalPathFromExistingAncestor(path.dirname(absolute));
canonicalAbsolute = path.join(canonicalParent, path.basename(absolute));
} catch {}
const canonicalRoot = await fs$1.realpath(containmentRoot).catch(() => containmentRoot);
return toRelativeSandboxPath(canonicalRoot, canonicalAbsolute, pathOptions);
};
return withPatchMemoryWriteProvenance({
observer: options.memoryWriteProvenance,
operations: {
readFile: async (filePath) => {
const opened = await openRootFileFollowingParents({
absolutePath: filePath,
rootPath: containmentRoot,
boundaryLabel: "workspace root"
});
assertBoundaryRead(opened, filePath);
try {
return decodeUtf8File(fs.readFileSync(opened.fd), filePath);
} finally {
fs.closeSync(opened.fd);
}
},
writeFile: async (filePath, content) => {
const relative = await toCanonicalMutationRelative(filePath);
options.signal?.throwIfAborted();
await root$1.write(relative, content, { encoding: "utf8" });
},
createFileExclusive: async (filePath, content) => {
const relative = await toCanonicalMutationRelative(filePath);
try {
options.signal?.throwIfAborted();
await root$1.create(relative, content, { encoding: "utf8" });
return "created";
} catch (error) {
if (error instanceof FsSafeError && (error.code === "already-exists" || error.code === "symlink")) return "exists";
throw error;
}
},
remove: async (filePath) => {
const relative = await toCanonicalMutationRelative(filePath);
options.signal?.throwIfAborted();
await root$1.remove(relative);
},
mkdirp: async (dir) => {
const relative = await toCanonicalMutationRelative(dir, { allowRoot: true });
options.signal?.throwIfAborted();
if (relative === "" || relative === ".") {
await root$1.ensureRoot();
return;
}
await root$1.mkdir(relative);
}
}
});
}
var PatchCreateExistsSignal = class extends Error {};
function withPatchMemoryWriteProvenance(params) {
const observer = params.observer;
const operations = withMemoryWriteProvenance(params.operations, observer);
if (!observer) return operations;
return {
...operations,
createFileExclusive: async (filePath, content) => {
if (!await observer.classifies(filePath)) return params.operations.createFileExclusive(filePath, content);
try {
await observer.write({
absolutePath: filePath,
contentBefore: "",
contentAfter: content,
commit: async () => {
if (await params.operations.createFileExclusive(filePath, content) === "exists") throw new PatchCreateExistsSignal();
}
});
return "created";
} catch (error) {
if (error instanceof PatchCreateExistsSignal) return "exists";
throw error;
}
}
};
}
function assertBoundaryRead(opened, targetPath) {
if (opened.ok) return;
const reason = opened.reason === "validation" ? "unsafe path" : "path not found";
const error = /* @__PURE__ */ new Error(`Failed boundary read for ${targetPath} (${reason})`);
const sourceCode = opened.error && typeof opened.error === "object" && "code" in opened.error ? opened.error.code : void 0;
if (sourceCode === "ENOENT" || sourceCode === "ENOTDIR") error.code = sourceCode;
throw error;
}
//#endregion
//#region src/agents/apply-patch-update.ts
/**
* Update-hunk application for the apply_patch parser.
* Locates expected old lines with tolerant matching, applies chunks in order,
* and retains source bytes that the patch does not change.
*/
const DASH_PUNCTUATION = /[\u2010-\u2015\u2212]/g;
const SINGLE_QUOTE_PUNCTUATION = /[\u2018-\u201B]/g;
const DOUBLE_QUOTE_PUNCTUATION = /[\u201C-\u201F]/g;
const SPACE_PUNCTUATION = /[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g;
async function defaultReadFile(filePath) {
return fs$1.readFile(filePath, "utf8");
}
/** Apply parsed update chunks to one file and return the new file contents. */
async function applyUpdateHunk(filePath, chunks, options) {
const source = parseSourceFile(await (options?.readFile ?? defaultReadFile)(filePath).catch((err) => {
throw new Error(`Failed to read file to update ${filePath}: ${formatErrorMessage(err)}`);
}));
const replacements = computeReplacements(source, filePath, chunks);
const newLines = applyReplacements(source.lines, replacements);
ensureInteriorEndings(newLines, source.preferredEnding);
return source.bom + newLines.map((line) => line.text + line.ending).join("");
}
function computeReplacements(source, filePath, chunks) {
const originalLines = source.lines.map((line) => line.text);
const replacements = [];
let lineIndex = 0;
for (const chunk of chunks) {
if (chunk.changeContext) {
const contextSearch = seekSequence(originalLines, [chunk.changeContext], lineIndex, false);
if (contextSearch.kind === "ambiguous") throw new Error(`Found ${contextSearch.occurrences} occurrences of context '${chunk.changeContext}' in ${filePath}. The context must be unique; use a more specific @@ context line.`);
if (contextSearch.kind === "missing") throw new Error(`Failed to find context '${chunk.changeContext}' in ${filePath}`);
lineIndex = contextSearch.index + 1;
}
if (chunk.oldLines.length === 0) {
const insertionIndex = chunk.changeContext && !chunk.isEndOfFile ? lineIndex : source.lines.length;
const insertedLines = buildChangedLines({
sourceLines: source.lines,
startIndex: insertionIndex,
oldCount: 0,
newLines: chunk.newLines,
preferredEnding: source.preferredEnding
});
const finalInsertedLine = insertedLines.at(-1);
if (source.missingFinalEnding && insertionIndex === source.lines.length && finalInsertedLine) finalInsertedLine.ending = "";
replacements.push([
insertionIndex,
0,
insertedLines
]);
lineIndex = insertionIndex;
continue;
}
let pattern = chunk.oldLines;
let newSlice = chunk.newLines;
let search = seekSequence(originalLines, pattern, lineIndex, chunk.isEndOfFile);
if (search.kind === "missing" && pattern[pattern.length - 1] === "") {
pattern = pattern.slice(0, -1);
if (newSlice.length > 0 && newSlice[newSlice.length - 1] === "") newSlice = newSlice.slice(0, -1);
search = seekSequence(originalLines, pattern, lineIndex, chunk.isEndOfFile);
}
if (search.kind === "ambiguous") throw new Error(`Found ${search.occurrences} occurrences of these lines in ${filePath}. The lines must be unique; include more surrounding lines in the hunk:\n${chunk.oldLines.join("\n")}`);
if (search.kind === "missing") throw new Error(`Failed to find expected lines in ${filePath}:\n${chunk.oldLines.join("\n")}`);
const found = search.index;
replacements.push([
found,
pattern.length,
buildChunkLines({
source,
matchIndex: found,
pattern,
newSlice,
contextOldIndexes: chunk.contextOldIndexes
})
]);
lineIndex = found + pattern.length;
}
replacements.sort((a, b) => a[0] - b[0]);
return replacements;
}
function buildChunkLines(params) {
const { source, matchIndex, pattern, newSlice, contextOldIndexes } = params;
const lines = [];
let oldStart = 0;
let newStart = 0;
for (const [newContext, oldContext] of contextOldIndexes.entries()) {
if (oldContext === void 0 || oldContext >= pattern.length || newContext >= newSlice.length) continue;
lines.push(...buildChangedLines({
sourceLines: source.lines,
startIndex: matchIndex + oldStart,
oldCount: oldContext - oldStart,
newLines: newSlice.slice(newStart, newContext),
preferredEnding: source.preferredEnding
}), source.lines[matchIndex + oldContext]);
oldStart = oldContext + 1;
newStart = newContext + 1;
}
lines.push(...buildChangedLines({
sourceLines: source.lines,
startIndex: matchIndex + oldStart,
oldCount: pattern.length - oldStart,
newLines: newSlice.slice(newStart),
preferredEnding: source.preferredEnding
}));
return lines;
}
function buildChangedLines(params) {
const { sourceLines, startIndex, oldCount, newLines, preferredEnding } = params;
const replacedLines = sourceLines.slice(startIndex, startIndex + oldCount);
const nearbyEnding = replacedLines.find((line) => line.ending)?.ending || sourceLines.at(startIndex)?.ending || sourceLines.at(startIndex - 1)?.ending || preferredEnding;
return newLines.map((text, index) => {
let ending = nearbyEnding;
if (replacedLines.length === newLines.length) ending = replacedLines.at(index)?.ending ?? nearbyEnding;
else if (index === newLines.length - 1 && replacedLines.length > 0) ending = replacedLines.at(-1)?.ending ?? nearbyEnding;
else if (index < replacedLines.length - 1) ending = replacedLines.at(index)?.ending ?? nearbyEnding;
return {
text,
ending
};
});
}
function applyReplacements(lines, replacements) {
const result = [...lines];
for (const [startIndex, oldLen, newLines] of [...replacements].toReversed()) result.splice(startIndex, oldLen, ...newLines);
return result;
}
function parseSourceFile(contents) {
const bom = contents.startsWith("") ? "" : "";
const lines = ((bom ? contents.slice(1) : contents).match(/[^\r\n]*(?:\r\n|\r|\n)|[^\r\n]+/g) ?? []).map(splitLineEnding);
return {
bom,
lines,
preferredEnding: lines.find((line) => line.ending)?.ending || "\n",
missingFinalEnding: lines.length > 0 && lines.at(-1)?.ending === ""
};
}
function splitLineEnding(line) {
if (line.endsWith("\r\n")) return {
text: line.slice(0, -2),
ending: "\r\n"
};
if (line.endsWith("\r")) return {
text: line.slice(0, -1),
ending: "\r"
};
if (line.endsWith("\n")) return {
text: line.slice(0, -1),
ending: "\n"
};
return {
text: line,
ending: ""
};
}
function ensureInteriorEndings(lines, preferredEnding) {
for (const line of lines.slice(0, -1)) line.ending ||= preferredEnding;
}
function seekSequence(lines, pattern, start, eof) {
if (pattern.length === 0) return {
kind: "found",
index: start
};
if (pattern.length > lines.length) return { kind: "missing" };
const maxStart = lines.length - pattern.length;
const searchStart = eof ? Math.max(start, maxStart) : start;
if (searchStart > maxStart) return { kind: "missing" };
const normalizers = [
(value) => value,
(value) => value.trimEnd(),
(value) => value.trim(),
(value) => normalizePunctuation(value.trim())
];
for (const normalize of normalizers) {
let index = null;
let occurrences = 0;
for (let i = searchStart; i <= maxStart; i += 1) if (linesMatch(lines, pattern, i, normalize)) {
index ??= i;
occurrences += 1;
}
if (index !== null) return occurrences === 1 ? {
kind: "found",
index
} : {
kind: "ambiguous",
occurrences
};
}
return { kind: "missing" };
}
function linesMatch(lines, pattern, start, normalize) {
for (let idx = 0; idx < pattern.length; idx += 1) {
const line = lines.at(start + idx);
const expected = pattern.at(idx);
if (line === void 0 || expected === void 0 || normalize(line) !== normalize(expected)) return false;
}
return true;
}
function normalizePunctuation(value) {
return value.replace(DASH_PUNCTUATION, "-").replace(SINGLE_QUOTE_PUNCTUATION, "'").replace(DOUBLE_QUOTE_PUNCTUATION, "\"").replace(SPACE_PUNCTUATION, " ");
}
//#endregion
//#region src/agents/apply-patch.ts
/**
* Runtime apply_patch tool and parser.
* Parses OpenAI-style patch envelopes and applies add/update/delete/move hunks
* through guarded host or sandbox filesystem operations.
*/
const BEGIN_PATCH_MARKER = "*** Begin Patch";
const END_PATCH_MARKER = "*** End Patch";
const ADD_FILE_MARKER = "*** Add File: ";
const DELETE_FILE_MARKER = "*** Delete File: ";
const UPDATE_FILE_MARKER = "*** Update File: ";
const MOVE_TO_MARKER = "*** Move to: ";
const EOF_MARKER = "*** End of File";
const CHANGE_CONTEXT_MARKER = "@@ ";
const EMPTY_CHANGE_CONTEXT_MARKER = "@@";
function normalizeUpdateComparison(content) {
const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
if (normalized.length === 0 || normalized.endsWith("\n")) return normalized;
return `${normalized}\n`;
}
const applyPatchSchema = Type.Object({ input: Type.String({ description: "Patch content using the *** Begin Patch/End Patch format." }) });
const ApplyPatchToolOutputSchema = Type.Object({ summary: Type.Object({
added: Type.Array(Type.String()),
modified: Type.Array(Type.String()),
deleted: Type.Array(Type.String())
}, { additionalProperties: false }) }, { additionalProperties: false });
/** Create the agent tool wrapper for applying patch-envelope input. */
function createApplyPatchTool(options = {}) {
const cwd = options.cwd ?? process.cwd();
const root = options.root ?? cwd;
const sandbox = options.sandbox;
const workspaceOnly = options.workspaceOnly !== false;
return {
name: "apply_patch",
label: "apply_patch",
description: "Patch one/many files. Input requires *** Begin Patch and *** End Patch.",
parameters: applyPatchSchema,
outputSchema: ApplyPatchToolOutputSchema,
execute: async (_toolCallId, args, signal) => {
const executionSignal = options.abortSignal ? AbortSignal.any(signal ? [signal, options.abortSignal] : [options.abortSignal]) : signal;
const params = args;
const input = typeof params.input === "string" ? params.input : "";
if (!input.trim()) throw new Error("Provide a patch input.");
if (executionSignal?.aborted) throw createAbortError("Aborted");
const result = await applyPatch(input, {
cwd,
root,
sandbox,
workspaceOnly,
memoryWriteProvenance: options.memoryWriteProvenance,
signal: executionSignal
});
return {
content: [{
type: "text",
text: result.text
}],
details: { summary: result.summary }
};
}
};
}
/** Parse and apply a patch envelope to the configured filesystem target. */
async function applyPatch(input, options) {
const parsed = parsePatchText(input);
if (parsed.hunks.length === 0) throw new Error("No files were modified.");
const patchOptions = {
...options,
patchInputPaths: await resolvePatchInputPaths(parsed.hunks, options)
};
const summary = {
added: [],
modified: [],
deleted: []
};
const seen = {
added: /* @__PURE__ */ new Set(),
modified: /* @__PURE__ */ new Set(),
deleted: /* @__PURE__ */ new Set()
};
const noOpPaths = /* @__PURE__ */ new Set();
let fileOpsPromise;
const getFileOps = () => fileOpsPromise ??= resolvePatchFileOps(patchOptions);
for (const hunk of parsed.hunks) {
if (patchOptions.signal?.aborted) throw createAbortError("Aborted");
if (hunk.kind === "add") {
const targetResolution = resolvePatchPath(hunk.path, patchOptions);
await withFileMutationQueueKeyResolution(targetResolution.then((target) => target.queueKey), async () => {
const target = await targetResolution;
const fileOps = await getFileOps();
await assertPatchParentPath(hunk.path, patchOptions);
await ensureDir(target.resolved, fileOps);
await createPatchTarget({
target,
contents: hunk.contents,
ops: fileOps,
hint: `Use "*** Update File: ${target.display}" to change it, or delete it earlier in the same patch.`
});
});
recordSummary(summary, seen, "added", (await targetResolution).display);
continue;
}
if (hunk.kind === "delete") {
const targetResolution = resolvePatchPath(hunk.path, patchOptions, PATH_ALIAS_POLICIES.unlinkTarget);
await withFileMutationQueueKeyResolution(targetResolution.then((target) => target.queueKey), async () => {
const target = await targetResolution;
await (await getFileOps()).remove(target.resolved);
});
recordSummary(summary, seen, "deleted", (await targetResolution).display);
continue;
}
const targetResolution = resolvePatchPath(hunk.path, patchOptions);
const moveTargetResolution = hunk.movePath ? resolvePatchPath(hunk.movePath, patchOptions) : void 0;
await withFileMutationQueueKeysResolution(Promise.all([targetResolution.then((target) => target.queueKey), ...moveTargetResolution ? [moveTargetResolution.then((moveTarget) => moveTarget.queueKey)] : []]), async () => {
const target = await targetResolution;
const moveTarget = moveTargetResolution ? await moveTargetResolution : void 0;
const fileOps = await getFileOps();
const applied = await applyUpdateHunk(target.resolved, hunk.chunks, fileOps);
if (hunk.movePath && moveTarget) {
await assertPatchParentPath(hunk.movePath, patchOptions);
await ensureDir(moveTarget.resolved, fileOps);
const moveResolvesToSource = path.resolve(moveTarget.resolved) === path.resolve(target.resolved);
if (moveResolvesToSource) {
if (normalizeUpdateComparison(await fileOps.readFile(target.resolved)) === normalizeUpdateComparison(applied)) noOpPaths.add(target.display);
else {
noOpPaths.delete(target.display);
await fileOps.writeFile(target.resolved, applied);
}
} else {
noOpPaths.delete(target.display);
await createPatchTarget({
target: moveTarget,
contents: applied,
ops: fileOps,
hint: "Delete it earlier in the same patch to replace it."
});
await fileOps.remove(target.resolved);
}
if (!noOpPaths.has(target.display)) recordSummary(summary, seen, "modified", moveResolvesToSource ? target.display : moveTarget.display);
return;
}
if (normalizeUpdateComparison(await fileOps.readFile(target.resolved)) === normalizeUpdateComparison(applied)) noOpPaths.add(target.display);
else {
noOpPaths.delete(target.display);
await fileOps.writeFile(target.resolved, applied);
recordSummary(summary, seen, "modified", target.display);
}
});
}
const noOp = noOpPaths.size > 0 && Object.values(summary).every((paths) => paths.length === 0);
return {
summary,
text: noOp ? `No changes made to ${Array.from(noOpPaths).join(", ")}.` : formatSummary(summary),
...noOp ? { noOp: true } : {}
};
}
async function resolvePatchInputPaths(hunks, options) {
const rawPaths = /* @__PURE__ */ new Set();
for (const hunk of hunks) {
rawPaths.add(hunk.path);
if (hunk.kind === "update" && hunk.movePath) rawPaths.add(hunk.movePath);
}
const resolved = /* @__PURE__ */ new Map();
for (const rawPath of rawPaths) resolved.set(rawPath, options.sandbox ? await resolveApplyPatchInputPath(rawPath, options) : preserveAtPrefixedRelativePath(rawPath, options.cwd));
return resolved;
}
function recordSummary(summary, seen, bucket, value) {
if (seen[bucket].has(value)) return;
seen[bucket].add(value);
summary[bucket].push(value);
}
function formatSummary(summary) {
const lines = ["Success. Updated the following files:"];
for (const file of summary.added) lines.push(`A ${file}`);
for (const file of summary.modified) lines.push(`M ${file}`);
for (const file of summary.deleted) lines.push(`D ${file}`);
return lines.join("\n");
}
async function ensureDir(filePath, ops) {
const parent = path.dirname(filePath);
if (!parent || parent === ".") return;
await ops.mkdirp(parent);
}
async function assertPatchParentPath(rawFilePath, options) {
if (options.workspaceOnly === false || options.sandbox) return;
const filePath = preserveAtPrefixedRelativePath(rawFilePath, options.cwd);
const parent = path.dirname(filePath);
if (!parent || parent === ".") return;
await assertNoExistingParentAliases({
parentPath: (await assertSandboxPath({
filePath: parent,
cwd: options.cwd,
root: options.root ?? options.cwd
})).resolved,
rootPath: options.root ?? options.cwd
});
}
async function assertNoExistingParentAliases(params) {
const rootPath = path.resolve(params.rootPath);
const parentPath = path.resolve(params.parentPath);
const relative = path.relative(rootPath, parentPath);
if (!relative || relative === "" || relativePathEscapesRoot(relative)) return;
let current = rootPath;
for (const segment of relative.split(path.sep)) {
if (!segment) continue;
current = path.join(current, segment);
const stat = await fs$1.lstat(current).catch((error) => {
if (error.code === "ENOENT") return null;
throw error;
});
if (!stat) return;
if (stat.isSymbolicLink()) throw new Error(`Path alias under sandbox root: ${path.relative(rootPath, current)}`);
}
}
async function resolvePatchPath(rawFilePath, options, aliasPolicy = PATH_ALIAS_POLICIES.strict) {
if (options.sandbox) {
const filePath = options.patchInputPaths?.get(rawFilePath) ?? await resolveApplyPatchInputPath(rawFilePath, options);
const resolved = options.sandbox.bridge.resolvePath({
filePath,
cwd: options.cwd
});
if (options.workspaceOnly !== false && resolved.hostPath) await assertSandboxPath({
filePath: resolved.hostPath,
cwd: options.cwd,
root: options.root ?? options.cwd,
allowFinalSymlinkForUnlink: aliasPolicy.allowFinalSymlinkForUnlink,
allowFinalHardlinkForUnlink: aliasPolicy.allowFinalHardlinkForUnlink
});
return {
resolved: resolved.hostPath ?? resolved.containerPath,
queueKey: await resolveSandboxFileMutationQueueKey({
bridge: options.sandbox.bridge,
root: options.sandbox.root,
filePath,
cwd: options.cwd,
signal: options.signal
}),
display: resolved.relativePath || resolved.containerPath
};
}
const filePath = options.patchInputPaths?.get(rawFilePath) ?? preserveAtPrefixedRelativePath(rawFilePath, options.cwd);
const resolved = options.workspaceOnly !== false ? (await assertSandboxPath({
filePath,
cwd: options.cwd,
root: options.root ?? options.cwd,
allowFinalSymlinkForUnlink: aliasPolicy.allowFinalSymlinkForUnlink,
allowFinalHardlinkForUnlink: aliasPolicy.allowFinalHardlinkForUnlink
})).resolved : resolvePathFromInput(filePath, options.cwd);
return {
resolved,
queueKey: await resolveFileMutationQueueKey(resolved),
display: toDisplayPath(resolved, options.cwd)
};
}
function toDisplayPath(resolved, cwd) {
const relative = path.relative(cwd, resolved);
if (!relative || relative === "") return path.basename(resolved);
if (relativePathEscapesRoot(relative)) return resolved;
return relative;
}
function relativePathEscapesRoot(relativePath) {
return relativePath === ".." || relativePath.startsWith("../") || relativePath.startsWith("..\\") || path.isAbsolute(relativePath);
}
function parsePatchText(input) {
const trimmed = input.trim();
if (!trimmed) throw new Error("Invalid patch: input is empty.");
const validated = checkPatchBoundariesLenient(trimmed.split(/\r?\n/));
const hunks = [];
const lastLineIndex = validated.length - 1;
let remaining = validated.slice(1, lastLineIndex);
let lineNumber = 2;
while (remaining.length > 0) {
const { hunk, consumed } = parseOneHunk(remaining, lineNumber);
hunks.push(hunk);
lineNumber += consumed;
remaining = remaining.slice(consumed);
}
return {
hunks,
patch: validated.join("\n")
};
}
function checkPatchBoundariesLenient(lines) {
const strictError = checkPatchBoundariesStrict(lines);
if (!strictError) return lines;
if (lines.length < 4) throw new Error(strictError);
const first = lines[0];
const last = lines.at(-1);
if (last && (first === "<<EOF" || first === "<<'EOF'" || first === "<<\"EOF\"") && last.endsWith("EOF")) {
const inner = lines.slice(1, -1);
const innerError = checkPatchBoundariesStrict(inner);
if (!innerError) return inner;
throw new Error(innerError);
}
throw new Error(strictError);
}
function checkPatchBoundariesStrict(lines) {
const firstLine = lines[0]?.trim();
const lastLine = lines[lines.length - 1]?.trim();
if (firstLine === BEGIN_PATCH_MARKER && lastLine === END_PATCH_MARKER) return null;
if (firstLine !== BEGIN_PATCH_MARKER) return "The first line of the patch must be '*** Begin Patch'";
return "The last line of the patch must be '*** End Patch'";
}
function parseOneHunk(lines, lineNumber) {
if (lines.length === 0) throw new Error(`Invalid patch hunk at line ${lineNumber}: empty hunk`);
const firstLine = lines.at(0)?.trim();
if (firstLine === void 0) throw new Error(`Invalid patch hunk at line ${lineNumber}: empty hunk`);
if (firstLine.startsWith(ADD_FILE_MARKER)) {
const targetPath = firstLine.slice(14);
let contents = "";
let consumed = 1;
for (const addLine of lines.slice(1)) if (addLine.startsWith("+")) {
contents += `${addLine.slice(1)}\n`;
consumed += 1;
} else break;
return {
hunk: {
kind: "add",
path: targetPath,
contents
},
consumed
};
}
if (firstLine.startsWith(DELETE_FILE_MARKER)) return {
hunk: {
kind: "delete",
path: firstLine.slice(17)
},
consumed: 1
};
if (firstLine.startsWith(UPDATE_FILE_MARKER)) {
const targetPath = firstLine.slice(17);
let remaining = lines.slice(1);
let consumed = 1;
let movePath;
const moveCandidate = remaining[0]?.trim();
if (moveCandidate?.startsWith(MOVE_TO_MARKER)) {
movePath = moveCandidate.slice(13);
remaining = remaining.slice(1);
consumed += 1;
}
const chunks = [];
while (remaining.length > 0) {
const firstRemaining = remaining.at(0);
if (firstRemaining === void 0) break;
if (firstRemaining.trim() === "") {
remaining = remaining.slice(1);
consumed += 1;
continue;
}
if (firstRemaining.startsWith("***")) break;
const { chunk, consumed: chunkLines } = parseUpdateFileChunk(remaining, lineNumber + consumed, chunks.length === 0);
chunks.push(chunk);
remaining = remaining.slice(chunkLines);
consumed += chunkLines;
}
if (chunks.length === 0) throw new Error(`Invalid patch hunk at line ${lineNumber}: Update file hunk for path '${targetPath}' is empty`);
return {
hunk: {
kind: "update",
path: targetPath,
movePath,
chunks
},
consumed
};
}
throw new Error(`Invalid patch hunk at line ${lineNumber}: '${lines[0]}' is not a valid hunk header. Valid hunk headers: '*** Add File: {path}', '*** Delete File: {path}', '*** Update File: {path}'`);
}
function parseUpdateFileChunk(lines, lineNumber, allowMissingContext) {
if (lines.length === 0) throw new Error(`Invalid patch hunk at line ${lineNumber}: Update hunk does not contain any lines`);
let changeContext;
let startIndex = 0;
const firstLine = lines.at(0);
if (firstLine === EMPTY_CHANGE_CONTEXT_MARKER) startIndex = 1;
else if (firstLine?.startsWith(CHANGE_CONTEXT_MARKER)) {
changeContext = firstLine.slice(3);
startIndex = 1;
} else if (!allowMissingContext) throw new Error(`Invalid patch hunk at line ${lineNumber}: Expected update hunk to start with a @@ context marker, got: '${firstLine}'`);
if (startIndex >= lines.length) throw new Error(`Invalid patch hunk at line ${lineNumber + 1}: Update hunk does not contain any lines`);
const chunk = {
changeContext,
oldLines: [],
newLines: [],
contextOldIndexes: [],
isEndOfFile: false
};
let parsedLines = 0;
for (const line of lines.slice(startIndex)) {
if (line === EOF_MARKER) {
if (parsedLines === 0) throw new Error(`Invalid patch hunk at line ${lineNumber + 1}: Update hunk does not contain any lines`);
chunk.isEndOfFile = true;
parsedLines += 1;
break;
}
const marker = line[0];
if (!marker) {
chunk.contextOldIndexes.push(chunk.oldLines.length);
chunk.oldLines.push("");
chunk.newLines.push("");
parsedLines += 1;
continue;
}
if (marker === " ") {
const content = line.slice(1);
chunk.contextOldIndexes.push(chunk.oldLines.length);
chunk.oldLines.push(content);
chunk.newLines.push(content);
parsedLines += 1;
continue;
}
if (marker === "+") {
chunk.contextOldIndexes.push(void 0);
chunk.newLines.push(line.slice(1));
parsedLines += 1;
continue;
}
if (marker === "-") {
chunk.oldLines.push(line.slice(1));
parsedLines += 1;
continue;
}
if (parsedLines === 0) throw new Error(`Invalid patch hunk at line ${lineNumber + 1}: Unexpected line found in update hunk: '${line}'. Every line should start with ' ' (context line), '+' (added line), or '-' (removed line)`);
break;
}
return {
chunk,
consumed: parsedLines + startIndex
};
}
if (process.env.VITEST || false) globalThis[Symbol.for("openclaw.applyPatchTestApi")] = { applyPatch };
//#endregion
//#region src/agents/lazy-process-tool.ts
const bashToolsModuleLoader = createLazyImportLoader(() => import("./bash-tools-B3QMgbTm.js"));
/** Build process lazily so tool discovery does not load the shell runtime. */
function createLazyProcessTool(defaults) {
let loadedTool;
const loadTool = async () => {
if (!loadedTool) {
const { createProcessTool } = await bashToolsModuleLoader.load();
loadedTool = createProcessTool(defaults);
}
return loadedTool;
};
return {
name: "process",
label: "process",
displaySummary: PROCESS_TOOL_DISPLAY_SUMMARY,
description: describeProcessTool({ hasCronTool: defaults?.hasCronTool === true }),
parameters: processSchema,
execute: async (toolCallId, params, signal, onUpdate) => (await loadTool()).execute(toolCallId, params, signal, onUpdate)
};
}
//#endregion
//#region src/agents/core-coding-tools.ts
function sandboxReadMounts(sandbox) {
const mounts = buildSandboxFsMounts(sandbox).filter((mount) => mount.source !== "workspace").map((mount) => ({
containerRoot: mount.containerRoot,
hostRoot: mount.hostRoot
}));
return mounts.length > 0 ? mounts : void 0;
}
function resolveSkillReadRoots(skillsSnapshot) {
const roots = /* @__PURE__ */ new Set();
for (const skill of skillsSnapshot?.resolvedSkills ?? []) {
const baseDir = typeof skill.baseDir === "string" ? skill.baseDir.trim() : "";
const filePath = typeof skill.filePath === "string" ? skill.filePath.trim() : "";
const root = baseDir || (filePath ? path.dirname(filePath) : "");
if (!root || !path.isAbsolute(root)) continue;
roots.add(path.resolve(root));
}
return roots.size > 0 ? Array.from(roots) : void 0;
}
function guardHostWorkspaceTool(tool, options) {
return wrapToolWorkspaceRootGuardWithOptions(tool, options.containmentRoot, {
resolutionCwd: options.codingRoot,
normalizeGuardedPathParams: true
});
}
/** Materialize only the core file and shell families selected by the runtime owner. */
function createCoreCodingTools(options) {
const sandbox = options.sandbox;
const sandboxRoot = sandbox?.workspaceDir;
const sandboxFsBridge = sandbox?.fsBridge;
const allowWorkspaceWrites = sandbox?.workspaceAccess !== "ro";
if (sandboxRoot && !sandboxFsBridge && (options.includeBaseCodingTools || options.includeShellTools)) throw new Error("Sandbox filesystem bridge is unavailable.");
const skillReadRoots = sandboxRoot ? void 0 : resolveSkillReadRoots(options.skillsSnapshot);
const needsReadOnlyWorkspaceSkillMounts = options.includeShellTools || options.includeBaseCodingTools && options.workspaceOnly;
const readOnlyWorkspaceSkillMounts = sandbox && needsReadOnlyWorkspaceSkillMounts ? resolveReadOnlyWorkspaceSkillMounts({
workspaceDir: sandbox.workspaceDir,
agentWorkspaceDir: sandbox.agentWorkspaceDir,
skillsWorkspaceDir: sandbox.skillsWorkspaceDir,
workdir: sandbox.containerWorkdir,
workspaceAccess: sandbox.workspaceAccess
}) : [];
const base = [];
if (options.includeBaseCodingTools) {
const read = sandboxRoot ? createSandboxedReadTool({
root: sandboxRoot,
bridge: sandboxFsBridge,
modelContextWindowTokens: options.modelContextWindowTokens,
imageSanitization: options.imageSanitization,
modelHasVision: options.modelHasVision
}) : createReadTool(options.codingRoot, {
maxBytes: resolveAdaptiveReadMaxBytes(options),
modelBudget: resolveToolResultBudget(options.modelContextWindowTokens),
modelHasVision: options.modelHasVision
});
const guarded = options.workspaceOnly ? wrapToolWorkspaceRootGuardWithOptions(read, sandboxRoot ?? options.containmentRoot, sandboxRoot ? {
additionalContainerMounts: sandboxReadMounts(sandbox),
containerWorkdir: sandbox.containerWorkdir,
bridge: sandboxFsBridge
} : {
additionalRoots: skillReadRoots,
resolutionCwd: options.codingRoot,
normalizeGuardedPathParams: true
}) : read;
const wrapped = sandboxRoot ? guarded : createOpenClawReadTool(guarded, {
modelContextWindowTokens: options.modelContextWindowTokens,
imageSanitization: options.imageSanitization,
cwd: options.codingRoot
});
base.push(wrapReadToolWithSkillContent(wrapped, options.skillsSnapshot?.resolvedSkills, {
modelContextWindowTokens: options.modelContextWindowTokens,
imageSanitization: options.imageSanitization,
cwd: options.codingRoot,
containerWorkdir: sandbox?.containerWorkdir,
instructionPaths: options.skillInstructionPaths,
instructionDeliveryCache: options.skillInstructionDeliveryCache
}));
if (!options.readOnly && !sandboxRoot) {
const edit = createHostWorkspaceEditTool(options.codingRoot, {
containmentRoot: options.containmentRoot,
workspaceOnly: options.workspaceOnly,
memoryWriteProvenance: options.memoryWriteProvenance,
abortSignal: options.abortSignal
});
base.push(options.workspaceOnly ? guardHostWorkspaceTool(edit, options) : edit);
const write = createHostWorkspaceWriteTool(options.codingRoot, {
containmentRoot: options.containmentRoot,
workspaceOnly: options.workspaceOnly,
memoryWriteProvenance: options.memoryWriteProvenance,
abortSignal: options.abortSignal
});
base.push(options.workspaceOnly ? guardHostWorkspaceTool(write, options) : write);
}
}
if (options.includeBaseCodingTools && !options.readOnly && sandboxRoot && allowWorkspaceWrites) {
const toolOptions = {
root: sandboxRoot,
bridge: sandboxFsBridge,
memoryWriteProvenance: options.memoryWriteProvenance,
abortSignal: options.abortSignal
};
const edit = createSandboxedEditTool(toolOptions);
const write = createSandboxedWriteTool(toolOptions);
base.push(options.workspaceOnly ? wrapToolWorkspaceRootGuardWithOptions(edit, sandboxRoot, {
containerWorkdir: sandbox.containerWorkdir,
bridge: sandboxFsBridge
}) : edit, options.workspaceOnly ? wrapToolWorkspaceRootGuardWithOptions(write, sandboxRoot, {
containerWorkdir: sandbox.containerWorkdir,
bridge: sandboxFsBridge
}) : write);
}
options.recordToolPrepStage?.("base-coding-tools");
const shell = [];
if (options.includeShellTools) {
if (options.applyPatchEnabled && (!sandboxRoot || allowWorkspaceWrites)) shell.push(createApplyPatchTool({
cwd: options.codingRoot,
root: options.containmentRoot,
sandbox: sandboxRoot && allowWorkspaceWrites ? {
root: sandboxRoot,
bridge: sandboxFsBridge
} : void 0,
workspaceOnly: options.applyPatchWorkspaceOnly,
memoryWriteProvenance: options.memoryWriteProvenance,
abortSignal: options.abortSignal
}));
shell.push(createLazyExecTool({
...options.execDefaults,
...sandbox?.required ? { sandboxRequired: true } : {},
cwd: options.codingRoot,
sandbox: sandbox ? {
containerName: sandbox.containerName,
workspaceDir: sandbox.workspaceDir,
containerWorkdir: sandbox.containerWorkdir,
workdirValidation: sandbox.backend?.workdirValidation,
validateWorkdir: sandbox.backend?.validateWorkdir?.bind(sandbox.backend),
discardPreparedWorkdir: sandbox.backend?.discardPreparedWorkdir?.bind(sandbox.backend),
workdirRoots: sandbox.backend?.workdirRoots,
readOnlyWorkspaceSkillMounts,
env: sandbox.backend?.env ?? sandbox.docker.env,
buildExecSpec: sandbox.backend?.buildExecSpec.bind(sandbox.backend),
finalizeExec: sandbox.backend?.finalizeExec?.bind(sandbox.backend)
} : void 0
}), createLazyProcessTool(options.processDefaults));
}
options.recordToolPrepStage?.("shell-tools");
base.forEach((tool) => bindAgentToolActionDescriptor(tool, {
family: "data",
operation: "filesystem"
}));
shell.forEach((tool) => bindAgentToolActionDescriptor(tool, {
family: "tool",
operation: "process"
}));
return [...base, ...shell];
}
//#endregion
export { isApplyPatchAllowedForModel as n, finalizeAgentTools as r, createCoreCodingTools as t };