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