Clone an SCV's repository onto the durable volume

1bacb84d05ba · AtlantisPleb · · parent 2f3666181a7a

Clone an SCV's repository onto the durable volume

The SCV workspace root was `System.tmp_dir!()`, hardcoded. Inside the release
container that is the writable image layer on the boot disk -- the same 20 GB
the node shares with Docker images and the import workspace, and the disk that
already ran out of room during a repository import today. Meanwhile the
durable volume that exists for exactly this kind of data sat idle, because the
only paths pointed at it are the ones `RuntimeConfig` already refuses to leave
under `/tmp`.

The root is now `OPENAGENTS_SCV_TEMPORARY_ROOT`, and a staging or production
node with an SCV lane enabled refuses to boot with a clone root under `/tmp`.
The clone is the largest thing the lane writes, so it belongs with the other
durable directories rather than one repository away from filling a node.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016o8HwTaqLKEWCHTjsjFtrB
Co-Authored-By
Claude Opus 5 (1M context) <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 INVARIANTS.md
  • modified config/runtime.exs
  • modified docs/runtime-configuration.md
  • modified lib/openagents/runtime_config.ex
  • modified test/openagents/runtime_config_test.exs

Diff

5 files changed, +50 -2

INVARIANTS.md modified +6 -1

@@ -1088,7 +1088,12 @@ than assumed.

1088 1088
  resolved by the application. The caller names a repository the operator may
1089 1089
  read as `owner/name`; a filesystem path from a caller never reaches an SCV.
1090 1090
  The workspace is removed on every terminal path, including the one that runs
1091
  when the worker died.
1091
  when the worker died. In staging and production that workspace must sit on a
1092
  durable path: a container's `System.tmp_dir!()` is the writable image layer
1093
  on the boot disk, which the node already shares with Docker and the import
1094
  workspace, so a repository cloned there is how a node runs out of room while
1095
  its durable volume idles. Configuration names the root, and a node refuses
1096
  to boot with an SCV lane enabled and a clone root under `/tmp`.
1092 1097
- **No job may deploy one.** `scv.deploy` is a turn authority only. Job
1093 1098
  authorities never include it, so neither a deep-work job, a delegation, a
1094 1099
  coding job, nor an SCV can start another SCV.
config/runtime.exs modified +3 -1

@@ -228,7 +228,9 @@ if config_env() == :prod and runtime_role == :web do

228 228
    file_root:
229 229
      optional_text.("OPENAGENTS_SCV_CODEX_FILE_ROOT") ||
230 230
        Application.fetch_env!(:openagents, :scv_codex)[:file_root],
231
    temporary_root: System.tmp_dir!(),
231
    temporary_root:
232
      optional_text.("OPENAGENTS_SCV_TEMPORARY_ROOT") ||
233
        Application.fetch_env!(:openagents, :scv_codex)[:temporary_root],
232 234
    client_options: []
233 235
  ]
234 236
docs/runtime-configuration.md modified +1

@@ -115,6 +115,7 @@ arguments, and the compiled defaults are the safe values.

115 115
| `OPENAGENTS_SCV_DEPLOY_OPENCODE_BIN` | Absolute path to the pinned OpenCode executable; the release image uses `/usr/local/bin/opencode` |
116 116
| `OPENAGENTS_SCV_DEPLOY_OPENCODE_API_KEY` | Optional OpenCode gateway key; the default model runs without one |
117 117
| `OPENAGENTS_SCV_DEPLOY_OUTPUT_ROOT` | Durable directory for run artifacts; must not be under `/tmp` |
118
| `OPENAGENTS_SCV_TEMPORARY_ROOT` | Durable directory for SCV repository clones; must not be under `/tmp` when an SCV lane is enabled in staging or production |
118 119
119 120
The compiled defaults cap concurrency at two simultaneous SCVs, the wall clock
120 121
at 15 minutes, and captured output at 16 MB. The lane runs read-only against a
lib/openagents/runtime_config.ex modified +10

@@ -459,6 +459,16 @@ defmodule OpenAgents.RuntimeConfig do

459 459
      features.scv_deploy and not valid_scv_deploy?(Map.get(settings, :scv_deploy)) ->
460 460
        error(:scv_deploy, "requires an admitted model, bounds, and output root")
461 461
462
      # An SCV clones the repository before it runs, and that clone is the
463
      # largest thing the lane writes. `System.tmp_dir!()` inside a container
464
      # is the writable image layer on the boot disk, which the node already
465
      # shares with Docker and the import workspace; a repository landing
466
      # there is how a node runs out of room while its durable volume idles.
467
      environment in [:staging, :production] and
468
        (features.scv_deploy or Map.get(settings, :scv_codex, [])[:enabled] == true) and
469
          not durable_path?(keyword_value(Map.get(settings, :scv_codex), :temporary_root)) ->
470
        error(:scv_temporary_root, "must be durable when an SCV lane is enabled")
471
462 472
      features.forge_deploy and not features.forge ->
463 473
        error(:forge_deploy_lane_enabled, "requires the forge")
464 474
test/openagents/runtime_config_test.exs modified +30

@@ -247,6 +247,36 @@ defmodule OpenAgents.RuntimeConfigTest do

247 247
    end
248 248
  end
249 249
250
  test "an SCV lane refuses a clone root on the container layer" do
251
    # Gate 14 admits the lane; the clone root is what decides whether the
252
    # repository lands on the durable volume or on the boot disk.
253
    settings =
254
      staging_settings()
255
      |> Map.put(:staging_gate, 14)
256
      |> Map.put(:build_revision, String.duplicate("a", 40))
257
      |> Map.put(:image_digest, "sha256:" <> String.duplicate("b", 64))
258
      |> put_nested(:work, :enabled, true)
259
      |> Map.put(:work_workers_enabled, true)
260
      |> put_nested(:scv_deploy, :enabled, true)
261
262
    assert {:error, %{setting: :scv_temporary_root}} =
263
             settings
264
             |> put_nested(:scv_codex, :temporary_root, "/tmp")
265
             |> RuntimeConfig.validate()
266
267
    assert {:error, %{setting: :scv_temporary_root}} =
268
             settings
269
             |> put_nested(:scv_codex, :temporary_root, "/tmp/openagents-scv")
270
             |> RuntimeConfig.validate()
271
272
    assert {:ok, config} =
273
             settings
274
             |> put_nested(:scv_codex, :temporary_root, "/var/lib/openagents/workspace/scv")
275
             |> RuntimeConfig.validate()
276
277
    assert RuntimeConfig.feature_enabled?(config, :scv_deploy)
278
  end
279
250 280
  defp staging_settings do
251 281
    current = Map.new(Application.get_all_env(:openagents))
252 282

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