8f50dd324b
The fcntl() calls for FD_CLOEXEC were removed in a previous commit but the include statement was left behind. This removes the dead include.
172 lines
6.1 KiB
C++
172 lines
6.1 KiB
C++
#include "shell_tool.hpp"
|
|
#include "json_types.hpp"
|
|
#include <array>
|
|
#include <cstdio>
|
|
#include <iostream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <sys/wait.h>
|
|
#include <sys/poll.h>
|
|
#include <unistd.h>
|
|
#include <signal.h>
|
|
#include <cstring>
|
|
#include "utility.hpp"
|
|
|
|
Tool make_shell_tool(bool safe) {
|
|
std::string tool_name = safe ? "run_shell" : "run_dangerous_shell";
|
|
|
|
return Tool{
|
|
.name = tool_name,
|
|
.description = "Run a shell command and return its stdout and stderr. "
|
|
"Use for inspecting files, running programs, or querying the system.",
|
|
.parameters_schema = {
|
|
{"type", "object"},
|
|
{"properties", {
|
|
{"command", {
|
|
{"type", "string"},
|
|
{"description", "The shell command to execute. Runs via /bin/sh."}
|
|
}},
|
|
{"timeout_seconds", {
|
|
{"type", "integer"},
|
|
{"description", "Maximum seconds to wait for the command to complete. Defaults to 30."}
|
|
}}
|
|
}},
|
|
{"required", {"command"}}
|
|
},
|
|
.execute = create_tool_execute<ShellToolParams>([safe](const ShellToolParams& params) -> std::string {
|
|
|
|
if (safe) {
|
|
std::cerr << "Approve command: " << params.command << " ? [y/N]: ";
|
|
char response = 0;
|
|
std::cin.get(response);
|
|
if (response != 'y' && response != 'Y') {
|
|
return "Execution cancelled by user.";
|
|
}
|
|
}
|
|
|
|
// 1. Create a pipe for stdout
|
|
int pipefd[2];
|
|
if (pipe(pipefd) < 0) {
|
|
return "Error: pipe creation failed";
|
|
}
|
|
|
|
pid_t pid = fork();
|
|
if (pid < 0) {
|
|
close(pipefd[0]); close(pipefd[1]);
|
|
return "Error: fork failed";
|
|
}
|
|
|
|
if (pid == 0) {
|
|
// Child process
|
|
// Set process group so we can kill the whole tree
|
|
setpgid(0, 0);
|
|
|
|
// Close read end (child writes to the pipe)
|
|
close(pipefd[0]);
|
|
// Duplicate pipe write end to stdout
|
|
if (dup2(pipefd[1], STDOUT_FILENO) < 0) _exit(1);
|
|
close(pipefd[1]);
|
|
|
|
// Merge stderr to stdout so it doesn't block
|
|
dup2(STDOUT_FILENO, STDERR_FILENO);
|
|
|
|
// Execute command
|
|
std::string sh = "/bin/sh";
|
|
char* argv[] = { const_cast<char*>(sh.c_str()), const_cast<char*>("-c"),
|
|
const_cast<char*>(params.command.c_str()), nullptr };
|
|
execvp(argv[0], argv);
|
|
_exit(127); // exec failed
|
|
}
|
|
|
|
// Parent process
|
|
// Close write end (parent reads from pipe)
|
|
close(pipefd[1]);
|
|
|
|
std::string output;
|
|
std::array<char, 4096> buf;
|
|
struct pollfd pfd;
|
|
pfd.fd = pipefd[0];
|
|
pfd.events = POLLIN;
|
|
|
|
bool timed_out = false;
|
|
auto start_time = std::chrono::steady_clock::now();
|
|
|
|
while (true) {
|
|
// 1. Poll for stdout or process exit
|
|
// Wait 1 sec before we check timeout
|
|
int res = poll(&pfd, 1, 1000);
|
|
|
|
if (res == 0) {
|
|
auto diff = std::chrono::steady_clock::now() - start_time;
|
|
|
|
if (diff < std::chrono::seconds(params.timeout_seconds) &&
|
|
!ctrlc_request)
|
|
{
|
|
continue; // Not timed out yet
|
|
}
|
|
// Timeout!
|
|
// Kill the entire process group to stop sleep/dd/etc.
|
|
killpg(pid, SIGKILL);
|
|
timed_out = true;
|
|
break;
|
|
} else if (res < 0) {
|
|
if (errno == EINTR) continue; // Restart if interrupted by signal
|
|
return "Error: poll failed";
|
|
}
|
|
|
|
// 2. Check if process finished (POLLHUP)
|
|
if (pfd.revents & POLLHUP) {
|
|
// Data might still be available in buffer!
|
|
if (pfd.revents & POLLIN) {
|
|
// Read ALL remaining data in a loop
|
|
while (true) {
|
|
res = read(pfd.fd, buf.data(), buf.size());
|
|
if (res <= 0) break; // EOF or error
|
|
output += std::string(buf.data(), res);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
// 3. Read data if available - read once, don't block waiting for more
|
|
// If POLLHUP is not set, a background process may keep the pipe open,
|
|
// so we can't read in a loop (would block waiting for more data).
|
|
if (pfd.revents & POLLIN) {
|
|
res = read(pfd.fd, buf.data(), buf.size());
|
|
if (res > 0) {
|
|
output += std::string(buf.data(), res);
|
|
}
|
|
// If POLLHUP is also set, we're done
|
|
if (pfd.revents & POLLHUP) {
|
|
break;
|
|
}
|
|
// Otherwise, continue polling for more data or timeout
|
|
// Don't break here - continue the loop
|
|
}
|
|
|
|
}
|
|
|
|
// Check exit code
|
|
int status;
|
|
waitpid(pid, &status, 0);
|
|
|
|
if (ctrlc_request)
|
|
{
|
|
return "[user requested halt of program]\n" + output;
|
|
}
|
|
|
|
if (timed_out) {
|
|
return "[timed out after " + std::to_string(params.timeout_seconds) + "s]\n" + output;
|
|
}
|
|
|
|
int code = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
|
|
if (code != 0 && !timed_out) {
|
|
output = "[exit " + std::to_string(code) + "]\n" + output;
|
|
}
|
|
|
|
close(pipefd[0]);
|
|
return output.empty() ? "(no output)" : output;
|
|
})
|
|
};
|
|
}
|