docs: Updated and improved the Sandbox docs with the new fixes
+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
|
||||
# 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
|
||||
# under its kebab-cased secret name:
|
||||
sbx secret set <secret-name> # e.g. {{GITHUB_PAT}} registers as github-pat
|
||||
# one of two ways:
|
||||
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)
|
||||
# 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".
|
||||
# Coyote then renders a "coyote-mcp" mixin kit that declares each proxy-managed
|
||||
# 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
|
||||
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 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
|
||||
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.
|
||||
- **Proxy-injected service secrets.** 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 conflict-free
|
||||
request-header value (see [the demotion rules](#the-generated-coyote-mcp-mixin)).
|
||||
- **Placeholder-based custom secrets.** Everything else (e.g. MCP secrets that can't be expressed as a proxy header
|
||||
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
|
||||
|
||||
@@ -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
|
||||
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`.
|
||||
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
|
||||
[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
|
||||
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
|
||||
as a proxy-managed credential. On launch that mixin is discovered alongside the generated `coyote-mcp` one, the key is
|
||||
registered with the sbx secret store on your host, and the proxy writes it into the outgoing request header at the
|
||||
network edge (the same treatment LLM provider keys get). Queries then behave identically inside and outside the
|
||||
sandbox.
|
||||
When you attach a RAG, Coyote writes a small mixin next to it declaring the host as an allowed domain. On launch that
|
||||
mixin is discovered alongside the generated `coyote-mcp` one, and every secret referenced by the RAG's driver
|
||||
configuration is registered as a [placeholder-based custom secret](#placeholder-based-custom-secrets) targeting the
|
||||
host(s) found in that configuration (any http(s) URL in the values contributes its host; the `host`/`url` keys also
|
||||
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
|
||||
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
|
||||
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`).
|
||||
- **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 the proxy inject rules that let sbx write the real value
|
||||
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
|
||||
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:
|
||||
header value. It is provisioned as a placeholder-based custom secret instead 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.
|
||||
tell which credential a given request needs, so Coyote provisions *all* the conflicting secrets as custom
|
||||
secrets and explains why on stderr at launch. Because custom secrets are swapped by **placeholder value**, not
|
||||
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
|
||||
*where* the secret value lives changes.
|
||||
Either way, every server's domain stays in the generated allow list, so connectivity is never affected; only the
|
||||
mechanism carrying the secret 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).
|
||||
|
||||
### 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
|
||||
|
||||
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.
|
||||
|
||||
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 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
|
||||
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.
|
||||
- **`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
|
||||
- **`coyote-mcp` mixin** (auto-generated on every non-`--fresh` launch; not a file on disk). Declares your
|
||||
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,
|
||||
which only lists discovered mixin *files*.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user