31
31
32
32
MAX_PROMPT_LENGTH = 20000
33
33
SAMPLE_HEADERS_COUNT = 30
34
+ MAX_DISCOVERY_ROUND = 100
34
35
35
36
36
37
class BuildScriptAgent (BaseAgent ):
@@ -51,6 +52,7 @@ def __init__(self,
51
52
self .last_status = False
52
53
self .last_result = ''
53
54
self .target_files = {}
55
+ self .discovery_stage = False
54
56
55
57
# Get sample fuzzing harness
56
58
_ , _ , self .harness_path , self .harness_code = (
@@ -83,31 +85,48 @@ def _parse_tags(self, response: str, tag: str) -> list[str]:
83
85
def _container_handle_bash_commands (self , response : str , tool : BaseTool ,
84
86
prompt : Prompt ) -> Prompt :
85
87
"""Handles the command from LLM with container |tool|."""
86
- # Update fuzzing harness
87
- harness = self ._parse_tag (response , 'fuzzer' )
88
- if harness :
89
- self .harness_code = harness
90
- if isinstance (tool , ProjectContainerTool ):
91
- tool .write_to_file (self .harness_code , self .harness_path )
92
-
93
- # Update build script
94
- command = '\n ' .join (self ._parse_tags (response , 'bash' ))
88
+ # Initialise variables
95
89
prompt_text = ''
96
90
success = False
97
- if command :
98
- # Add set -e to ensure docker image failing is reflected.
99
- command = command .replace ('#!/bin/bash' , '' )
100
- command = f'#!/bin/bash\n set -e\n { command } '
101
91
102
- # Update build script
92
+ # Retrieve data from response
93
+ harness = self ._parse_tag (response , 'fuzzer' )
94
+ build_script = '\n ' .join (self ._parse_tags (response , 'bash' ))
95
+ commands = '; ' .join (self ._parse_tags (response , 'command' ))
96
+
97
+ if build_script :
98
+ self .discovery_stage = False
99
+
100
+ # Update fuzzing harness
101
+ if harness :
102
+ self .harness_code = harness
103
103
if isinstance (tool , ProjectContainerTool ):
104
- tool .write_to_file (command , '/src/build.sh' )
104
+ tool .write_to_file (self . harness_code , self . harness_path )
105
105
106
- # Test and parse result
107
- result = tool .execute ('compile' )
108
- format_result = self ._format_bash_execution_result (result ,
109
- previous_prompt = prompt )
110
- prompt_text = self ._parse_tag (format_result , 'stderr' ) + '\n '
106
+ # Update build script
107
+ if build_script :
108
+ # Add set -e to ensure docker image failing is reflected.
109
+ build_script = build_script .replace ('#!/bin/bash' , '' )
110
+ build_script = f'#!/bin/bash\n set -e\n { build_script } '
111
+
112
+ # Update build script
113
+ if isinstance (tool , ProjectContainerTool ):
114
+ tool .write_to_file (build_script , '/src/build.sh' )
115
+
116
+ # Test and parse result
117
+ result = tool .execute ('compile' )
118
+ format_result = self ._format_bash_execution_result (
119
+ result , previous_prompt = prompt )
120
+ prompt_text = self ._parse_tag (format_result , 'stderr' ) + '\n '
121
+ if result .returncode == 0 :
122
+ success = True
123
+
124
+ elif commands :
125
+ # Execute the command directly, then return the formatted result
126
+ self .discovery_stage = True
127
+ result = tool .execute (commands )
128
+ prompt_text = self ._format_bash_execution_result (result ,
129
+ previous_prompt = prompt )
111
130
if result .returncode == 0 :
112
131
success = True
113
132
@@ -192,8 +211,6 @@ def _discover_headers(self) -> list[str]:
192
211
if file .endswith ((".h" , ".hpp" )):
193
212
header_path = os .path .join (root , file )
194
213
headers .add (header_path .replace (target_path , '' ))
195
- if len (headers ) > SAMPLE_HEADERS_COUNT :
196
- return list (headers )
197
214
198
215
return list (headers )
199
216
@@ -205,6 +222,7 @@ def execute(self, result_history: list[Result]) -> BuildResult:
205
222
self .inspect_tool = ProjectContainerTool (benchmark , name = 'inspect' )
206
223
self .inspect_tool .compile (extra_commands = ' && rm -rf /out/* > /dev/null' )
207
224
cur_round = 1
225
+ dis_round = 1
208
226
build_result = BuildResult (benchmark = benchmark ,
209
227
trial = last_result .trial ,
210
228
work_dirs = last_result .work_dirs ,
@@ -214,7 +232,7 @@ def execute(self, result_history: list[Result]) -> BuildResult:
214
232
prompt = self ._initial_prompt (result_history )
215
233
try :
216
234
client = self .llm .get_chat_client (model = self .llm .get_model ())
217
- while prompt and cur_round < self . max_round :
235
+ while prompt :
218
236
# Sleep for a minute to avoid over RPM
219
237
time .sleep (60 )
220
238
@@ -224,7 +242,15 @@ def execute(self, result_history: list[Result]) -> BuildResult:
224
242
trial = last_result .trial )
225
243
prompt = self ._container_tool_reaction (cur_round , response ,
226
244
build_result )
227
- cur_round += 1
245
+
246
+ if self .discovery_stage :
247
+ dis_round += 1
248
+ if dis_round >= MAX_DISCOVERY_ROUND :
249
+ break
250
+ else :
251
+ cur_round += 1
252
+ if cur_round >= self .max_round :
253
+ break
228
254
finally :
229
255
logger .info ('Stopping and removing the inspect container %s' ,
230
256
self .inspect_tool .container_id ,
@@ -291,6 +317,7 @@ def _initial_prompt(self, results: list[Result]) -> Prompt: # pylint: disable=u
291
317
# Extract template Dockerfile content
292
318
dockerfile_str = templates .CLEAN_OSS_FUZZ_DOCKER
293
319
dockerfile_str = dockerfile_str .replace ('{additional_packages}' , '' )
320
+ dockerfile_str = dockerfile_str .replace ('{fuzzer_dir}' , '$SRC/' )
294
321
dockerfile_str = dockerfile_str .replace ('{repo_url}' , self .github_url )
295
322
dockerfile_str = dockerfile_str .replace ('{project_repo_dir}' ,
296
323
self .github_url .split ('/' )[- 1 ])
@@ -304,7 +331,8 @@ def _initial_prompt(self, results: list[Result]) -> Prompt: # pylint: disable=u
304
331
self .harness_path .split ('/' )[- 1 ])
305
332
306
333
headers = self ._discover_headers ()
307
- problem = problem .replace ('{HEADERS}' , ',' .join (headers ))
334
+ problem = problem .replace ('{HEADERS}' ,
335
+ ',' .join (headers [:SAMPLE_HEADERS_COUNT ]))
308
336
309
337
prompt .add_priming (templates .LLM_PRIMING )
310
338
prompt .add_problem (problem )
@@ -324,3 +352,66 @@ def execute(self, result_history: list[Result]) -> BuildResult:
324
352
chat_history = {self .name : '' })
325
353
326
354
return super ().execute (result_history )
355
+
356
+
357
+ class AutoDiscoveryBuildScriptAgent (BuildScriptAgent ):
358
+ """Generate a working Dockerfile and build script from scratch
359
+ with LLM auto discovery"""
360
+
361
+ def _initial_prompt (self , results : list [Result ]) -> Prompt : # pylint: disable=unused-argument
362
+ """Constructs initial prompt of the agent."""
363
+ prompt = self .llm .prompt_type ()(None )
364
+
365
+ # Extract template Dockerfile content
366
+ dockerfile_str = templates .CLEAN_OSS_FUZZ_DOCKER
367
+ dockerfile_str = dockerfile_str .replace ('{additional_packages}' , '' )
368
+ dockerfile_str = dockerfile_str .replace ('{repo_url}' , self .github_url )
369
+ dockerfile_str = dockerfile_str .replace ('{project_repo_dir}' ,
370
+ self .github_url .split ('/' )[- 1 ])
371
+
372
+ # Prepare prompt problem string
373
+ problem = templates .LLM_AUTO_DISCOVERY
374
+ problem = problem .replace ('{PROJECT_NAME}' , self .github_url .split ('/' )[- 1 ])
375
+ problem = problem .replace ('{DOCKERFILE}' , dockerfile_str )
376
+ problem = problem .replace ('{FUZZER}' , self .harness_code )
377
+ problem = problem .replace ('{MAX_DISCOVERY_ROUND}' , str (MAX_DISCOVERY_ROUND ))
378
+ problem = problem .replace ('{FUZZING_FILE}' ,
379
+ self .harness_path .split ('/' )[- 1 ])
380
+
381
+ prompt .add_priming (templates .LLM_PRIMING )
382
+ prompt .add_problem (problem )
383
+
384
+ return prompt
385
+
386
+ def _container_tool_reaction (self , cur_round : int , response : str ,
387
+ build_result : BuildResult ) -> Optional [Prompt ]:
388
+ """Validates LLM conclusion or executes its command."""
389
+ prompt = self .llm .prompt_type ()(None )
390
+
391
+ if response :
392
+ prompt = self ._container_handle_bash_commands (response , self .inspect_tool ,
393
+ prompt )
394
+
395
+ if self .discovery_stage :
396
+ # Relay the command output back to LLM
397
+ feedback = templates .LLM_DOCKER_FEEDBACK
398
+ feedback = feedback .replace ('{RESULT}' , self .last_result )
399
+ prompt .add_problem (feedback )
400
+ else :
401
+ # Check result and try building with the new builds script
402
+ prompt = self ._container_handle_conclusion (cur_round , response ,
403
+ build_result , prompt )
404
+
405
+ if prompt is None :
406
+ return None
407
+
408
+ if not response or not prompt or not prompt .get ():
409
+ prompt = self ._container_handle_invalid_tool_usage (
410
+ self .inspect_tool , cur_round , response , prompt )
411
+
412
+ return prompt
413
+
414
+ def execute (self , result_history : list [Result ]) -> BuildResult :
415
+ """Executes the agent based on previous result."""
416
+ self ._prepare_repository ()
417
+ return super ().execute (result_history )
0 commit comments