mcp-hangar 0.2.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 (160) hide show
  1. mcp_hangar/__init__.py +139 -0
  2. mcp_hangar/application/__init__.py +1 -0
  3. mcp_hangar/application/commands/__init__.py +67 -0
  4. mcp_hangar/application/commands/auth_commands.py +118 -0
  5. mcp_hangar/application/commands/auth_handlers.py +296 -0
  6. mcp_hangar/application/commands/commands.py +59 -0
  7. mcp_hangar/application/commands/handlers.py +189 -0
  8. mcp_hangar/application/discovery/__init__.py +21 -0
  9. mcp_hangar/application/discovery/discovery_metrics.py +283 -0
  10. mcp_hangar/application/discovery/discovery_orchestrator.py +497 -0
  11. mcp_hangar/application/discovery/lifecycle_manager.py +315 -0
  12. mcp_hangar/application/discovery/security_validator.py +414 -0
  13. mcp_hangar/application/event_handlers/__init__.py +50 -0
  14. mcp_hangar/application/event_handlers/alert_handler.py +191 -0
  15. mcp_hangar/application/event_handlers/audit_handler.py +203 -0
  16. mcp_hangar/application/event_handlers/knowledge_base_handler.py +120 -0
  17. mcp_hangar/application/event_handlers/logging_handler.py +69 -0
  18. mcp_hangar/application/event_handlers/metrics_handler.py +152 -0
  19. mcp_hangar/application/event_handlers/persistent_audit_store.py +217 -0
  20. mcp_hangar/application/event_handlers/security_handler.py +604 -0
  21. mcp_hangar/application/mcp/tooling.py +158 -0
  22. mcp_hangar/application/ports/__init__.py +9 -0
  23. mcp_hangar/application/ports/observability.py +237 -0
  24. mcp_hangar/application/queries/__init__.py +52 -0
  25. mcp_hangar/application/queries/auth_handlers.py +237 -0
  26. mcp_hangar/application/queries/auth_queries.py +118 -0
  27. mcp_hangar/application/queries/handlers.py +227 -0
  28. mcp_hangar/application/read_models/__init__.py +11 -0
  29. mcp_hangar/application/read_models/provider_views.py +139 -0
  30. mcp_hangar/application/sagas/__init__.py +11 -0
  31. mcp_hangar/application/sagas/group_rebalance_saga.py +137 -0
  32. mcp_hangar/application/sagas/provider_failover_saga.py +266 -0
  33. mcp_hangar/application/sagas/provider_recovery_saga.py +172 -0
  34. mcp_hangar/application/services/__init__.py +9 -0
  35. mcp_hangar/application/services/provider_service.py +208 -0
  36. mcp_hangar/application/services/traced_provider_service.py +211 -0
  37. mcp_hangar/bootstrap/runtime.py +328 -0
  38. mcp_hangar/context.py +178 -0
  39. mcp_hangar/domain/__init__.py +117 -0
  40. mcp_hangar/domain/contracts/__init__.py +57 -0
  41. mcp_hangar/domain/contracts/authentication.py +225 -0
  42. mcp_hangar/domain/contracts/authorization.py +229 -0
  43. mcp_hangar/domain/contracts/event_store.py +178 -0
  44. mcp_hangar/domain/contracts/metrics_publisher.py +59 -0
  45. mcp_hangar/domain/contracts/persistence.py +383 -0
  46. mcp_hangar/domain/contracts/provider_runtime.py +146 -0
  47. mcp_hangar/domain/discovery/__init__.py +20 -0
  48. mcp_hangar/domain/discovery/conflict_resolver.py +267 -0
  49. mcp_hangar/domain/discovery/discovered_provider.py +185 -0
  50. mcp_hangar/domain/discovery/discovery_service.py +412 -0
  51. mcp_hangar/domain/discovery/discovery_source.py +192 -0
  52. mcp_hangar/domain/events.py +433 -0
  53. mcp_hangar/domain/exceptions.py +525 -0
  54. mcp_hangar/domain/model/__init__.py +70 -0
  55. mcp_hangar/domain/model/aggregate.py +58 -0
  56. mcp_hangar/domain/model/circuit_breaker.py +152 -0
  57. mcp_hangar/domain/model/event_sourced_api_key.py +413 -0
  58. mcp_hangar/domain/model/event_sourced_provider.py +423 -0
  59. mcp_hangar/domain/model/event_sourced_role_assignment.py +268 -0
  60. mcp_hangar/domain/model/health_tracker.py +183 -0
  61. mcp_hangar/domain/model/load_balancer.py +185 -0
  62. mcp_hangar/domain/model/provider.py +810 -0
  63. mcp_hangar/domain/model/provider_group.py +656 -0
  64. mcp_hangar/domain/model/tool_catalog.py +105 -0
  65. mcp_hangar/domain/policies/__init__.py +19 -0
  66. mcp_hangar/domain/policies/provider_health.py +187 -0
  67. mcp_hangar/domain/repository.py +249 -0
  68. mcp_hangar/domain/security/__init__.py +85 -0
  69. mcp_hangar/domain/security/input_validator.py +710 -0
  70. mcp_hangar/domain/security/rate_limiter.py +387 -0
  71. mcp_hangar/domain/security/roles.py +237 -0
  72. mcp_hangar/domain/security/sanitizer.py +387 -0
  73. mcp_hangar/domain/security/secrets.py +501 -0
  74. mcp_hangar/domain/services/__init__.py +20 -0
  75. mcp_hangar/domain/services/audit_service.py +376 -0
  76. mcp_hangar/domain/services/image_builder.py +328 -0
  77. mcp_hangar/domain/services/provider_launcher.py +1046 -0
  78. mcp_hangar/domain/value_objects.py +1138 -0
  79. mcp_hangar/errors.py +818 -0
  80. mcp_hangar/fastmcp_server.py +1105 -0
  81. mcp_hangar/gc.py +134 -0
  82. mcp_hangar/infrastructure/__init__.py +79 -0
  83. mcp_hangar/infrastructure/async_executor.py +133 -0
  84. mcp_hangar/infrastructure/auth/__init__.py +37 -0
  85. mcp_hangar/infrastructure/auth/api_key_authenticator.py +388 -0
  86. mcp_hangar/infrastructure/auth/event_sourced_store.py +567 -0
  87. mcp_hangar/infrastructure/auth/jwt_authenticator.py +360 -0
  88. mcp_hangar/infrastructure/auth/middleware.py +340 -0
  89. mcp_hangar/infrastructure/auth/opa_authorizer.py +243 -0
  90. mcp_hangar/infrastructure/auth/postgres_store.py +659 -0
  91. mcp_hangar/infrastructure/auth/projections.py +366 -0
  92. mcp_hangar/infrastructure/auth/rate_limiter.py +311 -0
  93. mcp_hangar/infrastructure/auth/rbac_authorizer.py +323 -0
  94. mcp_hangar/infrastructure/auth/sqlite_store.py +624 -0
  95. mcp_hangar/infrastructure/command_bus.py +112 -0
  96. mcp_hangar/infrastructure/discovery/__init__.py +110 -0
  97. mcp_hangar/infrastructure/discovery/docker_source.py +289 -0
  98. mcp_hangar/infrastructure/discovery/entrypoint_source.py +249 -0
  99. mcp_hangar/infrastructure/discovery/filesystem_source.py +383 -0
  100. mcp_hangar/infrastructure/discovery/kubernetes_source.py +247 -0
  101. mcp_hangar/infrastructure/event_bus.py +260 -0
  102. mcp_hangar/infrastructure/event_sourced_repository.py +443 -0
  103. mcp_hangar/infrastructure/event_store.py +396 -0
  104. mcp_hangar/infrastructure/knowledge_base/__init__.py +259 -0
  105. mcp_hangar/infrastructure/knowledge_base/contracts.py +202 -0
  106. mcp_hangar/infrastructure/knowledge_base/memory.py +177 -0
  107. mcp_hangar/infrastructure/knowledge_base/postgres.py +545 -0
  108. mcp_hangar/infrastructure/knowledge_base/sqlite.py +513 -0
  109. mcp_hangar/infrastructure/metrics_publisher.py +36 -0
  110. mcp_hangar/infrastructure/observability/__init__.py +10 -0
  111. mcp_hangar/infrastructure/observability/langfuse_adapter.py +534 -0
  112. mcp_hangar/infrastructure/persistence/__init__.py +33 -0
  113. mcp_hangar/infrastructure/persistence/audit_repository.py +371 -0
  114. mcp_hangar/infrastructure/persistence/config_repository.py +398 -0
  115. mcp_hangar/infrastructure/persistence/database.py +333 -0
  116. mcp_hangar/infrastructure/persistence/database_common.py +330 -0
  117. mcp_hangar/infrastructure/persistence/event_serializer.py +280 -0
  118. mcp_hangar/infrastructure/persistence/event_upcaster.py +166 -0
  119. mcp_hangar/infrastructure/persistence/in_memory_event_store.py +150 -0
  120. mcp_hangar/infrastructure/persistence/recovery_service.py +312 -0
  121. mcp_hangar/infrastructure/persistence/sqlite_event_store.py +386 -0
  122. mcp_hangar/infrastructure/persistence/unit_of_work.py +409 -0
  123. mcp_hangar/infrastructure/persistence/upcasters/README.md +13 -0
  124. mcp_hangar/infrastructure/persistence/upcasters/__init__.py +7 -0
  125. mcp_hangar/infrastructure/query_bus.py +153 -0
  126. mcp_hangar/infrastructure/saga_manager.py +401 -0
  127. mcp_hangar/logging_config.py +209 -0
  128. mcp_hangar/metrics.py +1007 -0
  129. mcp_hangar/models.py +31 -0
  130. mcp_hangar/observability/__init__.py +54 -0
  131. mcp_hangar/observability/health.py +487 -0
  132. mcp_hangar/observability/metrics.py +319 -0
  133. mcp_hangar/observability/tracing.py +433 -0
  134. mcp_hangar/progress.py +542 -0
  135. mcp_hangar/retry.py +613 -0
  136. mcp_hangar/server/__init__.py +120 -0
  137. mcp_hangar/server/__main__.py +6 -0
  138. mcp_hangar/server/auth_bootstrap.py +340 -0
  139. mcp_hangar/server/auth_cli.py +335 -0
  140. mcp_hangar/server/auth_config.py +305 -0
  141. mcp_hangar/server/bootstrap.py +735 -0
  142. mcp_hangar/server/cli.py +161 -0
  143. mcp_hangar/server/config.py +224 -0
  144. mcp_hangar/server/context.py +215 -0
  145. mcp_hangar/server/http_auth_middleware.py +165 -0
  146. mcp_hangar/server/lifecycle.py +467 -0
  147. mcp_hangar/server/state.py +117 -0
  148. mcp_hangar/server/tools/__init__.py +16 -0
  149. mcp_hangar/server/tools/discovery.py +186 -0
  150. mcp_hangar/server/tools/groups.py +75 -0
  151. mcp_hangar/server/tools/health.py +301 -0
  152. mcp_hangar/server/tools/provider.py +939 -0
  153. mcp_hangar/server/tools/registry.py +320 -0
  154. mcp_hangar/server/validation.py +113 -0
  155. mcp_hangar/stdio_client.py +229 -0
  156. mcp_hangar-0.2.0.dist-info/METADATA +347 -0
  157. mcp_hangar-0.2.0.dist-info/RECORD +160 -0
  158. mcp_hangar-0.2.0.dist-info/WHEEL +4 -0
  159. mcp_hangar-0.2.0.dist-info/entry_points.txt +2 -0
  160. mcp_hangar-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,501 @@
1
+ """
2
+ Secrets management for the MCP Registry.
3
+
4
+ Provides secure handling of sensitive data:
5
+ - Masking sensitive values in logs
6
+ - Secure environment variable handling
7
+ - Detection of sensitive keys
8
+ - Redaction utilities
9
+ """
10
+
11
+ from dataclasses import dataclass, field
12
+ import os
13
+ import re
14
+ from typing import Any, Dict, FrozenSet, List, Optional, Pattern, Set
15
+
16
+ # Patterns that indicate a key might contain sensitive data
17
+ SENSITIVE_KEY_PATTERNS: List[Pattern] = [
18
+ re.compile(r"(?i)password"),
19
+ re.compile(r"(?i)passwd"),
20
+ re.compile(r"(?i)secret"),
21
+ re.compile(r"(?i)api[_-]?key"),
22
+ re.compile(r"(?i)apikey"),
23
+ re.compile(r"(?i)auth[_-]?token"),
24
+ re.compile(r"(?i)access[_-]?token"),
25
+ re.compile(r"(?i)bearer"),
26
+ re.compile(r"(?i)credential"),
27
+ re.compile(r"(?i)private[_-]?key"),
28
+ re.compile(r"(?i)ssh[_-]?key"),
29
+ re.compile(r"(?i)encryption[_-]?key"),
30
+ re.compile(r"(?i)signing[_-]?key"),
31
+ re.compile(r"(?i)client[_-]?secret"),
32
+ re.compile(r"(?i)db[_-]?pass"),
33
+ re.compile(r"(?i)database[_-]?password"),
34
+ re.compile(r"(?i)connection[_-]?string"),
35
+ re.compile(r"(?i)conn[_-]?str"),
36
+ re.compile(r"(?i)jwt"),
37
+ re.compile(r"(?i)session[_-]?id"),
38
+ re.compile(r"(?i)cookie"),
39
+ re.compile(r"(?i)oauth"),
40
+ re.compile(r"(?i)_token$"),
41
+ re.compile(r"(?i)_key$"),
42
+ re.compile(r"(?i)_secret$"),
43
+ ]
44
+
45
+ # Exact key names that are always considered sensitive
46
+ SENSITIVE_KEYS: FrozenSet[str] = frozenset(
47
+ [
48
+ "PASSWORD",
49
+ "PASSWD",
50
+ "SECRET",
51
+ "API_KEY",
52
+ "APIKEY",
53
+ "AUTH_TOKEN",
54
+ "ACCESS_TOKEN",
55
+ "REFRESH_TOKEN",
56
+ "BEARER_TOKEN",
57
+ "PRIVATE_KEY",
58
+ "SSH_KEY",
59
+ "AWS_SECRET_ACCESS_KEY",
60
+ "AWS_SESSION_TOKEN",
61
+ "AZURE_CLIENT_SECRET",
62
+ "GCP_PRIVATE_KEY",
63
+ "GITHUB_TOKEN",
64
+ "GITLAB_TOKEN",
65
+ "NPM_TOKEN",
66
+ "PYPI_TOKEN",
67
+ "DATABASE_URL",
68
+ "REDIS_URL",
69
+ "MONGODB_URI",
70
+ "POSTGRES_PASSWORD",
71
+ "MYSQL_PASSWORD",
72
+ "JWT_SECRET",
73
+ "ENCRYPTION_KEY",
74
+ "SIGNING_KEY",
75
+ "MASTER_KEY",
76
+ ]
77
+ )
78
+
79
+
80
+ def is_sensitive_key(key: str) -> bool:
81
+ """
82
+ Check if a key name likely contains sensitive data.
83
+
84
+ Args:
85
+ key: The key name to check
86
+
87
+ Returns:
88
+ True if the key is likely sensitive
89
+ """
90
+ if not key:
91
+ return False
92
+
93
+ # Check exact matches (case-insensitive)
94
+ if key.upper() in SENSITIVE_KEYS:
95
+ return True
96
+
97
+ # Check patterns
98
+ for pattern in SENSITIVE_KEY_PATTERNS:
99
+ if pattern.search(key):
100
+ return True
101
+
102
+ return False
103
+
104
+
105
+ def mask_sensitive_value(
106
+ value: str,
107
+ visible_prefix: int = 4,
108
+ visible_suffix: int = 0,
109
+ mask_char: str = "*",
110
+ min_mask_length: int = 8,
111
+ max_visible: int = 8,
112
+ ) -> str:
113
+ """
114
+ Mask a sensitive value, optionally showing prefix/suffix.
115
+
116
+ Args:
117
+ value: The value to mask
118
+ visible_prefix: Number of characters to show at the start
119
+ visible_suffix: Number of characters to show at the end
120
+ mask_char: Character to use for masking
121
+ min_mask_length: Minimum number of mask characters to show
122
+ max_visible: Maximum total visible characters
123
+
124
+ Returns:
125
+ Masked string
126
+ """
127
+ if not value:
128
+ return ""
129
+
130
+ # For very short values, mask everything
131
+ if len(value) <= min_mask_length:
132
+ return mask_char * len(value)
133
+
134
+ # Limit visible characters
135
+ total_visible = visible_prefix + visible_suffix
136
+ if total_visible > max_visible:
137
+ # Reduce proportionally
138
+ ratio = max_visible / total_visible
139
+ visible_prefix = int(visible_prefix * ratio)
140
+ visible_suffix = int(visible_suffix * ratio)
141
+ total_visible = visible_prefix + visible_suffix
142
+
143
+ # Ensure we have enough characters to mask
144
+ if len(value) <= total_visible + min_mask_length:
145
+ # Show less prefix
146
+ visible_prefix = max(0, len(value) - min_mask_length - visible_suffix)
147
+
148
+ # Calculate mask length
149
+ mask_length = max(min_mask_length, len(value) - visible_prefix - visible_suffix)
150
+
151
+ # Build the masked string
152
+ parts = []
153
+ if visible_prefix > 0:
154
+ parts.append(value[:visible_prefix])
155
+ parts.append(mask_char * mask_length)
156
+ if visible_suffix > 0 and len(value) > visible_prefix + visible_suffix:
157
+ parts.append(value[-visible_suffix:])
158
+
159
+ return "".join(parts)
160
+
161
+
162
+ @dataclass
163
+ class SecretsMask:
164
+ """
165
+ Configuration for masking secrets in various contexts.
166
+ """
167
+
168
+ # Characters to show at start of masked values
169
+ visible_prefix: int = 4
170
+
171
+ # Characters to show at end of masked values
172
+ visible_suffix: int = 0
173
+
174
+ # Character used for masking
175
+ mask_char: str = "*"
176
+
177
+ # Minimum mask length
178
+ min_mask_length: int = 8
179
+
180
+ # Additional patterns to consider sensitive
181
+ additional_patterns: List[Pattern] = field(default_factory=list)
182
+
183
+ # Additional exact keys to consider sensitive
184
+ additional_keys: Set[str] = field(default_factory=set)
185
+
186
+ # Keys to never mask (override)
187
+ safe_keys: Set[str] = field(default_factory=set)
188
+
189
+ def is_sensitive(self, key: str) -> bool:
190
+ """Check if a key is sensitive according to this configuration."""
191
+ if key in self.safe_keys:
192
+ return False
193
+
194
+ if key.upper() in self.additional_keys:
195
+ return True
196
+
197
+ for pattern in self.additional_patterns:
198
+ if pattern.search(key):
199
+ return True
200
+
201
+ return is_sensitive_key(key)
202
+
203
+ def mask(self, value: str) -> str:
204
+ """Mask a value according to this configuration."""
205
+ return mask_sensitive_value(
206
+ value,
207
+ visible_prefix=self.visible_prefix,
208
+ visible_suffix=self.visible_suffix,
209
+ mask_char=self.mask_char,
210
+ min_mask_length=self.min_mask_length,
211
+ )
212
+
213
+ def mask_dict(
214
+ self,
215
+ data: Dict[str, Any],
216
+ recursive: bool = True,
217
+ ) -> Dict[str, Any]:
218
+ """
219
+ Mask sensitive values in a dictionary.
220
+
221
+ Args:
222
+ data: Dictionary to mask
223
+ recursive: Whether to process nested dictionaries
224
+
225
+ Returns:
226
+ New dictionary with sensitive values masked
227
+ """
228
+ result = {}
229
+ for key, value in data.items():
230
+ if isinstance(value, dict) and recursive:
231
+ result[key] = self.mask_dict(value, recursive=True)
232
+ elif isinstance(value, str) and self.is_sensitive(key):
233
+ result[key] = self.mask(value)
234
+ else:
235
+ result[key] = value
236
+ return result
237
+
238
+
239
+ class SecureEnvironment:
240
+ """
241
+ Secure wrapper for environment variable handling.
242
+
243
+ Provides safe access to environment variables with automatic
244
+ masking of sensitive values in logs and error messages.
245
+ """
246
+
247
+ def __init__(
248
+ self,
249
+ env: Optional[Dict[str, str]] = None,
250
+ mask_config: Optional[SecretsMask] = None,
251
+ ):
252
+ """
253
+ Initialize secure environment.
254
+
255
+ Args:
256
+ env: Dictionary of environment variables (defaults to os.environ)
257
+ mask_config: Configuration for masking (uses defaults if not provided)
258
+ """
259
+ self._env = dict(env) if env is not None else dict(os.environ)
260
+ self._mask = mask_config or SecretsMask()
261
+ self._accessed_keys: Set[str] = set()
262
+
263
+ def get(
264
+ self,
265
+ key: str,
266
+ default: Optional[str] = None,
267
+ required: bool = False,
268
+ ) -> Optional[str]:
269
+ """
270
+ Get an environment variable value.
271
+
272
+ Args:
273
+ key: Environment variable name
274
+ default: Default value if not found
275
+ required: If True, raise error if not found
276
+
277
+ Returns:
278
+ The value or default
279
+
280
+ Raises:
281
+ KeyError: If required and not found
282
+ """
283
+ self._accessed_keys.add(key)
284
+
285
+ if key in self._env:
286
+ return self._env[key]
287
+
288
+ if required:
289
+ raise KeyError(f"Required environment variable not set: {key}")
290
+
291
+ return default
292
+
293
+ def get_masked(
294
+ self,
295
+ key: str,
296
+ default: Optional[str] = None,
297
+ ) -> Optional[str]:
298
+ """
299
+ Get an environment variable value, masked if sensitive.
300
+
301
+ Args:
302
+ key: Environment variable name
303
+ default: Default value if not found
304
+
305
+ Returns:
306
+ The value (masked if sensitive) or default
307
+ """
308
+ value = self.get(key, default)
309
+ if value is None:
310
+ return None
311
+
312
+ if self._mask.is_sensitive(key):
313
+ return self._mask.mask(value)
314
+
315
+ return value
316
+
317
+ def set(self, key: str, value: str) -> None:
318
+ """
319
+ Set an environment variable.
320
+
321
+ Args:
322
+ key: Environment variable name
323
+ value: Value to set
324
+ """
325
+ self._env[key] = value
326
+
327
+ def unset(self, key: str) -> None:
328
+ """
329
+ Remove an environment variable.
330
+
331
+ Args:
332
+ key: Environment variable name
333
+ """
334
+ self._env.pop(key, None)
335
+
336
+ def to_dict(self, mask_sensitive: bool = True) -> Dict[str, str]:
337
+ """
338
+ Export environment as dictionary.
339
+
340
+ Args:
341
+ mask_sensitive: Whether to mask sensitive values
342
+
343
+ Returns:
344
+ Dictionary of environment variables
345
+ """
346
+ if not mask_sensitive:
347
+ return dict(self._env)
348
+
349
+ return self._mask.mask_dict(self._env, recursive=False)
350
+
351
+ def to_subprocess_env(
352
+ self,
353
+ include_parent: bool = True,
354
+ whitelist: Optional[Set[str]] = None,
355
+ blacklist: Optional[Set[str]] = None,
356
+ ) -> Dict[str, str]:
357
+ """
358
+ Prepare environment for subprocess execution.
359
+
360
+ Args:
361
+ include_parent: Whether to include parent process environment
362
+ whitelist: If set, only include these keys
363
+ blacklist: Keys to exclude
364
+
365
+ Returns:
366
+ Dictionary suitable for subprocess env parameter
367
+ """
368
+ if include_parent:
369
+ result = dict(os.environ)
370
+ result.update(self._env)
371
+ else:
372
+ result = dict(self._env)
373
+
374
+ # Apply whitelist
375
+ if whitelist is not None:
376
+ result = {k: v for k, v in result.items() if k in whitelist}
377
+
378
+ # Apply blacklist
379
+ if blacklist is not None:
380
+ result = {k: v for k, v in result.items() if k not in blacklist}
381
+
382
+ return result
383
+
384
+ def validate(self, required_keys: List[str]) -> List[str]:
385
+ """
386
+ Validate that required environment variables are set.
387
+
388
+ Args:
389
+ required_keys: List of required key names
390
+
391
+ Returns:
392
+ List of missing key names (empty if all present)
393
+ """
394
+ missing = []
395
+ for key in required_keys:
396
+ if key not in self._env or not self._env[key]:
397
+ missing.append(key)
398
+ return missing
399
+
400
+ @property
401
+ def accessed_keys(self) -> Set[str]:
402
+ """Get the set of keys that have been accessed."""
403
+ return self._accessed_keys.copy()
404
+
405
+ def __contains__(self, key: str) -> bool:
406
+ """Check if a key exists."""
407
+ return key in self._env
408
+
409
+ def __repr__(self) -> str:
410
+ """Safe representation that doesn't expose secrets."""
411
+ keys = sorted(self._env.keys())
412
+ return f"SecureEnvironment({len(keys)} variables)"
413
+
414
+
415
+ # --- Utility Functions ---
416
+
417
+
418
+ def redact_secrets_in_string(
419
+ text: str,
420
+ patterns: Optional[List[Pattern]] = None,
421
+ replacement: str = "[REDACTED]",
422
+ ) -> str:
423
+ """
424
+ Redact potential secrets in a string.
425
+
426
+ Useful for sanitizing log messages or error output.
427
+
428
+ Args:
429
+ text: The text to redact
430
+ patterns: Custom patterns to match (uses defaults if not provided)
431
+ replacement: Replacement text for redacted values
432
+
433
+ Returns:
434
+ Text with secrets redacted
435
+ """
436
+ # Common patterns that might contain secrets in text
437
+ default_patterns = [
438
+ # API keys (various formats)
439
+ re.compile(r'(?i)(api[_-]?key|apikey)["\s:=]+["\']?([a-zA-Z0-9_\-]{16,})'),
440
+ # Bearer tokens
441
+ re.compile(r"(?i)bearer\s+([a-zA-Z0-9_\-\.]+)"),
442
+ # Basic auth in URLs
443
+ re.compile(r"://([^:]+):([^@]+)@"),
444
+ # Password in query strings
445
+ re.compile(r"(?i)password=([^&\s]+)"),
446
+ # Secret in query strings
447
+ re.compile(r"(?i)secret=([^&\s]+)"),
448
+ # Token in query strings
449
+ re.compile(r"(?i)token=([^&\s]+)"),
450
+ # AWS keys
451
+ re.compile(r"(?i)(AKIA[A-Z0-9]{16})"),
452
+ # Private keys
453
+ re.compile(r"-----BEGIN[A-Z\s]+PRIVATE KEY-----"),
454
+ ]
455
+
456
+ patterns_to_use = patterns or default_patterns
457
+ result = text
458
+
459
+ for pattern in patterns_to_use:
460
+ result = pattern.sub(replacement, result)
461
+
462
+ return result
463
+
464
+
465
+ def create_secure_env_for_provider(
466
+ base_env: Optional[Dict[str, str]] = None,
467
+ provider_env: Optional[Dict[str, str]] = None,
468
+ inherit_parent: bool = True,
469
+ sensitive_key_filter: bool = True,
470
+ ) -> SecureEnvironment:
471
+ """
472
+ Create a secure environment for provider execution.
473
+
474
+ Args:
475
+ base_env: Base environment variables
476
+ provider_env: Provider-specific environment variables
477
+ inherit_parent: Whether to inherit from parent process
478
+ sensitive_key_filter: Whether to filter out inherited sensitive keys
479
+
480
+ Returns:
481
+ SecureEnvironment configured for provider use
482
+ """
483
+ env_dict: Dict[str, str] = {}
484
+
485
+ # Start with parent environment if requested
486
+ if inherit_parent:
487
+ # Filter sensitive keys from parent env if requested
488
+ for key, value in os.environ.items():
489
+ if sensitive_key_filter and is_sensitive_key(key):
490
+ continue
491
+ env_dict[key] = value
492
+
493
+ # Add base environment (overrides parent)
494
+ if base_env:
495
+ env_dict.update(base_env)
496
+
497
+ # Add provider-specific environment (overrides all)
498
+ if provider_env:
499
+ env_dict.update(provider_env)
500
+
501
+ return SecureEnvironment(env_dict)
@@ -0,0 +1,20 @@
1
+ """Domain services - interfaces for infrastructure operations."""
2
+
3
+ # Re-export exception from canonical location for convenience
4
+ from ..exceptions import ProviderStartError
5
+ from .audit_service import AuditService
6
+ from .image_builder import BuildConfig, get_image_builder, ImageBuilder
7
+ from .provider_launcher import ContainerConfig, ContainerLauncher, DockerLauncher, ProviderLauncher, SubprocessLauncher
8
+
9
+ __all__ = [
10
+ "AuditService",
11
+ "ProviderLauncher",
12
+ "SubprocessLauncher",
13
+ "DockerLauncher",
14
+ "ContainerLauncher",
15
+ "ContainerConfig",
16
+ "ImageBuilder",
17
+ "BuildConfig",
18
+ "get_image_builder",
19
+ "ProviderStartError",
20
+ ]