diff --git a/content/manuals/ai/sandboxes/agents/claude-code.md b/content/manuals/ai/sandboxes/agents/claude-code.md index 9b55656d769..7869f32f5c7 100644 --- a/content/manuals/ai/sandboxes/agents/claude-code.md +++ b/content/manuals/ai/sandboxes/agents/claude-code.md @@ -38,9 +38,10 @@ Claude Code requires either an Anthropic API key or a Claude subscription. $ sbx secret set -g anthropic ``` -Alternatively, export the `ANTHROPIC_API_KEY` environment variable in your -shell before running the sandbox. See -[Credentials](../security/credentials.md) for details on both methods. +You can also source the key from the `ANTHROPIC_API_KEY` environment variable +through a [credential binding](../security/credentials.md#credential-bindings); +the sandbox prompts you to approve one on first run. See +[Credentials](../security/credentials.md) for details. **Claude subscription**: If no API key is set, use the `/login` command inside Claude Code to authenticate via OAuth. diff --git a/content/manuals/ai/sandboxes/agents/codex.md b/content/manuals/ai/sandboxes/agents/codex.md index 86e06cd78dd..77431dab702 100644 --- a/content/manuals/ai/sandboxes/agents/codex.md +++ b/content/manuals/ai/sandboxes/agents/codex.md @@ -52,8 +52,9 @@ so browser-based authentication works without any extra setup. $ sbx secret set -g openai ``` -Alternatively, export the `OPENAI_API_KEY` environment variable in your shell -before running the sandbox. +You can also source the key from the `OPENAI_API_KEY` environment variable +through a [credential binding](../security/credentials.md#credential-bindings); +the sandbox prompts you to approve one on first run. See [Credentials](../security/credentials.md) for more details. diff --git a/content/manuals/ai/sandboxes/agents/copilot.md b/content/manuals/ai/sandboxes/agents/copilot.md index e8f2c80e722..40d0f495a7d 100644 --- a/content/manuals/ai/sandboxes/agents/copilot.md +++ b/content/manuals/ai/sandboxes/agents/copilot.md @@ -36,9 +36,11 @@ Copilot requires a GitHub token with Copilot access. Store your token using $ echo "$(gh auth token)" | sbx secret set -g github ``` -Alternatively, export the `GH_TOKEN` or `GITHUB_TOKEN` environment variable in -your shell before running the sandbox. See -[Credentials](../security/credentials.md) for details on both methods. +You can also source the token from the `GH_TOKEN` or `GITHUB_TOKEN` environment +variable through a +[credential binding](../security/credentials.md#credential-bindings); the +sandbox prompts you to approve one on first run. See +[Credentials](../security/credentials.md) for details. ## Configuration diff --git a/content/manuals/ai/sandboxes/agents/cursor.md b/content/manuals/ai/sandboxes/agents/cursor.md index f6481665213..4fb36ecc1e5 100644 --- a/content/manuals/ai/sandboxes/agents/cursor.md +++ b/content/manuals/ai/sandboxes/agents/cursor.md @@ -38,9 +38,10 @@ Cursor supports two authentication methods: an API key or OAuth. $ sbx secret set -g cursor ``` -Alternatively, export the `CURSOR_API_KEY` environment variable in your shell -before running the sandbox. See -[Credentials](../security/credentials.md) for details on both methods. +You can also source the key from the `CURSOR_API_KEY` environment variable +through a [credential binding](../security/credentials.md#credential-bindings); +the sandbox prompts you to approve one on first run. See +[Credentials](../security/credentials.md) for details. **OAuth**: If no API key is set, Cursor prompts you to sign in interactively on first run. The proxy intercepts the token exchange with diff --git a/content/manuals/ai/sandboxes/agents/docker-agent.md b/content/manuals/ai/sandboxes/agents/docker-agent.md index de68d668f49..09fafd952a2 100644 --- a/content/manuals/ai/sandboxes/agents/docker-agent.md +++ b/content/manuals/ai/sandboxes/agents/docker-agent.md @@ -38,11 +38,12 @@ $ sbx secret set -g openrouter You only need to configure the providers you want to use. Docker Agent detects available credentials and routes requests to the appropriate provider. -Alternatively, export the environment variables (`OPENAI_API_KEY`, +You can also source these from environment variables (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_API_KEY`, `XAI_API_KEY`, `NEBIUS_API_KEY`, -`MISTRAL_API_KEY`, `OPENROUTER_API_KEY`) in your shell before running the -sandbox. See -[Credentials](../security/credentials.md) for details on both methods. +`MISTRAL_API_KEY`, `OPENROUTER_API_KEY`) through +[credential bindings](../security/credentials.md#credential-bindings); the +sandbox prompts you to approve one per provider on first run. See +[Credentials](../security/credentials.md) for details. ## Configuration diff --git a/content/manuals/ai/sandboxes/agents/droid.md b/content/manuals/ai/sandboxes/agents/droid.md index d62612100c3..c3efe01c7ea 100644 --- a/content/manuals/ai/sandboxes/agents/droid.md +++ b/content/manuals/ai/sandboxes/agents/droid.md @@ -40,9 +40,10 @@ your Factory account. $ sbx secret set -g droid ``` -Alternatively, export the `FACTORY_API_KEY` environment variable in your shell -before running the sandbox. See -[Credentials](../security/credentials.md) for details on both methods. +You can also source the key from the `FACTORY_API_KEY` environment variable +through a [credential binding](../security/credentials.md#credential-bindings); +the sandbox prompts you to approve one on first run. See +[Credentials](../security/credentials.md) for details. **OAuth**: If no API key is set, Droid prompts you to authenticate interactively on first run. The proxy handles the OAuth flow, so credentials diff --git a/content/manuals/ai/sandboxes/agents/gemini.md b/content/manuals/ai/sandboxes/agents/gemini.md index 4bd194ca949..095e17937e3 100644 --- a/content/manuals/ai/sandboxes/agents/gemini.md +++ b/content/manuals/ai/sandboxes/agents/gemini.md @@ -38,9 +38,11 @@ Gemini requires either a Google API key or a Google account with Gemini access. $ sbx secret set -g google ``` -Alternatively, export the `GEMINI_API_KEY` or `GOOGLE_API_KEY` environment -variable in your shell before running the sandbox. See -[Credentials](../security/credentials.md) for details on both methods. +You can also source the key from the `GEMINI_API_KEY` or `GOOGLE_API_KEY` +environment variable through a +[credential binding](../security/credentials.md#credential-bindings); the +sandbox prompts you to approve one on first run. See +[Credentials](../security/credentials.md) for details. **Google account**: If no API key is set, Gemini prompts you to sign in interactively when it starts. Interactive authentication is scoped to the diff --git a/content/manuals/ai/sandboxes/agents/opencode.md b/content/manuals/ai/sandboxes/agents/opencode.md index 23896acb460..bc4665e7b4e 100644 --- a/content/manuals/ai/sandboxes/agents/opencode.md +++ b/content/manuals/ai/sandboxes/agents/opencode.md @@ -48,10 +48,12 @@ $ sbx secret set -g openrouter You only need to configure the providers you want to use. OpenCode detects available credentials and offers those providers in the TUI. -You can also use environment variables (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, -`GOOGLE_GENERATIVE_AI_API_KEY`, `XAI_API_KEY`, `GROQ_API_KEY`, -`AWS_ACCESS_KEY_ID`, `OPENROUTER_API_KEY`). See -[Credentials](../security/credentials.md) for details on both methods. +You can also source these from environment variables (`OPENAI_API_KEY`, +`ANTHROPIC_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY`, `XAI_API_KEY`, +`GROQ_API_KEY`, `AWS_ACCESS_KEY_ID`, `OPENROUTER_API_KEY`) through +[credential bindings](../security/credentials.md#credential-bindings); the +sandbox prompts you to approve one per provider on first run. See +[Credentials](../security/credentials.md) for details. ## Configuration diff --git a/content/manuals/ai/sandboxes/agents/shell.md b/content/manuals/ai/sandboxes/agents/shell.md index fab2621b172..25eafa9e93c 100644 --- a/content/manuals/ai/sandboxes/agents/shell.md +++ b/content/manuals/ai/sandboxes/agents/shell.md @@ -33,9 +33,11 @@ $ sbx run shell -- -c "echo hi" # runs bash -l -c "echo hi" When the first argument is a bare word, it replaces `-l` instead. -Set your API keys as environment variables so the sandbox proxy can inject -them into API requests automatically. Credentials are never stored inside -the VM: +Provide your API keys as environment variables so the sandbox proxy can inject +them into API requests. The proxy injects a key once a +[credential binding](../security/credentials.md#credential-bindings) authorizes +it — the sandbox prompts you to approve one on first run. Credentials are never +stored inside the VM: ```console $ export ANTHROPIC_API_KEY=sk-ant-xxxxx diff --git a/content/manuals/ai/sandboxes/security/credentials.md b/content/manuals/ai/sandboxes/security/credentials.md index 2aba19b3858..f8c63462f3a 100644 --- a/content/manuals/ai/sandboxes/security/credentials.md +++ b/content/manuals/ai/sandboxes/security/credentials.md @@ -21,17 +21,22 @@ declares the match and the header; you provide the value on the host. The real value never enters the sandbox — the agent sees only a sentinel like `proxy-managed`. -There are several ways to provide that value. When more than one source has a -value for the same service, the stored secret takes precedence over a host -environment variable. - -| Form | What it is | Use it when | -| ---- | ---------- | ----------- | -| [Stored secrets](#stored-secrets) (`sbx secret set`) | A value in your OS keychain, keyed by service | The default for any built-in or kit-declared service | -| [Custom secrets](#custom-secrets) (`sbx secret set-custom`) | A value keyed to a domain and environment variable | The service model doesn't fit — the agent validates the variable's format, or the secret rides in a request body | -| [Environment variables](#environment-variables) | Read from your shell session | One-off testing or CI, where keychain storage isn't worth it | -| OAuth | A host-side sign-in flow; the token never enters the sandbox | The agent supports it, such as Claude Code, Codex, or Cursor | -| [Registry credentials](#registry-credentials) (`sbx secret set --registry`) | Authentication for pulling images and kits | Pulling templates or kits from a private registry | +There are several ways to provide that value. For built-in agents, a +[credential bindings](#credential-bindings) entry authorizes each credential: it +records where the value is sourced from and which domains it may be injected +into. `sbx` creates this entry interactively the first time an agent needs the +credential. Without an authorizing binding, the credential is withheld rather +than injected. When a binding resolves more than one source, the stored secret +takes precedence over an environment variable or file. + +| Form | What it is | Use it when | +| --------------------------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | +| [Stored secrets](#stored-secrets) (`sbx secret set`) | A value in your OS keychain, keyed by service | The default for any built-in or kit-declared service | +| [Custom secrets](#custom-secrets) (`sbx secret set-custom`) | A value keyed to a domain and environment variable | The service model doesn't fit — the agent validates the variable's format, or the secret rides in a request body | +| [Environment variables](#environment-variables) | A shell variable a binding's discovery points at | One-off testing, where keychain storage isn't worth it | +| OAuth | A host-side sign-in flow; the token never enters the sandbox | The agent supports it, such as Claude Code, Codex, or Cursor | +| [Credential bindings](#credential-bindings) (`credentials.yaml`) | Per-service sourcing and domain approval | The default authorization for built-in agents; also restricts which domains a credential reaches | +| [Registry credentials](#registry-credentials) (`sbx secret set --registry`) | Authentication for pulling images and kits | Pulling templates or kits from a private registry | For multi-provider agents (OpenCode, Docker Agent), the proxy selects credentials based on the API endpoint being called. See individual @@ -236,24 +241,124 @@ the kit handles the wiring; you only provide the value. ## Environment variables -As an alternative to stored secrets, export the relevant environment variable -in your shell before running a sandbox: +A host environment variable isn't a credential source on its own — built-in +agents don't read host variables implicitly, so exporting `ANTHROPIC_API_KEY` +and running `sbx run claude` does nothing by itself. To use one, point a +[credential binding](#credential-bindings) at it, listing the variable under the +binding's `discovery`. `sbx` prompts you to create that binding the first time +an agent needs the credential, or you can write it yourself. + +With a binding in place, export the variable before you run the sandbox. See +individual [agent pages](../agents/) for the variable names each agent expects: ```console $ export ANTHROPIC_API_KEY=sk-ant-api03-xxxxx $ sbx run claude ``` -The proxy reads the variable from your terminal session. See individual -[agent pages](../agents/) for the variable names each agent expects. - > [!NOTE] -> These environment variables are set on your host, not inside the sandbox. +> These environment variables are read on your host, not set inside the sandbox. > Sandbox agents are pre-configured to use credentials managed by the > host-side proxy. For custom environment variables not tied to a > [built-in service](#built-in-services), see > [Setting custom environment variables](../faq.md#how-do-i-set-custom-environment-variables-inside-a-sandbox). +## Credential bindings + +A credential bindings file records, per service, where `sbx` finds each +credential value and which domains it may be injected into. It lives at +`~/.config/sbx/credentials.yaml`, or `%APPDATA%\sbx\credentials.yaml` on +Windows. + +Built-in agents require an authorizing binding for each credential they use. +`sbx` creates one interactively the first time you run an agent (see +[First-run approval](#first-run-approval)); you can also write entries by hand. + +Each entry under `bindings` is keyed by a +[service identifier](#built-in-services) and has two parts: + +- `discovery` — where to find the value: one or more environment variables, + or a file. Entries are tried in order. Omit `discovery` to resolve the value + from the [secret store](#stored-secrets) as usual. +- `allowedDomains` — the domains the proxy may inject this credential into. + The credential is never attached to a domain outside this list, even if a kit + declares it. + +```yaml +bindings: + anthropic: + discovery: + - env: [ANTHROPIC_API_KEY] + allowedDomains: [api.anthropic.com] + github: + discovery: + - env: [GH_TOKEN, GITHUB_TOKEN] + allowedDomains: [api.github.com, github.com] +``` + +For a file source, set `parser: json:` to pull a field from a JSON +file, or omit `parser` to use the whole file — see the +[file parser format](../customize/kit-reference.md#fileparser) in the kit spec +reference. Bindings +apply to services a kit or built-in agent already declares; they control how an +existing service's credential is sourced and scoped, not which services exist. + +For example, to source the GitHub token from a field in a JSON file: + +```yaml +bindings: + github: + discovery: + - file: + path: "~/.config/myapp/creds.json" + parser: "json:credentials.github.token" + allowedDomains: [api.github.com, github.com] +``` + +### First-run approval + +Built-in agents inject a credential only where a binding approves it. The first +time an agent needs a credential that has no binding, `sbx` walks you through +creating one. For an API key, you choose where the value comes from (the secret +store, an environment variable, or a file) and approve the domains it may reach. +For OAuth, you approve the sign-in domains and authenticate in the host flow — +there's no source to pick. Either way, `sbx` writes the entry to +`credentials.yaml`, and the same prompt appears in the terminal and in the +interactive TUI. + +In non-interactive contexts (CI or `--detached`), there's no one to answer the +prompt, so a missing binding is reported as a clear error naming the service +rather than a silently absent credential. Pre-create the binding — by running +the agent interactively once, or by writing `credentials.yaml` directly — before +running unattended. + +This makes the bindings file an allowlist of credential-to-domain approvals: an +agent can use only the credentials you've approved, only on the domains you've +approved. + + + +#### Which kits require a binding + +Requiring an approved binding is a property of the kit's `schemaVersion`, not of +whether the agent is built-in. Every built-in agent uses `schemaVersion: "2"`, +and so does any custom kit authored against it — all of them require a binding +and behave identically. Kits still on `schemaVersion: "1"` inject their declared +credentials without a binding. + +To hold older-schema kits to the same rule, turn on fail-closed mode: + +```console +$ sbx settings set credentials.failClosed true +``` + +With fail-closed on, every injected credential requires an approved binding, +regardless of the kit's schema. + ## Registry credentials Registry credentials authenticate to private OCI registries when pulling diff --git a/content/manuals/ai/sandboxes/troubleshooting.md b/content/manuals/ai/sandboxes/troubleshooting.md index db80a53a2a6..734f0e79855 100644 --- a/content/manuals/ai/sandboxes/troubleshooting.md +++ b/content/manuals/ai/sandboxes/troubleshooting.md @@ -118,6 +118,13 @@ If the agent can't reach its model provider or you see API key errors, the key is likely invalid, expired, or not configured. Verify it's set in your shell configuration file and that you sourced it or opened a new terminal. +If the agent starts unauthenticated, or a non-interactive run (`--detached` or +CI) fails with a "no approved binding" error, the credential has no +[credential binding](security/credentials.md#credential-bindings). Run the +agent interactively once to approve the binding at the prompt, or pre-create +the entry in `credentials.yaml`. A credential is injected only where a binding +authorizes it. + For agents that use the [credential proxy](security/credentials.md), make sure you haven't set the API key to an invalid value inside the sandbox — the proxy injects credentials automatically on outbound requests.