diff --git a/Makefile b/Makefile index 184c44097..fc052dbe3 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ export TMPDIR := /var/tmp PYTEST_BASETEMP_INIT := BASETEMP=$$(mktemp -d /var/tmp/solstone-pytest-XXXXXX); trap 'rm -rf "$$BASETEMP"' EXIT INT TERM; PYTEST_BASETEMP_FLAG := --basetemp "$$BASETEMP" -.PHONY: install hopper-install uninstall test test-cov test-integration test-performance test-app test-only format format-check install-checks ci clean clean-install coverage watch versions update update-prices preflight pre-commit skills render-packaging check-rust-fmt check-rust-msrv check-rust-clippy check-rust-test check-rust-ios check-rust-deny check-rust-release-manifest audit openapi check-openapi check-openapi-observer-client-contract contract check-contract journal-resolution-vectors check-journal-resolution-vectors dev all sandbox sandbox-stop install-models parakeet-helper parakeet-helper-clean wheel-macos wheel-macos-clean verify verify-api verify-schemathesis update-api-baselines eval-schemas service-logs check-layer-hygiene check-api-conventions check-journal-io-access check-journal-io-mechanic check-journal-config-owner check-call-http-only check-no-legacy-chat check-tools-http-only check-access-imports-clean check-convey-bind-imports-clean check-schema-bounds check-thin-base-install check-cogitate-prompts smoke-cogitate release release-test FORCE +.PHONY: install hopper-install uninstall test test-cov test-integration test-performance test-app test-only format format-check install-checks ci clean clean-install coverage watch versions update update-prices preflight pre-commit skills render-packaging check-rust-fmt check-rust-msrv check-rust-clippy check-rust-test check-rust-ios check-rust-deny check-rust-release-manifest audit openapi check-openapi check-openapi-observer-client-contract contract check-contract journal-resolution-vectors check-journal-resolution-vectors dev all sandbox sandbox-stop install-models parakeet-helper parakeet-helper-clean wheel-macos wheel-macos-clean verify verify-api verify-schemathesis update-api-baselines eval-schemas service-logs check-layer-hygiene check-api-conventions check-journal-io-access check-journal-io-mechanic check-journal-config-owner check-call-http-only check-no-legacy-chat check-brain-health-cutover check-tools-http-only check-access-imports-clean check-convey-bind-imports-clean check-schema-bounds check-thin-base-install check-cogitate-prompts smoke-cogitate release release-test FORCE # Default target - install package in editable mode all: install @@ -500,6 +500,9 @@ install-checks: .installed @echo "=== Running legacy-chat surface check ===" @$(MAKE) check-no-legacy-chat @echo "" + @echo "=== Running brain-health cutover check ===" + @$(MAKE) check-brain-health-cutover + @echo "" @echo "=== Running schema-bounds check ===" @$(MAKE) check-schema-bounds @echo "" @@ -650,6 +653,10 @@ check-call-http-only: .installed check-no-legacy-chat: .installed $(VENV_BIN)/python scripts/check_no_legacy_chat.py +# Brain health cutover guard +check-brain-health-cutover: .installed + $(VENV_BIN)/python scripts/check_brain_health_cutover.py + # Generation schema bounds ratchet check-schema-bounds: .installed $(VENV_BIN)/python scripts/check_schema_bounds.py diff --git a/scripts/check_brain_health_cutover.py b/scripts/check_brain_health_cutover.py new file mode 100644 index 000000000..b48663b76 --- /dev/null +++ b/scripts/check_brain_health_cutover.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc + +"""Guard the active-brain health cutover.""" + +from __future__ import annotations + +import argparse +import ast +import re +import subprocess +from dataclasses import dataclass +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SOURCE_SUFFIXES = {".py", ".md", ".js", ".html"} +LEGACY_HEALTH_FILE = "talents" + ".json" +PROVIDER_CLI_TOKEN = "providers" + "_cli" +PROVIDER_WORD = "providers" +CHECK_WORD = "check" +PROVIDER_CHECK_TEXT = PROVIDER_WORD + " " + CHECK_WORD +JOURNAL_PROVIDER_CHECK = "jour" + "nal " + PROVIDER_CHECK_TEXT +SOL_PROVIDER_CHECK = "s" + "ol " + PROVIDER_CHECK_TEXT +OWNER_LABELS = ("Provider " + "Readiness", "Agents " + "Health") +LEGACY_QUOTED_KEYS = ( + '"' + "provider" + "_readiness" + '"', + "'" + "provider" + "_readiness" + "'", + '"' + "ai" + "_readiness" + '"', + "'" + "ai" + "_readiness" + "'", +) +COMMAND_LIST_RE = re.compile( + r"\[\s*['\"](?:jour" + r"nal|s" + r"ol)['\"]\s*,\s*['\"]providers['\"]\s*,\s*['\"]check['\"]" +) +BRAIN_READER_ALLOWLIST = { + "solstone/apps/health/routes.py", + "solstone/apps/home/routes.py", + "solstone/apps/support/diagnostics.py", + "solstone/apps/thinking/routes.py", + "solstone/think/brain_cli.py", + "solstone/think/brain_health.py", + "solstone/think/cortex.py", + "solstone/think/doctor.py", + "solstone/think/surfaces/health.py", + "solstone/think/top.py", +} + + +@dataclass(frozen=True) +class Finding: + path: str + rule: str + detail: str + + +def _tracked_files(root: Path, *, all_files: bool) -> list[Path]: + if all_files: + return [ + path + for path in root.rglob("*") + if path.is_file() and path.suffix in SOURCE_SUFFIXES + ] + result = subprocess.run( + ["git", "ls-files"], + cwd=root, + check=True, + capture_output=True, + text=True, + ) + files: list[Path] = [] + for line in result.stdout.splitlines(): + if not line or Path(line).suffix not in SOURCE_SUFFIXES: + continue + path = root / line + if path.is_file(): + files.append(path) + return files + + +def _rel(root: Path, path: Path) -> str: + return path.relative_to(root).as_posix() + + +def _is_test_path(rel: str) -> bool: + parts = Path(rel).parts + return "tests" in parts or Path(rel).name.startswith("test_") + + +def _contains_command_list(path: Path, text: str) -> bool: + if path.suffix == ".py": + try: + tree = ast.parse(text, filename=str(path)) + except SyntaxError: + return False + for node in ast.walk(tree): + if not isinstance(node, (ast.List, ast.Tuple)) or len(node.elts) < 3: + continue + values = [ + item.value if isinstance(item, ast.Constant) else None + for item in node.elts[:3] + ] + if values in ( + ["journal", "providers", "check"], + ["sol", "providers", "check"], + ): + return True + return False + return COMMAND_LIST_RE.search(text) is not None + + +def _imported_names(path: Path) -> set[str]: + if path.suffix != ".py": + return set() + try: + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + except SyntaxError: + return set() + names: set[str] = set() + for node in ast.walk(tree): + if isinstance(node, ast.ImportFrom): + for alias in node.names: + names.add(alias.name) + elif isinstance(node, ast.Import): + for alias in node.names: + names.add(alias.name.rsplit(".", 1)[-1]) + return names + + +def scan(root: Path, *, all_files: bool = False) -> list[Finding]: + findings: list[Finding] = [] + for path in _tracked_files(root, all_files=all_files): + rel = _rel(root, path) + try: + text = path.read_text(encoding="utf-8") + except UnicodeDecodeError: + continue + + if LEGACY_HEALTH_FILE in text: + findings.append(Finding(rel, "legacy-health-file", LEGACY_HEALTH_FILE)) + + if PROVIDER_CLI_TOKEN in text: + findings.append(Finding(rel, "legacy-provider-cli", PROVIDER_CLI_TOKEN)) + if JOURNAL_PROVIDER_CHECK in text or SOL_PROVIDER_CHECK in text: + findings.append( + Finding(rel, "legacy-provider-check-text", PROVIDER_CHECK_TEXT) + ) + if _contains_command_list(path, text): + findings.append( + Finding(rel, "legacy-provider-check-cmd", PROVIDER_CHECK_TEXT) + ) + + for label in OWNER_LABELS: + if label in text: + findings.append(Finding(rel, "legacy-owner-label", label)) + for key in LEGACY_QUOTED_KEYS: + if key in text: + findings.append(Finding(rel, "legacy-payload-key", key)) + + if rel.startswith(("solstone/", "scripts/")) and not _is_test_path(rel): + imported = _imported_names(path) + if {"inspect_brain_state", "build_brain_snapshot"} & imported: + if rel not in BRAIN_READER_ALLOWLIST: + findings.append( + Finding( + rel, + "unauthorized-brain-health-reader", + ", ".join( + sorted( + {"inspect_brain_state", "build_brain_snapshot"} + & imported + ) + ), + ) + ) + return findings + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=ROOT) + parser.add_argument( + "--all-files", + action="store_true", + help="Scan source files under --root instead of git-tracked files.", + ) + args = parser.parse_args(argv) + root = args.root.resolve() + findings = scan(root, all_files=args.all_files) + if not findings: + return 0 + print("brain-health cutover guard failed:") + for finding in findings: + print(f" {finding.path}: {finding.rule}: {finding.detail}") + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_tools_http_only.py b/scripts/check_tools_http_only.py index eca85cf33..f390817c7 100755 --- a/scripts/check_tools_http_only.py +++ b/scripts/check_tools_http_only.py @@ -22,7 +22,6 @@ ALLOW_SET: frozenset[str] = frozenset( { "solstone.think.convey_client", "solstone.convey.reasons", - "solstone.convey.readiness_snapshot", "solstone.think.pipeline_health", } ) diff --git a/solstone/think/backup/engine.py b/solstone/think/backup/engine.py index 131d985df..c8abeb8e4 100644 --- a/solstone/think/backup/engine.py +++ b/solstone/think/backup/engine.py @@ -72,13 +72,14 @@ BACKUP_EXCLUDES = ( "*.lock", "*.tmp", ".tmp*", + "brain.json", + "brain.log", "brain-fingerprint.key", + "brain-refresh.lease", "supervisor.ready", "supervisor.start_time", "parakeet-cpp.placement", "scheduler.json", - "talents.json", - "agents.json", ) PRUNE_MAX_REPACK_SIZE = "1G" UNLOCK_TIMEOUT_SECONDS = 5 * 60