File size: 11,194 Bytes
3060aa0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6511c4e
 
 
 
 
3060aa0
 
 
 
 
 
 
 
 
 
 
 
6511c4e
3060aa0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# =============================================================================
# app/app.py
# Universal MCP Hub (Sandboxed) - based on PyFundaments Architecture
# Copyright 2026 - Volkan Kücükbudak
# Apache License V. 2 + ESOL 1.1
# Repo: https://github.com/VolkanSah/Universal-MCP-Hub-sandboxed
# =============================================================================
# ARCHITECTURE NOTE:
#   This file is the Orchestrator of the sandboxed app/* layer.
#   It is ONLY started by main.py (the "Guardian").
#   All fundament services are injected via the `fundaments` dictionary.
#   Direct execution is blocked by design.
#
# SANDBOX RULE:
#   app/* has NO direct access to .env or fundaments/*.
#   Config for app/* lives in app/.pyfun (provider URLs, models, tool settings).
#   Secrets stay in .env → Guardian reads them → injects what app/* needs.
# =============================================================================

from quart import Quart, request, jsonify  # async Flask — required for async cloud providers + Neon DB
import logging
from waitress import serve                  # WSGI server — keeps Flask non-blocking alongside asyncio
import threading                            # bank-pattern: each blocking service gets its own thread
import requests                             # sync HTTP for health check worker
import time
from datetime import datetime
import asyncio
import sys
from typing import Dict, Any, Optional

# =============================================================================
# Import app/* modules
# Config/settings for all modules below live in app/.pyfun — not in .env!
# =============================================================================
#from . import mcp          # MCP transport layer (stdio / SSE)
#from . import providers    # API provider registry (LLM, Search, Web)
#from . import models       # Model config + token/rate limits
#from . import tools        # MCP tool definitions + provider mapping
#from . import db_sync      # Internal SQLite IPC — app/* state & communication
                           # db_sync ≠ cloud DB! Cloud DB is Guardian-only via main.py.

# Future modules (soon uncommented when ready):
# from . import discord_api  # Discord bot integration
# from . import hf_hooks     # HuggingFace Space hooks
# from . import git_hooks    # GitHub/GitLab webhook handler
# from . import web_api      # Generic REST API handler

# =============================================================================
# Loggers — one per module for clean log filtering
# =============================================================================
logger          = logging.getLogger('application')
#logger          = logging.getLogger('config')
# logger_mcp      = logging.getLogger('mcp')
# logger_tools    = logging.getLogger('tools')
# logger_providers = logging.getLogger('providers')
# logger_models   = logging.getLogger('models')
# logger_db_sync  = logging.getLogger('db_sync')

# =============================================================================
# Flask app instance
# =============================================================================
app = Quart(__name__)
START_TIME = datetime.utcnow()

# =============================================================================
# Global service references (set during initialize_services)
# =============================================================================
_fundaments: Optional[Dict[str, Any]] = None
PORT = None

# =============================================================================
# Service initialization
# =============================================================================
def initialize_services(fundaments: Dict[str, Any]) -> None:
    """
    Initializes all app/* services with injected fundaments from Guardian.
    Called once during start_application — sets global service references.
    """
    global _fundaments, PORT

    _fundaments = fundaments
    PORT = fundaments["config"].get_int("PORT", 7860)

    # Initialize internal SQLite state store for app/* IPC
    db_sync.initialize()

    # Initialize provider registry from app/.pyfun + ENV key presence check
    providers.initialize(fundaments["config"])

    # Initialize model registry from app/.pyfun
    models.initialize()

    # Initialize tool registry — tools only register if their provider is active
    tools.initialize(providers, models, fundaments)

    logger.info("app/* services initialized.")


# =============================================================================
# Background workers
# =============================================================================
def start_mcp_in_thread() -> None:
    """
    Starts the MCP Hub (stdio or SSE) in its own thread with its own event loop.
    Mirrors the bank-thread pattern from the Discord bot architecture.
    """
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    try:
        loop.run_until_complete(mcp.start_mcp(_fundaments))
    finally:
        loop.close()


def health_check_worker() -> None:
    """
    Periodic self-ping to keep the app alive on hosting platforms (e.g. HuggingFace).
    Runs in its own daemon thread — does not block the main loop.
    """
    while True:
        time.sleep(3600)
        try:
            response = requests.get(f"http://127.0.0.1:{PORT}/")
            logger.info(f"Health check ping: {response.status_code}")
        except Exception as e:
            logger.error(f"Health check failed: {e}")


# =============================================================================
# Flask Routes
# =============================================================================

@app.route("/", methods=["GET"])
async def health_check():
    """
    Health check endpoint.
    Used by HuggingFace Spaces and monitoring systems to verify the app is running.
    """
    uptime = datetime.utcnow() - START_TIME
    return jsonify({
        "status": "running",
        "service": "Universal MCP Hub",
        "uptime_seconds": int(uptime.total_seconds()),
        "active_providers": providers.get_active_names() if providers else [],
    })


@app.route("/api", methods=["POST"])
async def api_endpoint():
    """
    Generic REST API endpoint for direct tool invocation.
    Accepts JSON: { "tool": "tool_name", "params": { ... } }
    Auth and validation handled by tools layer.
    """
    # TODO: implement tool dispatch via tools.invoke()
    data = await request.get_json()
    return jsonify({"status": "not_implemented", "received": data}), 501


@app.route("/crypto", methods=["POST"])
async def crypto_endpoint():
    """
    Encrypted API endpoint.
    Payload is decrypted via fundaments/encryption.py (injected by Guardian).
    Only active if encryption_service is available in fundaments.
    """
    encryption_service = _fundaments.get("encryption") if _fundaments else None
    if not encryption_service:
        return jsonify({"error": "Encryption service not available"}), 503

    # TODO: decrypt payload, dispatch, re-encrypt response
    data = await request.get_json()
    return jsonify({"status": "not_implemented"}), 501


# Future routes (uncomment when ready):
# @app.route("/discord", methods=["POST"])
# async def discord_interactions():
#     """Discord interactions endpoint — signature verification via discord_api module."""
#     pass

# @app.route("/webhook/hf", methods=["POST"])
# async def hf_webhook():
#     """HuggingFace Space event hooks."""
#     pass

# @app.route("/webhook/git", methods=["POST"])
# async def git_webhook():
#     """GitHub / GitLab webhook handler."""
#     pass


# =============================================================================
# Main entry point — called by Guardian (main.py)
# =============================================================================
async def start_application(fundaments: Dict[str, Any]) -> None:
    """
    Main entry point for the sandboxed app layer.
    Called exclusively by main.py after all fundament services are initialized.

    Args:
        fundaments: Dictionary of initialized services from Guardian (main.py).
                    All services already validated — may be None if not configured.
    """
    logger.info("Application starting...")

    # --- Unpack fundament services (read-only references) ---
    config_service          = fundaments["config"]
    db_service              = fundaments["db"]              # None if no DB configured
    encryption_service      = fundaments["encryption"]      # None if keys not set
    access_control_service  = fundaments["access_control"]  # None if no DB
    user_handler_service    = fundaments["user_handler"]    # None if no DB
    security_service        = fundaments["security"]        # None if deps missing

    # --- Initialize all app/* services ---
    initialize_services(fundaments)

    # --- Log active fundament services ---
    if encryption_service:
        logger.info("Encryption service active.")

    if user_handler_service and security_service:
        logger.info("Auth services active (user_handler + security).")

    if access_control_service and security_service:
        logger.info("Access control active.")

    if db_service and not user_handler_service:
        logger.info("Database-only mode active (e.g. ML pipeline).")

    if not db_service:
        logger.info("Database-free mode active (e.g. Discord bot, API client).")

    # --- Start MCP Hub in its own thread (stdio or SSE) ---
    mcp_thread = threading.Thread(target=start_mcp_in_thread, daemon=True)
    mcp_thread.start()
    logger.info("MCP Hub thread started.")

    # Allow MCP to initialize before Flask comes up
    await asyncio.sleep(1)

    # --- Start health check worker ---
    health_thread = threading.Thread(target=health_check_worker, daemon=True)
    health_thread.start()

    # --- Start Flask/Quart via Waitress in its own thread ---
    def run_server():
        serve(app, host="0.0.0.0", port=PORT)

    server_thread = threading.Thread(target=run_server, daemon=True)
    server_thread.start()
    logger.info(f"HTTP server started on port {PORT}.")

    logger.info("All services running. Entering heartbeat loop...")

    # --- Heartbeat loop — keeps Guardian's async context alive ---
    try:
        while True:
            await asyncio.sleep(60)
            logger.debug("Heartbeat.")
    except KeyboardInterrupt:
        logger.info("Shutdown signal received.")


# =============================================================================
# Direct execution guard
# =============================================================================
if __name__ == '__main__':
    print("WARNING: Running app.py directly. Fundament modules might not be correctly initialized.")
    print("Please run 'python main.py' instead for proper initialization.")

    test_fundaments = {
        "config":           None,
        "db":               None,
        "encryption":       None,
        "access_control":   None,
        "user_handler":     None,
        "security":         None,
    }

    asyncio.run(start_application(test_fundaments))