authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 15:12:36-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 15:12:36-07:00
logdd48bfd98c440029c1ecaf946916105850cfd0ab
treea0936825d098e7a412040e1197d65d33a7da3355
parent6d8d995621065e0513def2fa28bf0f7d8b54dd90
signature Signed by PGP key B5690EEEBB952194

[codex] Structure server settings failures (#3376)

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

4 files changed, 150 insertions(+), 53 deletions(-)

apps/server/src/serverRuntimeStartup.ts+3-1
......@@ -327,7 +327,9 @@ export const make = Effect.gen(function* () {
327327 Effect.catch((error) =>
328328 Effect.logWarning("failed to start server settings runtime", {
329329 path: error.settingsPath,
330 detail: error.detail,
330 operation: error.operation,
331 providerInstanceId: error.providerInstanceId,
332 environmentVariable: error.environmentVariable,
331333 cause: error.cause,
332334 }),
333335 ),
apps/server/src/serverSettings.test.ts+57
......@@ -12,6 +12,7 @@ import * as Effect from "effect/Effect";
1212import * as Duration from "effect/Duration";
1313import * as FileSystem from "effect/FileSystem";
1414import * as Layer from "effect/Layer";
15import * as PlatformError from "effect/PlatformError";
1516import * as Schema from "effect/Schema";
1617import * as ServerSecretStore from "./auth/ServerSecretStore.ts";
1718import * as ServerConfig from "./config.ts";
......@@ -32,7 +33,63 @@ const makeServerSettingsLayer = () =>
3233 ),
3334 );
3435
36const makeFailingSecretStoreLayer = (cause: ServerSecretStore.SecretStoreError) =>
37 Layer.succeed(
38 ServerSecretStore.ServerSecretStore,
39 ServerSecretStore.ServerSecretStore.of({
40 get: () => Effect.fail(cause),
41 set: () => Effect.void,
42 create: () => Effect.void,
43 getOrCreateRandom: () => Effect.succeed(new Uint8Array()),
44 remove: () => Effect.void,
45 }),
46 );
47
3548it.layer(NodeServices.layer)("server settings", (it) => {
49 it.effect("preserves context when reading a provider environment secret fails", () => {
50 const platformCause = PlatformError.systemError({
51 _tag: "PermissionDenied",
52 module: "FileSystem",
53 method: "readFile",
54 pathOrDescriptor: "provider environment secret",
55 description: "Secret backend unavailable.",
56 });
57 const cause = new ServerSecretStore.SecretStoreReadError({
58 resource: "provider environment secret",
59 cause: platformCause,
60 });
61 const configLayer = Layer.fresh(
62 ServerConfig.layerTest(process.cwd(), {
63 prefix: "t3code-server-settings-secret-failure-test-",
64 }),
65 );
66 const settingsLayer = ServerSettingsModule.layer.pipe(
67 Layer.provide(makeFailingSecretStoreLayer(cause)),
68 Layer.provideMerge(configLayer),
69 );
70
71 return Effect.gen(function* () {
72 const serverConfig = yield* ServerConfig.ServerConfig;
73 const fileSystem = yield* FileSystem.FileSystem;
74 const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
75 yield* fileSystem.writeFileString(
76 serverConfig.settingsPath,
77 '{"providerInstances":{"codex_personal":{"driver":"codex","environment":[{"name":"OPENROUTER_API_KEY","value":"","sensitive":true,"valueRedacted":true}],"config":{}}}}',
78 );
79
80 const error = yield* Effect.flip(serverSettings.getSettings);
81
82 assert.deepInclude(error, {
83 _tag: "ServerSettingsError",
84 operation: "read-secret",
85 providerInstanceId: "codex_personal",
86 environmentVariable: "OPENROUTER_API_KEY",
87 });
88 assert.strictEqual(error.cause, cause);
89 assert.notInclude(error.message, cause.message);
90 }).pipe(Effect.provide(settingsLayer));
91 });
92
3693 it.effect("decodes nested settings patches", () =>
3794 Effect.gen(function* () {
3895 assert.deepEqual(
apps/server/src/serverSettings.ts+66-49
......@@ -40,7 +40,6 @@ import * as Path from "effect/Path";
4040import * as PubSub from "effect/PubSub";
4141import * as Ref from "effect/Ref";
4242import * as Schema from "effect/Schema";
43import * as SchemaIssue from "effect/SchemaIssue";
4443import * as Semaphore from "effect/Semaphore";
4544import * as Scope from "effect/Scope";
4645import * as Stream from "effect/Stream";
......@@ -67,7 +66,7 @@ const normalizeServerSettings = (
6766 (cause) =>
6867 new ServerSettingsError({
6968 settingsPath: "<memory>",
70 detail: `failed to normalize server settings: ${SchemaIssue.makeFormatterDefault()(cause.issue)}`,
69 operation: "normalize",
7170 cause,
7271 }),
7372 ),
......@@ -277,7 +276,7 @@ const make = Effect.gen(function* () {
277276 (cause) =>
278277 new ServerSettingsError({
279278 settingsPath,
280 detail: "failed to check settings file existence",
279 operation: "check-exists",
281280 cause,
282281 }),
283282 ),
......@@ -288,7 +287,7 @@ const make = Effect.gen(function* () {
288287 (cause) =>
289288 new ServerSettingsError({
290289 settingsPath,
291 detail: "failed to read settings file",
290 operation: "read-file",
292291 cause,
293292 }),
294293 ),
......@@ -305,6 +304,7 @@ const make = Effect.gen(function* () {
305304 yield* Effect.logWarning("failed to parse settings.json, using defaults", {
306305 path: settingsPath,
307306 issues: Cause.pretty(decoded.cause),
307 cause: decoded.cause,
308308 });
309309 return DEFAULT_SERVER_SETTINGS;
310310 }
......@@ -318,13 +318,6 @@ const make = Effect.gen(function* () {
318318
319319 const getSettingsFromCache = Cache.get(settingsCache, cacheKey);
320320
321 const toSettingsError = (detail: string, cause: unknown) =>
322 new ServerSettingsError({
323 settingsPath,
324 detail,
325 cause,
326 });
327
328321 const materializeProviderEnvironmentSecrets = (
329322 settings: ServerSettings,
330323 ): Effect.Effect<ServerSettings, ServerSettingsError> =>
......@@ -343,11 +336,15 @@ const make = Effect.gen(function* () {
343336 const secret = yield* secretStore
344337 .get(providerEnvironmentSecretName({ instanceId, name: variable.name }))
345338 .pipe(
346 Effect.mapError((cause) =>
347 toSettingsError(
348 `failed to read sensitive environment variable ${variable.name}`,
349 cause,
350 ),
339 Effect.mapError(
340 (cause) =>
341 new ServerSettingsError({
342 settingsPath,
343 operation: "read-secret",
344 providerInstanceId: instanceId,
345 environmentVariable: variable.name,
346 cause,
347 }),
351348 ),
352349 );
353350 environment.push({
......@@ -382,13 +379,18 @@ const make = Effect.gen(function* () {
382379 for (const variable of instance.environment) {
383380 const secretName = providerEnvironmentSecretName({ instanceId, name: variable.name });
384381 if (!variable.sensitive) {
385 yield* secretStore
386 .remove(secretName)
387 .pipe(
388 Effect.mapError((cause) =>
389 toSettingsError(`failed to remove environment secret ${variable.name}`, cause),
390 ),
391 );
382 yield* secretStore.remove(secretName).pipe(
383 Effect.mapError(
384 (cause) =>
385 new ServerSettingsError({
386 settingsPath,
387 operation: "remove-secret",
388 providerInstanceId: instanceId,
389 environmentVariable: variable.name,
390 cause,
391 }),
392 ),
393 );
392394 environment.push(redactProviderEnvironmentVariable(variable));
393395 continue;
394396 }
......@@ -396,22 +398,32 @@ const make = Effect.gen(function* () {
396398 nextSecretKeys.add(secretName);
397399 if (!variable.valueRedacted) {
398400 if (variable.value.length > 0) {
399 yield* secretStore
400 .set(secretName, textEncoder.encode(variable.value))
401 .pipe(
402 Effect.mapError((cause) =>
403 toSettingsError(`failed to persist environment secret ${variable.name}`, cause),
404 ),
405 );
401 yield* secretStore.set(secretName, textEncoder.encode(variable.value)).pipe(
402 Effect.mapError(
403 (cause) =>
404 new ServerSettingsError({
405 settingsPath,
406 operation: "write-secret",
407 providerInstanceId: instanceId,
408 environmentVariable: variable.name,
409 cause,
410 }),
411 ),
412 );
406413 environment.push({ ...variable, value: "", valueRedacted: true });
407414 } else {
408 yield* secretStore
409 .remove(secretName)
410 .pipe(
411 Effect.mapError((cause) =>
412 toSettingsError(`failed to remove environment secret ${variable.name}`, cause),
413 ),
414 );
415 yield* secretStore.remove(secretName).pipe(
416 Effect.mapError(
417 (cause) =>
418 new ServerSettingsError({
419 settingsPath,
420 operation: "remove-secret",
421 providerInstanceId: instanceId,
422 environmentVariable: variable.name,
423 cause,
424 }),
425 ),
426 );
415427 const { valueRedacted: _omit, ...rest } = variable;
416428 environment.push(rest);
417429 }
......@@ -431,16 +443,18 @@ const make = Effect.gen(function* () {
431443 if (!variable.sensitive) continue;
432444 const secretName = providerEnvironmentSecretName({ instanceId, name: variable.name });
433445 if (nextSecretKeys.has(secretName)) continue;
434 yield* secretStore
435 .remove(secretName)
436 .pipe(
437 Effect.mapError((cause) =>
438 toSettingsError(
439 `failed to remove stale environment secret ${variable.name}`,
446 yield* secretStore.remove(secretName).pipe(
447 Effect.mapError(
448 (cause) =>
449 new ServerSettingsError({
450 settingsPath,
451 operation: "remove-stale-secret",
452 providerInstanceId: instanceId,
453 environmentVariable: variable.name,
440454 cause,
441 ),
442 ),
443 );
455 }),
456 ),
457 );
444458 }
445459 }
446460
......@@ -468,7 +482,7 @@ const make = Effect.gen(function* () {
468482 (cause) =>
469483 new ServerSettingsError({
470484 settingsPath,
471 detail: "failed to write settings file",
485 operation: "write-file",
472486 cause,
473487 }),
474488 ),
......@@ -492,7 +506,7 @@ const make = Effect.gen(function* () {
492506 (cause) =>
493507 new ServerSettingsError({
494508 settingsPath,
495 detail: "failed to prepare settings directory",
509 operation: "prepare-directory",
496510 cause,
497511 }),
498512 ),
......@@ -571,7 +585,10 @@ const make = Effect.gen(function* () {
571585 materializeProviderEnvironmentSecrets(settings).pipe(
572586 Effect.catch((error: ServerSettingsError) =>
573587 Effect.logWarning("failed to materialize provider environment secrets", {
574 detail: error.detail,
588 operation: error.operation,
589 providerInstanceId: error.providerInstanceId,
590 environmentVariable: error.environmentVariable,
591 cause: error.cause,
575592 }).pipe(Effect.as(settings)),
576593 ),
577594 ),
packages/contracts/src/settings.ts+24-3
......@@ -414,16 +414,37 @@ export type ServerSettings = typeof ServerSettings.Type;
414414
415415export const DEFAULT_SERVER_SETTINGS: ServerSettings = Schema.decodeSync(ServerSettings)({});
416416
417export const ServerSettingsOperation = Schema.Literals([
418 "normalize",
419 "check-exists",
420 "read-file",
421 "read-secret",
422 "remove-secret",
423 "remove-stale-secret",
424 "write-secret",
425 "write-file",
426 "prepare-directory",
427]);
428export type ServerSettingsOperation = typeof ServerSettingsOperation.Type;
429
417430export class ServerSettingsError extends Schema.TaggedErrorClass<ServerSettingsError>()(
418431 "ServerSettingsError",
419432 {
420433 settingsPath: Schema.String,
421 detail: Schema.String,
422 cause: Schema.optional(Schema.Defect()),
434 operation: ServerSettingsOperation,
435 providerInstanceId: Schema.optional(Schema.String),
436 environmentVariable: Schema.optional(Schema.String),
437 cause: Schema.Defect(),
423438 },
424439) {
425440 override get message(): string {
426 return `Server settings error at ${this.settingsPath}: ${this.detail}`;
441 const provider =
442 this.providerInstanceId === undefined ? "" : ` for provider ${this.providerInstanceId}`;
443 const variable =
444 this.environmentVariable === undefined
445 ? ""
446 : ` and environment variable ${this.environmentVariable}`;
447 return `Server settings ${this.operation} failed${provider}${variable} at ${this.settingsPath}.`;
427448 }
428449}
429450