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.
Files changed (67) hide show
  1. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/PKG-INFO +1 -1
  2. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/agent_profiles_router.py +10 -9
  3. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/api.py +24 -27
  4. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/bash_router.py +20 -6
  5. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/conversation_service.py +24 -9
  6. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/dependencies.py +35 -32
  7. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/docker/Dockerfile +13 -4
  8. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/event_service.py +38 -15
  9. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/file_router.py +30 -16
  10. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/init_router.py +17 -0
  11. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/openai/router.py +17 -18
  12. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/openai/service.py +7 -4
  13. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/persistence/__init__.py +4 -0
  14. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/persistence/store.py +61 -0
  15. openhands_agent_server-1.29.3/openhands/agent_server/plugins_router.py +333 -0
  16. openhands_agent_server-1.29.3/openhands/agent_server/plugins_service.py +295 -0
  17. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/profiles_router.py +10 -10
  18. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/sockets.py +44 -6
  19. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands_agent_server.egg-info/PKG-INFO +1 -1
  20. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands_agent_server.egg-info/SOURCES.txt +4 -0
  21. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/pyproject.toml +1 -1
  22. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/__init__.py +0 -0
  23. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/__main__.py +0 -0
  24. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/_secrets_exposure.py +0 -0
  25. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/auth_router.py +0 -0
  26. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/bash_service.py +0 -0
  27. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/config.py +0 -0
  28. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/conversation_lease.py +0 -0
  29. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/conversation_router.py +0 -0
  30. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/desktop_router.py +0 -0
  31. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/desktop_service.py +0 -0
  32. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/docker/build.py +0 -0
  33. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/docker/wallpaper.svg +0 -0
  34. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/env_parser.py +0 -0
  35. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/event_router.py +0 -0
  36. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/git_router.py +0 -0
  37. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/hooks_router.py +0 -0
  38. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/hooks_service.py +0 -0
  39. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/llm_router.py +0 -0
  40. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/logging_config.py +0 -0
  41. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/mcp_router.py +0 -0
  42. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/middleware.py +0 -0
  43. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/models.py +0 -0
  44. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/openai/__init__.py +0 -0
  45. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/openai/models.py +0 -0
  46. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/openapi.py +0 -0
  47. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/persistence/models.py +0 -0
  48. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/pub_sub.py +0 -0
  49. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/py.typed +0 -0
  50. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/server_details_router.py +0 -0
  51. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/settings_router.py +0 -0
  52. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/skills_router.py +0 -0
  53. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/skills_service.py +0 -0
  54. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/tool_preload_service.py +0 -0
  55. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/tool_router.py +0 -0
  56. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/utils.py +0 -0
  57. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
  58. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
  59. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/vscode_router.py +0 -0
  60. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/vscode_service.py +0 -0
  61. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/workspace_router.py +0 -0
  62. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands/agent_server/workspaces_router.py +0 -0
  63. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
  64. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands_agent_server.egg-info/entry_points.txt +0 -0
  65. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands_agent_server.egg-info/requires.txt +0 -0
  66. {openhands_agent_server-1.29.0 → openhands_agent_server-1.29.3}/openhands_agent_server.egg-info/top_level.txt +0 -0
  67. {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.0
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 = AgentProfileStore()
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 = AgentProfileStore()
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 = AgentProfileStore()
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 = AgentProfileStore()
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 = AgentProfileStore()
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 = AgentProfileStore()
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 = AgentProfileStore()
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 = LLMProfileStore()
596
+ llm_store = get_llm_profile_store()
596
597
  return resolve_agent_profile_dry_run(
597
598
  profile,
598
599
  llm_store=llm_store,
@@ -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
- create_session_api_key_dependency,
33
- create_workspace_session_dependency,
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
- create_openai_api_key_dependency,
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
- get_default_bash_event_service().run_retention_cleanup_loop(
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, config: Config) -> None:
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
- dependencies = []
334
- if config.session_api_keys:
335
- dependencies.append(Depends(create_session_api_key_dependency(config)))
336
- # Dormant gate: when ``deferred_init`` is True this 503s every /api/*
337
- # route until POST /api/init completes. No-op for non-deferred deployments.
338
- dependencies.append(Depends(require_initialized))
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
- openai_dependencies = []
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
- workspace_dependencies = []
373
- if config.session_api_keys:
374
- workspace_dependencies.append(
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, config)
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 get_default_bash_event_service
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(event_id: str) -> BashEventBase:
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(request: ExecuteBashRequest) -> BashCommand:
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(request: ExecuteBashRequest) -> BashOutput:
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() -> dict[str, int]:
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.sdk.llm.llm_profile_store import LLMProfileStore
265
- from openhands.sdk.profiles.agent_profile_store import AgentProfileStore
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 = AgentProfileStore()
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 = LLMProfileStore()
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.sdk.llm.llm_profile_store import LLMProfileStore
1349
+ from openhands.agent_server.persistence.store import (
1350
+ get_llm_profile_store,
1351
+ )
1347
1352
 
1348
- profile_store = LLMProfileStore()
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 asyncio.sleep(self.spec.retry_delay)
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 asyncio.sleep(self.spec.flush_delay)
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 asyncio.sleep(self.spec.retry_delay)
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 create_session_api_key_dependency(config: Config):
24
- """Create a session API key dependency with the given config."""
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
- def check_session_api_key(
27
- session_api_key: str | None = Depends(_SESSION_API_KEY_HEADER),
28
- ):
29
- """Check the session API key and throw an exception if incorrect. Having this as
30
- a dependency means it appears in OpenAPI Docs
31
- """
32
- if config.session_api_keys and session_api_key not in config.session_api_keys:
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 create_workspace_session_dependency(config: Config):
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
- def check_workspace_session(
51
- header_key: str | None = Depends(_SESSION_API_KEY_HEADER),
52
- cookie_key: str | None = Depends(_WORKSPACE_SESSION_COOKIE),
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
- for candidate in (header_key, cookie_key):
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
- uv sync --frozen --dev --no-editable --extra boto3
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
- if events[i].id == page_id:
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[i]
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 event in events:
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
- if self._main_loop and self._main_loop.is_running() and self._conversation:
588
- # Capture conversation reference for closure
589
- conversation = self._conversation
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
- self._main_loop.run_in_executor(None, locked_on_event)
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
- event = LLMCompletionLogEvent(
616
- filename=filename,
617
- log_data=log_data,
618
- model_name=model,
619
- usage_id=uid,
620
- )
621
- self._emit_event_from_thread(event)
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