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.
135 lines
5.2 KiB
Python
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)
|