diff --git a/apps/api/src/api/tool_instances.py b/apps/api/src/api/tool_instances.py index 3e868d6..0dc0874 100644 --- a/apps/api/src/api/tool_instances.py +++ b/apps/api/src/api/tool_instances.py @@ -1341,11 +1341,25 @@ async def start_instance( manifest_def = await session.get(ToolDefinitionManifest, tool_type.manifest_id) if manifest_def: manifest = dict(manifest_def.manifest) + # Merge with base definition if referenced (user config is often in base) + if manifest_def.base_definition_id: + base_def = await session.get( + ToolDefinitionManifest, manifest_def.base_definition_id + ) + if base_def: + manifest = resolve_base(deep_merge(dict(base_def.manifest), manifest)) home_dir = get_manifest_home_dir(manifest) user_cfg = manifest.get("user") if user_cfg: container_uid = user_cfg.get("uid", 0) container_gid = user_cfg.get("gid", 0) + logger.debug( + "Manifest user resolved for instance %s: uid=%s, gid=%s, home=%s", + instance.id, + container_uid, + container_gid, + home_dir, + ) # Apply selected config profile if any instance_dir = os.path.dirname(instance.compose_path) diff --git a/apps/api/src/services/ssh_keys.py b/apps/api/src/services/ssh_keys.py index c2ade84..2ab4c67 100644 --- a/apps/api/src/services/ssh_keys.py +++ b/apps/api/src/services/ssh_keys.py @@ -1,5 +1,6 @@ """SSH key service utilities for preparing keys for container use.""" +import logging import os from pathlib import Path @@ -7,6 +8,8 @@ from cryptography.fernet import Fernet from src.config import Settings +logger = logging.getLogger(__name__) + def _get_fernet() -> Fernet: """Generate a valid Fernet key from the session secret.""" @@ -75,10 +78,20 @@ def prepare_ssh_key_files( os.chown(private_key_path, effective_uid, effective_gid) os.chown(public_key_path, effective_uid, effective_gid) os.chown(config_path, effective_uid, effective_gid) - except PermissionError: - # API process may not be running as root; permission fixer will - # handle this post-start if the mount is read-write - pass + logger.debug( + "Set SSH key ownership to uid=%s gid=%s for %s", + effective_uid, + effective_gid, + ssh_dir, + ) + except PermissionError as exc: + logger.warning( + "Cannot chown SSH keys to uid=%s gid=%s (running as uid=%s): %s", + effective_uid, + effective_gid, + os.getuid(), + exc, + ) return str(ssh_dir) diff --git a/apps/api/tests/unit/test_ssh_keys.py b/apps/api/tests/unit/test_ssh_keys.py index 426fb4d..c659adf 100644 --- a/apps/api/tests/unit/test_ssh_keys.py +++ b/apps/api/tests/unit/test_ssh_keys.py @@ -13,7 +13,9 @@ class TestPrepareSshKeyFiles: """Tests for prepare_ssh_key_files.""" @patch("src.services.ssh_keys._get_fernet") - def test_creates_files_with_default_permissions(self, mock_fernet, tmp_path) -> None: + def test_creates_files_with_default_permissions( + self, mock_fernet, tmp_path + ) -> None: mock_fernet.return_value.decrypt.return_value = b"private-key-content" ssh_key = MagicMock() ssh_key.private_key_encrypted = "enc" @@ -35,9 +37,7 @@ class TestPrepareSshKeyFiles: ssh_key.public_key = "ssh-ed25519 AAA test@test" with patch("os.chown") as mock_chown: - ssh_dir = prepare_ssh_key_files( - str(tmp_path), ssh_key, uid=1001, gid=1001 - ) + ssh_dir = prepare_ssh_key_files(str(tmp_path), ssh_key, uid=1001, gid=1001) # os.chown is called for the directory and each of the 3 files assert mock_chown.call_count == 4 @@ -56,8 +56,6 @@ class TestPrepareSshKeyFiles: with patch("os.chown", side_effect=PermissionError("not allowed")): # Should not raise - ssh_dir = prepare_ssh_key_files( - str(tmp_path), ssh_key, uid=1001, gid=1001 - ) + ssh_dir = prepare_ssh_key_files(str(tmp_path), ssh_key, uid=1001, gid=1001) assert Path(ssh_dir).exists()