@agentforce/adk
Version:
AgentForce Agent Development Kit - A powerful framework for building AI agents and servers
280 lines (256 loc) • 8.94 kB
text/typescript
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,
});
});
});
},
};