mirror of
https://github.com/FuzzingLabs/fuzzforge_ai.git
synced 2026-07-06 08:17:48 +02:00
feat(modules): add harness-tester module for Rust fuzzing pipeline
This commit is contained in:
@@ -322,14 +322,21 @@ class ModuleExecutor:
|
||||
self,
|
||||
assets_path: Path,
|
||||
configuration: dict[str, Any] | None = None,
|
||||
project_path: Path | None = None,
|
||||
execution_id: str | None = None,
|
||||
) -> Path:
|
||||
"""Prepare input directory with assets and configuration.
|
||||
|
||||
Creates a temporary directory with input.json describing all resources.
|
||||
Creates a directory with input.json describing all resources.
|
||||
This directory can be volume-mounted into the container.
|
||||
|
||||
If assets_path is a directory, it is used directly (zero-copy mount).
|
||||
If assets_path is a file (e.g., tar.gz), it is extracted first.
|
||||
|
||||
:param assets_path: Path to the assets (file or directory).
|
||||
:param configuration: Optional module configuration dict.
|
||||
:param project_path: Project directory for storing inputs in .fuzzforge/.
|
||||
:param execution_id: Execution ID for organizing inputs.
|
||||
:returns: Path to prepared input directory.
|
||||
:raises SandboxError: If preparation fails.
|
||||
|
||||
@@ -339,12 +346,65 @@ class ModuleExecutor:
|
||||
logger.info("preparing input directory", assets=str(assets_path))
|
||||
|
||||
try:
|
||||
# Create temporary directory - caller must clean it up after container finishes
|
||||
from tempfile import mkdtemp
|
||||
# If assets_path is already a directory, use it directly (zero-copy mount)
|
||||
if assets_path.exists() and assets_path.is_dir():
|
||||
# Create input.json directly in the source directory
|
||||
input_json_path = assets_path / "input.json"
|
||||
|
||||
# Scan files and build resource list
|
||||
resources = []
|
||||
for item in assets_path.iterdir():
|
||||
if item.name == "input.json":
|
||||
continue
|
||||
if item.is_file():
|
||||
resources.append(
|
||||
{
|
||||
"name": item.stem,
|
||||
"description": f"Input file: {item.name}",
|
||||
"kind": "unknown",
|
||||
"path": f"/data/input/{item.name}",
|
||||
}
|
||||
)
|
||||
elif item.is_dir():
|
||||
resources.append(
|
||||
{
|
||||
"name": item.name,
|
||||
"description": f"Input directory: {item.name}",
|
||||
"kind": "unknown",
|
||||
"path": f"/data/input/{item.name}",
|
||||
}
|
||||
)
|
||||
|
||||
temp_path = Path(mkdtemp(prefix="fuzzforge-input-"))
|
||||
input_data = {
|
||||
"settings": configuration or {},
|
||||
"resources": resources,
|
||||
}
|
||||
input_json_path.write_text(json.dumps(input_data, indent=2))
|
||||
|
||||
# Copy assets to temp directory
|
||||
logger.debug("using source directory directly", path=str(assets_path))
|
||||
return assets_path
|
||||
|
||||
# File input: extract to a directory first
|
||||
# Determine input directory location
|
||||
if project_path:
|
||||
# Store inputs in .fuzzforge/inputs/ for visibility
|
||||
from fuzzforge_runner.storage import FUZZFORGE_DIR_NAME
|
||||
exec_id = execution_id or "latest"
|
||||
input_dir = project_path / FUZZFORGE_DIR_NAME / "inputs" / exec_id
|
||||
input_dir.mkdir(parents=True, exist_ok=True)
|
||||
# Clean previous contents if exists
|
||||
import shutil
|
||||
for item in input_dir.iterdir():
|
||||
if item.is_file():
|
||||
item.unlink()
|
||||
elif item.is_dir():
|
||||
shutil.rmtree(item)
|
||||
else:
|
||||
# Fallback to temporary directory
|
||||
from tempfile import mkdtemp
|
||||
input_dir = Path(mkdtemp(prefix="fuzzforge-input-"))
|
||||
|
||||
# Copy/extract assets to input directory
|
||||
if assets_path.exists():
|
||||
if assets_path.is_file():
|
||||
# Check if it's a tar.gz archive that needs extraction
|
||||
@@ -353,26 +413,26 @@ class ModuleExecutor:
|
||||
import tarfile
|
||||
|
||||
with tarfile.open(assets_path, "r:gz") as tar:
|
||||
tar.extractall(path=temp_path)
|
||||
tar.extractall(path=input_dir)
|
||||
logger.debug("extracted tar.gz archive", archive=str(assets_path))
|
||||
else:
|
||||
# Single file - copy it
|
||||
import shutil
|
||||
|
||||
shutil.copy2(assets_path, temp_path / assets_path.name)
|
||||
shutil.copy2(assets_path, input_dir / assets_path.name)
|
||||
else:
|
||||
# Directory - copy all files (including subdirectories)
|
||||
import shutil
|
||||
|
||||
for item in assets_path.iterdir():
|
||||
if item.is_file():
|
||||
shutil.copy2(item, temp_path / item.name)
|
||||
shutil.copy2(item, input_dir / item.name)
|
||||
elif item.is_dir():
|
||||
shutil.copytree(item, temp_path / item.name)
|
||||
shutil.copytree(item, input_dir / item.name, dirs_exist_ok=True)
|
||||
|
||||
# Scan files and directories and build resource list
|
||||
resources = []
|
||||
for item in temp_path.iterdir():
|
||||
for item in input_dir.iterdir():
|
||||
if item.name == "input.json":
|
||||
continue
|
||||
if item.is_file():
|
||||
@@ -399,11 +459,11 @@ class ModuleExecutor:
|
||||
"settings": configuration or {},
|
||||
"resources": resources,
|
||||
}
|
||||
input_json_path = temp_path / "input.json"
|
||||
input_json_path = input_dir / "input.json"
|
||||
input_json_path.write_text(json.dumps(input_data, indent=2))
|
||||
|
||||
logger.debug("prepared input directory", resources=len(resources), path=str(temp_path))
|
||||
return temp_path
|
||||
logger.debug("prepared input directory", resources=len(resources), path=str(input_dir))
|
||||
return input_dir
|
||||
|
||||
except Exception as exc:
|
||||
message = f"Failed to prepare input directory"
|
||||
@@ -542,6 +602,8 @@ class ModuleExecutor:
|
||||
module_identifier: str,
|
||||
assets_path: Path,
|
||||
configuration: dict[str, Any] | None = None,
|
||||
project_path: Path | None = None,
|
||||
execution_id: str | None = None,
|
||||
) -> Path:
|
||||
"""Execute a module end-to-end.
|
||||
|
||||
@@ -552,9 +614,17 @@ class ModuleExecutor:
|
||||
4. Pull results
|
||||
5. Terminate sandbox
|
||||
|
||||
All intermediate files are stored in {project_path}/.fuzzforge/ for
|
||||
easy debugging and visibility.
|
||||
|
||||
Source directories are mounted directly without tar.gz compression
|
||||
for better performance.
|
||||
|
||||
:param module_identifier: Name/identifier of the module to execute.
|
||||
:param assets_path: Path to the input assets archive.
|
||||
:param assets_path: Path to the input assets (file or directory).
|
||||
:param configuration: Optional module configuration.
|
||||
:param project_path: Project directory for .fuzzforge/ storage.
|
||||
:param execution_id: Execution ID for organizing files.
|
||||
:returns: Path to the results archive.
|
||||
:raises ModuleExecutionError: If any step fails.
|
||||
|
||||
@@ -562,10 +632,20 @@ class ModuleExecutor:
|
||||
logger = get_logger()
|
||||
sandbox: str | None = None
|
||||
input_dir: Path | None = None
|
||||
# Don't cleanup if we're using the source directory directly
|
||||
cleanup_input = False
|
||||
|
||||
try:
|
||||
# 1. Prepare input directory with assets
|
||||
input_dir = self.prepare_input_directory(assets_path, configuration)
|
||||
input_dir = self.prepare_input_directory(
|
||||
assets_path,
|
||||
configuration,
|
||||
project_path=project_path,
|
||||
execution_id=execution_id,
|
||||
)
|
||||
|
||||
# Only cleanup if we created a temp directory (file input case)
|
||||
cleanup_input = input_dir != assets_path and project_path is None
|
||||
|
||||
# 2. Spawn sandbox with volume mount
|
||||
sandbox = self.spawn_sandbox(module_identifier, input_volume=input_dir)
|
||||
@@ -585,12 +665,12 @@ class ModuleExecutor:
|
||||
return results_path
|
||||
|
||||
finally:
|
||||
# 5. Always cleanup
|
||||
# 5. Always cleanup sandbox
|
||||
if sandbox:
|
||||
self.terminate_sandbox(sandbox)
|
||||
if input_dir and input_dir.exists():
|
||||
# Only cleanup input if it was a temp directory
|
||||
if cleanup_input and input_dir and input_dir.exists():
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(input_dir, ignore_errors=True)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -602,22 +682,34 @@ class ModuleExecutor:
|
||||
module_identifier: str,
|
||||
assets_path: Path,
|
||||
configuration: dict[str, Any] | None = None,
|
||||
project_path: Path | None = None,
|
||||
execution_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Start a module in continuous/background mode without waiting.
|
||||
|
||||
Returns immediately with container info. Use read_module_output() to
|
||||
get current status and stop_module_continuous() to stop.
|
||||
|
||||
Source directories are mounted directly without tar.gz compression
|
||||
for better performance.
|
||||
|
||||
:param module_identifier: Name/identifier of the module to execute.
|
||||
:param assets_path: Path to the input assets archive.
|
||||
:param assets_path: Path to the input assets (file or directory).
|
||||
:param configuration: Optional module configuration.
|
||||
:param project_path: Project directory for .fuzzforge/ storage.
|
||||
:param execution_id: Execution ID for organizing files.
|
||||
:returns: Dict with container_id, input_dir for later cleanup.
|
||||
|
||||
"""
|
||||
logger = get_logger()
|
||||
|
||||
# 1. Prepare input directory with assets
|
||||
input_dir = self.prepare_input_directory(assets_path, configuration)
|
||||
input_dir = self.prepare_input_directory(
|
||||
assets_path,
|
||||
configuration,
|
||||
project_path=project_path,
|
||||
execution_id=execution_id,
|
||||
)
|
||||
|
||||
# 2. Spawn sandbox with volume mount
|
||||
sandbox = self.spawn_sandbox(module_identifier, input_volume=input_dir)
|
||||
|
||||
@@ -214,11 +214,13 @@ class WorkflowOrchestrator:
|
||||
message = f"No assets available for step {step_index}"
|
||||
raise WorkflowExecutionError(message)
|
||||
|
||||
# Execute the module
|
||||
# Execute the module (inputs stored in .fuzzforge/inputs/)
|
||||
results_path = await self._executor.execute(
|
||||
module_identifier=step.module_identifier,
|
||||
assets_path=current_assets,
|
||||
configuration=step.configuration,
|
||||
project_path=project_path,
|
||||
execution_id=step_execution_id,
|
||||
)
|
||||
|
||||
completed_at = datetime.now(UTC)
|
||||
|
||||
@@ -53,6 +53,36 @@ class ModuleInfo:
|
||||
#: Whether module image exists locally.
|
||||
available: bool = True
|
||||
|
||||
#: Module category (analyzer, validator, fuzzer, reporter).
|
||||
category: str | None = None
|
||||
|
||||
#: Target programming language (e.g., "rust", "python").
|
||||
language: str | None = None
|
||||
|
||||
#: Pipeline stage name (e.g., "analysis", "fuzzing").
|
||||
pipeline_stage: str | None = None
|
||||
|
||||
#: Numeric order in pipeline for sorting.
|
||||
pipeline_order: int | None = None
|
||||
|
||||
#: Module identifiers that must run before this one.
|
||||
dependencies: list[str] | None = None
|
||||
|
||||
#: Whether module supports continuous/background execution.
|
||||
continuous_mode: bool = False
|
||||
|
||||
#: Expected runtime (e.g., "30s", "5m", "continuous").
|
||||
typical_duration: str | None = None
|
||||
|
||||
#: Typical use cases and scenarios for this module.
|
||||
use_cases: list[str] | None = None
|
||||
|
||||
#: Input requirements (e.g., ["rust-source-code", "Cargo.toml"]).
|
||||
input_requirements: list[str] | None = None
|
||||
|
||||
#: Output artifacts produced (e.g., ["fuzzable_functions.json"]).
|
||||
output_artifacts: list[str] | None = None
|
||||
|
||||
|
||||
class Runner:
|
||||
"""Main FuzzForge Runner interface.
|
||||
@@ -125,16 +155,19 @@ class Runner:
|
||||
return self._storage.init_project(project_path)
|
||||
|
||||
def set_project_assets(self, project_path: Path, assets_path: Path) -> Path:
|
||||
"""Set initial assets for a project.
|
||||
"""Set source path for a project (no copying).
|
||||
|
||||
Just stores a reference to the source directory.
|
||||
The source is mounted directly into containers at runtime.
|
||||
|
||||
:param project_path: Path to the project directory.
|
||||
:param assets_path: Path to assets (file or directory).
|
||||
:returns: Path to stored assets.
|
||||
:param assets_path: Path to source directory.
|
||||
:returns: The assets path (unchanged).
|
||||
|
||||
"""
|
||||
logger = get_logger()
|
||||
logger.info("setting project assets", project=str(project_path), assets=str(assets_path))
|
||||
return self._storage.store_assets(project_path, assets_path)
|
||||
return self._storage.set_project_assets(project_path, assets_path)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Module Discovery
|
||||
@@ -182,12 +215,15 @@ class Runner:
|
||||
"""List available module images from the container engine.
|
||||
|
||||
Uses the container engine API to discover built module images.
|
||||
Reads metadata from pyproject.toml inside each image.
|
||||
|
||||
:param filter_prefix: Prefix to filter images (default: "fuzzforge-").
|
||||
:param include_all_tags: If True, include all image tags, not just 'latest'.
|
||||
:returns: List of available module images.
|
||||
|
||||
"""
|
||||
import tomllib # noqa: PLC0415
|
||||
|
||||
logger = get_logger()
|
||||
modules: list[ModuleInfo] = []
|
||||
seen: set[str] = set()
|
||||
@@ -223,18 +259,67 @@ class Runner:
|
||||
# Add unique modules
|
||||
if module_name not in seen:
|
||||
seen.add(module_name)
|
||||
|
||||
# Read metadata from pyproject.toml inside the image
|
||||
image_ref = f"{image.repository}:{image.tag}"
|
||||
module_meta = self._get_module_metadata_from_image(engine, image_ref)
|
||||
|
||||
# Get basic info from pyproject.toml [project] section
|
||||
project_info = module_meta.get("_project", {})
|
||||
fuzzforge_meta = module_meta.get("module", {})
|
||||
|
||||
modules.append(
|
||||
ModuleInfo(
|
||||
identifier=module_name,
|
||||
description=None,
|
||||
version=image.tag,
|
||||
identifier=fuzzforge_meta.get("identifier", module_name),
|
||||
description=project_info.get("description"),
|
||||
version=project_info.get("version", image.tag),
|
||||
available=True,
|
||||
category=fuzzforge_meta.get("category"),
|
||||
language=fuzzforge_meta.get("language"),
|
||||
pipeline_stage=fuzzforge_meta.get("pipeline_stage"),
|
||||
pipeline_order=fuzzforge_meta.get("pipeline_order"),
|
||||
dependencies=fuzzforge_meta.get("dependencies", []),
|
||||
continuous_mode=fuzzforge_meta.get("continuous_mode", False),
|
||||
typical_duration=fuzzforge_meta.get("typical_duration"),
|
||||
use_cases=fuzzforge_meta.get("use_cases", []),
|
||||
input_requirements=fuzzforge_meta.get("input_requirements", []),
|
||||
output_artifacts=fuzzforge_meta.get("output_artifacts", []),
|
||||
)
|
||||
)
|
||||
|
||||
logger.info("listed module images", count=len(modules))
|
||||
return modules
|
||||
|
||||
def _get_module_metadata_from_image(self, engine: Any, image_ref: str) -> dict:
|
||||
"""Read module metadata from pyproject.toml inside a container image.
|
||||
|
||||
:param engine: Container engine instance.
|
||||
:param image_ref: Image reference (e.g., "fuzzforge-rust-analyzer:latest").
|
||||
:returns: Dict with module metadata from [tool.fuzzforge] section.
|
||||
|
||||
"""
|
||||
import tomllib # noqa: PLC0415
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
try:
|
||||
# Read pyproject.toml from the image
|
||||
content = engine.read_file_from_image(image_ref, "/app/pyproject.toml")
|
||||
if not content:
|
||||
logger.debug("no pyproject.toml found in image", image=image_ref)
|
||||
return {}
|
||||
|
||||
pyproject = tomllib.loads(content)
|
||||
|
||||
# Return the [tool.fuzzforge] section plus [project] info
|
||||
result = pyproject.get("tool", {}).get("fuzzforge", {})
|
||||
result["_project"] = pyproject.get("project", {})
|
||||
return result
|
||||
|
||||
except Exception as exc:
|
||||
logger.debug("failed to read metadata from image", image=image_ref, error=str(exc))
|
||||
return {}
|
||||
|
||||
def get_module_info(self, module_identifier: str) -> ModuleInfo | None:
|
||||
"""Get information about a specific module.
|
||||
|
||||
|
||||
@@ -34,23 +34,14 @@ class EngineSettings(BaseModel):
|
||||
|
||||
|
||||
class StorageSettings(BaseModel):
|
||||
"""Storage configuration for local or S3 storage."""
|
||||
"""Storage configuration for local filesystem storage.
|
||||
|
||||
#: Storage backend type.
|
||||
type: Literal["local", "s3"] = "local"
|
||||
OSS uses direct file mounting without archiving for simplicity.
|
||||
"""
|
||||
|
||||
#: Base path for local storage (used when type is "local").
|
||||
#: Base path for local storage.
|
||||
path: Path = Field(default=Path.home() / ".fuzzforge" / "storage")
|
||||
|
||||
#: S3 endpoint URL (used when type is "s3").
|
||||
s3_endpoint: str | None = None
|
||||
|
||||
#: S3 access key (used when type is "s3").
|
||||
s3_access_key: str | None = None
|
||||
|
||||
#: S3 secret key (used when type is "s3").
|
||||
s3_secret_key: str | None = None
|
||||
|
||||
|
||||
class ProjectSettings(BaseModel):
|
||||
"""Project configuration."""
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
"""FuzzForge Runner - Local filesystem storage.
|
||||
|
||||
This module provides local filesystem storage as an alternative to S3,
|
||||
enabling zero-configuration operation for OSS deployments.
|
||||
This module provides local filesystem storage for OSS deployments.
|
||||
|
||||
Storage is placed directly in the project directory as `.fuzzforge/`
|
||||
for maximum visibility and ease of debugging.
|
||||
|
||||
In OSS mode, source files are referenced (not copied) and mounted
|
||||
directly into containers at runtime for zero-copy performance.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
from pathlib import Path, PurePath
|
||||
from pathlib import Path
|
||||
from tarfile import open as Archive # noqa: N812
|
||||
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from fuzzforge_runner.constants import RESULTS_ARCHIVE_FILENAME
|
||||
@@ -19,6 +23,9 @@ from fuzzforge_runner.exceptions import StorageError
|
||||
if TYPE_CHECKING:
|
||||
from structlog.stdlib import BoundLogger
|
||||
|
||||
#: Name of the FuzzForge storage directory within projects.
|
||||
FUZZFORGE_DIR_NAME: str = ".fuzzforge"
|
||||
|
||||
|
||||
def get_logger() -> BoundLogger:
|
||||
"""Get structlog logger instance.
|
||||
@@ -32,33 +39,36 @@ def get_logger() -> BoundLogger:
|
||||
|
||||
|
||||
class LocalStorage:
|
||||
"""Local filesystem storage backend.
|
||||
"""Local filesystem storage backend for FuzzForge OSS.
|
||||
|
||||
Provides S3-like operations using local filesystem, enabling
|
||||
FuzzForge operation without external storage infrastructure.
|
||||
Provides lightweight storage for execution results while using
|
||||
direct source mounting (no copying) for input assets.
|
||||
|
||||
Directory structure:
|
||||
{base_path}/
|
||||
projects/
|
||||
{project_id}/
|
||||
assets/ # Initial project assets
|
||||
runs/
|
||||
{execution_id}/
|
||||
Storage is placed directly in the project directory as `.fuzzforge/`
|
||||
so users can easily inspect outputs and configuration.
|
||||
|
||||
Directory structure (inside project directory):
|
||||
{project_path}/.fuzzforge/
|
||||
config.json # Project config (source path reference)
|
||||
runs/ # Execution results
|
||||
{execution_id}/
|
||||
results.tar.gz
|
||||
{workflow_id}/
|
||||
modules/
|
||||
step-0-{exec_id}/
|
||||
results.tar.gz
|
||||
{workflow_id}/
|
||||
modules/
|
||||
step-0-{exec_id}/
|
||||
results.tar.gz
|
||||
|
||||
Source files are NOT copied - they are referenced and mounted directly.
|
||||
|
||||
"""
|
||||
|
||||
#: Base path for all storage operations.
|
||||
#: Base path for global storage (only used for fallback/config).
|
||||
_base_path: Path
|
||||
|
||||
def __init__(self, base_path: Path) -> None:
|
||||
"""Initialize an instance of the class.
|
||||
|
||||
:param base_path: Root directory for storage.
|
||||
:param base_path: Root directory for global storage (fallback only).
|
||||
|
||||
"""
|
||||
self._base_path = base_path
|
||||
@@ -71,17 +81,22 @@ class LocalStorage:
|
||||
def _get_project_path(self, project_path: Path) -> Path:
|
||||
"""Get the storage path for a project.
|
||||
|
||||
:param project_path: Original project path (used as identifier).
|
||||
:returns: Storage path for the project.
|
||||
Storage is placed directly inside the project as `.fuzzforge/`.
|
||||
|
||||
:param project_path: Path to the project directory.
|
||||
:returns: Storage path for the project (.fuzzforge inside project).
|
||||
|
||||
"""
|
||||
# Use project path name as identifier
|
||||
project_id = project_path.name
|
||||
return self._base_path / "projects" / project_id
|
||||
return project_path / FUZZFORGE_DIR_NAME
|
||||
|
||||
def init_project(self, project_path: Path) -> Path:
|
||||
"""Initialize storage for a new project.
|
||||
|
||||
Creates a .fuzzforge/ directory inside the project for storing:
|
||||
- assets/: Input files (source code, etc.)
|
||||
- inputs/: Prepared module inputs (for debugging)
|
||||
- runs/: Execution results from each module
|
||||
|
||||
:param project_path: Path to the project directory.
|
||||
:returns: Path to the project storage directory.
|
||||
|
||||
@@ -89,102 +104,91 @@ class LocalStorage:
|
||||
logger = get_logger()
|
||||
storage_path = self._get_project_path(project_path)
|
||||
|
||||
# Create directory structure
|
||||
(storage_path / "assets").mkdir(parents=True, exist_ok=True)
|
||||
# Create directory structure (minimal for OSS)
|
||||
storage_path.mkdir(parents=True, exist_ok=True)
|
||||
(storage_path / "runs").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create .gitignore to avoid committing large files
|
||||
gitignore_path = storage_path / ".gitignore"
|
||||
if not gitignore_path.exists():
|
||||
gitignore_content = """# FuzzForge storage - ignore large/temporary files
|
||||
# Execution results (can be very large)
|
||||
runs/
|
||||
|
||||
# Project configuration
|
||||
!config.json
|
||||
"""
|
||||
gitignore_path.write_text(gitignore_content)
|
||||
|
||||
logger.info("initialized project storage", project=project_path.name, storage=str(storage_path))
|
||||
|
||||
return storage_path
|
||||
|
||||
def get_project_assets_path(self, project_path: Path) -> Path | None:
|
||||
"""Get the path to project assets archive.
|
||||
"""Get the path to project assets (source directory).
|
||||
|
||||
Returns the configured source path for the project.
|
||||
In OSS mode, this is just a reference to the user's source - no copying.
|
||||
|
||||
:param project_path: Path to the project directory.
|
||||
:returns: Path to assets archive, or None if not found.
|
||||
:returns: Path to source directory, or None if not configured.
|
||||
|
||||
"""
|
||||
storage_path = self._get_project_path(project_path)
|
||||
assets_dir = storage_path / "assets"
|
||||
config_path = storage_path / "config.json"
|
||||
|
||||
# Look for assets archive
|
||||
archive_path = assets_dir / "assets.tar.gz"
|
||||
if archive_path.exists():
|
||||
return archive_path
|
||||
if config_path.exists():
|
||||
import json
|
||||
config = json.loads(config_path.read_text())
|
||||
source_path = config.get("source_path")
|
||||
if source_path:
|
||||
path = Path(source_path)
|
||||
if path.exists():
|
||||
return path
|
||||
|
||||
# Check if there are any files in assets directory
|
||||
if assets_dir.exists() and any(assets_dir.iterdir()):
|
||||
# Create archive from directory contents
|
||||
return self._create_archive_from_directory(assets_dir)
|
||||
# Fallback: check if project_path itself is the source
|
||||
# (common case: user runs from their project directory)
|
||||
if (project_path / "Cargo.toml").exists() or (project_path / "src").exists():
|
||||
return project_path
|
||||
|
||||
return None
|
||||
|
||||
def _create_archive_from_directory(self, directory: Path) -> Path:
|
||||
"""Create a tar.gz archive from a directory's contents.
|
||||
def set_project_assets(self, project_path: Path, assets_path: Path) -> Path:
|
||||
"""Set the source path for a project (no copying).
|
||||
|
||||
:param directory: Directory to archive.
|
||||
:returns: Path to the created archive.
|
||||
|
||||
"""
|
||||
archive_path = directory.parent / f"{directory.name}.tar.gz"
|
||||
|
||||
with Archive(archive_path, "w:gz") as tar:
|
||||
for item in directory.iterdir():
|
||||
tar.add(item, arcname=item.name)
|
||||
|
||||
return archive_path
|
||||
|
||||
def create_empty_assets_archive(self, project_path: Path) -> Path:
|
||||
"""Create an empty assets archive for a project.
|
||||
Just stores a reference to the source directory.
|
||||
The source is mounted directly into containers at runtime.
|
||||
|
||||
:param project_path: Path to the project directory.
|
||||
:returns: Path to the empty archive.
|
||||
:param assets_path: Path to source directory.
|
||||
:returns: The assets path (unchanged).
|
||||
:raises StorageError: If path doesn't exist.
|
||||
|
||||
"""
|
||||
storage_path = self._get_project_path(project_path)
|
||||
assets_dir = storage_path / "assets"
|
||||
assets_dir.mkdir(parents=True, exist_ok=True)
|
||||
import json
|
||||
|
||||
archive_path = assets_dir / "assets.tar.gz"
|
||||
|
||||
# Create empty archive
|
||||
with Archive(archive_path, "w:gz") as tar:
|
||||
pass # Empty archive
|
||||
|
||||
return archive_path
|
||||
|
||||
def store_assets(self, project_path: Path, assets_path: Path) -> Path:
|
||||
"""Store project assets from a local path.
|
||||
|
||||
:param project_path: Path to the project directory.
|
||||
:param assets_path: Source path (file or directory) to store.
|
||||
:returns: Path to the stored assets.
|
||||
:raises StorageError: If storage operation fails.
|
||||
|
||||
"""
|
||||
logger = get_logger()
|
||||
|
||||
if not assets_path.exists():
|
||||
raise StorageError(f"Assets path does not exist: {assets_path}")
|
||||
|
||||
# Resolve to absolute path
|
||||
assets_path = assets_path.resolve()
|
||||
|
||||
# Store reference in config
|
||||
storage_path = self._get_project_path(project_path)
|
||||
assets_dir = storage_path / "assets"
|
||||
assets_dir.mkdir(parents=True, exist_ok=True)
|
||||
storage_path.mkdir(parents=True, exist_ok=True)
|
||||
config_path = storage_path / "config.json"
|
||||
|
||||
try:
|
||||
if assets_path.is_file():
|
||||
# Copy archive directly
|
||||
dest_path = assets_dir / "assets.tar.gz"
|
||||
shutil.copy2(assets_path, dest_path)
|
||||
else:
|
||||
# Create archive from directory
|
||||
dest_path = assets_dir / "assets.tar.gz"
|
||||
with Archive(dest_path, "w:gz") as tar:
|
||||
for item in assets_path.iterdir():
|
||||
tar.add(item, arcname=item.name)
|
||||
config: dict = {}
|
||||
if config_path.exists():
|
||||
config = json.loads(config_path.read_text())
|
||||
|
||||
logger.info("stored project assets", project=project_path.name, path=str(dest_path))
|
||||
return dest_path
|
||||
config["source_path"] = str(assets_path)
|
||||
config_path.write_text(json.dumps(config, indent=2))
|
||||
|
||||
except Exception as exc:
|
||||
message = f"Failed to store assets: {exc}"
|
||||
raise StorageError(message) from exc
|
||||
logger.info("set project assets", project=project_path.name, source=str(assets_path))
|
||||
return assets_path
|
||||
|
||||
def store_execution_results(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user