brainlayer 1.5.45__tar.gz → 1.5.46__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.
- {brainlayer-1.5.45 → brainlayer-1.5.46}/PKG-INFO +1 -1
- {brainlayer-1.5.45 → brainlayer-1.5.46}/pyproject.toml +1 -1
- {brainlayer-1.5.45 → brainlayer-1.5.46}/server.json +2 -2
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/__init__.py +1 -1
- brainlayer-1.5.46/src/brainlayer/_build.py +1 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/chunk_write.py +63 -1
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/cli/__init__.py +41 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/doctor.py +87 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/hooks/indexer.py +4 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/maintenance.py +9 -1
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/cloud_scrub.py +44 -5
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/secret_scrub.py +20 -7
- brainlayer-1.5.46/src/brainlayer/scrub_at_rest.py +350 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/store.py +8 -2
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/vector_store.py +77 -7
- brainlayer-1.5.45/src/brainlayer/_build.py +0 -1
- {brainlayer-1.5.45 → brainlayer-1.5.46}/.gitignore +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/LICENSE +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/README.md +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/hotlane_brainbar_daemon.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/backup-daily.sh +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/brainlayer-env-run.sh +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/brainlayer.env.example +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/com.brainlayer.backup-daily.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/com.brainlayer.decay.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/com.brainlayer.drain.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/com.brainlayer.enrichment.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/com.brainlayer.health-check.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/com.brainlayer.hotlane-brainbar.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/com.brainlayer.index.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/com.brainlayer.jsonl-backup.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/com.brainlayer.log-cap.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/com.brainlayer.maintenance-nightly.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/com.brainlayer.maintenance-weekly.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/com.brainlayer.observability.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/com.brainlayer.p0-counter.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/com.brainlayer.repair-fts.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/com.brainlayer.t3-ingest.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/com.brainlayer.throughput-watchdog.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/com.brainlayer.tier0-watchdog.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/com.brainlayer.wal-checkpoint.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/com.brainlayer.watch.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/com.etanhey.brainlayer-fleet-watchdog.plist +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/fleet-watchdog.sh +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/install.sh +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/jsonl-backup.sh +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/launchd/throughput-watchdog.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/release-verify-signatures.sh +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/scripts/tier0-watchdog.sh +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/__main__.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/_helpers.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/agent_profiles.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/agent_provenance.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/alarm.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/archive_collapse.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/archive_lifecycle.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/backup_daily.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/backup_retention_invariant.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/badge_state.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/bitemporal.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/brainbar_hybrid_helper.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/cachedelete_purge.swift +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/calibrate.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/chunk_origin.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/chunk_origin_backfill.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/chunk_origin_wipe.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/classify.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/claude_paths.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/cli/wizard.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/cli_new.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/cloud_backfill.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/clustering.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/config.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/content_class.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/correction_judge.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/data/agents_registry.yaml +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/data/sandbox_seeds/skill-eval-baseline.json +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/db_shrink.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/decay.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/decay_backfill.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/decay_job.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/dedupe.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/dedupe_merge.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/deploy_drift.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/drain.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/drain_liveness.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/drive_credentials.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/embeddings.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/engine.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/enrichment_controller.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/eval/__init__.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/eval/abcde_enrich_runner.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/eval/abcde_variants.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/eval/abcde_variants.yaml +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/eval/benchmark.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/eval/enrichment_gold.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/eval/enrichment_graders.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/eval/enrichment_judge.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/eval/enrichment_llm_judge.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/eval/enrichment_quality_benchmark.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/eval/experiment_store.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/eval/fixtures/README.md +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/eval/labeling/enrichment-labeler.html +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/eval/phoenix_gate/__init__.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/eval/phoenix_gate/__main__.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/eval/phoenix_gate/baseline_store.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/eval/phoenix_gate/cli.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/eval/phoenix_gate/models.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/eval/phoenix_gate/phoenix_client.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/eval/phoenix_gate/regression_gate.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/eval/phoenix_gate/triggers.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/fallback_replay.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/git_learning.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/health_check.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/hook_python.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/hooks/__init__.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/import_sweep.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/index_completeness.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/index_new.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/index_watchdog.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/ingest/__init__.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/ingest/codex.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/ingest/t3.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/ingest_denylist.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/ingest_guard.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/isolation_proof.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/job_alerts.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/job_lifecycle_health.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/jobs.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/jsonl_backup.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/kg/__init__.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/kg_cleanup.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/kg_judge.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/kg_promotion.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/kg_repo.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/kg_review_session.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/kg_session_finish.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/kg_session_harvest.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/launchd_primitive.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/lexical_defense.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/lexical_defense_dictionary.json +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/log_cap.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/mcp/__init__.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/mcp/_format.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/mcp/_shared.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/mcp/enrich_handler.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/mcp/entity_handler.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/mcp/palette.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/mcp/search_handler.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/mcp/store_handler.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/mcp/tags_handler.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/mcp_socket_config.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/mcp_stdio_bridge.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/memory_types.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/migrate.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/notification_policy.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/observability_backup.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/observability_surface.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/p0_longitudinal_count.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/parent_death.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/paths.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pause.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/phonetic.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/__init__.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/agent_enrichment.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/analyze_communication.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/batch_extraction.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/brain_graph.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/chat_tags.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/chunk.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/classify.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/cluster_sampling.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/code_intelligence.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/correction_detection.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/correction_mining.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/digest.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/enrichment.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/enrichment_tiers.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/entity_extraction.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/entity_resolution.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/extract.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/extract_claude_desktop.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/extract_corrections.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/extract_markdown.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/extract_whatsapp.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/frustration_mining.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/git_overlay.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/groq.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/kg_extraction.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/kg_extraction_groq.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/longitudinal_analyzer.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/obsidian_export.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/operation_grouping.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/plan_linking.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/rate_limiter.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/relation_backfill.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/sanitize.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/semantic_style.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/sentiment.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/session_enrichment.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/style_embed.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/style_index.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/tag_entity_promotion.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/temporal_chains.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/time_batcher.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/unified_timeline.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/pipeline/write_queue.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/provenance.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/provenance_autosupersede.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/provenance_integration.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/queue_io.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/queue_merge.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/reembed_backfill.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/runtime_store.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/sandbox_db.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/scoping.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/search_fanout.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/search_profile.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/search_repo.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/session_repo.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/setup.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/socket_hygiene.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/source_class_migration.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/spotlight.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/storage.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/system_prompt_guard.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/t3_provenance.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/tag_normalization.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/taxonomy.json +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/telemetry.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/timestamp_iso.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/types.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/wal_checkpoint.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/watcher.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/src/brainlayer/watcher_bridge.py +0 -0
- {brainlayer-1.5.45 → brainlayer-1.5.46}/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.
|
|
3
|
+
Version: 1.5.46
|
|
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
|
|
@@ -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.
|
|
6
|
+
"version": "1.5.46",
|
|
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.
|
|
17
|
+
"version": "1.5.46",
|
|
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.
|
|
5
|
+
__version__ = "1.5.46"
|
|
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 = "87ae86dbec7bc8f824bb742eed1ebac285d9724e"
|
|
@@ -148,6 +148,62 @@ def _table_columns(conn: Any, table: str) -> set[str]:
|
|
|
148
148
|
return {row[1] for row in rows}
|
|
149
149
|
|
|
150
150
|
|
|
151
|
+
def scrub_chunk_values(values: Mapping[str, Any]) -> dict[str, Any]:
|
|
152
|
+
"""Scrub stored text before deriving hashes, previews or merge inputs.
|
|
153
|
+
|
|
154
|
+
JSON fields are decoded/normalized before scanning so escaped credentials
|
|
155
|
+
and mapping keys cannot bypass the at-rest policy. Quarantine remains
|
|
156
|
+
metadata only. Already-scrubbed callers retain their findings.
|
|
157
|
+
"""
|
|
158
|
+
from .pipeline.cloud_scrub import normalize_json_strings
|
|
159
|
+
from .pipeline.secret_scrub import merge_scrub_metadata, scrub_for_storage
|
|
160
|
+
|
|
161
|
+
clean = dict(values)
|
|
162
|
+
identity_fields = {"id", "brick_id", "conversation_id", "superseded_by", "aggregated_into"}
|
|
163
|
+
findings: dict[str, Any] = {}
|
|
164
|
+
body_findings: dict[str, Any] = {}
|
|
165
|
+
for key, value in clean.items():
|
|
166
|
+
structured = isinstance(value, (dict, list, tuple))
|
|
167
|
+
if structured:
|
|
168
|
+
value = json.dumps(value, ensure_ascii=False)
|
|
169
|
+
if not isinstance(value, str):
|
|
170
|
+
continue
|
|
171
|
+
if key != "content" and key not in identity_fields:
|
|
172
|
+
value = normalize_json_strings(value)
|
|
173
|
+
text, found = scrub_for_storage(value)
|
|
174
|
+
if key in identity_fields and text != value:
|
|
175
|
+
raise ValueError("chunk identifier requires redaction; refusing to change identity")
|
|
176
|
+
clean[key] = json.loads(text) if structured else text
|
|
177
|
+
# Quarantine count describes the body, not keys/derived placeholders.
|
|
178
|
+
findings = merge_scrub_metadata(
|
|
179
|
+
findings, {k: v for k, v in found.items() if k != "secret_scrub_quarantine_count"}
|
|
180
|
+
)
|
|
181
|
+
if key == "content":
|
|
182
|
+
body_findings = found
|
|
183
|
+
if findings or body_findings:
|
|
184
|
+
metadata = clean.get("metadata")
|
|
185
|
+
if metadata is None:
|
|
186
|
+
metadata = {}
|
|
187
|
+
if isinstance(metadata, str):
|
|
188
|
+
try:
|
|
189
|
+
metadata = json.loads(metadata)
|
|
190
|
+
except ValueError:
|
|
191
|
+
metadata = {"value": metadata}
|
|
192
|
+
if not isinstance(metadata, dict):
|
|
193
|
+
metadata = {"value": metadata}
|
|
194
|
+
if clean.get("content") == values.get("content") and (
|
|
195
|
+
"secret_scrub_quarantine_count" in metadata or "secret_scrub_redactions" in metadata
|
|
196
|
+
):
|
|
197
|
+
body_findings.pop("secret_scrub_quarantine_count", None)
|
|
198
|
+
clean["metadata"] = merge_scrub_metadata(merge_scrub_metadata(metadata, findings), body_findings)
|
|
199
|
+
if clean.get("content") != values.get("content"):
|
|
200
|
+
clean["char_count"] = len(str(clean.get("content") or ""))
|
|
201
|
+
# Caller-supplied dedupe fields describe the pre-scrub text. Recompute.
|
|
202
|
+
for key in ("dedupe_hash", "simhash", *(f"simhash_band_{i}" for i in range(4))):
|
|
203
|
+
clean.pop(key, None)
|
|
204
|
+
return clean
|
|
205
|
+
|
|
206
|
+
|
|
151
207
|
def prepare_canonical_insert(
|
|
152
208
|
values: Mapping[str, Any],
|
|
153
209
|
*,
|
|
@@ -158,6 +214,7 @@ def prepare_canonical_insert(
|
|
|
158
214
|
"""Fill required insert columns. Does not write."""
|
|
159
215
|
from .timestamp_iso import normalize_timestamp
|
|
160
216
|
|
|
217
|
+
values = scrub_chunk_values(values)
|
|
161
218
|
content = str(values.get("content") or "")
|
|
162
219
|
if not content.strip():
|
|
163
220
|
raise ValueError("content must be non-empty")
|
|
@@ -271,7 +328,12 @@ def insert_canonical_chunk(
|
|
|
271
328
|
on_conflict: str = "ignore",
|
|
272
329
|
now: datetime | None = None,
|
|
273
330
|
) -> dict[str, Any]:
|
|
274
|
-
"""Insert
|
|
331
|
+
"""Insert a scrubbed chunk; hashes and dedupe fields describe stored text.
|
|
332
|
+
|
|
333
|
+
Tokens of the same provider collapse to the same placeholder, so dedupe
|
|
334
|
+
treats otherwise identical scrubbed memories as the same content. Existing
|
|
335
|
+
unsanitized rows are not rewritten by this go-forward boundary.
|
|
336
|
+
"""
|
|
275
337
|
columns = _table_columns(conn, "chunks")
|
|
276
338
|
row = prepare_canonical_insert(values, now=now, columns=columns)
|
|
277
339
|
names = [key for key in row if key in columns]
|
|
@@ -1285,6 +1285,8 @@ def doctor_command(
|
|
|
1285
1285
|
f"[bold]{status}[/] chunks={result.chunk_count} "
|
|
1286
1286
|
f"recent_unvectored={result.recent_unvectored_chunks} queue={result.queue_count}"
|
|
1287
1287
|
)
|
|
1288
|
+
if result.cli_path_shadow and result.cli_path_shadow["state"] == "skipped":
|
|
1289
|
+
rprint(f"CLI PATH check skipped: {result.cli_path_shadow['reason']}")
|
|
1288
1290
|
for issue in result.issues:
|
|
1289
1291
|
color = "red" if issue.severity == "fatal" else "yellow"
|
|
1290
1292
|
rprint(f"[{color}]{issue.severity.upper()}[/] {issue.code}: {issue.message}")
|
|
@@ -2865,6 +2867,45 @@ def wal_checkpoint(
|
|
|
2865
2867
|
report_job_alert("wal-checkpoint", None)
|
|
2866
2868
|
|
|
2867
2869
|
|
|
2870
|
+
@app.command("scrub-at-rest")
|
|
2871
|
+
def scrub_at_rest_command(
|
|
2872
|
+
db_path: Path = typer.Option(..., "--db", help="Explicit database path; live apply requires guarded opt-in."),
|
|
2873
|
+
providers: str = typer.Option("google_oauth", "--providers", help="Only google_oauth is supported."),
|
|
2874
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Count matches without writing."),
|
|
2875
|
+
batch_size: int = typer.Option(100, "--batch-size", min=1, max=1000),
|
|
2876
|
+
allow_live_db: bool = typer.Option(
|
|
2877
|
+
False, "--allow-live-db", help="Apply with maintenance safety gates and writer quiescence."
|
|
2878
|
+
),
|
|
2879
|
+
expect_rows: int | None = typer.Option(
|
|
2880
|
+
None, "--expect-rows", min=0, help="Required guarded apply total from the preceding dry run."
|
|
2881
|
+
),
|
|
2882
|
+
) -> None:
|
|
2883
|
+
"""Redact provider credentials on a copy or a guarded runtime DB; print counts only."""
|
|
2884
|
+
from ..scrub_at_rest import scrub_at_rest
|
|
2885
|
+
|
|
2886
|
+
try:
|
|
2887
|
+
result = scrub_at_rest(
|
|
2888
|
+
db_path,
|
|
2889
|
+
providers=providers,
|
|
2890
|
+
dry_run=dry_run,
|
|
2891
|
+
batch_size=batch_size,
|
|
2892
|
+
allow_live_db=allow_live_db,
|
|
2893
|
+
expect_rows=expect_rows,
|
|
2894
|
+
)
|
|
2895
|
+
except Exception as exc:
|
|
2896
|
+
typer.echo(
|
|
2897
|
+
json.dumps(
|
|
2898
|
+
{
|
|
2899
|
+
"error": "at-rest scrub refused or failed",
|
|
2900
|
+
"error_type": type(exc).__name__,
|
|
2901
|
+
"reason": getattr(exc, "reason", "scrub-failed"),
|
|
2902
|
+
}
|
|
2903
|
+
)
|
|
2904
|
+
)
|
|
2905
|
+
raise typer.Exit(1) from None
|
|
2906
|
+
typer.echo(json.dumps(result, sort_keys=True))
|
|
2907
|
+
|
|
2908
|
+
|
|
2868
2909
|
@app.command("repair-fts")
|
|
2869
2910
|
def repair_fts(
|
|
2870
2911
|
db_path: Path | None = typer.Argument(None, help="Offline database copy; omitted means read-only FTS count check."),
|
|
@@ -8,6 +8,9 @@ indexes.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import shlex
|
|
11
14
|
import sqlite3
|
|
12
15
|
import sys
|
|
13
16
|
import time
|
|
@@ -250,11 +253,93 @@ class DoctorResult:
|
|
|
250
253
|
hotlane_running: bool = False
|
|
251
254
|
roundtrip_latency_seconds: float | None = None
|
|
252
255
|
fts5_health: dict[str, Any] | None = None
|
|
256
|
+
cli_path_shadow: dict[str, Any] | None = None
|
|
253
257
|
|
|
254
258
|
def to_dict(self) -> dict[str, Any]:
|
|
255
259
|
return asdict(self)
|
|
256
260
|
|
|
257
261
|
|
|
262
|
+
def _check_cli_path_shadow(result: DoctorResult) -> None:
|
|
263
|
+
"""Keep an advisory PATH probe from aborting the other doctor checks."""
|
|
264
|
+
try:
|
|
265
|
+
_probe_cli_path_shadow(result)
|
|
266
|
+
except Exception as exc:
|
|
267
|
+
reason = f"{type(exc).__name__}: {exc}"
|
|
268
|
+
result.cli_path_shadow = {"state": "warning", "reason": reason}
|
|
269
|
+
result.issues.append(DoctorIssue("cli_path_shadow_check_failed", "warning", reason))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _probe_cli_path_shadow(result: DoctorResult) -> None:
|
|
273
|
+
prefix = Path(sys.prefix).resolve()
|
|
274
|
+
keg = next(
|
|
275
|
+
(p for p in (prefix, *prefix.parents) if p.parent.name == "brainlayer" and p.parent.parent.name == "Cellar"),
|
|
276
|
+
None,
|
|
277
|
+
)
|
|
278
|
+
result.cli_path_shadow = {"state": "skipped", "reason": "running interpreter is not a BrainLayer keg install"}
|
|
279
|
+
if keg is None:
|
|
280
|
+
return
|
|
281
|
+
paths: list[Path] = []
|
|
282
|
+
seen: set[Path] = set()
|
|
283
|
+
probe_failed = False
|
|
284
|
+
for directory in os.environ.get("PATH", "").split(os.pathsep):
|
|
285
|
+
candidate = Path(directory) / "brainlayer"
|
|
286
|
+
try:
|
|
287
|
+
if candidate.is_file() and os.access(candidate, os.X_OK):
|
|
288
|
+
resolved = candidate.resolve()
|
|
289
|
+
if resolved not in seen:
|
|
290
|
+
paths.append(candidate.absolute())
|
|
291
|
+
seen.add(resolved)
|
|
292
|
+
except OSError as exc:
|
|
293
|
+
probe_failed = True
|
|
294
|
+
result.issues.append(DoctorIssue("cli_path_shadow_probe_failed", "warning", f"Skipped {candidate}: {exc}"))
|
|
295
|
+
severity = "fatal" if paths and not paths[0].resolve().is_relative_to(keg) else "warning"
|
|
296
|
+
duplicates = [p for p in paths if not p.resolve().is_relative_to(keg)]
|
|
297
|
+
result.cli_path_shadow = {
|
|
298
|
+
"state": severity if duplicates or not paths or probe_failed else "pass",
|
|
299
|
+
"paths": [str(p) for p in paths],
|
|
300
|
+
"prefix": str(keg),
|
|
301
|
+
}
|
|
302
|
+
if not paths:
|
|
303
|
+
result.issues.append(DoctorIssue("cli_path_shadow", "warning", "No brainlayer executable on PATH"))
|
|
304
|
+
for path in duplicates:
|
|
305
|
+
severity = "fatal" if path == paths[0] else "warning"
|
|
306
|
+
kind, remedy = "plain script", "put the running BrainLayer keg bin directory first on PATH"
|
|
307
|
+
try:
|
|
308
|
+
with path.open() as stream:
|
|
309
|
+
shebang = stream.readline(4096).strip()
|
|
310
|
+
interpreter = Path(shebang[2:].split()[0]) if shebang.startswith("#!/") else None
|
|
311
|
+
if interpreter and interpreter.name.startswith("python"):
|
|
312
|
+
install = interpreter.parent.parent
|
|
313
|
+
metadata = list(install.glob("lib/python*/site-packages/brainlayer-*.dist-info/direct_url.json"))
|
|
314
|
+
version_match = re.search(r"python(\d+\.\d+)$", interpreter.resolve().name)
|
|
315
|
+
version = version_match.group(1) if version_match else None
|
|
316
|
+
user_metadata = []
|
|
317
|
+
if version:
|
|
318
|
+
user_metadata = list(
|
|
319
|
+
(Path.home() / "Library/Python" / version / "lib/python/site-packages").glob(
|
|
320
|
+
"brainlayer-*.dist-info"
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
user_metadata += list(
|
|
324
|
+
(Path.home() / ".local/lib" / f"python{version}/site-packages").glob("brainlayer-*.dist-info")
|
|
325
|
+
)
|
|
326
|
+
if any(json.loads(p.read_text()).get("dir_info", {}).get("editable") is True for p in metadata):
|
|
327
|
+
kind = "editable install"
|
|
328
|
+
env = "PYTHONNOUSERSITE=1 " if user_metadata else ""
|
|
329
|
+
remedy = f"sudo {env}{shlex.quote(str(interpreter.parent / 'pip3'))} uninstall brainlayer"
|
|
330
|
+
if path.is_relative_to(Path.home() / ".local/bin") or path.is_relative_to(
|
|
331
|
+
Path.home() / "Library/Python"
|
|
332
|
+
):
|
|
333
|
+
remedy = f"{shlex.quote(str(interpreter))} -m pip uninstall brainlayer"
|
|
334
|
+
except (OSError, ValueError, TypeError, AttributeError):
|
|
335
|
+
pass
|
|
336
|
+
result.issues.append(
|
|
337
|
+
DoctorIssue(
|
|
338
|
+
"cli_path_shadow", severity, f"{path}: {kind}; {remedy}", {"path": str(path), "remediation": remedy}
|
|
339
|
+
)
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
258
343
|
def _ro_conn(db_path: Path) -> sqlite3.Connection:
|
|
259
344
|
uri = f"file:{db_path.expanduser()}?mode=ro"
|
|
260
345
|
conn = sqlite3.connect(uri, uri=True, timeout=5)
|
|
@@ -581,6 +666,8 @@ def run_doctor(
|
|
|
581
666
|
def warning(code: str, message: str, **details: Any) -> None:
|
|
582
667
|
result.issues.append(DoctorIssue(code, "warning", message, details))
|
|
583
668
|
|
|
669
|
+
_check_cli_path_shadow(result)
|
|
670
|
+
|
|
584
671
|
db_config_error = get_brainlayer_db_config_error()
|
|
585
672
|
if db_config_error is not None:
|
|
586
673
|
fatal(
|
|
@@ -181,6 +181,10 @@ class RealtimeIndexer:
|
|
|
181
181
|
next_index = row[0] if row else 0
|
|
182
182
|
|
|
183
183
|
try:
|
|
184
|
+
from ..pipeline.secret_scrub import scrub_secrets
|
|
185
|
+
|
|
186
|
+
compact_summary = scrub_secrets(compact_summary).text
|
|
187
|
+
trigger = scrub_secrets(trigger).text
|
|
184
188
|
cursor = self._db.execute(
|
|
185
189
|
"""INSERT INTO chapters (session_id, chapter_index, compact_summary, trigger)
|
|
186
190
|
VALUES (?, ?, ?, ?)""",
|
|
@@ -376,6 +376,8 @@ def quarantine_stale_queue_files(
|
|
|
376
376
|
|
|
377
377
|
|
|
378
378
|
def _launchd_label(service: str) -> str:
|
|
379
|
+
if service == "fleet-watchdog":
|
|
380
|
+
return "com.etanhey.brainlayer-fleet-watchdog"
|
|
379
381
|
return f"com.brainlayer.{service}"
|
|
380
382
|
|
|
381
383
|
|
|
@@ -437,6 +439,12 @@ def _verify_enrichment_template_flex_backend(repo_root_or_launchd_dir: Path) ->
|
|
|
437
439
|
|
|
438
440
|
|
|
439
441
|
def _resume_service(repo_root: Path, service: str) -> None:
|
|
442
|
+
if service in {"brainbar", "brainbar-daemon", "fleet-watchdog"}:
|
|
443
|
+
# These already-installed jobs have no normal install.sh service option
|
|
444
|
+
# (or own a different namespace). Restore their existing configuration.
|
|
445
|
+
plist = Path.home() / "Library" / "LaunchAgents" / f"{_launchd_label(service)}.plist"
|
|
446
|
+
run_command(["launchctl", "bootstrap", f"gui/{os.getuid()}", str(plist)], check=True)
|
|
447
|
+
return
|
|
440
448
|
launchd_dir = _launchd_dir_for_resume(repo_root)
|
|
441
449
|
if service == "enrichment":
|
|
442
450
|
_verify_enrichment_template_flex_backend(launchd_dir)
|
|
@@ -582,7 +590,7 @@ def _service_is_deliberately_paused(service: str) -> bool:
|
|
|
582
590
|
payload, active, _stale = pause_sentinel_state(PAUSE_SENTINEL_PATH, datetime.now(UTC))
|
|
583
591
|
if not active:
|
|
584
592
|
return False
|
|
585
|
-
return pause_applies_to_label(payload,
|
|
593
|
+
return pause_applies_to_label(payload, _launchd_label(service))
|
|
586
594
|
|
|
587
595
|
|
|
588
596
|
def _maintenance_keep_down_services() -> set[str]:
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Two directions, both fail closed:
|
|
4
4
|
|
|
5
|
-
- ``scrub_for_cloud`` runs on every text sent to a cloud model.
|
|
6
|
-
|
|
5
|
+
- ``scrub_for_cloud`` runs on every text sent to a cloud model. It
|
|
6
|
+
redacts provider secrets and quarantine spans. Scrub errors or unresolvable
|
|
7
|
+
quarantine records raise ``CloudScrubError`` and the caller must not send.
|
|
7
8
|
- ``scrub_llm_output`` runs on every LLM output value before it is persisted.
|
|
8
9
|
A cloud model copies tokens from its prompt into summaries and key facts,
|
|
9
10
|
so output is scrubbed even when the input already was. If scrubbing raises,
|
|
@@ -32,7 +33,8 @@ class CloudScrubError(RuntimeError):
|
|
|
32
33
|
|
|
33
34
|
def _scrub_text(text: str) -> str:
|
|
34
35
|
try:
|
|
35
|
-
|
|
36
|
+
result = scrub_secrets(text)
|
|
37
|
+
scrubbed = result.text
|
|
36
38
|
except Exception as exc:
|
|
37
39
|
raise CloudScrubError(f"secret scrub failed ({type(exc).__name__}); refusing to pass text on") from None
|
|
38
40
|
if not isinstance(scrubbed, str):
|
|
@@ -41,10 +43,47 @@ def _scrub_text(text: str) -> str:
|
|
|
41
43
|
|
|
42
44
|
|
|
43
45
|
def scrub_for_cloud(text: str) -> str:
|
|
44
|
-
"""
|
|
46
|
+
"""Redact provider secrets and quarantine spans before a remote send.
|
|
47
|
+
|
|
48
|
+
A second pass finds quarantine offsets in the already-redacted text, so
|
|
49
|
+
provider replacements cannot shift them. Each offset must identify its value;
|
|
50
|
+
a mismatch redacts every occurrence, or fails closed if none can be found.
|
|
51
|
+
Storage retains its local-review quarantine policy; cloud prompts redact even
|
|
52
|
+
identifier-like false positives.
|
|
53
|
+
"""
|
|
45
54
|
if not isinstance(text, str):
|
|
46
55
|
raise CloudScrubError(f"remote LLM payload must be text, got {type(text).__name__}")
|
|
47
|
-
|
|
56
|
+
scrubbed = _scrub_text(text)
|
|
57
|
+
try:
|
|
58
|
+
quarantine = scrub_secrets(scrubbed).quarantine
|
|
59
|
+
spans: list[tuple[int, int]] = []
|
|
60
|
+
resolved_values: set[str] = set()
|
|
61
|
+
for token in quarantine:
|
|
62
|
+
if not token.value:
|
|
63
|
+
raise CloudScrubError("quarantine value is empty; refusing to send text")
|
|
64
|
+
if 0 <= token.start < token.end <= len(scrubbed) and scrubbed[token.start : token.end] == token.value:
|
|
65
|
+
spans.append((token.start, token.end))
|
|
66
|
+
elif token.value not in resolved_values:
|
|
67
|
+
start = scrubbed.find(token.value)
|
|
68
|
+
if start < 0:
|
|
69
|
+
raise CloudScrubError("quarantine value not found; refusing to send text")
|
|
70
|
+
while start >= 0:
|
|
71
|
+
end = start + len(token.value)
|
|
72
|
+
spans.append((start, end))
|
|
73
|
+
start = scrubbed.find(token.value, start + 1)
|
|
74
|
+
resolved_values.add(token.value)
|
|
75
|
+
parts: list[str] = []
|
|
76
|
+
cursor = 0
|
|
77
|
+
for start, end in sorted(spans):
|
|
78
|
+
if end <= cursor:
|
|
79
|
+
continue
|
|
80
|
+
if start >= cursor:
|
|
81
|
+
parts.extend((scrubbed[cursor:start], "[REDACTED:quarantine]"))
|
|
82
|
+
cursor = end
|
|
83
|
+
parts.append(scrubbed[cursor:])
|
|
84
|
+
return "".join(parts)
|
|
85
|
+
except Exception as exc:
|
|
86
|
+
raise CloudScrubError(f"secret scrub failed ({type(exc).__name__}); refusing to pass text on") from None
|
|
48
87
|
|
|
49
88
|
|
|
50
89
|
def scrub_llm_output(value: T) -> T:
|
|
@@ -60,6 +60,9 @@ _PROVIDER_PATTERNS = (
|
|
|
60
60
|
),
|
|
61
61
|
_ProviderPattern("slack", re.compile(r"\bxox[baprs]-(?:[A-Za-z0-9]+-){1,}[A-Za-z0-9]{16,}\b")),
|
|
62
62
|
_ProviderPattern("google", re.compile(r"\bAIza[A-Za-z0-9_-]{32,}\b")),
|
|
63
|
+
_ProviderPattern("google_oauth_access", re.compile(r"ya29\.[A-Za-z0-9_.-]{20,}(?![A-Za-z0-9_-])")),
|
|
64
|
+
_ProviderPattern("google_oauth_refresh", re.compile(r"1//[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])")),
|
|
65
|
+
_ProviderPattern("google_client_secret", re.compile(r"GOCSPX-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])")),
|
|
63
66
|
_ProviderPattern("gitlab", re.compile(r"\bglpat-[A-Za-z0-9_-]{20,}\b")),
|
|
64
67
|
_ProviderPattern("supabase", re.compile(r"\b(?:sbp_[A-Za-z0-9]{20,}|sb_secret_[A-Za-z0-9_-]{20,})\b")),
|
|
65
68
|
_ProviderPattern("sendgrid", re.compile(r"\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{32,}\b")),
|
|
@@ -93,20 +96,28 @@ _HEX_RE = re.compile(r"\b[0-9a-fA-F]{16,}\b")
|
|
|
93
96
|
_UUID_RE = re.compile(r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b")
|
|
94
97
|
|
|
95
98
|
|
|
96
|
-
def scrub_secrets(text: str) -> SecretScrubResult:
|
|
97
|
-
"""Redact
|
|
99
|
+
def scrub_secrets(text: str, *, providers: frozenset[str] | None = None) -> SecretScrubResult:
|
|
100
|
+
"""Redact secrets; an explicit provider set selects only those provider spans."""
|
|
101
|
+
if providers is not None and not providers <= {p.provider for p in _PROVIDER_PATTERNS}:
|
|
102
|
+
raise ValueError("unknown secret provider selector")
|
|
98
103
|
if not text:
|
|
99
104
|
return SecretScrubResult(text=text)
|
|
100
105
|
|
|
101
|
-
|
|
106
|
+
# One-off provider selection must see real token boundaries, not window ends.
|
|
107
|
+
windows = [(0, len(text))] if providers is not None else list(_scan_windows(text))
|
|
102
108
|
spans: list[SecretRedaction] = []
|
|
103
109
|
for start, end in windows:
|
|
104
110
|
spans.extend(_provider_redactions(text[start:end], offset=start))
|
|
105
|
-
|
|
106
|
-
spans
|
|
111
|
+
if providers is not None:
|
|
112
|
+
spans = [span for span in spans if span.provider in providers]
|
|
113
|
+
else:
|
|
114
|
+
for start, end in windows:
|
|
115
|
+
spans.extend(_assignment_redactions(text[start:end], spans, offset=start))
|
|
107
116
|
spans = _without_overlaps(sorted(spans, key=lambda item: (item.start, item.end)))
|
|
108
117
|
|
|
109
118
|
scrubbed = _apply_redactions(text, spans) if spans else text
|
|
119
|
+
if providers is not None:
|
|
120
|
+
return SecretScrubResult(text=scrubbed, redactions=spans)
|
|
110
121
|
quarantine: list[QuarantinedToken] = []
|
|
111
122
|
for start, end in windows:
|
|
112
123
|
quarantine.extend(_quarantine_unlabeled_entropy(text[start:end], spans, offset=start))
|
|
@@ -198,8 +209,10 @@ def _quarantine_unlabeled_entropy(
|
|
|
198
209
|
quarantined: list[QuarantinedToken] = []
|
|
199
210
|
redaction_index = _SpanIndex(redactions)
|
|
200
211
|
for match in _TOKEN_RE.finditer(text):
|
|
201
|
-
|
|
202
|
-
|
|
212
|
+
raw_value = match.group(0)
|
|
213
|
+
value = raw_value.strip(".,;)")
|
|
214
|
+
leading_trim = len(raw_value) - len(raw_value.lstrip(".,;)"))
|
|
215
|
+
start = offset + match.start() + leading_trim
|
|
203
216
|
end = start + len(value)
|
|
204
217
|
if redaction_index.overlaps(start, end):
|
|
205
218
|
continue
|