resolve static method

AiConfig resolve({
  1. Map<String, dynamic>? yaml,
  2. Map<String, String?> overrides = const {},
  3. Map<String, dynamic>? global,
  4. Map<String, String>? environment,
})

Builds a configuration from the layered sources.

yaml is the ai: mapping from distribution.yaml (already variable-substituted); overrides are the command line flags.

Implementation

static AiConfig resolve({
  Map<String, dynamic>? yaml,
  Map<String, String?> overrides = const {},
  Map<String, dynamic>? global,
  Map<String, String>? environment,
}) {
  final env = environment ?? Platform.environment;
  final stored = global ?? readGlobalStore();

  /// Where a resolved value came from. Ordered most to least trusted.
  AiSource sourceOf(String key) {
    final override = overrides[key];
    if (override != null && override.isNotEmpty) return AiSource.flag;
    final fromYaml = yaml?[key]?.toString();
    if (fromYaml != null && fromYaml.isNotEmpty) return AiSource.project;
    final fromGlobal = stored?[key]?.toString();
    if (fromGlobal != null && fromGlobal.isNotEmpty) return AiSource.machine;
    return AiSource.none;
  }

  String? pick(String key) {
    final override = overrides[key];
    if (override != null && override.isNotEmpty) return override;
    final fromYaml = yaml?[key]?.toString();
    if (fromYaml != null && fromYaml.isNotEmpty) return fromYaml;
    final fromGlobal = stored?[key]?.toString();
    if (fromGlobal != null && fromGlobal.isNotEmpty) return fromGlobal;
    return null;
  }

  final provider = AiProviderKind.parse(pick('provider'));

  /// Whether a layer's `base-url`, `model` and `api-key` still apply.
  ///
  /// They are provider-scoped: an Anthropic key and model left over from the
  /// machine-wide store, paired with `--ai-provider openai`, is not a
  /// configuration anyone asked for, and the 404 it produces blames settings
  /// the user never touched. A layer that names no provider inherits the
  /// resolved one and keeps its values, so pinning the provider in the
  /// project while keeping the key on the machine still works.
  bool agrees(Map<String, dynamic>? layer) {
    final declared = layer?['provider']?.toString();
    if (declared == null || declared.isEmpty) return true;
    try {
      return AiProviderKind.parse(declared) == provider;
    } on ArgumentError {
      return false;
    }
  }

  final yamlApplies = agrees(yaml);
  final globalApplies = agrees(stored);

  String? pickScoped(String key) {
    final override = overrides[key];
    if (override != null && override.isNotEmpty) return override;
    if (yamlApplies) {
      final fromYaml = yaml?[key]?.toString();
      if (fromYaml != null && fromYaml.isNotEmpty) return fromYaml;
    }
    if (globalApplies) {
      final fromGlobal = stored?[key]?.toString();
      if (fromGlobal != null && fromGlobal.isNotEmpty) return fromGlobal;
    }
    return null;
  }

  final permissionSource = sourceOf('permission');
  var permission = AiPermission.parse(pick('permission'));

  // `permission` is the trust decision, and `distribution.yaml` is the file
  // the assistant is being asked to reason about — a cloned repository must
  // not be able to pre-authorise its own builds and uploads. The project may
  // still restrict, which is always safe.
  var permissionDemoted = false;
  if (permissionSource == AiSource.project &&
      permission == AiPermission.auto) {
    permission = AiPermission.manual;
    permissionDemoted = true;
  }

  return AiConfig(
    provider: provider,
    baseUrl: _trimTrailingSlash(
      pickScoped('base-url') ?? provider.defaultBaseUrl,
    ),
    baseUrlSource: sourceOf('base-url'),
    model: pickScoped('model') ?? provider.defaultModel,
    apiKey: pickScoped('api-key') ??
        env[provider.apiKeyEnvironmentVariable] ??
        '',
    apiKeySource: sourceOf('api-key'),
    permission: permission,
    permissionDemoted: permissionDemoted,
    maxTokens: int.tryParse(pick('max-tokens') ?? '') ?? 2048,
  );
}