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";...@@ -3,11 +3,13 @@ import { describe, expect, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";4import * as FileSystem from "effect/FileSystem";
5import * as Path from "effect/Path";5import * as Path from "effect/Path";
6import * as PlatformError from "effect/PlatformError";
6import * as Schema from "effect/Schema";7import * as Schema from "effect/Schema";
78
8import { CodexSettings } from "@t3tools/contracts";9import { CodexSettings } from "@t3tools/contracts";
9import {10import {
10 CodexShadowHomeError,11 CodexShadowHomeEntryConflictError,
12 CodexShadowHomePathConflictError,
11 materializeCodexShadowHome,13 materializeCodexShadowHome,
12 resolveCodexHomeLayout,14 resolveCodexHomeLayout,
13} from "./CodexHomeLayout.ts";15} from "./CodexHomeLayout.ts";
...@@ -184,7 +186,14 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => {...@@ -184,7 +186,14 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => {
184186
185 const error = yield* materializeCodexShadowHome(layout).pipe(Effect.flip);187 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 );
188 }),197 }),
189 );198 );
190199
...@@ -206,7 +215,52 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => {...@@ -206,7 +215,52 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => {
206215
207 const error = yield* materializeCodexShadowHome(layout).pipe(Effect.flip);216 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 );
210 }),264 }),
211 );265 );
212 });266 });
apps/server/src/provider/Drivers/CodexHomeLayout.ts+216-80
...@@ -63,18 +63,71 @@ export const resolveCodexHomeLayout = Effect.fn("resolveCodexHomeLayout")(functi...@@ -63,18 +63,71 @@ export const resolveCodexHomeLayout = Effect.fn("resolveCodexHomeLayout")(functi
63 };63 };
64});64});
6565
66export class CodexShadowHomeError extends Schema.TaggedErrorClass<CodexShadowHomeError>()(66const CodexShadowHomeContext = {
67 "CodexShadowHomeError",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",
68 {99 {
69 detail: Schema.String,100 ...CodexShadowHomeContext,
70 cause: Schema.optional(Schema.Unknown),101 entryName: Schema.String,
102 linkPath: Schema.String,
103 targetPath: Schema.String,
71 },104 },
72) {105) {
73 override get message(): string {106 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.`;
75 }108 }
76}109}
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
79type LinkState =132type LinkState =
80 | {133 | {
...@@ -88,21 +141,6 @@ type LinkState =...@@ -88,21 +141,6 @@ type LinkState =
88 readonly target: string;141 readonly target: string;
89 };142 };
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
106function isNotSymlinkError(error: PlatformError.PlatformError): boolean {144function isNotSymlinkError(error: PlatformError.PlatformError): boolean {
107 const cause = error.reason.cause;145 const cause = error.reason.cause;
108 return (146 return (
...@@ -114,78 +152,151 @@ function isNotSymlinkError(error: PlatformError.PlatformError): boolean {...@@ -114,78 +152,151 @@ function isNotSymlinkError(error: PlatformError.PlatformError): boolean {
114 );152 );
115}153}
116154
117const readLinkState = Effect.fn("CodexHomeLayout.readLinkState")(function* (155const readLinkState = Effect.fn("CodexHomeLayout.readLinkState")(function* (input: {
118 fileSystem: FileSystem.FileSystem,156 readonly fileSystem: FileSystem.FileSystem;
119 linkPath: string,157 readonly sharedHomePath: string;
120): Effect.fn.Return<LinkState, CodexShadowHomeError> {158 readonly effectiveHomePath: string;
121 return yield* fileSystem.readLink(linkPath).pipe(159 readonly entryName: string;
160 readonly linkPath: string;
161}): Effect.fn.Return<LinkState, CodexShadowHomeError> {
162 return yield* input.fileSystem.readLink(input.linkPath).pipe(
122 Effect.map((target): LinkState => ({ _tag: "Symlink", target })),163 Effect.map((target): LinkState => ({ _tag: "Symlink", target })),
123 Effect.catch((error) => {164 Effect.catchTags({
124 if (error.reason._tag === "NotFound") {165 PlatformError: (cause) => {
125 return Effect.succeed<LinkState>({ _tag: "Missing" });166 if (cause.reason._tag === "NotFound") {
126 }167 return Effect.succeed<LinkState>({ _tag: "Missing" });
127 if (isNotSymlinkError(error)) {168 }
128 return Effect.succeed<LinkState>({ _tag: "NotSymlink" });169 if (isNotSymlinkError(cause)) {
129 }170 return Effect.succeed<LinkState>({ _tag: "NotSymlink" });
130 return Effect.fail(toShadowHomeError(error));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 },
131 }),181 }),
132 );182 );
133});183});
134184
135const removePrivateSymlink = Effect.fn("CodexHomeLayout.removePrivateSymlink")(function* (input: {185const removePrivateSymlink = Effect.fn("CodexHomeLayout.removePrivateSymlink")(function* (input: {
136 readonly fileSystem: FileSystem.FileSystem;186 readonly fileSystem: FileSystem.FileSystem;
137 readonly shadowPath: string;187 readonly sharedHomePath: string;
188 readonly effectiveHomePath: string;
138 readonly entryName: string;189 readonly entryName: string;
139}): Effect.fn.Return<void, CodexShadowHomeError, Path.Path> {190}): Effect.fn.Return<void, CodexShadowHomeError, Path.Path> {
140 const path = yield* Path.Path;191 const path = yield* Path.Path;
141 const privatePath = path.join(input.shadowPath, input.entryName);192 const privatePath = path.join(input.effectiveHomePath, input.entryName);
142 const state = yield* readLinkState(input.fileSystem, privatePath);193 const state = yield* readLinkState({
194 ...input,
195 linkPath: privatePath,
196 });
143 if (state._tag === "Symlink") {197 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 );
145 }211 }
146});212});
147213
148const ensureSymlink = Effect.fn("CodexHomeLayout.ensureSymlink")(function* (input: {214const ensureSymlink = Effect.fn("CodexHomeLayout.ensureSymlink")(function* (input: {
149 readonly fileSystem: FileSystem.FileSystem;215 readonly fileSystem: FileSystem.FileSystem;
150 readonly shadowPath: string;216 readonly sharedHomePath: string;
151 readonly sharedPath: string;217 readonly effectiveHomePath: string;
152 readonly entryName: string;218 readonly entryName: string;
153}): Effect.fn.Return<void, CodexShadowHomeError, Path.Path> {219}): Effect.fn.Return<void, CodexShadowHomeError, Path.Path> {
154 const path = yield* Path.Path;220 const path = yield* Path.Path;
155 const target = path.join(input.sharedPath, input.entryName);221 const target = path.join(input.sharedHomePath, input.entryName);
156 const link = path.join(input.shadowPath, input.entryName);222 const link = path.join(input.effectiveHomePath, input.entryName);
157 const state = yield* readLinkState(input.fileSystem, link);223 const state = yield* readLinkState({
224 ...input,
225 linkPath: link,
226 });
158227
159 if (state._tag === "NotSymlink") {228 if (state._tag === "NotSymlink") {
160 return yield* new CodexShadowHomeError({229 return yield* new CodexShadowHomeEntryConflictError({
161 detail: `Cannot create Codex shadow home because '${link}' already exists and is not a symlink.`,230 sharedHomePath: input.sharedHomePath,
231 effectiveHomePath: input.effectiveHomePath,
232 entryName: input.entryName,
233 linkPath: link,
234 targetPath: target,
162 });235 });
163 }236 }
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
165 if (state._tag === "Missing") {253 if (state._tag === "Missing") {
166 return yield* normalizeShadowHomeError(input.fileSystem.symlink(target, link));254 return yield* createLink;
167 }255 }
168256
169 const resolvedExisting = path.resolve(path.dirname(link), state.target);257 const resolvedExisting = path.resolve(path.dirname(link), state.target);
170 if (resolvedExisting !== target) {258 if (resolvedExisting !== target) {
171 yield* normalizeShadowHomeError(input.fileSystem.remove(link));259 yield* input.fileSystem.remove(link).pipe(
172 yield* normalizeShadowHomeError(input.fileSystem.symlink(target, link));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;
173 }273 }
174});274});
175275
176const ensureShadowAuthIsPrivate = Effect.fn("CodexHomeLayout.ensureShadowAuthIsPrivate")(function* (276const ensureShadowAuthIsPrivate = Effect.fn("CodexHomeLayout.ensureShadowAuthIsPrivate")(
177 fileSystem: FileSystem.FileSystem,277 function* (input: {
178 shadowPath: string,278 readonly fileSystem: FileSystem.FileSystem;
179): Effect.fn.Return<void, CodexShadowHomeError, Path.Path> {279 readonly sharedHomePath: string;
180 const path = yield* Path.Path;280 readonly effectiveHomePath: string;
181 const authPath = path.join(shadowPath, "auth.json");281 }): Effect.fn.Return<void, CodexShadowHomeError, Path.Path> {
182 const state = yield* readLinkState(fileSystem, authPath);282 const path = yield* Path.Path;
183 if (state._tag === "Symlink") {283 const entryName = "auth.json";
184 return yield* new CodexShadowHomeError({284 const authPath = path.join(input.effectiveHomePath, entryName);
185 detail: `Codex shadow auth file '${authPath}' must be a real file, not a symlink.`,285 const state = yield* readLinkState({
286 ...input,
287 entryName,
288 linkPath: authPath,
186 });289 });
187 }290 if (state._tag === "Symlink") {
188});291 return yield* new CodexShadowHomePrivateEntrySymlinkError({
292 sharedHomePath: input.sharedHomePath,
293 effectiveHomePath: input.effectiveHomePath,
294 entryName,
295 path: authPath,
296 });
297 }
298 },
299);
189300
190export const materializeCodexShadowHome = Effect.fn("materializeCodexShadowHome")(function* (301export const materializeCodexShadowHome = Effect.fn("materializeCodexShadowHome")(function* (
191 layout: CodexHomeLayout,302 layout: CodexHomeLayout,
...@@ -194,31 +305,51 @@ export const materializeCodexShadowHome = Effect.fn("materializeCodexShadowHome"...@@ -194,31 +305,51 @@ export const materializeCodexShadowHome = Effect.fn("materializeCodexShadowHome"
194 const effectiveHomePath = layout.effectiveHomePath;305 const effectiveHomePath = layout.effectiveHomePath;
195 if (!effectiveHomePath) return;306 if (!effectiveHomePath) return;
196 if (layout.sharedHomePath === effectiveHomePath) {307 if (layout.sharedHomePath === effectiveHomePath) {
197 return yield* new CodexShadowHomeError({308 return yield* new CodexShadowHomePathConflictError({
198 detail: "Codex shadow home path must be different from the shared home path.",309 sharedHomePath: layout.sharedHomePath,
310 effectiveHomePath,
199 });311 });
200 }312 }
201313
202 const fileSystem = yield* FileSystem.FileSystem;314 const fileSystem = yield* FileSystem.FileSystem;
203 const path = yield* Path.Path;315 const path = yield* Path.Path;
204316
205 yield* normalizeShadowHomeError(317 const makeDirectory = (directoryPath: string) =>
206 Effect.all(318 fileSystem.makeDirectory(directoryPath, { recursive: true }).pipe(
207 [319 Effect.catchTags({
208 fileSystem.makeDirectory(layout.sharedHomePath, { recursive: true }),320 PlatformError: (cause) =>
209 fileSystem.makeDirectory(effectiveHomePath, { recursive: true }),321 new CodexShadowHomeFileSystemError({
210 ...KNOWN_SHARED_DIRECTORIES.map((directory) =>322 sharedHomePath: layout.sharedHomePath,
211 fileSystem.makeDirectory(path.join(layout.sharedHomePath, directory), {323 effectiveHomePath,
212 recursive: true,324 operation: "makeDirectory",
325 path: directoryPath,
326 cause,
213 }),327 }),
214 ),328 }),
215 ],329 );
216 { concurrency: "unbounded" },330
217 ),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" },
218 );340 );
219341
220 const sharedEntryNames = yield* normalizeShadowHomeError(342 const sharedEntryNames = yield* fileSystem.readDirectory(layout.sharedHomePath).pipe(
221 fileSystem.readDirectory(layout.sharedHomePath),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 }),
222 );353 );
223 const entries = new Set<string>(KNOWN_SHARED_DIRECTORIES);354 const entries = new Set<string>(KNOWN_SHARED_DIRECTORIES);
224 for (const entryName of sharedEntryNames) {355 for (const entryName of sharedEntryNames) {
...@@ -234,7 +365,8 @@ export const materializeCodexShadowHome = Effect.fn("materializeCodexShadowHome"...@@ -234,7 +365,8 @@ export const materializeCodexShadowHome = Effect.fn("materializeCodexShadowHome"
234 ? Effect.void365 ? Effect.void
235 : removePrivateSymlink({366 : removePrivateSymlink({
236 fileSystem,367 fileSystem,
237 shadowPath: effectiveHomePath,368 sharedHomePath: layout.sharedHomePath,
369 effectiveHomePath,
238 entryName,370 entryName,
239 }),371 }),
240 { discard: true },372 { discard: true },
...@@ -248,15 +380,19 @@ export const materializeCodexShadowHome = Effect.fn("materializeCodexShadowHome"...@@ -248,15 +380,19 @@ export const materializeCodexShadowHome = Effect.fn("materializeCodexShadowHome"
248 }380 }
249 return ensureSymlink({381 return ensureSymlink({
250 fileSystem,382 fileSystem,
251 shadowPath: effectiveHomePath,383 sharedHomePath: layout.sharedHomePath,
252 sharedPath: layout.sharedHomePath,384 effectiveHomePath,
253 entryName,385 entryName,
254 });386 });
255 },387 },
256 { discard: true },388 { discard: true },
257 );389 );
258390
259 yield* ensureShadowAuthIsPrivate(fileSystem, effectiveHomePath);391 yield* ensureShadowAuthIsPrivate({
392 fileSystem,
393 sharedHomePath: layout.sharedHomePath,
394 effectiveHomePath,
395 });
260});396});
261397
262export function codexContinuationIdentity(layout: CodexHomeLayout) {398export function codexContinuationIdentity(layout: CodexHomeLayout) {