"""hive — the swarm address book.
A tiny standalone service for the main server: Navi instances across the
network announce themselves here (name, uuid, address, machine info), and
agents ask this book who is currently alive and where. Later this grows into
a mini knowledge base about the fleet (the free-form ``meta`` field is the seed).
Not part of the Navi server — it never starts by default. Run it manually on
the main server (see hive/README.md):
.venv/bin/uvicorn hive.app:app --host 0.0.0.0 --port 8087
Auth: the shared swarm key (PSK). Every announce/peers request must carry the
X-Swarm-Key header matching ``.swarm-key`` (or ``.swarm-key.previous`` — the
rotation window). /health is open.
"""
from __future__ import annotations
import hmac
import os
from pathlib import Path
from fastapi import FastAPI, Header, HTTPException, Request
from pydantic import BaseModel, Field
from hive.db import HiveDB
_KEY_FILE = Path(os.environ.get("HIVE_KEY_FILE", ".swarm-key"))
# Rotation window: the previous key keeps working until it is deleted.
_PREVIOUS_KEY_FILE = _KEY_FILE.with_name(_KEY_FILE.name + ".previous")
_DB_PATH = os.environ.get("HIVE_DB", "hive.db")
_TTL_SEC = int(os.environ.get("HIVE_TTL_SEC", "300"))
app = FastAPI(title="navi hive", description="Swarm address book for Navi instances.")
_db = HiveDB(_DB_PATH)
def _load_keys() -> list[str]:
keys: list[str] = []
for p in (_KEY_FILE, _PREVIOUS_KEY_FILE):
try:
value = p.read_text(encoding="utf-8").strip()
except OSError:
continue
if value:
keys.append(value)
return keys
def _check_key(x_swarm_key: str | None) -> None:
keys = _load_keys()
if not keys:
raise HTTPException(status_code=503, detail="hive has no swarm key configured")
if not x_swarm_key:
raise HTTPException(status_code=401, detail="X-Swarm-Key header required")
if not any(hmac.compare_digest(x_swarm_key, k) for k in keys):
raise HTTPException(status_code=403, detail="invalid swarm key")
class AnnouncePayload(BaseModel):
name: str = Field(min_length=1, max_length=64)
instance_id: str = Field(min_length=8, max_length=64)
version: str = Field(default="", max_length=32)
port: int = Field(ge=1, le=65535)
meta: dict = Field(default_factory=dict)
@app.get("/health")
async def health() -> dict:
return {"status": "ok", "hive": True, "machines": len(_db.list_peers(_TTL_SEC))}
@app.post("/announce")
async def announce(
payload: AnnouncePayload,
request: Request,
x_swarm_key: str | None = Header(default=None),
) -> dict:
_check_key(x_swarm_key)
# The source IP is the reliable address for a floating-IP network; the
# announced payload only supplies the port (and could not be trusted for
# the host anyway — anyone with the key may announce from anywhere).
host = request.client.host if request.client else None
_db.upsert(
instance_id=payload.instance_id,
name=payload.name,
host=host,
port=payload.port,
version=payload.version,
meta=payload.meta,
)
return {"ok": True, "host": host}
@app.get("/peers")
async def peers(x_swarm_key: str | None = Header(default=None)) -> dict:
_check_key(x_swarm_key)
return {"peers": _db.list_peers(_TTL_SEC), "ttl_sec": _TTL_SEC}