openhands-agent-server 1.34.0__tar.gz → 1.36.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.34.0 → openhands_agent_server-1.36.0}/PKG-INFO +1 -1
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/docker/Dockerfile +1 -1
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/file_router.py +23 -5
- openhands_agent_server-1.36.0/openhands/agent_server/git_router.py +181 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/llm_router.py +3 -2
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/skills_router.py +2 -2
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/skills_service.py +2 -32
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands_agent_server.egg-info/PKG-INFO +1 -1
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/pyproject.toml +1 -1
- openhands_agent_server-1.34.0/openhands/agent_server/git_router.py +0 -88
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/__init__.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/__main__.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/_secrets_exposure.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/agent_profiles_router.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/api.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/auth_router.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/bash_router.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/bash_service.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/config.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/conversation_lease.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/conversation_router.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/conversation_service.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/dependencies.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/desktop_router.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/desktop_service.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/docker/build.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/docker/wallpaper.svg +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/env_parser.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/event_compat.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/event_router.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/event_service.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/hooks_router.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/hooks_service.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/init_router.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/logging_config.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/mcp_oauth_store.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/mcp_router.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/middleware.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/models.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/openai/__init__.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/openai/models.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/openai/router.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/openai/service.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/openapi.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/persistence/__init__.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/persistence/models.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/persistence/store.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/plugins_router.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/plugins_service.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/profiles_router.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/pub_sub.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/py.typed +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/server_details_router.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/settings_router.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/sockets.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/sub_agents_router.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/tool_preload_service.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/tool_router.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/utils.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/vscode_router.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/vscode_service.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/workspace_router.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/workspaces_router.py +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands_agent_server.egg-info/SOURCES.txt +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands_agent_server.egg-info/entry_points.txt +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands_agent_server.egg-info/requires.txt +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands_agent_server.egg-info/top_level.txt +0 -0
- {openhands_agent_server-1.34.0 → openhands_agent_server-1.36.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.36.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
|
|
@@ -181,7 +181,7 @@ RUN set -ux; \
|
|
|
181
181
|
PATH="$ACP_NODE_DIR/bin:$PATH"; \
|
|
182
182
|
if "$ACP_NODE_DIR/bin/npm" install -g \
|
|
183
183
|
@agentclientprotocol/claude-agent-acp@0.44.0 \
|
|
184
|
-
@
|
|
184
|
+
@agentclientprotocol/codex-acp@1.1.2 \
|
|
185
185
|
@google/gemini-cli@0.46.0; then \
|
|
186
186
|
# Create wrappers in /usr/local/bin that prepend ACP's Node 22 to PATH.
|
|
187
187
|
# This ensures the ACP binary's #!/usr/bin/env node shebang resolves
|
|
@@ -10,6 +10,7 @@ import tempfile
|
|
|
10
10
|
import zipfile
|
|
11
11
|
from pathlib import Path, PurePosixPath
|
|
12
12
|
from typing import IO, Annotated, Literal
|
|
13
|
+
from urllib.parse import quote
|
|
13
14
|
from uuid import UUID
|
|
14
15
|
|
|
15
16
|
from fastapi import (
|
|
@@ -30,6 +31,7 @@ from openhands.agent_server.server_details_router import update_last_execution_t
|
|
|
30
31
|
from openhands.sdk.git.exceptions import GitCommandError, GitRepositoryError
|
|
31
32
|
from openhands.sdk.git.utils import (
|
|
32
33
|
GIT_EMPTY_TREE_HASH,
|
|
34
|
+
get_git_repository_metadata,
|
|
33
35
|
get_valid_ref,
|
|
34
36
|
run_git_command,
|
|
35
37
|
validate_git_repository,
|
|
@@ -323,6 +325,11 @@ def _head_is_detached(root: Path) -> bool:
|
|
|
323
325
|
return branch.strip() == "HEAD"
|
|
324
326
|
|
|
325
327
|
|
|
328
|
+
def _header_safe(value: str) -> str:
|
|
329
|
+
"""Percent-encode a header value."""
|
|
330
|
+
return quote(value, safe="")
|
|
331
|
+
|
|
332
|
+
|
|
326
333
|
def _create_git_delta(
|
|
327
334
|
root: Path, base_ref: str | None, output_path: Path, excludes: list[str]
|
|
328
335
|
) -> str:
|
|
@@ -972,13 +979,24 @@ async def archive_directory(
|
|
|
972
979
|
detail=f"Failed to archive directory: {str(e)}",
|
|
973
980
|
)
|
|
974
981
|
|
|
975
|
-
|
|
982
|
+
# Repo identity (remote / branch / HEAD) applies to both formats so a
|
|
983
|
+
# tar.gz snapshot is self-describing too; base_commit/base_ref are
|
|
984
|
+
# git-delta-only (they describe the patch's replay base).
|
|
985
|
+
headers: dict[str, str] = {}
|
|
986
|
+
if (repo_root / ".git").exists():
|
|
987
|
+
repo_metadata = await asyncio.to_thread(get_git_repository_metadata, repo_root)
|
|
988
|
+
for key, header in (
|
|
989
|
+
("repo_remote", "X-Archive-Repo-Remote"),
|
|
990
|
+
("branch", "X-Archive-Branch"),
|
|
991
|
+
("head_commit", "X-Archive-Head-Commit"),
|
|
992
|
+
):
|
|
993
|
+
if value := repo_metadata.get(key):
|
|
994
|
+
headers[header] = _header_safe(value)
|
|
995
|
+
headers["X-Archive-Repo-Root"] = _header_safe(str(repo_root))
|
|
976
996
|
if base_commit:
|
|
977
997
|
# Make a git-delta self-describing so consumers can replay the patch.
|
|
978
|
-
headers =
|
|
979
|
-
|
|
980
|
-
"X-Archive-Base-Ref": base_ref or "auto",
|
|
981
|
-
}
|
|
998
|
+
headers["X-Archive-Base-Commit"] = base_commit
|
|
999
|
+
headers["X-Archive-Base-Ref"] = base_ref or "auto"
|
|
982
1000
|
|
|
983
1001
|
return FileResponse(
|
|
984
1002
|
path=output_path,
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Git router for OpenHands SDK."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import functools
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter, HTTPException, Path as PathParam, Query
|
|
9
|
+
|
|
10
|
+
from openhands.agent_server.server_details_router import update_last_execution_time
|
|
11
|
+
from openhands.sdk.git.exceptions import GitError, GitRepositoryError
|
|
12
|
+
from openhands.sdk.git.git_changes import get_git_changes
|
|
13
|
+
from openhands.sdk.git.git_commits import (
|
|
14
|
+
get_commit_changes,
|
|
15
|
+
get_commit_file_diff,
|
|
16
|
+
get_git_commits,
|
|
17
|
+
)
|
|
18
|
+
from openhands.sdk.git.git_diff import get_git_diff
|
|
19
|
+
from openhands.sdk.git.models import GitChange, GitCommitsPage, GitDiff
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
git_router = APIRouter(prefix="/git", tags=["Git"])
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_REF_QUERY_DESCRIPTION = (
|
|
27
|
+
"Optional git ref to diff against (e.g. 'HEAD' for git status-style "
|
|
28
|
+
"changes, or a commit hash). When omitted, the upstream/default branch "
|
|
29
|
+
"is auto-detected."
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_COMMIT_QUERY_DESCRIPTION = (
|
|
33
|
+
"Optional commit SHA. When set, the diff is the change that commit "
|
|
34
|
+
"introduced (vs its first parent), read entirely from git objects so "
|
|
35
|
+
"deleted files still render. Mutually exclusive with 'ref'."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Hex-only so a path/query value can never reach git argv as an option
|
|
39
|
+
# (list-args protect against shell injection, not option injection).
|
|
40
|
+
_SHA_PATTERN = r"^[0-9a-fA-F]{4,64}$"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def _get_git_changes(path: str, ref: str | None) -> list[GitChange]:
|
|
44
|
+
"""Internal helper to get git changes for a given path."""
|
|
45
|
+
update_last_execution_time()
|
|
46
|
+
loop = asyncio.get_running_loop()
|
|
47
|
+
try:
|
|
48
|
+
return await loop.run_in_executor(
|
|
49
|
+
None, functools.partial(get_git_changes, Path(path), ref=ref)
|
|
50
|
+
)
|
|
51
|
+
except GitRepositoryError:
|
|
52
|
+
# A non-repo workspace has no git changes to report; respond with an
|
|
53
|
+
# empty list so the Changes tab can render normally instead of 500ing.
|
|
54
|
+
logger.debug("Path %s is not a git repository; returning no changes", path)
|
|
55
|
+
return []
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def _get_git_diff(path: str, ref: str | None) -> GitDiff:
|
|
59
|
+
"""Internal helper to get git diff for a given path."""
|
|
60
|
+
update_last_execution_time()
|
|
61
|
+
loop = asyncio.get_running_loop()
|
|
62
|
+
try:
|
|
63
|
+
return await loop.run_in_executor(
|
|
64
|
+
None, functools.partial(get_git_diff, Path(path), ref=ref)
|
|
65
|
+
)
|
|
66
|
+
except GitRepositoryError:
|
|
67
|
+
# Only collapse the not-a-repo case to an empty diff; file-level
|
|
68
|
+
# GitPathError (missing/oversize/outside-repo) stays a 500 so
|
|
69
|
+
# callers can distinguish it from "no changes".
|
|
70
|
+
logger.debug("Path %s is not in a git repository; returning empty diff", path)
|
|
71
|
+
return GitDiff(modified=None, original=None)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def _get_git_commits(path: str, limit: int) -> GitCommitsPage:
|
|
75
|
+
"""Internal helper to list commits for a given repo path."""
|
|
76
|
+
update_last_execution_time()
|
|
77
|
+
loop = asyncio.get_running_loop()
|
|
78
|
+
try:
|
|
79
|
+
return await loop.run_in_executor(
|
|
80
|
+
None, functools.partial(get_git_commits, Path(path), limit=limit)
|
|
81
|
+
)
|
|
82
|
+
except GitRepositoryError:
|
|
83
|
+
# A non-repo workspace has no commits to report; respond with an
|
|
84
|
+
# empty page so the commits section can render its empty state.
|
|
85
|
+
logger.debug("Path %s is not a git repository; returning no commits", path)
|
|
86
|
+
return GitCommitsPage(commits=[], has_more=False)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def _get_commit_changes(path: str, sha: str) -> list[GitChange]:
|
|
90
|
+
"""Internal helper to get the files changed by one commit."""
|
|
91
|
+
update_last_execution_time()
|
|
92
|
+
loop = asyncio.get_running_loop()
|
|
93
|
+
try:
|
|
94
|
+
return await loop.run_in_executor(
|
|
95
|
+
None, functools.partial(get_commit_changes, Path(path), sha)
|
|
96
|
+
)
|
|
97
|
+
except GitRepositoryError:
|
|
98
|
+
logger.debug("Path %s is not a git repository; returning no changes", path)
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def _get_commit_file_diff(path: str, commit: str) -> GitDiff:
|
|
103
|
+
"""Internal helper to get one file's diff as changed by one commit."""
|
|
104
|
+
update_last_execution_time()
|
|
105
|
+
loop = asyncio.get_running_loop()
|
|
106
|
+
try:
|
|
107
|
+
return await loop.run_in_executor(
|
|
108
|
+
None, functools.partial(get_commit_file_diff, Path(path), commit)
|
|
109
|
+
)
|
|
110
|
+
except GitRepositoryError:
|
|
111
|
+
logger.debug("Path %s is not in a git repository; returning empty diff", path)
|
|
112
|
+
return GitDiff(modified=None, original=None)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@git_router.get("/changes")
|
|
116
|
+
async def git_changes_query(
|
|
117
|
+
path: str = Query(..., description="The git repository path"),
|
|
118
|
+
ref: str | None = Query(None, description=_REF_QUERY_DESCRIPTION),
|
|
119
|
+
) -> list[GitChange]:
|
|
120
|
+
"""Get git changes using query parameter (preferred method)."""
|
|
121
|
+
try:
|
|
122
|
+
return await _get_git_changes(path, ref)
|
|
123
|
+
except GitError as e:
|
|
124
|
+
# GitRepositoryError is already handled in the helper (returns []).
|
|
125
|
+
# Any remaining GitError subclass (e.g. GitCommandError) surfaces as
|
|
126
|
+
# 400 so the client can show an actionable error instead of an
|
|
127
|
+
# opaque 500.
|
|
128
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@git_router.get("/diff")
|
|
132
|
+
async def git_diff_query(
|
|
133
|
+
path: str = Query(..., description="The file path to get diff for"),
|
|
134
|
+
ref: str | None = Query(None, description=_REF_QUERY_DESCRIPTION),
|
|
135
|
+
commit: str | None = Query(
|
|
136
|
+
None, pattern=_SHA_PATTERN, description=_COMMIT_QUERY_DESCRIPTION
|
|
137
|
+
),
|
|
138
|
+
) -> GitDiff:
|
|
139
|
+
"""Get git diff using query parameter (preferred method)."""
|
|
140
|
+
if ref is not None and commit is not None:
|
|
141
|
+
raise HTTPException(
|
|
142
|
+
status_code=400, detail="'ref' and 'commit' are mutually exclusive"
|
|
143
|
+
)
|
|
144
|
+
try:
|
|
145
|
+
if commit is not None:
|
|
146
|
+
return await _get_commit_file_diff(path, commit)
|
|
147
|
+
return await _get_git_diff(path, ref)
|
|
148
|
+
except GitError as e:
|
|
149
|
+
# GitRepositoryError is already handled in the helpers (returns an
|
|
150
|
+
# empty diff). Any remaining GitError subclass (e.g. GitCommandError,
|
|
151
|
+
# GitPathError) surfaces as 400 so the client can show an actionable
|
|
152
|
+
# error instead of an opaque 500.
|
|
153
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@git_router.get("/commits")
|
|
157
|
+
async def git_commits_query(
|
|
158
|
+
path: str = Query(..., description="The git repository path"),
|
|
159
|
+
limit: int = Query(50, ge=1, le=200, description="Maximum commits to return"),
|
|
160
|
+
) -> GitCommitsPage:
|
|
161
|
+
"""List the repository's most recent commits, newest first."""
|
|
162
|
+
try:
|
|
163
|
+
return await _get_git_commits(path, limit)
|
|
164
|
+
except GitError as e:
|
|
165
|
+
# GitRepositoryError is already handled in the helper (returns an
|
|
166
|
+
# empty page). Any remaining GitError subclass surfaces as 400.
|
|
167
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@git_router.get("/commits/{sha}/changes")
|
|
171
|
+
async def git_commit_changes_query(
|
|
172
|
+
sha: str = PathParam(..., pattern=_SHA_PATTERN, description="Commit SHA"),
|
|
173
|
+
path: str = Query(..., description="The git repository path"),
|
|
174
|
+
) -> list[GitChange]:
|
|
175
|
+
"""Get the files changed by a single commit (vs its first parent)."""
|
|
176
|
+
try:
|
|
177
|
+
return await _get_commit_changes(path, sha)
|
|
178
|
+
except GitError as e:
|
|
179
|
+
# GitRepositoryError is already handled in the helper (returns []).
|
|
180
|
+
# An unknown/unresolvable sha raises GitCommandError -> 400.
|
|
181
|
+
raise HTTPException(status_code=400, detail=str(e))
|
{openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/llm_router.py
RENAMED
|
@@ -138,9 +138,10 @@ async def list_models(
|
|
|
138
138
|
models = sorted(set(all_models))
|
|
139
139
|
else:
|
|
140
140
|
filtered_models = []
|
|
141
|
+
verified_provider_models = set(VERIFIED_MODELS.get(provider, ()))
|
|
141
142
|
for model in all_models:
|
|
142
|
-
model_provider,
|
|
143
|
-
if model_provider == provider:
|
|
143
|
+
model_provider, _, _ = _extract_model_and_provider(model)
|
|
144
|
+
if model_provider == provider or model in verified_provider_models:
|
|
144
145
|
filtered_models.append(model)
|
|
145
146
|
models = sorted(set(filtered_models))
|
|
146
147
|
|
|
@@ -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.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
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
"""Git router for OpenHands SDK."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import functools
|
|
5
|
-
import logging
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
from fastapi import APIRouter, HTTPException, Query
|
|
9
|
-
|
|
10
|
-
from openhands.agent_server.server_details_router import update_last_execution_time
|
|
11
|
-
from openhands.sdk.git.exceptions import GitError, GitRepositoryError
|
|
12
|
-
from openhands.sdk.git.git_changes import get_git_changes
|
|
13
|
-
from openhands.sdk.git.git_diff import get_git_diff
|
|
14
|
-
from openhands.sdk.git.models import GitChange, GitDiff
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
git_router = APIRouter(prefix="/git", tags=["Git"])
|
|
18
|
-
logger = logging.getLogger(__name__)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
_REF_QUERY_DESCRIPTION = (
|
|
22
|
-
"Optional git ref to diff against (e.g. 'HEAD' for git status-style "
|
|
23
|
-
"changes, or a commit hash). When omitted, the upstream/default branch "
|
|
24
|
-
"is auto-detected."
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
async def _get_git_changes(path: str, ref: str | None) -> list[GitChange]:
|
|
29
|
-
"""Internal helper to get git changes for a given path."""
|
|
30
|
-
update_last_execution_time()
|
|
31
|
-
loop = asyncio.get_running_loop()
|
|
32
|
-
try:
|
|
33
|
-
return await loop.run_in_executor(
|
|
34
|
-
None, functools.partial(get_git_changes, Path(path), ref=ref)
|
|
35
|
-
)
|
|
36
|
-
except GitRepositoryError:
|
|
37
|
-
# A non-repo workspace has no git changes to report; respond with an
|
|
38
|
-
# empty list so the Changes tab can render normally instead of 500ing.
|
|
39
|
-
logger.debug("Path %s is not a git repository; returning no changes", path)
|
|
40
|
-
return []
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
async def _get_git_diff(path: str, ref: str | None) -> GitDiff:
|
|
44
|
-
"""Internal helper to get git diff for a given path."""
|
|
45
|
-
update_last_execution_time()
|
|
46
|
-
loop = asyncio.get_running_loop()
|
|
47
|
-
try:
|
|
48
|
-
return await loop.run_in_executor(
|
|
49
|
-
None, functools.partial(get_git_diff, Path(path), ref=ref)
|
|
50
|
-
)
|
|
51
|
-
except GitRepositoryError:
|
|
52
|
-
# Only collapse the not-a-repo case to an empty diff; file-level
|
|
53
|
-
# GitPathError (missing/oversize/outside-repo) stays a 500 so
|
|
54
|
-
# callers can distinguish it from "no changes".
|
|
55
|
-
logger.debug("Path %s is not in a git repository; returning empty diff", path)
|
|
56
|
-
return GitDiff(modified=None, original=None)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
@git_router.get("/changes")
|
|
60
|
-
async def git_changes_query(
|
|
61
|
-
path: str = Query(..., description="The git repository path"),
|
|
62
|
-
ref: str | None = Query(None, description=_REF_QUERY_DESCRIPTION),
|
|
63
|
-
) -> list[GitChange]:
|
|
64
|
-
"""Get git changes using query parameter (preferred method)."""
|
|
65
|
-
try:
|
|
66
|
-
return await _get_git_changes(path, ref)
|
|
67
|
-
except GitError as e:
|
|
68
|
-
# GitRepositoryError is already handled in the helper (returns []).
|
|
69
|
-
# Any remaining GitError subclass (e.g. GitCommandError) surfaces as
|
|
70
|
-
# 400 so the client can show an actionable error instead of an
|
|
71
|
-
# opaque 500.
|
|
72
|
-
raise HTTPException(status_code=400, detail=str(e))
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
@git_router.get("/diff")
|
|
76
|
-
async def git_diff_query(
|
|
77
|
-
path: str = Query(..., description="The file path to get diff for"),
|
|
78
|
-
ref: str | None = Query(None, description=_REF_QUERY_DESCRIPTION),
|
|
79
|
-
) -> GitDiff:
|
|
80
|
-
"""Get git diff using query parameter (preferred method)."""
|
|
81
|
-
try:
|
|
82
|
-
return await _get_git_diff(path, ref)
|
|
83
|
-
except GitError as e:
|
|
84
|
-
# GitRepositoryError is already handled in the helper (returns an
|
|
85
|
-
# empty diff). Any remaining GitError subclass (e.g. GitCommandError,
|
|
86
|
-
# GitPathError) surfaces as 400 so the client can show an actionable
|
|
87
|
-
# error instead of an opaque 500.
|
|
88
|
-
raise HTTPException(status_code=400, detail=str(e))
|
{openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/__init__.py
RENAMED
|
File without changes
|
{openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/api.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.34.0 → openhands_agent_server-1.36.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
|
{openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/env_parser.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
|
{openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/mcp_router.py
RENAMED
|
File without changes
|
{openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/middleware.py
RENAMED
|
File without changes
|
{openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/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.34.0 → openhands_agent_server-1.36.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.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/pub_sub.py
RENAMED
|
File without changes
|
{openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.34.0 → openhands_agent_server-1.36.0}/openhands/agent_server/sockets.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openhands_agent_server-1.34.0 → openhands_agent_server-1.36.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
|
|
File without changes
|