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
|
# 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*.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user