jackin'
Role Authoring

The Construct Image

What's inside the shared base image that every agent starts from

This page has two readers. Role authors (user-facing) read the top half — what is in the construct, what tools come for free, how to extend it from a role's Dockerfile. Contributors (internals-facing) read the lower half — how to build, validate, and publish the construct image itself (mise run construct-build-local, CI workflow, advanced publish rehearsal). Operators using jackin' as a product do not need to read either half; jackin load pulls and updates the construct automatically.

What is the construct?

projectjackin/construct:trixie is the shared base Docker image for every agent — the foundation layer that every role extends. It provides system tools, shell environment, and container infrastructure, kept in one image so every agent inherits the same baseline.

Every agent Dockerfile starts from the construct:

FROM projectjackin/construct:0.4-trixie

What's inside

The construct is built on Debian Trixie and includes:

System tools

PackagePurpose
bashDefault shell for scripts
zsh + Oh My ZshDefault interactive shell for the agent user. Oh My Zsh runs with ZSH_THEME="" so the git plugin, autosuggestions plugin, and OSC 0/2 + OSC 7 auto-title hooks are active without overriding the Starship prompt.
fishOpt-in alternative shell. Pre-configured ~/.config/fish/config.fish initialises Starship and emits the same OSC 0/2 + OSC 7 pane title escapes as zsh, so pane border titles render identically. Enter with fish from a default zsh pane, or set as the login shell with sudo chsh -s /usr/bin/fish agent.
git + Git LFSSource control with large file support
curlHTTP client
jq + yqJSON and YAML processors
openssh-clientSSH for git operations
sudoPrivilege escalation (passwordless for agent user)
treeDirectory visualization

Search tools

PackagePurpose
ripgrep (rg)Fast regex search
fd-find (fd)Fast file finder
fzfFuzzy finder for interactive selection

Development infrastructure

PackagePurpose
misePolyglot language version manager
Docker CLI + ComposeContainer operations via DinD
GitHub CLI (gh)Repository, PR, and issue operations
Starship promptInformative terminal prompt

User environment

The construct creates an agent user with:

  • Home directory at /home/agent
  • Zsh as the default shell, with fish available as an opt-in alternative (fish from zsh, or sudo chsh -s /usr/bin/fish agent to change the login shell)
  • Passwordless sudo access
  • Oh My Zsh sourced with ZSH_THEME="" so the git plugin, autosuggestions plugin, and OSC 0/2 + OSC 7 title hooks are active without competing with Starship for the prompt
  • Starship prompt configured for both zsh and fish so the prompt surface stays the same when switching shells
  • Pane border title (user@host:cwd) emitted via OSC 0/2 on every prompt — the jackin-capsule multiplexer reads it and renders it as the pane title (the same mechanism zellij uses)
  • mise shims in $PATH for both shells

How it's built

The construct source starts at docker/construct/Dockerfile in the jackin' repository. The declarative build definition lives in docker-bake.hcl at the repo root, and the supported command surface is the construct-* mise tasks defined in mise.toml, backed by the crates/jackin-xtask/src/construct.rs Rust task runner.

docker/construct/versions.env remains the source of truth for the pinned tirith, shellfirm, and MISE_VERSION build args used by docker/construct/Dockerfile. mise is installed with the official standalone installer during the Docker build, which downloads the pinned version's release asset from GitHub — where every version stays available, so the pin never goes stale. The pinned version gives Buildx a stable cache key, and Renovate bumps invalidate the mise install layer when upstream publishes a new release.

Build workflow

Use the construct-* mise tasks for day-to-day construct work. Docker Bake is the underlying build engine, but contributors should treat mise run as the supported command surface.

Local validation

Before opening a pull request for construct changes, validate the image locally:

mise run construct-init-buildx
mise run construct-build-local

By default, local validation loads the image into your Docker daemon as jackin-local/construct:trixie, so it does not silently replace the canonical projectjackin/construct:trixie base image that normal jackin workflows consume.

To test jackin load against your local construct build, set JACKIN_CONSTRUCT_IMAGE after building:

mise run construct-init-buildx
mise run construct-build-local
export JACKIN_CONSTRUCT_IMAGE="jackin-local/construct:trixie"

With JACKIN_CONSTRUCT_IMAGE set, jackin' validates role Dockerfiles against the canonical image name as usual (role repos reference a versioned construct tag such as projectjackin/construct:0.4-trixie), but substitutes your local image at derived-build time so docker build uses jackin-local/construct:trixie as the actual base.

Use mise run construct-inspect to print the fully resolved Bake config without building the image.

If builder state gets stale or confusing, run mise run construct-doctor-buildx to inspect it or mise run construct-reset-buildx to recreate it. Set BUILDX_BUILDER before running the task if you want to isolate this workflow under a different local builder name.

Platform-specific debugging

To force a specific target architecture during local debugging:

mise run construct-build-platform amd64
mise run construct-build-platform arm64

These commands work when your buildx builder supports the target platform natively or through emulation.

Advanced publish rehearsal

If you need to rehearse the release path against a temporary registry, point REGISTRY_IMAGE at your own namespace instead of the canonical projectjackin/construct repository:

REGISTRY_IMAGE=ttl.sh/jackin-construct-$USER mise run construct-push-platform amd64
REGISTRY_IMAGE=ttl.sh/jackin-construct-$USER mise run construct-push-platform arm64
REGISTRY_IMAGE=ttl.sh/jackin-construct-$USER mise run construct-publish-manifest

mise run construct-push-platform and mise run construct-publish-manifest intentionally refuse to publish to projectjackin/construct unless they are running in CI.

CI behavior

GitHub Actions reuses the same mise run commands on native ubuntu-24.04 and ubuntu-24.04-arm runners. Construct CI validates broad build plumbing when changes touch docker/construct/**, docker-bake.hcl, the crates/jackin-xtask/ task runner, or .github/workflows/construct.yml. It only treats a run as a construct-image publish candidate when changes touch inputs baked into the image itself: docker-bake.hcl, docker/construct/Dockerfile, docker/construct/versions.env, docker/construct/zshrc, or docker/construct/fish-config.fish.

Pull requests, plus manual workflow runs against non-main branches, build both architectures natively without pushing images. Those validation jobs use the GitHub Actions cache backend; the cache key includes the pinned MISE_VERSION, so a Renovate mise bump invalidates the mise install layer while unrelated earlier layers can still be reused. They still do not receive Docker Hub credentials.

Pushes to main, plus manual workflow runs against main, publish each platform by digest first, then assemble the final multi-platform manifest from those digests when the run is a construct-image publish candidate. Build-plumbing-only changes still validate the construct workflow, but they skip the immutable-version guard and registry publish path because no baked image input changed. The registry cache uses the same pinned MISE_VERSION keying behavior as PR builds. That keeps the public tag surface limited to the canonical release tags instead of leaving permanent public -amd64 and -arm64 tags behind.

Published images also carry explicit BuildKit provenance and SBOM attestations.

The published tags are:

  • projectjackin/construct:trixie — the stable tag
  • projectjackin/construct:trixie-<sha> — commit-specific tag

The derived image layer

When you load an agent, jackin' doesn't use the construct directly. It generates a derived Dockerfile that adds:

  1. User remapping — adjusts the agent user's UID/GID to match your host user
  2. Agent installation — copies jackin's cached agent binaries for every agent declared in the role manifest
  3. Plugin installation — any Claude plugins declared in the role manifest
  4. Runtime entrypoint + setup bridge — the script delegates deterministic setup to jackin-capsule runtime-setup, then keeps the shell-native work: sourcing role hooks and exec-ing the selected agent. The Rust setup path handles one-time container git/GitHub initialization, the shared git trailer hook, durable agent-home seeding, auth handoff refreshes, and Claude MCP registration. It wires up GitHub if gh is already authenticated; it does not perform gh auth login, so an unauthenticated gh is left alone with a notice.

Every load re-runs docker build on the derived Dockerfile. Docker's layer cache makes most subsequent loads fast, but there is no separate "skip build if image exists" shortcut. Before the build, jackin' resolves the latest agent binary metadata and stores downloaded binaries under its host cache. Passing --rebuild invalidates agent installation layers; unchanged versions are copied from the local cache instead of downloaded again.

Extending the construct

Role repos add their tools on top of the construct. The construct provides the foundation — agents provide the specialization:

┌─────────────────────────────────┐
│  Derived Layer (jackin-managed) │  Agent CLIs, entrypoint, user mapping
├─────────────────────────────────┤
│  Agent Layer (your Dockerfile)  │  Rust, Node, Python, custom tools
├─────────────────────────────────┤
│  Construct (shared base)        │  Debian, git, Docker CLI, mise, zsh
└─────────────────────────────────┘

This layered approach means:

  • Agent authors focus on their tools, not infrastructure
  • The construct can be updated independently (security patches, new tools)
  • Docker layer caching makes builds fast

On this page