mdb-engine 0.1.6__py3-none-any.whl → 0.4.12__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 (92) hide show
  1. mdb_engine/__init__.py +116 -11
  2. mdb_engine/auth/ARCHITECTURE.md +112 -0
  3. mdb_engine/auth/README.md +654 -11
  4. mdb_engine/auth/__init__.py +136 -29
  5. mdb_engine/auth/audit.py +592 -0
  6. mdb_engine/auth/base.py +252 -0
  7. mdb_engine/auth/casbin_factory.py +265 -70
  8. mdb_engine/auth/config_defaults.py +5 -5
  9. mdb_engine/auth/config_helpers.py +19 -18
  10. mdb_engine/auth/cookie_utils.py +12 -16
  11. mdb_engine/auth/csrf.py +483 -0
  12. mdb_engine/auth/decorators.py +10 -16
  13. mdb_engine/auth/dependencies.py +69 -71
  14. mdb_engine/auth/helpers.py +3 -3
  15. mdb_engine/auth/integration.py +61 -88
  16. mdb_engine/auth/jwt.py +11 -15
  17. mdb_engine/auth/middleware.py +79 -35
  18. mdb_engine/auth/oso_factory.py +21 -41
  19. mdb_engine/auth/provider.py +270 -171
  20. mdb_engine/auth/rate_limiter.py +505 -0
  21. mdb_engine/auth/restrictions.py +21 -36
  22. mdb_engine/auth/session_manager.py +24 -41
  23. mdb_engine/auth/shared_middleware.py +977 -0
  24. mdb_engine/auth/shared_users.py +775 -0
  25. mdb_engine/auth/token_lifecycle.py +10 -12
  26. mdb_engine/auth/token_store.py +17 -32
  27. mdb_engine/auth/users.py +99 -159
  28. mdb_engine/auth/utils.py +236 -42
  29. mdb_engine/cli/commands/generate.py +546 -10
  30. mdb_engine/cli/commands/validate.py +3 -7
  31. mdb_engine/cli/utils.py +7 -7
  32. mdb_engine/config.py +13 -28
  33. mdb_engine/constants.py +65 -0
  34. mdb_engine/core/README.md +117 -6
  35. mdb_engine/core/__init__.py +39 -7
  36. mdb_engine/core/app_registration.py +31 -50
  37. mdb_engine/core/app_secrets.py +289 -0
  38. mdb_engine/core/connection.py +20 -12
  39. mdb_engine/core/encryption.py +222 -0
  40. mdb_engine/core/engine.py +2862 -115
  41. mdb_engine/core/index_management.py +12 -16
  42. mdb_engine/core/manifest.py +628 -204
  43. mdb_engine/core/ray_integration.py +436 -0
  44. mdb_engine/core/seeding.py +13 -21
  45. mdb_engine/core/service_initialization.py +20 -30
  46. mdb_engine/core/types.py +40 -43
  47. mdb_engine/database/README.md +140 -17
  48. mdb_engine/database/__init__.py +17 -6
  49. mdb_engine/database/abstraction.py +37 -50
  50. mdb_engine/database/connection.py +51 -30
  51. mdb_engine/database/query_validator.py +367 -0
  52. mdb_engine/database/resource_limiter.py +204 -0
  53. mdb_engine/database/scoped_wrapper.py +747 -237
  54. mdb_engine/dependencies.py +427 -0
  55. mdb_engine/di/__init__.py +34 -0
  56. mdb_engine/di/container.py +247 -0
  57. mdb_engine/di/providers.py +206 -0
  58. mdb_engine/di/scopes.py +139 -0
  59. mdb_engine/embeddings/README.md +54 -24
  60. mdb_engine/embeddings/__init__.py +31 -24
  61. mdb_engine/embeddings/dependencies.py +38 -155
  62. mdb_engine/embeddings/service.py +78 -75
  63. mdb_engine/exceptions.py +104 -12
  64. mdb_engine/indexes/README.md +30 -13
  65. mdb_engine/indexes/__init__.py +1 -0
  66. mdb_engine/indexes/helpers.py +11 -11
  67. mdb_engine/indexes/manager.py +59 -123
  68. mdb_engine/memory/README.md +95 -4
  69. mdb_engine/memory/__init__.py +1 -2
  70. mdb_engine/memory/service.py +363 -1168
  71. mdb_engine/observability/README.md +4 -2
  72. mdb_engine/observability/__init__.py +26 -9
  73. mdb_engine/observability/health.py +17 -17
  74. mdb_engine/observability/logging.py +10 -10
  75. mdb_engine/observability/metrics.py +40 -19
  76. mdb_engine/repositories/__init__.py +34 -0
  77. mdb_engine/repositories/base.py +325 -0
  78. mdb_engine/repositories/mongo.py +233 -0
  79. mdb_engine/repositories/unit_of_work.py +166 -0
  80. mdb_engine/routing/README.md +1 -1
  81. mdb_engine/routing/__init__.py +1 -3
  82. mdb_engine/routing/websockets.py +41 -75
  83. mdb_engine/utils/__init__.py +3 -1
  84. mdb_engine/utils/mongo.py +117 -0
  85. mdb_engine-0.4.12.dist-info/METADATA +492 -0
  86. mdb_engine-0.4.12.dist-info/RECORD +97 -0
  87. {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/WHEEL +1 -1
  88. mdb_engine-0.1.6.dist-info/METADATA +0 -213
  89. mdb_engine-0.1.6.dist-info/RECORD +0 -75
  90. {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/entry_points.txt +0 -0
  91. {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/licenses/LICENSE +0 -0
  92. {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/top_level.txt +0 -0
mdb_engine/auth/jwt.py CHANGED
@@ -10,7 +10,7 @@ This module is part of MDB_ENGINE - MongoDB Engine.
10
10
  import logging
11
11
  import uuid
12
12
  from datetime import datetime, timedelta
13
- from typing import Any, Dict, Optional, Tuple
13
+ from typing import Any
14
14
 
15
15
  import jwt
16
16
 
@@ -21,7 +21,7 @@ from ..constants import CURRENT_TOKEN_VERSION
21
21
  logger = logging.getLogger(__name__)
22
22
 
23
23
 
24
- def decode_jwt_token(token: Any, secret_key: str) -> Dict[str, Any]:
24
+ def decode_jwt_token(token: Any, secret_key: str) -> dict[str, Any]:
25
25
  """
26
26
  Helper function to decode JWT tokens with automatic fallback to bytes format.
27
27
 
@@ -74,7 +74,7 @@ def decode_jwt_token(token: Any, secret_key: str) -> Dict[str, Any]:
74
74
 
75
75
 
76
76
  def encode_jwt_token(
77
- payload: Dict[str, Any], secret_key: str, expires_in: Optional[int] = None
77
+ payload: dict[str, Any], secret_key: str, expires_in: int | None = None
78
78
  ) -> str:
79
79
  """
80
80
  Encode a JWT token with enhanced claims.
@@ -123,12 +123,12 @@ def encode_jwt_token(
123
123
 
124
124
 
125
125
  def generate_token_pair(
126
- user_data: Dict[str, Any],
126
+ user_data: dict[str, Any],
127
127
  secret_key: str,
128
- device_info: Optional[Dict[str, Any]] = None,
129
- access_token_ttl: Optional[int] = None,
130
- refresh_token_ttl: Optional[int] = None,
131
- ) -> Tuple[str, str, Dict[str, Any]]:
128
+ device_info: dict[str, Any] | None = None,
129
+ access_token_ttl: int | None = None,
130
+ refresh_token_ttl: int | None = None,
131
+ ) -> tuple[str, str, dict[str, Any]]:
132
132
  """
133
133
  Generate a pair of access and refresh tokens.
134
134
 
@@ -164,9 +164,7 @@ def generate_token_pair(
164
164
  "jti": access_jti,
165
165
  "device_id": device_id,
166
166
  }
167
- access_token = encode_jwt_token(
168
- access_payload, secret_key, expires_in=access_token_ttl
169
- )
167
+ access_token = encode_jwt_token(access_payload, secret_key, expires_in=access_token_ttl)
170
168
 
171
169
  # Generate refresh token
172
170
  refresh_jti = str(uuid.uuid4())
@@ -177,9 +175,7 @@ def generate_token_pair(
177
175
  "email": user_data.get("email"),
178
176
  "device_id": device_id,
179
177
  }
180
- refresh_token = encode_jwt_token(
181
- refresh_payload, secret_key, expires_in=refresh_token_ttl
182
- )
178
+ refresh_token = encode_jwt_token(refresh_payload, secret_key, expires_in=refresh_token_ttl)
183
179
 
184
180
  # Token metadata
185
181
  token_metadata = {
@@ -194,7 +190,7 @@ def generate_token_pair(
194
190
  return access_token, refresh_token, token_metadata
195
191
 
196
192
 
197
- def extract_token_metadata(token: str, secret_key: str) -> Optional[Dict[str, Any]]:
193
+ def extract_token_metadata(token: str, secret_key: str) -> dict[str, Any] | None:
198
194
  """
199
195
  Extract metadata from a token without full validation.
200
196
 
@@ -4,12 +4,19 @@ Security Middleware
4
4
  Middleware for enforcing security settings from manifest configuration.
5
5
 
6
6
  This module is part of MDB_ENGINE - MongoDB Engine.
7
+
8
+ Security Features:
9
+ - HTTPS enforcement in production
10
+ - HSTS (HTTP Strict Transport Security) header
11
+ - Security headers (X-Content-Type-Options, X-Frame-Options, etc.)
12
+ - CSRF token generation (legacy, prefer CSRFMiddleware for new apps)
7
13
  """
8
14
 
9
15
  import logging
10
16
  import os
11
17
  import secrets
12
- from typing import Awaitable, Callable
18
+ from collections.abc import Awaitable, Callable
19
+ from typing import Any
13
20
 
14
21
  from fastapi import HTTPException, Request, Response, status
15
22
  from fastapi.responses import RedirectResponse
@@ -17,6 +24,18 @@ from starlette.middleware.base import BaseHTTPMiddleware
17
24
 
18
25
  logger = logging.getLogger(__name__)
19
26
 
27
+ # Default HSTS settings
28
+ DEFAULT_HSTS_MAX_AGE = 31536000 # 1 year in seconds
29
+
30
+
31
+ def _is_production() -> bool:
32
+ """Check if we're running in production environment."""
33
+ return (
34
+ os.getenv("MDB_ENGINE_ENV", "").lower() == "production"
35
+ or os.getenv("ENVIRONMENT", "").lower() == "production"
36
+ or os.getenv("G_NOME_ENV", "").lower() == "production"
37
+ )
38
+
20
39
 
21
40
  class SecurityMiddleware(BaseHTTPMiddleware):
22
41
  """
@@ -24,9 +43,9 @@ class SecurityMiddleware(BaseHTTPMiddleware):
24
43
 
25
44
  Features:
26
45
  - HTTPS enforcement in production
27
- - CSRF token generation and validation
28
- - Security headers
29
- - Token validation
46
+ - HSTS header for forcing HTTPS
47
+ - Security headers (X-Content-Type-Options, X-Frame-Options, etc.)
48
+ - Legacy CSRF token generation (prefer CSRFMiddleware for new apps)
30
49
  """
31
50
 
32
51
  def __init__(
@@ -35,6 +54,7 @@ class SecurityMiddleware(BaseHTTPMiddleware):
35
54
  require_https: bool = False,
36
55
  csrf_protection: bool = True,
37
56
  security_headers: bool = True,
57
+ hsts_config: dict[str, Any] | None = None,
38
58
  ):
39
59
  """
40
60
  Initialize security middleware.
@@ -42,38 +62,60 @@ class SecurityMiddleware(BaseHTTPMiddleware):
42
62
  Args:
43
63
  app: FastAPI application
44
64
  require_https: Require HTTPS in production (default: False, auto-detected)
45
- csrf_protection: Enable CSRF protection (default: True)
65
+ csrf_protection: Enable legacy CSRF protection (default: True)
46
66
  security_headers: Add security headers (default: True)
67
+ hsts_config: HSTS configuration dict with keys:
68
+ - enabled: Enable HSTS (default: True in production)
69
+ - max_age: Max-age in seconds (default: 31536000)
70
+ - include_subdomains: Include subdomains (default: True)
71
+ - preload: Add preload directive (default: False)
47
72
  """
48
73
  super().__init__(app)
49
74
  self.require_https = require_https
50
75
  self.csrf_protection = csrf_protection
51
76
  self.security_headers = security_headers
52
77
 
78
+ # HSTS configuration
79
+ self.hsts_config = hsts_config or {}
80
+ self.hsts_enabled = self.hsts_config.get("enabled", True)
81
+ self.hsts_max_age = self.hsts_config.get("max_age", DEFAULT_HSTS_MAX_AGE)
82
+ self.hsts_include_subdomains = self.hsts_config.get("include_subdomains", True)
83
+ self.hsts_preload = self.hsts_config.get("preload", False)
84
+
85
+ def _build_hsts_header(self) -> str:
86
+ """Build the HSTS header value."""
87
+ parts = [f"max-age={self.hsts_max_age}"]
88
+
89
+ if self.hsts_include_subdomains:
90
+ parts.append("includeSubDomains")
91
+
92
+ if self.hsts_preload:
93
+ parts.append("preload")
94
+
95
+ return "; ".join(parts)
96
+
53
97
  async def dispatch(
54
98
  self, request: Request, call_next: Callable[[Request], Awaitable[Response]]
55
99
  ) -> Response:
56
100
  """
57
101
  Process request through security middleware.
58
102
  """
103
+ is_production = _is_production()
104
+ is_https = request.url.scheme == "https"
105
+
59
106
  # Check HTTPS requirement
60
- if self.require_https:
61
- is_production = (
62
- os.getenv("G_NOME_ENV") == "production"
63
- or os.getenv("ENVIRONMENT") == "production"
64
- )
65
- if is_production and request.url.scheme != "https":
66
- if request.method == "GET":
67
- # Redirect to HTTPS
68
- https_url = str(request.url).replace("http://", "https://", 1)
69
- return RedirectResponse(url=https_url, status_code=301)
70
- else:
71
- raise HTTPException(
72
- status_code=status.HTTP_403_FORBIDDEN,
73
- detail="HTTPS required in production",
74
- )
75
-
76
- # Generate CSRF token if not present (for GET requests)
107
+ if self.require_https and is_production and not is_https:
108
+ if request.method == "GET":
109
+ # Redirect to HTTPS
110
+ https_url = str(request.url).replace("http://", "https://", 1)
111
+ return RedirectResponse(url=https_url, status_code=301)
112
+ else:
113
+ raise HTTPException(
114
+ status_code=status.HTTP_403_FORBIDDEN,
115
+ detail="HTTPS required in production",
116
+ )
117
+
118
+ # Generate CSRF token if not present (for GET requests) - legacy support
77
119
  if self.csrf_protection and request.method == "GET":
78
120
  csrf_token = request.cookies.get("csrf_token")
79
121
  if not csrf_token:
@@ -90,19 +132,27 @@ class SecurityMiddleware(BaseHTTPMiddleware):
90
132
  response.headers["X-XSS-Protection"] = "1; mode=block"
91
133
  response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
92
134
 
135
+ # Permissions-Policy (modern replacement for some legacy headers)
136
+ response.headers["Permissions-Policy"] = (
137
+ "accelerometer=(), camera=(), geolocation=(), gyroscope=(), "
138
+ "magnetometer=(), microphone=(), payment=(), usb=()"
139
+ )
140
+
93
141
  # Content Security Policy (basic)
94
142
  if request.url.path.startswith("/api"):
95
143
  response.headers["Content-Security-Policy"] = "default-src 'self'"
96
144
 
97
- # Set CSRF token cookie if generated
145
+ # Add HSTS header in production (only over HTTPS or always if configured)
146
+ if self.hsts_enabled and (is_production or is_https):
147
+ response.headers["Strict-Transport-Security"] = self._build_hsts_header()
148
+
149
+ # Set CSRF token cookie if generated (legacy support)
98
150
  if (
99
151
  self.csrf_protection
100
152
  and request.method == "GET"
101
153
  and not request.cookies.get("csrf_token")
102
154
  ):
103
155
  csrf_token = secrets.token_urlsafe(32)
104
- is_https = request.url.scheme == "https"
105
- is_production = os.getenv("G_NOME_ENV") == "production"
106
156
  response.set_cookie(
107
157
  key="csrf_token",
108
158
  value=csrf_token,
@@ -154,10 +204,7 @@ class StaleSessionMiddleware(BaseHTTPMiddleware):
154
204
  # Check if we need to clear a stale session cookie
155
205
  # Only act if explicitly flagged - this ensures we don't interfere with
156
206
  # apps that don't use get_app_user()
157
- if (
158
- hasattr(request.state, "clear_stale_session")
159
- and request.state.clear_stale_session
160
- ):
207
+ if hasattr(request.state, "clear_stale_session") and request.state.clear_stale_session:
161
208
  try:
162
209
  # Get cookie name from app config
163
210
  cookie_name = None
@@ -188,7 +235,7 @@ class StaleSessionMiddleware(BaseHTTPMiddleware):
188
235
  "session_cookie_name", "app_session"
189
236
  )
190
237
  cookie_name = f"{session_cookie_name}_{self.slug_id}"
191
- except (AttributeError, KeyError, TypeError, Exception):
238
+ except (AttributeError, KeyError, TypeError):
192
239
  pass
193
240
 
194
241
  # Final fallback to default naming convention
@@ -197,8 +244,7 @@ class StaleSessionMiddleware(BaseHTTPMiddleware):
197
244
 
198
245
  # Get cookie settings to match how it was set
199
246
  should_use_secure = (
200
- request.url.scheme == "https"
201
- or os.getenv("G_NOME_ENV") == "production"
247
+ request.url.scheme == "https" or os.getenv("G_NOME_ENV") == "production"
202
248
  )
203
249
 
204
250
  # Delete the stale cookie
@@ -208,9 +254,7 @@ class StaleSessionMiddleware(BaseHTTPMiddleware):
208
254
  secure=should_use_secure,
209
255
  samesite="lax",
210
256
  )
211
- logger.debug(
212
- f"Cleared stale session cookie '{cookie_name}' for {self.slug_id}"
213
- )
257
+ logger.debug(f"Cleared stale session cookie '{cookie_name}' for {self.slug_id}")
214
258
  except (ValueError, TypeError, AttributeError, RuntimeError) as e:
215
259
  # Don't fail the request if cookie cleanup fails
216
260
  logger.warning(
@@ -11,7 +11,7 @@ from __future__ import annotations
11
11
 
12
12
  import logging
13
13
  import os
14
- from typing import TYPE_CHECKING, Any, Dict, List, Optional
14
+ from typing import TYPE_CHECKING, Any
15
15
 
16
16
  if TYPE_CHECKING:
17
17
  from .provider import OsoAdapter
@@ -20,8 +20,8 @@ logger = logging.getLogger(__name__)
20
20
 
21
21
 
22
22
  async def create_oso_cloud_client(
23
- api_key: Optional[str] = None,
24
- url: Optional[str] = None,
23
+ api_key: str | None = None,
24
+ url: str | None = None,
25
25
  max_retries: int = 3,
26
26
  retry_delay: float = 2.0,
27
27
  ) -> Any:
@@ -46,13 +46,11 @@ async def create_oso_cloud_client(
46
46
 
47
47
  # Import OSO Cloud SDK - the class is named "Oso"
48
48
  try:
49
- from oso_cloud import Oso
49
+ from oso_cloud import Oso # type: ignore
50
50
 
51
51
  logger.debug("✅ Imported Oso from oso_cloud")
52
- except ImportError:
53
- raise ImportError(
54
- "OSO Cloud SDK not installed. Install with: pip install oso-cloud"
55
- )
52
+ except ImportError as e:
53
+ raise ImportError("OSO Cloud SDK not installed. Install with: pip install oso-cloud") from e
56
54
 
57
55
  # Get API key from parameter or environment
58
56
  if not api_key:
@@ -81,9 +79,7 @@ async def create_oso_cloud_client(
81
79
 
82
80
  # Note: OSO client creation doesn't actually connect to the server
83
81
  # The connection happens on first API call, so we'll catch errors then
84
- logger.info(
85
- f"✅ OSO Cloud client created successfully (URL: {url or 'default'})"
86
- )
82
+ logger.info(f"✅ OSO Cloud client created successfully (URL: {url or 'default'})")
87
83
  if url:
88
84
  logger.info(f" Using OSO Dev Server at: {url}")
89
85
  return oso_client
@@ -118,8 +114,8 @@ async def create_oso_cloud_client(
118
114
 
119
115
  async def setup_initial_oso_facts(
120
116
  authz_provider: OsoAdapter,
121
- initial_roles: Optional[List[Dict[str, Any]]] = None,
122
- initial_policies: Optional[List[Dict[str, Any]]] = None,
117
+ initial_roles: list[dict[str, Any]] | None = None,
118
+ initial_policies: list[dict[str, Any]] | None = None,
123
119
  ) -> None:
124
120
  """
125
121
  Set up initial roles and policies in OSO Cloud.
@@ -137,29 +133,23 @@ async def setup_initial_oso_facts(
137
133
  try:
138
134
  user = role_assignment.get("user")
139
135
  role = role_assignment.get("role")
140
- resource = role_assignment.get(
141
- "resource", "documents"
142
- ) # Default to "documents"
136
+ resource = role_assignment.get("resource", "documents") # Default to "documents"
143
137
 
144
138
  if user and role:
145
139
  # For OSO Cloud, we add has_role facts with resource context
146
140
  # This supports resource-based authorization
147
141
  await authz_provider.add_role_for_user(user, role, resource)
148
- logger.debug(
149
- f"Added role '{role}' for user '{user}' on resource '{resource}'"
150
- )
142
+ logger.debug(f"Added role '{role}' for user '{user}' on resource '{resource}'")
151
143
  except (ValueError, TypeError, AttributeError, RuntimeError) as e:
152
- logger.warning(
153
- f"Failed to add initial role assignment {role_assignment}: {e}"
154
- )
144
+ logger.warning(f"Failed to add initial role assignment {role_assignment}: {e}")
155
145
 
156
146
  # Note: initial_policies are not used - we use has_role facts instead
157
147
  # The policy derives permissions from roles, not from explicit grants_permission facts
158
148
 
159
149
 
160
150
  async def initialize_oso_from_manifest(
161
- engine, app_slug: str, auth_config: Dict[str, Any]
162
- ) -> Optional[OsoAdapter]:
151
+ engine, app_slug: str, auth_config: dict[str, Any]
152
+ ) -> OsoAdapter | None:
163
153
  """
164
154
  Initialize OSO Cloud provider from manifest configuration.
165
155
 
@@ -181,9 +171,7 @@ async def initialize_oso_from_manifest(
181
171
 
182
172
  # Only proceed if provider is oso
183
173
  if provider != "oso":
184
- logger.debug(
185
- f"Provider is '{provider}', not 'oso' - skipping OSO initialization"
186
- )
174
+ logger.debug(f"Provider is '{provider}', not 'oso' - skipping OSO initialization")
187
175
  return None
188
176
 
189
177
  logger.info(f"Initializing OSO Cloud provider for app '{app_slug}'...")
@@ -224,24 +212,18 @@ async def initialize_oso_from_manifest(
224
212
  try:
225
213
  import asyncio
226
214
 
227
- from oso_cloud import Value
215
+ from oso_cloud import Value # type: ignore
228
216
 
229
217
  # Try a simple test authorization to verify connection
230
218
  test_actor = Value("User", "test")
231
219
  test_resource = Value("Document", "test")
232
220
  # This might fail, but it tests if the server is responding
233
- await asyncio.to_thread(
234
- oso_client.authorize, test_actor, "read", test_resource
235
- )
221
+ await asyncio.to_thread(oso_client.authorize, test_actor, "read", test_resource)
236
222
  logger.debug("✅ OSO Dev Server connection test successful")
237
223
  except (TimeoutError, OSError, RuntimeError) as test_error:
238
224
  # Type 2: Recoverable - connection test failed, check if it's a connection error
239
225
  error_str = str(test_error).lower()
240
- if (
241
- "connection" in error_str
242
- or "refused" in error_str
243
- or "timeout" in error_str
244
- ):
226
+ if "connection" in error_str or "refused" in error_str or "timeout" in error_str:
245
227
  logger.warning(
246
228
  f"⚠️ OSO Dev Server connection test failed - "
247
229
  f"server might not be ready: {test_error}"
@@ -289,9 +271,7 @@ async def initialize_oso_from_manifest(
289
271
  )
290
272
  logger.info("✅ Initial OSO facts set up successfully")
291
273
  except (ValueError, TypeError, AttributeError, RuntimeError) as e:
292
- logger.warning(
293
- f"⚠️ Failed to set up initial OSO facts: {e}", exc_info=True
294
- )
274
+ logger.warning(f"⚠️ Failed to set up initial OSO facts: {e}", exc_info=True)
295
275
  # Continue anyway - adapter is still usable
296
276
 
297
277
  logger.info(f"✅ OSO Cloud provider initialized for app '{app_slug}'")
@@ -299,13 +279,13 @@ async def initialize_oso_from_manifest(
299
279
  return adapter
300
280
 
301
281
  except ImportError as e:
302
- logger.error(
282
+ logger.exception(
303
283
  f"❌ OSO Cloud SDK not available for app '{app_slug}': {e}. "
304
284
  "Install with: pip install oso-cloud"
305
285
  )
306
286
  return None
307
287
  except ValueError as e:
308
- logger.error(f"❌ OSO Cloud configuration error for app '{app_slug}': {e}")
288
+ logger.exception(f"❌ OSO Cloud configuration error for app '{app_slug}': {e}")
309
289
  return None
310
290
  except (
311
291
  ImportError,