Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8188707
refactor(upstream): add typed adapter boundary (#2)
Oct 25, 2026
a2f8691
fix(upstream): allow receipt-only submissions safely (#2)
Oct 25, 2026
767e706
feat(upstream): add opt-in OpenCode V2 session transport (#3)
Oct 25, 2026
a1362e8
fix(card): avoid advertising V2 execution capabilities (#3)
Oct 25, 2026
5d83233
test(upstream): cover V2 session status failures (#3)
Oct 25, 2026
73ffcd8
fix(execution): enforce upstream outcome safety
Oct 25, 2026
8d5494a
fix(compatibility): gate extension capabilities by API version (#6)
Oct 25, 2026
7845d0e
test(compatibility): retain session-slice fail-closed wording (#6)
Oct 25, 2026
cc8470d
style(tests): format session capability assertion
Oct 25, 2026
b129d3a
feat(upstream): add bounded exclusive V2 execution engine (#4)
Oct 25, 2026
a2c76f3
feat(v2): enable guarded exclusive execution and interventions (#4, #5)
Oct 25, 2026
6a907ae
test(v2): mark synthetic credentials and source digest as false posit…
Oct 25, 2026
16e99ea
test(v2): mark second synthetic upstream password fixture
Oct 25, 2026
6471dbb
test(compatibility): add isolated V2 acceptance and fault probes (#6)
Oct 25, 2026
372b84e
test(smoke): preserve author script mode
Oct 25, 2026
880299d
docs(v2): retain author correlation evidence referenced by profile
Oct 25, 2026
e98b225
Merge branch 'upstream/v2-exclusive-integration' into upstream/v2-exc…
Oct 25, 2026
69865e1
docs(v2): separate upstream regression evidence from author live runs
Oct 25, 2026
4a496e5
test(compatibility): identify synthetic fixtures for secret scanning
Oct 25, 2026
8e06272
test(v2): drop redundant digest scanner baseline entry
Oct 25, 2026
ea94fcb
Merge branch 'upstream/v2-exclusive-integration' into upstream/v2-exc…
Oct 25, 2026
0dc3e13
docs(v2): align upstream validation with tracked-head scanner fix
Oct 25, 2026
ca26159
test(v2): verify session ownership and external-directory denials
Oct 25, 2026
aa61f8e
docs(v2): distinguish expanded author acceptance from upstream QA
Oct 25, 2026
bbddca7
fix(v2): reject ambiguous exclusive workspace changes
Oct 25, 2026
fd4c88e
docs(v2): record fixed-workspace upstream regression evidence
Oct 25, 2026
a4f448f
fix(v2): require committed ownership before preferred-session reuse
Oct 25, 2026
b37a849
fix(v2): reject malformed reserved metadata without fallback
Oct 25, 2026
8f4fdb7
docs(v2): record ownership and metadata guard regression evidence
Oct 25, 2026
6e38d10
test(v2): separate SQLite startup from cancellation deadlines
Oct 25, 2026
dfa15f7
docs(v2): document separate SDK bootstrap budget and observed timeout
Oct 25, 2026
fab667a
feat(config)!: default to V2 with explicit V1 compatibility
Oct 25, 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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pip-delete-this-directory.txt
# Local logs
logs/
run/
.opencode/a2a/state/

# Unit test / coverage reports
htmlcov/
Expand Down Expand Up @@ -200,6 +201,9 @@ cython_debug/
# Ruff stuff:
.ruff_cache/

# Isolated issue-scoped agent worktrees
.worktrees/

# PyPI configuration file
.pypirc

Expand Down
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,16 @@ Then start the A2A runtime in another terminal:

```bash
A2A_STATIC_AUTH_CREDENTIALS='[{"scheme":"bearer","token":"dev-token","principal":"automation"}]' \
OPENCODE_API_VERSION=v1 \
OPENCODE_BASE_URL=http://127.0.0.1:4096 \
OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
uv run opencode-a2a serve
```

This development example explicitly selects V1 compatibility mode. V2 is the
runtime default, but execution requires the opt-in and private deployment
preconditions in [docs/v2-exclusive-execution.md](docs/v2-exclusive-execution.md).

## Validation

Run the default validation baseline before opening a PR:
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ flowchart TD

## Quick Start

**Breaking default change:** V2 is now the default upstream API, but A2A
execution remains disabled until `OPENCODE_V2_EXECUTION=exclusive` is explicitly
set under the [private deployment contract](docs/v2-exclusive-execution.md).
This does not enable unrestricted shared sessions. Existing V1 deployments must
set `OPENCODE_API_VERSION=v1` before upgrading; there is no automatic fallback.
The quick start below uses that explicit V1 compatibility mode, not V2 deployment.

Install the released CLI with `uv tool`:

```bash
Expand Down Expand Up @@ -74,6 +81,7 @@ Then start `opencode-a2a` against that upstream:
```bash
DEMO_BEARER_TOKEN="$(python3 -c 'import secrets; print(secrets.token_hex(24))')"
A2A_STATIC_AUTH_CREDENTIALS='[{"scheme":"bearer","token":"'"${DEMO_BEARER_TOKEN}"'","principal":"automation"}]' \
OPENCODE_API_VERSION=v1 \
OPENCODE_BASE_URL=http://127.0.0.1:4096 \
A2A_TASK_STORE_DATABASE_URL=sqlite+aiosqlite:///./opencode-a2a.db \
A2A_HOST=127.0.0.1 \
Expand All @@ -91,6 +99,17 @@ curl http://127.0.0.1:8000/.well-known/agent-card.json

## Capabilities

The following capability list describes explicit OpenCode API V1 compatibility
mode (`OPENCODE_API_VERSION=v1`). With the default V2 API, only internal
experimental session transport is available: A2A message execution fails before upstream requests, streaming and
all extensions are disabled. A separate opt-in
[`OPENCODE_V2_EXECUTION=exclusive` profile](docs/v2-exclusive-execution.md)
adds restricted text execution and streaming read/question interventions in a
dedicated private instance pair. It does not support shared upstream sessions,
arbitrary tools, model overrides, or automatic recovery after event loss. Read
the deployment preconditions and [compatibility matrix](docs/compatibility.md#runtime-support)
before enabling it.

- A2A HTTP+JSON endpoints such as `/message:send` and `/message:stream`
- A2A JSON-RPC support on `POST /`
- SDK-owned A2A task surfaces such as `GET /tasks`, task push notification config routes, JSON-RPC `GetExtendedAgentCard`, and HTTP `GET /extendedAgentCard`
Expand Down
1 change: 1 addition & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This repository wraps OpenCode as an A2A runtime. It exposes A2A HTTP+JSON and J
- 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.
- This project does not ship host bootstrap or process-manager wrappers as an official product capability. Operators remain responsible for file permissions, secret storage, service users, and supervisor-specific hardening.
- Experimental V2 exclusive execution additionally requires a dedicated private network namespace, unshared upstream credentials, one gateway process, and no autonomous session writers. Loopback URL validation is not topology verification. See [the restricted profile](docs/v2-exclusive-execution.md) before enabling it; shared everyday OpenCode services are unsupported.
- Release artifacts are built in a read-only job, audited, and checksummed before separate least-privilege PyPI and GitHub Release jobs can upload them. Releases include a CycloneDX runtime SBOM.

## Threat Model
Expand Down
207 changes: 206 additions & 1 deletion docs/compatibility.md

Large diffs are not rendered by default.

79 changes: 79 additions & 0 deletions docs/evidence/opencode-v2.0.28-correlation-probe.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
"evidence_type": "sanitized live upstream observation",
"upstream_version": "2.0.28",
"model": "openai/gpt-6-luna-fast",
"adapter_used": false,
"workspace": "fresh temporary workspace",
"event_subscription_status": 200,
"event_subscription_ready_before_admission": true,
"session_permissions_denied": [
"external_directory",
"bash",
"edit",
"write"
],
"prompt_observations": {
"delivery": "queue",
"inputs": ["A", "B"],
"second_input_submitted_after_first_step_started": true,
"admission_receipt_ids_match_input_ids": [true, true],
"delivered_inbox_ids_match_input_ids": [true, true],
"projected_user_messages": 2,
"projected_user_message_ids_match_input_ids": true,
"assistant_messages": 2,
"both_expected_sentinel_strings_were_found_in_assistant_history": [true, true],
"terminal_events": 1,
"terminal_event_type": "session.execution.succeeded",
"terminal_event_properties": ["sessionID"],
"terminal_event_has_input_id": false,
"assistant_message_has_input_link_field": false
},
"admission_retry_and_pending_cancel": {
"same_id_retry_returned_same_receipt": true,
"undelivered_input_delete_status": 204,
"deleted_input_remained_pending": false
},
"after_volatile_sse_disconnect": {
"event_feed_closed_during_execution": false,
"experimental_session_log_status": 200,
"sse_data_frames": 1,
"event_types": ["log.synced"]
},
"sanitized_event_trace": [
{"type": "session.inbox.enqueued", "inbox": "CANCELLED", "properties": ["inboxID", "item", "sessionID"]},
{"type": "session.inbox.cancelled", "inbox": "CANCELLED", "properties": ["inboxID", "sessionID"]},
{"type": "session.inbox.enqueued", "inbox": "A", "properties": ["inboxID", "item", "sessionID"]},
{"type": "session.execution.started", "properties": ["sessionID"]},
{"type": "session.instructions.updated", "properties": ["delta", "sessionID"]},
{"type": "session.inbox.delivered", "inbox": "A", "properties": ["inboxID", "sessionID"]},
{"type": "session.step.started", "properties": ["agent", "assistantMessageID", "model", "sessionID", "started"]},
{"type": "session.reasoning.started", "properties": ["assistantMessageID", "ordinal", "sessionID", "state"]},
{"type": "session.inbox.enqueued", "inbox": "B", "properties": ["inboxID", "item", "sessionID"]},
{"type": "session.reasoning.ended", "properties": ["assistantMessageID", "ordinal", "sessionID", "state", "text"]},
{"type": "session.text.started", "properties": ["assistantMessageID", "ordinal", "sessionID"]},
{"type": "session.text.delta", "properties": ["assistantMessageID", "delta", "ordinal", "sessionID"]},
{"type": "session.text.ended", "properties": ["assistantMessageID", "ordinal", "sessionID", "state", "text"]},
{"type": "session.step.streamed", "properties": ["assistantMessageID", "sessionID"]},
{"type": "session.step.ended", "properties": ["assistantMessageID", "cost", "finish", "providerState", "sessionID", "tokens"]},
{"type": "session.usage.updated", "properties": ["cost", "sessionID", "tokens"]},
{"type": "session.inbox.delivered", "inbox": "B", "properties": ["inboxID", "sessionID"]},
{"type": "session.step.started", "properties": ["agent", "assistantMessageID", "model", "sessionID", "started"]},
{"type": "session.reasoning.started", "properties": ["assistantMessageID", "ordinal", "sessionID", "state"]},
{"type": "session.reasoning.ended", "properties": ["assistantMessageID", "ordinal", "sessionID", "state", "text"]},
{"type": "session.text.started", "properties": ["assistantMessageID", "ordinal", "sessionID"]},
{"type": "session.text.delta", "properties": ["assistantMessageID", "delta", "ordinal", "sessionID"]},
{"type": "session.text.delta", "properties": ["assistantMessageID", "delta", "ordinal", "sessionID"]},
{"type": "session.text.ended", "properties": ["assistantMessageID", "ordinal", "sessionID", "state", "text"]},
{"type": "session.step.streamed", "properties": ["assistantMessageID", "sessionID"]},
{"type": "session.step.ended", "properties": ["assistantMessageID", "cost", "finish", "providerState", "sessionID", "tokens"]},
{"type": "session.usage.updated", "properties": ["cost", "sessionID", "tokens"]},
{"type": "session.execution.succeeded", "properties": ["sessionID"]}
],
"redactions": [
"API credential and Basic-auth password",
"temporary workspace and session IDs",
"prompt text, assistant text, reasoning text, and sentinel strings",
"raw event payloads"
],
"scope_note": "One controlled upstream run only; this is not a general API guarantee or an A2A execution acceptance test."
}
59 changes: 59 additions & 0 deletions docs/evidence/v2-default-acceptance.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"scope": "V2 is the default API; execution remains disabled without explicit exclusive acknowledgement; V1 requires explicit selection",
"source_tree_sha256": ["9650ee76", "46b5a371", "e45794c3", "0e900489", "8f806438", "9808a3e5", "b26385aa", "b1c8e4da"],
"source_hash_method": "Concatenate digest groups. Input: sorted src/opencode_a2a/**/*.py; relative path, NUL, file bytes, NUL",
"versions": {
"opencode_v1": "1.18.19",
"opencode_v2": "2.0.28",
"a2a_sdk": "1.2.3",
"observed_model": "openai/gpt-6-luna-fast"
},
"configuration": {
"default_api": "v2",
"default_v2_execution": "disabled",
"explicit_v1": "OPENCODE_API_VERSION=v1",
"explicit_v2_execution": "OPENCODE_V2_EXECUTION=exclusive",
"automatic_detection_or_fallback": false,
"private_deployment_preconditions_unchanged": true
},
"deterministic_validation": {
"doctor_command": "bash ./scripts/doctor.sh",
"tests_passed": 1026,
"tests_failed": 0,
"line_coverage_percent": 92.67,
"lint_format_typing_coverage_build_wheel_smoke": "PASS",
"conformance_passed": 8,
"conformance_failed": 0,
"environment_selection_and_disabled_execution_http_tests": "PASS"
},
"fresh_live_validation": {
"v1_explicit_selection_auth_extensions_and_real_model_streaming_smoke": "PASS",
"v2_omitted_api_selector_session_transport_and_disabled_execution_setting": "PASS",
"v2_native_namespace_and_execution_probe": "PASS within its recorded subset",
"v2_gateway_api_selector_omitted": true,
"v2_exclusive_acknowledgement_explicit": true,
"v2_rest_jsonrpc_stream_unary_followup_and_persisted_terminal": "PASS",
"v2_concurrent_sessions_serialization_local_queued_and_active_cancel": "PASS",
"v2_read_allow_reject_question_string_multiselect_and_callback_denials": "PASS",
"v2_session_ownership_workspace_metadata_and_external_directory_denials": "PASS",
"v2_prompt_admissions": 14,
"v2_global_event_connections": 1,
"v2_negative_scenarios": [
"gap", "lost-terminal", "lost-admission", "provider-failure",
"intervention-gap", "expired-intervention"
],
"v2_negative_scenarios_passed": 6,
"v2_each_negative_admissions": 1,
"v2_each_negative_event_connections": 1,
"v2_negative_false_success_or_resubmission": false,
"live_reasoning_blocks": "NOT VERIFIED"
},
"reproduction": {
"v1": "OPENCODE_BIN=<pinned-1.18.19-binary> OPENCODE_CONFIG_CONTENT='{\"model\":\"openai/gpt-6-luna-fast\"}' bash scripts/live_opencode_smoke.sh",
"v2_session": "bash scripts/live_opencode_v2_session_smoke.sh",
"v2_execution_and_faults": "docs/v2-exclusive-execution.md#reproduce-the-live-evidence"
},
"limitations": "Private gateway-exclusive single-flight deployment only; no unrestricted sessions or transparent recovery; unsupported capabilities remain unchanged",
"historical_evidence": "v2-exclusive-acceptance.json describes the earlier V1-default source; it is not an exact-source claim for this default switch",
"redactions": ["Credentials", "Raw prompts/model output", "Opaque session/task/request IDs", "Runtime paths and PIDs"]
}
142 changes: 142 additions & 0 deletions docs/evidence/v2-exclusive-acceptance.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
{
"verification_date": "2026-10-25",
"verified_at": "2026-10-25T19:31:05+01:00",
"source_tree_sha256": ["ecd2b57c", "fab35067", "04aeabac", "b8acdc58", "d4ddc9e6", "f4bd578b", "e691536e", "8cf3bef8"],
"source_hash_method": "Concatenate public digest groups. Input: sorted src/opencode_a2a/**/*.py; relative path, NUL, file bytes, NUL",
"scope": "Experimental private, gateway-created, single-flight sessions only; not general OpenCode V2 compatibility",
"versions": {
"opencode_v1": "1.18.19",
"opencode_v2": "2.0.28",
"a2a_sdk": "1.2.3",
"observed_v2_model": "openai/gpt-6-luna-fast"
},
"operator_boundary": {
"upstream_loopback_inside_disposable_network_namespace": "PASS",
"authenticated_readiness_before_network_denial_checks": "PASS",
"host_and_independent_container_cannot_reach_upstream_listener": "PASS",
"missing_and_wrong_upstream_credentials_rejected": "PASS",
"topology_automatically_certified_by_adapter": false,
"fresh_private_state_and_no_external_writers_required": true,
"protected_configuration_and_one_gateway_process_required": true
},
"v2_native_single_flight": {
"known_input_admission_and_delivery": "PASS",
"server_connected_before_admission": "PASS",
"ordered_assistant_step_and_later_durable_terminal": "PASS",
"exact_observed_assistant_projection": "PASS",
"sequential_followup_retains_context": "PASS",
"parallel_distinct_sessions": "PASS",
"question_form_answer_and_active_interruption": "PASS"
},
"v2_a2a_live": {
"rest_streaming": "PASS",
"rest_unary": "PASS",
"jsonrpc_streaming": "PASS",
"jsonrpc_unary": "PASS",
"assistant_artifact_only_output_assertions": "PASS",
"append_and_snapshot_reconstruction": "PASS",
"persisted_terminal_task_and_sequential_context": "PASS",
"parallel_distinct_sessions": "PASS",
"same_session_serialization": "PASS",
"local_queued_cancel_without_owner_interrupt": "PASS",
"active_cancel_from_observed_native_interruption": "PASS",
"read_permission_once_and_reject": "PASS",
"string_and_multiselect_question_answers": "PASS",
"invalid_typed_answer_rejected_before_native_reply": "PASS",
"cross_owner_task_and_callback_rejected": "PASS",
"cross_owner_session_hijack_rejected_without_admission": "PASS",
"outside_workspace_metadata_rejected_without_admission": "PASS",
"inside_root_location_changes_rejected_without_admission": "PASS",
"real_external_directory_read_failed_without_approval_or_contents": "PASS",
"stale_callback_rejected": "PASS",
"observed_block_types": ["text", "tool_call"],
"reasoning_blocks_live": "NOT VERIFIED"
},
"v2_live_http_counts": {
"prompt_admissions": 14,
"global_event_connections": 1,
"session_creations": 12,
"profile_config_reads": 26,
"plugin_inventory_reads": 26,
"persisted_permission_checks": 14,
"active_and_inbox_checks_each": 27,
"foreground_wait_and_message_projection_reads_each": 13,
"form_replies": 3,
"permission_replies": 2,
"active_interrupts": 1,
"normal_path_periodic_polling": false
},
"v2_real_upstream_negative_runs": [
{"scenario": "mid_execution_sse_disconnect", "result": "PASS", "admissions": 1, "event_connections": 1, "false_success": false, "resubmission": false},
{"scenario": "dropped_terminal_event", "result": "PASS", "admissions": 1, "event_connections": 1, "false_success": false, "resubmission": false},
{"scenario": "lost_admission_response", "result": "PASS", "admissions": 1, "event_connections": 1, "false_success": false, "resubmission": false},
{"scenario": "real_provider_rejects_synthetic_invalid_credential", "result": "PASS", "native_terminal": "session.execution.failed", "admissions": 1, "false_success": false},
{"scenario": "sse_disconnect_while_input_required", "result": "PASS", "admissions": 1, "native_late_replies": 0, "false_success": false},
{"scenario": "expired_intervention", "result": "PASS", "admissions": 1, "native_late_replies": 0, "false_success": false}
],
"deterministic_adversarial_coverage": [
"Delayed interrupt and pending-delete races cannot reach a successor invocation",
"Real ASGI/SDK cancellation settlement preserves natural completion instead of FAILED",
"Duplicate transient and durable events fail closed",
"Poisoned or ended leases cannot authorize callbacks",
"Concurrent callbacks issue one native mutation",
"Readiness timeout closes reader and releases stream capacity",
"Unknown sessions and weakened permissions reject before prompt admission",
"Preferred IDs cannot claim a registered session before creator ownership is committed",
"Empty, null, numeric and malformed reserved metadata reject without default fallback or native HTTP",
"Idle registry retirement and active/quarantined execution budgets remain bounded"
],
"unsupported": [
"External or unrestricted shared-session writers",
"Automatic replay or continuation after upstream observation loss",
"Adoption of unknown, post-restart, or retired sessions",
"Unary interventions and generic/rich forms",
"Blanket or persistent permission approval",
"Attachments, request-scoped models, arbitrary tools, and a2a_call",
"Claiming native pending deletion succeeded without terminal proof"
],
"v1_expanded_matrix": {
"checks_passed": 12,
"checks_failed": 0,
"checks_not_verified": 0,
"positive_checks": 7,
"negative_auth_ownership_checks": 5,
"assistant_artifact_only_assertions": true,
"artifact_append_and_snapshot_reconstruction": true,
"permission_allow_side_effect_exact_file_content": "PASS",
"permission_reject_native_tool_artifact_and_absent_file": "PASS",
"stream_unary_followup_question_auth_ownership": "PASS",
"cancellation_trials_passed": 6,
"pending_question_trials": 3,
"approved_long_running_shell_trials": 3,
"additional_observation_seconds": 46,
"late_canceled_to_failed_or_completed_transitions": 0,
"persisted_canceled_state": "PASS",
"native_abort_successes": 6,
"native_abort_timeouts_or_errors": 0,
"reproduction_note": "Independent disposable manual runner; repository live_opencode_smoke.sh separately passed with assistant-artifact assertions. No model output or runtime credentials retained here."
},
"deterministic_validation": {
"doctor_command": "bash ./scripts/doctor.sh",
"tests_passed": 1021,
"tests_failed": 0,
"line_coverage_percent": 92.66,
"qa_tested_source": "source_tree_sha256",
"critical_file_coverage_gates": "PASS",
"typing_lint_format_build_wheel_smoke": "PASS",
"conformance_passed": 8,
"conformance_failed": 0,
"default_v2_session_smoke": "PASS",
"v1_repository_streaming_smoke": "PASS",
"sdk_cancel_boundary_repeats": {
"runs": 8,
"tests_per_run": 6,
"failures": 0,
"startup_budget_seconds": 10,
"task_store_poll_interval_seconds": 0.01,
"semantic_cancellation_deadlines_changed": false
}
},
"reproduction": "docs/v2-exclusive-execution.md#reproduce-the-live-evidence",
"redactions": ["Credentials", "Raw prompts and model output", "Opaque session/task/request IDs", "Runtime paths and PIDs"]
}
Loading