Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@ updates:
directory: "/"
schedule:
interval: "weekly"
ignore:
# a2a-sdk still constrains protobuf to <7, so major protobuf bumps are unsatisfiable.
- dependency-name: "protobuf"
update-types:
- "version-update:semver-major"
open-pull-requests-limit: 1
commit-message:
prefix: "deps"
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,35 @@ jobs:

- name: Run runtime regression tests
run: uv run pytest --no-cov

protobuf-compatibility:
name: Validate Protobuf 6 Compatibility
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.11"

- name: Set up uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: false

- name: Sync locked dependencies
run: uv sync --all-extras --frozen

- name: Install supported protobuf 6 release
run: |
uv pip install 'protobuf==6.33.6'
uv pip check

- name: Run protobuf 6 regression tests
env:
# Keep nested uv commands (including the conformance SUT) on protobuf 6.
UV_NO_SYNC: "1"
run: uv run pytest --no-cov
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ repos:
args: ["--severity=error"]
files: ^scripts/.*\.sh$
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.14
rev: v0.16.8
hooks:
- id: ruff
args: ["--fix"]
Expand Down
15 changes: 15 additions & 0 deletions docs/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ This is the single canonical place where the supported upstream OpenCode version

The repository currently pins one concrete SDK release in `pyproject.toml` within that v1 line. Upgrade the SDK deliberately rather than relying on floating dependency resolution. The SDK-owned core JSON-RPC method set follows that pinned release and is locked by repository tests so SDK upgrades trigger an explicit compatibility review.

### SDK and Dependency Review

The [SDK 1.1.2 to 1.1.5 changes](https://github.com/a2aproject/a2a-python/compare/v1.1.2...v1.1.5) include cross-event-loop locking fixes, task-list ordering fixes, and protobuf 7 support. The adapter's reviewed integration boundaries are:

| Boundary | Decision and regression evidence |
| --- | --- |
| `DatabaseTaskStoreCompat` | Retained: SDK `DatabaseTaskStore.save` still performs an unconditional ORM merge. The adapter needs an atomic first-terminal-state-wins guard; task-store tests cover shape drift, ORM conversion parity, and independent store instances. |
| Legacy stored task conversion | Retained for SDK-owned persisted rows. This does not restore legacy A2A 0.3 wire support. |
| Request-handler hooks and queue lifecycle | Retained for output negotiation, cancel idempotency, and persistence after client disconnect. Handler regressions exercise these behaviors against the pinned SDK; private hooks remain an SDK upgrade review boundary. |
| Outbound header forwarding | Uses the SDK `service_parameters` channel. Client transport tests check the final HTTP request, including authentication and tracing headers. |
| Protobuf | Supports `>=6.33.5,<8.0`. The default CI uses locked 7.36.2; a separate Python 3.11 job installs 6.33.6 and disables uv synchronization throughout the test process tree, so nested protocol probes also exercise protobuf 6. SDK 1.1.5 removes the former protobuf 7 blocker, so the obsolete Dependabot ignore is removed. |
| Security floors | `click>=8.3.3` and `pyasn1>=0.6.4` remain necessary because upstream metadata permits older versions. They are runtime requirements so wheel and sdist installs enforce them without uv-specific constraints. Installed-metadata tests protect these floors and the SQLAlchemy asyncio extra. |

The missing-import typing overrides for `google.protobuf.*` and `jsonrpc.*` remain limited to those third-party modules. Runtime support is validated by the SDK and transport regressions, not inferred from the typing overrides. Ruff's pre-commit revision is aligned with the lockfile version when upgrading the lint toolchain.

## Contract Honesty

Machine-readable discovery surfaces must reflect actual runtime behavior:
Expand Down
4 changes: 4 additions & 0 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,10 @@ The supported persistence profile is one `opencode-a2a` application process usin

The runtime configures local durability-oriented SQLite connection settings (`WAL`, `busy_timeout`, `synchronous=NORMAL`) and creates missing parent directories for file-backed database paths.

The package declares `sqlalchemy[asyncio]` so standalone installs such as `uv tool install opencode-a2a` include `greenlet`, which is required by the async database engine. SQLAlchemy 2.1 no longer installs `greenlet` by default; declaring the extra in package metadata keeps wheel installs independent of the repository's `uv.lock`.

Published package metadata also enforces the security floors `click>=8.3.3` and `pyasn1>=0.6.4`. These apply to standalone wheel/sdist installs as well as repository environments; no separate uv constraint configuration is needed.

### SQLite Persistence Hardening

File-backed SQLite databases are hardened at startup and on every new connection on POSIX systems:
Expand Down
18 changes: 8 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,19 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP",
]
dependencies = [
"a2a-sdk==1.1.2",
"a2a-sdk==1.1.5",
"aiosqlite>=0.20,<1.0",
# Carry transitive security floors into wheel metadata as well as uv installs.
# click: PYSEC-2026-2132 (via uvicorn).
"click>=8.3.3",
"fastapi>=0.139,<1.0",
"httpx>=0.27,<1.0",
"pydantic>=2.6,<3.0",
"pydantic-settings>=2.14.2,<3.0",
"protobuf>=6.33.5,<7.0",
"sqlalchemy>=2.0,<3.0",
"protobuf>=6.33.5,<8.0",
# pyasn1: PYSEC-2026-3455/3456/3457 (via google-auth <- a2a-sdk).
"pyasn1>=0.6.4",
"sqlalchemy[asyncio]>=2.0,<3.0",
"sse-starlette>=2.1,<4.0",
"uvicorn>=0.29,<1.0",
]
Expand Down Expand Up @@ -69,13 +74,6 @@ quote-style = "double"

[tool.uv]
package = true
# Keep transitive dependency floors above versions with known vulnerabilities:
# - click < 8.3.3: PYSEC-2026-2132 (pulled in via uvicorn)
# - pyasn1 < 0.6.4: PYSEC-2026-3455/3456/3457 (pulled in via google-auth <- a2a-sdk)
constraint-dependencies = [
"click>=8.3.3",
"pyasn1>=0.6.4",
]

[tool.setuptools_scm]
fallback_version = "0+unknown"
Expand Down
43 changes: 43 additions & 0 deletions tests/package/test_dependency_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from importlib.metadata import requires

import pytest
from packaging.requirements import Requirement


def _runtime_requirement(name: str) -> Requirement:
requirements = [
requirement
for raw in requires("opencode-a2a") or []
if (requirement := Requirement(raw)).name == name
]
assert len(requirements) == 1, f"Expected one requirement for {name}"
assert requirements[0].marker is None, f"{name} must be required on every installation"
return requirements[0]


@pytest.mark.parametrize(
("name", "unsafe_version", "safe_version"),
[("click", "8.3.2", "8.3.3"), ("pyasn1", "0.6.3", "0.6.4")],
)
def test_installed_metadata_enforces_security_floors(
name: str, unsafe_version: str, safe_version: str
) -> None:
requirement = _runtime_requirement(name)
assert unsafe_version not in requirement.specifier
assert safe_version in requirement.specifier


def test_installed_metadata_requests_sqlalchemy_asyncio() -> None:
requirement = _runtime_requirement("sqlalchemy")
assert "asyncio" in requirement.extras
assert "2.0.54" in requirement.specifier
assert "2.1.1" in requirement.specifier
assert "3.0.0" not in requirement.specifier


def test_installed_metadata_bounds_supported_protobuf_versions() -> None:
requirement = _runtime_requirement("protobuf")
assert "6.33.4" not in requirement.specifier
assert "6.33.5" in requirement.specifier
assert "7.36.2" in requirement.specifier
assert "8.0.0" not in requirement.specifier
4 changes: 0 additions & 4 deletions tests/package/test_typing_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,5 @@ def test_package_declares_py_typed_marker() -> None:
assert 'opencode_a2a = ["py.typed"]' in PYPROJECT_TEXT


def test_package_declares_runtime_protobuf_dependency() -> None:
assert '"protobuf>=6.33.5,<7.0"' in PYPROJECT_TEXT


def test_build_backend_pins_setuptools_scm_below_next_major_version() -> None:
assert 'requires = ["setuptools>=80", "setuptools-scm[toml]>=8,<11"]' in PYPROJECT_TEXT
Loading
Loading