Skip to main content

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

MethodEndpointPurpose
GET/api/v1/features/catalogList installable devcontainer features
GET/api/v1/runtimes/catalogList mise-managed runtimes
GET/api/v1/crews/{crewId}/provisionGet crew provision status
POST/api/v1/crews/{crewId}/provisionTrigger a provision
POST/api/v1/crews/{crewId}/rebuildClear the cache marker and rebuild
POST/api/v1/crews/{crewId}/restart-agentsRecreate the runtime container without rebuilding
GET/api/v1/cache/imagesList 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"
    }
  ]
}
FieldDescription
refOCI reference — what you put in devcontainer_config.features.
categoryOne of languages, tools, cloud, databases.
iconLucide icon name for UI rendering.
size_hintApproximate 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-onlycrewship 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"]
    }
  ]
}
FieldDescription
toolMise tool ID — what you put in mise.json.
categorySame set as the feature catalog: languages, tools, cloud, databases.
versionsAvailable versions, ordered newest-first. May be empty when mise resolves dynamically.
default_versionWhat Crewship picks when the crew config does not pin one.
backendsMise 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:
ValueMeaning
idleNo active job and no cached_image — crew has nothing built or in flight.
pendingJob enqueued, not yet picked up.
runningProvisioner is mid-pipeline.
completedCached image exists at cached_image (no active job).
failedJob ended in failure. error carries the reason.
FieldWhen presentMeaning
devcontainer_configalwaysRaw JSON blob pulled from crews.devcontainer_config (or null).
cached_imagealwaysLast successfully committed cache tag, or null.
config_hashalwaysSHA-256 of (base, devcontainer, mise) used as the cache key.
agents_pending_restartalwaysCount of crew agents whose live container is on a stale image.
step / totalactive jobNumeric progress (e.g. 3 / 6). The UI’s CrewProvisioningCard renders a checklist from this pair.
messageactive jobFree-form short message for the current step.
stepsactive jobOrdered list of step labels for the whole pipeline.
log_tailactive jobTrailing log lines (bounded) for inline display.
started_atactive jobRFC3339 start time.
completed_atfinished jobRFC3339 end time (only after completed/failed).
errorfailed jobHuman-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):
StatusCondition
400Invalid crew ID, or crew has no devcontainer_config.
403Caller lacks create permission.
404Crew not in workspace.
409A 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.
429Per-workspace rate limit exceeded.
500Unexpected server-side error (panic-recovered).
503Provisioner 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).
StatusCondition
400Empty crew ID.
403Caller lacks update permission.
404Crew not in workspace.
500DB error or Docker ContainerRemove failure.
503Docker 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

GET /api/v1/cache/images
Response: 200 OK
{
  "images": [
    {
      "tag": "crewship-cache:a1b2c3d4e5f6",
      "size": 824191022,
      "created_at": 1714415721,
      "referenced_by": ["backend"]
    }
  ]
}
FieldTypeDescription
tagstringThe crewship-cache:* image tag.
sizeintegerImage size in bytes.
created_atintegerImage creation time as Unix seconds (from the Docker image summary), not an RFC3339 string.
referenced_bystring[]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:
StatusCondition
400Empty tag, or tag outside the crewship-cache: namespace ({ "error": "only crewship-cache:* tags may be deleted" }).
403Caller is below the ADMIN role.
409Image still referenced by a live crew and ?force=true not set.
500Docker ImageRemove failed.
503Docker 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.