Skip to content

Making API keys completely write only for the frontend#608

Merged
Wirasm merged 1 commit intomainfrom
bugfix-unencrypted-credential-get
Sep 10, 2025
Merged

Making API keys completely write only for the frontend#608
Wirasm merged 1 commit intomainfrom
bugfix-unencrypted-credential-get

Conversation

@coleam00
Copy link
Copy Markdown
Owner

@coleam00 coleam00 commented Sep 6, 2025

Pull Request

Summary

Making API keys completely write only for the frontend. Before this PR, there was a GET request to the credentials/ endpoint that was returning API keys in plain text over HTTP.

Changes Made

  • The credentials API no longer returns decrypted API keys
  • The frontend decrypt option is disabled for all encrypted values unless you are setting it for the first time (so the key is in the frontend state, nothing is ever sent in plain text over HTTP anymore)

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Performance improvement
  • Code refactoring

Affected Services

  • Frontend (React UI)
  • Server (FastAPI backend)
  • MCP Server (Model Context Protocol)
  • Agents (PydanticAI service)
  • Database (migrations/schema)
  • Docker/Infrastructure
  • Documentation site

Testing

  • All existing tests pass
  • Added new tests for new functionality
  • Manually tested affected user flows
  • Docker builds succeed for all services

Test Evidence

Tested by resetting my DB, setting the key for OpenAI and making sure crawls still work. Did the same thing on an existing Archon setup as well to make sure this doesn't affect anyone with Archon already up and running.

Checklist

  • My code follows the service architecture patterns
  • If using an AI coding assistant, I used the CLAUDE.md rules
  • I have added tests that prove my fix/feature works
  • All new and existing tests pass locally
  • My changes generate no new warnings
  • I have updated relevant documentation
  • I have verified no regressions in existing features

Summary by CodeRabbit

  • New Features
    • Encrypted credentials are now masked as “[ENCRYPTED]” and cannot be viewed directly.
    • Masked credentials show contextual tooltips; Show Value and Encrypt/Decrypt controls are disabled with guidance.
    • Editing a masked credential converts it to a user-managed value for replacement.
  • Changes
    • Credential listings and detail views consistently hide encrypted values.
    • API responses no longer return encrypted ciphertext or allow on-demand decryption.
  • Documentation
    • Updated in-app security notice to explain the new masking and editing workflow.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Sep 6, 2025

Walkthrough

Encrypted credentials are no longer exposed to the UI or clients. Backend and UI now return masked values "[ENCRYPTED]" for encrypted items, remove decrypt flows, and add UI handling to treat server-provided credentials as immutable until edited. A new isFromBackend flag distinguishes backend-provided vs. user-created credentials.

Changes

Cohort / File(s) Summary of changes
UI component behavior and flags
archon-ui-main/src/components/settings/APIKeysSection.tsx
Added CustomCredential.isFromBackend?: boolean. On load, marks non-new creds as from backend. New rows set isFromBackend=false. Backend-encrypted values render as masked (“[ENCRYPTED]”): value/visibility/encrypt toggles disabled with tooltips/toasts. Editing masked value clears it, hides it, and flips isFromBackend=false. Updated security notice.
Frontend credential mapping
archon-ui-main/src/services/credentialsService.ts
getCredentialsByCategory: For encrypted items, map to value:"[ENCRYPTED]" and omit encrypted_value. Non-encrypted path unchanged.
Server API endpoint (GET credential)
python/src/server/api_routes/settings_api.py
Removed decrypt parameter from get_credential signature and logic. Always returns masked value "[ENCRYPTED]" with is_encrypted=True for encrypted creds; returns plain value for non-encrypted. Honors OPTIONAL_SETTINGS_WITH_DEFAULTS when not found; otherwise 404. Simplified logging.
Server services (listing/mapping)
python/src/server/services/credential_service.py
get_credentials_by_category and list_all_credentials: For encrypted items, return value:"[ENCRYPTED]", omit encrypted_value, keep is_encrypted/category/description. Removed decryption in listing paths.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant UI as APIKeysSection (UI)
  participant FE as credentialsService (FE)
  participant API as settings_api.get_credential / lists
  participant SVC as credential_service

  User->>UI: Open API Keys
  UI->>FE: fetch credentials by category
  FE->>API: GET credentials (list)
  API->>SVC: list_all_credentials / get_credentials_by_category
  SVC-->>API: Items (encrypted -> value="[ENCRYPTED]", is_encrypted=true)
  API-->>FE: Masked credentials
  FE-->>UI: Normalized items (value="[ENCRYPTED]")
  UI-->>User: Render masked rows (actions disabled)

  note over UI,User: Editing masked value
  User->>UI: Click value field on masked row
  UI->>UI: Clear value, hide value, set isFromBackend=false
  UI-->>User: Row becomes editable like user-owned
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

In burrows of bytes where secrets sleep,
I thump my paws and promise to keep—
The locks stay shut, the carrots stay veiled,
“[ENCRYPTED],” I squeak, security hailed.
Tap to edit, nibble to own,
From backend roots to fields you’ve sown—
Safe hops only, down this zone.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bugfix-unencrypted-credential-get

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@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: 1

Caution

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

⚠️ Outside diff range comments (2)
archon-ui-main/src/services/credentialsService.ts (1)

80-86: Add retry with exponential backoff for backend calls.

Per guidelines, wrap fetches with retries and clear errors. Apply here and across this service.

Example utility:

async function fetchWithRetry(input: RequestInfo, init?: RequestInit, attempts = 3, baseDelayMs = 300) {
  let lastErr: unknown;
  for (let i = 0; i < attempts; i++) {
    try {
      const res = await fetch(input, init);
      if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
      return res;
    } catch (e) {
      lastErr = e;
      if (i < attempts - 1) await new Promise(r => setTimeout(r, baseDelayMs * 2 ** i));
    }
  }
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
}

Usage example here:

-    const response = await fetch(`${this.baseUrl}/api/credentials`);
+    const response = await fetchWithRetry(`${this.baseUrl}/api/credentials`);
python/src/server/services/credential_service.py (1)

448-457: Use uppercase key for LLM_PROVIDER in set_active_provider
Change the literal in set_active_provider from "llm_provider" to "LLM_PROVIDER" so it matches the getters in lines 395 and 418, as well as the provider_credentials listing.
Location: python/src/server/services/credential_service.py:453

🧹 Nitpick comments (7)
archon-ui-main/src/services/credentialsService.ts (1)

103-110: Centralize the “[ENCRYPTED]” sentinel to avoid drift.

Hardcoding the sentinel increases the risk of FE/BE mismatch. Use a shared constant and reuse it here.

Add a shared constant (e.g., archon-ui-main/src/constants/security.ts):

export const ENCRYPTED_PLACEHOLDER = "[ENCRYPTED]";

Then here:

-              value: "[ENCRYPTED]",
+              value: ENCRYPTED_PLACEHOLDER,
python/src/server/services/credential_service.py (1)

306-310: LGTM: encrypted values are masked.

This prevents plaintext exposure. Consider a module-level constant to keep FE/BE in sync.

Example:

ENCRYPTED_PLACEHOLDER = "[ENCRYPTED]"
# ...
credentials[key] = {
    "value": ENCRYPTED_PLACEHOLDER,
    "is_encrypted": True,
    "description": item["description"],
}
python/src/server/api_routes/settings_api.py (1)

166-177: Consider including metadata for non-encrypted values as well (consistency).

Return category/description for both cases to keep the shape uniform. Optional, but helps clients.

Example:

-# For non-encrypted credentials, return the actual value
-return {"key": key, "value": value, "is_encrypted": False}
+# For non-encrypted credentials, include metadata if available
+return {
+    "key": key,
+    "value": value,
+    "is_encrypted": False,
+    "category": value.get("category") if isinstance(value, dict) else None,
+    "description": value.get("description") if isinstance(value, dict) else None,
+}
archon-ui-main/src/components/settings/APIKeysSection.tsx (4)

55-71: Remove the unused isEncryptedFromBackend local.

Dead code trips linters and confuses readers.

-      const uiCredentials = apiKeys.map(cred => {
-        const isEncryptedFromBackend = cred.is_encrypted && cred.value === '[ENCRYPTED]';
-        
-        return {
+      const uiCredentials = apiKeys.map(cred => {
         return {

315-319: Clarify tooltip wording to reflect “on save” semantics (not live decrypt).

Avoid implying decryption will reveal the value.

-                          : cred.is_encrypted ? 'Encrypted - click to decrypt' : 'Not encrypted - click to encrypt'
+                          : cred.is_encrypted
+                            ? 'Stored encrypted — click to store as plaintext on save'
+                            : 'Stored plaintext — click to store encrypted on save'

268-276: Consider a shared ENCRYPTED_PLACEHOLDER constant.

You compare against '[ENCRYPTED]' in multiple places; use a single constant to avoid drift and ease future changes.

Add near top:

const ENCRYPTED_PLACEHOLDER = '[ENCRYPTED]';

Then replace string literals in comparisons and placeholders.

Also applies to: 289-293, 305-319


23-26: Align state names with guidelines (optional).

Rename to isLoading/isSaving for consistency with “is[Action]ing”.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 012d2c5 and 1776ccd.

📒 Files selected for processing (4)
  • archon-ui-main/src/components/settings/APIKeysSection.tsx (7 hunks)
  • archon-ui-main/src/services/credentialsService.ts (1 hunks)
  • python/src/server/api_routes/settings_api.py (2 hunks)
  • python/src/server/services/credential_service.py (2 hunks)
🧰 Additional context used
📓 Path-based instructions (9)
python/src/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

python/src/**/*.py: Fail fast on service startup failures, missing configuration, database connection failures, authentication/authorization failures, critical dependencies unavailable, and invalid/corrupting data
Never accept or store corrupted data; on operation failure skip the item entirely rather than writing placeholders (e.g., zero embeddings)
For batch/background operations, continue processing but log each failure with details; track both successes and failures
Use specific exception types (avoid bare Exception), include context/IDs/URLs in messages, preserve full stack traces with logging (exc_info=True), and never return None/null to indicate failure—raise with details
Use database task status values directly: todo, doing, review, done
Target Python 3.12 style with 120-character line length; use Ruff for linting and Mypy for type checking

python/src/**/*.py: Target Python 3.12 and keep lines within 120 characters
Use Ruff for linting (errors, warnings, unused imports, code style) and keep code Ruff-clean
Use Mypy for type checking; maintain type-safe code across backend

Files:

  • python/src/server/api_routes/settings_api.py
  • python/src/server/services/credential_service.py
python/src/server/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Use FastAPI exception handlers to return rich error responses with appropriate HTTP status codes and typed error payloads

Files:

  • python/src/server/api_routes/settings_api.py
  • python/src/server/services/credential_service.py
python/src/{server,agents,mcp}/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

python/src/{server,agents,mcp}/**/*.py: Fail fast on service startup failures (credentials, DB, or service init) with clear errors
Treat missing configuration (env vars/invalid settings) as fatal and stop execution
Do not suppress database connection failures; bubble up immediately
Authentication/authorization failures must halt the operation and be clearly surfaced
Data corruption or validation errors should raise (use Pydantic to enforce), never silently accept
If a critical dependency is unavailable, fail immediately
Never persist invalid data that would corrupt state (e.g., zero embeddings, null FKs, malformed JSON)
Batch processing should continue, logging detailed errors per item; always return both successes and failures
Background tasks (e.g., embedding generation) should finish queues and log failures without crashing the whole process
Include context and IDs/URLs in error messages; preserve full stack traces with logging (exc_info=True)
Use specific exception types; avoid catching bare Exception
Never return None to signal failure; raise exceptions with details
For crawling/document processing: continue batches, skip failed items entirely, and log detailed errors; never store placeholders (e.g., zeroed embeddings)

Files:

  • python/src/server/api_routes/settings_api.py
  • python/src/server/services/credential_service.py
archon-ui-main/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use state naming conventions: is[Action]ing for loading, [resource]Error for errors, selected[Resource] for selections

archon-ui-main/src/**/*.{ts,tsx}: WebSocket event failures should be logged and not crash serving other clients
External API calls should retry with exponential backoff and ultimately fail with a clear, specific message
Include actionable context in frontend error logs/messages (what was attempted, relevant IDs/URLs)
Never return null to signal failure in async/data flows; throw errors with details
Use polling (HTTP) with provided hooks (usePolling, useDatabaseMutation, useProjectMutation); Socket.IO is removed
State naming: is[Action]ing for loading, [resource]Error for errors, selected[Resource] for selections
Persist theme choice in localStorage and respect Tailwind dark mode classes across components

Files:

  • archon-ui-main/src/services/credentialsService.ts
  • archon-ui-main/src/components/settings/APIKeysSection.tsx
archon-ui-main/src/services/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

archon-ui-main/src/services/**/*.ts: Frontend service method naming: get[Resource]sByProject(projectId), getResource, create/update/delete[Resource]
Use GET /api/projects/{id}/tasks (not getTasks) for project tasks

Files:

  • archon-ui-main/src/services/credentialsService.ts
archon-ui-main/src/**/*.{ts,tsx,py}

📄 CodeRabbit inference engine (AGENTS.md)

Avoid comment keywords like LEGACY, CHANGED, REMOVED; write comments that document current functionality only

Files:

  • archon-ui-main/src/services/credentialsService.ts
  • archon-ui-main/src/components/settings/APIKeysSection.tsx
archon-ui-main/src/components/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use ESLint (standard React rules) for legacy UI code in /src/components

Files:

  • archon-ui-main/src/components/settings/APIKeysSection.tsx
archon-ui-main/src/components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Legacy components live under src/components and may be used by pages; add related tests under archon-ui-main/test

Files:

  • archon-ui-main/src/components/settings/APIKeysSection.tsx
python/src/server/services/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Backend service method naming mirrors CRUD patterns: get/create/update/delete with clear resource scoping

Files:

  • python/src/server/services/credential_service.py
🧬 Code graph analysis (2)
python/src/server/api_routes/settings_api.py (1)
python/src/server/services/credential_service.py (2)
  • get_credential (158-175)
  • get_credential (467-469)
archon-ui-main/src/components/settings/APIKeysSection.tsx (1)
archon-ui-main/src/services/credentialsService.ts (1)
  • updateCredential (222-247)
🔇 Additional comments (7)
python/src/server/services/credential_service.py (1)

334-341: LGTM: list output masks and omits encrypted_value.

No plaintext nor ciphertext is leaked in list views. Good alignment with PR goal.

python/src/server/api_routes/settings_api.py (1)

142-149: Never decrypt over HTTP — good change.

Forcing decrypt=False ensures the single-credential endpoint stays write-only for secrets.

archon-ui-main/src/components/settings/APIKeysSection.tsx (5)

19-20: LGTM: track backend-origin for write-only semantics.

isFromBackend cleanly distinguishes masked, backend-provisioned credentials.


105-111: LGTM: editing a masked value clears placeholder and flips ownership.

This avoids sending the sentinel back to the server and matches write-only behavior.


118-124: LGTM: prevent viewing masked backend credentials.

Good UX guard with a clear warning.


127-133: LGTM: disable encryption toggle for masked credentials.

Prevents misleading “decrypt” actions on write-only data.


396-397: LGTM: security notice matches new write-only flow.

Clear guidance for users about masked/edited behavior.

Comment on lines +55 to 71
const uiCredentials = apiKeys.map(cred => {
const isEncryptedFromBackend = cred.is_encrypted && cred.value === '[ENCRYPTED]';

return {
key: cred.key,
value: cred.value || '',
description: cred.description || '',
originalValue: cred.value || '',
originalKey: cred.key, // Track original key for updates
hasChanges: false,
is_encrypted: cred.is_encrypted || false,
showValue: false,
isNew: false,
isFromBackend: !cred.isNew, // Mark as from backend unless it's a new credential
};
});

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Sep 6, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Block saves/renames while value is masked to prevent persisting “[ENCRYPTED]”.

If a user renames or saves without editing, you’ll POST/PUT the literal placeholder, corrupting the real secret. Guard early.

     for (const cred of customCredentials) {
       if (cred.hasChanges || cred.isNew) {
         if (!cred.key) {
           showToast('Key name cannot be empty', 'error');
           hasErrors = true;
           continue;
         }
+        // Prevent persisting the placeholder for backend-encrypted items
+        if (cred.isFromBackend && cred.is_encrypted && cred.value === '[ENCRYPTED]') {
+          showToast(`Edit ${cred.key} to provide a new value before saving or renaming.`, 'warning');
+          hasErrors = true;
+          continue;
+        }

Optionally, also disable key renames for masked items (prevents confusion):

<input
  type="text"
  value={cred.key}
  onChange={(e) => updateCredential(index, 'key', e.target.value)}
  disabled={cred.isFromBackend && cred.is_encrypted && cred.value === '[ENCRYPTED]'}
  className="w-full px-3 py-2 rounded-md bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 text-sm font-mono"
/>

Also applies to: 153-210

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

No way to save something without editing it first

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

So we are good here

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@coleam00 You're right that the hasChanges flag only gets set when specific fields are modified. However, there's still a potential edge case I was concerned about:

If a user renames the key of a masked credential (changes cred.key), that sets hasChanges = true, but the value remains [ENCRYPTED]. Then when they save, the system would attempt to persist that placeholder string as the actual credential value.

Looking at the code:

if (field === 'key' || field === 'value' || field === 'is_encrypted') {
  updated.hasChanges = true;
}

So while you can't save without some edit, you could potentially save after just renaming the key, which would be problematic.

That said, if this scenario seems unlikely in practice or if you prefer to handle it differently (like showing a more targeted error when it happens), I understand. The core security goal of not exposing decrypted values is already achieved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@coleam00 You're absolutely right! Looking at the code flow more carefully:

  1. Masked credentials start with hasChanges: false
  2. The save logic only processes items where hasChanges || isNew is true
  3. To get hasChanges: true, the credential must be edited via updateCredential()
  4. When editing a masked credential's value, the code automatically clears the "[ENCRYPTED]" placeholder and sets isFromBackend: false

So there's indeed no pathway to save the placeholder - the existing logic already prevents this issue. Thanks for the clarification!

🐰✨ happy rabbit ears perk up with understanding


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@apolopena
Copy link
Copy Markdown

Looks good!

@Wirasm Wirasm merged commit 192c45d into main Sep 10, 2025
28 checks passed
@Wirasm Wirasm deleted the bugfix-unencrypted-credential-get branch September 10, 2025 11:24
coleam00 added a commit that referenced this pull request Apr 7, 2026
Fix: WorkflowExecution stale state on runId change and back button escape (#580)
Tyone88 pushed a commit to Tyone88/Archon that referenced this pull request Apr 16, 2026
…e-580-v2

Fix: WorkflowExecution stale state on runId change and back button escape (coleam00#580)
joaobmonteiro pushed a commit to joaobmonteiro/Archon that referenced this pull request Apr 26, 2026
…e-580-v2

Fix: WorkflowExecution stale state on runId change and back button escape (coleam00#580)
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.

3 participants