From 90c131b2153598903dab9676c0a70bd8d599ebcf Mon Sep 17 00:00:00 2001 From: agent-ed374b2a16b664d2 Date: Wed, 15 Apr 2026 21:41:49 +0200 Subject: [PATCH] build(agent): molt-x#ed374b iteration --- .gitignore | 21 +++ AGENTS.md | 26 +++ README.md | 22 ++- promptledger_verifiable_provenance_and_l.py | 1 + .../__init__.py | 159 ++++++++++++++++++ pyproject.toml | 17 ++ test.sh | 13 ++ tests/test_provenance.py | 57 +++++++ 8 files changed, 315 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 promptledger_verifiable_provenance_and_l.py create mode 100644 promptledger_verifiable_provenance_and_l/__init__.py create mode 100644 pyproject.toml create mode 100644 test.sh create mode 100644 tests/test_provenance.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd5590b --- /dev/null +++ b/.gitignore @@ -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 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..60c4e3d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,26 @@ +# AGENTS + +Architecture overview for PromptLedger MVP + +- Language/Stack: Python 3.8+ with standard library; packaging metadata via pyproject.toml +- Core modules: + - promptledger_verifiable_provenance_and_l: LocalProvenanceBlock, MerkleAuditLog, DeltaSync, Adapters +- Data model: + - LocalProvenanceBlock records a creative step with author, tool, action, metadata, license, timestamp, and a crypto-like signature +- Provenance storage: + - MerkleAuditLog keeps blocks and a Merkle root for tamper-evidence +- Synchronization: DeltaSync provides delta-exports/imports of provenance blocks for offline-first operation +- Adapters: + - BlenderAdapter, FigmaAdapter emit provenance blocks for their respective tools +- Governance: Draft governance scaffolding in code comments and API design; real RBAC and policy templates to extend later + +- Testing: + - pytest-based tests under tests/ +- Build/publish: python3 -m build; test.sh will run tests and build, per repository requirements + +How to run locally: +- Install dependencies (only standard library for MVP): no extra deps required +- Run unit tests: pytest +- Build package: python3 -m build + +Note: This is a minimal MVP to bootstrap the architecture. Future work will introduce real cryptographic PKI, policy engines, delta-sync optimizations, and cross-tool adapters. diff --git a/README.md b/README.md index 8a2e85b..608474d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,23 @@ # promptledger-verifiable-provenance-and-l -A cross-tool, offline-first ledger that records prompts, model versions, seeds, parameter configurations, sources of assets, licensing terms, and outputs in a tamper-evident, auditable log. It integrates a schema registry for prompt contracts and a d \ No newline at end of file +This project provides a minimal MVP for a cross-tool, offline-first provenance ledger intended for generative AI creative workflows. It demonstrates a tamper-evident log of prompts, model configurations, assets, licenses, and outputs, with a simple Merkle-based audit trail and delta-sync scaffolding. Adapters map Blender and Figma provenance into a canonical model. + +How to run locally +- Prereqs: Python 3.8+, pip, and build tooling (setuptools, wheel) +- Build metadata: pyproject.toml (name: promptledger-verifiable-provenance-and-l) +- Run tests: ./test.sh (requires pytest; should pass without extra dependencies) +- Build package: ./test.sh (will run python3 -m build) + +Project structure (核心) +- promptledger_verifiable_provenance_and_l/: Python package containing: + - LocalProvenanceBlock: a single provenance step with author, tool, action, metadata, and license + - MerkleAuditLog: a simple, tamper-evident log built on a Merkle tree of blocks + - DeltaSync: lightweight export/import of provenance deltas for offline-first operation + - BlenderAdapter, FigmaAdapter: sample adapters emitting provenance blocks + +Notes +- This is a minimal MVP intended to bootstrap the architecture. Real-world deployment would require robust crypto (PKI), policy engines, RBAC, and robust delta-sync guarantees with privacy protections. + +License: MIT + +READY_TO_PUBLISH marker is created when the repo is ready to publish. diff --git a/promptledger_verifiable_provenance_and_l.py b/promptledger_verifiable_provenance_and_l.py new file mode 100644 index 0000000..f36f1e6 --- /dev/null +++ b/promptledger_verifiable_provenance_and_l.py @@ -0,0 +1 @@ +from promptledger_verifiable_provenance_and_l import * # re-export for easier imports in tests diff --git a/promptledger_verifiable_provenance_and_l/__init__.py b/promptledger_verifiable_provenance_and_l/__init__.py new file mode 100644 index 0000000..6006840 --- /dev/null +++ b/promptledger_verifiable_provenance_and_l/__init__.py @@ -0,0 +1,159 @@ +import json +import hashlib +import time +from typing import List, Dict, Any +import hmac + +# Simple, self-contained MVP: local provenance ledger with a Merkle audit log +HASH_ALGO = hashlib.sha256 +SIGNING_KEY = b"demo-secret-key" # In a real product, use a proper KMS/PKI; kept here for MVP. + + +def _serialize(obj: Any) -> bytes: + return json.dumps(obj, sort_keys=True, separators=(",", ":")).encode("utf-8") + + +def _hash_block(block: Dict[str, Any]) -> str: + data = _serialize(block) + return HASH_ALGO(data).hexdigest() + + +def _sign(data: bytes) -> str: + # Simple HMAC-based signature for MVP (not a full cryptographic signature scheme) + return hmac.new(SIGNING_KEY, data, HASH_ALGO).hexdigest() + + +class LocalProvenanceBlock: + def __init__(self, author: str, tool: str, action: str, metadata: Dict[str, Any], license_: str): + self.author = author + self.tool = tool + self.action = action # e.g., "create", "modify" + self.metadata = metadata + self.license = license_ + self.timestamp = time.time() + self.block_id = hashlib.sha256(f"{author}:{tool}:{action}:{self.timestamp}".encode("utf-8")).hexdigest() + self.signature = None # to be filled by ledger when appended + + def to_dict(self) -> Dict[str, Any]: + return { + "block_id": self.block_id, + "author": self.author, + "tool": self.tool, + "action": self.action, + "metadata": self.metadata, + "license": self.license, + "timestamp": self.timestamp, + "signature": self.signature, + } + + def __repr__(self) -> str: + return f"LocalProvenanceBlock(id={self.block_id})" + + +class MerkleAuditLog: + def __init__(self): + self.blocks: List[Dict[str, Any]] = [] + self.merkle_root: str = "" + + def append(self, block: LocalProvenanceBlock) -> None: + blob = block.to_dict() + blob["signature"] = block.signature + self.blocks.append(blob) + # Recompute signature and Merkle root for simplicity on each append + block_data = self._compute_hash_chain() + self.merkle_root = block_data[0] if isinstance(block_data, (list, tuple)) else block_data + + def _compute_hash_chain(self) -> List[str]: + leaves = [HASH_ALGO(_serialize(b)).hexdigest() for b in self.blocks] + if not leaves: + return [""] + # Simple binary Merkle; pad with last leaf if needed + current = leaves + while len(current) > 1: + next_level = [] + for i in range(0, len(current), 2): + a = current[i] + b = current[i + 1] if i + 1 < len(current) else a + next_level.append(HASH_ALGO((a + b).encode("utf-8")).hexdigest()) + current = next_level + return current + + def get_root(self) -> str: + return self.merkle_root + + def to_dict(self) -> Dict[str, Any]: + return { + "root": self.merkle_root, + "count": len(self.blocks), + "blocks": self.blocks, + } + + +class DeltaSync: + def __init__(self, log: MerkleAuditLog): + self.log = log + self.synced_index = 0 + + def create_delta(self) -> Dict[str, Any]: + # Return the delta since last sync + delta_blocks = self.log.blocks[self.synced_index :] + delta = { + "start_index": self.synced_index, + "count": len(delta_blocks), + "blocks": delta_blocks, + "root": self.log.get_root(), + } + self.synced_index = len(self.log.blocks) + return delta + + def apply_delta(self, delta: Dict[str, Any]) -> None: + # For MVP: simply set internal state to the provided delta's root and blocks + # In real use, this would verify delta provenance and apply safely + for b in delta.get("blocks", []): + if b not in self.log.blocks: + self.log.blocks.append(b) + self.log.merkle_root = delta.get("root", self.log.get_root()) + + def is_in_sync_with(self, other_root: str) -> bool: + return self.log.get_root() == other_root + + +class Adapter: + def __init__(self, author: str, license_: str = "CC-BY-4.0"): + self.author = author + self.license = license_ + + def emit(self) -> LocalProvenanceBlock: + raise NotImplementedError + + +class BlenderAdapter(Adapter): + def emit(self) -> LocalProvenanceBlock: + block = LocalProvenanceBlock( + author=self.author, + tool="Blender", + action="create_asset", + metadata={"asset_type": "3d_model", "scene": "SampleScene"}, + license_=self.license, + ) + return block + + +class FigmaAdapter(Adapter): + def emit(self) -> LocalProvenanceBlock: + block = LocalProvenanceBlock( + author=self.author, + tool="Figma", + action="update_design", + metadata={"frame": "HeroSection", "pages": ["Landing", "Docs"]}, + license_=self.license, + ) + return block + + +def attach_signature(block: LocalProvenanceBlock) -> None: + data = block.to_dict() + # Sign the block serialization excluding signature itself + data.pop("signature", None) + sig = _sign(_serialize(data)) + block.signature = sig diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..31f8dca --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[build-system] +requires = ["setuptools>=40.6.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "promptledger-verifiable-provenance-and-l" +version = "0.1.0" +description = "MVP: offline-first provenance ledger with adapters for Blender and Figma" +requires-python = ">=3.8" +readme = "README.md" +license = {text = "MIT"} +authors = [ { name = "OpenCode Assistant" } ] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Topic :: Software Development :: Libraries" +] diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..1948903 --- /dev/null +++ b/test.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "Setting up package in editable mode..." +python3 -m pip install -e . + +echo "Running tests..." +pytest -q + +echo "Building package..." +python3 -m build + +echo "All tests passed and package built." diff --git a/tests/test_provenance.py b/tests/test_provenance.py new file mode 100644 index 0000000..91b2916 --- /dev/null +++ b/tests/test_provenance.py @@ -0,0 +1,57 @@ +import json +from promptledger_verifiable_provenance_and_l import ( + LocalProvenanceBlock, + MerkleAuditLog, + DeltaSync, + BlenderAdapter, + FigmaAdapter, + attach_signature, +) + + +def test_merkle_audit_log_basic(): + log = MerkleAuditLog() + blk1 = LocalProvenanceBlock("alice", "Blender", "create_asset", {"asset_type": "mesh"}, "CC-BY-4.0") + attach_signature(blk1) + log.append(blk1) + + blk2 = LocalProvenanceBlock("bob", "Figma", "update_design", {"frame": "Intro"}, "MIT") + attach_signature(blk2) + log.append(blk2) + + root = log.get_root() + assert isinstance(root, str) and len(root) > 0 + # Recompute by creating a new log with same blocks and ensure root matches + log2 = MerkleAuditLog() + log2.append(blk1) + log2.append(blk2) + assert log2.get_root() == root + + +def test_delta_sync_roundtrip(): + log = MerkleAuditLog() + delta_sync = DeltaSync(log) + + a = LocalProvenanceBlock("carol", "Blender", "create_asset", {"asset_type": "texture"}, "CC0") + attach_signature(a) + log.append(a) + + delta1 = delta_sync.create_delta() + # Simulate recipient with empty log getting delta + recipient_log = MerkleAuditLog() + recipient_sync = DeltaSync(recipient_log) + recipient_sync.apply_delta(delta1) + assert recipient_log.get_root() == log.get_root() + + +def test_adapters_emit_and_sign(): + blender = BlenderAdapter("dave") + figma = FigmaAdapter("eve") + + b = blender.emit() + attach_signature(b) + f = figma.emit() + attach_signature(f) + + assert b.tool == "Blender" and b.author == "dave" + assert f.tool == "Figma" and f.author == "eve"