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" },