fix: resolve project variable in create_tool_instance

create_tool_instance fetched `project` only inside the auto-display-name
branch, so callers supplying display_name left it unbound and the
manifest/dockerfile/compose branches crashed with UnboundLocalError at
_get_repository_mount_name (in-container layout /home/user/{project_name}).

Resolve project unconditionally after repo validation with a not-found
guard (narrowing Project|None -> Project), and drop the now-redundant
fetch from the auto-name branch. Add a regression test covering the
manifest + display_name path.

Quality gates: ruff, mypy, pytest (74 unit tests passed)
This commit is contained in:
2026-06-17 16:12:22 +02:00
parent 3e59a257dc
commit 1c6dbcede8
2 changed files with 140 additions and 20 deletions
+41 -16
View File
@@ -76,7 +76,6 @@ from src.services.shared.readiness_probe import execute_probe
from src.services.shared.ssh_keys import prepare_ssh_key_files
from src.services.instance.event_bus import InstanceEventBus
from src.services.instance.lifecycle_hooks import publish_lifecycle_event
from src.utils.git_url_parser import extract_base_repo_url
logger = logging.getLogger(__name__)
_event_bus = InstanceEventBus()
@@ -281,9 +280,7 @@ def clone_git_repo(
# Include the branch in the hash so different branches of the same repo
# get separate clone directories and cannot race each other.
branch_segment = branch or "default"
url_hash = hashlib.md5(
f"{remote_url}:{branch_segment}".encode()
).hexdigest()[:12]
url_hash = hashlib.md5(f"{remote_url}:{branch_segment}".encode()).hexdigest()[:12]
repo_name = remote_url.split("/")[-1].replace(".git", "") or "repo"
clone_dir = os.path.join(clone_parent, "git-mounts", f"{repo_name}-{url_hash}")
repo_path = clone_repository(
@@ -1028,7 +1025,9 @@ async def prepare_manifest_instance(
workspace: Workspace | None = None
if instance.workspace_id:
workspace = await session.get(Workspace, instance.workspace_id)
project = await session.get(Project, instance.project_id) if instance.project_id else None
project = (
await session.get(Project, instance.project_id) if instance.project_id else None
)
repo_name = (
_get_repository_mount_name(project, repo, workspace)
if project and repo
@@ -1087,6 +1086,13 @@ async def create_tool_instance(
if repo is None or repo.project_id != project_id:
raise ValueError("repository not found")
# Resolve the project up front: it is needed both for auto-generated display
# names and for the in-container mount name (/home/user/{project_name}),
# regardless of the tool definition type below.
project = await session.get(Project, project_id)
if project is None:
raise ValueError("project not found")
tool_type_id = uuid.UUID(data.tool_type_id)
tool_type = await session.get(ToolType, tool_type_id)
if tool_type is None:
@@ -1120,7 +1126,6 @@ async def create_tool_instance(
if data.display_name:
instance_display = data.display_name
else:
project = await session.get(Project, project_id)
project_name = project.name if project else "Unknown"
scope_name = workspace.name if workspace else repo.name
auto_name = f"{project_name} / {scope_name} / {tool_type.display_name}"
@@ -1348,7 +1353,9 @@ async def start_tool_instance(
# Fetch tool type early to determine home directory and container user
tool_type = await session.get(ToolType, instance.tool_type_id)
home_dir = tool_type.home_directory if tool_type and tool_type.home_directory else "/root"
home_dir = (
tool_type.home_directory if tool_type and tool_type.home_directory else "/root"
)
container_uid = 0
container_gid = 0
if tool_type and tool_type.definition_type == "manifest" and tool_type.manifest_id:
@@ -1583,8 +1590,16 @@ async def start_tool_instance(
else:
# ── LEGACY FLOW ──────────────────────────────────────────
if instance.clone_mode == "clone" and not instance.workspace_id:
clone_project = await session.get(Project, instance.project_id) if instance.project_id else None
clone_name = _slugify_directory_name(clone_project.name) if clone_project else "repo-clone"
clone_project = (
await session.get(Project, instance.project_id)
if instance.project_id
else None
)
clone_name = (
_slugify_directory_name(clone_project.name)
if clone_project
else "repo-clone"
)
repo_path = os.path.join(instance_dir, clone_name)
repo = await session.get(GitRepository, instance.repository_id)
@@ -1613,11 +1628,13 @@ async def start_tool_instance(
)
# Ensure the legacy compose mounts the project-named clone directory.
extra_volumes.append({
"source": repo_path,
"target": f"{home_dir}/{clone_name}",
"type": "bind",
})
extra_volumes.append(
{
"source": repo_path,
"target": f"{home_dir}/{clone_name}",
"type": "bind",
}
)
if port_override or start_command or working_directory or extra_volumes:
modify_compose_file(
@@ -2124,8 +2141,16 @@ async def delete_tool_instance(
os.path.dirname(instance.compose_path) if instance.compose_path else None
)
if instance_dir:
clone_project = await session.get(Project, instance.project_id) if instance.project_id else None
clone_name = _slugify_directory_name(clone_project.name) if clone_project else "repo-clone"
clone_project = (
await session.get(Project, instance.project_id)
if instance.project_id
else None
)
clone_name = (
_slugify_directory_name(clone_project.name)
if clone_project
else "repo-clone"
)
clone_path = os.path.join(instance_dir, clone_name)
if not os.path.exists(clone_path):
clone_path = os.path.join(instance_dir, "repo-clone")