fix(opencode-native): accept 1.18.x — bump version gate to <1.19.0 (#4725)

The upper bound was pinned at <1.18.0 (added in #1550 with 'refuse 1.18+
until validated'). OpenCode 1.18.x has since shipped 17 releases, making the
gate reject every current upstream install.

The 1.17.x-shaped assumptions in the forwarder are already forward-compatible:
- part-based message events (message.updated / message.part.updated) are
  unchanged in 1.18.x
- both permission.asked and permission.v2.asked are already handled

Changes:
- OPENCODE_MAX_VERSION_EXCLUSIVE: 1.18.0 -> 1.19.0
- npm install pin: opencode-ai@~1.17.7 -> opencode-ai@~1.18.0
- update tests and comments to match the new range

Fixes #4670

Signed-off-by: Tomu Hirata <tomu.hirata@gmail.com>
This commit is contained in:
Tomu Hirata
2026-08-13 22:57:13 +09:00
committed by GitHub
parent 10e5cf6059
commit ce6dba9c88
5 changed files with 21 additions and 17 deletions
+3 -3
View File
@@ -201,17 +201,17 @@ _HARNESS_INSTALL: dict[str, HarnessInstallSpec] = {
# ``pi >= 0.79.0``; older CLIs would prompt mid-session.
min_version=_PI_MIN_VERSION,
),
# Pin the install to the supported 1.17.x range: opencode-ai's npm ``latest``
# Pin the install to the supported 1.18.x range: opencode-ai's npm ``latest``
# is a ``0.0.0-beta-*`` pre-release, so a bare ``opencode-ai`` would install a
# version the runtime version-check (``check_opencode_version``,
# >=1.17.7,<1.18.0) then rejects. ``~1.17.7`` mirrors that exact range.
# >=1.17.7,<1.19.0) then rejects. ``~1.18.0`` resolves to the latest 1.18.x.
# The same version bounds are enforced in setup via ``min_version`` /
# ``max_version_exclusive`` so the install/upgrade prompt fires before
# the runtime gate does.
OPENCODE_KEY: HarnessInstallSpec(
"OpenCode",
"opencode",
"opencode-ai@~1.17.7",
"opencode-ai@~1.18.0",
min_version=OPENCODE_MIN_VERSION,
max_version_exclusive=OPENCODE_MAX_VERSION_EXCLUSIVE,
),
+5 -4
View File
@@ -1,6 +1,6 @@
"""Typed HTTP + SSE client for an ``opencode serve`` native server.
Shaped from the pinned OpenCode OpenAPI (``opencode`` 1.17.x,
Shaped from the pinned OpenCode OpenAPI (``opencode`` 1.17.x1.18.x,
``packages/sdk/openapi.json``). This is a thin typed wrapper over the v1
REST endpoints the Omnigent OpenCode-native harness needs plus the SSE
``GET /event`` stream — not a full generated SDK. Unknown response fields
@@ -29,10 +29,11 @@ from omnigent.json_types import JsonObject as _JsonObject
_logger = logging.getLogger(__name__)
# Pinned OpenCode CLI/API version range. The source monorepo reports
# 1.17.7; we accept 1.17.x and refuse 1.18+ until validated.
# Supported OpenCode CLI/API version range. Accepts 1.17.7+ through the
# entire 1.18.x line (validated against 1.18.x event/API shapes); refuses
# 1.19+ until validated against that release.
OPENCODE_MIN_VERSION = "1.17.7"
OPENCODE_MAX_VERSION_EXCLUSIVE = "1.18.0"
OPENCODE_MAX_VERSION_EXCLUSIVE = "1.19.0"
_DEFAULT_TIMEOUT = httpx.Timeout(30.0, connect=10.0)
+3 -3
View File
@@ -1043,8 +1043,8 @@ def opencode_tool_output_text(state: _JsonMapping) -> str:
# resolves the method on the instance. Keys are OpenCode event ``type``
# discriminators (see openapi.json Event* schemas).
_HANDLERS: dict[str, Callable[[OpenCodeNativeForwarder, OpenCodeEvent], Awaitable[None]]] = {
# opencode 1.17.x is part-based: text/tool live on message PARTS, lifecycle
# on the message + session. (Verified against a real ``opencode serve``.)
# opencode 1.17.x1.18.x is part-based: text/tool live on message PARTS,
# lifecycle on the message + session. (Verified against real ``opencode serve``.)
"message.updated": OpenCodeNativeForwarder._on_message_updated,
"message.part.updated": OpenCodeNativeForwarder._on_part_updated,
# NB: ``message.part.delta`` (live token stream) is intentionally NOT
@@ -1066,7 +1066,7 @@ _HANDLERS: dict[str, Callable[[OpenCodeNativeForwarder, OpenCodeEvent], Awaitabl
"session.compacted": OpenCodeNativeForwarder._on_compaction_ended,
# Mirror a TUI model switch back to Omnigent (in-harness session-cmd sync).
"session.next.model.switched": OpenCodeNativeForwarder._on_model_switched,
# Permission ask: 1.17.x emits ``permission.asked``; keep the ``v2`` spelling
# Permission ask: pre-1.18 emits ``permission.asked``; keep the ``v2`` spelling
# too so a point-release rename still routes through the policy gate.
"permission.asked": OpenCodeNativeForwarder._on_permission_asked,
"permission.v2.asked": OpenCodeNativeForwarder._on_permission_asked,
+4 -3
View File
@@ -1154,7 +1154,7 @@ def test_ui_setup_steps_generic_for_non_installable() -> None:
@pytest.mark.parametrize(
"key,min_version,max_version_exclusive",
[
(hi.OPENCODE_KEY, "1.17.7", "1.18.0"),
(hi.OPENCODE_KEY, "1.17.7", "1.19.0"),
(hi.CURSOR_KEY, "2026.06.02", None),
(hi.KIMI_KEY, "0.7.0", None),
(ANTHROPIC_FAMILY, "2.1.161", None),
@@ -1180,9 +1180,10 @@ def test_versioned_specs_declare_bounds(
"version,expected",
[
("1.17.6", False), # below min
("1.18.0", False), # at max exclusive
("1.19.0", False), # at max exclusive
("2.0.0", False), # above max
("1.17.8", True), # inside range
("1.18.16", True), # inside range (1.18.x)
],
)
def test_harness_cli_installed_checks_version_for_versioned_specs(
@@ -1194,7 +1195,7 @@ def test_harness_cli_installed_checks_version_for_versioned_specs(
def _run(argv: list[str], **k: object) -> subprocess.CompletedProcess[str]:
if len(argv) >= 2 and argv[1] == "--version":
# OpenCode's supported range is [1.17.7, 1.18.0).
# OpenCode's supported range is [1.17.7, 1.19.0).
return subprocess.CompletedProcess(
args=argv, returncode=0, stdout=f"{version}\n", stderr=""
)
+6 -4
View File
@@ -31,9 +31,11 @@ def test_parse_opencode_version() -> None:
def test_check_version_in_range() -> None:
check_opencode_version("1.17.7")
check_opencode_version("1.17.99")
check_opencode_version("1.18.0")
check_opencode_version("1.18.16")
@pytest.mark.parametrize("version", ["1.16.0", "1.18.0", "2.0.0"])
@pytest.mark.parametrize("version", ["1.16.0", "1.19.0", "2.0.0"])
def test_check_version_out_of_range_raises(version: str) -> None:
with pytest.raises(OpenCodeVersionError):
check_opencode_version(version)
@@ -186,7 +188,7 @@ async def test_start_raises_on_unsupported_version_without_env(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(appsrv.shutil, "which", lambda name: f"/usr/bin/{name}")
monkeypatch.setattr(appsrv, "resolve_opencode_version", lambda _path: "1.18.0")
monkeypatch.setattr(appsrv, "resolve_opencode_version", lambda _path: "1.19.0")
monkeypatch.delenv("OMNIGENT_OPENCODE_SKIP_VERSION_CHECK", raising=False)
server = OpenCodeNativeServer(
bridge_dir=tmp_path,
@@ -214,7 +216,7 @@ async def test_start_skips_version_gate_when_env_set(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(appsrv.shutil, "which", lambda name: f"/usr/bin/{name}")
monkeypatch.setattr(appsrv, "resolve_opencode_version", lambda _path: "1.18.0")
monkeypatch.setattr(appsrv, "resolve_opencode_version", lambda _path: "1.19.0")
monkeypatch.setenv("OMNIGENT_OPENCODE_SKIP_VERSION_CHECK", "1")
server = OpenCodeNativeServer(
bridge_dir=tmp_path,
@@ -235,7 +237,7 @@ async def test_start_skips_version_gate_when_env_set(
monkeypatch.setattr(appsrv.subprocess, "Popen", lambda argv, **kwargs: _FakeProc())
monkeypatch.setattr(OpenCodeNativeServer, "_wait_until_ready", fake_wait)
await server.start()
assert server.version == "1.18.0"
assert server.version == "1.19.0"
assert server.process is not None