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* () {...@@ -327,7 +327,9 @@ export const make = Effect.gen(function* () {
327 Effect.catch((error) =>327 Effect.catch((error) =>
328 Effect.logWarning("failed to start server settings runtime", {328 Effect.logWarning("failed to start server settings runtime", {
329 path: error.settingsPath,329 path: error.settingsPath,
330 detail: error.detail,330 operation: error.operation,
331 providerInstanceId: error.providerInstanceId,
332 environmentVariable: error.environmentVariable,
331 cause: error.cause,333 cause: error.cause,
332 }),334 }),
333 ),335 ),
apps/server/src/serverSettings.test.ts+57
...@@ -12,6 +12,7 @@ import * as Effect from "effect/Effect";...@@ -12,6 +12,7 @@ import * as Effect from "effect/Effect";
12import * as Duration from "effect/Duration";12import * as Duration from "effect/Duration";
13import * as FileSystem from "effect/FileSystem";13import * as FileSystem from "effect/FileSystem";
14import * as Layer from "effect/Layer";14import * as Layer from "effect/Layer";
15import * as PlatformError from "effect/PlatformError";
15import * as Schema from "effect/Schema";16import * as Schema from "effect/Schema";
16import * as ServerSecretStore from "./auth/ServerSecretStore.ts";17import * as ServerSecretStore from "./auth/ServerSecretStore.ts";
17import * as ServerConfig from "./config.ts";18import * as ServerConfig from "./config.ts";
...@@ -32,7 +33,63 @@ const makeServerSettingsLayer = () =>...@@ -32,7 +33,63 @@ const makeServerSettingsLayer = () =>
32 ),33 ),
33 );34 );
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
35it.layer(NodeServices.layer)("server settings", (it) => {48it.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
36 it.effect("decodes nested settings patches", () =>93 it.effect("decodes nested settings patches", () =>
37 Effect.gen(function* () {94 Effect.gen(function* () {
38 assert.deepEqual(95 assert.deepEqual(
apps/server/src/serverSettings.ts+66-49
...@@ -40,7 +40,6 @@ import * as Path from "effect/Path";...@@ -40,7 +40,6 @@ import * as Path from "effect/Path";
40import * as PubSub from "effect/PubSub";40import * as PubSub from "effect/PubSub";
41import * as Ref from "effect/Ref";41import * as Ref from "effect/Ref";
42import * as Schema from "effect/Schema";42import * as Schema from "effect/Schema";
43import * as SchemaIssue from "effect/SchemaIssue";
44import * as Semaphore from "effect/Semaphore";43import * as Semaphore from "effect/Semaphore";
45import * as Scope from "effect/Scope";44import * as Scope from "effect/Scope";
46import * as Stream from "effect/Stream";45import * as Stream from "effect/Stream";
...@@ -67,7 +66,7 @@ const normalizeServerSettings = (...@@ -67,7 +66,7 @@ const normalizeServerSettings = (
67 (cause) =>66 (cause) =>
68 new ServerSettingsError({67 new ServerSettingsError({
69 settingsPath: "<memory>",68 settingsPath: "<memory>",
70 detail: `failed to normalize server settings: ${SchemaIssue.makeFormatterDefault()(cause.issue)}`,69 operation: "normalize",
71 cause,70 cause,
72 }),71 }),
73 ),72 ),
...@@ -277,7 +276,7 @@ const make = Effect.gen(function* () {...@@ -277,7 +276,7 @@ const make = Effect.gen(function* () {
277 (cause) =>276 (cause) =>
278 new ServerSettingsError({277 new ServerSettingsError({
279 settingsPath,278 settingsPath,
280 detail: "failed to check settings file existence",279 operation: "check-exists",
281 cause,280 cause,
282 }),281 }),
283 ),282 ),
...@@ -288,7 +287,7 @@ const make = Effect.gen(function* () {...@@ -288,7 +287,7 @@ const make = Effect.gen(function* () {
288 (cause) =>287 (cause) =>
289 new ServerSettingsError({288 new ServerSettingsError({
290 settingsPath,289 settingsPath,
291 detail: "failed to read settings file",290 operation: "read-file",
292 cause,291 cause,
293 }),292 }),
294 ),293 ),
...@@ -305,6 +304,7 @@ const make = Effect.gen(function* () {...@@ -305,6 +304,7 @@ const make = Effect.gen(function* () {
305 yield* Effect.logWarning("failed to parse settings.json, using defaults", {304 yield* Effect.logWarning("failed to parse settings.json, using defaults", {
306 path: settingsPath,305 path: settingsPath,
307 issues: Cause.pretty(decoded.cause),306 issues: Cause.pretty(decoded.cause),
307 cause: decoded.cause,
308 });308 });
309 return DEFAULT_SERVER_SETTINGS;309 return DEFAULT_SERVER_SETTINGS;
310 }310 }
...@@ -318,13 +318,6 @@ const make = Effect.gen(function* () {...@@ -318,13 +318,6 @@ const make = Effect.gen(function* () {
318318
319 const getSettingsFromCache = Cache.get(settingsCache, cacheKey);319 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
328 const materializeProviderEnvironmentSecrets = (321 const materializeProviderEnvironmentSecrets = (
329 settings: ServerSettings,322 settings: ServerSettings,
330 ): Effect.Effect<ServerSettings, ServerSettingsError> =>323 ): Effect.Effect<ServerSettings, ServerSettingsError> =>
...@@ -343,11 +336,15 @@ const make = Effect.gen(function* () {...@@ -343,11 +336,15 @@ const make = Effect.gen(function* () {
343 const secret = yield* secretStore336 const secret = yield* secretStore
344 .get(providerEnvironmentSecretName({ instanceId, name: variable.name }))337 .get(providerEnvironmentSecretName({ instanceId, name: variable.name }))
345 .pipe(338 .pipe(
346 Effect.mapError((cause) =>339 Effect.mapError(
347 toSettingsError(340 (cause) =>
348 `failed to read sensitive environment variable ${variable.name}`,341 new ServerSettingsError({
349 cause,342 settingsPath,
350 ),343 operation: "read-secret",
344 providerInstanceId: instanceId,
345 environmentVariable: variable.name,
346 cause,
347 }),
351 ),348 ),
352 );349 );
353 environment.push({350 environment.push({
...@@ -382,13 +379,18 @@ const make = Effect.gen(function* () {...@@ -382,13 +379,18 @@ const make = Effect.gen(function* () {
382 for (const variable of instance.environment) {379 for (const variable of instance.environment) {
383 const secretName = providerEnvironmentSecretName({ instanceId, name: variable.name });380 const secretName = providerEnvironmentSecretName({ instanceId, name: variable.name });
384 if (!variable.sensitive) {381 if (!variable.sensitive) {
385 yield* secretStore382 yield* secretStore.remove(secretName).pipe(
386 .remove(secretName)383 Effect.mapError(
387 .pipe(384 (cause) =>
388 Effect.mapError((cause) =>385 new ServerSettingsError({
389 toSettingsError(`failed to remove environment secret ${variable.name}`, cause),386 settingsPath,
390 ),387 operation: "remove-secret",
391 );388 providerInstanceId: instanceId,
389 environmentVariable: variable.name,
390 cause,
391 }),
392 ),
393 );
392 environment.push(redactProviderEnvironmentVariable(variable));394 environment.push(redactProviderEnvironmentVariable(variable));
393 continue;395 continue;
394 }396 }
...@@ -396,22 +398,32 @@ const make = Effect.gen(function* () {...@@ -396,22 +398,32 @@ const make = Effect.gen(function* () {
396 nextSecretKeys.add(secretName);398 nextSecretKeys.add(secretName);
397 if (!variable.valueRedacted) {399 if (!variable.valueRedacted) {
398 if (variable.value.length > 0) {400 if (variable.value.length > 0) {
399 yield* secretStore401 yield* secretStore.set(secretName, textEncoder.encode(variable.value)).pipe(
400 .set(secretName, textEncoder.encode(variable.value))402 Effect.mapError(
401 .pipe(403 (cause) =>
402 Effect.mapError((cause) =>404 new ServerSettingsError({
403 toSettingsError(`failed to persist environment secret ${variable.name}`, cause),405 settingsPath,
404 ),406 operation: "write-secret",
405 );407 providerInstanceId: instanceId,
408 environmentVariable: variable.name,
409 cause,
410 }),
411 ),
412 );
406 environment.push({ ...variable, value: "", valueRedacted: true });413 environment.push({ ...variable, value: "", valueRedacted: true });
407 } else {414 } else {
408 yield* secretStore415 yield* secretStore.remove(secretName).pipe(
409 .remove(secretName)416 Effect.mapError(
410 .pipe(417 (cause) =>
411 Effect.mapError((cause) =>418 new ServerSettingsError({
412 toSettingsError(`failed to remove environment secret ${variable.name}`, cause),419 settingsPath,
413 ),420 operation: "remove-secret",
414 );421 providerInstanceId: instanceId,
422 environmentVariable: variable.name,
423 cause,
424 }),
425 ),
426 );
415 const { valueRedacted: _omit, ...rest } = variable;427 const { valueRedacted: _omit, ...rest } = variable;
416 environment.push(rest);428 environment.push(rest);
417 }429 }
...@@ -431,16 +443,18 @@ const make = Effect.gen(function* () {...@@ -431,16 +443,18 @@ const make = Effect.gen(function* () {
431 if (!variable.sensitive) continue;443 if (!variable.sensitive) continue;
432 const secretName = providerEnvironmentSecretName({ instanceId, name: variable.name });444 const secretName = providerEnvironmentSecretName({ instanceId, name: variable.name });
433 if (nextSecretKeys.has(secretName)) continue;445 if (nextSecretKeys.has(secretName)) continue;
434 yield* secretStore446 yield* secretStore.remove(secretName).pipe(
435 .remove(secretName)447 Effect.mapError(
436 .pipe(448 (cause) =>
437 Effect.mapError((cause) =>449 new ServerSettingsError({
438 toSettingsError(450 settingsPath,
439 `failed to remove stale environment secret ${variable.name}`,451 operation: "remove-stale-secret",
452 providerInstanceId: instanceId,
453 environmentVariable: variable.name,
440 cause,454 cause,
441 ),455 }),
442 ),456 ),
443 );457 );
444 }458 }
445 }459 }
446460
...@@ -468,7 +482,7 @@ const make = Effect.gen(function* () {...@@ -468,7 +482,7 @@ const make = Effect.gen(function* () {
468 (cause) =>482 (cause) =>
469 new ServerSettingsError({483 new ServerSettingsError({
470 settingsPath,484 settingsPath,
471 detail: "failed to write settings file",485 operation: "write-file",
472 cause,486 cause,
473 }),487 }),
474 ),488 ),
...@@ -492,7 +506,7 @@ const make = Effect.gen(function* () {...@@ -492,7 +506,7 @@ const make = Effect.gen(function* () {
492 (cause) =>506 (cause) =>
493 new ServerSettingsError({507 new ServerSettingsError({
494 settingsPath,508 settingsPath,
495 detail: "failed to prepare settings directory",509 operation: "prepare-directory",
496 cause,510 cause,
497 }),511 }),
498 ),512 ),
...@@ -571,7 +585,10 @@ const make = Effect.gen(function* () {...@@ -571,7 +585,10 @@ const make = Effect.gen(function* () {
571 materializeProviderEnvironmentSecrets(settings).pipe(585 materializeProviderEnvironmentSecrets(settings).pipe(
572 Effect.catch((error: ServerSettingsError) =>586 Effect.catch((error: ServerSettingsError) =>
573 Effect.logWarning("failed to materialize provider environment secrets", {587 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,
575 }).pipe(Effect.as(settings)),592 }).pipe(Effect.as(settings)),
576 ),593 ),
577 ),594 ),
packages/contracts/src/settings.ts+24-3
...@@ -414,16 +414,37 @@ export type ServerSettings = typeof ServerSettings.Type;...@@ -414,16 +414,37 @@ export type ServerSettings = typeof ServerSettings.Type;
414414
415export const DEFAULT_SERVER_SETTINGS: ServerSettings = Schema.decodeSync(ServerSettings)({});415export 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
417export class ServerSettingsError extends Schema.TaggedErrorClass<ServerSettingsError>()(430export class ServerSettingsError extends Schema.TaggedErrorClass<ServerSettingsError>()(
418 "ServerSettingsError",431 "ServerSettingsError",
419 {432 {
420 settingsPath: Schema.String,433 settingsPath: Schema.String,
421 detail: Schema.String,434 operation: ServerSettingsOperation,
422 cause: Schema.optional(Schema.Defect()),435 providerInstanceId: Schema.optional(Schema.String),
436 environmentVariable: Schema.optional(Schema.String),
437 cause: Schema.Defect(),
423 },438 },
424) {439) {
425 override get message(): string {440 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}.`;
427 }448 }
428}449}
429450