Source code for atloop.tools.system.run_shell_script_string
"""Run shell script string tool.
This tool executes shell scripts directly without shell escaping issues.
It uses the SHELL_SCRIPT_#N placeholder system for script content.
"""
import tempfile
from pathlib import Path
from typing import Any, Dict, Optional
from atloop.runtime.sandbox_adapter import SandboxAdapter
from atloop.tools.base import BaseTool, ToolResult
[docs]
class RunShellScriptStringTool(BaseTool):
"""
Tool for executing shell script strings without shell escaping issues.
**Use this tool instead of `run` with `bash -c "..."` for complex shell scripts.**
**Benefits:**
- No shell escaping issues (quotes, variables, etc.)
- Better error messages (can show script content)
- Cleaner code (no need to escape quotes)
- Supports multi-line scripts easily
**Usage:**
Use SHELL_SCRIPT_#N placeholder in JSON, then provide script content:
{
"tool": "run_shell_script_string",
"args": {
"script": "SHELL_SCRIPT_#1"
}
}
---(SHELL_SCRIPT_#1)---
#!/bin/bash
set -e
echo "Processing files..."
for file in *.txt; do
if [ -f "$file" ]; then
echo "Found: $file"
wc -l "$file"
fi
done
---
**Important notes:**
- Script runs in /workspace directory
- Success is determined by stderr content, NOT exit code
- Script is written to a temporary file, executed, then cleaned up
- Uses bash to execute the script
"""
[docs]
def __init__(self, sandbox: SandboxAdapter):
"""
Initialize run shell script string tool.
Args:
sandbox: Sandbox adapter instance
"""
self.sandbox = sandbox
@property
def name(self) -> str:
"""Tool name."""
return "run_shell_script_string"
@property
def description(self) -> str:
"""Tool description."""
return (
"Execute shell script strings without shell escaping issues. "
"Use this tool instead of `run` with `bash -c \"...\"` for complex shell scripts. "
"Benefits: no shell escaping issues (quotes, variables, etc.), better error messages, "
"supports multi-line scripts easily. Script runs in /workspace directory. "
"Success is determined by stderr content, NOT exit code. "
"Use SHELL_SCRIPT_#N placeholder in JSON, then provide script content after JSON."
)
[docs]
def validate_args(self, args: Dict[str, Any]) -> tuple[bool, Optional[str]]:
"""Validate arguments."""
if "script" not in args:
return False, "Missing required argument: 'script'"
if not isinstance(args["script"], str):
return False, "Argument 'script' must be a string"
if not args["script"].strip():
return False, "Argument 'script' cannot be empty"
return True, None
[docs]
def execute(self, args: Dict[str, Any]) -> ToolResult:
"""
Execute shell script string.
**Args:**
args: Tool arguments dictionary
- script (str, required): Shell script to execute. Can be multi-line.
Should use SHELL_SCRIPT_#N placeholder in JSON, actual content provided separately.
- timeout_sec (int, optional): Timeout in seconds. Default: 600 (10 minutes).
**Returns:**
ToolResult with:
- ok (bool): True if script succeeded (no errors in stderr)
- stdout (str): Standard output from the script
- stderr (str): Standard error from the script (empty string means success)
- meta (dict): Contains script_preview, duration_ms
**Examples:**
# Simple script
run_shell_script_string(script="SHELL_SCRIPT_#1")
---(SHELL_SCRIPT_#1)---
echo "Hello, World!"
ls -la
---
# Complex script with loops and conditionals
run_shell_script_string(script="SHELL_SCRIPT_#1")
---(SHELL_SCRIPT_#1)---
#!/bin/bash
set -e
for file in *.py; do
if [ -f "$file" ]; then
echo "Processing $file"
python3 -m py_compile "$file"
fi
done
---
**⚠️ Critical: Success Determination**
- **DO NOT rely on exit_code** - it's unreliable!
- **Success = empty stderr** (no error messages)
- **Failure = non-empty stderr** (contains error messages)
- Always read stdout and stderr content to judge success
"""
script = args["script"]
timeout_sec = args.get("timeout_sec", 600)
# Create temporary shell script file
# Use /workspace/.atloop_temp for temp files (will be cleaned up)
temp_dir = Path("/workspace") / ".atloop_temp"
try:
temp_dir.mkdir(parents=True, exist_ok=True)
except Exception as e:
# If we can't create in /workspace, fall back to system temp
import logging
logger = logging.getLogger(__name__)
logger.warning(
f"[RunShellScriptStringTool] Failed to create /workspace/.atloop_temp: {e}. "
f"Falling back to system temp directory."
)
temp_dir = Path(tempfile.gettempdir()) / "atloop_temp"
temp_dir.mkdir(parents=True, exist_ok=True)
# Create temp file with .sh extension
with tempfile.NamedTemporaryFile(
mode="w", suffix=".sh", dir=str(temp_dir), delete=False
) as f:
temp_file = Path(f.name)
# Write script with shebang if not present
if not script.strip().startswith("#!"):
f.write("#!/bin/bash\n")
f.write(script)
# Make executable
temp_file.chmod(0o755)
try:
# Execute shell script
# Use bash explicitly, run from /workspace
cmd = f"cd /workspace && bash {temp_file}"
result = self.sandbox.exec_shell(
command=cmd,
workdir="/workspace",
timeout_seconds=timeout_sec,
)
stdout = result.get("stdout", "")
stderr = result.get("stderr", "")
exit_code = result.get("exitCode", result.get("exit_code", -1))
# Determine success based on stderr content, not exit code
ok = not bool(stderr.strip()) # Success if no error messages in stderr
# Create script preview for meta (first 200 chars)
script_preview = script[:200] + "..." if len(script) > 200 else script
return ToolResult(
ok=ok,
stdout=stdout,
stderr=stderr,
meta={
"script_preview": script_preview,
"exitCode": exit_code, # Include exit code for proper display
"script_length": len(script),
"duration_ms": result.get("durationMs", 0),
},
)
finally:
# Clean up temp file
try:
temp_file.unlink()
except Exception as e:
# Log but don't fail if cleanup fails
import logging
logger = logging.getLogger(__name__)
logger.warning(
f"[RunShellScriptStringTool] Failed to cleanup temp file {temp_file}: {e}"
)