Skip to content

[AGE-3945] fix(api): dedupe provider_key secrets on create and delete#5270

Open
sridhar852002 wants to merge 2 commits into
Agenta-AI:mainfrom
sridhar852002:fix/age-3945-provider-key-duplicates
Open

[AGE-3945] fix(api): dedupe provider_key secrets on create and delete#5270
sridhar852002 wants to merge 2 commits into
Agenta-AI:mainfrom
sridhar852002:fix/age-3945-provider-key-duplicates

Conversation

@sridhar852002

@sridhar852002 sridhar852002 commented Jul 13, 2026

Copy link
Copy Markdown

Summary

Fixes #5255 / AGE-3945.

When users removed Anthropic connections in the UI, GET /api/secrets/ could still return duplicate provider_key rows. The settings UI hides duplicates (standardSecretsAtom uses .find()), so delete only removed one row. Agent runs with connection.mode: "agenta" then failed with:

multiple connections for provider 'anthropic'; name one in the config

Changes

  1. Backend (VaultService.create_secret): upsert standard provider_key secrets by provider kind so duplicates cannot accumulate.
  2. Frontend (deleteSecretAtom): delete all vault secrets matching the provider name, not only the single visible row id.
  3. Tests: unit coverage for upsert behavior (same provider updates; different providers stay separate).

Test plan

  • cd api && uv run --python 3.13 pytest oss/tests/pytest/unit/secrets/ -v (7 passed, including new upsert tests)
  • ruff format + ruff check --fix on changed Python files
  • Manual QA (needs local/EE stack):
    1. Seed or create two Anthropic provider_key secrets for one project
    2. Confirm GET /api/secrets/ returns 2 Anthropic rows
    3. Delete Anthropic in Settings / Model Hub UI
    4. Confirm GET /api/secrets/ returns 0 Anthropic provider_key rows
    5. Create agent with Claude + Anthropic + connection.mode: "agenta" and run — no ambiguous-connection error
  • Creating a second Anthropic key updates the existing row instead of inserting a duplicate

Demo

Before/after of the duplicate provider_key bug and fix:

AGE-3945 provider key dedupe before/after

  • Before: UI shows one Anthropic row, but vault still has two provider_key records after delete → agent run fails as ambiguous.
  • After: delete removes all matching secrets; create upserts by provider kind so duplicates cannot accumulate.

Prevent duplicate standard provider vault rows from leaving agent
credential resolution ambiguous after UI delete (Agenta-AI#5255 / AGE-3945).

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

✅ Thanks @sridhar852002! This PR now meets the contribution requirements and has been reopened. A maintainer will review it soon.

@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Jul 13, 2026
@github-actions github-actions Bot added the incomplete-pr PR is missing required template sections or a demo recording label Jul 13, 2026
@dosubot dosubot Bot added the bug Something isn't working label Jul 13, 2026
@github-actions github-actions Bot closed this Jul 13, 2026
@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

@sridhar-codes is attempting to deploy a commit to the agenta projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Provider key creation now performs a deduplication/upsert for the same project and provider kind, preventing duplicate rows.
    • When a matching provider key exists, the stored header/data are updated instead of inserting a new entry.
    • Deleting a provider key now removes all duplicate secret rows associated with that provider (including name-based matches when available).
    • Provider keys for different provider kinds still remain separate.
  • Tests
    • Added unit tests validating provider-key upsert vs non-upsert behavior and multi-row deletion handling.

Walkthrough

Provider-key creation now updates matching secrets instead of creating duplicates. Frontend deletion removes all vault rows matching a provider name, with provider ID fallback behavior.

Changes

Provider key lifecycle

Layer / File(s) Summary
Provider-key upsert flow
api/oss/src/core/secrets/services.py, api/oss/tests/pytest/unit/secrets/test_provider_key_upsert.py
Provider-key creation matches secrets by scope and provider kind, updates matches, and creates separate records for different kinds. Unit tests cover both paths.
Duplicate vault-row deletion
web/packages/agenta-entities/src/secret/state/atoms.ts
Secret deletion finds and removes all matching vault rows, falling back to the provider ID when needed.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the provider_key dedupe fix on create and delete.
Description check ✅ Passed The description matches the implemented backend, frontend, and test changes.
Linked Issues check ✅ Passed [#5255] The code upserts duplicate provider_key secrets and deletes all matching vault rows, addressing the ambiguous agent-run bug.
Out of Scope Changes check ✅ Passed The reviewed code changes stay focused on the duplicate provider_key fix and related tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions github-actions Bot removed the incomplete-pr PR is missing required template sections or a demo recording label Jul 13, 2026
@github-actions github-actions Bot reopened this Jul 13, 2026
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
web/packages/agenta-entities/src/secret/state/atoms.ts (1)

288-289: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider continuing deletion after individual failures.

The sequential await loop stops on the first error, leaving remaining duplicate IDs undeleted. If one secret was already removed by another path, the loop aborts and the user must retry to clean up the rest. Consider collecting errors and attempting all deletions before throwing.

♻️ Proposed refactor: attempt all deletions, then throw aggregated error
-        for (const secret_id of secretIds) {
-            await deleteMutation.mutateAsync({projectId, secret_id})
+        const errors: unknown[] = []
+        for (const secret_id of secretIds) {
+            try {
+                await deleteMutation.mutateAsync({projectId, secret_id})
+            } catch (error) {
+                errors.push(error)
+            }
+        }
+        if (errors.length > 0) {
+            throw new Error(
+                `[vault] Failed to delete ${errors.length} of ${secretIds.length} secret(s)`
+            )
         }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8f4cce40-8f88-4e2c-ba4a-d741e0aabc2f

📥 Commits

Reviewing files that changed from the base of the PR and between 765da6e and fcf6150.

⛔ Files ignored due to path filters (1)
  • docs/assets/demos/age-3945-provider-key-dedupe.png is excluded by !**/*.png
📒 Files selected for processing (3)
  • api/oss/src/core/secrets/services.py
  • api/oss/tests/pytest/unit/secrets/test_provider_key_upsert.py
  • web/packages/agenta-entities/src/secret/state/atoms.ts

Comment on lines +28 to +47
if create_secret_dto.secret.kind == SecretKind.PROVIDER_KEY:
existing = await self.secrets_dao.list(
project_id=project_id,
organization_id=organization_id,
)
provider_kind = create_secret_dto.secret.data.kind
for secret in existing:
if secret.kind != SecretKind.PROVIDER_KEY:
continue
existing_kind = getattr(secret.data, "kind", None)
if existing_kind == provider_kind and secret.id is not None:
return await self.secrets_dao.update(
secret_id=secret.id,
update_secret_dto=UpdateSecretDTO(
header=create_secret_dto.header,
secret=create_secret_dto.secret,
),
project_id=project_id,
organization_id=organization_id,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for uniqueness constraints on secrets model/table
rg -n "UniqueConstraint\|unique_together\|unique=" api/oss/src/core/secrets --type=py | head -20

# Check the real SecretsDAO update method signature and return type
rg -n "async def update" api/oss/src/core/secrets --type=py -A 15 | head -40

# Check for any existing migrations adding uniqueness on secrets
fd -e py . api/oss/src --exec rg -l "secrets" {} \; | head -10

Repository: Agenta-AI/agenta

Length of output: 2908


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== secrets core files =="
git ls-files api/oss/src/core/secrets api/oss/src | sed -n '1,200p'

echo
echo "== inspect secrets DTOs / interfaces / services / DAO implementation =="
for f in \
  api/oss/src/core/secrets/dtos.py \
  api/oss/src/core/secrets/interfaces.py \
  api/oss/src/core/secrets/services.py \
  api/oss/src/core/secrets/*.py \
  api/oss/src/core/secrets/**/*dao*.py \
  api/oss/src/core/secrets/**/*repository*.py
do
  [ -f "$f" ] && { echo "--- $f"; wc -l "$f"; }
done

echo
echo "== search for uniqueness / locking / transaction hints in secrets core =="
rg -n "UniqueConstraint|unique_together|unique=|upsert|ON CONFLICT|for update|SELECT .*FOR UPDATE|transaction|lock" api/oss/src/core/secrets api/oss/src -g '*.py'

echo
echo "== find secret model definitions and migrations mentioning secrets =="
rg -n "class .*Secret|secrets table|SecretKind|provider_kind|kind" api/oss/src -g '*.py'
fd -a -t f . api/oss/src/migrations api/oss/migrations . | rg 'secrets|secret|migration|alembic|versions'

echo
echo "== read the relevant service file section =="
sed -n '1,180p' api/oss/src/core/secrets/services.py

echo
echo "== read interfaces =="
sed -n '1,200p' api/oss/src/core/secrets/interfaces.py

echo
echo "== read DTOs =="
sed -n '1,240p' api/oss/src/core/secrets/dtos.py

Repository: Agenta-AI/agenta

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== secrets package outline =="
ast-grep outline api/oss/src/core/secrets/services.py --view expanded || true
ast-grep outline api/oss/src/core/secrets/interfaces.py --view expanded || true
ast-grep outline api/oss/src/core/secrets/dtos.py --view expanded || true
ast-grep outline api/oss/src/core/secrets/enums.py --view expanded || true

echo
echo "== locate secrets DAO / model implementations =="
rg -n "class .*Secrets?DAO|class .*Secret.*DAO|def list\\(|def create\\(|def update\\(" api/oss/src -g '*.py' | sed -n '1,200p'

echo
echo "== search for secrets table/model definitions =="
rg -n "SecretKind|provider_kind|secrets? table|class .*Secret|__tablename__.*secret|__tablename__.*secrets" api/oss/src -g '*.py' | sed -n '1,240p'

echo
echo "== search for migrations mentioning secret(s) =="
fd -a -t f api/oss/src | rg '/migrations/|/versions/|alembic|migration|secret'
rg -n "secret|secrets|provider_kind|SecretKind" api/oss/src/migrations api/oss/src -g '*.py' | sed -n '1,240p'

echo
echo "== read services.py relevant section =="
sed -n '1,140p' api/oss/src/core/secrets/services.py

echo
echo "== read interfaces.py =="
sed -n '1,120p' api/oss/src/core/secrets/interfaces.py

Repository: Agenta-AI/agenta

Length of output: 9710


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== secrets postgres model =="
sed -n '1,220p' api/oss/src/dbs/postgres/secrets/dbes.py

echo
echo "== secrets postgres DBA =="
sed -n '1,220p' api/oss/src/dbs/postgres/secrets/dbas.py

echo
echo "== secrets postgres DAO =="
sed -n '1,220p' api/oss/src/dbs/postgres/secrets/dao.py

echo
echo "== secrets mappings =="
sed -n '1,180p' api/oss/src/dbs/postgres/secrets/mappings.py

echo
echo "== secrets migrations / alembic mentions =="
fd -a -t f . api/oss/src/dbs api/oss/migrations . | rg 'secrets|secret|alembic|versions|migration' | sed -n '1,200p'

echo
echo "== search for constraints in secrets postgres package =="
rg -n "UniqueConstraint|unique=True|index=True|primary_key=True|ForeignKey|Constraint" api/oss/src/dbs/postgres/secrets api/oss/src/dbs/postgres -g '*.py' | sed -n '1,240p'

echo
echo "== search for transaction / lock usage in secrets DAO =="
rg -n "transaction|begin\\(|for update|with_for_update|lock|acquire|commit|rollback|session" api/oss/src/dbs/postgres/secrets api/oss/src/dbs/postgres -g '*.py' | sed -n '1,240p'

Repository: Agenta-AI/agenta

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== service file =="
sed -n '1,140p' api/oss/src/core/secrets/services.py

echo
echo "== provider-key upsert test =="
sed -n '1,240p' api/oss/tests/pytest/unit/secrets/test_provider_key_upsert.py

echo
echo "== any secrets uniqueness tests =="
rg -n "provider_key|upsert|duplicate|unique" api/oss/tests/pytest/unit/secrets -g '*.py' | sed -n '1,200p'

Repository: Agenta-AI/agenta

Length of output: 9100


Make the provider-key upsert atomic. SecretsDBE has no uniqueness constraint for (scope, provider_kind), and create_secret() does a separate list() then update()/create() in different sessions. Two concurrent calls can still insert duplicate provider keys. Add a scoped unique constraint or serialize this path with a transaction/row lock.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

(bug) Removed provider connections still make agent runs ambiguous

3 participants