SANDBOX/Sandbox Functions

#Sandbox Functions

Sandbox Functions expose public HTTP handlers inside a sandbox without running a long-lived HTTP server. They are configured as function-backed Sandbox Services, so each function uses the same public URL, route policy, auth, CORS, rate limit, timeout, path rewrite, and auto-resume model.

Use functions for:

  • public webhook receivers
  • lightweight control endpoints
  • HTTP handlers that should resume a paused sandbox on demand
  • handlers that return normal HTTP responses, SSE streams, or WebSocket messages

Use Case: Warm Repository Webhooks#

One common use case is keeping a coding-agent repository Volume warm. A GitHub push webhook calls a Sandbox Function, the handler refreshes the base workspace inside the sandbox, publishes a snapshot, and later agent tasks fork from that fresh snapshot instead of cloning from scratch. For the full workflow, see Sandbox Functions: Keep Coding Agent Repositories Warm.

Runtime Model#

A function service has runtime.type: function. It stores inline source code in the service config. On each matching public request, cluster-gateway applies route policy, forwards an execution request to procd, and returns the function handler response.

Function services are different from listener-backed services:

  • omit port in the request
  • do not start or expose a user HTTP listener
  • use the returned public_url as the only public entrypoint
  • execute the handler once per matching request

Python is the first supported function runtime. The sandbox image must include python3 in PATH and use Python 3.9 or later for the supported function feature set.

Sandbox0 injects python-runner into each sandbox pod, but the runner executes python3 from the sandbox template image. Custom templates and custom images therefore own the Python runtime used by Sandbox Functions. Older Python versions are not supported; async handlers, streaming responses, and WebSocket handlers depend on Python's modern asyncio runtime.

Function Config#

yaml
services: - id: webhook runtime: type: function function: runtime: python handler: handler max_concurrency: 1 source: type: inline code: | def handler(request): return { "status": 200, "headers": {"content-type": "application/json"}, "body": { "service_id": request["service_id"], "route_id": request["route_id"], "method": request["method"], "path": request["path"], }, } ingress: public: true routes: - id: webhook path_prefix: /webhook methods: [POST] timeout_seconds: 30 resume: true

Apply it with the CLI:

bash
s0 sandbox service update --services-file function-services.yaml -s sb_abc123

Or with the API:

json
{ "services": [ { "id": "webhook", "runtime": { "type": "function", "function": { "runtime": "python", "handler": "handler", "max_concurrency": 1, "source": { "type": "inline", "code": "def handler(request):\n return {\n \"status\": 200,\n \"headers\": {\"content-type\": \"application/json\"},\n \"body\": {\n \"service_id\": request[\"service_id\"],\n \"route_id\": request[\"route_id\"],\n \"method\": request[\"method\"],\n \"path\": request[\"path\"],\n },\n }\n" } } }, "ingress": { "public": true, "routes": [ { "id": "webhook", "path_prefix": "/webhook", "methods": ["POST"], "timeout_seconds": 30, "resume": true } ] } } ] }

Set max_concurrency when a function must serialize access to shared sandbox state, such as refreshing a repository volume or publishing snapshots. The limit is enforced inside procd for one function service in one sandbox runtime. Omit it or set it to 0 for unlimited execution. When the limit is reached, Sandbox0 rejects new HTTP and SSE invocations with 429 Too Many Requests; WebSocket invocations are closed by the function runtime.

bash
curl -X PUT "$SANDBOX0_API_URL/api/v1/sandboxes/$SANDBOX_ID/services" \ -H "authorization: Bearer $SANDBOX0_API_TOKEN" \ -H "x-team-id: $SANDBOX0_TEAM_ID" \ -H "content-type: application/json" \ --data-binary @function-services.json

The service response includes a public_url:

json
{ "success": true, "data": { "sandbox_id": "rs-example", "exposure_domain": "aws-us-east-1.sandbox0.app", "services": [ { "id": "webhook", "port": 49983, "public_url": "https://rs-example--p49983.aws-us-east-1.sandbox0.app" } ] } }

The response includes port: 49983 because Sandbox0 normalizes the service for routing. Do not send port when configuring a function service, and do not depend on hand-built URLs. Use the public_url returned by GET or PUT /api/v1/sandboxes/{'{id}'}/services.

Call A Function#

Use the returned public_url and append a path that matches the route.

bash
curl -X POST "$PUBLIC_URL/webhook/github" \ -H "content-type: application/json" \ -d '{"event":"ping"}'

With the config above, the function handler receives:

json
{ "service_id": "webhook", "route_id": "webhook", "method": "POST", "path": "/webhook/github", "body_base64": "eyJldmVudCI6InBpbmcifQ==" }

The function response is the HTTP response body and status. The function does not return a URL.

Function Request Object#

FieldTypeDescription
service_idstringMatched service ID
route_idstringMatched route ID
methodstringHTTP method
pathstringRequest path after any rewrite_prefix
raw_querystringRaw query string
headersobjectRequest headers
body_base64stringBase64-encoded request body

Function Return Value#

Handlers may return any JSON-serializable value, or a response object.

python
def handler(request): return { "status": 201, "headers": {"content-type": "text/plain"}, "body": "created", }

For SSE, clients send Accept: text/event-stream. Return an iterable or async iterable body.

python
def handler(request): def events(): yield "event: ready\n" yield "data: ok\n\n" return { "status": 200, "headers": {"content-type": "text/event-stream"}, "body": events(), }

For WebSocket requests, define an async handler that accepts (request, ws).

python
async def handler(request, ws): async for message in ws: await ws.send("echo:" + message)

Set Functions At Claim Time#

You can attach function services when claiming a sandbox. Function services omit port in claim config just as they do in PUT /services.

yaml
template: default config: auto_resume: true services: - id: webhook runtime: type: function function: runtime: python handler: handler max_concurrency: 1 source: type: inline code: | def handler(request): return {"status": 200, "body": "ok"} ingress: public: true routes: - id: webhook path_prefix: /webhook methods: [POST] resume: true
go
functionSource := "def handler(request):\n return {\"status\": 200, \"body\": \"ok\"}\n" sandbox, err := client.ClaimSandbox(ctx, "default", sandbox0.WithSandboxAutoResume(true), sandbox0.WithSandboxServices([]apispec.SandboxAppService{ { ID: "webhook", Runtime: apispec.NewOptSandboxAppServiceRuntime(apispec.SandboxAppServiceRuntime{ Type: apispec.SandboxAppServiceRuntimeTypeFunction, Function: apispec.NewOptSandboxFunction(apispec.SandboxFunction{ Runtime: apispec.SandboxFunctionRuntimePython, Handler: apispec.NewOptString("handler"), MaxConcurrency: apispec.NewOptInt32(1), Source: apispec.SandboxFunctionSource{ Type: apispec.SandboxFunctionSourceTypeInline, Code: functionSource, }, }), }), Ingress: apispec.SandboxAppServiceIngress{ Public: true, Routes: []apispec.SandboxAppServiceRoute{ { ID: "webhook", PathPrefix: apispec.NewOptString("/webhook"), Methods: []string{"POST"}, Resume: true, }, }, }, }, }), ) if err != nil { log.Fatal(err) }

Auto-Resume#

Public function auto-resume has two gates:

  • sandbox auto_resume must be true
  • matched route resume must be true

Both gates are required for public URL requests to wake a paused sandbox. Sandbox0 creates a new runtime pod for the same sandbox identity, restores the latest rootfs checkpoint, and then executes the handler. The sandbox ID and function public_url stay unchanged until the sandbox is deleted or hard_ttl expires.

Notes#

  • PUT /services replaces the entire service list. Include every listener-backed service and function that should remain active.
  • Public traffic must match a route on a public function service.
  • If methods is empty, all methods are allowed.
  • Route auth, CORS, rate limits, timeouts, path rewrite, and resume behavior are applied before the handler runs.
  • Function max_concurrency limits simultaneous handler executions for one function service inside one sandbox runtime. It does not coordinate across different sandboxes or different function services.

Next Steps#

Webhooks

Receive signed sandbox lifecycle, service, and file-related events.

Contexts

Run and inspect command contexts that can prepare state for function handlers.