build(agent): new-agents-3#dd492b iteration

This commit is contained in:
agent-dd492b85242a98c5 2026-04-24 17:49:00 +02:00
parent 6131a9b1bb
commit f9e2daaee2
9 changed files with 326 additions and 2 deletions

21
.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
node_modules/
.npmrc
.env
.env.*
__tests__/
coverage/
.nyc_output/
dist/
build/
.cache/
*.log
.DS_Store
tmp/
.tmp/
__pycache__/
*.pyc
.venv/
venv/
*.egg-info/
.pytest_cache/
READY_TO_PUBLISH

31
AGENTS.md Normal file
View File

@ -0,0 +1,31 @@
Architecture and Contribution Guide for AI Agents
Purpose
This repository contains Phase0 primitives for SoundGraph Studio: content-addressed audio modules, patch graphs with Merkle roots, and provenance events. It's intentionally small and well-tested so subsequent agents can expand to delta-sync, DAW adapters, and a governance ledger.
Tech stack
- Language: Python 3.8+
- Crypto: cryptography (ed25519 signatures)
- Testing: pytest
- Packaging: setuptools/pyproject.toml
Repository layout
- pyproject.toml - build metadata and dependencies
- setup.cfg - setuptools configuration
- README.md - user-facing documentation
- AGENTS.md - this file
- test.sh - run tests and build
- idea44_soundgraph_studio_verifiable/ - Python package code
- tests/ - pytest tests
Agent rules
1. Always run the test suite after changes: `./test.sh`.
2. Avoid making broad refactors unless requested; prefer small, well-scoped patches.
3. When adding dependencies, update pyproject.toml and setup.cfg accordingly.
4. Do NOT create `READY_TO_PUBLISH` unless the repository fully implements the original idea and all tests pass (the orchestrator enforces this).
5. If you need to run commands, use the provided tooling and keep changes minimal.
Development notes
- The package name must remain `idea44_soundgraph_studio_verifiable`.
- Packaging must build successfully with `python3 -m build`.
- Tests must be present and passing; any added functionality should include at least one test.

View File

@ -1,3 +1,24 @@
# idea44-soundgraph-studio-verifiable
# SoundGraph Studio — Phase0 primitives
Source logic for Idea #44
This repository contains Phase0 primitives for SoundGraph Studio: a content-addressed representation of AudioModule objects, an AudioPatch structure with a Merkle-based root for provable inclusion, and simple provenance events with cryptographic signatures.
What is included
- A small Python package `idea44_soundgraph_studio_verifiable` implementing:
- AudioModule: content-addressed modules (SHA-256 over canonical JSON)
- AudioPatch: nodes referencing module CIDs and edges; Merkle root over nodes+edges
- ProvenanceEvent: lightweight signed events (ed25519) for tamper-evident audit trails
- Tests (pytest) that validate CID reproducibility, Merkle inclusion proofs, and signature verification
- Packaging (pyproject.toml + setup.cfg) and a `test.sh` that runs the test suite and verifies build
How to run tests
1. Make sure you have Python 3.8+ and `python3 -m pip` available.
2. Run:
```bash
./test.sh
```
This will install the package in editable mode, run pytest, and run `python3 -m build` to verify packaging.
Notes
- This is intentionally focused on Phase0 local graph primitives — a solid foundation for future delta-sync, adapters, and governance ledger work.

View File

@ -0,0 +1,9 @@
"""idea44_soundgraph_studio_verifiable
Phase0 primitives: content-addressed AudioModule, AudioPatch with Merkle root,
and ProvenanceEvent signing/verification.
"""
from .core import AudioModule, AudioPatch, ProvenanceEvent
__all__ = ["AudioModule", "AudioPatch", "ProvenanceEvent"]

View File

@ -0,0 +1,169 @@
"""Core primitives: AudioModule, AudioPatch, ProvenanceEvent
Small, well-scoped implementations suitable for Phase0 tests.
"""
from __future__ import annotations
import json
import hashlib
import time
import uuid
from dataclasses import dataclass, field, asdict
from typing import List, Dict, Tuple
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
from cryptography.hazmat.primitives import serialization
def canonical_json(data) -> bytes:
# deterministic JSON encoding
return json.dumps(data, separators=(",", ":"), sort_keys=True, ensure_ascii=False).encode("utf-8")
def sha256(b: bytes) -> str:
return hashlib.sha256(b).hexdigest()
@dataclass
class AudioModule:
id: str
type: str
params: Dict
version: str = "0.1"
license: str = ""
def to_dict(self) -> Dict:
return asdict(self)
def cid(self) -> str:
"""Content ID: SHA-256 over canonical JSON of the module."""
data = self.to_dict()
return sha256(canonical_json(data))
@staticmethod
def make(type: str, params: Dict, version: str = "0.1", license: str = "") -> "AudioModule":
return AudioModule(id=str(uuid.uuid4()), type=type, params=params, version=version, license=license)
def merkle_tree_root(leaves: List[bytes]) -> str:
"""Simple Merkle tree root (binary) over raw leaf bytes. Returns hex digest."""
if not leaves:
return sha256(b"")
level = [hashlib.sha256(l).digest() for l in leaves]
while len(level) > 1:
next_level = []
for i in range(0, len(level), 2):
left = level[i]
right = level[i + 1] if i + 1 < len(level) else left
next_level.append(hashlib.sha256(left + right).digest())
level = next_level
return level[0].hex()
def merkle_proof(leaves: List[bytes], index: int) -> List[Tuple[str, str]]:
"""Return a list of (sibling_hash(hex), direction) where direction is 'L' or 'R'."""
if index < 0 or index >= len(leaves):
raise IndexError("leaf index")
# compute tree levels as bytes
levels = [ [hashlib.sha256(l).digest() for l in leaves] ]
while len(levels[-1]) > 1:
cur = levels[-1]
nxt = []
for i in range(0, len(cur), 2):
left = cur[i]
right = cur[i+1] if i+1 < len(cur) else left
nxt.append(hashlib.sha256(left + right).digest())
levels.append(nxt)
proof = []
idx = index
for level in levels[:-1]:
sibling_idx = idx ^ 1
sibling = level[sibling_idx] if sibling_idx < len(level) else level[idx]
direction = 'L' if sibling_idx < idx else 'R'
proof.append((sibling.hex(), direction))
idx = idx // 2
return proof
def verify_merkle_proof(leaf: bytes, proof: List[Tuple[str, str]], root_hex: str) -> bool:
cur = hashlib.sha256(leaf).digest()
for sibling_hex, direction in proof:
sibling = bytes.fromhex(sibling_hex)
if direction == 'L':
cur = hashlib.sha256(sibling + cur).digest()
else:
cur = hashlib.sha256(cur + sibling).digest()
return cur.hex() == root_hex
@dataclass
class AudioPatch:
nodes: List[Dict] = field(default_factory=list) # each node: {id, module_cid, params}
edges: List[Tuple[str, str]] = field(default_factory=list) # list of (from_node_id, to_node_id)
metadata: Dict = field(default_factory=dict)
def to_dict(self) -> Dict:
return {
"nodes": self.nodes,
"edges": self.edges,
"metadata": self.metadata,
}
def merkle_root(self) -> str:
# leaves are canonical JSON of each node (sorted by node id) and each edge
node_items = sorted(self.nodes, key=lambda n: n.get("id"))
node_leaves = [canonical_json(n) for n in node_items]
edge_items = sorted([{"from": f, "to": t} for f,t in self.edges], key=lambda e: (e['from'], e['to']))
edge_leaves = [canonical_json(e) for e in edge_items]
leaves = node_leaves + edge_leaves
return merkle_tree_root(leaves)
def inclusion_proof_for_module_cid(self, module_cid: str) -> Tuple[List[Tuple[str,str]], bytes]:
# find the node(s) that reference this module_cid and return proof for first occurrence
node_items = sorted(self.nodes, key=lambda n: n.get("id"))
node_leaves = [canonical_json(n) for n in node_items]
edge_items = sorted([{"from": f, "to": t} for f,t in self.edges], key=lambda e: (e['from'], e['to']))
edge_leaves = [canonical_json(e) for e in edge_items]
leaves = node_leaves + edge_leaves
for idx, n in enumerate(node_items):
if n.get("module_cid") == module_cid:
proof = merkle_proof(leaves, idx)
return proof, node_leaves[idx]
raise KeyError("module_cid not referenced in patch")
@dataclass
class ProvenanceEvent:
patch_id: str
actor: str
timestamp: float
op_hash: str
signature: bytes = b""
def to_dict(self) -> Dict:
return {"patch_id": self.patch_id, "actor": self.actor, "timestamp": self.timestamp, "op_hash": self.op_hash}
def sign(self, private_key: Ed25519PrivateKey) -> None:
self.signature = private_key.sign(canonical_json(self.to_dict()))
def verify(self, public_key: Ed25519PublicKey) -> bool:
try:
public_key.verify(self.signature, canonical_json(self.to_dict()))
return True
except Exception:
return False
def generate_ed25519_keypair() -> Tuple[Ed25519PrivateKey, Ed25519PublicKey]:
sk = Ed25519PrivateKey.generate()
pk = sk.public_key()
return sk, pk

12
pyproject.toml Normal file
View File

@ -0,0 +1,12 @@
[build-system]
requires = ["setuptools>=61.0","wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "idea44_soundgraph_studio_verifiable"
version = "0.0.1"
description = "SoundGraph Studio primitives: Merkle-DAG patches, content-addressed modules, and provenance events (MVP Phase0)"
readme = "README.md"
authors = [ { name = "OpenCode" } ]
requires-python = ">=3.8"
dependencies = ["cryptography>=39.0.0"]

13
setup.cfg Normal file
View File

@ -0,0 +1,13 @@
[metadata]
name = idea44_soundgraph_studio_verifiable
version = 0.0.1
description = SoundGraph Studio primitives: Merkle-DAG patches, content-addressed modules, and provenance events (MVP Phase0)
long_description = file: README.md
long_description_content_type = text/markdown
author = OpenCode
[options]
packages = find:
install_requires =
cryptography>=39.0.0
python_requires = >=3.8

13
test.sh Normal file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
echo "Installing package in editable mode..."
python3 -m pip install -e .
echo "Running tests..."
pytest -q
echo "Building package..."
python3 -m build
echo "All done"

35
tests/test_core.py Normal file
View File

@ -0,0 +1,35 @@
import json
from idea44_soundgraph_studio_verifiable.core import AudioModule, AudioPatch, generate_ed25519_keypair, ProvenanceEvent
def test_audio_module_cid_is_stable():
m = AudioModule.make("oscillator", {"freq": 440, "wave": "sine"}, version="0.1")
cid1 = m.cid()
# recompute from dict to ensure canonicalization is stable
cid2 = AudioModule(id=m.id, type=m.type, params=m.params, version=m.version, license=m.license).cid()
assert cid1 == cid2
def test_patch_merkle_and_inclusion_proof():
m1 = AudioModule.make("oscillator", {"freq": 220})
m2 = AudioModule.make("filter", {"type": "lowpass", "cutoff": 1000})
node1 = {"id": "n1", "module_cid": m1.cid(), "params": {}}
node2 = {"id": "n2", "module_cid": m2.cid(), "params": {}}
patch = AudioPatch(nodes=[node1, node2], edges=[("n1","n2")])
root = patch.merkle_root()
assert isinstance(root, str) and len(root) == 64
proof, leaf = patch.inclusion_proof_for_module_cid(m1.cid())
# verify proof using the exported verification helper
from idea44_soundgraph_studio_verifiable.core import verify_merkle_proof
assert verify_merkle_proof(leaf, proof, root)
def test_provenance_event_sign_and_verify():
sk, pk = generate_ed25519_keypair()
ev = ProvenanceEvent(patch_id="patch-1", actor="alice", timestamp=123456.0, op_hash="deadbeef")
ev.sign(sk)
assert ev.verify(pk)