SANDBOX/Sandbox Services

#Sandbox Services

Sandbox Services expose public HTTP entrypoints for workloads inside a sandbox. A service can point to a long-running listener on a sandbox port, start a command on first public access, or route to a Sandbox Function.

Use services for:

  • public HTTP routes into a sandbox
  • route-level method filtering, auth, CORS, rate limits, timeout, and path rewrite
  • public URL auto-resume for paused sandboxes
  • public entrypoints for listeners, command-backed services, and sandbox functions

Mental Model#

A sandbox service has three layers:

LayerPurpose
ServiceNames the public entrypoint and, for listener-backed services, the sandbox port
RuntimeDescribes who owns the backing process: user, command, or function
RouteMatches public HTTP requests and applies route policy before proxying or executing

The service response includes a public_url when ingress.public is true and the deployment has an exposure domain. Treat public_url as the service base URL. Append a route path when calling it.

bash
curl "$PUBLIC_URL/api/health"

Runtime Behavior#

When runtime is omitted, Sandbox0 treats the service as manual.

RuntimeportPublic access behavior
manualRequiredSandbox0 proxies traffic to port. The template, user, or agent must keep the listener running
cmdRequiredOn public access, Sandbox0 creates the configured procd cmd context when it is missing, waits for readiness, then proxies traffic to port
functionOmitSandbox0 routes the request to procd and executes the configured inline function source. No user-managed listener is required

For cmd services, health_check.path controls readiness. If it is omitted, Sandbox0 falls back to a TCP connect check on the service port.

Public URL Shape#

For listener-backed services, the public URL is derived from the sandbox ID, service port, region ID, and root domain:

text
https://<sandbox_id>--p<port>.<region_id>.<root_domain>

Function services also return a service-level public_url, but they use Sandbox0's reserved function routing label. See Sandbox Functions for function-specific URL and config details.

The URL itself does not include a route path. If a route has path_prefix: /webhook, call:

bash
curl -X POST "$PUBLIC_URL/webhook/github"

Routes And Paths#

Routes are evaluated before proxying or function execution.

FieldBehavior
path_prefixMatches request paths. Defaults to /
methodsAllows only listed HTTP methods. Empty allows all methods
rewrite_prefixRewrites the matched path prefix before proxying or function execution
authApplies bearer or header authentication before the request reaches the sandbox
corsHandles CORS preflight and response headers at the gateway
rate_limitApplies per-route rate limiting at the gateway
timeout_secondsSets the upstream or function execution timeout
resumeAllows this route to resume a paused sandbox when sandbox auto_resume is also true. Resume-enabled public routes require runtime.type: cmd or runtime.type: function

path_prefix is not stripped automatically. Without rewrite_prefix, the upstream service or function handler sees the original request path.

yaml
routes: - id: webhook path_prefix: /webhook resume: true

A request to $PUBLIC_URL/webhook/github reaches the backing service or function as /webhook/github.

To strip /webhook, set rewrite_prefix: /:

yaml
routes: - id: webhook path_prefix: /webhook rewrite_prefix: / resume: true

The same request reaches the backing service or function as /github.

Sandbox Functions#

Use Sandbox Functions when you want a public HTTP handler without running a long-lived listener inside the sandbox. A function is still configured as a service with runtime.type: function, so it uses the same public URL, route policy, auth, CORS, rate limit, timeout, rewrite, and auto-resume model.

Function services omit port, execute inline source through procd, and can return normal HTTP responses, SSE streams, or WebSocket messages. Set runtime.function.max_concurrency when one function service must limit simultaneous handler executions inside a sandbox runtime.

Cmd Services#

A cmd service starts a procd command on first matching public access when the service command is not already running. Sandbox0 injects these environment variables into the command:

Environment variableValue
SANDBOX0_SERVICE_IDService ID
SANDBOX0_SERVICE_PORTConfigured service port
SANDBOX0_SERVICE_RUNTIMEcmd
yaml
services: - id: api port: 8080 runtime: type: cmd command: - python3 - -c - | import http.server import json import os import socketserver port = int(os.environ["SANDBOX0_SERVICE_PORT"]) class Handler(http.server.BaseHTTPRequestHandler): def do_GET(self): if self.path == "/healthz": self.send_response(204) self.end_headers() return self.send_response(200) self.send_header("content-type", "application/json") self.end_headers() self.wfile.write(json.dumps({ "service_id": os.environ["SANDBOX0_SERVICE_ID"], "path": self.path, }).encode()) socketserver.TCPServer(("", port), Handler).serve_forever() health_check: path: /healthz ingress: public: true routes: - id: api path_prefix: /api rewrite_prefix: / methods: [GET] timeout_seconds: 30 resume: true

After applying this service, the first request starts the command, waits for /healthz, rewrites /api/hello to /hello, and proxies to the command.

bash
curl "$PUBLIC_URL/api/hello"

Manual Services#

Use manual when another actor starts and owns the listener.

yaml
services: - id: web port: 3000 runtime: type: manual ingress: public: true routes: - id: web path_prefix: / resume: false

Manual services can expose a listener while the sandbox is running, but they cannot be restarted by public auto-resume. Use a cmd service or a function service when a public route needs resume: true.

API Reference#

List Services#

GET

/api/v1/sandboxes/{id}/services

The response includes the normalized services, publishable, publish_blockers, public_url, and exposure_domain.

go
resp, err := sandbox.GetServices(ctx) if err != nil { log.Fatal(err) } for _, service := range resp.Services { fmt.Printf("%s url=%s port=%d\n", service.ID, service.PublicURL.Or(""), service.Port.Or(0)) }

Replace Services#

PUT

/api/v1/sandboxes/{id}/services

PUT replaces the full service list. Include every service that should remain active.

go
tokenHash := sha256.Sum256([]byte(os.Getenv("PUBLIC_API_TOKEN"))) resp, err := sandbox.UpdateServices(ctx, []apispec.SandboxAppService{ { ID: "api", Port: apispec.NewOptInt32(8080), Runtime: apispec.NewOptSandboxAppServiceRuntime(apispec.SandboxAppServiceRuntime{ Type: apispec.SandboxAppServiceRuntimeTypeCmd, Command: []string{"python3", "-m", "http.server", "8080"}, Cwd: apispec.NewOptString("/workspace"), }), Ingress: apispec.SandboxAppServiceIngress{ Public: true, Routes: []apispec.SandboxAppServiceRoute{ { ID: "api", PathPrefix: apispec.NewOptString("/api"), Methods: []string{"GET", "POST"}, Auth: apispec.NewOptSandboxAppServiceRouteAuth(apispec.SandboxAppServiceRouteAuth{ Mode: apispec.SandboxAppServiceRouteAuthModeBearer, BearerTokenSHA256: apispec.NewOptString(hex.EncodeToString(tokenHash[:])), }), TimeoutSeconds: apispec.NewOptInt32(30), Resume: true, }, }, }, }, }) if err != nil { log.Fatal(err) } fmt.Printf("services: %d\n", len(resp.Services))

Delete One Service#

The public API replaces the full service list; the CLI wraps that flow for deleting one service by ID.

bash
s0 sandbox service delete api -s sb_abc123

Clear Services#

Clearing services removes public HTTP exposure from the sandbox.

go
_, err := sandbox.ClearServices(ctx) if err != nil { log.Fatal(err) }

Set Services At Claim Time#

You can attach services when claiming a sandbox. The same service schema is accepted at claim time and by PUT /services.

yaml
template: default config: auto_resume: true services: - id: api port: 8080 runtime: type: cmd command: - python3 - -m - http.server - "8080" cwd: /workspace ingress: public: true routes: - id: api path_prefix: /api rewrite_prefix: / methods: [GET, POST] resume: true
go
sandbox, err := client.ClaimSandbox(ctx, "default", sandbox0.WithSandboxAutoResume(true), sandbox0.WithSandboxServices([]apispec.SandboxAppService{ { ID: "api", Port: apispec.NewOptInt32(8080), Runtime: apispec.NewOptSandboxAppServiceRuntime(apispec.SandboxAppServiceRuntime{ Type: apispec.SandboxAppServiceRuntimeTypeCmd, Command: []string{"python3", "-m", "http.server", "8080"}, Cwd: apispec.NewOptString("/workspace"), }), Ingress: apispec.SandboxAppServiceIngress{ Public: true, Routes: []apispec.SandboxAppServiceRoute{ { ID: "api", PathPrefix: apispec.NewOptString("/api"), RewritePrefix: apispec.NewOptString("/"), Methods: []string{"GET", "POST"}, Resume: true, }, }, }, }, }), ) if err != nil { log.Fatal(err) }

Auto-Resume#

Public service auto-resume has three requirements:

  • sandbox auto_resume must be true
  • matched route resume must be true
  • the matched service must be restartable with runtime.type: cmd or runtime.type: function

All three requirements are needed for public URL requests to wake a paused sandbox and make the backing service available.

For a paused sandbox, Sandbox0 creates a new runtime pod for the same sandbox identity and restores the latest rootfs checkpoint. The sandbox ID and service public_url stay unchanged until the sandbox is deleted or hard_ttl expires. For cmd services, the command is started again in the new runtime pod. For function services, there is still no long-running listener; each request executes the handler after the sandbox runtime is available.

Notes#

  • PUT /services replaces the entire service list.
  • Public traffic must match a route on a public service.
  • If methods is empty, all methods are allowed.
  • Route auth stores SHA-256 digests, not plaintext tokens.
  • Route rate limits use the gateway rate limit backend. Self-hosted deployments default to process-local memory; configure Sandbox0Infra.spec.redis for shared Redis-backed limits across gateway replicas.

Next Steps#

Sandbox Functions

Expose public handlers without running long-lived HTTP listeners.

Contexts

Create and inspect the command processes that can back services.