openhands-agent-server 1.44.0__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.0 → openhands_agent_server-1.45.0}/PKG-INFO +1 -1
  2. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/agent_profiles_router.py +7 -3
  3. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/api.py +3 -20
  4. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/canvas_extensions/installed.py +2 -1
  5. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/config.py +15 -4
  6. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/conversation_service.py +119 -33
  7. openhands_agent_server-1.45.0/openhands/agent_server/desktop_router.py +28 -0
  8. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/docker/Dockerfile +139 -46
  9. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/docker/build.py +127 -6
  10. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/event_service.py +19 -4
  11. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/persistence/store.py +9 -14
  12. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/pub_sub.py +7 -1
  13. openhands_agent_server-1.45.0/openhands/agent_server/session_protocol.py +156 -0
  14. openhands_agent_server-1.45.0/openhands/agent_server/session_socket.py +437 -0
  15. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/skills_router.py +0 -12
  16. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/sockets.py +3 -0
  17. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands_agent_server.egg-info/PKG-INFO +1 -1
  18. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands_agent_server.egg-info/SOURCES.txt +4 -4
  19. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/pyproject.toml +1 -1
  20. openhands_agent_server-1.44.0/openhands/agent_server/desktop_router.py +0 -47
  21. openhands_agent_server-1.44.0/openhands/agent_server/desktop_service.py +0 -224
  22. openhands_agent_server-1.44.0/openhands/agent_server/docker/wallpaper.svg +0 -22
  23. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/__init__.py +0 -0
  24. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/__main__.py +0 -0
  25. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/_secret_redaction.py +0 -0
  26. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/_secrets_exposure.py +0 -0
  27. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/auth_router.py +0 -0
  28. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/bash_router.py +0 -0
  29. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/bash_service.py +0 -0
  30. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/canvas_extensions/__init__.py +0 -0
  31. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/canvas_extensions/manifest.py +0 -0
  32. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/canvas_extensions_router.py +0 -0
  33. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/conversation_lease.py +0 -0
  34. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/conversation_router.py +0 -0
  35. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/credential_binding.py +0 -0
  36. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/dependencies.py +0 -0
  37. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/env_parser.py +0 -0
  38. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/event_router.py +0 -0
  39. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/file_router.py +0 -0
  40. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/git_router.py +0 -0
  41. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/hooks_router.py +0 -0
  42. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/hooks_service.py +0 -0
  43. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/init_router.py +0 -0
  44. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/llm_router.py +0 -0
  45. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/logging_config.py +0 -0
  46. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/mcp_oauth_store.py +0 -0
  47. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/mcp_router.py +0 -0
  48. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/middleware.py +0 -0
  49. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/models.py +0 -0
  50. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/openai/__init__.py +0 -0
  51. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/openai/models.py +0 -0
  52. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/openai/router.py +0 -0
  53. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/openai/service.py +0 -0
  54. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/openapi.py +0 -0
  55. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/persistence/__init__.py +0 -0
  56. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/persistence/models.py +0 -0
  57. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/plugins_router.py +0 -0
  58. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/plugins_service.py +0 -0
  59. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/profiles_router.py +0 -0
  60. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/provider_connections_router.py +0 -0
  61. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/py.typed +0 -0
  62. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/server_details_router.py +0 -0
  63. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/settings_router.py +0 -0
  64. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/skills_service.py +0 -0
  65. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/sub_agents_router.py +0 -0
  66. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry/__init__.py +0 -0
  67. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry/factory.py +0 -0
  68. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry/http_exporter.py +0 -0
  69. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry/models.py +0 -0
  70. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry/policy.py +0 -0
  71. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry/posthog_exporter.py +0 -0
  72. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry/sanitizer.py +0 -0
  73. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry/service.py +0 -0
  74. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry/sink.py +0 -0
  75. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry/subscriber.py +0 -0
  76. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/telemetry_types.py +0 -0
  77. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/tool_preload_service.py +0 -0
  78. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/tool_router.py +0 -0
  79. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/utils.py +0 -0
  80. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
  81. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
  82. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/vscode_router.py +0 -0
  83. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/vscode_service.py +0 -0
  84. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/workspace_router.py +0 -0
  85. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands/agent_server/workspaces_router.py +0 -0
  86. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
  87. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands_agent_server.egg-info/entry_points.txt +0 -0
  88. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands_agent_server.egg-info/requires.txt +0 -0
  89. {openhands_agent_server-1.44.0 → openhands_agent_server-1.45.0}/openhands_agent_server.egg-info/top_level.txt +0 -0
  90. {openhands_agent_server-1.44.0 → 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.0
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
@@ -514,13 +514,17 @@ async def materialize_agent_profile(
514
514
  mcp_config = settings.agent_settings.mcp_config
515
515
 
516
516
  # Discover skills off the event loop so the dry-run can report which skills
517
- # (catalog minus ``disabled_skills``) resolve. Only OpenHands profiles carry
518
- # user/public skills; ACP profiles do not. A discovery failure must not 500
517
+ # (catalog minus ``disabled_skills``) resolve. Mirrors the launch rule in
518
+ # ``conversation_service._resolve_agent_from_profile`` so the preview matches
519
+ # a real launch: an ACP profile is only given a catalog where the CLI cannot
520
+ # read the user's own configuration (#4019). A discovery failure must not 500
519
521
  # the preview: pass ``available_skills=None`` and surface the failure as its
520
522
  # own diagnostic below.
521
523
  discovery_error: str | None = None
522
524
  available_skills = None
523
- if profile.agent_kind == "openhands":
525
+ if profile.agent_kind == "openhands" or (
526
+ config.acp_skill_sourcing == "openhands_managed"
527
+ ):
524
528
  try:
525
529
  available_skills = await asyncio.to_thread(discover_profile_skills)
526
530
  except Exception as exc:
@@ -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(
@@ -27,6 +27,7 @@ CONFIG_PATH_ENV = "OPENHANDS_AGENT_SERVER_CONFIG_PATH"
27
27
  DEFAULT_CONFIG_PATH = Path("workspace/openhands_agent_server_config.json")
28
28
  # 20 minutes, matching the idle timeout used by OpenHands Cloud.
29
29
  DEFAULT_CONVERSATION_IDLE_TTL_SECONDS: Final[float] = 20 * 60.0
30
+ ACPSkillSourcing = Literal["native", "openhands_managed"]
30
31
  _logger = logging.getLogger(__name__)
31
32
 
32
33
 
@@ -318,10 +319,6 @@ class Config(BaseModel):
318
319
  "For example, '/{runtime_id}/vscode' when using path-based routing."
319
320
  ),
320
321
  )
321
- enable_vnc: bool = Field(
322
- default=False,
323
- description="Whether to enable VNC desktop functionality",
324
- )
325
322
  preload_tools: bool = Field(
326
323
  default=True,
327
324
  description="Whether to preload tools",
@@ -349,6 +346,20 @@ class Config(BaseModel):
349
346
  "The URL where this agent server instance is available externally"
350
347
  ),
351
348
  )
349
+ acp_skill_sourcing: ACPSkillSourcing = Field(
350
+ default="native",
351
+ description=(
352
+ "Who supplies an ACP agent's skills. 'native' (the default, for a "
353
+ "host-local agent-server): nobody but the ACP CLI — it reads the "
354
+ "user's own home configuration and the repository, so OpenHands "
355
+ "injects none of its managed skills. 'openhands_managed' (for "
356
+ "container runtimes, where that host configuration is absent): also "
357
+ "inject the user/org/public/marketplace skills the server "
358
+ "discovers. Project/repository skills are never injected either way "
359
+ "— the CLI reads AGENTS.md itself (#4019). Set explicitly per "
360
+ "deployment; the agent-server image sets 'openhands_managed'."
361
+ ),
362
+ )
352
363
  registered_marketplaces: list[MarketplaceRegistration] = Field(
353
364
  default_factory=list,
354
365
  description=(
@@ -15,7 +15,7 @@ from weakref import WeakValueDictionary
15
15
  import httpx
16
16
  from pydantic import BaseModel
17
17
 
18
- from openhands.agent_server.config import Config, WebhookSpec
18
+ from openhands.agent_server.config import ACPSkillSourcing, Config, WebhookSpec
19
19
  from openhands.agent_server.conversation_lease import (
20
20
  DEFAULT_LEASE_TTL_SECONDS,
21
21
  ConversationLeaseHeldError,
@@ -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)
@@ -290,11 +306,52 @@ def _same_workspace(a: LocalWorkspace, b: LocalWorkspace) -> bool:
290
306
  return Path(a.working_dir).resolve() == Path(b.working_dir).resolve()
291
307
 
292
308
 
309
+ def _apply_acp_skill_sourcing(
310
+ agent: "AgentBase", sourcing: ACPSkillSourcing
311
+ ) -> "AgentBase":
312
+ """Strip OpenHands-managed skills from an ACP agent under ``native`` sourcing.
313
+
314
+ A host-local ACP CLI reads the user's own skills from its home directory, so
315
+ a second, OpenHands-managed set injected into its prompt is at best noise —
316
+ and the catalog listing tells it to call ``invoke_skill``, a tool no ACP
317
+ agent has. Container runtimes set ``openhands_managed`` because that home
318
+ configuration is absent there. Project skills are excluded either way, by
319
+ ``ACPAgent`` itself (#4019).
320
+
321
+ A caller that sends ``agent`` / ``agent_settings`` puts its own skills on the
322
+ context, so the strip happens here rather than at profile resolution.
323
+ """
324
+ if sourcing != "native" or not isinstance(agent, ACPAgent):
325
+ return agent
326
+ context = agent.agent_context
327
+ if context is None:
328
+ return agent
329
+ if not (
330
+ context.skills
331
+ or context.load_user_skills
332
+ or context.load_public_skills
333
+ or context.registered_marketplaces
334
+ ):
335
+ return agent
336
+ return agent.model_copy(
337
+ update={
338
+ "agent_context": context.model_copy(
339
+ update={
340
+ "skills": [],
341
+ "load_user_skills": False,
342
+ "load_public_skills": False,
343
+ "registered_marketplaces": [],
344
+ }
345
+ )
346
+ }
347
+ )
348
+
349
+
293
350
  def _resolve_agent_from_profile(
294
351
  profile_id: "UUID",
295
352
  cipher: "Cipher | None",
296
353
  mcp_config: "dict[str, MCPServer]",
297
- load_memory: bool = False,
354
+ acp_skill_sourcing: ACPSkillSourcing = "native",
298
355
  ) -> "tuple[AgentBase, LaunchedAgentProfile]":
299
356
  """Load and resolve an agent profile by id, returning the built agent + provenance.
300
357
 
@@ -305,11 +362,9 @@ def _resolve_agent_from_profile(
305
362
  server's cipher. Passed explicitly so this free function never
306
363
  touches the settings-store singleton (which may not have been
307
364
  initialised with the correct cipher yet).
308
- load_memory: The user's global persistent-memory preference
309
- (``agent_settings.agent_context.load_memory``). An ``AgentProfile``
310
- has no ``agent_context`` field, so the preference cannot ride the
311
- profile — it is stamped onto the resolved agent below, else a
312
- profile-launched conversation would silently ignore the setting.
365
+ acp_skill_sourcing: This deployment's ACP skill policy
366
+ (``Config.acp_skill_sourcing``). Decides whether an ACP profile is
367
+ resolved with the server's managed skill catalog or with none.
313
368
 
314
369
  Raises:
315
370
  ProfileNotFound: No stored profile has ``profile_id``.
@@ -340,11 +395,15 @@ def _resolve_agent_from_profile(
340
395
  ) from exc
341
396
 
342
397
  # OpenHands profiles get the discovered catalog minus their ``disabled_skills``
343
- # deny-list; ACP profiles carry no user/public skills so discovery is skipped.
344
- # A genuine discovery failure fails the launch loudly rather than silently
345
- # producing a zero-skill agent.
398
+ # deny-list. An ACP profile gets it only where the CLI cannot reach the user's
399
+ # own configuration (``openhands_managed``); under ``native`` it sources its
400
+ # own skills and OpenHands injects none (#4019). A genuine discovery failure
401
+ # fails the launch loudly rather than silently producing a zero-skill agent.
346
402
  available_skills = None
347
- if profile.agent_kind == "openhands":
403
+ wants_skills = profile.agent_kind == "openhands" or (
404
+ acp_skill_sourcing == "openhands_managed"
405
+ )
406
+ if wants_skills:
348
407
  try:
349
408
  available_skills = discover_profile_skills()
350
409
  except Exception as exc:
@@ -386,15 +445,7 @@ def _resolve_agent_from_profile(
386
445
  agent = agent.model_copy(
387
446
  update={"tools": [*agent.tools, Tool(name=BROWSER_TOOL_NAME)]}
388
447
  )
389
- # Persistent memory is a global user preference, not a profile field, so it
390
- # is carried across the profile-resolution boundary the same way the global
391
- # ``mcp_config`` is. Left untouched when off, so the resolved agent stays
392
- # byte-identical for everyone who hasn't opted in.
393
- if load_memory:
394
- context = agent.agent_context or AgentContext()
395
- agent = agent.model_copy(
396
- update={"agent_context": context.model_copy(update={"load_memory": True})}
397
- )
448
+
398
449
  launched = LaunchedAgentProfile(
399
450
  agent_profile_id=profile.id,
400
451
  revision=profile.revision,
@@ -641,6 +692,7 @@ class ConversationService:
641
692
  conversation_worktree_root: Path = field(
642
693
  default=Path("/tmp/conversation-worktrees")
643
694
  )
695
+ acp_skill_sourcing: ACPSkillSourcing = "native"
644
696
  _event_services: dict[UUID, EventService] | None = field(default=None, init=False)
645
697
  _conversation_records: dict[UUID, _ConversationRecord] = field(
646
698
  default_factory=dict, init=False
@@ -1511,31 +1563,64 @@ class ConversationService:
1511
1563
  f"to a different workspace"
1512
1564
  )
1513
1565
 
1514
- # Profile resolution must happen before _prepare_request_workspace (which
1515
- # asserts request.agent is not None) and before model_dump so the resolved
1516
- # 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.
1517
1569
  launched_agent_profile: LaunchedAgentProfile | None = None
1518
- if request.agent_profile_id is not None:
1519
- # get_settings_store() is safe here: get_instance() initialises the
1520
- # singleton with the server cipher before any conversation can start.
1521
- from openhands.agent_server.persistence import (
1522
- PersistedSettings,
1523
- 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,
1524
1589
  )
1590
+ settings = PersistedSettings()
1525
1591
 
1526
- 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:
1527
1597
  mcp_config = settings.agent_settings.mcp_config
1528
- # ``ACPAgentSettings.agent_context`` is nullable, hence the guard.
1529
- stored_context = settings.agent_settings.agent_context
1530
1598
  resolved_agent, launched_agent_profile = await asyncio.to_thread(
1531
1599
  _resolve_agent_from_profile,
1532
1600
  request.agent_profile_id,
1533
1601
  self.cipher,
1534
1602
  mcp_config,
1535
- load_memory=bool(stored_context and stored_context.load_memory),
1603
+ acp_skill_sourcing=self.acp_skill_sourcing,
1536
1604
  )
1537
1605
  request = request.model_copy(update={"agent": resolved_agent})
1538
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
+
1616
+ request = request.model_copy(
1617
+ update={
1618
+ "agent": _apply_acp_skill_sourcing(
1619
+ request.agent, self.acp_skill_sourcing
1620
+ )
1621
+ }
1622
+ )
1623
+
1539
1624
  additions = request.agent_launch_additions
1540
1625
  suffix = (
1541
1626
  additions.system_message_suffix_append.strip()
@@ -2238,6 +2323,7 @@ class ConversationService:
2238
2323
  lease_ttl_seconds=config.lease_ttl_seconds,
2239
2324
  conversation_idle_ttl_seconds=config.conversation_idle_ttl_seconds,
2240
2325
  conversation_worktree_root=config.conversation_worktree_root,
2326
+ acp_skill_sourcing=config.acp_skill_sourcing,
2241
2327
  )
2242
2328
 
2243
2329
  async def _start_event_service(
@@ -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
+ )