syntaxmatrix-core 1.0.8__tar.gz → 1.0.9__tar.gz

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 (106) hide show
  1. {syntaxmatrix_core-1.0.8/syntaxmatrix_core.egg-info → syntaxmatrix_core-1.0.9}/PKG-INFO +1 -1
  2. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/setup.py +1 -1
  3. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/agentic/agents_orchestrer.py +5 -4
  4. syntaxmatrix_core-1.0.9/syntaxmatrix/premium/__init__.py +18 -0
  5. syntaxmatrix_core-1.0.9/syntaxmatrix/premium/catalogue/__init__.py +267 -0
  6. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/premium/gate.py +23 -39
  7. syntaxmatrix_core-1.0.9/syntaxmatrix/premium/runtime_paths.py +109 -0
  8. syntaxmatrix_core-1.0.9/syntaxmatrix/premium/state.py +867 -0
  9. syntaxmatrix_core-1.0.9/syntaxmatrix/premium/verify.py +357 -0
  10. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/settings/model_map.py +149 -16
  11. syntaxmatrix_core-1.0.9/syntaxmatrix/settings/prompts.py +3656 -0
  12. syntaxmatrix_core-1.0.9/syntaxmatrix/static/icons/bot_icon.png +0 -0
  13. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9/syntaxmatrix_core.egg-info}/PKG-INFO +1 -1
  14. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix_core.egg-info/SOURCES.txt +5 -0
  15. syntaxmatrix_core-1.0.8/syntaxmatrix/premium/__init__.py +0 -10
  16. syntaxmatrix_core-1.0.8/syntaxmatrix/settings/prompts.py +0 -1755
  17. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/MANIFEST.in +0 -0
  18. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/README.md +0 -0
  19. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/pyproject.toml +0 -0
  20. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/setup.cfg +0 -0
  21. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/__init__.py +0 -0
  22. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/agentic/__init__.py +0 -0
  23. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/agentic/agent_tools.py +0 -0
  24. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/agentic/agents.py +0 -0
  25. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/agentic/code_tools_registry.py +0 -0
  26. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/agentic/model_templates.py +0 -0
  27. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/auth.py +0 -0
  28. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/bootstrap.py +0 -0
  29. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/client_docs.py +0 -0
  30. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/commentary.py +0 -0
  31. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/core.py +0 -0
  32. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/dataset_preprocessing.py +0 -0
  33. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/db.py +0 -0
  34. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/db_backends/__init__.py +0 -0
  35. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/db_backends/postgres_backend.py +0 -0
  36. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/db_backends/sqlite_backend.py +0 -0
  37. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/db_contract.py +0 -0
  38. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/display_html.py +0 -0
  39. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/emailer.py +0 -0
  40. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/file_processor.py +0 -0
  41. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/gpt_models_latest.py +0 -0
  42. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/history_store.py +0 -0
  43. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/kernel_manager.py +0 -0
  44. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/llm_store.py +0 -0
  45. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/media/__init__.py +0 -0
  46. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/media/media_pixabay.py +0 -0
  47. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/models.py +0 -0
  48. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/page_builder_defaults.py +0 -0
  49. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/page_builder_generation.py +0 -0
  50. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/page_layout_contract.py +0 -0
  51. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/page_patch_publish.py +0 -0
  52. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/plottings.py +0 -0
  53. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/plugin_manager.py +0 -0
  54. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/preface.py +0 -0
  55. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/profiles.py +0 -0
  56. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/project_root.py +0 -0
  57. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/routes.py +0 -0
  58. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/selftest_page_templates.py +0 -0
  59. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/session.py +0 -0
  60. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/settings/__init__.py +0 -0
  61. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/settings/client_items.py +0 -0
  62. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/settings/logging.py +0 -0
  63. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/settings/string_navbar.py +0 -0
  64. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/smiv.py +0 -0
  65. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/smpv.py +0 -0
  66. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/static/assets/hero-default.svg +0 -0
  67. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/static/css/style.css +0 -0
  68. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/static/docs.md +0 -0
  69. /syntaxmatrix_core-1.0.8/syntaxmatrix/static/icons/bot_icon.png → /syntaxmatrix_core-1.0.9/syntaxmatrix/static/icons/bot_icon2.png +0 -0
  70. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/static/icons/favicon.png +0 -0
  71. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/static/icons/logo.png +0 -0
  72. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/static/icons/logo2.png +0 -0
  73. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/static/icons/svg_497526.svg +0 -0
  74. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/static/icons/svg_497528.svg +0 -0
  75. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/static/js/chat.js +0 -0
  76. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/static/js/sidebar.js +0 -0
  77. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/static/js/widgets.js +0 -0
  78. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/templates/admin_billing.html +0 -0
  79. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/templates/admin_branding.html +0 -0
  80. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/templates/admin_features.html +0 -0
  81. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/templates/admin_secretes.html +0 -0
  82. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/templates/change_password.html +0 -0
  83. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/templates/code_cell.html +0 -0
  84. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/templates/dashboard.html +0 -0
  85. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/templates/dataset_resize.html +0 -0
  86. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/templates/docs.html +0 -0
  87. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/templates/edit_page.html +0 -0
  88. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/templates/error.html +0 -0
  89. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/templates/login.html +0 -0
  90. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/templates/register.html +0 -0
  91. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/themes.py +0 -0
  92. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/ui_modes.py +0 -0
  93. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/utils.py +0 -0
  94. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/vector_db.py +0 -0
  95. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/vectordb/__init__.py +0 -0
  96. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/vectordb/adapters/__init__.py +0 -0
  97. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/vectordb/adapters/milvus_adapter.py +0 -0
  98. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/vectordb/adapters/pgvector_adapter.py +0 -0
  99. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/vectordb/adapters/sqlite_adapter.py +0 -0
  100. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/vectordb/base.py +0 -0
  101. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/vectordb/registry.py +0 -0
  102. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/vectorizer.py +0 -0
  103. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix/workspace_db.py +0 -0
  104. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix_core.egg-info/dependency_links.txt +0 -0
  105. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix_core.egg-info/requires.txt +0 -0
  106. {syntaxmatrix_core-1.0.8 → syntaxmatrix_core-1.0.9}/syntaxmatrix_core.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syntaxmatrix-core
3
- Version: 1.0.8
3
+ Version: 1.0.9
4
4
  Summary: SyntaxMatrix: A Framework for building owned AI Platform.
5
5
  Author: Bob Nti
6
6
  Author-email: info@syntaxmatrix.net
@@ -8,7 +8,7 @@ with open(os.path.join(this_directory, "README.md"), encoding="utf-8") as f:
8
8
 
9
9
  setup(
10
10
  name="syntaxmatrix-core",
11
- version="1.0.8",
11
+ version="1.0.9",
12
12
  license="MIT",
13
13
  classifiers=[
14
14
  "Operating System :: OS Independent",
@@ -92,10 +92,11 @@ class OrchestrateMLSystem:
92
92
  def reasoning_and_verbosity():
93
93
  reasoning_effort, verbosity = "medium", "medium"
94
94
  if "mini" not in model:
95
- if model == "gpt-5-nano":
95
+ if "nano" in model:
96
96
  reasoning_effort = "low"
97
- else: reasoning_effort, verbosity = "high", "high"
98
-
97
+ elif "high" in model:
98
+ reasoning_effort, verbosity = "high", "high"
99
+
99
100
  return reasoning_effort, verbosity
100
101
 
101
102
  reasoning, verbosity = reasoning_and_verbosity()
@@ -324,7 +325,7 @@ class OrchestrateMLSystem:
324
325
  if not coder_profile:
325
326
  return "Error!: Set an appropriate coder profile"
326
327
 
327
- thought, raw_code, usage = self._generate_ml_response(coder_profile ,system_prompt, user_prompt)
328
+ thought, raw_code, usage = self._generate_ml_response(coder_profile,system_prompt, user_prompt)
328
329
 
329
330
  # Robustly strip any potential markdown formatting
330
331
  code = re.sub(r'```python|```', '', raw_code).strip()
@@ -0,0 +1,18 @@
1
+ """Premium support.
2
+
3
+ This package contains runtime plumbing for premium features (entitlements +
4
+ plugin loading).
5
+
6
+ Key idea:
7
+ - FeatureGate reads a single entitlement dict from env/db/licence.json
8
+ - ensure_premium_state() resolves trial + plan entitlements and writes them
9
+ into app_settings so the rest of the app can safely gate routes + navbar.
10
+
11
+ Premium implementations (e.g. non-SQLite DB backends) should live in separate,
12
+ private distributions.
13
+ """
14
+
15
+ from .gate import FeatureGate
16
+ from .state import ensure_premium_state, PremiumState, clear_premium_state_cache
17
+
18
+ __all__ = ["FeatureGate", "ensure_premium_state", "PremiumState", "clear_premium_state_cache"]
@@ -0,0 +1,267 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from typing import Any, Dict, Optional, Tuple
6
+ from importlib import resources as _resources
7
+
8
+
9
+ def _safe_json_loads(raw: str, default: Any) -> Any:
10
+ try:
11
+ return json.loads(raw)
12
+ except Exception:
13
+ return default
14
+
15
+
16
+ def _read_json(path: str) -> Dict[str, Any]:
17
+ try:
18
+ with open(path, "r", encoding="utf-8") as f:
19
+ data = json.load(f)
20
+ return data if isinstance(data, dict) else {}
21
+ except Exception:
22
+ return {}
23
+ ##########################################################
24
+ def _read_json_resource(name: str) -> Dict[str, Any]:
25
+ """Read JSON packaged with the module (works even when running from a wheel)."""
26
+ try:
27
+ raw = _resources.files(__package__).joinpath(name).read_text(encoding="utf-8")
28
+ data = _safe_json_loads(raw, {})
29
+ return data if isinstance(data, dict) else {}
30
+ except Exception:
31
+ return {}
32
+
33
+
34
+ _FALLBACK_INDEX: Dict[str, Any] = {
35
+ "default": "2026-01-19",
36
+ "versions": {"2026-01-19": "catalogue.v1.json"},
37
+ }
38
+
39
+ _FALLBACK_CATALOGUE_V1: Dict[str, Any] = {
40
+ "schema": "smx-premium-catalogue/v1",
41
+ "version": "2026-01-19",
42
+ "currency": "EUR",
43
+ "plans": {
44
+ "trial": {
45
+ "display_name": "Trial",
46
+ "entitlements": {
47
+ "docs": True,
48
+ "ml_lab": True,
49
+ "registration": True,
50
+ "theme_toggle": True,
51
+ "branding_controls": True,
52
+ "plugins": True,
53
+ "premium_db_backends": True,
54
+ "audit_export": True,
55
+ "max_users": 9999,
56
+ "max_pages": 9999,
57
+ "max_upload_mb": 9999,
58
+ "max_pdf_pages_per_doc": 999999,
59
+ "max_vector_records": 999999999,
60
+ },
61
+ },
62
+ "free": {
63
+ "display_name": "Free",
64
+ "entitlements": {
65
+ "docs": False,
66
+ "ml_lab": False,
67
+ "registration": False,
68
+ "theme_toggle": False,
69
+ "branding_controls": False,
70
+ "plugins": False,
71
+ "premium_db_backends": False,
72
+ "audit_export": False,
73
+ "max_users": 1,
74
+ "max_pages": 3,
75
+ "max_upload_mb": 5,
76
+ "max_pdf_pages_per_doc": 20,
77
+ "max_vector_records": 1500,
78
+ },
79
+ },
80
+ "pro": {
81
+ "display_name": "Pro",
82
+ "entitlements": {
83
+ "docs": True,
84
+ "ml_lab": True,
85
+ "registration": True,
86
+ "theme_toggle": True,
87
+ "branding_controls": True,
88
+ "plugins": True,
89
+ "premium_db_backends": False,
90
+ "audit_export": True,
91
+ "max_users": 10,
92
+ "max_pages": 50,
93
+ "max_upload_mb": 200,
94
+ "max_pdf_pages_per_doc": 300,
95
+ "max_vector_records": 50000,
96
+ },
97
+ },
98
+ "business": {
99
+ "display_name": "Business",
100
+ "entitlements": {
101
+ "docs": True,
102
+ "ml_lab": True,
103
+ "registration": True,
104
+ "theme_toggle": True,
105
+ "branding_controls": True,
106
+ "plugins": True,
107
+ "premium_db_backends": True,
108
+ "audit_export": True,
109
+ "max_users": 25,
110
+ "max_pages": 150,
111
+ "max_upload_mb": 500,
112
+ "max_pdf_pages_per_doc": 750,
113
+ "max_vector_records": 200000,
114
+ },
115
+ },
116
+ "enterprise": {
117
+ "display_name": "Enterprise",
118
+ "entitlements": {
119
+ "docs": True,
120
+ "ml_lab": True,
121
+ "registration": True,
122
+ "theme_toggle": True,
123
+ "branding_controls": True,
124
+ "plugins": True,
125
+ "premium_db_backends": True,
126
+ "audit_export": True,
127
+ "max_users": 200,
128
+ "max_pages": 500,
129
+ "max_upload_mb": 2000,
130
+ "max_pdf_pages_per_doc": 2000,
131
+ "max_vector_records": 500000,
132
+ },
133
+ },
134
+ },
135
+ "addons": {},
136
+ "ui": {
137
+ "labels": {
138
+ "docs": {"name": "Docs", "category": "Features", "type": "bool"},
139
+ "ml_lab": {"name": "ML Lab", "category": "Features", "type": "bool"},
140
+ "registration": {"name": "Registration", "category": "Features", "type": "bool"},
141
+ "theme_toggle": {"name": "Theme toggle", "category": "Features", "type": "bool"},
142
+ "branding_controls": {"name": "Branding controls", "category": "Features", "type": "bool"},
143
+ "plugins": {"name": "Plugins", "category": "Features", "type": "bool"},
144
+ "premium_db_backends": {"name": "Premium DB backends", "category": "Features", "type": "bool"},
145
+ "audit_export": {"name": "Audit/export", "category": "Features", "type": "bool"},
146
+ "max_users": {"name": "Max users", "category": "Limits", "type": "int", "unit": "users"},
147
+ "max_pages": {"name": "Max pages", "category": "Limits", "type": "int", "unit": "pages"},
148
+ "max_upload_mb": {"name": "Max upload size", "category": "Limits", "type": "int", "unit": "MB"},
149
+ "max_pdf_pages_per_doc": {
150
+ "name": "Max PDF pages per document",
151
+ "category": "Limits",
152
+ "type": "int",
153
+ "unit": "pages",
154
+ },
155
+ "max_vector_records": {"name": "Max vector records", "category": "Limits", "type": "int", "unit": "records"},
156
+ }
157
+ },
158
+ }
159
+ #########################################
160
+
161
+ def _catalogue_dir() -> str:
162
+ return os.path.join(os.path.dirname(__file__))
163
+
164
+
165
+ def load_index() -> Dict[str, Any]:
166
+ """Load catalogue index.json (safe, returns fallback on failure)."""
167
+ data = _read_json(os.path.join(_catalogue_dir(), "index.json"))
168
+ if data:
169
+ return data
170
+ data = _read_json_resource("index.json")
171
+ return data if data else dict(_FALLBACK_INDEX)
172
+
173
+
174
+ def load_catalogue(version: Optional[str] = None) -> Tuple[Dict[str, Any], str]:
175
+ """Load a catalogue for the requested version.
176
+
177
+ Returns: (catalogue_dict, resolved_version)
178
+ """
179
+ idx = load_index()
180
+ resolved = (version or "").strip() or str(idx.get("default") or "").strip()
181
+ versions = idx.get("versions") if isinstance(idx.get("versions"), dict) else {}
182
+ fname = versions.get(resolved) if isinstance(versions, dict) else None
183
+
184
+ if not fname:
185
+ # Fall back to default file name if index is missing or version unknown.
186
+ fname = "catalogue.v1.json"
187
+
188
+ cat = _read_json(os.path.join(_catalogue_dir(), str(fname)))
189
+ if not cat:
190
+ cat = _read_json_resource(str(fname))
191
+ if not cat:
192
+ cat = dict(_FALLBACK_CATALOGUE_V1)
193
+
194
+ # If the loaded file advertises a version, prefer that.
195
+ advertised = str(cat.get("version") or "").strip()
196
+ if advertised:
197
+ resolved = advertised
198
+ return cat, resolved
199
+
200
+
201
+ def ui_labels(version: Optional[str] = None) -> Dict[str, Any]:
202
+ cat, _ = load_catalogue(version)
203
+ ui = cat.get("ui") if isinstance(cat.get("ui"), dict) else {}
204
+ labels = ui.get("labels") if isinstance(ui.get("labels"), dict) else {}
205
+ return labels if isinstance(labels, dict) else {}
206
+
207
+
208
+ def resolve_entitlements(
209
+ *,
210
+ plan_id: str,
211
+ version: Optional[str] = None,
212
+ addons: Optional[list] = None,
213
+ custom_overrides: Optional[Dict[str, Any]] = None,
214
+ ) -> Dict[str, Any]:
215
+ """Resolve final entitlements from catalogue.
216
+
217
+ This keeps entitlements data-driven:
218
+ - base entitlements come from the plan in the catalogue
219
+ - optional add-ons apply deltas/overrides
220
+ - optional custom_overrides apply last
221
+ """
222
+ pid = (plan_id or "free").strip().lower() or "free"
223
+ cat, resolved_version = load_catalogue(version)
224
+
225
+ plans = cat.get("plans") if isinstance(cat.get("plans"), dict) else {}
226
+ p = plans.get(pid) if isinstance(plans, dict) else None
227
+ base = {}
228
+ if isinstance(p, dict) and isinstance(p.get("entitlements"), dict):
229
+ base = dict(p.get("entitlements") or {})
230
+
231
+ # Always stamp plan + version for UI/debugging.
232
+ base["plan"] = pid
233
+ base["entitlement_version"] = resolved_version
234
+
235
+ # Apply add-ons.
236
+ addons_list = addons if isinstance(addons, list) else []
237
+ if addons_list:
238
+ addons_def = cat.get("addons") if isinstance(cat.get("addons"), dict) else {}
239
+ for addon_id in addons_list:
240
+ aid = str(addon_id or "").strip()
241
+ if not aid:
242
+ continue
243
+ a = addons_def.get(aid) if isinstance(addons_def, dict) else None
244
+ if not isinstance(a, dict):
245
+ continue
246
+ delta = a.get("entitlements_delta") if isinstance(a.get("entitlements_delta"), dict) else {}
247
+ for k, v in (delta or {}).items():
248
+ # Support "+N" increment for numeric limits.
249
+ if isinstance(v, str) and v.strip().startswith("+"):
250
+ try:
251
+ inc = int(v.strip()[1:])
252
+ cur = int(base.get(k) or 0)
253
+ base[k] = cur + inc
254
+ continue
255
+ except Exception:
256
+ pass
257
+ base[k] = v
258
+ base["addons"] = addons_list
259
+
260
+ # Apply overrides last.
261
+ if isinstance(custom_overrides, dict) and custom_overrides:
262
+ for k, v in custom_overrides.items():
263
+ if k in ("sig",):
264
+ continue
265
+ base[k] = v
266
+
267
+ return base
@@ -3,8 +3,12 @@ from __future__ import annotations
3
3
  import json
4
4
  import os
5
5
  from dataclasses import dataclass
6
+ from datetime import datetime
6
7
  from typing import Any, Dict, Optional
7
8
 
9
+ from .verify import verify_licence_payload
10
+ from .runtime_paths import runtime_lease_path
11
+
8
12
 
9
13
  def _safe_json_loads(raw: str, *, default: Any) -> Any:
10
14
  try:
@@ -15,24 +19,14 @@ def _safe_json_loads(raw: str, *, default: Any) -> Any:
15
19
 
16
20
  @dataclass
17
21
  class GateSources:
18
- """Where the gate reads entitlements from.
19
-
20
- Precedence (highest first):
21
- 1) env_json
22
- 2) db_setting_key
23
- 3) licence_file
24
- """
25
-
26
- env_json: str = "SMX_PREMIUM_ENTITLEMENTS"
27
- db_setting_key: str = "premium.entitlements"
28
- licence_file_relpath: str = os.path.join("premium", "licence.json")
22
+
23
+ lease_file_relpath: str = os.path.join("premium", "runtime_lease.json")
29
24
 
30
25
 
31
26
  class FeatureGate:
32
27
  """Runtime entitlement checks for premium features.
33
-
34
- This is intentionally small, dependency-free, and safe:
35
- - If anything fails, it returns 'not entitled' rather than crashing the app.
28
+ The gate now trusts only a signed runtime lease that is bound to the
29
+ instance and current runtime fingerprint.
36
30
  """
37
31
 
38
32
  def __init__(
@@ -47,45 +41,36 @@ class FeatureGate:
47
41
  self._sources = sources or GateSources()
48
42
  self._cache: Optional[Dict[str, Any]] = None
49
43
 
50
- def _load_from_env(self) -> Optional[Dict[str, Any]]:
51
- raw = os.environ.get(self._sources.env_json)
52
- if not raw:
44
+ def _normalise(self, data: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
45
+ if not isinstance(data, dict):
53
46
  return None
54
- data = _safe_json_loads(raw, default=None)
55
- return data if isinstance(data, dict) else None
56
-
57
- def _load_from_db(self) -> Optional[Dict[str, Any]]:
58
- if not self._db:
59
- return None
60
- get_setting = getattr(self._db, "get_setting", None)
61
- if not callable(get_setting):
62
- return None
63
-
64
- raw = get_setting(self._sources.db_setting_key, "{}")
65
- data = _safe_json_loads(str(raw or "{}"), default={})
66
- return data if isinstance(data, dict) else None
47
+ if isinstance(data.get("entitlements"), dict):
48
+ ent = dict(data.get("entitlements") or {})
49
+ if data.get("plan") and not ent.get("plan"):
50
+ ent["plan"] = data.get("plan")
51
+ return ent
52
+ return data
67
53
 
68
54
  def _load_from_file(self) -> Optional[Dict[str, Any]]:
69
- p = os.path.join(self._client_dir, self._sources.licence_file_relpath)
55
+ p = runtime_lease_path(self._client_dir)
70
56
  if not os.path.exists(p):
71
57
  return None
72
58
  try:
73
59
  with open(p, "r", encoding="utf-8") as f:
74
60
  data = json.load(f)
75
- return data if isinstance(data, dict) else None
76
61
  except Exception:
77
62
  return None
78
63
 
64
+ verified = verify_licence_payload(data, now=datetime.utcnow(), allow_unsigned=False)
65
+ if not verified.ok:
66
+ return None
67
+ return self._normalise(verified.entitlements)
68
+
79
69
  def entitlements(self, *, refresh: bool = False) -> Dict[str, Any]:
80
- """Returns entitlement dict (possibly empty)."""
81
70
  if self._cache is not None and not refresh:
82
71
  return self._cache
83
72
 
84
- ent = self._load_from_env()
85
- if ent is None:
86
- ent = self._load_from_db()
87
- if ent is None:
88
- ent = self._load_from_file()
73
+ ent = self._load_from_file()
89
74
  if ent is None:
90
75
  ent = {}
91
76
 
@@ -93,7 +78,6 @@ class FeatureGate:
93
78
  return ent
94
79
 
95
80
  def enabled(self, key: str) -> bool:
96
- """True if entitlement exists and is truthy."""
97
81
  key = (key or "").strip()
98
82
  if not key:
99
83
  return False
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import os
5
+ import tempfile
6
+ from typing import Dict
7
+
8
+
9
+ _RUNTIME_ENV_NAMES = ("SMX_PREMIUM_RUNTIME_DIR", "SMX_RUNTIME_DIR")
10
+ _AUDIT_ENV_NAME = "SMX_PREMIUM_AUDIT_DIR"
11
+
12
+
13
+ def _clean(value: str) -> str:
14
+ return str(value or "").strip()
15
+
16
+
17
+ def _ensure_dir(path: str) -> str:
18
+ if not path:
19
+ return path
20
+ os.makedirs(path, exist_ok=True)
21
+ return path
22
+
23
+
24
+ def _stable_runtime_bucket_name(client_dir: str) -> str:
25
+ raw = _clean(client_dir) or "default"
26
+ digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]
27
+ return f"smx_runtime_{digest}"
28
+
29
+
30
+ def _default_runtime_root(client_dir: str) -> str:
31
+ base_tmp = tempfile.gettempdir()
32
+ leaf = _stable_runtime_bucket_name(client_dir)
33
+ return os.path.join(base_tmp, leaf)
34
+
35
+
36
+ def resolve_runtime_root(client_dir: str, *, ensure: bool = True) -> str:
37
+ root = ""
38
+ for env_name in _RUNTIME_ENV_NAMES:
39
+ candidate = _clean(os.environ.get(env_name))
40
+ if candidate:
41
+ root = candidate
42
+ break
43
+
44
+ if not root:
45
+ root = _default_runtime_root(client_dir)
46
+
47
+ if ensure and root:
48
+ try:
49
+ _ensure_dir(root)
50
+ except Exception:
51
+ pass
52
+ return root
53
+
54
+
55
+ def resolve_runtime_premium_dir(client_dir: str, *, ensure: bool = True) -> str:
56
+ root = resolve_runtime_root(client_dir, ensure=ensure)
57
+ path = os.path.join(root, "premium") if root else os.path.join("premium")
58
+ if ensure:
59
+ try:
60
+ _ensure_dir(path)
61
+ except Exception:
62
+ pass
63
+ return path
64
+
65
+
66
+ def resolve_audit_root(client_dir: str, *, ensure: bool = True) -> str:
67
+ root = _clean(os.environ.get(_AUDIT_ENV_NAME)) or _clean(client_dir)
68
+ if ensure and root:
69
+ try:
70
+ _ensure_dir(root)
71
+ except Exception:
72
+ pass
73
+ return root
74
+
75
+
76
+ def resolve_audit_premium_dir(client_dir: str, *, ensure: bool = True) -> str:
77
+ root = resolve_audit_root(client_dir, ensure=ensure)
78
+ path = os.path.join(root, "premium") if root else os.path.join("premium")
79
+ if ensure:
80
+ try:
81
+ _ensure_dir(path)
82
+ except Exception:
83
+ pass
84
+ return path
85
+
86
+
87
+ def runtime_lease_path(client_dir: str) -> str:
88
+ return os.path.join(resolve_runtime_premium_dir(client_dir), "runtime_lease.json")
89
+
90
+
91
+ def tamper_queue_path(client_dir: str) -> str:
92
+ return os.path.join(resolve_runtime_premium_dir(client_dir), "tamper_queue.jsonl")
93
+
94
+
95
+ def tamper_audit_path(client_dir: str) -> str:
96
+ return os.path.join(resolve_audit_premium_dir(client_dir), "tamper_audit.log")
97
+
98
+
99
+ def describe_premium_paths(client_dir: str) -> Dict[str, str]:
100
+ runtime_root = resolve_runtime_root(client_dir, ensure=False)
101
+ audit_root = resolve_audit_root(client_dir, ensure=False)
102
+ return {
103
+ "client_dir": _clean(client_dir),
104
+ "runtime_root": runtime_root,
105
+ "runtime_lease_path": os.path.join(runtime_root, "premium", "runtime_lease.json") if runtime_root else "",
106
+ "tamper_queue_path": os.path.join(runtime_root, "premium", "tamper_queue.jsonl") if runtime_root else "",
107
+ "audit_root": audit_root,
108
+ "tamper_audit_path": os.path.join(audit_root, "premium", "tamper_audit.log") if audit_root else "",
109
+ }