Skip to content

Commit 2d05e6f

Browse files
Merge pull request #1400 from MervinPraison/claude/issue-1395-20260416-1148
feat: implement remote execution environment backends (SSH, Modal, Daytona)
2 parents 7e5ce3a + 1c3055b commit 2d05e6f

File tree

9 files changed

+2553
-2
lines changed

9 files changed

+2553
-2
lines changed

examples/python/sandbox_remote.py

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Remote Sandbox Examples for PraisonAI
4+
5+
Demonstrates how to use SSH, Modal, and Daytona sandbox backends
6+
for remote code execution.
7+
"""
8+
9+
import asyncio
10+
import os
11+
from typing import Optional
12+
13+
from praisonai.sandbox import SSHSandbox, ModalSandbox, DaytonaSandbox
14+
from praisonaiagents.sandbox import ResourceLimits
15+
16+
17+
async def ssh_example():
18+
"""Example using SSH sandbox for remote execution."""
19+
print("🔗 SSH Sandbox Example")
20+
print("=" * 50)
21+
22+
# Configure SSH connection
23+
# Note: You need actual SSH server access for this to work
24+
ssh_host = os.getenv("SSH_HOST", "localhost")
25+
ssh_user = os.getenv("SSH_USER", "ubuntu")
26+
ssh_key = os.getenv("SSH_KEY_FILE", "~/.ssh/id_rsa")
27+
28+
print(f"Connecting to {ssh_user}@{ssh_host}")
29+
30+
try:
31+
sandbox = SSHSandbox(
32+
host=ssh_host,
33+
user=ssh_user,
34+
key_file=ssh_key,
35+
working_dir="/tmp/praisonai_demo"
36+
)
37+
38+
# Check availability
39+
if not sandbox.is_available:
40+
print("❌ SSH backend not available. Install with: pip install praisonai[ssh]")
41+
return
42+
43+
# Start sandbox
44+
await sandbox.start()
45+
print("✅ SSH connection established")
46+
47+
# Execute Python code
48+
print("\n📄 Executing Python code...")
49+
result = await sandbox.execute("""
50+
import sys
51+
import platform
52+
print(f"Python version: {sys.version}")
53+
print(f"Platform: {platform.platform()}")
54+
print("Hello from remote SSH server! 🚀")
55+
""", language="python")
56+
57+
print(f"Exit code: {result.exit_code}")
58+
print(f"Output:\n{result.stdout}")
59+
if result.stderr:
60+
print(f"Errors:\n{result.stderr}")
61+
62+
# Execute shell command
63+
print("\n🐚 Executing shell command...")
64+
result = await sandbox.run_command("uname -a && uptime")
65+
print(f"System info:\n{result.stdout}")
66+
67+
# Write and read file
68+
print("\n📝 File operations...")
69+
await sandbox.write_file("/tmp/praisonai_demo/hello.py", "print('Hello from file!')")
70+
content = await sandbox.read_file("/tmp/praisonai_demo/hello.py")
71+
print(f"File content: {content}")
72+
73+
# Execute the file
74+
result = await sandbox.execute_file("/tmp/praisonai_demo/hello.py")
75+
print(f"File execution result: {result.stdout}")
76+
77+
# Test resource limits
78+
print("\n⏱️ Testing resource limits...")
79+
limits = ResourceLimits(timeout_seconds=5, memory_mb=128)
80+
result = await sandbox.execute("""
81+
import time
82+
print("Starting long task...")
83+
time.sleep(10) # This will timeout
84+
print("Task completed")
85+
""", limits=limits)
86+
87+
print(f"Limited execution status: {result.status}")
88+
if result.error:
89+
print(f"Error (expected timeout): {result.error}")
90+
91+
await sandbox.stop()
92+
print("✅ SSH sandbox stopped")
93+
94+
except Exception as e:
95+
print(f"❌ SSH example failed: {e}")
96+
97+
98+
async def modal_example():
99+
"""Example using Modal sandbox for serverless GPU execution."""
100+
print("\n☁️ Modal Sandbox Example")
101+
print("=" * 50)
102+
103+
try:
104+
# Note: Requires Modal account and API key
105+
sandbox = ModalSandbox(
106+
gpu="A100", # Request A100 GPU
107+
timeout=120
108+
)
109+
110+
# Check availability
111+
if not sandbox.is_available:
112+
print("❌ Modal backend not available. Install with: pip install praisonai[modal]")
113+
return
114+
115+
print("🚀 Starting Modal app...")
116+
await sandbox.start()
117+
print("✅ Modal app initialized")
118+
119+
# Execute GPU-accelerated Python code
120+
print("\n🔥 Executing GPU code...")
121+
result = await sandbox.execute("""
122+
import torch
123+
import numpy as np
124+
125+
print(f"PyTorch version: {torch.__version__}")
126+
print(f"CUDA available: {torch.cuda.is_available()}")
127+
128+
if torch.cuda.is_available():
129+
device = torch.cuda.get_device_name()
130+
print(f"GPU device: {device}")
131+
132+
# Simple tensor operations on GPU
133+
x = torch.randn(1000, 1000, device='cuda')
134+
y = torch.randn(1000, 1000, device='cuda')
135+
z = torch.matmul(x, y)
136+
print(f"GPU computation result shape: {z.shape}")
137+
else:
138+
print("Running on CPU")
139+
x = torch.randn(100, 100)
140+
y = torch.randn(100, 100)
141+
z = torch.matmul(x, y)
142+
print(f"CPU computation result shape: {z.shape}")
143+
144+
print("🚀 Modal execution completed!")
145+
""", language="python")
146+
147+
print(f"Exit code: {result.exit_code}")
148+
print(f"Output:\n{result.stdout}")
149+
if result.stderr:
150+
print(f"Errors:\n{result.stderr}")
151+
152+
# Test different languages
153+
print("\n🐚 Testing bash execution...")
154+
result = await sandbox.run_command("nvidia-smi || echo 'No GPU info available'")
155+
print(f"GPU info:\n{result.stdout}")
156+
157+
await sandbox.stop()
158+
print("✅ Modal sandbox stopped")
159+
160+
except Exception as e:
161+
print(f"❌ Modal example failed: {e}")
162+
163+
164+
async def daytona_example():
165+
"""Example using Daytona sandbox for cloud dev environment."""
166+
print("\n🌤️ Daytona Sandbox Example")
167+
print("=" * 50)
168+
169+
try:
170+
# Configure Daytona workspace
171+
sandbox = DaytonaSandbox(
172+
workspace_template="python-dev",
173+
provider="aws", # or "gcp", "azure", "local"
174+
timeout=180
175+
)
176+
177+
# Check availability
178+
if not sandbox.is_available:
179+
print("❌ Daytona backend not available. Install with: pip install praisonai[daytona]")
180+
return
181+
182+
print(f"🏗️ Creating Daytona workspace: {sandbox.workspace_name}")
183+
await sandbox.start()
184+
print("✅ Daytona workspace ready")
185+
186+
# Execute development tasks
187+
print("\n💻 Running development tasks...")
188+
result = await sandbox.execute("""
189+
import sys
190+
import os
191+
import subprocess
192+
193+
print("🐍 Python Development Environment")
194+
print(f"Python version: {sys.version}")
195+
print(f"Working directory: {os.getcwd()}")
196+
197+
# Check installed packages
198+
try:
199+
result = subprocess.run([sys.executable, "-m", "pip", "list"],
200+
capture_output=True, text=True)
201+
print("📦 Installed packages:")
202+
print(result.stdout[:500] + "..." if len(result.stdout) > 500 else result.stdout)
203+
except Exception as e:
204+
print(f"Could not list packages: {e}")
205+
206+
print("✨ Daytona workspace is ready for development!")
207+
""", language="python")
208+
209+
print(f"Exit code: {result.exit_code}")
210+
print(f"Output:\n{result.stdout}")
211+
212+
# Test file operations
213+
print("\n📁 Workspace file operations...")
214+
await sandbox.write_file("/workspace/demo.py", """
215+
def hello_daytona():
216+
print("Hello from Daytona workspace!")
217+
return "🎉 Success!"
218+
219+
if __name__ == "__main__":
220+
result = hello_daytona()
221+
print(result)
222+
""")
223+
224+
# Execute the file
225+
result = await sandbox.execute_file("/workspace/demo.py")
226+
print(f"Demo script output: {result.stdout}")
227+
228+
# List workspace files
229+
files = await sandbox.list_files("/workspace")
230+
print(f"Workspace files: {files}")
231+
232+
await sandbox.stop()
233+
print("✅ Daytona workspace stopped")
234+
235+
except Exception as e:
236+
print(f"❌ Daytona example failed: {e}")
237+
238+
239+
async def comparison_example():
240+
"""Compare execution across different sandbox backends."""
241+
print("\n⚖️ Sandbox Comparison")
242+
print("=" * 50)
243+
244+
# Simple test code
245+
test_code = """
246+
import time
247+
import sys
248+
start_time = time.time()
249+
print(f"Hello from {sys.platform}!")
250+
print(f"Execution time: {time.time() - start_time:.3f}s")
251+
"""
252+
253+
# Test each sandbox type
254+
sandboxes = [
255+
("SSH", SSHSandbox(host=os.getenv("SSH_HOST", "localhost"))),
256+
("Modal", ModalSandbox()),
257+
("Daytona", DaytonaSandbox())
258+
]
259+
260+
for name, sandbox in sandboxes:
261+
print(f"\n🧪 Testing {name} sandbox...")
262+
263+
if not sandbox.is_available:
264+
print(f"❌ {name} not available")
265+
continue
266+
267+
try:
268+
await sandbox.start()
269+
result = await sandbox.execute(test_code, language="python")
270+
271+
print(f"✅ {name}: {result.status}")
272+
print(f" Duration: {result.duration_seconds:.3f}s")
273+
print(f" Output: {result.stdout.strip()}")
274+
275+
await sandbox.stop()
276+
277+
except Exception as e:
278+
print(f"❌ {name} failed: {e}")
279+
280+
281+
async def main():
282+
"""Run all examples."""
283+
print("🚀 PraisonAI Remote Sandbox Examples")
284+
print("=" * 70)
285+
286+
# Run examples
287+
await ssh_example()
288+
await modal_example()
289+
await daytona_example()
290+
await comparison_example()
291+
292+
print("\n✅ All examples completed!")
293+
print("\nNext steps:")
294+
print("1. Set up SSH access to remote servers")
295+
print("2. Configure Modal account for GPU workloads")
296+
print("3. Set up Daytona for cloud development")
297+
print("4. Use these sandboxes in your PraisonAI agents!")
298+
299+
300+
if __name__ == "__main__":
301+
asyncio.run(main())

src/praisonai/praisonai/sandbox/__init__.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
Sandbox implementations for PraisonAI.
33
4-
Provides Docker, subprocess, and sandlock sandbox for safe code execution.
4+
Provides Docker, subprocess, sandlock, SSH, Modal, and Daytona sandbox for safe code execution.
55
"""
66

77
from typing import TYPE_CHECKING
@@ -10,6 +10,9 @@
1010
from .docker import DockerSandbox
1111
from .subprocess import SubprocessSandbox
1212
from .sandlock import SandlockSandbox
13+
from .ssh import SSHSandbox
14+
from .modal import ModalSandbox
15+
from .daytona import DaytonaSandbox
1316

1417
def __getattr__(name: str):
1518
"""Lazy loading of sandbox components."""
@@ -22,6 +25,22 @@ def __getattr__(name: str):
2225
if name == "SandlockSandbox":
2326
from .sandlock import SandlockSandbox
2427
return SandlockSandbox
28+
if name == "SSHSandbox":
29+
from .ssh import SSHSandbox
30+
return SSHSandbox
31+
if name == "ModalSandbox":
32+
from .modal import ModalSandbox
33+
return ModalSandbox
34+
if name == "DaytonaSandbox":
35+
from .daytona import DaytonaSandbox
36+
return DaytonaSandbox
2537
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
2638

27-
__all__ = ["DockerSandbox", "SubprocessSandbox", "SandlockSandbox"]
39+
__all__ = [
40+
"DockerSandbox",
41+
"SubprocessSandbox",
42+
"SandlockSandbox",
43+
"SSHSandbox",
44+
"ModalSandbox",
45+
"DaytonaSandbox"
46+
]

0 commit comments

Comments
 (0)