collectRoleSecrets function

Map<String, String> collectRoleSecrets(
  1. ModelRolesConfig rolesConfig,
  2. SecureKeyCache keys, {
  3. Map<String, String>? env,
  4. List<CustomProviderEntry> customProviders = const [],
})

Collects the secrets snapshot for the model-roles resolver: every provider catalog env name plus its rotation stack (NAME, NAME_2, NAME_3, ...), plus any base name referenced by an explicit apiKeyName in the roles config. The platform secure store backs up base names where the environment has none (env wins; rotation stacks stay env-only — secure storage holds base names only). customProviders (the customProviders: section) feeds the registry leg (issue #1552): a chain entry naming one of these entries resolves keys through the entry — its pinned keyName first, else the endpoint-scoped slot — so both names ride the snapshot, scoped to the entries the chains actually NAME: an unreferenced saved entry changes nothing (existing configs stay byte-identical — the PR contract).

Implementation

Map<String, String> collectRoleSecrets(
  ModelRolesConfig rolesConfig,
  SecureKeyCache keys, {
  Map<String, String>? env,
  List<CustomProviderEntry> customProviders = const [],
}) {
  final baseNames = <String>{
    for (final spec in providerCatalog.values) ...spec.apiKeyEnvNames,
    ...roleKeyNames(rolesConfig, customProviders: customProviders),
    ..._referencedCustomKeyNames(rolesConfig, customProviders),
  };
  final secrets = <String, String>{};
  final environment = env ?? Platform.environment;
  for (final base in baseNames) {
    final suffix = RegExp('^${RegExp.escape(base)}_\\d+\$');
    for (final entry in environment.entries) {
      if (entry.key == base || suffix.hasMatch(entry.key)) {
        if (entry.value.isNotEmpty) secrets[entry.key] = entry.value;
      }
    }
    if (!secrets.containsKey(base)) {
      final stored = keys.read(base);
      if (stored != null) secrets[base] = stored;
    }
  }
  return secrets;
}