opencode-a2a-server 0.2.2__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {opencode_a2a_server-0.2.2/src/opencode_a2a_server.egg-info → opencode_a2a_server-0.2.3}/PKG-INFO +53 -4
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/README.md +52 -3
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/docs/guide.md +23 -3
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/app.py +12 -2
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/config.py +17 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/opencode_client.py +19 -3
- opencode_a2a_server-0.2.3/src/opencode_a2a_server/opencode_upstream.py +122 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3/src/opencode_a2a_server.egg-info}/PKG-INFO +53 -4
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server.egg-info/SOURCES.txt +2 -0
- opencode_a2a_server-0.2.3/tests/test_opencode_upstream.py +162 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_settings.py +10 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/.github/workflows/ci.yml +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/.github/workflows/dependency-health.yml +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/.github/workflows/publish.yml +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/.gitignore +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/.pre-commit-config.yaml +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/.secrets.baseline +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/AGENTS.md +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/CONTRIBUTING.md +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/LICENSE +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/SECURITY.md +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/pyproject.toml +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/scripts/README.md +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/scripts/dependency_health.sh +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/scripts/doctor.sh +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/scripts/health_common.sh +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/scripts/lint.sh +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/scripts/smoke_test_built_cli.sh +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/setup.cfg +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/__init__.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/agent.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/cli.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/extension_contracts.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/jsonrpc_ext.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/jsonrpc_models.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/parts_mapper.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/text_parts.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server.egg-info/dependency_links.txt +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server.egg-info/entry_points.txt +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server.egg-info/requires.txt +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server.egg-info/top_level.txt +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/__init__.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/helpers.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_agent_card.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_agent_errors.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_call_context_builder.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_cancel_contract.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_cancellation.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_cli.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_directory_validation.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_extension_contract_consistency.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_jsonrpc_models.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_jsonrpc_unsupported_method.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_metrics.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_multipart_input.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_opencode_agent_session_binding.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_opencode_client_params.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_opencode_session_extension.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_script_health_contract.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_session_ownership.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_streaming_output_contract.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_transport_contract.py +0 -0
- {opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/uv.lock +0 -0
{opencode_a2a_server-0.2.2/src/opencode_a2a_server.egg-info → opencode_a2a_server-0.2.3}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opencode-a2a-server
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: A2A wrapper service for opencode
|
|
5
5
|
Author: liujuanjuan1984@Intelligent-Internet
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -186,7 +186,10 @@ OPENCODE_MODEL_ID=gemini-3.1-pro-preview \
|
|
|
186
186
|
opencode serve
|
|
187
187
|
|
|
188
188
|
A2A_BEARER_TOKEN=prod-token \
|
|
189
|
+
A2A_HOST=127.0.0.1 \
|
|
190
|
+
A2A_PORT=8000 \
|
|
189
191
|
A2A_PUBLIC_URL=http://127.0.0.1:8000 \
|
|
192
|
+
OPENCODE_MANAGED_SERVER=true \
|
|
190
193
|
OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
|
|
191
194
|
opencode-a2a-server serve
|
|
192
195
|
```
|
|
@@ -196,12 +199,25 @@ exposes to OpenCode.
|
|
|
196
199
|
|
|
197
200
|
Default address: `http://127.0.0.1:8000`
|
|
198
201
|
|
|
202
|
+
OpenCode upstream modes:
|
|
203
|
+
|
|
204
|
+
- Managed upstream: set `OPENCODE_MANAGED_SERVER=true` and
|
|
205
|
+
`opencode-a2a-server` will start a local `opencode serve`, capture its actual
|
|
206
|
+
listening URL, and stop it on shutdown.
|
|
207
|
+
- External upstream: you start and manage `opencode serve` yourself, then point
|
|
208
|
+
`OPENCODE_BASE_URL` at that HTTP endpoint.
|
|
209
|
+
|
|
199
210
|
Common runtime variables:
|
|
200
211
|
|
|
201
212
|
| Variable | Required | Default | Purpose |
|
|
202
213
|
| --- | --- | --- | --- |
|
|
203
214
|
| `A2A_BEARER_TOKEN` | Yes | None | Bearer token required for authenticated runtime requests. |
|
|
204
|
-
| `OPENCODE_BASE_URL` | No | `http://127.0.0.1:4096` | Upstream OpenCode HTTP endpoint. |
|
|
215
|
+
| `OPENCODE_BASE_URL` | No | `http://127.0.0.1:4096` | Upstream OpenCode HTTP endpoint for externally managed upstream mode. |
|
|
216
|
+
| `OPENCODE_MANAGED_SERVER` | No | `false` | Start and manage a local `opencode serve` child process. |
|
|
217
|
+
| `OPENCODE_MANAGED_SERVER_HOST` | No | `127.0.0.1` | Bind host used when managed upstream mode is enabled. |
|
|
218
|
+
| `OPENCODE_MANAGED_SERVER_PORT` | No | auto-pick | Bind port used when managed upstream mode is enabled. |
|
|
219
|
+
| `OPENCODE_COMMAND` | No | `opencode` | OpenCode CLI executable used for managed upstream mode. |
|
|
220
|
+
| `OPENCODE_STARTUP_TIMEOUT` | No | `20` | Seconds to wait for managed upstream startup. |
|
|
205
221
|
| `OPENCODE_WORKSPACE_ROOT` | No | None | Default workspace root exposed to OpenCode. |
|
|
206
222
|
| `OPENCODE_PROVIDER_ID` | No | None | Default provider for the upstream runtime. |
|
|
207
223
|
| `OPENCODE_MODEL_ID` | No | None | Default model for the upstream runtime. Set together with `OPENCODE_PROVIDER_ID`. |
|
|
@@ -220,6 +236,9 @@ Common runtime variables:
|
|
|
220
236
|
If you omit `OPENCODE_PROVIDER_ID` / `OPENCODE_MODEL_ID`, `opencode serve`
|
|
221
237
|
uses your local OpenCode defaults (for example `~/.config/opencode/opencode.json`).
|
|
222
238
|
|
|
239
|
+
When `OPENCODE_MANAGED_SERVER=true`, `OPENCODE_BASE_URL` is ignored and the
|
|
240
|
+
runtime binds itself to the managed child process instead.
|
|
241
|
+
|
|
223
242
|
For provider-specific auth, model IDs, and config details, use the OpenCode
|
|
224
243
|
official docs and CLI:
|
|
225
244
|
|
|
@@ -240,7 +259,7 @@ Use any supervisor you prefer for long-running operation:
|
|
|
240
259
|
The project no longer ships built-in host bootstrap or process-manager
|
|
241
260
|
wrappers. The official product surface is the runtime entrypoint itself.
|
|
242
261
|
|
|
243
|
-
Minimal `systemd` example:
|
|
262
|
+
Minimal self-managed `systemd` example:
|
|
244
263
|
|
|
245
264
|
1. Create an env file such as `/etc/opencode-a2a/alpha.env`:
|
|
246
265
|
|
|
@@ -249,7 +268,7 @@ A2A_BEARER_TOKEN=replace-me
|
|
|
249
268
|
A2A_HOST=127.0.0.1
|
|
250
269
|
A2A_PORT=8000
|
|
251
270
|
A2A_PUBLIC_URL=https://a2a.example.com
|
|
252
|
-
|
|
271
|
+
OPENCODE_MANAGED_SERVER=true
|
|
253
272
|
OPENCODE_WORKSPACE_ROOT=/srv/my-workspace
|
|
254
273
|
```
|
|
255
274
|
|
|
@@ -275,6 +294,33 @@ WantedBy=multi-user.target
|
|
|
275
294
|
|
|
276
295
|
Replace `ExecStart` with the absolute path returned by `command -v opencode-a2a-server`.
|
|
277
296
|
|
|
297
|
+
Minimal managed-upstream foreground example:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
A2A_BEARER_TOKEN=dev-token \
|
|
301
|
+
A2A_HOST=127.0.0.1 \
|
|
302
|
+
A2A_PORT=8000 \
|
|
303
|
+
A2A_PUBLIC_URL=http://127.0.0.1:8000 \
|
|
304
|
+
OPENCODE_MANAGED_SERVER=true \
|
|
305
|
+
OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
|
|
306
|
+
opencode-a2a-server serve
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Advanced: externally managed upstream
|
|
310
|
+
|
|
311
|
+
Use this mode when you intentionally want `opencode serve` and
|
|
312
|
+
`opencode-a2a-server` to be supervised independently.
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
OPENCODE_BASE_URL=http://127.0.0.1:4096 \
|
|
316
|
+
A2A_BEARER_TOKEN=dev-token \
|
|
317
|
+
A2A_HOST=127.0.0.1 \
|
|
318
|
+
A2A_PORT=8000 \
|
|
319
|
+
A2A_PUBLIC_URL=http://127.0.0.1:8000 \
|
|
320
|
+
OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
|
|
321
|
+
opencode-a2a-server serve
|
|
322
|
+
```
|
|
323
|
+
|
|
278
324
|
Migration notes:
|
|
279
325
|
|
|
280
326
|
- `OPENCODE_DIRECTORY` has been removed. Use `OPENCODE_WORKSPACE_ROOT`.
|
|
@@ -297,6 +343,9 @@ OPENCODE_MODEL_ID=gemini-3.1-pro-preview \
|
|
|
297
343
|
opencode serve
|
|
298
344
|
|
|
299
345
|
A2A_BEARER_TOKEN=dev-token \
|
|
346
|
+
OPENCODE_BASE_URL=http://127.0.0.1:4096 \
|
|
347
|
+
A2A_HOST=127.0.0.1 \
|
|
348
|
+
A2A_PORT=8000 \
|
|
300
349
|
OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
|
|
301
350
|
uv run opencode-a2a-server serve
|
|
302
351
|
```
|
|
@@ -148,7 +148,10 @@ OPENCODE_MODEL_ID=gemini-3.1-pro-preview \
|
|
|
148
148
|
opencode serve
|
|
149
149
|
|
|
150
150
|
A2A_BEARER_TOKEN=prod-token \
|
|
151
|
+
A2A_HOST=127.0.0.1 \
|
|
152
|
+
A2A_PORT=8000 \
|
|
151
153
|
A2A_PUBLIC_URL=http://127.0.0.1:8000 \
|
|
154
|
+
OPENCODE_MANAGED_SERVER=true \
|
|
152
155
|
OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
|
|
153
156
|
opencode-a2a-server serve
|
|
154
157
|
```
|
|
@@ -158,12 +161,25 @@ exposes to OpenCode.
|
|
|
158
161
|
|
|
159
162
|
Default address: `http://127.0.0.1:8000`
|
|
160
163
|
|
|
164
|
+
OpenCode upstream modes:
|
|
165
|
+
|
|
166
|
+
- Managed upstream: set `OPENCODE_MANAGED_SERVER=true` and
|
|
167
|
+
`opencode-a2a-server` will start a local `opencode serve`, capture its actual
|
|
168
|
+
listening URL, and stop it on shutdown.
|
|
169
|
+
- External upstream: you start and manage `opencode serve` yourself, then point
|
|
170
|
+
`OPENCODE_BASE_URL` at that HTTP endpoint.
|
|
171
|
+
|
|
161
172
|
Common runtime variables:
|
|
162
173
|
|
|
163
174
|
| Variable | Required | Default | Purpose |
|
|
164
175
|
| --- | --- | --- | --- |
|
|
165
176
|
| `A2A_BEARER_TOKEN` | Yes | None | Bearer token required for authenticated runtime requests. |
|
|
166
|
-
| `OPENCODE_BASE_URL` | No | `http://127.0.0.1:4096` | Upstream OpenCode HTTP endpoint. |
|
|
177
|
+
| `OPENCODE_BASE_URL` | No | `http://127.0.0.1:4096` | Upstream OpenCode HTTP endpoint for externally managed upstream mode. |
|
|
178
|
+
| `OPENCODE_MANAGED_SERVER` | No | `false` | Start and manage a local `opencode serve` child process. |
|
|
179
|
+
| `OPENCODE_MANAGED_SERVER_HOST` | No | `127.0.0.1` | Bind host used when managed upstream mode is enabled. |
|
|
180
|
+
| `OPENCODE_MANAGED_SERVER_PORT` | No | auto-pick | Bind port used when managed upstream mode is enabled. |
|
|
181
|
+
| `OPENCODE_COMMAND` | No | `opencode` | OpenCode CLI executable used for managed upstream mode. |
|
|
182
|
+
| `OPENCODE_STARTUP_TIMEOUT` | No | `20` | Seconds to wait for managed upstream startup. |
|
|
167
183
|
| `OPENCODE_WORKSPACE_ROOT` | No | None | Default workspace root exposed to OpenCode. |
|
|
168
184
|
| `OPENCODE_PROVIDER_ID` | No | None | Default provider for the upstream runtime. |
|
|
169
185
|
| `OPENCODE_MODEL_ID` | No | None | Default model for the upstream runtime. Set together with `OPENCODE_PROVIDER_ID`. |
|
|
@@ -182,6 +198,9 @@ Common runtime variables:
|
|
|
182
198
|
If you omit `OPENCODE_PROVIDER_ID` / `OPENCODE_MODEL_ID`, `opencode serve`
|
|
183
199
|
uses your local OpenCode defaults (for example `~/.config/opencode/opencode.json`).
|
|
184
200
|
|
|
201
|
+
When `OPENCODE_MANAGED_SERVER=true`, `OPENCODE_BASE_URL` is ignored and the
|
|
202
|
+
runtime binds itself to the managed child process instead.
|
|
203
|
+
|
|
185
204
|
For provider-specific auth, model IDs, and config details, use the OpenCode
|
|
186
205
|
official docs and CLI:
|
|
187
206
|
|
|
@@ -202,7 +221,7 @@ Use any supervisor you prefer for long-running operation:
|
|
|
202
221
|
The project no longer ships built-in host bootstrap or process-manager
|
|
203
222
|
wrappers. The official product surface is the runtime entrypoint itself.
|
|
204
223
|
|
|
205
|
-
Minimal `systemd` example:
|
|
224
|
+
Minimal self-managed `systemd` example:
|
|
206
225
|
|
|
207
226
|
1. Create an env file such as `/etc/opencode-a2a/alpha.env`:
|
|
208
227
|
|
|
@@ -211,7 +230,7 @@ A2A_BEARER_TOKEN=replace-me
|
|
|
211
230
|
A2A_HOST=127.0.0.1
|
|
212
231
|
A2A_PORT=8000
|
|
213
232
|
A2A_PUBLIC_URL=https://a2a.example.com
|
|
214
|
-
|
|
233
|
+
OPENCODE_MANAGED_SERVER=true
|
|
215
234
|
OPENCODE_WORKSPACE_ROOT=/srv/my-workspace
|
|
216
235
|
```
|
|
217
236
|
|
|
@@ -237,6 +256,33 @@ WantedBy=multi-user.target
|
|
|
237
256
|
|
|
238
257
|
Replace `ExecStart` with the absolute path returned by `command -v opencode-a2a-server`.
|
|
239
258
|
|
|
259
|
+
Minimal managed-upstream foreground example:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
A2A_BEARER_TOKEN=dev-token \
|
|
263
|
+
A2A_HOST=127.0.0.1 \
|
|
264
|
+
A2A_PORT=8000 \
|
|
265
|
+
A2A_PUBLIC_URL=http://127.0.0.1:8000 \
|
|
266
|
+
OPENCODE_MANAGED_SERVER=true \
|
|
267
|
+
OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
|
|
268
|
+
opencode-a2a-server serve
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Advanced: externally managed upstream
|
|
272
|
+
|
|
273
|
+
Use this mode when you intentionally want `opencode serve` and
|
|
274
|
+
`opencode-a2a-server` to be supervised independently.
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
OPENCODE_BASE_URL=http://127.0.0.1:4096 \
|
|
278
|
+
A2A_BEARER_TOKEN=dev-token \
|
|
279
|
+
A2A_HOST=127.0.0.1 \
|
|
280
|
+
A2A_PORT=8000 \
|
|
281
|
+
A2A_PUBLIC_URL=http://127.0.0.1:8000 \
|
|
282
|
+
OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
|
|
283
|
+
opencode-a2a-server serve
|
|
284
|
+
```
|
|
285
|
+
|
|
240
286
|
Migration notes:
|
|
241
287
|
|
|
242
288
|
- `OPENCODE_DIRECTORY` has been removed. Use `OPENCODE_WORKSPACE_ROOT`.
|
|
@@ -259,6 +305,9 @@ OPENCODE_MODEL_ID=gemini-3.1-pro-preview \
|
|
|
259
305
|
opencode serve
|
|
260
306
|
|
|
261
307
|
A2A_BEARER_TOKEN=dev-token \
|
|
308
|
+
OPENCODE_BASE_URL=http://127.0.0.1:4096 \
|
|
309
|
+
A2A_HOST=127.0.0.1 \
|
|
310
|
+
A2A_PORT=8000 \
|
|
262
311
|
OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
|
|
263
312
|
uv run opencode-a2a-server serve
|
|
264
313
|
```
|
|
@@ -25,8 +25,16 @@ own process manager, container runtime, or host orchestration.
|
|
|
25
25
|
Key variables to understand protocol behavior:
|
|
26
26
|
|
|
27
27
|
- `A2A_BEARER_TOKEN`: required for all authenticated runtime requests.
|
|
28
|
-
- `OPENCODE_BASE_URL`: upstream OpenCode HTTP endpoint
|
|
29
|
-
`http://127.0.0.1:4096`.
|
|
28
|
+
- `OPENCODE_BASE_URL`: upstream OpenCode HTTP endpoint for externally managed
|
|
29
|
+
upstream mode. Default: `http://127.0.0.1:4096`.
|
|
30
|
+
- `OPENCODE_MANAGED_SERVER`: when enabled, the service starts a local
|
|
31
|
+
`opencode serve`, captures its actual listening URL, and uses that as the
|
|
32
|
+
internal upstream endpoint.
|
|
33
|
+
- `OPENCODE_MANAGED_SERVER_HOST` / `OPENCODE_MANAGED_SERVER_PORT`: bind address
|
|
34
|
+
for managed upstream mode. If the port is omitted, the service picks a free
|
|
35
|
+
localhost port before starting the child process.
|
|
36
|
+
- `OPENCODE_COMMAND`: OpenCode CLI executable used for managed upstream mode.
|
|
37
|
+
- `OPENCODE_STARTUP_TIMEOUT`: startup timeout for managed upstream mode.
|
|
30
38
|
- `OPENCODE_WORKSPACE_ROOT`: service-level default workspace root exposed to
|
|
31
39
|
OpenCode when clients do not request a narrower directory override.
|
|
32
40
|
- `OPENCODE_PROVIDER_ID` / `OPENCODE_MODEL_ID`: default upstream model
|
|
@@ -54,14 +62,26 @@ Key variables to understand protocol behavior:
|
|
|
54
62
|
optional stream timeout override.
|
|
55
63
|
- Runtime authentication is bearer-token only via `A2A_BEARER_TOKEN`.
|
|
56
64
|
|
|
57
|
-
Minimal
|
|
65
|
+
Minimal managed-upstream example:
|
|
58
66
|
|
|
59
67
|
```bash
|
|
60
68
|
A2A_BEARER_TOKEN=dev-token \
|
|
61
69
|
A2A_HOST=127.0.0.1 \
|
|
62
70
|
A2A_PORT=8000 \
|
|
63
71
|
A2A_PUBLIC_URL=http://127.0.0.1:8000 \
|
|
72
|
+
OPENCODE_MANAGED_SERVER=true \
|
|
73
|
+
OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
|
|
74
|
+
opencode-a2a-server serve
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Advanced externally managed upstream example:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
64
80
|
OPENCODE_BASE_URL=http://127.0.0.1:4096 \
|
|
81
|
+
A2A_BEARER_TOKEN=dev-token \
|
|
82
|
+
A2A_HOST=127.0.0.1 \
|
|
83
|
+
A2A_PORT=8000 \
|
|
84
|
+
A2A_PUBLIC_URL=http://127.0.0.1:8000 \
|
|
65
85
|
OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
|
|
66
86
|
opencode-a2a-server serve
|
|
67
87
|
```
|
|
@@ -69,6 +69,7 @@ from .jsonrpc_ext import (
|
|
|
69
69
|
OpencodeSessionQueryJSONRPCApplication,
|
|
70
70
|
)
|
|
71
71
|
from .opencode_client import OpencodeClient
|
|
72
|
+
from .opencode_upstream import ManagedOpencodeServer, launch_managed_opencode_server
|
|
72
73
|
|
|
73
74
|
logger = logging.getLogger(__name__)
|
|
74
75
|
|
|
@@ -993,8 +994,17 @@ def create_app(settings: Settings) -> FastAPI:
|
|
|
993
994
|
|
|
994
995
|
@asynccontextmanager
|
|
995
996
|
async def lifespan(_app: FastAPI):
|
|
996
|
-
|
|
997
|
-
|
|
997
|
+
managed_upstream: ManagedOpencodeServer | None = None
|
|
998
|
+
try:
|
|
999
|
+
if settings.opencode_managed_server:
|
|
1000
|
+
managed_upstream = await launch_managed_opencode_server(settings)
|
|
1001
|
+
await client.rebind_base_url(managed_upstream.base_url)
|
|
1002
|
+
_app.state.managed_opencode_upstream = managed_upstream
|
|
1003
|
+
yield
|
|
1004
|
+
finally:
|
|
1005
|
+
if managed_upstream is not None:
|
|
1006
|
+
await managed_upstream.close()
|
|
1007
|
+
await client.close()
|
|
998
1008
|
|
|
999
1009
|
agent_card = build_agent_card(settings)
|
|
1000
1010
|
context_builder = IdentityAwareCallContextBuilder()
|
|
@@ -19,6 +19,23 @@ class Settings(BaseSettings):
|
|
|
19
19
|
|
|
20
20
|
# OpenCode settings
|
|
21
21
|
opencode_base_url: str = Field(default="http://127.0.0.1:4096", alias="OPENCODE_BASE_URL")
|
|
22
|
+
opencode_managed_server: bool = Field(default=False, alias="OPENCODE_MANAGED_SERVER")
|
|
23
|
+
opencode_managed_server_host: str = Field(
|
|
24
|
+
default="127.0.0.1",
|
|
25
|
+
alias="OPENCODE_MANAGED_SERVER_HOST",
|
|
26
|
+
)
|
|
27
|
+
opencode_managed_server_port: int | None = Field(
|
|
28
|
+
default=None,
|
|
29
|
+
ge=1,
|
|
30
|
+
le=65535,
|
|
31
|
+
alias="OPENCODE_MANAGED_SERVER_PORT",
|
|
32
|
+
)
|
|
33
|
+
opencode_command: str = Field(default="opencode", alias="OPENCODE_COMMAND")
|
|
34
|
+
opencode_startup_timeout: float = Field(
|
|
35
|
+
default=20.0,
|
|
36
|
+
gt=0.0,
|
|
37
|
+
alias="OPENCODE_STARTUP_TIMEOUT",
|
|
38
|
+
)
|
|
22
39
|
opencode_workspace_root: str | None = Field(default=None, alias="OPENCODE_WORKSPACE_ROOT")
|
|
23
40
|
opencode_provider_id: str | None = Field(default=None, alias="OPENCODE_PROVIDER_ID")
|
|
24
41
|
opencode_model_id: str | None = Field(default=None, alias="OPENCODE_MODEL_ID")
|
{opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/opencode_client.py
RENAMED
|
@@ -54,15 +54,27 @@ class OpencodeClient:
|
|
|
54
54
|
self._interrupt_request_ttl_seconds = 600.0
|
|
55
55
|
self._interrupt_request_clock = time.monotonic
|
|
56
56
|
self._interrupt_requests: dict[str, InterruptRequestBinding] = {}
|
|
57
|
-
self._client =
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
self._client = self._build_http_client(self._base_url)
|
|
58
|
+
|
|
59
|
+
def _build_http_client(self, base_url: str) -> httpx.AsyncClient:
|
|
60
|
+
return httpx.AsyncClient(
|
|
61
|
+
base_url=base_url,
|
|
62
|
+
timeout=self._settings.opencode_timeout,
|
|
60
63
|
headers={"Accept": "application/json"},
|
|
61
64
|
)
|
|
62
65
|
|
|
63
66
|
async def close(self) -> None:
|
|
64
67
|
await self._client.aclose()
|
|
65
68
|
|
|
69
|
+
async def rebind_base_url(self, base_url: str) -> None:
|
|
70
|
+
normalized = base_url.rstrip("/")
|
|
71
|
+
if normalized == self._base_url:
|
|
72
|
+
return
|
|
73
|
+
previous_client = self._client
|
|
74
|
+
self._base_url = normalized
|
|
75
|
+
self._client = self._build_http_client(self._base_url)
|
|
76
|
+
await previous_client.aclose()
|
|
77
|
+
|
|
66
78
|
@staticmethod
|
|
67
79
|
def _response_body_preview(response: httpx.Response, *, limit: int = 200) -> str:
|
|
68
80
|
body = response.text.strip()
|
|
@@ -177,6 +189,10 @@ class OpencodeClient:
|
|
|
177
189
|
def settings(self) -> Settings:
|
|
178
190
|
return self._settings
|
|
179
191
|
|
|
192
|
+
@property
|
|
193
|
+
def base_url(self) -> str:
|
|
194
|
+
return self._base_url
|
|
195
|
+
|
|
180
196
|
def _query_params(self, directory: str | None = None) -> dict[str, str]:
|
|
181
197
|
d = directory or self._directory
|
|
182
198
|
if not d:
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
import socket
|
|
8
|
+
from collections import deque
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
from .config import Settings
|
|
12
|
+
|
|
13
|
+
_LISTENING_PATTERN = re.compile(r"opencode server listening on (?P<url>https?://\S+)")
|
|
14
|
+
_OUTPUT_BUFFER_LIMIT = 40
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _pick_managed_server_port(host: str) -> int:
|
|
18
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
19
|
+
sock.bind((host, 0))
|
|
20
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
21
|
+
return int(sock.getsockname()[1])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _resolve_opencode_command(command: str) -> str:
|
|
25
|
+
if os.path.sep in command:
|
|
26
|
+
if os.path.exists(command):
|
|
27
|
+
return command
|
|
28
|
+
raise RuntimeError(f"Managed upstream command not found: {command}")
|
|
29
|
+
resolved = shutil.which(command)
|
|
30
|
+
if resolved:
|
|
31
|
+
return resolved
|
|
32
|
+
raise RuntimeError(
|
|
33
|
+
f"Managed upstream command not found on PATH: {command}. "
|
|
34
|
+
"Install opencode or set OPENCODE_COMMAND."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class ManagedOpencodeServer:
|
|
40
|
+
process: asyncio.subprocess.Process
|
|
41
|
+
base_url: str
|
|
42
|
+
_output_task: asyncio.Task[None]
|
|
43
|
+
|
|
44
|
+
async def close(self) -> None:
|
|
45
|
+
if self.process.returncode is None:
|
|
46
|
+
self.process.terminate()
|
|
47
|
+
try:
|
|
48
|
+
await asyncio.wait_for(self.process.wait(), timeout=5.0)
|
|
49
|
+
except TimeoutError:
|
|
50
|
+
self.process.kill()
|
|
51
|
+
await self.process.wait()
|
|
52
|
+
await self._output_task
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def _consume_process_output(
|
|
56
|
+
stream: asyncio.StreamReader | None,
|
|
57
|
+
*,
|
|
58
|
+
ready_future: asyncio.Future[str],
|
|
59
|
+
output_buffer: deque[str],
|
|
60
|
+
) -> None:
|
|
61
|
+
if stream is None:
|
|
62
|
+
return
|
|
63
|
+
while True:
|
|
64
|
+
line = await stream.readline()
|
|
65
|
+
if not line:
|
|
66
|
+
return
|
|
67
|
+
text = line.decode("utf-8", errors="replace").rstrip()
|
|
68
|
+
if not text:
|
|
69
|
+
continue
|
|
70
|
+
output_buffer.append(text)
|
|
71
|
+
match = _LISTENING_PATTERN.search(text)
|
|
72
|
+
if match and not ready_future.done():
|
|
73
|
+
ready_future.set_result(match.group("url"))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def launch_managed_opencode_server(settings: Settings) -> ManagedOpencodeServer:
|
|
77
|
+
host = settings.opencode_managed_server_host
|
|
78
|
+
port = settings.opencode_managed_server_port or _pick_managed_server_port(host)
|
|
79
|
+
command = _resolve_opencode_command(settings.opencode_command)
|
|
80
|
+
cmd = [
|
|
81
|
+
command,
|
|
82
|
+
"serve",
|
|
83
|
+
"--hostname",
|
|
84
|
+
host,
|
|
85
|
+
"--port",
|
|
86
|
+
str(port),
|
|
87
|
+
]
|
|
88
|
+
process = await asyncio.create_subprocess_exec(
|
|
89
|
+
*cmd,
|
|
90
|
+
stdout=asyncio.subprocess.PIPE,
|
|
91
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
92
|
+
)
|
|
93
|
+
ready_future: asyncio.Future[str] = asyncio.get_running_loop().create_future()
|
|
94
|
+
output_buffer: deque[str] = deque(maxlen=_OUTPUT_BUFFER_LIMIT)
|
|
95
|
+
output_task = asyncio.create_task(
|
|
96
|
+
_consume_process_output(
|
|
97
|
+
process.stdout,
|
|
98
|
+
ready_future=ready_future,
|
|
99
|
+
output_buffer=output_buffer,
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
try:
|
|
103
|
+
base_url = await asyncio.wait_for(ready_future, timeout=settings.opencode_startup_timeout)
|
|
104
|
+
except Exception as exc:
|
|
105
|
+
if process.returncode is None:
|
|
106
|
+
process.terminate()
|
|
107
|
+
try:
|
|
108
|
+
await asyncio.wait_for(process.wait(), timeout=5.0)
|
|
109
|
+
except TimeoutError:
|
|
110
|
+
process.kill()
|
|
111
|
+
await process.wait()
|
|
112
|
+
await output_task
|
|
113
|
+
buffered_output = "\n".join(output_buffer).strip() or "<no output>"
|
|
114
|
+
raise RuntimeError(
|
|
115
|
+
"Managed OpenCode upstream failed to become ready. "
|
|
116
|
+
f"command={' '.join(cmd)} output={buffered_output}"
|
|
117
|
+
) from exc
|
|
118
|
+
return ManagedOpencodeServer(
|
|
119
|
+
process=process,
|
|
120
|
+
base_url=base_url.rstrip("/"),
|
|
121
|
+
_output_task=output_task,
|
|
122
|
+
)
|
{opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3/src/opencode_a2a_server.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opencode-a2a-server
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: A2A wrapper service for opencode
|
|
5
5
|
Author: liujuanjuan1984@Intelligent-Internet
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -186,7 +186,10 @@ OPENCODE_MODEL_ID=gemini-3.1-pro-preview \
|
|
|
186
186
|
opencode serve
|
|
187
187
|
|
|
188
188
|
A2A_BEARER_TOKEN=prod-token \
|
|
189
|
+
A2A_HOST=127.0.0.1 \
|
|
190
|
+
A2A_PORT=8000 \
|
|
189
191
|
A2A_PUBLIC_URL=http://127.0.0.1:8000 \
|
|
192
|
+
OPENCODE_MANAGED_SERVER=true \
|
|
190
193
|
OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
|
|
191
194
|
opencode-a2a-server serve
|
|
192
195
|
```
|
|
@@ -196,12 +199,25 @@ exposes to OpenCode.
|
|
|
196
199
|
|
|
197
200
|
Default address: `http://127.0.0.1:8000`
|
|
198
201
|
|
|
202
|
+
OpenCode upstream modes:
|
|
203
|
+
|
|
204
|
+
- Managed upstream: set `OPENCODE_MANAGED_SERVER=true` and
|
|
205
|
+
`opencode-a2a-server` will start a local `opencode serve`, capture its actual
|
|
206
|
+
listening URL, and stop it on shutdown.
|
|
207
|
+
- External upstream: you start and manage `opencode serve` yourself, then point
|
|
208
|
+
`OPENCODE_BASE_URL` at that HTTP endpoint.
|
|
209
|
+
|
|
199
210
|
Common runtime variables:
|
|
200
211
|
|
|
201
212
|
| Variable | Required | Default | Purpose |
|
|
202
213
|
| --- | --- | --- | --- |
|
|
203
214
|
| `A2A_BEARER_TOKEN` | Yes | None | Bearer token required for authenticated runtime requests. |
|
|
204
|
-
| `OPENCODE_BASE_URL` | No | `http://127.0.0.1:4096` | Upstream OpenCode HTTP endpoint. |
|
|
215
|
+
| `OPENCODE_BASE_URL` | No | `http://127.0.0.1:4096` | Upstream OpenCode HTTP endpoint for externally managed upstream mode. |
|
|
216
|
+
| `OPENCODE_MANAGED_SERVER` | No | `false` | Start and manage a local `opencode serve` child process. |
|
|
217
|
+
| `OPENCODE_MANAGED_SERVER_HOST` | No | `127.0.0.1` | Bind host used when managed upstream mode is enabled. |
|
|
218
|
+
| `OPENCODE_MANAGED_SERVER_PORT` | No | auto-pick | Bind port used when managed upstream mode is enabled. |
|
|
219
|
+
| `OPENCODE_COMMAND` | No | `opencode` | OpenCode CLI executable used for managed upstream mode. |
|
|
220
|
+
| `OPENCODE_STARTUP_TIMEOUT` | No | `20` | Seconds to wait for managed upstream startup. |
|
|
205
221
|
| `OPENCODE_WORKSPACE_ROOT` | No | None | Default workspace root exposed to OpenCode. |
|
|
206
222
|
| `OPENCODE_PROVIDER_ID` | No | None | Default provider for the upstream runtime. |
|
|
207
223
|
| `OPENCODE_MODEL_ID` | No | None | Default model for the upstream runtime. Set together with `OPENCODE_PROVIDER_ID`. |
|
|
@@ -220,6 +236,9 @@ Common runtime variables:
|
|
|
220
236
|
If you omit `OPENCODE_PROVIDER_ID` / `OPENCODE_MODEL_ID`, `opencode serve`
|
|
221
237
|
uses your local OpenCode defaults (for example `~/.config/opencode/opencode.json`).
|
|
222
238
|
|
|
239
|
+
When `OPENCODE_MANAGED_SERVER=true`, `OPENCODE_BASE_URL` is ignored and the
|
|
240
|
+
runtime binds itself to the managed child process instead.
|
|
241
|
+
|
|
223
242
|
For provider-specific auth, model IDs, and config details, use the OpenCode
|
|
224
243
|
official docs and CLI:
|
|
225
244
|
|
|
@@ -240,7 +259,7 @@ Use any supervisor you prefer for long-running operation:
|
|
|
240
259
|
The project no longer ships built-in host bootstrap or process-manager
|
|
241
260
|
wrappers. The official product surface is the runtime entrypoint itself.
|
|
242
261
|
|
|
243
|
-
Minimal `systemd` example:
|
|
262
|
+
Minimal self-managed `systemd` example:
|
|
244
263
|
|
|
245
264
|
1. Create an env file such as `/etc/opencode-a2a/alpha.env`:
|
|
246
265
|
|
|
@@ -249,7 +268,7 @@ A2A_BEARER_TOKEN=replace-me
|
|
|
249
268
|
A2A_HOST=127.0.0.1
|
|
250
269
|
A2A_PORT=8000
|
|
251
270
|
A2A_PUBLIC_URL=https://a2a.example.com
|
|
252
|
-
|
|
271
|
+
OPENCODE_MANAGED_SERVER=true
|
|
253
272
|
OPENCODE_WORKSPACE_ROOT=/srv/my-workspace
|
|
254
273
|
```
|
|
255
274
|
|
|
@@ -275,6 +294,33 @@ WantedBy=multi-user.target
|
|
|
275
294
|
|
|
276
295
|
Replace `ExecStart` with the absolute path returned by `command -v opencode-a2a-server`.
|
|
277
296
|
|
|
297
|
+
Minimal managed-upstream foreground example:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
A2A_BEARER_TOKEN=dev-token \
|
|
301
|
+
A2A_HOST=127.0.0.1 \
|
|
302
|
+
A2A_PORT=8000 \
|
|
303
|
+
A2A_PUBLIC_URL=http://127.0.0.1:8000 \
|
|
304
|
+
OPENCODE_MANAGED_SERVER=true \
|
|
305
|
+
OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
|
|
306
|
+
opencode-a2a-server serve
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Advanced: externally managed upstream
|
|
310
|
+
|
|
311
|
+
Use this mode when you intentionally want `opencode serve` and
|
|
312
|
+
`opencode-a2a-server` to be supervised independently.
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
OPENCODE_BASE_URL=http://127.0.0.1:4096 \
|
|
316
|
+
A2A_BEARER_TOKEN=dev-token \
|
|
317
|
+
A2A_HOST=127.0.0.1 \
|
|
318
|
+
A2A_PORT=8000 \
|
|
319
|
+
A2A_PUBLIC_URL=http://127.0.0.1:8000 \
|
|
320
|
+
OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
|
|
321
|
+
opencode-a2a-server serve
|
|
322
|
+
```
|
|
323
|
+
|
|
278
324
|
Migration notes:
|
|
279
325
|
|
|
280
326
|
- `OPENCODE_DIRECTORY` has been removed. Use `OPENCODE_WORKSPACE_ROOT`.
|
|
@@ -297,6 +343,9 @@ OPENCODE_MODEL_ID=gemini-3.1-pro-preview \
|
|
|
297
343
|
opencode serve
|
|
298
344
|
|
|
299
345
|
A2A_BEARER_TOKEN=dev-token \
|
|
346
|
+
OPENCODE_BASE_URL=http://127.0.0.1:4096 \
|
|
347
|
+
A2A_HOST=127.0.0.1 \
|
|
348
|
+
A2A_PORT=8000 \
|
|
300
349
|
OPENCODE_WORKSPACE_ROOT=/abs/path/to/workspace \
|
|
301
350
|
uv run opencode-a2a-server serve
|
|
302
351
|
```
|
{opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server.egg-info/SOURCES.txt
RENAMED
|
@@ -27,6 +27,7 @@ src/opencode_a2a_server/extension_contracts.py
|
|
|
27
27
|
src/opencode_a2a_server/jsonrpc_ext.py
|
|
28
28
|
src/opencode_a2a_server/jsonrpc_models.py
|
|
29
29
|
src/opencode_a2a_server/opencode_client.py
|
|
30
|
+
src/opencode_a2a_server/opencode_upstream.py
|
|
30
31
|
src/opencode_a2a_server/parts_mapper.py
|
|
31
32
|
src/opencode_a2a_server/text_parts.py
|
|
32
33
|
src/opencode_a2a_server.egg-info/PKG-INFO
|
|
@@ -52,6 +53,7 @@ tests/test_multipart_input.py
|
|
|
52
53
|
tests/test_opencode_agent_session_binding.py
|
|
53
54
|
tests/test_opencode_client_params.py
|
|
54
55
|
tests/test_opencode_session_extension.py
|
|
56
|
+
tests/test_opencode_upstream.py
|
|
55
57
|
tests/test_script_health_contract.py
|
|
56
58
|
tests/test_session_ownership.py
|
|
57
59
|
tests/test_settings.py
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from fastapi.testclient import TestClient
|
|
7
|
+
|
|
8
|
+
from opencode_a2a_server.app import create_app
|
|
9
|
+
from opencode_a2a_server.opencode_upstream import (
|
|
10
|
+
ManagedOpencodeServer,
|
|
11
|
+
_resolve_opencode_command,
|
|
12
|
+
launch_managed_opencode_server,
|
|
13
|
+
)
|
|
14
|
+
from tests.helpers import make_settings
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _DummyProcess:
|
|
18
|
+
def __init__(self, lines: list[str], *, returncode: int | None = None) -> None:
|
|
19
|
+
self.stdout = asyncio.StreamReader()
|
|
20
|
+
for line in lines:
|
|
21
|
+
self.stdout.feed_data(line.encode("utf-8"))
|
|
22
|
+
self.stdout.feed_eof()
|
|
23
|
+
self.returncode = returncode
|
|
24
|
+
self.terminated = False
|
|
25
|
+
self.killed = False
|
|
26
|
+
|
|
27
|
+
def terminate(self) -> None:
|
|
28
|
+
self.terminated = True
|
|
29
|
+
self.returncode = 0
|
|
30
|
+
|
|
31
|
+
def kill(self) -> None:
|
|
32
|
+
self.killed = True
|
|
33
|
+
self.returncode = -9
|
|
34
|
+
|
|
35
|
+
async def wait(self) -> int:
|
|
36
|
+
if self.returncode is None:
|
|
37
|
+
self.returncode = 0
|
|
38
|
+
return self.returncode
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_resolve_opencode_command_errors_when_not_found(monkeypatch) -> None:
|
|
42
|
+
monkeypatch.setattr("opencode_a2a_server.opencode_upstream.shutil.which", lambda _: None)
|
|
43
|
+
|
|
44
|
+
with pytest.raises(RuntimeError) as excinfo:
|
|
45
|
+
_resolve_opencode_command("opencode")
|
|
46
|
+
|
|
47
|
+
assert "OPENCODE_COMMAND" in str(excinfo.value)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.asyncio
|
|
51
|
+
async def test_launch_managed_opencode_server_uses_listening_url(monkeypatch) -> None:
|
|
52
|
+
dummy = _DummyProcess(
|
|
53
|
+
[
|
|
54
|
+
"Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.\n",
|
|
55
|
+
"opencode server listening on http://127.0.0.1:40419\n",
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
captured: list[str] = []
|
|
59
|
+
|
|
60
|
+
async def _fake_create_subprocess_exec(*cmd, **kwargs): # noqa: ANN001
|
|
61
|
+
del kwargs
|
|
62
|
+
captured.extend(cmd)
|
|
63
|
+
return dummy
|
|
64
|
+
|
|
65
|
+
monkeypatch.setattr(
|
|
66
|
+
"opencode_a2a_server.opencode_upstream._resolve_opencode_command",
|
|
67
|
+
lambda command: command,
|
|
68
|
+
)
|
|
69
|
+
monkeypatch.setattr(
|
|
70
|
+
"opencode_a2a_server.opencode_upstream._pick_managed_server_port",
|
|
71
|
+
lambda host: 40419,
|
|
72
|
+
)
|
|
73
|
+
monkeypatch.setattr(
|
|
74
|
+
"opencode_a2a_server.opencode_upstream.asyncio.create_subprocess_exec",
|
|
75
|
+
_fake_create_subprocess_exec,
|
|
76
|
+
)
|
|
77
|
+
settings = make_settings(
|
|
78
|
+
opencode_managed_server=True,
|
|
79
|
+
opencode_command="opencode",
|
|
80
|
+
opencode_startup_timeout=1.0,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
handle = await launch_managed_opencode_server(settings)
|
|
84
|
+
|
|
85
|
+
assert captured == ["opencode", "serve", "--hostname", "127.0.0.1", "--port", "40419"]
|
|
86
|
+
assert handle.base_url == "http://127.0.0.1:40419"
|
|
87
|
+
await handle.close()
|
|
88
|
+
assert dummy.terminated is True
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@pytest.mark.asyncio
|
|
92
|
+
async def test_launch_managed_opencode_server_surfaces_startup_failure(monkeypatch) -> None:
|
|
93
|
+
dummy = _DummyProcess(["fatal: boom\n"], returncode=1)
|
|
94
|
+
|
|
95
|
+
async def _fake_create_subprocess_exec(*_cmd, **_kwargs): # noqa: ANN001
|
|
96
|
+
return dummy
|
|
97
|
+
|
|
98
|
+
monkeypatch.setattr(
|
|
99
|
+
"opencode_a2a_server.opencode_upstream._resolve_opencode_command",
|
|
100
|
+
lambda command: command,
|
|
101
|
+
)
|
|
102
|
+
monkeypatch.setattr(
|
|
103
|
+
"opencode_a2a_server.opencode_upstream._pick_managed_server_port",
|
|
104
|
+
lambda host: 40420,
|
|
105
|
+
)
|
|
106
|
+
monkeypatch.setattr(
|
|
107
|
+
"opencode_a2a_server.opencode_upstream.asyncio.create_subprocess_exec",
|
|
108
|
+
_fake_create_subprocess_exec,
|
|
109
|
+
)
|
|
110
|
+
settings = make_settings(
|
|
111
|
+
opencode_managed_server=True,
|
|
112
|
+
opencode_command="opencode",
|
|
113
|
+
opencode_startup_timeout=0.01,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
with pytest.raises(RuntimeError) as excinfo:
|
|
117
|
+
await launch_managed_opencode_server(settings)
|
|
118
|
+
|
|
119
|
+
assert "failed to become ready" in str(excinfo.value)
|
|
120
|
+
assert "fatal: boom" in str(excinfo.value)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_create_app_manages_upstream_lifecycle(monkeypatch) -> None:
|
|
124
|
+
import opencode_a2a_server.app as app_module
|
|
125
|
+
|
|
126
|
+
calls: list[str] = []
|
|
127
|
+
|
|
128
|
+
class _DummyHandle(ManagedOpencodeServer):
|
|
129
|
+
def __init__(self) -> None:
|
|
130
|
+
self.process = _DummyProcess([])
|
|
131
|
+
self.base_url = "http://127.0.0.1:40421"
|
|
132
|
+
self._output_task = asyncio.get_event_loop().create_task(asyncio.sleep(0))
|
|
133
|
+
|
|
134
|
+
async def close(self) -> None: # type: ignore[override]
|
|
135
|
+
calls.append("managed_close")
|
|
136
|
+
|
|
137
|
+
async def _fake_launch(settings): # noqa: ANN001
|
|
138
|
+
assert settings.opencode_managed_server is True
|
|
139
|
+
calls.append("launch")
|
|
140
|
+
return _DummyHandle()
|
|
141
|
+
|
|
142
|
+
async def _fake_rebind(self, base_url: str) -> None: # noqa: ANN001
|
|
143
|
+
calls.append(f"rebind:{base_url}")
|
|
144
|
+
|
|
145
|
+
async def _fake_close(self) -> None: # noqa: ANN001
|
|
146
|
+
calls.append("client_close")
|
|
147
|
+
|
|
148
|
+
monkeypatch.setattr(app_module, "launch_managed_opencode_server", _fake_launch)
|
|
149
|
+
monkeypatch.setattr(app_module.OpencodeClient, "rebind_base_url", _fake_rebind)
|
|
150
|
+
monkeypatch.setattr(app_module.OpencodeClient, "close", _fake_close)
|
|
151
|
+
|
|
152
|
+
app = create_app(
|
|
153
|
+
make_settings(
|
|
154
|
+
a2a_bearer_token="test-token",
|
|
155
|
+
opencode_managed_server=True,
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
with TestClient(app):
|
|
160
|
+
assert calls[:2] == ["launch", "rebind:http://127.0.0.1:40421"]
|
|
161
|
+
|
|
162
|
+
assert calls == ["launch", "rebind:http://127.0.0.1:40421", "managed_close", "client_close"]
|
|
@@ -22,6 +22,11 @@ def test_settings_valid():
|
|
|
22
22
|
env = {
|
|
23
23
|
"A2A_BEARER_TOKEN": "test-token",
|
|
24
24
|
"OPENCODE_TIMEOUT": "300",
|
|
25
|
+
"OPENCODE_MANAGED_SERVER": "true",
|
|
26
|
+
"OPENCODE_MANAGED_SERVER_HOST": "127.0.0.1",
|
|
27
|
+
"OPENCODE_MANAGED_SERVER_PORT": "42111",
|
|
28
|
+
"OPENCODE_COMMAND": "/usr/local/bin/opencode",
|
|
29
|
+
"OPENCODE_STARTUP_TIMEOUT": "9.5",
|
|
25
30
|
"OPENCODE_WORKSPACE_ROOT": "/srv/workspaces/alpha",
|
|
26
31
|
"A2A_MAX_REQUEST_BODY_BYTES": "2048",
|
|
27
32
|
"A2A_CANCEL_ABORT_TIMEOUT_SECONDS": "0.75",
|
|
@@ -31,6 +36,11 @@ def test_settings_valid():
|
|
|
31
36
|
settings = Settings.from_env()
|
|
32
37
|
assert settings.a2a_bearer_token == "test-token"
|
|
33
38
|
assert settings.opencode_timeout == 300.0
|
|
39
|
+
assert settings.opencode_managed_server is True
|
|
40
|
+
assert settings.opencode_managed_server_host == "127.0.0.1"
|
|
41
|
+
assert settings.opencode_managed_server_port == 42111
|
|
42
|
+
assert settings.opencode_command == "/usr/local/bin/opencode"
|
|
43
|
+
assert settings.opencode_startup_timeout == 9.5
|
|
34
44
|
assert settings.opencode_workspace_root == "/srv/workspaces/alpha"
|
|
35
45
|
assert settings.a2a_max_request_body_bytes == 2048
|
|
36
46
|
assert settings.a2a_cancel_abort_timeout_seconds == 0.75
|
|
File without changes
|
{opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/.github/workflows/dependency-health.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/jsonrpc_ext.py
RENAMED
|
File without changes
|
{opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/jsonrpc_models.py
RENAMED
|
File without changes
|
{opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/parts_mapper.py
RENAMED
|
File without changes
|
{opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/src/opencode_a2a_server/text_parts.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_extension_contract_consistency.py
RENAMED
|
File without changes
|
|
File without changes
|
{opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_jsonrpc_unsupported_method.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_opencode_agent_session_binding.py
RENAMED
|
File without changes
|
{opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_opencode_client_params.py
RENAMED
|
File without changes
|
{opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_opencode_session_extension.py
RENAMED
|
File without changes
|
{opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_script_health_contract.py
RENAMED
|
File without changes
|
|
File without changes
|
{opencode_a2a_server-0.2.2 → opencode_a2a_server-0.2.3}/tests/test_streaming_output_contract.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|