867 lines · 31.2 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_TEXT_GENERATION_MODEL,  DEFAULT_TEXT_GENERATION_MODEL_BY_PROVIDER,  DEFAULT_MODEL_BY_PROVIDER,  DEFAULT_SERVER_SETTINGS,  type ModelSelection,  type ProviderInstanceConfig,  type ProviderInstanceEnvironmentVariable,  type UsageLimitSourceConfig,  ProviderDriverKind,  ProviderInstanceId,  resolveProviderInstanceEnabled,  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 * as SqlClient from "effect/unstable/sql/SqlClient";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,  isModelSelectionProviderEnabled,} from "@t3tools/shared/serverSettings";import * as ServerSecretStore from "./auth/ServerSecretStore.ts"; export { resolveSourceControlWriterModelSelection } from "@t3tools/shared/serverSettings"; const encodeServerSettings = Schema.encodeEffect(ServerSettings);const encodeServerSettingsJson = Schema.encodeUnknownEffect(fromJsonStringPretty(ServerSettings));const decodeServerSettings = Schema.decodeUnknownEffect(ServerSettings); const textEncoder = new TextEncoder();const textDecoder = new TextDecoder(); /** * Fold the legacy in-config `enabled` flag into the envelope-level * `ProviderInstanceConfig.enabled` and strip it from the config blob, so * explicit provider instances carry exactly one enabled flag. Old settings * files can hold both flags with conflicting values; an explicit false on * either side wins so a user's disable is never silently undone. Runs on * every load and update — the file converges on the next write. */const foldProviderInstanceEnabledFlags = (settings: ServerSettings): ServerSettings => {  let changed = false;  const providerInstances: Record<string, ProviderInstanceConfig> = {};  for (const [instanceId, instance] of Object.entries(settings.providerInstances)) {    const config = instance.config;    // Only fold boolean flags: a malformed `enabled` (e.g. `"false"`) must    // stay in the blob so driver schema validation flags it instead of the    // fold silently repairing the config.    if (      config === null ||      typeof config !== "object" ||      Array.isArray(config) ||      typeof (config as { readonly enabled?: unknown }).enabled !== "boolean"    ) {      providerInstances[instanceId] = instance;      continue;    }    const { enabled: configEnabled, ...restConfig } = config as Record<string, unknown> & {      readonly enabled: boolean;    };    const resolved =      instance.enabled === false || configEnabled === false        ? false        : (instance.enabled ?? configEnabled);    changed = true;    providerInstances[instanceId] = {      ...instance,      enabled: resolved,      config: restConfig,    } satisfies ProviderInstanceConfig;  }  if (!changed) {    return settings;  }  return {    ...settings,    providerInstances: providerInstances as ServerSettings["providerInstances"],  };}; const normalizeServerSettings = (  settings: ServerSettings,): Effect.Effect<ServerSettings, ServerSettingsError> =>  encodeServerSettings(settings).pipe(    Effect.flatMap(decodeServerSettings),    Effect.map(foldProviderInstanceEnabledFlags),    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")}`;} /** * On disk the hub key is replaced by this marker and the real value lives in * the secret store, mirroring provider environment secrets. A client that * sends the marker back means "keep what you have". */const USAGE_LIMIT_SOURCE_KEY_REDACTED = "\u2022\u2022\u2022\u2022\u2022\u2022"; function usageLimitSourceSecretName(sourceId: string): string {  return `usage-limit-source-${Buffer.from(sourceId, "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,    ]),  );  // The hub key is a bearer secret; clients only need to know one is set.  const usageLimitSources = Object.fromEntries(    Object.entries(settings.usageLimitSources).map(([id, source]) => [      id,      {        ...source,        managementKey: source.managementKey.length > 0 ? USAGE_LIMIT_SOURCE_KEY_REDACTED : "",      },    ]),  );  return { ...settings, providerInstances, usageLimitSources };} 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>;     /**     * Acquire a settings change subscription synchronously in the current     * fiber. Use this before reading a snapshot when changes between the     * snapshot and a lazily started stream must not be lost.     */    readonly subscribeChanges: Effect.Effect<Stream.Stream<ServerSettings>, never, Scope.Scope>;  }>()("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, providerHealthRefreshInterval, ...overridesForMerge } =      overrides;    const merged = deepMerge(DEFAULT_SERVER_SETTINGS, overridesForMerge);    const initialSettings = yield* normalizeServerSettings({      ...merged,      ...(automaticGitFetchInterval !== undefined        ? { automaticGitFetchInterval: automaticGitFetchInterval as Duration.Duration }        : {}),      ...(providerHealthRefreshInterval !== undefined        ? { providerHealthRefreshInterval: providerHealthRefreshInterval as Duration.Duration }        : {}),    });    const currentSettingsRef = yield* Ref.make<ServerSettings>(initialSettings);     return {      start: Effect.void,      ready: Effect.void,      getSettings: Ref.get(currentSettingsRef).pipe(Effect.map(resolveTextGenerationProvider)),      updateSettings: (patch) =>        Ref.get(currentSettingsRef).pipe(          Effect.map((currentSettings) => applyServerSettingsPatch(currentSettings, patch)),          Effect.flatMap(normalizeServerSettings),          Effect.tap((nextSettings) => Ref.set(currentSettingsRef, nextSettings)),          Effect.map(resolveTextGenerationProvider),        ),      streamChanges: Stream.empty,      subscribeChanges: Effect.succeed(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);const PersistedOptionalProviderSettings = Schema.Struct({  providers: Schema.optionalKey(    Schema.Struct({      cursor: Schema.optionalKey(Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean) })),      grok: Schema.optionalKey(Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean) })),      opencode: Schema.optionalKey(Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean) })),    }),  ),});const decodePersistedOptionalProviderSettingsJsonExit = Schema.decodeUnknownExit(  fromLenientJson(PersistedOptionalProviderSettings),); function restoreUsedProviders(  settings: ServerSettings,  persisted: typeof PersistedOptionalProviderSettings.Type,  providerHistory: ReadonlyArray<{    readonly providerName: string;    readonly providerInstanceId: string | null;  }>,): ServerSettings {  const usedProviders = new Set(providerHistory.map(({ providerName }) => providerName));  const usedProviderInstances = new Set(    providerHistory.map(      ({ providerName, providerInstanceId }) => providerInstanceId ?? providerName,    ),  );  const providerInstances = Object.fromEntries(    Object.entries(settings.providerInstances).map(([instanceId, instance]) => [      instanceId,      instance.enabled === undefined &&      (instance.driver === "cursor" ||        instance.driver === "grok" ||        instance.driver === "opencode") &&      usedProviderInstances.has(instanceId)        ? { ...instance, enabled: true }        : instance,    ]),  );   return {    ...settings,    providers: {      ...settings.providers,      cursor: {        ...settings.providers.cursor,        enabled: persisted.providers?.cursor?.enabled ?? usedProviders.has("cursor"),      },      grok: {        ...settings.providers.grok,        enabled: persisted.providers?.grok?.enabled ?? usedProviders.has("grok"),      },      opencode: {        ...settings.providers.opencode,        enabled: persisted.providers?.opencode?.enabled ?? usedProviders.has("opencode"),      },    },    providerInstances,  };} function resolveTextGenerationProvider(settings: ServerSettings): ServerSettings {  return isModelSelectionProviderEnabled(settings, settings.textGenerationModelSelection)    ? settings    : fallbackTextGenerationProvider(settings);} function fallbackTextGenerationProvider(settings: ServerSettings): ServerSettings {  // Same precedence as isModelSelectionProviderEnabled: an explicit provider  // instance wins over the legacy providers map, which decodes to defaults  // (codex enabled) when the Providers UI has only written providerInstances.  const fallbackEntry = Object.entries(settings.providers).find(([driver, provider]) => {    const instance = settings.providerInstances[ProviderInstanceId.make(driver)];    return instance === undefined ? provider.enabled : resolveProviderInstanceEnabled(instance);  });  const fallback = fallbackEntry ? ProviderDriverKind.make(fallbackEntry[0]) : undefined;  if (!fallback) {    return settings;  }   return {    ...settings,    textGenerationModelSelection: {      instanceId: ProviderInstanceId.make(fallback),      model:        DEFAULT_TEXT_GENERATION_MODEL_BY_PROVIDER[fallback] ??        DEFAULT_MODEL_BY_PROVIDER[fallback] ??        DEFAULT_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([  "backgroundActivity",  "automaticGitFetchInterval",  "providerHealthRefreshInterval",  "sourceControlWriterModelSelection",  "textGenerationModelSelection",]); // Preserve both enabled states because provider history cannot recover a new opt-in.const PERSISTED_SERVER_SETTINGS_DEFAULTS = {  ...DEFAULT_SERVER_SETTINGS,  providers: {    ...DEFAULT_SERVER_SETTINGS.providers,    cursor: { ...DEFAULT_SERVER_SETTINGS.providers.cursor, enabled: undefined },    grok: { ...DEFAULT_SERVER_SETTINGS.providers.grok, enabled: undefined },    opencode: { ...DEFAULT_SERVER_SETTINGS.providers.opencode, enabled: undefined },  },}; 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 sql = yield* SqlClient.SqlClient;  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* () {    let settings = DEFAULT_SERVER_SETTINGS;    let persisted: typeof PersistedOptionalProviderSettings.Type = {};     if (yield* readConfigExists) {      const raw = yield* readRawConfig;      const decoded = decodeServerSettingsJsonExit(raw);      const persistedSettings = decodePersistedOptionalProviderSettingsJsonExit(raw);      if (persistedSettings._tag === "Success") {        persisted = persistedSettings.value;      }      if (decoded._tag === "Failure" || persistedSettings._tag === "Failure") {        const failure = decoded._tag === "Failure" ? decoded : persistedSettings;        if (failure._tag === "Failure") {          yield* Effect.logWarning("failed to parse settings.json, using defaults", {            path: settingsPath,            issues: Cause.pretty(failure.cause),            cause: failure.cause,          });        }      } else {        settings = decoded.value;      }    }     const providerHistory = yield* sql<{      readonly providerName: string;      readonly providerInstanceId: string | null;    }>`      SELECT DISTINCT        provider_name AS "providerName",        provider_instance_id AS "providerInstanceId"      FROM projection_thread_sessions      WHERE provider_name IN ('cursor', 'grok', 'opencode')      UNION      SELECT DISTINCT        provider_name AS "providerName",        provider_instance_id AS "providerInstanceId"      FROM provider_session_runtime      WHERE provider_name IN ('cursor', 'grok', 'opencode')    `.pipe(      Effect.mapError(        (cause) =>          new ServerSettingsError({            settingsPath,            operation: "read-provider-history",            cause,          }),      ),    );     return foldProviderInstanceEnabledFlags(      restoreUsedProviders(settings, persisted, providerHistory),    );  });   const settingsCache = yield* Cache.make<typeof cacheKey, ServerSettings, ServerSettingsError>({    capacity: 1,    lookup: () => loadSettingsFromDisk,

Lines 1–500 of 867.