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.
- {memorymaster-3.18.0/memorymaster.egg-info → memorymaster-3.21.0}/PKG-INFO +6 -3
- {memorymaster-3.18.0 → memorymaster-3.21.0}/README.md +5 -2
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/__init__.py +1 -1
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/cli.py +24 -1
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/cli_handlers_basic.py +92 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/cli_handlers_curation.py +26 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config_templates/hooks/memorymaster-auto-ingest.py +24 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/dashboard.py +47 -0
- memorymaster-3.21.0/memorymaster/dashboard_auth.py +196 -0
- memorymaster-3.21.0/memorymaster/delta_sync.py +172 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/daydream_ingest.py +45 -1
- memorymaster-3.21.0/memorymaster/llm_budget.py +190 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/llm_provider.py +26 -0
- memorymaster-3.21.0/memorymaster/mcp_path_policy.py +146 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/mcp_server.py +65 -5
- memorymaster-3.21.0/memorymaster/migrations/0001_initial.py +25 -0
- memorymaster-3.21.0/memorymaster/migrations/0002_miner_state.py +38 -0
- memorymaster-3.21.0/memorymaster/migrations/__init__.py +41 -0
- memorymaster-3.21.0/memorymaster/migrations/runner.py +268 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/postgres_store.py +9 -0
- memorymaster-3.21.0/memorymaster/rule_miner.py +443 -0
- memorymaster-3.21.0/memorymaster/rules.py +104 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/service.py +111 -46
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/storage.py +10 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/verbatim_store.py +41 -9
- memorymaster-3.21.0/memorymaster/webhook.py +161 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/wiki_engine.py +41 -1
- {memorymaster-3.18.0 → memorymaster-3.21.0/memorymaster.egg-info}/PKG-INFO +6 -3
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster.egg-info/SOURCES.txt +19 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/pyproject.toml +1 -1
- memorymaster-3.21.0/tests/conftest.py +103 -0
- memorymaster-3.21.0/tests/test_backend_parity.py +167 -0
- memorymaster-3.21.0/tests/test_dashboard_auth.py +290 -0
- memorymaster-3.21.0/tests/test_delta_sync.py +261 -0
- memorymaster-3.21.0/tests/test_llm_budget.py +232 -0
- memorymaster-3.21.0/tests/test_mcp_path_policy.py +206 -0
- memorymaster-3.21.0/tests/test_migrations.py +292 -0
- memorymaster-3.21.0/tests/test_rule_claims.py +181 -0
- memorymaster-3.21.0/tests/test_rule_miner.py +297 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_verbatim_dedup.py +36 -0
- memorymaster-3.21.0/tests/test_webhook_hmac.py +216 -0
- memorymaster-3.18.0/memorymaster/webhook.py +0 -58
- memorymaster-3.18.0/tests/conftest.py +0 -37
- {memorymaster-3.18.0 → memorymaster-3.21.0}/LICENSE +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/artifacts/bm25-per-field-eval-harness.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/benchmarks/longmemeval_runner.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/benchmarks/longmemeval_vector_runner.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/benchmarks/perf_smoke.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/__main__.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/_storage_lifecycle.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/_storage_read.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/_storage_schema.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/_storage_shared.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/_storage_sources.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/_storage_write_claims.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/access_control.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/action_exporters.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/action_extractor.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/atlas_claim_extractor.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/atlas_contract.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/auto_extractor.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/auto_resolver.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/candidate_dedupe.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/claim_edges.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/claim_verifier.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/cli_helpers.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/closets.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config_templates/claude-md-append.md +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config_templates/codex-agents-md-append.md +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config_templates/hooks/memorymaster-classify.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config_templates/hooks/memorymaster-dream-sync.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config_templates/hooks/memorymaster-precompact.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config_templates/hooks/memorymaster-recall.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config_templates/hooks/memorymaster-session-start.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config_templates/hooks/memorymaster-steward-cycle.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/config_templates/hooks/memorymaster-validate-wiki.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/conflict_resolver.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/connectors/__init__.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/connectors/whatsapp.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/context_hook.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/context_optimizer.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/daily_notes.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/db_merge.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/dream_bridge.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/embeddings.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/entity_extractor.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/entity_graph.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/entity_registry.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/federated_graphify.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/feedback.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/graph_store.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/hook_log.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/__init__.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/calibration.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/compact_summaries.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/compactor.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/decay.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/dedup.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/deterministic.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/entity_graph_export.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/extractor.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/staleness.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/jobs/validator.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/key_rotator.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/lifecycle.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/llm_rerank.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/llm_steward.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/mcp_usage.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/media_processing.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/media_providers.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/metrics_exporter.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/models.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/observability.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/operator.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/operator_queue.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/plugins.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/policy.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/qdrant_backend.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/qdrant_recall_fallback.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/qmd_bridge.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/query_classifier.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/query_expansion.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/recall_fusion.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/recall_tokenizer.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/retrieval.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/retry.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/review.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/rl_trainer.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/scheduler.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/schema.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/schema.sql +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/schema_postgres.sql +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/scope_utils.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/security.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/session_tracker.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/setup_hooks.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/skill_evolver.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/snapshot.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/steward.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/steward_classifier.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/steward_features.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/store_factory.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/transcript_miner.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/turn_schema.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/vault_bases.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/vault_curator.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/vault_exporter.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/vault_linter.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/vault_log.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/vault_query_capture.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/vault_synthesis.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/verbatim_recall.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/wiki_freshness.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/wiki_similarity.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/wiki_suggest.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster/wiki_validate.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster.egg-info/dependency_links.txt +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster.egg-info/entry_points.txt +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster.egg-info/requires.txt +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/memorymaster.egg-info/top_level.txt +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/agg_recall_latency.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/alert_operator_metrics.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/audit_dedupe_precision.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/autoresearch_daemon.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/backfill_entity_extraction.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/backfill_graph_store.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/backfill_stop_hook_citations.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/backtest_steward_classifier.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/build_steward_training_set.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/check_hook_template_drift.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/claude_to_turns.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/codex_live_to_turns.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/compaction_edge_cases.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/compaction_trace_report.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/compaction_trace_validate.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/confusion_matrix_eval.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/conversation_importer.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/conversation_to_turns.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/e2e_operator.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/email_live_to_turns.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/eval_bm25_sweep.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/eval_classify_f1.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/eval_memorymaster.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/eval_recall_precision_at_5.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/eval_recall_quality.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/eval_steward_pareto.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/eval_verbatim_recall.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/expand_recall_eval.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/generate_drill_signoff.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/git_to_turns.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/github_live_to_turns.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/gitnexus_to_claims.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/grid_recall_weights.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/index_claims_to_qdrant.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/ingest_planning_docs.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/jira_live_to_turns.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/label_prompts_with_judge.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/llm_benchmark.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/measure_dedupe_thresholds.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/merge_scope_variants.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/messages_to_turns.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/operator_metrics.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/precompute_candidates.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/recurring_incident_drill.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/release_readiness.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/run_codex_autologger.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/run_incident_drill.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/scheduled_ingest.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/setup-hooks.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/slack_live_to_turns.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/sync_hook_templates.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/tickets_to_turns.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/train_steward_classifier.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/scripts/webhook_to_turns.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/setup.cfg +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/bench_longmemeval.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/integration/test_extract_llm_ollama_live.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_access_control.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_action_exporters.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_action_extractor.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_atlas_claim_extractor.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_atlas_contract.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_atlas_source_schema.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_auto_extractor.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_auto_ingest_hook_citations.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_auto_ingest_hook_schema.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_auto_resolver.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_auto_validate.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_bm25_per_field.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_calibration.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_calibration_priors_applied.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_candidate_dedupe.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_claim_edges.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_claim_links.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_claim_type_ranking.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_classify_hook_f1.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_classify_hook_latency.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_claude_to_turns.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_cli_dry_run.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_cli_json_flag.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_cli_ready.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_cli_review_queue.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_cli_subcommands.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_closets.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_closets_recall_integration.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_compact_summaries.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_compact_summaries_sensitivity.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_compaction_trace.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_compactor_artifact_order.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_config.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_conflict_resolver.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_confusion_matrix_eval.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_connection_retry.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_connectors.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_context_hook.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_context_optimizer.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_context_optimizer_provider.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_conversation_to_turns.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_dashboard.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_dashboard_coverage.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_dashboard_latency.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_dashboard_lineage.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_dashboard_review_queue.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_daydream_ingest.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_db_merge_confidence_conflict.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_db_merge_coverage_v2.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_decay_coverage.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_decay_respects_pinned.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_dedup.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_dedup_cli.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_dedup_conflict_disambiguation.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_deterministic_predicates.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_dream_bridge_coverage_v2.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_dream_bridge_sensitivity.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_embeddings_coverage.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_entity_extractor.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_entity_extractor_llm.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_entity_graph.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_entity_graph_export.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_entity_new_kinds.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_entity_regex_v3.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_entity_registry.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_eval_harness.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_events_schema.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_extract_llm_ollama.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_federated_graphify_mcp.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_federated_query_safety.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_feedback.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_fts5_search.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_graph_distance.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_graph_store.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_handler_regressions.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_hook_env_isolation.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_human_id.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_incident_drill_runner.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_integration_workflows.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_key_rotator.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_lifecycle.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_lifecycle_supersede_invariant.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_llm_fallback.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_llm_provider_claude_cli.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_llm_provider_key_rotation.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_llm_steward_coverage.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_llm_steward_key_rotation.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_mcp_filter_bypass.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_mcp_helpers.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_mcp_rate_limit.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_mcp_server_validation.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_mcp_usage.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_media_processing.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_meta_decisions.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_metrics_exporter.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_observability.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_obsidian_mind_patterns.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_operator.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_operator_queue.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_perf_smoke_config.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_plugins.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_policy_coverage.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_policy_mode_env.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_postgres_parity.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_qdrant_backend.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_qmd_bridge.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_query_classifier.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_query_expansion.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_recall_entity_fanout.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_recall_fusion.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_recall_latency.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_recall_precision_at_5.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_recall_tokenizer.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_recall_vector_fallback.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_reliability_hardening.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_resolvers_concurrent_supersede.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_retrieval_profile.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_retrieval_profiles.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_retrieval_rrf_tiebreaker.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_retrieval_weights.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_review.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_rl_trainer.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_rrf_auto_gate.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_scheduler.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_schema.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_scope_boost.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_scope_utils.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_security_access.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_security_patterns.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_sensitivity_filter_adversarial.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_sensitivity_filter_adversarial_v2.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_sensitivity_filter_t07.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_service_coverage.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_session_tracker.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_snapshot.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_snapshot_roundtrip.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_sqlite_core.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_staleness.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_stealth_mode.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_steward.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_steward_classifier.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_steward_daydream_hook.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_steward_features.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_steward_features_v3.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_steward_resolution_parity.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_storage_parity.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_store_factory.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_tenant_isolation.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_turn_schema.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_two_pass_recall.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_v311_fixes.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_v313_e2e.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_v313_run_cycle_dedupe.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_v390_e2e.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_v391_strict_warnings.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_vault_exporter.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_vault_linter_orphan.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_vector_search.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_verbatim_recall.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_verbatim_store.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_verbatim_store_qdrant.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_webhook.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_whatsapp_importer.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_wiki_autopromote.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_wiki_binding.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_wiki_engine_idempotency.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_wiki_explored_and_contradictions.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_wiki_freshness.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_wiki_similarity_multiscope.py +0 -0
- {memorymaster-3.18.0 → memorymaster-3.21.0}/tests/test_wiki_suggest.py +0 -0
- {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.
|
|
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)
|
|
54
54
|
[](https://www.python.org/downloads/)
|
|
55
|
-
[]()
|
|
56
56
|
[]()
|
|
57
57
|
[]()
|
|
58
58
|
[](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
|
-
|
|
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)
|
|
8
8
|
[](https://www.python.org/downloads/)
|
|
9
|
-
[]()
|
|
10
10
|
[]()
|
|
11
11
|
[]()
|
|
12
12
|
[](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
|
-
|
|
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
|
|
|
@@ -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
|
+
)
|