Repository navigation
refactor(workspace): run the workspace agent on OpenCode V2 when the image selects it - #1983
Conversation
Systematic 3.27.x supports both V1 and V2, so the V2 path installs DEFAULT_SYSTEMATIC_VERSION (Dockerfile ARG SYSTEMATIC_VERSION) instead of a separate SYSTEMATIC_V2_VERSION pin that Renovate does not manage.
|
|
||
| test('every version test in the runtime stage and the V2 stage uses the same V2 arm', () => { | ||
| // #then a drifted pattern would send one step down the V1 path and another down the V2 path | ||
| const arms = dockerfile.split(V2_VERSION_ARM).length - 1 |
fro-bot
left a comment
There was a problem hiding this comment.
Verdict: CONDITIONAL
Two V2 lifecycle issues need correction before merge.
Blocking issues
| Priority | Location | Issue and required fix |
|---|---|---|
| P2 | opencode-server-v2.ts:399–405 |
Bound and cancel the permission-gate request. serving.fetch('/api/agent') and body consumption have no timeout or cancellation signal; the handle exposes raw fetch, so requestTimeoutMs does not apply. Since startAbort is already cleared, a stalled response prevents retry and shutdown until the service's 25-second forced exit. Apply a timeout combined with service cancellation through body consumption, and shut down the acquired handle on failure/cancellation. |
| P2 | opencode-server-v2.ts:348–351 |
Recheck cancellation after database preparation. An abort during await prepareDatabase() fires while startAbort is undefined. Preparation then creates a fresh, unaborted controller and starts another server during shutdown. Startup can exceed the 25-second drain. Check isAborted() immediately after preparation and return before spawning. |
Non-blocking concerns
- V2 rollout prerequisite: the gateway currently has no
opencodeVersionconsumer and still attaches through the existing client/run loop. The description's claim that it already selects V2 from/readyzis premature. Track the gateway wiring and correct that claim before switching the deployed image.
Missing tests
- Stall the permission-gate response headers or body; verify timeout/cancellation shuts down the handle and leaves readiness false.
- Abort while database preparation is deferred; resolve preparation and verify
startServeris never called. - Automate the V2 image smoke path in CI, including normal entrypoint startup and
/readyz. Current image CI builds the default V1 image; the real-binary integration test is opt-in.
Risk assessment (MED): likelihood of regression, security exposure, or blast radius
The default V1 path limits immediate exposure. V2 startup failures can prevent graceful shutdown and force container termination. No concrete authentication bypass was identified.
Run Summary
| Field | Value |
|---|---|
| Event | pull_request |
| Repository | fro-bot/agent |
| Run ID | 40818772970 |
| Cache | hit |
| Session | ses_e7a0a22aeffea37Va7CgDdZ5ze |
| Review | Read-only source/test inspection; both lifecycle findings independently validated |
| Checks | No tests, build, lint, or type-check commands run |
| Delivery | Response-file review; inline annotations unsupported, so findings link to exact lines |
| Workflow limitation | Packaged synthesis helper inaccessible; findings verified directly against source |
…llation after database preparation
fro-bot
left a comment
There was a problem hiding this comment.
Verdict: PASS
Both previous blockers are resolved in 11c40c3: the permission-gate read is bounded and cancellable through body consumption, and cancellation after database preparation prevents spawning. Regression tests cover both fixes. The description now correctly identifies gateway support as a separate rollout prerequisite.
Blocking issues
None
Non-blocking concerns
None
Missing tests
- Automated V2 image smoke coverage remains absent: current image CI exercises V1, while V2 verification is manual/opt-in. Add normal-entrypoint and
/readyzcoverage before production rollout; this remains non-blocking for the staged implementation.
Risk assessment (MED): likelihood of regression, security exposure, or blast radius
The V2 lifecycle and image path remain a substantial change, but the default V1 selection is preserved. No additional blocking correctness, security, or API-contract issue was identified in the follow-up.
Run Summary
| Field | Value |
|---|---|
| Event | pull_request |
| Repository | fro-bot/agent |
| Run ID | 40866251093 |
| Cache | hit |
| Session | ses_e7a0a22aeffea37Va7CgDdZ5ze |
| Scope | Re-review of efcfc61 → 11c40c3, including both prior blockers and regression tests |
| Checks | Source/test inspection; no test suites, build, lint, or type-check commands run |
| Probe | Checked a possible pre-aborted-helper rejection. Reproduced in isolation, but excluded after independent validation found no reachable production cancellation path. |
Exact minimal probe command:
node --input-type=module -e 'async function untilAborted(signal, work) { return new Promise((resolve, reject) => { const onAbort = () => reject(signal.reason); if (signal.aborted) { onAbort(); return; } signal.addEventListener("abort", onAbort, {once:true}); work.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort)); }); } const controller = new AbortController(); controller.abort(); process.on("unhandledRejection", error => { process.stdout.write(`UNHANDLED: ${error.name}\n`); }); try { await untilAborted(controller.signal, fetch("http://127.0.0.1:54321/api/agent", {signal:controller.signal})); } catch (error) { process.stdout.write(`GATE CAUGHT: ${error.name}\n`); } await new Promise(resolve => setTimeout(resolve, 20));'
The workspace agent and its image can now run on OpenCode V2 (#1805) when the image is built with a V2
OPENCODE_VERSION. The default build stays on the V1 harness and behaves exactly as before. Switching the deployed image is a separate step.Selection
WORKSPACE_IMAGE_OPENCODE_VERSIONfrom the sameOPENCODE_VERSIONbuild argument that picks the binary./readyzand/healthzreport the value asopencodeVersion, and omit the key when it's unset. This is the field the gateway will read to choose its V1 or V2 run loop, so the image pin stays the only switch. The gateway side lands separately; until then the gateway runs its V1 loop, so a V2 image must not be deployed yet.V2 runner and proxy
Starting. Each spawn gets its own database directory, so no session state carries over between spawns. The config comes from the V2 workspace builder:
.envis denied;session_moveandworktree_createare denied.The runner spawns through the runtime V2 server lifecycle as uid 10001. If it can't take that identity, the spawn fails instead of running as root. It then requires both plugins,
systematicandfro-bot.opencode-plugin, to beactive, and runs the structural permission gate.Stopping. Owned sessions are interrupted before stdin closes. A cancel that arrives while a start is still in flight aborts only that start, so it can't leave an execution claim behind.
Proxy. It turns the gateway's Bearer token into the server's Basic auth on each request, passes
x-opencode-directorythrough, and streams SSE uncompressed.File watcher. It stays at V2's default.
Image
The V2 branch of
deploy/workspace.Dockerfile:SYSTEMATIC_VERSION, the existing pin, now 3.27.1) and@fro.bot/opencode-plugin@0.1.0into the agent's cache at build time, as uid 10001, using the boundeddeploy/scripts/install-opencode-v2-plugins.sh. Nothing is installed at boot;pluginkey, plus a root-owned V2 managed config;Verification
Real binary. The integration test against stock v2.2.0 (digest-verified):
It's gated on
FRO_BOT_OPENCODE_V2_BIN, so CI skips it. It also caught the abort-during-start claim leak, which is now fixed and has its own test.Docker, local, arm64.
docker build --build-arg OPENCODE_VERSION=2.2.0succeeded. In the image:opencode --versionis stockv2.2.0;WORKSPACE_IMAGE_OPENCODE_VERSION=2.2.0;A
--network none --user 10001:10001run ofdeploy/scripts/v2-image-smoke.mjsreached/api/infoand/api/agent, with both pluginsactive. The V1 default build is covered by CI's existing workspace smoke test.Tests and gates.
ARGdefaults to the runtime constants.check-types,lint,test,test:scripts,buildandnode --test deploy/scripts/*.test.mjsall pass.