diff --git a/.pi-map.index.md b/.pi-map.index.md index 06d5739..d5775fc 100644 --- a/.pi-map.index.md +++ b/.pi-map.index.md @@ -16,7 +16,7 @@ dir: . Trust boundary: index routes, map orients, source decides. ## role -Infrastructure and deployment configuration package for a self-hosted project management platform with containerized services, SSO integration, and reverse proxy support. +Infrastructure and deployment configuration for a self-hosted project management platform with containerized services, reverse proxy setup, and AI agent development workflows. ## parent - ## children diff --git a/.pi-map.md b/.pi-map.md index 3509322..4cc8e32 100644 --- a/.pi-map.md +++ b/.pi-map.md @@ -18,7 +18,7 @@ index: ./.pi-map.index.md Trust boundary: index routes, map orients, source decides. ## role -Infrastructure and deployment configuration package for a self-hosted project management platform with containerized services, SSO integration, and reverse proxy support. +Infrastructure and deployment configuration for a self-hosted project management platform with containerized services, reverse proxy setup, and AI agent development workflows. ## files - .env.example | Provides a template of environment variables for configuring a Headquarter application with PostgreSQL, Redis, Authentik SSO, and Docker/Traefik deployment - .gitignore | Specifies files and directories for Git to ignore across a multi-language project with Python, Node, and custom tooling | dep: Git @@ -31,7 +31,7 @@ Infrastructure and deployment configuration package for a self-hosted project ma - progress.md | Tracks completed and remaining tasks for a backend-frontend code refactoring project organized in 7 phases - swap-pane | Empty file with no functionality ## arch -Docker Compose-based microservices architecture with PostgreSQL/Redis data layer, separate API/frontend containers, Traefik edge routing, and environment-driven configuration management. +Docker Compose-based microservices with PostgreSQL/Redis backing, Traefik edge routing, OAuth2 SSO integration, and OpenSpec-driven AI agent orchestration layered over a Python/Node full-stack application. ## tags docker, redis, git, application, postgresql, compose, traefik, project ## symbols diff --git a/apps/.pi-map.index.md b/apps/.pi-map.index.md index 0064852..f411a93 100644 --- a/apps/.pi-map.index.md +++ b/apps/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps ## role -Contains the main executable application entry points and top-level app configurations for the project. +This directory contains the main application entry points and executable modules for the project. ## parent index: ./.pi-map.index.md map: ./.pi-map.md diff --git a/apps/.pi-map.md b/apps/.pi-map.md index e3aa731..0aa737f 100644 --- a/apps/.pi-map.md +++ b/apps/.pi-map.md @@ -4,10 +4,10 @@ dir: apps index: apps/.pi-map.index.md ## role -Contains the main executable application entry points and top-level app configurations for the project. +This directory contains the main application entry points and executable modules for the project. ## files ## arch -Monorepo-style directory hosting multiple distinct application targets (e.g., web, mobile, desktop, CLI) that consume shared libraries/packages, typically with per-app configuration, routing, and platform-specific bootstrapping. +Modular application structure with separate deployable units, likely following a microservices or multi-app monorepo pattern where each subdirectory represents an independent application with its own configuration, dependencies, and lifecycle. ## tags - ## symbols diff --git a/apps/api/.pi-map.index.md b/apps/api/.pi-map.index.md index e390dc1..94457c2 100644 --- a/apps/api/.pi-map.index.md +++ b/apps/api/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/api ## role -Self-hosted FastAPI backend API that manages projects, git repositories, and development tools by orchestrating Docker instances with database persistence. +Self-hosted FastAPI backend API that manages projects, git repositories, and development tools via Docker instances. ## parent index: apps/.pi-map.index.md map: apps/.pi-map.md diff --git a/apps/api/.pi-map.md b/apps/api/.pi-map.md index 2755e45..5a11a10 100644 --- a/apps/api/.pi-map.md +++ b/apps/api/.pi-map.md @@ -4,7 +4,7 @@ dir: apps/api index: apps/api/.pi-map.index.md ## role -Self-hosted FastAPI backend API that manages projects, git repositories, and development tools by orchestrating Docker instances with database persistence. +Self-hosted FastAPI backend API that manages projects, git repositories, and development tools via Docker instances. ## files - .dockerignore | Specifies files and directories to exclude from Docker build context to reduce image size and avoid copying unnecessary files into containers. | dep: Docker - Dockerfile | Multi-stage Docker build for a Python application with Docker socket access, Cloudflare tunneling, and database dependency waiting | dep: python:3.11-slim, gcc, libpq-dev, docker-ce-cli, docker-compose-plugin, cloudflared, uvicorn, pyproject.toml dependencies @@ -14,7 +14,7 @@ Self-hosted FastAPI backend API that manages projects, git repositories, and dev - uv.lock | Lock file for the uv Python package manager that pins exact dependency versions and their artifact hashes for reproducible installations | dep: uv, Python 3.11+, aiosqlite, alembic, annotated-doc, annotated-types, anyio, ast-serialize, asyncpg, and many other PyPI packages - wait-for-db.sh | Wait for a PostgreSQL database to become available before executing a command, with configurable retry logic. | dep: nc (netcat), sh (POSIX shell), sleep ## arch -Async Python backend using FastAPI with SQLAlchemy/Alembic for PostgreSQL migrations, multi-stage Docker containerization with Docker-in-Docker socket access, Cloudflare tunnel integration, and uv for deterministic dependency management. +Async Python backend with FastAPI, PostgreSQL via Alembic migrations, multi-stage Docker containerization with Cloudflare tunneling, and uv-based dependency management. ## tags docker, alembic, python, database, fastapi, postgresql, asyncpg, uvicorn ## symbols diff --git a/apps/api/src/.pi-map.index.md b/apps/api/src/.pi-map.index.md index 68cb9f0..b483498 100644 --- a/apps/api/src/.pi-map.index.md +++ b/apps/api/src/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/api/src ## role -Core application package for the Headquarter API, providing centralized configuration, database connectivity, structured logging, and FastAPI application initialization. +Core backend API package for the "Headquarter" service, providing application bootstrap, configuration, database connectivity, and structured logging infrastructure. ## parent index: apps/api/.pi-map.index.md map: apps/api/.pi-map.md diff --git a/apps/api/src/.pi-map.md b/apps/api/src/.pi-map.md index 1afce1b..19e6004 100644 --- a/apps/api/src/.pi-map.md +++ b/apps/api/src/.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 centralized configuration, database connectivity, structured logging, and FastAPI application initialization. +Core backend API package for the "Headquarter" service, providing application bootstrap, configuration, database connectivity, and structured logging infrastructure. ## 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 centralized configur - 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 Alembic migrations, structured JSON logging with correlation IDs, and FastAPI middleware/routing pattern. +Layered FastAPI application architecture using Pydantic settings with environment overrides, async SQLAlchemy with Alembic migrations, dependency injection via lifespan context, and middleware-based cross-cutting concerns (correlation ID tracking, JSON logging, request/exception handling). ## tags src, database, logging, call:logger.info, api, middleware, fastapi, filter ## symbols diff --git a/apps/api/src/services/.pi-map.index.md b/apps/api/src/services/.pi-map.index.md index 92d7ae8..3351447 100644 --- a/apps/api/src/services/.pi-map.index.md +++ b/apps/api/src/services/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/api/src/services ## role -Service layer package for business logic implementation in the API application +This package serves as a Python namespace marker for the services layer in the API application, indicating a modular architecture where business logic services are organized as a subpackage. ## parent index: apps/api/src/.pi-map.index.md map: apps/api/src/.pi-map.md diff --git a/apps/api/src/services/.pi-map.md b/apps/api/src/services/.pi-map.md index 68dc150..15bcead 100644 --- a/apps/api/src/services/.pi-map.md +++ b/apps/api/src/services/.pi-map.md @@ -4,11 +4,11 @@ dir: apps/api/src/services index: apps/api/src/services/.pi-map.index.md ## role -Service layer package for business logic implementation in the API application +This package serves as a Python namespace marker for the services layer in the API application, indicating a modular architecture where business logic services are organized as a subpackage. ## files - __init__.py | Empty file with no functionality ## arch -Standard Python package structure with placeholder for service-oriented architecture; currently uninitialized with no implemented services +Standard Python package structure using __init__.py for namespace declaration, following conventional layered architecture with services separated into their own directory for future dependency injection and domain logic encapsulation. ## tags init, empty, functionality ## symbols diff --git a/apps/api/src/services/shared/.pi-map.index.md b/apps/api/src/services/shared/.pi-map.index.md index c6ecad9..3b7a1ad 100644 --- a/apps/api/src/services/shared/.pi-map.index.md +++ b/apps/api/src/services/shared/.pi-map.index.md @@ -2,12 +2,14 @@ dir: apps/api/src/services/shared ## role -Provides common infrastructure and utility services used across the API backend, including cross-cutting concerns like request tracing, file operations, notifications, Docker container management, and workspace lifecycle handling. +Provides common infrastructure services used across the API backend, including security, I/O, container operations, and workspace lifecycle management. ## parent index: apps/api/src/services/.pi-map.index.md map: apps/api/src/services/.pi-map.md ## children -- +- apps/api/src/services/shared/.ruff_cache + index: apps/api/src/services/shared/.ruff_cache/.pi-map.index.md + map: apps/api/src/services/shared/.ruff_cache/.pi-map.md ## files - __init__.py - correlation.py @@ -24,5 +26,7 @@ map: apps/api/src/services/shared/.pi-map.md ## workflows - change shared behavior read: __init__.py, correlation.py, file_service.py +- explore shared subdirectories + index: apps/api/src/services/shared/.ruff_cache/.pi-map.index.md ## dirty - diff --git a/apps/api/src/services/shared/.pi-map.md b/apps/api/src/services/shared/.pi-map.md index e383c0a..274eb90 100644 --- a/apps/api/src/services/shared/.pi-map.md +++ b/apps/api/src/services/shared/.pi-map.md @@ -4,9 +4,9 @@ dir: apps/api/src/services/shared index: apps/api/src/services/shared/.pi-map.index.md ## role -Provides common infrastructure and utility services used across the API backend, including cross-cutting concerns like request tracing, file operations, notifications, Docker container management, and workspace lifecycle handling. +Provides common infrastructure services used across the API backend, including security, I/O, container operations, and workspace lifecycle management. ## files -- __init__.py | Re-exports shared service classes and functions from a services package to provide a unified public API | dep: src.services.shared.correlation, src.services.shared.file_service, src.services.shared.notification_service, src.services.shared.permission_fixer, src.services.shared.readiness_probe, src.services.shared.ssh_keys, src.services.shared.tunnel, src.services.shared.workspace_manager, correlation, file_service, notification_service, permission_fixer, readiness_probe, ssh_keys, tunnel, workspace_manager +- __init__.py | Re-exports shared service classes and functions from a services package to provide a unified public API | dep: correlation, file_service, notification_service, permission_fixer, readiness_probe, ssh_keys, tunnel, workspace_manager - correlation.py | Provides async correlation ID tracking via context variables and FastAPI middleware for request tracing. | exp: class:CorrelationIdMiddleware, method:dispatch(self, request: Request, call_next), call:request.headers.get, call:str, call:uuid.uuid4, call:CORRELATION_ID.set, call:call_next, call:CORRELATION_ID.reset, func:get_correlation_id() → str, call:CORRELATION_ID.get, call:str, call:uuid.uuid4 | dep: contextvars, uuid, fastapi, starlette.middleware.base, fastapi.Request, starlette.middleware.base.BaseHTTPMiddleware - file_service.py | Provides safe file operations (list, read, write) scoped within a workspace directory with path traversal protection. | exp: class:FileEntry, class:FileService, method:list_directory(self, workspace: Workspace, relative_path) → list[FileEntry], call:os.path.join, call:os.path.normpath, call:abs_path.startswith, call:os.path.exists, call:sorted, call:os.listdir, call:os.path.isdir, call:os.path.getsize, call:os.path.isfile, call:entries.append, call:FileEntry, call:rel.replace, call:entries.sort, call:e.name.lower, raise:ValueError, method:read_file(self, workspace: Workspace, relative_path: str) → str, call:self._resolve_path, call:os.path.isfile, call:open, call:f.read, raise:FileNotFoundError, raise:ValueError, method:write_file(self, workspace: Workspace, relative_path: str, content: str) → None, call:self._resolve_path, call:os.makedirs, call:os.path.dirname, call:open, call:f.write, call:logger.info, method:_resolve_path(self, workspace: Workspace, relative_path: str) → str, call:os.path.normpath, call:os.path.join, call:abs_path.startswith, raise:ValueError | dep: logging, os, dataclasses, src.models, src.models.Workspace - notification_service.py | Provides an async singleton service for creating, listing, reading, and dismissing user-owned notifications with strict ownership isolation. | exp: class:NotificationService, method:create_notification(self, session: AsyncSession, user_id: uuid.UUID, category: str, severity: str, title: str, message, source_type, source_id, metadata) → Notification, call:Notification, call:session.add, call:session.commit, call:session.refresh, method:list_notifications(self, session: AsyncSession, user_id: uuid.UUID, limit, offset, unread_only, mute_categories) → tuple[list[Notification], int], call:Notification.dismissed_at.is_, call:where_clauses.append, call:Notification.read_at.is_, call:Notification.category.not_in, call:select(func.count()).select_from(Notification).where, call:func.count, call:session.execute, call:total_result.scalar_one, call:select(Notification) .where(*where_clauses) .order_by(Notification.created_at.desc()) .limit(limit) .offset, call:Notification.created_at.desc, call:list, call:items_result.scalars().all, method:get_unread_count(self, session: AsyncSession, user_id: uuid.UUID) → int, call:select(func.count()) .select_from(Notification) .where, call:func.count, call:Notification.read_at.is_, call:Notification.dismissed_at.is_, call:session.execute, call:result.scalar_one, method:mark_read(self, session: AsyncSession, notification_id: uuid.UUID, user_id: uuid.UUID) → Notification, call:self._get_owned_notification, call:datetime.now, call:session.commit, call:session.refresh, method:mark_all_read(self, session: AsyncSession, user_id: uuid.UUID) → int, call:update(Notification) .where( Notification.user_id == user_id, Notification.read_at.is_(None), Notification.dismissed_at.is_(None), ) .values, call:Notification.read_at.is_, call:Notification.dismissed_at.is_, call:datetime.now, call:session.execute, call:session.commit, method:dismiss_all(self, session: AsyncSession, user_id: uuid.UUID) → int, call:update(Notification) .where( Notification.user_id == user_id, Notification.dismissed_at.is_(None), ) .values, call:Notification.dismissed_at.is_, call:datetime.now, call:session.execute, call:session.commit, method:dismiss(self, session: AsyncSession, notification_id: uuid.UUID, user_id: uuid.UUID) → None, call:self._get_owned_notification, call:datetime.now, call:session.commit, method:_get_owned_notification(self, session: AsyncSession, notification_id: uuid.UUID, user_id: uuid.UUID) → Notification, call:session.get, raise:ValueError | dep: uuid, datetime, typing, sqlalchemy, sqlalchemy.engine, sqlalchemy.ext.asyncio, src.models, src.models.Notification @@ -14,11 +14,11 @@ Provides common infrastructure and utility services used across the API backend, - readiness_probe.py | Executes a retryable readiness probe command inside a Docker container with configurable timeout and interval | exp: func:execute_probe(container_id: str, command: str, timeout, interval) → tuple[bool, list[str]], call:asyncio.get_event_loop().time, call:logs.append, call:logger.debug, call:subprocess.run, call:result.stdout.strip, call:result.stderr.strip, call:asyncio.sleep | dep: asyncio, logging, subprocess - ssh_keys.py | Decrypts and writes SSH key files to instance directories for container mounting, with optional ownership configuration and SSH config generation. | exp: func:_get_fernet() → Fernet, call:Settings, call:hashlib.sha256(settings.session_secret.encode()).digest, call:settings.session_secret.encode, call:base64.urlsafe_b64encode, call:Fernet, func:_sanitize_filename(name: str) → str, call:re.sub, call:sanitized.strip, func:prepare_ssh_key_files(instance_dir: str, ssh_key, subdir, uid, gid, key_filename, write_config) → str, call:Path, call:ssh_dir.mkdir, call:_get_fernet, call:fernet.decrypt(ssh_key.private_key_encrypted.encode()).decode, call:ssh_key.private_key_encrypted.encode, call:private_key_path.write_text, call:os.chmod, call:public_key_path.write_text, call:config_path.write_text, call:os.chown, call:logger.debug, call:logger.warning, call:os.getuid, call:str, func:write_ssh_config(ssh_dir: str, key_filenames: list[str], uid, gid) → None, call:Path, call:ssh_dir_path.mkdir, call:lines.append, call:"\n".join, call:config_path.write_text, call:os.chmod, call:os.chown, func:cleanup_ssh_key_files(instance_dir: str) → None, call:Path, call:ssh_dir.exists, call:ssh_dir.iterdir, call:file_path.unlink, call:ssh_dir.rmdir | dep: logging, os, re, pathlib, cryptography.fernet, src.config, base64, hashlib - tunnel.py | Re-exports Docker tunnel functions from a nested module for backward compatibility. | dep: src.services.docker.tunnel -- workspace_manager.py | Manages workspace lifecycle operations including creation, deletion, synchronization, and migration of legacy tool instances into workspaces with git repository cloning. | exp: class:SyncResult, class:WorkspaceHasInstancesError, method:__init__(self, instances: list[dict]) → None, call:super().__init__, call:len, class:WorkspaceManager, method:_workspace_path(self, workspace_id: uuid.UUID, repo: "GitRepository") → str, call:os.path.join, call:str, call:self._repo_directory_name, method:create(self, repo: GitRepository, user_id: uuid.UUID, name: str, branch, session) → Workspace, call:self._repo_directory_name, call:uuid.uuid4, call:os.path.join, call:str, call:logger.info, call:os.path.exists, call:logger.warning, call:shutil.rmtree, call:os.makedirs, call:contextlib.suppress, call:os.chmod, call:getattr, call:session.execute, call:select(SSHKey).where, call:result.scalar_one_or_none, call:_get_fernet, call:fernet.decrypt( ssh_key_obj.private_key_encrypted.encode() ).decode, call:ssh_key_obj.private_key_encrypted.encode, call:GitService.clone, call:os.path.isdir, call:os.listdir, call:len, call:self._make_world_writable, call:Workspace, call:datetime.now, raise:ValueError, raise:RuntimeError, method:delete(self, workspace: Workspace, force, session) → None, call:self._get_instances, call:self._stop_and_delete_instance, call:os.path.exists, call:shutil.rmtree, call:logger.info, call:session.delete, raise:ValueError, raise:WorkspaceHasInstancesError, method:sync(self, workspace: Workspace, session) → SyncResult, call:logger.info, call:session.get, call:getattr, call:session.execute, call:select(SSHKey).where, call:result.scalar_one_or_none, call:_get_fernet, call:fernet.decrypt( ssh_key_obj.private_key_encrypted.encode() ).decode, call:ssh_key_obj.private_key_encrypted.encode, call:GitService.fetch, call:GitService.branch_exists_remotely, call:SyncResult, call:GitService.pull, call:self._make_world_writable, call:datetime.now, method:_make_world_writable(self, path: str) → None, call:contextlib.suppress, call:os.chmod, call:os.walk, call:os.path.join, call:os.stat, method:_get_instances(self, workspace: Workspace, session: AsyncSession) → list[ToolInstance], call:session.execute, call:select(ToolInstance).where, call:list, call:result.scalars().all, method:_stop_and_delete_instance(self, instance: ToolInstance, session: AsyncSession) → None, call:delete_tool_instance, call:logger.info, call:logger.error, method:ensure_instance_workspace(self, instance: ToolInstance, session: AsyncSession) → Workspace, call:session.get, call:self._workspace_name_exists, call:self._migrate_clone_into_workspace, call:self.create, call:session.add, call:session.commit, call:session.refresh, call:logger.info, raise:RuntimeError, method:_workspace_name_exists(self, session: AsyncSession, repo_id: uuid.UUID, name: str) → bool, call:session.execute, call:select(Workspace).where, call:result.scalar_one_or_none, method:_migrate_clone_into_workspace(self, instance: ToolInstance, repo: "GitRepository", session: AsyncSession, name: str) → Workspace, call:os.path.dirname, call:os.path.join, call:os.path.exists, call:uuid.uuid4, call:str, call:self._repo_directory_name, call:os.makedirs, call:contextlib.suppress, call:os.chmod, call:shutil.rmtree, call:shutil.move, call:self._make_world_writable, call:Workspace, call:datetime.now, call:session.add, call:session.flush, raise:RuntimeError | dep: contextlib, logging, os, shutil, stat, uuid, dataclasses, datetime, typing, sqlalchemy, src.models, src.services.git.git_service, src.services.shared.ssh_keys, src.utils.git_url_parser, sqlalchemy.ext.asyncio, src.services.tool.instance_service +- workspace_manager.py | Manages the full lifecycle of development workspaces including creation via git clone, deletion with cleanup, remote sync, and migration of legacy tool instances into workspace-bound structures. | exp: class:SyncResult, class:WorkspaceHasInstancesError, method:__init__(self, instances: list[dict]) → None, call:super().__init__, call:len, class:WorkspaceManager, method:_workspace_path(self, workspace_id: uuid.UUID, repo: "GitRepository") → str, call:os.path.join, call:str, call:self._repo_directory_name, method:create(self, repo: GitRepository, user_id: uuid.UUID, name: str, branch, session) → Workspace, call:self._repo_directory_name, call:uuid.uuid4, call:os.path.join, call:str, call:logger.info, call:os.path.exists, call:logger.warning, call:shutil.rmtree, call:os.makedirs, call:contextlib.suppress, call:os.chmod, call:getattr, call:session.execute, call:select(SSHKey).where, call:result.scalar_one_or_none, call:_get_fernet, call:fernet.decrypt( ssh_key_obj.private_key_encrypted.encode() ).decode, call:ssh_key_obj.private_key_encrypted.encode, call:GitService.clone, call:logger.error, call:os.path.isdir, call:os.listdir, call:len, call:self._make_world_writable, call:Workspace, call:datetime.now, raise:ValueError, raise:RuntimeError, method:delete(self, workspace: Workspace, force, session) → None, call:self._get_instances, call:self._stop_and_delete_instance, call:os.path.exists, call:shutil.rmtree, call:logger.info, call:session.delete, raise:ValueError, raise:WorkspaceHasInstancesError, method:sync(self, workspace: Workspace, session) → SyncResult, call:logger.info, call:session.get, call:getattr, call:session.execute, call:select(SSHKey).where, call:result.scalar_one_or_none, call:_get_fernet, call:fernet.decrypt( ssh_key_obj.private_key_encrypted.encode() ).decode, call:ssh_key_obj.private_key_encrypted.encode, call:GitService.fetch, call:GitService.branch_exists_remotely, call:SyncResult, call:GitService.pull, call:self._make_world_writable, call:datetime.now, method:_make_world_writable(self, path: str) → None, call:contextlib.suppress, call:os.chmod, call:os.walk, call:os.path.join, call:os.stat, method:_get_instances(self, workspace: Workspace, session: AsyncSession) → list[ToolInstance], call:session.execute, call:select(ToolInstance).where, call:list, call:result.scalars().all, method:_stop_and_delete_instance(self, instance: ToolInstance, session: AsyncSession) → None, call:delete_tool_instance, call:logger.info, call:logger.error, method:ensure_instance_workspace(self, instance: ToolInstance, session: AsyncSession) → Workspace, call:session.get, call:self._workspace_name_exists, call:self._migrate_clone_into_workspace, call:self.create, call:session.add, call:session.commit, call:session.refresh, call:logger.info, raise:RuntimeError, method:_workspace_name_exists(self, session: AsyncSession, repo_id: uuid.UUID, name: str) → bool, call:session.execute, call:select(Workspace).where, call:result.scalar_one_or_none, method:_migrate_clone_into_workspace(self, instance: ToolInstance, repo: "GitRepository", session: AsyncSession, name: str) → Workspace, call:os.path.dirname, call:os.path.join, call:os.path.exists, call:uuid.uuid4, call:str, call:self._repo_directory_name, call:os.makedirs, call:contextlib.suppress, call:os.chmod, call:shutil.rmtree, call:shutil.move, call:self._make_world_writable, call:Workspace, call:datetime.now, call:session.add, call:session.flush, raise:RuntimeError | dep: contextlib, logging, os, shutil, stat, uuid, dataclasses, datetime, typing, sqlalchemy, src.models, src.services.git.git_service, src.services.shared.ssh_keys, src.utils.git_url_parser, sqlalchemy.ext.asyncio, src.services.tool.instance_service ## arch -Modular service-oriented architecture with singleton async services, context variable-based state propagation for request correlation, defensive path traversal protections, and Docker-centric container orchestration patterns with privilege escalation for filesystem operations. +Modular service-oriented architecture with singleton async services, context variable-based request tracing, path traversal-safe file operations, Docker container exec-based operations, and git-based workspace provisioning with cleanup and migration support. ## tags -error, call:self., get, workspace, src, key, call:str, call:session.execute +error, call:self., get, workspace, key, call:str, call:session.execute, call:ssh ## symbols - CorrelationIdMiddleware - FileEntry @@ -31,5 +31,7 @@ error, call:self., get, workspace, src, key, call:str, call:session.execute ## workflows - change shared behavior read: __init__.py, correlation.py, file_service.py +- explore shared subdirectories + index: apps/api/src/services/shared/.ruff_cache/.pi-map.index.md ## dirty - diff --git a/apps/api/src/services/shared/workspace_manager.py b/apps/api/src/services/shared/workspace_manager.py index 2b80628..f9904de 100644 --- a/apps/api/src/services/shared/workspace_manager.py +++ b/apps/api/src/services/shared/workspace_manager.py @@ -139,7 +139,19 @@ class WorkspaceManager: ssh_key_obj.private_key_encrypted.encode() ).decode() - await GitService.clone(repo.remote_url, branch, clone_target, ssh_key=ssh_key) + try: + await GitService.clone( + repo.remote_url, branch, clone_target, ssh_key=ssh_key + ) + except Exception as exc: + logger.error( + "Git clone failed for workspace %s (repo=%s, url=%s): %s", + workspace_id, + repo.id, + repo.remote_url, + exc, + ) + raise expected_path = os.path.join(parent_path, repo_dir_name) if not os.path.isdir(expected_path): @@ -150,6 +162,12 @@ class WorkspaceManager: for entry in os.listdir(parent_path) if os.path.isdir(os.path.join(parent_path, entry)) ] + logger.info( + "Workspace %s: expected %s/ but found directories: %s", + workspace_id, + repo_dir_name, + entries, + ) if len(entries) == 1: expected_path = os.path.join(parent_path, entries[0]) else: