memorymaster 3.18.0__tar.gz → 3.21.0__tar.gz

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.
Files changed (389) hide show
  1. {memorymaster-3.18.0/memorymaster.egg-info → memorymaster-3.21.0}/PKG-INFO +6 -3
  2. {memorymaster-3.18.0 → memorymaster-3.21.0}/README.md +5 -2
  3. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/__init__.py +1 -1
  4. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/cli.py +24 -1
  5. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/cli_handlers_basic.py +92 -0
  6. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/cli_handlers_curation.py +26 -0
  7. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config_templates/hooks/memorymaster-auto-ingest.py +24 -0
  8. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/dashboard.py +47 -0
  9. memorymaster-3.21.0/memorymaster/dashboard_auth.py +196 -0
  10. memorymaster-3.21.0/memorymaster/delta_sync.py +172 -0
  11. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/daydream_ingest.py +45 -1
  12. memorymaster-3.21.0/memorymaster/llm_budget.py +190 -0
  13. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/llm_provider.py +26 -0
  14. memorymaster-3.21.0/memorymaster/mcp_path_policy.py +146 -0
  15. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/mcp_server.py +65 -5
  16. memorymaster-3.21.0/memorymaster/migrations/0001_initial.py +25 -0
  17. memorymaster-3.21.0/memorymaster/migrations/0002_miner_state.py +38 -0
  18. memorymaster-3.21.0/memorymaster/migrations/__init__.py +41 -0
  19. memorymaster-3.21.0/memorymaster/migrations/runner.py +268 -0
  20. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/postgres_store.py +9 -0
  21. memorymaster-3.21.0/memorymaster/rule_miner.py +443 -0
  22. memorymaster-3.21.0/memorymaster/rules.py +104 -0
  23. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/service.py +111 -46
  24. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/storage.py +10 -0
  25. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/verbatim_store.py +41 -9
  26. memorymaster-3.21.0/memorymaster/webhook.py +161 -0
  27. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/wiki_engine.py +41 -1
  28. {memorymaster-3.18.0 → memorymaster-3.21.0/memorymaster.egg-info}/PKG-INFO +6 -3
  29. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster.egg-info/SOURCES.txt +19 -0
  30. {memorymaster-3.18.0 → memorymaster-3.21.0}/pyproject.toml +1 -1
  31. memorymaster-3.21.0/tests/conftest.py +103 -0
  32. memorymaster-3.21.0/tests/test_backend_parity.py +167 -0
  33. memorymaster-3.21.0/tests/test_dashboard_auth.py +290 -0
  34. memorymaster-3.21.0/tests/test_delta_sync.py +261 -0
  35. memorymaster-3.21.0/tests/test_llm_budget.py +232 -0
  36. memorymaster-3.21.0/tests/test_mcp_path_policy.py +206 -0
  37. memorymaster-3.21.0/tests/test_migrations.py +292 -0
  38. memorymaster-3.21.0/tests/test_rule_claims.py +181 -0
  39. memorymaster-3.21.0/tests/test_rule_miner.py +297 -0
  40. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_verbatim_dedup.py +36 -0
  41. memorymaster-3.21.0/tests/test_webhook_hmac.py +216 -0
  42. memorymaster-3.18.0/memorymaster/webhook.py +0 -58
  43. memorymaster-3.18.0/tests/conftest.py +0 -37
  44. {memorymaster-3.18.0 → memorymaster-3.21.0}/LICENSE +0 -0
  45. {memorymaster-3.18.0 → memorymaster-3.21.0}/artifacts/bm25-per-field-eval-harness.py +0 -0
  46. {memorymaster-3.18.0 → memorymaster-3.21.0}/benchmarks/longmemeval_runner.py +0 -0
  47. {memorymaster-3.18.0 → memorymaster-3.21.0}/benchmarks/longmemeval_vector_runner.py +0 -0
  48. {memorymaster-3.18.0 → memorymaster-3.21.0}/benchmarks/perf_smoke.py +0 -0
  49. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/__main__.py +0 -0
  50. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/_storage_lifecycle.py +0 -0
  51. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/_storage_read.py +0 -0
  52. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/_storage_schema.py +0 -0
  53. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/_storage_shared.py +0 -0
  54. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/_storage_sources.py +0 -0
  55. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/_storage_write_claims.py +0 -0
  56. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/access_control.py +0 -0
  57. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/action_exporters.py +0 -0
  58. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/action_extractor.py +0 -0
  59. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/atlas_claim_extractor.py +0 -0
  60. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/atlas_contract.py +0 -0
  61. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/auto_extractor.py +0 -0
  62. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/auto_resolver.py +0 -0
  63. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/candidate_dedupe.py +0 -0
  64. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/claim_edges.py +0 -0
  65. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/claim_verifier.py +0 -0
  66. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/cli_helpers.py +0 -0
  67. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/closets.py +0 -0
  68. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config.py +0 -0
  69. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config_templates/claude-md-append.md +0 -0
  70. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config_templates/codex-agents-md-append.md +0 -0
  71. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config_templates/hooks/memorymaster-classify.py +0 -0
  72. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config_templates/hooks/memorymaster-dream-sync.py +0 -0
  73. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config_templates/hooks/memorymaster-precompact.py +0 -0
  74. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config_templates/hooks/memorymaster-recall.py +0 -0
  75. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config_templates/hooks/memorymaster-session-start.py +0 -0
  76. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config_templates/hooks/memorymaster-steward-cycle.py +0 -0
  77. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config_templates/hooks/memorymaster-validate-wiki.py +0 -0
  78. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/conflict_resolver.py +0 -0
  79. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/connectors/__init__.py +0 -0
  80. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/connectors/whatsapp.py +0 -0
  81. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/context_hook.py +0 -0
  82. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/context_optimizer.py +0 -0
  83. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/daily_notes.py +0 -0
  84. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/db_merge.py +0 -0
  85. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/dream_bridge.py +0 -0
  86. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/embeddings.py +0 -0
  87. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/entity_extractor.py +0 -0
  88. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/entity_graph.py +0 -0
  89. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/entity_registry.py +0 -0
  90. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/federated_graphify.py +0 -0
  91. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/feedback.py +0 -0
  92. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/graph_store.py +0 -0
  93. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/hook_log.py +0 -0
  94. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/__init__.py +0 -0
  95. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/calibration.py +0 -0
  96. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/compact_summaries.py +0 -0
  97. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/compactor.py +0 -0
  98. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/decay.py +0 -0
  99. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/dedup.py +0 -0
  100. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/deterministic.py +0 -0
  101. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/entity_graph_export.py +0 -0
  102. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/extractor.py +0 -0
  103. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/staleness.py +0 -0
  104. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/validator.py +0 -0
  105. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/key_rotator.py +0 -0
  106. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/lifecycle.py +0 -0
  107. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/llm_rerank.py +0 -0
  108. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/llm_steward.py +0 -0
  109. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/mcp_usage.py +0 -0
  110. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/media_processing.py +0 -0
  111. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/media_providers.py +0 -0
  112. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/metrics_exporter.py +0 -0
  113. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/models.py +0 -0
  114. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/observability.py +0 -0
  115. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/operator.py +0 -0
  116. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/operator_queue.py +0 -0
  117. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/plugins.py +0 -0
  118. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/policy.py +0 -0
  119. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/qdrant_backend.py +0 -0
  120. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/qdrant_recall_fallback.py +0 -0
  121. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/qmd_bridge.py +0 -0
  122. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/query_classifier.py +0 -0
  123. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/query_expansion.py +0 -0
  124. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/recall_fusion.py +0 -0
  125. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/recall_tokenizer.py +0 -0
  126. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/retrieval.py +0 -0
  127. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/retry.py +0 -0
  128. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/review.py +0 -0
  129. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/rl_trainer.py +0 -0
  130. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/scheduler.py +0 -0
  131. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/schema.py +0 -0
  132. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/schema.sql +0 -0
  133. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/schema_postgres.sql +0 -0
  134. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/scope_utils.py +0 -0
  135. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/security.py +0 -0
  136. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/session_tracker.py +0 -0
  137. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/setup_hooks.py +0 -0
  138. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/skill_evolver.py +0 -0
  139. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/snapshot.py +0 -0
  140. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/steward.py +0 -0
  141. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/steward_classifier.py +0 -0
  142. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/steward_features.py +0 -0
  143. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/store_factory.py +0 -0
  144. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/transcript_miner.py +0 -0
  145. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/turn_schema.py +0 -0
  146. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/vault_bases.py +0 -0
  147. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/vault_curator.py +0 -0
  148. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/vault_exporter.py +0 -0
  149. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/vault_linter.py +0 -0
  150. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/vault_log.py +0 -0
  151. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/vault_query_capture.py +0 -0
  152. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/vault_synthesis.py +0 -0
  153. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/verbatim_recall.py +0 -0
  154. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/wiki_freshness.py +0 -0
  155. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/wiki_similarity.py +0 -0
  156. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/wiki_suggest.py +0 -0
  157. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/wiki_validate.py +0 -0
  158. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster.egg-info/dependency_links.txt +0 -0
  159. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster.egg-info/entry_points.txt +0 -0
  160. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster.egg-info/requires.txt +0 -0
  161. {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster.egg-info/top_level.txt +0 -0
  162. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/agg_recall_latency.py +0 -0
  163. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/alert_operator_metrics.py +0 -0
  164. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/audit_dedupe_precision.py +0 -0
  165. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/autoresearch_daemon.py +0 -0
  166. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/backfill_entity_extraction.py +0 -0
  167. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/backfill_graph_store.py +0 -0
  168. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/backfill_stop_hook_citations.py +0 -0
  169. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/backtest_steward_classifier.py +0 -0
  170. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/build_steward_training_set.py +0 -0
  171. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/check_hook_template_drift.py +0 -0
  172. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/claude_to_turns.py +0 -0
  173. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/codex_live_to_turns.py +0 -0
  174. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/compaction_edge_cases.py +0 -0
  175. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/compaction_trace_report.py +0 -0
  176. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/compaction_trace_validate.py +0 -0
  177. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/confusion_matrix_eval.py +0 -0
  178. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/conversation_importer.py +0 -0
  179. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/conversation_to_turns.py +0 -0
  180. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/e2e_operator.py +0 -0
  181. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/email_live_to_turns.py +0 -0
  182. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/eval_bm25_sweep.py +0 -0
  183. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/eval_classify_f1.py +0 -0
  184. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/eval_memorymaster.py +0 -0
  185. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/eval_recall_precision_at_5.py +0 -0
  186. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/eval_recall_quality.py +0 -0
  187. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/eval_steward_pareto.py +0 -0
  188. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/eval_verbatim_recall.py +0 -0
  189. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/expand_recall_eval.py +0 -0
  190. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/generate_drill_signoff.py +0 -0
  191. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/git_to_turns.py +0 -0
  192. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/github_live_to_turns.py +0 -0
  193. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/gitnexus_to_claims.py +0 -0
  194. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/grid_recall_weights.py +0 -0
  195. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/index_claims_to_qdrant.py +0 -0
  196. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/ingest_planning_docs.py +0 -0
  197. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/jira_live_to_turns.py +0 -0
  198. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/label_prompts_with_judge.py +0 -0
  199. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/llm_benchmark.py +0 -0
  200. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/measure_dedupe_thresholds.py +0 -0
  201. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/merge_scope_variants.py +0 -0
  202. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/messages_to_turns.py +0 -0
  203. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/operator_metrics.py +0 -0
  204. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/precompute_candidates.py +0 -0
  205. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/recurring_incident_drill.py +0 -0
  206. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/release_readiness.py +0 -0
  207. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/run_codex_autologger.py +0 -0
  208. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/run_incident_drill.py +0 -0
  209. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/scheduled_ingest.py +0 -0
  210. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/setup-hooks.py +0 -0
  211. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/slack_live_to_turns.py +0 -0
  212. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/sync_hook_templates.py +0 -0
  213. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/tickets_to_turns.py +0 -0
  214. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/train_steward_classifier.py +0 -0
  215. {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/webhook_to_turns.py +0 -0
  216. {memorymaster-3.18.0 → memorymaster-3.21.0}/setup.cfg +0 -0
  217. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/bench_longmemeval.py +0 -0
  218. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/integration/test_extract_llm_ollama_live.py +0 -0
  219. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_access_control.py +0 -0
  220. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_action_exporters.py +0 -0
  221. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_action_extractor.py +0 -0
  222. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_atlas_claim_extractor.py +0 -0
  223. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_atlas_contract.py +0 -0
  224. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_atlas_source_schema.py +0 -0
  225. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_auto_extractor.py +0 -0
  226. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_auto_ingest_hook_citations.py +0 -0
  227. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_auto_ingest_hook_schema.py +0 -0
  228. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_auto_resolver.py +0 -0
  229. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_auto_validate.py +0 -0
  230. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_bm25_per_field.py +0 -0
  231. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_calibration.py +0 -0
  232. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_calibration_priors_applied.py +0 -0
  233. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_candidate_dedupe.py +0 -0
  234. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_claim_edges.py +0 -0
  235. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_claim_links.py +0 -0
  236. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_claim_type_ranking.py +0 -0
  237. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_classify_hook_f1.py +0 -0
  238. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_classify_hook_latency.py +0 -0
  239. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_claude_to_turns.py +0 -0
  240. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_cli_dry_run.py +0 -0
  241. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_cli_json_flag.py +0 -0
  242. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_cli_ready.py +0 -0
  243. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_cli_review_queue.py +0 -0
  244. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_cli_subcommands.py +0 -0
  245. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_closets.py +0 -0
  246. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_closets_recall_integration.py +0 -0
  247. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_compact_summaries.py +0 -0
  248. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_compact_summaries_sensitivity.py +0 -0
  249. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_compaction_trace.py +0 -0
  250. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_compactor_artifact_order.py +0 -0
  251. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_config.py +0 -0
  252. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_conflict_resolver.py +0 -0
  253. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_confusion_matrix_eval.py +0 -0
  254. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_connection_retry.py +0 -0
  255. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_connectors.py +0 -0
  256. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_context_hook.py +0 -0
  257. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_context_optimizer.py +0 -0
  258. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_context_optimizer_provider.py +0 -0
  259. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_conversation_to_turns.py +0 -0
  260. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_dashboard.py +0 -0
  261. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_dashboard_coverage.py +0 -0
  262. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_dashboard_latency.py +0 -0
  263. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_dashboard_lineage.py +0 -0
  264. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_dashboard_review_queue.py +0 -0
  265. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_daydream_ingest.py +0 -0
  266. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_db_merge_confidence_conflict.py +0 -0
  267. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_db_merge_coverage_v2.py +0 -0
  268. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_decay_coverage.py +0 -0
  269. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_decay_respects_pinned.py +0 -0
  270. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_dedup.py +0 -0
  271. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_dedup_cli.py +0 -0
  272. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_dedup_conflict_disambiguation.py +0 -0
  273. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_deterministic_predicates.py +0 -0
  274. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_dream_bridge_coverage_v2.py +0 -0
  275. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_dream_bridge_sensitivity.py +0 -0
  276. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_embeddings_coverage.py +0 -0
  277. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_entity_extractor.py +0 -0
  278. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_entity_extractor_llm.py +0 -0
  279. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_entity_graph.py +0 -0
  280. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_entity_graph_export.py +0 -0
  281. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_entity_new_kinds.py +0 -0
  282. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_entity_regex_v3.py +0 -0
  283. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_entity_registry.py +0 -0
  284. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_eval_harness.py +0 -0
  285. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_events_schema.py +0 -0
  286. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_extract_llm_ollama.py +0 -0
  287. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_federated_graphify_mcp.py +0 -0
  288. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_federated_query_safety.py +0 -0
  289. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_feedback.py +0 -0
  290. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_fts5_search.py +0 -0
  291. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_graph_distance.py +0 -0
  292. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_graph_store.py +0 -0
  293. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_handler_regressions.py +0 -0
  294. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_hook_env_isolation.py +0 -0
  295. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_human_id.py +0 -0
  296. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_incident_drill_runner.py +0 -0
  297. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_integration_workflows.py +0 -0
  298. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_key_rotator.py +0 -0
  299. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_lifecycle.py +0 -0
  300. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_lifecycle_supersede_invariant.py +0 -0
  301. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_llm_fallback.py +0 -0
  302. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_llm_provider_claude_cli.py +0 -0
  303. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_llm_provider_key_rotation.py +0 -0
  304. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_llm_steward_coverage.py +0 -0
  305. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_llm_steward_key_rotation.py +0 -0
  306. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_mcp_filter_bypass.py +0 -0
  307. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_mcp_helpers.py +0 -0
  308. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_mcp_rate_limit.py +0 -0
  309. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_mcp_server_validation.py +0 -0
  310. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_mcp_usage.py +0 -0
  311. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_media_processing.py +0 -0
  312. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_meta_decisions.py +0 -0
  313. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_metrics_exporter.py +0 -0
  314. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_observability.py +0 -0
  315. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_obsidian_mind_patterns.py +0 -0
  316. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_operator.py +0 -0
  317. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_operator_queue.py +0 -0
  318. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_perf_smoke_config.py +0 -0
  319. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_plugins.py +0 -0
  320. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_policy_coverage.py +0 -0
  321. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_policy_mode_env.py +0 -0
  322. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_postgres_parity.py +0 -0
  323. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_qdrant_backend.py +0 -0
  324. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_qmd_bridge.py +0 -0
  325. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_query_classifier.py +0 -0
  326. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_query_expansion.py +0 -0
  327. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_recall_entity_fanout.py +0 -0
  328. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_recall_fusion.py +0 -0
  329. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_recall_latency.py +0 -0
  330. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_recall_precision_at_5.py +0 -0
  331. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_recall_tokenizer.py +0 -0
  332. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_recall_vector_fallback.py +0 -0
  333. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_reliability_hardening.py +0 -0
  334. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_resolvers_concurrent_supersede.py +0 -0
  335. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_retrieval_profile.py +0 -0
  336. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_retrieval_profiles.py +0 -0
  337. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_retrieval_rrf_tiebreaker.py +0 -0
  338. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_retrieval_weights.py +0 -0
  339. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_review.py +0 -0
  340. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_rl_trainer.py +0 -0
  341. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_rrf_auto_gate.py +0 -0
  342. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_scheduler.py +0 -0
  343. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_schema.py +0 -0
  344. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_scope_boost.py +0 -0
  345. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_scope_utils.py +0 -0
  346. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_security_access.py +0 -0
  347. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_security_patterns.py +0 -0
  348. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_sensitivity_filter_adversarial.py +0 -0
  349. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_sensitivity_filter_adversarial_v2.py +0 -0
  350. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_sensitivity_filter_t07.py +0 -0
  351. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_service_coverage.py +0 -0
  352. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_session_tracker.py +0 -0
  353. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_snapshot.py +0 -0
  354. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_snapshot_roundtrip.py +0 -0
  355. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_sqlite_core.py +0 -0
  356. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_staleness.py +0 -0
  357. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_stealth_mode.py +0 -0
  358. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_steward.py +0 -0
  359. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_steward_classifier.py +0 -0
  360. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_steward_daydream_hook.py +0 -0
  361. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_steward_features.py +0 -0
  362. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_steward_features_v3.py +0 -0
  363. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_steward_resolution_parity.py +0 -0
  364. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_storage_parity.py +0 -0
  365. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_store_factory.py +0 -0
  366. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_tenant_isolation.py +0 -0
  367. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_turn_schema.py +0 -0
  368. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_two_pass_recall.py +0 -0
  369. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_v311_fixes.py +0 -0
  370. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_v313_e2e.py +0 -0
  371. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_v313_run_cycle_dedupe.py +0 -0
  372. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_v390_e2e.py +0 -0
  373. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_v391_strict_warnings.py +0 -0
  374. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_vault_exporter.py +0 -0
  375. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_vault_linter_orphan.py +0 -0
  376. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_vector_search.py +0 -0
  377. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_verbatim_recall.py +0 -0
  378. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_verbatim_store.py +0 -0
  379. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_verbatim_store_qdrant.py +0 -0
  380. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_webhook.py +0 -0
  381. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_whatsapp_importer.py +0 -0
  382. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_wiki_autopromote.py +0 -0
  383. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_wiki_binding.py +0 -0
  384. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_wiki_engine_idempotency.py +0 -0
  385. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_wiki_explored_and_contradictions.py +0 -0
  386. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_wiki_freshness.py +0 -0
  387. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_wiki_similarity_multiscope.py +0 -0
  388. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_wiki_suggest.py +0 -0
  389. {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_wiki_validate_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memorymaster
3
- Version: 3.18.0
3
+ Version: 3.21.0
4
4
  Summary: Production-grade memory reliability system for AI coding agents. Lifecycle-managed claims with citations, conflict detection, steward governance, and MCP integration.
5
5
  Author: wolverin0
6
6
  License: MIT
@@ -52,7 +52,7 @@ Lifecycle-managed claims with citations, conflict detection, steward governance,
52
52
 
53
53
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
54
54
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
55
- [![Tests](https://img.shields.io/badge/tests-1953-green.svg)]()
55
+ [![Tests](https://img.shields.io/badge/tests-2194-green.svg)]()
56
56
  [![MCP Tools](https://img.shields.io/badge/MCP%20tools-24-purple.svg)]()
57
57
  [![CLI Commands](https://img.shields.io/badge/CLI%20commands-86-orange.svg)]()
58
58
  [![PyPI](https://img.shields.io/pypi/v/memorymaster.svg)](https://pypi.org/project/memorymaster/)
@@ -87,6 +87,9 @@ recent PR status, and sensitivity-filter invariants.
87
87
  - **Hybrid retrieval**: vector (sentence-transformers / Gemini) + FTS5 + freshness + confidence
88
88
  - **Context optimizer**: `query_for_context(budget=4000)` returns auto-curated memory that fits your token budget
89
89
  - **Entity graph** with typed relationships and alias resolution
90
+ - **Rule-shaped claims** (new in v3.21.0): prescriptive `when <trigger>, do <action> because <rationale>` claims (`ingest_rule` / `query_rules`) — the shape an agent needs to actually change behaviour next time, not just recall a fact
91
+ - **Correction mining** (new in v3.21.0): `mine-rules` scans the verbatim transcript archive for user corrections and distills them into rule claims; the Stop hook also mines each session's latest correction automatically
92
+ - **Versioned schema migrations** (new in v3.20.0): `migrate` applies SQLite/Postgres migrations with sha256 drift detection; incremental `export-delta` ships small claim deltas for cheap cross-machine sync
90
93
  - **Steward governance**: multi-probe validators (filesystem, format, citation, semantic, tool) with proposal review
91
94
  - **Conflict resolution**: 5-tier auto (confidence > freshness > citations > LLM > manual)
92
95
  - **Auto-redaction** at ingest: JWT, GitHub tokens, Bearer, AWS keys, SSH keys, custom patterns
@@ -187,7 +190,7 @@ For zero-cost offline use, install [Ollama](https://ollama.com), `ollama pull ll
187
190
  }
188
191
  ```
189
192
 
190
- 22 MCP tools: `init_db`, `ingest_claim`, `run_cycle`, `run_steward`, `classify_query`, `query_memory`, `query_for_context`, `list_claims`, `redact_claim_payload`, `pin_claim`, `compact_memory`, `list_events`, `search_verbatim`, `open_dashboard`, `list_steward_proposals`, `resolve_steward_proposal`, `extract_entities`, `entity_stats`, `find_related_claims`, `quality_scores`, `recompute_tiers`, `federated_query`.
193
+ 24 MCP tools: `init_db`, `ingest_claim`, `ingest_rule`, `query_rules`, `run_cycle`, `run_steward`, `classify_query`, `query_memory`, `query_for_context`, `list_claims`, `redact_claim_payload`, `pin_claim`, `compact_memory`, `list_events`, `search_verbatim`, `open_dashboard`, `list_steward_proposals`, `resolve_steward_proposal`, `extract_entities`, `entity_stats`, `find_related_claims`, `quality_scores`, `recompute_tiers`, `federated_query`.
191
194
 
192
195
  See [`.mcp.json.example`](.mcp.json.example) for the full template.
193
196
 
@@ -6,7 +6,7 @@ Lifecycle-managed claims with citations, conflict detection, steward governance,
6
6
 
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
8
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
9
- [![Tests](https://img.shields.io/badge/tests-1953-green.svg)]()
9
+ [![Tests](https://img.shields.io/badge/tests-2194-green.svg)]()
10
10
  [![MCP Tools](https://img.shields.io/badge/MCP%20tools-24-purple.svg)]()
11
11
  [![CLI Commands](https://img.shields.io/badge/CLI%20commands-86-orange.svg)]()
12
12
  [![PyPI](https://img.shields.io/pypi/v/memorymaster.svg)](https://pypi.org/project/memorymaster/)
@@ -41,6 +41,9 @@ recent PR status, and sensitivity-filter invariants.
41
41
  - **Hybrid retrieval**: vector (sentence-transformers / Gemini) + FTS5 + freshness + confidence
42
42
  - **Context optimizer**: `query_for_context(budget=4000)` returns auto-curated memory that fits your token budget
43
43
  - **Entity graph** with typed relationships and alias resolution
44
+ - **Rule-shaped claims** (new in v3.21.0): prescriptive `when <trigger>, do <action> because <rationale>` claims (`ingest_rule` / `query_rules`) — the shape an agent needs to actually change behaviour next time, not just recall a fact
45
+ - **Correction mining** (new in v3.21.0): `mine-rules` scans the verbatim transcript archive for user corrections and distills them into rule claims; the Stop hook also mines each session's latest correction automatically
46
+ - **Versioned schema migrations** (new in v3.20.0): `migrate` applies SQLite/Postgres migrations with sha256 drift detection; incremental `export-delta` ships small claim deltas for cheap cross-machine sync
44
47
  - **Steward governance**: multi-probe validators (filesystem, format, citation, semantic, tool) with proposal review
45
48
  - **Conflict resolution**: 5-tier auto (confidence > freshness > citations > LLM > manual)
46
49
  - **Auto-redaction** at ingest: JWT, GitHub tokens, Bearer, AWS keys, SSH keys, custom patterns
@@ -141,7 +144,7 @@ For zero-cost offline use, install [Ollama](https://ollama.com), `ollama pull ll
141
144
  }
142
145
  ```
143
146
 
144
- 22 MCP tools: `init_db`, `ingest_claim`, `run_cycle`, `run_steward`, `classify_query`, `query_memory`, `query_for_context`, `list_claims`, `redact_claim_payload`, `pin_claim`, `compact_memory`, `list_events`, `search_verbatim`, `open_dashboard`, `list_steward_proposals`, `resolve_steward_proposal`, `extract_entities`, `entity_stats`, `find_related_claims`, `quality_scores`, `recompute_tiers`, `federated_query`.
147
+ 24 MCP tools: `init_db`, `ingest_claim`, `ingest_rule`, `query_rules`, `run_cycle`, `run_steward`, `classify_query`, `query_memory`, `query_for_context`, `list_claims`, `redact_claim_payload`, `pin_claim`, `compact_memory`, `list_events`, `search_verbatim`, `open_dashboard`, `list_steward_proposals`, `resolve_steward_proposal`, `extract_entities`, `entity_stats`, `find_related_claims`, `quality_scores`, `recompute_tiers`, `federated_query`.
145
148
 
146
149
  See [`.mcp.json.example`](.mcp.json.example) for the full template.
147
150
 
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "3.4.1"
5
+ __version__ = "3.21.0"
@@ -23,7 +23,9 @@ from memorymaster.cli_handlers_curation import COMMAND_HANDLERS
23
23
  from memorymaster.cli_handlers_basic import (
24
24
  _handle_decay,
25
25
  _handle_entity_graph_export,
26
+ _handle_export_delta,
26
27
  _handle_ingest_daydream,
28
+ _handle_migrate,
27
29
  _handle_recompute_confidence_priors,
28
30
  _handle_wiki_suggest_links,
29
31
  handle_mcp_usage_report,
@@ -38,6 +40,8 @@ COMMAND_HANDLERS["ingest-daydream"] = _handle_ingest_daydream
38
40
  COMMAND_HANDLERS["mcp-usage-report"] = (
39
41
  lambda args, service, parser, effective_db: handle_mcp_usage_report(args, effective_db)
40
42
  )
43
+ COMMAND_HANDLERS["migrate"] = _handle_migrate
44
+ COMMAND_HANDLERS["export-delta"] = _handle_export_delta
41
45
 
42
46
 
43
47
  def build_parser() -> argparse.ArgumentParser:
@@ -51,6 +55,11 @@ def build_parser() -> argparse.ArgumentParser:
51
55
 
52
56
  sub.add_parser("init-db", help="Create schema in SQLite database")
53
57
 
58
+ migrate = sub.add_parser("migrate", help="Apply pending versioned schema migrations (v3.20.0+)")
59
+ migrate_mode = migrate.add_mutually_exclusive_group()
60
+ migrate_mode.add_argument("--list", action="store_true", help="List known migrations without touching the DB")
61
+ migrate_mode.add_argument("--status", action="store_true", help="Report applied vs pending per migration")
62
+
54
63
  sub.add_parser("stealth-status", help="Show whether stealth mode is active and which DB is in use")
55
64
 
56
65
  ingest = sub.add_parser("ingest", help="Ingest a raw claim with citations")
@@ -447,6 +456,13 @@ def build_parser() -> argparse.ArgumentParser:
447
456
  mine_cmd.add_argument("--scope", default="project", help="Scope for ingested claims")
448
457
  mine_cmd.add_argument("--max", type=int, default=100, help="Max claims to ingest")
449
458
 
459
+ mine_rules_cmd = sub.add_parser("mine-rules", help="Mine verbatim corrections into rule-shaped claims (v3.21.0-R1b)")
460
+ mine_rules_cmd.add_argument("--since-id", dest="since_id", type=int, default=None, help="Override the stored watermark; start scanning after this verbatim id")
461
+ mine_rules_cmd.add_argument("--limit", type=int, default=None, help="Max candidate windows to examine this run (caps LLM calls)")
462
+ mine_rules_cmd.add_argument("--batch-size", dest="batch_size", type=int, default=200, help="Rows fetched per SQL pre-filter page (default: 200)")
463
+ mine_rules_cmd.add_argument("--provider", default="claude_cli", help="LLM provider for this run (default: claude_cli)")
464
+ mine_rules_cmd.add_argument("--reset", action="store_true", help="Clear the stored watermark before running (re-scan from the start)")
465
+
450
466
  verify_cmd = sub.add_parser("verify-claims", help="Cross-check claims against current codebase")
451
467
  verify_cmd.add_argument("--scope", default="", help="Scope filter")
452
468
  verify_cmd.add_argument("--limit", type=int, default=200, help="Max claims to check")
@@ -504,6 +520,13 @@ def build_parser() -> argparse.ArgumentParser:
504
520
  merge_cmd = sub.add_parser("merge-db", help="Merge claims from a remote memorymaster DB (bidirectional sync)")
505
521
  merge_cmd.add_argument("--source", required=True, help="Path to source DB file to merge from")
506
522
 
523
+ delta_cmd = sub.add_parser(
524
+ "export-delta",
525
+ help="Export claims changed since a watermark into a small SQLite delta file (incremental sync)",
526
+ )
527
+ delta_cmd.add_argument("--since", default="", help="ISO-8601 watermark; export claims with updated_at after this (empty = full export)")
528
+ delta_cmd.add_argument("--output", required=True, help="Path to write the delta SQLite file (overwritten if it exists)")
529
+
507
530
  daily = sub.add_parser("daily-note", help="Generate a daily note summarizing today's activity")
508
531
  daily.add_argument("--date", default="", help="Date to generate for (YYYY-MM-DD, default: today)")
509
532
  daily.add_argument("--output", default="", help="Directory to save .md file (default: print to stdout)")
@@ -556,7 +579,7 @@ def main(argv: list[str] | None = None) -> int:
556
579
  effective_db = _resolve_db_path(args)
557
580
 
558
581
  # Commands that don't need MemoryService run first; service is lazy-created once for all others.
559
- _NO_SERVICE_COMMANDS = {"stealth-status", "export-metrics", "wiki-freshness", "mcp-usage-report"}
582
+ _NO_SERVICE_COMMANDS = {"stealth-status", "export-metrics", "wiki-freshness", "mcp-usage-report", "export-delta"}
560
583
 
561
584
  try:
562
585
  handler = COMMAND_HANDLERS.get(args.command)
@@ -1357,3 +1357,95 @@ def _handle_check_staleness(args: argparse.Namespace, service, parser: argparse.
1357
1357
  return 0
1358
1358
 
1359
1359
 
1360
+ def _handle_migrate(args: argparse.Namespace, service, parser: argparse.ArgumentParser, effective_db: str) -> int:
1361
+ """v3.20.0-S1: apply pending schema migrations, or report status.
1362
+
1363
+ Default (no flags): apply every pending migration in version order.
1364
+ --list: dump known migrations (version + description) without touching the DB.
1365
+ --status: query the DB and show applied vs pending per migration.
1366
+ """
1367
+ from memorymaster.migrations import (
1368
+ MigrationRunner,
1369
+ discover_migrations,
1370
+ )
1371
+ from memorymaster.store_factory import is_postgres_dsn
1372
+
1373
+ # --list works without a DB connection at all.
1374
+ if getattr(args, "list", False):
1375
+ migrations = discover_migrations()
1376
+ if args.json_output:
1377
+ payload = [{"version": m.version, "description": m.description} for m in migrations]
1378
+ print(_json_envelope(payload))
1379
+ else:
1380
+ print(f"known migrations ({len(migrations)}):")
1381
+ for m in migrations:
1382
+ print(f" v{m.version:04d} {m.description}")
1383
+ return 0
1384
+
1385
+ backend = "postgres" if is_postgres_dsn(effective_db) else "sqlite"
1386
+ store = service.store
1387
+ with store.connect() as conn:
1388
+ runner = MigrationRunner(conn, backend=backend)
1389
+
1390
+ if getattr(args, "status", False):
1391
+ entries = runner.status()
1392
+ if args.json_output:
1393
+ payload = [
1394
+ {
1395
+ "version": e.version,
1396
+ "description": e.description,
1397
+ "applied": e.applied,
1398
+ "applied_at": e.applied_at,
1399
+ }
1400
+ for e in entries
1401
+ ]
1402
+ print(_json_envelope(payload))
1403
+ else:
1404
+ print(f"backend={backend} db={effective_db}")
1405
+ for e in entries:
1406
+ marker = "[applied]" if e.applied else "[pending]"
1407
+ when = f" applied_at={e.applied_at}" if e.applied_at else ""
1408
+ print(f" v{e.version:04d} {marker} {e.description}{when}")
1409
+ return 0
1410
+
1411
+ # Default: apply pending
1412
+ newly = runner.apply_pending()
1413
+ if args.json_output:
1414
+ print(_json_envelope({"applied": newly, "backend": backend}))
1415
+ else:
1416
+ if not newly:
1417
+ print(f"migrate: nothing to apply (backend={backend}, db={effective_db})")
1418
+ else:
1419
+ print(f"migrate: applied {len(newly)} migration(s) on backend={backend}:")
1420
+ for v in newly:
1421
+ print(f" v{v:04d}")
1422
+ return 0
1423
+
1424
+
1425
+ def _handle_export_delta(args: argparse.Namespace, service, parser: argparse.ArgumentParser, effective_db: str) -> int:
1426
+ """Export claims changed since a watermark into a small SQLite delta file.
1427
+
1428
+ The delta file is a valid `merge-db --source` input. Prints (or JSON-emits)
1429
+ the export counts and the new watermark — callers should record
1430
+ `max_updated_at` and pass it as `--since` on the next run.
1431
+ """
1432
+ from memorymaster.delta_sync import export_delta
1433
+
1434
+ t0 = time.perf_counter()
1435
+ result = export_delta(effective_db, args.since, args.output)
1436
+ elapsed_ms = (time.perf_counter() - t0) * 1000
1437
+ if args.json_output:
1438
+ print(_json_envelope(result, query_ms=elapsed_ms))
1439
+ else:
1440
+ since_label = result["since"] or "(full export)"
1441
+ print(
1442
+ f"export-delta: {result['exported']} claims + {result['citations']} citations "
1443
+ f"since {since_label} -> {args.output}"
1444
+ )
1445
+ if result["max_updated_at"]:
1446
+ print(f" next watermark (--since): {result['max_updated_at']}")
1447
+ else:
1448
+ print(" delta is empty — nothing changed since the watermark")
1449
+ return 0
1450
+
1451
+
@@ -343,6 +343,31 @@ def _handle_mine_transcript(args: argparse.Namespace, service, parser: argparse.
343
343
  return 0
344
344
 
345
345
 
346
+ def _handle_mine_rules(args: argparse.Namespace, service, parser: argparse.ArgumentParser, effective_db: str) -> int:
347
+ from memorymaster.rule_miner import mine_rules
348
+ t0 = time.perf_counter()
349
+ result = mine_rules(
350
+ effective_db,
351
+ service,
352
+ since_id=getattr(args, "since_id", None),
353
+ limit=getattr(args, "limit", None),
354
+ batch_size=getattr(args, "batch_size", 200),
355
+ provider=getattr(args, "provider", "claude_cli"),
356
+ reset=getattr(args, "reset", False),
357
+ )
358
+ elapsed_ms = (time.perf_counter() - t0) * 1000
359
+ if args.json_output:
360
+ print(_json_envelope(result, query_ms=elapsed_ms))
361
+ else:
362
+ abort = f", ABORTED ({result['aborted_reason']})" if result.get("aborted_reason") else ""
363
+ print(
364
+ f"Mined rules: {result['candidates']} candidates, {result['llm_calls']} llm calls, "
365
+ f"{result['ingested']} ingested, {result['duplicates']} dupes, {result['skipped']} skipped "
366
+ f"(watermark={result['last_id']}{abort}, {elapsed_ms:.0f}ms)"
367
+ )
368
+ return 0
369
+
370
+
346
371
  def _handle_wiki_breakdown(args: argparse.Namespace, service, parser: argparse.ArgumentParser, effective_db: str) -> int:
347
372
  from memorymaster.wiki_engine import breakdown
348
373
  t0 = time.perf_counter()
@@ -703,6 +728,7 @@ COMMAND_HANDLERS: dict[str, object] = {
703
728
  "wiki-freshness": _handle_wiki_freshness,
704
729
  "bases-generate": _handle_bases_generate,
705
730
  "mine-transcript": _handle_mine_transcript,
731
+ "mine-rules": _handle_mine_rules,
706
732
  "verify-claims": _handle_verify_claims,
707
733
  "extract-entities": _handle_extract_entities,
708
734
  "entity-stats": _handle_entity_stats,
@@ -180,6 +180,27 @@ Only: bug root causes, decisions, gotchas, constraints. Never: credentials, IPs,
180
180
  pass
181
181
 
182
182
 
183
+ def _run_rule_extraction(transcript_path, cwd):
184
+ """R1b ongoing: mine the latest correction in this session into a rule claim.
185
+
186
+ Reuses memorymaster.rule_miner.mine_transcript_rules (single source of truth
187
+ for the correction->rule prompt + ingest path). Bounded to one window per
188
+ stop to keep the hook fast; rules land as low-confidence candidates."""
189
+ try:
190
+ if not transcript_path or not os.path.exists(transcript_path) or not os.path.exists(DB_PATH):
191
+ return
192
+ from memorymaster.rule_miner import mine_transcript_rules
193
+ from memorymaster.service import MemoryService
194
+
195
+ scope = "project:" + os.path.basename(cwd).lower().replace(" ", "-") if cwd else "global"
196
+ svc = MemoryService(DB_PATH, workspace_root=Path(cwd or PROJECT_ROOT))
197
+ stats = mine_transcript_rules(transcript_path, svc, scope=scope, max_windows=1)
198
+ if stats.get("ingested"):
199
+ sys.stderr.write(f"[MemoryMaster] mined {stats['ingested']} rule(s) from corrections\n")
200
+ except Exception:
201
+ pass
202
+
203
+
183
204
  def main():
184
205
  try:
185
206
  data = json.loads(sys.stdin.read() or "{}")
@@ -232,6 +253,9 @@ def main():
232
253
  # Not time to block — run passive Gemini extraction
233
254
  _run_gemini_extraction(transcript_path, cwd)
234
255
 
256
+ # R1b: mine the latest correction in this session into a rule claim
257
+ _run_rule_extraction(transcript_path, cwd)
258
+
235
259
  sys.stdout.write(json.dumps({"decision": "approve"}))
236
260
 
237
261
 
@@ -19,6 +19,7 @@ import urllib.error
19
19
  import urllib.request
20
20
  from urllib.parse import parse_qs, urlparse
21
21
 
22
+ from memorymaster import dashboard_auth
22
23
  from memorymaster.config import get_config
23
24
  from memorymaster.review import build_review_queue, queue_to_dicts
24
25
  from memorymaster.security import is_sensitive_claim
@@ -626,6 +627,9 @@ class DashboardHTTPServer(ThreadingHTTPServer):
626
627
  self.db_target = str(db_target) if db_target is not None else "memorymaster.db"
627
628
  self.workspace_root = Path(workspace_root) if workspace_root is not None else Path.cwd()
628
629
  self._operator_proc: subprocess.Popen[str] | None = None
630
+ # Pin the configured host:port string so the CSRF check can compare it
631
+ # against the request's Origin/Referer header in authenticate POST routes.
632
+ self.configured_host_port = f"{server_address[0]}:{server_address[1]}"
629
633
  super().__init__(server_address, DashboardRequestHandler)
630
634
 
631
635
  def operator_status(self) -> dict[str, Any]:
@@ -701,10 +705,45 @@ class DashboardRequestHandler(BaseHTTPRequestHandler):
701
705
  def _server(self) -> DashboardHTTPServer:
702
706
  return self.server # type: ignore[return-value]
703
707
 
708
+ def _enforce_auth(self, *, method: str, route: str) -> bool:
709
+ """Run the v3.19.0-H2 auth + role + CSRF gates. Returns True on pass.
710
+
711
+ On failure, the JSON error response has already been written; the
712
+ caller must return immediately without dispatching the route.
713
+ Health endpoints (/health, /healthz, /readyz) are intentionally
714
+ exempt so external monitors can probe without credentials.
715
+ """
716
+ if route in {"/health", "/healthz", "/readyz"}:
717
+ return True
718
+
719
+ decision = dashboard_auth.authenticate(self.headers)
720
+ decision = dashboard_auth.authorize(decision, method=method, route=route)
721
+ if not decision.ok:
722
+ self._write_json(
723
+ {"ok": False, "error": decision.reason},
724
+ status=decision.status,
725
+ )
726
+ return False
727
+
728
+ if method.upper() == "POST":
729
+ csrf = dashboard_auth.check_csrf(
730
+ self.headers,
731
+ configured_host_port=getattr(self._server, "configured_host_port", None),
732
+ )
733
+ if not csrf.ok:
734
+ self._write_json(
735
+ {"ok": False, "error": csrf.reason},
736
+ status=csrf.status,
737
+ )
738
+ return False
739
+ return True
740
+
704
741
  def do_GET(self) -> None: # noqa: N802
705
742
  parsed = urlparse(self.path)
706
743
  route = parsed.path
707
744
  try:
745
+ if not self._enforce_auth(method="GET", route=route):
746
+ return
708
747
  if _route_get_request(self, route, parsed.query):
709
748
  return
710
749
  self._write_json({"ok": False, "error": "Not found"}, status=HTTPStatus.NOT_FOUND)
@@ -717,6 +756,8 @@ class DashboardRequestHandler(BaseHTTPRequestHandler):
717
756
  parsed = urlparse(self.path)
718
757
  route = parsed.path
719
758
  try:
759
+ if not self._enforce_auth(method="POST", route=route):
760
+ return
720
761
  payload = self._read_json_body()
721
762
  if route == "/api/triage/action":
722
763
  self._handle_triage_action(payload)
@@ -1443,6 +1484,12 @@ def create_dashboard_server(
1443
1484
  port: int = 8765,
1444
1485
  operator_log_jsonl: str | Path = "artifacts/operator/operator_events.jsonl",
1445
1486
  ) -> DashboardHTTPServer:
1487
+ # v3.19.0-H2: refuse non-loopback bind without an auth secret unless the
1488
+ # operator explicitly opts in. Raises dashboard_auth.BindUnsafeError on
1489
+ # an unsafe configuration; callers should let it propagate so the
1490
+ # mistake is visible at startup instead of silently exposed.
1491
+ dashboard_auth.check_bind_safety(host)
1492
+
1446
1493
  if service is None:
1447
1494
  if db_target is None:
1448
1495
  db_target = "memorymaster.db"
@@ -0,0 +1,196 @@
1
+ """HTTP-layer authentication, CSRF, and bind-safety for the dashboard (v3.19.0-H2).
2
+
3
+ The dashboard's prior posture was "trusted local-only" — zero HTTP auth,
4
+ default loopback bind. Anyone who could reach the port had full read +
5
+ operator-control. This module adds opt-in token auth with viewer/operator
6
+ role separation, CSRF for browser POSTs, and refusal to bind non-loopback
7
+ hosts without an explicit secret.
8
+
9
+ Backwards compatibility: when no auth secrets are configured (both
10
+ ``MEMORYMASTER_DASHBOARD_TOKEN_VIEWER`` and ``MEMORYMASTER_DASHBOARD_TOKEN_OPERATOR``
11
+ are empty), the dashboard runs in legacy mode — no auth enforced. Bind
12
+ safety still applies: legacy mode refuses non-loopback bind unless the
13
+ operator explicitly opts in with ``MEMORYMASTER_DASHBOARD_UNSAFE_BIND=1``.
14
+
15
+ Env vars:
16
+ MEMORYMASTER_DASHBOARD_TOKEN_VIEWER — bearer token granting read-only access
17
+ MEMORYMASTER_DASHBOARD_TOKEN_OPERATOR — bearer token granting full mutating access
18
+ MEMORYMASTER_DASHBOARD_UNSAFE_BIND — set to 1 to allow non-loopback bind
19
+ without an auth secret (logs WARNING)
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import hmac
24
+ import logging
25
+ import os
26
+ from dataclasses import dataclass
27
+ from enum import Enum
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ class DashboardRole(str, Enum):
33
+ VIEWER = "viewer"
34
+ OPERATOR = "operator"
35
+
36
+
37
+ # Routes that require operator role regardless of HTTP method. POST routes
38
+ # always require operator role separately. This list captures GET routes that
39
+ # are mutating-adjacent (start/stop operator control, stream that may carry
40
+ # sensitive operator events).
41
+ OPERATOR_ONLY_GET_ROUTES: frozenset[str] = frozenset({
42
+ "/api/operator/control", # POST in practice but pin it here for completeness
43
+ "/api/operator/stream", # SSE — operator-only since it streams operator events
44
+ })
45
+
46
+ # POST routes — all require operator.
47
+ POST_OPERATOR_ROUTES: frozenset[str] = frozenset({
48
+ "/api/triage/action",
49
+ "/api/operator/control",
50
+ "/api/action-proposals/status",
51
+ })
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class AuthDecision:
56
+ """Outcome of an authentication or authorization check."""
57
+
58
+ ok: bool
59
+ role: DashboardRole | None = None
60
+ reason: str = ""
61
+ status: int = 200
62
+
63
+
64
+ class BindUnsafeError(RuntimeError):
65
+ """Raised when the dashboard refuses to bind a non-loopback host without auth."""
66
+
67
+
68
+ def _env_token(name: str) -> str:
69
+ return os.environ.get(name, "").strip()
70
+
71
+
72
+ def legacy_mode() -> bool:
73
+ """True when no auth secrets are configured (back-compat path).
74
+
75
+ Legacy mode preserves the pre-v3.19 behaviour for loopback bind — no
76
+ auth check, no CSRF. Bind safety still applies regardless.
77
+ """
78
+ return not (
79
+ _env_token("MEMORYMASTER_DASHBOARD_TOKEN_VIEWER")
80
+ or _env_token("MEMORYMASTER_DASHBOARD_TOKEN_OPERATOR")
81
+ )
82
+
83
+
84
+ def _extract_bearer(headers) -> str:
85
+ raw = headers.get("Authorization", "") if headers else ""
86
+ if not raw or not raw.lower().startswith("bearer "):
87
+ return ""
88
+ return raw[7:].strip()
89
+
90
+
91
+ def authenticate(headers) -> AuthDecision:
92
+ """Map an incoming request's Authorization header to a role.
93
+
94
+ Behaviour:
95
+ - Legacy mode (no tokens set): return ok with role=operator (no enforcement).
96
+ - Missing token: 401 (``missing_token``).
97
+ - Token matches operator: ok with role=operator.
98
+ - Token matches viewer: ok with role=viewer.
99
+ - Token unrecognized: 401 (``invalid_token``).
100
+
101
+ Constant-time comparison (``hmac.compare_digest``) prevents timing attacks.
102
+ """
103
+ if legacy_mode():
104
+ return AuthDecision(True, role=DashboardRole.OPERATOR)
105
+
106
+ token = _extract_bearer(headers)
107
+ if not token:
108
+ return AuthDecision(False, reason="missing_token", status=401)
109
+
110
+ op_token = _env_token("MEMORYMASTER_DASHBOARD_TOKEN_OPERATOR")
111
+ vw_token = _env_token("MEMORYMASTER_DASHBOARD_TOKEN_VIEWER")
112
+
113
+ if op_token and hmac.compare_digest(token, op_token):
114
+ return AuthDecision(True, role=DashboardRole.OPERATOR)
115
+ if vw_token and hmac.compare_digest(token, vw_token):
116
+ return AuthDecision(True, role=DashboardRole.VIEWER)
117
+ return AuthDecision(False, reason="invalid_token", status=401)
118
+
119
+
120
+ def authorize(decision: AuthDecision, *, method: str, route: str) -> AuthDecision:
121
+ """Refine an authentication decision against the role-vs-route policy.
122
+
123
+ Returns 403 (``role_required_operator``) for routes where viewer is
124
+ insufficient. Passes ``decision`` through unchanged on success.
125
+ """
126
+ if not decision.ok:
127
+ return decision
128
+
129
+ method_u = method.upper()
130
+ operator_required = (
131
+ method_u == "POST"
132
+ or route in OPERATOR_ONLY_GET_ROUTES
133
+ or route in POST_OPERATOR_ROUTES
134
+ )
135
+ if operator_required and decision.role != DashboardRole.OPERATOR:
136
+ return AuthDecision(
137
+ False,
138
+ role=decision.role,
139
+ reason="role_required_operator",
140
+ status=403,
141
+ )
142
+ return decision
143
+
144
+
145
+ def check_csrf(headers, *, configured_host_port: str | None) -> AuthDecision:
146
+ """Validate Origin/Referer for browser-originated POSTs.
147
+
148
+ Non-browser clients (curl, scripts, MCP-style integrations) typically
149
+ omit ``Origin`` — those requests pass through unchallenged. Browsers
150
+ always set ``Origin``; when present, it must contain the configured
151
+ host:port string. Returns 403 (``csrf_origin_mismatch``) on mismatch.
152
+
153
+ Legacy mode skips CSRF entirely.
154
+ """
155
+ if legacy_mode():
156
+ return AuthDecision(True)
157
+
158
+ origin = ""
159
+ if headers:
160
+ origin = headers.get("Origin", "") or headers.get("Referer", "") or ""
161
+ if not origin:
162
+ return AuthDecision(True) # non-browser caller
163
+ if configured_host_port and configured_host_port not in origin:
164
+ return AuthDecision(False, reason="csrf_origin_mismatch", status=403)
165
+ return AuthDecision(True)
166
+
167
+
168
+ def check_bind_safety(host: str) -> None:
169
+ """Refuse non-loopback bind without an auth secret or explicit opt-in.
170
+
171
+ Raises ``BindUnsafeError`` if all of: (a) host is non-loopback,
172
+ (b) no auth tokens configured, (c) ``MEMORYMASTER_DASHBOARD_UNSAFE_BIND``
173
+ not set. Otherwise returns silently. Logs a WARNING for the unsafe opt-in
174
+ case so operators see they're running exposed.
175
+ """
176
+ loopback_hosts = {"127.0.0.1", "::1", "localhost", ""}
177
+ if host in loopback_hosts:
178
+ return
179
+ if not legacy_mode():
180
+ return # token-based auth is enforced; non-loopback bind is acceptable
181
+
182
+ unsafe_raw = os.environ.get("MEMORYMASTER_DASHBOARD_UNSAFE_BIND", "").strip().lower()
183
+ if unsafe_raw in {"1", "true", "yes", "on"}:
184
+ logger.warning(
185
+ "dashboard binding to non-loopback host '%s' with no auth secret "
186
+ "and MEMORYMASTER_DASHBOARD_UNSAFE_BIND=1 — running exposed",
187
+ host,
188
+ )
189
+ return
190
+
191
+ raise BindUnsafeError(
192
+ f"Refusing to bind dashboard to non-loopback host '{host}' without auth secret. "
193
+ f"Set MEMORYMASTER_DASHBOARD_TOKEN_OPERATOR (and optionally "
194
+ f"MEMORYMASTER_DASHBOARD_TOKEN_VIEWER), or explicitly opt-in with "
195
+ f"MEMORYMASTER_DASHBOARD_UNSAFE_BIND=1."
196
+ )