blob: 7b78e17059e8ac12369a6f7c95f18c1fd9139762 [file]
import { MAX_READ_IMAGE_BYTES, type BackendKind, type StorageRef } from '@maka/core';
import type { EffectiveProductToolSurface, MakaTool } from '@maka/runtime';
import {
assertProductBindingCatalogClean,
bashToolShellGuidance,
buildForegroundBashTool,
buildManagedBashTool,
buildParentAgentTools,
buildStopBackgroundTaskTool,
buildWriteStdinTool,
classifyRuntimeResourceRef,
computeEditedSource,
isSupportedImagePath,
projectEffectiveProductToolSurface,
validateImageBytes,
} from '@maka/runtime';
import { withFileWriteLock } from '@maka/runtime';
import { createHash } from 'node:crypto';
import { posix as pathPosix } from 'node:path';
import { z } from 'zod';
import type { HeavyTaskEvidenceRecorder } from './heavy-task-evidence.js';
import {
buildHeavyTaskProgressTools,
type HeavyTaskProgressRecorder,
} from './heavy-task-progress.js';
import {
buildHeavyTaskSelfCheckTools,
type HeavyTaskSelfCheckRecorder,
} from './heavy-task-self-check.js';
import type { IsolatedToolExecutor } from './isolation.js';
import {
buildTaskLedgerExperimentTools,
type TaskLedgerExperimentStore,
} from './task-ledger-experiment.js';
export interface BuildIsolatedHeadlessToolsOptions {
agentTools?: boolean;
heavyTaskEvidence?: HeavyTaskEvidenceRecorder;
heavyTaskProgress?: HeavyTaskProgressRecorder;
heavyTaskSelfCheck?: HeavyTaskSelfCheckRecorder;
taskLedgerExperiment?: {
store: TaskLedgerExperimentStore;
};
snapshotImage?: (input: {
sessionId: string;
turnId: string;
name: string;
bytes: Uint8Array;
mimeType: string;
}) => Promise<Extract<StorageRef, { kind: 'session_file' }>>;
}
// Key Write and Edit on a JSON [cwd, path] pair (JSON.stringify so no path
// character can pose as the separator) and serialize them with the shared
// withFileWriteLock — see its definition for why concurrent writes are serialized
// and which aliases a lexical key cannot merge. The path is lexically normalized
// so spellings of one file ("a.txt", "./a.txt", "d//a.txt") share a key. Keying
// stays lexical here by necessity: the executor boundary hides the filesystem
// (which may be remote, with its own symlink / hard-link / case-fold semantics),
// so the executor — not this layer — owns canonicalization.
const fileWriteKey = (cwd: string, normalizedPath: string) =>
JSON.stringify([pathPosix.normalize(cwd), pathPosix.normalize(normalizedPath)]);
const EDIT_READ_FRAME_END = 'MAKA_EDIT_BYTES_END';
const EDIT_READ_FRAME_HEADER_PATTERN =
/^MAKA_EDIT_BYTES_V1 length=(0|[1-9]\d*) sha256=([a-f0-9]{64})$/;
const FILE_TOOL_OUTPUT_FRAME_END = 'MAKA_FILE_TOOL_OUTPUT_END';
const FILE_TOOL_OUTPUT_FRAME_HEADER_PATTERN =
/^MAKA_FILE_TOOL_OUTPUT_V1 tool=([A-Za-z]+) nonce=(0|[1-9]\d*)$/;
const FILE_TOOL_OUTPUT_STATUS_PREFIX = '\0MAKA_FILE_TOOL_STATUS_';
const CANONICAL_BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
type FramedFileToolName = 'Read' | 'Glob' | 'Grep';
// Base64 expands the decoded tool payload by 4/3 before the local executor sees
// it. Keep the framed transport budget above the existing 10 MiB decoded-output
// capacity, with room for the embedded status marker and frame lines.
const FRAMED_FILE_TOOL_MAX_DECODED_BYTES = 10 * 1024 * 1024;
export const FRAMED_FILE_TOOL_MAX_TRANSPORT_BYTES = 16 * 1024 * 1024;
/**
* Build Maka's standard headless tool surface with shell and file operations
* routed through the isolated executor boundary.
*/
export function buildIsolatedHeadlessTools(
executor: IsolatedToolExecutor,
options: BuildIsolatedHeadlessToolsOptions = {},
): MakaTool[] {
const productSurface = buildIsolatedHeadlessProductToolSurface(executor, options);
return [...productSurface.tools, ...buildIsolatedHeadlessSupplementalTools(options)];
}
export function buildIsolatedHeadlessProductToolSurface(
executor: IsolatedToolExecutor,
options: Pick<
BuildIsolatedHeadlessToolsOptions,
'agentTools' | 'heavyTaskEvidence' | 'snapshotImage'
> = {},
): EffectiveProductToolSurface {
const productTools = [
buildIsolatedBashTool(executor, options),
// Bound together with the managed Bash that produces their refs: a model
// told to run something in the background must be able to observe, write
// to, and stop it, or it is better off never starting it.
...(executor.shellSessions
? [
buildStopBackgroundTaskTool(executor.shellSessions),
buildWriteStdinTool(executor.shellSessions),
]
: []),
buildIsolatedReadTool(executor, options),
buildIsolatedWriteTool(executor, options),
buildIsolatedEditTool(executor, options),
buildIsolatedGlobTool(executor, options),
buildIsolatedGrepTool(executor, options),
...buildParentAgentTools(),
];
assertProductBindingCatalogClean(
'headless',
productTools.map((tool) => tool.name),
);
return projectEffectiveProductToolSurface({
host: 'headless',
tools: productTools,
policy: {
economy: true,
...(options.agentTools ? {} : { disabledSurfaceIds: ['agent'] }),
},
});
}
export function headlessBackendBindsMakaProductTools(backend: BackendKind): boolean {
return backend === 'ai-sdk';
}
export function buildHeadlessProductToolSurfaceForBackend(
backend: BackendKind,
executor: IsolatedToolExecutor | undefined,
options: Pick<
BuildIsolatedHeadlessToolsOptions,
'agentTools' | 'heavyTaskEvidence' | 'snapshotImage'
> = {},
): EffectiveProductToolSurface | undefined {
if (!headlessBackendBindsMakaProductTools(backend) || !executor) return undefined;
return buildIsolatedHeadlessProductToolSurface(executor, options);
}
export function buildIsolatedHeadlessSupplementalTools(
options: Pick<
BuildIsolatedHeadlessToolsOptions,
'heavyTaskProgress' | 'heavyTaskSelfCheck' | 'taskLedgerExperiment'
> = {},
): MakaTool[] {
const tools: MakaTool[] = [];
if (options.heavyTaskProgress) {
tools.push(...buildHeavyTaskProgressTools(options.heavyTaskProgress));
}
if (options.heavyTaskSelfCheck) {
tools.push(...buildHeavyTaskSelfCheckTools(options.heavyTaskSelfCheck));
}
if (options.taskLedgerExperiment) {
tools.push(...buildTaskLedgerExperimentTools(options.taskLedgerExperiment));
}
return tools;
}
const ISOLATED_BASH_LEAD =
'Run a shell command in the isolated headless task workspace. ' +
'Use it for inspection, builds, and task-local generation; prefer Read/Grep/Write/Edit for exact file operations and preserve required deliverables.';
export function buildIsolatedBashTool(
executor: IsolatedToolExecutor,
options: Pick<BuildIsolatedHeadlessToolsOptions, 'heavyTaskEvidence'> = {},
): MakaTool {
const sessions = executor.shellSessions;
if (sessions) {
return buildManagedBashTool(sessions, {
lead: ISOLATED_BASH_LEAD,
...(executor.shell ? { shell: executor.shell } : {}),
// No in-process sandbox manager exists behind an external isolation
// boundary, so there is no authority for the model to declare.
declareSandboxBoundary: false,
// Same resolver as the unmanaged path: a command must not be killed at a
// different time depending on which isolation mode ran it.
defaultTimeoutMs: (command) =>
cleanupCommandTimeoutMs(command) ?? sessions.defaultCommandTimeoutMs,
// The only reason this hook exists: ShellRunProcessManager falls back to
// the headless process's own env when a launch carries none, and in a
// Harbor cell that env holds the provider credentials. The executor's
// stripped env is the same one `exec` uses.
transformCommand: ({ ctx }) => ({ cwd: ctx.cwd, env: sessions.commandEnv }),
...(options.heavyTaskEvidence
? {
afterResult: async (input, result, ctx) => {
// Only a foreground result carries a completed command's output,
// which is what the Bash evidence contract describes. A background
// launch has produced none yet, and reading its ref later yields a
// live task's screen — neither a file read nor a finished command —
// so nothing is recorded for it on either call.
if (result.kind !== 'terminal' || result.output.mode !== 'pipes') return;
await options.heavyTaskEvidence?.recordToolEvidence(
{
name: 'Bash',
input: {
command: input.command,
cwd: input.cwd,
...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
},
result: {
exitCode: result.exitCode ?? 1,
stdout: result.output.stdout,
stderr: result.output.stderr,
stdoutTruncated: result.output.stdoutTruncated,
stderrTruncated: result.output.stderrTruncated,
...(result.status === 'timed_out' ? { timedOut: true } : {}),
},
},
ctx,
);
},
}
: {}),
});
}
const guidance = executor.shell ? bashToolShellGuidance(executor.shell) : '';
return buildForegroundBashTool({
description: ISOLATED_BASH_LEAD + (guidance ? ` ${guidance}` : ''),
defaultTimeoutMs: cleanupCommandTimeoutMs,
emitReturnedOutput: true,
execute: async ({ command, cwd, timeoutMs, ctx }) => {
// boundedTail: Bash is the one caller that wants a recoverable tail of a
// huge, never-killed output. Read/Glob/Grep deliberately omit it so they
// get full, head-first content from the executor.
return await executor.exec(
{
command,
cwd,
...(timeoutMs !== undefined ? { timeoutMs } : {}),
boundedTail: true,
},
{ abortSignal: ctx.abortSignal },
);
},
afterResult: async (input, result, ctx) => {
await options.heavyTaskEvidence?.recordToolEvidence({ name: 'Bash', input, result }, ctx);
},
});
}
function cleanupCommandTimeoutMs(command: string): number | undefined {
// Existing Harbor cleanup can exceed the default in large gcov tasks. Keep the
// match exact so only the known generated cleanup command gets the allowance.
return command === 'rm -f *.gcda *.gcno *.gcov' ? 120_000 : undefined;
}
export function buildIsolatedReadTool(
executor: IsolatedToolExecutor,
options: Pick<BuildIsolatedHeadlessToolsOptions, 'heavyTaskEvidence' | 'snapshotImage'> = {},
): MakaTool {
const sessions = executor.shellSessions;
const offsetField = z.number().int().nonnegative().optional();
const limitField = z.number().int().positive().optional();
return {
name: 'Read',
description:
'Read a text file or view a PNG, JPEG, GIF, or WebP image from the isolated headless task workspace. Use Read on screenshots when visual inspection is needed.' +
(sessions
? ' Read a background task with ref instead of path to see its current output or terminal screen.'
: ''),
// The ref field only appears when something can return a ref. Without
// managed sessions the schema stays byte-for-byte what it was.
parameters: sessions
? z
.object({
path: z.string().optional(),
ref: z
.string()
.optional()
.describe('A background task ref returned by Bash; provide it instead of path'),
offset: offsetField,
limit: limitField,
})
.superRefine((value, ctx) => {
if ((value.path === undefined) === (value.ref === undefined)) {
ctx.addIssue({
code: 'custom',
path: ['path'],
message: 'Provide exactly one of path or ref',
});
}
})
: z.object({ path: z.string(), offset: offsetField, limit: limitField }),
impl: async (rawInput, ctx) => {
const { offset, limit } = rawInput;
const ref = 'ref' in rawInput ? rawInput.ref : undefined;
if (ref !== undefined) {
if (!sessions) throw new Error('Runtime resources are not available in this toolset');
if (classifyRuntimeResourceRef(ref) !== 'runtime') {
throw new Error(`Unsupported runtime resource ref: ${ref}`);
}
// Not recorded as heavy-task evidence: that contract describes a file
// read (cwd + path + content), and a live task's screen is neither.
return await sessions.readRuntimeResource(ctx.sessionId, ref, ctx.abortSignal);
}
const { cwd } = ctx;
if (rawInput.path === undefined) throw new Error('Read requires path or ref');
const path = rawInput.path;
const normalizedPath = normalizeWorkspacePath(path, cwd, 'Read path');
const input = { cwd, path: normalizedPath, offset, limit };
if (isSupportedImagePath(normalizedPath)) {
if (!options.snapshotImage) {
throw new Error('Read image snapshots are not available in this toolset.');
}
const bytes = await readIsolatedFileBytes(
executor,
cwd,
normalizedPath,
ctx.abortSignal,
READ_IMAGE_BYTES_SCRIPT,
'Read image',
);
const image = validateImageBytes(bytes);
const ref = await options.snapshotImage({
sessionId: ctx.sessionId,
turnId: ctx.turnId,
name: pathPosix.basename(normalizedPath),
bytes: image.bytes,
mimeType: image.mimeType,
});
const result = { kind: 'image' as const, mimeType: image.mimeType, ref };
await options.heavyTaskEvidence?.recordToolEvidence({ name: 'Read', input, result }, ctx);
return result;
}
// Read has no native fast path: every result must go through READ_SCRIPT so
// it carries the line-number / line+byte-cap / binary-guard contract (#92).
const stdout = await execFramedFileCommand(
executor,
cwd,
'Read',
READ_SCRIPT,
[normalizedPath, numberArg(offset), numberArg(limit)],
ctx.abortSignal,
);
const result = { content: stdout };
await options.heavyTaskEvidence?.recordToolEvidence({ name: 'Read', input, result }, ctx);
return result;
},
};
}
export function buildIsolatedWriteTool(
executor: IsolatedToolExecutor,
options: Pick<BuildIsolatedHeadlessToolsOptions, 'heavyTaskEvidence'> = {},
): MakaTool {
return {
name: 'Write',
description:
'Write content to a file in the isolated headless task workspace using a path ' +
'relative to the session cwd. Absolute paths and parent traversal are rejected.',
parameters: z.object({
path: z
.string()
.describe(
'Path relative to the session cwd. Absolute paths and parent traversal are rejected.',
),
content: z.string(),
}),
impl: async ({ path, content }, ctx) => {
const { cwd } = ctx;
const normalizedPath = normalizeWorkspacePath(path, cwd, 'Write path');
const input = { cwd, path: normalizedPath, content };
return await withFileWriteLock(fileWriteKey(cwd, normalizedPath), async () => {
if (executor.writeFile) {
const result = await executor.writeFile(input, { abortSignal: ctx.abortSignal });
await options.heavyTaskEvidence?.recordToolEvidence(
{ name: 'Write', input, result },
ctx,
);
return result;
}
await execFileCommand(
executor,
cwd,
shellFileCommand(WRITE_SCRIPT, [normalizedPath, content]),
ctx.abortSignal,
);
const result = {
ok: true,
path: normalizedPath,
bytes: Buffer.byteLength(content, 'utf8'),
};
await options.heavyTaskEvidence?.recordToolEvidence({ name: 'Write', input, result }, ctx);
return result;
});
},
};
}
export function buildIsolatedEditTool(
executor: IsolatedToolExecutor,
options: Pick<BuildIsolatedHeadlessToolsOptions, 'heavyTaskEvidence'> = {},
): MakaTool {
return {
name: 'Edit',
description:
'Replace old_string with new_string in a file in the isolated headless task workspace. ' +
'Prefers an exact, unique match; if exact fails it tolerates limited whitespace/indentation/escape ' +
'drift in old_string, but only when the match is unambiguous (otherwise it errors — re-read and retry ' +
'with exact text). new_string is written verbatim, so provide the exact final text/indentation you want. ' +
'Errors if old_string is not found or not unique.',
parameters: z.object({
path: z.string(),
old_string: z.string(),
new_string: z.string(),
}),
impl: async ({ path, old_string, new_string }, ctx) => {
const { cwd } = ctx;
const normalizedPath = normalizeWorkspacePath(path, cwd, 'Edit path');
const input = { cwd, path: normalizedPath, oldString: old_string, newString: new_string };
return await withFileWriteLock(fileWriteKey(cwd, normalizedPath), async () => {
const raw = await readIsolatedFileBytes(executor, cwd, normalizedPath, ctx.abortSignal);
const { content, ...metadata } = computeEditBytes(
raw,
old_string,
new_string,
normalizedPath,
);
await writeIsolatedFileBytes(executor, cwd, normalizedPath, content, ctx.abortSignal);
const stored = await readIsolatedFileBytes(executor, cwd, normalizedPath, ctx.abortSignal);
if (!stored.equals(content)) {
throw new Error(
`Edit post-write verification failed for ${normalizedPath}: stored bytes differ from the replacement result`,
);
}
const result = { ok: true, path: normalizedPath, replacements: 1, ...metadata };
await options.heavyTaskEvidence?.recordToolEvidence({ name: 'Edit', input, result }, ctx);
return result;
});
},
};
}
export function buildIsolatedGlobTool(
executor: IsolatedToolExecutor,
options: Pick<BuildIsolatedHeadlessToolsOptions, 'heavyTaskEvidence'> = {},
): MakaTool {
return {
name: 'Glob',
description: 'Find files in the isolated headless task workspace matching a glob pattern.',
parameters: z.object({
pattern: z
.string()
.describe(
'Relative glob pattern without an absolute path or "..", for example "**/*.txt".',
),
cwd: z
.string()
.optional()
.describe(
'Optional search directory inside the session cwd. Absolute or relative directory paths are accepted.',
),
}),
impl: async ({ pattern, cwd: relCwd }, ctx) => {
const { cwd } = ctx;
const normalizedPattern = normalizeWorkspaceGlobPattern(pattern, cwd, 'Glob pattern');
const normalizedRelCwd =
relCwd === undefined ? undefined : normalizeWorkspacePath(relCwd, cwd, 'Glob cwd');
const input = { cwd, pattern: normalizedPattern, searchCwd: normalizedRelCwd };
if (executor.globFiles) {
const result = await executor.globFiles(input, { abortSignal: ctx.abortSignal });
await options.heavyTaskEvidence?.recordToolEvidence({ name: 'Glob', input, result }, ctx);
return result;
}
const stdout = await execFramedFileCommand(
executor,
cwd,
'Glob',
GLOB_SCRIPT,
[normalizedPattern, globPatternToEre(normalizedPattern), normalizedRelCwd ?? ''],
ctx.abortSignal,
);
const result = { files: parseLineArray(stdout) };
await options.heavyTaskEvidence?.recordToolEvidence({ name: 'Glob', input, result }, ctx);
return result;
},
};
}
export function buildIsolatedGrepTool(
executor: IsolatedToolExecutor,
options: Pick<BuildIsolatedHeadlessToolsOptions, 'heavyTaskEvidence'> = {},
): MakaTool {
return {
name: 'Grep',
description: 'Search file contents with a regex in the isolated headless task workspace.',
parameters: z.object({
pattern: z.string(),
path: z.string().optional(),
glob: z.string().optional(),
}),
impl: async ({ pattern, path, glob }, ctx) => {
const { cwd } = ctx;
const normalizedPath =
path === undefined ? undefined : normalizeWorkspacePath(path, cwd, 'Grep path');
const normalizedGlob =
glob === undefined ? undefined : normalizeWorkspaceGlobPattern(glob, cwd, 'Grep glob');
const input = {
cwd,
pattern,
path: normalizedPath,
glob: normalizedGlob,
};
if (executor.grepFiles) {
const result = await executor.grepFiles(input, { abortSignal: ctx.abortSignal });
await options.heavyTaskEvidence?.recordToolEvidence({ name: 'Grep', input, result }, ctx);
return result;
}
const stdout = await execFramedFileCommand(
executor,
cwd,
'Grep',
GREP_SCRIPT,
[
pattern,
normalizedPath ?? '',
normalizedGlob ?? '',
normalizedGlob === undefined ? '' : globPatternToEre(normalizedGlob),
],
ctx.abortSignal,
);
const result = { matches: parseLineArray(stdout) };
await options.heavyTaskEvidence?.recordToolEvidence({ name: 'Grep', input, result }, ctx);
return result;
},
};
}
async function execFileCommand(
executor: IsolatedToolExecutor,
cwd: string,
command: string,
abortSignal: AbortSignal,
): Promise<string> {
const result = await executor.exec({ command, cwd, timeoutMs: 120_000 }, { abortSignal });
if (result.exitCode !== 0) {
throw new Error(
result.stderr.trim() || `isolated file command failed with exit code ${result.exitCode}`,
);
}
return result.stdout;
}
async function execFramedFileCommand(
executor: IsolatedToolExecutor,
cwd: string,
toolName: FramedFileToolName,
script: string,
args: string[],
abortSignal: AbortSignal,
): Promise<string> {
const result = await executor.exec(
{
command: framedShellFileCommand(toolName, script, args),
cwd,
timeoutMs: 120_000,
},
{ abortSignal },
);
if (result.exitCode !== 0) {
throw new Error(
result.stderr.trim() || `isolated file command failed with exit code ${result.exitCode}`,
);
}
const frame = parseFileToolOutputFrame(result.stdout, toolName);
if (frame.exitCode !== 0) {
throw new Error(
result.stderr.trim() || `isolated file command failed with exit code ${frame.exitCode}`,
);
}
return frame.content;
}
async function readIsolatedFileBytes(
executor: IsolatedToolExecutor,
cwd: string,
path: string,
abortSignal: AbortSignal,
script = EDIT_READ_BYTES_SCRIPT,
integrityLabel = 'Edit read',
): Promise<Buffer> {
const stdout = await execFileCommand(
executor,
cwd,
shellFileCommand(script, [path]),
abortSignal,
);
return parseEditReadFrame(stdout, integrityLabel);
}
function parseEditReadFrame(stdout: string, integrityLabel = 'Edit read'): Buffer {
const lines = stdout.split('\n');
if (lines.length !== 4 || lines[3] !== '' || lines[2] !== EDIT_READ_FRAME_END) {
throw editReadIntegrityError(
'expected exactly one complete frame with no surrounding output',
integrityLabel,
);
}
const header = lines[0]?.match(EDIT_READ_FRAME_HEADER_PATTERN);
if (!header) throw editReadIntegrityError('frame header is missing or malformed', integrityLabel);
const expectedLength = Number(header[1]);
if (!Number.isSafeInteger(expectedLength))
throw editReadIntegrityError('declared byte length is not safe', integrityLabel);
const payload = lines[1] ?? '';
if (!CANONICAL_BASE64_PATTERN.test(payload)) {
throw editReadIntegrityError('payload is not canonical Base64', integrityLabel);
}
const decoded = Buffer.from(payload, 'base64');
if (decoded.toString('base64') !== payload) {
throw editReadIntegrityError('payload is not canonical Base64', integrityLabel);
}
if (decoded.length !== expectedLength) {
throw editReadIntegrityError(
`declared ${expectedLength} bytes but decoded ${decoded.length}`,
integrityLabel,
);
}
const actualDigest = createHash('sha256').update(decoded).digest('hex');
if (actualDigest !== header[2])
throw editReadIntegrityError('SHA-256 digest mismatch', integrityLabel);
return decoded;
}
function editReadIntegrityError(reason: string, label: string): Error {
return new Error(`${label} transport integrity check failed: ${reason}`);
}
function parseFileToolOutputFrame(
stdout: string,
expectedTool: FramedFileToolName,
): { content: string; exitCode: number } {
const lines = stdout.split('\n');
const fail = (reason: string): never => {
throw new Error(`${expectedTool} output transport integrity check failed: ${reason}`);
};
if (lines.length < 4 || lines.at(-1) !== '' || lines.at(-2) !== FILE_TOOL_OUTPUT_FRAME_END) {
return fail('expected exactly one complete frame with no surrounding output');
}
const header = lines[0]?.match(FILE_TOOL_OUTPUT_FRAME_HEADER_PATTERN);
if (!header) return fail('frame header is missing or malformed');
if (header[1] !== expectedTool)
return fail(`expected ${expectedTool} frame but received ${header[1]}`);
const payload = lines.slice(1, -2).join('');
const decoded = Buffer.from(payload, 'base64');
if (decoded.toString('base64') !== payload) return fail('payload is not canonical Base64');
const statusMarker = Buffer.from(`${FILE_TOOL_OUTPUT_STATUS_PREFIX}${header[2]}=`, 'ascii');
const markerIndex = decoded.lastIndexOf(statusMarker);
if (markerIndex < 0) return fail('inner command status is missing');
if (markerIndex > FRAMED_FILE_TOOL_MAX_DECODED_BYTES) {
return fail(`decoded payload exceeds ${FRAMED_FILE_TOOL_MAX_DECODED_BYTES} bytes`);
}
const status = decoded.subarray(markerIndex + statusMarker.length).toString('ascii');
if (!/^(0|[1-9]\d*)$/.test(status)) return fail('inner command status is malformed');
const exitCode = Number(status);
if (!Number.isSafeInteger(exitCode)) return fail('inner command status is not safe');
return { content: decoded.subarray(0, markerIndex).toString('utf8'), exitCode };
}
async function writeIsolatedFileBytes(
executor: IsolatedToolExecutor,
cwd: string,
path: string,
content: Buffer,
abortSignal: AbortSignal,
): Promise<void> {
await execFileCommand(
executor,
cwd,
shellFileCommand(EDIT_WRITE_BYTES_SCRIPT, [path, content.toString('base64')]),
abortSignal,
);
}
function computeEditBytes(
raw: Buffer,
oldString: string,
newString: string,
inputPath: string,
): { content: Buffer; matchedVia: string; startLine: number; endLine: number } {
const source = raw.toString('utf8');
if (Buffer.compare(Buffer.from(source, 'utf8'), raw) === 0) {
const result = computeEditedSource(source, oldString, newString, inputPath);
return {
content: Buffer.from(result.content, 'utf8'),
matchedVia: result.matchedVia,
startLine: result.startLine,
endLine: result.endLine,
};
}
const oldBuf = Buffer.from(oldString, 'utf8');
const newBuf = Buffer.from(newString, 'utf8');
if (oldBuf.length === 0) throw new Error(`old_string must not be empty in ${inputPath}`);
const first = raw.indexOf(oldBuf);
if (first === -1) {
throw new Error(
`Refusing a non-exact match in ${inputPath}: the file is not valid UTF-8 (looks binary). Re-read it and pass the exact bytes to replace.`,
);
}
if (raw.indexOf(oldBuf, first + oldBuf.length) !== -1) {
throw new Error(
`old_string is not unique in ${inputPath} (binary file: the exact bytes match more than once)`,
);
}
const startLine = countNewlines(raw, first) + 1;
const endsWithNewline = oldBuf[oldBuf.length - 1] === 10;
const spanLineCount = Math.max(
countNewlines(oldBuf, oldBuf.length) + 1 - (endsWithNewline ? 1 : 0),
1,
);
return {
content: Buffer.concat([raw.slice(0, first), newBuf, raw.slice(first + oldBuf.length)]),
matchedVia: 'exact',
startLine,
endLine: startLine + spanLineCount - 1,
};
}
function countNewlines(buffer: Buffer, end: number): number {
let count = 0;
for (let i = 0; i < end; i += 1) {
if (buffer[i] === 10) count += 1;
}
return count;
}
function shellFileCommand(script: string, args: string[]): string {
return ['sh', '-c', shellQuote(script), '--', ...args.map(shellQuote)].join(' ');
}
function framedShellFileCommand(
toolName: FramedFileToolName,
script: string,
args: string[],
): string {
return shellFileCommand(FRAME_FILE_TOOL_OUTPUT_SCRIPT, [
toolName,
shellFileCommand(script, args),
]);
}
function shellQuote(value: string): string {
return `'${value.replaceAll("'", "'\\''")}'`;
}
function numberArg(value: number | undefined): string {
return value === undefined ? '' : String(value);
}
function parseLineArray(stdout: string): string[] {
if (!stdout) return [];
return stdout
.replace(/\n$/, '')
.split('\n')
.filter((line) => line.length > 0);
}
function globPatternToEre(pattern: string): string {
let output = '^';
for (let i = 0; i < pattern.length; i += 1) {
const ch = pattern[i];
const next = pattern[i + 1];
if (ch === '*' && next === '*') {
output += '.*';
i += 1;
} else if (ch === '*') {
output += '[^/]*';
} else if (ch === '?') {
output += '[^/]';
} else {
output += escapeEreChar(ch);
}
}
return `${output}$`;
}
function escapeEreChar(ch: string): string {
return /[\\.^$+{}()[\]|]/.test(ch) ? `\\${ch}` : ch;
}
function normalizeWorkspacePath(inputPath: string, cwd: string, label: string): string {
assertNoDriveOrParentSegment(inputPath, label);
if (inputPath.startsWith('/')) {
return assertNormalizedRelativePath(
pathPosix.relative(normalizeWorkspaceRoot(cwd), pathPosix.normalize(inputPath)) || '.',
label,
);
}
return assertNormalizedRelativePath(inputPath, label);
}
function normalizeWorkspaceGlobPattern(pattern: string, cwd: string, label: string): string {
assertNoDriveOrParentSegment(pattern, label);
if (!pattern.startsWith('/')) return assertNormalizedRelativePath(pattern, label);
return assertNormalizedRelativePath(
pathPosix.relative(normalizeWorkspaceRoot(cwd), pattern) || '.',
label,
);
}
function normalizeWorkspaceRoot(cwd: string): string {
return pathPosix.normalize(cwd);
}
function assertNoDriveOrParentSegment(inputPath: string, label: string): void {
if (
inputPath.length === 0 ||
/^[A-Za-z]:[\\/]/.test(inputPath) ||
inputPath.split(/[\\/]+/).includes('..')
) {
throw new Error(`${label} must stay inside the isolated workspace`);
}
}
function assertNormalizedRelativePath(inputPath: string, label: string): string {
if (
inputPath.length === 0 ||
inputPath.startsWith('/') ||
/^[A-Za-z]:[\\/]/.test(inputPath) ||
inputPath.split(/[\\/]+/).includes('..')
) {
throw new Error(`${label} must stay inside the isolated workspace`);
}
return inputPath;
}
const COMMON_SHELL_HELPERS = String.raw`
fail() {
printf '%s\n' "$1" >&2
exit 1
}
inside_workspace() {
case "$root" in
/)
case "$1" in /*) return 0 ;; esac
;;
*)
case "$1" in "$root"|"$root"/*) return 0 ;; esac
;;
esac
return 1
}
existing_target() {
input_path=$1
label=$2
target=$root/$input_path
[ -L "$target" ] && fail "$label must stay inside workspace"
[ -e "$target" ] || fail "$label does not exist: $input_path"
if [ -d "$target" ]; then
real=$(cd -P "$target" 2>/dev/null && pwd -P) || fail "$label must stay inside workspace"
else
parent=$(dirname "$target")
base=$(basename "$target")
parent_real=$(cd -P "$parent" 2>/dev/null && pwd -P) || fail "$label must stay inside workspace"
real=$parent_real/$base
fi
inside_workspace "$real" || fail "$label must stay inside workspace"
printf '%s\n' "$real"
}
writable_target() {
input_path=$1
label=$2
target=$root/$input_path
parent=$(dirname "$target")
base=$(basename "$target")
parent_real=$(cd -P "$parent" 2>/dev/null && pwd -P) || fail "$label must stay inside workspace"
inside_workspace "$parent_real" || fail "$label must stay inside workspace"
real=$parent_real/$base
[ -L "$real" ] && fail "$label must stay inside workspace"
printf '%s\n' "$real"
}
`;
const FRAME_FILE_TOOL_OUTPUT_SCRIPT = String.raw`
tool=$1
command=$2
nonce=$$
printf 'MAKA_FILE_TOOL_OUTPUT_V1 tool=%s nonce=%s\n' "$tool" "$nonce"
{
sh -c "$command"
status=$?
printf '\000MAKA_FILE_TOOL_STATUS_%s=%s' "$nonce" "$status"
} | base64
encoded_status=$?
[ "$encoded_status" -eq 0 ] || exit "$encoded_status"
printf 'MAKA_FILE_TOOL_OUTPUT_END\n'
`;
const READ_SCRIPT = `${COMMON_SHELL_HELPERS}
root=$(pwd -P) || exit 1
target=$(existing_target "$1" 'Read path') || exit 1
offset=\${2:-0}
limit=\${3:-2000}
# Binary guard: a NUL byte in the head means this is not text. Dumping it would
# flood the model with garbage (and command substitution strips NULs anyway), so
# report it instead. Matches Claude Code / opencode behavior. LC_ALL=C is
# mandatory: in a UTF-8 locale, BSD/macOS tr aborts on an invalid high byte
# ("Illegal byte sequence") and, with no pipefail, the count silently becomes 0,
# letting a binary file with a high byte before its first NUL slip through.
nul=$(head -c 8192 "$target" | LC_ALL=C tr -dc '\\000' | wc -c | tr -d ' ')
if [ "\${nul:-0}" -gt 0 ]; then
size=$(wc -c < "$target" | tr -d ' ')
printf '[binary file: %s bytes, contents omitted]' "$size"
exit 0
fi
# cat -n style: each line carries its absolute 1-based number; over-long lines are
# clipped at maxcol bytes. Output stops at the line cap ("limit" lines from
# "offset") OR a ~50KB byte budget, whichever comes first — both keep a large file
# from flooding the model's context (#92) — with a hint giving the offset to resume.
# LC_ALL=C so awk treats the file as bytes and never aborts on invalid UTF-8 in a
# non-NUL file; line content is byte-for-byte and length()/maxcol/maxbytes count bytes.
LC_ALL=C awk -v start="$offset" -v limit="$limit" '
BEGIN { first = start + 1; last = start + limit; maxcol = 2000; maxbytes = 51200 }
NR < first { next }
{
if (NR > last) { stopped = 1; exit }
line = $0
if (length(line) > maxcol) line = substr(line, 1, maxcol) "... [line truncated]"
out = sprintf("%6d\\t%s\\n", NR, line)
# Always emit at least one line (shown > 0 guard); otherwise stop before the
# budget is exceeded so total output stays under maxbytes.
if (shown > 0 && bytes + length(out) > maxbytes) { stopped = 1; exit }
printf "%s", out
bytes += length(out)
shown += 1
lastline = NR
}
END { if (stopped) printf "... (truncated at line %d; pass offset=%d to read more)\\n", lastline, lastline }
' "$target"
`;
const WRITE_SCRIPT = `${COMMON_SHELL_HELPERS}
root=$(pwd -P) || exit 1
target=$(writable_target "$1" 'Write path') || exit 1
printf '%s' "$2" > "$target"
`;
const EDIT_READ_BYTES_SCRIPT = `${COMMON_SHELL_HELPERS}
root=$(pwd -P) || exit 1
target=$(existing_target "$1" 'Edit path') || exit 1
size=$(wc -c < "$target" | tr -d '[:space:]') || exit 1
if command -v sha256sum >/dev/null 2>&1; then
digest=$(sha256sum "$target" | awk '{print $1}') || exit 1
elif command -v shasum >/dev/null 2>&1; then
digest=$(shasum -a 256 "$target" | awk '{print $1}') || exit 1
else
fail 'SHA-256 utility is required for Edit transport'
fi
printf 'MAKA_EDIT_BYTES_V1 length=%s sha256=%s\n' "$size" "$digest"
base64 < "$target" | LC_ALL=C tr -d '\\r\\n'
printf '\nMAKA_EDIT_BYTES_END\n'
`;
const READ_IMAGE_BYTES_SCRIPT = `${COMMON_SHELL_HELPERS}
root=$(pwd -P) || exit 1
target=$(existing_target "$1" 'Read path') || exit 1
size=$(wc -c < "$target" | tr -d '[:space:]') || exit 1
[ "$size" -le ${MAX_READ_IMAGE_BYTES} ] || fail 'Image exceeds the 5MB model input limit; downscale it and try again.'
if command -v sha256sum >/dev/null 2>&1; then
digest=$(sha256sum "$target" | awk '{print $1}') || exit 1
elif command -v shasum >/dev/null 2>&1; then
digest=$(shasum -a 256 "$target" | awk '{print $1}') || exit 1
else
fail 'SHA-256 utility is required for Read image transport'
fi
printf 'MAKA_EDIT_BYTES_V1 length=%s sha256=%s\n' "$size" "$digest"
base64 < "$target" | LC_ALL=C tr -d '\\r\\n'
printf '\nMAKA_EDIT_BYTES_END\n'
`;
const EDIT_WRITE_BYTES_SCRIPT = `${COMMON_SHELL_HELPERS}
root=$(pwd -P) || exit 1
target=$(existing_target "$1" 'Edit path') || exit 1
payload=$2
tmp=
created=
cleanup() {
[ -n "$created" ] && [ -n "$tmp" ] && rm -f "$tmp"
}
trap cleanup EXIT HUP INT TERM
tmp=$(mktemp "$target.maka-edit.XXXXXX") || fail 'Edit temp file creation failed'
created=1
if printf '%s' "$payload" | base64 -d > "$tmp" 2>/dev/null; then
:
elif printf '%s' "$payload" | base64 -D > "$tmp" 2>/dev/null; then
:
else
fail 'base64 decode failed for Edit payload'
fi
mode=$( (stat -c '%a' "$target" 2>/dev/null || stat -f '%Lp' "$target" 2>/dev/null) | head -n 1 )
[ -n "$mode" ] && chmod "$mode" "$tmp" 2>/dev/null || true
mv "$tmp" "$target" || fail 'Edit atomic rename failed'
created=
trap - EXIT HUP INT TERM
`;
const GLOB_SCRIPT = `${COMMON_SHELL_HELPERS}
root=$(pwd -P) || exit 1
pattern=$1
pattern_re=$2
search_cwd=$3
if [ -n "$search_cwd" ]; then
base=$(existing_target "$search_cwd" 'Glob cwd') || exit 1
else
base=$root
fi
# Enumerate with ripgrep when present (fast), else POSIX find. Both branches emit
# root-relative paths filtered by the shared ERE ($pattern_re) -- membership never
# depends on rg's glob dialect -- then sort under a fixed locale BEFORE the 200 cap
# so a truncated result is the same set either way. rg's listing goes to a temp
# file (not a shell var) so the full set streams into the filter without being
# buffered in memory, while its exit code is still checked in this shell: rc>1
# surfaces a real error instead of "no files" (mirrors the Grep rg branch).
# rg flags: --no-config keeps it hermetic (a host RIPGREP_CONFIG_PATH cannot
# inject --follow/--glob); --no-ignore --hidden match find's file set; an explicit
# path avoids rg's never-closing stdin.
rel_base=.
[ "$base" != "$root" ] && rel_base=\${base#"$root"/}
if command -v rg >/dev/null 2>&1; then
list=$(mktemp) || exit 1
trap 'rm -f "$list"' EXIT
( cd "$root" && rg --no-config --files --no-ignore --hidden -- "$rel_base" ) > "$list"
rc=$?
[ "$rc" -gt 1 ] && { echo "ripgrep failed (exit $rc)" >&2; exit "$rc"; }
sed 's#^\\./##' "$list" | awk -v re="$pattern_re" '$0 ~ re' | LC_ALL=C sort | awk 'NR <= 200'
else
find "$base" -type f -print | awk -v root="$root" -v re="$pattern_re" '
BEGIN { prefix = root "/" }
{
rel = $0
if (index(rel, prefix) == 1) rel = substr(rel, length(prefix) + 1)
if (rel ~ re) print rel
}
' | LC_ALL=C sort | awk 'NR <= 200'
fi
`;
const GREP_SCRIPT = `${COMMON_SHELL_HELPERS}
root=$(pwd -P) || exit 1
grep_pattern=$1
input_path=$2
glob=$3
glob_re=$4
if [ -n "$input_path" ]; then
start=$(existing_target "$input_path" 'Grep path') || exit 1
else
start=$root
fi
# Prefer rg for the common no-glob case: faster, skips binaries, and a far richer
# regex than awk. rg's gitignore-style --glob is a different dialect from the
# fallback's ERE, so routing every glob through the single fallback path keeps
# results identical with or without rg installed; otherwise the POSIX find/awk
# walk runs. Output stays "<relpath>:<line>:<text>" and the per-file (50) / total
# (200) caps are preserved. existing_target above already enforced the inside-
# workspace + no-symlink-escape invariant for the search root in both paths;
# input_path is workspace-relative so rg, run from root, emits workspace-relative
# paths.
if [ -z "$glob" ] && command -v rg >/dev/null 2>&1; then
# Always pass an explicit path: with none, rg reads from its (never-closing)
# stdin pipe and hangs. "." searches the whole workspace; rg prefixes those
# hits with "./", stripped below to the bare relative-path contract.
search=$input_path
[ -n "$search" ] || search=.
# The rg arg list is fixed — no glob is routed here, so nothing is assembled
# dynamically (and --glob is never re-grown into this branch):
# --no-config: ignore RIPGREP_CONFIG_PATH so a host-set rg config cannot inject
# flags (e.g. --follow) that would break the no-workspace-escape invariant.
# --no-follow: never traverse symlinks, matching the find/awk fallback (find
# without -L), so a workspace-internal symlink cannot leak external files.
# --no-ignore --hidden: search the same file set as the fallback, so results do
# not depend on whether rg happens to be installed (rg still skips binaries).
# --with-filename keeps the filename on single-file searches so the
# "<relpath>:<line>:<text>" contract holds in every case.
# Capture only stdout; rg's stderr flows through to the script's stderr so a
# real error is surfaced to the agent (via the executor) instead of swallowed.
# rg exit: 0 = matched, 1 = no match (-> empty result), >1 = a real error (bad
# regex, I/O) that must surface instead of masquerading as "no hits".
matches=$(rg --no-config --no-follow --line-number --no-heading --with-filename --color never --no-ignore --hidden --max-count 50 -e "$grep_pattern" -- "$search")
rc=$?
[ "$rc" -gt 1 ] && { echo "ripgrep failed (exit $rc)" >&2; exit "$rc"; }
printf '%s\n' "$matches" | sed 's#^\\./##' | awk 'NR <= 200'
else
if [ -f "$start" ]; then
file_list=$start
else
file_list=$(find "$start" -type f -print)
fi
printf '%s\n' "$file_list" | while IFS= read -r file; do
[ -n "$file" ] || continue
rel=$file
prefix=$root/
case "$rel" in "$prefix"*) rel=\${rel#"$prefix"} ;; esac
if [ -n "$glob_re" ]; then
printf '%s\n' "$rel" | awk -v re="$glob_re" 'BEGIN { ok = 1 } $0 ~ re { ok = 0 } END { exit ok }' || continue
fi
awk -v rel="$rel" -v pattern="$grep_pattern" '
$0 ~ pattern {
print rel ":" NR ":" $0
count += 1
if (count >= 50) exit
}
' "$file"
done | awk 'NR <= 200'
fi
`;