From 9358b58e83faf261c6e3ec3495b60d90122879d6 Mon Sep 17 00:00:00 2001 From: emeisazam <255706292+emeisazam@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:33:38 -0500 Subject: [PATCH 1/4] Harden local API security boundaries --- dashboard/static/app.js | 40 +++++++++++++++++++++++++++------ server/huggingface_access.py | 17 +++++++------- server/routes/cosmoaudition.py | 5 ++++- server/routes/lora.py | 17 ++++++++++---- server/routes/strains.py | 8 +++++-- server/storage.py | 5 ++++- server/wavetable.py | 5 ++++- tests/test_server.py | 41 ++++++++++++++++++++++++++++++++++ 8 files changed, 113 insertions(+), 25 deletions(-) diff --git a/dashboard/static/app.js b/dashboard/static/app.js index 2d2a48f..bc17e54 100644 --- a/dashboard/static/app.js +++ b/dashboard/static/app.js @@ -2275,6 +2275,20 @@ function outputUrl(path) { return `${baseUrl()}/files/${safePath}`; } +function downloadableOutputUrl(path) { + const candidate = outputUrl(path); + if (candidate.startsWith("blob:")) return candidate; + try { + const url = new URL(candidate, window.location.href); + const backendOrigin = new URL(baseUrl(), window.location.href).origin; + if (!new Set([window.location.origin, backendOrigin]).has(url.origin)) return ""; + if (url.protocol !== "http:" && url.protocol !== "https:") return ""; + return url.href; + } catch { + return ""; + } +} + function wavetableExportUrl(id, format = "gwt") { if (!id) return "#"; return `${baseUrl()}/wavetables/${encodeURIComponent(id)}/export?format=${encodeURIComponent(format)}`; @@ -2336,8 +2350,8 @@ function metadataSummary(metadata) { return pieces.filter(Boolean).join(" | "); } -function trackChips(metadata) { - if (!metadata) return "provider -model -seed -"; +function trackChipValues(metadata) { + if (!metadata) return ["provider -", "model -", "seed -"]; const mode = metadata.germinator_mode || modeAliases[metadata.mode] || metadata.mode; return [ `provider ${metadata.provider || "-"}`, @@ -2347,9 +2361,16 @@ function trackChips(metadata) { `seed ${metadata.seed ?? "-"}`, `cfg ${metadata.cfg_scale ?? "-"}`, `steps ${metadata.steps ?? "-"}`, - ] - .map((item) => `${escapeHtml(item)}`) - .join(""); + ]; +} + +function renderTrackChips(target, metadata) { + const chips = trackChipValues(metadata).map((value) => { + const chip = document.createElement("span"); + chip.textContent = String(value); + return chip; + }); + target.replaceChildren(...chips); } async function setCurrentTrack(audioPath, metadataPath, metadata = null) { @@ -2372,7 +2393,7 @@ async function setCurrentTrack(audioPath, metadataPath, metadata = null) { $("trackTitle").textContent = displayNameFromPath(audioPath); $("audioPath").value = audioPath; $("metadataPath").value = metadataPath || loadedMetadata?.metadata_path || ""; - $("trackMeta").innerHTML = trackChips(loadedMetadata); + renderTrackChips($("trackMeta"), loadedMetadata); $("audioPlayer").src = outputUrl(audioPath); $("playPauseBtn").disabled = false; $("playhead").disabled = false; @@ -20779,8 +20800,13 @@ if ($("downloadBtn")) { $("downloadBtn").addEventListener("click", () => { const path = $("audioPath")?.value; if (!path) return; + const href = downloadableOutputUrl(path); + if (!href) { + setState("Download Blocked", "warn", "The selected sound does not have a trusted download URL."); + return; + } const a = document.createElement("a"); - a.href = outputUrl(path); + a.href = href; a.download = path.split("/").pop() || "download.wav"; document.body.appendChild(a); a.click(); diff --git a/server/huggingface_access.py b/server/huggingface_access.py index c968a0a..7e82aba 100644 --- a/server/huggingface_access.py +++ b/server/huggingface_access.py @@ -75,18 +75,18 @@ def auth_status() -> dict[str, Any]: "detail": None, } if not result["available"]: - status["detail"] = result["stderr"] + status["detail"] = "Hugging Face CLI is unavailable." return status if result["returncode"] != 0: - detail = (result["stderr"] or result["stdout"]).strip() - status["detail"] = detail or "hf auth whoami failed." + status["detail"] = "Hugging Face CLI authentication is unavailable." return status try: account = json.loads(result["stdout"] or "{}") except (json.JSONDecodeError, RecursionError): - account = {"raw": result["stdout"].strip()} + status["detail"] = "Hugging Face CLI returned an unreadable account response." + return status status["logged_in"] = True status["account"] = account @@ -99,11 +99,10 @@ def model_access_status(repo_id: str) -> dict[str, Any]: base = { "repo": repo_id, "file": "model_config.json", - "command": result["command"], "returncode": result["returncode"], } if not result["available"]: - return {**base, "status": "hf_missing", "detail": result["stderr"]} + return {**base, "status": "hf_missing", "detail": "Hugging Face CLI is unavailable."} output = f"{result['stdout']}\n{result['stderr']}".strip() lowered = output.lower() @@ -113,11 +112,11 @@ def model_access_status(repo_id: str) -> dict[str, Any]: return { **base, "status": "requires_approval_or_login", - "detail": output, + "detail": "Model access requires accepted terms and an authenticated read token.", } if "not logged in" in lowered or "401" in lowered or "unauthorized" in lowered: - return {**base, "status": "not_logged_in", "detail": output} - return {**base, "status": "error", "detail": output} + return {**base, "status": "not_logged_in", "detail": "Hugging Face authentication is required."} + return {**base, "status": "error", "detail": "Hugging Face model access check failed."} def stable_audio_hf_status(*, check_models: bool = False) -> dict[str, Any]: diff --git a/server/routes/cosmoaudition.py b/server/routes/cosmoaudition.py index 05eeb57..50cd75d 100644 --- a/server/routes/cosmoaudition.py +++ b/server/routes/cosmoaudition.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import logging from pathlib import Path from typing import Any, Literal from uuid import uuid4 @@ -26,6 +27,7 @@ router = APIRouter(prefix="/cosmoaudition", tags=["cosmoaudition"]) MAX_ARCHIVES = 12 MAX_ARCHIVE_BYTES = 1_000_000 +LOGGER = logging.getLogger(__name__) def _bridge() -> CosmoauditionBridge: @@ -37,11 +39,12 @@ def _bridge() -> CosmoauditionBridge: def _bridge_status_from_error(exc: Exception) -> dict[str, Any]: + LOGGER.warning("Cosmoaudition bridge unavailable: %s", exc) return { "available": False, "contract": COSMOAUDITION_GERM_CONTRACT, "baseUrl": settings.cosmoaudition_url, - "error": str(exc)[:2_000], + "error": "Cosmoaudition bridge unavailable", } diff --git a/server/routes/lora.py b/server/routes/lora.py index dd5678f..c7d0330 100644 --- a/server/routes/lora.py +++ b/server/routes/lora.py @@ -1,5 +1,7 @@ from __future__ import annotations +import logging + from fastapi import APIRouter from server.registry import registry @@ -7,14 +9,16 @@ router = APIRouter() +LOGGER = logging.getLogger(__name__) @router.post("/lora/load") def load_lora(request: LoraLoadRequest) -> dict: try: return registry.get(request.provider).load_lora(request.paths) - except Exception as exc: - return {"status": "error", "provider": request.provider, "error": str(exc)} + except Exception: + LOGGER.exception("LoRA load failed for provider %s", request.provider) + return {"status": "error", "provider": request.provider, "error": "LoRA load failed"} @router.post("/lora/strength") @@ -22,5 +26,10 @@ def set_lora_strength(request: LoraStrengthRequest) -> dict: try: provider = registry.get(request.provider) return provider.set_lora_strength(request.strength, request.lora_index) - except Exception as exc: - return {"status": "error", "provider": request.provider, "error": str(exc)} + except Exception: + LOGGER.exception("LoRA strength update failed for provider %s", request.provider) + return { + "status": "error", + "provider": request.provider, + "error": "LoRA strength update failed", + } diff --git a/server/routes/strains.py b/server/routes/strains.py index 70521a9..1431695 100644 --- a/server/routes/strains.py +++ b/server/routes/strains.py @@ -1,5 +1,7 @@ from __future__ import annotations +import logging + from fastapi import APIRouter, HTTPException from server.registry import registry, strain_registry @@ -7,6 +9,7 @@ router = APIRouter(prefix="/strains", tags=["strains"]) +LOGGER = logging.getLogger(__name__) @router.get("", response_model=StrainRegistryResponse) @@ -50,8 +53,9 @@ def load_strains(request: StrainLoadRequest) -> dict: raise HTTPException(status_code=404, detail=f"strain not found: {exc.args[0]}") from exc except ValueError as exc: raise HTTPException(status_code=422, detail=str(exc)) from exc - except Exception as exc: - return {"status": "error", "provider": request.provider, "error": str(exc)} + except Exception: + LOGGER.exception("strain load failed for provider %s", request.provider) + return {"status": "error", "provider": request.provider, "error": "strain load failed"} return { **result, "provider": request.provider, diff --git a/server/storage.py b/server/storage.py index 4b04a67..4b186e3 100644 --- a/server/storage.py +++ b/server/storage.py @@ -1043,7 +1043,10 @@ async def save_upload_stream( ) -> tuple[Path, int]: stem = safe_stem(Path(filename).stem, fallback="upload") suffix = safe_suffix(Path(filename).suffix) - target_dir = Path(directory) if directory is not None else self.upload_dir + target_dir = (Path(directory) if directory is not None else self.upload_dir).resolve() + allowed_upload_roots = (self.upload_dir.resolve(), self.scratch_dir.resolve()) + if not any(self.is_within(target_dir, root) for root in allowed_upload_roots): + raise ValueError("upload directory must be inside a managed upload root") target_dir.mkdir(parents=True, exist_ok=True) path = target_dir / f"{stem}_{uuid4().hex[:8]}{suffix}" total = 0 diff --git a/server/wavetable.py b/server/wavetable.py index 96d33e3..10399ab 100644 --- a/server/wavetable.py +++ b/server/wavetable.py @@ -28,7 +28,10 @@ def note_to_frequency(note: str) -> float: - match = re.fullmatch(r"\s*([A-Ga-g])([#b]?)(-?\d+)\s*", note or "") + normalized = str(note or "").strip() + if len(normalized) > 5: + raise ValueError(f"invalid note name: {note}") + match = re.fullmatch(r"([A-Ga-g])([#b]?)(-?[0-9]{1,2})", normalized) if not match: raise ValueError(f"invalid note name: {note}") note_name, accidental, octave_text = match.groups() diff --git a/tests/test_server.py b/tests/test_server.py index 5ec1c2e..e3a6ea0 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio import base64 import io import json @@ -49,6 +50,7 @@ MAX_TRACKED_JOBS, MAX_TRACKED_JOBS_HARD, ) +from server.wavetable import note_to_frequency client = TestClient(app) @@ -908,6 +910,20 @@ def get_json(self, path: str, *, params: dict | None = None) -> dict: assert deleted.status_code == 200 +def test_cosmoaudition_bridge_errors_do_not_expose_backend_details( + monkeypatch: pytest.MonkeyPatch, +) -> None: + class FailingBridge: + def status(self) -> dict: + raise ValueError("private backend path: /Users/listener/secret") + + monkeypatch.setattr(cosmoaudition_routes, "_bridge", lambda: FailingBridge()) + response = client.get("/cosmoaudition/status") + assert response.status_code == 200 + assert response.json()["error"] == "Cosmoaudition bridge unavailable" + assert "secret" not in response.text + + @pytest.mark.parametrize( ("value", "expected"), [ @@ -1088,6 +1104,31 @@ def test_huggingface_status_reports_cli_auth_without_model_check() -> None: assert body["models_checked"] is False +def test_note_parser_rejects_oversized_or_ambiguous_input() -> None: + assert note_to_frequency(" C#4 ") == pytest.approx(277.1826309768721) + for value in ("C" + "0" * 10_000, "C4 trailing"): + with pytest.raises(ValueError, match="invalid note name"): + note_to_frequency(value) + with pytest.raises(ValueError, match="outside the supported MIDI range"): + note_to_frequency("C99") + + +def test_upload_stream_rejects_directory_outside_managed_roots(tmp_path: Path) -> None: + class Upload: + async def read(self, _size: int) -> bytes: + return b"payload" + + with pytest.raises(ValueError, match="managed upload root"): + asyncio.run( + storage.save_upload_stream( + filename="outside.wav", + upload=Upload(), + max_bytes=1024, + directory=tmp_path, + ) + ) + + def test_mock_generate_creates_wav_and_metadata() -> None: response = client.post( "/generate", From dcfa6885b38316e2d46960e8b938dc6f57664f00 Mon Sep 17 00:00:00 2001 From: emeisazam <255706292+emeisazam@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:40:14 -0500 Subject: [PATCH 2/4] Document managed filesystem boundaries --- SECURITY.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index 16b83ff..95231d0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -19,6 +19,12 @@ only through configured model roots. Treat every model artifact as executable input: use the official Safetensors releases, verify provenance, and do not load untrusted pickle-based checkpoints. +The default server binds to `127.0.0.1`, validates Host headers, and rejects +foreign browser origins for state-changing requests. Audio and metadata routes +normalize paths and require resolved files to remain inside their configured +input, output, metadata, model, upload, or scratch roots before filesystem +access. Upload writes are confined to managed upload or scratch directories. + ## Temporary Upstream PyTorch Exceptions Stable Audio 3 still pins PyTorch 2.7.1 upstream. GERM overrides that constraint From 92f9a3c2aa49837666b87f42ca9875d13de43d7f Mon Sep 17 00:00:00 2001 From: emeisazam <255706292+emeisazam@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:43:00 -0500 Subject: [PATCH 3/4] Isolate download URLs from DOM input --- dashboard/static/app.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/dashboard/static/app.js b/dashboard/static/app.js index bc17e54..1dcf422 100644 --- a/dashboard/static/app.js +++ b/dashboard/static/app.js @@ -20797,7 +20797,7 @@ if ($("loopToggle")) { } if ($("downloadBtn")) { - $("downloadBtn").addEventListener("click", () => { + $("downloadBtn").addEventListener("click", async () => { const path = $("audioPath")?.value; if (!path) return; const href = downloadableOutputUrl(path); @@ -20805,12 +20805,20 @@ if ($("downloadBtn")) { setState("Download Blocked", "warn", "The selected sound does not have a trusted download URL."); return; } - const a = document.createElement("a"); - a.href = href; - a.download = path.split("/").pop() || "download.wav"; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); + try { + const response = await fetch(href, { credentials: "same-origin" }); + if (!response.ok) throw new Error(`Download failed (${response.status})`); + const objectUrl = URL.createObjectURL(await response.blob()); + const a = document.createElement("a"); + a.href = objectUrl; + a.download = safeOutputName(path.split("/").pop() || "download.wav"); + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.setTimeout(() => URL.revokeObjectURL(objectUrl), 0); + } catch (error) { + setState("Download Failed", "bad", error.message); + } }); } From 644270fc9bcb7583eb0a4635f67aad035610cc49 Mon Sep 17 00:00:00 2001 From: emeisazam <255706292+emeisazam@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:50:00 -0500 Subject: [PATCH 4/4] Release GERM 0.3.1 with current stack contracts --- CHANGELOG.md | 7 ++++++- CITATION.cff | 4 ++-- README.md | 14 +++++++------- apps/macos/script/build_and_run.sh | 4 ++-- docs/oida-integration.md | 8 ++++---- pyproject.toml | 6 +++--- server/akousma_store.py | 4 ++-- server/identity.py | 2 +- tests/test_akousma_routes.py | 8 ++++---- tests/test_project_consistency.py | 18 +++++++++--------- uv.lock | 8 ++++---- 11 files changed, 44 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5f7bfe..ec5980a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 0.3.1 — Security and stack alignment - Overrode Stable Audio 3's upstream Torch 2.7.1 constraint with the locally validated Torch and Torchaudio 2.10 pair, removing every fixable advisory @@ -8,6 +8,11 @@ - Added an all-extras dependency audit and a dated security exception for the two remaining upstream PyTorch findings in APIs GERM does not call directly. - Updated Setuptools to 83.0.0 to close its Unicode-normalization sdist issue. +- Removed unsafe DOM-to-HTML and DOM-to-download flows, bounded note parsing, + kept backend exception details out of API payloads, and confined upload + writes to managed roots. +- Updated the embedded Earworm/Akousma package from 0.4.0 to 0.6.0 and aligned + the documented Listening Stack versions with the canonical public releases. ## 0.3.0 — Cosmoaudition, Matter Analysis, and audio reliability diff --git a/CITATION.cff b/CITATION.cff index 6d0d719..6d48c8b 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -5,8 +5,8 @@ authors: - family-names: "Isaza" given-names: "eme" affiliation: "Sonic Field Labs" -version: "0.3.0" -date-released: "2026-07-31" +version: "0.3.1" +date-released: "2026-08-03" license: "MPL-2.0" repository-code: "https://github.com/sonicfieldlabs/germ" abstract: "germ is a local generative microsound environment whose generated sounds retain prompts, parents, mutations, listening metadata, and Earworm-compatible lineage." diff --git a/README.md b/README.md index 0987b78..f87e3b3 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ listened to, and traced through lineage. A listening from Oída can become a prompt or source in GERM; a successful render can become a descendant in Akousmata and return to Oída for another listening. -Current release: `0.3.0`. +Current release: `0.3.1`. GERM is an independent Sonic Field Labs project. It can use Stable Audio 3 providers, but it is not an official Stability AI product. @@ -147,12 +147,12 @@ listening, re-listening, sonic memory, and cultivation. | Component | Version / contract | GERM integration | | --- | --- | --- | -| [OÍDA](https://github.com/sonicfieldlabs/oida) | 0.6.0 / `oida/gateway/v0.2` | Re-listen to generated sound, derive editable prompts, and retain a listening only when requested. | -| [Earworm](https://github.com/sonicfieldlabs/earworm) | 0.4.0 / akousma spec v1.3 | Export generation context and preserve provenance, lineage, location/capture, and covenants. | -| [Akousmata](https://github.com/sonicfieldlabs/akousmata) | 0.4.0 | Import remembered sound, prompt, or lineage; write successful generations back as child akousmata. | -| [AKOÚŌ](https://github.com/sonicfieldlabs/akouo) | `akouo/v0.7` | Keeps listening claims, evidence permissions, apparatus, and covenants consistent across the stack. | -| [Algophony](https://github.com/sonicfieldlabs/algophony) | 0.5.0 | Can evaluate lineage-bearing generation batches without changing GERM's generation state. | -| [ORAM](https://github.com/sonicfieldlabs/oram) | 0.4.0 | Uses the local GERM-compatible generation surface for constrained sound summoning and transformation. | +| [OÍDA](https://github.com/sonicfieldlabs/oida) | 0.9.1 / `oida/gateway/v0.5` | Re-listen to generated sound, derive editable prompts, and retain a listening only when requested. | +| [Earworm](https://github.com/sonicfieldlabs/earworm) | 0.6.0 / akousma spec v1.5 | Export generation context and preserve provenance, lineage, location/capture, and covenants. | +| [Akousmata](https://github.com/sonicfieldlabs/akousmata) | 0.6.0 | Import remembered sound, prompt, or lineage; write successful generations back as child akousmata. | +| [AKOÚŌ](https://github.com/sonicfieldlabs/akouo) | 0.9.0 / `akouo/v0.9` | Keeps listening claims, evidence permissions, apparatus, temporal passes, and covenants consistent across the stack. | +| [Algophony](https://github.com/sonicfieldlabs/algophony) | 0.5.1 | Can evaluate lineage-bearing generation batches without changing GERM's generation state. | +| [ORAM](https://github.com/sonicfieldlabs/oram) | 0.4.1 | Uses the local GERM-compatible generation surface for constrained sound summoning and transformation. | The core handoff is: diff --git a/apps/macos/script/build_and_run.sh b/apps/macos/script/build_and_run.sh index 16708fe..79992e7 100755 --- a/apps/macos/script/build_and_run.sh +++ b/apps/macos/script/build_and_run.sh @@ -6,8 +6,8 @@ APP_NAME="germ" EXECUTABLE_NAME="germ-macos" BUNDLE_ID="org.sonicfield.germ" MIN_SYSTEM_VERSION="13.0" -MARKETING_VERSION="0.3.0" -BUNDLE_VERSION="3" +MARKETING_VERSION="0.3.1" +BUNDLE_VERSION="4" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # Keep the runnable bundle where repository users expect to find apps. The diff --git a/docs/oida-integration.md b/docs/oida-integration.md index 720cc75..8b48793 100644 --- a/docs/oida-integration.md +++ b/docs/oida-integration.md @@ -58,23 +58,23 @@ The shared-store bridge, `/import` handler, structured editable prompt handoff, record/lineage endpoints, prompt and sound handoffs, re-listening action, optional derived-memory write, and the self-contained lineage explorer are **implemented and tested**. -## Current contract: spec v1.3 (Earworm v0.4, 2026-07-14) +## Current contract: spec v1.5 (Earworm v0.6) -The bridge consumes and writes the current Akousma spec v1.3 while retaining the +The bridge consumes and writes the current Akousma spec v1.5 while retaining the v1.0/v1.1 read compatibility required by existing memories: - **Skimmable summaries** — generation records carry `summary: "germ : "`; prompt derivation prefers the record's own summary, then reads both raw (v1.0) and enveloped (v1.1 `{contract, created_at, summary, payload}`) listening entries. germ's own entries are pinned to `germ/v0.1`; raw AKOÚŌ - output is pinned to the current `akouo/v0.7` contract. Existing envelopes and + output is pinned to the current `akouo/v0.9` contract. Existing envelopes and foreign producer blocks are preserved rather than reshaped. - **Kinship** — `POST /akousma/generation` accepts typed `relations` (`variant_of`, `series_with`, …), and re-registering the same audio content auto-links `same_source_as` to the previous holder. The lineage endpoint and explorer expose relations in both directions without confusing them with causal parents. -- **Sovereign listening** — generation registration accepts the optional v1.3 +- **Sovereign listening** — generation registration accepts the optional v1.5 `covenant` identity/honest-absence block and validates it through py-akousma. Sound imports carry that covenant context into germ source metadata, never reconstruct withheld content, and deliberately do not duplicate the diff --git a/pyproject.toml b/pyproject.toml index bbcd454..6e40a48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "germ" -version = "0.3.0" +version = "0.3.1" description = "Local-first modular laboratory for generative microsound, Stable Audio workflows, and lineage-aware cultivation." readme = "README.md" license = "MPL-2.0" requires-python = ">=3.10" authors = [{ name = "Sonic Field Labs" }] dependencies = [ - "akousma>=0.4.0", + "akousma>=0.6.0", "fastapi>=0.139", "httpx>=0.27.0", "pydantic>=2.7.0", @@ -23,7 +23,7 @@ override-dependencies = [ ] [tool.uv.sources] -akousma = { git = "https://github.com/sonicfieldlabs/earworm.git", tag = "v0.4.0", subdirectory = "packages/py-akousma" } +akousma = { git = "https://github.com/sonicfieldlabs/earworm.git", tag = "v0.6.0", subdirectory = "packages/py-akousma" } [project.urls] Homepage = "https://github.com/sonicfieldlabs/germ" diff --git a/server/akousma_store.py b/server/akousma_store.py index dff1b1c..617864e 100644 --- a/server/akousma_store.py +++ b/server/akousma_store.py @@ -93,7 +93,7 @@ def resolve_audio_path(store, record: dict[str, Any]) -> Path | None: ) GERM_CONTRACT = "germ/v0.1" -AKOUO_CONTRACT = "akouo/v0.7" +AKOUO_CONTRACT = "akouo/v0.9" PROMPT_HANDOFF_CONTRACT = "oida-germ.prompt/v0.1" @@ -423,7 +423,7 @@ def record_generation( store=None, ) -> dict[str, Any]: """Write a germ generation into the shared store as a new akousma - (spec v1.3). + (spec v1.5). The audio stays where germ wrote it (referenced by ``file://`` uri + content hash); ``lineage.parent_akousma_ids`` points at the source diff --git a/server/identity.py b/server/identity.py index 39502f8..07b82a0 100644 --- a/server/identity.py +++ b/server/identity.py @@ -3,7 +3,7 @@ PRODUCT_NAME = "germ" PRODUCT_DESCRIPTION = "open-source modular lab for generative microsound" -__version__ = "0.3.0" +__version__ = "0.3.1" LEGACY_ENGINE_NAME = "Germinator" SOUND_MATTER_CONCEPT = "sound_matter" SOUND_MATTER_SCALES = ["micro", "meso", "macro"] diff --git a/tests/test_akousma_routes.py b/tests/test_akousma_routes.py index dc2ffcb..a30bd37 100644 --- a/tests/test_akousma_routes.py +++ b/tests/test_akousma_routes.py @@ -349,7 +349,7 @@ def test_prompt_derivation_prioritizes_dynamic_generative_namespace(client, stor assert body["handoff"]["evidence"][0]["namespace"] == "akouo.generative-listening" -def test_generation_writes_v13_record_with_sa_lineage_bridge(client, seeded, store_path): +def test_generation_writes_v15_record_with_sa_lineage_bridge(client, seeded, store_path): audio_file = _allowed_audio("organism with spaces.wav") organism_metadata = { "sound_id": "organism_007", @@ -399,7 +399,7 @@ def test_generation_writes_v13_record_with_sa_lineage_bridge(client, seeded, sto ) assert response.status_code == 200, response.text record = response.json()["record"] - assert record["schema_version"] == "1.3.0" + assert record["schema_version"] == "1.5.0" assert "%20" in record["audio"]["uri"] # skimmable summary + earworm session link @@ -426,7 +426,7 @@ def test_generation_writes_v13_record_with_sa_lineage_bridge(client, seeded, sto # Current AKOÚŌ output is pinned; existing and foreign producer blocks are # preserved instead of being rewritten by germ. - assert record["listening"]["akouo.memory-lineage"]["contract"] == "akouo/v0.7" + assert record["listening"]["akouo.memory-lineage"]["contract"] == "akouo/v0.9" assert record["listening"]["akouo.describe"]["producer_metadata"] == {"future": True} assert record["listening"]["akouo.describe"]["payload"] == { "main_reading": "pre-enveloped reading" @@ -435,7 +435,7 @@ def test_generation_writes_v13_record_with_sa_lineage_bridge(client, seeded, sto covenant = record["covenant"] assert covenant["id"] == "river-covenant/2" - assert covenant["contract"] == "akouo/v0.7" + assert covenant["contract"] == "akouo/v0.9" assert covenant["withheld"][0]["subject"] == "transcript" assert covenant["future_policy"] == {"retention": "ephemeral"} diff --git a/tests/test_project_consistency.py b/tests/test_project_consistency.py index 2db8e6b..914c5fc 100644 --- a/tests/test_project_consistency.py +++ b/tests/test_project_consistency.py @@ -37,8 +37,8 @@ def test_runtime_entrypoints_share_the_canonical_germ_port() -> None: def test_akousma_dependency_matches_the_current_earworm_store_contract() -> None: project = _read("pyproject.toml") - assert '"akousma>=0.4.0"' in project - assert 'tag = "v0.4.0"' in project + assert '"akousma>=0.6.0"' in project + assert 'tag = "v0.6.0"' in project assert 'subdirectory = "packages/py-akousma"' in project assert 'path = "../earworm' not in project @@ -50,10 +50,10 @@ def test_stable_audio_uses_the_audited_torch_override() -> None: def test_release_version_is_consistent_across_runtime_and_packaging() -> None: - assert __version__ == "0.3.0" - assert 'version = "0.3.0"' in _read("pyproject.toml") - assert 'Current release: `0.3.0`.' in _read("README.md") - assert 'version: "0.3.0"' in _read("CITATION.cff") - assert 'date-released: "2026-07-31"' in _read("CITATION.cff") - assert 'MARKETING_VERSION="0.3.0"' in _read("apps/macos/script/build_and_run.sh") - assert 'BUNDLE_VERSION="3"' in _read("apps/macos/script/build_and_run.sh") + assert __version__ == "0.3.1" + assert 'version = "0.3.1"' in _read("pyproject.toml") + assert 'Current release: `0.3.1`.' in _read("README.md") + assert 'version: "0.3.1"' in _read("CITATION.cff") + assert 'date-released: "2026-08-03"' in _read("CITATION.cff") + assert 'MARKETING_VERSION="0.3.1"' in _read("apps/macos/script/build_and_run.sh") + assert 'BUNDLE_VERSION="4"' in _read("apps/macos/script/build_and_run.sh") diff --git a/uv.lock b/uv.lock index 06a55ca..9107f90 100644 --- a/uv.lock +++ b/uv.lock @@ -30,8 +30,8 @@ overrides = [ [[package]] name = "akousma" -version = "0.4.0" -source = { git = "https://github.com/sonicfieldlabs/earworm.git?subdirectory=packages%2Fpy-akousma&tag=v0.4.0#9e2603e6137fbf7a8acb568919df09eba3f4c314" } +version = "0.6.0" +source = { git = "https://github.com/sonicfieldlabs/earworm.git?subdirectory=packages%2Fpy-akousma&tag=v0.6.0#4aac663ab9a81cdf8d8c2f5c93f4cc84587c1572" } dependencies = [ { name = "jsonschema" }, ] @@ -283,7 +283,7 @@ wheels = [ [[package]] name = "germ" -version = "0.3.0" +version = "0.3.1" source = { virtual = "." } dependencies = [ { name = "akousma" }, @@ -307,7 +307,7 @@ python-provider = [ [package.metadata] requires-dist = [ - { name = "akousma", git = "https://github.com/sonicfieldlabs/earworm.git?subdirectory=packages%2Fpy-akousma&tag=v0.4.0" }, + { name = "akousma", git = "https://github.com/sonicfieldlabs/earworm.git?subdirectory=packages%2Fpy-akousma&tag=v0.6.0" }, { name = "fastapi", specifier = ">=0.139" }, { name = "httpx", specifier = ">=0.27.0" }, { name = "httpx2", marker = "extra == 'dev'", specifier = ">=2.7" },