Research for design.md §16 (M5: Connect), 2026-10-03.
The question: how do other cloud-VM agents connect Gmail, Calendar, GitHub, Notion, Slack and the rest without putting every vendor’s logic into one broker? Each option below is checked against two decided constraints:
Labels: E = evidence (code or a documented statement), I = inference.
Source: Meta’s engineering post “Security and safety for AI agents: our approach with Muse” (research.meta.ai, 2026-09-08), plus the help-center page “How Muse works with Connectors”.
Token custody (E):
systemd-nspawn “runtime cell” inside that VM, and authd sits outside the cell.Surrogates and injection (E):
Who runs vendor logic (E):
Approvals (E):
How many connectors, and how they are added:
Pattern. Muse does not avoid per-vendor code. It isolates that code: one privsep worker per built-in connector. Surrogate injection covers the long tail. Its custody (tokens in the VM) is ruled out for us, because a token in the VM is in every hibernate image and state upload (§12). Its split, which keeps vendor logic away from the agent and lets one gatekeeper apply policy on the semantic call, is the part worth copying.
readOnlyHint count as writes.mcp_oauth credentials are injected by a proxy into the MCP connections, and the provider refreshes them.environment_variable credentials are “stored in the sandbox as an opaque placeholder … substituted with the real secret at egress”. They are scoped by allowed_hosts and by injection_location (header and/or body).Two patterns, often used together:
Nobody gets per-action approval out of pattern 2. Muse gets it only because the semantic call goes to Sentinel before any network request.
Read from cocoonstack/sandbox origin/main (db065eb). Paths are under sandboxd/.
| Capability | State | Evidence |
|---|---|---|
| Rule fields | host (exact, *.suffix, *), methods, ports, secret, intercept; no path field |
egress/policy.go:19-25 |
| Where rules come from | node config only (pool policy and egress classes); PUT /v1/pools rejects egress; only pool rules may intercept |
pool/setpools.go:25-28, config/config.go:520-521 |
| Composed tenant claims | the pool rule wins, so only its secret is injected | egress/policy.go:157-166 |
| Injection | sets one header and overwrites any value the guest sent; the value is literal (no template), so Basic base64(x-access-token:…) must be precomputed |
egress/proxy.go:378-389 |
| Value source | the claim’s guest: false env entry, or else the node environment |
pool/env.go:142-155 |
| Injection paths | plain HTTP and absolute-form https:// without intercept; CONNECT only with intercept: true |
egress/proxy.go:283-285, egress/intercept.go:92 |
| Placeholder, query or body substitution | none | egress/proxy.go:378-389 |
| Inside the intercept | guest-side TLS offers only http/1.1; Upgrade is stripped, so no WebSocket; h2-only and pinning clients fail |
egress/intercept.go:32, egress/proxy.go:33-36 |
| Streaming | the forward and intercept paths copy without flushing (egress/proxy.go:338), so SSE stalls; opaque CONNECT splices raw bytes (:253) |
as cited |
| Runtime secret update | PUT/PATCH /v1/sandboxes/{id}/env; a change to guest: false entries only is accepted on a paused or hibernated claim, and the next request uses it |
pool/env.go:66-77 |
Host storage of guest: false values |
memory, plus claims.json mode 0600 in plaintext; never written to the guest or into checkpoints; GET redacts |
pool/claims.go:161,169, pool/env.go:26-35 |
| Audit | method, host, port, decision, secret name; no path or status | egress/proxy.go:55-63 |
| Capture | the default no-NIC lane hibernates and forks; only the egress (NIC) lane cannot | types/types.go:379-382 |
| Capture of traffic | explicit proxy only (http_proxy/https_proxy set by silkd); no transparent mode |
docs/egress.md:16-28 |
Correction for §16.3: the non-flushing copy that stalls SSE on the box door is egress/proxy.go:338 (the forward path). :253 is the CONNECT splice, which streams.
(A) Broker as MCP client of vendor-hosted servers (§16’s plan). The broker does OAuth and custody, and serves the upstream tools through its MCP doors.
(B) Custody on the host, logic in the box, credential injected at egress.
(C) Mix. (A) where a vendor hosts a good MCP server, and (B) for git, gh and plain REST APIs.
| (A) MCP proxy | (B) egress injection | (C) mix | |
|---|---|---|---|
| Per-vendor code in the broker | none for MCP vendors: a catalog entry (MCP URL, or OAuth client and scopes); an adapter for each vendor without MCP (§16.3: Slack, maybe Calendar) | none: a catalog entry of OAuth endpoints, scopes, hosts and header format | none: (B) replaces the adapters |
| Where vendor logic runs | the vendor’s MCP server | the box: gh, git, SDKs, community MCP servers, agent-written scripts |
both |
| sandboxd primitive needed | none new: the box-door pool rule and guest: false key (§16.3). SSE flush only if the door ever streams |
today: every vendor host and secret declared in node config (intercept pool rules). To scale: new per-claim rules (below) | GitHub works today (M5.2); others need the (B) primitive |
| Mode B (local agents) | yes, box hibernated included | only while the box runs, through the box’s shell tool; local agents’ own traffic never crosses sandboxd | yes, through (A) |
| Unattended runs | yes; writes need a standing allow (§16.5) |
yes, if the broker pushes a fresh token before wake (the api’s Ensure path) and on a timer while the box runs (Google: about 1 h, GitHub: 8 h) |
yes |
| Per-action approval | yes: the broker sees the tool and its arguments and renders the summary | no: the proxy sees host and method. Method gating breaks gh (GraphQL reads are POST /graphql), so read-only comes only from vendor scopes and repository selection |
per action on (A); per grant on (B) |
| Prompt injection exposure | ask on writes |
the model’s shell has the whole grant on the allowed hosts | only (B) grants carry it; keep them narrow |
| Failure modes | vendor MCP quality and churn (Google’s is a developer preview); directory gating (Slack); upstream schema changes | clients that ignore HTTPS_PROXY (Node’s built-in fetch without NODE_USE_ENV_PROXY) or carry their own CA bundle (Python certifi, Node) need image env; h2-only, gRPC, WebSocket and pinning fail; tokens in a query or body (key=, form posts) need substitution that does not exist; a token minted inside the box by an exchange is unprotected |
the union, each scoped to the vendors that use it |
New sandboxd primitive (B) needs, all mechanism with no vendor knowledge:
credentials list of {hosts, header, value} set through the env endpoint. It must:
intercept;Today, every new vendor is a node-config change on every node. That puts cocoonbox’s catalog into infra config.
injection_location: header). Needed only where a client puts the token somewhere other than one fixed header. Query and body substitution is a larger change: it needs buffering and a Content-Length rewrite.text/event-stream. (A) needs it only if the box door ever streams, and (B) needs it only if the box speaks to a vendor’s MCP server directly.ask in (B), and it puts an RPC on every intercepted request.Keep (A) as the base, and use (B) instead of broker adapters. That is (C), with this first cut:
github.com Basic, api.github.com Bearer), with the values precomputed by the broker;guest: false push on refresh and before wake.What we copy, and from whom: