diff --git a/Dockerfile b/Dockerfile index 9a1c2d3..feaa01f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,5 +28,5 @@ EXPOSE 8080 # Set environment variables ENV PYTHONPATH=/app -# Start OpenCode web interface -CMD ["opencode", "web", "--host", "0.0.0.0", "--port", "8080", "--workdir", "/app/somedir"] \ No newline at end of file +# Start OpenCode server +CMD ["/bin/bash", "-c", "source /root/.bashrc && opencode serve --hostname 0.0.0.0 --port 8080 --mdns"] \ No newline at end of file diff --git a/config_opencode/opencode.jsonc b/config_opencode/opencode.jsonc index f9f1085..7b2095a 100644 --- a/config_opencode/opencode.jsonc +++ b/config_opencode/opencode.jsonc @@ -12,6 +12,11 @@ "@modelcontextprotocol/server-sequential-thinking" ], "enabled": true + }, + "lovdata": { + "type": "http", + "url": "${MCP_SERVER}", + "enabled": true } } } diff --git a/requirements.txt b/requirements.txt index 696850d..589b03c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ fastapi==0.104.1 uvicorn==0.24.0 -docker==7.0.0 +docker==7.1.0 pydantic==2.5.0 python-multipart==0.0.6 httpx==0.25.2 \ No newline at end of file diff --git a/session-manager/main.py b/session-manager/main.py index 9df7b98..b1a6419 100644 --- a/session-manager/main.py +++ b/session-manager/main.py @@ -21,7 +21,6 @@ from fastapi.responses import JSONResponse, StreamingResponse from pydantic import BaseModel import uvicorn import httpx -import asyncio # Configuration @@ -45,84 +44,15 @@ class SessionData(BaseModel): status: str = "creating" # creating, running, stopped, error -class SimpleDockerClient: - """Simple Docker client using direct HTTP requests to Unix socket""" - - def __init__(self, socket_path="/var/run/docker.sock"): - self.socket_path = socket_path - self.base_url = ( - "http://localhost" # Docker socket uses HTTP over Unix domain socket - ) - - async def _request(self, method, path, json_data=None): - """Make HTTP request to Docker socket""" - import httpx - - # Create Unix socket connector - connector = httpx.AsyncHTTPTransport(uds=self.socket_path) - - async with httpx.AsyncClient( - transport=connector, base_url=self.base_url - ) as client: - try: - response = await client.request(method, path, json=json_data) - response.raise_for_status() - return response.json() - except httpx.HTTPStatusError as e: - print(f"Docker API error: {e.response.status_code} - {e.response.text}") - raise - except Exception as e: - print(f"Docker request failed: {e}") - raise - - async def ping(self): - """Test Docker connectivity""" - result = await self._request("GET", "/_ping") - return result - - async def create_container(self, image, name, **kwargs): - """Create a container""" - data = {"Image": image, "Names": [name], **kwargs} - result = await self._request("POST", "/containers/create", json_data=data) - return result - - async def start_container(self, container_id): - """Start a container""" - await self._request("POST", f"/containers/{container_id}/start") - return True - - async def stop_container(self, container_id): - """Stop a container""" - await self._request("POST", f"/containers/{container_id}/stop") - return True - - async def remove_container(self, container_id): - """Remove a container""" - await self._request("DELETE", f"/containers/{container_id}") - return True - - async def inspect_container(self, container_id): - """Inspect a container""" - result = await self._request("GET", f"/containers/{container_id}/json") - return result - - class SessionManager: def __init__(self): - # Try Docker library first, fall back to httpx if it fails - self.docker_client = None - try: - # Set DOCKER_HOST to the mounted socket - os.environ["DOCKER_HOST"] = "unix:///var/run/docker.sock" - import docker + # Use Docker library 7.1.0 with Unix socket support + import docker - self.docker_client = docker.from_env() - # Test the connection - self.docker_client.ping() - print("Docker library client initialized successfully") - except Exception as e: - print(f"Docker library failed ({e}), falling back to httpx client") - self.docker_client = SimpleDockerClient() + self.docker_client = docker.from_env() + # Test the connection + self.docker_client.ping() + print("Docker library client initialized successfully") self.sessions: Dict[str, SessionData] = {} self._load_sessions() @@ -163,7 +93,7 @@ class SessionManager: def _get_available_port(self) -> int: """Find an available port for the container""" used_ports = {s.port for s in self.sessions.values() if s.port} - port = 8080 + port = 8081 # Start from 8081 to avoid conflicts while port in used_ports: port += 1 return port @@ -213,18 +143,27 @@ class SessionManager: async def _start_container(self, session: SessionData): """Start the OpenCode container for a session""" - if not self.docker_client: - session.status = "error" - self._save_sessions() - print("Docker client not available") - return - try: - # Mock container creation for development - session.container_id = f"mock-{session.session_id}" + # Create and start the OpenCode container + container = self.docker_client.containers.run( + "lovdata-opencode:latest", # Will be built from the Dockerfile + name=session.container_name, + volumes={session.host_dir: {"bind": "/app/somedir", "mode": "rw"}}, + ports={f"8080/tcp": session.port}, + detach=True, + environment={ + "MCP_SERVER": os.getenv("MCP_SERVER", ""), + "OPENAI_API_KEY": os.getenv("OPENAI_API_KEY", ""), + "ANTHROPIC_API_KEY": os.getenv("ANTHROPIC_API_KEY", ""), + "GOOGLE_API_KEY": os.getenv("GOOGLE_API_KEY", ""), + }, + network_mode="bridge", + ) + + session.container_id = container.id session.status = "running" self._save_sessions() - print(f"Container {session.container_name} ready on port {session.port}") + print(f"Container {session.container_name} started on port {session.port}") except Exception as e: session.status = "error" @@ -254,10 +193,12 @@ class SessionManager: expired_sessions.append(session_id) # Stop and remove container - if not self.docker_client: - continue try: - # Mock container cleanup for development + container = self.docker_client.containers.get( + session.container_name + ) + container.stop(timeout=10) + container.remove() print(f"Cleaned up container {session.container_name}") except Exception as e: print(f"Error cleaning up container {session.container_name}: {e}") @@ -375,14 +316,12 @@ async def proxy_to_session(request: Request, session_id: str, path: str): if not session or session.status != "running": raise HTTPException(status_code=404, detail="Session not found or not running") - session = await session_manager.get_session(session_id) - if not session or session.status != "running": - raise HTTPException(status_code=404, detail="Session not found or not running") + # Proxy the request to the container (use host.docker.internal for Docker Desktop, or host IP) + # For Linux, we need to use the host IP + host_ip = os.getenv("HOST_IP", "172.17.0.1") # Default Docker bridge IP + container_url = f"http://{host_ip}:{session.port}" - # Proxy the request to the container - container_url = f"http://localhost:{session.port}" - - # Prepare the request + # Prepare the request URL url = f"{container_url}/{path}" if request.url.query: url += f"?{request.url.query}" @@ -390,36 +329,49 @@ async def proxy_to_session(request: Request, session_id: str, path: str): # Get request body body = await request.body() + # Prepare headers (exclude host header to avoid conflicts) + headers = dict(request.headers) + headers.pop("host", None) + + # Make the proxy request async with httpx.AsyncClient(timeout=30.0) as client: try: response = await client.request( method=request.method, url=url, - headers={ - k: v - for k, v in request.headers.items() - if k.lower() not in ["host", "x-session-id"] - }, + headers=headers, content=body, - follow_redirects=False, + follow_redirects=False, # Let the client handle redirects ) - # Return the proxied response + # Return the response return Response( content=response.content, status_code=response.status_code, headers=dict(response.headers), ) + + except httpx.TimeoutException: + raise HTTPException( + status_code=504, detail="Request to session container timed out" + ) except httpx.RequestError as e: raise HTTPException( - status_code=502, detail=f"Container proxy error: {str(e)}" + status_code=502, + detail=f"Failed to connect to session container: {str(e)}", ) @app.get("/health") async def health_check(): """Health check endpoint""" - docker_ok = True # Docker connectivity assumed for development + docker_ok = False + try: + # Check Docker connectivity + session_manager.docker_client.ping() + docker_ok = True + except: + docker_ok = False return { "status": "healthy" if docker_ok else "unhealthy", diff --git a/session-manager/requirements.txt b/session-manager/requirements.txt new file mode 100644 index 0000000..1589966 --- /dev/null +++ b/session-manager/requirements.txt @@ -0,0 +1,6 @@ +fastapi==0.104.1 +uvicorn==0.24.0 +docker>=7.1.0 +pydantic==2.5.0 +python-multipart==0.0.6 +httpx==0.25.2 \ No newline at end of file