openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,164 lines • 51.1 kB
JavaScript
import { c as normalizeOptionalLowercaseString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-6kR-86XC.mjs";
import { i as asOptionalObjectRecord } from "./record-coerce-TIsquoVR.mjs";
import { t as createLazyImportLoader } from "./lazy-promise-DGqyc4Y4.mjs";
import { t as FsSafeError, w as root } from "./fs-safe-CS1DrPTK.mjs";
import { t as createAbortError } from "./abort-signal-Z3A36sLL.mjs";
import { a as openRootFile } from "./boundary-file-read-CCth2Hwg.mjs";
import { t as formatErrorMessage } from "./errors-BSdPMFao.mjs";
import { o as PROCESS_TOOL_DISPLAY_SUMMARY } from "./tool-description-presets-CcdpQmwX.mjs";
import { c as rewrapToolWithBeforeToolCallHook, l as wrapToolWithBeforeToolCallHook } from "./agent-tools.before-tool-call-twVNco_m.mjs";
import { i as getAgentWorkspaceAccess, t as WorkspaceAccessUnavailableError } from "./workspace-access-CCdQbCKD.mjs";
import { a as resolveToolResultBudget } from "./tool-result-limits-B-fhY8wF.mjs";
import { g as isToolWrappedWithBeforeToolCallHook, r as copyAgentToolMetadata, t as bindAgentToolActionDescriptor } from "./agent-tool-metadata-Duxf3Dwc.mjs";
import { a as markHostRootEscape, i as isPathBoundaryEscapeError, n as assertSandboxPath, r as isHostRootEscapeError } from "./sandbox-paths-IwSGCnhL.mjs";
import { a as toRelativeSandboxPath, i as resolveSandboxPathMapping, n as relativePathInsideSandboxRoot, r as resolvePathFromInput, t as preserveAtPrefixedRelativePath } from "./path-policy-o1nxUmmt.mjs";
import { o as normalizeFileToolPathParam } from "./agent-tools.before-tool-call.decision-C_YSuWAa.mjs";
import { i as toDisplayPath, r as resolveApplyPatchInputPath } from "./apply-patch-paths-DcLS2Anx.mjs";
import { r as captureAgentToolSourceExecutionGuard } from "./agent-tool-source-execution-guard-DJwXOEPL.mjs";
import { i as processSchema } from "./bash-tools.schemas-CB8uAUJo.mjs";
import { n as wrapToolWithAbortSignal } from "./agent-tools.abort-Di2JgW04.mjs";
import { r as describeProcessTool } from "./bash-tools.descriptions-CdVoGX1S.mjs";
import { t as applyToolAvailabilityDescriptions } from "./agent-tools.deferred-followup-CeSSU_x-.mjs";
import { a as createSandboxedReadTool, c as resolveAdaptiveReadMaxBytes, f as wrapToolWorkspaceRootGuardWithOptions, h as writeHostFile, i as createSandboxedEditTool, l as wrapReadToolWithSkillContent, m as withMemoryWriteProvenance, n as createHostWorkspaceWriteTool, o as createSandboxedWriteTool, r as createOpenClawReadTool, t as createHostWorkspaceEditTool, u as wrapSandboxFileToolPath } from "./agent-tools.read-Bw32eea9.mjs";
import { r as resolveSandboxFileMutationQueueKey } from "./file-mutation-identity--w5QwGUN.mjs";
import { A as withFileMutationQueueKeysResolution, D as resolveFileMutationQueueKey, f as createReadTool, g as createLsTool, j as decodeUtf8File, k as withFileMutationQueueKeyResolution } from "./tools-5aKF_125.mjs";
import { n as withToolOperatorHint } from "./tool-operator-hint-CRgpH6bj.mjs";
import { t as createLazyExecTool } from "./lazy-exec-tool-D0byu9W5.mjs";
import { t as ProcessToolOutputSchema } from "./bash-tools.process-schema-DY_9dK9d.mjs";
import { r as resolveReadOnlyWorkspaceSkillMounts } from "./workspace-mounts-DN4AQsV5.mjs";
import { t as buildSandboxFsMounts } from "./fs-paths-CRatQHRY.mjs";
import fs from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
import { PATH_ALIAS_POLICIES } from "@openclaw/fs-safe/advanced";
import { Type } from "typebox";
import { normalizeToolParameterSchema } from "@openclaw/ai/internal/tool-schema";
//#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);
options.recordToolPrepStage?.("deferred-followup-descriptions");
return finalized;
}
//#endregion
//#region src/agents/apply-patch-policy.ts
function resolveConfiguredApplyPatchPolicy(params) {
const applyPatchWorkspaceOnly = params.workspaceOnly || (params.sessionPolicy?.applyPatchWorkspaceOnly ?? params.config?.workspaceOnly !== false);
const applyPatchContainmentSource = params.requireWorkspaceOnly ? "required-root" : params.sessionPolicy ? "session" : "config";
return {
applyPatchEnabled: !params.readOnly && params.config?.enabled !== false && isApplyPatchAllowedForModel({
modelProvider: params.modelProvider,
modelId: params.modelId,
allowModels: params.config?.allowModels
}),
applyPatchWorkspaceOnly,
applyPatchContainmentSource
};
}
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-containment-hint.ts
/**
* Operator-facing remediation for apply_patch workspace containment.
*
* Which control imposed the boundary decides which remedy is true, so the runtime owner
* names its own source rather than the tool guessing from configuration it cannot see.
*/
const HINTS = {
"required-root": "apply_patch is contained by this run's required workspace root. Full session permission mode and workspaceOnly configuration settings do not lift this boundary. Keep patch paths within the required root.",
config: "apply_patch is workspace-contained by configuration. Both tools.exec.applyPatch.workspaceOnly (default true) and tools.fs.workspaceOnly impose this independently, so clearing one can leave the other in force. tools.exec.mode does not affect it.",
session: "apply_patch is workspace-contained by this session's permission mode. Only a full permission mode lifts it; configuration settings do not override a session mode.",
worker: "apply_patch is workspace-contained by default on worker placements, which do not read tools.exec.applyPatch.workspaceOnly or tools.fs.workspaceOnly. Only an explicit full session permission mode lifts it here."
};
/**
* Name the control that contained this write, for the operator log only. Applies to host
* workspace-root rejections; sandbox bridges enforce their own mount boundary, report it in
* their own message, and are left untouched because host controls cannot lift a mount.
*/
function withApplyPatchContainmentHint(error, source) {
return source && isHostRootEscapeError(error) ? withToolOperatorHint(error, HINTS[source]) : error;
}
//#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) {
const assertCurrent = captureAgentToolSourceExecutionGuard(options.signal);
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) => {
assertCurrent();
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.");
assertCurrent();
return bridge.createFileExclusive({
filePath,
cwd: root,
data: content,
signal: options.signal
});
},
remove: (filePath) => {
assertCurrent();
return bridge.remove({
filePath,
cwd: root,
force: false,
signal: options.signal
});
},
mkdirp: (dir) => {
assertCurrent();
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 {
assertCurrent();
await fs$1.writeFile(filePath, content, {
encoding: "utf8",
flag: "wx"
});
return "created";
} catch (error) {
if (error.code === "EEXIST") return "exists";
throw error;
}
},
remove: (filePath) => {
assertCurrent();
return fs$1.rm(filePath);
},
mkdirp: async (dir) => {
assertCurrent();
await fs$1.mkdir(dir, { recursive: true });
}
}
});
const containmentRoot = options.root ?? options.cwd;
const root$1 = await root(containmentRoot, { assertBeforeMutation: assertCurrent });
const toRootPath = (filePath) => path.resolve(root$1.rootReal, toRelativeSandboxPath(root$1.rootDir, filePath));
return withPatchMemoryWriteProvenance({
observer: options.memoryWriteProvenance,
operations: {
readFile: async (filePath) => {
const opened = await openRootFile({
absolutePath: filePath,
rootPath: containmentRoot,
boundaryLabel: "workspace root",
symlinks: "follow-parents-within-root"
});
assertBoundaryRead(opened, filePath);
try {
return decodeUtf8File(fs.readFileSync(opened.fd), filePath);
} finally {
fs.closeSync(opened.fd);
}
},
writeFile: async (filePath, content) => {
assertCurrent();
await root$1.write(toRootPath(filePath), content, {
encoding: "utf8",
mutationSymlinks: "follow-parents-within-root"
}).catch(rethrowHostMutationError);
},
createFileExclusive: async (filePath, content) => {
try {
assertCurrent();
await root$1.create(toRootPath(filePath), content, {
encoding: "utf8",
mutationSymlinks: "reject"
});
return "created";
} catch (error) {
if (error instanceof FsSafeError && error.code === "already-exists") return "exists";
return rethrowHostMutationError(error);
}
},
remove: async (filePath) => {
assertCurrent();
await root$1.remove(`./${toRelativeSandboxPath(root$1.rootDir, filePath)}`).catch(rethrowHostMutationError);
}
}
});
}
function rethrowHostMutationError(error) {
if (error instanceof FsSafeError && (error.code === "outside-workspace" || error.code === "path-alias" && error.cause instanceof Error && /^(?:Path escapes|Path resolves outside|Symlink escapes) root \(/.test(error.cause.message))) markHostRootEscape(error);
throw error;
}
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;
if (opened.reason === "validation" && (opened.error instanceof FsSafeError && opened.error.code === "outside-workspace" || isPathBoundaryEscapeError(opened.error, "workspace root"))) markHostRootEscape(error);
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");
let result;
try {
result = await applyPatch(input, {
cwd,
root,
sandbox,
workspaceOnly,
memoryWriteProvenance: options.memoryWriteProvenance,
signal: executionSignal
});
} catch (error) {
throw withApplyPatchContainmentHint(error, workspaceOnly ? options.containmentSource : void 0);
}
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 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 ensureDir(moveTarget.resolved, fileOps);
if (moveTarget && moveTarget.queueKey !== target.queueKey) {
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);
recordSummary(summary, seen, "modified", 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 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) {
const legacyBridge = options.sandbox.bridge.pathMappings === void 0;
const workspaceMapping = resolveSandboxPathMapping(options.sandbox.workspaceMounts ?? [], resolved.containerPath);
if (!legacyBridge && !workspaceMapping) throw markHostRootEscape(/* @__PURE__ */ new Error(`Path escapes sandbox root (${options.sandbox.root}): ${filePath}`));
if (resolved.hostPath) {
const root = legacyBridge ? options.sandbox.root : workspaceMapping.mapping.hostRoot;
await assertSandboxPath({
filePath: resolved.hostPath,
cwd: root,
root,
allowFinalSymlinkForUnlink: aliasPolicy.allowFinalSymlinkForUnlink,
allowFinalHardlinkForUnlink: aliasPolicy.allowFinalHardlinkForUnlink
});
}
}
return {
resolved: 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 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 };
}
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 || 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-DeLzEt_o.mjs"));
/** 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,
outputSchema: ProcessToolOutputSchema,
execute: async (toolCallId, params, signal, onUpdate) => (await loadTool()).execute(toolCallId, params, signal, onUpdate)
};
}
//#endregion
//#region src/agents/core-coding-tools.ts
const filesystemAction = Object.freeze({
family: "data",
operation: "filesystem"
});
const processAction = Object.freeze({
family: "tool",
operation: "process"
});
function resolveSkillReadRoots(skills) {
const roots = /* @__PURE__ */ new Set();
for (const skill of skills ?? []) {
if (skill.fileHost === "workspace") continue;
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;
}
/** Route only selected workspace Skills; their paths never name Gateway files. */
function wrapWorkspaceSkillRead(localRead, skills, options) {
const remoteSkills = skills?.filter((skill) => skill.fileHost === "workspace") ?? [];
if (remoteSkills.length === 0) return localRead;
const access = getAgentWorkspaceAccess(options.skillsSnapshot?.skillRoots?.agentWorkspaceDir ?? options.codingRoot, "loadSkills");
const reader = access?.loadSkills ? access.skillResources : void 0;
return {
...localRead,
execute: async (toolCallId, params, signal, onUpdate) => {
const record = asOptionalObjectRecord(params);
const input = record?.path ?? record?.file_path;
const absolutePath = typeof input === "string" ? resolvePathFromInput(normalizeFileToolPathParam(input), options.codingRoot) : void 0;
const skill = absolutePath ? remoteSkills.find((candidate) => relativePathInsideSandboxRoot(candidate.baseDir, absolutePath) !== null) : void 0;
if (!skill || !absolutePath) return localRead.execute(toolCallId, params, signal, onUpdate);
if (!reader) throw new WorkspaceAccessUnavailableError("Remote workspace Skill resources are unavailable.");
const active = AbortSignal.any([signal, options.abortSignal].filter((value) => Boolean(value)));
const remoteRead = createReadTool(options.codingRoot, {
maxBytes: resolveAdaptiveReadMaxBytes(options),
modelBudget: resolveToolResultBudget(options.modelContextWindowTokens),
modelHasVision: options.modelHasVision,
operations: {
resolvePath: () => absolutePath,
resolveQueueKey: (filePath) => `workspace-skill:${filePath}`,
access: async () => active.throwIfAborted(),
readFile: async () => {
active.throwIfAborted();
if (absolutePath === skill.filePath) return Buffer.from(await reader.readInstructions(skill.filePath, { signal: active }));
const { prepareSkillBundle } = await import("./bundle-MushRAna.mjs");
const files = await reader.readSkillFiles(skill, { allowMissingRoot: false });
active.throwIfAborted();
const relative = relativePathInsideSandboxRoot(skill.baseDir, absolutePath);
const bundlePath = !skill.baseDir.startsWith("/") && path.win32.isAbsolute(skill.baseDir) ? relative.split("\\").join("/") : relative;
const file = files && prepareSkillBundle(files).files.find((entry) => entry.path === bundlePath);
if (!file) throw Object.assign(/* @__PURE__ */ new Error(`Skill file not found: ${absolutePath}`), { code: "ENOENT" });
return file.bytes;
}
}
});
return createOpenClawReadTool(remoteRead, {
modelContextWindowTokens: options.modelContextWindowTokens,
imageSanitization: options.imageSanitization,
cwd: options.codingRoot
}).execute(toolCallId, {
...record,
path: absolutePath
}, active, onUpdate);
}
};
}
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.shellTools !== "disabled")) throw new Error("Sandbox filesystem bridge is unavailable.");
const skillReadResources = options.skillReadResources ?? options.skillsSnapshot?.resolvedSkills;
const skillReadRoots = sandboxRoot ? void 0 : resolveSkillReadRoots(skillReadResources);
const attachmentReadRoot = !sandboxRoot ? options.attachmentReadRoot : void 0;
const hostReadRoots = [...skillReadRoots ?? [], ...attachmentReadRoot && fs.existsSync(attachmentReadRoot) ? [attachmentReadRoot] : []];
const needsReadOnlyWorkspaceSkillMounts = options.shellTools !== "disabled" || 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 sandboxFileMounts = sandbox && (options.includeBaseCodingTools && options.workspaceOnly || options.shellTools !== "disabled" && options.applyPatchEnabled && options.applyPatchWorkspaceOnly) ? sandboxFsBridge?.pathMappings ?? buildSandboxFsMounts(sandbox) : [];
const sandboxWorkspaceMounts = sandbox ? sandboxFileMounts.filter((mount) => relativePathInsideSandboxRoot(sandbox.containerWorkdir, mount.containerRoot) !== null) : [];
const sandboxReadMounts = sandboxFileMounts;
const base = [];
if (options.includeBaseCodingTools) {
const readDirectory = sandboxFsBridge?.readDirectory?.bind(sandboxFsBridge);
const listingOperations = readDirectory ? { readDirectory: (filePath, signal) => readDirectory({
filePath,
cwd: sandbox?.containerWorkdir,
signal
}) } : options.workspaceOnly && !sandbox ? { readDirectory: async (filePath) => {
return (await (await root(options.containmentRoot)).list(path.relative(options.containmentRoot, filePath), { withFileTypes: true })).map(({ name, isDirectory }) => ({
name,
isDirectory
}));
} } : void 0;
if (!sandbox || readDirectory) {
const ls = createLsTool(options.codingRoot, {
operations: listingOperations,
modelBudget: resolveToolResultBudget(options.modelContextWindowTokens)
});
const guardedLs = options.workspaceOnly ? wrapToolWorkspaceRootGuardWithOptions(ls, sandboxRoot ?? options.containmentRoot, sandboxRoot ? {
containerMounts: sandboxWorkspaceMounts,
containerWorkdir: sandbox.containerWorkdir,
bridge: sandboxFsBridge,
normalizeGuardedPathParams: true
} : {
resolutionCwd: options.codingRoot,
normalizeGuardedPathParams: true
}) : ls;
base.push(sandboxRoot ? wrapSandboxFileToolPath(guardedLs, {
root: sandboxRoot,
bridge: sandboxFsBridge,
defaultPath: "."
}) : guardedLs);
}
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 ? {
containerMounts: sandboxReadMounts,
containerWorkdir: sandbox.containerWorkdir,
bridge: sandboxFsBridge,
readPathValidation: "bridge"
} : {
additionalRoots: hostReadRoots.length > 0 ? hostReadRoots : void 0,
resolutionCwd: options.codingRoot,
normalizeGuardedPathParams: true
}) : read;
const wrapped = sandboxRoot ? guarded : createOpenClawReadTool(guarded, {
modelContextWindowTokens: options.modelContextWindowTokens,
imageSanitization: options.imageSanitization,
cwd: options.codingRoot
});
base.push(wrapReadToolWithSkillContent(sandboxRoot ? wrapped : wrapWorkspaceSkillRead(wrapped, skillReadResources, options), skillReadResources, {
modelContextWindowTokens: options.modelContextWindowTokens,
imageSanitization: options.imageSanitization,
cwd: options.codingRoot,
containerWorkdir: sandbox?.containerWorkdir,
instructionPaths: options.skillInstructionPaths,
instructionDeliveryCache: options.skillInstructionDeliveryCache
}));
if (!options.readOnly && !sandboxRoot) for (const createTool of [createHostWorkspaceEditTool, createHostWorkspaceWriteTool]) {
const tool = createTool(options.codingRoot, {
containmentRoot: options.containmentRoot,
workspaceOnly: options.workspaceOnly,
memoryWriteProvenance: options.memoryWriteProvenance,
abortSignal: options.abortSignal
});
base.push(options.workspaceOnly ? guardHostWorkspaceTool(tool, options) : tool);
}
}
if (options.includeBaseCodingTools && !options.readOnly && sandboxRoot && allowWorkspaceWrites) {
const toolOptions = {
root: sandboxRoot,
bridge: sandboxFsBridge,
memoryWriteProvenance: options.memoryWriteProvenance,
abortSignal: options.abortSignal
};
for (const tool of [createSandboxedEditTool(toolOptions), createSandboxedWriteTool(toolOptions)]) base.push(options.workspaceOnly ? wrapToolWorkspaceRootGuardWithOptions(tool, sandboxRoot, {
containerMounts: sandboxWorkspaceMounts,
containerWorkdir: sandbox.containerWorkdir,
bridge: sandboxFsBridge,
normalizeGuardedPathParams: true
}) : tool);
}
options.recordToolPrepStage?.("base-coding-tools");
const shell = [];
if (options.shellTools !== "disabled" && options.applyPatchEnabled && (!sandboxRoot || allowWorkspaceWrites)) shell.push(createApplyPatchTool({
cwd: options.codingRoot,
root: options.containmentRoot,
sandbox: sandboxRoot && allowWorkspaceWrites ? {
root: sandboxRoot,
bridge: sandboxFsBridge,
workspaceMounts: sandboxWorkspaceMounts
} : void 0,
workspaceOnly: options.applyPatchWorkspaceOnly,
containmentSource: options.applyPatchContainmentSource,
memoryWriteProvenance: options.memoryWriteProvenance,
abortSignal: options.abortSignal
}));
if (options.shellTools === "full") 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,
prepareProcessCleanup: sandbox.backend?.prepareProcessCleanup?.bind(sandbox.backend),
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, filesystemAction));
shell.forEach((tool) => bindAgentToolActionDescriptor(tool, processAction));
return [...base, ...shell];
}
//#endregion
export { finalizeAgentTools as i, isApplyPatchAllowedForModel as n, resolveConfiguredApplyPatchPolicy as r, createCoreCodingTools as t };