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.
Files changed (30) hide show
  1. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/PKG-INFO +1 -1
  2. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/pyproject.toml +1 -1
  3. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode/host_actions.py +8 -1
  4. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode/session.py +16 -8
  5. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode.egg-info/PKG-INFO +1 -1
  6. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_host_local.py +44 -0
  7. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_session_hooks.py +1 -1
  8. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_session_local.py +2 -1
  9. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_session_resume.py +2 -1
  10. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/README.md +0 -0
  11. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/setup.cfg +0 -0
  12. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode/__init__.py +0 -0
  13. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode/prompt.py +0 -0
  14. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode/snapshots.py +0 -0
  15. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode/types.py +0 -0
  16. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode.egg-info/SOURCES.txt +0 -0
  17. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode.egg-info/dependency_links.txt +0 -0
  18. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode.egg-info/requires.txt +0 -0
  19. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/src/optio_opencode.egg-info/top_level.txt +0 -0
  20. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_host_primitives_local.py +0 -0
  21. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_host_primitives_remote.py +0 -0
  22. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_host_remote_resume.py +0 -0
  23. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_host_resume.py +0 -0
  24. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_prompt.py +0 -0
  25. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_sanity.py +0 -0
  26. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_session_blob_hooks.py +0 -0
  27. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_session_remote.py +0 -0
  28. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_smart_install.py +0 -0
  29. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_snapshots.py +0 -0
  30. {optio_opencode-0.1.1 → optio_opencode-0.1.2}/tests/test_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: optio-opencode
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Run opencode web as an optio task; local subprocess or remote via SSH.
5
5
  Author-email: Kristof Csillag <kristof.csillag@deai-labs.com>
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "optio-opencode"
7
- version = "0.1.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"
@@ -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=127.0.0.1"
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: optio-opencode
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Run opencode web as an optio task; local subprocess or remote via SSH.
5
5
  Author-email: Kristof Csillag <kristof.csillag@deai-labs.com>
6
6
  License-Expression: Apache-2.0
@@ -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