feat: add fail-closed Hub access profile foundation
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / pytest-smoke (push) Failing after 3s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e747-8f27-7242-8df8-8bc44f88c929
This commit is contained in:
tegwick 2026-09-28 11:44:50 +02:00
parent df39fd5f43
commit 3e386147fd
35 changed files with 2009 additions and 195 deletions

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import json
from typing import Any
from collections.abc import Callable
import httpx
from fastmcp import FastMCP
@ -57,8 +58,14 @@ class HubCoreMCPServer:
api_base: str,
instructions: str | None = None,
register_tools: bool = True,
token_provider: Callable[[], str] | None = None,
require_credentials: bool = False,
trailing_slash: bool = True,
) -> None:
self.api_base = api_base.rstrip("/")
self.token_provider = token_provider
self.require_credentials = require_credentials
self.trailing_slash = trailing_slash
self.mcp = FastMCP(
name=name,
instructions=instructions or "Generic FOS hub MCP server.",
@ -503,40 +510,51 @@ class HubCoreMCPServer:
try:
with self._client() as client:
response = client.get(
normalize_trailing_slash(path),
normalize_trailing_slash(path, trailing=self.trailing_slash),
params=self._clean(params or {}),
)
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as exc:
return {"error": f"API {exc.response.status_code}: {exc.response.text[:300]}"}
except Exception as exc:
return {"error": f"Request failed: {exc}"}
return {"error": f"API {exc.response.status_code}"}
except Exception:
return {"error": "Request failed"}
def _post(self, path: str, body: dict[str, Any]) -> Any:
try:
with self._client() as client:
response = client.post(normalize_trailing_slash(path), json=self._clean(body))
response = client.post(normalize_trailing_slash(path, trailing=self.trailing_slash), json=self._clean(body))
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as exc:
return {"error": f"API {exc.response.status_code}: {exc.response.text[:300]}"}
except Exception as exc:
return {"error": f"Request failed: {exc}"}
return {"error": f"API {exc.response.status_code}"}
except Exception:
return {"error": "Request failed"}
def _patch(self, path: str, body: dict[str, Any]) -> Any:
try:
with self._client() as client:
response = client.patch(normalize_trailing_slash(path), json=self._clean(body))
response = client.patch(normalize_trailing_slash(path, trailing=self.trailing_slash), json=self._clean(body))
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as exc:
return {"error": f"API {exc.response.status_code}: {exc.response.text[:300]}"}
except Exception as exc:
return {"error": f"Request failed: {exc}"}
return {"error": f"API {exc.response.status_code}"}
except Exception:
return {"error": "Request failed"}
def _client(self) -> httpx.Client:
return httpx.Client(base_url=self.api_base, timeout=30.0, follow_redirects=True)
# The host resolves a Hub-audience credential from the current invocation.
# Never retain it on the MCP server or fall back to a shared root token.
headers = {}
if self.token_provider is not None:
token = self.token_provider()
if not token or any(c.isspace() for c in token):
raise ValueError("current invocation has no Hub credential")
headers["Authorization"] = f"Bearer {token}"
elif self.require_credentials:
raise ValueError("MCP host must provide a current Hub credential")
return httpx.Client(base_url=self.api_base, timeout=30.0,
headers=headers, follow_redirects=not bool(headers))
@staticmethod
def _clean(data: dict[str, Any]) -> dict[str, Any]: