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.
- package/.claude-plugin/marketplace.json +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +129 -0
- package/README.md +205 -123
- package/package.json +12 -3
- package/plugin/.claude-plugin/plugin.json +2 -3
- package/plugin/CLAUDE.md +8 -8
- package/plugin/agents/slm-governance-advisor.md +80 -0
- package/plugin/agents/slm-loop-runner.md +71 -0
- package/plugin/agents/slm-memory-advisor.md +10 -5
- package/plugin/agents/slm-optimize-advisor.md +9 -3
- package/plugin/commands/slm-loop.md +31 -0
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/slm-launch +46 -7
- package/plugin/settings.json +9 -0
- package/plugin/skills/slm-cache/SKILL.md +9 -1
- package/plugin/skills/slm-compress/SKILL.md +8 -1
- package/plugin/skills/slm-governance/SKILL.md +248 -0
- package/plugin/skills/slm-graph/SKILL.md +17 -3
- package/plugin/skills/slm-loop/SKILL.md +99 -0
- package/plugin/skills/slm-mesh/SKILL.md +282 -0
- package/plugin/skills/slm-profile/SKILL.md +148 -0
- package/plugin/skills/slm-recall/SKILL.md +46 -10
- package/plugin/skills/slm-remember/SKILL.md +48 -1
- package/plugin/skills/slm-scope/SKILL.md +176 -0
- package/plugin/skills/slm-session/SKILL.md +24 -1
- package/plugin/skills/slm-status/SKILL.md +18 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/skills/slm-cache/SKILL.md +9 -1
- package/plugin-src/skills/slm-compress/SKILL.md +8 -1
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- package/plugin-src/skills/slm-recall/SKILL.md +46 -10
- package/plugin-src/skills/slm-remember/SKILL.md +48 -1
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +2 -1
- package/scripts/postinstall/validation.js +2 -0
- package/scripts/postinstall-interactive.js +74 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/__init__.py +3 -0
- package/src/superlocalmemory/access/rbac.py +477 -0
- package/src/superlocalmemory/cli/commands.py +228 -17
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/daemon.py +7 -0
- package/src/superlocalmemory/cli/loop_cmd.py +187 -0
- package/src/superlocalmemory/cli/main.py +49 -8
- package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
- package/src/superlocalmemory/cli/pending_store.py +49 -13
- package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
- package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
- package/src/superlocalmemory/cli/setup_wizard.py +22 -13
- package/src/superlocalmemory/cli/version_banner.py +17 -3
- package/src/superlocalmemory/compliance/audit.py +6 -0
- package/src/superlocalmemory/compliance/gdpr.py +128 -138
- package/src/superlocalmemory/compliance/retention.py +176 -45
- package/src/superlocalmemory/core/backend_orchestrator.py +23 -59
- package/src/superlocalmemory/core/community_summary.py +267 -0
- package/src/superlocalmemory/core/config.py +216 -3
- package/src/superlocalmemory/core/consolidation_engine.py +95 -22
- package/src/superlocalmemory/core/context_cache.py +61 -18
- package/src/superlocalmemory/core/embedding_worker.py +21 -7
- package/src/superlocalmemory/core/embeddings.py +131 -46
- package/src/superlocalmemory/core/engine.py +41 -22
- package/src/superlocalmemory/core/engine_ingestion.py +359 -43
- package/src/superlocalmemory/core/engine_wiring.py +13 -0
- package/src/superlocalmemory/core/entity_community.py +178 -0
- package/src/superlocalmemory/core/graph_analyzer.py +39 -2
- package/src/superlocalmemory/core/graph_pruner.py +13 -8
- package/src/superlocalmemory/core/ingestion_command.py +134 -25
- package/src/superlocalmemory/core/injection.py +12 -7
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
- package/src/superlocalmemory/core/modes.py +1 -1
- package/src/superlocalmemory/core/mutations.py +2 -2
- package/src/superlocalmemory/core/pii.py +105 -0
- package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -3
- package/src/superlocalmemory/core/recall_worker.py +20 -6
- package/src/superlocalmemory/core/scale_engine.py +60 -1
- package/src/superlocalmemory/core/security_primitives.py +40 -2
- package/src/superlocalmemory/core/store_pipeline.py +186 -29
- package/src/superlocalmemory/core/worker_pool.py +21 -6
- package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
- package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
- package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
- package/src/superlocalmemory/evolution/evolution_store.py +122 -45
- package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
- package/src/superlocalmemory/evolution/model_selection.py +160 -0
- package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
- package/src/superlocalmemory/evolution/triggers.py +22 -13
- package/src/superlocalmemory/graph/cozo_backend.py +43 -20
- package/src/superlocalmemory/hooks/adapter_base.py +5 -1
- package/src/superlocalmemory/hooks/auto_recall.py +13 -1
- package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
- package/src/superlocalmemory/hooks/codex_assets.py +64 -5
- package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
- package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +148 -3
- package/src/superlocalmemory/infra/backup.py +12 -1
- package/src/superlocalmemory/infra/daemon_identity.py +40 -4
- package/src/superlocalmemory/infra/data_root.py +43 -4
- package/src/superlocalmemory/infra/event_bus.py +107 -24
- package/src/superlocalmemory/infra/rate_limiter.py +93 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
- package/src/superlocalmemory/ingestion/credentials.py +1 -1
- package/src/superlocalmemory/learning/cross_project.py +28 -19
- package/src/superlocalmemory/learning/model_rollback.py +3 -0
- package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
- package/src/superlocalmemory/learning/reward.py +50 -0
- package/src/superlocalmemory/learning/reward_proxy.py +42 -9
- package/src/superlocalmemory/learning/source_quality.py +523 -1
- package/src/superlocalmemory/loops/__init__.py +56 -0
- package/src/superlocalmemory/loops/budget.py +58 -0
- package/src/superlocalmemory/loops/engine.py +164 -0
- package/src/superlocalmemory/loops/ledger.py +263 -0
- package/src/superlocalmemory/loops/models.py +152 -0
- package/src/superlocalmemory/loops/rules.py +52 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
- package/src/superlocalmemory/mcp/profiles.py +103 -0
- package/src/superlocalmemory/mcp/server.py +32 -79
- package/src/superlocalmemory/mcp/tools_active.py +4 -7
- package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
- package/src/superlocalmemory/mcp/tools_core.py +12 -4
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_learning.py +2 -2
- package/src/superlocalmemory/mcp/tools_loops.py +300 -0
- package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
- package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
- package/src/superlocalmemory/mesh/broker.py +237 -129
- package/src/superlocalmemory/mesh/remote_sync.py +50 -8
- package/src/superlocalmemory/optimize/NOTICE +1 -6
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
- package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
- package/src/superlocalmemory/optimize/compress/align.py +32 -26
- package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
- package/src/superlocalmemory/optimize/compress/router.py +105 -22
- package/src/superlocalmemory/optimize/config/defaults.py +1 -1
- package/src/superlocalmemory/optimize/config/schema.py +87 -4
- package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
- package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
- package/src/superlocalmemory/optimize/storage/db.py +38 -9
- package/src/superlocalmemory/optimize/storage/schema.py +10 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
- package/src/superlocalmemory/retrieval/engine.py +221 -47
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/reranker.py +3 -4
- package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
- package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
- package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
- package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
- package/src/superlocalmemory/retrieval/time_window.py +181 -0
- package/src/superlocalmemory/server/api.py +4 -4
- package/src/superlocalmemory/server/config_file.py +90 -0
- package/src/superlocalmemory/server/origin.py +50 -0
- package/src/superlocalmemory/server/profile_runtime.py +125 -8
- package/src/superlocalmemory/server/rbac_enforce.py +142 -0
- package/src/superlocalmemory/server/recall_health.py +24 -3
- package/src/superlocalmemory/server/recall_serializer.py +19 -1
- package/src/superlocalmemory/server/routes/abstraction.py +115 -0
- package/src/superlocalmemory/server/routes/agents.py +128 -38
- package/src/superlocalmemory/server/routes/backup.py +317 -70
- package/src/superlocalmemory/server/routes/behavioral.py +349 -71
- package/src/superlocalmemory/server/routes/brain.py +69 -12
- package/src/superlocalmemory/server/routes/chat.py +10 -5
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +438 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +108 -26
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +189 -68
- package/src/superlocalmemory/server/routes/helpers.py +16 -1
- package/src/superlocalmemory/server/routes/ingest.py +7 -4
- package/src/superlocalmemory/server/routes/insights.py +3 -3
- package/src/superlocalmemory/server/routes/learning.py +289 -118
- package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +182 -57
- package/src/superlocalmemory/server/routes/mesh.py +200 -31
- package/src/superlocalmemory/server/routes/optimize.py +33 -1
- package/src/superlocalmemory/server/routes/prewarm.py +2 -0
- package/src/superlocalmemory/server/routes/profiles.py +63 -17
- package/src/superlocalmemory/server/routes/ratelimit.py +132 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +103 -158
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/token.py +3 -13
- package/src/superlocalmemory/server/routes/v3_api.py +247 -89
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +20 -5
- package/src/superlocalmemory/server/unified_daemon.py +827 -72
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +132 -1
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
- package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
- package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
- package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +136 -1
- package/src/superlocalmemory/storage/schema_v32.py +2 -0
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/storage/schema_v347.py +4 -0
- package/src/superlocalmemory/trust/gate.py +49 -8
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
- package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
- package/src/superlocalmemory/ui/css/design-system.css +621 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
- package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
- package/src/superlocalmemory/ui/favicon.svg +35 -4
- package/src/superlocalmemory/ui/index.html +303 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +100 -41
- package/src/superlocalmemory/ui/js/dashboard.js +403 -65
- package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
- package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
- package/src/superlocalmemory/ui/js/math-health.js +1 -1
- package/src/superlocalmemory/ui/js/memories.js +15 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
- package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
- package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
- package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
- package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
- package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
- package/src/superlocalmemory/ui/js/od-agents.js +544 -0
- package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
- package/src/superlocalmemory/ui/js/od-backup.js +871 -0
- package/src/superlocalmemory/ui/js/od-brain.js +816 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +600 -0
- package/src/superlocalmemory/ui/js/od-health.js +539 -0
- package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
- package/src/superlocalmemory/ui/js/od-memories.js +929 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +553 -0
- package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
- package/src/superlocalmemory/ui/js/od-settings.js +1107 -0
- package/src/superlocalmemory/ui/js/od-shell.js +809 -0
- package/src/superlocalmemory/ui/js/od-skills.js +600 -0
- package/src/superlocalmemory/ui/js/od-team.js +258 -0
- package/src/superlocalmemory/ui/js/profiles.js +159 -46
- package/src/superlocalmemory/ui/js/settings.js +17 -3
- package/src/superlocalmemory/ui/js/timeline.js +34 -5
- package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
- package/plugin-src/.mcp.json +0 -12
- package/plugin-src/agents/slm-memory-advisor.md +0 -44
- package/plugin-src/agents/slm-optimize-advisor.md +0 -38
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +0 -23
- package/plugin-src/manifest.json +0 -25
- package/plugin-src/requirements.txt +0 -1
- package/plugin-src/rules/CLAUDE.md.fragment +0 -44
- package/plugin-src/scripts/ensure-venv.bat +0 -122
- package/plugin-src/scripts/ensure-venv.sh +0 -105
- package/plugin-src/scripts/slm-launch +0 -23
- package/plugin-src/scripts/slm-launch.bat +0 -23
- package/plugin-src/settings.json +0 -16
- package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -92,6 +92,36 @@ from superlocalmemory.storage.migrations import (
|
|
|
92
92
|
from superlocalmemory.storage.migrations import (
|
|
93
93
|
M020_model_state_integrity as _M020,
|
|
94
94
|
)
|
|
95
|
+
from superlocalmemory.storage.migrations import (
|
|
96
|
+
M021_ingestion_log_profile as _M021,
|
|
97
|
+
)
|
|
98
|
+
from superlocalmemory.storage.migrations import (
|
|
99
|
+
M022_entity_aliases_profile as _M022,
|
|
100
|
+
)
|
|
101
|
+
from superlocalmemory.storage.migrations import (
|
|
102
|
+
M023_mesh_profile_isolation as _M023,
|
|
103
|
+
)
|
|
104
|
+
from superlocalmemory.storage.migrations import (
|
|
105
|
+
M024_rbac_users_roles as _M024,
|
|
106
|
+
)
|
|
107
|
+
from superlocalmemory.storage.migrations import (
|
|
108
|
+
M025_perf_indexes as _M025,
|
|
109
|
+
)
|
|
110
|
+
from superlocalmemory.storage.migrations import (
|
|
111
|
+
M026_rbac_memberships_fk as _M026,
|
|
112
|
+
)
|
|
113
|
+
from superlocalmemory.storage.migrations import (
|
|
114
|
+
M027_transferable_patterns_profile as _M027,
|
|
115
|
+
)
|
|
116
|
+
from superlocalmemory.storage.migrations import (
|
|
117
|
+
M028_fact_entity_associations as _M028,
|
|
118
|
+
)
|
|
119
|
+
from superlocalmemory.storage.migrations import (
|
|
120
|
+
M029_behavioral_history_indexes as _M029,
|
|
121
|
+
)
|
|
122
|
+
from superlocalmemory.storage.migrations import (
|
|
123
|
+
M030_entity_explorer_indexes as _M030,
|
|
124
|
+
)
|
|
95
125
|
|
|
96
126
|
# Map migration name → module (used for the optional ``verify(conn)`` hook
|
|
97
127
|
# that lets the runner detect "already applied" state when an idempotent
|
|
@@ -116,6 +146,16 @@ _MODULES = {
|
|
|
116
146
|
_M018.NAME: _M018,
|
|
117
147
|
_M019.NAME: _M019,
|
|
118
148
|
_M020.NAME: _M020,
|
|
149
|
+
_M021.NAME: _M021,
|
|
150
|
+
_M022.NAME: _M022,
|
|
151
|
+
_M023.NAME: _M023,
|
|
152
|
+
_M024.NAME: _M024,
|
|
153
|
+
_M025.NAME: _M025,
|
|
154
|
+
_M026.NAME: _M026,
|
|
155
|
+
_M027.NAME: _M027,
|
|
156
|
+
_M028.NAME: _M028,
|
|
157
|
+
_M029.NAME: _M029,
|
|
158
|
+
_M030.NAME: _M030,
|
|
119
159
|
}
|
|
120
160
|
|
|
121
161
|
logger = logging.getLogger(__name__)
|
|
@@ -171,6 +211,9 @@ MIGRATIONS: list[Migration] = [
|
|
|
171
211
|
Migration(name=_M007.NAME, db_target="memory", ddl=_M007.DDL),
|
|
172
212
|
# M018 is additive and independent of runtime-bootstrapped tables.
|
|
173
213
|
Migration(name=_M018.NAME, db_target="memory", ddl=_M018.DDL),
|
|
214
|
+
# M024 creates RBAC tables (users / memberships / sessions). Independent
|
|
215
|
+
# brand-new tables, so it runs pre-engine-init.
|
|
216
|
+
Migration(name=_M024.NAME, db_target="memory", ddl=_M024.DDL),
|
|
174
217
|
Migration(name=_M019.NAME, db_target="memory", ddl=_M019.DDL,
|
|
175
218
|
dependencies=(_M018.NAME,)),
|
|
176
219
|
# M006 + M011 are deliberately NOT here — see DEFERRED_MIGRATIONS below.
|
|
@@ -187,6 +230,10 @@ MIGRATIONS: list[Migration] = [
|
|
|
187
230
|
# position proxy when the column is absent, so a failed deferred apply never
|
|
188
231
|
# crashes the trainer — it just keeps the old label path.
|
|
189
232
|
DEFERRED_MIGRATIONS: list[Migration] = [
|
|
233
|
+
# M028 captures an atomic_facts rowid high-water mark before readiness.
|
|
234
|
+
# atomic_facts/canonical_entities are bootstrapped by MemoryEngine.
|
|
235
|
+
Migration(name=_M028.NAME, db_target="memory", ddl=_M028.DDL,
|
|
236
|
+
dependencies=(_M018.NAME,)),
|
|
190
237
|
Migration(name=_M006.NAME, db_target="memory", ddl=_M006.DDL),
|
|
191
238
|
# M011 extends atomic_facts + creates memory_archive / memory_merge_log.
|
|
192
239
|
# atomic_facts is bootstrapped at engine init, so M011 defers alongside M006.
|
|
@@ -203,6 +250,32 @@ DEFERRED_MIGRATIONS: list[Migration] = [
|
|
|
203
250
|
Migration(name=_M016.NAME, db_target="memory", ddl=_M016.DDL),
|
|
204
251
|
# M017 adds scope to the engine-bootstrapped CCQ consolidation table.
|
|
205
252
|
Migration(name=_M017.NAME, db_target="memory", ddl=_M017.DDL),
|
|
253
|
+
# M021 rebuilds ingestion_log with a profile-scoped dedup constraint.
|
|
254
|
+
# Deferred: ingestion_log is created at engine init (apply_v343_schema).
|
|
255
|
+
Migration(name=_M021.NAME, db_target="memory", ddl=_M021.DDL),
|
|
256
|
+
# M022 adds profile_id to entity_aliases, backfilled from the parent entity.
|
|
257
|
+
# Deferred: entity_aliases is created at engine init (create_all_tables).
|
|
258
|
+
Migration(name=_M022.NAME, db_target="memory", ddl=_M022.DDL),
|
|
259
|
+
# M023 profile-scopes every mesh coordination table (peers/messages/state/
|
|
260
|
+
# locks/events). Deferred: mesh tables are created at engine init
|
|
261
|
+
# (apply_v343_schema), same as M021.
|
|
262
|
+
Migration(name=_M023.NAME, db_target="memory", ddl=_M023.DDL),
|
|
263
|
+
# M025 adds hot-path perf indexes (atomic_facts dedup, mesh cleanup/list).
|
|
264
|
+
Migration(name=_M025.NAME, db_target="memory", ddl=_M025.DDL),
|
|
265
|
+
# M026 rebuilds rbac_memberships with a profiles FK (ON DELETE CASCADE) so
|
|
266
|
+
# deleting a profile cascade-purges its role grants (SEC-H-01 defense-in-
|
|
267
|
+
# depth). Deferred: the FK target `profiles` is created at engine init.
|
|
268
|
+
Migration(name=_M026.NAME, db_target="memory", ddl=_M026.DDL),
|
|
269
|
+
# M027 rebuilds transferable_patterns with profile_id + UNIQUE(profile_id,
|
|
270
|
+
# pattern_type, key) to prevent cross-profile preference contamination (H-01,
|
|
271
|
+
# cycle-3 audit). Deferred: CrossProjectAggregator creates the table on first
|
|
272
|
+
# consolidation run, not during engine init or apply_all. apply() is a no-op
|
|
273
|
+
# when the table is absent (first install after the schema change).
|
|
274
|
+
Migration(name=_M027.NAME, db_target="learning", ddl=_M027.DDL),
|
|
275
|
+
# M029 indexes behavioral tables bootstrapped during engine initialization.
|
|
276
|
+
Migration(name=_M029.NAME, db_target="memory", ddl=_M029.DDL),
|
|
277
|
+
# M030 bounds Entity Explorer pagination and profile-summary ranking.
|
|
278
|
+
Migration(name=_M030.NAME, db_target="memory", ddl=_M030.DDL),
|
|
206
279
|
]
|
|
207
280
|
|
|
208
281
|
|
|
@@ -348,7 +421,65 @@ def _apply_single(
|
|
|
348
421
|
)
|
|
349
422
|
logger.warning(detail)
|
|
350
423
|
return ("failed", detail)
|
|
351
|
-
|
|
424
|
+
# A matching migration-log row is not proof that the promised
|
|
425
|
+
# schema still exists. Existing installs can retain a migration log
|
|
426
|
+
# while a partial restore drops an additive table or index.
|
|
427
|
+
#
|
|
428
|
+
# Never replay a historical migration merely because verify()
|
|
429
|
+
# fails. Some migrations rebuild tables and transform data; replay
|
|
430
|
+
# would be destructive (M002 is the canonical example). Only a
|
|
431
|
+
# module-supplied repair(conn) hook is allowed to reconcile a
|
|
432
|
+
# completed migration's end-state.
|
|
433
|
+
mod = _MODULES.get(migration.name)
|
|
434
|
+
verify_fn = (
|
|
435
|
+
getattr(mod, "verify", None) if mod is not None else None
|
|
436
|
+
)
|
|
437
|
+
if verify_fn is None:
|
|
438
|
+
return ("skipped", "already complete")
|
|
439
|
+
try:
|
|
440
|
+
schema_complete = bool(verify_fn(conn))
|
|
441
|
+
except sqlite3.Error as exc:
|
|
442
|
+
return (
|
|
443
|
+
"failed",
|
|
444
|
+
f"schema verification failed for {migration.name}: {exc}",
|
|
445
|
+
)
|
|
446
|
+
if schema_complete:
|
|
447
|
+
return ("skipped", "already complete (schema verified)")
|
|
448
|
+
if dry_run:
|
|
449
|
+
return (
|
|
450
|
+
"skipped",
|
|
451
|
+
"dry-run: would repair missing migration end-state",
|
|
452
|
+
)
|
|
453
|
+
repair_fn = (
|
|
454
|
+
getattr(mod, "repair", None) if mod is not None else None
|
|
455
|
+
)
|
|
456
|
+
if not callable(repair_fn):
|
|
457
|
+
detail = (
|
|
458
|
+
f"schema incomplete for completed migration "
|
|
459
|
+
f"{migration.name}; automatic replay is disabled"
|
|
460
|
+
)
|
|
461
|
+
logger.warning(detail)
|
|
462
|
+
return ("failed", detail)
|
|
463
|
+
try:
|
|
464
|
+
repair_fn(conn)
|
|
465
|
+
except sqlite3.Error as exc:
|
|
466
|
+
return (
|
|
467
|
+
"failed",
|
|
468
|
+
f"safe repair failed for {migration.name}: {exc}",
|
|
469
|
+
)
|
|
470
|
+
try:
|
|
471
|
+
if not bool(verify_fn(conn)):
|
|
472
|
+
return (
|
|
473
|
+
"failed",
|
|
474
|
+
f"safe repair did not restore {migration.name}",
|
|
475
|
+
)
|
|
476
|
+
except sqlite3.Error as exc:
|
|
477
|
+
return (
|
|
478
|
+
"failed",
|
|
479
|
+
f"post-repair verification failed for "
|
|
480
|
+
f"{migration.name}: {exc}",
|
|
481
|
+
)
|
|
482
|
+
return ("applied", "missing end-state repaired safely")
|
|
352
483
|
# status is 'failed' or 'in_progress' → retry from scratch.
|
|
353
484
|
if dry_run:
|
|
354
485
|
return ("skipped", f"dry-run: would retry (status={status})")
|
|
@@ -50,6 +50,11 @@ def verify(conn: sqlite3.Connection) -> bool:
|
|
|
50
50
|
return _REQUIRED_TABLES.issubset(names)
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
def repair(conn: sqlite3.Connection) -> None:
|
|
54
|
+
"""Restore additive Skill Evolution tables without replaying other migrations."""
|
|
55
|
+
conn.executescript(DDL)
|
|
56
|
+
|
|
57
|
+
|
|
53
58
|
DDL = """
|
|
54
59
|
CREATE TABLE IF NOT EXISTS evolution_config (
|
|
55
60
|
profile_id TEXT PRIMARY KEY,
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory — per-profile isolation (I-4)
|
|
4
|
+
|
|
5
|
+
"""M021 — per-profile ingestion_log dedup (memory.db, deferred).
|
|
6
|
+
|
|
7
|
+
``ingestion_log`` is the compatibility ledger for external-adapter ingests. Its
|
|
8
|
+
dedup UNIQUE constraint was global — ``UNIQUE(source_type, dedup_key)`` — so a
|
|
9
|
+
second profile ingesting the same source key could not record its own ledger
|
|
10
|
+
row (INSERT OR IGNORE silently no-op'd), losing that profile's fact_ids in the
|
|
11
|
+
ledger. (The authoritative dedup already moved to the M018 ingestion_operations
|
|
12
|
+
table, which is profile-scoped, so the memory itself is not starved — this
|
|
13
|
+
migration fixes the ledger so per-profile bookkeeping is complete.)
|
|
14
|
+
|
|
15
|
+
SQLite cannot alter a UNIQUE constraint in place, so the table is rebuilt:
|
|
16
|
+
rename → create with profile_id + UNIQUE(profile_id, source_type, dedup_key) →
|
|
17
|
+
copy rows backfilling profile_id='default' → drop old. Idempotent and tolerant
|
|
18
|
+
of a missing table (fresh installs create it correctly via schema_v343).
|
|
19
|
+
|
|
20
|
+
Deferred because ingestion_log is created at engine init (apply_v343_schema),
|
|
21
|
+
after ``apply_all`` runs — same reason as M016.
|
|
22
|
+
|
|
23
|
+
Author: Varun Pratap Bhardwaj / Qualixar
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import sqlite3
|
|
29
|
+
|
|
30
|
+
NAME = "M021_ingestion_log_profile"
|
|
31
|
+
DB_TARGET = "memory"
|
|
32
|
+
|
|
33
|
+
# Documentation + drift hash. apply() below is the authoritative executor.
|
|
34
|
+
DDL = (
|
|
35
|
+
"ALTER TABLE ingestion_log ADD COLUMN profile_id TEXT NOT NULL DEFAULT 'default';"
|
|
36
|
+
"-- rebuild for UNIQUE(profile_id, source_type, dedup_key)"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
_NEW_TABLE = """
|
|
40
|
+
CREATE TABLE ingestion_log (
|
|
41
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
42
|
+
profile_id TEXT NOT NULL DEFAULT 'default',
|
|
43
|
+
source_type TEXT NOT NULL,
|
|
44
|
+
dedup_key TEXT NOT NULL,
|
|
45
|
+
fact_ids TEXT DEFAULT '[]',
|
|
46
|
+
metadata TEXT DEFAULT '{}',
|
|
47
|
+
status TEXT DEFAULT 'ingested',
|
|
48
|
+
ingested_at TEXT NOT NULL,
|
|
49
|
+
UNIQUE(profile_id, source_type, dedup_key)
|
|
50
|
+
)
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _cols(conn: sqlite3.Connection, table: str) -> set[str]:
|
|
55
|
+
return {r[1] for r in conn.execute(f"PRAGMA table_info({table})").fetchall()}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _table_exists(conn: sqlite3.Connection, table: str) -> bool:
|
|
59
|
+
return conn.execute(
|
|
60
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (table,)
|
|
61
|
+
).fetchone() is not None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def apply(conn: sqlite3.Connection) -> None:
|
|
65
|
+
"""Rebuild ingestion_log with a profile-scoped dedup constraint.
|
|
66
|
+
|
|
67
|
+
No-op on a fresh install (table already has profile_id) or when the table
|
|
68
|
+
doesn't exist yet. Existing rows backfill to the 'default' profile.
|
|
69
|
+
"""
|
|
70
|
+
if not _table_exists(conn, "ingestion_log"):
|
|
71
|
+
return
|
|
72
|
+
if "profile_id" in _cols(conn, "ingestion_log"):
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
conn.execute("ALTER TABLE ingestion_log RENAME TO _ingestion_log_old")
|
|
76
|
+
conn.executescript(_NEW_TABLE)
|
|
77
|
+
# Copy legacy rows under the 'default' profile. Column set matches the
|
|
78
|
+
# pre-migration schema (id, source_type, dedup_key, fact_ids, metadata,
|
|
79
|
+
# status, ingested_at).
|
|
80
|
+
old_cols = _cols(conn, "_ingestion_log_old")
|
|
81
|
+
has_meta = "metadata" in old_cols
|
|
82
|
+
if has_meta:
|
|
83
|
+
conn.execute(
|
|
84
|
+
"INSERT INTO ingestion_log "
|
|
85
|
+
"(id, profile_id, source_type, dedup_key, fact_ids, metadata, "
|
|
86
|
+
" status, ingested_at) "
|
|
87
|
+
"SELECT id, 'default', source_type, dedup_key, fact_ids, metadata, "
|
|
88
|
+
" status, ingested_at FROM _ingestion_log_old"
|
|
89
|
+
)
|
|
90
|
+
else:
|
|
91
|
+
conn.execute(
|
|
92
|
+
"INSERT INTO ingestion_log "
|
|
93
|
+
"(id, profile_id, source_type, dedup_key, fact_ids, status, ingested_at) "
|
|
94
|
+
"SELECT id, 'default', source_type, dedup_key, fact_ids, "
|
|
95
|
+
" status, ingested_at FROM _ingestion_log_old"
|
|
96
|
+
)
|
|
97
|
+
conn.execute("DROP TABLE _ingestion_log_old")
|
|
98
|
+
conn.execute(
|
|
99
|
+
"CREATE INDEX IF NOT EXISTS idx_ingestion_dedup "
|
|
100
|
+
"ON ingestion_log(profile_id, source_type, dedup_key)"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def verify(conn: sqlite3.Connection) -> bool:
|
|
105
|
+
"""Applied once ingestion_log carries profile_id (or is absent on fresh DB)."""
|
|
106
|
+
if not _table_exists(conn, "ingestion_log"):
|
|
107
|
+
return True # nothing to migrate; fresh install creates it correctly
|
|
108
|
+
return "profile_id" in _cols(conn, "ingestion_log")
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory — per-profile isolation (I-7)
|
|
4
|
+
|
|
5
|
+
"""M022 — profile_id on entity_aliases (memory.db, deferred).
|
|
6
|
+
|
|
7
|
+
entity_aliases had no profile_id, so aliases were keyed by entity_id alone. When
|
|
8
|
+
the same entity_id appears under two profiles, one profile's aliases were
|
|
9
|
+
visible to (and deduped against) the other. Add profile_id and backfill each
|
|
10
|
+
alias from its parent canonical entity's profile (the correct owner), falling
|
|
11
|
+
back to 'default' for orphans.
|
|
12
|
+
|
|
13
|
+
Additive column — no UNIQUE change, so a simple ADD COLUMN + backfill. Deferred
|
|
14
|
+
because entity_aliases is created at engine init (create_all_tables), after
|
|
15
|
+
apply_all runs — same reason as M016.
|
|
16
|
+
|
|
17
|
+
Author: Varun Pratap Bhardwaj / Qualixar
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import sqlite3
|
|
23
|
+
|
|
24
|
+
NAME = "M022_entity_aliases_profile"
|
|
25
|
+
DB_TARGET = "memory"
|
|
26
|
+
|
|
27
|
+
DDL = "ALTER TABLE entity_aliases ADD COLUMN profile_id TEXT NOT NULL DEFAULT 'default'"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _cols(conn: sqlite3.Connection, table: str) -> set[str]:
|
|
31
|
+
return {r[1] for r in conn.execute(f"PRAGMA table_info({table})").fetchall()}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _table_exists(conn: sqlite3.Connection, table: str) -> bool:
|
|
35
|
+
return conn.execute(
|
|
36
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (table,)
|
|
37
|
+
).fetchone() is not None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def apply(conn: sqlite3.Connection) -> None:
|
|
41
|
+
"""Add profile_id to entity_aliases and backfill from the parent entity.
|
|
42
|
+
|
|
43
|
+
No-op on a fresh install (column already present) or a missing table.
|
|
44
|
+
"""
|
|
45
|
+
if not _table_exists(conn, "entity_aliases"):
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
if "profile_id" not in _cols(conn, "entity_aliases"):
|
|
49
|
+
conn.execute(
|
|
50
|
+
"ALTER TABLE entity_aliases "
|
|
51
|
+
"ADD COLUMN profile_id TEXT NOT NULL DEFAULT 'default'"
|
|
52
|
+
)
|
|
53
|
+
# Backfill each alias from its parent canonical entity's profile. If the
|
|
54
|
+
# parent is missing (orphan) the COALESCE keeps 'default'.
|
|
55
|
+
if _table_exists(conn, "canonical_entities"):
|
|
56
|
+
conn.execute(
|
|
57
|
+
"UPDATE entity_aliases SET profile_id = COALESCE("
|
|
58
|
+
" (SELECT ce.profile_id FROM canonical_entities ce "
|
|
59
|
+
" WHERE ce.entity_id = entity_aliases.entity_id LIMIT 1), 'default')"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Always ensure the index (this migration owns it — schema.create_all_tables
|
|
63
|
+
# deliberately does NOT create it, since on an upgrade the column doesn't
|
|
64
|
+
# exist yet at create-all time). Idempotent for fresh installs too.
|
|
65
|
+
conn.execute(
|
|
66
|
+
"CREATE INDEX IF NOT EXISTS idx_aliases_profile_entity "
|
|
67
|
+
"ON entity_aliases(profile_id, entity_id)"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def verify(conn: sqlite3.Connection) -> bool:
|
|
72
|
+
"""Applied once entity_aliases has profile_id AND its index (or is absent).
|
|
73
|
+
|
|
74
|
+
Checking the index (not just the column) means a fresh install — where
|
|
75
|
+
create_all_tables made the column but not the index — still runs apply()
|
|
76
|
+
to create the index.
|
|
77
|
+
"""
|
|
78
|
+
if not _table_exists(conn, "entity_aliases"):
|
|
79
|
+
return True
|
|
80
|
+
if "profile_id" not in _cols(conn, "entity_aliases"):
|
|
81
|
+
return False
|
|
82
|
+
idx = conn.execute(
|
|
83
|
+
"SELECT 1 FROM sqlite_master WHERE type='index' "
|
|
84
|
+
"AND name='idx_aliases_profile_entity'"
|
|
85
|
+
).fetchone()
|
|
86
|
+
return idx is not None
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory — per-profile isolation (I-6 / C2)
|
|
4
|
+
|
|
5
|
+
"""M023 — profile_id on all mesh coordination tables (memory.db, deferred).
|
|
6
|
+
|
|
7
|
+
The mesh broker is a per-agent coordination bus (peers, notification messages,
|
|
8
|
+
shared key/value state, file locks, event log). Every mesh table was GLOBAL —
|
|
9
|
+
no ``profile_id`` — so in a multi-tenant deployment (profiles = individual /
|
|
10
|
+
team / company tenants) tenant A could see tenant B's peers, receive B's
|
|
11
|
+
broadcast messages, read/overwrite B's shared state by key collision, and be
|
|
12
|
+
blocked by B's file locks. That is a concrete cross-tenant isolation leak.
|
|
13
|
+
|
|
14
|
+
This migration makes the mesh namespace per-profile:
|
|
15
|
+
|
|
16
|
+
* mesh_peers, mesh_messages, mesh_events — additive ``profile_id`` column +
|
|
17
|
+
profile-leading indexes (no primary-key change).
|
|
18
|
+
* mesh_state — rebuilt with PRIMARY KEY(profile_id, key) so the same key can
|
|
19
|
+
exist independently per tenant.
|
|
20
|
+
* mesh_locks — rebuilt with PRIMARY KEY(profile_id, file_path) so one tenant's
|
|
21
|
+
lock on a path never blocks another tenant.
|
|
22
|
+
|
|
23
|
+
mesh_reads is intentionally left unchanged: it is (message_id, peer_id), both of
|
|
24
|
+
which are already profile-scoped by their parent rows, so a cross-profile read
|
|
25
|
+
row is unreachable.
|
|
26
|
+
|
|
27
|
+
Existing rows backfill to the 'default' profile (the default active profile).
|
|
28
|
+
Idempotent and tolerant of missing tables; deferred because the mesh tables are
|
|
29
|
+
created at engine init (apply_v343_schema), after ``apply_all`` runs — same
|
|
30
|
+
reason as M021.
|
|
31
|
+
|
|
32
|
+
Author: Varun Pratap Bhardwaj / Qualixar
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import sqlite3
|
|
38
|
+
|
|
39
|
+
NAME = "M023_mesh_profile_isolation"
|
|
40
|
+
DB_TARGET = "memory"
|
|
41
|
+
|
|
42
|
+
# Documentation + drift hash. apply() below is the authoritative executor.
|
|
43
|
+
DDL = (
|
|
44
|
+
"ALTER TABLE mesh_peers ADD COLUMN profile_id TEXT NOT NULL DEFAULT 'default';"
|
|
45
|
+
"ALTER TABLE mesh_messages ADD COLUMN profile_id TEXT NOT NULL DEFAULT 'default';"
|
|
46
|
+
"ALTER TABLE mesh_events ADD COLUMN profile_id TEXT NOT NULL DEFAULT 'default';"
|
|
47
|
+
"-- rebuild mesh_state PK(profile_id, key); mesh_locks PK(profile_id, file_path)"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
_NEW_STATE = """
|
|
51
|
+
CREATE TABLE mesh_state (
|
|
52
|
+
profile_id TEXT NOT NULL DEFAULT 'default',
|
|
53
|
+
key TEXT NOT NULL,
|
|
54
|
+
value TEXT NOT NULL,
|
|
55
|
+
set_by TEXT NOT NULL,
|
|
56
|
+
updated_at TEXT NOT NULL,
|
|
57
|
+
PRIMARY KEY (profile_id, key)
|
|
58
|
+
)
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
_NEW_LOCKS = """
|
|
62
|
+
CREATE TABLE mesh_locks (
|
|
63
|
+
profile_id TEXT NOT NULL DEFAULT 'default',
|
|
64
|
+
file_path TEXT NOT NULL,
|
|
65
|
+
locked_by TEXT NOT NULL,
|
|
66
|
+
locked_at TEXT NOT NULL,
|
|
67
|
+
expires_at TEXT NOT NULL DEFAULT '9999-12-31T23:59:59Z',
|
|
68
|
+
PRIMARY KEY (profile_id, file_path)
|
|
69
|
+
)
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _cols(conn: sqlite3.Connection, table: str) -> set[str]:
|
|
74
|
+
return {r[1] for r in conn.execute(f"PRAGMA table_info({table})").fetchall()}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _table_exists(conn: sqlite3.Connection, table: str) -> bool:
|
|
78
|
+
return conn.execute(
|
|
79
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (table,)
|
|
80
|
+
).fetchone() is not None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _add_profile_column(conn: sqlite3.Connection, table: str) -> None:
|
|
84
|
+
"""Additive profile_id on a table whose primary key does not change."""
|
|
85
|
+
if not _table_exists(conn, table):
|
|
86
|
+
return
|
|
87
|
+
if "profile_id" not in _cols(conn, table):
|
|
88
|
+
conn.execute(
|
|
89
|
+
f"ALTER TABLE {table} "
|
|
90
|
+
"ADD COLUMN profile_id TEXT NOT NULL DEFAULT 'default'"
|
|
91
|
+
)
|
|
92
|
+
# New column defaults 'default' for every legacy row; no extra UPDATE
|
|
93
|
+
# needed. (NOT NULL DEFAULT is applied to existing rows by SQLite.)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _atomic_rebuild(conn: sqlite3.Connection, table: str, create_sql: str,
|
|
97
|
+
insert_sql: str) -> None:
|
|
98
|
+
"""RENAME→CREATE→copy→DROP as ONE transaction.
|
|
99
|
+
|
|
100
|
+
Uses conn.execute (NOT executescript, which force-commits mid-rebuild and
|
|
101
|
+
could leave a zombie *_old table with the only copy of the data if a later
|
|
102
|
+
step fails). BEGIN IMMEDIATE + explicit COMMIT/ROLLBACK guarantees the
|
|
103
|
+
rebuild is all-or-nothing even under the runner's autocommit connection.
|
|
104
|
+
"""
|
|
105
|
+
if not _table_exists(conn, table):
|
|
106
|
+
return
|
|
107
|
+
if "profile_id" in _cols(conn, table):
|
|
108
|
+
return
|
|
109
|
+
old = f"_{table}_old"
|
|
110
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
111
|
+
try:
|
|
112
|
+
conn.execute(f"ALTER TABLE {table} RENAME TO {old}")
|
|
113
|
+
conn.execute(create_sql.strip().rstrip(";"))
|
|
114
|
+
conn.execute(insert_sql.replace("__OLD__", old))
|
|
115
|
+
conn.execute(f"DROP TABLE {old}")
|
|
116
|
+
conn.execute("COMMIT")
|
|
117
|
+
except Exception:
|
|
118
|
+
conn.execute("ROLLBACK")
|
|
119
|
+
raise
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _rebuild_state(conn: sqlite3.Connection) -> None:
|
|
123
|
+
_atomic_rebuild(
|
|
124
|
+
conn, "mesh_state", _NEW_STATE,
|
|
125
|
+
"INSERT INTO mesh_state (profile_id, key, value, set_by, updated_at) "
|
|
126
|
+
"SELECT 'default', key, value, set_by, updated_at FROM __OLD__",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _rebuild_locks(conn: sqlite3.Connection) -> None:
|
|
131
|
+
_atomic_rebuild(
|
|
132
|
+
conn, "mesh_locks", _NEW_LOCKS,
|
|
133
|
+
"INSERT INTO mesh_locks "
|
|
134
|
+
"(profile_id, file_path, locked_by, locked_at, expires_at) "
|
|
135
|
+
"SELECT 'default', file_path, locked_by, locked_at, expires_at FROM __OLD__",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _indexes(conn: sqlite3.Connection) -> None:
|
|
140
|
+
"""Profile-leading indexes. Created here (not in schema_v343) so an upgrade
|
|
141
|
+
never runs CREATE INDEX on a column that does not exist yet."""
|
|
142
|
+
if _table_exists(conn, "mesh_peers") and "profile_id" in _cols(conn, "mesh_peers"):
|
|
143
|
+
peer_cols = _cols(conn, "mesh_peers")
|
|
144
|
+
if "session_id" in peer_cols:
|
|
145
|
+
conn.execute(
|
|
146
|
+
"CREATE INDEX IF NOT EXISTS idx_mesh_peers_profile "
|
|
147
|
+
"ON mesh_peers(profile_id, session_id)"
|
|
148
|
+
)
|
|
149
|
+
if "status" in peer_cols:
|
|
150
|
+
conn.execute(
|
|
151
|
+
"CREATE INDEX IF NOT EXISTS idx_mesh_peers_profile_status "
|
|
152
|
+
"ON mesh_peers(profile_id, status)"
|
|
153
|
+
)
|
|
154
|
+
if _table_exists(conn, "mesh_messages") and "profile_id" in _cols(conn, "mesh_messages"):
|
|
155
|
+
msg_cols = _cols(conn, "mesh_messages")
|
|
156
|
+
conn.execute(
|
|
157
|
+
"CREATE INDEX IF NOT EXISTS idx_mesh_messages_profile_to "
|
|
158
|
+
"ON mesh_messages(profile_id, to_peer, read)"
|
|
159
|
+
)
|
|
160
|
+
# target_type / project_path are v3.4.6 additions. In the normal upgrade
|
|
161
|
+
# path they exist (schema_v343 applies its ALTERs before deferred
|
|
162
|
+
# migrations run), but guard anyway so M023 is robust on any DB.
|
|
163
|
+
if {"target_type", "project_path"} <= msg_cols:
|
|
164
|
+
conn.execute(
|
|
165
|
+
"CREATE INDEX IF NOT EXISTS idx_mesh_messages_profile_target "
|
|
166
|
+
"ON mesh_messages(profile_id, target_type, project_path)"
|
|
167
|
+
)
|
|
168
|
+
if _table_exists(conn, "mesh_events") and "profile_id" in _cols(conn, "mesh_events"):
|
|
169
|
+
conn.execute(
|
|
170
|
+
"CREATE INDEX IF NOT EXISTS idx_mesh_events_profile "
|
|
171
|
+
"ON mesh_events(profile_id, id)"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def apply(conn: sqlite3.Connection) -> None:
|
|
176
|
+
"""Profile-scope every mesh table. Idempotent; no-op on a fresh/migrated DB.
|
|
177
|
+
|
|
178
|
+
Existing rows backfill to the 'default' profile.
|
|
179
|
+
"""
|
|
180
|
+
_add_profile_column(conn, "mesh_peers")
|
|
181
|
+
_add_profile_column(conn, "mesh_messages")
|
|
182
|
+
_add_profile_column(conn, "mesh_events")
|
|
183
|
+
_rebuild_state(conn)
|
|
184
|
+
_rebuild_locks(conn)
|
|
185
|
+
_indexes(conn)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def verify(conn: sqlite3.Connection) -> bool:
|
|
189
|
+
"""Applied once every mesh table carries profile_id (or is absent)."""
|
|
190
|
+
for table in ("mesh_peers", "mesh_messages", "mesh_events",
|
|
191
|
+
"mesh_state", "mesh_locks"):
|
|
192
|
+
if _table_exists(conn, table) and "profile_id" not in _cols(conn, table):
|
|
193
|
+
return False
|
|
194
|
+
return True
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 — RBAC / teams (C3)
|
|
4
|
+
|
|
5
|
+
"""M024 — user identity + role-based access control tables (memory.db).
|
|
6
|
+
|
|
7
|
+
SLM's tenancy boundary is the profile (individual / team / company). Until now
|
|
8
|
+
the daemon had no *user* identity — access was machine-level (install token /
|
|
9
|
+
daemon capability). For team and company deployments an org needs real users,
|
|
10
|
+
each with a role on a profile, managed from the dashboard.
|
|
11
|
+
|
|
12
|
+
Adopts the Cognee reference model (Tenant -> User -> Role):
|
|
13
|
+
* rbac_users — a person/agent identity with a hashed password.
|
|
14
|
+
* rbac_memberships — (profile_id, user_id) -> role, the tenant/role grant.
|
|
15
|
+
* rbac_sessions — dashboard login sessions (hashed bearer token + expiry).
|
|
16
|
+
|
|
17
|
+
RBAC is additive: with zero users the daemon stays single-operator (the machine
|
|
18
|
+
operator over loopback is the implicit owner). Once users + memberships exist,
|
|
19
|
+
logged-in users are enforced against their role. Brand-new independent tables,
|
|
20
|
+
so this is a normal (pre-engine-init) migration with an idempotent apply().
|
|
21
|
+
|
|
22
|
+
Author: Varun Pratap Bhardwaj / Qualixar
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import sqlite3
|
|
28
|
+
|
|
29
|
+
NAME = "M024_rbac_users_roles"
|
|
30
|
+
DB_TARGET = "memory"
|
|
31
|
+
|
|
32
|
+
DDL = """
|
|
33
|
+
CREATE TABLE IF NOT EXISTS rbac_users (
|
|
34
|
+
user_id TEXT PRIMARY KEY,
|
|
35
|
+
username TEXT NOT NULL UNIQUE,
|
|
36
|
+
display_name TEXT DEFAULT '',
|
|
37
|
+
password_hash TEXT NOT NULL,
|
|
38
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
39
|
+
created_at TEXT NOT NULL,
|
|
40
|
+
created_by TEXT DEFAULT 'owner'
|
|
41
|
+
);
|
|
42
|
+
CREATE TABLE IF NOT EXISTS rbac_memberships (
|
|
43
|
+
profile_id TEXT NOT NULL,
|
|
44
|
+
user_id TEXT NOT NULL,
|
|
45
|
+
role TEXT NOT NULL,
|
|
46
|
+
added_at TEXT NOT NULL,
|
|
47
|
+
added_by TEXT DEFAULT 'owner',
|
|
48
|
+
PRIMARY KEY (profile_id, user_id)
|
|
49
|
+
);
|
|
50
|
+
CREATE TABLE IF NOT EXISTS rbac_sessions (
|
|
51
|
+
token_hash TEXT PRIMARY KEY,
|
|
52
|
+
user_id TEXT NOT NULL,
|
|
53
|
+
created_at TEXT NOT NULL,
|
|
54
|
+
expires_at TEXT NOT NULL,
|
|
55
|
+
last_seen TEXT NOT NULL
|
|
56
|
+
);
|
|
57
|
+
CREATE TABLE IF NOT EXISTS rbac_settings (
|
|
58
|
+
key TEXT PRIMARY KEY,
|
|
59
|
+
value TEXT NOT NULL
|
|
60
|
+
);
|
|
61
|
+
CREATE INDEX IF NOT EXISTS idx_rbac_memberships_user
|
|
62
|
+
ON rbac_memberships(user_id);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_rbac_sessions_user
|
|
64
|
+
ON rbac_sessions(user_id);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_rbac_sessions_expiry
|
|
66
|
+
ON rbac_sessions(expires_at);
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _table_exists(conn: sqlite3.Connection, table: str) -> bool:
|
|
71
|
+
return conn.execute(
|
|
72
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (table,)
|
|
73
|
+
).fetchone() is not None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def apply(conn: sqlite3.Connection) -> None:
|
|
77
|
+
"""Create the RBAC tables. Idempotent (CREATE TABLE IF NOT EXISTS)."""
|
|
78
|
+
conn.executescript(DDL)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def verify(conn: sqlite3.Connection) -> bool:
|
|
82
|
+
"""Applied once all RBAC tables exist."""
|
|
83
|
+
return all(
|
|
84
|
+
_table_exists(conn, t)
|
|
85
|
+
for t in ("rbac_users", "rbac_memberships", "rbac_sessions",
|
|
86
|
+
"rbac_settings")
|
|
87
|
+
)
|