| 1 | import { ProviderDriverKind, type CodexSettings } from "@t3tools/contracts"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as FileSystem from "effect/FileSystem"; |
| 4 | import * as Path from "effect/Path"; |
| 5 | import * as Schema from "effect/Schema"; |
| 6 | import * as PlatformError from "effect/PlatformError"; |
| 7 | |
| 8 | import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; |
| 9 | import * as HostProcess from "@t3tools/shared/HostProcess"; |
| 10 | |
| 11 | export interface CodexHomeLayout { |
| 12 | readonly mode: "direct" | "authOverlay"; |
| 13 | readonly sharedHomePath: string; |
| 14 | readonly effectiveHomePath: string | undefined; |
| 15 | readonly continuationKey: string; |
| 16 | } |
| 17 | |
| 18 | const 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 | |
| 31 | const PRIVATE_ENTRY_NAMES = new Set(["auth.json", "models_cache.json"]); |
| 32 | const 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. |
| 35 | const REPLACEABLE_SHARED_RUNTIME_ENTRIES = new Set(["mcp-oauth-locks", ".sqlite-maintenance.lock"]); |
| 36 | |
| 37 | function 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 | |
| 43 | export 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 | |
| 68 | const CodexShadowHomeContext = { |
| 69 | sharedHomePath: Schema.String, |
| 70 | effectiveHomePath: Schema.String, |
| 71 | }; |
| 72 | |
| 73 | export 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 | |
| 90 | export 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 | |
| 99 | export 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 | |
| 113 | export 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 | |
| 126 | export const CodexShadowHomeError = Schema.Union([ |
| 127 | CodexShadowHomeFileSystemError, |
| 128 | CodexShadowHomePathConflictError, |
| 129 | CodexShadowHomeEntryConflictError, |
| 130 | CodexShadowHomePrivateEntrySymlinkError, |
| 131 | ]); |
| 132 | export type CodexShadowHomeError = typeof CodexShadowHomeError.Type; |
| 133 | |
| 134 | type LinkState = |
| 135 | | { |
| 136 | readonly _tag: "Missing"; |
| 137 | } |
| 138 | | { |
| 139 | readonly _tag: "NotSymlink"; |
| 140 | } |
| 141 | | { |
| 142 | readonly _tag: "Symlink"; |
| 143 | readonly target: string; |
| 144 | }; |
| 145 | |
| 146 | function 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 | |
| 157 | const 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 | |
| 187 | const 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 | |
| 216 | const 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 | |
| 295 | const 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 | |
| 320 | export 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 | |
| 417 | export function codexContinuationIdentity(layout: CodexHomeLayout) { |
| 418 | return { |
| 419 | driverKind: ProviderDriverKind.make("codex"), |
| 420 | continuationKey: layout.continuationKey, |
| 421 | }; |
| 422 | } |