solace-agent-mesh 1.4.0__py3-none-any.whl → 1.4.1__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.

Potentially problematic release.


This version of solace-agent-mesh might be problematic. Click here for more details.

Files changed (67) hide show
  1. solace_agent_mesh/assets/docs/404.html +3 -3
  2. solace_agent_mesh/assets/docs/assets/js/ae4415af.16cc58d3.js +1 -0
  3. solace_agent_mesh/assets/docs/assets/js/{main.1de3da6a.js → main.9bc1a102.js} +2 -2
  4. solace_agent_mesh/assets/docs/assets/js/{runtime~main.3188e049.js → runtime~main.f2b4ea70.js} +1 -1
  5. solace_agent_mesh/assets/docs/docs/documentation/Enterprise/installation/index.html +3 -3
  6. solace_agent_mesh/assets/docs/docs/documentation/Enterprise/single-sign-on/index.html +3 -3
  7. solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-gateway-upgrade-to-0.3.0/index.html +3 -3
  8. solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-technical-migration-map/index.html +3 -3
  9. solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +3 -3
  10. solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +3 -3
  11. solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +20 -5
  12. solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +3 -3
  13. solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +3 -3
  14. solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +3 -3
  15. solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +3 -3
  16. solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +3 -3
  17. solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +3 -3
  18. solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +3 -3
  19. solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/index.html +3 -3
  20. solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +3 -3
  21. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +3 -3
  22. solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +3 -3
  23. solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +3 -3
  24. solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +3 -3
  25. solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +3 -3
  26. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +3 -3
  27. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +3 -3
  28. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +3 -3
  29. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +3 -3
  30. solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +3 -3
  31. solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +3 -3
  32. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +3 -3
  33. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +3 -3
  34. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +3 -3
  35. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +3 -3
  36. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +3 -3
  37. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +3 -3
  38. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +3 -3
  39. solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-python-tools/index.html +3 -3
  40. solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +3 -3
  41. solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +3 -3
  42. solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +3 -3
  43. solace_agent_mesh/assets/docs/lunr-index-1758036158289.json +1 -0
  44. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  45. solace_agent_mesh/assets/docs/search-doc-1758036158289.json +1 -0
  46. solace_agent_mesh/assets/docs/search-doc.json +1 -1
  47. solace_agent_mesh/cli/__init__.py +1 -1
  48. solace_agent_mesh/cli/commands/plugin_cmd/__init__.py +2 -0
  49. solace_agent_mesh/cli/commands/plugin_cmd/add_cmd.py +10 -245
  50. solace_agent_mesh/cli/commands/plugin_cmd/install_cmd.py +283 -0
  51. solace_agent_mesh/client/webui/frontend/static/assets/{main-Dq4AJNvn.js → main-B6BpuH9K.js} +2 -2
  52. solace_agent_mesh/client/webui/frontend/static/index.html +1 -1
  53. solace_agent_mesh/common/services/identity_service.py +2 -1
  54. solace_agent_mesh/common/services/providers/local_file_identity_service.py +1 -1
  55. solace_agent_mesh/common/utils/pydantic_utils.py +4 -0
  56. solace_agent_mesh/gateway/base/app.py +12 -1
  57. solace_agent_mesh/gateway/http_sse/main.py +337 -375
  58. solace_agent_mesh/templates/webui.yaml +1 -1
  59. {solace_agent_mesh-1.4.0.dist-info → solace_agent_mesh-1.4.1.dist-info}/METADATA +7 -1
  60. {solace_agent_mesh-1.4.0.dist-info → solace_agent_mesh-1.4.1.dist-info}/RECORD +64 -63
  61. solace_agent_mesh/assets/docs/assets/js/ae4415af.24cdc514.js +0 -1
  62. solace_agent_mesh/assets/docs/lunr-index-1757991496554.json +0 -1
  63. solace_agent_mesh/assets/docs/search-doc-1757991496554.json +0 -1
  64. /solace_agent_mesh/assets/docs/assets/js/{main.1de3da6a.js.LICENSE.txt → main.9bc1a102.js.LICENSE.txt} +0 -0
  65. {solace_agent_mesh-1.4.0.dist-info → solace_agent_mesh-1.4.1.dist-info}/WHEEL +0 -0
  66. {solace_agent_mesh-1.4.0.dist-info → solace_agent_mesh-1.4.1.dist-info}/entry_points.txt +0 -0
  67. {solace_agent_mesh-1.4.0.dist-info → solace_agent_mesh-1.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -30,8 +30,6 @@ from ...gateway.http_sse.routers import (
30
30
  tasks,
31
31
  visualization,
32
32
  )
33
-
34
- # Import persistence-aware controllers
35
33
  from .routers.sessions import router as session_router
36
34
  from .routers.tasks import router as task_router
37
35
  from .routers.users import router as user_router
@@ -46,90 +44,132 @@ app = FastAPI(
46
44
  )
47
45
 
48
46
 
49
- def setup_dependencies(component: "WebUIBackendComponent", database_url: str = None):
50
- """
51
- This function initializes the simplified architecture while maintaining full
52
- backward compatibility with existing API contracts.
47
+ def _extract_access_token(request: FastAPIRequest) -> str:
48
+ auth_header = request.headers.get("Authorization")
49
+ if auth_header and auth_header.startswith("Bearer "):
50
+ return auth_header[7:]
53
51
 
54
- If database_url is None, runs in compatibility mode with in-memory sessions.
55
- """
52
+ try:
53
+ if "access_token" in request.session:
54
+ log.debug("AuthMiddleware: Found token in session.")
55
+ return request.session["access_token"]
56
+ except AssertionError:
57
+ log.debug("AuthMiddleware: Could not access request.session.")
56
58
 
57
- dependencies.set_component_instance(component)
59
+ if "token" in request.query_params:
60
+ return request.query_params["token"]
58
61
 
59
- if database_url:
60
- dependencies.init_database(database_url)
61
- log.info("Persistence enabled - sessions will be stored in database")
62
-
63
- log.info("Checking database migrations...")
64
- try:
65
- from sqlalchemy import create_engine
66
- engine = create_engine(database_url)
67
- inspector = sa.inspect(engine)
68
- existing_tables = inspector.get_table_names()
69
-
70
- if not existing_tables or "sessions" not in existing_tables:
71
- log.info("Running database migrations...")
72
- alembic_cfg = Config()
73
- alembic_cfg.set_main_option(
74
- "script_location",
75
- os.path.join(os.path.dirname(__file__), "alembic"),
76
- )
77
- alembic_cfg.set_main_option("sqlalchemy.url", database_url)
78
- command.upgrade(alembic_cfg, "head")
79
- log.info("Database migrations complete.")
80
- else:
81
- log.info("Database tables already exist, skipping migrations.")
82
- except Exception as e:
83
- log.warning(
84
- f"Migration check failed, attempting to run migrations anyway: {e}"
85
- )
86
- try:
87
- alembic_cfg = Config()
88
- alembic_cfg.set_main_option(
89
- "script_location",
90
- os.path.join(os.path.dirname(__file__), "alembic"),
91
- )
92
- alembic_cfg.set_main_option("sqlalchemy.url", database_url)
93
- command.upgrade(alembic_cfg, "head")
94
- log.info("Database migrations complete.")
95
- except Exception as migration_error:
96
- log.warning(f"Migration failed but continuing: {migration_error}")
97
- else:
98
- log.warning(
99
- "No database URL provided - using in-memory session storage (data not persisted across restarts)"
62
+ return None
63
+
64
+
65
+ async def _validate_token(auth_service_url: str, auth_provider: str, access_token: str) -> bool:
66
+ async with httpx.AsyncClient() as client:
67
+ validation_response = await client.post(
68
+ f"{auth_service_url}/is_token_valid",
69
+ json={"provider": auth_provider},
70
+ headers={"Authorization": f"Bearer {access_token}"},
100
71
  )
101
- log.info("This maintains backward compatibility for existing SAM installations")
72
+ return validation_response.status_code == 200
102
73
 
103
- webui_app = component.get_app()
104
- app_config = {}
105
- if webui_app:
106
- app_config = getattr(webui_app, "app_config", {})
107
- if app_config is None:
108
- log.warning("webui_app.app_config is None, using empty dict.")
109
- app_config = {}
110
- else:
111
- log.warning("Could not get webui_app from component. Using empty app_config.")
112
74
 
113
- api_config_dict = {
114
- "external_auth_service_url": app_config.get(
115
- "external_auth_service_url", "http://localhost:8080"
116
- ),
117
- "external_auth_callback_uri": app_config.get(
118
- "external_auth_callback_uri", "http://localhost:8000/api/v1/auth/callback"
119
- ),
120
- "external_auth_provider": app_config.get("external_auth_provider", "azure"),
121
- "frontend_use_authorization": app_config.get(
122
- "frontend_use_authorization", False
123
- ),
124
- "frontend_redirect_url": app_config.get(
125
- "frontend_redirect_url", "http://localhost:3000"
126
- ),
127
- "persistence_enabled": database_url is not None,
75
+ async def _get_user_info(auth_service_url: str, auth_provider: str, access_token: str) -> dict:
76
+ async with httpx.AsyncClient() as client:
77
+ userinfo_response = await client.get(
78
+ f"{auth_service_url}/user_info?provider={auth_provider}",
79
+ headers={"Authorization": f"Bearer {access_token}"},
80
+ )
81
+
82
+ if userinfo_response.status_code != 200:
83
+ return None
84
+
85
+ return userinfo_response.json()
86
+
87
+
88
+ def _extract_user_identifier(user_info: dict) -> str:
89
+ user_identifier = (
90
+ user_info.get("sub")
91
+ or user_info.get("client_id")
92
+ or user_info.get("username")
93
+ or user_info.get("oid")
94
+ or user_info.get("preferred_username")
95
+ or user_info.get("upn")
96
+ or user_info.get("unique_name")
97
+ or user_info.get("email")
98
+ or user_info.get("name")
99
+ or user_info.get("azp")
100
+ )
101
+
102
+ if user_identifier and user_identifier.lower() == "unknown":
103
+ log.warning("AuthMiddleware: IDP returned 'Unknown' as user identifier. Using fallback.")
104
+ return "sam_dev_user"
105
+
106
+ return user_identifier
107
+
108
+
109
+ def _extract_user_details(user_info: dict, user_identifier: str) -> tuple:
110
+ email_from_auth = (
111
+ user_info.get("email")
112
+ or user_info.get("preferred_username")
113
+ or user_info.get("upn")
114
+ or user_identifier
115
+ )
116
+
117
+ display_name = (
118
+ user_info.get("name")
119
+ or user_info.get("given_name", "") + " " + user_info.get("family_name", "")
120
+ or user_info.get("preferred_username")
121
+ or user_identifier
122
+ ).strip()
123
+
124
+ return email_from_auth, display_name
125
+
126
+
127
+ async def _create_user_state_without_identity_service(user_identifier: str, email_from_auth: str, display_name: str) -> dict:
128
+ final_user_id = user_identifier or email_from_auth or "sam_dev_user"
129
+ if not final_user_id or final_user_id.lower() in ["unknown", "null", "none", ""]:
130
+ final_user_id = "sam_dev_user"
131
+ log.warning(
132
+ "AuthMiddleware: Had to use fallback user ID due to invalid identifier: %s",
133
+ user_identifier,
134
+ )
135
+
136
+ log.error(
137
+ "AuthMiddleware: Internal IdentityService not configured on component. Using user ID: %s",
138
+ final_user_id,
139
+ )
140
+ return {
141
+ "id": final_user_id,
142
+ "email": email_from_auth or final_user_id,
143
+ "name": display_name or final_user_id,
144
+ "authenticated": True,
145
+ "auth_method": "oidc",
128
146
  }
129
147
 
130
- dependencies.set_api_config(api_config_dict)
131
- log.info("API configuration extracted and stored.")
132
148
 
149
+ async def _create_user_state_with_identity_service(identity_service, user_identifier: str, email_from_auth: str, display_name: str, user_info: dict) -> dict:
150
+ lookup_value = email_from_auth if "@" in email_from_auth else user_identifier
151
+ user_profile = await identity_service.get_user_profile(
152
+ {identity_service.lookup_key: lookup_value},
153
+ user_info=user_info
154
+ )
155
+
156
+ if not user_profile:
157
+ return None
158
+
159
+ user_state = user_profile.copy()
160
+ if not user_state.get("id"):
161
+ user_state["id"] = user_identifier
162
+ if not user_state.get("email"):
163
+ user_state["email"] = email_from_auth
164
+ if not user_state.get("name"):
165
+ user_state["name"] = display_name
166
+ user_state["authenticated"] = True
167
+ user_state["auth_method"] = "oidc"
168
+
169
+ return user_state
170
+
171
+
172
+ def _create_auth_middleware(component):
133
173
  class AuthMiddleware:
134
174
  def __init__(self, app, component):
135
175
  self.app = app
@@ -147,304 +187,247 @@ def setup_dependencies(component: "WebUIBackendComponent", database_url: str = N
147
187
  return
148
188
 
149
189
  skip_paths = [
150
- "/api/v1/config",
151
- "/api/v1/auth/callback",
152
- "/api/v1/auth/login",
153
- "/api/v1/auth/refresh",
154
- "/api/v1/csrf-token",
155
- "/health",
190
+ "/api/v1/config", "/api/v1/auth/callback", "/api/v1/auth/login",
191
+ "/api/v1/auth/refresh", "/api/v1/csrf-token", "/health",
156
192
  ]
157
193
 
158
194
  if any(request.url.path.startswith(path) for path in skip_paths):
159
195
  await self.app(scope, receive, send)
160
196
  return
161
197
 
162
- use_auth = dependencies.api_config and dependencies.api_config.get(
163
- "frontend_use_authorization"
164
- )
198
+ use_auth = dependencies.api_config and dependencies.api_config.get("frontend_use_authorization")
165
199
 
166
200
  if use_auth:
167
- access_token = None
168
- auth_header = request.headers.get("Authorization")
169
- if auth_header and auth_header.startswith("Bearer "):
170
- access_token = auth_header[7:]
171
-
172
- if not access_token:
173
- try:
174
- if "access_token" in request.session:
175
- access_token = request.session["access_token"]
176
- log.debug("AuthMiddleware: Found token in session.")
177
- except AssertionError:
178
- log.debug("AuthMiddleware: Could not access request.session.")
179
- pass
180
-
181
- if not access_token:
182
- if "token" in request.query_params:
183
- access_token = request.query_params["token"]
184
-
185
- if not access_token:
186
- log.warning("AuthMiddleware: No access token found. Returning 401.")
201
+ await self._handle_authenticated_request(request, scope, receive, send)
202
+ else:
203
+ request.state.user = {
204
+ "id": "sam_dev_user", "name": "Sam Dev User", "email": "sam@dev.local",
205
+ "authenticated": True, "auth_method": "development",
206
+ }
207
+ log.debug("AuthMiddleware: Set development user state with id: sam_dev_user")
208
+
209
+ await self.app(scope, receive, send)
210
+
211
+ async def _handle_authenticated_request(self, request, scope, receive, send):
212
+ access_token = _extract_access_token(request)
213
+
214
+ if not access_token:
215
+ log.warning("AuthMiddleware: No access token found. Returning 401.")
216
+ response = JSONResponse(
217
+ status_code=status.HTTP_401_UNAUTHORIZED,
218
+ content={"detail": "Not authenticated", "error_type": "authentication_required"},
219
+ )
220
+ await response(scope, receive, send)
221
+ return
222
+
223
+ try:
224
+ auth_service_url = dependencies.api_config.get("external_auth_service_url")
225
+ auth_provider = dependencies.api_config.get("external_auth_provider")
226
+
227
+ if not auth_service_url:
228
+ log.error("Auth service URL not configured.")
229
+ response = JSONResponse(
230
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
231
+ content={"detail": "Auth service not configured"},
232
+ )
233
+ await response(scope, receive, send)
234
+ return
235
+
236
+ if not await _validate_token(auth_service_url, auth_provider, access_token):
237
+ log.warning("AuthMiddleware: Token validation failed")
187
238
  response = JSONResponse(
188
239
  status_code=status.HTTP_401_UNAUTHORIZED,
189
- content={
190
- "detail": "Not authenticated",
191
- "error_type": "authentication_required",
192
- },
240
+ content={"detail": "Invalid token", "error_type": "invalid_token"},
193
241
  )
194
242
  await response(scope, receive, send)
195
243
  return
196
244
 
197
- try:
198
- auth_service_url = dependencies.api_config.get(
199
- "external_auth_service_url"
245
+ user_info = await _get_user_info(auth_service_url, auth_provider, access_token)
246
+ if not user_info:
247
+ log.warning("AuthMiddleware: Failed to get user info from external auth service")
248
+ response = JSONResponse(
249
+ status_code=status.HTTP_401_UNAUTHORIZED,
250
+ content={"detail": "Could not retrieve user info from auth provider", "error_type": "user_info_failed"},
200
251
  )
201
- auth_provider = dependencies.api_config.get(
202
- "external_auth_provider"
252
+ await response(scope, receive, send)
253
+ return
254
+
255
+ user_identifier = _extract_user_identifier(user_info)
256
+ if not user_identifier or user_identifier.lower() in ["null", "none", ""]:
257
+ log.error("AuthMiddleware: No valid user identifier from OAuth provider")
258
+ response = JSONResponse(
259
+ status_code=status.HTTP_401_UNAUTHORIZED,
260
+ content={"detail": "OAuth provider returned no valid user identifier", "error_type": "invalid_user_identifier_from_provider"},
203
261
  )
262
+ await response(scope, receive, send)
263
+ return
264
+
265
+ email_from_auth, display_name = _extract_user_details(user_info, user_identifier)
204
266
 
205
- if not auth_service_url:
206
- log.error("Auth service URL not configured.")
267
+ identity_service = self.component.identity_service
268
+ if not identity_service:
269
+ request.state.user = await _create_user_state_without_identity_service(user_identifier, email_from_auth, display_name)
270
+ else:
271
+ user_state = await _create_user_state_with_identity_service(identity_service, user_identifier, email_from_auth, display_name, user_info)
272
+ if not user_state:
273
+ log.error("AuthMiddleware: User authenticated but not found in internal IdentityService")
207
274
  response = JSONResponse(
208
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
209
- content={"detail": "Auth service not configured"},
275
+ status_code=status.HTTP_403_FORBIDDEN,
276
+ content={"detail": "User not authorized for this application", "error_type": "not_authorized"},
210
277
  )
211
278
  await response(scope, receive, send)
212
279
  return
280
+ request.state.user = user_state
213
281
 
214
- async with httpx.AsyncClient() as client:
215
- validation_response = await client.post(
216
- f"{auth_service_url}/is_token_valid",
217
- json={"provider": auth_provider},
218
- headers={"Authorization": f"Bearer {access_token}"},
219
- )
282
+ except httpx.RequestError as exc:
283
+ log.error("Error calling auth service: %s", exc)
284
+ response = JSONResponse(
285
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
286
+ content={"detail": "Auth service is unavailable"},
287
+ )
288
+ await response(scope, receive, send)
289
+ return
290
+ except Exception as exc:
291
+ log.error("An unexpected error occurred during token validation: %s", exc)
292
+ response = JSONResponse(
293
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
294
+ content={"detail": "An internal error occurred during authentication"},
295
+ )
296
+ await response(scope, receive, send)
297
+ return
220
298
 
221
- if validation_response.status_code != 200:
222
- log.warning(
223
- "AuthMiddleware: Token validation failed with status %s: %s",
224
- validation_response.status_code,
225
- validation_response.text,
226
- )
227
- response = JSONResponse(
228
- status_code=status.HTTP_401_UNAUTHORIZED,
229
- content={
230
- "detail": "Invalid token",
231
- "error_type": "invalid_token",
232
- },
233
- )
234
- await response(scope, receive, send)
235
- return
299
+ return AuthMiddleware
236
300
 
237
- async with httpx.AsyncClient() as client:
238
- userinfo_response = await client.get(
239
- f"{auth_service_url}/user_info?provider={auth_provider}",
240
- headers={"Authorization": f"Bearer {access_token}"},
241
- )
242
301
 
243
- if userinfo_response.status_code != 200:
244
- log.warning(
245
- "AuthMiddleware: Failed to get user info from external auth service: %s",
246
- userinfo_response.status_code,
247
- )
248
- response = JSONResponse(
249
- status_code=status.HTTP_401_UNAUTHORIZED,
250
- content={
251
- "detail": "Could not retrieve user info from auth provider",
252
- "error_type": "user_info_failed",
253
- },
254
- )
255
- await response(scope, receive, send)
256
- return
302
+ def _setup_alembic_config(database_url: str) -> Config:
303
+ alembic_cfg = Config()
304
+ alembic_cfg.set_main_option(
305
+ "script_location",
306
+ os.path.join(os.path.dirname(__file__), "alembic"),
307
+ )
308
+ alembic_cfg.set_main_option("sqlalchemy.url", database_url)
309
+ return alembic_cfg
257
310
 
258
- user_info = userinfo_response.json()
259
- log.info(
260
- "AuthMiddleware: Raw user info from OAuth provider: %s",
261
- user_info,
262
- )
263
311
 
264
- # Priority order for user identifier (most specific to least specific)
265
- user_identifier = (
266
- user_info.get("sub") # Standard OIDC subject claim
267
- or user_info.get("client_id") # Mini IDP and some custom IDPs
268
- or user_info.get("username") # Mini IDP returns username field
269
- or user_info.get("oid") # Azure AD object ID
270
- or user_info.get(
271
- "preferred_username"
272
- ) # Common in enterprise IDPs
273
- or user_info.get("upn") # Azure AD User Principal Name
274
- or user_info.get("unique_name") # Some Azure configurations
275
- or user_info.get("email") # Fallback to email
276
- or user_info.get("name") # Last resort
277
- or user_info.get("azp") # Authorized party (rare but possible)
278
- )
312
+ def _run_community_migrations(database_url: str) -> None:
313
+ try:
314
+ from sqlalchemy import create_engine
279
315
 
280
- # IMPORTANT: If the extracted identifier is "Unknown", it means the IDP
281
- # didn't properly authenticate or is misconfigured. Use a fallback.
282
- if user_identifier and user_identifier.lower() == "unknown":
283
- log.warning(
284
- "AuthMiddleware: IDP returned 'Unknown' as user identifier. This indicates misconfiguration. Using fallback."
285
- )
286
- # In development mode with mini IDP, default to sam_dev_user
287
- # This is a workaround for the OAuth2 proxy service returning "Unknown"
288
- user_identifier = "sam_dev_user" # Fallback for development
289
- log.info(
290
- "AuthMiddleware: Using development fallback user: sam_dev_user"
291
- )
316
+ engine = create_engine(database_url)
317
+ inspector = sa.inspect(engine)
318
+ existing_tables = inspector.get_table_names()
292
319
 
293
- # Extract email separately (may be different from user identifier)
294
- email_from_auth = (
295
- user_info.get("email")
296
- or user_info.get("preferred_username")
297
- or user_info.get("upn")
298
- or user_identifier
299
- )
320
+ if not existing_tables or "sessions" not in existing_tables:
321
+ log.info("Running community database migrations...")
322
+ alembic_cfg = _setup_alembic_config(database_url)
323
+ command.upgrade(alembic_cfg, "head")
324
+ log.info("Community database migrations complete.")
325
+ else:
326
+ log.info(
327
+ "Community database tables already exist, skipping community migrations."
328
+ )
329
+ except Exception as e:
330
+ log.warning(
331
+ "Community migration check failed, attempting to run migrations anyway: %s",
332
+ e,
333
+ )
334
+ try:
335
+ alembic_cfg = _setup_alembic_config(database_url)
336
+ command.upgrade(alembic_cfg, "head")
337
+ log.info("Community database migrations complete.")
338
+ except Exception as migration_error:
339
+ log.warning(
340
+ "Community migration failed but continuing: %s", migration_error
341
+ )
300
342
 
301
- # Extract display name
302
- display_name = (
303
- user_info.get("name")
304
- or user_info.get("given_name", "")
305
- + " "
306
- + user_info.get("family_name", "")
307
- or user_info.get("preferred_username")
308
- or user_identifier
309
- ).strip()
310
-
311
- log.info(
312
- "AuthMiddleware: Extracted user identifier: %s, email: %s, name: %s",
313
- user_identifier,
314
- email_from_auth,
315
- display_name,
316
- )
317
343
 
318
- if not user_identifier or user_identifier.lower() in [
319
- "null",
320
- "none",
321
- "",
322
- ]:
323
- log.error(
324
- "AuthMiddleware: No valid user identifier from OAuth provider. Full user info: %s. Expected valid user identifier.",
325
- user_info,
326
- )
327
- response = JSONResponse(
328
- status_code=status.HTTP_401_UNAUTHORIZED,
329
- content={
330
- "detail": "OAuth provider returned no valid user identifier. Provider must return at least one of: sub, username, client_id, preferred_username, email, or name field.",
331
- "error_type": "invalid_user_identifier_from_provider",
332
- "received_user_info": user_info,
333
- },
334
- )
335
- await response(scope, receive, send)
336
- return
344
+ def _run_enterprise_migrations(component: "WebUIBackendComponent", database_url: str) -> None:
345
+ try:
346
+ from solace_agent_mesh_enterprise.webui_backend.migration_runner import (
347
+ run_migrations,
348
+ )
337
349
 
338
- identity_service = self.component.identity_service
339
- if not identity_service:
340
- # Make absolutely sure we have a valid user ID - never "Unknown"
341
- final_user_id = (
342
- user_identifier or email_from_auth or "sam_dev_user"
343
- )
344
- if not final_user_id or final_user_id.lower() in [
345
- "unknown",
346
- "null",
347
- "none",
348
- "",
349
- ]:
350
- final_user_id = "sam_dev_user"
351
- log.warning(
352
- "AuthMiddleware: Had to use fallback user ID due to invalid identifier: %s",
353
- user_identifier,
354
- )
355
-
356
- log.error(
357
- "AuthMiddleware: Internal IdentityService not configured on component. Using user ID: %s",
358
- final_user_id,
359
- )
360
- request.state.user = {
361
- "id": final_user_id,
362
- "email": email_from_auth or final_user_id,
363
- "name": display_name or final_user_id,
364
- "authenticated": True,
365
- "auth_method": "oidc",
366
- }
367
- log.info(
368
- "AuthMiddleware: Set fallback user state with id: %s",
369
- final_user_id,
370
- )
371
- else:
372
- # Try to look up user profile using the email or user identifier
373
- lookup_value = (
374
- email_from_auth
375
- if "@" in email_from_auth
376
- else user_identifier
377
- )
378
- user_profile = await identity_service.get_user_profile(
379
- {identity_service.lookup_key: lookup_value}
380
- )
381
- if not user_profile:
382
- log.error(
383
- "AuthMiddleware: User '%s' authenticated but not found in internal IdentityService.",
384
- lookup_value,
385
- )
386
- response = JSONResponse(
387
- status_code=status.HTTP_403_FORBIDDEN,
388
- content={
389
- "detail": "User not authorized for this application",
390
- "error_type": "not_authorized",
391
- },
392
- )
393
- await response(scope, receive, send)
394
- return
395
-
396
- request.state.user = user_profile.copy()
397
- # Ensure the ID is set from the OAuth provider if not present in the profile
398
- if not request.state.user.get("id"):
399
- request.state.user["id"] = user_identifier
400
- # Also ensure email and name are set if not in profile
401
- if not request.state.user.get("email"):
402
- request.state.user["email"] = email_from_auth
403
- if not request.state.user.get("name"):
404
- request.state.user["name"] = display_name
405
- request.state.user["authenticated"] = True
406
- request.state.user["auth_method"] = "oidc"
407
- log.info(
408
- "AuthMiddleware: Set enriched user profile with id: %s",
409
- request.state.user.get("id"),
410
- )
350
+ webui_app = component.get_app()
351
+ app_config = getattr(webui_app, "app_config", {}) if webui_app else {}
352
+ log.info("Running enterprise migrations...")
353
+ run_migrations(database_url, app_config)
354
+ log.info("Enterprise migrations completed")
355
+ except (ImportError, ModuleNotFoundError):
356
+ log.debug("Enterprise module not found - skipping enterprise migrations")
357
+ except Exception as e:
358
+ log.warning("Enterprise migration failed but continuing: %s", e)
411
359
 
412
- except httpx.RequestError as exc:
413
- log.error("Error calling auth service: %s", exc)
414
- response = JSONResponse(
415
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
416
- content={"detail": "Auth service is unavailable"},
417
- )
418
- await response(scope, receive, send)
419
- return
420
- except Exception as exc:
421
- log.error(
422
- "An unexpected error occurred during token validation: %s", exc
423
- )
424
- response = JSONResponse(
425
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
426
- content={
427
- "detail": "An internal error occurred during authentication"
428
- },
429
- )
430
- await response(scope, receive, send)
431
- return
432
- else:
433
- # If auth is not used, set a default user
434
- request.state.user = {
435
- "id": "sam_dev_user",
436
- "name": "Sam Dev User",
437
- "email": "sam@dev.local",
438
- "authenticated": True,
439
- "auth_method": "development",
440
- }
441
- log.debug(
442
- "AuthMiddleware: Set development user state with id: sam_dev_user"
443
- )
444
360
 
445
- await self.app(scope, receive, send)
361
+ def _setup_database(component: "WebUIBackendComponent", database_url: str) -> None:
362
+ dependencies.init_database(database_url)
363
+ log.info("Persistence enabled - sessions will be stored in database")
364
+ log.info("Checking database migrations...")
365
+
366
+ _run_community_migrations(database_url)
367
+ _run_enterprise_migrations(component, database_url)
368
+
369
+
370
+ def _get_app_config(component: "WebUIBackendComponent") -> dict:
371
+ webui_app = component.get_app()
372
+ app_config = {}
373
+ if webui_app:
374
+ app_config = getattr(webui_app, "app_config", {})
375
+ if app_config is None:
376
+ log.warning("webui_app.app_config is None, using empty dict.")
377
+ app_config = {}
378
+ else:
379
+ log.warning("Could not get webui_app from component. Using empty app_config.")
380
+ return app_config
381
+
382
+
383
+ def _create_api_config(app_config: dict, database_url: str) -> dict:
384
+ return {
385
+ "external_auth_service_url": app_config.get(
386
+ "external_auth_service_url", "http://localhost:8080"
387
+ ),
388
+ "external_auth_callback_uri": app_config.get(
389
+ "external_auth_callback_uri", "http://localhost:8000/api/v1/auth/callback"
390
+ ),
391
+ "external_auth_provider": app_config.get("external_auth_provider", "azure"),
392
+ "frontend_use_authorization": app_config.get(
393
+ "frontend_use_authorization", False
394
+ ),
395
+ "frontend_redirect_url": app_config.get(
396
+ "frontend_redirect_url", "http://localhost:3000"
397
+ ),
398
+ "persistence_enabled": database_url is not None,
399
+ }
400
+
401
+
402
+ def setup_dependencies(component: "WebUIBackendComponent", database_url: str = None):
403
+ """
404
+ This function initializes the simplified architecture while maintaining full
405
+ backward compatibility with existing API contracts.
406
+
407
+ If database_url is None, runs in compatibility mode with in-memory sessions.
408
+ """
409
+ dependencies.set_component_instance(component)
446
410
 
447
- # Add middleware
411
+ if database_url:
412
+ _setup_database(component, database_url)
413
+ else:
414
+ log.warning(
415
+ "No database URL provided - using in-memory session storage (data not persisted across restarts)"
416
+ )
417
+ log.info("This maintains backward compatibility for existing SAM installations")
418
+
419
+ app_config = _get_app_config(component)
420
+ api_config_dict = _create_api_config(app_config, database_url)
421
+
422
+ dependencies.set_api_config(api_config_dict)
423
+ log.info("API configuration extracted and stored.")
424
+
425
+ _setup_middleware(component)
426
+ _setup_routers()
427
+ _setup_static_files()
428
+
429
+
430
+ def _setup_middleware(component: "WebUIBackendComponent") -> None:
448
431
  allowed_origins = component.get_cors_origins()
449
432
  app.add_middleware(
450
433
  CORSMiddleware,
@@ -459,54 +442,33 @@ def setup_dependencies(component: "WebUIBackendComponent", database_url: str = N
459
442
  app.add_middleware(SessionMiddleware, secret_key=session_manager.secret_key)
460
443
  log.info("SessionMiddleware added.")
461
444
 
462
- app.add_middleware(AuthMiddleware, component=component)
445
+ auth_middleware_class = _create_auth_middleware(component)
446
+ app.add_middleware(auth_middleware_class, component=component)
463
447
  log.info("AuthMiddleware added.")
464
448
 
465
- # Mount API routers
449
+
450
+ def _setup_routers() -> None:
466
451
  api_prefix = "/api/v1"
467
452
 
468
- # Mount persistence-aware controllers (your original controllers with full functionality)
469
- # These provide the complete API surface with database persistence
470
- app.include_router(
471
- session_router, prefix=api_prefix, tags=["Sessions"]
472
- ) # Provides /api/v1/sessions/* endpoints
473
- app.include_router(
474
- user_router, prefix=f"{api_prefix}/users", tags=["Users"]
475
- ) # Provides /api/v1/users/me
476
- app.include_router(
477
- task_router, prefix=f"{api_prefix}/tasks", tags=["Tasks"]
478
- ) # Provides /api/v1/tasks/send, /subscribe, /cancel
479
-
480
- # Mount new A2A SDK routers with different paths to avoid conflicts
453
+ app.include_router(session_router, prefix=api_prefix, tags=["Sessions"])
454
+ app.include_router(user_router, prefix=f"{api_prefix}/users", tags=["Users"])
455
+ app.include_router(task_router, prefix=f"{api_prefix}/tasks", tags=["Tasks"])
481
456
  app.include_router(config.router, prefix=api_prefix, tags=["Config"])
482
457
  app.include_router(agent_cards.router, prefix=api_prefix, tags=["Agent Cards"])
483
- # New A2A message endpoints (non-conflicting paths)
484
- app.include_router(
485
- tasks.router, prefix=api_prefix, tags=["A2A Messages"]
486
- ) # Provides /api/v1/message:send, /message:stream
487
- # Note: We only use the full-featured session_router (from api/controllers/session_controller.py)
488
- # which provides complete session management with database persistence
458
+ app.include_router(tasks.router, prefix=api_prefix, tags=["A2A Messages"])
489
459
  app.include_router(sse.router, prefix=f"{api_prefix}/sse", tags=["SSE"])
490
- app.include_router(
491
- artifacts.router, prefix=f"{api_prefix}/artifacts", tags=["Artifacts"]
492
- )
493
- app.include_router(
494
- visualization.router,
495
- prefix=f"{api_prefix}/visualization",
496
- tags=["Visualization"],
497
- )
498
- app.include_router(
499
- people.router,
500
- prefix=api_prefix,
501
- tags=["People"],
502
- )
460
+ app.include_router(artifacts.router, prefix=f"{api_prefix}/artifacts", tags=["Artifacts"])
461
+ app.include_router(visualization.router, prefix=f"{api_prefix}/visualization", tags=["Visualization"])
462
+ app.include_router(people.router, prefix=api_prefix, tags=["People"])
503
463
  app.include_router(auth.router, prefix=api_prefix, tags=["Auth"])
504
464
  log.info("Legacy routers mounted for endpoints not yet migrated")
505
465
 
506
- # Mount static files
466
+
467
+ def _setup_static_files() -> None:
507
468
  current_dir = os.path.dirname(os.path.abspath(__file__))
508
469
  root_dir = Path(os.path.normpath(os.path.join(current_dir, "..", "..")))
509
470
  static_files_dir = Path.joinpath(root_dir, "client", "webui", "frontend", "static")
471
+
510
472
  if not os.path.isdir(static_files_dir):
511
473
  log.warning(
512
474
  "Static files directory '%s' not found. Frontend may not be served.",