fix(desktop): authenticate profile session slices against oauth gateways - #71503
fix(desktop): authenticate profile session slices against oauth gateways#71503zakhounet wants to merge 1 commit into
Conversation
The two batched `/api/profiles/sessions` dispatches resolved the primary
connection and then passed `conn.token` straight to `fetchJson`, skipping the
`authMode` branch that `requestJsonForProfile` applies to every other
per-profile path. Against an oauth-gated gateway that token is not a valid
credential, so both requests 401. Each call site collapses a rejected fetch
into `{ sessions: [], total: 0 }`, so the failure surfaces nowhere: the
Desktop session sidebar renders empty for the root profile while the sessions
are intact server-side and the named profiles list normally.
Route both dispatches through `fetchJsonForProfile`, and name the branch they
skipped as `resolveProfileRestAuth`. It delegates to `resolveReadinessProbeAuth`
and keeps a single deliberate difference: a REST call retains the connection
token when `authMode` is not yet known, where the readiness probe treats an
unknown gateway as public — probing anonymously is safe, dropping the
credential from a real request is not. Unit tests pin both the oauth contract
and that divergence.
Fixes NousResearch#67600
|
Closing this — #67600 is already fixed on Verified before closing: a Desktop client built from current The only piece here not already in 704a321 was the |
What does this PR do?
The Desktop session sidebar renders empty for the root (
default) profile on an oauth-gated remote gateway, while named profiles list normally and the sessions are intact server-side.The two batched
/api/profiles/sessionsdispatches inmain.tsresolved the primary connection and then passedconn.tokenstraight tofetchJson:That skips the
authModebranchrequestJsonForProfileapplies to every other per-profile path. Against an oauth gateway the connection token is not a valid credential — the request 401s. Both call sites collapse a rejected fetch into{ sessions: [], total: 0, profile_totals: {} }, so the failure surfaces nowhere: no error, no toast, just an empty list that reads as data loss.This routes both dispatches through the existing
fetchJsonForProfilehelper, and extracts the branch they skipped into a pure, unit-tested decision —resolveProfileRestAuth— which reusesresolveOauthRestAuthrather than duplicating it. It states the contract those call sites broke: only a non-oauth gateway may authenticate with the connection token.This follows the precedent set by
native-auth-decisions.ts, whose own header says the value of these seams is "the test that pins the contract so the god-file call sites can't drift back to the buggy shape". These two call sites are exactly such a drift, so the fix is added as a fourth seam in that same module.Related Issue
Fixes #67600
Type of Change
Changes Made
apps/desktop/electron/native-auth-decisions.ts— addresolveProfileRestAuth(authMode, nativeAccessToken, connectionToken)and theProfileRestAuthtype; delegates toresolveOauthRestAuthfor the oauth case. Header docblock extended with the fourth seam.apps/desktop/electron/main.ts—requestJsonForProfilenow delegates its credential choice toresolveProfileRestAuth(no behaviour change);fetchProfilesSessionSliceandmergeRemoteProfileSessionsroute throughfetchJsonForProfileinstead of a rawfetchJson(…, primary.token).apps/desktop/electron/native-auth-decisions.test.ts— 5 tests covering non-oauth, missingauthMode, oauth-with-bearer, oauth-without-bearer, and the regression itself: an oauth gateway must never resolve to the connection token.How to Test
Reproduce (before this change) — remote Desktop against an oauth-gated gateway, multi-profile install:
default) profile. The sidebar showsSESSIONS 0— or only the session created in the current run.local,minimax): sessions list normally, so the sidebar component itself works.default:SESSIONS 0again, while the profile DB holds 800+ non-archived sessions./api/profiles/sessions?profile=default): rows are returned. Request correct, data available, screen empty.tcpdumpon the fan-out shows the dispatches returning 401, swallowed by the.catch().Verify the fix:
The regression test was validated by mutation: removing the
authModebranch fromresolveProfileRestAuth(i.e. reintroducing the bug) fails 3 tests, includingresolveProfileRestAuth never authenticates an oauth gateway with the connection token. Restoring it turns them green again — the tests fail for the right reason, rather than passing by construction.Runtime check: a Desktop built from this branch lists the root profile's sessions immediately on an oauth-gated remote gateway; the patched build has been in daily use for three days.
Checklist
Code
fix(scope):,feat(scope):, etc.)vitest --project electron(730 passed),npm run check:lint(0 errors). The change is TypeScript-only, sopytest tests/is untouched by it.Documentation & Housekeeping
docs/, docstrings) — the new seam is documented in thenative-auth-decisions.tsheader alongside the existing threecli-config.yaml.exampleif I added/changed config keys — N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/ANotes on scope
resolveProfileRestAuthdeliberately stops at the decision.requestJsonForProfilekeeps ownership ofensureNativeAccessTokenand of performing the request, so this PR changes no network behaviour for any path that was already correct — the oauth and non-oauth branches resolve exactly as before. The only behavioural change is that the two session-slice dispatches now take the same branch as every other per-profile call.Related but distinct: #70944 / #42467 describe the same symptom (an empty Desktop sidebar) with a different cause — a profile
state.dbwhose schema lags, raising inside the read-only aggregation endpoint. That path is server-side and is addressed by #70964. This PR is the client-side auth path: the DB here is migrated and the endpoint returns rows when replayed directly. The two do not overlap, and neither fix subsumes the other.