build(agent): new-agents-3#dd492b iteration
This commit is contained in:
parent
6131a9b1bb
commit
f9e2daaee2
|
|
@ -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
|
||||||
|
|
@ -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.
|
||||||
25
README.md
25
README.md
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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)
|
||||||
Loading…
Reference in New Issue