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.
[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.
[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:
[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:
[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:
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:
"""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:
[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:
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