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
@@ -0,0 +1,436 @@
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
+ - All other fields: hot-applied on next engine reconfigure cycle.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ import logging
29
+ import os
30
+ from pathlib import Path
31
+ from typing import Annotated, Literal, Optional
32
+
33
+ from fastapi import APIRouter, Request
34
+ from fastapi.responses import JSONResponse
35
+ from pydantic import BaseModel, ConfigDict, Field, StrictBool
36
+
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 config.json as a raw dict. Returns {} on missing/corrupt file."""
77
+ p = _config_path()
78
+ if not p.exists():
79
+ return {}
80
+ try:
81
+ return json.loads(p.read_text())
82
+ except (json.JSONDecodeError, OSError) as exc:
83
+ logger.warning("config_api: could not read config.json: %s", exc)
84
+ return {}
85
+
86
+
87
+ def _atomic_write(data: dict) -> None:
88
+ """Atomic config.json write: .tmp → os.replace."""
89
+ p = _config_path()
90
+ p.parent.mkdir(parents=True, exist_ok=True)
91
+ tmp = p.with_suffix(".json.tmp")
92
+ tmp.write_text(json.dumps(data, indent=2), encoding="utf-8")
93
+ os.replace(tmp, p)
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Pydantic request models (extra="forbid" → 422 on unknown keys)
98
+ # ---------------------------------------------------------------------------
99
+
100
+
101
+ class StorageConfigUpdate(BaseModel):
102
+ model_config = ConfigDict(extra="forbid")
103
+
104
+ graph_backend: Optional[Annotated[str, Field(pattern=r"^(auto|sqlite|cozo)$")]] = None
105
+ vector_backend: Optional[Annotated[str, Field(pattern=r"^(auto|lancedb|sqlite-vec)$")]] = None
106
+
107
+
108
+ class DaemonConfigUpdate(BaseModel):
109
+ model_config = ConfigDict(extra="forbid")
110
+
111
+ idle_timeout: Optional[int] = Field(None, ge=0)
112
+ port: Optional[int] = Field(None, ge=1, le=65535)
113
+ legacy_port: Optional[int] = Field(None, ge=1, le=65535)
114
+ enable_legacy_port: Optional[StrictBool] = None
115
+
116
+
117
+ class MeshConfigUpdate(BaseModel):
118
+ model_config = ConfigDict(extra="forbid")
119
+
120
+ enabled: StrictBool
121
+
122
+
123
+ class TrustConfigUpdate(BaseModel):
124
+ model_config = ConfigDict(extra="forbid")
125
+
126
+ use_trust_weighting: Optional[StrictBool] = None
127
+ trust_first_party: Optional[StrictBool] = None
128
+ promotion_min_trust: Optional[float] = Field(None, ge=0.0, le=1.0)
129
+
130
+
131
+ class ForgettingConfigUpdate(BaseModel):
132
+ model_config = ConfigDict(extra="forbid")
133
+
134
+ enabled: Optional[StrictBool] = None
135
+ alpha: Optional[float] = Field(None, gt=0.0)
136
+ beta: Optional[float] = Field(None, gt=0.0)
137
+ gamma: Optional[float] = Field(None, gt=0.0)
138
+ delta: Optional[float] = Field(None, gt=0.0)
139
+ min_strength: Optional[float] = Field(None, gt=0.0)
140
+ max_strength: Optional[float] = Field(None, gt=0.0)
141
+ archive_threshold: Optional[float] = Field(None, ge=0.0, le=1.0)
142
+ forget_threshold: Optional[float] = Field(None, ge=0.0, le=1.0)
143
+ learning_rate: Optional[float] = Field(None, gt=0.0)
144
+ forgetting_drift_scale: Optional[float] = Field(None, gt=0.0)
145
+ trust_kappa: Optional[float] = Field(None, gt=0.0)
146
+ scheduler_interval_minutes: Optional[int] = Field(None, ge=1)
147
+ core_memory_immune: Optional[StrictBool] = None
148
+
149
+
150
+ # ---------------------------------------------------------------------------
151
+ # Default value constants (mirrors ForgettingConfig dataclass defaults)
152
+ # ---------------------------------------------------------------------------
153
+
154
+ _FORGETTING_DEFAULTS: dict = {
155
+ "enabled": True,
156
+ "alpha": 2.0,
157
+ "beta": 1.5,
158
+ "gamma": 1.0,
159
+ "delta": 0.5,
160
+ "min_strength": 0.1,
161
+ "max_strength": 100.0,
162
+ "archive_threshold": 0.2,
163
+ "forget_threshold": 0.05,
164
+ "learning_rate": 1.0,
165
+ "forgetting_drift_scale": 0.5,
166
+ "trust_kappa": 2.0,
167
+ "scheduler_interval_minutes": 30,
168
+ "core_memory_immune": True,
169
+ }
170
+
171
+
172
+ # ---------------------------------------------------------------------------
173
+ # GET /api/v3/storage/config
174
+ # ---------------------------------------------------------------------------
175
+
176
+
177
+ @router.get("/storage/config")
178
+ async def get_storage_config():
179
+ """Return current storage backend configuration.
180
+
181
+ base_dir is read-only — it is derived from the process namespace and
182
+ cannot be changed via this endpoint.
183
+ """
184
+ try:
185
+ data = _read_config()
186
+ return {
187
+ "graph_backend": data.get("graph_backend", "auto"),
188
+ "vector_backend": data.get("vector_backend", "auto"),
189
+ "base_dir": data.get("base_dir", str(MEMORY_DIR)),
190
+ }
191
+ except Exception as exc:
192
+ logger.exception("get_storage_config failed")
193
+ return JSONResponse({"error": "Internal server error"}, status_code=500)
194
+
195
+
196
+ # ---------------------------------------------------------------------------
197
+ # PUT /api/v3/storage/config
198
+ # ---------------------------------------------------------------------------
199
+
200
+
201
+ @router.put("/storage/config")
202
+ async def put_storage_config(request: Request, body: StorageConfigUpdate):
203
+ """Update graph_backend and/or vector_backend.
204
+
205
+ Both fields require a daemon restart to take effect.
206
+ Returns restart_required: true unconditionally.
207
+ """
208
+ _require_admin(request)
209
+ try:
210
+ data = _read_config()
211
+ if body.graph_backend is not None:
212
+ data["graph_backend"] = body.graph_backend
213
+ if body.vector_backend is not None:
214
+ data["vector_backend"] = body.vector_backend
215
+ _atomic_write(data)
216
+ return {
217
+ "graph_backend": data.get("graph_backend", "auto"),
218
+ "vector_backend": data.get("vector_backend", "auto"),
219
+ "base_dir": data.get("base_dir", str(MEMORY_DIR)),
220
+ "restart_required": True,
221
+ }
222
+ except Exception as exc:
223
+ logger.exception("put_storage_config failed")
224
+ return JSONResponse({"error": "Internal server error"}, status_code=500)
225
+
226
+
227
+ # ---------------------------------------------------------------------------
228
+ # GET /api/v3/daemon/config
229
+ # ---------------------------------------------------------------------------
230
+
231
+
232
+ @router.get("/daemon/config")
233
+ async def get_daemon_config():
234
+ """Return current daemon configuration."""
235
+ try:
236
+ data = _read_config()
237
+ return {
238
+ "idle_timeout": data.get("daemon_idle_timeout", 0),
239
+ "port": data.get("daemon_port", 8765),
240
+ "legacy_port": data.get("daemon_legacy_port", 8767),
241
+ "enable_legacy_port": data.get("daemon_enable_legacy_port", True),
242
+ }
243
+ except Exception as exc:
244
+ logger.exception("get_daemon_config failed")
245
+ return JSONResponse({"error": "Internal server error"}, status_code=500)
246
+
247
+
248
+ # ---------------------------------------------------------------------------
249
+ # PUT /api/v3/daemon/config
250
+ # ---------------------------------------------------------------------------
251
+
252
+
253
+ @router.put("/daemon/config")
254
+ async def put_daemon_config(request: Request, body: DaemonConfigUpdate):
255
+ """Update daemon configuration.
256
+
257
+ Port / legacy_port changes require a daemon restart.
258
+ restart_required is True if any port field is included in the request.
259
+ """
260
+ _require_admin(request)
261
+ try:
262
+ data = _read_config()
263
+ port_changed = False
264
+ if body.idle_timeout is not None:
265
+ data["daemon_idle_timeout"] = body.idle_timeout
266
+ if body.port is not None:
267
+ data["daemon_port"] = body.port
268
+ port_changed = True
269
+ if body.legacy_port is not None:
270
+ data["daemon_legacy_port"] = body.legacy_port
271
+ port_changed = True
272
+ if body.enable_legacy_port is not None:
273
+ data["daemon_enable_legacy_port"] = body.enable_legacy_port
274
+ _atomic_write(data)
275
+ return {
276
+ "idle_timeout": data.get("daemon_idle_timeout", 0),
277
+ "port": data.get("daemon_port", 8765),
278
+ "legacy_port": data.get("daemon_legacy_port", 8767),
279
+ "enable_legacy_port": data.get("daemon_enable_legacy_port", True),
280
+ "restart_required": port_changed,
281
+ }
282
+ except Exception as exc:
283
+ logger.exception("put_daemon_config failed")
284
+ return JSONResponse({"error": "Internal server error"}, status_code=500)
285
+
286
+
287
+ # ---------------------------------------------------------------------------
288
+ # GET /api/v3/mesh/config
289
+ # ---------------------------------------------------------------------------
290
+
291
+
292
+ @router.get("/mesh/config")
293
+ async def get_mesh_config():
294
+ """Return current mesh configuration."""
295
+ try:
296
+ data = _read_config()
297
+ return {"enabled": data.get("mesh_enabled", True)}
298
+ except Exception as exc:
299
+ logger.exception("get_mesh_config failed")
300
+ return JSONResponse({"error": "Internal server error"}, status_code=500)
301
+
302
+
303
+ # ---------------------------------------------------------------------------
304
+ # PUT /api/v3/mesh/config
305
+ # ---------------------------------------------------------------------------
306
+
307
+
308
+ @router.put("/mesh/config")
309
+ async def put_mesh_config(request: Request, body: MeshConfigUpdate):
310
+ """Enable or disable the mesh sync layer."""
311
+ _require_admin(request)
312
+ try:
313
+ data = _read_config()
314
+ data["mesh_enabled"] = body.enabled
315
+ _atomic_write(data)
316
+ return {"enabled": body.enabled}
317
+ except Exception as exc:
318
+ logger.exception("put_mesh_config failed")
319
+ return JSONResponse({"error": "Internal server error"}, status_code=500)
320
+
321
+
322
+ # ---------------------------------------------------------------------------
323
+ # GET /api/v3/trust/config
324
+ # ---------------------------------------------------------------------------
325
+
326
+
327
+ @router.get("/trust/config")
328
+ async def get_trust_config():
329
+ """Return current trust configuration.
330
+
331
+ Fields are spread across three config sections:
332
+ - retrieval.use_trust_weighting (Bayesian trust in retrieval ranking)
333
+ - injection.trust_first_party (framing of injected context)
334
+ - consolidation.promotion_min_trust (min trust required for promotion)
335
+ """
336
+ try:
337
+ data = _read_config()
338
+ retrieval = data.get("retrieval", {})
339
+ injection = data.get("injection", {})
340
+ consolidation = data.get("consolidation", {})
341
+ return {
342
+ "use_trust_weighting": retrieval.get("use_trust_weighting", True),
343
+ "trust_first_party": injection.get("trust_first_party", False),
344
+ "promotion_min_trust": consolidation.get("promotion_min_trust", 0.5),
345
+ }
346
+ except Exception as exc:
347
+ logger.exception("get_trust_config failed")
348
+ return JSONResponse({"error": "Internal server error"}, status_code=500)
349
+
350
+
351
+ # ---------------------------------------------------------------------------
352
+ # PUT /api/v3/trust/config
353
+ # ---------------------------------------------------------------------------
354
+
355
+
356
+ @router.put("/trust/config")
357
+ async def put_trust_config(request: Request, body: TrustConfigUpdate):
358
+ """Update trust configuration.
359
+
360
+ Each field is stored in its canonical config.json sub-section.
361
+ Unrelated keys in those sub-sections are preserved.
362
+ """
363
+ _require_admin(request)
364
+ try:
365
+ data = _read_config()
366
+ if body.use_trust_weighting is not None:
367
+ retrieval = data.setdefault("retrieval", {})
368
+ retrieval["use_trust_weighting"] = body.use_trust_weighting
369
+ if body.trust_first_party is not None:
370
+ injection = data.setdefault("injection", {})
371
+ injection["trust_first_party"] = body.trust_first_party
372
+ if body.promotion_min_trust is not None:
373
+ consolidation = data.setdefault("consolidation", {})
374
+ consolidation["promotion_min_trust"] = body.promotion_min_trust
375
+ _atomic_write(data)
376
+ retrieval = data.get("retrieval", {})
377
+ injection = data.get("injection", {})
378
+ consolidation = data.get("consolidation", {})
379
+ return {
380
+ "use_trust_weighting": retrieval.get("use_trust_weighting", True),
381
+ "trust_first_party": injection.get("trust_first_party", False),
382
+ "promotion_min_trust": consolidation.get("promotion_min_trust", 0.5),
383
+ }
384
+ except Exception as exc:
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
+ async 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 as exc:
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
+ async 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 on the next daemon startup (or after
420
+ the scheduler fires its next cycle).
421
+ """
422
+ _require_admin(request)
423
+ try:
424
+ data = _read_config()
425
+ stored = data.get("forgetting", {})
426
+ # Apply defaults for missing fields, then overlay the stored values,
427
+ # then overlay the requested updates.
428
+ merged = {**_FORGETTING_DEFAULTS, **stored}
429
+ updates = body.model_dump(exclude_none=True)
430
+ merged.update(updates)
431
+ data["forgetting"] = merged
432
+ _atomic_write(data)
433
+ return {k: merged[k] for k in _FORGETTING_DEFAULTS}
434
+ except Exception as exc:
435
+ logger.exception("put_forgetting_config failed")
436
+ 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")
@@ -8,7 +8,7 @@ from __future__ import annotations
8
8
 
9
9
  from fastapi import APIRouter, HTTPException, Request, Query
10
10
 
11
- from .helpers import require_engine
11
+ from .helpers import require_engine, get_active_profile
12
12
 
13
13
  router = APIRouter(prefix="/api/entity", tags=["entity"])
14
14
 
@@ -16,12 +16,15 @@ router = APIRouter(prefix="/api/entity", tags=["entity"])
16
16
  @router.get("/list")
17
17
  async def list_entities(
18
18
  request: Request,
19
- profile: str = Query(default="default"),
19
+ profile: str | None = Query(default=None),
20
20
  limit: int = Query(default=100, ge=1, le=1000),
21
21
  offset: int = Query(default=0, ge=0),
22
22
  ):
23
23
  """List all entities with basic info (canonical name, type, fact count)."""
24
24
  engine = require_engine(request)
25
+ # Default to the ACTIVE profile (request runtime truth), never the literal
26
+ # "default" — otherwise every profile sees the default profile's entities.
27
+ profile = profile or get_active_profile()
25
28
 
26
29
  import sqlite3
27
30
  import json
@@ -71,11 +74,12 @@ async def list_entities(
71
74
  async def get_entity(
72
75
  entity_name: str,
73
76
  request: Request,
74
- profile: str = Query(default="default"),
77
+ profile: str | None = Query(default=None),
75
78
  project: str = Query(default=""),
76
79
  ):
77
80
  """Get compiled truth + timeline for an entity."""
78
81
  engine = require_engine(request)
82
+ profile = profile or get_active_profile()
79
83
 
80
84
  import sqlite3
81
85
  import json
@@ -115,11 +119,12 @@ async def get_entity(
115
119
  async def recompile_entity(
116
120
  entity_name: str,
117
121
  request: Request,
118
- profile: str = Query(default="default"),
122
+ profile: str | None = Query(default=None),
119
123
  project: str = Query(default=""),
120
124
  ):
121
125
  """Force immediate recompilation of an entity."""
122
126
  engine = require_engine(request)
127
+ profile = profile or get_active_profile()
123
128
 
124
129
  import sqlite3
125
130
  conn = sqlite3.connect(str(engine._config.db_path))
@@ -17,7 +17,7 @@ from datetime import datetime, timezone
17
17
  from fastapi import APIRouter, HTTPException, Query
18
18
  from fastapi.responses import StreamingResponse
19
19
 
20
- from .helpers import DB_PATH
20
+ from .helpers import DB_PATH, get_active_profile
21
21
 
22
22
  logger = logging.getLogger("superlocalmemory.routes.events")
23
23
  router = APIRouter()
@@ -71,6 +71,11 @@ async def event_stream(
71
71
 
72
72
  import asyncio
73
73
 
74
+ # Scope this stream to the profile active at connect time. The dashboard
75
+ # opens a fresh stream after a profile switch, so a connection only ever
76
+ # surfaces ONE profile's events — never another profile's (GDPR).
77
+ active_profile = get_active_profile()
78
+
74
79
  client_queue = _queue.Queue(maxsize=100)
75
80
  with _sse_queues_lock:
76
81
  _sse_queues.add(client_queue)
@@ -86,6 +91,7 @@ async def event_stream(
86
91
  bus = EventBus.get_instance(DB_PATH)
87
92
  missed = bus.get_recent_events(
88
93
  since_id=last_event_id, limit=50, event_type=event_type,
94
+ profile_id=active_profile,
89
95
  )
90
96
  for evt in missed:
91
97
  data = json.dumps(evt)
@@ -96,7 +102,9 @@ async def event_stream(
96
102
  else:
97
103
  try:
98
104
  bus = EventBus.get_instance(DB_PATH)
99
- recent = bus.get_recent_events(limit=1)
105
+ recent = bus.get_recent_events(
106
+ limit=1, profile_id=active_profile,
107
+ )
100
108
  if recent:
101
109
  last_db_id = recent[-1].get('id', 0)
102
110
  except Exception:
@@ -110,6 +118,9 @@ async def event_stream(
110
118
  event = client_queue.get_nowait()
111
119
  if event_type and event.get("event_type") != event_type:
112
120
  continue
121
+ # Profile isolation: never forward another profile's event.
122
+ if event.get("profile_id", "default") != active_profile:
123
+ continue
113
124
  data = json.dumps(event)
114
125
  event_id = event.get("id", event.get("seq", ""))
115
126
  yield f"id: {event_id}\nevent: {event['event_type']}\ndata: {data}\n\n"
@@ -126,6 +137,7 @@ async def event_stream(
126
137
  bus = EventBus.get_instance(DB_PATH)
127
138
  new_events = bus.get_recent_events(
128
139
  since_id=last_db_id, limit=10, event_type=event_type,
140
+ profile_id=active_profile,
129
141
  )
130
142
  for evt in new_events:
131
143
  data = json.dumps(evt)
@@ -162,13 +174,16 @@ async def get_events(
162
174
  return {"events": [], "count": 0, "message": "Event Bus not available"}
163
175
  try:
164
176
  bus = EventBus.get_instance(DB_PATH)
177
+ active_profile = get_active_profile()
165
178
  events = bus.get_recent_events(
166
179
  since_id=since_id, limit=limit, event_type=event_type,
180
+ profile_id=active_profile,
167
181
  )
168
- stats = bus.get_event_stats()
182
+ stats = bus.get_event_stats(profile_id=active_profile)
169
183
  return {"events": events, "count": len(events), "stats": stats}
170
- except Exception as e:
171
- raise HTTPException(status_code=500, detail=f"Event retrieval error: {str(e)}")
184
+ except Exception:
185
+ logger.exception("events route error")
186
+ raise HTTPException(status_code=500, detail="Event retrieval error")
172
187
 
173
188
 
174
189
  @router.get("/api/events/stats")
@@ -178,6 +193,7 @@ async def get_event_stats():
178
193
  return {"total_events": 0, "message": "Event Bus not available"}
179
194
  try:
180
195
  bus = EventBus.get_instance(DB_PATH)
181
- return bus.get_event_stats()
182
- except Exception as e:
183
- raise HTTPException(status_code=500, detail=f"Event stats error: {str(e)}")
196
+ return bus.get_event_stats(profile_id=get_active_profile())
197
+ except Exception:
198
+ logger.exception("events route error")
199
+ raise HTTPException(status_code=500, detail="Event stats error")