ChefAdorous commited on
Commit
a89f25d
·
1 Parent(s): ec9973e

Deploy Code Execution Sandbox with FastAPI and Docker

Browse files
.env.example ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Environment Configuration
2
+ MAX_EXECUTION_TIME=30
3
+ MAX_MEMORY_MB=512
4
+ ENABLE_NETWORK=false
5
+ LOG_LEVEL=INFO
6
+ MAX_OUTPUT_SIZE=1048576
.gitignore ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .env
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ .Python
7
+ *.so
8
+ *.egg
9
+ *.egg-info/
10
+ dist/
11
+ build/
12
+ .pytest_cache/
13
+ .coverage
14
+ htmlcov/
15
+ .vscode/
16
+ .idea/
17
+ *.log
Dockerfile CHANGED
@@ -13,4 +13,5 @@ COPY --chown=user ./requirements.txt requirements.txt
13
  RUN pip install --no-cache-dir --upgrade -r requirements.txt
14
 
15
  COPY --chown=user . /app
 
16
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
13
  RUN pip install --no-cache-dir --upgrade -r requirements.txt
14
 
15
  COPY --chown=user . /app
16
+
17
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,352 @@
1
  ---
2
- title: Isolated Sandbox
3
  emoji: 🏃
4
  colorFrom: blue
5
- colorTo: yellow
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Code Execution Sandbox
3
  emoji: 🏃
4
  colorFrom: blue
5
+ colorTo: purple
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
+ # Code Execution Sandbox API 🚀
11
+
12
+ A secure, isolated code execution sandbox API built with FastAPI and Docker. Execute Python, JavaScript, and Bash code in ephemeral containers with strict resource limits and security controls.
13
+
14
+ ## ✨ Features
15
+
16
+ - **Multi-Language Support**: Python, JavaScript (Node.js), and Bash
17
+ - **Security First**:
18
+ - Network isolation (no internet access)
19
+ - Resource limits (CPU, memory, timeout)
20
+ - Read-only filesystem
21
+ - Non-root user execution
22
+ - **Automatic Cleanup**: Containers are destroyed after each execution
23
+ - **RESTful API**: Simple JSON-based interface
24
+ - **Resource Management**: Configurable CPU, memory, and execution timeouts
25
+
26
+ ## 🚀 Quick Start
27
+
28
+ ### Prerequisites
29
+
30
+ - **Docker** installed and running
31
+ - **Python 3.9+**
32
+
33
+ ### Installation
34
+
35
+ 1. **Clone the repository**:
36
+ ```bash
37
+ git clone https://huggingface.co/spaces/fariasultanacodes/isolated-sandbox
38
+ cd isolated-sandbox
39
+ ```
40
+
41
+ 2. **Install dependencies**:
42
+ ```bash
43
+ pip install -r requirements.txt
44
+ ```
45
+
46
+ 3. **Run the API**:
47
+ ```bash
48
+ uvicorn app:app --host 0.0.0.0 --port 7860
49
+ ```
50
+
51
+ The API will be available at `http://localhost:7860`
52
+
53
+ ## 📡 API Endpoints
54
+
55
+ ### `POST /execute`
56
+
57
+ Execute code in an isolated sandbox.
58
+
59
+ **Request Body**:
60
+ ```json
61
+ {
62
+ "code": "print('Hello, World!')",
63
+ "language": "python",
64
+ "stdin": "",
65
+ "timeout": 10,
66
+ "memory_limit": 256
67
+ }
68
+ ```
69
+
70
+ **Response**:
71
+ ```json
72
+ {
73
+ "stdout": "Hello, World!\n",
74
+ "stderr": "",
75
+ "exit_code": 0,
76
+ "execution_time": 0.123,
77
+ "error": null
78
+ }
79
+ ```
80
+
81
+ **Parameters**:
82
+ - `code` (string, required): Code to execute
83
+ - `language` (enum, required): `"python"`, `"javascript"`, or `"bash"`
84
+ - `stdin` (string, optional): Standard input for the code
85
+ - `timeout` (integer, optional): Execution timeout in seconds (1-30, default: 10)
86
+ - `memory_limit` (integer, optional): Memory limit in MB (64-512, default: 256)
87
+
88
+ ### `GET /languages`
89
+
90
+ List all supported programming languages.
91
+
92
+ **Response**:
93
+ ```json
94
+ {
95
+ "languages": [
96
+ {
97
+ "name": "Python",
98
+ "version": "3.11",
99
+ "image": "python:3.11-slim",
100
+ "extensions": [".py"]
101
+ },
102
+ ...
103
+ ]
104
+ }
105
+ ```
106
+
107
+ ### `GET /health`
108
+
109
+ Health check endpoint.
110
+
111
+ **Response**:
112
+ ```json
113
+ {
114
+ "status": "healthy",
115
+ "docker": "connected"
116
+ }
117
+ ```
118
+
119
+ ## 💡 Usage Examples
120
+
121
+ ### Python Execution
122
+
123
+ ```bash
124
+ curl -X POST http://localhost:7860/execute \
125
+ -H "Content-Type: application/json" \
126
+ -d '{
127
+ "code": "for i in range(5):\n print(f\"Count: {i}\")",
128
+ "language": "python"
129
+ }'
130
+ ```
131
+
132
+ ### JavaScript Execution
133
+
134
+ ```bash
135
+ curl -X POST http://localhost:7860/execute \
136
+ -H "Content-Type: application/json" \
137
+ -d '{
138
+ "code": "const arr = [1, 2, 3, 4, 5];\nconsole.log(arr.reduce((a, b) => a + b));",
139
+ "language": "javascript"
140
+ }'
141
+ ```
142
+
143
+ ### Bash Execution
144
+
145
+ ```bash
146
+ curl -X POST http://localhost:7860/execute \
147
+ -H "Content-Type: application/json" \
148
+ -d '{
149
+ "code": "echo \"System Info:\"; uname -a",
150
+ "language": "bash"
151
+ }'
152
+ ```
153
+
154
+ ### With Timeout
155
+
156
+ ```bash
157
+ curl -X POST http://localhost:7860/execute \
158
+ -H "Content-Type: application/json" \
159
+ -d '{
160
+ "code": "import time\ntime.sleep(5)\nprint(\"Done!\")",
161
+ "language": "python",
162
+ "timeout": 2
163
+ }'
164
+ ```
165
+
166
+ ## 🔒 Security Features
167
+
168
+ 1. **Network Isolation**: Containers run with `network_mode="none"`, preventing internet access
169
+ 2. **Resource Limits**:
170
+ - CPU: Limited to 0.5 cores
171
+ - Memory: Configurable (64-512 MB)
172
+ - Timeout: Configurable (1-30 seconds)
173
+ - PIDs: Limited to 50 processes
174
+ 3. **Filesystem**: Read-only root filesystem (except `/tmp`)
175
+ 4. **User Privileges**: Code runs as non-root user (`nobody`)
176
+ 5. **Output Limits**: Stdout/stderr truncated at 1MB to prevent memory attacks
177
+ 6. **Automatic Cleanup**: Containers are removed immediately after execution
178
+
179
+ ## ⚙️ Configuration
180
+
181
+ Create a `.env` file based on `.env.example`:
182
+
183
+ ```bash
184
+ MAX_EXECUTION_TIME=30
185
+ MAX_MEMORY_MB=512
186
+ ENABLE_NETWORK=false
187
+ LOG_LEVEL=INFO
188
+ MAX_OUTPUT_SIZE=1048576
189
+ ```
190
+
191
+ # Custom Docker Images
192
+
193
+ The sandbox uses pre-built, hardened Docker images for each language:
194
+
195
+ ### Python Image
196
+ ```dockerfile
197
+ FROM python:3.11-slim-alpine
198
+ # Custom Python sandbox image with security hardening
199
+ ```
200
+ Location: `sandbox/images/python.Dockerfile`
201
+
202
+ ### JavaScript Image
203
+ ```dockerfile
204
+ FROM node:20-alpine
205
+ # Custom Node.js sandbox image with security hardening
206
+ ```
207
+ Location: `sandbox/images/javascript.Dockerfile`
208
+
209
+ ### Bash Image
210
+ ```dockerfile
211
+ FROM bash:5.2-alpine
212
+ # Custom Bash sandbox image with security hardening
213
+ ```
214
+ Location: `sandbox/images/bash.Dockerfile`
215
+
216
+ **Benefits**:
217
+ - Smaller image sizes (Alpine Linux base)
218
+ - Security hardened with non-root users
219
+ - Minimal attack surface
220
+ - Faster container startup times
221
+
222
+ You can build these images locally if needed:
223
+ ```bash
224
+ # Build Python sandbox image
225
+ docker build -t sandbox-python:latest -f sandbox/images/python.Dockerfile sandbox/images/
226
+
227
+ # Build JavaScript sandbox image
228
+ docker build -t sandbox-javascript:latest -f sandbox/images/javascript.Dockerfile sandbox/images/
229
+
230
+ # Build Bash sandbox image
231
+ docker build -t sandbox-bash:latest -f sandbox/images/bash.Dockerfile sandbox/images/
232
+ ```
233
+
234
+ ## 🐳 Docker Deployment
235
+
236
+ ### Option 1: Self-Hosted
237
+
238
+ The application requires access to the Docker daemon. Run with Docker socket mounted:
239
+
240
+ ```bash
241
+ docker build -t code-sandbox .
242
+ docker run -d \
243
+ -p 7860:7860 \
244
+ -v /var/run/docker.sock:/var/run/docker.sock \
245
+ --name code-sandbox \
246
+ code-sandbox
247
+ ```
248
+
249
+ ### Option 2: Cloud Platforms
250
+
251
+ Deploy to platforms with Docker support:
252
+ - **Modal**: Use Modal's container runtime
253
+ - **Fly.io**: Deploy with Docker support
254
+ - **Railway**: Deploy with Docker socket access
255
+ - **Render**: Deploy with Docker enabled
256
+
257
+ > **Note**: Hugging Face Spaces does not support Docker-in-Docker, so this requires self-hosting or alternative platforms.
258
+
259
+ ## 📝 Examples
260
+
261
+ Check the `examples/` directory for sample code in multiple languages:
262
+
263
+ ### Python Examples
264
+ - `examples/hello_world.py` - Simple hello world
265
+ - `examples/python_stdin.py` - Input/output and calculations
266
+ - `examples/python_classes.py` - Object-oriented programming
267
+ - `examples/python_data.py` - Data processing and algorithms
268
+
269
+ ### JavaScript Examples
270
+ - `examples/hello_world.js` - Simple hello world
271
+ - `examples/javascript_arrays.js` - Array operations and functions
272
+ - `examples/javascript_objects.js` - Objects and classes
273
+
274
+ ### Bash Examples
275
+ - `examples/hello_world.sh` - Simple hello world
276
+ - `examples/bash_system_info.sh` - System information display
277
+ - `examples/bash_loops.sh` - Loop operations and functions
278
+
279
+ ## 🛠️ Development
280
+
281
+ ### Run Tests
282
+
283
+ ```bash
284
+ # Install dev dependencies
285
+ pip install pytest pytest-asyncio httpx
286
+
287
+ # Run tests
288
+ pytest tests/ -v
289
+ ```
290
+
291
+ ### Local Development
292
+
293
+ ```bash
294
+ # Run with hot reload
295
+ uvicorn app:app --reload --host 0.0.0.0 --port 7860
296
+ ```
297
+
298
+ ## 📊 Architecture
299
+
300
+ ```
301
+ ┌─────────────┐
302
+ │ Client │
303
+ └──────┬──────┘
304
+ │ HTTP POST /execute
305
+
306
+ ┌─────────────────┐
307
+ │ FastAPI App │
308
+ │ (app.py) │
309
+ └──────┬──────────┘
310
+
311
+
312
+ ┌─────────────────┐
313
+ │ SandboxExecutor │
314
+ │ (executor.py) │
315
+ └──────┬──────────┘
316
+ │ Docker SDK
317
+
318
+ ┌─────────────────┐
319
+ │ Ephemeral │
320
+ │ Container │
321
+ │ (Python/JS/Bash)│
322
+ └─────────────────┘
323
+
324
+ ▼ (cleanup)
325
+ ♻️ Removed
326
+ ```
327
+
328
+ ## ⚠️ Important Notes
329
+
330
+ 1. **Rate Limiting**: For production use, implement rate limiting to prevent abuse
331
+ 2. **Authentication**: Add authentication for public deployments
332
+ 3. **Monitoring**: Monitor Docker resource usage and container counts
333
+ 4. **Resource Costs**: Each execution creates a new container; consider costs at scale
334
+ 5. **Docker Requirement**: The host system must have Docker installed and accessible
335
+
336
+ ## 📄 License
337
+
338
+ This project is open-source and available under the MIT License.
339
+
340
+ ## 🤝 Contributing
341
+
342
+ Contributions welcome! Please feel free to submit issues or pull requests.
343
+
344
+ ## 🔗 Links
345
+
346
+ - [Hugging Face Space](https://huggingface.co/spaces/fariasultanacodes/isolated-sandbox)
347
+ - [Docker Documentation](https://docs.docker.com/)
348
+ - [FastAPI Documentation](https://fastapi.tiangolo.com/)
349
+
350
+ ---
351
+
352
+ **Built with ❤️ using FastAPI and Docker**
app.py CHANGED
@@ -1,7 +1,530 @@
1
- from fastapi import FastAPI
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- app = FastAPI()
4
 
5
  @app.get("/")
6
- def greet_json():
7
- return {"Hello": "World!"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, status, UploadFile, File, Path as FastAPIPath
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.responses import JSONResponse, Response
4
+ import logging
5
+ from contextlib import asynccontextmanager
6
+ from typing import List
7
+
8
+ from sandbox.executor import SandboxExecutor
9
+ from sandbox.session_manager import SessionManager
10
+ from sandbox.file_manager import FileManager
11
+ from sandbox.container_builder import ContainerBuilder
12
+ from sandbox.models import (
13
+ ExecutionRequest, ExecutionResponse, SandboxConfig, Language,
14
+ CreateSessionRequest, SessionResponse, FileInfo, FileUploadResponse,
15
+ ExecuteInSessionRequest, ExecuteFileRequest
16
+ )
17
+ from sandbox.language_runners import LanguageRunner
18
+
19
+ # Configure logging
20
+ logging.basicConfig(
21
+ level=logging.INFO,
22
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
23
+ )
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # Global instances
27
+ executor: SandboxExecutor = None
28
+ session_manager: SessionManager = None
29
+ file_manager: FileManager = None
30
+
31
+
32
+ @asynccontextmanager
33
+ async def lifespan(app: FastAPI):
34
+ """Initialize and cleanup resources"""
35
+ global executor, session_manager, file_manager
36
+ try:
37
+ # Build/verify devenv image
38
+ logger.info("Checking development environment image...")
39
+ builder = ContainerBuilder()
40
+ if not builder.ensure_devenv_image():
41
+ logger.warning("Dev environment image not available, sessions will fail")
42
+
43
+ # Initialize services
44
+ logger.info("Initializing sandbox services...")
45
+ executor = SandboxExecutor(SandboxConfig())
46
+ session_manager = SessionManager(SandboxConfig())
47
+ file_manager = FileManager()
48
+ logger.info("All services initialized successfully")
49
+ yield
50
+ except Exception as e:
51
+ logger.error(f"Failed to initialize services: {e}")
52
+ raise
53
+ finally:
54
+ # Cleanup on shutdown
55
+ if session_manager:
56
+ logger.info("Shutting down session manager...")
57
+ session_manager.shutdown()
58
+
59
+
60
+ app = FastAPI(
61
+ title="Code Execution Sandbox API",
62
+ description="Execute code in isolated containers with persistent VM-like sessions and file system operations",
63
+ version="2.0.0",
64
+ lifespan=lifespan
65
+ )
66
+
67
+ # CORS middleware
68
+ app.add_middleware(
69
+ CORSMiddleware,
70
+ allow_origins=["*"],
71
+ allow_credentials=True,
72
+ allow_methods=["*"],
73
+ allow_headers=["*"],
74
+ )
75
 
 
76
 
77
  @app.get("/")
78
+ def root():
79
+ """Root endpoint with API information"""
80
+ return {
81
+ "name": "Code Execution Sandbox API",
82
+ "version": "2.0.0",
83
+ "status": "running",
84
+ "features": {
85
+ "stateless_execution": "/execute",
86
+ "persistent_sessions": "/sessions",
87
+ "file_operations": True,
88
+ "multi_language": True
89
+ },
90
+ "supported_languages": ["python", "javascript", "bash"],
91
+ "endpoints": {
92
+ "execute": "/execute (stateless)",
93
+ "sessions": "/sessions (create/list)",
94
+ "session_detail": "/sessions/{session_id}",
95
+ "files": "/sessions/{session_id}/files",
96
+ "execute_in_session": "/sessions/{session_id}/execute",
97
+ "languages": "/languages",
98
+ "health": "/health"
99
+ }
100
+ }
101
+
102
+
103
+ @app.get("/health")
104
+ def health_check():
105
+ """Health check endpoint"""
106
+ try:
107
+ if executor and executor.client:
108
+ executor.client.ping()
109
+
110
+ # Check session manager
111
+ session_count = len(session_manager.sessions) if session_manager else 0
112
+
113
+ return {
114
+ "status": "healthy",
115
+ "docker": "connected",
116
+ "active_sessions": session_count
117
+ }
118
+ except Exception as e:
119
+ logger.error(f"Health check failed: {e}")
120
+ raise HTTPException(
121
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
122
+ detail=f"Service unhealthy: {str(e)}"
123
+ )
124
+
125
+
126
+ @app.get("/languages")
127
+ def list_languages():
128
+ """List all supported programming languages"""
129
+ return {
130
+ "languages": LanguageRunner.get_all_languages()
131
+ }
132
+
133
+
134
+ # ========== Stateless Execution (backward compatible) ==========
135
+
136
+ @app.post("/execute", response_model=ExecutionResponse)
137
+ def execute_code(request: ExecutionRequest):
138
+ """
139
+ Execute code in an isolated ephemeral container (stateless).
140
+
141
+ This is the original execution method - creates a fresh container,
142
+ executes code, and destroys the container immediately.
143
+ """
144
+ if not executor:
145
+ raise HTTPException(
146
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
147
+ detail="Sandbox executor not initialized"
148
+ )
149
+
150
+ try:
151
+ logger.info(f"Stateless execution: {request.language} code")
152
+ result = executor.execute(request)
153
+ return result
154
+ except Exception as e:
155
+ logger.error(f"Execution failed: {e}", exc_info=True)
156
+ raise HTTPException(
157
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
158
+ detail=f"Execution failed: {str(e)}"
159
+ )
160
+
161
+
162
+ # ========== Session Management ==========
163
+
164
+ @app.post("/sessions", response_model=SessionResponse, status_code=status.HTTP_201_CREATED)
165
+ def create_session(request: CreateSessionRequest):
166
+ """
167
+ Create a new persistent VM-like session.
168
+
169
+ The session is a long-running container with persistent storage,
170
+ supporting file uploads and multiple code executions.
171
+ """
172
+ if not session_manager:
173
+ raise HTTPException(
174
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
175
+ detail="Session manager not initialized"
176
+ )
177
+
178
+ try:
179
+ logger.info(f"Creating new session with metadata: {request.metadata}")
180
+ session = session_manager.create_session(request)
181
+ return session
182
+ except RuntimeError as e:
183
+ raise HTTPException(
184
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
185
+ detail=str(e)
186
+ )
187
+
188
+
189
+ @app.get("/sessions", response_model=List[SessionResponse])
190
+ def list_sessions():
191
+ """List all active sessions"""
192
+ if not session_manager:
193
+ raise HTTPException(
194
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
195
+ detail="Session manager not initialized"
196
+ )
197
+
198
+ sessions = session_manager.list_sessions()
199
+ return sessions
200
+
201
+
202
+ @app.get("/sessions/{session_id}", response_model=SessionResponse)
203
+ def get_session(session_id: str = FastAPIPath(..., description="Session ID")):
204
+ """Get session details by ID"""
205
+ if not session_manager:
206
+ raise HTTPException(
207
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
208
+ detail="Session manager not initialized"
209
+ )
210
+
211
+ session = session_manager.get_session(session_id)
212
+ if not session:
213
+ raise HTTPException(
214
+ status_code=status.HTTP_404_NOT_FOUND,
215
+ detail=f"Session {session_id} not found"
216
+ )
217
+
218
+ # Update file count
219
+ if file_manager:
220
+ try:
221
+ files = file_manager.list_files(session.container_id)
222
+ session.files_count = len(files)
223
+ except:
224
+ pass
225
+
226
+ return session
227
+
228
+
229
+ @app.delete("/sessions/{session_id}")
230
+ def destroy_session(session_id: str = FastAPIPath(..., description="Session ID")):
231
+ """Destroy a session and cleanup all resources"""
232
+ if not session_manager:
233
+ raise HTTPException(
234
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
235
+ detail="Session manager not initialized"
236
+ )
237
+
238
+ success = session_manager.destroy_session(session_id)
239
+ if not success:
240
+ raise HTTPException(
241
+ status_code=status.HTTP_404_NOT_FOUND,
242
+ detail=f"Session {session_id} not found"
243
+ )
244
+
245
+ return {"message": f"Session {session_id} destroyed successfully"}
246
+
247
+
248
+ # ========== File Operations ==========
249
+
250
+ @app.post("/sessions/{session_id}/files", response_model=FileUploadResponse)
251
+ async def upload_file(
252
+ session_id: str = FastAPIPath(..., description="Session ID"),
253
+ file: UploadFile = File(..., description="File to upload")
254
+ ):
255
+ """Upload a file to session workspace"""
256
+ if not session_manager or not file_manager:
257
+ raise HTTPException(
258
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
259
+ detail="Services not initialized"
260
+ )
261
+
262
+ # Get session
263
+ session = session_manager.get_session(session_id)
264
+ if not session:
265
+ raise HTTPException(
266
+ status_code=status.HTTP_404_NOT_FOUND,
267
+ detail=f"Session {session_id} not found"
268
+ )
269
+
270
+ try:
271
+ # Read file data
272
+ file_data = await file.read()
273
+
274
+ # Upload to container
275
+ result = file_manager.upload_file(
276
+ container_id=session.container_id,
277
+ filename=file.filename,
278
+ file_data=file_data
279
+ )
280
+
281
+ # Update session activity
282
+ session_manager.update_activity(session_id)
283
+
284
+ return result
285
+
286
+ except ValueError as e:
287
+ raise HTTPException(
288
+ status_code=status.HTTP_400_BAD_REQUEST,
289
+ detail=str(e)
290
+ )
291
+ except Exception as e:
292
+ logger.error(f"File upload failed: {e}", exc_info=True)
293
+ raise HTTPException(
294
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
295
+ detail=f"File upload failed: {str(e)}"
296
+ )
297
+
298
+
299
+ @app.get("/sessions/{session_id}/files", response_model=List[FileInfo])
300
+ def list_files(session_id: str = FastAPIPath(..., description="Session ID")):
301
+ """List files in session workspace"""
302
+ if not session_manager or not file_manager:
303
+ raise HTTPException(
304
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
305
+ detail="Services not initialized"
306
+ )
307
+
308
+ # Get session
309
+ session = session_manager.get_session(session_id)
310
+ if not session:
311
+ raise HTTPException(
312
+ status_code=status.HTTP_404_NOT_FOUND,
313
+ detail=f"Session {session_id} not found"
314
+ )
315
+
316
+ try:
317
+ files = file_manager.list_files(session.container_id)
318
+ return files
319
+ except Exception as e:
320
+ logger.error(f"File listing failed: {e}", exc_info=True)
321
+ raise HTTPException(
322
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
323
+ detail=f"File listing failed: {str(e)}"
324
+ )
325
+
326
+
327
+ @app.get("/sessions/{session_id}/files/{filepath:path}")
328
+ def download_file(
329
+ session_id: str = FastAPIPath(..., description="Session ID"),
330
+ filepath: str = FastAPIPath(..., description="File path relative to workspace")
331
+ ):
332
+ """Download a file from session workspace"""
333
+ if not session_manager or not file_manager:
334
+ raise HTTPException(
335
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
336
+ detail="Services not initialized"
337
+ )
338
+
339
+ # Get session
340
+ session = session_manager.get_session(session_id)
341
+ if not session:
342
+ raise HTTPException(
343
+ status_code=status.HTTP_404_NOT_FOUND,
344
+ detail=f"Session {session_id} not found"
345
+ )
346
+
347
+ try:
348
+ file_data = file_manager.download_file(session.container_id, filepath)
349
+
350
+ # Determine content type
351
+ import mimetypes
352
+ content_type, _ = mimetypes.guess_type(filepath)
353
+
354
+ return Response(
355
+ content=file_data,
356
+ media_type=content_type or "application/octet-stream",
357
+ headers={
358
+ "Content-Disposition": f"attachment; filename={filepath.split('/')[-1]}"
359
+ }
360
+ )
361
+
362
+ except ValueError as e:
363
+ raise HTTPException(
364
+ status_code=status.HTTP_404_NOT_FOUND,
365
+ detail=str(e)
366
+ )
367
+ except Exception as e:
368
+ logger.error(f"File download failed: {e}", exc_info=True)
369
+ raise HTTPException(
370
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
371
+ detail=f"File download failed: {str(e)}"
372
+ )
373
+
374
+
375
+ # ========== Execute in Session ==========
376
+
377
+ @app.post("/sessions/{session_id}/execute", response_model=ExecutionResponse)
378
+ def execute_in_session(
379
+ session_id: str = FastAPIPath(..., description="Session ID"),
380
+ request: ExecuteInSessionRequest = None
381
+ ):
382
+ """Execute code in an existing session (persistent state)"""
383
+ if not session_manager:
384
+ raise HTTPException(
385
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
386
+ detail="Session manager not initialized"
387
+ )
388
+
389
+ # Get session
390
+ session = session_manager.get_session(session_id)
391
+ if not session:
392
+ raise HTTPException(
393
+ status_code=status.HTTP_404_NOT_FOUND,
394
+ detail=f"Session {session_id} not found"
395
+ )
396
+
397
+ try:
398
+ import time
399
+ from docker.errors import DockerException
400
+
401
+ container = executor.client.containers.get(session.container_id)
402
+ runner_config = LanguageRunner.get_runner_config(request.language)
403
+
404
+ start_time = time.time()
405
+
406
+ # Execute command in running container
407
+ exec_result = container.exec_run(
408
+ cmd=runner_config["command"] + [request.code],
409
+ workdir=request.working_dir,
410
+ demux=True,
411
+ stream=False
412
+ )
413
+
414
+ execution_time = time.time() - start_time
415
+
416
+ # Parse output
417
+ stdout = exec_result.output[0].decode('utf-8', errors='replace') if exec_result.output[0] else ""
418
+ stderr = exec_result.output[1].decode('utf-8', errors='replace') if exec_result.output[1] else ""
419
+
420
+ # Update session activity
421
+ session_manager.update_activity(session_id)
422
+
423
+ return ExecutionResponse(
424
+ stdout=stdout,
425
+ stderr=stderr,
426
+ exit_code=exec_result.exit_code,
427
+ execution_time=round(execution_time, 3),
428
+ error=None if exec_result.exit_code == 0 else "Execution failed"
429
+ )
430
+
431
+ except DockerException as e:
432
+ logger.error(f"Docker error: {e}", exc_info=True)
433
+ raise HTTPException(
434
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
435
+ detail=f"Execution failed: {str(e)}"
436
+ )
437
+
438
+
439
+ @app.post("/sessions/{session_id}/execute-file", response_model=ExecutionResponse)
440
+ def execute_file_in_session(
441
+ session_id: str = FastAPIPath(..., description="Session ID"),
442
+ request: ExecuteFileRequest = None
443
+ ):
444
+ """Execute an uploaded file in session"""
445
+ if not session_manager:
446
+ raise HTTPException(
447
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
448
+ detail="Session manager not initialized"
449
+ )
450
+
451
+ # Get session
452
+ session = session_manager.get_session(session_id)
453
+ if not session:
454
+ raise HTTPException(
455
+ status_code=status.HTTP_404_NOT_FOUND,
456
+ detail=f"Session {session_id} not found"
457
+ )
458
+
459
+ try:
460
+ import time
461
+ container = executor.client.containers.get(session.container_id)
462
+ runner_config = LanguageRunner.get_runner_config(request.language)
463
+
464
+ # Build command based on language
465
+ if request.language == Language.PYTHON:
466
+ cmd = ["python", request.filepath] + request.args
467
+ elif request.language == Language.JAVASCRIPT:
468
+ cmd = ["node", request.filepath] + request.args
469
+ elif request.language == Language.BASH:
470
+ cmd = ["bash", request.filepath] + request.args
471
+ else:
472
+ cmd = runner_config["command"] + [request.filepath] + request.args
473
+
474
+ start_time = time.time()
475
+
476
+ # Execute file
477
+ exec_result = container.exec_run(
478
+ cmd=cmd,
479
+ workdir="/workspace",
480
+ demux=True,
481
+ stream=False
482
+ )
483
+
484
+ execution_time = time.time() - start_time
485
+
486
+ # Parse output
487
+ stdout = exec_result.output[0].decode('utf-8', errors='replace') if exec_result.output[0] else ""
488
+ stderr = exec_result.output[1].decode('utf-8', errors='replace') if exec_result.output[1] else ""
489
+
490
+ # Update session activity
491
+ session_manager.update_activity(session_id)
492
+
493
+ return ExecutionResponse(
494
+ stdout=stdout,
495
+ stderr=stderr,
496
+ exit_code=exec_result.exit_code,
497
+ execution_time=round(execution_time, 3),
498
+ error=None if exec_result.exit_code == 0 else "Execution failed"
499
+ )
500
+
501
+ except Exception as e:
502
+ logger.error(f"File execution failed: {e}", exc_info=True)
503
+ raise HTTPException(
504
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
505
+ detail=f"File execution failed: {str(e)}"
506
+ )
507
+
508
+
509
+ @app.exception_handler(Exception)
510
+ async def global_exception_handler( request, exc):
511
+ """Global exception handler"""
512
+ logger.error(f"Unhandled exception: {exc}", exc_info=True)
513
+ return JSONResponse(
514
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
515
+ content={
516
+ "error": "Internal server error",
517
+ "detail": str(exc)
518
+ }
519
+ )
520
+
521
+
522
+ if __name__ == "__main__":
523
+ import uvicorn
524
+ uvicorn.run(
525
+ "app:app",
526
+ host="0.0.0.0",
527
+ port=7860,
528
+ reload=False,
529
+ log_level="info"
530
+ )
examples/bash_loops.sh ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Bash Example: Loop and Array Processing
3
+ echo "=== Bash Scripting Examples ==="
4
+ echo
5
+
6
+ # Array of numbers
7
+ numbers=(1 2 3 4 5 6 7 8 9 10)
8
+
9
+ # Calculate sum using loop
10
+ sum=0
11
+ for num in "${numbers[@]}"; do
12
+ sum=$((sum + num))
13
+ done
14
+
15
+ echo "Array: ${numbers[*]}"
16
+ echo "Sum of numbers: $sum"
17
+ echo "Average: $((sum / ${#numbers[@]}))"
18
+ echo
19
+
20
+ # Find even numbers
21
+ echo "Even numbers:"
22
+ for num in "${numbers[@]}"; do
23
+ if [ $((num % 2)) -eq 0 ]; then
24
+ echo " $num"
25
+ fi
26
+ done
27
+ echo
28
+
29
+ # Create a simple counter
30
+ count=1
31
+ while [ $count -le 5 ]; do
32
+ echo "Count: $count"
33
+ count=$((count + 1))
34
+ done
35
+ echo
36
+
37
+ # Function example
38
+ greet() {
39
+ local name=$1
40
+ echo "Hello, $name! Welcome to bash scripting."
41
+ }
42
+
43
+ greet "Alice"
44
+ greet "Bob"
examples/bash_system_info.sh ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Bash Example: System Information
3
+ echo "=== System Information ==="
4
+ echo
5
+
6
+ # Display basic system info
7
+ echo "Hostname: $(hostname)"
8
+ echo "Current user: $(whoami)"
9
+ echo "Current directory: $(pwd)"
10
+ echo "Current time: $(date)"
11
+ echo
12
+
13
+ # Display system resources
14
+ echo "Memory usage:"
15
+ free -h
16
+ echo
17
+
18
+ echo "Disk usage:"
19
+ df -h / | tail -1
20
+ echo
21
+
22
+ # Display OS information
23
+ echo "Operating System:"
24
+ cat /etc/os-release | grep PRETTY_NAME
25
+ echo
26
+
27
+ # Process information
28
+ echo "Number of running processes: $(ps aux | wc -l)"
29
+ echo "Current shell: $SHELL"
examples/hello_world.js ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ // Example: Hello World in JavaScript
2
+ console.log("Hello, World!");
examples/hello_world.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Example: Hello World in Python
2
+ print("Hello, World!")
examples/hello_world.sh ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ #!/bin/bash
2
+ # Example: Hello World in Bash
3
+ echo "Hello, World!"
examples/javascript_arrays.js ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // JavaScript Example: Array Processing and Functions
2
+ const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
3
+
4
+ // Calculate sum of all numbers
5
+ const sum = numbers.reduce((acc, num) => acc + num, 0);
6
+ console.log(`Sum of numbers: ${sum}`);
7
+
8
+ // Find even numbers
9
+ const evenNumbers = numbers.filter(num => num % 2 === 0);
10
+ console.log(`Even numbers: ${evenNumbers.join(', ')}`);
11
+
12
+ // Calculate square of each number
13
+ const squares = numbers.map(num => num * num);
14
+ console.log(`Squares: ${squares.join(', ')}`);
15
+
16
+ // Fibonacci sequence
17
+ function fibonacci(n) {
18
+ if (n <= 1) return n;
19
+ return fibonacci(n - 1) + fibonacci(n - 2);
20
+ }
21
+
22
+ console.log('\nFirst 10 Fibonacci numbers:');
23
+ for (let i = 0; i < 10; i++) {
24
+ console.log(fibonacci(i));
25
+ }
examples/javascript_objects.js ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // JavaScript Example: Objects and Classes
2
+ class Calculator {
3
+ constructor() {
4
+ this.history = [];
5
+ }
6
+
7
+ add(a, b) {
8
+ const result = a + b;
9
+ this.history.push(`${a} + ${b} = ${result}`);
10
+ return result;
11
+ }
12
+
13
+ multiply(a, b) {
14
+ const result = a * b;
15
+ this.history.push(`${a} * ${b} = ${result}`);
16
+ return result;
17
+ }
18
+
19
+ showHistory() {
20
+ console.log('Calculation History:');
21
+ this.history.forEach(entry => console.log(entry));
22
+ }
23
+ }
24
+
25
+ // Create calculator instance
26
+ const calc = new Calculator();
27
+
28
+ // Perform calculations
29
+ console.log('10 + 5 =', calc.add(10, 5));
30
+ console.log('10 * 5 =', calc.multiply(10, 5));
31
+ console.log('7 * 8 =', calc.multiply(7, 8));
32
+
33
+ // Show history
34
+ calc.showHistory();
35
+
36
+ // Object with methods
37
+ const person = {
38
+ name: 'Alice',
39
+ age: 30,
40
+ greet() {
41
+ return `Hello, I'm ${this.name} and I'm ${this.age} years old.`;
42
+ },
43
+ haveBirthday() {
44
+ this.age++;
45
+ }
46
+ };
47
+
48
+ console.log(person.greet());
49
+ person.haveBirthday();
50
+ console.log(person.greet());
examples/python_classes.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python Example: Classes and Object-Oriented Programming
2
+ class BankAccount:
3
+ def __init__(self, owner, balance=0):
4
+ self.owner = owner
5
+ self.balance = balance
6
+ self.transactions = []
7
+
8
+ def deposit(self, amount):
9
+ if amount > 0:
10
+ self.balance += amount
11
+ self.transactions.append(f"Deposited: ${amount}")
12
+ return True
13
+ return False
14
+
15
+ def withdraw(self, amount):
16
+ if amount <= self.balance and amount > 0:
17
+ self.balance -= amount
18
+ self.transactions.append(f"Withdrew: ${amount}")
19
+ return True
20
+ return False
21
+
22
+ def get_balance(self):
23
+ return f"${self.balance}"
24
+
25
+ def show_transactions(self):
26
+ print(f"Transaction history for {self.owner}:")
27
+ for transaction in self.transactions:
28
+ print(f" - {transaction}")
29
+
30
+ # Create account
31
+ account = BankAccount("John Doe", 1000)
32
+
33
+ print(f"Account created for {account.owner}")
34
+ print(f"Initial balance: {account.get_balance()}")
35
+
36
+ # Perform transactions
37
+ account.deposit(500)
38
+ account.withdraw(200)
39
+ account.deposit(100)
40
+
41
+ print(f"Current balance: {account.get_balance()}")
42
+ account.show_transactions()
examples/python_data.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python Example: Data Processing and File Operations
2
+ import json
3
+
4
+ # Sample data
5
+ data = [
6
+ {"name": "Alice", "age": 25, "city": "New York"},
7
+ {"name": "Bob", "age": 30, "city": "San Francisco"},
8
+ {"name": "Charlie", "age": 35, "city": "Boston"},
9
+ {"name": "Diana", "age": 28, "city": "Chicago"},
10
+ {"name": "Eve", "age": 22, "city": "New York"}
11
+ ]
12
+
13
+ print("=== Data Processing Demo ===\n")
14
+
15
+ # Filter people over 27
16
+ older_people = [person for person in data if person["age"] > 27]
17
+ print(f"People over 27: {len(older_people)}")
18
+
19
+ # Group by city
20
+ cities = {}
21
+ for person in data:
22
+ city = person["city"]
23
+ if city not in cities:
24
+ cities[city] = []
25
+ cities[city].append(person)
26
+
27
+ print("\nPeople by city:")
28
+ for city, people in cities.items():
29
+ print(f" {city}: {len(people)} people")
30
+
31
+ # Calculate average age
32
+ total_age = sum(person["age"] for person in data)
33
+ avg_age = total_age / len(data)
34
+ print(f"\nAverage age: {avg_age:.2f}")
35
+
36
+ # Find youngest and oldest
37
+ youngest = min(data, key=lambda x: x["age"])
38
+ oldest = max(data, key=lambda x: x["age"])
39
+
40
+ print(f"Youngest: {youngest['name']} ({youngest['age']})")
41
+ print(f"Oldest: {oldest['name']} ({oldest['age']})")
examples/python_stdin.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python Example: Input and Output
2
+ name = input("What is your name? ")
3
+ age = int(input("How old are you? "))
4
+
5
+ print(f"Hello {name}!")
6
+ print(f"You will be {age + 1} next year!")
7
+
8
+ # Calculate factorial
9
+ def factorial(n):
10
+ if n <= 1:
11
+ return 1
12
+ return n * factorial(n - 1)
13
+
14
+ print(f"Factorial of {age} is {factorial(age)}")
requirements-dev.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ pytest
2
+ pytest-asyncio
3
+ httpx
requirements.txt CHANGED
@@ -1,2 +1,7 @@
1
  fastapi
2
- uvicorn[standard]
 
 
 
 
 
 
1
  fastapi
2
+ uvicorn[standard]
3
+ docker
4
+ pydantic
5
+ pydantic-settings
6
+ python-multipart
7
+ aiofiles
sandbox/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Sandbox package for isolated code execution
sandbox/container_builder.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import docker
2
+ from docker.errors import DockerException, ImageNotFound
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class ContainerBuilder:
10
+ """Builds and manages the development environment Docker image"""
11
+
12
+ IMAGE_NAME = "sandbox-devenv"
13
+ IMAGE_TAG = "latest"
14
+
15
+ def __init__(self):
16
+ try:
17
+ self.client = docker.from_env()
18
+ logger.info("Container builder initialized")
19
+ except DockerException as e:
20
+ logger.error(f"Failed to initialize Docker client: {e}")
21
+ raise RuntimeError("Docker is not available") from e
22
+
23
+ def image_exists(self) -> bool:
24
+ """Check if devenv image exists"""
25
+ try:
26
+ self.client.images.get(f"{self.IMAGE_NAME}:{self.IMAGE_TAG}")
27
+ return True
28
+ except ImageNotFound:
29
+ return False
30
+
31
+ def build_devenv_image(self) -> bool:
32
+ """
33
+ Build the development environment image.
34
+
35
+ Returns:
36
+ True if successful, False otherwise
37
+ """
38
+ try:
39
+ dockerfile_path = Path(__file__).parent / "images" / "devenv.Dockerfile"
40
+
41
+ if not dockerfile_path.exists():
42
+ logger.error(f"Dockerfile not found: {dockerfile_path}")
43
+ return False
44
+
45
+ logger.info("Building development environment image (this may take several minutes)...")
46
+
47
+ # Build image
48
+ image, build_logs = self.client.images.build(
49
+ path=str(dockerfile_path.parent),
50
+ dockerfile=str(dockerfile_path.name),
51
+ tag=f"{self.IMAGE_NAME}:{self.IMAGE_TAG}",
52
+ rm=True, # Remove intermediate containers
53
+ pull=True, # Pull base image
54
+ decode=True # Decode build logs
55
+ )
56
+
57
+ # Print build progress
58
+ for log in build_logs:
59
+ if 'stream' in log:
60
+ print(log['stream'], end='')
61
+ elif 'error' in log:
62
+ logger.error(f"Build error: {log['error']}")
63
+ return False
64
+
65
+ logger.info(f"Successfully built {self.IMAGE_NAME}:{self.IMAGE_TAG}")
66
+ return True
67
+
68
+ except Exception as e:
69
+ logger.error(f"Failed to build image: {e}", exc_info=True)
70
+ return False
71
+
72
+ def ensure_devenv_image(self) -> bool:
73
+ """
74
+ Ensure devenv image exists, build if necessary.
75
+
76
+ Returns:
77
+ True if image is available, False otherwise
78
+ """
79
+ if self.image_exists():
80
+ logger.info(f"Image {self.IMAGE_NAME}:{self.IMAGE_TAG} already exists")
81
+ return True
82
+
83
+ logger.info(f"Image {self.IMAGE_NAME}:{self.IMAGE_TAG} not found, building...")
84
+ return self.build_devenv_image()
sandbox/executor.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import docker
2
+ from docker.errors import DockerException, ContainerError, ImageNotFound, APIError
3
+ import time
4
+ import logging
5
+ from typing import Optional
6
+ from sandbox.models import ExecutionRequest, ExecutionResponse, SandboxConfig, Language
7
+ from sandbox.language_runners import LanguageRunner
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class SandboxExecutor:
13
+ """
14
+ Executes code in isolated Docker containers with security controls.
15
+
16
+ Features:
17
+ - Resource limits (CPU, memory)
18
+ - Network isolation
19
+ - Automatic cleanup
20
+ - Timeout enforcement
21
+ - Read-only filesystem
22
+ """
23
+
24
+ def __init__(self, config: Optional[SandboxConfig] = None):
25
+ self.config = config or SandboxConfig()
26
+ try:
27
+ self.client = docker.from_env()
28
+ # Test Docker connection
29
+ self.client.ping()
30
+ logger.info("Docker client initialized successfully")
31
+ except DockerException as e:
32
+ logger.error(f"Failed to initialize Docker client: {e}")
33
+ raise RuntimeError(
34
+ "Docker is not available. Please ensure Docker is running and accessible."
35
+ ) from e
36
+
37
+ def _prepare_container_config(
38
+ self,
39
+ request: ExecutionRequest,
40
+ runner_config: dict
41
+ ) -> dict:
42
+ """Prepare Docker container configuration with security settings"""
43
+
44
+ # Calculate resource limits
45
+ memory_limit = f"{request.memory_limit}m"
46
+ cpu_quota = 50000 # 0.5 CPU cores (out of 100000)
47
+
48
+ container_config = {
49
+ "image": runner_config["image"],
50
+ "command": runner_config["command"] + [request.code],
51
+ "stdin_open": bool(request.stdin),
52
+ "detach": True,
53
+ "remove": False, # We'll remove manually after getting logs
54
+
55
+ # Security settings
56
+ "network_mode": "none" if not self.config.enable_network else "bridge",
57
+ "read_only": self.config.read_only_root,
58
+ "security_opt": ["no-new-privileges"],
59
+
60
+ # Resource limits
61
+ "mem_limit": memory_limit,
62
+ "memswap_limit": memory_limit, # Disable swap
63
+ "cpu_quota": cpu_quota,
64
+ "cpu_period": 100000,
65
+
66
+ # Prevent fork bombs
67
+ "pids_limit": 50,
68
+
69
+ # Working directory
70
+ "working_dir": "/tmp",
71
+
72
+ # User (non-root when possible)
73
+ "user": "nobody" if runner_config["image"] != "bash:5.2-alpine" else None,
74
+ }
75
+
76
+ return container_config
77
+
78
+ def execute(self, request: ExecutionRequest) -> ExecutionResponse:
79
+ """
80
+ Execute code in an isolated container.
81
+
82
+ Args:
83
+ request: Execution request with code and parameters
84
+
85
+ Returns:
86
+ ExecutionResponse with stdout, stderr, and execution metadata
87
+ """
88
+ start_time = time.time()
89
+ container = None
90
+
91
+ try:
92
+ # Get language-specific configuration
93
+ runner_config = LanguageRunner.get_runner_config(request.language)
94
+
95
+ # Prepare container configuration
96
+ container_config = self._prepare_container_config(request, runner_config)
97
+
98
+ # Pull image if not available
99
+ try:
100
+ self.client.images.get(runner_config["image"])
101
+ except ImageNotFound:
102
+ logger.info(f"Pulling image {runner_config['image']}...")
103
+ self.client.images.pull(runner_config["image"])
104
+
105
+ # Create and start container
106
+ logger.info(f"Executing {request.language} code in container")
107
+ container = self.client.containers.create(**container_config)
108
+ container.start()
109
+
110
+ # Provide stdin if specified
111
+ if request.stdin:
112
+ sock = container.attach_socket(params={'stdin': 1, 'stream': 1})
113
+ sock._sock.sendall(request.stdin.encode())
114
+ sock.close()
115
+
116
+ # Wait for container to finish with timeout
117
+ try:
118
+ result = container.wait(timeout=request.timeout)
119
+ exit_code = result.get("StatusCode", -1)
120
+ error_msg = None
121
+ except Exception as e:
122
+ # Timeout or other error
123
+ logger.warning(f"Container execution timeout or error: {e}")
124
+ container.stop(timeout=1)
125
+ exit_code = 124 # Timeout exit code
126
+ error_msg = f"Execution timed out after {request.timeout} seconds"
127
+
128
+ # Get logs (stdout and stderr combined)
129
+ try:
130
+ logs = container.logs(stdout=True, stderr=True).decode('utf-8', errors='replace')
131
+
132
+ # Truncate if too large
133
+ if len(logs) > self.config.max_output_size:
134
+ logs = logs[:self.config.max_output_size] + "\n... (output truncated)"
135
+
136
+ # Try to separate stdout and stderr (Docker combines them)
137
+ stdout = logs
138
+ stderr = ""
139
+
140
+ # If there's an error, try to extract stderr
141
+ if exit_code != 0:
142
+ stderr = logs
143
+ stdout = ""
144
+
145
+ except Exception as e:
146
+ logger.error(f"Failed to get container logs: {e}")
147
+ stdout = ""
148
+ stderr = str(e)
149
+
150
+ execution_time = time.time() - start_time
151
+
152
+ return ExecutionResponse(
153
+ stdout=stdout,
154
+ stderr=stderr,
155
+ exit_code=exit_code,
156
+ execution_time=round(execution_time, 3),
157
+ error=error_msg
158
+ )
159
+
160
+ except ImageNotFound as e:
161
+ logger.error(f"Image not found: {e}")
162
+ return ExecutionResponse(
163
+ stdout="",
164
+ stderr="",
165
+ exit_code=-1,
166
+ execution_time=time.time() - start_time,
167
+ error=f"Language image not available: {runner_config['image']}"
168
+ )
169
+
170
+ except ContainerError as e:
171
+ logger.error(f"Container error: {e}")
172
+ return ExecutionResponse(
173
+ stdout="",
174
+ stderr=str(e),
175
+ exit_code=e.exit_status,
176
+ execution_time=time.time() - start_time,
177
+ error="Container execution failed"
178
+ )
179
+
180
+ except APIError as e:
181
+ logger.error(f"Docker API error: {e}")
182
+ return ExecutionResponse(
183
+ stdout="",
184
+ stderr="",
185
+ exit_code=-1,
186
+ execution_time=time.time() - start_time,
187
+ error=f"Docker API error: {str(e)}"
188
+ )
189
+
190
+ except Exception as e:
191
+ logger.error(f"Unexpected error during execution: {e}", exc_info=True)
192
+ return ExecutionResponse(
193
+ stdout="",
194
+ stderr="",
195
+ exit_code=-1,
196
+ execution_time=time.time() - start_time,
197
+ error=f"Unexpected error: {str(e)}"
198
+ )
199
+
200
+ finally:
201
+ # Always cleanup container
202
+ if container:
203
+ try:
204
+ container.remove(force=True)
205
+ logger.debug(f"Container {container.id[:12]} removed")
206
+ except Exception as e:
207
+ logger.warning(f"Failed to remove container: {e}")
208
+
209
+ def cleanup_all(self):
210
+ """Remove all stopped containers (maintenance task)"""
211
+ try:
212
+ containers = self.client.containers.list(all=True, filters={"status": "exited"})
213
+ for container in containers:
214
+ try:
215
+ container.remove()
216
+ logger.info(f"Cleaned up container {container.id[:12]}")
217
+ except Exception as e:
218
+ logger.warning(f"Failed to remove container {container.id[:12]}: {e}")
219
+ except Exception as e:
220
+ logger.error(f"Failed to cleanup containers: {e}")
sandbox/file_manager.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import docker
2
+ from docker.errors import DockerException, NotFound
3
+ import os
4
+ import logging
5
+ from datetime import datetime
6
+ from typing import Optional, List, BinaryIO
7
+ from pathlib import Path
8
+ import mimetypes
9
+
10
+ from sandbox.models import FileInfo, FileUploadResponse
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class FileManager:
16
+ """
17
+ Manages file operations within session containers.
18
+
19
+ Handles:
20
+ - File uploads to session volumes
21
+ - File downloads from sessions
22
+ - Directory listing
23
+ - File deletion
24
+ - Path validation and security
25
+ """
26
+
27
+ # Security settings
28
+ MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
29
+ MAX_SESSION_SIZE = 100 * 1024 * 1024 # 100MB
30
+
31
+ ALLOWED_EXTENSIONS = {
32
+ '.py', '.js', '.ts', '.java', '.go', '.rs',
33
+ '.sh', '.bash', '.txt', '.md', '.json',
34
+ '.yaml', '.yml', '.toml', '.xml', '.html',
35
+ '.css', '.c', '.cpp', '.h', '.hpp'
36
+ }
37
+
38
+ def __init__(self):
39
+ try:
40
+ self.client = docker.from_env()
41
+ logger.info("File manager initialized")
42
+ except DockerException as e:
43
+ logger.error(f"Failed to initialize Docker client: {e}")
44
+ raise RuntimeError("Docker is not available") from e
45
+
46
+ def _validate_path(self, filepath: str) -> str:
47
+ """
48
+ Validate and sanitize file path.
49
+
50
+ Prevents path traversal attacks.
51
+ """
52
+ # Remove any path traversal attempts
53
+ clean_path = os.path.normpath(filepath).lstrip('/')
54
+
55
+ # Ensure it doesn't escape workspace
56
+ if clean_path.startswith('..') or '/../' in clean_path:
57
+ raise ValueError("Path traversal detected")
58
+
59
+ return f"/workspace/{clean_path}"
60
+
61
+ def _validate_extension(self, filename: str) -> bool:
62
+ """Check if file extension is allowed"""
63
+ ext = os.path.splitext(filename)[1].lower()
64
+ return ext in self.ALLOWED_EXTENSIONS or ext == ''
65
+
66
+ def upload_file(
67
+ self,
68
+ container_id: str,
69
+ filename: str,
70
+ file_data: bytes
71
+ ) -> FileUploadResponse:
72
+ """
73
+ Upload file to session container.
74
+
75
+ Args:
76
+ container_id: Target container ID
77
+ filename: Name of file to create
78
+ file_data: File binary data
79
+
80
+ Returns:
81
+ FileUploadResponse with file details
82
+ """
83
+ # Validate file size
84
+ if len(file_data) > self.MAX_FILE_SIZE:
85
+ raise ValueError(f"File size exceeds limit of {self.MAX_FILE_SIZE} bytes")
86
+
87
+ # Validate filename
88
+ if not filename or '/' in filename or '\\\\' in filename:
89
+ raise ValueError("Invalid filename")
90
+
91
+ # Validate extension
92
+ if not self._validate_extension(filename):
93
+ raise ValueError(f"File extension not allowed. Allowed: {', '.join(self.ALLOWED_EXTENSIONS)}")
94
+
95
+ try:
96
+ container = self.client.containers.get(container_id)
97
+
98
+ # Create file path
99
+ file_path = f"/workspace/{filename}"
100
+
101
+ # Write file using docker exec
102
+ # Create temp directory and write file
103
+ import tarfile
104
+ import io
105
+
106
+ # Create tar archive in memory
107
+ tar_stream = io.BytesIO()
108
+ tar = tarfile.open(fileobj=tar_stream, mode='w')
109
+
110
+ # Create file info
111
+ tarinfo = tarfile.TarInfo(name=filename)
112
+ tarinfo.size = len(file_data)
113
+ tarinfo.mtime = datetime.utcnow().timestamp()
114
+
115
+ # Add file to tar
116
+ tar.addfile(tarinfo, io.BytesIO(file_data))
117
+ tar.close()
118
+
119
+ # Upload tar to container
120
+ tar_stream.seek(0)
121
+ container.put_archive('/workspace', tar_stream)
122
+
123
+ logger.info(f"Uploaded file {filename} to container {container_id[:12]}")
124
+
125
+ # Detect MIME type
126
+ mime_type, _ = mimetypes.guess_type(filename)
127
+
128
+ return FileUploadResponse(
129
+ filename=filename,
130
+ path=file_path,
131
+ size=len(file_data),
132
+ message=f"File '{filename}' uploaded successfully"
133
+ )
134
+
135
+ except NotFound:
136
+ raise ValueError(f"Container {container_id} not found")
137
+ except Exception as e:
138
+ logger.error(f"Error uploading file: {e}", exc_info=True)
139
+ raise RuntimeError(f"Failed to upload file: {str(e)}")
140
+
141
+ def download_file(self, container_id: str, filepath: str) -> bytes:
142
+ """
143
+ Download file from session container.
144
+
145
+ Args:
146
+ container_id: Source container ID
147
+ filepath: Path to file (relative to /workspace)
148
+
149
+ Returns:
150
+ File binary data
151
+ """
152
+ # Validate and sanitize path
153
+ safe_path = self._validate_path(filepath)
154
+
155
+ try:
156
+ container = self.client.containers.get(container_id)
157
+
158
+ # Get file as tar stream
159
+ bits, stat = container.get_archive(safe_path)
160
+
161
+ # Extract file from tar
162
+ import tarfile
163
+ import io
164
+
165
+ tar_stream = io.BytesIO()
166
+ for chunk in bits:
167
+ tar_stream.write(chunk)
168
+ tar_stream.seek(0)
169
+
170
+ tar = tarfile.open(fileobj=tar_stream)
171
+
172
+ # Get first member (the file)
173
+ member = tar.getmembers()[0]
174
+ file_obj = tar.extractfile(member)
175
+ file_data = file_obj.read()
176
+
177
+ logger.info(f"Downloaded file {filepath} from container {container_id[:12]}")
178
+ return file_data
179
+
180
+ except NotFound:
181
+ raise ValueError(f"File {filepath} not found in container")
182
+ except Exception as e:
183
+ logger.error(f"Error downloading file: {e}", exc_info=True)
184
+ raise RuntimeError(f"Failed to download file: {str(e)}")
185
+
186
+ def list_files(self, container_id: str, directory: str = "/workspace") -> List[FileInfo]:
187
+ """
188
+ List files in session directory.
189
+
190
+ Args:
191
+ container_id: Container ID
192
+ directory: Directory to list
193
+
194
+ Returns:
195
+ List of FileInfo objects
196
+ """
197
+ # Validate path
198
+ safe_dir = self._validate_path(directory)
199
+
200
+ try:
201
+ container = self.client.containers.get(container_id)
202
+
203
+ # Execute ls command
204
+ result = container.exec_run(
205
+ f"find {safe_dir} -maxdepth 1 -type f -exec stat -c '%n|%s|%Y' {{}} \\;",
206
+ demux=True
207
+ )
208
+
209
+ if result.exit_code != 0:
210
+ logger.warning(f"Failed to list files: {result.output}")
211
+ return []
212
+
213
+ stdout = result.output[0].decode('utf-8') if result.output[0] else ""
214
+
215
+ files = []
216
+ for line in stdout.strip().split('\\n'):
217
+ if not line:
218
+ continue
219
+
220
+ try:
221
+ path, size, mtime = line.split('|')
222
+ filename = os.path.basename(path)
223
+
224
+ # Get MIME type
225
+ mime_type, _ = mimetypes.guess_type(filename)
226
+
227
+ files.append(FileInfo(
228
+ filename=filename,
229
+ path=path,
230
+ size=int(size),
231
+ modified_at=datetime.fromtimestamp(int(mtime)),
232
+ mime_type=mime_type or 'application/octet-stream'
233
+ ))
234
+ except Exception as e:
235
+ logger.warning(f"Failed to parse file info: {line}, error: {e}")
236
+ continue
237
+
238
+ return files
239
+
240
+ except NotFound:
241
+ raise ValueError(f"Container {container_id} not found")
242
+ except Exception as e:
243
+ logger.error(f"Error listing files: {e}", exc_info=True)
244
+ return []
245
+
246
+ def delete_file(self, container_id: str, filepath: str) -> bool:
247
+ """
248
+ Delete file from session.
249
+
250
+ Args:
251
+ container_id: Container ID
252
+ filepath: Path to file
253
+
254
+ Returns:
255
+ True if deleted successfully
256
+ """
257
+ # Validate path
258
+ safe_path = self._validate_path(filepath)
259
+
260
+ try:
261
+ container = self.client.containers.get(container_id)
262
+
263
+ # Execute rm command
264
+ result = container.exec_run(f"rm {safe_path}")
265
+
266
+ if result.exit_code == 0:
267
+ logger.info(f"Deleted file {filepath} from container {container_id[:12]}")
268
+ return True
269
+ else:
270
+ logger.warning(f"Failed to delete file: {result.output}")
271
+ return False
272
+
273
+ except NotFound:
274
+ raise ValueError(f"Container {container_id} not found")
275
+ except Exception as e:
276
+ logger.error(f"Error deleting file: {e}", exc_info=True)
277
+ return False
sandbox/images/bash.Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Minimal, hardened Bash execution image
2
+ FROM bash:5.2-alpine
3
+
4
+ # Install security updates and minimal utilities
5
+ RUN apk update && apk upgrade && \
6
+ apk add --no-cache \
7
+ coreutils \
8
+ procps \
9
+ net-tools \
10
+ curl \
11
+ && \
12
+ rm -rf /var/cache/apk/*
13
+
14
+ # Create non-root user
15
+ RUN adduser -D -s /bin/bash sandbox
16
+
17
+ # Set working directory
18
+ WORKDIR /sandbox
19
+
20
+ # Change ownership to sandbox user
21
+ RUN chown -R sandbox:sandbox /sandbox
22
+
23
+ # Switch to non-root user
24
+ USER sandbox
25
+
26
+ # Default command
27
+ CMD ["bash"]
sandbox/images/devenv.Dockerfile ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Multi-Language Development Environment
2
+ # Based on Ubuntu 24.04 with Python, Node.js, Java, Go, Rust, Docker
3
+
4
+ FROM ubuntu:24.04
5
+
6
+ ENV DEBIAN_FRONTEND=noninteractive
7
+ ENV TZ=UTC
8
+
9
+ # Install base utilities
10
+ RUN apt-get update && apt-get install -y \\
11
+ curl wget git build-essential ca-certificates gnupg lsb-release \\
12
+ software-properties-common apt-transport-https \\
13
+ sudo vim nano tmux \\
14
+ jq ripgrep fd-find bat \\
15
+ && rm -rf /var/lib/apt/lists/*
16
+
17
+ # ========== Python Environment ==========
18
+ RUN apt-get update && apt-get install -y \\
19
+ python3.12 python3.12-dev python3.12-venv \\
20
+ python3-pip pipx \\
21
+ && rm -rf /var/lib/apt/lists/*
22
+
23
+ # Install pyenv for Python version management
24
+ RUN curl https://pyenv.run | bash
25
+ ENV PATH="/root/.pyenv/bin:${PATH}"
26
+ RUN echo 'eval "$(pyenv init -)"' >> ~/.bashrc
27
+
28
+ # Install Poetry
29
+ RUN pipx install poetry && pipx ensurepath
30
+
31
+ # Install Python tools
32
+ RUN pip3 install --no-cache-dir --break-system-packages \\
33
+ uv black mypy pytest ruff
34
+
35
+ # ========== Node.js Environment ==========
36
+ # Install nvm
37
+ RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash
38
+ ENV NVM_DIR="/root/.nvm"
39
+ RUN . "$NVM_DIR/nvm.sh" && \\
40
+ nvm install 22 && \\
41
+ nvm install 20 && \\
42
+ nvm install 18 && \\
43
+ nvm alias default 22 && \\
44
+ nvm use 22
45
+
46
+ # Install npm global tools
47
+ RUN . "$NVM_DIR/nvm.sh" && \\
48
+ npm install -g yarn pnpm eslint prettier
49
+
50
+ # ========== Java Environment ==========
51
+ RUN apt-get update && apt-get install -y \\
52
+ openjdk-21-jdk maven gradle \\
53
+ && rm -rf /var/lib/apt/lists/*
54
+
55
+ # ========== Go Environment ==========
56
+ RUN wget https://go.dev/dl/go1.24.3.linux-amd64.tar.gz && \\
57
+ tar -C /usr/local -xzf go1.24.3.linux-amd64.tar.gz && \\
58
+ rm go1.24.3.linux-amd64.tar.gz
59
+ ENV PATH="/usr/local/go/bin:${PATH}"
60
+ ENV GOPATH="/root/go"
61
+ ENV PATH="${GOPATH}/bin:${PATH}"
62
+
63
+ # ========== Rust Environment ==========
64
+ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
65
+ ENV PATH="/root/.cargo/bin:${PATH}"
66
+
67
+ # ========== Docker-in-Docker ==========
68
+ RUN curl -fsSL https://get.docker.com -o get-docker.sh && \\
69
+ sh get-docker.sh && \\
70
+ rm get-docker.sh
71
+
72
+ # ========== C/C++ Compilers ==========
73
+ RUN apt-get update && apt-get install -y \\
74
+ clang gcc g++ cmake ninja-build \\
75
+ && rm -rf /var/lib/apt/lists/*
76
+
77
+ # Install Conan
78
+ RUN pip3 install --no-cache-dir --break-system-packages conan
79
+
80
+ # ========== Other Utilities ==========
81
+ RUN apt-get update && apt-get install -y \\
82
+ gawk sed grep tar gzip bzip2 xz-utils \\
83
+ make automake autoconf \\
84
+ openssh-client \\
85
+ && rm -rf /var/lib/apt/lists/*
86
+
87
+ # Install yq
88
+ RUN wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq && \\
89
+ chmod +x /usr/bin/yq
90
+
91
+ # Create developer user
92
+ RUN useradd -m -u 1000 -G sudo developer && \\
93
+ echo "developer:developer" | chpasswd && \\
94
+ echo "developer ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
95
+
96
+ # Create workspace
97
+ RUN mkdir -p /workspace && chown developer:developer /workspace
98
+
99
+ # Copy environment check script
100
+ COPY environment_check.sh /opt/environment_check.sh
101
+ RUN chmod +x /opt/environment_check.sh
102
+
103
+ # Set default user
104
+ USER developer
105
+ WORKDIR /workspace
106
+
107
+ # Default command (keep container running)
108
+ CMD ["tail", "-f", "/dev/null"]
sandbox/images/environment_check.sh ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Environment validation script
3
+
4
+ echo "============================="
5
+ echo "Environment Check"
6
+ echo "============================="
7
+ echo ""
8
+
9
+ # Colors for output
10
+ GREEN='\\033[0;32m'
11
+ RED='\\033[0;31m'
12
+ NC='\\033[0m' # No Color
13
+
14
+ check_command() {
15
+ if command -v $1 &> /dev/null; then
16
+ echo -e "${GREEN}✅${NC} $1: $($@)"
17
+ else
18
+ echo -e "${RED}❌${NC} $1: not found"
19
+ fi
20
+ }
21
+
22
+ echo "-------- Python --------"
23
+ check_command python3 --version
24
+ check_command python --version
25
+ check_command pip3 --version
26
+ check_command pipx --version
27
+ check_command poetry --version
28
+ check_command uv --version
29
+ check_command black --version
30
+ check_command mypy --version
31
+ check_command pytest --version
32
+ check_command ruff --version
33
+ echo ""
34
+
35
+ echo "-------- NodeJS --------"
36
+ if [ -s "$NVM_DIR/nvm.sh" ]; then
37
+ . "$NVM_DIR/nvm.sh"
38
+ check_command node --version
39
+ nvm list
40
+ else
41
+ check_command node --version
42
+ fi
43
+ check_command npm --version
44
+ check_command yarn --version
45
+ check_command pnpm --version
46
+ check_command eslint --version
47
+ check_command prettier --version
48
+ echo ""
49
+
50
+ echo "-------- Java --------"
51
+ check_command java --version
52
+ check_command mvn --version
53
+ check_command gradle --version
54
+ echo ""
55
+
56
+ echo "-------- Go --------"
57
+ check_command go version
58
+ echo ""
59
+
60
+ echo "-------- Rust --------"
61
+ check_command rustc --version
62
+ check_command cargo --version
63
+ echo ""
64
+
65
+ echo "-------- C/C++ Compilers --------"
66
+ check_command clang --version
67
+ check_command gcc --version
68
+ check_command cmake --version
69
+ check_command ninja --version
70
+ check_command conan --version
71
+ echo ""
72
+
73
+ echo "-------- Docker --------"
74
+ check_command docker --version
75
+ check_command docker compose version
76
+ echo ""
77
+
78
+ echo "-------- Other Utilities --------"
79
+ check_command awk --version
80
+ check_command curl --version
81
+ check_command git --version
82
+ check_command grep --version
83
+ check_command gzip --version
84
+ check_command jq --version
85
+ check_command make --version
86
+ check_command rg --version
87
+ check_command sed --version
88
+ check_command tar --version
89
+ check_command tmux -V
90
+ check_command yq --version
91
+ echo ""
92
+
93
+ echo "============================="
94
+ echo "Environment check complete!"
95
+ echo "============================="
sandbox/images/javascript.Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Minimal, hardened Node.js execution image
2
+ FROM node:20-alpine
3
+
4
+ # Install security updates and minimal dependencies
5
+ RUN apk update && apk upgrade && \
6
+ apk add --no-cache \
7
+ bash \
8
+ curl \
9
+ && \
10
+ rm -rf /var/cache/apk/*
11
+
12
+ # Create non-root user
13
+ RUN addgroup -g 1001 -S nodejs && \
14
+ adduser -S nodejs -u 1001
15
+
16
+ # Set working directory
17
+ WORKDIR /sandbox
18
+
19
+ # Change ownership to nodejs user
20
+ RUN chown -R nodejs:nodejs /sandbox
21
+
22
+ # Switch to non-root user
23
+ USER nodejs
24
+
25
+ # Default command
26
+ CMD ["node"]
sandbox/images/python.Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Minimal, hardened Python execution image
2
+ FROM python:3.11-slim-alpine
3
+
4
+ # Install security updates and minimal dependencies
5
+ RUN apk update && apk upgrade && \
6
+ apk add --no-cache \
7
+ bash \
8
+ && \
9
+ rm -rf /var/cache/apk/*
10
+
11
+ # Create non-root user
12
+ RUN adduser -D -s /bin/bash sandbox
13
+
14
+ # Set working directory
15
+ WORKDIR /sandbox
16
+
17
+ # Switch to non-root user
18
+ USER sandbox
19
+
20
+ # Default command
21
+ CMD ["python3"]
sandbox/language_runners.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sandbox.models import Language, LanguageInfo
2
+
3
+
4
+ class LanguageRunner:
5
+ """Base configuration for language runners"""
6
+
7
+ @staticmethod
8
+ def get_runner_config(language: Language) -> dict:
9
+ """Get Docker configuration for a specific language"""
10
+ configs = {
11
+ Language.PYTHON: {
12
+ "image": "python:3.11-slim",
13
+ "command": ["python", "-c"],
14
+ "version": "3.11",
15
+ "extensions": [".py"],
16
+ },
17
+ Language.JAVASCRIPT: {
18
+ "image": "node:20-alpine",
19
+ "command": ["node", "-e"],
20
+ "version": "20",
21
+ "extensions": [".js"],
22
+ },
23
+ Language.BASH: {
24
+ "image": "bash:5.2-alpine",
25
+ "command": ["bash", "-c"],
26
+ "version": "5.2",
27
+ "extensions": [".sh"],
28
+ },
29
+ }
30
+ return configs.get(language, configs[Language.PYTHON])
31
+
32
+ @staticmethod
33
+ def get_all_languages() -> list[LanguageInfo]:
34
+ """Get information about all supported languages"""
35
+ return [
36
+ LanguageInfo(
37
+ name="Python",
38
+ version="3.11",
39
+ image="python:3.11-slim",
40
+ extensions=[".py"]
41
+ ),
42
+ LanguageInfo(
43
+ name="JavaScript",
44
+ version="20",
45
+ image="node:20-alpine",
46
+ extensions=[".js"]
47
+ ),
48
+ LanguageInfo(
49
+ name="Bash",
50
+ version="5.2",
51
+ image="bash:5.2-alpine",
52
+ extensions=[".sh"]
53
+ ),
54
+ ]
sandbox/models.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field, field_validator
2
+ from typing import Optional, Literal, Dict, Any
3
+ from enum import Enum
4
+ from datetime import datetime
5
+ import uuid
6
+
7
+
8
+ class Language(str, Enum):
9
+ """Supported programming languages"""
10
+ PYTHON = "python"
11
+ JAVASCRIPT = "javascript"
12
+ BASH = "bash"
13
+
14
+
15
+ class ExecutionRequest(BaseModel):
16
+ """Request model for code execution"""
17
+ code: str = Field(..., description="Code to execute", min_length=1, max_length=50000)
18
+ language: Language = Field(..., description="Programming language")
19
+ stdin: str = Field(default="", description="Standard input for the code")
20
+ timeout: int = Field(default=10, ge=1, le=30, description="Execution timeout in seconds")
21
+ memory_limit: int = Field(default=256, ge=64, le=512, description="Memory limit in MB")
22
+
23
+ @field_validator('code')
24
+ @classmethod
25
+ def validate_code(cls, v: str) -> str:
26
+ if not v.strip():
27
+ raise ValueError("Code cannot be empty or whitespace only")
28
+ return v
29
+
30
+
31
+ class ExecutionResponse(BaseModel):
32
+ """Response model for code execution"""
33
+ stdout: str = Field(default="", description="Standard output")
34
+ stderr: str = Field(default="", description="Standard error")
35
+ exit_code: int = Field(..., description="Exit code of the process")
36
+ execution_time: float = Field(..., description="Execution time in seconds")
37
+ error: Optional[str] = Field(default=None, description="Error message if execution failed")
38
+
39
+
40
+ class SandboxConfig(BaseModel):
41
+ """Configuration for sandbox execution"""
42
+ max_execution_time: int = Field(default=30, description="Maximum execution time in seconds")
43
+ max_memory_mb: int = Field(default=512, description="Maximum memory in MB")
44
+ enable_network: bool = Field(default=False, description="Enable network access in sandbox")
45
+ read_only_root: bool = Field(default=True, description="Make root filesystem read-only")
46
+ max_output_size: int = Field(default=1048576, description="Maximum output size in bytes (1MB)")
47
+
48
+
49
+ class LanguageInfo(BaseModel):
50
+ """Information about a supported language"""
51
+ name: str
52
+ version: str
53
+ image: str
54
+ extensions: list[str]
55
+
56
+
57
+ class SessionStatus(str, Enum):
58
+ """Session lifecycle status"""
59
+ CREATING = "creating"
60
+ READY = "ready"
61
+ BUSY = "busy"
62
+ STOPPING = "stopping"
63
+ ERROR = "error"
64
+
65
+
66
+ class CreateSessionRequest(BaseModel):
67
+ """Request to create a new persistent session"""
68
+ metadata: Dict[str, Any] = Field(default_factory=dict, description="User-defined metadata")
69
+ timeout_minutes: int = Field(default=30, ge=5, le=120, description="Session idle timeout in minutes")
70
+
71
+
72
+ class SessionResponse(BaseModel):
73
+ """Response with session information"""
74
+ session_id: str
75
+ container_id: str
76
+ volume_name: str
77
+ status: SessionStatus
78
+ created_at: datetime
79
+ last_activity: datetime
80
+ timeout_minutes: int
81
+ metadata: Dict[str, Any]
82
+ files_count: Optional[int] = None
83
+
84
+
85
+ class FileInfo(BaseModel):
86
+ """Information about a file in session"""
87
+ filename: str
88
+ path: str
89
+ size: int
90
+ modified_at: datetime
91
+ mime_type: str
92
+
93
+
94
+ class FileUploadResponse(BaseModel):
95
+ """Response after file upload"""
96
+ filename: str
97
+ path: str
98
+ size: int
99
+ message: str = "File uploaded successfully"
100
+
101
+
102
+ class ExecuteInSessionRequest(BaseModel):
103
+ """Request to execute code in an existing session"""
104
+ code: str = Field(..., min_length=1, max_length=50000)
105
+ language: Language
106
+ working_dir: str = Field(default="/workspace", description="Working directory for execution")
107
+ timeout: int = Field(default=30, ge=1, le=120)
108
+
109
+
110
+ class ExecuteFileRequest(BaseModel):
111
+ """Request to execute an uploaded file"""
112
+ filepath: str = Field(..., description="Path to file in session workspace")
113
+ language: Language
114
+ args: list[str] = Field(default_factory=list, description="Command-line arguments")
115
+ timeout: int = Field(default=30, ge=1, le=120)
sandbox/session_manager.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import docker
2
+ from docker.errors import DockerException, NotFound, APIError
3
+ import logging
4
+ import uuid
5
+ from datetime import datetime, timedelta
6
+ from typing import Dict, Optional, List
7
+ import threading
8
+ import time
9
+
10
+ from sandbox.models import (
11
+ SessionResponse, SessionStatus, CreateSessionRequest,
12
+ SandboxConfig
13
+ )
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class SessionManager:
19
+ """
20
+ Manages persistent VM-like container sessions.
21
+
22
+ Each session is a long-running Docker container with:
23
+ - Persistent volume for file storage
24
+ - Multi-language development environment
25
+ - Dedicated workspace directory
26
+ """
27
+
28
+ def __init__(self, config: Optional[SandboxConfig] = None):
29
+ self.config = config or SandboxConfig()
30
+ try:
31
+ self.client = docker.from_env()
32
+ self.client.ping()
33
+ logger.info("Docker client initialized for session management")
34
+ except DockerException as e:
35
+ logger.error(f"Failed to initialize Docker client: {e}")
36
+ raise RuntimeError("Docker is not available") from e
37
+
38
+ # In-memory session registry (use Redis for production)
39
+ self.sessions: Dict[str, SessionResponse] = {}
40
+ self._lock = threading.Lock()
41
+
42
+ # Start cleanup thread
43
+ self._cleanup_thread = threading.Thread(target=self._cleanup_loop, daemon=True)
44
+ self._cleanup_thread.start()
45
+ logger.info("Session cleanup thread started")
46
+
47
+ def create_session(self, request: CreateSessionRequest) -> SessionResponse:
48
+ """
49
+ Create a new persistent session.
50
+
51
+ Args:
52
+ request: Session creation request
53
+
54
+ Returns:
55
+ SessionResponse with session details
56
+ """
57
+ session_id = str(uuid.uuid4())
58
+ volume_name = f"sandbox-session-{session_id}"
59
+
60
+ try:
61
+ # Create dedicated volume
62
+ logger.info(f"Creating volume {volume_name}")
63
+ volume = self.client.volumes.create(
64
+ name=volume_name,
65
+ driver='local',
66
+ labels={'session_id': session_id}
67
+ )
68
+
69
+ # Create long-running container
70
+ logger.info(f"Creating session container for {session_id}")
71
+ container = self.client.containers.create(
72
+ image='sandbox-devenv:latest', # Our multi-language image
73
+ name=f"sandbox-session-{session_id}",
74
+ detach=True,
75
+
76
+ # Mount volume to workspace
77
+ volumes={
78
+ volume_name: {'bind': '/workspace', 'mode': 'rw'}
79
+ },
80
+
81
+ # Keep container running
82
+ command='tail -f /dev/null',
83
+
84
+ # Resource limits
85
+ mem_limit=f"{self.config.max_memory_mb}m",
86
+ memswap_limit=f"{self.config.max_memory_mb}m",
87
+ cpu_quota=100000, # 1 CPU core
88
+ cpu_period=100000,
89
+
90
+ # Network isolation (can be disabled for package installation)
91
+ network_mode="bridge" if self.config.enable_network else "none",
92
+
93
+ # Security
94
+ read_only=False, # Need write access for development
95
+ security_opt=["no-new-privileges"],
96
+
97
+ # Working directory
98
+ working_dir='/workspace',
99
+
100
+ # Labels for tracking
101
+ labels={
102
+ 'session_id': session_id,
103
+ 'managed_by': 'sandbox-api'
104
+ }
105
+ )
106
+
107
+ # Start the container
108
+ container.start()
109
+ logger.info(f"Started container {container.id[:12]} for session {session_id}")
110
+
111
+ # Create session object
112
+ now = datetime.utcnow()
113
+ session = SessionResponse(
114
+ session_id=session_id,
115
+ container_id=container.id,
116
+ volume_name=volume_name,
117
+ status=SessionStatus.READY,
118
+ created_at=now,
119
+ last_activity=now,
120
+ timeout_minutes=request.timeout_minutes,
121
+ metadata=request.metadata,
122
+ files_count=0
123
+ )
124
+
125
+ # Store in registry
126
+ with self._lock:
127
+ self.sessions[session_id] = session
128
+
129
+ logger.info(f"Session {session_id} created successfully")
130
+ return session
131
+
132
+ except Exception as e:
133
+ logger.error(f"Failed to create session: {e}", exc_info=True)
134
+ # Cleanup on failure
135
+ try:
136
+ if volume_name:
137
+ vol = self.client.volumes.get(volume_name)
138
+ vol.remove(force=True)
139
+ except:
140
+ pass
141
+ raise RuntimeError(f"Failed to create session: {str(e)}")
142
+
143
+ def get_session(self, session_id: str) -> Optional[SessionResponse]:
144
+ """Get session by ID"""
145
+ with self._lock:
146
+ session = self.sessions.get(session_id)
147
+ if session:
148
+ # Update status from container
149
+ try:
150
+ container = self.client.containers.get(session.container_id)
151
+ if container.status == 'running':
152
+ session.status = SessionStatus.READY
153
+ else:
154
+ session.status = SessionStatus.ERROR
155
+ except:
156
+ session.status = SessionStatus.ERROR
157
+ return session
158
+
159
+ def list_sessions(self) -> List[SessionResponse]:
160
+ """List all active sessions"""
161
+ with self._lock:
162
+ return list(self.sessions.values())
163
+
164
+ def update_activity(self, session_id: str):
165
+ """Update last activity timestamp for a session"""
166
+ with self._lock:
167
+ if session_id in self.sessions:
168
+ self.sessions[session_id].last_activity = datetime.utcnow()
169
+
170
+ def destroy_session(self, session_id: str) -> bool:
171
+ """
172
+ Destroy a session and cleanup resources.
173
+
174
+ Args:
175
+ session_id: Session to destroy
176
+
177
+ Returns:
178
+ True if destroyed, False if not found
179
+ """
180
+ with self._lock:
181
+ session = self.sessions.pop(session_id, None)
182
+
183
+ if not session:
184
+ logger.warning(f"Session {session_id} not found")
185
+ return False
186
+
187
+ try:
188
+ # Stop and remove container
189
+ try:
190
+ container = self.client.containers.get(session.container_id)
191
+ container.stop(timeout=5)
192
+ container.remove(force=True)
193
+ logger.info(f"Removed container {session.container_id[:12]}")
194
+ except NotFound:
195
+ logger.warning(f"Container {session.container_id[:12]} not found")
196
+ except Exception as e:
197
+ logger.error(f"Error removing container: {e}")
198
+
199
+ # Remove volume
200
+ try:
201
+ volume = self.client.volumes.get(session.volume_name)
202
+ volume.remove(force=True)
203
+ logger.info(f"Removed volume {session.volume_name}")
204
+ except NotFound:
205
+ logger.warning(f"Volume {session.volume_name} not found")
206
+ except Exception as e:
207
+ logger.error(f"Error removing volume: {e}")
208
+
209
+ logger.info(f"Session {session_id} destroyed successfully")
210
+ return True
211
+
212
+ except Exception as e:
213
+ logger.error(f"Error destroying session {session_id}: {e}", exc_info=True)
214
+ return False
215
+
216
+ def _cleanup_loop(self):
217
+ """Background thread to cleanup idle sessions"""
218
+ while True:
219
+ try:
220
+ time.sleep(60) # Check every minute
221
+ self._cleanup_idle_sessions()
222
+ except Exception as e:
223
+ logger.error(f"Error in cleanup loop: {e}", exc_info=True)
224
+
225
+ def _cleanup_idle_sessions(self):
226
+ """Cleanup sessions that have exceeded their timeout"""
227
+ now = datetime.utcnow()
228
+ sessions_to_destroy = []
229
+
230
+ with self._lock:
231
+ for session_id, session in self.sessions.items():
232
+ timeout = timedelta(minutes=session.timeout_minutes)
233
+ if (now - session.last_activity) > timeout:
234
+ sessions_to_destroy.append(session_id)
235
+ logger.info(f"Session {session_id} exceeded timeout, will cleanup")
236
+
237
+ # Destroy outside lock to avoid deadlock
238
+ for session_id in sessions_to_destroy:
239
+ self.destroy_session(session_id)
240
+
241
+ def shutdown(self):
242
+ """Cleanup all sessions on shutdown"""
243
+ logger.info("Shutting down session manager, cleaning up all sessions")
244
+ session_ids = list(self.sessions.keys())
245
+ for session_id in session_ids:
246
+ self.destroy_session(session_id)
tests/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Test package
tests/test_api.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from fastapi.testclient import TestClient
3
+ from app import app
4
+
5
+
6
+ @pytest.fixture
7
+ def client():
8
+ """FastAPI test client fixture"""
9
+ return TestClient(app)
10
+
11
+
12
+ def test_root_endpoint(client):
13
+ """Test root endpoint returns API info"""
14
+ response = client.get("/")
15
+ assert response.status_code == 200
16
+ data = response.json()
17
+ assert data["name"] == "Code Execution Sandbox API"
18
+ assert data["version"] == "1.0.0"
19
+ assert "supported_languages" in data
20
+
21
+
22
+ def test_health_endpoint(client):
23
+ """Test health check endpoint"""
24
+ response = client.get("/health")
25
+ # May fail if Docker is not available, but endpoint should exist
26
+ assert response.status_code in [200, 503]
27
+
28
+
29
+ def test_languages_endpoint(client):
30
+ """Test languages listing endpoint"""
31
+ response = client.get("/languages")
32
+ assert response.status_code == 200
33
+ data = response.json()
34
+ assert "languages" in data
35
+ languages = data["languages"]
36
+ assert len(languages) == 3
37
+
38
+ # Check language names
39
+ lang_names = [lang["name"] for lang in languages]
40
+ assert "Python" in lang_names
41
+ assert "JavaScript" in lang_names
42
+ assert "Bash" in lang_names
43
+
44
+
45
+ def test_execute_python_hello_world(client):
46
+ """Test executing simple Python code"""
47
+ response = client.post("/execute", json={
48
+ "code": "print('Hello, World!')",
49
+ "language": "python"
50
+ })
51
+
52
+ # If Docker is not available, this may fail with 503
53
+ if response.status_code == 200:
54
+ data = response.json()
55
+ assert "Hello, World!" in data["stdout"]
56
+ assert data["exit_code"] == 0
57
+ assert data["error"] is None
58
+
59
+
60
+ def test_execute_python_math(client):
61
+ """Test Python arithmetic"""
62
+ response = client.post("/execute", json={
63
+ "code": "result = 2 + 2\nprint(f'Result: {result}')",
64
+ "language": "python"
65
+ })
66
+
67
+ if response.status_code == 200:
68
+ data = response.json()
69
+ assert "Result: 4" in data["stdout"]
70
+ assert data["exit_code"] == 0
71
+
72
+
73
+ def test_execute_javascript_hello_world(client):
74
+ """Test executing simple JavaScript code"""
75
+ response = client.post("/execute", json={
76
+ "code": "console.log('Hello from Node.js!');",
77
+ "language": "javascript"
78
+ })
79
+
80
+ if response.status_code == 200:
81
+ data = response.json()
82
+ assert "Hello from Node.js!" in data["stdout"]
83
+ assert data["exit_code"] == 0
84
+
85
+
86
+ def test_execute_bash_command(client):
87
+ """Test executing Bash command"""
88
+ response = client.post("/execute", json={
89
+ "code": "echo 'Bash works!'",
90
+ "language": "bash"
91
+ })
92
+
93
+ if response.status_code == 200:
94
+ data = response.json()
95
+ assert "Bash works!" in data["stdout"]
96
+ assert data["exit_code"] == 0
97
+
98
+
99
+ def test_execute_with_syntax_error(client):
100
+ """Test executing code with syntax error"""
101
+ response = client.post("/execute", json={
102
+ "code": "print('missing closing quote)",
103
+ "language": "python"
104
+ })
105
+
106
+ if response.status_code == 200:
107
+ data = response.json()
108
+ assert data["exit_code"] != 0
109
+ # Should have error in stderr or error field
110
+
111
+
112
+ def test_execute_with_timeout(client):
113
+ """Test timeout enforcement"""
114
+ response = client.post("/execute", json={
115
+ "code": "import time\ntime.sleep(5)",
116
+ "language": "python",
117
+ "timeout": 2
118
+ })
119
+
120
+ if response.status_code == 200:
121
+ data = response.json()
122
+ # Should timeout
123
+ assert data["execution_time"] < 3
124
+ assert data["error"] is not None or data["exit_code"] == 124
125
+
126
+
127
+ def test_invalid_language(client):
128
+ """Test invalid language rejection"""
129
+ response = client.post("/execute", json={
130
+ "code": "print('test')",
131
+ "language": "rust" # Not supported
132
+ })
133
+
134
+ assert response.status_code == 422 # Validation error
135
+
136
+
137
+ def test_empty_code(client):
138
+ """Test empty code rejection"""
139
+ response = client.post("/execute", json={
140
+ "code": "",
141
+ "language": "python"
142
+ })
143
+
144
+ assert response.status_code == 422 # Validation error
145
+
146
+
147
+ def test_timeout_out_of_range(client):
148
+ """Test timeout validation"""
149
+ response = client.post("/execute", json={
150
+ "code": "print('test')",
151
+ "language": "python",
152
+ "timeout": 100 # Exceeds max of 30
153
+ })
154
+
155
+ assert response.status_code == 422 # Validation error
156
+
157
+
158
+ def test_memory_limit_validation(client):
159
+ """Test memory limit validation"""
160
+ response = client.post("/execute", json={
161
+ "code": "print('test')",
162
+ "language": "python",
163
+ "memory_limit": 1024 # Exceeds max of 512
164
+ })
165
+
166
+ assert response.status_code == 422 # Validation error
tests/test_executor.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from sandbox.executor import SandboxExecutor
3
+ from sandbox.models import ExecutionRequest, SandboxConfig, Language
4
+
5
+
6
+ # Skip all tests if Docker is not available
7
+ docker = pytest.importorskip("docker")
8
+
9
+
10
+ @pytest.fixture
11
+ def executor():
12
+ """Sandbox executor fixture"""
13
+ try:
14
+ return SandboxExecutor(SandboxConfig())
15
+ except RuntimeError:
16
+ pytest.skip("Docker is not available")
17
+
18
+
19
+ def test_executor_initialization(executor):
20
+ """Test executor can be initialized"""
21
+ assert executor is not None
22
+ assert executor.client is not None
23
+
24
+
25
+ def test_execute_python_simple(executor):
26
+ """Test simple Python execution"""
27
+ request = ExecutionRequest(
28
+ code="print('Test')",
29
+ language=Language.PYTHON
30
+ )
31
+
32
+ response = executor.execute(request)
33
+
34
+ assert "Test" in response.stdout
35
+ assert response.exit_code == 0
36
+ assert response.error is None
37
+ assert response.execution_time > 0
38
+
39
+
40
+ def test_execute_python_with_variables(executor):
41
+ """Test Python with variable assignment"""
42
+ request = ExecutionRequest(
43
+ code="x = 10\ny = 20\nprint(x + y)",
44
+ language=Language.PYTHON
45
+ )
46
+
47
+ response = executor.execute(request)
48
+
49
+ assert "30" in response.stdout
50
+ assert response.exit_code == 0
51
+
52
+
53
+ def test_execute_javascript_simple(executor):
54
+ """Test simple JavaScript execution"""
55
+ request = ExecutionRequest(
56
+ code="console.log('JavaScript works!');",
57
+ language=Language.JAVASCRIPT
58
+ )
59
+
60
+ response = executor.execute(request)
61
+
62
+ assert "JavaScript works!" in response.stdout
63
+ assert response.exit_code == 0
64
+
65
+
66
+ def test_execute_bash_simple(executor):
67
+ """Test simple Bash execution"""
68
+ request = ExecutionRequest(
69
+ code="echo 'Bash test'",
70
+ language=Language.BASH
71
+ )
72
+
73
+ response = executor.execute(request)
74
+
75
+ assert "Bash test" in response.stdout
76
+ assert response.exit_code == 0
77
+
78
+
79
+ def test_timeout_enforcement(executor):
80
+ """Test that timeout is enforced"""
81
+ request = ExecutionRequest(
82
+ code="import time\nwhile True:\n time.sleep(1)",
83
+ language=Language.PYTHON,
84
+ timeout=2
85
+ )
86
+
87
+ response = executor.execute(request)
88
+
89
+ # Should timeout
90
+ assert response.execution_time < 3
91
+ assert response.exit_code == 124 or response.error is not None
92
+
93
+
94
+ def test_syntax_error_handling(executor):
95
+ """Test handling of code with syntax errors"""
96
+ request = ExecutionRequest(
97
+ code="print('missing quote)",
98
+ language=Language.PYTHON
99
+ )
100
+
101
+ response = executor.execute(request)
102
+
103
+ assert response.exit_code != 0
104
+ # Error should be captured in stderr or stdout
105
+ assert len(response.stdout + response.stderr) > 0
106
+
107
+
108
+ def test_runtime_error_handling(executor):
109
+ """Test handling of runtime errors"""
110
+ request = ExecutionRequest(
111
+ code="x = 1 / 0", # Division by zero
112
+ language=Language.PYTHON
113
+ )
114
+
115
+ response = executor.execute(request)
116
+
117
+ assert response.exit_code != 0
118
+
119
+
120
+ def test_container_cleanup(executor):
121
+ """Test that containers are cleaned up after execution"""
122
+ import docker
123
+ client = docker.from_env()
124
+
125
+ # Get initial container count
126
+ initial_containers = len(client.containers.list(all=True))
127
+
128
+ # Execute code
129
+ request = ExecutionRequest(
130
+ code="print('cleanup test')",
131
+ language=Language.PYTHON
132
+ )
133
+ executor.execute(request)
134
+
135
+ # Check container count after execution
136
+ final_containers = len(client.containers.list(all=True))
137
+
138
+ # Should be the same (container was cleaned up)
139
+ assert final_containers == initial_containers
140
+
141
+
142
+ def test_memory_limit_config(executor):
143
+ """Test that memory limit is applied"""
144
+ request = ExecutionRequest(
145
+ code="print('memory test')",
146
+ language=Language.PYTHON,
147
+ memory_limit=128
148
+ )
149
+
150
+ response = executor.execute(request)
151
+
152
+ # Should execute successfully with lower memory
153
+ assert response.exit_code == 0
154
+
155
+
156
+ def test_output_truncation(executor):
157
+ """Test that large output is truncated"""
158
+ # Generate large output
159
+ code = "for i in range(100000):\n print('x' * 100)"
160
+
161
+ request = ExecutionRequest(
162
+ code=code,
163
+ language=Language.PYTHON
164
+ )
165
+
166
+ response = executor.execute(request)
167
+
168
+ # Output should be truncated
169
+ assert "truncated" in response.stdout or len(response.stdout) <= executor.config.max_output_size + 100
170
+
171
+
172
+ def test_multiple_executions(executor):
173
+ """Test multiple consecutive executions"""
174
+ for i in range(5):
175
+ request = ExecutionRequest(
176
+ code=f"print('Execution {i}')",
177
+ language=Language.PYTHON
178
+ )
179
+
180
+ response = executor.execute(request)
181
+
182
+ assert f"Execution {i}" in response.stdout
183
+ assert response.exit_code == 0