Files
headquarter/apps/api/src/services/tool/.pi-map.md
T
Developer ddd92e3dd4 feat: implement configurable tool container home directory
- Add ToolType.home_directory column with default /home/user
- Add Alembic migration to add column, set existing rows, and rewrite
  /workspace to /home/user/{{WORKSPACE_NAME}} in legacy templates
- Add merge migration fc8f1a20cbf6 to resolve Alembic multiple heads
- Update manifest compiler to honor manifest.home_directory for HOME,
  WORKDIR, /workspace symlink, and default repo mount target
- Update legacy dockerfile/compose instance generation to use
  tool_type.home_directory
- Thread resolved home_dir through config profile and git mount expansion
- Generate entrypoint permission fixer to chown home/mounts at startup
- Update base.dockerfile with sudo/passwordless sudo for permission fixer
- Add unit tests for manifest compiler, instance service, and migrations
- Add placeholder integration test for container lifecycle
- Update openspec/tasks/home-path-expansion.md task checkboxes
- Update project maps for modified files

Quality gates: py_compile, ruff, mypy, pytest tests/unit (205 passed),
pytest tests/integration (110 passed, 35 skipped). Alembic round-trip
and container lifecycle integration tests require Docker/PostgreSQL.
2026-06-14 13:09:41 +00:00

11 KiB

apps/api/src/services/tool

dir: apps/api/src/services/tool

index: apps/api/src/services/tool/.pi-map.index.md

role

Orchestrates end-to-end deployment and runtime management of development tool instances via containerized environments with remote access capabilities.

files

  • instance_service.py | Manages tool instance lifecycle operations including git repository mounting, config profile resolution, Docker compose file generation, container orchestration, and SSH tunnel management for development environments. | exp: func:_chown_path(path: str, uid: int, gid: int) → None, call:os.path.isdir, call:os.walk, call:os.path.join, call:contextlib.suppress, call:os.chown, call:logger.warning, func:_chown_staged_mounts(extra_volumes: list[dict], instance_dir: str, uid: int, gid: int) → None, call:vol.get, call:source.startswith, call:_chown_path, func:resolve_git_mounts(session: AsyncSession, resolved: ResolvedProfile, instance_dir, working_directory, home_dir) → list[dict], call:tasks.append, call:resolve_single_git_mount, call:asyncio.gather, call:isinstance, call:logger.warning, call:volume_mounts.extend, func:normalize_git_mount(entry: dict) → dict, call:dict, call:entry.get, call:entry.pop, func:clone_git_repo(remote_url: str, branch: str | None, clone_parent: str) → str, call:hashlib.md5(remote_url.encode()).hexdigest, call:remote_url.encode, call:remote_url.split("/")[-1].replace, call:os.path.join, call:os.path.exists, call:os.makedirs, call:clone_repository, call:logger.debug, call:logger.warning, call:pull_repository_updates, call:checkout_branch, func:resolve_git_mount_mappings(repo_path: str, mappings: list[dict], working_directory: str | None, home_dir) → list[dict], call:mapping.get, call:logger.warning, call:expand_container_path, call:target_path.startswith, call:os.path.join, call:expand_glob_source, call:os.path.exists, call:len, call:os.path.relpath, call:volume_mounts.append, call:logger.debug, func:resolve_single_git_mount(session: AsyncSession, git_mount: dict, instance_dir, working_directory, home_dir) → list[dict], call:normalize_git_mount, call:git_mount.get, call:logger.warning, call:asyncio.to_thread, call:resolve_git_mount_mappings, func:checkout_branch(repo_path: str, branch: str) → bool, call:subprocess.run, call:logger.warning, call:result.stderr.strip, func:pull_repository_updates(repo_path: str, remote_url: str) → None, call:subprocess.run, raise:RuntimeError, func:expand_glob_source(source_path: str, repo_path: str) → list[str], call:any, call:os.path.exists, call:glob_module.glob, call:len, call:os.path.abspath, call:abs_path.startswith, call:results.append, call:logger.warning, func:validate_config_profile(session: AsyncSession, profile_id: str | None, user_id: uuid.UUID, project_id: uuid.UUID, tool_type_id: uuid.UUID) → uuid.UUID | None, call:uuid.UUID, call:session.get, raise:HTTPException, func:sanitize_compose_file(compose_path: str) → None, call:Path, call:compose_file.exists, call:compose_file.read_text, call:yaml.safe_load, call:compose_data["services"].items, call:isinstance, call:port_mapping.split, call:len, call:valid_ports.append, call:compose_file.write_text, call:yaml.dump, func:modify_compose_file(compose_path: str, port_override, start_command, working_directory, extra_volumes, home_dir) → None, call:Path, call:compose_file.read_text, call:yaml.safe_load, call:compose_data["services"].items, call:enumerate, call:isinstance, call:port_mapping.split, call:expand_container_path, call:vol.get, call:service_config["volumes"].append, call:service_config.get, call:sort_volumes_by_specificity, call:compose_file.write_text, call:yaml.dump, func:ensure_container_name_in_compose(compose_path: str, container_name: str) → None, call:Path, call:compose_file.exists, call:compose_file.read_text, call:yaml.safe_load, call:compose_data["services"].items, call:container_name.lower, call:compose_file.write_text, call:yaml.dump, call:logger.info, func:ensure_web_bind_address(compose_path: str, tool_type_name: str, default_port: int) → None, call:KNOWN_BIND_FIXES.get, call:Path, call:compose_file.exists, call:compose_file.read_text, call:yaml.safe_load, call:compose_data["services"].values, call:service_config.get, call:compose_file.write_text, call:yaml.dump, call:logger.warning, call:logger.info, func:ensure_backend_network_in_compose(compose_path: str) → None, call:Path, call:compose_file.exists, call:compose_file.read_text, call:yaml.safe_load, call:get_backend_network_name, call:compose_data["services"].values, call:svc_config.get, call:compose_file.write_text, call:yaml.dump, call:logger.info, func:prepare_manifest_instance(session: AsyncSession, instance: ToolInstance, instance_dir: str, repo_path: str, env_vars: dict, extra_volumes: list, working_directory: str | None) → tuple[str, str, dict, str], call:session.get, call:dict, call:resolve_base, call:deep_merge, call:logger.warning, call:merge_with_config, call:manifest.pop, call:env_vars.update, call:extra_volumes.extend, call:compute_image_tag, call:subprocess.run, call:check.stdout.strip, call:compile_dockerfile, call:compile_entrypoint, call:logger.debug, call:len, call:asyncio.to_thread, call:logger.info, call:resolve_profile, call:gm.get, call:instance.name.lower, call:compile_compose, call:manifest.get, call:datetime.now, call:get_manifest_home_dir, raise:RuntimeError, func:create_tool_instance(session: AsyncSession, user_id: uuid.UUID, project_id: uuid.UUID, repo_id: uuid.UUID, data: "CreateInstanceRequest") → ToolInstance, call:session.get, call:uuid.UUID, call:validate_config_profile, call:uuid.uuid4, call:select(ToolInstance) .where(ToolInstance.workspace_id == workspace_id) .where(ToolInstance.tool_type_id == tool_type_id) .where, call:select(ToolInstance) .where(ToolInstance.repository_id == repo_id) .where(ToolInstance.tool_type_id == tool_type_id) .where, call:session.execute, call:len, call:result.scalars().all, call:ensure_instance_directory, call:os.path.join, call:find_free_port, call:f"headquarter/{instance_name}:latest".lower, call:asyncio.to_thread, call:logger.error, call:logger.info, call:os.path.basename, call:os.path.normpath, call:instance_name.lower, call:write_compose_file, call:dict, call:resolve_base, call:deep_merge, call:compute_image_tag, call:compile_compose, call:str, call:render_compose_template, call:ToolInstance, call:session.add, call:session.commit, call:session.refresh, call:publish_lifecycle_event, raise:ValueError, raise:RuntimeError, func:start_tool_instance(session: AsyncSession, user_id: uuid.UUID, project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, data: "StartInstanceRequest | None") → dict, call:session.get, call:validate_config_profile, call:session.commit, call:os.path.exists, call:logger.info, call:dict, call:resolve_base, call:deep_merge, call:get_manifest_home_dir, call:manifest.get, call:user_cfg.get, call:logger.debug, call:os.path.dirname, call:resolve_profile, call:apply_resolved_profile, call:env_vars.update, call:config_files.update, call:extra_volumes.extend, call:resolve_git_mounts, call:profile_hints.get, call:len, call:logger.error, call:write_env_file, call:write_config_files, call:uuid.UUID, call:ssh_keys_to_mount.append, call:logger.warning, call:os.path.join, call:os.makedirs, call:_sanitize_filename, call:key_filenames.append, call:prepare_ssh_key_files, call:write_ssh_config, call:extra_volumes.append, call:_chown_staged_mounts, call:prepare_manifest_instance, call:write_compose_file, call:logger.exception, call:modify_compose_file, call:sanitize_compose_file, call:ensure_web_bind_address, call:ensure_container_name_in_compose, call:ensure_backend_network_in_compose, call:execute_compose_command, call:instance.name.lower, call:get_container_id, call:publish_lifecycle_event, call:wait_for_container_running, call:get_container_logs, call:resolved_manifest.get, call:apply_mount_permissions, call:home_dir.startswith, call:apply_ssh_permissions, call:probe_config.get, call:execute_probe, call:datetime.now().isoformat, call:"\n".join, call:start_tunnel, call:str, call:traceback.format_exc, raise:ValueError, raise:RuntimeError, func:restart_tool_instance(session: AsyncSession, user_id: uuid.UUID, project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID) → dict, call:session.get, call:stop_tunnel, call:logger.debug, call:logger.warning, call:os.path.exists, call:os.path.dirname, call:resolve_profile, call:apply_resolved_profile, call:write_env_file, call:logger.error, call:sanitize_compose_file, call:ensure_web_bind_address, call:ensure_container_name_in_compose, call:ensure_backend_network_in_compose, call:execute_compose_command, call:datetime.now, call:session.commit, call:start_tunnel, call:publish_lifecycle_event, raise:ValueError, raise:RuntimeError, func:delete_tool_instance(session: AsyncSession, user_id: uuid.UUID, project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, force) → None, call:session.get, call:os.path.dirname, call:os.path.join, call:os.path.exists, call:check_dirty_state, call:stop_tunnel, call:logger.debug, call:logger.warning, call:execute_compose_command, call:shutil.rmtree, call:publish_lifecycle_event, call:session.delete, call:session.commit, raise:ValueError, raise:RuntimeError, func:recreate_instance_tunnel(session: AsyncSession, user_id: uuid.UUID, project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID) → dict, call:session.get, call:instance.name.lower, call:logger.info, call:get_container_id, call:logger.error, call:get_backend_network_name, call:is_container_on_network, call:connect_container_to_network, call:get_container_ip_on_network, call:logger.warning, call:recreate_tunnel, call:check_tunnel_health, call:health.get, call:subprocess.run, call:probe.stdout.strip, call:session.commit, call:logger.exception, raise:ValueError, raise:RuntimeError, func:stop_tool_instance(session: AsyncSession, user_id: uuid.UUID, project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID) → dict, call:session.get, call:stop_tunnel, call:logger.warning, call:os.path.exists, call:execute_compose_command, call:datetime.now, call:session.commit, call:publish_lifecycle_event, raise:ValueError, func:rename_tool_instance(session: AsyncSession, user_id: uuid.UUID, project_id: uuid.UUID, repo_id: uuid.UUID, instance_id: uuid.UUID, display_name: str) → ToolInstance, call:session.get, call:display_name.strip, call:session.commit, call:session.refresh, raise:ValueError | dep: asyncio, contextlib, logging, os, subprocess, uuid, datetime, fastapi, sqlalchemy, sqlalchemy.ext.asyncio, src.models, src.schemas.tool, src.services.git.clone, src.services.config.config_profile_resolver, src.services.docker, src.services.shared.tunnel, src.services.build.docker_build, src.services.build.manifest_compiler, src.services.shared.permission_fixer, src.services.shared.readiness_probe, src.services.shared.ssh_keys, src.services.instance.event_bus, src.services.instance.lifecycle_hooks, hashlib, yaml, pathlib, traceback, shutil

arch

Service-layer facade combining infrastructure-as-code (Docker Compose generation), GitOps (repository mounting), external configuration resolution, and network tunneling (SSH) into a unified lifecycle management pattern.

tags

compose, call:compose, call:logger.warning, error, instance, container, git, text

symbols

  • _chown_path
  • _chown_staged_mounts
  • resolve_git_mounts
  • normalize_git_mount
  • clone_git_repo
  • resolve_git_mount_mappings
  • resolve_single_git_mount
  • checkout_branch

workflows

  • change tool behavior read: instance_service.py

dirty