Devcontainers & Runtime Images
Crewship used to ship a single monolithicghcr.io/crewship-ai/agent-runtime image with every CLI, tool, and the sidecar baked in. That image has been retired (commit dd86356). Any glibc-based Linux base image now works — Crewship provisions the crew-specific tooling on top via devcontainer features and bind-mounts the sidecar from the host.
This guide explains the whole pipeline end-to-end, from setting a base image on a crew to running agents inside the cached per-crew image.
What you get
Bring Your Own Image (BYOI)
debian:bookworm-slim, ubuntu:24.04, mcr.microsoft.com/devcontainers/base:bookworm, or any other glibc Linux image. Musl/Alpine is unsupported (the Go sidecar binary is CGO-free but still glibc-linked).Community Features
installsAfter).mise Runtime Tools
mise.json block — no Dockerfile required.Per-Crew Cached Image
crewship-cache:{hash[:12]}. Re-running agents skips the install phase entirely.Mental model
Phase 1: Dockerfile + BuildKit provisioning
As of Phase 1, when the Docker CLI with BuildKit is available the provisioner no longer builds the runtime by spinning up a temp container anddocker commit-ing it. Instead it generates a Dockerfile per crew and
builds it with BuildKit, which buys:
- Per-feature layer caching — adding one feature rebuilds only that layer;
common-utilsand the previously-installed features hit the cache. - Package-manager cache mounts — apt / npm / pip caches persist across builds, so re-provisions don’t re-download the world.
- Deterministic ordering — features install in dependency order
(topological sort with
common-utilsfirst).
docker CLI (that is how BuildKit’s cache mounts
and the dockerfile frontend behave identically everywhere), and the CLI is
pinned to the daemon the container provider resolved — not to whatever
docker context happens to be current. This matters on any machine with more
than one runtime installed: colima start and a Rancher Desktop launch both
make themselves the current context without touching /var/run/docker.sock, so
an unpinned build would land the image on one daemon while the container create
looked for it on another. Your docker context is yours; it has no effect on
where Crewship builds.
Intermediate per-feature layers are tagged crewship-feat:{hash} (regenerable,
never referenced directly); the final per-crew image is still tagged
crewship-cache:{hash[:12]}. The transition is transparent: if BuildKit is
unavailable on the host, provisioning falls back to the container-commit path
shown above with no operator action required. Either way you trigger it
with the same crewship crew provision / crewship crew rebuild commands.
Prerequisites
Build the sidecar on the host
internal/config/config.go searches:{binDir}/crewship-sidecar(next to thecrewshipbinary)/usr/local/bin/crewship-sidecar
{binDir}/entrypoint.sh{cwd}/scripts/entrypoint.sh{cwd}/entrypoint.sh/usr/local/share/crewship/entrypoint.sh
CREWSHIP_SIDECAR_PATH / CREWSHIP_ENTRYPOINT_PATH.Verify fail-fast is in effect
crewship start exits with:Confirm Docker can pull images
docker pull against the base image you declare. On air-gapped hosts, pre-pull:Configuration shape
Every crew stores three optional fields in thecrews table:
Supported devcontainer.json fields
Parser lives in internal/devcontainer/config.go:
postCreateCommand accepts the polymorphic forms from the spec: a single string, an array of strings (run sequentially), or an object of named commands (keys ignored, values executed). See parsePolymorphicCommand in internal/devcontainer/config.go.mise config
agent user (UID 1001) under /opt/mise — data and shims in /opt/mise/data, the global config in /opt/mise/config/config.toml — and the runtime container carries the matching MISE_* variables (cache and state, the two mise writes to on every call, point under /home/agent, the writable volume: the container’s root filesystem is read-only). Not under /home/agent for the tools themselves: that directory is a per-crew named volume at runtime, and a volume hides whatever the image has at its mount point, so tools installed into the home inside the image were absent from the running container. /opt/mise is image-resident and survives cache rebuilds.
The agent’s PATH
The agent runs through a non-logindocker exec, which does not read /etc/profile.d. The build therefore puts the agent’s own tool directories at the front of the image PATH — /home/agent/.local/bin, /opt/crewship/bin and /opt/mise/data/shims — in the image ENV, in /etc/environment and in the runtime container environment, so every mise tool and every agent-installed CLI resolves without a login shell. At container start the runtime merges three sources into one PATH — these directories, the build’s aggregated containerEnv PATH (feature-declared directories and the image’s own, with ${containerEnv:PATH} expanded), and the login PATH captured at build time — so nothing a feature or a profile script added is lost, and a captured login PATH that does not know about mise’s shims cannot hide them. You do not need to spell these out in containerEnv.PATH; a PATH you set there is kept behind them.
Adapter CLIs are installed and verified
Provisioning also records the required agent CLIs’ observed versions and paths inside the image.crewship crew provision status <slug> shows the built
versions; JSON output separates toolchain.requested selectors from
toolchain.built evidence and its immutable image ID. An old image, an
unsupported version response, or an unavailable inspector is reported as unknown.
This describes the built image, not a running agent’s writable files or a
compatibility qualification for a particular model/account. Native Apple
Containers currently bakes the inventory but cannot read it back through this
inspector; Docker-backed installations can.
Crewship disables Claude Code’s background updater with DISABLE_AUTOUPDATER=1
and Codex’s startup update check with check_for_update_on_startup=false.
OpenCode receives OPENCODE_DISABLE_AUTOUPDATE=1. Gemini receives per-run system
settings that disable automatic updates and update notifications, including the
older inverted setting names. Failure to write those settings stops preflight.
The Gemini and OpenCode controls were checked against
Gemini v0.62.0
and OpenCode v1.18.34.
Their current automatic updater call sites are interactive; these settings also
cover interactive descendants inheriting the run environment. They do not stop
an agent with a shell from deliberately installing or running another binary.
Equivalent controls for Cursor and Droid have not been qualified.
Updates belong to environment provisioning.
An agent on CLAUDE_CODE needs claude in the image; CODEX_CLI needs codex, GEMINI_CLI gemini, OPENCODE opencode, CURSOR_CLI cursor-agent, FACTORY_DROID droid. Nobody has to remember that: every build reads the crew’s agents and adds what is missing — a mise tool for each CLI no declared feature already provides (claude, codex, gemini-cli, opencode, cursor-agent), an install command for droid. Your own choices are kept: a pinned claude version in the mise config, or the claude-code feature, is used as-is.
After the install steps the build runs command -v for every required binary as the agent user, with the agent’s PATH. A binary that did not land fails the build with its name — adapter CLI not installed in this image: claude — instead of the first chat message failing with “No such file or directory”.
The verified binaries are recorded with the image, and the dispatch gate reads them: before an agent runs — from chat, an issue, a routine, a container start — the crew’s image must be present and verified for the adapter of every live agent, or a rebuild is enqueued and the message waits for it. So an agent added by any path (the wizard, a hire, a template, onboarding, a manifest) is covered before it first runs, and a plain base image with no features still gets the build its agents need. A crew whose image was built before this verification existed rebuilds once. Creating or moving an agent onto an uncovered adapter enqueues the rebuild right away rather than at the first message.
Binaries the build installs on the agent’s behalf that mise does not carry (the Factory droid CLI) land in /opt/crewship/bin, an image-resident directory owned by the agent user and on its PATH.
Managing a crew’s runtime config
- UI (Crew Wizard)
- CLI
- REST API
- Base image dropdown (populated from
/api/v1/runtimes/catalog) - Feature picker (searchable, powered by
/api/v1/features/catalog) - mise tool picker (searchable, same
/api/v1/runtimes/catalogpayload) - A side-by-side preview of the generated
devcontainer.jsonandmise.jsonblobs
containers.dev scrape, TTL 24 h.Triggering provisioning
Provisioning is asynchronous. The CLI command returns immediately after the backend enqueues a job; you then poll for status. Five triggers can put a crew into provisioning state:- Proactive auto-provision on config save — when a crew is created (
POST /api/v1/crews,crewship apply,crewship crew create) or its devcontainer / mise / runtime-image config changes (PATCH /api/v1/crews/{crewId}), the handler immediately kicks off the build in the background (CrewHandler.maybeAutoProvision). This is the primary path: by the time anyone dispatches an issue the image is usually already cached, so operators never touch a “Build now” button. Editing the config invalidates the old cache selection and prepares an updated image. Managed Docker starts wait for current reservations and replace the container only after confirming it is idle. Unresolved occupancy leaves the update pending; an explicit stop/restart remains available after finishing existing work. - Explicit CLI/API trigger —
crewship crew provision <slug>orPOST /api/v1/crews/{crewId}/provision. - Crew rebuild —
crewship crew rebuild <slug>bypasses the provisioned-image cache and installation-layer caches. It re-runs provisioning, so floating tool selectors can resolve newer versions; explicit versions in the configuration are retained. A rejected request (already provisioning or rate limited) leaves the current cache marker intact. This builds an image; it does not itself replace an already running container. - Auto-provision on first chat (PR #230) — when an operator opens a chat with an agent in a crew that has a config but no cached image, the chat handler calls
ProvisioningHandler.EnqueueForCrewdirectly. The chat surface renders an inlineCrewProvisioningCardwith the live progress checklist; once provisioning succeeds, the agent’s first message starts on the freshly cached image. - Auto-provision on dispatch — when an issue, mission, or routine is dispatched to an agent in a crew that needs provisioning but has no usable cached image (never built, or the cache tag was pruned from the daemon), the dispatch path (
AssignmentHandler.runAssignment→ProvisioningHandler.EnsureProvisioned) blocks until the image is built, then starts the agent on it. The sameprovision.*workspace events fire, so the top-right toolbar provisioning popover lights up (“preparing container”) with no extra wiring. If the build fails or times out, the run finishes with a clear “preparing the crew container failed: …” message instead of the crypticexit 127you’d get from launching the agent on a bare base image with noclaudeCLI. This guarantee means a crew is runnable the moment it’s created — operators don’t have to remember tocrewship crew provisionfirst.
EnqueueForCrew API is idempotent: if a job is already running for the crew, the second call returns the existing job’s ID rather than starting a duplicate. Rate limiting is enforced — ErrRateLimited is returned as RFC 7807 Problem Details if a workspace is enqueueing more than the per-minute cap.
Trigger
POST /api/v1/crews/{crewId}/provision → ProvisioningHandler.ProvisionTrigger → spawns a background goroutine that calls provisioner.Provision(ctx, baseImage, cfg, miseConfig).Poll status
(baseImage, devcontainer_config, mise_config) plus the generated-build fingerprint, the crew’s required adapter CLIs and the provisioner schema version — see configHash in internal/devcontainer/provisioner_cache.go. Any change to those invalidates the cache.Re-build on demand
What Provision does, step by step
The entire pipeline is in internal/devcontainer/provisioner.go:Provision:
1. Compute config hash, check cache
1. Compute config hash, check cache
2. Skip if nothing to do
2. Skip if nothing to do
postCreateCommand, no containerEnv, and no mise tools, Provision returns CachedImage="" — the runtime launcher uses the bare RuntimeImage as-is.3. Pull base image if missing
3. Pull base image if missing
ensureImage runs ImageList then ImagePull if absent. The stream is drained to completion before proceeding — otherwise the next ContainerCreate fails with No such image.4. Create a temporary container
4. Create a temporary container
crewship-provision-{hash[:8]}-{unixnano}, entrypoint ["sleep", "infinity"], runs as root so install.sh scripts can write everywhere.5. Install devcontainer features
5. Install devcontainer features
feature: {} entry:- Resolve feature ID → OCI artifact (
ghcr.io/devcontainers/features/common-utils:2). - Fetch manifest, pull the single layer (media type
application/vnd.devcontainers.layer.v1+tar, raw tar — not gzipped). - Write the tar stream into
/tmp/devcontainer-features/{featureId}/inside the container viaCopyToContainer. - Exec
install.shwith the feature options injected as env vars (e.g.USERNAME=agent USERUID=1001 /tmp/devcontainer-features/common-utils/install.sh).
installsAfter (topological sort on feature IDs) — see features.go:SortFeatures. Legacy wild-form [{id: string}] metadata is accepted alongside the spec-compliant []string.6. Install mise tools (if declared)
6. Install mise tools (if declared)
curl -fsSL https://mise.run | sh as the agent user, then mise install for each {tool: version} entry. Fails the provisioning job if any tool download 404s or the version is invalid.7. Run postCreateCommand
7. Run postCreateCommand
agent) via docker exec, with stdout/stderr streamed to the server log. Non-zero exit aborts the whole provision.8. Clean up, commit, tag
8. Clean up, commit, tag
Running agents on the cached image
Every path that starts a crew — chat,crewship issue start, a scheduled
agent, a webhook, a routine’s agent or script step, the web terminal, the
dashboard’s start button — resolves the crew’s runtime config through the same
crew-start contract (internal/crewstart) and therefore boots the cached
image. The terminal is worth calling out: it is the surface an operator opens
specifically to inspect a crew’s environment, and it used to pass only the
crew’s id and slug, so it started debian:bookworm-slim with none of the
crew’s provisioned toolchain — gh, node and the agent CLI all missing —
while crewship crew provision reported success (#1717). If you are debugging
a crew through the terminal on an older build, check
docker inspect <container> --format '{{.Config.Image}}' before trusting what
the shell tells you about the toolchain.
Underneath, container.EnsureCrewRuntime(ctx, team, ...) in internal/provider/docker/docker.go:
-
Resolve which image to boot:
- If
team.CachedImage != ""→ use the cached image. - Else → use
team.RuntimeImageor config default (debian:bookworm-slim).
- If
-
Ensure Docker network
crewship-agentsexists (Internal: false— containers need outbound HTTPS for LLM providers). -
ContainerCreatewith:- Entrypoint forced to
/usr/local/bin/entrypoint.sh(bind-mounted). - HostConfig:
CapDrop: ALL, CapAdd: NET_RAW, no-new-privileges, ReadonlyRootfs: true.- Memory / CPU /
PidsLimit: 200. ExtraHosts: ["host.docker.internal:host-gateway"]— lets the sidecar reachcrewshipdon Linux too.Mounts:/workspace,/output,/crew(rw binds),/secrets(in-memory tmpfs — 16 MiB, mode0700, uid/gid 1001, never host-backed),/home/agent+/opt/crew-tools(named volumes), plus the two read-only bind mounts for the sidecar binary + entrypoint.
- Entrypoint forced to
-
ContainerStart. -
Sanity-check the sidecar bind mount for BYOI images:
Catches Alpine/musl base images that silently can’t run the glibc-linked sidecar.
-
From then on, agent execution uses
docker exec(neverdocker run).
Seed data: end-to-end demo
Thecrewship seed command seeds demo crews with sensible devcontainer + mise configs, then provisions all of them in parallel:
Container actuals
devcontainer.json declares what the container should look like. After agents have run a session — installing packages with apt-get, pip, or npm — the container’s actual state usually drifts from the declared intent. PR #231 closes that gap with the container.snapshot journal entry.
After every successful agent exec, internal/containerstate.Snapshot runs four short probes inside the crew container:
pip in a Node-only image, no npm in a Python-only one) yield empty lists rather than errors. The snapshot is hashed (SHA-256 over the canonical-sorted package set + os details). The orchestrator emits a container.snapshot journal entry only when the hash changes — so quiet sessions that don’t mutate the container produce no churn at all.
A typical session that adds one Python dependency emits exactly one container.snapshot entry (the new pip line), which then survives compaction the same way other observability entries do — see Crew Journal — container.snapshot for the payload schema.
Operators can use these entries to diff intent vs reality:
- “What did this agent install last week?” — query
entry_type=container.snapshoton the crew, sort byts. - “What’s drift between today’s container and the cached image?” — compare the latest snapshot against the manifest baked at provision time.
Variable expansion in mounts and env
PR #225 extends the devcontainer expansion vocabulary the spec defines:${devcontainerId}— resolves to a stable, Docker-volume-safe identifier derived from the crew ID (SHA-256 of the crew ID, first 16 hex chars). Useful for naming per-crew persistent volumes.${VAR}/${containerEnv:VAR}— resolved against the base image’s environment (everything in the image’sENVdirectives), not the host’s. So referencing${HOME}resolves to whatever the image set it to (e.g./home/agent), not the operator’s home directory. Only the curly-brace form is expanded — bare$VARis left untouched.
${devcontainerId} is expanded in mount source/target strings; ${VAR} / ${containerEnv:VAR} is expanded in containerEnv values (against the image’s default ENV). Unknown variables are left in place verbatim (e.g. ${TYPO} stays as the literal token) so an operator can spot and debug them in the rendered config — they are not treated as a hard provisioning error.
Runtime bind-mount semantics
- To roll out a new sidecar or entrypoint across existing crews, rebuild the sidecar (
make build:sidecar) and then remove each crew’s container:docker rm -f crewship-team-<slug>. The container is recreated transparently on the next agent message, picking up the updated bind mount. - Containers still serving an old sidecar are flagged on the next agent run as a
sidecar.staleseverity-error journal entry. Servers built withmake build/make build:gocarry the expected sidecar hash baked in at build time, so a deploy that ships a new server but forgets to rebuild/recopycrewship-sidecaris also flagged (plus astale sidecar ARTIFACT detectedserver log) — that variant needs the sidecar binary redeployed first; recreating containers alone remounts the same old file. postStartCommandruns on every start, so it is the right hook for operations that must reflect host state at the moment of launch (e.g. refreshing a secret viavault read). Do not try to bake those intopostCreateCommand— they’d freeze at provisioning time.- Cached images (
crewship-cache:{hash}) live on the Docker host indefinitely. Runcrewship crew cache prune --older-than 30das part of your maintenance rotation; cached images referenced by live crews are protected automatically.
Troubleshooting
docker build failed for crewship-feat:… — where is the reason?
docker build failed for crewship-feat:… — where is the reason?
RUN that echoes a build-arg or an API key shows [REDACTED].Need more than the tail? Two places have the rest:crewship crew provision status <crew>— the durableprovisioning.build_failedjournal row keeps a larger window, and it survives after the live stream is gone.- Start the server with debug logging to stream every build line as it happens.
docker CLI exited without printing anything, which almost always means it
never reached a daemon. Check crewship doctor.sidecar binary not found; run 'make build:sidecar'
sidecar binary not found; run 'make build:sidecar'
autodetectSidecarPaths in internal/config/config.go failed. Run make build:sidecar on the host, or set CREWSHIP_SIDECAR_PATH=/absolute/path/to/crewship-sidecar and CREWSHIP_ENTRYPOINT_PATH=/absolute/path/to/entrypoint.sh. For tests that never launch containers, set CREWSHIP_SKIP_SIDECAR=1.pull access denied for crewship-feat… / crewship-cache… may require 'docker login'
pull access denied for crewship-feat… / crewship-cache… may require 'docker login'
crewship-feat:* and
crewship-cache:* are local-only tags that exist on exactly one daemon.
This message means the create went looking for a locally-built image on a
daemon that never built it, so it fell back to pulling.Historically that happened when the build (docker CLI, docker context) and
the create (the provider’s probed socket) landed on different daemons — the
default state on a machine where colima start or Rancher Desktop took the
current context. The builder is now pinned to the provider’s endpoint, so
the split cannot occur.If you still see it, the image really was removed from the provider’s
daemon (a docker system prune on that daemon, most often). Re-provision:crewship doctor or
GET /api/v1/system/runtime, and list the tags there with
DOCKER_HOST=<that socket> docker images | grep crewship-.sidecar bind mount sanity check failed (exit 126)
sidecar bind mount sanity check failed (exit 126)
feature 'xyz' install.sh exits non-zero
feature 'xyz' install.sh exits non-zero
curl, git, ca-certificates). debian:bookworm-slim is intentionally minimal. Either:- Switch to
mcr.microsoft.com/devcontainers/base:bookworm(pre-installed), or - Declare
ghcr.io/devcontainers/features/common-utils:2as the first feature — it installs prerequisites.
base/runtime image freshness unknown; keeping the local copy without pulling
base/runtime image freshness unknown; keeping the local copy without pulling
HEAD) and compares it against the local image’s RepoDigests. If that lookup fails, the pull is skipped and the local copy is used as-is — the right call when you’re offline, but it means a permanently failing lookup pins you to a stale image indefinitely. That case now logs at WARN:- No route to the registry (air-gapped host, egress firewall). Pre-pull images on the host and ignore the warning.
-
A broken or wedged Docker credential helper.
~/.docker/config.jsonnames acredsStore, and Docker execsdocker-credential-<store>for every registry. If that binary is missing, hangs, or prompts for an unlocked keychain, the digest lookup times out (5s). Verify with:Digest lookups for registries that need no credentials — loopback/localhostregistries — skip the helper entirely, and a helper that errors falls back to anonymous access rather than giving up, so public images keep resolving.
cache image never refreshes
cache image never refreshes
configHash is deterministic over (baseImage, devcontainer_config, mise_config) plus the generated-build fingerprint, required adapter CLIs and the provisioner schema version. Upstream feature releases that re-use the same tag don’t change the hash. Force a rebuild:tmux: not found (noisy log on startup)
tmux: not found (noisy log on startup)
44da863: setupTmuxExec now pre-checks command -v tmux and falls back to stdbuf -oL <cmd> when absent. debian:bookworm-slim doesn’t ship tmux — this is expected.claude: not found inside agent container
claude: not found inside agent container
ghcr.io/devcontainers-extra/features/claude-code:2 feature (replaces our former Go EnsureClaudeCode helper). Make sure it’s in the crew’s devcontainer_config.features. Seed crews include it automatically.Migrating from the legacy agent-runtime image
If you have a long-running deployment that still references ghcr.io/crewship-ai/agent-runtime:latest:
Update Crewship to the current release
dd86356.Build the sidecar on the host
Reconfigure each existing crew
Restart crewshipd
agent-runtime.yml workflow deleted). Pulling its old tag still works but sits frozen; nothing in Crewship itself assumes it anymore.
Reference
- Configuration → Devcontainers — config fields + environment variables reference.
- Configuration → Environment Variables —
CREWSHIP_SIDECAR_PATH,CREWSHIP_ENTRYPOINT_PATH,CREWSHIP_SKIP_SIDECAR. - Security → Container Isolation — UID boundaries, capability drops, network policy.
- Code of record:
internal/devcontainer/,internal/provider/docker/docker.go,cmd/crewship/seeddata/crews.go,cmd/crewship/cmd_crew_config.go,cmd/crewship/cmd_crew_provision.go.
Built environment history
crewship crew provision revisions <crew> lists the latest 100 immutable build
records for a crew. The API is GET /api/v1/crews/{crewId}/provision/revisions.
Each record identifies the definition hash, build hash, immutable image ID,
observed CLI inventory and build time. Records are scoped to the crew’s
workspace; raw configuration and environment variable values are not copied
into history. Reusing the same image for the same definition reuses its record.
Offline qualification is included in that immutable build evidence; it is not
a current provider-authentication claim.
By default, offline AI CLI checks record evidence without preventing publication.
To require them for a crew, set spec.mise.ai_cli_check: required in its Crew
manifest, or "ai_cli_check":"required" alongside tools in the managed
mise_config JSON. record explicitly selects the compatibility default;
other values are rejected. This is Crewship metadata and is not emitted into
native mise TOML.
The crew creation wizard preserves this policy when importing Crew YAML, even
when the mise section has no explicit tools. A nonempty typed ai_cli_check
overrides the value in spec.mise.raw; unsupported typed values are rejected.
A required check accepts only successful evidence bound to the built immutable
image, with a passing startup/version probe for every recorded and required
agent CLI. Missing inventory, an unavailable runtime, mismatched evidence or
failed probes fail the provisioning job before a new revision/current image is
published. The previous image and requirements stay selected. Native providers
without the necessary inspection and offline sandbox capability cannot satisfy
this opt-in requirement. These checks do not prove account/model compatibility,
real provider authentication or safe automatic activation.
Workspace and crew backups include these records; restoring them preserves
provenance and does not restore the referenced image or activate an instance.
A cache hit whose feature requirements cannot be resolved fails provisioning
and leaves the previous selection intact. A successful empty build clears the
previous artifact’s runtime requirements and feature evidence.
A completed build verifies that the crew definition and required agent adapters
still match the inputs it built. If they changed, Crewship refuses to publish the stale result and retries the
current definition within the same job, retaining pending messages. Retries share
the original 30-minute budget and stop after eight superseded attempts; ongoing
edits or a deleted crew produce a terminal failure without replacing the previous
selection.
The check and publication are one database transaction.
This history is provenance metadata. It neither preserves Docker images from
pruning nor changes a running container, and it does not provide rollback or
RAM snapshots. Builds without recorded immutable image identity do not invent
a revision. These records will support a later explicit retention and safe
activation policy.
Immutable Docker build references
Docker provisioning records a full local image ID incached_image. A shared
crewship-cache:* tag is only a cache lookup alias; moving it during a later
build does not change what a previously provisioned crew selected. A failed
explicit rebuild leaves the previous reference and requirements intact. Builds
still in progress do not replace the running container.
The build cache key changed once for this transition: new builds use new aliases
so they do not move tags retained by older crew records. Existing records are not
rewritten. Native Apple builds still return tags because that provider does not
yet expose equivalent image inspection through this interface.
The provisioning API’s cache inventory, deletion and orphan sweeper recognize
crew references by image ID. The separate opt-in
idle cache eviction policy also keeps
images selected by live crews in this installation, including legacy tag aliases. Revision history does not retain image bytes or guarantee rollback. External
docker image prune, forced admin deletion and disk loss also remain outside
this protection. A bare provider start requesting a different image returns
image update pending while the current container is running, paused or its
state is unknown. Managed starts reserve the runtime through preparation and
execution; an image change waits for existing reservations, then verifies
installation ownership and controller/runtime occupancy before stopping the
old instance. A freshly inspected stopped container can be removed without
force and recreated. Selecting the same immutable artifact by ID does not
require a restart even if the container was created using a tag. A runtime
confirmed to be in restart backoff retains the separate crash-recovery path.
The orphan sweeper also recognizes published image IDs recorded in revision
history after a rebuild removes their last tag. With cache auto-deletion enabled,
the existing age floor and live-crew reference checks apply to those artifacts
too; removal remains non-force. Unrecognized images without tags are left alone.
An unpublished build, or an artifact whose history was removed with its crew,
cannot be identified this way. Revision history is not a promise to retain an
old image for rollback.
Legacy agent runs inspect their actual container at admission and retain its
immutable source image ID as runtime_image_id in run state and exec.command
events. The lookup has a two-second timeout; unavailable evidence is omitted,
never inferred from the crew’s currently selected image. This identifies the
container’s source, not subsequent filesystem changes, the executed CLI’s
version, or a unique environment definition revision. Restricted attempts have
their own frozen-input evidence; this field does not change their contract.
Automatic activation requires no local holds, local or unresolved recovered
invocations, external exposures, or additional workload processes. A bounded
kernel process census recognizes the standard sleep/init entrypoint and the
trusted sidecar UID. Missing state, incomplete observations and custom resident
process trees keep the update pending. Idle expiry uses the same reservation
exclusion. Terminal sessions retain the existing runtime rather than selecting
a pending image, and prevent its replacement while connected.
This is controller-local coordination for managed runtime users, not a guarantee
against an operator concurrently using Docker directly or replacing the daemon.
Providers without managed admission do not gain automatic activation. Explicit
operator restart remains disruptive; finish existing work before using it to
resolve a pending update. No scheduled updater or crash-safe output replay is
implied by this activation path.
Supplying a native mise lock
The managed build can consume a complete native mise lock bundle. Generatemise.lock with mise for the container’s target platform, then run:
mise.lock and .mise/locks/, rejects symlinks,
limits file sizes, and does not contact Crewship or change the environment. Keep
auxiliary files: npm-backed tools may use an aube lock beside mise.lock.
The bundle supports up to 128 UTF-8 files, 128 KiB per file and 512 KiB total.
Put the resulting JSON object in spec.mise.lock in a kind: Crew manifest,
alongside spec.mise.tools. JSON configuration stored in mise_config accepts
that same lock object. Manifest export/import and the visual tools editor
preserve it. Raw TOML input still supports the existing [tools]/[env] subset;
the lock bundle is a Crewship JSON/YAML field, not a new TOML section.
With a bundle, builds install using mise install --locked --force --yes.
An absent version/platform, invalid lock or checksum mismatch fails the build;
it does not fall back to a floating install. Force reinstalls tools so an
already-installed version in the base image cannot bypass this check. Added AI
adapters still need matching entries in the supplied lock. Changing a selector
in the visual editor preserves the lock and may therefore require regenerating
it before the next build. Remove the lock explicitly to return to ordinary
unlocked installation.
Mise owns resolution and backend semantics. A native lock without an upstream
checksum does not gain one from Crewship, and support varies by backend. The
bundle does not lock the base image, OS packages, features, build hooks or the
mise executable itself. It is dependency input, separate from observed CLI
versions and from qualification of real model/auth combinations. This slice
adds no scheduled updates; runtime activation is governed separately above.
Offline CLI qualification
On Docker, provisioning also checks the recorded CLI binaries in a disposable instance pinned to the built image ID. The instance runs as UID 1001 with no external network, no persistent mounts, a read-only root filesystem and bounded CPU, memory, process count, output and execution time. Its temporary home is removed after the check. Crewship supplies no provider credentials. The qualification requests a 90-second lifetime; the initial backend accepts at most two minutes and uses a separate UID 1002 for its lifetime keeper. This temporary profile disables inherited image healthchecks and is not a durable agent-run backend.toolchain.built.qualification reports the image ID, per-tool result and overall
status. passed means that the recorded binaries started and reported the same
versions in this offline profile. It does not verify provider login, model
compatibility, streaming, or isolation of ordinary crew runs. A failed or
unavailable check is reported without replacing an active environment or
silently relaxing the qualification profile. Older images and unsupported
backends can report unavailable. The initial runtime port refuses persistent
mounts and RAM suspension explicitly.
Resolving or updating a lock with Crewship
crewship crew provision lock-resolve mise.json --image sha256:<local-image-id>
resolves a mise JSON/TOML tool configuration in a disposable Docker builder.
Supply a trusted, already-present Linux image containing /usr/local/bin/mise,
/usr/bin/env, /bin/sleep and /bin/tar. No image is pulled automatically.
The resolver selects linux-x64 or linux-arm64 from that image’s architecture;
it does not infer the target from the CLI host’s operating system.
The JSON result contains lock, resolver_image_id, platform, mise_version
and tools with native previous/resolved version lists and version_changed.
selectors_sha256 binds the requested tool selectors; previous_lock_sha256
(when supplied) and lock_sha256 cover every file in the complete bundle.
lock_changed includes source, checksum, auxiliary-file and formatting changes,
so it can be true even when no tool version changes. These hashes identify the
proposal’s inputs and output; they are not signed approval or authorization.
Apply its lock through spec.mise.lock and the ordinary build
path. For an update, include the previous lock alongside tools in the input
JSON and pass --bump. Mise updates within each selector; an exact version
remains exact. The result is a proposal, with no running-crew or server changes.
This is trusted build work with Docker bridge networking for registries. It
uses a read-only root, no persistent or host mounts, UID 1001 for resolution,
dropped capabilities, bounded memory/CPU/processes, a private 256 MiB temporary
filesystem and a five-minute operation deadline. A separate UID-1002 keeper
exits after six minutes if the client disappears; stopped-container cleanup
still depends on the client or provisioning cleanup. The image, its tool
backends and the Docker host remain trusted. This is not the restricted agent
network policy or hostile-image execution.
Configuration environment values are excluded; private registry credentials
and host mise configuration are not inherited. Input is limited to 4 MiB;
native bundles retain the limits above, and resolver diagnostics are bounded.
Resolution does not install the resulting toolchain, prove a real provider
login, schedule an update or activate a revision.
The required check also compares explicit exact mise CLI selectors with the
observed version. An overlapping devcontainer feature does not hide an explicit
mise pin. Floating selectors remain the responsibility of native locked install;
this check does not implement another dependency resolver.
Python tools need a prepared resolver and builder
The resolver image must contain the native backend’s prerequisites as well as mise. For Python package graphs, prepare a compatible Python interpreter anduv in that trusted image; the build image must also provide the interpreter
mise selects. Crewship’s resolver does not install these prerequisites or
silently relax a locked installation. See the upstream
PyPI backend requirements.
In a live Ansible check, an image without uv produced only a top-level version
record; the subsequent mise install --locked correctly rejected its missing
Python graph. Adding uv without a sufficiently recent interpreter also failed.
Treat resolution as a proposal, then verify installation and the actual tool
before selecting the resulting environment. The Ansible distribution version
and the ansible-core version printed by ansible --version are different
identities.
An explicit --bump can refresh dependency metadata even when an exact
package version stays unchanged. Review lock_changed and the complete bundle,
not only the top-level version diff. A normal resolution without --bump
reuses the existing native lock; no update scheduler is introduced here.
Applying a reviewed native lock proposal
crewship crew provision lock-apply mise.json proposal.json prints updated mise
JSON only if the current selectors and previous complete lock match the plan,
and the proposed lock still matches its digest. Redirect to a new output
file, then review and apply that configuration through your ordinary manifest
or crew API workflow. The command is local and offline; it does not install
software, start a build or replace a running environment.
Environment and policy fields come from the current configuration, not from
the resolver. Unknown JSON fields are preserved. A changed selector or old
lock requires a new plan. Hashes prevent accidentally applying a stale or
corrupted proposal; they do not authenticate its author or independently prove
native resolution. The normal locked build remains responsible for checking
that lock against the selectors. Provisioning can also require the separate
image-bound offline AI CLI checks described above.