Uanderson Silva commited on
Commit
2c8beb8
·
1 Parent(s): 004e1f0

improve reflection loop and add cache

Browse files
src/agents/auditor/agent.ts CHANGED
@@ -24,11 +24,12 @@ import {
24
  GATHER_CONTEXT_PROMPT,
25
  JUDGE_FINDINGS_PROMPT,
26
  RANK_FILES_PROMPT,
 
27
  } from "./prompts.ts";
28
  import { AuditorState, CandidateFindingSchema, FileRankingSchema, JudgeReviewSchema } from "./state.ts";
29
  import { buildRepoTree } from "./tools/repo-tree/tool.ts";
30
  import { analyzeSolidityFile } from "./tools/solidity-analyzer/tool.ts";
31
- import { matchLines } from "./utils.ts";
32
 
33
  const llmHaiku = createLLM("anthropic", { model: "claude-haiku-4-5", maxTokens: 20000 });
34
  const llmOpus = createLLM("anthropic", { model: "claude-opus-4-8", temperature: null, maxTokens: 20000 });
@@ -85,9 +86,9 @@ const defineScope: GraphNode<typeof AuditorState> = async (state) => {
85
  const rankingModel = llmHaiku.withStructuredOutput(RankFilesSchema);
86
 
87
  const { rankings } = await rankingModel.invoke([
88
- new SystemMessage(RANK_FILES_PROMPT),
89
  new HumanMessage(
90
- `File tree:\n\`\`\`\n${fileTree}\n\`\`\`\n\nSolidity files to rank:\n${solFiles.map((f) => `- ${f}`).join("\n")}`,
91
  ),
92
  ]);
93
 
@@ -136,21 +137,24 @@ const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
136
  const parts: string[] = [];
137
 
138
  if (docEntries.length > 0) {
139
- parts.push("## Documentation\n");
140
  for (const { filePath, content } of docEntries) {
141
  parts.push(`### ${filePath}\n${content}`);
142
  }
143
  }
144
 
145
- parts.push(`## File Tree\n\n\`\`\`\n${state.fileTree}\n\`\`\``);
146
 
147
- parts.push("## Structural Analysis\n");
148
  for (const { analysis } of solidityEntries) {
149
  parts.push(analysis);
150
  }
151
 
152
  const model = llmHaiku.withStructuredOutput(z.object({ context: z.string() }));
153
- const result = await model.invoke([new SystemMessage(GATHER_CONTEXT_PROMPT), new HumanMessage(parts.join("\n\n"))]);
 
 
 
154
 
155
  const fileTreeBlock = `## Árvore de Arquivos\n\n\`\`\`\n${state.fileTree}\n\`\`\``;
156
  const structuralBlock = `## Análise Estrutural dos Contratos\n\n${solidityEntries.map(({ analysis }) => analysis).join("\n\n---\n\n")}`;
@@ -166,20 +170,14 @@ const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
166
  const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
167
  const model = llmOpus.withStructuredOutput(z.object({ findings: z.array(CandidateFindingSchema) }));
168
 
169
- const previousFeedback =
170
- state.judgeReviews.length > 0
171
- ? state.judgeReviews
172
- .map((r, i) => {
173
- const title = state.candidateFindings[i]?.title ?? `Finding ${i + 1}`;
174
- return `- "${title}": ${r.isFalsePositive ? "FALSE POSITIVE" : "TRUE POSITIVE"}\n Judge: ${r.review}`;
175
- })
176
- .join("\n")
177
- : null;
178
 
179
  logger.info(
180
  `findVulnerabilities: invoking LLM for ${state.scope.length} file(s) in parallel (iteration ${state.reflectionCount + 1})`,
181
  );
182
 
 
 
183
  const allFindings = await Promise.all(
184
  state.scope.map(async (filePath) => {
185
  let source: string;
@@ -190,16 +188,23 @@ const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
190
  }
191
  if (!source) return [];
192
 
193
- let userMessage = `Contract (${filePath}):\n\n${source}\n\nProtocol Context:\n${state.repoContext}`;
194
- if (previousFeedback) {
195
- userMessage += `\n\nJudge feedback from previous iteration (iteration ${state.reflectionCount}):\n${previousFeedback}\n\nRevise your findings accordingly.`;
196
- }
 
 
 
 
 
 
 
197
 
198
  logger.debug(`findVulnerabilities: processing ${filePath}`);
199
 
200
  const result = await model.invoke([
201
- new SystemMessage(FIND_VULNERABILITIES_PROMPT),
202
- new HumanMessage(userMessage),
203
  ]);
204
 
205
  return result.findings.map((finding: any) => ({
@@ -231,6 +236,8 @@ const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
231
 
232
  logger.info(`judgeFindings: reviewing ${state.candidateFindings.length} candidate finding(s) in parallel`);
233
 
 
 
234
  const reviews = await Promise.all(
235
  state.candidateFindings.map(async (finding, i) => {
236
  let source: string;
@@ -240,14 +247,17 @@ const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
240
  source = "";
241
  }
242
 
243
- const findingText = `[Finding ${i + 1}] ${finding.title}\nSeverity: ${finding.severity}\nDescription: ${finding.description}\nLocation: ${finding.path} lines ${finding.location}\nCode:\n\`\`\`solidity\n${finding.codeSnippet}\n\`\`\``;
244
 
245
  logger.debug(`judgeFindings: reviewing finding ${i + 1}: ${finding.title}`);
246
  return model.invoke([
247
- new SystemMessage(JUDGE_FINDINGS_PROMPT),
248
- new HumanMessage(
249
- `Contract (${finding.path}):\n\n${source}\n\nProtocol Context:\n${state.repoContext}\n\nFinding to Review:\n\n${findingText}`,
250
- ),
 
 
 
251
  ]);
252
  }),
253
  );
 
24
  GATHER_CONTEXT_PROMPT,
25
  JUDGE_FINDINGS_PROMPT,
26
  RANK_FILES_PROMPT,
27
+ REFINE_VULNERABILITIES_PROMPT,
28
  } from "./prompts.ts";
29
  import { AuditorState, CandidateFindingSchema, FileRankingSchema, JudgeReviewSchema } from "./state.ts";
30
  import { buildRepoTree } from "./tools/repo-tree/tool.ts";
31
  import { analyzeSolidityFile } from "./tools/solidity-analyzer/tool.ts";
32
+ import { buildReviewBlocks, matchLines } from "./utils.ts";
33
 
34
  const llmHaiku = createLLM("anthropic", { model: "claude-haiku-4-5", maxTokens: 20000 });
35
  const llmOpus = createLLM("anthropic", { model: "claude-opus-4-8", temperature: null, maxTokens: 20000 });
 
86
  const rankingModel = llmHaiku.withStructuredOutput(RankFilesSchema);
87
 
88
  const { rankings } = await rankingModel.invoke([
89
+ new SystemMessage({ content: [{ type: "text", text: RANK_FILES_PROMPT, cache_control: { type: "ephemeral" } }] }),
90
  new HumanMessage(
91
+ `Árvore de arquivos:\n\`\`\`\n${fileTree}\n\`\`\`\n\nArquivos Solidity para classificar:\n${solFiles.map((f) => `- ${f}`).join("\n")}`,
92
  ),
93
  ]);
94
 
 
137
  const parts: string[] = [];
138
 
139
  if (docEntries.length > 0) {
140
+ parts.push("## Documentação\n");
141
  for (const { filePath, content } of docEntries) {
142
  parts.push(`### ${filePath}\n${content}`);
143
  }
144
  }
145
 
146
+ parts.push(`## Árvore de Arquivos\n\n\`\`\`\n${state.fileTree}\n\`\`\``);
147
 
148
+ parts.push("## Análise Estrutural\n");
149
  for (const { analysis } of solidityEntries) {
150
  parts.push(analysis);
151
  }
152
 
153
  const model = llmHaiku.withStructuredOutput(z.object({ context: z.string() }));
154
+ const result = await model.invoke([
155
+ new SystemMessage({ content: [{ type: "text", text: GATHER_CONTEXT_PROMPT, cache_control: { type: "ephemeral" } }] }),
156
+ new HumanMessage(parts.join("\n\n")),
157
+ ]);
158
 
159
  const fileTreeBlock = `## Árvore de Arquivos\n\n\`\`\`\n${state.fileTree}\n\`\`\``;
160
  const structuralBlock = `## Análise Estrutural dos Contratos\n\n${solidityEntries.map(({ analysis }) => analysis).join("\n\n---\n\n")}`;
 
170
  const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
171
  const model = llmOpus.withStructuredOutput(z.object({ findings: z.array(CandidateFindingSchema) }));
172
 
173
+ const isReflection = state.judgeReviews.length > 0;
 
 
 
 
 
 
 
 
174
 
175
  logger.info(
176
  `findVulnerabilities: invoking LLM for ${state.scope.length} file(s) in parallel (iteration ${state.reflectionCount + 1})`,
177
  );
178
 
179
+ const cachedContext = { type: "text" as const, text: `Contexto do Protocolo:\n${state.repoContext}`, cache_control: { type: "ephemeral" as const } };
180
+
181
  const allFindings = await Promise.all(
182
  state.scope.map(async (filePath) => {
183
  let source: string;
 
188
  }
189
  if (!source) return [];
190
 
191
+ const fileEntries = isReflection
192
+ ? state.candidateFindings
193
+ .map((f, i) => ({ finding: f, review: state.judgeReviews[i] }))
194
+ .filter(({ finding }) => finding.path === filePath)
195
+ : [];
196
+
197
+ const isRefinement = fileEntries.length > 0;
198
+ const promptText = isRefinement ? REFINE_VULNERABILITIES_PROMPT : FIND_VULNERABILITIES_PROMPT;
199
+ const contractText = isRefinement
200
+ ? `Contrato (${filePath}):\n\n${source}\n\n${buildReviewBlocks(fileEntries, state.reflectionCount)}`
201
+ : `Contrato (${filePath}):\n\n${source}`;
202
 
203
  logger.debug(`findVulnerabilities: processing ${filePath}`);
204
 
205
  const result = await model.invoke([
206
+ new SystemMessage({ content: [{ type: "text", text: promptText, cache_control: { type: "ephemeral" } }] }),
207
+ new HumanMessage({ content: [cachedContext, { type: "text", text: contractText }] }),
208
  ]);
209
 
210
  return result.findings.map((finding: any) => ({
 
236
 
237
  logger.info(`judgeFindings: reviewing ${state.candidateFindings.length} candidate finding(s) in parallel`);
238
 
239
+ const cachedContext = { type: "text" as const, text: `Contexto do Protocolo:\n${state.repoContext}`, cache_control: { type: "ephemeral" as const } };
240
+
241
  const reviews = await Promise.all(
242
  state.candidateFindings.map(async (finding, i) => {
243
  let source: string;
 
247
  source = "";
248
  }
249
 
250
+ const findingText = `[Achado ${i + 1}] ${finding.title}\nSeveridade: ${finding.severity}\nDescrição: ${finding.description}\nLocalização: ${finding.path} linhas ${finding.location}\nCódigo:\n\`\`\`solidity\n${finding.codeSnippet}\n\`\`\``;
251
 
252
  logger.debug(`judgeFindings: reviewing finding ${i + 1}: ${finding.title}`);
253
  return model.invoke([
254
+ new SystemMessage({ content: [{ type: "text", text: JUDGE_FINDINGS_PROMPT, cache_control: { type: "ephemeral" } }] }),
255
+ new HumanMessage({
256
+ content: [
257
+ cachedContext,
258
+ { type: "text", text: `Contrato (${finding.path}):\n\n${source}\n\nAchado para Revisão:\n\n${findingText}` },
259
+ ],
260
+ }),
261
  ]);
262
  }),
263
  );
src/agents/auditor/prompts.ts CHANGED
@@ -80,14 +80,32 @@ Para cada vulnerabilidade encontrada, forneça OBRIGATORIAMENTE todos os campos
80
 
81
  ## Regra de completude
82
 
83
- Reporte vulnerabilidades mesmo que não sejam imediatamente exploráveis. Vulnerabilidades podem ser de segurança, inconsistências de design, desalinhamentos econômicos ou falhas de observabilidade. Se nenhuma vulnerabilidade for encontrada, retorne um array vazio.
84
 
85
- ## Processamento de feedback de revisão
86
 
87
- Se feedback de uma iteração anterior for fornecido:
88
- - Remova todos os achados marcados como falso positivo com confiança ≥ 80%.
89
- - Para achados marcados como falso positivo com confiança < 80%, reavalie e inclua apenas se houver argumento novo.
90
- - Adicione novos achados se o feedback apontar superfícies de ataque não cobertas.`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
  export const JUDGE_FINDINGS_PROMPT = `Você é um revisor rigoroso de segurança de smart contracts com profundo conhecimento em Solidity, execução EVM e design de protocolos. Avalie cada vulnerabilidade candidata submetida pelo auditor e determine se é um verdadeiro positivo ou um falso positivo.
93
 
 
80
 
81
  ## Regra de completude
82
 
83
+ Reporte vulnerabilidades mesmo que não sejam imediatamente exploráveis. Vulnerabilidades podem ser de segurança, inconsistências de design, desalinhamentos econômicos ou falhas de observabilidade. Se nenhuma vulnerabilidade for encontrada, retorne um array vazio.`;
84
 
85
+ export const REFINE_VULNERABILITIES_PROMPT = `Você é um auditor sênior de segurança de smart contracts refinando seus próprios achados com base no feedback de um revisor especialista independente.
86
 
87
+ Na iteração anterior, você analisou um contrato Solidity e gerou uma lista de vulnerabilidades candidatas. Um revisor especialista avaliou cada achado e forneceu: veredicto (verdadeiro ou falso positivo), análise técnica detalhada, nível de confiança e caminhos de exploit ou razões de bloqueio.
88
+
89
+ **Sua tarefa**: produzir uma lista final e refinada de vulnerabilidades incorporando o feedback do revisor.
90
+
91
+ ## Regras de refinamento
92
+
93
+ 1. **Falso positivo com confiança ≥ 80%**: remova o achado sem exceção.
94
+ 2. **Falso positivo com confiança < 80%**: reavalie com base na análise do revisor. Mantenha apenas se encontrar evidência nova ou argumento técnico que o revisor não considerou — e reflita isso na descrição.
95
+ 3. **Verdadeiro positivo**: mantenha o achado. Incorpore melhorias sugeridas pelo revisor (descrição mais precisa, snippet mais completo, recomendação mais específica, caminhos de exploit detalhados).
96
+ 4. **Novos achados**: se o revisor apontou superfícies de ataque não cobertas em seus achados originais, investigue o código-fonte e adicione novos achados para elas.
97
+ 5. Não adicione achados que não sejam suportados pelo código-fonte ou pelo feedback do revisor.
98
+
99
+ ## Formato de saída
100
+
101
+ Idêntico ao da análise inicial. Para cada vulnerabilidade:
102
+ - **title**: nome curto e preciso
103
+ - **description**: (a) comportamento esperado, (b) comportamento observado, (c) impacto concreto — mínimo 3 frases, máximo 5
104
+ - **recommendation**: correção específica e acionável com referência ao padrão correto
105
+ - **severity**: \`"high"\` / \`"medium"\` / \`"low"\`
106
+ - **codeSnippet**: trecho exato e completo copiado literalmente do código-fonte, sem omissões, reticências ou pseudocódigo
107
+
108
+ Se nenhuma vulnerabilidade restar após o refinamento, retorne um array vazio.`;
109
 
110
  export const JUDGE_FINDINGS_PROMPT = `Você é um revisor rigoroso de segurança de smart contracts com profundo conhecimento em Solidity, execução EVM e design de protocolos. Avalie cada vulnerabilidade candidata submetida pelo auditor e determine se é um verdadeiro positivo ou um falso positivo.
111
 
src/agents/auditor/utils.ts CHANGED
@@ -34,3 +34,49 @@ export const matchLines = (fileContent: string, codeSnippet: string): string | n
34
 
35
  return null;
36
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
  return null;
36
  };
37
+
38
+ type ReviewEntry = {
39
+ finding: {
40
+ title: string;
41
+ severity: string;
42
+ location: string;
43
+ description: string;
44
+ codeSnippet: string;
45
+ };
46
+ review: {
47
+ isFalsePositive: boolean;
48
+ confidence: number;
49
+ review: string;
50
+ exploitablePaths: string[];
51
+ };
52
+ };
53
+
54
+ export const buildReviewBlocks = (fileEntries: ReviewEntry[], iterationCount: number): string => {
55
+ const blocks = fileEntries.map(({ finding, review }, i) => {
56
+ const verdict = review.isFalsePositive
57
+ ? `FALSO POSITIVO (confiança: ${review.confidence}/100)`
58
+ : `VERDADEIRO POSITIVO (confiança: ${review.confidence}/100)`;
59
+ const pathsLabel = review.isFalsePositive ? "Razão de Bloqueio" : "Caminhos de Exploração";
60
+ const pathsContent =
61
+ review.exploitablePaths.length > 0
62
+ ? review.exploitablePaths.map((p) => ` - ${p}`).join("\n")
63
+ : " (nenhum fornecido)";
64
+
65
+ return `[Achado ${i + 1}/${fileEntries.length}] ${finding.title}
66
+ Severidade: ${finding.severity}
67
+ Localização: linhas ${finding.location}
68
+ Descrição: ${finding.description}
69
+
70
+ Código:
71
+ \`\`\`solidity
72
+ ${finding.codeSnippet}
73
+ \`\`\`
74
+
75
+ Veredito do Revisor: ${verdict}
76
+ Análise do Revisor: ${review.review}
77
+ ${pathsLabel}:
78
+ ${pathsContent}`;
79
+ });
80
+
81
+ return `=== Iteração ${iterationCount} — Achados e Revisões do Especialista (${fileEntries.length} achado(s)) ===\n\n${blocks.join("\n\n---\n\n")}`;
82
+ };