Skip to content

feat(settings): API key management tab (#555)#588

Merged
frankbria merged 4 commits intomainfrom
issue-555-api-keys
May 8, 2026
Merged

feat(settings): API key management tab (#555)#588
frankbria merged 4 commits intomainfrom
issue-555-api-keys

Conversation

@frankbria
Copy link
Copy Markdown
Owner

@frankbria frankbria commented May 8, 2026

Summary

Adds the API Keys tab to /settings so users can store, verify, and remove the three core credentials (Anthropic, OpenAI, GitHub PAT) without touching environment variables. Builds on the settings page skeleton from #587.

Closes #555.

Backend (codeframe/ui/routers/settings_v2.py)

  • GET /api/v2/settings/keys — status for the 3 providers (never returns plaintext)
  • PUT /api/v2/settings/keys/{provider} — store with format validation
  • DELETE /api/v2/settings/keys/{provider} — idempotent removal
  • POST /api/v2/settings/verify-key — live check (Anthropic models.list / OpenAI models.list / GitHub /user)

Storage delegates to the existing CredentialManager (keyring with encrypted-file fallback). Env vars continue to take precedence at runtime; the status endpoint reports the source (environment / stored / none) so the UI can render env-loaded keys read-only.

A new get_credential_manager FastAPI dependency lets tests inject an isolated, file-backed store so they don't touch the developer's real keyring.

Frontend

  • ApiKeysTab + KeySlot components with: masked input, last-4 display, Verify / Save / Remove flows, source badge, env-var read-only mode
  • settingsApi extended with getKeys / storeKey / removeKey / verifyKey (machine-wide — no workspace_path)
  • /settings now renders its tab shell even without a workspace so the API Keys flow works for first-time users hitting the env-var dead end. The Agent tab still requires a workspace and shows the existing prompt.

Test plan

  • uv run pytest tests/ui/test_settings_v2.py — 29 passed (9 existing + 20 new)
  • uv run pytest tests/ui/ tests/core/test_credentials.py — 227 passed
  • uv run ruff check . — all checks passed
  • npm test — 799 passed (includes 9 new ApiKeysTab tests + 7 unchanged SettingsPage tests)
  • npm run build/settings static route emitted
  • Manual demo: visit /settings → API Keys tab → store/verify/remove a real key (covered by demo verification phase)

Acceptance criteria (from issue)

  • Can store and verify each key from the UI
  • Keys are never returned in plaintext by any API response (covered by test_status_never_returns_plaintext, test_store_response_excludes_plaintext)
  • Removing a key clears it from storage (test_delete_removes_credential)
  • npm test and npm run build pass

Out of scope (per issue)

OAuth flows, key rotation/expiry, sidebar hasWorkspace gating refactor.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an API Keys section in Settings to securely store and manage Anthropic, OpenAI, and GitHub credentials.
    • Live-verify keys (using provided or stored/env values) and surface verification results.
    • Show per-provider key status/source (stored, environment, or not set) with masked last-four hint only.
  • Tests

    • Added comprehensive UI and API tests covering listing, storing, deleting, and verifying keys.
  • Documentation

    • Minor roadmap update reflecting settings progress.

Adds the API Keys tab to /settings so users can store, verify, and remove
the three core credentials without touching environment variables. Builds
on the settings page skeleton from #587.

Backend (codeframe/ui/routers/settings_v2.py)
- GET    /api/v2/settings/keys             — status for the 3 providers (no plaintext)
- PUT    /api/v2/settings/keys/{provider}  — store with format validation
- DELETE /api/v2/settings/keys/{provider}  — idempotent removal
- POST   /api/v2/settings/verify-key       — live check (Anthropic / OpenAI / GitHub)

Storage delegates to existing CredentialManager (keyring with encrypted-file
fallback). Env vars continue to take precedence at runtime; the status
endpoint reports the source so the UI can surface that.

Frontend
- ApiKeysTab + KeySlot components with masked input, last-4 display,
  Verify / Save / Remove flows, source badge, env-var read-only mode
- settingsApi extended with getKeys / storeKey / removeKey / verifyKey
- /settings page renders tabs even without a workspace so the API Keys
  flow works for first-time users hitting the env-var dead end

Tests
- 20 new pytest cases covering store/status/delete/verify, plaintext-leak
  guards, env-precedence, format validation, unknown-provider rejection
- 9 new jest tests for ApiKeysTab covering render, save/verify/remove,
  invalid verification, env-source disabling

Closes #555

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 8, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 107e323c-7ed6-45d6-a027-6f2d3fb8d879

📥 Commits

Reviewing files that changed from the base of the PR and between 165eb58 and 613c6b8.

📒 Files selected for processing (4)
  • codeframe/ui/routers/settings_v2.py
  • tests/ui/test_settings_v2.py
  • web-ui/src/components/settings/ApiKeysTab.tsx
  • web-ui/src/components/settings/KeySlot.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • web-ui/src/components/settings/ApiKeysTab.tsx
  • web-ui/src/components/settings/KeySlot.tsx
  • tests/ui/test_settings_v2.py
  • codeframe/ui/routers/settings_v2.py

Walkthrough

Adds machine-wide API key management: backend endpoints and verification helpers, Pydantic/TS types, frontend client + ApiKeysTab/KeySlot UI, page integration, comprehensive backend/frontend tests, and a small roadmap text update.

Changes

API Key Management

Layer / File(s) Summary
Data Contracts & Types
codeframe/ui/models.py, web-ui/src/types/index.ts
New KeyProvider and KeySource types; StoreKeyRequest, KeyStatusResponse, VerifyKeyRequest, and VerifyKeyResponse models for safe key storage/status/verification.
Backend Router & Dependency
codeframe/ui/routers/settings_v2.py
Added imports and get_credential_manager() dependency used by key endpoints.
Backend API Endpoints
codeframe/ui/routers/settings_v2.py
GET /api/v2/settings/keys (list statuses), PUT /api/v2/settings/keys/{provider} (store), DELETE /api/v2/settings/keys/{provider} (idempotent delete).
Backend Verification
codeframe/ui/routers/settings_v2.py
Provider-specific checkers (threadpool-wrapped Anthropic/OpenAI sync calls and async GitHub verifier) and POST /api/v2/settings/verify-key.
Backend Tests
tests/ui/test_settings_v2.py
Temp-backed credential fixtures and tests for list/store/delete/verify flows, provider mocking, env-var detection, and plaintext-redaction assertions.
Frontend API Client
web-ui/src/lib/api.ts
New settingsApi methods: getKeys(), storeKey(provider,value), removeKey(provider), verifyKey(provider,value?) with key-related TS types.
Frontend UI Components
web-ui/src/components/settings/ApiKeysTab.tsx, web-ui/src/components/settings/KeySlot.tsx
ApiKeysTab fetches statuses and renders slots; KeySlot masks input, supports Save/Verify/Remove flows, shows source badge/last-four, and disables editing for env-sourced keys.
Frontend Integration
web-ui/src/app/settings/page.tsx
Imports and renders ApiKeysTab in API Keys tab and refactors NoWorkspaceMessage handling.
Frontend Component Tests
web-ui/src/__tests__/components/settings/ApiKeysTab.test.tsx
Jest + RTL tests for loading/error/success, secure rendering (no plaintext), save/verify/remove flows, env-key UI restrictions, and SWR mutate usage.
Docs
docs/PRODUCT_ROADMAP.md
Minor Phase 5.1 roadmap text update reflecting completed task numbers.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant UI
  participant settingsApi
  participant Server
  participant CredentialManager
  participant ProviderAPI
  User->>UI: open Settings > API Keys
  UI->>settingsApi: getKeys() / storeKey / verifyKey / removeKey
  settingsApi->>Server: HTTP request to /api/v2/settings/*
  Server->>CredentialManager: read/store/delete credentials
  Server->>ProviderAPI: verification call (async/sync)
  ProviderAPI-->>Server: verification result
  Server-->>settingsApi: response (no plaintext)
  settingsApi-->>UI: result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hide keys safe beneath my fur,
No plaintext peeks — that would be a blur,
Anthropic, OpenAI, GitHub in slots,
Verify, store, delete — hop, skip, and trot,
A tidy settings tab, secured by a whisker.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(settings): API key management tab (#555)' directly summarizes the main change: adding an API key management tab to the settings page. It is concise, clear, and specific enough for a teammate to understand the primary change.
Linked Issues check ✅ Passed The pull request successfully implements all coding requirements from issue #555: API key storage/retrieval endpoints [#555], key verification endpoints [#555], password-masked UI inputs [#555], last-four display [#555], remove/delete functionality [#555], encrypted server-side storage via CredentialManager [#555], and plaintext protection [#555].
Out of Scope Changes check ✅ Passed The PR changes are well-scoped to issue #555 requirements. The roadmap update (docs/PRODUCT_ROADMAP.md) is directly related to marking #555 as complete. No unrelated features or refactoring changes were introduced.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-555-api-keys

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link
Copy Markdown

claude Bot commented May 8, 2026

PR #588 — API Key Management Tab

Good implementation overall. The security-first design (no plaintext in responses, env-var precedence, format validation before storage) is exactly right. The test coverage is solid across both layers. A few things worth addressing:


Security / Correctness

_check_github_token exposes raw token in error message (settings_v2.py, _check_github_token):

except Exception as e:
    return False, f"GitHub verification failed: {e}"

If aiohttp raises a connection error that includes the URL or request details, the token string could leak into the message that gets returned to the client in VerifyKeyResponse.message. The error message from network exceptions is unpredictable — safer to return a generic string and log the detail server-side.

_verify_anthropic_sync / _verify_openai_sync same risk — the raw Exception message is passed back:

return False, f"Anthropic verification failed: {e}"

SDK auth errors are usually safe, but the pattern is worth making explicit. Log e and return a canned message.

Verify endpoint rate limit@rate_limit_standard() is applied here, but verify-key makes live outbound API calls. The standard rate limit may not be restrictive enough; an attacker can use it to enumerate key validity or burn quota on the user's real keys. Consider @rate_limit_auth() or a separate tighter limit for this endpoint.


Architecture

Direct anthropic/openai imports in the router layer — Per CLAUDE.md rule 1, codeframe/ui/routers/ should delegate to codeframe/core/ or codeframe/adapters/. The _verify_*_sync helpers create SDK clients directly in the router. This is a thin enough wrapper that it's borderline acceptable, but it breaks the layering rule and means verification logic is in two different places if adapters ever grow. Worth flagging even if the decision is to keep it as-is for now.

aiohttp lazy importimport aiohttp inside _check_github_token is a fragile runtime dependency. If aiohttp isn't installed, the error only surfaces during verification (not on startup). It should be a declared dependency in pyproject.toml, or the import should be at the top of the file with a clear ImportError message. A httpx call could replace it with no new dep since FastAPI already depends on it.


Minor Issues

_last_four with short valuesvalue[-4:] on a string shorter than 4 chars returns the full string. Format validation prevents this in practice, but the function has no guard. Low risk, easy fix.

delete_key string-based "not found" detection is fragile:

if "no such" in msg or "not found" in msg:
    return Response(status_code=204)

If CredentialManager.delete_credential can raise a typed KeyNotFoundError (or just swallow absence internally), that's cleaner than substring-matching error messages.

store_key uses # type: ignore[arg-type] — the comment explains why it's safe, but the underlying issue is that provider is str from the path parameter while _build_status expects KeyProvider. Using cast(KeyProvider, provider) (or re-resolving via _PROVIDER_MAP) would be cleaner than the ignore.


Frontend

The KeySlot and ApiKeysTab components are well-structured. A few small observations:

  • aria-label on the Input reads "${displayName} API key", but the tests query by /Anthropic API Key/i which matches displayName (Anthropic API Key) + " API key" — so the label ends up as "Anthropic API Key API key". Consider using aria-label={displayName} directly.
  • handleRemove clears value after removal, which is good. But there's no confirmation dialog before a destructive remove — for API keys a window.confirm or a secondary "Are you sure?" button state might avoid accidental deletions, especially since keyring removal can't be undone from the UI.
  • verifyResult state is cleared on every action (setVerifyResult(null)) but not on provider-level re-mount. This is fine for the current usage since KeySlot is always mounted.

Test Coverage

The 20 new Python tests and 9 frontend tests are comprehensive for the happy path and most error branches. One gap: no test for the _check_github_token path where resp.status is something other than 200/401 (the third branch: return False, f"GitHub API returned status {resp.status}"). Low priority.


Summary: The security fundamentals are solid. The main items worth a follow-up are (1) tightening the verify rate limit, (2) sanitizing error messages in verification helpers to avoid token leakage, and (3) either moving verification logic to the adapter layer or adding aiohttp/httpx as an explicit dependency.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
codeframe/ui/routers/settings_v2.py (1)

146-355: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Move machine-scope key behavior out of the UI router.

This block makes codeframe/ui own provider resolution, status shaping, live verification, and the first workspace-less routes in this router. Please push that behavior into a core credential service and keep this module as a thin HTTP adapter, or split these machine-scope routes into a module with an explicit non-workspace contract.

As per coding guidelines, "codeframe/ui/**/*.py: FastAPI server and web UI are thin adapters over core — do NOT implement domain logic in codeframe/ui/ routers" and "codeframe/ui/routers/**/*.py: Web UI API client must require workspace_path query parameter for all endpoints"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@codeframe/ui/routers/settings_v2.py` around lines 146 - 355, The router
currently implements domain logic (provider mapping, key status shaping, live
verification) in functions like _PROVIDER_MAP/_resolve_provider, _build_status,
_check_github_token, _verify_anthropic_sync, _verify_openai_sync and the
verify_key/store_key/delete_key endpoints; move that logic into a core
credential service (eg. CredentialService) in the core package that exposes
methods such as resolve_provider(provider: str) -> CredentialProvider,
get_key_status(provider: KeyProvider, manager: CredentialManager) ->
KeyStatusResponse, verify_key(provider: CredentialProvider, key: str) ->
(bool,str), store_credential(cp, value), delete_credential(cp) and use
CredentialManager only as a persistence dependency inside the service; then
update this UI router so store_key/delete_key/list_key_status/verify_key simply
call the new service methods and return results (or split these machine-scope
routes into a dedicated non-workspace module with an explicit non-workspace
contract if you prefer), removing provider mapping and verification helpers from
codeframe/ui and keeping the router a thin HTTP adapter.
🧹 Nitpick comments (1)
tests/ui/test_settings_v2.py (1)

461-478: 💤 Low value

Consider a clearer pattern for raising exceptions in mocks.

The generator throw pattern (_ for _ in ()).throw(Exception(...)) on line 466 works but is obscure. A straightforward helper function would improve readability.

♻️ Suggested improvement
         class FakeAnthropicClient:
             def __init__(self, api_key):
+                def raise_auth_error():
+                    raise Exception("401 unauthorized")
                 self.models = type(
                     "M",
                     (),
-                    {"list": lambda self_: (_ for _ in ()).throw(Exception("401 unauthorized"))},
+                    {"list": lambda self_: raise_auth_error()},
                 )()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ui/test_settings_v2.py` around lines 461 - 478, The mock in
FakeAnthropicClient uses an obscure generator throw to raise an exception;
replace it with a clear helper that raises the error and assign that to the
models.list attribute: inside FakeAnthropicClient.__init__ create a small
function (e.g., raise_unauthorized) that raises Exception("401 unauthorized")
and set self.models = type("M", (), {"list": raise_unauthorized})(), then keep
the monkeypatch on settings_v2._AnthropicClient and the keys_client.post
assertions unchanged so the test behavior is identical but readable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@codeframe/ui/routers/settings_v2.py`:
- Around line 307-313: The _verify_anthropic_sync function currently calls the
non-existent client.models.list(); update it to call client.messages.create(...)
(e.g., send a minimal test message like role="system", content="ping") to
validate the API key and catch auth errors instead. Replace the models.list()
call with a call to client.messages.create and preserve the same try/except
behavior so failures return False with the error message and success returns
True with "Anthropic key accepted".

In `@web-ui/src/__tests__/components/settings/ApiKeysTab.test.tsx`:
- Around line 213-214: The test currently checks
slot.querySelector('button[aria-label]') which always passes because the KeySlot
buttons lack aria-labels; update the assertion to verify that no visible
"Remove" button is rendered for environment-source keys (e.g., assert that there
is no button whose accessible name or innerText matches "Remove" /
case-insensitive) so the env-source restriction is actually tested; locate the
check in the ApiKeysTab.test.tsx around the KeySlot usage and replace the
selector-based assertion with one that queries by button text/accessible name
"Remove".

---

Outside diff comments:
In `@codeframe/ui/routers/settings_v2.py`:
- Around line 146-355: The router currently implements domain logic (provider
mapping, key status shaping, live verification) in functions like
_PROVIDER_MAP/_resolve_provider, _build_status, _check_github_token,
_verify_anthropic_sync, _verify_openai_sync and the
verify_key/store_key/delete_key endpoints; move that logic into a core
credential service (eg. CredentialService) in the core package that exposes
methods such as resolve_provider(provider: str) -> CredentialProvider,
get_key_status(provider: KeyProvider, manager: CredentialManager) ->
KeyStatusResponse, verify_key(provider: CredentialProvider, key: str) ->
(bool,str), store_credential(cp, value), delete_credential(cp) and use
CredentialManager only as a persistence dependency inside the service; then
update this UI router so store_key/delete_key/list_key_status/verify_key simply
call the new service methods and return results (or split these machine-scope
routes into a dedicated non-workspace module with an explicit non-workspace
contract if you prefer), removing provider mapping and verification helpers from
codeframe/ui and keeping the router a thin HTTP adapter.

---

Nitpick comments:
In `@tests/ui/test_settings_v2.py`:
- Around line 461-478: The mock in FakeAnthropicClient uses an obscure generator
throw to raise an exception; replace it with a clear helper that raises the
error and assign that to the models.list attribute: inside
FakeAnthropicClient.__init__ create a small function (e.g., raise_unauthorized)
that raises Exception("401 unauthorized") and set self.models = type("M", (),
{"list": raise_unauthorized})(), then keep the monkeypatch on
settings_v2._AnthropicClient and the keys_client.post assertions unchanged so
the test behavior is identical but readable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 84e1ad79-ab12-429a-8f78-ee4a4015b672

📥 Commits

Reviewing files that changed from the base of the PR and between 66794ac and 80fa972.

📒 Files selected for processing (9)
  • codeframe/ui/models.py
  • codeframe/ui/routers/settings_v2.py
  • tests/ui/test_settings_v2.py
  • web-ui/src/__tests__/components/settings/ApiKeysTab.test.tsx
  • web-ui/src/app/settings/page.tsx
  • web-ui/src/components/settings/ApiKeysTab.tsx
  • web-ui/src/components/settings/KeySlot.tsx
  • web-ui/src/lib/api.ts
  • web-ui/src/types/index.ts

Comment thread codeframe/ui/routers/settings_v2.py Outdated
Comment thread web-ui/src/__tests__/components/settings/ApiKeysTab.test.tsx Outdated
Test User and others added 2 commits May 8, 2026 11:45
1. Anthropic verification: switch from client.models.list() to
   client.messages.create() with max_tokens=1. The codebase pins
   anthropic>=0.18.0 and the .models attribute was added later, so
   models.list() was unreliable. messages.create() has been the
   stable surface since v0.18 and AuthenticationError lets us
   distinguish a bad key from a transient/network failure.

2. Test assertion fix: the env-source remove-button assertion in
   ApiKeysTab.test.tsx was vacuous (it queried for buttons with
   aria-label, but the component renders none). Replaced with a
   text-based search for a button labelled Remove.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented May 8, 2026

PR #588 Review — Follow-up

My previous comment was truncated mid-sentence. Picking up and completing it here.


Security / Correctness

_check_github_token may expose token in error message (settings_v2.py):

except Exception as e:
    return False, f"GitHub verification failed: {e}"

If the underlying aiohttp exception includes the request URL or headers (which can contain the Bearer token), the raw token could appear in the response message field. Fix with a safe fallback:

except Exception:
    return False, "GitHub verification failed: network or server error"

_verify_openai_sync has no specific auth-error handling — unlike the Anthropic path which catches _AnthropicAuthError explicitly, the OpenAI path uses a bare except Exception. This means a network timeout and an invalid-key rejection both return the same generic message, making it hard for users to distinguish connection problems from bad credentials. Import and catch openai.AuthenticationError separately.


Code Quality

type: ignore on _build_status call in store_key:

return _build_status(provider, manager)  # type: ignore[arg-type]

The provider arg is str but _build_status expects KeyProvider. Runtime is safe because _resolve_provider already validated it, but an explicit cast(KeyProvider, provider) from typing would make the intent clearer without silencing the checker.

Redundant aria-label in KeySlot.tsx:

aria-label={`${displayName} API key`}

displayName for Anthropic is "Anthropic API Key", making the full label "Anthropic API Key API key". Either drop " API key" from the template or strip it from displayName.

Unnecessary refresh wrapper in ApiKeysTab.tsx:

const refresh = async () => {
  await mutate();
};

This is just mutate — pass it directly as onChanged={mutate}.

Hardcoded model ID in _verify_anthropic_sync:

The code uses "claude-haiku-4-5" but per the project conventions (CLAUDE.md), the full model ID is "claude-haiku-4-5-20251001". Using the short form may work as an alias today but is less explicit.


Testing

The fixture bypasses CredentialManager.__init__ via CredentialManager.__new__:

manager = CredentialManager.__new__(CredentialManager)
manager._store = store

This is fragile — if __init__ gains new required attributes, the fixture will fail silently or with cryptic errors. Worth a note to keep this in sync if CredentialManager changes, or to refactor the class to accept a storage_dir parameter in its constructor.

from codeframe.core.credentials import CredentialProvider is repeated inside several test methods. Moving it to module level is cleaner.


Summary

The implementation is solid and the security fundamentals (no plaintext in responses, env-var precedence, format validation, idempotent delete) are all correct. The two items above worth fixing before merge are:

  1. Sanitize exception messages in _check_github_token (token may leak)
  2. Add specific auth-error handling in _verify_openai_sync

The remaining points are style/quality improvements that can be addressed here or as a follow-up.

@claude
Copy link
Copy Markdown

claude Bot commented May 8, 2026

PR #588 - Follow-up Review

Referencing my two earlier comments. The code has not changed since those were posted, so this is a status summary of what still needs attention before merge.

Still Outstanding (critical)

1. Token leakage in _check_github_token (settings_v2.py)

The bare except Exception as e path returns the raw exception message string to the client. aiohttp exceptions can include request URLs or auth headers, meaning the GitHub token could leak into VerifyKeyResponse.message. This was flagged in both prior reviews.

Minimal fix: catch Exception without binding e, log via logger.exception(), and return a static string.

2. No specific auth-error handling in _verify_openai_sync (settings_v2.py)

Anthropic catches _AnthropicAuthError to distinguish a rejected key from a network error. The OpenAI path uses bare except Exception, so a timeout and a 401 produce the same generic message. Import and catch openai.AuthenticationError separately before the fallback.

Still Outstanding (style/quality, lower priority)

Covered in my second comment; can be addressed as a fast follow-up after merge:

  • Use cast(KeyProvider, provider) instead of the type: ignore comment in store_key
  • aria-label on the Input reads "Anthropic API Key API key" - remove the redundant suffix
  • The refresh wrapper in ApiKeysTab.tsx is just mutate; pass it directly as onChanged={mutate}
  • Add a comment to the CredentialManager.__new__ fixture warning that it bypasses __init__ and must stay in sync if new attributes are added
  • Move the repeated CredentialProvider import to module scope in the test file

Verdict

The two security items (token leakage, OpenAI auth-error discrimination) should be fixed before merge. Everything else is non-blocking. The overall design is solid: no plaintext in responses, env-var precedence, format validation, idempotent delete, isolated test fixtures.

Critical (security):
- _check_github_token, _verify_anthropic_sync, _verify_openai_sync no
  longer echo raw exception detail to the client. aiohttp errors can
  carry the request URL and headers (which include the bearer token);
  the detail is now logged server-side and a generic message returned.
- _verify_openai_sync gained a specific openai.AuthenticationError
  branch so users see "key rejected" vs "network error".
- POST /verify-key uses rate_limit_ai (20/min) instead of standard
  (100/min) — the endpoint makes outbound API calls and can be used
  to enumerate key validity or burn quota.

Quality:
- _last_four guards against values shorter than 4 chars.
- store_key uses cast(KeyProvider, provider) instead of type:ignore.
- Anthropic verification uses the full model id "claude-haiku-4-5-20251001".
- KeySlot aria-label drops the redundant " API key" suffix.
- ApiKeysTab passes mutate inline instead of wrapping it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented May 8, 2026

PR #588 — Follow-up Review

Building on my earlier review: several items from the first pass have been addressed in the latest commits. Noting what is resolved and what is still open.

Resolved from prior review

  • Error message sanitization — all three verification helpers now return canned messages and log the detail server-side. No raw exception strings reach the client.
  • Verify rate limit/verify-key now correctly uses @rate_limit_ai() instead of @rate_limit_standard().
  • store_key type safetycast(KeyProvider, provider) is used cleanly instead of a # type: ignore.
  • Anthropic verification method — switched to messages.create() with max_tokens=1, which is the right call (models list is not a stable surface across SDK versions).
  • Frontend aria-labelaria-label={displayName} is used directly; no double-label issue.
  • Auth error mock — uses AuthenticationError.__new__ pattern instead of the obscure generator-throw form.

Still open

aiohttp lazy import / undeclared dependency (settings_v2.py, _check_github_token): the import aiohttp lives inside the function body. If aiohttp is not installed, the error only surfaces at call time, not on startup. FastAPI already pulls in httpx (via Starlette), which supports async GET with no new dependency. At minimum, add aiohttp to pyproject.toml so the dep is declared.

delete_key fragile string matching: checking "no such" in msg or "not found" in msg against the exception message is brittle across keyring backends. If CredentialManager.delete_credential can raise a typed exception for absence (or silently no-op), that is the cleaner contract. Worth checking whether CredentialStore can absorb this so the router does not need to guess.

Architecture concern (also flagged by CodeRabbit): _verify_anthropic_sync, _verify_openai_sync, and _check_github_token instantiate SDK clients directly in the UI router. CLAUDE.md rule 1 says routers should delegate to codeframe/adapters/ or codeframe/core/. The adapters layer already wraps these clients. Acceptable to defer, but should be tracked as a follow-up issue rather than left implicit.

Minor remaining gap

No test covers _check_github_token when GitHub returns a status other than 200 or 401 (the "GitHub API returned status {resp.status}" branch). Low priority but easy to add.

Overall

The security-critical items from the first review are addressed. The three issues above are real but none is a blocker — the aiohttp dep is the most operationally risky. If it is already a transitive dep in the install, it is fine; if not, switching to httpx is a one-line change worth doing before merge.

@frankbria frankbria merged commit c4023b4 into main May 8, 2026
11 checks passed
@frankbria frankbria deleted the issue-555-api-keys branch May 8, 2026 18:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Phase 5.1] Settings page: API key management

1 participant