feat(perception): add autonomous vegetation shadow lab

This commit is contained in:
DCCONSTRUCTIONS
2026-08-27 23:26:35 +03:00
parent 7594c71dd1
commit 57204b3b0b
26 changed files with 2494 additions and 5 deletions
+12
View File
@@ -138,6 +138,7 @@ from k1link.web.m49_physical_safety_playback_api import (
)
from k1link.web.m49_tgs_fail_closed_api import build_m49_tgs_fail_closed_router
from k1link.web.m49_tgs_full_shadow_api import build_m49_tgs_full_shadow_router
from k1link.web.vegetation_shadow_lab_api import build_vegetation_shadow_lab_router
from k1link.web.map_api import (
MapGatewayConfiguration,
MapGatewayProxy,
@@ -1019,6 +1020,17 @@ app.include_router(
),
)
)
app.include_router(
build_vegetation_shadow_lab_router(
root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "lab-v1-vegetation"
/ "results"
),
)
)
app.include_router(
build_m49_physical_safety_playback_router(
root_provider=lambda: (
+166
View File
@@ -0,0 +1,166 @@
"""Read-only API for the autonomous vegetation policy shadow LAB."""
from __future__ import annotations
import copy
import json
import re
from collections.abc import Callable
from functools import lru_cache
from pathlib import Path, PurePosixPath
from typing import Any, Final
from fastapi import APIRouter, HTTPException
from fastapi.responses import FileResponse
from k1link.laboratory.evidence_registry import LaboratoryEvidenceDefinition
from k1link.laboratory.evidence_report import (
LaboratoryEvidenceReportError,
verify_laboratory_evidence_result,
)
from k1link.laboratory.vegetation_shadow_lab import LAB_SCHEMA, RESULT_PREFIX
RootProvider = Callable[[], Path | None]
RESULT_ID: Final = re.compile(rf"^{re.escape(RESULT_PREFIX)}[a-f0-9]{{64}}$")
_MAX_DOCUMENT_BYTES: Final = 1024 * 1024
_DEFINITION: Final = LaboratoryEvidenceDefinition(
work_id="lab-v1-vegetation-shadow",
runtime_relative_root=PurePosixPath("lab-v1-vegetation/results"),
result_id_prefix="lab-v1-vegetation-shadow",
document_name="result.json",
result_schema_version=LAB_SCHEMA,
)
def build_vegetation_shadow_lab_router(
*, root_provider: RootProvider = lambda: None,
) -> APIRouter:
router = APIRouter(
prefix="/api/v1/laboratory/vegetation-shadow",
tags=["laboratory"],
)
@router.get("/{result_id}")
def get_result(result_id: str) -> dict[str, object]:
candidate = _resolve_candidate(root_provider, result_id)
return {**copy.deepcopy(_read_verified(candidate)), "access": "read-only"}
@router.get("/{result_id}/assets/{asset_path:path}")
def get_asset(result_id: str, asset_path: str) -> FileResponse:
candidate = _resolve_candidate(root_provider, result_id)
manifest = _read_verified(candidate)
artifacts = manifest.get("artifacts")
if not isinstance(artifacts, list):
raise HTTPException(status_code=404, detail="Vegetation LAB asset not found")
descriptor = next(
(
item
for item in artifacts
if isinstance(item, dict) and item.get("path") == asset_path
),
None,
)
if descriptor is None:
raise HTTPException(status_code=404, detail="Vegetation LAB asset not found")
relative = PurePosixPath(asset_path)
path = candidate.joinpath(*relative.parts)
if (
relative.is_absolute()
or str(relative) != asset_path
or any(part in {"", ".", ".."} for part in relative.parts)
or path.is_symlink()
or not path.is_file()
or not path.resolve().is_relative_to(candidate)
):
raise HTTPException(status_code=404, detail="Vegetation LAB asset not found")
media_type = descriptor.get("media_type")
if not isinstance(media_type, str) or not media_type.startswith("image/"):
raise HTTPException(status_code=404, detail="Vegetation LAB asset not found")
return FileResponse(
path,
media_type=media_type,
headers={
"Cache-Control": "private, max-age=31536000, immutable",
"ETag": f'"{descriptor.get("sha256", "")}"',
"X-Content-Type-Options": "nosniff",
},
)
return router
def _configured_root(provider: RootProvider) -> Path | None:
candidate = provider()
if candidate is None:
return None
absolute = candidate.expanduser().absolute()
if absolute.is_symlink():
return None
try:
root = absolute.resolve(strict=True)
except OSError:
return None
return root if root.is_dir() else None
def _resolve_candidate(provider: RootProvider, result_id: str) -> Path:
root = _configured_root(provider)
if root is None or RESULT_ID.fullmatch(result_id) is None:
raise HTTPException(status_code=404, detail="Vegetation LAB result not found")
candidate = root / result_id
if candidate.is_symlink():
raise HTTPException(status_code=404, detail="Vegetation LAB result not found")
try:
resolved = candidate.resolve(strict=True)
except OSError:
raise HTTPException(status_code=404, detail="Vegetation LAB result not found") from None
if not resolved.is_dir() or not resolved.is_relative_to(root):
raise HTTPException(status_code=404, detail="Vegetation LAB result not found")
return resolved
def _read_verified(candidate: Path) -> dict[str, Any]:
try:
rows: list[tuple[str, int, int, int, int]] = []
for path in sorted(candidate.rglob("*"), key=lambda item: item.as_posix()):
stat = path.lstat()
rows.append((
path.relative_to(candidate).as_posix(),
stat.st_mode,
stat.st_size,
stat.st_mtime_ns,
stat.st_ctime_ns,
))
signature = tuple(rows)
except OSError:
raise HTTPException(
status_code=503,
detail="Vegetation LAB evidence failed verification",
) from None
return _read_verified_cached(str(candidate), signature)
@lru_cache(maxsize=16)
def _read_verified_cached(
candidate_text: str,
signature: tuple[tuple[str, int, int, int, int], ...],
) -> dict[str, Any]:
del signature
candidate = Path(candidate_text)
try:
verify_laboratory_evidence_result(_DEFINITION, candidate)
path = candidate / "result.json"
if path.stat().st_size > _MAX_DOCUMENT_BYTES:
raise LaboratoryEvidenceReportError("Vegetation LAB document is too large")
payload = json.loads(path.read_text("utf-8"))
except (json.JSONDecodeError, OSError, LaboratoryEvidenceReportError):
raise HTTPException(
status_code=503,
detail="Vegetation LAB evidence failed verification",
) from None
if not isinstance(payload, dict):
raise HTTPException(status_code=503, detail="Vegetation LAB evidence is invalid")
return payload
__all__ = ["build_vegetation_shadow_lab_router"]