AgentEnv Framework
Configuration

Configuration reference

Every .agentenv/config.toml table, key, and precedence rule

agent-env reads one file, .agentenv/config.toml, and finds it git-style: it walks up from the current working directory to the nearest .agentenv/config.toml. Nothing in it is required — with no file at all agent-env runs on the local stores Getting Started assumes — and AGENT_ENV_CONFIG overrides the walk with an explicit path, which must point at a file that exists: a typo raises ConfigError rather than silently dropping your whole configuration.

AGENT_ENV_CONFIG also moves your local database

Local store state is rooted at the directory holding the discovered config file. Found by the upward walk, that directory is your project's .agentenv/, so SQLite lands in .agentenv/document_store/documents.db. Point AGENT_ENV_CONFIG at /etc/agent-env/prod.toml and the same default writes to /etc/agent-env/document_store/documents.db — a different, empty database, with nothing logged about the switch. agent-env creates that directory and writes a .gitignore into it covering document_store/ and object_store/.

A complete example

One realistic file: a shared MongoDB + S3 control plane, images on GHCR, secrets from one AWS Secrets Manager bundle, envs on E2B VMs, and one custom step.

# .agentenv/config.toml

[stores.document]
impl = "agent_env.store.document_store:MongoDocumentStore"
[stores.document.config]
uri      = "secret:mongodb_uri"     # read through [stores.secret], never stored here
database = "agent_env"

[stores.object]
impl = "agent_env.store.object_store:S3ObjectStore"
[stores.object.config]
bucket = "my-team-agent-env"
region = "us-west-2"

[stores.image]
impl = "agent_env.store.image_store:OciRegistryImageStore"
[stores.image.config]
registry_host     = "ghcr.io"
repository_prefix = "my-org/agent-env"
[stores.image.config.credentials]
impl       = "agent_env.store.image_store:SecretStoreCredentials"
secret_key = "registry_auths"       # a Docker config.json-shaped JSON string

[stores.secret]
impl = "agent_env.store.secret_store:AwsSecretsManagerSecretStore"
[stores.secret.config]
secret_name = "my-team/agent-env"   # one secret holding a flat name -> value mapping
region      = "us-west-2"

[sandbox]
default       = "e2b,local"         # fallback chain: E2B first, then the host Docker daemon
agent_default = "e2b"
[sandbox.providers.e2b.config]
api_key       = "secret:e2b_api_key"
base_template = "agent-env-docker-v2"   # an immutable, versioned E2B template
[sandbox.attribution]
team = "platform"                   # filled into any dimension a caller leaves unset

[model]
base_url = "https://openrouter.ai/api/v1"
api_key  = "secret:openrouter_api_key"
default  = "openrouter/anthropic/claude-sonnet-4"
[model.roles]
judge = "openrouter/openai/gpt-4o"

[task_steps]
impls = ["mycorp.steps:GradeEssayStep"]

[runner]
impl = "agent_env.runner.local_runner:LocalRunner"
[runner.config]
workers = 4

Every seam has the same shape: an impl pointer — the string module.path:ClassName — plus a config table. agent-env imports the class, checks it subclasses the expected interface, and constructs it with YourClass.from_config(**config). The default from_config forwards the table straight to __init__. The extension model covers what each seam expects of the class you point at, and how the failure policies differ between seams.

Value interpolation

A config value that is exactly env:NAME or secret:KEY is a reference: agent-env resolves it when it builds the thing that needs it. Both forms take an optional ?default suffix — env:AWS_REGION?us-west-2 — and env:NAME? resolves to the empty string. Without a default, an unresolved reference raises ConfigError naming the reference.

[stores.object.config]
bucket = "env:MY_BUCKET"                  # from the process environment
region = "env:AWS_REGION?us-west-2"       # with a fallback

[stores.document.config]
uri = "secret:mongodb_uri"                # through the configured secret store

The reference must be the whole value. "postgres://env:DB_HOST/agent_env" is a literal string; agent-env does not substitute inside it. Lists and nested tables are walked recursively, and non-string values pass through untouched.

env: resolves against the process environment everywhere. secret: resolves through whatever [stores.secret] selects, which means secret values never appear in the file — only the key names. Where no secret store is available, a secret: reference fails loud instead of resolving to nothing. Not every table gets a resolver:

Tableenv:secret:
[stores.document.config], [stores.object.config], [stores.image.config] (including the nested credentials table)yesyes
[stores.secret.config]yesno — the secret store cannot resolve itself; fails loud
[sandbox.providers.<name>.config], [state.providers.<name>.config], [runner.config]yesyes
[model] base_url / default / [model.roles] / [model.params]yes, at first readyes, at first read
[model] api_keyyes, lazily per callyes, lazily per call
[sandbox.attribution]yesno — no resolver is passed; fails loud
[conversations] default_human_a2a_urlyesyes
[sandbox] default / agent_default, any impls list, any impl pointer, [explorer]nono

[model] api_key is deliberately late-bound: resolving it eagerly would make every read of the [model] table require credentials, including reads that never call a model.

Precedence

Four layers set the same values. Later layers win.

LayerExampleWins over
Built-in defaultlocal stores, local sandbox, LocalRunnernothing
.agentenv/config.toml[stores.document]built-in defaults
AGENT_ENV_* environment variableAGENT_ENV_DOCUMENT_STORE=localthe config file
Explicit Python callconfigure(document_store=...), set_runner(...)everything

Per setting, highest first:

SettingPrecedenceUnconfigured
Config file pathAGENT_ENV_CONFIG → upward walk to the nearest .agentenv/config.tomlno file; every default applies
Document / object / image / secret storeset_<kind>_store() or configure(<kind>_store=…) → AGENT_ENV_<KIND>_STORE → [stores.<kind>]local
Runnerset_runner() → AGENT_ENV_RUNNER → [runner]local (LocalRunner, workers=2)
Env / CUA / general sandbox--sandbox on a deploy, or --env-sandbox / --cua-sandbox on a run, or set_env_sandbox_provider() / set_cua_sandbox_provider() / set_sandbox_provider() in process → [sandbox] defaultlocal
Agent sandbox--sandbox on an agent deploy, or --agent-sandbox on a run, or set_agent_sandbox_provider() in process → [sandbox] agent_defaultlocal
Env state provider--env-state-type or the env_state_type deploy argumentlocal_postgres
Model endpointper-call base_override → LITELLM_BASE_URL → [model] base_urlnative routing for a provider-prefixed model; ConfigError for a bare name
Model API keycaller-supplied key → LITELLM_API_KEY → [model] api_keyConfigError where a key is needed
Model paramsthe prompt_agent step's model_params, merged per key → [model.params]{}
Attribution dimensionthe value a caller passes → [sandbox.attribution]the dimension is omitted
Human-A2A base URLAGENT_ENV_HUMAN_A2A_URL → configure(default_human_a2a_url=…) → [conversations]ConfigError at the step that needs it

Two rows deviate from the general rule and are worth reading twice. [conversations] puts the environment variable above the explicit configure() call, so local development can repoint a process at a locally-running endpoint and win over both the checked-in file and application code. [sandbox] has no environment variable at all: the only overrides are the per-command sandbox flags and the in-process setters, one of which exists per sandbox kind.

Registries are built once per process

The store, task-step, artifact, env, sandbox and state registries are memoized on first use. Editing config.toml has no effect on a running process, and reset_config() does not clear the type registries. Every process that reads, reconnects to, or reaps a resource — a second CLI invocation, a worker, the explorer — needs the same config.toml and the same custom modules importable.

Table reference

[stores]

Selects an implementation for each of the four stores. A bare string is an alias; a sub-table is an explicit impl + config.

[stores]
document = "local"
object   = "local"
image    = "local"
secret   = "local"

local is the only alias that resolves. Each store also recognises the name of its external backend — mongo for document, s3 for object, ecr for image, aws for secret — purely to raise ConfigError explaining that no coordinates are compiled into the package and that the backend needs a full [stores.<name>] table. Any other value raises too; note that the message names the AGENT_ENV_*_STORE variable even when the value came from the file.

[stores.<name>]

name is one of document, object, image, secret. Each table takes impl plus a config table whose keys are the class's from_config parameters. The built-ins:

Storeimplconfig keys
documentagent_env.store.document_store:LocalSqliteDocumentStorepath (str)
documentagent_env.store.document_store:MongoDocumentStoreuri (str, required), database (str, required)
objectagent_env.store.object_store:LocalFilesystemObjectStoreroot (str)
objectagent_env.store.object_store:S3ObjectStorebucket (str, required), region (str, optional)
imageagent_env.store.image_store:LocalRegistryImageStoreregistry_host (str, default localhost:5000), repository_prefix (str, default ""), credentials
imageagent_env.store.image_store:OciRegistryImageStoreregistry_host (str, required), repository_prefix (str, default ""), credentials
secretagent_env.store.secret_store:LocalSecretStorevalues (table), file_path (str), use_env (bool, default true)
secretagent_env.store.secret_store:AwsSecretsManagerSecretStoresecret_name (str, required), region (str, required), ttl_seconds (float, default 300.0), min_refresh_interval (float, default 10.0)

Any class subclassing DocumentStore, ObjectStore, ImageStore, or SecretStore on your PYTHONPATH works the same way — see Storage backends. A key beside impl and config is ignored with a log warning, not an error.

[stores.image.config.credentials]

A nested impl pointer resolved by the image store itself, against the OciRegistryCredentials interface. It mints docker-login material lazily, at authentication time, so secret rotation is observed without a restart.

[stores.image.config.credentials]
impl       = "agent_env.store.image_store:SecretStoreCredentials"
secret_key = "registry_auths"      # default: "registry_auths"

SecretStoreCredentials reads a Docker config.json-shaped JSON string from the secret store. It must stay a JSON string — a nested YAML mapping raises ConfigError. Inline base64 auth values work; credsStore and credHelpers entries are ignored, because the process that pulls the image has no helper binaries. The alternative built-in is agent_env.store.image_store:EcrCredentials, which requires region, access_key, and secret_key and mints a short-lived ECR token per pull.

[task_steps], [artifacts], [envs]

Three lists of impl pointers that register classes agent-env has never seen, so a stored document referencing an unknown type still deserializes.

[task_steps]
impls = ["mycorp.steps:GradeEssayStep"]

[artifacts]
impls = ["mycorp.artifacts:RubricArtifact"]

[envs]
impls = ["mycorp.envs:MyCustomEnv"]

Each class registers under its own type. None of the three takes a config table — a step's, artifact's, or env's runtime configuration comes from its stored document, not from the TOML. agent-env fails loud on an unimportable pointer, a class that does not subclass the expected base, a class that inherits the base type instead of declaring its own, and a type that collides with a built-in or another entry. Without registration, reading such a document raises Unknown task step type: … / Unknown artifact type … / Unknown env type: …. See Custom types.

[sandbox]

Process-wide compute defaults. default covers env, CUA, and general sandboxes; agent_default covers the agent sandbox. Both default to local. A comma-separated value is a fallback chain, tried left to right.

[sandbox]
default       = "e2b,local"
agent_default = "e2b"

Built-in names, always available: modal, modal_vm, e2b, local. Values here are read literally — env: references do not work in this table.

[sandbox.providers.<name>]

Registers a sandbox provider. The entry is either a bare impl pointer string or a table with impl and an optional config.

[sandbox.providers.my_cloud]
impl = "mycorp.compute:MyCloudSandboxProvider"
[sandbox.providers.my_cloud.config]
region  = "env:AWS_REGION?us-west-2"
ssh_key = "secret:my_cloud_ssh_key"

A built-in name may appear here with a config table only; its implementation is fixed, so setting impl on a built-in name raises ConfigError, as does any key other than config. For a custom provider, the registry name must equal the .type of the Sandbox objects it produces — that string is persisted and is how a later process rebuilds the provider to reconnect or tear down. agent-env enforces this at deploy: a mismatched sandbox is terminated and SandboxProviderTypeError names both values. See Providers.

A built-in may still need config of its own: e2b takes an api_key and a base_template naming an immutable, versioned E2B template, and raises ConfigError without the latter.

[sandbox.attribution]

Default cost-attribution dimensions. The map is open — core threads it through without reading it, and each provider picks out the keys its billing backend understands.

[sandbox.attribution]
team       = "platform"
project_id = "env:MY_PROJECT_ID"

Any dimension a caller leaves unset is filled from here; with no table, unset dimensions are omitted entirely. Values take env: references only.

[state.providers.<name>]

Registers a state provider: where an env's data lives. The only built-in is local_postgres, a co-deployed database container.

[state.providers.my_warehouse]
impl = "mycorp.state:MyWarehouseStateProvider"
[state.providers.my_warehouse.config]
region = "env:AWS_REGION?us-west-2"

The registry name must equal the class's type attribute, and — unlike sandbox providers — this is checked at registration, so the process fails before it provisions a store nothing can reattach to. Colliding with a built-in name raises ConfigError; keys beside impl and config are dropped with a warning. A deploy picks a state provider with --env-state-type or the env_state_type deploy argument. The extension model explains why the two provider seams check their names at different moments.

[model]

The model gateway: endpoint, credential, and the model names agent-env picks by role. Every key is optional, and the allowed set is closed — an unknown key raises ConfigError listing what is accepted.

[model]
base_url = "https://openrouter.ai/api/v1"
api_key  = "secret:openrouter_api_key"
default  = "openrouter/anthropic/claude-sonnet-4"

[model.roles]
agent = "openrouter/anthropic/claude-sonnet-4"
judge = "openrouter/openai/gpt-4o"

[model.params]
aws_region_name = "us-west-2"
KeyTypeDefault
base_urlstrnone; LITELLM_BASE_URL wins over it
api_keystr (usually a secret: reference)none; LITELLM_API_KEY wins over it
defaultstrnone
rolestable of role → model{}; an unset role falls back to default
paramstable{}

[model.roles] is an open map; agent-env itself asks for agent and judge. [model.params] is schema-free provider configuration — Azure's api_version, Bedrock's credentials — splatted onto the in-process model call and passed to a deployed agent that advertises support for it. It may not set a key agent-env already puts on the call: model, messages, api_key, api_base, user, metadata, timeout, response_format. Doing so raises ConfigError.

There is no default model endpoint

With no [model] table and no LITELLM_* variables, a provider-prefixed model such as openai/gpt-4o routes natively through LiteLLM, and a bare model name raises ConfigError: No model endpoint configured for …. agent-env compiles in no proxy URL, no API key, and no default model. Configure base_url + api_key, or use provider-prefixed names with that provider's own credentials. See Model gateway.

[conversations]

The base URL a human-in-the-loop step parks its conversation on. There is no default.

[conversations]
default_human_a2a_url = "https://hub.example.com/api/v1/a2a/human"

A non-string or blank value raises ConfigError at the config seam rather than inside a step. Resolution happens only where the URL is consumed, so runs that never contact a human work with this unset. The deploy step run-scopes what you configure by appending /instance/{instance_id}, so configure the base URL.

[runner]

Selects the dispatcher behind a submitted run. local is the only alias that resolves; temporal is recognised and raises, because an address, namespace, and certificates are deployment facts rather than package defaults.

[runner]
impl = "agent_env.runner.local_runner:LocalRunner"
[runner.config]
workers = 4        # default 2; must be >= 1

The runner's type is persisted as each run record's runner field, so a control plane restarted under a different [runner] logs a warning when it is asked about a run it did not submit.

[explorer]

Knobs for the local control plane that agent-env up serves.

[explorer]
port = 8234                                   # default 8234
# cors_origins = ["http://localhost:3000"]    # default: loopback origins only
# allowed_hosts = ["explorer.internal"]       # extra Host header values to accept
# static_dir = "path/to/built/ui"             # omit for API-only or the packaged UI

host is deliberately not configurable: the explorer is unauthenticated, so it always binds 127.0.0.1. Reach it from elsewhere over an SSH tunnel, or run your own server and opt hostnames in through allowed_hosts. [explorer.plugins].impls mounts additional API routers the same way [envs].impls registers env classes.

What is not validated

There is no whole-file schema check. An unknown top-level table — [task_step] instead of [task_steps] — is silently ignored: nothing registers and nothing is logged. Inside a store section, a key other than impl and config produces a log warning only. The fail-loud behaviour begins once agent-env is inside a table it recognises.

Environment variables

Every variable that changes agent-env's behaviour. agent-env never loads a .env file; export what you need yourself.

VariableOverridesDefault
AGENT_ENV_CONFIGconfig-file discovery, and the root of local store statethe nearest .agentenv/config.toml walking up from the CWD
AGENT_ENV_DOCUMENT_STORE[stores.document]local
AGENT_ENV_OBJECT_STORE[stores.object]local
AGENT_ENV_IMAGE_STORE[stores.image]local
AGENT_ENV_SECRET_STORE[stores.secret]local
AGENT_ENV_RUNNER[runner]local
AGENT_ENV_HUMAN_A2A_URL[conversations] default_human_a2a_url and configure()unset; resolution raises
AGENT_ENV_FIXTURE_PREFIXnothing; prepends <prefix>/ to artifact object keys so a fresh object store can share a bucket""
AGENT_ENV_LOCAL_SANDBOX_DIRthe local provider's work-directory root~/.agent-env-sandboxes
AGENT_ENV_MODAL_REGIONthe region the modal provider pins a gateway's sandboxes tous-east-1
AGENT_ENV_MODAL_APP_NAMEthe Modal app base nameagent-env
AGENT_ENV_LOAD_CONCURRENCYthe derived per-load concurrency cap; must be >= 1derived from the sandbox's core count
AGENT_ENV_SNAPSHOT_AFTER_LOADthe default for snapshot_after_load when a caller does not set it (1/true/yes)off
AGENT_ENV_INSECURE_TLSdisables TLS verification in the MCP spec-conformance verifier (1/true/yes)verification on
AGENT_ENV_RDS_HOST, _PORT, _DBNAME, _USERNAME, _PASSWORD, _SSLMODE, _AUTH, _REGIONa state provider's own admin-credential lookup. Setting _HOST selects the whole group; there is no per-key mergeunset; the provider looks up its own

Variables agent-env reads that are not AGENT_ENV_-prefixed: LITELLM_BASE_URL and LITELLM_API_KEY (both above [model]), MODAL_TOKEN_ID / MODAL_TOKEN_SECRET (above ~/.modal.toml and the secret store), and GITHUB_TOKEN (above the secret store). The AWS-backed stores and providers additionally use boto3's own credential chain.

AGENT_ENV_ENVIRONMENT appears in .env.example but agent-env itself reads nothing from it; it exists for a CLI plugin that chooses which config file AGENT_ENV_CONFIG lands on.

Defaults with no configuration

With no config.toml and no environment variables, agent-env is fully functional and entirely local. Paths below are relative to the discovered .agentenv/ directory, or to .agentenv/ under the current directory when no config file exists at all.

ConcernDefaultWhere it lands
Document storeLocalSqliteDocumentStore.agentenv/document_store/documents.db (stdlib SQLite, WAL mode)
Object storeLocalFilesystemObjectStore.agentenv/object_store/
Image storeLocalRegistryImageStorean OCI registry at localhost:5000; the push path brings up a registry:2 container if nothing is already serving that port
Secret storeLocalSecretStoreprocess environment variables; no file until you configure file_path
Sandboxlocal for both default and agent_defaultcontainers on the host Docker daemon, work dirs under ~/.agent-env-sandboxes
Env statelocal_postgresa database container co-deployed with the gateway
RunnerLocalRunner(workers=2)in-process asyncio; a run left behind by a previous process is failed at startup, not resumed
Explorerport 8234http://127.0.0.1:8234
Modelnoneany bare model name raises ConfigError

agent-env also creates .agentenv/ and writes a .gitignore there ignoring document_store/ and object_store/, so generated state stays out of git while config.toml stays tracked.

The local sandbox has no VM boundary

local runs env instances and agents as containers on your own Docker daemon, sharing your kernel and your network. It is the right default for developing a task and the wrong one for executing anything you do not trust. Point [sandbox] default and agent_default at a remote sandbox provider before running untrusted code or untrusted agents.

On this page