Skip to content
Draft
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ This repository improves the service boundary around OpenCode, but it does not t
- Provider auth and default model configuration remain on the OpenCode side; deployment-time precedence details and HOME/XDG state impact are documented in [docs/guide.md](docs/guide.md#troubleshooting-provider-auth-state).
- Use `A2A_CLIENT_BEARER_TOKEN` for server-side outbound peer calls initiated by `a2a_call`.
- Deployment supervision is intentionally BYO. Use `systemd`, Docker, Kubernetes, or another supervisor if you need long-running operation.
- The supported persistence profile runs one `opencode-a2a` application process against its own local SQLite database. Do not run multiple Uvicorn workers or replicas against the same database file.
- PostgreSQL and other SQLAlchemy dialects are not part of the supported deployment matrix.
- For mutually untrusted tenants, run separate instance pairs with isolated users, containers, workspaces, credentials, and ports.

Read before deployment:
Expand Down
4 changes: 2 additions & 2 deletions docs/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ External TCK runs and local conformance experiments are investigation inputs. Th

## Compatibility-Sensitive Surface

This repository still ships as an alpha project. Within that alpha line, these declared surfaces should not drift silently:
This repository ships as a beta project. Its documented public contracts are intended to remain stable, while implementation details and explicitly provider-private surfaces may continue to evolve. These declared surfaces should not drift silently:

- core A2A send / stream / task methods
- v1-only request/response payloads and enum values
Expand Down Expand Up @@ -88,7 +88,7 @@ Task-store behavior that should remain stable for clients:
- 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 default SQLite-first profile is intended for local or controlled single-instance deployments. Wider SQLAlchemy dialect compatibility should be treated as implementation latitude rather than a strong public promise unless explicitly documented later.
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.

## Extension Stability

Expand Down
12 changes: 7 additions & 5 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ Key variables to understand protocol behavior:
- `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).
- `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).
- `A2A_CANCEL_ABORT_TIMEOUT_SECONDS`: best-effort timeout for upstream `session.abort` in cancel flow.
- `OPENCODE_TIMEOUT` / `OPENCODE_TIMEOUT_STREAM`: upstream request timeout and optional stream timeout override.
- `OPENCODE_MAX_CONCURRENT_REQUESTS`: optional fast-fail concurrency limit for unary/control upstream calls. `0` disables the limit.
- `OPENCODE_MAX_CONCURRENT_STREAMS`: optional fast-fail concurrency limit for long-lived upstream `/event` streams. `0` disables the limit.
- `OPENCODE_TIMEOUT` / `OPENCODE_TIMEOUT_STREAM`: upstream request timeout and stream timeout. Defaults: `120` and `900` seconds.
- `OPENCODE_MAX_CONCURRENT_REQUESTS`: fast-fail concurrency limit for unary/control upstream calls. Default: `32`; `0` disables the limit explicitly.
- `OPENCODE_MAX_CONCURRENT_STREAMS`: fast-fail concurrency limit for long-lived upstream `/event` streams. Default: `8`; `0` disables the limit explicitly.
- `A2A_CLIENT_TIMEOUT_SECONDS`: outbound client timeout. Default: `30` seconds.
- `A2A_CLIENT_CARD_FETCH_TIMEOUT_SECONDS`: outbound Agent Card fetch timeout. Default: `5` seconds.
- `A2A_CLIENT_USE_CLIENT_PREFERENCE`: whether the outbound client prefers its own transport choices.
Expand Down Expand Up @@ -174,11 +174,13 @@ With the default `database` backend, the unified lightweight persistence layer p
- pending preferred-session claims
- interrupt request bindings and tombstones

This project is SQLite-first for local single-instance deployments. 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 supported persistence profile is one `opencode-a2a` application process using its own local SQLite database. Run Uvicorn with one worker and do not share the SQLite file between processes, containers, or replicas. Horizontal application scaling and PostgreSQL deployments are outside the supported deployment matrix. SQLAlchemy remains an internal implementation detail rather than a promise of dialect portability.

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 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. Any wider SQLAlchemy dialect compatibility should be treated as incidental implementation latitude rather than a documented deployment target.
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.

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
9 changes: 4 additions & 5 deletions docs/maintainer-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,11 @@ flowchart TD

subgraph Upstream["OpenCode Upstream Layer"]
UpstreamClient["opencode_upstream_client.py"]
Invocation["invocation.py"]
end

subgraph Extensions["Extension / Contract Layer"]
Jsonrpc["jsonrpc/"]
Contracts["contracts/extensions.py"]
Contracts["contracts/extensions/"]
Profile["profile/runtime.py"]
end

Expand Down Expand Up @@ -61,7 +60,7 @@ flowchart TD

1. `jsonrpc/application.py` owns the adapter-specific JSON-RPC application boundary.
2. `jsonrpc/dispatch.py` and handler modules under `jsonrpc/handlers/` route provider-private methods.
3. `contracts/extensions.py` remains the SSOT for extension metadata exposed through Agent Card and OpenAPI.
3. `contracts/extensions/` remains the SSOT for extension metadata exposed through Agent Card and OpenAPI.
4. Tests under `tests/contracts/` and `tests/jsonrpc/` guard contract drift.

### Outbound Peer Call Path
Expand Down Expand Up @@ -89,7 +88,7 @@ flowchart TD

### Extension and Contract Layer

- `contracts/extensions.py`: SSOT for extension metadata, compatibility profile, and wire-contract payloads
- `contracts/extensions/`: SSOT for extension metadata, compatibility profile, and wire-contract payloads
- `jsonrpc/`: provider-private JSON-RPC extension surface
- `profile/runtime.py`: runtime profile that feeds Agent Card, OpenAPI, and compatibility metadata
- `protocol_versions.py`: protocol normalization and negotiation helpers
Expand Down Expand Up @@ -128,5 +127,5 @@ For maintainers new to the codebase, this order usually gives the fastest payoff
2. `docs/architecture.md`
3. `src/opencode_a2a/server/application.py`
4. `src/opencode_a2a/execution/executor.py`
5. `src/opencode_a2a/contracts/extensions.py`
5. `src/opencode_a2a/contracts/extensions/`
6. `docs/guide.md`
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ authors = [
]
keywords = ["a2a", "opencode", "fastapi", "json-rpc", "sse"]
classifiers = [
"Development Status :: 3 - Alpha",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
Expand Down
9 changes: 7 additions & 2 deletions src/opencode_a2a/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import base64
import binascii
import secrets
from dataclasses import dataclass

from starlette.requests import Request
Expand Down Expand Up @@ -83,7 +84,7 @@ def authenticate_static_credential(
for credential in credentials:
if credential.auth_scheme != "bearer" or credential.token is None:
continue
if auth_value == credential.token:
if secrets.compare_digest(auth_value, credential.token):
return AuthenticatedPrincipal(
identity=credential.principal,
auth_scheme="bearer",
Expand All @@ -102,7 +103,11 @@ def authenticate_static_credential(
for credential in credentials:
if credential.auth_scheme != "basic":
continue
if credential.username == username and credential.password == password:
if credential.username is None or credential.password is None:
continue
username_matches = secrets.compare_digest(username, credential.username)
password_matches = secrets.compare_digest(password, credential.password)
if username_matches and password_matches:
return AuthenticatedPrincipal(
identity=credential.principal,
auth_scheme="basic",
Expand Down
12 changes: 9 additions & 3 deletions src/opencode_a2a/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,14 @@ class Settings(BaseSettings):
opencode_system: str | None = Field(default=None, alias="OPENCODE_SYSTEM")
opencode_variant: str | None = Field(default=None, alias="OPENCODE_VARIANT")
opencode_timeout: float = Field(default=120.0, alias="OPENCODE_TIMEOUT")
opencode_timeout_stream: float | None = Field(default=None, alias="OPENCODE_TIMEOUT_STREAM")
opencode_timeout_stream: float | None = Field(default=900.0, alias="OPENCODE_TIMEOUT_STREAM")
opencode_max_concurrent_requests: int = Field(
default=0,
default=32,
ge=0,
alias="OPENCODE_MAX_CONCURRENT_REQUESTS",
)
opencode_max_concurrent_streams: int = Field(
default=0,
default=8,
ge=0,
alias="OPENCODE_MAX_CONCURRENT_STREAMS",
)
Expand Down Expand Up @@ -292,6 +292,12 @@ def _validate_sandbox_policy(self) -> Settings:
raise ValueError(
"A2A_TASK_STORE_DATABASE_URL is required when A2A_TASK_STORE_BACKEND=database"
)
if (
self.a2a_task_store_backend == "database"
and self.a2a_task_store_database_url
and not self.a2a_task_store_database_url.startswith("sqlite+aiosqlite:")
):
raise ValueError("A2A_TASK_STORE_DATABASE_URL must use the sqlite+aiosqlite scheme")
if self.a2a_static_auth_credentials:
if not any(credential.enabled for credential in self.a2a_static_auth_credentials):
raise ValueError(
Expand Down
Loading
Loading