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.
Files changed (30) hide show
  1. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/PKG-INFO +1 -1
  2. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/pyproject.toml +3 -1
  3. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode/host_actions.py +8 -1
  4. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode/session.py +22 -3
  5. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode.egg-info/PKG-INFO +1 -1
  6. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_host_local.py +44 -0
  7. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_session_hooks.py +1 -1
  8. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_session_local.py +2 -1
  9. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_session_resume.py +2 -1
  10. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/README.md +0 -0
  11. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/setup.cfg +0 -0
  12. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode/__init__.py +0 -0
  13. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode/prompt.py +0 -0
  14. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode/snapshots.py +0 -0
  15. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode/types.py +0 -0
  16. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode.egg-info/SOURCES.txt +0 -0
  17. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode.egg-info/dependency_links.txt +0 -0
  18. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode.egg-info/requires.txt +0 -0
  19. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/src/optio_opencode.egg-info/top_level.txt +0 -0
  20. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_host_primitives_local.py +0 -0
  21. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_host_primitives_remote.py +0 -0
  22. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_host_remote_resume.py +0 -0
  23. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_host_resume.py +0 -0
  24. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_prompt.py +0 -0
  25. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_sanity.py +0 -0
  26. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_session_blob_hooks.py +0 -0
  27. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_session_remote.py +0 -0
  28. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_smart_install.py +0 -0
  29. {optio_opencode-0.1.0 → optio_opencode-0.1.2}/tests/test_snapshots.py +0 -0
  30. {optio_opencode-0.1.0 → 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.0
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.0"
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=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,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
- # --- tunnel + widget registration --------------------------------
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://127.0.0.1:{worker_port}",
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: optio-opencode
3
- Version: 0.1.0
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