1import { ProviderDriverKind, type CodexSettings } from "@t3tools/contracts";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Path from "effect/Path";
5import * as Schema from "effect/Schema";
6import * as PlatformError from "effect/PlatformError";
7
8import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
9import * as HostProcess from "@t3tools/shared/HostProcess";
10
11export interface CodexHomeLayout {
12 readonly mode: "direct" | "authOverlay";
13 readonly sharedHomePath: string;
14 readonly effectiveHomePath: string | undefined;
15 readonly continuationKey: string;
16}
17
18const KNOWN_SHARED_DIRECTORIES = [
19 "sessions",
20 "archived_sessions",
21 "sqlite",
22 "shell_snapshots",
23 "worktrees",
24 "skills",
25 "plugins",
26 "cache",
27 "logs",
28 "mcp-oauth-locks",
29] as const;
30
31const PRIVATE_ENTRY_NAMES = new Set(["auth.json", "models_cache.json"]);
32const SHADOW_LOCAL_ENTRY_NAMES = new Set(["log", "memories", "tmp"]);
33// Codex creates these runtime locks itself when it starts in a shadow home before the shared
34// home has one. They guard shared state, so a local copy is replaced with the shared link.
35const REPLACEABLE_SHARED_RUNTIME_ENTRIES = new Set(["mcp-oauth-locks", ".sqlite-maintenance.lock"]);
36
37function resolveHomePath(path: Path.Path, home: string, value: string | undefined): string {
38 const expanded =
39 value && value.trim().length > 0 ? expandHomePath(value, home) : path.join(home, ".codex");
40 return path.resolve(expanded);
41}
42
43export const resolveCodexHomeLayout = Effect.fn("resolveCodexHomeLayout")(function* (
44 config: CodexSettings,
45): Effect.fn.Return<CodexHomeLayout, never, Path.Path> {
46 const path = yield* Path.Path;
47 const home = yield* HostProcess.HomeDirectory;
48 const sharedHomePath = resolveHomePath(path, home, config.homePath);
49 const shadowHomePath = config.shadowHomePath.trim();
50 if (shadowHomePath.length === 0) {
51 return {
52 mode: "direct",
53 sharedHomePath,
54 effectiveHomePath: config.homePath.trim().length > 0 ? sharedHomePath : undefined,
55 continuationKey: `codex:home:${sharedHomePath}`,
56 };
57 }
58
59 const effectiveHomePath = path.resolve(expandHomePath(shadowHomePath, home));
60 return {
61 mode: "authOverlay",
62 sharedHomePath,
63 effectiveHomePath,
64 continuationKey: `codex:home:${sharedHomePath}`,
65 };
66});
67
68const CodexShadowHomeContext = {
69 sharedHomePath: Schema.String,
70 effectiveHomePath: Schema.String,
71};
72
73export class CodexShadowHomeFileSystemError extends Schema.TaggedError<CodexShadowHomeFileSystemError>()(
74 "CodexShadowHomeFileSystemError",
75 {
76 ...CodexShadowHomeContext,
77 operation: Schema.Literals(["readLink", "makeDirectory", "readDirectory", "remove", "symlink"]),
78 path: Schema.String,
79 targetPath: Schema.optional(Schema.String),
80 entryName: Schema.optional(Schema.String),
81 cause: Schema.Defect(),
82 },
83) {
84 override get message(): string {
85 const target = this.targetPath === undefined ? "" : ` to '${this.targetPath}'`;
86 return `Codex shadow home filesystem operation '${this.operation}' failed for '${this.path}'${target}.`;
87 }
88}
89
90export class CodexShadowHomePathConflictError extends Schema.TaggedError<CodexShadowHomePathConflictError>()(
91 "CodexShadowHomePathConflictError",
92 CodexShadowHomeContext,
93) {
94 override get message(): string {
95 return `Codex shadow home path '${this.effectiveHomePath}' must be different from the shared home path '${this.sharedHomePath}'.`;
96 }
97}
98
99export class CodexShadowHomeEntryConflictError extends Schema.TaggedError<CodexShadowHomeEntryConflictError>()(
100 "CodexShadowHomeEntryConflictError",
101 {
102 ...CodexShadowHomeContext,
103 entryName: Schema.String,
104 linkPath: Schema.String,
105 targetPath: Schema.String,
106 },
107) {
108 override get message(): string {
109 return `Cannot create Codex shadow home entry '${this.entryName}' because '${this.linkPath}' already exists and is not a symlink.`;
110 }
111}
112
113export class CodexShadowHomePrivateEntrySymlinkError extends Schema.TaggedError<CodexShadowHomePrivateEntrySymlinkError>()(
114 "CodexShadowHomePrivateEntrySymlinkError",
115 {
116 ...CodexShadowHomeContext,
117 entryName: Schema.String,
118 path: Schema.String,
119 },
120) {
121 override get message(): string {
122 return `Codex shadow home private entry '${this.entryName}' at '${this.path}' must be a real file, not a symlink.`;
123 }
124}
125
126export const CodexShadowHomeError = Schema.Union([
127 CodexShadowHomeFileSystemError,
128 CodexShadowHomePathConflictError,
129 CodexShadowHomeEntryConflictError,
130 CodexShadowHomePrivateEntrySymlinkError,
131]);
132export type CodexShadowHomeError = typeof CodexShadowHomeError.Type;
133
134type LinkState =
135 | {
136 readonly _tag: "Missing";
137 }
138 | {
139 readonly _tag: "NotSymlink";
140 }
141 | {
142 readonly _tag: "Symlink";
143 readonly target: string;
144 };
145
146function isNotSymlinkError(error: PlatformError.PlatformError): boolean {
147 const cause = error.reason.cause;
148 return (
149 error.reason._tag === "Unknown" &&
150 typeof cause === "object" &&
151 cause !== null &&
152 "code" in cause &&
153 cause.code === "EINVAL"
154 );
155}
156
157const readLinkState = Effect.fn("CodexHomeLayout.readLinkState")(function* (input: {
158 readonly fileSystem: FileSystem.FileSystem;
159 readonly sharedHomePath: string;
160 readonly effectiveHomePath: string;
161 readonly entryName: string;
162 readonly linkPath: string;
163}): Effect.fn.Return<LinkState, CodexShadowHomeError> {
164 return yield* input.fileSystem.readLink(input.linkPath).pipe(
165 Effect.map((target): LinkState => ({ _tag: "Symlink", target })),
166 Effect.catchTags({
167 PlatformError: (cause) => {
168 if (cause.reason._tag === "NotFound") {
169 return Effect.succeed<LinkState>({ _tag: "Missing" });
170 }
171 if (isNotSymlinkError(cause)) {
172 return Effect.succeed<LinkState>({ _tag: "NotSymlink" });
173 }
174 return new CodexShadowHomeFileSystemError({
175 sharedHomePath: input.sharedHomePath,
176 effectiveHomePath: input.effectiveHomePath,
177 operation: "readLink",
178 path: input.linkPath,
179 entryName: input.entryName,
180 cause,
181 });
182 },
183 }),
184 );
185});
186
187const removePrivateSymlink = Effect.fn("CodexHomeLayout.removePrivateSymlink")(function* (input: {
188 readonly fileSystem: FileSystem.FileSystem;
189 readonly sharedHomePath: string;
190 readonly effectiveHomePath: string;
191 readonly entryName: string;
192}): Effect.fn.Return<void, CodexShadowHomeError, Path.Path> {
193 const path = yield* Path.Path;
194 const privatePath = path.join(input.effectiveHomePath, input.entryName);
195 const state = yield* readLinkState({
196 ...input,
197 linkPath: privatePath,
198 });
199 if (state._tag === "Symlink") {
200 yield* input.fileSystem.remove(privatePath).pipe(
201 Effect.catchTags({
202 PlatformError: (cause) =>
203 new CodexShadowHomeFileSystemError({
204 sharedHomePath: input.sharedHomePath,
205 effectiveHomePath: input.effectiveHomePath,
206 operation: "remove",
207 path: privatePath,
208 entryName: input.entryName,
209 cause,
210 }),
211 }),
212 );
213 }
214});
215
216const ensureSymlink = Effect.fn("CodexHomeLayout.ensureSymlink")(function* (input: {
217 readonly fileSystem: FileSystem.FileSystem;
218 readonly sharedHomePath: string;
219 readonly effectiveHomePath: string;
220 readonly entryName: string;
221}): Effect.fn.Return<void, CodexShadowHomeError, Path.Path> {
222 const path = yield* Path.Path;
223 const target = path.join(input.sharedHomePath, input.entryName);
224 const link = path.join(input.effectiveHomePath, input.entryName);
225 const state = yield* readLinkState({
226 ...input,
227 linkPath: link,
228 });
229
230 const createLink = input.fileSystem.symlink(target, link).pipe(
231 Effect.catchTags({
232 PlatformError: (cause) =>
233 new CodexShadowHomeFileSystemError({
234 sharedHomePath: input.sharedHomePath,
235 effectiveHomePath: input.effectiveHomePath,
236 operation: "symlink",
237 path: link,
238 targetPath: target,
239 entryName: input.entryName,
240 cause,
241 }),
242 }),
243 );
244
245 if (state._tag === "NotSymlink") {
246 if (!REPLACEABLE_SHARED_RUNTIME_ENTRIES.has(input.entryName)) {
247 return yield* new CodexShadowHomeEntryConflictError({
248 sharedHomePath: input.sharedHomePath,
249 effectiveHomePath: input.effectiveHomePath,
250 entryName: input.entryName,
251 linkPath: link,
252 targetPath: target,
253 });
254 }
255
256 yield* input.fileSystem.remove(link, { recursive: true }).pipe(
257 Effect.catchTags({
258 PlatformError: (cause) =>
259 new CodexShadowHomeFileSystemError({
260 sharedHomePath: input.sharedHomePath,
261 effectiveHomePath: input.effectiveHomePath,
262 operation: "remove",
263 path: link,
264 entryName: input.entryName,
265 cause,
266 }),
267 }),
268 );
269 return yield* createLink;
270 }
271
272 if (state._tag === "Missing") {
273 return yield* createLink;
274 }
275
276 const resolvedExisting = path.resolve(path.dirname(link), state.target);
277 if (resolvedExisting !== target) {
278 yield* input.fileSystem.remove(link).pipe(
279 Effect.catchTags({
280 PlatformError: (cause) =>
281 new CodexShadowHomeFileSystemError({
282 sharedHomePath: input.sharedHomePath,
283 effectiveHomePath: input.effectiveHomePath,
284 operation: "remove",
285 path: link,
286 entryName: input.entryName,
287 cause,
288 }),
289 }),
290 );
291 yield* createLink;
292 }
293});
294
295const ensureShadowAuthIsPrivate = Effect.fn("CodexHomeLayout.ensureShadowAuthIsPrivate")(
296 function* (input: {
297 readonly fileSystem: FileSystem.FileSystem;
298 readonly sharedHomePath: string;
299 readonly effectiveHomePath: string;
300 }): Effect.fn.Return<void, CodexShadowHomeError, Path.Path> {
301 const path = yield* Path.Path;
302 const entryName = "auth.json";
303 const authPath = path.join(input.effectiveHomePath, entryName);
304 const state = yield* readLinkState({
305 ...input,
306 entryName,
307 linkPath: authPath,
308 });
309 if (state._tag === "Symlink") {
310 return yield* new CodexShadowHomePrivateEntrySymlinkError({
311 sharedHomePath: input.sharedHomePath,
312 effectiveHomePath: input.effectiveHomePath,
313 entryName,
314 path: authPath,
315 });
316 }
317 },
318);
319
320export const materializeCodexShadowHome = Effect.fn("materializeCodexShadowHome")(function* (
321 layout: CodexHomeLayout,
322) {
323 if (layout.mode !== "authOverlay") return;
324 const effectiveHomePath = layout.effectiveHomePath;
325 if (!effectiveHomePath) return;
326 if (layout.sharedHomePath === effectiveHomePath) {
327 return yield* new CodexShadowHomePathConflictError({
328 sharedHomePath: layout.sharedHomePath,
329 effectiveHomePath,
330 });
331 }
332
333 const fileSystem = yield* FileSystem.FileSystem;
334 const path = yield* Path.Path;
335
336 const makeDirectory = (directoryPath: string) =>
337 fileSystem.makeDirectory(directoryPath, { recursive: true }).pipe(
338 Effect.catchTags({
339 PlatformError: (cause) =>
340 new CodexShadowHomeFileSystemError({
341 sharedHomePath: layout.sharedHomePath,
342 effectiveHomePath,
343 operation: "makeDirectory",
344 path: directoryPath,
345 cause,
346 }),
347 }),
348 );
349
350 yield* Effect.all(
351 [
352 makeDirectory(layout.sharedHomePath),
353 makeDirectory(effectiveHomePath),
354 ...KNOWN_SHARED_DIRECTORIES.map((directory) =>
355 makeDirectory(path.join(layout.sharedHomePath, directory)),
356 ),
357 ],
358 { concurrency: "unbounded" },
359 );
360
361 const sharedEntryNames = yield* fileSystem.readDirectory(layout.sharedHomePath).pipe(
362 Effect.catchTags({
363 PlatformError: (cause) =>
364 new CodexShadowHomeFileSystemError({
365 sharedHomePath: layout.sharedHomePath,
366 effectiveHomePath,
367 operation: "readDirectory",
368 path: layout.sharedHomePath,
369 cause,
370 }),
371 }),
372 );
373 const entries = new Set<string>(KNOWN_SHARED_DIRECTORIES);
374 for (const entryName of sharedEntryNames) {
375 if (!PRIVATE_ENTRY_NAMES.has(entryName) && !SHADOW_LOCAL_ENTRY_NAMES.has(entryName)) {
376 entries.add(entryName);
377 }
378 }
379
380 yield* Effect.forEach(
381 PRIVATE_ENTRY_NAMES,
382 (entryName) =>
383 entryName === "auth.json"
384 ? Effect.void
385 : removePrivateSymlink({
386 fileSystem,
387 sharedHomePath: layout.sharedHomePath,
388 effectiveHomePath,
389 entryName,
390 }),
391 { discard: true },
392 );
393
394 yield* Effect.forEach(
395 entries,
396 (entryName) => {
397 if (PRIVATE_ENTRY_NAMES.has(entryName)) {
398 return Effect.void;
399 }
400 return ensureSymlink({
401 fileSystem,
402 sharedHomePath: layout.sharedHomePath,
403 effectiveHomePath,
404 entryName,
405 });
406 },
407 { discard: true },
408 );
409
410 yield* ensureShadowAuthIsPrivate({
411 fileSystem,
412 sharedHomePath: layout.sharedHomePath,
413 effectiveHomePath,
414 });
415});
416
417export function codexContinuationIdentity(layout: CodexHomeLayout) {
418 return {
419 driverKind: ProviderDriverKind.make("codex"),
420 continuationKey: layout.continuationKey,
421 };
422}