Script Execution Service
The Script Execution Service provides secure, controlled execution of Python scripts using subprocess management. It includes comprehensive security validation, timeout handling, JSON output support, and cross-platform compatibility.
Overview
The service offers a robust abstraction for script execution with emphasis on security and reliability. It prevents common security vulnerabilities while providing flexible execution options including timeout controls and JSON mode for structured output.
Architecture
ScriptExecutionService (Abstract)
├── execute_script()
├── execute_script_with_timeout()
├── execute_script_json_mode()
└── validate_script_path()
SubprocessScriptExecutionService (Implementation)
├── Subprocess management
├── Security validation
├── Timeout handling
├── Project root detection
└── Cross-platform support
Key Classes
ScriptResult
A data class representing the result of script execution:
from dataclasses import dataclass
@dataclass
class ScriptResult:
success: bool # True if script executed successfully (return code 0)
output: str # Standard output from the script
error: str # Standard error from the script
return_code: int # Process exit code
ScriptExecutionService (Abstract)
The base interface defining script execution operations:
from abc import ABC, abstractmethod
from pathlib import Path
from typing import List, Optional
class ScriptExecutionService(ABC):
@abstractmethod
def execute_script(self, script_path: Path, args: List[str]) -> ScriptResult:
"""Execute Python script with given arguments"""
pass
@abstractmethod
def execute_script_with_timeout(
self, script_path: Path, args: List[str], timeout_seconds: int
) -> ScriptResult:
"""Execute Python script with timeout"""
pass
@abstractmethod
def execute_script_json_mode(
self, script_path: Path, args: List[str]
) -> ScriptResult:
"""Execute script expecting JSON output"""
pass
@abstractmethod
def validate_script_path(
self, script_path: Path, project_path: Optional[Path] = None
) -> bool:
"""Validate script path for security"""
pass
SubprocessScriptExecutionService
The primary implementation using Python's subprocess module:
from specify_cli.services.script_execution_service import SubprocessScriptExecutionService
# Initialize service
service = SubprocessScriptExecutionService()
# Execute a script
result = service.execute_script(
script_path=Path("/project/.specify/scripts/deploy.py"),
args=["--env", "production", "--verbose"]
)
if result.success:
print(f"Output: {result.output}")
else:
print(f"Error: {result.error}")
print(f"Exit code: {result.return_code}")
Core Methods
execute_script(script_path, args)
Basic script execution with default timeout:
# Execute deployment script
result = service.execute_script(
script_path=Path(".specify/scripts/deploy.py"),
args=["--target", "production"]
)
print(f"Success: {result.success}")
print(f"Output: {result.output}")
if not result.success:
print(f"Error: {result.error}")
execute_script_with_timeout(script_path, args, timeout_seconds)
Execute script with custom timeout control:
# Execute with 60-second timeout
result = service.execute_script_with_timeout(
script_path=Path(".specify/scripts/long_running_task.py"),
args=["--iterations", "1000"],
timeout_seconds=60
)
if not result.success and "timed out" in result.error:
print("Script execution timed out")
execute_script_json_mode(script_path, args)
Execute script expecting JSON output with automatic validation:
# Execute script that returns JSON
result = service.execute_script_json_mode(
script_path=Path(".specify/scripts/status_check.py"),
args=["--format", "json"]
)
if result.success:
import json
data = json.loads(result.output)
print(f"Status: {data['status']}")
print(f"Health: {data['health_check']}")
validate_script_path(script_path, project_path)
Comprehensive security validation:
# Validate script before execution
is_safe = service.validate_script_path(
script_path=Path("/project/.specify/scripts/deploy.py"),
project_path=Path("/project")
)
if is_safe:
result = service.execute_script(script_path, args)
else:
print("Script path failed security validation")
Security Features
Path Validation
The service implements comprehensive path validation to prevent security vulnerabilities:
- File existence and type validation
- Python file extension requirement (
.py
) - Symlink resolution and validation
- Project containment checks
- System directory prevention
# Blocked system directories
system_dirs = [
"/usr", "/bin", "/sbin", "/etc", "/var", "/tmp", # Unix/Linux
"/System", "/Library", # macOS
"C:\\Windows", "C:\\Program Files" # Windows
]
Shell Injection Prevention
- Uses
shell=False
in subprocess calls - Constructs command arrays instead of shell strings
- Validates all path components
Resource Controls
- Configurable timeout limits
- Memory and CPU controls via subprocess
- Proper cleanup of child processes
Advanced Features
Project Root Detection
The service automatically detects the project root using multiple strategies:
def _find_project_root(self, script_path: Path) -> Path:
# 1. Try git repository root
result = subprocess.run(["git", "rev-parse", "--show-toplevel"])
# 2. Search upward for .specify directory
current_dir = script_path.parent
while current_dir != current_dir.parent:
if (current_dir / ".specify").exists():
return current_dir
current_dir = current_dir.parent
# 3. Fallback to script's parent directory
return script_path.parent
Timeout Configuration
# Set custom default timeout
service.set_default_timeout(45) # 45 seconds
# Get current timeout
current_timeout = service.get_default_timeout()
print(f"Default timeout: {current_timeout} seconds")
Error Handling
Comprehensive error handling for various failure modes:
# Example error scenarios handled:
- FileNotFoundError: Python interpreter not found
- subprocess.TimeoutExpired: Script execution timeout
- OSError: File system or permission errors
- JSON validation errors in JSON mode
- Path security validation failures
Usage Examples
Basic Script Execution
from pathlib import Path
from specify_cli.services.script_execution_service import SubprocessScriptExecutionService
def run_deployment_script():
service = SubprocessScriptExecutionService()
script_path = Path(".specify/scripts/deploy.py")
args = ["--env", "production", "--verify"]
print("Starting deployment...")
result = service.execute_script(script_path, args)
if result.success:
print("✓ Deployment completed successfully")
print(f"Output: {result.output}")
else:
print("✗ Deployment failed")
print(f"Error: {result.error}")
print(f"Exit code: {result.return_code}")
Script with Timeout and Progress
def run_backup_with_progress():
service = SubprocessScriptExecutionService()
script_path = Path(".specify/scripts/backup.py")
args = ["--full-backup", "--compress"]
print("Starting backup (max 300 seconds)...")
result = service.execute_script_with_timeout(
script_path, args, timeout_seconds=300
)
if result.success:
print("✓ Backup completed")
elif "timed out" in result.error:
print("⚠ Backup timed out - may need manual intervention")
else:
print(f"✗ Backup failed: {result.error}")
JSON Mode for Structured Results
def check_system_status():
service = SubprocessScriptExecutionService()
script_path = Path(".specify/scripts/health_check.py")
result = service.execute_script_json_mode(script_path, [])
if result.success:
import json
status = json.loads(result.output)
print(f"System Health: {status['overall_health']}")
print(f"Database: {status['database_status']}")
print(f"API: {status['api_status']}")
print(f"Disk Space: {status['disk_usage']}%")
else:
print(f"Health check failed: {result.error}")
Safe Script Execution with Validation
def execute_user_script(script_name: str, project_path: Path):
service = SubprocessScriptExecutionService()
# Build script path
script_path = project_path / ".specify" / "scripts" / f"{script_name}.py"
# Validate before execution
if not service.validate_script_path(script_path, project_path):
print(f"❌ Script '{script_name}' failed security validation")
return False
if not script_path.exists():
print(f"❌ Script '{script_name}' not found")
return False
# Execute with timeout
print(f"🔧 Executing script: {script_name}")
result = service.execute_script_with_timeout(script_path, [], 30)
if result.success:
print(f"✅ Script completed successfully")
if result.output:
print(f"Output: {result.output}")
return True
else:
print(f"❌ Script failed: {result.error}")
return False
Integration Patterns
With Script Discovery Service
from specify_cli.services.script_discovery_service import FileSystemScriptDiscoveryService
from specify_cli.services.script_execution_service import SubprocessScriptExecutionService
def run_available_script(script_name: str, project_path: Path, args: List[str]):
# Discover script
discovery = FileSystemScriptDiscoveryService(project_path)
script_path = discovery.find_script(script_name)
if not script_path:
print(f"Script '{script_name}' not found")
return False
# Get script info
info = discovery.get_script_info(script_name)
if info:
print(f"Executing: {info['description']}")
# Execute script
execution = SubprocessScriptExecutionService()
result = execution.execute_script(script_path, args)
return result.success
CLI Command Integration
def script_command(script_name: str, args: List[str], timeout: int = 30):
"""CLI command to execute project scripts"""
project_path = Path.cwd()
service = SubprocessScriptExecutionService()
script_path = project_path / ".specify" / "scripts" / f"{script_name}.py"
# Validate and execute
if not service.validate_script_path(script_path, project_path):
raise click.ClickException(f"Script '{script_name}' failed security validation")
result = service.execute_script_with_timeout(script_path, args, timeout)
# Display results
if result.output:
click.echo(result.output)
if not result.success:
if result.error:
click.echo(click.style(f"Error: {result.error}", fg="red"), err=True)
raise click.ClickException(f"Script failed with exit code {result.return_code}")
Best Practices
- Always validate paths - Use
validate_script_path()
before execution - Set appropriate timeouts - Prevent runaway scripts from consuming resources
- Handle errors gracefully - Check
ScriptResult.success
and handle failures - Use absolute paths - Always resolve paths for security and reliability
- Log execution details - Track script executions for debugging and auditing
- Consider JSON mode - For scripts that need to return structured data
- Test timeout scenarios - Ensure your application handles timeout gracefully
Performance Considerations
- Subprocess overhead - Each execution creates a new Python process
- Timeout precision - Timeouts are approximate, not exact
- Memory usage - Output capture holds script output in memory
- Cross-platform compatibility - Different behavior on Windows vs Unix systems
The Script Execution Service provides a secure, reliable foundation for running Python scripts within SpecifyX projects, with comprehensive safety controls and flexible execution options.