FrameBridge 0.3.1 community source release
This commit is contained in:
commit
b23819fff1
1354 files changed
+343716
No files matched your search
@@ -0,0 +1,42 @@
|
||||
"""Separate loopback-only MCP entry point for the six Fleet READ tools."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
from framebridge_node_gateway import discover_nodes, node_read
|
||||
from framebridge_linux_chatgpt.fleet_hub import FleetHub
|
||||
from framebridge_linux_chatgpt.fleet_mcp_surface import register_fleet_read_tools
|
||||
|
||||
|
||||
def _server_address() -> tuple[str, int]:
|
||||
host = os.environ.get("FRAMEBRIDGE_FLEET_HUB_HOST", "127.0.0.1")
|
||||
if not isinstance(host, str) or host not in ("127.0.0.1", "::1"):
|
||||
raise ValueError("FLEET_HUB_HOST_INVALID")
|
||||
try:
|
||||
port = int(os.environ.get("FRAMEBRIDGE_FLEET_HUB_PORT", "8877"))
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError("FLEET_HUB_PORT_INVALID") from None
|
||||
if not 1024 <= port <= 65535:
|
||||
raise ValueError("FLEET_HUB_PORT_INVALID")
|
||||
return host, port
|
||||
|
||||
|
||||
host, port = _server_address()
|
||||
mcp = FastMCP(
|
||||
"FrameBridge Fleet Hub",
|
||||
host=host,
|
||||
port=port,
|
||||
streamable_http_path="/mcp",
|
||||
)
|
||||
hub = FleetHub(discover_nodes, node_read)
|
||||
register_fleet_read_tools(mcp, hub)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
mcp.run(transport="streamable-http")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+1
@@ -0,0 +1 @@
|
||||
"""FrameBridge Fleet Hub runtime."""
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
"""Injected Node Gateway READ boundary; no transport or live gateway imports."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
from .fleet_registry import FleetNode
|
||||
from .fleet_routing import Operation, ReadRequest, normalize_read_args
|
||||
|
||||
|
||||
class GatewayReadError(ValueError):
|
||||
"""Fixed local error codes; remote exceptions are never copied."""
|
||||
|
||||
|
||||
class GatewayReadAdapter:
|
||||
def __init__(self, gateway_node_read: Callable[[str, str, dict], dict]) -> None:
|
||||
if not callable(gateway_node_read):
|
||||
raise GatewayReadError("FLEET_GATEWAY_CONFIG_INVALID")
|
||||
self._gateway_node_read = gateway_node_read
|
||||
|
||||
def __call__(self, node: FleetNode, request: ReadRequest) -> dict:
|
||||
if type(node) is not FleetNode or type(request) is not ReadRequest:
|
||||
raise GatewayReadError("FLEET_GATEWAY_REQUEST_INVALID")
|
||||
if request.node_id != node.node_id:
|
||||
raise GatewayReadError("FLEET_GATEWAY_NODE_MISMATCH")
|
||||
if request.operation is not Operation.READ or not node.online:
|
||||
raise GatewayReadError("FLEET_GATEWAY_READ_BLOCKED")
|
||||
try:
|
||||
args = normalize_read_args(request.capability, request.args)
|
||||
except ValueError:
|
||||
raise GatewayReadError("FLEET_GATEWAY_REQUEST_INVALID") from None
|
||||
try:
|
||||
response = self._gateway_node_read(node.alias, request.capability.value, args)
|
||||
except Exception:
|
||||
raise GatewayReadError("FLEET_GATEWAY_READ_FAILED") from None
|
||||
if type(response) is not dict or response.get("ok") is not True:
|
||||
raise GatewayReadError("FLEET_GATEWAY_READ_FAILED")
|
||||
for key in ("node", "alias"):
|
||||
if key in response and (type(response[key]) is not str
|
||||
or response[key] != node.alias):
|
||||
raise GatewayReadError("FLEET_GATEWAY_ALIAS_MISMATCH")
|
||||
# Metadata is payload only. It cannot update identity, routing or policy.
|
||||
return response
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
"""Synchronous, in-memory Fleet READ hub using injected discovery and gateway."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
from .fleet_gateway_adapter import GatewayReadAdapter
|
||||
from .fleet_registry import FleetNode, FleetRegistry, RegistryError
|
||||
from .fleet_routing import FleetRequest, FleetRouter, RouteResult
|
||||
|
||||
|
||||
class FleetHubError(ValueError):
|
||||
"""Sanitized discovery/configuration failure."""
|
||||
|
||||
|
||||
class FleetHub:
|
||||
def __init__(self, discover_nodes: Callable[[], dict],
|
||||
gateway_node_read: Callable[[str, str, dict], dict]) -> None:
|
||||
if not callable(discover_nodes):
|
||||
raise FleetHubError("FLEET_HUB_CONFIG_INVALID")
|
||||
self._discover_nodes = discover_nodes
|
||||
self._adapter = GatewayReadAdapter(gateway_node_read)
|
||||
registry = FleetRegistry()
|
||||
self._snapshot = (registry, FleetRouter(registry, self._adapter))
|
||||
|
||||
def refresh(self) -> tuple[FleetNode, ...]:
|
||||
"""Publish only a complete valid registry; failures preserve the old one.
|
||||
|
||||
Entries with explicit pairing state must be paired. Legacy entries need
|
||||
an explicit stable ID too. Missing descriptive metadata stays unknown;
|
||||
it never supplies identity or readiness. Invalid entries are omitted,
|
||||
but duplicate IDs/aliases and cross-namespace collisions abort refresh.
|
||||
"""
|
||||
try:
|
||||
discovery = self._discover_nodes()
|
||||
if (type(discovery) is not dict or discovery.get("ok") is not True
|
||||
or type(discovery.get("nodes")) is not list):
|
||||
raise FleetHubError("FLEET_REFRESH_FAILED")
|
||||
registry = FleetRegistry()
|
||||
for entry in discovery["nodes"]:
|
||||
if type(entry) is not dict:
|
||||
continue
|
||||
if "pairing_state" in entry and entry["pairing_state"] != "paired":
|
||||
continue
|
||||
try:
|
||||
node = FleetNode(
|
||||
node_id=entry.get("node_id"), alias=entry.get("alias"),
|
||||
hostname=entry.get("hostname", "unknown"),
|
||||
transport=entry.get("transport", "unknown"),
|
||||
capability_profile=entry.get("capability_profile", "unknown"),
|
||||
online=entry.get("operational_transport_ready") is True,
|
||||
)
|
||||
except RegistryError:
|
||||
continue
|
||||
registry.register(node)
|
||||
router = FleetRouter(registry, self._adapter)
|
||||
except Exception:
|
||||
raise FleetHubError("FLEET_REFRESH_FAILED") from None
|
||||
self._snapshot = (registry, router)
|
||||
return registry.list_nodes()
|
||||
|
||||
def read(self, target: str, capability: str, args: dict | None = None) -> RouteResult:
|
||||
return self._snapshot[1].route(FleetRequest(target, capability, args=args))
|
||||
|
||||
def nodes(self) -> tuple[FleetNode, ...]:
|
||||
return self._snapshot[0].list_nodes()
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
"""Register exactly six READ tools on an injected MCP-compatible registrar."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
import json
|
||||
|
||||
from .fleet_hub import FleetHub
|
||||
from .fleet_routing import RouteResult
|
||||
|
||||
|
||||
_ANNOTATIONS = {
|
||||
"readOnlyHint": True,
|
||||
"destructiveHint": False,
|
||||
"idempotentHint": True,
|
||||
"openWorldHint": False,
|
||||
}
|
||||
|
||||
|
||||
def _public_data(value: object) -> object:
|
||||
"""Copy JSON-only data and omit inert context even in nested gateway payloads."""
|
||||
if value is None or type(value) in (str, bool, int, float):
|
||||
return value
|
||||
if type(value) is list:
|
||||
return [_public_data(item) for item in value]
|
||||
if type(value) is dict and all(type(key) is str for key in value):
|
||||
return {key: _public_data(item) for key, item in value.items()
|
||||
if key != "context_text"}
|
||||
raise ValueError("FLEET_RESPONSE_INVALID")
|
||||
|
||||
|
||||
def _serializable(value: dict) -> dict:
|
||||
# Reject nonfinite numbers as well as custom objects; no repr/str fallback.
|
||||
return json.loads(json.dumps(_public_data(value), allow_nan=False))
|
||||
|
||||
|
||||
def register_fleet_read_tools(mcp, hub: FleetHub) -> None:
|
||||
def read(node: str, capability: str, args: dict | None = None) -> dict:
|
||||
try:
|
||||
hub.refresh()
|
||||
except Exception:
|
||||
return {"ok": False, "error": "FLEET_REFRESH_FAILED"}
|
||||
try:
|
||||
result = hub.read(node, capability, args=args)
|
||||
if type(result) is not RouteResult or result.ok is not True:
|
||||
return {"ok": False, "error": "FLEET_READ_FAILED"}
|
||||
return _serializable({"ok": True, "node_id": result.node_id,
|
||||
"capability": result.capability, "data": result.data})
|
||||
except Exception:
|
||||
return {"ok": False, "error": "FLEET_READ_FAILED"}
|
||||
|
||||
@mcp.tool(annotations=dict(_ANNOTATIONS))
|
||||
def framebridge_fleet_nodes() -> dict:
|
||||
"""Refresh and list Fleet nodes with stable identities and readiness."""
|
||||
try:
|
||||
hub.refresh()
|
||||
return _serializable({"ok": True, "nodes": [asdict(node) for node in hub.nodes()]})
|
||||
except Exception:
|
||||
return {"ok": False, "error": "FLEET_NODES_FAILED"}
|
||||
|
||||
@mcp.tool(annotations=dict(_ANNOTATIONS))
|
||||
def framebridge_fleet_node_system_status(node: str) -> dict:
|
||||
"""Read system status for a Fleet node ID or alias."""
|
||||
return read(node, "system.status")
|
||||
|
||||
@mcp.tool(annotations=dict(_ANNOTATIONS))
|
||||
def framebridge_fleet_node_hardware_summary(node: str) -> dict:
|
||||
"""Read hardware summary for a Fleet node ID or alias."""
|
||||
return read(node, "hardware.summary")
|
||||
|
||||
@mcp.tool(annotations=dict(_ANNOTATIONS))
|
||||
def framebridge_fleet_node_storage_inspect(node: str) -> dict:
|
||||
"""Inspect storage for a Fleet node ID or alias."""
|
||||
return read(node, "storage.inspect")
|
||||
|
||||
@mcp.tool(annotations=dict(_ANNOTATIONS))
|
||||
def framebridge_fleet_node_network_inspect(node: str) -> dict:
|
||||
"""Inspect networking for a Fleet node ID or alias."""
|
||||
return read(node, "network.inspect")
|
||||
|
||||
@mcp.tool(annotations=dict(_ANNOTATIONS))
|
||||
def framebridge_fleet_node_service_status(node: str, service: str) -> dict:
|
||||
"""Read status of one validated service for a Fleet node ID or alias."""
|
||||
return read(node, "service.status", {"service": service})
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
"""In-memory Fleet registry. Identity is explicit; no discovery or transport I/O."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, replace
|
||||
import re
|
||||
|
||||
|
||||
_IDENTIFIER = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,127}")
|
||||
|
||||
|
||||
class RegistryError(ValueError):
|
||||
"""Stable error code only; never includes supplied metadata."""
|
||||
|
||||
|
||||
def _identifier(value: object) -> bool:
|
||||
return type(value) is str and _IDENTIFIER.fullmatch(value) is not None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FleetNode:
|
||||
node_id: str
|
||||
alias: str
|
||||
hostname: str
|
||||
transport: str
|
||||
capability_profile: str
|
||||
online: bool = False
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not _identifier(self.node_id) or not _identifier(self.alias):
|
||||
raise RegistryError("FLEET_IDENTIFIER_INVALID")
|
||||
for value in (self.hostname, self.transport, self.capability_profile):
|
||||
if (type(value) is not str or not value or len(value) > 253
|
||||
or any(ord(char) < 32 or ord(char) == 127 for char in value)):
|
||||
raise RegistryError("FLEET_METADATA_INVALID")
|
||||
if type(self.online) is not bool:
|
||||
raise RegistryError("FLEET_STATE_INVALID")
|
||||
|
||||
|
||||
class FleetRegistry:
|
||||
"""Case-sensitive identifiers, sorted by node_id; offline unless specified.
|
||||
|
||||
Aliases and IDs share one namespace to prevent ambiguous destination lookup.
|
||||
Metadata cannot grant capabilities. Instances are for synchronous local use.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._nodes: dict[str, FleetNode] = {}
|
||||
self._aliases: dict[str, str] = {}
|
||||
|
||||
def register(self, node: FleetNode) -> FleetNode:
|
||||
if type(node) is not FleetNode:
|
||||
raise RegistryError("FLEET_NODE_INVALID")
|
||||
if node.node_id in self._nodes:
|
||||
raise RegistryError("FLEET_DUPLICATE_NODE_ID")
|
||||
if node.alias in self._aliases:
|
||||
raise RegistryError("FLEET_DUPLICATE_ALIAS")
|
||||
if node.node_id in self._aliases or node.alias in self._nodes:
|
||||
raise RegistryError("FLEET_IDENTIFIER_COLLISION")
|
||||
self._nodes[node.node_id] = node
|
||||
self._aliases[node.alias] = node.node_id
|
||||
return node
|
||||
|
||||
def list_nodes(self) -> tuple[FleetNode, ...]:
|
||||
return tuple(self._nodes[key] for key in sorted(self._nodes))
|
||||
|
||||
def resolve(self, target: str) -> FleetNode:
|
||||
if not _identifier(target):
|
||||
raise RegistryError("FLEET_IDENTIFIER_INVALID")
|
||||
node_id = target if target in self._nodes else self._aliases.get(target)
|
||||
if node_id is None:
|
||||
raise RegistryError("FLEET_NODE_NOT_FOUND")
|
||||
return self._nodes[node_id]
|
||||
|
||||
def set_online(self, target: str, online: bool) -> FleetNode:
|
||||
node = replace(self.resolve(target), online=online)
|
||||
self._nodes[node.node_id] = node
|
||||
return node
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
"""Typed READ dispatch only. The injected adapter is the sole execution boundary.
|
||||
|
||||
No default adapter, shell, SSH, sockets, plan engine, or authority from text.
|
||||
The embedding application must supply a trusted read-only adapter; this module
|
||||
does not sandbox arbitrary Python callbacks or authenticate remote callers.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import re
|
||||
from types import MappingProxyType
|
||||
from typing import Callable, Mapping
|
||||
|
||||
from .fleet_registry import FleetNode, FleetRegistry, RegistryError
|
||||
|
||||
|
||||
class Operation(Enum):
|
||||
READ = "READ"
|
||||
WRITE = "WRITE"
|
||||
PLAN = "PLAN"
|
||||
APPLY = "APPLY"
|
||||
ROLLBACK = "ROLLBACK"
|
||||
EXEC = "EXEC"
|
||||
|
||||
|
||||
class ReadCapability(Enum):
|
||||
SYSTEM_STATUS = "system.status"
|
||||
HARDWARE_SUMMARY = "hardware.summary"
|
||||
STORAGE_INSPECT = "storage.inspect"
|
||||
NETWORK_INSPECT = "network.inspect"
|
||||
SERVICE_STATUS = "service.status"
|
||||
|
||||
|
||||
READ_ALLOWLIST = frozenset(item.value for item in ReadCapability)
|
||||
_FORBIDDEN_EXEC = frozenset({"framebridge_exec", "shell", "bash", "exec"})
|
||||
_SERVICE_NAME = re.compile(r"[A-Za-z0-9_.@:-]{1,128}")
|
||||
|
||||
|
||||
def normalize_read_args(capability: ReadCapability, args: object) -> dict[str, str]:
|
||||
"""Return a fresh, closed-schema dictionary; never interpret supplied text."""
|
||||
if type(capability) is not ReadCapability:
|
||||
raise ValueError("FLEET_ARGS_INVALID")
|
||||
if args is None:
|
||||
args = {}
|
||||
if type(args) not in (dict, MappingProxyType):
|
||||
raise ValueError("FLEET_ARGS_INVALID")
|
||||
if capability is ReadCapability.SERVICE_STATUS:
|
||||
if (set(args) != {"service"} or type(args["service"]) is not str
|
||||
or _SERVICE_NAME.fullmatch(args["service"]) is None):
|
||||
raise ValueError("FLEET_ARGS_INVALID")
|
||||
return {"service": args["service"]}
|
||||
if args:
|
||||
raise ValueError("FLEET_ARGS_INVALID")
|
||||
return {}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FleetRequest:
|
||||
target: str
|
||||
capability: str
|
||||
operation: Operation = Operation.READ
|
||||
context_text: str = "" # Inert text; never forwarded to an adapter.
|
||||
args: Mapping[str, str] | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
# Snapshot caller dictionaries before dispatch, including read-only views.
|
||||
# Invalid inputs remain available for the router's fixed error result.
|
||||
if type(self.args) in (dict, MappingProxyType):
|
||||
object.__setattr__(self, "args", MappingProxyType(dict(self.args)))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReadRequest:
|
||||
node_id: str
|
||||
capability: ReadCapability
|
||||
operation: Operation = Operation.READ
|
||||
args: Mapping[str, str] | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
object.__setattr__(self, "args", MappingProxyType(
|
||||
normalize_read_args(self.capability, self.args)))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RouteResult:
|
||||
ok: bool
|
||||
error: str | None = None
|
||||
node_id: str | None = None
|
||||
capability: str | None = None
|
||||
data: object = None
|
||||
|
||||
|
||||
class FleetRouter:
|
||||
def __init__(self, registry: FleetRegistry,
|
||||
adapter: Callable[[FleetNode, ReadRequest], object]) -> None:
|
||||
if not isinstance(registry, FleetRegistry) or not callable(adapter):
|
||||
raise ValueError("FLEET_ROUTER_CONFIG_INVALID")
|
||||
self._registry = registry
|
||||
self._adapter = adapter
|
||||
|
||||
def route(self, request: FleetRequest) -> RouteResult:
|
||||
if type(request) is not FleetRequest:
|
||||
return RouteResult(False, "FLEET_REQUEST_INVALID")
|
||||
# Enum identity deliberately rejects strings and foreign enum values.
|
||||
if request.operation is not Operation.READ:
|
||||
return RouteResult(False, "FLEET_OPERATION_BLOCKED")
|
||||
if type(request.capability) is not str or type(request.context_text) is not str:
|
||||
return RouteResult(False, "FLEET_REQUEST_INVALID")
|
||||
if request.capability.strip().casefold() in _FORBIDDEN_EXEC:
|
||||
return RouteResult(False, "FLEET_EXEC_FORBIDDEN")
|
||||
if request.capability not in READ_ALLOWLIST:
|
||||
return RouteResult(False, "FLEET_CAPABILITY_NOT_ALLOWED")
|
||||
try:
|
||||
args = normalize_read_args(ReadCapability(request.capability), request.args)
|
||||
except ValueError:
|
||||
return RouteResult(False, "FLEET_ARGS_INVALID")
|
||||
try:
|
||||
node = self._registry.resolve(request.target)
|
||||
except RegistryError as exc:
|
||||
return RouteResult(False, str(exc))
|
||||
if not node.online:
|
||||
return RouteResult(False, "FLEET_NODE_OFFLINE", node.node_id)
|
||||
typed = ReadRequest(node.node_id, ReadCapability(request.capability), args=args)
|
||||
try:
|
||||
data = self._adapter(node, typed)
|
||||
except Exception:
|
||||
return RouteResult(False, "FLEET_ADAPTER_FAILED", node.node_id,
|
||||
request.capability)
|
||||
return RouteResult(True, node_id=node.node_id,
|
||||
capability=request.capability, data=data)
|
||||
+1308
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user