xenfra-sdk 0.2.5__py3-none-any.whl → 0.2.7__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 (42) hide show
  1. xenfra_sdk/__init__.py +46 -2
  2. xenfra_sdk/blueprints/base.py +150 -0
  3. xenfra_sdk/blueprints/factory.py +99 -0
  4. xenfra_sdk/blueprints/node.py +219 -0
  5. xenfra_sdk/blueprints/python.py +57 -0
  6. xenfra_sdk/blueprints/railpack.py +99 -0
  7. xenfra_sdk/blueprints/schema.py +70 -0
  8. xenfra_sdk/cli/main.py +175 -49
  9. xenfra_sdk/client.py +6 -2
  10. xenfra_sdk/constants.py +26 -0
  11. xenfra_sdk/db/session.py +8 -3
  12. xenfra_sdk/detection.py +262 -191
  13. xenfra_sdk/dockerizer.py +76 -120
  14. xenfra_sdk/engine.py +767 -172
  15. xenfra_sdk/events.py +254 -0
  16. xenfra_sdk/exceptions.py +9 -0
  17. xenfra_sdk/governance.py +150 -0
  18. xenfra_sdk/manifest.py +93 -138
  19. xenfra_sdk/mcp_client.py +7 -5
  20. xenfra_sdk/{models.py → models/__init__.py} +17 -1
  21. xenfra_sdk/models/context.py +61 -0
  22. xenfra_sdk/orchestrator.py +223 -99
  23. xenfra_sdk/privacy.py +11 -0
  24. xenfra_sdk/protocol.py +38 -0
  25. xenfra_sdk/railpack_adapter.py +357 -0
  26. xenfra_sdk/railpack_detector.py +587 -0
  27. xenfra_sdk/railpack_manager.py +312 -0
  28. xenfra_sdk/recipes.py +152 -19
  29. xenfra_sdk/resources/activity.py +45 -0
  30. xenfra_sdk/resources/build.py +157 -0
  31. xenfra_sdk/resources/deployments.py +22 -2
  32. xenfra_sdk/resources/intelligence.py +25 -0
  33. xenfra_sdk-0.2.7.dist-info/METADATA +118 -0
  34. xenfra_sdk-0.2.7.dist-info/RECORD +49 -0
  35. {xenfra_sdk-0.2.5.dist-info → xenfra_sdk-0.2.7.dist-info}/WHEEL +1 -1
  36. xenfra_sdk/templates/Caddyfile.j2 +0 -14
  37. xenfra_sdk/templates/Dockerfile.j2 +0 -41
  38. xenfra_sdk/templates/cloud-init.sh.j2 +0 -90
  39. xenfra_sdk/templates/docker-compose-multi.yml.j2 +0 -29
  40. xenfra_sdk/templates/docker-compose.yml.j2 +0 -30
  41. xenfra_sdk-0.2.5.dist-info/METADATA +0 -116
  42. xenfra_sdk-0.2.5.dist-info/RECORD +0 -38
@@ -12,7 +12,18 @@ from typing import Callable, Dict, List, Optional
12
12
  import yaml
13
13
 
14
14
  from .manifest import ServiceDefinition, load_services_from_xenfra_yaml
15
- from . import dockerizer
15
+ from . import constants
16
+ from .governance import get_resource_limits
17
+ from .blueprints.factory import render_blueprint
18
+ from .blueprints.schema import (
19
+ ComposeModel,
20
+ ServiceDetail,
21
+ DeployModel,
22
+ DeployResourcesModel,
23
+ ResourceLimitsModel,
24
+ ResourceReservationsModel
25
+ )
26
+ from .models.context import DeploymentContext
16
27
 
17
28
 
18
29
  class ServiceOrchestrator:
@@ -24,7 +35,7 @@ class ServiceOrchestrator:
24
35
  - multi-droplet: Each service on separate droplets with private networking
25
36
  """
26
37
 
27
- def __init__(self, engine, services: List[ServiceDefinition], project_name: str, mode: str = "single-droplet", file_manifest: List[dict] = None):
38
+ def __init__(self, engine, services: List[ServiceDefinition], project_name: str, mode: str = "single-droplet", file_manifest: List[dict] = None, tier: str = "FREE"):
28
39
  """
29
40
  Initialize the orchestrator.
30
41
 
@@ -34,6 +45,7 @@ class ServiceOrchestrator:
34
45
  project_name: Name of the project
35
46
  mode: Deployment mode (single-droplet or multi-droplet)
36
47
  file_manifest: List of files to be uploaded (delta upload)
48
+ tier: User tier for resource governance
37
49
  """
38
50
  self.engine = engine
39
51
  self.services = services
@@ -42,6 +54,7 @@ class ServiceOrchestrator:
42
54
  self.file_manifest = file_manifest or []
43
55
  self.token = engine.token
44
56
  self.manager = engine.manager
57
+ self.tier = tier
45
58
 
46
59
  def deploy(self, logger: Callable = print, **kwargs) -> Dict:
47
60
  """
@@ -89,7 +102,8 @@ class ServiceOrchestrator:
89
102
  logger(f"\n[dim]Recommended droplet size: {size}[/dim]")
90
103
 
91
104
  # Step 2: Generate multi-service docker-compose.yml
92
- compose_content = self._generate_docker_compose()
105
+ global_env = kwargs.get("env_vars") or {}
106
+ compose_content = self._generate_docker_compose(global_env=global_env)
93
107
  logger(f"[dim]Generated docker-compose.yml ({len(compose_content)} bytes)[/dim]")
94
108
 
95
109
  # Step 3: Generate Caddyfile for path-based routing
@@ -117,7 +131,7 @@ class ServiceOrchestrator:
117
131
  explicit_args = [
118
132
  "name", "size", "framework", "port", "is_dockerized",
119
133
  "entrypoint", "multi_service_compose", "multi_service_caddy",
120
- "services", "logger", "extra_assets"
134
+ "services", "logger", "extra_assets", "tier"
121
135
  ]
122
136
  for arg in explicit_args:
123
137
  safe_kwargs.pop(arg, None)
@@ -155,7 +169,7 @@ class ServiceOrchestrator:
155
169
 
156
170
  # Find the file in manifest and get its content
157
171
  existing_finfo = next(f for f in self.file_manifest if f.get("path") in [dockerfile_name, f"./{dockerfile_name}", f"{svc.path}/Dockerfile" if (svc.path and svc.path != ".") else "Dockerfile"])
158
- from .engine import DeploymentError
172
+ from .exceptions import DeploymentError
159
173
  if not self.engine.get_file_content:
160
174
  raise DeploymentError("Cannot inject deps into existing Dockerfile: get_file_content not available", stage="Asset Generation")
161
175
 
@@ -180,50 +194,118 @@ class ServiceOrchestrator:
180
194
  extra_assets[dockerfile_name] = content
181
195
  continue
182
196
 
183
- # Determine command (same logic as _generate_docker_compose)
184
- command = svc.command
185
- if not command and svc.entrypoint:
186
- if svc.framework == "fastapi":
187
- command = f"uvicorn {svc.entrypoint} --host 0.0.0.0 --port {svc.port}"
188
- elif svc.framework == "flask":
189
- command = f"gunicorn {svc.entrypoint} -b 0.0.0.0:{svc.port}"
190
- elif svc.framework == "django":
191
- command = f"gunicorn {svc.entrypoint} --bind 0.0.0.0:{svc.port}"
197
+ # Filter file_manifest for this specific service path
198
+ service_manifest = []
199
+ if self.file_manifest:
200
+ prefix = f"{svc.path}/" if (svc.path and svc.path != ".") else ""
201
+
202
+ # Critical files that need content hydration for blueprint detection
203
+ CRITICAL_CONFIGS = {
204
+ "package.json", "package-lock.json",
205
+ "next.config.js", "next.config.ts", "next.config.mjs",
206
+ "nuxt.config.js", "nuxt.config.ts",
207
+ "vite.config.js", "vite.config.ts",
208
+ "requirements.txt", "pyproject.toml", "Pipfile",
209
+ "go.mod", "Cargo.toml", "deno.json"
210
+ }
211
+
212
+ for f in self.file_manifest:
213
+ fpath = f.get("path", "")
214
+ if fpath.startswith(prefix):
215
+ # Strip prefix for the blueprint logic
216
+ new_f = f.copy()
217
+ new_f["path"] = fpath[len(prefix):]
218
+
219
+ # Hydrate content for critical config files
220
+ # This allows RailpackBlueprint to inspect dependencies (e.g. detect "next" in package.json)
221
+ if new_f["path"] in CRITICAL_CONFIGS and "content" not in new_f and self.engine.get_file_content:
222
+ try:
223
+ logger(f" - [Zen Mode] Hydrating manifest content for {new_f['path']}...")
224
+ content_bytes = self.engine.get_file_content(f.get("sha"))
225
+ if content_bytes:
226
+ new_f["content"] = content_bytes.decode("utf-8", errors="ignore")
227
+ except Exception as e:
228
+ logger(f" [yellow]Warning: Failed to hydrate {new_f['path']}: {e}[/yellow]")
229
+
230
+ service_manifest.append(new_f)
231
+
232
+ # Protocol Compliance: Use Blueprint Factory instead of legacy dockerizer
233
+ # Build context for this specific service
234
+ # ZEN DEBUG: Log framework being used for Dockerfile generation
235
+ # Fix: Do not default to 'python'. Let factory.py resolve it (supports Railpack fallback)
236
+ framework_to_use = svc.framework
237
+ logger(f" - [Blueprint] Generating Dockerfile for {svc.name} (framework={framework_to_use})")
192
238
 
193
- # Render assets using dockerizer
194
- ctx = {
195
- "framework": svc.framework,
196
- "port": svc.port,
197
- "command": command,
198
- "missing_deps": svc.missing_deps,
199
- # Pass through other potential context
200
- "database": None,
201
- }
202
- assets = dockerizer.render_deployment_assets(ctx)
239
+ # Resolve source path for Railpack analysis
240
+ # Fixes "User Environment Leak" where Railpack defaults to CWD if no source_path is provided
241
+ source_path = None
242
+ local_root = self.engine.context.get('local_source_path')
243
+ if local_root:
244
+ if svc.path and svc.path != ".":
245
+ source_path = str(Path(local_root) / svc.path)
246
+ else:
247
+ source_path = local_root
248
+
249
+ ctx = DeploymentContext(
250
+ project_name=f"{self.project_name}-{svc.name}",
251
+ email=self.engine.get_user_info().email,
252
+ framework=framework_to_use,
253
+ port=svc.port,
254
+ entrypoint=svc.entrypoint,
255
+ env_vars={**(svc.env or {}), **global_env},
256
+ tier=self.tier,
257
+ file_manifest=service_manifest,
258
+ source_path=source_path # Pass explicit source path
259
+ )
260
+
261
+ assets = render_blueprint(ctx.model_dump())
203
262
  if "Dockerfile" in assets:
204
263
  extra_assets[dockerfile_name] = assets["Dockerfile"]
264
+
205
265
 
266
+ # Consolidate all environment variables into a single .env file
267
+ # This ensures they are available during 'docker build' for Next.js
268
+ master_env = {**(global_env or {})}
269
+ for svc in self.services:
270
+ if svc.env:
271
+ master_env.update(svc.env)
272
+
273
+ if master_env:
274
+ # Use double quotes for values to handle spaces and special chars safely
275
+ env_content = "\n".join([f'{k}="{v}"' for k, v in master_env.items()])
276
+ extra_assets[".env"] = env_content
277
+ logger(f" - [Zen Mode] Consolidated {len(master_env)} variables into .env asset")
278
+
206
279
  if extra_assets:
207
- logger(f"[dim]Generated {len(extra_assets)} service Dockerfiles[/dim]")
280
+ logger(f"[dim]Generated {len(extra_assets)} deployment assets[/dim]")
208
281
 
209
282
  deployment_result = self.engine.deploy_server(
210
283
  name=self.project_name,
211
284
  size=size,
212
285
  framework=primary_service.framework,
213
- port=80, # Caddy listens on 80
286
+ port=80, # Caddy always listens on 80 for the gateway
214
287
  is_dockerized=True,
215
- entrypoint=None, # We'll use docker-compose
216
- # Pass multi-service config
288
+ entrypoint=None,
217
289
  multi_service_compose=compose_content,
218
290
  multi_service_caddy=caddy_content,
219
- extra_assets=extra_assets, # Pass generated Dockerfiles
291
+ extra_assets=extra_assets,
220
292
  services=self.services,
221
293
  logger=logger,
294
+ tier=self.tier,
222
295
  **safe_kwargs
223
296
  )
224
297
 
225
- result["droplet"] = deployment_result.get("droplet")
226
- result["url"] = f"http://{deployment_result.get('ip_address')}"
298
+ # ZEN MODE: Propagate Dry Run
299
+ if isinstance(deployment_result, dict) and deployment_result.get("status") == "DRY_RUN":
300
+ return deployment_result
301
+
302
+ if isinstance(deployment_result, dict):
303
+ result["droplet"] = deployment_result.get("droplet")
304
+ result["url"] = f"http://{deployment_result.get('ip_address')}"
305
+ else:
306
+ # DigitalOcean Droplet object
307
+ result["droplet"] = deployment_result
308
+ result["url"] = f"http://{deployment_result.ip_address}"
227
309
 
228
310
  # Step 5: Health check all services
229
311
  if result["droplet"]:
@@ -254,82 +336,107 @@ class ServiceOrchestrator:
254
336
 
255
337
  def _calculate_droplet_size(self) -> str:
256
338
  """
257
- Recommend droplet size based on number of services.
258
-
259
- Guidelines:
260
- - 1-2 services: s-1vcpu-2gb ($12/month)
261
- - 3-5 services: s-2vcpu-4gb ($24/month)
262
- - 6+ services: s-4vcpu-8gb ($48/month)
339
+ Recommend droplet size based on number of services using constants.
263
340
  """
341
+ from . import constants
264
342
  service_count = len(self.services)
265
343
 
266
344
  if service_count <= 2:
267
- return "s-1vcpu-2gb"
345
+ return constants.SIZE_LIGHT
268
346
  elif service_count <= 5:
269
- return "s-2vcpu-4gb"
347
+ return constants.SIZE_MEDIUM
270
348
  else:
271
- return "s-4vcpu-8gb"
349
+ return constants.SIZE_HEAVY
272
350
 
273
- def _generate_docker_compose(self) -> str:
351
+ def _generate_docker_compose(self, global_env: Dict[str, str] = None) -> str:
274
352
  """
275
- Generate docker-compose.yml for all services.
276
-
277
- Each service gets:
278
- - Its own container
279
- - Port mapping
280
- - Environment variables
281
- - Restart policy
353
+ Generate docker-compose.yml using ComposeModel Pydantic schema.
282
354
  """
283
- services_config = {}
355
+ services_map = {}
284
356
 
285
357
  for svc in self.services:
286
- # Build command based on framework and entrypoint
358
+ # Resolve source path for Railpack analysis
359
+ source_path = None
360
+ local_root = self.engine.context.get('local_source_path')
361
+ if local_root:
362
+ if svc.path and svc.path != ".":
363
+ source_path = str(Path(local_root) / svc.path)
364
+ else:
365
+ source_path = local_root
366
+
367
+ # Build context for this service to get protocol-compliant command/limits
368
+ temp_ctx = DeploymentContext(
369
+ project_name=svc.name,
370
+ email="admin@xenfra.tech",
371
+ framework=svc.framework,
372
+ port=svc.port,
373
+ entrypoint=svc.entrypoint,
374
+ tier=self.tier,
375
+ source_path=source_path # Pass explicit source path
376
+ )
377
+
378
+ # Resolve command if not explicit (Zen Mode)
287
379
  command = svc.command
288
380
  if not command and svc.entrypoint:
289
- if svc.framework == "fastapi":
290
- command = f"uvicorn {svc.entrypoint} --host 0.0.0.0 --port {svc.port}"
291
- elif svc.framework == "flask":
292
- command = f"gunicorn {svc.entrypoint} -b 0.0.0.0:{svc.port}"
293
- elif svc.framework == "django":
294
- command = f"gunicorn {svc.entrypoint} --bind 0.0.0.0:{svc.port}"
295
-
296
- service_entry = {
297
- "build": {
298
- "context": svc.path or ".",
299
- "dockerfile": f"Dockerfile.{svc.name}"
300
- },
301
- "ports": [f"{svc.port}:{svc.port}"],
302
- "restart": "unless-stopped",
303
- }
304
-
305
- if command:
306
- service_entry["command"] = command
307
-
308
- if svc.env:
309
- service_entry["environment"] = svc.env
381
+ # Use standard blueprint-derived command logic
382
+ from .blueprints.python import PythonBlueprint
383
+ from .blueprints.node import NodeBlueprint
384
+ # This is still a bit coupled, but better than naked if-elifs
385
+ if svc.framework in ("fastapi", "flask", "django"):
386
+ command = PythonBlueprint(temp_ctx.model_dump())._get_default_command()
387
+ elif svc.framework == "nodejs":
388
+ command = NodeBlueprint(temp_ctx.model_dump())._get_default_command()
389
+
390
+ # Create ServiceDetail model
391
+ # Create ServiceDetail model
392
+ # ZEN FIX: Map flat ResourceLimits to nested DeployModel
393
+ tier_limits = get_resource_limits(self.tier)
394
+ service_detail = ServiceDetail(
395
+ build=svc.path or ".",
396
+ ports=[f"{svc.port}:{svc.port}"],
397
+ env_file=[".env"],
398
+ command=command,
399
+ deploy=DeployModel(
400
+ resources=DeployResourcesModel(
401
+ limits=ResourceLimitsModel(
402
+ memory=tier_limits.memory,
403
+ cpus=tier_limits.cpus
404
+ ),
405
+ reservations=ResourceReservationsModel(
406
+ memory=tier_limits.memory_reserved,
407
+ cpus=tier_limits.cpus_reserved
408
+ )
409
+ )
410
+ )
411
+ )
310
412
 
311
- services_config[svc.name] = service_entry
312
-
313
- compose = {
314
- "services": services_config
315
- }
413
+ # Use specific Dockerfile if generated
414
+ if f"Dockerfile.{svc.name}" in [f.get("path") for f in self.file_manifest] or True: # Force for orchestrator
415
+ service_detail.build_context = {
416
+ "context": svc.path or ".",
417
+ "dockerfile": f"Dockerfile.{svc.name}"
418
+ }
419
+
420
+ services_map[svc.name] = service_detail
316
421
 
317
- return yaml.dump(compose, default_flow_style=False, sort_keys=False)
422
+ compose = ComposeModel(services=services_map)
423
+ return yaml.dump(compose.model_dump(by_alias=True, exclude_none=True), sort_keys=False, default_flow_style=False)
318
424
 
319
425
  def _generate_caddyfile(self) -> str:
320
426
  """
321
427
  Generate Caddyfile for path-based routing.
322
428
 
323
429
  Routes:
324
- - /<service-name>/* → localhost:<service-port> (Strip prefix)
430
+ - /<service-name>/* → <service-name>:<service-port> (Strip prefix)
325
431
  - / → Gateway info page (Exact match only)
326
432
  """
327
433
  routes = []
328
434
 
329
435
  for svc in self.services:
330
436
  # handle_path strips the prefix automatically
437
+ # Fix: Use service name instead of localhost for Docker networking
331
438
  route = f""" handle_path /{svc.name}* {{
332
- reverse_proxy localhost:{svc.port}
439
+ reverse_proxy {svc.name}:{svc.port}
333
440
  }}"""
334
441
  routes.append(route)
335
442
 
@@ -450,7 +557,8 @@ class ServiceOrchestrator:
450
557
  logger(f"\n[cyan]Deploying {svc.name}...[/cyan]")
451
558
 
452
559
  # Generate single-service docker-compose
453
- compose_content = self._generate_single_service_compose(svc)
560
+ global_env = kwargs.get("env_vars") or {}
561
+ compose_content = self._generate_single_service_compose(svc, global_env=global_env)
454
562
 
455
563
  try:
456
564
  droplet_result = self.engine.deploy_server(
@@ -559,7 +667,7 @@ class ServiceOrchestrator:
559
667
 
560
668
  return result
561
669
 
562
- def _generate_single_service_compose(self, svc: ServiceDefinition) -> str:
670
+ def _generate_single_service_compose(self, svc: ServiceDefinition, global_env: Dict[str, str] = None) -> str:
563
671
  """Generate docker-compose.yml for a single service."""
564
672
  command = svc.command
565
673
  if not command and svc.entrypoint:
@@ -574,13 +682,17 @@ class ServiceOrchestrator:
574
682
  "build": svc.path or ".",
575
683
  "ports": [f"{svc.port}:{svc.port}"],
576
684
  "restart": "unless-stopped",
685
+ # ZEN GAP FIX: Inject resource governance
686
+ "deploy": {
687
+ "resources": get_resource_limits(self.tier).model_dump(by_alias=True, exclude_none=True)
688
+ }
577
689
  }
578
690
 
579
691
  if command:
580
692
  service_entry["command"] = command
581
693
 
582
- if svc.env:
583
- service_entry["environment"] = svc.env
694
+ if svc.env or global_env:
695
+ service_entry["environment"] = {**(svc.env or {}), **(global_env or {})}
584
696
 
585
697
  compose = {
586
698
  "services": {
@@ -634,33 +746,45 @@ class ServiceOrchestrator:
634
746
  return None
635
747
 
636
748
 
637
- def get_orchestrator_for_project(engine, project_path: str = ".") -> Optional[ServiceOrchestrator]:
749
+ def get_orchestrator_for_project(engine, project_path: str = ".", tier: str = "FREE") -> Optional[ServiceOrchestrator]:
638
750
  """
639
- Factory function to create an orchestrator if xenfra.yaml has services.
751
+ Factory function to create an orchestrator if xenfra.yaml or auto-detection finds services.
640
752
 
641
753
  Args:
642
754
  engine: InfraEngine instance
643
755
  project_path: Path to project directory
756
+ tier: User tier for resource governance
644
757
 
645
758
  Returns:
646
- ServiceOrchestrator if services found in xenfra.yaml, None otherwise
759
+ ServiceOrchestrator if services found, None otherwise
647
760
  """
648
- from .manifest import load_services_from_xenfra_yaml, get_deployment_mode
761
+ from .manifest import load_services_from_xenfra_yaml, create_services_from_detected
762
+ from .detection import auto_detect_services
649
763
 
764
+ # 1. Try manifest first (User intent)
650
765
  services = load_services_from_xenfra_yaml(project_path)
766
+ yaml_path = Path(project_path) / "xenfra.yaml"
651
767
 
652
- if services and len(services) > 1:
653
- # Get project name from xenfra.yaml
654
- yaml_path = Path(project_path) / "xenfra.yaml"
655
- project_name = Path(project_path).name
656
- mode = "single-droplet"
657
-
658
- if yaml_path.exists():
659
- with open(yaml_path) as f:
768
+ project_name = Path(project_path).name
769
+ mode = "single-droplet"
770
+
771
+ if yaml_path.exists():
772
+ with open(yaml_path) as f:
773
+ try:
660
774
  data = yaml.safe_load(f)
661
- project_name = data.get("project_name", project_name)
662
- mode = data.get("mode", "single-droplet")
663
-
664
- return ServiceOrchestrator(engine, services, project_name, mode)
775
+ if data:
776
+ project_name = data.get("project_name", project_name)
777
+ mode = data.get("mode", "single-droplet")
778
+ except Exception:
779
+ pass
780
+
781
+ # 2. Fallback to Autonomous Detection
782
+ if not services:
783
+ detected = auto_detect_services(project_path)
784
+ if detected and len(detected) > 1:
785
+ services = create_services_from_detected(detected)
786
+
787
+ if services and len(services) > 1:
788
+ return ServiceOrchestrator(engine, services, project_name, mode, tier=tier)
665
789
 
666
790
  return None
xenfra_sdk/privacy.py CHANGED
@@ -124,6 +124,17 @@ def scrub_logs(logs: str) -> str:
124
124
 
125
125
  return scrubbed_logs
126
126
 
127
+ # Protocol Compliance Alias
128
+ scrub_pii = scrub_logs
129
+
130
+
131
+ def scrubbed_print(message: str, **kwargs) -> None:
132
+ """
133
+ Protocol-compliant print wrapper that scrubs PII before output.
134
+ Use this instead of raw print() throughout the SDK.
135
+ """
136
+ print(scrub_pii(str(message)), **kwargs)
137
+
127
138
 
128
139
  if __name__ == "__main__":
129
140
  # Example Usage
xenfra_sdk/protocol.py ADDED
@@ -0,0 +1,38 @@
1
+ """
2
+ Xenfra Protocol Registry - Centralized constants for compliance with XENFRA_PROTOCOL.md.
3
+ Eliminates hardcoded strings across SDK and Services.
4
+ """
5
+
6
+ from typing import Final
7
+
8
+
9
+ class ProtocolRegistry:
10
+ """
11
+ Structured registry for all strings and constants used in the Xenfra stack.
12
+ Satisfies Anti-Pattern #4 (No Hardcoded Constants).
13
+ """
14
+
15
+ # Log Prefixes
16
+ LOG_PREFIX_INFRA_ENGINE: Final[str] = "[InfraEngine]"
17
+ LOG_PREFIX_ZEN_MODE: Final[str] = "[ZenMode]"
18
+ LOG_PREFIX_AUTO_MIGRATION: Final[str] = "[AutoMigration]"
19
+
20
+ # Rate Limiting
21
+ RATE_LIMIT_USER_KEY: Final[str] = "user:{}" # f-string template
22
+
23
+ # Error Messages
24
+ MIGRATION_FAILED_MSG: Final[str] = "Auto-migration failed"
25
+ IDENTITY_SYNC_FAILED_MSG: Final[str] = "User identity cannot be synced"
26
+
27
+ # Header Names
28
+ HEADER_USER_ID: Final[str] = "X-User-ID"
29
+ HEADER_USER_EMAIL: Final[str] = "X-User-Email"
30
+ HEADER_REQUEST_ID: Final[str] = "X-Request-ID"
31
+
32
+ # JWT Claims
33
+ JWT_ISSUER: Final[str] = "xenfra-sso"
34
+ JWT_AUDIENCE: Final[str] = "xenfra-api"
35
+
36
+ # Default Deployment Values
37
+ DEFAULT_DOCKER_IMAGE: Final[str] = "ubuntu-22-04-x64"
38
+ DEFAULT_PORT: Final[int] = 8000