Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
8d7dfec
fix(ci): reject pre-commit rewrites in the lint gate (#537)
liujuanjuan1984 Oct 2, 2026
9b5a143
test(ci): pin the shared repo-state fingerprint contract (#537)
liujuanjuan1984 Oct 2, 2026
a3a5636
chore(lint): enable Ruff security rules and converge src asserts (#539)
liujuanjuan1984 Oct 2, 2026
ed35453
deps: bump the uv-all-updates group across 1 directory with 5 updates
dependabot[bot] Oct 15, 2026
acf641a
fix(deps): address dependency audit findings (#542)
liujuanjuan1984 Oct 24, 2026
d9399ee
fix: upgrade a2a-sdk to 1.2.3 and adapt transport contracts (#542)
liujuanjuan1984 Oct 24, 2026
1071b32
test: cover a2a-sdk keyset pagination (#542)
liujuanjuan1984 Oct 24, 2026
ed7f5cc
test(ci): verify repo fingerprint detects worktree rewrites (#537)
liujuanjuan1984 Oct 24, 2026
5c970d1
refactor(server): reduce private a2a-sdk coupling (#542)
liujuanjuan1984 Oct 24, 2026
b2fc5dc
test: cover a2a-sdk lifecycle and parse error mapping (#542)
liujuanjuan1984 Oct 24, 2026
5ed2608
docs: pin the reviewed a2a-sdk version (#542)
liujuanjuan1984 Oct 24, 2026
1862654
refactor(server): migrate to SDK v2 versioned task lifecycle (#542)
liujuanjuan1984 Oct 24, 2026
6013695
chore: format and type-check v2 lifecycle migration (#542)
liujuanjuan1984 Oct 24, 2026
bac13af
refactor(server): finish SDK v2 lifecycle migration (#542)
liujuanjuan1984 Oct 24, 2026
d7cee59
chore(deps): refresh locked dependency set (#542)
liujuanjuan1984 Oct 24, 2026
0f12b4a
docs: remove redundant SDK version references (#542)
liujuanjuan1984 Oct 24, 2026
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
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ bash ./scripts/doctor.sh

`doctor.sh` is the primary repository validation entrypoint. It currently runs locked-environment sync, dependency compatibility checks, `pre-commit`, `mypy`, `pytest`, the repository coverage gate, package build, and a built-wheel smoke test.

Ruff runs with the `S` (flake8-bandit) security rules enabled for shipped code. `tests/**` is exempt from `S` because test modules intentionally use asserts and fake credentials; do not widen that exemption beyond `tests/**`.

If you change shell scripts, also run `bash -n` on each modified script, for example:

```bash
Expand Down
28 changes: 22 additions & 6 deletions docs/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,30 @@ The SDK-owned core JSON-RPC method set follows the supported SDK line and is loc

### SDK and Adapter 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 and task-list ordering fixes. The adapter's reviewed integration boundaries are:
The adapter's reviewed SDK 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 SDK; private hooks remain an SDK upgrade review boundary. |
| REST success media type | Adopted: successful HTTP+JSON responses use `application/a2a+json`, including adapter-owned `/message:send` and `/tasks` routes. REST error envelopes remain AIP-193 JSON. Transport and OpenAPI tests lock both runtime headers and documented response media types. |
| JSON-RPC and REST request parsing | Adopted: unknown request fields are ignored for forward compatibility, matching SDK parsing behavior. Shared middleware rejects cross-transport envelopes plus legacy `content`, lowercase role, and `kind`/`type`/`file` part shapes before either transport dispatches. |
| Terminal task follow-up | Adopted: SDK now raises `UnsupportedOperationError` with the named terminal `TaskState`; handler regressions lock this behavior for both memory and database stores. |
| Versioned database task lifecycle | Adopted: database mode uses `VersionedDatabaseTaskStore` with SDK optimistic-concurrency control and `DatabaseTaskEventStream` for persisted event replay. The adapter no longer carries `GuardedTaskStore`, `DatabaseTaskStoreCompat`, or a private SQL terminal-write policy. |
| Legacy stored task conversion | Retained only at the SDK migration boundary for SDK-owned persisted rows. This does not restore legacy A2A 0.3 wire support. |
| Request-handler lifecycle | Adopted: `OpencodeRequestHandler` extends SDK `DefaultRequestHandlerV2` and uses the public `RequestContextBuilder` extension point for output-mode persistence. Active-task execution, queueing, cancellation, subscription, direct-Message completion, and shutdown draining stay in the SDK lifecycle. |
| Outbound header forwarding | Uses the SDK `service_parameters` channel. Client transport tests check the final HTTP request, including authentication and tracing headers. |

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.

### Intentional SDK Private Seams

Core REST routes, task persistence, and the request-handler lifecycle now use SDK public interfaces. The remaining seams are explicit and regression-bounded:

| Seam | Why it remains |
| --- | --- |
| JSON-RPC dispatcher overrides | `opencode.*` extension methods and repository error redaction require explicit method registration and error adaptation. Core method schemas and response helpers remain SDK-owned. |

The repository still wraps public request-handler streams to enforce byte, duration, and idle budgets, but it no longer subclasses `RestDispatcher` or calls `_handle_streaming`. Shape and transport checks run in middleware before dispatch. Every SDK upgrade must rerun shape/migration tests, handler lifecycle tests, transport media-type tests, and conformance probes.

## Contract Honesty

Machine-readable discovery surfaces must reflect actual runtime behavior:
Expand Down Expand Up @@ -74,7 +87,7 @@ Changes to those surfaces should be treated as compatibility-sensitive and shoul
Service-level behavior layered on top of those core methods should also be declared explicitly when interoperability depends on it. Current examples:

- `SubscribeToTask` rejection with `UnsupportedOperationError` for terminal tasks
- first-terminal-state-wins task persistence policy
- SDK versioned task persistence with compare-and-swap conflict detection
- task-scoped `acceptedOutputModes` negotiation persistence across send / stream / get / subscribe
- request-body rejection behavior for oversized transport payloads

Expand All @@ -99,13 +112,16 @@ Task durability is deployment-dependent:

Task-store behavior that should remain stable for clients:

- once a task reaches a terminal state, later conflicting writes are dropped on a first-terminal-state-wins basis
- concurrent task mutations are serialized by the SDK versioned-store compare-and-swap contract; stale writers receive `ConcurrentTaskModificationError`
- the v2 request handler rejects follow-up messages to terminal tasks and does not persist mutations after their terminal event
- task-store I/O failures are surfaced as stable service errors instead of leaking backend-specific exceptions
- accepted output-mode negotiation for a task is persisted with the task so later reads keep the same filtered output contract
- adapter-managed migrations only own adapter state tables; SDK-managed task schema remains SDK-owned

The supported persistence profile is one application process using its own local SQLite database. Multiple Uvicorn workers or application replicas must not share that SQLite file. PostgreSQL and other SQLAlchemy dialects are not supported deployment targets; any apparent dialect compatibility is implementation latitude rather than a public promise.

The SDK task-store migration adds `task_versions` and `task_events`. This runtime adopts those stores even in its supported single-process profile so task writes and event replay follow the SDK lifecycle; the migration is additive and does not change existing `tasks` rows.

## Extension Stability

- Shared metadata and extension contracts should stay synchronized across Agent Card, OpenAPI, and runtime behavior.
Expand Down
5 changes: 3 additions & 2 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This guide covers configuration, authentication, API behavior, streaming re-subs
- JSON-RPC (`POST /`)
- Agent Card exposes both HTTP+JSON and JSON-RPC through `supportedInterfaces`.
- HTTP+JSON REST endpoints are served at the root path (e.g. `/message:send`, `/tasks`). A2A 1.0 negotiates protocol version via the `A2A-Version` header, not a URL prefix, so the URL path carries no version segment.
- Successful HTTP+JSON responses use `application/a2a+json`. Request payloads and examples remain JSON, while REST errors keep the AIP-193 `application/json` envelope.
- Breaking change for earlier releases: versions through v1.1.2 served the HTTP+JSON surface under a `/v1` prefix. Deployments upgrading from those releases must repoint clients, reverse proxies, and any `/v1`-prefixed routes to the root paths.
- The public Agent Card is intentionally slimmed to the minimum discovery surface; per-extension disclosure policy is defined in [`extension-specifications.md`](./extension-specifications.md).
- Detailed provider-private contracts are served through the authenticated extended card endpoint `/extendedAgentCard`.
Expand Down Expand Up @@ -235,7 +236,7 @@ Deployment requirements:

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.

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.
Database-backed task persistence uses the SDK `VersionedDatabaseTaskStore`: writes are ordered by version, stale writers fail with `ConcurrentTaskModificationError`, and events are recorded in the SDK `task_events` table for replay by `DatabaseTaskEventStream`. The supported deployment remains one application process with its own SQLite database.

At startup, the runtime logs a concise persistence summary covering the active backend, the redacted database URL when applicable, the shared persistence scope, and whether the SQLite local durability profile is active.

Expand Down Expand Up @@ -1340,7 +1341,7 @@ If an SSE connection drops, use `GET /tasks/{task_id}:subscribe` to re-subscribe
- Upstream interruption is best-effort: if upstream returns 404, network errors, or other HTTP errors, A2A cancellation still completes with `TaskState.TASK_STATE_CANCELED`.
- Idempotency contract: repeated `CancelTask` on an already `canceled` task returns the current terminal task state without error.
- Terminal subscribe contract: calling `SubscribeToTask` or `GET /tasks/{task_id}:subscribe` on a terminal task returns `UnsupportedOperationError`, as required by A2A 1.0.
- Terminal persistence contract: once a terminal task snapshot is persisted, this service treats it as immutable. Producers must emit final text and artifact updates before the terminal event, and any final usage or stream metadata must be attached to that terminal event itself. Late terminal-state mutations are rejected by the task-store write policy.
- Terminal lifecycle contract: the v2 handler treats a persisted terminal task as non-resumable and rejects follow-up messages and subscriptions; cancellation is rejected except for the documented idempotent response for an already `canceled` task. Producers must emit final text and artifact updates before the terminal event, and any final usage or stream metadata must be attached to that terminal event itself.
- The cancellation idempotency enhancement is also declared under machine-readable `service_behaviors`. Terminal-task subscription rejection remains a core A2A 1.0 rule and is therefore not republished as a custom enhancement.
- The service records process-local metrics for authenticated Prometheus scraping at `GET /metrics`; `A2A_METRICS_ENABLED=false` disables the endpoint. At `A2A_LOG_LEVEL=DEBUG`, the same updates are emitted as lightweight metric log records (`logger=opencode_a2a.execution.executor`):
- `a2a_stream_requests_total`
Expand Down
3 changes: 2 additions & 1 deletion docs/maintainer-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,9 @@ flowchart TD

- `server/application.py`: app assembly, route wiring, request handler customization, and top-level lifecycle integration
- `server/middleware.py`: auth, protocol negotiation, payload/body guards, logging, and response decoration
- `server/request_handler.py`: SDK v2 request lifecycle plus adapter output-mode and extension policy
- `server/task_store.py`: SDK task-store construction and output-mode persistence adapter
- `server/agent_card.py` / `server/openapi.py`: machine-readable contract publication
- `server/rest_tasks.py`: SDK-owned REST task routes plus adapter-specific list behavior

### Execution Layer

Expand Down
18 changes: 14 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP",
]
dependencies = [
"a2a-sdk==1.1.5",
"a2a-sdk==1.2.3",
"aiosqlite>=0.20,<1.0",
# certifi: PYSEC-2026-4320 (via httpx and requests <- google-api-core <- a2a-sdk).
"certifi>=2026.10.19",
# Carry transitive security floors into wheel metadata as well as uv installs.
# click: PYSEC-2026-2132 (via uvicorn).
"click>=8.3.3",
Expand All @@ -46,17 +48,20 @@ dependencies = [

[project.optional-dependencies]
migrations = [
"a2a-sdk[db-cli]==1.1.5",
"a2a-sdk[db-cli]==1.2.3",
]
dev = [
# filelock / virtualenv: development-only security floors (via pre-commit).
"filelock>=4.2.1",
"mypy>=1.19.1,<3.0",
"pip-audit>=2.9,<3.0",
"pre-commit>=4.5.1,<5.0",
"pygments>=2.20.0,<3.0",
"pytest>=8.0,<10.0",
"pytest-asyncio>=0.23,<2.0",
"pytest-cov>=7.0.0,<8.0.0",
"ruff>=0.5,<0.17",
"ruff>=0.5,<0.18",
"virtualenv>=21.14.9",
]

[project.scripts]
Expand All @@ -72,7 +77,12 @@ line-length = 100
src = ["src"]

[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]
select = ["E", "F", "I", "B", "UP", "S"]

[tool.ruff.lint.per-file-ignores]
# Bandit-style security rules target shipped code. Test modules intentionally use
# asserts and hardcoded fake credentials, mirroring codex-a2a's configuration.
"tests/**" = ["S"]

[tool.ruff.format]
quote-style = "double"
Expand Down
3 changes: 2 additions & 1 deletion scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ Executable scripts live in this directory. This file is the entry index for the
- [`dependency_health.sh`](./dependency_health.sh): development dependency review entrypoint (`sync`/`pip check` + outdated + dev audit), while blocking CI/publish audits focus on runtime dependencies
- [`check_coverage.py`](./check_coverage.py): enforces the overall coverage floor and per-file minimums for critical modules
- [`find_thin_wrappers.py`](./find_thin_wrappers.py): static analysis helper that reports local functions with low caller counts and flags likely thin forwarding wrappers for manual abstraction review
- [`lint.sh`](./lint.sh): lint helper
- [`lint.sh`](./lint.sh): lint entrypoint used by CI; runs the repository `pre-commit` pipeline and **rejects hook-generated rewrites** instead of retrying them, so an unformatted or un-fixed commit cannot pass the gate
- [`smoke_test_built_cli.sh`](./smoke_test_built_cli.sh): built-artifact smoke test for the released CLI runtime; defaults to the only local wheel, supports explicit wheel/sdist paths, and rejects ambiguous local artifact selection

## Notes

- `doctor.sh` and `dependency_health.sh` intentionally remain separate entrypoints and share common prerequisites through [`health_common.sh`](./health_common.sh).
- `doctor.sh` covers the default local validation baseline, while `dependency_health.sh` remains focused on standalone dependency review and audit flow.
- `doctor.sh` stops early when `pre-commit` rewrites files so you can review the changes and rerun `doctor.sh` from the updated worktree.
- `lint.sh` shares the same working-tree fingerprint check through [`health_common.sh`](./health_common.sh): auto-fixing hooks (`trailing-whitespace`, `end-of-file-fixer`, `ruff --fix`, `ruff format`) make the gate fail instead of being masked by a retry.
- [`.github/dependabot.yml`](../.github/dependabot.yml) prefers a single weekly grouped Dependabot PR for `uv`, while `dependency_health.sh` remains the explicit review/audit entrypoint.
- Repository-owned compatibility probes remain separate from the default regression path. See [`../docs/conformance.md`](../docs/conformance.md).
10 changes: 9 additions & 1 deletion scripts/conformance_probe.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import argparse
import json
import os
import shutil
import subprocess
from collections.abc import Callable
from pathlib import Path
Expand Down Expand Up @@ -177,11 +178,18 @@ def main() -> int:
args.output_dir.mkdir(parents=True, exist_ok=True)
results, card = run_probes(args.base_url.rstrip("/"), token)
failures = [result for result in results if result["status"] == "failed"]
git_executable = shutil.which("git")
if git_executable is None:
parser.error("git executable is required to record the repository commit")
report = {
"schema_version": 1,
"scope": "repository-owned-a2a-1.0-compatibility-probes",
"sut_url": args.base_url,
"repo_commit": subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip(),
# The git executable is resolved via shutil.which and invoked with a
# fixed argument list, so this is not untrusted input execution.
"repo_commit": subprocess.check_output( # noqa: S603
[git_executable, "rev-parse", "HEAD"], text=True
).strip(),
"summary": {
"passed": len(results) - len(failures),
"failed": len(failures),
Expand Down
16 changes: 2 additions & 14 deletions scripts/doctor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,21 @@ set -euo pipefail
# shellcheck source=./health_common.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/health_common.sh"

doctor_repo_state_fingerprint() {
{
git diff --no-ext-diff --binary --cached -- .
git diff --no-ext-diff --binary -- .
while IFS= read -r -d '' path; do
printf 'untracked %s\n' "$path"
cat "$path"
printf '\n'
done < <(git ls-files --others --exclude-standard -z)
} | git hash-object --stdin
}

run_doctor_fix_phase() {
local pre_commit_exit_code=0
local repo_state_before
local repo_state_after

echo "[doctor] run fix phase"
repo_state_before="$(doctor_repo_state_fingerprint)"
repo_state_before="$(repo_state_fingerprint)"

if uv run pre-commit run --all-files; then
pre_commit_exit_code=0
else
pre_commit_exit_code=$?
fi

repo_state_after="$(doctor_repo_state_fingerprint)"
repo_state_after="$(repo_state_fingerprint)"
if [[ "$repo_state_before" != "$repo_state_after" ]]; then
echo "[doctor] pre-commit modified files; review the changes and rerun doctor" >&2
exit 1
Expand Down
14 changes: 14 additions & 0 deletions scripts/health_common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,17 @@ run_shared_repo_health_prerequisites() {
echo "[${label}] verify dependency compatibility"
uv pip check
}

# Fingerprint of tracked and untracked working-tree state. Used to detect
# whether a tool rewrote repository files instead of only reporting on them.
repo_state_fingerprint() {
{
git diff --no-ext-diff --binary --cached -- .
git diff --no-ext-diff --binary -- .
while IFS= read -r -d '' path; do
printf 'untracked %s\n' "$path"
cat "$path"
printf '\n'
done < <(git ls-files --others --exclude-standard -z)
} | git hash-object --stdin
}
26 changes: 25 additions & 1 deletion scripts/lint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,42 @@
set -euo pipefail

# Run the repo canonical lint pipeline to keep local and CI checks identical.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=./health_common.sh
source "${SCRIPT_DIR}/health_common.sh"

cd "$ROOT_DIR"

max_retries="${PRE_COMMIT_MAX_RETRIES:-3}"
retry_delay_seconds="${PRE_COMMIT_RETRY_DELAY_SECONDS:-5}"

attempt=1
while true; do
state_before="$(repo_state_fingerprint)"

pre_commit_status=0
if uv run pre-commit run --all-files; then
pre_commit_status=0
else
pre_commit_status=$?
fi

# Auto-fixing hooks (trailing-whitespace, end-of-file-fixer, ruff --fix,
# ruff-format) exit non-zero when they rewrite files, so a retry would turn a
# real failure into a green run. Report it as a failure instead of retrying.
if [[ "$(repo_state_fingerprint)" != "${state_before}" ]]; then
echo "ERROR: pre-commit rewrote repository files; the lint gate rejects hook-generated edits." >&2
echo "Review and commit the changes, then rerun." >&2
exit 1
fi

if (( pre_commit_status == 0 )); then
break
fi

if (( attempt >= max_retries )); then
echo "ERROR: pre-commit failed after ${attempt} attempts." >&2
exit 1
exit "${pre_commit_status}"
fi

echo "WARN: pre-commit failed on attempt ${attempt}/${max_retries}, retrying in ${retry_delay_seconds}s..." >&2
Expand Down
Loading
Loading