# How Tunnel Ports Are Exposed Inside Sandboxes in Background Agents

> Learn how Background Agents expose tunnel ports inside sandboxes. Discover the generated env file that maps ports to public URLs, ensuring fresh connections before your code runs.

- Repository: [Cole Murray/background-agents](https://github.com/ColeMurray/background-agents)
- Tags: internals
- Published: 2026-07-13

---

**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`](https://github.com/ColeMurray/background-agents/blob/main/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.

```python

# 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`](https://github.com/ColeMurray/background-agents/blob/main/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.

```python

# 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`](https://github.com/ColeMurray/background-agents/blob/main/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.

```python

# 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`](https://github.com/ColeMurray/background-agents/blob/main/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.

```python

# 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.

```python

# 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.

```python
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`](https://github.com/ColeMurray/background-agents/blob/main/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`](https://github.com/ColeMurray/background-agents/blob/main/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`](https://github.com/ColeMurray/background-agents/blob/main/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`](https://github.com/ColeMurray/background-agents/blob/main/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.