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
124 changes: 89 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,88 @@
# opencode-a2a-serve

> Turn OpenCode into a stateful A2A service with a clear security boundary and production-friendly deployment workflow.
> Turn OpenCode into a stateful A2A service with a clear runtime boundary and production-friendly deployment workflow.

`opencode-a2a-serve` exposes OpenCode through standard A2A interfaces and adds
the operational pieces that raw agent runtimes usually do not provide by
default: authentication, session continuity, streaming contracts, interrupt
handling, deployment tooling, and explicit security guidance.

## Why This Project Exists

OpenCode is useful as an interactive runtime, but applications and gateways
need a stable service layer around it. This repository provides that layer by:

- bridging A2A transport contracts to OpenCode session/message/event APIs
- making session and interrupt behavior explicit and auditable
- packaging deployment scripts and operational guidance for long-running use

## What It Already Provides

- A2A HTTP+JSON endpoints (`/v1/message:send`, `/v1/message:stream`,
`GET /v1/tasks/{task_id}:subscribe`)
- A2A JSON-RPC endpoint (`POST /`) for standard methods and OpenCode-oriented
extensions
- SSE streaming with normalized `text`, `reasoning`, and `tool_call` blocks
- session continuation via `metadata.shared.session.id`
- request-scoped model selection via `metadata.shared.model`
- OpenCode session query/control extensions and provider/model discovery
- systemd multi-instance deployment and lightweight current-user deployment

## Design Principle

One `OpenCode + opencode-a2a-serve` instance pair is treated as a
single-tenant trust boundary.

- OpenCode may manage multiple projects/directories, but one deployed instance
is not a secure multi-tenant runtime.
- Shared-instance identity/session checks are best-effort coordination, not
hard tenant isolation.
- For mutually untrusted tenants, deploy separate instance pairs with isolated
Linux users or containers, isolated workspace roots, and isolated
credentials.

## Logical Components

```mermaid
flowchart TD
Hub["A2A client / a2a-client-hub / app"] --> Api["opencode-a2a-serve transport"]
Api --> Mapping["Task / session / interrupt mapping"]
Mapping --> Runtime["OpenCode HTTP runtime"]

Api --> Auth["Bearer auth + request logging controls"]
Api --> Deploy["systemd and lightweight deployment scripts"]
Runtime --> Workspace["Shared workspace / environment boundary"]
```

This repository wraps OpenCode in a service layer. It does not change OpenCode
into a hard multi-tenant isolation platform.

## Recommended Client Side

## Vision
If you need a client-side integration layer to consume this service, prefer
[a2a-client-hub](https://github.com/liujuanjuan1984/a2a-client-hub).

Provide a practical adapter layer that lets individuals and small teams expose OpenCode through standard A2A interfaces (REST + JSON-RPC) while keeping operations, auth, and session behavior explicit and auditable.
It is a better place for client concerns such as A2A consumption, upstream
adapter normalization, and application-facing integration, while
`opencode-a2a-serve` stays focused on the server/runtime boundary around
OpenCode.

## Core Value
## Security Model

- Protocol bridge: map A2A message/task semantics to OpenCode session/message/event APIs.
- Stateful interaction: support session continuation and reconnection workflows.
- Operational readiness: include systemd multi-instance deployment scripts and guardrails.
- Security baseline: enforce bearer-token auth and document key risk boundaries.
This project improves the service boundary around OpenCode, but it is not a
hard multi-tenant isolation layer.

## Core Capabilities
- `A2A_BEARER_TOKEN` protects the A2A surface, but it is not a tenant
isolation boundary inside one deployed instance.
- LLM provider keys are consumed by the OpenCode process. Prompt injection or
indirect exfiltration attempts may still expose sensitive values.
- systemd deploy defaults use operator-provisioned root-only secret files
unless `ENABLE_SECRET_PERSISTENCE=true` is explicitly enabled.

- A2A HTTP+JSON endpoints (`/v1/message:send`, `/v1/message:stream`, `GET /v1/tasks/{task_id}:subscribe`).
- A2A JSON-RPC endpoint (`POST /`) for standard methods and OpenCode-oriented extensions.
- Streaming with incremental task artifacts and terminal status events.
- Session continuation via `metadata.shared.session.id`.
- Request-scoped model selection via `metadata.shared.model`.
- OpenCode session query/control (`opencode.sessions.*`) and provider/model discovery (`opencode.providers.*`, `opencode.models.*`) extension methods.
- Shared interrupt callback methods.
Read before deployment:

- [SECURITY.md](SECURITY.md)
- [scripts/deploy_readme.md](scripts/deploy_readme.md)

## Quick Start & Development

Expand All @@ -45,33 +106,26 @@ A2A_BEARER_TOKEN=dev-token uv run opencode-a2a-serve

Default address: `http://127.0.0.1:8000`

Development & validation baseline:
Baseline validation:

```bash
uv run pre-commit run --all-files
uv run mypy src/opencode_a2a_serve
uv run pytest
```

For deployment and operations scripts, see [`scripts/README.md`](scripts/README.md).

## Documentation Map

- Product/protocol behavior:
- [`docs/guide.md`](docs/guide.md)
- Script entry and operations:
- [`scripts/README.md`](scripts/README.md)
- [`scripts/deploy_readme.md`](scripts/deploy_readme.md)
- [`scripts/deploy_light_readme.md`](scripts/deploy_light_readme.md)
- [`scripts/init_system_readme.md`](scripts/init_system_readme.md)
- [`scripts/start_services_readme.md`](scripts/start_services_readme.md)
- [`scripts/uninstall_readme.md`](scripts/uninstall_readme.md)

## Security Boundary

- `A2A_BEARER_TOKEN` is required for startup.
- LLM provider keys are consumed by the OpenCode process. This model is best suited for trusted/internal environments unless stronger credential isolation is introduced.
- Within one service instance, consumers share the same underlying OpenCode workspace/environment (not tenant-isolated by default).
- [docs/guide.md](docs/guide.md)
Product behavior, API contracts, streaming/session/interrupt details.
- [scripts/README.md](scripts/README.md)
Entry points for init, deploy, lightweight deploy, local start, and
uninstall scripts.
- [scripts/deploy_readme.md](scripts/deploy_readme.md)
systemd deployment, runtime secret strategy, and operations guidance.
- [scripts/deploy_light_readme.md](scripts/deploy_light_readme.md)
current-user lightweight deployment without systemd.
- [SECURITY.md](SECURITY.md)
threat model, deployment caveats, and vulnerability disclosure guidance.

## License

Expand Down
54 changes: 54 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Security Policy

## Scope

This repository is an adapter layer that exposes OpenCode through A2A
HTTP+JSON and JSON-RPC interfaces. It adds authentication, task/session
contracts, streaming, interrupt handling, and deployment tooling, but it does
not fully isolate upstream model credentials from OpenCode runtime behavior.

## Security Boundary

- `A2A_BEARER_TOKEN` protects access to the A2A surface, but it is not a
tenant-isolation boundary inside one deployed instance.
- One `OpenCode + opencode-a2a-serve` instance pair is treated as a
single-tenant trust boundary by design.
- Within one instance, consumers share the same underlying OpenCode
workspace/environment by default.
- LLM provider keys are consumed by the `opencode` process. Prompt injection or
indirect exfiltration attempts may still expose sensitive values.
- Payload logging is opt-in. When `A2A_LOG_PAYLOADS=true`, operators should
treat logs as potentially sensitive operational data.
- In systemd deployment mode, secret persistence is opt-in. The deploy scripts
should not write `GH_TOKEN`, `A2A_BEARER_TOKEN`, or provider keys to disk
unless `ENABLE_SECRET_PERSISTENCE=true` is explicitly set.

## Threat Model

This project is currently best suited for trusted or internal environments.
Important limits:

- no per-tenant workspace isolation inside one instance
- no hard guarantee that upstream provider keys are inaccessible to agent logic
- bearer-token auth only by default; stronger identity propagation is still a
follow-up hardening area
- operators remain responsible for host hardening, secret rotation, process
access controls, and reverse-proxy exposure strategy

## Reporting a Vulnerability

Please avoid posting active secrets, bearer tokens, or reproduction payloads
that contain private data in public issues.

Preferred disclosure order:

1. Use GitHub private vulnerability reporting if it is available for this
repository.
2. If private reporting is unavailable, contact the repository maintainer
directly through GitHub before opening a public issue.
3. For low-risk hardening ideas that do not expose private data, a normal
GitHub issue is acceptable.

## Supported Branches

Security fixes are expected to land on the active `main` branch first.
2 changes: 2 additions & 0 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Executable scripts live in this directory. This file is the entry index for scri

- Product/API behavior (transport, protocol contracts, extension semantics):
- [`../docs/guide.md`](../docs/guide.md)
- Security boundary and disclosure guidance:
- [`../SECURITY.md`](../SECURITY.md)
- Script operational details (how to run and operate each script):
- kept in this `scripts/` directory as `*_readme.md`

Expand Down
16 changes: 11 additions & 5 deletions scripts/deploy.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#!/usr/bin/env bash
# Docs: scripts/deploy_readme.md
# Deploy one isolated OpenCode + A2A systemd instance.
# Secret env vars are only required when persisting them during deploy or when
# setup actions need them.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
Expand All @@ -9,8 +11,6 @@ source "${SCRIPT_DIR}/deploy/provider_secret_env_keys.sh"
PROVIDER_SECRET_ENV_LIST="$(join_provider_secret_env_keys " | ")"

PROJECT_NAME=""
GH_TOKEN="${GH_TOKEN:-}"
A2A_BEARER_TOKEN="${A2A_BEARER_TOKEN:-}"
A2A_PORT_INPUT=""
A2A_HOST_INPUT=""
A2A_PUBLIC_URL_INPUT=""
Expand All @@ -32,6 +32,7 @@ OPENCODE_TIMEOUT_INPUT=""
OPENCODE_TIMEOUT_STREAM_INPUT=""
GIT_IDENTITY_NAME_INPUT=""
GIT_IDENTITY_EMAIL_INPUT=""
ENABLE_SECRET_PERSISTENCE_INPUT=""
UPDATE_A2A_INPUT=""
FORCE_RESTART_INPUT=""

Expand Down Expand Up @@ -119,6 +120,9 @@ for arg in "$@"; do
git_identity_email)
GIT_IDENTITY_EMAIL_INPUT="$value"
;;
enable_secret_persistence)
ENABLE_SECRET_PERSISTENCE_INPUT="$value"
;;
update_a2a)
UPDATE_A2A_INPUT="$value"
;;
Expand All @@ -136,17 +140,17 @@ for arg in "$@"; do
esac
done

if [[ -z "$PROJECT_NAME" || -z "$GH_TOKEN" || -z "$A2A_BEARER_TOKEN" ]]; then
if [[ -z "$PROJECT_NAME" ]]; then
cat >&2 <<USAGE
Usage:
GH_TOKEN=<token> A2A_BEARER_TOKEN=<token> [<PROVIDER_SECRET_ENV>=<key>] \
[GH_TOKEN=<token>] [A2A_BEARER_TOKEN=<token>] [<PROVIDER_SECRET_ENV>=<key>] \
./scripts/deploy.sh project=<name> [data_root=<path>] [a2a_port=<port>] [a2a_host=<host>] [a2a_public_url=<url>] \
[a2a_streaming=<bool>] [a2a_log_level=<level>] [a2a_otel_instrumentation_enabled=<bool>] \
[a2a_log_payloads=<bool>] [a2a_log_body_limit=<int>] [a2a_cancel_abort_timeout_seconds=<seconds>] \
[a2a_enable_session_shell=<bool>] \
[opencode_provider_id=<id>] [opencode_model_id=<id>] [opencode_lsp=<bool>] [opencode_log_level=<level>] \
[repo_url=<url>] [repo_branch=<branch>] \
[opencode_timeout=<seconds>] [opencode_timeout_stream=<seconds>] [git_identity_name=<name>] \
[opencode_timeout=<seconds>] [opencode_timeout_stream=<seconds>] [git_identity_name=<name>] [enable_secret_persistence=<bool>] \
[git_identity_email=<email>] [update_a2a=true] [force_restart=true]

Provider secret env vars:
Expand Down Expand Up @@ -185,6 +189,7 @@ export OPENCODE_BIND_HOST="${OPENCODE_BIND_HOST:-127.0.0.1}"
export OPENCODE_LOG_LEVEL="${OPENCODE_LOG_LEVEL:-WARNING}"
export OPENCODE_EXTRA_ARGS="${OPENCODE_EXTRA_ARGS:-}"
export OPENCODE_LSP="${OPENCODE_LSP:-false}"
export ENABLE_SECRET_PERSISTENCE="${ENABLE_SECRET_PERSISTENCE:-false}"

if [[ -n "$A2A_HOST_INPUT" ]]; then
export A2A_HOST="$A2A_HOST_INPUT"
Expand Down Expand Up @@ -224,6 +229,7 @@ export_if_present "A2A_LOG_PAYLOADS" "$A2A_LOG_PAYLOADS_INPUT"
export_if_present "A2A_LOG_BODY_LIMIT" "$A2A_LOG_BODY_LIMIT_INPUT"
export_if_present "A2A_CANCEL_ABORT_TIMEOUT_SECONDS" "$A2A_CANCEL_ABORT_TIMEOUT_SECONDS_INPUT"
export_if_present "A2A_ENABLE_SESSION_SHELL" "$A2A_ENABLE_SESSION_SHELL_INPUT"
export_if_present "ENABLE_SECRET_PERSISTENCE" "$ENABLE_SECRET_PERSISTENCE_INPUT"

is_truthy() {
case "${1,,}" in
Expand Down
2 changes: 2 additions & 0 deletions scripts/deploy/install_units.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Environment=OPENCODE_A2A_DIR=${OPENCODE_A2A_DIR}
Environment=UV_PYTHON_DIR=${UV_PYTHON_DIR}
Environment=PATH=${OPENCODE_CORE_DIR}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
EnvironmentFile=${DATA_ROOT}/%i/config/opencode.env
EnvironmentFile=-${DATA_ROOT}/%i/config/opencode.auth.env
EnvironmentFile=-${DATA_ROOT}/%i/config/opencode.secret.env
Environment=HOME=${DATA_ROOT}/%i

Expand Down Expand Up @@ -66,6 +67,7 @@ Environment=OPENCODE_A2A_DIR=${OPENCODE_A2A_DIR}
Environment=OPENCODE_CORE_DIR=${OPENCODE_CORE_DIR}
Environment=UV_PYTHON_DIR=${UV_PYTHON_DIR}
EnvironmentFile=${DATA_ROOT}/%i/config/a2a.env
EnvironmentFile=-${DATA_ROOT}/%i/config/a2a.secret.env
Environment=HOME=${DATA_ROOT}/%i

ExecStart=${OPENCODE_A2A_DIR}/scripts/deploy/run_a2a.sh
Expand Down
Loading
Loading