Import core (art-dag) as core/

This commit is contained in:
giles
2026-02-24 23:09:39 +00:00
80 changed files with 25711 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""
Compute content hash of a git repository.
Hashes all tracked files (respects .gitignore) in sorted order.
"""
import hashlib
import subprocess
import sys
from pathlib import Path
def repo_hash(repo_path: Path) -> str:
"""
Compute SHA3-256 hash of all tracked files in a repo.
Uses git ls-files to respect .gitignore.
Files are hashed in sorted order for determinism.
Each file contributes: relative_path + file_contents
"""
# Get list of tracked files
result = subprocess.run(
["git", "ls-files"],
cwd=repo_path,
capture_output=True,
text=True,
check=True,
)
files = sorted(result.stdout.strip().split("\n"))
hasher = hashlib.sha3_256()
for rel_path in files:
if not rel_path:
continue
file_path = repo_path / rel_path
if not file_path.is_file():
continue
# Include path in hash
hasher.update(rel_path.encode())
# Include contents
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
hasher.update(chunk)
return hasher.hexdigest()
def main():
if len(sys.argv) > 1:
repo_path = Path(sys.argv[1])
else:
repo_path = Path.cwd()
h = repo_hash(repo_path)
print(f"Repository: {repo_path}")
print(f"Hash: {h}")
return h
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,82 @@
#!/bin/bash
# Install ffglitch for datamosh effects
# Usage: ./install-ffglitch.sh [install_dir]
set -e
FFGLITCH_VERSION="0.10.2"
INSTALL_DIR="${1:-/usr/local/bin}"
# Detect architecture
ARCH=$(uname -m)
case "$ARCH" in
x86_64)
URL="https://ffglitch.org/pub/bin/linux64/ffglitch-${FFGLITCH_VERSION}-linux-x86_64.zip"
ARCHIVE="ffglitch.zip"
;;
aarch64)
URL="https://ffglitch.org/pub/bin/linux-aarch64/ffglitch-${FFGLITCH_VERSION}-linux-aarch64.7z"
ARCHIVE="ffglitch.7z"
;;
*)
echo "Unsupported architecture: $ARCH"
exit 1
;;
esac
echo "Installing ffglitch ${FFGLITCH_VERSION} for ${ARCH}..."
# Create temp directory
TMPDIR=$(mktemp -d)
cd "$TMPDIR"
# Download
echo "Downloading from ${URL}..."
curl -L -o "$ARCHIVE" "$URL"
# Extract
echo "Extracting..."
if [[ "$ARCHIVE" == *.zip ]]; then
unzip -q "$ARCHIVE"
elif [[ "$ARCHIVE" == *.7z ]]; then
# Requires p7zip
if ! command -v 7z &> /dev/null; then
echo "7z not found. Install with: apt install p7zip-full"
exit 1
fi
7z x "$ARCHIVE" > /dev/null
fi
# Find and install binaries
echo "Installing to ${INSTALL_DIR}..."
find . -name "ffgac" -o -name "ffedit" | while read bin; do
chmod +x "$bin"
if [ -w "$INSTALL_DIR" ]; then
cp "$bin" "$INSTALL_DIR/"
else
sudo cp "$bin" "$INSTALL_DIR/"
fi
echo " Installed: $(basename $bin)"
done
# Cleanup
cd /
rm -rf "$TMPDIR"
# Verify
echo ""
echo "Verifying installation..."
if command -v ffgac &> /dev/null; then
echo "ffgac: $(which ffgac)"
else
echo "Warning: ffgac not in PATH. Add ${INSTALL_DIR} to PATH."
fi
if command -v ffedit &> /dev/null; then
echo "ffedit: $(which ffedit)"
else
echo "Warning: ffedit not in PATH. Add ${INSTALL_DIR} to PATH."
fi
echo ""
echo "Done! ffglitch installed."

View File

@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""
Register the identity effect owned by giles.
"""
import hashlib
from pathlib import Path
import sys
# Add parent to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from artdag.activitypub.ownership import OwnershipManager
def folder_hash(folder: Path) -> str:
"""
Compute SHA3-256 hash of an entire folder.
Hashes all files in sorted order for deterministic results.
Each file contributes: relative_path + file_contents
"""
hasher = hashlib.sha3_256()
# Get all files sorted by relative path
files = sorted(folder.rglob("*"))
for file_path in files:
if file_path.is_file():
# Include relative path in hash for structure
rel_path = file_path.relative_to(folder)
hasher.update(str(rel_path).encode())
# Include file contents
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
hasher.update(chunk)
return hasher.hexdigest()
def main():
# Use .cache as the ownership data directory
base_dir = Path(__file__).parent.parent / ".cache" / "ownership"
manager = OwnershipManager(base_dir)
# Create or get giles actor
actor = manager.get_actor("giles")
if not actor:
actor = manager.create_actor("giles", "Giles Bradshaw")
print(f"Created actor: {actor.handle}")
else:
print(f"Using existing actor: {actor.handle}")
# Register the identity effect folder
effect_path = Path(__file__).parent.parent / "effects" / "identity"
cid = folder_hash(effect_path)
asset, activity = manager.register_asset(
actor=actor,
name="effect:identity",
cid=cid,
local_path=effect_path,
tags=["effect", "primitive", "identity"],
metadata={
"type": "effect",
"description": "The identity effect - returns input unchanged",
"signature": "identity(input) → input",
},
)
print(f"\nRegistered: {asset.name}")
print(f" Hash: {asset.cid}")
print(f" Path: {asset.local_path}")
print(f" Activity: {activity.activity_id}")
print(f" Owner: {actor.handle}")
# Verify ownership
verified = manager.verify_ownership(asset.name, actor)
print(f" Ownership verified: {verified}")
if __name__ == "__main__":
main()

120
core/scripts/setup_actor.py Normal file
View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""
Set up actor with keypair stored securely.
Private key: ~/.artdag/keys/{username}.pem
Public key: exported for registry
"""
import json
import os
import sys
from datetime import datetime, timezone
from pathlib import Path
# Add artdag to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
def create_keypair():
"""Generate RSA-2048 keypair."""
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend(),
)
return private_key
def save_private_key(private_key, path: Path):
"""Save private key to PEM file."""
pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(pem)
os.chmod(path, 0o600) # Owner read/write only
return pem.decode()
def get_public_key_pem(private_key) -> str:
"""Extract public key as PEM string."""
public_key = private_key.public_key()
pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
return pem.decode()
def create_actor_json(username: str, display_name: str, public_key_pem: str, domain: str = "artdag.rose-ash.com"):
"""Create ActivityPub actor JSON."""
return {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1"
],
"type": "Person",
"id": f"https://{domain}/users/{username}",
"preferredUsername": username,
"name": display_name,
"inbox": f"https://{domain}/users/{username}/inbox",
"outbox": f"https://{domain}/users/{username}/outbox",
"publicKey": {
"id": f"https://{domain}/users/{username}#main-key",
"owner": f"https://{domain}/users/{username}",
"publicKeyPem": public_key_pem
}
}
def main():
username = "giles"
display_name = "Giles Bradshaw"
domain = "artdag.rose-ash.com"
keys_dir = Path.home() / ".artdag" / "keys"
private_key_path = keys_dir / f"{username}.pem"
# Check if key already exists
if private_key_path.exists():
print(f"Private key already exists: {private_key_path}")
print("Delete it first if you want to regenerate.")
sys.exit(1)
# Create new keypair
print(f"Creating new keypair for @{username}@{domain}...")
private_key = create_keypair()
# Save private key
save_private_key(private_key, private_key_path)
print(f"Private key saved: {private_key_path}")
print(f" Mode: 600 (owner read/write only)")
print(f" BACK THIS UP!")
# Get public key
public_key_pem = get_public_key_pem(private_key)
# Create actor JSON
actor = create_actor_json(username, display_name, public_key_pem, domain)
# Output actor JSON
actor_json = json.dumps(actor, indent=2)
print(f"\nActor JSON (for registry/actors/{username}.json):")
print(actor_json)
# Save to registry
registry_path = Path.home() / "artdag-registry" / "actors" / f"{username}.json"
registry_path.parent.mkdir(parents=True, exist_ok=True)
registry_path.write_text(actor_json)
print(f"\nSaved to: {registry_path}")
if __name__ == "__main__":
main()

143
core/scripts/sign_assets.py Normal file
View File

@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""
Sign assets in the registry with giles's private key.
Creates ActivityPub Create activities with RSA signatures.
"""
import base64
import hashlib
import json
import sys
import uuid
from datetime import datetime, timezone
from pathlib import Path
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
def load_private_key(path: Path):
"""Load private key from PEM file."""
pem_data = path.read_bytes()
return serialization.load_pem_private_key(pem_data, password=None, backend=default_backend())
def sign_data(private_key, data: str) -> str:
"""Sign data with RSA private key, return base64 signature."""
signature = private_key.sign(
data.encode(),
padding.PKCS1v15(),
hashes.SHA256(),
)
return base64.b64encode(signature).decode()
def create_activity(actor_id: str, asset_name: str, cid: str, asset_type: str, domain: str = "artdag.rose-ash.com"):
"""Create a Create activity for an asset."""
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
return {
"activity_id": str(uuid.uuid4()),
"activity_type": "Create",
"actor_id": actor_id,
"object_data": {
"type": asset_type_to_ap(asset_type),
"name": asset_name,
"id": f"https://{domain}/objects/{cid}",
"contentHash": {
"algorithm": "sha3-256",
"value": cid
},
"attributedTo": actor_id
},
"published": now,
}
def asset_type_to_ap(asset_type: str) -> str:
"""Convert asset type to ActivityPub type."""
type_map = {
"image": "Image",
"video": "Video",
"audio": "Audio",
"effect": "Application",
"infrastructure": "Application",
}
return type_map.get(asset_type, "Document")
def sign_activity(activity: dict, private_key, actor_id: str, domain: str = "artdag.rose-ash.com") -> dict:
"""Add signature to activity."""
# Create canonical string to sign
to_sign = json.dumps(activity["object_data"], sort_keys=True, separators=(",", ":"))
signature_value = sign_data(private_key, to_sign)
activity["signature"] = {
"type": "RsaSignature2017",
"creator": f"{actor_id}#main-key",
"created": activity["published"],
"signatureValue": signature_value
}
return activity
def main():
username = "giles"
domain = "artdag.rose-ash.com"
actor_id = f"https://{domain}/users/{username}"
# Load private key
private_key_path = Path.home() / ".artdag" / "keys" / f"{username}.pem"
if not private_key_path.exists():
print(f"Private key not found: {private_key_path}")
print("Run setup_actor.py first.")
sys.exit(1)
private_key = load_private_key(private_key_path)
print(f"Loaded private key: {private_key_path}")
# Load registry
registry_path = Path.home() / "artdag-registry" / "registry.json"
with open(registry_path) as f:
registry = json.load(f)
# Create signed activities for each asset
activities = []
for asset_name, asset_data in registry["assets"].items():
print(f"\nSigning: {asset_name}")
print(f" Hash: {asset_data['cid'][:16]}...")
activity = create_activity(
actor_id=actor_id,
asset_name=asset_name,
cid=asset_data["cid"],
asset_type=asset_data["asset_type"],
domain=domain,
)
signed_activity = sign_activity(activity, private_key, actor_id, domain)
activities.append(signed_activity)
print(f" Activity ID: {signed_activity['activity_id']}")
print(f" Signature: {signed_activity['signature']['signatureValue'][:32]}...")
# Save activities
activities_path = Path.home() / "artdag-registry" / "activities.json"
activities_data = {
"version": "1.0",
"activities": activities
}
with open(activities_path, "w") as f:
json.dump(activities_data, f, indent=2)
print(f"\nSaved {len(activities)} signed activities to: {activities_path}")
if __name__ == "__main__":
main()