| """Durable artifact handoffs between ephemeral research sandboxes.""" |
|
|
| from __future__ import annotations |
|
|
| import json |
| import posixpath |
| import tempfile |
| from pathlib import Path, PurePosixPath |
| from typing import Any |
|
|
| from huggingface_hub import HfApi |
|
|
| from fast_agent import AgentAuth |
|
|
| from .app_jobs import ResearchJob |
|
|
| SCHEMA_VERSION = 1 |
| RESEARCH_MANIFEST = "scratch/research/manifest.json" |
| MAX_MANIFEST_ARTIFACTS = 128 |
|
|
|
|
| def verify_research_handoff( |
| job: ResearchJob, |
| auth: AgentAuth | None, |
| *, |
| api: HfApi | None = None, |
| ) -> dict[str, Any]: |
| """Verify the durable research handoff before presentation begins.""" |
| if auth is None or not auth.token: |
| raise RuntimeError("Caller authentication is required to verify artifacts") |
|
|
| api = api or HfApi() |
| username = api.whoami(token=auth.token)["name"] |
| bucket_id = f"{username}/research-agent" |
| workspace = job.artifact_id |
| manifest_path = f"{workspace}/{RESEARCH_MANIFEST}" |
|
|
| with tempfile.TemporaryDirectory() as directory: |
| local = Path(directory) / "manifest.json" |
| api.download_bucket_files( |
| bucket_id, |
| [(manifest_path, local)], |
| raise_on_missing_files=True, |
| token=auth.token, |
| ) |
| manifest = json.loads(local.read_text()) |
|
|
| artifacts = validate_stage_manifest( |
| manifest, |
| stage="research", |
| allowed_prefixes=("scratch/research/", "output/"), |
| ) |
| paths = {f"{workspace}/{path}" for path in artifacts} |
| paths.add(f"{workspace}/output/report.md") |
| available = { |
| getattr(item, "path", ""): int(getattr(item, "size", 0) or 0) |
| for item in api.list_bucket_tree( |
| bucket_id, |
| prefix=workspace, |
| recursive=True, |
| token=auth.token, |
| ) |
| if getattr(item, "type", None) == "file" |
| } |
| missing = sorted(path for path in paths if available.get(path, 0) <= 0) |
| if missing: |
| raise FileNotFoundError( |
| "Research handoff declared missing or empty artifacts: " |
| + ", ".join(missing) |
| ) |
| if "output/report.md" not in artifacts: |
| raise ValueError("Research manifest must declare output/report.md") |
| return manifest |
|
|
|
|
| def validate_stage_manifest( |
| manifest: object, |
| *, |
| stage: str, |
| allowed_prefixes: tuple[str, ...], |
| ) -> tuple[str, ...]: |
| """Validate a bounded stage manifest and return declared relative paths.""" |
| if not isinstance(manifest, dict): |
| raise ValueError("Artifact manifest must be a JSON object") |
| if manifest.get("schema_version") != SCHEMA_VERSION: |
| raise ValueError(f"Artifact manifest schema_version must be {SCHEMA_VERSION}") |
| if manifest.get("stage") != stage: |
| raise ValueError(f"Artifact manifest stage must be {stage!r}") |
| if manifest.get("status") != "complete": |
| raise ValueError("Artifact manifest status must be 'complete'") |
|
|
| records = manifest.get("artifacts") |
| if not isinstance(records, list) or not records: |
| raise ValueError("Artifact manifest must declare at least one artifact") |
| if len(records) > MAX_MANIFEST_ARTIFACTS: |
| raise ValueError( |
| f"Artifact manifest exceeds {MAX_MANIFEST_ARTIFACTS} artifacts" |
| ) |
|
|
| paths: list[str] = [] |
| for record in records: |
| if not isinstance(record, dict): |
| raise ValueError("Artifact manifest entries must be JSON objects") |
| path = safe_artifact_path(record.get("path")) |
| if not any( |
| path == prefix or path.startswith(prefix) |
| for prefix in allowed_prefixes |
| ): |
| raise ValueError(f"Artifact path is outside the {stage} boundary: {path}") |
| paths.append(path) |
| if len(paths) != len(set(paths)): |
| raise ValueError("Artifact manifest contains duplicate paths") |
| return tuple(paths) |
|
|
|
|
| def safe_artifact_path(value: object) -> str: |
| """Return one normalized workspace-relative artifact path.""" |
| raw = str(value or "").strip() |
| candidate = PurePosixPath(raw) |
| normalized = posixpath.normpath(raw) |
| if ( |
| not raw |
| or candidate.is_absolute() |
| or ".." in candidate.parts |
| or normalized in {"", "."} |
| or normalized.startswith("../") |
| ): |
| raise ValueError(f"Artifact path must be workspace-relative: {raw!r}") |
| return normalized |
|
|