How Tunnel Ports Are Exposed Inside Sandboxes in Background Agents

Background-Agents expose tunnel ports inside sandboxes through a generated environment file at /workspace/.tunnels.env that maps ports to public URLs, with the entrypoint ensuring freshness before user code executes.

The ColeMurray/background-agents repository orchestrates user code execution inside Modal-provided sandboxes. When sessions require public tunnel ports for web servers or development tools, the system exposes these ports through a carefully coordinated pipeline involving environment variables, URL resolution, and file-based discovery. Understanding how tunnel ports are exposed inside sandboxes is essential for debugging connectivity issues and building network-aware applications.

The Tunnel Port Exposure Pipeline

The exposure process follows a six-step pipeline that ensures tunnel URLs are correctly resolved, persisted, and made available to user processes before execution begins.

Step 1: Declaring Expected Ports via Environment Variables

The process begins in the control plane, where session settings define the required ports. According to the source code in packages/sandbox-runtime/src/sandbox_runtime/constants.py (lines 28-31), the system converts the settings.tunnelPorts array into a comma-separated list stored in the EXPECTED_TUNNEL_PORTS environment variable. This variable is injected into the sandbox environment before initialization.


# Control-plane configuration

session.settings.tunnelPorts = [3000, 5173]
os.environ["EXPECTED_TUNNEL_PORTS"] = "3000,5173"

Step 2: Resolving Tunnel URLs with SandboxManager

Inside packages/modal-infra/src/sandbox/manager.py (lines 56-65), the SandboxManager._resolve_tunnels method queries the Modal API via sandbox.tunnels() to obtain a mapping of ports to tunnel objects. This method includes retry logic for transient failures and returns a dictionary mapping each port to its corresponding public URL.


# Inside packages/modal-infra/src/sandbox/manager.py

urls = await SandboxManager._resolve_tunnels(
    sandbox,               # Modal sandbox object

    sandbox_id="sb-1",
    ports=[3000, 5173],
)

# urls => {3000: "https://tunnel-3000.example.com", 5173: "https://tunnel-5173.example.com"}

Step 3: Writing the Tunnel Environment File

After resolving the URLs, SandboxManager._resolve_and_setup_tunnels writes the results to /workspace/.tunnels.env. As implemented in packages/sandbox-runtime/src/sandbox_runtime/entrypoint.py (lines 1652-1664), each line follows the format TUNNEL_<port>=<url>, with the first line containing a marker TUNNEL_SANDBOX_ID=<sandbox-id> to track freshness.


# The manager creates the env file (simplified)

with open("/workspace/.tunnels.env", "w") as f:
    f.write(f"TUNNEL_SANDBOX_ID={sandbox_id}\n")
    for port, url in urls.items():
        f.write(f"TUNNEL_{port}={url}\n")

Step 4: Entrypoint Validation and Stale File Cleanup

On every startup, the sandbox entrypoint in packages/sandbox-runtime/src/sandbox_runtime/entrypoint.py (lines 1652-1688) parses EXPECTED_TUNNEL_PORTS using _expected_tunnel_ports(). If a tunnel env file exists from a previous run, _clear_stale_tunnel_env_file removes it unless the TUNNEL_SANDBOX_ID marker matches the current sandbox ID, preventing stale URL consumption.


# In entrypoint.py

expected_ports = self._expected_tunnel_ports()          # → [3000, 5173]

# Cleanup happens automatically if sandbox ID mismatch detected

Step 5: Blocking Until Tunnels Are Ready

Before executing user code, the entrypoint calls _wait_for_tunnel_env_file (lines 1696-1722), which blocks until the env file contains a line for every expected port. This guarantees that user processes only start after all tunnel URLs are fully provisioned and written to disk.


# In entrypoint.py

await self._wait_for_tunnel_env_file(expected_ports)   # blocks until both lines appear

Step 6: Accessing Tunnel URLs from User Code

Once the env file is confirmed, user processes inside the sandbox can read /workspace/.tunnels.env directly or inherit the environment variables to discover public URLs. The standard pattern uses os.getenv with the TUNNEL_<port> naming convention.

import os

tunnel_3000 = os.getenv("TUNNEL_3000")
print("My public URL:", tunnel_3000)   # https://tunnel-3000.example.com

Summary

  • Environment Variable Declaration: The control plane sets EXPECTED_TUNNEL_PORTS with comma-separated port numbers before sandbox initialization.
  • URL Resolution: SandboxManager._resolve_tunnels in packages/modal-infra/src/sandbox/manager.py queries Modal's API and implements retry logic for reliability.
  • File-Based Persistence: Tunnel URLs are written to /workspace/.tunnels.env with a TUNNEL_SANDBOX_ID marker to prevent stale data consumption.
  • Freshness Validation: The entrypoint clears stale env files and validates the sandbox ID marker before proceeding.
  • Blocking Guarantee: The _wait_for_tunnel_env_file method ensures user code only executes after all tunnel URLs are confirmed written.
  • User Discovery: Processes read TUNNEL_<port> environment variables or parse the env file directly to obtain public URLs.

Frequently Asked Questions

How does the sandbox prevent using stale tunnel URLs from previous sessions?

The entrypoint implements freshness checks in packages/sandbox-runtime/src/sandbox_runtime/entrypoint.py. When parsing _expected_tunnel_ports, it calls _clear_stale_tunnel_env_file, which compares the TUNNEL_SANDBOX_ID marker in /workspace/.tunnels.env against the current sandbox ID. If they differ, the file is deleted, forcing a wait for new tunnel URLs to be written by the current SandboxManager.

What happens if the tunnel URL resolution fails temporarily?

The SandboxManager._resolve_tunnels method includes built-in retry logic to handle transient failures when calling sandbox.tunnels(). The system will attempt to resolve the mapping of ports to URLs multiple times before failing, ensuring network hiccups don't immediately terminate the session setup process.

Can user code access the tunnel environment file directly, or only through environment variables?

User code can access tunnel URLs both ways. While the environment variables (e.g., TUNNEL_3000) are typically inherited by processes, the underlying file at /workspace/.tunnels.env is also readable directly. This flexibility allows applications to parse the file manually if needed, though the environment variable approach is the standard pattern as shown in packages/sandbox-runtime/src/sandbox_runtime/entrypoint.py.

Where are the tunnel port constants and file paths defined?

All tunnel-related constants reside in packages/sandbox-runtime/src/sandbox_runtime/constants.py (lines 16-31). This includes the EXPECTED_TUNNEL_PORTS environment variable name, the default path /workspace/.tunnels.env, and the TUNNEL_SANDBOX_ID marker format used for freshness validation.

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 →