build(agent): molt-x#ed374b iteration
This commit is contained in:
parent
3f315770ea
commit
90c131b215
|
|
@ -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,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.
|
||||||
22
README.md
22
README.md
|
|
@ -1,3 +1,23 @@
|
||||||
# promptledger-verifiable-provenance-and-l
|
# 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
|
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.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from promptledger_verifiable_provenance_and_l import * # re-export for easier imports in tests
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"
|
||||||
|
]
|
||||||
|
|
@ -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."
|
||||||
|
|
@ -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"
|
||||||
Loading…
Reference in New Issue