Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions reflexio/cli/commands/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from reflexio.cli import run_services as run_mod
from reflexio.cli import stop_services as stop_mod
from reflexio.cli.bootstrap_config import _VALID_STORAGE_BACKENDS
from reflexio.server.env_utils import env_truthy

_logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -228,9 +229,8 @@ def start(
# (e.g. REFLEXIO_STORAGE=supabase) are visible to the resolution chain.
load_reflexio_env()

if (
not os.environ.get("REFLEXIO_EMBEDDING_PROVIDER")
and os.environ.get("CLAUDE_SMART_USE_LOCAL_EMBEDDING") == "1"
if not os.environ.get("REFLEXIO_EMBEDDING_PROVIDER") and env_truthy(
os.environ.get("CLAUDE_SMART_USE_LOCAL_EMBEDDING", "")
):
os.environ["REFLEXIO_EMBEDDING_PROVIDER"] = "local_service"

Expand Down
4 changes: 3 additions & 1 deletion reflexio/cli/commands/setup_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

import typer

from reflexio.server.env_utils import env_truthy

app = typer.Typer(
help=(
"Configure Reflexio: run 'init' for plain CLI setup, use 'openclaw' "
Expand Down Expand Up @@ -188,7 +190,7 @@ def _is_non_interactive() -> bool:
- stdin is not a TTY (``nohup``, container, pipe-fed shell)
- ``REFLEXIO_NONINTERACTIVE=1`` in the environment (explicit opt-out)
"""
if os.environ.get("REFLEXIO_NONINTERACTIVE") == "1":
if env_truthy(os.environ.get("REFLEXIO_NONINTERACTIVE", "")):
return True
return not sys.stdin.isatty()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from pathlib import Path
from typing import Any

from reflexio.server.env_utils import env_truthy

INTERNAL_ENV = "OPENCLAW_SMART_INTERNAL"

# Plugin layout (in-repo / editable):
Expand Down Expand Up @@ -59,7 +61,7 @@ def is_internal_invocation(payload: dict[str, Any]) -> bool:
reflexio repository. False otherwise, including when ``cwd`` is
missing or unresolvable.
"""
if os.environ.get(INTERNAL_ENV) == "1":
if env_truthy(os.environ.get(INTERNAL_ENV, "")):
return True
cwd = payload.get("cwd") or payload.get("workspaceDir")
if not isinstance(cwd, str) or not cwd:
Expand Down
3 changes: 2 additions & 1 deletion reflexio/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from reflexio.cli.env_loader import load_reflexio_env
from reflexio.cli.paths import reflexio_home
from reflexio.server.env_utils import env_truthy

# Load environment variables using shared discovery logic
load_reflexio_env()
Expand Down Expand Up @@ -131,7 +132,7 @@ def format(self, record: logging.LogRecord) -> str:
def _truthy_env(name: str) -> bool:
"""Return whether an environment variable is explicitly truthy."""
raw = os.environ.get(name, "").strip().lower()
return raw in ("true", "yes", "1", "on")
return env_truthy(raw)


def _is_production_environment() -> bool:
Expand Down
4 changes: 2 additions & 2 deletions reflexio/server/api_endpoints/account_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from __future__ import annotations

import logging
import os
from typing import Any, cast

from reflexio.lib._storage_labels import describe_storage
Expand All @@ -24,6 +23,7 @@
WhoamiResponse,
)
from reflexio.server.cache.reflexio_cache import get_reflexio
from reflexio.server.env_utils import env_bool

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -87,7 +87,7 @@ def my_config_allowed() -> bool:
FastAPI endpoint wrapper in ``reflexio.server.api`` and does not
flow through this helper.
"""
return os.environ.get(_ALLOW_MY_CONFIG_ENV_VAR, "").lower() in {"1", "true", "yes"}
return env_bool(_ALLOW_MY_CONFIG_ENV_VAR, default=False)


def my_config(org_id: str) -> MyConfigResponse:
Expand Down
73 changes: 72 additions & 1 deletion reflexio/server/env_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,77 @@ def env_required_literal(
return value


_TRUE = "true"
_FALSE = "false"


class EnvBoolError(ValueError):
"""A boolean environment variable carried a value that is not true/false."""


def env_bool(
name: str,
*,
default: bool,
env: Mapping[str, str] | None = None,
) -> bool:
"""Parse a boolean environment variable, refusing anything ambiguous.

Accepts ``true`` / ``false`` only, case-insensitively and after stripping.
Unset or blank resolves to ``default``, matching :func:`env_str`'s
"set to blank behaves like unset" rule. **Every other value raises.**

Raising is the point, and it is what this codebase previously lacked. The
predecessor (``env_truthy``) was ``value.lower() in {...}``, so anything
unrecognised returned ``False`` -- meaning ``REFLEXIO_REQUIRE_DATA_DB=ture``
silently DISABLED a guard rather than failing. A typo that switches a safety
knob off without a word is the same silent-green class as a CI lane that
never runs.

Erroring on unrecognised input is the universal convention: Go's
``strconv.ParseBool`` states "Any other value returns an error", and Pydantic
raises ``bool_parsing``. Neither defaults.

The accepted SET is deliberately narrower than either, which both admit
``1``/``0``. ``.env.template`` documents ~28 variables as
``options: true | false``, so accepting more would leave that documentation a
half-truth -- and the direction of travel is narrowing, not widening: YAML
1.2 removed ``yes``/``no``/``on``/``off`` as booleans outright after ``NO``
(Norway) silently parsed as false.

Args:
name (str): Environment variable name, used in the error message.
default (bool): Value returned when the variable is unset or blank.
env (Mapping[str, str] | None): Mapping to read; defaults to os.environ.

Returns:
bool: The parsed value, or ``default`` when unset or blank.

Raises:
EnvBoolError: The variable is set to anything other than true/false.
"""
raw = (env if env is not None else os.environ).get(name)
if raw is None or not raw.strip():
return default
value = raw.strip().lower()
if value == _TRUE:
return True
if value == _FALSE:
return False
raise EnvBoolError(
f"{name} must be {_TRUE!r} or {_FALSE!r} (got {raw.strip()!r}). "
f"Values such as '1', '0', 'yes' and 'on' are deliberately NOT accepted: "
f"they were parsed inconsistently across this codebase, and an "
f"unrecognised value used to read as false without any error."
)


def env_truthy(value: str) -> bool:
"""Return whether a string environment value is truthy."""
"""Return whether a string environment value is truthy.

Deprecated in favour of :func:`env_bool`, which takes the variable NAME and
can therefore name it when refusing a bad value. Retained because it is a
public export of this package; it keeps the historical permissive set so an
external caller's behaviour does not change silently.
"""
return value.strip().lower() in {"1", "true", "yes", "on"}
4 changes: 2 additions & 2 deletions reflexio/server/llm/providers/claude_code_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
)
from pydantic import BaseModel

from reflexio.server.env_utils import env_truthy
from reflexio.server.llm.providers.claude_code_stream_parser import (
ParseResult,
classify_stall,
Expand Down Expand Up @@ -80,7 +81,6 @@ def _is_windows() -> bool:
_WINDOWS_ARGV_SYSTEM_PROMPT_LIMIT = 3_000
_WINDOWS_CLI_SUFFIXES = (".cmd", ".exe", ".bat")

_TRUTHY_ENV_VALUES = {"1", "true", "yes"}
_UNSUPPORTED_PARAMS_WARNED: set[str] = set()
_IMAGE_WARNED = False
_MULTITURN_WARNED = False
Expand All @@ -103,7 +103,7 @@ def _env_enabled() -> bool:
bool: True if the opt-in env var is set, False otherwise.
"""
raw = os.environ.get(ENV_ENABLE)
return bool(raw) and raw.lower() in _TRUTHY_ENV_VALUES
return bool(raw) and env_truthy(raw)


def _host() -> str:
Expand Down
3 changes: 2 additions & 1 deletion reflexio/server/llm/providers/embedding_service_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import httpx

from reflexio.server.env_utils import env_truthy
from reflexio.server.tracing import profile_step

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -333,7 +334,7 @@ def embedding_provider_mode(model: str | None = None) -> EmbeddingProviderMode:
if model and not _uses_embedding_service(model):
return "cloud"

if os.environ.get(_ENV_CLAUDE_SMART_LOCAL) == "1":
if env_truthy(os.environ.get(_ENV_CLAUDE_SMART_LOCAL, "")):
return "local_service"

if os.environ.get(_ENV_SERVICE_URL, "").strip():
Expand Down
4 changes: 3 additions & 1 deletion reflexio/server/llm/providers/local_embedding_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
from pathlib import Path
from typing import Any, BinaryIO, cast

from reflexio.server.env_utils import env_truthy

try:
import fcntl
except ImportError: # pragma: no cover - Windows only
Expand Down Expand Up @@ -505,7 +507,7 @@ def is_local_embedder_available() -> bool:
bool: True when ``CLAUDE_SMART_USE_LOCAL_EMBEDDING=1``
AND the ONNX dependencies are importable.
"""
if os.environ.get(_ENV_ENABLE) != "1":
if not env_truthy(os.environ.get(_ENV_ENABLE, "")):
return False
return are_local_embedding_dependencies_available()

Expand Down
5 changes: 3 additions & 2 deletions reflexio/server/llm/providers/nomic_embedding_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import threading
from typing import Any

from reflexio.server.env_utils import env_truthy
from reflexio.server.llm.llm_utils import positive_int_env

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -286,7 +287,7 @@ def register_if_enabled() -> bool:
global _REGISTERED
if _REGISTERED:
return True
if os.environ.get(_ENV_ENABLE) != "1":
if not env_truthy(os.environ.get(_ENV_ENABLE, "")):
return False
provider = os.environ.get(_ENV_PROVIDER, "").strip().lower()
if provider in {"local_service", "internal_service", "off"}:
Expand All @@ -296,7 +297,7 @@ def register_if_enabled() -> bool:
provider,
)
return False
if not provider and os.environ.get(_ENV_DAEMON) != "1":
if not provider and not env_truthy(os.environ.get(_ENV_DAEMON, "")):
_LOGGER.info(
"Nomic in-process prewarm skipped; %s=1 now defaults to the "
"shared embedding service.",
Expand Down
5 changes: 3 additions & 2 deletions reflexio/server/llm/providers/openclaw_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
from litellm.llms.custom_llm import CustomLLM
from litellm.types.utils import Choices, Message, ModelResponse, Usage

from reflexio.server.env_utils import env_truthy

_LOGGER = logging.getLogger(__name__)

PROVIDER_KEY = "openclaw"
Expand All @@ -34,7 +36,6 @@
ENV_TIMEOUT = "OPENCLAW_CLI_TIMEOUT"
_DEFAULT_TIMEOUT_SECONDS = 180

_TRUTHY = {"1", "true", "yes"}

# Module-level state reset by tests via the _reset_module_state fixture.
_REGISTERED: bool = False
Expand All @@ -51,7 +52,7 @@ def _env_enabled() -> bool:
Returns:
bool: True if the opt-in env var is set to a truthy value, else False.
"""
return os.environ.get(ENV_ENABLE, "").lower() in _TRUTHY
return env_truthy(os.environ.get(ENV_ENABLE, ""))


def _resolve_cli_path() -> str | None:
Expand Down
4 changes: 2 additions & 2 deletions reflexio/server/services/base_generation/_should_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@
"""

import logging
import os
import time
from typing import TYPE_CHECKING, Any, Generic, TypeVar

from reflexio.models.api_schema.internal_schema import RequestInteractionDataModel
from reflexio.server.api_endpoints.request_context import RequestContext
from reflexio.server.env_utils import env_bool
from reflexio.server.llm.litellm_client import LiteLLMClient
from reflexio.server.services.extractor_config_utils import get_extractor_name
from reflexio.server.services.extractor_interaction_utils import (
Expand Down Expand Up @@ -101,7 +101,7 @@ def _should_run_before_extraction(self, extractor_config: TExtractorConfig) -> b
return True

# Skip for mock mode
if os.getenv("MOCK_LLM_RESPONSE", "").lower() == "true":
if env_bool("MOCK_LLM_RESPONSE", default=False):
return True

# `force_extraction=True` is the caller's explicit "no gates" signal —
Expand Down
3 changes: 2 additions & 1 deletion reflexio/server/services/braintrust/_cron.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from collections.abc import Callable
from dataclasses import dataclass, field

from reflexio.server.env_utils import env_bool
from reflexio.server.services.braintrust.client import (
DEFAULT_BASE_URL,
BraintrustClient,
Expand All @@ -41,7 +42,7 @@

def _interval_seconds() -> int:
"""Pick the recurring interval based on `IS_TEST_ENV`."""
if os.environ.get("IS_TEST_ENV", "").strip().lower() == "true":
if env_bool("IS_TEST_ENV", default=False):
return _TEST_INTERVAL_SECONDS
return _DEFAULT_INTERVAL_SECONDS

Expand Down
11 changes: 5 additions & 6 deletions reflexio/server/services/playbook/components/aggregator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import logging
import math
import os
import time
import uuid
from collections.abc import Callable, Sequence
Expand All @@ -25,6 +24,7 @@
PlaybookAggregatorConfig,
)
from reflexio.server.api_endpoints.request_context import RequestContext
from reflexio.server.env_utils import env_bool
from reflexio.server.error_reporting import capture_anomaly, error_tags
from reflexio.server.llm._litellm_types import ModelProvenance
from reflexio.server.llm.litellm_client import LiteLLMClient
Expand Down Expand Up @@ -1771,9 +1771,8 @@ def run(self, playbook_aggregator_request: PlaybookAggregatorRequest) -> dict:
# other caller still aborts on a missing embedding: a
# centroid-less cluster row would silently break the
# incremental re-aggregation this table exists to feed.
if (
not saved_fb.embedding
and os.getenv("MOCK_LLM_RESPONSE", "").lower() != "true"
if not saved_fb.embedding and not env_bool(
"MOCK_LLM_RESPONSE", default=False
):
raise RuntimeError(
"rerun agent playbook has no centroid embedding"
Expand Down Expand Up @@ -2051,7 +2050,7 @@ def get_clusters(
model_name=self.storage.embedding_model_name,
)
# Mock mode: cluster by trigger
if os.getenv("MOCK_LLM_RESPONSE", "").lower() == "true":
if env_bool("MOCK_LLM_RESPONSE", default=False):
Comment thread
coderabbitai[bot] marked this conversation as resolved.
logger.info("Mock mode: clustering by trigger")
return aggregator_clustering.cluster_by_trigger_mock(
user_playbooks, min_cluster_size
Expand Down Expand Up @@ -2327,7 +2326,7 @@ def _generate_playbook_from_cluster_outcome(
if not cluster_playbooks:
return AggregationGenerationOutcome("retryable_failure", [])

if os.getenv("MOCK_LLM_RESPONSE", "").lower() == "true":
if env_bool("MOCK_LLM_RESPONSE", default=False):
# Extract structured fields directly from cluster
triggers = [fb.trigger for fb in cluster_playbooks if fb.trigger]

Expand Down
4 changes: 2 additions & 2 deletions reflexio/server/services/playbook/components/consolidator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"""

import logging
import os
from datetime import UTC, datetime
from typing import Annotated, Literal

Expand All @@ -21,6 +20,7 @@
normalize_provider_value,
)
from reflexio.server.api_endpoints.request_context import RequestContext
from reflexio.server.env_utils import env_bool
from reflexio.server.error_reporting import error_tags
from reflexio.server.llm._litellm_types import ModelProvenance
from reflexio.server.llm.litellm_client import (
Expand Down Expand Up @@ -966,7 +966,7 @@ def deduplicate(
raise TypeError("agent_version is required")

# Check if mock mode is enabled
if os.getenv("MOCK_LLM_RESPONSE", "").lower() == "true":
if env_bool("MOCK_LLM_RESPONSE", default=False):
logger.info("Mock mode: skipping consolidation")
all_playbooks: list[UserPlaybook] = []
for result in results:
Expand Down
Loading
Loading