superlocalmemory 3.7.7 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +85 -0
- package/README.md +199 -29
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- 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/agents/slm-governance-advisor.md +80 -0
- package/plugin-src/agents/slm-loop-runner.md +71 -0
- package/plugin-src/agents/slm-memory-advisor.md +10 -5
- package/plugin-src/agents/slm-optimize-advisor.md +9 -3
- package/plugin-src/commands/slm-loop.md +31 -0
- package/plugin-src/hooks/hooks.json +79 -0
- package/plugin-src/manifest.json +7 -2
- package/plugin-src/requirements.txt +1 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/rules/CLAUDE.md.fragment +8 -8
- package/plugin-src/scripts/slm-launch +46 -7
- package/plugin-src/settings.json +9 -0
- 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-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- 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-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +1 -2
- 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 +96 -12
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/loop_cmd.py +192 -0
- package/src/superlocalmemory/cli/main.py +39 -4
- 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/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 +5 -43
- 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 +17 -2
- package/src/superlocalmemory/core/embeddings.py +12 -1
- package/src/superlocalmemory/core/engine.py +17 -1
- package/src/superlocalmemory/core/engine_ingestion.py +29 -0
- 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/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- 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 +2 -0
- 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 +35 -11
- 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/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +114 -1
- package/src/superlocalmemory/infra/auth_middleware.py +28 -0
- 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/reward_proxy.py +42 -9
- 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 +243 -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 +21 -49
- 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 +50 -5
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- 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 +168 -26
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- 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 +21 -4
- 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 +34 -10
- package/src/superlocalmemory/server/routes/behavioral.py +13 -12
- package/src/superlocalmemory/server/routes/brain.py +21 -5
- package/src/superlocalmemory/server/routes/chat.py +72 -16
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +436 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +9 -4
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +135 -17
- 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 +14 -14
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +221 -49
- package/src/superlocalmemory/server/routes/mesh.py +95 -15
- 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 +124 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +13 -6
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/v3_api.py +194 -81
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +30 -5
- package/src/superlocalmemory/server/unified_daemon.py +431 -75
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +53 -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/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +87 -0
- package/src/superlocalmemory/storage/schema_v32.py +0 -9
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- 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 +306 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +47 -31
- package/src/superlocalmemory/ui/js/dashboard.js +314 -63
- 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 +780 -0
- package/src/superlocalmemory/ui/js/od-brain.js +779 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +593 -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 +887 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +539 -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 +1053 -0
- package/src/superlocalmemory/ui/js/od-shell.js +593 -0
- package/src/superlocalmemory/ui/js/od-skills.js +573 -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 +2 -2
- 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/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -73,6 +73,38 @@ _LEGACY_PORT = 8767
|
|
|
73
73
|
_ACTIVE_DAEMON_DESCRIPTOR: DaemonDescriptor | None = None
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
def _rbac_read_gate(request, app_state):
|
|
77
|
+
"""RBAC gate for sensitive content reads. Returns a JSONResponse to reject,
|
|
78
|
+
or None to allow. No-op unless RBAC is active (>=1 user)."""
|
|
79
|
+
from fastapi.responses import JSONResponse
|
|
80
|
+
rbac = getattr(app_state, "rbac", None)
|
|
81
|
+
if rbac is None:
|
|
82
|
+
return None
|
|
83
|
+
try:
|
|
84
|
+
active = rbac.user_count() > 0
|
|
85
|
+
except Exception:
|
|
86
|
+
# Fail CLOSED: if we cannot determine RBAC state we must not silently
|
|
87
|
+
# allow reads (a DB error would otherwise open the whole read surface).
|
|
88
|
+
return JSONResponse(status_code=503,
|
|
89
|
+
content={"error": "authorization temporarily unavailable"})
|
|
90
|
+
if not active:
|
|
91
|
+
return None # single-operator install — reads are open
|
|
92
|
+
token = (request.headers.get("x-slm-user-session", "")
|
|
93
|
+
or (request.cookies.get("slm_session", "") if request.cookies else ""))
|
|
94
|
+
user = rbac.resolve_session(token) if token else None
|
|
95
|
+
if user is None:
|
|
96
|
+
if rbac.require_login():
|
|
97
|
+
return JSONResponse(status_code=401,
|
|
98
|
+
content={"error": "Login required to read memory."})
|
|
99
|
+
return None # owner/operator, personal mode
|
|
100
|
+
from superlocalmemory.access.rbac import Permission
|
|
101
|
+
from superlocalmemory.server.routes.helpers import get_active_profile
|
|
102
|
+
if rbac.has_permission(user["user_id"], get_active_profile(), Permission.READ):
|
|
103
|
+
return None
|
|
104
|
+
return JSONResponse(status_code=403,
|
|
105
|
+
content={"error": "Your role cannot read this workspace."})
|
|
106
|
+
|
|
107
|
+
|
|
76
108
|
def _configured_daemon_port() -> int:
|
|
77
109
|
"""Return the configured bind port, falling back safely to the default."""
|
|
78
110
|
try:
|
|
@@ -210,7 +242,10 @@ class EngineRecallAdapter:
|
|
|
210
242
|
if r.fact.memory_id
|
|
211
243
|
})
|
|
212
244
|
memory_map = (
|
|
213
|
-
self._engine._db.get_memory_content_batch(
|
|
245
|
+
self._engine._db.get_memory_content_batch(
|
|
246
|
+
memory_ids, self._engine.profile_id,
|
|
247
|
+
include_global=True, include_shared=True,
|
|
248
|
+
)
|
|
214
249
|
if memory_ids else {}
|
|
215
250
|
)
|
|
216
251
|
# v3.6.6: same shared chokepoint as the HTTP route — identical output.
|
|
@@ -534,7 +569,7 @@ class ObserveBuffer:
|
|
|
534
569
|
"captured": False,
|
|
535
570
|
"durable": False,
|
|
536
571
|
"reason": "durable admission failed",
|
|
537
|
-
"error":
|
|
572
|
+
"error": "internal error",
|
|
538
573
|
}
|
|
539
574
|
|
|
540
575
|
def _clear_seen(self) -> None:
|
|
@@ -643,6 +678,43 @@ async def _start_legacy_redirect(primary_port: int, legacy_port: int) -> None:
|
|
|
643
678
|
# Lifespan
|
|
644
679
|
# ---------------------------------------------------------------------------
|
|
645
680
|
|
|
681
|
+
def _warm_spreading_activation(engine, runtime) -> bool:
|
|
682
|
+
"""Pre-warm the spreading-activation channel for the active profile.
|
|
683
|
+
|
|
684
|
+
The ``--fast`` warmup recalls deliberately skip spreading activation (and the
|
|
685
|
+
Mode-C remote agentic verification), which left the first FULL user recall
|
|
686
|
+
paying the cold graph-load cost: the ``graph_edges`` + ``association_edges``
|
|
687
|
+
page cache and the ``fact_importance`` PageRank/community cache. This warms
|
|
688
|
+
that channel directly — pure local graph work, never a remote/LLM call — so
|
|
689
|
+
the first full recall is warm. Fail-soft; returns True only when it ran.
|
|
690
|
+
"""
|
|
691
|
+
try:
|
|
692
|
+
retr = getattr(engine, "_retrieval_engine", None)
|
|
693
|
+
sa = getattr(retr, "_spreading_activation", None) if retr else None
|
|
694
|
+
embedder = getattr(retr, "_embedder", None) if retr else None
|
|
695
|
+
if sa is None or embedder is None or not hasattr(embedder, "embed"):
|
|
696
|
+
return False
|
|
697
|
+
query_embedding = embedder.embed("memory recall performance")
|
|
698
|
+
if query_embedding is None:
|
|
699
|
+
return False
|
|
700
|
+
active_pid = getattr(engine, "profile_id", "default") or "default"
|
|
701
|
+
lease = runtime.operation_nowait() if runtime is not None else None
|
|
702
|
+
if lease is not None:
|
|
703
|
+
with lease as snap:
|
|
704
|
+
if snap is None:
|
|
705
|
+
return False
|
|
706
|
+
sa.search(query_embedding, profile_id=active_pid, top_k=7)
|
|
707
|
+
else:
|
|
708
|
+
sa.search(query_embedding, profile_id=active_pid, top_k=7)
|
|
709
|
+
logger.info(
|
|
710
|
+
"Spreading-activation graph pre-warmed for profile %s", active_pid,
|
|
711
|
+
)
|
|
712
|
+
return True
|
|
713
|
+
except Exception as exc:
|
|
714
|
+
logger.warning("Spreading-activation warmup failed (non-fatal): %s", exc)
|
|
715
|
+
return False
|
|
716
|
+
|
|
717
|
+
|
|
646
718
|
@asynccontextmanager
|
|
647
719
|
async def lifespan(application: FastAPI):
|
|
648
720
|
"""Initialize engine, workers, and optional services on startup."""
|
|
@@ -765,14 +837,10 @@ async def lifespan(application: FastAPI):
|
|
|
765
837
|
engine = MemoryEngine(config)
|
|
766
838
|
engine.initialize()
|
|
767
839
|
|
|
768
|
-
#
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
db.execute("PRAGMA journal_mode=WAL")
|
|
773
|
-
db.execute("PRAGMA synchronous=NORMAL")
|
|
774
|
-
except Exception:
|
|
775
|
-
pass
|
|
840
|
+
# WAL is already established at DB creation (DatabaseManager._enable_wal
|
|
841
|
+
# / schema init). Re-asserting PRAGMA journal_mode=WAL here is a
|
|
842
|
+
# schema-level write on the shared connection that raced in-flight
|
|
843
|
+
# startup requests for the writer lock — removed (H-CONC-3).
|
|
776
844
|
|
|
777
845
|
from superlocalmemory.server.profile_runtime import bind_profile_runtime
|
|
778
846
|
|
|
@@ -846,24 +914,28 @@ async def lifespan(application: FastAPI):
|
|
|
846
914
|
try:
|
|
847
915
|
import sqlite3 as _sqlite3
|
|
848
916
|
_idx_conn = _sqlite3.connect(str(_memory_db))
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
917
|
+
try:
|
|
918
|
+
_idx_conn.execute("PRAGMA journal_mode=WAL")
|
|
919
|
+
_idx_conn.execute(
|
|
920
|
+
"CREATE INDEX IF NOT EXISTS idx_edges_source_weight "
|
|
921
|
+
"ON graph_edges(profile_id, source_id, weight DESC)"
|
|
922
|
+
)
|
|
923
|
+
_idx_conn.execute(
|
|
924
|
+
"CREATE INDEX IF NOT EXISTS idx_edges_target_weight "
|
|
925
|
+
"ON graph_edges(profile_id, target_id, weight DESC)"
|
|
926
|
+
)
|
|
927
|
+
_idx_conn.execute(
|
|
928
|
+
"CREATE INDEX IF NOT EXISTS idx_assoc_source_weight "
|
|
929
|
+
"ON association_edges(profile_id, source_fact_id, weight DESC)"
|
|
930
|
+
)
|
|
931
|
+
_idx_conn.execute(
|
|
932
|
+
"CREATE INDEX IF NOT EXISTS idx_assoc_target_weight "
|
|
933
|
+
"ON association_edges(profile_id, target_fact_id, weight DESC)"
|
|
934
|
+
)
|
|
935
|
+
finally:
|
|
936
|
+
# CP-09: close even if an execute() raises, so the connection
|
|
937
|
+
# (and its file handle / shared DB lock) never leaks.
|
|
938
|
+
_idx_conn.close()
|
|
867
939
|
except Exception as _idx_exc:
|
|
868
940
|
logger.debug("SpreadingActivation covering indexes skipped: %s", _idx_exc)
|
|
869
941
|
|
|
@@ -909,6 +981,11 @@ async def lifespan(application: FastAPI):
|
|
|
909
981
|
|
|
910
982
|
Runs after embedding warm (embed first so recall can use it).
|
|
911
983
|
Named 'recall-warmup' so it appears clearly in thread dumps.
|
|
984
|
+
|
|
985
|
+
v3.x: each warmup query holds its own operation_nowait() lease
|
|
986
|
+
(previously one lease across both queries held for up to 20s,
|
|
987
|
+
blocking any profile switch issued at daemon start). A pending
|
|
988
|
+
transition preempts remaining queries; they complete on next boot.
|
|
912
989
|
"""
|
|
913
990
|
import time as _t
|
|
914
991
|
for _ in range(60):
|
|
@@ -920,9 +997,20 @@ async def lifespan(application: FastAPI):
|
|
|
920
997
|
# Fire 2 warmup queries: one to load the graph page cache,
|
|
921
998
|
# second to warm the reranker subprocess + all producers.
|
|
922
999
|
# Without this, dashboard POST /api/search hits 11s cold.
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1000
|
+
# Each query holds its own brief operation_nowait() lease so a
|
|
1001
|
+
# concurrent profile switch is not blocked by both recalls.
|
|
1002
|
+
for wq in ("memory recall performance", "context injection retrieval"):
|
|
1003
|
+
with profile_runtime.operation_nowait() as _snap:
|
|
1004
|
+
if _snap is None:
|
|
1005
|
+
logger.debug(
|
|
1006
|
+
"Recall warmup preempted by profile transition "
|
|
1007
|
+
"— skipping remaining warmup queries"
|
|
1008
|
+
)
|
|
1009
|
+
break
|
|
1010
|
+
engine.recall(wq, limit=5, fast=True) # short lease so a profile switch can drain within 5s
|
|
1011
|
+
# v3.8: the --fast recalls above skip spreading activation; warm
|
|
1012
|
+
# that channel directly so the first FULL recall is not cold.
|
|
1013
|
+
_warm_spreading_activation(engine, profile_runtime)
|
|
926
1014
|
elapsed = round((_t.monotonic() - t0) * 1000)
|
|
927
1015
|
logger.info(
|
|
928
1016
|
"Recall engine pre-warmed in %dms", elapsed,
|
|
@@ -1079,9 +1167,50 @@ async def lifespan(application: FastAPI):
|
|
|
1079
1167
|
else:
|
|
1080
1168
|
application.state.mesh_broker = None
|
|
1081
1169
|
except Exception as exc:
|
|
1082
|
-
logger.
|
|
1170
|
+
logger.warning("Mesh broker init failed: %s", exc)
|
|
1083
1171
|
application.state.mesh_broker = None
|
|
1084
1172
|
|
|
1173
|
+
# RBAC / teams (C3): user identity + role enforcement over memory.db.
|
|
1174
|
+
# Additive — with zero users the daemon stays single-operator (owner).
|
|
1175
|
+
try:
|
|
1176
|
+
from superlocalmemory.access.rbac import RbacEngine
|
|
1177
|
+
rbac_db = config.db_path if config else state_path("memory.db")
|
|
1178
|
+
rbac_engine = RbacEngine(str(rbac_db))
|
|
1179
|
+
rbac_engine.purge_expired_sessions()
|
|
1180
|
+
application.state.rbac = rbac_engine
|
|
1181
|
+
logger.info("RBAC engine ready (users=%d)", rbac_engine.user_count())
|
|
1182
|
+
except Exception as exc:
|
|
1183
|
+
logger.warning("RBAC engine init failed: %s", exc)
|
|
1184
|
+
application.state.rbac = None
|
|
1185
|
+
|
|
1186
|
+
# Deployment config (v3.8.0) — read [deployment] from config.toml and wire.
|
|
1187
|
+
# Additive / fail-open: personal defaults are a no-op so existing installs
|
|
1188
|
+
# with no [deployment] section behave EXACTLY as before.
|
|
1189
|
+
# ENFORCE rule: only UPGRADE a setting, NEVER downgrade an already-stronger
|
|
1190
|
+
# runtime setting (e.g. RBAC require_login already True → leave it alone).
|
|
1191
|
+
try:
|
|
1192
|
+
from superlocalmemory.core.config import load_deployment_config
|
|
1193
|
+
deployment = load_deployment_config()
|
|
1194
|
+
application.state.deployment = deployment
|
|
1195
|
+
if deployment.require_login:
|
|
1196
|
+
_dep_rbac = getattr(application.state, "rbac", None)
|
|
1197
|
+
if _dep_rbac is not None and not _dep_rbac.require_login():
|
|
1198
|
+
_dep_rbac.set_require_login(True)
|
|
1199
|
+
logger.info(
|
|
1200
|
+
"Deployment: require_login enforced via enterprise deployment config"
|
|
1201
|
+
)
|
|
1202
|
+
# TODO: Wire deployment.pii_redaction → PII redaction subsystem (WP-10)
|
|
1203
|
+
# TODO: Wire deployment.retention_enabled → retention scheduler (WP-11)
|
|
1204
|
+
logger.info(
|
|
1205
|
+
"Deployment config loaded: mode=%s require_login=%s "
|
|
1206
|
+
"pii=%s retention=%s audit=%s",
|
|
1207
|
+
deployment.mode, deployment.require_login,
|
|
1208
|
+
deployment.pii_redaction, deployment.retention_enabled, deployment.audit,
|
|
1209
|
+
)
|
|
1210
|
+
except Exception as _dep_exc:
|
|
1211
|
+
logger.warning("Deployment config wire failed (non-fatal): %s", _dep_exc)
|
|
1212
|
+
application.state.deployment = None
|
|
1213
|
+
|
|
1085
1214
|
# Start idle watchdog if configured
|
|
1086
1215
|
idle_timeout = int(os.environ.get("SLM_DAEMON_IDLE_TIMEOUT", "0"))
|
|
1087
1216
|
if config and hasattr(config, 'daemon_idle_timeout'):
|
|
@@ -1114,7 +1243,9 @@ async def lifespan(application: FastAPI):
|
|
|
1114
1243
|
try:
|
|
1115
1244
|
from superlocalmemory.cli.context_commands import build_default_adapters
|
|
1116
1245
|
from superlocalmemory.hooks.sync_loop import schedule as _schedule_sync
|
|
1117
|
-
|
|
1246
|
+
# Keep the task handle so it can be cancelled at shutdown (H-CONC-2)
|
|
1247
|
+
# — otherwise adapter file I/O outlives the daemon.
|
|
1248
|
+
application.state._sync_task = _schedule_sync(build_default_adapters())
|
|
1118
1249
|
except Exception as exc: # pragma: no cover — defensive
|
|
1119
1250
|
logger.warning("cross-platform sync loop failed to start: %s", exc)
|
|
1120
1251
|
|
|
@@ -1221,6 +1352,19 @@ async def lifespan(application: FastAPI):
|
|
|
1221
1352
|
)
|
|
1222
1353
|
yield
|
|
1223
1354
|
|
|
1355
|
+
# Cancel the cross-platform sync loop (H-CONC-2) so adapter file I/O does
|
|
1356
|
+
# not outlive the daemon.
|
|
1357
|
+
try:
|
|
1358
|
+
_sync_task = getattr(application.state, "_sync_task", None)
|
|
1359
|
+
if _sync_task is not None and not _sync_task.done():
|
|
1360
|
+
_sync_task.cancel()
|
|
1361
|
+
try:
|
|
1362
|
+
await _sync_task
|
|
1363
|
+
except asyncio.CancelledError:
|
|
1364
|
+
pass
|
|
1365
|
+
except Exception: # pragma: no cover — defensive
|
|
1366
|
+
pass
|
|
1367
|
+
|
|
1224
1368
|
# Cancel optimize metrics flush loop + run final flush before shutdown
|
|
1225
1369
|
try:
|
|
1226
1370
|
_flush_task = getattr(application.state, "_optimize_flush_task", None)
|
|
@@ -1463,7 +1607,9 @@ def create_app() -> FastAPI:
|
|
|
1463
1607
|
allow_origins=[
|
|
1464
1608
|
"http://localhost:8765", "http://127.0.0.1:8765",
|
|
1465
1609
|
"http://localhost:8767", "http://127.0.0.1:8767", # legacy compat
|
|
1466
|
-
|
|
1610
|
+
# M-04 (3.7.9): removed the undocumented port 8417 origin — it had
|
|
1611
|
+
# no known consumer and let any local service on 8417 make
|
|
1612
|
+
# credentialed cross-origin requests to the API.
|
|
1467
1613
|
],
|
|
1468
1614
|
allow_credentials=True,
|
|
1469
1615
|
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
|
@@ -1568,13 +1714,19 @@ def create_app() -> FastAPI:
|
|
|
1568
1714
|
# toggled INDEPENDENTLY from the UI with no restart. UI save fires the
|
|
1569
1715
|
# callback immediately; external edits are caught by the 2s watchdog.
|
|
1570
1716
|
_opt_store.register_change_callback(_proxy.reload_from_config)
|
|
1571
|
-
_opt_store.start_watchdog()
|
|
1572
1717
|
logger.info(
|
|
1573
1718
|
"optimize.proxy mounted on /v1/*, /v1beta/* port=8765 "
|
|
1574
1719
|
"(runtime cache/compress hot-reload enabled)"
|
|
1575
1720
|
)
|
|
1576
1721
|
else:
|
|
1577
1722
|
application.state.optimize_proxy = None
|
|
1723
|
+
# H1 fix: the config watchdog must run REGARDLESS of proxy state so
|
|
1724
|
+
# optimize.json edits (cache/compress toggles, and a future
|
|
1725
|
+
# proxy_enabled flip) are picked up at runtime. Previously it only
|
|
1726
|
+
# started when the proxy was already on, so a daemon that booted with
|
|
1727
|
+
# the proxy off never saw any optimize.json change. start_watchdog()
|
|
1728
|
+
# is idempotent.
|
|
1729
|
+
_opt_store.start_watchdog()
|
|
1578
1730
|
except ImportError:
|
|
1579
1731
|
application.state.optimize_proxy = None
|
|
1580
1732
|
logger.debug("optimize.proxy not installed — skipping")
|
|
@@ -1610,6 +1762,14 @@ def create_app() -> FastAPI:
|
|
|
1610
1762
|
if _mcp_allowed:
|
|
1611
1763
|
from mcp.server.transport_security import TransportSecuritySettings
|
|
1612
1764
|
if _mcp_allowed == "*":
|
|
1765
|
+
# M-05 (3.7.9): "*" fully disables DNS-rebinding protection.
|
|
1766
|
+
# Never silent — a convenience setting in a CI/Docker env must
|
|
1767
|
+
# not quietly expose the instance.
|
|
1768
|
+
logger.warning(
|
|
1769
|
+
"SLM_MCP_ALLOWED_HOSTS=* disables MCP DNS-rebinding "
|
|
1770
|
+
"protection entirely. Prefer an explicit host list; only "
|
|
1771
|
+
"use '*' on a trusted private network."
|
|
1772
|
+
)
|
|
1613
1773
|
_mcp_fastmcp.settings.transport_security = TransportSecuritySettings(
|
|
1614
1774
|
enable_dns_rebinding_protection=False,
|
|
1615
1775
|
)
|
|
@@ -1688,22 +1848,47 @@ def _register_dashboard_routes(application: FastAPI) -> None:
|
|
|
1688
1848
|
_write_limiter = RateLimiter(max_requests=_rl_write, window_seconds=_rl_window)
|
|
1689
1849
|
_read_limiter = RateLimiter(max_requests=_rl_read, window_seconds=_rl_window)
|
|
1690
1850
|
|
|
1691
|
-
# S9-DASH-09: loopback (127.0.0.1 / ::1) is
|
|
1692
|
-
#
|
|
1693
|
-
#
|
|
1694
|
-
#
|
|
1695
|
-
#
|
|
1696
|
-
#
|
|
1697
|
-
|
|
1698
|
-
|
|
1851
|
+
# S9-DASH-09: loopback (127.0.0.1 / ::1) is the local dashboard and
|
|
1852
|
+
# makes many rapid reads (Brain + tabs + polling). L-03 (3.7.9): rather
|
|
1853
|
+
# than exempt loopback entirely, give it a *generous* limit — far above
|
|
1854
|
+
# normal UI polling — so a runaway local agent's write flood is still
|
|
1855
|
+
# eventually throttled. A LAN browser allowlisted in SLM_REMOTE mode
|
|
1856
|
+
# (is_rate_limit_exempt, non-loopback) stays fully exempt.
|
|
1857
|
+
_lb_write = max(300, _rl_write * 10)
|
|
1858
|
+
_lb_read = max(2000, _rl_read * 20)
|
|
1859
|
+
_lb_write_limiter = RateLimiter(max_requests=_lb_write, window_seconds=_rl_window)
|
|
1860
|
+
_lb_read_limiter = RateLimiter(max_requests=_lb_read, window_seconds=_rl_window)
|
|
1861
|
+
|
|
1862
|
+
# Task #47: register the live limiters so the dashboard PUT
|
|
1863
|
+
# /api/v3/ratelimit reconfigures them at runtime (no restart), then
|
|
1864
|
+
# apply any persisted override from config.json.
|
|
1865
|
+
try:
|
|
1866
|
+
from superlocalmemory.infra.rate_limiter import (
|
|
1867
|
+
register_managed as _reg_rl, reset_managed as _reset_rl,
|
|
1868
|
+
)
|
|
1869
|
+
_reset_rl()
|
|
1870
|
+
_reg_rl("write", _write_limiter)
|
|
1871
|
+
_reg_rl("read", _read_limiter)
|
|
1872
|
+
_reg_rl("lb_write", _lb_write_limiter)
|
|
1873
|
+
_reg_rl("lb_read", _lb_read_limiter)
|
|
1874
|
+
from superlocalmemory.server.routes.ratelimit import (
|
|
1875
|
+
load_persisted_limits as _load_rl,
|
|
1876
|
+
)
|
|
1877
|
+
_load_rl()
|
|
1878
|
+
except Exception as _reg_exc: # pragma: no cover - defensive
|
|
1879
|
+
logger.debug("rate-limit runtime registration skipped: %s", _reg_exc)
|
|
1699
1880
|
|
|
1700
1881
|
@application.middleware("http")
|
|
1701
1882
|
async def rate_limit_middleware(request, call_next):
|
|
1702
1883
|
client_ip = request.client.host if request.client else "unknown"
|
|
1703
|
-
if is_rate_limit_exempt(client_ip):
|
|
1704
|
-
return await call_next(request)
|
|
1705
1884
|
is_write = request.method in ("POST", "PUT", "DELETE", "PATCH")
|
|
1706
|
-
|
|
1885
|
+
loopback = client_ip in ("127.0.0.1", "::1")
|
|
1886
|
+
if not loopback and is_rate_limit_exempt(client_ip):
|
|
1887
|
+
return await call_next(request)
|
|
1888
|
+
if loopback:
|
|
1889
|
+
limiter = _lb_write_limiter if is_write else _lb_read_limiter
|
|
1890
|
+
else:
|
|
1891
|
+
limiter = _write_limiter if is_write else _read_limiter
|
|
1707
1892
|
allowed, remaining = limiter.is_allowed(client_ip)
|
|
1708
1893
|
if not allowed:
|
|
1709
1894
|
from fastapi.responses import JSONResponse
|
|
@@ -1726,6 +1911,7 @@ def _register_dashboard_routes(application: FastAPI) -> None:
|
|
|
1726
1911
|
from superlocalmemory.infra.auth_middleware import (
|
|
1727
1912
|
authorize_http_mcp_request,
|
|
1728
1913
|
check_api_key,
|
|
1914
|
+
loopback_strict_mode_enabled,
|
|
1729
1915
|
)
|
|
1730
1916
|
from superlocalmemory.server.write_identity import (
|
|
1731
1917
|
require_http_mutation_actor,
|
|
@@ -1801,25 +1987,84 @@ def _register_dashboard_routes(application: FastAPI) -> None:
|
|
|
1801
1987
|
content={"error": str(_identity_exc.detail)},
|
|
1802
1988
|
)
|
|
1803
1989
|
raise
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1990
|
+
# v3.7.6 (#71/#73/#74): require_http_mutation_actor above is
|
|
1991
|
+
# the authoritative write-auth boundary — it accepts the
|
|
1992
|
+
# daemon capability, the dashboard install token, a matching
|
|
1993
|
+
# X-SLM-API-Key, or an uncredentialed loopback caller, and
|
|
1994
|
+
# fails closed for everyone else. Running check_api_key as an
|
|
1995
|
+
# unconditional second gate used to 401 write paths stage 1
|
|
1996
|
+
# had already authorized: capability-authenticated daemon
|
|
1997
|
+
# write-throughs (MCP `remember`, #71) and install-token
|
|
1998
|
+
# dashboard writes / config tests (#73/#74) whenever an
|
|
1999
|
+
# api_key file existed.
|
|
2000
|
+
#
|
|
2001
|
+
# v3.7.8 (F1/F2): that fix silently narrowed the api_key
|
|
2002
|
+
# feature — a configured api_key file used to force even
|
|
2003
|
+
# loopback writes to present a credential; after #71/#73/#74
|
|
2004
|
+
# loopback writes need none, with no opt-back-in. This is the
|
|
2005
|
+
# single enforcement point for the opt-in strict posture
|
|
2006
|
+
# (SLM_REQUIRE_API_KEY_LOOPBACK): re-run check_api_key, but
|
|
2007
|
+
# ONLY for the uncredentialed-loopback case — a caller who
|
|
2008
|
+
# presented none of the three credential headers above and
|
|
2009
|
+
# was authorized purely by being loopback. Credentialed
|
|
2010
|
+
# callers (capability / install token / api key) were already
|
|
2011
|
+
# validated by require_http_mutation_actor and are never
|
|
2012
|
+
# re-gated here, so #71/#73/#74 stay fixed.
|
|
2013
|
+
if loopback_strict_mode_enabled():
|
|
2014
|
+
_has_credential = any(
|
|
2015
|
+
headers.get(_h)
|
|
2016
|
+
for _h in (
|
|
2017
|
+
"x-slm-daemon-capability",
|
|
2018
|
+
"x-install-token",
|
|
2019
|
+
"x-slm-api-key",
|
|
2020
|
+
)
|
|
2021
|
+
)
|
|
2022
|
+
if not _has_credential and not check_api_key(
|
|
2023
|
+
headers, is_write=True
|
|
2024
|
+
):
|
|
2025
|
+
from fastapi.responses import JSONResponse
|
|
2026
|
+
return JSONResponse(
|
|
2027
|
+
status_code=401,
|
|
2028
|
+
content={
|
|
2029
|
+
"error": (
|
|
2030
|
+
"SLM_REQUIRE_API_KEY_LOOPBACK is set: "
|
|
2031
|
+
"loopback writes must present a matching "
|
|
2032
|
+
"X-SLM-API-Key."
|
|
2033
|
+
)
|
|
2034
|
+
},
|
|
2035
|
+
)
|
|
2036
|
+
# RBAC read gate (C3/audit SEC-C-01): sensitive content reads must
|
|
2037
|
+
# also respect roles. Engages ONLY when RBAC is active (>=1 user) —
|
|
2038
|
+
# single-operator installs are unaffected. Owner (no session) reads
|
|
2039
|
+
# freely unless company mode (require_login) is on; a logged-in user
|
|
2040
|
+
# must hold READ on the active workspace.
|
|
2041
|
+
_p = request.url.path
|
|
2042
|
+
_is_sensitive_read = (
|
|
2043
|
+
(request.method == "GET" and _p.startswith((
|
|
2044
|
+
"/api/memories", "/api/facts", "/api/clusters", "/api/graph",
|
|
2045
|
+
"/api/v3/associations", "/api/v3/core-memory",
|
|
2046
|
+
"/api/v3/soft-prompts",
|
|
2047
|
+
# Config metadata GETs expose the install path (base_dir) and
|
|
2048
|
+
# LLM stack (provider/model/endpoint). Harmless to the loopback
|
|
2049
|
+
# owner (personal mode: gate is a no-op), but in company mode an
|
|
2050
|
+
# unauthenticated caller must not read them — same login gate as
|
|
2051
|
+
# content reads.
|
|
2052
|
+
"/api/v3/dashboard", "/api/v3/mode", "/api/v3/embedding/config",
|
|
2053
|
+
# SEC-M-02: the remaining config GETs also expose base_dir,
|
|
2054
|
+
# daemon port, and backend topology — gate them in company mode.
|
|
2055
|
+
"/api/v3/scope/config", "/api/v3/storage/config",
|
|
2056
|
+
"/api/v3/daemon/config", "/api/v3/mesh/config",
|
|
2057
|
+
"/api/v3/trust/config", "/api/v3/forgetting/config",
|
|
2058
|
+
# MCP profile metadata — reveals which tools agent clients
|
|
2059
|
+
# can invoke; consistent with other config GETs.
|
|
2060
|
+
"/api/v3/mcp/profiles")))
|
|
2061
|
+
or _p in ("/api/search", "/api/v3/recall/trace")
|
|
2062
|
+
or _p.startswith("/api/v3/recall")
|
|
2063
|
+
)
|
|
2064
|
+
if _is_sensitive_read:
|
|
2065
|
+
_resp = _rbac_read_gate(request, application.state)
|
|
2066
|
+
if _resp is not None:
|
|
2067
|
+
return _resp
|
|
1823
2068
|
return await call_next(request)
|
|
1824
2069
|
except Exception as _auth_exc:
|
|
1825
2070
|
# v3.6.12 (failopen-1): security middleware must NEVER fail open silently.
|
|
@@ -1892,6 +2137,21 @@ def _register_dashboard_routes(application: FastAPI) -> None:
|
|
|
1892
2137
|
application.include_router(v3_router)
|
|
1893
2138
|
application.include_router(adapters_router)
|
|
1894
2139
|
|
|
2140
|
+
# RBAC / teams (C3) — user & role administration + login.
|
|
2141
|
+
try:
|
|
2142
|
+
from superlocalmemory.server.routes.rbac import router as rbac_router
|
|
2143
|
+
application.include_router(rbac_router)
|
|
2144
|
+
except ImportError:
|
|
2145
|
+
logger.debug("rbac_router not available")
|
|
2146
|
+
|
|
2147
|
+
# Config endpoints (storage, daemon, mesh, trust, forgetting)
|
|
2148
|
+
from superlocalmemory.server.routes.config_api import router as config_api_router
|
|
2149
|
+
application.include_router(config_api_router)
|
|
2150
|
+
|
|
2151
|
+
# Task #47: dashboard-editable rate limits (GET/PUT /api/v3/ratelimit)
|
|
2152
|
+
from superlocalmemory.server.routes.ratelimit import router as ratelimit_router
|
|
2153
|
+
application.include_router(ratelimit_router)
|
|
2154
|
+
|
|
1895
2155
|
# v3.4.1 chat SSE
|
|
1896
2156
|
for _mod_name in ("chat",):
|
|
1897
2157
|
try:
|
|
@@ -1903,7 +2163,7 @@ def _register_dashboard_routes(application: FastAPI) -> None:
|
|
|
1903
2163
|
pass
|
|
1904
2164
|
|
|
1905
2165
|
# Optional routers
|
|
1906
|
-
for _mod_name in ("learning", "lifecycle", "behavioral", "compliance", "insights", "timeline"):
|
|
2166
|
+
for _mod_name in ("learning", "lifecycle", "behavioral", "compliance", "insights", "timeline", "abstraction"):
|
|
1907
2167
|
try:
|
|
1908
2168
|
_mod = __import__(
|
|
1909
2169
|
f"superlocalmemory.server.routes.{_mod_name}", fromlist=["router"],
|
|
@@ -2028,7 +2288,7 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
2028
2288
|
)
|
|
2029
2289
|
|
|
2030
2290
|
@application.get("/health")
|
|
2031
|
-
async def health():
|
|
2291
|
+
async def health(request: Request = None):
|
|
2032
2292
|
_update_activity()
|
|
2033
2293
|
# Non-blocking peek: report status without forcing a re-init.
|
|
2034
2294
|
engine = getattr(application.state, "engine", None)
|
|
@@ -2062,6 +2322,27 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
2062
2322
|
from superlocalmemory.server.profile_runtime import get_profile_runtime
|
|
2063
2323
|
|
|
2064
2324
|
profile_snapshot = get_profile_runtime(application.state).snapshot
|
|
2325
|
+
# H-05 (3.7.9): operational metadata (pid, daemon identity including
|
|
2326
|
+
# capability_fingerprint/instance_id, active profile, readiness detail)
|
|
2327
|
+
# is returned only to loopback callers. A remote or unauthenticated
|
|
2328
|
+
# probe receives liveness fields only, so it cannot harvest targeting
|
|
2329
|
+
# intel (version stays, since clients legitimately gate on it).
|
|
2330
|
+
public = {
|
|
2331
|
+
"status": "ok",
|
|
2332
|
+
"ready": fully_ready,
|
|
2333
|
+
# Runtime readiness is more precise than descriptor lifecycle.
|
|
2334
|
+
# A process can be alive and identity-valid while retrieval warms.
|
|
2335
|
+
"state": runtime_state,
|
|
2336
|
+
"version": getattr(application, 'version', 'unknown'),
|
|
2337
|
+
}
|
|
2338
|
+
# request is None only for direct internal/test calls (no HTTP client),
|
|
2339
|
+
# which are trusted; over HTTP FastAPI always injects the real Request.
|
|
2340
|
+
client_host = request.client.host if (request and request.client) else ""
|
|
2341
|
+
_trusted = request is None or client_host in (
|
|
2342
|
+
"127.0.0.1", "::1", "localhost", "testclient",
|
|
2343
|
+
)
|
|
2344
|
+
if not _trusted:
|
|
2345
|
+
return public
|
|
2065
2346
|
return {
|
|
2066
2347
|
"status": "ok",
|
|
2067
2348
|
"ready": fully_ready,
|
|
@@ -2076,8 +2357,9 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
2076
2357
|
# health probe; includes self-heal counters.
|
|
2077
2358
|
"recall_health": _recall_health,
|
|
2078
2359
|
**(identity.public_health_fields() if identity is not None else {}),
|
|
2079
|
-
#
|
|
2080
|
-
#
|
|
2360
|
+
# runtime_state must come AFTER the identity spread so the live
|
|
2361
|
+
# readiness state wins over the descriptor's last-known lifecycle
|
|
2362
|
+
# value (which can be "starting").
|
|
2081
2363
|
"state": runtime_state,
|
|
2082
2364
|
"active_profile": profile_snapshot.profile_id,
|
|
2083
2365
|
"profile_generation": profile_snapshot.generation,
|
|
@@ -2093,6 +2375,7 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
2093
2375
|
include_source: bool = False,
|
|
2094
2376
|
include_global: bool | None = None,
|
|
2095
2377
|
include_shared: bool | None = None,
|
|
2378
|
+
window: str = "",
|
|
2096
2379
|
):
|
|
2097
2380
|
_update_activity()
|
|
2098
2381
|
search_query = q or query # Accept both ?q= and ?query= for compatibility
|
|
@@ -2142,6 +2425,7 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
2142
2425
|
fast=fast,
|
|
2143
2426
|
include_global=include_global,
|
|
2144
2427
|
include_shared=include_shared,
|
|
2428
|
+
window=window or None,
|
|
2145
2429
|
)
|
|
2146
2430
|
# v3.4.26: return the same field shape as recall_worker so
|
|
2147
2431
|
# MCP processes proxying through the daemon get recall_trace-
|
|
@@ -2151,7 +2435,10 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
2151
2435
|
if r.fact.memory_id
|
|
2152
2436
|
})
|
|
2153
2437
|
memory_map = (
|
|
2154
|
-
engine._db.get_memory_content_batch(
|
|
2438
|
+
engine._db.get_memory_content_batch(
|
|
2439
|
+
memory_ids, engine.profile_id,
|
|
2440
|
+
include_global=True, include_shared=True,
|
|
2441
|
+
)
|
|
2155
2442
|
if memory_ids else {}
|
|
2156
2443
|
)
|
|
2157
2444
|
# v3.6.6: single shared serialization chokepoint — budget + source
|
|
@@ -2376,7 +2663,8 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
2376
2663
|
maint_result = _run_maint(engine._db, engine._config, pid)
|
|
2377
2664
|
results["langevin"] = {"updated": maint_result.get("updated", 0)}
|
|
2378
2665
|
except Exception as exc:
|
|
2379
|
-
|
|
2666
|
+
logger.exception("maintenance langevin step failed")
|
|
2667
|
+
results["langevin"] = {"error": "internal error"}
|
|
2380
2668
|
try:
|
|
2381
2669
|
from superlocalmemory.math.ebbinghaus import EbbinghausCurve
|
|
2382
2670
|
from superlocalmemory.learning.forgetting_scheduler import (
|
|
@@ -2388,7 +2676,8 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
2388
2676
|
)
|
|
2389
2677
|
results["forgetting"] = sched.run_decay_cycle(pid, force=False)
|
|
2390
2678
|
except Exception as exc:
|
|
2391
|
-
|
|
2679
|
+
logger.exception("maintenance forgetting step failed")
|
|
2680
|
+
results["forgetting"] = {"error": "internal error"}
|
|
2392
2681
|
try:
|
|
2393
2682
|
from superlocalmemory.learning.consolidation_worker import (
|
|
2394
2683
|
ConsolidationWorker,
|
|
@@ -2400,7 +2689,8 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
2400
2689
|
count = cw._generate_patterns(pid, False)
|
|
2401
2690
|
results["behavioral"] = {"patterns_mined": count}
|
|
2402
2691
|
except Exception as exc:
|
|
2403
|
-
|
|
2692
|
+
logger.exception("maintenance behavioral step failed")
|
|
2693
|
+
results["behavioral"] = {"error": "internal error"}
|
|
2404
2694
|
authorization.complete()
|
|
2405
2695
|
return {"ok": True, "profile": pid, **results}
|
|
2406
2696
|
except HTTPException:
|
|
@@ -2492,6 +2782,46 @@ def _register_daemon_routes(application: FastAPI) -> None:
|
|
|
2492
2782
|
os.kill(os.getpid(), signal.SIGTERM)
|
|
2493
2783
|
return {"status": "stopping"}
|
|
2494
2784
|
|
|
2785
|
+
@application.post("/api/daemon/restart")
|
|
2786
|
+
async def restart_daemon(request: Request):
|
|
2787
|
+
"""Restart the daemon from the dashboard (non-technical end users).
|
|
2788
|
+
|
|
2789
|
+
Spawns a DETACHED ``slm restart`` process (its own session) so it
|
|
2790
|
+
survives this daemon being stopped, then returns immediately. The child
|
|
2791
|
+
stops this daemon and starts a fresh one via the standard 5-step
|
|
2792
|
+
pipeline (namespace lock → stop → start → warmup → verify).
|
|
2793
|
+
"""
|
|
2794
|
+
# Dashboard-callable: accept the install-token principal (same auth as
|
|
2795
|
+
# /remember and other dashboard mutations), not just the private CLI
|
|
2796
|
+
# capability — a non-technical user restarts from their own dashboard.
|
|
2797
|
+
_require_write_actor(request)
|
|
2798
|
+
import subprocess
|
|
2799
|
+
import sys as _sys
|
|
2800
|
+
from superlocalmemory.core.platform_utils import popen_platform_kwargs
|
|
2801
|
+
|
|
2802
|
+
logger.info("Daemon restart requested via API")
|
|
2803
|
+
_observe_buffer.flush_sync()
|
|
2804
|
+
try:
|
|
2805
|
+
subprocess.Popen(
|
|
2806
|
+
[_sys.executable, "-m", "superlocalmemory.cli.main", "restart",
|
|
2807
|
+
"--json"],
|
|
2808
|
+
stdout=subprocess.DEVNULL,
|
|
2809
|
+
stderr=subprocess.DEVNULL,
|
|
2810
|
+
# CP-04: use the shared platform kwargs (CREATE_NO_WINDOW on
|
|
2811
|
+
# Windows, start_new_session on POSIX) like every other Popen so
|
|
2812
|
+
# a GUI-triggered restart never flashes a console window.
|
|
2813
|
+
# close_fds defaults to True on all platforms.
|
|
2814
|
+
**popen_platform_kwargs(),
|
|
2815
|
+
)
|
|
2816
|
+
except Exception:
|
|
2817
|
+
logger.exception("Failed to spawn restart process")
|
|
2818
|
+
return {"success": False, "error": "Could not initiate restart"}
|
|
2819
|
+
return {
|
|
2820
|
+
"success": True,
|
|
2821
|
+
"status": "restarting",
|
|
2822
|
+
"message": "Daemon restart initiated — it will be back in a few seconds.",
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2495
2825
|
@application.post("/session/open")
|
|
2496
2826
|
async def session_open(req: SessionOpenRequest, request: Request):
|
|
2497
2827
|
"""#49: Open a session locally — warm recall context with no model
|
|
@@ -2647,7 +2977,17 @@ def _materializer_actor_id() -> str:
|
|
|
2647
2977
|
|
|
2648
2978
|
|
|
2649
2979
|
def _run_materializer_operation(runtime, engine_supplier, operation):
|
|
2650
|
-
"""Run one bounded background unit against an admitted engine snapshot.
|
|
2980
|
+
"""Run one bounded background unit against an admitted engine snapshot.
|
|
2981
|
+
|
|
2982
|
+
Cooperative preemption: if a profile transition is already in progress,
|
|
2983
|
+
skip this materialization cycle entirely and return None. The caller's
|
|
2984
|
+
loop retries on the next iteration, by which time the switch has committed
|
|
2985
|
+
and a clean admission is available. This prevents the materializer from
|
|
2986
|
+
holding the operation lease during the transition drain window.
|
|
2987
|
+
"""
|
|
2988
|
+
# Writer-priority: don't acquire a new lease when a transition is draining.
|
|
2989
|
+
if runtime is not None and runtime.transitioning:
|
|
2990
|
+
return None
|
|
2651
2991
|
with runtime.operation():
|
|
2652
2992
|
# Resolve the engine only after admission. A concurrent mode/provider
|
|
2653
2993
|
# reconfiguration may have replaced the module-level engine while this
|
|
@@ -2803,7 +3143,11 @@ def _start_pending_materializer() -> None:
|
|
|
2803
3143
|
),
|
|
2804
3144
|
)
|
|
2805
3145
|
durable_complete, durable_failed = cycle_result or (0, 0)
|
|
2806
|
-
pending
|
|
3146
|
+
# Only backfill legacy pending items enqueued under the active
|
|
3147
|
+
# profile — never materialize another profile's queued memory
|
|
3148
|
+
# under whichever profile happens to be active now.
|
|
3149
|
+
_active_profile = runtime.snapshot.profile_id
|
|
3150
|
+
pending = get_pending(limit=50, profile_id=_active_profile)
|
|
2807
3151
|
if not pending and not durable_complete and not durable_failed:
|
|
2808
3152
|
time.sleep(1.0)
|
|
2809
3153
|
continue
|
|
@@ -2887,6 +3231,18 @@ def start_server(port: int = _DEFAULT_PORT) -> None:
|
|
|
2887
3231
|
or os.environ.get("SLM_HOST")
|
|
2888
3232
|
or "127.0.0.1"
|
|
2889
3233
|
)
|
|
3234
|
+
# M-01 / L-02 (3.7.9): binding beyond loopback exposes the write API to the
|
|
3235
|
+
# network, where the loopback trusted-actor bypass no longer protects it.
|
|
3236
|
+
# Warn loudly unless the operator has opted into credential enforcement.
|
|
3237
|
+
if bind_host not in ("127.0.0.1", "::1", "localhost") and \
|
|
3238
|
+
os.environ.get("SLM_REQUIRE_CREDENTIALS") != "1":
|
|
3239
|
+
logger.warning(
|
|
3240
|
+
"SLM daemon binding to %s (non-loopback): the write API is "
|
|
3241
|
+
"reachable from the network but SLM_REQUIRE_CREDENTIALS is not set "
|
|
3242
|
+
"and API-key auth may be off. A remote caller could write without "
|
|
3243
|
+
"credentials. Set SLM_REQUIRE_CREDENTIALS=1 and configure an API "
|
|
3244
|
+
"key before exposing this instance.", bind_host,
|
|
3245
|
+
)
|
|
2890
3246
|
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
2891
3247
|
# This handles a just-closed connection in TIME_WAIT. It is safe only
|
|
2892
3248
|
# with the active-listener probe immediately below; without that guard,
|