UNPKG

@agentforce/adk

Version:

AgentForce Agent Development Kit - A powerful framework for building AI agents and servers

280 lines (256 loc) 8.94 kB
import type { ToolImplementation } from "../types"; import { spawn } from "child_process"; import { resolve } from "path"; /** * Maximum output size in bytes (1MB default) */ const MAX_OUTPUT_BYTES_DEFAULT = 1_000_000; /** * Default timeout in milliseconds (15 seconds) */ const TIMEOUT_MS_DEFAULT = 15_000; /** * Allowlisted commands that can be executed * This can be made configurable in the future */ const ALLOWLIST = new Set([ "node", "bun", "npm", "pnpm", "yarn", "git", "ls", "cat", "grep", "sed", "awk", "echo", "which", "whoami", "pwd", "date", "curl", "wget", "tar", "zip", "unzip", "mkdir", "rm", "cp", "mv", "find", "sort", "uniq", "head", "tail", "wc", "diff", "patch", ]); /** * Execute a system command safely without shell injection * Enforces allowlist, sandboxing, timeouts, and output limits */ export const os_exec: ToolImplementation = { definition: { type: "function", function: { name: "os_exec", description: "Execute a system command safely without a shell. Captures stdout/stderr with limits and timeout. Commands are allowlisted for security.", parameters: { type: "object", properties: { command: { type: "string", description: "Executable name (must be in allowlist: node, bun, npm, git, ls, cat, grep, etc.)", }, args: { type: "string", description: "Arguments to pass to the command as JSON array (e.g., '[\"--version\", \"--help\"]'). Defaults to empty array.", }, cwd: { type: "string", description: "Working directory (must be inside project root). Defaults to project root.", }, env: { type: "string", description: "Extra environment variables as JSON object (e.g., '{\"NODE_ENV\":\"dev\"}'). Defaults to empty object.", }, timeout_ms: { type: "number", description: "Timeout in milliseconds (default 15000)", }, max_output_bytes: { type: "number", description: "Maximum combined stdout+stderr bytes (default 1MB)", }, }, required: ["command"], }, }, }, execute: async (args: Record<string, any>) => { const command = String(args.command || "").trim(); const timeoutMs = Number.isFinite(args.timeout_ms) ? Number(args.timeout_ms) : TIMEOUT_MS_DEFAULT; const maxBytes = Number.isFinite(args.max_output_bytes) ? Number(args.max_output_bytes) : MAX_OUTPUT_BYTES_DEFAULT; // Parse arguments array from string let argv: string[] = []; if (args.args) { try { const parsed = JSON.parse(String(args.args)); if (Array.isArray(parsed)) { argv = parsed.map(String); } else { return { success: false, error: "args must be a JSON array of strings", }; } } catch { return { success: false, error: "args must be valid JSON array", }; } } // Parse environment variables from string let envVars: Record<string, string> = {}; if (args.env) { try { const parsed = JSON.parse(String(args.env)); if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) { envVars = parsed; } else { return { success: false, error: "env must be a JSON object with string values", }; } } catch { return { success: false, error: "env must be valid JSON object", }; } } // Validate command if (!command) { return { success: false, error: "command is required and cannot be empty", }; } if (!ALLOWLIST.has(command)) { return { success: false, error: `Command '${command}' is not allowed. Allowed commands: ${Array.from(ALLOWLIST).sort().join(", ")}`, }; } // Validate and sanitize working directory const projectRoot = resolve(process.cwd()); const cwdArg = args.cwd ? resolve(String(args.cwd)) : projectRoot; if (!cwdArg.startsWith(projectRoot)) { return { success: false, error: "Working directory must be inside project root for security", projectRoot, requestedCwd: cwdArg, }; } // Prepare environment const childEnv = { ...process.env, ...envVars }; // Execute command return await new Promise((resolve) => { const child = spawn(command, argv, { cwd: cwdArg, env: childEnv, shell: false, stdio: ["ignore", "pipe", "pipe"], }); let stdoutBufs: Buffer[] = []; let stderrBufs: Buffer[] = []; let totalBytes = 0; let killedForLimit = false; let timedOut = false; const killForLimit = (): void => { if (!child.killed) { child.kill("SIGKILL"); } killedForLimit = true; }; const onData = (chunk: Buffer, target: Buffer[]): void => { totalBytes += chunk.length; if (totalBytes > maxBytes) { killForLimit(); return; } target.push(chunk); }; child.stdout?.on("data", (chunk: Buffer) => onData(chunk, stdoutBufs)); child.stderr?.on("data", (chunk: Buffer) => onData(chunk, stderrBufs)); // Set timeout const timer = setTimeout(() => { if (!child.killed) { child.kill("SIGKILL"); } timedOut = true; }, timeoutMs); child.on("close", (code, signal) => { clearTimeout(timer); const stdoutStr = Buffer.concat(stdoutBufs).toString("utf8"); const stderrStr = Buffer.concat(stderrBufs).toString("utf8"); if (timedOut) { resolve({ success: false, error: "Command timed out", timedOut: true, timeoutMs, exitCode: null, signal: signal || "SIGKILL", stdout: stdoutStr, stderr: stderrStr, command, args: argv, }); return; } if (killedForLimit) { resolve({ success: false, error: "Output exceeded maximum size limit", outputLimitExceeded: true, maxBytes, exitCode: null, signal: signal || "SIGKILL", stdout: stdoutStr, stderr: stderrStr, command, args: argv, }); return; } resolve({ success: code === 0, exitCode: code, signal: signal || null, stdout: stdoutStr, stderr: stderrStr, command, args: argv, cwd: cwdArg, }); }); child.on("error", (error) => { clearTimeout(timer); resolve({ success: false, error: `Failed to start command: ${error.message}`, spawnError: true, command, args: argv, }); }); }); }, };