Repository navigation
fix: cloud machines install the host's OpenCode generation - #258
Merged
aicodeking merged 7 commits intoNov 2, 2026
Merged
Conversation
- The secure store rejects control characters, so saving the multi-line private key always failed a provider's first cloud task with "That secret looks empty or malformed." (issue aicodeking#206); the keypair is now one base64-encoded secret holding both halves. - The public line is no longer re-derived with ssh-keygen -y on every run, which failed inside the app and read as a corrupt key. - A serialized key_pair keeps concurrent task preparations from racing the read against the save; an undecodable value regenerates a fresh pair instead of blocking every task; the key file's 0600 mode is re-asserted on every write.
Independently authored failing-first against the unfixed main tip by a separate test-author session (qualified fail-first there with exactly 'That secret looks empty or malformed.'), then merged onto the fix: the saved value must pass the secure store's own validation and the materialized file must derive the returned public line via ssh-keygen. The two lifecycle tests share one locked store dir (the secret store's dir and test map are process-globals).
…race
POST /v2/account/keys answers {"ssh_key": {...}}, not {"key": ...}, so a
successful registration read as failure once the earlier fixes let tasks
reach it. Parse ssh_key (key kept as a tolerated alternate), and when the
create is refused or answers without the key, re-list the account and
reuse the registered key instead of failing or registering a duplicate.
decode_pair now also treats a decodable but bogus private half as an
unusable pair, so it self-heals like any other.
- sync::upload_file and credentials::upload_file split every file into 46 KiB pieces before the provider upload, so VPS machines paid one ssh exec per piece (a 135 KB checkout bundle cost minutes; a 64 MiB state archive, hundreds). The whole file now goes to the sandbox in one call: one scp on the SSH-family providers, their own single-call file APIs elsewhere, and the chunked trait default still covers providers without one (issue aicodeking#206). - Cloud OpenCode installs at the exact version this computer runs instead of npm latest, whose model catalog differs between releases (a 2.1.2 host got a 2.2.0 machine that refused deepseek/deepseek-flash). The runtime marker re-checks the program's own --version, so a host upgrade reinstalls once and older markers compare correctly. - OpenCode 2.x keeps provider credentials in its database (auth.json is ignored there), so the host's 'opencode auth export --standalone' payload is imported on the machine and its staging file removed by an EXIT trap; the auth.json copy stays for 1.x hosts. - providers::run_provider runs an installed provider CLI and captures stdout, drained on a thread so output past the OS pipe buffer cannot stall the poll loop. - Tests: one-upload-per-file sync round trip, marker version pinning, npm pin parsing, and the export/import login flow against a fake machine running real sh. cargo test --locked: 1721 passed, 74 ignored; clippy -D warnings clean.
… errors name the mismatch OpenCode v2 ships as the separate npm package @opencode/cli; opencode-ai publishes only 1.x, so the version pin from the previous fix resolved to a package that does not exist for v2 hosts and the machine could never match the host's model catalog (issue aicodeking#209). The package and the runtime marker now follow the host's major generation, with a looser parse so a bare 2 or v2 still selects the right package, and a rejected session/set_config_option names the setting, its rejected value and the values the harness offers.
The marker lookup compared the machine CLI's own --version line with the raw host output, so a prefixed line (OpenCode 2.1.2) or a bare major could never match a freshly installed CLI and every run reinstalled. It now asks for the parsed pin, which is what the CLI actually prints (review finding).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
Fixes #209: a cloud machine installed npm
opencode-ai(OpenCode v1; latest 1.19.5, no 2.x exists) even when the host ran OpenCode v2, so the sandbox's harness never advertised the model picked from the host's v2 catalog and every first run died withThe harness does not advertise the selected setting.about 15 seconds in. Switching the task's model to Default unblocked it, which is how the reporter isolated the harness, not the machine.#215 pinned the install to the host's version, but for a v2 host that pin produces
opencode-ai@2.1.2: an npm version that does not exist, so the install now fails outright instead of installing the wrong generation. The package itself has to follow the host's major generation. This PR is stacked on #215 (its head, 54164e8); only the two commits shown as "commits" below are new.What (in
src-tauri/src/cloud_agents/setup.rs)opencode_package(version)returns@opencode/clifor a host major of 2 or higher andopencode-aiotherwise, pinned to the host's exact version whenparse_versionyields one (@opencode/cli@2.1.2,opencode-ai@1.19.5). npm facts checked today:opencode-aipublishes only 1.x,@opencode/clionly 2.x, and both expose the sameopencodebin, somark_runtime's resolution is unchanged.host_generation(version)reads the major from the parsed pin, falling back to a new looserparse_generationso a bare2,v2or2.xstill selects the right package without inventing a pin. Anything unparsable keeps the v1 package and latest, as before.opencode-2,opencode-1), so a machine provisioned for one generation never reuses another's marker; the plainopencodemarker written by older builds is likewise never read by a generation-scoped lookup. Other harnesses keep their own name as the marker id.cached_programandmark_runtimetake the runtime id in place of the harness name; behavior forclaude/glm/mimo/codexis byte-identical.OpenCode 2.1.2or a bare2could never equal the machine CLI's own--versionline, so the gate missed and every run reinstalled through npm.What (in
src-tauri/src/providers/acp.rs)choice_requestnow reportsThe harness does not advertise the selected <setting> "<value>". It offers: <values>. Refresh its models or choose the harness default.The setting name comes from the option'sid, else itscategory, else the word "setting"; up to 12 offered values are listed, then, …, ornothingwhen the harness offers none. The generic sentence named nothing, which is what left the reporter guessing across model, mode and effort.How it was tested
8 new tests, authored independently of the fix by a separate test-author pass and qualified failing-first against stubs of the old behavior before implementation. Verbatim failures included
left: "opencode-ai@2.1.2"/right: "@opencode/cli@2.1.2"and all threechoice_requesttests panicking on the old generic line. Coverage: package selection across real--versionoutputs (2.1.2,v2.1.2,OpenCode 2.1.2,2.2.0-beta.3, bare2/v2/2.x,1.19.5,None,"",latest, junk), generation-scoped marker ids, marker read/write scoped to the id (legacy plainopencodenever satisfies a v2 lookup), the 12-value cap with zero-padded ids so no substring false positives pass, and the id-then-category fallback.The cache-gate seam test is qualified by construction: the direct
cached_programcall with the raw host line returnsNonein the same test, which is exactly the miss the unfixed call site produced (running the unfixed path end to end would have performed a real network npm install).cargo test --locked --manifest-path src-tauri/Cargo.toml --lib: 1796 passed, 0 failed, 74 ignored (live smokes).cargo clippy --locked --all-targets -- -D warnings: clean. Rust-only change; no frontend surface touched, so the npm/Playwright gates are not exercised by this diff.Review passes: one deep review (approve; two nits ignored with reasons: a u32-overflow version string falls back to the v1 package exactly as the base commit did and npm 404s clearly, and values beyond the pre-existing 10,000-entry catalog cap were already rejected before this change) and one independent quick pass whose single finding is the cache-gate fix above; both ran without seeing each other's findings.
npm run build(typecheck + bundle) — N/A, Rust-onlynpm test— N/A, Rust-onlycargo test --manifest-path src-tauri/Cargo.toml(lib suite, locked; clippy clean)Relevant Playwright tests — N/A, no UI changed
Checklist
<type>: <description>docs/updated if behavior changed — nothing to update:docs/agent-updates.mdalready documents both packages per generation, and no doc states the installed package or marker namesrc/preview.tsfor new IPC commands — N/A, no new IPC commands