Add the fluksio CLI: serve, enroll, worker

`pip install fluksio && fluksio serve` on a machine with no Docker, no
database and no configuration — which is the case this is for: a node on
a cluster where ports cannot be opened. It makes its data directory, its
key and an admin account, prints the password once, and serves. Pairing
is `fluksio enroll <code> --portal …`, doing what the Settings screen
does through the same function, before the engine starts and without one
running — a machine nobody can route to has no browser pointed at it
either. The portal serves the dashboard, so nothing is served here.

Two things had to give way. `fastapi[standard]` pulls a cloud CLI that
wants sentry-sdk 2.x while we pinned below it — no pip resolution
existed, so the pin is lifted, which the comment beside it had been
waiting for and which also lets the Python cap go. And `uv` is now a
dependency rather than something to find on PATH: the Modules screen is
how a data scientist installs torch, and it was quietly falling back to
the engine's own interpreter.

The CLI imports nothing from the engine before it has set DATA_DIR — the
settings are built on the first import of core.config, and reaching it
early put the database in the working directory. There is a test for
that now, because the failure is silent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 22:51:13 +02:00
co-authored by Claude Opus 5
parent 73eeec29b1
commit dffdfce9e4
15 changed files with 1811 additions and 197 deletions
+9 -64
View File
@@ -15,11 +15,11 @@ from __future__ import annotations
import asyncio
import logging
import secrets
from datetime import datetime, timezone
from typing import Any
import httpx
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.concurrency import run_in_threadpool
from pydantic import BaseModel, Field
from sqlmodel import select
@@ -31,6 +31,7 @@ from fluksio.api.deps import (
get_current_user,
)
from fluksio.cloud import config as cloud_config
from fluksio.cloud import enroll as enroll_mod
from fluksio.core.security import get_password_hash
from fluksio.models import Message, User, UserPublic
@@ -83,72 +84,16 @@ async def enroll(
installation, so the owner's portal sessions arrive here as them. Widening
that to anyone else is a local decision made one person at a time, below —
never something the portal can do from its side.
The same work as `fluksio enroll` on the command line, which is how a
machine with no browser pointed at it does this.
"""
if cloud_config.exists():
raise HTTPException(
status_code=409,
detail="This installation is already connected to a portal",
)
base = body.portal_url.rstrip("/")
try:
async with httpx.AsyncClient(timeout=15.0) as client:
response = await client.post(
f"{base}/api/v1/enroll/",
json={"claim_code": body.claim_code, "app_version": "0.1.0"},
)
except httpx.HTTPError as exc:
raise HTTPException(
status_code=502, detail=f"Could not reach the portal: {exc}"
) from exc
if response.status_code == 404:
raise HTTPException(
status_code=400, detail="That claim code is unknown or has expired"
await run_in_threadpool(
enroll_mod.enroll, session, current_user, body.portal_url, body.claim_code
)
if response.status_code != 200:
raise HTTPException(
status_code=502,
detail=f"The portal refused the claim ({response.status_code})",
)
data = response.json()
owner_id = data.get("owner_id")
if not owner_id:
# A portal older than remote users does not say who owns the
# installation, and without that the enrolling account cannot be mapped
# to anyone — which would leave the portal connected but refused here.
raise HTTPException(
status_code=502,
detail="That portal is too old for this installation: it did not "
"say which account owns the installation",
)
config = cloud_config.CloudConfig(
portal_url=base,
ws_url=data["ws_url"],
installation_id=data["installation_id"],
token=data["installation_token"],
issuer=data["issuer"],
# Pinned here, at the one moment the claim code proves who we are
# talking to. Nothing refreshes this.
jwks=data["jwks"],
local_user_id=str(current_user.id),
enrolled_at=datetime.now(timezone.utc).isoformat(),
portal_account=current_user.email,
)
cloud_config.save(config)
owner_id = str(owner_id)
# Re-enrolling from a different local account moves the mapping rather than
# leaving two accounts claiming the same portal identity, which the unique
# index would refuse and the lookup could not choose between anyway.
for other in session.exec(
select(User).where(User.portal_sub == owner_id, User.id != current_user.id)
):
other.portal_sub = None
session.add(other)
current_user.portal_sub = owner_id
session.add(current_user)
session.commit()
except enroll_mod.EnrollError as exc:
raise HTTPException(status_code=exc.status, detail=exc.detail) from exc
_start_connector(request.app)
return Message(message="Connected to the portal")