602 lines · 21.0 KB · TypeScript
RawDownload
/** * ServerSettings - Server-authoritative settings service. * * Owns persistence, validation, and change notification of settings that affect * server-side behavior (binary paths, streaming mode, env mode, custom models, * text generation model selection). * * Follows the same pattern as `keybindings.ts`: JSON file + Cache + PubSub + * Semaphore + FileSystem.watch for concurrency and external edit detection. * * @module ServerSettings */import {  DEFAULT_GIT_TEXT_GENERATION_MODEL,  DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER,  DEFAULT_MODEL_BY_PROVIDER,  DEFAULT_SERVER_SETTINGS,  isProviderDriverKind,  type ModelSelection,  type ProviderInstanceConfig,  type ProviderInstanceEnvironmentVariable,  ProviderDriverKind,  ProviderInstanceId,  ServerSettings,  ServerSettingsError,  type ServerSettingsPatch,} from "@t3tools/contracts";import * as Cache from "effect/Cache";import * as Cause from "effect/Cause";import * as Context from "effect/Context";import * as Deferred from "effect/Deferred";import * as Duration from "effect/Duration";import * as Equal from "effect/Equal";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as PubSub from "effect/PubSub";import * as Ref from "effect/Ref";import * as Schema from "effect/Schema";import * as Semaphore from "effect/Semaphore";import * as Scope from "effect/Scope";import * as Stream from "effect/Stream";import { writeFileStringAtomically } from "./atomicWrite.ts";import * as ServerConfig from "./config.ts";import { type DeepPartial, deepMerge } from "@t3tools/shared/Struct";import { fromJsonStringPretty, fromLenientJson } from "@t3tools/shared/schemaJson";import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";import * as ServerSecretStore from "./auth/ServerSecretStore.ts"; const encodeServerSettings = Schema.encodeEffect(ServerSettings);const encodeServerSettingsJson = Schema.encodeUnknownEffect(fromJsonStringPretty(ServerSettings));const decodeServerSettings = Schema.decodeUnknownEffect(ServerSettings); const textEncoder = new TextEncoder();const textDecoder = new TextDecoder(); const normalizeServerSettings = (  settings: ServerSettings,): Effect.Effect<ServerSettings, ServerSettingsError> =>  encodeServerSettings(settings).pipe(    Effect.flatMap(decodeServerSettings),    Effect.mapError(      (cause) =>        new ServerSettingsError({          settingsPath: "<memory>",          operation: "normalize",          cause,        }),    ),  ); function providerEnvironmentSecretName(input: {  readonly instanceId: string;  readonly name: string;}): string {  return `provider-env-${Buffer.from(input.instanceId, "utf8").toString("base64url")}-${Buffer.from(input.name, "utf8").toString("base64url")}`;} function redactProviderEnvironmentVariable(  variable: ProviderInstanceEnvironmentVariable,): ProviderInstanceEnvironmentVariable {  if (!variable.sensitive) {    const { valueRedacted: _omit, ...rest } = variable;    return rest;  }  return {    ...variable,    value: "",    ...(variable.value.length > 0 || variable.valueRedacted ? { valueRedacted: true } : {}),  };} export function redactServerSettingsForClient(settings: ServerSettings): ServerSettings {  const providerInstances = Object.fromEntries(    Object.entries(settings.providerInstances).map(([instanceId, instance]) => [      instanceId,      instance.environment        ? {            ...instance,            environment: instance.environment.map(redactProviderEnvironmentVariable),          }        : instance,    ]),  );  return { ...settings, providerInstances };} export class ServerSettingsService extends Context.Service<  ServerSettingsService,  {    /** Start the settings runtime and attach file watching. */    readonly start: Effect.Effect<void, ServerSettingsError>;     /** Await settings runtime readiness. */    readonly ready: Effect.Effect<void, ServerSettingsError>;     /** Read the current settings. */    readonly getSettings: Effect.Effect<ServerSettings, ServerSettingsError>;     /** Patch settings and persist. Returns the new full settings object. */    readonly updateSettings: (      patch: ServerSettingsPatch,    ) => Effect.Effect<ServerSettings, ServerSettingsError>;     /** Stream of settings change events. */    readonly streamChanges: Stream.Stream<ServerSettings>;  }>()("t3/serverSettings/ServerSettingsService") {  /** @deprecated Import and use `layerTest` from this module. */  static readonly layerTest = (overrides: DeepPartial<ServerSettings> = {}) => layerTest(overrides);} const makeTest = (overrides: DeepPartial<ServerSettings> = {}) =>  Effect.gen(function* () {    const { automaticGitFetchInterval, ...overridesForMerge } = overrides;    const merged = deepMerge(DEFAULT_SERVER_SETTINGS, overridesForMerge);    const initialSettings = yield* normalizeServerSettings({      ...merged,      ...(automaticGitFetchInterval !== undefined        ? { automaticGitFetchInterval: automaticGitFetchInterval as Duration.Duration }        : {}),    });    const currentSettingsRef = yield* Ref.make<ServerSettings>(initialSettings);     return {      start: Effect.void,      ready: Effect.void,      getSettings: Ref.get(currentSettingsRef),      updateSettings: (patch) =>        Ref.get(currentSettingsRef).pipe(          Effect.map((currentSettings) => applyServerSettingsPatch(currentSettings, patch)),          Effect.flatMap(normalizeServerSettings),          Effect.tap((nextSettings) => Ref.set(currentSettingsRef, nextSettings)),        ),      streamChanges: Stream.empty,    } satisfies ServerSettingsService["Service"];  }); export const layerTest = (overrides: DeepPartial<ServerSettings> = {}) =>  Layer.effect(ServerSettingsService, makeTest(overrides)); const ServerSettingsJson = fromLenientJson(ServerSettings);const decodeServerSettingsJsonExit = Schema.decodeUnknownExit(ServerSettingsJson); type LegacyProviderSettings = ServerSettings["providers"][keyof ServerSettings["providers"]]; const getLegacyProviderSettings = (  settings: ServerSettings,  provider: ProviderDriverKind,): LegacyProviderSettings | undefined =>  (settings.providers as Record<string, LegacyProviderSettings | undefined>)[provider]; /** * Ensure the `textGenerationModelSelection` points to an enabled provider. * If the selected provider is disabled, fall back to the first enabled * provider with its default model.  This is applied at read-time so the * persisted preference is preserved for when a provider is re-enabled. */function resolveTextGenerationProvider(settings: ServerSettings): ServerSettings {  const selection = settings.textGenerationModelSelection;  const instanceConfig = settings.providerInstances[selection.instanceId];  if (instanceConfig !== undefined) {    return (instanceConfig.enabled ?? true) ? settings : fallbackTextGenerationProvider(settings);  }   if (    isProviderDriverKind(selection.instanceId) &&    getLegacyProviderSettings(settings, selection.instanceId)?.enabled  ) {    return settings;  }   return fallbackTextGenerationProvider(settings);} function fallbackTextGenerationProvider(settings: ServerSettings): ServerSettings {  const fallbackEntry = Object.entries(settings.providers).find(([, provider]) => provider.enabled);  const fallback = fallbackEntry ? ProviderDriverKind.make(fallbackEntry[0]) : undefined;  if (!fallback) {    return settings;  }   return {    ...settings,    textGenerationModelSelection: {      instanceId: ProviderInstanceId.make(fallback),      model:        DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER[fallback] ??        DEFAULT_MODEL_BY_PROVIDER[fallback] ??        DEFAULT_GIT_TEXT_GENERATION_MODEL,    } satisfies ModelSelection,  };} // Values under these keys are compared as a whole — never stripped field-by-field.const ATOMIC_SETTINGS_KEYS: ReadonlySet<string> = new Set([  "automaticGitFetchInterval",  "textGenerationModelSelection",]); function stripDefaultServerSettings(current: unknown, defaults: unknown): unknown | undefined {  if (Array.isArray(current) || Array.isArray(defaults)) {    return Equal.equals(current, defaults) ? undefined : current;  }   if (    current !== null &&    defaults !== null &&    typeof current === "object" &&    typeof defaults === "object"  ) {    const currentRecord = current as Record<string, unknown>;    const defaultsRecord = defaults as Record<string, unknown>;    const next: Record<string, unknown> = {};     for (const key of Object.keys(currentRecord)) {      if (ATOMIC_SETTINGS_KEYS.has(key)) {        if (!Equal.equals(currentRecord[key], defaultsRecord[key])) {          next[key] = currentRecord[key];        }      } else {        const stripped = stripDefaultServerSettings(currentRecord[key], defaultsRecord[key]);        if (stripped !== undefined) {          next[key] = stripped;        }      }    }     return Object.keys(next).length > 0 ? next : undefined;  }   return Object.is(current, defaults) ? undefined : current;} const make = Effect.gen(function* () {  const { settingsPath } = yield* ServerConfig.ServerConfig;  const fs = yield* FileSystem.FileSystem;  const pathService = yield* Path.Path;  const secretStore = yield* ServerSecretStore.ServerSecretStore;  const writeSemaphore = yield* Semaphore.make(1);  const cacheKey = "settings" as const;  const changesPubSub = yield* PubSub.unbounded<ServerSettings>();  const startedRef = yield* Ref.make(false);  const startedDeferred = yield* Deferred.make<void, ServerSettingsError>();  const watcherScope = yield* Scope.make("sequential");  yield* Effect.addFinalizer(() => Scope.close(watcherScope, Exit.void));   const emitChange = (settings: ServerSettings) =>    PubSub.publish(changesPubSub, settings).pipe(Effect.asVoid);   const readConfigExists = fs.exists(settingsPath).pipe(    Effect.mapError(      (cause) =>        new ServerSettingsError({          settingsPath,          operation: "check-exists",          cause,        }),    ),  );   const readRawConfig = fs.readFileString(settingsPath).pipe(    Effect.mapError(      (cause) =>        new ServerSettingsError({          settingsPath,          operation: "read-file",          cause,        }),    ),  );   const loadSettingsFromDisk = Effect.gen(function* () {    if (!(yield* readConfigExists)) {      return DEFAULT_SERVER_SETTINGS;    }     const raw = yield* readRawConfig;    const decoded = decodeServerSettingsJsonExit(raw);    if (decoded._tag === "Failure") {      yield* Effect.logWarning("failed to parse settings.json, using defaults", {        path: settingsPath,        issues: Cause.pretty(decoded.cause),        cause: decoded.cause,      });      return DEFAULT_SERVER_SETTINGS;    }    return decoded.value;  });   const settingsCache = yield* Cache.make<typeof cacheKey, ServerSettings, ServerSettingsError>({    capacity: 1,    lookup: () => loadSettingsFromDisk,  });   const getSettingsFromCache = Cache.get(settingsCache, cacheKey);   const materializeProviderEnvironmentSecrets = (    settings: ServerSettings,  ): Effect.Effect<ServerSettings, ServerSettingsError> =>    Effect.gen(function* () {      const providerInstances: Record<string, ProviderInstanceConfig> = {        ...settings.providerInstances,      };      for (const [instanceId, instance] of Object.entries(settings.providerInstances)) {        if (!instance.environment) continue;        const environment: ProviderInstanceEnvironmentVariable[] = [];        for (const variable of instance.environment) {          if (!variable.sensitive || !variable.valueRedacted) {            environment.push(variable);            continue;          }          const secret = yield* secretStore            .get(providerEnvironmentSecretName({ instanceId, name: variable.name }))            .pipe(              Effect.mapError(                (cause) =>                  new ServerSettingsError({                    settingsPath,                    operation: "read-secret",                    providerInstanceId: instanceId,                    environmentVariable: variable.name,                    cause,                  }),              ),            );          environment.push({            ...variable,            value: Option.isSome(secret) ? textDecoder.decode(secret.value) : "",          });        }        providerInstances[instanceId] = {          ...instance,          environment,        } satisfies ProviderInstanceConfig;      }      return {        ...settings,        providerInstances: providerInstances as ServerSettings["providerInstances"],      };    });   const persistProviderEnvironmentSecrets = (    current: ServerSettings,    next: ServerSettings,  ): Effect.Effect<ServerSettings, ServerSettingsError> =>    Effect.gen(function* () {      const providerInstances: Record<string, ProviderInstanceConfig> = {        ...next.providerInstances,      };       const nextSecretKeys = new Set<string>();      for (const [instanceId, instance] of Object.entries(next.providerInstances)) {        if (!instance.environment) continue;        const environment: ProviderInstanceEnvironmentVariable[] = [];        for (const variable of instance.environment) {          const secretName = providerEnvironmentSecretName({ instanceId, name: variable.name });          if (!variable.sensitive) {            yield* secretStore.remove(secretName).pipe(              Effect.mapError(                (cause) =>                  new ServerSettingsError({                    settingsPath,                    operation: "remove-secret",                    providerInstanceId: instanceId,                    environmentVariable: variable.name,                    cause,                  }),              ),            );            environment.push(redactProviderEnvironmentVariable(variable));            continue;          }           nextSecretKeys.add(secretName);          if (!variable.valueRedacted) {            if (variable.value.length > 0) {              yield* secretStore.set(secretName, textEncoder.encode(variable.value)).pipe(                Effect.mapError(                  (cause) =>                    new ServerSettingsError({                      settingsPath,                      operation: "write-secret",                      providerInstanceId: instanceId,                      environmentVariable: variable.name,                      cause,                    }),                ),              );              environment.push({ ...variable, value: "", valueRedacted: true });            } else {              yield* secretStore.remove(secretName).pipe(                Effect.mapError(                  (cause) =>                    new ServerSettingsError({                      settingsPath,                      operation: "remove-secret",                      providerInstanceId: instanceId,                      environmentVariable: variable.name,                      cause,                    }),                ),              );              const { valueRedacted: _omit, ...rest } = variable;              environment.push(rest);            }            continue;          }           environment.push(redactProviderEnvironmentVariable(variable));        }        providerInstances[instanceId] = {          ...instance,          environment,        } satisfies ProviderInstanceConfig;      }       for (const [instanceId, instance] of Object.entries(current.providerInstances)) {        for (const variable of instance.environment ?? []) {          if (!variable.sensitive) continue;          const secretName = providerEnvironmentSecretName({ instanceId, name: variable.name });          if (nextSecretKeys.has(secretName)) continue;          yield* secretStore.remove(secretName).pipe(            Effect.mapError(              (cause) =>                new ServerSettingsError({                  settingsPath,                  operation: "remove-stale-secret",                  providerInstanceId: instanceId,                  environmentVariable: variable.name,                  cause,                }),            ),          );        }      }       return {        ...next,        providerInstances: providerInstances as ServerSettings["providerInstances"],      };    });   const writeSettingsAtomically = Effect.fnUntraced(    function* (settings: ServerSettings) {      const sparseSettingsJson = yield* encodeServerSettingsJson(        stripDefaultServerSettings(settings, DEFAULT_SERVER_SETTINGS) ?? {},      );       return yield* writeFileStringAtomically({        filePath: settingsPath,        contents: `${sparseSettingsJson}\n`,      }).pipe(        Effect.provideService(FileSystem.FileSystem, fs),        Effect.provideService(Path.Path, pathService),      );    },    Effect.mapError(      (cause) =>        new ServerSettingsError({          settingsPath,          operation: "write-file",          cause,        }),    ),  );   const revalidateAndEmit = writeSemaphore.withPermits(1)(    Effect.gen(function* () {      yield* Cache.invalidate(settingsCache, cacheKey);      const settings = yield* getSettingsFromCache;      yield* emitChange(settings);    }),  );   const startWatcher = Effect.gen(function* () {    const settingsDir = pathService.dirname(settingsPath);

Lines 1–500 of 602.