Skip to content

llm-auth: one credential per provider per tenant — add a multi-account credential pool with headroom-based selection #15029

Open

Description

@mrveiss

Parent: #15021

Problem

AutoBot can hold exactly one credential per provider per tenant. There is no account dimension
anywhere in the platform, so "this account is nearly out of quota, use the other one" is not
expressible.

Audit evidence:

  • llm_shared/provider_auth.py:146 — _vault_secret_name(provider_name, subject="global") returns
    f"provider_auth:{provider_name}:{subject}". subject is the org/tenant (org_vault_subject,
    :66), not an account.
  • api/provider_auth.py:588,620 — GET /api/llm-auth/status/{provider_name} and
    DELETE /api/llm-auth/{provider_name}. Singular; no list endpoint, no account id.
  • autobot-frontend/src/components/settings/ProviderOAuthConnect.vue:125,136 — connect/disconnect a
    single provider. No account list UI exists.
  • llm_shared/provider_registry.py — providers registered by name; set_fallback_chain is a list
    of provider names. No account selection at any layer.
  • grep -rniE "account_pool|credential_rotation|rotate_account|switch_account|multi[- _]account" over
    autobot-backend, autobot_shared, autobot-frontend/src → zero hits.

The storage layer does not block this: models/secret.py keys on (owner_id, name) with a
String(256) name, JSONB metadata, expires_at and is_active. A name of the form
provider_auth:{provider}:{subject}:{account_id} fits today — no migration to the secrets table is
required
.

Scope

Risks to hold the line on

An account pool is a shared resource that RBAC currently has no vocabulary for. Multi-tenancy makes
this sharper: one tenant must never consume another's account, and an account grantable to a team
must go through the existing grant model, not a new one.

Acceptance criteria

  • Existing single-credential deployments work with zero config change and zero migration —
    proven, not asserted.
  • Multiple accounts can be connected per provider and listed.
  • Dispatch selects the healthiest account by observed headroom; cooling / needs_reauth
    accounts are skipped.
  • Every response and every cost event names the serving account.
  • A tenant cannot read, select, or delete another tenant's account — covered by a test, not by
    inspection.
  • Deleting a non-default account does not disturb the default; deleting the last account is
    handled explicitly.
  • Pool membership resolves through VaultRef / principal resolution; no second credential store
    is introduced.
  • Tests: backward compatibility, multi-account selection, tenant isolation, per-account
    attribution, exhausted-pool behaviour.

Implementation Order

Wave: 3
Depends on: #15026, #15022, #15028
Unblocks: #15021 Task 6

Activity

  1. added this to the v0.10.0 milestone on Sep 12, 2026
  2. mrveiss commented on Sep 12, 2026

    @mrveiss
    OwnerAuthor

    Not implementable yet: this issue's own body says "Depends on: #15026, #15022, #15028" — all three are still OPEN. Adding blocked so the backlog picker (which reads formal blocked_by relationships, not free-text "Depends on:") skips this until the quota-monitor prerequisites land.

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions