Uanderson Ricardo commited on
Commit
5cf0f74
·
2 Parent(s): 3ada69242ebf1f

Merge pull request #8 from uandersonricardo/unify-logs

Browse files
frontend/index.html CHANGED
@@ -3,7 +3,27 @@
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Multi-Agent Smart Contracts</title>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  </head>
8
  <body>
9
  <div id="root"></div>
 
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>TALP1 Smart Contract Agents</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap" rel="stylesheet" />
10
+ <style>
11
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
12
+ html, body { height: 100%; }
13
+ body { overflow: hidden; background: #070c10; font-family: 'JetBrains Mono', 'Fira Code', monospace; }
14
+ #root { height: 100%; }
15
+
16
+ ::-webkit-scrollbar { width: 5px; height: 5px; }
17
+ ::-webkit-scrollbar-track { background: transparent; }
18
+ ::-webkit-scrollbar-thumb { background: #21262d; border-radius: 3px; }
19
+ ::-webkit-scrollbar-thumb:hover { background: #30363d; }
20
+ * { scrollbar-width: thin; scrollbar-color: #21262d transparent; }
21
+
22
+ @keyframes spin { to { transform: rotate(360deg); } }
23
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
24
+ @keyframes fadein { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: translateY(0); } }
25
+ @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
26
+ </style>
27
  </head>
28
  <body>
29
  <div id="root"></div>
frontend/src/App.tsx CHANGED
@@ -1,56 +1,46 @@
1
  import { useState, useRef, useCallback } from "react";
2
 
3
- interface Finding {
4
- title: string;
5
- description: string;
6
- recommendation: string;
7
- severity: "high" | "medium" | "low";
8
- codeSnippet: string;
9
- location: string;
10
- path: string;
11
- judgeReview: {
12
- review: string;
13
- confidence: number;
14
- exploitablePaths: string[];
15
- };
16
- }
17
 
18
- interface AgentResult {
19
- contract?: string;
20
- compilationErrors?: string[];
21
- reviewSummary?: string;
22
- findings?: Finding[];
23
- // Tester fields
24
- status?: string;
25
- pocCode?: string;
26
- executionLogs?: string[];
27
- iterations?: number;
28
- }
29
 
30
  export function App() {
31
  const [requirements, setRequirements] = useState("");
32
- const [logs, setLogs] = useState<string[]>([]);
33
- const [coderResult, setCoderResult] = useState<AgentResult | null>(null);
34
- const [auditorResult, setAuditorResult] = useState<AgentResult | null>(null);
35
- const [testerResult, setTesterResult] = useState<AgentResult | null>(null);
36
  const [running, setRunning] = useState(false);
37
- const logsEndRef = useRef<HTMLDivElement>(null);
 
 
 
 
 
 
 
 
 
 
 
38
 
39
  const appendLog = useCallback((msg: string) => {
40
  setLogs((prev) => [...prev, msg]);
41
- setTimeout(() => logsEndRef.current?.scrollIntoView({ behavior: "smooth" }), 50);
42
  }, []);
43
 
44
- const handleSubmit = async (e: React.FormEvent) => {
45
  e.preventDefault();
46
  if (!requirements.trim() || running) return;
47
 
48
  setRunning(true);
49
  setLogs([]);
50
- setCoderResult(null);
51
- setAuditorResult(null);
52
- setTesterResult(null);
53
- appendLog("Iniciando pipeline...");
 
 
 
54
 
55
  try {
56
  const res = await fetch("/api/run", {
@@ -72,30 +62,88 @@ export function App() {
72
 
73
  buffer += decoder.decode(value, { stream: true });
74
  const lines = buffer.split("\n");
75
- buffer = lines.pop() || "";
76
 
77
  for (const line of lines) {
78
  if (line.startsWith("event:")) {
79
  currentEvent = line.slice(6).trim();
80
- console.log("event", currentEvent);
81
  } else if (line.startsWith("data:")) {
82
  const data = line.slice(5).trim();
83
- console.log("data", data);
84
  switch (currentEvent) {
85
  case "log":
86
  appendLog(data);
87
  break;
88
- case "coder":
89
- setCoderResult(JSON.parse(data));
 
90
  break;
91
- case "auditor":
92
- setAuditorResult(JSON.parse(data));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  break;
94
- case "tester":
95
- setTesterResult(JSON.parse(data));
 
 
 
 
 
 
 
 
 
 
 
 
96
  break;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  case "error":
98
- appendLog(`ERRO: ${data}`);
99
  break;
100
  }
101
  currentEvent = "";
@@ -103,328 +151,43 @@ export function App() {
103
  }
104
  }
105
  } catch (err) {
106
- appendLog(` Erro de conexão: ${err instanceof Error ? err.message : String(err)}`);
107
  } finally {
108
  setRunning(false);
109
  }
110
  };
111
 
112
- return (
113
- <div style={styles.container}>
114
- <header style={styles.header}>
115
- <h1 style={styles.title}>Multi-Agent: Geração, Auditoria e Teste de Smart Contracts</h1>
116
- <p style={styles.subtitle}>
117
- Descreva um cenário ou requisito cuja solução seja um smart contract em Solidity. O sistema irá gerar,
118
- compilar, auditar e testar o contrato automaticamente.
119
- </p>
120
- </header>
121
-
122
- <form onSubmit={handleSubmit} style={styles.form}>
123
- <textarea
124
- value={requirements}
125
- onChange={(e) => setRequirements(e.target.value)}
126
- placeholder={
127
- "Ex: Crie um token ERC20 com as seguintes características:\n- Nome: MeuToken, Símbolo: MTK\n- Supply inicial de 1.000.000 tokens\n- Funções de mint (apenas owner) e burn\n- Pausável pelo owner"
128
- }
129
- style={styles.textarea}
130
- rows={6}
131
- disabled={running}
132
- />
133
- <button
134
- type="submit"
135
- disabled={running || !requirements.trim()}
136
- style={{
137
- ...styles.button,
138
- opacity: running || !requirements.trim() ? 0.5 : 1,
139
- }}
140
- >
141
- {running ? "Executando pipeline..." : "Executar Pipeline"}
142
- </button>
143
- </form>
144
-
145
- {/* Logs */}
146
- {logs.length > 0 && (
147
- <section style={styles.section}>
148
- <h2 style={styles.sectionTitle}>📋 Log de Execução</h2>
149
- <div style={styles.logBox}>
150
- {logs.map((log, i) => (
151
- <div key={i} style={styles.logLine}>
152
- {log}
153
- </div>
154
- ))}
155
- <div ref={logsEndRef} />
156
- </div>
157
- </section>
158
- )}
159
 
160
- {/* Coder */}
161
- {coderResult && (
162
- <section style={styles.section}>
163
- <h2 style={styles.sectionTitle}>🔨 Agente Coder</h2>
164
-
165
- <h3 style={styles.subTitle}>Contrato Gerado</h3>
166
- <div style={styles.codeBox}>
167
- <pre style={styles.code}>{coderResult.contract}</pre>
168
- </div>
169
-
170
- {coderResult.compilationErrors && coderResult.compilationErrors.length > 0 && (
171
- <>
172
- <h3 style={{ ...styles.subTitle, color: "#ef4444" }}>Erros de Compilação</h3>
173
- <div style={{ ...styles.codeBox, borderColor: "#ef4444" }}>
174
- <pre style={styles.code}>{coderResult.compilationErrors.join("\n")}</pre>
175
- </div>
176
- </>
177
- )}
178
-
179
- {coderResult.reviewSummary && (
180
- <>
181
- <h3 style={styles.subTitle}>Revisão de Segurança</h3>
182
- <div style={styles.resultBox}>
183
- <p style={styles.resultText}>{coderResult.reviewSummary}</p>
184
- </div>
185
- </>
186
- )}
187
- </section>
188
- )}
189
-
190
- {/* Auditor */}
191
- {auditorResult && (
192
- <section style={styles.section}>
193
- <h2 style={styles.sectionTitle}>🔍 Agente Auditor</h2>
194
- {auditorResult.findings && auditorResult.findings.length > 0 ? (
195
- auditorResult.findings.map((f, i) => (
196
- <div key={i} style={{ ...styles.findingCard, borderColor: severityColor(f.severity) }}>
197
- <div style={styles.findingHeader}>
198
- <span style={{ ...styles.severityBadge, background: severityColor(f.severity) }}>
199
- {f.severity.toUpperCase()}
200
- </span>
201
- <span style={styles.findingTitle}>{f.title}</span>
202
- </div>
203
- <p style={styles.findingText}>{f.description}</p>
204
- <p style={{ ...styles.findingText, color: "#94a3b8" }}>
205
- <strong>Localização:</strong> {f.location ?? "-"}
206
- </p>
207
- {f.codeSnippet && <pre style={styles.code}>{f.codeSnippet}</pre>}
208
- <p style={{ ...styles.findingText, color: "#94a3b8" }}>
209
- <strong>Recomendação:</strong> {f.recommendation}
210
- </p>
211
- <p style={{ ...styles.findingText, color: "#64748b", fontSize: 12 }}>
212
- Confiança: {Math.round(f.judgeReview.confidence)}% — {f.judgeReview.review}
213
- </p>
214
- </div>
215
- ))
216
- ) : (
217
- <div style={styles.resultBox}>
218
- <p style={styles.resultText}>Nenhuma vulnerabilidade encontrada.</p>
219
- </div>
220
- )}
221
- </section>
222
- )}
223
-
224
- {/* Tester */}
225
- {testerResult && (
226
- <section style={styles.section}>
227
- <h2 style={styles.sectionTitle}>🧪 Agente Tester</h2>
228
- <div style={styles.resultBox}>
229
- <p style={styles.resultText}>
230
- <strong>Status:</strong>{" "}
231
- <span style={{ color: testerResult.status === "success" ? "#22c55e" : "#ef4444" }}>
232
- {testerResult.status?.toUpperCase()}
233
- </span>
234
- <br />
235
- <strong>Iterações:</strong> {testerResult.iterations}
236
- </p>
237
- </div>
238
-
239
- {testerResult.pocCode && (
240
- <>
241
- <h3 style={styles.subTitle}>Proof of Concept (Exploit)</h3>
242
- <div style={styles.codeBox}>
243
- <pre style={styles.code}>{testerResult.pocCode}</pre>
244
- </div>
245
- </>
246
- )}
247
-
248
- {testerResult.executionLogs && testerResult.executionLogs.length > 0 && (
249
- <>
250
- <h3 style={styles.subTitle}>Logs de Execução (Foundry)</h3>
251
- <div style={styles.logBox}>
252
- {testerResult.executionLogs.map((log, i) => (
253
- <div key={i} style={styles.logLine}>
254
- {log}
255
- </div>
256
- ))}
257
- </div>
258
- </>
259
- )}
260
- </section>
261
- )}
262
  </div>
263
  );
264
  }
265
-
266
- const severityColor = (severity: string) => {
267
- switch (severity) {
268
- case "high":
269
- return "#ef4444";
270
- case "medium":
271
- return "#f97316";
272
- case "low":
273
- return "#eab308";
274
- default:
275
- return "#64748b";
276
- }
277
- };
278
-
279
- const styles: Record<string, React.CSSProperties> = {
280
- container: {
281
- maxWidth: 900,
282
- margin: "0 auto",
283
- padding: "32px 20px",
284
- fontFamily: "'Segoe UI', system-ui, -apple-system, sans-serif",
285
- color: "#e2e8f0",
286
- background: "#0f172a",
287
- minHeight: "100vh",
288
- },
289
- header: {
290
- textAlign: "center",
291
- marginBottom: 32,
292
- },
293
- title: {
294
- fontSize: 28,
295
- fontWeight: 700,
296
- color: "#f8fafc",
297
- margin: "0 0 12px",
298
- lineHeight: 1.3,
299
- },
300
- subtitle: {
301
- fontSize: 15,
302
- color: "#94a3b8",
303
- margin: 0,
304
- lineHeight: 1.6,
305
- },
306
- form: {
307
- display: "flex",
308
- flexDirection: "column",
309
- gap: 12,
310
- marginBottom: 32,
311
- },
312
- textarea: {
313
- width: "100%",
314
- padding: 16,
315
- fontSize: 14,
316
- fontFamily: "inherit",
317
- borderRadius: 8,
318
- border: "1px solid #334155",
319
- background: "#1e293b",
320
- color: "#e2e8f0",
321
- resize: "vertical",
322
- outline: "none",
323
- boxSizing: "border-box",
324
- lineHeight: 1.6,
325
- },
326
- button: {
327
- padding: "12px 24px",
328
- fontSize: 15,
329
- fontWeight: 600,
330
- borderRadius: 8,
331
- border: "none",
332
- background: "#3b82f6",
333
- color: "#fff",
334
- cursor: "pointer",
335
- transition: "background 0.2s",
336
- },
337
- section: {
338
- marginBottom: 28,
339
- },
340
- sectionTitle: {
341
- fontSize: 18,
342
- fontWeight: 600,
343
- color: "#f8fafc",
344
- marginBottom: 10,
345
- },
346
- subTitle: {
347
- fontSize: 14,
348
- fontWeight: 600,
349
- color: "#94a3b8",
350
- marginTop: 14,
351
- marginBottom: 6,
352
- },
353
- logBox: {
354
- background: "#1e293b",
355
- border: "1px solid #334155",
356
- borderRadius: 8,
357
- padding: 16,
358
- maxHeight: 220,
359
- overflowY: "auto",
360
- fontSize: 13,
361
- fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
362
- },
363
- logLine: {
364
- padding: "2px 0",
365
- color: "#a5f3fc",
366
- whiteSpace: "pre-wrap",
367
- wordBreak: "break-word",
368
- },
369
- codeBox: {
370
- background: "#1e293b",
371
- border: "1px solid #334155",
372
- borderRadius: 8,
373
- padding: 16,
374
- maxHeight: 400,
375
- overflowY: "auto",
376
- },
377
- code: {
378
- margin: 0,
379
- fontSize: 13,
380
- fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
381
- color: "#a5f3fc",
382
- whiteSpace: "pre-wrap",
383
- wordBreak: "break-word",
384
- },
385
- resultBox: {
386
- background: "#1e293b",
387
- border: "1px solid #334155",
388
- borderRadius: 8,
389
- padding: 16,
390
- },
391
- resultText: {
392
- margin: 0,
393
- fontSize: 14,
394
- lineHeight: 1.6,
395
- color: "#cbd5e1",
396
- whiteSpace: "pre-wrap",
397
- },
398
- findingCard: {
399
- background: "#1e293b",
400
- border: "1px solid",
401
- borderRadius: 8,
402
- padding: 16,
403
- marginBottom: 12,
404
- },
405
- findingHeader: {
406
- display: "flex",
407
- alignItems: "center",
408
- gap: 10,
409
- marginBottom: 8,
410
- },
411
- severityBadge: {
412
- fontSize: 11,
413
- fontWeight: 700,
414
- color: "#fff",
415
- padding: "2px 8px",
416
- borderRadius: 4,
417
- letterSpacing: "0.05em",
418
- },
419
- findingTitle: {
420
- fontSize: 15,
421
- fontWeight: 600,
422
- color: "#f8fafc",
423
- },
424
- findingText: {
425
- margin: "4px 0",
426
- fontSize: 13,
427
- lineHeight: 1.6,
428
- color: "#cbd5e1",
429
- },
430
- };
 
1
  import { useState, useRef, useCallback } from "react";
2
 
3
+ import type { CoderResult, AuditorResult, TesterResult, AgentState } from "./types";
4
+ import { computeDiff, type DiffLine } from "./utils/diff";
5
+ import { INITIAL_AGENT_STATES, applyStepEvent, type StepEvent } from "./utils/status";
6
+ import { ArtifactPanel } from "./components/ArtifactPanel";
7
+ import { ControlPanel } from "./components/ControlPanel";
 
 
 
 
 
 
 
 
 
8
 
9
+ export type ArtifactTab = "contract" | "findings" | "poc";
 
 
 
 
 
 
 
 
 
 
10
 
11
  export function App() {
12
  const [requirements, setRequirements] = useState("");
 
 
 
 
13
  const [running, setRunning] = useState(false);
14
+ const [logs, setLogs] = useState<string[]>([]);
15
+ const [coder, setCoder] = useState<CoderResult | null>(null);
16
+ const [auditor, setAuditor] = useState<AuditorResult | null>(null);
17
+ const [tester, setTester] = useState<TesterResult | null>(null);
18
+ const [agentStates, setAgentStates] = useState<AgentState[]>(INITIAL_AGENT_STATES);
19
+ const [selectedTab, setSelectedTab] = useState<ArtifactTab>("contract");
20
+ const [contractDiff, setContractDiff] = useState<DiffLine[] | null>(null);
21
+ const [pocDiff, setPocDiff] = useState<DiffLine[] | null>(null);
22
+ const [diffMode, setDiffMode] = useState<Record<string, boolean>>({});
23
+
24
+ const prevContract = useRef<string | null>(null);
25
+ const prevPoc = useRef<string | null>(null);
26
 
27
  const appendLog = useCallback((msg: string) => {
28
  setLogs((prev) => [...prev, msg]);
 
29
  }, []);
30
 
31
+ const handleRun = async (e: React.FormEvent) => {
32
  e.preventDefault();
33
  if (!requirements.trim() || running) return;
34
 
35
  setRunning(true);
36
  setLogs([]);
37
+ setCoder(null);
38
+ setAuditor(null);
39
+ setTester(null);
40
+ setAgentStates(INITIAL_AGENT_STATES);
41
+ setContractDiff(null);
42
+ setPocDiff(null);
43
+ setSelectedTab("contract");
44
 
45
  try {
46
  const res = await fetch("/api/run", {
 
62
 
63
  buffer += decoder.decode(value, { stream: true });
64
  const lines = buffer.split("\n");
65
+ buffer = lines.pop() ?? "";
66
 
67
  for (const line of lines) {
68
  if (line.startsWith("event:")) {
69
  currentEvent = line.slice(6).trim();
 
70
  } else if (line.startsWith("data:")) {
71
  const data = line.slice(5).trim();
 
72
  switch (currentEvent) {
73
  case "log":
74
  appendLog(data);
75
  break;
76
+ case "step": {
77
+ const event: StepEvent = JSON.parse(data);
78
+ setAgentStates((prev) => applyStepEvent(prev, event));
79
  break;
80
+ }
81
+ case "coder": {
82
+ const result: CoderResult = JSON.parse(data);
83
+ const hasPrev = prevContract.current !== null;
84
+ const diff = computeDiff(prevContract.current, result.contract);
85
+ prevContract.current = result.contract;
86
+ setContractDiff(diff);
87
+ setDiffMode((prev) => ({ ...prev, contract: hasPrev }));
88
+ setCoder(result);
89
+ setSelectedTab("contract");
90
+ setAgentStates((prev) => {
91
+ const s = prev.map((a) => ({ ...a, steps: a.steps.map((st) => ({ ...st })) }));
92
+ const a = s.find((x) => x.id === "coder")!;
93
+ a.status = "done";
94
+ a.steps.forEach((st) => {
95
+ if (st.status !== "error") st.status = "done";
96
+ });
97
+ return s;
98
+ });
99
  break;
100
+ }
101
+ case "auditor": {
102
+ const result: AuditorResult = JSON.parse(data);
103
+ setAuditor(result);
104
+ if (result.findings.length > 0) setSelectedTab("findings");
105
+ setAgentStates((prev) => {
106
+ const s = prev.map((a) => ({ ...a, steps: a.steps.map((st) => ({ ...st })) }));
107
+ const a = s.find((x) => x.id === "auditor")!;
108
+ a.status = "done";
109
+ a.steps.forEach((st) => {
110
+ if (st.status !== "error") st.status = "done";
111
+ });
112
+ return s;
113
+ });
114
  break;
115
+ }
116
+ case "tester": {
117
+ const result: TesterResult = JSON.parse(data);
118
+ if (result.pocCode) {
119
+ const hasPrev = prevPoc.current !== null;
120
+ const diff = computeDiff(prevPoc.current, result.pocCode);
121
+ prevPoc.current = result.pocCode;
122
+ setPocDiff(diff);
123
+ setDiffMode((prev) => ({ ...prev, poc: hasPrev }));
124
+ setSelectedTab("poc");
125
+ }
126
+ setTester(result);
127
+ setAgentStates((prev) => {
128
+ const s = prev.map((a) => ({ ...a, steps: a.steps.map((st) => ({ ...st })) }));
129
+ const a = s.find((x) => x.id === "tester")!;
130
+ if (result.status === "skipped") {
131
+ a.status = "skipped";
132
+ a.steps.forEach((st) => {
133
+ st.status = "skipped";
134
+ });
135
+ } else {
136
+ a.status = result.status === "success" ? "done" : "error";
137
+ a.steps.forEach((st) => {
138
+ if (st.status !== "error") st.status = "done";
139
+ });
140
+ }
141
+ return s;
142
+ });
143
+ break;
144
+ }
145
  case "error":
146
+ appendLog(`[ERRO] ${data}`);
147
  break;
148
  }
149
  currentEvent = "";
 
151
  }
152
  }
153
  } catch (err) {
154
+ appendLog(`[ERRO] ${err instanceof Error ? err.message : String(err)}`);
155
  } finally {
156
  setRunning(false);
157
  }
158
  };
159
 
160
+ const toggleDiffMode = (key: string) => setDiffMode((prev) => ({ ...prev, [key]: !prev[key] }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
+ return (
163
+ <div
164
+ style={{
165
+ display: "flex",
166
+ height: "100vh",
167
+ background: "#070c10",
168
+ overflow: "hidden",
169
+ color: "#c9d1d9",
170
+ }}
171
+ >
172
+ <ControlPanel
173
+ requirements={requirements}
174
+ onRequirementsChange={setRequirements}
175
+ onRun={handleRun}
176
+ running={running}
177
+ agentStates={agentStates}
178
+ logs={logs}
179
+ />
180
+ <ArtifactPanel
181
+ coder={coder}
182
+ auditor={auditor}
183
+ tester={tester}
184
+ contractDiff={contractDiff}
185
+ pocDiff={pocDiff}
186
+ selectedTab={selectedTab}
187
+ onSelectTab={setSelectedTab}
188
+ diffMode={diffMode}
189
+ onToggleDiff={toggleDiffMode}
190
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  </div>
192
  );
193
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/ArtifactPanel.tsx ADDED
@@ -0,0 +1,523 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { CoderResult, AuditorResult, TesterResult, Finding } from "../types";
2
+ import type { DiffLine } from "../utils/diff";
3
+ import type { ArtifactTab } from "../App";
4
+
5
+ const C = {
6
+ bg: "#0d1117",
7
+ surface: "#161b22",
8
+ border: "#21262d",
9
+ text: "#c9d1d9",
10
+ muted: "#8b949e",
11
+ dim: "#484f58",
12
+ accent: "#58a6ff",
13
+ green: "#3fb950",
14
+ red: "#f85149",
15
+ yellow: "#d29922",
16
+ orange: "#db6d28",
17
+ addedBg: "rgba(46, 160, 67, 0.1)",
18
+ removedBg: "rgba(248, 81, 73, 0.09)",
19
+ };
20
+
21
+ const SEVERITY_COLOR: Record<string, string> = {
22
+ high: C.red,
23
+ medium: C.yellow,
24
+ low: "#58a6ff",
25
+ };
26
+
27
+ interface Props {
28
+ coder: CoderResult | null;
29
+ auditor: AuditorResult | null;
30
+ tester: TesterResult | null;
31
+ contractDiff: DiffLine[] | null;
32
+ pocDiff: DiffLine[] | null;
33
+ selectedTab: ArtifactTab;
34
+ onSelectTab: (tab: ArtifactTab) => void;
35
+ diffMode: Record<string, boolean>;
36
+ onToggleDiff: (key: string) => void;
37
+ }
38
+
39
+ export function ArtifactPanel({
40
+ coder,
41
+ auditor,
42
+ tester,
43
+ contractDiff,
44
+ pocDiff,
45
+ selectedTab,
46
+ onSelectTab,
47
+ diffMode,
48
+ onToggleDiff,
49
+ }: Props) {
50
+ const findingCount = auditor?.findings?.length ?? 0;
51
+ const hasPoc = !!tester?.pocCode;
52
+
53
+ const treeItems: {
54
+ id: ArtifactTab;
55
+ icon: string;
56
+ name: string;
57
+ badge?: string;
58
+ badgeColor?: string;
59
+ available: boolean;
60
+ }[] = [
61
+ {
62
+ id: "contract",
63
+ icon: "◈",
64
+ name: "Contract.sol",
65
+ badge: coder ? "sol" : undefined,
66
+ badgeColor: C.accent,
67
+ available: !!coder,
68
+ },
69
+ {
70
+ id: "findings",
71
+ icon: "⚑",
72
+ name: "findings/",
73
+ badge: auditor ? (findingCount > 0 ? `${findingCount} issue${findingCount > 1 ? "s" : ""}` : "clean") : undefined,
74
+ badgeColor: auditor ? (findingCount > 0 ? C.red : C.green) : C.dim,
75
+ available: !!auditor,
76
+ },
77
+ {
78
+ id: "poc",
79
+ icon: "◈",
80
+ name: "ExploitTest.t.sol",
81
+ badge: tester ? tester.status : undefined,
82
+ badgeColor: tester?.status === "success" ? C.green : tester?.status === "failed" ? C.red : C.yellow,
83
+ available: hasPoc,
84
+ },
85
+ ];
86
+
87
+ return (
88
+ <div
89
+ style={{
90
+ flex: 1,
91
+ minWidth: 0,
92
+ display: "flex",
93
+ flexDirection: "column",
94
+ overflow: "hidden",
95
+ borderLeft: `1px solid ${C.border}`,
96
+ background: C.bg,
97
+ }}
98
+ >
99
+ {/* Panel titlebar */}
100
+ <div
101
+ style={{
102
+ height: 38,
103
+ display: "flex",
104
+ alignItems: "center",
105
+ gap: 8,
106
+ padding: "0 16px",
107
+ borderBottom: `1px solid ${C.border}`,
108
+ flexShrink: 0,
109
+ }}
110
+ >
111
+ <span style={{ fontSize: 10, color: C.dim, letterSpacing: "0.12em", textTransform: "uppercase" }}>
112
+ explorer
113
+ </span>
114
+ </div>
115
+
116
+ {/* File tree */}
117
+ <div
118
+ style={{
119
+ flexShrink: 0,
120
+ borderBottom: `1px solid ${C.border}`,
121
+ padding: "6px 0",
122
+ }}
123
+ >
124
+ <div style={{ padding: "3px 16px 4px", display: "flex", alignItems: "center", gap: 6 }}>
125
+ <span style={{ fontSize: 10, color: C.dim, letterSpacing: "0.08em" }}>▾</span>
126
+ <span style={{ fontSize: 11, color: C.muted, letterSpacing: "0.05em" }}>artifacts</span>
127
+ </div>
128
+ {treeItems.map((item) => {
129
+ const selected = selectedTab === item.id;
130
+ return (
131
+ <button
132
+ key={item.id}
133
+ onClick={() => item.available && onSelectTab(item.id)}
134
+ disabled={!item.available}
135
+ style={{
136
+ display: "flex",
137
+ alignItems: "center",
138
+ gap: 8,
139
+ width: "100%",
140
+ padding: "4px 16px 4px 28px",
141
+ background: selected ? "rgba(88, 166, 255, 0.08)" : "transparent",
142
+ border: "none",
143
+ borderLeft: selected ? `2px solid ${C.accent}` : "2px solid transparent",
144
+ cursor: item.available ? "pointer" : "default",
145
+ textAlign: "left",
146
+ color: selected ? C.text : item.available ? C.muted : C.dim,
147
+ fontSize: 13,
148
+ transition: "background 0.1s",
149
+ }}
150
+ >
151
+ <span style={{ fontSize: 10, color: selected ? C.accent : item.available ? C.dim : "#2d333b" }}>
152
+ {item.icon}
153
+ </span>
154
+ <span style={{ flex: 1 }}>{item.name}</span>
155
+ {item.badge && (
156
+ <span
157
+ style={{
158
+ fontSize: 10,
159
+ color: item.badgeColor,
160
+ padding: "1px 5px",
161
+ borderRadius: 3,
162
+ border: `1px solid ${item.badgeColor}30`,
163
+ background: `${item.badgeColor}12`,
164
+ }}
165
+ >
166
+ {item.badge}
167
+ </span>
168
+ )}
169
+ </button>
170
+ );
171
+ })}
172
+ </div>
173
+
174
+ {/* Content area */}
175
+ <div style={{ flex: 1, overflow: "hidden", display: "flex", flexDirection: "column" }}>
176
+ {selectedTab === "contract" && coder && contractDiff ? (
177
+ <CodeViewer
178
+ title="Contract.sol"
179
+ diff={contractDiff}
180
+ showDiff={diffMode["contract"] ?? false}
181
+ onToggleDiff={() => onToggleDiff("contract")}
182
+ />
183
+ ) : selectedTab === "findings" && auditor ? (
184
+ <FindingsViewer findings={auditor.findings} reviewSummary={coder?.reviewSummary} />
185
+ ) : selectedTab === "poc" && tester?.pocCode && pocDiff ? (
186
+ <CodeViewer
187
+ title="ExploitTest.t.sol"
188
+ diff={pocDiff}
189
+ showDiff={diffMode["poc"] ?? false}
190
+ onToggleDiff={() => onToggleDiff("poc")}
191
+ status={tester.status}
192
+ iterations={tester.iterations}
193
+ />
194
+ ) : (
195
+ <EmptyState />
196
+ )}
197
+ </div>
198
+ </div>
199
+ );
200
+ }
201
+
202
+ function CodeViewer({
203
+ title,
204
+ diff,
205
+ showDiff,
206
+ onToggleDiff,
207
+ status,
208
+ iterations,
209
+ }: {
210
+ title: string;
211
+ diff: DiffLine[];
212
+ showDiff: boolean;
213
+ onToggleDiff: () => void;
214
+ status?: string;
215
+ iterations?: number;
216
+ }) {
217
+ const addedCount = diff.filter((l) => l.type === "added").length;
218
+ const removedCount = diff.filter((l) => l.type === "removed").length;
219
+ const totalLines = diff.filter((l) => l.type !== "removed").length;
220
+ const hasDiff = addedCount > 0 || removedCount > 0;
221
+ const hasChanges =
222
+ diff.some((l) => l.type === "removed") ||
223
+ diff.some((l) => l.type === "added" && diff.some((x) => x.type === "unchanged"));
224
+
225
+ return (
226
+ <div style={{ display: "flex", flexDirection: "column", height: "100%", overflow: "hidden" }}>
227
+ {/* Toolbar */}
228
+ <div
229
+ style={{
230
+ height: 36,
231
+ display: "flex",
232
+ alignItems: "center",
233
+ padding: "0 16px",
234
+ borderBottom: `1px solid ${C.border}`,
235
+ gap: 10,
236
+ flexShrink: 0,
237
+ background: C.bg,
238
+ }}
239
+ >
240
+ <span style={{ fontSize: 12, color: C.muted }}>{title}</span>
241
+ {status && (
242
+ <span
243
+ style={{
244
+ fontSize: 10,
245
+ color: status === "success" ? C.green : status === "failed" ? C.red : C.yellow,
246
+ padding: "1px 6px",
247
+ border: `1px solid currentColor`,
248
+ borderRadius: 3,
249
+ opacity: 0.8,
250
+ }}
251
+ >
252
+ {status}
253
+ </span>
254
+ )}
255
+ {iterations !== undefined && iterations > 0 && (
256
+ <span style={{ fontSize: 10, color: C.dim }}>{iterations} iter</span>
257
+ )}
258
+ <span style={{ flex: 1 }} />
259
+ {hasDiff && (
260
+ <>
261
+ {hasChanges && (
262
+ <span style={{ fontSize: 10, color: C.dim }}>
263
+ <span style={{ color: C.green }}>+{addedCount}</span>{" "}
264
+ <span style={{ color: C.red }}>-{removedCount}</span>
265
+ </span>
266
+ )}
267
+ <button
268
+ onClick={onToggleDiff}
269
+ style={{
270
+ padding: "2px 8px",
271
+ fontSize: 11,
272
+ background: showDiff ? "rgba(88, 166, 255, 0.12)" : "transparent",
273
+ border: `1px solid ${showDiff ? C.accent : C.border}`,
274
+ color: showDiff ? C.accent : C.muted,
275
+ cursor: "pointer",
276
+ borderRadius: 3,
277
+ fontFamily: "inherit",
278
+ }}
279
+ >
280
+ diff
281
+ </button>
282
+ </>
283
+ )}
284
+ <span style={{ fontSize: 10, color: C.dim }}>{totalLines}L</span>
285
+ </div>
286
+
287
+ {/* Code */}
288
+ <div style={{ flex: 1, overflow: "auto", background: "#0a0e14" }}>
289
+ <table
290
+ style={{
291
+ width: "100%",
292
+ borderCollapse: "collapse",
293
+ fontSize: 12.5,
294
+ lineHeight: 1.65,
295
+ }}
296
+ >
297
+ <tbody>
298
+ {diff.map((dl, idx) => {
299
+ if (!showDiff && dl.type === "removed") return null;
300
+ const isAdded = showDiff && dl.type === "added";
301
+ const isRemoved = showDiff && dl.type === "removed";
302
+ return (
303
+ <tr
304
+ key={idx}
305
+ style={{
306
+ background: isAdded ? C.addedBg : isRemoved ? C.removedBg : "transparent",
307
+ }}
308
+ >
309
+ {showDiff && (
310
+ <td
311
+ style={{
312
+ width: 18,
313
+ textAlign: "center",
314
+ color: isAdded ? C.green : isRemoved ? C.red : "transparent",
315
+ fontSize: 11,
316
+ userSelect: "none",
317
+ paddingLeft: 8,
318
+ paddingRight: 4,
319
+ }}
320
+ >
321
+ {isAdded ? "+" : isRemoved ? "−" : " "}
322
+ </td>
323
+ )}
324
+ <td
325
+ style={{
326
+ width: 44,
327
+ textAlign: "right",
328
+ paddingRight: 14,
329
+ paddingLeft: 8,
330
+ color: isRemoved ? "#5e3535" : C.dim,
331
+ userSelect: "none",
332
+ fontSize: 11,
333
+ }}
334
+ >
335
+ {dl.lineNo > 0 ? dl.lineNo : ""}
336
+ </td>
337
+ <td
338
+ style={{
339
+ padding: "0 16px 0 0",
340
+ color: isAdded ? "#b5e3b5" : isRemoved ? "#e5a0a0" : C.text,
341
+ whiteSpace: "pre",
342
+ fontFamily: "inherit",
343
+ }}
344
+ >
345
+ {dl.line}
346
+ </td>
347
+ </tr>
348
+ );
349
+ })}
350
+ </tbody>
351
+ </table>
352
+ </div>
353
+ </div>
354
+ );
355
+ }
356
+
357
+ function FindingsViewer({ findings, reviewSummary }: { findings: Finding[]; reviewSummary?: string }) {
358
+ return (
359
+ <div style={{ flex: 1, overflow: "auto", padding: 20 }}>
360
+ {/* Review summary */}
361
+ {reviewSummary && (
362
+ <div style={{ marginBottom: 20 }}>
363
+ <div
364
+ style={{
365
+ fontSize: 10,
366
+ color: C.dim,
367
+ letterSpacing: "0.1em",
368
+ textTransform: "uppercase",
369
+ marginBottom: 8,
370
+ }}
371
+ >
372
+ coder / security-review
373
+ </div>
374
+ <div
375
+ style={{
376
+ background: C.surface,
377
+ border: `1px solid ${C.border}`,
378
+ borderRadius: 6,
379
+ padding: 14,
380
+ fontSize: 12.5,
381
+ color: C.muted,
382
+ lineHeight: 1.7,
383
+ whiteSpace: "pre-wrap",
384
+ }}
385
+ >
386
+ {reviewSummary}
387
+ </div>
388
+ </div>
389
+ )}
390
+
391
+ {/* Findings */}
392
+ <div
393
+ style={{
394
+ fontSize: 10,
395
+ color: C.dim,
396
+ letterSpacing: "0.1em",
397
+ textTransform: "uppercase",
398
+ marginBottom: 8,
399
+ }}
400
+ >
401
+ auditor / findings — {findings.length} issue{findings.length !== 1 ? "s" : ""}
402
+ </div>
403
+
404
+ {findings.length === 0 ? (
405
+ <div
406
+ style={{
407
+ background: C.surface,
408
+ border: `1px solid ${C.border}`,
409
+ borderRadius: 6,
410
+ padding: 16,
411
+ color: C.green,
412
+ fontSize: 13,
413
+ }}
414
+ >
415
+ ✓ Nenhuma vulnerabilidade encontrada.
416
+ </div>
417
+ ) : (
418
+ findings.map((f, i) => <FindingCard key={i} finding={f} index={i} />)
419
+ )}
420
+ </div>
421
+ );
422
+ }
423
+
424
+ function FindingCard({ finding, index }: { finding: Finding; index: number }) {
425
+ const color = SEVERITY_COLOR[finding.severity] ?? C.muted;
426
+
427
+ return (
428
+ <div
429
+ style={{
430
+ background: C.surface,
431
+ border: `1px solid ${C.border}`,
432
+ borderLeft: `3px solid ${color}`,
433
+ borderRadius: 6,
434
+ marginBottom: 12,
435
+ overflow: "hidden",
436
+ animation: "fadein 0.2s ease",
437
+ }}
438
+ >
439
+ {/* Header */}
440
+ <div
441
+ style={{
442
+ display: "flex",
443
+ alignItems: "center",
444
+ gap: 10,
445
+ padding: "10px 14px",
446
+ borderBottom: `1px solid ${C.border}`,
447
+ }}
448
+ >
449
+ <span
450
+ style={{
451
+ fontSize: 9,
452
+ fontWeight: 700,
453
+ color: color,
454
+ padding: "2px 6px",
455
+ border: `1px solid ${color}`,
456
+ borderRadius: 3,
457
+ letterSpacing: "0.1em",
458
+ background: `${color}15`,
459
+ }}
460
+ >
461
+ {finding.severity.toUpperCase()}
462
+ </span>
463
+ <span style={{ fontSize: 13, color: C.text, fontWeight: 600 }}>
464
+ [{index + 1}] {finding.title}
465
+ </span>
466
+ {finding.location && <span style={{ marginLeft: "auto", fontSize: 10, color: C.dim }}>{finding.location}</span>}
467
+ </div>
468
+
469
+ {/* Body */}
470
+ <div style={{ padding: 14 }}>
471
+ <p style={{ fontSize: 12.5, color: C.muted, lineHeight: 1.7, marginBottom: 10 }}>{finding.description}</p>
472
+
473
+ {finding.codeSnippet && (
474
+ <div
475
+ style={{
476
+ background: "#0a0e14",
477
+ border: `1px solid ${C.border}`,
478
+ borderRadius: 4,
479
+ padding: "8px 12px",
480
+ marginBottom: 10,
481
+ fontSize: 12,
482
+ color: "#b5c0cc",
483
+ whiteSpace: "pre",
484
+ overflow: "auto",
485
+ maxHeight: 140,
486
+ fontFamily: "inherit",
487
+ }}
488
+ >
489
+ {finding.codeSnippet}
490
+ </div>
491
+ )}
492
+
493
+ <div style={{ fontSize: 12, color: C.muted, marginBottom: 6 }}>
494
+ <span style={{ color: C.dim }}>recomendação: </span>
495
+ {finding.recommendation}
496
+ </div>
497
+
498
+ <div style={{ fontSize: 11, color: C.dim, marginTop: 8 }}>
499
+ confiança: {Math.round(finding.judgeReview.confidence)}% — {finding.judgeReview.review}
500
+ </div>
501
+ </div>
502
+ </div>
503
+ );
504
+ }
505
+
506
+ function EmptyState() {
507
+ return (
508
+ <div
509
+ style={{
510
+ flex: 1,
511
+ display: "flex",
512
+ flexDirection: "column",
513
+ alignItems: "center",
514
+ justifyContent: "center",
515
+ color: C.dim,
516
+ gap: 12,
517
+ }}
518
+ >
519
+ <div style={{ fontSize: 28, opacity: 0.3 }}>◈</div>
520
+ <div style={{ fontSize: 12, letterSpacing: "0.05em" }}>aguardando execução do pipeline</div>
521
+ </div>
522
+ );
523
+ }
frontend/src/components/ControlPanel.tsx ADDED
@@ -0,0 +1,443 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useRef } from "react";
2
+
3
+ import type { AgentState, StepStatus } from "../types";
4
+
5
+ const C = {
6
+ bg: "#0d1117",
7
+ surface: "#161b22",
8
+ border: "#21262d",
9
+ text: "#c9d1d9",
10
+ muted: "#8b949e",
11
+ dim: "#484f58",
12
+ accent: "#58a6ff",
13
+ green: "#3fb950",
14
+ red: "#f85149",
15
+ yellow: "#d29922",
16
+ };
17
+
18
+ const AGENT_COLORS: Record<string, string> = {
19
+ coder: "#58a6ff",
20
+ auditor: "#d29922",
21
+ tester: "#3fb950",
22
+ };
23
+
24
+ const DEFAULT_REQUIREMENT = `Crie um contrato de staking com as seguintes características:
25
+ - Usuários podem depositar ETH e receber créditos proporcionais ao valor
26
+ - O owner pode pausar e retomar os depósitos
27
+ - Função de saque que devolve ETH proporcional ao crédito do usuário
28
+ - Acumula recompensas de 1% ao dia sobre o saldo depositado
29
+ - Emite eventos para depósito, saque e distribuição de recompensas`;
30
+
31
+ interface Props {
32
+ requirements: string;
33
+ onRequirementsChange: (v: string) => void;
34
+ onRun: (e: React.FormEvent) => void;
35
+ running: boolean;
36
+ agentStates: AgentState[];
37
+ logs: string[];
38
+ }
39
+
40
+ export function ControlPanel({ requirements, onRequirementsChange, onRun, running, agentStates, logs }: Props) {
41
+ return (
42
+ <div
43
+ style={{
44
+ width: 400,
45
+ flexShrink: 0,
46
+ display: "flex",
47
+ flexDirection: "column",
48
+ background: C.bg,
49
+ overflow: "hidden",
50
+ }}
51
+ >
52
+ <Header />
53
+ <InputForm requirements={requirements} onChange={onRequirementsChange} onRun={onRun} running={running} />
54
+ <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden", minHeight: 0 }}>
55
+ <PipelineStatus agents={agentStates} running={running} />
56
+ <TerminalLog logs={logs} />
57
+ </div>
58
+ </div>
59
+ );
60
+ }
61
+
62
+ function Header() {
63
+ return (
64
+ <div
65
+ style={{
66
+ padding: "14px 18px 10px",
67
+ borderBottom: `1px solid ${C.border}`,
68
+ flexShrink: 0,
69
+ }}
70
+ >
71
+ <div style={{ display: "flex", alignItems: "baseline", gap: 8, marginBottom: 3 }}>
72
+ <span style={{ fontSize: 15, fontWeight: 700, color: C.accent, letterSpacing: "0.05em" }}>TALP1</span>
73
+ <span style={{ fontSize: 10, color: C.dim }}>v0.1</span>
74
+ </div>
75
+ <div style={{ fontSize: 11, color: C.dim, lineHeight: 1.5 }}>multi-agent smart contract pipeline</div>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ function InputForm({
81
+ requirements,
82
+ onChange,
83
+ onRun,
84
+ running,
85
+ }: {
86
+ requirements: string;
87
+ onChange: (v: string) => void;
88
+ onRun: (e: React.FormEvent) => void;
89
+ running: boolean;
90
+ }) {
91
+ const canRun = !running && requirements.trim().length > 0;
92
+
93
+ return (
94
+ <form
95
+ onSubmit={onRun}
96
+ style={{
97
+ padding: "12px 18px",
98
+ borderBottom: `1px solid ${C.border}`,
99
+ flexShrink: 0,
100
+ display: "flex",
101
+ flexDirection: "column",
102
+ gap: 8,
103
+ }}
104
+ >
105
+ <div style={{ fontSize: 10, color: C.dim, letterSpacing: "0.1em", textTransform: "uppercase", marginBottom: 2 }}>
106
+ requisitos
107
+ </div>
108
+ <textarea
109
+ value={requirements}
110
+ onChange={(e) => onChange(e.target.value)}
111
+ disabled={running}
112
+ rows={5}
113
+ placeholder="Descreva o smart contract a ser gerado, auditado e testado..."
114
+ style={{
115
+ width: "100%",
116
+ padding: "10px 12px",
117
+ fontSize: 12,
118
+ fontFamily: "inherit",
119
+ background: C.surface,
120
+ border: `1px solid ${C.border}`,
121
+ borderRadius: 5,
122
+ color: C.text,
123
+ resize: "none",
124
+ outline: "none",
125
+ lineHeight: 1.65,
126
+ transition: "border-color 0.15s",
127
+ }}
128
+ onFocus={(e) => (e.target.style.borderColor = `${C.accent}60`)}
129
+ onBlur={(e) => (e.target.style.borderColor = C.border)}
130
+ />
131
+ <div style={{ display: "flex", gap: 8 }}>
132
+ <button
133
+ type="button"
134
+ onClick={() => onChange(DEFAULT_REQUIREMENT)}
135
+ disabled={running}
136
+ style={{
137
+ padding: "7px 10px",
138
+ fontSize: 11,
139
+ fontFamily: "inherit",
140
+ background: "transparent",
141
+ border: `1px solid ${C.border}`,
142
+ borderRadius: 4,
143
+ color: C.muted,
144
+ cursor: running ? "default" : "pointer",
145
+ opacity: running ? 0.4 : 1,
146
+ whiteSpace: "nowrap",
147
+ }}
148
+ >
149
+ usar exemplo
150
+ </button>
151
+ <button
152
+ type="submit"
153
+ disabled={!canRun}
154
+ style={{
155
+ flex: 1,
156
+ padding: "7px 14px",
157
+ fontSize: 12,
158
+ fontFamily: "inherit",
159
+ fontWeight: 600,
160
+ background: canRun ? C.accent : "transparent",
161
+ border: `1px solid ${canRun ? C.accent : C.border}`,
162
+ borderRadius: 4,
163
+ color: canRun ? "#fff" : C.dim,
164
+ cursor: canRun ? "pointer" : "default",
165
+ transition: "all 0.15s",
166
+ display: "flex",
167
+ alignItems: "center",
168
+ justifyContent: "center",
169
+ gap: 7,
170
+ }}
171
+ >
172
+ {running && (
173
+ <span
174
+ style={{
175
+ width: 10,
176
+ height: 10,
177
+ borderRadius: "50%",
178
+ border: "1.5px solid rgba(255,255,255,0.3)",
179
+ borderTopColor: "#fff",
180
+ display: "inline-block",
181
+ animation: "spin 0.7s linear infinite",
182
+ flexShrink: 0,
183
+ }}
184
+ />
185
+ )}
186
+ {running ? "executando..." : "▶ executar"}
187
+ </button>
188
+ </div>
189
+ </form>
190
+ );
191
+ }
192
+
193
+ function PipelineStatus({ agents, running }: { agents: AgentState[]; running: boolean }) {
194
+ const anyActive = agents.some((a) => a.status === "running");
195
+
196
+ return (
197
+ <div
198
+ style={{
199
+ flexShrink: 0,
200
+ borderBottom: `1px solid ${C.border}`,
201
+ padding: "10px 0",
202
+ maxHeight: 280,
203
+ overflow: "auto",
204
+ }}
205
+ >
206
+ <div
207
+ style={{
208
+ display: "flex",
209
+ alignItems: "center",
210
+ gap: 8,
211
+ padding: "0 18px 6px",
212
+ }}
213
+ >
214
+ <span style={{ fontSize: 10, color: C.dim, letterSpacing: "0.1em", textTransform: "uppercase" }}>pipeline</span>
215
+ {anyActive && (
216
+ <span
217
+ style={{
218
+ width: 6,
219
+ height: 6,
220
+ borderRadius: "50%",
221
+ background: C.accent,
222
+ animation: "pulse 1.2s ease-in-out infinite",
223
+ flexShrink: 0,
224
+ }}
225
+ />
226
+ )}
227
+ </div>
228
+
229
+ {agents.map((agent) => (
230
+ <AgentBlock key={agent.id} agent={agent} />
231
+ ))}
232
+ </div>
233
+ );
234
+ }
235
+
236
+ function AgentBlock({ agent }: { agent: AgentState }) {
237
+ const color = AGENT_COLORS[agent.id] ?? C.muted;
238
+ const isRunning = agent.status === "running";
239
+ const isDone = agent.status === "done";
240
+ const isError = agent.status === "error";
241
+ const isSkipped = agent.status === "skipped";
242
+
243
+ return (
244
+ <div style={{ padding: "4px 18px" }}>
245
+ {/* Agent header */}
246
+ <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 3 }}>
247
+ <AgentDot status={agent.status} color={color} />
248
+ <span
249
+ style={{
250
+ fontSize: 11,
251
+ fontWeight: 600,
252
+ letterSpacing: "0.08em",
253
+ color: isDone || isRunning ? color : C.dim,
254
+ }}
255
+ >
256
+ {agent.label}
257
+ </span>
258
+ <span
259
+ style={{
260
+ marginLeft: "auto",
261
+ fontSize: 10,
262
+ color: isDone ? C.green : isError ? C.red : isSkipped ? C.dim : isRunning ? color : C.dim,
263
+ opacity: isDone || isError || isSkipped || isRunning ? 1 : 0.5,
264
+ }}
265
+ >
266
+ {isDone ? "done" : isError ? "error" : isSkipped ? "skip" : isRunning ? "running" : "pending"}
267
+ </span>
268
+ </div>
269
+
270
+ {/* Steps */}
271
+ <div style={{ paddingLeft: 20 }}>
272
+ {agent.steps.map((step) => (
273
+ <StepRow key={step.id} step={step} agentColor={color} />
274
+ ))}
275
+ </div>
276
+ </div>
277
+ );
278
+ }
279
+
280
+ function AgentDot({ status, color }: { status: StepStatus; color: string }) {
281
+ if (status === "running") {
282
+ return (
283
+ <span
284
+ style={{
285
+ width: 8,
286
+ height: 8,
287
+ borderRadius: "50%",
288
+ border: `1.5px solid ${color}40`,
289
+ borderTopColor: color,
290
+ display: "inline-block",
291
+ animation: "spin 0.7s linear infinite",
292
+ flexShrink: 0,
293
+ }}
294
+ />
295
+ );
296
+ }
297
+ if (status === "done") {
298
+ return <span style={{ color, fontSize: 10, lineHeight: 1 }}>✓</span>;
299
+ }
300
+ if (status === "error") {
301
+ return <span style={{ color: C.red, fontSize: 10, lineHeight: 1 }}>✗</span>;
302
+ }
303
+ if (status === "skipped") {
304
+ return <span style={{ color: C.dim, fontSize: 10, lineHeight: 1 }}>–</span>;
305
+ }
306
+ return <span style={{ color: C.dim, fontSize: 10, lineHeight: 1 }}>○</span>;
307
+ }
308
+
309
+ function StepRow({
310
+ step,
311
+ agentColor,
312
+ }: {
313
+ step: { label: string; status: StepStatus; detail?: string };
314
+ agentColor: string;
315
+ }) {
316
+ const isRunning = step.status === "running";
317
+ const isDone = step.status === "done";
318
+ const isError = step.status === "error";
319
+ const isSkipped = step.status === "skipped";
320
+
321
+ return (
322
+ <div
323
+ style={{
324
+ display: "flex",
325
+ alignItems: "center",
326
+ gap: 6,
327
+ padding: "2px 0",
328
+ fontSize: 11,
329
+ color: isDone ? C.muted : isRunning ? C.text : C.dim,
330
+ animation: isRunning ? "fadein 0.15s ease" : undefined,
331
+ }}
332
+ >
333
+ <span style={{ width: 10, flexShrink: 0, textAlign: "center" }}>
334
+ {isDone ? (
335
+ <span style={{ color: C.green, fontSize: 9 }}>✓</span>
336
+ ) : isRunning ? (
337
+ <span
338
+ style={{
339
+ width: 6,
340
+ height: 6,
341
+ borderRadius: "50%",
342
+ border: `1px solid ${agentColor}40`,
343
+ borderTopColor: agentColor,
344
+ display: "inline-block",
345
+ animation: "spin 0.7s linear infinite",
346
+ }}
347
+ />
348
+ ) : isError ? (
349
+ <span style={{ color: C.red, fontSize: 9 }}>✗</span>
350
+ ) : isSkipped ? (
351
+ <span style={{ color: C.dim, fontSize: 9 }}>–</span>
352
+ ) : (
353
+ <span style={{ color: C.dim, fontSize: 9 }}>·</span>
354
+ )}
355
+ </span>
356
+ <span>{step.label}</span>
357
+ {step.detail && <span style={{ color: agentColor, fontSize: 10, opacity: 0.8 }}>{step.detail}</span>}
358
+ </div>
359
+ );
360
+ }
361
+
362
+ function logColor(log: string): string {
363
+ if (log.includes("[Coder]")) return "#58a6ff";
364
+ if (log.includes("[Auditor]")) return "#d29922";
365
+ if (log.includes("[Tester]")) return "#3fb950";
366
+ if (log.includes("[ERRO]") || log.toLowerCase().startsWith("error")) return "#f85149";
367
+ if (log === "Pipeline concluído.") return "#3fb950";
368
+ return "#6e7681";
369
+ }
370
+
371
+ function TerminalLog({ logs }: { logs: string[] }) {
372
+ const endRef = useRef<HTMLDivElement>(null);
373
+
374
+ useEffect(() => {
375
+ endRef.current?.scrollIntoView({ behavior: "smooth" });
376
+ }, [logs]);
377
+
378
+ return (
379
+ <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden", minHeight: 0 }}>
380
+ <div
381
+ style={{
382
+ padding: "6px 18px 4px",
383
+ borderBottom: `1px solid ${C.border}`,
384
+ flexShrink: 0,
385
+ display: "flex",
386
+ alignItems: "center",
387
+ gap: 8,
388
+ }}
389
+ >
390
+ <span style={{ fontSize: 10, color: C.dim, letterSpacing: "0.1em", textTransform: "uppercase" }}>terminal</span>
391
+ {logs.length > 0 && <span style={{ fontSize: 10, color: C.dim }}>{logs.length} lines</span>}
392
+ </div>
393
+ <div
394
+ style={{
395
+ flex: 1,
396
+ overflow: "auto",
397
+ padding: "10px 18px",
398
+ background: "#070c10",
399
+ fontSize: 11.5,
400
+ lineHeight: 1.7,
401
+ }}
402
+ >
403
+ {logs.length === 0 ? (
404
+ <span style={{ color: C.dim }}>
405
+ <BlinkCursor />
406
+ </span>
407
+ ) : (
408
+ logs.map((log, i) => (
409
+ <div
410
+ key={i}
411
+ style={{
412
+ color: logColor(log),
413
+ display: "flex",
414
+ gap: 8,
415
+ animation: "fadein 0.1s ease",
416
+ }}
417
+ >
418
+ <span style={{ color: C.dim, userSelect: "none", flexShrink: 0 }}>›</span>
419
+ <span style={{ wordBreak: "break-word", whiteSpace: "pre-wrap" }}>{log}</span>
420
+ </div>
421
+ ))
422
+ )}
423
+ <div ref={endRef} />
424
+ </div>
425
+ </div>
426
+ );
427
+ }
428
+
429
+ function BlinkCursor() {
430
+ return (
431
+ <span
432
+ style={{
433
+ display: "inline-block",
434
+ width: 7,
435
+ height: 13,
436
+ background: C.dim,
437
+ verticalAlign: "middle",
438
+ animation: "blink 1.2s step-end infinite",
439
+ borderRadius: 1,
440
+ }}
441
+ />
442
+ );
443
+ }
frontend/src/types.ts ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface Finding {
2
+ title: string;
3
+ description: string;
4
+ recommendation: string;
5
+ severity: "high" | "medium" | "low";
6
+ codeSnippet: string;
7
+ location: string;
8
+ path: string;
9
+ judgeReview: {
10
+ review: string;
11
+ confidence: number;
12
+ exploitablePaths: string[];
13
+ };
14
+ }
15
+
16
+ export interface CoderResult {
17
+ contract: string;
18
+ compilationErrors: string[];
19
+ reviewSummary: string;
20
+ }
21
+
22
+ export interface AuditorResult {
23
+ findings: Finding[];
24
+ }
25
+
26
+ export interface TesterResult {
27
+ status: "success" | "failed" | "timeout" | "skipped" | "running";
28
+ pocCode?: string;
29
+ executionLogs?: string[];
30
+ iterations: number;
31
+ }
32
+
33
+ export type StepStatus = "pending" | "running" | "done" | "error" | "skipped";
34
+
35
+ export interface PipelineStep {
36
+ id: string;
37
+ label: string;
38
+ status: StepStatus;
39
+ detail?: string;
40
+ }
41
+
42
+ export interface AgentState {
43
+ id: "coder" | "auditor" | "tester";
44
+ label: string;
45
+ color: string;
46
+ status: StepStatus;
47
+ steps: PipelineStep[];
48
+ }
frontend/src/utils/diff.ts ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface DiffLine {
2
+ type: "added" | "removed" | "unchanged";
3
+ line: string;
4
+ lineNo: number;
5
+ }
6
+
7
+ export function computeDiff(prev: string | null, next: string): DiffLine[] {
8
+ const nextLines = next.split("\n");
9
+
10
+ if (!prev) {
11
+ return nextLines.map((line, i) => ({ type: "added", line, lineNo: i + 1 }));
12
+ }
13
+ if (prev === next) {
14
+ return nextLines.map((line, i) => ({ type: "unchanged", line, lineNo: i + 1 }));
15
+ }
16
+
17
+ const prevLines = prev.split("\n");
18
+ const m = prevLines.length;
19
+ const n = nextLines.length;
20
+
21
+ if (m > 600 || n > 600) {
22
+ return nextLines.map((line, i) => ({ type: "added", line, lineNo: i + 1 }));
23
+ }
24
+
25
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0) as number[]);
26
+ for (let i = 1; i <= m; i++) {
27
+ for (let j = 1; j <= n; j++) {
28
+ dp[i][j] = prevLines[i - 1] === nextLines[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);
29
+ }
30
+ }
31
+
32
+ const result: DiffLine[] = [];
33
+ let i = m;
34
+ let j = n;
35
+ while (i > 0 || j > 0) {
36
+ if (i > 0 && j > 0 && prevLines[i - 1] === nextLines[j - 1]) {
37
+ result.unshift({ type: "unchanged", line: nextLines[j - 1], lineNo: j });
38
+ i--;
39
+ j--;
40
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
41
+ result.unshift({ type: "added", line: nextLines[j - 1], lineNo: j });
42
+ j--;
43
+ } else {
44
+ result.unshift({ type: "removed", line: prevLines[i - 1], lineNo: 0 });
45
+ i--;
46
+ }
47
+ }
48
+
49
+ return result;
50
+ }
51
+
52
+ export function diffHasChanges(lines: DiffLine[]): boolean {
53
+ return lines.some((l) => l.type !== "unchanged");
54
+ }
frontend/src/utils/status.ts ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { AgentState, StepStatus } from "../types";
2
+
3
+ export interface StepEvent {
4
+ agent: "coder" | "auditor" | "tester";
5
+ step: string;
6
+ status: "running" | "done" | "error" | "skipped";
7
+ detail?: string;
8
+ }
9
+
10
+ export const INITIAL_AGENT_STATES: AgentState[] = [
11
+ {
12
+ id: "coder",
13
+ label: "CODER",
14
+ color: "#58a6ff",
15
+ status: "pending",
16
+ steps: [
17
+ { id: "coder.gen", label: "Gerando contrato", status: "pending" },
18
+ { id: "coder.compile", label: "Compilando", status: "pending" },
19
+ { id: "coder.review", label: "Revisando segurança", status: "pending" },
20
+ ],
21
+ },
22
+ {
23
+ id: "auditor",
24
+ label: "AUDITOR",
25
+ color: "#d29922",
26
+ status: "pending",
27
+ steps: [
28
+ { id: "audit.scope", label: "Mapeando escopo", status: "pending" },
29
+ { id: "audit.ctx", label: "Coletando contexto", status: "pending" },
30
+ { id: "audit.find", label: "Analisando vulnerabilidades", status: "pending" },
31
+ { id: "audit.judge", label: "Julgando findings", status: "pending" },
32
+ ],
33
+ },
34
+ {
35
+ id: "tester",
36
+ label: "TESTER",
37
+ color: "#3fb950",
38
+ status: "pending",
39
+ steps: [
40
+ { id: "test.oracle", label: "Preparando scaffold", status: "pending" },
41
+ { id: "test.gen", label: "Gerando PoC", status: "pending" },
42
+ { id: "test.run", label: "Executando Foundry", status: "pending" },
43
+ { id: "test.reflect", label: "Analisando falha", status: "pending" },
44
+ ],
45
+ },
46
+ ];
47
+
48
+ const AGENT_ORDER = ["coder", "auditor", "tester"] as const;
49
+
50
+ const STEP_PREFIX: Record<string, string> = {
51
+ coder: "coder.",
52
+ auditor: "audit.",
53
+ tester: "test.",
54
+ };
55
+
56
+ export function applyStepEvent(states: AgentState[], event: StepEvent): AgentState[] {
57
+ const s = states.map((a) => ({ ...a, steps: a.steps.map((st) => ({ ...st })) }));
58
+
59
+ const fullStepId = STEP_PREFIX[event.agent] + event.step;
60
+
61
+ if (event.status === "running") {
62
+ // Mark all preceding agents as done when a new agent starts its first step
63
+ const agentIndex = AGENT_ORDER.indexOf(event.agent);
64
+ for (let i = 0; i < agentIndex; i++) {
65
+ const prev = s.find((a) => a.id === AGENT_ORDER[i]);
66
+ if (prev && prev.status !== "done" && prev.status !== "error") {
67
+ prev.status = "done";
68
+ prev.steps.forEach((st) => {
69
+ if (st.status === "pending") st.status = "done";
70
+ });
71
+ }
72
+ }
73
+
74
+ const agent = s.find((a) => a.id === event.agent);
75
+ if (agent && agent.status === "pending") agent.status = "running";
76
+ }
77
+
78
+ // Update the step
79
+ const step = s.flatMap((a) => a.steps).find((st) => st.id === fullStepId);
80
+ if (step) {
81
+ step.status = event.status;
82
+ if (event.detail !== undefined) step.detail = event.detail;
83
+ }
84
+
85
+ // When a step finishes, check if all steps are settled → update agent status
86
+ if (event.status !== "running") {
87
+ const agent = s.find((a) => a.id === event.agent);
88
+ if (agent) {
89
+ const settled: StepStatus[] = ["done", "error", "skipped"];
90
+ const allSettled = agent.steps.every((st) => settled.includes(st.status));
91
+ if (allSettled) {
92
+ const hasError = agent.steps.some((st) => st.status === "error");
93
+ const allSkipped = agent.steps.every((st) => st.status === "skipped");
94
+ agent.status = hasError ? "error" : allSkipped ? "skipped" : "done";
95
+ }
96
+ }
97
+ }
98
+
99
+ return s;
100
+ }
src/agents/auditor/agent.ts CHANGED
@@ -5,7 +5,7 @@ import { END, type GraphNode, START, StateGraph } from "@langchain/langgraph";
5
  import { z } from "zod";
6
 
7
  import { createLLM } from "../../config/llm.ts";
8
- import { logger } from "../../logger.ts";
9
  import { MAX_DOC_CHARS, MAX_REFLECTIONS, MAX_SOL_CHARS, MIN_FILE_IMPORTANCE } from "./config.ts";
10
  import {
11
  FIND_VULNERABILITIES_PROMPT,
@@ -24,7 +24,8 @@ const llmOpus = createLLM("anthropic", { model: "claude-opus-4-8", temperature:
24
  const llmSonnet = createLLM("anthropic", { model: "claude-sonnet-4-6", maxTokens: 20000 });
25
 
26
  const defineScope: GraphNode<typeof AuditorState> = async (state) => {
27
- logger.info(`defineScope: walking repo at ${state.repoPath}`);
 
28
 
29
  const solFiles: string[] = [];
30
  const docFiles: string[] = [];
@@ -33,12 +34,16 @@ const defineScope: GraphNode<typeof AuditorState> = async (state) => {
33
 
34
  const fileTree = buildRepoTree(state.repoPath);
35
 
36
- logger.info(`defineScope: found ${solFiles.length} Solidity file(s), ${docFiles.length} doc file(s)`);
37
- logger.debug(`defineScope: Solidity files: ${JSON.stringify(solFiles)}`);
38
- logger.debug(`defineScope: doc files: ${JSON.stringify(docFiles)}`);
39
- logger.debug(`defineScope: file tree:\n${fileTree}`);
 
 
 
 
40
 
41
- logger.info("defineScope: ranking files by importance");
42
 
43
  const RankFilesSchema = z.object({ rankings: z.array(FileRankingSchema) });
44
  const rankingModel = llmHaiku.withStructuredOutput(RankFilesSchema);
@@ -52,20 +57,25 @@ const defineScope: GraphNode<typeof AuditorState> = async (state) => {
52
 
53
  const sorted = [...rankings].sort((a, b) => b.importance - a.importance);
54
  logger.info(
55
- `defineScope: rankings:\n${sorted.map((r) => ` [${r.importance}/5] ${r.filePath} — ${r.reasoning}`).join("\n")}`,
56
  );
57
 
58
  const importantFiles = sorted.filter((r) => r.importance >= MIN_FILE_IMPORTANCE).map((r) => r.filePath);
59
  const skipped = solFiles.length - importantFiles.length;
60
  if (skipped > 0) {
61
- logger.info(`defineScope: skipping ${skipped} low-importance file(s) (importance < ${MIN_FILE_IMPORTANCE})`);
 
 
62
  }
63
 
64
  return { scope: importantFiles, docs: docFiles, fileTree, fileRankings: sorted };
65
  };
66
 
67
  const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
68
- logger.info(`gatherContext: processing ${state.scope.length} Solidity file(s) and ${state.docs.length} doc file(s)`);
 
 
 
69
 
70
  const readFile = (filePath: string): string => {
71
  try {
@@ -120,9 +130,11 @@ const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
120
  const structuralBlock = `## Análise Estrutural dos Contratos\n\n${solidityEntries.map(({ analysis }) => analysis).join("\n\n---\n\n")}`;
121
  const repoContext = [result.context, fileTreeBlock, structuralBlock].join("\n\n");
122
 
123
- logger.debug(`gatherContext: full context:\n${parts.join("\n\n")}`);
124
- logger.info(`gatherContext: context built (${repoContext.length} chars)`);
125
- logger.debug(`gatherContext: compact context:\n${repoContext}`);
 
 
126
 
127
  return { repoContext };
128
  };
@@ -133,9 +145,11 @@ const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
133
  const isReflection = state.judgeReviews.length > 0;
134
 
135
  logger.info(
136
- `findVulnerabilities: invoking LLM for ${state.scope.length} file(s) in parallel (iteration ${state.reflectionCount + 1})`,
137
  );
138
 
 
 
139
  const cachedContext = {
140
  type: "text" as const,
141
  text: `Contexto do Protocolo:\n${state.repoContext}`,
@@ -163,7 +177,7 @@ const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
163
  ? `Contrato (${filePath}):\n\n${source}\n\n${buildReviewBlocks(fileEntries, state.reflectionCount)}`
164
  : `Contrato (${filePath}):\n\n${source}`;
165
 
166
- logger.debug(`findVulnerabilities: processing ${filePath}`);
167
 
168
  const result = await model.invoke([
169
  new SystemMessage({ content: [{ type: "text", text: promptText, cache_control: { type: "ephemeral" } }] }),
@@ -182,15 +196,20 @@ const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
182
  const restFindings = await Promise.all(restFiles.map(processFile));
183
  const candidateFindings = [firstFindings, ...restFindings].flat();
184
 
185
- logger.info(`findVulnerabilities: LLM returned ${candidateFindings.length} total candidate finding(s)`);
186
- logger.debug(`findVulnerabilities: findings:\n${JSON.stringify(candidateFindings, null, 2)}`);
 
 
187
 
 
188
  return { candidateFindings };
189
  };
190
 
191
  const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
 
192
  if (state.candidateFindings.length === 0) {
193
- logger.info("judgeFindings: no candidate findings to review, skipping LLM call");
 
194
  return {
195
  judgeReviews: [],
196
  findings: [],
@@ -200,7 +219,9 @@ const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
200
 
201
  const model = llmSonnet.withStructuredOutput(JudgeReviewSchema);
202
 
203
- logger.info(`judgeFindings: reviewing ${state.candidateFindings.length} candidate finding(s) in parallel`);
 
 
204
 
205
  const cachedContext = {
206
  type: "text" as const,
@@ -218,7 +239,7 @@ const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
218
 
219
  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\`\`\``;
220
 
221
- logger.debug(`judgeFindings: reviewing finding ${i + 1}: ${finding.title}`);
222
  return model.invoke([
223
  new SystemMessage({
224
  content: [{ type: "text", text: JUDGE_FINDINGS_PROMPT, cache_control: { type: "ephemeral" } }],
@@ -252,9 +273,10 @@ const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
252
 
253
  const falsePositiveCount = state.candidateFindings.length - findings.length;
254
 
255
- logger.info(`judgeFindings: ${findings.length} confirmed, ${falsePositiveCount} false positive(s)`);
256
- logger.debug(`judgeFindings: reviews:\n${JSON.stringify(reviews, null, 2)}`);
257
 
 
258
  return {
259
  judgeReviews: reviews,
260
  findings,
 
5
  import { z } from "zod";
6
 
7
  import { createLLM } from "../../config/llm.ts";
8
+ import { emitStep, logger } from "../../logger.ts";
9
  import { MAX_DOC_CHARS, MAX_REFLECTIONS, MAX_SOL_CHARS, MIN_FILE_IMPORTANCE } from "./config.ts";
10
  import {
11
  FIND_VULNERABILITIES_PROMPT,
 
24
  const llmSonnet = createLLM("anthropic", { model: "claude-sonnet-4-6", maxTokens: 20000 });
25
 
26
  const defineScope: GraphNode<typeof AuditorState> = async (state) => {
27
+ emitStep({ agent: "auditor", step: "scope", status: "running" });
28
+ logger.info(`[Auditor] defineScope: percorrendo repositório em ${state.repoPath}`);
29
 
30
  const solFiles: string[] = [];
31
  const docFiles: string[] = [];
 
34
 
35
  const fileTree = buildRepoTree(state.repoPath);
36
 
37
+ logger.info(
38
+ `[Auditor] defineScope: encontrado(s) ${solFiles.length} arquivo(s) Solidity e ${docFiles.length} arquivo(s) de documentação`,
39
+ );
40
+ logger.debug(`[Auditor] defineScope: arquivos Solidity: ${JSON.stringify(solFiles)}`);
41
+ logger.debug(`[Auditor] defineScope: arquivos de documentação: ${JSON.stringify(docFiles)}`);
42
+ logger.debug(`[Auditor] defineScope: árvore de arquivos:\n${fileTree}`);
43
+
44
+ emitStep({ agent: "auditor", step: "scope", status: "done" });
45
 
46
+ logger.info("[Auditor] defineScope: rankeando arquivos por importância");
47
 
48
  const RankFilesSchema = z.object({ rankings: z.array(FileRankingSchema) });
49
  const rankingModel = llmHaiku.withStructuredOutput(RankFilesSchema);
 
57
 
58
  const sorted = [...rankings].sort((a, b) => b.importance - a.importance);
59
  logger.info(
60
+ `[Auditor] defineScope: rankings:\n${sorted.map((r) => ` [${r.importance}/5] ${r.filePath} — ${r.reasoning}`).join("\n")}`,
61
  );
62
 
63
  const importantFiles = sorted.filter((r) => r.importance >= MIN_FILE_IMPORTANCE).map((r) => r.filePath);
64
  const skipped = solFiles.length - importantFiles.length;
65
  if (skipped > 0) {
66
+ logger.info(
67
+ `[Auditor] defineScope: pulando ${skipped} arquivo(s) de baixa importância (importância < ${MIN_FILE_IMPORTANCE})`,
68
+ );
69
  }
70
 
71
  return { scope: importantFiles, docs: docFiles, fileTree, fileRankings: sorted };
72
  };
73
 
74
  const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
75
+ emitStep({ agent: "auditor", step: "ctx", status: "running" });
76
+ logger.info(
77
+ `[Auditor] gatherContext: processando ${state.scope.length} arquivo(s) Solidity e ${state.docs.length} arquivo(s) de documentação`,
78
+ );
79
 
80
  const readFile = (filePath: string): string => {
81
  try {
 
130
  const structuralBlock = `## Análise Estrutural dos Contratos\n\n${solidityEntries.map(({ analysis }) => analysis).join("\n\n---\n\n")}`;
131
  const repoContext = [result.context, fileTreeBlock, structuralBlock].join("\n\n");
132
 
133
+ logger.debug(`[Auditor] gatherContext: contexto completo:\n${parts.join("\n\n")}`);
134
+ logger.info(`[Auditor] gatherContext: contexto construído (${repoContext.length} caracteres)`);
135
+ logger.debug(`[Auditor] gatherContext: contexto compactado:\n${repoContext}`);
136
+
137
+ emitStep({ agent: "auditor", step: "ctx", status: "done" });
138
 
139
  return { repoContext };
140
  };
 
145
  const isReflection = state.judgeReviews.length > 0;
146
 
147
  logger.info(
148
+ `[Auditor] findVulnerabilities: invocando LLM para ${state.scope.length} arquivo(s) em paralelo (iteração ${state.reflectionCount + 1})`,
149
  );
150
 
151
+ emitStep({ agent: "auditor", step: "find", status: "running", detail: `iter ${state.reflectionCount + 1}` });
152
+
153
  const cachedContext = {
154
  type: "text" as const,
155
  text: `Contexto do Protocolo:\n${state.repoContext}`,
 
177
  ? `Contrato (${filePath}):\n\n${source}\n\n${buildReviewBlocks(fileEntries, state.reflectionCount)}`
178
  : `Contrato (${filePath}):\n\n${source}`;
179
 
180
+ logger.debug(`[Auditor] findVulnerabilities: processando ${filePath}`);
181
 
182
  const result = await model.invoke([
183
  new SystemMessage({ content: [{ type: "text", text: promptText, cache_control: { type: "ephemeral" } }] }),
 
196
  const restFindings = await Promise.all(restFiles.map(processFile));
197
  const candidateFindings = [firstFindings, ...restFindings].flat();
198
 
199
+ logger.info(
200
+ `[Auditor] findVulnerabilities: LLM retornou ${candidateFindings.length} finding(s) candidato(s) no total`,
201
+ );
202
+ logger.debug(`[Auditor] findVulnerabilities: findings:\n${JSON.stringify(candidateFindings, null, 2)}`);
203
 
204
+ emitStep({ agent: "auditor", step: "find", status: "done" });
205
  return { candidateFindings };
206
  };
207
 
208
  const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
209
+ emitStep({ agent: "auditor", step: "judge", status: "running" });
210
  if (state.candidateFindings.length === 0) {
211
+ logger.info("[Auditor] judgeFindings: sem findings candidatos para revisar, pulando chamada ao LLM");
212
+ emitStep({ agent: "auditor", step: "judge", status: "done" });
213
  return {
214
  judgeReviews: [],
215
  findings: [],
 
219
 
220
  const model = llmSonnet.withStructuredOutput(JudgeReviewSchema);
221
 
222
+ logger.info(
223
+ `[Auditor] judgeFindings: revisando ${state.candidateFindings.length} finding(s) candidato(s) em paralelo`,
224
+ );
225
 
226
  const cachedContext = {
227
  type: "text" as const,
 
239
 
240
  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\`\`\``;
241
 
242
+ logger.debug(`[Auditor] judgeFindings: revisando finding ${i + 1}: ${finding.title}`);
243
  return model.invoke([
244
  new SystemMessage({
245
  content: [{ type: "text", text: JUDGE_FINDINGS_PROMPT, cache_control: { type: "ephemeral" } }],
 
273
 
274
  const falsePositiveCount = state.candidateFindings.length - findings.length;
275
 
276
+ logger.info(`[Auditor] judgeFindings: ${findings.length} confirmado(s), ${falsePositiveCount} falso(s) positivo(s)`);
277
+ logger.debug(`[Auditor] judgeFindings: revisões:\n${JSON.stringify(reviews, null, 2)}`);
278
 
279
+ emitStep({ agent: "auditor", step: "judge", status: "done" });
280
  return {
281
  judgeReviews: reviews,
282
  findings,
src/agents/coder/agent.ts CHANGED
@@ -4,6 +4,7 @@ import { CoderState } from "./state.ts";
4
  import { solidityCoderPrompt, solidityFixPrompt, solidityReviewPrompt } from "./prompts.ts";
5
  import { compileSolidityTool } from "./tools/compile-solidity.ts";
6
  import { createLLM } from "../../config/llm.ts";
 
7
 
8
  const MAX_FIX_ATTEMPTS = 3;
9
 
@@ -21,6 +22,7 @@ function extractSolidityCode(text: string): string {
21
  * Nó 1: Gera o smart contract a partir dos requisitos.
22
  */
23
  const generateContract: GraphNode<typeof CoderState> = async (state) => {
 
24
  const llm = createLLM();
25
  const chain = solidityCoderPrompt.pipe(llm);
26
 
@@ -33,6 +35,7 @@ const generateContract: GraphNode<typeof CoderState> = async (state) => {
33
  typeof result.content === "string" ? result.content : JSON.stringify(result.content),
34
  );
35
 
 
36
  return { contract: code, compilationErrors: [] };
37
  };
38
 
@@ -40,11 +43,15 @@ const generateContract: GraphNode<typeof CoderState> = async (state) => {
40
  * Nó 2: Compila o contrato e armazena erros (se houver).
41
  */
42
  const compileContract: GraphNode<typeof CoderState> = async (state) => {
 
43
  const result = await compileSolidityTool.invoke({
44
  sourceCode: state.contract,
45
  filename: "Contract.sol",
46
  });
47
 
 
 
 
48
  return { compilationErrors: result.errors };
49
  };
50
 
@@ -52,6 +59,7 @@ const compileContract: GraphNode<typeof CoderState> = async (state) => {
52
  * Nó 3: Corrige o contrato com base nos erros de compilação.
53
  */
54
  const fixContract: GraphNode<typeof CoderState> = async (state) => {
 
55
  const llm = createLLM();
56
  const chain = solidityFixPrompt.pipe(llm);
57
 
@@ -73,6 +81,7 @@ const fixContract: GraphNode<typeof CoderState> = async (state) => {
73
  * Nó 4: Revisa o contrato compilado quanto a segurança e boas práticas.
74
  */
75
  const reviewContract: GraphNode<typeof CoderState> = async (state) => {
 
76
  const llm = createLLM();
77
  const chain = solidityReviewPrompt.pipe(llm);
78
 
@@ -86,6 +95,7 @@ const reviewContract: GraphNode<typeof CoderState> = async (state) => {
86
  const summary =
87
  typeof result.content === "string" ? result.content : JSON.stringify(result.content);
88
 
 
89
  return { reviewSummary: summary };
90
  };
91
 
@@ -100,7 +110,10 @@ function shouldFix(state: { compilationErrors: string[] }): "fixContract" | "rev
100
  fixAttempts++;
101
  return "fixContract";
102
  }
103
- fixAttempts = 0; // reset para próxima execução
 
 
 
104
  return "reviewContract";
105
  }
106
 
 
4
  import { solidityCoderPrompt, solidityFixPrompt, solidityReviewPrompt } from "./prompts.ts";
5
  import { compileSolidityTool } from "./tools/compile-solidity.ts";
6
  import { createLLM } from "../../config/llm.ts";
7
+ import { emitStep } from "../../logger.ts";
8
 
9
  const MAX_FIX_ATTEMPTS = 3;
10
 
 
22
  * Nó 1: Gera o smart contract a partir dos requisitos.
23
  */
24
  const generateContract: GraphNode<typeof CoderState> = async (state) => {
25
+ emitStep({ agent: "coder", step: "gen", status: "running" });
26
  const llm = createLLM();
27
  const chain = solidityCoderPrompt.pipe(llm);
28
 
 
35
  typeof result.content === "string" ? result.content : JSON.stringify(result.content),
36
  );
37
 
38
+ emitStep({ agent: "coder", step: "gen", status: "done" });
39
  return { contract: code, compilationErrors: [] };
40
  };
41
 
 
43
  * Nó 2: Compila o contrato e armazena erros (se houver).
44
  */
45
  const compileContract: GraphNode<typeof CoderState> = async (state) => {
46
+ emitStep({ agent: "coder", step: "compile", status: "running" });
47
  const result = await compileSolidityTool.invoke({
48
  sourceCode: state.contract,
49
  filename: "Contract.sol",
50
  });
51
 
52
+ if (result.errors.length === 0) {
53
+ emitStep({ agent: "coder", step: "compile", status: "done" });
54
+ }
55
  return { compilationErrors: result.errors };
56
  };
57
 
 
59
  * Nó 3: Corrige o contrato com base nos erros de compilação.
60
  */
61
  const fixContract: GraphNode<typeof CoderState> = async (state) => {
62
+ emitStep({ agent: "coder", step: "compile", status: "running", detail: `fix ${fixAttempts}/${MAX_FIX_ATTEMPTS}` });
63
  const llm = createLLM();
64
  const chain = solidityFixPrompt.pipe(llm);
65
 
 
81
  * Nó 4: Revisa o contrato compilado quanto a segurança e boas práticas.
82
  */
83
  const reviewContract: GraphNode<typeof CoderState> = async (state) => {
84
+ emitStep({ agent: "coder", step: "review", status: "running" });
85
  const llm = createLLM();
86
  const chain = solidityReviewPrompt.pipe(llm);
87
 
 
95
  const summary =
96
  typeof result.content === "string" ? result.content : JSON.stringify(result.content);
97
 
98
+ emitStep({ agent: "coder", step: "review", status: "done" });
99
  return { reviewSummary: summary };
100
  };
101
 
 
110
  fixAttempts++;
111
  return "fixContract";
112
  }
113
+ if (state.compilationErrors.length > 0) {
114
+ emitStep({ agent: "coder", step: "compile", status: "error" });
115
+ }
116
+ fixAttempts = 0;
117
  return "reviewContract";
118
  }
119
 
src/agents/tester/agent.ts CHANGED
@@ -1,25 +1,28 @@
1
- import "dotenv/config";
2
  import { StateGraph, END, START } from "@langchain/langgraph";
3
- import { PoCStateAnnotation, PoCState } from "./state.js";
 
4
  import { generateLocalScaffold } from "./tools/scaffoldGenerator.js";
5
- import { OracleContext } from "./types.js";
6
  import { createLLM } from "../../config/llm.ts";
7
  import { SYSTEM_PROMPT } from "./prompts/system.js";
8
  import { extractSolidity } from "./utils/extractSolidity.js";
9
  import { runFoundry } from "./tools/foundryRunner.js";
10
  import { analyzeFoundryLog } from "./utils/logAnalyzer.js";
 
11
 
12
  const MAX_ITERATIONS = 5;
13
 
14
  const llm = createLLM();
15
 
16
  async function oracleNode(state: PoCState): Promise<Partial<PoCState>> {
17
- console.log("[oracleNode] gerando scaffold para:", state.report.title);
 
18
 
19
  const solidityScaffold = generateLocalScaffold(state.report);
20
  const oracleContext: OracleContext = { solidityScaffold };
21
 
22
- console.log("[oracleNode] scaffold gerado, tamanho:", solidityScaffold.length, "chars");
 
23
  return { oracleContext };
24
  }
25
 
@@ -53,7 +56,8 @@ Scaffold (complete APENAS test_Exploit):
53
  ${oracleContext!.solidityScaffold}
54
  \`\`\``;
55
 
56
- console.log(`[testerAgent] generatePoCNode iteração ${iterations + 1}, isRetry=${isRetry}`);
 
57
 
58
  try {
59
  const response = await llm.invoke([
@@ -61,16 +65,19 @@ ${oracleContext!.solidityScaffold}
61
  { role: "user", content: userMessage },
62
  ]);
63
  const solidityCode = extractSolidity(response.content as string);
64
- console.log("[testerAgent] Solidity extraído, tamanho:", solidityCode.length);
 
65
  return { pocCode: solidityCode, iterations: 1 };
66
  } catch (err) {
67
- console.error("[testerAgent] falha na geração:", (err as Error).message);
 
68
  return { iterations: 1, lastError: `Erro na geração/extração: ${(err as Error).message}` };
69
  }
70
  }
71
 
72
  async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
73
- console.log("[testerAgent] Executando runFoundryNode...");
 
74
 
75
  const trimmedCode = state.pocCode.trim();
76
  const isMissingCode = trimmedCode.length === 0;
@@ -78,14 +85,15 @@ async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
78
  const isMissingTest = !trimmedCode.includes("function test_Exploit()");
79
  const isPlaceholder = trimmedCode.includes("TODO: implementar exploit");
80
  if (isMissingCode || isMissingContract || isMissingTest || isPlaceholder) {
81
- const summary = state.lastError ?? (isMissingCode
82
- ? "Código Solidity ausente. O LLM não retornou o arquivo do exploit."
83
- : isMissingContract
84
- ? "Contrato ExploitTest não encontrado no arquivo."
85
- : isMissingTest
86
- ? "Função test_Exploit() não encontrada no arquivo."
87
- : "Exploit não implementado (placeholder TODO ainda presente)."
88
- );
 
89
  const status = state.iterations >= MAX_ITERATIONS ? "failed" : "running";
90
  return {
91
  executionLogs: [summary],
@@ -94,51 +102,52 @@ async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
94
  };
95
  }
96
 
97
- const result = await runFoundry(state.pocCode);
98
  const analysis = analyzeFoundryLog(result);
99
  const noTestsFound = result.combined.includes("No tests found");
100
  const summary = noTestsFound
101
  ? "Forge não encontrou nenhum teste. Verifique se o contrato se chama ExploitTest e se existe test_Exploit()."
102
  : analysis.summary;
103
- const passed = result.exitCode === 0 && result.stdout.includes("ok") && !noTestsFound;
104
  const isLastAttempt = state.iterations >= MAX_ITERATIONS;
105
 
106
- const status = passed
107
- ? "success"
108
- : result.timedOut
109
- ? "timeout"
110
- : isLastAttempt
111
- ? "failed"
112
- : "running";
113
 
114
- console.log(`[testerAgent] Resultado Foundry: exitCode=${result.exitCode}, passed=${passed}`);
115
  if (!passed) {
116
- console.log(`[testerAgent] Falha detectada: ${analysis.summary}`);
117
  }
118
 
 
 
119
  return {
120
- executionLogs: [result.combined], // reducer append
121
  lastError: summary,
122
  status,
123
  };
124
  }
125
 
126
  async function reflectNode(state: PoCState): Promise<Partial<PoCState>> {
 
127
  const lastLog = state.executionLogs[state.executionLogs.length - 1];
128
  if (!lastLog) {
129
  return { lastError: "Sem logs disponíveis para análise." };
130
  }
131
 
132
  const mockResult = {
133
- exitCode: 1, timedOut: lastLog.includes("TIMEOUT"),
134
- stdout: "", stderr: "", combined: lastLog,
 
 
 
135
  };
136
 
137
  const analysis = analyzeFoundryLog(mockResult as any);
138
 
139
- console.log(`[reflectNode] categoria: ${analysis.category}`);
140
- console.log(`[reflectNode] resumo: ${analysis.summary}`);
141
 
 
142
  return {
143
  lastError: `[${analysis.category.toUpperCase()}] ${analysis.summary}\n\nLinhas relevantes:\n${analysis.relevantLines.join("\n")}`,
144
  };
 
 
1
  import { StateGraph, END, START } from "@langchain/langgraph";
2
+
3
+ import { PoCStateAnnotation, type PoCState } from "./state.js";
4
  import { generateLocalScaffold } from "./tools/scaffoldGenerator.js";
5
+ import type { OracleContext } from "./types.js";
6
  import { createLLM } from "../../config/llm.ts";
7
  import { SYSTEM_PROMPT } from "./prompts/system.js";
8
  import { extractSolidity } from "./utils/extractSolidity.js";
9
  import { runFoundry } from "./tools/foundryRunner.js";
10
  import { analyzeFoundryLog } from "./utils/logAnalyzer.js";
11
+ import { logger, emitStep } from "../../logger.ts";
12
 
13
  const MAX_ITERATIONS = 5;
14
 
15
  const llm = createLLM();
16
 
17
  async function oracleNode(state: PoCState): Promise<Partial<PoCState>> {
18
+ emitStep({ agent: "tester", step: "oracle", status: "running" });
19
+ logger.info(`[Tester] oracleNode: gerando scaffold para: ${state.report.title}`);
20
 
21
  const solidityScaffold = generateLocalScaffold(state.report);
22
  const oracleContext: OracleContext = { solidityScaffold };
23
 
24
+ logger.info(`[Tester] oracleNode: scaffold gerado, tamanho: ${solidityScaffold.length} chars`);
25
+ emitStep({ agent: "tester", step: "oracle", status: "done" });
26
  return { oracleContext };
27
  }
28
 
 
56
  ${oracleContext!.solidityScaffold}
57
  \`\`\``;
58
 
59
+ logger.info(`[Tester] generatePoCNode: iteração ${iterations + 1}, isRetry=${isRetry}`);
60
+ emitStep({ agent: "tester", step: "gen", status: "running", detail: `iter ${iterations + 1}` });
61
 
62
  try {
63
  const response = await llm.invoke([
 
65
  { role: "user", content: userMessage },
66
  ]);
67
  const solidityCode = extractSolidity(response.content as string);
68
+ logger.info(`[Tester] generatePoCNode: Solidity extraído, tamanho: ${solidityCode.length}`);
69
+ emitStep({ agent: "tester", step: "gen", status: "done" });
70
  return { pocCode: solidityCode, iterations: 1 };
71
  } catch (err) {
72
+ logger.error(`[Tester] generatePoCNode: falha na geração: ${(err as Error).message}`);
73
+ emitStep({ agent: "tester", step: "gen", status: "error" });
74
  return { iterations: 1, lastError: `Erro na geração/extração: ${(err as Error).message}` };
75
  }
76
  }
77
 
78
  async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
79
+ emitStep({ agent: "tester", step: "run", status: "running" });
80
+ logger.info("[Tester] runFoundryNode: executando...");
81
 
82
  const trimmedCode = state.pocCode.trim();
83
  const isMissingCode = trimmedCode.length === 0;
 
85
  const isMissingTest = !trimmedCode.includes("function test_Exploit()");
86
  const isPlaceholder = trimmedCode.includes("TODO: implementar exploit");
87
  if (isMissingCode || isMissingContract || isMissingTest || isPlaceholder) {
88
+ const summary =
89
+ state.lastError ??
90
+ (isMissingCode
91
+ ? "Código Solidity ausente. O LLM não retornou o arquivo do exploit."
92
+ : isMissingContract
93
+ ? "Contrato ExploitTest não encontrado no arquivo."
94
+ : isMissingTest
95
+ ? "Função test_Exploit() não encontrada no arquivo."
96
+ : "Exploit não implementado (placeholder TODO ainda presente).");
97
  const status = state.iterations >= MAX_ITERATIONS ? "failed" : "running";
98
  return {
99
  executionLogs: [summary],
 
102
  };
103
  }
104
 
105
+ const result = await runFoundry(state.pocCode);
106
  const analysis = analyzeFoundryLog(result);
107
  const noTestsFound = result.combined.includes("No tests found");
108
  const summary = noTestsFound
109
  ? "Forge não encontrou nenhum teste. Verifique se o contrato se chama ExploitTest e se existe test_Exploit()."
110
  : analysis.summary;
111
+ const passed = result.exitCode === 0 && result.stdout.includes("ok") && !noTestsFound;
112
  const isLastAttempt = state.iterations >= MAX_ITERATIONS;
113
 
114
+ const status = passed ? "success" : result.timedOut ? "timeout" : isLastAttempt ? "failed" : "running";
 
 
 
 
 
 
115
 
116
+ logger.info(`[Tester] runFoundryNode: resultado Foundry: exitCode=${result.exitCode}, passed=${passed}`);
117
  if (!passed) {
118
+ logger.info(`[Tester] runFoundryNode: falha detectada: ${analysis.summary}`);
119
  }
120
 
121
+ emitStep({ agent: "tester", step: "run", status: passed ? "done" : result.timedOut ? "error" : "done" });
122
+
123
  return {
124
+ executionLogs: [result.combined], // reducer append
125
  lastError: summary,
126
  status,
127
  };
128
  }
129
 
130
  async function reflectNode(state: PoCState): Promise<Partial<PoCState>> {
131
+ emitStep({ agent: "tester", step: "reflect", status: "running" });
132
  const lastLog = state.executionLogs[state.executionLogs.length - 1];
133
  if (!lastLog) {
134
  return { lastError: "Sem logs disponíveis para análise." };
135
  }
136
 
137
  const mockResult = {
138
+ exitCode: 1,
139
+ timedOut: lastLog.includes("TIMEOUT"),
140
+ stdout: "",
141
+ stderr: "",
142
+ combined: lastLog,
143
  };
144
 
145
  const analysis = analyzeFoundryLog(mockResult as any);
146
 
147
+ logger.info(`[Tester] reflectNode: categoria: ${analysis.category}`);
148
+ logger.info(`[Tester] reflectNode: resumo: ${analysis.summary}`);
149
 
150
+ emitStep({ agent: "tester", step: "reflect", status: "done" });
151
  return {
152
  lastError: `[${analysis.category.toUpperCase()}] ${analysis.summary}\n\nLinhas relevantes:\n${analysis.relevantLines.join("\n")}`,
153
  };
src/agents/tester/index.ts DELETED
@@ -1,27 +0,0 @@
1
- import { testerAgent } from "./agent.js";
2
- import { VulnerabilityReport, PoCResult } from "./types.js";
3
-
4
- /**
5
- * Entry point para o Agente Gerador de PoCs.
6
- * @param report O relatório de vulnerabilidade (mapeado a partir do Finding do Auditor).
7
- * @returns PoCResult contendo o código do exploit e o status da execução.
8
- */
9
- export async function runPoCGenerator(report: VulnerabilityReport): Promise<PoCResult> {
10
- console.log(`[runPoCGenerator] Iniciando para: ${report.id} — ${report.title}`);
11
-
12
- const finalState = await testerAgent.invoke({ report });
13
-
14
- const result: PoCResult = {
15
- reportId: report.id,
16
- status: finalState.status === "running" ? "failed" : finalState.status,
17
- solidityCode: finalState.pocCode,
18
- executionLogs: finalState.executionLogs,
19
- iterations: finalState.iterations,
20
- };
21
-
22
- console.log(`[runPoCGenerator] Concluído — status=${result.status}, iterações=${result.iterations}`);
23
- return result;
24
- }
25
-
26
- export type { VulnerabilityReport, PoCResult, Finding, OracleContext } from "./types.js";
27
- export { testerAgent } from "./agent.js";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/agents/tester/tools/foundryRunner.ts CHANGED
@@ -3,7 +3,9 @@ import { promisify } from "util";
3
  import { writeFile, access } from "fs/promises";
4
  import { join } from "path";
5
 
6
- const execAsync = promisify(exec);
 
 
7
  const SANDBOX = process.env.SANDBOX_DIR || "/tmp/poc-sandbox";
8
  const TIMEOUT_MS = 60_000;
9
 
@@ -22,7 +24,7 @@ async function ensureSandbox() {
22
  try {
23
  await access(join(SANDBOX, "foundry.toml"));
24
  } catch {
25
- console.log("[foundryRunner] Sandbox não encontrado. Inicializando...");
26
  // Caminho absoluto para o script de setup (assume execução da raiz do projeto)
27
  await execAsync("./scripts/setup-sandbox.sh");
28
  }
@@ -30,19 +32,16 @@ async function ensureSandbox() {
30
 
31
  export async function runFoundry(solidityCode: string): Promise<FoundryResult> {
32
  await ensureSandbox();
33
-
34
  // Escrever o arquivo no sandbox
35
  await writeFile(`${SANDBOX}/test/Exploit.t.sol`, solidityCode, "utf-8");
36
 
37
  try {
38
- const { stdout, stderr } = await execAsync(
39
- "forge test --match-contract ExploitTest -vvvv",
40
- {
41
- cwd: SANDBOX,
42
- timeout: TIMEOUT_MS,
43
- env: { ...process.env, PATH: `${process.env.HOME}/.foundry/bin:${process.env.PATH}` }
44
- }
45
- );
46
  return {
47
  exitCode: 0,
48
  stdout,
@@ -53,7 +52,9 @@ export async function runFoundry(solidityCode: string): Promise<FoundryResult> {
53
  } catch (err: any) {
54
  if (err.killed || err.signal === "SIGTERM") {
55
  return {
56
- exitCode: -1, stdout: "", stderr: "Forge timed out",
 
 
57
  combined: `TIMEOUT após ${TIMEOUT_MS / 1000}s`,
58
  timedOut: true,
59
  };
 
3
  import { writeFile, access } from "fs/promises";
4
  import { join } from "path";
5
 
6
+ import { logger } from "../../../logger.js";
7
+
8
+ const execAsync = promisify(exec);
9
  const SANDBOX = process.env.SANDBOX_DIR || "/tmp/poc-sandbox";
10
  const TIMEOUT_MS = 60_000;
11
 
 
24
  try {
25
  await access(join(SANDBOX, "foundry.toml"));
26
  } catch {
27
+ logger.info("[Tester] foundryRunner: sandbox não encontrado, inicializando...");
28
  // Caminho absoluto para o script de setup (assume execução da raiz do projeto)
29
  await execAsync("./scripts/setup-sandbox.sh");
30
  }
 
32
 
33
  export async function runFoundry(solidityCode: string): Promise<FoundryResult> {
34
  await ensureSandbox();
35
+
36
  // Escrever o arquivo no sandbox
37
  await writeFile(`${SANDBOX}/test/Exploit.t.sol`, solidityCode, "utf-8");
38
 
39
  try {
40
+ const { stdout, stderr } = await execAsync("forge test --match-contract ExploitTest -vvvv", {
41
+ cwd: SANDBOX,
42
+ timeout: TIMEOUT_MS,
43
+ env: { ...process.env, PATH: `${process.env.HOME}/.foundry/bin:${process.env.PATH}` },
44
+ });
 
 
 
45
  return {
46
  exitCode: 0,
47
  stdout,
 
52
  } catch (err: any) {
53
  if (err.killed || err.signal === "SIGTERM") {
54
  return {
55
+ exitCode: -1,
56
+ stdout: "",
57
+ stderr: "Forge timed out",
58
  combined: `TIMEOUT após ${TIMEOUT_MS / 1000}s`,
59
  timedOut: true,
60
  };
src/logger.ts CHANGED
@@ -1,5 +1,6 @@
1
  import fs from "node:fs";
2
  import path from "node:path";
 
3
 
4
  import winston from "winston";
5
 
@@ -15,6 +16,50 @@ const lineFormat = printf(({ level, message, timestamp: ts, stack }) => {
15
  return stack ? `${base}\n${stack}` : base;
16
  });
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  export const logger = winston.createLogger({
19
  level: "debug",
20
  transports: [
@@ -25,5 +70,10 @@ export const logger = winston.createLogger({
25
  filename: path.join(logsDir, `app-${runTimestamp}.log`),
26
  format: combine(timestamp(), errors({ stack: true }), lineFormat),
27
  }),
 
 
 
 
 
28
  ],
29
  });
 
1
  import fs from "node:fs";
2
  import path from "node:path";
3
+ import { Writable } from "node:stream";
4
 
5
  import winston from "winston";
6
 
 
16
  return stack ? `${base}\n${stack}` : base;
17
  });
18
 
19
+ type LogSink = (message: string) => void | Promise<void>;
20
+
21
+ let activeSink: LogSink | null = null;
22
+
23
+ export function setLogSink(sink: LogSink): void {
24
+ activeSink = sink;
25
+ }
26
+
27
+ export function clearLogSink(): void {
28
+ activeSink = null;
29
+ }
30
+
31
+ const sinkStream = new Writable({
32
+ write(chunk: Buffer, _encoding: string, callback: () => void) {
33
+ if (activeSink) {
34
+ void activeSink(chunk.toString().trim());
35
+ }
36
+ callback();
37
+ },
38
+ });
39
+
40
+ export interface StepEvent {
41
+ agent: "coder" | "auditor" | "tester";
42
+ step: string;
43
+ status: "running" | "done" | "error" | "skipped";
44
+ detail?: string;
45
+ }
46
+
47
+ type StepSink = (event: StepEvent) => void | Promise<void>;
48
+
49
+ let activeStepSink: StepSink | null = null;
50
+
51
+ export function setStepSink(sink: StepSink): void {
52
+ activeStepSink = sink;
53
+ }
54
+
55
+ export function clearStepSink(): void {
56
+ activeStepSink = null;
57
+ }
58
+
59
+ export function emitStep(event: StepEvent): void {
60
+ if (activeStepSink) void activeStepSink(event);
61
+ }
62
+
63
  export const logger = winston.createLogger({
64
  level: "debug",
65
  transports: [
 
70
  filename: path.join(logsDir, `app-${runTimestamp}.log`),
71
  format: combine(timestamp(), errors({ stack: true }), lineFormat),
72
  }),
73
+ new winston.transports.Stream({
74
+ stream: sinkStream,
75
+ level: "info",
76
+ format: winston.format.printf(({ message }) => String(message)),
77
+ }),
78
  ],
79
  });
src/server.ts CHANGED
@@ -13,6 +13,7 @@ import { coderAgent } from "./agents/coder/agent.ts";
13
  import { auditorAgent } from "./agents/auditor/agent.ts";
14
  import { testerAgent } from "./agents/tester/agent.ts";
15
  import { mapFindingToReport } from "./utils/mapFinding.js";
 
16
 
17
  const app = new Hono();
18
 
@@ -35,16 +36,17 @@ app.post("/api/run", (c) => {
35
  };
36
 
37
  try {
 
 
 
38
  // === CODER ===
39
- await send("log", "[Coder] Gerando smart contract a partir dos requisitos...");
40
  const coderResult = await coderAgent.invoke({ requirements: [requirements] });
41
 
42
- await send("log", "[Coder] Contrato gerado com sucesso.");
43
-
44
  if (coderResult.compilationErrors.length > 0) {
45
- await send("log", `[Coder] Erros de compilação restantes: ${coderResult.compilationErrors.length}`);
46
  } else {
47
- await send("log", "[Coder] Contrato compilado sem erros.");
48
  }
49
 
50
  await send(
@@ -62,11 +64,11 @@ app.post("/api/run", (c) => {
62
  writeFileSync(resolve(outputDir, "Contract.sol"), coderResult.contract, "utf-8");
63
  writeFileSync(resolve(outputDir, "README.md"), requirements, "utf-8");
64
 
65
- await send("log", "[Auditor] Iniciando auditoria de segurança...");
66
  const auditorResult = await auditorAgent.invoke({ repoPath: outputDir });
67
- await send("log", `[Auditor] ${auditorResult.findings.length} vulnerabilidade(s) encontrada(s).`);
68
  for (const f of auditorResult.findings) {
69
- await send("log", `[Auditor] [${f.severity.toUpperCase()}] ${f.title} — ${f.location}`);
70
  }
71
 
72
  await send(
@@ -77,14 +79,14 @@ app.post("/api/run", (c) => {
77
  );
78
 
79
  // === TESTER ===
80
- await send("log", "[Tester] Gerando testes de prova de conceito...");
81
 
82
  if (auditorResult.findings.length > 0) {
83
  const report = mapFindingToReport(auditorResult.findings[0], coderResult.contract);
84
  const testerResult = await testerAgent.invoke({ report });
85
 
86
- await send("log", `[Tester] Execução concluída com status: ${testerResult.status}`);
87
-
88
  // Garante que o objeto enviado tem exatamente o que o front espera
89
  await send(
90
  "tester",
@@ -96,15 +98,18 @@ app.post("/api/run", (c) => {
96
  }),
97
  );
98
  } else {
99
- await send("log", "[Tester] Nenhuma vulnerabilidade para testar.");
100
  await send("tester", JSON.stringify({ status: "skipped", iterations: 0 }));
101
  }
102
 
103
- await send("log", "Pipeline concluído.");
104
  await send("done", "ok");
105
  } catch (err) {
106
  const message = err instanceof Error ? err.message : String(err);
107
  await send("error", message);
 
 
 
108
  }
109
  });
110
  });
 
13
  import { auditorAgent } from "./agents/auditor/agent.ts";
14
  import { testerAgent } from "./agents/tester/agent.ts";
15
  import { mapFindingToReport } from "./utils/mapFinding.js";
16
+ import { logger, setLogSink, clearLogSink, setStepSink, clearStepSink } from "./logger.ts";
17
 
18
  const app = new Hono();
19
 
 
36
  };
37
 
38
  try {
39
+ setLogSink((msg) => send("log", msg));
40
+ setStepSink((event) => send("step", JSON.stringify(event)));
41
+
42
  // === CODER ===
43
+ logger.info("[Coder] Gerando smart contract a partir dos requisitos...");
44
  const coderResult = await coderAgent.invoke({ requirements: [requirements] });
45
 
 
 
46
  if (coderResult.compilationErrors.length > 0) {
47
+ logger.info(`[Coder] Erros de compilação restantes: ${coderResult.compilationErrors.length}`);
48
  } else {
49
+ logger.info("[Coder] Contrato compilado sem erros.");
50
  }
51
 
52
  await send(
 
64
  writeFileSync(resolve(outputDir, "Contract.sol"), coderResult.contract, "utf-8");
65
  writeFileSync(resolve(outputDir, "README.md"), requirements, "utf-8");
66
 
67
+ logger.info("[Auditor] Iniciando auditoria de segurança...");
68
  const auditorResult = await auditorAgent.invoke({ repoPath: outputDir });
69
+ logger.info(`[Auditor] ${auditorResult.findings.length} vulnerabilidade(s) encontrada(s).`);
70
  for (const f of auditorResult.findings) {
71
+ logger.info(`[Auditor] [${f.severity.toUpperCase()}] ${f.title} — ${f.location}`);
72
  }
73
 
74
  await send(
 
79
  );
80
 
81
  // === TESTER ===
82
+ logger.info("[Tester] Gerando testes de prova de conceito...");
83
 
84
  if (auditorResult.findings.length > 0) {
85
  const report = mapFindingToReport(auditorResult.findings[0], coderResult.contract);
86
  const testerResult = await testerAgent.invoke({ report });
87
 
88
+ logger.info(`[Tester] Execução concluída com status: ${testerResult.status}`);
89
+
90
  // Garante que o objeto enviado tem exatamente o que o front espera
91
  await send(
92
  "tester",
 
98
  }),
99
  );
100
  } else {
101
+ logger.info("[Tester] Nenhuma vulnerabilidade para testar.");
102
  await send("tester", JSON.stringify({ status: "skipped", iterations: 0 }));
103
  }
104
 
105
+ logger.info("Pipeline concluído.");
106
  await send("done", "ok");
107
  } catch (err) {
108
  const message = err instanceof Error ? err.message : String(err);
109
  await send("error", message);
110
+ } finally {
111
+ clearLogSink();
112
+ clearStepSink();
113
  }
114
  });
115
  });