openhands-agent-server 1.8.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openhands/agent_server/__init__.py +0 -0
- openhands/agent_server/__main__.py +118 -0
- openhands/agent_server/api.py +331 -0
- openhands/agent_server/bash_router.py +105 -0
- openhands/agent_server/bash_service.py +379 -0
- openhands/agent_server/config.py +187 -0
- openhands/agent_server/conversation_router.py +321 -0
- openhands/agent_server/conversation_service.py +692 -0
- openhands/agent_server/dependencies.py +72 -0
- openhands/agent_server/desktop_router.py +47 -0
- openhands/agent_server/desktop_service.py +212 -0
- openhands/agent_server/docker/Dockerfile +244 -0
- openhands/agent_server/docker/build.py +825 -0
- openhands/agent_server/docker/wallpaper.svg +22 -0
- openhands/agent_server/env_parser.py +460 -0
- openhands/agent_server/event_router.py +204 -0
- openhands/agent_server/event_service.py +648 -0
- openhands/agent_server/file_router.py +121 -0
- openhands/agent_server/git_router.py +34 -0
- openhands/agent_server/logging_config.py +56 -0
- openhands/agent_server/middleware.py +32 -0
- openhands/agent_server/models.py +307 -0
- openhands/agent_server/openapi.py +21 -0
- openhands/agent_server/pub_sub.py +80 -0
- openhands/agent_server/py.typed +0 -0
- openhands/agent_server/server_details_router.py +43 -0
- openhands/agent_server/sockets.py +173 -0
- openhands/agent_server/tool_preload_service.py +76 -0
- openhands/agent_server/tool_router.py +22 -0
- openhands/agent_server/utils.py +63 -0
- openhands/agent_server/vscode_extensions/openhands-settings/extension.js +22 -0
- openhands/agent_server/vscode_extensions/openhands-settings/package.json +12 -0
- openhands/agent_server/vscode_router.py +70 -0
- openhands/agent_server/vscode_service.py +232 -0
- openhands_agent_server-1.8.2.dist-info/METADATA +15 -0
- openhands_agent_server-1.8.2.dist-info/RECORD +39 -0
- openhands_agent_server-1.8.2.dist-info/WHEEL +5 -0
- openhands_agent_server-1.8.2.dist-info/entry_points.txt +2 -0
- openhands_agent_server-1.8.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from uuid import UUID
|
|
2
|
+
|
|
3
|
+
from fastapi import Depends, HTTPException, Query, Request, status
|
|
4
|
+
from fastapi.security import APIKeyHeader
|
|
5
|
+
|
|
6
|
+
from openhands.agent_server.config import Config
|
|
7
|
+
from openhands.agent_server.conversation_service import ConversationService
|
|
8
|
+
from openhands.agent_server.event_service import EventService
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_SESSION_API_KEY_HEADER = APIKeyHeader(name="X-Session-API-Key", auto_error=False)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_session_api_key_dependency(config: Config):
|
|
15
|
+
"""Create a session API key dependency with the given config."""
|
|
16
|
+
|
|
17
|
+
def check_session_api_key(
|
|
18
|
+
session_api_key: str | None = Depends(_SESSION_API_KEY_HEADER),
|
|
19
|
+
):
|
|
20
|
+
"""Check the session API key and throw an exception if incorrect. Having this as
|
|
21
|
+
a dependency means it appears in OpenAPI Docs
|
|
22
|
+
"""
|
|
23
|
+
if config.session_api_keys and session_api_key not in config.session_api_keys:
|
|
24
|
+
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
|
|
25
|
+
|
|
26
|
+
return check_session_api_key
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def create_websocket_session_api_key_dependency(config: Config):
|
|
30
|
+
"""Create a WebSocket session API key dependency with the given config.
|
|
31
|
+
|
|
32
|
+
WebSocket connections cannot send custom headers directly from browsers,
|
|
33
|
+
so we use query parameters instead.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def check_websocket_session_api_key(
|
|
37
|
+
session_api_key: str | None = Query(None, alias="session_api_key"),
|
|
38
|
+
):
|
|
39
|
+
"""Check the session API key from query parameter for WebSocket connections."""
|
|
40
|
+
if config.session_api_keys and session_api_key not in config.session_api_keys:
|
|
41
|
+
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
|
|
42
|
+
|
|
43
|
+
return check_websocket_session_api_key
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_conversation_service(request: Request):
|
|
47
|
+
"""Get the conversation service from app state.
|
|
48
|
+
|
|
49
|
+
This dependency ensures that the conversation service is properly initialized
|
|
50
|
+
through the application lifespan context manager.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
service = getattr(request.app.state, "conversation_service", None)
|
|
54
|
+
if service is None:
|
|
55
|
+
raise HTTPException(
|
|
56
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
57
|
+
detail="Conversation service is not available",
|
|
58
|
+
)
|
|
59
|
+
return service
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def get_event_service(
|
|
63
|
+
conversation_id: UUID,
|
|
64
|
+
conversation_service: ConversationService = Depends(get_conversation_service),
|
|
65
|
+
) -> EventService:
|
|
66
|
+
event_service = await conversation_service.get_event_service(conversation_id)
|
|
67
|
+
if event_service is None:
|
|
68
|
+
raise HTTPException(
|
|
69
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
70
|
+
detail=f"Conversation not found: {conversation_id}",
|
|
71
|
+
)
|
|
72
|
+
return event_service
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Desktop router for agent server API endpoints."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from openhands.agent_server.desktop_service import get_desktop_service
|
|
7
|
+
from openhands.sdk.logger import get_logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
desktop_router = APIRouter(prefix="/desktop", tags=["Desktop"])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DesktopUrlResponse(BaseModel):
|
|
16
|
+
"""Response model for Desktop URL."""
|
|
17
|
+
|
|
18
|
+
url: str | None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@desktop_router.get("/url", response_model=DesktopUrlResponse)
|
|
22
|
+
async def get_desktop_url(
|
|
23
|
+
base_url: str = "http://localhost:8002",
|
|
24
|
+
) -> DesktopUrlResponse:
|
|
25
|
+
"""Get the noVNC URL for desktop access.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
base_url: Base URL for the noVNC server (default: http://localhost:8002)
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
noVNC URL if available, None otherwise
|
|
32
|
+
"""
|
|
33
|
+
desktop_service = get_desktop_service()
|
|
34
|
+
if desktop_service is None:
|
|
35
|
+
raise HTTPException(
|
|
36
|
+
status_code=503,
|
|
37
|
+
detail=(
|
|
38
|
+
"Desktop is disabled in configuration. Set enable_vnc=true to enable."
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
url = desktop_service.get_vnc_url(base_url)
|
|
44
|
+
return DesktopUrlResponse(url=url)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
logger.error(f"Error getting desktop URL: {e}")
|
|
47
|
+
raise HTTPException(status_code=500, detail="Failed to get desktop URL")
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Desktop service for launching VNC desktop via desktop_launch.sh script."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from openhands.agent_server.config import get_default_config
|
|
11
|
+
from openhands.sdk.logger import get_logger
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
logger = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DesktopService:
|
|
18
|
+
"""Simple desktop service that launches desktop_launch.sh script."""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self._proc: asyncio.subprocess.Process | None = None
|
|
22
|
+
self.novnc_port: int = int(os.getenv("NOVNC_PORT", "8002"))
|
|
23
|
+
|
|
24
|
+
async def start(self) -> bool:
|
|
25
|
+
"""Start the VNC desktop stack."""
|
|
26
|
+
if self.is_running():
|
|
27
|
+
logger.info("Desktop already running")
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
# --- Env defaults (match bash behavior) ---
|
|
31
|
+
env = os.environ.copy()
|
|
32
|
+
display = env.get("DISPLAY", ":1")
|
|
33
|
+
user = env.get("USER") or env.get("USERNAME") or "openhands"
|
|
34
|
+
home = Path(env.get("HOME") or f"/home/{user}")
|
|
35
|
+
vnc_geometry = env.get("VNC_GEOMETRY", "1280x800")
|
|
36
|
+
novnc_proxy = Path("/usr/share/novnc/utils/novnc_proxy")
|
|
37
|
+
novnc_web = Path(env.get("NOVNC_WEB", "/opt/novnc-web"))
|
|
38
|
+
|
|
39
|
+
# --- Dirs & ownership (idempotent) ---
|
|
40
|
+
try:
|
|
41
|
+
for p in (home / ".vnc", home / ".config", home / "Downloads"):
|
|
42
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
logger.error("Failed preparing directories/ownership: %s", e)
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
# --- xstartup for XFCE (create once) ---
|
|
48
|
+
xstartup = home / ".vnc" / "xstartup"
|
|
49
|
+
if not xstartup.exists():
|
|
50
|
+
try:
|
|
51
|
+
xstartup.write_text(
|
|
52
|
+
"#!/bin/sh\n"
|
|
53
|
+
"unset SESSION_MANAGER\n"
|
|
54
|
+
"unset DBUS_SESSION_BUS_ADDRESS\n"
|
|
55
|
+
"exec startxfce4\n"
|
|
56
|
+
)
|
|
57
|
+
xstartup.chmod(0o755)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.error("Failed writing xstartup: %s", e)
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
# --- Start TigerVNC if not running (bind to loopback; novnc proxies) ---
|
|
63
|
+
try:
|
|
64
|
+
# Roughly equivalent to: pgrep -f "Xvnc .*:1"
|
|
65
|
+
xvnc_running = (
|
|
66
|
+
subprocess.run(
|
|
67
|
+
["pgrep", "-f", f"Xvnc .*{display}"],
|
|
68
|
+
capture_output=True,
|
|
69
|
+
text=True,
|
|
70
|
+
timeout=3,
|
|
71
|
+
).returncode
|
|
72
|
+
== 0
|
|
73
|
+
)
|
|
74
|
+
except Exception:
|
|
75
|
+
xvnc_running = False
|
|
76
|
+
|
|
77
|
+
if not xvnc_running:
|
|
78
|
+
logger.info("Starting TigerVNC on %s (%s)...", display, vnc_geometry)
|
|
79
|
+
# vncserver <DISPLAY> -geometry <geom> -depth 24 -localhost yes
|
|
80
|
+
rc = subprocess.run(
|
|
81
|
+
[
|
|
82
|
+
"vncserver",
|
|
83
|
+
display,
|
|
84
|
+
"-geometry",
|
|
85
|
+
vnc_geometry,
|
|
86
|
+
"-depth",
|
|
87
|
+
"24",
|
|
88
|
+
"-localhost",
|
|
89
|
+
"yes",
|
|
90
|
+
"-SecurityTypes",
|
|
91
|
+
"None",
|
|
92
|
+
],
|
|
93
|
+
env=env,
|
|
94
|
+
).returncode
|
|
95
|
+
if rc != 0:
|
|
96
|
+
logger.error("vncserver failed with rc=%s", rc)
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
# --- Start noVNC proxy (as our foreground/managed process) ---
|
|
100
|
+
# Equivalent to: pgrep -f "[n]ovnc_proxy .*--listen .*<port>"
|
|
101
|
+
try:
|
|
102
|
+
novnc_running = (
|
|
103
|
+
subprocess.run(
|
|
104
|
+
["pgrep", "-f", rf"novnc_proxy .*--listen .*{self.novnc_port}"],
|
|
105
|
+
capture_output=True,
|
|
106
|
+
text=True,
|
|
107
|
+
timeout=3,
|
|
108
|
+
).returncode
|
|
109
|
+
== 0
|
|
110
|
+
)
|
|
111
|
+
except Exception:
|
|
112
|
+
novnc_running = False
|
|
113
|
+
|
|
114
|
+
if novnc_running:
|
|
115
|
+
logger.info("noVNC already running on port %d", self.novnc_port)
|
|
116
|
+
self._proc = None # we didn't start it; don't own its lifecycle
|
|
117
|
+
else:
|
|
118
|
+
if not novnc_proxy.exists():
|
|
119
|
+
logger.error("noVNC proxy not found at %s", novnc_proxy)
|
|
120
|
+
return False
|
|
121
|
+
logger.info(
|
|
122
|
+
"Starting noVNC proxy on 0.0.0.0:%d -> 127.0.0.1:5901 ...",
|
|
123
|
+
self.novnc_port,
|
|
124
|
+
)
|
|
125
|
+
try:
|
|
126
|
+
# Store this as the managed long-running process
|
|
127
|
+
self._proc = await asyncio.create_subprocess_exec(
|
|
128
|
+
str(novnc_proxy),
|
|
129
|
+
"--listen",
|
|
130
|
+
f"0.0.0.0:{self.novnc_port}",
|
|
131
|
+
"--vnc",
|
|
132
|
+
"127.0.0.1:5901",
|
|
133
|
+
"--web",
|
|
134
|
+
str(novnc_web),
|
|
135
|
+
stdout=asyncio.subprocess.PIPE,
|
|
136
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
137
|
+
env=env,
|
|
138
|
+
)
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.error("Failed to start noVNC proxy: %s", e)
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
logger.info(
|
|
144
|
+
"noVNC URL: http://localhost:%d/vnc.html?autoconnect=1&resize=remote",
|
|
145
|
+
self.novnc_port,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Small grace period so callers relying on your old sleep(2) don't break
|
|
149
|
+
await asyncio.sleep(2)
|
|
150
|
+
|
|
151
|
+
# Final sanity: either our managed noVNC is alive or Xvnc is alive
|
|
152
|
+
if (self._proc and self._proc.returncode is None) or self.is_running():
|
|
153
|
+
logger.info("Desktop started successfully")
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
logger.error("Desktop failed to start (noVNC/Xvnc not healthy)")
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
async def stop(self) -> None:
|
|
160
|
+
"""Stop the desktop process."""
|
|
161
|
+
if self._proc and self._proc.returncode is None:
|
|
162
|
+
try:
|
|
163
|
+
self._proc.terminate()
|
|
164
|
+
await asyncio.wait_for(self._proc.wait(), timeout=5)
|
|
165
|
+
logger.info("Desktop stopped")
|
|
166
|
+
except TimeoutError:
|
|
167
|
+
logger.warning("Desktop did not stop gracefully, killing process")
|
|
168
|
+
self._proc.kill()
|
|
169
|
+
await self._proc.wait()
|
|
170
|
+
except Exception as e:
|
|
171
|
+
logger.error("Error stopping desktop: %s", e)
|
|
172
|
+
finally:
|
|
173
|
+
self._proc = None
|
|
174
|
+
|
|
175
|
+
def is_running(self) -> bool:
|
|
176
|
+
"""Check if desktop is running."""
|
|
177
|
+
if self._proc and self._proc.returncode is None:
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
# Check if VNC server is running
|
|
181
|
+
try:
|
|
182
|
+
result = subprocess.run(
|
|
183
|
+
["pgrep", "-f", "Xvnc"], capture_output=True, text=True, timeout=3
|
|
184
|
+
)
|
|
185
|
+
return result.returncode == 0
|
|
186
|
+
except Exception:
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
def get_vnc_url(self, base: str = "http://localhost:8003") -> str | None:
|
|
190
|
+
"""Get the noVNC URL for desktop access."""
|
|
191
|
+
if not self.is_running():
|
|
192
|
+
return None
|
|
193
|
+
return f"{base}/vnc.html?autoconnect=1&resize=remote"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ------- module-level accessor -------
|
|
197
|
+
|
|
198
|
+
_desktop_service: DesktopService | None = None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def get_desktop_service() -> DesktopService | None:
|
|
202
|
+
"""Get the desktop service instance if VNC is enabled."""
|
|
203
|
+
global _desktop_service
|
|
204
|
+
config = get_default_config()
|
|
205
|
+
|
|
206
|
+
if not config.enable_vnc:
|
|
207
|
+
logger.info("VNC desktop is disabled in configuration")
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
if _desktop_service is None:
|
|
211
|
+
_desktop_service = DesktopService()
|
|
212
|
+
return _desktop_service
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1.7
|
|
2
|
+
|
|
3
|
+
ARG BASE_IMAGE=nikolaik/python-nodejs:python3.12-nodejs22
|
|
4
|
+
ARG USERNAME=openhands
|
|
5
|
+
ARG UID=10001
|
|
6
|
+
ARG GID=10001
|
|
7
|
+
ARG PORT=8000
|
|
8
|
+
|
|
9
|
+
####################################################################################
|
|
10
|
+
# Builder (source mode)
|
|
11
|
+
# We copy source + build a venv here for local dev and debugging.
|
|
12
|
+
####################################################################################
|
|
13
|
+
FROM python:3.12-bullseye AS builder
|
|
14
|
+
ARG USERNAME UID GID
|
|
15
|
+
ENV UV_PROJECT_ENVIRONMENT=/agent-server/.venv
|
|
16
|
+
ENV UV_PYTHON_INSTALL_DIR=/agent-server/uv-managed-python
|
|
17
|
+
|
|
18
|
+
COPY --from=ghcr.io/astral-sh/uv /uv /uvx /bin/
|
|
19
|
+
|
|
20
|
+
RUN groupadd -g ${GID} ${USERNAME} \
|
|
21
|
+
&& useradd -m -u ${UID} -g ${GID} -s /usr/sbin/nologin ${USERNAME}
|
|
22
|
+
USER ${USERNAME}
|
|
23
|
+
WORKDIR /agent-server
|
|
24
|
+
# Cache-friendly: lockfiles first
|
|
25
|
+
COPY --chown=${USERNAME}:${USERNAME} pyproject.toml uv.lock README.md LICENSE ./
|
|
26
|
+
COPY --chown=${USERNAME}:${USERNAME} openhands-sdk ./openhands-sdk
|
|
27
|
+
COPY --chown=${USERNAME}:${USERNAME} openhands-tools ./openhands-tools
|
|
28
|
+
COPY --chown=${USERNAME}:${USERNAME} openhands-workspace ./openhands-workspace
|
|
29
|
+
COPY --chown=${USERNAME}:${USERNAME} openhands-agent-server ./openhands-agent-server
|
|
30
|
+
RUN --mount=type=cache,target=/home/${USERNAME}/.cache,uid=${UID},gid=${GID} \
|
|
31
|
+
uv python install 3.12 && uv venv --python 3.12 .venv && uv sync --frozen --no-editable --managed-python
|
|
32
|
+
|
|
33
|
+
####################################################################################
|
|
34
|
+
# Binary Builder (binary mode)
|
|
35
|
+
# We run pyinstaller here to produce openhands-agent-server
|
|
36
|
+
####################################################################################
|
|
37
|
+
FROM builder AS binary-builder
|
|
38
|
+
ARG USERNAME UID GID
|
|
39
|
+
|
|
40
|
+
# We need --dev for pyinstaller
|
|
41
|
+
RUN --mount=type=cache,target=/home/${USERNAME}/.cache,uid=${UID},gid=${GID} \
|
|
42
|
+
uv sync --frozen --dev --no-editable
|
|
43
|
+
|
|
44
|
+
RUN --mount=type=cache,target=/home/${USERNAME}/.cache,uid=${UID},gid=${GID} \
|
|
45
|
+
uv run pyinstaller openhands-agent-server/openhands/agent_server/agent-server.spec
|
|
46
|
+
# Fail fast if the expected binary is missing
|
|
47
|
+
RUN test -x /agent-server/dist/openhands-agent-server
|
|
48
|
+
|
|
49
|
+
####################################################################################
|
|
50
|
+
# Base image (minimal)
|
|
51
|
+
# It includes only basic packages and the UV runtime.
|
|
52
|
+
# No Docker, no VNC, no Desktop, no VSCode Web.
|
|
53
|
+
# Suitable for running in headless/evaluation mode.
|
|
54
|
+
####################################################################################
|
|
55
|
+
FROM ${BASE_IMAGE} AS base-image-minimal
|
|
56
|
+
ARG USERNAME UID GID PORT
|
|
57
|
+
|
|
58
|
+
# Install base packages and create user
|
|
59
|
+
RUN set -eux; \
|
|
60
|
+
# Install base packages (works for both Debian-based images)
|
|
61
|
+
apt-get update; \
|
|
62
|
+
apt-get install -y --no-install-recommends \
|
|
63
|
+
ca-certificates curl wget sudo apt-utils git jq tmux build-essential \
|
|
64
|
+
coreutils util-linux procps findutils grep sed \
|
|
65
|
+
# Docker dependencies
|
|
66
|
+
apt-transport-https gnupg lsb-release; \
|
|
67
|
+
\
|
|
68
|
+
# Create user and group
|
|
69
|
+
(getent group ${GID} || groupadd -g ${GID} ${USERNAME}); \
|
|
70
|
+
(id -u ${USERNAME} >/dev/null 2>&1 || useradd -m -u ${UID} -g ${GID} -s /bin/bash ${USERNAME}); \
|
|
71
|
+
# Add user to sudo group
|
|
72
|
+
usermod -aG sudo ${USERNAME}; \
|
|
73
|
+
echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers; \
|
|
74
|
+
# Create workspace directory
|
|
75
|
+
mkdir -p /workspace/project; \
|
|
76
|
+
chown -R ${USERNAME}:${USERNAME} /workspace; \
|
|
77
|
+
rm -rf /var/lib/apt/lists/*
|
|
78
|
+
|
|
79
|
+
# NOTE: we should NOT include UV_PROJECT_ENVIRONMENT here,
|
|
80
|
+
# since the agent might use it to perform other work (e.g. tools that use Python)
|
|
81
|
+
COPY --from=ghcr.io/astral-sh/uv /uv /uvx /bin/
|
|
82
|
+
|
|
83
|
+
USER ${USERNAME}
|
|
84
|
+
WORKDIR /
|
|
85
|
+
ENV OH_ENABLE_VNC=false
|
|
86
|
+
ENV LOG_JSON=true
|
|
87
|
+
EXPOSE ${PORT}
|
|
88
|
+
|
|
89
|
+
####################################################################################
|
|
90
|
+
# Base image (full)
|
|
91
|
+
# It includes additional Docker, VNC, Desktop, and VSCode Web.
|
|
92
|
+
####################################################################################
|
|
93
|
+
FROM base-image-minimal AS base-image
|
|
94
|
+
ARG USERNAME
|
|
95
|
+
|
|
96
|
+
USER root
|
|
97
|
+
# --- VSCode Web ---
|
|
98
|
+
ENV EDITOR=code \
|
|
99
|
+
VISUAL=code \
|
|
100
|
+
GIT_EDITOR="code --wait" \
|
|
101
|
+
OPENVSCODE_SERVER_ROOT=/openhands/.openvscode-server
|
|
102
|
+
ARG RELEASE_TAG="openvscode-server-v1.98.2"
|
|
103
|
+
ARG RELEASE_ORG="gitpod-io"
|
|
104
|
+
RUN set -eux; \
|
|
105
|
+
# Create necessary directories
|
|
106
|
+
mkdir -p $(dirname ${OPENVSCODE_SERVER_ROOT}); \
|
|
107
|
+
\
|
|
108
|
+
# Determine architecture
|
|
109
|
+
arch=$(uname -m); \
|
|
110
|
+
if [ "${arch}" = "x86_64" ]; then \
|
|
111
|
+
arch="x64"; \
|
|
112
|
+
elif [ "${arch}" = "aarch64" ]; then \
|
|
113
|
+
arch="arm64"; \
|
|
114
|
+
elif [ "${arch}" = "armv7l" ]; then \
|
|
115
|
+
arch="armhf"; \
|
|
116
|
+
fi; \
|
|
117
|
+
\
|
|
118
|
+
# Download and install VSCode Server
|
|
119
|
+
wget https://github.com/${RELEASE_ORG}/openvscode-server/releases/download/${RELEASE_TAG}/${RELEASE_TAG}-linux-${arch}.tar.gz; \
|
|
120
|
+
tar -xzf ${RELEASE_TAG}-linux-${arch}.tar.gz; \
|
|
121
|
+
if [ -d "${OPENVSCODE_SERVER_ROOT}" ]; then rm -rf "${OPENVSCODE_SERVER_ROOT}"; fi; \
|
|
122
|
+
mv ${RELEASE_TAG}-linux-${arch} ${OPENVSCODE_SERVER_ROOT}; \
|
|
123
|
+
cp ${OPENVSCODE_SERVER_ROOT}/bin/remote-cli/openvscode-server ${OPENVSCODE_SERVER_ROOT}/bin/remote-cli/code; \
|
|
124
|
+
rm -f ${RELEASE_TAG}-linux-${arch}.tar.gz; \
|
|
125
|
+
\
|
|
126
|
+
# Set proper ownership
|
|
127
|
+
chown -R ${USERNAME}:${USERNAME} ${OPENVSCODE_SERVER_ROOT}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# Include VSCode extensions alongside the server so targets inheriting base-image
|
|
131
|
+
# implicitly get the extensions; minimal images (without VSCode) won't.
|
|
132
|
+
COPY --chown=${USERNAME}:${USERNAME} --from=builder /agent-server/openhands-agent-server/openhands/agent_server/vscode_extensions ${OPENVSCODE_SERVER_ROOT}/extensions
|
|
133
|
+
|
|
134
|
+
# --- Docker ---
|
|
135
|
+
RUN set -eux; \
|
|
136
|
+
# Determine OS type and install Docker accordingly
|
|
137
|
+
if grep -q "ubuntu" /etc/os-release; then \
|
|
138
|
+
# Handle Ubuntu
|
|
139
|
+
install -m 0755 -d /etc/apt/keyrings; \
|
|
140
|
+
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc; \
|
|
141
|
+
chmod a+r /etc/apt/keyrings/docker.asc; \
|
|
142
|
+
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null; \
|
|
143
|
+
else \
|
|
144
|
+
# Handle Debian
|
|
145
|
+
install -m 0755 -d /etc/apt/keyrings; \
|
|
146
|
+
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc; \
|
|
147
|
+
chmod a+r /etc/apt/keyrings/docker.asc; \
|
|
148
|
+
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null; \
|
|
149
|
+
fi; \
|
|
150
|
+
# Install Docker Engine, containerd, and Docker Compose
|
|
151
|
+
apt-get update; \
|
|
152
|
+
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; \
|
|
153
|
+
apt-get clean; \
|
|
154
|
+
rm -rf /var/lib/apt/lists/*
|
|
155
|
+
|
|
156
|
+
# Configure Docker daemon with MTU 1450 to prevent packet fragmentation issues
|
|
157
|
+
RUN mkdir -p /etc/docker && \
|
|
158
|
+
echo '{"mtu": 1450}' > /etc/docker/daemon.json
|
|
159
|
+
|
|
160
|
+
# --- GitHub CLI ---
|
|
161
|
+
RUN set -eux; \
|
|
162
|
+
mkdir -p -m 755 /etc/apt/keyrings; \
|
|
163
|
+
wget -nv -O /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
|
164
|
+
https://cli.github.com/packages/githubcli-archive-keyring.gpg; \
|
|
165
|
+
chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg; \
|
|
166
|
+
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
|
|
167
|
+
> /etc/apt/sources.list.d/github-cli.list; \
|
|
168
|
+
apt-get update; \
|
|
169
|
+
apt-get install -y gh; \
|
|
170
|
+
apt-get clean; \
|
|
171
|
+
rm -rf /var/lib/apt/lists/*
|
|
172
|
+
|
|
173
|
+
# --- VNC + Desktop + noVNC ---
|
|
174
|
+
RUN set -eux; \
|
|
175
|
+
apt-get update; \
|
|
176
|
+
apt-get install -y --no-install-recommends \
|
|
177
|
+
# GUI bits (remove entirely if headless)
|
|
178
|
+
tigervnc-standalone-server xfce4 dbus-x11 novnc websockify \
|
|
179
|
+
# Browser
|
|
180
|
+
$(if grep -q "ubuntu" /etc/os-release; then echo "chromium-browser"; else echo "chromium"; fi); \
|
|
181
|
+
apt-get clean; rm -rf /var/lib/apt/lists/*
|
|
182
|
+
|
|
183
|
+
ENV NOVNC_WEB=/usr/share/novnc \
|
|
184
|
+
NOVNC_PORT=8002 \
|
|
185
|
+
DISPLAY=:1 \
|
|
186
|
+
VNC_GEOMETRY=1280x800 \
|
|
187
|
+
CHROME_BIN=/usr/bin/chromium \
|
|
188
|
+
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium \
|
|
189
|
+
CHROMIUM_FLAGS="--no-sandbox --disable-dev-shm-usage --disable-gpu"
|
|
190
|
+
|
|
191
|
+
RUN chown -R ${USERNAME}:${USERNAME} ${NOVNC_WEB}
|
|
192
|
+
# Override default XFCE wallpaper
|
|
193
|
+
COPY --chown=${USERNAME}:${USERNAME} openhands-agent-server/openhands/agent_server/docker/wallpaper.svg /usr/share/backgrounds/xfce/xfce-shapes.svg
|
|
194
|
+
|
|
195
|
+
USER ${USERNAME}
|
|
196
|
+
WORKDIR /
|
|
197
|
+
ENV OH_ENABLE_VNC=false
|
|
198
|
+
ENV LOG_JSON=true
|
|
199
|
+
EXPOSE ${PORT} ${NOVNC_PORT}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
####################################################################################
|
|
203
|
+
####################################################################################
|
|
204
|
+
# Build Targets
|
|
205
|
+
####################################################################################
|
|
206
|
+
####################################################################################
|
|
207
|
+
|
|
208
|
+
############################
|
|
209
|
+
# Target A: source
|
|
210
|
+
# Local dev and debugging mode: copy source + venv from builder
|
|
211
|
+
############################
|
|
212
|
+
FROM base-image AS source
|
|
213
|
+
ARG USERNAME
|
|
214
|
+
ENV UV_PYTHON_INSTALL_DIR=/agent-server/uv-managed-python
|
|
215
|
+
COPY --chown=${USERNAME}:${USERNAME} --from=builder /agent-server /agent-server
|
|
216
|
+
ENTRYPOINT ["/agent-server/.venv/bin/python", "-m", "openhands.agent_server"]
|
|
217
|
+
|
|
218
|
+
FROM base-image-minimal AS source-minimal
|
|
219
|
+
ARG USERNAME
|
|
220
|
+
ENV UV_PYTHON_INSTALL_DIR=/agent-server/uv-managed-python
|
|
221
|
+
COPY --chown=${USERNAME}:${USERNAME} --from=builder /agent-server /agent-server
|
|
222
|
+
ENTRYPOINT ["/agent-server/.venv/bin/python", "-m", "openhands.agent_server"]
|
|
223
|
+
|
|
224
|
+
############################
|
|
225
|
+
# Target B: binary-runtime
|
|
226
|
+
# Production mode: build the binary inside Docker and copy it in.
|
|
227
|
+
# NOTE: no support for external artifact contexts anymore.
|
|
228
|
+
############################
|
|
229
|
+
FROM base-image AS binary
|
|
230
|
+
ARG USERNAME
|
|
231
|
+
|
|
232
|
+
COPY --chown=${USERNAME}:${USERNAME} --from=binary-builder /agent-server/dist/openhands-agent-server /usr/local/bin/openhands-agent-server
|
|
233
|
+
RUN chmod +x /usr/local/bin/openhands-agent-server
|
|
234
|
+
# Fix library path to use system GCC libraries instead of bundled ones
|
|
235
|
+
ENV LD_LIBRARY_PATH=/usr/lib/aarch64-linux-gnu:/usr/lib:/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH
|
|
236
|
+
ENTRYPOINT ["/usr/local/bin/openhands-agent-server"]
|
|
237
|
+
|
|
238
|
+
FROM base-image-minimal AS binary-minimal
|
|
239
|
+
ARG USERNAME
|
|
240
|
+
COPY --chown=${USERNAME}:${USERNAME} --from=binary-builder /agent-server/dist/openhands-agent-server /usr/local/bin/openhands-agent-server
|
|
241
|
+
RUN chmod +x /usr/local/bin/openhands-agent-server
|
|
242
|
+
# Fix library path to use system GCC libraries instead of bundled ones
|
|
243
|
+
ENV LD_LIBRARY_PATH=/usr/lib/aarch64-linux-gnu:/usr/lib:/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH
|
|
244
|
+
ENTRYPOINT ["/usr/local/bin/openhands-agent-server"]
|