Skip to main content

Manage secrets

This page walks through the full life of a project secret: create it, list it, inspect its versions, rotate it, reveal it (the one audited exception to write-only), and delete it. Every call is shown with its real response.

Before you begin

  • You need an account (ask your administrator for an account or an invitation link) and a bearer token. See API authentication for the ways to get one.
  • There is no public API hostname yet: use the endpoint your administrator gives you, or the $CAI_API pattern below. The web console at https://console.codyhill.dev offers all of these operations under Security → Secrets if you prefer clicking.
  • Managing project secrets requires project membership; revealing and deleting require the project admin role.

Set up your shell once:

export CAI_API="http://localhost:8081" # your agent-engine-api endpoint (from your admin, or a port-forward)
export TOKEN="$CAI_TOKEN" # a bearer token: session token, API key, or automation token
export PROJECT="your-project-id" # the project's ID
No CLI for project secrets yet

platformctl currently has no commands for project secrets — this surface is console and API only. The one secrets-related CLI command, platformctl secrets set, targets the older per-agent path described in Use secrets in workloads.

1. Create a secret

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-abc123"}'

You should see (status 201):

{"name":"openai-api-key","version":1,"created":true,"note":"the value is stored. Nothing - this API included - will show it back to you except an explicit, audited reveal."}

Rules the API enforces:

  • Name: 1–63 characters of letters, digits, - or _, starting with a letter or digit. Otherwise: 400 with secret name must be 1-63 characters of letters, digits, '-' or '_', starting with a letter or digit (got "...").
  • Value: not empty, at most 65,536 bytes (64 KiB). Otherwise: 400 with secret value must not be empty or secret value is N bytes, limit is 65536.
  • The whole request body is capped at 128 KiB.
  • The name crusoe-cloud-credential is reserved by the platform. Every call that names it — create, get, delete, reveal — returns 404, and it never appears in listings.

In the console: Security → Secrets → Add a secret. The dialog validates the same rules and can generate a random 32-byte value for you (shown once).

2. List secrets

curl -s "$CAI_API/v1/projects/$PROJECT/secrets" \
-H "Authorization: Bearer $TOKEN"

You should see:

{"secrets":[{"name":"openai-api-key","current_version":1,"created_at":"2026-08-12T09:14:03Z","updated_at":"2026-08-12T09:14:03Z"}],"path_prefix":"projects/acme"}

Names and version numbers only — no values, by construction. The list is paginated with page_size (default 50, max 200) and page_token; when there are more results, the response includes next_page_token.

3. Inspect one secret

curl -s "$CAI_API/v1/projects/$PROJECT/secrets/openai-api-key" \
-H "Authorization: Bearer $TOKEN"

You should see:

{"name":"openai-api-key","current_version":1,"created_at":"2026-08-12T09:14:03Z","updated_at":"2026-08-12T09:14:03Z","versions":[{"version":1,"created_at":"2026-08-12T09:14:03Z","destroyed":false}],"used_by":[{"id":"b1c2...","agent_id":"a9f8...","agent_slug":"research-buddy","env_name":"OPENAI_API_KEY","created_at":"...","updated_at":"..."}]}
  • versions is the full history; destroyed: true marks versions that can no longer be read or bound.
  • used_by lists every binding that references this secret (an absent version field means the binding tracks the latest version; a pinned binding carries "version": N). When nothing binds it, the field is absent entirely — not an empty list.
  • There is a versions-only endpoint too: GET /v1/projects/$PROJECT/secrets/openai-api-key/versions.

There is no value field on any of these responses. That is the design, not an omission.

4. Rotate a secret

There is no separate rotate endpoint: writing again is rotating. The same POST creates version 2:

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"}'

You should see (status 200 this time — it already existed):

{"name":"openai-api-key","version":2,"created":false,"note":"the value is stored. Nothing - this API included - will show it back to you except an explicit, audited reveal."}
Rotation does not reach agents by itself

Agents keep the value they were last given. A new version reaches latest-tracking bindings only on the next apply (or the next deploy). After rotating, re-apply: see Use secrets in workloads.

5. Reveal a value (admin only, always audited)

:reveal is the single call on the platform that returns a stored value.

curl -s -X POST "$CAI_API/v1/projects/$PROJECT/secrets/openai-api-key:reveal" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"version":0,"reason":"rotating into payments"}'

You should see:

{"name":"openai-api-key","version":2,"value":"sk-live-NEW","note":"this read is in the audit log with your identity, the version and the time."}
  • version of 0 (or absent) means the current version. reason is optional and lands in the audit log.
  • Only project admins may reveal. A member gets 403 with this action requires the project admin role.
  • The audit is fail-closed: if the audit entry cannot be written, the call returns 503 and no value. There is no unaudited read path.
  • In the console, Reveal shows the value once in a copy-once dialog; nothing keeps a copy.

6. Delete a secret (admin only)

Deleting destroys the secret and every version. It is not recoverable.

Delete refuses to run while bindings still reference the secret, because the failure would otherwise show up later as an agent that will not start:

409 {"error":"openai-api-key is still bound by 1 binding(s): research-buddy.OPENAI_API_KEY. Remove them first - nothing in the database prevents this delete, so the failure would otherwise appear later, as an agent that will not start."}

Remove the bindings, then:

curl -s -X DELETE "$CAI_API/v1/projects/$PROJECT/secrets/openai-api-key" \
-H "Authorization: Bearer $TOKEN"

You should see:

{"name":"openai-api-key","deleted":true,"note":"every version is destroyed. This is not recoverable."}

Deleting a name that does not exist returns a genuine 404.

Reading the store's error states

The store's failure modes are deliberately distinct — a broken store never looks like "no secrets":

StatusMeaningWhat to do
404The secret genuinely does not exist (or is the reserved name).Check the name.
409It already exists (on create paths) or is still bound (on delete).See the message; it names the bindings.
503 — "the project secret store is not configured: agent-engine-api starts it only when BAO_ADDR and BAO_TOKEN are set..."The platform was deployed without a secret store.Ask your administrator.
503 — from the store itselfThe store is sealed or unreachable.Ask your administrator to unseal it, then retry.

What gets audited

Recorded in the project audit log: every reveal (with your identity, the version, the reason, and the time) and every value read performed by an apply (delivering bindings reveals each bound value — one audit entry per read). See Quotas and audit.

Next steps