openhands-agent-server 1.48.0__tar.gz → 1.49.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.48.0 → openhands_agent_server-1.49.0}/PKG-INFO +1 -1
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/api.py +26 -16
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/config.py +7 -0
- openhands_agent_server-1.49.0/openhands/agent_server/conversation_registry.py +74 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/conversation_router.py +3 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/conversation_service.py +53 -12
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/docker/build.py +6 -4
- openhands_agent_server-1.49.0/openhands/agent_server/docker_runtime/__init__.py +1 -0
- openhands_agent_server-1.49.0/openhands/agent_server/docker_runtime/mediation.py +112 -0
- openhands_agent_server-1.49.0/openhands/agent_server/docker_runtime/provisioning.py +163 -0
- openhands_agent_server-1.49.0/openhands/agent_server/docker_runtime/proxy.py +253 -0
- openhands_agent_server-1.49.0/openhands/agent_server/docker_runtime/registry.py +319 -0
- openhands_agent_server-1.49.0/openhands/agent_server/docker_runtime/routers.py +345 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/init_router.py +14 -1
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/runtime_router.py +1 -3
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/server_details_router.py +11 -4
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/settings_router.py +16 -2
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/sockets.py +4 -3
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands_agent_server.egg-info/PKG-INFO +1 -1
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands_agent_server.egg-info/SOURCES.txt +14 -2
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/pyproject.toml +1 -1
- openhands_agent_server-1.48.0/openhands/agent_server/desktop_router.py +0 -28
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/__init__.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/__main__.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/_secret_redaction.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/_secrets_exposure.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/agent_profiles_router.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/auth_router.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/bash_router.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/bash_service.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/canvas_extensions/__init__.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/canvas_extensions/installed.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/canvas_extensions/manifest.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/canvas_extensions_router.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/conversation_lease.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/credential_binding.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/dependencies.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/docker/Dockerfile +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/env_parser.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/event_router.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/event_service.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/file_router.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/git_router.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/hooks_router.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/hooks_service.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/llm_router.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/logging_config.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/mcp_oauth_store.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/mcp_router.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/middleware.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/models.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/openai/__init__.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/openai/models.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/openai/router.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/openai/service.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/openapi.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/persistence/__init__.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/persistence/models.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/persistence/store.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/plugins_router.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/plugins_service.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/profiles_router.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/provider_connections_router.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/pub_sub.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/py.typed +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/session_protocol.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/session_socket.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/skills_router.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/skills_service.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/sub_agents_router.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry/__init__.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry/factory.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry/http_exporter.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry/models.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry/policy.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry/posthog_exporter.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry/sanitizer.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry/service.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry/sink.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry/subscriber.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/telemetry_types.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/tool_preload_service.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/tool_router.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/utils.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/vscode_router.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/vscode_service.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/workspace_router.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/workspaces_router.py +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands_agent_server.egg-info/entry_points.txt +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands_agent_server.egg-info/requires.txt +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands_agent_server.egg-info/top_level.txt +0 -0
- {openhands_agent_server-1.48.0 → openhands_agent_server-1.49.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.49.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
|
{openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/api.py
RENAMED
|
@@ -26,7 +26,13 @@ from openhands.agent_server.config import (
|
|
|
26
26
|
Config,
|
|
27
27
|
get_default_config,
|
|
28
28
|
)
|
|
29
|
-
from openhands.agent_server.
|
|
29
|
+
from openhands.agent_server.conversation_registry import (
|
|
30
|
+
create_conversation_registry,
|
|
31
|
+
)
|
|
32
|
+
from openhands.agent_server.conversation_router import (
|
|
33
|
+
conversation_catalog_router,
|
|
34
|
+
conversation_router,
|
|
35
|
+
)
|
|
30
36
|
from openhands.agent_server.conversation_service import (
|
|
31
37
|
CredentialBindingActivationRequired,
|
|
32
38
|
get_default_conversation_service,
|
|
@@ -38,8 +44,6 @@ from openhands.agent_server.dependencies import (
|
|
|
38
44
|
check_session_api_key,
|
|
39
45
|
check_workspace_session,
|
|
40
46
|
)
|
|
41
|
-
from openhands.agent_server.desktop_router import desktop_router
|
|
42
|
-
from openhands.agent_server.event_router import event_router
|
|
43
47
|
from openhands.agent_server.file_router import file_discovery_router, file_router
|
|
44
48
|
from openhands.agent_server.git_router import git_router
|
|
45
49
|
from openhands.agent_server.hooks_router import hooks_router
|
|
@@ -60,16 +64,13 @@ from openhands.agent_server.profiles_router import profiles_router
|
|
|
60
64
|
from openhands.agent_server.provider_connections_router import (
|
|
61
65
|
provider_connections_router,
|
|
62
66
|
)
|
|
63
|
-
from openhands.agent_server.runtime_router import create_runtime_router
|
|
64
67
|
from openhands.agent_server.server_details_router import (
|
|
65
68
|
get_server_info,
|
|
66
69
|
mark_initialization_complete,
|
|
67
70
|
server_details_router,
|
|
68
71
|
)
|
|
69
|
-
from openhands.agent_server.session_socket import session_router
|
|
70
72
|
from openhands.agent_server.settings_router import settings_router
|
|
71
73
|
from openhands.agent_server.skills_router import skills_router
|
|
72
|
-
from openhands.agent_server.sockets import sockets_router
|
|
73
74
|
from openhands.agent_server.sub_agents_router import sub_agents_router
|
|
74
75
|
from openhands.agent_server.telemetry import (
|
|
75
76
|
build_telemetry_sink,
|
|
@@ -92,7 +93,6 @@ from openhands.agent_server.tool_preload_service import get_tool_preload_service
|
|
|
92
93
|
from openhands.agent_server.tool_router import tool_router
|
|
93
94
|
from openhands.agent_server.vscode_router import vscode_router
|
|
94
95
|
from openhands.agent_server.vscode_service import get_vscode_service
|
|
95
|
-
from openhands.agent_server.workspace_router import workspace_router
|
|
96
96
|
from openhands.agent_server.workspaces_router import workspaces_router
|
|
97
97
|
from openhands.sdk.logger import DEBUG, get_logger
|
|
98
98
|
from openhands.sdk.utils.redact import sanitize_dict
|
|
@@ -160,6 +160,10 @@ async def api_lifespan(api: FastAPI) -> AsyncIterator[None]:
|
|
|
160
160
|
|
|
161
161
|
config: Config = api.state.config
|
|
162
162
|
deferred = config.deferred_init
|
|
163
|
+
conversation_registry = getattr(
|
|
164
|
+
api.state, "conversation_registry", None
|
|
165
|
+
) or create_conversation_registry(config)
|
|
166
|
+
api.state.conversation_registry = conversation_registry
|
|
163
167
|
|
|
164
168
|
# Deferred pods boot with telemetry disabled and are rebuilt by
|
|
165
169
|
# InitService, so they emit `server_started` there instead.
|
|
@@ -254,6 +258,11 @@ async def api_lifespan(api: FastAPI) -> AsyncIterator[None]:
|
|
|
254
258
|
bash_svc = get_default_bash_event_service()
|
|
255
259
|
api.state.bash_event_service = bash_svc
|
|
256
260
|
|
|
261
|
+
conversation_registry.configure_service(service)
|
|
262
|
+
# Runtime cleanup must precede external-catalog recovery so stale
|
|
263
|
+
# runtime owners cannot lose their expired leases to the outer service.
|
|
264
|
+
await conversation_registry.start()
|
|
265
|
+
|
|
257
266
|
async with service:
|
|
258
267
|
api.state.conversation_service = service
|
|
259
268
|
|
|
@@ -273,6 +282,7 @@ async def api_lifespan(api: FastAPI) -> AsyncIterator[None]:
|
|
|
273
282
|
try:
|
|
274
283
|
yield
|
|
275
284
|
finally:
|
|
285
|
+
await conversation_registry.shutdown()
|
|
276
286
|
if retention_task is not None:
|
|
277
287
|
retention_task.cancel()
|
|
278
288
|
with suppress(asyncio.CancelledError):
|
|
@@ -388,6 +398,7 @@ def _find_http_exception(exc: BaseExceptionGroup) -> HTTPException | None:
|
|
|
388
398
|
|
|
389
399
|
def _add_api_routes(app: FastAPI) -> None:
|
|
390
400
|
"""Add all API routes to the FastAPI application."""
|
|
401
|
+
conversation_registry = app.state.conversation_registry
|
|
391
402
|
app.include_router(server_details_router)
|
|
392
403
|
|
|
393
404
|
# The /api/init endpoint bypasses both the session-key auth and the
|
|
@@ -413,8 +424,10 @@ def _add_api_routes(app: FastAPI) -> None:
|
|
|
413
424
|
|
|
414
425
|
api_router = APIRouter(prefix="/api", dependencies=dependencies)
|
|
415
426
|
api_router.include_router(file_discovery_router)
|
|
416
|
-
|
|
417
|
-
|
|
427
|
+
# Collection routes must precede runtime catch-alls such as
|
|
428
|
+
# ``/conversations/{conversation_id}``.
|
|
429
|
+
api_router.include_router(conversation_catalog_router)
|
|
430
|
+
conversation_registry.add_execution_routes(api_router)
|
|
418
431
|
api_router.include_router(conversation_router)
|
|
419
432
|
api_router.include_router(credential_binding_router)
|
|
420
433
|
api_router.include_router(tool_router)
|
|
@@ -422,7 +435,6 @@ def _add_api_routes(app: FastAPI) -> None:
|
|
|
422
435
|
api_router.include_router(git_router)
|
|
423
436
|
api_router.include_router(file_router)
|
|
424
437
|
api_router.include_router(vscode_router)
|
|
425
|
-
api_router.include_router(desktop_router)
|
|
426
438
|
api_router.include_router(skills_router)
|
|
427
439
|
api_router.include_router(sub_agents_router)
|
|
428
440
|
api_router.include_router(plugins_router)
|
|
@@ -438,8 +450,6 @@ def _add_api_routes(app: FastAPI) -> None:
|
|
|
438
450
|
# /api/auth/* mints workspace cookies and requires the header to bootstrap,
|
|
439
451
|
# so it lives under the header-only auth group.
|
|
440
452
|
api_router.include_router(auth_router)
|
|
441
|
-
app.include_router(api_router)
|
|
442
|
-
|
|
443
453
|
app.include_router(openai_router, dependencies=[Depends(check_openai_api_key)])
|
|
444
454
|
|
|
445
455
|
# Workspace static-file routes get their own auth group that accepts
|
|
@@ -450,12 +460,11 @@ def _add_api_routes(app: FastAPI) -> None:
|
|
|
450
460
|
workspace_api_router = APIRouter(
|
|
451
461
|
prefix="/api", dependencies=[Depends(check_workspace_session)]
|
|
452
462
|
)
|
|
453
|
-
workspace_api_router.include_router(workspace_router)
|
|
463
|
+
workspace_api_router.include_router(conversation_registry.workspace_router)
|
|
454
464
|
app.include_router(workspace_api_router)
|
|
465
|
+
app.include_router(api_router)
|
|
455
466
|
|
|
456
|
-
app.include_router(sockets_router)
|
|
457
|
-
|
|
458
|
-
app.include_router(session_router)
|
|
467
|
+
app.include_router(conversation_registry.sockets_router)
|
|
459
468
|
|
|
460
469
|
|
|
461
470
|
def _setup_static_files(app: FastAPI, config: Config) -> None:
|
|
@@ -683,6 +692,7 @@ def create_app(config: Config | None = None) -> FastAPI:
|
|
|
683
692
|
config = get_default_config()
|
|
684
693
|
app = _create_fastapi_instance(config)
|
|
685
694
|
app.state.config = config
|
|
695
|
+
app.state.conversation_registry = create_conversation_registry(config)
|
|
686
696
|
|
|
687
697
|
_add_api_routes(app)
|
|
688
698
|
_setup_static_files(app, config)
|
{openhands_agent_server-1.48.0 → openhands_agent_server-1.49.0}/openhands/agent_server/config.py
RENAMED
|
@@ -346,6 +346,13 @@ class Config(BaseModel):
|
|
|
346
346
|
"The URL where this agent server instance is available externally"
|
|
347
347
|
),
|
|
348
348
|
)
|
|
349
|
+
conversation_runtime: Literal["local", "docker"] = "local"
|
|
350
|
+
conversation_image: str = "ghcr.io/openhands/agent-server:latest-python"
|
|
351
|
+
conversation_container_memory: str | None = "4g"
|
|
352
|
+
conversation_container_cpus: float | None = Field(default=2.0, gt=0)
|
|
353
|
+
conversation_container_pids_limit: int | None = Field(default=512, gt=0)
|
|
354
|
+
conversation_container_startup_timeout: float = Field(default=120, gt=0)
|
|
355
|
+
|
|
349
356
|
acp_skill_sourcing: ACPSkillSourcing = Field(
|
|
350
357
|
default="native",
|
|
351
358
|
description=(
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Select and manage the configured conversation runtime."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter
|
|
8
|
+
|
|
9
|
+
from openhands.agent_server.config import Config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from openhands.agent_server.conversation_service import ConversationService
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConversationRegistry:
|
|
17
|
+
"""Route and lifecycle adapter for host-local conversations."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, config: Config) -> None:
|
|
20
|
+
self.config = config
|
|
21
|
+
|
|
22
|
+
def configure_service(self, service: ConversationService) -> None:
|
|
23
|
+
"""Connect runtime-specific persistence to the shared catalog."""
|
|
24
|
+
|
|
25
|
+
async def start(self) -> None:
|
|
26
|
+
"""Start resources owned by this registry."""
|
|
27
|
+
|
|
28
|
+
async def shutdown(self) -> None:
|
|
29
|
+
"""Stop resources owned by this registry."""
|
|
30
|
+
|
|
31
|
+
def add_execution_routes(self, router: APIRouter) -> None:
|
|
32
|
+
from openhands.agent_server.event_router import event_router
|
|
33
|
+
from openhands.agent_server.runtime_router import create_runtime_router
|
|
34
|
+
|
|
35
|
+
router.include_router(create_runtime_router())
|
|
36
|
+
router.include_router(event_router)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def workspace_router(self) -> APIRouter:
|
|
40
|
+
from openhands.agent_server.workspace_router import workspace_router
|
|
41
|
+
|
|
42
|
+
return workspace_router
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def conversation_sockets_router(self) -> APIRouter:
|
|
46
|
+
from openhands.agent_server.sockets import conversation_sockets_router
|
|
47
|
+
|
|
48
|
+
return conversation_sockets_router
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def session_sockets_router(self) -> APIRouter:
|
|
52
|
+
from openhands.agent_server.session_socket import session_router
|
|
53
|
+
|
|
54
|
+
return session_router
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def sockets_router(self) -> APIRouter:
|
|
58
|
+
from openhands.agent_server.sockets import bash_sockets_router
|
|
59
|
+
|
|
60
|
+
router = APIRouter()
|
|
61
|
+
router.include_router(self.conversation_sockets_router)
|
|
62
|
+
router.include_router(self.session_sockets_router)
|
|
63
|
+
router.include_router(bash_sockets_router)
|
|
64
|
+
return router
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def create_conversation_registry(config: Config) -> ConversationRegistry:
|
|
68
|
+
if config.conversation_runtime == "docker":
|
|
69
|
+
from openhands.agent_server.docker_runtime.registry import (
|
|
70
|
+
DockerConversationRegistry,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return DockerConversationRegistry(config)
|
|
74
|
+
return ConversationRegistry(config)
|
|
@@ -63,6 +63,7 @@ from openhands.sdk.workspace import LocalWorkspace
|
|
|
63
63
|
from openhands.tools.preset.default import get_default_tools
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
conversation_catalog_router = APIRouter(prefix="/conversations", tags=["Conversations"])
|
|
66
67
|
conversation_router = APIRouter(prefix="/conversations", tags=["Conversations"])
|
|
67
68
|
|
|
68
69
|
# Examples
|
|
@@ -88,6 +89,7 @@ START_CONVERSATION_EXAMPLES = [
|
|
|
88
89
|
# Read methods
|
|
89
90
|
|
|
90
91
|
|
|
92
|
+
@conversation_catalog_router.get("/search", include_in_schema=False)
|
|
91
93
|
@conversation_router.get("/search")
|
|
92
94
|
async def search_conversations(
|
|
93
95
|
page_id: Annotated[
|
|
@@ -128,6 +130,7 @@ async def search_conversations(
|
|
|
128
130
|
return page
|
|
129
131
|
|
|
130
132
|
|
|
133
|
+
@conversation_catalog_router.get("/count", include_in_schema=False)
|
|
131
134
|
@conversation_router.get("/count")
|
|
132
135
|
async def count_conversations(
|
|
133
136
|
status: Annotated[
|
|
@@ -693,6 +693,7 @@ class ConversationService:
|
|
|
693
693
|
webhook_specs: list[WebhookSpec] = field(default_factory=list)
|
|
694
694
|
session_api_key: str | None = field(default=None)
|
|
695
695
|
cipher: Cipher | None = None
|
|
696
|
+
runtime_cipher_resolver: Callable[[UUID], Cipher] | None = None
|
|
696
697
|
mcp_tool_provider: MCPToolProvider | None = None
|
|
697
698
|
secrets_store: FileSecretsStore | None = None
|
|
698
699
|
owner_instance_id: str = field(default_factory=lambda: uuid4().hex)
|
|
@@ -703,6 +704,7 @@ class ConversationService:
|
|
|
703
704
|
default=Path("/tmp/conversation-worktrees")
|
|
704
705
|
)
|
|
705
706
|
acp_skill_sourcing: ACPSkillSourcing = "native"
|
|
707
|
+
sync_external_catalog: bool = False
|
|
706
708
|
_event_services: dict[UUID, EventService] | None = field(default=None, init=False)
|
|
707
709
|
_conversation_records: dict[UUID, _ConversationRecord] = field(
|
|
708
710
|
default_factory=dict, init=False
|
|
@@ -726,16 +728,23 @@ class ConversationService:
|
|
|
726
728
|
default_factory=dict, init=False
|
|
727
729
|
)
|
|
728
730
|
|
|
729
|
-
def _load_catalog_sync(
|
|
731
|
+
def _load_catalog_sync(
|
|
732
|
+
self, conversation_id: UUID | None = None
|
|
733
|
+
) -> dict[UUID, _ConversationRecord]:
|
|
730
734
|
records: dict[UUID, _ConversationRecord] = {}
|
|
731
|
-
|
|
735
|
+
directories = (
|
|
736
|
+
[self.conversations_dir / conversation_id.hex]
|
|
737
|
+
if conversation_id is not None
|
|
738
|
+
else self.conversations_dir.iterdir()
|
|
739
|
+
)
|
|
740
|
+
for conversation_dir in directories:
|
|
732
741
|
meta_file = conversation_dir / "meta.json"
|
|
733
742
|
if not meta_file.exists():
|
|
734
743
|
continue
|
|
735
744
|
try:
|
|
736
745
|
stored = StoredConversation.model_validate_json(
|
|
737
746
|
meta_file.read_text(),
|
|
738
|
-
context={"cipher": self.
|
|
747
|
+
context={"cipher": self._cipher_for(UUID(conversation_dir.name))},
|
|
739
748
|
)
|
|
740
749
|
execution_status = ConversationExecutionStatus.IDLE
|
|
741
750
|
base_state_file = conversation_dir / BASE_STATE
|
|
@@ -778,13 +787,19 @@ class ConversationService:
|
|
|
778
787
|
record.base_state_path = path
|
|
779
788
|
return path
|
|
780
789
|
|
|
790
|
+
def _cipher_for(self, conversation_id: UUID) -> Cipher | None:
|
|
791
|
+
if self.runtime_cipher_resolver is not None:
|
|
792
|
+
return self.runtime_cipher_resolver(conversation_id)
|
|
793
|
+
return self.cipher
|
|
794
|
+
|
|
781
795
|
def _load_persisted_state_sync(
|
|
782
796
|
self, conversation_id: UUID
|
|
783
797
|
) -> ConversationState | None:
|
|
784
798
|
base_state_file = self.conversations_dir / conversation_id.hex / BASE_STATE
|
|
785
799
|
if not base_state_file.exists():
|
|
786
800
|
return None
|
|
787
|
-
|
|
801
|
+
cipher = self._cipher_for(conversation_id)
|
|
802
|
+
context = {"cipher": cipher} if cipher else None
|
|
788
803
|
return ConversationState.model_validate_json(
|
|
789
804
|
base_state_file.read_text(), context=context
|
|
790
805
|
)
|
|
@@ -1093,16 +1108,34 @@ class ConversationService:
|
|
|
1093
1108
|
record.cached_info = None
|
|
1094
1109
|
record.state_signature = signature
|
|
1095
1110
|
|
|
1096
|
-
async def _reconcile_active_records(
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
small reconciliation keeps direct embedders and existing test fixtures
|
|
1101
|
-
that populate ``_event_services`` compatible.
|
|
1102
|
-
"""
|
|
1111
|
+
async def _reconcile_active_records(
|
|
1112
|
+
self, conversation_id: UUID | None = None
|
|
1113
|
+
) -> None:
|
|
1114
|
+
"""Discover externally persisted records and injected live services."""
|
|
1103
1115
|
event_services = self._event_services
|
|
1104
1116
|
if event_services is None:
|
|
1105
1117
|
raise ValueError("inactive_service")
|
|
1118
|
+
if self.sync_external_catalog:
|
|
1119
|
+
disk_records = await asyncio.to_thread(
|
|
1120
|
+
self._load_catalog_sync, conversation_id
|
|
1121
|
+
)
|
|
1122
|
+
stale_ids = (
|
|
1123
|
+
{conversation_id}
|
|
1124
|
+
if conversation_id is not None
|
|
1125
|
+
else set(self._conversation_records)
|
|
1126
|
+
) - set(disk_records)
|
|
1127
|
+
for record_id in stale_ids:
|
|
1128
|
+
event_service = event_services.get(record_id)
|
|
1129
|
+
if event_service is None or not event_service.is_open():
|
|
1130
|
+
self._conversation_records.pop(record_id, None)
|
|
1131
|
+
for record_id, record in disk_records.items():
|
|
1132
|
+
event_service = event_services.get(record_id)
|
|
1133
|
+
if event_service is not None and event_service.is_open():
|
|
1134
|
+
continue
|
|
1135
|
+
existing = self._conversation_records.setdefault(record_id, record)
|
|
1136
|
+
if existing.stored != record.stored:
|
|
1137
|
+
existing.stored = record.stored
|
|
1138
|
+
existing.cached_info = None
|
|
1106
1139
|
for conversation_id, event_service in event_services.items():
|
|
1107
1140
|
if conversation_id in self._conversation_records:
|
|
1108
1141
|
continue
|
|
@@ -1237,6 +1270,8 @@ class ConversationService:
|
|
|
1237
1270
|
async def get_conversation(self, conversation_id: UUID) -> ConversationInfo | None:
|
|
1238
1271
|
if self._event_services is None:
|
|
1239
1272
|
raise ValueError("inactive_service")
|
|
1273
|
+
if self.sync_external_catalog:
|
|
1274
|
+
await self._reconcile_active_records(conversation_id)
|
|
1240
1275
|
record = self._conversation_records.get(conversation_id)
|
|
1241
1276
|
if record is None:
|
|
1242
1277
|
event_service = self._event_services.get(conversation_id)
|
|
@@ -1615,7 +1650,12 @@ class ConversationService:
|
|
|
1615
1650
|
# Profile resolution and the load_memory stamp must happen before
|
|
1616
1651
|
# _prepare_request_workspace (which asserts request.agent is not None)
|
|
1617
1652
|
# and before model_dump so the resolved agent is captured in request_data.
|
|
1618
|
-
|
|
1653
|
+
runtime_profile = os.getenv("OH_RUNTIME_LAUNCHED_PROFILE")
|
|
1654
|
+
launched_agent_profile = (
|
|
1655
|
+
LaunchedAgentProfile.model_validate_json(runtime_profile)
|
|
1656
|
+
if runtime_profile
|
|
1657
|
+
else None
|
|
1658
|
+
)
|
|
1619
1659
|
|
|
1620
1660
|
from openhands.agent_server.persistence import (
|
|
1621
1661
|
PersistedSettings,
|
|
@@ -2386,6 +2426,7 @@ class ConversationService:
|
|
|
2386
2426
|
conversation_idle_ttl_seconds=config.conversation_idle_ttl_seconds,
|
|
2387
2427
|
conversation_worktree_root=config.conversation_worktree_root,
|
|
2388
2428
|
acp_skill_sourcing=config.acp_skill_sourcing,
|
|
2429
|
+
sync_external_catalog=False,
|
|
2389
2430
|
)
|
|
2390
2431
|
|
|
2391
2432
|
async def _start_event_service(
|
|
@@ -398,7 +398,7 @@ class BuildOptions(BaseModel):
|
|
|
398
398
|
default="", description="Comma-separated list of custom tags."
|
|
399
399
|
)
|
|
400
400
|
image: str = Field(default="ghcr.io/openhands/agent-server")
|
|
401
|
-
image_flavor: Literal["default", "slim"] = Field(default="default")
|
|
401
|
+
image_flavor: Literal["default", "slim", "minimal"] = Field(default="default")
|
|
402
402
|
target: TargetType = Field(default="binary")
|
|
403
403
|
platforms: list[PlatformType] = Field(default=["linux/amd64"])
|
|
404
404
|
push: bool | None = Field(
|
|
@@ -600,8 +600,10 @@ class BuildOptions(BaseModel):
|
|
|
600
600
|
for versioned_tag in self.versioned_tags:
|
|
601
601
|
tags.append(f"{self.image}:{versioned_tag}{arch_suffix}")
|
|
602
602
|
|
|
603
|
-
#
|
|
604
|
-
if self.target != "binary"
|
|
603
|
+
# The minimal image flavor already names its binary-minimal target.
|
|
604
|
+
if self.target != "binary" and not (
|
|
605
|
+
self.target == "binary-minimal" and self.image_flavor == "minimal"
|
|
606
|
+
):
|
|
605
607
|
tags = [f"{t}-{self.target}" for t in tags]
|
|
606
608
|
return list(dict.fromkeys(tags))
|
|
607
609
|
|
|
@@ -1083,7 +1085,7 @@ def main(argv: list[str]) -> int:
|
|
|
1083
1085
|
parser.add_argument(
|
|
1084
1086
|
"--image-flavor",
|
|
1085
1087
|
default=_env("IMAGE_FLAVOR", "default"),
|
|
1086
|
-
choices=("default", "slim"),
|
|
1088
|
+
choices=("default", "slim", "minimal"),
|
|
1087
1089
|
help="Image flavor (default from $IMAGE_FLAVOR).",
|
|
1088
1090
|
)
|
|
1089
1091
|
parser.add_argument(
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Per-conversation Docker runtime support."""
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Prepare a conversation request for an isolated agent-server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import SecretStr
|
|
10
|
+
|
|
11
|
+
from openhands.agent_server.config import Config
|
|
12
|
+
from openhands.agent_server.conversation_service import (
|
|
13
|
+
_resolve_agent_from_profile,
|
|
14
|
+
_with_load_memory,
|
|
15
|
+
)
|
|
16
|
+
from openhands.agent_server.docker_runtime.provisioning import RuntimeIdentity
|
|
17
|
+
from openhands.agent_server.persistence import PersistedSettings, get_settings_store
|
|
18
|
+
from openhands.sdk.agent.base import AgentBase
|
|
19
|
+
from openhands.sdk.conversation.request import StartConversationRequest
|
|
20
|
+
from openhands.sdk.conversation.secret_registry import SecretRegistry
|
|
21
|
+
from openhands.sdk.profiles.agent_profile import LaunchedAgentProfile
|
|
22
|
+
from openhands.sdk.secret import SecretSource, SecretValue, StaticSecret
|
|
23
|
+
from openhands.sdk.settings.model import validate_agent_settings
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def materialize_secrets(
|
|
27
|
+
secrets: Mapping[str, SecretValue],
|
|
28
|
+
) -> dict[str, SecretSource]:
|
|
29
|
+
"""Resolve the SDK's secret sources before crossing a runtime boundary."""
|
|
30
|
+
registry = SecretRegistry()
|
|
31
|
+
registry.update_secrets(secrets)
|
|
32
|
+
return {
|
|
33
|
+
name: StaticSecret(
|
|
34
|
+
value=SecretStr(value)
|
|
35
|
+
if (value := registry.get_secret_value(name))
|
|
36
|
+
else None,
|
|
37
|
+
description=source.description,
|
|
38
|
+
)
|
|
39
|
+
for name, source in registry.secret_sources.items()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _materialize_agent_context(agent: AgentBase) -> AgentBase:
|
|
44
|
+
context = agent.agent_context
|
|
45
|
+
if context is None or not context.secrets:
|
|
46
|
+
return agent
|
|
47
|
+
return agent.model_copy(
|
|
48
|
+
update={
|
|
49
|
+
"agent_context": context.model_copy(
|
|
50
|
+
update={"secrets": materialize_secrets(context.secrets)}
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def prepare_start(
|
|
57
|
+
body: dict[str, Any], config: Config
|
|
58
|
+
) -> tuple[StartConversationRequest, LaunchedAgentProfile | None]:
|
|
59
|
+
body = {
|
|
60
|
+
name: value
|
|
61
|
+
for name, value in body.items()
|
|
62
|
+
if value is not None or name not in {"agent", "agent_settings"}
|
|
63
|
+
}
|
|
64
|
+
context = {"cipher": config.cipher} if body.get("secrets_encrypted") else None
|
|
65
|
+
if body.get("agent_settings") is not None:
|
|
66
|
+
settings = validate_agent_settings(body["agent_settings"], context=context)
|
|
67
|
+
body = {**body, "agent": settings.create_agent(), "agent_settings": None}
|
|
68
|
+
request = StartConversationRequest.model_validate(body, context=context)
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
settings = await asyncio.to_thread(get_settings_store(config).load)
|
|
72
|
+
except (OSError, PermissionError):
|
|
73
|
+
settings = None
|
|
74
|
+
settings = settings or PersistedSettings()
|
|
75
|
+
launched = None
|
|
76
|
+
if request.agent_profile_id is not None:
|
|
77
|
+
agent, launched, allowed = await asyncio.to_thread(
|
|
78
|
+
_resolve_agent_from_profile,
|
|
79
|
+
request.agent_profile_id,
|
|
80
|
+
config.cipher,
|
|
81
|
+
settings.agent_settings.mcp_config,
|
|
82
|
+
acp_skill_sourcing=config.acp_skill_sourcing,
|
|
83
|
+
)
|
|
84
|
+
secrets = request.secrets
|
|
85
|
+
if allowed is not None:
|
|
86
|
+
secrets = {
|
|
87
|
+
name: value for name, value in secrets.items() if name in allowed
|
|
88
|
+
}
|
|
89
|
+
request = request.model_copy(
|
|
90
|
+
update={"agent": agent, "agent_profile_id": None, "secrets": secrets}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
context_settings = settings.agent_settings.agent_context
|
|
94
|
+
if context_settings is not None and context_settings.load_memory:
|
|
95
|
+
request = request.model_copy(update={"agent": _with_load_memory(request.agent)})
|
|
96
|
+
request = request.model_copy(
|
|
97
|
+
update={
|
|
98
|
+
"agent": await asyncio.to_thread(_materialize_agent_context, request.agent),
|
|
99
|
+
"secrets": await asyncio.to_thread(materialize_secrets, request.secrets),
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
return request, launched
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def serialize_start(
|
|
106
|
+
request: StartConversationRequest, identity: RuntimeIdentity
|
|
107
|
+
) -> dict[str, Any]:
|
|
108
|
+
payload = request.model_dump(
|
|
109
|
+
mode="json", context={"cipher": identity.cipher}, exclude={"agent_profile_id"}
|
|
110
|
+
)
|
|
111
|
+
payload["secrets_encrypted"] = True
|
|
112
|
+
return payload
|