| @@ -1059,6 +1059,128 @@ it.layer(NodeServices.layer)("server settings", (it) => { · 1058 unmodified lines |
| 1059 | 1059 | }).pipe(Effect.provide(makeServerSettingsLayer())), |
| 1060 | 1060 | ); |
| 1061 | 1061 | |
| 1062 | + it.effect("keeps the inline value on disk when secret migration fails", () => { |
| 1063 | + const cause = new ServerSecretStore.SecretStorePersistError({ |
| 1064 | + resource: "provider environment secret", |
| 1065 | + cause: new Error("Secret storage unavailable"), |
| 1066 | + }); |
| 1067 | + const secretLayer = Layer.effect( |
| 1068 | + ServerSecretStore.ServerSecretStore, |
| 1069 | + Effect.map(ServerSecretStore.ServerSecretStore, (store) => ({ |
| 1070 | + ...store, |
| 1071 | + set: () => Effect.fail(cause), |
| 1072 | + })), |
| 1073 | + ).pipe(Layer.provide(ServerSecretStore.layer)); |
| 1074 | + const settingsLayer = ServerSettingsModule.layer.pipe( |
| 1075 | + Layer.provide(secretLayer), |
| 1076 | + Layer.provideMerge(Layer.fresh(SqlitePersistenceMemory)), |
| 1077 | + Layer.provideMerge( |
| 1078 | + Layer.fresh( |
| 1079 | + ServerConfig.layerTest(process.cwd(), { |
| 1080 | + prefix: "t3code-inline-secret-failure-test-", |
| 1081 | + }), |
| 1082 | + ), |
| 1083 | + ), |
| 1084 | + ); |
| 1085 | + return Effect.gen(function* () { |
| 1086 | + const instanceId = ProviderInstanceId.make("codex_personal"); |
| 1087 | + const service = yield* ServerSettingsModule.ServerSettingsService; |
| 1088 | + const config = yield* ServerConfig.ServerConfig; |
| 1089 | + const fs = yield* FileSystem.FileSystem; |
| 1090 | + const original = |
| 1091 | + '{"providerInstances":{"codex_personal":{"driver":"codex","environment":[{"name":"API_TOKEN","value":"inline-test-token","sensitive":true}],"config":{}}}}'; |
| 1092 | + yield* fs.writeFileString(config.settingsPath, original); |
| 1093 | + const error = yield* Effect.flip( |
| 1094 | + service.updateSettings({ |
| 1095 | + providerInstances: { |
| 1096 | + [instanceId]: { |
| 1097 | + driver: ProviderDriverKind.make("codex"), |
| 1098 | + environment: [{ name: "API_TOKEN", value: "", sensitive: true, valueRedacted: true }], |
| 1099 | + config: {}, |
| 1100 | + }, |
| 1101 | + }, |
| 1102 | + }), |
| 1103 | + ); |
| 1104 | + assert.equal(error.operation, "write-secret"); |
| 1105 | + assert.strictEqual(error.cause, cause); |
| 1106 | + assert.equal(yield* fs.readFileString(config.settingsPath), original); |
| 1107 | + const settings = yield* service.getSettings; |
| 1108 | + assert.equal( |
| 1109 | + settings.providerInstances[instanceId]?.environment?.[0]?.value, |
| 1110 | + "inline-test-token", |
| 1111 | + ); |
| 1112 | + }).pipe(Effect.provide(settingsLayer)); |
| 1113 | + }); |
| 1114 | + |
| 1115 | + for (const { label, variable, expected, duplicate } of [ |
| 1116 | + { |
| 1117 | + label: "preserves an inline secret on a redacted settings save", |
| 1118 | + variable: { name: "API_TOKEN", value: "", sensitive: true, valueRedacted: true }, |
| 1119 | + expected: "inline-test-token", |
| 1120 | + }, |
| 1121 | + { |
| 1122 | + label: "preserves the effective last inline secret when names are duplicated", |
| 1123 | + variable: { name: "API_TOKEN", value: "", sensitive: true, valueRedacted: true }, |
| 1124 | + expected: "last-inline-test-token", |
| 1125 | + duplicate: true, |
| 1126 | + }, |
| 1127 | + { |
| 1128 | + label: "replaces an inline secret with an explicit value", |
| 1129 | + variable: { name: "API_TOKEN", value: "replacement-test-token", sensitive: true }, |
| 1130 | + expected: "replacement-test-token", |
| 1131 | + }, |
| 1132 | + { |
| 1133 | + label: "clears an inline secret with an explicit empty value", |
| 1134 | + variable: { name: "API_TOKEN", value: "", sensitive: true }, |
| 1135 | + expected: "", |
| 1136 | + }, |
| 1137 | + ]) { |
| 1138 | + it.effect(label, () => |
| 1139 | + Effect.gen(function* () { |
| 1140 | + const instanceId = ProviderInstanceId.make("codex_personal"); |
| 1141 | + const serverSettings = yield* ServerSettingsModule.ServerSettingsService; |
| 1142 | + const serverConfig = yield* ServerConfig.ServerConfig; |
| 1143 | + const fileSystem = yield* FileSystem.FileSystem; |
| 1144 | + yield* fileSystem.writeFileString( |
| 1145 | + serverConfig.settingsPath, |
| 1146 | + duplicate |
| 1147 | + ? '{"providerInstances":{"codex_personal":{"driver":"codex","environment":[{"name":"API_TOKEN","value":"inline-test-token","sensitive":true},{"name":"API_TOKEN","value":"last-inline-test-token","sensitive":true}],"config":{}}}}' |
| 1148 | + : '{"providerInstances":{"codex_personal":{"driver":"codex","environment":[{"name":"API_TOKEN","value":"inline-test-token","sensitive":true}],"config":{}}}}', |
| 1149 | + ); |
| 1150 | + const initial = yield* serverSettings.getSettings; |
| 1151 | + assert.equal( |
| 1152 | + initial.providerInstances[instanceId]?.environment?.[0]?.value, |
| 1153 | + "inline-test-token", |
| 1154 | + ); |
| 1155 | + |
| 1156 | + const next = yield* serverSettings.updateSettings({ |
| 1157 | + providerInstances: { |
| 1158 | + [instanceId]: { |
| 1159 | + driver: ProviderDriverKind.make("codex"), |
| 1160 | + displayName: "Renamed provider", |
| 1161 | + environment: duplicate ? [variable, variable] : [variable], |
| 1162 | + config: {}, |
| 1163 | + }, |
| 1164 | + }, |
| 1165 | + }); |
| 1166 | + assert.equal(next.providerInstances[instanceId]?.environment?.[0]?.value, expected); |
| 1167 | + const raw = yield* fileSystem.readFileString(serverConfig.settingsPath); |
| 1168 | + assert.notInclude(raw, "inline-test-token"); |
| 1169 | + assert.notInclude(raw, "replacement-test-token"); |
| 1170 | + |
| 1171 | + const reloaded = yield* Effect.gen(function* () { |
| 1172 | + const fresh = yield* ServerSettingsModule.ServerSettingsService; |
| 1173 | + return yield* fresh.getSettings; |
| 1174 | + }).pipe( |
| 1175 | + Effect.provide( |
| 1176 | + Layer.fresh(ServerSettingsModule.layer).pipe(Layer.provide(ServerSecretStore.layer)), |
| 1177 | + ), |
| 1178 | + ); |
| 1179 | + assert.equal(reloaded.providerInstances[instanceId]?.environment?.[0]?.value, expected); |
| 1180 | + }).pipe(Effect.provide(makeServerSettingsLayer())), |
| 1181 | + ); |
| 1182 | + } |
| 1183 | + |
| 1062 | 1184 | it.effect("stores sensitive provider instance environment values outside settings.json", () => |
| 1063 | 1185 | Effect.gen(function* () { |
| 1064 | 1186 | const serverSettings = yield* ServerSettingsModule.ServerSettingsService; |