mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-16 14:06:27 +03:00
fix: relocate binary analysis skill
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
"""Project manifest — load, save, validate, and atomically write project.json.
|
||||
|
||||
Uses the atomic write utility (tempfile + os.rename) to ensure that
|
||||
project.json is never partially written. A process crash during a write
|
||||
leaves the previous valid manifest (or no manifest) but never a corrupted one.
|
||||
|
||||
Key guarantees:
|
||||
- Loads project manifests as typed dicts with validation.
|
||||
- Saves project manifests atomically via atomic_write_json.
|
||||
- Detects corrupted manifests (invalid JSON) and raises InvalidConfigError
|
||||
with exit code 4.
|
||||
- Detects missing required fields in manifest and treats as corruption.
|
||||
- Provides helpers to create new project manifests with proper defaults.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from binary_analysis.domain.enums import ProjectState
|
||||
from binary_analysis.domain.errors import InvalidConfigError
|
||||
from binary_analysis.projects.atomic import atomic_write_json
|
||||
|
||||
# Required top-level fields in project.json
|
||||
_REQUIRED_FIELDS: tuple[str, ...] = (
|
||||
"id",
|
||||
"name",
|
||||
"state",
|
||||
"created_at",
|
||||
"workspace_version",
|
||||
"binary_count",
|
||||
"is_stale",
|
||||
)
|
||||
|
||||
# Current workspace format version
|
||||
_WORKSPACE_VERSION = "1"
|
||||
|
||||
# Manifest filename within a project workspace
|
||||
MANIFEST_FILENAME = "project.json"
|
||||
|
||||
|
||||
def create_manifest(
|
||||
project_name: str,
|
||||
project_id: UUID | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Create a new project manifest dict with default values.
|
||||
|
||||
The manifest is in the CREATED state, with a new UUID and current timestamp.
|
||||
|
||||
Args:
|
||||
project_name: The project name.
|
||||
project_id: Optional UUID; auto-generated if not provided.
|
||||
|
||||
Returns:
|
||||
A dict representing the project manifest, ready to be saved.
|
||||
"""
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
if project_id is None:
|
||||
project_id = uuid4()
|
||||
|
||||
return {
|
||||
"id": str(project_id),
|
||||
"name": project_name,
|
||||
"state": ProjectState.CREATED.value,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
"workspace_version": _WORKSPACE_VERSION,
|
||||
"binary_count": 0,
|
||||
"is_stale": False,
|
||||
"lock": None,
|
||||
"description": None,
|
||||
"max_binary_size_bytes": None,
|
||||
}
|
||||
|
||||
|
||||
def save_manifest(project_path: str, manifest: dict[str, Any]) -> None:
|
||||
"""Atomically save a project manifest to project.json.
|
||||
|
||||
Uses tempfile + os.rename to guarantee the file is never partially
|
||||
written. If the process crashes mid-write, the previous valid manifest
|
||||
(or no file) is left intact.
|
||||
|
||||
Args:
|
||||
project_path: Absolute path to the project workspace directory.
|
||||
manifest: The manifest dict to save.
|
||||
|
||||
Raises:
|
||||
ValueError: If the manifest is missing required fields.
|
||||
"""
|
||||
_validate_manifest(manifest)
|
||||
manifest_path = f"{project_path}/{MANIFEST_FILENAME}"
|
||||
atomic_write_json(manifest_path, manifest)
|
||||
|
||||
|
||||
def load_manifest(project_path: str) -> dict[str, Any]:
|
||||
"""Load a project manifest from project.json.
|
||||
|
||||
Reads and validates the manifest. If the file is missing, raises
|
||||
FileNotFoundError. If the JSON is invalid, raises InvalidConfigError
|
||||
(exit code 4) with a diagnostic explaining the corruption.
|
||||
If required fields are missing, raises InvalidConfigError.
|
||||
|
||||
Args:
|
||||
project_path: Absolute path to the project workspace directory.
|
||||
|
||||
Returns:
|
||||
The parsed and validated manifest dict.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If project.json does not exist.
|
||||
InvalidConfigError: If the manifest is corrupted (invalid JSON or
|
||||
missing required fields). Exit code 4.
|
||||
"""
|
||||
manifest_path = f"{project_path}/{MANIFEST_FILENAME}"
|
||||
|
||||
try:
|
||||
with open(manifest_path, encoding="utf-8") as f:
|
||||
raw_text = f.read()
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError(f"Project manifest not found: {manifest_path}") from None
|
||||
|
||||
# Parse JSON — detect corruption
|
||||
try:
|
||||
manifest = json.loads(raw_text)
|
||||
except json.JSONDecodeError as e:
|
||||
raise InvalidConfigError(
|
||||
f"Corrupted project manifest at {manifest_path}: invalid JSON. "
|
||||
f"Parse error: {e.msg} at line {e.lineno}, column {e.colno}. "
|
||||
f"The file must be repaired or the project workspace re-created."
|
||||
) from e
|
||||
|
||||
if not isinstance(manifest, dict):
|
||||
raise InvalidConfigError(
|
||||
f"Corrupted project manifest at {manifest_path}: "
|
||||
f"expected a JSON object, got {type(manifest).__name__}."
|
||||
)
|
||||
|
||||
# Validate required fields
|
||||
_validate_manifest(manifest)
|
||||
|
||||
return manifest
|
||||
|
||||
|
||||
def _validate_manifest(manifest: dict[str, Any]) -> None:
|
||||
"""Validate that a manifest dict has all required fields.
|
||||
|
||||
Args:
|
||||
manifest: The manifest dict to validate.
|
||||
|
||||
Raises:
|
||||
InvalidConfigError: If required fields are missing.
|
||||
"""
|
||||
missing = [field for field in _REQUIRED_FIELDS if field not in manifest]
|
||||
if missing:
|
||||
raise InvalidConfigError(
|
||||
f"Corrupted project manifest: missing required fields: {', '.join(missing)}."
|
||||
)
|
||||
|
||||
|
||||
def update_manifest_field(
|
||||
project_path: str,
|
||||
updates: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Load, update fields, and atomically save a project manifest.
|
||||
|
||||
This is a convenience for state transitions and field updates.
|
||||
Automatically updates the `updated_at` timestamp.
|
||||
|
||||
Args:
|
||||
project_path: Absolute path to the project workspace directory.
|
||||
updates: Dict of field names to new values.
|
||||
|
||||
Returns:
|
||||
The updated manifest dict (post-save).
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the project doesn't exist.
|
||||
InvalidConfigError: If the current or updated manifest is corrupted.
|
||||
"""
|
||||
manifest = load_manifest(project_path)
|
||||
manifest.update(updates)
|
||||
manifest["updated_at"] = datetime.now(timezone.utc).isoformat()
|
||||
save_manifest(project_path, manifest)
|
||||
return manifest
|
||||
Reference in New Issue
Block a user