docs: Updated and improved the Sandbox docs with the new fixes

2026-08-17 17:55:15 -06:00
parent c666b53565
commit f3d766007e
+76 -29
@@ -78,11 +78,14 @@ sbx secret set <llm-provider> # e.g. anthropic, openai, gemini
# 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, args, URLs, every occurrence of every secret; any number of # config (env, headers, args, URLs, every occurrence of every secret; any number of
# secrets per server), decrypts each distinct secret on the host, and registers it # secrets per server), decrypts each distinct secret on the host, and registers it
# under its kebab-cased secret name: # one of two ways:
sbx secret set <secret-name> # e.g. {{GITHUB_PAT}} registers as github-pat sbx secret set <secret-name> # proxy-injectable header secrets, e.g. {{GITHUB_PAT}} github-pat
sbx secret set-custom --env COYOTE_SECRET_<NAME> <host>...
# ...for everything else, plus every attached RAG's driver secrets: the env var
# holds a placeholder the proxy swaps for the real value at the network edge
# (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 # Coyote then renders a "coyote-mcp" mixin kit that declares each proxy-managed
# and allows egress to every remote MCP server. See "The generated coyote-mcp mixin". # 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
@@ -153,14 +156,18 @@ Run `sbx --help` for the full surface. None of these are reimplemented in Coyote
The vault is **not used inside a sandbox**. Instead, Coyote registers your LLM API credentials and MCP secrets with The vault is **not used inside a sandbox**. Instead, Coyote registers your LLM API credentials and MCP secrets with
the sbx secret store **on your host** before the sandbox starts. Inside the sandbox, each credential is supplied one the sbx secret store **on your host** before the sandbox starts. Inside the sandbox, each credential is supplied one
of two ways: of two ways, and in both, the real value never enters the VM:
- **Proxy-injected.** The sbx network proxy intercepts outbound HTTPS requests and writes the real credential into - **Proxy-injected service secrets.** The sbx network proxy intercepts outbound HTTPS requests and writes the real
the request header at the network edge. The environment inside the VM only ever holds the literal string credential into the request header at the network edge. The environment inside the VM only ever holds the literal
`proxy-managed`. Used for LLM providers, and for any MCP secret whose every occurrence is a request-header value. string `proxy-managed`. Used for LLM providers, and for any MCP secret whose every occurrence is a conflict-free
- **Environment-injected.** Secrets that can't be expressed as a proxy header rule are exposed inside the sandbox as request-header value (see [the demotion rules](#the-generated-coyote-mcp-mixin)).
`COYOTE_SECRET_<NAME>` environment variables, and Coyote resolves their `{{SECRET_NAME}}` placeholders from those - **Placeholder-based custom secrets.** Everything else (e.g. MCP secrets that can't be expressed as a proxy header
at startup. Either way, your `mcp.json` works unmodified inside the sandbox. rule, and every attached RAG's driver secrets) is registered with `sbx secret set-custom`. Inside the sandbox,
`COYOTE_SECRET_<NAME>` holds a unique placeholder (`sbx-cs-…`); whenever that placeholder appears in an outbound
HTTP(S) request header to one of the secret's target hosts, the proxy swaps in the real value at the network
edge. Coyote resolves `{{SECRET_NAME}}` placeholders from those env vars at startup. Either way, your `mcp.json`
works unmodified inside the sandbox. See [Placeholder-based custom secrets](#placeholder-based-custom-secrets).
### How injection works ### How injection works
@@ -172,7 +179,10 @@ Before `sbx create`, Coyote:
4. Unless `--fresh` is passed, repeats the same process for **every** `{{SECRET_NAME}}` placeholder found anywhere in 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 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 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`. registered once: proxy-injectable ones as a service secret under their kebab-cased name (`{{GITHUB_PAT}}`
registers as `github-pat`), the rest as [placeholder-based custom secrets](#placeholder-based-custom-secrets).
5. Registers every attached RAG's driver secrets as placeholder-based custom secrets too. A vault secret missing
for an MCP server fails the launch; one missing for a RAG only warns. That RAG's queries would fail, nothing else.
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.
@@ -186,11 +196,12 @@ A RAG [attached to a remote Qdrant collection](RAG#qdrant--attaching-an-existing
inside a sandbox, and Coyote arranges both without being asked: the collection's host has to be reachable through the inside a sandbox, and Coyote arranges both without being asked: the collection's host has to be reachable through the
network policy, and the API key has to reach the server without ever entering the VM. network policy, and the API key has to reach the server without ever entering the VM.
When you attach a RAG, Coyote writes a small mixin next to it declaring the host as an allowed domain and the API key When you attach a RAG, Coyote writes a small mixin next to it declaring the host as an allowed domain. On launch that
as a proxy-managed credential. On launch that mixin is discovered alongside the generated `coyote-mcp` one, the key is mixin is discovered alongside the generated `coyote-mcp` one, and every secret referenced by the RAG's driver
registered with the sbx secret store on your host, and the proxy writes it into the outgoing request header at the configuration is registered as a [placeholder-based custom secret](#placeholder-based-custom-secrets) targeting the
network edge (the same treatment LLM provider keys get). Queries then behave identically inside and outside the host(s) found in that configuration (any http(s) URL in the values contributes its host; the `host`/`url` keys also
sandbox. accept a bare `host[:port]`). Inside the sandbox the driver sees the placeholder; the proxy swaps in the real key at
the network edge. Queries then behave identically inside and outside the sandbox.
This is also why a RAG's `api_key` must be a `{{SECRET_NAME}}` placeholder rather than the key itself: Coyote reads This is also why a RAG's `api_key` must be a `{{SECRET_NAME}}` placeholder rather than the key itself: Coyote reads
that placeholder back out to learn which vault secret to hand to sbx. A literal key can't be provisioned, so Coyote that placeholder back out to learn which vault secret to hand to sbx. A literal key can't be provisioned, so Coyote
@@ -204,33 +215,57 @@ 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 - **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 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. 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 - **One credential declaration per proxy-managed 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 in-sandbox env var (`COYOTE_SECRET_GITHUB_PAT`), and the proxy inject rules that let sbx write the real value
let sbx write the real value into request headers at the network edge (`proxyManaged: true`). into request headers at the network edge (`proxyManaged: true`). Custom secrets are **not** declared in the
mixin (sbx binds them itself), but their target hosts still contribute allow-list entries (ports preserved).
When sbx asks you to approve credential bindings on the first interactive run, each entry carries a description 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. 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 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 header value. It is provisioned as a placeholder-based custom secret instead when any of these apply:
these apply:
- It appears anywhere other than a request header: `env`, `args`, a URL, or any nested field. - 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 `%`. - 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.) - 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 - **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 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 tell which credential a given request needs, so Coyote provisions *all* the conflicting secrets as custom
injection and warns on stderr at launch. Different secrets in different headers on one domain are fine and stay secrets and explains why on stderr at launch. Because custom secrets are swapped by **placeholder value**, not
proxy-managed. by domain + header, the conflict resolves itself: each secret gets its own placeholder and every server
authenticates correctly. 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 Either way, every server's domain stays in the generated allow list, so connectivity is never affected; only the
*where* the secret value lives changes. mechanism carrying the secret changes.
> **Not covered:** servers whose URL is buried in `args` rather than the `url` field (e.g. `npx mcp-remote > **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 > 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). > [user mixin](#extending-the-sandbox-auto-discovered-mixins).
### Placeholder-based custom secrets
`sbx secret set-custom` binds an env var to a secret value and a set of target hosts. Inside the sandbox the env
var holds a unique placeholder (`sbx-cs-…`); whenever that placeholder appears verbatim in an outbound HTTP(S)
request header to a target host, the proxy substitutes the real value at the network edge. Semantics worth knowing:
- **The secret must reach the wire verbatim, in a header.** A secret that gets *transformed* before it's sent (e.g.
AWS SigV4 request signing, HMAC computation, a client that encodes or locally inspects the key), can never work
under this scheme: the placeholder would be signed or encoded instead of the key. Such secrets have no sandbox
support today.
- **Host-scoped targets.** Targets are host-only (sbx rejects `host:port` targets; the network allow list keeps
the port). When Coyote can't derive any host for a secret (e.g. a stdio server whose config contains no URL)
it registers the wildcard target `'**'` and says so at launch; the swap then applies to any destination the
sandbox is allowed to reach.
- **Write-once values, drift-safe targets.** Re-launches skip secrets already registered with the right targets.
If the derived host set changes, Coyote removes the old registration (by placeholder) and re-registers with the
**union** of old and new targets. It never narrows a registration. An existing registration targeting `'**'` is
left alone with a note that it's wider than needed.
- **Existing sandboxes.** Value updates reach running sandboxes, but a **newly registered env var** only exists in
sandboxes created after it; Coyote prints a restart notice when that happens. Separately, Coyote hashes the
generated mixin per sandbox and warns on re-attach when the sandbox's baked-in network and credential rules have
drifted from your current config. Remove and re-create the sandbox to pick up the new rules.
### 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
@@ -260,6 +295,18 @@ 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.
Placeholder-based custom secrets are keyed by placeholder instead. Update the value in your vault first
(`coyote --update-secret`), then:
```bash
sbx secret ls # the CUSTOM SECRETS section lists env, targets, placeholder
sbx secret rm --placeholder sbx-cs-XXXX -f # drop the old registration
coyote --sandbox <NAME> # next launch re-registers it from the vault
```
Re-registration mints a **new placeholder**, so existing sandboxes hold a stale env value: re-create the sandbox,
or update the env var inside it to the new placeholder (Coyote and sbx both print the exact value to use).
### Vault operations inside the sandbox ### Vault operations inside the sandbox
Vault management commands (`coyote --add-secret`, `--update-secret`, `--delete-secret`, `--list-secrets`, and `.vault` Vault management commands (`coyote --add-secret`, `--update-secret`, `--delete-secret`, `--list-secrets`, and `.vault`
@@ -400,8 +447,8 @@ 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 - **`coyote-mcp` mixin** (auto-generated on every non-`--fresh` launch; not a file on disk). Declares your
credentials and allowlists every remote MCP server in your `mcp.json`. See proxy-managed 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, [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*. which only lists discovered mixin *files*.