ccproxy-api 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. ccproxy/__init__.py +4 -0
  2. ccproxy/__main__.py +7 -0
  3. ccproxy/_version.py +21 -0
  4. ccproxy/adapters/__init__.py +11 -0
  5. ccproxy/adapters/base.py +80 -0
  6. ccproxy/adapters/openai/__init__.py +43 -0
  7. ccproxy/adapters/openai/adapter.py +915 -0
  8. ccproxy/adapters/openai/models.py +412 -0
  9. ccproxy/adapters/openai/streaming.py +449 -0
  10. ccproxy/api/__init__.py +28 -0
  11. ccproxy/api/app.py +225 -0
  12. ccproxy/api/dependencies.py +140 -0
  13. ccproxy/api/middleware/__init__.py +11 -0
  14. ccproxy/api/middleware/auth.py +0 -0
  15. ccproxy/api/middleware/cors.py +55 -0
  16. ccproxy/api/middleware/errors.py +703 -0
  17. ccproxy/api/middleware/headers.py +51 -0
  18. ccproxy/api/middleware/logging.py +175 -0
  19. ccproxy/api/middleware/request_id.py +69 -0
  20. ccproxy/api/middleware/server_header.py +62 -0
  21. ccproxy/api/responses.py +84 -0
  22. ccproxy/api/routes/__init__.py +16 -0
  23. ccproxy/api/routes/claude.py +181 -0
  24. ccproxy/api/routes/health.py +489 -0
  25. ccproxy/api/routes/metrics.py +1033 -0
  26. ccproxy/api/routes/proxy.py +238 -0
  27. ccproxy/auth/__init__.py +75 -0
  28. ccproxy/auth/bearer.py +68 -0
  29. ccproxy/auth/credentials_adapter.py +93 -0
  30. ccproxy/auth/dependencies.py +229 -0
  31. ccproxy/auth/exceptions.py +79 -0
  32. ccproxy/auth/manager.py +102 -0
  33. ccproxy/auth/models.py +118 -0
  34. ccproxy/auth/oauth/__init__.py +26 -0
  35. ccproxy/auth/oauth/models.py +49 -0
  36. ccproxy/auth/oauth/routes.py +396 -0
  37. ccproxy/auth/oauth/storage.py +0 -0
  38. ccproxy/auth/storage/__init__.py +12 -0
  39. ccproxy/auth/storage/base.py +57 -0
  40. ccproxy/auth/storage/json_file.py +159 -0
  41. ccproxy/auth/storage/keyring.py +192 -0
  42. ccproxy/claude_sdk/__init__.py +20 -0
  43. ccproxy/claude_sdk/client.py +169 -0
  44. ccproxy/claude_sdk/converter.py +331 -0
  45. ccproxy/claude_sdk/options.py +120 -0
  46. ccproxy/cli/__init__.py +14 -0
  47. ccproxy/cli/commands/__init__.py +8 -0
  48. ccproxy/cli/commands/auth.py +553 -0
  49. ccproxy/cli/commands/config/__init__.py +14 -0
  50. ccproxy/cli/commands/config/commands.py +766 -0
  51. ccproxy/cli/commands/config/schema_commands.py +119 -0
  52. ccproxy/cli/commands/serve.py +630 -0
  53. ccproxy/cli/docker/__init__.py +34 -0
  54. ccproxy/cli/docker/adapter_factory.py +157 -0
  55. ccproxy/cli/docker/params.py +278 -0
  56. ccproxy/cli/helpers.py +144 -0
  57. ccproxy/cli/main.py +193 -0
  58. ccproxy/cli/options/__init__.py +14 -0
  59. ccproxy/cli/options/claude_options.py +216 -0
  60. ccproxy/cli/options/core_options.py +40 -0
  61. ccproxy/cli/options/security_options.py +48 -0
  62. ccproxy/cli/options/server_options.py +117 -0
  63. ccproxy/config/__init__.py +40 -0
  64. ccproxy/config/auth.py +154 -0
  65. ccproxy/config/claude.py +124 -0
  66. ccproxy/config/cors.py +79 -0
  67. ccproxy/config/discovery.py +87 -0
  68. ccproxy/config/docker_settings.py +265 -0
  69. ccproxy/config/loader.py +108 -0
  70. ccproxy/config/observability.py +158 -0
  71. ccproxy/config/pricing.py +88 -0
  72. ccproxy/config/reverse_proxy.py +31 -0
  73. ccproxy/config/scheduler.py +89 -0
  74. ccproxy/config/security.py +14 -0
  75. ccproxy/config/server.py +81 -0
  76. ccproxy/config/settings.py +534 -0
  77. ccproxy/config/validators.py +231 -0
  78. ccproxy/core/__init__.py +274 -0
  79. ccproxy/core/async_utils.py +675 -0
  80. ccproxy/core/constants.py +97 -0
  81. ccproxy/core/errors.py +256 -0
  82. ccproxy/core/http.py +328 -0
  83. ccproxy/core/http_transformers.py +428 -0
  84. ccproxy/core/interfaces.py +247 -0
  85. ccproxy/core/logging.py +189 -0
  86. ccproxy/core/middleware.py +114 -0
  87. ccproxy/core/proxy.py +143 -0
  88. ccproxy/core/system.py +38 -0
  89. ccproxy/core/transformers.py +259 -0
  90. ccproxy/core/types.py +129 -0
  91. ccproxy/core/validators.py +288 -0
  92. ccproxy/docker/__init__.py +67 -0
  93. ccproxy/docker/adapter.py +588 -0
  94. ccproxy/docker/docker_path.py +207 -0
  95. ccproxy/docker/middleware.py +103 -0
  96. ccproxy/docker/models.py +228 -0
  97. ccproxy/docker/protocol.py +192 -0
  98. ccproxy/docker/stream_process.py +264 -0
  99. ccproxy/docker/validators.py +173 -0
  100. ccproxy/models/__init__.py +123 -0
  101. ccproxy/models/errors.py +42 -0
  102. ccproxy/models/messages.py +243 -0
  103. ccproxy/models/requests.py +85 -0
  104. ccproxy/models/responses.py +227 -0
  105. ccproxy/models/types.py +102 -0
  106. ccproxy/observability/__init__.py +51 -0
  107. ccproxy/observability/access_logger.py +400 -0
  108. ccproxy/observability/context.py +447 -0
  109. ccproxy/observability/metrics.py +539 -0
  110. ccproxy/observability/pushgateway.py +366 -0
  111. ccproxy/observability/sse_events.py +303 -0
  112. ccproxy/observability/stats_printer.py +755 -0
  113. ccproxy/observability/storage/__init__.py +1 -0
  114. ccproxy/observability/storage/duckdb_simple.py +665 -0
  115. ccproxy/observability/storage/models.py +55 -0
  116. ccproxy/pricing/__init__.py +19 -0
  117. ccproxy/pricing/cache.py +212 -0
  118. ccproxy/pricing/loader.py +267 -0
  119. ccproxy/pricing/models.py +106 -0
  120. ccproxy/pricing/updater.py +309 -0
  121. ccproxy/scheduler/__init__.py +39 -0
  122. ccproxy/scheduler/core.py +335 -0
  123. ccproxy/scheduler/exceptions.py +34 -0
  124. ccproxy/scheduler/manager.py +186 -0
  125. ccproxy/scheduler/registry.py +150 -0
  126. ccproxy/scheduler/tasks.py +484 -0
  127. ccproxy/services/__init__.py +10 -0
  128. ccproxy/services/claude_sdk_service.py +614 -0
  129. ccproxy/services/credentials/__init__.py +55 -0
  130. ccproxy/services/credentials/config.py +105 -0
  131. ccproxy/services/credentials/manager.py +562 -0
  132. ccproxy/services/credentials/oauth_client.py +482 -0
  133. ccproxy/services/proxy_service.py +1536 -0
  134. ccproxy/static/.keep +0 -0
  135. ccproxy/testing/__init__.py +34 -0
  136. ccproxy/testing/config.py +148 -0
  137. ccproxy/testing/content_generation.py +197 -0
  138. ccproxy/testing/mock_responses.py +262 -0
  139. ccproxy/testing/response_handlers.py +161 -0
  140. ccproxy/testing/scenarios.py +241 -0
  141. ccproxy/utils/__init__.py +6 -0
  142. ccproxy/utils/cost_calculator.py +210 -0
  143. ccproxy/utils/streaming_metrics.py +199 -0
  144. ccproxy_api-0.1.0.dist-info/METADATA +253 -0
  145. ccproxy_api-0.1.0.dist-info/RECORD +148 -0
  146. ccproxy_api-0.1.0.dist-info/WHEEL +4 -0
  147. ccproxy_api-0.1.0.dist-info/entry_points.txt +2 -0
  148. ccproxy_api-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,207 @@
1
+ """Docker path management with clean API."""
2
+
3
+ from pathlib import Path
4
+ from typing import Self
5
+
6
+ from pydantic import BaseModel, field_validator
7
+ from structlog import get_logger
8
+
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ class DockerPath(BaseModel):
14
+ """Represents a mapping between host and container paths.
15
+
16
+ Provides a clean API for Docker volume mounting and path resolution.
17
+
18
+ Example:
19
+ workspace = DockerPath(host_path="/some/host/local/path", container_path="/tmp/docker/workspace")
20
+ docker_vol = workspace.vol() # Returns volume mapping tuple
21
+ container_path = workspace.container() # Returns container path
22
+ host_path = workspace.host() # Returns host path
23
+ """
24
+
25
+ host_path: Path | None = None
26
+ container_path: str
27
+ env_definition_variable_name: str | None = None
28
+
29
+ @field_validator("host_path", mode="before")
30
+ @classmethod
31
+ def _resolve_host_path(cls, v: str | Path | None) -> Path | None:
32
+ """Resolve host path to an absolute path."""
33
+ if v is None:
34
+ return None
35
+ return Path(v).resolve()
36
+
37
+ def vol(self) -> tuple[str, str]:
38
+ """Get Docker volume mapping tuple.
39
+
40
+ Returns:
41
+ tuple[str, str]: (host_path, container_path) for Docker -v flag
42
+ """
43
+ if self.host_path is None:
44
+ raise ValueError("host_path is not set, cannot create volume mapping")
45
+ return (str(self.host_path), self.container_path)
46
+
47
+ def host(self) -> Path:
48
+ """Get host path as Path object.
49
+
50
+ Returns:
51
+ Path: Resolved host path
52
+ """
53
+ if self.host_path is None:
54
+ raise ValueError("host_path is not set")
55
+ return self.host_path
56
+
57
+ def container(self) -> str:
58
+ """Get container path as string.
59
+
60
+ Returns:
61
+ str: Container path
62
+ """
63
+ return self.container_path
64
+
65
+ def join(self, *subpaths: str) -> "DockerPath":
66
+ """Create new DockerPath with subpaths joined to both host and container paths.
67
+
68
+ Args:
69
+ *subpaths: Path components to join
70
+
71
+ Returns:
72
+ DockerPath: New instance with joined paths
73
+ """
74
+ host_joined = self.host_path
75
+ if host_joined:
76
+ for subpath in subpaths:
77
+ host_joined = host_joined / subpath
78
+
79
+ container_joined = self.container_path
80
+ for subpath in subpaths:
81
+ container_joined = f"{container_joined}/{subpath}".replace("//", "/")
82
+
83
+ return DockerPath(host_path=host_joined, container_path=container_joined)
84
+
85
+ def get_env_definition(self) -> str:
86
+ return f"{self.env_definition_variable_name}={self.container_path} # {self.host_path}"
87
+
88
+ def __str__(self) -> str:
89
+ """String representation showing the mapping."""
90
+ if self.host_path:
91
+ return f"DockerPath({self.host_path} -> {self.container_path})"
92
+ return f"DockerPath(container_path={self.container_path})"
93
+
94
+ def __repr__(self) -> str:
95
+ """Detailed representation."""
96
+ return f"DockerPath(host_path={self.host_path!r}, container_path={self.container_path!r})"
97
+
98
+
99
+ class DockerPathSet:
100
+ """Collection of named Docker paths for organized path management.
101
+
102
+ Example:
103
+ paths = DockerPathSet("/tmp/build")
104
+ paths.add("workspace", "/workspace")
105
+ paths.add("config", "/workspace/config")
106
+
107
+ workspace_vol = paths.get("workspace").vol()
108
+ config_path = paths.get("config").container()
109
+ """
110
+
111
+ def __init__(self, base_host_path: str | Path | None = None) -> None:
112
+ """Initialize Docker path set.
113
+
114
+ Args:
115
+ base_host_path: Base path on host for all paths in this set
116
+ """
117
+ self.base_host_path = Path(base_host_path).resolve() if base_host_path else None
118
+ self.paths: dict[str, DockerPath] = {}
119
+ self.logger = get_logger(f"{__name__}.{self.__class__.__name__}")
120
+
121
+ def add(
122
+ self, name: str, container_path: str, host_subpath: str | None = None
123
+ ) -> Self:
124
+ """Add a named Docker path to the set.
125
+
126
+ Args:
127
+ name: Logical name for the path
128
+ container_path: Path inside the Docker container
129
+ host_subpath: Optional subpath from base_host_path, defaults to name
130
+
131
+ Returns:
132
+ Self: For method chaining
133
+ """
134
+ if self.base_host_path is None:
135
+ raise ValueError("base_host_path must be set to use add() method")
136
+
137
+ if host_subpath is None:
138
+ host_subpath = name
139
+
140
+ # Handle empty string to mean no subpath (use base path directly)
141
+ if host_subpath == "":
142
+ host_path = self.base_host_path
143
+ else:
144
+ host_path = self.base_host_path / host_subpath
145
+
146
+ self.paths[name] = DockerPath(
147
+ host_path=host_path, container_path=container_path
148
+ )
149
+ return self
150
+
151
+ def add_path(self, name: str, docker_path: DockerPath) -> Self:
152
+ """Add a pre-created DockerPath to the set.
153
+
154
+ Args:
155
+ name: Logical name for the path
156
+ docker_path: DockerPath instance to add
157
+
158
+ Returns:
159
+ Self: For method chaining
160
+ """
161
+ self.paths[name] = docker_path
162
+ return self
163
+
164
+ def get(self, name: str) -> DockerPath:
165
+ """Get Docker path by name.
166
+
167
+ Args:
168
+ name: Logical name of the path
169
+
170
+ Returns:
171
+ DockerPath: The Docker path instance
172
+
173
+ Raises:
174
+ KeyError: If path name is not found
175
+ """
176
+ if name not in self.paths:
177
+ raise KeyError(
178
+ f"Docker path '{name}' not found. Available: {list(self.paths.keys())}"
179
+ )
180
+ return self.paths[name]
181
+
182
+ def has(self, name: str) -> bool:
183
+ """Check if a path name exists in the set.
184
+
185
+ Args:
186
+ name: Logical name to check
187
+
188
+ Returns:
189
+ bool: True if path exists
190
+ """
191
+ return name in self.paths
192
+
193
+ def volumes(self) -> list[tuple[str, str]]:
194
+ """Get all volume mappings for Docker.
195
+
196
+ Returns:
197
+ list[tuple[str, str]]: List of (host_path, container_path) tuples
198
+ """
199
+ return [path.vol() for path in self.paths.values()]
200
+
201
+ def names(self) -> list[str]:
202
+ """Get all path names in the set.
203
+
204
+ Returns:
205
+ list[str]: List of logical path names
206
+ """
207
+ return list(self.paths.keys())
@@ -0,0 +1,103 @@
1
+ """Docker output middleware for processing and logging container output."""
2
+
3
+ from typing import Any
4
+
5
+ from structlog import get_logger
6
+
7
+ from .stream_process import OutputMiddleware, create_chained_middleware
8
+
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ class LoggerOutputMiddleware(OutputMiddleware[str]):
14
+ """Simple middleware that prints output with optional prefixes.
15
+
16
+ This middleware prints each line to the console with configurable
17
+ prefixes for stdout and stderr streams.
18
+ """
19
+
20
+ def __init__(self, logger: Any, stdout_prefix: str = "", stderr_prefix: str = ""):
21
+ """Initialize middleware with custom prefixes.
22
+
23
+ Args:
24
+ stdout_prefix: Prefix for stdout lines (default: "")
25
+ stderr_prefix: Prefix for stderr lines (default: "")
26
+ """
27
+ self.logger = logger
28
+ self.stderr_prefix = stderr_prefix
29
+ self.stdout_prefix = stdout_prefix
30
+
31
+ async def process(self, line: str, stream_type: str) -> str:
32
+ """Process and print a line with the appropriate prefix.
33
+
34
+ Args:
35
+ line: Output line to process
36
+ stream_type: Either "stdout" or "stderr"
37
+
38
+ Returns:
39
+ The original line (unmodified)
40
+ """
41
+ if stream_type == "stdout":
42
+ self.logger.info(
43
+ "docker_stdout", prefix=self.stdout_prefix, line=line, stream="stdout"
44
+ )
45
+ else:
46
+ self.logger.info(
47
+ "docker_stderr", prefix=self.stderr_prefix, line=line, stream="stderr"
48
+ )
49
+ return line
50
+
51
+
52
+ def create_logger_middleware(
53
+ logger_instance: Any | None = None,
54
+ stdout_prefix: str = "",
55
+ stderr_prefix: str = "",
56
+ ) -> LoggerOutputMiddleware:
57
+ """Factory function to create a LoggerOutputMiddleware instance.
58
+
59
+ Args:
60
+ logger_instance: Logger instance to use (defaults to module logger)
61
+ stdout_prefix: Prefix for stdout lines
62
+ stderr_prefix: Prefix for stderr lines
63
+
64
+ Returns:
65
+ Configured LoggerOutputMiddleware instance
66
+ """
67
+ if logger_instance is None:
68
+ logger_instance = logger
69
+ return LoggerOutputMiddleware(logger_instance, stdout_prefix, stderr_prefix)
70
+
71
+
72
+ def create_chained_docker_middleware(
73
+ middleware_chain: list[OutputMiddleware[Any]],
74
+ include_logger: bool = True,
75
+ logger_instance: Any | None = None,
76
+ stdout_prefix: str = "",
77
+ stderr_prefix: str = "",
78
+ ) -> OutputMiddleware[Any]:
79
+ """Factory function to create chained middleware for Docker operations.
80
+
81
+ Args:
82
+ middleware_chain: List of middleware components to chain together
83
+ include_logger: Whether to automatically add logger middleware at the end
84
+ logger_instance: Logger instance to use (defaults to module logger)
85
+ stdout_prefix: Prefix for stdout lines in logger middleware
86
+ stderr_prefix: Prefix for stderr lines in logger middleware
87
+
88
+ Returns:
89
+ Chained middleware instance
90
+
91
+ """
92
+ final_chain = list(middleware_chain)
93
+
94
+ if include_logger:
95
+ logger_middleware = create_logger_middleware(
96
+ logger_instance, stdout_prefix, stderr_prefix
97
+ )
98
+ final_chain.append(logger_middleware)
99
+
100
+ if len(final_chain) == 1:
101
+ return final_chain[0]
102
+
103
+ return create_chained_middleware(final_chain)
@@ -0,0 +1,228 @@
1
+ """Docker-specific models for cross-domain operations."""
2
+
3
+ import os
4
+ import platform
5
+ from pathlib import Path
6
+ from typing import ClassVar, Literal
7
+
8
+ from pydantic import BaseModel, Field, field_validator
9
+
10
+ from .docker_path import DockerPath
11
+
12
+
13
+ class DockerUserContext(BaseModel):
14
+ """Docker user context for volume permission handling.
15
+
16
+ Represents user information needed for Docker --user flag to
17
+ solve volume permission issues when mounting host directories.
18
+ """
19
+
20
+ uid: int = Field(..., description="User ID for Docker --user flag")
21
+ gid: int = Field(..., description="Group ID for Docker --user flag")
22
+ username: str = Field(..., description="Username for reference")
23
+ enable_user_mapping: bool = Field(
24
+ default=True, description="Whether to enable --user flag in Docker commands"
25
+ )
26
+
27
+ # Path settings using DockerPath
28
+ home_path: DockerPath | None = Field(
29
+ default=None, description="Home directory mapping between host and container"
30
+ )
31
+ workspace_path: DockerPath | None = Field(
32
+ default=None,
33
+ description="Workspace directory mapping between host and container",
34
+ )
35
+
36
+ # Platform compatibility
37
+ _supported_platforms: ClassVar[set[str]] = {"Linux", "Darwin"}
38
+
39
+ @field_validator("uid", "gid")
40
+ @classmethod
41
+ def validate_positive_ids(cls, v: int) -> int:
42
+ """Validate that UID/GID are positive integers."""
43
+ if v < 0:
44
+ raise ValueError("UID and GID must be non-negative")
45
+ return v
46
+
47
+ @field_validator("username")
48
+ @classmethod
49
+ def validate_username(cls, v: str) -> str:
50
+ """Validate username is not empty."""
51
+ if not v or not v.strip():
52
+ raise ValueError("Username cannot be empty")
53
+ return v.strip()
54
+
55
+ @classmethod
56
+ def detect_current_user(
57
+ cls,
58
+ home_path: DockerPath | None = None,
59
+ workspace_path: DockerPath | None = None,
60
+ ) -> "DockerUserContext":
61
+ """Detect current user context from system.
62
+
63
+ Args:
64
+ home_path: Optional home directory DockerPath override
65
+ workspace_path: Optional workspace directory DockerPath override
66
+
67
+ Returns:
68
+ DockerUserContext: Current user's context
69
+
70
+ Raises:
71
+ RuntimeError: If user detection fails or platform unsupported
72
+ """
73
+ current_platform = platform.system()
74
+
75
+ if current_platform not in cls._supported_platforms:
76
+ raise RuntimeError(
77
+ f"User detection not supported on {current_platform}. "
78
+ f"Supported platforms: {', '.join(cls._supported_platforms)}"
79
+ )
80
+
81
+ try:
82
+ uid = os.getuid()
83
+ gid = os.getgid()
84
+ username = os.getenv("USER") or os.getenv("USERNAME") or "unknown"
85
+
86
+ # Create default home path if not provided
87
+ if home_path is None:
88
+ host_home_env = os.getenv("HOME")
89
+ if host_home_env:
90
+ home_path = DockerPath(
91
+ host_path=Path(host_home_env), container_path="/data/home"
92
+ )
93
+
94
+ return cls(
95
+ uid=uid,
96
+ gid=gid,
97
+ username=username,
98
+ enable_user_mapping=True,
99
+ home_path=home_path,
100
+ workspace_path=workspace_path,
101
+ )
102
+
103
+ except AttributeError as e:
104
+ raise RuntimeError(
105
+ f"Failed to detect user on {current_platform}: {e}"
106
+ ) from e
107
+ except Exception as e:
108
+ raise RuntimeError(f"Unexpected error detecting user: {e}") from e
109
+
110
+ @classmethod
111
+ def create_manual(
112
+ cls,
113
+ uid: int,
114
+ gid: int,
115
+ username: str,
116
+ home_path: DockerPath | None = None,
117
+ workspace_path: DockerPath | None = None,
118
+ enable_user_mapping: bool = True,
119
+ ) -> "DockerUserContext":
120
+ """Create manual user context with custom values.
121
+
122
+ Args:
123
+ uid: User ID for Docker --user flag
124
+ gid: Group ID for Docker --user flag
125
+ username: Username for reference
126
+ home_path: Optional home directory DockerPath
127
+ workspace_path: Optional workspace directory DockerPath
128
+ enable_user_mapping: Whether to enable --user flag in Docker commands
129
+
130
+ Returns:
131
+ DockerUserContext: Manual user context
132
+
133
+ Raises:
134
+ ValueError: If validation fails for any parameter
135
+ """
136
+ return cls(
137
+ uid=uid,
138
+ gid=gid,
139
+ username=username,
140
+ enable_user_mapping=enable_user_mapping,
141
+ home_path=home_path,
142
+ workspace_path=workspace_path,
143
+ )
144
+
145
+ def get_docker_user_flag(self) -> str:
146
+ """Get Docker --user flag value.
147
+
148
+ Returns:
149
+ str: Docker user flag in format "uid:gid"
150
+ """
151
+ return f"{self.uid}:{self.gid}"
152
+
153
+ def is_supported_platform(self) -> bool:
154
+ """Check if current platform supports user mapping.
155
+
156
+ Returns:
157
+ bool: True if platform supports user mapping
158
+ """
159
+ return platform.system() in self._supported_platforms
160
+
161
+ def should_use_user_mapping(self) -> bool:
162
+ """Check if user mapping should be used.
163
+
164
+ Returns:
165
+ bool: True if user mapping is enabled and platform is supported
166
+ """
167
+ return self.enable_user_mapping and self.is_supported_platform()
168
+
169
+ def get_environment_variables(self) -> dict[str, str]:
170
+ """Get environment variables for home and workspace directory configuration.
171
+
172
+ Returns:
173
+ dict[str, str]: Environment variables to set in container
174
+ """
175
+ env = {}
176
+ if self.home_path:
177
+ env["HOME"] = self.home_path.container()
178
+ env["CLAUDE_HOME"] = self.home_path.container()
179
+ if self.workspace_path:
180
+ env["CLAUDE_WORKSPACE"] = self.workspace_path.container()
181
+ return env
182
+
183
+ def get_volumes(self) -> list[tuple[str, str]]:
184
+ """Get Docker volume mappings for home and workspace directories.
185
+
186
+ Returns:
187
+ list[tuple[str, str]]: List of (host_path, container_path) tuples
188
+ """
189
+ volumes = []
190
+ if self.home_path and self.home_path.host_path:
191
+ volumes.append(self.home_path.vol())
192
+ if self.workspace_path and self.workspace_path.host_path:
193
+ volumes.append(self.workspace_path.vol())
194
+ return volumes
195
+
196
+ def get_home_volumes(self) -> list[tuple[str, str]]:
197
+ """Get Docker volume mappings for home directory only (for backwards compatibility).
198
+
199
+ Returns:
200
+ list[tuple[str, str]]: List of (host_path, container_path) tuples
201
+ """
202
+ volumes = []
203
+ if self.home_path and self.home_path.host_path:
204
+ volumes.append(self.home_path.vol())
205
+ return volumes
206
+
207
+ def describe_context(self) -> str:
208
+ """Get human-readable description of user context.
209
+
210
+ Returns:
211
+ str: Description of user context for debugging
212
+ """
213
+ parts = [
214
+ f"uid={self.uid}",
215
+ f"gid={self.gid}",
216
+ f"username={self.username}",
217
+ ]
218
+
219
+ if self.home_path:
220
+ parts.append(f"home_path={self.home_path}")
221
+
222
+ if self.workspace_path:
223
+ parts.append(f"workspace_path={self.workspace_path}")
224
+
225
+ return f"DockerUserContext({', '.join(parts)})"
226
+
227
+
228
+ __all__ = ["DockerUserContext"]