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
|
@@ -30,6 +30,14 @@ Design notes (LLD-04 §7 hard rules):
|
|
|
30
30
|
never spell those names anywhere in this module.
|
|
31
31
|
* **U6** — install token required on ``/api/v3/brain`` and all
|
|
32
32
|
deprecated shim routes. See ``require_install_token`` below.
|
|
33
|
+
|
|
34
|
+
External API consumers (L1): the dashboard auto-fetches the token from
|
|
35
|
+
``GET /internal/token`` (loopback + same-origin only) and sends it as the
|
|
36
|
+
``X-Install-Token`` header. Non-browser callers (scripts, IDE adapters,
|
|
37
|
+
third-party integrations) instead read the token from
|
|
38
|
+
``~/.superlocalmemory/.install_token`` and send the same header:
|
|
39
|
+
``curl -H "X-Install-Token: $(cat ~/.superlocalmemory/.install_token)" \
|
|
40
|
+
http://127.0.0.1:8765/api/v3/brain``.
|
|
33
41
|
* **U10** — feature count surfaced from ``features.FEATURE_DIM``;
|
|
34
42
|
stratum total from module constant ``_STRATA_TOTAL`` (48 = 4×3×4).
|
|
35
43
|
"""
|
|
@@ -52,6 +60,7 @@ from superlocalmemory.core.security_primitives import (
|
|
|
52
60
|
from superlocalmemory.learning.database import LearningDatabase
|
|
53
61
|
from superlocalmemory.learning.features import FEATURE_DIM
|
|
54
62
|
from superlocalmemory.infra.data_root import canonical_data_root
|
|
63
|
+
from .helpers import get_active_profile
|
|
55
64
|
|
|
56
65
|
logger = logging.getLogger("superlocalmemory.routes.brain")
|
|
57
66
|
|
|
@@ -894,13 +903,47 @@ def _action_outcomes_count(lrn_db: LearningDatabase,
|
|
|
894
903
|
return 0
|
|
895
904
|
|
|
896
905
|
|
|
906
|
+
def _compute_action_outcomes_preview(profile_id: str) -> dict:
|
|
907
|
+
"""Count profile-scoped action outcomes from their canonical database."""
|
|
908
|
+
empty = {
|
|
909
|
+
"action_outcomes_rows": 0,
|
|
910
|
+
"source": "memory.db:action_outcomes",
|
|
911
|
+
"is_real": True,
|
|
912
|
+
}
|
|
913
|
+
db_path = _memory_db_path()
|
|
914
|
+
if not db_path.exists():
|
|
915
|
+
return empty
|
|
916
|
+
try:
|
|
917
|
+
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=1.0)
|
|
918
|
+
try:
|
|
919
|
+
row = conn.execute(
|
|
920
|
+
"SELECT COUNT(*) FROM action_outcomes WHERE profile_id = ?",
|
|
921
|
+
(profile_id,),
|
|
922
|
+
).fetchone()
|
|
923
|
+
finally:
|
|
924
|
+
conn.close()
|
|
925
|
+
except sqlite3.Error:
|
|
926
|
+
return empty
|
|
927
|
+
return {**empty, "action_outcomes_rows": int(row[0] or 0) if row else 0}
|
|
928
|
+
|
|
929
|
+
|
|
897
930
|
# ---------------------------------------------------------------------------
|
|
898
931
|
# Routes
|
|
899
932
|
# ---------------------------------------------------------------------------
|
|
900
933
|
|
|
901
934
|
|
|
935
|
+
def _authorized_profile(request: Request, profile_id: str | None) -> str:
|
|
936
|
+
"""Resolve and authorize the exact Brain profile requested by the caller."""
|
|
937
|
+
from superlocalmemory.access.rbac import Permission
|
|
938
|
+
from superlocalmemory.server.rbac_enforce import require_permission
|
|
939
|
+
|
|
940
|
+
effective_profile = profile_id or get_active_profile()
|
|
941
|
+
require_permission(request, Permission.READ, profile=effective_profile)
|
|
942
|
+
return effective_profile
|
|
943
|
+
|
|
944
|
+
|
|
902
945
|
@router.get("/brain", dependencies=[Depends(require_install_token)])
|
|
903
|
-
async def get_brain(profile_id: str =
|
|
946
|
+
async def get_brain(request: Request, profile_id: str | None = None) -> dict:
|
|
904
947
|
"""Unified Brain endpoint — LLD-04 §3.1.
|
|
905
948
|
|
|
906
949
|
Fan-out: each section is a synchronous SQLite reader. Running them
|
|
@@ -915,11 +958,14 @@ async def get_brain(profile_id: str = "default") -> dict:
|
|
|
915
958
|
"""
|
|
916
959
|
import asyncio
|
|
917
960
|
|
|
961
|
+
# Default to the ACTIVE profile (request runtime truth), never literal
|
|
962
|
+
# "default" — the Brain must reflect whichever profile is active.
|
|
963
|
+
profile_id = _authorized_profile(request, profile_id)
|
|
918
964
|
lrn_db = LearningDatabase(_learning_db_path())
|
|
919
965
|
|
|
920
966
|
(
|
|
921
967
|
preferences, learning, usage, bandit_snap, cache,
|
|
922
|
-
cross_platform,
|
|
968
|
+
cross_platform, outcomes_preview, evolution,
|
|
923
969
|
) = await asyncio.gather(
|
|
924
970
|
asyncio.to_thread(_compute_preferences, profile_id),
|
|
925
971
|
asyncio.to_thread(_compute_learning_status, profile_id, lrn_db),
|
|
@@ -927,7 +973,7 @@ async def get_brain(profile_id: str = "default") -> dict:
|
|
|
927
973
|
asyncio.to_thread(_compute_bandit_snapshot, profile_id, lrn_db),
|
|
928
974
|
asyncio.to_thread(_compute_cache_stats),
|
|
929
975
|
asyncio.to_thread(_compute_cross_platform),
|
|
930
|
-
asyncio.to_thread(
|
|
976
|
+
asyncio.to_thread(_compute_action_outcomes_preview, profile_id),
|
|
931
977
|
asyncio.to_thread(
|
|
932
978
|
_compute_evolution_timeseries, profile_id, lrn_db,
|
|
933
979
|
days=_EVOLUTION_DEFAULT_DAYS,
|
|
@@ -965,11 +1011,11 @@ async def get_brain(profile_id: str = "default") -> dict:
|
|
|
965
1011
|
"is_real": True, "source": "learning_signals",
|
|
966
1012
|
"days": _EVOLUTION_DEFAULT_DAYS, "total_signals": 0, "points": [],
|
|
967
1013
|
}),
|
|
968
|
-
"outcomes_preview": {
|
|
969
|
-
"action_outcomes_rows":
|
|
970
|
-
|
|
971
|
-
"
|
|
972
|
-
},
|
|
1014
|
+
"outcomes_preview": _ok(outcomes_preview, {
|
|
1015
|
+
"action_outcomes_rows": 0,
|
|
1016
|
+
"source": "memory.db:action_outcomes",
|
|
1017
|
+
"is_real": True,
|
|
1018
|
+
}),
|
|
973
1019
|
# S9-defer H-22: live tile data for the Reward / Shadow /
|
|
974
1020
|
# Evolution-Cost dashboard tiles. Each block is a honest-empty
|
|
975
1021
|
# default when the underlying table is missing (fresh install
|
|
@@ -1176,7 +1222,8 @@ def _compute_evolution_cost_preview(profile_id: str) -> dict:
|
|
|
1176
1222
|
@router.get("/brain/evolution-timeseries",
|
|
1177
1223
|
dependencies=[Depends(require_install_token)])
|
|
1178
1224
|
async def get_brain_evolution_timeseries(
|
|
1179
|
-
|
|
1225
|
+
request: Request,
|
|
1226
|
+
profile_id: str | None = None,
|
|
1180
1227
|
days: int = _EVOLUTION_DEFAULT_DAYS,
|
|
1181
1228
|
) -> dict:
|
|
1182
1229
|
"""Daily learning-signal counts for ``profile_id`` over the last ``days``.
|
|
@@ -1186,6 +1233,7 @@ async def get_brain_evolution_timeseries(
|
|
|
1186
1233
|
"""
|
|
1187
1234
|
import asyncio
|
|
1188
1235
|
|
|
1236
|
+
profile_id = _authorized_profile(request, profile_id)
|
|
1189
1237
|
lrn_db = LearningDatabase(_learning_db_path())
|
|
1190
1238
|
result = await asyncio.to_thread(
|
|
1191
1239
|
_compute_evolution_timeseries, profile_id, lrn_db, days=days,
|
|
@@ -1201,7 +1249,10 @@ async def get_brain_evolution_timeseries(
|
|
|
1201
1249
|
|
|
1202
1250
|
@router.get("/learning/stats",
|
|
1203
1251
|
dependencies=[Depends(require_install_token)])
|
|
1204
|
-
async def learning_stats_deprecated(
|
|
1252
|
+
async def learning_stats_deprecated(
|
|
1253
|
+
request: Request, profile_id: str | None = None,
|
|
1254
|
+
) -> dict:
|
|
1255
|
+
profile_id = _authorized_profile(request, profile_id)
|
|
1205
1256
|
lrn_db = LearningDatabase(_learning_db_path())
|
|
1206
1257
|
return {
|
|
1207
1258
|
"deprecated": True,
|
|
@@ -1212,7 +1263,10 @@ async def learning_stats_deprecated(profile_id: str = "default") -> dict:
|
|
|
1212
1263
|
|
|
1213
1264
|
@router.get("/patterns",
|
|
1214
1265
|
dependencies=[Depends(require_install_token)])
|
|
1215
|
-
async def patterns_deprecated(
|
|
1266
|
+
async def patterns_deprecated(
|
|
1267
|
+
request: Request, profile_id: str | None = None,
|
|
1268
|
+
) -> dict:
|
|
1269
|
+
profile_id = _authorized_profile(request, profile_id)
|
|
1216
1270
|
return {
|
|
1217
1271
|
"deprecated": True,
|
|
1218
1272
|
"use_instead": "/api/v3/brain",
|
|
@@ -1222,7 +1276,10 @@ async def patterns_deprecated(profile_id: str = "default") -> dict:
|
|
|
1222
1276
|
|
|
1223
1277
|
@router.get("/behavioral",
|
|
1224
1278
|
dependencies=[Depends(require_install_token)])
|
|
1225
|
-
async def behavioral_deprecated(
|
|
1279
|
+
async def behavioral_deprecated(
|
|
1280
|
+
request: Request, profile_id: str | None = None,
|
|
1281
|
+
) -> dict:
|
|
1282
|
+
profile_id = _authorized_profile(request, profile_id)
|
|
1226
1283
|
return {
|
|
1227
1284
|
"deprecated": True,
|
|
1228
1285
|
"use_instead": "/api/v3/brain",
|
|
@@ -101,8 +101,9 @@ async def _stream_chat(
|
|
|
101
101
|
memories = await loop.run_in_executor(
|
|
102
102
|
None, _recall_memories, app_state, query, limit,
|
|
103
103
|
)
|
|
104
|
-
except Exception
|
|
105
|
-
|
|
104
|
+
except Exception:
|
|
105
|
+
logger.exception("chat: memory retrieval failed")
|
|
106
|
+
yield _sse_event("error", json.dumps({"message": "Memory retrieval failed"}))
|
|
106
107
|
yield _sse_event("done", "")
|
|
107
108
|
return
|
|
108
109
|
|
|
@@ -226,8 +227,9 @@ async def _stream_mode_bc(
|
|
|
226
227
|
yield _sse_event("token", token)
|
|
227
228
|
except httpx.ConnectError:
|
|
228
229
|
yield _sse_event("token", f"\n\n[Connection failed — is {provider} running?]")
|
|
229
|
-
except Exception
|
|
230
|
-
|
|
230
|
+
except Exception:
|
|
231
|
+
logger.exception("chat: LLM streaming error")
|
|
232
|
+
yield _sse_event("token", "\n\n[LLM error: request failed]")
|
|
231
233
|
|
|
232
234
|
|
|
233
235
|
# ── Ollama Streaming (/api/chat with messages) ───────────────────
|
|
@@ -359,7 +361,10 @@ def _recall_via_resident_engine(app_state, query: str, limit: int) -> list:
|
|
|
359
361
|
if r.fact.memory_id
|
|
360
362
|
})
|
|
361
363
|
memory_map = (
|
|
362
|
-
engine._db.get_memory_content_batch(
|
|
364
|
+
engine._db.get_memory_content_batch(
|
|
365
|
+
memory_ids, engine.profile_id,
|
|
366
|
+
include_global=True, include_shared=True,
|
|
367
|
+
) if memory_ids else {}
|
|
363
368
|
)
|
|
364
369
|
from superlocalmemory.server.recall_serializer import (
|
|
365
370
|
serialize_recall_response,
|
|
@@ -13,14 +13,20 @@ import logging
|
|
|
13
13
|
import sqlite3
|
|
14
14
|
from typing import Optional
|
|
15
15
|
|
|
16
|
-
from fastapi import APIRouter, Query
|
|
16
|
+
from fastapi import APIRouter, Query, Request
|
|
17
|
+
from fastapi.responses import JSONResponse
|
|
17
18
|
|
|
18
|
-
from .helpers import get_active_profile, MEMORY_DIR, DB_PATH
|
|
19
|
+
from .helpers import get_active_profile, get_engine_lazy, MEMORY_DIR, DB_PATH
|
|
20
|
+
from superlocalmemory.server.route_mutations import authorize_route_mutation
|
|
19
21
|
|
|
20
22
|
logger = logging.getLogger("superlocalmemory.routes.compliance")
|
|
21
23
|
router = APIRouter()
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
# The engine's audit post-hooks write to <data_root>/audit_chain.db
|
|
26
|
+
# (core/engine_wiring.py). The route MUST read that same file — an earlier
|
|
27
|
+
# "audit.db" path pointed at a file nothing ever wrote, so the Compliance tab
|
|
28
|
+
# always showed zero events.
|
|
29
|
+
AUDIT_DB = MEMORY_DIR / "audit_chain.db"
|
|
24
30
|
|
|
25
31
|
# Feature detection
|
|
26
32
|
COMPLIANCE_AVAILABLE = False
|
|
@@ -28,6 +34,7 @@ try:
|
|
|
28
34
|
from superlocalmemory.compliance.audit import AuditChain
|
|
29
35
|
from superlocalmemory.compliance.retention import RetentionEngine
|
|
30
36
|
from superlocalmemory.compliance.abac import ABACEngine
|
|
37
|
+
from superlocalmemory.compliance.gdpr import GDPRCompliance
|
|
31
38
|
COMPLIANCE_AVAILABLE = True
|
|
32
39
|
except ImportError:
|
|
33
40
|
logger.info("V3 compliance engine not available")
|
|
@@ -42,22 +49,24 @@ async def compliance_status():
|
|
|
42
49
|
try:
|
|
43
50
|
profile = get_active_profile()
|
|
44
51
|
|
|
45
|
-
# Audit events
|
|
52
|
+
# Audit events (hash-chained, from the engine's live audit_chain.db).
|
|
53
|
+
# Scope the count + recent list to the active profile.
|
|
46
54
|
audit_events_count = 0
|
|
47
55
|
recent_audit_events = []
|
|
48
56
|
try:
|
|
49
57
|
audit = AuditChain(str(AUDIT_DB))
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
recent_audit_events = audit.query(profile_id=profile, limit=30)
|
|
59
|
+
# get_stats() is global; count this profile's rows for the header.
|
|
60
|
+
audit_events_count = len(audit.query(profile_id=profile, limit=100000))
|
|
52
61
|
except Exception as exc:
|
|
53
62
|
logger.debug("audit chain: %s", exc)
|
|
54
63
|
|
|
55
|
-
# Retention policies
|
|
64
|
+
# Retention policies (scoped to the active profile)
|
|
56
65
|
retention_policies = []
|
|
57
66
|
try:
|
|
58
67
|
conn = sqlite3.connect(str(DB_PATH))
|
|
59
68
|
engine = RetentionEngine(conn)
|
|
60
|
-
retention_policies = engine.list_rules()
|
|
69
|
+
retention_policies = engine.list_rules(profile)
|
|
61
70
|
conn.close()
|
|
62
71
|
except Exception as exc:
|
|
63
72
|
logger.debug("retention engine: %s", exc)
|
|
@@ -78,13 +87,14 @@ async def compliance_status():
|
|
|
78
87
|
"retention_policies": retention_policies,
|
|
79
88
|
"abac_policies_count": abac_policies_count,
|
|
80
89
|
}
|
|
81
|
-
except Exception
|
|
82
|
-
logger.
|
|
83
|
-
return {"available": False, "error":
|
|
90
|
+
except Exception:
|
|
91
|
+
logger.exception("compliance_status error")
|
|
92
|
+
return {"available": False, "error": "Internal server error"}
|
|
84
93
|
|
|
85
94
|
|
|
86
95
|
@router.get("/api/compliance/audit")
|
|
87
96
|
async def query_audit_trail(
|
|
97
|
+
request: Request,
|
|
88
98
|
limit: int = Query(default=50, ge=1, le=500),
|
|
89
99
|
event_type: Optional[str] = Query(default=None),
|
|
90
100
|
since: Optional[str] = Query(default=None),
|
|
@@ -92,20 +102,33 @@ async def query_audit_trail(
|
|
|
92
102
|
"""Query audit trail events with optional filters."""
|
|
93
103
|
if not COMPLIANCE_AVAILABLE:
|
|
94
104
|
return {"available": False, "error": "Compliance engine not available"}
|
|
95
|
-
|
|
105
|
+
# The audit trail reveals operations, actors, and fact_ids. This GET is not
|
|
106
|
+
# covered by the mutation middleware; the same-origin dashboard reads it via
|
|
107
|
+
# a plain GET (no token header), so gate on the loopback-trusted mutation
|
|
108
|
+
# boundary — local owner allowed, remote uncredentialed caller fails closed.
|
|
109
|
+
from superlocalmemory.server.write_identity import require_http_mutation_actor
|
|
110
|
+
require_http_mutation_actor(request, getattr(request.app.state, "daemon_descriptor", None),
|
|
111
|
+
actor_kind="audit-read")
|
|
96
112
|
try:
|
|
113
|
+
profile = get_active_profile()
|
|
97
114
|
audit = AuditChain(str(AUDIT_DB))
|
|
98
|
-
events = audit.
|
|
99
|
-
|
|
115
|
+
events = audit.query(
|
|
116
|
+
profile_id=profile, operation=event_type, start_date=since,
|
|
117
|
+
limit=limit,
|
|
100
118
|
)
|
|
101
|
-
|
|
119
|
+
chain_ok = True
|
|
120
|
+
try:
|
|
121
|
+
chain_ok = audit.verify_integrity()
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
102
124
|
return {
|
|
103
125
|
"available": True, "events": events, "total": len(events),
|
|
126
|
+
"chain_verified": chain_ok, "active_profile": profile,
|
|
104
127
|
"filters": {"event_type": event_type, "since": since, "limit": limit},
|
|
105
128
|
}
|
|
106
|
-
except Exception
|
|
107
|
-
logger.
|
|
108
|
-
return {"available": False, "error":
|
|
129
|
+
except Exception:
|
|
130
|
+
logger.exception("query_audit_trail error")
|
|
131
|
+
return {"available": False, "error": "Internal server error"}
|
|
109
132
|
|
|
110
133
|
|
|
111
134
|
@router.post("/api/compliance/retention-policy")
|
|
@@ -155,6 +178,133 @@ async def create_retention_policy(data: dict):
|
|
|
155
178
|
"active_profile": profile,
|
|
156
179
|
"message": f"Retention policy '{name}' created ({retention_days}d, {action})",
|
|
157
180
|
}
|
|
158
|
-
except Exception
|
|
159
|
-
logger.
|
|
160
|
-
return {"success": False, "error":
|
|
181
|
+
except Exception:
|
|
182
|
+
logger.exception("create_retention_policy error")
|
|
183
|
+
return {"success": False, "error": "Internal server error"}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@router.delete("/api/compliance/retention-policy")
|
|
187
|
+
async def delete_retention_policy(name: str = Query(...)):
|
|
188
|
+
"""Delete a retention policy by name for the active profile."""
|
|
189
|
+
if not COMPLIANCE_AVAILABLE:
|
|
190
|
+
return {"success": False, "error": "Compliance engine not available"}
|
|
191
|
+
try:
|
|
192
|
+
profile = get_active_profile()
|
|
193
|
+
conn = sqlite3.connect(str(DB_PATH))
|
|
194
|
+
engine = RetentionEngine(conn)
|
|
195
|
+
removed = engine.delete_rule(profile, name)
|
|
196
|
+
conn.close()
|
|
197
|
+
if not removed:
|
|
198
|
+
return {"success": False, "error": f"Policy '{name}' not found"}
|
|
199
|
+
return {"success": True, "active_profile": profile,
|
|
200
|
+
"message": f"Retention policy '{name}' deleted"}
|
|
201
|
+
except Exception:
|
|
202
|
+
logger.exception("delete_retention_policy error")
|
|
203
|
+
return {"success": False, "error": "Internal server error"}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@router.post("/api/compliance/retention/enforce")
|
|
207
|
+
async def enforce_retention():
|
|
208
|
+
"""Run all retention policies for the active profile now.
|
|
209
|
+
|
|
210
|
+
Moves expired facts to their rule's terminal lifecycle zone (archive/
|
|
211
|
+
tombstone) or counts them (notify). Soft-state only — never a raw delete.
|
|
212
|
+
"""
|
|
213
|
+
if not COMPLIANCE_AVAILABLE:
|
|
214
|
+
return {"success": False, "error": "Compliance engine not available"}
|
|
215
|
+
try:
|
|
216
|
+
profile = get_active_profile()
|
|
217
|
+
conn = sqlite3.connect(str(DB_PATH))
|
|
218
|
+
engine = RetentionEngine(conn)
|
|
219
|
+
result = engine.enforce(profile)
|
|
220
|
+
conn.close()
|
|
221
|
+
return {"success": True, **result}
|
|
222
|
+
except Exception:
|
|
223
|
+
logger.exception("enforce_retention error")
|
|
224
|
+
return {"success": False, "error": "Internal server error"}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ── GDPR Art. 15/20 — Right to Access / Portability ─────────────────────────
|
|
228
|
+
|
|
229
|
+
@router.get("/api/compliance/gdpr/export")
|
|
230
|
+
async def gdpr_export(request: Request):
|
|
231
|
+
"""Export ALL data for the active profile (GDPR Art. 20 portability).
|
|
232
|
+
|
|
233
|
+
Comprehensive 14-table export (memories, facts, entities, edges, trust,
|
|
234
|
+
feedback, behavioral patterns, provenance, audit, …) as a downloadable
|
|
235
|
+
JSON attachment. Read-only.
|
|
236
|
+
"""
|
|
237
|
+
if not COMPLIANCE_AVAILABLE:
|
|
238
|
+
return {"available": False, "error": "Compliance engine not available"}
|
|
239
|
+
# A full-workspace data dump is an administrative action. Triggered by a
|
|
240
|
+
# top-level navigation (a.href) that cannot carry a credential header, so
|
|
241
|
+
# gate on the loopback-trusted mutation boundary (local owner allowed,
|
|
242
|
+
# remote uncredentialed fails closed) AND require MANAGE — in company mode
|
|
243
|
+
# a session cookie flows on navigation, so a non-admin user is still denied;
|
|
244
|
+
# the machine owner keeps MANAGE.
|
|
245
|
+
from superlocalmemory.server.write_identity import require_http_mutation_actor
|
|
246
|
+
from superlocalmemory.server.rbac_enforce import require_manage
|
|
247
|
+
require_http_mutation_actor(request, getattr(request.app.state, "daemon_descriptor", None),
|
|
248
|
+
actor_kind="gdpr-export")
|
|
249
|
+
require_manage(request)
|
|
250
|
+
try:
|
|
251
|
+
engine = get_engine_lazy(request.app.state)
|
|
252
|
+
if engine is None:
|
|
253
|
+
return {"available": False, "error": "Engine not initialized"}
|
|
254
|
+
profile = get_active_profile()
|
|
255
|
+
data = GDPRCompliance(engine._db).export_profile_data(profile)
|
|
256
|
+
filename = f"slm-export-{profile}.json"
|
|
257
|
+
return JSONResponse(
|
|
258
|
+
content=data,
|
|
259
|
+
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
|
260
|
+
)
|
|
261
|
+
except Exception:
|
|
262
|
+
logger.exception("gdpr_export error")
|
|
263
|
+
return {"available": False, "error": "Internal server error"}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# ── GDPR Art. 17 — Right to Erasure ─────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
@router.post("/api/compliance/gdpr/erase")
|
|
269
|
+
async def gdpr_erase(request: Request, data: dict = {}):
|
|
270
|
+
"""Permanently erase ALL data for the active profile (GDPR Art. 17).
|
|
271
|
+
|
|
272
|
+
IRREVERSIBLE. Requires an explicit ``confirm`` field in the body that
|
|
273
|
+
exactly matches the active profile name — this is the guard against an
|
|
274
|
+
accidental one-click wipe. The 'default' profile can never be erased
|
|
275
|
+
(enforced in GDPRCompliance.forget_profile). Mutation-authorized.
|
|
276
|
+
"""
|
|
277
|
+
if not COMPLIANCE_AVAILABLE:
|
|
278
|
+
return {"success": False, "error": "Compliance engine not available"}
|
|
279
|
+
try:
|
|
280
|
+
engine = get_engine_lazy(request.app.state)
|
|
281
|
+
if engine is None:
|
|
282
|
+
return {"success": False, "error": "Engine not initialized"}
|
|
283
|
+
profile = get_active_profile()
|
|
284
|
+
confirm = (data or {}).get("confirm", "")
|
|
285
|
+
if confirm != profile:
|
|
286
|
+
return {
|
|
287
|
+
"success": False,
|
|
288
|
+
"error": (
|
|
289
|
+
"Confirmation required: send {\"confirm\": \"" + profile +
|
|
290
|
+
"\"} to erase this profile. This is irreversible."
|
|
291
|
+
),
|
|
292
|
+
}
|
|
293
|
+
if profile == "default":
|
|
294
|
+
return {"success": False,
|
|
295
|
+
"error": "The 'default' profile cannot be erased."}
|
|
296
|
+
# Irreversible erasure is admin-only (beyond mutation auth).
|
|
297
|
+
from superlocalmemory.server.rbac_enforce import require_manage
|
|
298
|
+
require_manage(request, profile=profile)
|
|
299
|
+
authorization = authorize_route_mutation(
|
|
300
|
+
request,
|
|
301
|
+
operation="delete",
|
|
302
|
+
source_agent_id="http-gdpr-erase",
|
|
303
|
+
profile_id=profile,
|
|
304
|
+
)
|
|
305
|
+
result = GDPRCompliance(engine._db).forget_profile(profile)
|
|
306
|
+
authorization.complete()
|
|
307
|
+
return {"success": True, "active_profile": profile, **(result or {})}
|
|
308
|
+
except Exception:
|
|
309
|
+
logger.exception("gdpr_erase error")
|
|
310
|
+
return {"success": False, "error": "Internal server error"}
|