credential ownership model and BYO provider keys

draft

1. Sumário

A Koder Stack mistura hoje, num mesmo lugar (meta/context/credentials/), dois tipos de credenciais que têm donos e ciclos de vida diferentes: segredos de infra da Stack e chaves de API externas que pertencem a um usuário individual. Este RFC define uma taxonomia de propriedade (infra / org / workspace / usuário-BYO), onde cada classe deve ser armazenada, e como o Koder AI gateway resolve qual chave usar por requisição, dado que a Stack é multi-tenant, multi-user e multi-domínio/workspace.

2. O problema (concreto, desta sessão)

Ao provisionar o Gemini para o Edictus (#063), as chaves foram criadas no Google AI Studio sob a conta pessoal rpm32510@gmail.com — não sob um contrato comercial Koder↔Google. Mesmo assim acabaram tratadas como segredo Stack-global em meta/context/credentials/. Isso está errado de modelagem:

  • A chave é de um usuário (rodrigo@koder.dev), não da empresa Koder.
  • O billing/quota é da conta dele, não de um contrato da Stack.
  • Num ambiente multi-tenant, "uma chave global no repo de infra" não tem como

    representar "a chave do usuário X no workspace Y".

  • Reforça o vetor do post-mortem 2026-05-06-credentials-leak.kmd (segredo de

    pessoa física versionado no monorepo).

3. Contexto: a Stack é multi-tenant / multi-user / multi-workspace

Modelo de identidade (ver kompass-RFC-001 + specs/multi-tenancy/contract.kmd):

  • Tenant/Org — fronteira de isolamento (RLS). Pode ser a própria Koder ou um

    cliente.

  • Domínio/Workspace — uma org pode ter vários workspaces (modelo Google

    Workspace; workspace_id resolvido no login → JWT, cf. chat multi-tenancy).

  • User — pertence a um ou mais workspaces; o mesmo user pode atuar em vários.

Logo, "dono de uma credencial" não é binário (infra vs usuário). É um eixo de escopo que precisa casar com esse modelo de identidade.

4. Taxonomia de propriedade (4 escopos)

Escopo Dono Exemplos Lar canônico Quem é alertado de expiração
infra a Stack (operacional, sem tenant) TLS, signing keys, DB, Flow service PATs, tokens de serviço interno secret store de infra (meta/context/credentials/ hoje → pass infra-scope) admin da Stack
org/tenant uma org sob contrato futura conta enterprise Koder↔Anthropic/Google; chave de IA paga pela org pass, escopo tenant_id admin da org
workspace/domínio um workspace chave de IA compartilhada pelos membros de um workspace pass, escopo (tenant_id, workspace_id) admin do workspace
user (BYO) um usuário a chave GeminiOpenAIMS pessoal do rodrigo@koder.dev (conta rpm32510) vault do Koder ID do usuário (pass, escopo user_id) o próprio usuário

A chave Gemini de hoje é user-BYO — caixa 4. Não é infra, não é org.

5. Armazenamento

  • infra continua no secret store de infra (curto prazo meta/context/credentials/;

    longo prazo pass com escopo infra). infra mora aqui.

  • org / workspace / user-BYO moram no services/foundation/pass (o secrets

    plane), com registro tipado e RLS por (tenant_id, workspace_id?, user_id?) (multi-tenant-by-default.kmd + specs/multi-tenancy/contract.kmd). A UI é o vault do Koder ID (id/app/.../sections/vault/), numa aba "Provedores conectados / Chaves de API".

  • Chave de terceiro de um usuário é dado do usuário → sujeita a

    identity-data-retention.kmd (retenção, erasure/LGPD).

A entrada tipada (proto koder/id/keys/v1) carrega, no mínimo: { id, scope (infra|org|workspace|user), tenant_id?, workspace_id?, user_id?, provider (google|openai|anthropic|azure|brave|…), kind (api_key|oauth|sa_key), secret_ref, label, created_at, expires_at?, rotation_policy, last_verified_at? }. O segredo fica no pass; o metadado (sem segredo) é auditável.

6. Resolução no Koder AI gateway (precedência)

Componentes nunca retêm chave de provedor. Eles chamam a API da Koder AI autenticados como (user_id, workspace_id, tenant_id) (claims do JWT do Koder ID). O gateway resolve a chave por precedência, do mais específico ao mais geral:

1. user-BYO        (chave pessoal do user, se existir e habilitada p/ este uso)
2. workspace       (chave do workspace atual)
3. org/tenant      (chave contratada pela org)
4. Stack-managed   (chave operada pela Koder como serviço gerenciado — se ofertado)
→ senão: 402/403 "nenhuma credencial de IA disponível para esta identidade"

Billingquota seguem o dono da chave resolvida. Trocarrotacionar a chave é mudança só no gateway/vault — nenhum componente é afetado (o desacoplamento que teria evitado a dor do #063).

7. Consumo pelos componentes

  • Componentes (Edictus, etc.) consomem um endpoint interno da Koder AI, não o

    Google/OpenAI direto. Caso aioverview (grounded-search): vira client do gateway (grounded-search capability), sem GEMINI_API_KEY no Edictus (fecha o Gemini do edictus#064).

  • Para LLM comum, o gateway já encaixa; grounded-search (Gemini google_search →

    citações) precisa de capability dedicada no gateway.

8. Observabilidade / auditoria (cred-audit)

O koder-tools/cred-audit (ticket KTOOLS) opera sobre o metadado (nunca descriptografa em massa — anti-padrão pós-leak). Cada credencial tem scope:

  • infra → alerta vai pro admin da Stack (notice + ticket no backlog do dono).
  • org|workspace|user → alerta vai pro dono (na conta/workspace dele),

    respeitando isolamento de tenant. Nunca expor cross-tenant.

Sinais por classe: notAfter (certs/keystores), expiry via API (SA keys, PATs), deadlines de política (ex.: Google "chaves irrestritas param 2026-06-19), e liveness opt-in (a chave ainda autentica? — pega revogação/quota-morta, como o 429/AQ expired desta sessão). Auto-renovação só com verify-then-swap (cria nova → valida → cutover → revoga antiga; nunca apaga-antes-de-validar) e em classes renováveis (TLS via certs; SA keys; PATs). CSEBraveGemini-AIza sem API de criação → alerta+ticket.

9. Segurança

  • multi-tenancy contract: leitura de chave por identidade passa por RLS; acesso

    cross-tenant = 404 (specs/multi-tenancy/contract.kmd).

  • sem mass-decrypt diário: auditoria é metadata-driven; liveness usa o segredo

    só quando marcado, em ambiente isolado, sem logar valor.

  • erasure: apagar a conta do usuário apaga as chaves BYO dele (cascata LGPD).

10. Migração

  1. Curto prazo (agora): cred-audit introduz o campo scope no manifesto;

    classificamos as credenciais atuais (a maioria de credentials/ é infra; as chaves de IA pessoais viram user:rodrigo). Nada quebra.

  2. Médio: Koder ID ganha "Provedores conectados" no vault (via pass); chaves

    userworkspaceorg migram pra lá; saem de credentials/.

  3. Médio: gateway implementa a resolução por precedência (§6) + grounded-search.
  4. Edictus: aioverview passa a chamar o gateway (sem chave local).

11. Tickets desbloqueados

  • AIGW — per-identity provider-key resolution (§6); grounded-search capability (§7).
  • id — connected-provider-keys no vault, tipado e escopado (§4–5).
  • KTOOLScred-audit com scope + verify-then-swap (§8).
  • edictusaioverview via gateway, depreca chave local (§7; supersede o

    trecho Gemini do edictus#064).

12. Apêndice — por que a distinção importa (a "explicação da explicação")

A intuição errada é tratar toda credencial como "segredo da Stack". Mas uma API key não é da Koder só porque está no monorepo da Koder — ela pertence a quem a emitiu na conta do provedor. A chave Gemini foi emitida na conta Google pessoal do Rodrigo (rpm32510, plano Ultra), então:

  • Posse: é do usuário. A Koder não tem contrato com o Google que a cubra.
  • Billing: cai na conta dele, não num contrato Koder.
  • Ciclo de vida: ele criarevogarotaciona; a Stack não "mantém" a chave.
  • Multi-tenant: noutro tenant/workspace, outro usuário traria a sua chave —

    impossível representar isso com "uma chave global no repo".

Por isso o lar é a conta do usuário no Koder ID (vault/pass), e o uso é mediado pelo gateway em nome dele. Quando (e se) a Koder fechar um contrato enterprise com um provedor, aí sim nasce uma credencial de escopo org/Stack — uma caixa diferente desta. As duas coexistem; o que não pode é confundi-las.