Scaffold SimpleX Orchestrate: supervisor over official binaries
A standalone control-plane app that spawns and drives the official SimpleX binaries (never modifies simplex source). Validated against simplex-chat built from source (stable v6.5.4, GHC 9.6.3). - CLAUDE.md: architecture notes mined from the upstream docs (WebSocket bot API, per-profile DBs, directory/broadcast bot config) - supervisor/: process registry + port allocation (supervisor.py), corrId/cmd<->resp WebSocket client (ws_client.py), binary locator (binaries.py), FastAPI front with REST control + /events stream (server.py) - smoke_test.py: Pattern-1 handshake (spawn simplex-chat -p, create+read user) — PASS - group_test.py: two accounts, invitation connect + group invite/join, verified membership over the real SMP network — PASS - build_chat.sh / install_ghc.sh: reproducible toolchain + from-source build Key finding: fresh DB prompts for a display name on stdin; spawn with --create-bot-display-name to start the WebSocket server non-interactively. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
90
supervisor/server.py
Normal file
90
supervisor/server.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""Front-facing API the website talks to.
|
||||
|
||||
The browser:
|
||||
- opens a WebSocket to /events to receive live chat events from every profile
|
||||
(each event is tagged with the originating profile name);
|
||||
- uses REST to spawn/stop profiles and to send commands to cli profiles.
|
||||
|
||||
This process sits between the website and the SimpleX binaries; it is the only
|
||||
thing that touches the binaries. Run: uvicorn supervisor.server:app
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||
|
||||
from .supervisor import Supervisor
|
||||
|
||||
app = FastAPI(title="SimpleX Orchestrate")
|
||||
|
||||
_browser_clients: set[WebSocket] = set()
|
||||
|
||||
|
||||
async def _broadcast(name: str, event: dict) -> None:
|
||||
dead = []
|
||||
for ws in _browser_clients:
|
||||
try:
|
||||
await ws.send_json({"profile": name, "event": event})
|
||||
except Exception:
|
||||
dead.append(ws)
|
||||
for ws in dead:
|
||||
_browser_clients.discard(ws)
|
||||
|
||||
|
||||
sup = Supervisor(on_event=_broadcast)
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def _shutdown() -> None:
|
||||
await sup.stop_all()
|
||||
|
||||
|
||||
# ── Control (REST) ──────────────────────────────────────────────────────────────
|
||||
@app.get("/profiles")
|
||||
async def profiles() -> dict:
|
||||
return {"profiles": sup.list()}
|
||||
|
||||
|
||||
@app.post("/profiles/{name}/start-cli")
|
||||
async def start_cli(name: str) -> dict:
|
||||
await sup.start_cli(name)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.post("/profiles/{name}/start-directory")
|
||||
async def start_directory(name: str, body: dict) -> dict:
|
||||
await sup.start_directory(name, body["super_users"], body["web_folder"])
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.post("/profiles/{name}/start-broadcast")
|
||||
async def start_broadcast(name: str, body: dict) -> dict:
|
||||
await sup.start_broadcast(name, body["display_name"], body["publishers"])
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.post("/profiles/{name}/cmd")
|
||||
async def cmd(name: str, body: dict) -> dict:
|
||||
"""Send a raw chat command string to a cli profile (e.g. '/_send @1 text hi')."""
|
||||
return {"resp": await sup.send(name, body["cmd"])}
|
||||
|
||||
|
||||
@app.post("/profiles/{name}/stop")
|
||||
async def stop(name: str) -> dict:
|
||||
await sup.stop(name)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ── Live events (WebSocket) ──────────────────────────────────────────────────────
|
||||
@app.websocket("/events")
|
||||
async def events(ws: WebSocket) -> None:
|
||||
await ws.accept()
|
||||
_browser_clients.add(ws)
|
||||
try:
|
||||
while True:
|
||||
await ws.receive_text() # keep the socket open; ignore inbound
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
finally:
|
||||
_browser_clients.discard(ws)
|
||||
Reference in New Issue
Block a user