optio-opencode 0.1.1__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.1 → optio_opencode-0.1.2}/PKG-INFO +1 -1
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/pyproject.toml +1 -1
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode/host_actions.py +8 -1
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode/session.py +16 -8
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode.egg-info/PKG-INFO +1 -1
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_host_local.py +44 -0
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_session_hooks.py +1 -1
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_session_local.py +2 -1
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_session_resume.py +2 -1
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/README.md +0 -0
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/setup.cfg +0 -0
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode/__init__.py +0 -0
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode/prompt.py +0 -0
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode/snapshots.py +0 -0
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode/types.py +0 -0
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode.egg-info/SOURCES.txt +0 -0
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode.egg-info/dependency_links.txt +0 -0
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode.egg-info/requires.txt +0 -0
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode.egg-info/top_level.txt +0 -0
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_host_primitives_local.py +0 -0
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_host_primitives_remote.py +0 -0
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_host_remote_resume.py +0 -0
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_host_resume.py +0 -0
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_prompt.py +0 -0
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_sanity.py +0 -0
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_session_blob_hooks.py +0 -0
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_session_remote.py +0 -0
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_smart_install.py +0 -0
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_snapshots.py +0 -0
- {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_types.py +0 -0
|
@@ -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,14 +210,6 @@ 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
|
-
ctx.report_progress(None, f"Launching opencode{version_suffix}…")
|
|
214
|
-
handle, opencode_port = await host_actions.launch_opencode(
|
|
215
|
-
host, password,
|
|
216
|
-
ready_timeout_s=READY_TIMEOUT_S,
|
|
217
|
-
opencode_executable=opencode_exec,
|
|
218
|
-
)
|
|
219
|
-
launched_handle = handle
|
|
220
|
-
|
|
221
213
|
# --- tunnel + widget registration --------------------------------
|
|
222
214
|
# By default the SSH tunnel listens on 127.0.0.1 — only the worker
|
|
223
215
|
# process (this engine) can reach it. For multi-container deploys
|
|
@@ -230,6 +222,22 @@ async def run_opencode_session(ctx: ProcessContext, config: OpencodeTaskConfig)
|
|
|
230
222
|
# 127.0.0.1 so single-host deploys are unchanged.
|
|
231
223
|
bind_addr = os.environ.get("OPTIO_WIDGET_TUNNEL_BIND", "127.0.0.1")
|
|
232
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
|
+
|
|
232
|
+
ctx.report_progress(None, f"Launching opencode{version_suffix}…")
|
|
233
|
+
handle, opencode_port = await host_actions.launch_opencode(
|
|
234
|
+
host, password,
|
|
235
|
+
ready_timeout_s=READY_TIMEOUT_S,
|
|
236
|
+
opencode_executable=opencode_exec,
|
|
237
|
+
hostname=opencode_hostname,
|
|
238
|
+
)
|
|
239
|
+
launched_handle = handle
|
|
240
|
+
|
|
233
241
|
worker_port = await host.establish_tunnel(opencode_port, bind_addr=bind_addr)
|
|
234
242
|
|
|
235
243
|
if preserved_session_id is not None:
|
|
@@ -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.1 → 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
|