Secrets
Named environment values, like API keys, for the code your sandboxes, agents and apps run.
A secret is a named set of values, such as API keys and passwords, that the code you run reads from its environment. Give one to a sandbox, an agent's shell or an app, and each key becomes an environment variable there: OPENAI_API_KEY, ERP_PASSWORD. Values are write-only: once saved, nothing reads one back, including the console.
Secret or connection
Both hold credentials, for different jobs:
- A connection is an account at a service NoClick knows how to call: your user's Gmail, their HubSpot. NoClick signs the user in, keeps the tokens fresh, and makes the calls for you and your agents. Your code never holds the token.
- A secret is raw values for code you run yourself: an API key your script sends, a database password, a license key. NoClick stores them and puts them in the environment; your code does the calling.
If NoClick has an integration for the service, use a connection. For anything else your code talks to, use a secret.
Create a secret
import os
import noclick
openai = noclick.Secret.from_name(
"openai", values={"OPENAI_API_KEY": os.environ["OPENAI_API_KEY"]}, create_if_missing=True,
)
print(openai.keys) # ['OPENAI_API_KEY']from_name with create_if_missing=True creates the secret on first use, holding values, and after that sets the keys in values on the one that exists, so a key you rotate in your environment reaches the secret the next time your code runs. NoClick compares the values itself (you can't read them back), so running it again with the same ones changes nothing. Keys it doesn't name are kept: remove one with update. create makes one outright and raises noclick.Conflict if the name is taken.
You can also declare a secret empty and let someone paste the values in the console, so they never pass through your code:
erp = noclick.Secret.from_name("acme-erp", create_if_missing=True, user="acme")A secret's name is 1 to 64 letters, digits and . _ -. A key is an environment variable name: letters, digits and _, not starting with a digit. keys lists a secret's names; its values never come back, and NoClick keeps them encrypted.
Use it in a sandbox
Pass secrets when you create the sandbox (a Secret, its id or its name). Each key is an environment variable for every command:
import noclick
with noclick.Sandbox.create(secrets=[noclick.Secret.from_name("openai")], network=True) as sb:
sb.exec("python summarize.py") # reads os.environ["OPENAI_API_KEY"]Anything the sandbox runs can read the values, and print them. Give a sandbox only the secrets its job needs, especially when it runs code a model wrote.
Use it in an agent's shell
Give an agent secrets and its shell has them as environment variables in every turn:
import noclick
analyst = noclick.Agent.from_name(
"analyst",
create_if_missing=True,
instructions="You analyse our orders. Call the shipping API with $SHIPPING_API_KEY when you need tracking data.",
secrets=["shipping"],
)secrets is part of the agent's definition: with create_if_missing=True, from_name keeps it in step with your code like the rest. The agent sees only the names, and the values when a command reads them. An account agent is shared by every user who messages it, and so are its secrets, so it takes your account's secrets only; a user's agent takes that user's and your account's.
An agent's shell takes no model provider's key (OPENAI_API_KEY, ANTHROPIC_API_KEY, OPENROUTER_API_KEY and the like): the key an agent thinks with belongs on its model_connection, and a key in its shell could change which account its harness bills. Listing a secret that holds one, or adding one to a secret an agent lists, raises noclick.InvalidRequest. A sandbox takes them: it's your code calling the provider.
Use it in an app
An app binds a secret like any other resource. Each key reaches the deployed code as an environment variable of the same name:
import noclick
app = noclick.Project.from_name("booking")
app.bind("SHIPPING", noclick.Secret.from_name("shipping"))
app.deploy() # the app reads SHIPPING_API_KEYThe binding's name, SHIPPING, is how app.bindings() lists it. An app's own secrets (app.secrets.set) are for values only that app needs; a bound secret is shared, so one change reaches every app, sandbox and agent that uses it. See Apps.
Change values
update merges by default: the keys you pass are set, a key set to None is removed, and the rest are kept. replace=True makes the secret hold exactly what you pass.
import os
import noclick
openai = noclick.Secret.from_name("openai")
openai.update({"OPENAI_API_KEY": os.environ["NEW_OPENAI_API_KEY"]}) # rotate one key
erp = noclick.Secret.from_name("acme-erp", user="acme")
erp.update({"ERP_TOKEN": None}) # remove a key
erp.update({"ERP_USER": "svc-noclick", "ERP_PASSWORD": "..."}, replace=True)A change reaches new sandboxes at once, agents from their next turn, and apps at their next deployment. Sandboxes already running keep the values they started with.
When names clash
- Secrets are applied in the order you list them, so when two hold the same key, the later one wins.
- NoClick's own variables can't be overridden.
PATH,HOME,PWD,SHELL,USER,LOGNAME,PYTHONPATH,PYTHONHOME, and any name starting withLD_,NC_orNOCLICK_are reserved: a secret holding one is refused withnoclick.InvalidRequestwhen you save it. - In an app, its own secrets win over a bound secret's keys of the same name.
Whose secret it is
A secret created inside as_user belongs to that user; one created outside belongs to your account. A user's sandboxes and agents can use that user's secrets and your account's, never another user's, and acting for a user, from_name finds the user's own first. A user session, bound to one user, can use that user's secrets only. A user's call reads your account's secrets (names only) but changes only its own. See Users.
A secret's used_by lists who reads it: agents and running sandboxes (and, once apps ship, apps with their binding name). A user's call sees only its own and your account's readers.
for reader in noclick.Secret.from_name("openai").used_by:
print(reader["kind"], reader["name"] or reader["id"]) # agent assistantDelete
noclick.Secret.from_name("old-provider").delete()While an agent lists the secret, delete raises noclick.Conflict naming it: take it out of the agent's secrets first. Sandboxes already running keep their environment until they end. An export lists secrets by name only.
Limits and billing
| Limit | Value |
|---|---|
| Keys in one secret | 100 |
| One value | 32,768 characters |
| Secrets per sandbox or agent | 20 |
| Secrets per account, its users' included | 1,000 |
Secrets cost nothing.
Errors
| Error | When |
|---|---|
NotFound | no secret by that name or id for the user the call acts for |
PermissionDenied | a user's call changing or deleting your account's secret |
Conflict | the name is taken; delete while an agent lists it |
InvalidRequest | a name or key that isn't valid, a reserved key, a value over 32,768 characters, more than 100 keys; a model provider's key in a secret an agent lists |
QuotaExceeded | the account already holds 1,000 secrets |
Every method is in the Secret reference: update, from_name and the rest. The secrets option is on Sandbox.create and Agent.from_name.