skills update; USER ACCOUNT system! Alpha!

This commit is contained in:
D.Popov
2026-07-03 19:22:39 +03:00
parent 5179806a67
commit 7caf12980a
14 changed files with 1260 additions and 11 deletions

View File

@@ -36,7 +36,8 @@ import uuid
from importlib.resources import files
from typing import Any
from .billing import BillingLedger
from .accounts import DEFAULT_ACCOUNTS_DB_PATH, AccountStore
from .billing import DEFAULT_BILLING_DB_PATH, BillingLedger
from .gossip import NodeGossip
from .raft import RaftNode
@@ -1261,6 +1262,7 @@ class _TrackerHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
gossip: "NodeGossip | None" = None,
stats: "_StatsCollector | None" = None,
billing: "BillingLedger | None" = None,
accounts: "AccountStore | None" = None,
benchmark_results_path: str | None = None,
) -> None:
super().__init__(addr, handler)
@@ -1275,6 +1277,7 @@ class _TrackerHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
self.gossip = gossip
self.stats: _StatsCollector | None = stats
self.billing: BillingLedger | None = billing
self.accounts: AccountStore | None = accounts
self.benchmark_results_path = benchmark_results_path or os.path.join(
os.getcwd(), "benchmark_results.json"
)
@@ -1337,6 +1340,24 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
if self.path == "/v1/billing/forfeit":
self._handle_billing_forfeit()
return
if self.path == "/v1/auth/register":
self._handle_auth_register()
return
if self.path == "/v1/auth/login":
self._handle_auth_login()
return
if self.path == "/v1/auth/logout":
self._handle_auth_logout()
return
if self.path == "/v1/account/keys":
self._handle_account_key_create()
return
if self.path == "/v1/account/keys/revoke":
self._handle_account_key_revoke()
return
if self.path == "/v1/accounts/gossip":
self._handle_accounts_gossip()
return
if self.path == "/v1/benchmark/hop-penalty":
self._handle_benchmark_hop_penalty()
return
@@ -1382,6 +1403,10 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
self._handle_billing_summary()
elif parsed.path == "/v1/billing/settlements":
self._handle_billing_settlements()
elif parsed.path == "/v1/account":
self._handle_account_me()
elif parsed.path == "/v1/admin/accounts":
self._handle_admin_accounts()
elif parsed.path == "/v1/benchmark/results":
self._handle_benchmark_results()
elif parsed.path == "/v1/registry/wallets":
@@ -1649,6 +1674,13 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
"code": "missing_api_key",
}})
return
if server.accounts is not None and server.accounts.is_key_revoked(api_key):
self._send_json(401, {"error": {
"message": "API key has been revoked",
"type": "invalid_request_error",
"code": "invalid_api_key",
}})
return
if not server.billing.has_funds(api_key):
self._send_json(402, {"error": {
"message": "insufficient balance: deposit USDT to continue",
@@ -2431,6 +2463,165 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
)
self._send_json(200, {"forfeited": event["amount"], **strike_state})
# ---- user accounts (registration, login, API keys) ----
def _session_account(self) -> dict | None:
"""Resolve the session token in the Authorization header, or None."""
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
if server.accounts is None:
return None
return server.accounts.session_account(_api_key_from_headers(self.headers))
def _require_accounts(self) -> "AccountStore | None":
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
if server.accounts is None:
self._send_json(404, {"error": "accounts are not enabled on this tracker"})
return None
return server.accounts
def _handle_auth_register(self):
accounts = self._require_accounts()
if accounts is None:
return
body = self._read_json_body()
if body is None:
return
try:
account = accounts.register(
email=body.get("email"),
wallet=body.get("wallet"),
password=str(body.get("password") or ""),
)
except ValueError as exc:
self._send_json(400, {"error": str(exc)})
return
token = accounts.create_session(account["account_id"])
api_key = accounts.create_api_key(account["account_id"])
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
if server.billing is not None:
server.billing.ensure_client(api_key) # grant Caller Credit up front
print(
f"[tracker] account registered: {account.get('email') or account.get('wallet')} "
f"role={account['role']}", flush=True,
)
self._send_json(200, {"account": account, "session_token": token, "api_key": api_key})
def _handle_auth_login(self):
accounts = self._require_accounts()
if accounts is None:
return
body = self._read_json_body()
if body is None:
return
identifier = body.get("identifier") or body.get("email") or body.get("wallet") or ""
account = accounts.verify_login(str(identifier), str(body.get("password") or ""))
if account is None:
self._send_json(401, {"error": "invalid credentials"})
return
token = accounts.create_session(account["account_id"])
self._send_json(200, {"account": account, "session_token": token})
def _handle_auth_logout(self):
accounts = self._require_accounts()
if accounts is None:
return
accounts.destroy_session(_api_key_from_headers(self.headers))
self._send_json(200, {"ok": True})
def _handle_account_me(self):
"""Balance, usage, and API keys for the logged-in account."""
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
if self._require_accounts() is None:
return
account = self._session_account()
if account is None:
self._send_json(401, {"error": "login required"})
return
keys = server.accounts.keys_for(account["account_id"]) # type: ignore[union-attr]
balances = {}
usage: dict = {"requests": 0, "total_tokens": 0, "total_cost": 0.0, "recent": []}
if server.billing is not None:
balances = {key: server.billing.get_client_balance(key) for key in keys}
usage = server.billing.usage_for(keys)
self._send_json(200, {
"account": account,
"api_keys": keys,
"balances": balances,
"total_balance": sum(balances.values()),
"usage": usage,
})
def _handle_account_key_create(self):
accounts = self._require_accounts()
if accounts is None:
return
account = self._session_account()
if account is None:
self._send_json(401, {"error": "login required"})
return
api_key = accounts.create_api_key(account["account_id"])
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
if server.billing is not None:
server.billing.ensure_client(api_key)
self._send_json(200, {"api_key": api_key})
def _handle_account_key_revoke(self):
accounts = self._require_accounts()
if accounts is None:
return
account = self._session_account()
if account is None:
self._send_json(401, {"error": "login required"})
return
body = self._read_json_body()
if body is None:
return
api_key = body.get("api_key")
if not api_key or not isinstance(api_key, str):
self._send_json(400, {"error": "api_key is required"})
return
if not accounts.revoke_api_key(account["account_id"], api_key):
self._send_json(404, {"error": "key not found on this account"})
return
self._send_json(200, {"revoked": api_key})
def _handle_admin_accounts(self):
"""Admin-only: all accounts with their keys and balances."""
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
accounts = self._require_accounts()
if accounts is None:
return
account = self._session_account()
if account is None:
self._send_json(401, {"error": "login required"})
return
if account["role"] != "admin":
self._send_json(403, {"error": "admin role required"})
return
listing = accounts.list_accounts()
if server.billing is not None:
for entry in listing:
entry["balances"] = {
key: server.billing.get_client_balance(key)
for key in entry.get("api_keys", [])
}
self._send_json(200, {"accounts": listing})
def _handle_accounts_gossip(self):
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
body = self._read_json_body()
if body is None:
return
if server.accounts is None:
self._send_json(200, {"applied": 0})
return
events = body.get("events")
if not isinstance(events, list):
self._send_json(400, {"error": "events must be a list"})
return
applied = server.accounts.apply_events([e for e in events if isinstance(e, dict)])
self._send_json(200, {"applied": applied})
def _handle_wallet_register(self):
"""Bind the caller's client wallet pubkey to their API key (US-032).
@@ -2996,7 +3187,10 @@ class TrackerServer:
stats_db: str | None = None,
relay_url: str | None = None,
billing: BillingLedger | None = None,
enable_billing: bool = False,
billing_db: str | None = None,
accounts: AccountStore | None = None,
accounts_db: str | None = None,
benchmark_results_path: str | None = None,
treasury: Any | None = None,
deposit_poll_interval: float = 15.0,
@@ -3028,15 +3222,27 @@ class TrackerServer:
self._stats: _StatsCollector | None = _StatsCollector(db_path=stats_db) if stats_db else _StatsCollector()
self._stats_stop = threading.Event()
self._stats_thread: threading.Thread | None = None
if billing is None and billing_db:
preset_prices = {
name: float(preset["price_per_1k_tokens"])
for name, preset in self._model_presets.items()
if isinstance(preset, dict) and "price_per_1k_tokens" in preset
}
billing = BillingLedger(db_path=billing_db, prices=preset_prices)
if billing is None:
db_path = billing_db
if db_path is None and enable_billing:
db_path = DEFAULT_BILLING_DB_PATH
if db_path:
preset_prices = {
name: float(preset["price_per_1k_tokens"])
for name, preset in self._model_presets.items()
if isinstance(preset, dict) and "price_per_1k_tokens" in preset
}
billing = BillingLedger(db_path=db_path, prices=preset_prices)
self._billing: BillingLedger | None = billing
self._billing_gossip_cursor = 0
if accounts is None:
accounts_path = accounts_db
if accounts_path is None and enable_billing:
accounts_path = DEFAULT_ACCOUNTS_DB_PATH
if accounts_path:
accounts = AccountStore(db_path=accounts_path)
self._accounts: AccountStore | None = accounts
self._accounts_gossip_cursor = 0
self._benchmark_results_path = benchmark_results_path
self._treasury = treasury
self._deposit_poll_interval = deposit_poll_interval
@@ -3074,6 +3280,7 @@ class TrackerServer:
relay_url=effective_relay_url,
stats=self._stats,
billing=self._billing,
accounts=self._accounts,
benchmark_results_path=self._benchmark_results_path,
)
self.port = self._server.server_address[1]
@@ -3250,6 +3457,8 @@ class TrackerServer:
self._stats.save_to_db()
if self._billing is not None:
self._billing.save_to_db()
if self._accounts is not None:
self._accounts.save_to_db()
if self._stats is not None and self._cluster_peers:
self_url = self._cluster_self_url or f"http://{self._host}:{self.port}"
local_rpms = self._stats.get_local_rpms()
@@ -3288,6 +3497,25 @@ class TrackerServer:
# via event-id dedupe on the receiving side).
if delivered_all:
self._billing_gossip_cursor = cursor
if self._accounts is not None and self._cluster_peers:
events, cursor = self._accounts.events_since(self._accounts_gossip_cursor)
if events:
delivered_all = True
body = json.dumps({"events": events}).encode()
for peer in self._cluster_peers:
try:
req = urllib.request.Request(
f"{peer}/v1/accounts/gossip",
data=body,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=2.0) as r:
r.read()
except Exception:
delivered_all = False
if delivered_all:
self._accounts_gossip_cursor = cursor
def stop(self) -> None:
if self._raft is not None:
@@ -3304,6 +3532,8 @@ class TrackerServer:
self._stats.save_to_db()
if self._billing is not None:
self._billing.save_to_db()
if self._accounts is not None:
self._accounts.save_to_db()
self._server.shutdown()
self._server.server_close()
if self._thread is not None: