Skip to content

Configuration -- Environment Variable Reference ​


Generating Your Environment File ​

Use the provided script to generate a .env.operator file with unique secrets:

bash
./generate-operator-env.sh <registry-name> > .env.operator

Example:

bash
./generate-operator-env.sh acme-labs > .env.operator

This generates:

  • A unique OPERATOR_DID (e.g., did:theprotocol:operator-a1b2c3d4e5f6g7h8)
  • Cryptographically random database passwords (48-char hex)
  • Cryptographically random signing keys (64-char hex)
  • Sensible defaults for all feature flags

IMPORTANT: The .env.operator file contains secrets. Never commit it to version control.


Environment Variables by Category ​

Identity ​

VariableExampleDescription
REGISTRY_NAMEacme-labsUnique name for your registry. Used as container name prefix.
OPERATOR_DIDdid:theprotocol:operator-<hex>Auto-generated. Your cryptographic operator identity.
TRUST_DOMAIN<registry-name>.local (generated)The trust domain the registry signs its card with and mints its agents' DIDs under. The standalone compose passes it as both SPIRE_TRUST_DOMAIN and DID_ISSUER_NAMESPACE, and a registry whose issuer namespace is not its own trust domain refuses to boot. Set it to your DNS name; the generated .local value suits a local trial only.
FEDERATION_OPERATOR_ID(same as OPERATOR_DID)Identifies your events in the mainframe EventStore.

Images and Host Port ​

VariableDefaultDescription
IMAGE_REGISTRYtheprotocolStandalone compose: prefix of the application images. scripts/distribution/build.sh tags what it builds theprotocol/<name>, a local name, so the default runs what you built. The federated compose does not read it: it names its parent frame's image host directly.
VERSIONstableImage tag, read by both composes. build.sh tags every build with its version, latest and stable.
REGISTRY_HTTP_PORT8000Standalone compose: the host port the registry's plain HTTP is published on; the container still listens on 8000.

The env generator writes none of the three; add them to .env.operator only to change a default.

Database Passwords ​

VariableGeneratedDescription
PG_PASS_REGISTRY48-char hexPostgreSQL password for the registry database.
PG_PASS_TEG48-char hexPostgreSQL password for the TEG database.

These are generated randomly by generate-operator-env.sh. Never share or reuse them.

Security Keys ​

Service-to-service authentication on this platform is mTLS, not API keys. Your containers hold SPIFFE SVIDs (short-lived X.509 certificates, auto-rotated by the SPIRE agent), and the frame authenticates them by certificate -- no shared key crosses the wire for federation or EventStore writes. Only three of the generated secrets are actually load-bearing:

VariableLengthDescription
API_KEY_SECRET64-char hexThe one signing secret: developer JWTs, agent JWTs, API keys and 2FA tokens are all signed with it. Must match TEG_REGISTRY_JWT_SECRET -- the generator handles this automatically.
TEG_REGISTRY_JWT_SECRET(same as API_KEY_SECRET)TEG uses this to validate registry-issued JWTs.
TEG_ADMIN_API_KEY64-char hexAuthorizes the registry's proxied TEG admin calls (treasury, enforcement). The transport is mTLS; this key is the authorization on top.

The generator also writes several legacy variables that nothing reads anymore -- they date from before the mTLS identity fabric and are kept only so older env files stay valid. Safe to ignore (or delete): SECRET_KEY, JWT_SECRET_KEY, TEG_AVTP_SYSTEM_API_KEY, TEG_TOKEN_SIGNING_KEY. EVENT_STORE_INTERNAL_API_KEY is likewise inert in federated mode: with EVENTSTORE_MTLS_REQUIRED=true the EventStore identifies your registry by its SVID and the key fallback is disabled.

Federation ​

VariableDefaultDescription
EVENT_STORE_URLhttps://events.theprotocol.cloudMainframe EventStore URL. Set automatically in compose for federated mode.
EVENT_STORE_ENABLEDtrueEnable EventStore event submission. Set to false for standalone mode (legacy).
EVENTSTORE_MTLS_REQUIREDtrueRequire mTLS for EventStore writes. Must be true in federated mode. API key fallback is disabled.
FEDERATION_BASE_URLhttps://nginx-federation:8443Internal URL for federation mTLS sidecar. Do not change unless customizing nginx.
CORS_ALLOW_ALL_ORIGINSfalseSet to true to allow cross-origin requests for federation discovery. Required if other registries or frontends need to query your /api/v1/public/registry-config or federation endpoints directly.

Note: FEDERATION_LICENSE_KEY is a legacy variable that is no longer used for authentication. All federation auth uses mTLS via SPIRE. You can safely remove it from your .env.operator.

The registry reads neither KAFKA_ENABLED nor KAFKA_BOOTSTRAP_SERVERS: it writes its ledger events to the EventStore over HTTPS. An older .env.operator may still carry both; you can safely remove them.

Token Economy ​

VariableDefaultDescription
MINTING_AUTHORITYdisabledSet to disabled for federated mode (no local minting). Set to teg-layer for standalone mode (legacy).
DEFAULT_FEE_RATE0.5Written by the generator and read by no service. Transaction fees come from the frame policy document (transaction_fees.*), changed by a governance vote within the federation's bounds.
MAX_FEE_RATE5.0Written by the generator and read by no service (see DEFAULT_FEE_RATE).
GENESIS_GRANT_ENABLEDtrueWhen true, new agents receive a genesis AVT grant on creation.
GENESIS_GRANT_AMOUNT1000.0AVT amount granted to new agents. Set to 0 to disable.

Email (SMTP) ​

VariableDefaultDescription
MAIL_SERVERsmtp.example.comSMTP server hostname. A placeholder; pick a mail posture (below) before real users register.
MAIL_PORT587SMTP port (587 for STARTTLS, 465 for SSL).
MAIL_USERNAME(empty)SMTP authentication username.
MAIL_PASSWORDCHANGE_MESMTP authentication password.
MAIL_FROMnoreply@example.comSender email address for verification emails.
MAIL_FROM_NAME<registry-name>Display name in email "From" field.
MAIL_STARTTLSTrueUse STARTTLS encryption.
MAIL_SSL_TLSFalseUse direct SSL/TLS. Set to True and MAIL_STARTTLS=False for port 465.
PUBLIC_UI_URL(unset)The console a person typed, e.g. https://theprotocol.example.com. Unset means links are relative to the host serving the page. Loopback values are rejected.

Email carries developer account verification, password resets and notifications. Every message names your registry in its subject and footer.

Links a human clicks come from PUBLIC_UI_URL, not from PUBLIC_URL. The two are different addresses and confusing them produces a verification mail nobody can act on:

VariableAnswersUsed for
BASE_URLwhere this registry is, as an outside caller would type itAPI links, the served operator manual
PUBLIC_URLthe address other registries reach this one onfederation. On a cloud operator this is the external mTLS port, which a browser cannot open
PUBLIC_UI_URLthe console a person typedevery link in every mail: verification, password reset, the console link, invite links

Set PUBLIC_UI_URL when the console lives somewhere other than <BASE_URL>/ui — for example when an apex domain serves the app at / while the API answers on an api. host. Leave it unset and links stay relative, which keeps the reader on whatever host they typed. That is the safe default; what is never safe is deriving a human link from PUBLIC_URL.

Sessions are per host

A verification link that lands someone on a different hostname than the one they registered on logs them in there. They return to the site they typed and it has forgotten them, which reads as "the link didn't work". Point people at the console they used.

The image bakes a truthy wrong default

A cloud operator's BASE_URL defaults to http://localhost:8000. That is worse than empty, because it is truthy: a naive read produces a link that looks valid and points at the reader's own machine. The registry rejects loopback explicitly before falling back to the request headers. Never set BASE_URL on an operator without also setting ALLOWED_ORIGINS — that combination fails production config validation and crash-loops the container at import.

Pick exactly one of three mail postures. The one broken combination is a registry that demands verification and cannot send it: every self-registered account is then locked out, and the only remedy sits on an admin endpoint.

  1. direct -- real SMTP settings above, REQUIRE_EMAIL_VERIFICATION=true. The normal choice.
  2. relay -- no SMTP credential of your own; a parent frame sends credential mail (verification, password reset) on your behalf via MAIL_RELAY_PARENT_URL + MAIL_RELAY_LICENSE. Used on hosted deployments where the operator must not hold the platform's sending credential. Non-credential notification mail is intentionally skipped in this posture.
  3. none -- no send path at all: set REQUIRE_EMAIL_VERIFICATION=false andBETA_INVITE_REQUIRED=true. With verification off, invite codes are the only remaining gate; never run with neither.

The defaults ship a placeholder (smtp.example.com / CHANGE_ME): that is raw material for posture none, not a working configuration. Choose a posture deliberately before real users register.

Monitoring ​

VariableGeneratedDescription
GRAFANA_ADMIN_PASSWORD32-char hexGrafana admin password (if you add a Grafana container).

Feature Flags ​

VariableDefaultDescription
ENABLE_PROJECTION_SNAPSHOTStrueEnable TEG-to-EventStore projection snapshots (balance baseline).
GENESIS_GRANT_ENABLEDtrueGrant a starting balance to each developer's first agent, paid from your treasury.
GENESIS_GRANT_AMOUNT1000.0How much. Inert while GENESIS_GRANT_ENABLED is false — see below.
BETA_INVITE_REQUIREDfalseWhen true, developer registration requires an invite code.
COCKPIT_DESK_ENABLEDfalseShow the arrangeable operator desk. Off means the route and the sidebar entry do not exist.
SPAWN_WALLET_ANCHOR_MODEoffoff | on. Decides whether a spawned child agent gets a wallet at all: with on, a child holds none and spends its parent's purse.
STATUS_DATA_DIR/app/dataWhere the public status page keeps its incident list and its uptime rollup. Must be on a durable volume or the history resets on every recreate.
PROMETHEUS_URLhttp://prometheus:9090Where the status page reads per-day availability from. Unreachable means no history bars, which is the honest rendering.
FEDERATION_SYNC_INTERVAL_SECONDS(built-in)Seconds between federation sync cycles. Lower means fresher peer data and more chatter.

⚠ GENESIS_GRANT_AMOUNT and GENESIS_GRANT_ENABLED move together, and only the log line proves it. The amount is read inside the branch the enable flag guards, so editing the amount on a registry where the grant is disabled changes nothing and reports no error — you find out when a fresh agent holds zero next to a full treasury. The tell is the absence of a First agent for developer … minting line in the agent-creation log. When a paired flag exists, inspect both before concluding a policy changed.

Note also that the grant fires for a developer's first agent only; the second and later ones report not_first_agent, which is not a failure. | AUDITED_BALANCE_ENABLED | true | Enable balance auditing against EventStore projections. | | CORS_ALLOW_ALL_ORIGINS | false | Allow all CORS origins for federation discovery endpoints. | | EVENT_STORE_ENABLED | true | Enable EventStore event submission (federated mode). | | EVENTSTORE_MTLS_REQUIRED | true | Require mTLS for EventStore (federated mode). |

Agent Authorization, Liability and Policy ​

These govern what your agents are allowed to do and who is answerable when they do it. Every one of them is byte-inert at its default — an untouched registry behaves exactly as it did before these systems existed. Turn them on deliberately, and read the chapter before you do.

VariableDefaultDescription
AGENT_RBAC_MODEoffoff | shadow | enforce. Per-agent roles and permissions (IronKey L1/L2). shadow decides and records without blocking.
AGENT_DELEGATION_MODEoffoff | shadow | enforce. Delegation grants and capability tokens (L3/L4).
CAPABILITY_CIRCUIT_BREAKER_ENABLEDfalseAuto-revokes a capability token and its whole subtree after 5 misuse denials in 5 minutes. Only acts when AGENT_DELEGATION_MODE=enforce. Benign denials (expiry, budget, uses) never count.
LIABILITY_MODEoffoff | shadow | enforce. The Cockpit Card gate — deny-only and pre-escrow, so it can never move value.
LIABILITY_PRESENCE_THRESHOLD_AVT0Amount at or above which a spend escalates to a live human presence confirmation. 0 disables the step-up.
REPUTATION_BOND_MODEoffoff | shadow | enforce. The reputation bond as the gate to providing services. shadow ranks bonded providers first and counts what it would refuse; enforce refuses an unbonded provider at discovery, listing, pricing, Guild bids, contract accept and A2A authorize — never at settle. The public frames run shadow.
REPUTATION_BOND_AMOUNT50The bond your treasury holds in escrow per provider, in your frame's currency (env → network profile → the reputation_bond policy section's amount_avt). The public frames run 250; a fresh network derives 25 % of the genesis grant. Served by GET /api/v1/reputation-bond/config.
REPUTATION_BOND_TREASURY_BACKSTOPfalseLet an owner ask your treasury to underwrite a bond without moving money — a demonstration mechanism. Leave it off on a real network.
REPUTATION_BOND_MIN_TRANSACTIONS · _MIN_AGE_DAYS · _MIN_RATED_PEERS · _MIN_TRUST_PERCENTILE10 · 30 · 3 · 50Maturity conditions (all required), each overriding the frame policy document's enforcement.bond_maturity_* value: satisfactory interactions, days since lock, distinct rating peers owned by other developers, and a standing at or above that percentile of the frame's ranked agents (waived while fewer than 10 agents are ranked). REPUTATION_BOND_MATURITY_RELEASES_PERCENT (60, clamped to 0..80) is the share a matured bond pays back, and REPUTATION_BOND_RELEASE_COOLDOWN_DAYS (7) delays release after maturity. The absolute REPUTATION_BOND_MIN_EIGENTRUST is retired and read by nothing.
ENFORCEMENT_SLASH_MODEcachecache | shadow | enforce. Whether a slash moves money down the slash_waterfall policy (bond → stake → liquid, each leg capped). Only enforce moves value; shadow plans and records; cache touches the registry's cached balance only.
AGENT_XREG_DELEGATION_ENABLEDfalseHonour peer-signed cross-frame delegations. Must be true on both issuer and receiver for a cross-frame hand to resolve.
IRONHAND_INLINE_SVID_ENABLEDfalseIssue short-TTL X.509 SVIDs inline to agents running on infrastructure you do not control.
IRONHAND_INLINE_SVID_TTL_SECONDS1800TTL for those inline SVIDs.
TEG_TESTING_FAUCET_ENABLED (TEG)falseThe TEG's test faucet. Leave it off outside a throwaway frame — it takes no authentication and mints to whatever target it is given.

🔴 A test surface takes a positive flag, never a negative environment check. The faucet used to be gated on "is this production?", which is open on every deployment where the environment variable simply is not set — the common case, not the exception. If you write your own gates, gate on a flag that must be explicitly switched on, and default it off.

🔴 AGENT_RBAC_MODE and AGENT_DELEGATION_MODE must always move together. They are documented as independent and in the allow direction they are — but with enforce + off, capability resolution is never consulted, so any agent whose authority rides a capability token keeps only its (correctly empty) role set and is denied everything. Never flip one alone.

⚠ Before enforcing RBAC on an existing registry, confirm your agents actually carry roles. An agent with no role grant resolves to {read.profile, enforcement.self} and can do nothing economic. Run in shadow first and read the denial feed (GET /api/v1/agents/{did}/authz/denials) — that is exactly what shadow mode is for.

Cross-currency (FX) — mainframes only ​

An operator registry carries no FX pool: FX_ENABLED, FX_ROUTE_PEERS and TEG_SIDECAR_MAP are deliberately unset on every operator, and a cross-currency send from an agent homed on your registry answers 409 naming the FX booth on the mainframe. Same-currency transfers to and from your parent frame and any peer work as documented in chapter 04; an agent that needs a cross-currency move is homed on a frame. This is intended, not a gap.

Compliance and Cosmetics ​

VariableDefaultDescription
JURISDICTION_MODEoffoff | shadow | enforce. Master switch for jurisdiction profiles and the regulatory-authorization rail. off is byte-inert. Individual declared actions carry their own mode and an entry-level enforce applies even while the global mode is shadow.
REGAUTH_COUNTERSIGN_WATCH_ENABLEDtrueBackground watcher that alerts you when a countersigned attestation contract stops being valid.
REGAUTH_COUNTERSIGN_WATCH_INTERVAL120Seconds between watcher cycles.
FLARE_LOCAL_PURCHASE_ENABLEDfalseSell cosmetic flare tiers for your registry's own currency. When off, the catalog still renders and purchase returns 503.
FLARE_SHOP_PRICES(unset)JSON per-tier price override. Malformed entries are logged and ignored, falling back to the defaults.
OPA_ENABLED / OPA_ENFORCEfalse / falseOpen Policy Agent. OPA_ENFORCE is honoured only when OPA_ENABLED is true; OPA can tighten an allow, never grant a deny.

⚠ A flag claim in a document is a hypothesis; the container is the answer. Re-derive with:

bash
docker inspect <registry-container> --format '{{range .Config.Env}}{{println .}}{{end}}' \
  | grep -E "MODE=|OPA_|FLARE|BREAKER"

PgBouncer Configuration ​

The pgbouncer.ini file configures connection pooling for the registry database:

ini
[databases]
agentvault_registry = host=db port=5432 dbname=agentvault_registry user=registry password=<PG_PASS_REGISTRY>

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = any
pool_mode = transaction
max_client_conn = 200
default_pool_size = 20
ignore_startup_parameters = extra_float_digits,options
SettingValueDescription
pool_modetransactionConnections returned to pool after each transaction. Required for multi-worker FastAPI.
max_client_conn200Maximum simultaneous client connections (4 workers x ~50 each).
default_pool_size20Active connections to PostgreSQL per database.

IMPORTANT: Update the password value in pgbouncer.ini to match the PG_PASS_REGISTRY value generated in your .env.operator file.


Redis Configuration ​

Redis runs as a single instance with LRU eviction:

redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru
SettingValueDescription
maxmemory128mbMaximum memory before eviction. Sufficient for leader election, rate limiting, and session state.
maxmemory-policyallkeys-lruEvict least-recently-used keys when memory is full.
Database0Single database (unlike the mainframe which uses DB 0-3 for multiple registries).

Redis is used for:

  • Leader election: Ensures only one worker runs each background task
  • Rate limiting: Shared counters across 4 uvicorn workers
  • WebSocket pub/sub: Cross-worker message broadcasting
  • Session state: Temporary cross-worker data sharing

Registry Service Configuration ​

These environment variables are set automatically in docker-compose.operator.yml and generally do not need manual changes:

VariableValueDescription
DATABASE_URLpostgresql+asyncpg://registry:<pass>@pgbouncer:6432/agentvault_registryAsync DB URL via PgBouncer.
DATABASE_URL_SYNCpostgresql://registry:<pass>@pgbouncer:6432/agentvault_registrySync DB URL for migrations.
TEG_LAYER_URLhttp://teg-layer:8080Internal TEG endpoint.
REDIS_URLredis://redis:6379/0Redis connection.
SPIFFE_ENDPOINT_SOCKETunix:///opt/spire/sockets/agent.sockSPIRE agent socket.
UVICORN_WORKERS4Number of FastAPI worker processes.

TEG Layer Configuration ​

These are set automatically in docker-compose.operator.yml:

VariableValueDescription
DATABASE_URLpostgresql://teg:<pass>@teg-db:5432/teg_layerTEG database (synchronous psycopg).
REGISTRY_URLhttp://registry:8000Registry URL for TEG to fetch emission policies.
ADMIN_API_KEY<TEG_ADMIN_API_KEY>Admin API key (from .env.operator).
MINTING_AUTHORITYdisabledFederated mode: no local minting.
UVICORN_WORKERS4Number of TEG worker processes.

Authentication Configuration ​

VariableDefaultDescription
ACCESS_TOKEN_EXPIRE_MINUTES90JWT access token lifetime in minutes (cluster default; env-overridable).
BOOTSTRAP_TOKEN_LIFETIME300Bootstrap token lifetime in seconds (5 minutes).
EMAIL_VERIFICATION_TOKEN_EXPIRE_HOURS24Email verification link expiry.
REQUIRE_EMAIL_VERIFICATIONtrueWhen false, new developers are verified immediately.

Rate Limiting ​

VariableDefaultDescription
RATE_LIMIT_PUBLIC60/minuteUnauthenticated requests.
RATE_LIMIT_DEVELOPER300/minuteDeveloper JWT-authenticated requests.
RATE_LIMIT_AGENT300/minuteAgent JWT-authenticated requests.
RATE_LIMIT_ADMIN(none)No rate limit for admin accounts.
RATE_LIMIT_BYPASS_KEYS(empty)Comma-separated API keys that bypass rate limiting.

Secret Rotation Guidance ​

When to Rotate ​

TriggerAction
Suspected compromiseRotate ALL live keys immediately (the pair + TEG_ADMIN_API_KEY); every issued JWT and API key is invalidated by rotating API_KEY_SECRET
Personnel changeRotate TEG_ADMIN_API_KEY and the API_KEY_SECRET / TEG_REGISTRY_JWT_SECRET pair
Routine (quarterly)Rotate the API_KEY_SECRET / TEG_REGISTRY_JWT_SECRET pair

Service certificates never need manual rotation -- SPIRE rotates SVIDs automatically.

How to Rotate ​

  1. Generate a new value: openssl rand -hex 32
  2. Update the value in .env.operator
  3. Restart affected containers:
    bash
    docker compose -f docker-compose.operator.yml --env-file .env.operator up -d registry teg-layer
  4. For PG_PASS_REGISTRY or PG_PASS_TEG: you must also update the database user password inside the container before changing the env var, or re-create the database volumes.

Keys That Must Match ​

Key AKey BReason
API_KEY_SECRETTEG_REGISTRY_JWT_SECRETTEG validates JWTs signed by the registry. The generator sets both to the same value.

If these diverge, agent authentication will fail on TEG proxy calls.


Complete .env.operator Template ​

bash
# .env.operator -- Generated by generate-operator-env.sh
# DO NOT COMMIT -- contains secrets

# -- Identity
REGISTRY_NAME="<your-registry-name>"
OPERATOR_DID=did:theprotocol:operator-<auto-generated>
TRUST_DOMAIN=<your-registry-name>.local

# -- Database passwords
PG_PASS_REGISTRY=<auto-generated-48-char-hex>
PG_PASS_TEG=<auto-generated-48-char-hex>

# -- Security keys
API_KEY_SECRET=<auto-generated-64-char-hex>
TEG_REGISTRY_JWT_SECRET=<must-match-API_KEY_SECRET>
TEG_ADMIN_API_KEY=<auto-generated-64-char-hex>
# -- legacy, read by nothing (service auth is mTLS); safe to omit
SECRET_KEY=<auto-generated-64-char-hex>
JWT_SECRET_KEY=<auto-generated-64-char-hex>
TEG_AVTP_SYSTEM_API_KEY=<auto-generated-64-char-hex>
TEG_TOKEN_SIGNING_KEY=<auto-generated-64-char-hex>
EVENT_STORE_INTERNAL_API_KEY=<auto-generated-64-char-hex>

# -- Token economy
MINTING_AUTHORITY=disabled
# the two fee lines are read by no service: fees come from the frame policy document
DEFAULT_FEE_RATE=0.5
MAX_FEE_RATE=5.0

# -- Federation
EVENT_STORE_ENABLED=true
EVENTSTORE_MTLS_REQUIRED=true
CORS_ALLOW_ALL_ORIGINS=false

# -- Email (CONFIGURE BEFORE DEPLOY)
MAIL_SERVER=smtp.example.com
MAIL_PORT=587
MAIL_USERNAME=<your-smtp-username>
MAIL_PASSWORD=<your-smtp-password>
MAIL_FROM=noreply@example.com
MAIL_FROM_NAME="<your-registry-name>"
MAIL_STARTTLS=True
MAIL_SSL_TLS=False

# -- Monitoring
GRAFANA_ADMIN_PASSWORD=<auto-generated-32-char-hex>

# -- Features
ENABLE_PROJECTION_SNAPSHOTS=true
GENESIS_GRANT_ENABLED=true
GENESIS_GRANT_AMOUNT=1000.0
BETA_INVITE_REQUIRED=false
AUDITED_BALANCE_ENABLED=true

See also: Quickstart (Section 00) | Architecture (Section 01) | Federation (Section 03)

Server components AGPL-3.0-or-later · SDKs and the auditor Apache-2.0 · this documentation CC BY 4.0. If a doc and the running stack disagree, trust the stack. Legal notice (Impressum) · Privacy · Terms