Coppice still supports the blocking POST /templates OCI
import path, but hosted-builder parity needs a non-blocking shape.
POST /templates/build enqueues the same
coppice-import-oci.sh pipeline, returns immediately with a
job id, and exposes live stdout/stderr through pollable logs plus an
SSE stream.
API
Start a build:
POST /templates/build
{
"name": "fbsd14-min",
"from": "oci:quay.io/dougrabson/freebsd14-minimal"
}
Response:
202 ACCEPTED
{
"jobID": "8b5c...",
"name": "fbsd14-min",
"from": "oci:quay.io/dougrabson/freebsd14-minimal",
"status": "queued",
"createdAt": "2026-04-25T...",
"logsURL": "/templates/builds/8b5c.../logs",
"eventsURL": "/templates/builds/8b5c.../events",
"templateURL": "/templates/fbsd14-min"
}
Inspect jobs:
GET /templates/builds
GET /templates/builds/:job_id
GET /templates/builds/:job_id/logs?limit=200&since=2026-04-25T12:00:00Z
GET /templates/builds/:job_id/events
The log envelope matches the sandbox log shape:
{
"logs": [
{
"ts": "2026-04-25T12:00:01Z",
"source": "template-build",
"stream": "stderr",
"text": "buildah pull quay.io/..."
}
],
"truncated": false
}
The SSE endpoint emits event: log records containing one
JSON-encoded log line, then a terminal event: done record
containing the final job object. Final statuses are
succeeded or failed; success includes
snapshot and best-effort sizeBytes.
Semantics
The async path deliberately reuses the blocking import runner:
buildah pull, buildah mount,
rsync into zroot/jails/<name>-template,
zfs snapshot @base, then a registry reload. That keeps
template correctness identical between POST /templates and
POST /templates/build.
Conflict handling is conservative:
- If the template already exists, create returns
409. - If another non-terminal build for the same template name exists, create
returns
409. - If the script fails, the job is retained as
failedwith the script error and retained logs for diagnosis.
The registry is in-memory today. That is sufficient for live progress and does not affect the durable artifact: the template dataset and snapshot still live in ZFS. A future build-history pass can persist the job envelope if we want records across gateway restarts.
Receipt
e2b-compat/src/routes.rswiresPOST /templates/build,GET /templates/builds,GET /templates/builds/:job_id,GET /templates/builds/:job_id/logs, andGET /templates/builds/:job_id/events.e2b-compat/src/state.rs::TemplateBuildJobowns job state and the retainedLogBuffer.POST /templatesandPOST /templates/buildboth call the same import runner, so the synchronous path remains compatibility-preserving while the async path closes the hosted-builder gap.