From 412137962c62608d5c5543bd457ab9a063e9e988 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 1/8] 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 596ed605548c..333392e4d1e4 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: @@ -342,7 +342,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: @@ -351,22 +351,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: @@ -526,9 +526,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 7958557c9b0f2bd52e1267ce7e84c27548cfe619 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 2/8] 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 595d75fadd77..28ebc904e5cd 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, Cursor, or Droid | +| [Credential bindings](#credential-bindings) (`credentials.yaml`) | Per-service mechanism and domain approval | Required for third-party `schemaVersion: "2"` kits | | [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 8649bbfa01b946f3b0e534b4730f57575e4af1a8 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 3/8] 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 | 97 +++++++------------ 1 file changed, 36 insertions(+), 61 deletions(-) diff --git a/content/manuals/ai/sandboxes/security/credentials.md b/content/manuals/ai/sandboxes/security/credentials.md index 28ebc904e5cd..b9fac69f9d8a 100644 --- a/content/manuals/ai/sandboxes/security/credentials.md +++ b/content/manuals/ai/sandboxes/security/credentials.md @@ -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 f94560d1a0023b0d90a74434695c0768753fddc0 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 4/8] 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 b9fac69f9d8a..cacb5b23b53d 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 4b87761052b086b957f31f1190380037338a1d33 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 5/8] 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 From c3f5cb26a686d21c53cc6b7c6a0d74b2fccc8bd2 Mon Sep 17 00:00:00 2001 From: David Karlsson <35727626+dvdksn@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:06:04 +0000 Subject: [PATCH 6/8] docs(sandboxes): align kit spec v2 docs with grammar Update the kit spec v2 draft to match the sbx-kits-contrib spec-v2 grammar branch and PR #150. Use the current v2 field names for permissions, setup, agentInstructions, ports, requires, extends image inheritance, credential schemes, OAuth structure, and required credential behavior. --- .../ai/sandboxes/customize/kit-examples.md | 45 +- .../ai/sandboxes/customize/kit-reference.md | 407 +++++++++++------- .../manuals/ai/sandboxes/customize/kits.md | 69 +-- .../ai/sandboxes/security/credentials.md | 23 +- 4 files changed, 323 insertions(+), 221 deletions(-) diff --git a/content/manuals/ai/sandboxes/customize/kit-examples.md b/content/manuals/ai/sandboxes/customize/kit-examples.md index 30831e5b3b15..0e1c94cd4649 100644 --- a/content/manuals/ai/sandboxes/customize/kit-examples.md +++ b/content/manuals/ai/sandboxes/customize/kit-examples.md @@ -1,7 +1,7 @@ --- title: Kit examples linkTitle: Examples -description: Copy-and-adapt spec.yaml snippets for common mixin and sandbox kit patterns — static files, install commands, shell customization, background services, initFiles, Claude Code skills, and agent forks. +description: Copy-and-adapt spec.yaml snippets for common mixin and sandbox kit patterns — static files, install commands, shell customization, background services, setup files, Claude Code skills, and agent forks. keywords: sandboxes, sbx, kits, mixins, examples, patterns, skills weight: 25 --- @@ -41,7 +41,7 @@ name: ruff-lint displayName: Ruff description: Python linting with shared team config -commands: +setup: install: - command: "uv tool install ruff@latest" user: "1000" @@ -56,12 +56,12 @@ select = ["E", "F", "I"] ## Install a tool at sandbox creation -`commands.install` runs once per sandbox, at creation time. It's where +`setup.install` runs once per sandbox, at creation time. It's where anything that needs to land in the image goes — package managers (`apt-get`, `pip`, `npm`), binary downloads, or vendor install scripts. ```yaml -commands: +setup: install: - command: "apt-get update && apt-get install -y jq" - command: "curl -fsSL https://example.com/install.sh | sh" @@ -93,7 +93,7 @@ instance, needs `zip` and `unzip`, so add an > failed fetch fails the step: > > ```yaml -> commands: +> setup: > install: > - command: "curl -fsSL https://example.com/install.sh -o /tmp/install.sh && bash /tmp/install.sh" > user: "1000" @@ -123,7 +123,7 @@ name: nvm displayName: nvm description: Node version manager available in every shell -commands: +setup: install: - command: "curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash" user: "1000" @@ -183,7 +183,7 @@ schemaVersion: "2" kind: mixin name: internal-ca -commands: +setup: install: - command: "install -m 0644 /home/agent/internal-ca.crt /usr/local/share/ca-certificates/internal-ca.crt && update-ca-certificates" user: "0" @@ -196,13 +196,13 @@ certificates without further configuration. ## Run a background service -`commands.startup` runs on every sandbox start. To keep a long-running +`setup.startup` runs on every sandbox start. To keep a long-running service such as a dev server or daemon alive, set `background: true`. The sandbox runs the command in the background and replays startup commands on each start, so the service comes back after a stop/start cycle: ```yaml -commands: +setup: startup: - command: ["my-service", "--port", "8080"] user: "1000" @@ -215,7 +215,7 @@ for debugging, wrap the command in a shell and redirect to a log file. Let trailing `&` yourself: ```yaml -commands: +setup: startup: - command: - sh @@ -228,16 +228,16 @@ commands: An empty log file tells you the wrapper ran; a populated one tells you why the service failed. -## Bake runtime values into a file with initFiles +## Bake runtime values into a file with setup files When a config file needs a value that isn't known until sandbox start -— most often the absolute workspace path — use `commands.initFiles`. +— most often the absolute workspace path — use `setup.files`. The `${WORKDIR}` placeholder expands to the primary workspace path when the file is written. ```yaml -commands: - initFiles: +setup: + files: - path: /home/agent/.local/bin/start-code-server.sh content: | exec code-server --bind-addr 0.0.0.0:8080 --auth none "${WORKDIR}" @@ -253,7 +253,7 @@ commands: `mode: "0755"` makes the generated file executable so the startup command can invoke it directly. -Use `initFiles` instead of a static file whenever the content depends +Use `setup.files` instead of a static file whenever the content depends on a runtime value. Use a static file otherwise. > [!TIP] @@ -313,12 +313,12 @@ for details. Sandboxes seed settings files for some built-in agents during setup. For example, the sandbox writes `/home/agent/.claude/settings.json` for the `claude` agent. This happens after the kit's static files and -`initFiles`, so a kit can't override those paths with either mechanism. -Use `commands.startup` instead, which runs after the sandbox seeds its +`setup.files`, so a kit can't override those paths with either mechanism. +Use `setup.startup` instead, which runs after the sandbox seeds its files: ```yaml -commands: +setup: startup: - command: - sh @@ -352,11 +352,12 @@ description: Claude Code without --dangerously-skip-permissions sandbox: image: "docker/sandbox-templates:claude-code-docker" - aiFilename: CLAUDE.md - entrypoint: - run: [claude] + entrypoint: [claude] -caps: +agentInstructions: + filename: CLAUDE.md + +permissions: network: allow: - "claude.com:443" diff --git a/content/manuals/ai/sandboxes/customize/kit-reference.md b/content/manuals/ai/sandboxes/customize/kit-reference.md index a90b1c6b2672..72562ad189d1 100644 --- a/content/manuals/ai/sandboxes/customize/kit-reference.md +++ b/content/manuals/ai/sandboxes/customize/kit-reference.md @@ -1,7 +1,7 @@ --- title: Kit spec reference linkTitle: Spec reference -description: Field-by-field reference for a kit's spec.yaml — credentials, network rules, environment, commands, files, agent context, and the sandbox block. +description: Field-by-field reference for a kit's spec.yaml, including credentials, network rules, environment, setup, files, agent instructions, and the sandbox block. keywords: sandboxes, sbx, kits, spec.yaml, reference, schema, fields weight: 22 --- @@ -30,30 +30,32 @@ my-kit/ ## 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. +recommended; `"1"` is still accepted through the legacy path. -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. +The loader forks on `schemaVersion`. A v2 spec uses the v2 grammar only. Legacy +v1 fields in a `schemaVersion: "2"` spec are rejected during decode instead of +being folded into the v2 model. Keep each `spec.yaml` on one grammar. 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) | +| v1 | v2 | +| ------------------------------------------- | ---------------------------------------- | +| `credentials.sources.` | `credentials:` list entry with `service` | +| `network.allowedDomains` / `deniedDomains` | `permissions.network.allow` / `deny` | +| `network.serviceDomains` / `serviceAuth` | `credentials[].apiKey.inject` | +| `network.publishedPorts` / `publishedPorts` | top-level `ports` | +| standalone `oauth:` block | `credentials[].oauth` | +| `environment.proxyManaged` | `credentials[].apiKey.proxyManaged` | +| `memory` / `agentContext` | `agentInstructions.content` | +| `kind: agent` / `agent:` block | `kind: sandbox` / `sandbox:` block | +| `sandbox.aiFilename` | `agentInstructions.filename` | +| `sandbox.entrypoint.run` | `sandbox.entrypoint` | +| `sandbox.entrypoint.args` | `sandbox.command.default` | +| `sandbox.entrypoint.ttyArgs` | `sandbox.command.interactive` | +| `tmpfs:` | `volumes:` entries with `type: tmpfs` | +| `volumes:` (mapping form) | `volumes:` sequence (`- path: `) | +| `commands:` / `commands.initFiles` | `setup:` / `setup.files` | +| `settings:` / `kitDir` / `persistence` | Removed | 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 @@ -61,51 +63,144 @@ 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. +> `mixins` and `sandbox.build` are accepted by the parser, but runtime support +> is pending. A kit that sets `sandbox.build` must also set `sandbox.image`. -## Changelog +## Top-level fields -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. For the full -v1-to-v2 field mapping, see [Schema versions](#schema-versions). +```yaml +schemaVersion: "2" +kind: +name: +version: +displayName: +description: +sourceURL: +licenses: + - MIT +locked: + - sandbox.image +security: + privileged: false +``` -### v0.32.0 +| Field | Required | Description | +| --------------- | -------- | ----------------------------------------------------------------------------------------------- | +| `schemaVersion` | Yes | Spec schema version. Use `"2"` for this grammar. | +| `kind` | Yes | `mixin` for kits that extend an agent; `sandbox` for kits that define one. | +| `name` | Yes | Unique identifier. Lowercase alphanumeric with hyphens, 1 to 64 characters. | +| `version` | No | Kit version. | +| `displayName` | No | Human-readable name. | +| `description` | No | Short description. | +| `sourceURL` | No | Source repository or documentation URL. | +| `licenses` | No | SPDX license identifiers. | +| `locked` | No | Dotted paths child kits may not override. | +| `security` | No | Container security settings. `security.privileged: true` runs the container in privileged mode. | -The following `spec.yaml` fields were renamed: +A kit also declares behavior blocks such as `agentInstructions`, +`permissions`, `ports`, `credentials`, `environment`, `setup`, and `volumes`. -| Previous | Current | -| -------------- | ---------------- | -| `memory` | `agentContext` | -| `kind: agent` | `kind: sandbox` | -| `agent:` block | `sandbox:` block | +## Kit kinds -The per-kit directory was also renamed from `kits-memory/` to -`kits-agent-context/`. An existing `kits-memory/` directory is migrated -automatically the next time the sandbox starts. +### `kind: mixin` -## Top-level fields +A mixin layers capabilities onto an existing sandbox. It must not declare a +`sandbox:` block, `extends:`, or `mixins:`. A mixin can declare `requires:` to +pin the base agent it is designed for: ```yaml schemaVersion: "2" -kind: -name: -displayName: -description: +kind: mixin +name: github-tools +requires: + agent: claude +``` + +`requires.agent` takes one base-agent name. It is validated as a kit name and +enforced during composition. + +### `kind: sandbox` + +A sandbox kit defines a full agent. A root sandbox must declare a `sandbox:` +block. A sandbox that uses `extends:` can inherit the parent image and omit its +own `sandbox:` block: + +```yaml +schemaVersion: "2" +kind: sandbox +name: claude-safe +extends: claude +sandbox: + entrypoint: [claude] +``` + +`extends:` is sandbox-only. The parent must resolve to a sandbox kit. `mixins:` +is also sandbox-only and accepted by the parser, but runtime composition support +is pending. + +## Sandbox block + +```yaml +sandbox: + image: + build: + context: . + dockerfile: Dockerfile + args: + AGENT_VERSION: "1.0.0" + target: runtime + platforms: + - linux/amd64 + entrypoint: [my-agent, "--flag"] + command: + default: ["--task-mode"] + interactive: [] + resources: + cpu: 2 + memory: 4g + gpu: "1" +``` + +| Field | Required | Description | +| -------------------- | -------- | --------------------------------------------------------------------------------------------------------------- | +| `sandbox.image` | Yes | Docker image reference. A sandbox that sets `extends:` can inherit this from its parent. | +| `sandbox.build` | No | Build configuration. Runtime support is pending, so a kit with `build:` must also set `image:`. | +| `sandbox.entrypoint` | No | Fixed process prefix as a string array. The first element is the agent binary. | +| `sandbox.command` | No | Mode-specific argument tail. Use a list shorthand for `default`, or a mapping with `default` and `interactive`. | +| `sandbox.resources` | No | Optional CPU, memory, and GPU constraints. Memory uses byte-size strings such as `4096m` or `4g`. | + +The effective command is `entrypoint` plus `command.default` for non-interactive +launches, and `entrypoint` plus `command.interactive` for TTY sessions. If +`interactive` is omitted, it falls back to `default`. + +The agent's container image must provide: + +- A non-root `agent` user at UID 1000 with passwordless sudo. +- A `/home/agent/` home directory owned by `agent`. +- HTTP proxy environment variables (`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`) preserved across sudo. +- The agent binary, either baked in or installed with [`setup.install`](#setup). + +Build on top of `docker/sandbox-templates:shell-docker` to get these base +requirements. + +## Agent instructions + +```yaml +agentInstructions: + filename: CLAUDE.md + content: | + Ruff is installed. Run `ruff check` before committing. ``` -| 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. | +| Field | Description | +| ---------- | --------------------------------------------------------------------------------------------------- | +| `filename` | AI profile filename. Meaningful for `kind: sandbox`; ignored with a warning for `kind: mixin`. | +| `content` | Markdown instructions. For a sandbox, inlined into the profile. For a mixin, written to kit memory. | -The sections below apply to both kinds. Sandbox kits also declare a -[`sandbox:` block](#sandbox-block). +For mixins, the engine writes `content` to +`/kits-memory/.md` and adds a `## Kits` pointer +section to the base AI file. This keeps each mixin's instructions in a separate +file. ## Credentials @@ -120,12 +215,18 @@ credentials: - service: description: # optional required: # optional, default false + provider: # optional, reserved apiKey: name: + proxyManaged: true inject: - domain: header:
format: + - domain: + scheme: bearer + - domain: + scheme: basic username: # optional, for HTTP basic auth oauth: tokenEndpoint: @@ -136,33 +237,37 @@ credentials: refreshToken: credentialFile: path: - template: + structure: + service: + accessToken: "{{.AccessToken}}" ``` `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)). | +| 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`, the resolver fails fast when the credential has no binding and no host fallback. Default `false`. | +| `provider` | Reserved for a provider registry. Accepted with a warning and no runtime effect. | +| `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. +Each service must declare `apiKey`, `oauth`, or both. When both resolve at +runtime, the API key takes precedence and OAuth acts as the fallback. ### `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). | +| Field | Description | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | Environment variable name for the credential (for example, `ANTHROPIC_API_KEY`). | +| `proxyManaged` | If `true`, `sbx` sets `name` inside the container to the `proxy-managed` sentinel. Default `false`. | +| `inject[].domain` | Domain to inject the credential into. Must also be allowed in [`permissions.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"`). Mutually exclusive with `scheme`. | +| `inject[].scheme` | Shorthand for common auth schemes. `bearer` expands to `Authorization: Bearer %s`; `basic` requires `username`. Mutually exclusive with `format`. | +| `inject[].username` | Username for HTTP Basic auth, for example `x-access-token` for Git over HTTPS. | ### `oauth` @@ -170,40 +275,71 @@ 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. | +| 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.structure` | Declarative JSON shape for the credential file. `{{.AccessToken}}`, `{{.RefreshToken}}`, `{{.ExpiresAt}}`, and `{{.Scopes}}` are substituted at runtime. | +| `credentialFile.template` | Deprecated Go template form. If both `structure` and `template` are set, `structure` wins. | +| `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 -Network egress is declared under `caps.network`. Credentials no longer carry +Network egress is declared under `permissions.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 -caps: +permissions: network: allow: [, ...] deny: [, ...] ``` -| 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. | +| Field | Description | +| --------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `permissions.network.allow` | Domains the sandbox can reach. | +| `permissions.network.deny` | Domains the sandbox is blocked from reaching. Deny takes precedence over allow, including across composed kits. | + +Allow and deny patterns: + +| Pattern | Example | Status | +| --------------------- | ------------------------ | --------------------------- | +| Exact host | `api.example.com` | Enforced | +| Exact host and port | `api.example.com:8080` | Enforced | +| Single-label wildcard | `*.example.com` | Enforced | +| Multi-label wildcard | `**.example.com` | Parsed; enforcement pending | +| Port range | `api.example.com:80-443` | Parsed; enforcement pending | +| Port wildcard | `api.example.com:*` | Parsed; enforcement pending | +| CIDR | `10.0.0.0/8` | Parsed; enforcement pending | 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). +`serviceDomains` / `serviceAuth`). In v2, those fields are decode errors. + +## Ports + +Use `ports` for inbound service exposure from the sandbox to the host: + +```yaml +ports: + - container: 8080 + protocol: tcp + name: web +``` + +| Field | Description | +| ----------- | ------------------------------------------------------------------- | +| `container` | Container port, 1 to 65535. | +| `protocol` | `tcp` or `udp`. Empty means `tcp`. | +| `name` | Optional label surfaced by tools that list published port bindings. | + +Host ports are allocated ephemerally on `127.0.0.1`. Users can pin host ports +with `sbx ports --publish :`. ## Environment @@ -219,15 +355,14 @@ environment: 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. +Do not set `DASH_`, `SBX_`, or `DOCKER_` variables, and avoid overriding +`HOME`, `USER`, `SHELL`, `PATH`, `LD_PRELOAD`, and `LD_LIBRARY_PATH`. The +runtime reserves these names and may override them. -## Commands +## Setup ```yaml -commands: +setup: install: - command: user: @@ -237,7 +372,7 @@ commands: user: background: description: - initFiles: + files: - path: content: mode: @@ -272,7 +407,7 @@ attaches, with no terminal connected, so they can't prompt the user don't gate the agent's entrypoint: the agent launches once startup commands have been dispatched, regardless of `background`. Use them for non-interactive prep — launching daemons, warming caches, -refreshing config — and use `commands.initFiles` for any value that +refreshing config — and use `setup.files` for any value that needs to land on disk before the agent runs. Startup commands must be idempotent. They run on every sandbox start @@ -282,7 +417,7 @@ work with existence checks, use upserts instead of inserts, and prefer commands that converge to the same end state regardless of how many times they run. -### initFiles +### files Files written at sandbox start, with runtime substitution. @@ -310,65 +445,25 @@ Parent directories are created automatically. Existing files are overwritten. Absolute paths and path-traversal sequences (`../../`) are rejected. -## Agent context +## Volumes ```yaml -agentContext: | - +volumes: + - path: /workspace + size: 10g + mode: "0755" + - path: /tmp/scratch + type: tmpfs + size: 512m + mode: "1777" ``` -Top-level field. Available in both mixin and sandbox kits. Markdown -appended to the agent's memory file at sandbox creation. The agent reads -this content at startup. Write it as instructions or notes the agent -should follow when working in the sandbox. Applied only when the active -sandbox kit sets [`sandbox.aiFilename`](#sandbox-block). - -The file is written to the parent of the workspace path inside the -sandbox, not to the workspace itself. For a workspace mounted at -`/Users/you/myproject`, the memory file lands at -`/Users/you/AGENTS.md` (or whatever `aiFilename` is set to). It exists -only inside the sandbox. Nothing is written to the host. - -When several loaded kits declare `agentContext:` blocks, the content is split -across files instead of being concatenated into the main one: - -- Each kit's agent context is written to `.md` in a sibling - `kits-agent-context/` directory next to the main memory file. -- The main memory file gets a `## Kits` section listing every kit with - a pointer to its file. The section is delimited by - `` and `` - markers so it can be regenerated when kits are added or removed. - -## Sandbox block - -Required for `kind: sandbox`. - -```yaml -sandbox: - image: - aiFilename: - entrypoint: - run: [, ...] - args: [, ...] -``` - -| Field | Required | Description | -| ------------------------- | -------- | ----------------------------------------------------------------------------------------------------------- | -| `sandbox.image` | Yes | Docker image reference. See [Base image requirements](#base-image-requirements). | -| `sandbox.aiFilename` | No | Memory filename (for example, `AGENTS.md`). Appends top-level [`agentContext`](#agent-context) at creation. | -| `sandbox.entrypoint.run` | No | Command and args as a string array. Replaces the image's entrypoint. | -| `sandbox.entrypoint.args` | No | Args appended to the image's existing entrypoint. | - -### Base image requirements - -The agent's container image must provide: - -- A non-root `agent` user at UID 1000 with passwordless sudo. -- A `/home/agent/` home directory owned by `agent`. -- HTTP proxy environment variables (`HTTP_PROXY`, `HTTPS_PROXY`, - `NO_PROXY`) preserved across sudo. -- The agent binary (baked in, or installed via - [`commands.install`](#commands)). +| Field | Description | +| ------ | ------------------------------------------------------------------- | +| `path` | Required absolute container path. | +| `type` | Empty for a block-backed volume, or `tmpfs` for RAM-backed storage. | +| `size` | Optional byte-size string. | +| `mode` | Optional octal permissions. | -Build on top of `docker/sandbox-templates:shell-docker` to get these for -free. +Volumes are applied only when a sandbox is created. `sbx kit add` cannot attach +volumes to a running container. diff --git a/content/manuals/ai/sandboxes/customize/kits.md b/content/manuals/ai/sandboxes/customize/kits.md index 333392e4d1e4..d5f73aeb39c9 100644 --- a/content/manuals/ai/sandboxes/customize/kits.md +++ b/content/manuals/ai/sandboxes/customize/kits.md @@ -48,7 +48,7 @@ Install commands are the place to put anything an agent needs into the image, via `apt`, `pip`, `npm`, `curl | bash`, or whatever fits: ```yaml -commands: +setup: install: - command: "apt-get update && apt-get install -y jq" ``` @@ -58,7 +58,7 @@ warming caches, or refreshing config on each start. They must be idempotent — see the [`startup`](kit-reference.md#startup) spec reference: ```yaml -commands: +setup: startup: - command: ["my-daemon"] background: true @@ -67,7 +67,7 @@ commands: ### Inject files Kits can inject files into the sandbox in two ways: **static files** bundled -with the kit, and **`initFiles`** written at startup with runtime values +with the kit, and **`setup.files`** written at startup with runtime values substituted in. Static files work well for content that doesn't vary between sandboxes, such @@ -84,28 +84,29 @@ my-kit/ └── .editorconfig ``` -`initFiles` cover content that depends on runtime values, such as an +`setup.files` cover content that depends on runtime values, such as an absolute workspace path that a tool needs to bake into its config file at startup: ```yaml -commands: - initFiles: +setup: + files: - path: /home/agent/.my-tool/config.json content: '{"workspace": "${WORKDIR}"}' onlyIfMissing: true ``` -See [`initFiles`](kit-reference.md#initfiles) in the spec reference for all fields. +See [`setup.files`](kit-reference.md#files) in the spec reference for all +fields. Sandboxes seed settings files for some built-in agents during setup. For example, the sandbox writes `/home/agent/.claude/settings.json` for the `claude` agent. This happens after the kit's static files and -`initFiles`, so kit-injected files at those paths get overwritten. +`setup.files`, so kit-injected files at those paths get overwritten. Workspace files (such as `/.claude/settings.local.json`) aren't affected, and you can ship them under `files/workspace/` as usual. To override a path the sandbox writes to, use a -[`commands.startup`](kit-reference.md#startup) script instead. See +[`setup.startup`](kit-reference.md#startup) script instead. See [Override agent settings](kit-examples.md#override-agent-settings) for an example. @@ -142,7 +143,7 @@ Network rules define which domains the sandbox can reach or block. Kit network rules apply only to sandboxes that use the kit: ```yaml -caps: +permissions: network: allow: - api.example.com @@ -188,7 +189,7 @@ credentials: header: Authorization # overwrite this header format: "Bearer %s" -caps: +permissions: network: allow: - api.example.com # the domain must also be reachable @@ -213,31 +214,33 @@ the agent project conventions, usage tips for a tool the kit installs, or other guidance that should be in scope when the sandbox runs. ```yaml -agentContext: | - Ruff is installed. Run `ruff check` before committing. - Shared config lives at `/workspace/ruff.toml`. +agentInstructions: + content: | + Ruff is installed. Run `ruff check` before committing. + Shared config lives at `/workspace/ruff.toml`. ``` -Both mixin and sandbox kits can declare `agentContext:`. The content is written -only when the active sandbox kit sets [`sandbox.aiFilename`](kit-reference.md#sandbox-block), -which determines the memory file's name. +Both mixin and sandbox kits can declare `agentInstructions.content`. The active +sandbox kit sets `agentInstructions.filename`, which determines the memory +file's name. -When more than one loaded kit declares an `agentContext:` block, each kit's +When more than one loaded kit declares `agentInstructions.content`, each kit's content is written to its own `.md` file under a sibling -`kits-agent-context/` directory. The main memory file gets a `## Kits` +`kits-memory/` directory. The main memory file gets a `## Kits` section that points to each kit file: ```text /Users/you/ ├── myproject/ # workspace ├── AGENTS.md # main memory file with a "## Kits" index -└── kits-agent-context/ +└── kits-memory/ ├── ruff-lint.md ├── vale.md └── git-ssh-sign.md ``` -See [`agentContext`](kit-reference.md#agent-context) in the spec reference for the full field schema. +See [`agentInstructions`](kit-reference.md#agent-instructions) in the spec +reference for the full field schema. ### Define an agent @@ -247,8 +250,7 @@ the command the user attaches to when they launch the sandbox: ```yaml sandbox: image: "my-registry/my-agent:latest" - entrypoint: - run: [my-agent, "--yolo"] + entrypoint: [my-agent, "--yolo"] ``` See [Sandbox kits](#sandbox-kits) for use cases and an example. @@ -283,13 +285,13 @@ name: ruff-lint displayName: Ruff Linter description: Python linting with shared team config -caps: +permissions: network: allow: - pypi.org - files.pythonhosted.org -commands: +setup: install: - command: "uv tool install ruff@latest" user: "1000" @@ -339,7 +341,7 @@ agent. For a step-by-step walkthrough, see The `claude` agent you get from `sbx run claude` is defined as a kit. Here is an abbreviated version of its spec, showing how the sandbox block combines -with network, credentials, environment, and commands: +with network, credentials, environment, and setup: ```yaml {title="claude/spec.yaml"} schemaVersion: "2" @@ -347,11 +349,12 @@ kind: sandbox name: claude sandbox: image: "docker/sandbox-templates:claude-code-docker" - aiFilename: CLAUDE.md - entrypoint: - run: [claude, "--dangerously-skip-permissions"] + entrypoint: [claude, "--dangerously-skip-permissions"] -caps: +agentInstructions: + filename: CLAUDE.md + +permissions: network: allow: - "claude.com:443" @@ -372,7 +375,7 @@ environment: variables: IS_SANDBOX: "1" -commands: +setup: install: - command: "curl -fsSL https://claude.ai/install.sh | bash" user: "1000" @@ -513,8 +516,8 @@ Docker credential store, so pushing to a private registry requires a prior ## Spec reference For a field-by-field reference of every `spec.yaml` block — top-level -fields, credentials, network, environment, commands, static files, -agent context, and the sandbox block — see [Kit spec reference](kit-reference.md). +fields, credentials, network, environment, setup, static files, +agent instructions, and the sandbox block — see [Kit spec reference](kit-reference.md). ## Debugging diff --git a/content/manuals/ai/sandboxes/security/credentials.md b/content/manuals/ai/sandboxes/security/credentials.md index cacb5b23b53d..ebf4570616ee 100644 --- a/content/manuals/ai/sandboxes/security/credentials.md +++ b/content/manuals/ai/sandboxes/security/credentials.md @@ -153,15 +153,16 @@ credentials: - service: my-service apiKey: name: MY_SERVICE_TOKEN + proxyManaged: true inject: - domain: api.my-service.com - header: Authorization - format: "Bearer %s" + scheme: bearer ``` -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: +Each service declares `apiKey`, `oauth`, or both. When both resolve at runtime, +the API key takes precedence and OAuth acts as the fallback. To provide the +credential value, run `sbx secret set` with the same identifier the kit +declares: ```console $ sbx secret set -g my-service @@ -291,7 +292,8 @@ mechanisms: 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. + and the proxy handles token refresh and routing. OAuth domains include the + token endpoint host and any resource hosts declared by the kit. 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 @@ -318,10 +320,11 @@ 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. `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. +prompt. If the kit marks the credential as `required: true`, resolution fails +fast when no binding or host fallback exists. For optional credentials, the +sandbox can start with that credential withheld. 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. From 697152d02e4c1fff80104f486b21865f92e5572e Mon Sep 17 00:00:00 2001 From: David Karlsson <35727626+dvdksn@users.noreply.github.com> Date: Thu, 16 Jul 2026 13:58:17 +0000 Subject: [PATCH 7/8] docs(sandboxes): sync kit v2 docs with merged spec The merged v2 grammar clarified release timing, OAuth passthrough, proxy-managed API keys, and agent instruction placement. Document version 0.36 support, link the normative spec, and correct the affected examples and field requirements. --- .../ai/sandboxes/customize/kit-examples.md | 2 ++ .../ai/sandboxes/customize/kit-reference.md | 15 +++++++++++---- content/manuals/ai/sandboxes/customize/kits.md | 13 +++++++------ .../manuals/ai/sandboxes/security/credentials.md | 14 +++++++++----- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/content/manuals/ai/sandboxes/customize/kit-examples.md b/content/manuals/ai/sandboxes/customize/kit-examples.md index 0e1c94cd4649..c590574e0bfb 100644 --- a/content/manuals/ai/sandboxes/customize/kit-examples.md +++ b/content/manuals/ai/sandboxes/customize/kit-examples.md @@ -361,6 +361,8 @@ permissions: network: allow: - "claude.com:443" + - "api.anthropic.com:443" + - "console.anthropic.com:443" credentials: - service: anthropic diff --git a/content/manuals/ai/sandboxes/customize/kit-reference.md b/content/manuals/ai/sandboxes/customize/kit-reference.md index 72562ad189d1..92c634313172 100644 --- a/content/manuals/ai/sandboxes/customize/kit-reference.md +++ b/content/manuals/ai/sandboxes/customize/kit-reference.md @@ -17,6 +17,10 @@ weight: 22 This page documents every field in a kit's `spec.yaml`. For an overview of what kits are and how to use them, see [Kits](kits.md). +For the normative v2 grammar used by the parser and tests, see the +[`schemaVersion: "2"` specification](https://github.com/docker/sbx-kits-contrib/blob/main/spec/SPEC-v2.md) +in the `docker/sbx-kits-contrib` repository. + A kit directory has a required `spec.yaml` and an optional `files/` tree: ```text @@ -29,8 +33,9 @@ my-kit/ ## Schema versions -Two schema versions are supported. `schemaVersion: "2"` is current and -recommended; `"1"` is still accepted through the legacy path. +Starting with Docker Sandboxes version 0.36, two schema versions are supported. +Use `schemaVersion: "2"` for new kits. Version `"1"` remains accepted through +the legacy path. The loader forks on `schemaVersion`. A v2 spec uses the v2 grammar only. Legacy v1 fields in a `schemaVersion: "2"` spec are rejected during decode instead of @@ -227,7 +232,7 @@ credentials: scheme: bearer - domain: scheme: basic - username: # optional, for HTTP basic auth + username: # required with scheme: basic oauth: tokenEndpoint: host: @@ -273,7 +278,9 @@ runtime, the API key takes precedence and OAuth acts as the fallback. 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. +the real token back in on outbound requests. By default, the token never enters +the sandbox. Setting `passthrough: true` opts out of sentinel masking and sends +the real token response into the sandbox. | Field | Description | | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/content/manuals/ai/sandboxes/customize/kits.md b/content/manuals/ai/sandboxes/customize/kits.md index d5f73aeb39c9..036bebe52855 100644 --- a/content/manuals/ai/sandboxes/customize/kits.md +++ b/content/manuals/ai/sandboxes/customize/kits.md @@ -184,6 +184,7 @@ credentials: - service: my-service apiKey: name: MY_SERVICE_API_KEY # in-VM env var, set to a sentinel + proxyManaged: true inject: - domain: api.example.com # inject on requests to this domain header: Authorization # overwrite this header @@ -222,12 +223,10 @@ agentInstructions: Both mixin and sandbox kits can declare `agentInstructions.content`. The active sandbox kit sets `agentInstructions.filename`, which determines the memory -file's name. - -When more than one loaded kit declares `agentInstructions.content`, each kit's -content is written to its own `.md` file under a sibling -`kits-memory/` directory. The main memory file gets a `## Kits` -section that points to each kit file: +file's name. The sandbox kit's content is written inline in that file. Each +mixin's content is written to its own `.md` file under a sibling +`kits-memory/` directory, and the main memory file gets a `## Kits` section that +points to each mixin file: ```text /Users/you/ @@ -358,6 +357,8 @@ permissions: network: allow: - "claude.com:443" + - "api.anthropic.com:443" + - "console.anthropic.com:443" credentials: - service: anthropic diff --git a/content/manuals/ai/sandboxes/security/credentials.md b/content/manuals/ai/sandboxes/security/credentials.md index ebf4570616ee..d0baac1070ac 100644 --- a/content/manuals/ai/sandboxes/security/credentials.md +++ b/content/manuals/ai/sandboxes/security/credentials.md @@ -8,8 +8,8 @@ keywords: docker sandboxes, credentials, api keys, authentication, proxy, ssh ag Most agents need an API key for their model provider. An HTTP/HTTPS proxy on your host intercepts outbound requests from the sandbox, looks up the matching credential on the host, and overwrites the auth header before forwarding. The -real credential stays on the host; the sandbox sees only a sentinel value. For -the security model behind this, see +real credential stays on the host when proxy management is active; the sandbox +sees only a sentinel value. For the security model behind this, see [Credential isolation](isolation.md#credential-isolation). ## How credential injection works @@ -17,9 +17,13 @@ the security model behind this, see When a sandbox makes an outbound request, the host-side proxy decides three things: whether the request **matches** a service the kit (or built-in agent) declares, what **header** to write, and what **value** to inject. The kit -declares the match and the header; you provide the value on the host. The real -value never enters the sandbox — the agent sees only a sentinel like -`proxy-managed`. +declares the match and the header; you provide the value on the host. For +proxy-managed credentials, the real value never enters the sandbox — the agent +sees only a sentinel like `proxy-managed`. + +A kit can set OAuth `passthrough: true` to opt out of sentinel masking. This +sends the real token response into the sandbox and reduces credential isolation. +See the [`oauth` kit fields](../customize/kit-reference.md#oauth). 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. From 68a69a8b88ce99fea37057707fdd03141b21501e Mon Sep 17 00:00:00 2001 From: David Karlsson <35727626+dvdksn@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:07:39 +0000 Subject: [PATCH 8/8] docs(sandboxes): correct kit v2 credential bindings --- .../ai/sandboxes/customize/kit-reference.md | 24 +++++------ .../manuals/ai/sandboxes/customize/kits.md | 12 +++--- .../ai/sandboxes/security/credentials.md | 43 ++++++++++--------- 3 files changed, 41 insertions(+), 38 deletions(-) diff --git a/content/manuals/ai/sandboxes/customize/kit-reference.md b/content/manuals/ai/sandboxes/customize/kit-reference.md index 92c634313172..c6ef300dca65 100644 --- a/content/manuals/ai/sandboxes/customize/kit-reference.md +++ b/content/manuals/ai/sandboxes/customize/kit-reference.md @@ -210,9 +210,9 @@ file. ## 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 +outbound requests. It does not declare a host discovery source. The user +provides the value through the secret store or the first-run prompt, and a +[credential binding](../security/credentials.md) authorizes its use. A kit can't read arbitrary host environment variables or files. ```yaml @@ -250,14 +250,14 @@ credentials: `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`, the resolver fails fast when the credential has no binding and no host fallback. Default `false`. | -| `provider` | Reserved for a provider registry. Accepted with a warning and no runtime effect. | -| `apiKey` | API-key injection (see [apiKey](#apikey)). | -| `oauth` | OAuth interception (see [oauth](#oauth)). | +| 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` | Marks the credential as essential to the agent. If it has no binding, `sbx` warns and starts with the credential withheld. Default `false`. | +| `provider` | Reserved for a provider registry. Accepted with a warning and no runtime effect. | +| `apiKey` | API-key injection (see [apiKey](#apikey)). | +| `oauth` | OAuth interception (see [oauth](#oauth)). | Each service must declare `apiKey`, `oauth`, or both. When both resolve at runtime, the API key takes precedence and OAuth acts as the fallback. @@ -290,7 +290,7 @@ the real token response into the sandbox. | `credentialFile.structure` | Declarative JSON shape for the credential file. `{{.AccessToken}}`, `{{.RefreshToken}}`, `{{.ExpiresAt}}`, and `{{.Scopes}}` are substituted at runtime. | | `credentialFile.template` | Deprecated Go template form. If both `structure` and `template` are set, `structure` wins. | | `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. | +| `skipIfEnv` | Accepted for compatibility, but ignored for schema v2. A v2 binding is authoritative instead of host environment variables. | | `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. | diff --git a/content/manuals/ai/sandboxes/customize/kits.md b/content/manuals/ai/sandboxes/customize/kits.md index 036bebe52855..23ee81c2d3af 100644 --- a/content/manuals/ai/sandboxes/customize/kits.md +++ b/content/manuals/ai/sandboxes/customize/kits.md @@ -175,9 +175,9 @@ the proxy reads the real credential on the host and overwrites the auth header before the request leaves the sandbox. 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): +to inject the credential. It doesn't declare a host discovery source. The user +provides the value through the secret store or first-run prompt, and a +[credential binding](../security/credentials.md) authorizes its use: ```yaml credentials: @@ -203,9 +203,9 @@ 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. To scope where -a kit-declared credential is sourced or which domains it's injected into, -see [Credential bindings](../security/credentials.md). +above doesn't fit, and what the proxy does at request time. See +[Credential bindings](../security/credentials.md) to approve the mechanisms +and domains declared by a third-party v2 kit. ### Inject agent memory diff --git a/content/manuals/ai/sandboxes/security/credentials.md b/content/manuals/ai/sandboxes/security/credentials.md index d0baac1070ac..8f7f8e9a31c3 100644 --- a/content/manuals/ai/sandboxes/security/credentials.md +++ b/content/manuals/ai/sandboxes/security/credentials.md @@ -277,16 +277,16 @@ the kit handles the wiring; you only provide the value. ## 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 +A credential bindings file records which credential mechanisms and domains +you've approved for each service. 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. +write entries by hand. Credentials declared only by embedded, built-in kits are +authorized by provenance and don't need a binding. Each entry under `bindings` is keyed by a [service identifier](#built-in-services) and approves one or both credential @@ -299,9 +299,9 @@ mechanisms: and the proxy handles token refresh and routing. OAuth domains include the token endpoint host and any resource hosts declared by the kit. -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. +Each mechanism takes a `domains` list that records the domains you approved. +`sbx` asks for approval when a kit requests domains that the existing binding +doesn't cover. ```yaml bindings: @@ -319,26 +319,29 @@ 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 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`. +through approving one. For an API key, you can use a value already in the secret +store or enter one at the prompt. For OAuth, you approve the sign-in flow. In +both cases, you approve the domains declared by the kit. `sbx` writes the entry +to `credentials.yaml`. In non-interactive contexts (CI or `--detached`), there's no one to answer the -prompt. If the kit marks the credential as `required: true`, resolution fails -fast when no binding or host fallback exists. For optional credentials, the -sandbox can start with that credential withheld. Pre-create the binding — by -running the kit interactively once, or by writing `credentials.yaml` directly — -before running unattended. +prompt. Without a binding, the sandbox starts with the credential withheld. If +the kit marks the credential as `required: true`, `sbx` also prints a warning. +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. +The bindings file gates whether a third-party v2 kit can use a service +credential. The kit's credential injection rules and network permissions still +constrain which requests can carry the credential. ### 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. +Built-in agents also use `schemaVersion: "2"`, but credentials declared only by +embedded kits are authorized by provenance and inject automatically. If a +third-party kit also declares the same service, that service requires approval. +Kits on `schemaVersion: "1"` inject their declared credentials without a +binding. ## Registry credentials