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
@@ -43,6 +43,7 @@ _MAX_RETRY_DELAY_SECONDS = 3600
43
43
  _SCHEMA = """
44
44
  CREATE TABLE IF NOT EXISTS pending_memories (
45
45
  id INTEGER PRIMARY KEY AUTOINCREMENT,
46
+ profile_id TEXT NOT NULL DEFAULT 'default',
46
47
  content TEXT NOT NULL,
47
48
  tags TEXT DEFAULT '',
48
49
  metadata TEXT DEFAULT '{}',
@@ -62,6 +63,12 @@ def _get_db(base_dir: Path | None = None) -> sqlite3.Connection:
62
63
  db_path = d / _PENDING_DB
63
64
  conn = sqlite3.connect(str(db_path), timeout=5)
64
65
  conn.execute("PRAGMA journal_mode=WAL")
66
+ # C4: pending queue can hold not-yet-materialized memory content owner-only.
67
+ try:
68
+ from superlocalmemory.core.security_primitives import harden_db_perms
69
+ harden_db_perms(db_path)
70
+ except Exception:
71
+ pass
65
72
  conn.execute(_SCHEMA)
66
73
  columns = {
67
74
  row[1]
@@ -73,6 +80,15 @@ def _get_db(base_dir: Path | None = None) -> sqlite3.Connection:
73
80
  "next_retry_at REAL DEFAULT 0"
74
81
  )
75
82
  conn.commit()
83
+ # Per-profile isolation: a queued item must materialize under the profile
84
+ # that was active when it was enqueued — never under whatever profile is
85
+ # active at drain time. Existing rows backfill to 'default'.
86
+ if "profile_id" not in columns:
87
+ conn.execute(
88
+ "ALTER TABLE pending_memories ADD COLUMN "
89
+ "profile_id TEXT NOT NULL DEFAULT 'default'"
90
+ )
91
+ conn.commit()
76
92
  # Pre-V3.7 rows were terminally hidden after three failures. Restore them
77
93
  # to the retry queue; M018 makes replay idempotent and no raw evidence may
78
94
  # remain stranded solely because an older version exhausted its counter.
@@ -89,8 +105,12 @@ def store_pending(
89
105
  tags: str = "",
90
106
  metadata: dict | None = None,
91
107
  base_dir: Path | None = None,
108
+ profile_id: str = "default",
92
109
  ) -> int:
93
- """Store content in pending table. Returns the row ID.
110
+ """Store content in pending table under a profile. Returns the row ID.
111
+
112
+ ``profile_id`` captures the profile active at ENQUEUE time so a later
113
+ profile switch can never redirect this memory to a different profile.
94
114
 
95
115
  This is intentionally FAST — no engine init, no embedding, no model loading.
96
116
  Just a raw SQLite INSERT (~0.1s).
@@ -98,9 +118,11 @@ def store_pending(
98
118
  conn = _get_db(base_dir)
99
119
  try:
100
120
  cur = conn.execute(
101
- "INSERT INTO pending_memories (content, tags, metadata, created_at, status) "
102
- "VALUES (?, ?, ?, ?, 'pending')",
103
- (content, tags, json.dumps(metadata or {}), time.strftime("%Y-%m-%dT%H:%M:%S")),
121
+ "INSERT INTO pending_memories "
122
+ "(profile_id, content, tags, metadata, created_at, status) "
123
+ "VALUES (?, ?, ?, ?, ?, 'pending')",
124
+ (profile_id, content, tags, json.dumps(metadata or {}),
125
+ time.strftime("%Y-%m-%dT%H:%M:%S")),
104
126
  )
105
127
  conn.commit()
106
128
  return cur.lastrowid or 0
@@ -108,20 +130,34 @@ def store_pending(
108
130
  conn.close()
109
131
 
110
132
 
111
- def get_pending(base_dir: Path | None = None, limit: int = 50) -> list[dict]:
112
- """Get unprocessed pending memories."""
133
+ def get_pending(
134
+ base_dir: Path | None = None,
135
+ limit: int = 50,
136
+ profile_id: str | None = None,
137
+ ) -> list[dict]:
138
+ """Get unprocessed pending memories, optionally scoped to one profile.
139
+
140
+ The drain passes ``profile_id`` = its engine's active profile so it only
141
+ ever claims items enqueued under that profile. Items for other profiles
142
+ wait until their profile is active — never materialized under the wrong one.
143
+ """
113
144
  conn = _get_db(base_dir)
114
145
  try:
115
- rows = conn.execute(
116
- "SELECT id, content, tags, metadata, created_at, retry_count "
146
+ query = (
147
+ "SELECT id, content, tags, metadata, created_at, retry_count, profile_id "
117
148
  "FROM pending_memories WHERE status = 'pending' "
118
- "AND COALESCE(next_retry_at, 0) <= ? "
119
- "ORDER BY id ASC LIMIT ?",
120
- (time.time(), limit),
121
- ).fetchall()
149
+ "AND COALESCE(next_retry_at, 0) <= ?"
150
+ )
151
+ params: list = [time.time()]
152
+ if profile_id is not None:
153
+ query += " AND profile_id = ?"
154
+ params.append(profile_id)
155
+ query += " ORDER BY id ASC LIMIT ?"
156
+ params.append(limit)
157
+ rows = conn.execute(query, params).fetchall()
122
158
  return [
123
159
  {"id": r[0], "content": r[1], "tags": r[2], "metadata": r[3],
124
- "created_at": r[4], "retry_count": r[5]}
160
+ "created_at": r[4], "retry_count": r[5], "profile_id": r[6]}
125
161
  for r in rows
126
162
  ]
127
163
  finally:
@@ -99,6 +99,10 @@ def cmd_proxy(args: Namespace) -> None:
99
99
  print("Or run: slm wrap claude")
100
100
  print()
101
101
  print("Proxy ready.")
102
+ print()
103
+ print("Note: 'slm proxy' enables the proxy independently — it does not")
104
+ print("flip the master optimize switch, so 'slm optimize status' may")
105
+ print("show OFF while the proxy is running.")
102
106
  else:
103
107
  print("Error: proxy failed to start. Check logs.", file=sys.stderr)
104
108
  sys.exit(1)
@@ -37,10 +37,16 @@ def cmd_db_scale(args: Namespace) -> int:
37
37
  if not args.stage_id:
38
38
  raise ScaleEngineError("promote requires --stage-id (see `slm db scale status`)")
39
39
  result = manager.promote(args.stage_id)
40
+ # The running daemon reads scale_engine_state once at startup; the
41
+ # promoted Cozo/Lance backends are only wired in on the next start.
42
+ # Without this flag users see a clean promote and keep hitting
43
+ # SQLite-only silently (no error, no speedup).
44
+ result = {**result, "restart_required": True}
40
45
  elif action == "rollback":
41
46
  if not args.backup_id:
42
47
  raise ScaleEngineError("rollback requires --backup-id (see `slm db scale status`)")
43
48
  result = manager.rollback(args.backup_id)
49
+ result = {**result, "restart_required": True}
44
50
  else:
45
51
  raise ScaleEngineError(f"unknown Scale Engine action: {action}")
46
52
  except (ScaleEngineError, CanonicalVectorError) as exc:
@@ -114,17 +114,19 @@ def _download_model(model_name: str, label: str) -> bool:
114
114
  print(f"\n Downloading {label}: {model_name}")
115
115
  print(f" (this may take a few minutes on first run)\n")
116
116
 
117
+ # H-03: pass the model name as argv, never interpolated into executed
118
+ # source, so a crafted model_name cannot become arbitrary Python.
117
119
  script = (
118
- f"import sys; "
119
- f"from sentence_transformers import SentenceTransformer; "
120
- f"m = SentenceTransformer('{model_name}', trust_remote_code=True); "
121
- f"d = m.get_sentence_embedding_dimension(); "
122
- f"print(f'OK dim={{d}}'); "
120
+ "import sys; "
121
+ "from sentence_transformers import SentenceTransformer; "
122
+ "m = SentenceTransformer(sys.argv[1], trust_remote_code=True); "
123
+ "d = m.get_sentence_embedding_dimension(); "
124
+ "print(f'OK dim={d}'); "
123
125
  )
124
126
 
125
127
  try:
126
128
  result = subprocess.run(
127
- [sys.executable, "-c", script],
129
+ [sys.executable, "-c", script, model_name],
128
130
  timeout=600, # 10 min for large model downloads
129
131
  capture_output=False, # Show download progress
130
132
  text=True,
@@ -156,15 +158,16 @@ def _download_reranker(model_name: str) -> bool:
156
158
  print(f"\n Downloading reranker: {model_name}")
157
159
  print(f" (cross-encoder for result re-ranking)\n")
158
160
 
161
+ # H-03: model name via argv, never interpolated into executed source.
159
162
  script = (
160
- f"from sentence_transformers import CrossEncoder; "
161
- f"m = CrossEncoder('{model_name}', trust_remote_code=True); "
162
- f"print('OK'); "
163
+ "import sys; from sentence_transformers import CrossEncoder; "
164
+ "m = CrossEncoder(sys.argv[1], trust_remote_code=True); "
165
+ "print('OK'); "
163
166
  )
164
167
 
165
168
  try:
166
169
  result = subprocess.run(
167
- [sys.executable, "-c", script],
170
+ [sys.executable, "-c", script, model_name],
168
171
  timeout=300,
169
172
  capture_output=False,
170
173
  text=True,
@@ -195,16 +198,17 @@ def _download_compressor(model_name: str) -> bool:
195
198
  print(f"\n Downloading compression model: {model_name}")
196
199
  print(f" (LLMLingua-2 prose compressor, ~560MB — aggressive mode only)\n")
197
200
 
201
+ # H-03: model name via argv, never interpolated into executed source.
198
202
  script = (
199
- "from llmlingua import PromptCompressor; "
200
- f"PromptCompressor(model_name='{model_name}', use_llmlingua2=True, "
203
+ "import sys; from llmlingua import PromptCompressor; "
204
+ "PromptCompressor(model_name=sys.argv[1], use_llmlingua2=True, "
201
205
  "device_map='cpu'); "
202
206
  "print('OK')"
203
207
  )
204
208
 
205
209
  try:
206
210
  result = subprocess.run(
207
- [sys.executable, "-c", script],
211
+ [sys.executable, "-c", script, model_name],
208
212
  timeout=900, # 560MB on a slow link can exceed 5 min
209
213
  capture_output=False,
210
214
  text=True,
@@ -494,6 +498,11 @@ def run_wizard(auto: bool = False) -> None:
494
498
 
495
499
  # -- Step 4: Download models --
496
500
  print()
501
+ # H-06 (CVE-2025-14926): a malicious HuggingFace checkpoint can execute code
502
+ # at load time. SLM only downloads its pinned defaults, but warn users who
503
+ # point config at a custom model.
504
+ print(" ⚠ Only install models from sources you trust — a malicious model")
505
+ print(" checkpoint can run code on your machine at load time. See SECURITY.md.")
497
506
  print("─── Step 4/10: Download Embedding Model ───")
498
507
 
499
508
  if _embedding_is_remote(config):
@@ -92,6 +92,12 @@ class AuditChain:
92
92
  conn = sqlite3.connect(path)
93
93
  conn.execute("PRAGMA journal_mode=WAL")
94
94
  conn.row_factory = sqlite3.Row
95
+ # C4: audit chain holds a tamper-evident record — keep it owner-only.
96
+ try:
97
+ from superlocalmemory.core.security_primitives import harden_db_perms
98
+ harden_db_perms(path)
99
+ except Exception:
100
+ pass
95
101
  return conn
96
102
 
97
103
  def _make_conn(self) -> sqlite3.Connection:
@@ -19,6 +19,15 @@ from datetime import UTC, datetime
19
19
 
20
20
  logger = logging.getLogger(__name__)
21
21
 
22
+ # Friendly export keys → canonical table names (stable Art.20 export contract).
23
+ _EXPORT_ALIASES = {
24
+ "facts": "atomic_facts",
25
+ "entities": "canonical_entities",
26
+ "edges": "graph_edges",
27
+ "feedback": "feedback_records",
28
+ "scenes": "memory_scenes",
29
+ }
30
+
22
31
 
23
32
  class GDPRCompliance:
24
33
  """GDPR compliance operations for memory data.
@@ -30,170 +39,146 @@ class GDPRCompliance:
30
39
  - Audit Trail: Log all data operations
31
40
  """
32
41
 
42
+ # Tables that carry a profile_id column but are NOT tenant memory to be
43
+ # erased/exported wholesale. `profiles` is the tenant record (handled
44
+ # separately, deleted last).
45
+ _NON_MEMORY_SCOPED = frozenset({"profiles"})
46
+
33
47
  def __init__(self, db) -> None:
34
48
  self._db = db
35
49
 
50
+ def _profile_scoped_tables(self) -> list[str]:
51
+ """Every table carrying a ``profile_id`` column — discovered live from
52
+ the schema so a newly-added table can never be silently missed by
53
+ export or erasure (the class of bug that breaks GDPR completeness)."""
54
+ try:
55
+ names = [
56
+ dict(r)["name"]
57
+ for r in self._db.execute(
58
+ "SELECT name FROM sqlite_master WHERE type='table'"
59
+ )
60
+ ]
61
+ except Exception:
62
+ return []
63
+ out: list[str] = []
64
+ for t in names:
65
+ if t.startswith("sqlite_") or t in self._NON_MEMORY_SCOPED:
66
+ continue
67
+ try:
68
+ cols = {dict(c)["name"] for c in self._db.execute(f"PRAGMA table_info({t})")}
69
+ except Exception:
70
+ continue
71
+ if "profile_id" in cols:
72
+ out.append(t)
73
+ return out
74
+
36
75
  # -- Right to Access (Art. 15) -----------------------------------------
37
76
 
38
77
  def export_profile_data(self, profile_id: str) -> dict:
39
- """Export ALL data for a profile in machine-readable format.
40
-
41
- Returns a dict containing all memories, facts, entities,
42
- edges, trust scores, feedback, and behavioral patterns.
43
- """
78
+ """Export ALL data for a profile in machine-readable format (Art. 15 /
79
+ Art. 20). Covers every profile-scoped table discovered from the schema,
80
+ plus the profile record itself."""
44
81
  self._audit("export", "profile", profile_id, "Full data export")
45
82
 
46
83
  data: dict = {"profile_id": profile_id, "exported_at": _now()}
84
+ for table in self._profile_scoped_tables():
85
+ try:
86
+ rows = self._db.execute(
87
+ f"SELECT * FROM {table} WHERE profile_id = ?", (profile_id,)
88
+ )
89
+ data[table] = [dict(r) for r in rows]
90
+ except Exception as exc: # pragma: no cover — defensive per-table
91
+ logger.warning("export: table %s skipped: %s", table, exc)
92
+
93
+ # Profile record itself (the tenant metadata).
94
+ try:
95
+ rows = self._db.execute(
96
+ "SELECT * FROM profiles WHERE profile_id = ?", (profile_id,)
97
+ )
98
+ data["profile_record"] = [dict(r) for r in rows]
99
+ except Exception:
100
+ data["profile_record"] = []
47
101
 
48
- # Memories
49
- rows = self._db.execute(
50
- "SELECT * FROM memories WHERE profile_id = ?", (profile_id,)
51
- )
52
- data["memories"] = [dict(r) for r in rows]
53
-
54
- # Facts
55
- rows = self._db.execute(
56
- "SELECT * FROM atomic_facts WHERE profile_id = ?", (profile_id,)
57
- )
58
- data["facts"] = [dict(r) for r in rows]
59
-
60
- # Entities
61
- rows = self._db.execute(
62
- "SELECT * FROM canonical_entities WHERE profile_id = ?", (profile_id,)
63
- )
64
- data["entities"] = [dict(r) for r in rows]
65
-
66
- # Graph edges
67
- rows = self._db.execute(
68
- "SELECT * FROM graph_edges WHERE profile_id = ?", (profile_id,)
69
- )
70
- data["edges"] = [dict(r) for r in rows]
71
-
72
- # Trust scores
73
- rows = self._db.execute(
74
- "SELECT * FROM trust_scores WHERE profile_id = ?", (profile_id,)
75
- )
76
- data["trust_scores"] = [dict(r) for r in rows]
77
-
78
- # Feedback
79
- rows = self._db.execute(
80
- "SELECT * FROM feedback_records WHERE profile_id = ?", (profile_id,)
81
- )
82
- data["feedback"] = [dict(r) for r in rows]
83
-
84
- # Entity profiles
85
- rows = self._db.execute(
86
- "SELECT * FROM entity_profiles WHERE profile_id = ?", (profile_id,)
87
- )
88
- data["entity_profiles"] = [dict(r) for r in rows]
89
-
90
- # Memory scenes
91
- rows = self._db.execute(
92
- "SELECT * FROM memory_scenes WHERE profile_id = ?", (profile_id,)
93
- )
94
- data["scenes"] = [dict(r) for r in rows]
95
-
96
- # Temporal events
97
- rows = self._db.execute(
98
- "SELECT * FROM temporal_events WHERE profile_id = ?", (profile_id,)
99
- )
100
- data["temporal_events"] = [dict(r) for r in rows]
101
-
102
- # Consolidation log
103
- rows = self._db.execute(
104
- "SELECT * FROM consolidation_log WHERE profile_id = ?", (profile_id,)
105
- )
106
- data["consolidation_log"] = [dict(r) for r in rows]
107
-
108
- # Behavioral patterns
109
- rows = self._db.execute(
110
- "SELECT * FROM behavioral_patterns WHERE profile_id = ?", (profile_id,)
111
- )
112
- data["behavioral_patterns"] = [dict(r) for r in rows]
113
-
114
- # Action outcomes
115
- rows = self._db.execute(
116
- "SELECT * FROM action_outcomes WHERE profile_id = ?", (profile_id,)
117
- )
118
- data["action_outcomes"] = [dict(r) for r in rows]
119
-
120
- # Compliance audit trail
121
- rows = self._db.execute(
122
- "SELECT * FROM compliance_audit WHERE profile_id = ?", (profile_id,)
123
- )
124
- data["compliance_audit"] = [dict(r) for r in rows]
125
-
126
- # Provenance (data lineage — EU AI Act Art. 10)
127
- rows = self._db.execute(
128
- "SELECT * FROM provenance WHERE profile_id = ?", (profile_id,)
129
- )
130
- data["provenance"] = [dict(r) for r in rows]
131
-
132
- # Entity aliases (indirect PII via entity relationships)
133
- rows = self._db.execute(
134
- "SELECT ea.* FROM entity_aliases ea "
135
- "JOIN canonical_entities ce ON ea.entity_id = ce.entity_id "
136
- "WHERE ce.profile_id = ?", (profile_id,)
137
- )
138
- data["entity_aliases"] = [dict(r) for r in rows]
139
-
140
- # Profile record itself
141
- rows = self._db.execute(
142
- "SELECT * FROM profiles WHERE profile_id = ?", (profile_id,)
143
- )
144
- data["profile_record"] = [dict(r) for r in rows]
145
-
102
+ # total_items counts the canonical (table-name) keys only, before
103
+ # friendly aliases are added, so it is not double-counted.
146
104
  data["total_items"] = sum(
147
105
  len(v) for v in data.values() if isinstance(v, list)
148
106
  )
149
107
 
108
+ # Backward-compatible friendly aliases for the well-known keys (stable
109
+ # export contract) — they reference the same lists, not copies.
110
+ for friendly, table in _EXPORT_ALIASES.items():
111
+ if table in data:
112
+ data[friendly] = data[table]
113
+
150
114
  logger.info("Exported %d items for profile '%s'", data["total_items"], profile_id)
151
115
  return data
152
116
 
153
117
  # -- Right to Erasure (Art. 17) ----------------------------------------
154
118
 
155
119
  def forget_profile(self, profile_id: str) -> dict:
156
- """Delete ALL data for a profile (right to be forgotten).
120
+ """Delete ALL data for a profile (right to be forgotten, Art. 17).
157
121
 
158
- CASCADE deletes handle most cleanup via foreign keys.
159
- Returns counts of deleted items.
122
+ Erases every profile-scoped table discovered from the live schema, so a
123
+ newly-added table is covered automatically. The erasure is recorded in
124
+ the tamper-proof audit chain BEFORE any deletion (Art. 5(2)
125
+ accountability) — the in-DB compliance_audit row is itself erased, so
126
+ the chain in a separate DB is the durable evidence.
160
127
  """
161
128
  if profile_id == "default":
162
129
  raise ValueError("Cannot delete the default profile via GDPR erasure. "
163
130
  "Use profile deletion instead.")
164
131
 
132
+ # 1) Durable, tamper-evident record FIRST — survives the erasure.
133
+ try:
134
+ from superlocalmemory.compliance.audit import AuditChain
135
+ from superlocalmemory.infra.data_root import state_path
136
+ AuditChain(str(state_path("audit_chain.db"))).log(
137
+ "gdpr_erase", agent_id="gdpr", profile_id=profile_id,
138
+ metadata={"basis": "GDPR Art.17 right-to-erasure"},
139
+ )
140
+ except Exception as exc:
141
+ logger.warning("GDPR erase: audit-chain log failed: %s", exc)
165
142
  self._audit("delete", "profile", profile_id, "GDPR erasure request")
166
143
 
167
144
  counts: dict[str, int] = {}
168
- tables = [
169
- "compliance_audit", "action_outcomes", "behavioral_patterns",
170
- "feedback_records", "trust_scores", "provenance",
171
- "consolidation_log", "graph_edges", "temporal_events",
172
- "memory_scenes", "entity_profiles", "bm25_tokens",
173
- "atomic_facts", "memories", "canonical_entities",
174
- ]
145
+ tables = self._profile_scoped_tables()
146
+ # Pass 1 — count every table BEFORE any deletion, so a CASCADE that
147
+ # removes a child (e.g. atomic_facts via memories) does not zero the
148
+ # attribution. Completeness is independent of this.
175
149
  for table in tables:
176
- rows = self._db.execute(
177
- f"SELECT COUNT(*) AS c FROM {table} WHERE profile_id = ?",
178
- (profile_id,),
179
- )
180
- counts[table] = int(dict(rows[0])["c"]) if rows else 0
181
- self._db.execute(
182
- f"DELETE FROM {table} WHERE profile_id = ?", (profile_id,)
183
- )
184
-
185
- # Delete entity aliases (orphan PII via entity relationships)
186
- self._db.execute(
187
- "DELETE FROM entity_aliases WHERE entity_id IN "
188
- "(SELECT entity_id FROM canonical_entities WHERE profile_id = ?)",
189
- (profile_id,),
190
- )
191
-
192
- # Delete profile itself
193
- self._db.execute(
194
- "DELETE FROM profiles WHERE profile_id = ?", (profile_id,)
195
- )
196
- counts["profiles"] = 1
150
+ try:
151
+ rows = self._db.execute(
152
+ f"SELECT COUNT(*) AS c FROM {table} WHERE profile_id = ?",
153
+ (profile_id,),
154
+ )
155
+ counts[table] = int(dict(rows[0])["c"]) if rows else 0
156
+ except Exception as exc: # pragma: no cover
157
+ logger.warning("GDPR erase: count %s failed: %s", table, exc)
158
+ counts[table] = 0
159
+ # Pass 2 full-tenant wipe with FK enforcement OFF so table order is
160
+ # irrelevant (every profile row in every table goes). FTS shadow rows
161
+ # are still removed by the base-table delete triggers.
162
+ try:
163
+ self._db.execute("PRAGMA foreign_keys=OFF")
164
+ except Exception:
165
+ pass
166
+ try:
167
+ for table in tables:
168
+ try:
169
+ self._db.execute(
170
+ f"DELETE FROM {table} WHERE profile_id = ?", (profile_id,)
171
+ )
172
+ except Exception as exc: # pragma: no cover — defensive per-table
173
+ logger.warning("GDPR erase: delete %s failed: %s", table, exc)
174
+ # Delete the profile record itself.
175
+ self._db.execute("DELETE FROM profiles WHERE profile_id = ?", (profile_id,))
176
+ counts["profiles"] = 1
177
+ finally:
178
+ try:
179
+ self._db.execute("PRAGMA foreign_keys=ON")
180
+ except Exception:
181
+ pass
197
182
 
198
183
  # Erase learning database (separate DB file)
199
184
  try:
@@ -211,7 +196,7 @@ class GDPRCompliance:
211
196
  except Exception:
212
197
  pass
213
198
 
214
- logger.info("GDPR erasure for '%s': %s", profile_id, counts)
199
+ logger.info("GDPR erasure for '%s': %d tables, %s", profile_id, len(tables), counts)
215
200
  return counts
216
201
 
217
202
  def forget_entity(self, entity_name: str, profile_id: str) -> dict:
@@ -254,9 +239,14 @@ class GDPRCompliance:
254
239
  (eid, profile_id),
255
240
  )
256
241
 
257
- # Delete aliases + entity
258
- self._db.execute("DELETE FROM entity_aliases WHERE entity_id = ?", (eid,))
259
- self._db.execute("DELETE FROM canonical_entities WHERE entity_id = ?", (eid,))
242
+ # Delete aliases + entity (profile-scoped — entity_id is UUID-global but
243
+ # keep the tenant predicate for consistent Art.17 isolation).
244
+ self._db.execute(
245
+ "DELETE FROM entity_aliases WHERE entity_id = ? AND profile_id = ?",
246
+ (eid, profile_id))
247
+ self._db.execute(
248
+ "DELETE FROM canonical_entities WHERE entity_id = ? AND profile_id = ?",
249
+ (eid, profile_id))
260
250
  counts["entity"] = 1
261
251
 
262
252
  logger.info("Entity erasure '%s' in '%s': %s", entity_name, profile_id, counts)