Skip to content
BeeuniBeeuni
English
Esc
↑↓navigate↵open⌘Jpreview
On this page

Secrets

How Beeuni stores project credentials, and how it controls whether agent code can read each value.

A secret is a per-project credential — an API key, token, or connection string — that a session needs but that must not live in the repository. Beeuni stores secrets on the project, never on the account, and encrypts every value with AES-256-GCM using a key derived per project.

Identifier and name

Each secret has two names. The identifier is the handle you use in the CLI and in an agent’s secrets grant. The name is the uppercase environment-variable key in the sandbox, for example STRIPE_API_KEY. In most projects the identifier and the name match. They differ only when a project holds several candidate values for one name — for example, a primary and a backup Google Maps key can both resolve to GOOGLE_MAPS_API_KEY.

Every access rule uses the identifier. None of them uses the name.

Exposure and usage

A secret carries two independent settings. Read them separately; they answer different questions.

Setting Question it answers Values
Exposure Can agent code read the real value? environment, egress-enforced, none
Usage Who spends it? Agent code, LLM gateway, Connector, Git

Older Beeuni documentation presented one list that mixed the two. It is gone. There is no choice between a “network boundary” and an “HTTPS broker”: one mechanism serves every egress-enforced secret on every sandbox provider.

The three exposures

Exposure What the sandbox holds Use it for
Environment The real value, as a plain environment variable The default. Values the agent must compute with, and protocols that are not HTTPS
Egress-enforced A handle — a self-describing placeholder, worth nothing on its own Experimental. HTTPS calls, once the secrets_egress flag is on
None Nothing at all A credential only a Beeuni service spends, or a value kept on file and disabled

Usages

Most usages are assigned by Beeuni, not by you:

  • Agent code — implied whenever exposure is not none.
  • LLM gateway — assigned when the value is a recognized model-provider key. The gateway authenticates provider requests server-side.
  • Connector:<slug> — assigned by the connector binding flow.
  • Git — assigned by Beeuni for its own Git access. Read-only; you cannot set or clear it.

A secret with exposure none and no usage renders as Disabled: stored, encrypted, and spent by nothing.

Sent secrets and computed secrets

Which exposure a secret can use is a property of the upstream, not of Beeuni.

  • Sent secrets — the value travels on the wire. API keys, bearer tokens, passwords. This is the vast majority. There is a moment where the value is bytes in an outbound request, so Beeuni can put it there itself, outside the sandbox. A sent secret can move to egress-enforced once the secrets_egress flag is on; by default it loads into the sandbox environment.
  • Computed secrets — the value is an ingredient in a calculation and never travels. AWS SigV4 signing keys, HMAC webhook-signing secrets, JWT client assertions, SSH private keys. Whoever computes must hold the value. No network boundary helps, because nothing on the wire contains the credential. Computed secrets must stay on environment.

Environment is the default exposure, and the only one that can serve a credential the sandbox has to do math with. Non-HTTPS protocols — a Postgres connection string, SMTP credentials — are in the same position: they must stay on environment.

When you save a value that looks like signing material, such as an AKIA… access-key pair or PEM/SSH material, Beeuni defaults it to environment and says why: this key signs requests locally, so egress enforcement cannot apply.

Egress-enforced exposure

The sandbox receives an environment variable whose value is a handle, not the credential. The agent uses that variable exactly as it would use the real key — in a header, a query string, or a body. Authorization: Bearer $VAR, Cookie: …=$VAR, an X-Api-Key header, a query parameter, and a JSON or form body field all work: Beeuni finds the handle wherever it appears (raw, URL-encoded, standalone base64, or JSON-escaped) and swaps in the real value. On the way out, Beeuni replaces the handle with the real value, but only for requests to hosts you approved.

Facts that follow from that shape:

  • The real value is never in the sandbox: not an environment variable, not a file, not an alias.
  • A handle sent to a host you did not approve arrives as the literal handle string. The upstream rejects it. It is worth nothing.
  • A handle with a bad signature is never honored, and Beeuni records it as a forged handle. A valid handle for a secret this session may not spend is recorded as a stolen one.
  • Every relayed request writes a per-request audit record.
  • Hosts that are not on the list are tunnelled without being read. Pinned-TLS and mTLS clients to those hosts are unaffected.

Hosts match exactly

List every host you call, one exact hostname per line.

Rejected Reason
A wildcard host, *.example.com The agent must never choose the destination
A URL scheme other than HTTPS Beeuni terminates TLS to substitute

api.example.com does not cover uploads.api.example.com. Add the second host to the same secret. Beeuni rejects an unenforceable policy with 400 when you save it — it never stores a rule it cannot apply.

Two egress-enforced secrets may share one host. Each handle maps to its own value, so both substitute correctly in the same request.

Verify it with two probes

Run both probes from inside the sandbox, against a host that is on the list. The example uses postman-echo.com, which serves one endpoint of each kind. Add it as an allowed host for the duration of the test.

# 1. Reachability — an endpoint that does NOT echo request headers.
curl -s -o /dev/null -w '%{http_code}\n' https://postman-echo.com/status/200
# expected: 200

# 2. Substitution — an endpoint that DOES echo request headers.
curl -sS -H "authorization: Bearer $STRIPE_API_KEY" https://postman-echo.com/get
# expected: 200, with "Bearer [REDACTED]" in the echoed headers

Read the pair together, probe 1 first: it is the only one that tells you the host is reachable, and probe 2 proves nothing until it passes.

Probe 2 result Meaning
200, echoed header shows [REDACTED] Working. The real value went upstream and the echo was redacted on the way back.
200, echoed header shows the handle itself The substitution did not run. Check the host list and the agent grant.
401 from a real API host The substitution did not run. Same two checks.
An empty reply or a connection error A real failure. This is not a success symptom.

Confirm the real value is nowhere in the guest:

env | grep '^STRIPE_API_KEY='
# expected: the handle, not the credential

The identifier also appears inside BEEUNI_SECRET_CAPABILITIES, the value-free catalog that tells the agent which secrets exist, which variable holds each handle, and which hosts each one covers. It never carries a value.

A host on the list presents a certificate Beeuni issued for this sandbox, which the sandbox already trusts, rather than the origin’s own:

curl -sv https://postman-echo.com/status/200 2>&1 | grep 'issuer:'
# issuer: CN=Beeuni Egress CA (<project prefix>); O=Beeuni

What the relay changes

An approved host is reached through Beeuni, so it behaves a little differently from a direct call:

  • Responses are not streamed. Server-sent events and websockets do not work through an approved host.
  • A request body is capped at 1 MiB, a response at 5 MiB, and the whole call at 30 seconds. Beeuni follows at most 3 redirects.
  • Only a fixed set of response headers comes back — content type and language, caching validators, the rate-limit family, retry-after, x-request-id.
  • Only clients that honour https_proxy are intercepted. Beeuni sets that variable, and the matching CA trust, in the agent’s environment. A process started with a scrubbed environment calls the host directly, and its request leaves carrying the handle.

For a request Beeuni cannot intercept, the agent has an explicit door to the same hosts under the same policy:

beeuni secrets call STRIPE_API_KEY https://api.stripe.com/v1/customers

Environment exposure

The sandbox receives the real value as a plain environment variable. Agent code can read it, print it, and forward it anywhere. Beeuni cannot redact it, cannot audit its use, and cannot stop it leaving.

Environment is the default exposure. Every new secret uses it unless you move the secret to egress-enforced, which is experimental and needs the secrets_egress flag. A computed credential, and any non-HTTPS protocol, must stay on environment.

Who can use a secret

Each secret value has an audience — the same Who can use it choice as a connector account:

Choice Who can use the value
Everyone in the project (default) Every session in the project, including triggers, schedules, and shared sessions.
Only you You, directly, and your own private sessions.
Specific people, groups, or agents The people and groups you pick (directly, and in their own private sessions), and the agents you pick (in every one of their sessions).

A session acts as two things at once: its agent, always, and its person, only while the session is private.

  • A person reaches a value in a session that is private and was started by that person. A prompt from anyone else clears the session’s person, so the value stops reaching it. Shared sessions, triggers, schedules, webhooks, email and Telegram sessions have no person.
  • An agent reaches a value in every session of that agent — triggers, schedules and shared sessions included. Share a value with the agent when an unattended run needs it. Anyone who can run that agent can then use the value through it, so running the agent is the gate (who may run an agent is set on the agent).
  • Server-side consumers follow the same rule. A connector bound to the secret spends it only for a caller in the audience. Anyone else gets credential_not_shared, and no request leaves Beeuni.
  • Beeuni services that act for nobody — the Git proxy, webhook signature checks, channel installs, and Beeuni Apps — use values shared with everyone only.

Several values may share one name. A value shared with the session’s person wins over one shared with its agent, which wins over the value shared with everyone. Example: DEEL_API_TOKEN holds a team value, and a second value under the identifier DEEL_API_TOKEN-finance is shared only with the Finance group. Finance members get the Finance value in their private sessions; everyone else, and every trigger, gets the team value.

A person sets the audience. An agent session cannot change it (403), so an agent cannot widen a value narrowed away from it, or add itself. People who manage the project’s secrets still see a value that is not shared with them, marked Not shared with you, so they can widen it again.

Sharing a session that holds a personal value

A value shared only with you that reached your private session as an environment variable is plaintext inside that sandbox. Sharing the session would let every new viewer read it, and an environment variable cannot be taken back out of a running sandbox. So Beeuni refuses to share that session, or mint a public link to it, with 409 PERSONAL_SECRET_REQUIRES_PRIVATE_SESSION and the names it holds. Start a new session to share, or share the value with everyone first. Values reached through the agent, and egress-enforced or server-side values (checked on every request), do not block sharing.

When you narrow an audience, Beeuni re-pushes every running sandbox. A value already delivered as an environment variable stays in a sandbox until it restarts. If it may have been read, rotate it.

An agent that needs a value you have not given it sends a link. The link page offers Who can use it: everyone in the project (the default), or only the person who asked. Choose the second for anything personal — a payroll token, a personal login. The page names the person who asked by display name only, never by email, because anyone with the link can open it.

Personal override

A personal override is your own value for one name, used instead of the shared value in sessions you start. Today Beeuni uses it only for one OAuth login credential. Beeuni never lets an LLM provider key, for example ANTHROPIC_API_KEY, become a personal override.

Add a secret

Set the value from the CLI

beeuni secrets set STRIPE_API_KEY=sk_live_...

Beeuni saves it for everyone in the project. Add --only-me to make it usable only by you. To store more than one value under the same name, add --identifier <id>. Names can’t start with BEEUNI_ — Beeuni reserves that prefix for platform values.

Change who can use it later:

beeuni secrets share DEEL_API_TOKEN --user me
beeuni secrets share DEEL_API_TOKEN --user finance@example.com --group <group-id>
beeuni secrets share NIGHTLY_REPORT_TOKEN --agent reporter
beeuni secrets share DEEL_API_TOKEN --everyone

Or use the dashboard

Open the project’s Secrets page, enter the key and value, pick Who can use it, and save. Beeuni encrypts the value immediately and defaults every new secret to Everyone in the project and environment exposure — the real value loads into the sandbox. The Who can use it column shows each value’s audience.

To move a secret to egress-enforced exposure, first enable the secrets_egress feature flag (Settings → Feature flags); it is experimental. With the flag on, the Secrets page shows the “Can your code read this value?” control: answering no moves the secret to egress enforcement and shows the host list; answering yes keeps environment exposure. With the flag off, a secret stays on environment.

Grant it to an agent

Pick an agent on the Secrets page, or add the identifier to that agent’s secrets list in beeuni.yaml yourself. A session only receives the secrets its agent is granted. See Grant a secret to an agent.

Grant a secret to an agent

A session receives a secret only when the agent it runs names the identifier in its secrets list. Matching uses the identifier, not the name, and ignores case.

From the dashboard

The Secrets page marks a secret no agent can receive: No agent can receive this secret. Choose an agent there and confirm. Beeuni edits beeuni.yaml and commits it as chore(agents): grant <IDENTIFIER> to <agent>.

The grant works whether or not the manifest already declares that agent. An agent the manifest does not declare gets a new entry holding this one secrets list. An agent that is already declared keeps every other field — model, tools, connectors — and the identifier joins its existing list. An agent that already admits the identifier needs no commit, and Beeuni makes none.

An agent on secrets: all is a special case. all cannot carry an egress-enforced secret, so Beeuni writes an explicit list: every identifier the project has today, plus this one. Nothing the agent receives today changes. A secret you add later needs its own grant.

Two cases refuse the grant:

  • A project on beeuni_version: 1 (beeuni.toml) has no agents map to edit. The request fails with 400 and manifest_v1_unsupported. Edit the manifest by hand, or move the project to beeuni_version: 2.
  • A secret that is Disabled has nothing to deliver. The request fails with 409 and secret_not_grantable. Give it an exposure first.

If the project has no agents: block yet, read The first agents: block changes the whole project before you confirm. That one edit changes secret access for every other agent.

By hand

The same grant, written directly:

beeuni_version: 2
agents:
  my-agent:
    secrets: [STRIPE_API_KEY]

When a session asks for a secret its agent is not granted

An agent that needs a credential mints a secret link (beeuni secrets request). The value you enter is saved even when the session’s agent is not granted it. That session never receives it, so Beeuni says so on every surface:

  • The link form shows Saved, but <agent> can’t read it yet and names the secret.
  • The mint response and the follow-up message to the session list the withheld names with the fix.
  • Inside the session, beeuni secrets ls shows a declared key outside the grant as not granted, not missing.

The agent cannot widen its own grant. Open Customize → Agents → <agent> → Secrets and enable the secret. Beeuni pushes the change to the agent’s running sessions when you save it. The agent can also pull it at once with beeuni secrets sync, and every prompt re-syncs a session’s secrets. No restart is needed.

The first agents: block changes the whole project

So list every agent that needs secrets, not only the one you are fixing. This is why the dashboard asks you to confirm the first time: after that commit, agents: is the project’s allow-list, and an agent missing from it runs with no project secrets.

Exposing capabilities to third-party users

If you are building something where untrusted third-party users reach a capability — a public chat surface, a shared app, an agent anyone on the internet can prompt — do not hand them secret policies at all. Every user of that surface shares one project agent, one grant, and one host list. Beeuni has no way to tell one of your customers from another, so an egress-enforced secret that any user’s prompt can reach is a credential every user can spend, up to the full scope the upstream key carries.

Build the boundary you actually need, on your side:

  1. Stand up your own authorization and proxy service. It holds the upstream credential.
  2. Point the agent at your service, not at the upstream. Give the agent only a credential for your service — that one can be egress-enforced to your own host.
  3. Your service identifies the end user, applies your own authorization rules and per-user quotas, and only then makes the upstream call with the credential it holds.

That service is where per-user rules belong: who may call what, how often, for which records. Beeuni secret exposure is one layer below it, and it does not substitute for it.

List your secrets

Run beeuni secrets ls to see which secrets a project declares and which ones have a value set.

The list is configuration metadata. It never returns secret values. A scoped agent token sees only identifiers in its agent grant. A session-specific secrets_allowlist controls delivery into that session, but it does not hide configuration metadata that the agent grant permits.

Rotate a secret

Set a new value

Run the same command with the new value, or set it again on the project’s Secrets page:

beeuni secrets set STRIPE_API_KEY=sk_live_new...

Beeuni pushes it to running sessions

Beeuni pushes the change to every sandbox with an active session for the project, on a best-effort basis. For model or gateway credentials, Beeuni restarts the default runtime compatibility process.

Rotating an egress-enforced secret needs no push of the value at all: the sandbox holds a handle, and Beeuni reads the current value when the next request comes through.

Remove a secret

Run beeuni secrets unset STRIPE_API_KEY (or unset <identifier>), or delete it from the project’s Secrets page.

Share a value without seeing it

Run beeuni secrets request STRIPE_API_KEY to create a link. Anyone with the link can enter the value. You never see it. Links stay valid for 7 days by default; adjust with --expires <minutes> (max 30 days). An expired link shows a clear “expired” page — mint a fresh one with the same command.

Give an agent a value directly

If you paste a key in a session, the agent stores it at once with beeuni secrets set. It does not send you a link. The agent needs the project’s secret-write permission. Without it, the agent gets 403 and mints a secret link instead. An agent can store an environment secret or a connector secret (--scope connector). Enforced delivery and host lists stay human-only.

End-to-end example

beeuni.yaml:

beeuni_version: 2
default_agent: my-agent
agents:
  my-agent:
    secrets: [STRIPE_API_KEY]

Secret configuration. The first command stores the value with the default environment exposure. The second moves it to egress-enforced exposure, which is experimental and needs the secrets_egress flag (Settings → Feature flags); it returns 403 feature_disabled while the flag is off.

beeuni secrets set STRIPE_API_KEY=sk_live_...
beeuni secrets delivery STRIPE_API_KEY egress --allow-host api.stripe.com

The agent then calls Stripe with the variable it was given:

curl -s -o /dev/null -w '%{http_code}\n' \
  -H "authorization: Bearer $STRIPE_API_KEY" \
  https://api.stripe.com/v1/customers
# expected: 200

$STRIPE_API_KEY holds a handle. Stripe receives the real key, because api.stripe.com is on the list. The same request to a host that is not on the list sends the handle, and the upstream rejects it.

The same request from an agent whose secrets list omits STRIPE_API_KEY returns 401. That session starts normally — an ungranted secret is not an error, it is simply never delivered.

CLI commands

Command What it does
beeuni secrets ls List secrets declared and set for the project
beeuni secrets set KEY=VALUE [--identifier <id>] [--scope runtime|connector] Create or update a secret. KEY=- reads the value from stdin. --scope connector keeps it server-side
beeuni secrets unset IDENTIFIER Remove a secret
beeuni secrets delivery IDENTIFIER egress --allow-host <host> Egress-enforced exposure for the listed hosts. Experimental; needs the secrets_egress flag
beeuni secrets delivery IDENTIFIER runtime Environment exposure (the default)
beeuni secrets delivery IDENTIFIER denied Disabled
beeuni secrets call IDENTIFIER URL Send one policy-bound HTTPS request through Beeuni. Experimental; needs the secrets_egress flag
beeuni secrets sync In an agent session: pull this session’s secrets and grant now. As a person: re-push every active sandbox of the project
beeuni secrets request NAME [--scope runtime|connector] [--expires <min>] Create a link so someone else can enter a value
beeuni env push --from <path> Upload a .env file as secrets
beeuni env pull [--out <path>] [--force] Export secret names, not values, to a .env file

The CLI keeps the stored vocabulary: runtime is environment exposure, egress is egress-enforced, denied is disabled. Run beeuni secrets --help for every flag.

Setup links default to connector. Use --scope runtime only when the agent’s shell must receive the value. A secret bound to a connector stays server-side.

Names and permissions

Format rules for the two names:

Name Format
identifier ^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$
name ^[A-Z_][A-Z0-9_]{0,63}$

Reading or writing a secret needs the project.secret.read or project.secret.write permission. A project manager holds both. A custom role only adds permissions — Beeuni has no deny rule — so no role can withhold either one from a manager. To restrict a manager, remove the manager role.

Delivery to a session is the role verdict of the person who started it, intersected with the launched agent’s manifest grant. Both must allow the secret. See One vocabulary, two bindings.

REST routes

All routes sit under /v1/projects/{projectId}.

Method Path Description
GET /secrets List secrets. Scoped to the caller’s grant if the caller is a scoped agent token.
POST /secrets Create or update the shared value. Body: {name, identifier?, value}, plus an optional delivery policy.
PUT /secrets/{identifier}/strategy Change the exposure and its host list.
POST /secrets/{identifier}/grant Add the identifier to one agent’s secrets list in beeuni.yaml. Body: {agent}.
DELETE /secrets/{name} Delete the shared value. Personal overrides stay in place.
PUT /secrets/{name}/personal Set or turn on the caller’s personal override.
DELETE /secrets/{name}/personal Remove the caller’s personal override.

POST /secrets rejects names that start with BEEUNI_. It returns 409 if the identifier already exists with a different name. It rejects the exact name CODEX_AUTH_JSON with 400 — Beeuni manages that secret through ChatGPT subscription onboarding.

Both write routes return a delivery_sync object when the change had to reach running sandboxes. ok: false means the value is saved but at least one live session still uses the previous one; the listed sessions pick it up on restart.

A secret in the list carries delivery_blocked_reason. The value no_agent_grant means no agent can receive this secret. null means it is granted, the exposure needs no grant, or Beeuni could not read the manifest.

POST /secrets/{identifier}/grant clears that reason. It returns already_granted: true when the agent’s list already admits the identifier, in which case Beeuni commits nothing. It returns adopted_governance: true when the edit added the project’s first agents: block — the change described above. It answers 400 manifest_v1_unsupported for a beeuni.toml project and 409 secret_not_grantable for a disabled secret.

Rotation and propagation

A secret write does not wait for a session restart. Beeuni pushes the change to every active sandbox in the project:

  1. Beeuni builds a new environment snapshot, using the running agent’s secrets grant.
  2. The sandbox writes the snapshot to the live agent environment. New tool calls pick up the change right away.
  3. If the changed secret is an LLM provider credential, Beeuni restarts default runtime.

This push is best-effort. The API call that changes the secret returns before the push finishes. A failed push is only logged, not retried. A sandbox with a failed push keeps the old value until the next successful push, or until the session restarts.

Model credentials

A project on Beeuni’s managed model access needs no key of its own. To bring your own, set the provider variables your default runtime provider config references. A recognized model key is assigned the LLM gateway usage, which spends it server-side; it needs no sandbox presence.

Do not use a generic provider verification result as runtime proof. It cannot prove the selected model, region, entitlement, and API dialect. Send a real prompt through the exact model.

Was this page helpful?