opencode-a2a 1.2.0__tar.gz → 1.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/.github/workflows/publish.yml +20 -1
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/PKG-INFO +2 -1
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/README.md +1 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/SECURITY.md +3 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/docs/architecture.md +1 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/docs/compatibility.md +1 -1
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/docs/extension-specifications.md +1 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/docs/guide.md +53 -3
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/docs/maintainer-architecture.md +3 -0
- opencode_a2a-1.3.0/docs/security-architecture.md +175 -0
- opencode_a2a-1.3.0/src/opencode_a2a/client/network_policy.py +155 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/config.py +40 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/execution/executor.py +2 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/jsonrpc/application.py +72 -17
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/jsonrpc/error_responses.py +11 -7
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/jsonrpc/handlers/workspace_control.py +65 -7
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/profile/runtime.py +5 -1
- opencode_a2a-1.3.0/src/opencode_a2a/redact.py +82 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/server/agent_card.py +0 -3
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/server/application.py +82 -4
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/server/client_manager.py +28 -2
- opencode_a2a-1.3.0/src/opencode_a2a/server/database.py +152 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/server/middleware.py +179 -0
- opencode_a2a-1.3.0/src/opencode_a2a/server/runtime_limits.py +199 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a.egg-info/PKG-INFO +2 -1
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a.egg-info/SOURCES.txt +11 -0
- opencode_a2a-1.3.0/tests/client/test_network_policy.py +210 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/config/test_settings.py +50 -0
- opencode_a2a-1.3.0/tests/execution/test_error_redaction.py +57 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/execution/test_opencode_agent_session_binding.py +4 -0
- opencode_a2a-1.3.0/tests/jsonrpc/test_error_redaction.py +116 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/jsonrpc/test_opencode_session_extension_queries.py +7 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/jsonrpc/test_opencode_workspace_control_extension.py +126 -5
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/profile/test_profile_runtime.py +13 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/server/test_a2a_client_manager.py +128 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/server/test_agent_card.py +18 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/server/test_app_behaviors.py +2 -1
- opencode_a2a-1.3.0/tests/server/test_database.py +219 -0
- opencode_a2a-1.3.0/tests/server/test_http_boundary.py +282 -0
- opencode_a2a-1.3.0/tests/server/test_runtime_limits.py +450 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/support/session_query_client.py +6 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/support/workspace_control_client.py +50 -6
- opencode_a2a-1.3.0/tests/test_redact.py +89 -0
- opencode_a2a-1.2.0/src/opencode_a2a/server/database.py +0 -57
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/.github/dependabot.yml +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/.github/workflows/dependency-review.yml +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/.github/workflows/validate.yml +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/.gitignore +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/.pre-commit-config.yaml +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/.secrets.baseline +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/AGENTS.md +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/CODE_OF_CONDUCT.md +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/CONTRIBUTING.md +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/LICENSE +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/MANIFEST.in +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/SUPPORT.md +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/docs/conformance-triage.md +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/docs/conformance.md +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/pyproject.toml +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/scripts/README.md +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/scripts/check_coverage.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/scripts/conformance.sh +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/scripts/dependency_health.sh +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/scripts/doctor.sh +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/scripts/find_thin_wrappers.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/scripts/health_common.sh +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/scripts/lint.sh +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/scripts/live_opencode_smoke.sh +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/scripts/smoke_test_built_cli.sh +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/setup.cfg +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/a2a_protocol.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/a2a_utils.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/auth.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/cli.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/client/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/client/agent_card.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/client/auth.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/client/client.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/client/config.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/client/error_mapping.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/client/errors.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/client/payload_text.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/client/polling.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/client/request_context.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/contracts/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/contracts/extensions/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/contracts/extensions/capabilities.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/contracts/extensions/catalog.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/contracts/extensions/compatibility.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/contracts/extensions/contract_docs.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/contracts/extensions/identifiers.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/contracts/extensions/private_params.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/contracts/extensions/public_params.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/execution/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/execution/coordinator.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/execution/event_helpers.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/execution/metrics.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/execution/session_manager.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/execution/stream_events.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/execution/stream_runtime.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/execution/stream_state.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/execution/tool_error_mapping.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/execution/tool_orchestration.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/execution/upstream_error_translator.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/extension_negotiation.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/interrupt_request_tracker.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/jsonrpc/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/jsonrpc/dispatch.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/jsonrpc/handlers/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/jsonrpc/handlers/common.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/jsonrpc/handlers/interrupt_callbacks.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/jsonrpc/handlers/interrupt_queries.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/jsonrpc/handlers/provider_discovery.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/jsonrpc/handlers/session_control.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/jsonrpc/handlers/session_lifecycle.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/jsonrpc/handlers/session_queries.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/jsonrpc/methods.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/jsonrpc/models.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/jsonrpc/params.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/metadata_access.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/opencode_upstream_client.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/output_modes.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/parsing.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/parts/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/parts/mapping.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/parts/text.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/profile/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/protocol_versions.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/py.typed +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/runtime_state.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/sandbox_policy.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/server/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/server/context_helpers.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/server/lifespan.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/server/migrations.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/server/openapi.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/server/request_parsing.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/server/rest_tasks.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/server/state_store.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/server/task_store.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/server/task_store_sdk_compat.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/task_states.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/trace_context.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a/upstream_taxonomy.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a.egg-info/dependency_links.txt +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a.egg-info/entry_points.txt +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a.egg-info/requires.txt +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/src/opencode_a2a.egg-info/top_level.txt +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/client/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/client/test_agent_card.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/client/test_client_config.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/client/test_client_facade.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/client/test_error_mapping.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/client/test_payload_text.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/client/test_polling.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/client/test_request_context.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/config/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/conftest.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/contracts/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/contracts/test_extension_contract_consistency.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/execution/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/execution/test_agent_errors.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/execution/test_agent_helpers.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/execution/test_cancellation.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/execution/test_directory_validation.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/execution/test_metrics.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/execution/test_multipart_input.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/execution/test_session_lock_lifecycle.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/execution/test_session_ownership.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/execution/test_streaming_output_contract_blocks.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/execution/test_streaming_output_contract_core.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/execution/test_streaming_output_contract_interrupts.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/execution/test_streaming_output_contract_logging.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/jsonrpc/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/jsonrpc/test_application_dispatch.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/jsonrpc/test_dispatch_registry.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/jsonrpc/test_error_responses.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/jsonrpc/test_jsonrpc_methods.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/jsonrpc/test_jsonrpc_params.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/jsonrpc/test_jsonrpc_unsupported_method.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/jsonrpc/test_opencode_session_extension_commands.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/jsonrpc/test_opencode_session_extension_interrupts.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/jsonrpc/test_opencode_session_extension_lifecycle.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/jsonrpc/test_opencode_session_extension_prompt_async.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/package/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/package/test_logging.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/package/test_typing_contract.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/package/test_version.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/parts/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/parts/test_parts_text.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/profile/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/scripts/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/scripts/test_script_health_contract.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/server/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/server/test_auth.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/server/test_call_context_builder.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/server/test_cancel_contract.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/server/test_cli.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/server/test_context_helpers.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/server/test_database_app_persistence.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/server/test_output_negotiation.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/server/test_state_store.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/server/test_task_store_factory.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/server/test_transport_contract.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/support/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/support/async_iterators.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/support/fake_client_errors.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/support/helpers.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/support/interrupt_clients.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/support/jsonrpc_error_assertions.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/support/session_extensions.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/support/settings.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/support/streaming_output.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/test_a2a_utils.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/test_metadata_access.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/test_parsing.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/test_trace_context.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/upstream/__init__.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/tests/upstream/test_opencode_upstream_client_params.py +0 -0
- {opencode_a2a-1.2.0 → opencode_a2a-1.3.0}/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"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opencode-a2a
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
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.
|
|
@@ -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
|
+
)
|
|
@@ -174,6 +174,8 @@ class Settings(BaseSettings):
|
|
|
174
174
|
|
|
175
175
|
# A2A settings
|
|
176
176
|
a2a_public_url: str = Field(default="http://127.0.0.1:8000", alias="A2A_PUBLIC_URL")
|
|
177
|
+
a2a_allowed_origins: DeclaredStringList = Field(default=(), alias="A2A_ALLOWED_ORIGINS")
|
|
178
|
+
a2a_allowed_hosts: DeclaredStringList = Field(default=(), alias="A2A_ALLOWED_HOSTS")
|
|
177
179
|
a2a_project: str | None = Field(default=None, alias="A2A_PROJECT")
|
|
178
180
|
a2a_title: str = Field(default="OpenCode A2A", alias="A2A_TITLE")
|
|
179
181
|
a2a_description: str = Field(default="OpenCode A2A runtime", alias="A2A_DESCRIPTION")
|
|
@@ -191,6 +193,32 @@ class Settings(BaseSettings):
|
|
|
191
193
|
ge=0,
|
|
192
194
|
alias="A2A_MAX_REQUEST_BODY_BYTES",
|
|
193
195
|
)
|
|
196
|
+
a2a_rate_limit_enabled: bool = Field(default=True, alias="A2A_RATE_LIMIT_ENABLED")
|
|
197
|
+
a2a_rate_limit_window_seconds: float = Field(
|
|
198
|
+
default=60.0,
|
|
199
|
+
gt=0.0,
|
|
200
|
+
alias="A2A_RATE_LIMIT_WINDOW_SECONDS",
|
|
201
|
+
)
|
|
202
|
+
a2a_rate_limit_max_requests: int = Field(
|
|
203
|
+
default=120,
|
|
204
|
+
gt=0,
|
|
205
|
+
alias="A2A_RATE_LIMIT_MAX_REQUESTS",
|
|
206
|
+
)
|
|
207
|
+
a2a_stream_max_bytes: int = Field(
|
|
208
|
+
default=64 * 1024 * 1024,
|
|
209
|
+
ge=0,
|
|
210
|
+
alias="A2A_STREAM_MAX_BYTES",
|
|
211
|
+
)
|
|
212
|
+
a2a_stream_max_duration_seconds: float = Field(
|
|
213
|
+
default=3600.0,
|
|
214
|
+
ge=0.0,
|
|
215
|
+
alias="A2A_STREAM_MAX_DURATION_SECONDS",
|
|
216
|
+
)
|
|
217
|
+
a2a_stream_idle_timeout_seconds: float = Field(
|
|
218
|
+
default=120.0,
|
|
219
|
+
ge=0.0,
|
|
220
|
+
alias="A2A_STREAM_IDLE_TIMEOUT_SECONDS",
|
|
221
|
+
)
|
|
194
222
|
a2a_documentation_url: str | None = Field(default=None, alias="A2A_DOCUMENTATION_URL")
|
|
195
223
|
a2a_allow_directory_override: bool = Field(default=True, alias="A2A_ALLOW_DIRECTORY_OVERRIDE")
|
|
196
224
|
a2a_enable_session_shell: bool = Field(default=False, alias="A2A_ENABLE_SESSION_SHELL")
|
|
@@ -198,6 +226,10 @@ class Settings(BaseSettings):
|
|
|
198
226
|
default=False,
|
|
199
227
|
alias="A2A_ENABLE_WORKSPACE_MUTATIONS",
|
|
200
228
|
)
|
|
229
|
+
a2a_expose_workspace_root_in_card: bool = Field(
|
|
230
|
+
default=False,
|
|
231
|
+
alias="A2A_EXPOSE_WORKSPACE_ROOT_IN_CARD",
|
|
232
|
+
)
|
|
201
233
|
a2a_sandbox_mode: SandboxMode = Field(default="unknown", alias="A2A_SANDBOX_MODE")
|
|
202
234
|
a2a_sandbox_filesystem_scope: SandboxFilesystemScope = Field(
|
|
203
235
|
default="unknown",
|
|
@@ -281,6 +313,14 @@ class Settings(BaseSettings):
|
|
|
281
313
|
default=("JSONRPC", "HTTP+JSON"),
|
|
282
314
|
alias="A2A_CLIENT_SUPPORTED_TRANSPORTS",
|
|
283
315
|
)
|
|
316
|
+
a2a_client_allowed_hosts: DeclaredStringList = Field(
|
|
317
|
+
default=(),
|
|
318
|
+
alias="A2A_CLIENT_ALLOWED_HOSTS",
|
|
319
|
+
)
|
|
320
|
+
a2a_client_allow_private_hosts: bool = Field(
|
|
321
|
+
default=False,
|
|
322
|
+
alias="A2A_CLIENT_ALLOW_PRIVATE_HOSTS",
|
|
323
|
+
)
|
|
284
324
|
# Task store settings
|
|
285
325
|
a2a_task_store_backend: TaskStoreBackend = Field(
|
|
286
326
|
default="database",
|