Skip to content

Add public sandbox tunnels and custom-domain ingress - #6

Merged
bas3line merged 6 commits into
mainfrom
agent/public-tunnels
Jul 19, 2026
Merged

bas3line merged 6 commits into
mainfrom
agent/public-tunnels

Conversation

@bas3line

@bas3line bas3line commented Jul 19, 2026

Copy link
Copy Markdown
Owner

What changed

  • add lifecycle-bound public HTTP and WebSocket tunnel exposure with exact-host routing
  • add direct Traefik, Caddy on-demand TLS, Cloudflare Origin CA + Full (strict), fixed HTTP compatibility, and origin-hidden Cloudflare Tunnel deployment profiles
  • keep public domains operator-controlled; CLI and MCP return controller-issued URLs without permitting agents to rewrite ingress policy
  • expand sandbox-mcp to 12 typed tools, including tunnel create/list/delete
  • add server, client-PC, and custom-public-domain setup guides plus agent-readable tunnel/operations references
  • prepare the workspace and registry artifacts for Sandbox v0.1.1

Validation

  • cargo fmt --check
  • cargo check --workspace
  • workspace unit tests and doctests on Rust 1.97.1
  • strict workspace Clippy on Rust 1.97.1
  • all five Docker Compose ingress topologies render successfully
  • macOS arm64 dist binaries build and report v0.1.1
  • deployed Linux amd64 controller, worker, CLI, and MCP bridge report v0.1.1

Security model

Docker mode remains intended for dedicated or trusted worker hosts. Public exposure is explicit, policy-controlled, and lifecycle-bound. The external runtime driver remains the extension point for stronger isolation.

@bas3line bas3line changed the title Add public HTTP and WebSocket sandbox tunnels Add public sandbox tunnels and custom-domain ingress Jul 19, 2026
@bas3line
bas3line marked this pull request as ready for review July 19, 2026 16:58
Copilot AI review requested due to automatic review settings July 19, 2026 16:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — one error-path robustness concern worth a look, inline.

Reviewed changes — an initial review of the public-tunnel feature: lifecycle-bound HTTP/WebSocket exposure with exact-host routing, five Compose ingress topologies, and the v0.1.1 version bump.

  • Controller tunnel endpointscreate_tunnel, delete_tunnel, and an unauthenticated authorize_tunnel_domain (for Caddy on_demand_tls) in cmd/sandboxd/src/controller.rs; materialize_tunnels enforces DNS-label subdomains, blocks confidential/restricted sensitivity, authenticated=true, and non-HTTP protocols.
  • Model & configTunnel/TunnelState plus a strict validate_dns_label, and a thoroughly validated TunnelConfig (base-domain shape, config-token charset, absolute config_dir).
  • Docker tunnel managercrates/runtime/src/tunnel.rs writes per-tunnel Traefik file-provider routes and manages a per-sandbox --internal network shared only with the edge container, including none-mode save/restore for deny sandboxes.
  • Storage & schedulerfind_tunnel_by_hostname (memory + Postgres @> jsonb with a new GIN index) matching only Active tunnels; apply_tunnel_completion state machine; scheduler rejects placement when exposures are requested but the node reports !supports_http_tunnels.
  • CLI & MCPsandbox tunnel subcommands, --expose on create, and sandbox_tunnel_create/sandbox_tunnel_delete MCP tools that return operator-issued URLs and forbid client scheme rewrites.

The security model holds up well: subdomain and base-domain validation prevent Traefik route injection, the on-demand-TLS authorize endpoint is exact-match and length-capped, and the sensitivity/authentication/protocol gates are enforced consistently across controller, CLI, and MCP. The Docker image tags (traefik:v3.7.8, caddy:2.11.4-alpine, cloudflare/cloudflared:2026.7.2) were verified against upstream releases and are real.

One note on the CI validation: the "all five topologies render successfully" step runs docker compose ... config, which only validates YAML rendering — it does not prove the images are pullable. That's fine here since the tags are real, but the validation claim is weaker than it reads.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

}
Ok(())
} else {
Err(command_error("network rm", &output.stderr))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ The network rm failure branch returns Err without restoring none mode, but the success branch above does restore it. By this point both endpoints have already been disconnected (lines 200-202) and expose earlier detached the sandbox from none. So if docker network rm fails transiently, a deny-mode sandbox is left attached to no network at all — silently, because expose's rollback calls discard the result (let _cleanup = self.teardown_network(...).await). This contradicts the contract documented in docs/tunnels.md ("Removing the final tunnel restores none").

Technical details
# `teardown_network` skips `none`-mode restore on `network rm` failure

## Affected sites
- `crates/runtime/src/tunnel.rs` `teardown_network` — success branch restores `none` (the `if self.uses_none_mode(...) { self.connect("none", ...) }` block), but the `else` branch returning `Err(command_error("network rm", ...))` does not.
- `crates/runtime/src/tunnel.rs` `expose` — rollback sites discard the teardown result: `let _cleanup = self.teardown_network(sandbox_id).await;` and `let _cleanup = self.unexpose(...)`. A teardown failure is therefore invisible to the caller.
- Only reachable for `NetworkMode::Deny` sandboxes (mapped to Docker `none`); non-deny sandboxes keep their original network regardless.

## Required outcome
- A failed `docker network rm` during teardown must not leave the sandbox container detached from every network. The `none`-mode restoration should run on the failure path too (or before the `rm` is attempted), so denied-egress sandboxes are always reattached to `none`.

## Suggested approach (optional)
- Perform the `uses_none_mode` check + `connect("none", ...)` restore before returning, regardless of the `network rm` result (e.g. compute the restore need up front, attempt `rm`, then reattach to `none` if needed, and only then propagate any `rm` error).

@bas3line
bas3line merged commit 29e260b into main Jul 19, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants