From 72bf4ed9620446954eeaab644cff2358480c3c35 Mon Sep 17 00:00:00 2001 From: Developer Date: Fri, 12 Jun 2026 22:08:57 +0000 Subject: [PATCH] fix: chown staged mount sources to container user Config-profile and git mounts staged under instance_dir were created by the API process (root), so when bind-mounted over ~/.config the container user could not write. Recursively chown staged sources to the resolved container uid/gid before compose up. Quality gates: python3 -m py_compile passed; ruff/pytest skipped (test tooling not available in this shell, helper smoke tested with a temporary directory). --- .../api/src/services/tool/instance_service.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/apps/api/src/services/tool/instance_service.py b/apps/api/src/services/tool/instance_service.py index 5ca711f..6b1af3f 100644 --- a/apps/api/src/services/tool/instance_service.py +++ b/apps/api/src/services/tool/instance_service.py @@ -1,6 +1,7 @@ """Tool instance service functions.""" import asyncio +import contextlib import glob as glob_module import logging import os @@ -71,6 +72,41 @@ from src.auth.dependencies import _get_owned_project, _get_user logger = logging.getLogger(__name__) _event_bus = InstanceEventBus() + +def _chown_path(path: str, uid: int, gid: int) -> None: + """Recursively chown a path, suppressing permission errors.""" + try: + if os.path.isdir(path): + for root, dirs, files in os.walk(path): + for name in dirs + files: + full = os.path.join(root, name) + with contextlib.suppress(OSError): + os.chown(full, uid, gid) + with contextlib.suppress(OSError): + os.chown(path, uid, gid) + except Exception as exc: + logger.warning("Failed to chown %s to %s:%s: %s", path, uid, gid, exc) + + +def _chown_staged_mounts( + extra_volumes: list[dict], + instance_dir: str, + uid: int, + gid: int, +) -> None: + """Recursively chown staged mount sources to the container user. + + Config-profile mounts, git mounts, and SSH key mounts are staged under + instance_dir by the API process (root). Without this, the container + user cannot write into bind-mounted directories such as ~/.config. + """ + for vol in extra_volumes: + source = vol.get("source", "") + if not source or not source.startswith(instance_dir): + continue + _chown_path(source, uid, gid) + + async def resolve_git_mounts( session: AsyncSession, resolved: ResolvedProfile, @@ -1308,6 +1344,10 @@ async def start_tool_instance( ssh_target, ) + # Ensure staged bind-mount sources are owned by the container user so + # directories like ~/.config remain writable inside the container. + _chown_staged_mounts(extra_volumes, instance_dir, container_uid, container_gid) + # ── MANIFEST-BASED FLOW ────────────────────────────────────── resolved_manifest = None