openhands-agent-server 1.40.1__tar.gz → 1.41.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.
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/PKG-INFO +1 -1
- openhands_agent_server-1.41.0/openhands/agent_server/canvas_extensions/__init__.py +44 -0
- openhands_agent_server-1.41.0/openhands/agent_server/canvas_extensions/installed.py +403 -0
- openhands_agent_server-1.41.0/openhands/agent_server/canvas_extensions/manifest.py +166 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/mcp_router.py +0 -35
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/models.py +1 -33
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands_agent_server.egg-info/PKG-INFO +1 -1
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands_agent_server.egg-info/SOURCES.txt +6 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/pyproject.toml +1 -1
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/__init__.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/__main__.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/_secret_redaction.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/_secrets_exposure.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/agent_profiles_router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/api.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/auth_router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/bash_router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/bash_service.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/config.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/conversation_lease.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/conversation_router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/conversation_service.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/credential_binding.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/dependencies.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/desktop_router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/desktop_service.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/docker/Dockerfile +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/docker/build.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/docker/wallpaper.svg +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/env_parser.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/event_router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/event_service.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/file_router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/git_router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/hooks_router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/hooks_service.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/init_router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/llm_router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/logging_config.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/mcp_oauth_store.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/middleware.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/openai/__init__.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/openai/models.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/openai/router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/openai/service.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/openapi.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/persistence/__init__.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/persistence/models.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/persistence/store.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/plugins_router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/plugins_service.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/profiles_router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/pub_sub.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/py.typed +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/server_details_router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/settings_router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/skills_router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/skills_service.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/sockets.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/sub_agents_router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/telemetry/__init__.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/telemetry/factory.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/telemetry/http_exporter.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/telemetry/models.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/telemetry/policy.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/telemetry/posthog_exporter.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/telemetry/sanitizer.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/telemetry/service.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/telemetry/sink.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/telemetry/subscriber.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/tool_preload_service.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/tool_router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/utils.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/vscode_router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/vscode_service.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/workspace_router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/workspaces_router.py +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands_agent_server.egg-info/entry_points.txt +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands_agent_server.egg-info/requires.txt +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands_agent_server.egg-info/top_level.txt +0 -0
- {openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/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.41.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
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Canvas Extensions: installable UI bundles that contribute pages to Canvas."""
|
|
2
|
+
|
|
3
|
+
from openhands.agent_server.canvas_extensions.installed import (
|
|
4
|
+
CanvasExtensionUpdateCheck,
|
|
5
|
+
InstalledCanvasExtensionInfo,
|
|
6
|
+
apply_canvas_extension_update,
|
|
7
|
+
check_canvas_extension_update,
|
|
8
|
+
disable_canvas_extension,
|
|
9
|
+
enable_canvas_extension,
|
|
10
|
+
get_installed_canvas_extension,
|
|
11
|
+
get_installed_canvas_extensions_dir,
|
|
12
|
+
install_canvas_extension,
|
|
13
|
+
list_installed_canvas_extensions,
|
|
14
|
+
load_installed_canvas_extensions,
|
|
15
|
+
uninstall_canvas_extension,
|
|
16
|
+
)
|
|
17
|
+
from openhands.agent_server.canvas_extensions.manifest import (
|
|
18
|
+
MANIFEST_FILENAME,
|
|
19
|
+
CanvasExtensionContributes,
|
|
20
|
+
CanvasExtensionManifest,
|
|
21
|
+
CanvasExtensionPage,
|
|
22
|
+
resolve_entrypoint,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"CanvasExtensionManifest",
|
|
28
|
+
"CanvasExtensionContributes",
|
|
29
|
+
"CanvasExtensionPage",
|
|
30
|
+
"MANIFEST_FILENAME",
|
|
31
|
+
"resolve_entrypoint",
|
|
32
|
+
"InstalledCanvasExtensionInfo",
|
|
33
|
+
"install_canvas_extension",
|
|
34
|
+
"uninstall_canvas_extension",
|
|
35
|
+
"enable_canvas_extension",
|
|
36
|
+
"disable_canvas_extension",
|
|
37
|
+
"list_installed_canvas_extensions",
|
|
38
|
+
"load_installed_canvas_extensions",
|
|
39
|
+
"get_installed_canvas_extension",
|
|
40
|
+
"get_installed_canvas_extensions_dir",
|
|
41
|
+
"CanvasExtensionUpdateCheck",
|
|
42
|
+
"check_canvas_extension_update",
|
|
43
|
+
"apply_canvas_extension_update",
|
|
44
|
+
]
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""Installed Canvas Extensions.
|
|
2
|
+
|
|
3
|
+
Built on ``openhands.sdk.extensions.installation``, shared with Plugins/
|
|
4
|
+
Skills, with two behaviors specific to this module:
|
|
5
|
+
|
|
6
|
+
Disabled by default -- ``InstallationInfo.enabled`` defaults to ``True``
|
|
7
|
+
and neither ``InstallationManager.install()`` nor its directory discovery
|
|
8
|
+
override that for a new entry, so every write path that can create one
|
|
9
|
+
forces ``enabled=False`` as an explicit post-write step.
|
|
10
|
+
|
|
11
|
+
Staged refresh -- ``check``/``apply`` replace ``update()``/
|
|
12
|
+
``install(force=True)``, which rmtree+copytree straight onto the live
|
|
13
|
+
install path with no staging directory. ``check`` fetches and validates
|
|
14
|
+
into ``.staging/`` without touching the active install; ``apply`` swaps it
|
|
15
|
+
in via two atomic renames (POSIX can't atomically replace a non-empty
|
|
16
|
+
directory in one rename), rolling back if the second fails.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import shutil
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from pydantic import BaseModel, Field, ValidationError
|
|
24
|
+
|
|
25
|
+
from openhands.agent_server.canvas_extensions.manifest import (
|
|
26
|
+
MANIFEST_FILENAME,
|
|
27
|
+
CanvasExtensionManifest,
|
|
28
|
+
resolve_entrypoint,
|
|
29
|
+
)
|
|
30
|
+
from openhands.sdk.extensions.fetch import fetch_with_resolution
|
|
31
|
+
from openhands.sdk.extensions.installation import (
|
|
32
|
+
InstallationInfo,
|
|
33
|
+
InstallationInterface,
|
|
34
|
+
InstallationManager,
|
|
35
|
+
InstallationMetadata,
|
|
36
|
+
)
|
|
37
|
+
from openhands.sdk.extensions.installation.manager import DEFAULT_CACHE_DIR
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Matches the InstalledPluginInfo naming convention.
|
|
41
|
+
InstalledCanvasExtensionInfo = InstallationInfo
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_installed_canvas_extensions_dir() -> Path:
|
|
45
|
+
"""Get the default directory for installed canvas extensions."""
|
|
46
|
+
return Path.home() / ".openhands" / "canvas-extensions" / "installed"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CanvasExtensionInstallationInterface(
|
|
50
|
+
InstallationInterface[CanvasExtensionManifest]
|
|
51
|
+
):
|
|
52
|
+
@staticmethod
|
|
53
|
+
def load_from_dir(extension_dir: Path) -> CanvasExtensionManifest:
|
|
54
|
+
manifest_path = extension_dir / MANIFEST_FILENAME
|
|
55
|
+
manifest = CanvasExtensionManifest.model_validate_json(
|
|
56
|
+
manifest_path.read_text()
|
|
57
|
+
)
|
|
58
|
+
# Containment must hold too -- a parseable manifest alone isn't enough.
|
|
59
|
+
resolve_entrypoint(manifest, extension_dir)
|
|
60
|
+
return manifest
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _resolve_installed_dir(installed_dir: Path | None) -> Path:
|
|
64
|
+
return (
|
|
65
|
+
installed_dir
|
|
66
|
+
if installed_dir is not None
|
|
67
|
+
else get_installed_canvas_extensions_dir()
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _manager(installed_dir: Path) -> InstallationManager[CanvasExtensionManifest]:
|
|
72
|
+
return InstallationManager(
|
|
73
|
+
installation_dir=installed_dir,
|
|
74
|
+
installation_interface=CanvasExtensionInstallationInterface(),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _tracked_names(installed_dir: Path) -> set[str]:
|
|
79
|
+
"""Names with a real, valid tracked entry, not just a metadata key.
|
|
80
|
+
|
|
81
|
+
A stale record with no matching directory must not count as "already
|
|
82
|
+
installed" -- otherwise a real install of that name would wrongly
|
|
83
|
+
inherit its state as if this were a force-reinstall.
|
|
84
|
+
"""
|
|
85
|
+
if not installed_dir.exists():
|
|
86
|
+
return set()
|
|
87
|
+
metadata = InstallationMetadata.load_from_dir(installed_dir)
|
|
88
|
+
return {info.name for info in metadata.validate_tracked(installed_dir)}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _force_disable_new(
|
|
92
|
+
manager: InstallationManager[CanvasExtensionManifest],
|
|
93
|
+
info: InstallationInfo,
|
|
94
|
+
pre_existing: set[str],
|
|
95
|
+
) -> InstallationInfo:
|
|
96
|
+
"""Force a newly created tracked entry to ``enabled=False``.
|
|
97
|
+
|
|
98
|
+
``pre_existing`` distinguishes a genuinely new entry from a
|
|
99
|
+
force-reinstall, whose prior enabled state is already preserved.
|
|
100
|
+
"""
|
|
101
|
+
if info.name in pre_existing or not info.enabled:
|
|
102
|
+
return info
|
|
103
|
+
manager.disable(info.name)
|
|
104
|
+
info.enabled = False
|
|
105
|
+
return info
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def install_canvas_extension(
|
|
109
|
+
source: str,
|
|
110
|
+
ref: str | None = None,
|
|
111
|
+
repo_path: str | None = None,
|
|
112
|
+
installed_dir: Path | None = None,
|
|
113
|
+
force: bool = False,
|
|
114
|
+
) -> InstalledCanvasExtensionInfo:
|
|
115
|
+
"""Install a canvas extension from a source.
|
|
116
|
+
|
|
117
|
+
A newly created entry always lands disabled, regardless of what the
|
|
118
|
+
caller passes in.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
source: ``"github:owner/repo"``, a git URL, or a local path.
|
|
122
|
+
ref: Optional branch, tag, or commit to install.
|
|
123
|
+
repo_path: Subdirectory within the repository (for monorepos).
|
|
124
|
+
installed_dir: Defaults to ``~/.openhands/canvas-extensions/installed/``.
|
|
125
|
+
force: If True, overwrite an existing installation.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
InstalledCanvasExtensionInfo for the installed extension.
|
|
129
|
+
"""
|
|
130
|
+
installed_dir = _resolve_installed_dir(installed_dir)
|
|
131
|
+
manager = _manager(installed_dir)
|
|
132
|
+
pre_existing = _tracked_names(installed_dir)
|
|
133
|
+
info = manager.install(source, ref=ref, repo_path=repo_path, force=force)
|
|
134
|
+
return _force_disable_new(manager, info, pre_existing)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def uninstall_canvas_extension(name: str, installed_dir: Path | None = None) -> bool:
|
|
138
|
+
"""Uninstall a canvas extension by name.
|
|
139
|
+
|
|
140
|
+
Also drops any staged, not-yet-applied update for *name*, so refreshing
|
|
141
|
+
a since-uninstalled name never leaves an orphaned staging slot behind.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
True if uninstalled, False if it wasn't installed.
|
|
145
|
+
"""
|
|
146
|
+
installed_dir = _resolve_installed_dir(installed_dir)
|
|
147
|
+
uninstalled = _manager(installed_dir).uninstall(name)
|
|
148
|
+
if uninstalled:
|
|
149
|
+
shutil.rmtree(_staging_root(installed_dir) / name, ignore_errors=True)
|
|
150
|
+
return uninstalled
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def enable_canvas_extension(name: str, installed_dir: Path | None = None) -> bool:
|
|
154
|
+
"""Enable an installed canvas extension by name."""
|
|
155
|
+
return _manager(_resolve_installed_dir(installed_dir)).enable(name)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def disable_canvas_extension(name: str, installed_dir: Path | None = None) -> bool:
|
|
159
|
+
"""Disable an installed canvas extension by name."""
|
|
160
|
+
return _manager(_resolve_installed_dir(installed_dir)).disable(name)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def list_installed_canvas_extensions(
|
|
164
|
+
installed_dir: Path | None = None,
|
|
165
|
+
) -> list[InstalledCanvasExtensionInfo]:
|
|
166
|
+
"""List all installed canvas extensions.
|
|
167
|
+
|
|
168
|
+
Self-healing like ``InstallationManager.list_installed()``; directories
|
|
169
|
+
discovered this way also land disabled.
|
|
170
|
+
"""
|
|
171
|
+
installed_dir = _resolve_installed_dir(installed_dir)
|
|
172
|
+
manager = _manager(installed_dir)
|
|
173
|
+
pre_existing = _tracked_names(installed_dir)
|
|
174
|
+
infos = manager.list_installed()
|
|
175
|
+
return [_force_disable_new(manager, info, pre_existing) for info in infos]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def load_installed_canvas_extensions(
|
|
179
|
+
installed_dir: Path | None = None,
|
|
180
|
+
) -> list[CanvasExtensionManifest]:
|
|
181
|
+
"""Load all enabled canvas extensions' manifests.
|
|
182
|
+
|
|
183
|
+
Runs through ``list_installed_canvas_extensions`` first so discovery
|
|
184
|
+
is force-disabled before anything is loaded -- see the module
|
|
185
|
+
docstring. Mirrors ``InstallationManager.load_installed()``'s own
|
|
186
|
+
enabled-filter/load logic on top of the corrected info list.
|
|
187
|
+
"""
|
|
188
|
+
installed_dir = _resolve_installed_dir(installed_dir)
|
|
189
|
+
manager = _manager(installed_dir)
|
|
190
|
+
manifests: list[CanvasExtensionManifest] = []
|
|
191
|
+
for info in list_installed_canvas_extensions(installed_dir):
|
|
192
|
+
if not info.enabled:
|
|
193
|
+
continue
|
|
194
|
+
extension_path = installed_dir / info.name
|
|
195
|
+
if extension_path.exists():
|
|
196
|
+
manifests.append(
|
|
197
|
+
manager.installation_interface.load_from_dir(extension_path)
|
|
198
|
+
)
|
|
199
|
+
return manifests
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def get_installed_canvas_extension(
|
|
203
|
+
name: str, installed_dir: Path | None = None
|
|
204
|
+
) -> InstalledCanvasExtensionInfo | None:
|
|
205
|
+
"""Get information about a specific installed canvas extension."""
|
|
206
|
+
return _manager(_resolve_installed_dir(installed_dir)).get(name)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class CanvasExtensionUpdateCheck(BaseModel):
|
|
210
|
+
"""Result of ``check_canvas_extension_update``.
|
|
211
|
+
|
|
212
|
+
The active install is untouched; only ``.staging/`` is written. Pass
|
|
213
|
+
``resolved_ref`` back to ``apply_canvas_extension_update`` to confirm
|
|
214
|
+
and swap this exact staged content into place.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
requested_ref: str | None = Field(
|
|
218
|
+
description="Ref the staged fetch was resolved against"
|
|
219
|
+
)
|
|
220
|
+
resolved_ref: str | None = Field(
|
|
221
|
+
description="Commit SHA the staged content was resolved to"
|
|
222
|
+
)
|
|
223
|
+
validated: bool = Field(
|
|
224
|
+
description="Whether staged content passed validation; only True may be applied"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _staging_root(installed_dir: Path) -> Path:
|
|
229
|
+
return installed_dir / ".staging"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _staged_path(installed_dir: Path, name: str, resolved_ref: str | None) -> Path:
|
|
233
|
+
"""Path to the staging slot for (*name*, *resolved_ref*).
|
|
234
|
+
|
|
235
|
+
*resolved_ref* is untrusted (``apply_canvas_extension_update`` takes it
|
|
236
|
+
as a caller-supplied argument), so it's rejected if it could escape the
|
|
237
|
+
staging directory -- same check as ``entrypoint`` in manifest.py.
|
|
238
|
+
"""
|
|
239
|
+
if resolved_ref is not None and (
|
|
240
|
+
not resolved_ref
|
|
241
|
+
or resolved_ref.startswith("/")
|
|
242
|
+
or ".." in Path(resolved_ref).parts
|
|
243
|
+
):
|
|
244
|
+
raise ValueError(f"Invalid resolved_ref: {resolved_ref!r}")
|
|
245
|
+
return _staging_root(installed_dir) / name / (resolved_ref or "local")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _stage_fetched_content(
|
|
249
|
+
installed_dir: Path, name: str, resolved_ref: str | None, fetched_path: Path
|
|
250
|
+
) -> Path:
|
|
251
|
+
"""Copy fetched content into a clean staging slot for *name*.
|
|
252
|
+
|
|
253
|
+
Clears any previously staged candidate first -- at most one is kept
|
|
254
|
+
per extension at a time.
|
|
255
|
+
"""
|
|
256
|
+
slot_root = _staging_root(installed_dir) / name
|
|
257
|
+
shutil.rmtree(slot_root, ignore_errors=True)
|
|
258
|
+
staged_path = _staged_path(installed_dir, name, resolved_ref)
|
|
259
|
+
# symlinks=True: dereferencing here would copy a malicious symlink's
|
|
260
|
+
# target in as a plain file, defeating containment validation below.
|
|
261
|
+
shutil.copytree(fetched_path, staged_path, symlinks=True)
|
|
262
|
+
return staged_path
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _load_validated_manifest(
|
|
266
|
+
staged_path: Path, expected_name: str
|
|
267
|
+
) -> CanvasExtensionManifest:
|
|
268
|
+
"""Load and validate a staged extension's manifest.
|
|
269
|
+
|
|
270
|
+
Raises if the manifest is malformed, its entrypoint escapes the package
|
|
271
|
+
root, or its name no longer matches *expected_name*.
|
|
272
|
+
"""
|
|
273
|
+
manifest = CanvasExtensionInstallationInterface.load_from_dir(staged_path)
|
|
274
|
+
if manifest.name != expected_name:
|
|
275
|
+
raise ValueError(
|
|
276
|
+
f"Staged content for {expected_name!r} declares a different "
|
|
277
|
+
f"name {manifest.name!r}; refusing to treat it as an update"
|
|
278
|
+
)
|
|
279
|
+
return manifest
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def check_canvas_extension_update(
|
|
283
|
+
name: str, installed_dir: Path | None = None
|
|
284
|
+
) -> CanvasExtensionUpdateCheck | None:
|
|
285
|
+
"""Check for an update to *name*, staging and validating it.
|
|
286
|
+
|
|
287
|
+
Re-fetches the tracked source at its original ref (never "latest")
|
|
288
|
+
into ``.staging/``; the active install is untouched. See
|
|
289
|
+
``apply_canvas_extension_update`` for the step that swaps it into
|
|
290
|
+
place.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
name: Name of the installed extension to check.
|
|
294
|
+
installed_dir: Defaults to ``~/.openhands/canvas-extensions/installed/``.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
None if not installed, else a result -- only apply when ``validated``.
|
|
298
|
+
|
|
299
|
+
Raises:
|
|
300
|
+
ValueError: If *name* is not valid kebab-case.
|
|
301
|
+
ExtensionFetchError: If fetching the tracked source fails.
|
|
302
|
+
"""
|
|
303
|
+
installed_dir = _resolve_installed_dir(installed_dir)
|
|
304
|
+
manager = _manager(installed_dir)
|
|
305
|
+
current_info = manager.get(name)
|
|
306
|
+
if current_info is None:
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
fetched_path, resolved_ref = fetch_with_resolution(
|
|
310
|
+
source=current_info.source,
|
|
311
|
+
cache_dir=DEFAULT_CACHE_DIR,
|
|
312
|
+
ref=current_info.requested_ref,
|
|
313
|
+
repo_path=current_info.repo_path,
|
|
314
|
+
update=True,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
staged_path = _stage_fetched_content(
|
|
318
|
+
installed_dir, name, resolved_ref, fetched_path
|
|
319
|
+
)
|
|
320
|
+
try:
|
|
321
|
+
_load_validated_manifest(staged_path, name)
|
|
322
|
+
validated = True
|
|
323
|
+
except (ValidationError, ValueError, OSError):
|
|
324
|
+
validated = False
|
|
325
|
+
shutil.rmtree(staged_path.parent, ignore_errors=True)
|
|
326
|
+
|
|
327
|
+
return CanvasExtensionUpdateCheck(
|
|
328
|
+
requested_ref=current_info.requested_ref,
|
|
329
|
+
resolved_ref=resolved_ref,
|
|
330
|
+
validated=validated,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def apply_canvas_extension_update(
|
|
335
|
+
name: str,
|
|
336
|
+
resolved_ref: str | None,
|
|
337
|
+
enabled: bool,
|
|
338
|
+
installed_dir: Path | None = None,
|
|
339
|
+
) -> InstalledCanvasExtensionInfo | None:
|
|
340
|
+
"""Apply a previously checked and validated update.
|
|
341
|
+
|
|
342
|
+
*resolved_ref* must match a staged candidate already validated by
|
|
343
|
+
``check_canvas_extension_update``, reconfirming what's being applied.
|
|
344
|
+
Atomically swaps staged content into the active install path;
|
|
345
|
+
*enabled* is always applied explicitly, never inherited.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
name: Name of the installed extension to update.
|
|
349
|
+
resolved_ref: The ``resolved_ref`` from a prior, validated check.
|
|
350
|
+
enabled: Enabled state to apply to the new bundle.
|
|
351
|
+
installed_dir: Defaults to ``~/.openhands/canvas-extensions/installed/``.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
None if not installed, else the InstallationInfo for the new bundle.
|
|
355
|
+
|
|
356
|
+
Raises:
|
|
357
|
+
ValueError: If *name* is invalid, *resolved_ref* could escape the
|
|
358
|
+
staging directory, or no validated staged candidate matches it
|
|
359
|
+
-- call check first.
|
|
360
|
+
"""
|
|
361
|
+
installed_dir = _resolve_installed_dir(installed_dir)
|
|
362
|
+
manager = _manager(installed_dir)
|
|
363
|
+
current_info = manager.get(name)
|
|
364
|
+
if current_info is None:
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
staged_path = _staged_path(installed_dir, name, resolved_ref)
|
|
368
|
+
if not staged_path.is_dir():
|
|
369
|
+
raise ValueError(
|
|
370
|
+
f"No validated staged update found for {name!r} at ref "
|
|
371
|
+
f"{resolved_ref!r}. Call check_canvas_extension_update() first."
|
|
372
|
+
)
|
|
373
|
+
manifest = _load_validated_manifest(staged_path, name)
|
|
374
|
+
|
|
375
|
+
install_path = installed_dir / name
|
|
376
|
+
backup_path = _staging_root(installed_dir) / f"{name}.previous"
|
|
377
|
+
shutil.rmtree(backup_path, ignore_errors=True)
|
|
378
|
+
|
|
379
|
+
# POSIX rename() can't atomically replace a non-empty directory, so the
|
|
380
|
+
# active bundle moves aside first; roll back if the second rename fails.
|
|
381
|
+
os.replace(install_path, backup_path)
|
|
382
|
+
try:
|
|
383
|
+
os.replace(staged_path, install_path)
|
|
384
|
+
except OSError:
|
|
385
|
+
os.replace(backup_path, install_path)
|
|
386
|
+
raise
|
|
387
|
+
shutil.rmtree(backup_path, ignore_errors=True)
|
|
388
|
+
shutil.rmtree(_staging_root(installed_dir) / name, ignore_errors=True)
|
|
389
|
+
|
|
390
|
+
info = InstallationInfo.from_extension(
|
|
391
|
+
manifest,
|
|
392
|
+
source=current_info.source,
|
|
393
|
+
install_path=install_path,
|
|
394
|
+
requested_ref=current_info.requested_ref,
|
|
395
|
+
resolved_ref=resolved_ref,
|
|
396
|
+
repo_path=current_info.repo_path,
|
|
397
|
+
)
|
|
398
|
+
info.enabled = enabled
|
|
399
|
+
|
|
400
|
+
with manager.metadata_session as session:
|
|
401
|
+
session.extensions[name] = info
|
|
402
|
+
|
|
403
|
+
return info
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Canvas Extensions manifest: schema, validation, and entrypoint containment.
|
|
2
|
+
|
|
3
|
+
A Canvas extension is an installable UI bundle that contributes pages to the
|
|
4
|
+
OpenHands Canvas frontend. Extensions are installed and served entirely by
|
|
5
|
+
the agent-server (via ``openhands.sdk.extensions.installation``, the same
|
|
6
|
+
type-agnostic install-tracking framework Plugins/Skills use); nothing here
|
|
7
|
+
is consumed by ``Agent``/``Conversation``.
|
|
8
|
+
|
|
9
|
+
This module defines the manifest schema (``canvas-extension.json``) and the
|
|
10
|
+
two security-critical checks around it:
|
|
11
|
+
|
|
12
|
+
* Name / contribution-id / page-path validation (syntactic, on the model).
|
|
13
|
+
* Entrypoint containment (filesystem-level, once a package root is known) —
|
|
14
|
+
rejects both textual path traversal and symlink escapes.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Final
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel, Field, field_validator
|
|
22
|
+
|
|
23
|
+
from openhands.sdk.extensions.installation.utils import validate_extension_name
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Filename a canvas extension's manifest is loaded from, at its package root.
|
|
27
|
+
MANIFEST_FILENAME: Final[str] = "canvas-extension.json"
|
|
28
|
+
|
|
29
|
+
# Absolute, kebab-case, multi-segment UI route, e.g. "/dashboard/settings".
|
|
30
|
+
_PAGE_PATH_PATTERN: re.Pattern[str] = re.compile(
|
|
31
|
+
r"^/[a-z0-9]+(?:-[a-z0-9]+)*(?:/[a-z0-9]+(?:-[a-z0-9]+)*)*$"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CanvasExtensionPage(BaseModel):
|
|
36
|
+
"""A single page contributed to the Canvas UI by an extension."""
|
|
37
|
+
|
|
38
|
+
id: str = Field(description="Unique contribution id within the extension")
|
|
39
|
+
title: str = Field(description="Page title shown in Canvas navigation")
|
|
40
|
+
path: str = Field(description="Route the page is mounted at, e.g. '/dashboard'")
|
|
41
|
+
|
|
42
|
+
@field_validator("id")
|
|
43
|
+
@classmethod
|
|
44
|
+
def _validate_id(cls, v: str) -> str:
|
|
45
|
+
try:
|
|
46
|
+
validate_extension_name(v)
|
|
47
|
+
except ValueError as e:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"Invalid contribution id. Expected kebab-case, got {v!r}."
|
|
50
|
+
) from e
|
|
51
|
+
return v
|
|
52
|
+
|
|
53
|
+
@field_validator("path")
|
|
54
|
+
@classmethod
|
|
55
|
+
def _validate_path(cls, v: str) -> str:
|
|
56
|
+
if not _PAGE_PATH_PATTERN.fullmatch(v):
|
|
57
|
+
raise ValueError(
|
|
58
|
+
"Invalid page path. Expected an absolute kebab-case route "
|
|
59
|
+
f"(e.g. '/dashboard'), got {v!r}."
|
|
60
|
+
)
|
|
61
|
+
return v
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CanvasExtensionContributes(BaseModel):
|
|
65
|
+
"""Contributions an extension makes to the Canvas UI."""
|
|
66
|
+
|
|
67
|
+
pages: list[CanvasExtensionPage] = Field(
|
|
68
|
+
default_factory=list, description="Pages contributed to Canvas navigation"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@field_validator("pages")
|
|
72
|
+
@classmethod
|
|
73
|
+
def _validate_unique_pages(
|
|
74
|
+
cls, v: list[CanvasExtensionPage]
|
|
75
|
+
) -> list[CanvasExtensionPage]:
|
|
76
|
+
seen_ids: set[str] = set()
|
|
77
|
+
seen_paths: set[str] = set()
|
|
78
|
+
for page in v:
|
|
79
|
+
if page.id in seen_ids:
|
|
80
|
+
raise ValueError(f"Duplicate page contribution id: {page.id!r}")
|
|
81
|
+
if page.path in seen_paths:
|
|
82
|
+
raise ValueError(f"Duplicate page path: {page.path!r}")
|
|
83
|
+
seen_ids.add(page.id)
|
|
84
|
+
seen_paths.add(page.path)
|
|
85
|
+
return v
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class CanvasExtensionManifest(BaseModel):
|
|
89
|
+
"""Canvas extension manifest (``canvas-extension.json``)."""
|
|
90
|
+
|
|
91
|
+
schema_version: int = Field(description="Manifest schema version")
|
|
92
|
+
name: str = Field(description="Extension name (kebab-case)")
|
|
93
|
+
display_name: str = Field(description="Human-readable extension name")
|
|
94
|
+
version: str = Field(description="Extension version")
|
|
95
|
+
description: str = Field(default="", description="Extension description")
|
|
96
|
+
entrypoint: str = Field(
|
|
97
|
+
description=(
|
|
98
|
+
"Path, relative to the extension package root, to the bundle entry file"
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
contributes: CanvasExtensionContributes = Field(
|
|
102
|
+
default_factory=CanvasExtensionContributes,
|
|
103
|
+
description="Contributions this extension makes to the Canvas UI",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@field_validator("name")
|
|
107
|
+
@classmethod
|
|
108
|
+
def _validate_name(cls, v: str) -> str:
|
|
109
|
+
validate_extension_name(v)
|
|
110
|
+
return v
|
|
111
|
+
|
|
112
|
+
@field_validator("entrypoint")
|
|
113
|
+
@classmethod
|
|
114
|
+
def _validate_entrypoint(cls, v: str) -> str:
|
|
115
|
+
"""Reject textual traversal/absolute paths.
|
|
116
|
+
|
|
117
|
+
Syntactic only — see :func:`resolve_entrypoint` for the real,
|
|
118
|
+
symlink-aware containment check against the installed package root.
|
|
119
|
+
"""
|
|
120
|
+
if not v:
|
|
121
|
+
raise ValueError("entrypoint must not be empty")
|
|
122
|
+
if v.startswith("/"):
|
|
123
|
+
raise ValueError("entrypoint must be relative, not absolute")
|
|
124
|
+
if ".." in Path(v).parts:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
"entrypoint cannot contain '..' (parent directory traversal)"
|
|
127
|
+
)
|
|
128
|
+
return v
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def resolve_entrypoint(manifest: CanvasExtensionManifest, package_root: Path) -> Path:
|
|
132
|
+
"""Resolve ``manifest.entrypoint`` against ``package_root``, safely.
|
|
133
|
+
|
|
134
|
+
Field-level validation on ``entrypoint`` only rejects textual traversal
|
|
135
|
+
(``..``) and absolute paths. It cannot catch a symlink inside the
|
|
136
|
+
package that resolves outside of it. This performs the real
|
|
137
|
+
filesystem-level containment check (resolving symlinks) and must be
|
|
138
|
+
called both when an extension is installed and again immediately
|
|
139
|
+
before its entrypoint is read or served over HTTP.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
manifest: A validated manifest.
|
|
143
|
+
package_root: The extension's installed package root directory.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
The resolved, contained entrypoint path.
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
ValueError: If the resolved entrypoint escapes ``package_root``, or
|
|
150
|
+
does not resolve to a regular file within it (covers ``.``,
|
|
151
|
+
a directory, a dangling symlink, and symlink cycles — none of
|
|
152
|
+
which ``is_relative_to`` alone rejects).
|
|
153
|
+
"""
|
|
154
|
+
root = package_root.resolve()
|
|
155
|
+
candidate = (root / manifest.entrypoint).resolve()
|
|
156
|
+
if not candidate.is_relative_to(root):
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"entrypoint {manifest.entrypoint!r} resolves outside the "
|
|
159
|
+
"extension package root"
|
|
160
|
+
)
|
|
161
|
+
if not candidate.is_file():
|
|
162
|
+
raise ValueError(
|
|
163
|
+
f"entrypoint {manifest.entrypoint!r} does not resolve to a file "
|
|
164
|
+
"in the extension package"
|
|
165
|
+
)
|
|
166
|
+
return candidate
|
{openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/mcp_router.py
RENAMED
|
@@ -53,7 +53,6 @@ from openhands.sdk.mcp.config import (
|
|
|
53
53
|
)
|
|
54
54
|
from openhands.sdk.mcp.exceptions import MCPError, MCPTimeoutError
|
|
55
55
|
from openhands.sdk.utils.cipher import Cipher
|
|
56
|
-
from openhands.sdk.utils.deprecation import warn_deprecated
|
|
57
56
|
|
|
58
57
|
|
|
59
58
|
logger = get_logger(__name__)
|
|
@@ -100,44 +99,13 @@ class _RemoteMCPServerSpec(BaseModel):
|
|
|
100
99
|
type: Literal["http", "shttp", "streamable-http", "sse"]
|
|
101
100
|
url: str = Field(..., min_length=1)
|
|
102
101
|
headers: dict[str, str] = Field(default_factory=dict)
|
|
103
|
-
api_key: str | None = Field(
|
|
104
|
-
default=None,
|
|
105
|
-
deprecated=True,
|
|
106
|
-
description=(
|
|
107
|
-
"Deprecated bearer token. Prefer auth.strategy='bearer'. If provided "
|
|
108
|
-
"without auth, sent as 'Authorization: Bearer <token>'."
|
|
109
|
-
),
|
|
110
|
-
)
|
|
111
102
|
auth: MCPAuthCredential | None = None
|
|
112
103
|
timeout: float | None = None
|
|
113
104
|
sse_read_timeout: float | None = None
|
|
114
105
|
keep_alive: bool | None = None
|
|
115
106
|
|
|
116
|
-
@model_validator(mode="before")
|
|
117
|
-
@classmethod
|
|
118
|
-
def _warn_legacy_api_key(cls, value: object) -> object:
|
|
119
|
-
if isinstance(value, dict) and value.get("api_key") is not None:
|
|
120
|
-
warn_deprecated(
|
|
121
|
-
"_RemoteMCPServerSpec.api_key",
|
|
122
|
-
deprecated_in="1.36.0",
|
|
123
|
-
removed_in="1.41.0",
|
|
124
|
-
details="Use auth.strategy='bearer' with auth.value instead.",
|
|
125
|
-
stacklevel=3,
|
|
126
|
-
)
|
|
127
|
-
return value
|
|
128
|
-
|
|
129
107
|
@model_validator(mode="after")
|
|
130
108
|
def _reject_ambiguous_auth(self) -> _RemoteMCPServerSpec:
|
|
131
|
-
api_key = self.__dict__.get("api_key")
|
|
132
|
-
if api_key is not None and self.auth is not None:
|
|
133
|
-
raise ValueError("api_key cannot be combined with auth.")
|
|
134
|
-
if api_key is not None and any(
|
|
135
|
-
name.lower() == "authorization" for name in self.headers
|
|
136
|
-
):
|
|
137
|
-
raise ValueError(
|
|
138
|
-
"api_key cannot be combined with an explicit top-level "
|
|
139
|
-
"'Authorization' header; use auth.strategy='header' instead."
|
|
140
|
-
)
|
|
141
109
|
if self.auth is not None and any(
|
|
142
110
|
name.lower() == "authorization" for name in self.headers
|
|
143
111
|
):
|
|
@@ -149,7 +117,6 @@ class _RemoteMCPServerSpec(BaseModel):
|
|
|
149
117
|
|
|
150
118
|
def to_mcp_server(self) -> MCPServer:
|
|
151
119
|
transport = "http" if self.type == "shttp" else self.type
|
|
152
|
-
api_key = self.__dict__.get("api_key")
|
|
153
120
|
data: dict[str, Any] = {
|
|
154
121
|
"url": self.url,
|
|
155
122
|
"transport": transport,
|
|
@@ -160,8 +127,6 @@ class _RemoteMCPServerSpec(BaseModel):
|
|
|
160
127
|
}
|
|
161
128
|
if self.auth is not None:
|
|
162
129
|
data["auth"] = self.auth
|
|
163
|
-
elif api_key is not None:
|
|
164
|
-
data["auth"] = {"strategy": "bearer", "value": api_key}
|
|
165
130
|
return MCPServer.model_validate(data)
|
|
166
131
|
|
|
167
132
|
|
{openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/models.py
RENAMED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from abc import ABC
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from enum import Enum, StrEnum
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import Any
|
|
7
7
|
from uuid import UUID, uuid4
|
|
8
8
|
|
|
9
9
|
from pydantic import BaseModel, Field, field_validator
|
|
@@ -14,7 +14,6 @@ from openhands.sdk.conversation.conversation_stats import ConversationStats
|
|
|
14
14
|
from openhands.sdk.conversation.request import ( # re-export for backward compat
|
|
15
15
|
ACPEnabledAgent as ACPEnabledAgent,
|
|
16
16
|
SendMessageRequest as SendMessageRequest,
|
|
17
|
-
StartACPConversationRequest as StartACPConversationRequest,
|
|
18
17
|
StartConversationRequest as StartConversationRequest,
|
|
19
18
|
)
|
|
20
19
|
from openhands.sdk.conversation.secret_registry import SecretRegistry
|
|
@@ -38,7 +37,6 @@ from openhands.sdk.security.confirmation_policy import (
|
|
|
38
37
|
)
|
|
39
38
|
from openhands.sdk.tool.client_tool import ClientToolSpec
|
|
40
39
|
from openhands.sdk.utils import OpenHandsUUID, utc_now
|
|
41
|
-
from openhands.sdk.utils.deprecation import warn_deprecated
|
|
42
40
|
from openhands.sdk.utils.models import (
|
|
43
41
|
DiscriminatedUnionMixin,
|
|
44
42
|
OpenHandsModel,
|
|
@@ -392,36 +390,6 @@ def trim_conversation_response_skills(info: ConversationInfo) -> ConversationInf
|
|
|
392
390
|
return info.model_copy(update={"agent": trimmed_agent})
|
|
393
391
|
|
|
394
392
|
|
|
395
|
-
# Deprecated compatibility aliases for the old ACP-specific response names.
|
|
396
|
-
# Keep runtime assignment aliases so existing imports still resolve to the
|
|
397
|
-
# canonical Pydantic models; PEP 695 ``type`` aliases would not preserve that.
|
|
398
|
-
if TYPE_CHECKING:
|
|
399
|
-
ACPConversationInfo: TypeAlias = ConversationInfo # noqa: UP040
|
|
400
|
-
ACPConversationPage: TypeAlias = ConversationPage # noqa: UP040
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
_DEPRECATED_ACP_RESPONSE_ALIASES: dict[str, type[BaseModel]] = {
|
|
404
|
-
"ACPConversationInfo": ConversationInfo,
|
|
405
|
-
"ACPConversationPage": ConversationPage,
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
def __getattr__(name: str) -> Any:
|
|
410
|
-
if name in _DEPRECATED_ACP_RESPONSE_ALIASES:
|
|
411
|
-
warn_deprecated(
|
|
412
|
-
f"openhands.agent_server.models.{name}",
|
|
413
|
-
deprecated_in="1.36.0",
|
|
414
|
-
removed_in="1.41.0",
|
|
415
|
-
details=(
|
|
416
|
-
"The ACP-specific response model names are compatibility aliases. "
|
|
417
|
-
"Use ConversationInfo or ConversationPage instead."
|
|
418
|
-
),
|
|
419
|
-
stacklevel=2,
|
|
420
|
-
)
|
|
421
|
-
return _DEPRECATED_ACP_RESPONSE_ALIASES[name]
|
|
422
|
-
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
423
|
-
|
|
424
|
-
|
|
425
393
|
class ConfirmationResponseRequest(BaseModel):
|
|
426
394
|
"""Payload to accept or reject a pending action."""
|
|
427
395
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openhands-agent-server
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.41.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
|
|
@@ -49,6 +49,9 @@ pyproject.toml
|
|
|
49
49
|
./openhands/agent_server/vscode_service.py
|
|
50
50
|
./openhands/agent_server/workspace_router.py
|
|
51
51
|
./openhands/agent_server/workspaces_router.py
|
|
52
|
+
./openhands/agent_server/canvas_extensions/__init__.py
|
|
53
|
+
./openhands/agent_server/canvas_extensions/installed.py
|
|
54
|
+
./openhands/agent_server/canvas_extensions/manifest.py
|
|
52
55
|
./openhands/agent_server/docker/Dockerfile
|
|
53
56
|
./openhands/agent_server/docker/build.py
|
|
54
57
|
./openhands/agent_server/docker/wallpaper.svg
|
|
@@ -121,6 +124,9 @@ openhands/agent_server/vscode_router.py
|
|
|
121
124
|
openhands/agent_server/vscode_service.py
|
|
122
125
|
openhands/agent_server/workspace_router.py
|
|
123
126
|
openhands/agent_server/workspaces_router.py
|
|
127
|
+
openhands/agent_server/canvas_extensions/__init__.py
|
|
128
|
+
openhands/agent_server/canvas_extensions/installed.py
|
|
129
|
+
openhands/agent_server/canvas_extensions/manifest.py
|
|
124
130
|
openhands/agent_server/docker/Dockerfile
|
|
125
131
|
openhands/agent_server/docker/build.py
|
|
126
132
|
openhands/agent_server/docker/wallpaper.svg
|
{openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/__init__.py
RENAMED
|
File without changes
|
{openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/api.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/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
|
|
File without changes
|
{openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/env_parser.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/git_router.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/llm_router.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/openapi.py
RENAMED
|
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.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/pub_sub.py
RENAMED
|
File without changes
|
{openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/openhands/agent_server/sockets.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
|
|
File without changes
|
{openhands_agent_server-1.40.1 → openhands_agent_server-1.41.0}/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
|