brainlayer 1.5.44__tar.gz → 1.5.45__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 (235) hide show
  1. {brainlayer-1.5.44 → brainlayer-1.5.45}/PKG-INFO +8 -1
  2. {brainlayer-1.5.44 → brainlayer-1.5.45}/README.md +6 -0
  3. {brainlayer-1.5.44 → brainlayer-1.5.45}/pyproject.toml +2 -1
  4. {brainlayer-1.5.44 → brainlayer-1.5.45}/server.json +2 -2
  5. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/__init__.py +1 -1
  6. brainlayer-1.5.45/src/brainlayer/_build.py +1 -0
  7. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/backup_daily.py +17 -61
  8. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/cli/__init__.py +30 -0
  9. brainlayer-1.5.45/src/brainlayer/drive_credentials.py +313 -0
  10. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/health_check.py +6 -0
  11. brainlayer-1.5.45/src/brainlayer/job_alerts.py +91 -0
  12. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/jsonl_backup.py +40 -1
  13. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/maintenance.py +14 -0
  14. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/observability_backup.py +9 -0
  15. brainlayer-1.5.44/src/brainlayer/_build.py +0 -1
  16. {brainlayer-1.5.44 → brainlayer-1.5.45}/.gitignore +0 -0
  17. {brainlayer-1.5.44 → brainlayer-1.5.45}/LICENSE +0 -0
  18. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/hotlane_brainbar_daemon.py +0 -0
  19. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/backup-daily.sh +0 -0
  20. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/brainlayer-env-run.sh +0 -0
  21. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/brainlayer.env.example +0 -0
  22. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.backup-daily.plist +0 -0
  23. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.decay.plist +0 -0
  24. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.drain.plist +0 -0
  25. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.enrichment.plist +0 -0
  26. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.health-check.plist +0 -0
  27. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.hotlane-brainbar.plist +0 -0
  28. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.index.plist +0 -0
  29. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.jsonl-backup.plist +0 -0
  30. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.log-cap.plist +0 -0
  31. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.maintenance-nightly.plist +0 -0
  32. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.maintenance-weekly.plist +0 -0
  33. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.observability.plist +0 -0
  34. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.p0-counter.plist +0 -0
  35. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.repair-fts.plist +0 -0
  36. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.t3-ingest.plist +0 -0
  37. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.throughput-watchdog.plist +0 -0
  38. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.tier0-watchdog.plist +0 -0
  39. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.wal-checkpoint.plist +0 -0
  40. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.brainlayer.watch.plist +0 -0
  41. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/com.etanhey.brainlayer-fleet-watchdog.plist +0 -0
  42. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/fleet-watchdog.sh +0 -0
  43. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/install.sh +0 -0
  44. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/jsonl-backup.sh +0 -0
  45. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/launchd/throughput-watchdog.py +0 -0
  46. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/release-verify-signatures.sh +0 -0
  47. {brainlayer-1.5.44 → brainlayer-1.5.45}/scripts/tier0-watchdog.sh +0 -0
  48. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/__main__.py +0 -0
  49. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/_helpers.py +0 -0
  50. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/agent_profiles.py +0 -0
  51. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/agent_provenance.py +0 -0
  52. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/alarm.py +0 -0
  53. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/archive_collapse.py +0 -0
  54. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/archive_lifecycle.py +0 -0
  55. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/backup_retention_invariant.py +0 -0
  56. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/badge_state.py +0 -0
  57. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/bitemporal.py +0 -0
  58. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/brainbar_hybrid_helper.py +0 -0
  59. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/cachedelete_purge.swift +0 -0
  60. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/calibrate.py +0 -0
  61. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/chunk_origin.py +0 -0
  62. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/chunk_origin_backfill.py +0 -0
  63. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/chunk_origin_wipe.py +0 -0
  64. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/chunk_write.py +0 -0
  65. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/classify.py +0 -0
  66. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/claude_paths.py +0 -0
  67. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/cli/wizard.py +0 -0
  68. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/cli_new.py +0 -0
  69. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/cloud_backfill.py +0 -0
  70. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/clustering.py +0 -0
  71. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/config.py +0 -0
  72. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/content_class.py +0 -0
  73. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/correction_judge.py +0 -0
  74. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/data/agents_registry.yaml +0 -0
  75. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/data/sandbox_seeds/skill-eval-baseline.json +0 -0
  76. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/db_shrink.py +0 -0
  77. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/decay.py +0 -0
  78. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/decay_backfill.py +0 -0
  79. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/decay_job.py +0 -0
  80. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/dedupe.py +0 -0
  81. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/dedupe_merge.py +0 -0
  82. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/deploy_drift.py +0 -0
  83. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/doctor.py +0 -0
  84. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/drain.py +0 -0
  85. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/drain_liveness.py +0 -0
  86. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/embeddings.py +0 -0
  87. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/engine.py +0 -0
  88. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/enrichment_controller.py +0 -0
  89. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/__init__.py +0 -0
  90. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/abcde_enrich_runner.py +0 -0
  91. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/abcde_variants.py +0 -0
  92. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/abcde_variants.yaml +0 -0
  93. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/benchmark.py +0 -0
  94. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/enrichment_gold.py +0 -0
  95. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/enrichment_graders.py +0 -0
  96. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/enrichment_judge.py +0 -0
  97. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/enrichment_llm_judge.py +0 -0
  98. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/enrichment_quality_benchmark.py +0 -0
  99. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/experiment_store.py +0 -0
  100. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/fixtures/README.md +0 -0
  101. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/labeling/enrichment-labeler.html +0 -0
  102. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/__init__.py +0 -0
  103. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/__main__.py +0 -0
  104. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/baseline_store.py +0 -0
  105. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/cli.py +0 -0
  106. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/models.py +0 -0
  107. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/phoenix_client.py +0 -0
  108. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/regression_gate.py +0 -0
  109. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/eval/phoenix_gate/triggers.py +0 -0
  110. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/fallback_replay.py +0 -0
  111. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/git_learning.py +0 -0
  112. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/hook_python.py +0 -0
  113. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/hooks/__init__.py +0 -0
  114. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/hooks/indexer.py +0 -0
  115. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/import_sweep.py +0 -0
  116. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/index_completeness.py +0 -0
  117. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/index_new.py +0 -0
  118. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/index_watchdog.py +0 -0
  119. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/ingest/__init__.py +0 -0
  120. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/ingest/codex.py +0 -0
  121. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/ingest/t3.py +0 -0
  122. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/ingest_denylist.py +0 -0
  123. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/ingest_guard.py +0 -0
  124. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/isolation_proof.py +0 -0
  125. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/job_lifecycle_health.py +0 -0
  126. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/jobs.py +0 -0
  127. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/kg/__init__.py +0 -0
  128. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/kg_cleanup.py +0 -0
  129. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/kg_judge.py +0 -0
  130. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/kg_promotion.py +0 -0
  131. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/kg_repo.py +0 -0
  132. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/kg_review_session.py +0 -0
  133. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/kg_session_finish.py +0 -0
  134. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/kg_session_harvest.py +0 -0
  135. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/launchd_primitive.py +0 -0
  136. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/lexical_defense.py +0 -0
  137. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/lexical_defense_dictionary.json +0 -0
  138. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/log_cap.py +0 -0
  139. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp/__init__.py +0 -0
  140. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp/_format.py +0 -0
  141. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp/_shared.py +0 -0
  142. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp/enrich_handler.py +0 -0
  143. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp/entity_handler.py +0 -0
  144. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp/palette.py +0 -0
  145. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp/search_handler.py +0 -0
  146. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp/store_handler.py +0 -0
  147. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp/tags_handler.py +0 -0
  148. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp_socket_config.py +0 -0
  149. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/mcp_stdio_bridge.py +0 -0
  150. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/memory_types.py +0 -0
  151. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/migrate.py +0 -0
  152. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/notification_policy.py +0 -0
  153. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/observability_surface.py +0 -0
  154. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/p0_longitudinal_count.py +0 -0
  155. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/parent_death.py +0 -0
  156. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/paths.py +0 -0
  157. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pause.py +0 -0
  158. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/phonetic.py +0 -0
  159. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/__init__.py +0 -0
  160. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/agent_enrichment.py +0 -0
  161. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/analyze_communication.py +0 -0
  162. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/batch_extraction.py +0 -0
  163. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/brain_graph.py +0 -0
  164. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/chat_tags.py +0 -0
  165. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/chunk.py +0 -0
  166. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/classify.py +0 -0
  167. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/cloud_scrub.py +0 -0
  168. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/cluster_sampling.py +0 -0
  169. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/code_intelligence.py +0 -0
  170. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/correction_detection.py +0 -0
  171. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/correction_mining.py +0 -0
  172. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/digest.py +0 -0
  173. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/enrichment.py +0 -0
  174. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/enrichment_tiers.py +0 -0
  175. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/entity_extraction.py +0 -0
  176. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/entity_resolution.py +0 -0
  177. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/extract.py +0 -0
  178. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/extract_claude_desktop.py +0 -0
  179. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/extract_corrections.py +0 -0
  180. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/extract_markdown.py +0 -0
  181. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/extract_whatsapp.py +0 -0
  182. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/frustration_mining.py +0 -0
  183. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/git_overlay.py +0 -0
  184. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/groq.py +0 -0
  185. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/kg_extraction.py +0 -0
  186. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/kg_extraction_groq.py +0 -0
  187. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/longitudinal_analyzer.py +0 -0
  188. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/obsidian_export.py +0 -0
  189. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/operation_grouping.py +0 -0
  190. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/plan_linking.py +0 -0
  191. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/rate_limiter.py +0 -0
  192. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/relation_backfill.py +0 -0
  193. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/sanitize.py +0 -0
  194. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/secret_scrub.py +0 -0
  195. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/semantic_style.py +0 -0
  196. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/sentiment.py +0 -0
  197. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/session_enrichment.py +0 -0
  198. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/style_embed.py +0 -0
  199. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/style_index.py +0 -0
  200. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/tag_entity_promotion.py +0 -0
  201. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/temporal_chains.py +0 -0
  202. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/time_batcher.py +0 -0
  203. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/unified_timeline.py +0 -0
  204. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/pipeline/write_queue.py +0 -0
  205. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/provenance.py +0 -0
  206. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/provenance_autosupersede.py +0 -0
  207. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/provenance_integration.py +0 -0
  208. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/queue_io.py +0 -0
  209. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/queue_merge.py +0 -0
  210. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/reembed_backfill.py +0 -0
  211. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/runtime_store.py +0 -0
  212. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/sandbox_db.py +0 -0
  213. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/scoping.py +0 -0
  214. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/search_fanout.py +0 -0
  215. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/search_profile.py +0 -0
  216. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/search_repo.py +0 -0
  217. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/session_repo.py +0 -0
  218. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/setup.py +0 -0
  219. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/socket_hygiene.py +0 -0
  220. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/source_class_migration.py +0 -0
  221. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/spotlight.py +0 -0
  222. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/storage.py +0 -0
  223. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/store.py +0 -0
  224. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/system_prompt_guard.py +0 -0
  225. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/t3_provenance.py +0 -0
  226. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/tag_normalization.py +0 -0
  227. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/taxonomy.json +0 -0
  228. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/telemetry.py +0 -0
  229. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/timestamp_iso.py +0 -0
  230. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/types.py +0 -0
  231. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/vector_store.py +0 -0
  232. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/wal_checkpoint.py +0 -0
  233. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/watcher.py +0 -0
  234. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/watcher_bridge.py +0 -0
  235. {brainlayer-1.5.44 → brainlayer-1.5.45}/src/brainlayer/writer_telemetry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: brainlayer
3
- Version: 1.5.44
3
+ Version: 1.5.45
4
4
  Summary: Persistent memory MCP server for AI agents — 17 tools, semantic search, knowledge graph, on-device SQLite
5
5
  Project-URL: Homepage, https://brainlayer.etanheyman.com
6
6
  Project-URL: Repository, https://github.com/EtanHey/brainlayer
@@ -23,6 +23,7 @@ Requires-Dist: abydos>=0.5.0
23
23
  Requires-Dist: apsw>=3.45.0
24
24
  Requires-Dist: deprecation>=2.1.0
25
25
  Requires-Dist: google-api-python-client>=2.0.0
26
+ Requires-Dist: google-auth-oauthlib>=1.1.0
26
27
  Requires-Dist: google-auth>=2.0.0
27
28
  Requires-Dist: idna<4,>=2.5
28
29
  Requires-Dist: jsonschema>=4.20.0
@@ -370,6 +371,12 @@ BRAINLAYER_ENRICH_BACKEND=groq brainlayer enrich # Force Groq
370
371
 
371
372
  ## CLI Reference
372
373
 
374
+ ### Backups
375
+
376
+ Run `brainlayer backup auth` once to authorize BrainLayer's Drive backups in your browser.
377
+ The private token lives at `~/.config/brainlayer/drive-tokens.json`; `BRAINLAYER_DRIVE_TOKEN_PATH` overrides it.
378
+ BrainBar uses `brainlayer backup auth --status --json` to check and `brainlayer backup auth --json` to authorize.
379
+
373
380
  ```bash
374
381
  brainlayer setup # Create ~/.config/brainlayer/brainlayer.env
375
382
  brainlayer setup --launchd # Create config and install launchd agents
@@ -293,6 +293,12 @@ BRAINLAYER_ENRICH_BACKEND=groq brainlayer enrich # Force Groq
293
293
 
294
294
  ## CLI Reference
295
295
 
296
+ ### Backups
297
+
298
+ Run `brainlayer backup auth` once to authorize BrainLayer's Drive backups in your browser.
299
+ The private token lives at `~/.config/brainlayer/drive-tokens.json`; `BRAINLAYER_DRIVE_TOKEN_PATH` overrides it.
300
+ BrainBar uses `brainlayer backup auth --status --json` to check and `brainlayer backup auth --json` to authorize.
301
+
296
302
  ```bash
297
303
  brainlayer setup # Create ~/.config/brainlayer/brainlayer.env
298
304
  brainlayer setup --launchd # Create config and install launchd agents
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "brainlayer"
3
- version = "1.5.44"
3
+ version = "1.5.45"
4
4
  description = "Persistent memory MCP server for AI agents — 17 tools, semantic search, knowledge graph, on-device SQLite"
5
5
  license = {text = "Apache-2.0"}
6
6
  readme = "README.md"
@@ -34,6 +34,7 @@ dependencies = [
34
34
  "idna>=2.5,<4", # Imported through requests by backup and hook entrypoints
35
35
  "google-api-python-client>=2.0.0", # Daily DB backup upload to Google Drive
36
36
  "google-auth>=2.0.0", # OAuth refresh for Google Drive backups
37
+ "google-auth-oauthlib>=1.1.0", # Installed-app Drive consent for BrainLayer-owned backup tokens
37
38
  "ranx>=0.3.20", # IR evaluation metrics + significance testing
38
39
  "abydos>=0.5.0", # Beider-Morse phonetic matching for cross-script entity aliases
39
40
  "deprecation>=2.1.0", # Imported by abydos phonetic modules used by prompt hooks
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.etanhey/brainlayer",
4
4
  "title": "BrainLayer",
5
5
  "description": "Persistent memory for AI agents \u2014 local-first semantic search, knowledge graph, and 17 MCP tools with ToolAnnotations. One SQLite file, no cloud, no API keys.",
6
- "version": "1.5.44",
6
+ "version": "1.5.45",
7
7
  "websiteUrl": "https://brainlayer.etanheyman.com",
8
8
  "repository": {
9
9
  "url": "https://github.com/EtanHey/brainlayer",
@@ -14,7 +14,7 @@
14
14
  "registryType": "pypi",
15
15
  "registryBaseUrl": "https://pypi.org",
16
16
  "identifier": "brainlayer",
17
- "version": "1.5.44",
17
+ "version": "1.5.45",
18
18
  "runtimeHint": "pipx",
19
19
  "transport": {
20
20
  "type": "stdio"
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "1.5.44"
5
+ __version__ = "1.5.45"
6
6
  # Git sha the distributed package was built from; stamped by the release build, None in a source tree.
7
7
  # scripts/sprint_gate.py reads it in "keg" mode to prove the served package is the code under test.
8
8
  # No runtime `str | None` annotation: launchd/install.sh imports this under /usr/bin/python3 (3.9).
@@ -0,0 +1 @@
1
+ BUILD_SHA = "d3d00b64768a53e7c534ee82dc9f9938aab65f81"
@@ -36,13 +36,14 @@ from typing import Any
36
36
 
37
37
  import requests
38
38
 
39
+ from . import drive_credentials
39
40
  from .paths import get_db_path
40
41
  from .socket_hygiene import refuse_production_brainbar_socket
41
42
 
42
43
  _sleep = time.sleep
43
44
 
44
- DEFAULT_TOKEN_PATH = Path.home() / ".config" / "google-drive-mcp" / "tokens.json"
45
- DEFAULT_CLIENT_PATH = Path.home() / ".config" / "google-drive-mcp" / "gcp-oauth.keys.json"
45
+ DEFAULT_TOKEN_PATH = drive_credentials.DEFAULT_TOKEN_PATH
46
+ DEFAULT_CLIENT_PATH = drive_credentials.DEFAULT_CLIENT_PATH
46
47
  CANONICAL_MACHINE_ID = "MacBook-Pro"
47
48
  CANONICAL_FOLDER_PARTS = ["Brain Drive", "06_ARCHIVE", "backups", "brainlayer-db"]
48
49
  # Compatibility alias for callers that explicitly target the legacy M4 folder.
@@ -69,7 +70,7 @@ DRIVE_UPLOAD_DEADLINE_FLOOR_ENV = "BRAINLAYER_DRIVE_UPLOAD_DEADLINE_FLOOR_SECOND
69
70
  DRIVE_UPLOAD_MIN_BYTES_PER_SECOND_ENV = "BRAINLAYER_DRIVE_UPLOAD_MIN_BYTES_PER_SECOND"
70
71
  DRIVE_UPLOAD_STALL_MAX_ATTEMPTS_ENV = "BRAINLAYER_DRIVE_UPLOAD_STALL_MAX_ATTEMPTS"
71
72
  DRIVE_FOLDER_MIME = "application/vnd.google-apps.folder"
72
- DRIVE_SCOPES = ["https://www.googleapis.com/auth/drive"]
73
+ DRIVE_SCOPES = drive_credentials.DRIVE_SCOPES
73
74
  DEFAULT_LOCAL_COMPRESSED_KEEP = 3
74
75
  DEFAULT_LOCAL_UNCOMPRESSED_KEEP = 1
75
76
  DEFAULT_DRIVE_KEEP = 7
@@ -986,65 +987,12 @@ def _send_brainbar_json_request(
986
987
  return json.loads(data.decode("utf-8"))
987
988
 
988
989
 
989
- def _atomic_write_text(path: Path, content: str) -> None:
990
- temp_path = path.with_name(f".{path.name}.{os.getpid()}.tmp")
991
- try:
992
- temp_path.write_text(content)
993
- os.replace(temp_path, path)
994
- finally:
995
- temp_path.unlink(missing_ok=True)
996
-
997
-
998
- def get_drive_credentials(token_path: Path = DEFAULT_TOKEN_PATH, client_path: Path = DEFAULT_CLIENT_PATH):
999
- """Load and refresh Google Drive OAuth credentials from the existing MCP auth files."""
1000
- from google.auth.transport.requests import Request
1001
- from google.oauth2.credentials import Credentials
1002
-
1003
- token_path = Path(token_path).expanduser()
1004
- client_path = Path(client_path).expanduser()
1005
- if not token_path.exists():
1006
- raise FileNotFoundError(f"Google Drive token file not found: {token_path}")
1007
- if not client_path.exists():
1008
- raise FileNotFoundError(f"Google OAuth client file not found: {client_path}")
1009
-
1010
- lock_path = token_path.with_suffix(token_path.suffix + ".lock")
1011
- with lock_path.open("w") as lock_file:
1012
- fcntl.flock(lock_file, fcntl.LOCK_EX)
1013
- token_data = json.loads(token_path.read_text())
1014
- client_data = json.loads(client_path.read_text())["installed"]
1015
-
1016
- expiry = token_data.get("expiry")
1017
- if not expiry and token_data.get("expiry_date"):
1018
- expiry = dt.datetime.fromtimestamp(int(token_data["expiry_date"]) / 1000, tz=dt.UTC).isoformat()
1019
-
1020
- parsed_expiry = dt.datetime.fromisoformat(expiry.replace("Z", "+00:00")) if expiry else None
1021
- if parsed_expiry and parsed_expiry.tzinfo:
1022
- parsed_expiry = parsed_expiry.astimezone(dt.UTC).replace(tzinfo=None)
1023
- elif parsed_expiry:
1024
- parsed_expiry = parsed_expiry.replace(tzinfo=None)
1025
-
1026
- creds = Credentials(
1027
- token=token_data.get("access_token"),
1028
- refresh_token=token_data.get("refresh_token"),
1029
- token_uri=client_data["token_uri"],
1030
- client_id=client_data["client_id"],
1031
- client_secret=client_data["client_secret"],
1032
- scopes=token_data.get("scope", " ".join(DRIVE_SCOPES)).split(),
1033
- expiry=parsed_expiry,
1034
- )
1035
-
1036
- # google-auth Credentials.expired compares against a naive UTC helper, so keep expiry comparisons naive UTC.
1037
- refresh_before = dt.datetime.now(dt.UTC).replace(tzinfo=None) + dt.timedelta(hours=2)
1038
- if creds.expired or not creds.valid or (creds.expiry and creds.expiry < refresh_before):
1039
- creds.refresh(Request())
1040
- token_data["access_token"] = creds.token
1041
- token_data["expiry"] = creds.expiry.isoformat() if creds.expiry else None
1042
- _atomic_write_text(token_path, json.dumps(token_data, indent=2, sort_keys=True) + "\n")
990
+ def get_drive_credentials(token_path: Path | None = None, client_path: Path | None = None):
991
+ """Use BrainLayer-owned credentials for both database and JSONL backups."""
992
+ return drive_credentials.load_credentials(token_path=token_path, client_path=client_path)
1043
993
 
1044
- return creds
1045
994
 
1046
-
1047
- def build_drive_service(token_path: Path = DEFAULT_TOKEN_PATH, client_path: Path = DEFAULT_CLIENT_PATH):
995
+ def build_drive_service(token_path: Path | None = None, client_path: Path | None = None):
1048
996
  from googleapiclient.discovery import build
1049
997
 
1050
998
  return build("drive", "v3", credentials=get_drive_credentials(token_path, client_path))
@@ -1954,7 +1902,15 @@ def main() -> int:
1954
1902
  timeout_seconds = _configured_backup_timeout_seconds()
1955
1903
  if _env_flag_enabled(BACKUP_SUPERVISED_CHILD_ENV):
1956
1904
  return _run_backup_process(timeout_seconds)
1957
- return _supervise_backup_process(timeout_seconds)
1905
+ from .job_alerts import report
1906
+
1907
+ try:
1908
+ code = _supervise_backup_process(timeout_seconds)
1909
+ except Exception:
1910
+ report("backup-daily", "Database backup failed; check the backup log")
1911
+ raise
1912
+ report("backup-daily", None if code == 0 else "Database backup failed; check the backup log")
1913
+ return code
1958
1914
 
1959
1915
 
1960
1916
  if __name__ == "__main__":
@@ -50,6 +50,25 @@ sandbox_app = typer.Typer(help="Manage isolated sandbox BrainLayer databases")
50
50
  app.add_typer(sandbox_app, name="sandbox")
51
51
  jobs_app = typer.Typer(help="Inspect and restart installed BrainLayer LaunchAgents")
52
52
  app.add_typer(jobs_app, name="jobs")
53
+ backup_app = typer.Typer(help="Manage BrainLayer backup authorization")
54
+ app.add_typer(backup_app, name="backup")
55
+
56
+
57
+ @backup_app.command("auth")
58
+ def backup_auth_command(
59
+ status: bool = typer.Option(False, "--status", help="Check authorization without changing the token."),
60
+ json_output: bool = typer.Option(False, "--json", help="Print one JSON result for BrainBar."),
61
+ ) -> None:
62
+ """Authorize the BrainLayer-owned Google Drive token in the system browser."""
63
+ from .. import drive_credentials
64
+
65
+ result = drive_credentials.credential_status() if status else drive_credentials.authorize()
66
+ if json_output:
67
+ typer.echo(json.dumps(result, sort_keys=True))
68
+ else:
69
+ typer.echo(result["reason"])
70
+ if not status and result["status"] != "ok":
71
+ raise typer.Exit(1)
53
72
 
54
73
 
55
74
  @jobs_app.command("restart")
@@ -2804,6 +2823,7 @@ def wal_checkpoint(
2804
2823
  quiet: bool = typer.Option(False, "--quiet", help="Silent unless error"),
2805
2824
  ) -> None:
2806
2825
  """Run a SQLite WAL checkpoint through the BrainLayer CLI."""
2826
+ from ..job_alerts import report as report_job_alert
2807
2827
  from ..wal_checkpoint import run_wal_checkpoint
2808
2828
 
2809
2829
  normalized_mode = mode.upper()
@@ -2815,12 +2835,14 @@ def wal_checkpoint(
2815
2835
  run_wal_checkpoint(normalized_mode, retry_busy=True) if retry_busy else run_wal_checkpoint(normalized_mode)
2816
2836
  )
2817
2837
  except FileNotFoundError as e:
2838
+ report_job_alert("wal-checkpoint", "BrainLayer WAL checkpoint failed; check the job log")
2818
2839
  if json_output:
2819
2840
  console.print_json(data={"error": str(e)})
2820
2841
  elif not quiet:
2821
2842
  rprint(f"[bold red]Error:[/] {e}")
2822
2843
  raise typer.Exit(1)
2823
2844
  except Exception as e:
2845
+ report_job_alert("wal-checkpoint", "BrainLayer WAL checkpoint failed; check the job log")
2824
2846
  if json_output:
2825
2847
  console.print_json(data={"error": str(e)})
2826
2848
  elif not quiet:
@@ -2838,7 +2860,9 @@ def wal_checkpoint(
2838
2860
  )
2839
2861
 
2840
2862
  if result["busy"]:
2863
+ report_job_alert("wal-checkpoint", "BrainLayer WAL checkpoint was busy; check the job log")
2841
2864
  raise typer.Exit(1)
2865
+ report_job_alert("wal-checkpoint", None)
2842
2866
 
2843
2867
 
2844
2868
  @app.command("repair-fts")
@@ -2846,6 +2870,7 @@ def repair_fts(
2846
2870
  db_path: Path | None = typer.Argument(None, help="Offline database copy; omitted means read-only FTS count check."),
2847
2871
  ) -> None:
2848
2872
  """Check live FTS counts read-only, or rebuild an explicit offline copy."""
2873
+ from ..job_alerts import report as report_job_alert
2849
2874
  from ..runtime_store import OfflineMigrator, check_fts_counts
2850
2875
 
2851
2876
  try:
@@ -2858,8 +2883,13 @@ def repair_fts(
2858
2883
  finally:
2859
2884
  store.close()
2860
2885
  except PermissionError as exc:
2886
+ report_job_alert("repair-fts", "BrainLayer FTS repair failed; check the job log")
2861
2887
  rprint(f"[bold red]Repair refused:[/] {exc}")
2862
2888
  raise typer.Exit(1) from exc
2889
+ except Exception:
2890
+ report_job_alert("repair-fts", "BrainLayer FTS repair failed; check the job log")
2891
+ raise
2892
+ report_job_alert("repair-fts", None)
2863
2893
  console.print_json(data=result)
2864
2894
 
2865
2895
 
@@ -0,0 +1,313 @@
1
+ """BrainLayer-owned Google Drive credentials shared by both backup jobs.
2
+
3
+ The MCP's token is a migration source only. Backup refreshes and interactive auth
4
+ write solely to BrainLayer's private token path.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import contextlib
10
+ import datetime as dt
11
+ import fcntl
12
+ import json
13
+ import logging
14
+ import os
15
+ import socket
16
+ import stat
17
+ import tempfile
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from . import job_alerts
22
+
23
+ DRIVE_SCOPES = ["https://www.googleapis.com/auth/drive"]
24
+ TOKEN_ENV = "BRAINLAYER_DRIVE_TOKEN_PATH"
25
+ CLIENT_ENV = "BRAINLAYER_DRIVE_CLIENT_PATH"
26
+ DEFAULT_TOKEN_PATH = Path.home() / ".config/brainlayer/drive-tokens.json"
27
+ DEFAULT_CLIENT_PATH = Path.home() / ".config/google-drive-mcp/gcp-oauth.keys.json"
28
+ LEGACY_TOKEN_PATH = Path.home() / ".config/google-drive-mcp/tokens.json"
29
+ LOGGER = logging.getLogger(__name__)
30
+
31
+
32
+ def _report_consent(reporter: Any, reason: str | None) -> None:
33
+ try:
34
+ reporter("drive-consent", reason)
35
+ except OSError:
36
+ LOGGER.warning("Drive consent alert could not be recorded")
37
+
38
+
39
+ class DriveCredentialError(RuntimeError):
40
+ """A payload-free credential failure safe for logs and notifications."""
41
+
42
+
43
+ def resolve_paths(*, environ: dict[str, str] | None = None, home: Path | None = None) -> tuple[Path, Path, Path]:
44
+ environ = os.environ if environ is None else environ
45
+ home = Path.home() if home is None else Path(home)
46
+ token = Path(environ.get(TOKEN_ENV) or home / ".config/brainlayer/drive-tokens.json").expanduser()
47
+ client = Path(environ.get(CLIENT_ENV) or home / ".config/google-drive-mcp/gcp-oauth.keys.json").expanduser()
48
+ legacy = home / ".config/google-drive-mcp/tokens.json"
49
+ return token, client, legacy
50
+
51
+
52
+ def _paths(
53
+ token_path: Path | None, client_path: Path | None, legacy_token_path: Path | None
54
+ ) -> tuple[Path, Path, Path]:
55
+ defaults = resolve_paths()
56
+ return tuple(
57
+ Path(path).expanduser() if path is not None else default
58
+ for path, default in zip((token_path, client_path, legacy_token_path), defaults, strict=True)
59
+ )
60
+
61
+
62
+ def _read_object(path: Path, *, private: bool) -> dict[str, Any]:
63
+ try:
64
+ fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
65
+ except FileNotFoundError:
66
+ raise DriveCredentialError("BrainLayer Drive token missing; run brainlayer backup auth") from None
67
+ except OSError:
68
+ raise DriveCredentialError("Drive credential file cannot be read") from None
69
+ try:
70
+ metadata = os.fstat(fd)
71
+ if not stat.S_ISREG(metadata.st_mode) or metadata.st_uid != os.getuid():
72
+ raise DriveCredentialError("Drive credential file must be a regular file owned by this user")
73
+ if private and stat.S_IMODE(metadata.st_mode) != 0o600:
74
+ raise DriveCredentialError("BrainLayer Drive token must have mode 0600")
75
+ with os.fdopen(fd, "r", encoding="utf-8") as stream:
76
+ fd = -1
77
+ payload = json.load(stream)
78
+ if not isinstance(payload, dict):
79
+ raise ValueError("not an object")
80
+ return payload
81
+ except (UnicodeError, ValueError, OSError):
82
+ raise DriveCredentialError("Drive credential file is invalid") from None
83
+ finally:
84
+ if fd >= 0:
85
+ os.close(fd)
86
+
87
+
88
+ def _atomic_write(path: Path, payload: dict[str, Any]) -> None:
89
+ path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
90
+ fd, name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
91
+ try:
92
+ with os.fdopen(fd, "w", encoding="utf-8") as stream:
93
+ os.fchmod(stream.fileno(), 0o600)
94
+ json.dump(payload, stream, indent=2, sort_keys=True)
95
+ stream.write("\n")
96
+ stream.flush()
97
+ os.fsync(stream.fileno())
98
+ os.replace(name, path)
99
+ finally:
100
+ # Only an uncommitted temporary file is removable; a token is never deleted.
101
+ if os.path.exists(name):
102
+ os.unlink(name)
103
+
104
+
105
+ @contextlib.contextmanager
106
+ def _token_lock(path: Path):
107
+ path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
108
+ lock_path = path.with_suffix(path.suffix + ".lock")
109
+ fd = os.open(lock_path, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
110
+ try:
111
+ fcntl.flock(fd, fcntl.LOCK_EX)
112
+ yield
113
+ finally:
114
+ fcntl.flock(fd, fcntl.LOCK_UN)
115
+ os.close(fd)
116
+
117
+
118
+ def _migrate_if_missing(token: Path, legacy: Path) -> None:
119
+ if token.exists() or not legacy.exists():
120
+ return
121
+ payload = _read_object(legacy, private=False)
122
+ if not payload.get("access_token") or not payload.get("refresh_token"):
123
+ raise DriveCredentialError("Legacy Drive token is invalid; run brainlayer backup auth")
124
+ _atomic_write(token, payload)
125
+ LOGGER.warning("Copied legacy Google Drive token into BrainLayer-owned storage; legacy token retained")
126
+
127
+
128
+ def _expiry(data: dict[str, Any]) -> dt.datetime | None:
129
+ raw = data.get("expiry")
130
+ if not raw and data.get("expiry_date"):
131
+ raw = dt.datetime.fromtimestamp(int(data["expiry_date"]) / 1000, tz=dt.UTC).isoformat()
132
+ if not raw:
133
+ return None
134
+ parsed = dt.datetime.fromisoformat(str(raw).replace("Z", "+00:00"))
135
+ return parsed.astimezone(dt.UTC).replace(tzinfo=None) if parsed.tzinfo else parsed
136
+
137
+
138
+ def load_credentials(
139
+ *,
140
+ token_path: Path | None = None,
141
+ client_path: Path | None = None,
142
+ legacy_token_path: Path | None = None,
143
+ credentials_class: Any = None,
144
+ request_factory: Any = None,
145
+ migrate: bool = True,
146
+ persist_refresh: bool = True,
147
+ now: dt.datetime | None = None,
148
+ alert_reporter: Any = job_alerts.report,
149
+ ):
150
+ """Read/refresh under one lock; never erase the prior token on any failure."""
151
+ if credentials_class is None:
152
+ from google.oauth2.credentials import Credentials
153
+
154
+ credentials_class = Credentials
155
+ if request_factory is None:
156
+ from google.auth.transport.requests import Request
157
+
158
+ request_factory = Request
159
+ token, client, legacy = _paths(token_path, client_path, legacy_token_path)
160
+ with _token_lock(token):
161
+ if migrate:
162
+ _migrate_if_missing(token, legacy)
163
+ token_data = _read_object(token, private=True)
164
+ consented = token_data.get("consented_at")
165
+ if consented:
166
+ try:
167
+ consent_time = dt.datetime.fromisoformat(str(consented).replace("Z", "+00:00")).astimezone(dt.UTC)
168
+ except ValueError:
169
+ raise DriveCredentialError("BrainLayer Drive consent metadata is invalid") from None
170
+ remaining = consent_time + dt.timedelta(days=7) - (now or dt.datetime.now(dt.UTC)).astimezone(dt.UTC)
171
+ if remaining <= dt.timedelta(0):
172
+ _report_consent(alert_reporter, "Drive access expired: Reconnect in BrainBar")
173
+ raise DriveCredentialError("Drive access expired: Reconnect in BrainBar")
174
+ if remaining < dt.timedelta(days=1):
175
+ _report_consent(
176
+ alert_reporter,
177
+ "Google Drive access for BrainLayer backups expires tomorrow: click Reconnect in BrainBar",
178
+ )
179
+ if not token_data.get("refresh_token"):
180
+ raise DriveCredentialError("BrainLayer Drive token lacks refresh authorization; run brainlayer backup auth")
181
+ try:
182
+ client_data = _read_object(client, private=False)["installed"]
183
+ creds = credentials_class(
184
+ token=token_data.get("access_token"),
185
+ refresh_token=token_data.get("refresh_token"),
186
+ token_uri=client_data["token_uri"],
187
+ client_id=client_data["client_id"],
188
+ client_secret=client_data["client_secret"],
189
+ scopes=str(token_data.get("scope") or " ".join(DRIVE_SCOPES)).split(),
190
+ expiry=_expiry(token_data),
191
+ )
192
+ except (KeyError, TypeError, ValueError, DriveCredentialError):
193
+ raise DriveCredentialError("Google OAuth client or BrainLayer token is invalid") from None
194
+ refresh_before = dt.datetime.now(dt.UTC).replace(tzinfo=None) + dt.timedelta(hours=2)
195
+ if creds.expired or not creds.valid or (creds.expiry and creds.expiry < refresh_before):
196
+ try:
197
+ creds.refresh(request_factory())
198
+ except Exception as exc:
199
+ from google.auth.exceptions import TransportError
200
+
201
+ if isinstance(exc, TransportError):
202
+ raise TransportError("Drive token refresh network failure") from None
203
+ if "invalid_grant" in str(exc).lower():
204
+ _report_consent(alert_reporter, "Drive access expired: Reconnect in BrainBar")
205
+ raise DriveCredentialError("Drive access expired: Reconnect in BrainBar") from None
206
+ raise DriveCredentialError("Drive token refresh failed; run brainlayer backup auth") from None
207
+ if persist_refresh:
208
+ token_data["access_token"] = creds.token
209
+ token_data["expiry"] = creds.expiry.isoformat() if creds.expiry else None
210
+ _atomic_write(token, token_data)
211
+ return creds
212
+
213
+
214
+ def credential_status(*, now: dt.datetime | None = None, **kwargs: Any) -> dict[str, Any]:
215
+ """Check the owned token only; do not migrate or write a refreshed token."""
216
+ from google.auth.exceptions import TransportError
217
+
218
+ token, _, _ = _paths(kwargs.get("token_path"), kwargs.get("client_path"), None)
219
+ if not token.exists():
220
+ return {
221
+ "state": "missing",
222
+ "reason": "BrainLayer Drive authorization is missing",
223
+ "expires_at": None,
224
+ "days_left": None,
225
+ }
226
+ try:
227
+ data = _read_object(token, private=True)
228
+ consented = data.get("consented_at")
229
+ try:
230
+ expires = (
231
+ dt.datetime.fromisoformat(str(consented).replace("Z", "+00:00")) + dt.timedelta(days=7)
232
+ if consented
233
+ else None
234
+ )
235
+ if expires and expires.tzinfo is None:
236
+ raise ValueError("consent time has no timezone")
237
+ except ValueError:
238
+ raise DriveCredentialError("BrainLayer Drive consent metadata is invalid") from None
239
+ current = (now or dt.datetime.now(dt.UTC)).astimezone(dt.UTC)
240
+ days_left = max(0.0, (expires - current).total_seconds() / 86400) if expires else None
241
+ timing = {"expires_at": expires.isoformat() if expires else None, "days_left": days_left}
242
+ if expires and current >= expires:
243
+ _report_consent(
244
+ kwargs.get("alert_reporter", job_alerts.report), "Drive access expired: Reconnect in BrainBar"
245
+ )
246
+ return {"state": "invalid", "reason": "Drive access expired: Reconnect in BrainBar", **timing}
247
+ load_credentials(**kwargs, migrate=False, persist_refresh=False, now=now)
248
+ except TransportError:
249
+ return {
250
+ "state": "expiring" if days_left is not None and days_left < 1 else "valid",
251
+ "reason": "status checked offline; last known state",
252
+ **timing,
253
+ }
254
+ except DriveCredentialError as exc:
255
+ return {"state": "invalid", "reason": str(exc), "expires_at": None, "days_left": None}
256
+ if days_left is not None and days_left < 1:
257
+ return {
258
+ "state": "expiring",
259
+ "reason": "Google Drive access for BrainLayer backups expires tomorrow: click Reconnect in BrainBar",
260
+ **timing,
261
+ }
262
+ return {"state": "valid", "reason": "BrainLayer Drive authorization is ready", **timing}
263
+
264
+
265
+ def authorize(
266
+ *,
267
+ token_path: Path | None = None,
268
+ client_path: Path | None = None,
269
+ flow_class: Any = None,
270
+ now: dt.datetime | None = None,
271
+ alert_reporter: Any = job_alerts.report,
272
+ ) -> dict[str, str]:
273
+ """Open Google consent in the system browser and save only BrainLayer's token."""
274
+ from oauthlib.oauth2.rfc6749.errors import AccessDeniedError
275
+
276
+ token, client, _ = _paths(token_path, client_path, None)
277
+ if flow_class is None:
278
+ from google_auth_oauthlib.flow import InstalledAppFlow
279
+
280
+ flow_class = InstalledAppFlow
281
+ try:
282
+ _read_object(client, private=False)
283
+ flow = flow_class.from_client_secrets_file(str(client), scopes=DRIVE_SCOPES)
284
+ # Suppress the consent URL on stdout; BrainBar expects one JSON result.
285
+ creds = flow.run_local_server(
286
+ host="localhost",
287
+ bind_addr="127.0.0.1",
288
+ port=0,
289
+ timeout_seconds=180,
290
+ open_browser=True,
291
+ authorization_prompt_message=None,
292
+ access_type="offline",
293
+ prompt="consent",
294
+ )
295
+ if not creds.refresh_token:
296
+ raise DriveCredentialError("Google did not grant refresh authorization")
297
+ payload = {
298
+ "access_token": creds.token,
299
+ "refresh_token": creds.refresh_token,
300
+ "expiry": creds.expiry.isoformat() if creds.expiry else None,
301
+ "scope": " ".join(DRIVE_SCOPES),
302
+ "consented_at": (now or dt.datetime.now(dt.UTC)).astimezone(dt.UTC).isoformat(),
303
+ }
304
+ with _token_lock(token):
305
+ _atomic_write(token, payload)
306
+ _report_consent(alert_reporter, None)
307
+ return {"status": "ok", "reason": "BrainLayer Drive authorization saved"}
308
+ except (KeyboardInterrupt, AccessDeniedError):
309
+ return {"status": "cancelled", "reason": "Google authorization cancelled"}
310
+ except Exception as exc:
311
+ if isinstance(exc, (socket.timeout, TimeoutError)) or type(exc).__name__ == "WSGITimeoutError":
312
+ return {"status": "timeout", "reason": "Google authorization timed out"}
313
+ return {"status": "error", "reason": "Google authorization failed; retry brainlayer backup auth"}
@@ -121,6 +121,7 @@ class HealthIssue:
121
121
  class HealthCheckConfig:
122
122
  db_path: Path = field(default_factory=get_db_path)
123
123
  state_path: Path = field(default_factory=lambda: DEFAULT_STATE_PATH)
124
+ job_alert_path: Path | None = None
124
125
  badge_state_path: Path | None = None
125
126
  job_opt_path: Path | None = field(default_factory=installed_opt_path)
126
127
  job_plist_dir: Path = field(default_factory=lambda: Path("~/Library/LaunchAgents").expanduser())
@@ -1283,6 +1284,11 @@ def _run_health_check_locked(
1283
1284
  if state.get("_state_corrupt_now"):
1284
1285
  add_issue("job_state_unknown", "critical", str(state["state_corrupt"]))
1285
1286
 
1287
+ from .job_alerts import active_alerts
1288
+
1289
+ for job, reason in active_alerts(config.job_alert_path).items():
1290
+ add_issue(f"job_alert_{job}", "critical", reason)
1291
+
1286
1292
  def publish_badge_state() -> None:
1287
1293
  if config.badge_state_path is None:
1288
1294
  return