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_KEY

The 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 with LD_, NC_ or NOCLICK_ are reserved: a secret holding one is refused with noclick.InvalidRequest when 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 assistant

Delete

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

LimitValue
Keys in one secret100
One value32,768 characters
Secrets per sandbox or agent20
Secrets per account, its users' included1,000

Secrets cost nothing.

Errors

ErrorWhen
NotFoundno secret by that name or id for the user the call acts for
PermissionDenieda user's call changing or deleting your account's secret
Conflictthe name is taken; delete while an agent lists it
InvalidRequesta 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
QuotaExceededthe 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.