authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-10-14 03:12:10-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-14 03:12:10-07:00
log40d43a19670dd059f8d5705f801db7208620a2df
tree3ab2aa1c7c7f2beac5c91ce592b0e9000780ceac
parentffceb61fcbbdc7ac5268812a1cc5dfedd7a6f7b9
signature Signed by PGP key B5690EEEBB952194

fix(client-runtime): a new WSL backend no longer shows Limited permissions until reload (#19361)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

2 files changed, 64 insertions(+), 2 deletions(-)

packages/client-runtime/src/connection/registry.test.ts+42
......@@ -1410,6 +1410,48 @@ describe("EnvironmentRegistry", () => {
14101410 }),
14111411 );
14121412
1413 // The desktop rotates the bootstrap token, so the renderer exchanges it again
1414 // and the server revokes the old bearer while the socket stays open. HTTP
1415 // reads such as the session's scopes must move to the new bearer (#13324).
1416 it.effect("a replaced platform bearer reaches a connected session's HTTP requests", () =>
1417 Effect.gen(function* () {
1418 const harness = yield* makeHarness([]);
1419
1420 yield* Effect.gen(function* () {
1421 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1422 const registration = (token: string) =>
1423 new BearerConnectionRegistration({
1424 target: BEARER_TARGET,
1425 profile: BEARER_PROFILE,
1426 credential: new BearerConnectionCredential({ token }),
1427 });
1428 const preparedToken = registry.run(
1429 BEARER_TARGET.environmentId,
1430 EnvironmentSupervisor.EnvironmentSupervisor.pipe(
1431 Effect.flatMap((supervisor) => SubscriptionRef.get(supervisor.prepared)),
1432 Effect.map((prepared) =>
1433 Option.isSome(prepared) && prepared.value.httpAuthorization?._tag === "Bearer"
1434 ? prepared.value.httpAuthorization.token
1435 : null,
1436 ),
1437 ),
1438 );
1439 yield* registry.reconcilePlatform([registration("old-bearer")]);
1440 yield* awaitConnectionState(
1441 registry,
1442 BEARER_TARGET.environmentId,
1443 (state) => state.phase === "connected",
1444 );
1445 expect(yield* preparedToken).toBe("old-bearer");
1446
1447 yield* registry.reconcilePlatform([registration("new-bearer")]);
1448
1449 expect(yield* preparedToken).toBe("new-bearer");
1450 expect(yield* Ref.get(harness.sessions)).toHaveLength(1);
1451 }).pipe(Effect.provide(harness.layer), Effect.scoped);
1452 }),
1453 );
1454
14131455 it.effect("a bearer replaced while an attempt is in flight retries with the new one", () =>
14141456 Effect.gen(function* () {
14151457 const attemptStarted = yield* Deferred.make<void>();
packages/client-runtime/src/connection/registry.ts+22-2
......@@ -47,6 +47,7 @@ import {
4747 RELAY_ROUTE_ID,
4848 connectionRouteId,
4949 connectionRoutes,
50 credentialConnectionId,
5051 entryWithRoutes,
5152 findRouteToSameAddress,
5253 mergeLearnedRoutes,
......@@ -712,8 +713,10 @@ export const make = Effect.gen(function* () {
712713 // the equivalent runtime. Anything not yet connected may be using the
713714 // old bearer: an attempt in flight would fail with it and then wait
714715 // blocked for a manual retry. Retry with the new one instead. A
715 // connected session keeps its socket.
716 if (bearerReplaced) {
716 // connected session keeps its socket, but the server revoked the old
717 // bearer, so its HTTP requests (such as the session's scopes) move to
718 // the new one.
719 if (bearerReplaced && registration._tag === "BearerConnectionRegistration") {
717720 const scope = (yield* SubscriptionRef.get(serviceScopes)).get(target.environmentId);
718721 if (scope !== undefined) {
719722 const state = yield* SubscriptionRef.get(scope.supervisor.state);
......@@ -725,6 +728,23 @@ export const make = Effect.gen(function* () {
725728 state.lastFailure.reason === "authentication")
726729 ) {
727730 yield* scope.supervisor.retryNow;
731 } else if (state.phase === "connected") {
732 const token = registration.credential.token;
733 yield* SubscriptionRef.updateSome(scope.supervisor.prepared, (current) =>
734 Option.isSome(current) &&
735 current.value.target._tag === "BearerConnectionTarget" &&
736 credentialConnectionId(current.value.target.connectionId) ===
737 registration.target.connectionId &&
738 current.value.httpAuthorization?._tag === "Bearer" &&
739 current.value.httpAuthorization.token !== token
740 ? Option.some(
741 Option.some({
742 ...current.value,
743 httpAuthorization: { _tag: "Bearer", token },
744 }),
745 )
746 : Option.none(),
747 );
728748 }
729749 }
730750 }