blob: b1c18be422432cf8bfb297b083e1b37c2424936d [file]
import { randomUUID } from 'node:crypto';
import { realpath } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname } from 'node:path';
import {
canReadPath,
canWritePath,
compilePermissionProfile,
type ExecutionBoundary,
type PermissionMode,
type PermissionProfile,
type SandboxBoundaryExpansion,
} from '@maka/core';
import { normalizeSandboxBoundaryPath } from '../sandbox-boundary-path.js';
import { pinExistingLinuxProfilePath } from '../sandbox/linux-profile-path.js';
import type { SandboxManager } from '../sandbox/sandbox-manager.js';
import type { SandboxPlatform } from '../sandbox/types.js';
import type { FilesystemWorkerLaunchSpecProvider } from './launch-spec.js';
import {
FILESYSTEM_WORKER_DEFAULT_TIMEOUT_MS,
runFilesystemWorkerProcess,
type FilesystemWorkerProcessRunner,
} from './process-runner.js';
import {
FILESYSTEM_WORKER_PROTOCOL_VERSION,
FilesystemWorkerOperationSchema,
parseFilesystemWorkerResponse,
type FilesystemWorkerErrorCode,
type FilesystemWorkerOperation,
type FilesystemWorkerResult,
type FilesystemWorkerTarget,
} from './protocol.js';
export const FILESYSTEM_WORKER_MAX_REQUEST_BYTES = 16 * 1024 * 1024;
export type FilesystemWorkerClientOperation = FilesystemWorkerOperation extends infer Operation
? Operation extends { cwd: string }
? Omit<Operation, 'cwd'>
: never
: never;
export interface FilesystemWorkerClientInput {
getLaunchSpec: FilesystemWorkerLaunchSpecProvider;
sandboxManager: SandboxManager;
runProcess?: FilesystemWorkerProcessRunner;
newId?: () => string;
timeoutMs?: number;
platform?: SandboxPlatform;
}
export interface FilesystemWorkerExecuteInput {
operation: FilesystemWorkerClientOperation;
cwd: string;
executionBoundary?: ExecutionBoundary;
mode?: PermissionMode;
/** Explicit embedding policy. Mode-based defaults are compiled only when omitted. */
permissionProfile?: PermissionProfile;
abortSignal?: AbortSignal;
}
export type FilesystemWorkerClientErrorReason =
| 'invalid_operation'
| 'invalid_request'
| 'request_overflow'
| 'worker_bundle_unavailable'
| 'runtime_executable_unavailable'
| 'spawn_failed'
| 'timeout'
| 'aborted'
| 'response_overflow'
| 'worker_crashed'
| 'invalid_response'
| 'response_id_mismatch'
| 'response_kind_mismatch'
| FilesystemWorkerErrorCode
| 'unsupported_platform'
| 'backend_not_available'
| 'backend_not_implemented'
| 'sandbox_required'
| 'sandbox_boundary_required';
export class FilesystemWorkerClientError extends Error {
readonly code = 'SANDBOX_FILESYSTEM_OPERATION_FAILED';
readonly domain = 'filesystem' as const;
readonly reason: FilesystemWorkerClientErrorReason;
readonly stage: 'validation' | 'transform' | 'launch' | 'protocol' | 'operation';
readonly recoverable: boolean;
readonly requestId?: string;
readonly backend?: 'none' | 'macos-seatbelt' | 'linux';
readonly profileName?: string;
readonly requiredExpansion?: SandboxBoundaryExpansion;
constructor(input: {
reason: FilesystemWorkerClientErrorReason;
stage: FilesystemWorkerClientError['stage'];
message?: string;
recoverable?: boolean;
requestId?: string;
backend?: 'none' | 'macos-seatbelt' | 'linux';
profileName?: string;
requiredExpansion?: SandboxBoundaryExpansion;
}) {
super(input.message ?? `Filesystem worker failed: ${input.reason}.`);
this.name = 'FilesystemWorkerClientError';
this.reason = input.reason;
this.stage = input.stage;
this.recoverable = input.recoverable ?? false;
this.requestId = input.requestId;
this.backend = input.backend;
this.profileName = input.profileName;
this.requiredExpansion = input.requiredExpansion;
}
}
export class FilesystemWorkerClient {
private readonly runProcess: FilesystemWorkerProcessRunner;
private readonly newId: () => string;
private readonly timeoutMs: number;
constructor(private readonly input: FilesystemWorkerClientInput) {
this.runProcess = input.runProcess ?? runFilesystemWorkerProcess;
this.newId = input.newId ?? randomUUID;
this.timeoutMs = input.timeoutMs ?? FILESYSTEM_WORKER_DEFAULT_TIMEOUT_MS;
}
async execute(input: FilesystemWorkerExecuteInput): Promise<FilesystemWorkerResult> {
const requestId = this.newId();
if (input.abortSignal?.aborted) throw clientError('aborted', 'launch', requestId);
if (input.executionBoundary && input.executionBoundary.kind !== 'managed') {
throw clientError(
'invalid_request',
'validation',
requestId,
'Filesystem worker execution requires a managed boundary.',
);
}
const canonicalCwd = await realpath(input.cwd).catch(() => {
throw clientError(
'invalid_operation',
'validation',
requestId,
'Session cwd is unavailable.',
);
});
const parsedOperation = FilesystemWorkerOperationSchema.safeParse({
...input.operation,
cwd: canonicalCwd,
});
if (!parsedOperation.success) throw clientError('invalid_operation', 'validation', requestId);
const access = operationAccess(parsedOperation.data.kind);
const target = await normalizeSandboxBoundaryPath({
path: parsedOperation.data.path,
access,
scope: operationScope(parsedOperation.data.kind),
cwd: canonicalCwd,
}).catch(() => {
throw clientError('invalid_operation', 'validation', requestId);
});
const compiled =
input.executionBoundary?.kind === 'managed'
? {
profile: input.executionBoundary.profile,
workspaceRoots: [canonicalCwd],
}
: input.permissionProfile
? {
profile: input.permissionProfile,
workspaceRoots: [canonicalCwd],
}
: compilePermissionProfile({ mode: input.mode ?? 'ask', cwd: canonicalCwd });
const effectiveProfile = compiled.profile;
const platform = this.input.platform ?? process.platform;
const runtimeWritableRoots = filesystemWorkerRuntimeWritableRoots({
platform,
access,
enforcementPath: target.enforcementPath,
targetType: target.targetType,
});
const pathContext = {
workspaceRoots: compiled.workspaceRoots,
tmpdir: await canonicalPath(tmpdir()),
slashTmp: await canonicalPath('/tmp'),
...(runtimeWritableRoots ? { runtimeWritableRoots } : {}),
};
const allowed =
access === 'write'
? canWritePath(effectiveProfile, target.enforcementPath, pathContext)
: canReadPath(effectiveProfile, target.enforcementPath, pathContext);
if (!allowed) {
throw clientError(
input.executionBoundary?.kind === 'managed' ? 'sandbox_boundary_required' : 'path_denied',
'validation',
requestId,
undefined,
true,
input.executionBoundary?.kind === 'managed'
? {
requiredExpansion: {
filesystem: {
entries: [
{
path: target.enforcementPath,
access,
scope: target.scope,
},
],
},
},
}
: {},
);
}
const operationBoundary = {
filesystem: {
entries: [{ path: target.enforcementPath, access, scope: target.scope }],
},
} as const;
const operation = FilesystemWorkerOperationSchema.parse({
...parsedOperation.data,
path: target.enforcementPath,
});
const request = {
version: FILESYSTEM_WORKER_PROTOCOL_VERSION,
requestId,
operation,
operationBoundary,
expectedTarget: {
enforcementPath: target.enforcementPath,
access,
scope: target.scope,
targetType: target.targetType,
},
} as const;
const requestJson = JSON.stringify(request);
if (Buffer.byteLength(requestJson, 'utf8') > FILESYSTEM_WORKER_MAX_REQUEST_BYTES) {
throw clientError('request_overflow', 'validation', requestId);
}
const launch = await this.input.getLaunchSpec();
if (!launch.ok) throw clientError(launch.reason, 'launch', requestId, launch.message);
const workerProfile = deriveWorkerProfile(effectiveProfile, operationBoundary);
const pinnedTarget =
platform === 'linux' && target.targetType !== 'missing'
? (() => {
try {
return pinExistingLinuxProfilePath({
path: target.enforcementPath,
access,
targetType: target.targetType,
childFd: 4,
});
} catch {
throw clientError(
'path_changed',
'validation',
requestId,
'The approved filesystem target changed before sandbox launch.',
);
}
})()
: undefined;
if (platform === 'linux' && target.targetType !== 'missing' && !pinnedTarget) {
throw clientError(
'path_changed',
'validation',
requestId,
'The approved filesystem target changed before sandbox launch.',
);
}
const pinnedRuntimeWritableRoot =
platform === 'linux' && target.targetType === 'missing' && runtimeWritableRoots?.[0]
? (() => {
try {
return pinExistingLinuxProfilePath({
path: runtimeWritableRoots[0],
access: 'write',
targetType: 'directory',
childFd: 4,
});
} catch {
throw clientError(
'path_changed',
'validation',
requestId,
'The approved filesystem target parent changed before sandbox launch.',
);
}
})()
: undefined;
if (
platform === 'linux' &&
target.targetType === 'missing' &&
runtimeWritableRoots &&
!pinnedRuntimeWritableRoot
) {
throw clientError(
'path_changed',
'validation',
requestId,
'The approved filesystem target parent changed before sandbox launch.',
);
}
let transformed: ReturnType<SandboxManager['transform']>;
try {
transformed = this.input.sandboxManager.transform({
platform,
command: {
program: launch.spec.program,
args: launch.spec.args,
cwd: canonicalCwd,
env: launch.spec.env,
profile: workerProfile,
pathContext: {
...pathContext,
runtimeReadableRoots: launch.spec.runtimeReadableRoots,
executableRoots: launch.spec.executableRoots,
...(pinnedTarget
? {
pinnedProfilePaths: [
{
path: pinnedTarget.path,
access: pinnedTarget.access,
fd: pinnedTarget.childFd,
sourceFd: pinnedTarget.sourceFd,
releaseSource: pinnedTarget.releaseSource,
},
],
}
: {}),
...(pinnedRuntimeWritableRoot
? {
pinnedRuntimeWritableRoots: [
{
path: pinnedRuntimeWritableRoot.path,
fd: pinnedRuntimeWritableRoot.childFd,
sourceFd: pinnedRuntimeWritableRoot.sourceFd,
releaseSource: pinnedRuntimeWritableRoot.releaseSource,
},
],
}
: {}),
},
},
});
} catch (error) {
pinnedTarget?.releaseSource();
pinnedRuntimeWritableRoot?.releaseSource();
throw error;
}
if (!transformed.ok) {
pinnedTarget?.releaseSource();
pinnedRuntimeWritableRoot?.releaseSource();
throw clientError(transformed.reason, 'transform', requestId, transformed.message, false, {
backend: transformed.sandboxType,
profileName: effectiveProfile.name ?? effectiveProfile.type,
});
}
let processResult: Awaited<ReturnType<FilesystemWorkerProcessRunner>>;
try {
processResult = await this.runProcess({
argv: transformed.exec.argv,
cwd: transformed.exec.cwd,
env: transformed.exec.env ?? {},
stdin: requestJson,
...(transformed.exec.fdInputs ? { fdInputs: transformed.exec.fdInputs } : {}),
timeoutMs: this.timeoutMs,
...(input.abortSignal ? { abortSignal: input.abortSignal } : {}),
});
} catch {
throw clientError('spawn_failed', 'launch', requestId);
} finally {
pinnedTarget?.releaseSource();
pinnedRuntimeWritableRoot?.releaseSource();
}
if (processResult.timedOut) throw clientError('timeout', 'launch', requestId);
if (processResult.aborted) throw clientError('aborted', 'launch', requestId);
if (processResult.responseOverflow) throw clientError('response_overflow', 'launch', requestId);
if (processResult.exitCode !== 0) {
throw clientError(
'worker_crashed',
'launch',
requestId,
processResult.stderrTail || undefined,
);
}
let response: ReturnType<typeof parseFilesystemWorkerResponse>;
try {
response = parseFilesystemWorkerResponse(JSON.parse(processResult.stdout));
} catch {
throw clientError('invalid_response', 'protocol', requestId);
}
if (response.requestId !== requestId)
throw clientError('response_id_mismatch', 'protocol', requestId);
if (!response.ok) {
throw clientError(
response.error.code,
'operation',
requestId,
response.error.message,
response.error.code === 'not_found' || response.error.code === 'edit_conflict',
{
backend: transformed.exec.sandboxType,
profileName: effectiveProfile.name ?? effectiveProfile.type,
},
);
}
if (
response.result.kind !== operation.kind &&
!(operation.kind === 'read' && response.result.kind === 'read_image')
) {
throw clientError('response_kind_mismatch', 'protocol', requestId);
}
return response.result;
}
}
/** @internal Runtime-only widening for a trusted, single-operation worker. */
export function filesystemWorkerRuntimeWritableRoots(input: {
platform: SandboxPlatform;
access: 'read' | 'write';
enforcementPath: string;
targetType: FilesystemWorkerTarget['targetType'];
}): readonly string[] | undefined {
if (input.platform !== 'linux' || input.access !== 'write' || input.targetType !== 'missing') {
return undefined;
}
return [dirname(input.enforcementPath)];
}
function deriveWorkerProfile(
profile: PermissionProfile,
operationBoundary: {
readonly filesystem: {
readonly entries: readonly [
{
readonly path: string;
readonly access: 'read' | 'write';
readonly scope: 'exact' | 'subtree';
},
];
};
},
): PermissionProfile {
if (profile.type !== 'managed' || profile.fileSystem.kind !== 'restricted') return profile;
const target = operationBoundary.filesystem.entries[0];
return {
...profile,
fileSystem: {
...profile.fileSystem,
entries: [
...profile.fileSystem.entries.filter((entry) => entry.access === 'deny'),
{
kind: 'path',
path: target.path,
access: target.access,
match: target.scope,
},
],
},
network: { kind: 'restricted' },
};
}
function operationAccess(kind: FilesystemWorkerOperation['kind']): 'read' | 'write' {
return kind === 'write' || kind === 'edit' || kind === 'format_json' ? 'write' : 'read';
}
function operationScope(kind: FilesystemWorkerOperation['kind']): 'exact' | 'subtree' | 'auto' {
if (kind === 'glob') return 'subtree';
return kind === 'grep' ? 'auto' : 'exact';
}
async function canonicalPath(path: string): Promise<string> {
return await realpath(path).catch(() => path);
}
function clientError(
reason: FilesystemWorkerClientErrorReason,
stage: FilesystemWorkerClientError['stage'],
requestId: string,
message?: string,
recoverable = false,
metadata: {
backend?: 'none' | 'macos-seatbelt' | 'linux';
profileName?: string;
requiredExpansion?: SandboxBoundaryExpansion;
} = {},
): FilesystemWorkerClientError {
return new FilesystemWorkerClientError({
reason,
stage,
requestId,
message,
recoverable,
...metadata,
});
}