diff --git a/content/manuals/ai/sandboxes/agents/claude-code.md b/content/manuals/ai/sandboxes/agents/claude-code.md index 9b55656d769..13b4d097641 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 86e06cd78dd..ed7156434d5 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 e8f2c80e722..007a066572a 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 f6481665213..15d41517e03 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 de68d668f49..db8daf227d0 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 d62612100c3..18866041b5f 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 4bd194ca949..8dc0e68324b 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 23896acb460..cefad09337b 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 fab2621b172..ec8c8cbda86 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/customize/kit-examples.md b/content/manuals/ai/sandboxes/customize/kit-examples.md index 4f28e25d77a..30831e5b3b1 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 fd8d6cde247..a90b1c6b267 100644 --- a/content/manuals/ai/sandboxes/customize/kit-reference.md +++ b/content/manuals/ai/sandboxes/customize/kit-reference.md @@ -27,11 +27,50 @@ 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` | +| `network.publishedPorts` | top-level `publishedPorts` | +| standalone `oauth:` block | `credentials[].oauth` | +| `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 +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,137 +89,121 @@ 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`. | - -Service identifiers link credentials to [network rules](#network). - -### file.parser +`credentials` is a list; each entry names a `service` and configures one or more +auth mechanisms. + +| Field | Description | +| ------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| `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`, `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)). | + +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 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"`). | +| `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` | 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. | -`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. - -| 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. - -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" -``` +## Network -Nested JSON field, with an environment variable as fallback: +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) lists, and every domain the +sandbox reaches must be allowed here. ```yaml -credentials: - sources: - github: - env: - - GH_TOKEN - file: - path: "~/.config/myapp/creds.json" - parser: "json:credentials.github.token" - priority: file-first +caps: + network: + allow: [, ...] + deny: [, ...] ``` -Given `~/.config/myapp/creds.json`: - -```json -{ - "credentials": { - "github": { "token": "ghp_xyz", "expires": "2026-12-31" } - } -} -``` - -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. - -Common errors when using `json:` parsers: +| 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. | -| 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 - -```yaml -network: - allowedDomains: [, ...] - deniedDomains: [, ...] - serviceDomains: - : - serviceAuth: - : - headerName:
- valueFormat: -``` - -| 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"`). | +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 +211,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 0e66ef55a47..369114bcf4f 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/`, diff --git a/content/manuals/ai/sandboxes/get-started.md b/content/manuals/ai/sandboxes/get-started.md index 0c308899015..2af4ecf1e9c 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 170c95b70a2..f1277e09085 100644 --- a/content/manuals/ai/sandboxes/security/credentials.md +++ b/content/manuals/ai/sandboxes/security/credentials.md @@ -22,16 +22,15 @@ 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 | +| [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 credentials based on the API endpoint being called. See individual @@ -93,16 +92,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 -$ echo "$ANTHROPIC_API_KEY" | sbx secret set -g anthropic +$ 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 +$ 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,16 +140,28 @@ 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 -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" +``` + +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 @@ -136,7 +170,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 @@ -236,25 +270,68 @@ 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 +## Credential bindings + +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 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 approves one or both credential +mechanisms: + +- `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: + apiKey: + domains: [api.anthropic.com] + github: + apiKey: + domains: [api.github.com, github.com] +``` -As an alternative to stored secrets, export the relevant environment variable -in your shell before running a sandbox: +A binding is only an approval record: the presence of `apiKey` or `oauth` +authorizes that mechanism. Declining a credential writes no entry at all. -```console -$ export ANTHROPIC_API_KEY=sk-ant-api03-xxxxx -$ sbx run claude -``` +### First-run approval -The proxy reads the variable from your terminal session. See individual -[agent pages](../agents/) for the variable names each agent expects. +When a third-party kit needs a credential that has no binding, `sbx` walks you +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`. -> [!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). +In non-interactive contexts (CI or `--detached`), there's no one to answer the +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 and 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"` 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 @@ -348,10 +425,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 c84e3dafabc..f30fcbc2fed 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 }}" ```