superlocalmemory 3.7.7 → 3.8.0

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 (262) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +85 -0
  4. package/README.md +199 -29
  5. package/package.json +4 -2
  6. package/plugin/.claude-plugin/plugin.json +2 -2
  7. package/plugin/CLAUDE.md +8 -8
  8. package/plugin/agents/slm-governance-advisor.md +80 -0
  9. package/plugin/agents/slm-loop-runner.md +71 -0
  10. package/plugin/agents/slm-memory-advisor.md +10 -5
  11. package/plugin/agents/slm-optimize-advisor.md +9 -3
  12. package/plugin/commands/slm-loop.md +31 -0
  13. package/plugin/hooks/hooks.json +79 -0
  14. package/plugin/requirements.txt +1 -1
  15. package/plugin/scripts/slm-launch +46 -7
  16. package/plugin/settings.json +9 -0
  17. package/plugin/skills/slm-cache/SKILL.md +9 -1
  18. package/plugin/skills/slm-compress/SKILL.md +8 -1
  19. package/plugin/skills/slm-governance/SKILL.md +248 -0
  20. package/plugin/skills/slm-graph/SKILL.md +17 -3
  21. package/plugin/skills/slm-loop/SKILL.md +99 -0
  22. package/plugin/skills/slm-mesh/SKILL.md +282 -0
  23. package/plugin/skills/slm-profile/SKILL.md +148 -0
  24. package/plugin/skills/slm-recall/SKILL.md +46 -10
  25. package/plugin/skills/slm-remember/SKILL.md +48 -1
  26. package/plugin/skills/slm-scope/SKILL.md +176 -0
  27. package/plugin/skills/slm-session/SKILL.md +24 -1
  28. package/plugin/skills/slm-status/SKILL.md +18 -1
  29. package/plugin-src/agents/slm-governance-advisor.md +80 -0
  30. package/plugin-src/agents/slm-loop-runner.md +71 -0
  31. package/plugin-src/agents/slm-memory-advisor.md +10 -5
  32. package/plugin-src/agents/slm-optimize-advisor.md +9 -3
  33. package/plugin-src/commands/slm-loop.md +31 -0
  34. package/plugin-src/hooks/hooks.json +79 -0
  35. package/plugin-src/manifest.json +7 -2
  36. package/plugin-src/requirements.txt +1 -1
  37. package/plugin-src/rules/AGENTS.md +57 -18
  38. package/plugin-src/rules/CLAUDE.md.fragment +8 -8
  39. package/plugin-src/scripts/slm-launch +46 -7
  40. package/plugin-src/settings.json +9 -0
  41. package/plugin-src/skills/slm-cache/SKILL.md +9 -1
  42. package/plugin-src/skills/slm-compress/SKILL.md +8 -1
  43. package/plugin-src/skills/slm-governance/SKILL.md +248 -0
  44. package/plugin-src/skills/slm-graph/SKILL.md +17 -3
  45. package/plugin-src/skills/slm-loop/SKILL.md +99 -0
  46. package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
  47. package/plugin-src/skills/slm-profile/SKILL.md +148 -0
  48. package/plugin-src/skills/slm-recall/SKILL.md +46 -10
  49. package/plugin-src/skills/slm-remember/SKILL.md +48 -1
  50. package/plugin-src/skills/slm-scope/SKILL.md +176 -0
  51. package/plugin-src/skills/slm-session/SKILL.md +24 -1
  52. package/plugin-src/skills/slm-status/SKILL.md +18 -1
  53. package/pyproject.toml +1 -2
  54. package/scripts/postinstall/validation.js +2 -0
  55. package/scripts/postinstall-interactive.js +74 -2
  56. package/src/superlocalmemory/__init__.py +1 -1
  57. package/src/superlocalmemory/access/__init__.py +3 -0
  58. package/src/superlocalmemory/access/rbac.py +477 -0
  59. package/src/superlocalmemory/cli/commands.py +96 -12
  60. package/src/superlocalmemory/cli/compress_cmd.py +17 -7
  61. package/src/superlocalmemory/cli/loop_cmd.py +192 -0
  62. package/src/superlocalmemory/cli/main.py +39 -4
  63. package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
  64. package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
  65. package/src/superlocalmemory/cli/pending_store.py +49 -13
  66. package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
  67. package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
  68. package/src/superlocalmemory/cli/setup_wizard.py +22 -13
  69. package/src/superlocalmemory/compliance/audit.py +6 -0
  70. package/src/superlocalmemory/compliance/gdpr.py +128 -138
  71. package/src/superlocalmemory/compliance/retention.py +176 -45
  72. package/src/superlocalmemory/core/backend_orchestrator.py +5 -43
  73. package/src/superlocalmemory/core/community_summary.py +267 -0
  74. package/src/superlocalmemory/core/config.py +216 -3
  75. package/src/superlocalmemory/core/consolidation_engine.py +95 -22
  76. package/src/superlocalmemory/core/context_cache.py +61 -18
  77. package/src/superlocalmemory/core/embedding_worker.py +17 -2
  78. package/src/superlocalmemory/core/embeddings.py +12 -1
  79. package/src/superlocalmemory/core/engine.py +17 -1
  80. package/src/superlocalmemory/core/engine_ingestion.py +29 -0
  81. package/src/superlocalmemory/core/engine_wiring.py +13 -0
  82. package/src/superlocalmemory/core/entity_community.py +178 -0
  83. package/src/superlocalmemory/core/graph_analyzer.py +39 -2
  84. package/src/superlocalmemory/core/graph_pruner.py +13 -8
  85. package/src/superlocalmemory/core/key_expander.py +138 -0
  86. package/src/superlocalmemory/core/maintenance.py +23 -0
  87. package/src/superlocalmemory/core/modes.py +1 -1
  88. package/src/superlocalmemory/core/mutations.py +2 -2
  89. package/src/superlocalmemory/core/pii.py +105 -0
  90. package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
  91. package/src/superlocalmemory/core/recall_pipeline.py +2 -0
  92. package/src/superlocalmemory/core/recall_worker.py +20 -6
  93. package/src/superlocalmemory/core/scale_engine.py +60 -1
  94. package/src/superlocalmemory/core/security_primitives.py +40 -2
  95. package/src/superlocalmemory/core/store_pipeline.py +35 -11
  96. package/src/superlocalmemory/core/worker_pool.py +21 -6
  97. package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
  98. package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
  99. package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
  100. package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
  101. package/src/superlocalmemory/evolution/evolution_store.py +122 -45
  102. package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
  103. package/src/superlocalmemory/evolution/model_selection.py +160 -0
  104. package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
  105. package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
  106. package/src/superlocalmemory/evolution/triggers.py +22 -13
  107. package/src/superlocalmemory/graph/cozo_backend.py +43 -20
  108. package/src/superlocalmemory/hooks/adapter_base.py +5 -1
  109. package/src/superlocalmemory/hooks/auto_recall.py +13 -1
  110. package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
  111. package/src/superlocalmemory/hooks/codex_assets.py +64 -5
  112. package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
  113. package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
  114. package/src/superlocalmemory/hooks/portable_kit.py +114 -1
  115. package/src/superlocalmemory/infra/auth_middleware.py +28 -0
  116. package/src/superlocalmemory/infra/backup.py +12 -1
  117. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  118. package/src/superlocalmemory/infra/data_root.py +43 -4
  119. package/src/superlocalmemory/infra/event_bus.py +107 -24
  120. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  121. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  122. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  123. package/src/superlocalmemory/learning/cross_project.py +28 -19
  124. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  125. package/src/superlocalmemory/loops/__init__.py +56 -0
  126. package/src/superlocalmemory/loops/budget.py +58 -0
  127. package/src/superlocalmemory/loops/engine.py +164 -0
  128. package/src/superlocalmemory/loops/ledger.py +243 -0
  129. package/src/superlocalmemory/loops/models.py +152 -0
  130. package/src/superlocalmemory/loops/rules.py +52 -0
  131. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  132. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  133. package/src/superlocalmemory/mcp/profiles.py +103 -0
  134. package/src/superlocalmemory/mcp/server.py +21 -49
  135. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  136. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  137. package/src/superlocalmemory/mcp/tools_core.py +50 -5
  138. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  139. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  140. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  141. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  142. package/src/superlocalmemory/mesh/broker.py +237 -129
  143. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  144. package/src/superlocalmemory/optimize/NOTICE +1 -6
  145. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  146. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  147. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  148. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  149. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  150. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  151. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  152. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  153. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  154. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  155. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  156. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  157. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  158. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  159. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  160. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  161. package/src/superlocalmemory/retrieval/engine.py +168 -26
  162. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  163. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  164. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  165. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  166. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  167. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  168. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  169. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  170. package/src/superlocalmemory/server/api.py +21 -4
  171. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  172. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  173. package/src/superlocalmemory/server/recall_health.py +24 -3
  174. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  175. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  176. package/src/superlocalmemory/server/routes/agents.py +128 -38
  177. package/src/superlocalmemory/server/routes/backup.py +34 -10
  178. package/src/superlocalmemory/server/routes/behavioral.py +13 -12
  179. package/src/superlocalmemory/server/routes/brain.py +21 -5
  180. package/src/superlocalmemory/server/routes/chat.py +72 -16
  181. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  182. package/src/superlocalmemory/server/routes/config_api.py +436 -0
  183. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  184. package/src/superlocalmemory/server/routes/entity.py +9 -4
  185. package/src/superlocalmemory/server/routes/events.py +24 -8
  186. package/src/superlocalmemory/server/routes/evolution.py +135 -17
  187. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  188. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  189. package/src/superlocalmemory/server/routes/insights.py +3 -3
  190. package/src/superlocalmemory/server/routes/learning.py +14 -14
  191. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  192. package/src/superlocalmemory/server/routes/memories.py +221 -49
  193. package/src/superlocalmemory/server/routes/mesh.py +95 -15
  194. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  195. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  196. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  197. package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
  198. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  199. package/src/superlocalmemory/server/routes/stats.py +13 -6
  200. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  201. package/src/superlocalmemory/server/routes/v3_api.py +194 -81
  202. package/src/superlocalmemory/server/routes/ws.py +5 -2
  203. package/src/superlocalmemory/server/security_middleware.py +12 -5
  204. package/src/superlocalmemory/server/ui.py +30 -5
  205. package/src/superlocalmemory/server/unified_daemon.py +431 -75
  206. package/src/superlocalmemory/server/write_identity.py +38 -8
  207. package/src/superlocalmemory/storage/database.py +265 -53
  208. package/src/superlocalmemory/storage/migration_runner.py +53 -0
  209. package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
  210. package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
  211. package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
  212. package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
  213. package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
  214. package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
  215. package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
  216. package/src/superlocalmemory/storage/models.py +4 -0
  217. package/src/superlocalmemory/storage/schema.py +87 -0
  218. package/src/superlocalmemory/storage/schema_v32.py +0 -9
  219. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  220. package/src/superlocalmemory/trust/gate.py +49 -8
  221. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  222. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  223. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  224. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  225. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  226. package/src/superlocalmemory/ui/favicon.svg +35 -4
  227. package/src/superlocalmemory/ui/index.html +306 -173
  228. package/src/superlocalmemory/ui/js/brain.js +5 -20
  229. package/src/superlocalmemory/ui/js/core.js +47 -31
  230. package/src/superlocalmemory/ui/js/dashboard.js +314 -63
  231. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  232. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  233. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  234. package/src/superlocalmemory/ui/js/memories.js +15 -4
  235. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  236. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  237. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  238. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  239. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  240. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  241. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  242. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  243. package/src/superlocalmemory/ui/js/od-backup.js +780 -0
  244. package/src/superlocalmemory/ui/js/od-brain.js +779 -0
  245. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  246. package/src/superlocalmemory/ui/js/od-graph.js +593 -0
  247. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  248. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  249. package/src/superlocalmemory/ui/js/od-memories.js +887 -0
  250. package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
  251. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  252. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  253. package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
  254. package/src/superlocalmemory/ui/js/od-shell.js +593 -0
  255. package/src/superlocalmemory/ui/js/od-skills.js +573 -0
  256. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  257. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  258. package/src/superlocalmemory/ui/js/settings.js +2 -2
  259. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  260. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  261. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  262. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -19,6 +19,18 @@ logger = logging.getLogger(__name__)
19
19
  router = APIRouter(prefix="/api/v3", tags=["v3"])
20
20
 
21
21
 
22
+ def _internal_error() -> JSONResponse:
23
+ """SEC-H-02: log the full traceback server-side, return a generic message.
24
+
25
+ Returning ``str(e)`` to the client leaked DB schema (column/constraint
26
+ names), the data-directory filesystem path, and — for config routes — could
27
+ surface LLM config internals. Call this only from inside an ``except`` block
28
+ so ``logger.exception`` captures the active traceback.
29
+ """
30
+ logger.exception("v3_api request failed")
31
+ return JSONResponse({"error": "Internal server error"}, status_code=500)
32
+
33
+
22
34
  async def _apply_runtime_config(request: Request, config, *, mode_change: bool) -> None:
23
35
  """Persist and hot-swap config only after the daemon transition succeeds."""
24
36
  import asyncio
@@ -99,7 +111,7 @@ async def dashboard(request: Request):
99
111
  "version": SLM_VERSION,
100
112
  }
101
113
  except Exception as e:
102
- return JSONResponse({"error": str(e)}, status_code=500)
114
+ return _internal_error()
103
115
 
104
116
 
105
117
  # ── Mode ─────────────────────────────────────────────────────
@@ -109,26 +121,36 @@ async def get_mode(request: Request):
109
121
  """Get current mode, provider, model — single source of truth for UI."""
110
122
  try:
111
123
  from superlocalmemory.core.config import SLMConfig
124
+ from urllib.parse import urlparse
112
125
  config = getattr(request.app.state, "config", None) or SLMConfig.load()
113
126
  current = config.mode.value
127
+ # SEC-L-01: expose only the endpoint HOST, not the full URL. A full
128
+ # api_base (e.g. an internal Azure OpenAI deployment path) leaks
129
+ # infrastructure topology to non-admin (viewer) users.
130
+ _api_base = config.llm.api_base or ""
131
+ _endpoint_host = urlparse(_api_base).netloc if _api_base else ""
114
132
  return {
115
133
  "mode": current,
116
134
  "provider": config.llm.provider or "none",
117
135
  "model": config.llm.model or "",
118
136
  "has_key": bool(config.llm.api_key),
119
- "endpoint": config.llm.api_base or "",
137
+ "endpoint": _endpoint_host,
120
138
  "capabilities": {
121
139
  "llm_available": bool(config.llm.provider),
122
140
  "cross_encoder": config.retrieval.use_cross_encoder if hasattr(config, 'retrieval') else False,
123
141
  },
124
142
  }
125
143
  except Exception as e:
126
- return JSONResponse({"error": str(e)}, status_code=500)
144
+ return _internal_error()
127
145
 
128
146
 
129
147
  @router.put("/mode")
130
148
  async def set_mode(request: Request):
131
149
  """Switch operating mode. Body: {"mode": "a"|"b"|"c"}"""
150
+ # SEC-H-01: mode switch reconfigures the whole engine — admin-only (MANAGE)
151
+ # on top of machine auth. Owner keeps MANAGE; personal mode unaffected.
152
+ from superlocalmemory.server.rbac_enforce import require_manage
153
+ require_manage(request)
132
154
  try:
133
155
  body = await request.json()
134
156
  new_mode = body.get("mode", "").lower()
@@ -195,7 +217,7 @@ async def set_mode(request: Request):
195
217
  "message": "Embedding re-indexing will run on next recall." if needs_reindex else "",
196
218
  }
197
219
  except Exception as e:
198
- return JSONResponse({"error": str(e)}, status_code=500)
220
+ return _internal_error()
199
221
 
200
222
 
201
223
  @router.post("/mode/set")
@@ -204,6 +226,9 @@ async def set_full_config(request: Request):
204
226
 
205
227
  V3.4.24: Also accepts embedding_* fields for custom embedding endpoints.
206
228
  """
229
+ # SEC-H-01: sets the LLM provider + API key for the whole engine — admin-only.
230
+ from superlocalmemory.server.rbac_enforce import require_manage
231
+ require_manage(request)
207
232
  try:
208
233
  body = await request.json()
209
234
  new_mode = body.get("mode", "a").lower()
@@ -287,7 +312,7 @@ async def set_full_config(request: Request):
287
312
  "embedding_dimension": config.embedding.dimension,
288
313
  }
289
314
  except Exception as e:
290
- return JSONResponse({"error": str(e)}, status_code=500)
315
+ return _internal_error()
291
316
 
292
317
 
293
318
  # ── V3.4.24: Embedding Configuration ────────────────────────────────
@@ -309,12 +334,15 @@ async def get_embedding_config(request: Request):
309
334
  "mode": config.mode.value,
310
335
  }
311
336
  except Exception as e:
312
- return JSONResponse({"error": str(e)}, status_code=500)
337
+ return _internal_error()
313
338
 
314
339
 
315
340
  @router.put("/embedding/config")
316
341
  async def set_embedding_config(request: Request):
317
342
  """Update embedding configuration independently of mode switch."""
343
+ # SEC-H-01: swapping the embedding provider/key affects all recall — admin-only.
344
+ from superlocalmemory.server.rbac_enforce import require_manage
345
+ require_manage(request)
318
346
  try:
319
347
  body = await request.json()
320
348
  from superlocalmemory.core.config import SLMConfig, EmbeddingConfig
@@ -362,7 +390,7 @@ async def set_embedding_config(request: Request):
362
390
  "needs_reindex": needs_reindex,
363
391
  }
364
392
  except Exception as e:
365
- return JSONResponse({"error": str(e)}, status_code=500)
393
+ return _internal_error()
366
394
 
367
395
 
368
396
  @router.get("/scope/config")
@@ -374,12 +402,15 @@ async def get_scope_config(request: Request):
374
402
  config = getattr(request.app.state, "config", None) or SLMConfig.load()
375
403
  return {"success": True, **config.scope.as_dict()}
376
404
  except Exception as exc:
377
- return JSONResponse({"error": str(exc)}, status_code=500)
405
+ return _internal_error()
378
406
 
379
407
 
380
408
  @router.put("/scope/config")
381
409
  async def set_scope_config(request: Request):
382
410
  """Validate, persist, and hot-apply explicit multi-scope defaults."""
411
+ # SEC-H-01: scope defaults govern cross-profile recall visibility — admin-only.
412
+ from superlocalmemory.server.rbac_enforce import require_manage
413
+ require_manage(request)
383
414
  try:
384
415
  body = await request.json()
385
416
  from superlocalmemory.core.config import SLMConfig, ScopeConfig
@@ -411,7 +442,7 @@ async def set_scope_config(request: Request):
411
442
  await _apply_runtime_config(request, config, mode_change=False)
412
443
  return {"success": True, **config.scope.as_dict()}
413
444
  except Exception as exc:
414
- return JSONResponse({"error": str(exc)}, status_code=500)
445
+ return _internal_error()
415
446
 
416
447
 
417
448
  @router.post("/embedding/test")
@@ -475,8 +506,9 @@ async def embed_ping():
475
506
  from .helpers import get_engine_lazy
476
507
  # We just need to confirm the route is alive — engine check is optional
477
508
  return {"ok": True}
478
- except Exception as e:
479
- return JSONResponse({"ok": False, "error": str(e)}, status_code=503)
509
+ except Exception:
510
+ logger.exception("embedder liveness probe failed")
511
+ return JSONResponse({"ok": False, "error": "Internal server error"}, status_code=503)
480
512
 
481
513
 
482
514
  @router.post("/embed")
@@ -509,7 +541,7 @@ async def embed_texts(request: Request):
509
541
  )
510
542
  return {"embeddings": embeddings}
511
543
  except Exception as e:
512
- return JSONResponse({"error": str(e)}, status_code=500)
544
+ return _internal_error()
513
545
 
514
546
 
515
547
  def _validate_provider_url(url: str, client_host: str) -> str | None:
@@ -636,7 +668,8 @@ async def test_provider(request: Request):
636
668
  except httpx.HTTPStatusError as e:
637
669
  return {"success": False, "error": f"HTTP {e.response.status_code}: Invalid key or endpoint"}
638
670
  except Exception as e:
639
- return {"success": False, "error": str(e)}
671
+ logger.exception("verify_key failed")
672
+ return {"success": False, "error": "internal error"}
640
673
 
641
674
 
642
675
  @router.get("/ollama/status")
@@ -666,7 +699,8 @@ async def list_providers():
666
699
  from superlocalmemory.core.config import SLMConfig
667
700
  return {"providers": SLMConfig.provider_presets()}
668
701
  except Exception as exc:
669
- return {"error": str(exc), "providers": []}
702
+ logger.exception("list_providers failed")
703
+ return {"error": "could not load provider list", "providers": []}
670
704
 
671
705
 
672
706
  @router.get("/provider")
@@ -685,12 +719,20 @@ async def get_provider():
685
719
  "has_key": bool(key),
686
720
  }
687
721
  except Exception as exc:
688
- return {"error": str(exc), "provider": "unknown"}
722
+ logger.exception("get_provider failed")
723
+ return {"error": "could not load configuration", "provider": "unknown"}
689
724
 
690
725
 
691
726
  @router.put("/provider")
692
727
  async def set_provider(request: Request):
693
728
  """Set LLM provider. Body: {"provider": "openai", "api_key": "...", "model": "..."}"""
729
+ # Swapping the provider + API key redirects all LLM traffic (prompt/output
730
+ # interception risk) — admin-only, on top of mutation auth.
731
+ from superlocalmemory.server.route_mutations import authorize_route_mutation
732
+ from superlocalmemory.server.rbac_enforce import require_manage
733
+ _auth = authorize_route_mutation(request, operation="update",
734
+ source_agent_id="http-set-provider")
735
+ require_manage(request)
694
736
  try:
695
737
  body = await request.json()
696
738
  provider = body.get("provider", "")
@@ -720,7 +762,7 @@ async def set_provider(request: Request):
720
762
 
721
763
  return {"success": True, "provider": provider, "model": model}
722
764
  except Exception as e:
723
- return JSONResponse({"error": str(e)}, status_code=500)
765
+ return _internal_error()
724
766
 
725
767
 
726
768
  # ── Recall Trace ─────────────────────────────────────────────
@@ -739,6 +781,7 @@ async def recall_trace(request: Request):
739
781
  body = await request.json()
740
782
  query = body.get("query", "")
741
783
  limit = body.get("limit", 10)
784
+ window = body.get("window", "") or ""
742
785
 
743
786
  # Use daemon engine — already loaded, shares warm page cache.
744
787
  # run_in_executor keeps event loop alive so browser doesn't abort.
@@ -751,7 +794,7 @@ async def recall_trace(request: Request):
751
794
  t0 = _time.monotonic()
752
795
  response = await loop.run_in_executor(
753
796
  None,
754
- lambda: engine.recall(query, limit=limit, fast=False),
797
+ lambda: engine.recall(query, limit=limit, fast=False, window=window or None),
755
798
  )
756
799
  elapsed_ms = round((_time.monotonic() - t0) * 1000, 1)
757
800
 
@@ -784,7 +827,7 @@ async def recall_trace(request: Request):
784
827
  **recall_response_metadata(response),
785
828
  }
786
829
  except Exception as e:
787
- return JSONResponse({"error": str(e)}, status_code=500)
830
+ return _internal_error()
788
831
 
789
832
 
790
833
  def _record_learning_signals(query: str, results: list) -> None:
@@ -874,7 +917,7 @@ async def trust_dashboard(request: Request):
874
917
  "profile": pid,
875
918
  }
876
919
  except Exception as e:
877
- return JSONResponse({"error": str(e)}, status_code=500)
920
+ return _internal_error()
878
921
 
879
922
 
880
923
  # ── Math Health ──────────────────────────────────────────────
@@ -916,7 +959,7 @@ async def math_health(request: Request):
916
959
  "note": "config-derived; not a live runtime probe",
917
960
  }
918
961
  except Exception as e:
919
- return JSONResponse({"error": str(e)}, status_code=500)
962
+ return _internal_error()
920
963
 
921
964
 
922
965
  # ── Auto-Capture / Auto-Recall Config ────────────────────────
@@ -930,7 +973,8 @@ async def get_auto_capture_config():
930
973
  rules = RulesEngine(config_path=DEFAULT_BASE_DIR / "config.json")
931
974
  return {"config": rules.get_capture_config()}
932
975
  except Exception as exc:
933
- return {"error": str(exc), "config": {}}
976
+ logger.exception("get_auto_capture_config failed")
977
+ return {"error": "internal error", "config": {}}
934
978
 
935
979
 
936
980
  @router.put("/auto-capture/config")
@@ -947,7 +991,7 @@ async def set_auto_capture_config(request: Request):
947
991
  rules.save(config_path)
948
992
  return {"success": True, "config": rules.get_capture_config()}
949
993
  except Exception as e:
950
- return JSONResponse({"error": str(e)}, status_code=500)
994
+ return _internal_error()
951
995
 
952
996
 
953
997
  @router.get("/auto-recall/config")
@@ -959,7 +1003,8 @@ async def get_auto_recall_config():
959
1003
  rules = RulesEngine(config_path=DEFAULT_BASE_DIR / "config.json")
960
1004
  return {"config": rules.get_recall_config()}
961
1005
  except Exception as exc:
962
- return {"error": str(exc), "config": {}}
1006
+ logger.exception("get_auto_recall_config failed")
1007
+ return {"error": "internal error", "config": {}}
963
1008
 
964
1009
 
965
1010
  @router.put("/auto-recall/config")
@@ -976,7 +1021,7 @@ async def set_auto_recall_config(request: Request):
976
1021
  rules.save(config_path)
977
1022
  return {"success": True, "config": rules.get_recall_config()}
978
1023
  except Exception as e:
979
- return JSONResponse({"error": str(e)}, status_code=500)
1024
+ return _internal_error()
980
1025
 
981
1026
 
982
1027
  # ── IDE Status ───────────────────────────────────────────────
@@ -989,7 +1034,8 @@ async def ide_status():
989
1034
  connector = IDEConnector()
990
1035
  return {"ides": connector.get_status()}
991
1036
  except Exception as exc:
992
- return {"error": str(exc), "ides": []}
1037
+ logger.exception("ide_status failed")
1038
+ return {"error": "internal error", "ides": []}
993
1039
 
994
1040
 
995
1041
  @router.post("/ide/connect")
@@ -1009,7 +1055,7 @@ async def ide_connect(request: Request):
1009
1055
  results = connector.connect_all()
1010
1056
  return {"results": results}
1011
1057
  except Exception as e:
1012
- return JSONResponse({"error": str(e)}, status_code=500)
1058
+ return _internal_error()
1013
1059
 
1014
1060
 
1015
1061
  # ── Active Memory (V3.1) ────────────────────────────────────
@@ -1027,7 +1073,8 @@ async def learning_signals():
1027
1073
  pid = config.active_profile
1028
1074
  return {"success": True, **signals.get_signal_stats(pid)}
1029
1075
  except Exception as exc:
1030
- return {"success": False, "error": str(exc)}
1076
+ logger.exception("learning_signals failed")
1077
+ return {"success": False, "error": "internal error"}
1031
1078
 
1032
1079
 
1033
1080
  @router.post("/learning/consolidate")
@@ -1078,7 +1125,8 @@ async def run_consolidation(request: Request):
1078
1125
  except HTTPException:
1079
1126
  raise
1080
1127
  except Exception as exc:
1081
- return {"success": False, "error": str(exc)}
1128
+ logger.exception("run_consolidation failed")
1129
+ return {"success": False, "error": "internal error"}
1082
1130
 
1083
1131
 
1084
1132
  @router.get("/hooks/status")
@@ -1088,7 +1136,8 @@ async def hooks_status():
1088
1136
  from superlocalmemory.hooks.claude_code_hooks import check_status
1089
1137
  return {"success": True, **check_status()}
1090
1138
  except Exception as exc:
1091
- return {"success": False, "error": str(exc)}
1139
+ logger.exception("hooks_status failed")
1140
+ return {"success": False, "error": "internal error"}
1092
1141
 
1093
1142
 
1094
1143
  # ── Phase 6: V3.2 API Endpoints ──────────────────────────────
@@ -1153,7 +1202,7 @@ async def get_auto_invoke_config(request: Request):
1153
1202
  "last_invocation": persisted.get("last_invocation", None),
1154
1203
  }
1155
1204
  except Exception as e:
1156
- return JSONResponse({"error": str(e)}, status_code=500)
1205
+ return _internal_error()
1157
1206
 
1158
1207
 
1159
1208
  # ── 2. PUT /api/v3/auto-invoke/config ─────────────────────────
@@ -1192,7 +1241,7 @@ async def set_auto_invoke_config(request: Request):
1192
1241
 
1193
1242
  return updated
1194
1243
  except Exception as e:
1195
- return JSONResponse({"error": str(e)}, status_code=500)
1244
+ return _internal_error()
1196
1245
 
1197
1246
 
1198
1247
  # ── 3. GET /api/v3/associations ───────────────────────────────
@@ -1264,7 +1313,7 @@ async def get_associations(
1264
1313
 
1265
1314
  return {"edges": edges, "total": total}
1266
1315
  except Exception as e:
1267
- return JSONResponse({"error": str(e)}, status_code=500)
1316
+ return _internal_error()
1268
1317
 
1269
1318
 
1270
1319
  # ── 4. GET /api/v3/associations/stats ─────────────────────────
@@ -1365,7 +1414,7 @@ async def get_association_stats(request: Request, profile: str = ""):
1365
1414
  "top_connected_facts": top_facts,
1366
1415
  }
1367
1416
  except Exception as e:
1368
- return JSONResponse({"error": str(e)}, status_code=500)
1417
+ return _internal_error()
1369
1418
 
1370
1419
 
1371
1420
  # ── 5. GET /api/v3/consolidation/status ───────────────────────
@@ -1452,7 +1501,7 @@ async def get_consolidation_status(request: Request, profile: str = ""):
1452
1501
  conn.close()
1453
1502
  return result
1454
1503
  except Exception as e:
1455
- return JSONResponse({"error": str(e)}, status_code=500)
1504
+ return _internal_error()
1456
1505
 
1457
1506
 
1458
1507
  # ── 6. POST /api/v3/consolidation/trigger ─────────────────────
@@ -1462,7 +1511,8 @@ async def trigger_consolidation(request: Request):
1462
1511
  """Trigger consolidation manually.
1463
1512
 
1464
1513
  Body: {"lightweight": false, "profile": ""}
1465
- Uses WorkerPool for thread safety (Rule 18).
1514
+ Runs under the daemon's profile-runtime operation lease (Rule 18) so a
1515
+ concurrent profile switch cannot commit mid-consolidation.
1466
1516
  """
1467
1517
  try:
1468
1518
  body = await request.json()
@@ -1478,51 +1528,61 @@ async def trigger_consolidation(request: Request):
1478
1528
  profile_id=pid,
1479
1529
  )
1480
1530
 
1481
- # Use WorkerPool to run consolidation in the worker subprocess (Rule 18)
1482
- try:
1483
- from superlocalmemory.core.worker_pool import WorkerPool
1484
- pool = WorkerPool.shared()
1485
- result = pool.send_command({
1486
- "action": "consolidate",
1487
- "profile_id": pid,
1488
- "lightweight": lightweight,
1489
- })
1490
- if result and result.get("ok"):
1491
- authorization.complete()
1492
- return {"success": True, **result}
1493
- except Exception:
1494
- pass
1495
-
1496
- # Fallback: direct consolidation if WorkerPool unavailable
1531
+ # v3.7.8 SEC-M-01: the prior "WorkerPool" fast path called
1532
+ # ``pool.send_command(...)``, a method that does not exist on
1533
+ # WorkerPool every call raised AttributeError, was silently
1534
+ # swallowed by the bare ``except``, and fell through to this direct
1535
+ # path unconditionally. That dead branch is removed; consolidation
1536
+ # always runs directly against a lease-protected DB connection so a
1537
+ # concurrent profile switch cannot commit mid-consolidation.
1538
+ #
1539
+ # v3.4.64: ConsolidationEngine.consolidate() is CPU/IO bound (seconds
1540
+ # to minutes). Calling it directly in an async route blocks the ASGI
1541
+ # event loop. Moved into asyncio.to_thread() so the event loop stays
1542
+ # live. The runtime.operation() lease is acquired INSIDE the thread —
1543
+ # blocking a thread is fine; blocking the event loop is not.
1544
+ import asyncio as _asyncio
1497
1545
  from superlocalmemory.core.config import SLMConfig
1498
1546
  from superlocalmemory.storage.database import DatabaseManager
1499
1547
  from superlocalmemory.storage import schema as _schema
1500
1548
  from superlocalmemory.core.consolidation_engine import ConsolidationEngine
1549
+ from superlocalmemory.server.profile_runtime import get_profile_runtime
1501
1550
 
1502
- config = SLMConfig.load()
1503
- db = DatabaseManager(config.db_path)
1504
- db.initialize(_schema)
1505
-
1506
- engine = ConsolidationEngine(db=db, config=config.consolidation, slm_config=config)
1507
- result = engine.consolidate(profile_id=pid, lightweight=lightweight)
1551
+ _app_state = request.app.state
1508
1552
 
1509
- # v3.4.1: Auto-trigger behavioral pattern mining after consolidation
1510
- try:
1511
- from superlocalmemory.learning.consolidation_worker import ConsolidationWorker
1512
- learning_db = config.base_dir / "learning.db"
1513
- cw = ConsolidationWorker(str(config.db_path), str(learning_db))
1514
- pattern_count = cw._generate_patterns(pid, False)
1515
- result["patterns_mined"] = pattern_count
1516
- logger.info("Auto-mined %d patterns after consolidation", pattern_count)
1517
- except Exception as exc:
1518
- logger.debug("Pattern mining after consolidation failed: %s", exc)
1553
+ def _run_consolidation() -> dict:
1554
+ runtime = get_profile_runtime(_app_state)
1555
+ with runtime.operation():
1556
+ config = SLMConfig.load()
1557
+ db = DatabaseManager(config.db_path)
1558
+ db.initialize(_schema)
1559
+ engine = ConsolidationEngine(
1560
+ db=db, config=config.consolidation, slm_config=config,
1561
+ )
1562
+ res = engine.consolidate(profile_id=pid, lightweight=lightweight)
1563
+ # v3.4.1: Auto-trigger behavioral pattern mining after consolidation
1564
+ try:
1565
+ from superlocalmemory.learning.consolidation_worker import (
1566
+ ConsolidationWorker,
1567
+ )
1568
+ learning_db = config.base_dir / "learning.db"
1569
+ cw = ConsolidationWorker(str(config.db_path), str(learning_db))
1570
+ pattern_count = cw._generate_patterns(pid, False)
1571
+ res["patterns_mined"] = pattern_count
1572
+ logger.info(
1573
+ "Auto-mined %d patterns after consolidation", pattern_count
1574
+ )
1575
+ except Exception as exc:
1576
+ logger.debug("Pattern mining after consolidation failed: %s", exc)
1577
+ return res
1519
1578
 
1579
+ result = await _asyncio.to_thread(_run_consolidation)
1520
1580
  authorization.complete()
1521
1581
  return {"success": True, **result}
1522
1582
  except HTTPException:
1523
1583
  raise
1524
1584
  except Exception as e:
1525
- return JSONResponse({"error": str(e)}, status_code=500)
1585
+ return _internal_error()
1526
1586
 
1527
1587
 
1528
1588
  # ── 7. GET /api/v3/core-memory ────────────────────────────────
@@ -1568,7 +1628,7 @@ async def get_core_memory(request: Request, profile: str = ""):
1568
1628
 
1569
1629
  return {"blocks": blocks, "total_chars": total_chars, "char_limit": 2000}
1570
1630
  except Exception as e:
1571
- return JSONResponse({"error": str(e)}, status_code=500)
1631
+ return _internal_error()
1572
1632
 
1573
1633
 
1574
1634
  # ── 8. PUT /api/v3/core-memory/{block_id} ─────────────────────
@@ -1651,7 +1711,7 @@ async def update_core_memory_block(block_id: str, request: Request):
1651
1711
  except HTTPException:
1652
1712
  raise
1653
1713
  except Exception as e:
1654
- return JSONResponse({"error": str(e)}, status_code=500)
1714
+ return _internal_error()
1655
1715
 
1656
1716
 
1657
1717
  # ── 9. GET /api/v3/vector-store/status ────────────────────────
@@ -1698,7 +1758,7 @@ async def get_vector_store_status(request: Request, profile: str = ""):
1698
1758
 
1699
1759
  return result
1700
1760
  except Exception as e:
1701
- return JSONResponse({"error": str(e)}, status_code=500)
1761
+ return _internal_error()
1702
1762
 
1703
1763
 
1704
1764
  # ── Phase 10: V3.3 API Endpoints ────────────────────────────
@@ -1753,7 +1813,7 @@ async def forgetting_stats(request: Request, profile: str = ""):
1753
1813
  conn.close()
1754
1814
  return {"total": total, "zones": zones}
1755
1815
  except Exception as e:
1756
- return JSONResponse({"error": str(e)}, status_code=500)
1816
+ return _internal_error()
1757
1817
 
1758
1818
 
1759
1819
  # ── 1b. POST /api/v3/forgetting/run ─────────────────────────
@@ -1829,8 +1889,9 @@ async def run_forgetting(request: Request):
1829
1889
 
1830
1890
  conn.commit()
1831
1891
  except Exception as exc:
1892
+ logger.exception("run_forgetting decay failed")
1832
1893
  conn.close()
1833
- return {"success": False, "error": str(exc)}
1894
+ return {"success": False, "error": "internal error"}
1834
1895
 
1835
1896
  conn.close()
1836
1897
  authorization.complete()
@@ -1838,7 +1899,7 @@ async def run_forgetting(request: Request):
1838
1899
  except HTTPException:
1839
1900
  raise
1840
1901
  except Exception as e:
1841
- return JSONResponse({"error": str(e)}, status_code=500)
1902
+ return _internal_error()
1842
1903
 
1843
1904
 
1844
1905
  # ── 1c. GET /api/v3/quantization/stats ──────────────────────
@@ -1901,7 +1962,7 @@ async def quantization_stats(request: Request, profile: str = ""):
1901
1962
  conn.close()
1902
1963
  return {"total": total, "tiers": tiers, "compression_ratio": compression_ratio}
1903
1964
  except Exception as e:
1904
- return JSONResponse({"error": str(e)}, status_code=500)
1965
+ return _internal_error()
1905
1966
 
1906
1967
 
1907
1968
  # ── 1d. GET /api/v3/ccq/blocks ──────────────────────────────
@@ -1961,7 +2022,7 @@ async def ccq_blocks(request: Request, profile: str = "", limit: int = 50):
1961
2022
  conn.close()
1962
2023
  return {"blocks": blocks, "total": total}
1963
2024
  except Exception as e:
1964
- return JSONResponse({"error": str(e)}, status_code=500)
2025
+ return _internal_error()
1965
2026
 
1966
2027
 
1967
2028
  # ── 1e. GET /api/v3/soft-prompts ─────────────────────────────
@@ -2014,7 +2075,7 @@ async def get_soft_prompts(request: Request, profile: str = ""):
2014
2075
  conn.close()
2015
2076
  return {"prompts": prompts, "total": len(prompts), "total_tokens": total_tokens}
2016
2077
  except Exception as e:
2017
- return JSONResponse({"error": str(e)}, status_code=500)
2078
+ return _internal_error()
2018
2079
 
2019
2080
 
2020
2081
  # ── 1f. GET /api/v3/health/processes ─────────────────────────
@@ -2061,7 +2122,7 @@ async def process_health(request: Request):
2061
2122
  "healthy": processes["parent"]["status"] != "dead",
2062
2123
  }
2063
2124
  except Exception as e:
2064
- return JSONResponse({"error": str(e)}, status_code=500)
2125
+ return _internal_error()
2065
2126
 
2066
2127
 
2067
2128
  # ── 1g. GET /api/v3/v33/overview ─────────────────────────────
@@ -2193,7 +2254,7 @@ async def get_graph_communities(request: Request, profile: str = ""):
2193
2254
  return {"communities": communities, "total": len(communities)}
2194
2255
 
2195
2256
  except Exception as e:
2196
- return JSONResponse({"error": str(e)}, status_code=500)
2257
+ return _internal_error()
2197
2258
 
2198
2259
 
2199
2260
  @router.post("/graph/run-communities")
@@ -2222,7 +2283,7 @@ async def run_community_detection(request: Request):
2222
2283
  except HTTPException:
2223
2284
  raise
2224
2285
  except Exception as e:
2225
- return JSONResponse({"error": str(e)}, status_code=500)
2286
+ return _internal_error()
2226
2287
 
2227
2288
 
2228
2289
  @router.get("/v33/overview")
@@ -2355,4 +2416,56 @@ async def v33_overview(request: Request, profile: str = ""):
2355
2416
  conn.close()
2356
2417
  return overview
2357
2418
  except Exception as e:
2358
- return JSONResponse({"error": str(e)}, status_code=500)
2419
+ return _internal_error()
2420
+
2421
+
2422
+ # ── MCP Profiles ─────────────────────────────────────────────
2423
+
2424
+ @router.get("/mcp/profiles")
2425
+ async def get_mcp_profiles(request: Request):
2426
+ """MCP profile summary: current profile, all profiles with tool counts and names.
2427
+
2428
+ Reads only the env var SLM_MCP_PROFILE and the pure-data profiles module.
2429
+ No engine required — safe to call at any point in the daemon lifecycle.
2430
+
2431
+ Note on 'current': when SLM_MCP_PROFILE is unset the MCP server falls
2432
+ back to the legacy _ESSENTIAL_TOOLS set. The UI reports 'core' for that
2433
+ state because 'core' is the recommended named-profile equivalent for new
2434
+ installs and is the closest documented starting point for users.
2435
+ """
2436
+ try:
2437
+ from superlocalmemory.mcp.profiles import (
2438
+ _PROFILE_DEFINITIONS,
2439
+ _PROFILE_ALIASES,
2440
+ PROFILE_DESCRIPTIONS,
2441
+ )
2442
+
2443
+ raw_profile = os.environ.get("SLM_MCP_PROFILE", "").strip().lower()
2444
+ canonical = _PROFILE_ALIASES.get(raw_profile, raw_profile)
2445
+ # Blank env var, "whole", or an unknown value all resolve to "core"
2446
+ # for UI display purposes (safe, conservative default).
2447
+ if not canonical or canonical == "whole" or canonical not in _PROFILE_DEFINITIONS:
2448
+ current = "core"
2449
+ else:
2450
+ current = canonical
2451
+
2452
+ profiles_out: dict = {}
2453
+ for name, tool_set in _PROFILE_DEFINITIONS.items():
2454
+ profiles_out[name] = {
2455
+ "count": len(tool_set),
2456
+ "tools": sorted(tool_set),
2457
+ "description": PROFILE_DESCRIPTIONS.get(name, ""),
2458
+ }
2459
+
2460
+ all_tools: set = set()
2461
+ for tool_set in _PROFILE_DEFINITIONS.values():
2462
+ all_tools.update(tool_set)
2463
+
2464
+ return {
2465
+ "current": current,
2466
+ "profiles": profiles_out,
2467
+ "aliases": dict(_PROFILE_ALIASES),
2468
+ "total_tools": len(all_tools),
2469
+ }
2470
+ except Exception:
2471
+ return _internal_error()