feat(cli): castle mesh command + shared-config API + NATS integration tests

- castle mesh status|nodes|config (list/get/set) — hits the local castle-api;
  surfaces the shared-config write path (authority-gated)
- API: GET/PUT /mesh/config/{key:path}, GET /mesh/config; nats_client gains
  list_shared_config
- tests: NATS integration suite against a real broker via a docker fixture
  (peer discovery, presence, graceful-offline, shared config, secrets-off-wire);
  plus HTTP-layer /mesh/config tests. Closes the runtime-coverage gap.
- AGENTS.md §8 updated for NATS + the mesh CLI

100 api + 211 core tests pass.
This commit is contained in:
2026-07-07 06:13:57 -07:00
parent ae4b19f8d4
commit 835f3f94eb
8 changed files with 373 additions and 5 deletions

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
from fastapi import APIRouter, HTTPException, Request, status
from pydantic import BaseModel
from castle_api.config import get_registry, settings
from castle_api.mesh import mesh_state
@@ -11,6 +12,42 @@ from castle_api.models import DeploymentSummary, MeshStatus, NodeDetail, NodeSum
router = APIRouter(tags=["nodes"])
class ConfigValue(BaseModel):
value: str
@router.get("/mesh/config")
async def list_mesh_config(request: Request) -> dict:
"""List shared-config keys + this node's role (only the authority may write)."""
client = getattr(request.app.state, "nats_client", None)
if client is None:
return {"keys": [], "role": get_registry().node.role}
return {"keys": await client.list_shared_config(), "role": client.role}
@router.get("/mesh/config/{key:path}")
async def get_mesh_config(key: str, request: Request) -> dict:
"""Read a shared-config value."""
client = getattr(request.app.state, "nats_client", None)
value = await client.get_shared_config(key) if client else None
if value is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, f"config key '{key}' not set")
return {"key": key, "value": value}
@router.put("/mesh/config/{key:path}")
async def set_mesh_config(key: str, body: ConfigValue, request: Request) -> dict:
"""Write a shared-config value (authority only)."""
client = getattr(request.app.state, "nats_client", None)
if client is None:
raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE, "mesh not enabled")
try:
await client.put_shared_config(key, body.value)
except PermissionError as exc:
raise HTTPException(status.HTTP_403_FORBIDDEN, str(exc)) from exc
return {"key": key, "ok": True}
def _local_node_summary(registry: object) -> NodeSummary:
"""Build a NodeSummary for the local node from the registry."""
return NodeSummary(