Skip to content

Quickstart

This tutorial walks through a complete sandbox lifecycle using grpcurl against a local compute instance. By the end you will have created a sandbox, executed a Python command inside it, and verified that the response carries a usage_event_id.

  • grpcurl installed (brew install grpcurl or go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest)
  • A running compute server (see Operations for local bring-up)
  • The paper-board/proto repository checked out alongside compute, or access to the compiled .proto descriptors in the compute Docker image
Terminal window
grpcurl -plaintext localhost:50051 grpc.health.v1.Health/Check

Expected response:

{
"status": "SERVING"
}

A sandbox is a gVisor-backed pod + 5 GiB workspace PVC. Pass a tenant_id and session_id that identify the calling tenant and the agent session:

Terminal window
grpcurl -plaintext \
-d '{
"tenant_id": "acme-tenant-uuid",
"session_id": "session-uuid-001",
"image": "ghcr.io/paper-board/sandbox-python:v0.1.0"
}' \
localhost:50051 \
compute.v1.ComputeService/CreateSandbox

Expected response:

{
"sandbox_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"status": "SANDBOX_STATUS_STARTING",
"created_at": "2026-05-24T10:00:00Z"
}

Record the sandbox_id — you need it for the next two calls.

The sandbox status is STARTING immediately after CreateSandbox returns. The ExecCommand RPC blocks internally until the pod transitions to Running before streaming output, so you do not need to poll DescribeSandbox before calling ExecCommand.

ExecCommand is a server-streaming RPC. grpcurl prints each event as it arrives. Run a Python one-liner that also prints the sandbox hostname (confirms isolation):

Terminal window
grpcurl -plaintext \
-d '{
"sandbox_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"command": "python3",
"args": ["-c", "import socket, os; print(socket.gethostname()); print(42 * 2)"],
"working_dir": "/workspace"
}' \
localhost:50051 \
compute.v1.ComputeService/ExecCommand

Expected response (three streamed events):

{ "stdout": { "data": "c2FuZGJveC1mNDdhYzEwYgo=" }, "emitted_at": "..." }
{ "stdout": { "data": "ODQK" }, "emitted_at": "..." }
{
"completed": {
"exit_code": 0,
"wall_time": "0.412s",
"usage": {
"cpu_ms": 110,
"memory_peak_bytes": 18874368,
"disk_read_bytes": 4096,
"disk_write_bytes": 0
}
},
"emitted_at": "..."
}

stdout.data is base64-encoded. Decode to see the hostname and 84:

Terminal window
echo "c2FuZGJveC1mNDdhYzEwYgo=" | base64 -d # sandbox-f47ac10b
echo "ODQK" | base64 -d # 84

The agents service writes a usage_events row after each ExecCommand. If you are running the full stack (agents + compute), confirm the row was written:

SELECT id, event_type, cpu_ms, memory_peak_bytes, wall_time_ms
FROM agents.usage_events
WHERE session_id = 'session-uuid-001'
ORDER BY occurred_at DESC
LIMIT 1;

Expected columns:

idevent_typecpu_msmemory_peak_byteswall_time_ms
<uuid>exec_command11018874368412

This id is the usage_event_id that will be referenced by the Phase 5 billing engine.

Terminal window
# Write a result file
grpcurl -plaintext \
-d '{
"sandbox_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"path": "result.txt",
"data": "aGVsbG8gd29ybGQK"
}' \
localhost:50051 \
compute.v1.ComputeService/WriteFile
# Read it back
grpcurl -plaintext \
-d '{
"sandbox_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"path": "result.txt"
}' \
localhost:50051 \
compute.v1.ComputeService/ReadFile

WriteFile.data and ReadFile.data are base64-encoded. The example writes hello world\n (base64: aGVsbG8gd29ybGQK).

DestroySandbox deletes the sandbox pod and annotates the PVC for 24-hour soft-delete:

Terminal window
grpcurl -plaintext \
-d '{"sandbox_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"}' \
localhost:50051 \
compute.v1.ComputeService/DestroySandbox

Expected response:

{
"sandbox_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"total_usage": {
"cpu_ms": 110,
"memory_peak_bytes": 18874368
}
}

The PVC (workspace-f47ac10b) is not deleted immediately. The GC CronJob removes it after the compute.paper-board.io/delete-after annotation timestamp has passed (24 h by default).

  • Read the API Reference for full message shapes and error codes.
  • See Operations for environment variables, Helm values, and the RuntimeClass setup required in production.
  • Read the gVisor install guide for DaemonSet troubleshooting.