blob: 08cb9e42ef62d7f358c6979e50b68fa05b49d6b7 [file]
import type {
WorkspaceExecInput,
WorkspaceExecResult,
WorkspaceExistingPathResolver,
WorkspaceExecutorFactsProvider,
WorkspaceExecutorFacts,
WorkspaceResolvePathInput,
WorkspaceResolvePathResult,
WorkspaceGlobInput,
WorkspaceGlobResult,
WorkspaceGrepInput,
WorkspaceGrepResult,
WorkspaceWritablePathResolver,
WorkspaceWriteExecutor,
WorkspaceWriteFileInput,
WorkspaceWriteFileResult,
WorkspaceWriteLockKeyInput,
WorkspaceWriteLockKeyResult,
} from '@maka/runtime/workspace-executor';
import { posix as pathPosix } from 'node:path';
import { isPathInside } from '@maka/runtime';
import type { IsolatedToolExecutor } from './isolation.js';
export const ISOLATED_WORKSPACE_EXECUTOR_FACTS: WorkspaceExecutorFacts = {
isolation: 'none',
writesAffectHost: true,
writeBack: 'direct',
network: 'host',
secrets: 'host_env',
};
export const EXTERNAL_ISOLATED_WORKSPACE_EXECUTOR_FACTS: WorkspaceExecutorFacts = {
isolation: 'remote',
writesAffectHost: false,
writeBack: 'diff_review',
network: 'sandbox',
secrets: 'brokered',
};
export type IsolatedWorkspaceExecutorAdapter = WorkspaceExecutorFactsProvider &
WorkspaceExistingPathResolver &
WorkspaceWritablePathResolver &
WorkspaceWriteExecutor;
export function isolatedToolExecutorToWorkspaceExecutor(
executor: IsolatedToolExecutor,
facts: WorkspaceExecutorFacts = ISOLATED_WORKSPACE_EXECUTOR_FACTS,
): IsolatedWorkspaceExecutorAdapter {
const adapter = {
facts,
exec: unsupportedExec,
writeFile: (input: WorkspaceWriteFileInput) => isolatedWriteFile(executor, input),
resolveExistingPath: isolatedResolvePath,
resolveWritablePath: isolatedResolvePath,
writeLockKey: isolatedWriteLockKey,
globFiles: unsupportedGlobFiles,
grepFiles: unsupportedGrepFiles,
};
return adapter;
}
async function unsupportedExec(_input: WorkspaceExecInput): Promise<WorkspaceExecResult> {
throw new Error(
'IsolatedToolExecutor adapter does not adapt Bash; WorkspaceExecutor.exec requires abort, timeout, and live-output controls that IsolatedToolExecutor cannot preserve. Use buildIsolatedHeadlessTools instead.',
);
}
async function isolatedWriteFile(
executor: IsolatedToolExecutor,
input: WorkspaceWriteFileInput,
): Promise<WorkspaceWriteFileResult> {
if (!executor.writeFile) {
throw new Error(
'IsolatedToolExecutor adapter requires native writeFile for WorkspaceExecutor.writeFile',
);
}
return await executor.writeFile({
cwd: input.cwd,
path: input.path,
content: input.content,
});
}
async function isolatedResolvePath(
input: WorkspaceResolvePathInput,
): Promise<WorkspaceResolvePathResult> {
return { path: resolveIsolatedWorkspacePath(input.cwd, input.path, input.label) };
}
async function isolatedWriteLockKey(
input: WorkspaceWriteLockKeyInput,
): Promise<WorkspaceWriteLockKeyResult> {
return {
key: JSON.stringify([
pathPosix.normalize(input.cwd),
resolveIsolatedWorkspacePath(input.cwd, input.path, 'Write path'),
]),
};
}
async function unsupportedGlobFiles(_input: WorkspaceGlobInput): Promise<WorkspaceGlobResult> {
throw new Error(
'IsolatedToolExecutor adapter does not adapt Glob; WorkspaceExecutor.globFiles requires search and limit controls that IsolatedToolExecutor cannot preserve. Use buildIsolatedHeadlessTools instead.',
);
}
async function unsupportedGrepFiles(_input: WorkspaceGrepInput): Promise<WorkspaceGrepResult> {
throw new Error(
'IsolatedToolExecutor adapter does not adapt Grep; WorkspaceExecutor.grepFiles requires abort, timeout, max-count, and limit controls that IsolatedToolExecutor cannot preserve. Use buildIsolatedHeadlessTools instead.',
);
}
function resolveIsolatedWorkspacePath(cwd: string, inputPath: string, label: string): string {
// Lexical preflight only. The isolated workspace may live in a remote/container
// filesystem that this process cannot realpath; symlink and mount escape checks
// are the responsibility of the native isolated backend.
if (inputPath.length === 0 || /^[A-Za-z]:[\\/]/.test(inputPath)) {
throw new Error(`${label} must stay inside workspace`);
}
const root = pathPosix.normalize(cwd);
const target = inputPath.startsWith('/')
? pathPosix.normalize(inputPath)
: pathPosix.resolve(root, inputPath);
if (
!isPathInside(root, target, {
relative: pathPosix.relative,
isAbsolute: pathPosix.isAbsolute,
sep: pathPosix.sep,
})
) {
throw new Error(`${label} must stay inside workspace`);
}
return target;
}