Skip to content
AgentEnv Framework
RegistrySecret Store

Secret Store

Where the values behind secret references come from, so no secret sits in your config file

The secret store holds the values behind secret: references, so config.toml names keys and never holds a secret. It is one of the registry's four stores.

References

A config value that is exactly secret:KEY resolves through the secret store, while env:NAME reads the process environment and never asks the store. Both take a ?default, and a reference with neither a value nor a default raises ConfigError.

.agentenv/config.toml
[model]
base_url = "env:MODEL_URL?http://localhost:4000"
api_key  = "secret:MODEL_KEY"

[stores.secret] itself can use only env:, since the store can't resolve its own references.

What reads it

The model key comes from LITELLM_API_KEY when that variable is set, and otherwise from [model] api_key, resolved on each call rather than when the file is read. A deploy passes it to the agent's container as LITELLM_API_KEY, as Deploying your agent shows.

The secret: references in store, provider, runner and plugin tables resolve through it too, when the framework builds what needs them. SecretStoreCredentials reads registry logins from the registry_auths key by default, and the modal providers fall back to modal_token_id and modal_token_secret when neither the MODAL_TOKEN_* variables nor ~/.modal.toml has a token.

Use a Supported Secret Store

Three secret stores are built in. A [stores.secret] table selects one by its impl and passes its settings under config.

Environment Variables and a File

The default, which needs no table and reads only environment variables. Name it to add a flat YAML or JSON mapping at file_path and a values table: environment variables still come first, then values, then the file. ~ in file_path is not expanded.

.agentenv/config.toml
[stores.secret]
impl = "agent_env.store.secret_store:LocalSecretStore"
config = { file_path = "/srv/agent-env/secrets.yaml" }

AWS Secrets Manager

It reads one secret holding a flat JSON or YAML mapping of every key, signs in through the standard boto3 credential chain, and needs secretsmanager:GetSecretValue on that secret:

.agentenv/config.toml
[stores.secret]
impl = "agent_env.store.secret_store:AwsSecretsManagerSecretStore"
[stores.secret.config]
secret_name = "my-team/agent-env"
region      = "us-west-2"

Google Cloud Secret Manager

From the gcp extra. It reads one secret version, latest by default, holding the same kind of mapping, signs in with Application Default Credentials, and needs roles/secretmanager.secretAccessor on the secret:

.agentenv/config.toml
[stores.secret]
impl = "agent_env.store.secret_store.gcp_secret_manager_secret_store:GcpSecretManagerSecretStore"
config = { secret_name = "agent-env", project = "<project>" }

Both cloud stores fetch the mapping on the first lookup and cache it for ttl_seconds, 300 by default, and a failed re-fetch keeps serving the last good copy.

agent-env config explain secret prints the store in effect without building it, and masks the secret's name:

Output
secret
  AwsSecretsManagerSecretStore  secret_name=***  region=us-west-2
  from [stores.secret]

Write Your Own Secret Store

A store of your own subclasses SecretStore and implements one method, get(name). It returns the value, None for a key it doesn't hold, and an empty value as an empty string.

This one reads HashiCorp Vault, which no built-in store covers: one KV v2 secret holding every key, cached like the built-in cloud stores. It calls Vault's HTTP API through httpx, which the framework already depends on:

mycorp/vault_store.py
"""A SecretStore on HashiCorp Vault: one KV v2 secret holding every key."""

from __future__ import annotations

import logging
import threading
import time

import httpx

from agent_env.store.secret_store import SecretStore

logger = logging.getLogger(__name__)


class VaultSecretStore(SecretStore):
    """Reads the KV v2 secret at `<mount>/<path>`, whose keys are the names `secret:`
    references use. It is read once per `ttl_seconds`, and a failed re-read keeps
    serving the last good copy."""

    def __init__(
        self,
        address: str,
        token: str,
        path: str,
        mount: str = "secret",
        ttl_seconds: float = 300,
    ) -> None:
        headers = {"X-Vault-Token": token}
        self._client = httpx.Client(base_url=address, headers=headers, timeout=10)
        self._url = f"/v1/{mount}/data/{path}"
        self._ttl_seconds = ttl_seconds
        self._lock = threading.Lock()
        self._values: dict | None = None
        self._read_at = 0.0

    def _read(self) -> dict:
        response = self._client.get(self._url)
        response.raise_for_status()
        return response.json()["data"]["data"]

    def _current(self) -> dict:
        with self._lock:
            stale = time.monotonic() - self._read_at >= self._ttl_seconds
            if self._values is None or stale:
                try:
                    self._values = self._read()
                except Exception:
                    if self._values is None:
                        raise
                    logger.warning(
                        "Re-reading %s failed; serving the last copy", self._url
                    )
                self._read_at = time.monotonic()
            return self._values

    def get(self, name: str) -> str | None:
        value = self._current().get(name)
        return None if value is None else str(value)

You select it like a built-in. Its address and token come from env: references, since the secret store can't resolve secret: ones in its own table:

.agentenv/config.toml
[stores.secret]
impl = "mycorp.vault_store:VaultSecretStore"
[stores.secret.config]
address = "env:VAULT_ADDR"
token   = "env:VAULT_TOKEN"
path    = "agent-env"

It passes all 3 SecretStore conformance cases against a Vault dev server, seeded with the suite's FIXTURE. The suite ships in the agentenv-framework repository's tst/, not in the wheel, so run it from a clone:

tests/test_vault_store.py
import uuid

import httpx
import pytest

from mycorp.vault_store import VaultSecretStore
from tst.store import secret_conformance

# vault server -dev -dev-root-token-id=root
ADDRESS, TOKEN = "http://127.0.0.1:8200", "root"


@pytest.fixture(scope="module")
def store():
    path = f"conformance-{uuid.uuid4().hex}"
    httpx.post(
        f"{ADDRESS}/v1/secret/data/{path}",
        headers={"X-Vault-Token": TOKEN},
        json={"data": secret_conformance.FIXTURE},
    ).raise_for_status()
    return VaultSecretStore(ADDRESS, TOKEN, path)


@pytest.mark.parametrize("case", secret_conformance.CASES, ids=lambda c: c.__name__)
def test_conformance(case, store):
    case(store)

The modal providers' fallback to modal_token_id and modal_token_secret reads a built-in store's whole mapping, so with a store of your own, give Modal its token through MODAL_TOKEN_ID and MODAL_TOKEN_SECRET or ~/.modal.toml.

Last updated on

Ask a question · Report an issue

On this page