Estendendo o Aurum
Este guia mostra os quatro pontos por onde o AurumCode cresce sem mudar o
núcleo: engine de scanner, ferramenta de deliberação, skill e
fonte de contexto (ContextProvider). Para cada um: o contrato exato
(tipos e métodos que existem no código; um teste compara cada nome citado
aqui com os pacotes), o que o produto garante em volta dele e um exemplo
mínimo. O tutorial executável Estendendo o Aurum na prática
roda a engine de exemplo, uma skill e uma ferramenta pedida pelo modelo.
A regra que vale para os quatro: dado nunca é autoridade. O que uma
extensão devolve é evidência (engine) ou contexto (ferramenta, skill, fonte
de contexto). Quem decide o gate é a configuração (.aurumcode.yml e a
política central) e o código; nenhum texto vindo de modelo, skill ou fonte
de contexto liga ou desliga regra, muda severidade ou afrouxa o limiar.
Os nomes abaixo vêm de internal/scanner, internal/deliberation,
internal/review/tools, internal/context/skills e internal/config. A
visão geral dos pacotes está em Arquitetura.
1. Engine de scanner
Uma engine é evidência determinística: seus achados entram no gate com uma origem tipada e o modelo pode comentá-los, nunca removê-los nem rebaixá-los.
O contrato
scanner.Scanneré a engine:scanner.Scanner.Namedevolve o nome registrado escanner.Scanner.Runrecebe umscanner.Requeste devolve umscanner.Reportou um erro.scanner.Requesttraz a árvore (scanner.Request.Root), quem mandou varrer (scanner.Request.Trust:scanner.TrustRepositoryouscanner.TrustPolicy; sob política a engine ignora as supressões do autor), as opções da entrada (scanner.Request.Options), o intervalo de commits revisado (scanner.Request.Range, comscanner.Range.Empty) e o executor de binários (scanner.Request.Command).scanner.Reporttemscanner.Report.Findings,scanner.Report.Completeescanner.Report.Version.Complete: falseé inconclusivo, nunca "zero achados"; a versão entra no digest do cache de veredito.scanner.Findingé um achado (Path,Line,Side,RuleID,Severity,Message);scanner.Finding.ToIssueé a única conversão em achado de revisão, com a origem da engine.scanner.Engineé a entrada do registro:scanner.Engine.Scanner,scanner.Engine.Category(o nome quegate.sourcestambém aceita, comosastousecrets),scanner.Engine.Origin(vazio: o nome) escanner.Engine.Validate, que confere asoptionsquando a configuração é lida.scanner.Engine.TypedOriginé a origem que aparece na linha do gate, na auditoria e no SARIF.scanner.Registeré chamado noinitdo pacote da engine. O registro é fechado: o binário só conhece as engines importadas porinternal/scanner/engines. Umengine:que o registro não tem é erro de configuração (unknown engine), nunca uma engine carregada em tempo de execução.scanner.Executorroda a engine (scanner.Executor.Scan, com o limitescanner.Timeout) e devolve umscanner.Outcome. A regra única de inconclusivo éscanner.FailureReason: erro, binário ausente, saída inválida (scanner.ErrInvalidOutput) ou relatório incompleto viram<categoria>_unavailable,_execution_error,_invalid_outputou_incomplete, escanner.Outcome.Findingsfica vazio. Falha nunca é lida como "limpo".
Ligar na configuração
A engine entra por quality_gates.scanners, uma entrada por engine:
quality_gates:
scanners:
- engine: exemplo
required: false # opcional: vira a ferramenta scanner_exemplo
fail_on: error
gate:
fail_on: [error]
Uma entrada roda a menos que enabled: false. Sob política central,
required: true impede o repositório de remover ou afrouxar a entrada. Com
a deliberação ligada, uma entrada required: false não roda antes do
modelo: é oferecida a ele como ferramenta (seção 2).
Exemplo mínimo
A engine de exemplo (internal/scanner/engines/exemplo) reporta cada linha
da árvore revisada que contém a marca exemplo.Marker (EXEMPLO-ACHADO),
sem binário externo. O pacote não se registra sozinho:
// internal/scanner/engines/exemplo/exemplo.go (resumo)
func Engine() scanner.Engine {
return scanner.Engine{Scanner: Scanner{}, Category: Name, Origin: Name, Validate: validateOptions}
}
func (Scanner) Run(ctx context.Context, req scanner.Request) (scanner.Report, error) {
// percorre req.Root sem .git; cada linha com a marca vira um scanner.Finding
return scanner.Report{Findings: findings, Complete: true, Version: Version}, nil
}
O registro fica num arquivo com tag de build, então o binário padrão não contém a engine de exemplo:
//go:build aurum_exemplo
package engines
func init() { scanner.Register(exemplo.Engine()) }
A imagem com a engine é construída com docker build --build-arg
GO_TAGS=aurum_exemplo (o Dockerfile passa GO_TAGS ao go build -tags).
Uma engine de verdade não precisa de tag: é um pacote em
internal/scanner/<engine> e uma linha de import em
internal/scanner/engines/engines.go. Nada em internal/gate,
internal/config ou cmd muda.
2. Ferramenta de deliberação
Na deliberação o modelo pode pedir ferramentas antes de responder. O código nunca pede uma ferramenta sozinho; o modelo decide pelo que o prompt mostra.
O contrato
deliberation.Tooltem dois métodos:deliberation.Tool.Specdevolve umllm.ToolSpec(nome, descrição e o JSON Schema dos argumentos) edeliberation.Tool.Runrecebe argumentos que já passaram pelo schema (deliberation.ValidateArguments) e devolve umdeliberation.Result.deliberation.Resulttemdeliberation.Result.Content(vai ao modelo, redigido e limitado em tamanho),deliberation.Result.Summary(a linha do transcript) edeliberation.Result.Digest(entra na chave do cache, para um resultado diferente nunca reaproveitar um veredito).- Uma ferramenta só lê: não escreve e não acessa a rede além do que o produto já faz.
deliberation.Limitslimita a conversa:deliberation.Limits.MaxRounds,deliberation.Limits.MaxCostTokensedeliberation.Limits.PerToolTimeout(deliberation.max_rounds,max_cost_tokenseper_tool_timeout_secondsna configuração);MaxCostTokensigual a0não impõe teto de tokens. Estourar um limite é umdeliberation.LimitError: a revisão é inconclusiva (deliberation_limit:<limite>), nunca um parecer.deliberation.Transcripté o registro na auditoria (campodeliberation): ferramentas oferecidas, pedidas e não pedidas, cadadeliberation.Callcom argumentos redigidos, duração e resultado resumido, e o limite estourado.- O manifesto (
tools.Manifest, a partir de cadatools.Offercom seu custo declarado emtools.Offer.Cost) vai ao prompt com o nome exato que o modelo deve usar.
Exemplo mínimo
Toda engine required: false já vira a ferramenta scanner_<engine>
(tools.ScannerTool, nome dado por tools.ScannerToolName): o tutorial usa
scanner_exemplo e o achado conta no gate com origem exemplo, qualquer
que seja a resposta do modelo. Uma ferramenta que não é scanner é código
compilado em internal/review/tools, oferecido em toolOffers
(cmd/aurumcode/review_deliberation.go):
type contarLinhas struct{ root string }
type contarArgs struct {
Path string `json:"path" desc:"arquivo alterado"`
}
func (t contarLinhas) Spec() llm.ToolSpec {
return llm.ToolSpec{Name: "contar_linhas", Description: "Conta as linhas de um arquivo alterado.", Parameters: llm.SchemaOf(contarArgs{})}
}
func (t contarLinhas) Run(ctx context.Context, raw json.RawMessage) (deliberation.Result, error) {
var args contarArgs
if err := json.Unmarshal(raw, &args); err != nil {
return deliberation.Result{}, err
}
n := 0 // ... lê args.Path sob t.root, sem escrever nada
return deliberation.Result{Content: fmt.Sprintf("%d linhas", n), Summary: fmt.Sprintf("%d linhas", n), Digest: args.Path}, nil
}
3. Skill
Skill é Markdown: orientação de revisão escrita pelo time, sem execução de código. Ela chega ao prompt como contexto e, por seção, vira regra citável.
- Em arquivo listado:
review.context.skills: [.aurumcode/skills/<nome>.md]; cada seção## Títulovira a regra<nome>#<titulo>, com piso de severidade opcional (severity: error). - Em diretório:
.aurumcode/skills/<nome>/SKILL.md(skills.DocName), com metadadosname,version,languagesepaths. Sem seletor, a skill fica desligada; ela vale quando todos os critérios declarados casam com um arquivo alterado. - Catálogo em camadas: a política central tem as suas em
<política>/.aurumcode/skills/; no mesmo seletor a da política vence e o repositório recebe um aviso. Sob política, só as seções da política entram no gate. UmSKILL.mdilegível na política é erro de carga; no repositório, aviso. - O texto da skill é dado: nada lê a skill para decidir regra, severidade,
limiar, redação ou custo (
skills.Provideré umconfig.ContextProvider).
Exemplo mínimo (do tutorial):
# Skill de exemplo
## Funcoes pequenas
severity: warning
Uma funcao faz uma coisa so. MARCA-SKILL-EXEMPLO
Detalhes em Prompts, skills e docs e no tutorial Skills de convenção.
4. Fonte de contexto (ContextProvider)
config.ContextProvider é a costura comum de todo contexto injetado na
revisão: arquivos, skills e fontes MCP (review.context.mcp, AUR-469).
config.ContextProvider.Nameidentifica a fonte no prompt e nos avisos.config.ContextProvider.Providerecebe os caminhos alterados e devolve texto livre;""significa "nada a contribuir".- Limites: cada chamada roda sob
config.ProviderTimeout(10 s). O texto de uma fonte não tem teto de tamanho: chega inteiro ao modelo, nunca truncado nem recusado por ser grande, e um prompt longo demais para o modelo faz a revisão se dividir em lotes. Uma fonte que falha ou estoura o tempo viraconfig.ProviderWarninge a revisão segue sem ela. O conteúdo curado do próprio repositório ou da política (prompt, instruções, skills e docs) se declara comconfig.CuratedProvider; uma fonte sua só deve fazê-lo se ler arquivos que o dono do repositório controla, nunca texto de fora. config.BuildContextBlockWithWarningsmonta o bloco, redigindo segredos em cada contribuição e de novo no bloco inteiro.- O texto é dado, nunca autoridade: só entra no prompt como contexto.
type notasDoTime struct{ texto string }
func (n notasDoTime) Name() string { return "notas do time" }
func (n notasDoTime) Provide(ctx context.Context, changedPaths []string) (string, error) {
if len(changedPaths) == 0 {
return "", nil
}
return n.texto, nil
}
O lugar do MCP. Um servidor MCP entra como fonte de contexto (AUR-469):
internal/context/mcp.Source é um config.ContextProvider cujo Provide
inicia o servidor declarado em review.context.mcp (só configuração
confiável: a política central, o config local no --base e, no --pr, o
config lido na base, nunca o da head), chama a única ferramenta declarada
com só o payload declarado e redigido, dentro de timeout_seconds (no
máximo config.ProviderTimeout), e devolve o texto com origem
mcp:<nome>/<ferramenta>. Ele informa o modelo; não vira ferramenta que
decide o gate nem engine de scanner. O servidor é um processo externo, como o
binário de um scanner: nada é carregado no binário do Aurum.
O que NÃO é ponto de extensão
- Plugin dinâmico. Não existe carga de código em tempo de execução: nem
.so, nem script de skill. Uma engine ou ferramenta nova é código compilado no binário, revisado como o resto. O binário de um scanner e o servidor de uma fonte MCP são processos externos que só devolvem dados. - Texto do modelo alterando o gate. A severidade que o modelo escreve não
é confiável; uma skill, uma fonte de contexto ou uma resposta de ferramenta
não liga regra, não rebaixa achado e não afrouxa
fail_on. - MCP como autoridade. MCP é contexto (AUR-469), nunca fonte de achado do gate nem de configuração.
- Engine fora do registro.
engine:com nome que o binário não contém é recusado ao ler a configuração; o tutorial mostra o binário padrão recusandoengine: exemplo.
Como fica
Capturas da execução gravada do tutorial Estendendo o Aurum na prática, geradas por scripts/docs/capturas.sh a partir de demo/tutoriais/extensao/out/.
A engine de exemplo reprovando o gate, no terminal e no comentário do PR:


A skill entrando no prompt e a ferramenta de deliberação pedida pelo modelo:


O binário padrão recusando a engine que não contém:
