- Change file mode from 644 to 755 for all project files - Add chain_id parameter to get_balance RPC endpoint with default "ait-devnet" - Rename Miner.extra_meta_data to extra_metadata for consistency
486 lines
16 KiB
Python
Executable File
486 lines
16 KiB
Python
Executable File
"""
|
|
Production Plugin Registry Service for AITBC
|
|
Handles plugin registration, discovery, versioning, and security validation
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
import hashlib
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
from typing import Dict, Any, List, Optional
|
|
from fastapi import FastAPI, HTTPException, UploadFile, File
|
|
from pydantic import BaseModel
|
|
|
|
# Configure logging
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
app = FastAPI(
|
|
title="AITBC Plugin Registry",
|
|
description="Production plugin registry for AITBC ecosystem",
|
|
version="1.0.0"
|
|
)
|
|
|
|
# Data models
|
|
class PluginRegistration(BaseModel):
|
|
name: str
|
|
version: str
|
|
description: str
|
|
author: str
|
|
category: str
|
|
tags: List[str]
|
|
repository_url: str
|
|
homepage_url: Optional[str] = None
|
|
license: str
|
|
dependencies: List[str] = []
|
|
aitbc_version: str
|
|
plugin_type: str # cli, blockchain, ai, web, etc.
|
|
|
|
class PluginVersion(BaseModel):
|
|
version: str
|
|
changelog: str
|
|
download_url: str
|
|
checksum: str
|
|
aitbc_compatibility: List[str]
|
|
release_date: datetime
|
|
|
|
class SecurityScan(BaseModel):
|
|
scan_id: str
|
|
plugin_id: str
|
|
version: str
|
|
scan_date: datetime
|
|
vulnerabilities: List[Dict[str, Any]]
|
|
risk_score: str # low, medium, high, critical
|
|
passed: bool
|
|
|
|
# In-memory storage (in production, use database)
|
|
plugins: Dict[str, Dict] = {}
|
|
plugin_versions: Dict[str, List[Dict]] = {}
|
|
security_scans: Dict[str, Dict] = {}
|
|
analytics: Dict[str, Dict] = {}
|
|
downloads: Dict[str, List[Dict]] = {}
|
|
|
|
@app.get("/")
|
|
async def root():
|
|
return {
|
|
"service": "AITBC Plugin Registry",
|
|
"status": "running",
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"version": "1.0.0"
|
|
}
|
|
|
|
@app.get("/health")
|
|
async def health_check():
|
|
return {
|
|
"status": "healthy",
|
|
"total_plugins": len(plugins),
|
|
"total_versions": sum(len(versions) for versions in plugin_versions.values()),
|
|
"security_scans": len(security_scans),
|
|
"downloads_today": len([d for downloads_list in downloads.values()
|
|
for d in downloads_list
|
|
if datetime.fromisoformat(d["timestamp"]).date() == datetime.utcnow().date()])
|
|
}
|
|
|
|
@app.post("/api/v1/plugins/register")
|
|
async def register_plugin(plugin: PluginRegistration):
|
|
"""Register a new plugin"""
|
|
plugin_id = f"{plugin.name.lower().replace(' ', '_')}"
|
|
|
|
if plugin_id in plugins:
|
|
raise HTTPException(status_code=400, detail="Plugin already registered")
|
|
|
|
# Create plugin record
|
|
plugin_record = {
|
|
"plugin_id": plugin_id,
|
|
"name": plugin.name,
|
|
"description": plugin.description,
|
|
"author": plugin.author,
|
|
"category": plugin.category,
|
|
"tags": plugin.tags,
|
|
"repository_url": plugin.repository_url,
|
|
"homepage_url": plugin.homepage_url,
|
|
"license": plugin.license,
|
|
"dependencies": plugin.dependencies,
|
|
"aitbc_version": plugin.aitbc_version,
|
|
"plugin_type": plugin.plugin_type,
|
|
"status": "active",
|
|
"created_at": datetime.utcnow().isoformat(),
|
|
"updated_at": datetime.utcnow().isoformat(),
|
|
"verified": False,
|
|
"featured": False,
|
|
"download_count": 0,
|
|
"rating": 0.0,
|
|
"rating_count": 0,
|
|
"latest_version": plugin.version
|
|
}
|
|
|
|
plugins[plugin_id] = plugin_record
|
|
plugin_versions[plugin_id] = []
|
|
|
|
# Initialize analytics
|
|
analytics[plugin_id] = {
|
|
"downloads": [],
|
|
"views": [],
|
|
"ratings": [],
|
|
"daily_stats": {}
|
|
}
|
|
|
|
logger.info(f"Plugin registered: {plugin.name}")
|
|
|
|
return {
|
|
"plugin_id": plugin_id,
|
|
"status": "registered",
|
|
"name": plugin.name,
|
|
"created_at": plugin_record["created_at"]
|
|
}
|
|
|
|
@app.post("/api/v1/plugins/{plugin_id}/versions")
|
|
async def add_plugin_version(plugin_id: str, version: PluginVersion):
|
|
"""Add a new version to an existing plugin"""
|
|
if plugin_id not in plugins:
|
|
raise HTTPException(status_code=404, detail="Plugin not found")
|
|
|
|
# Check if version already exists
|
|
for existing_version in plugin_versions[plugin_id]:
|
|
if existing_version["version"] == version.version:
|
|
raise HTTPException(status_code=400, detail="Version already exists")
|
|
|
|
# Create version record
|
|
version_record = {
|
|
"version_id": f"{plugin_id}_v_{version.version}",
|
|
"plugin_id": plugin_id,
|
|
"version": version.version,
|
|
"changelog": version.changelog,
|
|
"download_url": version.download_url,
|
|
"checksum": version.checksum,
|
|
"aitbc_compatibility": version.aitbc_compatibility,
|
|
"release_date": version.release_date.isoformat(),
|
|
"downloads": 0,
|
|
"security_scan_passed": False,
|
|
"created_at": datetime.utcnow().isoformat()
|
|
}
|
|
|
|
plugin_versions[plugin_id].append(version_record)
|
|
|
|
# Update plugin's latest version
|
|
plugins[plugin_id]["latest_version"] = version.version
|
|
plugins[plugin_id]["updated_at"] = datetime.utcnow().isoformat()
|
|
|
|
# Sort versions by version number (semantic versioning)
|
|
plugin_versions[plugin_id].sort(key=lambda x: x["version"], reverse=True)
|
|
|
|
logger.info(f"Version added to plugin {plugin_id}: {version.version}")
|
|
|
|
return {
|
|
"plugin_id": plugin_id,
|
|
"version": version.version,
|
|
"status": "added",
|
|
"created_at": version_record["created_at"]
|
|
}
|
|
|
|
@app.get("/api/v1/plugins")
|
|
async def list_plugins(category: Optional[str] = None, tag: Optional[str] = None,
|
|
search: Optional[str] = None, sort_by: str = "created_at"):
|
|
"""List all plugins with filtering and sorting"""
|
|
filtered_plugins = []
|
|
|
|
for plugin in plugins.values():
|
|
# Apply filters
|
|
if category and plugin["category"] != category:
|
|
continue
|
|
if tag and tag not in plugin["tags"]:
|
|
continue
|
|
if search and search.lower() not in plugin["name"].lower() and search.lower() not in plugin["description"].lower():
|
|
continue
|
|
|
|
filtered_plugins.append(plugin.copy())
|
|
|
|
# Sort plugins
|
|
if sort_by == "created_at":
|
|
filtered_plugins.sort(key=lambda x: x["created_at"], reverse=True)
|
|
elif sort_by == "updated_at":
|
|
filtered_plugins.sort(key=lambda x: x["updated_at"], reverse=True)
|
|
elif sort_by == "name":
|
|
filtered_plugins.sort(key=lambda x: x["name"])
|
|
elif sort_by == "downloads":
|
|
filtered_plugins.sort(key=lambda x: x["download_count"], reverse=True)
|
|
elif sort_by == "rating":
|
|
filtered_plugins.sort(key=lambda x: x["rating"], reverse=True)
|
|
|
|
return {
|
|
"plugins": filtered_plugins,
|
|
"total_plugins": len(filtered_plugins),
|
|
"filters": {
|
|
"category": category,
|
|
"tag": tag,
|
|
"search": search,
|
|
"sort_by": sort_by
|
|
}
|
|
}
|
|
|
|
@app.get("/api/v1/plugins/{plugin_id}")
|
|
async def get_plugin(plugin_id: str):
|
|
"""Get detailed plugin information"""
|
|
if plugin_id not in plugins:
|
|
raise HTTPException(status_code=404, detail="Plugin not found")
|
|
|
|
plugin = plugins[plugin_id].copy()
|
|
|
|
# Add version information
|
|
plugin["versions"] = plugin_versions.get(plugin_id, [])
|
|
|
|
# Add analytics
|
|
plugin_analytics = analytics.get(plugin_id, {})
|
|
plugin["analytics"] = {
|
|
"total_downloads": len(plugin_analytics.get("downloads", [])),
|
|
"total_views": len(plugin_analytics.get("views", [])),
|
|
"average_rating": sum(plugin_analytics.get("ratings", [])) / len(plugin_analytics.get("ratings", [])) if plugin_analytics.get("ratings") else 0.0,
|
|
"rating_count": len(plugin_analytics.get("ratings", []))
|
|
}
|
|
|
|
return plugin
|
|
|
|
@app.get("/api/v1/plugins/{plugin_id}/versions")
|
|
async def get_plugin_versions(plugin_id: str):
|
|
"""Get all versions of a plugin"""
|
|
if plugin_id not in plugins:
|
|
raise HTTPException(status_code=404, detail="Plugin not found")
|
|
|
|
return {
|
|
"plugin_id": plugin_id,
|
|
"versions": plugin_versions.get(plugin_id, []),
|
|
"total_versions": len(plugin_versions.get(plugin_id, []))
|
|
}
|
|
|
|
@app.get("/api/v1/plugins/{plugin_id}/download/{version}")
|
|
async def download_plugin(plugin_id: str, version: str):
|
|
"""Download a specific plugin version"""
|
|
if plugin_id not in plugins:
|
|
raise HTTPException(status_code=404, detail="Plugin not found")
|
|
|
|
# Find the version
|
|
version_record = None
|
|
for v in plugin_versions.get(plugin_id, []):
|
|
if v["version"] == version:
|
|
version_record = v
|
|
break
|
|
|
|
if not version_record:
|
|
raise HTTPException(status_code=404, detail="Version not found")
|
|
|
|
# Record download
|
|
download_record = {
|
|
"version": version,
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"ip_address": "client_ip", # In production, get actual IP
|
|
"user_agent": "user_agent" # In production, get actual user agent
|
|
}
|
|
|
|
if plugin_id not in downloads:
|
|
downloads[plugin_id] = []
|
|
downloads[plugin_id].append(download_record)
|
|
|
|
# Update analytics
|
|
if plugin_id not in analytics:
|
|
analytics[plugin_id] = {"downloads": [], "views": [], "ratings": []}
|
|
analytics[plugin_id]["downloads"].append(datetime.utcnow().timestamp())
|
|
|
|
# Update plugin download count
|
|
plugins[plugin_id]["download_count"] += 1
|
|
version_record["downloads"] += 1
|
|
|
|
# In production, this would return the actual file
|
|
return {
|
|
"plugin_id": plugin_id,
|
|
"version": version,
|
|
"download_url": version_record["download_url"],
|
|
"checksum": version_record["checksum"],
|
|
"download_count": version_record["downloads"]
|
|
}
|
|
|
|
@app.post("/api/v1/plugins/{plugin_id}/security-scan")
|
|
async def create_security_scan(plugin_id: str, scan: SecurityScan):
|
|
"""Create a security scan record for a plugin version"""
|
|
if plugin_id not in plugins:
|
|
raise HTTPException(status_code=404, detail="Plugin not found")
|
|
|
|
# Verify version exists
|
|
version_exists = any(v["version"] == scan.version for v in plugin_versions.get(plugin_id, []))
|
|
if not version_exists:
|
|
raise HTTPException(status_code=404, detail="Version not found")
|
|
|
|
# Create security scan record
|
|
security_scans[scan.scan_id] = {
|
|
"scan_id": scan.scan_id,
|
|
"plugin_id": plugin_id,
|
|
"version": scan.version,
|
|
"scan_date": scan.scan_date.isoformat(),
|
|
"vulnerabilities": scan.vulnerabilities,
|
|
"risk_score": scan.risk_score,
|
|
"passed": scan.passed,
|
|
"created_at": datetime.utcnow().isoformat()
|
|
}
|
|
|
|
# Update version security status
|
|
for version_record in plugin_versions.get(plugin_id, []):
|
|
if version_record["version"] == scan.version:
|
|
version_record["security_scan_passed"] = scan.passed
|
|
break
|
|
|
|
logger.info(f"Security scan created for {plugin_id} v{scan.version}: {scan.risk_score}")
|
|
|
|
return {
|
|
"scan_id": scan.scan_id,
|
|
"plugin_id": plugin_id,
|
|
"version": scan.version,
|
|
"risk_score": scan.risk_score,
|
|
"passed": scan.passed,
|
|
"scan_date": scan.scan_date.isoformat()
|
|
}
|
|
|
|
@app.get("/api/v1/plugins/{plugin_id}/security")
|
|
async def get_plugin_security(plugin_id: str):
|
|
"""Get security information for a plugin"""
|
|
if plugin_id not in plugins:
|
|
raise HTTPException(status_code=404, detail="Plugin not found")
|
|
|
|
plugin_scans = []
|
|
for scan_id, scan in security_scans.items():
|
|
if scan["plugin_id"] == plugin_id:
|
|
plugin_scans.append(scan)
|
|
|
|
# Sort by scan date
|
|
plugin_scans.sort(key=lambda x: x["scan_date"], reverse=True)
|
|
|
|
return {
|
|
"plugin_id": plugin_id,
|
|
"security_scans": plugin_scans,
|
|
"total_scans": len(plugin_scans),
|
|
"latest_scan": plugin_scans[0] if plugin_scans else None
|
|
}
|
|
|
|
@app.get("/api/v1/categories")
|
|
async def get_categories():
|
|
"""Get all plugin categories"""
|
|
categories = {}
|
|
for plugin in plugins.values():
|
|
category = plugin["category"]
|
|
if category not in categories:
|
|
categories[category] = {
|
|
"name": category,
|
|
"plugin_count": 0,
|
|
"description": f"Plugins in {category} category"
|
|
}
|
|
categories[category]["plugin_count"] += 1
|
|
|
|
return {
|
|
"categories": list(categories.values()),
|
|
"total_categories": len(categories)
|
|
}
|
|
|
|
@app.get("/api/v1/tags")
|
|
async def get_tags():
|
|
"""Get all plugin tags"""
|
|
tag_counts = {}
|
|
for plugin in plugins.values():
|
|
for tag in plugin["tags"]:
|
|
tag_counts[tag] = tag_counts.get(tag, 0) + 1
|
|
|
|
return {
|
|
"tags": [{"tag": tag, "count": count} for tag, count in sorted(tag_counts.items(), key=lambda x: x[1], reverse=True)],
|
|
"total_tags": len(tag_counts)
|
|
}
|
|
|
|
@app.get("/api/v1/analytics/popular")
|
|
async def get_popular_plugins(limit: int = 10):
|
|
"""Get most popular plugins by downloads"""
|
|
popular_plugins = sorted(plugins.values(), key=lambda x: x["download_count"], reverse=True)[:limit]
|
|
|
|
return {
|
|
"popular_plugins": popular_plugins,
|
|
"limit": limit,
|
|
"generated_at": datetime.utcnow().isoformat()
|
|
}
|
|
|
|
@app.get("/api/v1/analytics/recent")
|
|
async def get_recent_plugins(limit: int = 10):
|
|
"""Get recently updated plugins"""
|
|
recent_plugins = sorted(plugins.values(), key=lambda x: x["updated_at"], reverse=True)[:limit]
|
|
|
|
return {
|
|
"recent_plugins": recent_plugins,
|
|
"limit": limit,
|
|
"generated_at": datetime.utcnow().isoformat()
|
|
}
|
|
|
|
@app.get("/api/v1/analytics/dashboard")
|
|
async def get_analytics_dashboard():
|
|
"""Get registry analytics dashboard"""
|
|
total_plugins = len(plugins)
|
|
total_versions = sum(len(versions) for versions in plugin_versions.values())
|
|
total_downloads = sum(plugin["download_count"] for plugin in plugins.values())
|
|
|
|
# Category distribution
|
|
category_stats = {}
|
|
for plugin in plugins.values():
|
|
category = plugin["category"]
|
|
category_stats[category] = category_stats.get(category, 0) + 1
|
|
|
|
# Recent activity
|
|
recent_downloads = 0
|
|
today = datetime.utcnow().date()
|
|
for download_list in downloads.values():
|
|
recent_downloads += len([d for d in download_list
|
|
if datetime.fromisoformat(d["timestamp"]).date() == today])
|
|
|
|
return {
|
|
"dashboard": {
|
|
"total_plugins": total_plugins,
|
|
"total_versions": total_versions,
|
|
"total_downloads": total_downloads,
|
|
"recent_downloads_today": recent_downloads,
|
|
"categories": category_stats,
|
|
"security_scans": len(security_scans),
|
|
"passed_scans": len([s for s in security_scans.values() if s["passed"]])
|
|
},
|
|
"generated_at": datetime.utcnow().isoformat()
|
|
}
|
|
|
|
# Background task for analytics processing
|
|
async def process_analytics():
|
|
"""Background task to process analytics data"""
|
|
while True:
|
|
await asyncio.sleep(3600) # Process every hour
|
|
|
|
# Update daily statistics
|
|
current_date = datetime.utcnow().date()
|
|
|
|
for plugin_id, plugin_analytics in analytics.items():
|
|
daily_key = current_date.isoformat()
|
|
|
|
if daily_key not in plugin_analytics["daily_stats"]:
|
|
plugin_analytics["daily_stats"][daily_key] = {
|
|
"downloads": len([d for d in plugin_analytics.get("downloads", [])
|
|
if datetime.fromtimestamp(d).date() == current_date]),
|
|
"views": len([v for v in plugin_analytics.get("views", [])
|
|
if datetime.fromtimestamp(v).date() == current_date]),
|
|
"ratings": len([r for r in plugin_analytics.get("ratings", [])
|
|
if datetime.fromtimestamp(r).date() == current_date])
|
|
}
|
|
|
|
@app.on_event("startup")
|
|
async def startup_event():
|
|
logger.info("Starting AITBC Plugin Registry")
|
|
# Start analytics processing
|
|
asyncio.create_task(process_analytics())
|
|
|
|
@app.on_event("shutdown")
|
|
async def shutdown_event():
|
|
logger.info("Shutting down AITBC Plugin Registry")
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(app, host="0.0.0.0", port=8013, log_level="info")
|