Acme-Claude-Shell
view release on metacpan or search on metacpan
lib/Acme/Claude/Shell/Tools.pm view on Meta::CPAN
my $colorful = $session->colorful;
# Stop spinner before prompting for approval
if ($session->can('_spinner') && $session->_spinner) {
stop_spinner($session->_spinner);
$session->_spinner(undef);
}
# Prompt for approval before executing
my ($approved, $new_command) = _confirm_command($session, $command);
unless ($approved) {
my $future = $loop->new_future;
$future->done(_mcp_result("User cancelled command", 1));
return $future;
}
# Use potentially edited command
$command = $new_command if defined $new_command;
# Start execution spinner
if ($colorful) {
$session->_spinner(start_spinner("Executing...", $loop));
}
# Record in history
push @{$session->_history}, {
time => _timestamp(),
command => $command,
status => 'running',
};
my $future = $loop->new_future;
my $stdout = '';
my $stderr = '';
my $process = IO::Async::Process->new(
command => [ '/bin/sh', '-c', $command ],
($dir && -d $dir ? (setup => [ chdir => $dir ]) : ()),
stdout => {
into => \$stdout,
},
stderr => {
into => \$stderr,
},
on_finish => sub {
my ($self, $exitcode) = @_;
my $exit_status = $exitcode >> 8;
if ($exit_status != 0) {
$session->_history->[-1]{status} = "exit $exit_status";
my $output = $stderr || $stdout || "Command failed with exit code $exit_status";
$future->done(_mcp_result($output));
} else {
$session->_history->[-1]{status} = 'success';
$future->done(_mcp_result($stdout // ''));
}
},
on_exception => sub {
my ($self, $exception, $errno, $exitcode) = @_;
$session->_history->[-1]{status} = 'error';
$future->done(_mcp_result("Error: $exception", 1));
},
);
$loop->add($process);
return $future;
}
# Helper to format tool results in MCP format
sub _mcp_result {
my ($text, $is_error) = @_;
return {
content => [{ type => 'text', text => $text }],
is_error => $is_error ? 1 : 0,
};
}
# Dangerous command patterns
my @DANGEROUS_PATTERNS = (
{ pattern => qr/\brm\s+(-[rf]+|--recursive|--force)/i,
reason => 'Recursive or forced file deletion' },
{ pattern => qr/\bsudo\b/,
reason => 'Superuser command' },
{ pattern => qr/\bmkfs\b/,
reason => 'Filesystem formatting' },
{ pattern => qr/\bdd\b.*\bof=/,
reason => 'Direct disk write' },
{ pattern => qr/>\s*\/dev\//,
reason => 'Writing to device file' },
{ pattern => qr/\bchmod\s+(-R\s+)?777\b/,
reason => 'World-writable permissions' },
{ pattern => qr/\bchown\s+-R\b.*\//,
reason => 'Recursive ownership change' },
{ pattern => qr/\bkill\s+-9\b/,
reason => 'Forceful process termination' },
{ pattern => qr/\b(reboot|shutdown|halt|poweroff)\b/,
reason => 'System shutdown/reboot' },
{ pattern => qr/\bformat\b/,
reason => 'Disk formatting' },
{ pattern => qr/:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;/,
reason => 'Fork bomb detected' },
{ pattern => qr/\bwget\b.*\|\s*(ba)?sh/i,
reason => 'Piping remote script to shell' },
{ pattern => qr/\bcurl\b.*\|\s*(ba)?sh/i,
reason => 'Piping remote script to shell' },
);
sub _check_dangerous {
my ($command) = @_;
for my $check (@DANGEROUS_PATTERNS) {
if ($command =~ $check->{pattern}) {
return $check;
}
}
return undef;
}
# Confirm command with user before executing
# Returns ($approved, $new_command) - $new_command is set if user edited it
sub _confirm_command {
my ($session, $command) = @_;
my $colorful = $session->colorful;
# Check for dangerous patterns
my $danger = _check_dangerous($command);
print "\n";
if ($danger && $session->safe_mode) {
if ($colorful) {
status('warning', "Potentially dangerous command detected!");
print colored(['yellow'], " Reason: $danger->{reason}\n");
} else {
( run in 0.960 second using v1.01-cache-2.11-cpan-39bf76dae61 )