Skip to content

Sandbox browser preview ​

Sometimes an agent starts a server inside its sandbox pod — a dev server, a freshly built web app, an API it stood up, a debug endpoint — and you want to actually open it in your browser and look. Because each run executes in its own isolated pod, that server isn't reachable by default. The sandbox browser preview is the supported way to reach it: a live preview served at a unique HTTPS URL that routes straight to the run's own pod, scoped to that one run.

This page walks through the user experience. For the API see the reference; for how the proxy works under the hood see the deep dive.

When the Preview button is available ​

A Preview Sandbox button appears in the coordinator run page when both are true:

  • the run is using the Kubernetes sandbox (the run's sandbox.selected event reports backend kubernetes-sandbox-claim), and
  • a preview lifecycle state has been recorded or an existing preview session is available.

Kubernetes placement alone does not make the header button appear. The current UI does not hide it solely because the run is terminal: a retained preview can still be inspected. An operator API request can also start a post-run preview if the required pod is still bound; agent/platform publication remains run-bound.

Step by step ​

  1. Click Preview. The Sandbox Preview dialog opens.
  2. Pick a port. Enter the port your app is listening on inside the sandbox pod. The Target port (inside sandbox) field defaults to 3000. It must be within the allowed preview range (3000–9000 by default); other values are rejected.
  3. Start. Click Start. The app calls POST /api/runs/{runId}/sandbox/port-forward with that port. The API resolves the run's bound pod from cluster state and provisions a per-preview HTTPRoute and ClusterIP Service wiring the shared preview gateway to your pod.
  4. See it become active. The dialog confirms "Preview active for port {target_port} on pod {pod_name}" and shows the session id.
    • When a preview_url is returned (the normal case in-cluster), the dialog embeds the live preview in an iframe (with referrerPolicy="no-referrer") and offers an Open preview button that opens it in a new tab.
    • On a local/dev backend where the gateway path is off, the dialog notes that no proxied preview URL was returned.
  5. Keepalive is bounded. While the run page has an active session with a keepalive URL, it sends keepalive about every 60 seconds. This is not gated on the dialog being open. Expiry is bounded by the original maximum lifetime; watching does not extend the hard cap.
  6. Stop when done. Click Stop to tear it down (DELETE on that session). Close just dismisses the dialog.

Agent-initiated preview (with your approval) ​

An agent can also open a preview for you, mid-run, when it has just started a server and wants to show you the result — without you opening the dialog or typing a port. The agent calls a start_preview(port) tool; instead of exposing the server silently, that request raises a human-in-the-loop approval on the run timeline:

  • A "the agent wants to expose a preview server on port N" approval card appears (the same kind of card used for the agent's URL-fetch requests). Approve it and the agent gets back the live preview_url; the preview behaves exactly like one you started yourself (same URL, same auto-expiry, same Stop).
  • The approval remains visible in the notification badge, a persistent toast, and the run timeline. The timeline card shows when it expires.
  • If you don't approve within the project's configured window (24 hours by default), the request lapses. Choose Retry approval on the expired card or preview status to create a fresh approval request. Agentweaver reuses the healthy server process instead of restarting the run or executing the preview command again.
  • The agent can only ever request a preview for its own run — the run is bound server-side, so a start_preview call can't reach another run's pod.

Operators running automated demos can set SANDBOX_PREVIEW_AUTO_APPROVE=true (or the per-run auto-approve-tools option) to grant these requests automatically. Project owners can set the manual approval expiry and preview lifetime to 1–1440 minutes in Project settings → Sandbox policy; both default to 24 hours. Preview lifetime is a single expiration and hard-cap setting. The deployment setting Sandbox:Preview:ApprovalTimeoutMinutes (or SANDBOX_PREVIEW_APPROVAL_TIMEOUT_MINUTES) is retained as a 24-hour-default fallback for legacy runs that are not associated with a project. In normal use the approval stays in your hands.

Build & Test preview ​

Workflows that include the platform-owned Build & Test step can also produce a browser preview. After an approved or request-changes Build & Test verdict, the platform attempts to start the app/service, discover its actual port, and register a preview URL through the approval flow. Declined verdicts skip it; preview failure does not block human review. It no longer injects PORT=3000 or --port; apps use their framework default or honor process.env.PORT if they already support it. Port discovery is dependency-free: AgentHost reads app log hints and the sandbox pod's /proc/net/tcp plus /proc/net/tcp6 socket tables, so it works even when the image lacks ss and when Node binds IPv6-any (::). Before registration, AgentHost fronts the observed app port with a pod-local TCP forwarder on an allowed public port (3000-9000), so even a loopback-only app is reachable from the Gateway.

The preview backend resolves either the run's AgentHost claim or a retained command-sandbox claim, so previews work whether the server was started in the agent-{runId} pod-per-run sandbox or the run-{runId} Build & Test command sandbox (apps/Agentweaver.Api/Sandbox/SandboxClaimConventions.cs:28, apps/Agentweaver.Api/Sandbox/Preview/SandboxPreviewService.cs:432).

What to expect ​

  • Kubernetes-only. The preview routes into the run's own sandbox pod. On local/dev runs there is no claim pod, so the button doesn't appear — the same "this is a cluster feature" boundary as the pod pill.
  • A real, public HTTPS URL. Unlike the older loopback design, the preview is reachable at https://{token}-preview.{ZoneSuffix}. The URL is an unguessable capability link (128-bit token): anyone with the URL can open it, so don't share it. It is short-lived and auto-expires.
  • Scoped to this run's pod. A preview reaches only the run's own sandbox pod, never another run's. Keepalive and stop verify the token actually belongs to the run before acting.
  • Auto-expiry. Project lifetime defaults to 1440 minutes (24 hours) and can be set to 1–1440 minutes. Initial expiry and maximum lifetime are both calculated from that same setting at creation; keepalive cannot extend the hard cap. Expiry, explicit Stop, or pod loss ends availability. The default retention policy permits a preview to outlive its run only while the required pod/process resources remain available. The route state is durable across API restarts.
  • Platform previews handle loopback binds. The Build & Test live-preview path runs the app and forwarder inside the sandbox pod and registers the forwarder's 0.0.0.0 public port, so apps that only bind 127.0.0.1 can still be previewed. Manual previews still expose the port you enter directly, so prefer all-interface binds there.
  • Gateway is the reachability test. The API does not probe the sandbox pod directly. Platform previews first check readiness in-pod; registration then waits for publication through the generated HTTPS Gateway URL. Opening that returned URL exercises the same path.
  • Failure is actionable, not blocking. If the app exits, no listening port appears, observe hits an unexpected error, or the forwarder cannot make the app reachable, you see Preview unavailable with a reason such as process_exited:exit={code}, no_listening_port_discovered, observe_error, bound_unreachable, or no_public_port_available; review can continue.
Diagram details and constraints
ElementContract
titlePreview readiness follows the public path
takeawayProvision the route, then probe its exact HTTPS URL; object creation alone is not ready.
group-title0CONTROL: PROVISION + PROBE
group-title1GATEWAY DATA PATH
Preview APIPreview API
Preview APIResolve bound SandboxClaim
Preview APIPatch run selector on pod
Preview APICreate Service + HTTPRoute
Preview APIState from cluster, not cache
Publication probePublication probe
Publication probeExact generated HTTPS URL
Publication probeWait for managed DNS
Publication probeCheck Gateway + application
Publication probeOnly then return ready
Browser previewBrowser preview
Browser previewOpen the returned URL
Browser previewRun-scoped capability host
Browser previewKeepalive via API
Browser previewIframe: no-referrer
Preview GatewayPreview Gateway
Preview GatewaySeparate shared Gateway
Preview GatewayHTTPS host match
Preview GatewayHTTPRoute selects Service
Preview GatewayNot API port-forward
ClusterIP ServiceClusterIP Service
ClusterIP ServicePer-preview target selector
ClusterIP ServiceService :80 → public port
ClusterIP ServiceRoutes to bound sandbox pod
ClusterIP ServiceAllowed ports 3000–9000
Sandbox preview appSandbox preview app
Sandbox preview appAgentHost pod-local path
Sandbox preview appLive preview: TCP forwarder
Sandbox preview app0.0.0.0 → loopback app
Sandbox preview appManual: chosen target port
relation-01 after create
relation-12 ready URL
relation-23 HTTPS probe
relation-34 HTTPS
relation-45 route
relation-56 public port
assuranceNo API → pod TCP readiness probe. Publication failure rolls back; DNS convergence has a bounded retry window.
assurance-0-labelPublic readiness
assurance-0-factProbe the exact generated HTTPS URL.
assurance-0-sourceSandboxPreviewService.cs
assurance-1-labelRollback on failure
assurance-1-factUnpublish failed preview resources.
assurance-1-sourceSandboxPreviewPublicationTests.cs
assurance-2-labelSeparate ingress
assurance-2-factDNS managed externally, not by API.
assurance-2-sourcegateway-preview.yaml
n0Patch run selector on pod; Create Service + HTTPRoute
n1Wait for managed DNS; Check Gateway + application
n2Run-scoped capability host; Keepalive via API
n3HTTPS host match; HTTPRoute selects Service
n4Service :80 → public port; Routes to bound sandbox pod
n5Live preview: TCP forwarder; 0.0.0.0 → loopback app
groupsCONTROL: PROVISION + PROBE; GATEWAY DATA PATH