From ba84ca8306029c4c26e334974acaaf76bee3770f Mon Sep 17 00:00:00 2001 From: Invix Date: Wed, 11 Nov 2026 12:28:50 +0100 Subject: [PATCH 1/2] fix(server): re-probe Codex when its auth.json changes Signing in again with `codex login` (or out with `codex logout`) rewrites the instance's auth.json outside T3, but nothing noticed, so the provider card kept the previous auth error until the next health interval (5 minutes by default) or a manual Retry. Watch the instance's auth home (the shadow home when one is set, otherwise the shared home) and run the same forced refresh Retry uses, debounced, when auth.json changes. The watcher lives in the instance scope. Fixes #34295 --- .../src/provider/Drivers/CodexDriver.test.ts | 51 +++++++++++++++++++ .../src/provider/Drivers/CodexDriver.ts | 13 +++++ 2 files changed, 64 insertions(+) diff --git a/apps/server/src/provider/Drivers/CodexDriver.test.ts b/apps/server/src/provider/Drivers/CodexDriver.test.ts index 868dc9c0af03b..4f15cbd480f82 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.test.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.test.ts @@ -17,6 +17,7 @@ import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as PlatformError from "effect/PlatformError"; +import * as Queue from "effect/Queue"; import * as Schema from "effect/Schema"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; @@ -726,3 +727,53 @@ it.layer(layerTest)("CodexDriver", (it) => { { skip: windowsHost }, ); }); + +// Live: the auth watcher debounces on the real clock. +it.live("probes again when auth.json in the Codex home changes", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const codexHome = yield* fs.makeTempDirectoryScoped({ prefix: "t3-codex-auth-watch-" }); + const watchedPath = yield* Deferred.make(); + const homeEvents = yield* Queue.unbounded(); + const probes = yield* Queue.unbounded(); + yield* CodexDriver.create({ + instanceId: ProviderInstanceId.make("codex-auth-watch"), + displayName: "Codex auth watch", + enabled: true, + environment: [], + config: { ...CodexDriver.defaultConfig(), homePath: codexHome }, + }).pipe( + // OS watchers can miss events written right after they start, so the test supplies them. + Effect.provideService(FileSystem.FileSystem, { + ...fs, + watch: (path) => + Stream.unwrap( + Deferred.succeed(watchedPath, path).pipe(Effect.as(Stream.fromQueue(homeEvents))), + ), + }), + Effect.provideService( + ChildProcessSpawner.ChildProcessSpawner, + ChildProcessSpawner.make((command) => + (ChildProcess.isStandardCommand(command) && command.args.includes("app-server") + ? Queue.offer(probes, undefined) + : Effect.void + ).pipe( + Effect.andThen( + PlatformError.badArgument({ + module: "ChildProcessSpawner", + method: "spawn", + description: "The fixture has no Codex executable", + }), + ), + ), + ), + ), + ); + expect(yield* Deferred.await(watchedPath)).toBe(codexHome); + yield* Queue.take(probes); + + yield* Queue.offer(homeEvents, { _tag: "Update", path: "auth.json" }); + + yield* Queue.take(probes); + }).pipe(Effect.scoped, Effect.provide(layerTest)), +); diff --git a/apps/server/src/provider/Drivers/CodexDriver.ts b/apps/server/src/provider/Drivers/CodexDriver.ts index dee8a04aee9fa..58ef7d27142c9 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.ts @@ -2,10 +2,12 @@ import { CodexSettings, ProviderDriverKind } from "@t3tools/contracts"; import * as Digest from "effect/crypto/Digest"; import * as SecureRandom from "effect/crypto/SecureRandom"; import type * as Signature from "effect/crypto/Signature"; +import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; +import * as Stream from "effect/Stream"; import { HttpClient } from "effect/http"; import { ChildProcessSpawner } from "effect/process"; @@ -371,6 +373,17 @@ export const CodexDriver: ProviderDriver< ), ); + // `codex login` and `codex logout` rewrite auth.json outside T3. + if (effectiveConfig.enabled) { + yield* fileSystem.watch(accountKey).pipe( + Stream.filter((event) => pathService.basename(event.path) === "auth.json"), + Stream.debounce(Duration.millis(250)), + Stream.runForEach(() => snapshot.refresh.pipe(Effect.ignoreCause({ log: true }))), + Effect.catchReason("PlatformError", "NotFound", () => Effect.void), + Effect.ignoreCause({ log: true }), + Effect.forkScoped, + ); + } return { instanceId, driverKind: DRIVER_KIND, From e700b34eac89fe080a073e4216355c2d58c8b0ee Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 19:23:21 +0000 Subject: [PATCH 2/2] test(server): run the Codex auth watcher test on the test clock it.live is reserved for subprocess and network fixtures; advance the debounce with TestClock inside the existing CodexDriver layer instead. AI-assisted; verified with the tests and tools listed above. --- .../src/provider/Drivers/CodexDriver.test.ts | 91 ++++++++++--------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/apps/server/src/provider/Drivers/CodexDriver.test.ts b/apps/server/src/provider/Drivers/CodexDriver.test.ts index 4f15cbd480f82..0c4fb617a27c5 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.test.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.test.ts @@ -21,6 +21,7 @@ import * as Queue from "effect/Queue"; import * as Schema from "effect/Schema"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; +import * as TestClock from "effect/testing/TestClock"; import { HttpClient, HttpClientResponse } from "effect/http"; import * as ChildProcess from "effect/process/ChildProcess"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; @@ -726,54 +727,54 @@ it.layer(layerTest)("CodexDriver", (it) => { }).pipe(Effect.scoped), { skip: windowsHost }, ); -}); -// Live: the auth watcher debounces on the real clock. -it.live("probes again when auth.json in the Codex home changes", () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const codexHome = yield* fs.makeTempDirectoryScoped({ prefix: "t3-codex-auth-watch-" }); - const watchedPath = yield* Deferred.make(); - const homeEvents = yield* Queue.unbounded(); - const probes = yield* Queue.unbounded(); - yield* CodexDriver.create({ - instanceId: ProviderInstanceId.make("codex-auth-watch"), - displayName: "Codex auth watch", - enabled: true, - environment: [], - config: { ...CodexDriver.defaultConfig(), homePath: codexHome }, - }).pipe( - // OS watchers can miss events written right after they start, so the test supplies them. - Effect.provideService(FileSystem.FileSystem, { - ...fs, - watch: (path) => - Stream.unwrap( - Deferred.succeed(watchedPath, path).pipe(Effect.as(Stream.fromQueue(homeEvents))), - ), - }), - Effect.provideService( - ChildProcessSpawner.ChildProcessSpawner, - ChildProcessSpawner.make((command) => - (ChildProcess.isStandardCommand(command) && command.args.includes("app-server") - ? Queue.offer(probes, undefined) - : Effect.void - ).pipe( - Effect.andThen( - PlatformError.badArgument({ - module: "ChildProcessSpawner", - method: "spawn", - description: "The fixture has no Codex executable", - }), + it.effect("probes again when auth.json in the Codex home changes", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const codexHome = yield* fs.makeTempDirectoryScoped({ prefix: "t3-codex-auth-watch-" }); + const watchedPath = yield* Deferred.make(); + const homeEvents = yield* Queue.unbounded(); + const probes = yield* Queue.unbounded(); + yield* CodexDriver.create({ + instanceId: ProviderInstanceId.make("codex-auth-watch"), + displayName: "Codex auth watch", + enabled: true, + environment: [], + config: { ...CodexDriver.defaultConfig(), homePath: codexHome }, + }).pipe( + // OS watchers can miss events written right after they start, so the test supplies them. + Effect.provideService(FileSystem.FileSystem, { + ...fs, + watch: (path) => + Stream.unwrap( + Deferred.succeed(watchedPath, path).pipe(Effect.as(Stream.fromQueue(homeEvents))), + ), + }), + Effect.provideService( + ChildProcessSpawner.ChildProcessSpawner, + ChildProcessSpawner.make((command) => + (ChildProcess.isStandardCommand(command) && command.args.includes("app-server") + ? Queue.offer(probes, undefined) + : Effect.void + ).pipe( + Effect.andThen( + PlatformError.badArgument({ + module: "ChildProcessSpawner", + method: "spawn", + description: "The fixture has no Codex executable", + }), + ), ), ), ), - ), - ); - expect(yield* Deferred.await(watchedPath)).toBe(codexHome); - yield* Queue.take(probes); + ); + expect(yield* Deferred.await(watchedPath)).toBe(codexHome); + yield* Queue.take(probes); - yield* Queue.offer(homeEvents, { _tag: "Update", path: "auth.json" }); + yield* Queue.offer(homeEvents, { _tag: "Update", path: "auth.json" }); + yield* TestClock.adjust("250 millis"); - yield* Queue.take(probes); - }).pipe(Effect.scoped, Effect.provide(layerTest)), -); + yield* Queue.take(probes); + }).pipe(Effect.scoped), + ); +});