Luxx/luxx/tools/builtin/shell.py

106 lines
3.3 KiB
Python

"""Shell command execution tool"""
import subprocess
from typing import Dict, Any
from luxx.tools.factory import tool
from luxx.tools.core import ToolContext, CommandPermission
from luxx.config import config
from pathlib import Path
def get_workspace_from_context(context: ToolContext) -> str:
"""Get workspace from context, auto-create if needed"""
import hashlib
from pathlib import Path
if context.workspace:
return context.workspace
if context.user_id is not None:
workspace_root = Path(config.workspace_root).resolve()
user_hash = hashlib.sha256(str(context.user_id).encode()).hexdigest()[:16]
user_workspace = workspace_root / user_hash
if config.workspace_auto_create and not user_workspace.exists():
user_workspace.mkdir(parents=True, exist_ok=True)
return str(user_workspace)
return None
@tool(
name="shell_exec",
description="Execute a shell command in the user's workspace directory",
parameters={
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Shell command to execute"
},
"timeout": {
"type": "integer",
"description": "Timeout in seconds (default 30)",
"default": 30
},
"cwd": {
"type": "string",
"description": "Working directory relative to workspace (default: workspace root)",
"default": "."
}
},
"required": ["command"]
},
required_params=["command"],
category="shell",
required_permission=CommandPermission.EXECUTE
)
def shell_exec(arguments: Dict[str, Any], context: ToolContext = None):
"""
Execute a shell command in the user's workspace.
Args:
command: Shell command to execute
timeout: Timeout in seconds (default 30)
cwd: Working directory relative to workspace (default: workspace root)
Returns:
{"stdout": str, "stderr": str, "returncode": int}
"""
workspace = get_workspace_from_context(context)
if not workspace:
return {"error": "Workspace not configured"}
command = arguments.get("command", "")
timeout = arguments.get("timeout", 30)
cwd_rel = arguments.get("cwd", ".")
# Build working directory
workspace_path = Path(workspace).resolve()
cwd_path = (workspace_path / cwd_rel).resolve()
# Security check: ensure cwd is within workspace
if not cwd_path.is_relative_to(workspace_path):
return {"error": "Working directory is outside workspace"}
try:
result = subprocess.run(
command,
shell=True,
cwd=str(cwd_path),
capture_output=True,
text=True,
timeout=min(timeout, 120) # Max 2 minutes
)
return {
"stdout": result.stdout,
"stderr": result.stderr,
"returncode": result.returncode,
"command": command,
"cwd": str(cwd_path.relative_to(workspace_path))
}
except subprocess.TimeoutExpired:
return {"error": f"Command timed out after {timeout} seconds"}
except Exception as e:
return {"error": f"Execution error: {str(e)}"}