mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
fix(wrap/opencode): verify the opencode binary before mutating config
Verify the OpenCode executable before changing configuration.
This commit is contained in:
parent
d0c1f5b8ad
commit
ae384862a4
2 changed files with 46 additions and 5 deletions
|
|
@ -6947,6 +6947,20 @@ def opencode(
|
||||||
)
|
)
|
||||||
subscription_resolution = _require_copilot_subscription_resolution()
|
subscription_resolution = _require_copilot_subscription_resolution()
|
||||||
|
|
||||||
|
# Verify the opencode binary exists BEFORE mutating any config. Otherwise a
|
||||||
|
# missing binary leaves headroom MCP/Serena/memory entries in the user's
|
||||||
|
# opencode config and an injected AGENTS.md, then errors with no cleanup --
|
||||||
|
# the config-before-verify anti-pattern (#1614). Siblings (claude, codex,
|
||||||
|
# goose, omp) already check first. `--prepare-only` intentionally writes
|
||||||
|
# config without launching, so it is exempt.
|
||||||
|
opencode_bin: str | None = None
|
||||||
|
if not prepare_only:
|
||||||
|
opencode_bin = shutil.which("opencode")
|
||||||
|
if not opencode_bin:
|
||||||
|
click.echo("Error: 'opencode' not found in PATH.")
|
||||||
|
click.echo("Install OpenCode: https://opencode.ai")
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
# Snapshot OpenCode config.json BEFORE any wrap-time mutation so
|
# Snapshot OpenCode config.json BEFORE any wrap-time mutation so
|
||||||
# `headroom unwrap opencode` can restore the user's pre-wrap state.
|
# `headroom unwrap opencode` can restore the user's pre-wrap state.
|
||||||
_opencode_config_file, _opencode_backup_file = opencode_config_paths()
|
_opencode_config_file, _opencode_backup_file = opencode_config_paths()
|
||||||
|
|
@ -6987,11 +7001,9 @@ def opencode(
|
||||||
inject_opencode_provider_config(port)
|
inject_opencode_provider_config(port)
|
||||||
return
|
return
|
||||||
|
|
||||||
opencode_bin = shutil.which("opencode")
|
# Past the prepare-only return the launch path always ran the binary check
|
||||||
if not opencode_bin:
|
# above, so opencode_bin is resolved.
|
||||||
click.echo("Error: 'opencode' not found in PATH.")
|
assert opencode_bin is not None
|
||||||
click.echo("Install OpenCode: https://opencode.ai")
|
|
||||||
raise SystemExit(1)
|
|
||||||
|
|
||||||
# Register our proxy client marker BEFORE _ensure_proxy so that another
|
# Register our proxy client marker BEFORE _ensure_proxy so that another
|
||||||
# wrapper's cleanup sees us as an active client and doesn't terminate a
|
# wrapper's cleanup sees us as an active client and doesn't terminate a
|
||||||
|
|
|
||||||
|
|
@ -364,6 +364,35 @@ def test_wrap_opencode_missing_binary_errors_clearly(
|
||||||
assert "'opencode' not found in PATH" in result.output
|
assert "'opencode' not found in PATH" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrap_opencode_missing_binary_does_not_mutate_config(
|
||||||
|
runner: CliRunner,
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""A missing opencode binary must not leave memory side-effects behind (#1614 class).
|
||||||
|
|
||||||
|
The MCP/Serena registrations are already gated on ``registrar.detect()``, but
|
||||||
|
the ``--memory`` injections (AGENTS.md, the .headroom dir, the memory MCP
|
||||||
|
config) are not -- they ran unconditionally before the binary check. Verify
|
||||||
|
the binary first, like claude/codex/goose/omp, so an absent tool cannot write
|
||||||
|
those and then error with nothing launched.
|
||||||
|
"""
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
monkeypatch.delenv("HEADROOM_CONTEXT_TOOL", raising=False)
|
||||||
|
_set_test_home(monkeypatch, tmp_path)
|
||||||
|
|
||||||
|
agents_md = tmp_path / "AGENTS.md"
|
||||||
|
headroom_dir = tmp_path / ".headroom"
|
||||||
|
|
||||||
|
with patch.object(wrap_mod.shutil, "which", return_value=None):
|
||||||
|
result = runner.invoke(main, ["wrap", "opencode", "--memory"])
|
||||||
|
|
||||||
|
assert result.exit_code == 1
|
||||||
|
assert "'opencode' not found in PATH" in result.output
|
||||||
|
assert not agents_md.exists(), "AGENTS.md was created before the missing-binary check"
|
||||||
|
assert not headroom_dir.exists(), ".headroom dir was created before the missing-binary check"
|
||||||
|
|
||||||
|
|
||||||
def test_wrap_opencode_prepare_only_injects_config(
|
def test_wrap_opencode_prepare_only_injects_config(
|
||||||
runner: CliRunner,
|
runner: CliRunner,
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue