# Secret Store (https://www.agentenvframework.com/docs/registry/secret-store)

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

1. `api_key = "secret:MODEL_KEY"` names a key in the secret store, so the file never holds the key itself.
2. The local store checks environment variables first, then its `file_path` file, where it finds `MODEL_KEY`.
3. The value reaches the agent’s container as `LITELLM_API_KEY`, and the file still holds only the reference.
4. With AWS Secrets Manager, the first lookup fetches one bundle secret holding every key, and caches it.
5. Another lookup in the same process within `ttl_seconds`, 300 by default, reads the cache and makes no call to AWS.

Parts of the scene:

- **[stores.secret]**: Picks the secret store. Without the table the store is `LocalSecretStore` with no file; its own config takes `env:` references but not `secret:` ones.
- **A secret: reference**: A value that is exactly `secret:KEY` resolves through the secret store when the framework builds what needs it; `?default` gives a fallback. `[model] api_key` resolves on each call.
- **The framework**: Resolves each reference by asking the secret store for its key. For the model key, a `LITELLM_API_KEY` environment variable wins over `[model] api_key`.
- **The secret store**: Answers `get(name)` with the value, or nothing. `LocalSecretStore` is the default; AWS Secrets Manager and Google Cloud Secret Manager are built in too, or you write one.
- **Environment variables**: The local store checks the process environment first, unless `use_env = false`. Here `MODEL_KEY` is not set, so it looks further.
- **file_path**: A flat YAML or JSON mapping of name to value, read once when the store is built. Entries in a `values` table win over the file.
- **The cache**: The fetched bundle, held in the process and fetched again once it is older than `ttl_seconds`; `0` keeps it for the process’s lifetime. A failed re-fetch keeps serving the last good bundle.
- **The bundle secret**: One secret, `secret_name`, holding a YAML or JSON mapping of every key. Google Cloud Secret Manager reads its bundle the same way.
- **The agent’s container**: `claudius-4msj7n4d`, from Deploying your agent. A deploy sets `LITELLM_API_KEY` in its environment, unless the deploy’s env vars or the agent’s defaults already set it.

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](https://www.agentenvframework.com/docs/core-concepts.md#the-registry).

## 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`.

```toml title=".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](https://www.agentenvframework.com/docs/agents/deploying.md#point-it-at-a-model)
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.

```toml title=".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:

```toml title=".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:

```toml title=".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:

```text title="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:

```python title="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:

```toml title=".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](https://github.com/scaleapi/agentenv-framework/tree/main/tst/store)
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:

```python title="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`.