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
3 changes: 2 additions & 1 deletion cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from cli import __version__
from cli.commands.init_cmd import init
from solace_agent_mesh.common.features import core as feature_flags
from cli.commands.run_cmd import run
from cli.commands.add_cmd import add
from cli.commands.plugin_cmd import plugin
Expand Down Expand Up @@ -63,7 +64,7 @@ def _version_callback(ctx, param, value):
)
def cli():
"""Solace CLI Application"""
pass
feature_flags.initialize()


cli.add_command(init)
Expand Down
12 changes: 12 additions & 0 deletions src/solace_agent_mesh/common/features/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,23 @@
from .checker import FeatureChecker
from .provider import SamFeatureProvider
from .registry import FeatureDefinition, FeatureRegistry, ReleasePhase
from .core import (
get_registry,
has_env_override,
initialize,
is_known_flag,
load_flags_from_yaml,
)

__all__ = [
"FeatureChecker",
"FeatureDefinition",
"FeatureRegistry",
"ReleasePhase",
"SamFeatureProvider",
"get_registry",
"has_env_override",
"initialize",
"is_known_flag",
"load_flags_from_yaml",
]
6 changes: 3 additions & 3 deletions src/solace_agent_mesh/common/features/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

Evaluation priority (highest to lowest):
1. Environment variable SAM_FEATURE_<UPPER_KEY>=true|false
2. Registry default FeatureDefinition.default_enabled
2. Registry default FeatureDefinition.default
"""

from __future__ import annotations
Expand Down Expand Up @@ -57,9 +57,9 @@ def is_enabled(self, key: str) -> bool:
logger.debug(
"Feature '%s' resolved from registry default: %s",
key,
definition.default_enabled,
definition.default,
)
return definition.default_enabled
return definition.default

def all_flags(self) -> dict[str, bool]:
"""Return {key: is_enabled(key)} for every registered feature."""
Expand Down
139 changes: 139 additions & 0 deletions src/solace_agent_mesh/common/features/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""
SAM feature flag initialisation and introspection utilities.

The OpenFeature client is the single source of truth for flag *evaluation*.
This module handles two concerns that OpenFeature does not cover:

1. Initialisation — loading ``features.yaml``, wiring the registry/checker,
and registering ``SamFeatureProvider`` with OpenFeature. Done once,
lazily, in a thread-safe way.

2. Introspection — ``is_known_flag()``, ``has_env_override()``, and
``get_registry()`` expose internal registry state that OpenFeature has
no equivalent for (used by the admin API endpoint).

For flag *evaluation* everywhere else, use the OpenFeature client directly:

from openfeature import api as openfeature_api

if openfeature_api.get_client().get_boolean_value("my_feature", False):
...
"""

from __future__ import annotations

import logging
import threading
from importlib.resources import files as pkg_files
from typing import Optional

from openfeature import api as openfeature_api

from .checker import FeatureChecker
from .provider import SamFeatureProvider
from .registry import FeatureRegistry

logger = logging.getLogger(__name__)

_checker: Optional[FeatureChecker] = None
_initialized = False
_lock = threading.Lock()


# ---------------------------------------------------------------------------
# Initialisation
# ---------------------------------------------------------------------------

def initialize() -> None:
"""Load community ``features.yaml`` and register the OpenFeature provider.

Idempotent and thread-safe — only the first call performs work.
"""
if _initialized:
return
with _lock:
if _initialized:
return
_do_initialize()


def _do_initialize() -> None:
global _checker, _initialized
registry = FeatureRegistry()
features_yaml = str(
pkg_files("solace_agent_mesh.common.features").joinpath("features.yaml")
)
registry.load_from_yaml(features_yaml)
_checker = FeatureChecker(registry=registry)
openfeature_api.set_provider(SamFeatureProvider(_checker))
# Set True before enterprise loading to prevent recursion: enterprise calls
# load_flags_from_yaml() → _ensure_initialized() → initialize(). The trade-off
# is that a concurrent caller whose first _initialized check races between this
# line and the end of enterprise loading may return slightly early, but the
# registry is already usable for community flags at this point and the enterprise
# load window is tiny. Using a separate recursion guard would add complexity
# without meaningful benefit given the narrow race window.
_initialized = True
logger.debug("Feature flags initialised with community features.yaml")
try:
from solace_agent_mesh_enterprise.init_enterprise import ( # pylint: disable=import-outside-toplevel
_register_enterprise_feature_flags,
)
_register_enterprise_feature_flags()
logger.debug("Enterprise feature flags loaded.")
except ImportError:
pass


def _ensure_initialized() -> None:
if not _initialized:
initialize()


def load_flags_from_yaml(path: str) -> None:
"""Merge additional flag definitions from a YAML file into the registry.

Used by enterprise to register enterprise-specific flags after community
flags are loaded.
"""
_ensure_initialized()
_checker.load_from_yaml(path)


# ---------------------------------------------------------------------------
# Introspection — not available through the OpenFeature client
# ---------------------------------------------------------------------------

def is_known_flag(key: str) -> bool:
"""Return True if *key* is registered in the feature registry."""
_ensure_initialized()
return _checker.is_known_flag(key)


def has_env_override(key: str) -> bool:
"""Return True if a ``SAM_FEATURE_<KEY>`` env var is set for *key*."""
_ensure_initialized()
return _checker.has_env_override(key)


def get_registry() -> FeatureRegistry:
"""Return the underlying ``FeatureRegistry``."""
_ensure_initialized()
return _checker.registry


# ---------------------------------------------------------------------------
# Test support
# ---------------------------------------------------------------------------

def _reset_for_testing() -> None:
"""Reset all module state for test isolation.

Clears the checker, initialized flag, and the OpenFeature global provider
so state never leaks between test files.
"""
global _checker, _initialized
with _lock:
_checker = None
_initialized = False
openfeature_api.clear_providers()
64 changes: 64 additions & 0 deletions src/solace_agent_mesh/common/features/fastapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""
FastAPI dependency helpers for feature flag gating.

These helpers wrap the OpenFeature client for use as FastAPI ``Depends``
injectors. They are framework-specific and live here rather than in
``core.py`` to keep the core module framework-agnostic.

Usage::

from solace_agent_mesh.common.features.fastapi import (
require_feature,
get_feature_value,
)

# Hard-gate an entire endpoint — returns 404 when flag is off:
@router.get("/my-endpoint")
async def my_endpoint(_: None = Depends(require_feature("my_flag"))):
...

# Inject the flag value for soft behaviour changes:
@router.get("/my-endpoint")
async def my_endpoint(flag: bool = Depends(get_feature_value("my_flag"))):
if flag:
...
"""

from __future__ import annotations

from collections.abc import Callable

from fastapi import HTTPException, status
from openfeature import api as openfeature_api


def require_feature(key: str) -> Callable:
"""
FastAPI dependency factory that hard-gates an endpoint on a feature flag.

Raises HTTP 404 when the flag is disabled so that the endpoint appears
non-existent to callers. Use when the entire endpoint should be
unavailable while the flag is off.

For soft behaviour changes based on a flag, use :func:`get_feature_value`.
"""
def _check() -> None:
if not openfeature_api.get_client().get_boolean_value(key, False):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Feature '{key}' is not enabled.",
)
return _check


def get_feature_value(key: str) -> Callable:
"""
FastAPI dependency factory that injects the current boolean value of a
feature flag into an endpoint without raising on disabled.

Use when the endpoint should remain accessible but needs to vary its
behaviour based on whether the flag is on or off.
"""
def _resolve() -> bool:
return openfeature_api.get_client().get_boolean_value(key, False)
return _resolve
31 changes: 17 additions & 14 deletions src/solace_agent_mesh/common/features/features.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,20 @@
# Example: Background Tasks
#
# release_phase (string, required)
# Lifecycle stage of the feature. One of:
# early_access - Limited release to specific customers for early feedback.
# beta - Broadly available but not yet fully stabilised.
# experimental - Proof-of-concept; may change or disappear at any time.
# ga - Generally available; stable and supported.
# Lifecycle stage of the feature. Aligned with Solace Cloud release stages
# (https://docs.solace.com/Cloud/stages_concept.htm). One of:
# experimental - Limited availability; proof-of-concept only.
# early_access - Closed group of testers; may change incompatibly.
# beta - Broadly available; complete but may have open issues.
# controlled_availability - Fully functional; available on request.
# general_availability - Open to all; production-ready with full SLA coverage.
# deprecated - Scheduled for removal.
#
# default_enabled (boolean, required)
# default (boolean, required)
# Baseline on/off state. In effect when no SAM_FEATURE_<KEY>
# environment variable is set.
#
# jira_epic (string, required)
# jira (string, required)
# Jira epic key that owns this feature (e.g. DATAGO-118673).
# Required so that every flag can be traced back to its product work.
#
Expand All @@ -36,7 +39,7 @@
# EVALUATION PRIORITY (highest wins)
# -----------------------------------
# 1. SAM_FEATURE_<UPPER_KEY> environment variable (e.g. SAM_FEATURE_BACKGROUND_TASKS=true)
# 2. default_enabled
# 2. default
#
# ENTERPRISE EXTENSIONS
# ----------------------
Expand All @@ -50,18 +53,18 @@ features:
# Example flag — additional flags are registered in DATAGO-126647 (flag migration story).
- key: background_tasks
name: Background Tasks
release_phase: ga
default_enabled: false
jira_epic: DATAGO-126647
release_phase: general_availability
default: false
jira: DATAGO-126647
description: >
Run long-running agent tasks in the background and receive results
when they complete.

- key: model_config_ui
name: Model Configuration UI
release_phase: ga
default_enabled: false
jira_epic: DATAGO-109787
release_phase: general_availability
default: false
jira: DATAGO-109787
description: >
Enable the Models tab in the Agent Mesh interface to view and manage
LLM model configurations. When disabled, the Models tab is hidden.
Loading
Loading