fix(box): carry the assignment credential inside the dispatch command

6651e503b653 · AtlantisPleb · · parent a95a0e4c4069

fix(box): carry the assignment credential inside the dispatch command

A credentialed dispatch added `env: {OPENAGENTS_FORGE_TOKEN}` to the provider
`/commands` call. The provider's published `CommandRequest` schema is
`command`, `cwd`, `timeoutSeconds`, and `detached` — there is no `env`, so the
field was accepted with the request and dropped. The wrapper then read
`$OPENAGENTS_FORGE_TOKEN` under `set -u`, died on the unbound variable, and
printed no pid, which is the `box_response_invalid` four assignment runs
recorded. Reproduced on box `bx_732ts8jg`: HTTP 2xx, `exit_code` 1, empty
stdout, `bash: line 12: OPENAGENTS_FORGE_TOKEN: unbound variable`.

`env` on box create is a different endpoint and does work, which is how
`OPENROUTER_API_KEY` reaches a box, and it is the better channel because the
secret never enters a command request. It is not available here: an assignment
credential is minted per attempt, long after its box exists, and
`PATCH /boxes/{id}` takes only `name`, `ttlSeconds`, and `subdomain`. So the
credential now travels inside the dispatch command, base64 only so a token
cannot break the surrounding shell quoting. The cost is explicit and bounded:
the credential is in the provider's request body and any log the provider keeps
of it, it is never in `box_runs.command`, and an assignment credential is
short-lived and scoped to one branch of one repository.

A 2xx that carries no pid now logs a bounded operational event — response
keys, exit code, output sizes, and one truncated stderr line with the
credential struck out by exact match — instead of a bare atom that could not
tell a refused request from a wrapper that ran and failed.

Verified live on `bx_732ts8jg` with a stand-in credential: dispatch returns a
parseable pid, the run root is created, the detached child runs under
`GIT_CONFIG_GLOBAL`, `git credential fill` for `https://openagents.com`
presents the credential, the exit sentinel is written, and the credential and
gitconfig files are removed when the run ends.

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 docs/2026-08-25-ox-alpha-stress-test-log.md
  • modified lib/openagents/box/client.ex
  • modified test/openagents/box_client_runs_test.exs

Diff

3 files changed, +250 -13

docs/2026-08-25-ox-alpha-stress-test-log.md modified +86

@@ -267,3 +267,89 @@ used.

267 267
3. Output logs, tokens, and push receipts on the server: **output logs
268 268
   demonstrated live**; tokens not applicable (no model turn); **push
269 269
   receipts blocked** by the credentialed-dispatch failure above.
270
271
## 8. Server-side Box fixes and live verification (2026-08-25)
272
273
Three defects named in §7 and in openagents#58 were fixed on the server. Boxes
274
`bx_732ts8jg` and `bx_se9xfq7q` on conversation `3dd6d813` carried the
275
verification; both were stopped afterwards, peak concurrency 2.
276
277
### Setup script: configuration first, pinned artifact, PATH the run sees
278
279
`OpenAgents.Box.setup_script/0` piped `https://opencode.ai/install` into bash.
280
That installer resolves its version through the unauthenticated GitHub API,
281
which answers 403 for the provider's shared egress IP, and the whole script
282
runs under `set -euo pipefail`, so one rate-limited lookup cost the box both
283
the binary and the `opencode.json` write that followed it.
284
285
The script now writes the configuration first, fetches a pinned release
286
tarball directly (`releases/download/v1.18.23/opencode-<target>.tar.gz`, arch
287
resolved from `uname -m`), retries a refused fetch twice before failing loudly,
288
and symlinks the binary into `$HOME/.local/bin` — already on the PATH a
289
non-interactive `sh -c` run gets, which the installer's
290
`$HOME/.opencode/bin` plus a shell-rc `export PATH` line never was.
291
292
Verified on `bx_se9xfq7q`: with `~/.opencode`, `~/.local/bin/opencode`, and
293
`~/.config/opencode` deleted, the rendered script exits 0 and
294
`sh -c 'command -v opencode && opencode --version'` answers
295
`/home/user/.local/bin/opencode` and `1.18.23`. An `opencode run` in that state
296
returns `> build · stealth/ox-alpha` and the model's reply, with
297
`OPENROUTER_API_KEY` supplied by the box environment as before.
298
299
Verified on `bx_732ts8jg`: the same script pointed at an unreachable release
300
tag retries twice, exits 1 — so `setup_status` still reports `failed` honestly
301
— and leaves `opencode.json` intact with no binary.
302
303
### Conversation bootstrap a box token can reach
304
305
`GET /api/v1/conversation` under the `box_control_api` pipeline answers
306
`conversation_id` for the calling account, creating the conversation when the
307
account has none. Confirmed against production that a `box:control`-only token
308
is refused `401` on `GET /api/v1/user`, which is why a route of its own exists
309
rather than a field added there.
310
311
The field is deliberately not on `/api/v1/user`. That response is
312
GitHub-shaped, and API-001 holds every OpenAgents field there to a namespaced
313
`openagents` object, which is not what `BoxClient.resolveConversationId` reads.
314
One canonical route carries it at the top level instead. The CLI change that
315
follows this is a one-line reader retarget in the `openagents` monorepo; the
316
body shape already matches what the probe expects.
317
318
### Credentialed dispatch: the provider command API has no `env`
319
320
The provider's published `CommandRequest` schema is `command`, `cwd`,
321
`timeoutSeconds`, and `detached`. There is no `env`, so
322
`OpenAgents.Box.Client.dispatch_run/5` sent a field that was accepted with the
323
request and dropped. The wrapper then read `$OPENAGENTS_FORGE_TOKEN` under
324
`set -u` and died on the unbound variable before printing a pid — the
325
`box_response_invalid` of §7. Reproduced exactly on `bx_732ts8jg`: the same
326
wrapper with the variable unset returns HTTP 2xx, `exit_code` 1, empty stdout,
327
and `bash: line 12: OPENAGENTS_FORGE_TOKEN: unbound variable`.
328
329
`env` on box *create* is a different endpoint and does work, which is how
330
`OPENROUTER_API_KEY` reaches a box. It is not available here: an assignment
331
credential is minted per attempt, long after its box exists, and
332
`PATCH /boxes/{id}` takes only `name`, `ttlSeconds`, and `subdomain`. So the
333
credential now travels inside the dispatch command, base64 only so a token
334
cannot break the surrounding shell quoting. The cost is explicit: the
335
credential is in the provider's request body and in whatever the provider logs
336
of it. It is not in `box_runs.command`, which holds the caller's script and
337
never sees this wrapper, and an assignment credential is short-lived and scoped
338
to one branch of one repository.
339
340
A 2xx that carries no pid now logs a bounded operational event — response keys,
341
exit code, output sizes, and one truncated stderr line with the credential
342
struck out by exact match — instead of a bare atom.
343
344
Verified on `bx_732ts8jg` with a stand-in credential: dispatch returns a
345
parseable pid, the run root is created, the detached child runs under
346
`GIT_CONFIG_GLOBAL` pointing at the run's own gitconfig, `git credential fill`
347
for `https://openagents.com` presents `username=x` and the password, the exit
348
sentinel is written, and `forge-credential` and `gitconfig` are removed when
349
the run ends.
350
351
A push receipt is still not demonstrated. It needs a real assignment
352
credential, which `Forge.Assignments.create/1` mints server-side and never
353
returns to an API caller, so it cannot be exercised until this change is
354
deployed. Every step before the authenticated git handshake is demonstrated
355
live above.
lib/openagents/box/client.ex modified +90 -9

@@ -8,8 +8,17 @@ defmodule OpenAgents.Box.Client do

8 8
  before any request leaves the host. Desktop and viewer URLs never pass
9 9
  through this module: no function requests them, so a token-bearing URL
10 10
  cannot reach a caller or a log.
11
12
  One secret does leave here by design. `dispatch_run/5` puts an assignment's
13
  Git credential in the command it sends, because the provider's command API
14
  takes no environment map and a box environment can only be set at create.
15
  That function says why the channel is what it is and what bounds it. No log
16
  this module writes carries that credential: the one path that reads a failed
17
  dispatch response strikes it out by exact match first.
11 18
  """
12 19
20
  require Logger
21
13 22
  @default_base_url "https://ascii.dev/api/box/v1"
14 23
15 24
  @box_id_pattern ~r/^bx_[23456789abcdefghjkmnpqrstuvwxyz]{8}$/

@@ -80,7 +89,7 @@ defmodule OpenAgents.Box.Client do

80 89
             "command" => dispatch_command(run_id, command, run_directory),
81 90
             "timeoutSeconds" => 30
82 91
           }),
83
         {:ok, pid} <- dispatch_pid(body) do
92
         {:ok, pid} <- dispatch_pid_logged(body, box_id, run_id) do
84 93
      {:ok, pid}
85 94
    end
86 95
  end

@@ -90,16 +99,33 @@ defmodule OpenAgents.Box.Client do

90 99
  def dispatch_run(box_id, run_id, command, run_directory, nil),
91 100
    do: dispatch_run(box_id, run_id, command, run_directory)
92 101
102
  # The credential travels inside the dispatch command, not beside it. The
103
  # provider's `CommandRequest` schema is `command`, `cwd`, `timeoutSeconds`,
104
  # and `detached` — there is no `env`, so an `env` map was accepted with the
105
  # rest of the request and dropped. The wrapper then read
106
  # `$OPENAGENTS_FORGE_TOKEN` under `set -u`, died on the unbound variable, and
107
  # printed no pid, which is the `box_response_invalid` this replaces.
108
  #
109
  # `env` on box *create* is a different endpoint and does work, and it is the
110
  # better channel because the secret never enters a command request. It is not
111
  # available here: an assignment credential is minted per attempt, long after
112
  # its box exists, and `PATCH /boxes/{id}` takes only `name`, `ttlSeconds`,
113
  # and `subdomain`. So this is the documented fallback, and its cost is
114
  # explicit — the credential is in the provider's request body and in whatever
115
  # the provider logs of it. It is not in `box_runs.command`, which holds the
116
  # caller's script and never sees this wrapper, and it is base64 only so that
117
  # a token cannot break the surrounding shell quoting. That is encoding, not
118
  # protection. An assignment credential is short-lived and scoped to one
119
  # branch of one repository, which is what bounds the exposure.
93 120
  def dispatch_run(box_id, run_id, command, run_directory, credential)
94 121
      when is_binary(box_id) and is_binary(run_id) and is_binary(command) and
95 122
             is_binary(run_directory) and is_binary(credential) do
96 123
    with {:ok, body} <-
97 124
           command(box_id, %{
98
             "command" => dispatch_command(run_id, command, run_directory, true),
99
             "timeoutSeconds" => 30,
100
             "env" => %{"OPENAGENTS_FORGE_TOKEN" => credential}
125
             "command" => dispatch_command(run_id, command, run_directory, credential),
126
             "timeoutSeconds" => 30
101 127
           }),
102
         {:ok, pid} <- dispatch_pid(body) do
128
         {:ok, pid} <- dispatch_pid_logged(body, box_id, run_id, credential) do
103 129
      {:ok, pid}
104 130
    end
105 131
  end

@@ -168,20 +194,22 @@ defmodule OpenAgents.Box.Client do

168 194
  end
169 195
170 196
  defp dispatch_command(run_id, command, run_directory) do
171
    dispatch_command(run_id, command, run_directory, false)
197
    dispatch_command(run_id, command, run_directory, nil)
172 198
  end
173 199
174
  defp dispatch_command(run_id, command, run_directory, with_credential) do
200
  defp dispatch_command(run_id, command, run_directory, credential) do
175 201
    root = run_root(run_id, run_directory)
176 202
    encoded = Base.encode64(command)
203
    with_credential = is_binary(credential)
177 204
178 205
    credential_setup =
179 206
      if with_credential do
207
        encoded_credential = Base.encode64("https://x:#{credential}@openagents.com\n")
208
180 209
        """
181 210
        umask 077
182
        printf 'https://x:%s@openagents.com\\n' "$OPENAGENTS_FORGE_TOKEN" > "$root/forge-credential"
211
        printf '%s' '#{encoded_credential}' | base64 -d > "$root/forge-credential"
183 212
        git config --file="$root/gitconfig" credential.helper "store --file=$root/forge-credential"
184
        unset OPENAGENTS_FORGE_TOKEN
185 213
        """
186 214
      else
187 215
        ""

@@ -276,6 +304,59 @@ defmodule OpenAgents.Box.Client do

276 304
  defp run_root(run_id, nil), do: "$HOME/.openagents/box-runs/#{run_id}"
277 305
  defp run_root(_run_id, run_directory), do: run_directory
278 306
307
  defp dispatch_pid_logged(body, box_id, run_id, credential \\ nil) do
308
    case dispatch_pid(body) do
309
      {:ok, pid} ->
310
        {:ok, pid}
311
312
      {:error, :box_response_invalid} = error ->
313
        log_dispatch_refusal(body, box_id, run_id, credential)
314
        error
315
    end
316
  end
317
318
  # A 2xx whose body carries no pid used to be a bare atom, which said nothing
319
  # about whether the provider refused the request or the wrapper ran and
320
  # failed. Those are opposite diagnoses, and the exit code plus the first line
321
  # of stderr separates them in one look.
322
  #
323
  # This is an operational event, so it stays bounded and carries no raw
324
  # command result: the response's top-level keys, the exit code, output sizes,
325
  # and one truncated stderr line with the assignment credential struck out by
326
  # exact match. The credential is the one secret that can reach this path, and
327
  # this function is the one place that holds it.
328
  defp log_dispatch_refusal(body, box_id, run_id, credential) do
329
    body = if is_map(body), do: body, else: %{}
330
331
    Logger.warning(
332
      "box_dispatch_response_invalid box_id=#{box_id} run_id=#{run_id} " <>
333
        "keys=#{body |> Map.keys() |> Enum.sort() |> Enum.join(",")} " <>
334
        "exit_code=#{inspect(body["exit_code"] || body["exitCode"])} " <>
335
        "stdout_bytes=#{output_bytes(body["stdout"])} " <>
336
        "stderr_bytes=#{output_bytes(body["stderr"])} " <>
337
        "timed_out=#{inspect(body["timed_out"] || body["timedOut"])} " <>
338
        "stderr_head=#{inspect(stderr_head(body["stderr"], credential))}"
339
    )
340
  end
341
342
  defp output_bytes(value) when is_binary(value), do: byte_size(value)
343
  defp output_bytes(_value), do: 0
344
345
  defp stderr_head(stderr, credential) when is_binary(stderr) do
346
    stderr
347
    |> redact(credential)
348
    |> String.split("\n", parts: 2)
349
    |> List.first()
350
    |> String.slice(0, 200)
351
  end
352
353
  defp stderr_head(_stderr, _credential), do: ""
354
355
  defp redact(text, credential) when is_binary(credential) and credential != "",
356
    do: String.replace(text, credential, "[redacted]")
357
358
  defp redact(text, _credential), do: text
359
279 360
  defp dispatch_pid(%{"stdout" => output}) when is_binary(output) do
280 361
    case Regex.run(~r/(?:\A|\n)(\d+)\s*\z/, output) do
281 362
      [_, pid] -> {:ok, String.to_integer(pid)}
test/openagents/box_client_runs_test.exs modified +74 -4

@@ -24,22 +24,28 @@ defmodule OpenAgents.BoxClientRunsTest do

24 24
25 25
  test "dispatch uses one detached mkdir-and-launch command", do: begin_dispatch()
26 26
27
  test "assignment dispatch keeps credential setup outside the child shell" do
27
  test "assignment dispatch carries the credential in the command, never in env" do
28 28
    token = "oa_assignment_11111111-1111-4111-8111-111111111111.secret-token"
29 29
    run_directory = "/home/box-user/.openagents/box-runs"
30 30
31 31
    Req.Test.expect(__MODULE__, fn request ->
32 32
      command = request.body_params["command"]
33 33
34
      assert request.body_params["env"] == %{"OPENAGENTS_FORGE_TOKEN" => token}
34
      # The provider's CommandRequest schema has no `env`, so a credential sent
35
      # there is dropped and the wrapper dies on an unbound variable.
36
      refute Map.has_key?(request.body_params, "env")
37
      refute command =~ "OPENAGENTS_FORGE_TOKEN"
38
39
      # The credential reaches the box inside the command, base64 only so a
40
      # token cannot break the surrounding shell quoting.
35 41
      refute command =~ token
42
      assert command =~ Base.encode64("https://x:#{token}@openagents.com\n")
36 43
37 44
      assert command =~ "root=#{run_directory}"
38 45
      assert command =~ ~s(> "$root/forge-credential")
39 46
      assert command =~ ~s(git config --file="$root/gitconfig")
40 47
      assert command =~ ~s(env GIT_CONFIG_GLOBAL="$root/gitconfig")
41
42
      assert command =~ "unset OPENAGENTS_FORGE_TOKEN"
48
      assert command =~ "umask 077"
43 49
      refute command =~ "credential_setup"
44 50
45 51
      script_path =

@@ -65,6 +71,70 @@ defmodule OpenAgents.BoxClientRunsTest do

65 71
             )
66 72
  end
67 73
74
  test "the credential setup a box executes reconstructs the git credential file" do
75
    token = "oa_assignment_11111111-1111-4111-8111-111111111111.secret-token"
76
    encoded = Base.encode64("https://x:#{token}@openagents.com\n")
77
78
    assert {output, 0} =
79
             System.cmd("sh", ["-c", "printf '%s' '#{encoded}' | base64 -d"])
80
81
    assert output == "https://x:#{token}@openagents.com\n"
82
  end
83
84
  test "a 2xx without a pid is logged bounded and with the credential struck out" do
85
    token = "oa_assignment_11111111-1111-4111-8111-111111111111.secret-token"
86
87
    Req.Test.expect(__MODULE__, fn request ->
88
      Req.Test.json(request, %{
89
        "exit_code" => 1,
90
        "stdout" => "",
91
        "stderr" => "bash: line 12: refused with #{token}\nsecond line\n",
92
        "timed_out" => false
93
      })
94
    end)
95
96
    log =
97
      ExUnit.CaptureLog.capture_log(fn ->
98
        assert {:error, :box_response_invalid} =
99
                 Client.dispatch_run(
100
                   "bx_8bhkse3n",
101
                   "11111111-1111-4111-8111-111111111111",
102
                   "git push https://openagents.com/repo.git",
103
                   "/home/box-user/.openagents/box-runs",
104
                   token
105
                 )
106
      end)
107
108
    assert log =~ "box_dispatch_response_invalid"
109
    assert log =~ "box_id=bx_8bhkse3n"
110
    assert log =~ "run_id=11111111-1111-4111-8111-111111111111"
111
    assert log =~ "exit_code=1"
112
    assert log =~ "stdout_bytes=0"
113
    assert log =~ "[redacted]"
114
    refute log =~ token
115
    refute log =~ "second line"
116
  end
117
118
  test "an uncredentialed 2xx without a pid is logged without a credential" do
119
    Req.Test.expect(__MODULE__, fn request ->
120
      Req.Test.json(request, %{"exit_code" => 73, "stdout" => "OPENAGENTS_RUN_EXISTS\n"})
121
    end)
122
123
    log =
124
      ExUnit.CaptureLog.capture_log(fn ->
125
        assert {:error, :box_response_invalid} =
126
                 Client.dispatch_run(
127
                   "bx_8bhkse3n",
128
                   "11111111-1111-4111-8111-111111111111",
129
                   "echo detached"
130
                 )
131
      end)
132
133
    assert log =~ "box_dispatch_response_invalid"
134
    assert log =~ "exit_code=73"
135
    assert log =~ "keys=exit_code,stdout"
136
  end
137
68 138
  test "poll decodes bounded output from a recorded offset" do
69 139
    encoded = Base.encode64("hello")
70 140

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