Database Hardening: - Add docker-compose.db.yml with persistent PostgreSQL volume - Add automatic DB backups every 12h (7 days, 4 weeks, 6 months retention) - Add MinIO S3-compatible storage for assets Assets Migration: - Add MinIO client (lib/assets_client.py) for upload/delete - Update upload endpoint to use MinIO (with local fallback) - Add migration 043_asset_urls_to_text.sql for full HTTPS URLs - Simplify normalizeAssetUrl for S3 URLs Recovery: - Add seed_full_city_reset.py for emergency city recovery - Add DB_RESTORE.md with backup restore instructions - Add SEED_RECOVERY.md with recovery procedures - Add INFRA_ASSETS_MINIO.md with MinIO setup guide Task: TASK_PHASE_DATABASE_HARDENING_AND_ASSETS_MIGRATION_v1
291 lines
9.3 KiB
Python
291 lines
9.3 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Emergency recovery script for DAARION City.
|
|
Restores essential data after database loss:
|
|
- Core MicroDAOs
|
|
- City districts
|
|
- Basic city rooms
|
|
- Core agents for NODE1
|
|
- Asset URLs (from MinIO/S3)
|
|
"""
|
|
import asyncio
|
|
import os
|
|
import sys
|
|
import asyncpg
|
|
from typing import List, Dict, Any
|
|
|
|
# Add parent directory to path for imports
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
|
|
|
DATABASE_URL = os.getenv(
|
|
"DATABASE_URL",
|
|
"postgresql://postgres:postgres@localhost:5432/daarion"
|
|
)
|
|
|
|
# Base MicroDAOs with S3 asset URLs
|
|
BASE_MICRODAO = [
|
|
{
|
|
"id": "dao_daarion",
|
|
"slug": "daarion",
|
|
"name": "DAARION DAO",
|
|
"description": "Main ecosystem DAO",
|
|
"district": "Core",
|
|
"is_public": True,
|
|
"is_platform": True,
|
|
"is_active": True,
|
|
"is_pinned": True,
|
|
"pinned_weight": 1,
|
|
"orchestrator_agent_id": "daarwizz",
|
|
"logo_url": "https://assets.daarion.space/daarion-assets/microdao/logo/daarion.png",
|
|
"banner_url": None,
|
|
},
|
|
{
|
|
"id": "dao_energy",
|
|
"slug": "energy-union",
|
|
"name": "Energy Union",
|
|
"description": "Energy optimization & sustainability",
|
|
"district": "Energy",
|
|
"is_public": True,
|
|
"is_platform": True,
|
|
"is_active": True,
|
|
"is_pinned": True,
|
|
"pinned_weight": 2,
|
|
"orchestrator_agent_id": "helion",
|
|
"logo_url": "https://assets.daarion.space/daarion-assets/microdao/logo/energy-union.png",
|
|
"banner_url": None,
|
|
},
|
|
{
|
|
"id": "dao_greenfood",
|
|
"slug": "greenfood",
|
|
"name": "GreenFood DAO",
|
|
"description": "Sustainable food systems",
|
|
"district": "Green",
|
|
"is_public": True,
|
|
"is_platform": True,
|
|
"is_active": True,
|
|
"is_pinned": True,
|
|
"pinned_weight": 3,
|
|
"orchestrator_agent_id": "greenfood",
|
|
"logo_url": "https://assets.daarion.space/daarion-assets/microdao/logo/greenfood.png",
|
|
"banner_url": None,
|
|
},
|
|
{
|
|
"id": "dao_soul",
|
|
"slug": "soul-retreat",
|
|
"name": "Soul Retreat Hub",
|
|
"description": "Identity & reputation system",
|
|
"district": "Soul",
|
|
"is_public": True,
|
|
"is_platform": True,
|
|
"is_active": True,
|
|
"is_pinned": True,
|
|
"pinned_weight": 4,
|
|
"orchestrator_agent_id": "soul",
|
|
"logo_url": "https://assets.daarion.space/daarion-assets/microdao/logo/soul-retreat.png",
|
|
"banner_url": None,
|
|
},
|
|
]
|
|
|
|
# Core agents for NODE1
|
|
CORE_AGENTS_NODE1 = [
|
|
{
|
|
"id": "daarwizz",
|
|
"display_name": "DAARWIZZ",
|
|
"kind": "orchestrator",
|
|
"role": "Core Orchestrator",
|
|
"node_id": "node-1-hetzner-gex44",
|
|
"home_node_id": "node-1-hetzner-gex44",
|
|
"district": "Core",
|
|
"is_public": True,
|
|
"is_orchestrator": True,
|
|
"status": "online",
|
|
},
|
|
{
|
|
"id": "helion",
|
|
"display_name": "Helion",
|
|
"kind": "orchestrator",
|
|
"role": "Energy Orchestrator",
|
|
"node_id": "node-1-hetzner-gex44",
|
|
"home_node_id": "node-1-hetzner-gex44",
|
|
"district": "Energy",
|
|
"is_public": True,
|
|
"is_orchestrator": True,
|
|
"status": "online",
|
|
},
|
|
{
|
|
"id": "greenfood",
|
|
"display_name": "GreenFood Bot",
|
|
"kind": "orchestrator",
|
|
"role": "Green Orchestrator",
|
|
"node_id": "node-1-hetzner-gex44",
|
|
"home_node_id": "node-1-hetzner-gex44",
|
|
"district": "Green",
|
|
"is_public": True,
|
|
"is_orchestrator": True,
|
|
"status": "online",
|
|
},
|
|
{
|
|
"id": "soul",
|
|
"display_name": "Soul Bot",
|
|
"kind": "orchestrator",
|
|
"role": "Soul Orchestrator",
|
|
"node_id": "node-1-hetzner-gex44",
|
|
"home_node_id": "node-1-hetzner-gex44",
|
|
"district": "Soul",
|
|
"is_public": True,
|
|
"is_orchestrator": True,
|
|
"status": "online",
|
|
},
|
|
]
|
|
|
|
|
|
async def seed_microdaos(conn: asyncpg.Connection):
|
|
"""Seed base MicroDAOs."""
|
|
print("📦 Seeding MicroDAOs...")
|
|
|
|
for dao in BASE_MICRODAO:
|
|
await conn.execute("""
|
|
INSERT INTO microdao (
|
|
id, slug, name, description, district,
|
|
is_public, is_platform, is_active, is_pinned, pinned_weight,
|
|
orchestrator_agent_id, logo_url, banner_url
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13
|
|
)
|
|
ON CONFLICT (id) DO UPDATE SET
|
|
name = EXCLUDED.name,
|
|
description = EXCLUDED.description,
|
|
district = EXCLUDED.district,
|
|
is_public = EXCLUDED.is_public,
|
|
is_platform = EXCLUDED.is_platform,
|
|
is_active = EXCLUDED.is_active,
|
|
is_pinned = EXCLUDED.is_pinned,
|
|
pinned_weight = EXCLUDED.pinned_weight,
|
|
orchestrator_agent_id = EXCLUDED.orchestrator_agent_id,
|
|
logo_url = EXCLUDED.logo_url,
|
|
banner_url = EXCLUDED.banner_url,
|
|
updated_at = NOW()
|
|
""",
|
|
dao["id"], dao["slug"], dao["name"], dao["description"], dao["district"],
|
|
dao["is_public"], dao["is_platform"], dao["is_active"],
|
|
dao["is_pinned"], dao["pinned_weight"], dao["orchestrator_agent_id"],
|
|
dao["logo_url"], dao["banner_url"]
|
|
)
|
|
print(f" ✅ {dao['name']}")
|
|
|
|
print(f"✅ Seeded {len(BASE_MICRODAO)} MicroDAOs")
|
|
|
|
|
|
async def seed_core_agents(conn: asyncpg.Connection):
|
|
"""Seed core agents for NODE1."""
|
|
print("🤖 Seeding core agents (NODE1)...")
|
|
|
|
for agent in CORE_AGENTS_NODE1:
|
|
await conn.execute("""
|
|
INSERT INTO agents (
|
|
id, display_name, kind, role, node_id, home_node_id,
|
|
district, is_public, is_orchestrator, status,
|
|
slug, public_slug, public_title, public_tagline,
|
|
public_district, is_listed_in_directory
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
|
|
$11, $12, $13, $14, $15, $16
|
|
)
|
|
ON CONFLICT (id) DO UPDATE SET
|
|
display_name = EXCLUDED.display_name,
|
|
kind = EXCLUDED.kind,
|
|
role = EXCLUDED.role,
|
|
node_id = EXCLUDED.node_id,
|
|
home_node_id = EXCLUDED.home_node_id,
|
|
district = EXCLUDED.district,
|
|
is_public = EXCLUDED.is_public,
|
|
is_orchestrator = EXCLUDED.is_orchestrator,
|
|
status = EXCLUDED.status,
|
|
updated_at = NOW()
|
|
""",
|
|
agent["id"], agent["display_name"], agent["kind"], agent["role"],
|
|
agent["node_id"], agent["home_node_id"], agent["district"],
|
|
agent["is_public"], agent["is_orchestrator"], agent["status"],
|
|
agent["id"], agent["id"], agent["display_name"], agent["role"],
|
|
agent["district"], True
|
|
)
|
|
print(f" ✅ {agent['display_name']}")
|
|
|
|
print(f"✅ Seeded {len(CORE_AGENTS_NODE1)} core agents")
|
|
|
|
|
|
async def link_agents_to_microdaos(conn: asyncpg.Connection):
|
|
"""Link orchestrator agents to their MicroDAOs."""
|
|
print("🔗 Linking agents to MicroDAOs...")
|
|
|
|
links = [
|
|
("daarwizz", "dao_daarion", True), # is_core
|
|
("helion", "dao_energy", True),
|
|
("greenfood", "dao_greenfood", True),
|
|
("soul", "dao_soul", True),
|
|
]
|
|
|
|
for agent_id, microdao_id, is_core in links:
|
|
await conn.execute("""
|
|
INSERT INTO microdao_agents (microdao_id, agent_id, is_core, role)
|
|
VALUES ($1, $2, $3, 'orchestrator')
|
|
ON CONFLICT (microdao_id, agent_id) DO UPDATE SET
|
|
is_core = EXCLUDED.is_core,
|
|
role = EXCLUDED.role
|
|
""", microdao_id, agent_id, is_core)
|
|
print(f" ✅ Linked {agent_id} → {microdao_id}")
|
|
|
|
print("✅ Linked agents to MicroDAOs")
|
|
|
|
|
|
async def main():
|
|
"""Main recovery function."""
|
|
print("=" * 60)
|
|
print("🏙️ DAARION City Emergency Recovery")
|
|
print("=" * 60)
|
|
print()
|
|
|
|
conn = None
|
|
try:
|
|
print(f"🔗 Connecting to database...")
|
|
conn = await asyncpg.connect(DATABASE_URL)
|
|
print("✅ Connected")
|
|
print()
|
|
|
|
# Seed in order
|
|
await seed_microdaos(conn)
|
|
print()
|
|
await seed_core_agents(conn)
|
|
print()
|
|
await link_agents_to_microdaos(conn)
|
|
print()
|
|
|
|
# Summary
|
|
microdao_count = await conn.fetchval("SELECT COUNT(*) FROM microdao")
|
|
agent_count = await conn.fetchval("SELECT COUNT(*) FROM agents")
|
|
|
|
print("=" * 60)
|
|
print("✅ Recovery complete!")
|
|
print(f" MicroDAOs: {microdao_count}")
|
|
print(f" Agents: {agent_count}")
|
|
print()
|
|
print("📝 Next steps:")
|
|
print(" 1. Run migrations if needed")
|
|
print(" 2. Run scripts/sync-node2-dagi-agents.py for NODE2 agents")
|
|
print(" 3. Upload logos/banners to MinIO if not already done")
|
|
print("=" * 60)
|
|
|
|
except Exception as e:
|
|
print(f"❌ Error during recovery: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
sys.exit(1)
|
|
finally:
|
|
if conn:
|
|
await conn.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|
|
|