Skip to content

keyring

keyring

API key keyring — rotation, cooldown tracking, key selection.

The conductor maintains a keyring of API keys per instrument. Keys are NEVER stored in config files, score YAML, or anything in the git repo. Keys live in $SECRETS_DIR/ and are referenced by path.

Key files are read at selection time. The key value is returned to the caller and not cached — minimizing the time secrets live in memory.

Thread safety: all mutable state is protected by an asyncio Lock.

Classes

ApiKeyKeyring

ApiKeyKeyring(config)

Manages API key selection with rotation and rate limit tracking.

Reads KeyringConfig from daemon config. Loads keys from disk at selection time (path references, never caches values). Selects keys via configurable rotation policies.

Usage::

keyring = ApiKeyKeyring(config.keyring)

if keyring.has_keys("openrouter"):
    key = await keyring.select_key("openrouter")
    # Use key for API request
    # If rate limited:
    keyring.report_rate_limit("openrouter", key_index=0, cooldown_seconds=30.0)
Source code in src/marianne/daemon/keyring.py
def __init__(self, config: KeyringConfig) -> None:
    self._config = config

    # Per-instrument, per-key state for cooldown tracking
    self._states: dict[str, list[_KeyState]] = {}
    for instrument_name, instr_keyring in config.instruments.items():
        self._states[instrument_name] = [
            _KeyState() for _ in instr_keyring.keys
        ]

    # Round-robin index per instrument
    self._rr_index: dict[str, int] = dict.fromkeys(config.instruments, 0)

    # Protect mutable state
    self._lock = asyncio.Lock()
Functions
has_keys
has_keys(instrument_name)

Check if keys are configured for an instrument.

Source code in src/marianne/daemon/keyring.py
def has_keys(self, instrument_name: str) -> bool:
    """Check if keys are configured for an instrument."""
    return instrument_name in self._config.instruments
select_key async
select_key(instrument_name)

Select and load an API key for the given instrument.

Reads the key file from disk. The key value is returned to the caller and not cached in the keyring.

Parameters:

Name Type Description Default
instrument_name str

Instrument to select a key for.

required

Returns:

Type Description
str

The API key string.

Raises:

Type Description
KeyError

If no keys are configured for the instrument.

FileNotFoundError

If the key file doesn't exist.

ValueError

If the key file is empty.

Source code in src/marianne/daemon/keyring.py
async def select_key(self, instrument_name: str) -> str:
    """Select and load an API key for the given instrument.

    Reads the key file from disk. The key value is returned to the
    caller and not cached in the keyring.

    Args:
        instrument_name: Instrument to select a key for.

    Returns:
        The API key string.

    Raises:
        KeyError: If no keys are configured for the instrument.
        FileNotFoundError: If the key file doesn't exist.
        ValueError: If the key file is empty.
    """
    if instrument_name not in self._config.instruments:
        msg = (
            f"No keys configured for instrument '{instrument_name}'. "
            f"Configured instruments: {sorted(self._config.instruments.keys())}"
        )
        raise KeyError(msg)

    instr_keyring = self._config.instruments[instrument_name]
    rotation = instr_keyring.rotation

    async with self._lock:
        if rotation == "round-robin":
            key_index = self._select_round_robin(instrument_name, instr_keyring)
        else:
            key_index = self._select_least_recently_rate_limited(
                instrument_name, instr_keyring,
            )

    key_entry = instr_keyring.keys[key_index]
    key_value = self._load_key_from_disk(key_entry.path)

    _logger.debug(
        "keyring.key_selected",
        extra={
            "instrument": instrument_name,
            "key_label": key_entry.label,
            "key_index": key_index,
            "rotation": rotation,
        },
    )

    return key_value
report_rate_limit
report_rate_limit(instrument_name, key_index, *, cooldown_seconds)

Report that a key hit a rate limit.

Parameters:

Name Type Description Default
instrument_name str

The instrument whose key was rate-limited.

required
key_index int

Index of the key in the instrument's key list.

required
cooldown_seconds float

How long to wait before retrying this key.

required

Raises:

Type Description
KeyError

If the instrument is not configured.

IndexError

If key_index is out of range.

Source code in src/marianne/daemon/keyring.py
def report_rate_limit(
    self,
    instrument_name: str,
    key_index: int,
    *,
    cooldown_seconds: float,
) -> None:
    """Report that a key hit a rate limit.

    Args:
        instrument_name: The instrument whose key was rate-limited.
        key_index: Index of the key in the instrument's key list.
        cooldown_seconds: How long to wait before retrying this key.

    Raises:
        KeyError: If the instrument is not configured.
        IndexError: If key_index is out of range.
    """
    if instrument_name not in self._states:
        msg = f"No keys configured for instrument '{instrument_name}'"
        raise KeyError(msg)

    states = self._states[instrument_name]
    if key_index < 0 or key_index >= len(states):
        msg = (
            f"Key index {key_index} out of range for instrument "
            f"'{instrument_name}' (has {len(states)} keys)"
        )
        raise IndexError(msg)

    states[key_index].cooldown_until = time.monotonic() + cooldown_seconds

    _logger.debug(
        "keyring.rate_limit_reported",
        extra={
            "instrument": instrument_name,
            "key_index": key_index,
            "cooldown_seconds": cooldown_seconds,
        },
    )
get_key_label
get_key_label(instrument_name, key_index)

Get the human-readable label for a key.

Parameters:

Name Type Description Default
instrument_name str

The instrument name.

required
key_index int

Index of the key.

required

Returns:

Type Description
str

The label string.

Raises:

Type Description
KeyError

If the instrument is not configured.

IndexError

If key_index is out of range.

Source code in src/marianne/daemon/keyring.py
def get_key_label(self, instrument_name: str, key_index: int) -> str:
    """Get the human-readable label for a key.

    Args:
        instrument_name: The instrument name.
        key_index: Index of the key.

    Returns:
        The label string.

    Raises:
        KeyError: If the instrument is not configured.
        IndexError: If key_index is out of range.
    """
    if instrument_name not in self._config.instruments:
        msg = f"No keys configured for instrument '{instrument_name}'"
        raise KeyError(msg)
    keys = self._config.instruments[instrument_name].keys
    if key_index < 0 or key_index >= len(keys):
        msg = f"Key index {key_index} out of range (has {len(keys)} keys)"
        raise IndexError(msg)
    return keys[key_index].label

Functions