docs: Updated documentation to reflect the new v2 sbx kit spec changes
+7
-6
@@ -898,16 +898,17 @@ A custom agent that uses `httpie` to call an internal API:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# <coyote-config-dir>/agents/internal-api-agent/sbx-mixin.yaml
|
# <coyote-config-dir>/agents/internal-api-agent/sbx-mixin.yaml
|
||||||
schemaVersion: "1"
|
schemaVersion: "2"
|
||||||
kind: mixin
|
kind: mixin
|
||||||
name: agent-internal-api
|
name: agent-internal-api
|
||||||
description: Installs httpie and allows access to our internal API host
|
description: Installs httpie and allows access to our internal API host
|
||||||
|
|
||||||
network:
|
permissions:
|
||||||
allowedDomains:
|
network:
|
||||||
- "internal-api.example.com:443"
|
allow:
|
||||||
|
- "internal-api.example.com"
|
||||||
|
|
||||||
commands:
|
setup:
|
||||||
install:
|
install:
|
||||||
- command: |
|
- command: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
@@ -931,4 +932,4 @@ path table and the [official sbx mixin reference](https://docs.docker.com/ai/san
|
|||||||
installs/domains they bring. Not a bug, just visibility.
|
installs/domains they bring. Not a bug, just visibility.
|
||||||
- **The `sql` agent's `usql` install is in the base kit, not a per-agent mixin.** Same for `pandoc` (used by
|
- **The `sql` agent's `usql` install is in the base kit, not a per-agent mixin.** Same for `pandoc` (used by
|
||||||
`fetch_url_via_curl`). Both ship with every Coyote sandbox automatically. See `assets/sbx-kit/spec.yaml` for the
|
`fetch_url_via_curl`). Both ship with every Coyote sandbox automatically. See `assets/sbx-kit/spec.yaml` for the
|
||||||
full base prereq list.
|
full base prereq list.
|
||||||
+9
-7
@@ -61,16 +61,17 @@ Both are auto-discovered by Coyote on every `coyote --sandbox`. No flags or regi
|
|||||||
`sbx-mixin.yaml`:
|
`sbx-mixin.yaml`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
schemaVersion: "1"
|
schemaVersion: "2"
|
||||||
kind: mixin
|
kind: mixin
|
||||||
name: my-httpie-tool
|
name: my-httpie-tool
|
||||||
description: Adds httpie + access to example-api.com for the my-httpie-tool custom tool
|
description: Adds httpie + access to example-api.com for the my-httpie-tool custom tool
|
||||||
|
|
||||||
network:
|
permissions:
|
||||||
allowedDomains:
|
network:
|
||||||
- "example-api.com:443"
|
allow:
|
||||||
|
- "example-api.com"
|
||||||
|
|
||||||
commands:
|
setup:
|
||||||
install:
|
install:
|
||||||
- command: |
|
- command: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
@@ -86,7 +87,8 @@ path table and the [official sbx mixin reference](https://docs.docker.com/ai/san
|
|||||||
|
|
||||||
- **Custom tools that shell out to host-installed binaries fail in the sandbox** unless those binaries are also
|
- **Custom tools that shell out to host-installed binaries fail in the sandbox** unless those binaries are also
|
||||||
installed by the tool's mixin (or a global mixin, or the base kit).
|
installed by the tool's mixin (or a global mixin, or the base kit).
|
||||||
- **Custom tools that hit external APIs fail on DNS resolution** if the domain isn't in any `allowedDomains` block.
|
- **Custom tools that hit external APIs fail on DNS resolution** if the domain isn't in any network allow list
|
||||||
|
(`permissions.network.allow`; v1: `network.allowedDomains`).
|
||||||
The sandbox proxy denies all unlisted traffic.
|
The sandbox proxy denies all unlisted traffic.
|
||||||
- **Mixin install steps run as UID 1000 (the `agent` user) with passwordless sudo.** Don't assume root home paths;
|
- **Mixin install steps run as UID 1000 (the `agent` user) with passwordless sudo.** Don't assume root home paths;
|
||||||
use `~/` or explicit `/home/agent/` if you need a known location.
|
use `~/` or explicit `/home/agent/` if you need a known location.
|
||||||
@@ -394,4 +396,4 @@ export function run(name: string): string {
|
|||||||
```
|
```
|
||||||
|
|
||||||
This is useful for pinning a specific Python version, using an alternative TypeScript runtime like
|
This is useful for pinning a specific Python version, using an alternative TypeScript runtime like
|
||||||
[Bun](https://bun.sh/) or [Deno](https://deno.com/), or working with virtual environments.
|
[Bun](https://bun.sh/) or [Deno](https://deno.com/), or working with virtual environments.
|
||||||
+22
-13
@@ -436,25 +436,32 @@ For a full example configuration for an agent, see the [Agent Configuration Exam
|
|||||||
|
|
||||||
# Sandbox Compatibility
|
# Sandbox Compatibility
|
||||||
|
|
||||||
If you run Coyote inside a [Sandbox](Sandboxes), MCP servers often need extra setup beyond what works on your
|
If you run Coyote inside a [Sandbox](Sandboxes), the sandbox's network proxy denies all unlisted domains. Two
|
||||||
host because the sandbox's network proxy denies all unlisted domains.
|
mechanisms cover most MCP needs automatically:
|
||||||
|
|
||||||
The bundled `assets/functions/sbx-mixin.yaml` (installed via `coyote --install functions`) already allowlists the
|
- **Remote servers are auto-allowed.** At launch, Coyote generates a `coyote-mcp` mixin that adds every remote
|
||||||
defaults shipped in the built-in `mcp.json`: github MCP (`api.githubcopilot.com`), atlassian MCP (`mcp.atlassian.com`),
|
server `url` from your `mcp.json` to the sandbox's network allow list and declares your MCP secrets as sbx
|
||||||
ddg-search MCP (`duckduckgo.com` + subdomains), plus the npm registry and Docker registries that npx/uvx-based MCP
|
credentials, which are proxy-injected into request headers where possible, exposed as `COYOTE_SECRET_<NAME>`
|
||||||
servers pull from.
|
env vars otherwise. Any number of secrets per server is supported, and your `mcp.json` works unmodified inside
|
||||||
|
the sandbox. See [Sandboxes: The generated coyote-mcp mixin](Sandboxes#the-generated-coyote-mcp-mixin).
|
||||||
|
- **The bundled `functions/sbx-mixin.yaml`** (installed via `coyote --install functions`) allowlists what the
|
||||||
|
built-in `mcp.json` needs beyond its URLs: the npm registry and Docker registries that npx/uvx/docker-based
|
||||||
|
MCP servers pull from, the ddg-search endpoints, plus the github (`api.githubcopilot.com`) and atlassian
|
||||||
|
(`mcp.atlassian.com`) MCP hosts.
|
||||||
|
|
||||||
For your own MCP servers, add the relevant domains to a user-level `sbx-mixin.yaml`. Brief example:
|
You only need your own mixin for domains Coyote can't derive from `mcp.json`: OAuth authorization hosts,
|
||||||
|
registries for custom containerized servers, or URLs hidden inside `args` (e.g. `mcp-remote <url>`). Brief example:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# ~/.config/coyote/sbx-mixin.yaml
|
# ~/.config/coyote/sbx-mixin.yaml
|
||||||
schemaVersion: "1"
|
schemaVersion: "2"
|
||||||
kind: mixin
|
kind: mixin
|
||||||
name: my-mcp-domains
|
name: my-mcp-domains
|
||||||
network:
|
permissions:
|
||||||
allowedDomains:
|
network:
|
||||||
- "api.example-mcp.com:443"
|
allow:
|
||||||
- "auth.example-mcp.com:443"
|
- "auth.example-mcp.com"
|
||||||
|
- "sso.example-corp.com"
|
||||||
```
|
```
|
||||||
|
|
||||||
See [Sandboxes: Extending the Sandbox](Sandboxes#extending-the-sandbox-auto-discovered-mixins) for the full discovery
|
See [Sandboxes: Extending the Sandbox](Sandboxes#extending-the-sandbox-auto-discovered-mixins) for the full discovery
|
||||||
@@ -462,7 +469,9 @@ path table.
|
|||||||
|
|
||||||
## Sandbox caveats
|
## Sandbox caveats
|
||||||
|
|
||||||
- **Containerized MCP servers (`docker run -i ...`) require their image registry to be in `allowedDomains`.** The
|
- **Servers whose URL lives in `args` are not auto-allowed** (e.g. `npx mcp-remote https://…`). Coyote only derives
|
||||||
|
egress from each server's `url` field; add such domains to a user mixin.
|
||||||
|
- **Containerized MCP servers (`docker run -i ...`) require their image registry in the network allow list.** The
|
||||||
sandbox's nested Docker daemon needs to pull the image. Declare `ghcr.io`, `registry-1.docker.io`, and
|
sandbox's nested Docker daemon needs to pull the image. Declare `ghcr.io`, `registry-1.docker.io`, and
|
||||||
`auth.docker.io` (or whichever registry you pull from) explicitly. The built-in `functions/sbx-mixin.yaml` already
|
`auth.docker.io` (or whichever registry you pull from) explicitly. The built-in `functions/sbx-mixin.yaml` already
|
||||||
covers these for the defaults.
|
covers these for the defaults.
|
||||||
|
|||||||
+123
-24
@@ -19,7 +19,7 @@ coyote --sandbox
|
|||||||
# Or with an explicit name
|
# Or with an explicit name
|
||||||
coyote --sandbox my-project
|
coyote --sandbox my-project
|
||||||
|
|
||||||
# Start fresh — no copied config or MCP secrets; LLM credentials still injected via proxy
|
# Start fresh with no copied config, MCP secrets, or generated MCP mixin; LLM credentials still injected via proxy
|
||||||
coyote --sandbox throwaway --fresh
|
coyote --sandbox throwaway --fresh
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -71,24 +71,30 @@ coyote --info | grep -i "sbx_kit_dir" | awk '{print $2}'
|
|||||||
# 4. Inject your LLM API credentials into the sbx global secret store (host-side).
|
# 4. Inject your LLM API credentials into the sbx global secret store (host-side).
|
||||||
# Coyote reads your host config, finds any {{SECRET_NAME}} placeholder in your
|
# Coyote reads your host config, finds any {{SECRET_NAME}} placeholder in your
|
||||||
# client's api_key, decrypts it from your host vault, and registers it:
|
# client's api_key, decrypts it from your host vault, and registers it:
|
||||||
sbx secret set -g <llm-provider> # e.g. anthropic, openai, google
|
sbx secret set <llm-provider> # e.g. anthropic, openai, gemini
|
||||||
# (skipped silently if already registered; see "Updating a registered secret")
|
# (skipped silently if already registered; see "Updating a registered secret")
|
||||||
|
|
||||||
# 5. Inject MCP secrets into the sbx global secret store (host-side, skipped if --fresh).
|
# 5. Inject MCP secrets and generate the coyote-mcp mixin (host-side, skipped if --fresh).
|
||||||
# Coyote scans your mcp.json for {{SECRET_NAME}} placeholders anywhere in each server
|
# Coyote scans your mcp.json for {{SECRET_NAME}} placeholders anywhere in each server
|
||||||
# config (env, headers, or any nested field), decrypts on the host, and registers each:
|
# config (env, headers, args, URLs, every occurrence of every secret; any number of
|
||||||
sbx secret set -g <mcp-service>
|
# secrets per server), decrypts each distinct secret on the host, and registers it
|
||||||
|
# under its kebab-cased secret name:
|
||||||
|
sbx secret set <secret-name> # e.g. {{GITHUB_PAT}} registers as github-pat
|
||||||
# (each secret skipped silently if already registered)
|
# (each secret skipped silently if already registered)
|
||||||
|
# Coyote then renders a "coyote-mcp" mixin kit that declares each credential
|
||||||
|
# and allows egress to every remote MCP server. See "The generated coyote-mcp mixin".
|
||||||
|
|
||||||
# 6. Check if a sandbox with this name already exists
|
# 6. Check if a sandbox with this name already exists
|
||||||
sbx ls
|
sbx ls
|
||||||
|
|
||||||
# 7. If not, create it with the base kit + every discovered mixin layered on
|
# 7. If not, create it with the base kit + every discovered mixin + the generated
|
||||||
|
# coyote-mcp mixin layered on
|
||||||
sbx create \
|
sbx create \
|
||||||
--name <NAME> \
|
--name <NAME> \
|
||||||
--kit <cache>/sbx-kit/ \
|
--kit <cache>/sbx-kit/ \
|
||||||
--kit <user-mixin-1> \
|
--kit <user-mixin-1> \
|
||||||
--kit <user-mixin-N> \
|
--kit <user-mixin-N> \
|
||||||
|
--kit <cache>/sbx-mixin-kits/<hash>/ \
|
||||||
coyote .
|
coyote .
|
||||||
|
|
||||||
# 8. Copy your host config into the sandbox (skipped if --fresh). vault.yml is
|
# 8. Copy your host config into the sandbox (skipped if --fresh). vault.yml is
|
||||||
@@ -145,10 +151,16 @@ Run `sbx --help` for the full surface. None of these are reimplemented in Coyote
|
|||||||
|
|
||||||
## Credentials & Secrets
|
## Credentials & Secrets
|
||||||
|
|
||||||
The vault is **not used inside a sandbox**. Instead, Coyote injects your LLM API credentials and MCP secrets directly
|
The vault is **not used inside a sandbox**. Instead, Coyote registers your LLM API credentials and MCP secrets with
|
||||||
into the sbx global secret store **on your host** before the sandbox starts. Inside the sandbox, the sbx network proxy
|
the sbx secret store **on your host** before the sandbox starts. Inside the sandbox, each credential is supplied one
|
||||||
intercepts outbound HTTP requests and supplies the registered credentials automatically. Your API provider receives the
|
of two ways:
|
||||||
correct key without Coyote ever decrypting anything inside the VM.
|
|
||||||
|
- **Proxy-injected.** The sbx network proxy intercepts outbound HTTPS requests and writes the real credential into
|
||||||
|
the request header at the network edge. The environment inside the VM only ever holds the literal string
|
||||||
|
`proxy-managed`. Used for LLM providers, and for any MCP secret whose every occurrence is a request-header value.
|
||||||
|
- **Environment-injected.** Secrets that can't be expressed as a proxy header rule are exposed inside the sandbox as
|
||||||
|
`COYOTE_SECRET_<NAME>` environment variables, and Coyote resolves their `{{SECRET_NAME}}` placeholders from those
|
||||||
|
at startup. Either way, your `mcp.json` works unmodified inside the sandbox.
|
||||||
|
|
||||||
### How injection works
|
### How injection works
|
||||||
|
|
||||||
@@ -156,8 +168,11 @@ Before `sbx create`, Coyote:
|
|||||||
|
|
||||||
1. Reads your host `config.yaml` and locates any `{{SECRET_NAME}}` placeholder in your configured client's `api_key` field.
|
1. Reads your host `config.yaml` and locates any `{{SECRET_NAME}}` placeholder in your configured client's `api_key` field.
|
||||||
2. Decrypts that secret from your host vault.
|
2. Decrypts that secret from your host vault.
|
||||||
3. Registers it globally with `sbx secret set -g <service>` (e.g. `anthropic`, `openai`, `google`).
|
3. Registers it with `sbx secret set <service>` (e.g. `anthropic`, `openai`, `gemini`).
|
||||||
4. Unless `--fresh` is passed, repeats the same process for every `{{SECRET_NAME}}` placeholder found anywhere in your `mcp.json` server configurations (including `env`, `headers`, and any other nested fields).
|
4. Unless `--fresh` is passed, repeats the same process for **every** `{{SECRET_NAME}}` placeholder found anywhere in
|
||||||
|
your `mcp.json` server configurations; i.e. `env`, `headers`, `args`, URLs, any nested field. There is no limit on the
|
||||||
|
number of secrets per server, and one secret may be shared by several servers. Each **distinct secret** is
|
||||||
|
registered once, under its kebab-cased secret name: `{{GITHUB_PAT}}` registers as `github-pat`.
|
||||||
|
|
||||||
Secrets already registered with `sbx` are silently skipped to keep re-attaches fast. See
|
Secrets already registered with `sbx` are silently skipped to keep re-attaches fast. See
|
||||||
[Updating a registered secret](#updating-a-registered-secret) if you've rotated a credential.
|
[Updating a registered secret](#updating-a-registered-secret) if you've rotated a credential.
|
||||||
@@ -165,6 +180,41 @@ Secrets already registered with `sbx` are silently skipped to keep re-attaches f
|
|||||||
If your LLM client uses **OAuth** instead of an API key (`auth: oauth` in your config), no secret is injected. The
|
If your LLM client uses **OAuth** instead of an API key (`auth: oauth` in your config), no secret is injected. The
|
||||||
sbx proxy handles OAuth natively without a stored key.
|
sbx proxy handles OAuth natively without a stored key.
|
||||||
|
|
||||||
|
### The generated `coyote-mcp` mixin
|
||||||
|
|
||||||
|
Alongside secret registration, every non-`--fresh` launch renders a mixin kit named `coyote-mcp` and
|
||||||
|
passes it to `sbx create` as an extra `--kit`. It carries two things:
|
||||||
|
|
||||||
|
- **Network egress for your MCP servers.** Every remote server `url` in `mcp.json` contributes an entry to the
|
||||||
|
sandbox's network allow list (HTTPS on the default port as a bare host, anything else as `host:port`). You don't
|
||||||
|
need to hand-maintain a mixin just to reach the MCP servers you've already configured.
|
||||||
|
- **One credential declaration per distinct secret.** Each entry names the sbx service (`github-pat`), the
|
||||||
|
in-sandbox env var (`COYOTE_SECRET_GITHUB_PAT`), and, when the secret qualifies, the proxy inject rules that
|
||||||
|
let sbx write the real value into request headers at the network edge (`proxyManaged: true`).
|
||||||
|
|
||||||
|
When sbx asks you to approve credential bindings on the first interactive run, each entry carries a description
|
||||||
|
naming the vault secret and the MCP server(s) that use it, so you can tell exactly what you're approving.
|
||||||
|
|
||||||
|
A secret is proxy-managed only when **every** occurrence of it across all servers is a single-placeholder HTTPS
|
||||||
|
header value. It falls back to environment injection (`COYOTE_SECRET_<NAME>` holds the real value) when any of
|
||||||
|
these apply:
|
||||||
|
|
||||||
|
- It appears anywhere other than a request header: `env`, `args`, a URL, or any nested field.
|
||||||
|
- Its header value holds more than one placeholder (e.g. `Basic {{USER}}:{{PASS}}`) or a literal `%`.
|
||||||
|
- The server URL isn't HTTPS. (Non-default ports are fine: the inject rule targets `host:port`, mirroring the allow-list format.)
|
||||||
|
- **Two different secrets target the same header on the same domain**; e.g. two `mcp.json` entries pointing at
|
||||||
|
the same host with different `Authorization` bearer tokens. The proxy injects per domain + header and cannot
|
||||||
|
tell which credential a given request needs, so Coyote demotes *all* the conflicting secrets to environment
|
||||||
|
injection and warns on stderr at launch. Different secrets in different headers on one domain are fine and stay
|
||||||
|
proxy-managed.
|
||||||
|
|
||||||
|
Demoted or not, every server's domain stays in the generated allow list, so connectivity is never affected, and only
|
||||||
|
*where* the secret value lives changes.
|
||||||
|
|
||||||
|
> **Not covered:** servers whose URL is buried in `args` rather than the `url` field (e.g. `npx mcp-remote
|
||||||
|
> https://…`) contribute no allow entry, because Coyote only derives egress from `url`. Add those domains to a
|
||||||
|
> [user mixin](#extending-the-sandbox-auto-discovered-mixins).
|
||||||
|
|
||||||
### First-run wizard inside the sandbox
|
### First-run wizard inside the sandbox
|
||||||
|
|
||||||
If the sandbox has no Coyote config (e.g. after `--fresh` or on a brand-new sandbox), Coyote runs a lightweight
|
If the sandbox has no Coyote config (e.g. after `--fresh` or on a brand-new sandbox), Coyote runs a lightweight
|
||||||
@@ -185,8 +235,11 @@ rotated a credential, update it manually on your host:
|
|||||||
# See what's currently registered
|
# See what's currently registered
|
||||||
sbx secret ls
|
sbx secret ls
|
||||||
|
|
||||||
# Overwrite a specific secret (supply the new value via stdin)
|
# Overwrite an LLM provider credential (supply the new value via stdin)
|
||||||
echo "sk-new-value" | sbx secret set -g --force anthropic
|
echo "sk-new-value" | sbx secret set --force anthropic
|
||||||
|
|
||||||
|
# Overwrite an MCP secret, keyed by its kebab-cased secret name
|
||||||
|
echo "ghp-new-value" | sbx secret set --force github-pat
|
||||||
```
|
```
|
||||||
|
|
||||||
The updated credential takes effect immediately; you do not need to restart the sandbox.
|
The updated credential takes effect immediately; you do not need to restart the sandbox.
|
||||||
@@ -225,17 +278,18 @@ sandbox:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# <config-dir>/sbx-mixin.yaml
|
# <config-dir>/sbx-mixin.yaml
|
||||||
schemaVersion: "1"
|
schemaVersion: "2"
|
||||||
kind: mixin
|
kind: mixin
|
||||||
name: my-python-tooling
|
name: my-python-tooling
|
||||||
description: Install the ruff Python linter for use in any sandbox
|
description: Install the ruff Python linter for use in any sandbox
|
||||||
|
|
||||||
network:
|
permissions:
|
||||||
allowedDomains:
|
network:
|
||||||
- "files.pythonhosted.org:443"
|
allow:
|
||||||
- "pypi.org:443"
|
- "files.pythonhosted.org"
|
||||||
|
- "pypi.org"
|
||||||
|
|
||||||
commands:
|
setup:
|
||||||
install:
|
install:
|
||||||
- command: "uv tool install ruff"
|
- command: "uv tool install ruff"
|
||||||
user: "1000"
|
user: "1000"
|
||||||
@@ -245,6 +299,45 @@ commands:
|
|||||||
After saving this file, your next `coyote --sandbox` automatically applies it. See the [official sbx kit reference](https://docs.docker.com/ai/sandboxes/customize/kits/)
|
After saving this file, your next `coyote --sandbox` automatically applies it. See the [official sbx kit reference](https://docs.docker.com/ai/sandboxes/customize/kits/)
|
||||||
for the full mixin schema.
|
for the full mixin schema.
|
||||||
|
|
||||||
|
### What a mixin can declare
|
||||||
|
|
||||||
|
A mixin can carry everything a full kit can **except** `sandbox` (base image/entrypoint), `extends`, and
|
||||||
|
`mixins`, as those belong to the base kit. The full authoring surface:
|
||||||
|
|
||||||
|
| Section | Purpose |
|
||||||
|
|---------|---------|
|
||||||
|
| `permissions.network.allow` | Extra egress domains. Bare host = HTTPS on 443; use `host:port` for anything else. `*.host` matches **exactly one** subdomain label. It covers `api.host` but not `a.b.host`, and not the bare `host` itself. There is no allow-all `'*'`. |
|
||||||
|
| `setup.install` | One-time install commands, run once when the sandbox is created. `command` is a shell **string** (run via `sh -c`); set `user: "0"` for root or `"1000"` for the agent user. |
|
||||||
|
| `setup.startup` | Commands run on **every** sandbox start. `command` is an **argv list** (e.g. `['sh', '-c', '...']`); add `background: true` for daemons. |
|
||||||
|
| `setup.files` | Files written into the sandbox: `path`, `content`, `mode`. Only `${WORKDIR}` is expanded inside `content`. Other `${...}` sequences are rejected (unbraced `$VAR` in scripts is fine). |
|
||||||
|
| `environment.variables` | Static environment variables. |
|
||||||
|
| `credentials` | Secret declarations: each entry names an sbx service and an env var, and may add `apiKey.inject` rules so the sandbox proxy writes the real value into request headers at the network edge (`proxyManaged: true`). Bind values with `sbx secret set <service>`. Every inject `domain` **must** also appear in your `permissions.network.allow`. |
|
||||||
|
| `agentInstructions.content` | Instructions for the agent running inside the sandbox, written to the sandbox's `kits-memory/<mixin-name>.md` and progressively disclosed. (A mixin's `filename` field is ignored, and only the base kit controls the primary instructions file.) |
|
||||||
|
|
||||||
|
Composition across the base kit and all mixins is additive: allow lists concatenate, `setup` entries append in kit
|
||||||
|
order, and environment variables merge with later kits overriding earlier ones on key collisions. Kit names must be
|
||||||
|
unique across everything passed to one sandbox.
|
||||||
|
|
||||||
|
The `credentials` section deserves a call-out: it means a custom tool's mixin can ship real proxy-managed auth, not
|
||||||
|
just open domains. For example, a tool hitting an internal API could declare:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
credentials:
|
||||||
|
- service: internal-api
|
||||||
|
description: Internal API token for the my-report tool
|
||||||
|
apiKey:
|
||||||
|
name: INTERNAL_API_TOKEN
|
||||||
|
proxyManaged: true
|
||||||
|
inject:
|
||||||
|
- domain: internal-api.example.com
|
||||||
|
scheme: bearer
|
||||||
|
```
|
||||||
|
|
||||||
|
Inside the sandbox, `INTERNAL_API_TOKEN` holds the literal string `proxy-managed`; the real value (bound once via
|
||||||
|
`sbx secret set internal-api`) never enters the VM. Requests to `internal-api.example.com` get the
|
||||||
|
`Authorization: Bearer <token>` header added at the proxy. Use `header:` + `format: "%s"` instead of
|
||||||
|
`scheme: bearer` for non-standard headers (exactly one `%s` required, and `format`/`scheme` are mutually exclusive).
|
||||||
|
|
||||||
### Built-in mixins Coyote ships
|
### Built-in mixins Coyote ships
|
||||||
|
|
||||||
Coyote already ships mixins for the most common needs. They get auto-applied whenever they're relevant — you don't need to do anything to enable them:
|
Coyote already ships mixins for the most common needs. They get auto-applied whenever they're relevant — you don't need to do anything to enable them:
|
||||||
@@ -252,6 +345,10 @@ Coyote already ships mixins for the most common needs. They get auto-applied whe
|
|||||||
- **Built-in tools mixin** (auto-applied when `coyote --install functions` has run). Allowlists the domains used by
|
- **Built-in tools mixin** (auto-applied when `coyote --install functions` has run). Allowlists the domains used by
|
||||||
every built-in global tool and the default MCP server set: Wikipedia, arxiv, jina, wttr, WolframAlpha, Perplexity,
|
every built-in global tool and the default MCP server set: Wikipedia, arxiv, jina, wttr, WolframAlpha, Perplexity,
|
||||||
Tavily, Twilio, github MCP, atlassian MCP, ddg-search MCP, npm registry, Docker registries.
|
Tavily, Twilio, github MCP, atlassian MCP, ddg-search MCP, npm registry, Docker registries.
|
||||||
|
- **`coyote-mcp` mixin** (auto-generated on every non-`--fresh` launch; not a file on disk). Declares your MCP
|
||||||
|
credentials and allowlists every remote MCP server in your `mcp.json`. See
|
||||||
|
[The generated coyote-mcp mixin](#the-generated-coyote-mcp-mixin). It does not appear in the verbose mixin log,
|
||||||
|
which only lists discovered mixin *files*.
|
||||||
|
|
||||||
You can list everything that's about to be applied via the [verbose mixin log](#verbose-mixin-log) on every launch.
|
You can list everything that's about to be applied via the [verbose mixin log](#verbose-mixin-log) on every launch.
|
||||||
|
|
||||||
@@ -284,13 +381,13 @@ If something looks wrong, these are your three debugging flags:
|
|||||||
|
|
||||||
| Flag | Effect | Use when… |
|
| Flag | Effect | Use when… |
|
||||||
|----------------------|------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|
|
|----------------------|------------------------------------------------------------------------------------------|---------------------------------------------------------------------------|
|
||||||
| `--fresh` | Skips host config copy and MCP secret injection; LLM credentials still injected via proxy | You want a clean-slate sandbox with no copied state |
|
| `--fresh` | Skips host config copy, MCP secret injection, and the generated `coyote-mcp` mixin (so MCP servers get no auto egress); LLM credentials still injected via proxy | You want a clean-slate sandbox with no copied state |
|
||||||
| `COYOTE_SANDBOX_KIT` | Points at an alternate base kit instead of the embedded one | Developing a fork of the kit or hardening for a specific environment |
|
| `COYOTE_SANDBOX_KIT` | Points at an alternate base kit instead of the embedded one | Developing a fork of the kit or hardening for a specific environment |
|
||||||
|
|
||||||
### Common failures
|
### Common failures
|
||||||
|
|
||||||
- **A mixin causes `sbx create` to abort.** Check the verbose mixin log to identify which mixin was being applied when
|
- **A mixin causes `sbx create` to abort.** Check the verbose mixin log to identify which mixin was being applied when
|
||||||
it failed. Open that mixin file and inspect its `commands.install` block. Temporarily move the file aside to confirm.
|
it failed. Open that mixin file and inspect its `setup.install` (v1: `commands.install`) block. Temporarily move the file aside to confirm.
|
||||||
- **A malformed `sbx-mixin.yaml` aborts launch before `sbx create` runs.** Coyote fails fast with a parse error naming
|
- **A malformed `sbx-mixin.yaml` aborts launch before `sbx create` runs.** Coyote fails fast with a parse error naming
|
||||||
the file. Fix the YAML or temporarily move it aside.
|
the file. Fix the YAML or temporarily move it aside.
|
||||||
- **An API request from inside the sandbox fails with an auth error.** The secret may not have been registered or may
|
- **An API request from inside the sandbox fails with an auth error.** The secret may not have been registered or may
|
||||||
@@ -298,8 +395,10 @@ If something looks wrong, these are your three debugging flags:
|
|||||||
[Updating a registered secret](#updating-a-registered-secret).
|
[Updating a registered secret](#updating-a-registered-secret).
|
||||||
- **A custom tool fails with `command not found`.** The tool's required binary isn't installed in the sandbox. Add it
|
- **A custom tool fails with `command not found`.** The tool's required binary isn't installed in the sandbox. Add it
|
||||||
to the matching mixin (per-tool, per-agent, or top-level user mixin). See [Custom Tools](Custom-Tools#sandbox-support).
|
to the matching mixin (per-tool, per-agent, or top-level user mixin). See [Custom Tools](Custom-Tools#sandbox-support).
|
||||||
- **A network request from inside the sandbox hangs or fails.** The domain isn't in any `allowedDomains` block. Add it
|
- **A network request from inside the sandbox hangs or fails.** The domain isn't in any allow list
|
||||||
to a user mixin. The sandbox's proxy denies all unlisted traffic silently.
|
(`permissions.network.allow`; v1: `network.allowedDomains`). Remote MCP servers from `mcp.json` are auto-allowed
|
||||||
|
via the generated [`coyote-mcp` mixin](#the-generated-coyote-mcp-mixin); anything else needs a user mixin. The
|
||||||
|
sandbox's proxy denies all unlisted traffic silently.
|
||||||
|
|
||||||
## When not to use sandbox mode
|
## When not to use sandbox mode
|
||||||
|
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ If you use Coyote's [Sandbox mode](Sandboxes), installing a shared bundle that c
|
|||||||
files is a **privilege escalation event**. Those mixins are auto-discovered and applied silently on your next
|
files is a **privilege escalation event**. Those mixins are auto-discovered and applied silently on your next
|
||||||
`coyote --sandbox`, granting:
|
`coyote --sandbox`, granting:
|
||||||
|
|
||||||
- **Network access** to every domain listed in the mixin's `network.allowedDomains`.
|
- **Network access** to every domain listed in the mixin's `permissions.network.allow` (v1: `network.allowedDomains`).
|
||||||
- **Install commands** run with passwordless sudo inside the sandbox (the agent user has full sudo).
|
- **Install commands** run with passwordless sudo inside the sandbox (the agent user has full sudo).
|
||||||
- **Environment variables** set in `environment.variables`.
|
- **Environment variables** set in `environment.variables`.
|
||||||
|
|
||||||
@@ -373,8 +373,8 @@ unknown sources.
|
|||||||
find /tmp/audit -name 'sbx-mixin.yaml' -exec cat {} +
|
find /tmp/audit -name 'sbx-mixin.yaml' -exec cat {} +
|
||||||
```
|
```
|
||||||
2. **Look specifically at**:
|
2. **Look specifically at**:
|
||||||
- `network.allowedDomains`: What domains are being added to the sandbox's allowlist?
|
- `permissions.network.allow` (v1: `network.allowedDomains`): What domains are being added to the sandbox's allowlist?
|
||||||
- `commands.install`: What commands run with passwordless sudo? Do they pull from a recognized source
|
- `setup.install` (v1: `commands.install`): What commands run with passwordless sudo? Do they pull from a recognized source
|
||||||
(apt repo, official installer, signed release) or arbitrary URLs?
|
(apt repo, official installer, signed release) or arbitrary URLs?
|
||||||
- `environment.variables`: Do any look like credential exfiltration vectors (`*_API_URL` pointing at unknown
|
- `environment.variables`: Do any look like credential exfiltration vectors (`*_API_URL` pointing at unknown
|
||||||
hosts, `LD_PRELOAD`, etc.)?
|
hosts, `LD_PRELOAD`, etc.)?
|
||||||
@@ -396,4 +396,4 @@ off, abort with Ctrl-C (only safe before `sbx create` starts running install com
|
|||||||
|
|
||||||
If you publish a config repo, **document any `sbx-mixin.yaml` files in your `README.md`**. Explain what they add to
|
If you publish a config repo, **document any `sbx-mixin.yaml` files in your `README.md`**. Explain what they add to
|
||||||
the sandbox and why. Users who land on your repo will trust you more if you make the privilege grants explicit
|
the sandbox and why. Users who land on your repo will trust you more if you make the privilege grants explicit
|
||||||
instead of letting them discover via grep.
|
instead of letting them discover via grep.
|
||||||
+6
-4
@@ -125,10 +125,12 @@ If you set up Coyote with one provider and later want to switch, just edit your
|
|||||||
|
|
||||||
# Vault in Sandboxes
|
# Vault in Sandboxes
|
||||||
|
|
||||||
The vault is **not active** inside a [Docker Sandbox](Sandboxes). Coyote injects your LLM API credentials and MCP
|
The vault is **not active** inside a [Docker Sandbox](Sandboxes). Coyote registers your LLM API credentials and MCP
|
||||||
secrets directly into the sbx global secret store **on your host** before the sandbox starts, using
|
secrets with the sbx secret store **on your host** before the sandbox starts. LLM providers under their service
|
||||||
`sbx secret set -g <service>`. Inside the sandbox, the sbx network proxy intercepts outbound HTTP requests and
|
name (`sbx secret set anthropic`) and each distinct MCP secret under its kebab-cased secret name
|
||||||
supplies the registered credentials automatically; no vault password file is copied, and no vault provider CLI is
|
(`{{GITHUB_PAT}}` → `sbx secret set github-pat`; any number of secrets per server). Inside the sandbox,
|
||||||
|
header-only secrets are injected into requests by the sbx network proxy, and everything else is exposed as
|
||||||
|
`COYOTE_SECRET_<NAME>` environment variables; no vault password file is copied, and no vault provider CLI is
|
||||||
installed or configured inside the VM.
|
installed or configured inside the VM.
|
||||||
|
|
||||||
This means:
|
This means:
|
||||||
|
|||||||
Reference in New Issue
Block a user