redesign fully scaffolded and web login works

This commit is contained in:
2026-03-17 20:10:47 -04:00
parent b9cc397e05
commit f6bd22a8ef
143 changed files with 17317 additions and 693 deletions

275
client-py/main.py Normal file
View File

@@ -0,0 +1,275 @@
#!/usr/bin/env python3
"""
DexOrder User Container Main Entry Point
Brings together:
- Config and secrets loading from k8s mounted YAML files
- ZeroMQ event publisher for user events
- MCP server with minimal "hello world" resource
- Lifecycle management integration
"""
import asyncio
import logging
import os
import signal
import sys
from pathlib import Path
from typing import Optional
import yaml
from mcp.server import Server
from mcp.server.stdio import stdio_server
from dexorder import EventPublisher, start_lifecycle_manager, get_lifecycle_manager
from dexorder.events import EventType, UserEvent, DeliverySpec
# =============================================================================
# Configuration
# =============================================================================
class Config:
"""Application configuration loaded from config.yaml and secrets.yaml"""
def __init__(self):
# User ID (required)
self.user_id: str = os.getenv("USER_ID", "")
if not self.user_id:
raise ValueError("USER_ID environment variable required")
# Config and secrets paths (k8s mounted)
self.config_path = Path(os.getenv("CONFIG_PATH", "/app/config/config.yaml"))
self.secrets_path = Path(os.getenv("SECRETS_PATH", "/app/config/secrets.yaml"))
# ZMQ ports for event system
self.zmq_xpub_port: int = int(os.getenv("ZMQ_XPUB_PORT", "5570"))
self.zmq_gateway_endpoint: str = os.getenv(
"ZMQ_GATEWAY_ENDPOINT",
"tcp://gateway:5571"
)
# MCP server settings
self.mcp_server_name: str = os.getenv("MCP_SERVER_NAME", "dexorder-user")
# Lifecycle settings
self.idle_timeout_minutes: int = int(os.getenv("IDLE_TIMEOUT_MINUTES", "15"))
self.enable_idle_shutdown: bool = os.getenv("ENABLE_IDLE_SHUTDOWN", "true").lower() == "true"
# Loaded from files
self.config_data: dict = {}
self.secrets_data: dict = {}
def load(self) -> None:
"""Load configuration and secrets from YAML files"""
# Load config.yaml if exists
if self.config_path.exists():
with open(self.config_path) as f:
self.config_data = yaml.safe_load(f) or {}
logging.info(f"Loaded config from {self.config_path}")
else:
logging.warning(f"Config file not found: {self.config_path}")
# Load secrets.yaml if exists
if self.secrets_path.exists():
with open(self.secrets_path) as f:
self.secrets_data = yaml.safe_load(f) or {}
logging.info(f"Loaded secrets from {self.secrets_path}")
else:
logging.warning(f"Secrets file not found: {self.secrets_path}")
# =============================================================================
# MCP Server Setup
# =============================================================================
def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server:
"""Create MCP server with minimal hello world resource"""
server = Server(config.mcp_server_name)
@server.list_resources()
async def list_resources():
"""List available resources"""
return [
{
"uri": f"dexorder://user/{config.user_id}/hello",
"name": "Hello World",
"description": "A simple hello world resource",
"mimeType": "text/plain",
}
]
@server.read_resource()
async def read_resource(uri: str):
"""Read a resource by URI"""
if uri == f"dexorder://user/{config.user_id}/hello":
# Publish an event when resource is accessed
await event_publisher.publish(UserEvent(
event_type=EventType.STRATEGY_LOG,
payload={
"message": "Hello world resource accessed",
"uri": uri,
},
delivery=DeliverySpec.informational(),
))
return {
"uri": uri,
"mimeType": "text/plain",
"text": f"Hello from DexOrder user container!\nUser ID: {config.user_id}\n",
}
else:
raise ValueError(f"Unknown resource: {uri}")
logging.info(f"MCP server '{config.mcp_server_name}' created")
return server
# =============================================================================
# Main Application
# =============================================================================
class UserContainer:
"""Main user container application"""
def __init__(self):
self.config = Config()
self.event_publisher: Optional[EventPublisher] = None
self.mcp_server: Optional[Server] = None
self.running = False
async def start(self) -> None:
"""Start all subsystems"""
logging.info("Starting user container")
# Load configuration
self.config.load()
# Start lifecycle manager
await start_lifecycle_manager(
user_id=self.config.user_id,
idle_timeout_minutes=self.config.idle_timeout_minutes,
enable_idle_shutdown=self.config.enable_idle_shutdown,
)
logging.info("Lifecycle manager started")
# Start event publisher
self.event_publisher = EventPublisher(
user_id=self.config.user_id,
xpub_port=self.config.zmq_xpub_port,
gateway_router_endpoint=self.config.zmq_gateway_endpoint,
)
await self.event_publisher.start()
logging.info("Event publisher started")
# Publish CONTAINER_STARTING event
await self.event_publisher.publish(UserEvent(
event_type=EventType.CONTAINER_STARTING,
payload={
"user_id": self.config.user_id,
"timestamp": None, # Will be auto-filled
},
delivery=DeliverySpec.active_or_telegram(),
))
# Create MCP server
self.mcp_server = create_mcp_server(self.config, self.event_publisher)
# Publish CONTAINER_READY event
await self.event_publisher.publish(UserEvent(
event_type=EventType.CONTAINER_READY,
payload={
"user_id": self.config.user_id,
},
delivery=DeliverySpec.active_or_telegram(),
))
self.running = True
logging.info("User container ready")
async def stop(self) -> None:
"""Stop all subsystems"""
if not self.running:
return
logging.info("Stopping user container")
self.running = False
# Publish CONTAINER_SHUTTING_DOWN event
if self.event_publisher:
await self.event_publisher.publish(UserEvent(
event_type=EventType.CONTAINER_SHUTTING_DOWN,
payload={
"user_id": self.config.user_id,
},
delivery=DeliverySpec.active_or_telegram(),
))
# Stop subsystems
if self.event_publisher:
await self.event_publisher.stop()
logging.info("Event publisher stopped")
lifecycle = get_lifecycle_manager()
if lifecycle:
await lifecycle.stop()
logging.info("Lifecycle manager stopped")
logging.info("User container stopped")
async def run(self) -> None:
"""Run the MCP server via stdio"""
await self.start()
try:
# Run MCP server on stdio
async with stdio_server() as (read_stream, write_stream):
await self.mcp_server.run(
read_stream,
write_stream,
self.mcp_server.create_initialization_options()
)
finally:
await self.stop()
# =============================================================================
# Entry Point
# =============================================================================
async def main():
"""Main entry point"""
# Setup logging
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
logging.basicConfig(
level=getattr(logging, log_level),
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
stream=sys.stderr, # MCP uses stdout for protocol
)
# Create and run container
container = UserContainer()
# Handle shutdown signals
loop = asyncio.get_event_loop()
def handle_signal(sig):
logging.info(f"Received signal {sig}, shutting down...")
asyncio.create_task(container.stop())
loop.stop()
for sig in (signal.SIGTERM, signal.SIGINT):
loop.add_signal_handler(sig, lambda s=sig: handle_signal(s))
try:
await container.run()
except KeyboardInterrupt:
logging.info("Keyboard interrupt received")
except Exception as e:
logging.error(f"Fatal error: {e}", exc_info=True)
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())