chore: Complete Temporal migration with updated CLI/SDK/docs

This commit includes all remaining Temporal migration changes:

## CLI Updates (cli/)
- Updated workflow execution commands for Temporal
- Enhanced error handling and exceptions
- Updated dependencies in uv.lock

## SDK Updates (sdk/)
- Client methods updated for Temporal workflows
- Updated models for new workflow execution
- Updated dependencies in uv.lock

## Documentation Updates (docs/)
- Architecture documentation for Temporal
- Workflow concept documentation
- Resource management documentation (new)
- Debugging guide (new)
- Updated tutorials and how-to guides
- Troubleshooting updates

## README Updates
- Main README with Temporal instructions
- Backend README
- CLI README
- SDK README

## Other
- Updated IMPLEMENTATION_STATUS.md
- Removed old vulnerable_app.tar.gz

These changes complete the Temporal migration and ensure the
CLI/SDK work correctly with the new backend.
This commit is contained in:
Tanguy Duhamel
2025-10-02 11:26:32 +02:00
parent fe50d4ef72
commit 8e0e167ddd
21 changed files with 2159 additions and 459 deletions
+45 -2
View File
@@ -153,10 +153,10 @@ fuzzforge workflows parameters security_assessment --no-interactive
### Workflow Execution
#### `fuzzforge workflow <workflow> <target-path>`
Execute a security testing workflow.
Execute a security testing workflow with **automatic file upload**.
```bash
# Basic execution
# Basic execution - CLI automatically detects local files and uploads them
fuzzforge workflow security_assessment /path/to/code
# With parameters
@@ -172,6 +172,49 @@ fuzzforge workflow security_assessment /path/to/code \
fuzzforge workflow security_assessment /path/to/code --wait
```
**Automatic File Upload Behavior:**
The CLI intelligently handles target files based on whether they exist locally:
1. **Local file/directory exists****Automatic upload to MinIO**:
- CLI creates a compressed tarball (`.tar.gz`) for directories
- Uploads via HTTP to backend API
- Backend stores in MinIO with unique `target_id`
- Worker downloads from MinIO when ready to analyze
-**Works from any machine** (no shared filesystem needed)
2. **Path doesn't exist locally****Path-based submission** (legacy):
- Path is sent to backend as-is
- Backend expects target to be accessible on its filesystem
- ⚠️ Only works when CLI and backend share filesystem
**Example workflow:**
```bash
$ ff workflow security_assessment ./my-project
🔧 Getting workflow information for: security_assessment
📦 Detected local directory: ./my-project (21 files)
🗜️ Creating compressed tarball...
📤 Uploading to backend (0.01 MB)...
✅ Upload complete! Target ID: 548193a1-f73f-4ec1-8068-19ec2660b8e4
🎯 Executing workflow:
Workflow: security_assessment
Target: my-project.tar.gz (uploaded)
Volume Mode: ro
Status: 🔄 RUNNING
✅ Workflow started successfully!
Execution ID: security_assessment-52781925
```
**Upload Details:**
- **Max file size**: 10 GB (configurable on backend)
- **Compression**: Automatic for directories (reduces upload time)
- **Storage**: Files stored in MinIO (S3-compatible)
- **Lifecycle**: Automatic cleanup after 7 days
- **Caching**: Workers cache downloaded targets for faster repeated workflows
**Options:**
- `--param, -p` - Parameter in key=value format (can be used multiple times)
- `--param-file, -f` - JSON file containing parameters
+81 -30
View File
@@ -77,7 +77,7 @@ def execute_workflow_submission(
timeout: Optional[int],
interactive: bool
) -> Any:
"""Handle the workflow submission process"""
"""Handle the workflow submission process with file upload"""
# Get workflow metadata for parameter validation
console.print(f"🔧 Getting workflow information for: {workflow}")
workflow_meta = client.get_workflow_metadata(workflow)
@@ -87,7 +87,7 @@ def execute_workflow_submission(
if interactive and workflow_meta.parameters.get("properties"):
properties = workflow_meta.parameters.get("properties", {})
required_params = set(workflow_meta.parameters.get("required", []))
defaults = param_response.defaults
defaults = param_response.default_parameters
missing_required = required_params - set(parameters.keys())
@@ -131,14 +131,6 @@ def execute_workflow_submission(
f"one of: {', '.join(workflow_meta.supported_volume_modes)}"
)
# Create submission
submission = WorkflowSubmission(
target_path=target_path,
volume_mode=volume_mode,
parameters=parameters,
timeout=timeout
)
# Show submission summary
console.print(f"\n🎯 [bold]Executing workflow:[/bold]")
console.print(f" Workflow: {workflow}")
@@ -149,6 +141,22 @@ def execute_workflow_submission(
if timeout:
console.print(f" Timeout: {timeout}s")
# Check if target path exists locally
target_path_obj = Path(target_path)
use_upload = target_path_obj.exists()
if use_upload:
# Show file/directory info
if target_path_obj.is_dir():
num_files = sum(1 for _ in target_path_obj.rglob("*") if _.is_file())
console.print(f" Upload: Directory with {num_files} files")
else:
size_mb = target_path_obj.stat().st_size / (1024 * 1024)
console.print(f" Upload: File ({size_mb:.2f} MB)")
else:
console.print(f" [yellow]⚠️ Warning: Target path does not exist locally[/yellow]")
console.print(f" [yellow] Attempting to use path-based submission (backend must have access)[/yellow]")
# Only ask for confirmation in interactive mode
if interactive:
if not Confirm.ask("\nExecute workflow?", default=True, console=console):
@@ -160,32 +168,75 @@ def execute_workflow_submission(
# Submit the workflow with enhanced progress
console.print(f"\n🚀 Executing workflow: [bold yellow]{workflow}[/bold yellow]")
steps = [
"Validating workflow configuration",
"Connecting to FuzzForge API",
"Uploading parameters and settings",
"Creating workflow deployment",
"Initializing execution environment"
]
if use_upload:
# Use new upload-based submission
steps = [
"Validating workflow configuration",
"Creating tarball (if directory)",
"Uploading target to backend",
"Starting workflow execution",
"Initializing execution environment"
]
with step_progress(steps, f"Executing {workflow}") as progress:
progress.next_step() # Validating
time.sleep(PROGRESS_STEP_DELAYS["validating"])
with step_progress(steps, f"Executing {workflow}") as progress:
progress.next_step() # Validating
time.sleep(PROGRESS_STEP_DELAYS["validating"])
progress.next_step() # Connecting
time.sleep(PROGRESS_STEP_DELAYS["connecting"])
progress.next_step() # Creating tarball
time.sleep(PROGRESS_STEP_DELAYS["connecting"])
progress.next_step() # Uploading
response = client.submit_workflow(workflow, submission)
time.sleep(PROGRESS_STEP_DELAYS["uploading"])
progress.next_step() # Uploading
# Use the new upload method
response = client.submit_workflow_with_upload(
workflow_name=workflow,
target_path=target_path,
parameters=parameters,
volume_mode=volume_mode,
timeout=timeout
)
time.sleep(PROGRESS_STEP_DELAYS["uploading"])
progress.next_step() # Creating deployment
time.sleep(PROGRESS_STEP_DELAYS["creating"])
progress.next_step() # Starting
time.sleep(PROGRESS_STEP_DELAYS["creating"])
progress.next_step() # Initializing
time.sleep(PROGRESS_STEP_DELAYS["initializing"])
progress.next_step() # Initializing
time.sleep(PROGRESS_STEP_DELAYS["initializing"])
progress.complete(f"Workflow started successfully!")
progress.complete(f"Workflow started successfully!")
else:
# Fall back to path-based submission (for backward compatibility)
steps = [
"Validating workflow configuration",
"Connecting to FuzzForge API",
"Submitting workflow parameters",
"Creating workflow deployment",
"Initializing execution environment"
]
with step_progress(steps, f"Executing {workflow}") as progress:
progress.next_step() # Validating
time.sleep(PROGRESS_STEP_DELAYS["validating"])
progress.next_step() # Connecting
time.sleep(PROGRESS_STEP_DELAYS["connecting"])
progress.next_step() # Submitting
submission = WorkflowSubmission(
target_path=target_path,
volume_mode=volume_mode,
parameters=parameters,
timeout=timeout
)
response = client.submit_workflow(workflow, submission)
time.sleep(PROGRESS_STEP_DELAYS["uploading"])
progress.next_step() # Creating deployment
time.sleep(PROGRESS_STEP_DELAYS["creating"])
progress.next_step() # Initializing
time.sleep(PROGRESS_STEP_DELAYS["initializing"])
progress.complete(f"Workflow started successfully!")
return response
+1 -1
View File
@@ -193,7 +193,7 @@ def workflow_parameters(
parameters = {}
properties = workflow.parameters.get("properties", {})
required_params = set(workflow.parameters.get("required", []))
defaults = param_response.defaults
defaults = param_response.default_parameters
if interactive:
console.print("🔧 Enter parameter values (press Enter for default):\n")
+3 -2
View File
@@ -430,8 +430,9 @@ def validate_run_id(run_id: str) -> str:
if not run_id or len(run_id) < 8:
raise ValidationError("run_id", run_id, "at least 8 characters")
if not run_id.replace('-', '').isalnum():
raise ValidationError("run_id", run_id, "alphanumeric characters and hyphens only")
# Allow alphanumeric characters, hyphens, and underscores
if not run_id.replace('-', '').replace('_', '').isalnum():
raise ValidationError("run_id", run_id, "alphanumeric characters, hyphens, and underscores only")
return run_id
Generated
+3 -3
View File
@@ -1257,7 +1257,7 @@ wheels = [
[[package]]
name = "fuzzforge-ai"
version = "0.1.0"
version = "0.6.0"
source = { editable = "../ai" }
dependencies = [
{ name = "a2a-sdk" },
@@ -1303,7 +1303,7 @@ dev = [
[[package]]
name = "fuzzforge-cli"
version = "0.1.0"
version = "0.6.0"
source = { editable = "." }
dependencies = [
{ name = "fuzzforge-ai" },
@@ -1347,7 +1347,7 @@ provides-extras = ["dev"]
[[package]]
name = "fuzzforge-sdk"
version = "0.1.0"
version = "0.6.0"
source = { editable = "../sdk" }
dependencies = [
{ name = "httpx" },