openhands-sdk 1.54.0__tar.gz → 1.55.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 (328) hide show
  1. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/PKG-INFO +1 -1
  2. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/base.py +4 -1
  3. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/impl/local_conversation.py +25 -9
  4. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/impl/remote_conversation.py +0 -2
  5. openhands_sdk-1.55.0/openhands/sdk/conversation/message_request.py +19 -0
  6. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/request.py +57 -39
  7. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/extensions/fetch.py +32 -10
  8. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/extensions/installation/manager.py +57 -4
  9. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/git/cached_repo.py +20 -5
  10. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/git/git_diff.py +8 -2
  11. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/git/utils.py +17 -3
  12. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/launch/finalize.py +25 -2
  13. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/launch/preview.py +11 -3
  14. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/launch/resolve.py +47 -9
  15. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/auth/openai.py +10 -1
  16. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/utils/model_features.py +3 -0
  17. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/utils/telemetry.py +32 -6
  18. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/utils/unverified_models.py +8 -5
  19. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/utils/verified_models.py +14 -9
  20. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/mcp/client.py +4 -0
  21. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/plugin/format/agent_plugins.py +14 -5
  22. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/profiles/agent_profile.py +16 -0
  23. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/profiles/resolver.py +88 -47
  24. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/profiles/seed.py +1 -5
  25. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/security/_shell_ast.py +61 -0
  26. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/security/defense_in_depth/pattern.py +18 -9
  27. openhands_sdk-1.55.0/openhands/sdk/security/defense_in_depth/policy_rails.py +928 -0
  28. openhands_sdk-1.55.0/openhands/sdk/security/defense_in_depth/shell_semantics.py +1506 -0
  29. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/settings/acp_providers.py +7 -4
  30. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/settings/model.py +1 -1
  31. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/skills/skill.py +27 -10
  32. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/skills/utils.py +6 -1
  33. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/subagent/registry.py +29 -19
  34. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/subagent/schema.py +4 -1
  35. openhands_sdk-1.55.0/openhands/sdk/subagent/scope.py +129 -0
  36. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/tool/tool.py +3 -0
  37. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/utils/redact.py +13 -29
  38. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands_sdk.egg-info/PKG-INFO +1 -1
  39. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands_sdk.egg-info/SOURCES.txt +2 -0
  40. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/pyproject.toml +1 -1
  41. openhands_sdk-1.54.0/openhands/sdk/security/defense_in_depth/policy_rails.py +0 -185
  42. openhands_sdk-1.54.0/openhands/sdk/security/defense_in_depth/shell_semantics.py +0 -515
  43. openhands_sdk-1.54.0/openhands/sdk/subagent/scope.py +0 -81
  44. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/__init__.py +0 -0
  45. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/agent/__init__.py +0 -0
  46. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/agent/acp_agent.py +0 -0
  47. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/agent/acp_contracts.py +0 -0
  48. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/agent/acp_file_credentials.py +0 -0
  49. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/agent/acp_model_discovery.py +0 -0
  50. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/agent/acp_models.py +0 -0
  51. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/agent/acp_tracing.py +0 -0
  52. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/agent/agent.py +0 -0
  53. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/agent/base.py +0 -0
  54. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/agent/critic_mixin.py +0 -0
  55. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/agent/parallel_executor.py +0 -0
  56. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/agent/response_dispatch.py +0 -0
  57. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/agent/stream_context.py +0 -0
  58. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/agent/utils.py +0 -0
  59. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/automation/__init__.py +0 -0
  60. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/banner.py +0 -0
  61. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/__init__.py +0 -0
  62. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/agent_context.py +0 -0
  63. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/condenser/__init__.py +0 -0
  64. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/condenser/base.py +0 -0
  65. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/condenser/llm_summarizing_condenser.py +0 -0
  66. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/condenser/no_op_condenser.py +0 -0
  67. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/condenser/pipeline_condenser.py +0 -0
  68. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/condenser/prompts/summarizing_events.j2 +0 -0
  69. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/condenser/prompts/summarizing_system.j2 +0 -0
  70. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/condenser/utils.py +0 -0
  71. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/memory.py +0 -0
  72. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/prompts/__init__.py +0 -0
  73. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/prompts/presets.py +0 -0
  74. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/prompts/prompt.py +0 -0
  75. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/prompts/registry.py +0 -0
  76. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/prompts/section.py +0 -0
  77. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/prompts/sections/__init__.py +0 -0
  78. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/prompts/sections/dynamic.py +0 -0
  79. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/prompts/sections/planning.py +0 -0
  80. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/prompts/sections/static.py +0 -0
  81. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/prompts/templates/ask_agent_template.j2 +0 -0
  82. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +0 -0
  83. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/view/__init__.py +0 -0
  84. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/view/manipulation_indices.py +0 -0
  85. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/view/properties/__init__.py +0 -0
  86. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/view/properties/base.py +0 -0
  87. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/view/properties/batch_atomicity.py +0 -0
  88. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/view/properties/observation_uniqueness.py +0 -0
  89. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/view/properties/tool_call_matching.py +0 -0
  90. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/view/properties/tool_loop_atomicity.py +0 -0
  91. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/context/view/view.py +0 -0
  92. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/__init__.py +0 -0
  93. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/cancellation.py +0 -0
  94. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/conversation.py +0 -0
  95. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/conversation_stats.py +0 -0
  96. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/event_store.py +0 -0
  97. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/events_list_base.py +0 -0
  98. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/exceptions.py +0 -0
  99. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/fifo_lock.py +0 -0
  100. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/goal/__init__.py +0 -0
  101. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/goal/controller.py +0 -0
  102. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/goal/judge.py +0 -0
  103. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/goal/prompts.py +0 -0
  104. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/goal/runner.py +0 -0
  105. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/impl/__init__.py +0 -0
  106. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/persistence_const.py +0 -0
  107. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/resource_lock_manager.py +0 -0
  108. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/response_utils.py +0 -0
  109. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/secret_registry.py +0 -0
  110. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/state.py +0 -0
  111. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/stuck_detector.py +0 -0
  112. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/title_utils.py +0 -0
  113. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/types.py +0 -0
  114. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/visualizer/__init__.py +0 -0
  115. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/visualizer/base.py +0 -0
  116. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/conversation/visualizer/default.py +0 -0
  117. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/credential.py +0 -0
  118. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/critic/__init__.py +0 -0
  119. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/critic/base.py +0 -0
  120. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/critic/impl/__init__.py +0 -0
  121. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/critic/impl/agent_finished.py +0 -0
  122. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/critic/impl/api/__init__.py +0 -0
  123. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/critic/impl/api/chat_template.py +0 -0
  124. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/critic/impl/api/client.py +0 -0
  125. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/critic/impl/api/critic.py +0 -0
  126. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/critic/impl/api/taxonomy.py +0 -0
  127. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/critic/impl/empty_patch.py +0 -0
  128. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/critic/impl/pass_critic.py +0 -0
  129. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/critic/result.py +0 -0
  130. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/event/__init__.py +0 -0
  131. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/event/acp_tool_call.py +0 -0
  132. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/event/base.py +0 -0
  133. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/event/condenser.py +0 -0
  134. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/event/conversation_error.py +0 -0
  135. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/event/conversation_state.py +0 -0
  136. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/event/error_classification.py +0 -0
  137. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/event/hook_execution.py +0 -0
  138. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/event/llm_completion_log.py +0 -0
  139. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/event/llm_convertible/__init__.py +0 -0
  140. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/event/llm_convertible/action.py +0 -0
  141. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/event/llm_convertible/message.py +0 -0
  142. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/event/llm_convertible/observation.py +0 -0
  143. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/event/llm_convertible/reasoning_utils.py +0 -0
  144. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/event/llm_convertible/system.py +0 -0
  145. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/event/resume_transcript.py +0 -0
  146. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/event/security.py +0 -0
  147. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/event/streaming_delta.py +0 -0
  148. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/event/token.py +0 -0
  149. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/event/types.py +0 -0
  150. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/event/user_action.py +0 -0
  151. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/extensions/__init__.py +0 -0
  152. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/extensions/installation/__init__.py +0 -0
  153. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/extensions/installation/info.py +0 -0
  154. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/extensions/installation/interface.py +0 -0
  155. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/extensions/installation/metadata.py +0 -0
  156. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/extensions/installation/utils.py +0 -0
  157. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/git/exceptions.py +0 -0
  158. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/git/git_changes.py +0 -0
  159. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/git/git_commits.py +0 -0
  160. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/git/models.py +0 -0
  161. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/hooks/__init__.py +0 -0
  162. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/hooks/config.py +0 -0
  163. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/hooks/conversation_hooks.py +0 -0
  164. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/hooks/executor.py +0 -0
  165. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/hooks/manager.py +0 -0
  166. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/hooks/types.py +0 -0
  167. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/io/__init__.py +0 -0
  168. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/io/base.py +0 -0
  169. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/io/cache.py +0 -0
  170. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/io/local.py +0 -0
  171. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/io/memory.py +0 -0
  172. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/launch/__init__.py +0 -0
  173. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/launch/errors.py +0 -0
  174. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/__init__.py +0 -0
  175. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/_message_normalization.py +0 -0
  176. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/_response_stream.py +0 -0
  177. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/_tokenizer.py +0 -0
  178. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/auth/__init__.py +0 -0
  179. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/auth/credentials.py +0 -0
  180. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/call_context.py +0 -0
  181. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/cleanup_profile.py +0 -0
  182. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/exceptions/__init__.py +0 -0
  183. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/exceptions/classifier.py +0 -0
  184. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/exceptions/mapping.py +0 -0
  185. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/exceptions/types.py +0 -0
  186. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/fallback_strategy.py +0 -0
  187. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/llm.py +0 -0
  188. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/llm_profile_store.py +0 -0
  189. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/llm_registry.py +0 -0
  190. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/llm_response.py +0 -0
  191. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/message.py +0 -0
  192. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/meta_profile_store.py +0 -0
  193. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/mixins/fn_call_converter.py +0 -0
  194. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/mixins/fn_call_examples.py +0 -0
  195. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/mixins/non_native_fc.py +0 -0
  196. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/options/__init__.py +0 -0
  197. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/options/chat_options.py +0 -0
  198. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/options/common.py +0 -0
  199. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/options/responses_options.py +0 -0
  200. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/provider_connection_store.py +0 -0
  201. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/router/__init__.py +0 -0
  202. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/router/base.py +0 -0
  203. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/router/impl/multimodal.py +0 -0
  204. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/router/impl/random.py +0 -0
  205. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/streaming.py +0 -0
  206. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/utils/image_inline.py +0 -0
  207. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/utils/image_resize.py +0 -0
  208. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/utils/litellm_provider.py +0 -0
  209. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/utils/metrics.py +0 -0
  210. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/utils/model_info.py +0 -0
  211. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/utils/model_prompt_spec.py +0 -0
  212. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/utils/openhands_provider.py +0 -0
  213. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/utils/providers/__init__.py +0 -0
  214. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/utils/providers/openrouter.py +0 -0
  215. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/utils/responses_serialization.py +0 -0
  216. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/utils/retry_mixin.py +0 -0
  217. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/utils/runtime_metadata.py +0 -0
  218. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/llm/utils/vertex_preflight.py +0 -0
  219. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/logger/__init__.py +0 -0
  220. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/logger/logger.py +0 -0
  221. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/logger/rolling.py +0 -0
  222. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/marketplace/__init__.py +0 -0
  223. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/marketplace/registration.py +0 -0
  224. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/marketplace/registry.py +0 -0
  225. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/marketplace/types.py +0 -0
  226. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/mcp/__init__.py +0 -0
  227. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/mcp/config.py +0 -0
  228. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/mcp/definition.py +0 -0
  229. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/mcp/exceptions.py +0 -0
  230. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/mcp/oauth.py +0 -0
  231. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/mcp/tool.py +0 -0
  232. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/mcp/utils.py +0 -0
  233. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/observability/__init__.py +0 -0
  234. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/observability/laminar.py +0 -0
  235. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/observability/utils.py +0 -0
  236. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/plugin/__init__.py +0 -0
  237. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/plugin/discovery.py +0 -0
  238. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/plugin/fetch.py +0 -0
  239. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/plugin/format/__init__.py +0 -0
  240. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/plugin/format/agent_plugins_mcp.py +0 -0
  241. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/plugin/format/base.py +0 -0
  242. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/plugin/format/claude_code.py +0 -0
  243. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/plugin/format/schemas/mcp-1.0.0.schema.json +0 -0
  244. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/plugin/format/schemas/plugin-1.0.0.schema.json +0 -0
  245. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/plugin/installed.py +0 -0
  246. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/plugin/loader.py +0 -0
  247. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/plugin/plugin.py +0 -0
  248. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/plugin/source.py +0 -0
  249. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/plugin/types.py +0 -0
  250. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/profiles/__init__.py +0 -0
  251. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/profiles/agent_profile_store.py +0 -0
  252. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/profiles/profile_refs.py +0 -0
  253. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/py.typed +0 -0
  254. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/secret/__init__.py +0 -0
  255. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/secret/secrets.py +0 -0
  256. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/security/__init__.py +0 -0
  257. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/security/analyzer.py +0 -0
  258. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/security/confirmation_policy.py +0 -0
  259. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/security/defense_in_depth/__init__.py +0 -0
  260. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/security/defense_in_depth/utils.py +0 -0
  261. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/security/ensemble.py +0 -0
  262. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/security/grayswan/__init__.py +0 -0
  263. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/security/grayswan/analyzer.py +0 -0
  264. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/security/grayswan/utils.py +0 -0
  265. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/security/llm_analyzer.py +0 -0
  266. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/security/risk.py +0 -0
  267. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/security/shell_parser.py +0 -0
  268. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/security/toolshield_helpers.py +0 -0
  269. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/security/toolshield_llm_analyzer.py +0 -0
  270. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/settings/__init__.py +0 -0
  271. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/settings/acp_install_catalog.py +0 -0
  272. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/settings/api_models.py +0 -0
  273. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/settings/metadata.py +0 -0
  274. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/skills/__init__.py +0 -0
  275. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/skills/exceptions.py +0 -0
  276. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/skills/execute.py +0 -0
  277. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/skills/fetch.py +0 -0
  278. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/skills/installed.py +0 -0
  279. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/skills/trigger.py +0 -0
  280. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/skills/types.py +0 -0
  281. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/subagent/__init__.py +0 -0
  282. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/subagent/load.py +0 -0
  283. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/testing/__init__.py +0 -0
  284. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/testing/test_llm.py +0 -0
  285. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/tool/__init__.py +0 -0
  286. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/tool/builtins/__init__.py +0 -0
  287. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/tool/builtins/classify_and_switch_llm.py +0 -0
  288. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/tool/builtins/finish.py +0 -0
  289. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/tool/builtins/invoke_skill.py +0 -0
  290. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/tool/builtins/switch_llm.py +0 -0
  291. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/tool/builtins/think.py +0 -0
  292. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/tool/builtins/vision_inspect.py +0 -0
  293. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/tool/client_tool.py +0 -0
  294. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/tool/defaults.py +0 -0
  295. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/tool/registry.py +0 -0
  296. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/tool/schema.py +0 -0
  297. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/tool/spec.py +0 -0
  298. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/utils/__init__.py +0 -0
  299. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/utils/async_executor.py +0 -0
  300. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/utils/async_utils.py +0 -0
  301. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/utils/cipher.py +0 -0
  302. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/utils/command.py +0 -0
  303. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/utils/datetime.py +0 -0
  304. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/utils/deprecation.py +0 -0
  305. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/utils/files.py +0 -0
  306. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/utils/github.py +0 -0
  307. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/utils/json.py +0 -0
  308. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/utils/masking.py +0 -0
  309. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/utils/models.py +0 -0
  310. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/utils/paging.py +0 -0
  311. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/utils/path.py +0 -0
  312. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/utils/pydantic_secrets.py +0 -0
  313. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/utils/truncate.py +0 -0
  314. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/utils/visualize.py +0 -0
  315. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/workspace/__init__.py +0 -0
  316. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/workspace/base.py +0 -0
  317. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/workspace/local.py +0 -0
  318. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/workspace/models.py +0 -0
  319. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/workspace/remote/__init__.py +0 -0
  320. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/workspace/remote/async_remote_workspace.py +0 -0
  321. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/workspace/remote/base.py +0 -0
  322. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/workspace/remote/remote_workspace_mixin.py +0 -0
  323. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/workspace/repo.py +0 -0
  324. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands/sdk/workspace/workspace.py +0 -0
  325. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands_sdk.egg-info/dependency_links.txt +0 -0
  326. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands_sdk.egg-info/requires.txt +0 -0
  327. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/openhands_sdk.egg-info/top_level.txt +0 -0
  328. {openhands_sdk-1.54.0 → openhands_sdk-1.55.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-sdk
3
- Version: 1.54.0
3
+ Version: 1.55.0
4
4
  Summary: OpenHands SDK - Core functionality for building AI agents
5
5
  Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
6
6
  Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
@@ -444,7 +444,10 @@ class BaseConversation(ABC):
444
444
  if ``None``).
445
445
  agent: Agent for the fork. Defaults to a deep-copy of the
446
446
  source agent.
447
- title: Optional title for the forked conversation.
447
+ title: Deprecated on ``LocalConversation`` since 1.54.0 — has no
448
+ effect there and emits a ``DeprecationWarning``. Still
449
+ honoured by ``RemoteConversation`` (passed to the agent
450
+ server which stores it on ``StoredConversation``).
448
451
  tags: Optional tags for the forked conversation.
449
452
  reset_metrics: If ``True`` (default), cost/token stats start
450
453
  fresh on the fork.
@@ -112,6 +112,7 @@ from openhands.sdk.tool.builtins import InvokeSkillTool
112
112
  from openhands.sdk.tool.client_tool import ClientToolSpec
113
113
  from openhands.sdk.tool.schema import Action, Observation
114
114
  from openhands.sdk.utils.cipher import Cipher
115
+ from openhands.sdk.utils.deprecation import warn_deprecated
115
116
  from openhands.sdk.workspace import LocalWorkspace
116
117
 
117
118
 
@@ -204,6 +205,7 @@ class LocalConversation(BaseConversation):
204
205
  _plugins_loaded: bool
205
206
  _pending_hook_config: HookConfig | None # Hook config to combine with plugin hooks
206
207
  _mcp_tool_provider: MCPToolProvider
208
+ _mcp_config_from_parent: bool
207
209
  _llm_call_context: LLMCallContext
208
210
 
209
211
  def __init__(
@@ -243,6 +245,7 @@ class LocalConversation(BaseConversation):
243
245
  profile_store_dir: str | Path | None = None,
244
246
  stream_callbacks: list[StreamProgressCallbackType] | None = None,
245
247
  _parent_llm_call_context: LLMCallContext | None = None,
248
+ _mcp_config_from_parent: bool = False,
246
249
  **_: object,
247
250
  ):
248
251
  """Initialize the conversation.
@@ -305,6 +308,9 @@ class LocalConversation(BaseConversation):
305
308
  for state and EventLog storage.
306
309
  _parent_llm_call_context: Runtime LLM context inherited by an internal
307
310
  child conversation. Conversation-local identity is always replaced.
311
+ _mcp_config_from_parent: The agent's ``mcp_config`` is the resolved
312
+ config of a delegating parent. It is used as-is: no plugin MCP
313
+ servers are merged into it and no ``${VAR}`` is expanded again.
308
314
  profile_store_dir: Optional directory containing saved LLM profiles.
309
315
  Defaults to ``~/.openhands/profiles``.
310
316
  """
@@ -325,6 +331,7 @@ class LocalConversation(BaseConversation):
325
331
  self._pending_hook_config = hook_config # Will be combined with plugin hooks
326
332
  self._agent_ready = False # Agent initialized lazily after plugins loaded
327
333
  self._mcp_tool_provider = mcp_tool_provider or DefaultMCPToolProvider()
334
+ self._mcp_config_from_parent = _mcp_config_from_parent
328
335
 
329
336
  # Create-or-resume: factory inspects BASE_STATE to decide
330
337
  desired_id = conversation_id or uuid.uuid4()
@@ -815,7 +822,10 @@ class LocalConversation(BaseConversation):
815
822
  if ``None``).
816
823
  agent: Agent for the fork. Defaults to a deep-copy of the
817
824
  source agent.
818
- title: Optional title for the forked conversation.
825
+ title: Deprecated. ``LocalConversation`` has no title field;
826
+ ``ConversationState`` does not store titles (they live on
827
+ ``StoredConversation`` in the agent-server layer). Passing a
828
+ value is accepted for backwards compatibility but has no effect.
819
829
  tags: Optional tags for the forked conversation.
820
830
  reset_metrics: If ``True`` (default), cost/token stats start
821
831
  fresh on the fork.
@@ -830,6 +840,17 @@ class LocalConversation(BaseConversation):
830
840
  Raises:
831
841
  ValueError: If ``from_event_id`` is not an event in this conversation.
832
842
  """
843
+ if title is not None:
844
+ warn_deprecated(
845
+ "LocalConversation.fork() title parameter",
846
+ deprecated_in="1.54.0",
847
+ removed_in="1.60.0",
848
+ details=(
849
+ "LocalConversation has no title field; titles are managed by "
850
+ "StoredConversation in the agent-server layer."
851
+ ),
852
+ stacklevel=2,
853
+ )
833
854
  fork_id = conversation_id or uuid.uuid4()
834
855
  # Always deep-copy the agent (supplied or source) so the fork owns
835
856
  # its own object graph. Required because __init__ binds
@@ -911,13 +932,6 @@ class LocalConversation(BaseConversation):
911
932
  )
912
933
  fork_conv._state.agent_state = copy.deepcopy(self._state.agent_state)
913
934
 
914
- # Copy title via tags if provided
915
- if title is not None:
916
- fork_conv._state.tags = {
917
- **fork_conv._state.tags,
918
- "title": title,
919
- }
920
-
921
935
  # Reset or copy metrics
922
936
  if not reset_metrics:
923
937
  fork_conv._state.stats = self._state.stats.model_copy(deep=True)
@@ -1215,7 +1229,9 @@ class LocalConversation(BaseConversation):
1215
1229
  # - This is the ONLY place where defaults are applied (plugin loading preserves
1216
1230
  # placeholders with expand_defaults=False to avoid double-expansion)
1217
1231
  # Agent Plugins servers are left literal (see expand_mcp_servers).
1218
- if merged_mcp:
1232
+ if self._mcp_config_from_parent:
1233
+ merged_mcp = dict(self.agent.mcp_config)
1234
+ elif merged_mcp:
1219
1235
  # Pass the registry's lookup method as a callback - secrets are retrieved
1220
1236
  # lazily, one at a time, only when actually referenced in the config
1221
1237
  merged_mcp = expand_mcp_servers(
@@ -1787,8 +1787,6 @@ class RemoteConversation(BaseConversation):
1787
1787
  self.agent.model_dump(context={"expose_secrets": True}),
1788
1788
  )
1789
1789
 
1790
- # Use server-returned tags (which include merged title) rather than
1791
- # the input tags, so the client-side object stays consistent.
1792
1790
  server_tags: dict[str, str] | None = fork_info.get("tags") or None
1793
1791
 
1794
1792
  return RemoteConversation(
@@ -0,0 +1,19 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from openhands.sdk.llm.message import ImageContent, Message, TextContent
6
+
7
+
8
+ class SendMessageRequest(BaseModel):
9
+ """Payload to send a message to the agent."""
10
+
11
+ role: Literal["user", "system", "assistant", "tool"] = "user"
12
+ content: list[TextContent | ImageContent] = Field(default_factory=list)
13
+ run: bool = Field(
14
+ default=False,
15
+ description="Whether the agent loop should automatically run if not running",
16
+ )
17
+
18
+ def create_message(self) -> Message:
19
+ return Message(role=self.role, content=self.content)
@@ -8,7 +8,7 @@ agent-server.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- from typing import Annotated, Any, Literal, cast
11
+ from typing import Annotated, Any, cast
12
12
  from uuid import UUID
13
13
 
14
14
  from pydantic import (
@@ -24,6 +24,9 @@ from pydantic import (
24
24
  from openhands.sdk.agent.acp_agent import ACPAgent as ACPAgent
25
25
  from openhands.sdk.agent.agent import Agent as Agent
26
26
  from openhands.sdk.agent.base import AgentBase
27
+ from openhands.sdk.conversation.message_request import (
28
+ SendMessageRequest as SendMessageRequest,
29
+ )
27
30
  from openhands.sdk.conversation.types import (
28
31
  ConversationObservabilityMetadata,
29
32
  ConversationObservabilitySpanName,
@@ -31,7 +34,6 @@ from openhands.sdk.conversation.types import (
31
34
  ConversationTags,
32
35
  )
33
36
  from openhands.sdk.hooks import HookConfig
34
- from openhands.sdk.llm.message import ImageContent, Message, TextContent
35
37
  from openhands.sdk.plugin import PluginSource
36
38
  from openhands.sdk.secret import SecretSource
37
39
  from openhands.sdk.security.analyzer import SecurityAnalyzerBase
@@ -39,6 +41,8 @@ from openhands.sdk.security.confirmation_policy import (
39
41
  ConfirmationPolicyBase,
40
42
  NeverConfirm,
41
43
  )
44
+ from openhands.sdk.settings.model import validate_agent_settings
45
+ from openhands.sdk.skills import Skill
42
46
  from openhands.sdk.subagent.schema import AgentDefinition
43
47
  from openhands.sdk.tool.client_tool import ClientToolSpec
44
48
  from openhands.sdk.utils.models import kind_of
@@ -61,20 +65,6 @@ ACPEnabledAgent = Annotated[
61
65
  # ---------------------------------------------------------------------------
62
66
 
63
67
 
64
- class SendMessageRequest(BaseModel):
65
- """Payload to send a message to the agent."""
66
-
67
- role: Literal["user", "system", "assistant", "tool"] = "user"
68
- content: list[TextContent | ImageContent] = Field(default_factory=list)
69
- run: bool = Field(
70
- default=False,
71
- description="Whether the agent loop should automatically run if not running",
72
- )
73
-
74
- def create_message(self) -> Message:
75
- return Message(role=self.role, content=self.content)
76
-
77
-
78
68
  class AgentLaunchAdditions(BaseModel):
79
69
  """Add deployment context after agent resolution."""
80
70
 
@@ -88,6 +78,30 @@ class AgentLaunchAdditions(BaseModel):
88
78
  "system-message suffix."
89
79
  ),
90
80
  )
81
+ skills: list[Skill] | None = Field(
82
+ default=None,
83
+ exclude_if=lambda value: value is None,
84
+ description=(
85
+ "Deployment-supplied skills merged into the resolved agent's "
86
+ "context. For a client that ships its own skill catalog and so "
87
+ "cannot rely on the server's: an ``agent_profile_id`` launch sends "
88
+ "no ``agent_settings``, leaving it no other way to supply them. "
89
+ "Merged by name — a skill the resolved agent already carries wins, "
90
+ "and the profile's ``disabled_skills`` deny-list still applies, so "
91
+ "this cannot reintroduce something the profile turned off. None "
92
+ "(the default) adds nothing."
93
+ ),
94
+ )
95
+ llm_profile_ref: str | None = Field(
96
+ default=None,
97
+ min_length=1,
98
+ description=(
99
+ "LLM profile to launch an OpenHands Agent Profile with instead of "
100
+ "its own `llm_profile_ref`. Recorded in `launched_agent_profile`; "
101
+ "the stored profile is not modified. Only valid with "
102
+ "`agent_profile_id`."
103
+ ),
104
+ )
91
105
 
92
106
 
93
107
  class ConversationConfig(BaseModel):
@@ -291,9 +305,10 @@ class StartConversationRequest(ConversationConfig):
291
305
  """Payload to create a new conversation.
292
306
 
293
307
  Extends :class:`ConversationConfig` with the agent source: a stored Agent
294
- Profile (``agent_profile_id``), resolved agent settings (``agent_settings``),
295
- or an ``agent`` built in code. The server builds the conversation's agent
296
- from that source at launch; this model only validates it.
308
+ Profile (``agent_profile_id``), resolved agent settings
309
+ (``agent_settings``), or an ``agent`` built in code. The
310
+ server turns the source into the conversation's agent at launch; this model
311
+ only checks that exactly one source is given.
297
312
 
298
313
  Note: the agent lives here on the *request*, deliberately not on
299
314
  ``ConversationConfig``. The persisted record (``StoredConversation``) does
@@ -313,7 +328,7 @@ class StartConversationRequest(ConversationConfig):
313
328
  default=None,
314
329
  description=(
315
330
  "Stored Agent Profile to launch. The server resolves it with its own "
316
- "stores. Mutually exclusive with `agent` and `agent_settings`."
331
+ "stores. Mutually exclusive with the other agent sources."
317
332
  ),
318
333
  )
319
334
  agent: AgentBase = Field(default=cast(AgentBase, None))
@@ -323,25 +338,26 @@ class StartConversationRequest(ConversationConfig):
323
338
  def _normalize_agent_source(cls, data: Any) -> Any:
324
339
  if not isinstance(data, dict):
325
340
  return data
326
- # ``agent`` has no nullable type of its own; a null means "not this source".
341
+ # An explicit null means "not this source"; ``agent`` has no nullable
342
+ # type of its own.
327
343
  payload = {
328
344
  name: value
329
345
  for name, value in data.items()
330
- if name != "agent" or value is not None
346
+ if value is not None or name not in ("agent", "agent_settings")
331
347
  }
332
- has_profile_id = payload.get("agent_profile_id") is not None
333
- has_agent = payload.get("agent") is not None
334
- has_agent_settings = payload.get("agent_settings") is not None
335
- if has_profile_id and (has_agent or has_agent_settings):
348
+ other_sources = [
349
+ name
350
+ for name in ("agent", "agent_settings")
351
+ if payload.get(name) is not None
352
+ ]
353
+ if payload.get("agent_profile_id") is not None and other_sources:
336
354
  raise ValueError(
337
- "`agent_profile_id` is mutually exclusive with"
338
- " `agent` and `agent_settings`"
355
+ "`agent_profile_id` is mutually exclusive with "
356
+ + ", ".join(f"`{name}`" for name in other_sources)
339
357
  )
340
- if has_agent:
358
+ if payload.get("agent") is not None:
341
359
  payload["agent_settings"] = None
342
- if not has_agent and has_agent_settings:
343
- from openhands.sdk.settings.model import validate_agent_settings
344
-
360
+ if payload.get("agent") is None and payload.get("agent_settings") is not None:
345
361
  try:
346
362
  validate_agent_settings(payload["agent_settings"])
347
363
  except (TypeError, ValueError) as exc:
@@ -355,14 +371,16 @@ class StartConversationRequest(ConversationConfig):
355
371
 
356
372
  @model_validator(mode="after")
357
373
  def _require_agent(self) -> StartConversationRequest:
358
- if (
359
- self.agent is None
360
- and self.agent_settings is None
361
- and self.agent_profile_id is None
362
- ):
374
+ has_profile = self.agent_profile_id is not None
375
+ if not has_profile and self.agent is None and self.agent_settings is None:
376
+ raise ValueError(
377
+ "One of `agent`, `agent_settings`, or `agent_profile_id` must be"
378
+ " provided"
379
+ )
380
+ additions = self.agent_launch_additions
381
+ if additions is not None and additions.llm_profile_ref and not has_profile:
363
382
  raise ValueError(
364
- "One of `agent`, `agent_settings`, or"
365
- " `agent_profile_id` must be provided"
383
+ "`agent_launch_additions.llm_profile_ref` requires `agent_profile_id`"
366
384
  )
367
385
  return self
368
386
 
@@ -23,7 +23,7 @@ class SourceType(StrEnum):
23
23
 
24
24
  LOCAL -- a filesystem path (absolute, home-relative, or dot-relative).
25
25
  GIT -- any git-clonable URL (HTTPS, SSH, git://, etc.).
26
- GITHUB -- the ``github:owner/repo`` shorthand, expanded to an HTTPS URL.
26
+ GITHUB -- the ``github:owner/repo[/path]`` shorthand, expanded to an HTTPS URL.
27
27
  """
28
28
 
29
29
  LOCAL = "local"
@@ -31,12 +31,30 @@ class SourceType(StrEnum):
31
31
  GITHUB = "github"
32
32
 
33
33
 
34
+ def _split_github_shorthand(source: str) -> tuple[str, str | None]:
35
+ """Split ``github:owner/repo[/subpath]`` into ``("owner/repo", subpath)``.
36
+
37
+ Raises:
38
+ ExtensionFetchError: If owner or repo is missing.
39
+ """
40
+ owner, _, rest = source.strip()[len("github:") :].partition("/")
41
+ repo, _, subpath = rest.partition("/")
42
+ if not owner or not repo:
43
+ raise ExtensionFetchError(
44
+ f"Invalid GitHub shorthand format: {source}. "
45
+ f"Expected format: github:owner/repo[/path]"
46
+ )
47
+ return f"{owner}/{repo}", subpath.strip("/") or None
48
+
49
+
34
50
  def parse_extension_source(source: str) -> tuple[SourceType, str]:
35
51
  """Parse extension source into (SourceType, url).
36
52
 
37
53
  Args:
38
54
  source: Extension source string. Can be:
39
55
  - "github:owner/repo" - GitHub repository shorthand
56
+ - "github:owner/repo/path" - the same, naming a subdirectory
57
+ (fetch() uses the path as ``repo_path``)
40
58
  - "https://github.com/owner/repo.git" - Full git URL
41
59
  - "git@github.com:owner/repo.git" - SSH git URL
42
60
  - "/local/path" - Local path
@@ -57,16 +75,10 @@ def parse_extension_source(source: str) -> tuple[SourceType, str]:
57
75
  """
58
76
  source = source.strip()
59
77
 
60
- # GitHub shorthand: github:owner/repo
78
+ # GitHub shorthand: github:owner/repo[/subpath]
61
79
  if source.startswith("github:"):
62
- repo_path = source[7:] # Remove "github:" prefix
63
- # Validate format
64
- if "/" not in repo_path or repo_path.count("/") > 1:
65
- raise ExtensionFetchError(
66
- f"Invalid GitHub shorthand format: {source}. "
67
- f"Expected format: github:owner/repo"
68
- )
69
- url = f"https://github.com/{repo_path}.git"
80
+ repo, _ = _split_github_shorthand(source)
81
+ url = f"https://github.com/{repo}.git"
70
82
  return (SourceType.GITHUB, url)
71
83
 
72
84
  # Git URLs: detect by protocol/scheme rather than enumerating providers
@@ -194,6 +206,16 @@ def fetch_with_resolution(
194
206
  """
195
207
  source_type, url = parse_extension_source(source)
196
208
 
209
+ if source_type == SourceType.GITHUB:
210
+ _, subpath = _split_github_shorthand(source)
211
+ if subpath is not None:
212
+ if repo_path:
213
+ raise ExtensionFetchError(
214
+ f"GitHub shorthand {source} already names a subdirectory; "
215
+ "do not also pass repo_path"
216
+ )
217
+ repo_path = subpath
218
+
197
219
  if source_type == SourceType.LOCAL:
198
220
  base_path = _resolve_local_source(url)
199
221
  return _apply_subpath(base_path, repo_path, f"local source '{source}'"), None
@@ -1,10 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import shutil
4
+ import tempfile
4
5
  from dataclasses import dataclass
5
6
  from pathlib import Path
6
7
 
7
- from openhands.sdk.extensions.fetch import fetch_with_resolution
8
+ from openhands.sdk.extensions.fetch import (
9
+ SourceType,
10
+ fetch_with_resolution,
11
+ parse_extension_source,
12
+ )
8
13
  from openhands.sdk.extensions.installation.info import InstallationInfo
9
14
  from openhands.sdk.extensions.installation.interface import (
10
15
  ExtensionProtocol,
@@ -15,6 +20,7 @@ from openhands.sdk.extensions.installation.metadata import (
15
20
  MetadataSession,
16
21
  )
17
22
  from openhands.sdk.extensions.installation.utils import validate_extension_name
23
+ from openhands.sdk.git.utils import extract_repo_name
18
24
  from openhands.sdk.logger import get_logger
19
25
  from openhands.sdk.utils.path import get_user_persistence_dir
20
26
  from openhands.sdk.utils.redact import redact_url_credentials
@@ -82,6 +88,23 @@ class InstallationManager[T: ExtensionProtocol]:
82
88
  FileExistsError: If extension is already installed and force=False.
83
89
  ValueError: If the extension name is invalid.
84
90
  """
91
+ return self._install(source, ref=ref, repo_path=repo_path, force=force)
92
+
93
+ def _install(
94
+ self,
95
+ source: str | Path,
96
+ ref: str | None = None,
97
+ repo_path: str | None = None,
98
+ force: bool = False,
99
+ root_name: str | None = None,
100
+ ) -> InstallationInfo:
101
+ """Install as ``install`` does; ``root_name`` names a repository root.
102
+
103
+ ``root_name`` replaces the repository's name as the directory name a
104
+ remote source without ``repo_path`` is loaded from (see
105
+ ``_load_fetched``). ``update`` passes the installed name, so an
106
+ extension installed under another name keeps it.
107
+ """
85
108
  if isinstance(source, Path):
86
109
  source = str(source)
87
110
 
@@ -94,7 +117,7 @@ class InstallationManager[T: ExtensionProtocol]:
94
117
  update=True,
95
118
  )
96
119
 
97
- extension = self.installation_interface.load_from_dir(fetched_path)
120
+ extension = self._load_fetched(fetched_path, source, repo_path, root_name)
98
121
  validate_extension_name(extension.name)
99
122
 
100
123
  install_path = self.installation_dir / extension.name
@@ -132,6 +155,34 @@ class InstallationManager[T: ExtensionProtocol]:
132
155
  )
133
156
  return info
134
157
 
158
+ def _load_fetched(
159
+ self,
160
+ fetched_path: Path,
161
+ source: str,
162
+ repo_path: str | None,
163
+ root_name: str | None = None,
164
+ ) -> T:
165
+ """Load the fetched extension, named after its repository if it is one.
166
+
167
+ A remote source without ``repo_path`` is fetched to the root of its
168
+ clone cache, ``<repo>-<hash>``. A loader that infers the name from the
169
+ directory (a plugin without a manifest) must see ``<repo>`` instead, as
170
+ it would for a local directory, so load from a copy with that name.
171
+ """
172
+ source_type, url = parse_extension_source(source)
173
+ if repo_path or source_type == SourceType.LOCAL:
174
+ return self.installation_interface.load_from_dir(fetched_path)
175
+
176
+ with tempfile.TemporaryDirectory() as staging:
177
+ load_path = Path(staging) / (root_name or extract_repo_name(url))
178
+ shutil.copytree(
179
+ fetched_path,
180
+ load_path,
181
+ symlinks=True,
182
+ ignore=shutil.ignore_patterns(".git"),
183
+ )
184
+ return self.installation_interface.load_from_dir(load_path)
185
+
135
186
  def uninstall(self, name: str) -> bool:
136
187
  """Uninstall an extension by name.
137
188
 
@@ -297,7 +348,8 @@ class InstallationManager[T: ExtensionProtocol]:
297
348
  Re-fetches the extension from its original source with ``ref=None``
298
349
  (i.e. the latest available) and force-reinstalls it. The previous
299
350
  ``enabled`` state is preserved because ``install(force=True)``
300
- carries it over.
351
+ carries it over. A repository root without a manifest keeps its
352
+ installed name.
301
353
 
302
354
  Args:
303
355
  name: Name of the extension to update.
@@ -319,9 +371,10 @@ class InstallationManager[T: ExtensionProtocol]:
319
371
 
320
372
  redacted = redact_url_credentials(current_info.source)
321
373
  logger.info(f"Updating extension {name} from {redacted}")
322
- return self.install(
374
+ return self._install(
323
375
  source=current_info.source,
324
376
  ref=None,
325
377
  repo_path=current_info.repo_path,
326
378
  force=True,
379
+ root_name=name,
327
380
  )
@@ -201,6 +201,7 @@ def try_cached_clone_or_update(
201
201
  update: bool = True,
202
202
  git_helper: GitHelper | None = None,
203
203
  lock_timeout: float = DEFAULT_LOCK_TIMEOUT,
204
+ strict: bool = False,
204
205
  ) -> Path | None:
205
206
  """Clone or update a git repository in a cache directory.
206
207
 
@@ -227,6 +228,9 @@ def try_cached_clone_or_update(
227
228
  update: If True and repo exists, fetch and update it. If False, skip fetch.
228
229
  git_helper: GitHelper instance for git operations. If None, creates one.
229
230
  lock_timeout: Timeout in seconds for acquiring the lock. Default is 5 minutes.
231
+ strict: If True, an update whose fetch (or checkout of ``ref`` after the
232
+ fetch) fails returns None instead of the stale cached repository.
233
+ The cached repository is left on disk either way.
230
234
 
231
235
  Returns:
232
236
  Path to the local repository if successful, None on failure.
@@ -243,7 +247,7 @@ def try_cached_clone_or_update(
243
247
 
244
248
  try:
245
249
  with lock.acquire(timeout=lock_timeout):
246
- return _do_clone_or_update(url, repo_path, ref, update, git)
250
+ return _do_clone_or_update(url, repo_path, ref, update, git, strict)
247
251
  except Timeout:
248
252
  logger.warning(
249
253
  f"Timed out waiting for lock on {repo_path} after {lock_timeout}s"
@@ -263,6 +267,7 @@ def _do_clone_or_update(
263
267
  ref: str | None,
264
268
  update: bool,
265
269
  git: GitHelper,
270
+ strict: bool = False,
266
271
  ) -> Path:
267
272
  """Perform the actual clone or update operation (called while holding lock).
268
273
 
@@ -272,6 +277,7 @@ def _do_clone_or_update(
272
277
  ref: Branch, tag, or commit to checkout.
273
278
  update: Whether to update existing repos.
274
279
  git: GitHelper instance.
280
+ strict: Whether a failed update raises instead of keeping the cache.
275
281
 
276
282
  Returns:
277
283
  Path to the repository.
@@ -282,7 +288,7 @@ def _do_clone_or_update(
282
288
  if repo_path.exists() and (repo_path / ".git").exists():
283
289
  if update:
284
290
  logger.debug(f"Updating repository at {repo_path}")
285
- _update_repository(repo_path, ref, git)
291
+ _update_repository(repo_path, ref, git, strict=strict)
286
292
  elif ref:
287
293
  logger.debug(f"Checking out ref {ref} at {repo_path}")
288
294
  _checkout_ref(repo_path, ref, git)
@@ -329,6 +335,7 @@ def _update_repository(
329
335
  repo_path: Path,
330
336
  ref: str | None,
331
337
  git: GitHelper,
338
+ strict: bool = False,
332
339
  ) -> None:
333
340
  """Update an existing cached repository to the latest remote state.
334
341
 
@@ -354,6 +361,8 @@ def _update_repository(
354
361
  ref: Branch, tag, or commit to update to. If None, uses current branch
355
362
  or falls back to the remote's default branch.
356
363
  git: GitHelper instance.
364
+ strict: If True, a failed fetch, or a failed checkout of ``ref`` after
365
+ the fetch, raises ``GitCommandError`` instead of keeping the cache.
357
366
  """
358
367
  if ref:
359
368
  # Optimistically attempt a local checkout before touching the network.
@@ -367,13 +376,19 @@ def _update_repository(
367
376
  except GitCommandError:
368
377
  pass # ref not cached locally; fall through to fetch
369
378
 
370
- # Fetch from origin - if this fails, we still have a usable (stale) cache
371
- if not _try_fetch(repo_path, git):
379
+ # Fetch from origin - if this fails, we still have a usable (stale) cache;
380
+ # strict callers need to know the update did not happen.
381
+ if strict:
382
+ git.fetch(repo_path)
383
+ elif not _try_fetch(repo_path, git):
372
384
  return
373
385
 
374
386
  # If a specific ref was requested, check it out
375
387
  if ref:
376
- _try_checkout_and_reset(repo_path, ref, git)
388
+ if strict:
389
+ _checkout_ref(repo_path, ref, git)
390
+ else:
391
+ _try_checkout_and_reset(repo_path, ref, git)
377
392
  return
378
393
 
379
394
  # No ref specified - update based on current state
@@ -108,8 +108,14 @@ def get_git_diff(relative_file_path: str | Path, ref: str | None = None) -> GitD
108
108
 
109
109
  # Get old content (from the ref)
110
110
  try:
111
- original = run_git_command(
112
- ["git", "show", f"{current_rev}:{relative_path_from_repo}"], validated_repo
111
+ # Read the blob verbatim and normalize it like the modified side below,
112
+ # so an unchanged file keeps its edge whitespace on both sides.
113
+ original = "\n".join(
114
+ run_git_command(
115
+ ["git", "show", f"{current_rev}:{relative_path_from_repo}"],
116
+ validated_repo,
117
+ strip=False,
118
+ ).splitlines()
113
119
  )
114
120
  except GitCommandError:
115
121
  logger.debug(f"No old content found for {path} at ref {current_rev}")