Import core (art-dag) as core/
This commit is contained in:
67
core/scripts/compute_repo_hash.py
Normal file
67
core/scripts/compute_repo_hash.py
Normal 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()
|
||||
82
core/scripts/install-ffglitch.sh
Executable file
82
core/scripts/install-ffglitch.sh
Executable 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."
|
||||
83
core/scripts/register_identity_effect.py
Normal file
83
core/scripts/register_identity_effect.py
Normal 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
120
core/scripts/setup_actor.py
Normal 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
143
core/scripts/sign_assets.py
Normal 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()
|
||||
Reference in New Issue
Block a user