diff --git a/apps/api/src/services/tool/instance_service.py b/apps/api/src/services/tool/instance_service.py index 1771893..39a368a 100644 --- a/apps/api/src/services/tool/instance_service.py +++ b/apps/api/src/services/tool/instance_service.py @@ -204,7 +204,12 @@ def clone_git_repo( """ import hashlib - url_hash = hashlib.md5(remote_url.encode()).hexdigest()[:12] + # 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] 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 = os.path.join(clone_dir, "repo-clone") @@ -303,8 +308,20 @@ def resolve_git_mount_mappings( # Single match: mount directly to target_path mount_target = final_target else: - # Multiple matches: append relative path to target - rel_path = os.path.relpath(matched_path, repo_path) + # Multiple matches: append the path relative to the glob's base + # directory so `packages/*` → `/app/packages` yields + # `/app/packages/api` instead of `/app/packages/packages/api`. + first_glob_idx = min( + (source_path.find(c) for c in "*?[" if c in source_path), + default=len(source_path), + ) + base_relative = os.path.dirname(source_path[: first_glob_idx + 1]) + base_full = ( + os.path.join(repo_path, base_relative) + if base_relative + else repo_path + ) + rel_path = os.path.relpath(matched_path, base_full) mount_target = os.path.join(final_target, rel_path) volume_mounts.append( @@ -357,7 +374,14 @@ async def resolve_single_git_mount( repo_path = await asyncio.to_thread( clone_git_repo, remote_url, branch, instance_dir ) - except Exception: + except Exception as exc: + logger.warning( + "Git mount clone failed for %s (branch=%s): %s", + remote_url, + branch, + exc, + exc_info=True, + ) return [] # Resolve all mappings from the cloned repo @@ -1241,7 +1265,7 @@ 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 = "/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: @@ -1259,16 +1283,22 @@ async def start_tool_instance( deep_merge(dict(base_def.manifest), manifest) ) home_dir = get_manifest_home_dir(manifest) + runtime = manifest.get("runtime", {}) + if runtime.get("working_dir"): + working_directory = expand_container_path( + runtime["working_dir"], home_dir + ) user_cfg = manifest.get("user") if user_cfg: container_uid = user_cfg.get("uid", 0) container_gid = user_cfg.get("gid", 0) logger.debug( - "Manifest user resolved for instance %s: uid=%s, gid=%s, home=%s", + "Manifest user resolved for instance %s: uid=%s, gid=%s, home=%s, working_directory=%s", instance.id, container_uid, container_gid, home_dir, + working_directory, ) # Apply selected config profile if any @@ -1281,6 +1311,16 @@ async def start_tool_instance( profile_env, profile_files, profile_mounts, profile_hints = ( apply_resolved_profile(instance_dir, resolved, home_dir) ) + # Profile hints override the manifest/tool defaults, and git mounts + # need the final working directory to resolve relative target paths. + if profile_hints.get("start_command"): + start_command = profile_hints["start_command"] + if profile_hints.get("working_directory"): + working_directory = expand_container_path( + profile_hints["working_directory"], home_dir + ) + if profile_hints.get("port_override"): + port_override = profile_hints["port_override"] env_vars.update(profile_env) config_files.update(profile_files) extra_volumes.extend(profile_mounts) @@ -1288,12 +1328,6 @@ async def start_tool_instance( session, resolved, instance_dir, working_directory, home_dir ) extra_volumes.extend(git_mount_volumes) - if profile_hints.get("start_command"): - start_command = profile_hints["start_command"] - if profile_hints.get("working_directory"): - working_directory = profile_hints["working_directory"] - if profile_hints.get("port_override"): - port_override = profile_hints["port_override"] logger.debug( "Applied config profile %s to instance %s (env=%d, files=%d, mounts=%d, git_mounts=%d)", resolved.profile_name,