authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 12:24:32-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 12:24:32-07:00
log4407a5a6b716b1d37bed917345d79f627a42f59e
tree6f0083a118d90447e76a5056721a33c45c466f95
parentc7b375e9b0f0581a977a2745e3afbf5732aecb15
signature Signed by PGP key B5690EEEBB952194

[codex] Structure Codex shadow home errors (#3262)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 273 insertions(+), 83 deletions(-)

apps/server/src/provider/Drivers/CodexHomeLayout.test.ts+57-3
......@@ -3,11 +3,13 @@ import { describe, expect, it } from "@effect/vitest";
33import * as Effect from "effect/Effect";
44import * as FileSystem from "effect/FileSystem";
55import * as Path from "effect/Path";
6import * as PlatformError from "effect/PlatformError";
67import * as Schema from "effect/Schema";
78
89import { CodexSettings } from "@t3tools/contracts";
910import {
10 CodexShadowHomeError,
11 CodexShadowHomeEntryConflictError,
12 CodexShadowHomePathConflictError,
1113 materializeCodexShadowHome,
1214 resolveCodexHomeLayout,
1315} from "./CodexHomeLayout.ts";
......@@ -184,7 +186,14 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => {
184186
185187 const error = yield* materializeCodexShadowHome(layout).pipe(Effect.flip);
186188
187 expect(error).toBeInstanceOf(CodexShadowHomeError);
189 expect(error).toBeInstanceOf(CodexShadowHomePathConflictError);
190 expect(error).toMatchObject({
191 sharedHomePath: sharedHome,
192 effectiveHomePath: sharedHome,
193 });
194 expect(error.message).toBe(
195 `Codex shadow home path '${sharedHome}' must be different from the shared home path '${sharedHome}'.`,
196 );
188197 }),
189198 );
190199
......@@ -206,7 +215,52 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => {
206215
207216 const error = yield* materializeCodexShadowHome(layout).pipe(Effect.flip);
208217
209 expect(error.detail).toContain("already exists and is not a symlink");
218 expect(error).toBeInstanceOf(CodexShadowHomeEntryConflictError);
219 expect(error).toMatchObject({
220 sharedHomePath: sharedHome,
221 effectiveHomePath: shadowHome,
222 entryName: "config.toml",
223 linkPath: path.join(shadowHome, "config.toml"),
224 targetPath: path.join(sharedHome, "config.toml"),
225 });
226 expect(error.message).toBe(
227 `Cannot create Codex shadow home entry 'config.toml' because '${path.join(shadowHome, "config.toml")}' already exists and is not a symlink.`,
228 );
229 }),
230 );
231
232 it.effect("preserves filesystem operation, paths, and cause", () =>
233 Effect.gen(function* () {
234 const path = yield* Path.Path;
235 const sharedRoot = yield* makeTempDir("t3code-codex-shared-root-");
236 const sharedHome = path.join(sharedRoot, "shared-home");
237 const shadowRoot = yield* makeTempDir("t3code-codex-shadow-root-");
238 const shadowHome = path.join(shadowRoot, "shadow");
239 yield* writeTextFile(sharedHome, "not a directory\n");
240
241 const layout = yield* resolveCodexHomeLayout(
242 decodeCodexSettings({
243 homePath: sharedHome,
244 shadowHomePath: shadowHome,
245 }),
246 );
247
248 const error = yield* materializeCodexShadowHome(layout).pipe(Effect.flip);
249
250 expect(error._tag).toBe("CodexShadowHomeFileSystemError");
251 if (error._tag !== "CodexShadowHomeFileSystemError") {
252 return expect.fail("Expected CodexShadowHomeFileSystemError");
253 }
254 expect(error).toMatchObject({
255 operation: "makeDirectory",
256 sharedHomePath: sharedHome,
257 effectiveHomePath: shadowHome,
258 });
259 expect(error.path.startsWith(sharedHome)).toBe(true);
260 expect(error.cause).toBeInstanceOf(PlatformError.PlatformError);
261 expect(error.message).toBe(
262 `Codex shadow home filesystem operation 'makeDirectory' failed for '${error.path}'.`,
263 );
210264 }),
211265 );
212266 });
apps/server/src/provider/Drivers/CodexHomeLayout.ts+216-80
......@@ -63,18 +63,71 @@ export const resolveCodexHomeLayout = Effect.fn("resolveCodexHomeLayout")(functi
6363 };
6464});
6565
66export class CodexShadowHomeError extends Schema.TaggedErrorClass<CodexShadowHomeError>()(
67 "CodexShadowHomeError",
66const CodexShadowHomeContext = {
67 sharedHomePath: Schema.String,
68 effectiveHomePath: Schema.String,
69};
70
71export class CodexShadowHomeFileSystemError extends Schema.TaggedErrorClass<CodexShadowHomeFileSystemError>()(
72 "CodexShadowHomeFileSystemError",
73 {
74 ...CodexShadowHomeContext,
75 operation: Schema.Literals(["readLink", "makeDirectory", "readDirectory", "remove", "symlink"]),
76 path: Schema.String,
77 targetPath: Schema.optional(Schema.String),
78 entryName: Schema.optional(Schema.String),
79 cause: Schema.Defect(),
80 },
81) {
82 override get message(): string {
83 const target = this.targetPath === undefined ? "" : ` to '${this.targetPath}'`;
84 return `Codex shadow home filesystem operation '${this.operation}' failed for '${this.path}'${target}.`;
85 }
86}
87
88export class CodexShadowHomePathConflictError extends Schema.TaggedErrorClass<CodexShadowHomePathConflictError>()(
89 "CodexShadowHomePathConflictError",
90 CodexShadowHomeContext,
91) {
92 override get message(): string {
93 return `Codex shadow home path '${this.effectiveHomePath}' must be different from the shared home path '${this.sharedHomePath}'.`;
94 }
95}
96
97export class CodexShadowHomeEntryConflictError extends Schema.TaggedErrorClass<CodexShadowHomeEntryConflictError>()(
98 "CodexShadowHomeEntryConflictError",
6899 {
69 detail: Schema.String,
70 cause: Schema.optional(Schema.Unknown),
100 ...CodexShadowHomeContext,
101 entryName: Schema.String,
102 linkPath: Schema.String,
103 targetPath: Schema.String,
71104 },
72105) {
73106 override get message(): string {
74 return this.detail;
107 return `Cannot create Codex shadow home entry '${this.entryName}' because '${this.linkPath}' already exists and is not a symlink.`;
75108 }
76109}
77const isCodexShadowHomeError = Schema.is(CodexShadowHomeError);
110
111export class CodexShadowHomePrivateEntrySymlinkError extends Schema.TaggedErrorClass<CodexShadowHomePrivateEntrySymlinkError>()(
112 "CodexShadowHomePrivateEntrySymlinkError",
113 {
114 ...CodexShadowHomeContext,
115 entryName: Schema.String,
116 path: Schema.String,
117 },
118) {
119 override get message(): string {
120 return `Codex shadow home private entry '${this.entryName}' at '${this.path}' must be a real file, not a symlink.`;
121 }
122}
123
124export const CodexShadowHomeError = Schema.Union([
125 CodexShadowHomeFileSystemError,
126 CodexShadowHomePathConflictError,
127 CodexShadowHomeEntryConflictError,
128 CodexShadowHomePrivateEntrySymlinkError,
129]);
130export type CodexShadowHomeError = typeof CodexShadowHomeError.Type;
78131
79132type LinkState =
80133 | {
......@@ -88,21 +141,6 @@ type LinkState =
88141 readonly target: string;
89142 };
90143
91function toShadowHomeError(cause: unknown): CodexShadowHomeError {
92 return isCodexShadowHomeError(cause)
93 ? cause
94 : new CodexShadowHomeError({
95 detail: "Failed to materialize Codex shadow home.",
96 cause,
97 });
98}
99
100function normalizeShadowHomeError<A, E, R>(
101 effect: Effect.Effect<A, E, R>,
102): Effect.Effect<A, CodexShadowHomeError, R> {
103 return effect.pipe(Effect.mapError(toShadowHomeError));
104}
105
106144function isNotSymlinkError(error: PlatformError.PlatformError): boolean {
107145 const cause = error.reason.cause;
108146 return (
......@@ -114,78 +152,151 @@ function isNotSymlinkError(error: PlatformError.PlatformError): boolean {
114152 );
115153}
116154
117const readLinkState = Effect.fn("CodexHomeLayout.readLinkState")(function* (
118 fileSystem: FileSystem.FileSystem,
119 linkPath: string,
120): Effect.fn.Return<LinkState, CodexShadowHomeError> {
121 return yield* fileSystem.readLink(linkPath).pipe(
155const readLinkState = Effect.fn("CodexHomeLayout.readLinkState")(function* (input: {
156 readonly fileSystem: FileSystem.FileSystem;
157 readonly sharedHomePath: string;
158 readonly effectiveHomePath: string;
159 readonly entryName: string;
160 readonly linkPath: string;
161}): Effect.fn.Return<LinkState, CodexShadowHomeError> {
162 return yield* input.fileSystem.readLink(input.linkPath).pipe(
122163 Effect.map((target): LinkState => ({ _tag: "Symlink", target })),
123 Effect.catch((error) => {
124 if (error.reason._tag === "NotFound") {
125 return Effect.succeed<LinkState>({ _tag: "Missing" });
126 }
127 if (isNotSymlinkError(error)) {
128 return Effect.succeed<LinkState>({ _tag: "NotSymlink" });
129 }
130 return Effect.fail(toShadowHomeError(error));
164 Effect.catchTags({
165 PlatformError: (cause) => {
166 if (cause.reason._tag === "NotFound") {
167 return Effect.succeed<LinkState>({ _tag: "Missing" });
168 }
169 if (isNotSymlinkError(cause)) {
170 return Effect.succeed<LinkState>({ _tag: "NotSymlink" });
171 }
172 return new CodexShadowHomeFileSystemError({
173 sharedHomePath: input.sharedHomePath,
174 effectiveHomePath: input.effectiveHomePath,
175 operation: "readLink",
176 path: input.linkPath,
177 entryName: input.entryName,
178 cause,
179 });
180 },
131181 }),
132182 );
133183});
134184
135185const removePrivateSymlink = Effect.fn("CodexHomeLayout.removePrivateSymlink")(function* (input: {
136186 readonly fileSystem: FileSystem.FileSystem;
137 readonly shadowPath: string;
187 readonly sharedHomePath: string;
188 readonly effectiveHomePath: string;
138189 readonly entryName: string;
139190}): Effect.fn.Return<void, CodexShadowHomeError, Path.Path> {
140191 const path = yield* Path.Path;
141 const privatePath = path.join(input.shadowPath, input.entryName);
142 const state = yield* readLinkState(input.fileSystem, privatePath);
192 const privatePath = path.join(input.effectiveHomePath, input.entryName);
193 const state = yield* readLinkState({
194 ...input,
195 linkPath: privatePath,
196 });
143197 if (state._tag === "Symlink") {
144 yield* normalizeShadowHomeError(input.fileSystem.remove(privatePath));
198 yield* input.fileSystem.remove(privatePath).pipe(
199 Effect.catchTags({
200 PlatformError: (cause) =>
201 new CodexShadowHomeFileSystemError({
202 sharedHomePath: input.sharedHomePath,
203 effectiveHomePath: input.effectiveHomePath,
204 operation: "remove",
205 path: privatePath,
206 entryName: input.entryName,
207 cause,
208 }),
209 }),
210 );
145211 }
146212});
147213
148214const ensureSymlink = Effect.fn("CodexHomeLayout.ensureSymlink")(function* (input: {
149215 readonly fileSystem: FileSystem.FileSystem;
150 readonly shadowPath: string;
151 readonly sharedPath: string;
216 readonly sharedHomePath: string;
217 readonly effectiveHomePath: string;
152218 readonly entryName: string;
153219}): Effect.fn.Return<void, CodexShadowHomeError, Path.Path> {
154220 const path = yield* Path.Path;
155 const target = path.join(input.sharedPath, input.entryName);
156 const link = path.join(input.shadowPath, input.entryName);
157 const state = yield* readLinkState(input.fileSystem, link);
221 const target = path.join(input.sharedHomePath, input.entryName);
222 const link = path.join(input.effectiveHomePath, input.entryName);
223 const state = yield* readLinkState({
224 ...input,
225 linkPath: link,
226 });
158227
159228 if (state._tag === "NotSymlink") {
160 return yield* new CodexShadowHomeError({
161 detail: `Cannot create Codex shadow home because '${link}' already exists and is not a symlink.`,
229 return yield* new CodexShadowHomeEntryConflictError({
230 sharedHomePath: input.sharedHomePath,
231 effectiveHomePath: input.effectiveHomePath,
232 entryName: input.entryName,
233 linkPath: link,
234 targetPath: target,
162235 });
163236 }
164237
238 const createLink = input.fileSystem.symlink(target, link).pipe(
239 Effect.catchTags({
240 PlatformError: (cause) =>
241 new CodexShadowHomeFileSystemError({
242 sharedHomePath: input.sharedHomePath,
243 effectiveHomePath: input.effectiveHomePath,
244 operation: "symlink",
245 path: link,
246 targetPath: target,
247 entryName: input.entryName,
248 cause,
249 }),
250 }),
251 );
252
165253 if (state._tag === "Missing") {
166 return yield* normalizeShadowHomeError(input.fileSystem.symlink(target, link));
254 return yield* createLink;
167255 }
168256
169257 const resolvedExisting = path.resolve(path.dirname(link), state.target);
170258 if (resolvedExisting !== target) {
171 yield* normalizeShadowHomeError(input.fileSystem.remove(link));
172 yield* normalizeShadowHomeError(input.fileSystem.symlink(target, link));
259 yield* input.fileSystem.remove(link).pipe(
260 Effect.catchTags({
261 PlatformError: (cause) =>
262 new CodexShadowHomeFileSystemError({
263 sharedHomePath: input.sharedHomePath,
264 effectiveHomePath: input.effectiveHomePath,
265 operation: "remove",
266 path: link,
267 entryName: input.entryName,
268 cause,
269 }),
270 }),
271 );
272 yield* createLink;
173273 }
174274});
175275
176const ensureShadowAuthIsPrivate = Effect.fn("CodexHomeLayout.ensureShadowAuthIsPrivate")(function* (
177 fileSystem: FileSystem.FileSystem,
178 shadowPath: string,
179): Effect.fn.Return<void, CodexShadowHomeError, Path.Path> {
180 const path = yield* Path.Path;
181 const authPath = path.join(shadowPath, "auth.json");
182 const state = yield* readLinkState(fileSystem, authPath);
183 if (state._tag === "Symlink") {
184 return yield* new CodexShadowHomeError({
185 detail: `Codex shadow auth file '${authPath}' must be a real file, not a symlink.`,
276const ensureShadowAuthIsPrivate = Effect.fn("CodexHomeLayout.ensureShadowAuthIsPrivate")(
277 function* (input: {
278 readonly fileSystem: FileSystem.FileSystem;
279 readonly sharedHomePath: string;
280 readonly effectiveHomePath: string;
281 }): Effect.fn.Return<void, CodexShadowHomeError, Path.Path> {
282 const path = yield* Path.Path;
283 const entryName = "auth.json";
284 const authPath = path.join(input.effectiveHomePath, entryName);
285 const state = yield* readLinkState({
286 ...input,
287 entryName,
288 linkPath: authPath,
186289 });
187 }
188});
290 if (state._tag === "Symlink") {
291 return yield* new CodexShadowHomePrivateEntrySymlinkError({
292 sharedHomePath: input.sharedHomePath,
293 effectiveHomePath: input.effectiveHomePath,
294 entryName,
295 path: authPath,
296 });
297 }
298 },
299);
189300
190301export const materializeCodexShadowHome = Effect.fn("materializeCodexShadowHome")(function* (
191302 layout: CodexHomeLayout,
......@@ -194,31 +305,51 @@ export const materializeCodexShadowHome = Effect.fn("materializeCodexShadowHome"
194305 const effectiveHomePath = layout.effectiveHomePath;
195306 if (!effectiveHomePath) return;
196307 if (layout.sharedHomePath === effectiveHomePath) {
197 return yield* new CodexShadowHomeError({
198 detail: "Codex shadow home path must be different from the shared home path.",
308 return yield* new CodexShadowHomePathConflictError({
309 sharedHomePath: layout.sharedHomePath,
310 effectiveHomePath,
199311 });
200312 }
201313
202314 const fileSystem = yield* FileSystem.FileSystem;
203315 const path = yield* Path.Path;
204316
205 yield* normalizeShadowHomeError(
206 Effect.all(
207 [
208 fileSystem.makeDirectory(layout.sharedHomePath, { recursive: true }),
209 fileSystem.makeDirectory(effectiveHomePath, { recursive: true }),
210 ...KNOWN_SHARED_DIRECTORIES.map((directory) =>
211 fileSystem.makeDirectory(path.join(layout.sharedHomePath, directory), {
212 recursive: true,
317 const makeDirectory = (directoryPath: string) =>
318 fileSystem.makeDirectory(directoryPath, { recursive: true }).pipe(
319 Effect.catchTags({
320 PlatformError: (cause) =>
321 new CodexShadowHomeFileSystemError({
322 sharedHomePath: layout.sharedHomePath,
323 effectiveHomePath,
324 operation: "makeDirectory",
325 path: directoryPath,
326 cause,
213327 }),
214 ),
215 ],
216 { concurrency: "unbounded" },
217 ),
328 }),
329 );
330
331 yield* Effect.all(
332 [
333 makeDirectory(layout.sharedHomePath),
334 makeDirectory(effectiveHomePath),
335 ...KNOWN_SHARED_DIRECTORIES.map((directory) =>
336 makeDirectory(path.join(layout.sharedHomePath, directory)),
337 ),
338 ],
339 { concurrency: "unbounded" },
218340 );
219341
220 const sharedEntryNames = yield* normalizeShadowHomeError(
221 fileSystem.readDirectory(layout.sharedHomePath),
342 const sharedEntryNames = yield* fileSystem.readDirectory(layout.sharedHomePath).pipe(
343 Effect.catchTags({
344 PlatformError: (cause) =>
345 new CodexShadowHomeFileSystemError({
346 sharedHomePath: layout.sharedHomePath,
347 effectiveHomePath,
348 operation: "readDirectory",
349 path: layout.sharedHomePath,
350 cause,
351 }),
352 }),
222353 );
223354 const entries = new Set<string>(KNOWN_SHARED_DIRECTORIES);
224355 for (const entryName of sharedEntryNames) {
......@@ -234,7 +365,8 @@ export const materializeCodexShadowHome = Effect.fn("materializeCodexShadowHome"
234365 ? Effect.void
235366 : removePrivateSymlink({
236367 fileSystem,
237 shadowPath: effectiveHomePath,
368 sharedHomePath: layout.sharedHomePath,
369 effectiveHomePath,
238370 entryName,
239371 }),
240372 { discard: true },
......@@ -248,15 +380,19 @@ export const materializeCodexShadowHome = Effect.fn("materializeCodexShadowHome"
248380 }
249381 return ensureSymlink({
250382 fileSystem,
251 shadowPath: effectiveHomePath,
252 sharedPath: layout.sharedHomePath,
383 sharedHomePath: layout.sharedHomePath,
384 effectiveHomePath,
253385 entryName,
254386 });
255387 },
256388 { discard: true },
257389 );
258390
259 yield* ensureShadowAuthIsPrivate(fileSystem, effectiveHomePath);
391 yield* ensureShadowAuthIsPrivate({
392 fileSystem,
393 sharedHomePath: layout.sharedHomePath,
394 effectiveHomePath,
395 });
260396});
261397
262398export function codexContinuationIdentity(layout: CodexHomeLayout) {