Um agente de codificação que recebe uma issue, trabalha num container isolado dentro do GitHub Actions e devolve um pull request em rascunho para revisão humana.
Eu construí este projeto para estudar, na prática, como um agente de código funciona por dentro: como o modelo "enxerga" o resultado de um comando, como impedir que ele faça o que não deve e como transformar isso numa pipeline que qualquer repositório pode usar. Ele é escrito em Java 21 com Spring AI, usa um modelo servido pelo Ollama, e o código está organizado em Clean Architecture.
- Em uma frase
- Como as peças se encaixam
- Guia de leitura
- O que tem neste repositório
- Instalar o agente em um repositório
- Onde o modelo roda
- Segurança
- Rodar e testar localmente
- Problemas que eu já encontrei
- Limitações e próximos passos
A issue é a tarefa, o GitHub Actions é o computador, o container é a mesa de trabalho isolada e o PR em rascunho é a entrega.
Não há servidor próprio para manter, nem token pessoal com permissão de push.
flowchart LR
subgraph GH["GitHub (repositório alvo)"]
I["Issue<br/>a tarefa"]
PR["PR em rascunho<br/>a entrega"]
end
subgraph RUN["Runner do GitHub Actions"]
G1["gitops fetch-issue<br/>(tem token)"]
AR["agent-runner<br/>(sem token)"]
G2["gitops publish<br/>(tem token)"]
subgraph C["Container sem rede"]
REPO["/workspace<br/>o código do projeto"]
end
end
OL["Ollama<br/>o modelo"]
I -->|"lê"| G1
G1 -->|"task.json"| AR
AR <-->|"prompt e tool calls"| OL
AR -->|"docker exec<br/>build, testes"| C
AR -->|"result.json"| G2
G2 -->|"commit, push, PR"| PR
O ciclo do agente é pensa, age, observa:
- o modelo recebe a tarefa e a lista de ferramentas (
read_file,run_command...); - ele pede uma ferramenta, por exemplo
run_command("mvn -o -q test"); - o agent-runner executa no container e devolve
exit_code=1com o erro do teste; - o modelo lê o erro, corrige o arquivo e roda o teste de novo;
- quando termina, chama
finishcom um resumo.
Depois disso, a pipeline roda a verificação do projeto por conta própria. Assim, o revisor do PR não depende do que o modelo afirmou.
Eu separei a documentação por pergunta. Comece pelo que você quer entender:
| Quero entender... | Leia |
|---|---|
| O que cada step da pipeline faz, e como | docs/pipeline.md |
| Quem conversa com quem, em cada cenário (sucesso, erro, bloqueio, limite...) | docs/sequencias.md |
| Em que estado cada coisa está e o que a faz mudar | docs/estados.md |
| Como o código Java está organizado (camadas, classes, como estender) | docs/arquitetura-java.md |
| O que o Python faz (classes, métodos, fluxos) | docs/gitops-python.md |
Os diagramas são em Mermaid, e o GitHub os renderiza direto na página.
coding-agent/
├── .github/workflows/agent.yml # a pipeline (workflow reutilizável)
├── agent-runner/ # o agente, em Java 21 + Spring AI
│ └── src/main/java/com/example/codingagent/
│ ├── domain/ # regras puras: status final, política de comandos, orçamento
│ ├── application/ # o caso de uso e as portas (interfaces)
│ └── infrastructure/ # Spring AI, Docker, arquivos, JSON, configuração
├── scripts/
│ ├── gitops/ # operações de GitHub com token (Python, só biblioteca padrão)
│ ├── tests/ # testes do gitops
│ ├── setup-ollama.sh # Ollama externo ou instalado no runner
│ └── prepare-sandbox.sh # container do projeto, dependências, rede cortada
├── templates/target-repo/ # o que copiar para cada repositório alvo
│ ├── .github/workflows/coding-agent.yml
│ ├── .agent/ (Dockerfile, setup.sh, verify.sh)
│ └── AGENTS.md
└── docs/ # a documentação com os diagramas
-
Copie os templates de
templates/target-repo/para a raiz do repositório alvo e ajuste:Arquivo Para quê Quando roda .github/workflows/coding-agent.ymlO gatilho que chama a pipeline deste repositório No dispatch .agent/DockerfileA imagem com a stack do projeto (precisa ter bashetimeout)Step 8 .agent/setup.shBaixa as dependências Step 8, com internet .agent/verify.shBuild e testes, a verificação independente Step 9, sem internet AGENTS.mdInstruções para o agente: comandos exatos, convenções, o que não mexer Vai no prompt -
Libere o Actions para abrir PRs no repositório alvo: Settings → Actions → General → Workflow permissions, marque Allow GitHub Actions to create and approve pull requests. Sem isso, o push funciona, mas o PR falha com
HTTP 403. -
Opcional: secret
OLLAMA_BASE_URLe variáveisCODING_AGENT_MODELeCODING_AGENT_RUNNER(veja a seção 6). -
Teste à mão: abra uma issue e depois vá em Actions → coding-agent → Run workflow, com o número da issue, um
request_idqualquer (ex.:teste1) e uma branch (ex.:agent/teste1). No fim, confira o PR em rascunho, o comentário na issue e o artefatoagent-result.
Dica: escreva os critérios de aceite na issue como caixinhas (- [ ] responde 200 em /api/health). Elas viram uma lista objetiva no prompt do agente.
O repositório poc-websocket-demo já tem
esses arquivos e serve de exemplo completo.
| Opção | Configuração | Quando usar |
|---|---|---|
| Ollama no próprio runner (padrão) | Nenhuma | Para demonstrar. Roda em CPU: cada resposta pode levar minutos |
| Ollama externo | Secret OLLAMA_BASE_URL |
Uso real, com um servidor com GPU. Coloque autenticação na frente: o Ollama não tem |
| Runner na sua máquina | Variável CODING_AGENT_RUNNER=self-hosted e secret OLLAMA_BASE_URL=http://localhost:11434 |
Usar a sua GPU. A máquina precisa de Docker, Java 21 e Python 3 |
O modelo precisa suportar tools no Ollama (ex.: qwen3, llama3.1). O padrão é
qwen3:4b. Modelos pequenos se perdem em tarefas longas: o agente dá dois lembretes quando o
modelo para de usar ferramentas e para em max_iterations (padrão 30).
A ideia central é: quem conversa com o modelo não tem credencial, e quem tem credencial não conversa com o modelo.
| Risco | Como é tratado |
|---|---|
| Código gerado acessar a internet ou vazar dados | A rede do container é desligada depois do setup.sh |
| Modelo usar o token de Git | O token só aparece nos steps do gitops. O step do agente não o recebe, e o checkout usa persist-credentials: false |
| Modelo sair do repositório | O SafePathResolver barra ../, links simbólicos para fora e a pasta .git |
| Modelo tentar fazer o papel da pipeline | A CommandPolicy bloqueia git push, git commit, sudo etc., com uma mensagem clara |
Push direto na main |
O agente só cria agent/... e o PR nasce em rascunho. Proteja a main com revisão obrigatória |
| Laço infinito ou custo alto | Limites de iterações, de chamadas de ferramenta, de tempo por comando e do job (90 min) |
| Prompt injection na issue ou no código | Regras no prompt de sistema e, principalmente, as barreiras acima |
| Auditoria | journal.jsonl com cada chamada de ferramenta, no artefato agent-result |
# Java: domínio, caso de uso, laço com modelo falso, barreiras de arquivo
cd agent-runner && mvn test
# Python: gitops contra uma API do GitHub falsa e um repositório Git real
python3 -m unittest discover -s scripts/tests -v
# Workflows: pega erros como contexto inválido antes do push
docker run --rm -v "$PWD:/repo" --workdir /repo rhysd/actionlint:latestÚtil para ajustar prompts e ferramentas sem esperar o Actions. Com executor=local, os
comandos rodam direto na sua máquina: use só em repositórios de confiança.
cd agent-runner && mvn -q package -DskipTests
cat > /tmp/task.json <<'EOF'
{"requestId":"dev1","issueNumber":1,"title":"Criar endpoint GET /api/health",
"description":"Responder {\"status\":\"UP\"}","acceptanceCriteria":["GET /api/health responde 200"]}
EOF
OLLAMA_BASE_URL=http://localhost:11434 OLLAMA_MODEL=qwen3:4b \
java -jar target/agent-runner.jar \
--agent.workspace=/caminho/do/repo-alvo \
--agent.task-file=/tmp/task.json \
--agent.output-dir=/tmp/agent-output \
--agent.executor=localPara ter o isolamento completo, rode antes o scripts/prepare-sandbox.sh com WORKSPACE_DIR
e CONTAINER_NAME, e use --agent.executor=docker --agent.container=<nome>.
O GitHub recusou o workflow antes de rodar qualquer coisa. Eu tinha usado ${{ runner.temp }}
no env: do job, mas o contexto runner só existe dentro dos steps. Corrigi definindo os
caminhos num step, via $GITHUB_ENV (step 0 em docs/pipeline.md).
O actionlint pega esse tipo de erro na hora.
Quase sempre não está travada: é o modelo pensando em CPU. O primeiro prompt é grande
(instruções, AGENTS.md, a issue e a descrição das sete ferramentas), o qwen3 gera um
raciocínio longo antes de responder, e o log só mostra algo quando a resposta chega. Cada
iteração pode levar vários minutos no ubuntu-latest. Para acelerar, use um Ollama com GPU
(seção 6).
Se o Ollama estiver realmente fora do ar, o Spring AI tenta de novo várias vezes, esperando
cada vez mais, antes de desistir com status ERROR.
O agente terminou, o push da branch funcionou, mas o GitHub não deixou o GITHUB_TOKEN abrir
o PR. Habilite a opção do item 2 da seção 5 no repositório alvo. Para não perder a execução,
abra o PR à mão a partir da branch agent/..., que já está no GitHub.
- Falhas silenciosas na issue. Se o agente termina com
ERRORou o PR é recusado, a issue fica só com o comentário "comecei". O motivo está no log e no artefato. - PR aberto com
GITHUB_TOKENnão dispara outros workflows (regra do GitHub contra loops). Se o repositório tem CI em PRs, use o token de um GitHub App no step de publicação. - Desempenho no runner padrão. CPU e modelo pequeno servem para mostrar o fluxo, não para medir qualidade.
- Janela de contexto. O histórico cresce a cada volta, e o
num-ctxpadrão é 32768. Um próximo passo é resumir o histórico antigo. - Ajustes pelo PR. Rodar o agente de novo sobre a mesma branch a partir de um comentário.
- Imagem pronta. Publicar o agent-runner no GHCR para não compilar a cada execução.