openhands-agent-server 1.44.1__tar.gz → 1.45.0__tar.gz

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 (90) hide show
  1. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/PKG-INFO +1 -1
  2. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/api.py +3 -20
  3. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/canvas_extensions/installed.py +2 -1
  4. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/config.py +0 -4
  5. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/conversation_service.py +54 -28
  6. openhands_agent_server-1.45.0/openhands/agent_server/desktop_router.py +28 -0
  7. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/docker/Dockerfile +122 -57
  8. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/docker/build.py +93 -8
  9. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/event_service.py +19 -4
  10. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/persistence/store.py +9 -14
  11. openhands_agent_server-1.45.0/openhands/agent_server/session_protocol.py +156 -0
  12. openhands_agent_server-1.45.0/openhands/agent_server/session_socket.py +437 -0
  13. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/skills_router.py +0 -12
  14. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands_agent_server.egg-info/PKG-INFO +1 -1
  15. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands_agent_server.egg-info/SOURCES.txt +4 -4
  16. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/pyproject.toml +1 -1
  17. openhands_agent_server-1.44.1/openhands/agent_server/desktop_router.py +0 -47
  18. openhands_agent_server-1.44.1/openhands/agent_server/desktop_service.py +0 -224
  19. openhands_agent_server-1.44.1/openhands/agent_server/docker/wallpaper.svg +0 -22
  20. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/__init__.py +0 -0
  21. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/__main__.py +0 -0
  22. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/_secret_redaction.py +0 -0
  23. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/_secrets_exposure.py +0 -0
  24. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/agent_profiles_router.py +0 -0
  25. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/auth_router.py +0 -0
  26. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/bash_router.py +0 -0
  27. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/bash_service.py +0 -0
  28. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/canvas_extensions/__init__.py +0 -0
  29. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/canvas_extensions/manifest.py +0 -0
  30. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/canvas_extensions_router.py +0 -0
  31. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/conversation_lease.py +0 -0
  32. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/conversation_router.py +0 -0
  33. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/credential_binding.py +0 -0
  34. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/dependencies.py +0 -0
  35. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/env_parser.py +0 -0
  36. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/event_router.py +0 -0
  37. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/file_router.py +0 -0
  38. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/git_router.py +0 -0
  39. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/hooks_router.py +0 -0
  40. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/hooks_service.py +0 -0
  41. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/init_router.py +0 -0
  42. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/llm_router.py +0 -0
  43. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/logging_config.py +0 -0
  44. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/mcp_oauth_store.py +0 -0
  45. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/mcp_router.py +0 -0
  46. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/middleware.py +0 -0
  47. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/models.py +0 -0
  48. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/openai/__init__.py +0 -0
  49. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/openai/models.py +0 -0
  50. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/openai/router.py +0 -0
  51. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/openai/service.py +0 -0
  52. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/openapi.py +0 -0
  53. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/persistence/__init__.py +0 -0
  54. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/persistence/models.py +0 -0
  55. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/plugins_router.py +0 -0
  56. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/plugins_service.py +0 -0
  57. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/profiles_router.py +0 -0
  58. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/provider_connections_router.py +0 -0
  59. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/pub_sub.py +0 -0
  60. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/py.typed +0 -0
  61. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/server_details_router.py +0 -0
  62. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/settings_router.py +0 -0
  63. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/skills_service.py +0 -0
  64. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/sockets.py +0 -0
  65. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/sub_agents_router.py +0 -0
  66. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry/__init__.py +0 -0
  67. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry/factory.py +0 -0
  68. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry/http_exporter.py +0 -0
  69. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry/models.py +0 -0
  70. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry/policy.py +0 -0
  71. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry/posthog_exporter.py +0 -0
  72. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry/sanitizer.py +0 -0
  73. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry/service.py +0 -0
  74. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry/sink.py +0 -0
  75. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry/subscriber.py +0 -0
  76. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry_types.py +0 -0
  77. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/tool_preload_service.py +0 -0
  78. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/tool_router.py +0 -0
  79. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/utils.py +0 -0
  80. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
  81. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
  82. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/vscode_router.py +0 -0
  83. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/vscode_service.py +0 -0
  84. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/workspace_router.py +0 -0
  85. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands/agent_server/workspaces_router.py +0 -0
  86. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
  87. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands_agent_server.egg-info/entry_points.txt +0 -0
  88. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands_agent_server.egg-info/requires.txt +0 -0
  89. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/openhands_agent_server.egg-info/top_level.txt +0 -0
  90. {openhands_agent_server-1.44.1 → openhands_agent_server-1.45.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-agent-server
3
- Version: 1.44.1
3
+ Version: 1.45.0
4
4
  Summary: OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent
5
5
  Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
6
6
  Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
@@ -39,7 +39,6 @@ from openhands.agent_server.dependencies import (
39
39
  check_workspace_session,
40
40
  )
41
41
  from openhands.agent_server.desktop_router import desktop_router
42
- from openhands.agent_server.desktop_service import get_desktop_service
43
42
  from openhands.agent_server.event_router import event_router
44
43
  from openhands.agent_server.file_router import file_router
45
44
  from openhands.agent_server.git_router import git_router
@@ -66,6 +65,7 @@ from openhands.agent_server.server_details_router import (
66
65
  mark_initialization_complete,
67
66
  server_details_router,
68
67
  )
68
+ from openhands.agent_server.session_socket import session_router
69
69
  from openhands.agent_server.settings_router import settings_router
70
70
  from openhands.agent_server.skills_router import skills_router
71
71
  from openhands.agent_server.sockets import sockets_router
@@ -167,7 +167,6 @@ async def api_lifespan(api: FastAPI) -> AsyncIterator[None]:
167
167
  emit_server_started()
168
168
 
169
169
  vscode_service = get_vscode_service()
170
- desktop_service = get_desktop_service()
171
170
  tool_preload_service = get_tool_preload_service()
172
171
 
173
172
  # Define async functions for starting each service
@@ -183,18 +182,6 @@ async def api_lifespan(api: FastAPI) -> AsyncIterator[None]:
183
182
  else:
184
183
  logger.info("VSCode service is disabled")
185
184
 
186
- async def start_desktop_service():
187
- if desktop_service is not None:
188
- desktop_started = await desktop_service.start()
189
- if desktop_started:
190
- logger.info("Desktop service started successfully")
191
- else:
192
- logger.warning(
193
- "Desktop service failed to start, continuing without desktop"
194
- )
195
- else:
196
- logger.info("Desktop service is disabled")
197
-
198
185
  async def start_tool_preload_service():
199
186
  if tool_preload_service is not None:
200
187
  tool_preload_started = await tool_preload_service.start()
@@ -208,7 +195,6 @@ async def api_lifespan(api: FastAPI) -> AsyncIterator[None]:
208
195
  # Start all services concurrently
209
196
  results = await asyncio.gather(
210
197
  start_vscode_service(),
211
- start_desktop_service(),
212
198
  start_tool_preload_service(),
213
199
  return_exceptions=True,
214
200
  )
@@ -231,17 +217,12 @@ async def api_lifespan(api: FastAPI) -> AsyncIterator[None]:
231
217
  if vscode_service is not None:
232
218
  await vscode_service.stop()
233
219
 
234
- async def stop_desktop_service():
235
- if desktop_service is not None:
236
- await desktop_service.stop()
237
-
238
220
  async def stop_tool_preload_service():
239
221
  if tool_preload_service is not None:
240
222
  await tool_preload_service.stop()
241
223
 
242
224
  await asyncio.gather(
243
225
  stop_vscode_service(),
244
- stop_desktop_service(),
245
226
  stop_tool_preload_service(),
246
227
  return_exceptions=True,
247
228
  )
@@ -471,6 +452,8 @@ def _add_api_routes(app: FastAPI) -> None:
471
452
 
472
453
  app.include_router(sockets_router)
473
454
 
455
+ app.include_router(session_router)
456
+
474
457
 
475
458
  def _setup_static_files(app: FastAPI, config: Config) -> None:
476
459
  """Set up static file serving and root redirect if configured.
@@ -35,6 +35,7 @@ from openhands.sdk.extensions.installation import (
35
35
  InstallationMetadata,
36
36
  )
37
37
  from openhands.sdk.extensions.installation.manager import DEFAULT_CACHE_DIR
38
+ from openhands.sdk.utils.path import get_user_persistence_dir
38
39
 
39
40
 
40
41
  # Matches the InstalledPluginInfo naming convention.
@@ -43,7 +44,7 @@ InstalledCanvasExtensionInfo = InstallationInfo
43
44
 
44
45
  def get_installed_canvas_extensions_dir() -> Path:
45
46
  """Get the default directory for installed canvas extensions."""
46
- return Path.home() / ".openhands" / "canvas-extensions" / "installed"
47
+ return get_user_persistence_dir() / "canvas-extensions" / "installed"
47
48
 
48
49
 
49
50
  class CanvasExtensionInstallationInterface(
@@ -319,10 +319,6 @@ class Config(BaseModel):
319
319
  "For example, '/{runtime_id}/vscode' when using path-based routing."
320
320
  ),
321
321
  )
322
- enable_vnc: bool = Field(
323
- default=False,
324
- description="Whether to enable VNC desktop functionality",
325
- )
326
322
  preload_tools: bool = Field(
327
323
  default=True,
328
324
  description="Whether to preload tools",
@@ -130,6 +130,22 @@ def _append_system_message_suffix(agent: AgentBase, addition: str) -> AgentBase:
130
130
  return agent.model_copy(update={"agent_context": updated_context})
131
131
 
132
132
 
133
+ def _with_load_memory(agent: AgentBase) -> AgentBase:
134
+ """Stamp the global persistent-memory preference onto an agent.
135
+
136
+ ``load_memory`` is a user-level setting, not part of any agent, profile or
137
+ client payload, so it is applied here regardless of how the agent reached
138
+ the request.
139
+ """
140
+ # current_datetime stays suppressed on a synthesized context: a null
141
+ # agent_context means "no prompt context", and ACPAgent._render_suffix
142
+ # relies on that to keep a <CURRENT_DATETIME> block out of the prompt.
143
+ context = agent.agent_context or AgentContext(current_datetime=None)
144
+ return agent.model_copy(
145
+ update={"agent_context": context.model_copy(update={"load_memory": True})}
146
+ )
147
+
148
+
133
149
  def _has_git_remote(repo_root: Path, remote: str = "origin") -> bool:
134
150
  try:
135
151
  run_git_command(["git", "remote", "get-url", remote], repo_root)
@@ -335,7 +351,6 @@ def _resolve_agent_from_profile(
335
351
  profile_id: "UUID",
336
352
  cipher: "Cipher | None",
337
353
  mcp_config: "dict[str, MCPServer]",
338
- load_memory: bool = False,
339
354
  acp_skill_sourcing: ACPSkillSourcing = "native",
340
355
  ) -> "tuple[AgentBase, LaunchedAgentProfile]":
341
356
  """Load and resolve an agent profile by id, returning the built agent + provenance.
@@ -347,11 +362,6 @@ def _resolve_agent_from_profile(
347
362
  server's cipher. Passed explicitly so this free function never
348
363
  touches the settings-store singleton (which may not have been
349
364
  initialised with the correct cipher yet).
350
- load_memory: The user's global persistent-memory preference
351
- (``agent_settings.agent_context.load_memory``). An ``AgentProfile``
352
- has no ``agent_context`` field, so the preference cannot ride the
353
- profile — it is stamped onto the resolved agent below, else a
354
- profile-launched conversation would silently ignore the setting.
355
365
  acp_skill_sourcing: This deployment's ACP skill policy
356
366
  (``Config.acp_skill_sourcing``). Decides whether an ACP profile is
357
367
  resolved with the server's managed skill catalog or with none.
@@ -435,15 +445,7 @@ def _resolve_agent_from_profile(
435
445
  agent = agent.model_copy(
436
446
  update={"tools": [*agent.tools, Tool(name=BROWSER_TOOL_NAME)]}
437
447
  )
438
- # Persistent memory is a global user preference, not a profile field, so it
439
- # is carried across the profile-resolution boundary the same way the global
440
- # ``mcp_config`` is. Left untouched when off, so the resolved agent stays
441
- # byte-identical for everyone who hasn't opted in.
442
- if load_memory:
443
- context = agent.agent_context or AgentContext()
444
- agent = agent.model_copy(
445
- update={"agent_context": context.model_copy(update={"load_memory": True})}
446
- )
448
+
447
449
  launched = LaunchedAgentProfile(
448
450
  agent_profile_id=profile.id,
449
451
  revision=profile.revision,
@@ -1561,32 +1563,56 @@ class ConversationService:
1561
1563
  f"to a different workspace"
1562
1564
  )
1563
1565
 
1564
- # Profile resolution must happen before _prepare_request_workspace (which
1565
- # asserts request.agent is not None) and before model_dump so the resolved
1566
- # agent is captured in request_data.
1566
+ # Profile resolution and the load_memory stamp must happen before
1567
+ # _prepare_request_workspace (which asserts request.agent is not None)
1568
+ # and before model_dump so the resolved agent is captured in request_data.
1567
1569
  launched_agent_profile: LaunchedAgentProfile | None = None
1568
- if request.agent_profile_id is not None:
1569
- # get_settings_store() is safe here: get_instance() initialises the
1570
- # singleton with the server cipher before any conversation can start.
1571
- from openhands.agent_server.persistence import (
1572
- PersistedSettings,
1573
- get_settings_store,
1570
+
1571
+ from openhands.agent_server.persistence import (
1572
+ PersistedSettings,
1573
+ get_settings_store,
1574
+ )
1575
+
1576
+ # get_settings_store() is safe here: get_instance() initialises the
1577
+ # singleton with the server cipher before any conversation can start.
1578
+ # FileSettingsStore.load re-raises PermissionError/OSError by design;
1579
+ # now that every launch reads it, a bad file mode must not take down
1580
+ # request shapes that need nothing from settings.
1581
+ try:
1582
+ settings = await asyncio.to_thread(
1583
+ lambda: get_settings_store().load() or PersistedSettings()
1584
+ )
1585
+ except (PermissionError, OSError):
1586
+ logger.warning(
1587
+ "Cannot read settings; starting without the stored agent preferences",
1588
+ exc_info=True,
1574
1589
  )
1590
+ settings = PersistedSettings()
1575
1591
 
1576
- settings = get_settings_store().load() or PersistedSettings()
1592
+ # ``ACPAgentSettings.agent_context`` is nullable, hence the guard.
1593
+ stored_context = settings.agent_settings.agent_context
1594
+ load_memory = bool(stored_context and stored_context.load_memory)
1595
+
1596
+ if request.agent_profile_id is not None:
1577
1597
  mcp_config = settings.agent_settings.mcp_config
1578
- # ``ACPAgentSettings.agent_context`` is nullable, hence the guard.
1579
- stored_context = settings.agent_settings.agent_context
1580
1598
  resolved_agent, launched_agent_profile = await asyncio.to_thread(
1581
1599
  _resolve_agent_from_profile,
1582
1600
  request.agent_profile_id,
1583
1601
  self.cipher,
1584
1602
  mcp_config,
1585
- load_memory=bool(stored_context and stored_context.load_memory),
1586
1603
  acp_skill_sourcing=self.acp_skill_sourcing,
1587
1604
  )
1588
1605
  request = request.model_copy(update={"agent": resolved_agent})
1589
1606
 
1607
+ # Applied unconditionally: a serialized agent always carries
1608
+ # ``load_memory`` (model_dump emits defaults), so there is no way to
1609
+ # tell a deliberate ``false`` from an echoed one. Opting a single
1610
+ # conversation out needs a tri-state field; tracked separately.
1611
+ if load_memory and request.agent is not None:
1612
+ request = request.model_copy(
1613
+ update={"agent": _with_load_memory(request.agent)}
1614
+ )
1615
+
1590
1616
  request = request.model_copy(
1591
1617
  update={
1592
1618
  "agent": _apply_acp_skill_sourcing(
@@ -0,0 +1,28 @@
1
+ """Desktop router for agent server API endpoints."""
2
+
3
+ from fastapi import APIRouter, HTTPException
4
+ from pydantic import BaseModel
5
+
6
+
7
+ desktop_router = APIRouter(prefix="/desktop", tags=["Desktop"])
8
+
9
+
10
+ class DesktopUrlResponse(BaseModel):
11
+ """Response model for Desktop URL."""
12
+
13
+ url: str | None
14
+
15
+
16
+ @desktop_router.get("/url", response_model=DesktopUrlResponse, deprecated=True)
17
+ async def get_desktop_url(
18
+ base_url: str = "http://localhost:8002", # noqa: ARG001
19
+ ) -> DesktopUrlResponse:
20
+ """Deprecated since v1.44.1 and scheduled for removal in v1.49.0.
21
+
22
+ The VNC/desktop stack has been removed; this always returns 503, the
23
+ same response every deployment has always gotten since VNC defaults off.
24
+ """
25
+ raise HTTPException(
26
+ status_code=503,
27
+ detail="Desktop is disabled. VNC/desktop support has been removed.",
28
+ )
@@ -13,12 +13,18 @@ ARG PORT=8000
13
13
  # to bundle google-cloud-aiplatform so the resulting binary supports
14
14
  # `vertex_ai/*` partner models (MiniMax, Qwen, Kimi MaaS endpoints).
15
15
  ARG ENABLE_VERTEX=0
16
- # Comma-separated ACP provider keys (see ACP_PROVIDERS in
17
- # openhands-sdk/openhands/sdk/settings/acp_providers.py) to bake into the
16
+ # Comma-separated ACP provider keys (see ACP_INSTALL_CATALOG in
17
+ # openhands-sdk/openhands/sdk/settings/acp_install_catalog.py) to bake into the
18
18
  # acp-providers stage below. The default reproduces the full provider set
19
- # shipped today; an unknown key fails the build rather than silently
20
- # installing nothing.
19
+ # shipped today (DEFAULT_PREINSTALLED_ACP_PROVIDERS); an unknown key fails the
20
+ # build rather than silently installing nothing.
21
21
  ARG INSTALL_ACP_PROVIDERS=claude-code,codex,gemini-cli
22
+ # Comma-separated capability keys to include in the `base-image` stage below
23
+ # (`base-image-minimal` is unaffected). The default reproduces today's full
24
+ # contents; an unknown key fails the build. The GitHub CLI is expected to
25
+ # join this list later (see OpenHands/software-agent-sdk#4645) — the parsing
26
+ # below is written so that's a one-case addition, not a signature change.
27
+ ARG INSTALL_CAPABILITIES=vscode,browser,docker
22
28
 
23
29
  ####################################################################################
24
30
  # Builder (source mode)
@@ -96,6 +102,11 @@ RUN test -x /agent-server/dist/openhands-agent-server
96
102
  # Failures here are fatal: there is no per-BASE_IMAGE fallback for a broken build of
97
103
  # the shared payload itself (see the compatibility probe in base-image-minimal for
98
104
  # the per-target-image fallback).
105
+ #
106
+ # The Node pin below is a floor, not a preference: this stage installs every
107
+ # selected provider under one runtime, so it has to clear the highest
108
+ # `min_node_version` in ACP_INSTALL_CATALOG (asserted by
109
+ # tests/cross/test_agent_server_build_metadata.py).
99
110
  ####################################################################################
100
111
  FROM python:3.13-bookworm AS acp-providers
101
112
  ARG INSTALL_ACP_PROVIDERS
@@ -106,23 +117,17 @@ ARG INSTALL_ACP_PROVIDERS
106
117
  # parent, mirroring /agent-server's own top-level placement in the builder
107
118
  # stage above.
108
119
  ENV ACP_NODE_DIR=/acp-node
120
+ # The catalog module is dependency-free (stdlib only), so it can be COPYed
121
+ # into this bare `python` image and run as-is — no pip install, and no need
122
+ # to pull in the rest of the SDK source (which would break the zero-context
123
+ # fast path `build.py` uses for the `base-image-minimal` target). Adding an
124
+ # npm provider is a catalog edit only; this stage never needs to change.
125
+ COPY openhands-sdk/openhands/sdk/settings/acp_install_catalog.py /tmp/acp_install_catalog.py
109
126
  RUN set -eux; \
110
127
  ACP_WRAPPER_DIR=/opt/acp-wrappers; \
111
128
  mkdir -p "$ACP_NODE_DIR" "$ACP_WRAPPER_DIR"; \
112
- PACKAGES=""; \
113
- WRAPPER_BINS=""; \
114
- for provider in $(echo "$INSTALL_ACP_PROVIDERS" | tr ',' ' '); do \
115
- case "$provider" in \
116
- claude-code) PACKAGES="$PACKAGES @agentclientprotocol/claude-agent-acp@0.63.0"; \
117
- WRAPPER_BINS="$WRAPPER_BINS claude-agent-acp";; \
118
- codex) PACKAGES="$PACKAGES @agentclientprotocol/codex-acp@1.1.7"; \
119
- WRAPPER_BINS="$WRAPPER_BINS codex-acp";; \
120
- gemini-cli) PACKAGES="$PACKAGES @google/gemini-cli@0.46.0"; \
121
- WRAPPER_BINS="$WRAPPER_BINS gemini";; \
122
- *) echo "Unknown ACP provider '$provider' in INSTALL_ACP_PROVIDERS" \
123
- "(expected one of: claude-code, codex, gemini-cli)" >&2; exit 1;; \
124
- esac; \
125
- done; \
129
+ PLAN=$(python3 /tmp/acp_install_catalog.py "$INSTALL_ACP_PROVIDERS") || exit 1; \
130
+ eval "$PLAN"; \
126
131
  if [ -z "$PACKAGES" ]; then \
127
132
  echo "INSTALL_ACP_PROVIDERS is empty; no ACP providers will be installed"; \
128
133
  exit 0; \
@@ -133,29 +138,38 @@ RUN set -eux; \
133
138
  rm -rf /var/lib/apt/lists/*; \
134
139
  ARCH=$(uname -m); \
135
140
  case "$ARCH" in \
136
- x86_64|amd64) NARCH=x64; NODE_SHA256=69b09dba5c8dcb05c4e4273a4340db1005abeafe3927efda2bc5b249e80437ec;; \
137
- aarch64|arm64) NARCH=arm64; NODE_SHA256=08bfbf538bad0e8cbb0269f0173cca28d705874a67a22f60b57d99dc99e30050;; \
141
+ x86_64|amd64) NARCH=x64; NODE_SHA256=c0649af18e6a24f6fe5535a3e86b341dd49a8e71117c8b68bde973ef834f16f2;; \
142
+ aarch64|arm64) NARCH=arm64; NODE_SHA256=0b2d9f564b6594222a62c82e1df2efe119dd4a4aff29644f4dd325bf360b6bcc;; \
138
143
  *) echo "Unsupported architecture for ACP providers: '$ARCH'" >&2; exit 1;; \
139
144
  esac; \
140
- NODE_TARBALL="/tmp/node-v22.14.0-linux-${NARCH}.tar.xz"; \
145
+ NODE_TARBALL="/tmp/node-v22.19.0-linux-${NARCH}.tar.xz"; \
141
146
  curl -fsSL --retry 5 --retry-delay 2 --retry-connrefused \
142
- "https://nodejs.org/dist/v22.14.0/node-v22.14.0-linux-${NARCH}.tar.xz" -o "$NODE_TARBALL"; \
147
+ "https://nodejs.org/dist/v22.19.0/node-v22.19.0-linux-${NARCH}.tar.xz" -o "$NODE_TARBALL"; \
143
148
  echo "$NODE_SHA256 $NODE_TARBALL" | sha256sum -c -; \
144
149
  tar -xJ --strip-components=1 -C "$ACP_NODE_DIR" -f "$NODE_TARBALL"; \
145
150
  rm -f "$NODE_TARBALL"; \
146
151
  "$ACP_NODE_DIR/bin/node" --version; \
147
152
  PATH="$ACP_NODE_DIR/bin:$PATH" "$ACP_NODE_DIR/bin/npm" install -g $PACKAGES; \
148
153
  for bin in $WRAPPER_BINS; do \
154
+ # Fail the build rather than ship a wrapper that execs a missing target:
155
+ # a wrong binary_name is otherwise invisible until someone launches the
156
+ # provider, since the wrapper is only a shell script we chmod +x.
157
+ [ -x "$ACP_NODE_DIR/bin/$bin" ] || { \
158
+ echo "ACP wrapper target not installed: $ACP_NODE_DIR/bin/$bin" >&2; \
159
+ echo "(check binary_name in acp_install_catalog.py against the package's npm bin)" >&2; \
160
+ exit 1; \
161
+ }; \
149
162
  printf '#!/bin/sh\nPATH="%s/bin:$PATH" exec "%s/bin/%s" "$@"\n' \
150
163
  "$ACP_NODE_DIR" "$ACP_NODE_DIR" "$bin" \
151
164
  > "$ACP_WRAPPER_DIR/$bin"; \
152
165
  chmod +x "$ACP_WRAPPER_DIR/$bin"; \
153
- done
166
+ done; \
167
+ printf '%s\n' $WRAPPER_BINS > "$ACP_NODE_DIR/acp-wrappers.txt"
154
168
 
155
169
  ####################################################################################
156
170
  # Base image (minimal)
157
171
  # It includes only basic packages and the UV runtime.
158
- # No Docker, no VNC, no Desktop, no VSCode Web.
172
+ # No Docker, no browser, no VSCode Web.
159
173
  # Suitable for running in headless/evaluation mode.
160
174
  ####################################################################################
161
175
  FROM ${BASE_IMAGE} AS base-image-minimal
@@ -224,7 +238,7 @@ RUN set -eux; \
224
238
  mkdir -p /workspace/project; \
225
239
  chown -R "${USERNAME}:${USERNAME}" /workspace
226
240
 
227
- # Install ACP servers for ACPAgent support (Claude Code, Codex, Gemini CLI).
241
+ # Install the ACP servers selected by INSTALL_ACP_PROVIDERS (see the stage above).
228
242
  # The payload itself (Node.js 22 + the ACP npm packages, under $ACP_NODE_DIR) is
229
243
  # built once in the acp-providers stage above, independent of BASE_IMAGE, and
230
244
  # copied in here so the layer is byte-identical across every base image it lands
@@ -243,11 +257,16 @@ COPY --from=acp-providers /opt/acp-wrappers/. /usr/local/bin/
243
257
  # glibc-modern stage, so a failure here means "incompatible with this specific
244
258
  # BASE_IMAGE", not "the ACP install failed". When that happens we remove the
245
259
  # copied payload and wrappers so the rest of the build (and non-ACP agents)
246
- # still work, rather than leaving binaries in place that cannot execute.
260
+ # still work, rather than leaving binaries in place that cannot execute. The
261
+ # wrapper names come from the manifest the acp-providers stage wrote, so this
262
+ # tracks INSTALL_ACP_PROVIDERS instead of a list that goes stale per provider.
247
263
  RUN if ! "$ACP_NODE_DIR/bin/node" --version >/dev/null 2>&1; then \
248
264
  echo "Warning: ACP Node 22 runtime is not compatible with this base image (likely older glibc or musl libc); ACP agents will not be available" >&2; \
265
+ if [ -f "$ACP_NODE_DIR/acp-wrappers.txt" ]; then \
266
+ while read -r bin; do rm -f "/usr/local/bin/$bin"; done \
267
+ < "$ACP_NODE_DIR/acp-wrappers.txt"; \
268
+ fi; \
249
269
  rm -rf "$ACP_NODE_DIR"/*; \
250
- rm -f /usr/local/bin/claude-agent-acp /usr/local/bin/codex-acp /usr/local/bin/gemini; \
251
270
  fi
252
271
 
253
272
  # Configure Claude Code managed settings for headless operation:
@@ -264,7 +283,6 @@ WORKDIR /
264
283
  # Locale settings required for libtmux to work with PyInstaller builds
265
284
  ENV LC_ALL=C.UTF-8
266
285
  ENV LANG=C.UTF-8
267
- ENV OH_ENABLE_VNC=false
268
286
  ENV LOG_JSON=true
269
287
  # The ACP CLIs run without the user's host home here, so OpenHands supplies the
270
288
  # managed skill catalog they cannot read for themselves (#4019).
@@ -273,20 +291,49 @@ EXPOSE ${PORT}
273
291
 
274
292
  ####################################################################################
275
293
  # Base image (full)
276
- # It includes additional Docker, VNC, Desktop, and VSCode Web.
294
+ # It includes additional Docker, browser, and VSCode Web.
277
295
  ####################################################################################
278
296
  FROM base-image-minimal AS base-image
279
297
  ARG USERNAME
298
+ ARG INSTALL_CAPABILITIES
280
299
 
281
300
  USER root
301
+
302
+ # Fail fast on a typo'd capability name before spending any time on the
303
+ # installs below (each block only checks for ITS OWN keyword's presence, so
304
+ # an unrecognized one would otherwise be silently ignored rather than erroring).
305
+ RUN set -eux; \
306
+ for capability in $(echo "$INSTALL_CAPABILITIES" | tr ',' ' '); do \
307
+ case "$capability" in \
308
+ vscode|browser|docker) ;; \
309
+ *) echo "Unknown capability '$capability' in INSTALL_CAPABILITIES" \
310
+ "(expected one of: vscode, browser, docker)" >&2; exit 1;; \
311
+ esac; \
312
+ done
313
+
282
314
  # --- VSCode Web ---
315
+ # EDITOR/VISUAL/GIT_EDITOR/OPENVSCODE_SERVER_ROOT are declared unconditionally:
316
+ # nothing execs `code` unless VSCode Web itself is running, so a dangling
317
+ # path when `vscode` is excluded is harmless (same reasoning as CHROME_BIN
318
+ # below, applied to VSCode).
283
319
  ENV EDITOR=code \
284
320
  VISUAL=code \
285
321
  GIT_EDITOR="code --wait" \
286
322
  OPENVSCODE_SERVER_ROOT=/openhands/.openvscode-server
287
323
  ARG RELEASE_TAG="openvscode-server-v1.98.2"
288
324
  ARG RELEASE_ORG="gitpod-io"
325
+ # The --mount=from=builder on the extensions RUN below is unconditional at
326
+ # the Dockerfile level, so BuildKit still builds the full `builder` stage
327
+ # (the entire SDK venv) even when `vscode` is excluded from
328
+ # INSTALL_CAPABILITIES — the case check inside each RUN only decides what
329
+ # lands in THIS image, not what gets built as a dependency. Excluding vscode
330
+ # shrinks the result, not the build time.
289
331
  RUN set -eux; \
332
+ case ",$INSTALL_CAPABILITIES," in \
333
+ *,vscode,*) ;; \
334
+ *) echo "INSTALL_CAPABILITIES excludes vscode; skipping OpenVSCode Server"; exit 0;; \
335
+ esac; \
336
+ \
290
337
  # Create necessary directories
291
338
  mkdir -p $(dirname ${OPENVSCODE_SERVER_ROOT}); \
292
339
  \
@@ -311,13 +358,31 @@ RUN set -eux; \
311
358
  # Set proper ownership
312
359
  chown -R ${USERNAME}:${USERNAME} ${OPENVSCODE_SERVER_ROOT}
313
360
 
314
-
315
- # Include VSCode extensions alongside the server so targets inheriting base-image
316
- # implicitly get the extensions; minimal images (without VSCode) won't.
317
- COPY --chown=${USERNAME}:${USERNAME} --from=builder /agent-server/openhands-agent-server/openhands/agent_server/vscode_extensions ${OPENVSCODE_SERVER_ROOT}/extensions
361
+ # Kept as its own RUN, separate from the tarball download above: extensions
362
+ # are actively-edited local source (unlike the pinned release tarball), so
363
+ # splitting them keeps an extension-only change from invalidating the ~100MB
364
+ # download's cache layer.
365
+ RUN --mount=type=bind,from=builder,source=/agent-server/openhands-agent-server/openhands/agent_server/vscode_extensions,target=/tmp/vscode_extensions \
366
+ set -eux; \
367
+ case ",$INSTALL_CAPABILITIES," in \
368
+ *,vscode,*) ;; \
369
+ *) exit 0;; \
370
+ esac; \
371
+ \
372
+ # Trailing slashes matter: OpenVSCode Server's tarball already ships its
373
+ # own extensions/ dir, so a bare `cp -r src dst` would nest src under the
374
+ # existing dst instead of merging into it the way `COPY` does.
375
+ mkdir -p ${OPENVSCODE_SERVER_ROOT}/extensions; \
376
+ cp -r /tmp/vscode_extensions/. ${OPENVSCODE_SERVER_ROOT}/extensions/; \
377
+ chown -R ${USERNAME}:${USERNAME} ${OPENVSCODE_SERVER_ROOT}/extensions
318
378
 
319
379
  # --- Docker ---
320
380
  RUN set -eux; \
381
+ case ",$INSTALL_CAPABILITIES," in \
382
+ *,docker,*) ;; \
383
+ *) echo "INSTALL_CAPABILITIES excludes docker; skipping Docker Engine"; exit 0;; \
384
+ esac; \
385
+ \
321
386
  # Determine OS type and install Docker accordingly
322
387
  if grep -q "ubuntu" /etc/os-release; then \
323
388
  # Handle Ubuntu
@@ -336,10 +401,10 @@ RUN set -eux; \
336
401
  apt-get update; \
337
402
  apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; \
338
403
  apt-get clean; \
339
- rm -rf /var/lib/apt/lists/*
340
-
341
- # Configure Docker daemon with MTU 1450 to prevent packet fragmentation issues
342
- RUN mkdir -p /etc/docker && \
404
+ rm -rf /var/lib/apt/lists/*; \
405
+ \
406
+ # Configure Docker daemon with MTU 1450 to prevent packet fragmentation issues
407
+ mkdir -p /etc/docker; \
343
408
  echo '{"mtu": 1450}' > /etc/docker/daemon.json
344
409
 
345
410
  # --- GitHub CLI ---
@@ -355,36 +420,36 @@ RUN set -eux; \
355
420
  apt-get clean; \
356
421
  rm -rf /var/lib/apt/lists/*
357
422
 
358
- # --- VNC + Desktop + noVNC ---
359
- RUN set -eux; \
360
- apt-get update; \
361
- apt-get install -y --no-install-recommends \
362
- # GUI bits (remove entirely if headless)
363
- tigervnc-standalone-server xfce4 dbus-x11 novnc websockify \
364
- # Browser
365
- $(if grep -q "ubuntu" /etc/os-release; then echo "chromium-browser"; else echo "chromium"; fi); \
366
- apt-get clean; rm -rf /var/lib/apt/lists/*
367
-
368
- ENV NOVNC_WEB=/usr/share/novnc \
369
- NOVNC_PORT=8002 \
370
- DISPLAY=:1 \
371
- VNC_GEOMETRY=1280x800 \
372
- CHROME_BIN=/usr/bin/chromium \
423
+ # --- Browser ---
424
+ # Chromium for BrowserToolSet. Selected by the `browser` capability.
425
+ ENV CHROME_BIN=/usr/bin/chromium \
373
426
  PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium \
374
427
  CHROMIUM_FLAGS="--no-sandbox --disable-dev-shm-usage --disable-gpu"
375
428
 
376
- RUN chown -R ${USERNAME}:${USERNAME} ${NOVNC_WEB}
377
- # Override default XFCE wallpaper
378
- COPY --chown=${USERNAME}:${USERNAME} openhands-agent-server/openhands/agent_server/docker/wallpaper.svg /usr/share/backgrounds/xfce/xfce-shapes.svg
429
+ # CHROME_BIN/PUPPETEER_EXECUTABLE_PATH above are declared unconditionally
430
+ # regardless of which capabilities are selected. Nothing in this repo trusts
431
+ # them as an availability signal — browser_use's chromium detection
432
+ # (openhands-tools/openhands/tools/browser_use/impl.py) does its own filesystem
433
+ # probe of standard install paths — so when `browser` is excluded these simply
434
+ # point at a path that doesn't exist, rather than causing a false "available"
435
+ # signal downstream.
436
+ RUN set -eux; \
437
+ case ",$INSTALL_CAPABILITIES," in \
438
+ *,browser,*) \
439
+ apt-get update; \
440
+ apt-get install -y --no-install-recommends \
441
+ "$(if grep -q "ubuntu" /etc/os-release; then echo "chromium-browser"; else echo "chromium"; fi)"; \
442
+ apt-get clean; rm -rf /var/lib/apt/lists/*; \
443
+ ;; \
444
+ esac
379
445
 
380
446
  USER ${USERNAME}
381
447
  WORKDIR /
382
- ENV OH_ENABLE_VNC=false
383
448
  ENV LOG_JSON=true
384
449
  # The ACP CLIs run without the user's host home here, so OpenHands supplies the
385
450
  # managed skill catalog they cannot read for themselves (#4019).
386
451
  ENV OH_ACP_SKILL_SOURCING=openhands_managed
387
- EXPOSE ${PORT} ${NOVNC_PORT}
452
+ EXPOSE ${PORT}
388
453
 
389
454
 
390
455
  ####################################################################################