Tools MCP and External Integration

Tools MCP and External Integration

Tool Execution Sandbox — Patterns

Tool execution sandbox › Patterns

39 of 65 in this book · 3 min read

Nothing sensitive crosses the boundary inward

Problem. The obvious way to let confined code call an authenticated service is to put the credential in its environment. Anything running inside can then read it, and every mechanism for getting data out becomes a mechanism for getting the credential out.

Approach. Keep credentials entirely outside. Confined code addresses a local broker — a socket, a loopback endpoint, a passed file descriptor — that performs the privileged call on its behalf, attaches the credential at the edge, and returns only the result. The broker enforces which operations are available and records every one of them, so the confined code's reach is the broker's menu rather than the credential's full scope.

Trade-off. The broker is a new component on the hot path that must be written carefully and kept narrow; a broker that proxies arbitrary requests has merely relocated the problem. Libraries inside that expect ambient credentials need adapting, which is friction tool authors will push back on.

Reset by replacement, never by cleanup

Problem. Pooling environments is the obvious way to hide startup cost, and cleaning one for reuse is an open-ended problem: temporary files, background processes, modified libraries, shell state, and anything written outside the paths the cleanup script knows about.

Approach. Treat an environment as consumed once anything has run in it. Return it to a pool only by destroying it and creating a replacement from the immutable image, and keep the pool warm by provisioning ahead of demand rather than by reusing instances. Where a session needs continuity, give it a dedicated workspace for its lifetime and destroy that on session end, rather than passing it to the next tenant.

Trade-off. Provisioning ahead of demand costs idle capacity, and the pool size becomes a tuning parameter with a direct relationship to both latency and spend. Long sessions holding dedicated workspaces also change the capacity model from per-call to per-concurrent-session.

Default-deny egress with named destinations

Problem. Network policy is usually written against the public internet because that is where exfiltration is imagined to go. The confined code is then left able to reach every internal service, the orchestrator's API, and the host's metadata endpoint — all of which are more useful to an attacker than an outbound connection.

Approach. Start from no network at all and add specific destinations per tool, enforced outside the sandbox where the confined code cannot alter them. Explicitly block link-local and metadata addresses, resolve names through a controlled resolver so lookups cannot themselves be a channel, and route permitted traffic through a proxy that logs it.

Trade-off. Package installation, dependency resolution, and anything that fetches at runtime all break, so the image must be complete or an internal mirror must be in the allowlist. The allowlist is also per-tool configuration that someone has to maintain, and the pressure to add a wildcard entry is constant.

Bound the whole attempt, not just the process

Problem. A wall-clock limit on the main process does not stop a child it spawned, a disk-filling loop, or a tight allocation cycle that makes the node unhealthy before the limit fires. Limits applied per container against resources that are actually shared per node fail in exactly the cases they were added for.

Approach. Place the ceilings on the boundary itself — total processes, total memory, writable disk, open descriptors, and elapsed time — enforced by the isolation mechanism rather than by code inside it. On breach, destroy the environment outright and return a specific, distinguishable failure naming the limit that was hit, so the caller can tell exhaustion apart from a crash.

Trade-off. Hard ceilings turn some legitimate heavy work into failures, and the limits have to be tuned per tool class rather than set globally. Destroying on breach also discards whatever diagnostic state was inside, so a small amount of evidence must be streamed out continuously if failures are to be debuggable at all.