refactor: Remove dedicated /tools endpoints, use /programs?behavior=tool instead
Eliminates inconsistency where tools had their own filtered view but daemons and frontends didn't. Changes: - API: Removed tools router and ToolSummary/ToolDetail models. Added optional behavior query parameter to GET /programs for filtering by program type (tool, daemon, frontend). - Frontend: Updated hooks to pass behavior param to usePrograms instead of separate useTools. Updated components to use ProgramSummary type. Removed useToolDetail hook. - Docs: Updated API documentation to reflect program behavior filtering.
This commit is contained in:
@@ -25,7 +25,7 @@ from castle_api.stream import (
|
||||
unsubscribe,
|
||||
)
|
||||
from castle_api.nodes import router as nodes_router
|
||||
from castle_api.tools import programs_router, router as tools_router
|
||||
from castle_api.tools import programs_router
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -120,7 +120,6 @@ app.include_router(logs_router)
|
||||
app.include_router(nodes_router)
|
||||
app.include_router(secrets_router)
|
||||
app.include_router(services_router)
|
||||
app.include_router(tools_router)
|
||||
app.include_router(programs_router)
|
||||
|
||||
|
||||
|
||||
@@ -174,21 +174,3 @@ class ServiceActionResponse(BaseModel):
|
||||
component: str
|
||||
action: str
|
||||
status: str
|
||||
|
||||
|
||||
class ToolSummary(BaseModel):
|
||||
"""Summary of a single tool."""
|
||||
|
||||
id: str
|
||||
description: str | None = None
|
||||
source: str | None = None
|
||||
version: str | None = None
|
||||
runner: str | None = None
|
||||
system_dependencies: list[str] = []
|
||||
installed: bool = False
|
||||
|
||||
|
||||
class ToolDetail(ToolSummary):
|
||||
"""Full detail for a single tool, including documentation."""
|
||||
|
||||
docs: str | None = None
|
||||
|
||||
@@ -73,7 +73,9 @@ def _summary_from_deployed(name: str, deployed: object) -> ComponentSummary:
|
||||
)
|
||||
|
||||
|
||||
def _summary_from_service(name: str, svc: ServiceSpec, config: object) -> ComponentSummary:
|
||||
def _summary_from_service(
|
||||
name: str, svc: ServiceSpec, config: object
|
||||
) -> ComponentSummary:
|
||||
"""Build a ComponentSummary from a ServiceSpec (non-deployed)."""
|
||||
port = None
|
||||
health_path = None
|
||||
@@ -90,7 +92,9 @@ def _summary_from_service(name: str, svc: ServiceSpec, config: object) -> Compon
|
||||
if managed:
|
||||
unit_name = f"castle-{name}.service"
|
||||
unit_path = str(Path("~/.config/systemd/user") / unit_name)
|
||||
systemd_info = SystemdInfo(unit_name=unit_name, unit_path=unit_path, timer=False)
|
||||
systemd_info = SystemdInfo(
|
||||
unit_name=unit_name, unit_path=unit_path, timer=False
|
||||
)
|
||||
|
||||
description = svc.description
|
||||
source = None
|
||||
@@ -226,9 +230,7 @@ def _service_from_deployed(name: str, deployed: object) -> ServiceSummary:
|
||||
)
|
||||
|
||||
|
||||
def _service_from_spec(
|
||||
name: str, svc: ServiceSpec, config: object
|
||||
) -> ServiceSummary:
|
||||
def _service_from_spec(name: str, svc: ServiceSpec, config: object) -> ServiceSummary:
|
||||
"""Build a ServiceSummary from a ServiceSpec."""
|
||||
port = None
|
||||
health_path = None
|
||||
@@ -268,9 +270,7 @@ def _service_from_spec(
|
||||
|
||||
def _job_from_deployed(name: str, deployed: object) -> JobSummary:
|
||||
"""Build a JobSummary from a DeployedComponent."""
|
||||
systemd_info = (
|
||||
_make_systemd_info(name, timer=True) if deployed.managed else None
|
||||
)
|
||||
systemd_info = _make_systemd_info(name, timer=True) if deployed.managed else None
|
||||
return JobSummary(
|
||||
id=name,
|
||||
description=deployed.description,
|
||||
@@ -553,8 +553,11 @@ def get_job(name: str) -> JobDetail:
|
||||
|
||||
|
||||
@router.get("/programs", response_model=list[ProgramSummary], tags=["programs"])
|
||||
def list_programs() -> list[ProgramSummary]:
|
||||
"""List all programs from the software catalog (castle.yaml programs section)."""
|
||||
def list_programs(behavior: str | None = None) -> list[ProgramSummary]:
|
||||
"""List all programs from the software catalog (castle.yaml programs section).
|
||||
|
||||
Optionally filter by behavior: daemon, tool, or frontend.
|
||||
"""
|
||||
root = get_castle_root()
|
||||
if not root:
|
||||
return []
|
||||
@@ -573,6 +576,8 @@ def list_programs() -> list[ProgramSummary]:
|
||||
summary = _program_from_spec(name, comp, root, config)
|
||||
if summary.behavior is None:
|
||||
continue
|
||||
if behavior and summary.behavior != behavior:
|
||||
continue
|
||||
summary.node = hostname
|
||||
summaries.append(summary)
|
||||
|
||||
@@ -840,11 +845,16 @@ async def reload_gateway() -> dict[str, str]:
|
||||
# Include remote registries for cross-node routing
|
||||
remote_regs = {h: n.registry for h, n in mesh_state.all_nodes().items()}
|
||||
caddyfile_path.write_text(
|
||||
generate_caddyfile_from_registry(registry, remote_registries=remote_regs or None)
|
||||
generate_caddyfile_from_registry(
|
||||
registry, remote_registries=remote_regs or None
|
||||
)
|
||||
)
|
||||
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"systemctl", "--user", "reload", "castle-castle-gateway.service",
|
||||
"systemctl",
|
||||
"--user",
|
||||
"reload",
|
||||
"castle-castle-gateway.service",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
|
||||
@@ -1,88 +1,15 @@
|
||||
"""Tools router and program actions."""
|
||||
"""Program action endpoints."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
|
||||
from castle_core.manifest import ProgramSpec
|
||||
from castle_core.stacks import available_actions, get_handler
|
||||
|
||||
from castle_api.config import get_config
|
||||
from castle_api.models import ToolDetail, ToolSummary
|
||||
|
||||
router = APIRouter(tags=["tools"])
|
||||
programs_router = APIRouter(tags=["programs"])
|
||||
|
||||
|
||||
def _is_tool(comp: ProgramSpec) -> bool:
|
||||
"""Check if a component is a tool."""
|
||||
return comp.behavior == "tool"
|
||||
|
||||
|
||||
def _tool_summary(
|
||||
name: str, comp: ProgramSpec, root: Path | None = None
|
||||
) -> ToolSummary:
|
||||
"""Build a ToolSummary from a ProgramSpec that is a tool."""
|
||||
installed = shutil.which(name) is not None
|
||||
|
||||
# Infer runner from source directory
|
||||
runner = None
|
||||
source = comp.source
|
||||
if source:
|
||||
source_dir = Path(source)
|
||||
if (source_dir / "pyproject.toml").exists():
|
||||
runner = "python"
|
||||
elif source_dir.is_file():
|
||||
runner = "command"
|
||||
|
||||
return ToolSummary(
|
||||
id=name,
|
||||
description=comp.description,
|
||||
source=source,
|
||||
version=comp.version,
|
||||
runner=runner,
|
||||
system_dependencies=comp.system_dependencies,
|
||||
installed=installed,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tools", response_model=list[ToolSummary])
|
||||
def list_tools() -> list[ToolSummary]:
|
||||
"""List all registered tools (requires repo access)."""
|
||||
config = get_config()
|
||||
tools = {k: v for k, v in config.programs.items() if _is_tool(v)}
|
||||
|
||||
return sorted(
|
||||
[_tool_summary(name, comp, config.root) for name, comp in tools.items()],
|
||||
key=lambda t: t.id,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tools/{name}", response_model=ToolDetail)
|
||||
def get_tool(name: str) -> ToolDetail:
|
||||
"""Get detailed info for a single tool."""
|
||||
config = get_config()
|
||||
|
||||
if name not in config.programs:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Component '{name}' not found",
|
||||
)
|
||||
|
||||
comp = config.programs[name]
|
||||
if not _is_tool(comp):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"'{name}' is not a tool",
|
||||
)
|
||||
|
||||
summary = _tool_summary(name, comp, config.root)
|
||||
return ToolDetail(**summary.model_dump())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Unified program action endpoint
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user