openhands-agent-server 1.29.0__tar.gz → 1.29.3__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.29.0 → openhands_agent_server-1.29.3}/PKG-INFO +1 -1
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/agent_profiles_router.py +10 -9
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/api.py +24 -27
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/bash_router.py +20 -6
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/conversation_service.py +24 -9
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/dependencies.py +35 -32
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/docker/Dockerfile +13 -4
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/event_service.py +38 -15
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/file_router.py +30 -16
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/init_router.py +17 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/openai/router.py +17 -18
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/openai/service.py +7 -4
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/persistence/__init__.py +4 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/persistence/store.py +61 -0
- openhands_agent_server-1.29.3/openhands/agent_server/plugins_router.py +333 -0
- openhands_agent_server-1.29.3/openhands/agent_server/plugins_service.py +295 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/profiles_router.py +10 -10
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/sockets.py +44 -6
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands_agent_server.egg-info/PKG-INFO +1 -1
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands_agent_server.egg-info/SOURCES.txt +4 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/pyproject.toml +1 -1
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/__init__.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/__main__.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/_secrets_exposure.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/auth_router.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/bash_service.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/config.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/conversation_lease.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/conversation_router.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/desktop_router.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/desktop_service.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/docker/build.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/docker/wallpaper.svg +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/env_parser.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/event_router.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/git_router.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/hooks_router.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/hooks_service.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/llm_router.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/logging_config.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/mcp_router.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/middleware.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/models.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/openai/__init__.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/openai/models.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/openapi.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/persistence/models.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/pub_sub.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/py.typed +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/server_details_router.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/settings_router.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/skills_router.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/skills_service.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/tool_preload_service.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/tool_router.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/utils.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/vscode_router.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/vscode_service.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/workspace_router.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/workspaces_router.py +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands_agent_server.egg-info/entry_points.txt +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands_agent_server.egg-info/requires.txt +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands_agent_server.egg-info/top_level.txt +0 -0
- {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openhands-agent-server
|
|
3
|
-
Version: 1.29.
|
|
3
|
+
Version: 1.29.3
|
|
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
|
|
@@ -30,9 +30,10 @@ from openhands.agent_server._secrets_exposure import (
|
|
|
30
30
|
)
|
|
31
31
|
from openhands.agent_server.persistence import (
|
|
32
32
|
PersistedSettings,
|
|
33
|
+
get_agent_profile_store,
|
|
34
|
+
get_llm_profile_store,
|
|
33
35
|
get_settings_store,
|
|
34
36
|
)
|
|
35
|
-
from openhands.sdk.llm.llm_profile_store import LLMProfileStore
|
|
36
37
|
from openhands.sdk.logger import get_logger
|
|
37
38
|
from openhands.sdk.profiles import (
|
|
38
39
|
ACPAgentProfile,
|
|
@@ -318,7 +319,7 @@ async def list_agent_profiles(request: Request) -> AgentProfileListResponse:
|
|
|
318
319
|
settings_store = get_settings_store(config)
|
|
319
320
|
settings = settings_store.load() or PersistedSettings()
|
|
320
321
|
|
|
321
|
-
store =
|
|
322
|
+
store = get_agent_profile_store()
|
|
322
323
|
with _store_errors():
|
|
323
324
|
existing = store.list()
|
|
324
325
|
|
|
@@ -347,7 +348,7 @@ async def get_agent_profile(
|
|
|
347
348
|
expose_mode = parse_expose_secrets_header(request)
|
|
348
349
|
cipher = get_cipher(request)
|
|
349
350
|
|
|
350
|
-
store =
|
|
351
|
+
store = get_agent_profile_store()
|
|
351
352
|
try:
|
|
352
353
|
with _store_errors():
|
|
353
354
|
profile = store.load(name, cipher=cipher)
|
|
@@ -408,7 +409,7 @@ async def save_agent_profile(
|
|
|
408
409
|
# secret once rather than double-encrypting the token.
|
|
409
410
|
profile = _decrypt_profile_mcp_tools(profile, cipher)
|
|
410
411
|
|
|
411
|
-
store =
|
|
412
|
+
store = get_agent_profile_store()
|
|
412
413
|
# The id is server-managed (the active pointer is keyed on it): overwrite
|
|
413
414
|
# keeps the namesake's id and bumps revision; create mints a fresh id,
|
|
414
415
|
# ignoring any client-supplied one. The lock spans read + mint + save so two
|
|
@@ -448,7 +449,7 @@ async def delete_agent_profile(
|
|
|
448
449
|
If the deleted profile was the active one, ``active_agent_profile_id`` is
|
|
449
450
|
cleared.
|
|
450
451
|
"""
|
|
451
|
-
store =
|
|
452
|
+
store = get_agent_profile_store()
|
|
452
453
|
deleted_id = _summary_id_for_name(store, name)
|
|
453
454
|
|
|
454
455
|
with _store_errors():
|
|
@@ -485,7 +486,7 @@ async def rename_agent_profile(
|
|
|
485
486
|
survives the rename untouched. Returns 404 if the source is missing, 409 if
|
|
486
487
|
``new_name`` is taken.
|
|
487
488
|
"""
|
|
488
|
-
store =
|
|
489
|
+
store = get_agent_profile_store()
|
|
489
490
|
try:
|
|
490
491
|
with _store_errors():
|
|
491
492
|
store.rename(name, body.new_name)
|
|
@@ -520,7 +521,7 @@ async def activate_agent_profile(
|
|
|
520
521
|
``/activate``, this does **not** write ``agent_settings`` (the
|
|
521
522
|
creation-time-only contract). Returns 404 if no stored profile has that id.
|
|
522
523
|
"""
|
|
523
|
-
store =
|
|
524
|
+
store = get_agent_profile_store()
|
|
524
525
|
with _store_errors():
|
|
525
526
|
known_ids = {
|
|
526
527
|
str(s["id"]) for s in store.list_summaries() if s.get("id") is not None
|
|
@@ -574,7 +575,7 @@ async def materialize_agent_profile(
|
|
|
574
575
|
"""
|
|
575
576
|
cipher = get_cipher(request)
|
|
576
577
|
|
|
577
|
-
store =
|
|
578
|
+
store = get_agent_profile_store()
|
|
578
579
|
try:
|
|
579
580
|
with _store_errors():
|
|
580
581
|
profile = store.load(name, cipher=cipher)
|
|
@@ -592,7 +593,7 @@ async def materialize_agent_profile(
|
|
|
592
593
|
settings = get_settings_store(config).load() or PersistedSettings()
|
|
593
594
|
mcp_config = settings.agent_settings.mcp_config
|
|
594
595
|
|
|
595
|
-
llm_store =
|
|
596
|
+
llm_store = get_llm_profile_store()
|
|
596
597
|
return resolve_agent_profile_dry_run(
|
|
597
598
|
profile,
|
|
598
599
|
llm_store=llm_store,
|
{openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/api.py
RENAMED
|
@@ -29,8 +29,8 @@ from openhands.agent_server.conversation_service import (
|
|
|
29
29
|
get_default_conversation_service,
|
|
30
30
|
)
|
|
31
31
|
from openhands.agent_server.dependencies import (
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
check_session_api_key,
|
|
33
|
+
check_workspace_session,
|
|
34
34
|
)
|
|
35
35
|
from openhands.agent_server.desktop_router import desktop_router
|
|
36
36
|
from openhands.agent_server.desktop_service import get_desktop_service
|
|
@@ -47,9 +47,10 @@ from openhands.agent_server.llm_router import llm_router
|
|
|
47
47
|
from openhands.agent_server.mcp_router import mcp_router
|
|
48
48
|
from openhands.agent_server.middleware import CORSDispatcher
|
|
49
49
|
from openhands.agent_server.openai.router import (
|
|
50
|
-
|
|
50
|
+
check_openai_api_key,
|
|
51
51
|
openai_router,
|
|
52
52
|
)
|
|
53
|
+
from openhands.agent_server.plugins_router import plugins_router
|
|
53
54
|
from openhands.agent_server.profiles_router import profiles_router
|
|
54
55
|
from openhands.agent_server.server_details_router import (
|
|
55
56
|
get_server_info,
|
|
@@ -234,6 +235,9 @@ async def api_lifespan(api: FastAPI) -> AsyncIterator[None]:
|
|
|
234
235
|
mark_initialization_complete()
|
|
235
236
|
logger.info("Server initialization complete - ready to serve requests")
|
|
236
237
|
|
|
238
|
+
bash_svc = get_default_bash_event_service()
|
|
239
|
+
api.state.bash_event_service = bash_svc
|
|
240
|
+
|
|
237
241
|
async with service:
|
|
238
242
|
api.state.conversation_service = service
|
|
239
243
|
|
|
@@ -241,7 +245,7 @@ async def api_lifespan(api: FastAPI) -> AsyncIterator[None]:
|
|
|
241
245
|
retention_task: asyncio.Task | None = None
|
|
242
246
|
if config.bash_events_retention_seconds is not None:
|
|
243
247
|
retention_task = asyncio.create_task(
|
|
244
|
-
|
|
248
|
+
bash_svc.run_retention_cleanup_loop(
|
|
245
249
|
config.bash_events_retention_seconds
|
|
246
250
|
)
|
|
247
251
|
)
|
|
@@ -310,12 +314,8 @@ def _find_http_exception(exc: BaseExceptionGroup) -> HTTPException | None:
|
|
|
310
314
|
return None
|
|
311
315
|
|
|
312
316
|
|
|
313
|
-
def _add_api_routes(app: FastAPI
|
|
314
|
-
"""Add all API routes to the FastAPI application.
|
|
315
|
-
|
|
316
|
-
Args:
|
|
317
|
-
app: FastAPI application instance to add routes to.
|
|
318
|
-
"""
|
|
317
|
+
def _add_api_routes(app: FastAPI) -> None:
|
|
318
|
+
"""Add all API routes to the FastAPI application."""
|
|
319
319
|
app.include_router(server_details_router)
|
|
320
320
|
|
|
321
321
|
# The /api/init endpoint bypasses both the session-key auth and the
|
|
@@ -330,12 +330,14 @@ def _add_api_routes(app: FastAPI, config: Config) -> None:
|
|
|
330
330
|
# Header-only auth: applied to every /api/* route EXCEPT the workspace
|
|
331
331
|
# static-file routes (handled separately below). Cookies are NOT honored
|
|
332
332
|
# here so that we don't expand the CSRF surface across the whole API.
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
333
|
+
# check_session_api_key reads config from request.app.state at request time,
|
|
334
|
+
# so keys delivered via POST /api/init are honoured without re-registering routes.
|
|
335
|
+
dependencies = [
|
|
336
|
+
Depends(check_session_api_key),
|
|
337
|
+
# Dormant gate: 503s every /api/* route until POST /api/init completes.
|
|
338
|
+
# No-op for non-deferred deployments.
|
|
339
|
+
Depends(require_initialized),
|
|
340
|
+
]
|
|
339
341
|
|
|
340
342
|
api_router = APIRouter(prefix="/api", dependencies=dependencies)
|
|
341
343
|
api_router.include_router(event_router)
|
|
@@ -347,6 +349,7 @@ def _add_api_routes(app: FastAPI, config: Config) -> None:
|
|
|
347
349
|
api_router.include_router(vscode_router)
|
|
348
350
|
api_router.include_router(desktop_router)
|
|
349
351
|
api_router.include_router(skills_router)
|
|
352
|
+
api_router.include_router(plugins_router)
|
|
350
353
|
api_router.include_router(hooks_router)
|
|
351
354
|
api_router.include_router(llm_router)
|
|
352
355
|
api_router.include_router(mcp_router)
|
|
@@ -359,22 +362,16 @@ def _add_api_routes(app: FastAPI, config: Config) -> None:
|
|
|
359
362
|
api_router.include_router(auth_router)
|
|
360
363
|
app.include_router(api_router)
|
|
361
364
|
|
|
362
|
-
|
|
363
|
-
if config.session_api_keys:
|
|
364
|
-
openai_dependencies.append(Depends(create_openai_api_key_dependency(config)))
|
|
365
|
-
app.include_router(openai_router, dependencies=openai_dependencies)
|
|
365
|
+
app.include_router(openai_router, dependencies=[Depends(check_openai_api_key)])
|
|
366
366
|
|
|
367
367
|
# Workspace static-file routes get their own auth group that accepts
|
|
368
368
|
# EITHER the X-Session-API-Key header OR the workspace session cookie.
|
|
369
369
|
# The cookie is required so that <iframe src> / <img src> embeds of
|
|
370
370
|
# workspace artifacts work — browsers cannot attach custom headers to
|
|
371
371
|
# those requests.
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
Depends(create_workspace_session_dependency(config))
|
|
376
|
-
)
|
|
377
|
-
workspace_api_router = APIRouter(prefix="/api", dependencies=workspace_dependencies)
|
|
372
|
+
workspace_api_router = APIRouter(
|
|
373
|
+
prefix="/api", dependencies=[Depends(check_workspace_session)]
|
|
374
|
+
)
|
|
378
375
|
workspace_api_router.include_router(workspace_router)
|
|
379
376
|
app.include_router(workspace_api_router)
|
|
380
377
|
|
|
@@ -592,7 +589,7 @@ def create_app(config: Config | None = None) -> FastAPI:
|
|
|
592
589
|
app = _create_fastapi_instance(config)
|
|
593
590
|
app.state.config = config
|
|
594
591
|
|
|
595
|
-
_add_api_routes(app
|
|
592
|
+
_add_api_routes(app)
|
|
596
593
|
_setup_static_files(app, config)
|
|
597
594
|
app.add_middleware(
|
|
598
595
|
CORSDispatcher,
|
|
@@ -7,12 +7,14 @@ from uuid import UUID
|
|
|
7
7
|
|
|
8
8
|
from fastapi import (
|
|
9
9
|
APIRouter,
|
|
10
|
+
Depends,
|
|
10
11
|
HTTPException,
|
|
11
12
|
Query,
|
|
12
13
|
status,
|
|
13
14
|
)
|
|
14
15
|
|
|
15
|
-
from openhands.agent_server.bash_service import
|
|
16
|
+
from openhands.agent_server.bash_service import BashEventService
|
|
17
|
+
from openhands.agent_server.dependencies import get_bash_event_service
|
|
16
18
|
from openhands.agent_server.models import (
|
|
17
19
|
BashCommand,
|
|
18
20
|
BashEventBase,
|
|
@@ -25,7 +27,6 @@ from openhands.agent_server.server_details_router import update_last_execution_t
|
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
bash_router = APIRouter(prefix="/bash", tags=["Bash"])
|
|
28
|
-
bash_event_service = get_default_bash_event_service()
|
|
29
30
|
logger = logging.getLogger(__name__)
|
|
30
31
|
|
|
31
32
|
|
|
@@ -53,6 +54,7 @@ async def search_bash_events(
|
|
|
53
54
|
int,
|
|
54
55
|
Query(title="The max number of results in the page", gt=0, lte=100),
|
|
55
56
|
] = 100,
|
|
57
|
+
bash_event_service: BashEventService = Depends(get_bash_event_service),
|
|
56
58
|
) -> BashEventPage:
|
|
57
59
|
"""Search / List bash event events"""
|
|
58
60
|
assert limit > 0
|
|
@@ -73,7 +75,10 @@ async def search_bash_events(
|
|
|
73
75
|
@bash_router.get(
|
|
74
76
|
"/bash_events/{event_id}", responses={404: {"description": "Item not found"}}
|
|
75
77
|
)
|
|
76
|
-
async def get_bash_event(
|
|
78
|
+
async def get_bash_event(
|
|
79
|
+
event_id: str,
|
|
80
|
+
bash_event_service: BashEventService = Depends(get_bash_event_service),
|
|
81
|
+
) -> BashEventBase:
|
|
77
82
|
"""Get a bash event event given an id"""
|
|
78
83
|
event = await bash_event_service.get_bash_event(event_id)
|
|
79
84
|
if event is None:
|
|
@@ -84,6 +89,7 @@ async def get_bash_event(event_id: str) -> BashEventBase:
|
|
|
84
89
|
@bash_router.get("/bash_events/")
|
|
85
90
|
async def batch_get_bash_events(
|
|
86
91
|
event_ids: list[str],
|
|
92
|
+
bash_event_service: BashEventService = Depends(get_bash_event_service),
|
|
87
93
|
) -> list[BashEventBase | None]:
|
|
88
94
|
"""Get a batch of bash event events given their ids, returning null for any
|
|
89
95
|
missing item."""
|
|
@@ -92,7 +98,10 @@ async def batch_get_bash_events(
|
|
|
92
98
|
|
|
93
99
|
|
|
94
100
|
@bash_router.post("/start_bash_command")
|
|
95
|
-
async def start_bash_command(
|
|
101
|
+
async def start_bash_command(
|
|
102
|
+
request: ExecuteBashRequest,
|
|
103
|
+
bash_event_service: BashEventService = Depends(get_bash_event_service),
|
|
104
|
+
) -> BashCommand:
|
|
96
105
|
"""Execute a bash command in the background"""
|
|
97
106
|
update_last_execution_time()
|
|
98
107
|
command, _ = await bash_event_service.start_bash_command(request)
|
|
@@ -100,7 +109,10 @@ async def start_bash_command(request: ExecuteBashRequest) -> BashCommand:
|
|
|
100
109
|
|
|
101
110
|
|
|
102
111
|
@bash_router.post("/execute_bash_command")
|
|
103
|
-
async def execute_bash_command(
|
|
112
|
+
async def execute_bash_command(
|
|
113
|
+
request: ExecuteBashRequest,
|
|
114
|
+
bash_event_service: BashEventService = Depends(get_bash_event_service),
|
|
115
|
+
) -> BashOutput:
|
|
104
116
|
"""Execute a bash command and wait for a result"""
|
|
105
117
|
update_last_execution_time()
|
|
106
118
|
command, task = await bash_event_service.start_bash_command(request)
|
|
@@ -111,7 +123,9 @@ async def execute_bash_command(request: ExecuteBashRequest) -> BashOutput:
|
|
|
111
123
|
|
|
112
124
|
|
|
113
125
|
@bash_router.delete("/bash_events")
|
|
114
|
-
async def clear_all_bash_events(
|
|
126
|
+
async def clear_all_bash_events(
|
|
127
|
+
bash_event_service: BashEventService = Depends(get_bash_event_service),
|
|
128
|
+
) -> dict[str, int]:
|
|
115
129
|
"""Clear all bash events from storage"""
|
|
116
130
|
count = await bash_event_service.clear_all_events()
|
|
117
131
|
return {"cleared_count": count}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import importlib
|
|
3
3
|
import logging
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
4
5
|
from concurrent.futures import ThreadPoolExecutor
|
|
5
6
|
from contextlib import suppress
|
|
6
7
|
from dataclasses import dataclass, field
|
|
@@ -261,11 +262,13 @@ def _resolve_agent_from_profile(
|
|
|
261
262
|
DanglingMcpServerRef: A referenced MCP server is absent from the global config.
|
|
262
263
|
ValueError: Profile load or settings validation failure.
|
|
263
264
|
"""
|
|
264
|
-
from openhands.
|
|
265
|
-
|
|
265
|
+
from openhands.agent_server.persistence.store import (
|
|
266
|
+
get_agent_profile_store,
|
|
267
|
+
get_llm_profile_store,
|
|
268
|
+
)
|
|
266
269
|
from openhands.sdk.profiles.resolver import ProfileNotFound, resolve_agent_profile
|
|
267
270
|
|
|
268
|
-
store =
|
|
271
|
+
store = get_agent_profile_store()
|
|
269
272
|
profile_name = store.name_for_id(profile_id)
|
|
270
273
|
if profile_name is None:
|
|
271
274
|
raise ProfileNotFound(f"Agent profile with id '{profile_id}' not found")
|
|
@@ -281,7 +284,7 @@ def _resolve_agent_from_profile(
|
|
|
281
284
|
f"Failed to load agent profile '{profile_name}': {exc}"
|
|
282
285
|
) from exc
|
|
283
286
|
|
|
284
|
-
llm_store =
|
|
287
|
+
llm_store = get_llm_profile_store()
|
|
285
288
|
try:
|
|
286
289
|
settings_config = resolve_agent_profile(
|
|
287
290
|
profile, llm_store=llm_store, mcp_config=mcp_config, cipher=cipher
|
|
@@ -1343,9 +1346,11 @@ class AutoTitleSubscriber(Subscriber):
|
|
|
1343
1346
|
return None
|
|
1344
1347
|
|
|
1345
1348
|
try:
|
|
1346
|
-
from openhands.
|
|
1349
|
+
from openhands.agent_server.persistence.store import (
|
|
1350
|
+
get_llm_profile_store,
|
|
1351
|
+
)
|
|
1347
1352
|
|
|
1348
|
-
profile_store =
|
|
1353
|
+
profile_store = get_llm_profile_store()
|
|
1349
1354
|
return profile_store.load(profile_name, cipher=self.service.cipher)
|
|
1350
1355
|
except (FileNotFoundError, ValueError) as e:
|
|
1351
1356
|
logger.warning(
|
|
@@ -1363,6 +1368,12 @@ class WebhookSubscriber(Subscriber):
|
|
|
1363
1368
|
session_api_key: str | None = None
|
|
1364
1369
|
queue: list[Event] = field(default_factory=list)
|
|
1365
1370
|
_flush_timer: asyncio.Task | None = field(default=None, init=False)
|
|
1371
|
+
# Per-instance sleep seam so tests override delays without patching the
|
|
1372
|
+
# global asyncio.sleep. default_factory (not default) keeps it an instance
|
|
1373
|
+
# attribute, else the function would be descriptor-bound as a method.
|
|
1374
|
+
_sleep: Callable[[float], Awaitable[None]] = field(
|
|
1375
|
+
default_factory=lambda: asyncio.sleep, init=False
|
|
1376
|
+
)
|
|
1366
1377
|
|
|
1367
1378
|
async def __call__(self, event: Event):
|
|
1368
1379
|
"""Add event to queue and post to webhook when buffer size is reached."""
|
|
@@ -1433,7 +1444,7 @@ class WebhookSubscriber(Subscriber):
|
|
|
1433
1444
|
except Exception as e:
|
|
1434
1445
|
logger.warning(f"Webhook post attempt {attempt + 1} failed: {e}")
|
|
1435
1446
|
if attempt < self.spec.num_retries:
|
|
1436
|
-
await
|
|
1447
|
+
await self._sleep(self.spec.retry_delay)
|
|
1437
1448
|
else:
|
|
1438
1449
|
logger.error(
|
|
1439
1450
|
f"Failed to post events to webhook {events_url} "
|
|
@@ -1458,7 +1469,7 @@ class WebhookSubscriber(Subscriber):
|
|
|
1458
1469
|
async def _flush_after_delay(self):
|
|
1459
1470
|
"""Wait for flush_delay seconds then flush events if any exist."""
|
|
1460
1471
|
try:
|
|
1461
|
-
await
|
|
1472
|
+
await self._sleep(self.spec.flush_delay)
|
|
1462
1473
|
# Only flush if there are events in the queue
|
|
1463
1474
|
if self.queue:
|
|
1464
1475
|
await self._post_events()
|
|
@@ -1475,6 +1486,10 @@ class ConversationWebhookSubscriber:
|
|
|
1475
1486
|
|
|
1476
1487
|
spec: WebhookSpec
|
|
1477
1488
|
session_api_key: str | None = None
|
|
1489
|
+
# Per-instance sleep seam; see WebhookSubscriber._sleep.
|
|
1490
|
+
_sleep: Callable[[float], Awaitable[None]] = field(
|
|
1491
|
+
default_factory=lambda: asyncio.sleep, init=False
|
|
1492
|
+
)
|
|
1478
1493
|
|
|
1479
1494
|
async def post_conversation_info(self, conversation_info: BaseModel):
|
|
1480
1495
|
"""Post conversation info to the webhook immediately (no batching)."""
|
|
@@ -1512,7 +1527,7 @@ class ConversationWebhookSubscriber:
|
|
|
1512
1527
|
f"Conversation webhook post attempt {attempt + 1} failed: {e}"
|
|
1513
1528
|
)
|
|
1514
1529
|
if attempt < self.spec.num_retries:
|
|
1515
|
-
await
|
|
1530
|
+
await self._sleep(self.spec.retry_delay)
|
|
1516
1531
|
else:
|
|
1517
1532
|
# Log response content for debugging failures
|
|
1518
1533
|
response_content = (
|
|
@@ -3,6 +3,7 @@ from uuid import UUID
|
|
|
3
3
|
from fastapi import Depends, HTTPException, Request, status
|
|
4
4
|
from fastapi.security import APIKeyCookie, APIKeyHeader
|
|
5
5
|
|
|
6
|
+
from openhands.agent_server.bash_service import BashEventService
|
|
6
7
|
from openhands.agent_server.config import Config
|
|
7
8
|
from openhands.agent_server.conversation_service import ConversationService
|
|
8
9
|
from openhands.agent_server.event_service import EventService
|
|
@@ -20,22 +21,26 @@ _WORKSPACE_SESSION_COOKIE = APIKeyCookie(
|
|
|
20
21
|
)
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
def
|
|
24
|
-
|
|
24
|
+
def check_session_api_key(
|
|
25
|
+
request: Request,
|
|
26
|
+
session_api_key: str | None = Depends(_SESSION_API_KEY_HEADER),
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Reject the request if the supplied key is not in the current session keys.
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
|
|
34
|
-
|
|
35
|
-
return check_session_api_key
|
|
30
|
+
Reads ``session_api_keys`` from ``request.app.state.config`` at request time
|
|
31
|
+
so that keys delivered via ``POST /api/init`` take effect immediately without
|
|
32
|
+
restarting the server or re-registering routes.
|
|
33
|
+
"""
|
|
34
|
+
config: Config = request.app.state.config
|
|
35
|
+
if config.session_api_keys and session_api_key not in config.session_api_keys:
|
|
36
|
+
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
|
|
36
37
|
|
|
37
38
|
|
|
38
|
-
def
|
|
39
|
+
def check_workspace_session(
|
|
40
|
+
request: Request,
|
|
41
|
+
header_key: str | None = Depends(_SESSION_API_KEY_HEADER),
|
|
42
|
+
cookie_key: str | None = Depends(_WORKSPACE_SESSION_COOKIE),
|
|
43
|
+
) -> None:
|
|
39
44
|
"""Auth dependency for the workspace static-file routes.
|
|
40
45
|
|
|
41
46
|
Accepts EITHER the standard ``X-Session-API-Key`` header OR the
|
|
@@ -46,28 +51,16 @@ def create_workspace_session_dependency(config: Config):
|
|
|
46
51
|
frontend embeds workspace artifacts. The cookie is deliberately scoped
|
|
47
52
|
to this router only; no other endpoint honors it.
|
|
48
53
|
"""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if not config.session_api_keys:
|
|
54
|
+
config: Config = request.app.state.config
|
|
55
|
+
if not config.session_api_keys:
|
|
56
|
+
return
|
|
57
|
+
for candidate in (header_key, cookie_key):
|
|
58
|
+
if candidate and candidate in config.session_api_keys:
|
|
55
59
|
return
|
|
56
|
-
|
|
57
|
-
if candidate and candidate in config.session_api_keys:
|
|
58
|
-
return
|
|
59
|
-
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
|
|
60
|
-
|
|
61
|
-
return check_workspace_session
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def get_conversation_service(request: Request):
|
|
65
|
-
"""Get the conversation service from app state.
|
|
60
|
+
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
|
|
66
61
|
|
|
67
|
-
This dependency ensures that the conversation service is properly initialized
|
|
68
|
-
through the application lifespan context manager.
|
|
69
|
-
"""
|
|
70
62
|
|
|
63
|
+
def get_conversation_service(request: Request) -> ConversationService:
|
|
71
64
|
service = getattr(request.app.state, "conversation_service", None)
|
|
72
65
|
if service is None:
|
|
73
66
|
raise HTTPException(
|
|
@@ -77,6 +70,16 @@ def get_conversation_service(request: Request):
|
|
|
77
70
|
return service
|
|
78
71
|
|
|
79
72
|
|
|
73
|
+
def get_bash_event_service(request: Request) -> BashEventService:
|
|
74
|
+
service = getattr(request.app.state, "bash_event_service", None)
|
|
75
|
+
if service is None:
|
|
76
|
+
raise HTTPException(
|
|
77
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
78
|
+
detail="Bash event service is not available",
|
|
79
|
+
)
|
|
80
|
+
return service
|
|
81
|
+
|
|
82
|
+
|
|
80
83
|
async def get_event_service(
|
|
81
84
|
conversation_id: UUID,
|
|
82
85
|
conversation_service: ConversationService = Depends(get_conversation_service),
|
|
@@ -8,6 +8,11 @@ ARG USERNAME=openhands
|
|
|
8
8
|
ARG UID=10001
|
|
9
9
|
ARG GID=10001
|
|
10
10
|
ARG PORT=8000
|
|
11
|
+
# Opt-in build flag for the Vertex AI extra (`openhands-sdk[vertex]`). Off by
|
|
12
|
+
# default to keep the published image lean. Pass `--build-arg ENABLE_VERTEX=1`
|
|
13
|
+
# to bundle google-cloud-aiplatform so the resulting binary supports
|
|
14
|
+
# `vertex_ai/*` partner models (MiniMax, Qwen, Kimi MaaS endpoints).
|
|
15
|
+
ARG ENABLE_VERTEX=0
|
|
11
16
|
|
|
12
17
|
####################################################################################
|
|
13
18
|
# Builder (source mode)
|
|
@@ -26,7 +31,7 @@ ARG PORT=8000
|
|
|
26
31
|
# See OpenHands/software-agent-sdk#2761.
|
|
27
32
|
####################################################################################
|
|
28
33
|
FROM python:3.13-bookworm AS builder
|
|
29
|
-
ARG USERNAME UID GID
|
|
34
|
+
ARG USERNAME UID GID ENABLE_VERTEX
|
|
30
35
|
ENV UV_PROJECT_ENVIRONMENT=/agent-server/.venv
|
|
31
36
|
ENV UV_PYTHON_INSTALL_DIR=/agent-server/uv-managed-python
|
|
32
37
|
|
|
@@ -48,9 +53,11 @@ COPY --chown=${USERNAME}:${USERNAME} openhands-tools ./openhands-tools
|
|
|
48
53
|
COPY --chown=${USERNAME}:${USERNAME} openhands-workspace ./openhands-workspace
|
|
49
54
|
COPY --chown=${USERNAME}:${USERNAME} openhands-agent-server ./openhands-agent-server
|
|
50
55
|
RUN --mount=type=cache,target=/home/${USERNAME}/.cache,uid=${UID},gid=${GID} \
|
|
56
|
+
EXTRA_FLAGS=""; \
|
|
57
|
+
if [ "$ENABLE_VERTEX" = "1" ]; then EXTRA_FLAGS="--extra vertex"; fi; \
|
|
51
58
|
uv python install 3.13 && \
|
|
52
59
|
uv venv --python-preference only-managed --python 3.13 .venv && \
|
|
53
|
-
uv sync --frozen --no-editable --managed-python --extra boto3 && \
|
|
60
|
+
uv sync --frozen --no-editable --managed-python --extra boto3 $EXTRA_FLAGS && \
|
|
54
61
|
readlink -f .venv/bin/python | grep -q '^/agent-server/uv-managed-python/'
|
|
55
62
|
|
|
56
63
|
####################################################################################
|
|
@@ -58,11 +65,13 @@ RUN --mount=type=cache,target=/home/${USERNAME}/.cache,uid=${UID},gid=${GID} \
|
|
|
58
65
|
# We run pyinstaller here to produce openhands-agent-server
|
|
59
66
|
####################################################################################
|
|
60
67
|
FROM builder AS binary-builder
|
|
61
|
-
ARG USERNAME UID GID
|
|
68
|
+
ARG USERNAME UID GID ENABLE_VERTEX
|
|
62
69
|
|
|
63
70
|
# We need --dev for pyinstaller
|
|
64
71
|
RUN --mount=type=cache,target=/home/${USERNAME}/.cache,uid=${UID},gid=${GID} \
|
|
65
|
-
|
|
72
|
+
EXTRA_FLAGS=""; \
|
|
73
|
+
if [ "$ENABLE_VERTEX" = "1" ]; then EXTRA_FLAGS="--extra vertex"; fi; \
|
|
74
|
+
uv sync --frozen --dev --no-editable --extra boto3 $EXTRA_FLAGS
|
|
66
75
|
|
|
67
76
|
RUN --mount=type=cache,target=/home/${USERNAME}/.cache,uid=${UID},gid=${GID} \
|
|
68
77
|
uv run pyinstaller openhands-agent-server/openhands/agent_server/agent-server.spec
|
|
@@ -6,6 +6,8 @@ from datetime import datetime
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from uuid import UUID, uuid4
|
|
8
8
|
|
|
9
|
+
from pydantic import ValidationError
|
|
10
|
+
|
|
9
11
|
from openhands.agent_server.conversation_lease import (
|
|
10
12
|
ConversationLease,
|
|
11
13
|
ConversationOwnershipLostError,
|
|
@@ -20,6 +22,7 @@ from openhands.agent_server.pub_sub import PubSub, Subscriber
|
|
|
20
22
|
from openhands.sdk import LLM, AgentBase, Event, Message, TextContent, get_logger
|
|
21
23
|
from openhands.sdk.agent import ACPAgent
|
|
22
24
|
from openhands.sdk.conversation.base import BaseConversation
|
|
25
|
+
from openhands.sdk.conversation.events_list_base import EventsListBase
|
|
23
26
|
from openhands.sdk.conversation.goal import (
|
|
24
27
|
GoalController,
|
|
25
28
|
GoalDone,
|
|
@@ -217,6 +220,18 @@ class EventService:
|
|
|
217
220
|
return False
|
|
218
221
|
return True
|
|
219
222
|
|
|
223
|
+
def _get_searchable_event(self, events: EventsListBase, index: int) -> Event | None:
|
|
224
|
+
try:
|
|
225
|
+
return events[index]
|
|
226
|
+
except (FileNotFoundError, UnicodeDecodeError, ValidationError) as exc:
|
|
227
|
+
logger.warning(
|
|
228
|
+
"Skipping unreadable event at index %d for conversation %s (%s)",
|
|
229
|
+
index,
|
|
230
|
+
self.stored.id,
|
|
231
|
+
type(exc).__name__,
|
|
232
|
+
)
|
|
233
|
+
return None
|
|
234
|
+
|
|
220
235
|
def _search_events_sync(
|
|
221
236
|
self,
|
|
222
237
|
page_id: str | None = None,
|
|
@@ -272,7 +287,8 @@ class EventService:
|
|
|
272
287
|
start_index = None
|
|
273
288
|
else:
|
|
274
289
|
for i in range(total):
|
|
275
|
-
|
|
290
|
+
event = self._get_searchable_event(events, i)
|
|
291
|
+
if event is not None and event.id == page_id:
|
|
276
292
|
start_index = i
|
|
277
293
|
break
|
|
278
294
|
if start_index is None:
|
|
@@ -286,7 +302,9 @@ class EventService:
|
|
|
286
302
|
items: list[Event] = []
|
|
287
303
|
next_page_id: str | None = None
|
|
288
304
|
for i in indices:
|
|
289
|
-
event = events
|
|
305
|
+
event = self._get_searchable_event(events, i)
|
|
306
|
+
if event is None:
|
|
307
|
+
continue
|
|
290
308
|
if not self._event_matches_filters(
|
|
291
309
|
event, kind, source, body, timestamp_gte_str, timestamp_lt_str
|
|
292
310
|
):
|
|
@@ -360,7 +378,10 @@ class EventService:
|
|
|
360
378
|
timestamp_lt_str = timestamp__lt.isoformat() if timestamp__lt else None
|
|
361
379
|
|
|
362
380
|
count = 0
|
|
363
|
-
for
|
|
381
|
+
for i in range(len(events)):
|
|
382
|
+
event = self._get_searchable_event(events, i)
|
|
383
|
+
if event is None:
|
|
384
|
+
continue
|
|
364
385
|
if self._event_matches_filters(
|
|
365
386
|
event, kind, source, body, timestamp_gte_str, timestamp_lt_str
|
|
366
387
|
):
|
|
@@ -584,10 +605,9 @@ class EventService:
|
|
|
584
605
|
from callbacks that may run in different threads. Events are emitted through
|
|
585
606
|
the conversation's normal event flow to ensure they are persisted.
|
|
586
607
|
"""
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
608
|
+
main_loop = self._main_loop
|
|
609
|
+
conversation = self._conversation
|
|
610
|
+
if main_loop and main_loop.is_running() and conversation:
|
|
591
611
|
# Wrap _on_event with lock acquisition to ensure thread-safe access
|
|
592
612
|
# to conversation state and event log during concurrent operations
|
|
593
613
|
def locked_on_event():
|
|
@@ -596,7 +616,7 @@ class EventService:
|
|
|
596
616
|
|
|
597
617
|
# Run the locked callback in an executor to ensure the event is
|
|
598
618
|
# both persisted and sent to WebSocket subscribers
|
|
599
|
-
|
|
619
|
+
main_loop.run_in_executor(None, locked_on_event)
|
|
600
620
|
|
|
601
621
|
def _setup_llm_log_streaming(self, agent: AgentBase) -> None:
|
|
602
622
|
"""Configure LLM log callbacks to stream logs via events."""
|
|
@@ -612,13 +632,16 @@ class EventService:
|
|
|
612
632
|
filename: str, log_data: str, uid=usage_id, model=model_name
|
|
613
633
|
) -> None:
|
|
614
634
|
"""Callback to emit LLM completion logs as events."""
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
635
|
+
try:
|
|
636
|
+
event = LLMCompletionLogEvent(
|
|
637
|
+
filename=filename,
|
|
638
|
+
log_data=log_data,
|
|
639
|
+
model_name=model,
|
|
640
|
+
usage_id=uid,
|
|
641
|
+
)
|
|
642
|
+
self._emit_event_from_thread(event)
|
|
643
|
+
except Exception:
|
|
644
|
+
logger.exception("Failed to emit LLM completion log event")
|
|
622
645
|
|
|
623
646
|
llm.telemetry.set_log_completions_callback(log_callback)
|
|
624
647
|
|