diff --git a/solstone/apps/health/tests/test_routes.py b/solstone/apps/health/tests/test_routes.py index 40b5467c4..bd25a1183 100644 --- a/solstone/apps/health/tests/test_routes.py +++ b/solstone/apps/health/tests/test_routes.py @@ -47,7 +47,7 @@ def _readiness_snapshot(severity: str = "neutral") -> dict: "summary": "Anthropic needs credentials before it can read your screen descriptions", "detail": "Open provider setup.", "recovery_action": { - "label": "Open Settings", + "label": "Open Thinking", "href": "/app/thinking/#main", }, "operator_detail": "reason_code=provider_key_missing provider=anthropic", diff --git a/solstone/apps/settings/routes.py b/solstone/apps/settings/routes.py index d1eb6f58e..b468e5e43 100644 --- a/solstone/apps/settings/routes.py +++ b/solstone/apps/settings/routes.py @@ -213,8 +213,9 @@ def _project_public_config(config: dict[str, Any]) -> dict[str, Any]: if service_validation: projected["key_validation"] = service_validation projected.pop("service_key_validation", None) - if "env" in projected: - projected["env"] = {k: bool(v) for k, v in projected["env"].items()} + projected["env"] = { + k: bool((projected.get("env") or {}).get(k)) for k in API_KEY_ENV_VARS + } projected.pop("providers", None) convey_config = projected.setdefault("convey", {}) convey_config.pop("secret", None) diff --git a/solstone/apps/settings/tests/test_journal_config_routes.py b/solstone/apps/settings/tests/test_journal_config_routes.py index d1aecc870..1aae5fdee 100644 --- a/solstone/apps/settings/tests/test_journal_config_routes.py +++ b/solstone/apps/settings/tests/test_journal_config_routes.py @@ -4,6 +4,7 @@ from __future__ import annotations from pathlib import Path +from typing import Any from solstone.convey import create_app from solstone.convey.reasons import INVALID_CONFIG_VALUE @@ -108,3 +109,74 @@ def test_journal_config_get_without_journal_section(settings_env): assert response.status_code == 200 assert "journal" not in response.get_json() + + +def _payload_keys_and_strings(value: Any): + if isinstance(value, dict): + for key, item in value.items(): + yield str(key) + yield from _payload_keys_and_strings(item) + elif isinstance(value, list): + for item in value: + yield from _payload_keys_and_strings(item) + elif isinstance(value, str): + yield value + + +def test_settings_config_projection_drops_thinking_provider_secrets( + settings_env, + monkeypatch, +): + endpoint_credential = "SETTINGS-ENDPOINT-CREDENTIAL-SENTINEL" + config = { + **_base_config(), + "env": { + "GOOGLE_API_KEY": "google-secret", + "OPENAI_API_KEY": "openai-secret", + "ANTHROPIC_API_KEY": "anthropic-secret", + "TEST_LEAK_SENTINEL": "arbitrary-secret", + "REVAI_ACCESS_TOKEN": "revai-secret", + }, + "providers": { + "active": { + "provider": "anthropic", + "model": "claude-settings-leak-model", + }, + "local": { + "endpoint_url": "http://settings-leak.example/v1", + "served_model_id": "settings-local-model", + "credential": endpoint_credential, + }, + }, + } + journal_path, _config = settings_env(config) + monkeypatch.setenv("REVAI_ACCESS_TOKEN", "runtime-revai") + monkeypatch.delenv("PLAUD_ACCESS_TOKEN", raising=False) + client = _client(journal_path) + + response = client.get("/app/settings/api/config") + + assert response.status_code == 200 + payload = response.get_json() + serialized_projection = "\n".join(_payload_keys_and_strings(payload)) + for forbidden in ( + "GOOGLE_API_KEY", + "OPENAI_API_KEY", + "ANTHROPIC_API_KEY", + "TEST_LEAK_SENTINEL", + "anthropic", + "claude-settings-leak-model", + "endpoint_url", + endpoint_credential, + ): + assert forbidden not in serialized_projection + assert set(payload["env"]) == {"REVAI_ACCESS_TOKEN", "PLAUD_ACCESS_TOKEN"} + assert payload["env"] == { + "REVAI_ACCESS_TOKEN": True, + "PLAUD_ACCESS_TOKEN": False, + } + assert set(payload["runtime_env"]) == {"REVAI_ACCESS_TOKEN", "PLAUD_ACCESS_TOKEN"} + assert payload["runtime_env"] == { + "REVAI_ACCESS_TOKEN": True, + "PLAUD_ACCESS_TOKEN": False, + } diff --git a/solstone/apps/thinking/tests/test_providers_payload_extended.py b/solstone/apps/thinking/tests/test_providers_payload_extended.py index 0e55470c2..3f8fdb24e 100644 --- a/solstone/apps/thinking/tests/test_providers_payload_extended.py +++ b/solstone/apps/thinking/tests/test_providers_payload_extended.py @@ -1205,7 +1205,7 @@ def test_get_providers_ai_readiness_missing_key_blocks(settings_client, monkeypa group = readiness["groups"][0] assert group["reason_code"] == "provider_key_missing" assert group["recovery_action"] == { - "label": "Open Settings", + "label": "Open Thinking", "href": "/app/thinking/#main", } diff --git a/solstone/convey/provider_readiness.py b/solstone/convey/provider_readiness.py index c3e68f861..f532304e2 100644 --- a/solstone/convey/provider_readiness.py +++ b/solstone/convey/provider_readiness.py @@ -67,10 +67,6 @@ PROVIDER_LEVEL_CODES = frozenset( } ) -_SETTINGS_ACTION = RecoveryAction( - label="Open Settings", - target="/app/thinking/#main", -) _THINKING_ACTION = RecoveryAction( label="Open Thinking", target="/app/thinking/#main", @@ -96,7 +92,7 @@ _ENTRIES: dict[str, _Entry] = { klass="setup", summary="{provider} needs credentials before it can read your screen descriptions", detail="Open provider setup and add credentials, then try again.", - recovery_action=_SETTINGS_ACTION, + recovery_action=_THINKING_ACTION, ), "ram_insufficient": _Entry( klass="setup", @@ -114,7 +110,7 @@ _ENTRIES: dict[str, _Entry] = { "Local models require a supported GPU. This computer has no GPU " "acceleration available." ), - recovery_action=_SETTINGS_ACTION, + recovery_action=_THINKING_ACTION, ), "gpu_probe_failed": _Entry( klass="setup", @@ -123,7 +119,7 @@ _ENTRIES: dict[str, _Entry] = { "Local model setup couldn't confirm GPU acceleration — try again, " "or use a cloud provider if it keeps failing." ), - recovery_action=_SETTINGS_ACTION, + recovery_action=_THINKING_ACTION, ), "local_model_missing": _Entry( klass="setup", @@ -171,7 +167,7 @@ _ENTRIES: dict[str, _Entry] = { klass="provider", summary=LOCAL_ENDPOINT_UNREACHABLE_COPY, detail="Check the endpoint URL, confirm the server is running, then retry.", - recovery_action=_SETTINGS_ACTION, + recovery_action=_THINKING_ACTION, ), "local_endpoint_contract_failed": _Entry( klass="generic", @@ -180,7 +176,7 @@ _ENTRIES: dict[str, _Entry] = { "Confirm the endpoint serves /v1/chat/completions with vision and " "JSON-schema response_format support." ), - recovery_action=_SETTINGS_ACTION, + recovery_action=_THINKING_ACTION, ), "unsupported_platform": _Entry( klass="setup", @@ -220,7 +216,7 @@ _ENTRIES: dict[str, _Entry] = { klass="provider", summary="your {provider} key didn't validate", detail="Open provider setup and check the saved credentials.", - recovery_action=_SETTINGS_ACTION, + recovery_action=_THINKING_ACTION, ), "provider_quota_exceeded": _Entry( klass="provider", @@ -358,7 +354,7 @@ _FALLBACK_ENTRY = _Entry( klass="generic", summary="couldn't determine provider readiness — check your provider or local model setup", detail="Check provider setup and local model setup, then try again.", - recovery_action=_SETTINGS_ACTION, + recovery_action=_THINKING_ACTION, ) _NEUTRAL_SUMMARY = ( diff --git a/solstone/convey/static/chat_reasons.js b/solstone/convey/static/chat_reasons.js index a447491b4..ffeaa97fd 100644 --- a/solstone/convey/static/chat_reasons.js +++ b/solstone/convey/static/chat_reasons.js @@ -16,7 +16,7 @@ }, "provider_key_missing": { "template": "{provider} needs credentials before it can read your screen descriptions", - "action": {"label": "Open Settings", "href": "/app/thinking/#main"} + "action": {"label": "Open Thinking", "href": "/app/thinking/#main"} }, "ram_insufficient": { "template": "the local model needs more memory than this machine has", @@ -24,11 +24,11 @@ }, "gpu_unavailable": { "template": "local models need GPU acceleration on this computer", - "action": {"label": "Open Settings", "href": "/app/thinking/#main"} + "action": {"label": "Open Thinking", "href": "/app/thinking/#main"} }, "gpu_probe_failed": { "template": "local GPU check couldn't finish", - "action": {"label": "Open Settings", "href": "/app/thinking/#main"} + "action": {"label": "Open Thinking", "href": "/app/thinking/#main"} }, "local_model_missing": { "template": "local model setup is not finished", @@ -60,11 +60,11 @@ }, "local_endpoint_unreachable": { "template": "The inference endpoint you configured could not be reached.", - "action": {"label": "Open Settings", "href": "/app/thinking/#main"} + "action": {"label": "Open Thinking", "href": "/app/thinking/#main"} }, "local_endpoint_contract_failed": { "template": "The configured endpoint did not respond in the expected format.", - "action": {"label": "Open Settings", "href": "/app/thinking/#main"} + "action": {"label": "Open Thinking", "href": "/app/thinking/#main"} }, "unsupported_platform": { "template": "this machine is not supported for local model setup", @@ -88,7 +88,7 @@ }, "provider_key_invalid": { "template": "your {provider} key didn't validate", - "action": {"label": "Open Settings", "href": "/app/thinking/#main"} + "action": {"label": "Open Thinking", "href": "/app/thinking/#main"} }, "provider_quota_exceeded": { "template": "your {provider} quota is spent", diff --git a/solstone/convey/static/tests/chat-bar-reasons.html b/solstone/convey/static/tests/chat-bar-reasons.html index 14cce7bcc..0778a393a 100644 --- a/solstone/convey/static/tests/chat-bar-reasons.html +++ b/solstone/convey/static/tests/chat-bar-reasons.html @@ -83,7 +83,7 @@ const action = statusWrap.querySelector('.chat-bar-status-action'); assert(`${code} action presence`, hasAction ? !!action : !action); if (hasAction) { - assert(`${code} action label`, action.textContent === 'Open Settings'); + assert(`${code} action label`, action.textContent === 'Open Thinking'); assert(`${code} action href`, action.getAttribute('href') === '/app/thinking/#main'); } }); diff --git a/tests/baselines/api/settings/config.json b/tests/baselines/api/settings/config.json index 46514dc65..00d634299 100644 --- a/tests/baselines/api/settings/config.json +++ b/tests/baselines/api/settings/config.json @@ -14,7 +14,8 @@ ] }, "env": { - "TEST_CONFIG_ENV_VAR": true + "PLAUD_ACCESS_TOKEN": false, + "REVAI_ACCESS_TOKEN": false }, "identity": { "aliases": [ diff --git a/tests/baselines/api/thinking/providers.json b/tests/baselines/api/thinking/providers.json index 60a3bc034..bd0ab5ecd 100644 --- a/tests/baselines/api/thinking/providers.json +++ b/tests/baselines/api/thinking/providers.json @@ -30,7 +30,7 @@ "reason_code": "provider_key_missing", "recovery_action": { "href": "/app/thinking/#main", - "label": "Open Settings" + "label": "Open Thinking" }, "semantic_key": "provider_key_missing:google:", "severity": "blocker", @@ -50,7 +50,7 @@ "reason_code": "provider_key_missing", "recovery_action": { "href": "/app/thinking/#main", - "label": "Open Settings" + "label": "Open Thinking" }, "semantic_key": "provider_key_missing:google:", "severity": "blocker", @@ -68,7 +68,7 @@ "reason_code": "provider_key_missing", "recovery_action": { "href": "/app/thinking/#main", - "label": "Open Settings" + "label": "Open Thinking" }, "semantic_key": "provider_key_missing:google:", "severity": "blocker", diff --git a/tests/test_chat_reasons.py b/tests/test_chat_reasons.py index 3aa8d20fc..b0d169bc7 100644 --- a/tests/test_chat_reasons.py +++ b/tests/test_chat_reasons.py @@ -131,7 +131,7 @@ def test_render_known_codes(): assert "Gemini" in rendered["message"] if code == "provider_key_invalid": assert rendered["action"] == { - "label": "Open Settings", + "label": "Open Thinking", "href": "/app/thinking/#main", } else: