How Sandbox Port Exposure and Domain Mapping Work in Open Agents

Open Agents automatically maps TCP ports declared inside a Vercel Sandbox to unique public sub-domains, injecting routable URLs as environment variables so user processes can expose web services without hard-coding endpoints.

The vercel-labs/open-agents repository runs user code inside isolated Firecracker MicroVMs called Vercel Sandboxes. When you need to expose a development server or API from within these sandboxes, the platform handles port exposure and domain mapping through a declarative configuration system implemented in packages/sandbox/vercel/sandbox.ts.

Declaring Ports at Sandbox Creation

You specify which ports your application will listen on by passing a ports array when initializing the sandbox connection. These ports are stored in the private field this._ports and represent the explicit TCP endpoints you intend to expose.

import { connectVercelSandbox } from "packages/sandbox/vercel";

const sandbox = await connectVercelSandbox({
  name: "demo-session",
  ports: [3000, 5173],  // These become routable preview ports
});

The Domain Resolution Pipeline

The mapping logic follows a multi-step resolution process that combines declared ports with dynamically discovered routes, then resolves the first available public host.

Collecting Declared and Dynamic Ports

The system first aggregates all potential ports using getRoutePorts() and getPreviewPorts(). The getRoutePorts() method extracts ports from sdk.routes (lines 35-44), while getPreviewPorts() merges these with the initially declared ports stored in this._ports (lines 46-49).

// Logic from packages/sandbox/vercel/sandbox.ts
getPreviewPorts(): number[] {
  return [...this._ports, ...this.getRoutePorts()];
}

Building the Candidate Port Set

The getCandidatePorts() method creates a deduplicated list of all preview ports plus the implicit HTTP port 80 as a fallback mechanism (lines 52-54). This ensures that even if specific application ports fail to resolve, the sandbox can still attempt to route through the standard HTTP port.

Resolving the Public Host

The host getter implements the core resolution logic (lines 69-88). It iterates over the candidate ports and queries the Vercel SDK for a valid domain via sdk.domain(port). The first successful lookup becomes the canonical host. If all specific ports fail, the system attempts port 80 as a final fallback.

Once resolved, the domain(port) convenience wrapper exposes full URLs by forwarding to the SDK's session.domain(port) implementation (lines 1014-1015), returning strings like https://sbx-3000-abc123.vercel.run.

Runtime Environment Injection

To eliminate hard-coded URLs in user code, the sandbox injects connectivity information through environment variables generated by getRuntimePreviewEnv() (lines 56-62). Every command executed inside the sandbox receives:

  • SANDBOX_HOST — The canonical host derived from the first successful port resolution (e.g., sbx-abc123.vercel.run)
  • SANDBOX_URL_<PORT> — Full HTTPS URL for each routable port (e.g., SANDBOX_URL_3000=https://sbx-3000-abc123.vercel.run)

These variables are merged with any user-provided env configuration before command execution.

Practical Implementation Examples

Creating a Sandbox and Accessing URLs

This example demonstrates declaring ports and retrieving both the host and specific port URLs:

const sandbox = await connectVercelSandbox({
  name: "demo-session",
  ports: [3000, 5173],
});

console.log("Sandbox host:", sandbox.host);
// → sbx-abc123.vercel.run

console.log("Port 3000 URL:", sandbox.domain(3000));
// → https://sbx-3000-abc123.vercel.run

Using Environment Variables Inside Commands

Reference the injected URLs without knowing the actual sub-domain at build time:

await sandbox.exec(
  "echo $SANDBOX_HOST && echo $SANDBOX_URL_3000",
  "/vercel/sandbox",
  15_000,
);
// Output: sbx-abc123.vercel.run
//         https://sbx-3000-abc123.vercel.run

Handling Dynamic Port Discovery

If an application starts listening on a random port after sandbox initialization, the Vercel SDK adds the route to sdk.routes. The sandbox automatically includes these via getPreviewPorts():

// After the app has started...
const newPort = sandbox.getPreviewPorts().find(p => p !== 80);
console.log("Discovered port URL:", sandbox.domain(newPort!));

Summary

  • Port declaration occurs at sandbox creation via the ports array in connectVercelSandbox(), stored internally as this._ports.
  • Domain resolution aggregates declared ports and dynamic routes, deduplicates them with port 80 fallback, and resolves the first available public host using the host getter (lines 69-88).
  • URL generation happens through domain(port) (lines 1014-1015), returning complete HTTPS URLs mapped to unique Vercel sub-domains.
  • Runtime exposure uses getRuntimePreviewEnv() (lines 56-62) to inject SANDBOX_HOST and SANDBOX_URL_<PORT> variables into every sandbox process.
  • Fallback behavior ensures that if no ports are declared or routable, the system attempts to resolve port 80 to maintain connectivity.

Frequently Asked Questions

What happens if no ports are declared when creating a sandbox?

If you omit the ports array or none of the declared ports are currently routable, the domain resolution logic falls back to port 80. The getCandidatePorts() method (lines 52-54) always includes port 80 in the candidate set, and the host getter (lines 69-88) attempts this standard HTTP port as a final fallback before failing.

How does dynamic port discovery work for applications that bind to random ports?

When an application starts listening on a port not declared at initialization, the Vercel SDK reports this new route via sdk.routes. The getRoutePorts() method (lines 35-44) extracts these dynamic ports, and getPreviewPorts() (lines 46-49) merges them with the originally declared ports. This allows domain(port) to resolve URLs for runtime-allocated ports without restarting the sandbox.

How can processes inside the sandbox determine their public URLs?

Each command receives automatically generated environment variables built by getRuntimePreviewEnv() (lines 56-62). The SANDBOX_HOST variable contains the canonical host, while SANDBOX_URL_<PORT> variables provide the full HTTPS URL for each exposed port. User code can reference these variables instead of hard-coding domain names.

Why does the system include port 80 as a fallback candidate?

Port 80 serves as the default HTTP entry point provided by the Vercel infrastructure. By including it in the candidate set via getCandidatePorts() (lines 52-54), the sandbox ensures that a host is always resolvable when possible, even if application-specific ports are temporarily unavailable or were never declared. This provides a baseline connectivity option for health checks or basic HTTP responses.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →