Uanderson Silva commited on
Commit
fc99222
·
1 Parent(s): 48c7a9b

create new frontend interface

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
@@ -7,6 +7,7 @@ import { z } from "zod";
7
 
8
  import { logger } from "../../logger.ts";
9
  import { createLLM } from "../../config/llm.ts";
 
10
  import { JUDGE_FINDINGS_PROMPT, FIND_VULNERABILITIES_PROMPT, GATHER_CONTEXT_PROMPT } from "./prompts.ts";
11
  import { AuditorState, JudgeReviewSchema, CandidateFindingSchema } from "./state.ts";
12
  import { analyzeSolidityFile } from "./tools/solidity-analyzer-tool.ts";
@@ -57,6 +58,7 @@ const walkDirectory = (dir: string, depth: number, solFiles: string[], docFiles:
57
  };
58
 
59
  const defineScope: GraphNode<typeof AuditorState> = async (state) => {
 
60
  logger.info(`[Auditor] defineScope: percorrendo repositório em ${state.repoPath}`);
61
 
62
  const solFiles: string[] = [];
@@ -71,10 +73,12 @@ const defineScope: GraphNode<typeof AuditorState> = async (state) => {
71
  logger.debug(`[Auditor] defineScope: arquivos de documentação: ${JSON.stringify(docFiles)}`);
72
  logger.debug(`[Auditor] defineScope: árvore de arquivos:\n${fileTree}`);
73
 
 
74
  return { scope: solFiles, docs: docFiles, fileTree };
75
  };
76
 
77
  const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
 
78
  logger.info(`[Auditor] gatherContext: processando ${state.scope.length} arquivo(s) Solidity e ${state.docs.length} arquivo(s) de documentação`);
79
 
80
  const readFile = (filePath: string): string => {
@@ -127,6 +131,7 @@ const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
127
  logger.info(`[Auditor] gatherContext: contexto construído (${parts.join("\n\n").length} caracteres)`);
128
  logger.debug(`[Auditor] gatherContext: contexto completo:\n${parts.join("\n\n")}`);
129
 
 
130
  return { repoContext: result.context };
131
  };
132
 
@@ -146,6 +151,7 @@ const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
146
  logger.info(
147
  `[Auditor] findVulnerabilities: invocando LLM para ${state.scope.length} arquivo(s) em paralelo (iteração ${state.reflectionCount + 1})`,
148
  );
 
149
 
150
  const allFindings = await Promise.all(
151
  state.scope.map(async (filePath) => {
@@ -181,12 +187,15 @@ const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
181
  logger.info(`[Auditor] findVulnerabilities: LLM retornou ${candidateFindings.length} finding(s) candidato(s) no total`);
182
  logger.debug(`[Auditor] findVulnerabilities: findings:\n${JSON.stringify(candidateFindings, null, 2)}`);
183
 
 
184
  return { candidateFindings };
185
  };
186
 
187
  const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
 
188
  if (state.candidateFindings.length === 0) {
189
  logger.info("[Auditor] judgeFindings: sem findings candidatos para revisar, pulando chamada ao LLM");
 
190
  return {
191
  judgeReviews: [],
192
  findings: [],
@@ -237,6 +246,7 @@ const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
237
  logger.info(`[Auditor] judgeFindings: ${findings.length} confirmado(s), ${falsePositiveCount} falso(s) positivo(s)`);
238
  logger.debug(`[Auditor] judgeFindings: revisões:\n${JSON.stringify(reviews, null, 2)}`);
239
 
 
240
  return {
241
  judgeReviews: reviews,
242
  findings,
 
7
 
8
  import { logger } from "../../logger.ts";
9
  import { createLLM } from "../../config/llm.ts";
10
+ import { emitStep } from "../../logger.ts";
11
  import { JUDGE_FINDINGS_PROMPT, FIND_VULNERABILITIES_PROMPT, GATHER_CONTEXT_PROMPT } from "./prompts.ts";
12
  import { AuditorState, JudgeReviewSchema, CandidateFindingSchema } from "./state.ts";
13
  import { analyzeSolidityFile } from "./tools/solidity-analyzer-tool.ts";
 
58
  };
59
 
60
  const defineScope: GraphNode<typeof AuditorState> = async (state) => {
61
+ emitStep({ agent: "auditor", step: "scope", status: "running" });
62
  logger.info(`[Auditor] defineScope: percorrendo repositório em ${state.repoPath}`);
63
 
64
  const solFiles: string[] = [];
 
73
  logger.debug(`[Auditor] defineScope: arquivos de documentação: ${JSON.stringify(docFiles)}`);
74
  logger.debug(`[Auditor] defineScope: árvore de arquivos:\n${fileTree}`);
75
 
76
+ emitStep({ agent: "auditor", step: "scope", status: "done" });
77
  return { scope: solFiles, docs: docFiles, fileTree };
78
  };
79
 
80
  const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
81
+ emitStep({ agent: "auditor", step: "ctx", status: "running" });
82
  logger.info(`[Auditor] gatherContext: processando ${state.scope.length} arquivo(s) Solidity e ${state.docs.length} arquivo(s) de documentação`);
83
 
84
  const readFile = (filePath: string): string => {
 
131
  logger.info(`[Auditor] gatherContext: contexto construído (${parts.join("\n\n").length} caracteres)`);
132
  logger.debug(`[Auditor] gatherContext: contexto completo:\n${parts.join("\n\n")}`);
133
 
134
+ emitStep({ agent: "auditor", step: "ctx", status: "done" });
135
  return { repoContext: result.context };
136
  };
137
 
 
151
  logger.info(
152
  `[Auditor] findVulnerabilities: invocando LLM para ${state.scope.length} arquivo(s) em paralelo (iteração ${state.reflectionCount + 1})`,
153
  );
154
+ emitStep({ agent: "auditor", step: "find", status: "running", detail: `iter ${state.reflectionCount + 1}` });
155
 
156
  const allFindings = await Promise.all(
157
  state.scope.map(async (filePath) => {
 
187
  logger.info(`[Auditor] findVulnerabilities: LLM retornou ${candidateFindings.length} finding(s) candidato(s) no total`);
188
  logger.debug(`[Auditor] findVulnerabilities: findings:\n${JSON.stringify(candidateFindings, null, 2)}`);
189
 
190
+ emitStep({ agent: "auditor", step: "find", status: "done" });
191
  return { candidateFindings };
192
  };
193
 
194
  const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
195
+ emitStep({ agent: "auditor", step: "judge", status: "running" });
196
  if (state.candidateFindings.length === 0) {
197
  logger.info("[Auditor] judgeFindings: sem findings candidatos para revisar, pulando chamada ao LLM");
198
+ emitStep({ agent: "auditor", step: "judge", status: "done" });
199
  return {
200
  judgeReviews: [],
201
  findings: [],
 
246
  logger.info(`[Auditor] judgeFindings: ${findings.length} confirmado(s), ${falsePositiveCount} falso(s) positivo(s)`);
247
  logger.debug(`[Auditor] judgeFindings: revisões:\n${JSON.stringify(reviews, null, 2)}`);
248
 
249
+ emitStep({ agent: "auditor", step: "judge", status: "done" });
250
  return {
251
  judgeReviews: reviews,
252
  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
@@ -8,19 +8,21 @@ 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 } 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
  logger.info(`[Tester] oracleNode: gerando scaffold para: ${state.report.title}`);
19
 
20
  const solidityScaffold = generateLocalScaffold(state.report);
21
  const oracleContext: OracleContext = { solidityScaffold };
22
 
23
  logger.info(`[Tester] oracleNode: scaffold gerado, tamanho: ${solidityScaffold.length} chars`);
 
24
  return { oracleContext };
25
  }
26
 
@@ -55,6 +57,7 @@ ${oracleContext!.solidityScaffold}
55
  \`\`\``;
56
 
57
  logger.info(`[Tester] generatePoCNode: iteração ${iterations + 1}, isRetry=${isRetry}`);
 
58
 
59
  try {
60
  const response = await llm.invoke([
@@ -63,14 +66,17 @@ ${oracleContext!.solidityScaffold}
63
  ]);
64
  const solidityCode = extractSolidity(response.content as string);
65
  logger.info(`[Tester] generatePoCNode: Solidity extraído, tamanho: ${solidityCode.length}`);
 
66
  return { pocCode: solidityCode, iterations: 1 };
67
  } catch (err) {
68
  logger.error(`[Tester] generatePoCNode: falha na geração: ${(err as Error).message}`);
 
69
  return { iterations: 1, lastError: `Erro na geração/extração: ${(err as Error).message}` };
70
  }
71
  }
72
 
73
  async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
 
74
  logger.info("[Tester] runFoundryNode: executando...");
75
 
76
  const trimmedCode = state.pocCode.trim();
@@ -112,6 +118,8 @@ async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
112
  logger.info(`[Tester] runFoundryNode: falha detectada: ${analysis.summary}`);
113
  }
114
 
 
 
115
  return {
116
  executionLogs: [result.combined], // reducer append
117
  lastError: summary,
@@ -120,6 +128,7 @@ async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
120
  }
121
 
122
  async function reflectNode(state: PoCState): Promise<Partial<PoCState>> {
 
123
  const lastLog = state.executionLogs[state.executionLogs.length - 1];
124
  if (!lastLog) {
125
  return { lastError: "Sem logs disponíveis para análise." };
@@ -138,6 +147,7 @@ async function reflectNode(state: PoCState): Promise<Partial<PoCState>> {
138
  logger.info(`[Tester] reflectNode: categoria: ${analysis.category}`);
139
  logger.info(`[Tester] reflectNode: resumo: ${analysis.summary}`);
140
 
 
141
  return {
142
  lastError: `[${analysis.category.toUpperCase()}] ${analysis.summary}\n\nLinhas relevantes:\n${analysis.relevantLines.join("\n")}`,
143
  };
 
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
 
 
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([
 
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();
 
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,
 
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." };
 
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,28 +0,0 @@
1
- import { testerAgent } from "./agent.js";
2
- import { VulnerabilityReport, PoCResult } from "./types.js";
3
- import { logger } from "../../logger.js";
4
-
5
- /**
6
- * Entry point para o Agente Gerador de PoCs.
7
- * @param report O relatório de vulnerabilidade (mapeado a partir do Finding do Auditor).
8
- * @returns PoCResult contendo o código do exploit e o status da execução.
9
- */
10
- export async function runPoCGenerator(report: VulnerabilityReport): Promise<PoCResult> {
11
- logger.info(`[Tester] runPoCGenerator: iniciando para: ${report.id} — ${report.title}`);
12
-
13
- const finalState = await testerAgent.invoke({ report });
14
-
15
- const result: PoCResult = {
16
- reportId: report.id,
17
- status: finalState.status === "running" ? "failed" : finalState.status,
18
- solidityCode: finalState.pocCode,
19
- executionLogs: finalState.executionLogs,
20
- iterations: finalState.iterations,
21
- };
22
-
23
- logger.info(`[Tester] runPoCGenerator: concluído — status=${result.status}, iterações=${result.iterations}`);
24
- return result;
25
- }
26
-
27
- export type { VulnerabilityReport, PoCResult, Finding, OracleContext } from "./types.js";
28
- export { testerAgent } from "./agent.js";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/config/llm.ts CHANGED
@@ -18,7 +18,7 @@ export function createLLM(overrideProvider?: LLMProvider): BaseChatModel {
18
  });
19
  case "anthropic":
20
  return new ChatAnthropic({
21
- model: process.env.ANTHROPIC_MODEL || "claude-sonnet-4-6",
22
  temperature: 0.2,
23
  maxTokens: 4096,
24
  });
 
18
  });
19
  case "anthropic":
20
  return new ChatAnthropic({
21
+ model: process.env.ANTHROPIC_MODEL || "claude-haiku-4-5",
22
  temperature: 0.2,
23
  maxTokens: 4096,
24
  });
src/logger.ts CHANGED
@@ -37,6 +37,29 @@ const sinkStream = new Writable({
37
  },
38
  });
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  export const logger = winston.createLogger({
41
  level: "debug",
42
  transports: [
 
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: [
src/server.ts CHANGED
@@ -13,7 +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
- import { logger, setLogSink, clearLogSink } from "./logger.ts";
17
 
18
  const app = new Hono();
19
 
@@ -37,6 +37,7 @@ app.post("/api/run", (c) => {
37
 
38
  try {
39
  setLogSink((msg) => send("log", msg));
 
40
 
41
  // === CODER ===
42
  logger.info("[Coder] Gerando smart contract a partir dos requisitos...");
@@ -108,6 +109,7 @@ app.post("/api/run", (c) => {
108
  await send("error", message);
109
  } finally {
110
  clearLogSink();
 
111
  }
112
  });
113
  });
 
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
 
 
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...");
 
109
  await send("error", message);
110
  } finally {
111
  clearLogSink();
112
+ clearStepSink();
113
  }
114
  });
115
  });