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:
11
README.md
11
README.md
@@ -179,10 +179,11 @@ When enabled, the API publishes the node's registry to `castle/{hostname}/regist
|
|||||||
|----------|-------------|
|
|----------|-------------|
|
||||||
| `GET /health` | Health check |
|
| `GET /health` | Health check |
|
||||||
| `GET /stream` | SSE stream (health, service-action, mesh events) |
|
| `GET /stream` | SSE stream (health, service-action, mesh events) |
|
||||||
| **Programs & Components** | |
|
| **Programs** | |
|
||||||
| `GET /programs` | List all programs |
|
| `GET /programs` | List all programs (`?behavior=tool\|daemon\|frontend` to filter) |
|
||||||
| `GET /programs/{name}` | Program detail |
|
| `GET /programs/{name}` | Program detail |
|
||||||
| `GET /components` | Unified view of all components (`?include_remote=true` for cross-node) |
|
| `POST /programs/{name}/{action}` | Run a lifecycle action (build, test, lint, install, etc.) |
|
||||||
|
| `GET /components` | Unified view across nodes (`?include_remote=true` for cross-node) |
|
||||||
| `GET /components/{name}` | Component detail |
|
| `GET /components/{name}` | Component detail |
|
||||||
| **Services** | |
|
| **Services** | |
|
||||||
| `GET /services` | List all services with status |
|
| `GET /services` | List all services with status |
|
||||||
@@ -206,10 +207,6 @@ When enabled, the API publishes the node's registry to `castle/{hostname}/regist
|
|||||||
| `PUT /config/services/{name}` | Update a service entry |
|
| `PUT /config/services/{name}` | Update a service entry |
|
||||||
| `PUT /config/jobs/{name}` | Update a job entry |
|
| `PUT /config/jobs/{name}` | Update a job entry |
|
||||||
| `POST /config/apply` | Apply config changes (deploy + reload) |
|
| `POST /config/apply` | Apply config changes (deploy + reload) |
|
||||||
| **Tools** | |
|
|
||||||
| `GET /tools` | List all tools |
|
|
||||||
| `GET /tools/{name}` | Tool detail |
|
|
||||||
| `POST /programs/{name}/{action}` | Run a program action (build, test, lint, install) |
|
|
||||||
| **Secrets** | |
|
| **Secrets** | |
|
||||||
| `GET /secrets` | List secrets |
|
| `GET /secrets` | List secrets |
|
||||||
| `GET /secrets/{name}` | Read a secret |
|
| `GET /secrets/{name}` | Read a secret |
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
import { Terminal } from "lucide-react"
|
import { Terminal } from "lucide-react"
|
||||||
import type { ToolSummary } from "@/types"
|
import type { ProgramSummary } from "@/types"
|
||||||
import { runnerLabel } from "@/lib/labels"
|
import { runnerLabel } from "@/lib/labels"
|
||||||
|
|
||||||
interface ToolCardProps {
|
interface ToolCardProps {
|
||||||
tool: ToolSummary
|
tool: ProgramSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolCard({ tool }: ToolCardProps) {
|
export function ToolCard({ tool }: ToolCardProps) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useParams } from "react-router-dom"
|
import { useParams } from "react-router-dom"
|
||||||
import { useProgram, useEventStream, useToolDetail } from "@/services/api/hooks"
|
import { useProgram, useEventStream } from "@/services/api/hooks"
|
||||||
import { runnerLabel } from "@/lib/labels"
|
import { runnerLabel } from "@/lib/labels"
|
||||||
import { DetailHeader } from "@/components/detail/DetailHeader"
|
import { DetailHeader } from "@/components/detail/DetailHeader"
|
||||||
import { ConfigPanel } from "@/components/detail/ConfigPanel"
|
import { ConfigPanel } from "@/components/detail/ConfigPanel"
|
||||||
@@ -11,7 +11,6 @@ export function ComponentDetailPage() {
|
|||||||
const { name } = useParams<{ name: string }>()
|
const { name } = useParams<{ name: string }>()
|
||||||
const { data: component, isLoading, error, refetch } = useProgram(name ?? "")
|
const { data: component, isLoading, error, refetch } = useProgram(name ?? "")
|
||||||
const isTool = component?.behavior === "tool"
|
const isTool = component?.behavior === "tool"
|
||||||
const { data: toolDetail } = useToolDetail(isTool ? (name ?? "") : "")
|
|
||||||
const [actionOutput, setActionOutput] = useState<ActionOutput | null>(null)
|
const [actionOutput, setActionOutput] = useState<ActionOutput | null>(null)
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -52,7 +51,7 @@ export function ComponentDetailPage() {
|
|||||||
<p className="text-sm text-[var(--muted)] -mt-4 mb-6">{component.description}</p>
|
<p className="text-sm text-[var(--muted)] -mt-4 mb-6">{component.description}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{toolDetail && (
|
{isTool && (
|
||||||
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-5 mb-6">
|
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-5 mb-6">
|
||||||
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider mb-1">
|
<h2 className="text-sm font-semibold text-[var(--muted)] uppercase tracking-wider mb-1">
|
||||||
Tool Info
|
Tool Info
|
||||||
@@ -61,32 +60,32 @@ export function ComponentDetailPage() {
|
|||||||
How this tool is packaged and what it depends on.
|
How this tool is packaged and what it depends on.
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-2 gap-x-6 gap-y-2 text-sm mb-4">
|
<div className="grid grid-cols-2 gap-x-6 gap-y-2 text-sm mb-4">
|
||||||
{toolDetail.source && (
|
{component.source && (
|
||||||
<>
|
<>
|
||||||
<span className="text-[var(--muted)]">Source</span>
|
<span className="text-[var(--muted)]">Source</span>
|
||||||
<span className="font-mono">{toolDetail.source}</span>
|
<span className="font-mono">{component.source}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{toolDetail.version && (
|
{component.version && (
|
||||||
<>
|
<>
|
||||||
<span className="text-[var(--muted)]">Version</span>
|
<span className="text-[var(--muted)]">Version</span>
|
||||||
<span>{toolDetail.version}</span>
|
<span>{component.version}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{toolDetail.runner && (
|
{component.runner && (
|
||||||
<>
|
<>
|
||||||
<span className="text-[var(--muted)]">Runner</span>
|
<span className="text-[var(--muted)]">Runner</span>
|
||||||
<span>{runnerLabel(toolDetail.runner)}</span>
|
<span>{runnerLabel(component.runner)}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<span className="text-[var(--muted)]">Installed</span>
|
<span className="text-[var(--muted)]">Installed</span>
|
||||||
<span>{component.installed ? "Yes" : "No"}</span>
|
<span>{component.installed ? "Yes" : "No"}</span>
|
||||||
</div>
|
</div>
|
||||||
{toolDetail.system_dependencies.length > 0 && (
|
{component.system_dependencies.length > 0 && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<span className="text-sm text-[var(--muted)] block mb-1">System Dependencies</span>
|
<span className="text-sm text-[var(--muted)] block mb-1">System Dependencies</span>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{toolDetail.system_dependencies.map((dep) => (
|
{component.system_dependencies.map((dep) => (
|
||||||
<span
|
<span
|
||||||
key={dep}
|
key={dep}
|
||||||
className="text-xs px-2 py-0.5 rounded bg-amber-900/30 text-amber-400 border border-amber-800"
|
className="text-xs px-2 py-0.5 rounded bg-amber-900/30 text-amber-400 border border-amber-800"
|
||||||
@@ -97,14 +96,6 @@ export function ComponentDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{toolDetail.docs && (
|
|
||||||
<div>
|
|
||||||
<span className="text-sm text-[var(--muted)] block mb-1">Documentation</span>
|
|
||||||
<pre className="text-sm whitespace-pre-wrap bg-[var(--background)] rounded p-3 border border-[var(--border)]">
|
|
||||||
{toolDetail.docs}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
import { ArrowLeft } from "lucide-react"
|
import { ArrowLeft } from "lucide-react"
|
||||||
import { useTools } from "@/services/api/hooks"
|
import { usePrograms } from "@/services/api/hooks"
|
||||||
import { ToolCard } from "@/components/ToolCard"
|
import { ToolCard } from "@/components/ToolCard"
|
||||||
|
|
||||||
export function ToolsPage() {
|
export function ToolsPage() {
|
||||||
const { data: tools, isLoading } = useTools()
|
const { data: tools, isLoading } = usePrograms("tool")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto px-6 py-8">
|
<div className="max-w-6xl mx-auto px-6 py-8">
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ import type {
|
|||||||
MeshStatus,
|
MeshStatus,
|
||||||
NodeSummary,
|
NodeSummary,
|
||||||
NodeDetail,
|
NodeDetail,
|
||||||
ToolSummary,
|
|
||||||
ToolDetail,
|
|
||||||
} from "@/types"
|
} from "@/types"
|
||||||
|
|
||||||
// Legacy compat hook — used by ConfigEditorPage and ComponentRedirect
|
// Legacy compat hook — used by ConfigEditorPage and ComponentRedirect
|
||||||
@@ -59,10 +57,11 @@ export function useJob(name: string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePrograms() {
|
export function usePrograms(behavior?: string) {
|
||||||
|
const params = behavior ? `?behavior=${behavior}` : ""
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["programs"],
|
queryKey: ["programs", behavior ?? "all"],
|
||||||
queryFn: () => apiClient.get<ProgramSummary[]>("/programs"),
|
queryFn: () => apiClient.get<ProgramSummary[]>(`/programs${params}`),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,21 +186,6 @@ export function useMeshStatus() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTools() {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["tools"],
|
|
||||||
queryFn: () => apiClient.get<ToolSummary[]>("/tools"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useToolDetail(name: string) {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["tools", name],
|
|
||||||
queryFn: () => apiClient.get<ToolDetail>(`/tools/${name}`),
|
|
||||||
enabled: !!name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useEventStream() {
|
export function useEventStream() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
|
||||||
|
|||||||
@@ -152,16 +152,4 @@ export interface MeshStatus {
|
|||||||
peers: string[]
|
peers: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolSummary {
|
|
||||||
id: string
|
|
||||||
description: string | null
|
|
||||||
source: string | null
|
|
||||||
version: string | null
|
|
||||||
runner: string | null
|
|
||||||
system_dependencies: string[]
|
|
||||||
installed: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToolDetail extends ToolSummary {
|
|
||||||
docs: string | null
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from castle_api.stream import (
|
|||||||
unsubscribe,
|
unsubscribe,
|
||||||
)
|
)
|
||||||
from castle_api.nodes import router as nodes_router
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -120,7 +120,6 @@ app.include_router(logs_router)
|
|||||||
app.include_router(nodes_router)
|
app.include_router(nodes_router)
|
||||||
app.include_router(secrets_router)
|
app.include_router(secrets_router)
|
||||||
app.include_router(services_router)
|
app.include_router(services_router)
|
||||||
app.include_router(tools_router)
|
|
||||||
app.include_router(programs_router)
|
app.include_router(programs_router)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -174,21 +174,3 @@ class ServiceActionResponse(BaseModel):
|
|||||||
component: str
|
component: str
|
||||||
action: str
|
action: str
|
||||||
status: 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)."""
|
"""Build a ComponentSummary from a ServiceSpec (non-deployed)."""
|
||||||
port = None
|
port = None
|
||||||
health_path = None
|
health_path = None
|
||||||
@@ -90,7 +92,9 @@ def _summary_from_service(name: str, svc: ServiceSpec, config: object) -> Compon
|
|||||||
if managed:
|
if managed:
|
||||||
unit_name = f"castle-{name}.service"
|
unit_name = f"castle-{name}.service"
|
||||||
unit_path = str(Path("~/.config/systemd/user") / unit_name)
|
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
|
description = svc.description
|
||||||
source = None
|
source = None
|
||||||
@@ -226,9 +230,7 @@ def _service_from_deployed(name: str, deployed: object) -> ServiceSummary:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _service_from_spec(
|
def _service_from_spec(name: str, svc: ServiceSpec, config: object) -> ServiceSummary:
|
||||||
name: str, svc: ServiceSpec, config: object
|
|
||||||
) -> ServiceSummary:
|
|
||||||
"""Build a ServiceSummary from a ServiceSpec."""
|
"""Build a ServiceSummary from a ServiceSpec."""
|
||||||
port = None
|
port = None
|
||||||
health_path = None
|
health_path = None
|
||||||
@@ -268,9 +270,7 @@ def _service_from_spec(
|
|||||||
|
|
||||||
def _job_from_deployed(name: str, deployed: object) -> JobSummary:
|
def _job_from_deployed(name: str, deployed: object) -> JobSummary:
|
||||||
"""Build a JobSummary from a DeployedComponent."""
|
"""Build a JobSummary from a DeployedComponent."""
|
||||||
systemd_info = (
|
systemd_info = _make_systemd_info(name, timer=True) if deployed.managed else None
|
||||||
_make_systemd_info(name, timer=True) if deployed.managed else None
|
|
||||||
)
|
|
||||||
return JobSummary(
|
return JobSummary(
|
||||||
id=name,
|
id=name,
|
||||||
description=deployed.description,
|
description=deployed.description,
|
||||||
@@ -553,8 +553,11 @@ def get_job(name: str) -> JobDetail:
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/programs", response_model=list[ProgramSummary], tags=["programs"])
|
@router.get("/programs", response_model=list[ProgramSummary], tags=["programs"])
|
||||||
def list_programs() -> list[ProgramSummary]:
|
def list_programs(behavior: str | None = None) -> list[ProgramSummary]:
|
||||||
"""List all programs from the software catalog (castle.yaml programs section)."""
|
"""List all programs from the software catalog (castle.yaml programs section).
|
||||||
|
|
||||||
|
Optionally filter by behavior: daemon, tool, or frontend.
|
||||||
|
"""
|
||||||
root = get_castle_root()
|
root = get_castle_root()
|
||||||
if not root:
|
if not root:
|
||||||
return []
|
return []
|
||||||
@@ -573,6 +576,8 @@ def list_programs() -> list[ProgramSummary]:
|
|||||||
summary = _program_from_spec(name, comp, root, config)
|
summary = _program_from_spec(name, comp, root, config)
|
||||||
if summary.behavior is None:
|
if summary.behavior is None:
|
||||||
continue
|
continue
|
||||||
|
if behavior and summary.behavior != behavior:
|
||||||
|
continue
|
||||||
summary.node = hostname
|
summary.node = hostname
|
||||||
summaries.append(summary)
|
summaries.append(summary)
|
||||||
|
|
||||||
@@ -840,11 +845,16 @@ async def reload_gateway() -> dict[str, str]:
|
|||||||
# Include remote registries for cross-node routing
|
# Include remote registries for cross-node routing
|
||||||
remote_regs = {h: n.registry for h, n in mesh_state.all_nodes().items()}
|
remote_regs = {h: n.registry for h, n in mesh_state.all_nodes().items()}
|
||||||
caddyfile_path.write_text(
|
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(
|
proc = await asyncio.create_subprocess_exec(
|
||||||
"systemctl", "--user", "reload", "castle-castle-gateway.service",
|
"systemctl",
|
||||||
|
"--user",
|
||||||
|
"reload",
|
||||||
|
"castle-castle-gateway.service",
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,88 +1,15 @@
|
|||||||
"""Tools router and program actions."""
|
"""Program action endpoints."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import shutil
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, status
|
from fastapi import APIRouter, HTTPException, status
|
||||||
|
|
||||||
from castle_core.manifest import ProgramSpec
|
|
||||||
from castle_core.stacks import available_actions, get_handler
|
from castle_core.stacks import available_actions, get_handler
|
||||||
|
|
||||||
from castle_api.config import get_config
|
from castle_api.config import get_config
|
||||||
from castle_api.models import ToolDetail, ToolSummary
|
|
||||||
|
|
||||||
router = APIRouter(tags=["tools"])
|
|
||||||
programs_router = APIRouter(tags=["programs"])
|
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
|
# Unified program action endpoint
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user