63ae706dd0
Add support for tool categories, interface types, and per-tool configuration. Backend: - Add category and interfaces fields to ToolType model - Create ToolConfig model for storing tool-specific settings - Add tool_configs API endpoints (CRUD) - Update built-in tool types with categories and interfaces: - code-server: editor, [web] - jupyter-notebook: notebook, [web] - opencode: ai-assistant, [terminal] - Update instance API to include tool type interfaces - Create Alembic migrations 0008 and 0009 Frontend: - Update ToolType and Session interfaces with new fields - Conditionally show Open/Terminal buttons based on tool interfaces - Add API client for tool configs OpenSpec: tool-config-management change created and implemented.
174 lines
5.8 KiB
Python
174 lines
5.8 KiB
Python
"""Tool configuration API endpoints."""
|
|
|
|
import logging
|
|
import uuid
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from src.auth.dependencies import get_current_user_id, get_db_session
|
|
from src.models.tool_config import ToolConfig
|
|
from src.models.tool_type import ToolType
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/tool-configs", tags=["tool-configs"])
|
|
|
|
|
|
class ToolConfigCreate(BaseModel):
|
|
tool_type_id: str = Field(description="UUID of the tool type")
|
|
project_id: str | None = Field(default=None, description="Optional project ID for project-scoped config")
|
|
key: str = Field(description="Config key name")
|
|
value: str = Field(description="Config value")
|
|
config_type: str = Field(default="env", description="Type: env or file")
|
|
file_path: str | None = Field(default=None, description="File path for file-type configs")
|
|
|
|
|
|
class ToolConfigResponse(BaseModel):
|
|
id: str
|
|
tool_type_id: str
|
|
project_id: str | None
|
|
key: str
|
|
value: str
|
|
config_type: str
|
|
file_path: str | None
|
|
|
|
|
|
@router.get("", summary="List tool configs", description="Get all tool configs for the current user.")
|
|
async def list_configs(
|
|
tool_type_id: str | None = None,
|
|
project_id: str | None = None,
|
|
user_id: uuid.UUID = Depends(get_current_user_id),
|
|
session: AsyncSession = Depends(get_db_session),
|
|
) -> dict:
|
|
"""List tool configs for the current user."""
|
|
query = select(ToolConfig).where(ToolConfig.user_id == user_id)
|
|
|
|
if tool_type_id:
|
|
query = query.where(ToolConfig.tool_type_id == uuid.UUID(tool_type_id))
|
|
if project_id:
|
|
query = query.where(ToolConfig.project_id == uuid.UUID(project_id))
|
|
else:
|
|
# If no project specified, get only global configs (project_id is None)
|
|
query = query.where(ToolConfig.project_id.is_(None))
|
|
|
|
result = await session.execute(query)
|
|
configs = result.scalars().all()
|
|
|
|
return {
|
|
"configs": [
|
|
{
|
|
"id": str(c.id),
|
|
"tool_type_id": str(c.tool_type_id),
|
|
"project_id": str(c.project_id) if c.project_id else None,
|
|
"key": c.key,
|
|
"value": c.value,
|
|
"config_type": c.config_type,
|
|
"file_path": c.file_path,
|
|
}
|
|
for c in configs
|
|
]
|
|
}
|
|
|
|
|
|
@router.post("", summary="Create tool config", description="Create a new tool config.")
|
|
async def create_config(
|
|
data: ToolConfigCreate,
|
|
user_id: uuid.UUID = Depends(get_current_user_id),
|
|
session: AsyncSession = Depends(get_db_session),
|
|
) -> dict:
|
|
"""Create a tool config."""
|
|
# Verify tool type exists
|
|
tool_type = await session.get(ToolType, uuid.UUID(data.tool_type_id))
|
|
if tool_type is None:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="tool type not found")
|
|
|
|
# Check for existing config with same key
|
|
query = select(ToolConfig).where(
|
|
ToolConfig.user_id == user_id,
|
|
ToolConfig.tool_type_id == uuid.UUID(data.tool_type_id),
|
|
ToolConfig.key == data.key,
|
|
)
|
|
if data.project_id:
|
|
query = query.where(ToolConfig.project_id == uuid.UUID(data.project_id))
|
|
else:
|
|
query = query.where(ToolConfig.project_id.is_(None))
|
|
|
|
existing = await session.scalar(query)
|
|
if existing:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail=f"config with key '{data.key}' already exists"
|
|
)
|
|
|
|
config = ToolConfig(
|
|
user_id=user_id,
|
|
tool_type_id=uuid.UUID(data.tool_type_id),
|
|
project_id=uuid.UUID(data.project_id) if data.project_id else None,
|
|
key=data.key,
|
|
value=data.value,
|
|
config_type=data.config_type,
|
|
file_path=data.file_path,
|
|
)
|
|
session.add(config)
|
|
await session.commit()
|
|
await session.refresh(config)
|
|
|
|
return {
|
|
"id": str(config.id),
|
|
"tool_type_id": str(config.tool_type_id),
|
|
"project_id": str(config.project_id) if config.project_id else None,
|
|
"key": config.key,
|
|
"value": config.value,
|
|
"config_type": config.config_type,
|
|
"file_path": config.file_path,
|
|
}
|
|
|
|
|
|
@router.put("/{config_id}", summary="Update tool config", description="Update an existing tool config.")
|
|
async def update_config(
|
|
config_id: uuid.UUID,
|
|
data: ToolConfigCreate,
|
|
user_id: uuid.UUID = Depends(get_current_user_id),
|
|
session: AsyncSession = Depends(get_db_session),
|
|
) -> dict:
|
|
"""Update a tool config."""
|
|
config = await session.get(ToolConfig, config_id)
|
|
if config is None or config.user_id != user_id:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="config not found")
|
|
|
|
config.key = data.key
|
|
config.value = data.value
|
|
config.config_type = data.config_type
|
|
config.file_path = data.file_path
|
|
|
|
await session.commit()
|
|
await session.refresh(config)
|
|
|
|
return {
|
|
"id": str(config.id),
|
|
"tool_type_id": str(config.tool_type_id),
|
|
"project_id": str(config.project_id) if config.project_id else None,
|
|
"key": config.key,
|
|
"value": config.value,
|
|
"config_type": config.config_type,
|
|
"file_path": config.file_path,
|
|
}
|
|
|
|
|
|
@router.delete("/{config_id}", summary="Delete tool config", description="Delete a tool config.")
|
|
async def delete_config(
|
|
config_id: uuid.UUID,
|
|
user_id: uuid.UUID = Depends(get_current_user_id),
|
|
session: AsyncSession = Depends(get_db_session),
|
|
) -> None:
|
|
"""Delete a tool config."""
|
|
config = await session.get(ToolConfig, config_id)
|
|
if config is None or config.user_id != user_id:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="config not found")
|
|
|
|
await session.delete(config)
|
|
await session.commit()
|