From 8d3fe70e2c8786abd8589a30246a098af8a49908 Mon Sep 17 00:00:00 2001 From: Alan Wizemann Date: Fri, 17 Apr 2026 17:10:51 -0700 Subject: [PATCH] fix: Chat tab false-positive "no credentials" warning before session pick MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The orange "No AI provider credentials detected" banner was firing on the Chat tab whenever no session was selected, even for users whose credentials were configured and working. The banner only disappeared when a session started — not because credentials were actually found, but because the banner's `!hasActiveProcess` gate flipped to false once ACP launched. Root cause: `HermesFileService.hasAnyAICredential()` inspected only the shell environment and `~/.hermes/.env`, while Hermes itself resolves credentials from two additional places Scarf had never learned about: - `~/.hermes/auth.json` — the Credential Pools file written by the Configure → Credential Pools UI (the blessed v1.6 flow) - `~/.hermes/config.yaml` — embedded `api_key:` under auxiliary. and delegation The preflight now checks all four locations. For auth.json we parse the JSON and look for any `credential_pool.[*].access_token` that is non-empty. For config.yaml we line-scan for `api_key:` leaves with a non-empty value, matching the defensive style of the existing .env scanner (no YAML parser needed in a nonisolated function). Also updated the banner subtitle to point users at Credential Pools before .env, since the former is the blessed in-app flow. Co-Authored-By: Claude Opus 4.6 --- .../Core/Services/HermesFileService.swift | 44 +++++++++++++++++-- .../scarf/Features/Chat/Views/ChatView.swift | 2 +- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/scarf/scarf/Core/Services/HermesFileService.swift b/scarf/scarf/Core/Services/HermesFileService.swift index d3fadb6..099cd16 100644 --- a/scarf/scarf/Core/Services/HermesFileService.swift +++ b/scarf/scarf/Core/Services/HermesFileService.swift @@ -1356,10 +1356,16 @@ struct HermesFileService: Sendable { return env } - /// True if any known AI-provider credential is reachable — either already - /// in the current process env, present in the login-shell env we queried, - /// or present in `~/.hermes/.env`. Used by Chat to warn the user before - /// `hermes acp` fails on send with "No Anthropic credentials found". + /// True if any known AI-provider credential is reachable. Hermes itself + /// resolves credentials from four locations at runtime, so the preflight + /// mirrors that set to avoid false "no credentials" warnings: + /// 1. Current process env + login-shell env (queried once at startup) + /// 2. `~/.hermes/.env` + /// 3. `~/.hermes/auth.json` — Credential Pools (v1.6+ blessed flow) + /// 4. `~/.hermes/config.yaml` — embedded `api_key:` for auxiliary / + /// delegation tasks + /// Used by Chat to warn the user before `hermes acp` fails on send with + /// "No Anthropic credentials found". nonisolated static func hasAnyAICredential() -> Bool { let credentialKeys = shellEnvKeys.filter { $0 != "PATH" && $0 != "ANTHROPIC_BASE_URL" && $0 != "OPENAI_BASE_URL" } let env = enrichedEnvironment() @@ -1386,6 +1392,36 @@ struct HermesFileService: Sendable { } } } + // Scan ~/.hermes/auth.json — the Credential Pools file written by the + // Configure → Credential Pools UI. Schema is + // { "credential_pool": { "": [ { "access_token": "...", ... }, ... ] } } + // Defensive parse: any malformed input falls through to the next check. + let authPath = HermesPaths.home + "/auth.json" + if let data = try? Data(contentsOf: URL(fileURLWithPath: authPath)), + let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let pool = root["credential_pool"] as? [String: Any] { + for (_, entries) in pool { + guard let list = entries as? [[String: Any]] else { continue } + for cred in list { + if let token = cred["access_token"] as? String, !token.isEmpty { + return true + } + } + } + } + // Scan ~/.hermes/config.yaml for `api_key:` lines with a non-empty + // value. Covers both `auxiliary..api_key` and `delegation.api_key` + // without needing to parse the YAML structure — any leaf `api_key: ...` + // with a value means Hermes has a credential to fall back on. + if let text = try? String(contentsOfFile: HermesPaths.configYAML, encoding: .utf8) { + for line in text.split(separator: "\n") { + let trimmed = line.trimmingCharacters(in: .whitespaces) + guard trimmed.hasPrefix("api_key:") else { continue } + let value = trimmed.dropFirst("api_key:".count) + .trimmingCharacters(in: CharacterSet(charactersIn: "\"' ")) + if !value.isEmpty { return true } + } + } return false } diff --git a/scarf/scarf/Features/Chat/Views/ChatView.swift b/scarf/scarf/Features/Chat/Views/ChatView.swift index edcb995..b97902d 100644 --- a/scarf/scarf/Features/Chat/Views/ChatView.swift +++ b/scarf/scarf/Features/Chat/Views/ChatView.swift @@ -96,7 +96,7 @@ struct ChatView: View { VStack(alignment: .leading, spacing: 2) { Text("No AI provider credentials detected") .font(.callout) - Text("Add `ANTHROPIC_API_KEY` (or similar) to `~/.hermes/.env` or your shell profile, then restart Scarf.") + Text("Add credentials in **Configure → Credential Pools**, set `ANTHROPIC_API_KEY` (or similar) in `~/.hermes/.env`, or export it in your shell profile, then restart Scarf.") .font(.caption) .foregroundStyle(.secondary) }