superlocalmemory 3.7.8 → 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 (260) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +69 -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 -1
  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 +94 -10
  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/backup.py +12 -1
  116. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  117. package/src/superlocalmemory/infra/data_root.py +43 -4
  118. package/src/superlocalmemory/infra/event_bus.py +107 -24
  119. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  120. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  121. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  122. package/src/superlocalmemory/learning/cross_project.py +28 -19
  123. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  124. package/src/superlocalmemory/loops/__init__.py +56 -0
  125. package/src/superlocalmemory/loops/budget.py +58 -0
  126. package/src/superlocalmemory/loops/engine.py +164 -0
  127. package/src/superlocalmemory/loops/ledger.py +243 -0
  128. package/src/superlocalmemory/loops/models.py +152 -0
  129. package/src/superlocalmemory/loops/rules.py +52 -0
  130. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  131. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  132. package/src/superlocalmemory/mcp/profiles.py +103 -0
  133. package/src/superlocalmemory/mcp/server.py +21 -49
  134. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  135. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  136. package/src/superlocalmemory/mcp/tools_core.py +8 -1
  137. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  138. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  139. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  140. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  141. package/src/superlocalmemory/mesh/broker.py +237 -129
  142. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  143. package/src/superlocalmemory/optimize/NOTICE +1 -6
  144. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  145. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  146. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  147. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  148. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  149. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  150. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  151. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  152. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  153. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  154. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  155. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  156. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  157. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  158. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  159. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  160. package/src/superlocalmemory/retrieval/engine.py +168 -26
  161. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  162. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  163. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  164. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  165. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  166. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  167. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  168. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  169. package/src/superlocalmemory/server/api.py +4 -4
  170. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  171. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  172. package/src/superlocalmemory/server/recall_health.py +24 -3
  173. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  174. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  175. package/src/superlocalmemory/server/routes/agents.py +128 -38
  176. package/src/superlocalmemory/server/routes/backup.py +34 -10
  177. package/src/superlocalmemory/server/routes/behavioral.py +13 -12
  178. package/src/superlocalmemory/server/routes/brain.py +21 -5
  179. package/src/superlocalmemory/server/routes/chat.py +10 -5
  180. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  181. package/src/superlocalmemory/server/routes/config_api.py +436 -0
  182. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  183. package/src/superlocalmemory/server/routes/entity.py +9 -4
  184. package/src/superlocalmemory/server/routes/events.py +24 -8
  185. package/src/superlocalmemory/server/routes/evolution.py +135 -17
  186. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  187. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  188. package/src/superlocalmemory/server/routes/insights.py +3 -3
  189. package/src/superlocalmemory/server/routes/learning.py +14 -14
  190. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  191. package/src/superlocalmemory/server/routes/memories.py +182 -57
  192. package/src/superlocalmemory/server/routes/mesh.py +95 -15
  193. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  194. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  195. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  196. package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
  197. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  198. package/src/superlocalmemory/server/routes/stats.py +13 -6
  199. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  200. package/src/superlocalmemory/server/routes/v3_api.py +183 -69
  201. package/src/superlocalmemory/server/routes/ws.py +5 -2
  202. package/src/superlocalmemory/server/security_middleware.py +12 -5
  203. package/src/superlocalmemory/server/ui.py +20 -5
  204. package/src/superlocalmemory/server/unified_daemon.py +384 -56
  205. package/src/superlocalmemory/server/write_identity.py +38 -8
  206. package/src/superlocalmemory/storage/database.py +265 -53
  207. package/src/superlocalmemory/storage/migration_runner.py +53 -0
  208. package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
  209. package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
  210. package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
  211. package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
  212. package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
  213. package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
  214. package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
  215. package/src/superlocalmemory/storage/models.py +4 -0
  216. package/src/superlocalmemory/storage/schema.py +87 -0
  217. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  218. package/src/superlocalmemory/trust/gate.py +49 -8
  219. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  220. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  221. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  222. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  223. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  224. package/src/superlocalmemory/ui/favicon.svg +35 -4
  225. package/src/superlocalmemory/ui/index.html +306 -173
  226. package/src/superlocalmemory/ui/js/brain.js +5 -20
  227. package/src/superlocalmemory/ui/js/core.js +47 -31
  228. package/src/superlocalmemory/ui/js/dashboard.js +314 -63
  229. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  230. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  231. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  232. package/src/superlocalmemory/ui/js/memories.js +15 -4
  233. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  234. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  235. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  236. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  237. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  238. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  239. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  240. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  241. package/src/superlocalmemory/ui/js/od-backup.js +780 -0
  242. package/src/superlocalmemory/ui/js/od-brain.js +779 -0
  243. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  244. package/src/superlocalmemory/ui/js/od-graph.js +593 -0
  245. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  246. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  247. package/src/superlocalmemory/ui/js/od-memories.js +887 -0
  248. package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
  249. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  250. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  251. package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
  252. package/src/superlocalmemory/ui/js/od-shell.js +593 -0
  253. package/src/superlocalmemory/ui/js/od-skills.js +573 -0
  254. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  255. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  256. package/src/superlocalmemory/ui/js/settings.js +2 -2
  257. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  258. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  259. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  260. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -75,13 +75,13 @@ def reset_engine():
75
75
 
76
76
  # Register tools and resources -------------------------------------------------
77
77
  #
78
- # Essential-only default: 25 base tools + 8 mesh tools = 33 registered
78
+ # Essential-only default: 34 base tools + 8 mesh tools = 42 registered
79
79
  # when mesh is enabled. Set ``SLM_MCP_ALL_TOOLS=1`` to expose the full
80
80
  # toolset. Rationale: IDEs cap at 50-100 tools total (Cursor,
81
81
  # Antigravity, Windsurf) and a maximal SLM registration crowds out
82
82
  # other MCP servers the user may have installed.
83
83
  # Admin/diagnostics tools remain available via CLI (`slm <command>`).
84
- # Set SLM_MCP_ALL_TOOLS=1 to enable all 38 tools (power users).
84
+ # Set SLM_MCP_ALL_TOOLS=1 to enable all 84 tools (power users).
85
85
 
86
86
  import os as _os_reg
87
87
 
@@ -107,6 +107,10 @@ _ESSENTIAL_TOOLS: set[str] = {
107
107
  "evolve_skill", "skill_health", "skill_lineage",
108
108
  # v3.6.11: Surface B Optimize tools (5)
109
109
  "slm_compress", "slm_retrieve", "slm_cache_set", "slm_cache_get", "slm_optimize_stats",
110
+ # v3.8.0: bounded-loop tools (3) — CLI + /slm-loop command + MCP. Kept in
111
+ # the default exposure so any MCP client discovers gated loops, not just
112
+ # profile=code/full/power sessions.
113
+ "slm_loop_run", "slm_loop_history", "slm_loop_show",
110
114
  }
111
115
 
112
116
  # v3.4.4: Mesh tools — enabled if mesh_enabled in config or SLM_MCP_MESH_TOOLS=1
@@ -138,55 +142,21 @@ _user_allowlist_str = _os_reg.environ.get("SLM_MCP_TOOLS", "").strip()
138
142
 
139
143
  # ---------------------------------------------------------------------------
140
144
  # v3.6.14 WP-01: Named profile definitions
145
+ # Extracted to mcp/profiles.py (v3.8.0) — pure data, no side effects.
146
+ # All names re-exported here for backward compatibility with existing tests
147
+ # and any code that imports them from this module.
141
148
  # ---------------------------------------------------------------------------
142
149
 
143
- _PROFILE_CORE = frozenset({ # 14
144
- "remember", "recall", "search", "fetch", "list_recent", "update_memory", "forget",
145
- "session_init", "close_session",
146
- "slm_compress", "slm_retrieve", "slm_cache_set", "slm_cache_get", "slm_optimize_stats",
147
- })
148
- _PROFILE_CODE = _PROFILE_CORE | frozenset({ # 20
149
- "build_code_graph", "get_blast_radius", "query_graph",
150
- "semantic_search_code", "get_review_context", "detect_changes",
151
- })
152
- _PROFILE_FULL_MESH = frozenset({ # 8
153
- "mesh_summary", "mesh_peers", "mesh_send", "mesh_inbox",
154
- "mesh_state", "mesh_lock", "mesh_events", "mesh_status",
155
- })
156
- _PROFILE_FULL = frozenset({ # 30 base — EXPLICIT literal, NOT runtime _ESSENTIAL_TOOLS (OQ-2)
157
- "remember", "recall", "search", "fetch", "list_recent", "delete_memory", "update_memory",
158
- "get_status", "session_init", "observe", "close_session", "report_feedback", "forget",
159
- "run_maintenance", "consolidate_cognitive", "get_soft_prompts", "set_mode", "report_outcome",
160
- "log_tool_event", "get_assertions", "reinforce_assertion", "contradict_assertion",
161
- "evolve_skill", "skill_health", "skill_lineage",
162
- "slm_compress", "slm_retrieve", "slm_cache_set", "slm_cache_get", "slm_optimize_stats",
163
- }) | _PROFILE_FULL_MESH # 38
164
- _PROFILE_POWER = _PROFILE_FULL | frozenset({ # 50
165
- "get_version", "get_mode", "health", "consistency_check", "recall_trace",
166
- "get_lifecycle_status", "set_retention_policy", "compact_memories",
167
- "get_behavioral_patterns", "audit_trail", "quantize", "get_retention_stats",
168
- })
169
- _PROFILE_MESH = _PROFILE_FULL_MESH # 8
170
-
171
- _PROFILE_DEFINITIONS: dict[str, frozenset[str]] = {
172
- "core": _PROFILE_CORE,
173
- "code": _PROFILE_CODE,
174
- "full": _PROFILE_FULL,
175
- "power": _PROFILE_POWER,
176
- "mesh": _PROFILE_MESH,
177
- } # "whole" intentionally absent — maps to raw server (D-2 LOCKED)
178
-
179
- # Compatibility aliases published by the v3.6 README. Keep these explicit so
180
- # a stale client configuration has one deterministic meaning and emits a
181
- # migration warning. Any other value is a configuration error (fail closed).
182
- _PROFILE_ALIASES: dict[str, str] = {
183
- "core14": "core",
184
- "code20": "code",
185
- "full38": "full",
186
- "power50": "power",
187
- "mesh8": "mesh",
188
- "whole81": "whole",
189
- }
150
+ from superlocalmemory.mcp.profiles import ( # noqa: E402 (after env setups above)
151
+ _PROFILE_CORE,
152
+ _PROFILE_CODE,
153
+ _PROFILE_FULL_MESH,
154
+ _PROFILE_FULL,
155
+ _PROFILE_POWER,
156
+ _PROFILE_MESH,
157
+ _PROFILE_DEFINITIONS,
158
+ _PROFILE_ALIASES,
159
+ )
190
160
 
191
161
  _profile = _os_reg.environ.get("SLM_MCP_PROFILE", "").strip().lower()
192
162
 
@@ -291,6 +261,8 @@ register_learning_tools(_target, get_engine) # v3.4.7: Two-way learning tools
291
261
  register_evolution_tools(_target, get_engine) # v3.4.11: Skill evolution tools
292
262
  from superlocalmemory.mcp.tools_optimize import register_optimize_tools
293
263
  register_optimize_tools(_target) # v3.6.11: Surface B Optimize tools (proxy-free)
264
+ from superlocalmemory.mcp.tools_loops import register_loop_tools
265
+ register_loop_tools(_target, get_engine) # v3.8.0: bounded-loop tools (CLI+command+MCP)
294
266
 
295
267
 
296
268
  # V3.3.21: Eager engine warmup — start initializing BEFORE first tool call.
@@ -186,12 +186,6 @@ def register_active_tools(server, get_engine: Callable) -> None:
186
186
  relevance score is ≥ 0.70 (architectural decisions that remain
187
187
  permanently relevant still surface). Default: 30.
188
188
  Set to 0 to disable the age gate entirely.
189
-
190
- Scoring: five candidate producers (semantic + BM25 + temporal +
191
- spreading_activation + hopfield) feed RRF fusion; the entity graph then
192
- applies an optional post-fusion score enhancement. Combined with
193
- Ebbinghaus exponential recency decay and FSRS stability strengthening by
194
- access frequency.
195
189
  """
196
190
  try:
197
191
  from superlocalmemory.hooks.rules_engine import RulesEngine
@@ -689,7 +683,10 @@ def register_active_tools(server, get_engine: Callable) -> None:
689
683
  try:
690
684
  engine = get_engine()
691
685
  db = engine.db
692
- pid = profile_id or engine.profile_id
686
+ # Isolation: the tenant is ALWAYS the engine's active profile. The
687
+ # caller-supplied profile_id is ignored — honoring it let any MCP
688
+ # client read/pin another profile's facts by passing profile_id.
689
+ pid = engine.profile_id
693
690
 
694
691
  if action == "pin":
695
692
  if not fact_id:
@@ -14,10 +14,16 @@ All tools return {"success": bool, ...} envelope. Never raise.
14
14
  from __future__ import annotations
15
15
 
16
16
  import logging
17
+ import os
17
18
  import time
18
19
  from pathlib import Path
19
20
  from typing import Any, Callable
20
21
 
22
+ from superlocalmemory.core.security_primitives import (
23
+ PathTraversalError,
24
+ safe_resolve,
25
+ )
26
+
21
27
  logger = logging.getLogger(__name__)
22
28
 
23
29
  # ---------------------------------------------------------------------------
@@ -118,7 +124,14 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
118
124
  exclude_patterns: Comma-separated glob patterns to exclude.
119
125
  """
120
126
  try:
121
- repo = Path(repo_path)
127
+ # SEC: contain the untrusted repo_path under $HOME (same guard as
128
+ # update_code_graph) so repo_path="/" cannot ingest the whole
129
+ # filesystem into the code-graph DB.
130
+ from superlocalmemory.core.security_primitives import PathTraversalError
131
+ try:
132
+ repo = safe_resolve(Path.home(), repo_path)
133
+ except (PathTraversalError, OSError, ValueError) as exc:
134
+ return _error_response(f"Invalid repo_path: {exc}")
122
135
  if not repo.exists():
123
136
  return _error_response(
124
137
  f"Repository path does not exist: {repo_path}"
@@ -250,6 +263,22 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
250
263
  svc = _get_service()
251
264
  db = svc.db
252
265
 
266
+ # SEC 3.7.9 (B1): contain untrusted repo_path under $HOME and confirm
267
+ # it is a real git repo BEFORE any git subprocess runs with it as cwd.
268
+ # Blocks RCE via attacker-controlled .git config/hooks (core.fsmonitor,
269
+ # core.hooksPath) that git would otherwise execute in a hostile repo.
270
+ if repo_path:
271
+ try:
272
+ repo = safe_resolve(Path.home(), repo_path)
273
+ except PathTraversalError as exc:
274
+ return _error_response(f"Invalid repo_path: {exc}")
275
+ if not (repo / ".git").exists():
276
+ return _error_response(
277
+ f"Not a git repository (no .git dir): {repo_path}"
278
+ )
279
+ else:
280
+ repo = svc.config.repo_root
281
+
253
282
  files_list = [
254
283
  f.strip() for f in changed_files.split(",") if f.strip()
255
284
  ] if changed_files else []
@@ -258,11 +287,22 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
258
287
  # Auto-detect via git
259
288
  try:
260
289
  import subprocess
261
- repo = Path(repo_path) if repo_path else svc.config.repo_root
290
+ # repo is the safe_resolve-contained path computed above.
291
+ # Harden git: disable hooks + fsmonitor + system config so a
292
+ # hostile repo cannot execute code via the git invocation (B1).
262
293
  result = subprocess.run(
263
- ["git", "diff", "--name-only", "HEAD~1", "HEAD"],
294
+ [
295
+ "git", "-c", "core.hooksPath=/dev/null",
296
+ "-c", "core.fsmonitor=",
297
+ "diff", "--name-only", "HEAD~1", "HEAD",
298
+ ],
264
299
  capture_output=True, text=True, timeout=30,
265
300
  cwd=str(repo),
301
+ env={
302
+ **os.environ,
303
+ "GIT_CONFIG_NOSYSTEM": "1",
304
+ "GIT_TERMINAL_PROMPT": "0",
305
+ },
266
306
  )
267
307
  files_list = [
268
308
  f.strip() for f in result.stdout.strip().split("\n")
@@ -289,13 +329,19 @@ def register_code_graph_tools(server, get_engine: Callable) -> None:
289
329
  config = svc.config
290
330
  parser = CodeParser(config)
291
331
  store = GraphStore(db)
292
- repo = Path(repo_path) if repo_path else config.repo_root
332
+ # repo already contained via safe_resolve above (B1).
293
333
 
294
334
  nodes_before = db.get_node_count()
295
335
  edges_before = db.get_edge_count()
296
336
 
297
337
  for fp in files_list:
298
- full = repo / fp
338
+ # fp comes from caller-supplied changed_files; contain it under
339
+ # the (already safe-resolved) repo so "../../.ssh/id_rsa" cannot
340
+ # be read into the graph.
341
+ try:
342
+ full = safe_resolve(repo, fp)
343
+ except Exception:
344
+ continue
299
345
  if not full.exists():
300
346
  store.remove_file(fp)
301
347
  continue
@@ -284,10 +284,11 @@ def register_core_tools(server, get_engine: Callable) -> None:
284
284
  session_id: str = "", fast: bool = False,
285
285
  include_global: bool | None = None,
286
286
  include_shared: bool | None = None,
287
+ window: str = "",
287
288
  ) -> dict:
288
289
  """Search memories through hybrid retrieval, RRF fusion, and reranking.
289
290
 
290
- S9-DASH-02: optional ``session_id`` threads through to the
291
+ Optional ``session_id`` threads through to the
291
292
  engine's outcome-queue so PostToolUse / Stop hooks can attach
292
293
  engagement signals to this recall. Claude Code should pass its
293
294
  ``CLAUDE_SESSION_ID``. Omitting it degrades to "no closed-loop
@@ -297,6 +298,11 @@ def register_core_tools(server, get_engine: Callable) -> None:
297
298
  scopes participate in retrieval. Leave them unset (``None``) to use the
298
299
  configured default — shared memory is OPT-IN, so by default recall
299
300
  returns only this profile's own facts. Pass ``True`` to opt in per call.
301
+
302
+ Time window: optional ``window`` restricts results to a event-time
303
+ range. Accepts a relative span (``"24h"``, ``"7d"``, ``"30d"``,
304
+ ``"1y"``) or an explicit range (``"2026-07-01..2026-07-31"``). Empty =
305
+ no time filter.
300
306
  """
301
307
  # v3.6.10: resolve "mcp_client" sentinel → URL path (HTTP) or env var (stdio)
302
308
  if agent_id == "mcp_client":
@@ -353,6 +359,7 @@ def register_core_tools(server, get_engine: Callable) -> None:
353
359
  fast=bool(fast),
354
360
  include_global=include_global,
355
361
  include_shared=include_shared,
362
+ window=window or None,
356
363
  )
357
364
  if result.get("ok"):
358
365
  # Record implicit feedback: every returned result is a recall_hit
@@ -98,11 +98,14 @@ def register_evolution_tools(server, get_engine: Callable) -> None:
98
98
  engine = get_engine()
99
99
  profile_id = engine.profile_id if engine else "default"
100
100
 
101
- evolver._store.reset_cycle()
102
- outcome = evolver._process_candidate(candidate, profile_id)
101
+ evolver._store.reset_cycle(profile_id)
102
+ # audit-10 fix: go through evolve_candidate so the manual/MCP path
103
+ # runs under a budget cycle and honours the LLM-call / wall-time /
104
+ # per-day caps (previously _process_candidate ran uncapped here).
105
+ outcome = evolver.evolve_candidate(candidate, profile_id)
103
106
 
104
107
  # Fetch the latest record for this skill to return details
105
- recent = evolver._store.get_skill_history(skill_name, limit=1)
108
+ recent = evolver._store.get_skill_history(skill_name, profile_id, limit=1)
106
109
  record_info = {}
107
110
  if recent:
108
111
  r = recent[0]
@@ -0,0 +1,300 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """SLM v3.8.0 — MCP bounded-loop tools.
6
+
7
+ Exposes the ``superlocalmemory.loops`` engine over MCP so an agent connected
8
+ via MCP — not only the ``slm loop`` CLI or the ``/slm-loop`` command — can run
9
+ a gated, bounded loop and inspect its durable ledger. Bounded loops therefore
10
+ ship on three surfaces: CLI, plugin command, and MCP.
11
+
12
+ The loop's one invariant holds identically here: an INDEPENDENT gate decides
13
+ when the loop is done, never the agent's own claim. Over MCP the gate is an SLM
14
+ *recall*: the loop converges the first lap a memory matching ``gate_query``
15
+ becomes retrievable with confidence (the "verification lives in memory" model
16
+ the engine is designed around). That makes ``slm_loop_run`` a safe, shell-free
17
+ multi-agent coordination primitive — one agent waits, under strict bounds, for
18
+ a memory another agent will write into shared SLM.
19
+
20
+ Three tools:
21
+ * ``slm_loop_run`` — run one bounded, gate-verified loop to a terminal
22
+ outcome. Blocks (polling the gate) until the gate
23
+ passes or a bound trips. Every lap is persisted to
24
+ SLM memory (tag ``loop:<name>``) and shows on the
25
+ dashboard.
26
+ * ``slm_loop_history`` — list recorded runs for a loop name (read-only).
27
+ * ``slm_loop_show`` — show every lap of one run (read-only).
28
+
29
+ Fail-open: every tool body returns a dict; internal errors surface as
30
+ ``ok: False`` with a message, never a raised exception.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import asyncio
36
+ import logging
37
+ import time
38
+ from typing import Any, Callable
39
+
40
+ from mcp.types import ToolAnnotations
41
+
42
+ from superlocalmemory.loops import (
43
+ Bounds,
44
+ LapResult,
45
+ Verdict,
46
+ engine_backed_ledger,
47
+ run_bounded_loop,
48
+ )
49
+
50
+ logger = logging.getLogger("slm.mcp.tools_loops")
51
+
52
+ # ─── Exported tool name list (used by server.py + tests) ─────────────────────
53
+
54
+ _LOOP_TOOL_NAMES = (
55
+ "slm_loop_run",
56
+ "slm_loop_history",
57
+ "slm_loop_show",
58
+ )
59
+
60
+ # ─── Hard caps (a loop tool must never hang the daemon or spin unbounded) ────
61
+
62
+ _MAX_ITERATIONS = 200
63
+ _MAX_WALLCLOCK_S = 120.0
64
+ _MIN_POLL_S = 0.25
65
+ _MAX_NAME_CHARS = 128
66
+ _MAX_QUERY_CHARS = 2000
67
+
68
+
69
+ def _top_score(resp: Any) -> float:
70
+ """Highest result score in a RecallResponse (0.0 when there are none)."""
71
+ best = 0.0
72
+ for r in getattr(resp, "results", None) or []:
73
+ s = getattr(r, "score", None)
74
+ if s is None:
75
+ s = getattr(r, "relevance_score", 0.0) or 0.0
76
+ try:
77
+ best = max(best, float(s))
78
+ except (TypeError, ValueError):
79
+ continue
80
+ return best
81
+
82
+
83
+ def register_loop_tools(server, get_engine: Callable) -> None:
84
+ """Register the 3 bounded-loop tools on *server*.
85
+
86
+ *server* is duck-typed: must support the ``@server.tool()`` decorator.
87
+ Compatible with FastMCP, _FilteredServer, and the test mock server.
88
+ """
89
+
90
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False))
91
+ async def slm_loop_run(
92
+ name: str,
93
+ gate_query: str,
94
+ gate_min_score: float = 0.0,
95
+ max_iterations: int = 20,
96
+ max_wallclock_s: float = 15.0,
97
+ poll_interval_s: float = 1.0,
98
+ max_tokens: int = 0,
99
+ no_progress_window: int = 0,
100
+ ) -> dict:
101
+ """Run one bounded loop that finishes only when an INDEPENDENT gate passes.
102
+
103
+ The gate is an SLM recall of ``gate_query``: the loop converges on the
104
+ first lap that recall returns a confident match scoring at least
105
+ ``gate_min_score``. The agent cannot end the loop by asserting it is
106
+ done — only the gate can. The call BLOCKS (polling every
107
+ ``poll_interval_s``) until the gate passes or a bound trips, then
108
+ returns the outcome. Every lap is written to SLM memory (tag
109
+ ``loop:<name>``) and is visible on the dashboard.
110
+
111
+ Use it to wait, under strict bounds, for a verification or coordination
112
+ condition to hold — e.g. for another agent to record a "build passed"
113
+ memory in shared SLM.
114
+
115
+ Args:
116
+ name: Loop name, also the memory tag. 1–128 chars.
117
+ gate_query: Recall query the independent gate checks each lap.
118
+ gate_min_score: Minimum top-result score to pass (0.0 = any
119
+ confident hit above the evidence floor).
120
+ max_iterations: Hard cap on laps (1–200).
121
+ max_wallclock_s: Hard cap on wall-clock seconds (0 disables; capped at 120).
122
+ poll_interval_s: Seconds to wait between laps (minimum 0.25).
123
+ max_tokens: Optional token budget (0 disables).
124
+ no_progress_window: Halt after this many consecutive no-change laps
125
+ (0 disables). A pure watcher never "changes", so leave at 0
126
+ unless the runner reports progress.
127
+ """
128
+ try:
129
+ name = (name or "").strip()
130
+ if not name or len(name) > _MAX_NAME_CHARS:
131
+ return {"ok": False, "error": f"name must be 1–{_MAX_NAME_CHARS} chars"}
132
+ gate_query = (gate_query or "").strip()
133
+ if not gate_query or len(gate_query) > _MAX_QUERY_CHARS:
134
+ return {
135
+ "ok": False,
136
+ "error": f"gate_query must be 1–{_MAX_QUERY_CHARS} chars",
137
+ }
138
+
139
+ iters = max(1, min(int(max_iterations), _MAX_ITERATIONS))
140
+ wall = (
141
+ min(float(max_wallclock_s), _MAX_WALLCLOCK_S)
142
+ if max_wallclock_s and float(max_wallclock_s) > 0
143
+ else None
144
+ )
145
+ poll = max(float(poll_interval_s), _MIN_POLL_S)
146
+ tok = int(max_tokens) if max_tokens and int(max_tokens) > 0 else None
147
+ # 0 = disabled (a pure watcher never "changes", so no-progress must
148
+ # be off by default or it would halt before the gate can pass).
149
+ npw = (
150
+ int(no_progress_window)
151
+ if no_progress_window and int(no_progress_window) > 0
152
+ else 0
153
+ )
154
+ min_score = float(gate_min_score)
155
+
156
+ engine = get_engine()
157
+
158
+ def gate(lap: int) -> Verdict:
159
+ resp = engine.recall(gate_query, limit=3, fast=True)
160
+ results = getattr(resp, "results", None) or []
161
+ floored = bool(getattr(resp, "no_confident_match", False))
162
+ top = _top_score(resp)
163
+ passed = bool(results) and not floored and top >= min_score
164
+ return Verdict(
165
+ passed,
166
+ f"recall '{gate_query[:48]}': hits={len(results)} "
167
+ f"top={top:.3f} floor={floored}",
168
+ )
169
+
170
+ def runner(lap: int) -> LapResult:
171
+ # Watcher lap: no work of our own. Give the gate condition time
172
+ # to become true (e.g. another agent writing a memory) between
173
+ # polls. Bounded by poll interval so the wall-clock / iteration
174
+ # caps stay meaningful. The first lap checks immediately.
175
+ if lap > 1:
176
+ time.sleep(poll)
177
+ return LapResult(changed=False, tokens=0)
178
+
179
+ ledger = engine_backed_ledger(engine)
180
+
181
+ # The loop blocks (sleeps between laps); run it off the event loop.
182
+ # The engine's per-call WAL connection model makes this thread-safe.
183
+ outcome = await asyncio.to_thread(
184
+ run_bounded_loop,
185
+ name,
186
+ bounds=Bounds(
187
+ max_iterations=iters,
188
+ max_tokens=tok,
189
+ max_wallclock_s=wall,
190
+ no_progress_window=npw,
191
+ ),
192
+ runner=runner,
193
+ gate=gate,
194
+ ledger=ledger,
195
+ )
196
+ laps = await asyncio.to_thread(ledger.laps, outcome.run_id)
197
+ return {
198
+ "ok": True,
199
+ "status": outcome.status.value,
200
+ "reason": outcome.reason,
201
+ "passed": bool(outcome.ok),
202
+ "laps": outcome.laps,
203
+ "run_id": outcome.run_id,
204
+ "ledger": [
205
+ {
206
+ "lap": e.lap,
207
+ "decision": e.decision,
208
+ "passed": e.passed,
209
+ "detail": e.detail,
210
+ }
211
+ for e in laps
212
+ ],
213
+ "note": (
214
+ "The gate — an independent SLM recall — decided this "
215
+ "outcome; the agent's own done-claim never terminates a loop."
216
+ ),
217
+ }
218
+ except Exception as exc:
219
+ logger.exception("slm_loop_run failed (fail-open)")
220
+ return {"ok": False, "error": str(exc)}
221
+
222
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
223
+ async def slm_loop_history(name: str, limit: int = 20) -> dict:
224
+ """List recorded bounded-loop runs for a loop name, newest laps summarised.
225
+
226
+ Reads the durable SLM-backed ledger (the same rows ``slm loop history``
227
+ and the dashboard show). Read-only.
228
+
229
+ Args:
230
+ name: Loop name to list runs for.
231
+ limit: Maximum runs to return (1–200).
232
+ """
233
+ try:
234
+ name = (name or "").strip()
235
+ if not name:
236
+ return {"ok": False, "error": "name is required"}
237
+ lim = max(1, min(int(limit), 200))
238
+ engine = get_engine()
239
+ ledger = engine_backed_ledger(engine)
240
+
241
+ def _collect() -> list[dict]:
242
+ run_ids = ledger.runs(name)[:lim]
243
+ rows: list[dict] = []
244
+ for rid in run_ids:
245
+ laps = ledger.laps(rid)
246
+ last = laps[-1] if laps else None
247
+ rows.append(
248
+ {
249
+ "run_id": rid,
250
+ "laps": len(laps),
251
+ "final": last.decision if last else "unknown",
252
+ "ts": last.ts if last else "",
253
+ }
254
+ )
255
+ return rows
256
+
257
+ rows = await asyncio.to_thread(_collect)
258
+ return {"ok": True, "name": name, "count": len(rows), "runs": rows}
259
+ except Exception as exc:
260
+ logger.exception("slm_loop_history failed (fail-open)")
261
+ return {"ok": False, "error": str(exc)}
262
+
263
+ @server.tool(annotations=ToolAnnotations(readOnlyHint=True))
264
+ async def slm_loop_show(run_id: str, limit: int = 200) -> dict:
265
+ """Show every lap of one bounded-loop run, in order, from SLM memory.
266
+
267
+ Read-only. Each lap carries the gate verdict plus the agent's recorded
268
+ (advisory, never loop-terminating) done-claim.
269
+
270
+ Args:
271
+ run_id: Run identifier returned by ``slm_loop_run``.
272
+ limit: Maximum laps to return (1–1000).
273
+ """
274
+ try:
275
+ run_id = (run_id or "").strip()
276
+ if not run_id:
277
+ return {"ok": False, "error": "run_id is required"}
278
+ lim = max(1, min(int(limit), 1000))
279
+ engine = get_engine()
280
+ ledger = engine_backed_ledger(engine)
281
+
282
+ def _collect() -> list[dict]:
283
+ return [
284
+ {
285
+ "lap": e.lap,
286
+ "ts": e.ts,
287
+ "decision": e.decision,
288
+ "passed": e.passed,
289
+ "detail": e.detail,
290
+ "agent_claimed_done": e.agent_claimed_done,
291
+ "tokens": e.budget.get("tokens", 0),
292
+ }
293
+ for e in ledger.laps(run_id)[:lim]
294
+ ]
295
+
296
+ laps = await asyncio.to_thread(_collect)
297
+ return {"ok": True, "run_id": run_id, "count": len(laps), "laps": laps}
298
+ except Exception as exc:
299
+ logger.exception("slm_loop_show failed (fail-open)")
300
+ return {"ok": False, "error": str(exc)}