openhands-agent-server 1.32.0__tar.gz → 1.34.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.
Files changed (70) hide show
  1. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/PKG-INFO +1 -1
  2. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/agent_profiles_router.py +77 -52
  3. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/conversation_router.py +0 -6
  4. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/conversation_service.py +25 -12
  5. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/skills_service.py +11 -30
  6. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands_agent_server.egg-info/PKG-INFO +1 -1
  7. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/pyproject.toml +1 -1
  8. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/__init__.py +0 -0
  9. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/__main__.py +0 -0
  10. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/_secrets_exposure.py +0 -0
  11. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/api.py +0 -0
  12. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/auth_router.py +0 -0
  13. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/bash_router.py +0 -0
  14. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/bash_service.py +0 -0
  15. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/config.py +0 -0
  16. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/conversation_lease.py +0 -0
  17. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/dependencies.py +0 -0
  18. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/desktop_router.py +0 -0
  19. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/desktop_service.py +0 -0
  20. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/docker/Dockerfile +0 -0
  21. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/docker/build.py +0 -0
  22. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/docker/wallpaper.svg +0 -0
  23. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/env_parser.py +0 -0
  24. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/event_compat.py +0 -0
  25. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/event_router.py +0 -0
  26. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/event_service.py +0 -0
  27. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/file_router.py +0 -0
  28. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/git_router.py +0 -0
  29. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/hooks_router.py +0 -0
  30. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/hooks_service.py +0 -0
  31. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/init_router.py +0 -0
  32. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/llm_router.py +0 -0
  33. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/logging_config.py +0 -0
  34. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/mcp_oauth_store.py +0 -0
  35. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/mcp_router.py +0 -0
  36. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/middleware.py +0 -0
  37. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/models.py +0 -0
  38. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/openai/__init__.py +0 -0
  39. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/openai/models.py +0 -0
  40. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/openai/router.py +0 -0
  41. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/openai/service.py +0 -0
  42. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/openapi.py +0 -0
  43. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/persistence/__init__.py +0 -0
  44. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/persistence/models.py +0 -0
  45. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/persistence/store.py +0 -0
  46. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/plugins_router.py +0 -0
  47. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/plugins_service.py +0 -0
  48. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/profiles_router.py +0 -0
  49. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/pub_sub.py +0 -0
  50. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/py.typed +0 -0
  51. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/server_details_router.py +0 -0
  52. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/settings_router.py +0 -0
  53. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/skills_router.py +0 -0
  54. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/sockets.py +0 -0
  55. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/sub_agents_router.py +0 -0
  56. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/tool_preload_service.py +0 -0
  57. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/tool_router.py +0 -0
  58. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/utils.py +0 -0
  59. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/vscode_extensions/openhands-settings/extension.js +0 -0
  60. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/vscode_extensions/openhands-settings/package.json +0 -0
  61. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/vscode_router.py +0 -0
  62. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/vscode_service.py +0 -0
  63. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/workspace_router.py +0 -0
  64. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands/agent_server/workspaces_router.py +0 -0
  65. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands_agent_server.egg-info/SOURCES.txt +0 -0
  66. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands_agent_server.egg-info/dependency_links.txt +0 -0
  67. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands_agent_server.egg-info/entry_points.txt +0 -0
  68. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands_agent_server.egg-info/requires.txt +0 -0
  69. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/openhands_agent_server.egg-info/top_level.txt +0 -0
  70. {openhands_agent_server-1.32.0 → openhands_agent_server-1.34.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-agent-server
3
- Version: 1.32.0
3
+ Version: 1.34.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
@@ -19,21 +19,15 @@ from typing import Annotated, Any
19
19
  from fastapi import APIRouter, HTTPException, Path, Request, status
20
20
  from pydantic import BaseModel, Field, ValidationError
21
21
 
22
- from openhands.agent_server._secrets_exposure import (
23
- build_expose_context,
24
- get_cipher,
25
- get_config,
26
- parse_expose_secrets_header,
27
- translate_missing_cipher,
28
- )
22
+ from openhands.agent_server._secrets_exposure import get_cipher, get_config
29
23
  from openhands.agent_server.persistence import (
30
24
  PersistedSettings,
31
25
  get_agent_profile_store,
32
26
  get_llm_profile_store,
33
27
  get_settings_store,
34
28
  )
35
- from openhands.agent_server.profiles_router import MAX_PROFILES
36
- from openhands.agent_server.skills_service import discover_profile_skills_if_needed
29
+ from openhands.agent_server.profiles_router import MAX_PROFILES, _has_api_key
30
+ from openhands.agent_server.skills_service import discover_profile_skills
37
31
  from openhands.sdk.llm import LLM
38
32
  from openhands.sdk.llm.llm_profile_store import (
39
33
  ProfileLimitExceeded as LLMProfileLimitExceeded,
@@ -130,6 +124,25 @@ def _store_errors() -> Iterator[None]:
130
124
  raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
131
125
 
132
126
 
127
+ def _llm_has_real_config(llm: LLM) -> bool:
128
+ """True when ``llm`` carries real, user-provided configuration.
129
+
130
+ Distinguishes an already-working setup — a real API key, subscription auth,
131
+ or a custom endpoint — from bare SDK field defaults on a never-configured
132
+ account (``model="gpt-5.5"``, no ``api_key``). Only the former is worth
133
+ mirroring into a named ``default`` LLM profile; persisting the latter leaves
134
+ a keyless, non-functional "ghost" profile that the user never asked for and
135
+ that nothing ever cleans up (#4031).
136
+ """
137
+ if _has_api_key(llm):
138
+ return True
139
+ if llm.is_subscription or llm.auth_type == "subscription":
140
+ return True
141
+ if llm.base_url and llm.base_url.strip():
142
+ return True
143
+ return False
144
+
145
+
133
146
  def _seed_default_llm_profile(llm: LLM, cipher: Cipher | None) -> str:
134
147
  """Mirror the live LLM config into a ``SEED_PROFILE_NAME`` LLM profile.
135
148
 
@@ -141,6 +154,17 @@ def _seed_default_llm_profile(llm: LLM, cipher: Cipher | None) -> str:
141
154
  ``agent_settings.llm`` under that name so the reference resolves, unless a
142
155
  profile is already stored there (never clobber it).
143
156
 
157
+ Only a *real, pre-existing* LLM config is persisted (``_llm_has_real_config``
158
+ — an API key, subscription auth, or a custom base_url), which is exactly the
159
+ legacy/cloud migration case the backfill exists for. On a fresh,
160
+ never-configured account the live LLM is untouched SDK defaults (keyless
161
+ ``gpt-5.5``); persisting *that* only litters the profile list with a keyless,
162
+ unusable ``default`` "ghost" profile (#4031), so we skip the save and leave
163
+ ``llm_profile_ref="default"`` as a soft/dangling ref — the canvas
164
+ "LLM not configured" banner and the legible launch-time error already cover
165
+ that state, and materialize/dry-run reports it as ``dangling_llm_profile_ref``
166
+ rather than raising.
167
+
144
168
  Existence is checked via ``load()``, not ``list()``: the store resolves a
145
169
  name straight to a filesystem path, so on a case-insensitive filesystem
146
170
  (macOS/Windows) a differently-cased ``Default`` profile already occupies
@@ -164,6 +188,15 @@ def _seed_default_llm_profile(llm: LLM, cipher: Cipher | None) -> str:
164
188
  "failed to load; leaving it as-is"
165
189
  )
166
190
  return SEED_PROFILE_NAME
191
+ if not _llm_has_real_config(llm):
192
+ # Never-configured account: don't persist a keyless ghost profile.
193
+ # Leave the ref soft/dangling; the banner + launch-time error handle
194
+ # it, and it resolves for real once the user saves a profile.
195
+ logger.info(
196
+ f"Skipping default LLM profile '{SEED_PROFILE_NAME}' seed: "
197
+ "no real LLM config yet (leaving llm_profile_ref as a soft ref)"
198
+ )
199
+ return SEED_PROFILE_NAME
167
200
  try:
168
201
  llm_store.save(
169
202
  SEED_PROFILE_NAME,
@@ -213,7 +246,7 @@ def _seed_default_profile(
213
246
  settings.agent_settings.llm, cipher
214
247
  )
215
248
  profile = build_seed_profile(settings.agent_settings, active_llm_profile)
216
- store.save(profile, cipher=cipher, max_profiles=MAX_AGENT_PROFILES)
249
+ store.save(profile, max_profiles=MAX_AGENT_PROFILES)
217
250
 
218
251
  profile_id = str(profile.id)
219
252
  settings_store = get_settings_store(get_config(request))
@@ -266,31 +299,24 @@ async def list_agent_profiles(request: Request) -> AgentProfileListResponse:
266
299
 
267
300
 
268
301
  @agent_profiles_router.get("/{name}", response_model=AgentProfileDetailResponse)
269
- async def get_agent_profile(
270
- request: Request, name: ProfileName
271
- ) -> AgentProfileDetailResponse:
302
+ async def get_agent_profile(name: ProfileName) -> AgentProfileDetailResponse:
272
303
  """Get a stored profile.
273
304
 
274
- Use the ``X-Expose-Secrets`` header to control ``skills[].mcp_tools`` secret
275
- exposure (``encrypted`` / ``plaintext``); absent, those values are masked.
305
+ A profile is secret-free at rest (#4017), so there is nothing to mask or
306
+ expose — unlike the LLM ``/api/profiles`` router, this endpoint has no
307
+ ``X-Expose-Secrets`` behavior.
276
308
  """
277
- expose_mode = parse_expose_secrets_header(request)
278
- cipher = get_cipher(request)
279
-
280
309
  store = get_agent_profile_store()
281
310
  try:
282
311
  with _store_errors():
283
- profile = store.load(name, cipher=cipher)
312
+ profile = store.load(name)
284
313
  except FileNotFoundError:
285
314
  raise HTTPException(
286
315
  status_code=status.HTTP_404_NOT_FOUND,
287
316
  detail=f"Agent profile '{name}' not found",
288
317
  )
289
318
 
290
- context = build_expose_context(expose_mode, cipher)
291
- with translate_missing_cipher():
292
- payload = profile.model_dump(mode="json", context=context)
293
-
319
+ payload = profile.model_dump(mode="json")
294
320
  return AgentProfileDetailResponse(name=name, profile=payload)
295
321
 
296
322
 
@@ -300,31 +326,29 @@ async def get_agent_profile(
300
326
  status_code=status.HTTP_201_CREATED,
301
327
  )
302
328
  async def save_agent_profile(
303
- request: Request, name: ProfileName, body: dict[str, Any]
329
+ name: ProfileName, body: dict[str, Any]
304
330
  ) -> AgentProfileMutationResponse:
305
331
  """Save an ``AgentProfile`` under ``name`` (overwriting a namesake).
306
332
 
307
333
  The path ``name`` is authoritative — it overrides any ``name`` in the body.
308
- With ``OH_SECRET_KEY`` configured, ``skills[].mcp_tools`` secrets are
309
- encrypted at rest; otherwise they are redacted. Returns 409 if creating a
310
- new profile would exceed ``MAX_AGENT_PROFILES``.
334
+ The profile is secret-free at rest (#4017), so no cipher/encryption is
335
+ involved. Returns 409 if creating a new profile would exceed
336
+ ``MAX_AGENT_PROFILES``.
311
337
  """
312
- cipher = get_cipher(request)
313
338
  try:
314
339
  profile = validate_agent_profile({**body, "name": name})
315
340
  except ValidationError as e:
316
- # Match FastAPI's request-validation shape (``detail`` is a list of error
317
- # objects), but surface only ``loc``/``type`` — a nested mcp_tools
318
- # FastMCP config error embeds the input (which may carry secrets) in
319
- # ``msg``.
341
+ # Match FastAPI's request-validation shape (``detail`` is a list of
342
+ # error objects): ``loc``/``type``/``msg`` (``input`` dropped — see
343
+ # safe_validation_error_detail's docstring for why msg is now safe).
320
344
  raise HTTPException(
321
345
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
322
346
  detail=safe_validation_error_detail(e),
323
347
  )
324
348
  except Exception:
325
- # Any other validation failure (e.g. SkillValidationError from a
326
- # malformed mcp_tools, or a schema/migration error) is a client error,
327
- # never a 500. Stay generic — these messages can embed the input.
349
+ # Any other validation failure (e.g. a schema/migration error) is a
350
+ # client error, never a 500. Stay generic — these messages can embed
351
+ # the input.
328
352
  raise HTTPException(
329
353
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
330
354
  detail="Invalid agent profile",
@@ -339,7 +363,7 @@ async def save_agent_profile(
339
363
  try:
340
364
  with _store_errors():
341
365
  save_profile_preserving_identity(
342
- store, profile, cipher=cipher, max_profiles=MAX_AGENT_PROFILES
366
+ store, profile, max_profiles=MAX_AGENT_PROFILES
343
367
  )
344
368
  except ProfileLimitExceeded:
345
369
  raise HTTPException(
@@ -489,36 +513,37 @@ async def materialize_agent_profile(
489
513
  than raising — the only error status is 404 (unknown profile name).
490
514
  resolved_settings is redacted (api_key_set booleans; no raw secrets).
491
515
  """
492
- cipher = get_cipher(request)
493
-
494
516
  store = get_agent_profile_store()
495
517
  try:
496
518
  with _store_errors():
497
- profile = store.load(name, cipher=cipher)
519
+ profile = store.load(name)
498
520
  except FileNotFoundError:
499
521
  raise HTTPException(
500
522
  status_code=status.HTTP_404_NOT_FOUND,
501
523
  detail=f"Agent profile '{name}' not found",
502
524
  )
503
525
 
526
+ # Still needed here (unlike the profile load above): resolve_agent_profile_
527
+ # dry_run uses it to decrypt the *referenced LLM profile's* own secret.
528
+ cipher = get_cipher(request)
504
529
  config = get_config(request)
505
530
  settings = get_settings_store(config).load() or PersistedSettings()
506
531
  mcp_config = settings.agent_settings.mcp_config
507
532
 
508
- # Discover skills off the event loop so the dry-run can report which
509
- # ``skill_refs`` resolve vs. dangle. A discovery failure must not 500 the
510
- # preview: pass ``available_skills=None`` (catalog unknown — the dry-run then
511
- # reports nothing dangling, rather than flagging every selected skill) and
512
- # surface the failure as its own diagnostic below.
533
+ # Discover skills off the event loop so the dry-run can report which skills
534
+ # (catalog minus ``disabled_skills``) resolve. Only OpenHands profiles carry
535
+ # user/public skills; ACP profiles do not. A discovery failure must not 500
536
+ # the preview: pass ``available_skills=None`` and surface the failure as its
537
+ # own diagnostic below.
513
538
  discovery_error: str | None = None
514
- try:
515
- available_skills = await asyncio.to_thread(
516
- discover_profile_skills_if_needed, profile
517
- )
518
- except Exception as exc:
519
- available_skills = None
520
- discovery_error = str(exc)
521
- logger.warning("Skill discovery failed during materialize: %s", exc)
539
+ available_skills = None
540
+ if profile.agent_kind == "openhands":
541
+ try:
542
+ available_skills = await asyncio.to_thread(discover_profile_skills)
543
+ except Exception as exc:
544
+ available_skills = None
545
+ discovery_error = str(exc)
546
+ logger.warning("Skill discovery failed during materialize: %s", exc)
522
547
 
523
548
  llm_store = get_llm_profile_store()
524
549
  diagnostics = resolve_agent_profile_dry_run(
@@ -51,7 +51,6 @@ from openhands.sdk.marketplace.registry import (
51
51
  from openhands.sdk.plugin import PluginFetchError
52
52
  from openhands.sdk.profiles.resolver import (
53
53
  DanglingMcpServerRef,
54
- DanglingSkillRef,
55
54
  ProfileNotFound,
56
55
  )
57
56
  from openhands.sdk.tool.client_tool import ClientToolRegistrationError
@@ -217,11 +216,6 @@ async def start_conversation(
217
216
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
218
217
  detail={"message": str(e), "dangling_mcp_server_refs": e.missing},
219
218
  ) from e
220
- except DanglingSkillRef as e:
221
- raise HTTPException(
222
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
223
- detail={"message": str(e), "dangling_skill_refs": e.missing},
224
- ) from e
225
219
  except ClientToolRegistrationError as e:
226
220
  raise HTTPException(
227
221
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e)
@@ -32,7 +32,7 @@ from openhands.agent_server.models import (
32
32
  )
33
33
  from openhands.agent_server.pub_sub import Subscriber
34
34
  from openhands.agent_server.server_details_router import update_last_execution_time
35
- from openhands.agent_server.skills_service import discover_profile_skills_if_needed
35
+ from openhands.agent_server.skills_service import discover_profile_skills
36
36
  from openhands.agent_server.utils import safe_rmtree, utc_now
37
37
  from openhands.sdk import LLM, AgentContext, Event, Message
38
38
  from openhands.sdk.agent.base import AgentBase
@@ -267,7 +267,6 @@ def _resolve_agent_from_profile(
267
267
  Raises:
268
268
  ProfileNotFound: No stored profile has ``profile_id``.
269
269
  DanglingMcpServerRef: A referenced MCP server is absent from the global config.
270
- DanglingSkillRef: A referenced skill is absent from the discovered catalog.
271
270
  ValueError: Profile load or settings validation failure.
272
271
  """
273
272
  from openhands.agent_server.persistence.store import (
@@ -275,6 +274,7 @@ def _resolve_agent_from_profile(
275
274
  get_llm_profile_store,
276
275
  )
277
276
  from openhands.sdk.profiles.resolver import ProfileNotFound, resolve_agent_profile
277
+ from openhands.sdk.settings.model import OpenHandsAgentSettings
278
278
 
279
279
  store = get_agent_profile_store()
280
280
  profile_name = store.name_for_id(profile_id)
@@ -282,7 +282,7 @@ def _resolve_agent_from_profile(
282
282
  raise ProfileNotFound(f"Agent profile with id '{profile_id}' not found")
283
283
 
284
284
  try:
285
- profile = store.load(profile_name, cipher=cipher)
285
+ profile = store.load(profile_name)
286
286
  except FileNotFoundError:
287
287
  raise ProfileNotFound(
288
288
  f"Agent profile '{profile_name}' (id={profile_id}) not found"
@@ -292,15 +292,18 @@ def _resolve_agent_from_profile(
292
292
  f"Failed to load agent profile '{profile_name}': {exc}"
293
293
  ) from exc
294
294
 
295
- # Both variants honor ``skill_refs``; the helper skips discovery when it
296
- # selects none. A genuine discovery failure fails the launch loudly rather
297
- # than silently producing a zero-skill agent.
298
- try:
299
- available_skills = discover_profile_skills_if_needed(profile)
300
- except Exception as exc:
301
- raise ValueError(
302
- f"Skill discovery failed for profile '{profile_name}': {exc}"
303
- ) from exc
295
+ # OpenHands profiles get the discovered catalog minus their ``disabled_skills``
296
+ # deny-list; ACP profiles carry no user/public skills so discovery is skipped.
297
+ # A genuine discovery failure fails the launch loudly rather than silently
298
+ # producing a zero-skill agent.
299
+ available_skills = None
300
+ if profile.agent_kind == "openhands":
301
+ try:
302
+ available_skills = discover_profile_skills()
303
+ except Exception as exc:
304
+ raise ValueError(
305
+ f"Skill discovery failed for profile '{profile_name}': {exc}"
306
+ ) from exc
304
307
 
305
308
  llm_store = get_llm_profile_store()
306
309
  try:
@@ -314,6 +317,16 @@ def _resolve_agent_from_profile(
314
317
  except (TypeError, ValueError) as exc:
315
318
  raise ValueError(f"Profile '{profile_name}' failed to resolve: {exc}") from exc
316
319
 
320
+ if isinstance(settings_config, OpenHandsAgentSettings):
321
+ # Force streaming so this launch path wires on_token: a client can't set
322
+ # llm.stream on a profile's referenced LLM ahead of time. Safe at this
323
+ # layer (not the SDK resolver) because this server wires the token
324
+ # callback whenever any llm.stream is set; a headless resolver caller
325
+ # that never wires on_token is covered by LLM's graceful degradation.
326
+ settings_config = settings_config.model_copy(
327
+ update={"llm": settings_config.llm.model_copy(update={"stream": True})}
328
+ )
329
+
317
330
  agent = settings_config.create_agent()
318
331
  # Browser is deliberately absent from the deterministic SDK default
319
332
  # (environment-dependent); this server knows its runtime, so it injects
@@ -21,7 +21,6 @@ import tempfile
21
21
  from dataclasses import dataclass
22
22
  from pathlib import Path
23
23
  from time import monotonic
24
- from typing import TYPE_CHECKING
25
24
 
26
25
  from pydantic import BaseModel, ValidationError
27
26
 
@@ -57,10 +56,6 @@ from openhands.sdk.utils import sanitized_env
57
56
  from openhands.sdk.utils.path import to_posix_path
58
57
 
59
58
 
60
- if TYPE_CHECKING:
61
- from openhands.sdk.profiles import ACPAgentProfile, OpenHandsAgentProfile
62
-
63
-
64
59
  logger = get_logger(__name__)
65
60
 
66
61
 
@@ -458,20 +453,21 @@ def load_all_skills(
458
453
 
459
454
 
460
455
  def discover_profile_skills() -> list[Skill]:
461
- """Skill catalog for ``AgentProfile.skill_refs`` resolution (#3868).
456
+ """Skill catalog an OpenHands profile launches with (#4017).
462
457
 
463
458
  Returns the merged user + public skills — the deterministic sources of
464
- :func:`load_all_skills` that ``resolve_agent_profile`` filters by name.
465
- ``load_all_skills`` already absorbs and logs benign per-source failures, so
466
- this does not swallow errors: an unexpected failure propagates rather than
467
- silently resolving the profile to a zero-skill agent.
459
+ :func:`load_all_skills`. ``resolve_agent_profile`` keeps all of them except
460
+ the profile's ``disabled_skills`` deny-list. ``load_all_skills`` already
461
+ absorbs and logs benign per-source failures, so this does not swallow errors:
462
+ an unexpected failure propagates rather than silently resolving the profile
463
+ to a zero-skill agent.
468
464
 
469
465
  Org / project skills need auth / workspace context not available at resolve
470
- time, so they are not in this catalog (a follow-up). Because the resolver
471
- now hard-fails dangling ``skill_refs`` (mirroring MCP), a profile that
472
- selects an org/project skill via the picker would fail launch here until the
473
- catalog is broadened — the SaaS app-server, which has that context, is
474
- expected to supply the fuller catalog the same way it does for the picker.
466
+ time, so they are not in this catalog. That is safe under the deny-list
467
+ model: a skill missing from the catalog is simply never disabled (it stays
468
+ on when it does load, e.g. project skills loaded lazily by
469
+ ``LocalConversation``) — the old allow-list would instead have dangled and
470
+ failed the launch.
475
471
  """
476
472
  return list(
477
473
  load_all_skills(
@@ -483,21 +479,6 @@ def discover_profile_skills() -> list[Skill]:
483
479
  )
484
480
 
485
481
 
486
- def discover_profile_skills_if_needed(
487
- profile: "OpenHandsAgentProfile | ACPAgentProfile",
488
- ) -> list[Skill] | None:
489
- """Discover the skill catalog a profile needs, or ``None`` to skip discovery.
490
-
491
- ``skill_refs == []`` selects no discovered skills, so the (potentially
492
- network-bound) discovery is skipped and the resolver receives ``None``. Any
493
- other value — ``None`` (all discovered) or a name list — needs the catalog.
494
- Centralizes the skip guard shared by conversation start and the dry-run.
495
- """
496
- if profile.skill_refs == []:
497
- return None
498
- return discover_profile_skills()
499
-
500
-
501
482
  def sync_public_skills() -> tuple[bool, str]:
502
483
  """Force refresh of public skills from GitHub repository.
503
484
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-agent-server
3
- Version: 1.32.0
3
+ Version: 1.34.0
4
4
  Summary: OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent
5
5
  Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
6
6
  Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openhands-agent-server"
3
- version = "1.32.0"
3
+ version = "1.34.0"
4
4
  description = "OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent"
5
5
 
6
6
  requires-python = ">=3.12"