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.
Files changed (39) hide show
  1. openhands/agent_server/__init__.py +0 -0
  2. openhands/agent_server/__main__.py +118 -0
  3. openhands/agent_server/api.py +331 -0
  4. openhands/agent_server/bash_router.py +105 -0
  5. openhands/agent_server/bash_service.py +379 -0
  6. openhands/agent_server/config.py +187 -0
  7. openhands/agent_server/conversation_router.py +321 -0
  8. openhands/agent_server/conversation_service.py +692 -0
  9. openhands/agent_server/dependencies.py +72 -0
  10. openhands/agent_server/desktop_router.py +47 -0
  11. openhands/agent_server/desktop_service.py +212 -0
  12. openhands/agent_server/docker/Dockerfile +244 -0
  13. openhands/agent_server/docker/build.py +825 -0
  14. openhands/agent_server/docker/wallpaper.svg +22 -0
  15. openhands/agent_server/env_parser.py +460 -0
  16. openhands/agent_server/event_router.py +204 -0
  17. openhands/agent_server/event_service.py +648 -0
  18. openhands/agent_server/file_router.py +121 -0
  19. openhands/agent_server/git_router.py +34 -0
  20. openhands/agent_server/logging_config.py +56 -0
  21. openhands/agent_server/middleware.py +32 -0
  22. openhands/agent_server/models.py +307 -0
  23. openhands/agent_server/openapi.py +21 -0
  24. openhands/agent_server/pub_sub.py +80 -0
  25. openhands/agent_server/py.typed +0 -0
  26. openhands/agent_server/server_details_router.py +43 -0
  27. openhands/agent_server/sockets.py +173 -0
  28. openhands/agent_server/tool_preload_service.py +76 -0
  29. openhands/agent_server/tool_router.py +22 -0
  30. openhands/agent_server/utils.py +63 -0
  31. openhands/agent_server/vscode_extensions/openhands-settings/extension.js +22 -0
  32. openhands/agent_server/vscode_extensions/openhands-settings/package.json +12 -0
  33. openhands/agent_server/vscode_router.py +70 -0
  34. openhands/agent_server/vscode_service.py +232 -0
  35. openhands_agent_server-1.8.2.dist-info/METADATA +15 -0
  36. openhands_agent_server-1.8.2.dist-info/RECORD +39 -0
  37. openhands_agent_server-1.8.2.dist-info/WHEEL +5 -0
  38. openhands_agent_server-1.8.2.dist-info/entry_points.txt +2 -0
  39. 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"]