fix: remove explicit repo mount from pi-agent manifest and derive workspace name from remote URL

The pi-agent manifest still declared an explicit repo mount with
{{WORKSPACE_NAME}}, making the mount target dependent on tool config. The
instance service now synthesizes the repo mount, so the manifest no longer
needs the explicit mount.

- Add Alembic migration 2026_06_15_090500 to remove the source_type: repo
  mount from the built-in pi-agent manifest
- Add _get_repository_mount_name() helper to derive the workspace directory
  name from the repository remote URL (matching git clone behavior) and
  fall back to the user-provided repository name
- Use the helper for WORKSPACE_NAME/REPO_NAME in manifest, legacy dockerfile,
  and legacy compose template paths
- Update unit tests for the new migration and helper

Quality gates:
- pytest tests/unit: 218 passed
- ruff: clean on changed files
- mypy: clean on changed files
- alembic heads: single head
This commit is contained in:
Developer
2026-06-15 09:10:05 +00:00
parent f0ae9483f3
commit 6e33e8e4e9
29 changed files with 245 additions and 62 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api/src
## role
Core application package for the Headquarter API, providing configuration, database connectivity, structured logging, and FastAPI application initialization.
Core initialization and infrastructure package for the Headquarter API, providing centralized configuration, database connectivity, structured logging, and application bootstrap.
## parent
index: apps/api/.pi-map.index.md
map: apps/api/.pi-map.md
+2 -2
View File
@@ -4,7 +4,7 @@ dir: apps/api/src
index: apps/api/src/.pi-map.index.md
## role
Core application package for the Headquarter API, providing configuration, database connectivity, structured logging, and FastAPI application initialization.
Core initialization and infrastructure package for the Headquarter API, providing centralized configuration, database connectivity, structured logging, and application bootstrap.
## files
- __init__.py | Marks the directory as a Python package for the Headquarter API.
- config.py | Defines application configuration settings with environment-based overrides using Pydantic, including database URLs, service domains, OAuth/Authentik integration, JWT/session settings, and computed properties for environment-specific behavior. | exp: class:Settings, func:build_database_url(user: str, password: str, host: str, port: int, database: str) → str | dep: pydantic, pydantic_settings
@@ -12,7 +12,7 @@ Core application package for the Headquarter API, providing configuration, datab
- logging_config.py | Configures structured JSON logging with correlation ID injection, custom formatters, and HTTP request/exception middleware for a FastAPI application. | exp: class:CorrelationIdFilter, method:filter(self, record: logging.LogRecord) → bool, call:get_correlation_id, class:JSONFormatter, method:format(self, record: logging.LogRecord) → str, call:self.formatTime, call:record.getMessage, call:getattr, call:self.formatException, call:json.dumps, method:formatTime(self, record: logging.LogRecord, datefmt) → str, call:time.strftime, call:time.gmtime, class:RequestLoggingMiddleware, method:dispatch(self, request: Request, call_next: Callable) → Response, call:time.time, call:logger.info, call:call_next, call:int, call:logger.error, call:type, call:traceback.format_exc, class:ExceptionLoggingMiddleware, method:dispatch(self, request: Request, call_next: Callable) → Response, call:call_next, call:logger.critical, call:traceback.format_exc, func:configure_logging(level) → None, call:JSONFormatter, call:logging.StreamHandler, call:console_handler.setFormatter, call:console_handler.addFilter, call:CorrelationIdFilter, call:root_logger.setLevel, call:logging.getLogger("uvicorn").setLevel, call:logging.getLogger("uvicorn.access").setLevel, call:logging.getLogger("sqlalchemy.engine").setLevel, call:logger.info, call:logging.getLevelName | dep: json, logging, sys, time, traceback, collections.abc, fastapi, starlette.middleware.base, src.services.shared.correlation
- main.py | Initializes and configures a FastAPI application for the "Headquarter API" with database setup, middleware, routing, and background services. | exp: func:_sanitize_validation_errors(errors), call:error.get, call:str, call:ctx.items, call:isinstance, call:type, call:sanitized.append, func:validation_exception_handler(request: Request, exc: RequestValidationError), call:exc.errors, call:logger.warning, call:_sanitize_validation_errors, call:JSONResponse, func:on_startup(), call:logger.info, call:init_database, call:logger.error, call:sys.exit, call:_health_monitor.start, call:seed_builtin_tool_types, func:on_shutdown(), call:logger.info, call:_health_monitor.stop | dep: logging, os, fastapi, fastapi.exceptions, fastapi.middleware.cors, fastapi.responses, fastapi.staticfiles, src.api.config, src.api.project, src.api.system, src.api.tool, src.api.user, src.api.workspace, src.config, src.models, src.database, src.logging_config, src.seeds.builtin_tool_types, src.services.instance, src.services.shared, sys, src.api.*
## arch
Layered architecture with environment-based Pydantic configuration, async SQLAlchemy with retry resilience, structured JSON logging with correlation ID tracking, and modular FastAPI composition with middleware and background services.
Layered infrastructure pattern with environment-aware Pydantic settings, async SQLAlchemy with retry resilience, structured JSON logging with correlation ID tracking, and FastAPI factory composition with middleware pipeline.
## tags
src, database, logging, call:logger.info, api, middleware, fastapi, filter
## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api/src/services
## role
Service layer for business logic encapsulation in the API application
This directory serves as the services layer for the API application, intended to contain business logic and service implementations.
## parent
index: apps/api/src/.pi-map.index.md
map: apps/api/src/.pi-map.md
+2 -2
View File
@@ -4,11 +4,11 @@ dir: apps/api/src/services
index: apps/api/src/services/.pi-map.index.md
## role
Service layer for business logic encapsulation in the API application
This directory serves as the services layer for the API application, intended to contain business logic and service implementations.
## files
- __init__.py | Empty file with no functionality
## arch
Minimal/placeholder package following Python package convention with empty __init__.py, awaiting future service module implementations
The package follows a standard Python package structure with an empty `__init__.py` marker file, indicating it is a namespace package ready to house service modules, but currently contains no implemented services.
## tags
init, empty, functionality
## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api/src/services/tool
## role
Manages lifecycle and runtime environment for Docker-based tool execution instances including repository access, configuration, and network connectivity.
Provides containerized execution environment for tools with git repository access, configuration management, and secure remote connectivity.
## parent
index: apps/api/src/services/.pi-map.index.md
map: apps/api/src/services/.pi-map.md
File diff suppressed because one or more lines are too long
+29 -5
View File
@@ -73,11 +73,29 @@ 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()
def _get_repository_mount_name(repo: GitRepository) -> str:
"""Return the directory name a standard git clone would create.
Prefers the repository name parsed from the remote URL so the container
mount matches what users expect from ``git clone``. Falls back to the
user-provided repository name when no remote URL is available.
"""
if repo.remote_url:
base_url = extract_base_repo_url(repo.remote_url) or repo.remote_url
name = base_url.rstrip("/").split("/")[-1]
if name.endswith(".git"):
name = name[:-4]
if name:
return name
return repo.name
def _chown_path(path: str, uid: int, gid: int) -> None:
"""Recursively chown a path, suppressing permission errors."""
try:
@@ -889,8 +907,13 @@ async def prepare_manifest_instance(
# Use the repository name for the workspace/repo mount target, not the
# directory name of a workspace/clone path (which may be "main" or similar).
# Prefer the name parsed from the remote URL so it matches a standard clone.
repo = await session.get(GitRepository, instance.repository_id)
repo_name = repo.name if repo else os.path.basename(os.path.normpath(repo_path))
repo_name = (
_get_repository_mount_name(repo)
if repo
else os.path.basename(os.path.normpath(repo_path))
)
variables = {
"IMAGE_TAG": image_tag,
"INSTANCE_NAME": instance.name.lower(),
@@ -1052,7 +1075,8 @@ async def create_tool_instance(
)
home_dir = tool_type.home_directory or "/home/user"
workspace_target = f"{home_dir}/{repo.name}"
mount_name = _get_repository_mount_name(repo)
workspace_target = f"{home_dir}/{mount_name}"
compose_content = f"""version: "3.8"\nservices:
app:
@@ -1094,8 +1118,8 @@ async def create_tool_instance(
"INSTANCE_DIR": instance_dir,
"WORKSPACE_PATH": repo_path,
"REPO_PATH": repo_path,
"REPO_NAME": repo.name,
"WORKSPACE_NAME": repo.name,
"REPO_NAME": _get_repository_mount_name(repo),
"WORKSPACE_NAME": _get_repository_mount_name(repo),
"SSH_PATH": "",
"TOOL_PORT": tool_port,
"EXTRA_ENV": {},
@@ -1116,7 +1140,7 @@ async def create_tool_instance(
"TOOL_PORT": tool_port,
"USER_ID": str(user_id),
"PROJECT_ID": str(project_id),
"WORKSPACE_NAME": repo.name,
"WORKSPACE_NAME": _get_repository_mount_name(repo),
"HOME_DIRECTORY": tool_type.home_directory or "/home/user",
}
compose_content = render_compose_template(tool_type.compose_template, variables)