blob: b675b1f4ce382d2d30c2eec651506df252d04452 [file]
// packages/runtime/src/builtin-tools.ts
// Phase 1 baseline tool set. Each tool returned as MakaTool[] so
// ToolRuntime settlement decorates these with permission and durable tool facts.
//
// Read / Glob / Grep auto-approve.
// Bash / Write / Edit go through PermissionEngine.
import { z } from 'zod';
import { jsonSchema, zodSchema } from 'ai';
import {
closeSync,
constants,
fstatSync,
futimesSync,
lstatSync,
openSync,
realpathSync,
unlinkSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { basename, dirname, isAbsolute } from 'node:path';
import {
applyAdditionalPermissionProfile,
compilePermissionProfile,
type StorageRef,
type PermissionProfile,
} from '@maka/core';
import { computeEditedSource } from './edit-replace.js';
import { bashToolResultToModelOutput } from './bash-model-output.js';
import {
buildManagedBashTool,
buildStopBackgroundTaskTool,
buildWriteStdinTool,
bashSandboxPermissionsSchema,
shapeTerminalResult,
withShellGuidance,
} from './shell-tools.js';
import type { ManagedBashPermissionArgs, ShellRunLauncher } from './shell-tools.js';
import { defaultShellPlan, type ShellPlan } from './shell-detect.js';
import type {
BackgroundTaskStopper,
PtyControlWriter,
RuntimeResourceReader,
} from './shell-run-contract.js';
import {
createLocalWorkspaceExecutor,
type WorkspaceExecResult,
type WorkspaceExecutor,
} from './workspace-executor.js';
// tool-runtime.ts is the single source of truth for the tool shape; this
// re-export only keeps back-compat for callers that imported from
// builtin-tools directly.
import type { MakaTool, MakaToolContext } from './tool-runtime.js';
export type { MakaTool, MakaToolContext };
import { withFileWriteLock } from './file-write-lock.js';
import type { SandboxManager } from './sandbox/sandbox-manager.js';
import { SandboxCommandError } from './sandbox/errors.js';
import { isLikelySandboxDenial } from './sandbox/detect.js';
import { linuxExecutableRoots } from './sandbox/linux-sandbox.js';
import type { SandboxPlatform, SandboxType } from './sandbox/types.js';
import type { ChildFdInput } from './child-fd-input.js';
import { buildArchiveReadTool } from './archive-read-tool.js';
import type { ToolResultArchiveResourceReader } from './tool-result-archive-resource.js';
import {
normalizeAdditionalPermissionPath,
planDeclaredBashAdditionalPermission,
planFileToolAdditionalPermission,
type AdditionalPermissionGrant,
type AdditionalPermissionPlannerContext,
type AdditionalPermissionPlanResult,
} from './additional-permissions.js';
import type { FilesystemWorkerClient } from './filesystem-worker/client.js';
import {
assertSandboxEscalationGrantForExecution,
planDeclaredBashSandboxEscalation,
type SandboxEscalationPlannerContext,
type SandboxEscalationPlanResult,
} from './sandbox-escalation.js';
// Generous wall-clock cap for the ripgrep-backed Grep tool. A search should be
// near-instant; this only bounds a pathological hang now that the stream
// watchdog is paused during tool execution.
const GREP_TIMEOUT_MS = 120_000;
export interface BuildBuiltinToolsOptions {
shellRuns?: ShellRunLauncher;
runtimeResources?: RuntimeResourceReader;
archiveResources?: ToolResultArchiveResourceReader;
backgroundTasks?: BackgroundTaskStopper;
ptyControls?: PtyControlWriter;
executor?: WorkspaceExecutor;
/** Shell that runs Bash commands. Defaults to the process-wide detected shell. */
shell?: ShellPlan;
permissionProfile?: PermissionProfile;
sandboxManager?: SandboxManager;
/** Enable only when the host consumes additional-permission approval events. */
enableBashAdditionalPermissions?: boolean;
/** Sandboxed worker used for all local filesystem tools. */
filesystemWorker?: Pick<FilesystemWorkerClient, 'execute'>;
/** Enable inferred one-call path expansion for filesystem tools. */
enableFileToolAdditionalPermissions?: boolean;
/** Host-surface gate for Edit. Defaults to enabled. */
includeEdit?: boolean;
/** Test/embedding override. Production callers use the current process platform. */
sandboxPlatform?: SandboxPlatform;
snapshotImage?: (input: {
sessionId: string;
turnId: string;
name: string;
bytes: Uint8Array;
mimeType: string;
}) => Promise<Extract<StorageRef, { kind: 'session_file' }>>;
}
export function buildBuiltinTools(options: BuildBuiltinToolsOptions = {}): MakaTool[] {
if (options.enableBashAdditionalPermissions && !options.sandboxManager) {
throw new Error('Bash additional permissions require a sandbox manager.');
}
if (options.enableFileToolAdditionalPermissions && !options.filesystemWorker) {
throw new Error('File tool additional permissions require a sandboxed filesystem worker.');
}
const executor = options.executor ?? createLocalWorkspaceExecutor();
const executionFacts = executor.facts;
const readDescription = `Read a text file${options.snapshotImage ? ' or supported image' : ''} from disk${options.runtimeResources ? ', or read a whole runtime resource using ref' : ''}.`;
const pathField = z
.string()
.describe('A file path; relative paths are resolved from the session cwd');
const offsetField = z
.number()
.int()
.nonnegative()
.describe('Zero-based text file line offset')
.optional();
const limitField = z
.number()
.int()
.positive()
.describe('Maximum text file lines to read')
.optional();
const refField = z.string().describe('A runtime resource ref returned by another tool');
const fileReadParameters = z
.object({
path: pathField,
offset: offsetField,
limit: limitField,
})
.strict();
const runtimeResourceReadParameters = z
.object({
ref: refField,
})
.strict();
const strictReadParameters = z
.union([fileReadParameters, runtimeResourceReadParameters])
.describe('Read a file with path, or a whole runtime resource with ref; provide exactly one');
// Provider-facing schema: a single top-level object with every field optional.
// Anthropic rejects a tool definition whose input schema carries a top-level
// `anyOf`, so the file-vs-ref exclusivity is stated in the field descriptions
// here and enforced authoritatively by the strict union in `validate` below
// (see #1228 — a union-generated `anyOf` had been leaking onto the wire).
const providerReadParameters = z
.object({
path: pathField
.describe(
'A file path; relative paths are resolved from the session cwd. Provide either path (optionally with offset/limit) or ref, never both.',
)
.optional(),
offset: offsetField,
limit: limitField,
ref: refField
.describe(
'A runtime resource ref returned by another tool. Provide ref on its own, without path/offset/limit.',
)
.optional(),
})
.describe(
'Read a file with path (optionally offset/limit), or a whole runtime resource with ref; provide exactly one of path or ref.',
);
const providerReadSchema = zodSchema(providerReadParameters);
const readParameters = options.runtimeResources
? jsonSchema(async () => await providerReadSchema.jsonSchema, {
validate: async (value) => {
const result = await strictReadParameters.safeParseAsync(value);
return result.success
? { success: true, value: result.data }
: { success: false, error: result.error };
},
})
: fileReadParameters;
const shell = options.shell ?? defaultShellPlan();
const sandboxPlatform = options.sandboxPlatform ?? process.platform;
if (
options.enableBashAdditionalPermissions &&
sandboxPlatform !== 'darwin' &&
sandboxPlatform !== 'linux'
) {
throw new Error('Bash additional permissions are currently supported only on macOS and Linux.');
}
if (
options.enableFileToolAdditionalPermissions &&
sandboxPlatform !== 'darwin' &&
sandboxPlatform !== 'linux'
) {
throw new Error(
'File tool additional permissions are currently supported only on macOS and Linux.',
);
}
const bashAdditionalPermissionPlanner =
options.sandboxManager && options.enableBashAdditionalPermissions
? createBashAdditionalPermissionPlanner(
options.sandboxManager,
options.permissionProfile,
sandboxPlatform,
)
: undefined;
const bashSandboxEscalationPlanner =
options.sandboxManager && options.enableBashAdditionalPermissions
? createBashSandboxEscalationPlanner()
: undefined;
const filePermissionPlanner = options.enableFileToolAdditionalPermissions
? createFileToolAdditionalPermissionPlanner(options.permissionProfile)
: undefined;
const bashTools = options.shellRuns
? [
buildManagedBashTool(options.shellRuns, {
executionFacts,
shell,
...(options.sandboxManager
? {
sandbox: sandboxAvailabilityResolver(
options.sandboxManager,
options.permissionProfile,
sandboxPlatform,
),
transformCommand: ({ command, pty, ctx }) =>
sandboxCommand(
options.sandboxManager!,
options.permissionProfile,
sandboxPlatform,
command,
pty,
ctx,
'background_command',
),
}
: {}),
...(bashAdditionalPermissionPlanner
? { planAdditionalPermissions: bashAdditionalPermissionPlanner }
: {}),
...(bashSandboxEscalationPlanner
? { planSandboxEscalation: bashSandboxEscalationPlanner }
: {}),
}),
]
: [
buildExecutorBashTool(executor, shell, {
...(options.permissionProfile ? { permissionProfile: options.permissionProfile } : {}),
...(options.sandboxManager ? { sandboxManager: options.sandboxManager } : {}),
...(bashAdditionalPermissionPlanner
? { planAdditionalPermissions: bashAdditionalPermissionPlanner }
: {}),
...(bashSandboxEscalationPlanner
? { planSandboxEscalation: bashSandboxEscalationPlanner }
: {}),
sandboxPlatform,
}),
];
const backgroundTools = [
...(options.backgroundTasks ? [buildStopBackgroundTaskTool(options.backgroundTasks)] : []),
...(options.ptyControls ? [buildWriteStdinTool(options.ptyControls)] : []),
];
const tools: MakaTool[] = [
...bashTools,
...backgroundTools,
...(options.archiveResources ? [buildArchiveReadTool(options.archiveResources)] : []),
{
name: 'Read',
activityKind: 'read',
description: readDescription,
parameters: readParameters,
permissionRequired: false,
executionFacts,
...(filePermissionPlanner
? {
planAdditionalPermissions: filePermissionPlanner('Read', (args) =>
typeof args.path === 'string' && classifyRuntimeResourceRef(args.path) === 'file'
? args.path
: undefined,
),
}
: {}),
impl: async (input, ctx) => {
const { cwd, sessionId, abortSignal } = ctx;
if ('ref' in input) {
const { ref } = input;
if (classifyRuntimeResourceRef(ref) !== 'runtime') {
throw new Error(`Unsupported runtime resource ref: ${ref}`);
}
if (!options.runtimeResources)
throw new Error('Runtime resources are not available in this toolset');
return await options.runtimeResources.readRuntimeResource(sessionId, ref, abortSignal);
}
const { path, offset, limit } = input;
const runtimeRef = classifyRuntimeResourceRef(path);
if (runtimeRef === 'unsupported')
throw new Error(`Unsupported runtime resource ref: ${path}`);
if (runtimeRef === 'runtime') {
throw new Error('Runtime resources must be read with the ref parameter, not path');
}
if (options.filesystemWorker) {
const canonicalCwd = canonicalExistingPath(cwd);
const result = await options.filesystemWorker.execute({
operation: {
kind: 'read',
path,
...(offset !== undefined ? { offset } : {}),
...(limit !== undefined ? { limit } : {}),
},
cwd: canonicalCwd,
mode: ctx.permissionMode ?? 'ask',
...(options.permissionProfile ? { permissionProfile: options.permissionProfile } : {}),
...(ctx.permissionContext?.additionalGrant
? { additionalGrant: ctx.permissionContext.additionalGrant }
: {}),
...(abortSignal ? { abortSignal } : {}),
});
if (result.kind === 'read_image') {
if (!options.snapshotImage)
throw new Error('Read image snapshots are not available in this toolset.');
const ref = await options.snapshotImage({
sessionId,
turnId: ctx.turnId,
name: basename(path),
bytes: Buffer.from(result.base64, 'base64'),
mimeType: result.mimeType,
});
return { kind: 'image' as const, mimeType: result.mimeType, ref };
}
if (result.kind !== 'read')
throw new Error('Filesystem worker returned a mismatched Read result.');
return { content: result.content };
}
const { path: resolvedPath } = await executor.resolveExistingPath({
cwd,
path,
label: 'Read',
});
const result = await executor.readFile({
cwd,
path: resolvedPath,
...(offset !== undefined ? { offset } : {}),
...(limit !== undefined ? { limit } : {}),
});
if ('bytes' in result) {
if (!options.snapshotImage)
throw new Error('Read image snapshots are not available in this toolset.');
const ref = await options.snapshotImage({
sessionId,
turnId: ctx.turnId,
name: basename(path),
bytes: result.bytes,
mimeType: result.mimeType,
});
return { kind: 'image' as const, mimeType: result.mimeType, ref };
}
return result;
},
},
{
name: 'Write',
activityKind: 'edit',
description: 'Write content to a file (creates or overwrites). Subject to permission policy.',
parameters: z.object({ path: z.string(), content: z.string() }),
permissionRequired: true,
executionFacts,
...(filePermissionPlanner
? {
planAdditionalPermissions: filePermissionPlanner('Write', (args) => args.path),
}
: {}),
impl: async ({ path, content }, ctx) => {
const { cwd } = ctx;
const canonicalCwd = options.filesystemWorker ? canonicalExistingPath(cwd) : cwd;
const key = options.filesystemWorker
? await fileToolWriteLockKey(canonicalCwd, path)
: (await executor.writeLockKey({ cwd, path })).key;
return await withFileWriteLock(key, async () => {
if (options.filesystemWorker) {
const result = await options.filesystemWorker.execute({
operation: { kind: 'write', path, content },
cwd: canonicalCwd,
mode: ctx.permissionMode ?? 'ask',
...(options.permissionProfile
? { permissionProfile: options.permissionProfile }
: {}),
...(ctx.permissionContext?.additionalGrant
? { additionalGrant: ctx.permissionContext.additionalGrant }
: {}),
...(ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}),
});
if (result.kind !== 'write')
throw new Error('Filesystem worker returned a mismatched Write result.');
return { ok: result.ok, path: result.path, bytes: result.bytes };
}
const { path: resolvedPath } = await executor.resolveWritablePath({
cwd,
path,
label: 'Write',
});
return await executor.writeFile({ cwd, path: resolvedPath, content });
});
},
},
{
name: 'Edit',
activityKind: 'edit',
description:
'Replace old_string with new_string in a file. 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(),
}),
permissionRequired: true,
executionFacts,
...(filePermissionPlanner
? {
planAdditionalPermissions: filePermissionPlanner('Edit', (args) => args.path),
}
: {}),
impl: async ({ path, old_string, new_string }, ctx) => {
const { cwd } = ctx;
const canonicalCwd = options.filesystemWorker ? canonicalExistingPath(cwd) : cwd;
const key = options.filesystemWorker
? await fileToolWriteLockKey(canonicalCwd, path)
: (await executor.writeLockKey({ cwd, path })).key;
return await withFileWriteLock(key, async () => {
if (options.filesystemWorker) {
const result = await options.filesystemWorker.execute({
operation: {
kind: 'edit',
path,
oldString: old_string,
newString: new_string,
},
cwd: canonicalCwd,
mode: ctx.permissionMode ?? 'ask',
...(options.permissionProfile
? { permissionProfile: options.permissionProfile }
: {}),
...(ctx.permissionContext?.additionalGrant
? { additionalGrant: ctx.permissionContext.additionalGrant }
: {}),
...(ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}),
});
if (result.kind !== 'edit')
throw new Error('Filesystem worker returned a mismatched Edit result.');
return {
ok: result.ok,
path: result.path,
replacements: result.replacements,
matchedVia: result.matchedVia,
startLine: result.startLine,
endLine: result.endLine,
};
}
const { path: resolvedPath } = await executor.resolveExistingPath({
cwd,
path,
label: 'Edit',
});
const read = await executor.readFile({ cwd, path: resolvedPath });
if ('bytes' in read) throw new Error('Edit does not support image files.');
const current = read.content;
const result = computeEditedSource(current, old_string, new_string, path);
await executor.writeFile({ cwd, path: resolvedPath, content: result.content });
return {
ok: true,
path: resolvedPath,
replacements: 1,
matchedVia: result.matchedVia,
startLine: result.startLine,
endLine: result.endLine,
};
});
},
},
{
name: 'FormatJson',
activityKind: 'edit',
description:
'Validate and normalize a JSON file in place. Reads the file at `path`, ' +
'parses it (throwing a parse-error hint on invalid JSON), optionally sorts ' +
'object keys lexicographically, and rewrites it with canonical 2-space ' +
'indentation. Returns only a diagnostic (valid + byte delta) — the content ' +
'is never round-tripped back through the prompt. Useful for config hygiene ' +
'after a Write.',
parameters: z.object({
path: z
.string()
.describe(
'Path to the JSON file to validate and normalize, relative to the session cwd.',
),
sort_keys: z
.boolean()
.optional()
.describe('Sort object keys lexicographically; default false.'),
}),
permissionRequired: true,
executionFacts,
...(filePermissionPlanner
? {
planAdditionalPermissions: filePermissionPlanner('FormatJson', (args) => args.path),
}
: {}),
impl: async ({ path, sort_keys }, ctx) => {
const { cwd } = ctx;
const canonicalCwd = options.filesystemWorker ? canonicalExistingPath(cwd) : cwd;
const key = options.filesystemWorker
? await fileToolWriteLockKey(canonicalCwd, path)
: (await executor.writeLockKey({ cwd, path })).key;
return await withFileWriteLock(key, async () => {
if (options.filesystemWorker) {
const result = await options.filesystemWorker.execute({
operation: { kind: 'format_json', path, sortKeys: sort_keys ?? false },
cwd: canonicalCwd,
mode: ctx.permissionMode ?? 'ask',
...(options.permissionProfile
? { permissionProfile: options.permissionProfile }
: {}),
...(ctx.permissionContext?.additionalGrant
? { additionalGrant: ctx.permissionContext.additionalGrant }
: {}),
...(ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}),
});
if (result.kind !== 'format_json') {
throw new Error('Filesystem worker returned a mismatched FormatJson result.');
}
return result;
}
const { path: resolvedPath } = await executor.resolveExistingPath({
cwd,
path,
label: 'FormatJson',
});
const read = await executor.readFile({ cwd, path: resolvedPath });
if ('bytes' in read) throw new Error('FormatJson does not support image files.');
const original = read.content;
const bytesBefore = Buffer.byteLength(original, 'utf8');
let parsed: unknown;
try {
parsed = JSON.parse(original);
} catch (e) {
return {
ok: false,
valid: false,
error: `FormatJson: invalid JSON: ${(e as Error).message}`,
path: resolvedPath,
bytesBefore,
byteDelta: 0,
changed: false,
};
}
const value = sort_keys ? sortKeysDeep(parsed) : parsed;
const formatted = JSON.stringify(value, null, 2);
const { bytes: bytesAfter } = await executor.writeFile({
cwd,
path: resolvedPath,
content: formatted,
});
return {
ok: true,
path: resolvedPath,
valid: true,
bytesBefore,
bytesAfter,
byteDelta: bytesAfter - bytesBefore,
changed: formatted !== original,
};
});
},
},
{
name: 'Glob',
activityKind: 'search',
description:
'Find files matching a glob pattern (case-insensitive, capped at 200, sorted by walk order).',
parameters: z.object({
pattern: z.string(),
cwd: z.string().optional(),
}),
permissionRequired: false,
executionFacts,
...(filePermissionPlanner
? {
planAdditionalPermissions: filePermissionPlanner('Glob', (args) => args.cwd ?? '.'),
}
: {}),
impl: async ({ pattern, cwd: relCwd }, ctx) => {
const { cwd } = ctx;
assertRelativeGlobPattern(pattern);
if (options.filesystemWorker) {
const canonicalCwd = canonicalExistingPath(cwd);
const result = await options.filesystemWorker.execute({
operation: { kind: 'glob', path: relCwd ?? '.', pattern, limit: 200 },
cwd: canonicalCwd,
mode: ctx.permissionMode ?? 'ask',
...(options.permissionProfile ? { permissionProfile: options.permissionProfile } : {}),
...(ctx.permissionContext?.additionalGrant
? { additionalGrant: ctx.permissionContext.additionalGrant }
: {}),
...(ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}),
});
if (result.kind !== 'glob')
throw new Error('Filesystem worker returned a mismatched Glob result.');
return { files: result.files };
}
const { path: base } = await executor.resolveExistingPath({
cwd,
path: relCwd ?? '.',
label: 'Glob cwd',
});
return await executor.globFiles({ cwd: base, pattern, limit: 200 });
},
},
{
name: 'Grep',
activityKind: 'search',
description: 'Search file contents with a regex via ripgrep.',
parameters: z.object({
pattern: z.string(),
path: z.string().optional(),
glob: z.string().optional(),
}),
permissionRequired: false,
executionFacts,
...(filePermissionPlanner
? {
planAdditionalPermissions: filePermissionPlanner('Grep', (args) => args.path ?? '.'),
}
: {}),
impl: async ({ pattern, path, glob }, ctx) => {
const { cwd, abortSignal } = ctx;
if (options.filesystemWorker) {
const canonicalCwd = canonicalExistingPath(cwd);
const result = await options.filesystemWorker.execute({
operation: {
kind: 'grep',
path: path ?? '.',
pattern,
...(glob ? { glob } : {}),
maxCountPerFile: 50,
limit: 200,
timeoutMs: GREP_TIMEOUT_MS,
},
cwd: canonicalCwd,
mode: ctx.permissionMode ?? 'ask',
...(options.permissionProfile ? { permissionProfile: options.permissionProfile } : {}),
...(ctx.permissionContext?.additionalGrant
? { additionalGrant: ctx.permissionContext.additionalGrant }
: {}),
...(abortSignal ? { abortSignal } : {}),
});
if (result.kind !== 'grep')
throw new Error('Filesystem worker returned a mismatched Grep result.');
return { matches: result.matches };
}
const { path: searchPath } = await executor.resolveExistingPath({
cwd,
path: path ?? '.',
label: 'Grep',
});
// Self-bound: ripgrep finishes in well under a second normally, but a
// pathological tree (network mount, /proc, a FIFO) could hang it. The
// stream watchdog no longer caps tool execution, so each spawning tool
// must carry its own wall-clock timeout and honour the turn's abort.
return await executor.grepFiles({
cwd,
pattern,
path: searchPath,
...(glob ? { glob } : {}),
maxCountPerFile: 50,
limit: 200,
timeoutMs: GREP_TIMEOUT_MS,
...(abortSignal ? { abortSignal } : {}),
});
},
},
];
return tools.filter((tool) => options.includeEdit !== false || tool.name !== 'Edit');
}
interface ExecutorBashSandboxOptions {
permissionProfile?: PermissionProfile;
sandboxManager?: SandboxManager;
sandboxPlatform: SandboxPlatform;
planAdditionalPermissions?: BashAdditionalPermissionPlanner;
planSandboxEscalation?: BashSandboxEscalationPlanner;
}
type BashAdditionalPermissionPlanner = (
args: ManagedBashPermissionArgs,
context: AdditionalPermissionPlannerContext,
) => Promise<AdditionalPermissionPlanResult> | AdditionalPermissionPlanResult;
type BashSandboxEscalationPlanner = (
args: ManagedBashPermissionArgs,
context: SandboxEscalationPlannerContext,
) => Promise<SandboxEscalationPlanResult> | SandboxEscalationPlanResult;
function buildExecutorBashTool(
executor: WorkspaceExecutor,
shell: ShellPlan,
sandboxOptions: ExecutorBashSandboxOptions,
): MakaTool {
return {
name: 'Bash',
activityKind: 'command',
description:
withShellGuidance('Run a shell command in the session cwd.', shell) +
' Subject to permission policy.' +
(sandboxOptions.planAdditionalPermissions || sandboxOptions.planSandboxEscalation
? ' Request minimal one-call access with sandbox_permissions; use require_escalated only when sandboxed execution cannot work.'
: ''),
parameters: z
.object({
command: z.string().describe('The shell command to execute'),
timeout_ms: z.number().int().positive().max(600_000).optional(),
...(sandboxOptions.planAdditionalPermissions || sandboxOptions.planSandboxEscalation
? {
sandbox_permissions: bashSandboxPermissionsSchema
.describe(
'Optional one-call filesystem/network permission or explicit unsandboxed execution request.',
)
.optional(),
}
: {}),
})
.strict(),
permissionRequired: true,
toModelOutput: ({ output }) => bashToolResultToModelOutput(output),
executionFacts: executor.facts,
...(sandboxOptions.planAdditionalPermissions
? { planAdditionalPermissions: sandboxOptions.planAdditionalPermissions }
: {}),
...(sandboxOptions.planSandboxEscalation
? { planSandboxEscalation: sandboxOptions.planSandboxEscalation }
: {}),
...(sandboxOptions.sandboxManager
? {
sandbox: sandboxAvailabilityResolver(
sandboxOptions.sandboxManager,
sandboxOptions.permissionProfile,
sandboxOptions.sandboxPlatform,
),
}
: {}),
impl: async ({ command, timeout_ms }, ctx) => {
const { cwd, abortSignal, emitOutput } = ctx;
const timeout = timeout_ms ?? 120_000;
const transformed = sandboxOptions.sandboxManager
? sandboxCommand(
sandboxOptions.sandboxManager,
sandboxOptions.permissionProfile,
sandboxOptions.sandboxPlatform,
command,
false,
ctx,
)
: undefined;
let successful = false;
try {
const result = await executor.exec({
command,
cwd: transformed?.cwd ?? cwd,
...(transformed?.argv ? { argv: transformed.argv } : {}),
...(transformed?.env ? { env: transformed.env } : {}),
...(transformed?.fdInputs ? { fdInputs: transformed.fdInputs } : {}),
timeoutMs: timeout,
...(abortSignal ? { abortSignal } : {}),
emitOutput,
shell,
});
const executionResult = {
...result,
...(transformed?.sandboxType ? { sandboxType: transformed.sandboxType } : {}),
...(transformed?.profileName ? { profileName: transformed.profileName } : {}),
sandboxed:
transformed?.sandboxType === 'macos-seatbelt' || transformed?.sandboxType === 'linux',
};
if (executionResult.timedOut)
throw terminalError(`Command timed out after ${timeout}ms`, executionResult, 124);
if (executionResult.aborted) throw terminalError('Command aborted', executionResult, 130);
if (executionResult.exitCode !== 0) {
throw terminalError(
`Command failed with exit code ${executionResult.exitCode}`,
executionResult,
executionResult.exitCode,
);
}
successful = true;
return shapeTerminalResult({ cwd, command, result: executionResult });
} finally {
transformed?.onCompletion?.({ successful });
}
},
};
}
function sandboxAvailabilityResolver(
manager: SandboxManager,
explicitProfile: PermissionProfile | undefined,
platform: SandboxPlatform,
): NonNullable<MakaTool['sandbox']> {
return ({ permissionMode, cwd, args }) => {
const effective = effectivePermissionProfile(explicitProfile, permissionMode, cwd);
if (isPtyBashArgs(args) && profileRequiresSandbox(effective.profile)) {
return { platformSandboxAvailable: false };
}
return {
platformSandboxAvailable: manager.canEnforce({
profile: effective.profile,
platform,
}),
};
};
}
function sandboxCommand(
manager: SandboxManager,
explicitProfile: PermissionProfile | undefined,
platform: SandboxPlatform,
command: string,
pty: boolean,
ctx: MakaToolContext,
domain: 'command' | 'background_command' = 'command',
):
| {
argv?: readonly string[];
cwd: string;
env?: NodeJS.ProcessEnv;
fdInputs?: readonly ChildFdInput[];
sandboxType?: SandboxType;
profileName?: string;
onCompletion?: (outcome: { successful: boolean }) => void;
}
| undefined {
const cwd = canonicalExistingPath(ctx.cwd);
const effective = effectivePermissionProfile(explicitProfile, ctx.permissionMode ?? 'ask', cwd);
const env = { ...process.env };
const additionalGrant = ctx.permissionContext?.additionalGrant;
const escalationGrant = ctx.permissionContext?.sandboxEscalationGrant;
if (additionalGrant && escalationGrant) {
throw new SandboxCommandError({
domain,
stage: 'validation',
reason: 'conflicting_permission_context',
recoverable: false,
profileName: effective.profile.name ?? effective.profile.type,
message: 'Additional permissions and sandbox escalation cannot be applied together.',
});
}
if (escalationGrant) {
assertSandboxEscalationGrantForExecution({ grant: escalationGrant, command, cwd });
}
if (pty) {
if (escalationGrant) return { cwd, env, sandboxType: 'none' };
if (profileRequiresSandbox(effective.profile)) {
throw new SandboxCommandError({
domain,
stage: 'capability',
reason: 'pty_sandbox_unavailable',
recoverable: false,
profileName: effective.profile.name ?? effective.profile.type,
message:
'PTY Bash is unavailable while the active permission profile requires command sandboxing.',
});
}
return undefined;
}
if (!escalationGrant && !manager.canEnforce({ profile: effective.profile, platform })) {
if (profileRequiresSandbox(effective.profile)) {
const selection = manager.selectInitial({ profile: effective.profile, platform });
throw new SandboxCommandError({
domain,
stage: selection.ok ? 'capability' : 'selection',
reason: selection.ok ? 'backend_not_available' : selection.reason,
backend: selection.sandboxType,
recoverable: false,
profileName: effective.profile.name ?? effective.profile.type,
message: `Command sandbox is required but unavailable on platform ${platform}.`,
});
}
return undefined;
}
let preparedTargets: readonly PreparedExactWriteTarget[] = [];
try {
preparedTargets = prepareLinuxBashExactWriteTargets(platform, additionalGrant);
} catch {
throw new SandboxCommandError({
domain,
stage: 'validation',
reason: 'exact_write_target_changed',
backend: 'linux',
recoverable: false,
profileName: effective.profile.name ?? effective.profile.type,
message: 'An approved exact write target could not be prepared safely.',
});
}
const onCompletion = preparedExactWriteCompletion(preparedTargets);
let result: ReturnType<SandboxManager['transform']>;
try {
result = manager.transform({
platform,
command: {
program: '/bin/sh',
args: ['-c', command],
cwd,
env,
profile: effective.profile,
pathContext: {
workspaceRoots: effective.workspaceRoots,
tmpdir: tmpdir(),
slashTmp: '/tmp',
...(platform === 'darwin'
? {
executableRoots: macosRuntimeExecutableRoots(process.execPath),
}
: {}),
...(platform === 'linux'
? {
minimalRoots: linuxExecutableRoots({
execPath: process.execPath,
path: env.PATH,
}),
...(preparedTargets.length > 0
? {
pinnedWritableFiles: preparedTargets.map((target) => ({
path: target.path,
fd: target.childFd,
sourceFd: target.sourceFd,
releaseSource: target.releaseSource,
})),
}
: {}),
}
: {}),
},
},
...(additionalGrant ? { additionalPermissions: additionalGrant.profile } : {}),
...(escalationGrant ? { preference: 'forbid' as const } : {}),
});
} catch (error) {
onCompletion?.({ successful: false });
throw error;
}
if (!result.ok) {
onCompletion?.({ successful: false });
throw new SandboxCommandError({
domain,
stage: 'transform',
reason: result.reason,
backend: result.sandboxType,
recoverable: false,
profileName: effective.profile.name ?? effective.profile.type,
message: result.message ?? `Sandbox transform failed: ${result.reason}`,
});
}
return {
argv: result.exec.argv,
cwd: result.exec.cwd,
...(result.exec.env ? { env: { ...result.exec.env } } : {}),
...(result.exec.fdInputs ? { fdInputs: result.exec.fdInputs } : {}),
sandboxType: result.exec.sandboxType,
profileName: result.exec.effectiveProfile.name ?? result.exec.effectiveProfile.type,
...(onCompletion ? { onCompletion } : {}),
};
}
interface PreparedExactWriteTarget {
readonly path: string;
readonly sourceFd: number;
readonly releaseSource: () => void;
readonly childFd: number;
readonly device: bigint;
readonly inode: bigint;
readonly mtimeNs: bigint;
readonly ctimeNs: bigint;
}
function prepareLinuxBashExactWriteTargets(
platform: SandboxPlatform,
grant: AdditionalPermissionGrant | undefined,
): readonly PreparedExactWriteTarget[] {
if (platform !== 'linux' || !grant) return [];
const exactWrites = new Set(
grant.profile.fileSystem?.entries
.filter((entry) => entry.access === 'write' && entry.scope === 'exact')
.map((entry) => entry.path) ?? [],
);
const prepared: PreparedExactWriteTarget[] = [];
try {
for (const target of grant.normalizedPaths) {
if (
target.access !== 'write' ||
target.scope !== 'exact' ||
target.targetType !== 'missing' ||
!exactWrites.has(target.enforcementPath)
) {
continue;
}
const fd = openMissingExactWriteTarget(target.enforcementPath);
let sourceOpen = true;
const releaseSource = () => {
if (!sourceOpen) return;
sourceOpen = false;
closeSync(fd);
};
try {
// A deliberately old marker distinguishes a successful no-op from an
// intentional empty write, which updates mtime/ctime even at size zero.
futimesSync(fd, 1, 1);
const metadata = fstatSync(fd, { bigint: true });
prepared.push({
path: target.enforcementPath,
sourceFd: fd,
releaseSource,
childFd: 4 + prepared.length,
device: metadata.dev,
inode: metadata.ino,
mtimeNs: metadata.mtimeNs,
ctimeNs: metadata.ctimeNs,
});
} catch (error) {
releaseSource();
throw error;
}
}
return prepared;
} catch (error) {
completePreparedExactWriteTargets(prepared);
throw error;
}
}
/** @internal Exported for the Linux parent-swap regression test. */
export function openMissingExactWriteTarget(path: string, afterParentPinned?: () => void): number {
const parent = dirname(path);
if (realpathSync(parent) !== parent) throw new Error('Exact write target parent changed.');
const createFlags =
constants.O_CREAT | constants.O_EXCL | constants.O_RDWR | (constants.O_NOFOLLOW ?? 0);
if (process.platform !== 'linux') return openSync(path, createFlags, 0o666);
const linuxConstants = constants as typeof constants & { O_PATH?: number };
const parentFlags =
(linuxConstants.O_PATH ?? constants.O_RDONLY) |
(constants.O_DIRECTORY ?? 0) |
(constants.O_NOFOLLOW ?? 0);
const parentFd = openSync(parent, parentFlags);
try {
const pinnedParent = `/proc/self/fd/${parentFd}`;
afterParentPinned?.();
if (realpathSync(pinnedParent) !== parent) {
throw new Error('Exact write target parent changed after pinning.');
}
return openSync(`${pinnedParent}/${basename(path)}`, createFlags, 0o666);
} finally {
closeSync(parentFd);
}
}
function preparedExactWriteCompletion(
targets: readonly PreparedExactWriteTarget[],
): ((outcome: { successful: boolean }) => void) | undefined {
if (targets.length === 0) return undefined;
let completed = false;
return () => {
if (completed) return;
completed = true;
completePreparedExactWriteTargets(targets);
};
}
function completePreparedExactWriteTargets(targets: readonly PreparedExactWriteTarget[]): void {
for (const target of targets) {
try {
target.releaseSource();
} catch {
// Launch cleanup is best effort; the close-once owner prevents fd-number reuse bugs.
}
try {
const metadata = lstatSync(target.path, { bigint: true });
const untouched = metadata.mtimeNs === target.mtimeNs && metadata.ctimeNs === target.ctimeNs;
if (
metadata.isFile() &&
metadata.dev === target.device &&
metadata.ino === target.inode &&
metadata.size === 0n &&
untouched
) {
unlinkSync(target.path);
}
} catch {
// The target was already removed or changed; never delete an unverified replacement.
}
}
}
function profileRequiresSandbox(profile: PermissionProfile): boolean {
return profile.type === 'managed' && profile.fileSystem.kind === 'restricted';
}
function canonicalExistingPath(path: string): string {
try {
return realpathSync(path);
} catch {
return path;
}
}
function macosRuntimeExecutableRoots(execPath: string): readonly string[] {
return [
...linuxExecutableRoots({ execPath }),
...(execPath.startsWith('/opt/homebrew/') ? ['/opt/homebrew'] : []),
...(execPath.startsWith('/usr/local/') ? ['/usr/local'] : []),
];
}
function effectivePermissionProfile(
explicitProfile: PermissionProfile | undefined,
permissionMode: NonNullable<MakaToolContext['permissionMode']>,
cwd: string,
): { profile: PermissionProfile; workspaceRoots: readonly string[] } {
const canonicalCwd = canonicalExistingPath(cwd);
if (explicitProfile) return { profile: explicitProfile, workspaceRoots: [canonicalCwd] };
const compiled = compilePermissionProfile({ mode: permissionMode, cwd: canonicalCwd });
return { profile: compiled.profile, workspaceRoots: compiled.workspaceRoots };
}
function createBashAdditionalPermissionPlanner(
manager: SandboxManager,
explicitProfile: PermissionProfile | undefined,
platform: SandboxPlatform,
): BashAdditionalPermissionPlanner {
return async (args, context) => {
const effective = effectivePermissionProfile(explicitProfile, context.mode, context.cwd);
const plan = await planDeclaredBashAdditionalPermission({
declaration: args.sandbox_permissions,
cwd: context.cwd,
mode: context.mode,
command: args.command,
args: context.args,
context: {
profile: effective.profile,
workspaceRoots: effective.workspaceRoots,
pathContext: {
tmpdir: canonicalExistingPath(tmpdir()),
slashTmp: canonicalExistingPath('/tmp'),
},
},
});
if (plan.kind !== 'request') return plan;
if (args.pty === true) {
return {
kind: 'block',
reason: 'invalid_additional_permissions',
message: 'Additional Bash permissions cannot be applied to PTY execution.',
};
}
const effectiveWithAdditional = applyAdditionalPermissionProfile(
effective.profile,
plan.proposal.profile,
);
if (!manager.canEnforce({ profile: effectiveWithAdditional, platform })) {
return {
kind: 'block',
reason: 'invalid_additional_permissions',
message: `Additional Bash permissions cannot be enforced on platform ${platform}.`,
};
}
return plan;
};
}
function createBashSandboxEscalationPlanner(): BashSandboxEscalationPlanner {
return (args, context) =>
planDeclaredBashSandboxEscalation({
declaration: args.sandbox_permissions,
command: args.command,
cwd: canonicalExistingPath(context.cwd),
mode: context.mode,
args: context.args,
recentSandboxDenial: context.recentSandboxDenial,
});
}
type FileToolAdditionalPermissionName = 'Read' | 'Write' | 'Edit' | 'FormatJson' | 'Glob' | 'Grep';
type FileToolPathSelector = (args: Record<string, any>) => string | undefined;
function createFileToolAdditionalPermissionPlanner(
explicitProfile: PermissionProfile | undefined,
): (
toolName: FileToolAdditionalPermissionName,
selectPath: FileToolPathSelector,
) => NonNullable<MakaTool['planAdditionalPermissions']> {
return (toolName, selectPath) => async (args, context) => {
const path = selectPath(args as Record<string, any>);
if (!path) return { kind: 'not_required' };
const effective = effectivePermissionProfile(explicitProfile, context.mode, context.cwd);
return await planFileToolAdditionalPermission({
toolName,
path,
cwd: context.cwd,
mode: context.mode,
args: context.args,
context: {
profile: effective.profile,
workspaceRoots: effective.workspaceRoots,
pathContext: {
tmpdir: canonicalExistingPath(tmpdir()),
slashTmp: canonicalExistingPath('/tmp'),
},
},
});
};
}
async function fileToolWriteLockKey(cwd: string, path: string): Promise<string> {
const target = await normalizeAdditionalPermissionPath({
path,
access: 'write',
scope: 'exact',
cwd,
});
return target.enforcementPath;
}
function isPtyBashArgs(args: unknown): boolean {
return typeof args === 'object' && args !== null && (args as { pty?: unknown }).pty === true;
}
function terminalError(
message: string,
result: Pick<WorkspaceExecResult, 'stdout' | 'stderr' | 'stdoutTruncated' | 'stderrTruncated'> & {
sandboxType?: SandboxType;
sandboxed?: boolean;
profileName?: string;
},
code: number,
): Error {
const sandboxDenied = isLikelySandboxDenial({
stdout: result.stdout,
stderr: result.stderr,
sandboxed: result.sandboxed === true,
});
const error = sandboxDenied
? new SandboxCommandError({
domain: 'command',
stage: 'operation',
reason: 'sandbox_denial',
backend: result.sandboxType,
recoverable: true,
profileName: result.profileName,
message,
})
: new Error(message);
Object.assign(error, {
stdout: result.stdout,
stderr: result.stderr,
stdoutTruncated: result.stdoutTruncated,
stderrTruncated: result.stderrTruncated,
code,
...(result.sandboxType ? { sandboxType: result.sandboxType } : {}),
sandboxed: result.sandboxed === true,
...(sandboxDenied ? { reason: 'sandbox_denial', recoverable: true } : {}),
});
return error;
}
function assertRelativeGlobPattern(pattern: string): void {
if (isAbsolute(pattern) || pattern.split(/[\\/]+/).includes('..')) {
throw new Error('Glob pattern must stay inside session cwd');
}
}
export function classifyRuntimeResourceRef(path: string): 'runtime' | 'file' | 'unsupported' {
let url: URL;
try {
url = new URL(path);
} catch {
return path.trimStart().toLowerCase().startsWith('maka:') ? 'unsupported' : 'file';
}
if (url.protocol !== 'maka:') return 'file';
if (
url.hostname !== 'runtime' ||
url.username ||
url.password ||
url.port ||
!url.pathname ||
url.pathname === '/'
) {
return 'unsupported';
}
return 'runtime';
}
// Object.fromEntries creates own data properties, so special keys like
// "__proto__" are preserved instead of triggering the inherited setter.
function sortKeysDeep(value: unknown): unknown {
if (Array.isArray(value)) return value.map(sortKeysDeep);
if (value !== null && typeof value === 'object' && !(value instanceof Date)) {
return Object.fromEntries(
Object.keys(value)
.sort()
.map((key) => [key, sortKeysDeep((value as Record<string, unknown>)[key])]),
);
}
return value;
}