From 12aa7c37a39604f26efd18cc3fe8e68d4fd226e2 Mon Sep 17 00:00:00 2001 From: David Karlsson <35727626+dvdksn@users.noreply.github.com> Date: Fri, 3 Jul 2026 12:13:49 +0200 Subject: [PATCH 1/6] docs(sandboxes): update credentials documentation - Remove the environment variables section. Exporting API keys in your host shell is no longer a supported authentication path. - Document `sbx secret import` for moving API keys from environment variables into the keychain, with flags for non-interactive use (`--all`), overwriting (`--force`), and dry-run preview. - Lead the get-started authentication section with OAuth as the primary path for Claude Code, pointing to the credentials page for API keys. - Move CI credential setup to the workflows page, using `sbx secret import --all` and cross-referencing the built-in services table. - Revert agent-page language describing a first-run credential prompt that no longer applies to built-in agents. Co-Authored-By: Claude Sonnet 4.6 --- .../ai/sandboxes/agents/claude-code.md | 4 - content/manuals/ai/sandboxes/agents/codex.md | 3 - .../manuals/ai/sandboxes/agents/copilot.md | 4 - content/manuals/ai/sandboxes/agents/cursor.md | 4 - .../ai/sandboxes/agents/docker-agent.md | 6 -- content/manuals/ai/sandboxes/agents/droid.md | 4 - content/manuals/ai/sandboxes/agents/gemini.md | 4 - .../manuals/ai/sandboxes/agents/opencode.md | 5 -- content/manuals/ai/sandboxes/agents/shell.md | 10 +-- content/manuals/ai/sandboxes/get-started.md | 21 ++--- .../ai/sandboxes/security/credentials.md | 79 +++++++++---------- content/manuals/ai/sandboxes/workflows.md | 15 +++- 12 files changed, 61 insertions(+), 98 deletions(-) diff --git a/content/manuals/ai/sandboxes/agents/claude-code.md b/content/manuals/ai/sandboxes/agents/claude-code.md index 9b55656d769e..13b4d0976411 100644 --- a/content/manuals/ai/sandboxes/agents/claude-code.md +++ b/content/manuals/ai/sandboxes/agents/claude-code.md @@ -38,10 +38,6 @@ 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. - **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 86e06cd78dd5..ed7156434d59 100644 --- a/content/manuals/ai/sandboxes/agents/codex.md +++ b/content/manuals/ai/sandboxes/agents/codex.md @@ -52,9 +52,6 @@ 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. - See [Credentials](../security/credentials.md) for more details. ## Configuration diff --git a/content/manuals/ai/sandboxes/agents/copilot.md b/content/manuals/ai/sandboxes/agents/copilot.md index e8f2c80e7226..007a066572a7 100644 --- a/content/manuals/ai/sandboxes/agents/copilot.md +++ b/content/manuals/ai/sandboxes/agents/copilot.md @@ -36,10 +36,6 @@ 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. - ## Configuration Sandboxes don't pick up user-level configuration from your host. Only diff --git a/content/manuals/ai/sandboxes/agents/cursor.md b/content/manuals/ai/sandboxes/agents/cursor.md index f6481665213d..15d41517e039 100644 --- a/content/manuals/ai/sandboxes/agents/cursor.md +++ b/content/manuals/ai/sandboxes/agents/cursor.md @@ -38,10 +38,6 @@ 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. - **OAuth**: If no API key is set, Cursor prompts you to sign in interactively on first run. The proxy intercepts the token exchange with `api2.cursor.sh/auth/poll`, so credentials are managed by the host and aren't diff --git a/content/manuals/ai/sandboxes/agents/docker-agent.md b/content/manuals/ai/sandboxes/agents/docker-agent.md index de68d668f49a..db8daf227d05 100644 --- a/content/manuals/ai/sandboxes/agents/docker-agent.md +++ b/content/manuals/ai/sandboxes/agents/docker-agent.md @@ -38,12 +38,6 @@ $ 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`, -`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. - ## Configuration Sandboxes don't pick up user-level configuration from your host. Only diff --git a/content/manuals/ai/sandboxes/agents/droid.md b/content/manuals/ai/sandboxes/agents/droid.md index d62612100c34..18866041b5f7 100644 --- a/content/manuals/ai/sandboxes/agents/droid.md +++ b/content/manuals/ai/sandboxes/agents/droid.md @@ -40,10 +40,6 @@ 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. - **OAuth**: If no API key is set, Droid prompts you to authenticate interactively on first run. The proxy handles the OAuth flow, so credentials aren't stored inside the sandbox. diff --git a/content/manuals/ai/sandboxes/agents/gemini.md b/content/manuals/ai/sandboxes/agents/gemini.md index 4bd194ca9495..8dc0e68324bd 100644 --- a/content/manuals/ai/sandboxes/agents/gemini.md +++ b/content/manuals/ai/sandboxes/agents/gemini.md @@ -38,10 +38,6 @@ 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. - **Google account**: If no API key is set, Gemini prompts you to sign in interactively when it starts. Interactive authentication is scoped to the sandbox and doesn't persist if you remove and recreate it. diff --git a/content/manuals/ai/sandboxes/agents/opencode.md b/content/manuals/ai/sandboxes/agents/opencode.md index 23896acb4608..cefad09337b5 100644 --- a/content/manuals/ai/sandboxes/agents/opencode.md +++ b/content/manuals/ai/sandboxes/agents/opencode.md @@ -48,11 +48,6 @@ $ 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. - ## Configuration Sandboxes don't pick up user-level configuration from your host. Only diff --git a/content/manuals/ai/sandboxes/agents/shell.md b/content/manuals/ai/sandboxes/agents/shell.md index fab2621b1721..ec8c8cbda86a 100644 --- a/content/manuals/ai/sandboxes/agents/shell.md +++ b/content/manuals/ai/sandboxes/agents/shell.md @@ -33,13 +33,13 @@ $ 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: +Store credentials using [stored secrets](../security/credentials.md#stored-secrets) +before running the sandbox. The proxy injects them into outbound API requests; +credentials are never stored inside the VM: ```console -$ export ANTHROPIC_API_KEY=sk-ant-xxxxx -$ export OPENAI_API_KEY=sk-xxxxx +$ sbx secret set -g anthropic +$ sbx secret set -g openai ``` Once inside the shell, you can install agents using their standard methods, diff --git a/content/manuals/ai/sandboxes/get-started.md b/content/manuals/ai/sandboxes/get-started.md index 0c3088990151..2af4ecf1e9cc 100644 --- a/content/manuals/ai/sandboxes/get-started.md +++ b/content/manuals/ai/sandboxes/get-started.md @@ -124,25 +124,14 @@ option. ## Authenticate your agent -Agents need credentials for their model provider. How you provide them depends -on the agent. - For Claude Code with a Claude subscription (Max, Team, or Enterprise), no upfront setup is needed — use the `/login` command inside the sandbox to sign -in with OAuth. The session token stays on your host and is injected by a -proxy, not stored inside the sandbox. - -For agents that use API keys (or if you prefer API key authentication for -Claude Code), store the key before starting a sandbox: - -```console -$ sbx secret set -g anthropic -``` +in with OAuth. The session token stays on your host and is never stored inside +the sandbox. -This prompts for the secret value and stores it in your OS keychain. A proxy on -your host injects the key into outbound API requests so it's never exposed -inside the sandbox. See [Credentials](security/credentials.md) for details on -scoping, supported services, and alternative methods. +If you prefer to authenticate with an API key, see +[Credentials](security/credentials.md) for how to store one with +`sbx secret set`. To give the agent access to GitHub for creating pull requests or interacting with repositories: diff --git a/content/manuals/ai/sandboxes/security/credentials.md b/content/manuals/ai/sandboxes/security/credentials.md index 170c95b70a2c..89424f243934 100644 --- a/content/manuals/ai/sandboxes/security/credentials.md +++ b/content/manuals/ai/sandboxes/security/credentials.md @@ -22,16 +22,14 @@ 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. +value for the same service, the stored secret takes precedence. -| 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 | +| 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 | +| 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 | For multi-provider agents (OpenCode, Docker Agent), the proxy selects credentials based on the API endpoint being called. See individual @@ -93,16 +91,39 @@ $ sbx secret set my-sandbox openai > you set or change a global secret while a sandbox is running, recreate the > sandbox for the new value to take effect. -You can also pipe in a value for non-interactive use: +### Import from environment variables + +If you already have API keys set in your shell, `sbx secret import` reads them +and stores them in the keychain without typing each value manually: + +```console +$ sbx secret import +``` + +This scans your current session for the environment variables in the +[built-in services table](#built-in-services) below and prompts you to confirm +each one before writing. To import a single service: + +```console +$ sbx secret import openai +``` + +Pass `--all` to import everything without prompting (new entries only; existing +entries are left unchanged), or `--force` to overwrite existing entries: ```console -$ echo "$ANTHROPIC_API_KEY" | sbx secret set -g anthropic +$ sbx secret import --all +$ sbx secret import openai --force ``` +Pass `--dry-run` to preview what would be imported without writing anything. +Run `sbx secret ls` afterwards to confirm what's stored. For setting up +credentials in CI, see [CI and headless use](../workflows.md#ci-and-headless-use). + ### Built-in services -Each built-in service name maps to a set of environment variables the proxy -checks and the API domains it authenticates requests to: +Each built-in service name maps to the environment variables `sbx secret import` +reads and the API domains the proxy injects credentials into: | Service | Environment variables | API domains | | ------------ | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | @@ -118,9 +139,8 @@ checks and the API domains it authenticates requests to: | `openrouter` | `OPENROUTER_API_KEY` | `openrouter.ai` | | `xai` | `XAI_API_KEY` | `api.x.ai` | -When you store a secret with `sbx secret set -g `, the proxy uses it -the same way it would use the corresponding environment variable. You don't -need to set both. +When you store a secret with `sbx secret set -g `, the proxy injects +it into requests to the listed API domains. ### Services declared by kits @@ -236,26 +256,6 @@ proxy replaces it with the real value. The agent never sees the real secret. Prefer the [service-based flow](#stored-secrets) whenever it's an option — 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: - -```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. -> 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). - ## Registry credentials Registry credentials authenticate to private OCI registries when pulling @@ -348,10 +348,9 @@ $ sbx secret rm my-sandbox --registry ghcr.io -f ## Best practices -- Use [stored secrets](#stored-secrets) over environment variables. Stored - secrets are encrypted at rest in the OS keychain (or an encrypted file on - Linux hosts without a keychain), while environment variables are plaintext in - your shell. See [Where secrets are stored](#where-secrets-are-stored). +- Use [stored secrets](#stored-secrets) to provide credentials. They are + encrypted at rest in the OS keychain (or an encrypted file on Linux hosts + without a keychain). See [Where secrets are stored](#where-secrets-are-stored). - Don't set API keys manually inside the sandbox. Sandbox agents are pre-configured to use proxy-managed credentials. - Registry credentials you make available inside a sandbox are stored in the VM diff --git a/content/manuals/ai/sandboxes/workflows.md b/content/manuals/ai/sandboxes/workflows.md index c84e3dafabcd..f30fcbc2fed4 100644 --- a/content/manuals/ai/sandboxes/workflows.md +++ b/content/manuals/ai/sandboxes/workflows.md @@ -333,9 +333,18 @@ $ sbx rm ci-task ``` Agent credentials (API keys, GitHub token) can be pre-configured as global -secrets so they're available to any sandbox the CI runner creates: +secrets so they're available to any sandbox the CI runner creates. If the +relevant environment variables are already set in the CI environment (see the +[built-in services table](security/credentials.md#built-in-services) for which +variables each service reads), import them all at once: ```console -$ echo "$ANTHROPIC_API_KEY" | sbx secret set -g anthropic -$ echo "$GITHUB_TOKEN" | sbx secret set -g github +$ sbx secret import --all +``` + +To overwrite an existing stored entry, add `--force`. If the secret isn't +available as an environment variable, pass its value with `-t`: + +```console +$ sbx secret set -g anthropic -t "${{ secrets.ANTHROPIC_API_KEY }}" ``` From c13aa896fdcdf8336479879dd8c5e9047462c111 Mon Sep 17 00:00:00 2001 From: David Karlsson <35727626+dvdksn@users.noreply.github.com> Date: Fri, 3 Jul 2026 12:08:57 +0200 Subject: [PATCH 2/6] docs(sandboxes): document kit spec schemaVersion 2 Add schemaVersion 2 kit spec reference, update kits.md and kit-examples.md for the v2 credential declaration model. Co-Authored-By: Claude Sonnet 4.6 --- .../ai/sandboxes/customize/kit-examples.md | 38 +-- .../ai/sandboxes/customize/kit-reference.md | 222 +++++++++++------- .../manuals/ai/sandboxes/customize/kits.md | 106 ++++----- 3 files changed, 214 insertions(+), 152 deletions(-) diff --git a/content/manuals/ai/sandboxes/customize/kit-examples.md b/content/manuals/ai/sandboxes/customize/kit-examples.md index 4f28e25d77a8..30831e5b3b15 100644 --- a/content/manuals/ai/sandboxes/customize/kit-examples.md +++ b/content/manuals/ai/sandboxes/customize/kit-examples.md @@ -35,7 +35,7 @@ ruff-lint/ ``` ```yaml {title="ruff-lint/spec.yaml"} -schemaVersion: "1" +schemaVersion: "2" kind: mixin name: ruff-lint displayName: Ruff @@ -117,7 +117,7 @@ you'd set a custom environment variable; see the [FAQ](../faq.md#how-do-i-set-custom-environment-variables-inside-a-sandbox). ```yaml {title="nvm/spec.yaml"} -schemaVersion: "1" +schemaVersion: "2" kind: mixin name: nvm displayName: nvm @@ -179,7 +179,7 @@ the kit and install each certificate before running `update-ca-certificates`. ```yaml {title="internal-ca/spec.yaml"} -schemaVersion: "1" +schemaVersion: "2" kind: mixin name: internal-ca @@ -280,7 +280,7 @@ docker-review/ ``` ```yaml {title="docker-review/spec.yaml"} -schemaVersion: "1" +schemaVersion: "2" kind: mixin name: docker-review displayName: Dockerfile review skill @@ -344,7 +344,7 @@ built-in `claude` agent but drops `--dangerously-skip-permissions` so every tool call prompts for approval: ```yaml {title="claude-safe/spec.yaml"} -schemaVersion: "1" +schemaVersion: "2" kind: sandbox name: claude-safe displayName: Claude Code (with approval prompts) @@ -356,22 +356,22 @@ sandbox: entrypoint: run: [claude] -network: - serviceDomains: - api.anthropic.com: anthropic - console.anthropic.com: anthropic - serviceAuth: - anthropic: - headerName: x-api-key - valueFormat: "%s" - allowedDomains: - - "claude.com:443" +caps: + network: + allow: + - "claude.com:443" credentials: - sources: - anthropic: - env: - - ANTHROPIC_API_KEY + - service: anthropic + apiKey: + name: ANTHROPIC_API_KEY + inject: + - domain: api.anthropic.com + header: x-api-key + format: "%s" + - domain: console.anthropic.com + header: x-api-key + format: "%s" ``` Launch with the kit's `name:` as the agent argument to `sbx run`: diff --git a/content/manuals/ai/sandboxes/customize/kit-reference.md b/content/manuals/ai/sandboxes/customize/kit-reference.md index fd8d6cde2471..17f7ed195b6b 100644 --- a/content/manuals/ai/sandboxes/customize/kit-reference.md +++ b/content/manuals/ai/sandboxes/customize/kit-reference.md @@ -27,11 +27,48 @@ my-kit/ └── workspace/ ``` +## Schema versions + +Two schema versions are supported. `schemaVersion: "2"` is current and +recommended; `"1"` is still accepted. Both are parsed and validated the same way, +and a v1 spec is automatically normalized into the v2 model — so existing v1 kits +keep working unchanged. + +You don't have to migrate a kit all at once. Field validity isn't tied to the +version — you can adopt v2 fields incrementally, or even mix v1 and v2 spellings +in the same file, and `sbx kit validate` reports a deprecation warning naming the +v2 replacement for each legacy field. + +What changed in v2: + +| v1 | v2 | +| ------------------------------------------ | ---------------------------------------- | +| `credentials.sources.` | `credentials:` list entry with `service` | +| `network.allowedDomains` / `deniedDomains` | `caps.network.allow` / `deny` | +| `network.serviceDomains` / `serviceAuth` | `credentials[].apiKey.inject` | +| standalone `oauth:` block | `credentials[].oauth` | +| `environment.proxyManaged` | automatic — `credentials[].apiKey.name` | +| `memory` | `agentContext` | +| `kind: agent` / `agent:` block | `kind: sandbox` / `sandbox:` block | +| `tmpfs:` | `volumes:` entries with `type: tmpfs` | +| `settings:` / `kitDir` / `persistence` | removed (no replacement) | + +Credential discovery also moved out of the kit in v2: a kit declares which +credentials it needs and how to inject them, but where each value comes from is +controlled by the user through +[credential bindings](../security/credentials.md#credential-bindings). + +> [!NOTE] +> `mixins` and `sandbox.build` are accepted by the parser but not yet applied by +> the runtime — `sbx kit validate` reports them as accepted but not yet +> implemented. Don't rely on them yet. + ## Changelog Renamed fields are still accepted for backward compatibility, but `sbx kit validate` reports a deprecation warning for each, and a future -release may stop accepting them. Update kits to the current names. +release may stop accepting them. Update kits to the current names. For the full +v1-to-v2 field mapping, see [Schema versions](#schema-versions). ### v0.32.0 @@ -50,51 +87,101 @@ automatically the next time the sandbox starts. ## Top-level fields ```yaml -schemaVersion: "1" +schemaVersion: "2" kind: name: displayName: description: ``` -| Field | Required | Description | -| --------------- | -------- | -------------------------------------------------------------------------- | -| `schemaVersion` | Yes | Spec schema version. Set to `"1"`. | -| `kind` | Yes | `mixin` for kits that extend an agent; `sandbox` for kits that define one. | -| `name` | Yes | Unique identifier. Lowercase, alphanumeric, hyphens. | -| `displayName` | No | Human-readable name. | -| `description` | No | Short description. | +| Field | Required | Description | +| --------------- | -------- | ------------------------------------------------------------------------------------------------- | +| `schemaVersion` | Yes | Spec schema version. Use `"2"`; `"1"` is still accepted. See [Schema versions](#schema-versions). | +| `kind` | Yes | `mixin` for kits that extend an agent; `sandbox` for kits that define one. | +| `name` | Yes | Unique identifier. Lowercase, alphanumeric, hyphens. | +| `displayName` | No | Human-readable name. | +| `description` | No | Short description. | The sections below apply to both kinds. Sandbox kits also declare a [`sandbox:` block](#sandbox-block). ## Credentials +A kit declares the credentials it needs and how the proxy injects them into +outbound requests. It does not declare where the value comes from — discovery is +controlled by the user through +[credential bindings](../security/credentials.md), so a kit +can't read arbitrary host environment variables or files. + ```yaml credentials: - sources: - : - env: [, ...] - file: + - service: + description: # optional + required: # optional, default false + apiKey: + name: + inject: + - domain: + header:
+ format: + username: # optional, for HTTP basic auth + oauth: + tokenEndpoint: + host: path: - parser: - priority: + sentinels: + accessToken: + refreshToken: + credentialFile: + path: + template: ``` -| Field | Description | -| -------------------------- | ------------------------------------------------------------- | -| `sources` | Map of service identifier to credential source. | -| `sources..env` | Environment variables to read on the host, in priority order. | -| `sources..file.path` | Path on host. `~` expands to home directory. | -| `sources..file.parser` | How to extract the credential value from the file. | -| `sources..priority` | `env-first` (default) or `file-first`. | +`credentials` is a list; each entry names a `service` and configures one or more +auth mechanisms. + +| Field | Description | +| ------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| `service` | Credential identifier. Known providers (`anthropic`, `github`, `openai`, `google`, ...) auto-expand to their `apiKey` injection config. | +| `description` | Optional. Shown to the user when approving a [binding](../security/credentials.md#credential-bindings). | +| `required` | If `true`, sandbox creation fails when the credential is unavailable. Default `false`. | +| `apiKey` | API-key injection (see [apiKey](#apikey)). | +| `oauth` | OAuth interception (see [oauth](#oauth)). | + +For a known provider, `- service: anthropic` is enough — `apiKey.name` and +`inject` are filled in from the provider registry. Custom services must declare +`apiKey.name` and `apiKey.inject` (or `oauth`) themselves. -Service identifiers link credentials to [network rules](#network). +### `apiKey` + +| Field | Description | +| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | Environment variable set inside the container. The agent sees a sentinel (`proxy-managed`); the proxy injects the real value. Auto-derived for known providers. | +| `inject[].domain` | Domain to inject the credential into. Must also be allowed in [`caps.network`](#network). | +| `inject[].header` | HTTP header the proxy sets (for example, `x-api-key`, `Authorization`). | +| `inject[].format` | Header value format, with one `%s` placeholder (for example, `"%s"` or `"Bearer %s"`). | +| `inject[].username` | Optional. Use HTTP basic auth with this username instead of a bearer header (for example, `x-access-token` for Git over HTTPS). | + +### `oauth` + +For agents that authenticate with OAuth (for example, Claude Code), the proxy +intercepts token responses and replaces real tokens with sentinels, then swaps +the real token back in on outbound requests. The token never enters the sandbox. + +| Field | Description | +| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `tokenEndpoint.host` / `path` | The OAuth token endpoint the proxy intercepts. | +| `sentinels.accessToken` / `refreshToken` | Sentinel values written into the container in place of the real tokens. | +| `credentialFile.path` | Where to write the credential file inside the container (`~` expands). | +| `credentialFile.template` | JSON template for that file. `{{.AccessToken}}`, `{{.RefreshToken}}`, `{{.ExpiresAt}}`, and `{{.ScopesJSON}}` are substituted at runtime. | ### file.parser -`file.parser` tells the proxy how to extract a credential from the file at `file.path`. -Omit it for plain-text files; set it to `json:` to extract a field from a JSON file. +A credential sourced from a file — through a +[credential binding](../security/credentials.md#credential-bindings) `file` +source, or a legacy `credentials.sources` entry — can pull its value from a JSON +field. Omit the parser for plain-text files; set `json:` to extract a +field from a JSON file. | Value | Behavior | | ----------------- | ------------------------------------------------------------------------------------ | @@ -107,37 +194,7 @@ Only object keys can be navigated — arrays are not supported and there is no ` Keys that contain a literal `.` cannot be referenced. The resolved value must be a string, number, or boolean; numbers and booleans are converted to strings. Objects, arrays, and null are rejected. -When a source has both `env` and `file` defined, `priority` controls which is tried first. The -preferred source is used when it exists — the environment variable is set, or the file is -present on disk. If it doesn't, the other source is used instead. The choice is made once at -discovery time, so parser errors (missing JSON field, wrong value type, invalid JSON) surface -as errors rather than triggering a fallback. - -Plain-text token file: - -```yaml -credentials: - sources: - openai: - file: - path: "~/.openai/token" -``` - -Nested JSON field, with an environment variable as fallback: - -```yaml -credentials: - sources: - github: - env: - - GH_TOKEN - file: - path: "~/.config/myapp/creds.json" - parser: "json:credentials.github.token" - priority: file-first -``` - -Given `~/.config/myapp/creds.json`: +For example, given this file: ```json { @@ -147,9 +204,9 @@ Given `~/.config/myapp/creds.json`: } ``` -The proxy resolves the credential to `ghp_xyz`, falling back to `GH_TOKEN` if the file is -missing. If the file exists but the JSON path doesn't resolve, the request fails with the -parser error below instead of falling back. +`json:credentials.github.token` resolves to `ghp_xyz`. If the path doesn't +resolve — a missing field, or a value that isn't a string — the request fails +with one of the errors below. Common errors when using `json:` parsers: @@ -162,25 +219,27 @@ Common errors when using `json:` parsers: ## Network +Network egress is declared under `caps.network`. Credentials no longer carry +their own domain mapping — the proxy injects a credential only into the domains +its [`apiKey.inject`](#apikey) (or provider default) lists, and every domain the +sandbox reaches must be allowed here. + ```yaml -network: - allowedDomains: [, ...] - deniedDomains: [, ...] - serviceDomains: - : - serviceAuth: - : - headerName:
- valueFormat: +caps: + network: + allow: [, ...] + deny: [, ...] ``` -| Field | Description | -| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| `allowedDomains` | Domains the sandbox can reach. Wildcards supported. | -| `deniedDomains` | Domains the sandbox is blocked from reaching. Deny rules take precedence over allow rules, including those from other composed kits. | -| `serviceDomains` | Map of domain to service identifier from `credentials.sources`. | -| `serviceAuth.headerName` | HTTP header the proxy sets (for example, `Authorization`). | -| `serviceAuth.valueFormat` | Format string for the header value (for example, `"Bearer %s"`). | +| Field | Description | +| -------------------- | --------------------------------------------------------------------------------------------------------------- | +| `caps.network.allow` | Domains the sandbox can reach. Wildcards supported. | +| `caps.network.deny` | Domains the sandbox is blocked from reaching. Deny takes precedence over allow, including across composed kits. | + +In v1 this was the `network:` block (`allowedDomains` / `deniedDomains`, plus +`serviceDomains` / `serviceAuth`). Those still parse with a deprecation warning: +domain lists fold into `caps.network`, and `serviceDomains` / `serviceAuth` fold +into [`credentials[].apiKey.inject`](#apikey). ## Environment @@ -188,16 +247,19 @@ network: environment: variables: : - proxyManaged: [, ...] ``` -| Field | Description | -| -------------- | ------------------------------------------------------------------------------------------------------------------- | -| `variables` | Key-value pairs set directly in the container. | -| `proxyManaged` | Environment variable names populated by the proxy at request time. Pair with [`credentials.sources`](#credentials). | +| Field | Description | +| ----------- | ---------------------------------------------- | +| `variables` | Key-value pairs set directly in the container. | Variable names must be valid shell identifiers (`[A-Za-z_][A-Za-z0-9_]*`). +In v1, `proxyManaged` listed the variables the proxy populated at request time. +That's now automatic: declaring a credential with `apiKey.name: ` sets +`` to a sentinel in the container and injects the real value at the proxy. +`proxyManaged` still parses with a deprecation warning. + ## Commands ```yaml diff --git a/content/manuals/ai/sandboxes/customize/kits.md b/content/manuals/ai/sandboxes/customize/kits.md index 0e66ef55a47a..369114bcf4fc 100644 --- a/content/manuals/ai/sandboxes/customize/kits.md +++ b/content/manuals/ai/sandboxes/customize/kits.md @@ -142,16 +142,17 @@ Network rules define which domains the sandbox can reach or block. Kit network rules apply only to sandboxes that use the kit: ```yaml -network: - allowedDomains: - - api.example.com - - "*.cdn.example.com" - deniedDomains: - - telemetry.example.com +caps: + network: + allow: + - api.example.com + - "*.cdn.example.com" + deny: + - telemetry.example.com ``` -Use `allowedDomains` for hosts the agent needs, such as package -registries, install endpoints, or external APIs. Use `deniedDomains` for +Use `allow` for hosts the agent needs, such as package +registries, install endpoints, or external APIs. Use `deny` for hosts the agent should not reach, such as telemetry endpoints. If a domain matches both an allow rule and a deny rule, the deny rule wins. @@ -172,39 +173,37 @@ host-side proxy. The agent inside the VM works with a sentinel value; the proxy reads the real credential on the host and overwrites the auth header before the request leaves the sandbox. -The standard pattern uses four blocks tied to a service identifier -you choose (here, `my-service`): +A kit declares the service, the in-container environment variable, and how +to inject the credential. It does not declare where the value comes from — +that's the user's +[credential binding](../security/credentials.md): ```yaml -network: - allowedDomains: - - api.example.com - serviceDomains: - api.example.com: my-service # Tag traffic to this domain - serviceAuth: - my-service: - headerName: Authorization # Overwrite this header - valueFormat: "Bearer %s" - credentials: - sources: - my-service: - env: - - MY_SERVICE_API_KEY # Host-side credential lookup - -environment: - proxyManaged: - - MY_SERVICE_API_KEY # Set the in-VM env var to "proxy-managed" + - service: my-service + apiKey: + name: MY_SERVICE_API_KEY # in-VM env var, set to a sentinel + inject: + - domain: api.example.com # inject on requests to this domain + header: Authorization # overwrite this header + format: "Bearer %s" + +caps: + network: + allow: + - api.example.com # the domain must also be reachable ``` The agent boots with `MY_SERVICE_API_KEY=proxy-managed`, sends a -request with that value in `Authorization`, and the proxy overwrites +request with that sentinel in `Authorization`, and the proxy overwrites the header with the real credential before forwarding. The real secret never enters the VM. See [Credentials](../security/credentials.md) for how to provide the credential value on your host, other approaches for cases the example -above doesn't fit, and what the proxy does at request time. +above doesn't fit, and what the proxy does at request time. To scope where +a kit-declared credential is sourced or which domains it's injected into, +see [Credential bindings](../security/credentials.md). ### Inject agent memory @@ -278,16 +277,17 @@ ruff-lint/ ``` ```yaml {title="ruff-lint/spec.yaml"} -schemaVersion: "1" +schemaVersion: "2" kind: mixin name: ruff-lint displayName: Ruff Linter description: Python linting with shared team config -network: - allowedDomains: - - pypi.org - - files.pythonhosted.org +caps: + network: + allow: + - pypi.org + - files.pythonhosted.org commands: install: @@ -340,7 +340,7 @@ is an abbreviated version of its spec, showing how the sandbox block combines with network, credentials, environment, and commands: ```yaml {title="claude/spec.yaml"} -schemaVersion: "1" +schemaVersion: "2" kind: sandbox name: claude sandbox: @@ -349,22 +349,22 @@ sandbox: entrypoint: run: [claude, "--dangerously-skip-permissions"] -network: - serviceDomains: - api.anthropic.com: anthropic - console.anthropic.com: anthropic - serviceAuth: - anthropic: - headerName: x-api-key - valueFormat: "%s" - allowedDomains: - - "claude.com:443" +caps: + network: + allow: + - "claude.com:443" credentials: - sources: - anthropic: - env: - - ANTHROPIC_API_KEY + - service: anthropic + apiKey: + name: ANTHROPIC_API_KEY + inject: + - domain: api.anthropic.com + header: x-api-key + format: "%s" + - domain: console.anthropic.com + header: x-api-key + format: "%s" environment: variables: @@ -519,9 +519,9 @@ and direct inspection inside the sandbox: value, such as `forward`, `forward-bypass`, `transparent`, or `browser-open`. Use it to diagnose install-time download failures, blocked domains, and unexpected TLS interception. If downloads fail or - arrive corrupted after you add `serviceDomains`, check whether the - service mapping is too broad. Map only the hosts that need credential - injection. + arrive corrupted after you add a credential's `apiKey.inject`, check + whether an injection domain is too broad. Inject only on the hosts that + need credentials. - `sbx exec -- ` runs an arbitrary command inside an existing sandbox. Useful for inspecting post-install state without recreating: `which mytool`, `ls /home/agent/.local/bin/`, From 7f063bdd10971a3be1c1fca97370a19c83bcf6ac Mon Sep 17 00:00:00 2001 From: David Karlsson <35727626+dvdksn@users.noreply.github.com> Date: Fri, 3 Jul 2026 12:11:06 +0200 Subject: [PATCH 3/6] docs(sandboxes): add credential bindings section Document the credential bindings model for third-party schemaVersion 2 kits: credentials.yaml format, first-run approval flow, which kits require a binding, and fail-closed mode. Built-in agents are authorized by provenance and never need a binding. Stacked on top of the credentials base changes in #25468. Co-Authored-By: Claude Sonnet 4.6 --- .../ai/sandboxes/security/credentials.md | 113 +++++++++++++++++- 1 file changed, 108 insertions(+), 5 deletions(-) diff --git a/content/manuals/ai/sandboxes/security/credentials.md b/content/manuals/ai/sandboxes/security/credentials.md index 89424f243934..a6ec97ccd668 100644 --- a/content/manuals/ai/sandboxes/security/credentials.md +++ b/content/manuals/ai/sandboxes/security/credentials.md @@ -29,6 +29,7 @@ value for the same service, the stored secret takes precedence. | [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 | | 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 | Required for third-party `schemaVersion: "2"` kits; 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 @@ -144,10 +145,24 @@ it into requests to the listed API domains. ### Services declared by kits -Custom kits can declare their own service identifiers in `spec.yaml` — -they're not limited to the table above. To provide a credential for a -kit-declared service, run `sbx secret set` with the same identifier the kit -declares under `credentials.sources`: +Custom kits can declare their own service identifiers in `spec.yaml`. In +`schemaVersion: "2"`, credentials are declared under the `credentials:` list: + +```yaml +credentials: + - service: my-service + apiKey: + name: MY_SERVICE_TOKEN + inject: + - domain: api.my-service.com + header: Authorization + format: "Bearer %s" +``` + +For a known provider (such as `anthropic` or `openai`), `- service: ` is +enough — injection config is filled in from the provider registry. To provide +the credential value, run `sbx secret set` with the same identifier the kit +declares: ```console $ sbx secret set -g my-service @@ -156,7 +171,7 @@ $ sbx secret set -g my-service There's no separate registration step; the keychain entry is keyed on the identifier the kit already uses. See [Authenticate to external services](../customize/kits.md#authenticate-to-external-services) -for the kit-side wiring. +for the full kit-side wiring. ### List and remove secrets @@ -256,6 +271,94 @@ proxy replaces it with the real value. The agent never sees the real secret. Prefer the [service-based flow](#stored-secrets) whenever it's an option — the kit handles the wiring; you only provide the value. +## 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. + +Third-party kits that declare `schemaVersion: "2"` require an authorizing +binding for each credential they use. `sbx` creates one interactively the first +time you run such a kit (see [First-run approval](#first-run-approval)); you can +also write entries by hand. Built-in agents are authorized by provenance and +never need a binding. + +Each entry under `bindings` is keyed by a +[service identifier](#built-in-services) and has two optional parts: + +- `discovery` — where to find the value: one or more environment variables, + or a file. Entries are tried in order. Omit `discovery` to use the value from + the [secret store](#stored-secrets). +- `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 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 + +When a third-party kit needs a credential that has no binding, `sbx` walks you +through creating one. You choose where the value comes from (the secret store, +an environment variable, or a file) and approve the domains it may reach. +`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 kit interactively once, or by writing `credentials.yaml` directly — before +running unattended. + +This makes the bindings file an allowlist of credential-to-domain approvals: a +kit can use only the credentials you've approved, only on the domains you've +approved. + +#### Which kits require a binding + +Only third-party kits that declare `schemaVersion: "2"` require a binding. +Built-in agents also use `schemaVersion: "2"` internally but are authorized by +provenance — their credentials inject automatically without a binding. Kits on +`schemaVersion: "1"` also inject without a binding. + +To extend the binding requirement to `schemaVersion: "1"` kits, turn on +fail-closed mode: + +```console +$ sbx settings set credentials.failClosed true +``` + +With fail-closed on, every injected credential — regardless of schema version — +requires an approved binding. + ## Registry credentials Registry credentials authenticate to private OCI registries when pulling From f7f658b28bbcc10011e4fb71607b26fb50dcc6e0 Mon Sep 17 00:00:00 2001 From: David Karlsson <35727626+dvdksn@users.noreply.github.com> Date: Fri, 3 Jul 2026 12:29:44 +0200 Subject: [PATCH 4/6] docs(sandboxes): correct credential bindings schema Rewrite the credential bindings section to match the shipped v0.9.0 binding schema. Bindings are store-only, per-mechanism approval records: each service entry approves apiKey and/or oauth, each with a domains list. Remove the obsolete discovery/env/file/allowedDomains model and the fail-closed setting, which don't exist. Correct the non-interactive behavior (warning + withheld credential, not a blocking error) and the first-run source choices (stored or entered, not env/file). Co-Authored-By: Claude Sonnet 4.6 --- .../ai/sandboxes/security/credentials.md | 99 +++++++------------ 1 file changed, 37 insertions(+), 62 deletions(-) diff --git a/content/manuals/ai/sandboxes/security/credentials.md b/content/manuals/ai/sandboxes/security/credentials.md index a6ec97ccd668..e57c01086197 100644 --- a/content/manuals/ai/sandboxes/security/credentials.md +++ b/content/manuals/ai/sandboxes/security/credentials.md @@ -29,7 +29,7 @@ value for the same service, the stored secret takes precedence. | [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 | | 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 | Required for third-party `schemaVersion: "2"` kits; also restricts which domains a credential reaches | +| [Credential bindings](#credential-bindings) (`credentials.yaml`) | Per-service mechanism and domain approval | Required for third-party `schemaVersion: "2"` kits; 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 @@ -273,91 +273,66 @@ the kit handles the wiring; you only provide the value. ## 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 +A credential bindings file records which credential mechanisms you've approved +for each service and the domains each may be used on. It lives at `~/.config/sbx/credentials.yaml`, or `%APPDATA%\sbx\credentials.yaml` on Windows. -Third-party kits that declare `schemaVersion: "2"` require an authorizing -binding for each credential they use. `sbx` creates one interactively the first -time you run such a kit (see [First-run approval](#first-run-approval)); you can -also write entries by hand. Built-in agents are authorized by provenance and -never need a binding. +Third-party kits that declare `schemaVersion: "2"` require an approved binding +for each credential they use. `sbx` creates one interactively the first time you +run such a kit (see [First-run approval](#first-run-approval)); you can also +write entries by hand. Built-in agents are authorized by provenance and never +need a binding. Each entry under `bindings` is keyed by a -[service identifier](#built-in-services) and has two optional parts: +[service identifier](#built-in-services) and approves one or both credential +mechanisms: -- `discovery` — where to find the value: one or more environment variables, - or a file. Entries are tried in order. Omit `discovery` to use the value from - the [secret store](#stored-secrets). -- `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. +- `apiKey` — approves injecting the service's stored API key. The value comes + from the [secret store](#stored-secrets) (`sbx secret set `); the + binding records approval, it doesn't hold or locate the value. +- `oauth` — approves the OAuth flow for the service. You sign in on the host, + and the proxy handles token refresh and routing. + +Each mechanism takes a `domains` list — the domains the proxy may inject the +credential into. The credential is attached only where those domains and the +ones the kit requests overlap. ```yaml bindings: anthropic: - discovery: - - env: [ANTHROPIC_API_KEY] - allowedDomains: [api.anthropic.com] + apiKey: + domains: [api.anthropic.com] github: - discovery: - - env: [GH_TOKEN, GITHUB_TOKEN] - allowedDomains: [api.github.com, github.com] + apiKey: + domains: [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 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] -``` +A binding is only an approval record: the presence of `apiKey` or `oauth` +authorizes that mechanism. Declining a credential writes no entry at all. ### First-run approval When a third-party kit needs a credential that has no binding, `sbx` walks you -through creating one. You choose where the value comes from (the secret store, -an environment variable, or a file) and approve the domains it may reach. -`sbx` writes the entry to `credentials.yaml`, and the same prompt appears in -the terminal and in the interactive TUI. +through approving one. For each credential, you use the value already in the +secret store or enter one at the prompt, and you approve the domains it may +reach. `sbx` writes the entry to `credentials.yaml`. 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 kit interactively once, or by writing `credentials.yaml` directly — before -running unattended. +prompt. `sbx` logs a warning naming the service and creates the sandbox anyway +with the credential withheld, so the agent starts unauthenticated. Pre-create +the binding — by running the kit interactively once, or by writing +`credentials.yaml` directly — before running unattended. This makes the bindings file an allowlist of credential-to-domain approvals: a -kit can use only the credentials you've approved, only on the domains you've -approved. +kit can use only the credentials and domains you've approved. -#### Which kits require a binding +### Which kits require a binding Only third-party kits that declare `schemaVersion: "2"` require a binding. -Built-in agents also use `schemaVersion: "2"` internally but are authorized by -provenance — their credentials inject automatically without a binding. Kits on -`schemaVersion: "1"` also inject without a binding. - -To extend the binding requirement to `schemaVersion: "1"` kits, turn on -fail-closed mode: - -```console -$ sbx settings set credentials.failClosed true -``` - -With fail-closed on, every injected credential — regardless of schema version — -requires an approved binding. +Built-in agents also use `schemaVersion: "2"` but are authorized by provenance, +so their credentials inject automatically without a binding. Kits on +`schemaVersion: "1"` inject their declared credentials without a binding. ## Registry credentials From aa13a02681cdd4afc2334dcb846c8eee0324ec8c Mon Sep 17 00:00:00 2001 From: David Karlsson <35727626+dvdksn@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:12:41 +0200 Subject: [PATCH 5/6] docs(sandboxes): correct kit-spec credential claims for v0.9.0 - Remove the "known providers auto-expand their injection config" claim. In v0.9.0 the service name alone supplies no apiKey.name/inject; every service must declare an apiKey (name + inject) or oauth block. The worked examples already do this. - Delete the file.parser section: its two cited entry points (a binding file source and credentials.sources) are both removed/legacy in v2. - Drop the "or provider default" injection-domain aside in the network section for the same reason. Co-Authored-By: Claude Sonnet 4.6 --- .../ai/sandboxes/customize/kit-reference.md | 53 ++----------------- .../ai/sandboxes/security/credentials.md | 7 ++- 2 files changed, 8 insertions(+), 52 deletions(-) diff --git a/content/manuals/ai/sandboxes/customize/kit-reference.md b/content/manuals/ai/sandboxes/customize/kit-reference.md index 17f7ed195b6b..0895e1b86ea9 100644 --- a/content/manuals/ai/sandboxes/customize/kit-reference.md +++ b/content/manuals/ai/sandboxes/customize/kit-reference.md @@ -142,21 +142,20 @@ auth mechanisms. | Field | Description | | ------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| `service` | Credential identifier. Known providers (`anthropic`, `github`, `openai`, `google`, ...) auto-expand to their `apiKey` injection config. | +| `service` | Credential identifier, matched against the value stored with `sbx secret set`. Lowercase kebab-case. | | `description` | Optional. Shown to the user when approving a [binding](../security/credentials.md#credential-bindings). | | `required` | If `true`, sandbox creation fails when the credential is unavailable. Default `false`. | | `apiKey` | API-key injection (see [apiKey](#apikey)). | | `oauth` | OAuth interception (see [oauth](#oauth)). | -For a known provider, `- service: anthropic` is enough — `apiKey.name` and -`inject` are filled in from the provider registry. Custom services must declare -`apiKey.name` and `apiKey.inject` (or `oauth`) themselves. +Each service must declare either an `apiKey` block (with `name` and `inject`) +or an `oauth` block; the service name alone doesn't supply injection config. ### `apiKey` | Field | Description | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `name` | Environment variable set inside the container. The agent sees a sentinel (`proxy-managed`); the proxy injects the real value. Auto-derived for known providers. | +| `name` | Environment variable name for the credential. The agent sees a sentinel (`proxy-managed`); the proxy injects the real value on matching requests. | | `inject[].domain` | Domain to inject the credential into. Must also be allowed in [`caps.network`](#network). | | `inject[].header` | HTTP header the proxy sets (for example, `x-api-key`, `Authorization`). | | `inject[].format` | Header value format, with one `%s` placeholder (for example, `"%s"` or `"Bearer %s"`). | @@ -175,53 +174,11 @@ the real token back in on outbound requests. The token never enters the sandbox. | `credentialFile.path` | Where to write the credential file inside the container (`~` expands). | | `credentialFile.template` | JSON template for that file. `{{.AccessToken}}`, `{{.RefreshToken}}`, `{{.ExpiresAt}}`, and `{{.ScopesJSON}}` are substituted at runtime. | -### file.parser - -A credential sourced from a file — through a -[credential binding](../security/credentials.md#credential-bindings) `file` -source, or a legacy `credentials.sources` entry — can pull its value from a JSON -field. Omit the parser for plain-text files; set `json:` to extract a -field from a JSON file. - -| Value | Behavior | -| ----------------- | ------------------------------------------------------------------------------------ | -| omitted or empty | Reads the entire file as the credential. Leading and trailing whitespace is trimmed. | -| `json:` | Parses the file as JSON and returns the value at the dot-separated path. | -| any other value | Rejected — `unsupported parser: `. | - -For `json:` paths, segments are separated by `.` (for example, `json:credentials.github.token`). -Only object keys can be navigated — arrays are not supported and there is no `[0]`-style indexing. -Keys that contain a literal `.` cannot be referenced. The resolved value must be a string, number, -or boolean; numbers and booleans are converted to strings. Objects, arrays, and null are rejected. - -For example, given this file: - -```json -{ - "credentials": { - "github": { "token": "ghp_xyz", "expires": "2026-12-31" } - } -} -``` - -`json:credentials.github.token` resolves to `ghp_xyz`. If the path doesn't -resolve — a missing field, or a value that isn't a string — the request fails -with one of the errors below. - -Common errors when using `json:` parsers: - -| Error message | Cause | -| --------------------------------------------- | ------------------------------------------------------------------- | -| `field 'X' not found in JSON` | The path doesn't exist in the file. | -| `cannot navigate to field 'X': not an object` | A path segment hit a string, array, or scalar instead of an object. | -| `field 'X' is not a string value` | The resolved value is an object, array, or null. | -| `failed to parse JSON: ...` | The file is not valid JSON. | - ## Network Network egress is declared under `caps.network`. Credentials no longer carry their own domain mapping — the proxy injects a credential only into the domains -its [`apiKey.inject`](#apikey) (or provider default) lists, and every domain the +its [`apiKey.inject`](#apikey) lists, and every domain the sandbox reaches must be allowed here. ```yaml diff --git a/content/manuals/ai/sandboxes/security/credentials.md b/content/manuals/ai/sandboxes/security/credentials.md index e57c01086197..f1277e09085d 100644 --- a/content/manuals/ai/sandboxes/security/credentials.md +++ b/content/manuals/ai/sandboxes/security/credentials.md @@ -159,10 +159,9 @@ credentials: format: "Bearer %s" ``` -For a known provider (such as `anthropic` or `openai`), `- service: ` is -enough — injection config is filled in from the provider registry. To provide -the credential value, run `sbx secret set` with the same identifier the kit -declares: +Each service declares an `apiKey` block (with `name` and `inject`) or an +`oauth` block. To provide the credential value, run `sbx secret set` with the +same identifier the kit declares: ```console $ sbx secret set -g my-service From ea5c18de34f293c1757662285c46d33ff45b33fb Mon Sep 17 00:00:00 2001 From: David Karlsson <35727626+dvdksn@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:28:11 +0200 Subject: [PATCH 6/6] docs(sandboxes): document proxyManaged/oauth fields, correct required MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified each field against runtime wiring in docker/sandboxes, not just the sbx-kits-contrib struct. - apiKey: document proxyManaged (controls the in-container sentinel env var); the proxy injects on the inject domains regardless. - oauth: add the wired resourceHosts, skipIfEnv, responseFields, and passthrough fields; add {{.Scopes}} to the template vars. Omit credentialFile.structure, which the engine ignores. - required: correct to advisory-only — it logs a warning but doesn't block sandbox creation (the struct doc's fail-fast claim is stale). - Schema versions table: map environment.proxyManaged to apiKey.name + proxyManaged: true, and add the publishedPorts and volumes-form renames. Co-Authored-By: Claude Sonnet 4.6 --- .../ai/sandboxes/customize/kit-reference.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/content/manuals/ai/sandboxes/customize/kit-reference.md b/content/manuals/ai/sandboxes/customize/kit-reference.md index 0895e1b86ea9..a90b1c6b2672 100644 --- a/content/manuals/ai/sandboxes/customize/kit-reference.md +++ b/content/manuals/ai/sandboxes/customize/kit-reference.md @@ -46,11 +46,13 @@ What changed in v2: | `credentials.sources.` | `credentials:` list entry with `service` | | `network.allowedDomains` / `deniedDomains` | `caps.network.allow` / `deny` | | `network.serviceDomains` / `serviceAuth` | `credentials[].apiKey.inject` | +| `network.publishedPorts` | top-level `publishedPorts` | | standalone `oauth:` block | `credentials[].oauth` | -| `environment.proxyManaged` | automatic — `credentials[].apiKey.name` | +| `environment.proxyManaged` | `credentials[].apiKey.name` + `proxyManaged: true` | | `memory` | `agentContext` | | `kind: agent` / `agent:` block | `kind: sandbox` / `sandbox:` block | | `tmpfs:` | `volumes:` entries with `type: tmpfs` | +| `volumes:` (mapping form) | `volumes:` sequence (`- path: `) | | `settings:` / `kitDir` / `persistence` | removed (no replacement) | Credential discovery also moved out of the kit in v2: a kit declares which @@ -144,7 +146,7 @@ auth mechanisms. | ------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `service` | Credential identifier, matched against the value stored with `sbx secret set`. Lowercase kebab-case. | | `description` | Optional. Shown to the user when approving a [binding](../security/credentials.md#credential-bindings). | -| `required` | If `true`, sandbox creation fails when the credential is unavailable. Default `false`. | +| `required` | If `true`, `sbx` logs a warning when the credential can't be resolved. It doesn't block sandbox creation — the sandbox starts without the credential. Default `false`. | | `apiKey` | API-key injection (see [apiKey](#apikey)). | | `oauth` | OAuth interception (see [oauth](#oauth)). | @@ -155,7 +157,8 @@ or an `oauth` block; the service name alone doesn't supply injection config. | Field | Description | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `name` | Environment variable name for the credential. The agent sees a sentinel (`proxy-managed`); the proxy injects the real value on matching requests. | +| `name` | Environment variable name for the credential (for example, `ANTHROPIC_API_KEY`). | +| `proxyManaged` | If `true`, `sbx` sets `name` inside the container to a sentinel (`proxy-managed`) so agents that expect the variable start cleanly. The proxy injects the real value on the `inject` domains whether or not this is set. Default `false`. | | `inject[].domain` | Domain to inject the credential into. Must also be allowed in [`caps.network`](#network). | | `inject[].header` | HTTP header the proxy sets (for example, `x-api-key`, `Authorization`). | | `inject[].format` | Header value format, with one `%s` placeholder (for example, `"%s"` or `"Bearer %s"`). | @@ -172,7 +175,11 @@ the real token back in on outbound requests. The token never enters the sandbox. | `tokenEndpoint.host` / `path` | The OAuth token endpoint the proxy intercepts. | | `sentinels.accessToken` / `refreshToken` | Sentinel values written into the container in place of the real tokens. | | `credentialFile.path` | Where to write the credential file inside the container (`~` expands). | -| `credentialFile.template` | JSON template for that file. `{{.AccessToken}}`, `{{.RefreshToken}}`, `{{.ExpiresAt}}`, and `{{.ScopesJSON}}` are substituted at runtime. | +| `credentialFile.template` | Go template for that file. `{{.AccessToken}}`, `{{.RefreshToken}}`, `{{.ExpiresAt}}`, `{{.Scopes}}`, and `{{.ScopesJSON}}` are substituted at runtime. | +| `resourceHosts` | API hosts where the proxy attaches the token on outbound requests, distinct from the token endpoint host. | +| `skipIfEnv` | Environment variable names that, if set on the host, make a stored API key take precedence over the OAuth flow. | +| `responseFields` | Overrides the default field names the proxy reads from the token response. | +| `passthrough` | If `true`, the proxy passes the token response through unchanged instead of replacing the tokens with sentinels. | ## Network