From bd94cc9bbfdb48da9dbea3e025dc957f4180ed1a Mon Sep 17 00:00:00 2001 From: Developer Date: Sun, 14 Jun 2026 20:29:03 +0000 Subject: [PATCH] fix: use sudo/root to create /workspace symlink in manifest entrypoint The previous commit moved the pi-agent repo mount from /workspace to /home/user/{repo_name}. This exposed a permission bug: the Dockerfile creates /workspace as a root-owned symlink in the image, and the non-root entrypoint could not replace it because / is owned by root. - Update compile_entrypoint to recreate /workspace via sudo when running as the container user, or directly when running as root - Add unit test covering sudo/root symlink creation - Update OpenSpec change docs with the additional root cause Quality gates: - pytest tests/unit: 208 passed - ruff: clean on changed files - mypy: clean on changed files - alembic heads: single head --- .pi-map.index.md | 3 +-- .pi-map.md | 7 +++---- apps/.pi-map.md | 2 +- apps/api/.pi-map.index.md | 2 +- apps/api/.pi-map.md | 4 ++-- apps/api/src/services/build/manifest_compiler.py | 9 ++++++++- apps/api/tests/.pi-map.index.md | 2 +- apps/api/tests/.pi-map.md | 4 ++-- apps/api/tests/unit/.pi-map.index.md | 8 ++++++-- apps/api/tests/unit/.pi-map.md | 8 +++++--- apps/api/tests/unit/test_manifest_compiler.py | 14 ++++++++++++++ openspec/.pi-map.index.md | 2 +- openspec/changes/.pi-map.index.md | 2 +- .../.pi-map.index.md | 2 +- .../fix-pi-container-mount-permissions/.pi-map.md | 8 ++++---- .../fix-pi-container-mount-permissions/change.md | 2 ++ 16 files changed, 53 insertions(+), 26 deletions(-) diff --git a/.pi-map.index.md b/.pi-map.index.md index 76e3c64..71ffb76 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 -A self-hosted web-based project management and Git repository platform with OAuth2 authentication, designed for deployment via Docker Compose with optional Traefik reverse proxy. +Infrastructure and deployment configuration for a self-hosted project management platform with OAuth2 authentication, providing Docker orchestration, environment templates, and development tooling. ## parent - ## children @@ -69,7 +69,6 @@ A self-hosted web-based project management and Git repository platform with OAut - CHANGELOG.md - Makefile - README.md -- context.md - docker-compose.traefik.yml - docker-compose.yml - progress.md diff --git a/.pi-map.md b/.pi-map.md index 1f6ee4d..8d9b7f7 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 -A self-hosted web-based project management and Git repository platform with OAuth2 authentication, designed for deployment via Docker Compose with optional Traefik reverse proxy. +Infrastructure and deployment configuration for a self-hosted project management platform with OAuth2 authentication, providing Docker orchestration, environment templates, and development tooling. ## 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 @@ -26,15 +26,14 @@ A self-hosted web-based project management and Git repository platform with OAut - CHANGELOG.md | Documents version history and notable changes for a Git-based project management web application - Makefile | Provides standard development commands for containerized web application lifecycle management via Docker Compose | dep: docker compose, alembic, pytest, ruff, mypy, playwright, npm, postgres, redis - README.md | A self-hosted platform for managing projects, git repositories, and development tools with OAuth2 authentication. | dep: FastAPI, SQLAlchemy, Pydantic, Alembic, python-jose, React, TypeScript, Vite, React Router, Docker, PostgreSQL, Traefik, Authentik, Git -- context.md | Technical investigation report documenting a half-implemented home-directory feature for manifest-based tools, specifically analyzing why the `pi-agent` tool fails with permission errors and providing recommended code changes. | dep: alembic, docker, docker-compose, npm, python, shlex, os - docker-compose.traefik.yml | Deploys a multi-service web application (frontend, API, PostgreSQL, Redis) behind an existing Traefik reverse proxy with TLS termination and environment-configurable domains. | dep: docker, traefik, postgres, redis, authentik, docker-compose - docker-compose.yml | Defines a multi-service Docker Compose stack with PostgreSQL, Redis, API backend, and web frontend services for a "headquarter" application | dep: Docker, PostgreSQL, Redis, Vite, asyncpg, nginx - 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 -Multi-service containerized architecture (PostgreSQL, Redis, API backend, web frontend) using environment-driven configuration, with a frontend-backend split and infrastructure-as-code deployment patterns. +Containerized microservices architecture using Docker Compose with PostgreSQL/Redis data layer, Traefik reverse proxy for TLS/ingress, environment-driven configuration pattern, and Makefile-based lifecycle management across frontend/backend services. ## tags -docker, redis, git, compose, application, postgresql, traefik, project +docker, redis, git, application, postgresql, compose, traefik, project ## symbols - ## workflows diff --git a/apps/.pi-map.md b/apps/.pi-map.md index 477bc8d..8532383 100644 --- a/apps/.pi-map.md +++ b/apps/.pi-map.md @@ -7,7 +7,7 @@ index: apps/.pi-map.index.md Contains the main application entry points and executable modules for the project. ## files ## arch -Modular application structure with separate deployable units, typically following framework-specific patterns (e.g., MVC, MVVM, or component-based architecture depending on the stack). +Modular application architecture with separate deployable units, likely following microservices or layered architecture patterns with domain-driven organization. ## tags - ## symbols diff --git a/apps/api/.pi-map.index.md b/apps/api/.pi-map.index.md index 296a153..e08a95e 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 via Docker instances with PostgreSQL database. +FastAPI backend API that manages projects, git repositories, and development tools via Docker instances with PostgreSQL database and async Alembic migrations. ## 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 4a40831..ff8b083 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 via Docker instances with PostgreSQL database. +FastAPI backend API that manages projects, git repositories, and development tools via Docker instances with PostgreSQL database and async Alembic migrations. ## 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 -Modern Python async architecture using FastAPI with SQLAlchemy/Alembic for database migrations, multi-stage Docker containerization with Docker socket access for container orchestration, Cloudflare tunneling for secure external access, and uv for fast dependency management. +Self-hosted containerized service using multi-stage Docker builds, Cloudflare tunneling, uv package management, and startup dependency orchestration with shell-based health checks. ## tags docker, alembic, python, database, fastapi, postgresql, asyncpg, uvicorn ## symbols diff --git a/apps/api/src/services/build/manifest_compiler.py b/apps/api/src/services/build/manifest_compiler.py index 99ac307..2fd25b1 100644 --- a/apps/api/src/services/build/manifest_compiler.py +++ b/apps/api/src/services/build/manifest_compiler.py @@ -343,7 +343,14 @@ def compile_entrypoint(manifest: dict) -> str: lines.append('fix_owner "$WORKSPACE_TARGET"') lines.append("") lines.append("# Create /workspace compatibility symlink") - lines.append('ln -sfn "$WORKSPACE_TARGET" /workspace') + lines.append("# / is owned by root, so we need root or passwordless sudo.") + lines.append('if [ -n "$SUDO" ]; then') + lines.append(' sudo ln -sfn "$WORKSPACE_TARGET" /workspace') + lines.append('elif [ "$(id -u)" = "0" ]; then') + lines.append(' ln -sfn "$WORKSPACE_TARGET" /workspace') + lines.append('else') + lines.append(' ln -sfn "$WORKSPACE_TARGET" /workspace 2>/dev/null || true') + lines.append('fi') lines.append("") lines.append("# Fix ownership of declared mount targets (top-level only)") for mount in manifest.get("mounts", []): diff --git a/apps/api/tests/.pi-map.index.md b/apps/api/tests/.pi-map.index.md index 8180aca..77f726f 100644 --- a/apps/api/tests/.pi-map.index.md +++ b/apps/api/tests/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/api/tests ## role -Provides shared test infrastructure and fixtures for API integration testing with async database and authentication support. +Provides shared test infrastructure and fixtures for the FastAPI API application. ## parent index: apps/api/.pi-map.index.md map: apps/api/.pi-map.md diff --git a/apps/api/tests/.pi-map.md b/apps/api/tests/.pi-map.md index ec63587..fdff489 100644 --- a/apps/api/tests/.pi-map.md +++ b/apps/api/tests/.pi-map.md @@ -4,11 +4,11 @@ dir: apps/api/tests index: apps/api/tests/.pi-map.index.md ## role -Provides shared test infrastructure and fixtures for API integration testing with async database and authentication support. +Provides shared test infrastructure and fixtures for the FastAPI API application. ## files - conftest.py | Provides shared pytest fixtures for testing a FastAPI application with async SQLite database, authenticated clients, and test data setup. | exp: func:test_client() → Generator[TestClient, None, None], call:create_async_engine, call:engine.begin, call:conn.run_sync, call:asyncio.run, call:init_db, call:async_sessionmaker, call:patch, call:TestClient, call:app.dependency_overrides.pop, call:engine.dispose, func:init_db(), call:engine.begin, call:conn.run_sync, func:override_get_db_session() → AsyncGenerator[AsyncSession, None], call:async_sessionmaker, func:db_session(test_client) → AsyncGenerator[AsyncSession, None], call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:gen.aclose, call:create_async_engine, call:engine.begin, call:conn.run_sync, call:async_sessionmaker, call:engine.dispose, func:authenticated_client(test_client) → Generator[TestClient, None, None], call:str, call:uuid.uuid4, call:Settings, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, call:asyncio.run, call:create_test_user, call:create_session_cookie, call:test_client.cookies.set, func:create_test_user(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, func:test_project_and_repo(authenticated_client) → tuple[str, str], call:uuid.uuid4, call:Settings, call:authenticated_client.cookies.get, call:decode_session_cookie, call:uuid.UUID, call:asyncio.run, call:get_user_id, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:Project, call:session.add, call:GitRepository, call:session.commit, call:gen.aclose, call:create_project_and_repo, call:str, raise:RuntimeError, func:get_user_id(), call:Settings, call:authenticated_client.cookies.get, call:decode_session_cookie, call:uuid.UUID, func:create_project_and_repo(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:Project, call:session.add, call:GitRepository, call:session.commit, call:gen.aclose, func:admin_client(test_client) → Generator[TestClient, None, None], call:str, call:uuid.uuid4, call:Settings, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, call:asyncio.run, call:create_admin_user, call:create_session_cookie, call:test_client.cookies.set, func:create_admin_user(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose | dep: asyncio, os, typing, unittest.mock, pytest, pytest_asyncio, fastapi.testclient, sqlalchemy.ext.asyncio, src.config, src.models.base, src.main, src.auth.dependencies, uuid, src.auth.session, src.models.user.user, src.models.project.project, src.models.project.git_repository, fastapi, sqlalchemy, aiosqlite, src.models, src.auth ## arch -Pytest fixture-based architecture using dependency injection for async SQLite database sessions, JWT-authenticated HTTP clients, and modular test data setup across FastAPI test suites. +Pytest fixture-based testing architecture with async SQLite database setup, authenticated HTTP client factories, and modular test data initialization. ## tags call:app.dependency, call:create, overrides.get, call:override, fn, call:gen.asend, call:gen.aclose, user ## symbols diff --git a/apps/api/tests/unit/.pi-map.index.md b/apps/api/tests/unit/.pi-map.index.md index f20c6a6..cd0a681 100644 --- a/apps/api/tests/unit/.pi-map.index.md +++ b/apps/api/tests/unit/.pi-map.index.md @@ -2,12 +2,14 @@ dir: apps/api/tests/unit ## role -Contains unit tests for the API application's core services, utilities, and infrastructure components. +Unit test suite for the API application covering configuration, database migrations, Docker operations, Git integration, file services, monitoring, notifications, and manifest compilation. ## parent index: apps/api/tests/.pi-map.index.md map: apps/api/tests/.pi-map.md ## children -- +- apps/api/tests/unit/.ruff_cache + index: apps/api/tests/unit/.ruff_cache/.pi-map.index.md + map: apps/api/tests/unit/.ruff_cache/.pi-map.md ## files - __init__.py - test_alembic_migrations.py @@ -40,5 +42,7 @@ map: apps/api/tests/unit/.pi-map.md read: __init__.py, test_alembic_migrations.py, test_config.py - change unit config read: test_config.py, test_config_profile_resolver.py +- explore unit subdirectories + index: apps/api/tests/unit/.ruff_cache/.pi-map.index.md ## dirty - diff --git a/apps/api/tests/unit/.pi-map.md b/apps/api/tests/unit/.pi-map.md index 97eb5bd..e0b2f20 100644 --- a/apps/api/tests/unit/.pi-map.md +++ b/apps/api/tests/unit/.pi-map.md @@ -4,7 +4,7 @@ dir: apps/api/tests/unit index: apps/api/tests/unit/.pi-map.index.md ## role -Contains unit tests for the API application's core services, utilities, and infrastructure components. +Unit test suite for the API application covering configuration, database migrations, Docker operations, Git integration, file services, monitoring, notifications, and manifest compilation. ## files - __init__.py | Empty file with no functionality - test_alembic_migrations.py | Unit tests that verify Alembic database migrations are importable, have correct revision identifiers, and declare expected dependencies without requiring a live database. | exp: func:test_home_directory_migration_imports_and_rewrites() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable, func:test_merge_migration_resolves_heads() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable | dep: importlib.util, pathlib, pytest, importlib @@ -21,7 +21,7 @@ Contains unit tests for the API application's core services, utilities, and infr - test_home_path_expansion.py | Unit tests for home directory path expansion (~ and $HOME) in container paths and manifest home directory resolution. | exp: class:TestExpandContainerPath, method:test_tilde_slash_expands(self) → None, call:expand_container_path, method:test_tilde_alone_expands(self) → None, call:expand_container_path, method:test_dollar_home_slash_expands(self) → None, call:expand_container_path, method:test_dollar_home_alone_expands(self) → None, call:expand_container_path, method:test_absolute_path_unchanged(self) → None, call:expand_container_path, method:test_relative_path_unchanged(self) → None, call:expand_container_path, method:test_tilde_in_middle_unchanged(self) → None, call:expand_container_path, method:test_dollar_home_in_middle_unchanged(self) → None, call:expand_container_path, method:test_root_home(self) → None, call:expand_container_path, class:TestGetManifestHomeDir, method:test_with_user_block(self) → None, call:get_manifest_home_dir, method:test_without_user_block(self) → None, call:get_manifest_home_dir, method:test_with_empty_user_name(self) → None, call:get_manifest_home_dir, method:test_with_none_user_name(self) → None, call:get_manifest_home_dir | dep: pytest, src.services.config.config_profile_resolver, src.services.build.manifest_compiler - test_instance_service.py | Unit tests for home directory expansion in docker-compose file modification via instance_service | exp: class:TestModifyComposeFile, method:test_extra_volumes_expand_home_dir(self, tmp_path), call:compose_path.write_text, call:modify_compose_file, call:str, call:compose_path.read_text, method:test_working_directory_expands_home_dir(self, tmp_path), call:compose_path.write_text, call:modify_compose_file, call:str, call:compose_path.read_text | dep: pytest, src.services.tool.instance_service - test_lifecycle_hooks.py | Unit tests for lifecycle hook helper functions that derive notification titles and determine whether events should trigger notifications. | exp: class:TestDeriveTitle, method:test_known_event_types(self) → None, call:_derive_title, method:test_unknown_event_type(self) → None, call:_derive_title, class:TestShouldNotify, method:test_error_events_are_notified(self) → None, call:_should_notify, method:test_health_changed_running_is_notified(self) → None, call:_should_notify, method:test_created_started_stopped_restarted_deleted_filtered(self) → None, call:_should_notify, method:test_health_changed_non_running_filtered(self) → None, call:_should_notify | dep: pytest, src.services.instance.lifecycle_hooks -- test_manifest_compiler.py | Unit tests for a manifest compiler that generates Dockerfiles, docker-compose files, and entrypoint scripts from manifest configurations. | exp: class:TestGetManifestHomeDir, method:test_home_directory_in_manifest_wins(self) → None, call:get_manifest_home_dir, method:test_user_name_derives_home(self) → None, call:get_manifest_home_dir, method:test_root_fallback(self) → None, call:get_manifest_home_dir, method:test_empty_home_directory_falls_back(self) → None, call:get_manifest_home_dir, class:TestCompileDockerfileHomeDirectory, method:test_env_home_and_workdir_use_home_directory(self) → None, call:compile_dockerfile, method:test_workspace_symlink_created(self) → None, call:compile_dockerfile, method:test_runtime_working_dir_overrides_home_workdir(self) → None, call:compile_dockerfile, method:test_working_dir_expands_tilde(self) → None, call:compile_dockerfile, class:TestCompileComposeHomeDirectory, method:test_default_repo_mount_synthesized(self) → None, call:compile_compose, method:test_explicit_repo_mount_preserved(self) → None, call:compile_compose, method:test_workspace_name_substituted_in_mount_target(self) → None, call:compile_compose, method:test_working_dir_expands_home(self) → None, call:compile_compose, class:TestCompileEntrypoint, method:test_entrypoint_creates_home_and_workspace(self) → None, call:compile_entrypoint, method:test_entrypoint_fixes_mount_owners(self) → None, call:compile_entrypoint, func:test_compile_dockerfile_creates_config_dirs_for_user() → None, call:compile_dockerfile, func:test_compile_dockerfile_no_user_does_not_create_home() → None, call:compile_dockerfile, func:test_compile_dockerfile_uses_user_npm_prefix() → None, call:compile_dockerfile | dep: pytest, src.services.build.manifest_compiler +- test_manifest_compiler.py | Unit tests for a manifest compiler that generates Dockerfiles, docker-compose files, and entrypoint scripts based on manifest configurations. | exp: class:TestGetManifestHomeDir, method:test_home_directory_in_manifest_wins(self) → None, call:get_manifest_home_dir, method:test_user_name_derives_home(self) → None, call:get_manifest_home_dir, method:test_root_fallback(self) → None, call:get_manifest_home_dir, method:test_empty_home_directory_falls_back(self) → None, call:get_manifest_home_dir, class:TestCompileDockerfileHomeDirectory, method:test_env_home_and_workdir_use_home_directory(self) → None, call:compile_dockerfile, method:test_workspace_symlink_created(self) → None, call:compile_dockerfile, method:test_runtime_working_dir_overrides_home_workdir(self) → None, call:compile_dockerfile, method:test_working_dir_expands_tilde(self) → None, call:compile_dockerfile, class:TestCompileComposeHomeDirectory, method:test_default_repo_mount_synthesized(self) → None, call:compile_compose, method:test_explicit_repo_mount_preserved(self) → None, call:compile_compose, method:test_workspace_name_substituted_in_mount_target(self) → None, call:compile_compose, method:test_working_dir_expands_home(self) → None, call:compile_compose, class:TestCompileEntrypoint, method:test_entrypoint_creates_home_and_workspace(self) → None, call:compile_entrypoint, method:test_entrypoint_uses_sudo_for_workspace_symlink(self) → None, call:compile_entrypoint, method:test_entrypoint_fixes_mount_owners(self) → None, call:compile_entrypoint, func:test_compile_dockerfile_creates_config_dirs_for_user() → None, call:compile_dockerfile, func:test_compile_dockerfile_no_user_does_not_create_home() → None, call:compile_dockerfile, func:test_compile_dockerfile_uses_user_npm_prefix() → None, call:compile_dockerfile | dep: pytest, src.services.build.manifest_compiler - test_migration_metadata.py | Tests Alembic database migration files for correct table definitions and revision chain metadata | exp: func:test_initial_migration_defines_all_core_tables() → None, call:Path(__file__).resolve, call:spec_from_file_location, call:module_from_spec, call:spec.loader.exec_module, func:test_refresh_tokens_migration_has_expected_revision_chain() → None, call:Path(__file__).resolve, call:spec_from_file_location, call:module_from_spec, call:spec.loader.exec_module | dep: pytest, importlib.util, pathlib, pathlib.Path - test_monitoring_models.py | Unit tests verifying creation, persistence, and querying of monitoring models (InstanceEvent and HealthCheck) with database migration compatibility. | exp: func:test_instance_event_creation(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:InstanceEvent, call:db_session.refresh, call:isinstance, func:test_health_check_creation(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:HealthCheck, call:db_session.refresh, call:isinstance, func:test_instance_event_query_by_instance(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:InstanceEvent, call:db_session.execute, call:select(InstanceEvent).where, call:result.scalar_one | dep: uuid, datetime, pytest, sqlalchemy, src.models.system.health_check, src.models.system.instance_event, src.models.tool.tool_instance, src.models.user.user - test_notification_service.py | Unit tests for NotificationService covering CRUD operations, filtering, sorting, and ownership isolation. | exp: func:notification_service() → NotificationService, call:NotificationService, func:user_a(db_session: AsyncSession) → User, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, func:user_b(db_session: AsyncSession) → User, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, func:test_create_notification(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:uuid.uuid4, func:test_list_notifications_orders_by_created_at_desc(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:datetime.now, call:timedelta, call:db_session.commit, call:db_session.refresh, call:notification_service.list_notifications, func:test_list_notifications_excludes_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:notification_service.list_notifications, func:test_list_notifications_unread_only(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.mark_read, call:notification_service.list_notifications, func:test_get_unread_count(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_read, call:notification_service.get_unread_count, func:test_mark_read_sets_read_at(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.mark_read, func:test_mark_all_read_affects_all_unread(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_all_read, call:notification_service.get_unread_count, func:test_dismiss_sets_dismissed_at(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:db_session.execute, call:select(Notification).where, call:result.scalar_one, func:test_mark_read_wrong_owner_raises(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:notification_service.create_notification, call:pytest.raises, call:notification_service.mark_read, func:test_dismiss_wrong_owner_raises(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:notification_service.create_notification, call:pytest.raises, call:notification_service.dismiss, func:test_list_notifications_mute_categories(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.list_notifications, func:test_get_unread_count_excludes_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:notification_service.get_unread_count, func:test_dismiss_all_affects_all_non_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.dismiss_all, call:notification_service.list_notifications, func:test_dismiss_all_affects_only_caller(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:range, call:notification_service.create_notification, call:notification_service.dismiss_all, call:notification_service.list_notifications, func:test_mark_all_read_affects_only_caller(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_all_read, call:notification_service.get_unread_count | dep: uuid, datetime, pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.models.system.notification, src.models.user.user, src.services.shared.notification_service, NotificationService, Notification, User, AsyncSession @@ -30,7 +30,7 @@ Contains unit tests for the API application's core services, utilities, and infr - test_readiness_probe.py | Unit tests for a Docker container readiness probe service that executes commands via docker exec with retry logic. | exp: class:TestExecuteProbe, class:TestIntegrationScenarios | dep: unittest.mock, src.services.shared.readiness_probe, subprocess - test_ssh_keys.py | Unit tests for SSH key preparation functionality including file creation, permissions, ownership, and error handling | exp: class:TestPrepareSshKeyFiles | dep: os, pathlib, unittest.mock, pytest, src.services.shared.ssh_keys ## arch -Standard Python unittest/pytest pattern with heavy use of mocking for external dependencies (Docker, Git, subprocess, database) to test business logic in isolation without live services. +Standard pytest-based unit testing with heavy use of mocking (subprocess, filesystem, database) to test components in isolation without external dependencies; tests are organized by service/feature with descriptive naming. ## tags test, url, call:notification, git, home, call:, merge, call:db ## symbols @@ -47,5 +47,7 @@ test, url, call:notification, git, home, call:, merge, call:db read: __init__.py, test_alembic_migrations.py, test_config.py - change unit config read: test_config.py, test_config_profile_resolver.py +- explore unit subdirectories + index: apps/api/tests/unit/.ruff_cache/.pi-map.index.md ## dirty - diff --git a/apps/api/tests/unit/test_manifest_compiler.py b/apps/api/tests/unit/test_manifest_compiler.py index 145d6fe..7584f34 100644 --- a/apps/api/tests/unit/test_manifest_compiler.py +++ b/apps/api/tests/unit/test_manifest_compiler.py @@ -250,6 +250,20 @@ class TestCompileEntrypoint: assert 'ln -sfn "$WORKSPACE_TARGET" /workspace' in entrypoint assert 'WORKSPACE_NAME="${WORKSPACE_NAME:-workspace}"' in entrypoint + def test_entrypoint_uses_sudo_for_workspace_symlink(self) -> None: + """/workspace is under /, so the non-root entrypoint needs sudo to recreate it.""" + manifest = { + "base_image": "ubuntu:24.04", + "interface_type": "terminal", + "home_directory": "/home/custom", + "user": {"name": "dev", "uid": 1000, "gid": 1000}, + } + entrypoint = compile_entrypoint(manifest) + + assert 'if [ -n "$SUDO" ]; then' in entrypoint + assert 'sudo ln -sfn "$WORKSPACE_TARGET" /workspace' in entrypoint + assert 'elif [ "$(id -u)" = "0" ]; then' in entrypoint + def test_entrypoint_fixes_mount_owners(self) -> None: manifest = { "base_image": "ubuntu:24.04", diff --git a/openspec/.pi-map.index.md b/openspec/.pi-map.index.md index d1ebe12..994f021 100644 --- a/openspec/.pi-map.index.md +++ b/openspec/.pi-map.index.md @@ -2,7 +2,7 @@ dir: openspec ## role -Defines a living documentation methodology and configuration for managing software requirements, specifications, and development workflows within a project repository. +Defines the OpenSpec methodology and configuration for managing software requirements, specifications, and task tracking as living documentation within a project repository. ## parent index: ./.pi-map.index.md map: ./.pi-map.md diff --git a/openspec/changes/.pi-map.index.md b/openspec/changes/.pi-map.index.md index f1987be..b8b2e9a 100644 --- a/openspec/changes/.pi-map.index.md +++ b/openspec/changes/.pi-map.index.md @@ -2,7 +2,7 @@ dir: openspec/changes ## role -Manages change tracking and versioning for OpenAPI specification modifications +Manages and tracks specification changes, diffs, and versioning history for OpenAPI documents. ## parent index: openspec/.pi-map.index.md map: openspec/.pi-map.md diff --git a/openspec/changes/fix-pi-container-mount-permissions/.pi-map.index.md b/openspec/changes/fix-pi-container-mount-permissions/.pi-map.index.md index 09b51b9..56015dc 100644 --- a/openspec/changes/fix-pi-container-mount-permissions/.pi-map.index.md +++ b/openspec/changes/fix-pi-container-mount-permissions/.pi-map.index.md @@ -2,7 +2,7 @@ dir: openspec/changes/fix-pi-container-mount-permissions ## role -Documents a bug fix for resolving permission issues with Pi container repository mounts and npm updates in a development environment. +Documents a bug fix for resolving permission issues with Pi container repository mounts and npm updates in a configurable home directory environment. ## parent index: openspec/changes/.pi-map.index.md map: openspec/changes/.pi-map.md diff --git a/openspec/changes/fix-pi-container-mount-permissions/.pi-map.md b/openspec/changes/fix-pi-container-mount-permissions/.pi-map.md index f1baff9..e5b7cb9 100644 --- a/openspec/changes/fix-pi-container-mount-permissions/.pi-map.md +++ b/openspec/changes/fix-pi-container-mount-permissions/.pi-map.md @@ -4,14 +4,14 @@ dir: openspec/changes/fix-pi-container-mount-permissions index: openspec/changes/fix-pi-container-mount-permissions/.pi-map.index.md ## role -Documents a bug fix for resolving permission issues with Pi container repository mounts and npm updates in a development environment. +Documents a bug fix for resolving permission issues with Pi container repository mounts and npm updates in a configurable home directory environment. ## files -- change.md | Documents a bug fix for pi-agent container repository mounting and npm update permissions in a development environment system. | dep: Alembic, manifest_compiler.py, instance_service.py, pytest, ruff, mypy, npm +- change.md | Documents a bug fix for pi-agent container repository mount paths and npm update permissions in a configurable home directory system. | dep: Alembic, manifest_compiler.py, instance_service.py, Docker compose, npm, pytest - tasks.md | Tracks completion status of tasks for fixing a Pi container repository mount and npm update permissions issue ## arch -Simple documentation-based change tracking using markdown files for issue description (change.md) and task checklist (tasks.md) without code implementation. +Change-request documentation pattern using markdown files for specification (change.md) and task tracking (tasks.md) with checkbox-based completion status. ## tags -npm, tasks, container, repository, update, permissions, py, change +npm, tasks, container, repository, mount, update, permissions, py ## symbols - ## workflows diff --git a/openspec/changes/fix-pi-container-mount-permissions/change.md b/openspec/changes/fix-pi-container-mount-permissions/change.md index f3168ed..6cfa184 100644 --- a/openspec/changes/fix-pi-container-mount-permissions/change.md +++ b/openspec/changes/fix-pi-container-mount-permissions/change.md @@ -10,6 +10,7 @@ After implementing configurable tool container home directories, new `pi-agent` 2. `manifest_compiler.py` does not substitute the instance-specific `{{WORKSPACE_NAME}}` placeholder in explicit mount targets, and `instance_service.py` does not pass `WORKSPACE_NAME`/`REPO_NAME` to `compile_compose` for manifest-based tools. 3. The generated entrypoint hardcodes the literal string `{{WORKSPACE_NAME}}` as the symlink target. 4. `npm_global` packages are installed with `RUN npm install -g ...` as root into the system npm prefix, so the non-root container user cannot update them. +5. Once the repo mount moves out of `/workspace`, the generated `/workspace` compatibility symlink is created in the image as root. The non-root entrypoint cannot replace it (write permission is required on `/`), so container startup fails. ## Fix @@ -22,6 +23,7 @@ After implementing configurable tool container home directories, new `pi-agent` - Pass `WORKSPACE_NAME` as a container environment variable. - Generate the entrypoint symlink from the runtime `WORKSPACE_NAME` environment variable. - Install `npm_global` packages into a user-writable prefix (`{home_dir}/.npm-global`) and add it to `PATH`. + - Use `sudo` or root to create the `/workspace` compatibility symlink, because `/` is owned by root and the non-root entrypoint cannot replace a root-owned symlink. 3. Update `instance_service.py` to pass `REPO_NAME` and `WORKSPACE_NAME` into manifest compilation. 4. Update unit tests for the new behavior.