Open History to see the last commit
1// @effect-diagnostics nodeBuiltinImport:off - checks the host's default Codex directory without writing to it.
2import * as NodeURL from "node:url";
3import * as HostProcess from "@t3tools/shared/HostProcess";
4import * as Stream from "effect/Stream";
5import { ChildProcess, ChildProcessSpawner } from "effect/process";
6import * as CodexManagedTokenBroker from "./CodexManagedTokenBroker.ts";
7import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
8import * as NodeServices from "@effect/platform-node/NodeServices";
9import { assert, it } from "@effect/vitest";
10import { CodexSettings, EnvironmentId, ProviderInstanceId } from "@t3tools/contracts";
11import * as Clock from "effect/Clock";
12import * as Effect from "effect/Effect";
13import * as FileSystem from "effect/FileSystem";
14import * as Path from "effect/Path";
15import * as Layer from "effect/Layer";
16import * as Option from "effect/Option";
17import * as Schema from "effect/Schema";
18import { HttpClient, HttpClientResponse } from "effect/http";
19import * as ServerConfig from "../config.ts";
20import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
21import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
22import * as CodexInstallation from "./CodexInstallation.ts";
23import { makeCodexManagedRuntime } from "./CodexManagedRuntime.ts";
24import * as ProviderCredentialStore from "./ProviderCredentialStore.ts";
25import { codexAppServerArgs } from "./codexLaunchArgs.ts";
26
27const encodeJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
28const decodeSettings = Schema.decodeSync(CodexSettings);
29const decodeCommand = Schema.decodeUnknownSync(
30 Schema.fromJsonString(Schema.Tuple([Schema.String, Schema.Array(Schema.String)])),
31);
32it.live.each(
33 (["managed", "local"] as const).flatMap((source) =>
34 (["primary", "additional"] as const).map((account) => ({ source, account })),
35 ),
36)(
37 "$source Codex $account account shares home state without routing owned tokens through ambient CLI overrides",
38 ({ source, account }) =>
39 Effect.gen(function* () {
40 const instanceId = ProviderInstanceId.make(
41 account === "primary" ? "codex" : "codex-personal",
42 );
43 const fs = yield* FileSystem.FileSystem;
44 const path = yield* Path.Path;
45 const sharedHome = yield* fs.makeTempDirectoryScoped({ prefix: "codex-shared-home-" });
46 yield* fs.writeFileString(path.join(sharedHome, "auth.json"), "native-auth-unchanged");
47 yield* fs.writeFileString(path.join(sharedHome, "config.toml"), "# shared config\n");
48 const data = new Map<string, Uint8Array>();
49 let renewals = 0;
50 const secrets = ServerSecretStore.ServerSecretStore.of({
51 get: (key) => Effect.sync(() => Option.fromUndefinedOr(data.get(key))),
52 set: (key, value) =>
53 Effect.sync(() => {
54 data.set(key, value);
55 }),
56 remove: (key) =>
57 Effect.sync(() => {
58 data.delete(key);
59 }),
60 create: () => Effect.die("unused"),
61 getOrCreateRandom: () => Effect.die("unused"),
62 });
63 let leases = 0;
64 const executable = {
65 executablePath:
66 source === "managed" ? "/isolated/tools/codex/0.156.1/bin/codex" : "/user/bin/codex",
67 managedVersionDirectory: source === "managed" ? "/isolated/tools/codex/0.156.1" : null,
68 source,
69 version: "0.156.1",
70 };
71 const layerInstaller = Layer.mock(CodexInstallation.CodexInstallation)({
72 managedDirectory: "/isolated/tools/codex",
73 resolve: () => Effect.succeed(executable),
74 acquire: () =>
75 Effect.gen(function* () {
76 leases++;
77 yield* Effect.addFinalizer(() =>
78 Effect.sync(() => {
79 leases--;
80 }),
81 );
82 return executable;
83 }),
84 });
85 yield* Effect.gen(function* () {
86 const store = yield* ProviderCredentialStore.make("codex-chatgpt", instanceId);
87 const json = yield* encodeJson({
88 clientId: "oaiapp_test",
89 accessToken: "dummy-owned-access",
90 refreshToken: "dummy-refresh",
91 expiresAt: (yield* Clock.currentTimeMillis) + 3_600_000,
92 earliestRefreshAt: null,
93 scopes: ["chatgpt.tokens.use.direct"],
94 subject: "test-user",
95 email: null,
96 });
97 yield* store.set(new TextEncoder().encode(json));
98 const hostEnv = yield* HostProcess.Environment;
99 const ambient = {
100 CODEX_HOME: "/user/.codex",
101 ACCESS_TOKEN: "ambient-bearer",
102 T3CODE_HOME: path.join(sharedHome, "wrong-server-home"),
103 OPENAI_API_KEY: "dummy-global-key",
104 OPENAI_BASE_URL: "https://user-proxy.test",
105 T3CODE_CODEX_LAUNCH_ARGS: "--config model_provider=global-proxy",
106 PATH: hostEnv.PATH,
107 SystemRoot: hostEnv.SystemRoot,
108 };
109 const runtime = yield* makeCodexManagedRuntime({
110 instanceId,
111 enabled: true,
112 config: decodeSettings({ setupMode: "managed", homePath: sharedHome }),
113 environment: ambient,
114 });
115 yield* Effect.gen(function* () {
116 const effective = yield* runtime.resolve;
117 assert.strictEqual(leases, 1);
118 assert.strictEqual(effective.config.binaryPath, executable.executablePath);
119 assert.notStrictEqual(effective.config.homePath, ambient.CODEX_HOME);
120 assert.equal(runtime.homeLayout.sharedHomePath, sharedHome);
121 if (account === "primary") {
122 assert.equal(effective.config.homePath, sharedHome);
123 assert.equal(runtime.homeLayout.mode, "direct");
124 } else {
125 assert.include(effective.config.homePath, instanceId);
126 assert.include(effective.config.homePath, "userdata/providers/codex");
127 assert.equal(runtime.homeLayout.mode, "authOverlay");
128 assert.equal(
129 yield* fs.readLink(path.join(effective.config.homePath, "sessions")),
130 path.join(sharedHome, "sessions"),
131 );
132 assert.equal(
133 yield* fs.readLink(path.join(effective.config.homePath, "config.toml")),
134 path.join(sharedHome, "config.toml"),
135 );
136 assert.isFalse(yield* fs.exists(path.join(effective.config.homePath, "auth.json")));
137 }
138 assert.isUndefined(effective.environment.ACCESS_TOKEN);
139 assert.isUndefined(effective.environment.OPENAI_API_KEY);
140 assert.isUndefined(effective.environment.OPENAI_BASE_URL);
141 assert.isUndefined(effective.environment.T3CODE_CODEX_LAUNCH_ARGS);
142 const args = codexAppServerArgs(effective.config.launchArgs);
143 assert.include(args, 'model_provider="openai_token_sharing"');
144 const provider = args.find((arg) =>
145 arg.startsWith("model_providers.openai_token_sharing="),
146 )!;
147 assert.include(provider, 'base_url="https://api.openai.com/v1"');
148 assert.include(provider, 'model_catalog_url="https://api.openai.com/v1/models"');
149 assert.notInclude(provider, "env_key");
150 assert.include(provider, "refresh_interval_ms=0");
151 // regression: #16132 Codex fetches each token through this helper, so a running
152 // session picks up renewals instead of keeping the token it launched with.
153 if (source === "managed" && account === "primary") {
154 const commandMatch = /auth=\{command=(.*),args=(\[.*\]),timeout_ms=/u.exec(provider);
155 assert.isNotNull(commandMatch);
156 const [command, helperArgs] = decodeCommand(
157 `[${commandMatch![1]},${commandMatch![2]}]`,
158 );
159 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
160 const runHelper = Effect.gen(function* () {
161 const child = yield* spawner.spawn(
162 ChildProcess.make(command, helperArgs, {
163 extendEnv: false,
164 env: effective.environment,
165 }),
166 );
167 const [stdout, stderr, code] = yield* Effect.all(
168 [
169 child.stdout.pipe(Stream.decodeText(), Stream.mkString),
170 child.stderr.pipe(Stream.decodeText(), Stream.mkString),
171 child.exitCode,
172 ],
173 { concurrency: "unbounded" },
174 );
175 return { stdout, stderr, code: Number(code) };
176 }).pipe(Effect.scoped);
177 assert.deepEqual(yield* runHelper, {
178 stdout: "dummy-owned-access",
179 stderr: "",
180 code: 0,
181 });
182 const saved = Option.getOrThrow(yield* runtime.auth.read);
183 const expired = yield* encodeJson({ ...saved, expiresAt: 0 });
184 yield* store.set(new TextEncoder().encode(expired));
185 assert.deepEqual(yield* runHelper, { stdout: "renewed-access", stderr: "", code: 0 });
186 assert.equal(renewals, 1);
187 assert.equal(
188 Option.getOrThrow(yield* runtime.auth.read).refreshToken,
189 "renewed-refresh",
190 );
191 assert.equal((yield* runtime.resolve).revision, effective.revision);
192 const replaced = yield* encodeJson({ ...saved, subject: "replacement-user" });
193 yield* store.set(new TextEncoder().encode(replaced));
194 const refused = yield* runHelper;
195 assert.equal(refused.code, 1);
196 assert.equal(refused.stdout, "");
197 assert.isFalse(yield* fs.exists(ambient.T3CODE_HOME));
198 }
199 assert.include(args, "features.api_key_model_discovery=true");
200 assert.include(args, "features.multi_agent=false");
201 assert.include(args, "features.multi_agent_v2=false");
202 assert.notInclude(effective.config.launchArgs, "model_catalog_json");
203 assert.notInclude(effective.config.launchArgs, "x-openai-chatpass-test");
204 assert.include(provider, "supports_websockets=false");
205 assert.include(provider, "requires_openai_auth=false");
206 for (const secret of ["dummy-owned-access", "dummy-refresh", "ambient-bearer"]) {
207 assert.notInclude(effective.config.launchArgs, secret);
208 assert.notInclude(JSON.stringify(effective.environment), secret);
209 }
210 assert.strictEqual(ambient.CODEX_HOME, "/user/.codex");
211 }).pipe(Effect.scoped);
212 assert.strictEqual(leases, 0);
213 yield* runtime.auth.controller.logout(Effect.void);
214 assert.equal(
215 yield* fs.readFileString(path.join(sharedHome, "auth.json")),
216 "native-auth-unchanged",
217 );
218 }).pipe(
219 Effect.provide(CodexManagedTokenBroker.layer),
220 Effect.provideService(HostProcess.Arguments, [
221 process.execPath,
222 NodeURL.fileURLToPath(new URL("../bin.ts", import.meta.url)),
223 ]),
224 Effect.provideService(ServerSecretStore.ServerSecretStore, secrets),
225 Effect.provideService(ServerEnvironment.ServerEnvironmentIdentity, {
226 getEnvironmentId: Effect.succeed(
227 EnvironmentId.make("00000000-0000-4000-8000-000000000001"),
228 ),
229 }),
230 Effect.provide(layerInstaller),
231 Effect.provideService(
232 HttpClient.HttpClient,
233 HttpClient.make((request) =>
234 Effect.sync(() => {
235 assert.isTrue(
236 [
237 "https://auth.openai.com/.well-known/openid-configuration",
238 "https://auth.openai.com/revoke",
239 "https://auth.openai.com/api/accounts/oauth/token",
240 ].includes(request.url),
241 );
242 if (request.url.endsWith("/token")) {
243 renewals++;
244 return HttpClientResponse.fromWeb(
245 request,
246 Response.json({
247 access_token: "renewed-access",
248 refresh_token: "renewed-refresh",
249 token_type: "Bearer",
250 expires_in: 3600,
251 scope: "chatgpt.tokens.use.direct",
252 }),
253 );
254 }
255 return HttpClientResponse.fromWeb(
256 request,
257 request.url.endsWith("/revoke")
258 ? new Response(null, { status: 200 })
259 : Response.json({
260 issuer: "https://auth.openai.com",
261 authorization_endpoint: "https://auth.openai.com/api/accounts/authorize",
262 token_endpoint: "https://auth.openai.com/api/accounts/oauth/token",
263 jwks_uri: "https://auth.openai.com/jwks",
264 revocation_endpoint: "https://auth.openai.com/revoke",
265 }),
266 );
267 }),
268 ),
269 ),
270 );
271 }).pipe(
272 Effect.scoped,
273 Effect.provide(
274 ServerConfig.layerTest(process.cwd(), {
275 prefix: "t3-managed-runtime-",
276 }).pipe(Layer.provideMerge(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))),
277 ),
278 ),
279);