|
1
|
+ |
# Sarah, connected computers, and SCVs
|
|
2
|
+ |
|
|
3
|
+ |
**Date:** 2026-08-21
|
|
4
|
+ |
**Commit measured:** `50b882b8d909` (origin/main)
|
|
5
|
+ |
**Question:** How does Sarah interact with connected computers, how is the SCV system implemented, are the two connected, and what stands between today's code and Sarah deploying SCVs that run on system capacity instead of a person's own machine?
|
|
6
|
+ |
**Method:** direct reading of `lib/openagents/computer.ex`, `lib/openagents/computer_agent_jobs.ex`, `lib/openagents/work/`, `lib/openagents_web/channels/`, `lib/openagents/machines*`, `lib/openagents/tools/`, the whole `lib/openagents/scv/` tree, `lib/openagents/application.ex`, `config/`, `priv/repo/migrations/`, `ops/scv/`, and `infra/staging/`, plus bidirectional reference searches between the two subsystems. Every architectural claim cites a file and line current at the measured commit. Claims that could not be established from code are collected in section 7 rather than guessed at. Where `docs/scv-planning.md` describes behavior the code does not implement, section 6 says so.
|
|
7
|
+ |
|
|
8
|
+ |
---
|
|
9
|
+ |
|
|
10
|
+ |
## 0. Verdict
|
|
11
|
+ |
|
|
12
|
+ |
Both systems exist, both work, and they share no module, schema, job kind, or receipt type.
|
|
13
|
+ |
|
|
14
|
+ |
The connected-computer path is a complete remote execution system: a paired machine holds an authenticated WebSocket to the fleet, Sarah addresses it by machine id through a cluster-wide registry, a durable `work_jobs` row survives node death and resumes the remote session, and output streams into chat while it runs. What it lacks is capacity of our own — every delegation ends on hardware the customer owns and powers.
|
|
15
|
+ |
|
|
16
|
+ |
The SCV system is a complete execution *contract*: a typed run, an admitted environment with declared capabilities, a permission profile, pluggable drivers, sanitized events, and a durable terminal row. What it lacks is a way in. It splits into two lanes that share a struct and share almost nothing else, and neither lane can be started by Sarah:
|
|
17
|
+ |
|
|
18
|
+ |
- **Lane A, OpenCode.** A separate BEAM release booted in `runtime_role: :scv` inside a container. Its objective arrives as `SCV_OBJECTIVE` in the process environment (`lib/openagents/scv/worker.ex:61`), so submitting work means launching a container. It writes no database rows at all.
|
|
19
|
+ |
- **Lane B, Codex app server.** Durable, fenced, leased, reaped — and it runs as a `Port` child of the *web* node (`lib/openagents/runtime_supervisor.ex:35`). Its dispatcher, `OpenAgents.SCV.CodexRuns.start/5` (`lib/openagents/scv/codex_runs.ex:13`), has no caller anywhere in `lib/`. Only tests call it.
|
|
20
|
+ |
|
|
21
|
+ |
So the two halves of the owner's goal are already built, in separate places, facing away from each other:
|
|
22
|
+ |
|
|
23
|
+ |
| Half the goal needs | Where it lives today | What it is missing |
|
|
24
|
+ |
|---|---|---|
|
|
25
|
+ |
| Remote dispatch, streaming, durable jobs, recovery | the connected-computer path | our own capacity to dispatch to |
|
|
26
|
+ |
| Bounded execution with capability admission and receipts | the SCV system | a runtime entry point Sarah can call |
|
|
27
|
+ |
|
|
28
|
+ |
Neither is missing machinery the other has not already solved once. The work is joining them, and the join has a named seam on each side: `OpenAgents.Work.Job`'s `kind` field (`lib/openagents/work/job.ex:18`), and the `OpenAgents.SCV.Runner` behaviour (`lib/openagents/scv/runner.ex:6`), which today has exactly one implementation and it is in-process (`lib/openagents/scv/runner/local.ex:18`).
|
|
29
|
+ |
|
|
30
|
+ |
The precedent for the whole shape already shipped. The `coding` job kind runs Sarah's own multi-step work on our fleet, on our capacity, with its own approval class, its own sandbox, and metering into the shared grant ledger (`lib/openagents/work/coding.ex:1`). Sarah-deployed SCVs are that pattern again with a different executor.
|
|
31
|
+ |
|
|
32
|
+ |
One caveat frames everything below: an SCV worker refuses to boot in production. `config/runtime.exs:418` raises `"an SCV worker is admitted only in staging"` unless `OPENAGENTS_ENVIRONMENT` is `staging`, and the Codex lane's feature flag defaults to `false` (`config/config.exs:57`). Today's SCV system is a staging capability, not a production one.
|
|
33
|
+ |
|
|
34
|
+ |
---
|
|
35
|
+ |
|
|
36
|
+ |
## 1. Sarah's connected-computer path
|
|
37
|
+ |
|
|
38
|
+ |
### 1.1 Pairing and identity
|
|
39
|
+ |
|
|
40
|
+ |
A computer becomes addressable through a three-legged, code-based, owner-approved pairing flow (`lib/openagents/machines.ex:2`): the CLI starts a pairing unauthenticated, the signed-in account owner approves it in the browser, and the CLI claims it once with a poll secret.
|
|
41
|
+ |
|
|
42
|
+ |
1. **Start.** `POST /controller/pairings` (`lib/openagents_web/router.ex:215`) generates an eight-character code from an ambiguity-free alphabet (`lib/openagents/machines.ex:18`) and a 32-byte poll secret, storing only SHA-256 digests of both (`lib/openagents/machines.ex:33`). The pairing lives 600 seconds (`lib/openagents/machines.ex:17`).
|
|
43
|
+ |
2. **Approve.** The owner approves at `/computers` (`lib/openagents_web/router.ex:105`). Inside one transaction, `do_approve_pairing/3` locks the pairing and user rows, enforces a cap of eight active machines (`lib/openagents/machines.ex:20`), mints a token `smct_` plus 32 random bytes (`lib/openagents/machines.ex:80`), seals the plaintext with AES-256-GCM (`lib/openagents/machines/token_vault.ex:13`), and stores only the digest.
|
|
44
|
+ |
3. **Claim.** `GET /controller/pairings/:id` with an `x-pairing-secret` header compares the poll-secret digest with `Plug.Crypto.secure_compare` (`lib/openagents/machines.ex:128`), returns the plaintext token once, and wipes the ciphertext (`lib/openagents/machines.ex:332`).
|
|
45
|
+ |
|
|
46
|
+ |
The durable identity is the `machines` table (`lib/openagents/machines/machine.ex:13`). Two of its fields carry authority rather than description:
|
|
47
|
+ |
|
|
48
|
+ |
- `tier`, a capability ladder validated against `probe`, `curated`, and `shell` (`lib/openagents/machines/machine.ex:11`) and re-checked by a database constraint (`priv/repo/migrations/20260817140345_create_machines.exs:25`).
|
|
49
|
+ |
- `roots`, the array of filesystem paths the owner admitted, capped at 16 entries of 512 bytes (`lib/openagents/machines/machine.ex:50`).
|
|
50
|
+ |
|
|
51
|
+ |
This is unrelated to the `device_authorizations` table, which is a separate OAuth-device-style flow minting `forge:write` API tokens for the OpenAgents CLI (`lib/openagents/device_authorizations/device_authorization.ex:15`). No code path links the two.
|
|
52
|
+ |
|
|
53
|
+ |
### 1.2 Transport
|
|
54
|
+ |
|
|
55
|
+ |
The transport is a Phoenix socket mounted at `/controller/socket` (`lib/openagents_web/endpoint.ex:25`), serving one channel topic pattern, `computer:*` (`lib/openagents_web/channels/controller_socket.ex:15`).
|
|
56
|
+ |
|
|
57
|
+ |
Authentication happens in `connect/3`, which requires the feature flag and a machine token and assigns only identifiers (`lib/openagents_web/channels/controller_socket.ex:19`). `Machines.authenticate_token/1` requires the `smct_` prefix, a length under 128 bytes, a digest match, `status == "active"`, and an unexpired token (`lib/openagents/machines.ex:144`). The socket id is `controller_socket:#{machine_id}` (`lib/openagents_web/channels/controller_socket.ex:34`), so a single machine can be disconnected by id.
|
|
58
|
+ |
|
|
59
|
+ |
Joining re-authorizes three times over (`lib/openagents_web/channels/computer_channel.ex:22`): the topic's machine id must equal the socket's own, the machine must still resolve for that user, and its status must be `active`. A successful join registers the channel, subscribes to the machine's PubSub topic, records the sighting, schedules token expiry, and returns the protocol version `openagents.computer.v1`.
|
|
60
|
+ |
|
|
61
|
+ |
Message names are explicit in both directions. The server pushes `probe`, `cancel`, and the request kind stringified — `run`, `devin`, or `agent` (`lib/openagents_web/channels/computer_channel.ex:129`). The controller sends `hello`, `probe_result`, `probe_refused`, `chunk`, `session`, `exit`, and `refused` (`lib/openagents_web/channels/computer_channel.ex:50`–`110`); anything else is refused as `unknown_event` (`lib/openagents_web/channels/computer_channel.ex:117`). Every inbound payload is JSON-encoded and measured against a 64 KB ceiling (`lib/openagents_web/channels/computer_channel.ex:19`). Every request carries a server-minted `request_id`.
|
|
62
|
+ |
|
|
63
|
+ |
### 1.3 Addressing across the fleet
|
|
64
|
+ |
|
|
65
|
+ |
`OpenAgents.Computer` registers each connected machine's channel process under `{:machine, machine_id}` in the cluster-wide `OpenAgents.HordeRegistry` (`lib/openagents/computer.ex:2`). Channel pids are location-transparent, so any node can reach a controller whose WebSocket terminates elsewhere. Timeouts are tiered: 15 seconds for a probe, 120 seconds for a shell run, one hour for an agent delegation (`lib/openagents/computer.ex:24`).
|
|
66
|
+ |
|
|
67
|
+ |
### 1.4 How Sarah dispatches
|
|
68
|
+ |
|
|
69
|
+ |
Sarah reaches computers through five installed tools — `computer_list`, `computer_probe`, `computer_run`, `computer_devin`, and `computer_agent` — registered in the catalogue at `config/config.exs:138`. Three of them are always included when the person has a paired machine (`lib/openagents/tools/selector.ex:21`). All declare `required_scope: "browser_conversation"` and `required_authority: "computer.control"` (`lib/openagents/tools/computer_agent.ex:53`).
|
|
70
|
+ |
|
|
71
|
+ |
The durable coding delegation converges on one owner-scoped entry point, `OpenAgents.ComputerAgentJobs.start/5`, which both the `computer_agent` tool and the signed-in Computers API enter so that ownership, presence, probe evidence, and directory scope cannot drift between surfaces (`lib/openagents/computer_agent_jobs.ex:2`). It runs six checks before any work starts (`lib/openagents/computer_agent_jobs.ex:34`): the feature is enabled, the user owns both the machine and the conversation, the machine is `active` and online, the requested `agent_id` appears in the machine's last probe inventory, the prompt is within bounds, and the working directory sits inside an admitted root with no `..` segments (`lib/openagents/computer_agent_jobs.ex:133`).
|
|
72
|
+ |
|
|
73
|
+ |
Only then does it build the durable row, capturing three maps at start time (`lib/openagents/computer_agent_jobs.ex:42`): `delegation` (the request), `authority_snapshot` (machine tier, admitted roots, working directory, agent id, machine name), and `budget_snapshot` (one-hour wall clock, 8,000-byte prompt cap, 8,000-byte report cap).
|
|
74
|
+ |
|
|
75
|
+ |
The durable record is `OpenAgents.Work.Job`, table `work_jobs`. Its `kind` is validated against exactly three values (`lib/openagents/work/job.ex:18`):
|
|
76
|
+ |
|
|
77
|
+ |
```elixir
|
|
78
|
+ |
@kinds ~w(deep_work delegation coding)
|
|
79
|
+ |
```
|
|
80
|
+ |
|
|
81
|
+ |
and its `status` against seven (`lib/openagents/work/job.ex:15`): `queued`, `running`, `completed`, `failed`, `interrupted`, `budget_exhausted`, `cancelled`. The row also carries `owner_node` and `generation` (`lib/openagents/work/job.ex:50`), which are the fencing tokens for recovery.
|
|
82
|
+ |
|
|
83
|
+ |
Postgres enforces the invariants rather than trusting the application. A trigger makes identity immutable and status transitions forward-only (`priv/repo/migrations/20260818003358_create_work_jobs.exs:58`), a constraint requires terminal jobs to carry a report (`priv/repo/migrations/20260818003358_create_work_jobs.exs:53`), and a second trigger re-verifies on every write that the conversation owner matches, that the machine belongs to that owner through `visitors.user_id`, and that the authority snapshot equals the machine's live `tier`, `roots`, and `name` (`priv/repo/migrations/20260820085203_harden_async_runtime_boundaries.exs:83`).
|
|
84
|
+ |
|
|
85
|
+ |
Each job runs as a cluster-wide singleton under Horde, so the supervisor relocates it to a survivor when its node dies (`lib/openagents/work.ex:113`). A `delegation` job is driven by `OpenAgents.Work.DelegationServer`, a `:transient` GenServer that runs one `Computer.request_agent` call to completion in its own process so the requesting turn returns immediately and several delegations run at once (`lib/openagents/work/delegation_server.ex:1`).
|
|
86
|
+ |
|
|
87
|
+ |
### 1.5 Streaming back into chat
|
|
88
|
+ |
|
|
89
|
+ |
While a delegation runs, `OpenAgents.ComputerActivity` re-broadcasts a bounded projection on the owner's conversation topic, `computer_live:#{conversation_id}` (`lib/openagents/computer_activity.ex:44`), emitting four event types: started, chunk, truncated, and terminal. The module is explicit that this is a projection and never the authority — nothing broadcast is persisted, a missed broadcast loses nothing durable, and a reload mid-delegation degrades to status only (`lib/openagents/computer_activity.ex:9`). Caps are 16 KB per event, 64 KB cumulative, 512 chunk events (`lib/openagents/computer_activity.ex:33`).
|
|
90
|
+ |
|
|
91
|
+ |
`ChatLive` subscribes at mount and keeps one live panel at a time, superseding older delegations into a bounded summary list (`lib/openagents_web/live/chat_live.ex:269`). Chunk text never enters an assign; it is pushed to the client as a `delegation:chunk` event and rendered by a colocated hook (`lib/openagents_web/live/chat_live.ex:313`).
|
|
92
|
+ |
|
|
93
|
+ |
The durable report goes through `OpenAgents.Computer.AcpTranscript`, which decodes the controller's framed progress stream — record separator `0x1E`, fields split on unit separator `0x1F` — into readable prose and tool lines (`lib/openagents/computer/acp_transcript.ex:1`). Its moduledoc names the bug it exists to prevent: durable reports used to post the raw bytes, so a timeout wrote wire framing into the conversation. It labels eight tool kinds — `execute`, `read`, `edit`, `search`, `fetch`, `think`, `move`, `delete` (`lib/openagents/computer/acp_transcript.ex:16`) — and lists started-but-unfinished tools as in progress.
|
|
94
|
+ |
|
|
95
|
+ |
When the job finishes, `Work.finish_job/3` inserts an assistant `Message` carrying `work_job_id` inside the same transaction and links it back as `report_message_id` (`lib/openagents/work.ex:483`), so the report is a durable chat message rather than a broadcast.
|
|
96
|
+ |
|
|
97
|
+ |
### 1.6 Lifecycle and recovery
|
|
98
|
+ |
|
|
99
|
+ |
- **Cancel** is a first-class message. `ChatLive` calls `Work.cancel_active_delegations/2` (`lib/openagents_web/live/chat_live.ex:97`), which casts to the Horde singleton; the server kills the task, appends a cancellation note, and finishes the job `cancelled` (`lib/openagents/work/delegation_server.ex:59`).
|
|
100
|
+ |
- **Caller death** cancels the remote work: the channel monitors the requesting process and pushes `cancel` on `:DOWN` (`lib/openagents_web/channels/computer_channel.ex:144`).
|
|
101
|
+ |
- **Timeout** produces a synthetic terminal marked `truncated` and pushes a cancel rather than leaving the remote agent running (`lib/openagents/computer.ex:160`).
|
|
102
|
+ |
- **Revocation** is enforced live: a `machine_revoked` broadcast terminates the channel (`lib/openagents_web/channels/computer_channel.ex:155`), and token expiry is scheduled at join (`lib/openagents_web/channels/computer_channel.ex:30`).
|
|
103
|
+ |
- **Reconnect races** are refused rather than double-registered: a stale registration from a dead node yields `machine_reconnecting` and the controller's retry loop handles it (`lib/openagents_web/channels/computer_channel.ex:37`).
|
|
104
|
+ |
- **Node death** is survivable and, for delegations, *resumable*. `Work.recover_interrupted_jobs/0` runs at boot (`lib/openagents/work_recovery.ex:12`), records a `runtime_restarted` incident, and restarts each active job's worker with backoff, only marking a job `interrupted` after retries are exhausted (`lib/openagents/work.ex:566`). The restarted `DelegationServer` adopts the row through the generation fence and re-attaches the same session using a durably checkpointed session id held in both the job row and Ra (`lib/openagents/work.ex:645`), waiting up to 90 seconds for the controller to re-register (`lib/openagents/computer.ex:183`).
|
|
105
|
+ |
|
|
106
|
+ |
### 1.7 The trust boundary
|
|
107
|
+ |
|
|
108
|
+ |
The boundary is the person's explicit pairing, expressed as a receipt rather than a flag. `Machines.approval_receipts/2` mints one `sarah.module_approval.v1` receipt per active, unexpired machine per machine-effect module, with `approval_class` set to `explicit_operator_approval`, `actor_type` `operator`, and `receipt_ref` pointing at the machine (`lib/openagents/machines.ex:204`). The moduledoc is explicit that approving the pairing on `/computers` *is* the operator approval (`lib/openagents/machines.ex:196`).
|
|
109
|
+ |
|
|
110
|
+ |
Enforcement is real, not advisory. `OpenAgents.Modules.SurfacePolicy` requires a receipt whose schema, approval class, module id, version, scope ref, explicitness, and actor type all match the artifact, and returns `{:error, :module_approval_required}` otherwise (`lib/openagents/modules/surface_policy.ex:114`).
|
|
111
|
+ |
|
|
112
|
+ |
Two honest limits are worth recording:
|
|
113
|
+ |
|
|
114
|
+ |
- **Tier is carried but never enforced server-side.** `machine.tier` is snapshotted into `authority_snapshot` and cross-checked against the live row by a database trigger, but no code in `lib/` refuses a `run` or `agent` request because the tier is too low. Both tool moduledocs state that the machine's own policy decides and can return a typed refusal (`lib/openagents/tools/computer_agent.ex:8`). The final authority lives in the controller, which is a separate repository, `OpenAgentsInc/sarah-computer-controller` (`lib/openagents/tools/computer_agent.ex:63`), not in this codebase.
|
|
115
|
+ |
- **`computer_run` does not check `cwd` against `roots`.** Unlike `ComputerAgentJobs`, the synchronous run tool only bounds its argv (`lib/openagents/tools/computer_run.ex:91`); path scoping for that path is delegated entirely to the controller.
|
|
116
|
+ |
|
|
117
|
+ |
There is no per-delegation approval prompt anywhere in this repository. The one human approval is the one-time pairing.
|
|
118
|
+ |
|
|
119
|
+ |
---
|
|
120
|
+ |
|
|
121
|
+ |
## 2. The SCV system
|
|
122
|
+ |
|
|
123
|
+ |
### 2.1 What an SCV is
|
|
124
|
+ |
|
|
125
|
+ |
SCV means Space Construction Vehicle (`docs/scv-planning.md:17`). That expansion appears only in the planning document; no code carries it.
|
|
126
|
+ |
|
|
127
|
+ |
In code, an SCV is a bounded, single-objective execution of a coding agent against one repository under a declared capability set and permission profile. `OpenAgents.SCV` states the ownership directly (`lib/openagents/scv.ex:2`):
|
|
128
|
+ |
|
|
129
|
+ |
> The SCV owns the run contract. A driver supplies one coding implementation, such as OpenCode, and an environment supplies the admitted runtime capabilities. Callers deploy SCVs rather than deploying drivers directly.
|
|
130
|
+ |
|
|
131
|
+ |
The facade is one line of indirection (`lib/openagents/scv.ex:13`):
|
|
132
|
+ |
|
|
133
|
+ |
```elixir
|
|
134
|
+ |
def run(%Run{runner_module: runner} = run), do: runner.run(run)
|
|
135
|
+ |
```
|
|
136
|
+ |
|
|
137
|
+ |
The request struct is `OpenAgents.SCV.Run` (`lib/openagents/scv/run.ex:21`), requiring ten fields including `driver_module`, `environment`, `runner_module`, `runner_id`, `permission_profile`, and `capabilities`. `Run.new/3` performs capability admission: it computes `driver_module.required_capabilities(profile)` and rejects with `:capability_mismatch` if the environment does not cover it (`lib/openagents/scv/run.ex:71`). The objective and driver options are excluded from `Inspect` output (`lib/openagents/scv/run.ex:6`).
|
|
138
|
+ |
|
|
139
|
+ |
### 2.2 Environment and drivers: the capability contract
|
|
140
|
+ |
|
|
141
|
+ |
`OpenAgents.SCV.Environment` describes what an SCV may do, and is explicit that the environment id selects an immutable image at deployment time and does not identify the SCV or its driver (`lib/openagents/scv/environment.ex:2`). Two environments are admitted, and the pairing of each to an image is the clearest statement of where each lane runs:
|
|
142
|
+ |
|
|
143
|
+ |
| Environment | Image | Capabilities | Runtimes |
|
|
144
|
+ |
|---|---|---|---|
|
|
145
|
+ |
| `opencode-core` (`lib/openagents/scv/environment.ex:20`) | `scv-opencode-core` | inference, egress, execute, workspace read **and write** | elixir, git, node, bun, python |
|
|
146
|
+ |
| `codex-app-server` (`lib/openagents/scv/environment.ex:36`) | `openagents` | inference, egress, execute, workspace read | codex, git |
|
|
147
|
+ |
|
|
148
|
+ |
The second one names the *web* image. The Codex lane is not a separate container at all.
|
|
149
|
+ |
|
|
150
|
+ |
`OpenAgents.SCV.Driver` is a three-callback behaviour whose moduledoc sets the boundary: drivers may use different tool protocols, but every driver runs inside the same SCV lifecycle, capability, event, and receipt boundary (`lib/openagents/scv/driver.ex:2`). Selection is an allowlist returning `:driver_not_admitted` for anything else (`lib/openagents/scv/driver.ex:16`).
|
|
151
|
+ |
|
|
152
|
+ |
Both drivers spawn a local operating-system process with `Port.open` and `{:spawn_executable, …}` (`lib/openagents/scv/executor/open_code.ex:219`, `lib/openagents/scv/codex_app_server.ex:176`), and both scrub the inherited environment by mapping every existing variable to `false` before merging a small allowlist (`lib/openagents/scv/executor/open_code.ex:672`, `lib/openagents/scv/codex_app_server.ex:198`). **Neither driver has any remote dispatch.** An SCV always runs an OS process on the same host as the BEAM that called it.
|
|
153
|
+ |
|
|
154
|
+ |
Their sandboxes differ in kind:
|
|
155
|
+ |
|
|
156
|
+ |
- **OpenCode** runs a one-shot CLI and parses JSON lines from stdout, with a tool permission map that denies `bash`, `edit`, `external_directory`, `lsp`, `question`, `skill`, `task`, `webfetch`, and `websearch`, allowing only `glob`, `grep`, `list`, and `read` (`lib/openagents/scv/executor/open_code.ex:724`). The `:workspace_write` profile flips `edit` to allow (`lib/openagents/scv/executor/open_code.ex:742`). The process keeps network egress; only the tools are denied.
|
|
157
|
+ |
- **Codex app server** speaks bidirectional JSONL JSON-RPC over stdio, keeps stdout protocol-only, and rejects server requests it does not implement with `-32601` (`lib/openagents/scv/codex_app_server.ex:141`). It writes a per-run `config.toml` setting `approval_policy = "never"`, pinning the workspace root, and disabling network entirely (`lib/openagents/scv/executor/codex_app_server.ex:670`), then asserts the server echoed back the expected permission profile or fails (`lib/openagents/scv/executor/codex_app_server.ex:243`). It also enforces a structured `outputSchema` and re-validates the result locally (`lib/openagents/scv/executor/codex_app_server.ex:264`).
|
|
158
|
+ |
|
|
159
|
+ |
Credentials differ accordingly. OpenCode needs only `OPENAI_API_KEY` from the process environment, persisted nowhere. Codex needs a whole ChatGPT credential home, held behind the `OpenAgents.SCV.CodexCredentialStore` behaviour with two implementations: a `chmod 0600` local file (`lib/openagents/scv/codex_credential_store/file.ex:40`) and GCP Secret Manager, which pins reads to a stored integer version rather than `latest` (`lib/openagents/scv/codex_credential_store/gcp_secret_manager.ex:30`). Per run, the credential is materialized into an ephemeral `CODEX_HOME` and removed in an `after` block (`lib/openagents/scv/executor/codex_app_server.ex:27`), and its secrets are scraped into a redaction list applied to all report text (`lib/openagents/scv/executor/codex_app_server.ex:696`).
|
|
160
|
+ |
|
|
161
|
+ |
### 2.3 Lane A: the OpenCode container
|
|
162
|
+ |
|
|
163
|
+ |
`OpenAgents.Application.start/2` branches the *entire* boot on `:runtime_role`, defaulting to `:web` (`lib/openagents/application.ex:10`):
|
|
164
|
+ |
|
|
165
|
+ |
```elixir
|
|
166
|
+ |
case Application.get_env(:openagents, :runtime_role, :web) do
|
|
167
|
+ |
:scv -> start_scv_worker()
|
|
168
|
+ |
```
|
|
169
|
+ |
|
|
170
|
+ |
The `:scv` role starts nothing else — no Repo, no Endpoint, no runtime supervisor. It starts one `:temporary` Task (`lib/openagents/application.ex:81`) that calls `SCV.Worker.run_from_env!()` (`lib/openagents/application.ex:118`) and then halts the VM with `System.stop/1` (`lib/openagents/application.ex:126`). The whole node exists to run one SCV and exit.
|
|
171
|
+ |
|
|
172
|
+ |
Its input is the process environment. `parse_environment/1` requires `SCV_REPOSITORY`, `SCV_OBJECTIVE`, and `OPENAI_API_KEY`, and allowlists `SCV_DRIVER` to `opencode` only, `SCV_ENVIRONMENT` to `opencode-core` only, and `SCV_PERMISSION_PROFILE` to `read_only` only (`lib/openagents/scv/worker.ex:60`). The container therefore cannot run the Codex driver or write to a workspace.
|
|
173
|
+ |
|
|
174
|
+ |
The image bakes those values plus `OPENAGENTS_RUNTIME_ROLE=scv`, runs as uid 10001, and copies the repository into itself at `/workspace/openagents` with `SCV_REPOSITORY_REVISION` set to the build revision (`ops/scv/images/opencode-core/Dockerfile:164`). Base images are pinned by digest against a Debian snapshot (`ops/scv/images/versions.env`). `SCV_OBJECTIVE` and `OPENAI_API_KEY` are deliberately not baked in.
|
|
175
|
+ |
|
|
176
|
+ |
**This lane writes no database rows.** There is no `Repo.` call anywhere in `worker.ex`, `runner/local.ex`, `driver/open_code.ex`, `executor/open_code.ex`, `run.ex`, or `scv.ex`. Results are JSON lines on stdout: chunked report frames followed by one `openagents.scv.worker.result.v1` carrying status, duration, event count, tool calls, usage, resources, and a sha256 digest (`lib/openagents/scv/worker.ex:239`).
|
|
177
|
+ |
|
|
178
|
+ |
### 2.4 Lane B: the durable Codex path, and its missing caller
|
|
179
|
+ |
|
|
180
|
+ |
The second lane is where the durable design lives. `OpenAgents.SCV.Execution` is the "durable authority and terminal receipt for one Codex-backed SCV run" (`lib/openagents/scv/execution.ex:2`), stored in table `scv_runs` (`lib/openagents/scv/execution.ex:16`). Terminal statuses are `succeeded`, `failed`, `cancelled`, and `uncertain` (`lib/openagents/scv/execution.ex:14`) — note `uncertain`, which the job side has no equivalent of. There is no `queued` state; a row is inserted already `running` (`lib/openagents/scv/execution.ex:80`).
|
|
181
|
+ |
|
|
182
|
+ |
`OpenAgents.SCV.Executions` "claims, fences, records, and completes durable SCV executions" (`lib/openagents/scv/executions.ex:2`). `claim/4` takes `FOR UPDATE` on the driver account, computes a monotonically increasing `generation`, records `owner_node`, and sets a 120-second lease (`lib/openagents/scv/executions.ex:24`). Every persisted event renews the lease under a fence of id, status, owner node, and generation, rolling back with `:stale_execution_generation` if the fence does not match exactly one row (`lib/openagents/scv/executions.ex:229`). `OpenAgents.SCV.ExecutionReaper` sweeps expired leases to `uncertain` every 30 seconds (`lib/openagents/scv/execution_reaper.ex:2`), gated on its own flag (`lib/openagents/runtime_supervisor.ex:54`).
|
|
183
|
+ |
|
|
184
|
+ |
Where does it run? `OpenAgents.SCV.CodexRun` is a `:temporary` GenServer started under `OpenAgents.SCV.CodexRunSupervisor`, a plain node-local `DynamicSupervisor` in the *web* supervision tree (`lib/openagents/runtime_supervisor.ex:35`). The Codex binary is a `Port` child of that same BEAM. Its workspace is a fresh `git clone --no-local --no-checkout` with `core.hooksPath=/dev/null`, detached at the requested revision, verified clean, and destroyed in an `after` block (`lib/openagents/scv/workspace.ex:23`).
|
|
185
|
+ |
|
|
186
|
+ |
The dispatcher is `OpenAgents.SCV.CodexRuns.start/5` (`lib/openagents/scv/codex_runs.ex:13`). **It has no caller in `lib/`.** A search for `CodexRuns.` outside its own module returns only `test/openagents/scv/codex_runs_test.exs`.
|
|
187
|
+ |
|
|
188
|
+ |
The lane is hard-wired read-only. The claim changeset forces `driver`, `permission_profile: "read_only"`, `model`, and `status` regardless of what the caller passed (`lib/openagents/scv/execution.ex:77`), and a database constraint independently restricts `permission_profile` to `read_only` (`priv/repo/migrations/20260821082652_create_scv_runs.exs:50`). There is no code path to a writing Codex SCV.
|
|
189
|
+ |
|
|
190
|
+ |
The only SCV web surface is `/admin/scv/accounts` (`lib/openagents_web/router.ex:185`), behind an operator pipeline that requires an admin user (`lib/openagents_web/router.ex:60`). Its two events are `connect_account` and `cancel_login` (`lib/openagents_web/live/admin_scv_accounts_live.ex:30`). It manages credentials, not runs.
|
|
191
|
+ |
|
|
192
|
+ |
### 2.5 Receipts, quotas, and observability
|
|
193
|
+ |
|
|
194
|
+ |
The SCV side persists more per run than the job side does, and it sanitizes harder.
|
|
195
|
+ |
|
|
196
|
+ |
Every event is filtered before insert: the schema must be exactly `openagents.scv.event.v1`, and the payload is reduced to a fixed 20-key allowlist (`lib/openagents/scv/executions.ex:16`) and rejected above 16 KB, with the database enforcing the same bound (`priv/repo/migrations/20260821082652_create_scv_runs.exs:89`). No file paths, tool arguments, tool output, or report prose reach an event row; thread and turn identifiers are one-way hashed (`lib/openagents/scv/executor/codex_app_server.ex:762`). The terminal row holds `report`, `report_digest`, `usage`, and `resources`, each size-capped and degrading to `%{"truncated" => true}` rather than failing (`lib/openagents/scv/executions.ex:321`).
|
|
197
|
+ |
|
|
198
|
+ |
Two quotas exist, and both are structural rather than policy:
|
|
199
|
+ |
|
|
200
|
+ |
1. **One active run per driver account**, enforced by a unique partial index on `(driver_account_id) WHERE status = 'running'` (`priv/repo/migrations/20260821082652_create_scv_runs.exs:41`) and mirrored in the changeset (`lib/openagents/scv/execution.ex:83`).
|
|
201
|
+ |
2. **A fixed number of driver accounts**, since `credential_refs` is a static configuration list (`config/config.exs:61`), and creating an account rolls back with `:account_capacity_reached` when no slot is free (`lib/openagents/scv/codex_accounts.ex:236`).
|
|
202
|
+ |
|
|
203
|
+ |
There is no rate limit, no token budget, no cost cap, and no per-user quota. `scv_runs` has **no user foreign key**; its only principal is a synthesized string, `"scv:codex_app_server:#{account.id}"` (`lib/openagents/scv/executions.ex:37`). Isolation is per driver account, not per person. The `issue_id` foreign key is the only link to a work source, and nothing in `lib/` ever sets it.
|
|
204
|
+ |
|
|
205
|
+ |
Nothing meters an SCV against a grant or a ledger. `OpenAgents.SCV.OpenCodeEvents` accumulates token counts and a `cost_usd` figure read from OpenCode's own output (`lib/openagents/scv/open_code_events.ex:25`), and the mix task prints it, but no code posts it anywhere. `OpenAgents.SCV.ResourceSampler` shells out to `ps` for bounded RSS and CPU, noting that container workers would replace the adapter with cgroup measurements while preserving the result shape (`lib/openagents/scv/resource_sampler.ex:2`). "Receipt" appears in moduledocs (`lib/openagents/scv/execution.ex:2`), but there is no receipt type.
|
|
206
|
+ |
|
|
207
|
+ |
Public visibility is deliberately thin. `OpenAgents.SCV.Activity` keeps at most 32 live entries for 30 seconds each, replaces each run id with a truncated hash, allowlists tool names to ten values, and maps each event type to a fixed English sentence (`lib/openagents/scv/activity.ex:158`). `NetworkStatus` merges that with a database projection for `/status` and `/api/status` (`lib/openagents/network_status.ex:282`).
|
|
208
|
+ |
|
|
209
|
+ |
---
|
|
210
|
+ |
|
|
211
|
+ |
## 3. The two paths side by side
|
|
212
|
+ |
|
|
213
|
+ |
| Axis | Connected computer | SCV lane A (OpenCode) | SCV lane B (Codex) |
|
|
214
|
+ |
|---|---|---|---|
|
|
215
|
+ |
| Where compute runs | The person's own machine | A one-shot container, staging only | **The web BEAM node**, as a `Port` child |
|
|
216
|
+ |
| Who authorizes | The machine's owner, by pairing (`lib/openagents/machines.ex:204`) | Whoever launches the container | An admin who connected a driver account (`lib/openagents_web/router.ex:185`) |
|
|
217
|
+ |
| Authorization artifact | `sarah.module_approval.v1`, class `explicit_operator_approval` | None | None |
|
|
218
|
+ |
| Transport | Phoenix channel over WebSocket, topic `computer:*` | None; `Port` to a local process | None; `Port` to a local process |
|
|
219
|
+ |
| Protocol | `openagents.computer.v1` carrying framed ACP | JSON lines on stdout | JSONL JSON-RPC over stdio |
|
|
220
|
+ |
| Unit of work | `work_jobs` row, `kind: "delegation"` | `%SCV.Run{}` struct only | `%SCV.Run{}` plus a `scv_runs` row |
|
|
221
|
+ |
| Work identity | Job UUID, scoped to conversation and owner visitor | Run UUID, unpersisted | Run UUID, scoped to a driver account; **no user FK** |
|
|
222
|
+ |
| How work is submitted | A model tool call or the Computers API | `SCV_OBJECTIVE` at container boot (`lib/openagents/scv/worker.ex:61`) | `CodexRuns.start/5` — **no production caller** |
|
|
223
|
+ |
| Streaming | Live into chat via PubSub projection | JSON lines to stdout | Sanitized event rows plus the live `Activity` ring |
|
|
224
|
+ |
| Persistence | Job row, job steps, decoded report, chat message | **None** | Execution row, event rows, report plus digest |
|
|
225
|
+ |
| Failure and recovery | Horde relocation, generation fence, session resume | None; the container exits | Lease expiry swept to `uncertain`; no resume |
|
|
226
|
+ |
| Write access | Per the machine's own tier and roots | `read_only` only at this entry point | `read_only`, forced by changeset and constraint |
|
|
227
|
+ |
| Concurrency limit | Eight machines per account | Whatever is launched | One active run per driver account |
|
|
228
|
+ |
| Cost model | The customer's hardware | Our container | Our web node's CPU. Unmetered |
|
|
229
|
+ |
| Enabled by default | No (`lib/openagents/computer.ex:30`) | The image is the role | No (`config/config.exs:57`) |
|
|
230
|
+ |
| Production | Yes, when flagged on | **Refused at config load** (`config/runtime.exs:418`) | Flag-gated |
|
|
231
|
+ |
|
|
232
|
+ |
Two things stand out. First, the SCV side is *stronger* on sandboxing, payload sanitization, and per-run measurement; the computer side is stronger on dispatch, streaming, and recovery. Second, both independently implemented the same durable-worker pattern — `owner_node`, `generation`, and a lease or fence — in `work_jobs` (`lib/openagents/work/job.ex:50`) and in `scv_runs` (`lib/openagents/scv/execution.ex:27`), with no shared code.
|
|
233
|
+ |
|
|
234
|
+ |
---
|
|
235
|
+ |
|
|
236
|
+ |
## 4. Are they connected?
|
|
237
|
+ |
|
|
238
|
+ |
No. They share no module, no schema, no job kind, and no receipt type.
|
|
239
|
+ |
|
|
240
|
+ |
The evidence is a search in both directions at the measured commit:
|
|
241
|
+ |
|
|
242
|
+ |
- Searching `lib/openagents/scv/`, `lib/openagents/scv.ex`, and the SCV mix task for `computer`, `machine`, `device_auth`, `controller`, `acp`, `transcript`, `delegat`, `persona`, `sarah`, or `chat` returns ten hits, and every one of them is the substring `chatgpt` inside Codex login code. Nothing else.
|
|
243
|
+ |
- Searching the computer, machines, device-authorization, persona, tools, and conversations trees for `scv` returns nothing.
|
|
244
|
+ |
- No SCV migration references `machines`, `computers`, `device_authorizations`, or any controller table; no non-SCV migration references any `scv_*` table.
|
|
245
|
+ |
|
|
246
|
+ |
Outside its own tree, `OpenAgents.SCV` is referenced from exactly six files, none of which is a delegation path: `lib/openagents/application.ex`, `lib/openagents/runtime_supervisor.ex`, `lib/openagents/network_status.ex`, `lib/openagents_web/live/network_status_live.ex`, `lib/openagents_web/live/admin_scv_accounts_live.ex`, and `lib/mix/tasks/openagents.scv.opencode.ex`. There is no SCV controller and no SCV channel.
|
|
247
|
+ |
|
|
248
|
+ |
`NetworkStatus` is the only surface where both systems appear at once, and even there they are siblings rather than collaborators: it publishes SCV activity as a pseudonymous label with lifecycle state, and connected controller machines as a count only (`lib/openagents/network_status.ex:11`). Both are read-only projections. Nothing flows between them.
|
|
249
|
+ |
|
|
250
|
+ |
Sarah's installed tool catalogue confirms the same from her side (`config/config.exs:130`–`151`): the computer family, `deep_work`, the repository family, and the memory family. There is no SCV tool. **Sarah cannot start an SCV, list one, read one, or cancel one.**
|
|
251
|
+ |
|
|
252
|
+ |
Two couplings do exist and should be named honestly, because neither is a code dependency:
|
|
253
|
+ |
|
|
254
|
+ |
1. **A shared deployment surface.** The root `Dockerfile:170` installs the OpenAI `codex` binary into the web and fleet image, and `infra/staging/main.tf:404` grants the fleet service account read and add permissions on the SCV Codex credential secrets. The host that serves the web app carries the binary and the credentials. That is why lane B can run on the web node at all.
|
|
255
|
+ |
2. **A shared approval vocabulary.** `sarah.module_approval.v1` already carries two distinct classes — `explicit_operator_approval` for machine effects (`lib/openagents/machines.ex:204`) and `exact_current_user_consent` for coding-job repository writes (`lib/openagents/tools/repository.ex:169`) — both enforced at one choke point (`lib/openagents/modules/surface_policy.ex:114`). SCV uses neither, but that vocabulary is where a third class would go.
|
|
256
|
+ |
|
|
257
|
+ |
---
|
|
258
|
+ |
|
|
259
|
+ |
## 5. The gap to "Sarah deploys SCVs on system capacity"
|
|
260
|
+ |
|
|
261
|
+ |
### 5.1 What is actually missing
|
|
262
|
+ |
|
|
263
|
+ |
Five gaps. None requires inventing a pattern this codebase has not already used.
|
|
264
|
+ |
|
|
265
|
+ |
1. **No target concept.** `ComputerAgentJobs.start/5` takes a `%Machine{}` and nothing can stand in its place (`lib/openagents/computer_agent_jobs.ex:20`). "Where does this work run" is not a value anywhere; it is implied by which function you call.
|
|
266
|
+ |
|
|
267
|
+ |
2. **No remote runner.** `OpenAgents.SCV.Runner` is a two-callback behaviour that anticipates alternatives (`lib/openagents/scv/runner.ex:6`), and `Runner.Local` is careful to say that it describes the in-environment process boundary, not the host that scheduled the container (`lib/openagents/scv/runner/local.ex:2`). But `Run.new/3` admits only `Local` (`lib/openagents/scv/run.ex:117`), and it calls `driver.run(run)` in-process. Nothing can place a run on another host.
|
|
268
|
+ |
|
|
269
|
+ |
3. **No runtime entry point.** Lane A's objective arrives at container boot; lane B's dispatcher is uncalled outside tests. Something must accept a run request while the web node is already up.
|
|
270
|
+ |
|
|
271
|
+ |
4. **No tool, and no authority to attach to it.** Sarah's machine tools declare `side_effect: :external_effect` with `approval_class: "explicit_operator_approval"` (`lib/openagents/tools/computer_agent.ex:73`), backed by a receipt pointing at a machine the person paired. A system-capacity SCV has no machine to point at, so it cannot reuse that receipt, and `SurfacePolicy` will refuse it (`lib/openagents/modules/surface_policy.ex:114`).
|
|
272
|
+ |
|
|
273
|
+ |
5. **No meter and no per-user bound.** `scv_runs` records `usage` and `resources` but posts them nowhere, has no user foreign key, and is bounded only by one-run-per-account. Work on a customer's machine is free to us; work on our capacity is not.
|
|
274
|
+ |
|
|
275
|
+ |
Two of these are smaller than they look. Lane B already runs on our capacity today — it just runs on the web node, uncalled. And `Environment` already models the capability half of a placement decision (`lib/openagents/scv/environment.ex:2`); only the host half is absent.
|
|
276
|
+ |
|
|
277
|
+ |
### 5.2 The two seams worth using
|
|
278
|
+ |
|
|
279
|
+ |
**On the work side, `kind`.** `OpenAgents.Work.Job` validates `kind` against three values (`lib/openagents/work/job.ex:18`), and each kind answers the same lifecycle questions differently while sharing the row, the statuses, the Horde singleton, the generation fence, the recovery sweep, and the report-into-conversation ending. `OpenAgents.Work.Coding` is the model to copy: its moduledoc says the loop itself is the ordinary `JobServer` and the module "only answers the kind-specific questions it asks" (`lib/openagents/work/coding.ex:2`). An `scv` kind would answer them by handing off to `SCV.run/1` instead of to an LLM loop.
|
|
280
|
+ |
|
|
281
|
+ |
**On the SCV side, `Runner`.** Placement belongs behind `runner_module`, exactly where the struct already puts it (`lib/openagents/scv/run.ex:22`). A second runner is where "our fleet" becomes a value rather than a deployment fact.
|
|
282
|
+ |
|
|
283
|
+ |
### 5.3 New questions once the capacity is ours
|
|
284
|
+ |
|
|
285
|
+ |
Delegating to someone's laptop is bounded by a fact outside our control: they own the machine, so the worst case is bounded by what they admitted. Running on our capacity removes that bound.
|
|
286
|
+ |
|
|
287
|
+ |
- **What replaces the pairing receipt?** There is no `%Machine{}` to reference. A third approval class is needed, and the `coding` kind shows the reasoning to imitate: starting the job is the person's exact current consent for the reversible writes inside the job's own clone, while the one external effect stays operator-only under host policy (`lib/openagents/tools/repository.ex:155`).
|
|
288
|
+ |
- **Whose repository, and under what tenancy?** `Run.repository` is a filesystem path (`lib/openagents/scv/run.ex:23`) and `scv_runs` scopes to a driver account, never to a conversation or a visitor. Customer-initiated SCVs cross a tenancy boundary neither schema models, and the current per-account concurrency limit would make one customer's run block another's.
|
|
289
|
+ |
- **Who pays, and what stops a loop?** `Work.Coding` already meters inference into the shared `inference_grants` ledger (`lib/openagents/work/coding.ex:2`). An SCV kind should meter into the same ledger rather than grow a second one.
|
|
290
|
+ |
- **What does the network see?** `NetworkStatus` publishes SCV activity content-free (`lib/openagents/network_status.ex:11`). That shape has to hold even when the content belongs to a customer.
|
|
291
|
+ |
- **Which permission profile, and where?** Both SCV entry points force `read_only` — the worker by allowlist (`lib/openagents/scv/worker.ex:67`), lane B by changeset and database constraint (`lib/openagents/scv/execution.ex:78`). Sarah writing code on our capacity means opening that, which is the moment isolation stops being theoretical. Lane B currently runs on the web node, which is the wrong place to relax it.
|
|
292
|
+ |
- **Where does production come from?** Lane A refuses to boot outside staging (`config/runtime.exs:418`). Any plan that ends in customers using this has to pass through that guard deliberately.
|
|
293
|
+ |
|
|
294
|
+ |
### 5.4 A staged path
|
|
295
|
+ |
|
|
296
|
+ |
**Increment 1 — give lane B its first production caller, operator-only.**
|
|
297
|
+ |
Wire `CodexRuns.start/5` to the existing admin surface (`lib/openagents_web/live/admin_scv_accounts_live.ex`, route at `lib/openagents_web/router.ex:185`). No new authority model, no customer exposure, no new schema, no new table. This turns the durable Codex path from tested-but-dead into exercised, and answers empirically whether `claim/4`, the lease fence, the reaper, and the sanitized event stream hold up in real use. It also surfaces immediately whether running Codex as a `Port` child of the web node is acceptable, or whether increment 2 has to come sooner than planned.
|
|
298
|
+ |
|
|
299
|
+ |
**Increment 2 — make placement a value.**
|
|
300
|
+ |
Add a second `OpenAgents.SCV.Runner` implementation that places a run on capacity separate from the web node, widen the `Run.new/3` runner allowlist (`lib/openagents/scv/run.ex:117`), and record the chosen runner on the durable row. This is where "system capacity" stops being a deployment fact and becomes a decision the code makes, and it is the increment that lets the write profile be opened safely later. Open design question, recorded in section 7: whether that runner launches a container per run or hands work to a warm pool. Nothing in the codebase decides this today.
|
|
301
|
+ |
|
|
302
|
+ |
**Increment 3 — give Sarah the tool, with its own approval class.**
|
|
303
|
+ |
Add an `scv` value to `@kinds` (`lib/openagents/work/job.ex:18`) and an `OpenAgents.Work.Scv` module shaped like `OpenAgents.Work.Coding` (`lib/openagents/work/coding.ex:1`), so an SCV job inherits the job row, the seven statuses, the Horde singleton, the generation fence, the recovery sweep, and the report-into-conversation ending. Then add one tool shaped like `deep_work`, which already returns a job reference immediately and lets multi-step work run server-side (`lib/openagents/tools/deep_work.ex:2`), carrying a third `approval_class` and metering into `inference_grants` alongside the coding kind. Sarah's live rail needs no new machinery if SCV events are projected through `ComputerActivity`'s existing contract (`lib/openagents/computer_activity.ex:1`); whether that module should then be renamed is a judgment call, not a technical one.
|
|
304
|
+ |
|
|
305
|
+ |
**The smallest honest first increment is increment 1.** It is the only one that adds no abstraction, and it converts the largest unknown in this audit — whether the durable Codex SCV path works outside its test suite — into a fact. Increments 2 and 3 are both cheaper once that is known, and riskier before.
|
|
306
|
+ |
|
|
307
|
+ |
---
|
|
308
|
+ |
|
|
309
|
+ |
## 6. Where the planning documents describe more than the code does
|
|
310
|
+ |
|
|
311
|
+ |
`docs/scv-planning.md` and `docs/scv-codex-app-server-planning.md` are design documents, and a reader who takes them as descriptions of the running system will be wrong about several things. Recorded here so this audit is not misread the same way:
|
|
312
|
+ |
|
|
313
|
+ |
- **Ten of the thirteen documented tables do not exist.** `docs/scv-planning.md:1110` specifies `scvs`, `scv_work_items`, `scv_steps`, `scv_workers`, `scv_worker_images`, `scv_executions`, benchmark tables, `scv_resource_samples`, and `scv_candidates`. Only `scv_runs` was built, alongside three tables the document never names: `scv_run_events`, `scv_driver_accounts`, and `scv_driver_login_attempts`.
|
|
314
|
+ |
- **The worker pool and capability-routing scheduler do not exist** (`docs/scv-planning.md:766`). No module, no table, no Terraform.
|
|
315
|
+ |
- **The states `idle`, `paused`, `circuit_open`, and `disabled`** appear in the UI component's attribute validation (`lib/openagents_web/components/graph.ex:122`) but in no schema. The live surface collapses everything to running or idle (`lib/openagents_web/live/network_status_live.ex:148`).
|
|
316
|
+ |
- **Nothing runs continuously.** There is no loop, poller, queue, or work-discovery module. Every run is one-shot and externally triggered.
|
|
317
|
+ |
- **The staging Cloud Run job is not in Terraform.** `infra/staging/` declares SCV as two Secret Manager slots and four IAM bindings (`infra/staging/main.tf:46`, `:388`) and nothing more. The job, the Artifact Registry repository, and the service account are created by hand per `docs/operations/scv-staging-qualification.md:16`.
|
|
318
|
+ |
- **"No network by default"** (`docs/scv-planning.md:492`) holds only for Codex, which disables it in `config.toml` (`lib/openagents/scv/executor/codex_app_server.ex:688`). The OpenCode process keeps egress; only its `webfetch` and `websearch` tools are denied, and `Environment` declares `:network_egress` for both environments (`lib/openagents/scv/environment.ex:27`).
|
|
319
|
+ |
|
|
320
|
+ |
---
|
|
321
|
+ |
|
|
322
|
+ |
## 7. Open questions
|
|
323
|
+ |
|
|
324
|
+ |
These could not be settled from the code and should not be assumed either way.
|
|
325
|
+ |
|
|
326
|
+ |
1. **Whether `CodexRuns.start/5` has ever run outside tests.** The path is complete and tested, but with no production caller there is no evidence of it running against a real Codex account.
|
|
327
|
+ |
2. **What launches the staging SCV container.** `ops/scv/images/` builds the image and `infra/staging/main.tf` provisions the credential secrets, but this audit found no component that decides to start an SCV container and sets `SCV_OBJECTIVE`. `rel/` contains no SCV reference at all. It appears to be manual.
|
|
328
|
+ |
3. **Whether lane B running on the web node is intentional.** `Environment` gives the `codex-app-server` environment the image name `openagents` (`lib/openagents/scv/environment.ex:40`), which reads as deliberate, but nothing records whether that was a staging convenience or the intended end state.
|
|
329
|
+ |
4. **Whether `Runner` was designed for host placement or only for in-container process shape.** `Runner.Local` explicitly disclaims the scheduling host (`lib/openagents/scv/runner/local.ex:2`), which reads as either "placement lives elsewhere by design" or "placement is not built yet". The distinction changes whether increment 2 extends the seam or repurposes it.
|
|
330
|
+ |
5. **Whether the two lanes are meant to converge.** They share `Run`, `Environment`, `Driver`, and the event schema name, but differ on persistence, host, and permission ceiling. No document records whether lane A is expected to gain durable rows or lane B to gain a container.
|
|
331
|
+ |
6. **Whether machine `tier` is meant to be enforced server-side.** It is snapshotted and trigger-checked for consistency but never branched on in `lib/`. If the controller is the only enforcement point, that is a defensible design; no comment or document says so.
|
|
332
|
+ |
7. **Whether the two durable-worker implementations should converge.** Both `work_jobs` and `scv_runs` carry `owner_node` and `generation`, but the job side resumes interrupted work while the SCV side only reaps expired leases to `uncertain`. Whether that difference is intentional — a delegation can re-attach a session, a dead container genuinely cannot — or accidental duplication is a decision no document in `docs/` currently records.
|