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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user