n8n/packages/@n8n/fs-proxy/src/tools/shell/shell-execute.ts
oleg 629826ca1d
feat: Instance AI and local gateway modules (no-changelog) (#27206)
Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
Co-authored-by: Albert Alises <albert.alises@gmail.com>
Co-authored-by: Jaakko Husso <jaakko@n8n.io>
Co-authored-by: Dimitri Lavrenük <20122620+dlavrenuek@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Tuukka Kantola <Tuukkaa@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com>
Co-authored-by: Raúl Gómez Morales <raul00gm@gmail.com>
Co-authored-by: Elias Meire <elias@meire.dev>
Co-authored-by: Dimitri Lavrenük <dimitri.lavrenuek@n8n.io>
Co-authored-by: Tomi Turtiainen <10324676+tomi@users.noreply.github.com>
Co-authored-by: Mutasem Aldmour <mutasem@n8n.io>
2026-04-01 21:33:38 +03:00

105 lines
3 KiB
TypeScript

import { SandboxManager, type SandboxRuntimeConfig } from '@anthropic-ai/sandbox-runtime';
import { rgPath } from '@vscode/ripgrep';
import { spawn } from 'child_process';
import { z } from 'zod';
import type { CallToolResult, ToolDefinition } from '../types';
import { formatCallToolResult, formatErrorResult } from '../utils';
import { buildShellResource } from './build-shell-resource';
async function initializeSandbox({ dir }: { dir: string }) {
const config: SandboxRuntimeConfig = {
ripgrep: {
command: rgPath,
},
network: {
allowedDomains: [],
deniedDomains: [],
},
filesystem: {
denyRead: ['~/.ssh'],
allowRead: [],
allowWrite: [dir],
denyWrite: [],
},
};
await SandboxManager.initialize(config);
}
const inputSchema = z.object({
command: z.string().describe('Shell command to execute'),
timeout: z.number().int().optional().describe('Timeout in milliseconds (default: 30000)'),
cwd: z.string().optional().describe('Working directory for the command'),
});
export const shellExecuteTool: ToolDefinition<typeof inputSchema> = {
name: 'shell_execute',
description: 'Execute a shell command and return stdout, stderr, and exit code',
inputSchema,
annotations: { destructiveHint: true },
getAffectedResources({ command }) {
return [
{
toolGroup: 'shell' as const,
resource: buildShellResource(command),
description: `Execute shell command: ${command}`,
},
];
},
async execute({ command, timeout = 30_000, cwd }, { dir }) {
return await runCommand(command, { timeout, dir, cwd: cwd ?? dir });
},
};
async function spawnCommand(command: string, { dir, cwd }: { dir: string; cwd?: string }) {
const isWindows = process.platform === 'win32';
const isMac = process.platform === 'darwin';
if (isWindows) {
return spawn('cmd.exe', ['/C', command], { cwd });
}
if (isMac) {
await initializeSandbox({ dir });
const sandboxedCommand = await SandboxManager.wrapWithSandbox(command);
return spawn(sandboxedCommand, { shell: true, cwd });
}
return spawn('sh', ['-c', command], { cwd });
}
async function runCommand(
command: string,
{ timeout, cwd, dir }: { timeout: number; dir: string; cwd?: string },
): Promise<CallToolResult> {
return await new Promise<CallToolResult>((resolve, reject) => {
spawnCommand(command, { dir, cwd })
.then((child) => {
let stdout = '';
let stderr = '';
child.stdout?.on('data', (chunk: Buffer) => {
stdout += String(chunk);
});
child.stderr?.on('data', (chunk: Buffer) => {
stderr += String(chunk);
});
const timer = setTimeout(() => {
child.kill();
resolve(formatCallToolResult({ stdout, stderr, exitCode: null, timedOut: true }));
}, timeout);
child.on('close', (code) => {
clearTimeout(timer);
resolve(formatCallToolResult({ stdout, stderr, exitCode: code }));
});
child.on('error', (error) => {
clearTimeout(timer);
resolve(formatErrorResult(`Failed to start process: ${error.message}`));
});
})
.catch(reject);
});
}