superlocalmemory 3.7.8 → 3.8.1

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 (280) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +129 -0
  4. package/README.md +205 -123
  5. package/package.json +12 -3
  6. package/plugin/.claude-plugin/plugin.json +2 -3
  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/rules/AGENTS.md +57 -18
  30. package/plugin-src/skills/slm-cache/SKILL.md +9 -1
  31. package/plugin-src/skills/slm-compress/SKILL.md +8 -1
  32. package/plugin-src/skills/slm-graph/SKILL.md +17 -3
  33. package/plugin-src/skills/slm-recall/SKILL.md +46 -10
  34. package/plugin-src/skills/slm-remember/SKILL.md +48 -1
  35. package/plugin-src/skills/slm-session/SKILL.md +24 -1
  36. package/plugin-src/skills/slm-status/SKILL.md +18 -1
  37. package/pyproject.toml +2 -1
  38. package/scripts/postinstall/validation.js +2 -0
  39. package/scripts/postinstall-interactive.js +74 -2
  40. package/src/superlocalmemory/__init__.py +1 -1
  41. package/src/superlocalmemory/access/__init__.py +3 -0
  42. package/src/superlocalmemory/access/rbac.py +477 -0
  43. package/src/superlocalmemory/cli/commands.py +228 -17
  44. package/src/superlocalmemory/cli/compress_cmd.py +17 -7
  45. package/src/superlocalmemory/cli/daemon.py +7 -0
  46. package/src/superlocalmemory/cli/loop_cmd.py +187 -0
  47. package/src/superlocalmemory/cli/main.py +49 -8
  48. package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
  49. package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
  50. package/src/superlocalmemory/cli/pending_store.py +49 -13
  51. package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
  52. package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
  53. package/src/superlocalmemory/cli/setup_wizard.py +22 -13
  54. package/src/superlocalmemory/cli/version_banner.py +17 -3
  55. package/src/superlocalmemory/compliance/audit.py +6 -0
  56. package/src/superlocalmemory/compliance/gdpr.py +128 -138
  57. package/src/superlocalmemory/compliance/retention.py +176 -45
  58. package/src/superlocalmemory/core/backend_orchestrator.py +23 -59
  59. package/src/superlocalmemory/core/community_summary.py +267 -0
  60. package/src/superlocalmemory/core/config.py +216 -3
  61. package/src/superlocalmemory/core/consolidation_engine.py +95 -22
  62. package/src/superlocalmemory/core/context_cache.py +61 -18
  63. package/src/superlocalmemory/core/embedding_worker.py +21 -7
  64. package/src/superlocalmemory/core/embeddings.py +131 -46
  65. package/src/superlocalmemory/core/engine.py +41 -22
  66. package/src/superlocalmemory/core/engine_ingestion.py +359 -43
  67. package/src/superlocalmemory/core/engine_wiring.py +13 -0
  68. package/src/superlocalmemory/core/entity_community.py +178 -0
  69. package/src/superlocalmemory/core/graph_analyzer.py +39 -2
  70. package/src/superlocalmemory/core/graph_pruner.py +13 -8
  71. package/src/superlocalmemory/core/ingestion_command.py +134 -25
  72. package/src/superlocalmemory/core/injection.py +12 -7
  73. package/src/superlocalmemory/core/key_expander.py +138 -0
  74. package/src/superlocalmemory/core/maintenance.py +23 -0
  75. package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
  76. package/src/superlocalmemory/core/modes.py +1 -1
  77. package/src/superlocalmemory/core/mutations.py +2 -2
  78. package/src/superlocalmemory/core/pii.py +105 -0
  79. package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
  80. package/src/superlocalmemory/core/recall_pipeline.py +7 -3
  81. package/src/superlocalmemory/core/recall_worker.py +20 -6
  82. package/src/superlocalmemory/core/scale_engine.py +60 -1
  83. package/src/superlocalmemory/core/security_primitives.py +40 -2
  84. package/src/superlocalmemory/core/store_pipeline.py +186 -29
  85. package/src/superlocalmemory/core/worker_pool.py +21 -6
  86. package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
  87. package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
  88. package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
  89. package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
  90. package/src/superlocalmemory/evolution/evolution_store.py +122 -45
  91. package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
  92. package/src/superlocalmemory/evolution/model_selection.py +160 -0
  93. package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
  94. package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
  95. package/src/superlocalmemory/evolution/triggers.py +22 -13
  96. package/src/superlocalmemory/graph/cozo_backend.py +43 -20
  97. package/src/superlocalmemory/hooks/adapter_base.py +5 -1
  98. package/src/superlocalmemory/hooks/auto_recall.py +13 -1
  99. package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
  100. package/src/superlocalmemory/hooks/codex_assets.py +64 -5
  101. package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
  102. package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
  103. package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
  104. package/src/superlocalmemory/hooks/portable_kit.py +148 -3
  105. package/src/superlocalmemory/infra/backup.py +12 -1
  106. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  107. package/src/superlocalmemory/infra/data_root.py +43 -4
  108. package/src/superlocalmemory/infra/event_bus.py +107 -24
  109. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  110. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  111. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  112. package/src/superlocalmemory/learning/cross_project.py +28 -19
  113. package/src/superlocalmemory/learning/model_rollback.py +3 -0
  114. package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
  115. package/src/superlocalmemory/learning/reward.py +50 -0
  116. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  117. package/src/superlocalmemory/learning/source_quality.py +523 -1
  118. package/src/superlocalmemory/loops/__init__.py +56 -0
  119. package/src/superlocalmemory/loops/budget.py +58 -0
  120. package/src/superlocalmemory/loops/engine.py +164 -0
  121. package/src/superlocalmemory/loops/ledger.py +263 -0
  122. package/src/superlocalmemory/loops/models.py +152 -0
  123. package/src/superlocalmemory/loops/rules.py +52 -0
  124. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  125. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  126. package/src/superlocalmemory/mcp/profiles.py +103 -0
  127. package/src/superlocalmemory/mcp/server.py +32 -79
  128. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  129. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  130. package/src/superlocalmemory/mcp/tools_core.py +12 -4
  131. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  132. package/src/superlocalmemory/mcp/tools_learning.py +2 -2
  133. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  134. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  135. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  136. package/src/superlocalmemory/mesh/broker.py +237 -129
  137. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  138. package/src/superlocalmemory/optimize/NOTICE +1 -6
  139. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  140. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  141. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  142. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  143. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  144. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  145. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  146. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  147. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  148. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  149. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  150. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  151. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  152. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  153. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  154. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  155. package/src/superlocalmemory/retrieval/engine.py +221 -47
  156. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  157. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  158. package/src/superlocalmemory/retrieval/reranker.py +3 -4
  159. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  160. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  161. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  162. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  163. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  164. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  165. package/src/superlocalmemory/server/api.py +4 -4
  166. package/src/superlocalmemory/server/config_file.py +90 -0
  167. package/src/superlocalmemory/server/origin.py +50 -0
  168. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  169. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  170. package/src/superlocalmemory/server/recall_health.py +24 -3
  171. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  172. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  173. package/src/superlocalmemory/server/routes/agents.py +128 -38
  174. package/src/superlocalmemory/server/routes/backup.py +317 -70
  175. package/src/superlocalmemory/server/routes/behavioral.py +349 -71
  176. package/src/superlocalmemory/server/routes/brain.py +69 -12
  177. package/src/superlocalmemory/server/routes/chat.py +10 -5
  178. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  179. package/src/superlocalmemory/server/routes/config_api.py +438 -0
  180. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  181. package/src/superlocalmemory/server/routes/entity.py +108 -26
  182. package/src/superlocalmemory/server/routes/events.py +24 -8
  183. package/src/superlocalmemory/server/routes/evolution.py +189 -68
  184. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  185. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  186. package/src/superlocalmemory/server/routes/insights.py +3 -3
  187. package/src/superlocalmemory/server/routes/learning.py +289 -118
  188. package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
  189. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  190. package/src/superlocalmemory/server/routes/memories.py +182 -57
  191. package/src/superlocalmemory/server/routes/mesh.py +200 -31
  192. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  193. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  194. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  195. package/src/superlocalmemory/server/routes/ratelimit.py +132 -0
  196. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  197. package/src/superlocalmemory/server/routes/stats.py +103 -158
  198. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  199. package/src/superlocalmemory/server/routes/token.py +3 -13
  200. package/src/superlocalmemory/server/routes/v3_api.py +247 -89
  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 +827 -72
  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 +132 -1
  208. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -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/migrations/M028_fact_entity_associations.py +270 -0
  217. package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
  218. package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
  219. package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
  220. package/src/superlocalmemory/storage/models.py +4 -0
  221. package/src/superlocalmemory/storage/schema.py +136 -1
  222. package/src/superlocalmemory/storage/schema_v32.py +2 -0
  223. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  224. package/src/superlocalmemory/storage/schema_v347.py +4 -0
  225. package/src/superlocalmemory/trust/gate.py +49 -8
  226. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  227. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  228. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  229. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  230. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  231. package/src/superlocalmemory/ui/favicon.svg +35 -4
  232. package/src/superlocalmemory/ui/index.html +303 -173
  233. package/src/superlocalmemory/ui/js/brain.js +5 -20
  234. package/src/superlocalmemory/ui/js/core.js +100 -41
  235. package/src/superlocalmemory/ui/js/dashboard.js +403 -65
  236. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  237. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  238. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  239. package/src/superlocalmemory/ui/js/memories.js +15 -4
  240. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  241. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  242. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  243. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  244. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  245. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  246. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  247. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  248. package/src/superlocalmemory/ui/js/od-backup.js +871 -0
  249. package/src/superlocalmemory/ui/js/od-brain.js +816 -0
  250. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  251. package/src/superlocalmemory/ui/js/od-graph.js +600 -0
  252. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  253. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  254. package/src/superlocalmemory/ui/js/od-memories.js +929 -0
  255. package/src/superlocalmemory/ui/js/od-mesh.js +553 -0
  256. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  257. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  258. package/src/superlocalmemory/ui/js/od-settings.js +1107 -0
  259. package/src/superlocalmemory/ui/js/od-shell.js +809 -0
  260. package/src/superlocalmemory/ui/js/od-skills.js +600 -0
  261. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  262. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  263. package/src/superlocalmemory/ui/js/settings.js +17 -3
  264. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  265. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  266. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  267. package/plugin-src/.mcp.json +0 -12
  268. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  269. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  270. package/plugin-src/hooks/.gitkeep +0 -0
  271. package/plugin-src/hooks/hooks.json +0 -23
  272. package/plugin-src/manifest.json +0 -25
  273. package/plugin-src/requirements.txt +0 -1
  274. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  275. package/plugin-src/scripts/ensure-venv.bat +0 -122
  276. package/plugin-src/scripts/ensure-venv.sh +0 -105
  277. package/plugin-src/scripts/slm-launch +0 -23
  278. package/plugin-src/scripts/slm-launch.bat +0 -23
  279. package/plugin-src/settings.json +0 -16
  280. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -0,0 +1,438 @@
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
+ """Dashboard config endpoints — storage, daemon, mesh, trust, forgetting.
6
+
7
+ Each section provides GET (read current) and PUT (validate + persist) routes.
8
+ Writes use the same direct-JSON approach as the evolution config endpoint:
9
+ 1. Read config.json as a raw dict (fail-open → defaults when absent/corrupt).
10
+ 2. Update only the targeted keys; all other keys — including 'mode' — survive.
11
+ 3. Atomic write via a temp file + os.replace (no torn writes).
12
+
13
+ Auth: follows the same pattern as the auto-capture/auto-recall/auto-invoke
14
+ config endpoints — no route-level auth guard. Write-identity is enforced by
15
+ the middleware layer wired in unified_daemon.py / ui.py for non-localhost
16
+ callers. Tests use a bare FastAPI app (no middleware) and monkeypatch
17
+ MEMORY_DIR to a tmp_path.
18
+
19
+ Restart-required semantics:
20
+ - graph_backend / vector_backend: changes take effect only on daemon restart.
21
+ - daemon_port / daemon_legacy_port: changes take effect only on daemon restart.
22
+ - mesh, trust, and forgetting are persisted safely but require restart
23
+ because no complete supported worker-rebind transaction exists for them.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import logging
29
+ from pathlib import Path
30
+ from typing import Annotated, Optional
31
+
32
+ from fastapi import APIRouter, Request
33
+ from fastapi.responses import JSONResponse
34
+ from pydantic import BaseModel, ConfigDict, Field, StrictBool
35
+
36
+ from superlocalmemory.server.config_file import read_config, update_config
37
+ from superlocalmemory.server.routes.helpers import MEMORY_DIR
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+ router = APIRouter(prefix="/api/v3", tags=["config"])
42
+
43
+
44
+ def _require_admin(request: Request) -> None:
45
+ """SEC-H-01: system-configuration mutations are admin-only (MANAGE).
46
+
47
+ Machine auth (loopback / credential) is enforced by the daemon middleware;
48
+ this adds the RBAC layer so a logged-in non-admin (viewer/member) in company
49
+ mode cannot change the daemon port, swap the LLM key, or alter the forgetting
50
+ curve. The machine owner keeps MANAGE (personal mode is unaffected). Call
51
+ this BEFORE the handler's try/except so the 401/403 is not swallowed into a
52
+ 500.
53
+ """
54
+ from superlocalmemory.server.rbac_enforce import require_manage
55
+ require_manage(request)
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Allowed backend values
59
+ # ---------------------------------------------------------------------------
60
+
61
+ _GRAPH_BACKENDS = frozenset({"auto", "sqlite", "cozo"})
62
+ _VECTOR_BACKENDS = frozenset({"auto", "lancedb", "sqlite-vec"})
63
+
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # Internal helpers
67
+ # ---------------------------------------------------------------------------
68
+
69
+
70
+ def _config_path() -> Path:
71
+ """Return the config.json path, resolved from the active MEMORY_DIR."""
72
+ return MEMORY_DIR / "config.json"
73
+
74
+
75
+ def _read_config() -> dict:
76
+ """Read one coherent config snapshot."""
77
+ p = _config_path()
78
+ try:
79
+ return read_config(p)
80
+ except (ValueError, OSError) as exc:
81
+ logger.warning("config_api: could not read config.json: %s", exc)
82
+ raise
83
+
84
+
85
+ def _update_config(mutator) -> dict:
86
+ """Run one interprocess-locked read/modify/replace transaction."""
87
+ return update_config(_config_path(), mutator)
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # Pydantic request models (extra="forbid" → 422 on unknown keys)
92
+ # ---------------------------------------------------------------------------
93
+
94
+
95
+ class StorageConfigUpdate(BaseModel):
96
+ model_config = ConfigDict(extra="forbid")
97
+
98
+ graph_backend: Optional[Annotated[str, Field(pattern=r"^(auto|sqlite|cozo)$")]] = None
99
+ vector_backend: Optional[Annotated[str, Field(pattern=r"^(auto|lancedb|sqlite-vec)$")]] = None
100
+
101
+
102
+ class DaemonConfigUpdate(BaseModel):
103
+ model_config = ConfigDict(extra="forbid")
104
+
105
+ idle_timeout: Optional[int] = Field(None, ge=0)
106
+ port: Optional[int] = Field(None, ge=1, le=65535)
107
+ legacy_port: Optional[int] = Field(None, ge=1, le=65535)
108
+ enable_legacy_port: Optional[StrictBool] = None
109
+
110
+
111
+ class MeshConfigUpdate(BaseModel):
112
+ model_config = ConfigDict(extra="forbid")
113
+
114
+ enabled: StrictBool
115
+
116
+
117
+ class TrustConfigUpdate(BaseModel):
118
+ model_config = ConfigDict(extra="forbid")
119
+
120
+ use_trust_weighting: Optional[StrictBool] = None
121
+ trust_first_party: Optional[StrictBool] = None
122
+ promotion_min_trust: Optional[float] = Field(None, ge=0.0, le=1.0)
123
+
124
+
125
+ class ForgettingConfigUpdate(BaseModel):
126
+ model_config = ConfigDict(extra="forbid")
127
+
128
+ enabled: Optional[StrictBool] = None
129
+ alpha: Optional[float] = Field(None, gt=0.0)
130
+ beta: Optional[float] = Field(None, gt=0.0)
131
+ gamma: Optional[float] = Field(None, gt=0.0)
132
+ delta: Optional[float] = Field(None, gt=0.0)
133
+ min_strength: Optional[float] = Field(None, gt=0.0)
134
+ max_strength: Optional[float] = Field(None, gt=0.0)
135
+ archive_threshold: Optional[float] = Field(None, ge=0.0, le=1.0)
136
+ forget_threshold: Optional[float] = Field(None, ge=0.0, le=1.0)
137
+ learning_rate: Optional[float] = Field(None, gt=0.0)
138
+ forgetting_drift_scale: Optional[float] = Field(None, gt=0.0)
139
+ trust_kappa: Optional[float] = Field(None, gt=0.0)
140
+ scheduler_interval_minutes: Optional[int] = Field(None, ge=1)
141
+ core_memory_immune: Optional[StrictBool] = None
142
+
143
+
144
+ # ---------------------------------------------------------------------------
145
+ # Default value constants (mirrors ForgettingConfig dataclass defaults)
146
+ # ---------------------------------------------------------------------------
147
+
148
+ _FORGETTING_DEFAULTS: dict = {
149
+ "enabled": True,
150
+ "alpha": 2.0,
151
+ "beta": 1.5,
152
+ "gamma": 1.0,
153
+ "delta": 0.5,
154
+ "min_strength": 0.1,
155
+ "max_strength": 100.0,
156
+ "archive_threshold": 0.2,
157
+ "forget_threshold": 0.05,
158
+ "learning_rate": 1.0,
159
+ "forgetting_drift_scale": 0.5,
160
+ "trust_kappa": 2.0,
161
+ "scheduler_interval_minutes": 30,
162
+ "core_memory_immune": True,
163
+ }
164
+
165
+
166
+ # ---------------------------------------------------------------------------
167
+ # GET /api/v3/storage/config
168
+ # ---------------------------------------------------------------------------
169
+
170
+
171
+ @router.get("/storage/config")
172
+ def get_storage_config():
173
+ """Return current storage backend configuration.
174
+
175
+ base_dir is read-only — it is derived from the process namespace and
176
+ cannot be changed via this endpoint.
177
+ """
178
+ try:
179
+ data = _read_config()
180
+ return {
181
+ "graph_backend": data.get("graph_backend", "auto"),
182
+ "vector_backend": data.get("vector_backend", "auto"),
183
+ "base_dir": data.get("base_dir", str(MEMORY_DIR)),
184
+ }
185
+ except Exception:
186
+ logger.exception("get_storage_config failed")
187
+ return JSONResponse({"error": "Internal server error"}, status_code=500)
188
+
189
+
190
+ # ---------------------------------------------------------------------------
191
+ # PUT /api/v3/storage/config
192
+ # ---------------------------------------------------------------------------
193
+
194
+
195
+ @router.put("/storage/config")
196
+ def put_storage_config(request: Request, body: StorageConfigUpdate):
197
+ """Update graph_backend and/or vector_backend.
198
+
199
+ Both fields require a daemon restart to take effect.
200
+ Returns restart_required: true unconditionally.
201
+ """
202
+ _require_admin(request)
203
+ try:
204
+ def mutate(data: dict) -> None:
205
+ if body.graph_backend is not None:
206
+ data["graph_backend"] = body.graph_backend
207
+ if body.vector_backend is not None:
208
+ data["vector_backend"] = body.vector_backend
209
+
210
+ data = _update_config(mutate)
211
+ return {
212
+ "graph_backend": data.get("graph_backend", "auto"),
213
+ "vector_backend": data.get("vector_backend", "auto"),
214
+ "base_dir": data.get("base_dir", str(MEMORY_DIR)),
215
+ "restart_required": True,
216
+ }
217
+ except Exception:
218
+ logger.exception("put_storage_config failed")
219
+ return JSONResponse({"error": "Internal server error"}, status_code=500)
220
+
221
+
222
+ # ---------------------------------------------------------------------------
223
+ # GET /api/v3/daemon/config
224
+ # ---------------------------------------------------------------------------
225
+
226
+
227
+ @router.get("/daemon/config")
228
+ def get_daemon_config():
229
+ """Return current daemon configuration."""
230
+ try:
231
+ data = _read_config()
232
+ return {
233
+ "idle_timeout": data.get("daemon_idle_timeout", 0),
234
+ "port": data.get("daemon_port", 8765),
235
+ "legacy_port": data.get("daemon_legacy_port", 8767),
236
+ "enable_legacy_port": data.get("daemon_enable_legacy_port", True),
237
+ }
238
+ except Exception:
239
+ logger.exception("get_daemon_config failed")
240
+ return JSONResponse({"error": "Internal server error"}, status_code=500)
241
+
242
+
243
+ # ---------------------------------------------------------------------------
244
+ # PUT /api/v3/daemon/config
245
+ # ---------------------------------------------------------------------------
246
+
247
+
248
+ @router.put("/daemon/config")
249
+ def put_daemon_config(request: Request, body: DaemonConfigUpdate):
250
+ """Update daemon configuration.
251
+
252
+ Port / legacy_port changes require a daemon restart.
253
+ restart_required is True if any port field is included in the request.
254
+ """
255
+ _require_admin(request)
256
+ try:
257
+ port_changed = False
258
+
259
+ def mutate(data: dict) -> None:
260
+ nonlocal port_changed
261
+ if body.idle_timeout is not None:
262
+ data["daemon_idle_timeout"] = body.idle_timeout
263
+ if body.port is not None:
264
+ data["daemon_port"] = body.port
265
+ port_changed = True
266
+ if body.legacy_port is not None:
267
+ data["daemon_legacy_port"] = body.legacy_port
268
+ port_changed = True
269
+ if body.enable_legacy_port is not None:
270
+ data["daemon_enable_legacy_port"] = body.enable_legacy_port
271
+
272
+ data = _update_config(mutate)
273
+ return {
274
+ "idle_timeout": data.get("daemon_idle_timeout", 0),
275
+ "port": data.get("daemon_port", 8765),
276
+ "legacy_port": data.get("daemon_legacy_port", 8767),
277
+ "enable_legacy_port": data.get("daemon_enable_legacy_port", True),
278
+ "restart_required": port_changed,
279
+ }
280
+ except Exception:
281
+ logger.exception("put_daemon_config failed")
282
+ return JSONResponse({"error": "Internal server error"}, status_code=500)
283
+
284
+
285
+ # ---------------------------------------------------------------------------
286
+ # GET /api/v3/mesh/config
287
+ # ---------------------------------------------------------------------------
288
+
289
+
290
+ @router.get("/mesh/config")
291
+ def get_mesh_config():
292
+ """Return current mesh configuration."""
293
+ try:
294
+ data = _read_config()
295
+ return {"enabled": data.get("mesh_enabled", True)}
296
+ except Exception:
297
+ logger.exception("get_mesh_config failed")
298
+ return JSONResponse({"error": "Internal server error"}, status_code=500)
299
+
300
+
301
+ # ---------------------------------------------------------------------------
302
+ # PUT /api/v3/mesh/config
303
+ # ---------------------------------------------------------------------------
304
+
305
+
306
+ @router.put("/mesh/config")
307
+ def put_mesh_config(request: Request, body: MeshConfigUpdate):
308
+ """Persist mesh state; restart is required to rebuild the mesh worker."""
309
+ _require_admin(request)
310
+ try:
311
+ _update_config(
312
+ lambda data: data.update({"mesh_enabled": body.enabled}),
313
+ )
314
+ return {"enabled": body.enabled, "restart_required": True}
315
+ except Exception:
316
+ logger.exception("put_mesh_config failed")
317
+ return JSONResponse({"error": "Internal server error"}, status_code=500)
318
+
319
+
320
+ # ---------------------------------------------------------------------------
321
+ # GET /api/v3/trust/config
322
+ # ---------------------------------------------------------------------------
323
+
324
+
325
+ @router.get("/trust/config")
326
+ def get_trust_config():
327
+ """Return current trust configuration.
328
+
329
+ Fields are spread across three config sections:
330
+ - retrieval.use_trust_weighting (Bayesian trust in retrieval ranking)
331
+ - injection.trust_first_party (framing of injected context)
332
+ - consolidation.promotion_min_trust (min trust required for promotion)
333
+ """
334
+ try:
335
+ data = _read_config()
336
+ retrieval = data.get("retrieval", {})
337
+ injection = data.get("injection", {})
338
+ consolidation = data.get("consolidation", {})
339
+ return {
340
+ "use_trust_weighting": retrieval.get("use_trust_weighting", True),
341
+ "trust_first_party": injection.get("trust_first_party", False),
342
+ "promotion_min_trust": consolidation.get("promotion_min_trust", 0.5),
343
+ }
344
+ except Exception:
345
+ logger.exception("get_trust_config failed")
346
+ return JSONResponse({"error": "Internal server error"}, status_code=500)
347
+
348
+
349
+ # ---------------------------------------------------------------------------
350
+ # PUT /api/v3/trust/config
351
+ # ---------------------------------------------------------------------------
352
+
353
+
354
+ @router.put("/trust/config")
355
+ def put_trust_config(request: Request, body: TrustConfigUpdate):
356
+ """Update trust configuration.
357
+
358
+ Each field is stored in its canonical config.json sub-section.
359
+ Unrelated keys in those sub-sections are preserved.
360
+ """
361
+ _require_admin(request)
362
+ try:
363
+ def mutate(data: dict) -> None:
364
+ if body.use_trust_weighting is not None:
365
+ retrieval = data.setdefault("retrieval", {})
366
+ retrieval["use_trust_weighting"] = body.use_trust_weighting
367
+ if body.trust_first_party is not None:
368
+ injection = data.setdefault("injection", {})
369
+ injection["trust_first_party"] = body.trust_first_party
370
+ if body.promotion_min_trust is not None:
371
+ consolidation = data.setdefault("consolidation", {})
372
+ consolidation["promotion_min_trust"] = body.promotion_min_trust
373
+
374
+ data = _update_config(mutate)
375
+ retrieval = data.get("retrieval", {})
376
+ injection = data.get("injection", {})
377
+ consolidation = data.get("consolidation", {})
378
+ return {
379
+ "use_trust_weighting": retrieval.get("use_trust_weighting", True),
380
+ "trust_first_party": injection.get("trust_first_party", False),
381
+ "promotion_min_trust": consolidation.get("promotion_min_trust", 0.5),
382
+ "restart_required": True,
383
+ }
384
+ except Exception:
385
+ logger.exception("put_trust_config failed")
386
+ return JSONResponse({"error": "Internal server error"}, status_code=500)
387
+
388
+
389
+ # ---------------------------------------------------------------------------
390
+ # GET /api/v3/forgetting/config
391
+ # ---------------------------------------------------------------------------
392
+
393
+
394
+ @router.get("/forgetting/config")
395
+ def get_forgetting_config():
396
+ """Return all Ebbinghaus forgetting configuration fields."""
397
+ try:
398
+ data = _read_config()
399
+ stored = data.get("forgetting", {})
400
+ # Return defaults for any field not yet in config.json
401
+ result = {**_FORGETTING_DEFAULTS, **stored}
402
+ # Keep only known fields
403
+ return {k: result[k] for k in _FORGETTING_DEFAULTS}
404
+ except Exception:
405
+ logger.exception("get_forgetting_config failed")
406
+ return JSONResponse({"error": "Internal server error"}, status_code=500)
407
+
408
+
409
+ # ---------------------------------------------------------------------------
410
+ # PUT /api/v3/forgetting/config
411
+ # ---------------------------------------------------------------------------
412
+
413
+
414
+ @router.put("/forgetting/config")
415
+ def put_forgetting_config(request: Request, body: ForgettingConfigUpdate):
416
+ """Update Ebbinghaus forgetting configuration.
417
+
418
+ Only provided fields are changed; all other forgetting fields are
419
+ preserved. Changes take effect after the daemon restarts.
420
+ """
421
+ _require_admin(request)
422
+ try:
423
+ updates = body.model_dump(exclude_none=True)
424
+
425
+ def mutate(data: dict) -> None:
426
+ stored = data.get("forgetting", {})
427
+ merged = {**_FORGETTING_DEFAULTS, **stored, **updates}
428
+ data["forgetting"] = merged
429
+
430
+ data = _update_config(mutate)
431
+ merged = data["forgetting"]
432
+ return {
433
+ **{k: merged[k] for k in _FORGETTING_DEFAULTS},
434
+ "restart_required": True,
435
+ }
436
+ except Exception:
437
+ logger.exception("put_forgetting_config failed")
438
+ return JSONResponse({"error": "Internal server error"}, status_code=500)
@@ -27,6 +27,12 @@ from .helpers import (
27
27
 
28
28
  logger = logging.getLogger("superlocalmemory.routes.data_io")
29
29
 
30
+
31
+ def _internal_error(detail: str = "Internal server error") -> HTTPException:
32
+ """SEC-H-02: log full traceback server-side; return a generic message to the client."""
33
+ logger.exception("data_io route error")
34
+ return HTTPException(status_code=500, detail=detail)
35
+
30
36
  # WebSocket manager reference (set by ui_server.py at startup)
31
37
  ws_manager = None
32
38
 
@@ -35,11 +41,19 @@ router = APIRouter()
35
41
 
36
42
  @router.get("/api/export")
37
43
  async def export_memories(
44
+ request: Request,
38
45
  format: str = Query("json", pattern="^(json|jsonl|csv)$"),
39
46
  category: Optional[str] = None,
40
47
  project_name: Optional[str] = None,
41
48
  ):
42
49
  """Export memories as JSON, JSONL, or CSV."""
50
+ # Bulk data export. This GET is not covered by the mutation middleware and
51
+ # is reached both by a plain fetch and a top-level navigation, neither of
52
+ # which carries a credential header — so gate on the loopback-trusted
53
+ # mutation boundary: local owner allowed, remote uncredentialed fails closed.
54
+ from superlocalmemory.server.write_identity import require_http_mutation_actor
55
+ require_http_mutation_actor(request, getattr(request.app.state, "daemon_descriptor", None),
56
+ actor_kind="data-export")
43
57
  try:
44
58
  conn = get_db_connection()
45
59
  conn.row_factory = dict_factory
@@ -128,22 +142,29 @@ async def export_memories(
128
142
  },
129
143
  )
130
144
 
131
- except Exception as e:
132
- raise HTTPException(status_code=500, detail=f"Export error: {str(e)}")
145
+ except Exception:
146
+ raise _internal_error("Export error")
133
147
 
134
148
 
135
149
  @router.post("/api/import")
136
150
  async def import_memories(request: Request, file: UploadFile = File(...)):
137
151
  """Import memories from JSON file using V3 engine."""
138
152
  try:
139
- content = await file.read()
153
+ # Bound the upload so a huge file cannot OOM the daemon (read one byte
154
+ # past the cap to detect oversize without buffering the whole payload).
155
+ _MAX_IMPORT_BYTES = 50 * 1024 * 1024
156
+ content = await file.read(_MAX_IMPORT_BYTES + 1)
157
+ if len(content) > _MAX_IMPORT_BYTES:
158
+ raise HTTPException(status_code=413,
159
+ detail="Import file exceeds the 50 MB limit")
140
160
  if file.filename and file.filename.endswith('.gz'):
141
161
  content = gzip.decompress(content)
142
162
 
143
163
  try:
144
164
  data = json.loads(content)
145
- except json.JSONDecodeError as e:
146
- raise HTTPException(status_code=400, detail=f"Invalid JSON: {str(e)}")
165
+ except json.JSONDecodeError:
166
+ logger.warning("import: invalid JSON payload")
167
+ raise HTTPException(status_code=400, detail="Invalid JSON format")
147
168
 
148
169
  if isinstance(data, dict) and 'memories' in data:
149
170
  memories = data['memories']
@@ -224,7 +245,8 @@ async def import_memories(request: Request, file: UploadFile = File(...)):
224
245
  if "UNIQUE constraint failed" in str(e):
225
246
  skipped += 1
226
247
  else:
227
- errors.append(f"Memory {idx}: {str(e)}")
248
+ logger.warning("import: memory %d failed: %s", idx, e)
249
+ errors.append(f"Memory {idx}: import failed")
228
250
 
229
251
  return {
230
252
  "success": True, "imported_count": imported,
@@ -234,5 +256,5 @@ async def import_memories(request: Request, file: UploadFile = File(...)):
234
256
 
235
257
  except HTTPException:
236
258
  raise
237
- except Exception as e:
238
- raise HTTPException(status_code=500, detail=f"Import error: {str(e)}")
259
+ except Exception:
260
+ raise _internal_error("Import error")