ducklm/app/tools/shell_exec.py

66 lines
2.4 KiB
Python

from __future__ import annotations
from app.core.contracts import ToolResult, UserTask
from app.tools.base import BaseTool
from app.tools.sandbox import ToolSandbox
def _detect_sudo_auth_failure(output: str) -> bool:
normalized = output.lower()
return any(
marker in normalized
for marker in (
"incorrect password",
"incorrect password attempt",
"sudo: no password was provided",
"sudo: password incorrect",
"sorry, try again",
"authentication failure",
"wrong password",
)
)
class ShellExecTool(BaseTool):
name = "shell_exec"
def __init__(self, sandbox: ToolSandbox) -> None:
self._sandbox = sandbox
def execute(self, task: UserTask, args: dict[str, object]) -> ToolResult:
command = str(args.get("command", "")).strip()
if not command:
return ToolResult(tool=self.name, ok=False, error="Missing command", metadata={"exit_code": -1})
cwd = args.get("cwd")
stdin_secret = args.get("stdin_secret")
password = args.get("password")
output_callback = args.get("__output_callback")
if password:
command = f'echo "{password}" | sudo -S {command}'
completed = self._sandbox.run_shell(
command=command,
cwd=str(cwd) if cwd else None,
stdin_data=str(stdin_secret) if stdin_secret is not None else None,
output_callback=output_callback if callable(output_callback) else None,
)
output = completed.stdout if completed.returncode == 0 else completed.stderr or completed.stdout
error_output = completed.stderr or completed.stdout
sudo_auth_failed = completed.returncode != 0 and _detect_sudo_auth_failure(
f"{completed.stdout}\n{completed.stderr}"
)
needs_sudo = completed.returncode != 0 and "permission denied" in error_output.lower() and not sudo_auth_failed
return ToolResult(
tool=self.name,
ok=completed.returncode == 0,
output=output,
error=None if completed.returncode == 0 else f"Command failed with exit code {completed.returncode}",
metadata={
"exit_code": completed.returncode,
"needs_sudo": needs_sudo,
"sudo_auth_failed": sudo_auth_failed,
},
)