mirror of
https://github.com/aljazceru/Auto-GPT.git
synced 2025-12-24 01:14:22 +01:00
* Use JSON for command signature Signed-off-by: Merwane Hamadi <merwanehamadi@gmail.com> * Improve plugin backward compatibility (#4716) * Fixed plugin test Signed-off-by: Merwane Hamadi <merwanehamadi@gmail.com> * Fix Docker-CI Signed-off-by: Merwane Hamadi <merwanehamadi@gmail.com> * Put back commands, clean typing and signatures Signed-off-by: Merwane Hamadi <merwanehamadi@gmail.com> --------- Signed-off-by: Merwane Hamadi <merwanehamadi@gmail.com> Co-authored-by: Erik Peterson <e@eriklp.com> Co-authored-by: Luke K (pr-0f3t) <2609441+lc0rp@users.noreply.github.com>
295 lines
8.9 KiB
Python
295 lines
8.9 KiB
Python
"""Execute code in a Docker container"""
|
|
import os
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import docker
|
|
from docker.errors import ImageNotFound
|
|
|
|
from autogpt.agent.agent import Agent
|
|
from autogpt.command_decorator import command
|
|
from autogpt.config import Config
|
|
from autogpt.logs import logger
|
|
from autogpt.setup import CFG
|
|
from autogpt.workspace.workspace import Workspace
|
|
|
|
ALLOWLIST_CONTROL = "allowlist"
|
|
DENYLIST_CONTROL = "denylist"
|
|
|
|
|
|
@command(
|
|
"execute_python_code",
|
|
"Create a Python file and execute it",
|
|
{
|
|
"code": {
|
|
"type": "string",
|
|
"description": "The Python code to run",
|
|
"required": True,
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"description": "A name to be given to the python file",
|
|
"required": True,
|
|
},
|
|
},
|
|
)
|
|
def execute_python_code(code: str, basename: str, agent: Agent) -> str:
|
|
"""Create and execute a Python file in a Docker container and return the STDOUT of the
|
|
executed code. If there is any data that needs to be captured use a print statement
|
|
|
|
Args:
|
|
code (str): The Python code to run
|
|
basename (str): A name to be given to the Python file
|
|
|
|
Returns:
|
|
str: The STDOUT captured from the code when it ran
|
|
"""
|
|
ai_name = agent.ai_name
|
|
directory = os.path.join(agent.config.workspace_path, ai_name, "executed_code")
|
|
os.makedirs(directory, exist_ok=True)
|
|
|
|
if not basename.endswith(".py"):
|
|
basename = basename + ".py"
|
|
|
|
path = os.path.join(directory, basename)
|
|
|
|
try:
|
|
with open(path, "w+", encoding="utf-8") as f:
|
|
f.write(code)
|
|
|
|
return execute_python_file(f.name, agent)
|
|
except Exception as e:
|
|
return f"Error: {str(e)}"
|
|
|
|
|
|
@command(
|
|
"execute_python_file",
|
|
"Execute an existing Python file",
|
|
{
|
|
"filename": {
|
|
"type": "string",
|
|
"description": "The name of te file to execute",
|
|
"required": True,
|
|
},
|
|
},
|
|
)
|
|
def execute_python_file(filename: str, agent: Agent) -> str:
|
|
"""Execute a Python file in a Docker container and return the output
|
|
|
|
Args:
|
|
filename (str): The name of the file to execute
|
|
|
|
Returns:
|
|
str: The output of the file
|
|
"""
|
|
logger.info(
|
|
f"Executing python file '{filename}' in working directory '{CFG.workspace_path}'"
|
|
)
|
|
|
|
if not filename.endswith(".py"):
|
|
return "Error: Invalid file type. Only .py files are allowed."
|
|
|
|
workspace = Workspace(
|
|
agent.config.workspace_path, agent.config.restrict_to_workspace
|
|
)
|
|
|
|
path = workspace.get_path(filename)
|
|
if not path.is_file():
|
|
# Mimic the response that you get from the command line so that it's easier to identify
|
|
return (
|
|
f"python: can't open file '{filename}': [Errno 2] No such file or directory"
|
|
)
|
|
|
|
if we_are_running_in_a_docker_container():
|
|
result = subprocess.run(
|
|
["python", str(path)],
|
|
capture_output=True,
|
|
encoding="utf8",
|
|
cwd=CFG.workspace_path,
|
|
)
|
|
if result.returncode == 0:
|
|
return result.stdout
|
|
else:
|
|
return f"Error: {result.stderr}"
|
|
|
|
try:
|
|
client = docker.from_env()
|
|
# You can replace this with the desired Python image/version
|
|
# You can find available Python images on Docker Hub:
|
|
# https://hub.docker.com/_/python
|
|
image_name = "python:3-alpine"
|
|
try:
|
|
client.images.get(image_name)
|
|
logger.warn(f"Image '{image_name}' found locally")
|
|
except ImageNotFound:
|
|
logger.info(
|
|
f"Image '{image_name}' not found locally, pulling from Docker Hub"
|
|
)
|
|
# Use the low-level API to stream the pull response
|
|
low_level_client = docker.APIClient()
|
|
for line in low_level_client.pull(image_name, stream=True, decode=True):
|
|
# Print the status and progress, if available
|
|
status = line.get("status")
|
|
progress = line.get("progress")
|
|
if status and progress:
|
|
logger.info(f"{status}: {progress}")
|
|
elif status:
|
|
logger.info(status)
|
|
container = client.containers.run(
|
|
image_name,
|
|
["python", str(path.relative_to(workspace.root))],
|
|
volumes={
|
|
agent.config.workspace_path: {
|
|
"bind": "/workspace",
|
|
"mode": "ro",
|
|
}
|
|
},
|
|
working_dir="/workspace",
|
|
stderr=True,
|
|
stdout=True,
|
|
detach=True,
|
|
)
|
|
|
|
container.wait()
|
|
logs = container.logs().decode("utf-8")
|
|
container.remove()
|
|
|
|
# print(f"Execution complete. Output: {output}")
|
|
# print(f"Logs: {logs}")
|
|
|
|
return logs
|
|
|
|
except docker.errors.DockerException as e:
|
|
logger.warn(
|
|
"Could not run the script in a container. If you haven't already, please install Docker https://docs.docker.com/get-docker/"
|
|
)
|
|
return f"Error: {str(e)}"
|
|
|
|
except Exception as e:
|
|
return f"Error: {str(e)}"
|
|
|
|
|
|
def validate_command(command: str, config: Config) -> bool:
|
|
"""Validate a command to ensure it is allowed
|
|
|
|
Args:
|
|
command (str): The command to validate
|
|
|
|
Returns:
|
|
bool: True if the command is allowed, False otherwise
|
|
"""
|
|
if not command:
|
|
return False
|
|
|
|
command_name = command.split()[0]
|
|
|
|
if config.shell_command_control == ALLOWLIST_CONTROL:
|
|
return command_name in config.shell_allowlist
|
|
else:
|
|
return command_name not in config.shell_denylist
|
|
|
|
|
|
@command(
|
|
"execute_shell",
|
|
"Execute Shell Command, non-interactive commands only",
|
|
{
|
|
"command_line": {
|
|
"type": "string",
|
|
"description": "The command line to execute",
|
|
"required": True,
|
|
}
|
|
},
|
|
enabled=lambda cfg: cfg.execute_local_commands,
|
|
disabled_reason="You are not allowed to run local shell commands. To execute"
|
|
" shell commands, EXECUTE_LOCAL_COMMANDS must be set to 'True' "
|
|
"in your config file: .env - do not attempt to bypass the restriction.",
|
|
)
|
|
def execute_shell(command_line: str, agent: Agent) -> str:
|
|
"""Execute a shell command and return the output
|
|
|
|
Args:
|
|
command_line (str): The command line to execute
|
|
|
|
Returns:
|
|
str: The output of the command
|
|
"""
|
|
if not validate_command(command_line, agent.config):
|
|
logger.info(f"Command '{command_line}' not allowed")
|
|
return "Error: This Shell Command is not allowed."
|
|
|
|
current_dir = Path.cwd()
|
|
# Change dir into workspace if necessary
|
|
if not current_dir.is_relative_to(agent.config.workspace_path):
|
|
os.chdir(agent.config.workspace_path)
|
|
|
|
logger.info(
|
|
f"Executing command '{command_line}' in working directory '{os.getcwd()}'"
|
|
)
|
|
|
|
result = subprocess.run(command_line, capture_output=True, shell=True)
|
|
output = f"STDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
|
|
|
# Change back to whatever the prior working dir was
|
|
|
|
os.chdir(current_dir)
|
|
return output
|
|
|
|
|
|
@command(
|
|
"execute_shell_popen",
|
|
"Execute Shell Command, non-interactive commands only",
|
|
{
|
|
"query": {
|
|
"type": "string",
|
|
"description": "The search query",
|
|
"required": True,
|
|
}
|
|
},
|
|
lambda config: config.execute_local_commands,
|
|
"You are not allowed to run local shell commands. To execute"
|
|
" shell commands, EXECUTE_LOCAL_COMMANDS must be set to 'True' "
|
|
"in your config. Do not attempt to bypass the restriction.",
|
|
)
|
|
def execute_shell_popen(command_line, agent: Agent) -> str:
|
|
"""Execute a shell command with Popen and returns an english description
|
|
of the event and the process id
|
|
|
|
Args:
|
|
command_line (str): The command line to execute
|
|
|
|
Returns:
|
|
str: Description of the fact that the process started and its id
|
|
"""
|
|
if not validate_command(command_line, agent.config):
|
|
logger.info(f"Command '{command_line}' not allowed")
|
|
return "Error: This Shell Command is not allowed."
|
|
|
|
current_dir = os.getcwd()
|
|
# Change dir into workspace if necessary
|
|
if agent.config.workspace_path not in current_dir:
|
|
os.chdir(agent.config.workspace_path)
|
|
|
|
logger.info(
|
|
f"Executing command '{command_line}' in working directory '{os.getcwd()}'"
|
|
)
|
|
|
|
do_not_show_output = subprocess.DEVNULL
|
|
process = subprocess.Popen(
|
|
command_line, shell=True, stdout=do_not_show_output, stderr=do_not_show_output
|
|
)
|
|
|
|
# Change back to whatever the prior working dir was
|
|
|
|
os.chdir(current_dir)
|
|
|
|
return f"Subprocess started with PID:'{str(process.pid)}'"
|
|
|
|
|
|
def we_are_running_in_a_docker_container() -> bool:
|
|
"""Check if we are running in a Docker container
|
|
|
|
Returns:
|
|
bool: True if we are running in a Docker container, False otherwise
|
|
"""
|
|
return os.path.exists("/.dockerenv")
|