superlocalmemory 2.8.5 → 3.0.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.
- package/CHANGELOG.md +11 -0
- package/LICENSE +9 -1
- package/NOTICE +63 -0
- package/README.md +165 -480
- package/bin/slm +17 -449
- package/bin/slm-npm +2 -2
- package/bin/slm.bat +4 -2
- package/conftest.py +5 -0
- package/docs/api-reference.md +284 -0
- package/docs/architecture.md +149 -0
- package/docs/auto-memory.md +150 -0
- package/docs/cli-reference.md +276 -0
- package/docs/compliance.md +191 -0
- package/docs/configuration.md +182 -0
- package/docs/getting-started.md +102 -0
- package/docs/ide-setup.md +261 -0
- package/docs/mcp-tools.md +220 -0
- package/docs/migration-from-v2.md +170 -0
- package/docs/profiles.md +173 -0
- package/docs/troubleshooting.md +310 -0
- package/{configs → ide/configs}/antigravity-mcp.json +3 -3
- package/ide/configs/chatgpt-desktop-mcp.json +16 -0
- package/{configs → ide/configs}/claude-desktop-mcp.json +3 -3
- package/{configs → ide/configs}/codex-mcp.toml +4 -4
- package/{configs → ide/configs}/continue-mcp.yaml +4 -3
- package/{configs → ide/configs}/continue-skills.yaml +6 -6
- package/ide/configs/cursor-mcp.json +15 -0
- package/{configs → ide/configs}/gemini-cli-mcp.json +2 -2
- package/{configs → ide/configs}/jetbrains-mcp.json +2 -2
- package/{configs → ide/configs}/opencode-mcp.json +2 -2
- package/{configs → ide/configs}/perplexity-mcp.json +2 -2
- package/{configs → ide/configs}/vscode-copilot-mcp.json +2 -2
- package/{configs → ide/configs}/windsurf-mcp.json +3 -3
- package/{configs → ide/configs}/zed-mcp.json +2 -2
- package/{hooks → ide/hooks}/context-hook.js +9 -20
- package/ide/hooks/memory-list-skill.js +70 -0
- package/ide/hooks/memory-profile-skill.js +101 -0
- package/ide/hooks/memory-recall-skill.js +62 -0
- package/ide/hooks/memory-remember-skill.js +68 -0
- package/ide/hooks/memory-reset-skill.js +160 -0
- package/{hooks → ide/hooks}/post-recall-hook.js +2 -2
- package/ide/integrations/langchain/README.md +106 -0
- package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +9 -0
- package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +201 -0
- package/ide/integrations/langchain/pyproject.toml +38 -0
- package/{src/learning → ide/integrations/langchain}/tests/__init__.py +1 -0
- package/ide/integrations/langchain/tests/test_chat_message_history.py +215 -0
- package/ide/integrations/langchain/tests/test_security.py +117 -0
- package/ide/integrations/llamaindex/README.md +81 -0
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +9 -0
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +316 -0
- package/ide/integrations/llamaindex/pyproject.toml +43 -0
- package/{src/lifecycle → ide/integrations/llamaindex}/tests/__init__.py +1 -2
- package/ide/integrations/llamaindex/tests/test_chat_store.py +294 -0
- package/ide/integrations/llamaindex/tests/test_security.py +241 -0
- package/{skills → ide/skills}/slm-build-graph/SKILL.md +6 -6
- package/{skills → ide/skills}/slm-list-recent/SKILL.md +5 -5
- package/{skills → ide/skills}/slm-recall/SKILL.md +5 -5
- package/{skills → ide/skills}/slm-remember/SKILL.md +6 -6
- package/{skills → ide/skills}/slm-show-patterns/SKILL.md +7 -7
- package/{skills → ide/skills}/slm-status/SKILL.md +9 -9
- package/{skills → ide/skills}/slm-switch-profile/SKILL.md +9 -9
- package/package.json +13 -22
- package/pyproject.toml +85 -0
- package/scripts/build-dmg.sh +417 -0
- package/scripts/install-skills.ps1 +334 -0
- package/{install.ps1 → scripts/install.ps1} +36 -4
- package/{install.sh → scripts/install.sh} +14 -13
- package/scripts/postinstall.js +2 -2
- package/scripts/start-dashboard.ps1 +52 -0
- package/scripts/start-dashboard.sh +41 -0
- package/scripts/sync-wiki.ps1 +127 -0
- package/scripts/sync-wiki.sh +82 -0
- package/scripts/test-dmg.sh +161 -0
- package/scripts/test-npm-package.ps1 +252 -0
- package/scripts/test-npm-package.sh +207 -0
- package/scripts/verify-install.ps1 +294 -0
- package/scripts/verify-install.sh +266 -0
- package/src/superlocalmemory/__init__.py +0 -0
- package/src/superlocalmemory/attribution/__init__.py +9 -0
- package/src/superlocalmemory/attribution/mathematical_dna.py +235 -0
- package/src/superlocalmemory/attribution/signer.py +153 -0
- package/src/superlocalmemory/attribution/watermark.py +189 -0
- package/src/superlocalmemory/cli/__init__.py +5 -0
- package/src/superlocalmemory/cli/commands.py +245 -0
- package/src/superlocalmemory/cli/main.py +89 -0
- package/src/superlocalmemory/cli/migrate_cmd.py +55 -0
- package/src/superlocalmemory/cli/post_install.py +99 -0
- package/src/superlocalmemory/cli/setup_wizard.py +129 -0
- package/src/superlocalmemory/compliance/__init__.py +0 -0
- package/src/superlocalmemory/compliance/abac.py +204 -0
- package/src/superlocalmemory/compliance/audit.py +314 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +131 -0
- package/src/superlocalmemory/compliance/gdpr.py +294 -0
- package/src/superlocalmemory/compliance/lifecycle.py +158 -0
- package/src/superlocalmemory/compliance/retention.py +232 -0
- package/src/superlocalmemory/compliance/scheduler.py +148 -0
- package/src/superlocalmemory/core/__init__.py +0 -0
- package/src/superlocalmemory/core/config.py +391 -0
- package/src/superlocalmemory/core/embeddings.py +293 -0
- package/src/superlocalmemory/core/engine.py +701 -0
- package/src/superlocalmemory/core/hooks.py +65 -0
- package/src/superlocalmemory/core/maintenance.py +172 -0
- package/src/superlocalmemory/core/modes.py +140 -0
- package/src/superlocalmemory/core/profiles.py +234 -0
- package/src/superlocalmemory/core/registry.py +117 -0
- package/src/superlocalmemory/dynamics/__init__.py +0 -0
- package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +223 -0
- package/src/superlocalmemory/encoding/__init__.py +0 -0
- package/src/superlocalmemory/encoding/consolidator.py +485 -0
- package/src/superlocalmemory/encoding/emotional.py +125 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +525 -0
- package/src/superlocalmemory/encoding/entropy_gate.py +104 -0
- package/src/superlocalmemory/encoding/fact_extractor.py +775 -0
- package/src/superlocalmemory/encoding/foresight.py +91 -0
- package/src/superlocalmemory/encoding/graph_builder.py +302 -0
- package/src/superlocalmemory/encoding/observation_builder.py +160 -0
- package/src/superlocalmemory/encoding/scene_builder.py +183 -0
- package/src/superlocalmemory/encoding/signal_inference.py +90 -0
- package/src/superlocalmemory/encoding/temporal_parser.py +426 -0
- package/src/superlocalmemory/encoding/type_router.py +235 -0
- package/src/superlocalmemory/hooks/__init__.py +3 -0
- package/src/superlocalmemory/hooks/auto_capture.py +111 -0
- package/src/superlocalmemory/hooks/auto_recall.py +93 -0
- package/src/superlocalmemory/hooks/ide_connector.py +204 -0
- package/src/superlocalmemory/hooks/rules_engine.py +99 -0
- package/src/superlocalmemory/infra/__init__.py +3 -0
- package/src/superlocalmemory/infra/auth_middleware.py +82 -0
- package/src/superlocalmemory/infra/backup.py +317 -0
- package/src/superlocalmemory/infra/cache_manager.py +267 -0
- package/src/superlocalmemory/infra/event_bus.py +381 -0
- package/src/superlocalmemory/infra/rate_limiter.py +135 -0
- package/src/{webhook_dispatcher.py → superlocalmemory/infra/webhook_dispatcher.py} +104 -101
- package/src/superlocalmemory/learning/__init__.py +0 -0
- package/src/superlocalmemory/learning/adaptive.py +172 -0
- package/src/superlocalmemory/learning/behavioral.py +490 -0
- package/src/superlocalmemory/learning/behavioral_listener.py +94 -0
- package/src/superlocalmemory/learning/bootstrap.py +298 -0
- package/src/superlocalmemory/learning/cross_project.py +399 -0
- package/src/superlocalmemory/learning/database.py +376 -0
- package/src/superlocalmemory/learning/engagement.py +323 -0
- package/src/superlocalmemory/learning/features.py +138 -0
- package/src/superlocalmemory/learning/feedback.py +316 -0
- package/src/superlocalmemory/learning/outcomes.py +255 -0
- package/src/superlocalmemory/learning/project_context.py +366 -0
- package/src/superlocalmemory/learning/ranker.py +155 -0
- package/src/superlocalmemory/learning/source_quality.py +303 -0
- package/src/superlocalmemory/learning/workflows.py +309 -0
- package/src/superlocalmemory/llm/__init__.py +0 -0
- package/src/superlocalmemory/llm/backbone.py +316 -0
- package/src/superlocalmemory/math/__init__.py +0 -0
- package/src/superlocalmemory/math/fisher.py +356 -0
- package/src/superlocalmemory/math/langevin.py +398 -0
- package/src/superlocalmemory/math/sheaf.py +257 -0
- package/src/superlocalmemory/mcp/__init__.py +0 -0
- package/src/superlocalmemory/mcp/resources.py +245 -0
- package/src/superlocalmemory/mcp/server.py +61 -0
- package/src/superlocalmemory/mcp/tools.py +18 -0
- package/src/superlocalmemory/mcp/tools_core.py +305 -0
- package/src/superlocalmemory/mcp/tools_v28.py +223 -0
- package/src/superlocalmemory/mcp/tools_v3.py +286 -0
- package/src/superlocalmemory/retrieval/__init__.py +0 -0
- package/src/superlocalmemory/retrieval/agentic.py +295 -0
- package/src/superlocalmemory/retrieval/ann_index.py +223 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +185 -0
- package/src/superlocalmemory/retrieval/bridge_discovery.py +170 -0
- package/src/superlocalmemory/retrieval/engine.py +390 -0
- package/src/superlocalmemory/retrieval/entity_channel.py +179 -0
- package/src/superlocalmemory/retrieval/fusion.py +78 -0
- package/src/superlocalmemory/retrieval/profile_channel.py +105 -0
- package/src/superlocalmemory/retrieval/reranker.py +154 -0
- package/src/superlocalmemory/retrieval/semantic_channel.py +232 -0
- package/src/superlocalmemory/retrieval/strategy.py +96 -0
- package/src/superlocalmemory/retrieval/temporal_channel.py +175 -0
- package/src/superlocalmemory/server/__init__.py +1 -0
- package/src/superlocalmemory/server/api.py +248 -0
- package/src/superlocalmemory/server/routes/__init__.py +4 -0
- package/src/superlocalmemory/server/routes/agents.py +107 -0
- package/src/superlocalmemory/server/routes/backup.py +91 -0
- package/src/superlocalmemory/server/routes/behavioral.py +127 -0
- package/src/superlocalmemory/server/routes/compliance.py +160 -0
- package/src/superlocalmemory/server/routes/data_io.py +188 -0
- package/src/superlocalmemory/server/routes/events.py +183 -0
- package/src/superlocalmemory/server/routes/helpers.py +85 -0
- package/src/superlocalmemory/server/routes/learning.py +273 -0
- package/src/superlocalmemory/server/routes/lifecycle.py +116 -0
- package/src/superlocalmemory/server/routes/memories.py +399 -0
- package/src/superlocalmemory/server/routes/profiles.py +219 -0
- package/src/superlocalmemory/server/routes/stats.py +346 -0
- package/src/superlocalmemory/server/routes/v3_api.py +365 -0
- package/src/superlocalmemory/server/routes/ws.py +82 -0
- package/src/superlocalmemory/server/security_middleware.py +57 -0
- package/src/superlocalmemory/server/ui.py +245 -0
- package/src/superlocalmemory/storage/__init__.py +0 -0
- package/src/superlocalmemory/storage/access_control.py +182 -0
- package/src/superlocalmemory/storage/database.py +594 -0
- package/src/superlocalmemory/storage/migrations.py +303 -0
- package/src/superlocalmemory/storage/models.py +406 -0
- package/src/superlocalmemory/storage/schema.py +726 -0
- package/src/superlocalmemory/storage/v2_migrator.py +317 -0
- package/src/superlocalmemory/trust/__init__.py +0 -0
- package/src/superlocalmemory/trust/gate.py +130 -0
- package/src/superlocalmemory/trust/provenance.py +124 -0
- package/src/superlocalmemory/trust/scorer.py +347 -0
- package/src/superlocalmemory/trust/signals.py +153 -0
- package/ui/index.html +278 -5
- package/ui/js/auto-settings.js +70 -0
- package/ui/js/dashboard.js +90 -0
- package/ui/js/fact-detail.js +92 -0
- package/ui/js/feedback.js +2 -2
- package/ui/js/ide-status.js +102 -0
- package/ui/js/math-health.js +98 -0
- package/ui/js/recall-lab.js +127 -0
- package/ui/js/settings.js +2 -2
- package/ui/js/trust-dashboard.js +73 -0
- package/api_server.py +0 -724
- package/bin/aider-smart +0 -72
- package/bin/superlocalmemoryv2-learning +0 -4
- package/bin/superlocalmemoryv2-list +0 -3
- package/bin/superlocalmemoryv2-patterns +0 -4
- package/bin/superlocalmemoryv2-profile +0 -3
- package/bin/superlocalmemoryv2-recall +0 -3
- package/bin/superlocalmemoryv2-remember +0 -3
- package/bin/superlocalmemoryv2-reset +0 -3
- package/bin/superlocalmemoryv2-status +0 -3
- package/configs/chatgpt-desktop-mcp.json +0 -16
- package/configs/cursor-mcp.json +0 -15
- package/docs/SECURITY-QUICK-REFERENCE.md +0 -214
- package/hooks/memory-list-skill.js +0 -139
- package/hooks/memory-profile-skill.js +0 -273
- package/hooks/memory-recall-skill.js +0 -114
- package/hooks/memory-remember-skill.js +0 -127
- package/hooks/memory-reset-skill.js +0 -274
- package/mcp_server.py +0 -1800
- package/requirements-core.txt +0 -22
- package/requirements-learning.txt +0 -12
- package/requirements.txt +0 -12
- package/src/agent_registry.py +0 -411
- package/src/auth_middleware.py +0 -61
- package/src/auto_backup.py +0 -459
- package/src/behavioral/__init__.py +0 -49
- package/src/behavioral/behavioral_listener.py +0 -203
- package/src/behavioral/behavioral_patterns.py +0 -275
- package/src/behavioral/cross_project_transfer.py +0 -206
- package/src/behavioral/outcome_inference.py +0 -194
- package/src/behavioral/outcome_tracker.py +0 -193
- package/src/behavioral/tests/__init__.py +0 -4
- package/src/behavioral/tests/test_behavioral_integration.py +0 -108
- package/src/behavioral/tests/test_behavioral_patterns.py +0 -150
- package/src/behavioral/tests/test_cross_project_transfer.py +0 -142
- package/src/behavioral/tests/test_mcp_behavioral.py +0 -139
- package/src/behavioral/tests/test_mcp_report_outcome.py +0 -117
- package/src/behavioral/tests/test_outcome_inference.py +0 -107
- package/src/behavioral/tests/test_outcome_tracker.py +0 -96
- package/src/cache_manager.py +0 -518
- package/src/compliance/__init__.py +0 -48
- package/src/compliance/abac_engine.py +0 -149
- package/src/compliance/abac_middleware.py +0 -116
- package/src/compliance/audit_db.py +0 -215
- package/src/compliance/audit_logger.py +0 -148
- package/src/compliance/retention_manager.py +0 -289
- package/src/compliance/retention_scheduler.py +0 -186
- package/src/compliance/tests/__init__.py +0 -4
- package/src/compliance/tests/test_abac_enforcement.py +0 -95
- package/src/compliance/tests/test_abac_engine.py +0 -124
- package/src/compliance/tests/test_abac_mcp_integration.py +0 -118
- package/src/compliance/tests/test_audit_db.py +0 -123
- package/src/compliance/tests/test_audit_logger.py +0 -98
- package/src/compliance/tests/test_mcp_audit.py +0 -128
- package/src/compliance/tests/test_mcp_retention_policy.py +0 -125
- package/src/compliance/tests/test_retention_manager.py +0 -131
- package/src/compliance/tests/test_retention_scheduler.py +0 -99
- package/src/compression/__init__.py +0 -25
- package/src/compression/cli.py +0 -150
- package/src/compression/cold_storage.py +0 -217
- package/src/compression/config.py +0 -72
- package/src/compression/orchestrator.py +0 -133
- package/src/compression/tier2_compressor.py +0 -228
- package/src/compression/tier3_compressor.py +0 -153
- package/src/compression/tier_classifier.py +0 -148
- package/src/db_connection_manager.py +0 -536
- package/src/embedding_engine.py +0 -63
- package/src/embeddings/__init__.py +0 -47
- package/src/embeddings/cache.py +0 -70
- package/src/embeddings/cli.py +0 -113
- package/src/embeddings/constants.py +0 -47
- package/src/embeddings/database.py +0 -91
- package/src/embeddings/engine.py +0 -247
- package/src/embeddings/model_loader.py +0 -145
- package/src/event_bus.py +0 -562
- package/src/graph/__init__.py +0 -36
- package/src/graph/build_helpers.py +0 -74
- package/src/graph/cli.py +0 -87
- package/src/graph/cluster_builder.py +0 -188
- package/src/graph/cluster_summary.py +0 -148
- package/src/graph/constants.py +0 -47
- package/src/graph/edge_builder.py +0 -162
- package/src/graph/entity_extractor.py +0 -95
- package/src/graph/graph_core.py +0 -226
- package/src/graph/graph_search.py +0 -231
- package/src/graph/hierarchical.py +0 -207
- package/src/graph/schema.py +0 -99
- package/src/graph_engine.py +0 -52
- package/src/hnsw_index.py +0 -628
- package/src/hybrid_search.py +0 -46
- package/src/learning/__init__.py +0 -217
- package/src/learning/adaptive_ranker.py +0 -682
- package/src/learning/bootstrap/__init__.py +0 -69
- package/src/learning/bootstrap/constants.py +0 -93
- package/src/learning/bootstrap/db_queries.py +0 -316
- package/src/learning/bootstrap/sampling.py +0 -82
- package/src/learning/bootstrap/text_utils.py +0 -71
- package/src/learning/cross_project_aggregator.py +0 -857
- package/src/learning/db/__init__.py +0 -40
- package/src/learning/db/constants.py +0 -44
- package/src/learning/db/schema.py +0 -279
- package/src/learning/engagement_tracker.py +0 -628
- package/src/learning/feature_extractor.py +0 -708
- package/src/learning/feedback_collector.py +0 -806
- package/src/learning/learning_db.py +0 -915
- package/src/learning/project_context_manager.py +0 -572
- package/src/learning/ranking/__init__.py +0 -33
- package/src/learning/ranking/constants.py +0 -84
- package/src/learning/ranking/helpers.py +0 -278
- package/src/learning/source_quality_scorer.py +0 -676
- package/src/learning/synthetic_bootstrap.py +0 -755
- package/src/learning/tests/test_adaptive_ranker.py +0 -325
- package/src/learning/tests/test_adaptive_ranker_v28.py +0 -60
- package/src/learning/tests/test_aggregator.py +0 -306
- package/src/learning/tests/test_auto_retrain_v28.py +0 -35
- package/src/learning/tests/test_e2e_ranking_v28.py +0 -82
- package/src/learning/tests/test_feature_extractor_v28.py +0 -93
- package/src/learning/tests/test_feedback_collector.py +0 -294
- package/src/learning/tests/test_learning_db.py +0 -602
- package/src/learning/tests/test_learning_db_v28.py +0 -110
- package/src/learning/tests/test_learning_init_v28.py +0 -48
- package/src/learning/tests/test_outcome_signals.py +0 -48
- package/src/learning/tests/test_project_context.py +0 -292
- package/src/learning/tests/test_schema_migration.py +0 -319
- package/src/learning/tests/test_signal_inference.py +0 -397
- package/src/learning/tests/test_source_quality.py +0 -351
- package/src/learning/tests/test_synthetic_bootstrap.py +0 -429
- package/src/learning/tests/test_workflow_miner.py +0 -318
- package/src/learning/workflow_pattern_miner.py +0 -655
- package/src/lifecycle/__init__.py +0 -54
- package/src/lifecycle/bounded_growth.py +0 -239
- package/src/lifecycle/compaction_engine.py +0 -226
- package/src/lifecycle/lifecycle_engine.py +0 -355
- package/src/lifecycle/lifecycle_evaluator.py +0 -257
- package/src/lifecycle/lifecycle_scheduler.py +0 -130
- package/src/lifecycle/retention_policy.py +0 -285
- package/src/lifecycle/tests/test_bounded_growth.py +0 -193
- package/src/lifecycle/tests/test_compaction.py +0 -179
- package/src/lifecycle/tests/test_lifecycle_engine.py +0 -137
- package/src/lifecycle/tests/test_lifecycle_evaluation.py +0 -177
- package/src/lifecycle/tests/test_lifecycle_scheduler.py +0 -127
- package/src/lifecycle/tests/test_lifecycle_search.py +0 -109
- package/src/lifecycle/tests/test_mcp_compact.py +0 -149
- package/src/lifecycle/tests/test_mcp_lifecycle_status.py +0 -114
- package/src/lifecycle/tests/test_retention_policy.py +0 -162
- package/src/mcp_tools_v28.py +0 -281
- package/src/memory/__init__.py +0 -36
- package/src/memory/cli.py +0 -205
- package/src/memory/constants.py +0 -39
- package/src/memory/helpers.py +0 -28
- package/src/memory/schema.py +0 -166
- package/src/memory-profiles.py +0 -595
- package/src/memory-reset.py +0 -491
- package/src/memory_compression.py +0 -989
- package/src/memory_store_v2.py +0 -1155
- package/src/migrate_v1_to_v2.py +0 -629
- package/src/pattern_learner.py +0 -34
- package/src/patterns/__init__.py +0 -24
- package/src/patterns/analyzers.py +0 -251
- package/src/patterns/learner.py +0 -271
- package/src/patterns/scoring.py +0 -171
- package/src/patterns/store.py +0 -225
- package/src/patterns/terminology.py +0 -140
- package/src/provenance_tracker.py +0 -312
- package/src/qualixar_attribution.py +0 -139
- package/src/qualixar_watermark.py +0 -78
- package/src/query_optimizer.py +0 -511
- package/src/rate_limiter.py +0 -83
- package/src/search/__init__.py +0 -20
- package/src/search/cli.py +0 -77
- package/src/search/constants.py +0 -26
- package/src/search/engine.py +0 -241
- package/src/search/fusion.py +0 -122
- package/src/search/index_loader.py +0 -114
- package/src/search/methods.py +0 -162
- package/src/search_engine_v2.py +0 -401
- package/src/setup_validator.py +0 -482
- package/src/subscription_manager.py +0 -391
- package/src/tree/__init__.py +0 -59
- package/src/tree/builder.py +0 -185
- package/src/tree/nodes.py +0 -202
- package/src/tree/queries.py +0 -257
- package/src/tree/schema.py +0 -80
- package/src/tree_manager.py +0 -19
- package/src/trust/__init__.py +0 -45
- package/src/trust/constants.py +0 -66
- package/src/trust/queries.py +0 -157
- package/src/trust/schema.py +0 -95
- package/src/trust/scorer.py +0 -299
- package/src/trust/signals.py +0 -95
- package/src/trust_scorer.py +0 -44
- package/ui/app.js +0 -1588
- package/ui/js/graph-cytoscape-monolithic-backup.js +0 -1168
- package/ui/js/graph-cytoscape.js +0 -1168
- package/ui/js/graph-d3-backup.js +0 -32
- package/ui/js/graph.js +0 -32
- package/ui_server.py +0 -266
- /package/docs/{ACCESSIBILITY.md → v2-archive/ACCESSIBILITY.md} +0 -0
- /package/docs/{ARCHITECTURE.md → v2-archive/ARCHITECTURE.md} +0 -0
- /package/docs/{CLI-COMMANDS-REFERENCE.md → v2-archive/CLI-COMMANDS-REFERENCE.md} +0 -0
- /package/docs/{COMPRESSION-README.md → v2-archive/COMPRESSION-README.md} +0 -0
- /package/docs/{FRAMEWORK-INTEGRATIONS.md → v2-archive/FRAMEWORK-INTEGRATIONS.md} +0 -0
- /package/docs/{MCP-MANUAL-SETUP.md → v2-archive/MCP-MANUAL-SETUP.md} +0 -0
- /package/docs/{MCP-TROUBLESHOOTING.md → v2-archive/MCP-TROUBLESHOOTING.md} +0 -0
- /package/docs/{PATTERN-LEARNING.md → v2-archive/PATTERN-LEARNING.md} +0 -0
- /package/docs/{PROFILES-GUIDE.md → v2-archive/PROFILES-GUIDE.md} +0 -0
- /package/docs/{RESET-GUIDE.md → v2-archive/RESET-GUIDE.md} +0 -0
- /package/docs/{SEARCH-ENGINE-V2.2.0.md → v2-archive/SEARCH-ENGINE-V2.2.0.md} +0 -0
- /package/docs/{SEARCH-INTEGRATION-GUIDE.md → v2-archive/SEARCH-INTEGRATION-GUIDE.md} +0 -0
- /package/docs/{UI-SERVER.md → v2-archive/UI-SERVER.md} +0 -0
- /package/docs/{UNIVERSAL-INTEGRATION.md → v2-archive/UNIVERSAL-INTEGRATION.md} +0 -0
- /package/docs/{V2.2.0-OPTIONAL-SEARCH.md → v2-archive/V2.2.0-OPTIONAL-SEARCH.md} +0 -0
- /package/docs/{WINDOWS-INSTALL-README.txt → v2-archive/WINDOWS-INSTALL-README.txt} +0 -0
- /package/docs/{WINDOWS-POST-INSTALL.txt → v2-archive/WINDOWS-POST-INSTALL.txt} +0 -0
- /package/docs/{example_graph_usage.py → v2-archive/example_graph_usage.py} +0 -0
- /package/{completions → ide/completions}/slm.bash +0 -0
- /package/{completions → ide/completions}/slm.zsh +0 -0
- /package/{configs → ide/configs}/cody-commands.json +0 -0
- /package/{install-skills.sh → scripts/install-skills.sh} +0 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under the MIT License - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Interactive setup wizard for first-time configuration.
|
|
6
|
+
|
|
7
|
+
Guides new users through mode selection and provider setup.
|
|
8
|
+
|
|
9
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import shutil
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run_wizard() -> None:
|
|
19
|
+
"""Run the interactive setup wizard."""
|
|
20
|
+
print()
|
|
21
|
+
print("SuperLocalMemory V3 — First Time Setup")
|
|
22
|
+
print("=" * 40)
|
|
23
|
+
print()
|
|
24
|
+
print("Choose your operating mode:")
|
|
25
|
+
print()
|
|
26
|
+
print(" [A] Local Guardian (default)")
|
|
27
|
+
print(" Zero cloud. Zero LLM. Your data never leaves your machine.")
|
|
28
|
+
print(" EU AI Act compliant. Works immediately.")
|
|
29
|
+
print()
|
|
30
|
+
print(" [B] Smart Local")
|
|
31
|
+
print(" Local LLM via Ollama for answer synthesis.")
|
|
32
|
+
print(" Still private — nothing leaves your machine.")
|
|
33
|
+
print()
|
|
34
|
+
print(" [C] Full Power")
|
|
35
|
+
print(" Cloud LLM for best accuracy (~78% on LoCoMo).")
|
|
36
|
+
print(" Requires: API key from a supported provider.")
|
|
37
|
+
print()
|
|
38
|
+
|
|
39
|
+
choice = input("Select mode [A/B/C] (default: A): ").strip().lower() or "a"
|
|
40
|
+
|
|
41
|
+
if choice not in ("a", "b", "c"):
|
|
42
|
+
print(f"Invalid choice: {choice}. Using Mode A.")
|
|
43
|
+
choice = "a"
|
|
44
|
+
|
|
45
|
+
from superlocalmemory.core.config import SLMConfig
|
|
46
|
+
from superlocalmemory.storage.models import Mode
|
|
47
|
+
|
|
48
|
+
if choice == "a":
|
|
49
|
+
config = SLMConfig.for_mode(Mode.A)
|
|
50
|
+
config.save()
|
|
51
|
+
print()
|
|
52
|
+
print("Mode A configured. Zero cloud, zero LLM.")
|
|
53
|
+
print(f"Config saved to: {config.base_dir / 'config.json'}")
|
|
54
|
+
|
|
55
|
+
elif choice == "b":
|
|
56
|
+
config = SLMConfig.for_mode(Mode.B)
|
|
57
|
+
print()
|
|
58
|
+
print("Checking for Ollama...")
|
|
59
|
+
if shutil.which("ollama"):
|
|
60
|
+
print(" Ollama found!")
|
|
61
|
+
else:
|
|
62
|
+
print(" Ollama not found. Install it from https://ollama.ai")
|
|
63
|
+
print(" After installing, run: ollama pull llama3.2")
|
|
64
|
+
config.save()
|
|
65
|
+
print(f"Config saved to: {config.base_dir / 'config.json'}")
|
|
66
|
+
|
|
67
|
+
elif choice == "c":
|
|
68
|
+
config = SLMConfig.for_mode(Mode.C)
|
|
69
|
+
configure_provider(config)
|
|
70
|
+
|
|
71
|
+
print()
|
|
72
|
+
print("Ready! Your AI now remembers you.")
|
|
73
|
+
print()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def configure_provider(config: object) -> None:
|
|
77
|
+
"""Configure LLM provider for Mode C.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
config: An SLMConfig instance (typed as object to avoid circular import
|
|
81
|
+
at module level; actual type checked at runtime).
|
|
82
|
+
"""
|
|
83
|
+
from superlocalmemory.core.config import SLMConfig
|
|
84
|
+
from superlocalmemory.storage.models import Mode
|
|
85
|
+
|
|
86
|
+
presets = SLMConfig.provider_presets()
|
|
87
|
+
|
|
88
|
+
print()
|
|
89
|
+
print("Choose your LLM provider:")
|
|
90
|
+
print()
|
|
91
|
+
providers = list(presets.keys())
|
|
92
|
+
for i, name in enumerate(providers, 1):
|
|
93
|
+
preset = presets[name]
|
|
94
|
+
print(f" [{i}] {name.capitalize()} — {preset['model']}")
|
|
95
|
+
print()
|
|
96
|
+
|
|
97
|
+
idx = input(f"Select provider [1-{len(providers)}]: ").strip()
|
|
98
|
+
try:
|
|
99
|
+
provider_name = providers[int(idx) - 1]
|
|
100
|
+
except (ValueError, IndexError):
|
|
101
|
+
print("Invalid choice. Using OpenAI.")
|
|
102
|
+
provider_name = "openai"
|
|
103
|
+
|
|
104
|
+
preset = presets[provider_name]
|
|
105
|
+
|
|
106
|
+
# Resolve API key from environment or prompt
|
|
107
|
+
env_key = preset.get("env_key", "")
|
|
108
|
+
api_key = ""
|
|
109
|
+
if env_key:
|
|
110
|
+
existing = os.environ.get(env_key, "")
|
|
111
|
+
if existing:
|
|
112
|
+
print(f" Found {env_key} in environment.")
|
|
113
|
+
api_key = existing
|
|
114
|
+
else:
|
|
115
|
+
api_key = input(
|
|
116
|
+
f" Enter your {provider_name.capitalize()} API key: ",
|
|
117
|
+
).strip()
|
|
118
|
+
|
|
119
|
+
updated = SLMConfig.for_mode(
|
|
120
|
+
Mode.C,
|
|
121
|
+
llm_provider=provider_name,
|
|
122
|
+
llm_model=preset["model"],
|
|
123
|
+
llm_api_key=api_key,
|
|
124
|
+
llm_api_base=preset["base_url"],
|
|
125
|
+
)
|
|
126
|
+
updated.save()
|
|
127
|
+
print(f" Provider: {provider_name}")
|
|
128
|
+
print(f" Model: {preset['model']}")
|
|
129
|
+
print(f"Config saved to: {updated.base_dir / 'config.json'}")
|
|
File without changes
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under the MIT License - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Attribute-Based Access Control for memory operations.
|
|
6
|
+
|
|
7
|
+
Evaluates policies: (agent_id, profile_id, action) -> allow/deny.
|
|
8
|
+
Default: allow all (open access). Policies restrict specific agents.
|
|
9
|
+
|
|
10
|
+
Actions: "read", "write", "delete", "admin".
|
|
11
|
+
Policies are stored in-memory (loaded from DB on init).
|
|
12
|
+
Simple deny-list approach: if a deny policy matches, access is blocked.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import sqlite3
|
|
19
|
+
from typing import Any, Optional
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Valid ABAC actions
|
|
24
|
+
VALID_ACTIONS = frozenset({"read", "write", "delete", "admin"})
|
|
25
|
+
|
|
26
|
+
_POLICY_TABLE_SQL = """
|
|
27
|
+
CREATE TABLE IF NOT EXISTS abac_policies (
|
|
28
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
29
|
+
profile_id TEXT NOT NULL,
|
|
30
|
+
agent_id TEXT NOT NULL,
|
|
31
|
+
action TEXT NOT NULL,
|
|
32
|
+
deny INTEGER NOT NULL DEFAULT 1,
|
|
33
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
34
|
+
)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AccessDenied(PermissionError):
|
|
39
|
+
"""Raised when ABAC denies an operation."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ABACEngine:
|
|
43
|
+
"""Attribute-Based Access Control for memory operations.
|
|
44
|
+
|
|
45
|
+
Evaluates policies: (agent_id, profile_id, action) -> allow/deny.
|
|
46
|
+
Default: allow all (open access). Deny policies restrict specific
|
|
47
|
+
agent+profile+action combinations.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, db: Optional[sqlite3.Connection] = None) -> None:
|
|
51
|
+
self._policies: list[dict[str, Any]] = []
|
|
52
|
+
self._db = db
|
|
53
|
+
if db is not None:
|
|
54
|
+
self._load_policies_from_db(db)
|
|
55
|
+
|
|
56
|
+
# ------------------------------------------------------------------
|
|
57
|
+
# Policy loading
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
def _load_policies_from_db(self, db: sqlite3.Connection) -> None:
|
|
61
|
+
"""Load policies from the abac_policies table."""
|
|
62
|
+
try:
|
|
63
|
+
db.execute(_POLICY_TABLE_SQL)
|
|
64
|
+
db.commit()
|
|
65
|
+
rows = db.execute(
|
|
66
|
+
"SELECT profile_id, agent_id, action, deny "
|
|
67
|
+
"FROM abac_policies"
|
|
68
|
+
).fetchall()
|
|
69
|
+
for row in rows:
|
|
70
|
+
self._policies.append({
|
|
71
|
+
"profile_id": row[0],
|
|
72
|
+
"agent_id": row[1],
|
|
73
|
+
"action": row[2],
|
|
74
|
+
"deny": bool(row[3]),
|
|
75
|
+
})
|
|
76
|
+
logger.info("Loaded %d ABAC policies from DB", len(self._policies))
|
|
77
|
+
except sqlite3.OperationalError as exc:
|
|
78
|
+
logger.warning("Failed to load ABAC policies: %s", exc)
|
|
79
|
+
|
|
80
|
+
# ------------------------------------------------------------------
|
|
81
|
+
# Policy management
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
def add_policy(
|
|
85
|
+
self,
|
|
86
|
+
profile_id: str,
|
|
87
|
+
agent_id: str,
|
|
88
|
+
action: str,
|
|
89
|
+
deny: bool = True,
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Add an access control policy.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
profile_id: Profile this policy applies to.
|
|
95
|
+
agent_id: Agent this policy applies to.
|
|
96
|
+
action: The action to control (read/write/delete/admin).
|
|
97
|
+
deny: If True, this is a deny policy. Default True.
|
|
98
|
+
"""
|
|
99
|
+
if action not in VALID_ACTIONS:
|
|
100
|
+
raise ValueError(f"Invalid action '{action}'. Must be one of {VALID_ACTIONS}")
|
|
101
|
+
policy = {
|
|
102
|
+
"profile_id": profile_id,
|
|
103
|
+
"agent_id": agent_id,
|
|
104
|
+
"action": action,
|
|
105
|
+
"deny": deny,
|
|
106
|
+
}
|
|
107
|
+
self._policies.append(policy)
|
|
108
|
+
self._persist_policy(policy)
|
|
109
|
+
|
|
110
|
+
def remove_policy(
|
|
111
|
+
self,
|
|
112
|
+
profile_id: str,
|
|
113
|
+
agent_id: str,
|
|
114
|
+
action: str,
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Remove a policy matching profile_id + agent_id + action."""
|
|
117
|
+
self._policies = [
|
|
118
|
+
p for p in self._policies
|
|
119
|
+
if not (
|
|
120
|
+
p["profile_id"] == profile_id
|
|
121
|
+
and p["agent_id"] == agent_id
|
|
122
|
+
and p["action"] == action
|
|
123
|
+
)
|
|
124
|
+
]
|
|
125
|
+
if self._db is not None:
|
|
126
|
+
try:
|
|
127
|
+
self._db.execute(
|
|
128
|
+
"DELETE FROM abac_policies WHERE profile_id = ? AND agent_id = ? AND action = ?",
|
|
129
|
+
(profile_id, agent_id, action),
|
|
130
|
+
)
|
|
131
|
+
self._db.commit()
|
|
132
|
+
except sqlite3.OperationalError:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
def _persist_policy(self, policy: dict[str, Any]) -> None:
|
|
136
|
+
"""Write a policy to DB so it survives restart."""
|
|
137
|
+
if self._db is None:
|
|
138
|
+
return
|
|
139
|
+
try:
|
|
140
|
+
self._db.execute(
|
|
141
|
+
"INSERT INTO abac_policies (profile_id, agent_id, action, deny) "
|
|
142
|
+
"VALUES (?, ?, ?, ?)",
|
|
143
|
+
(policy["profile_id"], policy["agent_id"], policy["action"], int(policy["deny"])),
|
|
144
|
+
)
|
|
145
|
+
self._db.commit()
|
|
146
|
+
except sqlite3.OperationalError as exc:
|
|
147
|
+
logger.warning("Failed to persist ABAC policy: %s", exc)
|
|
148
|
+
|
|
149
|
+
# ------------------------------------------------------------------
|
|
150
|
+
# Access evaluation
|
|
151
|
+
# ------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
def check(self, agent_id: str, profile_id: str, action: str) -> bool:
|
|
154
|
+
"""Evaluate access. Returns True if allowed, False if denied.
|
|
155
|
+
|
|
156
|
+
Default: allow if no deny policy matches the request.
|
|
157
|
+
Deny-first semantics: any matching deny policy blocks access.
|
|
158
|
+
"""
|
|
159
|
+
for policy in self._policies:
|
|
160
|
+
if not policy.get("deny", True):
|
|
161
|
+
continue
|
|
162
|
+
if (
|
|
163
|
+
policy["agent_id"] == agent_id
|
|
164
|
+
and policy["profile_id"] == profile_id
|
|
165
|
+
and policy["action"] == action
|
|
166
|
+
):
|
|
167
|
+
return False
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
def check_or_raise(
|
|
171
|
+
self,
|
|
172
|
+
agent_id: str,
|
|
173
|
+
profile_id: str,
|
|
174
|
+
action: str,
|
|
175
|
+
) -> None:
|
|
176
|
+
"""Like check() but raises AccessDenied if denied."""
|
|
177
|
+
if not self.check(agent_id, profile_id, action):
|
|
178
|
+
raise AccessDenied(
|
|
179
|
+
f"Agent '{agent_id}' denied '{action}' "
|
|
180
|
+
f"on profile '{profile_id}'"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# ------------------------------------------------------------------
|
|
184
|
+
# Policy listing
|
|
185
|
+
# ------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
def list_policies(
|
|
188
|
+
self,
|
|
189
|
+
profile_id: Optional[str] = None,
|
|
190
|
+
) -> list[dict[str, Any]]:
|
|
191
|
+
"""List all policies, optionally filtered by profile.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
profile_id: If provided, only return policies for this profile.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
List of policy dicts with keys: profile_id, agent_id, action, deny.
|
|
198
|
+
"""
|
|
199
|
+
if profile_id is None:
|
|
200
|
+
return [dict(p) for p in self._policies]
|
|
201
|
+
return [
|
|
202
|
+
dict(p) for p in self._policies
|
|
203
|
+
if p["profile_id"] == profile_id
|
|
204
|
+
]
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under the MIT License - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""Tamper-proof hash-chain audit log for compliance.
|
|
6
|
+
|
|
7
|
+
Every operation is logged with a SHA-256 hash that includes the previous
|
|
8
|
+
entry's hash, creating a chain. Tampering with any entry breaks the chain
|
|
9
|
+
and is detectable via verify_integrity().
|
|
10
|
+
|
|
11
|
+
The audit chain uses its OWN sqlite3 connection (not shared DB manager)
|
|
12
|
+
for independence — audit must survive even if the main DB is corrupted.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import sqlite3
|
|
21
|
+
import threading
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Optional, Union
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
_GENESIS_HASH = "genesis"
|
|
29
|
+
|
|
30
|
+
_SCHEMA = """
|
|
31
|
+
CREATE TABLE IF NOT EXISTS audit_chain (
|
|
32
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
33
|
+
timestamp TEXT NOT NULL,
|
|
34
|
+
operation TEXT NOT NULL,
|
|
35
|
+
agent_id TEXT DEFAULT '',
|
|
36
|
+
profile_id TEXT DEFAULT '',
|
|
37
|
+
content_hash TEXT DEFAULT '',
|
|
38
|
+
prev_hash TEXT DEFAULT '',
|
|
39
|
+
event_hash TEXT NOT NULL,
|
|
40
|
+
metadata TEXT DEFAULT '{}'
|
|
41
|
+
);
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _compute_hash(
|
|
46
|
+
prev_hash: str,
|
|
47
|
+
operation: str,
|
|
48
|
+
agent_id: str,
|
|
49
|
+
profile_id: str,
|
|
50
|
+
content_hash: str,
|
|
51
|
+
timestamp: str,
|
|
52
|
+
) -> str:
|
|
53
|
+
"""Compute SHA-256 hash for an audit entry.
|
|
54
|
+
|
|
55
|
+
The hash incorporates: prev_hash + operation + agent_id +
|
|
56
|
+
profile_id + content_hash + timestamp.
|
|
57
|
+
"""
|
|
58
|
+
payload = (
|
|
59
|
+
f"{prev_hash}{operation}{agent_id}"
|
|
60
|
+
f"{profile_id}{content_hash}{timestamp}"
|
|
61
|
+
)
|
|
62
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class AuditChain:
|
|
66
|
+
"""Tamper-proof hash-chain audit log for compliance.
|
|
67
|
+
|
|
68
|
+
Every operation is logged with a hash that includes the previous hash,
|
|
69
|
+
creating a chain. Tampering with any entry breaks the chain.
|
|
70
|
+
|
|
71
|
+
Uses its own SQLite connection for independence from main DB.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, db_path: Optional[Union[str, Path]] = None) -> None:
|
|
75
|
+
self._db_path = str(db_path) if db_path else ":memory:"
|
|
76
|
+
self._is_memory = self._db_path == ":memory:"
|
|
77
|
+
self._lock = threading.Lock()
|
|
78
|
+
# For in-memory DBs, keep a persistent connection (each connect()
|
|
79
|
+
# to ":memory:" creates a separate empty database).
|
|
80
|
+
self._persistent_conn: Optional[sqlite3.Connection] = None
|
|
81
|
+
if self._is_memory:
|
|
82
|
+
self._persistent_conn = self._make_conn()
|
|
83
|
+
self._init_db()
|
|
84
|
+
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
# Internal helpers
|
|
87
|
+
# ------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def _make_conn_from_path(path: str) -> sqlite3.Connection:
|
|
91
|
+
"""Create a configured SQLite connection."""
|
|
92
|
+
conn = sqlite3.connect(path)
|
|
93
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
94
|
+
conn.row_factory = sqlite3.Row
|
|
95
|
+
return conn
|
|
96
|
+
|
|
97
|
+
def _make_conn(self) -> sqlite3.Connection:
|
|
98
|
+
"""Create a new configured connection to the audit database."""
|
|
99
|
+
return self._make_conn_from_path(self._db_path)
|
|
100
|
+
|
|
101
|
+
def _get_conn(self) -> sqlite3.Connection:
|
|
102
|
+
"""Get a connection to the audit database.
|
|
103
|
+
|
|
104
|
+
For in-memory databases, returns the persistent connection.
|
|
105
|
+
For file databases, creates a new connection each time.
|
|
106
|
+
"""
|
|
107
|
+
if self._is_memory and self._persistent_conn is not None:
|
|
108
|
+
return self._persistent_conn
|
|
109
|
+
return self._make_conn()
|
|
110
|
+
|
|
111
|
+
def _release_conn(self, conn: sqlite3.Connection) -> None:
|
|
112
|
+
"""Release a connection. Only closes file-based connections."""
|
|
113
|
+
if not self._is_memory:
|
|
114
|
+
conn.close()
|
|
115
|
+
|
|
116
|
+
def _init_db(self) -> None:
|
|
117
|
+
"""Initialize the audit_chain table."""
|
|
118
|
+
conn = self._get_conn()
|
|
119
|
+
try:
|
|
120
|
+
conn.executescript(_SCHEMA)
|
|
121
|
+
conn.commit()
|
|
122
|
+
finally:
|
|
123
|
+
self._release_conn(conn)
|
|
124
|
+
|
|
125
|
+
def _get_last_hash(self, conn: sqlite3.Connection) -> str:
|
|
126
|
+
"""Get the hash of the most recent entry, or genesis."""
|
|
127
|
+
row = conn.execute(
|
|
128
|
+
"SELECT event_hash FROM audit_chain "
|
|
129
|
+
"ORDER BY id DESC LIMIT 1"
|
|
130
|
+
).fetchone()
|
|
131
|
+
return row["event_hash"] if row else _GENESIS_HASH
|
|
132
|
+
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
# Public API
|
|
135
|
+
# ------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def log(
|
|
138
|
+
self,
|
|
139
|
+
operation: str,
|
|
140
|
+
agent_id: str = "",
|
|
141
|
+
profile_id: str = "",
|
|
142
|
+
content_hash: str = "",
|
|
143
|
+
metadata: Optional[dict[str, Any]] = None,
|
|
144
|
+
) -> str:
|
|
145
|
+
"""Log an audit event. Returns the event hash.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
operation: Type of operation (store, recall, delete, etc.).
|
|
149
|
+
agent_id: ID of the agent performing the operation.
|
|
150
|
+
profile_id: ID of the profile being accessed.
|
|
151
|
+
content_hash: Hash of the content involved (optional).
|
|
152
|
+
metadata: Additional metadata dict (optional).
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
The SHA-256 event hash for this entry.
|
|
156
|
+
"""
|
|
157
|
+
ts = datetime.now(timezone.utc).isoformat()
|
|
158
|
+
metadata_str = json.dumps(metadata or {}, sort_keys=True)
|
|
159
|
+
|
|
160
|
+
with self._lock:
|
|
161
|
+
conn = self._get_conn()
|
|
162
|
+
try:
|
|
163
|
+
prev_hash = self._get_last_hash(conn)
|
|
164
|
+
event_hash = _compute_hash(
|
|
165
|
+
prev_hash, operation, agent_id,
|
|
166
|
+
profile_id, content_hash, ts,
|
|
167
|
+
)
|
|
168
|
+
conn.execute(
|
|
169
|
+
"INSERT INTO audit_chain "
|
|
170
|
+
"(timestamp, operation, agent_id, profile_id, "
|
|
171
|
+
" content_hash, prev_hash, event_hash, metadata) "
|
|
172
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
173
|
+
(
|
|
174
|
+
ts, operation, agent_id, profile_id,
|
|
175
|
+
content_hash, prev_hash, event_hash, metadata_str,
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
conn.commit()
|
|
179
|
+
return event_hash
|
|
180
|
+
finally:
|
|
181
|
+
self._release_conn(conn)
|
|
182
|
+
|
|
183
|
+
def query(
|
|
184
|
+
self,
|
|
185
|
+
profile_id: Optional[str] = None,
|
|
186
|
+
agent_id: Optional[str] = None,
|
|
187
|
+
operation: Optional[str] = None,
|
|
188
|
+
start_date: Optional[str] = None,
|
|
189
|
+
end_date: Optional[str] = None,
|
|
190
|
+
limit: int = 100,
|
|
191
|
+
) -> list[dict[str, Any]]:
|
|
192
|
+
"""Query audit events with filters.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
profile_id: Filter by profile ID.
|
|
196
|
+
agent_id: Filter by agent ID.
|
|
197
|
+
operation: Filter by operation type.
|
|
198
|
+
start_date: ISO date string for range start.
|
|
199
|
+
end_date: ISO date string for range end.
|
|
200
|
+
limit: Maximum number of events to return.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
List of event dicts ordered by id descending.
|
|
204
|
+
"""
|
|
205
|
+
clauses: list[str] = []
|
|
206
|
+
params: list[Any] = []
|
|
207
|
+
|
|
208
|
+
if profile_id is not None:
|
|
209
|
+
clauses.append("profile_id = ?")
|
|
210
|
+
params.append(profile_id)
|
|
211
|
+
if agent_id is not None:
|
|
212
|
+
clauses.append("agent_id = ?")
|
|
213
|
+
params.append(agent_id)
|
|
214
|
+
if operation is not None:
|
|
215
|
+
clauses.append("operation = ?")
|
|
216
|
+
params.append(operation)
|
|
217
|
+
if start_date is not None:
|
|
218
|
+
clauses.append("timestamp >= ?")
|
|
219
|
+
params.append(start_date)
|
|
220
|
+
if end_date is not None:
|
|
221
|
+
clauses.append("timestamp <= ?")
|
|
222
|
+
params.append(end_date)
|
|
223
|
+
|
|
224
|
+
where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
|
|
225
|
+
sql = (
|
|
226
|
+
f"SELECT id, timestamp, operation, agent_id, profile_id, "
|
|
227
|
+
f"content_hash, prev_hash, event_hash, metadata "
|
|
228
|
+
f"FROM audit_chain {where} "
|
|
229
|
+
f"ORDER BY id DESC LIMIT ?"
|
|
230
|
+
)
|
|
231
|
+
params.append(limit)
|
|
232
|
+
|
|
233
|
+
with self._lock:
|
|
234
|
+
conn = self._get_conn()
|
|
235
|
+
try:
|
|
236
|
+
rows = conn.execute(sql, params).fetchall()
|
|
237
|
+
return [dict(r) for r in rows]
|
|
238
|
+
finally:
|
|
239
|
+
self._release_conn(conn)
|
|
240
|
+
|
|
241
|
+
def verify_integrity(self) -> bool:
|
|
242
|
+
"""Verify the entire hash chain. Returns True if chain is intact.
|
|
243
|
+
|
|
244
|
+
Walks every entry in order, recomputes each hash, and verifies
|
|
245
|
+
it matches the stored hash. Any mismatch means tampering.
|
|
246
|
+
"""
|
|
247
|
+
conn = self._get_conn()
|
|
248
|
+
try:
|
|
249
|
+
rows = conn.execute(
|
|
250
|
+
"SELECT id, timestamp, operation, agent_id, profile_id, "
|
|
251
|
+
"content_hash, prev_hash, event_hash "
|
|
252
|
+
"FROM audit_chain ORDER BY id"
|
|
253
|
+
).fetchall()
|
|
254
|
+
finally:
|
|
255
|
+
self._release_conn(conn)
|
|
256
|
+
|
|
257
|
+
if not rows:
|
|
258
|
+
return True
|
|
259
|
+
|
|
260
|
+
expected_prev = _GENESIS_HASH
|
|
261
|
+
for row in rows:
|
|
262
|
+
row_dict = dict(row)
|
|
263
|
+
|
|
264
|
+
# Check the prev_hash link
|
|
265
|
+
if row_dict["prev_hash"] != expected_prev:
|
|
266
|
+
logger.warning(
|
|
267
|
+
"Audit chain prev_hash mismatch at entry %d",
|
|
268
|
+
row_dict["id"],
|
|
269
|
+
)
|
|
270
|
+
return False
|
|
271
|
+
|
|
272
|
+
# Recompute the entry hash
|
|
273
|
+
computed = _compute_hash(
|
|
274
|
+
row_dict["prev_hash"],
|
|
275
|
+
row_dict["operation"],
|
|
276
|
+
row_dict["agent_id"],
|
|
277
|
+
row_dict["profile_id"],
|
|
278
|
+
row_dict["content_hash"],
|
|
279
|
+
row_dict["timestamp"],
|
|
280
|
+
)
|
|
281
|
+
if computed != row_dict["event_hash"]:
|
|
282
|
+
logger.warning(
|
|
283
|
+
"Audit chain event_hash mismatch at entry %d",
|
|
284
|
+
row_dict["id"],
|
|
285
|
+
)
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
expected_prev = row_dict["event_hash"]
|
|
289
|
+
|
|
290
|
+
return True
|
|
291
|
+
|
|
292
|
+
def get_stats(self) -> dict[str, int]:
|
|
293
|
+
"""Get audit statistics (event counts by operation type).
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
Dict mapping operation names to their counts, plus
|
|
297
|
+
a 'total' key with the chain length.
|
|
298
|
+
"""
|
|
299
|
+
conn = self._get_conn()
|
|
300
|
+
try:
|
|
301
|
+
rows = conn.execute(
|
|
302
|
+
"SELECT operation, COUNT(*) AS cnt "
|
|
303
|
+
"FROM audit_chain GROUP BY operation"
|
|
304
|
+
).fetchall()
|
|
305
|
+
stats: dict[str, int] = {}
|
|
306
|
+
total = 0
|
|
307
|
+
for row in rows:
|
|
308
|
+
count = row["cnt"]
|
|
309
|
+
stats[row["operation"]] = count
|
|
310
|
+
total += count
|
|
311
|
+
stats["total"] = total
|
|
312
|
+
return stats
|
|
313
|
+
finally:
|
|
314
|
+
self._release_conn(conn)
|