superlocalmemory 3.8.13 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ATTRIBUTION.md +4 -4
- package/CHANGELOG.md +113 -121
- package/README.md +65 -63
- package/docs/pi-dev-integration.md +1 -1
- package/package.json +6 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- 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 +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +11 -4
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +125 -11
- package/src/superlocalmemory/cli/daemon.py +5 -1
- package/src/superlocalmemory/cli/main.py +35 -2
- package/src/superlocalmemory/cli/ops_cmd.py +281 -0
- package/src/superlocalmemory/cli/setup_wizard.py +1 -1
- package/src/superlocalmemory/compliance/audit.py +65 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +27 -57
- package/src/superlocalmemory/compliance/gdpr.py +416 -20
- package/src/superlocalmemory/compliance/retention.py +74 -22
- package/src/superlocalmemory/compliance/scheduler.py +78 -9
- package/src/superlocalmemory/core/actor_context.py +166 -0
- package/src/superlocalmemory/core/admission.py +549 -0
- package/src/superlocalmemory/core/backend_orchestrator.py +23 -10
- package/src/superlocalmemory/core/config.py +202 -24
- package/src/superlocalmemory/core/consolidation_engine.py +13 -13
- package/src/superlocalmemory/core/context_cache.py +28 -0
- package/src/superlocalmemory/core/embeddings.py +64 -2
- package/src/superlocalmemory/core/engine.py +7 -2
- package/src/superlocalmemory/core/engine_ingestion.py +65 -3
- package/src/superlocalmemory/core/engine_wiring.py +36 -9
- package/src/superlocalmemory/core/ingest_policy.py +38 -0
- package/src/superlocalmemory/core/maintenance.py +255 -0
- package/src/superlocalmemory/core/modes.py +40 -13
- package/src/superlocalmemory/core/mutations.py +437 -44
- package/src/superlocalmemory/core/operation_policy.py +92 -0
- package/src/superlocalmemory/core/operation_policy_registry.py +542 -0
- package/src/superlocalmemory/core/operation_request.py +127 -0
- package/src/superlocalmemory/core/ops_remediation.py +542 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -0
- package/src/superlocalmemory/core/remember_runtime.py +202 -4
- package/src/superlocalmemory/core/remote_mode.py +20 -5
- package/src/superlocalmemory/core/store_pipeline.py +150 -0
- package/src/superlocalmemory/core/topic_signature.py +19 -4
- package/src/superlocalmemory/core/transactions/__init__.py +78 -0
- package/src/superlocalmemory/core/transactions/concrete_owners.py +597 -0
- package/src/superlocalmemory/core/transactions/erasure.py +825 -0
- package/src/superlocalmemory/core/transactions/manifest.py +255 -0
- package/src/superlocalmemory/core/transactions/manifest_key.py +155 -0
- package/src/superlocalmemory/core/transactions/obligations.py +272 -0
- package/src/superlocalmemory/core/transactions/owners.py +114 -0
- package/src/superlocalmemory/core/transactions/reconciler.py +285 -0
- package/src/superlocalmemory/core/transactions/service.py +330 -0
- package/src/superlocalmemory/core/worker_pool.py +33 -5
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +70 -28
- package/src/superlocalmemory/encoding/emotional.py +75 -14
- package/src/superlocalmemory/encoding/scene_builder.py +115 -13
- package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
- package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
- package/src/superlocalmemory/evolution/evolution_store.py +244 -4
- package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
- package/src/superlocalmemory/evolution/model_selection.py +18 -3
- package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
- package/src/superlocalmemory/evolution/skill_activator.py +270 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
- package/src/superlocalmemory/evolution/types.py +30 -8
- package/src/superlocalmemory/graph/cozo_backend.py +17 -9
- package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
- package/src/superlocalmemory/hooks/auto_recall.py +64 -30
- package/src/superlocalmemory/hooks/codex_assets.py +14 -1
- package/src/superlocalmemory/infra/backup.py +434 -7
- package/src/superlocalmemory/infra/process_reaper.py +18 -0
- package/src/superlocalmemory/infra/self_heal.py +401 -0
- package/src/superlocalmemory/learning/feedback.py +52 -9
- package/src/superlocalmemory/loops/engine.py +10 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/http_transport.py +30 -331
- package/src/superlocalmemory/mcp/profiles.py +5 -0
- package/src/superlocalmemory/mcp/resources.py +8 -0
- package/src/superlocalmemory/mcp/server.py +51 -4
- package/src/superlocalmemory/mcp/shared.py +19 -0
- package/src/superlocalmemory/mcp/tools_active.py +25 -4
- package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
- package/src/superlocalmemory/mcp/tools_context.py +50 -8
- package/src/superlocalmemory/mcp/tools_core.py +69 -21
- package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
- package/src/superlocalmemory/mcp/tools_learning.py +21 -10
- package/src/superlocalmemory/mcp/tools_loops.py +29 -18
- package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
- package/src/superlocalmemory/mcp/tools_ops.py +115 -0
- package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
- package/src/superlocalmemory/mcp/tools_v28.py +10 -3
- package/src/superlocalmemory/mcp/tools_v3.py +34 -14
- package/src/superlocalmemory/mcp/tools_v33.py +18 -33
- package/src/superlocalmemory/mesh/broker.py +124 -46
- package/src/superlocalmemory/mesh/broker_security.py +470 -0
- package/src/superlocalmemory/mesh/discovery.py +365 -0
- package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
- package/src/superlocalmemory/mesh/node_identity.py +97 -0
- package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
- package/src/superlocalmemory/mesh/remote_sync.py +511 -28
- package/src/superlocalmemory/mesh/state_sync.py +286 -0
- package/src/superlocalmemory/optimize/config/store.py +45 -0
- package/src/superlocalmemory/parameterization/cross_project.py +12 -0
- package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
- package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
- package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
- package/src/superlocalmemory/retrieval/ann_index.py +5 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
- package/src/superlocalmemory/retrieval/engine.py +19 -4
- package/src/superlocalmemory/retrieval/fusion.py +4 -1
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
- package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
- package/src/superlocalmemory/retrieval/reranker.py +32 -1
- package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
- package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
- package/src/superlocalmemory/retrieval/vector_store.py +214 -8
- package/src/superlocalmemory/server/api.py +5 -5
- package/src/superlocalmemory/server/egress_policy.py +258 -0
- package/src/superlocalmemory/server/rbac_enforce.py +32 -0
- package/src/superlocalmemory/server/route_mutations.py +20 -0
- package/src/superlocalmemory/server/routes/compliance.py +153 -7
- package/src/superlocalmemory/server/routes/data_io.py +43 -2
- package/src/superlocalmemory/server/routes/events.py +15 -0
- package/src/superlocalmemory/server/routes/memories.py +56 -3
- package/src/superlocalmemory/server/routes/mesh.py +82 -1
- package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
- package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
- package/src/superlocalmemory/server/routes/v3_api.py +50 -27
- package/src/superlocalmemory/server/routes/ws.py +86 -0
- package/src/superlocalmemory/server/ui.py +6 -6
- package/src/superlocalmemory/server/unified_daemon.py +942 -119
- package/src/superlocalmemory/storage/_migration_internals.py +568 -0
- package/src/superlocalmemory/storage/_schema_version.py +110 -0
- package/src/superlocalmemory/storage/database.py +329 -24
- package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
- package/src/superlocalmemory/storage/erasure_fence.py +45 -0
- package/src/superlocalmemory/storage/generation_fence.py +63 -0
- package/src/superlocalmemory/storage/migration_runner.py +140 -417
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
- package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
- package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
- package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
- package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
- package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
- package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
- package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +137 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -2
- package/src/superlocalmemory/storage/schema.py +67 -0
- package/src/superlocalmemory/storage/write_coordinator.py +125 -0
- package/src/superlocalmemory/trust/scorer.py +28 -4
- package/src/superlocalmemory/ui/index.html +14 -3
- package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
- package/src/superlocalmemory/ui/js/brain.js +6 -4
- package/src/superlocalmemory/ui/js/compliance.js +66 -12
- package/src/superlocalmemory/ui/js/dashboard.js +13 -3
- package/src/superlocalmemory/ui/js/feedback.js +8 -2
- package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
- package/src/superlocalmemory/ui/js/modal.js +272 -5
- package/src/superlocalmemory/ui/js/od-backup.js +9 -2
- package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
- package/src/superlocalmemory/ui/js/od-operations.js +154 -23
- package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
- package/src/superlocalmemory/ui/js/od-team.js +9 -2
- package/src/superlocalmemory/ui/js/optimize.js +13 -16
- package/src/superlocalmemory/ui/js/profiles.js +7 -3
- package/src/superlocalmemory/ui/js/settings.js +7 -1
- package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
- package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
- package/src/superlocalmemory/cli/post_install.py +0 -114
- package/src/superlocalmemory/core/clock_monitor.py +0 -45
- package/src/superlocalmemory/core/db_pool.py +0 -80
- package/src/superlocalmemory/core/error_catalog.py +0 -113
- package/src/superlocalmemory/core/loop_watchdog.py +0 -56
- package/src/superlocalmemory/core/priority_queue.py +0 -61
- package/src/superlocalmemory/core/pruning_engine.py +0 -216
- package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
- package/src/superlocalmemory/core/slmignore.py +0 -125
- package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
- package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
- package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
- package/src/superlocalmemory/storage/access_control.py +0 -182
|
@@ -23,6 +23,8 @@ from mcp.types import ToolAnnotations
|
|
|
23
23
|
|
|
24
24
|
logger = logging.getLogger(__name__)
|
|
25
25
|
|
|
26
|
+
from superlocalmemory.core.admission import admits
|
|
27
|
+
from superlocalmemory.core.operation_request import OperationKind
|
|
26
28
|
from superlocalmemory.infra.data_root import state_path
|
|
27
29
|
from superlocalmemory.mcp.shared import authorize_mcp_mutation
|
|
28
30
|
|
|
@@ -75,8 +77,8 @@ def register_v33_tools(server, get_engine: Callable) -> None:
|
|
|
75
77
|
# 1. forget — Ebbinghaus forgetting decay cycle
|
|
76
78
|
# ------------------------------------------------------------------
|
|
77
79
|
@server.tool(annotations=ToolAnnotations(destructiveHint=True))
|
|
80
|
+
@admits(OperationKind.FORGET)
|
|
78
81
|
async def forget(
|
|
79
|
-
profile_id: str = "",
|
|
80
82
|
dry_run: bool = True,
|
|
81
83
|
) -> dict:
|
|
82
84
|
"""Run Ebbinghaus forgetting decay cycle.
|
|
@@ -88,12 +90,11 @@ def register_v33_tools(server, get_engine: Callable) -> None:
|
|
|
88
90
|
Run with dry_run=True first to preview changes.
|
|
89
91
|
|
|
90
92
|
Args:
|
|
91
|
-
profile_id: Profile to process (default: active profile).
|
|
92
93
|
dry_run: If True, compute stats but don't apply transitions.
|
|
93
94
|
"""
|
|
94
95
|
try:
|
|
95
96
|
engine = get_engine()
|
|
96
|
-
pid =
|
|
97
|
+
pid = engine.profile_id
|
|
97
98
|
|
|
98
99
|
from superlocalmemory.math.ebbinghaus import EbbinghausCurve
|
|
99
100
|
from superlocalmemory.learning.forgetting_scheduler import (
|
|
@@ -147,8 +148,8 @@ def register_v33_tools(server, get_engine: Callable) -> None:
|
|
|
147
148
|
# 2. quantize — EAP embedding quantization cycle
|
|
148
149
|
# ------------------------------------------------------------------
|
|
149
150
|
@server.tool()
|
|
151
|
+
@admits(OperationKind.CONSOLIDATE)
|
|
150
152
|
async def quantize(
|
|
151
|
-
profile_id: str = "",
|
|
152
153
|
dry_run: bool = True,
|
|
153
154
|
) -> dict:
|
|
154
155
|
"""Run EAP quantization cycle.
|
|
@@ -160,12 +161,11 @@ def register_v33_tools(server, get_engine: Callable) -> None:
|
|
|
160
161
|
Run with dry_run=True first to preview changes.
|
|
161
162
|
|
|
162
163
|
Args:
|
|
163
|
-
profile_id: Profile to process (default: active profile).
|
|
164
164
|
dry_run: If True, compute stats but don't apply changes.
|
|
165
165
|
"""
|
|
166
166
|
try:
|
|
167
167
|
engine = get_engine()
|
|
168
|
-
pid =
|
|
168
|
+
pid = engine.profile_id
|
|
169
169
|
|
|
170
170
|
from superlocalmemory.math.ebbinghaus import EbbinghausCurve
|
|
171
171
|
from superlocalmemory.math.polar_quant import PolarQuantEncoder
|
|
@@ -218,21 +218,17 @@ def register_v33_tools(server, get_engine: Callable) -> None:
|
|
|
218
218
|
# 3. consolidate_cognitive — CCQ cognitive consolidation
|
|
219
219
|
# ------------------------------------------------------------------
|
|
220
220
|
@server.tool()
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
) -> dict:
|
|
221
|
+
@admits(OperationKind.CONSOLIDATE)
|
|
222
|
+
async def consolidate_cognitive() -> dict:
|
|
224
223
|
"""Run CCQ cognitive consolidation pipeline.
|
|
225
224
|
|
|
226
225
|
Extracts patterns from cold/archive memories by clustering
|
|
227
226
|
related facts, generating gist summaries, and compressing
|
|
228
227
|
source embeddings. Like sleep-time memory consolidation.
|
|
229
|
-
|
|
230
|
-
Args:
|
|
231
|
-
profile_id: Profile to process (default: active profile).
|
|
232
228
|
"""
|
|
233
229
|
try:
|
|
234
230
|
engine = get_engine()
|
|
235
|
-
pid =
|
|
231
|
+
pid = engine.profile_id
|
|
236
232
|
|
|
237
233
|
# v3.4.26: prefer the daemon's /consolidate/cognitive endpoint
|
|
238
234
|
# so the heavy CognitiveConsolidator import stays out of the
|
|
@@ -292,21 +288,16 @@ def register_v33_tools(server, get_engine: Callable) -> None:
|
|
|
292
288
|
# 4. get_soft_prompts — Retrieve active soft prompts
|
|
293
289
|
# ------------------------------------------------------------------
|
|
294
290
|
@server.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
295
|
-
async def get_soft_prompts(
|
|
296
|
-
profile_id: str = "",
|
|
297
|
-
) -> dict:
|
|
291
|
+
async def get_soft_prompts() -> dict:
|
|
298
292
|
"""Get active soft prompts (auto-learned user patterns).
|
|
299
293
|
|
|
300
294
|
Returns soft prompt templates generated from behavioral
|
|
301
295
|
patterns. These are injected into conversation context to
|
|
302
296
|
personalize AI responses.
|
|
303
|
-
|
|
304
|
-
Args:
|
|
305
|
-
profile_id: Profile to query (default: active profile).
|
|
306
297
|
"""
|
|
307
298
|
try:
|
|
308
299
|
engine = get_engine()
|
|
309
|
-
pid =
|
|
300
|
+
pid = engine.profile_id
|
|
310
301
|
|
|
311
302
|
rows = engine._db.execute(
|
|
312
303
|
"SELECT prompt_id, category, content, confidence, "
|
|
@@ -346,6 +337,7 @@ def register_v33_tools(server, get_engine: Callable) -> None:
|
|
|
346
337
|
# 5. reap_processes — Find and kill orphaned SLM processes
|
|
347
338
|
# ------------------------------------------------------------------
|
|
348
339
|
@server.tool()
|
|
340
|
+
@admits(OperationKind.CONSOLIDATE)
|
|
349
341
|
async def reap_processes(
|
|
350
342
|
dry_run: bool = True,
|
|
351
343
|
) -> dict:
|
|
@@ -394,22 +386,17 @@ def register_v33_tools(server, get_engine: Callable) -> None:
|
|
|
394
386
|
# ------------------------------------------------------------------
|
|
395
387
|
# 6. get_retention_stats — Memory retention zone distribution
|
|
396
388
|
# ------------------------------------------------------------------
|
|
397
|
-
@server.tool()
|
|
398
|
-
async def get_retention_stats(
|
|
399
|
-
profile_id: str = "",
|
|
400
|
-
) -> dict:
|
|
389
|
+
@server.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
390
|
+
async def get_retention_stats() -> dict:
|
|
401
391
|
"""Get memory retention statistics (zone distribution, decay rates).
|
|
402
392
|
|
|
403
393
|
Queries the fact_retention table for zone counts and average
|
|
404
394
|
retention scores per zone. Shows how memories are distributed
|
|
405
395
|
across the Ebbinghaus decay lifecycle.
|
|
406
|
-
|
|
407
|
-
Args:
|
|
408
|
-
profile_id: Profile to query (default: active profile).
|
|
409
396
|
"""
|
|
410
397
|
try:
|
|
411
398
|
engine = get_engine()
|
|
412
|
-
pid =
|
|
399
|
+
pid = engine.profile_id
|
|
413
400
|
|
|
414
401
|
# Zone distribution counts
|
|
415
402
|
rows = engine._db.execute(
|
|
@@ -451,19 +438,17 @@ def register_v33_tools(server, get_engine: Callable) -> None:
|
|
|
451
438
|
# 7. run_maintenance — V3.3.12: Combined periodic maintenance cycle
|
|
452
439
|
# ------------------------------------------------------------------
|
|
453
440
|
@server.tool()
|
|
454
|
-
|
|
441
|
+
@admits(OperationKind.CONSOLIDATE)
|
|
442
|
+
async def run_maintenance() -> dict:
|
|
455
443
|
"""Run all periodic maintenance tasks in a single call.
|
|
456
444
|
|
|
457
445
|
Combines Langevin dynamics stepping, Ebbinghaus forgetting decay,
|
|
458
446
|
and behavioral pattern mining into one convenient maintenance cycle.
|
|
459
447
|
Clients should call this periodically (e.g., at session end).
|
|
460
|
-
|
|
461
|
-
Args:
|
|
462
|
-
profile_id: Profile to maintain (default: active profile).
|
|
463
448
|
"""
|
|
464
449
|
try:
|
|
465
450
|
engine = get_engine()
|
|
466
|
-
pid =
|
|
451
|
+
pid = engine.profile_id
|
|
467
452
|
|
|
468
453
|
# v3.4.26: prefer the daemon so ForgettingScheduler /
|
|
469
454
|
# ConsolidationWorker / EbbinghausCurve don't load inside
|
|
@@ -24,6 +24,10 @@ from typing import Any, Callable, TypeVar
|
|
|
24
24
|
logger = logging.getLogger("superlocalmemory.mesh")
|
|
25
25
|
import os as _os
|
|
26
26
|
|
|
27
|
+
from .broker_security import ( # noqa: E501
|
|
28
|
+
apply_security_schema, check_cross_profile_sender, ensure_db_healthy, get_or_create_peer_key, reject_secret_state, scrub_message_content, seed_fencing_counter, _set_nonce_db_path, validate_lock_fence_query, # noqa: E501
|
|
29
|
+
)
|
|
30
|
+
|
|
27
31
|
# Remote sync support (optional, try/except to avoid import issues)
|
|
28
32
|
try:
|
|
29
33
|
from .remote_sync import RemoteSyncClient
|
|
@@ -32,8 +36,6 @@ except ImportError:
|
|
|
32
36
|
|
|
33
37
|
LOCAL_HOSTS = frozenset({"127.0.0.1", "localhost", "::1"})
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
|
|
37
39
|
MAX_MESSAGE_SIZE = 4096 # 4KB cap — mesh messages are notifications, not data dumps
|
|
38
40
|
MESSAGE_TTL_HOURS = 48 # Offline messages expire after 48h
|
|
39
41
|
LOCK_TTL_HOURS = 8 # M-02: file locks auto-expire so a crashed session can't deadlock a path
|
|
@@ -50,13 +52,7 @@ _WRITE_BUSY_TIMEOUT_MS = 2000
|
|
|
50
52
|
|
|
51
53
|
|
|
52
54
|
class MeshBroker:
|
|
53
|
-
"""Lightweight mesh broker
|
|
54
|
-
|
|
55
|
-
Provides peer management, messaging, state, locks, and events.
|
|
56
|
-
v3.4.6: broadcast, project-based routing, offline message queue.
|
|
57
|
-
All methods are synchronous (called from FastAPI via run_in_executor
|
|
58
|
-
or directly for quick operations).
|
|
59
|
-
"""
|
|
55
|
+
"""Lightweight mesh broker — peer lifecycle, messaging, state, locks, events."""
|
|
60
56
|
|
|
61
57
|
def __init__(self, db_path: str | Path):
|
|
62
58
|
self._db_path = str(db_path)
|
|
@@ -72,11 +68,17 @@ class MeshBroker:
|
|
|
72
68
|
self._remote_peers_lock = threading.RLock()
|
|
73
69
|
self._peer_url: str | None = _os.environ.get("SLM_MESH_PEER_URL", "") or None
|
|
74
70
|
self._sync_client: Any = None
|
|
71
|
+
self._degraded: bool = False
|
|
72
|
+
self._fencing_lock = threading.Lock()
|
|
73
|
+
self._fencing_counter: int = 0 # seeded below after schema is applied
|
|
75
74
|
if self._is_remote and not self._shared_secret:
|
|
76
75
|
raise RuntimeError(
|
|
77
76
|
"SLM_MESH_SHARED_SECRET is required when SLM_MESH_HOST is not localhost"
|
|
78
77
|
)
|
|
79
|
-
|
|
78
|
+
self._ensure_db_healthy()
|
|
79
|
+
self._apply_schema_updates()
|
|
80
|
+
_set_nonce_db_path(self._db_path) # SEC-3: wire SQLite nonce store for durability
|
|
81
|
+
self._fencing_counter = seed_fencing_counter(self._db_path) # 3a-3: restart-safe fence
|
|
80
82
|
|
|
81
83
|
# -- Remote / Multi-Machine support (v3.4.47) --
|
|
82
84
|
|
|
@@ -128,13 +130,31 @@ class MeshBroker:
|
|
|
128
130
|
if self._sync_client:
|
|
129
131
|
self._sync_client.stop()
|
|
130
132
|
|
|
133
|
+
# -- Startup safety helpers --
|
|
134
|
+
|
|
135
|
+
def _ensure_db_healthy(self) -> None:
|
|
136
|
+
"""Quarantine a corrupt DB; set _degraded if recovery was needed."""
|
|
137
|
+
self._degraded = ensure_db_healthy(self._db_path)
|
|
138
|
+
|
|
139
|
+
def _apply_schema_updates(self) -> None:
|
|
140
|
+
"""Apply idempotent schema additions (delegated to broker_security)."""
|
|
141
|
+
conn = self._conn()
|
|
142
|
+
try:
|
|
143
|
+
apply_security_schema(conn)
|
|
144
|
+
except sqlite3.Error as exc:
|
|
145
|
+
logger.debug("mesh schema update skipped: %s", exc)
|
|
146
|
+
finally:
|
|
147
|
+
conn.close()
|
|
148
|
+
|
|
149
|
+
def _next_fencing_token(self) -> int:
|
|
150
|
+
"""Return the next monotonically-increasing fencing token."""
|
|
151
|
+
with self._fencing_lock:
|
|
152
|
+
self._fencing_counter += 1
|
|
153
|
+
return self._fencing_counter
|
|
154
|
+
|
|
131
155
|
# -- Connection helper --
|
|
132
156
|
|
|
133
157
|
def _conn(self) -> sqlite3.Connection:
|
|
134
|
-
# WAL is configured during database initialization and persists with the
|
|
135
|
-
# database. Reissuing journal_mode=WAL for every short-lived mesh
|
|
136
|
-
# connection is itself a schema-level write that can contend with the
|
|
137
|
-
# daemon. Mesh writes below use bounded whole-transaction retries.
|
|
138
158
|
conn = sqlite3.connect(
|
|
139
159
|
self._db_path,
|
|
140
160
|
timeout=_WRITE_BUSY_TIMEOUT_MS / 1000,
|
|
@@ -148,17 +168,8 @@ class MeshBroker:
|
|
|
148
168
|
message = str(exc).lower()
|
|
149
169
|
return "database is locked" in message or "database is busy" in message
|
|
150
170
|
|
|
151
|
-
def _write_with_retry(
|
|
152
|
-
|
|
153
|
-
operation: Callable[[sqlite3.Connection], _T],
|
|
154
|
-
) -> _T:
|
|
155
|
-
"""Run one idempotent mesh mutation with a bounded SQLite retry budget.
|
|
156
|
-
|
|
157
|
-
SQLite WAL lets reads continue while a write is in progress, but it
|
|
158
|
-
still permits a single writer. Retrying the entire short transaction on
|
|
159
|
-
a fresh connection avoids leaking a transient writer collision to an
|
|
160
|
-
agent heartbeat or mesh command.
|
|
161
|
-
"""
|
|
171
|
+
def _write_with_retry(self, operation: Callable[[sqlite3.Connection], _T]) -> _T:
|
|
172
|
+
"""Run one idempotent mesh mutation with a bounded SQLite retry budget."""
|
|
162
173
|
last_error: sqlite3.OperationalError | None = None
|
|
163
174
|
for attempt in range(_WRITE_RETRY_ATTEMPTS):
|
|
164
175
|
conn = self._conn()
|
|
@@ -199,6 +210,7 @@ class MeshBroker:
|
|
|
199
210
|
).fetchone()
|
|
200
211
|
if existing:
|
|
201
212
|
peer_id = existing["peer_id"]
|
|
213
|
+
is_new_peer = False
|
|
202
214
|
conn.execute(
|
|
203
215
|
"UPDATE mesh_peers SET summary=?, host=?, port=?, last_heartbeat=?, "
|
|
204
216
|
"status='active', project_path=?, agent_type=? "
|
|
@@ -208,6 +220,7 @@ class MeshBroker:
|
|
|
208
220
|
)
|
|
209
221
|
else:
|
|
210
222
|
peer_id = str(uuid.uuid4())[:12]
|
|
223
|
+
is_new_peer = True
|
|
211
224
|
conn.execute(
|
|
212
225
|
"INSERT INTO mesh_peers (peer_id, session_id, summary, status, host, port, "
|
|
213
226
|
"registered_at, last_heartbeat, project_path, agent_type, profile_id) "
|
|
@@ -220,9 +233,17 @@ class MeshBroker:
|
|
|
220
233
|
}, profile_id=profile_id)
|
|
221
234
|
conn.commit()
|
|
222
235
|
|
|
236
|
+
# SEC-4 + P0: mint & RETURN the per-peer HMAC key ONLY on first
|
|
237
|
+
# registration. Re-registration MUST NOT re-export the key — otherwise
|
|
238
|
+
# a fleet-secret holder who learns a session_id (via /mesh/peers) could
|
|
239
|
+
# re-register as the victim and steal their peer_key (identity theft).
|
|
240
|
+
# The legitimate owner keeps the key it received at first registration.
|
|
223
241
|
# v3.4.6: Deliver pending broadcast/project messages on registration
|
|
224
242
|
pending = self._get_pending_for_peer(conn, peer_id, project_path, profile_id)
|
|
225
|
-
|
|
243
|
+
result = {"peer_id": peer_id, "ok": True, "pending_messages": len(pending)}
|
|
244
|
+
if is_new_peer:
|
|
245
|
+
result["peer_key"] = get_or_create_peer_key(conn, peer_id, profile_id)
|
|
246
|
+
return result
|
|
226
247
|
|
|
227
248
|
return self._write_with_retry(_register)
|
|
228
249
|
|
|
@@ -290,7 +311,8 @@ class MeshBroker:
|
|
|
290
311
|
|
|
291
312
|
def send_message(self, from_peer: str, to_peer: str, content: str,
|
|
292
313
|
msg_type: str = "text", project_path: str = "",
|
|
293
|
-
profile_id: str = "default"
|
|
314
|
+
profile_id: str = "default",
|
|
315
|
+
operation_id: str | None = None) -> dict:
|
|
294
316
|
# Guard: 4KB message size cap
|
|
295
317
|
if len(content) > MAX_MESSAGE_SIZE:
|
|
296
318
|
return {"ok": False, "error": f"message too large ({len(content)} bytes, max {MAX_MESSAGE_SIZE}). "
|
|
@@ -326,6 +348,20 @@ class MeshBroker:
|
|
|
326
348
|
now = datetime.now(timezone.utc).isoformat()
|
|
327
349
|
expires_at = self._compute_expires(now)
|
|
328
350
|
|
|
351
|
+
# Idempotency: return original result for a repeated operation_id.
|
|
352
|
+
if operation_id:
|
|
353
|
+
op = conn.execute(
|
|
354
|
+
"SELECT message_id FROM mesh_sent_ops WHERE operation_id=?", (operation_id,)
|
|
355
|
+
).fetchone()
|
|
356
|
+
if op:
|
|
357
|
+
return {"ok": True, "id": op["message_id"],
|
|
358
|
+
"idempotent": True, "operation_id": operation_id}
|
|
359
|
+
|
|
360
|
+
# Identity binding: cross-profile impersonation guard.
|
|
361
|
+
cross_profile_err = check_cross_profile_sender(conn, from_peer, profile_id)
|
|
362
|
+
if cross_profile_err is not None:
|
|
363
|
+
return cross_profile_err
|
|
364
|
+
|
|
329
365
|
# Determine target type
|
|
330
366
|
if _to_peer == "broadcast":
|
|
331
367
|
target_type = "broadcast"
|
|
@@ -361,18 +397,24 @@ class MeshBroker:
|
|
|
361
397
|
count - MAX_QUEUED_PER_TARGET + 1),
|
|
362
398
|
)
|
|
363
399
|
|
|
400
|
+
_content = scrub_message_content(content) # 3a-2: redact before storage
|
|
364
401
|
cursor = conn.execute(
|
|
365
402
|
"INSERT INTO mesh_messages (from_peer, to_peer, msg_type, content, read, "
|
|
366
403
|
"created_at, expires_at, target_type, project_path, profile_id) "
|
|
367
404
|
"VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ?)",
|
|
368
|
-
(from_peer, _to_peer, msg_type,
|
|
405
|
+
(from_peer, _to_peer, msg_type, _content, now, expires_at,
|
|
369
406
|
target_type, _project_path, profile_id),
|
|
370
407
|
)
|
|
371
|
-
|
|
408
|
+
msg_id = cursor.lastrowid
|
|
409
|
+
|
|
410
|
+
if operation_id:
|
|
411
|
+
conn.execute("INSERT OR IGNORE INTO mesh_sent_ops (operation_id, message_id, created_at) VALUES (?, ?, ?)", (operation_id, msg_id, now))
|
|
412
|
+
|
|
413
|
+
self._log_event(conn, "message_sent", from_peer or "system", {
|
|
372
414
|
"to": _to_peer, "target_type": target_type, "project": _project_path,
|
|
373
415
|
}, profile_id=profile_id)
|
|
374
416
|
conn.commit()
|
|
375
|
-
return {"ok": True, "id":
|
|
417
|
+
return {"ok": True, "id": msg_id, "target_type": target_type,
|
|
376
418
|
"expires_at": expires_at}
|
|
377
419
|
|
|
378
420
|
return self._write_with_retry(_send)
|
|
@@ -487,14 +529,36 @@ class MeshBroker:
|
|
|
487
529
|
conn.close()
|
|
488
530
|
|
|
489
531
|
def set_state(self, key: str, value: str, set_by: str,
|
|
490
|
-
profile_id: str = "default"
|
|
532
|
+
profile_id: str = "default",
|
|
533
|
+
expected_revision: int | None = None) -> dict:
|
|
534
|
+
"""Set coordination state; rejects secrets and enforces optimistic revision."""
|
|
535
|
+
secret_err = reject_secret_state(key, value)
|
|
536
|
+
if secret_err is not None:
|
|
537
|
+
return secret_err
|
|
538
|
+
|
|
491
539
|
def _set_state(conn: sqlite3.Connection) -> dict:
|
|
492
540
|
now = datetime.now(timezone.utc).isoformat()
|
|
541
|
+
if expected_revision is not None:
|
|
542
|
+
row = conn.execute(
|
|
543
|
+
"SELECT revision FROM mesh_state WHERE profile_id=? AND key=?",
|
|
544
|
+
(profile_id, key),
|
|
545
|
+
).fetchone()
|
|
546
|
+
current = row["revision"] if row else 0
|
|
547
|
+
if current != expected_revision:
|
|
548
|
+
return {"ok": False,
|
|
549
|
+
"error": f"revision mismatch: expected {expected_revision}, current {current}"}
|
|
493
550
|
conn.execute(
|
|
494
|
-
"INSERT INTO mesh_state
|
|
495
|
-
"
|
|
496
|
-
"
|
|
497
|
-
"
|
|
551
|
+
"INSERT INTO mesh_state"
|
|
552
|
+
" (profile_id, key, value, set_by, updated_at, revision, origin_node)"
|
|
553
|
+
" VALUES (?, ?, ?, ?, ?, 1, '')"
|
|
554
|
+
" ON CONFLICT(profile_id, key) DO UPDATE SET value=excluded.value,"
|
|
555
|
+
" set_by=excluded.set_by, updated_at=excluded.updated_at,"
|
|
556
|
+
" revision=COALESCE(mesh_state.revision, 0) + 1,"
|
|
557
|
+
# 3c-1: a local write resets provenance to THIS node ('' = local).
|
|
558
|
+
# Without this, a local set_state after a remote merge would keep
|
|
559
|
+
# the remote's origin_node and re-export under the wrong identity
|
|
560
|
+
# → permanent same-(revision,node) divergence (audit P0-1).
|
|
561
|
+
" origin_node=''",
|
|
498
562
|
(profile_id, key, value, set_by, now),
|
|
499
563
|
)
|
|
500
564
|
conn.commit()
|
|
@@ -506,8 +570,9 @@ class MeshBroker:
|
|
|
506
570
|
conn = self._conn()
|
|
507
571
|
try:
|
|
508
572
|
row = conn.execute(
|
|
509
|
-
"SELECT key, value, set_by, updated_at
|
|
510
|
-
"
|
|
573
|
+
"SELECT key, value, set_by, updated_at, "
|
|
574
|
+
"COALESCE(revision, 0) AS revision "
|
|
575
|
+
"FROM mesh_state WHERE profile_id=? AND key=?",
|
|
511
576
|
(profile_id, key),
|
|
512
577
|
).fetchone()
|
|
513
578
|
return dict(row) if row else None
|
|
@@ -522,12 +587,13 @@ class MeshBroker:
|
|
|
522
587
|
conn = self._conn()
|
|
523
588
|
try:
|
|
524
589
|
row = conn.execute(
|
|
525
|
-
"SELECT locked_by, locked_at
|
|
526
|
-
"WHERE profile_id=? AND file_path=?",
|
|
590
|
+
"SELECT locked_by, locked_at, COALESCE(fencing_token, 0) AS fencing_token"
|
|
591
|
+
" FROM mesh_locks WHERE profile_id=? AND file_path=?",
|
|
527
592
|
(profile_id, file_path),
|
|
528
593
|
).fetchone()
|
|
529
594
|
if row:
|
|
530
|
-
return {"locked": True, "by": row["locked_by"],
|
|
595
|
+
return {"locked": True, "by": row["locked_by"],
|
|
596
|
+
"since": row["locked_at"], "fencing_token": row["fencing_token"]}
|
|
531
597
|
return {"locked": False}
|
|
532
598
|
finally:
|
|
533
599
|
conn.close()
|
|
@@ -557,15 +623,18 @@ class MeshBroker:
|
|
|
557
623
|
lock_expires = (
|
|
558
624
|
datetime.fromisoformat(now) + timedelta(hours=LOCK_TTL_HOURS)
|
|
559
625
|
).isoformat()
|
|
626
|
+
token = self._next_fencing_token()
|
|
560
627
|
conn.execute(
|
|
561
|
-
"INSERT INTO mesh_locks (profile_id, file_path, locked_by, locked_at,
|
|
562
|
-
"VALUES (?, ?, ?, ?, ?)
|
|
563
|
-
"ON CONFLICT(profile_id, file_path) DO UPDATE SET locked_by=excluded.locked_by,
|
|
564
|
-
"locked_at=excluded.locked_at, expires_at=excluded.expires_at"
|
|
565
|
-
|
|
628
|
+
"INSERT INTO mesh_locks (profile_id, file_path, locked_by, locked_at,"
|
|
629
|
+
" expires_at, fencing_token) VALUES (?, ?, ?, ?, ?, ?)"
|
|
630
|
+
" ON CONFLICT(profile_id, file_path) DO UPDATE SET locked_by=excluded.locked_by,"
|
|
631
|
+
" locked_at=excluded.locked_at, expires_at=excluded.expires_at,"
|
|
632
|
+
" fencing_token=excluded.fencing_token",
|
|
633
|
+
(profile_id, file_path, locked_by, now, lock_expires, token),
|
|
566
634
|
)
|
|
567
635
|
conn.commit()
|
|
568
|
-
return {"ok": True, "action": "acquired",
|
|
636
|
+
return {"ok": True, "action": "acquired",
|
|
637
|
+
"expires_at": lock_expires, "fencing_token": token}
|
|
569
638
|
|
|
570
639
|
elif action == "release":
|
|
571
640
|
# v3.6.12 (mesh-2): report whether we actually released. The
|
|
@@ -585,6 +654,15 @@ class MeshBroker:
|
|
|
585
654
|
|
|
586
655
|
return self._write_with_retry(_lock_action)
|
|
587
656
|
|
|
657
|
+
def validate_lock_fence(self, file_path: str, fencing_token: int,
|
|
658
|
+
profile_id: str = "default") -> dict:
|
|
659
|
+
"""Reject a stale fencing token; allow the current one."""
|
|
660
|
+
conn = self._conn()
|
|
661
|
+
try:
|
|
662
|
+
return validate_lock_fence_query(conn, file_path, fencing_token, profile_id)
|
|
663
|
+
finally:
|
|
664
|
+
conn.close()
|
|
665
|
+
|
|
588
666
|
# -- Helpers (v3.4.6) --
|
|
589
667
|
|
|
590
668
|
@staticmethod
|