Secrets and environment variables
Most agents need two kinds of settings: plain configuration, like a log level or a base URL, and credentials, like an API key. The platform stores them separately on purpose. Env vars you can read back at any time. Secrets you can never read back — only their names. This page covers the per-agent version of both, which is the quick way to configure one agent.
Secrets vs env vars
| Secrets | Env vars | |
|---|---|---|
| Read back? | No — key names only, values never returned | Yes — names and values |
| For | API keys, tokens, passwords | Non-sensitive config |
| Reaches the agent as | Environment variables | Environment variables |
| Change behavior | Rolls a new revision | Rolls a new revision |
Both arrive in your agent's process as ordinary environment variables, which you read in Python with os.environ["MY_KEY"]. The only difference is what the platform will ever show back to you.
Every route on this page is a management route. Sign in first, and note that only the agent's owner or a project admin can call them. Add ?project=<slug> when the same agent name exists in more than one project.
Everything below stores a value on exactly one agent, with no version history. If you need a secret that several agents share, that keeps past versions, and whose reads are recorded, use the project secrets manager and attach the secret to your agents — see use secrets in workloads.
Key naming rule
Every secret and env key must be a valid environment variable name. The rule is ^[A-Za-z_][A-Za-z0-9_]*$: letters, digits, and underscores only, and it may not start with a digit. So DEMO_TOKEN is fine and my-key is not. Anything else is rejected with 400, and the two routes word the rejection differently.
The secrets routes (PUT and PATCH /v1/agents/{name}/secrets) answer:
invalid secret key (must be a valid environment variable name): my-key
The env route (PATCH /v1/agents/{name}/env) answers:
invalid env var name: my-key
Manage secrets
The three verbs behind every tab below:
| Method | Path | Behavior |
|---|---|---|
| GET | /v1/agents/{name}/secrets | List key names (never values) |
| PUT | /v1/agents/{name}/secrets | Replace the whole set with a flat map |
| PATCH | /v1/agents/{name}/secrets | Merge: set some keys, remove others |
Set a secret
- platformctl
- curl
- Console
platformctl secrets set research-buddy DEMO_TOKEN=abc123
You should see:
set 1 secret(s) for research-buddy
This merges — it never touches keys you didn't name. The CLI has no secrets unset; use the curl tab's PATCH with remove for that.
Merge with PATCH — this is what the CLI does:
curl -s -X PATCH "$CAI_API/v1/agents/research-buddy/secrets" \
-H "Authorization: Bearer $CAI_TOKEN" -H 'content-type: application/json' \
-d '{"set": {"DEMO_TOKEN": "abc123"}, "remove": ["OLD_KEY"]}'
You should see:
{"agent": "research-buddy", "secrets_updated": true, "keys": ["DEMO_TOKEN", "MODEL_API_KEY"]}
A PATCH with neither set nor remove fails with 400 nothing to do: provide 'set' and/or 'remove'. Setting a key to an empty string in set removes it.
Replace everything with PUT:
curl -s -X PUT "$CAI_API/v1/agents/research-buddy/secrets" \
-H "Authorization: Bearer $CAI_TOKEN" -H 'content-type: application/json' \
-d '{"DEMO_TOKEN": "abc123", "OTHER_KEY": "xyz"}'
PUT drops every key not in your request body — including MODEL_API_KEY if one was set. Prefer PATCH unless you really mean "exactly these and nothing else". Empty values in a PUT body are dropped — blank means inherit.
Secret request bodies are capped at 1 MiB.
On the agent's Access tab, open the Secrets and environment section, add the key and value, and save. The console refuses the platform's reserved variable names, with MODEL_API_KEY as the one deliberate exception.
List the key names
Values are never returned — not by any of these, and not by the API behind them. This answers "is MODEL_API_KEY set?" without anything being able to read what it is set to.
- platformctl
- curl
- Console
platformctl agents secrets get research-buddy
Note the different command group: reads live under platformctl agents secrets, while the write is platformctl secrets set on the service that holds the value.
curl -s "$CAI_API/v1/agents/research-buddy/secrets" \
-H "Authorization: Bearer $CAI_TOKEN"
You should see:
{"agent": "research-buddy", "keys": ["DEMO_TOKEN", "MODEL_API_KEY"]}
The Secrets and environment section on the agent's Access tab lists the configured key names, with the values masked.
Manage env vars
| Method | Path | Behavior |
|---|---|---|
| GET | /v1/agents/{name}/env | Names and values |
| PATCH | /v1/agents/{name}/env | Merge with {"set": {...}, "remove": [...]} |
- platformctl
- curl
- Console
platformctl agents env set research-buddy LOG_LEVEL=debug
platformctl agents env get research-buddy
Remove one with --remove:
platformctl agents env set research-buddy --remove LOG_LEVEL
curl -s -X PATCH "$CAI_API/v1/agents/research-buddy/env" \
-H "Authorization: Bearer $CAI_TOKEN" -H 'content-type: application/json' \
-d '{"set": {"LOG_LEVEL": "debug"}}'
You should see:
{"agent": "research-buddy", "env_updated": true, "env": {"LOG_LEVEL": "debug"}}
Plain environment variables live in their own Environment variables section, at the top of the agent's Access tab above Secrets and environment. Edit them as one KEY=value per line; deleting a line removes that variable.
Every change rolls a new revision
A revision is an immutable snapshot, so a running revision never changes. A secret or env change therefore edits nothing in place. It creates a brand-new revision with the new values baked in, and traffic moves there once that revision is ready. Two things follow:
- Changes are not instant. The new revision has to start before any request sees the new values.
- A change behaves like a deploy. It shows up in the revisions list, and you can roll traffic back to the previous one.
Your model key
The platform does not supply a model credential. Agents call inference with your own key from Crusoe Intelligence Foundry — your billing, your quota, your rate limits. There is no platform key to inherit and none to fall back to.
Set it once for the project, and every agent in that project uses it:
curl -X PUT "$CAI_API/v1/projects/$PROJECT/inference" \
-H "Authorization: Bearer $CAI_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"api_key":"<your Foundry key>"}'
In the console: Project Settings, which holds both of your project's own credentials — this Foundry key for inference, and your Crusoe Cloud key for the container registry and object storage. Until a key is mapped, deploying an agent that calls a model is refused with a message saying exactly this — it fails at deploy time rather than at the first invoke, and nothing you uploaded is touched. Functions and containers are unaffected; they never call the model.
To give one agent a different key — a separate bill, a separate quota — set
MODEL_API_KEY as a secret on that agent:
platformctl secrets set research-buddy MODEL_API_KEY=sk-your-own-key
That overrides the project's key for that one agent. Clearing it (or setting it
blank) goes back to inheriting the project key. To check which key is in force without revealing it, call the agent's GET /debug/config endpoint. It reports model_key_present: true/false and the resolved model name, never the key itself.
What the platform injects
Your agent starts with a set of variables the platform writes itself, as explicit container environment variables — which beat anything you store in the per-agent Secret. These are the ones your own code is most likely to read:
| Variable | Value in a deployed agent |
|---|---|
CAI_API_URL | The platform API at its internal address, not the public one |
CAI_PUBSUB_URL | The Pub/Sub REST API, at its internal address |
CAI_VECTORDB_URL | The Vectors REST API, at its internal address |
EMBED_BASE_URL, EMBED_MODEL | The embedding endpoint (already ending in /v1), and the project's embedding model |
CAI_PROJECT_ID, PROJECT_SHORT | Which project this agent belongs to |
MCP_SERVERS | The project's ready MCP servers, each with its name, internal address and own bearer token |
CRUSOE_MEMORYSTORE_ADDR | The agent's Memory Store address, where sessions are kept |
CRUSOE_VECTORDB_URL, CRUSOE_VECTORDB_API_KEY | The agent's VectorDB, where the memory bank lives |
SANDBOX_URL | The code sandbox that run_python calls |
AGENT_NAME, AGENT_IMAGE | The agent's own identity and image |
MODEL_BASE_URL, CHAT_MODEL, EMBED_MODEL, EMBED_BASE_URL | Model routing |
Those addresses are internal on purpose: they resolve only from inside the platform, and they are the addresses the project's network policy admits. Read each one from its variable rather than hard-coding it — the value is ours to change, the variable name is not. None of them reach a sandboxed tool — see tools.
Older names — VALKEY_ADDR, QDRANT_URL, QDRANT_API_KEY and their username/password companions — are still set alongside the CRUSOE_-prefixed ones so that agents deployed before the rename keep working. Runtime code reads the CRUSOE_ name first and falls back to the legacy one. Write new code against the CRUSOE_ names; treat the legacy aliases as deprecated.
MODEL_BASE_URL, CHAT_MODEL, EMBED_MODEL and EMBED_BASE_URL are deliberately overridable as plain env vars, so you can pin a model other than the project default or point an agent at your own inference endpoint:
platformctl agents env set research-buddy CHAT_MODEL=nvidia/NVIDIA-Nemotron-3-Super-120B-A12B
They are still refused as secret bindings — a hidden value that silently repoints inference is a different thing from a visible one you typed — which is why they also appear on the reserved list elsewhere.
PATCH /v1/agents/{name}/env answers 400 and names each key and the rule it broke — for remove as well as set, because removing one cannot unset the platform's value either. MODEL_API_KEY is the one supported override, and it goes in a Secret, not in env.
The full list is kept in step with the API by a test, so read it there rather than here: overriding the platform's own variables. In shape it is two groups — credentials the platform is accountable for, and the wiring and identity that say where your agent's services are and which tenant it is.
Two things that reference page does not carry:
- A second set of names cannot be bound from a Secret at all:
TOOL_SANDBOX,PATH,HOME,LD_PRELOAD,PYTHONPATH,BAO_ADDR,BAO_TOKEN,NATS_URL,NATS_PASSWORD,KUBERNETES_SERVICE_HOSTandKUBERNETES_SERVICE_PORT. A bind attempt is refused with<NAME> is reserved by the platform and cannot be bound. - The per-agent secrets routes (
PUTandPATCH /v1/agents/{name}/secrets) still validate only the name format. A platform-owned name stored there is accepted, listed byGET /secrets, and then silently shadowed at deploy by the platform's own container env. Keep your own names in secrets — with the one deliberate exception ofTOOL_SANDBOX=false, which the env route refuses and this route is the supported way to set.
Summary
| Question | Answer |
|---|---|
| Can I read a secret value back? | No. Names only, by design |
| Can I read env values back? | Yes |
| Key format | ^[A-Za-z_][A-Za-z0-9_]*$ |
| PUT vs PATCH | PUT replaces everything; PATCH merges |
| When do changes take effect? | When the new revision starts serving |
| Bring my own model key | Secret MODEL_API_KEY on the agent |
| Turn off tool sandboxing | platformctl secrets set <agent> TOOL_SANDBOX=false — the env route refuses that name |
| Shared/versioned/audited secrets | Secrets manager |
Next steps
- Use secrets in workloads — bindings, rotation, and reading a secret at call time instead of from the environment.
- Traffic and revisions — the revision every change here creates, and how to roll one back.
- Crusoe Cloud integration — where your own
MODEL_API_KEYand your model list come from. - Secrets API reference — every route, field, and status code.