Skip to main content

Use secrets in workloads

Storing a secret does nothing by itself — this page shows how values actually reach your agents: bindings and apply (the recommended path), call-time reads with short-lived tokens, and the older direct per-agent path.

Before you begin

  • A stored project secret — see Manage secrets.
  • A deployed agent and a bearer token with project membership.
export CAI_API="http://localhost:8081" # your agent-engine-api endpoint
export TOKEN="$CAI_TOKEN" # a bearer token
export PROJECT="your-project-id" # the project's ID

Bindings: the mental model

A binding is a rule: "stored secret openai-api-key arrives as environment variable OPENAI_API_KEY on agent research-buddy." Two things make bindings safe and predictable:

  1. Bind is not deliver. Recording a binding changes nothing running. Values reach the agent only when you apply — an explicit, all-or-nothing step.
  2. Version pin or latest. A binding either names an exact version (reproducible forever) or tracks the latest one (rotation reaches it on the next apply).

In the console, bindings live on the agent's own page, with a badge per binding showing whether it is actually delivered on the running agent or still pending, and an "Apply N pending bindings" button.

1. Bind a secret to an environment variable

curl -s -X PUT "$CAI_API/v1/projects/$PROJECT/agents/research-buddy/secret-maps/OPENAI_API_KEY" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"secret_name":"openai-api-key"}'

You should see:

{"binding":{"env_name":"OPENAI_API_KEY","secret_name":"openai-api-key","pinned_to":"latest",...},"note":"recorded. The value reaches the agent when the bindings are applied - POST /v1/projects/{projectID}/agents/{agent}/secret-maps:apply."}

To pin an exact version instead, send {"secret_name":"openai-api-key","version":2}.

Naming rules and refusals

  • The environment variable name must match ^[A-Za-z_][A-Za-z0-9_]*$ — letters, digits, and underscores, not starting with a digit.
  • The secret must already exist. Otherwise: 400 with no secret called "<x>" in this project. Create it first - a binding to a secret that does not exist would fail at deploy time instead of now.
  • A pinned version must exist and not be destroyed: 400 with <name> has no version N (current is M) or version N of <name> has been destroyed and cannot be bound.
  • Reserved names cannot be bound. These are the variables the platform sets for the agent itself — the inference endpoint and credential, the sandbox address, the messaging credential, the process loader. Binding one returns 400 naming the full list:
MODEL_API_KEY, MODEL_BASE_URL, EMBED_BASE_URL, SANDBOX_URL, VALKEY_ADDR, QDRANT_URL,
AGENT_NAME, AGENT_IMAGE, TOOL_SANDBOX, BAO_ADDR, BAO_TOKEN, NATS_URL, NATS_PASSWORD,
KUBERNETES_SERVICE_HOST, KUBERNETES_SERVICE_PORT, PATH, HOME, LD_PRELOAD, PYTHONPATH

To see the bindings on an agent: GET /v1/projects/$PROJECT/agents/research-buddy/secret-maps. For the whole project's binding table: GET /v1/projects/$PROJECT/secret-maps.

2. Apply: deliver the values

Apply reads every bound value (each read is audited), writes them all-or-nothing into the agent's environment, and rolls a new revision:

curl -s -X POST "$CAI_API/v1/projects/$PROJECT/agents/research-buddy/secret-maps:apply" \
-H "Authorization: Bearer $TOKEN"

You should see:

{"agent":"research-buddy","applied":[{"env_name":"OPENAI_API_KEY","secret_name":"openai-api-key","version":"latest","applied":true}],"revision_rolled":true,"note":"applied. The agent picks these up on its next cold start."}

If any value cannot be read, nothing is written. You get status 424 (Failed Dependency), every row marked applied: false with a per-row error, and:

"error": "nothing was written. At least one binding could not be read, and applying the rest would leave the agent half-configured."

Why a revision rolls

Agent revisions are immutable snapshots — a running revision never changes its environment. So every delivery rolls a new revision, and the agent picks up the values on its next cold start. The response reports revision_rolled honestly; false means the roll could not happen (for example, the agent was never deployed). See Traffic and revisions.

3. Rotate end to end

# write a new version (same endpoint as create)
curl -s -X POST "$CAI_API/v1/projects/$PROJECT/secrets" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"openai-api-key","value":"sk-live-NEW"}'

# re-apply so latest-tracking bindings pick it up
curl -s -X POST "$CAI_API/v1/projects/$PROJECT/agents/research-buddy/secret-maps:apply" \
-H "Authorization: Bearer $TOKEN"

Version-pinned bindings ignore rotation until you repoint them (same PUT as binding, with the new version).

4. Unbind: immediate revocation

Unbinding is deliberately asymmetric with binding. It does not wait for an apply — it deletes the binding, removes the variable from the agent, and rolls a new revision immediately:

curl -s -X DELETE "$CAI_API/v1/projects/$PROJECT/agents/research-buddy/secret-maps/OPENAI_API_KEY" \
-H "Authorization: Bearer $TOKEN"

You should see:

{"agent":"research-buddy","env_name":"OPENAI_API_KEY","deleted":true,"revoked":true,"revision_rolled":true,"note":"..."}
Check the revoked field

If the response says revoked: false, the value could not be removed from the running agent — the note ends with ...treat this credential as exposed and rotate it. Unbind intentionally works even while the secret store is sealed, so revocation is never blocked by store trouble.

Read secrets at call time (no env var at all)

A deployed workload carries three injected variables: CAI_API_URL, CAI_PROJECT_ID, and CAI_PROJECT_KEY — the last is a workload key (it starts with cai_wl_). A workload key can do exactly one management-plane thing: mint a short-lived, read-only token for its own project's secrets. Every other management call refuses it with 403 and this credential is a deployed workload's key; it can only mint a read token for its own project's secrets, not use management endpoints.

In Python, the harness wraps the whole dance in one call:

from crusoe_adk import secret

key = secret("openai-api-key") # latest version
pinned = secret("openai-api-key", 2) # version 2, pinned

Under the hood, secret() mints a token, reads the value straight from the store, and caches one token per invocation. It fails closed with a SecretError if the injected variables are missing — it never silently returns nothing.

The token-minting endpoints

Any project member — or a workload key, for its own project — can mint:

# scoped to read exactly one secret
curl -s -X POST "$CAI_API/v1/projects/$PROJECT/secrets/openai-api-key:issue-token" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"ttl_seconds":300}'

# or scoped to read every secret in the project
curl -s -X POST "$CAI_API/v1/projects/$PROJECT/secrets:issue-token" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"ttl_seconds":300}'

You should see:

{"token":"hvb....","ttl_seconds":300,"read":{"addr":"http://openbao...","path":"secret/data/projects/acme/openai-api-key"},"note":"this token is short-lived and scoped to read only. It reads secret VALUES directly from the store until it expires; it is never stored here."}
  • ttl_seconds defaults to 300 and is clamped to 3600 (silently — the returned ttl_seconds is the effective one; the store's own limits may clamp it further).
  • The token is presented directly to the store as the X-Vault-Token header on GET of the returned read.addr + read.path (add ?version=N to pin). The token's scope and expiry are enforced by the store itself.
  • A store backend that cannot mint scoped tokens returns 501 with the configured secret store cannot issue scoped tokens.

The older path: direct per-agent values

You can write values straight onto one agent, skipping the project store. No versions, no history, no sharing — but quick. These routes require the agent's owner (or a project admin), and take ?project=$PROJECT as a query parameter.

platformctl secrets set research-buddy DEMO_TOKEN=abc123

You should see:

set 1 secret(s) for research-buddy

The CLI calls PATCH /v1/agents/research-buddy/secrets — a merge, so it never wipes values set elsewhere. The raw API offers three verbs:

CallBehavior
GET /v1/agents/{name}/secretsReturns key names only: {"agent":"research-buddy","keys":["DEMO_TOKEN"]}. Never values.
PUT /v1/agents/{name}/secretsReplaces the whole set with the flat map you send. Empty values are dropped. Body cap 1 MiB.
PATCH /v1/agents/{name}/secretsMerges: {"set": {"KEY":"value"}, "remove": ["KEY"]}. An empty value in set acts as a removal. Sending neither returns 400 with nothing to do: provide 'set' and/or 'remove'.

Keys must be valid environment variable names (^[A-Za-z_][A-Za-z0-9_]*$); otherwise 400 with invalid secret key (must be a valid environment variable name): <k>. Every write rolls a new revision, like apply does.

Fewer guardrails on this path

The reserved-name check is enforced by the console but not by the server on this path. One override is officially supported here: setting MODEL_API_KEY repoints the agent's model billing to your own key. Avoid setting other platform names (PYTHONPATH, LD_PRELOAD, TOOL_SANDBOX, ...) — they would take effect and can break or weaken your agent.

Summary

ActionEffect on the running agentAudited value reads
Bind / repointNone until applyNo
ApplyNew revision with all bound values (all-or-nothing)Yes, one per value
Rotate (write new version)None until re-applyNo
UnbindImmediate: value removed, revision rolledNo
platformctl secrets set (PATCH)Immediate: new revisionNo
secret() at call timeNone (value never enters the environment)Read enforced + expired by the store

Next: Manage secrets · Agent secrets and environment · Secrets API reference