fix(box): install OpenCode from a pinned release the run PATH resolves

31ca43c6ec03 · AtlantisPleb · · parent 51e25abca40d

fix(box): install OpenCode from a pinned release the run PATH resolves

The setup script piped `https://opencode.ai/install` into bash. That installer
resolves its version through the unauthenticated GitHub API, which answers 403
for the provider's shared egress IP, and the whole script runs under
`set -euo pipefail`, so one rate-limited lookup cost the box both the binary
and the `opencode.json` write that followed it. Even when the install
succeeded, the binary landed in `$HOME/.opencode/bin` with an `export PATH`
line appended to a shell rc, and a box command runs under a non-interactive
`sh -c` that never sources it.

Write the configuration first, so a failed fetch costs the binary and nothing
else. Fetch a pinned release tarball directly, with the architecture resolved
from `uname -m` and a bounded retry that still fails loudly when its budget is
spent. Symlink the binary into `$HOME/.local/bin`, already on the PATH a run
gets.

Verified live on box `bx_se9xfq7q`: with the install wiped, the rendered
script exits 0 and `sh -c 'command -v opencode'` answers
`/home/user/.local/bin/opencode` at 1.18.23, and `opencode run` completes a
`stealth/ox-alpha` turn. Verified on `bx_732ts8jg` that the same script
pointed at an unreachable tag retries, exits 1, and leaves the configuration
intact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoZMfWRSGnf6FZX2Ar9rQ2
Co-Authored-By
Claude Fable 5 <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified lib/openagents/box.ex
  • modified test/openagents/box_test.exs

Diff

2 files changed, +117 -2

lib/openagents/box.ex modified +54 -1

@@ -27,6 +27,19 @@ defmodule OpenAgents.Box do

27 27
  @default_poll_attempts 30
28 28
  @runnable_states ~w(ready idle running)
29 29
30
  # OpenCode is pinned rather than tracked at `latest` on purpose. The upstream
31
  # installer resolves its version through the unauthenticated GitHub API,
32
  # which answers 403 for the provider's shared egress IP, and a `latest`
33
  # download URL reintroduces a version lookup on a network path the box has no
34
  # way to retry. A pinned tag is a plain artifact fetch: one request, one
35
  # cacheable URL, and a version we chose deliberately. Raise it by editing
36
  # this value and provisioning one box to confirm the new tag installs.
37
  @opencode_version "1.18.23"
38
  @opencode_install_dir "$HOME/.opencode/bin"
39
  # Already on the PATH a non-interactive `sh -c` run gets on a box.
40
  @opencode_link_dir "$HOME/.local/bin"
41
  @opencode_download_attempts 3
42
30 43
  @doc "The default number of active Boxes one conversation can hold."
31 44
  @spec maximum_active_boxes() :: pos_integer()
32 45
  def maximum_active_boxes do

@@ -434,6 +447,11 @@ defmodule OpenAgents.Box do

434 447
  # application's configured OpenRouter model. OpenCode reads the
435 448
  # OPENROUTER_API_KEY environment variable natively, so the setup script
436 449
  # never touches the credential.
450
  #
451
  # The order matters. The whole script runs under `set -euo pipefail`, so the
452
  # configuration is written first: an install that fails on a bad network day
453
  # then costs the binary and nothing else, and a later manual install finds
454
  # the model already pointed at Ox Alpha.
437 455
  defp setup_script do
438 456
    model = Application.get_env(:openagents, :openrouter_model, "stealth/ox-alpha")
439 457

@@ -446,11 +464,46 @@ defmodule OpenAgents.Box do

446 464
    """
447 465
    #!/bin/bash
448 466
    set -euo pipefail
449
    curl -fsSL https://opencode.ai/install | bash
467
450 468
    mkdir -p "$HOME/.config/opencode"
451 469
    cat > "$HOME/.config/opencode/opencode.json" <<'OPENCODE_CONFIGURATION'
452 470
    #{configuration}
453 471
    OPENCODE_CONFIGURATION
472
473
    case "$(uname -m)" in
474
      x86_64|amd64) opencode_target="linux-x64" ;;
475
      aarch64|arm64) opencode_target="linux-arm64" ;;
476
      *) echo "opencode: unsupported architecture $(uname -m)" >&2; exit 1 ;;
477
    esac
478
479
    opencode_url="https://github.com/anomalyco/opencode/releases/download/v#{@opencode_version}/opencode-$opencode_target.tar.gz"
480
    opencode_archive="$(mktemp)"
481
    mkdir -p "#{@opencode_install_dir}" "#{@opencode_link_dir}"
482
483
    # A bounded retry, because one refused connection should not cost the box
484
    # its harness. An exhausted budget still fails loudly: the box reports
485
    # setup_status failed rather than pretending to carry a binary it lacks.
486
    opencode_attempt=1
487
    until curl -fsSL --connect-timeout 10 --max-time 600 -o "$opencode_archive" "$opencode_url"; do
488
      if [ "$opencode_attempt" -ge #{@opencode_download_attempts} ]; then
489
        echo "opencode: download failed after $opencode_attempt attempts" >&2
490
        rm -f "$opencode_archive"
491
        exit 1
492
      fi
493
      sleep "$((opencode_attempt * 5))"
494
      opencode_attempt="$((opencode_attempt + 1))"
495
    done
496
497
    tar -xzf "$opencode_archive" -C "#{@opencode_install_dir}"
498
    rm -f "$opencode_archive"
499
    chmod +x "#{@opencode_install_dir}/opencode"
500
501
    # A box run is a non-interactive `sh -c`, which never sources the shell rc
502
    # the upstream installer appends its PATH line to. Link the binary into a
503
    # directory a plain exec already resolves.
504
    ln -sf "#{@opencode_install_dir}/opencode" "#{@opencode_link_dir}/opencode"
505
506
    "#{@opencode_link_dir}/opencode" --version
454 507
    """
455 508
  end
456 509
test/openagents/box_test.exs modified +63 -1

@@ -37,6 +37,32 @@ defmodule OpenAgents.BoxTest do

37 37
  defp restore_env(key, nil), do: Application.delete_env(:openagents, key)
38 38
  defp restore_env(key, value), do: Application.put_env(:openagents, key, value)
39 39
40
  # The setup script is private, and the provider payload is where it becomes
41
  # observable, so read it back the way the provider does.
42
  defp captured_setup_script(conversation_id) do
43
    owner = self()
44
45
    Req.Test.stub(__MODULE__, fn conn ->
46
      {:ok, raw, conn} = Plug.Conn.read_body(conn)
47
48
      case Jason.decode(raw) do
49
        {:ok, %{"setupScript" => script}} -> send(owner, {:setup_script, script})
50
        _other -> :ok
51
      end
52
53
      Req.Test.json(conn, box_body())
54
    end)
55
56
    assert {:ok, _record} = Box.create_box(conversation_id)
57
    assert_received {:setup_script, script}
58
    script
59
  end
60
61
  defp index_of(haystack, needle) do
62
    assert [index | _rest] = :binary.match(haystack, needle) |> Tuple.to_list()
63
    index
64
  end
65
40 66
  defp box_body(overrides \\ %{}) do
41 67
    %{
42 68
      "box" =>

@@ -64,7 +90,6 @@ defmodule OpenAgents.BoxTest do

64 90
        {:ok, raw, conn} = Plug.Conn.read_body(conn)
65 91
        payload = Jason.decode!(raw)
66 92
        assert payload["noEnv"] == true
67
        assert payload["setupScript"] =~ "opencode.ai/install"
68 93
        assert payload["setupScript"] =~ "openrouter/stealth/ox-alpha"
69 94
70 95
        Req.Test.json(conn, box_body(%{"state" => "provisioning", "setupStatus" => "pending"}))

@@ -92,6 +117,43 @@ defmodule OpenAgents.BoxTest do

92 117
      assert record.stopped_at == nil
93 118
    end
94 119
120
    test "writes the OpenCode configuration before installing", %{conversation_id: cid} do
121
      script = captured_setup_script(cid)
122
123
      configuration_at = index_of(script, "opencode.json")
124
      install_at = index_of(script, "releases/download")
125
126
      assert configuration_at < install_at,
127
             "the configuration write must precede the install so a failed fetch cannot cost both"
128
    end
129
130
    test "installs a pinned release without the unauthenticated GitHub API", %{
131
      conversation_id: cid
132
    } do
133
      script = captured_setup_script(cid)
134
135
      assert script =~
136
               ~r{https://github\.com/anomalyco/opencode/releases/download/v\d+\.\d+\.\d+/}
137
138
      refute script =~ "api.github.com"
139
      refute script =~ "opencode.ai/install"
140
      refute script =~ "releases/latest"
141
    end
142
143
    test "links the binary onto the PATH a non-interactive run gets", %{conversation_id: cid} do
144
      script = captured_setup_script(cid)
145
146
      assert script =~ ~s(ln -sf "$HOME/.opencode/bin/opencode" "$HOME/.local/bin/opencode")
147
    end
148
149
    test "retries a transient fetch a bounded number of times", %{conversation_id: cid} do
150
      script = captured_setup_script(cid)
151
152
      assert script =~ "until curl"
153
      assert script =~ ~r/if \[ "\$opencode_attempt" -ge \d+ \]/
154
      assert script =~ "exit 1"
155
    end
156
95 157
    test "injects the OpenRouter key through the box environment only", %{conversation_id: cid} do
96 158
      original = Application.get_env(:openagents, :openrouter_api_key)
97 159
      Application.put_env(:openagents, :openrouter_api_key, "sk-or-v1-test0000000000000000")

This page updates live while a promote is in flight · changelog