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.
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/PKG-INFO +1 -1
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/plugins_router.py +47 -11
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/plugins_service.py +115 -18
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/skills_router.py +2 -2
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/skills_service.py +2 -32
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands_agent_server.egg-info/PKG-INFO +1 -1
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/pyproject.toml +1 -1
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/__init__.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/__main__.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/_secrets_exposure.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/agent_profiles_router.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/api.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/auth_router.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/bash_router.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/bash_service.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/config.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/conversation_lease.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/conversation_router.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/conversation_service.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/dependencies.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/desktop_router.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/desktop_service.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/docker/Dockerfile +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/docker/build.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/docker/wallpaper.svg +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/env_parser.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/event_compat.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/event_router.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/event_service.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/file_router.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/git_router.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/hooks_router.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/hooks_service.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/init_router.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/llm_router.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/logging_config.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/mcp_oauth_store.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/mcp_router.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/middleware.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/models.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/openai/__init__.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/openai/models.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/openai/router.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/openai/service.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/openapi.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/persistence/__init__.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/persistence/models.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/persistence/store.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/profiles_router.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/pub_sub.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/py.typed +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/server_details_router.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/settings_router.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/sockets.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/sub_agents_router.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/tool_preload_service.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/tool_router.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/utils.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/vscode_router.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/vscode_service.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/workspace_router.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/workspaces_router.py +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands_agent_server.egg-info/SOURCES.txt +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands_agent_server.egg-info/entry_points.txt +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands_agent_server.egg-info/requires.txt +0 -0
- {openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands_agent_server.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
|
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=[
|
|
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
|
|
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=
|
|
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
|
-
#
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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(
|
|
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
|
-
``(
|
|
246
|
-
|
|
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[
|
|
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
|
-
|
|
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
|
|
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
|
-
"""
|
|
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
|
|
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.
|
|
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
|
{openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/__init__.py
RENAMED
|
File without changes
|
{openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/api.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/env_parser.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/git_router.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/llm_router.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/mcp_router.py
RENAMED
|
File without changes
|
{openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/middleware.py
RENAMED
|
File without changes
|
{openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/openapi.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/pub_sub.py
RENAMED
|
File without changes
|
{openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/sockets.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.35.0 → openhands_agent_server-1.36.1}/openhands/agent_server/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|