optio-opencode 0.1.0__tar.gz → 0.1.2__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.
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/PKG-INFO +1 -1
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/pyproject.toml +3 -1
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode/host_actions.py +8 -1
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode/session.py +22 -3
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode.egg-info/PKG-INFO +1 -1
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_host_local.py +44 -0
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_session_hooks.py +1 -1
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_session_local.py +2 -1
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_session_resume.py +2 -1
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/README.md +0 -0
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/setup.cfg +0 -0
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode/__init__.py +0 -0
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode/prompt.py +0 -0
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode/snapshots.py +0 -0
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode/types.py +0 -0
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode.egg-info/SOURCES.txt +0 -0
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode.egg-info/dependency_links.txt +0 -0
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode.egg-info/requires.txt +0 -0
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode.egg-info/top_level.txt +0 -0
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_host_primitives_local.py +0 -0
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_host_primitives_remote.py +0 -0
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_host_remote_resume.py +0 -0
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_host_resume.py +0 -0
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_prompt.py +0 -0
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_sanity.py +0 -0
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_session_blob_hooks.py +0 -0
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_session_remote.py +0 -0
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_smart_install.py +0 -0
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_snapshots.py +0 -0
- {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_types.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "optio-opencode"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Run opencode web as an optio task; local subprocess or remote via SSH."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -27,6 +27,8 @@ classifiers = [
|
|
|
27
27
|
]
|
|
28
28
|
dependencies = [
|
|
29
29
|
"optio-core>=0.1,<0.2",
|
|
30
|
+
# 0.1.1 introduces the bind_addr kwarg on Host.establish_tunnel —
|
|
31
|
+
# required for OPTIO_WIDGET_TUNNEL_BIND to work.
|
|
30
32
|
"optio-host>=0.1,<0.2",
|
|
31
33
|
"asyncssh>=2.14",
|
|
32
34
|
]
|
|
@@ -328,6 +328,7 @@ async def launch_opencode(
|
|
|
328
328
|
*,
|
|
329
329
|
ready_timeout_s: float = 30.0,
|
|
330
330
|
opencode_executable: str = "opencode",
|
|
331
|
+
hostname: str = "127.0.0.1",
|
|
331
332
|
) -> tuple[ProcessHandle, int]:
|
|
332
333
|
"""Launch ``opencode web`` on ``host``; wait for the listening URL.
|
|
333
334
|
|
|
@@ -340,6 +341,12 @@ async def launch_opencode(
|
|
|
340
341
|
directory to PATH so opencode's automatic browser-launch is
|
|
341
342
|
suppressed.
|
|
342
343
|
|
|
344
|
+
``hostname`` is passed to ``opencode web --hostname=`` so callers
|
|
345
|
+
can bind to a non-loopback interface when consumers reach the server
|
|
346
|
+
across a network boundary (e.g. LocalHost inside a docker container
|
|
347
|
+
serving a sibling API-proxy container). Defaults to ``127.0.0.1`` to
|
|
348
|
+
keep RemoteHost-over-SSH and single-host deployments unchanged.
|
|
349
|
+
|
|
343
350
|
Returns ``(handle, opencode_port)``. Caller is responsible for
|
|
344
351
|
eventually terminating the handle via ``host.terminate_subprocess``.
|
|
345
352
|
"""
|
|
@@ -374,7 +381,7 @@ async def launch_opencode(
|
|
|
374
381
|
f"exec env "
|
|
375
382
|
f"OPENCODE_SERVER_PASSWORD=\"$(cat {shlex.quote(host.workdir + '/' + pw_file)})\" "
|
|
376
383
|
f"BROWSER=true "
|
|
377
|
-
f"{opencode_executable} web --port=0 --hostname=
|
|
384
|
+
f"{opencode_executable} web --port=0 --hostname={shlex.quote(hostname)}"
|
|
378
385
|
)
|
|
379
386
|
|
|
380
387
|
# Prepend the noop-browsers bin dir to PATH via env on launch_subprocess.
|
|
@@ -210,16 +210,35 @@ async def run_opencode_session(ctx: ProcessContext, config: OpencodeTaskConfig)
|
|
|
210
210
|
host, opencode_executable=opencode_exec,
|
|
211
211
|
)
|
|
212
212
|
version_suffix = f" {version}" if version else ""
|
|
213
|
+
# --- tunnel + widget registration --------------------------------
|
|
214
|
+
# By default the SSH tunnel listens on 127.0.0.1 — only the worker
|
|
215
|
+
# process (this engine) can reach it. For multi-container deploys
|
|
216
|
+
# where the API proxy lives in a different container than the
|
|
217
|
+
# engine but on the same Docker network, set
|
|
218
|
+
# OPTIO_WIDGET_TUNNEL_BIND=0.0.0.0 so sibling containers can
|
|
219
|
+
# connect to the engine's port, and OPTIO_WIDGET_TUNNEL_HOST to
|
|
220
|
+
# the Docker DNS name other containers resolve to reach this
|
|
221
|
+
# engine (e.g. the compose service name). Both default to
|
|
222
|
+
# 127.0.0.1 so single-host deploys are unchanged.
|
|
223
|
+
bind_addr = os.environ.get("OPTIO_WIDGET_TUNNEL_BIND", "127.0.0.1")
|
|
224
|
+
upstream_host = os.environ.get("OPTIO_WIDGET_TUNNEL_HOST", "127.0.0.1")
|
|
225
|
+
|
|
226
|
+
# LocalHost has no SSH tunnel — establish_tunnel is a no-op — so
|
|
227
|
+
# opencode itself must bind to ``bind_addr`` for sibling containers
|
|
228
|
+
# to reach it. RemoteHost keeps opencode bound to the remote's
|
|
229
|
+
# loopback; the SSH tunnel on the engine side handles exposure.
|
|
230
|
+
opencode_hostname = bind_addr if isinstance(host, LocalHost) else "127.0.0.1"
|
|
231
|
+
|
|
213
232
|
ctx.report_progress(None, f"Launching opencode{version_suffix}…")
|
|
214
233
|
handle, opencode_port = await host_actions.launch_opencode(
|
|
215
234
|
host, password,
|
|
216
235
|
ready_timeout_s=READY_TIMEOUT_S,
|
|
217
236
|
opencode_executable=opencode_exec,
|
|
237
|
+
hostname=opencode_hostname,
|
|
218
238
|
)
|
|
219
239
|
launched_handle = handle
|
|
220
240
|
|
|
221
|
-
|
|
222
|
-
worker_port = await host.establish_tunnel(opencode_port)
|
|
241
|
+
worker_port = await host.establish_tunnel(opencode_port, bind_addr=bind_addr)
|
|
223
242
|
|
|
224
243
|
if preserved_session_id is not None:
|
|
225
244
|
session_id = preserved_session_id
|
|
@@ -235,7 +254,7 @@ async def run_opencode_session(ctx: ProcessContext, config: OpencodeTaskConfig)
|
|
|
235
254
|
)
|
|
236
255
|
|
|
237
256
|
await ctx.set_widget_upstream(
|
|
238
|
-
f"http://
|
|
257
|
+
f"http://{upstream_host}:{worker_port}",
|
|
239
258
|
inner_auth=BasicAuth(username="opencode", password=password),
|
|
240
259
|
)
|
|
241
260
|
# Point the iframe directly at the pre-created session so viewers
|
|
@@ -64,6 +64,50 @@ async def test_launch_times_out_on_no_url(tmp_path):
|
|
|
64
64
|
pass
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
async def test_launch_opencode_passes_hostname_into_cmd(local_host, monkeypatch):
|
|
68
|
+
"""Multi-container deploys need opencode bound to a non-loopback
|
|
69
|
+
interface so a sibling API-proxy container can reach it. The
|
|
70
|
+
``hostname`` kwarg must propagate into the ``opencode web
|
|
71
|
+
--hostname=`` argument."""
|
|
72
|
+
captured: dict[str, str] = {}
|
|
73
|
+
|
|
74
|
+
async def fake_launch_subprocess(self, cmd, *, env=None, cwd=None):
|
|
75
|
+
captured["cmd"] = cmd
|
|
76
|
+
raise RuntimeError("stop before waiting on stdout")
|
|
77
|
+
|
|
78
|
+
monkeypatch.setattr(LocalHost, "launch_subprocess", fake_launch_subprocess)
|
|
79
|
+
|
|
80
|
+
await local_host.setup_workdir()
|
|
81
|
+
with pytest.raises(RuntimeError, match="stop before"):
|
|
82
|
+
await host_actions.launch_opencode(
|
|
83
|
+
local_host, password="pw",
|
|
84
|
+
ready_timeout_s=1.0,
|
|
85
|
+
opencode_executable="opencode",
|
|
86
|
+
hostname="0.0.0.0",
|
|
87
|
+
)
|
|
88
|
+
assert "--hostname=0.0.0.0" in captured["cmd"]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def test_launch_opencode_default_hostname_is_loopback(local_host, monkeypatch):
|
|
92
|
+
"""Default keeps single-host / RemoteHost-over-SSH behaviour intact."""
|
|
93
|
+
captured: dict[str, str] = {}
|
|
94
|
+
|
|
95
|
+
async def fake_launch_subprocess(self, cmd, *, env=None, cwd=None):
|
|
96
|
+
captured["cmd"] = cmd
|
|
97
|
+
raise RuntimeError("stop")
|
|
98
|
+
|
|
99
|
+
monkeypatch.setattr(LocalHost, "launch_subprocess", fake_launch_subprocess)
|
|
100
|
+
|
|
101
|
+
await local_host.setup_workdir()
|
|
102
|
+
with pytest.raises(RuntimeError, match="stop"):
|
|
103
|
+
await host_actions.launch_opencode(
|
|
104
|
+
local_host, password="pw",
|
|
105
|
+
ready_timeout_s=1.0,
|
|
106
|
+
opencode_executable="opencode",
|
|
107
|
+
)
|
|
108
|
+
assert "--hostname=127.0.0.1" in captured["cmd"]
|
|
109
|
+
|
|
110
|
+
|
|
67
111
|
async def test_tail_file_yields_appended_lines(local_host):
|
|
68
112
|
await local_host.setup_workdir()
|
|
69
113
|
log_path = os.path.join(local_host.workdir, "optio.log")
|
|
@@ -118,7 +118,7 @@ def _patch_host_actions(monkeypatch, host):
|
|
|
118
118
|
async def _version(_host, *, opencode_executable="opencode"):
|
|
119
119
|
return None
|
|
120
120
|
|
|
121
|
-
async def _launch(_host, _password, *, ready_timeout_s=30.0, opencode_executable="opencode"):
|
|
121
|
+
async def _launch(_host, _password, *, ready_timeout_s=30.0, opencode_executable="opencode", hostname="127.0.0.1"):
|
|
122
122
|
host.timeline.append("launch_opencode")
|
|
123
123
|
raise RuntimeError("test never gets past launch")
|
|
124
124
|
|
|
@@ -105,7 +105,7 @@ def _supply_scenario(monkeypatch):
|
|
|
105
105
|
orig_launch = host_actions.launch_opencode
|
|
106
106
|
scenario_holder: dict = {"name": "happy"}
|
|
107
107
|
|
|
108
|
-
async def _launch(host, password, *, ready_timeout_s=30.0, opencode_executable="opencode"):
|
|
108
|
+
async def _launch(host, password, *, ready_timeout_s=30.0, opencode_executable="opencode", hostname="127.0.0.1"):
|
|
109
109
|
del opencode_executable # we substitute fully
|
|
110
110
|
return await orig_launch(
|
|
111
111
|
host, password,
|
|
@@ -114,6 +114,7 @@ def _supply_scenario(monkeypatch):
|
|
|
114
114
|
f"{sys.executable} {FAKE_OPENCODE} "
|
|
115
115
|
f"--scenario {scenario_holder['name']}"
|
|
116
116
|
),
|
|
117
|
+
hostname=hostname,
|
|
117
118
|
)
|
|
118
119
|
monkeypatch.setattr(host_actions, "launch_opencode", _launch)
|
|
119
120
|
|
|
@@ -53,7 +53,7 @@ def _supply_scenario(monkeypatch):
|
|
|
53
53
|
orig_launch = host_actions.launch_opencode
|
|
54
54
|
holder = {"name": "happy"}
|
|
55
55
|
|
|
56
|
-
async def _launch(host, password, *, ready_timeout_s=30.0, opencode_executable="opencode"):
|
|
56
|
+
async def _launch(host, password, *, ready_timeout_s=30.0, opencode_executable="opencode", hostname="127.0.0.1"):
|
|
57
57
|
del opencode_executable
|
|
58
58
|
return await orig_launch(
|
|
59
59
|
host, password,
|
|
@@ -61,6 +61,7 @@ def _supply_scenario(monkeypatch):
|
|
|
61
61
|
opencode_executable=(
|
|
62
62
|
f"{sys.executable} {FAKE_OPENCODE} --scenario {holder['name']}"
|
|
63
63
|
),
|
|
64
|
+
hostname=hostname,
|
|
64
65
|
)
|
|
65
66
|
monkeypatch.setattr(host_actions, "launch_opencode", _launch)
|
|
66
67
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode.egg-info/dependency_links.txt
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
|