Skip to content

Commit d921330

Browse files
authored
build_generation: Add auto discovery agent (#1014)
This PR add a new build_generation agent to auto discovery the target project in a docker image and create a build script after gathered enough information. --------- Signed-off-by: Arthur Chan <[email protected]>
1 parent 3451d29 commit d921330

File tree

4 files changed

+259
-74
lines changed

4 files changed

+259
-74
lines changed

experimental/build_generator/llm_agent.py

+116-25
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
MAX_PROMPT_LENGTH = 20000
3333
SAMPLE_HEADERS_COUNT = 30
34+
MAX_DISCOVERY_ROUND = 100
3435

3536

3637
class BuildScriptAgent(BaseAgent):
@@ -51,6 +52,7 @@ def __init__(self,
5152
self.last_status = False
5253
self.last_result = ''
5354
self.target_files = {}
55+
self.discovery_stage = False
5456

5557
# Get sample fuzzing harness
5658
_, _, self.harness_path, self.harness_code = (
@@ -83,31 +85,48 @@ def _parse_tags(self, response: str, tag: str) -> list[str]:
8385
def _container_handle_bash_commands(self, response: str, tool: BaseTool,
8486
prompt: Prompt) -> Prompt:
8587
"""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
9589
prompt_text = ''
9690
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\nset -e\n{command}'
10191

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
103103
if isinstance(tool, ProjectContainerTool):
104-
tool.write_to_file(command, '/src/build.sh')
104+
tool.write_to_file(self.harness_code, self.harness_path)
105105

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\nset -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)
111130
if result.returncode == 0:
112131
success = True
113132

@@ -192,8 +211,6 @@ def _discover_headers(self) -> list[str]:
192211
if file.endswith((".h", ".hpp")):
193212
header_path = os.path.join(root, file)
194213
headers.add(header_path.replace(target_path, ''))
195-
if len(headers) > SAMPLE_HEADERS_COUNT:
196-
return list(headers)
197214

198215
return list(headers)
199216

@@ -205,6 +222,7 @@ def execute(self, result_history: list[Result]) -> BuildResult:
205222
self.inspect_tool = ProjectContainerTool(benchmark, name='inspect')
206223
self.inspect_tool.compile(extra_commands=' && rm -rf /out/* > /dev/null')
207224
cur_round = 1
225+
dis_round = 1
208226
build_result = BuildResult(benchmark=benchmark,
209227
trial=last_result.trial,
210228
work_dirs=last_result.work_dirs,
@@ -214,7 +232,7 @@ def execute(self, result_history: list[Result]) -> BuildResult:
214232
prompt = self._initial_prompt(result_history)
215233
try:
216234
client = self.llm.get_chat_client(model=self.llm.get_model())
217-
while prompt and cur_round < self.max_round:
235+
while prompt:
218236
# Sleep for a minute to avoid over RPM
219237
time.sleep(60)
220238

@@ -224,7 +242,15 @@ def execute(self, result_history: list[Result]) -> BuildResult:
224242
trial=last_result.trial)
225243
prompt = self._container_tool_reaction(cur_round, response,
226244
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
228254
finally:
229255
logger.info('Stopping and removing the inspect container %s',
230256
self.inspect_tool.container_id,
@@ -291,6 +317,7 @@ def _initial_prompt(self, results: list[Result]) -> Prompt: # pylint: disable=u
291317
# Extract template Dockerfile content
292318
dockerfile_str = templates.CLEAN_OSS_FUZZ_DOCKER
293319
dockerfile_str = dockerfile_str.replace('{additional_packages}', '')
320+
dockerfile_str = dockerfile_str.replace('{fuzzer_dir}', '$SRC/')
294321
dockerfile_str = dockerfile_str.replace('{repo_url}', self.github_url)
295322
dockerfile_str = dockerfile_str.replace('{project_repo_dir}',
296323
self.github_url.split('/')[-1])
@@ -304,7 +331,8 @@ def _initial_prompt(self, results: list[Result]) -> Prompt: # pylint: disable=u
304331
self.harness_path.split('/')[-1])
305332

306333
headers = self._discover_headers()
307-
problem = problem.replace('{HEADERS}', ','.join(headers))
334+
problem = problem.replace('{HEADERS}',
335+
','.join(headers[:SAMPLE_HEADERS_COUNT]))
308336

309337
prompt.add_priming(templates.LLM_PRIMING)
310338
prompt.add_problem(problem)
@@ -324,3 +352,66 @@ def execute(self, result_history: list[Result]) -> BuildResult:
324352
chat_history={self.name: ''})
325353

326354
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)

experimental/build_generator/manager.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,8 @@ def create_clean_oss_fuzz_from_empty(github_repo: str, build_worker,
302302
dockerfile = templates.CLEAN_OSS_FUZZ_DOCKER.format(
303303
repo_url=github_repo,
304304
project_repo_dir=project_repo_dir,
305-
additional_packages=' '.join(additional_packages))
305+
additional_packages=' '.join(additional_packages),
306+
fuzzer_dir='$SRC/fuzzers/')
306307
with open(os.path.join(oss_fuzz_folder, 'Dockerfile'), 'w') as docker_out:
307308
docker_out.write(dockerfile)
308309

@@ -346,7 +347,8 @@ def create_clean_oss_fuzz_from_success(github_repo: str, out_dir: str,
346347
dockerfile = templates.CLEAN_OSS_FUZZ_DOCKER.format(
347348
repo_url=github_repo,
348349
project_repo_dir=project_repo_dir,
349-
additional_packages=' '.join(pkgs))
350+
additional_packages=' '.join(pkgs),
351+
fuzzer_dir='$SRC/fuzzers/')
350352
with open(os.path.join(oss_fuzz_folder, 'Dockerfile'), 'w') as docker_out:
351353
docker_out.write(dockerfile)
352354

@@ -546,8 +548,8 @@ def auto_generate(github_url, disable_testing_build_scripts=False, outdir=''):
546548
build_worker.build_suggestion, build_worker.build_script,
547549
build_worker.build_directory,
548550
build_worker.executable_files_build.copy())
549-
new_worker.build_suggestion.heuristic_id = new_worker.build_suggestion.heuristic_id + '-%d' % (
550-
b_idx)
551+
new_worker.build_suggestion.heuristic_id = (
552+
new_worker.build_suggestion.heuristic_id + f'-{b_idx}')
551553
new_worker.executable_files_build['refined-static-libs'] = [ref_lib]
552554
refined_builds.append((test_dir, new_worker))
553555
refined_builds.append((test_dir, build_worker))

experimental/build_generator/runner.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def setup_worker_project(oss_fuzz_base: str,
6363
file_content = templates.CLEAN_OSS_FUZZ_DOCKER
6464
file_content = file_content.replace('{additional_packages}', '')
6565
file_content = file_content.replace('{repo_url}', github_url)
66+
file_content = file_content.replace('{fuzzer_dir}', '$SRC/')
6667
file_content = file_content.replace('{project_repo_dir}',
6768
github_url.split('/')[-1])
6869
else:
@@ -379,7 +380,10 @@ def run_agent(target_repositories: List[str], args: argparse.Namespace):
379380
)
380381

381382
# All agents
382-
llm_agents = [llm_agent.BuildSystemBuildScriptAgent]
383+
llm_agents = [
384+
llm_agent.BuildSystemBuildScriptAgent,
385+
llm_agent.AutoDiscoveryBuildScriptAgent
386+
]
383387

384388
for target_repository in target_repositories:
385389
logger.info('Target repository: %s', target_repository)

0 commit comments

Comments
 (0)