From 51a98a0c6314511712a283dc58626c85bddfc0aa Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Fri, 5 Jun 2026 23:03:40 +0200 Subject: [PATCH 1/2] fix: add missing APIRouter import and None-safety in instance_service - Import APIRouter from fastapi (NameError on module load) - Add None check after session.get(ToolType) to prevent AttributeError - Type-annotate volume_mounts and guard extend() with isinstance(list) Quality gates: py_compile pass, LSP clean --- apps/api/src/services/tool/instance_service.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/api/src/services/tool/instance_service.py b/apps/api/src/services/tool/instance_service.py index c5f6f20..bfb8ab4 100644 --- a/apps/api/src/services/tool/instance_service.py +++ b/apps/api/src/services/tool/instance_service.py @@ -7,9 +7,10 @@ import os import subprocess import uuid from datetime import datetime +from typing import Any import httpx -from fastapi import HTTPException, status +from fastapi import APIRouter, HTTPException, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -97,12 +98,12 @@ async def resolve_git_mounts( results = await asyncio.gather(*tasks, return_exceptions=True) - volume_mounts = [] + volume_mounts: list[dict[str, Any]] = [] for result in results: if isinstance(result, Exception): logger.warning("Git mount failed: %s", result) continue - if result: + if isinstance(result, list): volume_mounts.extend(result) return volume_mounts @@ -758,6 +759,9 @@ async def prepare_manifest_instance( from src.models import ToolDefinitionManifest tool_type = await session.get(ToolType, instance.tool_type_id) + if not tool_type: + raise RuntimeError(f"Tool type {instance.tool_type_id} not found") + manifest_def = await session.get(ToolDefinitionManifest, tool_type.manifest_id) if not manifest_def: From a388a8bec9fe7959b4ead8ff2c6dd922fe1dda7e Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Fri, 5 Jun 2026 23:11:08 +0200 Subject: [PATCH 2/2] fix: move router to api layer and add missing imports/guards - Move APIRouter definition from instance_service.py back to tool_instances.py (service files should not define FastAPI routers) - Add missing prepare_manifest_instance import in tool_instances.py - Guard repo.remote_url before clone_repository call - Guard tool_type.compose_template before render_compose_template call - Rename subprocess result variable to avoid shadowing SQLAlchemy Result - Build error message as local string to avoid None/bool type issues Quality gates: py_compile pass, LSP clean --- apps/api/src/api/tool/tool_instances.py | 26 ++++++++++++++----- .../api/src/services/tool/instance_service.py | 7 +---- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/apps/api/src/api/tool/tool_instances.py b/apps/api/src/api/tool/tool_instances.py index 255e914..f82803e 100644 --- a/apps/api/src/api/tool/tool_instances.py +++ b/apps/api/src/api/tool/tool_instances.py @@ -92,6 +92,7 @@ from src.services.tool.instance_service import ( expand_glob_source, modify_compose_file, normalize_git_mount, + prepare_manifest_instance, pull_repository_updates, resolve_git_mount_mappings, resolve_git_mounts, @@ -104,6 +105,8 @@ from src.services.tool.instance_service import ( logger = logging.getLogger(__name__) _event_bus = InstanceEventBus() +router = APIRouter(prefix="/projects", tags=["tool-instances"]) + @router.post( "/{project_id}/repositories/{repo_id}/instances", @@ -257,6 +260,11 @@ async def create_instance( ssh_key_path = os.path.join(ssh_dir, "id_ed25519") # Clone repository + if not repo.remote_url: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Repository has no remote URL configured", + ) clone_path = clone_repository( remote_url=repo.remote_url, ssh_key_path=ssh_key_path, @@ -298,16 +306,16 @@ async def create_instance( # Create new local branch if requested if data.clone_mode == "clone" and data.new_branch: try: - result = subprocess.run( + git_result = subprocess.run( ["git", "-C", repo_path, "checkout", "-b", data.new_branch], capture_output=True, text=True, ) - if result.returncode != 0: + if git_result.returncode != 0: logger.error( - "Failed to create branch %s: %s", data.new_branch, result.stderr + "Failed to create branch %s: %s", data.new_branch, git_result.stderr ) - raise RuntimeError(f"Failed to create branch: {result.stderr}") + raise RuntimeError(f"Failed to create branch: {git_result.stderr}") logger.debug( "Created local branch %s in cloned repository", data.new_branch ) @@ -421,6 +429,11 @@ services: "USER_ID": str(user_id), "PROJECT_ID": str(project_id), } + if not tool_type.compose_template: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Tool type has no compose template configured", + ) compose_content = render_compose_template( tool_type.compose_template, variables ) @@ -1912,9 +1925,10 @@ async def check_instance_tunnel_health( # If container is not running, override error message if not container_healthy: - response["error"] = f"Container is {container_info['status']}" + error_msg = f"Container is {container_info['status']}" if container_info["exit_code"] is not None: - response["error"] += f" (exit code: {container_info['exit_code']})" + error_msg += f" (exit code: {container_info['exit_code']})" + response["error"] = error_msg return response diff --git a/apps/api/src/services/tool/instance_service.py b/apps/api/src/services/tool/instance_service.py index bfb8ab4..c2bf3ac 100644 --- a/apps/api/src/services/tool/instance_service.py +++ b/apps/api/src/services/tool/instance_service.py @@ -71,6 +71,7 @@ from src.auth.dependencies import _get_owned_project, _get_user logger = logging.getLogger(__name__) _event_bus = InstanceEventBus() + async def resolve_git_mounts( session: AsyncSession, resolved: ResolvedProfile, @@ -399,9 +400,6 @@ def expand_glob_source(source_path: str, repo_path: str) -> list[str]: return results -router = APIRouter(prefix="/projects", tags=["tool-instances"]) - - async def validate_config_profile( session: AsyncSession, profile_id: str | None, @@ -741,7 +739,6 @@ def ensure_backend_network_in_compose(compose_path: str) -> None: logger.info("Injected backend network '%s' into compose file", network_name) - async def prepare_manifest_instance( session: AsyncSession, instance: ToolInstance, @@ -882,5 +879,3 @@ async def prepare_manifest_instance( home_dir = get_manifest_home_dir(manifest) return image_tag, compose_content, manifest, home_dir - -