openhands-sdk 1.57.0__tar.gz → 1.58.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/PKG-INFO +1 -1
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/goal/controller.py +6 -4
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/impl/local_conversation.py +14 -1
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/git/git_diff.py +43 -5
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/hooks/conversation_hooks.py +43 -1
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/meta_profile_store.py +6 -1
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/verified_models.py +3 -1
- openhands_sdk-1.58.0/openhands/sdk/mcp/oauth.py +203 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/mcp/utils.py +7 -2
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/profiles/agent_profile_store.py +16 -1
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/command.py +11 -10
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/models.py +14 -1
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/remote/base.py +2 -2
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/repo.py +52 -11
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands_sdk.egg-info/PKG-INFO +1 -1
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/pyproject.toml +1 -1
- openhands_sdk-1.57.0/openhands/sdk/mcp/oauth.py +0 -85
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/acp_agent.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/acp_contracts.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/acp_file_credentials.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/acp_model_discovery.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/acp_models.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/acp_tracing.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/agent.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/base.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/critic_mixin.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/parallel_executor.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/response_dispatch.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/stream_context.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/utils.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/automation/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/banner.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/agent_context.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/base.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/llm_summarizing_condenser.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/no_op_condenser.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/pipeline_condenser.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/prompts/summarizing_events.j2 +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/prompts/summarizing_system.j2 +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/utils.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/memory.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/presets.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/prompt.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/registry.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/section.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/sections/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/sections/dynamic.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/sections/planning.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/sections/static.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/templates/ask_agent_template.j2 +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/manipulation_indices.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/properties/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/properties/base.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/properties/batch_atomicity.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/properties/observation_uniqueness.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/properties/tool_call_matching.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/properties/tool_loop_atomicity.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/view.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/base.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/cancellation.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/conversation.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/conversation_stats.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/event_store.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/events_list_base.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/exceptions.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/fifo_lock.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/goal/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/goal/judge.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/goal/prompts.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/goal/runner.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/impl/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/impl/remote_conversation.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/message_request.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/persistence_const.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/request.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/resource_lock_manager.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/response_utils.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/secret_registry.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/state.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/stuck_detector.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/title_utils.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/types.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/visualizer/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/visualizer/base.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/visualizer/default.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/credential.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/base.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/agent_finished.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/api/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/api/chat_template.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/api/client.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/api/critic.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/api/taxonomy.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/empty_patch.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/pass_critic.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/result.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/acp_tool_call.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/base.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/condenser.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/conversation_error.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/conversation_state.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/error_classification.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/hook_execution.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/llm_completion_log.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/llm_convertible/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/llm_convertible/action.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/llm_convertible/message.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/llm_convertible/observation.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/llm_convertible/reasoning_utils.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/llm_convertible/system.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/resume_transcript.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/security.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/streaming_delta.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/token.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/types.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/user_action.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/fetch.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/installation/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/installation/info.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/installation/interface.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/installation/manager.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/installation/metadata.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/installation/utils.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/git/cached_repo.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/git/exceptions.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/git/git_changes.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/git/git_commits.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/git/models.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/git/utils.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/hooks/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/hooks/config.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/hooks/executor.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/hooks/manager.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/hooks/types.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/io/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/io/base.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/io/cache.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/io/local.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/io/memory.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/launch/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/launch/errors.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/launch/finalize.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/launch/preview.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/launch/resolve.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/_message_normalization.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/_response_stream.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/_tokenizer.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/auth/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/auth/credentials.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/auth/openai.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/call_context.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/cleanup_profile.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/exceptions/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/exceptions/classifier.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/exceptions/mapping.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/exceptions/types.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/fallback_strategy.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/llm.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/llm_profile_store.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/llm_registry.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/llm_response.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/message.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/mixins/fn_call_converter.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/mixins/fn_call_examples.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/mixins/non_native_fc.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/options/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/options/chat_options.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/options/common.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/options/responses_options.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/provider_connection_store.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/router/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/router/base.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/router/impl/multimodal.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/router/impl/random.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/streaming.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/image_inline.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/image_resize.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/litellm_provider.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/metrics.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/model_features.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/model_info.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/model_prompt_spec.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/openhands_provider.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/providers/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/providers/openrouter.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/responses_serialization.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/retry_mixin.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/runtime_metadata.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/telemetry.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/unverified_models.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/vertex_preflight.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/logger/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/logger/_rich_handler.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/logger/logger.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/logger/rolling.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/marketplace/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/marketplace/registration.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/marketplace/registry.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/marketplace/types.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/mcp/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/mcp/client.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/mcp/config.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/mcp/definition.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/mcp/exceptions.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/mcp/tool.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/observability/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/observability/laminar.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/observability/utils.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/discovery.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/fetch.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/format/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/format/agent_plugins.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/format/agent_plugins_mcp.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/format/base.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/format/claude_code.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/format/schemas/mcp-1.0.0.schema.json +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/format/schemas/plugin-1.0.0.schema.json +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/installed.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/loader.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/paths.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/plugin.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/source.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/strict_fetch.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/types.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/profiles/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/profiles/agent_profile.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/profiles/profile_refs.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/profiles/resolver.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/profiles/seed.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/py.typed +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/secret/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/secret/secrets.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/_shell_ast.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/analyzer.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/confirmation_policy.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/defense_in_depth/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/defense_in_depth/pattern.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/defense_in_depth/policy_rails.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/defense_in_depth/shell_semantics.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/defense_in_depth/utils.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/ensemble.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/grayswan/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/grayswan/analyzer.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/grayswan/utils.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/llm_analyzer.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/risk.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/shell_parser.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/toolshield_helpers.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/toolshield_llm_analyzer.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/settings/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/settings/acp_install_catalog.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/settings/acp_providers.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/settings/api_models.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/settings/metadata.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/settings/model.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/exceptions.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/execute.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/fetch.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/installed.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/skill.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/trigger.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/types.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/utils.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/subagent/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/subagent/load.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/subagent/registry.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/subagent/schema.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/subagent/scope.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/testing/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/testing/test_llm.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/builtins/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/builtins/classify_and_switch_llm.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/builtins/finish.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/builtins/invoke_skill.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/builtins/switch_llm.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/builtins/think.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/builtins/vision_inspect.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/client_tool.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/defaults.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/registry.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/schema.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/spec.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/tool.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/async_executor.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/async_utils.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/cipher.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/datetime.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/deprecation.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/files.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/github.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/json.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/masking.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/paging.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/path.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/pydantic_secrets.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/redact.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/truncate.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/visualize.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/base.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/local.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/models.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/remote/__init__.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/remote/async_remote_workspace.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/remote/remote_workspace_mixin.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/workspace.py +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands_sdk.egg-info/SOURCES.txt +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands_sdk.egg-info/dependency_links.txt +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands_sdk.egg-info/requires.txt +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands_sdk.egg-info/top_level.txt +0 -0
- {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openhands-sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.58.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
|
|
@@ -103,12 +103,14 @@ class GoalController:
|
|
|
103
103
|
def on_run_finished(self, events: Sequence[Event]) -> GoalStep:
|
|
104
104
|
"""Judge the objective after a run and decide whether to continue.
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
and returns a :class:`GoalContinue` (with a
|
|
108
|
-
:class:`GoalDone` (with a :class:`GoalOutcome`).
|
|
106
|
+
Audits ``events`` with the judge LLM, counts the round once the judge
|
|
107
|
+
has returned a verdict, and returns a :class:`GoalContinue` (with a
|
|
108
|
+
followup) or a terminal :class:`GoalDone` (with a :class:`GoalOutcome`).
|
|
109
|
+
A judge call that raises leaves the count unchanged: a failed or
|
|
110
|
+
abandoned audit is not a completed round.
|
|
109
111
|
"""
|
|
110
|
-
self.iteration += 1
|
|
111
112
|
verdict = judge_goal(self.judge_llm, self.objective, events)
|
|
113
|
+
self.iteration += 1
|
|
112
114
|
logger.info(
|
|
113
115
|
"Goal audit %d/%d: score=%.2f complete=%s",
|
|
114
116
|
self.iteration,
|
{openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/impl/local_conversation.py
RENAMED
|
@@ -446,8 +446,14 @@ class LocalConversation(BaseConversation):
|
|
|
446
446
|
# Single chokepoint: stamps parent_id (catching any event a hook
|
|
447
447
|
# swapped in downstream of _tree_stamping) and advances HEAD.
|
|
448
448
|
self._state.append_event(e)
|
|
449
|
+
|
|
450
|
+
def _track_last_user_message(e):
|
|
449
451
|
# Track user MessageEvent IDs here so hook callbacks (which may
|
|
450
452
|
# synthesize or alter user messages) are captured in one place.
|
|
453
|
+
# This runs after the caller-supplied callbacks: setting the field
|
|
454
|
+
# can emit a ConversationStateUpdateEvent through on_state_change
|
|
455
|
+
# (the agent server wires it to this chain), and that update must
|
|
456
|
+
# reach subscribers after the message that caused it.
|
|
451
457
|
if isinstance(e, MessageEvent) and e.source == "user":
|
|
452
458
|
# Track the latest real user message ID for hook-blocked checks.
|
|
453
459
|
# Stop-hook feedback is emitted with source="environment".
|
|
@@ -461,7 +467,7 @@ class LocalConversation(BaseConversation):
|
|
|
461
467
|
# list never run. The visualizer is prepended below and so still renders
|
|
462
468
|
# ahead of persist — that is local terminal output, not an announcement
|
|
463
469
|
# a client can act on.
|
|
464
|
-
composed_list = [_default_callback] + callback_list
|
|
470
|
+
composed_list = [_default_callback] + callback_list + [_track_last_user_message]
|
|
465
471
|
# Handle visualization configuration
|
|
466
472
|
if isinstance(visualizer, ConversationVisualizerBase):
|
|
467
473
|
# Use custom visualizer instance
|
|
@@ -1605,6 +1611,13 @@ class LocalConversation(BaseConversation):
|
|
|
1605
1611
|
self._state,
|
|
1606
1612
|
on_event=self._on_event,
|
|
1607
1613
|
)
|
|
1614
|
+
|
|
1615
|
+
# Deliver any SessionStart additional_context now that
|
|
1616
|
+
# init_state() has emitted the SystemPromptEvent, so the system
|
|
1617
|
+
# prompt stays first in the LLM request. The hook processor
|
|
1618
|
+
# collected it earlier during _ensure_plugins_loaded().
|
|
1619
|
+
if self._hook_processor is not None:
|
|
1620
|
+
self._hook_processor.inject_pending_additional_context()
|
|
1608
1621
|
except Exception:
|
|
1609
1622
|
self._close_runtime_tools(runtime_mcp_tools)
|
|
1610
1623
|
raise
|
|
@@ -50,6 +50,21 @@ def get_closest_git_repo(path: Path) -> Path | None:
|
|
|
50
50
|
current_path = parent
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
def _base_file_size(repo: Path, ref: str, relative_path: Path) -> int | None:
|
|
54
|
+
"""Size of the file (blob) ``relative_path`` at ``ref``, or ``None``
|
|
55
|
+
when ``ref`` has no file at that path."""
|
|
56
|
+
spec = f"{ref}:{relative_path.as_posix()}"
|
|
57
|
+
try:
|
|
58
|
+
object_type = run_git_command(
|
|
59
|
+
["git", "cat-file", "-t", spec], repo, expected_failure=True
|
|
60
|
+
)
|
|
61
|
+
if object_type != "blob":
|
|
62
|
+
return None
|
|
63
|
+
return int(run_git_command(["git", "cat-file", "-s", spec], repo))
|
|
64
|
+
except (GitCommandError, ValueError):
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
53
68
|
def get_git_diff(relative_file_path: str | Path, ref: str | None = None) -> GitDiff:
|
|
54
69
|
"""Get git diff for a single file.
|
|
55
70
|
|
|
@@ -63,20 +78,23 @@ def get_git_diff(relative_file_path: str | Path, ref: str | None = None) -> GitD
|
|
|
63
78
|
GitDiff object containing diff information
|
|
64
79
|
|
|
65
80
|
Raises:
|
|
66
|
-
GitPathError: If file is too large or
|
|
81
|
+
GitPathError: If file is too large, or exists neither on disk nor
|
|
82
|
+
at the base ref (a working-tree deletion diffs as the base text
|
|
83
|
+
against ``""``)
|
|
67
84
|
GitRepositoryError: If not in a git repository
|
|
68
85
|
GitCommandError: If git commands fail (including when ``ref`` is
|
|
69
86
|
provided but does not resolve in the repository).
|
|
70
87
|
"""
|
|
71
88
|
path = Path(os.getcwd(), relative_file_path).resolve()
|
|
89
|
+
missing_error = GitPathError(f"File does not exist: {path}")
|
|
72
90
|
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
91
|
+
# A path missing from disk may still be a working-tree deletion (or the
|
|
92
|
+
# old side of a rename) that only exists at the base ref.
|
|
93
|
+
deleted = not path.exists()
|
|
76
94
|
|
|
77
95
|
# Check file size
|
|
78
96
|
try:
|
|
79
|
-
file_size = os.path.getsize(path)
|
|
97
|
+
file_size = 0 if deleted else os.path.getsize(path)
|
|
80
98
|
if file_size > MAX_FILE_SIZE_FOR_GIT_DIFF:
|
|
81
99
|
raise GitPathError(
|
|
82
100
|
f"File too large for git diff: {file_size} bytes "
|
|
@@ -88,6 +106,8 @@ def get_git_diff(relative_file_path: str | Path, ref: str | None = None) -> GitD
|
|
|
88
106
|
# Find git repository
|
|
89
107
|
closest_git_repo = get_closest_git_repo(path)
|
|
90
108
|
if not closest_git_repo:
|
|
109
|
+
if deleted:
|
|
110
|
+
raise missing_error
|
|
91
111
|
raise GitRepositoryError(f"File is not in a git repository: {path}")
|
|
92
112
|
|
|
93
113
|
# Validate the git repository
|
|
@@ -97,6 +117,8 @@ def get_git_diff(relative_file_path: str | Path, ref: str | None = None) -> GitD
|
|
|
97
117
|
# so the per-file diff lines up with the change list it was opened from.
|
|
98
118
|
current_rev = get_valid_ref(validated_repo, override=ref, purpose="display")
|
|
99
119
|
if not current_rev:
|
|
120
|
+
if deleted:
|
|
121
|
+
raise missing_error
|
|
100
122
|
logger.warning(f"No valid git reference found for {validated_repo}")
|
|
101
123
|
return GitDiff(modified="", original="")
|
|
102
124
|
|
|
@@ -106,6 +128,18 @@ def get_git_diff(relative_file_path: str | Path, ref: str | None = None) -> GitD
|
|
|
106
128
|
except ValueError as e:
|
|
107
129
|
raise GitPathError(f"File is not within git repository: {path}") from e
|
|
108
130
|
|
|
131
|
+
if deleted:
|
|
132
|
+
base_size = _base_file_size(
|
|
133
|
+
validated_repo, current_rev, relative_path_from_repo
|
|
134
|
+
)
|
|
135
|
+
if base_size is None:
|
|
136
|
+
raise missing_error
|
|
137
|
+
if base_size > MAX_FILE_SIZE_FOR_GIT_DIFF:
|
|
138
|
+
raise GitPathError(
|
|
139
|
+
f"File too large for git diff: {base_size} bytes "
|
|
140
|
+
f"(max: {MAX_FILE_SIZE_FOR_GIT_DIFF} bytes)"
|
|
141
|
+
)
|
|
142
|
+
|
|
109
143
|
# Get old content (from the ref)
|
|
110
144
|
try:
|
|
111
145
|
# Read the blob verbatim and normalize it like the modified side below,
|
|
@@ -121,6 +155,10 @@ def get_git_diff(relative_file_path: str | Path, ref: str | None = None) -> GitD
|
|
|
121
155
|
logger.debug(f"No old content found for {path} at ref {current_rev}")
|
|
122
156
|
original = ""
|
|
123
157
|
|
|
158
|
+
if deleted:
|
|
159
|
+
logger.info(f"Generated git diff for deleted file {path}")
|
|
160
|
+
return GitDiff(modified="", original=original)
|
|
161
|
+
|
|
124
162
|
# Get new content (current file)
|
|
125
163
|
try:
|
|
126
164
|
with open(path, encoding="utf-8") as f:
|
|
@@ -15,7 +15,7 @@ from openhands.sdk.hooks.config import HookConfig
|
|
|
15
15
|
from openhands.sdk.hooks.executor import HookResult
|
|
16
16
|
from openhands.sdk.hooks.manager import HookManager
|
|
17
17
|
from openhands.sdk.hooks.types import HookEventType
|
|
18
|
-
from openhands.sdk.llm import TextContent
|
|
18
|
+
from openhands.sdk.llm import Message, TextContent
|
|
19
19
|
from openhands.sdk.logger import get_logger
|
|
20
20
|
|
|
21
21
|
|
|
@@ -66,6 +66,9 @@ class HookEventProcessor:
|
|
|
66
66
|
self.original_callback = original_callback
|
|
67
67
|
self._conversation_state: ConversationState | None = None
|
|
68
68
|
self.emit_hook_events = emit_hook_events
|
|
69
|
+
# SessionStart additional_context collected by run_session_start() and
|
|
70
|
+
# delivered later by inject_pending_additional_context(); see those methods.
|
|
71
|
+
self._pending_additional_context: list[str] = []
|
|
69
72
|
|
|
70
73
|
def set_conversation_state(self, state: "ConversationState") -> None:
|
|
71
74
|
"""Set conversation state for blocking support."""
|
|
@@ -338,6 +341,45 @@ class HookEventProcessor:
|
|
|
338
341
|
if result.error:
|
|
339
342
|
logger.warning(f"SessionStart hook error: {result.error}")
|
|
340
343
|
|
|
344
|
+
# Collect additional_context for delivery to the agent, but do not emit
|
|
345
|
+
# it here. run_session_start() runs while plugins load, before the agent
|
|
346
|
+
# emits its SystemPromptEvent, so emitting now would place a user message
|
|
347
|
+
# ahead of the system prompt and break the system-first invariant (and the
|
|
348
|
+
# prompt cache, which only applies to a leading system message). The
|
|
349
|
+
# conversation flushes it with inject_pending_additional_context() once
|
|
350
|
+
# init_state() has emitted the system prompt.
|
|
351
|
+
self._pending_additional_context.extend(
|
|
352
|
+
r.additional_context for r in results if r.additional_context
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
def inject_pending_additional_context(self) -> None:
|
|
356
|
+
"""Deliver collected SessionStart additional_context to the agent.
|
|
357
|
+
|
|
358
|
+
Emits the context as a single ``MessageEvent``. Call this after the
|
|
359
|
+
agent's ``SystemPromptEvent`` is in the log so the system prompt stays
|
|
360
|
+
first in the request, mirroring the ``UserPromptSubmit`` path (which
|
|
361
|
+
extends the existing user turn rather than prepending a new one). The
|
|
362
|
+
pending context is cleared on delivery, so re-running SessionStart hooks
|
|
363
|
+
(for example when runtime plugin hooks merge) does not deliver it twice.
|
|
364
|
+
"""
|
|
365
|
+
if not self._pending_additional_context or not self.original_callback:
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
additional_context = "\n".join(self._pending_additional_context)
|
|
369
|
+
self._pending_additional_context = []
|
|
370
|
+
logger.debug(
|
|
371
|
+
f"SessionStart hook injecting context: {additional_context[:100]}..."
|
|
372
|
+
)
|
|
373
|
+
self.original_callback(
|
|
374
|
+
MessageEvent(
|
|
375
|
+
source="environment",
|
|
376
|
+
llm_message=Message(
|
|
377
|
+
role="user",
|
|
378
|
+
content=[TextContent(text=additional_context)],
|
|
379
|
+
),
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
|
|
341
383
|
def run_session_end(self) -> None:
|
|
342
384
|
"""Run SessionEnd hooks. Call before conversation is closed."""
|
|
343
385
|
hooks = self.hook_manager.config.get_hooks_for_event(HookEventType.SESSION_END)
|
|
@@ -88,7 +88,12 @@ class MetaProfile(BaseModel):
|
|
|
88
88
|
|
|
89
89
|
@model_validator(mode="after")
|
|
90
90
|
def validate_routing_mode(self) -> MetaProfile:
|
|
91
|
-
"""
|
|
91
|
+
"""Validate direct-prompt mode: a template needs ``{{ instance_text }}``
|
|
92
|
+
and excludes classes.
|
|
93
|
+
|
|
94
|
+
A meta-profile with neither classes nor a template is accepted here so
|
|
95
|
+
persisted ones still load; ``POST /api/meta-profiles/{name}`` refuses it.
|
|
96
|
+
"""
|
|
92
97
|
if self.prompt_template is None:
|
|
93
98
|
return self
|
|
94
99
|
if re.search(r"{{\s*instance_text\s*}}", self.prompt_template) is None:
|
|
@@ -70,8 +70,9 @@ VERIFIED_MOONSHOT_MODELS = [
|
|
|
70
70
|
"kimi-for-coding",
|
|
71
71
|
]
|
|
72
72
|
|
|
73
|
-
# M3 and M2.7.
|
|
73
|
+
# M3 (incl. flash) and M2.7.
|
|
74
74
|
VERIFIED_MINIMAX_MODELS = [
|
|
75
|
+
"minimax-m3.1-flash",
|
|
75
76
|
"minimax-m3",
|
|
76
77
|
"minimax-m2.7",
|
|
77
78
|
]
|
|
@@ -172,6 +173,7 @@ VERIFIED_OPENHANDS_MODELS = [
|
|
|
172
173
|
"gpt-5.6",
|
|
173
174
|
"gpt-5.3-codex",
|
|
174
175
|
"gpt-5.2-codex",
|
|
176
|
+
"minimax-m3.1-flash",
|
|
175
177
|
"minimax-m3",
|
|
176
178
|
"minimax-m2.7",
|
|
177
179
|
"gemini-3.1-pro",
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""FastMCP OAuth client that discovers the token endpoint before refreshing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncGenerator
|
|
6
|
+
from contextlib import aclosing
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
from urllib.parse import parse_qsl
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
from fastmcp.client.auth import OAuth
|
|
12
|
+
from mcp.client.auth.utils import (
|
|
13
|
+
build_oauth_authorization_server_metadata_discovery_urls,
|
|
14
|
+
build_protected_resource_metadata_discovery_urls,
|
|
15
|
+
create_client_registration_request,
|
|
16
|
+
create_oauth_metadata_request,
|
|
17
|
+
handle_auth_metadata_response,
|
|
18
|
+
handle_protected_resource_response,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from openhands.sdk.logger import get_logger
|
|
22
|
+
from openhands.sdk.mcp.exceptions import MCPError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
logger = get_logger(__name__)
|
|
26
|
+
|
|
27
|
+
_DISCOVERY_TIMEOUT = httpx.Timeout(10.0)
|
|
28
|
+
|
|
29
|
+
_TokenEndpointAuthMethod = Literal["none", "client_secret_post", "client_secret_basic"]
|
|
30
|
+
|
|
31
|
+
# Most preferred first: a public client is never issued a secret to keep.
|
|
32
|
+
_TOKEN_ENDPOINT_AUTH_METHODS: tuple[_TokenEndpointAuthMethod, ...] = (
|
|
33
|
+
"none",
|
|
34
|
+
"client_secret_post",
|
|
35
|
+
"client_secret_basic",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _preferred_token_endpoint_auth_method(
|
|
40
|
+
supported: list[str] | None,
|
|
41
|
+
) -> _TokenEndpointAuthMethod | None:
|
|
42
|
+
for method in _TOKEN_ENDPOINT_AUTH_METHODS:
|
|
43
|
+
if method in (supported or ()):
|
|
44
|
+
return method
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _without_client_id_when_basic(request: httpx.Request) -> httpx.Request:
|
|
49
|
+
"""Drop ``client_id`` from the form body of a Basic-authenticated request.
|
|
50
|
+
|
|
51
|
+
RFC 6749 section 2.3.1 forbids combining the ``Authorization`` header with
|
|
52
|
+
body credentials, and strict servers reject the mix as using multiple
|
|
53
|
+
authentication methods.
|
|
54
|
+
"""
|
|
55
|
+
if not request.headers.get("Authorization", "").startswith("Basic "):
|
|
56
|
+
return request
|
|
57
|
+
data = dict(parse_qsl(request.content.decode(), keep_blank_values=True))
|
|
58
|
+
data.pop("client_id", None)
|
|
59
|
+
headers = request.headers.copy()
|
|
60
|
+
headers.pop("Content-Length", None)
|
|
61
|
+
return httpx.Request(request.method, request.url, data=data, headers=headers)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class MCPOAuth(OAuth):
|
|
65
|
+
"""FastMCP ``OAuth`` whose token refresh targets the discovered endpoint.
|
|
66
|
+
|
|
67
|
+
The upstream ``OAuthClientProvider`` refreshes an expired access token
|
|
68
|
+
before sending a request, but discovers the authorization server's
|
|
69
|
+
metadata only inside its 401 branch and never persists it. Every
|
|
70
|
+
conversation builds a fresh client, so ``_refresh_token`` falls back to
|
|
71
|
+
``<MCP server origin>/token``, which is wrong for providers such as GitLab
|
|
72
|
+
(``/oauth/token``) and Atlassian (``/v1/token``). The 404 that follows
|
|
73
|
+
drops the tokens and starts the interactive browser flow, which cannot
|
|
74
|
+
complete inside a sandbox.
|
|
75
|
+
|
|
76
|
+
``_refresh_token`` runs under upstream's context lock after
|
|
77
|
+
``_initialize()`` restored the stored tokens and expiry, so discovery only
|
|
78
|
+
happens when a refresh is actually about to be attempted.
|
|
79
|
+
|
|
80
|
+
Dynamic client registration asks for a token endpoint auth method the
|
|
81
|
+
authorization server advertises, preferring a public client. Upstream
|
|
82
|
+
sends none, so servers fall back to the RFC 7591 default
|
|
83
|
+
``client_secret_basic`` and issue a secret. Token requests that do use
|
|
84
|
+
Basic auth leave ``client_id`` out of the body, which upstream always
|
|
85
|
+
includes and which strict servers reject.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
async def async_auth_flow(
|
|
89
|
+
self, request: httpx.Request
|
|
90
|
+
) -> AsyncGenerator[httpx.Request, httpx.Response]:
|
|
91
|
+
async with aclosing(super().async_auth_flow(request)) as flow:
|
|
92
|
+
outgoing = await anext(flow)
|
|
93
|
+
while True:
|
|
94
|
+
response = yield self._with_supported_auth_method(outgoing)
|
|
95
|
+
try:
|
|
96
|
+
outgoing = await flow.asend(response)
|
|
97
|
+
except StopAsyncIteration:
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
def _with_supported_auth_method(self, request: httpx.Request) -> httpx.Request:
|
|
101
|
+
"""Rebuild the registration request around a supported auth method.
|
|
102
|
+
|
|
103
|
+
Any other request, an explicitly configured method and a server that
|
|
104
|
+
advertises no known method all pass through unchanged.
|
|
105
|
+
"""
|
|
106
|
+
context = self.context
|
|
107
|
+
if (
|
|
108
|
+
context.client_info is not None
|
|
109
|
+
or context.oauth_metadata is None
|
|
110
|
+
or context.client_metadata.token_endpoint_auth_method is not None
|
|
111
|
+
):
|
|
112
|
+
return request
|
|
113
|
+
method = _preferred_token_endpoint_auth_method(
|
|
114
|
+
context.oauth_metadata.token_endpoint_auth_methods_supported
|
|
115
|
+
)
|
|
116
|
+
if method is None or request.url != self._registration_request().url:
|
|
117
|
+
return request
|
|
118
|
+
context.client_metadata.token_endpoint_auth_method = method
|
|
119
|
+
return self._registration_request()
|
|
120
|
+
|
|
121
|
+
def _registration_request(self) -> httpx.Request:
|
|
122
|
+
return create_client_registration_request(
|
|
123
|
+
self.context.oauth_metadata,
|
|
124
|
+
self.context.client_metadata,
|
|
125
|
+
self.context.get_authorization_base_url(self.context.server_url),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
async def _exchange_token_authorization_code(
|
|
129
|
+
self,
|
|
130
|
+
auth_code: str,
|
|
131
|
+
code_verifier: str,
|
|
132
|
+
*,
|
|
133
|
+
token_data: dict[str, Any] | None = None,
|
|
134
|
+
) -> httpx.Request:
|
|
135
|
+
request = await super()._exchange_token_authorization_code(
|
|
136
|
+
auth_code, code_verifier, token_data=token_data
|
|
137
|
+
)
|
|
138
|
+
return _without_client_id_when_basic(request)
|
|
139
|
+
|
|
140
|
+
async def _refresh_token(self) -> httpx.Request:
|
|
141
|
+
if self.context.oauth_metadata is None:
|
|
142
|
+
await self._discover_oauth_metadata()
|
|
143
|
+
return _without_client_id_when_basic(await super()._refresh_token())
|
|
144
|
+
|
|
145
|
+
async def _discover_oauth_metadata(self) -> None:
|
|
146
|
+
"""Populate the resource and authorization-server metadata.
|
|
147
|
+
|
|
148
|
+
Mirrors the discovery upstream performs on a 401 (protected resource
|
|
149
|
+
metadata first, then authorization server metadata, with the same
|
|
150
|
+
fallback URL lists). A network failure only logs: the refresh then
|
|
151
|
+
goes to upstream's default endpoint, exactly as before.
|
|
152
|
+
"""
|
|
153
|
+
server_url = self.context.server_url
|
|
154
|
+
try:
|
|
155
|
+
async with self.httpx_client_factory(timeout=_DISCOVERY_TIMEOUT) as client:
|
|
156
|
+
for url in build_protected_resource_metadata_discovery_urls(
|
|
157
|
+
None, server_url
|
|
158
|
+
):
|
|
159
|
+
response = await client.send(create_oauth_metadata_request(url))
|
|
160
|
+
prm = await handle_protected_resource_response(response)
|
|
161
|
+
if prm is None:
|
|
162
|
+
continue
|
|
163
|
+
await self._validate_resource_match(prm)
|
|
164
|
+
self.context.protected_resource_metadata = prm
|
|
165
|
+
# ``authorization_servers`` has a minimum length of one.
|
|
166
|
+
self.context.auth_server_url = str(prm.authorization_servers[0])
|
|
167
|
+
break
|
|
168
|
+
|
|
169
|
+
for url in build_oauth_authorization_server_metadata_discovery_urls(
|
|
170
|
+
self.context.auth_server_url, server_url
|
|
171
|
+
):
|
|
172
|
+
response = await client.send(create_oauth_metadata_request(url))
|
|
173
|
+
ok, metadata = await handle_auth_metadata_response(response)
|
|
174
|
+
if not ok:
|
|
175
|
+
break
|
|
176
|
+
if metadata is not None:
|
|
177
|
+
self.context.oauth_metadata = metadata
|
|
178
|
+
break
|
|
179
|
+
except httpx.HTTPError as exc:
|
|
180
|
+
logger.warning(
|
|
181
|
+
"OAuth metadata discovery failed for %s; refreshing against the "
|
|
182
|
+
"default token endpoint: %s",
|
|
183
|
+
server_url,
|
|
184
|
+
exc,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class NonInteractiveMCPOAuth(MCPOAuth):
|
|
189
|
+
"""``MCPOAuth`` that only uses the stored tokens and refreshes them.
|
|
190
|
+
|
|
191
|
+
When no stored token is usable, FastMCP starts a new authorization: it
|
|
192
|
+
preflights the authorization URL with a loopback redirect URI, and if the
|
|
193
|
+
server rejects that URI it clears the token storage and registers a new
|
|
194
|
+
client before opening a browser. Nobody can complete that flow for an
|
|
195
|
+
unattended client, so this one fails the connection instead and leaves
|
|
196
|
+
the stored state for the user to replace by authorizing again.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
async def redirect_handler(self, authorization_url: str) -> None: # noqa: ARG002
|
|
200
|
+
raise MCPError(
|
|
201
|
+
f"MCP server {self.mcp_url} must be authorized again: its stored "
|
|
202
|
+
"OAuth tokens are missing or could not be refreshed."
|
|
203
|
+
)
|
|
@@ -95,6 +95,7 @@ def _oauth_auth_from_authentication_config(
|
|
|
95
95
|
authentication: MCPOAuthAuthentication | None,
|
|
96
96
|
*,
|
|
97
97
|
mcp_oauth_token_storage: AsyncKeyValue | None = None,
|
|
98
|
+
mcp_oauth_class: type[MCPOAuth] = MCPOAuth,
|
|
98
99
|
) -> OAuth | None:
|
|
99
100
|
"""Build FastMCP OAuth auth from explicit SDK MCP auth metadata."""
|
|
100
101
|
if authentication is None:
|
|
@@ -105,7 +106,7 @@ def _oauth_auth_from_authentication_config(
|
|
|
105
106
|
if client_auth_method is not None:
|
|
106
107
|
additional_client_metadata["token_endpoint_auth_method"] = client_auth_method
|
|
107
108
|
|
|
108
|
-
return
|
|
109
|
+
return mcp_oauth_class(
|
|
109
110
|
scopes=authentication.scopes,
|
|
110
111
|
client_name=authentication.client_name or "FastMCP Client",
|
|
111
112
|
token_storage=mcp_oauth_token_storage,
|
|
@@ -169,6 +170,7 @@ def _prepare_mcp_config(
|
|
|
169
170
|
*,
|
|
170
171
|
mcp_oauth_token_storage: AsyncKeyValue | None = None,
|
|
171
172
|
mcp_oauth_factory: MCPOAuthFactory | None = None,
|
|
173
|
+
mcp_oauth_class: type[MCPOAuth] = MCPOAuth,
|
|
172
174
|
) -> FastMCPConfig:
|
|
173
175
|
"""Validate MCP config and apply explicit OpenHands runtime auth metadata."""
|
|
174
176
|
prepared = FastMCPConfig.model_validate(to_fastmcp_mcp_config(mcp_config))
|
|
@@ -198,12 +200,13 @@ def _prepare_mcp_config(
|
|
|
198
200
|
else _oauth_auth_from_authentication_config(
|
|
199
201
|
auth.authentication,
|
|
200
202
|
mcp_oauth_token_storage=mcp_oauth_token_storage,
|
|
203
|
+
mcp_oauth_class=mcp_oauth_class,
|
|
201
204
|
)
|
|
202
205
|
)
|
|
203
206
|
if oauth_auth is not None:
|
|
204
207
|
server.auth = oauth_auth
|
|
205
208
|
elif mcp_oauth_token_storage is not None:
|
|
206
|
-
server.auth =
|
|
209
|
+
server.auth = mcp_oauth_class(token_storage=mcp_oauth_token_storage)
|
|
207
210
|
|
|
208
211
|
return prepared
|
|
209
212
|
|
|
@@ -377,6 +380,7 @@ def create_mcp_tools(
|
|
|
377
380
|
on_tools_reconciled: ToolsReconciledCallback | None = None,
|
|
378
381
|
mcp_oauth_token_storage: AsyncKeyValue | None = None,
|
|
379
382
|
mcp_oauth_factory: MCPOAuthFactory | None = None,
|
|
383
|
+
mcp_oauth_class: type[MCPOAuth] = MCPOAuth,
|
|
380
384
|
) -> MCPClient:
|
|
381
385
|
"""Create MCP tools from OpenHands-native MCP server settings.
|
|
382
386
|
|
|
@@ -408,6 +412,7 @@ def create_mcp_tools(
|
|
|
408
412
|
mcp_config,
|
|
409
413
|
mcp_oauth_token_storage=mcp_oauth_token_storage,
|
|
410
414
|
mcp_oauth_factory=mcp_oauth_factory,
|
|
415
|
+
mcp_oauth_class=mcp_oauth_class,
|
|
411
416
|
)
|
|
412
417
|
handler = _ToolListChangedHandler(
|
|
413
418
|
client=None, # type: ignore[arg-type]
|
|
@@ -40,6 +40,15 @@ PROFILE_NAME_REGEX: Final[re.Pattern[str]] = re.compile(PROFILE_NAME_PATTERN)
|
|
|
40
40
|
logger = get_logger(__name__)
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
def canonical_profile_name(name: str) -> str:
|
|
44
|
+
"""Return the store key for ``name``: a trailing ``.json`` is dropped.
|
|
45
|
+
|
|
46
|
+
The file store keys a profile by its file stem, so ``x.json`` and ``x``
|
|
47
|
+
name the same profile.
|
|
48
|
+
"""
|
|
49
|
+
return name.removesuffix(".json")
|
|
50
|
+
|
|
51
|
+
|
|
43
52
|
class ProfileLimitExceeded(Exception):
|
|
44
53
|
"""Raised when saving would exceed the configured profile limit."""
|
|
45
54
|
|
|
@@ -107,7 +116,7 @@ class AgentProfileStore:
|
|
|
107
116
|
Raises:
|
|
108
117
|
ValueError: If ``name`` does not match ``PROFILE_NAME_PATTERN``.
|
|
109
118
|
"""
|
|
110
|
-
clean_name = name
|
|
119
|
+
clean_name = canonical_profile_name(name)
|
|
111
120
|
if not PROFILE_NAME_REGEX.match(clean_name):
|
|
112
121
|
raise ValueError(
|
|
113
122
|
f"Invalid profile name: {name!r}. "
|
|
@@ -153,6 +162,9 @@ class AgentProfileStore:
|
|
|
153
162
|
"""
|
|
154
163
|
profile_path = self._get_profile_path(profile.name)
|
|
155
164
|
payload = profile.model_dump(mode="json")
|
|
165
|
+
# Keep the stored name equal to the file stem (as ``rename`` does), so
|
|
166
|
+
# ``x.json`` and ``x`` are one profile with one name.
|
|
167
|
+
payload["name"] = profile_path.stem
|
|
156
168
|
|
|
157
169
|
with self._acquire_lock():
|
|
158
170
|
if max_profiles is not None and not profile_path.exists():
|
|
@@ -420,6 +432,9 @@ def save_profile_preserving_identity(
|
|
|
420
432
|
Raises:
|
|
421
433
|
ProfileLimitExceeded: If ``max_profiles`` would be exceeded.
|
|
422
434
|
"""
|
|
435
|
+
name = canonical_profile_name(profile.name)
|
|
436
|
+
if name != profile.name:
|
|
437
|
+
profile = profile.model_copy(update={"name": name})
|
|
423
438
|
with store.lock():
|
|
424
439
|
existing_id, existing_rev = _existing_identity(store, profile.name)
|
|
425
440
|
if existing_id is not None:
|
|
@@ -13,16 +13,17 @@ from openhands.sdk.utils.redact import redact_text_secrets
|
|
|
13
13
|
logger = get_logger(__name__)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
# Credentials needed by the SDK/server, not by agent-run subprocesses.
|
|
17
|
+
_SENSITIVE_ENV_VARS = frozenset(
|
|
18
|
+
{
|
|
19
|
+
"SESSION_API_KEY",
|
|
20
|
+
"OH_SECRET_KEY",
|
|
21
|
+
"LMNR_PROJECT_API_KEY",
|
|
22
|
+
"OTEL_HEADERS",
|
|
23
|
+
"OTEL_EXPORTER_OTLP_HEADERS",
|
|
24
|
+
"OTEL_EXPORTER_OTLP_TRACES_HEADERS",
|
|
25
|
+
}
|
|
26
|
+
)
|
|
26
27
|
|
|
27
28
|
# Session keys are also delivered as an indexed list ``OH_SESSION_API_KEYS_0``,
|
|
28
29
|
# ``OH_SESSION_API_KEYS_1``, ... (V1). Strip every slot by prefix so a rename or
|
|
@@ -43,6 +43,12 @@ def _is_abstract(type_: type) -> bool:
|
|
|
43
43
|
return False
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
def _is_object_input(data: Any, cls: type) -> bool:
|
|
47
|
+
"""Whether ``data`` is a dict or an instance of ``cls`` (a bool, so that
|
|
48
|
+
pyright does not narrow ``data`` at the call site)."""
|
|
49
|
+
return isinstance(data, (dict, cls))
|
|
50
|
+
|
|
51
|
+
|
|
46
52
|
def get_handler_class_name(handler: SerializerFunctionWrapHandler) -> str:
|
|
47
53
|
"""Extract the class name from a Pydantic serializer handler's repr string.
|
|
48
54
|
|
|
@@ -209,8 +215,16 @@ class DiscriminatedUnionMixin(OpenHandsModel):
|
|
|
209
215
|
def _validate_subtype(
|
|
210
216
|
cls, data: Any, handler: ModelWrapValidatorHandler[Self], info: ValidationInfo
|
|
211
217
|
) -> Self:
|
|
218
|
+
if not _is_object_input(data, cls):
|
|
219
|
+
# Let pydantic report a non-object input (a string, number, null,
|
|
220
|
+
# list...) as a ValidationError instead of failing on .pop().
|
|
221
|
+
return handler(data)
|
|
212
222
|
if isinstance(data, cls):
|
|
213
223
|
return data
|
|
224
|
+
if isinstance(data, dict):
|
|
225
|
+
# Never mutate the caller's dict: callers such as RemoteState
|
|
226
|
+
# validate the same cached payload more than once.
|
|
227
|
+
data = dict(data)
|
|
214
228
|
if not _is_abstract(cls):
|
|
215
229
|
has_kind_alias_field = any(
|
|
216
230
|
field_name != "kind" and field_info.alias == "kind"
|
|
@@ -229,7 +243,6 @@ class DiscriminatedUnionMixin(OpenHandsModel):
|
|
|
229
243
|
None,
|
|
230
244
|
)
|
|
231
245
|
if internal_kind_field in data:
|
|
232
|
-
data = data.copy()
|
|
233
246
|
data.pop("kind", None)
|
|
234
247
|
return handler(data)
|
|
235
248
|
kind = data.pop("kind", None)
|
|
@@ -634,8 +634,8 @@ class RemoteWorkspace(RemoteWorkspaceMixin, BaseWorkspace):
|
|
|
634
634
|
|
|
635
635
|
Clones specified repositories to meaningful directory names (e.g.,
|
|
636
636
|
'openhands-cli' instead of 'repo_0'). Automatically fetches GitHub,
|
|
637
|
-
GitLab, and
|
|
638
|
-
authentication.
|
|
637
|
+
GitLab, Bitbucket and Azure DevOps tokens from the agent server's
|
|
638
|
+
secrets for authentication.
|
|
639
639
|
|
|
640
640
|
Args:
|
|
641
641
|
repos: List of repositories to clone. Can be:
|