opencode-a2a 1.2.0__tar.gz → 1.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/.github/workflows/publish.yml +20 -1
  2. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/CONTRIBUTING.md +8 -0
  3. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/PKG-INFO +2 -1
  4. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/README.md +1 -0
  5. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/SECURITY.md +3 -0
  6. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/docs/architecture.md +1 -0
  7. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/docs/compatibility.md +1 -1
  8. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/docs/extension-specifications.md +1 -0
  9. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/docs/guide.md +53 -3
  10. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/docs/maintainer-architecture.md +3 -0
  11. opencode_a2a-1.3.1/docs/security-architecture.md +175 -0
  12. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/pyproject.toml +5 -0
  13. opencode_a2a-1.3.1/src/opencode_a2a/client/network_policy.py +155 -0
  14. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/client/request_context.py +9 -12
  15. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/config.py +40 -0
  16. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/execution/executor.py +2 -0
  17. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/jsonrpc/application.py +72 -17
  18. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/jsonrpc/error_responses.py +11 -7
  19. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/jsonrpc/handlers/workspace_control.py +65 -7
  20. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/profile/runtime.py +5 -1
  21. opencode_a2a-1.3.1/src/opencode_a2a/redact.py +82 -0
  22. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/server/agent_card.py +0 -3
  23. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/server/application.py +82 -4
  24. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/server/client_manager.py +28 -2
  25. opencode_a2a-1.3.1/src/opencode_a2a/server/database.py +177 -0
  26. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/server/middleware.py +179 -0
  27. opencode_a2a-1.3.1/src/opencode_a2a/server/runtime_limits.py +199 -0
  28. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a.egg-info/PKG-INFO +2 -1
  29. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a.egg-info/SOURCES.txt +12 -0
  30. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/client/test_client_facade.py +11 -9
  31. opencode_a2a-1.3.1/tests/client/test_client_http_headers.py +150 -0
  32. opencode_a2a-1.3.1/tests/client/test_network_policy.py +210 -0
  33. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/client/test_request_context.py +10 -12
  34. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/config/test_settings.py +50 -0
  35. opencode_a2a-1.3.1/tests/execution/test_error_redaction.py +57 -0
  36. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/execution/test_opencode_agent_session_binding.py +7 -3
  37. opencode_a2a-1.3.1/tests/jsonrpc/test_error_redaction.py +116 -0
  38. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/jsonrpc/test_opencode_session_extension_queries.py +7 -0
  39. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/jsonrpc/test_opencode_workspace_control_extension.py +126 -5
  40. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/profile/test_profile_runtime.py +13 -0
  41. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/server/test_a2a_client_manager.py +128 -0
  42. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/server/test_agent_card.py +18 -0
  43. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/server/test_app_behaviors.py +2 -1
  44. opencode_a2a-1.3.1/tests/server/test_database.py +268 -0
  45. opencode_a2a-1.3.1/tests/server/test_http_boundary.py +282 -0
  46. opencode_a2a-1.3.1/tests/server/test_runtime_limits.py +450 -0
  47. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/server/test_state_store.py +9 -5
  48. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/server/test_task_store_factory.py +15 -7
  49. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/support/session_query_client.py +6 -0
  50. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/support/workspace_control_client.py +50 -6
  51. opencode_a2a-1.3.1/tests/test_redact.py +89 -0
  52. opencode_a2a-1.2.0/src/opencode_a2a/server/database.py +0 -57
  53. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/.github/dependabot.yml +0 -0
  54. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/.github/workflows/dependency-review.yml +0 -0
  55. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/.github/workflows/validate.yml +0 -0
  56. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/.gitignore +0 -0
  57. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/.pre-commit-config.yaml +0 -0
  58. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/.secrets.baseline +0 -0
  59. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/AGENTS.md +0 -0
  60. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/CODE_OF_CONDUCT.md +0 -0
  61. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/LICENSE +0 -0
  62. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/MANIFEST.in +0 -0
  63. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/SUPPORT.md +0 -0
  64. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/docs/conformance-triage.md +0 -0
  65. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/docs/conformance.md +0 -0
  66. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/scripts/README.md +0 -0
  67. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/scripts/check_coverage.py +0 -0
  68. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/scripts/conformance.sh +0 -0
  69. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/scripts/dependency_health.sh +0 -0
  70. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/scripts/doctor.sh +0 -0
  71. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/scripts/find_thin_wrappers.py +0 -0
  72. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/scripts/health_common.sh +0 -0
  73. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/scripts/lint.sh +0 -0
  74. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/scripts/live_opencode_smoke.sh +0 -0
  75. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/scripts/smoke_test_built_cli.sh +0 -0
  76. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/setup.cfg +0 -0
  77. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/__init__.py +0 -0
  78. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/a2a_protocol.py +0 -0
  79. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/a2a_utils.py +0 -0
  80. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/auth.py +0 -0
  81. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/cli.py +0 -0
  82. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/client/__init__.py +0 -0
  83. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/client/agent_card.py +0 -0
  84. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/client/auth.py +0 -0
  85. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/client/client.py +0 -0
  86. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/client/config.py +0 -0
  87. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/client/error_mapping.py +0 -0
  88. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/client/errors.py +0 -0
  89. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/client/payload_text.py +0 -0
  90. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/client/polling.py +0 -0
  91. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/contracts/__init__.py +0 -0
  92. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/contracts/extensions/__init__.py +0 -0
  93. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/contracts/extensions/capabilities.py +0 -0
  94. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/contracts/extensions/catalog.py +0 -0
  95. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/contracts/extensions/compatibility.py +0 -0
  96. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/contracts/extensions/contract_docs.py +0 -0
  97. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/contracts/extensions/identifiers.py +0 -0
  98. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/contracts/extensions/private_params.py +0 -0
  99. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/contracts/extensions/public_params.py +0 -0
  100. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/execution/__init__.py +0 -0
  101. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/execution/coordinator.py +0 -0
  102. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/execution/event_helpers.py +0 -0
  103. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/execution/metrics.py +0 -0
  104. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/execution/session_manager.py +0 -0
  105. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/execution/stream_events.py +0 -0
  106. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/execution/stream_runtime.py +0 -0
  107. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/execution/stream_state.py +0 -0
  108. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/execution/tool_error_mapping.py +0 -0
  109. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/execution/tool_orchestration.py +0 -0
  110. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/execution/upstream_error_translator.py +0 -0
  111. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/extension_negotiation.py +0 -0
  112. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/interrupt_request_tracker.py +0 -0
  113. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/jsonrpc/__init__.py +0 -0
  114. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/jsonrpc/dispatch.py +0 -0
  115. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/jsonrpc/handlers/__init__.py +0 -0
  116. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/jsonrpc/handlers/common.py +0 -0
  117. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/jsonrpc/handlers/interrupt_callbacks.py +0 -0
  118. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/jsonrpc/handlers/interrupt_queries.py +0 -0
  119. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/jsonrpc/handlers/provider_discovery.py +0 -0
  120. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/jsonrpc/handlers/session_control.py +0 -0
  121. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/jsonrpc/handlers/session_lifecycle.py +0 -0
  122. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/jsonrpc/handlers/session_queries.py +0 -0
  123. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/jsonrpc/methods.py +0 -0
  124. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/jsonrpc/models.py +0 -0
  125. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/jsonrpc/params.py +0 -0
  126. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/metadata_access.py +0 -0
  127. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/opencode_upstream_client.py +0 -0
  128. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/output_modes.py +0 -0
  129. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/parsing.py +0 -0
  130. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/parts/__init__.py +0 -0
  131. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/parts/mapping.py +0 -0
  132. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/parts/text.py +0 -0
  133. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/profile/__init__.py +0 -0
  134. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/protocol_versions.py +0 -0
  135. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/py.typed +0 -0
  136. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/runtime_state.py +0 -0
  137. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/sandbox_policy.py +0 -0
  138. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/server/__init__.py +0 -0
  139. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/server/context_helpers.py +0 -0
  140. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/server/lifespan.py +0 -0
  141. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/server/migrations.py +0 -0
  142. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/server/openapi.py +0 -0
  143. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/server/request_parsing.py +0 -0
  144. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/server/rest_tasks.py +0 -0
  145. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/server/state_store.py +0 -0
  146. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/server/task_store.py +0 -0
  147. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/server/task_store_sdk_compat.py +0 -0
  148. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/task_states.py +0 -0
  149. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/trace_context.py +0 -0
  150. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a/upstream_taxonomy.py +0 -0
  151. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a.egg-info/dependency_links.txt +0 -0
  152. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a.egg-info/entry_points.txt +0 -0
  153. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a.egg-info/requires.txt +0 -0
  154. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/src/opencode_a2a.egg-info/top_level.txt +0 -0
  155. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/__init__.py +0 -0
  156. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/client/__init__.py +0 -0
  157. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/client/test_agent_card.py +0 -0
  158. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/client/test_client_config.py +0 -0
  159. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/client/test_error_mapping.py +0 -0
  160. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/client/test_payload_text.py +0 -0
  161. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/client/test_polling.py +0 -0
  162. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/config/__init__.py +0 -0
  163. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/conftest.py +0 -0
  164. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/contracts/__init__.py +0 -0
  165. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/contracts/test_extension_contract_consistency.py +0 -0
  166. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/execution/__init__.py +0 -0
  167. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/execution/test_agent_errors.py +0 -0
  168. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/execution/test_agent_helpers.py +0 -0
  169. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/execution/test_cancellation.py +0 -0
  170. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/execution/test_directory_validation.py +0 -0
  171. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/execution/test_metrics.py +0 -0
  172. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/execution/test_multipart_input.py +0 -0
  173. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/execution/test_session_lock_lifecycle.py +0 -0
  174. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/execution/test_session_ownership.py +0 -0
  175. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/execution/test_streaming_output_contract_blocks.py +0 -0
  176. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/execution/test_streaming_output_contract_core.py +0 -0
  177. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/execution/test_streaming_output_contract_interrupts.py +0 -0
  178. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/execution/test_streaming_output_contract_logging.py +0 -0
  179. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/jsonrpc/__init__.py +0 -0
  180. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/jsonrpc/test_application_dispatch.py +0 -0
  181. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/jsonrpc/test_dispatch_registry.py +0 -0
  182. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/jsonrpc/test_error_responses.py +0 -0
  183. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/jsonrpc/test_jsonrpc_methods.py +0 -0
  184. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/jsonrpc/test_jsonrpc_params.py +0 -0
  185. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/jsonrpc/test_jsonrpc_unsupported_method.py +0 -0
  186. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/jsonrpc/test_opencode_session_extension_commands.py +0 -0
  187. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/jsonrpc/test_opencode_session_extension_interrupts.py +0 -0
  188. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/jsonrpc/test_opencode_session_extension_lifecycle.py +0 -0
  189. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/jsonrpc/test_opencode_session_extension_prompt_async.py +0 -0
  190. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/package/__init__.py +0 -0
  191. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/package/test_logging.py +0 -0
  192. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/package/test_typing_contract.py +0 -0
  193. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/package/test_version.py +0 -0
  194. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/parts/__init__.py +0 -0
  195. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/parts/test_parts_text.py +0 -0
  196. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/profile/__init__.py +0 -0
  197. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/scripts/__init__.py +0 -0
  198. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/scripts/test_script_health_contract.py +0 -0
  199. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/server/__init__.py +0 -0
  200. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/server/test_auth.py +0 -0
  201. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/server/test_call_context_builder.py +0 -0
  202. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/server/test_cancel_contract.py +0 -0
  203. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/server/test_cli.py +0 -0
  204. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/server/test_context_helpers.py +0 -0
  205. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/server/test_database_app_persistence.py +0 -0
  206. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/server/test_output_negotiation.py +0 -0
  207. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/server/test_transport_contract.py +0 -0
  208. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/support/__init__.py +0 -0
  209. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/support/async_iterators.py +0 -0
  210. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/support/fake_client_errors.py +0 -0
  211. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/support/helpers.py +0 -0
  212. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/support/interrupt_clients.py +0 -0
  213. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/support/jsonrpc_error_assertions.py +0 -0
  214. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/support/session_extensions.py +0 -0
  215. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/support/settings.py +0 -0
  216. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/support/streaming_output.py +0 -0
  217. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/test_a2a_utils.py +0 -0
  218. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/test_metadata_access.py +0 -0
  219. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/test_parsing.py +0 -0
  220. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/test_trace_context.py +0 -0
  221. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/upstream/__init__.py +0 -0
  222. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/tests/upstream/test_opencode_upstream_client_params.py +0 -0
  223. {opencode_a2a-1.2.0 → opencode_a2a-1.3.1}/uv.lock +0 -0
@@ -146,11 +146,30 @@ jobs:
146
146
 
147
147
  ensure_release
148
148
 
149
+ python - <<'PY'
150
+ import hashlib
151
+ from pathlib import Path
152
+
153
+ dist_dir = Path("dist")
154
+ lines = []
155
+ for asset in sorted(
156
+ (
157
+ path
158
+ for path in dist_dir.iterdir()
159
+ if path.is_file() and path.name.endswith((".whl", ".tar.gz"))
160
+ ),
161
+ key=lambda item: item.name,
162
+ ):
163
+ digest = hashlib.sha256(asset.read_bytes()).hexdigest()
164
+ lines.append(f"{digest} {asset.name}")
165
+ (dist_dir / "SHA256SUMS").write_text("\n".join(lines) + "\n", encoding="utf-8")
166
+ PY
167
+
149
168
  mapfile -t existing_assets < <(
150
169
  retry 3 5 gh release view "$RELEASE_TAG" --json assets --jq '.assets[].name'
151
170
  )
152
171
 
153
- for asset in dist/*.tar.gz dist/*.whl; do
172
+ for asset in dist/*.tar.gz dist/*.whl dist/SHA256SUMS; do
154
173
  asset_name="$(basename "$asset")"
155
174
  if printf '%s\n' "${existing_assets[@]}" | grep -Fxq "$asset_name"; then
156
175
  echo "Release asset already present: $asset_name"
@@ -71,6 +71,14 @@ uv run pytest tests/contracts/test_extension_contract_consistency.py
71
71
  uv run mypy src/opencode_a2a
72
72
  ```
73
73
 
74
+ If you change outbound authentication, tracing, protocol-version, or extension headers, run the SDK transport regression directly:
75
+
76
+ ```bash
77
+ uv run pytest --no-cov tests/client/test_client_http_headers.py
78
+ ```
79
+
80
+ Assertions against a mocked SDK client or `ClientCallContext` alone do not prove that headers reached the HTTP request. Keep at least one regression test across Agent Card resolution, the real SDK transport, and the final `httpx.Request` boundary.
81
+
74
82
  ## Change Expectations
75
83
 
76
84
  - Keep code, comments, and docs in English.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencode-a2a
3
- Version: 1.2.0
3
+ Version: 1.3.1
4
4
  Summary: OpenCode A2A runtime
5
5
  Author: liujuanjuan1984@Intelligent-Internet
6
6
  License-Expression: Apache-2.0
@@ -218,6 +218,7 @@ Read before deployment:
218
218
  - [docs/maintainer-architecture.md](docs/maintainer-architecture.md) Internal module boundaries and maintainer call chains.
219
219
  - [docs/compatibility.md](docs/compatibility.md) Compatibility-sensitive surface and contract-honesty guidance.
220
220
  - [docs/guide.md](docs/guide.md) Usage guide, transport details, streaming behavior, extensions, and examples.
221
+ - [docs/security-architecture.md](docs/security-architecture.md) Security surface, boundaries, and residual-risk register.
221
222
  - [docs/conformance.md](docs/conformance.md) External TCK experiment workflow and artifact handling.
222
223
  - [SECURITY.md](SECURITY.md) Threat model, deployment caveats, and vulnerability disclosure guidance.
223
224
 
@@ -176,6 +176,7 @@ Read before deployment:
176
176
  - [docs/maintainer-architecture.md](docs/maintainer-architecture.md) Internal module boundaries and maintainer call chains.
177
177
  - [docs/compatibility.md](docs/compatibility.md) Compatibility-sensitive surface and contract-honesty guidance.
178
178
  - [docs/guide.md](docs/guide.md) Usage guide, transport details, streaming behavior, extensions, and examples.
179
+ - [docs/security-architecture.md](docs/security-architecture.md) Security surface, boundaries, and residual-risk register.
179
180
  - [docs/conformance.md](docs/conformance.md) External TCK experiment workflow and artifact handling.
180
181
  - [SECURITY.md](SECURITY.md) Threat model, deployment caveats, and vulnerability disclosure guidance.
181
182
 
@@ -24,6 +24,9 @@ This project is currently best suited for trusted or internal environments. Impo
24
24
  - static credential auth only by default; stronger identity propagation is still a follow-up hardening area
25
25
  - operators remain responsible for host hardening, secret rotation, process access controls, and reverse-proxy exposure strategy
26
26
 
27
+ For the security surface mapping and residual-risk register, see
28
+ [docs/security-architecture.md](./docs/security-architecture.md).
29
+
27
30
  ## Reporting a Vulnerability
28
31
 
29
32
  Please avoid posting active secrets, bearer tokens, or reproduction payloads that contain private data in public issues.
@@ -96,6 +96,7 @@ Use the docs by responsibility:
96
96
  - [Usage Guide](guide.md): runtime configuration, transport contracts, extensions, and examples
97
97
  - [Maintainer Architecture Guide](maintainer-architecture.md): internal module boundaries, request call chains, and persistence touchpoints
98
98
  - [Extension Specifications](extension-specifications.md): stable extension URI/spec index and disclosure policy
99
+ - [Security Architecture](security-architecture.md): security surface mapping and residual-risk register
99
100
  - [Conformance Notes](conformance.md): external TCK experiment workflow
100
101
  - [Contributing Guide](../CONTRIBUTING.md): contributor workflow and validation
101
102
  - [Security Policy](../SECURITY.md): threat model and disclosure guidance
@@ -100,7 +100,7 @@ The supported persistence profile is one application process using its own local
100
100
  - Deployment-conditional methods must be declared as conditional rather than silently disappearing.
101
101
  - `opencode.sessions.prompt_async` input-part passthrough is compatibility-sensitive. Changes to supported part types, passthrough field semantics, or rejection behavior should be treated as wire-level changes.
102
102
  - `opencode.sessions.shell` is compatibility-sensitive as a deployment-conditional shell snapshot surface. It should not silently widen into a general interactive shell API.
103
- - `opencode.workspaces.*` and `opencode.worktrees.*` are boundary-sensitive and should remain explicitly provider-private, operator-scoped, and deployment-conditional where applicable.
103
+ - `opencode.workspaces.*` and `opencode.worktrees.*` are boundary-sensitive and should remain explicitly provider-private, operator-scoped, and deployment-conditional where applicable. Discovery responses are normalized summaries that exclude upstream local paths (`directory`, `canonical`, worktree paths) and raw/internal fields.
104
104
  - Interrupt callback and recovery methods are compatibility-sensitive because clients may depend on request ID lifecycle, expiry semantics, and identity scoping.
105
105
  - Agent Card media modes and `acceptedOutputModes` handling are compatibility-sensitive. Changes to declared chat modes, to task-scoped negotiation persistence, or to output filtering of structured tool payloads should be treated as wire-level changes.
106
106
  - Agent Card and OpenAPI publication of `protocol_compatibility`, `service_behaviors`, and runtime feature toggles is compatibility-sensitive discoverability surface.
@@ -130,6 +130,7 @@ Extension URI: `urn:opencode-a2a:extension:workspace-control:v1`
130
130
  - Methods: stable project discovery plus experimental workspace/worktree discovery and deployment-conditional mutation methods
131
131
  - Dependencies: none declared by this version
132
132
  - Security boundary: this extension is provider-private, operator-scoped, and partly deployment-conditional
133
+ - Response hygiene: discovery responses are normalized summaries; upstream local paths (`directory`, `canonical`, worktree paths), raw entries, and credential-like fields are excluded
133
134
  - Versioning: breaking changes require a new versioned URI
134
135
 
135
136
  ## OpenCode Interrupt Recovery v1
@@ -32,12 +32,15 @@ Key variables to understand protocol behavior:
32
32
  - `A2A_ALLOW_DIRECTORY_OVERRIDE`: controls whether clients may pass `metadata.opencode.directory`.
33
33
  - `A2A_ENABLE_SESSION_SHELL`: gates high-risk JSON-RPC method `opencode.sessions.shell`.
34
34
  - `A2A_ENABLE_WORKSPACE_MUTATIONS`: gates operator-only workspace/worktree mutation methods such as `opencode.workspaces.create` and `opencode.worktrees.reset`.
35
+ - `A2A_EXPOSE_WORKSPACE_ROOT_IN_CARD`: default `false`; controls whether the authenticated extended Agent Card includes the local `OPENCODE_WORKSPACE_ROOT` in the structured profile `runtime_context`. Keep disabled unless trusted callers explicitly need the host path.
35
36
  - `A2A_SANDBOX_MODE` / `A2A_SANDBOX_FILESYSTEM_SCOPE` / `A2A_SANDBOX_WRITABLE_ROOTS`: declarative execution-boundary metadata for sandbox mode, filesystem scope, and optional writable roots.
36
37
  - `A2A_NETWORK_ACCESS` / `A2A_NETWORK_ALLOWED_DOMAINS`: declarative execution-boundary metadata for network policy and optional allowlist disclosure.
37
38
  - `A2A_APPROVAL_POLICY` / `A2A_APPROVAL_ESCALATION_BEHAVIOR`: declarative execution-boundary metadata for approval workflow.
38
39
  - `A2A_WRITE_ACCESS_SCOPE` / `A2A_WRITE_ACCESS_OUTSIDE_WORKSPACE`: declarative execution-boundary metadata for write scope and whether writes may extend outside the primary workspace boundary.
39
40
  - `A2A_HOST` / `A2A_PORT`: runtime bind address. Defaults: `127.0.0.1:8000`.
40
41
  - `A2A_PUBLIC_URL`: public base URL advertised by the Agent Card. Default: `http://127.0.0.1:8000`.
42
+ - `A2A_ALLOWED_ORIGINS`: comma-separated extra browser-origin allowlist for inbound requests. Requests carrying an `Origin` header must match the origin of `A2A_PUBLIC_URL` or an entry here; mismatches are rejected with `403` (CSRF guard). Requests without an `Origin` header (CLI/SDK clients) are unaffected.
43
+ - `A2A_ALLOWED_HOSTS`: comma-separated `Host` header allowlist (exact names or `*.example.com` wildcards). When configured, every inbound request must present a matching `Host` header. Binding to a non-loopback address without this allowlist logs a startup warning (DNS rebinding risk).
41
44
  - `A2A_LOG_LEVEL`: runtime log level. Default: `WARNING`.
42
45
  - `A2A_LOG_PAYLOADS` / `A2A_LOG_BODY_LIMIT`: payload logging behavior and truncation. When `A2A_LOG_LEVEL=DEBUG`, upstream OpenCode stream events are also logged with preview truncation controlled by `A2A_LOG_BODY_LIMIT`.
43
46
  - The runtime accepts W3C `traceparent` / `tracestate` headers on inbound requests. When `traceparent` is missing or invalid, the runtime generates a fresh valid value and exposes it on the HTTP response header.
@@ -45,6 +48,8 @@ Key variables to understand protocol behavior:
45
48
  - Logs derive a stable `trace_id` from the active `traceparent` so request-scoped log lines can be correlated without introducing high-cardinality metric labels.
46
49
  - `A2A_HTTP_GZIP_MINIMUM_SIZE`: minimum eligible response-body size in bytes for global non-streaming HTTP gzip compression. Default: `8192`.
47
50
  - `A2A_MAX_REQUEST_BODY_BYTES`: runtime request-body limit. Oversized requests return HTTP `413`.
51
+ - `A2A_RATE_LIMIT_ENABLED` / `A2A_RATE_LIMIT_WINDOW_SECONDS` / `A2A_RATE_LIMIT_MAX_REQUESTS`: per-credential sliding-window rate limiting (per-peer-IP for the unauthenticated Agent Card surface). Defaults: `true` / `60` seconds / `120` requests. Exceeding the window returns HTTP `429` with a `Retry-After` header; `A2A_RATE_LIMIT_ENABLED=false` disables the limiter.
52
+ - `A2A_STREAM_MAX_BYTES` / `A2A_STREAM_MAX_DURATION_SECONDS` / `A2A_STREAM_IDLE_TIMEOUT_SECONDS`: streaming (SSE) response budgets for total bytes, total duration, and idle gap. Defaults: `67108864` bytes / `3600` seconds / `120` seconds; `0` disables the respective budget.
48
53
  - `A2A_PENDING_SESSION_CLAIM_TTL_SECONDS`: lease duration for pending preferred session claims before they expire and stop blocking other identities.
49
54
  - `A2A_INTERRUPT_REQUEST_TTL_SECONDS`: active retention window for the interrupt request binding registry used by `a2a.interrupt.*` callback methods. Default: `10800` seconds (`180` minutes).
50
55
  - `A2A_INTERRUPT_REQUEST_TOMBSTONE_TTL_SECONDS`: retention window for expired interrupt tombstones after active TTL has elapsed. During this window, repeated replies keep returning `INTERRUPT_REQUEST_EXPIRED` instead of falling through to `INTERRUPT_REQUEST_NOT_FOUND`. Default: `600` seconds (`10` minutes).
@@ -59,6 +64,8 @@ Key variables to understand protocol behavior:
59
64
  - `A2A_CLIENT_BEARER_TOKEN`: optional bearer token attached to outbound peer calls made by the embedded A2A client and `a2a_call` tool path.
60
65
  - `A2A_CLIENT_BASIC_AUTH`: optional Basic auth credential attached to outbound peer calls made by the embedded A2A client and `a2a_call` tool path.
61
66
  - `A2A_CLIENT_SUPPORTED_TRANSPORTS`: ordered outbound transport preference list.
67
+ - `A2A_CLIENT_ALLOWED_HOSTS`: comma-separated allowlist of outbound target hosts for the embedded A2A client and `a2a_call` tool path (exact names or `*.example.com` wildcards). When configured, outbound calls are restricted to matching hosts, and outbound credentials (`A2A_CLIENT_BEARER_TOKEN` / `A2A_CLIENT_BASIC_AUTH`) are only sent to allowlisted hosts.
68
+ - `A2A_CLIENT_ALLOW_PRIVATE_HOSTS`: default `false`. When `false`, outbound `a2a_call(...)` targets that resolve to private, loopback, link-local, reserved, or multicast addresses are rejected (SSRF / DNS-rebinding guard). Set to `true` only when the deployment intentionally targets A2A peers on the local network.
62
69
  - `A2A_TASK_STORE_BACKEND`: unified lightweight persistence backend for SDK task rows plus adapter-managed session / interrupt state. Supported values: `database`, `memory`. Default: `database`.
63
70
  - `A2A_TASK_STORE_DATABASE_URL`: database URL used by the unified durable backend when `A2A_TASK_STORE_BACKEND=database`. Default: `sqlite+aiosqlite:///./opencode-a2a.db`.
64
71
  - On startup, the runtime only auto-migrates adapter-owned state tables; existing SDK-owned task tables must be upgraded explicitly with upstream `a2a-db`.
@@ -84,6 +91,20 @@ Key variables to understand protocol behavior:
84
91
  - `A2A_CLIENT_BASIC_AUTH`
85
92
  - `A2A_CLIENT_SUPPORTED_TRANSPORTS`
86
93
 
94
+ ## Inbound Origin and Host Boundary
95
+
96
+ Browsers store and automatically attach Basic credentials, and they send an `Origin` header with every request. Without an origin boundary, a malicious web page could cross-site trigger `SendMessage`, `CancelTask`, or task subscription against a Basic-protected service. The runtime therefore enforces:
97
+
98
+ - every request carrying an `Origin` header must match the origin of `A2A_PUBLIC_URL` (scheme, host, and non-default port) or an entry in `A2A_ALLOWED_ORIGINS`; mismatches return `403` before authentication. `Origin: null` (sandboxed iframes) is rejected unless explicitly allowlisted;
99
+ - requests without an `Origin` header — normal CLI/SDK peers — are not subject to origin checks;
100
+ - when `A2A_ALLOWED_HOSTS` is configured, every request must present a matching `Host` header (exact names, optional `host:port`, or `*.example.com` wildcards); mismatches return `403`. This also blocks DNS rebinding, where an attacker-controlled hostname resolves to the service address and the browser sends that hostname as `Host`;
101
+ - binding to a non-loopback address (`0.0.0.0`, `::`, or a LAN/interface address) without `A2A_ALLOWED_HOSTS` logs a startup warning: the service is then exposed to DNS rebinding and should only run behind a trusted network boundary or a reverse proxy that validates `Host`.
102
+
103
+ For browser-based clients, prefer the same origin as `A2A_PUBLIC_URL`, or explicitly allow the dashboard origin via `A2A_ALLOWED_ORIGINS` and treat `A2A_ALLOWED_HOSTS` as part of the deployment contract. Basic authentication should not be relied on as the sole browser-facing boundary: combine it with the origin/host checks above, short-lived credentials, and TLS at the public URL.
104
+
105
+ The security surface mapping and residual-risk register live in
106
+ [security-architecture.md](./security-architecture.md).
107
+
87
108
  ## Client Initialization Facade (Preview)
88
109
 
89
110
  `opencode-a2a` now includes a minimal client bootstrap module in `src/opencode_a2a/client/` to support downstream consumer usage while keeping server and client concerns separate.
@@ -103,6 +124,14 @@ Current client facade API:
103
124
 
104
125
  Server-side outbound peer calls read outbound credentials from environment variables. Configure `A2A_CLIENT_BEARER_TOKEN` or `A2A_CLIENT_BASIC_AUTH` when the remote agent protects its runtime surface. CLI outbound calls follow the same environment-only model.
105
126
 
127
+ The embedded `a2a_call(...)` tool lets the upstream model choose the target URL, so the adapter applies a fail-closed network policy before opening any connection:
128
+
129
+ - only `http`/`https` schemes are accepted, and URLs carrying userinfo credentials are rejected;
130
+ - when `A2A_CLIENT_ALLOWED_HOSTS` is set, the target host must match the allowlist (exact or `*.example.com` wildcard);
131
+ - unless `A2A_CLIENT_ALLOW_PRIVATE_HOSTS=true`, the resolved addresses must be public; private/loopback/link-local/reserved addresses are rejected even when the hostname matches the allowlist (DNS rebinding defense);
132
+ - outbound credentials are only attached to allowlisted hosts. If credentials are configured without an allowlist, they are never sent and a warning is logged;
133
+ - the `opencode-a2a call` CLI remains a manual operator action and is not subject to the allowlist, but still rejects non-http(s) schemes through the shared client URL handling.
134
+
106
135
  CLI outbound example:
107
136
 
108
137
  ```bash
@@ -181,6 +210,24 @@ The supported persistence profile is one `opencode-a2a` application process usin
181
210
 
182
211
  The runtime configures local durability-oriented SQLite connection settings (`WAL`, `busy_timeout`, `synchronous=NORMAL`) and creates missing parent directories for file-backed database paths.
183
212
 
213
+ ### SQLite Persistence Hardening
214
+
215
+ File-backed SQLite databases are hardened at startup and on every new connection on POSIX systems:
216
+
217
+ - the database file must be a regular file; symlinks are rejected and fail startup;
218
+ - the file must be owned by the user running `opencode-a2a`; a database owned by another user fails startup;
219
+ - the database file is forced to mode `0600`, and existing SQLite sidecar files (`-wal`/`-shm`/`-journal`) are converged to the same mode;
220
+ - the parent directory is created with mode `0700` when it does not exist; keep the database directory private (no group/world access);
221
+ - if the database is replaced by a symlink, a special file, or a foreign-owned file while the service is running, the next connection fails closed.
222
+
223
+ `A2A_TASK_STORE_DATABASE_URL` values that do not map to a plain file path are exempt: `:memory:` and `file:` URI-style database components (including in-memory shared-cache databases) are not hardened. Prefer a plain absolute file path for deployments that need the startup guarantees. Non-POSIX platforms rely on the user account ACLs of the hosting directory; the same private-directory requirement applies there.
224
+
225
+ Deployment requirements:
226
+
227
+ - run `opencode-a2a` as a dedicated user and keep the database directory outside world-readable workspace trees;
228
+ - do not place the SQLite file on network-mounted or sync-managed directories;
229
+ - when migrating an existing database, fix ownership and mode before startup: `chown <service-user> <db>` and `chmod 600 <db>`, and ensure the parent directory is not group/world accessible.
230
+
184
231
  The runtime automatically applies lightweight schema migrations for its custom state tables and records the applied version in `a2a_schema_version`. Schema-version writes are idempotent across concurrent first-start races, pending preferred-session claims now persist absolute `expires_at` timestamps, legacy rows without `expires_at` are pruned during migration instead of being reconstructed from historical TTL assumptions, and the built-in path currently targets the local SQLite deployment profile without requiring Alembic.
185
232
 
186
233
  Database-backed task persistence also keeps the existing first-terminal-state-wins contract while tightening the SQLite path with an atomic terminal-write guard instead of relying only on process-local read-before-write checks.
@@ -260,6 +307,8 @@ If one deployment works while another fails against the same upstream provider,
260
307
 
261
308
  - Requests require either `Authorization: Bearer <token>` or a configured `Authorization: Basic <base64(username:password)>`; otherwise `401` is returned. Agent Card endpoints are public.
262
309
  - Requests above `A2A_MAX_REQUEST_BODY_BYTES` are rejected with HTTP `413` before transport handling.
310
+ - Inbound requests are rate limited with a sliding window keyed by `credential_id` when configured, otherwise by the authenticated principal; the public Agent Card surface is keyed by the direct peer IP (never `X-Forwarded-For`). Exceeding `A2A_RATE_LIMIT_MAX_REQUESTS` within `A2A_RATE_LIMIT_WINDOW_SECONDS` returns HTTP `429` with a `Retry-After` header. The limiter is process-local; multi-process deployments should place a shared gateway in front or rely on per-instance limits.
311
+ - Streaming responses (JSON-RPC `SendStreamingMessage` / `SubscribeToTask` and REST `/message:stream` / `/tasks/{id}:subscribe`) are bounded by `A2A_STREAM_MAX_BYTES`, `A2A_STREAM_MAX_DURATION_SECONDS`, and `A2A_STREAM_IDLE_TIMEOUT_SECONDS`; when a budget is exceeded the server ends the stream with an SSE `event: error` frame. If the very first event already exceeds the budget, REST requests are rejected with HTTP `429` `RESOURCE_EXHAUSTED` before the SSE stream starts.
263
312
  - For validation failures, missing context (`task_id` / `context_id`), or internal errors, the service attempts to return standard A2A failure events via `event_queue`.
264
313
  - Failure events include concrete error details with `failed` state.
265
314
 
@@ -1039,6 +1088,7 @@ Behavior notes:
1039
1088
  - `metadata.opencode.workspace.id` is declared consistently across the adapter, but current workspace-control methods do not use it to change the target project.
1040
1089
  - `opencode.workspaces.*` and `opencode.worktrees.*` currently wrap upstream `/experimental/workspace` and `/experimental/worktree` endpoints; treat them as experimental-upstream surfaces even when declared by the adapter.
1041
1090
  - Mutating methods should be treated as operator-only control-plane actions and are disabled by default.
1091
+ - Discovery responses are normalized provider-private summaries: upstream entries are filtered to stable fields only (`id`/`name`/`vcs` for projects, `id`/`type`/`name`/`branch` for workspaces, `name`/`branch` for worktrees) and never include upstream local paths (`directory`, `canonical`, worktree paths), raw entries, or credential-like fields.
1042
1092
 
1043
1093
  ### Project Discovery (`opencode.projects.list`, `opencode.projects.current`)
1044
1094
 
@@ -1056,8 +1106,8 @@ curl -sS http://127.0.0.1:8000/ \
1056
1106
 
1057
1107
  Response:
1058
1108
 
1059
- - `opencode.projects.list` => `{"items": [...]}`
1060
- - `opencode.projects.current` => `{"item": {...}}`
1109
+ - `opencode.projects.list` => `{"items": [{"id": "<id>", "name": "<name>", "vcs": "<vcs>"}]}` (normalized summaries; no local paths)
1110
+ - `opencode.projects.current` => `{"item": {"id": "<id>", "name": "<name>", "vcs": "<vcs>"}}` (normalized summary; no local paths)
1061
1111
 
1062
1112
  ### Workspace Discovery
1063
1113
 
@@ -1075,7 +1125,7 @@ curl -sS http://127.0.0.1:8000/ \
1075
1125
 
1076
1126
  Response:
1077
1127
 
1078
- - `opencode.workspaces.list` => `{"items": [...]}`
1128
+ - `opencode.workspaces.list` => `{"items": [{"id": "<id>", "type": "<type>", "name": "<name>", "branch": "<branch>"}]}` (normalized summaries; no local paths)
1079
1129
 
1080
1130
  ### Workspace Mutation
1081
1131
 
@@ -128,4 +128,7 @@ For maintainers new to the codebase, this order usually gives the fastest payoff
128
128
  3. `src/opencode_a2a/server/application.py`
129
129
  4. `src/opencode_a2a/execution/executor.py`
130
130
  5. `src/opencode_a2a/contracts/extensions/`
131
+
132
+ For security-focused contributors, `docs/security-architecture.md` additionally
133
+ consolidates the security surface mapping and residual-risk register.
131
134
  6. `docs/guide.md`
@@ -0,0 +1,175 @@
1
+ # Security Architecture, Surface, and Residual Risks
2
+
3
+ This document is the single source of truth for the adapter's security
4
+ surface: network listeners and routes, authentication and authorization, input
5
+ limits and boundary guards, outbound side effects, the end-to-end mapping from
6
+ remote A2A input to OpenCode side effects, and the residual-risk register.
7
+
8
+ It is a living document. Security-relevant changes must update the mapping and
9
+ the risk register in the same change that alters the behavior. It complements,
10
+ and does not replace:
11
+
12
+ - [SECURITY.md](../SECURITY.md) — threat model, trust boundary, and
13
+ vulnerability reporting;
14
+ - [guide.md](./guide.md) — operational configuration, defaults, and runbooks;
15
+ - [compatibility.md](./compatibility.md) — wire-level compatibility promises.
16
+
17
+ One-off audit conclusions, review records, and test plans are intentionally
18
+ not stored here; they live in the issue/PR history of the repository.
19
+
20
+ ## Network Surface
21
+
22
+ ### Listeners and Bind
23
+
24
+ - The service is a FastAPI/uvicorn app bound to `A2A_HOST:A2A_PORT`
25
+ (defaults `127.0.0.1:8000`).
26
+ - Binding to a non-loopback address without `A2A_ALLOWED_HOSTS` logs a startup
27
+ warning (DNS-rebinding exposure); see residual risk R-3.
28
+
29
+ ### HTTP+JSON (REST) Routes
30
+
31
+ All REST routes are served at the root path (no `/v1` prefix).
32
+
33
+ | Method | Path | Purpose | Auth |
34
+ | --- | --- | --- | --- |
35
+ | GET | `/.well-known/agent-card.json` | Public Agent Card | Anonymous |
36
+ | GET | `/extendedAgentCard` | Authenticated extended Agent Card | Credential |
37
+ | POST | `/message:send` | Send a task message (unary) | Credential |
38
+ | POST | `/message:stream` | Send a task message (SSE stream) | Credential |
39
+ | POST | `/tasks/{id}:cancel` | Cancel a task | Credential |
40
+ | GET/POST | `/tasks/{id}:subscribe` | Subscribe to a task stream | Credential |
41
+ | GET | `/tasks/{id}` | Get a task | Credential |
42
+ | GET | `/tasks` | List tasks | Credential |
43
+ | GET/POST/DELETE | `/tasks/{id}/pushNotificationConfigs[/{push_id}]` | Push notification config (exposed-but-unsupported) | Credential |
44
+ | POST | `/` | JSON-RPC endpoint (core + extensions) | Credential |
45
+ | GET | `/health` | Health / runtime profile | Anonymous |
46
+ | GET | `/openapi.json` | Sanitized OpenAPI contract | Anonymous |
47
+
48
+ ### JSON-RPC Surface
49
+
50
+ - Core A2A methods from the SDK dispatcher: `message/send`, `message/stream`,
51
+ `tasks/get`, `tasks/cancel`, `tasks/list`, `tasks/subscribe`,
52
+ `tasks/pushNotificationConfig/*`.
53
+ - Provider-private `opencode.*` extensions (authenticated extended card only):
54
+ - `opencode.sessions.*`: `status`, `list`, `messages.list`, `get`, `children`,
55
+ `todo`, `diff`, `message.get`, `prompt_async`, `command`, `fork`, `share`,
56
+ `unshare`, `summarize`, `revert`, `unrevert`, `shell`
57
+ (deployment-conditional);
58
+ - `opencode.providers.*` / `opencode.models.*`: `list_providers`,
59
+ `list_models`;
60
+ - `opencode.projects.*` / `opencode.workspaces.*` / `opencode.worktrees.*`:
61
+ discovery plus gated mutations (`create_workspace`, `remove_workspace`,
62
+ `create_worktree`, `remove_worktree`, `reset_worktree`);
63
+ - `opencode.interrupt.*`: `list_permissions`, `list_questions`,
64
+ `reply_permission`, `reply_question`, `reject_question`.
65
+
66
+ ### Authentication and Authorization
67
+
68
+ - `A2A_STATIC_AUTH_CREDENTIALS` (Basic/Bearer) is enforced before routing on
69
+ every credential-protected endpoint; the public Agent Card and `/health` are
70
+ anonymous.
71
+ - `credential_id` is carried as runtime metadata for audit/logging/rotation; it
72
+ does not participate in principal resolution or authorization.
73
+ - Capability gates:
74
+ - `A2A_ENABLE_SESSION_SHELL` → `CAPABILITY_SESSION_SHELL`;
75
+ - `A2A_ENABLE_WORKSPACE_MUTATIONS` → `CAPABILITY_WORKSPACE_MUTATION`;
76
+ - enforced via `request_has_capability` at the handler boundary.
77
+ - The runtime is a single-tenant trust boundary by design; see
78
+ [SECURITY.md](../SECURITY.md) for the threat model.
79
+
80
+ ### Input Limits and Boundary Guards
81
+
82
+ | Guard | Mechanism | Defaults / Notes |
83
+ | --- | --- | --- |
84
+ | CSRF / Origin | `A2A_ALLOWED_ORIGINS` + `A2A_PUBLIC_URL` origin | `Origin`-bearing requests must match; `Origin: null` rejected |
85
+ | DNS rebinding / Host | `A2A_ALLOWED_HOSTS` | Enforced when configured; startup warning otherwise on non-loopback bind |
86
+ | Body size | `A2A_MAX_REQUEST_BODY_BYTES` | 1 MiB |
87
+ | Rate limit | `A2A_RATE_LIMIT_ENABLED` / `_WINDOW_SECONDS` / `_MAX_REQUESTS` | On by default; 60 s window, 120 requests; 429 + `Retry-After` |
88
+ | Stream budgets | `A2A_STREAM_MAX_BYTES` / `_MAX_DURATION_SECONDS` / `_IDLE_TIMEOUT_SECONDS` | 64 MiB / 3600 s / 120 s; `0` disables |
89
+ | Payload logging | `A2A_LOG_PAYLOADS` / `A2A_LOG_BODY_LIMIT` | Opt-in; logs treated as sensitive |
90
+
91
+ ### Outbound Side Effects
92
+
93
+ - `a2a_call` tool execution → `server/client_manager.py` (`borrow_client`) →
94
+ `client/network_policy.py` validation: http/https only, no userinfo,
95
+ `A2A_CLIENT_ALLOWED_HOSTS` allowlist, private/loopback address rejection, and
96
+ credentials attached to allowlisted hosts only.
97
+ - CLI `opencode-a2a call` is operator-invoked and shares URL handling, but is
98
+ not allowlist-bound.
99
+ - The upstream OpenCode client (`OPENCODE_BASE_URL`, auth, timeouts, concurrency
100
+ caps) is the only other outbound path.
101
+
102
+ ## Security Controls
103
+
104
+ ### Error Text Redaction
105
+
106
+ Client-visible error text must never expose absolute local filesystem paths.
107
+ All error responses that can leave the process pass through a single
108
+ deterministic masker (`opencode_a2a.redact.redact_absolute_paths`) before
109
+ serialization; masked output uses the fixed placeholder `<redacted-path>`.
110
+
111
+ Boundaries covered:
112
+
113
+ - Streaming task error messages — `execution/executor.py:_emit_error`
114
+ (task status messages and streamed error artifacts).
115
+ - JSON-RPC error responses — `jsonrpc/error_responses.py:adapt_jsonrpc_error`
116
+ (message, metadata values, and standard JSON-RPC error-code `data`).
117
+ - REST/HTTP error bodies — `jsonrpc/error_responses.py:build_http_error_body`.
118
+ - Raw-exception fallback — `jsonrpc/application.py:_generate_error_response`
119
+ (SDK base-class wrapping of non-JSON-RPC exceptions).
120
+
121
+ Masked: POSIX/Windows/UNC absolute paths and `file://` local URIs. Preserved:
122
+ remote URLs (`scheme://host/path`), relative paths, ordinary prose, and
123
+ slash-prefixed API route tokens such as `/message:send` or `/tasks/{id}:cancel`
124
+ (a path-like token immediately followed by a `:` method suffix or `{` route
125
+ template is treated as an API route, not a local path). Server-side logs
126
+ intentionally retain full exception context for diagnosability; logs that
127
+ leave the host must be redacted or access-restricted before export.
128
+
129
+ Remote peer error text (for example the upstream `detail` field surfaced by
130
+ `execution/upstream_error_translator.py`) is remote content, not a local path
131
+ leak; it is out of scope for this control and should be assessed separately if
132
+ it becomes a trust concern.
133
+
134
+ ### Release Integrity
135
+
136
+ Every GitHub Release must ship a `SHA256SUMS` checksum manifest alongside the
137
+ wheel and sdist artifacts so consumers can verify artifact integrity
138
+ independently of the registry. `.github/workflows/publish.yml` regenerates
139
+ `dist/SHA256SUMS` at publish time from the built assets (sorted by basename,
140
+ `sha256sum -c` compatible) and uploads it as a release asset; existing assets
141
+ are skipped idempotently.
142
+
143
+ ## End-to-End Mapping: Remote A2A Input → OpenCode Side Effects
144
+
145
+ | A2A input | Adapter path | OpenCode / host side effects |
146
+ | --- | --- | --- |
147
+ | `message/send` / `message/stream` | REST or JSON-RPC → middleware guards → `execution/executor.py` → `opencode_upstream_client.py` | Upstream OpenCode session prompt/command; stream normalization; task/message persistence |
148
+ | Session extension queries/mutations | `jsonrpc/handlers/session_*.py` | Read/shape session state; `fork`/`share`/`revert` mutate upstream session state |
149
+ | Workspace/worktree control | `jsonrpc/handlers/workspace_control.py` (mutation gated) | Project/workspace/worktree create/remove/reset → filesystem side effects inside the OpenCode workspace; responses normalized (no local paths) |
150
+ | Interrupt recovery/callback | `jsonrpc/handlers/interrupt_*.py` | Request-ID lifecycle, expiry, identity-scoped state |
151
+ | Tool execution during a session | `execution/tool_orchestration.py` | `a2a_call` may trigger remote agent work; other tools remain OpenCode-runtime behavior |
152
+ | Task/session persistence | `server/task_store.py`, `state_store.py`, `migrations.py` | SQLite task/state rows (hardened file access) |
153
+
154
+ ## Residual Risk Register
155
+
156
+ | ID | Risk | Status | Mitigation / Reference |
157
+ | --- | --- | --- | --- |
158
+ | R-1 | Outbound DNS-rebinding TOCTOU: `a2a_call` validation resolves the host, then the connection re-resolves (resolve-then-connect) | Accepted | Allowlist + private-IP rejection reduce exposure; pinned-IP connect is not implemented. [guide.md](./guide.md) "Client Initialization Facade" |
159
+ | R-2 | Rate limiter is process-local; multi-process deployments bypass per-credential limits | Accepted | Documented; use a gateway-level limiter or per-instance limits. [guide.md](./guide.md) "Auth, Limits, and Failure Contract" |
160
+ | R-3 | Non-loopback bind without `A2A_ALLOWED_HOSTS` only warns at startup | Accepted | Operator contract: trusted network or reverse proxy validates Host. [guide.md](./guide.md) "Inbound Origin and Host Boundary" |
161
+ | R-4 | Single-tenant shared-workspace boundary; static credentials only by default | Accepted by design | Threat model and tenant guidance in [SECURITY.md](../SECURITY.md) |
162
+ | R-5 | Payload logging can capture sensitive data when enabled | Accepted | Opt-in with `A2A_LOG_BODY_LIMIT` cap; treat logs as sensitive. [SECURITY.md](../SECURITY.md) |
163
+ | R-6 | Push notification config surface exposed-but-unsupported (HTTP 501 / JSON-RPC unsupported) | Accepted | Contract kept explicitly unsupported; capability recovery is intentionally deferred |
164
+ | R-7 | SQLite hardening exempts `:memory:` / `file:` URIs and non-POSIX platforms | Accepted | Plain absolute file path recommended for deployments. [guide.md](./guide.md) "SQLite Persistence Hardening" |
165
+
166
+ ## Maintenance Rules
167
+
168
+ - This document is the canonical security-surface reference. Update the route
169
+ table, mapping, or risk register in the same change that alters listeners,
170
+ routes, authentication/authorization, input limits, outbound policy,
171
+ persistence hardening, error-text redaction boundaries, release integrity,
172
+ or known residual risks.
173
+ - Do not turn this document into a report for a specific review cycle: keep it
174
+ focused on facts that remain true for the current code and must stay
175
+ maintainable. One-off conclusions belong in issue/PR history.
@@ -102,3 +102,8 @@ ignore_missing_imports = true
102
102
 
103
103
  [tool.pytest.ini_options]
104
104
  addopts = "--cov=src/opencode_a2a --cov-report=term-missing --cov-report=json:.coverage.json --cov-fail-under=90"
105
+ filterwarnings = [
106
+ "error::pytest.PytestUnhandledThreadExceptionWarning",
107
+ "error::pytest.PytestUnraisableExceptionWarning",
108
+ "always:.*was deleted before being closed.*:ResourceWarning:aiosqlite.core",
109
+ ]
@@ -0,0 +1,155 @@
1
+ """Outbound A2A client network policy (SSRF guard and credential binding).
2
+
3
+ The embedded ``a2a_call`` tool lets the upstream model choose the target URL,
4
+ so without an explicit policy the adapter could connect to private/loopback/
5
+ link-local addresses (cloud metadata, internal services) and would attach
6
+ globally configured outbound credentials to any endpoint.
7
+
8
+ This module centralizes the checks:
9
+
10
+ - only ``http``/``https`` schemes are accepted;
11
+ - userinfo credentials in the URL are rejected;
12
+ - when ``A2A_CLIENT_ALLOWED_HOSTS`` is configured, the host must match the
13
+ allowlist (exact or ``*.example.com`` wildcard);
14
+ - unless ``A2A_CLIENT_ALLOW_PRIVATE_HOSTS`` is set, DNS resolution results are
15
+ validated and private/loopback/link-local/reserved addresses are rejected
16
+ (DNS-rebinding defense);
17
+ - outbound credentials may only be attached to hosts that match the allowlist.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import asyncio
23
+ import ipaddress
24
+ import logging
25
+ import socket
26
+ from collections.abc import Sequence
27
+ from dataclasses import dataclass
28
+ from urllib.parse import urlsplit
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ _ALLOWED_SCHEMES = frozenset({"http", "https"})
33
+
34
+ _Address = ipaddress.IPv4Address | ipaddress.IPv6Address
35
+
36
+
37
+ class A2ANetworkPolicyError(ValueError):
38
+ """Raised when an outbound agent URL violates the network policy."""
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class NetworkPolicyDecision:
43
+ """Outcome of validating an outbound agent URL."""
44
+
45
+ host: str
46
+ allowed_host: bool
47
+ credentials_allowed: bool
48
+
49
+
50
+ def matches_allowed_host(host: str, allowed_hosts: Sequence[str]) -> bool:
51
+ """Return whether ``host`` matches an allowlist entry.
52
+
53
+ Entries may be exact hostnames or ``*.example.com`` wildcards. A wildcard
54
+ matches any number of subdomain labels but never the bare apex domain.
55
+ """
56
+
57
+ normalized = (host or "").strip().lower().rstrip(".")
58
+ if not normalized:
59
+ return False
60
+ for raw_entry in allowed_hosts or ():
61
+ entry = (raw_entry or "").strip().lower().rstrip(".")
62
+ if not entry:
63
+ continue
64
+ if entry.startswith("*."):
65
+ suffix = entry[1:] # ".example.com"
66
+ apex = suffix[1:] # "example.com"
67
+ if normalized != apex and normalized.endswith(suffix):
68
+ return True
69
+ elif normalized == entry:
70
+ return True
71
+ return False
72
+
73
+
74
+ def _is_private_address(address: _Address) -> bool:
75
+ return bool(
76
+ address.is_private
77
+ or address.is_loopback
78
+ or address.is_link_local
79
+ or address.is_reserved
80
+ or address.is_multicast
81
+ or address.is_unspecified
82
+ )
83
+
84
+
85
+ async def resolve_host_addresses(host: str) -> tuple[str, ...]:
86
+ """Resolve ``host`` to a de-duplicated tuple of IP address strings."""
87
+
88
+ loop = asyncio.get_running_loop()
89
+ try:
90
+ infos = await loop.getaddrinfo(host, None, type=socket.SOCK_STREAM)
91
+ except socket.gaierror as exc:
92
+ raise A2ANetworkPolicyError(f"Agent URL host could not be resolved: {host!r}") from exc
93
+ addresses: list[str] = []
94
+ for info in infos:
95
+ ip = str(info[4][0])
96
+ if ip not in addresses:
97
+ addresses.append(ip)
98
+ return tuple(addresses)
99
+
100
+
101
+ async def validate_agent_url(
102
+ agent_url: str,
103
+ *,
104
+ allowed_hosts: Sequence[str] | None = None,
105
+ allow_private_hosts: bool = False,
106
+ ) -> NetworkPolicyDecision:
107
+ """Validate an outbound agent URL against the configured network policy."""
108
+
109
+ normalized = (agent_url or "").strip()
110
+ if not normalized:
111
+ raise A2ANetworkPolicyError("agent_url is required")
112
+
113
+ parsed = urlsplit(normalized)
114
+ scheme = (parsed.scheme or "").lower()
115
+ if scheme not in _ALLOWED_SCHEMES:
116
+ raise A2ANetworkPolicyError(
117
+ f"Agent URL scheme must be http or https, got {scheme or 'empty'}"
118
+ )
119
+ host = (parsed.hostname or "").strip().lower()
120
+ if not host:
121
+ raise A2ANetworkPolicyError("Agent URL must include a host")
122
+ if parsed.username or parsed.password:
123
+ raise A2ANetworkPolicyError("Agent URL must not include userinfo credentials")
124
+
125
+ allowlist = tuple(
126
+ (item or "").strip().lower().rstrip(".")
127
+ for item in allowed_hosts or ()
128
+ if (item or "").strip()
129
+ )
130
+ matched = matches_allowed_host(host, allowlist)
131
+ if allowlist and not matched:
132
+ raise A2ANetworkPolicyError(
133
+ f"Agent URL host {host!r} is not allowed by A2A_CLIENT_ALLOWED_HOSTS"
134
+ )
135
+
136
+ if not allow_private_hosts:
137
+ try:
138
+ addresses = await resolve_host_addresses(host)
139
+ except A2ANetworkPolicyError:
140
+ raise
141
+ except OSError as exc:
142
+ raise A2ANetworkPolicyError(f"Agent URL host could not be resolved: {host!r}") from exc
143
+ private = [
144
+ address for address in addresses if _is_private_address(ipaddress.ip_address(address))
145
+ ]
146
+ if private:
147
+ raise A2ANetworkPolicyError(
148
+ f"Agent URL host {host!r} resolves to a private/loopback address: {private[0]}"
149
+ )
150
+
151
+ return NetworkPolicyDecision(
152
+ host=host,
153
+ allowed_host=matched,
154
+ credentials_allowed=bool(allowlist) and matched,
155
+ )
@@ -79,16 +79,13 @@ def build_call_context(
79
79
  if extra_headers:
80
80
  merged_headers.update(extra_headers)
81
81
  normalized_extensions = [value for value in (extensions or ()) if value]
82
- service_parameters = None
83
- if normalized_extensions:
84
- service_parameters = ServiceParametersFactory.create_from(
85
- None,
86
- [with_a2a_extensions(normalized_extensions)],
87
- )
88
- return ClientCallContext(
89
- state={
90
- "headers": dict(merged_headers),
91
- "http_kwargs": {"headers": dict(merged_headers)},
92
- },
93
- service_parameters=service_parameters,
82
+ service_parameter_updates = (
83
+ [with_a2a_extensions(normalized_extensions)] if normalized_extensions else []
94
84
  )
85
+ # a2a-sdk transports serialize service parameters as HTTP headers or gRPC
86
+ # metadata. This is also the channel used by the SDK's AuthInterceptor.
87
+ service_parameters = ServiceParametersFactory.create_from(
88
+ merged_headers,
89
+ service_parameter_updates,
90
+ )
91
+ return ClientCallContext(service_parameters=service_parameters)