collectQueueSecrets function

Map<String, String> collectQueueSecrets(
  1. List<ProviderQueueEntry> entries,
  2. SecureKeyCache keys, {
  3. Map<String, String>? env,
})

Collects the secrets snapshot for the queue: each entry's apiKeyEnv resolves from the environment, else the secure store (env wins — the same precedence as the roles snapshot). A chatgpt-codex entry that named no apiKeyEnv resolves its DEFAULT Keychain slot (chatGptOAuthCredentialsEnvName) under the same rule, and a refreshTokenEnv name resolves the same way (issue #1704). Every resolved base name also sweeps its ROTATION STACK from the environment (NAME_2, NAME_3, … — the same rule collectRoleSecrets applies, gh-1781): buildProviderQueueChain's collectKeyStack builds a multi-credential ApiKeyRing from it for in-entry key rotation. Rotation-stack names stay env-only (the store holds base names only).

Implementation

Map<String, String> collectQueueSecrets(
  List<ProviderQueueEntry> entries,
  SecureKeyCache keys, {
  Map<String, String>? env,
}) {
  final environment = env ?? Platform.environment;
  final secrets = <String, String>{};
  for (final entry in entries) {
    // gh-1781 review r1: one ladder for "which key names does a queue
    // entry reference" — [queueEntryKeyNames] is the source of truth.
    for (final name in queueEntryKeyNames([entry])) {
      if (secrets.containsKey(name)) continue;
      final fromEnv = environment[name];
      if (fromEnv != null && fromEnv.isNotEmpty) {
        secrets[name] = fromEnv;
      } else {
        final stored = keys.read(name);
        if (stored != null) secrets[name] = stored;
      }
      final suffix = RegExp('^${RegExp.escape(name)}_\\d+\$');
      for (final candidate in environment.entries) {
        if (suffix.hasMatch(candidate.key) && candidate.value.isNotEmpty) {
          secrets[candidate.key] = candidate.value;
        }
      }
    }
  }
  return secrets;
}