Phase1 skeleton: replace demo main.py with src.server contract

- Overlay ModelHub XC Phase1 (health/status, STRATEGY_ID gate, dry-run)
- Dockerfile CMD -> python -m src.server; FROM stays platform TCR base
- Remove demo main.py and requirements.txt (stdlib only)
- Add tests/, scripts/, docs/, Makefile, config/examples (no secrets)
This commit is contained in:
VC_noSugar
2026-10-16 15:47:10 +08:00
parent 26351cefee
commit 0d0f3b611a
31 changed files with 1436 additions and 106 deletions

83
src/server.py Normal file
View File

@@ -0,0 +1,83 @@
"""Minimal HTTP server for the adaptation agent (stdlib only).
Listens on PORT (default 8080). Handles SIGTERM and exits the main loop
within ~1s (platform window documented as ≤30s).
Startup: require STRATEGY_ID (Phase1 product plan — fail if missing).
"""
from __future__ import annotations
import os
import signal
import sys
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Optional
from .health import handle_get
from .strategy import require_strategy_id
_shutdown = threading.Event()
_httpd: Optional[ThreadingHTTPServer] = None
class AgentHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def log_message(self, fmt: str, *args) -> None: # noqa: A003
# Avoid noisy default logging; never log env secrets.
sys.stderr.write("[http] request handled (details omitted)\n")
def do_GET(self) -> None: # noqa: N802
code, headers, body = handle_get(self.path)
self.send_response(code)
for k, v in headers.items():
self.send_header(k, v)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_POST(self) -> None: # noqa: N802
body = b'{"error":"method_not_allowed"}'
self.send_response(405)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def _on_sigterm(signum: int, frame) -> None: # noqa: ARG001
sys.stderr.write(f"Received signal {signum}; shutting down...\n")
_shutdown.set()
httpd = _httpd
if httpd is not None:
# wake serve_forever
threading.Thread(target=httpd.shutdown, daemon=True).start()
def main() -> None:
global _httpd
require_strategy_id() # exit 1 if missing
port = int(os.environ.get("PORT", "8080"))
host = os.environ.get("HOST", "0.0.0.0")
signal.signal(signal.SIGTERM, _on_sigterm)
signal.signal(signal.SIGINT, _on_sigterm)
httpd = ThreadingHTTPServer((host, port), AgentHandler)
httpd.daemon_threads = True
_httpd = httpd
sys.stderr.write(f"Listening on {host}:{port} (GET /health, GET /status)\n")
try:
httpd.serve_forever(poll_interval=0.5)
finally:
httpd.server_close()
sys.stderr.write("Server stopped.\n")
if __name__ == "__main__":
main()