diff --git a/content/manuals/ai/sandboxes/customize/kit-examples.md b/content/manuals/ai/sandboxes/customize/kit-examples.md index 4f28e25d77a8..c590574e0bfb 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 --- @@ -35,13 +35,13 @@ ruff-lint/ ``` ```yaml {title="ruff-lint/spec.yaml"} -schemaVersion: "1" +schemaVersion: "2" kind: mixin 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" @@ -117,13 +117,13 @@ 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 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" @@ -179,11 +179,11 @@ 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 -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] @@ -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 @@ -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 @@ -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) @@ -352,26 +352,29 @@ description: Claude Code without --dangerously-skip-permissions sandbox: image: "docker/sandbox-templates:claude-code-docker" - aiFilename: CLAUDE.md - 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" + entrypoint: [claude] + +agentInstructions: + filename: CLAUDE.md + +permissions: + network: + allow: + - "claude.com:443" + - "api.anthropic.com:443" + - "console.anthropic.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..c6ef300dca65 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 --- @@ -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 @@ -27,160 +31,322 @@ my-kit/ └── workspace/ ``` -## 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. - -### v0.32.0 - -The following `spec.yaml` fields were renamed: +## Schema versions + +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 +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` | `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 +controlled by the user through +[credential bindings](../security/credentials.md#credential-bindings). -| Previous | Current | -| -------------- | ---------------- | -| `memory` | `agentContext` | -| `kind: agent` | `kind: sandbox` | -| `agent:` block | `sandbox:` block | - -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. +> [!NOTE] +> `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`. ## Top-level fields ```yaml -schemaVersion: "1" +schemaVersion: "2" kind: name: +version: displayName: description: +sourceURL: +licenses: + - MIT +locked: + - sandbox.image +security: + privileged: false ``` -| 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"` 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 sections below apply to both kinds. Sandbox kits also declare a -[`sandbox:` block](#sandbox-block). +A kit also declares behavior blocks such as `agentInstructions`, +`permissions`, `ports`, `credentials`, `environment`, `setup`, and `volumes`. -## Credentials +## Kit kinds + +### `kind: mixin` + +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 -credentials: - sources: - : - env: [, ...] - file: - path: - parser: - priority: +schemaVersion: "2" +kind: mixin +name: github-tools +requires: + agent: claude ``` -| 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`. | +`requires.agent` takes one base-agent name. It is validated as a kit name and +enforced during composition. -Service identifiers link credentials to [network rules](#network). +### `kind: sandbox` -### file.parser +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: -`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. +```yaml +schemaVersion: "2" +kind: sandbox +name: claude-safe +extends: claude +sandbox: + entrypoint: [claude] +``` -| 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: `. | +`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. -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. +## Sandbox block -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. +```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`. | -Plain-text token file: +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 -credentials: - sources: - openai: - file: - path: "~/.openai/token" +agentInstructions: + filename: CLAUDE.md + content: | + Ruff is installed. Run `ruff check` before committing. ``` -Nested JSON field, with an environment variable as fallback: +| 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. | + +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 + +A kit declares the credentials it needs and how the proxy injects them into +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 credentials: - sources: - github: - env: - - GH_TOKEN - file: - path: "~/.config/myapp/creds.json" - parser: "json:credentials.github.token" - priority: file-first + - 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: # required with scheme: basic + oauth: + tokenEndpoint: + host: + path: + sentinels: + accessToken: + refreshToken: + credentialFile: + path: + structure: + service: + accessToken: "{{.AccessToken}}" ``` -Given `~/.config/myapp/creds.json`: +`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` | 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. + +### `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 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` + +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. 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 | +| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `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` | 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. | + +## Network + +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. -```json -{ - "credentials": { - "github": { "token": "ghp_xyz", "expires": "2026-12-31" } - } -} +```yaml +permissions: + network: + allow: [, ...] + deny: [, ...] ``` -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. +| 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. | -Common errors when using `json:` parsers: +Allow and deny patterns: -| 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. | +| 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 | -## Network +In v1 this was the `network:` block (`allowedDomains` / `deniedDomains`, plus +`serviceDomains` / `serviceAuth`). In v2, those fields are decode errors. + +## Ports + +Use `ports` for inbound service exposure from the sandbox to the host: ```yaml -network: - allowedDomains: [, ...] - deniedDomains: [, ...] - serviceDomains: - : - serviceAuth: - : - headerName:
- valueFormat: +ports: + - container: 8080 + protocol: tcp + name: web ``` -| 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 | +| ----------- | ------------------------------------------------------------------- | +| `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 @@ -188,20 +354,22 @@ 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_]*`). -## Commands +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. + +## Setup ```yaml -commands: +setup: install: - command: user: @@ -211,7 +379,7 @@ commands: user: background: description: - initFiles: + files: - path: content: mode: @@ -246,7 +414,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 @@ -256,7 +424,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. @@ -284,65 +452,25 @@ Parent directories are created automatically. Existing files are overwritten. Absolute paths and path-traversal sequences (`../../`) are rejected. -## Agent context - -```yaml -agentContext: | - -``` - -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`. +## Volumes ```yaml -sandbox: - image: - aiFilename: - entrypoint: - run: [, ...] - args: [, ...] +volumes: + - path: /workspace + size: 10g + mode: "0755" + - path: /tmp/scratch + type: tmpfs + size: 512m + mode: "1777" ``` -| 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 596ed605548c..23ee81c2d3af 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,16 +143,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 +permissions: + 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 +174,38 @@ 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 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 -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 + proxyManaged: true + inject: + - domain: api.example.com # inject on requests to this domain + header: Authorization # overwrite this header + format: "Bearer %s" + +permissions: + 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. See +[Credential bindings](../security/credentials.md) to approve the mechanisms +and domains declared by a third-party v2 kit. ### Inject agent memory @@ -214,31 +215,31 @@ 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. - -When more than one loaded kit declares an `agentContext:` block, 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` -section that points to each kit file: +Both mixin and sandbox kits can declare `agentInstructions.content`. The active +sandbox kit sets `agentInstructions.filename`, which determines the memory +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/ ├── 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 @@ -248,8 +249,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. @@ -278,18 +278,19 @@ 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 +permissions: + network: + allow: + - pypi.org + - files.pythonhosted.org -commands: +setup: install: - command: "uv tool install ruff@latest" user: "1000" @@ -339,40 +340,43 @@ 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: "1" +schemaVersion: "2" kind: sandbox name: claude sandbox: image: "docker/sandbox-templates:claude-code-docker" - aiFilename: CLAUDE.md - 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" + entrypoint: [claude, "--dangerously-skip-permissions"] + +agentInstructions: + filename: CLAUDE.md + +permissions: + network: + allow: + - "claude.com:443" + - "api.anthropic.com:443" + - "console.anthropic.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: IS_SANDBOX: "1" -commands: +setup: install: - command: "curl -fsSL https://claude.ai/install.sh | bash" user: "1000" @@ -513,8 +517,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 @@ -526,9 +530,9 @@ and direct inspection inside the sandbox: value, such as `forward`, `forward-bypass`, `transparent`, or `browser-open`. Use it to diagnose install-time download failures, blocked domains, and unexpected TLS interception. If downloads fail or - arrive corrupted after you add `serviceDomains`, check whether the - service mapping is too broad. Map only the hosts that need credential - injection. + arrive corrupted after you add a credential's `apiKey.inject`, check + whether an injection domain is too broad. Inject only on the hosts that + need credentials. - `sbx exec -- ` runs an arbitrary command inside an existing sandbox. Useful for inspecting post-install state without recreating: `which mytool`, `ls /home/agent/.local/bin/`, diff --git a/content/manuals/ai/sandboxes/security/credentials.md b/content/manuals/ai/sandboxes/security/credentials.md index 595d75fadd77..8f7f8e9a31c3 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. @@ -29,6 +33,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 +149,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 + proxyManaged: true + inject: + - domain: api.my-service.com + scheme: bearer +``` + +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 @@ -156,7 +175,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 +275,74 @@ 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 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. 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 +mechanisms: + +- `apiKey` — approves injecting the service's stored API key. The value comes + from the [secret store](#stored-secrets) (`sbx secret set `); the + binding records approval, it doesn't hold or locate the value. +- `oauth` — approves the OAuth flow for the service. You sign in on the host, + and the proxy handles token refresh and routing. OAuth domains include the + token endpoint host and any resource hosts declared by the kit. + +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: + anthropic: + apiKey: + domains: [api.anthropic.com] + github: + apiKey: + domains: [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 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. 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. + +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 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 Registry credentials authenticate to private OCI registries when pulling