Skip to content

fix(opencode): carry the user's sign-ins into the isolated database - #780

Open
AliRezaei-Code wants to merge 3 commits into
alphaXiv:mainfrom
AliRezaei-Code:fix/opencode-isolated-credentials
Open

AliRezaei-Code wants to merge 3 commits into
alphaXiv:mainfrom
AliRezaei-Code:fix/opencode-isolated-credentials

Conversation

@AliRezaei-Code

@AliRezaei-Code AliRezaei-Code commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Closes #781

The bug

The OpenCode harness reports Signed out while OpenCode is signed in, and its catalog collapses to the anonymous free models.

orx runs OpenCode against an isolated database (OPENCODE_DB=…/agents/opencode/opencode.db) so sessions never touch the user's own history. But OpenCode keeps provider credentials in the credential table of its own database and does not follow OPENCODE_DB to find them — opencode auth list prints the same accounts with and without it. So the isolated database starts with none, /api/integration reports no connection, authenticated is false, and only opencode/*-free models survive.

The suggested repair (OPENCODE_DB=… opencode auth login, also what the Sign in button runs) cannot fix it either: that login writes back to the user's own database, so the isolated one stays empty.

Measured on a real installation:

before after
credential rows in the isolated DB 0 (13 in the user's DB) 13
authenticated false true
models 9 (free only) 670

The fix

Copy credential, account, account_state and control_account from the user's database on every prepare.

  • One-way. The user's database is opened read-only and never written.
  • Read from the default database, never the OPENCODE_DB override. That variable is what OpenCode ignores for credentials — honouring it here would read the user's session database and reproduce the bug. Session lookup keeps following it, because a session may legitimately live where the user put it.
  • One snapshot across the four tables. A sign-in committing account between the reads could otherwise leave account_state pointing at a row that was never copied.
  • Updated in place, never delete-then-insert. A replacement that half-succeeds would drop a working sign-in.
  • Presence is each table's own primary key, read from the schema: control_account is keyed by (email, url), account_state by its singleton id. A table whose key the two schemas disagree on is skipped rather than guessed.
  • Per-table update policy, because the tables do not agree on what "newer" means: where time_updated exists the user's copy wins only when strictly newer, so a token the isolated server refreshed mid-turn is not reverted while a deliberate re-login still lands; account_state carries no timestamp, so it is mirrored — switching accounts must not leave the isolated store pointing at the old one.
  • Schema drift cannot fail startup. Only shared columns are copied; an unreadable or locked user database is a warning, not an outage.
  • Only an existing isolated database is touched. OpenCode creates its schema on first boot; pre-creating tables would collide with its own migrations. A fresh install is seeded by the detection sweep that follows.

Every model stays selectable — nothing is filtered.

Platforms — this is not a macOS-only fix

The clients are one product on one crate: the CLI, the web dashboard (orx up), the macOS bundle (whose executable is orx), the Linux AppImage and the Windows app (whose launcher execs orx.exe app). All of them run this code and serve the same embedded ui/dist, so the copy runs everywhere without a per-platform branch.

The one place the platforms could disagree is where the user's OpenCode database lives, so that is one named function with tests that run on every runner:

  • macOS / Linux — $XDG_DATA_HOME if set, else ~/.local/share/opencode/opencode.db.
  • Windows — xdg-basedir, which OpenCode resolves its data directory through, has no Windows branch: the database is under %USERPROFILE%\.local\share\opencode, not %APPDATA%. the_users_database_sits_where_opencode_puts_it_on_every_platform asserts the path contains .local/share and no AppData, and runs on the Linux and Windows CI jobs as well.
  • A relocated XDG_DATA_HOME is followed. A custom OPENCODE_DB filename is followed for sessions only — credentials always come from the default database, which is what OpenCode does.

Because src/** selects the rust, linux, macos and windows checks in scripts/ci-changes.mjs, this PR builds the Windows and Linux apps as well as the Rust suite.

Verification

  • cargo fmt --all --check — clean
  • cargo clippy --all-targets --locked -- -D warnings — clean
  • cargo test --locked (with the two skips CI uses) — 1291 passed, 17 integration passed
  • cargo test --bin orx opencode_database — 21 passed, 9 of them for the copy: no revert of a refreshed token, re-login replaces, account_state mirrored, control_account composite-key update, update keeps columns the source lacks, V1 source without credential tables, missing source, column drift
  • cargo test --bin orx path_tests — 3 passed: macOS/Windows/Linux resolution, XDG_DATA_HOME relocation, and the OPENCODE_DB split between credentials and sessions
  • Applied to a live install and re-ran detection: authenticated: true, 670 models; repeated on a fresh data directory from the built macOS app: credential rows 0 → 13, authenticated: true, 671 models

🤖 Generated with OpenCode

@greptile-apps

greptile-apps Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 3/5

[Critical impact] The change copies existing provider credentials into the database used by OpenCode's isolated harness.

Fix the credential source path and read the source tables in one transaction before merging.

Findings

  1. P1 Sign-ins use the wrong database ▶
  2. P1 Active account has no row ▶
Fix with agent prompt
### Issue 1
src/local/native_store.rs:699
`prepare_opencode` uses `opencode_db(NativeStore::Legacy)` as the credential source, but that path honors `OPENCODE_DB`. OpenCode's sign-in commands ignore that variable, as this change explains. Users who set `OPENCODE_DB` therefore copy from their session database instead of the database holding their sign-ins, leaving the harness signed out.

Resolve the credential source separately, without the `OPENCODE_DB` override.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

### Issue 2
src/local/opencode_db.rs:228-231
`inherit_credentials` reads each source table separately without a source transaction. If the user's OpenCode process adds and selects an account after `account` is read but before `account_state` is read, the copy saves the new `active_account_id` without its account row. OpenCode can then remain signed out until another copy repairs it.

The destination transaction cannot prevent this mismatch. Read all four source tables from one read transaction so the account and its pointer come from the same saved state.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Copies OpenCode sign-ins into the existing isolated database during preparation.

  • OpenCode sign-ins now reach the isolated database.

Reviews (2) · Last reviewed commit: "fix(opencode): carry the user's sign-ins..." · Reviewed by Greptile

Comment thread src/local/opencode_db.rs Resolved
Comment thread src/local/opencode_db.rs Resolved
OpenCode keeps provider credentials in the credential table of its own
database and does not follow OPENCODE_DB to find them: opencode auth list
prints the same accounts with and without it. orx runs OpenCode against an
isolated database so sessions never touch the user's history, which therefore
started with no credentials at all - the served API reported no connected
integration, the harness read as signed out while the user was plainly signed
in, and its catalog collapsed to the anonymous free models. Signing in from the
harness could not repair it either, because that login writes back to the
user's own database.

Copy the credential, account, account_state and control_account tables from the
user's database on every prepare. One-way: the user's database is opened
read-only and never written. Present rows are updated in place, never deleted
and re-inserted, because a replacement that half-succeeds would drop a working
sign-in; presence is each table's own primary key, read from the schema
(control_account is keyed by (email, url), account_state by its singleton id).
A table whose key the two schemas disagree on is skipped rather than guessed.

Update policy follows what each table can answer. Where time_updated exists the
user's copy wins only when strictly newer, so a token the isolated server
refreshed mid-turn is not reverted while a deliberate re-login still lands.
account_state carries no timestamp, so it is mirrored: switching accounts must
not leave the isolated store pointing at the old one.

Only shared columns are copied, so schema drift cannot fail startup, and an
unreadable or locked user database is a warning rather than an outage.

Verified against a real installation: 13 stored sign-ins imported, harness
authenticated true, models 9 -> 670.
@AliRezaei-Code
AliRezaei-Code force-pushed the fix/opencode-isolated-credentials branch from b73690a to 99902ff Compare October 24, 2026 23:42
@AliRezaei-Code AliRezaei-Code changed the title fix(opencode): carry sign-ins into the isolated database; label models by provider; add oh-my-pi fix(opencode): carry the user's sign-ins into the isolated database Oct 24, 2026
@AliRezaei-Code

Copy link
Copy Markdown
Contributor Author

Both findings were valid, and both are fixed in the current head (99902ff). The review above is against b73690a8, the superseded three-commit tip — I force-pushed a single-commit branch after splitting the label change and the ACP preset into their own PRs (#783, #784).

P1 — account rows stayed stale. Replacement required id and time_updated; account_state has neither a timestamp nor an id-shaped key, and control_account is keyed by (email, url). The key now comes from the schema (pragma_table_info … WHERE pk > 0), and the update policy is explicit per table:

  • time_updated present (credential, account, control_account) → the user's copy wins only when strictly newer, so a token the isolated server refreshed mid-turn is not reverted;
  • time_updated absent (account_state, the singleton pointer) → mirrored, because switching accounts must not leave the isolated store pointing at the old one.

A table whose key the two schemas disagree on is skipped rather than guessed.

P2 — delete-then-insert could drop a working sign-in. There is no DELETE any more. Present rows are updated in place over shared columns only; inserts happen only when the key is absent. updating_a_row_keeps_columns_the_source_does_not_have reproduces the exact case — the target has a NOT NULL column without a default that the source lacks — and asserts the value survives alongside the updated token.

New tests: the_active_account_pointer_is_mirrored_because_it_carries_no_timestamp, a_console_relogin_updates_the_row_it_shares_its_composite_key_with, updating_a_row_keeps_columns_the_source_does_not_have. cargo test --bin orx opencode_database is 21 passed; full suite 1288 + 17 integration; clippy -D warnings clean.

@greptileai

Comment thread src/local/native_store.rs Outdated
// locked user database must not take the agent offline, and the resulting
// signed-out state is what the harness reports anyway.
if let Err(error) =
opencode_database::inherit_credentials(&db, &opencode_db(NativeStore::Legacy))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Sign-ins use the wrong database

prepare_opencode uses opencode_db(NativeStore::Legacy) as the credential source, but that path honors OPENCODE_DB. OpenCode's sign-in commands ignore that variable, as this change explains. Users who set OPENCODE_DB therefore copy from their session database instead of the database holding their sign-ins, leaving the harness signed out.

Resolve the credential source separately, without the OPENCODE_DB override.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/local/native_store.rs
Line: 699

Comment:
**Sign-ins use the wrong database**

`prepare_opencode` uses `opencode_db(NativeStore::Legacy)` as the credential source, but that path honors `OPENCODE_DB`. OpenCode's sign-in commands ignore that variable, as this change explains. Users who set `OPENCODE_DB` therefore copy from their session database instead of the database holding their sign-ins, leaving the harness signed out.

Resolve the credential source separately, without the `OPENCODE_DB` override.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread src/local/opencode_db.rs
Comment on lines +228 to +231
let reader = open_readonly(source)?;
let mut writer = Connection::open(database)?;
writer.busy_timeout(std::time::Duration::from_secs(5))?;
let transaction = writer.transaction()?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Active account has no row

inherit_credentials reads each source table separately without a source transaction. If the user's OpenCode process adds and selects an account after account is read but before account_state is read, the copy saves the new active_account_id without its account row. OpenCode can then remain signed out until another copy repairs it.

The destination transaction cannot prevent this mismatch. Read all four source tables from one read transaction so the account and its pointer come from the same saved state.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/local/opencode_db.rs
Line: 228-231

Comment:
**Active account has no row**

`inherit_credentials` reads each source table separately without a source transaction. If the user's OpenCode process adds and selects an account after `account` is read but before `account_state` is read, the copy saves the new `active_account_id` without its account row. OpenCode can then remain signed out until another copy repairs it.

The destination transaction cannot prevent this mismatch. Read all four source tables from one read transaction so the account and its pointer come from the same saved state.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

The copy reads the user's own OpenCode database, so a path that is only right on
one OS makes it a silent no-op on the others — and those are exactly the builds
a contributor on macOS cannot click through.

`xdg-basedir`, which OpenCode resolves its data directory through, has no
Windows branch: the database lives under `%USERPROFILE%\.local\share\opencode`
on Windows exactly as it does on macOS and Linux, never under `%APPDATA%`.
Resolution is now one named function instead of an inline expression, asserted
on every runner rather than only on the platform it describes, and it also pins
the two relocations OpenCode honours (`XDG_DATA_HOME` and a custom
`OPENCODE_DB` filename) so a user who moved their data is read from the file
they actually use.

The same crate and the same embedded `ui/dist` serve the CLI, the web dashboard,
the macOS bundle, the Linux AppImage and the Windows app — whose launcher execs
`orx.exe app` — so this path is the only place the platforms could disagree.
Two things the first pass got wrong.

The credential source was `opencode_db(NativeStore::Legacy)`, which honours
`OPENCODE_DB`. OpenCode does not honour it for credentials: a user who points
that variable at another database for their own sessions still signs in to the
default one, so the copy would read their session database and leave the harness
exactly as signed out as before. `opencode_credential_db` is now that default
path, resolved the same way on every platform, while session lookup keeps
following the override — a session may legitimately live where the user put it.

The table reads were independent, so a concurrent sign-in could commit `account`
between the reads and let `account_state` name an account the copy never saw.
All four tables are now read inside one read transaction, so they agree with each
other.

Tests pin both: the credential path ignores `OPENCODE_DB` while the session path
follows it.
@AliRezaei-Code

Copy link
Copy Markdown
Contributor Author

Both new findings were valid; both are fixed in b71b8848.

P1 — sign-ins read from the wrong database. Correct, and it is the same trap this PR documents one layer up. opencode_db(Legacy) honours OPENCODE_DB, so a user who points that variable at their own database would have had their session database read for credentials — the override is exactly what OpenCode ignores when signing in. There are now two paths, named for what they are for:

  • opencode_credential_db() — the default .../opencode/opencode.db, never the override. This is what the copy reads.
  • legacy_opencode_db() — session lookup, which does follow OPENCODE_DB, because a session may legitimately live in the database the user selected.

a_session_database_override_is_followed_but_never_used_for_credentials asserts both halves in one test, so the two cannot drift back together.

P1 — account_state could name an account that was never copied. Correct. The four tables were read independently; a sign-in committing account between the account and account_state reads could leave the pointer dangling. All reads now happen inside one read transaction on the source, so the copy sees a single consistent snapshot.

cargo fmt --all --check, cargo clippy --all-targets --locked -- -D warnings, and cargo test --locked (1291 unit + 17 integration) all pass on b71b8848.

@greptileai

@greptile-apps

greptile-apps Bot commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Thanks for the fixes. I verified the current head (b71b8848): credentials are read from the default OpenCode database rather than the OPENCODE_DB session override, and the source tables are read within one transaction snapshot. Both prior findings are resolved.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

@AliRezaei-Code

Copy link
Copy Markdown
Contributor Author

This one looks computed against the parent commit (c14d62e4): in b71b8848 the source is read through a single read transaction — let snapshot = reader.unchecked_transaction()? at line 235, and every table is read through it at line 307. Nothing to change.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenCode harness reports Signed out while OpenCode is signed in: the isolated database never receives the user's credentials

1 participant