API — parte 2 (desde RN-055)
Credenciais
Chave de LLM e token de git do usuário vivem na mesma tabela
(user_credentials), sob o mesmo envelope encryption. Esta seção vale para as
duas.
RN-055 — Credencial é sempre cifrada e gravada; verificar é ação à parte, com três respostas
O cadastro não julga a credencial: cifra e grava, mesmo que o provider fosse recusá-la. Verificar é uma ação explícita sobre a credencial já gravada — a api decifra, chama o provider e devolve só o veredito. O texto plano nunca volta em resposta nenhuma, nem em pedaço, nem no motivo da recusa.
Era o contrário: o cadastro testava antes de persistir e recusava a gravação (ADR 0004, estendido às chaves de LLM na Fase 11a). O modo de falha real foi o oposto do previsto — como o campo é write-only e a tela nunca reexibe o que foi digitado, uma recusa deixava o usuário sem credencial e sem o texto para corrigir.
O veredito tem três valores, e o terceiro é obrigatório:
| quando | |
|---|---|
ok |
o provider aceitou |
recusado |
o provider rejeitou — carrega o motivo dele |
nao_suportado |
não há endpoint de teste verificado (ollama, anthropic, openai) |
Sem nao_suportado, um provider cujo tester é NO-OP voltaria ok e a tela
afirmaria uma verificação que nunca aconteceu. É a regra de capability do
ADR 0041
aplicada aqui: só se declara o que foi provado. Por isso o port declara
supports() — o silêncio de test() sozinho é ambíguo.
Chave ruim não é exceção HTTP: o caso de uso captura
LLMCredentialConnectionTestFailedError/GitCredentialConnectionTestFailedError
e devolve resultado, com 200. A única exceção é não existir credencial para
(usuário, provider) — 404, porque aí não há o que testar.
- Onde:
src/application/use-cases/credentials/test-stored-credential.use-case.ts,src/application/use-cases/llm/upsert-user-credential.use-case.ts,src/application/use-cases/git/register-git-credential.use-case.ts,src/application/ports/llm-credential-connection-tester.port.ts(supports) - Teste:
test/application/use-cases/credentials/test-stored-credential.use-case.spec.ts(os três resultados, o despacho git×LLM, o 404 e a ausência do segredo na resposta);test/application/use-cases/llm/upsert-user-credential.use-case.spec.tsetest/application/use-cases/git/register-git-credential.use-case.spec.ts(a gravação incondicional, que é a inversão) - Origem: ADR 0050
RN-056 — Faceta de capability vem do provider; silêncio preserva o que estava
supports_vision, supports_reasoning e generates_image são fato do
provider, não opinião: saem do catálogo remoto no sync, com o mesmo fallback
de supports_tool_calling — remoto, depois local, depois false.
No OpenRouter (o único que publica isso hoje) as três saem de:
architecture.input_modalities contém image,
supported_parameters contém reasoning, e
architecture.output_modalities contém image. Aceitar imagem e produzir
imagem são eixos distintos: fundi-los mandaria o usuário para o modelo errado.
Antes, o sync lia supportsVision do que já estava GRAVADO e nunca consultava
o remoto — a coluna nascia false e não havia caminho para virar verdadeira.
Os 338 modelos do primeiro sync real ficaram todos false, incluindo 181 que o
provider declara como multimodais.
Ausência de declaração não é declaração de ausência
(ADR 0041): o
parser OMITE o campo quando o provider se cala, e undefined preserva o valor
local. Por isso a tela usa as facetas só como filtro POSITIVO e nunca escreve
"não lê imagem" — false aqui quer dizer "o provider não declarou".
- Onde:
src/infrastructure/llm/openrouter-provider.ts(temModalidade,parseCatalogoOpenRouter),src/application/use-cases/llm/sync-model-catalog.use-case.ts,src/db/schema/llm.ts(models) - Teste:
test/infrastructure/llm/openrouter-provider.contract.spec.ts(modalidade não declarada OMITE o campo em vez de afirmar false);test/application/use-cases/llm/sync-model-catalog.use-case.spec.ts(catálogo que se cala sobre modalidade preserva a faceta gravada) - Origem: ADR 0051
RN-057 — "Para que serve" é curadoria do workspace, e marcar uso não liga o modelo
Nenhum catálogo de provider publica "bom para código". Isso é opinião de quem
opera, descoberta usando — então mora em workspace_models.uses, ao lado da
outra decisão do workspace (RN-052), e não em models.
Vocabulário FECHADO — codigo, documentacao, analise, imagem,
conversa —, com prova de exaustividade em tempo de compilação nos dois lados.
Texto livre daria code, coding e código no mesmo filtro em uma semana.
Duas regras que mantêm os eixos separados:
- Marcar uso não liga o modelo.
workspace_models.is_activetem DEFAULTtrue, então a linha criada por uma marcação de uso é inserida comis_active = falseexplícito. Sem isso, opinar sobre um modelo o autorizaria a gastar, contra a RN-043. - Trocar o uso não desliga o que estava ligado.
is_activefica fora doSETdoON CONFLICT.
A lista de usos substitui a anterior, não soma: lista vazia é como se desmarca tudo, e é um estado legítimo — "ninguém opinou" não é "não serve".
- Onde:
src/domain/llm/model-uses.ts,src/db/schema/llm.ts(workspace_models.uses),src/infrastructure/persistence/drizzle/workspace-model.repository.ts(setUses),src/application/use-cases/llm/set-model-uses.use-case.ts - Teste:
test/application/use-cases/llm/set-model-uses.use-case.spec.ts(marcar uso NÃO liga o modelo — a linha nova nasce inativa,trocar o uso não desliga o que já estava ligado,o uso vale só neste workspace) - Origem: ADR 0051
RN-058 — A chave que o AGENTE gasta é a do owner do workspace
Credencial de LLM pertence a uma pessoa (user_credentials.user_id), e agente
não é pessoa. O turno de agente resolve a chave pelo owner do workspace
(workspaces.created_by), não por quem abriu a sessão nem por quem criou o
projeto: quem banca a conta banca os agentes, e isso não muda quando outra
pessoa da equipe começa a sessão.
created_by e não workspace_members.role = 'owner': pode haver vários
owners, e "qualquer um deles" faria a chave usada variar sem ninguém decidir.
Antes disto o turno passava o slug do agente (agentId ?? sessionId) na
coluna de usuário. A consulta ia ao banco com user_id = 'criativo', o
Postgres recusava o UUID inválido, e o erro virava resposta vazia no fio —
sem métrica, sem evento de falha, sem nada na tela. O efeito prático, que só
uma execução real revelou: nenhum agente jamais usou um provider com
credencial. Só ollama funcionava, porque para ele a busca é pulada — e foi
com modelo local que a Fase 4, o dogfooding da Fase 10 e todas as demos
rodaram.
O chat humano nunca teve o defeito: ele usa actor.id, que é o usuário de
verdade.
- Onde:
src/application/use-cases/llm/resolve-credential-owner.use-case.ts,src/application/use-cases/llm/stream-llm-turn.use-case.ts,src/application/use-cases/llm/run-llm-turn.use-case.ts - Teste:
test/application/use-cases/llm/resolve-credential-owner.use-case.spec.ts(o owner vence quem criou o projeto; a chave encontrada é a dele; projeto inexistente é 404 e não erro de banco) - Origem: execução real da FASE 13b
RN-060 — O gasto das chaves é do owner, e só ele vê
O relatório de consumo por credencial (GET /workspaces/:id/credential-spend)
exige owner no workspace. Não é maintainer: desde a
RN-058 os agentes de todos os projetos gastam a credencial do dono,
e a fatura dele não é assunto de quem só opera um projeto.
O relatório agrupa por provider, porque é essa a unidade da credencial — uma chave por provider, por pessoa. Um total único não bateria com fatura nenhuma.
E separa agente de pessoa: as duas coisas saem da mesma chave desde a
RN-058, e "meus agentes estão caros?" é uma pergunta diferente de "eu uso muito
o chat?". Por isso este é o único agregado de custo do produto sem o filtro
actor_kind = 'agent' da RN-038 — aqui a pergunta é quanto saiu da
chave, e o chat do próprio owner sai dela.
Gasto de credencial já removida continua no relatório, marcado: o consumo aconteceu, e escondê-lo daria um total que não fecha com o extrato do provider.
Nenhum segredo atravessa: a resposta tem provider, tokens e custo — nunca a chave, nem cifrada (ADR 0050).
- Onde:
src/application/use-cases/llm/get-credential-spend.use-case.ts,src/interfaces/http/llm/budgets.controller.ts,apps/web/src/components/CredentialSpendSection.tsx - Teste:
test/application/use-cases/llm/get-credential-spend.use-case.spec.ts(agrupa por provider; separa agente de pessoa; chave removida fica marcada);apps/web/src/components/CredentialSpendSection.test.tsx - Origem: decisão do usuário junto com a RN-058
RN-065 — Um module_map por SESSÃO; revisar é outra sessão
create_module_map recusa uma segunda emissão dentro da mesma sessão,
com uma mensagem que diz o próximo passo. Entre sessões o mapa continua
versionando (version + 1, findCurrent devolve o mais alto) — revisar a
arquitetura é comportamento desejado.
A distinção é o ponto: entre sessões, uma nova emissão é uma revisão;
dentro da mesma, é o modelo redecidindo do zero. Numa execução real o
Arquiteto emitiu quatro mapas seguidos, com nomes e recortes diferentes a
cada vez — greeting, hello_core, greeting, hello-api-core — e o laço
só terminou porque a rede caiu (%Req.TransportError{reason: :timeout}).
A recusa volta ao modelo pelo tool-result (RN-061): ele lê que já
existe um e segue para assign_story_modules, que é o passo 2 do kickoff
dele. É por isso que o turno não termina quando o mapa é emitido — o
Arquiteto ainda tem três passos pela frente (vincular histórias, propor um
ADR, registrar tensões), e terminar ali mataria os três.
- Onde:
src/application/use-cases/architecture/create-module-map.use-case.ts - Teste:
test/application/use-cases/architecture/create-module-map.use-case.spec.ts(refuses the SECOND map of the same session; e o versionamento entre sessões continua provado ao lado) - Origem: execução real da FASE 13b
RN-066 — Toda resposta sobre módulos traz os nomes canônicos
O Arquiteto não tem ferramenta para ler o module_map vigente. É por isso que as três respostas que ele recebe sobre módulos precisam dizer os nomes:
- Um
create_module_mapbem-sucedido devolve os módulos como a api os gravou — não apenas a versão. - Um
assign_story_modulesrecusado lista os módulos válidos, além dos inexistentes. - Um
create_module_maprecusado pela RN-065 diz quais módulos a sessão já definiu, não quantos.
Sem mapa nenhum não há nomes a oferecer, e uma lista vazia lê-se como "chute de novo": esse caminho nomeia o problema real — falta o passo 1 do kickoff.
O motivo é concreto. Numa execução real o Arquiteto emitiu o mapa
(saudacao, api_http), não conseguiu relê-lo e recorreu à força bruta: 18
chutes seguidos — api, core, http, greeting, domain, web,
hello-api, hello, greeting-api, saudacao, app, server, publico,
public-api, api-publica — até acertar um por sorte. Nas palavras dele
no event log: "vou descobrir os nomes válidos testando candidatos
plausíveis".
O dano não foi o desperdício, foi o resultado: as quatro histórias
acabaram no mesmo módulo (saudacao), inclusive a do endpoint, api_http
ficou sem história nenhuma, e o desfecho afirmou "Todas as 4 histórias foram
vinculadas a módulos com sucesso". Como a execução sobe um dev agent por
módulo, a arquitetura desenhada não seria a construída.
O laço da RN-065 era um sintoma disso: o Arquiteto reemitia o mapa justamente para tentar fixar nomes que não conseguia ler.
- Onde:
src/application/use-cases/architecture/assign-story-modules.use-case.ts,src/application/use-cases/architecture/create-module-map.use-case.ts,apps/engine/lib/engine/harness/tools/create_module_map.ex - Teste:
test/application/use-cases/architecture/assign-story-modules.use-case.spec.ts(the refusal lists the VALID modules;with no module_map, it directs creating the map) ecreate-module-map.use-case.spec.ts(the refusal says WHICH modules there are) - Origem: execução real da FASE 13b
RN-067 — Toda sessão nasce emitindo session.created
CreateSessionUseCase é o único lugar que cria uma sessão. Ele emite
session.created para o outbox na mesma transação do insert, e é esse
evento que faz o engine subir o SessionServer da sessão.
Quem chamou sessions.create(...) diretamente produziu uma sessão que o
engine nunca conheceu. O efeito é uma cascata silenciosa:
- o canal Phoenix responde
REFUSED JOINpara sempre — a UI só reclama no console e continua tentando a cada 10 segundos; - sem canal não há atualização ao vivo: o fio fica preso no indicador de
digitação, mesmo com o agente já
idle; - ninguém bate o heartbeat, e como é o heartbeat que fecha a sessão
(RN-064), ela fica
activepara sempre.
Três caminhos faziam isso: provision-repository (duas chamadas),
adopt-repository e activate-execution — este último cria a sessão onde
rodam os dev agents.
A prova por contraste, de uma execução real: a sessão do assistente não tinha
session.created, tinha engine.session_states vazio e REFUSED JOIN; a
sessão aberta pela rota normal tinha o evento, a linha de estado e JOINED.
O teste é sobre a FONTE de propósito: um teste de comportamento provaria um caminho por vez, e o defeito aqui é o caminho em que ninguém pensou.
- Onde:
src/application/use-cases/sessions/create-session.use-case.ts(o dono),git/provision-repository.use-case.ts,git/adopt-repository.use-case.ts,execution/activate-execution.use-case.ts(os chamadores) - Teste:
test/application/use-cases/sessions/toda-sessao-emite-created.spec.ts(only CreateSessionUseCase calls sessions.create) - Origem: execução real da FASE 13b
RN-068 — O dev agent lê o worktree sem pedir permissão
Ativar a execução semeia duas famílias de comandos no allow do projeto:
ler o próprio worktree (ls, pwd, find, cat, head, tail,
grep, wc, echo, git status, git diff, git log) e build e
teste.
A segunda já existia: ReportDone só deixa uma PR abrir depois de um
terminal com exit 0. A primeira foi acrescentada porque o agente olha
antes de construir, e sem ela não conseguia nem começar.
O motivo é concreto. Uma ferramenta :pipeline pendente devolve
proposed_action <id> status pending como RESULTADO — não a saída do
comando — e o ToolLoop segue em frente. Num repositório recém-provisionado,
cada ls -la do agente caía em aprovação, não lhe ensinava nada e queimava
uma iteração. Numa execução real o desfecho foi
toolloop.limit_reached {iteration: 8, max_iterations: 8}, tarefa bloqueada
por "limite de iterações atingido", sem uma linha escrita — e as aprovações
que o usuário deu chegaram depois de o laço esgotado.
Liberar leituras não afrouxa o pipeline, e é isso que o teste afirma: deny
vence allow, os BUILTIN_DENY_PATTERNS continuam ativos, o casamento é por
prefixo de TOKEN (um ls permitido não permite lsof), e um comando
composto exige que TODO segmento case — ls && rm -rf / não passa por causa
do ls.
A allowlist é mitigação, não solução: é uma lista de comandos previstos e o modelo inventa comandos. A correção estrutural — o agente ESPERAR a decisão em vez de queimar iterações — está no ADR 0052.
- Onde:
src/domain/actions/dev-terminal-patterns.ts, semeado porapplication/use-cases/execution/activate-execution.use-case.ts - Teste:
test/domain/actions/dev-terminal-patterns.spec.ts(allows ls -la;a composite command doesn't hitch a ride on the allowed segment) - Origem: execução real da FASE 13b
RN-070 — Todo gate declarado aponta para a evidência que o prova
Nenhuma entrada de config/gates.yml existe sem evidencia, e o registro não
pode afirmar mais do que verifica: um gate block exige verificacao: script, e um gate planned não carrega evidência de algo que ainda não
aconteceu.
Evidência é um localizador, não prosa: event_log traz os tipos de
evento e o filtro de payload que os distingue dos vizinhos; teste e ci
trazem o caminho, e um alvo que sumiu REPROVA. É o mesmo modo de falha que o
docmap chama de glob morto — uma regra que nunca dispara e finge cobertura.
Essa última afirmação é sobre o REPOSITÓRIO, e nunca é avaliada em
runtime. O registro é validado em duas camadas: validarRegistro (o
conteúdo — block precisa de script, os quatro gates humanos continuam
humanos, active precisa de evidência, nenhum id duplicado, nenhuma entrada
órfã) vale onde quer que o arquivo seja lido e é o que o loader da api chama;
validarLocalizadores (o arquivo alvo está em disco) só faz sentido dentro de
um checkout, e é cobrada pelo teste do arquivo real e pela fase 2 do
validacao-gates.ts.
As duas costumavam ser uma função só, e isso custou um defeito de produção,
medido na v6.1.0 instalada: GET /gates devolvia 500 com
RegistroDeGatesInvalido listando os onze alvos como ausentes — doze 5xx em
cerca de 55 minutos, em toda instalação. A imagem de produção leva
/app/config/gates.yml e mais nada; test/, scripts/ci/
e .github/ nunca entram nela. A regra não foi afrouxada — foi movida para
onde a pergunta existe. Não a ponha de volta no loader: arquivoExiste num
processo que não tem repositório responde sobre outra árvore.
A tela escondeu isso: a RN-084 faz o PrGateTimeline cair no
pipeline completo quando a rota falha, então o 500 só aparecia nos logs da
api.
Três tipos porque nem todo gate vive no event log:
merge-protegida é um teto numa regra pura
que não emite evento próprio (o que a garante é um teste) e backmerge é CI
com estado em .release/gate.json. Rebaixá-los a warn por isso mentiria
sobre as travas mais duras do produto.
O filtro importa tanto quanto o tipo: qa-verificada e secops-segura
gravam o MESMO pr.gate_changed, e o mesmo tipo dispara na ABERTURA do gate,
sem veredito — sem o filtro, abrir contaria como passar. O mesmo vale para
os dois gates de PR de infra. É por isso que nenhum par (event_types +
filtro) pode se repetir.
O filtro só alcança o PAYLOAD, de propósito: aceitar uma coluna arbitrária
abriria a consulta inteira. Quem promoveu uma story (humano ou o PO) mora na
coluna actor_kind e fica fora do vocabulário declarativo.
- Onde:
src/domain/gates/gate-registry.ts(validarRegistroevalidarLocalizadores), registrado emconfig/gates.yml, medido emscripts/validacao-gates.ts; o loader que deliberadamente chama só a primeira ésrc/infrastructure/gates/gate-registry.loader.ts - Teste:
test/domain/gates/gate-registry.spec.ts(é válido: nenhum problema acumulado;todo alvo de prova citado existe no repositório;validarRegistro ignora o disco: alvo ausente não é problema DELE;nenhum par (event_types + filtro) se repete entre gates) etest/infrastructure/gates/gate-registry.loader.spec.ts(carrega o registro real sem exigir os alvos que a imagem não leva, contra uma raiz reconstruída como a árvore da imagem de produção) - Origem: FASE 15a (ADR 0054); a divisão em duas camadas veio do 500 de
GET /gatesmedido na v6.1.0 instalada (AT-086)
RN-071 — Os quatro gates de autoridade do usuário não podem ser declarados automáticos
acao-aprovada, story-promovida, plano-de-adocao e merge-protegida
têm aprovacao_humana: true por construção. A lista mora no DOMÍNIO
(GATES_HUMANOS_IMUTAVEIS), não no teste: mexer nela tem de ser um ato
deliberado, revisado como código.
aprovacao_humana: true significa que a decisão é do usuário — diretamente
por um clique, ou delegada por uma política que ele mesmo escreveu no
permissions.json. É isso que permite a acao-aprovada coexistir com
status: auto_approved: a política decidindo sozinha é o usuário tendo
decidido de antemão. merge-protegida é o caso em que nem a delegação
existe — o teto rebaixa auto_approve para require_approval mesmo com a
autonomia ligada.
O contrário também reprova: um id na lista sem gate correspondente é uma regra morta, apontando para nada.
- Onde:
src/domain/gates/gate-registry.ts(GATES_HUMANOS_IMUTAVEIS); o teto emsrc/domain/actions/decide.ts - Teste:
test/domain/gates/gate-registry.spec.ts(%s cannot have aprovacao_humana false); o teto emtest/domain/actions/decide.spec.ts - Origem: FASE 15a (ADR 0054)
RN-072 — Sem escolha explícita, o modelo é o do agente Criativo
Quando a cascata de bindings pousa no default do workspace — isto é, ninguém decidiu nada para este projeto — o modelo herdado é o do agente Criativo, não o default global.
O agente Criativo é sempre a porta de entrada de um projeto: é com ele que a primeira conversa acontece, e o binding dele representa "o modelo que este projeto usa para pensar".
A herança preenche a lacuna, nunca sobrescreve: bindings de sessão, de agente ou de projeto são escolhas explícitas de alguém e continuam vencendo. É por isso que é um passo DEPOIS da cascata e não um novo escopo dentro dela — não disputa precedência. E o modelo herdado passa pelos mesmos filtros: sumido do catálogo ou sem tool calling significa que não é herdado, pelo mesmo motivo que a cascata os pula (RN-043).
O que isto conserta: o default do workspace é global e tende a ser um modelo
local pequeno. Uma sessão nova e um dev agent — que não têm binding próprio —
nasciam nele, e o ADR 0020 proíbe
um modelo local pequeno no passo semântico. Numa execução real foi preciso
trocar o modelo à mão em cada sessão aberta, e os três dev agents subiram em
llama3.2:1b sem que ninguém tivesse pedido.
- Onde:
src/domain/llm/binding-resolver.ts(herdarModeloDeStart), aplicado emapplication/use-cases/llm/resolve-model-binding.use-case.ts - Teste:
test/domain/llm/binding-resolver.spec.ts(fills the gap;does NOT override %s's explicit choice) - Origem: achados B e O da execução real (FASE 13c, fase A)
RN-075 — Comando de terminal é avaliado por onde toca, não só pelo verbo
A pasta do projeto (<PROJECT_WORKSPACES_ROOT>/<workspace_dir_name> — o
UUID puro num projeto de antes do RN-109, <slug>-<8 chars do id> num projeto novo) é o escopo.
Um comando de terminal que toca qualquer caminho fora dela nunca é
auto-aprovado, por mais que o verbo esteja em allow. Dentro dela, cd deixa
de exigir permissão — ele é a declaração de escopo, não um verbo.
Quatro propriedades que os testes fixam:
- Aperta:
Terminal(cat)liberado deixa de auto-executarcat /workspace/apps/engine/.../git_executor.ex. Era o achado U: o casamento é por VERBO, então o agente lia o código da plataforma que o executava, e alcançava o worktree de outros projetos. - Afrouxa:
cd <dentro> && cat README.mdviraauto_approve. Era o defeito mais caro da escada — o dev agent emite semprecd <caminho> && <verbo>,cdnão estava emallownenhum, e comando composto exige que TODOS os segmentos casem. - Permite sem isentar: dentro do escopo, verbo fora do
allowcontinua pedindo. Estar na pasta do projeto não tornacurl … | shseguro. - Fora do escopo é
require_approval, nuncadeny: o agente pode ter razão legítima para olhar fora, e a decisão continua sendo do usuário.
deny continua vencendo primeiro, e os dois tetos (RN-006,
RN-007) seguem intocados. Sem raiz informada ao decide(), o
veredito é o de antes desta regra — nenhum chamador tem comportamento alterado
por omissão.
A normalização é léxica, não realpath: <raiz>/../.. é resolvido e
reprovado, mas link simbólico de dentro apontando para fora não é detectado.
decide() é puro por contrato e resolver symlink exigiria IO no domínio.
Escopo é política; isolamento é outro problema, declarado em aberto no ADR.
Sem regex sobre a entrada, de propósito. Tirar as barras finais da raiz era
.replace(/\/+$/, ''), e o CodeQL apontou ReDoS polinomial
(js/polynomial-redos, HIGH): o padrão obriga o motor a tentar cada posição
inicial e varrer até o fim, degradando em O(n²). Hoje é varredura O(n),
equivalente inclusive no caso degenerado — a raiz / vira string vazia nos
dois, e é isso que faz startsWith('/') valer para todo caminho absoluto.
Quem for "simplificar" de volta para regex reabre o alerta.
- Onde:
src/domain/actions/path-scope.ts,domain/actions/decide.ts(teto do escopo e ocdno escopo), raiz derivada eminfrastructure/filesystem/project-workspaces-root.ts - Teste:
test/domain/actions/path-scope.spec.tsetest/domain/actions/decide.spec.ts(describedecide — escopo de caminho) - Origem: ADR 0055, achado U, Fase F do backlog
O escopo só vale enquanto ele próprio estiver dentro da raiz — quem garante isso é a RN-092.
RN-078 — Falha em proteger branches pode ser reconhecida, e só ela
protect_branches falha em repositório privado no plano gratuito do GitHub — e
o wizard avisa isso antes de começar. O usuário pode reconhecer a falha e
seguir; o bootstrap fecha e o projeto passa a ser alcançável.
O que isso destrava é maior do que parece. O único botão oferecido depois da
falha era "Tentar novamente", que falha sempre pelo mesmo motivo. E
provision_failed faz o dashboard redirecionar o clique do projeto de volta
para a página de provisionamento — o projeto ficava inalcançável para sempre,
preso num passo que não tem como suceder.
Só a proteção pode ser reconhecida. Ela é o ÚLTIMO passo e a única cuja falha deixa um repositório utilizável: o repo existe, os arquivos foram commitados, as branches foram criadas. Falhar em criar o repositório ou em commitar é outra coisa — ali "seguir" produziria um projeto sem onde trabalhar, e o botão seria uma segunda mentira em cima da primeira. A recusa diz isso, em vez de só negar.
A garantia do produto não muda. A trava de merge (RN-006) é
aplicada em decide.ts, não pela proteção do provider. Seguir sem ela remove a
segunda camada, a do GitHub — não a do Brabo. É o que torna esta saída honesta
em vez de um atalho.
A decisão vai para o event log com o usuário como ator e o erro original no payload: seguir sem proteção é escolha dele, e quem ler depois precisa saber o que exatamente foi dispensado.
- Onde:
src/application/use-cases/git/acknowledge-protection-failure.use-case.ts, rota eminterfaces/http/git/git.controller.ts, botão emapps/web/src/routes/ProvisioningPage.tsx - Teste:
test/application/use-cases/git/acknowledge-protection-failure.use-case.spec.ts(destrava; a decisão no log com o ator; e a recusa para falha anterior) - Origem: achado D, Fase D do backlog
RN-081 — História repetida: título igual recusa, justificativa igual avisa
Duas respostas diferentes para dois problemas diferentes:
- título idêntico no projeto é erro, não escolha: a história é recusada e nada é criado;
- mesma justificativa — todas as regras de negócio que a história cita já
estavam cobertas por outra — é suspeita, não erro. A história é criada e
sai um
backlog.story_overlap_warned. Um segundo recorte da mesma regra pode ser legítimo, então quem julga é o usuário; o produto só se recusa a deixar passar despercebido.
Contido, não intersecção. Duas histórias compartilharem uma regra é normal, e avisar disso viraria ruído que ninguém lê. O sinal só existe quando a nova não acrescenta cobertura nenhuma. História que não cita regra alguma não gera aviso: tratar o conjunto vazio como subconjunto de tudo acusaria todas.
O limite é o mesmo da RN-080, e o par do achado o atravessa: "Endpoint público de saudação determinística" e "Endpoint público GET /hello que responde saudação imediata" cobrem o mesmo endpoint com títulos e justificativas diferentes — nada mecânico os liga, e eles continuam passando. Há teste afirmando isso, para o limite ficar visível em vez de implícito. Desde a RN-681 o par é AVISADO por embedding — continua passando, e o teste que afirma o limite DESTE mecanismo segue valendo.
- Onde:
src/domain/backlog/story-overlap.ts,src/application/use-cases/backlog/create-story.use-case.ts - Teste:
test/domain/backlog/story-overlap.spec.ts,test/application/use-cases/backlog/create-story.use-case.spec.ts - Origem: achado R, Fase E do backlog
RN-082 — A credencial de git de uma ação é a do OWNER do workspace
Quando a api executa uma ação de git contra provider remoto (pr_open,
git_merge), o token vem do owner do workspace — o mesmo resolvedor da
RN-058, não de quem decidiu a ação.
Resolver por quem decidiu só funcionava com clique humano. Ação
auto-aprovada por política não tem decisor: decided_by fica NULL, o token
fica undefined, e o GitHub responde Requires authentication. Na prática,
com autonomia ligada — que é o modo que o ADR 0055 existe para viabilizar —
nenhum dev agent conseguia abrir PR em provider remoto.
O contraste que expôs o defeito aconteceu dentro de uma execução só: no
mesmo run, git_push passou e pr_open falhou. O push é executado pelo
ENGINE, que já injetava a credencial do owner
(RN-076); a PR é aberta pela API, que estava fora de simetria.
Não apareceu antes porque toda validação anterior usou o LocalGitProvider,
onde o token nem é consultado.
O princípio é o mesmo da RN-058, e vale repetir porque é o que impede as duas regras de divergirem com o tempo: quem banca a conta banca os agentes, e isso não muda conforme quem clica. Por isso o resolvedor é REUSADO em vez de reimplementado.
- Onde:
src/application/use-cases/actions/execute-git-action.use-case.ts, reusandoapplication/use-cases/llm/resolve-credential-owner.use-case.ts - Teste:
test/application/use-cases/actions/execute-git-action.use-case.spec.ts(pr_openauto-aprovado, comdecidedBy: null, pede a credencial do owner) - Origem: achado AA, validação real da 13b
RN-083 — O lead decide o paralelismo; acima do teto, você autoriza
Quantos agentes sobem deixa de ser um número no código: quem avalia é o lead
da área. Mas a decisão dele não é soberana sobre GASTO — até
agent_areas.max_parallel (nasce com 5 no dev, RN-910) ele sobe e segue; acima disso vira
proposed_action do tipo parallelize, pelo mesmo pipeline de toda ação com
efeito externo.
O teto é da SESSÃO, não do módulo. É a única parte da regra que não é óbvia, e a que um refactor desatento desfaz: contar por módulo permitiria N módulos × 2 agentes sem autorização nenhuma — o buraco anterior com outro nome. Há teste afirmando exatamente isso.
Teto zero ou negativo é configuração inválida, não "sem limite". Tratá-lo como ilimitado transformaria um erro de digitação em gasto irrestrito, que é o oposto do que o pipeline existe para fazer.
Quem PEDE é o lead; quem DECIDE é você. A proposed_action nasce com o
lead como ator, e a decisão fica no event log com o seu nome — é essa distinção
que faz a história ser reconstituível depois (ADR 0048).
O motivo viaja no payload IMUTÁVEL, com os três números (quantos há, quantos pede, qual o teto): quem ler daqui a seis meses precisa entender o que foi autorizado sem reconstruir o estado da sessão.
AcceptParallelizationUseCase não muda: ele continua sendo quem EXECUTA, tanto
no caminho direto quanto quando a ação é aprovada. Absorvê-lo por dentro, em vez
de reescrevê-lo, é o que mantém a suite da Fase 4 verde sem modificação — que é
a prova de que a troca não vazou para o contrato externo.
O teto é configurável por área, e só por você. PATCH /projects/:projectId/agent-areas/:key/max-parallel exige maintainer — o mesmo
papel de ativar a execução, e pelo mesmo motivo: mudar o teto é decidir quanto o
produto pode gastar sem perguntar. Não existe caminho automático de subi-lo. A
Anamnese pode PROPOR, quando notar que a autorização virou rotina, e a proposta
continua passando por esta rota depois que você aceita — um produto que eleva o
próprio teto de gasto é exatamente o que o pipeline de aprovação existe para
impedir.
Mudar o teto vale para os PRÓXIMOS pedidos. O que já está aguardando decisão continua aguardando: a ação carrega no payload o teto vigente quando foi criada, e reinterpretá-la sob o teto novo mudaria o que você está prestes a decidir depois de ler.
- Onde:
src/domain/execution/paralelismo.ts(a regra pura),application/use-cases/execution/request-parallelization.use-case.ts,application/use-cases/execution/set-area-max-parallel.use-case.ts, exposto eminterfaces/http/execution/execution.controller.tse configurado emapps/web/src/routes/settings/ParallelismSection.tsx - Teste:
test/domain/execution/paralelismo.spec.ts,test/application/use-cases/execution/request-parallelization.use-case.spec.ts,test/application/use-cases/execution/set-area-max-parallel.use-case.spec.tseapps/web/src/routes/ProjectSettingsTab.test.tsx(ParallelismSection) - Gate:
config/gates.yml(paralelismo-autorizado) —status: activedesde a auditoria fluxo.yml × código (achado A1/B5); o mecanismo em si não mudou, só o registro que ficouplannedpor engano desde a FASE 14d, ver gates.md - Origem: ADR 0053, FASE 14d
RN-084 — A esteira exibida deriva do registro de gates
O painel do time mostra a etapa em que uma PR está derivando-a de
config/gates.yml, não de uma lista escrita na tela. Gate que sai do registro
some da esteira; gate planned nunca aparece.
A regra existe por uma forma específica de envelhecimento. Antes da FASE
15b a tela tinha as etapas fixas no componente, e o registro (FASE 15a)
descrevia os gates em outro lugar. Nada ligava os dois: acrescentar um gate ao
YAML não mudava a tela, e remover um deixava a tela mostrando uma etapa que já
não existia — sem nenhum teste falhar, porque as duas fontes estavam certas
cada uma por si. É o mesmo apodrecimento que o docs/.docmap.yml existe para
impedir, e a resposta é a mesma: uma fonte só, com o consumo cobrado.
Três decisões de borda, todas para a tela degradar em vez de sumir:
-
gate de PR que a tela ainda não sabe desenhar é ignorado, não quebra o render — o registro pode ganhar um gate antes de a tela ganhar o rótulo;
-
os rótulos são de tela, não do registro: o YAML descreve engenharia, e a tela fala com quem espera uma PR;
-
sem registro (a rota falhou), mostra a esteira completa em vez de vazia — uma esteira genérica informa mais que nada.
-
Onde:
apps/web/src/components/PrGateTimeline.tsx(etapasDaEsteira), lendoGET /gates(src/interfaces/http/gates/gates.controller.ts) -
Teste:
apps/web/src/components/PrGateTimeline.test.ts(gate que SAI do registro some da tela,gate de PR que a tela ainda não sabe desenhar é IGNORADO,sem registro, mostra a esteira completa) -
Origem: ADR 0054, FASE 15b
RN-086 — Gastar com mais agentes nunca se auto-aprova
As duas ações que mexem em quanto o produto pode gastar sozinho —
parallelize (ultrapassar o teto agora) e raise_max_parallel (mudar o teto)
— nunca são auto-aprováveis. Nem por agent_autonomy, nem por
permissions.json. É a mesma classe de garantia da trava de merge e do teto do
patch de instrução.
Sem isto o teto da RN-083 seria decorativo. Um permissions.json
com Parallelize() no allow faria toda ultrapassagem se aprovar sozinha — a
regra que existe para EXIGIR a decisão do usuário passaria a dispensá-la. E
raise_max_parallel é o caso mais grave: seria o produto elevando o próprio
limite de gasto, exatamente o que o pipeline de aprovação existe para impedir.
A Anamnese pode PROPOR, e é isso que ela faz. Quando autorizar mais um
agente vira rotina, o teto está errado, e quem percebe primeiro é quem lê o
histórico. O sinal já chegava a ela: as decisões do usuário na janela vêm de
proposed_actions, com actionType e status.
O limiar é três aprovações e nenhuma negação, e as duas metades importam:
- duas não são rotina — são duas. Três é o que separa "aconteceu" de "está acontecendo sempre";
- uma negação derruba o sinal inteiro, por mais aprovações que haja. Se o usuário recusou alguma vez, o teto está fazendo o trabalho dele, e propor subi-lo seria ler o sinal ao contrário.
Propor um teto igual ou menor que o vigente é recusado pela api: a Anamnese roda periodicamente e reproporia a mesma coisa a cada rodada, enchendo de ruído uma fila que o usuário precisa ler.
Aprovar aplica o valor do payload, não um recalculado na hora — é o número que você leu ao decidir.
- Onde:
src/domain/actions/decide.ts(o teto),application/use-cases/execution/propose-max-parallel.use-case.ts,execute-max-parallel-raise.use-case.ts,apps/engine/lib/engine/anamnese/tools/propose_max_parallel.exe o limiar emapps/engine/lib/engine/workers/anamnese_worker.ex(nota_de_paralelismo/1) - Teste:
test/domain/actions/decide.spec.ts(decide — teto do paralelismo),test/application/use-cases/execution/propose-max-parallel.use-case.spec.ts,execute-max-parallel-raise.use-case.spec.ts,apps/engine/test/engine/anamnese/tools_test.exsetest/engine/workers/anamnese_worker_test.exs(duas aprovacoes NAO sao rotina,uma NEGACAO derruba o sinal) - Origem: ADR 0053, FASE 14d
Painel de projetos
RN-090 — O dashboard lê o workspace, não um projeto de cada vez
A grade de cards e os dots da barra lateral se alimentam de uma requisição
por ciclo — GET /workspaces/:workspaceId/projects-summary —, e o número de
requisições não cresce com a quantidade de projetos.
O card mostra provedor de git, status de provisionamento, orçamento, chips de
agentes e última atividade; a sidebar mostra o dot de status e o contador de
não lidos. Tudo isso vinha de sete consultas em POLL por projeto, o que fazia
23 projetos custarem 3.824 requisições por minuto contra um limite de 300
(RateLimitGuard) — a tela derrubava a si mesma em 429 antes de terminar de
carregar. A RN-088 fez a app parar de insistir quando isso acontece;
esta reduz o pedido.
Duas fronteiras que a regra fixa, e que não são detalhe de implementação:
- A api responde fatos, não roster.
rostertraz o que aconteceu no event log (execução ativada, módulos, gate já aberto, subagentes delegados, infra aceita). Quem é lead de área, que ícone cada agente tem e como os membros viram um chip continua sendo do web — e a regra de PRESENÇA é uma só (rosterFromFacts), compartilhada com o painel do time, para que um agente novo não apareça num lugar e falte no outro. - A gaveta do sino só busca quando aberta. Ela era, até a
RN-091, a única leitura que continuava sendo uma requisição por
projeto — o corte de "onde parei de ler" é
seqguardado no navegador (read-state), que o servidor não conhece. Hoje o navegador manda esse corte, e a gaveta também é uma requisição só.
Efeito colateral aceito e desejado: gatesEverOpened e delegatedSubagents
passam a cobrir a sessão INTEIRA. O cliente derivava dos últimos 200 eventos e
perdia chips em sessão longa (ver
ADR 0021) — o texto de
deriveAgentRoster sempre disse "já abriu alguma vez".
- Onde:
src/application/ports/projects-summary-repository.port.ts,src/infrastructure/persistence/drizzle/projects-summary.repository.ts,src/interfaces/http/iam/workspaces.controller.ts(getProjectsSummary),apps/web/src/lib/hooks.ts(useProjectsSummary),apps/web/src/lib/agent-status.ts(rosterFromFacts),apps/web/src/routes/Dashboard.tsx,apps/web/src/routes/Shell.tsx - Teste:
apps/web/src/routes/Dashboard.fanout.test.tsx(30 projetos custam o mesmo que 3; nenhum endpoint por projeto é chamado);test/infrastructure/persistence/drizzle/projects-summary.repository.spec.ts(o número de consultas ao banco não cresce com N);apps/web/src/lib/agent-status.test.ts(os fatos da api produzem os mesmos chips que o event log) - Origem: dashboard de 23 projetos medido no navegador — 3.824 req/min antes, 12 depois
RN-091 — O navegador manda onde parou de ler; o sino é uma requisição
Com a gaveta de notificações aberta, o conteúdo dela sai de uma
requisição por ciclo — POST /workspaces/:workspaceId/unread-events — e o
número de requisições não cresce com a quantidade de projetos.
É a metade que faltava da RN-090. Aquela derrubou o dashboard de
3.824 para 12 req/min, mas o sino continuou buscando um projeto de cada vez:
23 projetos com a gaveta aberta mediam 286 req/min contra o limite de 300
(RateLimitGuard). Passava, e sumia com um projeto a mais.
O obstáculo, e por que a saída é esta. Não existe "marcar como lido" no
servidor, de propósito: o corte é um seq por projeto guardado no
localStorage de cada navegador (read-state). Havia duas saídas, e a
diferença entre elas é de PRODUTO:
- parar de repolar enquanto a gaveta está aberta muda a atualidade do que o usuário vê — recusada, ninguém pediu essa troca;
- o navegador mandar o mapa
projeto → afterSeqdevolve exatamente os mesmos dados na mesma cadência. É batelamento puro: nada muda para quem olha a tela, nem a frescura nem o conteúdo.
É POST sem mutar nada, e a api responde 200 (nunca 201) para dizer
isso. O verbo foi escolhido por ser o único com CORPO: são dezenas de pares, e
em query string isso vira URL longa — que proxy trunca — além de pôr id de
projeto do usuário em log de acesso, contra a regra de não passar dado pessoal
por query string.
Três garantias de semântica que a rota fixa, porque batelar leituras com cortes diferentes é onde o erro fácil mora:
- mapa vazio devolve vazio, e sem tocar no banco. "Não perguntei nada" não é "me dê tudo";
- cada projeto respeita o SEU corte —
seqestritamente maior que o dele; - o teto de 50 eventos é por projeto, o mesmo que
GET .../eventsaplica semlimit. Um limite na resposta inteira deixaria o projeto barulhento comer a cota dos calados.
Cursor apontando para projeto de outro workspace é ignorado, não recusado: ele vem do armazenamento local de quem chama e pode ser sobra de um workspace antigo.
- Onde:
src/application/ports/projects-summary-repository.port.ts(unreadEventsForWorkspace),src/infrastructure/persistence/drizzle/projects-summary.repository.ts,src/interfaces/http/iam/workspaces.controller.ts(getUnreadEvents),src/interfaces/http/iam/dto/unread-events.dto.ts,apps/web/src/lib/notifications.ts(useNotificationGroups),apps/web/src/lib/api-client.ts(getUnreadEvents) - Teste:
apps/web/src/routes/Dashboard.fanout.test.tsx(com a gaveta ABERTA: uma requisição, e 30 projetos custam o mesmo que 3);test/infrastructure/persistence/drizzle/projects-summary.repository.spec.ts(o número de consultas ao banco não cresce com N; mapa vazio, corte por projeto, teto por projeto e isolamento de workspace) - Origem: residual medido da RN-090 — 286 req/min com a gaveta aberta num workspace de 23 projetos
RN-092 — O projectId é segmento de caminho, e o escopo nunca sai da raiz
projectScopeRoot() recusa um projectId que não seja segmento de caminho
simples (^[A-Za-z0-9_-]{1,64}$), lançando em vez de montar o caminho.
O motivo é que o id chega de @Param('projectId') sem pipe de validação, e o
Express decodifica o percent-encoding do segmento antes de entregá-lo: um
..%2F..%2Fetc chega como ../../etc, e o join resolveria para fora da raiz
sem reclamar. Os dois consumidores da função sofrem, e o segundo é o grave:
- o
permissions.jsonseria lido e escrito em caminho arbitrário; - o escopo da RN-075 autoriza comando de
terminalsob essa pasta. Um escopo que escapa da raiz é a política de aprovação apontando para o lugar errado — falha de SEGURANÇA, não de arquivo não encontrado.
A checagem é deliberadamente mais larga que UUID (aceita letra, dígito, hífen e sublinhado) para não amarrar o formato do id, e estreita o bastante para que o resultado nunca escape. E fica onde a raiz é derivada, não em cada chamador, pela mesma razão que fez a função existir: as duas derivações têm que concordar, e checagem duplicada é checagem que um dia diverge.
O caminho feliz não muda — todo id real é UUID vindo do banco.
- Onde:
src/infrastructure/filesystem/project-workspaces-root.ts(projectScopeRoot) - Teste:
test/infrastructure/filesystem/project-workspaces-root.spec.ts - Origem: ADR 0058,
alertas
js/path-injectiondo CodeQL
RN-093 — Em produção, a api não sobe com a chave de exemplo do state de OAuth
resolveOauthStateSecret() derruba o boot quando NODE_ENV === 'production'
e GIT_OAUTH_STATE_SECRET está ausente, é igual ao literal de exemplo do
repositório, ou tem menos de 16 caracteres. Fora de produção o default de
desenvolvimento continua valendo.
Essa chave assina o state do OAuth de git, e o state é o único que impede o
callback GET /git/oauth/:provider/callback — rota pública, por necessidade —
de ser forjado. Com a chave conhecida, qualquer um assina um state para
{projectId, userId, provider} à escolha e faz o callback gravar, no projeto
apontado por esse payload, o token de git obtido do provider.
Por que rejeitar o literal, e não só o vazio. O default estava no
.env.example de um repositório open source — é segredo publicado, não segredo
fraco. E o docker-compose.prod.yml o supria como fallback, então no caminho
real de erro a variável estava definida: uma verificação de "não vazia"
passaria por cima do defeito inteiro.
A resolução fica em função única, e não em cada chamador, pela mesma razão da
RN-092 — eram duas cópias do mesmo literal, e cópias divergem.
Divergindo aqui, o callback recusaria todo state legítimo.
- Onde:
src/infrastructure/security/oauth-state-secret.ts(resolveOauthStateSecret), chamada no boot emsrc/main.ts - Teste:
test/infrastructure/security/oauth-state-secret.spec.ts - Origem: ADR 0059
RN-094 — A área de agentes nasce com o projeto
Criar um projeto grava as três áreas (dev, qa, infra) em agent_areas,
com o lead de cada uma e os membros enumeráveis em agent_area_members —
na MESMA transação da criação. Se o seeding falha, o projeto não nasce: projeto
sem área é projeto onde a RN-083 lê tabela vazia e cai no default sem que
ninguém tenha decidido nada.
Isto é a correção de um defeito, não uma capacidade nova. agent_areas
existe desde a FASE 14d e nunca foi gravada — AgentAreaRepository.upsert
tinha teste e não tinha NENHUM chamador. Em produção a tabela estava vazia,
GET /projects/:projectId/agent-areas devolvia [], e os quatro casos de uso
que a leem operavam sobre o nada. É a mesma falha da própria FASE 14d, escrita
no CLAUDE.md: testar a peça não é testar o caminho até ela. Por isso o
teste desta regra entra pelo caso de uso que a rota chama, com repositório
real: um fake aqui provaria exatamente o que já estava provado e quebrado.
Semeia em DOIS lugares, e cada um responde uma pergunta diferente. A
criação do projeto faz a área EXISTIR — a tela de Configurações lê num projeto
que nunca executou. A ativação da execução diz quem são os MEMBROS da área de
dev: um dev-<modulo> por módulo do module_map, que não existia quando o
projeto nasceu. Enquanto não há membros gravados, quem sustenta a regra de
endereçamento (RN-087) é o predicado ehDevDeModulo, que não consulta o banco.
O seeding manda o teto de NASCENÇA, e só no INSERT. Desde a
RN-910 a área nasce com o valor do catálogo (dev 5, qa 4, infra 2),
mas o upsert o grava só quando a linha ainda não existe: a ativação é
repetível, e um teto que você subiu não volta ao valor de nascença em silêncio
— o produto desfazendo a sua decisão. A migração de backfill 0038 faz a área
existir e para aí; quem alinha os projetos existentes é a 0073 (RN-910).
Projetos criados antes disto são cobertos pela migração 0038, com ON CONFLICT DO NOTHING nas duas tabelas. Sem ela, o defeito ficaria corrigido só
para quem começasse do zero.
- Onde:
src/application/use-cases/agents/seed-agent-areas.use-case.ts, chamado porapplication/use-cases/iam/create-project.use-case.tseapplication/use-cases/execution/activate-execution.use-case.ts; lista canônica emsrc/domain/agents/agent-areas.ts; backfill emsrc/db/migrations/0038_wandering_lila_cheney.sql - Teste:
test/application/use-cases/iam/create-project-semeia-areas.spec.ts(o caminho, contra o banco, incluindo a falha que derruba a criação),test/db/agent-areas-backfill.spec.ts(a migração rodada de verdade, duas vezes) etest/application/use-cases/execution/activate-execution.use-case.spec.ts(os membros de dev, e o teto nunca enviado) - Origem: FASE 18, defeito achado na investigação do programa 16–26; a tabela vem do ADR 0053
RN-095 — A leitura de repositório é contida ao projeto e limitada
A superfície de leitura de código (GET /projects/:projectId/code/{tree,file,search}
e o diff de PR) tem duas garantias, e as duas são do mesmo tipo: o produto
recusando fazer o que o cliente pediu.
Contenção. Todo caminho de arquivo vindo do cliente passa por
caminhoDeRepositorioContido(), que ancora o pedido na pasta do projeto e
recusa o que sair dela — ../, absoluto, ou byte NUL. Ela é uma função só, no
mesmo arquivo do projectScopeRoot da RN-092, reusando as primitivas
do escopo de terminal (normalizarCaminho/dentroDoEscopo). Nenhuma rota
valida caminho por conta própria, e é isso que a regra afirma: quatro
implementações da mesma contenção seriam quatro chances de divergir, e o
CLAUDE.md já registra que a decisão foi manter a checagem central e pagar o
preço no painel do CodeQL (barreira em outra função ele não enxerga).
Ela devolve o caminho normalizado, e o chamador usa o que voltou. Devolver o
original permitiria conferir b e mandar a/../b ao provider — a forma mais
comum de a contenção existir e não valer.
O vetor não é "ler o arquivo errado". Em github/gitlab o caminho vira
segmento de URL da API do provider, então um ../ troca de endpoint com a
credencial do owner do workspace na mão (RN-058/RN-082).
Em local ele vira o lado direito de git show <ref>:<path>. A ref é
conferida no mesmo lugar, pelo mesmo motivo, e .. nela é recusado porque para
o git dev..main é intervalo de commits, não revisão.
Limite. Árvore e diff já vêm cortados pelo contrato
(GIT_TREE_ENTRY_LIMIT, GIT_DIFF_FILE_LIMIT, FASE 26a). A busca não: ela
não é operação do contrato — é composta sobre listTree e getFileContent, e
é a única leitura cujo custo cresce com o TAMANHO do repositório em vez do
tamanho do pedido. Três orçamentos a param (diretórios percorridos, arquivos
abertos, casamentos devolvidos), um cache de TTL curto evita repetir as mesmas
chamadas, e truncated diz que o corte aconteceu. Sem eles, um viewer
gastaria a credencial e o rate limit do owner à vontade — a mesma família de
defeito dos 3.824 req/min do dashboard (RN-090).
A indexação de docs tem teto próprio. O GIT_BLOB_MAX_BYTES (512 KiB) vale
para a aba Code e para a busca; a indexação de docs/adr lê cada .md até
RAG_INDEX_FILE_MAX_BYTES (4 MiB), pelo parâmetro maxBytes de
ReadProjectCodeUseCase.file (o default continua o da aba) — RN-1036.
Cortar é sempre visível: toda resposta que pode ter sido cortada diz isso
num campo. filesScanned vai junto na busca porque o custo que ninguém vê é o
que ninguém corrige.
Ler não vira proposed_action. Leitura não é efeito externo, e transformá-la
em ação de aprovação encheria a fila de ruído até ninguém mais ler as de
verdade. O congelamento da fase é o outro lado disso: a aba é só leitura, e
escrita — quando vier — nasce proposed_action.
- Onde:
src/infrastructure/filesystem/project-workspaces-root.ts(caminhoDeRepositorioContido),src/application/use-cases/git/read-project-code.use-case.ts,src/domain/git/git-read-limits.ts,src/domain/git/git-read-cache.ts,src/interfaces/http/git/code.controller.ts - Teste:
test/infrastructure/filesystem/project-workspaces-root.spec.ts(a contenção isolada),test/application/use-cases/git/read-project-code.use-case.spec.ts(o caminho malicioso recusado nas três rotas antes de o provider ser chamado, e cada um dos três orçamentos parando a busca),test/domain/git/git-read-cache.spec.ts - Origem: FASE 26b, item 34 do programa 16–26; a contenção estende a RN-092 (ADR 0058)
Sessão
RN-097 — O tipo da sessão é intenção de criação; a execução continua sendo evento
A sessão nasce consultiva ou criativa, escolhida por quem a abre e
gravada em sessions.kind. A consultiva é só conversa; a criativa é a que
produz — abre a ideação com o agente Criativo e é a única que entra em
execução. O tipo nunca muda: não há rota que o troque.
O risco desta regra não é o campo, é a segunda fonte de verdade. O
produto já sabia dizer se uma sessão está executando, e sabia por outro
caminho: findActiveExecutionSession procura a sessão active que carrega o
evento execution.activated (é o que faz a reativação cair na sessão onde os
dev agents já estão, achado #11 do primeiro dogfooding). As duas convivem sob
uma regra que as impede de brigar:
- O
kindclassifica a INTENÇÃO de criação. Uma sessãocriativaque nunca ativou a execução não é a sessão de execução corrente, e a derivação por evento continua sem olhar okind— se passasse a olhar, toda sessão criativa aberta viraria candidata a receber os dev agents. - O evento classifica o ESTADO de execução, como sempre.
execution.activatednuma sessãoconsultivaé ERRO (409), nunca conversão silenciosa. Este é o ponto que decide qual das duas fontes vence: deixar o evento promover o tipo seria justamente fazê-las escreverem uma por cima da outra.
A guarda mora no funil — AppendSessionEventUseCase — e não no
ActivateExecutionUseCase, porque os dois caminhos que gravam evento (a rota
do usuário e o /internal/* do engine) passam por ele. A leitura extra da
sessão só é paga quando o evento é o de execução. A recusa acontece antes
do incrementSeq: uma tentativa recusada não pode consumir seq, ou a
RN-002 quebraria.
O DEFAULT da coluna é consultiva — o tipo que pode menos: uma linha que
chega por um caminho que não passa pela rota (migration, SQL de manutenção)
não ganha o direito de executar. O backfill da migration faz o contrário,
e de propósito, pelo raciocínio da 0033: no momento em que roda, toda sessão
é anterior à distinção, e algumas são sessões onde há dev agents trabalhando —
acordá-las como consultiva faria a reativação de um projeto em andamento
falhar sem ninguém ter decidido nada.
- Onde:
src/domain/sessions/session-kind.ts:50(podeAtivarExecucao),src/db/schema/sessions.ts:68(a coluna),src/application/use-cases/sessions/append-session-event.use-case.ts:54(a guarda) - Teste:
test/application/use-cases/sessions/session-kind-e-nome.spec.ts - Origem: ADR 0061