diff --git a/.gitignore b/.gitignore index 058d287..1d0b29c 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ pip-delete-this-directory.txt # Local logs logs/ +run/ # Unit test / coverage reports htmlcov/ diff --git a/README.md b/README.md index 7134139..629ad48 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,29 @@ need a stable service layer around it. This repository provides that layer by: - OpenCode session query/control extensions and provider/model discovery - systemd multi-instance deployment and lightweight current-user deployment +## Extension Capability Overview + +The Agent Card declares six extension URIs. Shared contracts are intended for +any compatible consumer; OpenCode-specific contracts stay provider-scoped even +though they are exposed through A2A JSON-RPC. + +| Extension URI | Scope | Primary use | +| --- | --- | --- | +| `urn:a2a:session-binding/v1` | Shared | Bind a main chat request to an existing upstream session via `metadata.shared.session.id` | +| `urn:a2a:model-selection/v1` | Shared | Override the default upstream model for one main chat request | +| `urn:a2a:stream-hints/v1` | Shared | Advertise canonical stream metadata for blocks, usage, interrupts, and session hints | +| `urn:opencode-a2a:session-query/v1` | OpenCode-specific | Query external sessions and invoke OpenCode session control methods | +| `urn:opencode-a2a:provider-discovery/v1` | OpenCode-specific | Discover normalized OpenCode provider/model summaries | +| `urn:a2a:interactive-interrupt/v1` | Shared | Reply to interrupt callbacks observed from stream metadata | + +Detailed consumption guidance: + +- Shared session binding: [`docs/guide.md#shared-session-binding-contract`](docs/guide.md#shared-session-binding-contract) +- Shared model selection: [`docs/guide.md#shared-model-selection-contract`](docs/guide.md#shared-model-selection-contract) +- Shared stream hints: [`docs/guide.md#shared-stream-hints-contract`](docs/guide.md#shared-stream-hints-contract) +- OpenCode session query and provider discovery: [`docs/guide.md#opencode-session-query--provider-discovery-a2a-extensions`](docs/guide.md#opencode-session-query--provider-discovery-a2a-extensions) +- Shared interrupt callback: [`docs/guide.md#shared-interrupt-callback-a2a-extension`](docs/guide.md#shared-interrupt-callback-a2a-extension) + ## Design Principle One `OpenCode + opencode-a2a-server` instance pair is treated as a @@ -116,7 +139,11 @@ uv run pytest ## Documentation Map - [docs/guide.md](docs/guide.md) - Product behavior, API contracts, streaming/session/interrupt details. + Product behavior, API contracts, and detailed streaming/session/interrupt + consumption guidance. +- [docs/agent_deploy_sop.md](docs/agent_deploy_sop.md) + Operator-facing SOP for choosing, starting, verifying, and releasing + `deploy.sh` vs `deploy_light.sh`. - [scripts/README.md](scripts/README.md) Entry points for init, deploy, lightweight deploy, local start, and uninstall scripts. diff --git a/docs/agent_deploy_sop.md b/docs/agent_deploy_sop.md new file mode 100644 index 0000000..ee3651b --- /dev/null +++ b/docs/agent_deploy_sop.md @@ -0,0 +1,341 @@ +# Agent Self-Deploy and Release SOP + +Related issue: `#145` + +This SOP explains how a consumer-side agent can provision, verify, and release +its own `opencode-a2a-server` instance pair. + +## Goal + +The operator or calling agent should be able to: + +1. choose the correct deployment path +2. start one isolated OpenCode + `opencode-a2a-server` instance +3. verify readiness and basic availability +4. stop or uninstall the instance safely when it is no longer needed + +## Scope and Boundaries + +- This SOP covers two supported startup paths: + - `scripts/deploy.sh`: systemd-managed, production-oriented, multi-instance + deployment + - `scripts/deploy_light.sh`: lightweight current-user background supervisor +- This SOP does not replace protocol documentation. For API and runtime + behavior, see [`guide.md`](./guide.md). +- This SOP does not define Docker or Kubernetes flows. + +## Choose the Deployment Mode + +| Mode | Script | Best for | Trust boundary | Secret handling | +| --- | --- | --- | --- | --- | +| systemd deploy | `scripts/deploy.sh` | long-running, multi-instance, production-oriented setups | isolated project directory under `DATA_ROOT`, systemd units, root-managed config | supports secure default two-step provisioning; `ENABLE_SECRET_PERSISTENCE=true` is optional and explicit | +| lightweight deploy | `scripts/deploy_light.sh` | trusted local/self-host use under the current Linux user | current user and current workspace | secrets come from the current shell environment; `ENABLE_SECRET_PERSISTENCE` does not apply | + +Use `deploy.sh` when you need: + +- systemd restart behavior +- stable per-project runtime directories +- root-only secret files +- multiple named instances on one host + +Use `deploy_light.sh` when you need: + +- fast local startup +- no systemd units +- no root-managed instance layout +- one trusted current-user runtime boundary + +## Shared Input Contract + +### Required Inputs + +For `deploy.sh`: + +- `project=` +- `GH_TOKEN` and `A2A_BEARER_TOKEN` + - required immediately when `ENABLE_SECRET_PERSISTENCE=true` + - otherwise required in root-only secret env files before the second deploy + +For `deploy_light.sh`: + +- `A2A_BEARER_TOKEN` +- `workdir=/abs/path/to/workspace` + +### Common Optional Inputs + +- `a2a_host=` +- `a2a_port=` +- `a2a_public_url=` +- `opencode_provider_id=` +- `opencode_model_id=` + +### Provider Keys + +Provider secrets are environment-only inputs: + +- `GOOGLE_GENERATIVE_AI_API_KEY` +- `OPENAI_API_KEY` +- `ANTHROPIC_API_KEY` +- `AZURE_OPENAI_API_KEY` +- `OPENROUTER_API_KEY` + +Do not pass these values via CLI `key=value`. + +## Path A: systemd Deploy (`deploy.sh`) + +This is the preferred path for durable and production-oriented deployments. + +### Preconditions + +Recommended checks: + +```bash +command -v systemctl +command -v sudo +``` + +One-time host bootstrap: + +```bash +./scripts/init_system.sh +``` + +### Secret Strategy + +`deploy.sh` supports two secret modes. + +Default and recommended mode: + +- `ENABLE_SECRET_PERSISTENCE=false` +- deploy does not write `GH_TOKEN`, `A2A_BEARER_TOKEN`, or provider keys to disk +- root-only runtime secret files must be provisioned under + `/data/opencode-a2a//config/` + +Optional legacy-style mode: + +- `ENABLE_SECRET_PERSISTENCE=true` +- deploy writes root-only secret env files for the instance +- use only when you explicitly accept secret persistence on disk + +### Start Instructions + +#### Option A1: secure two-step deploy (`ENABLE_SECRET_PERSISTENCE=false`) + +Bootstrap directories and example files: + +```bash +./scripts/deploy.sh project=alpha a2a_port=8010 a2a_host=127.0.0.1 +``` + +Populate the generated templates as `root`: + +```bash +sudo cp /data/opencode-a2a/alpha/config/opencode.auth.env.example /data/opencode-a2a/alpha/config/opencode.auth.env +sudo cp /data/opencode-a2a/alpha/config/a2a.secret.env.example /data/opencode-a2a/alpha/config/a2a.secret.env +sudoedit /data/opencode-a2a/alpha/config/opencode.auth.env +sudoedit /data/opencode-a2a/alpha/config/a2a.secret.env +``` + +Re-run deploy to start services: + +```bash +./scripts/deploy.sh project=alpha a2a_port=8010 a2a_host=127.0.0.1 +``` + +#### Option A2: explicit secret persistence (`ENABLE_SECRET_PERSISTENCE=true`) + +```bash +read -rsp 'GH_TOKEN: ' GH_TOKEN; echo +read -rsp 'A2A_BEARER_TOKEN: ' A2A_BEARER_TOKEN; echo +GH_TOKEN="${GH_TOKEN}" A2A_BEARER_TOKEN="${A2A_BEARER_TOKEN}" ENABLE_SECRET_PERSISTENCE=true \ +./scripts/deploy.sh project=alpha a2a_port=8010 a2a_host=127.0.0.1 +``` + +Public URL example: + +```bash +GH_TOKEN="${GH_TOKEN}" A2A_BEARER_TOKEN="${A2A_BEARER_TOKEN}" ENABLE_SECRET_PERSISTENCE=true \ +./scripts/deploy.sh project=alpha a2a_port=8010 a2a_public_url=https://a2a.example.com +``` + +### Update or Restart + +```bash +./scripts/deploy.sh project=alpha update_a2a=true force_restart=true +``` + +### Readiness Checks + +Check systemd status: + +```bash +sudo systemctl status opencode@alpha.service --no-pager +sudo systemctl status opencode-a2a-server@alpha.service --no-pager +``` + +Check health: + +```bash +curl -fsS http://127.0.0.1:8010/health +``` + +Optional Agent Card check: + +```bash +curl -fsS http://127.0.0.1:8010/.well-known/agent-card.json +``` + +Success criteria: + +- `deploy.sh` exits with code `0` +- `opencode@.service` and `opencode-a2a-server@.service` + are active/running +- `GET /health` returns HTTP 200 with `{"status":"ok"}` + +### Release / Uninstall + +Preview first: + +```bash +./scripts/uninstall.sh project=alpha +``` + +Apply: + +```bash +./scripts/uninstall.sh project=alpha confirm=UNINSTALL +``` + +Notes: + +- shared template units are not removed +- preview mode is non-destructive +- uninstall may return exit code `2` when completion includes non-fatal warnings + +## Path B: Lightweight Deploy (`deploy_light.sh`) + +This path is for trusted local or self-host scenarios under the current Linux +user. + +### Key Differences from `deploy.sh` + +- no systemd units +- no root-only instance config layout +- no `ENABLE_SECRET_PERSISTENCE` +- provider keys and `A2A_BEARER_TOKEN` are inherited directly from the current + shell environment + +### Start Instructions + +Minimum example: + +```bash +export A2A_BEARER_TOKEN='' +./scripts/deploy_light.sh start workdir=/abs/path/to/workspace +``` + +Example with explicit ports and instance name: + +```bash +export A2A_BEARER_TOKEN='' +./scripts/deploy_light.sh start \ + instance=demo \ + workdir=/srv/workspaces/demo \ + a2a_host=127.0.0.1 \ + a2a_port=8010 \ + a2a_public_url=http://127.0.0.1:8010 \ + opencode_bind_host=127.0.0.1 \ + opencode_bind_port=4106 +``` + +If provider keys are needed, export them in the same shell before startup: + +```bash +export OPENAI_API_KEY='' +export A2A_BEARER_TOKEN='' +./scripts/deploy_light.sh start workdir=/abs/path/to/workspace +``` + +### Lifecycle Commands + +```bash +./scripts/deploy_light.sh status +./scripts/deploy_light.sh stop +./scripts/deploy_light.sh restart workdir=/abs/path/to/workspace +``` + +### Readiness Checks + +`deploy_light.sh start` already waits for both: + +1. OpenCode runtime readiness +2. local Agent Card readiness + +You can still verify manually: + +```bash +curl -fsS http://127.0.0.1:8000/health +curl -fsS http://127.0.0.1:8000/.well-known/agent-card.json +``` + +### Release + +```bash +./scripts/deploy_light.sh stop +``` + +This stops the current-user background processes and preserves local logs/run +metadata under `logs/light//` and `run/light//`. + +## Failure Modes and Recovery Guidance + +Common failure classes: + +1. missing required secrets +2. `sudo` unavailable or interactive policy not satisfied for systemd deploy +3. invalid `project` or port inputs +4. provider/model configuration without matching provider keys +5. readiness check failure after process start + +Recommended response: + +1. inspect command stderr +2. inspect systemd or local log files +3. fix missing inputs or secret files +4. re-run the same deploy command + +For systemd logs: + +```bash +sudo journalctl -u opencode@alpha.service -n 200 --no-pager +sudo journalctl -u opencode-a2a-server@alpha.service -n 200 --no-pager +``` + +## Security Baseline + +- Do not pass secrets through CLI flags or `key=value` arguments. +- `ENABLE_SECRET_PERSISTENCE=true` is an explicit tradeoff, not the secure + default. +- `deploy_light.sh` assumes the current user is already trusted with provider + keys and workspace access. +- `A2A_ENABLE_SESSION_SHELL=true` remains a high-risk switch and should be + limited to trusted internal cases. +- One deployed instance pair is a single-tenant trust boundary, not a secure + multi-tenant runtime. + +## Minimal Execution Templates + +### systemd deploy + +1. run `init_system.sh` once per host if needed +2. choose secret mode +3. execute `deploy.sh` +4. verify service state and `/health` +5. later run `uninstall.sh` with preview first + +### lightweight deploy + +1. export `A2A_BEARER_TOKEN` and any needed provider keys +2. execute `deploy_light.sh start` +3. verify `/health` or Agent Card +4. later run `deploy_light.sh stop` diff --git a/docs/guide.md b/docs/guide.md index e500ac9..4cdb45d 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -104,9 +104,29 @@ Key variables to understand protocol behavior: - Agent Card declares OAuth2 only when both `A2A_OAUTH_AUTHORIZATION_URL` and `A2A_OAUTH_TOKEN_URL` are set. -## Session Continuation Contract +## Extension Capability Overview -To continue a historical OpenCode session, include this metadata key in each invoke request: +For a quick capability showcase, see the README overview: + +- [`README.md#extension-capability-overview`](../README.md#extension-capability-overview) + +This guide focuses on how to consume the declared capabilities. + +Important distinction: + +- Agent Card extension declarations answer "what capability is available?" +- Runtime payload metadata answers "what happened on this request/stream?" +- Clients should not treat runtime metadata alone as a substitute for + capability discovery when an extension URI is already declared. + +## Shared Session Binding Contract + +Agent Card capability: + +- URI: `urn:a2a:session-binding/v1` + +To continue a historical OpenCode session, include this metadata key in each +invoke request: - `metadata.shared.session.id`: target upstream session ID @@ -115,6 +135,20 @@ Server behavior: - If provided, the request is sent to that exact OpenCode session. - If omitted, a new session is created and cached by `(identity, contextId) -> session_id`. +- `contextId` remains the A2A conversation context key for task continuity; it + is not a replacement for the upstream session identifier. +- OpenCode-private context such as `metadata.opencode.directory` may be + supplied alongside `metadata.shared.session.id`, but it does not change the + shared session-binding key. + +Consumer guidance: + +- Use this extension declaration to decide whether the server explicitly + supports shared session rebinding. +- On the request path, write the upstream session identity to + `metadata.shared.session.id`. +- On the response/query path, treat `metadata.shared.session` as runtime + metadata and not as a separate capability declaration. Minimal example: @@ -140,6 +174,10 @@ curl -sS http://127.0.0.1:8000/v1/message:send \ ## Shared Model Selection Contract +Agent Card capability: + +- URI: `urn:a2a:model-selection/v1` + To override the default upstream model for one main-chat request, include: - `metadata.shared.model.providerID` @@ -178,6 +216,56 @@ curl -sS http://127.0.0.1:8000/v1/message:send \ }' ``` +## Shared Stream Hints Contract + +Agent Card capability: + +- URI: `urn:a2a:stream-hints/v1` + +This extension declares that streaming and final task payloads use canonical +shared metadata for block, usage, interrupt, and session hints. + +Declaration versus runtime: + +- The URI `urn:a2a:stream-hints/v1` is the capability declaration. +- The actual request/stream payloads carry the runtime hints under shared + metadata fields. + +Shared runtime fields: + +- `metadata.shared.stream` + - block-level stream metadata such as `block_type`, `source`, `message_id`, + `event_id`, `sequence`, and `role` +- `metadata.shared.usage` + - normalized usage data such as `input_tokens`, `output_tokens`, + `total_tokens`, and optional `cost` +- `metadata.shared.interrupt` + - normalized interrupt request or resolution metadata including `request_id`, + `type`, `phase`, and callback-safe details +- `metadata.shared.session` + - session-level metadata such as the bound upstream session ID and session + title when available + +Consumer guidance: + +- Use the extension declaration to know the server emits canonical shared + stream hints. +- Use runtime metadata to render block timelines, token usage, and interactive + interruptions. +- Do not infer capability support only from seeing one runtime field on one + response; rely on Agent Card discovery first when possible. +- Treat `metadata.shared.interrupt` as observation data. Callback operations + are a separate shared capability declared by + `urn:a2a:interactive-interrupt/v1`. + +Minimal stream semantics summary: + +- `text`, `reasoning`, and `tool_call` are emitted as canonical block types +- `message_id` and `event_id` preserve stable timeline identity where possible +- `sequence` is the per-request canonical stream sequence +- final task/status metadata may repeat normalized usage and interrupt context + even after the streaming phase ends + ## OpenCode Session Query & Provider Discovery (A2A Extensions) This service exposes OpenCode session list/message-history queries, session diff --git a/scripts/README.md b/scripts/README.md index ab6bb01..5101ada 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -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) +- Operator-facing deploy SOP: + - [`../docs/agent_deploy_sop.md`](../docs/agent_deploy_sop.md) - Security boundary and disclosure guidance: - [`../SECURITY.md`](../SECURITY.md) - Script operational details (how to run and operate each script):