Skip to content

feat: Internet ingress — expose sandbox ports as public preview URLs #4

Description

@clemlesne

Business Need

AI agents and developers using exec-sandbox need to preview web applications running inside sandboxes without SSH tunneling or port-forwarding hacks. Today, expose_ports binds to 127.0.0.1 only — the sandbox is invisible outside the host machine.

Real-world use cases that are blocked:

  • Agent-built web apps — An AI agent scaffolds a React/Flask app inside a sandbox and needs to hand the user a clickable URL for review.
  • Webhook receivers — Testing Stripe/GitHub/Slack webhooks requires a public endpoint.
  • Multi-device preview — A developer runs exec-sandbox on a cloud server and previews the sandbox output from a phone or tablet.
  • Collaborative review — Share a sandbox preview URL with a teammate for real-time feedback.

Every major sandbox platform already ships this capability (E2B, CodeSandbox, Replit, GitHub Codespaces, Gitpod, Daytona, Modal). Without it, exec-sandbox cannot serve the "deploy a preview" workflow that modern AI coding agents expect.

Benefits

  • Instant shareable previews — Every ExposedPort optionally gets a public HTTPS URL (e.g., https://3000-abc123.preview.example.com)
  • Zero user-side setup — No ngrok install, no SSH tunnels, no firewall rules; the URL works from any browser
  • Security-first — Auth token per preview URL by default (matches GitHub Codespaces/Daytona pattern); opt-in public access
  • Pluggable backends — Self-hosted proxy for production, ngrok/Cloudflare for quick-start; no vendor lock-in
  • Minimal API surface change — One new optional parameter on run()/session(), one new field on ExposedPort

Industry Context

All sandbox platforms use the same pattern — wildcard DNS + reverse proxy + port-in-subdomain:

Platform URL Format Auth Default Proxy
E2B {port}-{id}.e2b.app Public Own (Firecracker)
CodeSandbox {id}-{port}.csb.app Public / Private Own
GitHub Codespaces {codespace}-{port}.app.github.dev Private Own
Gitpod {port}-{workspace}.ws-{region}.gitpod.io Private Own
Daytona {port}-{id}.daytona.work Private + BYOP Own
Modal {workspace}--{app}-{fn}.modal.run Public Own
Replit {slug}.{user}.replit.dev Public Own

Port is always encoded in the subdomain (not path) so each port gets its own browser security origin, preventing cross-port cookie/storage attacks.

Daytona's "Customer Proxy" (v0.25, 2025) is the only bring-your-own-proxy model — customers deploy their own proxy at their domain. Worth considering for enterprise.

Explored Alternatives

Tunnel providers (for zero-config / local dev)

Provider Python Control Subdomains HTTPS Self-Host Cost at Scale Verdict
ngrok SDK v1.7.0 Excellent — in-process, async-native, no binary Yes (paid plans) Auto No $8-20/mo + per-endpoint-hour Best DX; not self-hostable; 5x slower than CF Tunnel; Edges sunset Dec 31, 2025
Cloudflare Tunnel Good — official Python SDK, full REST API Yes (owned domain) Auto No Free (tunnel itself) Best throughput; requires CF domain delegation; DNS propagation delay kills fast startup
zrok v1.0 Moderate — official Python SDK, sync-only (no asyncio) Yes Via Caddy Yes (Apache-2.0) Free (self-hosted) Best self-hosted option; v2 in RC with breaking API changes; Docker Compose deployment requires wildcard DNS + OpenZiti stack
frp v0.67.0 Poor — config file + reload API, no CRUD REST API Yes (HTTP mode) Operator-managed Yes (Apache-2.0) Free 90k+ stars, battle-tested; v2 has not shipped; no Python SDK
Pangolin (YC 2025) None — no Python SDK Yes Auto Yes (Apache-2.0) Free ~19k stars; WireGuard-based; identity-aware; too new for infra dependency
bore / rathole / serveo Poor — subprocess only No No Yes Free Eliminated — no subdomain routing, no HTTPS

Build-your-own proxy (for production deployments)

This is what E2B, CodeSandbox, Modal, and every mature sandbox vendor does:

  • Wildcard DNS on a domain you control (*.preview.exec-sandbox.dev)
  • Reverse proxy (Caddy / Nginx / custom Go) mapping {port}-{sandbox-id}.preview.example.com127.0.0.1:{host_port}
  • Wildcard TLS via Let's Encrypt DNS-01 challenge (automated with Caddy)
  • Full control over latency, auth, and branding; no per-tunnel cost; no third-party dependency

Proposed Implementation (Overview)

API change

One new optional parameter on Scheduler.run() and Scheduler.session():

result = await scheduler.run(
    code="python -m http.server 8080",
    language="python",
    expose_ports=[8080],
    tunnel=True,  # ← new: create public URL for each exposed port
)
print(result.exposed_ports[0].public_url)
# → https://8080-a1b2c3.preview.example.com

One new field on ExposedPort:

class ExposedPort(BaseModel):
    internal: int
    external: int
    host: str
    protocol: Literal["tcp", "udp"]
    public_url: str | None = None  # ← new, None when no tunnel configured

Pluggable backend via TunnelProvider protocol

class TunnelProvider(Protocol):
    async def create_tunnel(self, local_port: int, sandbox_id: str) -> str:
        """Returns public HTTPS URL."""
    async def destroy_tunnel(self, public_url: str) -> None: ...

Backends: NgrokTunnelProvider, CloudflareTunnelProvider, CaddyTunnelProvider (self-hosted), NullTunnelProvider (default, no-op).

Lifecycle integration

  • Create tunnels: after _prepare_vm() returns (VM booted, gvproxy forwarding ports), before returning ExecutionResult / Session
  • Destroy tunnels: in QemuVM.destroy() before cleanup_vm_processes()
  • Env var injection: set EXEC_SANDBOX_PREVIEW_URL_{PORT} inside the VM so user code can self-reference its public URL

Phasing

  1. Phase 1 — ngrok backend — Fastest path to a working demo. In-process Python SDK, async-native, zero sidecar processes. Gated behind pip install exec-sandbox[tunnel].
  2. Phase 2 — Caddy self-hosted backend — Production-grade. Single Caddy instance with on-demand TLS and wildcard DNS. No per-tunnel cost.
  3. Phase 3 — Auth & Daytona-style BYOP — Per-URL bearer token (private by default), customer-deployable proxy, signed preview URLs for iframe embedding.

References

Competitor preview URL docs

Tunnel providers — SDKs & docs

Benchmarks & comparisons

Caddy (self-hosted proxy)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions