credential ownership model and BYO provider keys
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 depessoa 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_idresolvido 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
passcom escopo infra). Só infra mora aqui. - org / workspace / user-BYO moram no
services/foundation/pass(o secretsplane), 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-searchcapability), semGEMINI_API_KEYno 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 só 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
- Curto prazo (agora):
cred-auditintroduz o camposcopeno manifesto;classificamos as credenciais atuais (a maioria de
credentials/éinfra; as chaves de IA pessoais viramuser:rodrigo). Nada quebra. - Médio: Koder ID ganha "Provedores conectados" no vault (via
pass); chavesuserworkspaceorgmigram pra lá; saem decredentials/. - Médio: gateway implementa a resolução por precedência (§6) + grounded-search.
- Edictus:
aioverviewpassa 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).
- KTOOLS —
cred-auditcomscope+ verify-then-swap (§8). - edictus —
aioverviewvia gateway, depreca chave local (§7; supersede otrecho 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.