Provisioning
Endpoints that drive the Devcontainers pipeline: discover available features and runtimes, trigger or rebuild a crew’s cached image, and inspect the local cache.
Implementation lives in internal/api/crew_provisioning_jobs.go and internal/api/crew_provisioning_cache.go; the underlying provisioner is in internal/devcontainer/.
All routes require authentication and are workspace-scoped where they touch crew rows.
Endpoints
| Method | Endpoint | Purpose |
|---|
| GET | /api/v1/features/catalog | List installable devcontainer features |
| GET | /api/v1/runtimes/catalog | List mise-managed runtimes |
| GET | /api/v1/crews/{crewId}/provision | Get crew provision status |
| POST | /api/v1/crews/{crewId}/provision | Trigger a provision |
| POST | /api/v1/crews/{crewId}/rebuild | Clear the cache marker and rebuild |
| POST | /api/v1/crews/{crewId}/restart-agents | Recreate the runtime container without rebuilding |
| GET | /api/v1/cache/images | List cached images |
| DELETE | /api/v1/cache/images/{tag} | Delete a cached image |
Catalogs
Discover the features and runtimes a crew can pin in its devcontainer_config.
Feature catalog
GET /api/v1/features/catalog?search=…
Lists devcontainer features Crewship can install. The dynamic fetcher pulls from upstream registries (ghcr.io/devcontainers/features/*, ghcr.io/devcontainers-extra/features/*, ghcr.io/crewship-ai/features/*); when that fails, the handler falls back to internal/devcontainer.FallbackCatalog so the UI is never empty.
?search= filters by name, description, or category (server-side substring).
Response: 200 OK
{
"features": [
{
"ref": "ghcr.io/devcontainers/features/python:1",
"name": "Python",
"description": "Installs the specified Python version, pip, and pipx. Supports virtual environments.",
"category": "languages",
"icon": "code",
"size_hint": "~80 MB"
}
]
}
| Field | Description |
|---|
ref | OCI reference — what you put in devcontainer_config.features. |
category | One of languages, tools, cloud, databases. |
icon | Lucide icon name for UI rendering. |
size_hint | Approximate installed-size delta (~80 MB). |
Unknown publishers (anything outside the three allowlisted prefixes) are rejected at provisioning time even if they appear in a custom catalog — see internal/devcontainer/features.go.
Runtime catalog
GET /api/v1/runtimes/catalog?search=…
Lists the mise-managed runtimes (Node, Python, Go, Terraform, kubectl, …) the workspace can pin in devcontainer_config.mise. Falls back to FallbackRuntimeCatalog when the dynamic fetcher fails.
Not a base-image catalog. The base-image list is CLI-only — crewship features base-images reads baseImagesCatalog directly from cmd/crewship/cmd_features.go; there is no HTTP endpoint for it today.
Response: 200 OK
{
"runtimes": [
{
"name": "Node.js",
"tool": "node",
"description": "JavaScript runtime.",
"category": "languages",
"icon": "hexagon",
"versions": ["22", "20", "18"],
"default_version": "22",
"backends": ["asdf"]
}
]
}
| Field | Description |
|---|
tool | Mise tool ID — what you put in mise.json. |
category | Same set as the feature catalog: languages, tools, cloud, databases. |
versions | Available versions, ordered newest-first. May be empty when mise resolves dynamically. |
default_version | What Crewship picks when the crew config does not pin one. |
backends | Mise install backends (asdf, cargo, npm, pipx, github). Empty → mise default. |
Crew provisioning lifecycle
Inspect, trigger, rebuild, and recycle a crew’s cached image and runtime container.
Provision status
GET /api/v1/crews/{crewId}/provision
Response: 200 OK. Always-present fields come from the crews row; the step / total / message / steps / log_tail / started_at / completed_at / error fields appear only when an in-memory job is currently running or recently finished for this crew.
{
"status": "running",
"devcontainer_config": "{\"image\":\"…\",\"features\":{…}}",
"cached_image": null,
"config_hash": "a1b2c3d4e5f6…",
"agents_pending_restart": 0,
"step": 3,
"total": 6,
"message": "installing devcontainer features",
"steps": ["validate", "pull-base", "install-features", "install-mise", "snapshot", "tag"],
"log_tail": ["Feature python installed", "Feature node installed"],
"started_at": "2026-04-30T11:01:18Z"
}
status is one of:
| Value | Meaning |
|---|
idle | No active job and no cached_image — crew has nothing built or in flight. |
pending | Job enqueued, not yet picked up. |
running | Provisioner is mid-pipeline. |
completed | Cached image exists at cached_image (no active job). |
failed | Job ended in failure. error carries the reason. |
| Field | When present | Meaning |
|---|
devcontainer_config | always | Raw JSON blob pulled from crews.devcontainer_config (or null). |
cached_image | always | Last successfully committed cache tag, or null. |
config_hash | always | SHA-256 of (base, devcontainer, mise) used as the cache key. |
agents_pending_restart | always | Count of crew agents whose live container is on a stale image. |
step / total | active job | Numeric progress (e.g. 3 / 6). The UI’s CrewProvisioningCard renders a checklist from this pair. |
message | active job | Free-form short message for the current step. |
steps | active job | Ordered list of step labels for the whole pipeline. |
log_tail | active job | Trailing log lines (bounded) for inline display. |
started_at | active job | RFC3339 start time. |
completed_at | finished job | RFC3339 end time (only after completed/failed). |
error | failed job | Human-readable failure reason. |
Trigger provision
POST /api/v1/crews/{crewId}/provision
Requires the create role action (OWNER, ADMIN, MANAGER). Returns 202 immediately and runs the pipeline in a background goroutine; poll the status endpoint for progress.
Response: 202 Accepted
{
"status": "started",
"message": "Provisioning started. Monitor with 'crewship crew provision status <slug>'."
}
Errors (RFC 7807 Problem Details on every 4xx/5xx — type, title, status, detail, instance):
| Status | Condition |
|---|
| 400 | Invalid crew ID, or crew has no devcontainer_config. |
| 403 | Caller lacks create permission. |
| 404 | Crew not in workspace. |
| 409 | A provisioning job is already in progress for this crew. The 409 body extends Problem Details with job_status (the existing job’s state) so callers can decide whether to wait or fall through. Not idempotent — the second caller is told to back off, not silently joined to the running job. |
| 429 | Per-workspace rate limit exceeded. |
| 500 | Unexpected server-side error (panic-recovered). |
| 503 | Provisioner not configured (no Docker client wired). |
Rebuild
POST /api/v1/crews/{crewId}/rebuild
Clears the cached image marker on the crew row and triggers a fresh provision. Use when upstream features publish breaking updates under the same tag.
Discards the existing cache marker before rebuilding — the next agent run waits on a full pipeline pass.
Response: 202 Accepted — same shape as Trigger.
Restart agents
POST /api/v1/crews/{crewId}/restart-agents
Force-removes the crew’s runtime container so the next agent exec recreates it from the current cached_image — without rebuilding the image. Used when an env-var change should take effect without the cost of a new image. Idempotent: when no container is running it still returns 200 OK with { "restarted": 0 }.
Requires the update role action (OWNER, ADMIN, MANAGER).
Response: 200 OK with { "restarted": <count> } — the count is the number of non-deleted agents in the crew (they share one container, so all are recreated together).
| Status | Condition |
|---|
| 400 | Empty crew ID. |
| 403 | Caller lacks update permission. |
| 404 | Crew not in workspace. |
| 500 | DB error or Docker ContainerRemove failure. |
| 503 | Docker client not configured. |
Cache image registry
Inspect and prune the locally built crewship-cache:* images. This registry is per-host, not per-workspace — see Tenancy.
List cached images
Response: 200 OK
{
"images": [
{
"tag": "crewship-cache:a1b2c3d4e5f6",
"size": 824191022,
"created_at": 1714415721,
"referenced_by": ["backend"]
}
]
}
| Field | Type | Description |
|---|
tag | string | The crewship-cache:* image tag. |
size | integer | Image size in bytes. |
created_at | integer | Image creation time as Unix seconds (from the Docker image summary), not an RFC3339 string. |
referenced_by | string[] | Slugs of live crews referencing this tag (workspace-scoped). |
Requires the read role action (any workspace member). Returns 503 ({ "error": "cache management not available (Docker client not configured)" }) when no Docker client is wired, and 500 on a Docker ImageList failure.
Images orphaned from any crew (i.e. referenced_by is empty) are eligible for the GC sweeper. See CREWSHIP_CACHE_GC_AUTODELETE.
Delete cached image
DELETE /api/v1/cache/images/{tag}
Removes a cached image. By default refuses to delete an image still referenced by a live crew (returns 409 Conflict). Pass ?force=true to delete anyway.
?force=true removes an image even while live crews reference it — their next agent run rebuilds from scratch.
Requires the delete role action (OWNER, ADMIN only — a MANAGER is rejected with 403).
Response: 200 OK
{ "tag": "crewship-cache:a1b2c3d4e5f6", "status": "deleted" }
A 409 Conflict body lists the referencing crew slugs under referenced_by. The reference check is host-global — it counts crews in any workspace, so an image another tenant still uses cannot be force-free-deleted by accident:
{
"error": "image is referenced by live crews; pass ?force=true to delete anyway",
"referenced_by": ["backend"]
}
Errors:
| Status | Condition |
|---|
| 400 | Empty tag, or tag outside the crewship-cache: namespace ({ "error": "only crewship-cache:* tags may be deleted" }). |
| 403 | Caller is below the ADMIN role. |
| 409 | Image still referenced by a live crew and ?force=true not set. |
| 500 | Docker ImageRemove failed. |
| 503 | Docker client not configured ({ "error": "cache management not available (Docker client not configured)" }). |
Tenancy
crewId on the provision-status / trigger / rebuild / restart-agents routes is validated against the session workspace via the crews.workspace_id SELECT; a crew in another workspace returns 404.
- The cache image registry is per-host, not per-workspace — operators sharing a Docker daemon will see each other’s tags, and the cache routes do not workspace-scope the
{tag} path param. GET /cache/images scopes only the referenced_by annotation to the caller’s workspace; DELETE /cache/images/{tag} deliberately checks references across all workspaces so a tag another tenant depends on cannot be deleted without ?force=true. There is no cross-tenant 404 on a {tag} — the tag namespace is shared by design.