core/packs/audio/loader.py
Shay 7f619d44e5
feat(adr-0181-p3): audio_core_v1 pack + AudioProjectionHead (gate-closed) (#467)
PR-3 of ADR-0181. Externalises the PR-2 in-code operator registry to a
versioned, checksum-locked pack and wires the sensorium adapter. Mounts
gate-closed until the PR-4 eval gates pass. No core mutation (ADR-0013).

Pack (packs/audio/audio_core_v1/):
- manifest.json   canonical params, resampling config, gating defaults, ordering
- operators.jsonl the 8 elliptic operators (byte-equivalent to DEFAULT_OPERATOR_REGISTRY)
- basis_map.json  alias→index over the six elliptic planes {6,7,8,10,11,13}
- resample_fir_v1.npy  pinned odd-length symmetric windowed-sinc low-pass (unit DC)
- checksums.json  sha256 of every artifact

Loader (packs/audio/loader.py) — trust boundary (ADR-0051 / CLAUDE.md §Security):
- _validate_pack_id rejects traversal/separators/dotfiles before any path join
- fail-closed checksum verification: every file re-hashed vs checksums.json
- resolved path must stay under the packs root (defense in depth)
- JSON format (matches every other CORE pack loader; spec §6.1 TOML was
  illustrative; no tomllib dependency added)

Adapter (sensorium/adapters/audio.py):
- AudioProjectionHead wraps AudioCompiler; project(AudioSignal)->(32,) f32
- make_audio_pack loads+verifies the pack, builds a gate-closed ModalityPack

Tests (19): PR-2↔PR-3 registry byte-parity via manifest_sha256, elliptic-only
operators, FIR integrity, fail-closed checksum mismatch, path-traversal guard,
gate-closed refusal, mount-time unitarity (+ bad-unitarity blocks mount),
gate-engage requires checksum_verified, engaged pack projects (32,) f32.

All audio 32 + arch-invariants 40 + smoke 67 green.
2026-05-29 11:05:57 -07:00

135 lines
5.2 KiB
Python

"""
packs/audio/loader.py — audio pack loader (ADR-0181 PR-3).
Trust boundary (CLAUDE.md §Security / ADR-0051):
* The pack id is validated for filesystem safety *before* any path join —
traversal tokens, separators, and dotfiles fail closed.
* The loader is fail-closed on checksum mismatch: every file named in
``checksums.json`` is re-hashed and compared before the pack is observable
to runtime code. A tampered or stale artifact blocks the load (and therefore
the mount — bad checksum blocks pack mount, eval-plan §2).
* The loader never mutates a pack on disk.
The pack format is JSON (matching every other CORE pack loader and avoiding a
TOML parser dependency); ADR-0181 spec §6.1 showed TOML illustratively only.
The loaded operator registry is byte-equivalent to the in-code
``DEFAULT_OPERATOR_REGISTRY`` (asserted by tests via ``manifest_sha256``).
"""
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass
from pathlib import Path
import numpy as np
from sensorium.audio.operators import AudioOperatorRegistry, OperatorSpec
_PACKS_ROOT = Path(__file__).resolve().parent
class AudioPackError(ValueError):
"""Raised when an audio pack is missing, malformed, or fails verification."""
def _validate_pack_id(pack_id: object) -> str:
"""Reject unsafe pack ids before any filesystem access (ADR-0051)."""
from core._safe_display import safe_pack_id as _disp
if not isinstance(pack_id, str):
raise AudioPackError(f"pack_id must be a string, got {_disp(pack_id)!r}")
if pack_id == "":
raise AudioPackError("pack_id must not be empty")
if ".." in pack_id:
raise AudioPackError(f"pack_id must not contain '..': {_disp(pack_id)!r}")
if "/" in pack_id or "\\" in pack_id:
raise AudioPackError(f"pack_id must be a simple pack id, not a path: {_disp(pack_id)!r}")
if pack_id.startswith("."):
raise AudioPackError(f"pack_id must not start with '.': {_disp(pack_id)!r}")
for ch in pack_id:
if not (ch.isascii() and (ch.isalnum() or ch in {"_", "-"})):
raise AudioPackError(f"pack_id must be alphanumeric/_/-, got {_disp(pack_id)!r}")
return pack_id
@dataclass(frozen=True, slots=True)
class LoadedAudioPack:
pack_id: str
manifest: dict
registry: AudioOperatorRegistry
fir: np.ndarray | None
fir_sha256: str
def _verify_checksums(pack_dir: Path) -> None:
"""Re-hash every file in checksums.json; fail closed on any mismatch."""
checks_path = pack_dir / "checksums.json"
if not checks_path.is_file():
raise AudioPackError(f"checksums.json missing for pack at {pack_dir.name}")
checks = json.loads(checks_path.read_text())
for fname, expected in checks.get("files", {}).items():
fpath = pack_dir / fname
if not fpath.is_file():
raise AudioPackError(f"pack file '{fname}' named in checksums.json is missing")
actual = "sha256:" + hashlib.sha256(fpath.read_bytes()).hexdigest()
if actual != expected:
raise AudioPackError(
f"checksum mismatch for '{fname}': expected {expected}, got {actual}"
)
def _load_registry(pack_dir: Path) -> AudioOperatorRegistry:
specs: dict[str, OperatorSpec] = {}
for line in (pack_dir / "operators.jsonl").read_text().splitlines():
line = line.strip()
if not line:
continue
row = json.loads(line)
# gain_rules dict -> sorted (name, gain) pairs for stable serialization.
gains = tuple(sorted((k, int(v)) for k, v in row["gain_rules"].items()))
spec = OperatorSpec(
operator_id=row["operator_id"],
event_type=row["event_type"],
blade_alias=row["blade_alias"],
blade_index=int(row["blade_index"]),
base_theta_q=int(row["base_theta_q"]),
gain_rules=gains,
theta_clip_q=int(row["theta_clip_q"]),
version=str(row.get("version", "1")),
)
specs[spec.event_type] = spec
return AudioOperatorRegistry(specs)
def load_audio_pack(
pack_id: str = "audio_core_v1",
*,
packs_root: Path | None = None,
verify: bool = True,
) -> LoadedAudioPack:
"""Load and verify an audio pack from ``packs/audio/<pack_id>/``."""
safe_id = _validate_pack_id(pack_id)
root = packs_root if packs_root is not None else _PACKS_ROOT
pack_dir = (root / safe_id).resolve()
# Defense in depth: the resolved path must stay under the packs root.
if not str(pack_dir).startswith(str(root.resolve())):
raise AudioPackError(f"resolved pack path escapes packs root: {safe_id!r}")
if not pack_dir.is_dir():
raise AudioPackError(f"no audio pack mounted at {safe_id!r}")
if verify:
_verify_checksums(pack_dir)
manifest = json.loads((pack_dir / "manifest.json").read_text())
registry = _load_registry(pack_dir)
fir_path = pack_dir / manifest.get("resampling", {}).get("fir_path", "resample_fir_v1.npy")
fir: np.ndarray | None = np.load(fir_path) if fir_path.is_file() else None
fir_sha256 = (
"sha256:" + hashlib.sha256(fir_path.read_bytes()).hexdigest()
if fir_path.is_file() else ""
)
return LoadedAudioPack(safe_id, manifest, registry, fir, fir_sha256)