Skip to content

Commit 8ce246b

Browse files
Add REST API server with session management
- Add InstaVM-compatible REST API endpoints (/execute, /execute_async, /sessions) - Implement session management with automatic cleanup - Add multi-language support (Python, bash, JavaScript) - Create API schemas matching InstaVM interface - Update entrypoint to start both MCP and REST servers - Expose REST API on port 8223 alongside MCP on 8222 - Add comprehensive error handling and health checks
1 parent 0dc2b47 commit 8ce246b

22 files changed

+4528
-3
lines changed

.coverage

52 KB
Binary file not shown.

CLAUDE.md

Lines changed: 351 additions & 0 deletions
Large diffs are not rendered by default.

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ ENV FASTMCP_HOST="0.0.0.0"
7575
ENV FASTMCP_PORT="8222"
7676

7777

78-
# Expose the FastAPI port
79-
EXPOSE 8222
78+
# Expose both MCP and REST API ports
79+
EXPOSE 8222 8223
8080

8181
# Start the FastAPI application
8282
# CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8002", "--workers", "1", "--no-access-log"]

OpenCode.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# OpenCode.md for CodeRunner
2+
3+
## Build, Lint, and Test Commands
4+
- **Install dependencies:** `pip install -r examples/requirements.txt`
5+
- **Install main requirements:** `pip install -r requirements.txt`
6+
- **Run main server:** `python server.py`
7+
- **No test suite detected** (no pytest/unittest found; add tests in `tests/` or `test_*.py`)
8+
9+
## Code Style Guide
10+
- Follow [PEP8](https://peps.python.org/pep-0008/) for Python code.
11+
- Use meaningful, descriptive names for variables, functions, and classes.
12+
- Prefer absolute imports; group stdlib, third-party, then local imports.
13+
- Use type annotations for all public functions.
14+
- Use async/await for async flows where possible.
15+
- Log errors using `logging` (not print), unless interactive CLI.
16+
- Include basic docstrings for all public functions/classes.
17+
- Add comments for complex logic, but avoid redundant comments.
18+
- Write atomic, focused commits with clear messages.
19+
- Add tests for new features (see CONTRIBUTING.md).
20+
- Prefer pathlib for filesystem paths.
21+
- File uploads/storage: use `/app/uploads` as shared dir.
22+
- Respect containerization: never assume global system state.
23+
- Sensitive data should never be hardcoded or logged.
24+
- See CONTRIBUTING.md for workflow & quality rules.

api/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""CodeRunner REST API module"""

api/rest_server.py

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
"""REST API server for CodeRunner - InstaVM compatible interface"""
2+
3+
from fastapi import FastAPI, HTTPException, Depends
4+
from fastapi.middleware.cors import CORSMiddleware
5+
from fastapi.responses import JSONResponse
6+
import logging
7+
import time
8+
import uuid
9+
from typing import Dict, Any
10+
11+
from .schemas.execution import (
12+
CommandRequest, ExecutionResponse, AsyncExecutionResponse,
13+
SessionResponse, HealthResponse
14+
)
15+
from ..core.session_manager import session_manager, SessionError
16+
from ..core.language_processor import LanguageProcessor
17+
from ..core.exceptions import ExecutionError, KernelError
18+
19+
logger = logging.getLogger(__name__)
20+
21+
# Create FastAPI app
22+
app = FastAPI(
23+
title="CodeRunner REST API",
24+
description="Local code execution with InstaVM-compatible interface",
25+
version="1.0.0",
26+
docs_url="/docs",
27+
redoc_url="/redoc"
28+
)
29+
30+
# Add CORS middleware
31+
app.add_middleware(
32+
CORSMiddleware,
33+
allow_origins=["*"],
34+
allow_credentials=True,
35+
allow_methods=["*"],
36+
allow_headers=["*"],
37+
)
38+
39+
# Async task storage (simple in-memory for now)
40+
async_tasks: Dict[str, Dict[str, Any]] = {}
41+
42+
43+
@app.on_event("startup")
44+
async def startup_event():
45+
"""Initialize services on startup"""
46+
try:
47+
await session_manager.initialize()
48+
logger.info("REST API server started successfully")
49+
except Exception as e:
50+
logger.error(f"Failed to start REST API server: {e}")
51+
raise
52+
53+
54+
@app.on_event("shutdown")
55+
async def shutdown_event():
56+
"""Cleanup on shutdown"""
57+
try:
58+
await session_manager.shutdown()
59+
logger.info("REST API server shutdown complete")
60+
except Exception as e:
61+
logger.error(f"Error during shutdown: {e}")
62+
63+
64+
@app.get("/health", response_model=HealthResponse)
65+
async def health_check():
66+
"""Health check endpoint"""
67+
try:
68+
# Check session manager
69+
session_count = len(session_manager.sessions)
70+
71+
# Check kernel pool
72+
from server import kernel_pool
73+
kernel_status = {
74+
"total_kernels": len(kernel_pool.kernels),
75+
"available_kernels": len([k for k in kernel_pool.kernels.values() if k.is_available()]),
76+
"busy_kernels": len(kernel_pool.busy_kernels)
77+
}
78+
79+
return HealthResponse(
80+
status="healthy",
81+
version="1.0.0",
82+
services={
83+
"session_manager": {"active_sessions": session_count},
84+
"kernel_pool": kernel_status
85+
}
86+
)
87+
except Exception as e:
88+
logger.error(f"Health check failed: {e}")
89+
raise HTTPException(status_code=503, detail=f"Service unhealthy: {str(e)}")
90+
91+
92+
@app.post("/execute", response_model=ExecutionResponse)
93+
async def execute_command(request: CommandRequest):
94+
"""
95+
Execute command synchronously - InstaVM compatible interface
96+
97+
Args:
98+
request: Command execution request
99+
100+
Returns:
101+
Execution results with stdout, stderr, timing
102+
"""
103+
start_time = time.time()
104+
cpu_start_time = time.process_time()
105+
106+
try:
107+
# Validate language
108+
if not LanguageProcessor.validate_language(request.language):
109+
raise HTTPException(
110+
status_code=400,
111+
detail=f"Unsupported language: {request.language}"
112+
)
113+
114+
# Get or create session
115+
session_id = request.session_id
116+
if not session_id:
117+
session_id = await session_manager.create_session(request.language)
118+
elif not await session_manager.get_session(session_id):
119+
raise HTTPException(status_code=404, detail="Session not found")
120+
121+
# Execute command
122+
try:
123+
result = await session_manager.execute_in_session(session_id, request.command)
124+
except SessionError as e:
125+
raise HTTPException(status_code=400, detail=str(e))
126+
except Exception as e:
127+
raise HTTPException(status_code=500, detail=f"Execution failed: {str(e)}")
128+
129+
# Calculate timing
130+
execution_time = time.time() - start_time
131+
cpu_time = time.process_time() - cpu_start_time
132+
133+
# Extract output from result
134+
stdout = ""
135+
stderr = ""
136+
137+
if isinstance(result, dict):
138+
# Handle different result formats from kernel pool
139+
if "stdout" in result:
140+
stdout = result["stdout"]
141+
elif "output" in result:
142+
stdout = result["output"]
143+
else:
144+
stdout = str(result)
145+
146+
if "stderr" in result:
147+
stderr = result["stderr"]
148+
elif "error" in result and result.get("error"):
149+
stderr = str(result["error"])
150+
else:
151+
stdout = str(result)
152+
153+
return ExecutionResponse(
154+
stdout=stdout,
155+
stderr=stderr,
156+
execution_time=execution_time,
157+
cpu_time=cpu_time,
158+
session_id=session_id
159+
)
160+
161+
except HTTPException:
162+
raise
163+
except Exception as e:
164+
logger.error(f"Unexpected error in execute_command: {e}")
165+
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
166+
167+
168+
@app.post("/execute_async", response_model=AsyncExecutionResponse)
169+
async def execute_command_async(request: CommandRequest):
170+
"""
171+
Execute command asynchronously - InstaVM compatible interface
172+
173+
Args:
174+
request: Command execution request
175+
176+
Returns:
177+
Task ID for checking execution status
178+
"""
179+
try:
180+
# Validate language
181+
if not LanguageProcessor.validate_language(request.language):
182+
raise HTTPException(
183+
status_code=400,
184+
detail=f"Unsupported language: {request.language}"
185+
)
186+
187+
# Generate task ID
188+
task_id = str(uuid.uuid4())
189+
190+
# Store task info
191+
async_tasks[task_id] = {
192+
"id": task_id,
193+
"status": "queued",
194+
"command": request.command,
195+
"language": request.language,
196+
"session_id": request.session_id,
197+
"created_at": time.time(),
198+
"result": None,
199+
"error": None
200+
}
201+
202+
# TODO: Implement actual async execution with background tasks
203+
# For now, just return the task ID
204+
# In a full implementation, this would use Celery or similar
205+
206+
return AsyncExecutionResponse(task_id=task_id, status="queued")
207+
208+
except HTTPException:
209+
raise
210+
except Exception as e:
211+
logger.error(f"Error in execute_command_async: {e}")
212+
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
213+
214+
215+
@app.get("/tasks/{task_id}")
216+
async def get_task_status(task_id: str):
217+
"""Get async task status - InstaVM compatible"""
218+
task = async_tasks.get(task_id)
219+
if not task:
220+
raise HTTPException(status_code=404, detail="Task not found")
221+
222+
return task
223+
224+
225+
@app.post("/sessions", response_model=SessionResponse)
226+
async def create_session(language: str = "python"):
227+
"""
228+
Create new execution session
229+
230+
Args:
231+
language: Programming language for session
232+
233+
Returns:
234+
Session information
235+
"""
236+
try:
237+
session_id = await session_manager.create_session(language)
238+
239+
return SessionResponse(
240+
session_id=session_id,
241+
status="active",
242+
created_at=time.time()
243+
)
244+
245+
except SessionError as e:
246+
raise HTTPException(status_code=400, detail=str(e))
247+
except Exception as e:
248+
logger.error(f"Error creating session: {e}")
249+
raise HTTPException(status_code=500, detail=f"Failed to create session: {str(e)}")
250+
251+
252+
@app.get("/sessions/{session_id}")
253+
async def get_session(session_id: str):
254+
"""Get session information"""
255+
session = await session_manager.get_session(session_id)
256+
if not session:
257+
raise HTTPException(status_code=404, detail="Session not found")
258+
259+
return {
260+
"session_id": session.id,
261+
"language": session.language,
262+
"created_at": session.created_at.isoformat(),
263+
"last_used": session.last_used.isoformat(),
264+
"status": "active"
265+
}
266+
267+
268+
@app.delete("/sessions/{session_id}")
269+
async def close_session(session_id: str):
270+
"""Close execution session"""
271+
success = await session_manager.close_session(session_id)
272+
if not success:
273+
raise HTTPException(status_code=404, detail="Session not found")
274+
275+
return {"success": True, "message": f"Session {session_id} closed"}
276+
277+
278+
@app.get("/sessions")
279+
async def list_sessions():
280+
"""List all active sessions"""
281+
sessions = await session_manager.list_sessions()
282+
return {"sessions": sessions}
283+
284+
285+
@app.get("/languages")
286+
async def get_supported_languages():
287+
"""Get list of supported programming languages"""
288+
return {
289+
"languages": LanguageProcessor.get_supported_languages(),
290+
"default": "python"
291+
}
292+
293+
294+
# Error handlers
295+
@app.exception_handler(SessionError)
296+
async def session_error_handler(request, exc):
297+
return JSONResponse(
298+
status_code=400,
299+
content={"detail": str(exc), "type": "session_error"}
300+
)
301+
302+
303+
@app.exception_handler(ExecutionError)
304+
async def execution_error_handler(request, exc):
305+
return JSONResponse(
306+
status_code=500,
307+
content={"detail": str(exc), "type": "execution_error"}
308+
)
309+
310+
311+
@app.exception_handler(KernelError)
312+
async def kernel_error_handler(request, exc):
313+
return JSONResponse(
314+
status_code=503,
315+
content={"detail": str(exc), "type": "kernel_error"}
316+
)
317+
318+
319+
if __name__ == "__main__":
320+
import uvicorn
321+
uvicorn.run(app, host="0.0.0.0", port=8223)

api/schemas/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""API schema definitions"""

0 commit comments

Comments
 (0)