openhands-agent-server 1.43.1__tar.gz → 1.44.1__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 (87) hide show
  1. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/PKG-INFO +1 -1
  2. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/agent_profiles_router.py +7 -3
  3. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/api.py +2 -0
  4. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/canvas_extensions/__init__.py +4 -0
  5. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/canvas_extensions/installed.py +41 -0
  6. openhands_agent_server-1.44.1/openhands/agent_server/canvas_extensions_router.py +291 -0
  7. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/config.py +15 -0
  8. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/conversation_service.py +138 -18
  9. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/docker/Dockerfile +103 -53
  10. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/docker/build.py +36 -0
  11. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/event_service.py +7 -0
  12. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/persistence/models.py +48 -0
  13. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/persistence/store.py +2 -2
  14. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/pub_sub.py +7 -1
  15. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/sockets.py +3 -0
  16. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands_agent_server.egg-info/PKG-INFO +1 -1
  17. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands_agent_server.egg-info/SOURCES.txt +2 -0
  18. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/pyproject.toml +1 -1
  19. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/__init__.py +0 -0
  20. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/__main__.py +0 -0
  21. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/_secret_redaction.py +0 -0
  22. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/_secrets_exposure.py +0 -0
  23. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/auth_router.py +0 -0
  24. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/bash_router.py +0 -0
  25. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/bash_service.py +0 -0
  26. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/canvas_extensions/manifest.py +0 -0
  27. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/conversation_lease.py +0 -0
  28. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/conversation_router.py +0 -0
  29. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/credential_binding.py +0 -0
  30. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/dependencies.py +0 -0
  31. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/desktop_router.py +0 -0
  32. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/desktop_service.py +0 -0
  33. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/docker/wallpaper.svg +0 -0
  34. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/env_parser.py +0 -0
  35. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/event_router.py +0 -0
  36. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/file_router.py +0 -0
  37. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/git_router.py +0 -0
  38. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/hooks_router.py +0 -0
  39. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/hooks_service.py +0 -0
  40. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/init_router.py +0 -0
  41. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/llm_router.py +0 -0
  42. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/logging_config.py +0 -0
  43. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/mcp_oauth_store.py +0 -0
  44. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/mcp_router.py +0 -0
  45. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/middleware.py +0 -0
  46. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/models.py +0 -0
  47. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/openai/__init__.py +0 -0
  48. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/openai/models.py +0 -0
  49. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/openai/router.py +0 -0
  50. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/openai/service.py +0 -0
  51. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/openapi.py +0 -0
  52. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/persistence/__init__.py +0 -0
  53. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/plugins_router.py +0 -0
  54. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/plugins_service.py +0 -0
  55. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/profiles_router.py +0 -0
  56. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/provider_connections_router.py +0 -0
  57. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/py.typed +0 -0
  58. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/server_details_router.py +0 -0
  59. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/settings_router.py +0 -0
  60. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/skills_router.py +0 -0
  61. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/skills_service.py +0 -0
  62. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/sub_agents_router.py +0 -0
  63. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/telemetry/__init__.py +0 -0
  64. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/telemetry/factory.py +0 -0
  65. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/telemetry/http_exporter.py +0 -0
  66. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/telemetry/models.py +0 -0
  67. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/telemetry/policy.py +0 -0
  68. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/telemetry/posthog_exporter.py +0 -0
  69. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/telemetry/sanitizer.py +0 -0
  70. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/telemetry/service.py +0 -0
  71. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/telemetry/sink.py +0 -0
  72. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/telemetry/subscriber.py +0 -0
  73. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/telemetry_types.py +0 -0
  74. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/tool_preload_service.py +0 -0
  75. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/tool_router.py +0 -0
  76. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/utils.py +0 -0
  77. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
  78. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
  79. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/vscode_router.py +0 -0
  80. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/vscode_service.py +0 -0
  81. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/workspace_router.py +0 -0
  82. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands/agent_server/workspaces_router.py +0 -0
  83. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
  84. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands_agent_server.egg-info/entry_points.txt +0 -0
  85. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands_agent_server.egg-info/requires.txt +0 -0
  86. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/openhands_agent_server.egg-info/top_level.txt +0 -0
  87. {openhands_agent_server-1.43.1 → openhands_agent_server-1.44.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-agent-server
3
- Version: 1.43.1
3
+ Version: 1.44.1
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:
@@ -21,6 +21,7 @@ from openhands.agent_server.agent_profiles_router import agent_profiles_router
21
21
  from openhands.agent_server.auth_router import auth_router
22
22
  from openhands.agent_server.bash_router import bash_router
23
23
  from openhands.agent_server.bash_service import get_default_bash_event_service
24
+ from openhands.agent_server.canvas_extensions_router import canvas_extensions_router
24
25
  from openhands.agent_server.config import (
25
26
  Config,
26
27
  get_default_config,
@@ -441,6 +442,7 @@ def _add_api_routes(app: FastAPI) -> None:
441
442
  api_router.include_router(skills_router)
442
443
  api_router.include_router(sub_agents_router)
443
444
  api_router.include_router(plugins_router)
445
+ api_router.include_router(canvas_extensions_router)
444
446
  api_router.include_router(hooks_router)
445
447
  api_router.include_router(llm_router)
446
448
  api_router.include_router(provider_connections_router)
@@ -7,7 +7,9 @@ from openhands.agent_server.canvas_extensions.installed import (
7
7
  check_canvas_extension_update,
8
8
  disable_canvas_extension,
9
9
  enable_canvas_extension,
10
+ get_canvas_extension_bundle_path,
10
11
  get_installed_canvas_extension,
12
+ get_installed_canvas_extension_manifest,
11
13
  get_installed_canvas_extensions_dir,
12
14
  install_canvas_extension,
13
15
  list_installed_canvas_extensions,
@@ -37,7 +39,9 @@ __all__ = [
37
39
  "list_installed_canvas_extensions",
38
40
  "load_installed_canvas_extensions",
39
41
  "get_installed_canvas_extension",
42
+ "get_installed_canvas_extension_manifest",
40
43
  "get_installed_canvas_extensions_dir",
44
+ "get_canvas_extension_bundle_path",
41
45
  "CanvasExtensionUpdateCheck",
42
46
  "check_canvas_extension_update",
43
47
  "apply_canvas_extension_update",
@@ -206,6 +206,47 @@ def get_installed_canvas_extension(
206
206
  return _manager(_resolve_installed_dir(installed_dir)).get(name)
207
207
 
208
208
 
209
+ def get_installed_canvas_extension_manifest(
210
+ name: str, installed_dir: Path | None = None
211
+ ) -> CanvasExtensionManifest | None:
212
+ """Load the validated manifest for an installed extension.
213
+
214
+ Re-reads and re-validates the stored manifest against the live install
215
+ path -- the copy validated at install time may have changed on disk
216
+ since.
217
+
218
+ Returns:
219
+ None if not installed, or if the install no longer validates.
220
+ """
221
+ installed_dir = _resolve_installed_dir(installed_dir)
222
+ if get_installed_canvas_extension(name, installed_dir) is None:
223
+ return None
224
+ try:
225
+ return CanvasExtensionInstallationInterface.load_from_dir(installed_dir / name)
226
+ except (ValidationError, ValueError, OSError):
227
+ return None
228
+
229
+
230
+ def get_canvas_extension_bundle_path(
231
+ name: str, installed_dir: Path | None = None
232
+ ) -> Path | None:
233
+ """Resolve the on-disk path to *name*'s entrypoint bundle file.
234
+
235
+ Re-validates the manifest and entrypoint containment against the live
236
+ install path on every call -- this backs a bundle serve, so the check
237
+ done at install time isn't enough on its own (a symlink could change
238
+ between requests).
239
+
240
+ Returns:
241
+ None if not installed, or if the install no longer validates.
242
+ """
243
+ installed_dir = _resolve_installed_dir(installed_dir)
244
+ manifest = get_installed_canvas_extension_manifest(name, installed_dir)
245
+ if manifest is None:
246
+ return None
247
+ return resolve_entrypoint(manifest, installed_dir / name)
248
+
249
+
209
250
  class CanvasExtensionUpdateCheck(BaseModel):
210
251
  """Result of ``check_canvas_extension_update``.
211
252
 
@@ -0,0 +1,291 @@
1
+ """Canvas Extensions router for OpenHands Agent Server.
2
+
3
+ HTTP API endpoints for canvas extension operations. Business logic is
4
+ delegated to ``canvas_extensions/installed.py``; this module mirrors
5
+ ``plugins_router.py`` / ``skills_router.py`` and stays focused on HTTP
6
+ concerns: install / list / get / enable-disable / uninstall, plus serving
7
+ an installed extension's entrypoint bundle to the Canvas frontend.
8
+ """
9
+
10
+ from typing import Annotated, Final
11
+
12
+ from fastapi import APIRouter, HTTPException, Path
13
+ from fastapi.responses import FileResponse
14
+ from pydantic import BaseModel, Field, ValidationError
15
+
16
+ from openhands.agent_server.canvas_extensions.installed import (
17
+ InstalledCanvasExtensionInfo,
18
+ disable_canvas_extension,
19
+ enable_canvas_extension,
20
+ get_canvas_extension_bundle_path,
21
+ get_installed_canvas_extension,
22
+ get_installed_canvas_extension_manifest,
23
+ install_canvas_extension,
24
+ list_installed_canvas_extensions,
25
+ uninstall_canvas_extension,
26
+ )
27
+ from openhands.agent_server.canvas_extensions.manifest import CanvasExtensionManifest
28
+ from openhands.sdk.extensions.fetch import ExtensionFetchError
29
+
30
+
31
+ canvas_extensions_router = APIRouter(
32
+ prefix="/canvas-extensions", tags=["Canvas Extensions"]
33
+ )
34
+
35
+ # Matches the SDK's validate_extension_name() rule.
36
+ CANVAS_EXTENSION_NAME_PATTERN: Final[str] = r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
37
+
38
+ CanvasExtensionNamePath = Annotated[
39
+ str,
40
+ Path(
41
+ min_length=1,
42
+ max_length=255,
43
+ pattern=CANVAS_EXTENSION_NAME_PATTERN,
44
+ description="Canvas extension name (lowercase alphanumeric, hyphens)",
45
+ ),
46
+ ]
47
+
48
+
49
+ class InstallCanvasExtensionRequest(BaseModel):
50
+ """Request body for installing a canvas extension."""
51
+
52
+ source: str = Field(
53
+ min_length=1,
54
+ description=(
55
+ "Canvas extension source - git URL, GitHub shorthand, or local "
56
+ "path. Examples: 'github:owner/repo', '/path/to/extension'"
57
+ ),
58
+ )
59
+ ref: str | None = Field(
60
+ default=None, description="Optional branch, tag, or commit to install"
61
+ )
62
+ repo_path: str | None = Field(
63
+ default=None,
64
+ description="Subdirectory path within the repository (for monorepos)",
65
+ )
66
+ force: bool = Field(
67
+ default=False, description="If true, overwrite existing installation"
68
+ )
69
+
70
+
71
+ class InstalledCanvasExtensionResponse(BaseModel):
72
+ """Response containing installed canvas extension information."""
73
+
74
+ name: str = Field(description="Canvas extension name")
75
+ version: str = Field(default="", description="Canvas extension version")
76
+ description: str = Field(default="", description="Canvas extension description")
77
+ enabled: bool = Field(
78
+ default=False, description="Whether the canvas extension is enabled"
79
+ )
80
+ source: str = Field(description="Original source (e.g., 'github:owner/repo')")
81
+ resolved_ref: str | None = Field(
82
+ default=None, description="Resolved git commit SHA"
83
+ )
84
+ repo_path: str | None = Field(
85
+ default=None, description="Subdirectory path within the repository"
86
+ )
87
+ installed_at: str = Field(description="ISO 8601 timestamp of installation")
88
+ install_path: str = Field(description="Path where the extension is installed")
89
+ manifest: CanvasExtensionManifest | None = Field(
90
+ default=None,
91
+ description=(
92
+ "The extension's validated manifest (display_name, "
93
+ "contributes.pages, ...). None if the installed manifest can "
94
+ "no longer be read."
95
+ ),
96
+ )
97
+
98
+ @classmethod
99
+ def from_info(
100
+ cls,
101
+ info: InstalledCanvasExtensionInfo,
102
+ manifest: CanvasExtensionManifest | None,
103
+ ) -> "InstalledCanvasExtensionResponse":
104
+ return cls(
105
+ name=info.name,
106
+ version=info.version,
107
+ description=info.description,
108
+ enabled=info.enabled,
109
+ source=info.source,
110
+ resolved_ref=info.resolved_ref,
111
+ repo_path=info.repo_path,
112
+ installed_at=info.installed_at,
113
+ install_path=str(info.install_path),
114
+ manifest=manifest,
115
+ )
116
+
117
+
118
+ class InstalledCanvasExtensionsListResponse(BaseModel):
119
+ """Response containing the list of installed canvas extensions."""
120
+
121
+ canvas_extensions: list[InstalledCanvasExtensionResponse]
122
+
123
+
124
+ class UpdateCanvasExtensionStateRequest(BaseModel):
125
+ """Request body for updating canvas extension state (enable/disable)."""
126
+
127
+ enabled: bool
128
+
129
+
130
+ class UpdateCanvasExtensionStateResponse(BaseModel):
131
+ """Response from a canvas extension state update."""
132
+
133
+ name: str
134
+ enabled: bool
135
+
136
+
137
+ class UninstallCanvasExtensionResponse(BaseModel):
138
+ """Response from a canvas extension uninstall."""
139
+
140
+ message: str
141
+
142
+
143
+ @canvas_extensions_router.post(
144
+ "/install",
145
+ response_model=InstalledCanvasExtensionResponse,
146
+ responses={
147
+ 400: {"description": "Failed to fetch canvas extension source"},
148
+ 409: {"description": "Canvas extension already installed (use force=true)"},
149
+ 422: {"description": "Invalid canvas extension (bad manifest, etc.)"},
150
+ },
151
+ )
152
+ def install_canvas_extension_endpoint(
153
+ request: InstallCanvasExtensionRequest,
154
+ ) -> InstalledCanvasExtensionResponse:
155
+ """Install a canvas extension from a git URL, GitHub shorthand, or local
156
+ path.
157
+
158
+ A fresh install always lands disabled, regardless of the request body --
159
+ there is no ``enabled`` field to smuggle a different state through.
160
+ """
161
+ try:
162
+ info = install_canvas_extension(
163
+ source=request.source,
164
+ ref=request.ref,
165
+ repo_path=request.repo_path,
166
+ force=request.force,
167
+ )
168
+ return InstalledCanvasExtensionResponse.from_info(
169
+ info, get_installed_canvas_extension_manifest(info.name)
170
+ )
171
+ except FileExistsError:
172
+ raise HTTPException(
173
+ status_code=409,
174
+ detail="Canvas extension already installed. Use force=true to overwrite.",
175
+ )
176
+ except ExtensionFetchError:
177
+ raise HTTPException(
178
+ status_code=400,
179
+ detail=(
180
+ "Failed to fetch canvas extension source. "
181
+ "Check that the source is valid."
182
+ ),
183
+ )
184
+ except (ValidationError, ValueError, OSError):
185
+ raise HTTPException(
186
+ status_code=422,
187
+ detail="Invalid canvas extension. Ensure the manifest is well-formed.",
188
+ )
189
+
190
+
191
+ @canvas_extensions_router.get(
192
+ "/installed", response_model=InstalledCanvasExtensionsListResponse
193
+ )
194
+ def list_installed_canvas_extensions_endpoint() -> (
195
+ InstalledCanvasExtensionsListResponse
196
+ ):
197
+ """List all installed canvas extensions (enabled and disabled)."""
198
+ infos = list_installed_canvas_extensions()
199
+ return InstalledCanvasExtensionsListResponse(
200
+ canvas_extensions=[
201
+ InstalledCanvasExtensionResponse.from_info(
202
+ i, get_installed_canvas_extension_manifest(i.name)
203
+ )
204
+ for i in infos
205
+ ]
206
+ )
207
+
208
+
209
+ @canvas_extensions_router.get(
210
+ "/installed/{extension_name}",
211
+ response_model=InstalledCanvasExtensionResponse,
212
+ responses={404: {"description": "Canvas extension not installed"}},
213
+ )
214
+ def get_installed_canvas_extension_endpoint(
215
+ extension_name: CanvasExtensionNamePath,
216
+ ) -> InstalledCanvasExtensionResponse:
217
+ """Get information about a specific installed canvas extension."""
218
+ info = get_installed_canvas_extension(name=extension_name)
219
+ if info is None:
220
+ raise HTTPException(
221
+ status_code=404,
222
+ detail=f"Canvas extension '{extension_name}' is not installed",
223
+ )
224
+ return InstalledCanvasExtensionResponse.from_info(
225
+ info, get_installed_canvas_extension_manifest(extension_name)
226
+ )
227
+
228
+
229
+ @canvas_extensions_router.patch(
230
+ "/installed/{extension_name}",
231
+ response_model=UpdateCanvasExtensionStateResponse,
232
+ responses={404: {"description": "Canvas extension not installed"}},
233
+ )
234
+ def set_canvas_extension_enabled_endpoint(
235
+ extension_name: CanvasExtensionNamePath, request: UpdateCanvasExtensionStateRequest
236
+ ) -> UpdateCanvasExtensionStateResponse:
237
+ """Enable or disable an installed canvas extension."""
238
+ fn = enable_canvas_extension if request.enabled else disable_canvas_extension
239
+ if not fn(name=extension_name):
240
+ raise HTTPException(
241
+ status_code=404,
242
+ detail=f"Canvas extension '{extension_name}' is not installed",
243
+ )
244
+ return UpdateCanvasExtensionStateResponse(
245
+ name=extension_name, enabled=request.enabled
246
+ )
247
+
248
+
249
+ @canvas_extensions_router.delete(
250
+ "/installed/{extension_name}",
251
+ response_model=UninstallCanvasExtensionResponse,
252
+ responses={404: {"description": "Canvas extension not installed"}},
253
+ )
254
+ def uninstall_canvas_extension_endpoint(
255
+ extension_name: CanvasExtensionNamePath,
256
+ ) -> UninstallCanvasExtensionResponse:
257
+ """Uninstall a canvas extension by name."""
258
+ if not uninstall_canvas_extension(name=extension_name):
259
+ raise HTTPException(
260
+ status_code=404,
261
+ detail=f"Canvas extension '{extension_name}' is not installed",
262
+ )
263
+ return UninstallCanvasExtensionResponse(
264
+ message=f"Canvas extension '{extension_name}' uninstalled"
265
+ )
266
+
267
+
268
+ @canvas_extensions_router.get(
269
+ "/installed/{extension_name}/bundle",
270
+ responses={404: {"description": "Canvas extension or bundle not found"}},
271
+ )
272
+ def get_canvas_extension_bundle_endpoint(
273
+ extension_name: CanvasExtensionNamePath,
274
+ ) -> FileResponse:
275
+ """Serve an installed canvas extension's entrypoint bundle file.
276
+
277
+ Entrypoint containment is re-validated against the live install path on
278
+ every request, not just at install time -- see
279
+ ``get_canvas_extension_bundle_path``. ``Cache-Control: no-cache`` forces
280
+ revalidation on each request, so a client never keeps serving a bundle
281
+ from before the extension's last refresh: the actual bytes are re-read
282
+ from disk, and Starlette's file-stat-based ``ETag``/``Last-Modified``
283
+ change the moment the on-disk revision does.
284
+ """
285
+ bundle_path = get_canvas_extension_bundle_path(name=extension_name)
286
+ if bundle_path is None:
287
+ raise HTTPException(
288
+ status_code=404,
289
+ detail=f"Canvas extension '{extension_name}' bundle not found",
290
+ )
291
+ return FileResponse(bundle_path, headers={"Cache-Control": "no-cache"})
@@ -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
 
@@ -349,6 +350,20 @@ class Config(BaseModel):
349
350
  "The URL where this agent server instance is available externally"
350
351
  ),
351
352
  )
353
+ acp_skill_sourcing: ACPSkillSourcing = Field(
354
+ default="native",
355
+ description=(
356
+ "Who supplies an ACP agent's skills. 'native' (the default, for a "
357
+ "host-local agent-server): nobody but the ACP CLI — it reads the "
358
+ "user's own home configuration and the repository, so OpenHands "
359
+ "injects none of its managed skills. 'openhands_managed' (for "
360
+ "container runtimes, where that host configuration is absent): also "
361
+ "inject the user/org/public/marketplace skills the server "
362
+ "discovers. Project/repository skills are never injected either way "
363
+ "— the CLI reads AGENTS.md itself (#4019). Set explicitly per "
364
+ "deployment; the agent-server image sets 'openhands_managed'."
365
+ ),
366
+ )
352
367
  registered_marketplaces: list[MarketplaceRegistration] = Field(
353
368
  default_factory=list,
354
369
  description=(