honest framework

honest-features

A feature flag is a named state that routes execution to a handler.

Feature flags as a handler table. The flag state routes to a handler — no if/else, no hidden branching.
What you do now
# Environment variable flags
import os

NEW_CHECKOUT = os.getenv("NEW_CHECKOUT", "false") == "true"

def handle_checkout(manifest):
    if NEW_CHECKOUT:
        return new_checkout_handler(manifest)
    else:
        return legacy_checkout_handler(manifest)
# Read at import time. Changing it requires a restart.
# if/else dispatch. No record of what flags exist.
What honest-features does
from honest_features import vocabulary, feature_state

FEATURES = {
    "new_checkout": {"states": {"on", "off"}, "default": "off"},
}

CHECKOUT_HANDLERS = {
    "on":  new_checkout_handler,
    "off": legacy_checkout_handler,
}

def handle_checkout(manifest):
    return CHECKOUT_HANDLERS[feature_state("new_checkout")](manifest)
# Toggle via API. No restart. No if/else. Full flag inventory.
What you do now
if Flipper[:new_checkout].enabled?
  new_checkout_handler(manifest)
else
  legacy_checkout_handler(manifest)
end
# Database-backed flag state.
# if/else dispatch.
# The Flipper gem is a runtime dependency.
What honest-features does
FEATURES = {
  "new_checkout" => { states: %w[on off], default: "off" }
}

CHECKOUT_HANDLERS = {
  "on"  => method(:new_checkout_handler),
  "off" => method(:legacy_checkout_handler),
}

def handle_checkout(manifest)
  CHECKOUT_HANDLERS[feature_state("new_checkout")].call(manifest)
end
# Toggle via HMAC API. No restart. No if/else.
What you do now
if (config('features.new_checkout')) {
    return newCheckoutHandler($manifest);
} else {
    return legacyCheckoutHandler($manifest);
}
// Config file flag. Restart required to change.
// if/else dispatch.
What honest-features does
$features = [
    'new_checkout' => ['states' => ['on','off'], 'default' => 'off'],
];

$checkoutHandlers = [
    'on'  => 'newCheckoutHandler',
    'off' => 'legacyCheckoutHandler',
];

function handleCheckout($manifest) {
    return $checkoutHandlers[featureState('new_checkout')]($manifest);
}
// Toggle via HMAC API. No restart. No if/else.
What you do now
var newCheckout = os.Getenv("NEW_CHECKOUT") == "true"

func handleCheckout(manifest map[string]any) map[string]any {
    if newCheckout {
        return newCheckoutHandler(manifest)
    }
    return legacyCheckoutHandler(manifest)
}
// Read at startup. Restart required to change.
// if/else dispatch. No flag inventory.
What honest-features does
var Features = map[string]Feature{
    "new_checkout": {States: Set{"on","off"}, Default: "off"},
}

var CheckoutHandlers = map[string]Handler{
    "on":  newCheckoutHandler,
    "off": legacyCheckoutHandler,
}

func handleCheckout(manifest Manifest) Manifest {
    return CheckoutHandlers[FeatureState("new_checkout")](manifest)
}
// Toggle via HMAC API. No restart. No if/else.
The conventional pattern
# Flag state in environment variables or config files.
# Changing a flag requires a restart or redeploy.
# Dispatch is if/else.
# The full set of flags is implicit — read the codebase to find them.
# Testing requires environment manipulation.
The honest pattern
FEATURES = {
    "new_checkout": {"states": {"on","off"}, "default": "off"},
    "pricing":      {"states": {"a","b","control"}, "default": "control"},
}
# Vocabulary is the complete flag inventory.
# State is ephemeral in-memory data.
# Toggle via HMAC API — no restart, no redeploy.
# Dispatch is a handler table. Tests mutate _state directly.

You have an API endpoint. Someone calls it with ?sort=name&order=asc&page=3. At the other end of your chain, a function expects sort to be either "on" or "off", because this vocabulary was designed for a flag, and you wired the wrong vocab to this handler. The call succeeds. The wrong handler runs. No error.

This is the problem honest-features solves. A feature flag is a named state. A named state has a finite set of valid values. Code that acts on that state should only ever see one of those values — and should fail loudly, at the point of declaration, if you try to dispatch to a state that does not exist.

The vocabulary

Flags are declared once, as a plain dict:

FEATURES = {
    "new_checkout": {"states": {"on", "off"},          "default": "off"},
    "pricing":      {"states": {"a", "b", "control"},  "default": "control"},
}

The vocabulary is the authoritative list. Add a flag here, it exists everywhere. Try to reference an undeclared flag and you get a KeyError immediately — not a silent wrong branch, not a None default, not a flag that behaves as if it is off without saying so.

The handler table

The typical pattern is if flag == "on": ... elif flag == "off": .... The problem: there is no enforcement that both branches exist. Add a third state and the if/elif chain silently takes the else path, or raises, or does nothing, depending on how it was written.

A handler table cannot do this:

CHECKOUT_HANDLERS = {
    "on":  new_checkout_handler,
    "off": legacy_checkout_handler,
}

def handle_checkout(manifest):
    return CHECKOUT_HANDLERS[feature_state("new_checkout")](manifest)

If "on" is in FEATURES["new_checkout"]["states"] but not in CHECKOUT_HANDLERS, the lookup fails immediately when that state is dispatched to — not silently, not later, right there. Add a new state to the vocabulary and every handler table that does not cover it breaks explicitly.

Toggling state

State changes via a signed API call. No restart. No redeploy. No environment variable:

POST /hf/features/set
{ "flag": "new_checkout", "state": "on", "timestamp": 1710000000, "signature": "..." }

The signature covers the full payload — flag, state, and timestamp together. This prevents replay attacks (a valid request cannot be resent with a different body) and timestamp replay (old requests expire). On process restart, flags reinitialize from vocabulary defaults. A restarted process always starts from a known declared state.

Testing

_state is a plain dict. Tests set it directly:

def test_new_checkout_on():
    _state["new_checkout"] = "on"
    result = handle_checkout(manifest)
    assert result == expected_new

No API calls. No environment manipulation. A pytest fixture resets all flags to defaults between tests. honest-test generates every state combination automatically from the vocabulary.

The abstract principle

A feature flag is a named dimension of variation in a program's behavior. The set of valid values for that dimension is finite and declared. The current value is ephemeral runtime state.

These three sentences define the entire design space. Every implementation decision follows from them.

Dispatch as a total function

Routing behavior based on flag state is a function from state to handler:

dispatch : FlagState → Handler

For this function to be total — defined for every possible input — the handler table must contain an entry for every member of the flag's state set. A missing entry is an incomplete function definition, not a runtime edge case. honest-check HC-HF002 catches incomplete handler tables at static analysis time.

An if/else conditional on flag state is an incomplete, implicit, unverifiable representation of the same function. It cannot be checked for totality without running it. The handler table can be checked for totality by inspecting the keys: set(FEATURES[flag]["states"]) == set(HANDLER_TABLE.keys()). This is a set equality check — the same operation that honest-check performs.

Ephemeral state is not a compromise

_state — the in-memory dict — resets to vocabulary defaults on process restart. This is described in the design as a feature. It is not a compromise forced by implementation convenience. It is a formal property: every process starts from a declared, known state. The flag vocabulary is the specification of valid initial states. The defaults are the specified starting point.

If you need persistence across restarts, you persist the state externally and restore it via the toggle API after startup. The mechanism is explicit. The startup state is always declared and known. There are no hidden implicit state sources.

HMAC as intent binding

The toggle endpoint uses HMAC-SHA256 over {flag}:{state}:{timestamp}. A bearer token proves identity but not intent. An intercepted request with a valid bearer token can be replayed with a different body — different flag, different state. The HMAC signature covers the full payload: flag name, target state, and timestamp. A tampered body invalidates the signature. An intercepted and replayed request is rejected by the timestamp window.

This is the principle of intent binding: the cryptographic proof covers not just who sent the request but what they intended. The signature cannot be detached from the payload and reused for a different intention. This is the same principle that prevents SQL injection (parameterized queries bind the intent to specific values) and CSRF protection (tokens bind the intent to a specific form submission).

Why environment variables are dishonest

An environment variable is a configuration value that: - Has no declared vocabulary (any string is valid) - Has no declared set of valid states (no exhaustive list) - Changes require a process restart (not at runtime) - Cannot be toggled without infrastructure access - Emits no events when changed

FEATURES is honest about all five dimensions. The vocabulary declares valid states. Toggling requires no restart. The toggle API emits hf.features.changed to the event log. The full flag inventory is readable at module scope. Every property that makes environment variables dishonest, the vocabulary dict makes honest.

Full specification

The Flag Vocabulary Schema

FEATURES: dict[str, dict] = {
    "flag_name": {
        "states":  set[str],   # complete set of valid states, min 2 members
        "default": str,        # must be a member of states
    },
    ...
}

Rules: - states must contain at least two members - default must be a member of states - No other keys are permitted at the vocabulary level - The vocabulary is declared at module scope, not constructed dynamically - FEATURES is the canonical inventory of all flags. No flag may be referenced that is not declared here.

The _state Dict

_state: dict[str, str] = {k: v["default"] for k, v in FEATURES.items()}

_state is initialized at module import time. No I/O. No environment variables. No config files. Initialization is deterministic.

The feature_state() Contract

def feature_state(flag: str) -> str:
    return _state[flag]
  • Returns the current state of the named flag
  • Raises KeyError for undeclared flag names — not a silent default
  • This is intentional: an undeclared flag reference is a programming error, not a runtime condition

The Toggle Endpoint

POST /hf/features/set
Content-Type: application/json

{
    "flag":      string,    # must be in FEATURES
    "state":     string,    # must be in FEATURES[flag]["states"]
    "timestamp": integer,   # Unix timestamp
    "signature": string,    # HMAC-SHA256 hex digest
}

Validation order: 1. flag in FEATURES → 400 if not 2. state in FEATURES[flag]["states"] → 400 if not 3. timestamp within replay window → 403 if outside 4. signature valid → 403 if not

Signature construction:

message   = f"{flag}:{state}:{timestamp}"
signature = hmac_sha256(secret, message)

Signature verification must use hmac.compare_digest (or language equivalent constant-time comparison). String equality comparison is vulnerable to timing attacks.

Replay window: configurable, default 60 seconds. Requests with |now() - timestamp| > window are rejected with 403.

Successful response (200):

{"flag": "new_checkout", "state": "on", "previous": "off"}

The Handler Table Pattern

Every dispatch on flag state must use a handler table. if/else on feature_state() return values is an HC-P001 violation.

# Required pattern
HANDLERS: dict[str, Callable] = {
    "on":  handler_a,
    "off": handler_b,
}

def dispatch(manifest: dict) -> dict:
    return HANDLERS[feature_state("flag_name")](manifest)

Handler table completeness: the table must contain an entry for every state in the flag's vocabulary. A missing state raises KeyError at dispatch time. honest-check HC-HF002 catches incomplete handler tables statically.

honest-observe Integration

The toggle endpoint emits on every successful state change:

{
    "event_type":    "hf.features.changed",
    "flag":          "flag_name",
    "previous":      "old_state",
    "state":         "new_state",
    "timestamp":     1710000000,
    "requesting_ip": "10.0.0.1"
}

feature_state() emits on every call within a request context:

{
    "event_type":  "hf.features.evaluated",
    "flag":        "flag_name",
    "state":       "current_state",
    "request_id":  "req_abc123"
}

Evaluation events outside a request context (background tasks, startup) are not emitted.

Testing Contract

Tests manipulate _state directly. No API calls. No process restart. No environment manipulation.

from features import _state, FEATURES

def test_flag_on():
    _state["flag_name"] = "on"
    result = dispatch(manifest)
    assert result == expected

# Reset fixture — must be autouse
@pytest.fixture(autouse=True)
def reset_features():
    yield
    for flag, spec in FEATURES.items():
        _state[flag] = spec["default"]

The reset fixture must be autouse=True. Tests that do not reset _state produce ordering-dependent results, which is a honesty violation.

honest-check Rules

RuleDescriptionSeverity
HC-HF001feature_state() call references undeclared flag nameError
HC-HF002Handler table missing entry for declared flag stateWarning

Conformance Requirements

RequirementTest
_state initialized from FEATURES defaults at import, no I/OVerify no network/file calls
feature_state() raises KeyError for undeclared flagTest with unknown flag name
Toggle endpoint validates flag before stateSend invalid flag, verify 400
Toggle endpoint validates state against vocabularySend invalid state, verify 400
Signature uses constant-time comparisonCode review / linter
Replay window rejects stale timestampsSend timestamp 61s ago, verify 403
Process restart reinitializes _state from defaultsRestart, verify all flags at default
hf.features.changed emitted on successful toggleToggle, check event log
hf.features.evaluated emitted per feature_state() call in request contextInstrument and verify
No if/else dispatch on flag state anywhere in applicationhonest-check HC-P001

Reference