mirror of
https://github.com/FuzzingLabs/fuzzforge_ai.git
synced 2026-03-06 10:00:44 +00:00
Compare commits
5 Commits
fix/config
...
feat/artif
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85420c2328 | ||
|
|
4ad44332ee | ||
|
|
09821c1c43 | ||
|
|
6f24c88907 | ||
|
|
92b338f9ed |
70
.github/workflows/ci-python.yml
vendored
Normal file
70
.github/workflows/ci-python.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
name: Python CI
|
||||||
|
|
||||||
|
# This is a dumb Ci to ensure that the python client and backend builds correctly
|
||||||
|
# It could be optimized to run faster, building, testing and linting only changed code
|
||||||
|
# but for now it is good enough. It runs on every push and PR to any branch.
|
||||||
|
# It also runs on demand.
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "ai/**"
|
||||||
|
- "backend/**"
|
||||||
|
- "cli/**"
|
||||||
|
- "sdk/**"
|
||||||
|
- "src/**"
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "ai/**"
|
||||||
|
- "backend/**"
|
||||||
|
- "cli/**"
|
||||||
|
- "sdk/**"
|
||||||
|
- "src/**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
name: ci
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Setup uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
run: uv python install
|
||||||
|
|
||||||
|
# Validate no obvious issues
|
||||||
|
# Quick hack because CLI returns non-zero exit code when no args are provided
|
||||||
|
- name: Run base command
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
uv run ff
|
||||||
|
if [ $? -ne 2 ]; then
|
||||||
|
echo "Expected exit code 2 from 'uv run ff', got $?"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build fuzzforge_ai package
|
||||||
|
run: uv build
|
||||||
|
|
||||||
|
- name: Build ai package
|
||||||
|
working-directory: ai
|
||||||
|
run: uv build
|
||||||
|
|
||||||
|
- name: Build cli package
|
||||||
|
working-directory: cli
|
||||||
|
run: uv build
|
||||||
|
|
||||||
|
- name: Build sdk package
|
||||||
|
working-directory: sdk
|
||||||
|
run: uv build
|
||||||
|
|
||||||
|
- name: Build backend package
|
||||||
|
working-directory: backend
|
||||||
|
run: uv build
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<p align="center"><strong>AI-powered workflow automation and AI Agents for AppSec, Fuzzing & Offensive Security</strong></p>
|
<p align="center"><strong>AI-powered workflow automation and AI Agents for AppSec, Fuzzing & Offensive Security</strong></p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://discord.com/invite/acqv9FVG"><img src="https://img.shields.io/discord/1420767905255133267?logo=discord&label=Discord" alt="Discord"></a>
|
<a href="https://discord.gg/8XEX33UUwZ/"><img src="https://img.shields.io/discord/1420767905255133267?logo=discord&label=Discord" alt="Discord"></a>
|
||||||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-BSL%20%2B%20Apache-orange" alt="License: BSL + Apache"></a>
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-BSL%20%2B%20Apache-orange" alt="License: BSL + Apache"></a>
|
||||||
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.11%2B-blue" alt="Python 3.11+"/></a>
|
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.11%2B-blue" alt="Python 3.11+"/></a>
|
||||||
<a href="https://fuzzforge.ai"><img src="https://img.shields.io/badge/Website-fuzzforge.ai-blue" alt="Website"/></a>
|
<a href="https://fuzzforge.ai"><img src="https://img.shields.io/badge/Website-fuzzforge.ai-blue" alt="Website"/></a>
|
||||||
@@ -176,7 +176,7 @@ _AI agents automatically analyzing code and providing security insights_
|
|||||||
|
|
||||||
- 🌐 [Website](https://fuzzforge.ai)
|
- 🌐 [Website](https://fuzzforge.ai)
|
||||||
- 📖 [Documentation](https://docs.fuzzforge.ai)
|
- 📖 [Documentation](https://docs.fuzzforge.ai)
|
||||||
- 💬 [Community Discord](https://discord.com/invite/acqv9FVG)
|
- 💬 [Community Discord](https://discord.gg/8XEX33UUwZ)
|
||||||
- 🎓 [FuzzingLabs Academy](https://academy.fuzzinglabs.com/?coupon=GITHUB_FUZZFORGE)
|
- 🎓 [FuzzingLabs Academy](https://academy.fuzzinglabs.com/?coupon=GITHUB_FUZZFORGE)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -205,7 +205,7 @@ Planned features and improvements:
|
|||||||
- ☁️ Multi-tenant SaaS platform with team collaboration
|
- ☁️ Multi-tenant SaaS platform with team collaboration
|
||||||
- 📊 Advanced reporting & analytics
|
- 📊 Advanced reporting & analytics
|
||||||
|
|
||||||
👉 Follow updates in the [GitHub issues](../../issues) and [Discord](https://discord.com/invite/acqv9FVG).
|
👉 Follow updates in the [GitHub issues](../../issues) and [Discord](https://discord.gg/8XEX33UUwZ)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ def create_a2a_app():
|
|||||||
port = int(os.getenv('FUZZFORGE_PORT', 10100))
|
port = int(os.getenv('FUZZFORGE_PORT', 10100))
|
||||||
|
|
||||||
# Get the FuzzForge agent
|
# Get the FuzzForge agent
|
||||||
fuzzforge = get_fuzzforge_agent()
|
fuzzforge = get_fuzzforge_agent(auto_start_server=False)
|
||||||
|
|
||||||
# Print ASCII banner
|
# Print ASCII banner
|
||||||
print("\033[95m") # Purple color
|
print("\033[95m") # Purple color
|
||||||
|
|||||||
@@ -15,8 +15,12 @@ The core agent that combines all components
|
|||||||
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import socket
|
||||||
|
import asyncio
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List, Optional
|
||||||
from google.adk import Agent
|
from google.adk import Agent
|
||||||
from google.adk.models.lite_llm import LiteLlm
|
from google.adk.models.lite_llm import LiteLlm
|
||||||
from .agent_card import get_fuzzforge_agent_card
|
from .agent_card import get_fuzzforge_agent_card
|
||||||
@@ -43,11 +47,19 @@ class FuzzForgeAgent:
|
|||||||
model: str = None,
|
model: str = None,
|
||||||
cognee_url: str = None,
|
cognee_url: str = None,
|
||||||
port: int = 10100,
|
port: int = 10100,
|
||||||
|
auto_start_server: Optional[bool] = None,
|
||||||
):
|
):
|
||||||
"""Initialize FuzzForge agent with configuration"""
|
"""Initialize FuzzForge agent with configuration"""
|
||||||
self.model = model or os.getenv('LITELLM_MODEL', 'gpt-4o-mini')
|
self.model = model or os.getenv('LITELLM_MODEL', 'gpt-4o-mini')
|
||||||
self.cognee_url = cognee_url or os.getenv('COGNEE_MCP_URL')
|
self.cognee_url = cognee_url or os.getenv('COGNEE_MCP_URL')
|
||||||
self.port = port
|
self.port = int(os.getenv('FUZZFORGE_PORT', port))
|
||||||
|
self._auto_start_server = (
|
||||||
|
auto_start_server
|
||||||
|
if auto_start_server is not None
|
||||||
|
else os.getenv('FUZZFORGE_AUTO_A2A_SERVER', '1') not in {'0', 'false', 'False'}
|
||||||
|
)
|
||||||
|
self._uvicorn_server = None
|
||||||
|
self._a2a_server_thread: Optional[threading.Thread] = None
|
||||||
|
|
||||||
# Initialize ADK Memory Service for conversational memory
|
# Initialize ADK Memory Service for conversational memory
|
||||||
memory_type = os.getenv('MEMORY_SERVICE', 'inmemory')
|
memory_type = os.getenv('MEMORY_SERVICE', 'inmemory')
|
||||||
@@ -75,6 +87,9 @@ class FuzzForgeAgent:
|
|||||||
|
|
||||||
# Create the ADK agent (for A2A server mode)
|
# Create the ADK agent (for A2A server mode)
|
||||||
self.adk_agent = self._create_adk_agent()
|
self.adk_agent = self._create_adk_agent()
|
||||||
|
|
||||||
|
if self._auto_start_server:
|
||||||
|
self._ensure_a2a_server_running()
|
||||||
|
|
||||||
def _create_adk_agent(self) -> Agent:
|
def _create_adk_agent(self) -> Agent:
|
||||||
"""Create the ADK agent for A2A server mode"""
|
"""Create the ADK agent for A2A server mode"""
|
||||||
@@ -119,15 +134,85 @@ When responding to requests:
|
|||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
"""Clean up resources"""
|
"""Clean up resources"""
|
||||||
|
await self._stop_a2a_server()
|
||||||
await self.executor.cleanup()
|
await self.executor.cleanup()
|
||||||
|
|
||||||
|
def _ensure_a2a_server_running(self):
|
||||||
|
"""Start the A2A server in the background if it's not already running."""
|
||||||
|
if self._a2a_server_thread and self._a2a_server_thread.is_alive():
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
from uvicorn import Config, Server
|
||||||
|
from .a2a_server import create_a2a_app as create_custom_a2a_app
|
||||||
|
except ImportError as exc:
|
||||||
|
if os.getenv('FUZZFORGE_DEBUG', '0') == '1':
|
||||||
|
print(f"[DEBUG] Unable to start A2A server automatically: {exc}")
|
||||||
|
return
|
||||||
|
|
||||||
|
app = create_custom_a2a_app(
|
||||||
|
self.adk_agent,
|
||||||
|
port=self.port,
|
||||||
|
executor=self.executor,
|
||||||
|
)
|
||||||
|
|
||||||
|
log_level = os.getenv('FUZZFORGE_UVICORN_LOG_LEVEL', 'error')
|
||||||
|
config = Config(app=app, host='127.0.0.1', port=self.port, log_level=log_level, loop='asyncio')
|
||||||
|
server = Server(config=config)
|
||||||
|
self._uvicorn_server = server
|
||||||
|
|
||||||
|
def _run_server():
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
async def _serve():
|
||||||
|
await server.serve()
|
||||||
|
|
||||||
|
try:
|
||||||
|
loop.run_until_complete(_serve())
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
thread = threading.Thread(target=_run_server, name='FuzzForgeA2AServer', daemon=True)
|
||||||
|
thread.start()
|
||||||
|
self._a2a_server_thread = thread
|
||||||
|
|
||||||
|
# Give the server a moment to bind to the port for downstream agents
|
||||||
|
for _ in range(50):
|
||||||
|
if server.should_exit:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
with socket.create_connection(('127.0.0.1', self.port), timeout=0.1):
|
||||||
|
if os.getenv('FUZZFORGE_DEBUG', '0') == '1':
|
||||||
|
print(f"[DEBUG] Auto-started A2A server on http://127.0.0.1:{self.port}")
|
||||||
|
break
|
||||||
|
except OSError:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
async def _stop_a2a_server(self):
|
||||||
|
"""Shut down the background A2A server if we started one."""
|
||||||
|
server = self._uvicorn_server
|
||||||
|
if server is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
server.should_exit = True
|
||||||
|
if self._a2a_server_thread and self._a2a_server_thread.is_alive():
|
||||||
|
# Allow server loop to exit gracefully without blocking event loop
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(asyncio.to_thread(self._a2a_server_thread.join, 5), timeout=6)
|
||||||
|
except (asyncio.TimeoutError, RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._uvicorn_server = None
|
||||||
|
self._a2a_server_thread = None
|
||||||
|
|
||||||
|
|
||||||
# Create a singleton instance for import
|
# Create a singleton instance for import
|
||||||
_instance = None
|
_instance = None
|
||||||
|
|
||||||
def get_fuzzforge_agent() -> FuzzForgeAgent:
|
def get_fuzzforge_agent(auto_start_server: Optional[bool] = None) -> FuzzForgeAgent:
|
||||||
"""Get the singleton FuzzForge agent instance"""
|
"""Get the singleton FuzzForge agent instance"""
|
||||||
global _instance
|
global _instance
|
||||||
if _instance is None:
|
if _instance is None:
|
||||||
_instance = FuzzForgeAgent()
|
_instance = FuzzForgeAgent(auto_start_server=auto_start_server)
|
||||||
return _instance
|
return _instance
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import base64
|
|||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
from typing import Dict, Any, List, Union
|
from typing import Dict, Any, List, Union, Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import os
|
import os
|
||||||
import warnings
|
import warnings
|
||||||
@@ -93,7 +93,8 @@ class FuzzForgeExecutor:
|
|||||||
self._background_tasks: set[asyncio.Task] = set()
|
self._background_tasks: set[asyncio.Task] = set()
|
||||||
self.pending_runs: Dict[str, Dict[str, Any]] = {}
|
self.pending_runs: Dict[str, Dict[str, Any]] = {}
|
||||||
self.session_metadata: Dict[str, Dict[str, Any]] = {}
|
self.session_metadata: Dict[str, Dict[str, Any]] = {}
|
||||||
self._artifact_cache_dir = Path(os.getenv('FUZZFORGE_ARTIFACT_DIR', Path.cwd() / '.fuzzforge' / 'artifacts'))
|
self._project_root = self._detect_project_root()
|
||||||
|
self._artifact_cache_dir = self._resolve_artifact_cache_dir()
|
||||||
self._knowledge_integration = None
|
self._knowledge_integration = None
|
||||||
|
|
||||||
# Initialize Cognee service if available
|
# Initialize Cognee service if available
|
||||||
@@ -194,6 +195,38 @@ class FuzzForgeExecutor:
|
|||||||
if self.debug:
|
if self.debug:
|
||||||
print(f"[DEBUG] Auto-registration error for {url}: {e}")
|
print(f"[DEBUG] Auto-registration error for {url}: {e}")
|
||||||
|
|
||||||
|
def _detect_project_root(self) -> Optional[Path]:
|
||||||
|
"""Locate the active FuzzForge project root directory if available."""
|
||||||
|
env_root = os.getenv('FUZZFORGE_PROJECT_DIR')
|
||||||
|
if env_root:
|
||||||
|
candidate = Path(env_root).expanduser().resolve()
|
||||||
|
if candidate.joinpath('.fuzzforge').is_dir():
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
try:
|
||||||
|
config = ProjectConfigManager()
|
||||||
|
return config.config_path.parent.resolve()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
current = Path.cwd().resolve()
|
||||||
|
for path in (current,) + tuple(current.parents):
|
||||||
|
if path.joinpath('.fuzzforge').is_dir():
|
||||||
|
return path
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _resolve_artifact_cache_dir(self) -> Path:
|
||||||
|
"""Determine the artifact cache directory, prioritizing project context."""
|
||||||
|
env_dir = os.getenv('FUZZFORGE_ARTIFACT_DIR')
|
||||||
|
if env_dir:
|
||||||
|
return Path(env_dir).expanduser().resolve()
|
||||||
|
|
||||||
|
project_root = self._project_root
|
||||||
|
if project_root:
|
||||||
|
return (project_root / '.fuzzforge' / 'artifacts').resolve()
|
||||||
|
|
||||||
|
return (Path.cwd() / '.fuzzforge' / 'artifacts').resolve()
|
||||||
|
|
||||||
def _create_artifact_service(self):
|
def _create_artifact_service(self):
|
||||||
"""Create artifact service based on configuration"""
|
"""Create artifact service based on configuration"""
|
||||||
artifact_storage = os.getenv('ARTIFACT_STORAGE', 'inmemory')
|
artifact_storage = os.getenv('ARTIFACT_STORAGE', 'inmemory')
|
||||||
@@ -788,6 +821,39 @@ class FuzzForgeExecutor:
|
|||||||
|
|
||||||
tools.append(FunctionTool(send_file_to_agent))
|
tools.append(FunctionTool(send_file_to_agent))
|
||||||
|
|
||||||
|
async def send_code_snippet_to_agent(
|
||||||
|
agent_name: str,
|
||||||
|
code: str,
|
||||||
|
filename: str = "",
|
||||||
|
note: str = "",
|
||||||
|
tool_context: ToolContext | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Create an artifact from raw code and send it to a registered agent."""
|
||||||
|
if not agent_name:
|
||||||
|
return "agent_name is required"
|
||||||
|
if not code or not code.strip():
|
||||||
|
return "code is required"
|
||||||
|
|
||||||
|
session = None
|
||||||
|
context_id = None
|
||||||
|
if tool_context and getattr(tool_context, "invocation_context", None):
|
||||||
|
invocation = tool_context.invocation_context
|
||||||
|
session = invocation.session
|
||||||
|
context_id = self.session_lookup.get(getattr(session, 'id', None))
|
||||||
|
|
||||||
|
target_filename = filename or "snippet.rs"
|
||||||
|
snippet_note = note or "Please analyse the provided code snippet."
|
||||||
|
return await self.delegate_code_snippet_to_agent(
|
||||||
|
agent_name,
|
||||||
|
target_filename,
|
||||||
|
code,
|
||||||
|
note=snippet_note,
|
||||||
|
session=session,
|
||||||
|
context_id=context_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
tools.append(FunctionTool(send_code_snippet_to_agent))
|
||||||
|
|
||||||
if self.debug:
|
if self.debug:
|
||||||
print("[DEBUG] Added Cognee project integration tools")
|
print("[DEBUG] Added Cognee project integration tools")
|
||||||
|
|
||||||
@@ -1886,11 +1952,14 @@ Be concise and intelligent in your responses."""
|
|||||||
|
|
||||||
async def create_project_file_artifact_api(self, file_path: str) -> Dict[str, Any]:
|
async def create_project_file_artifact_api(self, file_path: str) -> Dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
config = ProjectConfigManager()
|
config = ProjectConfigManager(self._project_root) if self._project_root else ProjectConfigManager()
|
||||||
if not config.is_initialized():
|
if not config.is_initialized():
|
||||||
return {"error": "Project not initialized. Run 'fuzzforge init' first."}
|
return {"error": "Project not initialized. Run 'fuzzforge init' first."}
|
||||||
|
|
||||||
project_root = config.config_path.parent.resolve()
|
project_root = self._project_root or config.config_path.parent.resolve()
|
||||||
|
if self._project_root is None:
|
||||||
|
self._project_root = project_root
|
||||||
|
self._artifact_cache_dir = self._resolve_artifact_cache_dir()
|
||||||
requested_file = (project_root / file_path).resolve()
|
requested_file = (project_root / file_path).resolve()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -2101,6 +2170,45 @@ Be concise and intelligent in your responses."""
|
|||||||
await self._append_external_event(session, agent_name, response_text)
|
await self._append_external_event(session, agent_name, response_text)
|
||||||
return response_text
|
return response_text
|
||||||
|
|
||||||
|
async def delegate_code_snippet_to_agent(
|
||||||
|
self,
|
||||||
|
agent_name: str,
|
||||||
|
filename: str,
|
||||||
|
code: str,
|
||||||
|
note: str = "",
|
||||||
|
session: Any = None,
|
||||||
|
context_id: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
try:
|
||||||
|
if not code or not code.strip():
|
||||||
|
return "No code snippet provided for delegation."
|
||||||
|
|
||||||
|
cache_dir = self._prepare_artifact_cache_dir()
|
||||||
|
artifact_id = uuid.uuid4().hex
|
||||||
|
|
||||||
|
# Normalise filename and ensure extension
|
||||||
|
safe_filename = (filename or "snippet.rs").strip()
|
||||||
|
if not safe_filename:
|
||||||
|
safe_filename = "snippet.rs"
|
||||||
|
if "." not in safe_filename:
|
||||||
|
safe_filename = f"{safe_filename}.rs"
|
||||||
|
|
||||||
|
snippet_dir = cache_dir / artifact_id
|
||||||
|
snippet_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
file_path = snippet_dir / safe_filename
|
||||||
|
file_path.write_text(code, encoding="utf-8")
|
||||||
|
|
||||||
|
message_note = note or f"Please analyse the code snippet {safe_filename}."
|
||||||
|
return await self.delegate_file_to_agent(
|
||||||
|
agent_name,
|
||||||
|
str(file_path),
|
||||||
|
message_note,
|
||||||
|
session=session,
|
||||||
|
context_id=context_id,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
return f"Failed to delegate code snippet: {exc}"
|
||||||
|
|
||||||
async def delegate_file_to_agent(
|
async def delegate_file_to_agent(
|
||||||
self,
|
self,
|
||||||
agent_name: str,
|
agent_name: str,
|
||||||
|
|||||||
Reference in New Issue
Block a user