diff --git a/.agents/skills/debug-test-examples-workflow/SKILL.md b/.agents/skills/debug-test-examples-workflow/SKILL.md
index 278c1b9bdf..b20112f120 100644
--- a/.agents/skills/debug-test-examples-workflow/SKILL.md
+++ b/.agents/skills/debug-test-examples-workflow/SKILL.md
@@ -10,7 +10,6 @@ description: Guide for debugging failing example tests in the `test-examples` la
The `run-examples.yml` workflow runs example scripts from `examples/` directory. Triggers:
- Adding `test-examples` label to a PR
- Manual workflow dispatch
-- Scheduled nightly runs
## Debugging Steps
diff --git a/.github/ISSUE_TEMPLATE/bug_template.yml b/.github/ISSUE_TEMPLATE/bug_template.yml
index 69e45f5622..25f65e950d 100644
--- a/.github/ISSUE_TEMPLATE/bug_template.yml
+++ b/.github/ISSUE_TEMPLATE/bug_template.yml
@@ -62,6 +62,16 @@ body:
5. Error appears
validations:
required: false
+ - type: textarea
+ id: acceptance-criteria
+ attributes:
+ label: Acceptance Criteria
+ description: List the testable checklist item(s) that would prove this bug is fixed.
+ placeholder: |
+ - [ ] The reported error no longer occurs
+ - [ ] The fix works with a fresh agent instance
+ validations:
+ required: false
- type: input
id: installation
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index 3590a8a1a1..e158882bf3 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -31,15 +31,26 @@ body:
required: true
- type: textarea
- id: proposed-solution
+ id: desired-behavior
attributes:
- label: Proposed Solution
+ label: Desired Behavior
description: Describe your ideal solution. What should this feature do? How should it work?
placeholder: |
Example - Add a StateManager class that allows saving and loading agent state to/from disk or database. Provide methods like save_state(), load_state(), and clear_state(). Support multiple backend options (JSON files, SQLite, Redis, etc.).
validations:
required: true
+ - type: textarea
+ id: acceptance-criteria
+ attributes:
+ label: Acceptance Criteria
+ description: List the testable checklist item(s) that would prove this feature is complete.
+ placeholder: |
+ - [ ] Saving agent state to disk works end-to-end
+ - [ ] A new Agent restores state from a previously saved snapshot
+ validations:
+ required: true
+
- type: textarea
id: alternatives
attributes:
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 3a7f5d257c..90c43d10ee 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -48,6 +48,20 @@ Provide a video or screenshots of testing your PR. e.g. you added a new feature
-->
+## Design Doc
+
+
+
## Type
- [ ] Bug fix
diff --git a/.github/agent-server-openapi-weak-schema-allowlist.json b/.github/agent-server-openapi-weak-schema-allowlist.json
new file mode 100644
index 0000000000..92873333a3
--- /dev/null
+++ b/.github/agent-server-openapi-weak-schema-allowlist.json
@@ -0,0 +1,584 @@
+[
+ {
+ "pointer": "/components/schemas/ACPAgent-Input/properties/system_prompt_kwargs/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Provider-specific configuration is intentionally extensible.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/ACPAgent-Output/properties/system_prompt_kwargs/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Provider-specific configuration is intentionally extensible.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/ACPToolCallEvent/properties/content/anyOf/0/items",
+ "kind": "empty-object-schema",
+ "reason": "ACP protocol raw input, output, and content payloads are intentionally proxy-opaque.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/ACPToolCallEvent/properties/raw_input/anyOf/0",
+ "kind": "empty-object-schema",
+ "reason": "ACP protocol raw input, output, and content payloads are intentionally proxy-opaque.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/ACPToolCallEvent/properties/raw_output/anyOf/0",
+ "kind": "empty-object-schema",
+ "reason": "ACP protocol raw input, output, and content payloads are intentionally proxy-opaque.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/Agent-Input/properties/system_prompt_kwargs/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Provider-specific configuration is intentionally extensible.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/Agent-Output/properties/system_prompt_kwargs/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Provider-specific configuration is intentionally extensible.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/AgentContext-Output/properties/secrets/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Existing extensible or opaque public payload tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/AgentDefinition/properties/metadata/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/AgentProfileDetailResponse/properties/profile/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Existing extensible or opaque public payload tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/AgentProfileDiagnostics/properties/resolved_settings/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Existing extensible or opaque public payload tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/AgentSettings/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Agent settings retain a backward-compatible extension surface while known security-sensitive fields remain typed.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/AgentSettingsPatch/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Agent settings retain a backward-compatible extension surface while known security-sensitive fields remain typed.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/BrowserClickTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/BrowserCloseTabTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/BrowserGetContentTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/BrowserGetStateTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/BrowserGetStorageTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/BrowserGoBackTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/BrowserListTabsTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/BrowserNavigateTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/BrowserScrollTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/BrowserSetStorageAction/properties/storage_state/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Existing extensible or opaque public payload tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/BrowserSetStorageTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/BrowserStartRecordingTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/BrowserStopRecordingTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/BrowserSwitchTabTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/BrowserToolSet/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/BrowserTypeTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/ClientTool/properties/input_schema/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "The value is a caller or provider supplied JSON Schema or tool argument object.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/ClientTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/ClientToolSpec-Input/properties/parameters/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "The value is a caller or provider supplied JSON Schema or tool argument object.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/ClientToolSpec-Output/properties/parameters/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "The value is a caller or provider supplied JSON Schema or tool argument object.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/ConversationInfo/properties/agent_state/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Existing extensible or opaque public payload tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/ConversationStats/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Existing extensible or opaque public payload tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/CriticResult/properties/metadata/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/EditTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/FileEditorTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/FinishTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/GlobTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/GrepTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/HookExecutionEvent/properties/hook_input/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Existing extensible or opaque public payload tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/Icon/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Existing extensible or opaque public payload tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/InvokeSkillTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/LLM-Input/properties/litellm_extra_body/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Provider-specific configuration is intentionally extensible.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/LLM-Output/properties/litellm_extra_body/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Provider-specific configuration is intentionally extensible.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/ListDirectoryTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/MCPOAuthAuthentication-Input/properties/additional_client_metadata/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "The upstream MCP or OAuth payload intentionally preserves provider-defined extension fields.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/MCPOAuthAuthentication-Output/properties/additional_client_metadata/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "The upstream MCP or OAuth payload intentionally preserves provider-defined extension fields.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/MCPOAuthClientInfoState-Input/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "The upstream MCP or OAuth payload intentionally preserves provider-defined extension fields.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/MCPOAuthClientInfoState-Output/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "The upstream MCP or OAuth payload intentionally preserves provider-defined extension fields.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/MCPOAuthStateResponse/properties/client_info/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "The upstream MCP or OAuth payload intentionally preserves provider-defined extension fields.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/MCPOAuthStateResponse/properties/tokens/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "The upstream MCP or OAuth payload intentionally preserves provider-defined extension fields.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/MCPOAuthTokenState-Input/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "The upstream MCP or OAuth payload intentionally preserves provider-defined extension fields.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/MCPOAuthTokenState-Output/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "The upstream MCP or OAuth payload intentionally preserves provider-defined extension fields.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/MCPTestSuccess/properties/resolved_mcp_servers/anyOf/0/items/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "The upstream MCP or OAuth payload intentionally preserves provider-defined extension fields.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/MCPToolAction/properties/data/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Existing extensible or opaque public payload tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/MCPToolCallSpec/properties/arguments/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "The value is a caller or provider supplied JSON Schema or tool argument object.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/MCPToolDefinition/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/PlanningFileEditorTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/ProfileDetailResponse/properties/config/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Existing extensible or opaque public payload tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/ReadFileTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/SettingsResponse/properties/agent_settings/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Existing extensible or opaque public payload tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/SettingsResponse/properties/conversation_settings/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Existing extensible or opaque public payload tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/SettingsResponse/properties/misc_settings/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Misc settings are an intentionally opaque frontend-owned extension container.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/SettingsUpdateRequest/properties/agent_settings_diff/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Existing extensible or opaque public payload tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/SettingsUpdateRequest/properties/conversation_settings_diff/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Existing extensible or opaque public payload tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/SettingsUpdateRequest/properties/misc_settings_diff/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Misc settings are an intentionally opaque frontend-owned extension container.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/Skill-Output/properties/mcp_tools/anyOf/0/additionalProperties/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Existing extensible or opaque public payload tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/StartConversationRequest/properties/agent_settings/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Existing extensible or opaque public payload tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/SubAgentInfo/properties/metadata/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/SwitchLLMTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/TaskTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/TaskToolSet/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/TaskTrackerTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/TerminalTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/ThinkTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/Tool-Input/properties/params/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "The value is a caller or provider supplied JSON Schema or tool argument object.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/ToolExecution/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Existing extensible or opaque public payload tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/VisionInspectTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/WorkflowTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/WorkflowToolSet/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/WriteFileTool/properties/meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Tool metadata is intentionally extensible across tool providers.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/mcp__types__Tool/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Existing extensible or opaque public payload tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/mcp__types__Tool/properties/_meta/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Existing extensible or opaque public payload tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/mcp__types__Tool/properties/inputSchema/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "The value is a caller or provider supplied JSON Schema or tool argument object.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/mcp__types__Tool/properties/outputSchema/anyOf/0/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "The value is a caller or provider supplied JSON Schema or tool argument object.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/mcp__types__ToolAnnotations/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "Existing extensible or opaque public payload tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/components/schemas/openhands__sdk__tool__spec__Tool/properties/params/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "The value is a caller or provider supplied JSON Schema or tool argument object.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/paths/~1api~1agent-profiles~1{name}/post/requestBody/content/application~1json/schema/additionalProperties",
+ "kind": "unrestricted-additional-properties",
+ "reason": "This existing endpoint has an opaque or non-JSON response contract and is tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/paths/~1api~1conversations~1{conversation_id}~1events~1search/get/responses/200/content/application~1json/schema",
+ "kind": "empty-object-schema",
+ "reason": "This existing endpoint has an opaque or non-JSON response contract and is tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/paths/~1api~1conversations~1{conversation_id}~1workspace/get/responses/200/content/application~1json/schema",
+ "kind": "empty-object-schema",
+ "reason": "This existing endpoint has an opaque or non-JSON response contract and is tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/paths/~1api~1conversations~1{conversation_id}~1workspace~1{file_path}/get/responses/200/content/application~1json/schema",
+ "kind": "empty-object-schema",
+ "reason": "This existing endpoint has an opaque or non-JSON response contract and is tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/paths/~1api~1file~1archive/get/responses/200/content/application~1json/schema",
+ "kind": "empty-object-schema",
+ "reason": "This existing endpoint has an opaque or non-JSON response contract and is tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/paths/~1api~1file~1download-trajectory~1{conversation_id}/get/responses/200/content/application~1json/schema",
+ "kind": "empty-object-schema",
+ "reason": "This existing endpoint has an opaque or non-JSON response contract and is tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/paths/~1api~1file~1download/get/responses/200/content/application~1json/schema",
+ "kind": "empty-object-schema",
+ "reason": "This existing endpoint has an opaque or non-JSON response contract and is tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ },
+ {
+ "pointer": "/paths/~1api~1settings~1secrets~1{name}/get/responses/200/content/application~1json/schema",
+ "kind": "empty-object-schema",
+ "reason": "This existing endpoint has an opaque or non-JSON response contract and is tracked by the weak-type ratchet.",
+ "owner": "OpenHands OSS"
+ }
+]
diff --git a/.github/scripts/check_agent_server_openapi_quality.py b/.github/scripts/check_agent_server_openapi_quality.py
new file mode 100644
index 0000000000..b78945744c
--- /dev/null
+++ b/.github/scripts/check_agent_server_openapi_quality.py
@@ -0,0 +1,397 @@
+#!/usr/bin/env python3
+"""Ratchet weak types in the public Agent Server OpenAPI contract."""
+
+from __future__ import annotations
+
+import argparse
+import datetime as dt
+import json
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any
+
+
+REPO_ROOT = Path(__file__).resolve().parents[2]
+DEFAULT_ALLOWLIST = (
+ REPO_ROOT / ".github" / "agent-server-openapi-weak-schema-allowlist.json"
+)
+HTTP_METHODS = {
+ "get",
+ "put",
+ "post",
+ "delete",
+ "patch",
+ "options",
+ "head",
+ "trace",
+}
+EXPECTED_DISCRIMINATORS = {
+ "/components/schemas/MCPAuthCredential": "strategy",
+ "/components/schemas/MCPServer/properties/auth/anyOf/0": "strategy",
+ "/components/schemas/MCPServerPatch/properties/auth/anyOf/0": "strategy",
+ "/components/schemas/MCPTestRequest/properties/server": "type",
+ "/components/schemas/MCPTestResponse": "ok",
+}
+EXPECTED_STRUCTURED_LOCATIONS = {
+ "/components/schemas/AgentSettings",
+ "/components/schemas/AgentSettings/properties/mcp_config",
+ "/components/schemas/AgentSettingsPatch",
+ "/components/schemas/AgentSettingsPatch/properties/mcp_config/anyOf/0",
+ "/components/schemas/MCPAuthCredential",
+ "/components/schemas/MCPConfig",
+ "/components/schemas/MCPConfigPatch",
+ "/components/schemas/MCPOAuthStateResponse",
+ "/components/schemas/MCPServer",
+ "/components/schemas/MCPServerPatch",
+ "/components/schemas/MCPTestRequest",
+ "/components/schemas/MCPTestResponse",
+ "/components/schemas/RemoteMCPServer",
+ "/components/schemas/SettingsResponse/properties/agent_settings",
+ (
+ "/components/schemas/SettingsResponse/properties/agent_settings/"
+ "properties/mcp_config"
+ ),
+ (
+ "/components/schemas/SettingsUpdateRequest/properties/"
+ "agent_settings_diff/anyOf/0/properties/mcp_config"
+ ),
+ "/components/schemas/StdioMCPServer",
+}
+
+
+@dataclass(frozen=True, order=True)
+class WeakLocation:
+ pointer: str
+ kind: str
+
+
+@dataclass(frozen=True)
+class AllowlistEntry:
+ pointer: str
+ kind: str
+ reason: str
+ owner: str
+ expiry: dt.date | None = None
+ follow_up: str | None = None
+
+ @property
+ def key(self) -> WeakLocation:
+ return WeakLocation(pointer=self.pointer, kind=self.kind)
+
+
+def _escape_pointer_token(token: str) -> str:
+ return token.replace("~", "~0").replace("/", "~1")
+
+
+def _schema_is_unconstrained(schema: object) -> bool:
+ if not isinstance(schema, dict) or not schema:
+ return True
+ if "$ref" in schema:
+ return False
+ if schema.get("additionalProperties") is True and not schema.get("properties"):
+ return True
+ variants = schema.get("anyOf") or schema.get("oneOf")
+ if isinstance(variants, list):
+ non_null = [
+ variant
+ for variant in variants
+ if not (isinstance(variant, dict) and variant.get("type") == "null")
+ ]
+ return bool(non_null) and all(
+ _schema_is_unconstrained(variant) for variant in non_null
+ )
+ return False
+
+
+def _resolve_pointer(document: object, pointer: str) -> object:
+ current = document
+ for raw_token in pointer.removeprefix("/").split("/"):
+ token = raw_token.replace("~1", "/").replace("~0", "~")
+ if isinstance(current, dict):
+ if token not in current:
+ raise KeyError(pointer)
+ current = current[token]
+ elif isinstance(current, list):
+ current = current[int(token)]
+ else:
+ raise KeyError(pointer)
+ return current
+
+
+def _walk_schema(
+ schema: object,
+ pointer: str,
+ findings: set[WeakLocation],
+) -> None:
+ if not isinstance(schema, dict):
+ return
+ if not schema:
+ findings.add(WeakLocation(pointer=pointer, kind="empty-object-schema"))
+ return
+ if schema.get("additionalProperties") is True:
+ findings.add(
+ WeakLocation(
+ pointer=f"{pointer}/additionalProperties",
+ kind="unrestricted-additional-properties",
+ )
+ )
+
+ for keyword in ("oneOf", "anyOf", "allOf", "prefixItems"):
+ variants = schema.get(keyword)
+ if isinstance(variants, list):
+ for index, variant in enumerate(variants):
+ _walk_schema(variant, f"{pointer}/{keyword}/{index}", findings)
+
+ for keyword in (
+ "items",
+ "additionalProperties",
+ "contains",
+ "not",
+ "if",
+ "then",
+ "else",
+ ):
+ nested = schema.get(keyword)
+ if isinstance(nested, dict):
+ _walk_schema(nested, f"{pointer}/{keyword}", findings)
+
+ properties = schema.get("properties")
+ if isinstance(properties, dict):
+ for name, nested in properties.items():
+ _walk_schema(
+ nested,
+ f"{pointer}/properties/{_escape_pointer_token(name)}",
+ findings,
+ )
+
+
+def _walk_operation_schemas(
+ document: dict[str, Any],
+ findings: set[WeakLocation],
+) -> None:
+ for path, path_item in document.get("paths", {}).items():
+ if not isinstance(path_item, dict):
+ continue
+ escaped_path = _escape_pointer_token(path)
+ for method, operation in path_item.items():
+ if method not in HTTP_METHODS or not isinstance(operation, dict):
+ continue
+ operation_pointer = f"/paths/{escaped_path}/{method}"
+
+ request_body = operation.get("requestBody")
+ if isinstance(request_body, dict):
+ content = request_body.get("content")
+ if isinstance(content, dict):
+ for media_type, media in content.items():
+ schema_pointer = (
+ f"{operation_pointer}/requestBody/content/"
+ f"{_escape_pointer_token(media_type)}/schema"
+ )
+ schema = (
+ media.get("schema") if isinstance(media, dict) else None
+ )
+ if schema is None:
+ findings.add(
+ WeakLocation(
+ pointer=schema_pointer,
+ kind="missing-request-schema",
+ )
+ )
+ else:
+ _walk_schema(schema, schema_pointer, findings)
+
+ responses = operation.get("responses")
+ if not isinstance(responses, dict):
+ continue
+ for status_code, response in responses.items():
+ if not str(status_code).startswith("2") or not isinstance(
+ response, dict
+ ):
+ continue
+ content = response.get("content")
+ if not isinstance(content, dict):
+ continue
+ for media_type, media in content.items():
+ schema_pointer = (
+ f"{operation_pointer}/responses/"
+ f"{_escape_pointer_token(str(status_code))}/content/"
+ f"{_escape_pointer_token(media_type)}/schema"
+ )
+ schema = media.get("schema") if isinstance(media, dict) else None
+ if schema is None:
+ findings.add(
+ WeakLocation(
+ pointer=schema_pointer,
+ kind="missing-success-response-schema",
+ )
+ )
+ else:
+ _walk_schema(schema, schema_pointer, findings)
+
+
+def find_weak_locations(document: dict[str, Any]) -> set[WeakLocation]:
+ findings: set[WeakLocation] = set()
+ _walk_operation_schemas(document, findings)
+
+ schemas = document.get("components", {}).get("schemas", {})
+ if isinstance(schemas, dict):
+ for name, schema in schemas.items():
+ _walk_schema(
+ schema,
+ f"/components/schemas/{_escape_pointer_token(name)}",
+ findings,
+ )
+ return findings
+
+
+def find_contract_quality_errors(document: dict[str, Any]) -> list[str]:
+ errors: list[str] = []
+ for pointer, property_name in EXPECTED_DISCRIMINATORS.items():
+ try:
+ schema = _resolve_pointer(document, pointer)
+ except KeyError:
+ errors.append(f"{pointer}: required discriminated union is missing")
+ continue
+ discriminator = (
+ schema.get("discriminator") if isinstance(schema, dict) else None
+ )
+ if (
+ not isinstance(discriminator, dict)
+ or discriminator.get("propertyName") != property_name
+ ):
+ errors.append(
+ f"{pointer}: expected discriminator property {property_name!r}"
+ )
+
+ for pointer in sorted(EXPECTED_STRUCTURED_LOCATIONS):
+ try:
+ schema = _resolve_pointer(document, pointer)
+ except KeyError:
+ errors.append(f"{pointer}: required known contract location is missing")
+ continue
+ if _schema_is_unconstrained(schema):
+ errors.append(f"{pointer}: known contract location is unconstrained")
+ return errors
+
+
+def load_allowlist(path: Path) -> list[AllowlistEntry]:
+ raw = json.loads(path.read_text())
+ if not isinstance(raw, list):
+ raise ValueError("OpenAPI weak-schema allowlist must be a JSON array")
+
+ entries: list[AllowlistEntry] = []
+ for index, item in enumerate(raw):
+ if not isinstance(item, dict):
+ raise ValueError(f"Allowlist entry {index} must be an object")
+ missing = {
+ field
+ for field in ("pointer", "kind", "reason", "owner")
+ if not isinstance(item.get(field), str) or not item[field].strip()
+ }
+ if missing:
+ raise ValueError(
+ f"Allowlist entry {index} is missing: {', '.join(sorted(missing))}"
+ )
+ expiry_raw = item.get("expiry")
+ expiry = dt.date.fromisoformat(expiry_raw) if expiry_raw else None
+ follow_up = item.get("follow_up")
+ if follow_up is not None and not isinstance(follow_up, str):
+ raise ValueError(f"Allowlist entry {index} follow_up must be a string")
+ entries.append(
+ AllowlistEntry(
+ pointer=item["pointer"],
+ kind=item["kind"],
+ reason=item["reason"],
+ owner=item["owner"],
+ expiry=expiry,
+ follow_up=follow_up,
+ )
+ )
+ return entries
+
+
+def check_allowlist(
+ findings: set[WeakLocation],
+ entries: list[AllowlistEntry],
+ *,
+ today: dt.date | None = None,
+) -> list[str]:
+ today = today or dt.date.today()
+ entry_by_key: dict[WeakLocation, AllowlistEntry] = {}
+ errors: list[str] = []
+ for entry in entries:
+ if entry.key in entry_by_key:
+ errors.append(
+ f"{entry.pointer}: duplicate allowlist entry for {entry.kind}"
+ )
+ entry_by_key[entry.key] = entry
+ if entry.expiry is not None and entry.expiry < today:
+ expiry = entry.expiry.isoformat()
+ errors.append(f"{entry.pointer}: allowlist entry expired on {expiry}")
+
+ for finding in sorted(findings - set(entry_by_key)):
+ errors.append(f"{finding.pointer}: new weak schema ({finding.kind})")
+ for stale in sorted(set(entry_by_key) - findings):
+ errors.append(f"{stale.pointer}: stale allowlist entry ({stale.kind})")
+ return errors
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ "--schema",
+ type=Path,
+ help="Exported OpenAPI JSON. Builds the current contract when omitted.",
+ )
+ parser.add_argument(
+ "--allowlist",
+ type=Path,
+ default=DEFAULT_ALLOWLIST,
+ help="JSON allowlist of intentionally weak schema locations.",
+ )
+ parser.add_argument(
+ "--list-only",
+ action="store_true",
+ help="Print detected weak locations without checking the allowlist.",
+ )
+ return parser.parse_args()
+
+
+def main() -> int:
+ args = parse_args()
+ if args.schema is not None:
+ document = json.loads(args.schema.read_text())
+ else:
+ from openhands.agent_server.openapi import build_public_openapi
+
+ document = build_public_openapi()
+ findings = find_weak_locations(document)
+ contract_errors = find_contract_quality_errors(document)
+ if args.list_only:
+ for finding in sorted(findings):
+ print(f"{finding.kind}\t{finding.pointer}")
+ for error in contract_errors:
+ print(f"contract-error\t{error}")
+ return int(bool(contract_errors))
+
+ try:
+ allowlist = load_allowlist(args.allowlist)
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
+ print(f"Invalid OpenAPI weak-schema allowlist: {exc}")
+ return 1
+
+ errors = contract_errors + check_allowlist(findings, allowlist)
+ if errors:
+ print("Agent Server OpenAPI type-quality check failed:")
+ for error in errors:
+ print(f"- {error}")
+ return 1
+ print(
+ "Agent Server OpenAPI type-quality check passed "
+ f"({len(findings)} allowlisted weak locations)."
+ )
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/.github/scripts/check_agent_server_rest_api_breakage.py b/.github/scripts/check_agent_server_rest_api_breakage.py
index 511f7b1397..6639232c82 100644
--- a/.github/scripts/check_agent_server_rest_api_breakage.py
+++ b/.github/scripts/check_agent_server_rest_api_breakage.py
@@ -39,7 +39,15 @@
accepted types, the check passes and the workflow marks the PR
release-note-required.
-5) No in-place contract breakage
+5) Schema-only repairs of previously opaque MCP/settings locations are allowed
+ - The runtime already returned MCP objects at these locations, but historical
+ OpenAPI described them as empty/unconstrained schemas. Giving those existing
+ objects their real shape is not a wire-format change.
+ - Pydantic may also collapse identical validation/serialization components;
+ replacing ``MCPNoneAuthCredential-Input`` with the structurally identical
+ ``MCPNoneAuthCredential`` is a component-name repair, not a union removal.
+
+6) No in-place contract breakage
- Breaking REST contract changes that are not removals of previously-deprecated
operations/properties, additive oneOf expansions, or additive response property
type widenings fail the check. REST clients need 5 minor releases of runway, so
@@ -67,6 +75,8 @@
from packaging import version as pkg_version
+from openhands.agent_server.openapi import filter_public_openapi
+
REPO_ROOT = Path(__file__).resolve().parents[2]
AGENT_SERVER_PYPROJECT = REPO_ROOT / "openhands-agent-server" / "pyproject.toml"
@@ -88,7 +98,6 @@
"head",
"trace",
}
-PUBLIC_REST_PATH_PREFIX = "/api/"
AGENT_SERVER_REST_API_BASE_REF_ENV = "AGENT_SERVER_REST_API_BASE_REF"
RESPONSE_TYPE_WIDENING_REPORT_ENV = "AGENT_SERVER_REST_TYPE_WIDENING_REPORT_PATH"
@@ -316,14 +325,14 @@ def _find_sdk_deprecated_fastapi_routes(repo_root: Path) -> list[str]:
def _filter_public_rest_openapi(schema: dict) -> dict:
- filtered_schema = dict(schema)
- filtered_schema["paths"] = {
- path: path_item
- for path, path_item in schema.get("paths", {}).items()
- if path == PUBLIC_REST_PATH_PREFIX.rstrip("/")
- or path.startswith(PUBLIC_REST_PATH_PREFIX)
- }
- return filtered_schema
+ # Compatibility checks retain the historical component set so an approved,
+ # deprecated property can still be inspected after the route that referenced
+ # it is removed. Release artifacts use the pruned, canonical mode instead.
+ return filter_public_openapi(
+ schema,
+ prune_schemas=False,
+ add_contract_components=False,
+ )
def _find_deprecation_policy_errors(schema: dict) -> list[str]:
@@ -727,6 +736,46 @@ def _is_union_type_change_artifact(change: dict) -> bool:
return "type/format changed from `object`/`` to ``/``" in text
+_OPAQUE_MCP_RESPONSE_REPAIR_PATHS = (
+ "/mcp_config/",
+ "/mcp_servers/",
+ "oauth_state/",
+)
+_OPAQUE_TO_OBJECT_TYPE_CHANGE = (
+ "response's property type/format changed from ``/`` to `object`/``"
+)
+_NONE_AUTH_INPUT_COMPONENT = "#/components/schemas/MCPNoneAuthCredential-Input"
+_AGENT_SETTINGS_DIFF_SCHEMA_REPAIR = (
+ "removed `subschema #1` from the `agent_settings_diff` request property "
+ "`anyOf` list"
+)
+
+
+def _is_mcp_contract_schema_repair(change: dict) -> bool:
+ """Recognize wire-compatible repairs of historically opaque MCP schemas.
+
+ This is deliberately narrower than accepting arbitrary type changes. The
+ response exception only covers MCP settings and OAuth state locations whose
+ old schemas had no type at all. The request exceptions cover an identical
+ Pydantic component rename and the settings diff's replacement of an
+ unrestricted object schema with the same extensible object plus known fields.
+ """
+ text = str(change.get("text", ""))
+ if _OPAQUE_TO_OBJECT_TYPE_CHANGE in text and any(
+ path in text for path in _OPAQUE_MCP_RESPONSE_REPAIR_PATHS
+ ):
+ return True
+
+ if (
+ text.startswith(f"removed `{_NONE_AUTH_INPUT_COMPONENT}` from the `")
+ and "/auth/" in text
+ and "request property `oneOf` list" in text
+ ):
+ return True
+
+ return text == _AGENT_SETTINGS_DIFF_SCHEMA_REPAIR
+
+
def _split_breaking_changes(
breaking_changes: list[dict],
) -> tuple[list[dict], list[dict], list[dict], list[dict]]:
@@ -996,6 +1045,16 @@ def main() -> int:
for change in other_breaking_changes
if not _is_union_type_change_artifact(change)
]
+ mcp_contract_schema_repairs = [
+ change
+ for change in other_breaking_changes
+ if _is_mcp_contract_schema_repair(change)
+ ]
+ other_breaking_changes = [
+ change
+ for change in other_breaking_changes
+ if not _is_mcp_contract_schema_repair(change)
+ ]
accepted_response_type_widening_changes = [
change
for change in other_breaking_changes
@@ -1105,6 +1164,15 @@ def main() -> int:
for item in response_type_widenings:
print(f" - {item.text}")
+ if mcp_contract_schema_repairs:
+ print(
+ f"\n::notice title={PYPI_DISTRIBUTION} REST API::"
+ "Typed historically opaque MCP/settings schemas without changing "
+ "their runtime wire format."
+ )
+ for item in mcp_contract_schema_repairs:
+ print(f" - {item.get('text', str(item))}")
+
if other_breaking_changes:
print(
"::error "
@@ -1142,6 +1210,8 @@ def main() -> int:
"the accepted POST /api/cloud-proxy removal, the accepted "
"GET /api/vscode/url base_url default removal, additive response "
"oneOf expansions, and/or additive response property type widenings."
+ " It may also include wire-compatible repairs of historically "
+ "opaque MCP/settings schemas."
)
else:
return 1
diff --git a/.github/scripts/check_approval_drift.py b/.github/scripts/check_approval_drift.py
index 7202eb9eac..b3d13e681c 100644
--- a/.github/scripts/check_approval_drift.py
+++ b/.github/scripts/check_approval_drift.py
@@ -123,12 +123,15 @@ def audit_pr(
if not reviewed_head:
latest = approvals[-1]
reviewer = (latest.get("user") or {}).get("login", "?")
- approved_on = _short(latest.get("commit_id"))
+ approved_sha = latest.get("commit_id")
+ approved_on = _short(approved_sha)
+ merged_head = _short(head_sha)
+ diff_link = _compare_link(repo, approved_sha, head_sha, merged_head)
return (
"changed-after-approval",
(
f"last approval by @{reviewer} was on {approved_on}, but merged "
- f"head was {_short(head_sha)} — commits landed after review"
+ f"head was {diff_link} — commits landed after review"
),
)
@@ -139,6 +142,17 @@ def _short(sha: object) -> str:
return str(sha)[:9] if sha else "?"
+def _compare_link(repo: str, base: object, head: object, label: str) -> str:
+ """Markdown link to the GitHub compare page between *base* and *head*.
+
+ Falls back to plain text if either SHA is missing so the report never
+ breaks on a sparse API response.
+ """
+ if not base or not head:
+ return label
+ return f"[{label}](https://github.com/{repo}/compare/{base}...{head})"
+
+
def main() -> int:
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
repo = resolve_repo()
diff --git a/.github/scripts/check_dependency_diff.py b/.github/scripts/check_dependency_diff.py
index b61c271538..199fa3ab60 100644
--- a/.github/scripts/check_dependency_diff.py
+++ b/.github/scripts/check_dependency_diff.py
@@ -138,11 +138,21 @@ def main() -> int:
if before[n]["version"] != after[n]["version"]
}
+ # First-party openhands-* packages are version-bumped on every release;
+ # exclude them from the external-dep count but still surface them below.
+ def _is_openhands(name: str) -> bool:
+ return name.lower().startswith("openhands-")
+
+ bumped_external = {n: v for n, v in bumped.items() if not _is_openhands(n)}
+ bumped_openhands = {n: v for n, v in bumped.items() if _is_openhands(n)}
+
report.add(f"Baseline: `{baseline}`")
report.add(
- f"Added: **{len(added)}**, bumped: **{len(bumped)}**, "
+ f"Added: **{len(added)}**, bumped: **{len(bumped_external)}**, "
f"removed: **{len(removed)}**."
)
+ if bumped_openhands:
+ report.add(f"_(plus {len(bumped_openhands)} internal `openhands-*` bump(s))_")
report.add("")
# --- non-registry sources on new/changed packages (supply-chain surface) --
@@ -199,11 +209,17 @@ def main() -> int:
for name in sorted(added):
report.add(f"- `{name}=={added[name]['version']}`")
report.add("\n")
- if bumped:
+ if bumped_external:
report.add("Bumped dependencies
\n")
- for name in sorted(bumped):
- old, new = bumped[name]
- report.add(f"- `{name}`: {old} → {new}")
+ for name in sorted(bumped_external):
+ old_v, new_v = bumped_external[name]
+ report.add(f"- `{name}`: {old_v} → {new_v}")
+ report.add("\n ")
+ if bumped_openhands:
+ report.add("Internal `openhands-*` bumps
\n")
+ for name in sorted(bumped_openhands):
+ old_v, new_v = bumped_openhands[name]
+ report.add(f"- `{name}`: {old_v} → {new_v}")
report.add("\n ")
sys.stdout.write(report.render())
diff --git a/.github/scripts/check_issue_readiness.py b/.github/scripts/check_issue_readiness.py
new file mode 100644
index 0000000000..208fdb1aa8
--- /dev/null
+++ b/.github/scripts/check_issue_readiness.py
@@ -0,0 +1,265 @@
+"""Determine whether an issue meets the `ready-for-dev` readiness criteria.
+
+The criteria are type-specific:
+
+- Bug reports (labeled `bug`): the Actual Behavior section must describe a
+ reproducible SDK run and include a supported command (`python`, `pytest`,
+ `uv`, or `pip`), plus a non-empty Acceptance Criteria section with at least
+ one checklist item.
+
+- Enhancements (labeled `enhancement`): the body must contain non-empty
+ Desired Behavior and Acceptance Criteria sections, the latter with at least
+ one checklist item.
+
+GitHub issue forms render each field as an `###