diff --git a/solstone/apps/thinking/routes.py b/solstone/apps/thinking/routes.py index 1d319359c..8ca64859b 100644 --- a/solstone/apps/thinking/routes.py +++ b/solstone/apps/thinking/routes.py @@ -54,7 +54,6 @@ from solstone.think.models import ( DEFAULT_MODEL_BY_PROVIDER, LOCAL_MODEL, NO_BRAIN_PROVIDER, - resolve_provider, ) from solstone.think.providers import ( PROVIDER_REGISTRY, @@ -63,9 +62,11 @@ from solstone.think.providers import ( validate_key, validate_model, ) +from solstone.think.providers.brain_state import derive_active_brain_lane from solstone.think.providers.local_endpoint import ( normalize_local_endpoint_url, resolve_local_endpoint, + resolve_local_endpoint_from_config, ) from solstone.think.providers.runtime_health import ( RuntimeHealthConflictError, @@ -221,7 +222,7 @@ def _local_endpoint_public_payload(config: dict[str, Any]) -> dict[str, object]: def _local_override_payload(config: dict[str, Any]) -> dict[str, object]: - endpoint = resolve_local_endpoint() + endpoint = resolve_local_endpoint_from_config(config) local_config = _read_local_provider_config(config) return { "enabled": not endpoint.is_bundled, @@ -270,7 +271,21 @@ def _active_settings(providers_config: dict[str, Any]) -> dict[str, Any]: active_config = providers_config.get("active", {}) if not isinstance(active_config, dict): active_config = {} - provider, model = resolve_provider("generate") + raw_provider = active_config.get("provider") + provider = ( + raw_provider + if isinstance(raw_provider, str) and raw_provider + else NO_BRAIN_PROVIDER + ) + if provider == NO_BRAIN_PROVIDER: + model = "" + else: + raw_model = active_config.get("model") + model = ( + raw_model.strip() + if isinstance(raw_model, str) and raw_model.strip() + else DEFAULT_MODEL_BY_PROVIDER[provider] + ) return { "provider": provider, "model": active_config.get("model") or model, @@ -322,12 +337,13 @@ def _confidential_lane_active_for_config(config: dict[str, Any]) -> bool: def _active_lane_payload( active_settings: dict[str, Any], + config: dict[str, Any], transcribe_config: dict[str, Any], *, presentation: BrainPresentation, confidential_provenance_present: bool, ) -> dict[str, Any]: - endpoint = resolve_local_endpoint() + endpoint = resolve_local_endpoint_from_config(config) local_endpoint_configured = not endpoint.is_bundled active = _lane_for_provider( str(active_settings.get("provider") or ""), @@ -336,9 +352,9 @@ def _active_lane_payload( ) return { "lane": active, - "scout_enabled": scout.is_scout_enabled(), - "scout_provenance_configured": scout.scout_provenance() is not None, - "confidential_enabled": spp.is_confidential_enabled(), + "scout_enabled": scout.is_scout_enabled(config), + "scout_provenance_configured": scout.scout_provenance(config) is not None, + "confidential_enabled": spp.is_confidential_enabled(config), "confidential_audio": confidential_audio_enabled(transcribe_config), "confidential_provenance_configured": confidential_provenance_present, "confidential_operation": _remap_confidential_operation( @@ -380,7 +396,7 @@ def _keys_payload(config: dict[str, Any]) -> dict[str, Any]: "api_keys": _api_key_status(config), "env": _env_key_status(config), "key_validation": _filtered_ai_key_validation(config), - "scout_enabled": scout.is_scout_enabled(), + "scout_enabled": scout.is_scout_enabled(config), } @@ -480,11 +496,14 @@ def _validate_google_model_resolution_targets(value: Any) -> list[str] | Any: def _provider_status_payload( providers_list: list[dict[str, Any]], presentation: BrainPresentation, + *, + config: dict[str, Any], ) -> dict[str, dict[str, Any]]: if not presentation["spp_active"]: - return build_provider_status(providers_list) + return build_provider_status(providers_list, config=config) return build_provider_status( providers_list, + config=config, local_status={ "configured": True, "selected": True, @@ -504,19 +523,27 @@ def _provider_payload(config: dict[str, Any], local_model_id: str) -> dict[str, providers_list = get_provider_list() local_status = local_bootstrap.get_state(local_model_id) - confidential_provenance_present = spp.confidential_provenance() is not None + confidential_provenance_present = ( + spp.confidential_provenance_block(dict(config)) is not None + ) presentation = build_brain_presentation( datetime.now(timezone.utc), surface="thinking", spp_configured=confidential_provenance_present, + config=config, ) return { "providers": providers_list, - "provider_status": _provider_status_payload(providers_list, presentation), + "provider_status": _provider_status_payload( + providers_list, + presentation, + config=config, + ), "brain": presentation["brain"], "active_lane": _active_lane_payload( active_settings, + config, config.get("transcribe", {}) if isinstance(config.get("transcribe", {}), dict) else {}, @@ -533,7 +560,7 @@ def _provider_payload(config: dict[str, Any], local_model_id: str) -> dict[str, "local_runtime": local_recovery.runtime_view(), "local_override": _local_override_payload(config), "local_backend": "mlx" if local_bootstrap._is_mlx_backend() else "local", - "scout_enabled": scout.is_scout_enabled(), + "scout_enabled": scout.is_scout_enabled(config), } @@ -725,18 +752,13 @@ def confidential_disable() -> Any: @thinking_bp.route("/api/confidential/recheck", methods=["POST"]) def confidential_recheck() -> Any: try: - confidential_provenance_present = spp.confidential_provenance() is not None - presentation = build_brain_presentation( - datetime.now(timezone.utc), - surface="thinking", - spp_configured=confidential_provenance_present, - ) - if presentation["confidential_attestation"]["state"] in {"off", "inactive"}: + config = get_journal_config() + if derive_active_brain_lane(config) != "spp": return error_response( INVALID_OPERATION_FOR_STATE, detail="confidential processing is not active.", ) - return _brain_check_response() + return _brain_check_response(config=config) except Exception: logger.exception("error rechecking confidential processing") return _thinking_operation_failed() @@ -1228,10 +1250,14 @@ def get_providers() -> Any: return _thinking_operation_failed() -def _brain_check_response() -> Any: +def _brain_check_response(*, config: dict[str, Any] | None = None) -> Any: ok = request_brain_refresh(surface="thinking") try: - brain = build_brain_snapshot(datetime.now(timezone.utc), surface="thinking") + brain = build_brain_snapshot( + datetime.now(timezone.utc), + surface="thinking", + config=config, + ) except Exception: logger.exception("error loading brain health") return _thinking_operation_failed() @@ -1251,17 +1277,25 @@ def get_local_provider_status() -> Any: """Return local provider readiness status.""" try: - confidential_provenance_present = spp.confidential_provenance() is not None + config = get_journal_config() + confidential_provenance_present = ( + spp.confidential_provenance_block(dict(config)) is not None + ) presentation = build_brain_presentation( datetime.now(timezone.utc), surface="thinking", spp_configured=confidential_provenance_present, + config=config, ) providers_list = get_provider_list() local_provider = next( provider for provider in providers_list if provider["name"] == "local" ) - provider_status = _provider_status_payload([local_provider], presentation) + provider_status = _provider_status_payload( + [local_provider], + presentation, + config=config, + ) return jsonify(provider_status["local"]) except Exception: logger.exception("error loading local provider status") diff --git a/solstone/apps/thinking/tests/test_confidential_attestation_payload.py b/solstone/apps/thinking/tests/test_confidential_attestation_payload.py index faa1319fd..5572b3dbe 100644 --- a/solstone/apps/thinking/tests/test_confidential_attestation_payload.py +++ b/solstone/apps/thinking/tests/test_confidential_attestation_payload.py @@ -202,6 +202,7 @@ def test_active_lane_confidential_attestation_defaults_to_off(settings_env): "state": "off", "reason": "confidential_not_configured", "observed_at": None, + "expires_at": None, } @@ -214,6 +215,7 @@ def test_active_lane_confidential_attestation_configured_but_inactive(settings_e "state": "inactive", "reason": "confidential_not_active", "observed_at": None, + "expires_at": None, } @@ -222,7 +224,12 @@ def test_active_lane_confidential_attestation_uses_canonical_presentation( monkeypatch, ): client = _client(settings_env, confidential=True, active_provider="local") - attestation = {"state": "verified", "reason": None, "observed_at": NOW_ISO} + attestation = { + "state": "verified", + "reason": None, + "observed_at": NOW_ISO, + "expires_at": EXPIRES_ISO, + } monkeypatch.setattr( routes, "build_brain_presentation", @@ -238,6 +245,7 @@ def test_active_lane_confidential_attestation_uses_canonical_presentation( "state", "reason", "observed_at", + "expires_at", } serialized = response.get_data(as_text=True) assert "last_verified" not in serialized @@ -251,11 +259,13 @@ def test_active_lane_confidential_attestation_uses_canonical_presentation( "state": "verifying", "reason": "brain_check_in_progress", "observed_at": None, + "expires_at": None, }, { "state": "verifying", "reason": "brain_check_in_progress", "observed_at": None, + "expires_at": None, }, ), ( @@ -263,11 +273,13 @@ def test_active_lane_confidential_attestation_uses_canonical_presentation( "state": "unreachable", "reason": "attestation_not_verified", "observed_at": NOW_ISO, + "expires_at": None, }, { "state": "unreachable", "reason": "attestation_not_verified", "observed_at": NOW_ISO, + "expires_at": None, }, ), ( @@ -275,20 +287,42 @@ def test_active_lane_confidential_attestation_uses_canonical_presentation( "state": "failed", "reason": "attestation_rejected", "observed_at": NOW_ISO, + "expires_at": None, }, { "state": "failed", "reason": "attestation_rejected", "observed_at": NOW_ISO, + "expires_at": None, }, ), ( - {"state": "stale", "reason": "attestation_expired", "observed_at": NOW_ISO}, - {"state": "stale", "reason": "attestation_expired", "observed_at": NOW_ISO}, + { + "state": "stale", + "reason": "attestation_expired", + "observed_at": NOW_ISO, + "expires_at": EXPIRES_ISO, + }, + { + "state": "stale", + "reason": "attestation_expired", + "observed_at": NOW_ISO, + "expires_at": EXPIRES_ISO, + }, ), ( - {"state": "stale", "reason": "brain_record_stale", "observed_at": None}, - {"state": "stale", "reason": "brain_record_stale", "observed_at": None}, + { + "state": "stale", + "reason": "brain_record_stale", + "observed_at": None, + "expires_at": None, + }, + { + "state": "stale", + "reason": "brain_record_stale", + "observed_at": None, + "expires_at": None, + }, ), ], ) @@ -320,6 +354,7 @@ def test_route_serializes_closed_attestation_view( "state": "off", "reason": "confidential_not_configured", "observed_at": None, + "expires_at": None, }, ), ( @@ -329,6 +364,7 @@ def test_route_serializes_closed_attestation_view( "state": "inactive", "reason": "confidential_not_active", "observed_at": None, + "expires_at": None, }, ), ( @@ -338,6 +374,7 @@ def test_route_serializes_closed_attestation_view( "state": "verifying", "reason": "brain_check_in_progress", "observed_at": None, + "expires_at": None, }, ), ( @@ -347,7 +384,12 @@ def test_route_serializes_closed_attestation_view( record=_record(generate=_component("failed", "provider_unavailable")), ), True, - {"state": "verified", "reason": None, "observed_at": NOW_ISO}, + { + "state": "verified", + "reason": None, + "observed_at": NOW_ISO, + "expires_at": EXPIRES_ISO, + }, ), ( _inspection( @@ -367,6 +409,7 @@ def test_route_serializes_closed_attestation_view( "state": "unreachable", "reason": "attestation_not_verified", "observed_at": NOW_ISO, + "expires_at": None, }, ), ( @@ -384,6 +427,7 @@ def test_route_serializes_closed_attestation_view( "state": "failed", "reason": "attestation_rejected", "observed_at": NOW_ISO, + "expires_at": None, }, ), ( @@ -397,7 +441,12 @@ def test_route_serializes_closed_attestation_view( ), ), True, - {"state": "stale", "reason": "attestation_expired", "observed_at": NOW_ISO}, + { + "state": "stale", + "reason": "attestation_expired", + "observed_at": NOW_ISO, + "expires_at": None, + }, ), ( _inspection( @@ -406,7 +455,12 @@ def test_route_serializes_closed_attestation_view( record=None, ), True, - {"state": "stale", "reason": "brain_record_missing", "observed_at": None}, + { + "state": "stale", + "reason": "brain_record_missing", + "observed_at": None, + "expires_at": None, + }, ), ], ) diff --git a/solstone/apps/thinking/tests/test_confidential_routes.py b/solstone/apps/thinking/tests/test_confidential_routes.py index 45e7043ac..9d6a6593a 100644 --- a/solstone/apps/thinking/tests/test_confidential_routes.py +++ b/solstone/apps/thinking/tests/test_confidential_routes.py @@ -204,6 +204,7 @@ def test_enable_confidential_returns_operation_and_lands_not_verified( "state": "stale", "reason": "brain_record_missing", "observed_at": None, + "expires_at": None, } @@ -234,6 +235,7 @@ def test_enable_confidential_early_access_stays_off( "state": "off", "reason": "confidential_not_configured", "observed_at": None, + "expires_at": None, } @@ -702,6 +704,7 @@ def test_recheck_confidential_rejects_when_inactive_before_refresh( "state": "inactive", "reason": "confidential_not_active", "observed_at": None, + "expires_at": None, }, }, ) @@ -717,6 +720,35 @@ def test_recheck_confidential_rejects_when_inactive_before_refresh( assert response.get_json()["reason_code"] == "invalid_operation_for_state" +@pytest.mark.parametrize( + ("field", "value"), + [ + ("endpoint_url", "https://other.example.test"), + ("served_model_id", "other-model"), + ("credential", "other-credential"), + ], +) +def test_recheck_confidential_rejects_mismatched_canonical_spp_binding( + thinking_client, + journal_copy: Path, + monkeypatch: pytest.MonkeyPatch, + field: str, + value: str, +) -> None: + spp.provision_confidential_handoff(_payload("mismatch")) + config = _read_config(journal_copy) + config["providers"]["local"][field] = value + _write_config(config) + refresh = Mock(side_effect=AssertionError("brain refresh attempted")) + monkeypatch.setattr(routes, "request_brain_refresh", refresh) + + response = thinking_client.post("/app/thinking/api/confidential/recheck") + + assert response.status_code == 400 + assert response.get_json()["reason_code"] == "invalid_operation_for_state" + refresh.assert_not_called() + + def test_recheck_confidential_returns_brain_check_response( thinking_client, monkeypatch: pytest.MonkeyPatch, diff --git a/solstone/apps/thinking/tests/test_providers_payload_extended.py b/solstone/apps/thinking/tests/test_providers_payload_extended.py index 0fd49e7a9..f42777f03 100644 --- a/solstone/apps/thinking/tests/test_providers_payload_extended.py +++ b/solstone/apps/thinking/tests/test_providers_payload_extended.py @@ -246,6 +246,7 @@ def _presentation( "state": "off", "reason": "confidential_not_configured", "observed_at": None, + "expires_at": None, }, } @@ -599,6 +600,7 @@ def test_thinking_status_payloads_are_secret_free_with_scout_provenance( "state": "inactive", "reason": "confidential_not_active", "observed_at": None, + "expires_at": None, }, } @@ -1330,7 +1332,7 @@ def test_get_providers_uses_state_local_status(settings_client, monkeypatch): } monkeypatch.setattr( "solstone.think.providers.state.local_status_dict", - lambda: sentinel, + lambda **_kwargs: sentinel, ) response = settings_client.get("/app/thinking/api/providers") @@ -1388,6 +1390,146 @@ def test_get_providers_uses_canonical_spp_local_status_without_probe( assert _journal_tree_snapshot(journal_path) == before +@pytest.mark.parametrize("configuration", ["cloud", "bundled", "byo", "spp"]) +def test_get_providers_uses_one_config_snapshot_across_lanes( + settings_client_with_journal, + monkeypatch, + configuration, +): + from solstone.think import brain_health, models + from solstone.think import journal_config as journal_config_module + from solstone.think.providers import brain_state, local_endpoint + from solstone.think.services import scout, spp + + client, journal_path = settings_client_with_journal + config_path = journal_path / "config" / "journal.json" + config = json.loads(config_path.read_text(encoding="utf-8")) + config.setdefault("services", {}).pop("confidential", None) + if configuration == "cloud": + config["providers"]["active"] = { + "provider": "google", + "model": "gemini-3.5-flash", + } + config["providers"]["local"] = {} + elif configuration == "bundled": + config["providers"]["active"] = { + "provider": "local", + "model": LOCAL_MODEL, + } + config["providers"]["local"] = {} + elif configuration == "byo": + config["providers"]["active"] = { + "provider": "local", + "model": LOCAL_MODEL, + } + config["providers"]["local"] = { + "endpoint_url": "https://byo.example.test", + "served_model_id": "byo-model", + "credential": "byo-secret", + } + else: + config.update(_spp_configured_provider_config()) + _write_config(journal_path, config) + + reads: list[dict] = [] + + def read_once() -> dict: + reads.append(config) + assert len(reads) == 1 + return config + + inspections: list[dict] = [] + original_inspect = brain_health.inspect_brain_state + + def inspect_once(*args, **kwargs): + inspections.append(kwargs["config"]) + assert kwargs["config"] is config + return original_inspect(*args, **kwargs) + + def fail_reread(*_args, **_kwargs): + raise AssertionError("second config authority reached") + + monkeypatch.setattr(routes, "get_journal_config", read_once) + monkeypatch.setattr(brain_health, "inspect_brain_state", inspect_once) + monkeypatch.setattr(brain_state, "read_journal_config", fail_reread) + monkeypatch.setattr(local_endpoint, "read_journal_config", fail_reread) + monkeypatch.setattr(scout, "read_journal_config", fail_reread) + monkeypatch.setattr(spp, "read_journal_config", fail_reread) + monkeypatch.setattr(journal_config_module, "read_journal_config", fail_reread) + monkeypatch.setattr(models, "get_config", fail_reread) + monkeypatch.setattr( + local_endpoint, + "probe_local_endpoint", + lambda _endpoint: (False, "controlled-unreachable"), + ) + + response = client.get("/app/thinking/api/providers") + + assert response.status_code == 200 + assert reads == [config] + assert inspections == [config] + + +@pytest.mark.parametrize( + ("method", "path"), + [ + ("get", "/app/thinking/api/providers/local/status"), + ("post", "/app/thinking/api/confidential/recheck"), + ], +) +def test_spp_narrow_routes_use_one_config_and_one_brain_inspection( + settings_client_with_journal, + monkeypatch, + method, + path, +): + from solstone.think import brain_health + from solstone.think import journal_config as journal_config_module + from solstone.think.providers import brain_state, local_endpoint + from solstone.think.services import scout, spp + + client, journal_path = settings_client_with_journal + config = json.loads( + (journal_path / "config" / "journal.json").read_text(encoding="utf-8") + ) + config.update(_spp_configured_provider_config()) + _write_config(journal_path, config) + _write_ready_spp_brain_record(journal_path) + + reads: list[dict] = [] + + def read_once() -> dict: + reads.append(config) + assert len(reads) == 1 + return config + + inspections: list[dict] = [] + original_inspect = brain_health.inspect_brain_state + + def inspect_once(*args, **kwargs): + inspections.append(kwargs["config"]) + assert kwargs["config"] is config + return original_inspect(*args, **kwargs) + + def fail_reread(*_args, **_kwargs): + raise AssertionError("second config authority reached") + + monkeypatch.setattr(routes, "get_journal_config", read_once) + monkeypatch.setattr(routes, "request_brain_refresh", lambda **_kwargs: False) + monkeypatch.setattr(brain_health, "inspect_brain_state", inspect_once) + monkeypatch.setattr(brain_state, "read_journal_config", fail_reread) + monkeypatch.setattr(local_endpoint, "read_journal_config", fail_reread) + monkeypatch.setattr(scout, "read_journal_config", fail_reread) + monkeypatch.setattr(spp, "read_journal_config", fail_reread) + monkeypatch.setattr(journal_config_module, "read_journal_config", fail_reread) + + response = getattr(client, method)(path) + + assert response.status_code == 200 + assert reads == [config] + assert inspections == [config] + + def test_get_providers_brain_shape(settings_client, monkeypatch): _patch_brain(monkeypatch) @@ -1485,7 +1627,7 @@ def test_get_providers_brain_unknown_does_not_change_status_payload( } monkeypatch.setattr( "solstone.think.providers.state.local_status_dict", - lambda: sentinel, + lambda **_kwargs: sentinel, ) _patch_selected_providers(monkeypatch) _patch_brain( diff --git a/solstone/think/brain_health.py b/solstone/think/brain_health.py index 4d65ac1e5..dfac94d5e 100644 --- a/solstone/think/brain_health.py +++ b/solstone/think/brain_health.py @@ -7,9 +7,10 @@ from __future__ import annotations import logging import uuid +from collections.abc import Mapping from datetime import datetime, timezone from pathlib import Path -from typing import Literal, TypedDict +from typing import Any, Literal, TypedDict from solstone.think.callosum import callosum_send from solstone.think.providers.brain_state import ( @@ -120,6 +121,7 @@ class ConfidentialAttestationView(TypedDict): state: ConfidentialAttestationState reason: str | None observed_at: str | None + expires_at: str | None class BrainPresentation(TypedDict): @@ -396,6 +398,7 @@ def _confidential_attestation_from_inspection( "state": "off", "reason": "confidential_not_configured", "observed_at": None, + "expires_at": None, } projection = inspection["projection"] @@ -404,18 +407,21 @@ def _confidential_attestation_from_inspection( "state": "inactive", "reason": "confidential_not_active", "observed_at": None, + "expires_at": None, } if projection["aggregate_state"] == "checking": return { "state": "verifying", "reason": "brain_check_in_progress", "observed_at": None, + "expires_at": None, } if projection["reason_code"] in BRAIN_PROJECTION_ONLY_REASON_CODES: return { "state": "stale", "reason": projection["reason_code"], "observed_at": None, + "expires_at": None, } lane_prerequisites = _spp_component(inspection, "lane_prerequisites") @@ -424,22 +430,45 @@ def _confidential_attestation_from_inspection( "state": "stale", "reason": projection["reason_code"] or "brain_record_invalid", "observed_at": None, + "expires_at": None, } reason = _component_reason(lane_prerequisites) observed_at = lane_prerequisites.get("observed_at") + expires_at = lane_prerequisites.get("expires_at") if lane_prerequisites["status"] == "ok": - return {"state": "verified", "reason": None, "observed_at": observed_at} + return { + "state": "verified", + "reason": None, + "observed_at": observed_at, + "expires_at": expires_at, + } if reason == "attestation_rejected": - return {"state": "failed", "reason": reason, "observed_at": observed_at} + return { + "state": "failed", + "reason": reason, + "observed_at": observed_at, + "expires_at": expires_at, + } if reason == "attestation_not_verified": - return {"state": "unreachable", "reason": reason, "observed_at": observed_at} + return { + "state": "unreachable", + "reason": reason, + "observed_at": observed_at, + "expires_at": expires_at, + } if reason == "attestation_expired": - return {"state": "stale", "reason": reason, "observed_at": observed_at} + return { + "state": "stale", + "reason": reason, + "observed_at": observed_at, + "expires_at": expires_at, + } return { "state": "stale", "reason": reason or projection["reason_code"] or "brain_record_invalid", "observed_at": None, + "expires_at": None, } @@ -449,9 +478,14 @@ def build_brain_presentation( surface: BrainSurface, spp_configured: bool, journal_path: Path | None = None, + config: Mapping[str, Any] | None = None, ) -> BrainPresentation: now = _utc(now) - inspection = inspect_brain_state(now, journal_path=journal_path) + inspection = inspect_brain_state( + now, + journal_path=journal_path, + config=config, + ) return { "brain": _brain_snapshot_from_inspection(inspection, now, surface=surface), "spp_active": inspection["projection"]["active_lane"] == "spp", @@ -468,6 +502,7 @@ def build_brain_snapshot( *, surface: BrainSurface, journal_path: Path | None = None, + config: Mapping[str, Any] | None = None, ) -> BrainSnapshot: # The standalone brain wire shape does not depend on SPP setup state. return build_brain_presentation( @@ -475,6 +510,7 @@ def build_brain_snapshot( surface=surface, spp_configured=False, journal_path=journal_path, + config=config, )["brain"] diff --git a/solstone/think/providers/__init__.py b/solstone/think/providers/__init__.py index 33cbd30c1..c63cc7998 100644 --- a/solstone/think/providers/__init__.py +++ b/solstone/think/providers/__init__.py @@ -138,6 +138,7 @@ def get_provider_list() -> List[Dict[str, Any]]: def build_provider_status( providers_list: List[Dict[str, Any]] | None = None, *, + config: dict[str, Any] | None = None, local_status: dict[str, Any] | None = None, ) -> Dict[str, Dict[str, Any]]: """Build per-provider readiness status. @@ -149,6 +150,9 @@ def build_provider_status( local_status When provided, replaces the ``local`` row and ``local_status_dict()`` is not called. ``None`` preserves existing behavior. + config + An already-read journal config. When omitted, existing helper reads are + preserved. Returns ------- Dict[str, Dict[str, Any]] @@ -168,9 +172,9 @@ def build_provider_status( from solstone.think.providers import state as provider_state if name == "local": - status[name] = provider_state.local_status_dict() + status[name] = provider_state.local_status_dict(config=config) continue - configured = provider_state.cloud_key_configured(env_key) + configured = provider_state.cloud_key_configured(env_key, config=config) status[name] = { "provider": name, "configured": configured, diff --git a/solstone/think/providers/brain_state.py b/solstone/think/providers/brain_state.py index c24785f06..98f4ee2d6 100644 --- a/solstone/think/providers/brain_state.py +++ b/solstone/think/providers/brain_state.py @@ -1658,7 +1658,10 @@ def project_brain_state( def inspect_brain_state( - now: datetime, *, journal_path: str | Path | None = None + now: datetime, + *, + journal_path: str | Path | None = None, + config: Mapping[str, Any] | None = None, ) -> BrainStateInspection: now = _utc(now) try: @@ -1685,18 +1688,19 @@ def inspect_brain_state( if record is None and record_reason is None: status = "unavailable" record_reason = "brain_record_missing" - try: - config = read_journal_config(journal_path) - except (CorruptConfigError, OSError) as exc: - projection = _projection("unknown", "configuration_invalid") - return { - "status": status, - "path": str(path), - "record": record, - "projection": projection, - "reason_code": "configuration_invalid", - "error": str(exc), - } + if config is None: + try: + config = read_journal_config(journal_path) + except (CorruptConfigError, OSError) as exc: + projection = _projection("unknown", "configuration_invalid") + return { + "status": status, + "path": str(path), + "record": record, + "projection": projection, + "reason_code": "configuration_invalid", + "error": str(exc), + } lane, provider, model = _derive_lane(config) if lane is None: projection = _projection( @@ -2274,6 +2278,13 @@ def runtime_phase_reason(phase: RuntimePhase) -> BrainReasonCode | None: return RUNTIME_PHASE_TO_REASON[phase] +def derive_active_brain_lane(config: Mapping[str, Any]) -> BrainLaneId | None: + """Derive the canonical active-brain lane from an already-read config.""" + + lane, _, _ = _derive_lane(config) + return lane + + __all__ = [ "BRAIN_AGGREGATE_STATES", "BRAIN_COMPONENT_STATUSES", @@ -2309,6 +2320,7 @@ __all__ = [ "brain_state_path", "begin_brain_refresh", "build_active_brain_fingerprint", + "derive_active_brain_lane", "finish_brain_refresh", "inspect_brain_state", "project_brain_state", diff --git a/solstone/think/providers/local_endpoint.py b/solstone/think/providers/local_endpoint.py index 0176af766..cde4c9234 100644 --- a/solstone/think/providers/local_endpoint.py +++ b/solstone/think/providers/local_endpoint.py @@ -105,10 +105,9 @@ def _configured_byo_parallel_slots(local_config: dict[str, Any]) -> int: return raw -def resolve_local_endpoint() -> LocalEndpoint: - """Resolve whether local provider traffic uses bundled runtime or BYO endpoint.""" +def resolve_local_endpoint_from_config(config: dict[str, Any]) -> LocalEndpoint: + """Resolve local provider traffic from an already-read journal config.""" - config = read_journal_config() providers_config = config.get("providers", {}) local_config: Any = {} if isinstance(providers_config, dict): @@ -134,6 +133,12 @@ def resolve_local_endpoint() -> LocalEndpoint: return LocalEndpoint("", "", None, is_bundled=True) +def resolve_local_endpoint() -> LocalEndpoint: + """Resolve whether local provider traffic uses bundled runtime or BYO endpoint.""" + + return resolve_local_endpoint_from_config(read_journal_config()) + + def probe_local_endpoint( endpoint: LocalEndpoint, timeout_s: float = 1.0, @@ -355,5 +360,6 @@ __all__ = [ "redact_event_payload", "redact_local_endpoint_credential", "resolve_local_endpoint", + "resolve_local_endpoint_from_config", "wrap_on_event_redacting", ] diff --git a/solstone/think/providers/state.py b/solstone/think/providers/state.py index 03604e11a..731b24c7a 100644 --- a/solstone/think/providers/state.py +++ b/solstone/think/providers/state.py @@ -7,13 +7,20 @@ from __future__ import annotations import os import sys +from typing import Any -def cloud_key_configured(env_key: str) -> bool: +def cloud_key_configured( + env_key: str, + *, + config: dict[str, Any] | None = None, +) -> bool: if not env_key: return False if os.getenv(env_key): return True + if config is not None: + return bool(config.get("env", {}).get(env_key)) try: from solstone.think.journal_config import read_journal_config @@ -27,16 +34,21 @@ def _is_darwin() -> bool: return sys.platform == "darwin" -def local_status_dict() -> dict: +def local_status_dict(*, config: dict[str, Any] | None = None) -> dict: """Build the local provider setup status dict.""" from solstone.think.models import is_local_provider_needed from solstone.think.providers.local_endpoint import ( probe_local_endpoint, resolve_local_endpoint, + resolve_local_endpoint_from_config, ) - endpoint = resolve_local_endpoint() - selected = is_local_provider_needed() + endpoint = ( + resolve_local_endpoint() + if config is None + else resolve_local_endpoint_from_config(config) + ) + selected = is_local_provider_needed(config) if not endpoint.is_bundled: reachable, _ = probe_local_endpoint(endpoint) return { diff --git a/solstone/think/services/scout.py b/solstone/think/services/scout.py index d9f883835..c4df9d331 100644 --- a/solstone/think/services/scout.py +++ b/solstone/think/services/scout.py @@ -285,10 +285,10 @@ def apply_scout_state(payload: dict[str, Any]) -> ScoutStateResult: raise ScoutPayloadError("unexpected_payload") -def is_scout_enabled() -> bool: +def is_scout_enabled(config: dict[str, Any] | None = None) -> bool: """Return whether scout is enabled through service provisioning.""" - config = read_journal_config() + config = read_journal_config() if config is None else config block = config.get("services", {}).get("scout") return _is_approved_provision(block) and bool( config.get("env", {}).get("GOOGLE_API_KEY") @@ -305,10 +305,13 @@ def is_manual_key_present() -> bool: ) -def scout_provenance() -> dict[str, Any] | None: +def scout_provenance( + config: dict[str, Any] | None = None, +) -> dict[str, Any] | None: """Return the scout provenance block from journal config, if present.""" - provenance = read_journal_config().get("services", {}).get("scout") + config = read_journal_config() if config is None else config + provenance = config.get("services", {}).get("scout") return provenance if isinstance(provenance, dict) else None diff --git a/solstone/think/services/spp.py b/solstone/think/services/spp.py index 17a974e78..9c0c32f05 100644 --- a/solstone/think/services/spp.py +++ b/solstone/think/services/spp.py @@ -297,10 +297,10 @@ def confidential_provenance() -> dict[str, Any] | None: return confidential_provenance_block(read_journal_config()) -def is_confidential_enabled() -> bool: +def is_confidential_enabled(config: dict[str, Any] | None = None) -> bool: """Return whether confidential processing is provisioned with a credential.""" - config = read_journal_config() + config = read_journal_config() if config is None else config block = config.get("services", {}).get("confidential") local = config.get("providers", {}).get("local", {}) credential = local.get("credential") if isinstance(local, dict) else None diff --git a/tests/baselines/api/thinking/providers.json b/tests/baselines/api/thinking/providers.json index 9839d9746..dff21cea9 100644 --- a/tests/baselines/api/thinking/providers.json +++ b/tests/baselines/api/thinking/providers.json @@ -5,6 +5,7 @@ }, "active_lane": { "confidential_attestation": { + "expires_at": null, "observed_at": null, "reason": "confidential_not_configured", "state": "off" diff --git a/tests/test_brain_health_cutover_parity.py b/tests/test_brain_health_cutover_parity.py index 18a521bcd..815efd5e9 100644 --- a/tests/test_brain_health_cutover_parity.py +++ b/tests/test_brain_health_cutover_parity.py @@ -241,7 +241,11 @@ def test_state_parity_matrix(monkeypatch, capsys): thinking_brain = _snapshot(case, "thinking") monkeypatch.setattr(thinking_routes, "get_provider_list", lambda: []) - monkeypatch.setattr(thinking_routes, "build_provider_status", lambda _p: {}) + monkeypatch.setattr( + thinking_routes, + "build_provider_status", + lambda _p, **_kwargs: {}, + ) monkeypatch.setattr(thinking_routes.local_bootstrap, "get_state", lambda _m: {}) monkeypatch.setattr( thinking_routes, @@ -258,6 +262,7 @@ def test_state_parity_matrix(monkeypatch, capsys): "state": "off", "reason": "confidential_not_configured", "observed_at": None, + "expires_at": None, }, }, ) diff --git a/tests/test_brain_state.py b/tests/test_brain_state.py index c7b076f19..70fe40c8d 100644 --- a/tests/test_brain_state.py +++ b/tests/test_brain_state.py @@ -538,6 +538,30 @@ def test_inspect_brain_state_is_passive_host_probe_free_for_bundled_runtime( assert _health_snapshot(tmp_path) == before +def test_inspect_brain_state_uses_supplied_config_without_reread( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + config = _cloud_config() + _write_ready_record(tmp_path, config) + monkeypatch.setattr( + brain_state_module, + "read_journal_config", + lambda *_args, **_kwargs: (_ for _ in ()).throw( + AssertionError("config reread") + ), + ) + + inspection = inspect_brain_state( + NOW, + journal_path=tmp_path, + config=config, + ) + + assert inspection["projection"]["aggregate_state"] == "ready" + assert inspection["projection"]["active_lane"] == "byo-cloud" + + def test_inspect_brain_state_faults_do_not_modify_health( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: diff --git a/tests/test_no_implicit_cloud.py b/tests/test_no_implicit_cloud.py index e574fba8b..6476add66 100644 --- a/tests/test_no_implicit_cloud.py +++ b/tests/test_no_implicit_cloud.py @@ -467,6 +467,71 @@ def test_persisted_spp_brain_evidence_never_authorizes_generate_egress( establish.assert_called_once() +def test_persisted_spp_brain_evidence_never_authorizes_cogitate_egress( + tmp_path, + monkeypatch, +): + from solstone.think.providers.brain_state import ( + begin_brain_refresh, + finish_brain_refresh, + ) + from solstone.think.services import spp + + _empty_journal(tmp_path, monkeypatch) + config = _confidential_config() + config["providers"] = { + "active": {"provider": "local", "model": LOCAL_MODEL}, + } + _add_local_endpoint(config) + config["services"]["confidential"]["credential_fingerprint_sha256"] = ( + hashlib.sha256(b"confidential-credential").hexdigest() + ) + _seed_journal_config(tmp_path, config) + + now = datetime.now(timezone.utc) + permit = begin_brain_refresh(now, journal_path=tmp_path) + assert permit is not None + component = { + "status": "ok", + "observed_at": now.isoformat(), + "expires_at": (now + timedelta(hours=1)).isoformat(), + } + finish_brain_refresh( + permit, + { + "configuration": component, + "lane_prerequisites": component, + "generate": component, + "cogitate": component, + }, + now, + journal_path=tmp_path, + ) + spp.delete_attestation_state() + + establish = _install_failing_confidential_transport(monkeypatch) + local_cogitate = Mock(side_effect=AssertionError("local cogitate dispatched")) + build_llm = Mock(side_effect=AssertionError("llm build attempted")) + monkeypatch.setattr( + "solstone.think.providers.local.run_cogitate", + local_cogitate, + ) + monkeypatch.setattr("solstone.think.providers.openhands._build_llm", build_llm) + + with pytest.raises(AttestationFailedError) as cogitate_exc: + asyncio.run( + talents._execute_with_tools( + {"provider": "local", "model": LOCAL_MODEL, "type": "cogitate"}, + lambda _event: None, + ) + ) + + _assert_attestation_failed(cogitate_exc.value) + local_cogitate.assert_not_called() + build_llm.assert_not_called() + establish.assert_called_once() + + def test_confidential_cogitate_stops_before_any_provider_dispatch( tmp_path, monkeypatch, diff --git a/tests/test_thinking_call_parity.py b/tests/test_thinking_call_parity.py index d1e46f06d..4cca42b37 100644 --- a/tests/test_thinking_call_parity.py +++ b/tests/test_thinking_call_parity.py @@ -138,6 +138,7 @@ def test_show_verbs_select_http_fields() -> None: "state": "off", "reason": "confidential_not_configured", "observed_at": None, + "expires_at": None, } @@ -340,7 +341,7 @@ def test_providers_show_human_and_set_errors( monkeypatch.setattr( thinking_routes, "build_provider_status", - lambda providers: provider_status, + lambda providers, **_kwargs: provider_status, ) human = runner.invoke(thinking_call.app, ["providers", "show", "--human"])