openhands-agent-server 1.35.0__tar.gz → 1.36.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 (70) hide show
  1. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/PKG-INFO +1 -1
  2. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/plugins_router.py +47 -11
  3. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/plugins_service.py +115 -18
  4. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/skills_router.py +2 -2
  5. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/skills_service.py +2 -32
  6. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands_agent_server.egg-info/PKG-INFO +1 -1
  7. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/pyproject.toml +1 -1
  8. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/__init__.py +0 -0
  9. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/__main__.py +0 -0
  10. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/_secrets_exposure.py +0 -0
  11. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/agent_profiles_router.py +0 -0
  12. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/api.py +0 -0
  13. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/auth_router.py +0 -0
  14. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/bash_router.py +0 -0
  15. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/bash_service.py +0 -0
  16. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/config.py +0 -0
  17. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/conversation_lease.py +0 -0
  18. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/conversation_router.py +0 -0
  19. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/conversation_service.py +0 -0
  20. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/dependencies.py +0 -0
  21. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/desktop_router.py +0 -0
  22. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/desktop_service.py +0 -0
  23. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/docker/Dockerfile +0 -0
  24. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/docker/build.py +0 -0
  25. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/docker/wallpaper.svg +0 -0
  26. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/env_parser.py +0 -0
  27. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/event_compat.py +0 -0
  28. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/event_router.py +0 -0
  29. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/event_service.py +0 -0
  30. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/file_router.py +0 -0
  31. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/git_router.py +0 -0
  32. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/hooks_router.py +0 -0
  33. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/hooks_service.py +0 -0
  34. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/init_router.py +0 -0
  35. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/llm_router.py +0 -0
  36. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/logging_config.py +0 -0
  37. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/mcp_oauth_store.py +0 -0
  38. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/mcp_router.py +0 -0
  39. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/middleware.py +0 -0
  40. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/models.py +0 -0
  41. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/openai/__init__.py +0 -0
  42. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/openai/models.py +0 -0
  43. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/openai/router.py +0 -0
  44. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/openai/service.py +0 -0
  45. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/openapi.py +0 -0
  46. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/persistence/__init__.py +0 -0
  47. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/persistence/models.py +0 -0
  48. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/persistence/store.py +0 -0
  49. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/profiles_router.py +0 -0
  50. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/pub_sub.py +0 -0
  51. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/py.typed +0 -0
  52. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/server_details_router.py +0 -0
  53. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/settings_router.py +0 -0
  54. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/sockets.py +0 -0
  55. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/sub_agents_router.py +0 -0
  56. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/tool_preload_service.py +0 -0
  57. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/tool_router.py +0 -0
  58. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/utils.py +0 -0
  59. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
  60. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
  61. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/vscode_router.py +0 -0
  62. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/vscode_service.py +0 -0
  63. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/workspace_router.py +0 -0
  64. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/workspaces_router.py +0 -0
  65. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands_agent_server.egg-info/SOURCES.txt +0 -0
  66. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
  67. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands_agent_server.egg-info/entry_points.txt +0 -0
  68. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands_agent_server.egg-info/requires.txt +0 -0
  69. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands_agent_server.egg-info/top_level.txt +0 -0
  70. {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-agent-server
3
- Version: 1.35.0
3
+ Version: 1.36.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
@@ -16,6 +16,7 @@ from pydantic import BaseModel, Field
16
16
 
17
17
  from openhands.agent_server.plugins_service import (
18
18
  MarketplacePluginInfo,
19
+ PluginSkillSummary,
19
20
  service_disable_plugin,
20
21
  service_enable_plugin,
21
22
  service_get_installed_plugin,
@@ -23,11 +24,13 @@ from openhands.agent_server.plugins_service import (
23
24
  service_install_plugin,
24
25
  service_list_available_plugins,
25
26
  service_list_installed_plugins,
27
+ service_load_plugin_contents,
28
+ service_plugin_contents,
26
29
  service_uninstall_plugin,
27
30
  service_update_plugin,
28
31
  )
29
32
  from openhands.sdk.extensions.fetch import ExtensionFetchError
30
- from openhands.sdk.plugin import InstalledPluginInfo, PluginFetchError
33
+ from openhands.sdk.plugin import InstalledPluginInfo, Plugin, PluginFetchError
31
34
 
32
35
 
33
36
  plugins_router = APIRouter(prefix="/plugins", tags=["Plugins"])
@@ -73,6 +76,14 @@ class PluginInfo(BaseModel):
73
76
  name: str
74
77
  version: str = ""
75
78
  description: str = ""
79
+ path: str = Field(default="", description="Path to the plugin directory")
80
+ skills: list[PluginSkillSummary] = Field(
81
+ default_factory=list, description="Skills bundled in the plugin"
82
+ )
83
+ files: list[str] = Field(
84
+ default_factory=list,
85
+ description="Plugin files, relative to the plugin directory",
86
+ )
76
87
 
77
88
 
78
89
  class PluginsResponse(BaseModel):
@@ -119,6 +130,14 @@ class InstalledPluginResponse(BaseModel):
119
130
  )
120
131
  installed_at: str = Field(description="ISO 8601 timestamp of installation")
121
132
  install_path: str = Field(description="Path where the plugin is installed")
133
+ skills: list[PluginSkillSummary] | None = Field(
134
+ default=None,
135
+ description="Skills bundled in the plugin (None if it failed to load)",
136
+ )
137
+ files: list[str] | None = Field(
138
+ default=None,
139
+ description="Plugin files, relative to install_path (None if load failed)",
140
+ )
122
141
 
123
142
  @classmethod
124
143
  def from_plugin_info(cls, info: InstalledPluginInfo) -> "InstalledPluginResponse":
@@ -173,6 +192,28 @@ class MarketplaceCatalogResponse(BaseModel):
173
192
  plugins: list[MarketplacePluginInfo]
174
193
 
175
194
 
195
+ def _installed_response(info: InstalledPluginInfo) -> InstalledPluginResponse:
196
+ """Build an installed-plugin response enriched with the plugin's contents."""
197
+ response = InstalledPluginResponse.from_plugin_info(info)
198
+ contents = service_load_plugin_contents(info.install_path)
199
+ if contents is not None:
200
+ response.skills, response.files = contents
201
+ return response
202
+
203
+
204
+ def _plugin_info(plugin: Plugin) -> PluginInfo:
205
+ """Build an available-plugin summary including its contents."""
206
+ skills, files = service_plugin_contents(plugin)
207
+ return PluginInfo(
208
+ name=plugin.name,
209
+ version=plugin.version,
210
+ description=plugin.description,
211
+ path=plugin.path,
212
+ skills=skills,
213
+ files=files,
214
+ )
215
+
216
+
176
217
  # ---------------------------------------------------------------------------
177
218
  # Endpoints
178
219
  # ---------------------------------------------------------------------------
@@ -193,12 +234,7 @@ def get_plugins(request: PluginsRequest) -> PluginsResponse:
193
234
  load_project=request.load_project,
194
235
  project_dir=request.project_dir,
195
236
  )
196
- return PluginsResponse(
197
- plugins=[
198
- PluginInfo(name=p.name, version=p.version, description=p.description)
199
- for p in plugins
200
- ]
201
- )
237
+ return PluginsResponse(plugins=[_plugin_info(p) for p in plugins])
202
238
 
203
239
 
204
240
  @plugins_router.post(
@@ -219,7 +255,7 @@ def install_plugin_endpoint(request: InstallPluginRequest) -> InstalledPluginRes
219
255
  repo_path=request.repo_path,
220
256
  force=request.force,
221
257
  )
222
- return InstalledPluginResponse.from_plugin_info(info)
258
+ return _installed_response(info)
223
259
  except FileExistsError:
224
260
  raise HTTPException(
225
261
  status_code=409,
@@ -242,7 +278,7 @@ def list_installed_plugins_endpoint() -> InstalledPluginsListResponse:
242
278
  """List all installed plugins (enabled and disabled)."""
243
279
  plugins = service_list_installed_plugins()
244
280
  return InstalledPluginsListResponse(
245
- plugins=[InstalledPluginResponse.from_plugin_info(info) for info in plugins]
281
+ plugins=[_installed_response(info) for info in plugins]
246
282
  )
247
283
 
248
284
 
@@ -261,7 +297,7 @@ def get_installed_plugin_endpoint(
261
297
  status_code=404,
262
298
  detail=f"Plugin '{plugin_name}' is not installed",
263
299
  )
264
- return InstalledPluginResponse.from_plugin_info(info)
300
+ return _installed_response(info)
265
301
 
266
302
 
267
303
  @plugins_router.patch(
@@ -312,7 +348,7 @@ def refresh_plugin_endpoint(plugin_name: PluginNamePath) -> UpdatePluginResponse
312
348
  )
313
349
  return UpdatePluginResponse(
314
350
  message=f"Plugin '{plugin_name}' updated",
315
- plugin=InstalledPluginResponse.from_plugin_info(info),
351
+ plugin=_installed_response(info),
316
352
  )
317
353
 
318
354
 
@@ -14,6 +14,7 @@ counterparts (``skills_service.py``) so the router stays focused on HTTP:
14
14
  """
15
15
 
16
16
  import json
17
+ import os
17
18
  from pathlib import Path
18
19
  from time import monotonic
19
20
 
@@ -127,6 +128,73 @@ def service_list_available_plugins(
127
128
  return list(available.values())
128
129
 
129
130
 
131
+ # ---------------------------------------------------------------------------
132
+ # Plugin contents (bundled skills + file listing)
133
+ # ---------------------------------------------------------------------------
134
+
135
+ # Directories never listed in a plugin's file tree. Whole-repo installs copy
136
+ # the fetched clone verbatim (including ``.git``), which would flood the
137
+ # listing with repository internals.
138
+ _PLUGIN_FILES_EXCLUDED_DIRS = {".git"}
139
+ # Upper bound on listed files so a pathological plugin (e.g. a whole-repo
140
+ # install of a large repository) cannot bloat API payloads.
141
+ _PLUGIN_FILES_LIMIT = 2000
142
+
143
+
144
+ class PluginSkillSummary(BaseModel):
145
+ """Summary of a skill bundled in a plugin."""
146
+
147
+ name: str
148
+ description: str | None = None
149
+
150
+
151
+ def _list_plugin_files(root: Path) -> list[str]:
152
+ """List a plugin directory's files as sorted, POSIX, root-relative paths.
153
+
154
+ Uses ``os.walk`` so excluded directories are pruned before being descended
155
+ into, and symlinked directories are never followed (real plugins ship
156
+ manifest-dir symlinks such as ``.claude-plugin -> .plugin``).
157
+ """
158
+ files: list[str] = []
159
+ for dirpath, dirnames, filenames in os.walk(root):
160
+ dirnames[:] = [d for d in dirnames if d not in _PLUGIN_FILES_EXCLUDED_DIRS]
161
+ for filename in filenames:
162
+ files.append((Path(dirpath) / filename).relative_to(root).as_posix())
163
+ files.sort()
164
+ return files[:_PLUGIN_FILES_LIMIT]
165
+
166
+
167
+ def service_plugin_contents(
168
+ plugin: Plugin,
169
+ ) -> tuple[list[PluginSkillSummary], list[str]]:
170
+ """Contents of an already-loaded plugin: bundled skills + file listing.
171
+
172
+ Skills include command-derived ones (``<plugin>:<command>``), matching what
173
+ the plugin actually contributes to a conversation.
174
+ """
175
+ skills = [
176
+ PluginSkillSummary(name=skill.name, description=skill.description)
177
+ for skill in plugin.get_all_skills()
178
+ ]
179
+ return skills, _list_plugin_files(Path(plugin.path))
180
+
181
+
182
+ def service_load_plugin_contents(
183
+ plugin_dir: str | Path,
184
+ ) -> tuple[list[PluginSkillSummary], list[str]] | None:
185
+ """Load a plugin directory's contents, or None if it cannot be loaded.
186
+
187
+ Never raises: a corrupt or vanished plugin directory must not break the
188
+ listings that embed these contents.
189
+ """
190
+ try:
191
+ plugin = Plugin.load(plugin_dir)
192
+ except Exception as e:
193
+ logger.warning(f"Failed to load plugin contents from {plugin_dir}: {e}")
194
+ return None
195
+ return service_plugin_contents(plugin)
196
+
197
+
130
198
  # ---------------------------------------------------------------------------
131
199
  # Plugins-only marketplace catalog
132
200
  # ---------------------------------------------------------------------------
@@ -151,6 +219,12 @@ class MarketplacePluginInfo(BaseModel):
151
219
  ref: str | None = None
152
220
  repo_path: str | None = None
153
221
  installed: bool
222
+ # Local contents — populated when the entry resolves to a directory in the
223
+ # local marketplace clone; None when contents are not locally available
224
+ # (e.g. a structured source pointing at another repository).
225
+ path: str | None = None
226
+ skills: list[PluginSkillSummary] | None = None
227
+ files: list[str] | None = None
154
228
 
155
229
 
156
230
  # ---------------------------------------------------------------------------
@@ -163,9 +237,13 @@ class MarketplacePluginInfo(BaseModel):
163
237
  # transient fetch failure) is NOT cached, so one flaky fetch does not blank the
164
238
  # catalog for the whole TTL.
165
239
  #
166
- # Type: (timestamp, list-of-(name, description, source, ref, repo_path)) or None
167
- _PluginCatalogEntry = tuple[str, str | None, str, str | None, str | None]
168
- _plugin_catalog_cache: tuple[float, list[_PluginCatalogEntry]] | None = None
240
+ # Cached entries are full MarketplacePluginInfo objects (including contents)
241
+ # stored with ``installed=False``; serving stamps the fresh installed flag via
242
+ # a shallow ``model_copy``, so the ``skills``/``files`` lists are shared across
243
+ # requests — they are never mutated after construction.
244
+ #
245
+ # Type: (timestamp, catalog entries with installed=False) or None
246
+ _plugin_catalog_cache: tuple[float, list[MarketplacePluginInfo]] | None = None
169
247
  _PLUGIN_CATALOG_TTL_SECONDS = 300 # 5 minutes
170
248
 
171
249
 
@@ -211,15 +289,8 @@ def service_get_plugins_marketplace_catalog(
211
289
  p.name for p in list_installed_plugins(installed_dir=installed_dir)
212
290
  }
213
291
  return [
214
- MarketplacePluginInfo(
215
- name=name,
216
- description=desc,
217
- source=src,
218
- ref=ref,
219
- repo_path=repo_path,
220
- installed=name in installed_names,
221
- )
222
- for name, desc, src, ref, repo_path in entries
292
+ entry.model_copy(update={"installed": entry.name in installed_names})
293
+ for entry in entries
223
294
  ]
224
295
 
225
296
 
@@ -238,12 +309,15 @@ def _is_true_plugin(raw_source: object) -> bool:
238
309
  return subpath.startswith(_PLUGINS_SUBPATH_PREFIX)
239
310
 
240
311
 
241
- def _fetch_plugin_catalog_entries(marketplace_path: str) -> list[_PluginCatalogEntry]:
312
+ def _fetch_plugin_catalog_entries(
313
+ marketplace_path: str,
314
+ ) -> list[MarketplacePluginInfo]:
242
315
  """Fetch the marketplace and keep only true plugins.
243
316
 
244
- Slow path: git fetch + read the marketplace JSON. Returns
245
- ``(name, description, source, ref, repo_path)`` tuples, or an empty list on
246
- error.
317
+ Slow path: git fetch + read the marketplace JSON. Returns catalog entries
318
+ with ``installed=False`` (the caller stamps the fresh value), enriched with
319
+ local contents (``path``/``skills``/``files``) when an entry resolves to a
320
+ directory inside the local clone. Returns an empty list on error.
247
321
  """
248
322
  cache_dir = get_skills_cache_dir()
249
323
  repo_path = update_skills_repository(
@@ -282,7 +356,7 @@ def _fetch_plugin_catalog_entries(marketplace_path: str) -> list[_PluginCatalogE
282
356
  logger.warning(f"Failed to load marketplace: {e}, {e2}")
283
357
  return []
284
358
 
285
- entries: list[_PluginCatalogEntry] = []
359
+ entries: list[MarketplacePluginInfo] = []
286
360
  for plugin in marketplace.plugins:
287
361
  if not _is_true_plugin(plugin.source):
288
362
  continue
@@ -290,6 +364,29 @@ def _fetch_plugin_catalog_entries(marketplace_path: str) -> list[_PluginCatalogE
290
364
  # this yields an absolute path with ref/repo_path None; structured
291
365
  # github/url sources yield their ref + subpath.
292
366
  source, ref, repo_path = marketplace.resolve_plugin_source(plugin)
293
- entries.append((plugin.name, plugin.description, source, ref, repo_path))
367
+ entry = MarketplacePluginInfo(
368
+ name=plugin.name,
369
+ description=plugin.description,
370
+ source=source,
371
+ ref=ref,
372
+ repo_path=repo_path,
373
+ installed=False,
374
+ )
375
+ # A local ./plugins/<name> entry resolves to a directory inside the
376
+ # cached clone, so its contents can be loaded from disk. Structured
377
+ # github/url sources may point at other repositories — no local copy,
378
+ # so their contents stay None.
379
+ if ref is None and repo_path is None and Path(source).is_dir():
380
+ contents = service_load_plugin_contents(source)
381
+ if contents is not None:
382
+ skills, files = contents
383
+ entry = entry.model_copy(
384
+ update={
385
+ "path": to_posix_path(source),
386
+ "skills": skills,
387
+ "files": files,
388
+ }
389
+ )
390
+ entries.append(entry)
294
391
 
295
392
  return entries
@@ -104,8 +104,8 @@ class SkillsRequest(BaseModel):
104
104
  default_factory=list,
105
105
  description=(
106
106
  "Marketplace registrations for plugin-based skill loading. Registrations "
107
- "with auto_load=True or a list of plugin names replace legacy public "
108
- "skills."
107
+ "with auto_load=True or a list of plugin names add their skills to the "
108
+ "public skills."
109
109
  ),
110
110
  )
111
111
 
@@ -345,33 +345,7 @@ def load_all_skills(
345
345
  marketplace_path: str | None = DEFAULT_MARKETPLACE_PATH,
346
346
  registered_marketplaces: list[MarketplaceRegistration] | None = None,
347
347
  ) -> SkillLoadResult:
348
- """Load and merge skills from all configured sources.
349
-
350
- Skills are loaded from multiple sources and merged with the following
351
- precedence (later overrides earlier for duplicate names):
352
- 1. Sandbox skills (lowest) - Exposed URLs from sandbox
353
- 2. Registered marketplace skills or public skills legacy fallback
354
- 3. User skills - From ~/.openhands/skills/
355
- 4. Organization skills - From {org}/.openhands or equivalent
356
- 5. Project skills (highest) - From {workspace}/.openhands/skills/
357
-
358
- Args:
359
- load_public: Whether to load public skills from OpenHands/extensions repo.
360
- load_user: Whether to load user skills from ~/.openhands/skills/.
361
- load_project: Whether to load project skills from workspace.
362
- load_org: Whether to load organization-level skills.
363
- project_dir: Workspace directory path for project skills.
364
- org_repos: Pre-authenticated (Git URL, org name) pairs for org skills.
365
- All repos are loaded and merged into the single org tier.
366
- sandbox_exposed_urls: List of exposed URLs from sandbox.
367
- marketplace_path: Relative marketplace JSON path for public skills.
368
- Pass None to load all public skills without marketplace filtering.
369
- registered_marketplaces: Marketplace registrations whose auto-load plugins
370
- replace legacy public skills when provided.
371
-
372
- Returns:
373
- SkillLoadResult containing merged skills and source counts.
374
- """
348
+ """Merge skills: sandbox < marketplace < public < user < org < project."""
375
349
  sources: dict[str, int] = {}
376
350
  skill_lists: list[list[Skill]] = []
377
351
 
@@ -396,15 +370,11 @@ def load_all_skills(
396
370
  sources["registered_marketplaces"] = len(marketplace_skills)
397
371
  skill_lists.append(marketplace_skills)
398
372
 
399
- # 2-3. Load legacy public + user skills via helper (no project yet — org sits
400
- # between). Auto-load registered marketplaces replace legacy public skills,
401
- # while user skills keep their existing precedence above the public marketplace
402
- # tier.
403
373
  sdk_base = load_available_skills(
404
374
  work_dir=None,
405
375
  include_user=load_user,
406
376
  include_project=False,
407
- include_public=load_public and not auto_load_registrations,
377
+ include_public=load_public,
408
378
  marketplace_path=marketplace_path,
409
379
  )
410
380
  sources["sdk_base"] = len(sdk_base)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-agent-server
3
- Version: 1.35.0
3
+ Version: 1.36.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openhands-agent-server"
3
- version = "1.35.0"
3
+ version = "1.36.1"
4
4
  description = "OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent"
5
5
 
6
6
  requires-python = ">=3.12"